Adds long-lived API-key (ksk_) authentication for Kiro/AWS CodeWhisperer and a direct claude:kiro / kiro:claude translation route that avoids the lossy OpenAI two-hop pivot. - translator: claude-to-kiro request + kiro-to-claude response translators, registered on the exact source:target pair (direct route ahead of the OpenAI pivot in index.js). claude-to-kiro uses shared schema constants (ROLE/CLAUDE_BLOCK/DEFAULT_IMAGE_MIME) per app convention. - auth: POST /api/oauth/kiro/api-key imports + validates a key via ListAvailableProfiles, persists authMethod="api_key" (no refresh token). - executor: send tokentype: API_KEY header and try *.amazonaws.com hosts first for api-key creds; OAuth keeps kiro.dev first. - fix: never inject the default placeholder profileArn for api-key auth (CodeWhisperer 403s an ARN not owned by the key's account). - ui: API Key method in the Kiro connect modal; surface api-key accounts on the Quota Tracker and provider count. - stream: env-overridable TTFT vs stall timeouts + Kiro keepalive frame. - tests: claude-kiro-direct + kiro-profile-arn (11 tests). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
128 lines
4.6 KiB
JavaScript
128 lines
4.6 KiB
JavaScript
import { NextResponse } from "next/server";
|
|
import { getProviderConnections } from "@/lib/localDb";
|
|
import { backfillCodexEmails } from "@/lib/oauth/providers";
|
|
import { USAGE_APIKEY_PROVIDERS, USAGE_SUPPORTED_PROVIDERS } from "@/shared/constants/providers";
|
|
|
|
const SAFE_FIELDS = [
|
|
"id", "provider", "authType", "name", "email", "displayName",
|
|
"priority", "globalPriority", "isActive", "defaultModel",
|
|
"testStatus", "lastError", "lastErrorAt", "errorCode",
|
|
"expiresAt", "lastUsedAt", "consecutiveUseCount",
|
|
"createdAt", "updatedAt",
|
|
];
|
|
|
|
const SAFE_PSD_FIELDS = [
|
|
"baseUrl", "azureEndpoint", "deployment", "apiVersion", "accountId",
|
|
"region", "projectId", "resourceUrl", "proxyPoolId",
|
|
"connectionProxyEnabled", "connectionProxyUrl", "connectionNoProxy",
|
|
"githubLogin", "githubName", "githubEmail", "githubUserId",
|
|
"username", "firstName", "lastName", "authMethod", "authKind",
|
|
"profileArn",
|
|
];
|
|
|
|
const DEFAULT_PAGE_SIZE = 20;
|
|
const MAX_PAGE_SIZE = 500;
|
|
|
|
function maskName(name) {
|
|
if (typeof name !== "string" || name.length <= 16) return name;
|
|
if (/[a-zA-Z0-9_-]{32,}/.test(name)) return `${name.slice(0, 8)}***`;
|
|
return name;
|
|
}
|
|
|
|
function sanitize(c) {
|
|
const safe = {};
|
|
for (const f of SAFE_FIELDS) if (c[f] !== undefined) safe[f] = c[f];
|
|
if (safe.name) safe.name = maskName(safe.name);
|
|
if (c.providerSpecificData) {
|
|
const psd = {};
|
|
for (const f of SAFE_PSD_FIELDS) {
|
|
if (c.providerSpecificData[f] !== undefined) psd[f] = c.providerSpecificData[f];
|
|
}
|
|
safe.providerSpecificData = psd;
|
|
}
|
|
return safe;
|
|
}
|
|
|
|
function isUsageEligible(connection) {
|
|
return USAGE_SUPPORTED_PROVIDERS.includes(connection.provider) && (
|
|
connection.authType === "oauth" || USAGE_APIKEY_PROVIDERS.includes(connection.provider)
|
|
);
|
|
}
|
|
|
|
function parsePositiveInt(value, fallback) {
|
|
const parsed = Number.parseInt(value, 10);
|
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
}
|
|
|
|
function sortConnections(connections, sort) {
|
|
const list = [...connections];
|
|
|
|
if (sort === "provider") {
|
|
return list.sort((a, b) => {
|
|
const orderA = USAGE_SUPPORTED_PROVIDERS.indexOf(a.provider);
|
|
const orderB = USAGE_SUPPORTED_PROVIDERS.indexOf(b.provider);
|
|
if (orderA !== orderB) return orderA - orderB;
|
|
return a.provider.localeCompare(b.provider);
|
|
});
|
|
}
|
|
|
|
return list.sort((a, b) => {
|
|
const priorityA = a.priority ?? Number.MAX_SAFE_INTEGER;
|
|
const priorityB = b.priority ?? Number.MAX_SAFE_INTEGER;
|
|
if (priorityA !== priorityB) return priorityA - priorityB;
|
|
return (a.provider || "").localeCompare(b.provider || "");
|
|
});
|
|
}
|
|
|
|
export async function GET(request) {
|
|
try {
|
|
await backfillCodexEmails();
|
|
|
|
const { searchParams } = new URL(request.url);
|
|
const provider = searchParams.get("provider") || "all";
|
|
const accountStatus = searchParams.get("accountStatus") || "all";
|
|
const sort = searchParams.get("sort") || "priority";
|
|
const page = parsePositiveInt(searchParams.get("page"), 1);
|
|
const pageSize = Math.min(parsePositiveInt(searchParams.get("pageSize"), DEFAULT_PAGE_SIZE), MAX_PAGE_SIZE);
|
|
|
|
const allConnections = await getProviderConnections();
|
|
const eligibleConnections = allConnections.filter(isUsageEligible);
|
|
const providerOptions = Array.from(new Set(eligibleConnections.map((conn) => conn.provider))).sort();
|
|
|
|
const providerFilteredConnections = eligibleConnections.filter((conn) => (
|
|
provider === "all" || conn.provider === provider
|
|
));
|
|
|
|
const accountFilteredConnections = providerFilteredConnections.filter((conn) => {
|
|
if (accountStatus === "active") return conn.isActive ?? true;
|
|
if (accountStatus === "inactive") return !(conn.isActive ?? true);
|
|
return true;
|
|
});
|
|
|
|
const sortedConnections = sortConnections(accountFilteredConnections, sort);
|
|
const total = sortedConnections.length;
|
|
const totalPages = Math.max(1, Math.ceil(total / pageSize));
|
|
const currentPage = Math.min(page, totalPages);
|
|
const offset = (currentPage - 1) * pageSize;
|
|
const pageConnections = sortedConnections.slice(offset, offset + pageSize).map(sanitize);
|
|
|
|
return NextResponse.json({
|
|
connections: pageConnections,
|
|
providerOptions,
|
|
pagination: {
|
|
page: currentPage,
|
|
pageSize,
|
|
total,
|
|
totalPages,
|
|
},
|
|
totals: {
|
|
eligibleConnections: eligibleConnections.length,
|
|
providerFilteredConnections: providerFilteredConnections.length,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
console.log("Error fetching providers for client:", error);
|
|
return NextResponse.json({ error: "Failed to fetch providers" }, { status: 500 });
|
|
}
|
|
}
|