@gajae-code/ai 0.15.5 → 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 +40 -0
- package/dist/types/adapter-internals/aws-region.d.ts +7 -0
- package/dist/types/auth-broker/client.d.ts +6 -2
- package/dist/types/auth-broker/remote-store.d.ts +14 -2
- package/dist/types/auth-broker/types.d.ts +6 -0
- package/dist/types/auth-broker/wire-schemas.d.ts +19 -0
- package/dist/types/auth-gateway/server.d.ts +39 -5
- package/dist/types/auth-gateway/types.d.ts +16 -2
- package/dist/types/auth-storage.d.ts +92 -24
- package/dist/types/core.d.ts +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/provider-models/openai-compat.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/providers/register-builtins.d.ts +12 -12
- package/dist/types/stream.d.ts +2 -1
- package/dist/types/types.d.ts +22 -2
- package/dist/types/utils/oauth/api-key-login.d.ts +4 -1
- package/dist/types/utils/oauth/api-key-validation.d.ts +12 -6
- package/dist/types/utils/oauth/commandcode.d.ts +1 -0
- package/dist/types/utils/oauth/types.d.ts +1 -1
- package/dist/types/utils/retry.d.ts +2 -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-broker/client.ts +41 -13
- package/src/auth-broker/redact.ts +25 -1
- package/src/auth-broker/remote-store.ts +374 -115
- package/src/auth-broker/server.ts +131 -91
- package/src/auth-broker/types.ts +6 -0
- package/src/auth-broker/wire-schemas.ts +6 -0
- package/src/auth-gateway/server.ts +447 -79
- package/src/auth-gateway/types.ts +28 -2
- package/src/auth-storage.ts +672 -168
- package/src/cli.ts +1 -0
- package/src/core.ts +1 -0
- package/src/index.ts +1 -0
- package/src/model-thinking.ts +8 -0
- package/src/models.json +1224 -3
- package/src/provider-models/descriptors.ts +3 -1
- package/src/provider-models/openai-compat.ts +102 -1
- package/src/providers/amazon-bedrock.ts +5 -1
- package/src/providers/anthropic.d.ts +1 -1
- package/src/providers/anthropic.ts +8 -2
- package/src/providers/aws-credentials.ts +6 -0
- package/src/providers/azure-openai-responses.ts +4 -1
- package/src/providers/cursor.ts +256 -101
- package/src/providers/gitlab-duo.ts +18 -1
- package/src/providers/google-gemini-cli.ts +3 -0
- package/src/providers/google-gemini-headers.d.ts +1 -1
- package/src/providers/google-gemini-headers.ts +1 -1
- package/src/providers/google-shared.ts +3 -0
- package/src/providers/kiro-api-key.ts +33 -8
- package/src/providers/kiro-codewhisperer.ts +28 -9
- package/src/providers/ollama.ts +3 -0
- package/src/providers/openai-codex-responses.d.ts +6 -0
- package/src/providers/openai-codex-responses.ts +37 -8
- package/src/providers/openai-completions.ts +11 -1
- package/src/providers/openai-responses.ts +10 -1
- package/src/providers/pi-native-client.ts +25 -1
- package/src/providers/pi-native-server.ts +24 -0
- package/src/providers/register-builtins.d.ts +12 -12
- package/src/providers/register-builtins.ts +16 -3
- package/src/stream.d.ts +2 -1
- package/src/stream.ts +175 -67
- package/src/types.d.ts +22 -2
- package/src/types.ts +27 -1
- package/src/utils/oauth/api-key-login.ts +13 -2
- package/src/utils/oauth/api-key-validation.ts +242 -41
- package/src/utils/oauth/commandcode.ts +17 -0
- package/src/utils/oauth/index.ts +20 -5
- package/src/utils/oauth/kiro.ts +91 -22
- package/src/utils/oauth/types.d.ts +1 -1
- package/src/utils/oauth/types.ts +1 -0
- package/src/utils/retry.d.ts +2 -0
- package/src/utils/retry.ts +15 -2
- 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
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
|
+
}
|
|
@@ -7,7 +7,7 @@ export type OAuthCredentials = {
|
|
|
7
7
|
email?: string;
|
|
8
8
|
accountId?: string;
|
|
9
9
|
};
|
|
10
|
-
export type OAuthProvider = "kiro" | "alibaba-token-plan" | "anthropic" | "bizrouter" | "mara" | "cerebras" | "cloudflare-ai-gateway" | "cursor" | "deepseek" | "deepinfra" | "fireworks" | "firepass" | "fugu" | "github-copilot" | "google-gemini-cli" | "google-antigravity" | "gitlab-duo" | "huggingface" | "kimi-code" | "kilo" | "kagi" | "litellm" | "lm-studio" | "minimax-code" | "minimax-code-cn" | "moonshot" | "nvidia" | "nanogpt" | "ollama" | "ollama-cloud" | "openai-codex" | "openai-codex-device" | "opencode-go" | "opencode-zen" | "opengateway" | "openrouter" | "parallel" | "perplexity" | "qianfan" | "qwen-portal" | "sglang" | "synthetic" | "tavily" | "together" | "venice" | "vercel-ai-gateway" | "vllm" | "xai" | "glm-zcode" | "xiaomi" | "xiaomi-token-plan-sgp" | "xiaomi-token-plan-ams" | "xiaomi-token-plan-cn" | "zenmux" | "opencodex" | "zai";
|
|
10
|
+
export type OAuthProvider = "kiro" | "alibaba-token-plan" | "anthropic" | "bizrouter" | "mara" | "cerebras" | "cloudflare-ai-gateway" | "cursor" | "deepseek" | "deepinfra" | "fireworks" | "firepass" | "fugu" | "github-copilot" | "google-gemini-cli" | "google-antigravity" | "gitlab-duo" | "huggingface" | "kimi-code" | "kilo" | "kagi" | "litellm" | "lm-studio" | "minimax-code" | "minimax-code-cn" | "moonshot" | "nvidia" | "nanogpt" | "ollama" | "ollama-cloud" | "openai-codex" | "openai-codex-device" | "opencode-go" | "opencode-zen" | "commandcode-goat" | "opengateway" | "openrouter" | "parallel" | "perplexity" | "qianfan" | "qwen-portal" | "sglang" | "synthetic" | "tavily" | "together" | "venice" | "vercel-ai-gateway" | "vllm" | "xai" | "glm-zcode" | "xiaomi" | "xiaomi-token-plan-sgp" | "xiaomi-token-plan-ams" | "xiaomi-token-plan-cn" | "zenmux" | "opencodex" | "zai";
|
|
11
11
|
export type OAuthProviderId = OAuthProvider | (string & {});
|
|
12
12
|
export type OAuthPrompt = {
|
|
13
13
|
message: string;
|
package/src/utils/oauth/types.ts
CHANGED
package/src/utils/retry.d.ts
CHANGED
package/src/utils/retry.ts
CHANGED
|
@@ -34,9 +34,22 @@ const COPILOT_MODEL_RETRY_BASE_DELAY_MS = 400;
|
|
|
34
34
|
*/
|
|
35
35
|
export async function callWithCopilotModelRetry<T>(
|
|
36
36
|
fn: () => Promise<T>,
|
|
37
|
-
options: {
|
|
37
|
+
options: {
|
|
38
|
+
provider: string;
|
|
39
|
+
signal?: AbortSignal;
|
|
40
|
+
retryBaseDelayMs?: number;
|
|
41
|
+
fallbackManaged?: boolean;
|
|
42
|
+
requestMaxRetries?: number;
|
|
43
|
+
disableProviderRetries?: boolean;
|
|
44
|
+
},
|
|
38
45
|
): Promise<T> {
|
|
39
|
-
if (
|
|
46
|
+
if (
|
|
47
|
+
options.provider !== "github-copilot" ||
|
|
48
|
+
options.fallbackManaged ||
|
|
49
|
+
options.requestMaxRetries === 0 ||
|
|
50
|
+
options.disableProviderRetries
|
|
51
|
+
)
|
|
52
|
+
return fn();
|
|
40
53
|
|
|
41
54
|
let lastError: unknown;
|
|
42
55
|
const retryBaseDelayMs = options.retryBaseDelayMs ?? COPILOT_MODEL_RETRY_BASE_DELAY_MS;
|
|
@@ -11,69 +11,190 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { isJsonObject, type JsonObject } from "./types";
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
14
|
+
const JSON_SCHEMA_LITERAL_PAYLOAD_KEYS = new Set(["default", "const", "enum", "examples"]);
|
|
15
|
+
const JSON_SCHEMA_MAP_KEYS = new Set([
|
|
16
|
+
"properties",
|
|
17
|
+
"patternProperties",
|
|
18
|
+
"dependencies",
|
|
19
|
+
"dependentSchemas",
|
|
20
|
+
"$defs",
|
|
21
|
+
"definitions",
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
function setOwnKey(target: JsonObject, key: string, value: unknown): void {
|
|
25
|
+
if (key === "__proto__") {
|
|
26
|
+
Object.defineProperty(target, key, { value, writable: true, enumerable: true, configurable: true });
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
target[key] = value;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type DereferenceState = {
|
|
33
|
+
unresolvedRef: boolean;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** Resolve any local JSON Pointer, including escaped names and boolean schemas. */
|
|
37
|
+
function resolveLocalRef(ref: string, root: JsonObject): unknown | undefined {
|
|
38
|
+
if (!ref.startsWith("#")) return undefined;
|
|
39
|
+
let pointer: string;
|
|
40
|
+
try {
|
|
41
|
+
pointer = decodeURIComponent(ref.slice(1));
|
|
42
|
+
} catch {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
if (pointer === "") return root;
|
|
46
|
+
if (!pointer.startsWith("/")) return undefined;
|
|
22
47
|
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
48
|
+
let current: unknown = root;
|
|
49
|
+
for (const encodedSegment of pointer.slice(1).split("/")) {
|
|
50
|
+
const segment = encodedSegment.replaceAll("~1", "/").replaceAll("~0", "~");
|
|
51
|
+
if (Array.isArray(current)) {
|
|
52
|
+
if (!/^(0|[1-9]\d*)$/.test(segment) || !Object.hasOwn(current, segment)) return undefined;
|
|
53
|
+
current = current[Number(segment)];
|
|
54
|
+
} else if (isJsonObject(current)) {
|
|
55
|
+
if (!Object.hasOwn(current, segment)) return undefined;
|
|
56
|
+
current = current[segment];
|
|
57
|
+
} else {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return current;
|
|
62
|
+
}
|
|
26
63
|
|
|
27
|
-
|
|
28
|
-
|
|
64
|
+
/** Find definition maps anywhere in a schema graph, excluding literal instance data. */
|
|
65
|
+
function hasDefinitionMapDeep(value: unknown, seen: Set<object>, inSchemaMap = false): boolean {
|
|
66
|
+
if (Array.isArray(value)) {
|
|
67
|
+
if (seen.has(value)) return false;
|
|
68
|
+
seen.add(value);
|
|
69
|
+
return value.some(entry => hasDefinitionMapDeep(entry, seen, false));
|
|
70
|
+
}
|
|
71
|
+
if (!isJsonObject(value) || seen.has(value)) return false;
|
|
72
|
+
seen.add(value);
|
|
73
|
+
for (const key in value) {
|
|
74
|
+
if (!Object.hasOwn(value, key)) continue;
|
|
75
|
+
if (!inSchemaMap && (key === "$defs" || key === "definitions")) return true;
|
|
76
|
+
if (!inSchemaMap && JSON_SCHEMA_LITERAL_PAYLOAD_KEYS.has(key)) continue;
|
|
77
|
+
const childInSchemaMap = !inSchemaMap && JSON_SCHEMA_MAP_KEYS.has(key);
|
|
78
|
+
if (hasDefinitionMapDeep(value[key], seen, childInSchemaMap)) return true;
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function dereferenceSchemaMap(
|
|
84
|
+
schemaMap: JsonObject,
|
|
85
|
+
root: JsonObject,
|
|
86
|
+
visitingRefs: Set<string>,
|
|
87
|
+
visitingNodes: Set<object>,
|
|
88
|
+
state: DereferenceState,
|
|
89
|
+
): JsonObject {
|
|
90
|
+
const result: JsonObject = {};
|
|
91
|
+
for (const key in schemaMap) {
|
|
92
|
+
if (!Object.hasOwn(schemaMap, key)) continue;
|
|
93
|
+
setOwnKey(result, key, dereferenceNode(schemaMap[key], root, visitingRefs, visitingNodes, state));
|
|
94
|
+
}
|
|
95
|
+
return result;
|
|
29
96
|
}
|
|
30
97
|
|
|
31
98
|
/**
|
|
32
99
|
* Recursively dereference a JSON Schema node, inlining all local `$ref` pointers.
|
|
100
|
+
* Object and array path tracking preserves the existing `{}` cycle boundary.
|
|
33
101
|
*/
|
|
34
|
-
function dereferenceNode(
|
|
35
|
-
|
|
36
|
-
|
|
102
|
+
function dereferenceNode(
|
|
103
|
+
node: unknown,
|
|
104
|
+
root: JsonObject,
|
|
105
|
+
visitingRefs: Set<string>,
|
|
106
|
+
visitingNodes: Set<object>,
|
|
107
|
+
state: DereferenceState,
|
|
108
|
+
inSchemaMap = false,
|
|
109
|
+
): unknown {
|
|
110
|
+
if (!node || typeof node !== "object") return node;
|
|
111
|
+
if (visitingNodes.has(node)) return {};
|
|
112
|
+
visitingNodes.add(node);
|
|
113
|
+
|
|
114
|
+
if (Array.isArray(node)) {
|
|
115
|
+
const result = node.map(item => dereferenceNode(item, root, visitingRefs, visitingNodes, state));
|
|
116
|
+
visitingNodes.delete(node);
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
37
119
|
|
|
38
|
-
const
|
|
120
|
+
const schemaNode = node as JsonObject;
|
|
121
|
+
const ref = Object.hasOwn(schemaNode, "$ref") ? schemaNode.$ref : undefined;
|
|
39
122
|
if (typeof ref === "string") {
|
|
40
|
-
|
|
41
|
-
|
|
123
|
+
if (visitingRefs.has(ref)) {
|
|
124
|
+
visitingNodes.delete(node);
|
|
125
|
+
return {};
|
|
126
|
+
}
|
|
42
127
|
const resolved = resolveLocalRef(ref, root);
|
|
43
|
-
if (
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
128
|
+
if (resolved !== undefined) {
|
|
129
|
+
visitingRefs.add(ref);
|
|
130
|
+
const inlined = dereferenceNode(resolved, root, visitingRefs, visitingNodes, state);
|
|
131
|
+
visitingRefs.delete(ref);
|
|
47
132
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
133
|
+
// Merge sibling keywords (e.g. description, default) from the
|
|
134
|
+
// referencing node. In draft 2020-12 these are valid alongside `$ref`.
|
|
135
|
+
let hasSiblings = false;
|
|
136
|
+
for (const key in schemaNode) {
|
|
137
|
+
if (Object.hasOwn(schemaNode, key) && key !== "$ref") {
|
|
138
|
+
hasSiblings = true;
|
|
139
|
+
break;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (inlined === false) {
|
|
143
|
+
visitingNodes.delete(node);
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
if (!hasSiblings || (!isJsonObject(inlined) && inlined !== true)) {
|
|
147
|
+
visitingNodes.delete(node);
|
|
148
|
+
return inlined;
|
|
55
149
|
}
|
|
150
|
+
const merged: JsonObject = {};
|
|
151
|
+
if (isJsonObject(inlined)) {
|
|
152
|
+
for (const key in inlined) {
|
|
153
|
+
if (Object.hasOwn(inlined, key)) setOwnKey(merged, key, inlined[key]);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
for (const key in schemaNode) {
|
|
157
|
+
if (!Object.hasOwn(schemaNode, key) || key === "$ref") continue;
|
|
158
|
+
const siblingValue = schemaNode[key];
|
|
159
|
+
const sibling =
|
|
160
|
+
!inSchemaMap && JSON_SCHEMA_LITERAL_PAYLOAD_KEYS.has(key)
|
|
161
|
+
? schemaNode[key]
|
|
162
|
+
: !inSchemaMap && JSON_SCHEMA_MAP_KEYS.has(key) && isJsonObject(siblingValue)
|
|
163
|
+
? dereferenceSchemaMap(siblingValue, root, visitingRefs, visitingNodes, state)
|
|
164
|
+
: dereferenceNode(siblingValue, root, visitingRefs, visitingNodes, state);
|
|
165
|
+
setOwnKey(merged, key, sibling);
|
|
166
|
+
}
|
|
167
|
+
if (!state.unresolvedRef) {
|
|
168
|
+
delete merged.$defs;
|
|
169
|
+
delete merged.definitions;
|
|
170
|
+
}
|
|
171
|
+
visitingNodes.delete(node);
|
|
172
|
+
return merged;
|
|
56
173
|
}
|
|
57
|
-
|
|
58
|
-
const merged: JsonObject = { ...inlined, ...node };
|
|
59
|
-
delete merged.$ref;
|
|
60
|
-
return merged;
|
|
174
|
+
state.unresolvedRef = true;
|
|
61
175
|
}
|
|
62
176
|
|
|
63
177
|
const result: JsonObject = {};
|
|
64
|
-
for (const key in
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if (
|
|
70
|
-
result
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
result
|
|
178
|
+
for (const key in schemaNode) {
|
|
179
|
+
if (!Object.hasOwn(schemaNode, key)) continue;
|
|
180
|
+
const value = schemaNode[key];
|
|
181
|
+
// Literal instance data is not schema-shaped, even when it contains an
|
|
182
|
+
// object that happens to use schema-looking keys or a local `$ref`.
|
|
183
|
+
if (!inSchemaMap && JSON_SCHEMA_LITERAL_PAYLOAD_KEYS.has(key)) {
|
|
184
|
+
setOwnKey(result, key, value);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (!inSchemaMap && JSON_SCHEMA_MAP_KEYS.has(key) && isJsonObject(value)) {
|
|
188
|
+
setOwnKey(result, key, dereferenceSchemaMap(value, root, visitingRefs, visitingNodes, state));
|
|
189
|
+
continue;
|
|
75
190
|
}
|
|
191
|
+
setOwnKey(result, key, dereferenceNode(value, root, visitingRefs, visitingNodes, state));
|
|
192
|
+
}
|
|
193
|
+
if (!state.unresolvedRef) {
|
|
194
|
+
delete result.$defs;
|
|
195
|
+
delete result.definitions;
|
|
76
196
|
}
|
|
197
|
+
visitingNodes.delete(node);
|
|
77
198
|
return result;
|
|
78
199
|
}
|
|
79
200
|
|
|
@@ -90,9 +211,8 @@ function dereferenceNode(node: unknown, root: JsonObject, visiting: Set<string>)
|
|
|
90
211
|
export function dereferenceJsonSchema(schema: unknown): unknown {
|
|
91
212
|
if (!isJsonObject(schema)) return schema;
|
|
92
213
|
|
|
93
|
-
// Fast path: nothing to dereference
|
|
94
|
-
|
|
95
|
-
if (!hasDefs) return schema;
|
|
214
|
+
// Fast path: nothing to dereference anywhere in the schema graph
|
|
215
|
+
if (!hasDefinitionMapDeep(schema, new Set())) return schema;
|
|
96
216
|
|
|
97
|
-
return dereferenceNode(schema, schema, new Set());
|
|
217
|
+
return dereferenceNode(schema, schema, new Set(), new Set(), { unresolvedRef: false });
|
|
98
218
|
}
|