@gajae-code/ai 0.15.6 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/dist/types/adapter-internals/aws-region.d.ts +7 -0
- package/dist/types/core.d.ts +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/providers/anthropic.d.ts +1 -1
- package/dist/types/providers/google-gemini-headers.d.ts +1 -1
- package/dist/types/providers/openai-codex-responses.d.ts +6 -0
- package/dist/types/utils/schema/normalize.d.ts +0 -5
- package/dist/types/utils/sqlite-errors.d.ts +4 -0
- package/package.json +3 -3
- package/src/adapter-internals/aws-region.d.ts +7 -0
- package/src/adapter-internals/aws-region.ts +14 -0
- package/src/auth-storage.ts +14 -14
- package/src/core.ts +1 -0
- package/src/index.ts +1 -0
- package/src/model-thinking.ts +8 -0
- package/src/models.json +201 -3
- package/src/provider-models/openai-compat.ts +61 -0
- package/src/providers/amazon-bedrock.ts +5 -1
- package/src/providers/anthropic.d.ts +1 -1
- package/src/providers/anthropic.ts +1 -1
- package/src/providers/aws-credentials.ts +6 -0
- package/src/providers/google-gemini-headers.d.ts +1 -1
- package/src/providers/google-gemini-headers.ts +1 -1
- package/src/providers/kiro-api-key.ts +33 -8
- package/src/providers/kiro-codewhisperer.ts +4 -1
- package/src/providers/openai-codex-responses.d.ts +6 -0
- package/src/providers/openai-codex-responses.ts +17 -2
- package/src/providers/pi-native-client.ts +24 -1
- package/src/utils/oauth/kiro.ts +91 -22
- package/src/utils/schema/dereference.ts +169 -49
- package/src/utils/schema/draft.ts +46 -23
- package/src/utils/schema/normalize.d.ts +0 -5
- package/src/utils/schema/normalize.ts +396 -119
- package/src/utils/schema/types.ts +3 -1
- package/src/utils/schema/zod-decontaminate.ts +83 -29
- package/src/utils/sqlite-errors.d.ts +4 -0
- package/src/utils/sqlite-errors.ts +13 -0
- package/src/utils/tool-choice-capability.ts +2 -3
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import * as fs from "node:fs";
|
|
23
23
|
import * as path from "node:path";
|
|
24
24
|
import { $env, getTrustedHomeDir, isEnoent, logger } from "@gajae-code/utils";
|
|
25
|
+
import { assertAwsRegionLabel } from "../adapter-internals/aws-region";
|
|
25
26
|
import {
|
|
26
27
|
type AwsIniFile,
|
|
27
28
|
classifyAwsProfileCapability,
|
|
@@ -155,6 +156,7 @@ async function readSsoCredentials(
|
|
|
155
156
|
}
|
|
156
157
|
}
|
|
157
158
|
if (!startUrl || !ssoRegion) return undefined;
|
|
159
|
+
assertAwsRegionLabel(ssoRegion);
|
|
158
160
|
|
|
159
161
|
const token = await loadSsoCachedToken(startUrl, sessionName);
|
|
160
162
|
if (!token?.accessToken) {
|
|
@@ -172,6 +174,7 @@ async function readSsoCredentials(
|
|
|
172
174
|
const response = await fetch(url, {
|
|
173
175
|
method: "GET",
|
|
174
176
|
headers: { "x-amz-sso_bearer_token": token.accessToken },
|
|
177
|
+
redirect: "error",
|
|
175
178
|
signal,
|
|
176
179
|
});
|
|
177
180
|
if (!response.ok) {
|
|
@@ -411,6 +414,7 @@ async function readImdsCredentials(parentSignal: AbortSignal | undefined): Promi
|
|
|
411
414
|
const tokenRes = await fetch(`http://${IMDS_HOST}/latest/api/token`, {
|
|
412
415
|
method: "PUT",
|
|
413
416
|
headers: { "x-aws-ec2-metadata-token-ttl-seconds": "21600" },
|
|
417
|
+
redirect: "error",
|
|
414
418
|
signal,
|
|
415
419
|
});
|
|
416
420
|
if (!tokenRes.ok) return undefined;
|
|
@@ -418,6 +422,7 @@ async function readImdsCredentials(parentSignal: AbortSignal | undefined): Promi
|
|
|
418
422
|
|
|
419
423
|
const roleRes = await fetch(`http://${IMDS_HOST}/latest/meta-data/iam/security-credentials/`, {
|
|
420
424
|
headers: { "x-aws-ec2-metadata-token": token },
|
|
425
|
+
redirect: "error",
|
|
421
426
|
signal,
|
|
422
427
|
});
|
|
423
428
|
if (!roleRes.ok) return undefined;
|
|
@@ -428,6 +433,7 @@ async function readImdsCredentials(parentSignal: AbortSignal | undefined): Promi
|
|
|
428
433
|
`http://${IMDS_HOST}/latest/meta-data/iam/security-credentials/${encodeURIComponent(role)}`,
|
|
429
434
|
{
|
|
430
435
|
headers: { "x-aws-ec2-metadata-token": token },
|
|
436
|
+
redirect: "error",
|
|
431
437
|
signal,
|
|
432
438
|
},
|
|
433
439
|
);
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export declare const GEMINI_CLI_VERSION_ENV = "GJC_AI_GEMINI_CLI_VERSION";
|
|
7
7
|
export declare const LEGACY_GEMINI_CLI_VERSION_ENV = "PI_AI_GEMINI_CLI_VERSION";
|
|
8
|
-
export declare const DEFAULT_GEMINI_CLI_VERSION = "0.
|
|
8
|
+
export declare const DEFAULT_GEMINI_CLI_VERSION = "0.58.0";
|
|
9
9
|
export declare function getGeminiCliUserAgent(modelId?: string): string;
|
|
10
10
|
export declare const getGeminiCliHeaders: (modelId?: string) => {
|
|
11
11
|
"User-Agent": string;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export const GEMINI_CLI_VERSION_ENV = "GJC_AI_GEMINI_CLI_VERSION";
|
|
7
7
|
export const LEGACY_GEMINI_CLI_VERSION_ENV = "PI_AI_GEMINI_CLI_VERSION";
|
|
8
|
-
export const DEFAULT_GEMINI_CLI_VERSION = "0.
|
|
8
|
+
export const DEFAULT_GEMINI_CLI_VERSION = "0.58.0";
|
|
9
9
|
|
|
10
10
|
export function getGeminiCliUserAgent(modelId = "gemini-3.1-pro-preview"): string {
|
|
11
11
|
const version =
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* AWS SSO OIDC / CodeWhisperer streaming path used by `gjc auth-broker login kiro`.
|
|
7
7
|
*/
|
|
8
8
|
import { $env } from "@gajae-code/utils";
|
|
9
|
+
import { assertAwsRegionLabel } from "../adapter-internals/aws-region";
|
|
9
10
|
import { Effort } from "../model-thinking";
|
|
10
11
|
import type {
|
|
11
12
|
Api,
|
|
@@ -53,19 +54,40 @@ export function isKiroApiKey(value: string | undefined): value is string {
|
|
|
53
54
|
|
|
54
55
|
export function kiroApiRegion(options?: { region?: string }): string {
|
|
55
56
|
return (
|
|
56
|
-
options?.region
|
|
57
|
-
$env.KIRO_API_REGION
|
|
58
|
-
$env.KIRO_REGION
|
|
59
|
-
$env.AWS_REGION
|
|
60
|
-
$env.AWS_DEFAULT_REGION
|
|
57
|
+
options?.region ??
|
|
58
|
+
$env.KIRO_API_REGION ??
|
|
59
|
+
$env.KIRO_REGION ??
|
|
60
|
+
$env.AWS_REGION ??
|
|
61
|
+
$env.AWS_DEFAULT_REGION ??
|
|
61
62
|
DEFAULT_REGION
|
|
62
63
|
);
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
export function kiroApiBaseUrl(region: string): string {
|
|
67
|
+
assertAwsRegionLabel(region);
|
|
66
68
|
return `https://q.${region}.amazonaws.com/`;
|
|
67
69
|
}
|
|
68
70
|
|
|
71
|
+
function isRegionDerivedKiroApiBaseUrl(baseUrl: string): boolean {
|
|
72
|
+
try {
|
|
73
|
+
const url = new URL(baseUrl);
|
|
74
|
+
const match = /^q\.([a-z0-9-]+)\.amazonaws\.com$/.exec(url.hostname);
|
|
75
|
+
if (!match) return false;
|
|
76
|
+
assertAwsRegionLabel(match[1]);
|
|
77
|
+
return (
|
|
78
|
+
url.protocol === "https:" &&
|
|
79
|
+
url.username === "" &&
|
|
80
|
+
url.password === "" &&
|
|
81
|
+
url.port === "" &&
|
|
82
|
+
url.pathname === "/" &&
|
|
83
|
+
url.search === "" &&
|
|
84
|
+
url.hash === ""
|
|
85
|
+
);
|
|
86
|
+
} catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
69
91
|
export function toKiroModelId(modelId: string): string {
|
|
70
92
|
return modelId.replace(/(\d)-(\d)/g, "$1.$2");
|
|
71
93
|
}
|
|
@@ -279,12 +301,13 @@ export async function fetchKiroApiModels(
|
|
|
279
301
|
apiKey: string,
|
|
280
302
|
region?: string,
|
|
281
303
|
): Promise<Model<"kiro-codewhisperer-stream">[]> {
|
|
282
|
-
const resolvedRegion = region
|
|
304
|
+
const resolvedRegion = region ?? kiroApiRegion();
|
|
283
305
|
const baseUrl = kiroApiBaseUrl(resolvedRegion);
|
|
284
306
|
const response = await fetch(baseUrl, {
|
|
285
307
|
method: "POST",
|
|
286
308
|
headers: kiroApiHeaders(apiKey, LIST_TARGET),
|
|
287
309
|
body: JSON.stringify({ origin: KIRO_ORIGIN }),
|
|
310
|
+
redirect: "error",
|
|
288
311
|
signal: AbortSignal.timeout(15_000),
|
|
289
312
|
});
|
|
290
313
|
if (!response.ok) {
|
|
@@ -585,8 +608,9 @@ export const streamKiroApiKey: StreamFunction<"kiro-codewhisperer-stream"> = (
|
|
|
585
608
|
"Kiro API key missing. Set KIRO_API_KEY to a ksk_ key from https://app.kiro.dev/settings/api-keys.",
|
|
586
609
|
);
|
|
587
610
|
}
|
|
588
|
-
const
|
|
589
|
-
const
|
|
611
|
+
const configuredBaseUrl = model.baseUrl;
|
|
612
|
+
const usesExplicitBaseUrl = Boolean(configuredBaseUrl) && !isRegionDerivedKiroApiBaseUrl(configuredBaseUrl);
|
|
613
|
+
const endpoint = configuredBaseUrl || kiroApiBaseUrl(kiroApiRegion(options));
|
|
590
614
|
const request = buildApiKeyRequest(model, context, options);
|
|
591
615
|
options?.onPayload?.(request, model, options?.attemptScope);
|
|
592
616
|
|
|
@@ -594,6 +618,7 @@ export const streamKiroApiKey: StreamFunction<"kiro-codewhisperer-stream"> = (
|
|
|
594
618
|
method: "POST",
|
|
595
619
|
headers: { ...kiroApiHeaders(apiKey, CHAT_TARGET), ...(options.headers ?? {}) },
|
|
596
620
|
body: JSON.stringify(request),
|
|
621
|
+
...(usesExplicitBaseUrl ? {} : { redirect: "error" as const }),
|
|
597
622
|
signal: options.signal,
|
|
598
623
|
});
|
|
599
624
|
if (!response.ok) {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* not from any AGPL reference implementation.
|
|
12
12
|
*/
|
|
13
13
|
import { $credentialEnv, $env, extractHttpStatusFromError } from "@gajae-code/utils";
|
|
14
|
+
import { assertAwsRegionLabel } from "../adapter-internals/aws-region";
|
|
14
15
|
import type { Effort } from "../model-thinking";
|
|
15
16
|
import type {
|
|
16
17
|
Api,
|
|
@@ -178,9 +179,10 @@ export const streamKiroCodeWhisperer: StreamFunction<"kiro-codewhisperer-stream"
|
|
|
178
179
|
};
|
|
179
180
|
|
|
180
181
|
const blocks = output.content as Block[];
|
|
181
|
-
const region = options.region
|
|
182
|
+
const region = options.region ?? $env.KIRO_REGION ?? $env.AWS_REGION ?? $env.AWS_DEFAULT_REGION ?? DEFAULT_REGION;
|
|
182
183
|
|
|
183
184
|
try {
|
|
185
|
+
assertAwsRegionLabel(region);
|
|
184
186
|
// Resolve bearer token
|
|
185
187
|
const bearerToken = resolveBearerToken(options.apiKey);
|
|
186
188
|
if (!bearerToken) {
|
|
@@ -222,6 +224,7 @@ export const streamKiroCodeWhisperer: StreamFunction<"kiro-codewhisperer-stream"
|
|
|
222
224
|
method: "POST",
|
|
223
225
|
headers: requestHeaders,
|
|
224
226
|
body,
|
|
227
|
+
redirect: "error",
|
|
225
228
|
signal: options.signal,
|
|
226
229
|
});
|
|
227
230
|
|
|
@@ -25,6 +25,12 @@ export declare function normalizeCodexToolChoice(choice: ToolChoice | undefined,
|
|
|
25
25
|
export declare function formatCodexUserAgent(platform: string, release: string, arch: string): string;
|
|
26
26
|
export declare const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses">;
|
|
27
27
|
export declare function prewarmOpenAICodexResponses(model: Model<"openai-codex-responses">, options?: Pick<OpenAICodexResponsesOptions, "apiKey" | "headers" | "sessionId" | "signal" | "preferWebsockets" | "providerSessionState">): Promise<void>;
|
|
28
|
+
/**
|
|
29
|
+
* Bun 1.4.0's Windows WebSocket client can segfault during TLS handshakes.
|
|
30
|
+
* Keep the model's websocket preference available on other platforms, while
|
|
31
|
+
* requiring an explicit opt-in on Windows until the bundled runtime is fixed.
|
|
32
|
+
*/
|
|
33
|
+
export declare function isCodexWebSocketSafeByDefault(platform?: NodeJS.Platform): boolean;
|
|
28
34
|
export interface OpenAICodexTransportDetails {
|
|
29
35
|
websocketPreferred: boolean;
|
|
30
36
|
lastTransport?: CodexTransport;
|
|
@@ -2307,6 +2307,15 @@ function recordCodexWebSocketFailure(state: CodexWebSocketSessionState, activate
|
|
|
2307
2307
|
}
|
|
2308
2308
|
}
|
|
2309
2309
|
|
|
2310
|
+
/**
|
|
2311
|
+
* Bun 1.4.0's Windows WebSocket client can segfault during TLS handshakes.
|
|
2312
|
+
* Keep the model's websocket preference available on other platforms, while
|
|
2313
|
+
* requiring an explicit opt-in on Windows until the bundled runtime is fixed.
|
|
2314
|
+
*/
|
|
2315
|
+
export function isCodexWebSocketSafeByDefault(platform: NodeJS.Platform = process.platform): boolean {
|
|
2316
|
+
return platform !== "win32";
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2310
2319
|
function shouldUseCodexWebSocket(
|
|
2311
2320
|
model: Model<"openai-codex-responses">,
|
|
2312
2321
|
state: CodexWebSocketSessionState | undefined,
|
|
@@ -2314,7 +2323,11 @@ function shouldUseCodexWebSocket(
|
|
|
2314
2323
|
): boolean {
|
|
2315
2324
|
if (!state || state.disableWebsocket) return false;
|
|
2316
2325
|
if (preferWebsockets === false) return false;
|
|
2317
|
-
return
|
|
2326
|
+
return (
|
|
2327
|
+
isCodexWebSocketEnvEnabled() ||
|
|
2328
|
+
preferWebsockets === true ||
|
|
2329
|
+
(isCodexWebSocketSafeByDefault() && model.preferWebsockets === true)
|
|
2330
|
+
);
|
|
2318
2331
|
}
|
|
2319
2332
|
|
|
2320
2333
|
export interface OpenAICodexTransportDetails {
|
|
@@ -2372,7 +2385,9 @@ export function getOpenAICodexTransportDetails(
|
|
|
2372
2385
|
const websocketPreferred =
|
|
2373
2386
|
options?.preferWebsockets === false
|
|
2374
2387
|
? false
|
|
2375
|
-
: isCodexWebSocketEnvEnabled() ||
|
|
2388
|
+
: isCodexWebSocketEnvEnabled() ||
|
|
2389
|
+
options?.preferWebsockets === true ||
|
|
2390
|
+
(isCodexWebSocketSafeByDefault() && model.preferWebsockets === true);
|
|
2376
2391
|
const state = getCodexWebSocketStateForPublicSession(model, options);
|
|
2377
2392
|
|
|
2378
2393
|
return {
|
|
@@ -49,6 +49,29 @@ const NON_WIRE_KEYS = new Set<keyof SimpleStreamOptions>([
|
|
|
49
49
|
"fallbackAttempt",
|
|
50
50
|
]);
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Project the caller's {@link Context} onto the wire schema. Runtime tool
|
|
54
|
+
* objects routinely carry harness-only state (runners, session managers,
|
|
55
|
+
* fs-stat BigInts) that must never be serialized: BigInt fields make
|
|
56
|
+
* `JSON.stringify` throw outright, and the rest is dead weight the gateway
|
|
57
|
+
* re-derives from its own tool registry. Only the protocol-meaningful,
|
|
58
|
+
* JSON-safe `Tool` fields cross the wire.
|
|
59
|
+
*/
|
|
60
|
+
function buildWireContext(context: Context): Context {
|
|
61
|
+
if (!context.tools || context.tools.length === 0) return context;
|
|
62
|
+
return {
|
|
63
|
+
...context,
|
|
64
|
+
tools: context.tools.map(tool => ({
|
|
65
|
+
name: tool.name,
|
|
66
|
+
description: tool.description,
|
|
67
|
+
parameters: tool.parameters,
|
|
68
|
+
...(tool.strict !== undefined ? { strict: tool.strict } : {}),
|
|
69
|
+
...(tool.customFormat !== undefined ? { customFormat: tool.customFormat } : {}),
|
|
70
|
+
...(tool.customWireName !== undefined ? { customWireName: tool.customWireName } : {}),
|
|
71
|
+
})),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
52
75
|
function buildWireOptions(options: SimpleStreamOptions | undefined): Record<string, unknown> {
|
|
53
76
|
if (!options) return {};
|
|
54
77
|
const wire: Record<string, unknown> = {};
|
|
@@ -166,7 +189,7 @@ export function streamPiNative<TApi extends Api>(
|
|
|
166
189
|
const headers = buildHeaders(model as Model<Api>, options?.apiKey);
|
|
167
190
|
const body = JSON.stringify({
|
|
168
191
|
modelId: model.id,
|
|
169
|
-
context,
|
|
192
|
+
context: buildWireContext(context),
|
|
170
193
|
options: buildWireOptions(options),
|
|
171
194
|
stream: true,
|
|
172
195
|
});
|
package/src/utils/oauth/kiro.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* and AWS public documentation, not from any third-party reference.
|
|
9
9
|
*/
|
|
10
10
|
import { scheduler } from "node:timers/promises";
|
|
11
|
+
import { assertAwsRegionLabel } from "../../adapter-internals/aws-region";
|
|
11
12
|
import type { OAuthCredentials } from "./types";
|
|
12
13
|
|
|
13
14
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -72,23 +73,38 @@ interface CreateTokenError {
|
|
|
72
73
|
error_uri?: string;
|
|
73
74
|
}
|
|
74
75
|
|
|
76
|
+
interface CreateTokenResult {
|
|
77
|
+
response: Response;
|
|
78
|
+
status: number;
|
|
79
|
+
data: CreateTokenSuccess | CreateTokenError;
|
|
80
|
+
}
|
|
81
|
+
|
|
75
82
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
76
83
|
// Typed SSO OIDC error names from the published service model
|
|
77
84
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
78
85
|
|
|
79
86
|
const SSO_OIDC_FATAL_ERRORS = new Set([
|
|
80
87
|
"access_denied_exception",
|
|
88
|
+
"access_denied",
|
|
81
89
|
"expired_token_exception",
|
|
90
|
+
"expired_token",
|
|
82
91
|
"internal_server_exception",
|
|
92
|
+
"server_error",
|
|
83
93
|
"invalid_client_exception",
|
|
94
|
+
"invalid_client",
|
|
84
95
|
"invalid_client_metadata_exception",
|
|
85
96
|
"invalid_grant_exception",
|
|
97
|
+
"invalid_grant",
|
|
86
98
|
"invalid_redirect_uri_exception",
|
|
87
99
|
"invalid_request_exception",
|
|
100
|
+
"invalid_request",
|
|
88
101
|
"invalid_request_region_exception",
|
|
89
102
|
"invalid_scope_exception",
|
|
103
|
+
"invalid_scope",
|
|
90
104
|
"unauthorized_client_exception",
|
|
105
|
+
"unauthorized_client",
|
|
91
106
|
"unsupported_grant_type_exception",
|
|
107
|
+
"unsupported_grant_type",
|
|
92
108
|
]);
|
|
93
109
|
|
|
94
110
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -133,6 +149,7 @@ export async function registerClient(
|
|
|
133
149
|
method: "POST",
|
|
134
150
|
headers: { "Content-Type": "application/json" },
|
|
135
151
|
body: JSON.stringify(body),
|
|
152
|
+
redirect: "error",
|
|
136
153
|
signal,
|
|
137
154
|
});
|
|
138
155
|
|
|
@@ -180,6 +197,7 @@ export async function startDeviceAuthorization(
|
|
|
180
197
|
method: "POST",
|
|
181
198
|
headers: { "Content-Type": "application/json" },
|
|
182
199
|
body: JSON.stringify(body),
|
|
200
|
+
redirect: "error",
|
|
183
201
|
signal,
|
|
184
202
|
});
|
|
185
203
|
|
|
@@ -227,33 +245,54 @@ export async function pollForToken(
|
|
|
227
245
|
deviceCode,
|
|
228
246
|
};
|
|
229
247
|
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
248
|
+
const requestSignal = AbortSignal.any([
|
|
249
|
+
...(signal ? [signal] : []),
|
|
250
|
+
AbortSignal.timeout(Math.max(1, deadline - Date.now())),
|
|
251
|
+
]);
|
|
252
|
+
let result: CreateTokenResult;
|
|
253
|
+
try {
|
|
254
|
+
result = await createTokenOnce(url, {
|
|
255
|
+
method: "POST",
|
|
256
|
+
headers: { "Content-Type": "application/json" },
|
|
257
|
+
body: JSON.stringify(body),
|
|
258
|
+
signal: requestSignal,
|
|
259
|
+
});
|
|
260
|
+
} catch (error) {
|
|
261
|
+
if (signal?.aborted) throw new Error("Login cancelled");
|
|
262
|
+
if (Date.now() >= deadline) break;
|
|
263
|
+
throw error;
|
|
264
|
+
}
|
|
238
265
|
|
|
239
|
-
if (
|
|
266
|
+
if (Date.now() >= deadline) break;
|
|
267
|
+
const { status, data } = result;
|
|
268
|
+
|
|
269
|
+
if ("accessToken" in data) {
|
|
270
|
+
if (
|
|
271
|
+
status < 200 ||
|
|
272
|
+
status >= 300 ||
|
|
273
|
+
"error" in data ||
|
|
274
|
+
data.accessToken.length === 0 ||
|
|
275
|
+
!Number.isFinite(data.expiresIn) ||
|
|
276
|
+
data.expiresIn <= 0
|
|
277
|
+
) {
|
|
278
|
+
throw new Error("SSO OIDC CreateToken: invalid success response");
|
|
279
|
+
}
|
|
240
280
|
return data;
|
|
241
281
|
}
|
|
242
282
|
|
|
243
283
|
if ("error" in data) {
|
|
284
|
+
if (status !== 400) throw new Error(oidcRequestFailure(url, result.response));
|
|
244
285
|
const errorCode = data.error;
|
|
245
286
|
if (errorCode === "authorization_pending") continue;
|
|
246
287
|
if (errorCode === "slow_down") {
|
|
247
|
-
currentInterval
|
|
288
|
+
currentInterval += 5_000;
|
|
248
289
|
continue;
|
|
249
290
|
}
|
|
250
291
|
if (SSO_OIDC_FATAL_ERRORS.has(errorCode)) {
|
|
251
|
-
|
|
252
|
-
throw new Error(`SSO OIDC token error: ${errorCode}${desc}`);
|
|
292
|
+
throw new Error(`SSO OIDC token error: ${errorCode}`);
|
|
253
293
|
}
|
|
254
294
|
// Unknown error — fail closed
|
|
255
|
-
|
|
256
|
-
throw new Error(`SSO OIDC unrecognized token error: ${errorCode}${desc}`);
|
|
295
|
+
throw new Error(`SSO OIDC unrecognized token error: ${errorCode}`);
|
|
257
296
|
}
|
|
258
297
|
|
|
259
298
|
throw new Error("SSO OIDC CreateToken: unrecognized response shape");
|
|
@@ -292,6 +331,7 @@ export async function refreshKiroToken(credentials: OAuthCredentials): Promise<O
|
|
|
292
331
|
method: "POST",
|
|
293
332
|
headers: { "Content-Type": "application/json" },
|
|
294
333
|
body: JSON.stringify(body),
|
|
334
|
+
redirect: "error",
|
|
295
335
|
});
|
|
296
336
|
|
|
297
337
|
const data = (await response.json()) as CreateTokenSuccess | CreateTokenError;
|
|
@@ -430,19 +470,48 @@ export function importSsoCacheToken(): OAuthCredentials | undefined {
|
|
|
430
470
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
431
471
|
|
|
432
472
|
function ssoOidcEndpoint(region: string, pathSuffix: string): string {
|
|
473
|
+
assertAwsRegionLabel(region);
|
|
433
474
|
return `https://oidc.${region}.amazonaws.com${pathSuffix}`;
|
|
434
475
|
}
|
|
435
476
|
|
|
436
477
|
async function fetchOidc(url: string, init: RequestInit & { signal?: AbortSignal }): Promise<Response> {
|
|
437
|
-
const response = await fetch(url, init);
|
|
478
|
+
const response = await fetch(url, { ...init, redirect: "error" });
|
|
438
479
|
if (!response.ok) {
|
|
439
|
-
|
|
440
|
-
try {
|
|
441
|
-
errorBody = await response.text();
|
|
442
|
-
} catch {}
|
|
443
|
-
throw new Error(
|
|
444
|
-
`SSO OIDC request to ${url} failed: ${response.status} ${response.statusText}: ${errorBody.slice(0, 500)}`,
|
|
445
|
-
);
|
|
480
|
+
throw new Error(oidcRequestFailure(url, response));
|
|
446
481
|
}
|
|
447
482
|
return response;
|
|
448
483
|
}
|
|
484
|
+
|
|
485
|
+
function oidcRequestFailure(url: string, response: Response): string {
|
|
486
|
+
return `SSO OIDC request to ${url} failed: ${response.status} ${response.statusText}`;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* `CreateToken` reports the in-progress device-code states (`authorization_pending`,
|
|
491
|
+
* `slow_down`) as HTTP 400 responses whose body carries the error code, so the poll
|
|
492
|
+
* loop must read the payload instead of treating a non-2xx status as fatal.
|
|
493
|
+
* Non-2xx responses without an `error` field still fail closed.
|
|
494
|
+
*/
|
|
495
|
+
async function createTokenOnce(url: string, init: RequestInit & { signal?: AbortSignal }): Promise<CreateTokenResult> {
|
|
496
|
+
const response = await fetch(url, { ...init, redirect: "error" });
|
|
497
|
+
const rawBody = await response.text();
|
|
498
|
+
let data: CreateTokenSuccess | CreateTokenError;
|
|
499
|
+
try {
|
|
500
|
+
data = JSON.parse(rawBody) as CreateTokenSuccess | CreateTokenError;
|
|
501
|
+
} catch {
|
|
502
|
+
throw new Error(oidcRequestFailure(url, response));
|
|
503
|
+
}
|
|
504
|
+
if (data === null || typeof data !== "object") {
|
|
505
|
+
throw new Error(oidcRequestFailure(url, response));
|
|
506
|
+
}
|
|
507
|
+
if (!response.ok && !("error" in data)) {
|
|
508
|
+
throw new Error(oidcRequestFailure(url, response));
|
|
509
|
+
}
|
|
510
|
+
if ("error" in data && typeof data.error !== "string") {
|
|
511
|
+
throw new Error(oidcRequestFailure(url, response));
|
|
512
|
+
}
|
|
513
|
+
if ("accessToken" in data && typeof data.accessToken !== "string") {
|
|
514
|
+
throw new Error(oidcRequestFailure(url, response));
|
|
515
|
+
}
|
|
516
|
+
return { response, status: response.status, data };
|
|
517
|
+
}
|