@apifuse/provider-sdk 2.2.0-beta.26 → 2.2.0-beta.28

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.
Files changed (90) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/bin/apifuse-dev.ts +2 -2
  3. package/bin/apifuse-pack-smoke.ts +1 -1
  4. package/bin/apifuse-pack-types.ts +2 -1
  5. package/bin/apifuse-perf.ts +2 -4
  6. package/bin/apifuse-record.ts +1 -1
  7. package/dist/auth-turn/index.d.ts +1 -1
  8. package/dist/auth-turn/index.js +1 -1
  9. package/dist/ceremonies/index.js +52 -11
  10. package/dist/config/loader.d.ts +1 -1
  11. package/dist/config/loader.js +4 -2
  12. package/dist/contract-serialization.d.ts +1 -20
  13. package/dist/contract-serialization.js +8 -587
  14. package/dist/contract.d.ts +0 -2
  15. package/dist/contract.js +5 -9
  16. package/dist/index.d.ts +9 -8
  17. package/dist/index.js +6 -8
  18. package/dist/provider.d.ts +3 -2
  19. package/dist/provider.js +2 -2
  20. package/dist/runtime/auth-flow.js +1 -1
  21. package/dist/runtime/http.d.ts +1 -0
  22. package/dist/runtime/http.js +135 -12
  23. package/dist/runtime/instrumentation.js +1 -1
  24. package/dist/runtime/native-network-errors.d.ts +33 -0
  25. package/dist/runtime/native-network-errors.js +69 -0
  26. package/dist/runtime/native-network.d.ts +2 -33
  27. package/dist/runtime/native-network.js +2 -68
  28. package/dist/runtime/redis.d.ts +1 -1
  29. package/dist/runtime/redis.js +4 -2
  30. package/dist/runtime/resolver-config.d.ts +6 -0
  31. package/dist/runtime/resolver-config.js +6 -0
  32. package/dist/runtime/resolver-shared.d.ts +3 -0
  33. package/dist/runtime/resolver-shared.js +12 -0
  34. package/dist/runtime/resolver-vendors/twocaptcha.d.ts +2 -1
  35. package/dist/runtime/resolver-vendors/twocaptcha.js +80 -43
  36. package/dist/runtime/resolver-vendors/types.d.ts +1 -1
  37. package/dist/runtime/resolver.d.ts +6 -10
  38. package/dist/runtime/resolver.js +19 -18
  39. package/dist/runtime/state.js +5 -115
  40. package/dist/runtime/stealth-cookies.d.ts +20 -0
  41. package/dist/runtime/stealth-cookies.js +111 -0
  42. package/dist/runtime/stealth.js +7 -130
  43. package/dist/schema.d.ts +0 -63
  44. package/dist/schema.js +8 -808
  45. package/dist/serve.d.ts +1 -1
  46. package/dist/serve.js +1 -1
  47. package/dist/server/index.d.ts +1 -1
  48. package/dist/server/index.js +1 -1
  49. package/dist/server/self-test.d.ts +13 -0
  50. package/dist/server/self-test.js +124 -46
  51. package/dist/server/serve-implementation.d.ts +199 -0
  52. package/dist/server/serve-implementation.js +2054 -0
  53. package/dist/server/serve.d.ts +1 -187
  54. package/dist/server/serve.js +1 -1827
  55. package/dist/stateful/errors.d.ts +5 -0
  56. package/dist/stateful/errors.js +10 -0
  57. package/dist/stateful/stateful-provider-session-routing.d.ts +1 -5
  58. package/dist/stateful/stateful-provider-session-routing.js +2 -10
  59. package/dist/stream.js +7 -1
  60. package/package.json +27 -2
  61. package/src/auth-turn/index.ts +1 -1
  62. package/src/ceremonies/index.ts +68 -18
  63. package/src/config/loader.ts +5 -2
  64. package/src/contract-serialization.ts +8 -859
  65. package/src/contract.ts +5 -16
  66. package/src/index.ts +18 -34
  67. package/src/provider.ts +12 -24
  68. package/src/runtime/auth-flow.ts +1 -1
  69. package/src/runtime/http.ts +155 -11
  70. package/src/runtime/instrumentation.ts +1 -1
  71. package/src/runtime/native-network-errors.ts +99 -0
  72. package/src/runtime/native-network.ts +16 -97
  73. package/src/runtime/redis.ts +7 -2
  74. package/src/runtime/resolver-config.ts +6 -0
  75. package/src/runtime/resolver-shared.ts +17 -0
  76. package/src/runtime/resolver-vendors/twocaptcha.ts +100 -49
  77. package/src/runtime/resolver-vendors/types.ts +1 -0
  78. package/src/runtime/resolver.ts +47 -22
  79. package/src/runtime/state.ts +5 -144
  80. package/src/runtime/stealth-cookies.ts +132 -0
  81. package/src/runtime/stealth.ts +14 -157
  82. package/src/schema.ts +9 -1060
  83. package/src/serve.ts +6 -1
  84. package/src/server/index.ts +1 -0
  85. package/src/server/self-test.ts +184 -59
  86. package/src/server/serve-implementation.ts +3024 -0
  87. package/src/server/serve.ts +1 -2661
  88. package/src/stateful/errors.ts +12 -0
  89. package/src/stateful/stateful-provider-session-routing.ts +2 -11
  90. package/src/stream.ts +8 -1
@@ -1,4 +1,4 @@
1
- import { createHash, randomUUID } from "node:crypto";
1
+ import { createHash } from "node:crypto";
2
2
  import { type ZodType, z } from "zod";
3
3
  import {
4
4
  canonicalJson,
@@ -7,78 +7,11 @@ import {
7
7
  type JsonValue,
8
8
  toJsonValue,
9
9
  } from "./contract-json.js";
10
- import {
11
- APIFUSE_TEXT_TRUST_META_KEY,
12
- hasDynamicOutputFallback,
13
- inheritsUntrustedOutputTextTrust,
14
- invalidatesDescendantOutputTextAutoTrust,
15
- mutatorReturnIsProvablyNonText,
16
- mutatorUsesStaticArrayFallback,
17
- resolveOutputTextTrustProjection,
18
- suppressDynamicOutputFallbacksDuring,
19
- type TextTrust,
20
- } from "./schema.js";
21
10
  import type { SchemaLike } from "./types.js";
22
11
 
23
- const OUTPUT_TEXT_TRUST_PROJECTION_MARKER_KEY = "x-apifuse-internal-text-trust-projection";
24
-
25
- type OutputTextTrustProjectionMarker =
26
- | {
27
- readonly kind: "container-untrusted";
28
- }
29
- | {
30
- readonly kind: "container-unsafe";
31
- }
32
- | {
33
- readonly inherited: TextTrust;
34
- readonly kind: "leaf";
35
- readonly local: TextTrust;
36
- };
37
-
38
- interface DescribeSchemaOptions {
39
- readonly eventName?: string;
40
- readonly operationId?: string;
41
- readonly outputTextTrust?: boolean;
42
- }
43
-
44
- export class OutputTextTrustProjectionError extends Error {
45
- readonly classification = "untrusted" as const;
46
- readonly code = "output_text_trust_projection_failed";
47
-
48
- constructor(
49
- public readonly schemaPath: string,
50
- cause: unknown,
51
- public readonly operationId?: string,
52
- public readonly eventName?: string,
53
- ) {
54
- const context = [
55
- operationId === undefined ? undefined : `operation "${operationId}"`,
56
- eventName === undefined ? undefined : `event "${eventName}"`,
57
- ]
58
- .filter((value): value is string => value !== undefined)
59
- .join(", ");
60
- super(
61
- context.length === 0
62
- ? `Output text-trust projection failed at schema path ${schemaPath}.`
63
- : `Output text-trust projection failed for ${context} at schema path ${schemaPath}.`,
64
- );
65
- this.name = "OutputTextTrustProjectionError";
66
- this.cause = cause;
67
- }
68
- }
69
-
70
- export class OutputTextTrustProjectionMarkerError extends TypeError {
71
- readonly code = "invalid_output_text_trust_projection_marker";
72
-
73
- constructor(public readonly schemaPath: string) {
74
- super(`Invalid internal output text-trust projection marker at schema path ${schemaPath}.`);
75
- this.name = "OutputTextTrustProjectionMarkerError";
76
- }
77
- }
78
-
79
- export function describeSchema(schema: SchemaLike, options: DescribeSchemaOptions = {}): JsonValue {
12
+ export function describeSchema(schema: SchemaLike): JsonValue {
80
13
  if (isZodSchema(schema)) {
81
- const jsonSchema = zodJsonSchema(schema, options);
14
+ const jsonSchema = zodJsonSchema(schema);
82
15
  return compactObject({
83
16
  kind: "schema",
84
17
  vendor: "zod",
@@ -89,16 +22,6 @@ export function describeSchema(schema: SchemaLike, options: DescribeSchemaOption
89
22
  }
90
23
  const standard = isRecord(schema) ? schema["~standard"] : undefined;
91
24
  if (isRecord(standard)) {
92
- if (options.outputTextTrust) {
93
- throw new OutputTextTrustProjectionError(
94
- "$",
95
- new TypeError(
96
- "Standard Schema output cannot be projected to JSON Schema with text-trust classifications.",
97
- ),
98
- options.operationId,
99
- options.eventName,
100
- );
101
- }
102
25
  return compactObject({
103
26
  kind: "schema",
104
27
  standard: "standard-schema-v1",
@@ -141,786 +64,12 @@ function isZodSchema(schema: SchemaLike): schema is ZodType {
141
64
  return schema instanceof z.ZodType;
142
65
  }
143
66
 
144
- function zodJsonSchema(schema: ZodType, options: DescribeSchemaOptions): JsonValue {
145
- const projectionMarkers = new Map<string, OutputTextTrustProjectionMarker>();
146
- const expectedLeafMarkers = new Map<string, string>();
147
- const expectedUnsafeContainerMarkers = new Map<string, string>();
148
- const expectedHonestTextContainerMarkers = new Map<string, string>();
149
- try {
150
- const jsonSchemaOptions: z.core.ToJSONSchemaParams = {
151
- unrepresentable: options.outputTextTrust ? "any" : "throw",
152
- override: ({ zodSchema, jsonSchema: projectedSchema, path }) => {
153
- if (!options.outputTextTrust) {
154
- delete projectedSchema[OUTPUT_TEXT_TRUST_PROJECTION_MARKER_KEY];
155
- delete projectedSchema[APIFUSE_TEXT_TRUST_META_KEY];
156
- return;
157
- }
158
- try {
159
- const unrepresentable = unrepresentableZodOutputCause(zodSchema);
160
- if (unrepresentable) throw unrepresentable;
161
- const resolved = resolveOutputTextTrustProjection(zodSchema);
162
- delete projectedSchema[APIFUSE_TEXT_TRUST_META_KEY];
163
- if (hasDynamicOutputFallback(zodSchema)) delete projectedSchema.default;
164
- if (resolved !== undefined) {
165
- const markerId = writeOutputTextTrustProjectionMarker(
166
- projectedSchema,
167
- projectionMarkers,
168
- {
169
- inherited: resolved.inherited,
170
- kind: "leaf",
171
- local: resolved.local,
172
- },
173
- );
174
- expectedLeafMarkers.set(projectedJsonSchemaPath(path), markerId);
175
- } else if (invalidatesDescendantOutputTextAutoTrust(zodSchema)) {
176
- if (
177
- projectedJsonSchemaIsProvablyNonText(projectedSchema) &&
178
- mutatorReturnIsProvablyNonText(zodSchema)
179
- ) {
180
- return;
181
- }
182
- if (mutatorCanUseProjectedTextItems(zodSchema, projectedSchema)) {
183
- writeOutputTextTrustProjectionMarker(projectedSchema, projectionMarkers, {
184
- kind: "container-untrusted",
185
- });
186
- return;
187
- }
188
- admitPossibleRuntimeText(
189
- projectedSchema,
190
- projectionMarkers,
191
- expectedLeafMarkers,
192
- expectedUnsafeContainerMarkers,
193
- expectedHonestTextContainerMarkers,
194
- projectedJsonSchemaPath(path),
195
- );
196
- const containerMarkerId = writeOutputTextTrustProjectionMarker(
197
- projectedSchema,
198
- projectionMarkers,
199
- { kind: "container-unsafe" },
200
- );
201
- expectedUnsafeContainerMarkers.set(projectedJsonSchemaPath(path), containerMarkerId);
202
- expectedHonestTextContainerMarkers.set(
203
- projectedJsonSchemaPath(path),
204
- containerMarkerId,
205
- );
206
- } else if (inheritsUntrustedOutputTextTrust(zodSchema)) {
207
- writeOutputTextTrustProjectionMarker(projectedSchema, projectionMarkers, {
208
- kind: "container-untrusted",
209
- });
210
- }
211
- if (hasOpenObjectCatchall(zodSchema)) {
212
- const containerMarkerId = writeOutputTextTrustProjectionMarker(
213
- projectedSchema,
214
- projectionMarkers,
215
- { kind: "container-unsafe" },
216
- );
217
- expectedUnsafeContainerMarkers.set(projectedJsonSchemaPath(path), containerMarkerId);
218
- }
219
- if (
220
- zodSchema._zod.def.type === "record" &&
221
- projectedSchema.additionalProperties === false
222
- ) {
223
- const projectedPath = projectedJsonSchemaPath(path);
224
- expectedLeafMarkers.delete(appendJsonSchemaPath(projectedPath, "additionalProperties"));
225
- const containerMarkerId = writeOutputTextTrustProjectionMarker(
226
- projectedSchema,
227
- projectionMarkers,
228
- { kind: "container-unsafe" },
229
- );
230
- expectedUnsafeContainerMarkers.set(projectedPath, containerMarkerId);
231
- }
232
- } catch (error) {
233
- throw new OutputTextTrustProjectionError(
234
- jsonSchemaPath(path),
235
- error,
236
- options.operationId,
237
- options.eventName,
238
- );
239
- }
240
- },
241
- };
242
- const projectJsonSchema = () => z.toJSONSchema(schema, jsonSchemaOptions);
243
- const jsonSchema = options.outputTextTrust
244
- ? suppressDynamicOutputFallbacksDuring(projectJsonSchema)
245
- : projectJsonSchema();
246
- if (options.outputTextTrust) {
247
- finalizeProjectedTextTrust(
248
- jsonSchema,
249
- projectionMarkers,
250
- expectedLeafMarkers,
251
- expectedUnsafeContainerMarkers,
252
- expectedHonestTextContainerMarkers,
253
- options,
254
- );
255
- }
256
- const projected = toJsonValue(jsonSchema);
257
- if (projected === undefined) {
258
- throw new TypeError("z.toJSONSchema() returned a non-JSON value");
259
- }
260
- return projected;
261
- } catch (error) {
262
- if (error instanceof OutputTextTrustProjectionError) throw error;
263
- if (!options.outputTextTrust) throw error;
264
- throw new OutputTextTrustProjectionError("$", error, options.operationId, options.eventName);
265
- }
266
- }
267
-
268
- function writeOutputTextTrustProjectionMarker(
269
- schema: Record<string, unknown>,
270
- markers: Map<string, OutputTextTrustProjectionMarker>,
271
- marker: OutputTextTrustProjectionMarker,
272
- ): string {
273
- const existingId = schema[OUTPUT_TEXT_TRUST_PROJECTION_MARKER_KEY];
274
- if (typeof existingId === "string") {
275
- const existingMarker = markers.get(existingId);
276
- if (existingMarker) {
277
- markers.set(existingId, mergeOutputTextTrustProjectionMarkers(existingMarker, marker));
278
- return existingId;
279
- }
280
- }
281
- const id = randomUUID();
282
- markers.set(id, marker);
283
- schema[OUTPUT_TEXT_TRUST_PROJECTION_MARKER_KEY] = id;
284
- return id;
285
- }
286
-
287
- function mergeOutputTextTrustProjectionMarkers(
288
- existing: OutputTextTrustProjectionMarker,
289
- next: OutputTextTrustProjectionMarker,
290
- ): OutputTextTrustProjectionMarker {
291
- if (existing.kind === "container-unsafe" && next.kind === "container-untrusted") {
292
- return existing;
293
- }
294
- return next;
295
- }
296
-
297
- function hasOpenObjectCatchall(schema: z.core.$ZodType): boolean {
298
- const def = schema._zod.def;
299
- if (def.type !== "object") return false;
300
- const catchall = Reflect.get(def, "catchall");
301
- if (!catchall || typeof catchall !== "object") return false;
302
- const internals = Reflect.get(catchall, "_zod");
303
- if (!internals || typeof internals !== "object") return false;
304
- const catchallDef = Reflect.get(internals, "def");
305
- return (
306
- !catchallDef || typeof catchallDef !== "object" || Reflect.get(catchallDef, "type") !== "never"
307
- );
308
- }
309
-
310
- function unrepresentableZodOutputCause(schema: z.core.$ZodType): Error | undefined {
311
- const def = schema._zod.def;
312
- switch (def.type) {
313
- case "bigint":
314
- return new Error("BigInt cannot be represented in JSON Schema");
315
- case "symbol":
316
- return new Error("Symbols cannot be represented in JSON Schema");
317
- case "undefined":
318
- return new Error("Undefined cannot be represented in JSON Schema");
319
- case "void":
320
- return new Error("Void cannot be represented in JSON Schema");
321
- case "date":
322
- return new Error("Date cannot be represented in JSON Schema");
323
- case "nan":
324
- return new Error("NaN cannot be represented in JSON Schema");
325
- case "custom":
326
- return new Error("Custom types cannot be represented in JSON Schema");
327
- case "function":
328
- return new Error("Function types cannot be represented in JSON Schema");
329
- case "transform":
330
- return new Error("Transforms cannot be represented in JSON Schema");
331
- case "map":
332
- return new Error("Map cannot be represented in JSON Schema");
333
- case "set":
334
- return new Error("Set cannot be represented in JSON Schema");
335
- case "literal": {
336
- const values = Reflect.get(def, "values");
337
- if (!Array.isArray(values)) return undefined;
338
- if (values.some((value: unknown) => value === undefined)) {
339
- return new Error("Literal `undefined` cannot be represented in JSON Schema");
340
- }
341
- if (values.some((value: unknown) => typeof value === "bigint")) {
342
- return new Error("BigInt literals cannot be represented in JSON Schema");
343
- }
344
- return undefined;
345
- }
346
- default:
347
- return undefined;
348
- }
349
- }
350
-
351
- const SINGLE_SCHEMA_KEYWORDS = [
352
- "additionalProperties",
353
- "contains",
354
- "contentSchema",
355
- "else",
356
- "if",
357
- "items",
358
- "not",
359
- "propertyNames",
360
- "then",
361
- "unevaluatedItems",
362
- "unevaluatedProperties",
363
- ] as const;
364
-
365
- const ARRAY_SCHEMA_KEYWORDS = ["allOf", "anyOf", "oneOf", "prefixItems"] as const;
366
- const MAP_SCHEMA_KEYWORDS = [
367
- "$defs",
368
- "definitions",
369
- "dependentSchemas",
370
- "patternProperties",
371
- "properties",
372
- ] as const;
373
-
374
- interface ProjectedJsonSchemaNode {
375
- readonly path: string;
376
- readonly schema: Record<string, unknown>;
377
- }
378
-
379
- function finalizeProjectedTextTrust(
380
- value: unknown,
381
- projectionMarkers: ReadonlyMap<string, OutputTextTrustProjectionMarker>,
382
- expectedLeafMarkers: ReadonlyMap<string, string>,
383
- expectedUnsafeContainerMarkers: ReadonlyMap<string, string>,
384
- expectedHonestTextContainerMarkers: ReadonlyMap<string, string>,
385
- options: DescribeSchemaOptions,
386
- ): void {
387
- if (!isJsonSchemaNode(value)) {
388
- throw new OutputTextTrustProjectionError(
389
- "#",
390
- new TypeError("Projected output schema is not a JSON Schema object."),
391
- options.operationId,
392
- options.eventName,
393
- );
394
- }
395
- const root = value;
396
- const nodes = collectProjectedJsonSchemaNodes(root);
397
- const markers = new WeakMap<Record<string, unknown>, OutputTextTrustProjectionMarker>();
398
- for (const { path, schema } of nodes) {
399
- const marker = readOutputTextTrustProjectionMarker(
400
- schema[OUTPUT_TEXT_TRUST_PROJECTION_MARKER_KEY],
401
- projectionMarkers,
402
- path,
403
- options,
404
- );
405
- if (marker) markers.set(schema, marker);
406
- }
407
- for (const [expectedPath, expectedMarkerId] of expectedLeafMarkers) {
408
- const authenticatedLeaf = resolveExpectedProjectedSchema(
409
- root,
410
- expectedPath,
411
- expectedMarkerId,
412
- markers,
413
- options,
414
- );
415
- if (authenticatedLeaf === undefined || markers.get(authenticatedLeaf)?.kind !== "leaf") {
416
- throw new OutputTextTrustProjectionError(
417
- expectedPath,
418
- new TypeError("A projected output text leaf lost its authenticated trust marker."),
419
- options.operationId,
420
- options.eventName,
421
- );
422
- }
423
- if (!projectedJsonSchemaCanAllowString(authenticatedLeaf)) {
424
- throw new OutputTextTrustProjectionError(
425
- expectedPath,
426
- new TypeError("A projected output text leaf was mutated to a non-text type."),
427
- options.operationId,
428
- options.eventName,
429
- );
430
- }
431
- }
432
- for (const [expectedPath, expectedMarkerId] of expectedUnsafeContainerMarkers) {
433
- const authenticatedContainer = resolveExpectedProjectedSchema(
434
- root,
435
- expectedPath,
436
- expectedMarkerId,
437
- markers,
438
- options,
439
- );
440
- if (
441
- authenticatedContainer === undefined ||
442
- markers.get(authenticatedContainer)?.kind !== "container-unsafe"
443
- ) {
444
- throw new OutputTextTrustProjectionError(
445
- expectedPath,
446
- new TypeError("A type-mutating output container lost its authenticated trust marker."),
447
- options.operationId,
448
- options.eventName,
449
- );
450
- }
451
- }
452
- for (const [expectedPath, expectedMarkerId] of expectedHonestTextContainerMarkers) {
453
- const authenticatedContainer = resolveExpectedProjectedSchema(
454
- root,
455
- expectedPath,
456
- expectedMarkerId,
457
- markers,
458
- options,
459
- );
460
- const branches = authenticatedContainer?.anyOf;
461
- if (
462
- !Array.isArray(branches) ||
463
- branches.length !== 2 ||
464
- !branches.every(
465
- (branch) => isJsonSchemaNode(branch) && markers.get(branch)?.kind === "container-unsafe",
466
- ) ||
467
- !isJsonSchemaNode(branches[1]) ||
468
- !projectedJsonSchemaAllowsString(branches[1])
469
- ) {
470
- throw new OutputTextTrustProjectionError(
471
- expectedPath,
472
- new TypeError(
473
- "A type-mutating output container lost its authenticated runtime text shape.",
474
- ),
475
- options.operationId,
476
- options.eventName,
477
- );
478
- }
479
- }
480
- for (const { schema } of nodes) {
481
- if (!markers.has(schema) && projectedJsonSchemaAllowsString(schema)) {
482
- markers.set(schema, { inherited: "untrusted", kind: "leaf", local: "untrusted" });
483
- }
484
- }
485
-
486
- const desiredTrust = new WeakMap<Record<string, unknown>, TextTrust>();
487
- const desiredContainerCarriers = new WeakSet<Record<string, unknown>>();
488
- const visitedStates = new WeakMap<Record<string, unknown>, number>();
489
- const project = (
490
- schema: Record<string, unknown>,
491
- inheritedUntrusted: boolean,
492
- autoTrustAllowed: boolean,
493
- runtimeMutationUntrusted: boolean,
494
- ): void => {
495
- const state =
496
- 1 <<
497
- ((inheritedUntrusted ? 1 : 0) +
498
- (autoTrustAllowed ? 0 : 2) +
499
- (runtimeMutationUntrusted ? 4 : 0));
500
- const visited = visitedStates.get(schema) ?? 0;
501
- if ((visited & state) !== 0) return;
502
- visitedStates.set(schema, visited | state);
503
-
504
- const marker = markers.get(schema);
505
- if (marker?.kind === "leaf") {
506
- const trust = autoTrustAllowed
507
- ? inheritedUntrusted
508
- ? marker.inherited
509
- : marker.local
510
- : "untrusted";
511
- if (desiredTrust.get(schema) !== "untrusted") desiredTrust.set(schema, trust);
512
- return;
513
- }
514
- if (runtimeMutationUntrusted) desiredTrust.set(schema, "untrusted");
515
- if (marker?.kind === "container-unsafe") {
516
- desiredTrust.set(schema, "untrusted");
517
- desiredContainerCarriers.add(schema);
518
- }
519
- const descendantUntrusted = inheritedUntrusted || marker?.kind === "container-untrusted";
520
- const descendantAutoTrustAllowed = autoTrustAllowed && marker?.kind !== "container-unsafe";
521
- const descendantRuntimeMutationUntrusted =
522
- runtimeMutationUntrusted || marker?.kind === "container-unsafe";
523
- const reference = schema.$ref;
524
- if (reference !== undefined) {
525
- if (typeof reference !== "string") {
526
- throw new OutputTextTrustProjectionError(
527
- requireNodePath(nodes, schema, options),
528
- new TypeError("Projected JSON Schema $ref must be a string."),
529
- options.operationId,
530
- options.eventName,
531
- );
532
- }
533
- project(
534
- resolveLocalJsonSchemaReference(
535
- root,
536
- reference,
537
- requireNodePath(nodes, schema, options),
538
- options,
539
- ),
540
- descendantUntrusted,
541
- descendantAutoTrustAllowed,
542
- descendantRuntimeMutationUntrusted,
543
- );
544
- }
545
- for (const child of projectedJsonSchemaChildren(schema, false)) {
546
- project(
547
- child,
548
- descendantUntrusted,
549
- descendantAutoTrustAllowed,
550
- descendantRuntimeMutationUntrusted,
551
- );
552
- }
553
- };
554
- project(root, false, true, false);
555
-
556
- for (const { schema } of nodes) {
557
- delete schema[OUTPUT_TEXT_TRUST_PROJECTION_MARKER_KEY];
558
- delete schema[APIFUSE_TEXT_TRUST_META_KEY];
559
- }
560
- for (const { schema } of nodes) {
561
- const trust = desiredTrust.get(schema);
562
- if (trust !== undefined) {
563
- schema[APIFUSE_TEXT_TRUST_META_KEY] = desiredContainerCarriers.has(schema)
564
- ? { v: 1, trust, carrier: "container" }
565
- : { v: 1, trust };
566
- }
567
- }
568
- }
569
-
570
- function projectedJsonSchemaAllowsString(schema: Record<string, unknown>): boolean {
571
- return schema.type === "string" || (Array.isArray(schema.type) && schema.type.includes("string"));
572
- }
573
-
574
- function admitPossibleRuntimeText(
575
- schema: Record<string, unknown>,
576
- projectionMarkers: Map<string, OutputTextTrustProjectionMarker>,
577
- expectedLeafMarkers: Map<string, string>,
578
- expectedUnsafeContainerMarkers: Map<string, string>,
579
- expectedHonestTextContainerMarkers: Map<string, string>,
580
- projectedPath: string,
581
- ): void {
582
- const existingMarkerId = schema[OUTPUT_TEXT_TRUST_PROJECTION_MARKER_KEY];
583
- if (
584
- typeof existingMarkerId === "string" &&
585
- projectionMarkers.get(existingMarkerId)?.kind === "container-unsafe"
586
- ) {
587
- return;
588
- }
589
- const originalShape = { ...schema };
590
- for (const key of Object.keys(schema)) delete schema[key];
591
- const textShape: Record<string, unknown> = { type: "string" };
592
- schema.anyOf = [originalShape, textShape];
593
-
594
- const originalPath = appendJsonSchemaPath(appendJsonSchemaPath(projectedPath, "anyOf"), 0);
595
- rebaseExpectedProjectionPaths(expectedLeafMarkers, projectedPath, originalPath);
596
- expectedUnsafeContainerMarkers.delete(projectedPath);
597
- rebaseExpectedProjectionPaths(expectedUnsafeContainerMarkers, projectedPath, originalPath);
598
- expectedHonestTextContainerMarkers.delete(projectedPath);
599
- rebaseExpectedProjectionPaths(expectedHonestTextContainerMarkers, projectedPath, originalPath);
600
- writeOutputTextTrustProjectionMarker(originalShape, projectionMarkers, {
601
- kind: "container-unsafe",
602
- });
603
- writeOutputTextTrustProjectionMarker(textShape, projectionMarkers, {
604
- kind: "container-unsafe",
605
- });
606
- }
607
-
608
- function rebaseExpectedProjectionPaths(
609
- expectedMarkers: Map<string, string>,
610
- fromPath: string,
611
- toPath: string,
612
- ): void {
613
- for (const [path, markerId] of [...expectedMarkers]) {
614
- if (path !== fromPath && !path.startsWith(`${fromPath}/`)) continue;
615
- expectedMarkers.delete(path);
616
- expectedMarkers.set(`${toPath}${path.slice(fromPath.length)}`, markerId);
617
- }
618
- }
619
-
620
- function projectedJsonSchemaCanAllowString(schema: Record<string, unknown>): boolean {
621
- return schema.type === undefined || projectedJsonSchemaAllowsString(schema);
622
- }
623
-
624
- const PROVABLY_NON_TEXT_JSON_SCHEMA_TYPES = new Set(["boolean", "integer", "null", "number"]);
625
-
626
- function mutatorCanUseProjectedTextItems(
627
- zodSchema: z.core.$ZodType,
628
- projectedSchema: Record<string, unknown>,
629
- ): boolean {
630
- return (
631
- projectedJsonSchemaAllowsArray(projectedSchema) &&
632
- mutatorUsesStaticArrayFallback(zodSchema) &&
633
- isJsonSchemaNode(projectedSchema.items) &&
634
- projectedJsonSchemaCanAllowString(projectedSchema.items)
635
- );
636
- }
637
-
638
- function projectedJsonSchemaIsProvablyNonText(
639
- schema: Record<string, unknown>,
640
- visited = new WeakSet<Record<string, unknown>>(),
641
- ): boolean {
642
- if (visited.has(schema)) return false;
643
- visited.add(schema);
644
- try {
645
- const types = typeof schema.type === "string" ? [schema.type] : schema.type;
646
- if (Array.isArray(types) && types.length > 0) {
647
- return types.every(
648
- (type) =>
649
- typeof type === "string" &&
650
- (PROVABLY_NON_TEXT_JSON_SCHEMA_TYPES.has(type) ||
651
- (type === "array" &&
652
- isJsonSchemaNode(schema.items) &&
653
- projectedJsonSchemaIsProvablyNonText(schema.items, visited))),
654
- );
655
- }
656
- for (const keyword of ["anyOf", "oneOf"] as const) {
657
- const branches = schema[keyword];
658
- if (!Array.isArray(branches) || branches.length === 0) continue;
659
- return branches.every(
660
- (branch) =>
661
- isJsonSchemaNode(branch) && projectedJsonSchemaIsProvablyNonText(branch, visited),
662
- );
663
- }
664
- return false;
665
- } finally {
666
- visited.delete(schema);
667
- }
668
- }
669
-
670
- function projectedJsonSchemaAllowsArray(schema: Record<string, unknown>): boolean {
671
- return schema.type === "array" || (Array.isArray(schema.type) && schema.type.includes("array"));
672
- }
673
-
674
- function readOutputTextTrustProjectionMarker(
675
- value: unknown,
676
- projectionMarkers: ReadonlyMap<string, OutputTextTrustProjectionMarker>,
677
- schemaPath: string,
678
- options: DescribeSchemaOptions,
679
- ): OutputTextTrustProjectionMarker | undefined {
680
- if (value === undefined) return undefined;
681
- if (typeof value !== "string") throw invalidProjectionMarker(schemaPath, options);
682
- const marker = projectionMarkers.get(value);
683
- if (!marker) throw invalidProjectionMarker(schemaPath, options);
684
- return marker;
685
- }
686
-
687
- function invalidProjectionMarker(
688
- schemaPath: string,
689
- options: DescribeSchemaOptions,
690
- ): OutputTextTrustProjectionError {
691
- return new OutputTextTrustProjectionError(
692
- schemaPath,
693
- new OutputTextTrustProjectionMarkerError(schemaPath),
694
- options.operationId,
695
- options.eventName,
696
- );
697
- }
698
-
699
- function resolveExpectedProjectedSchema(
700
- root: Record<string, unknown>,
701
- expectedPath: string,
702
- expectedMarkerId: string,
703
- markers: WeakMap<Record<string, unknown>, OutputTextTrustProjectionMarker>,
704
- options: DescribeSchemaOptions,
705
- ): Record<string, unknown> | undefined {
706
- let target: unknown = root;
707
- let finalContainer: unknown;
708
- let finalSegment: string | undefined;
709
- let finalArrayKeyword: string | undefined;
710
- if (expectedPath !== "#") {
711
- if (!expectedPath.startsWith("#/")) {
712
- throw new OutputTextTrustProjectionError(
713
- expectedPath,
714
- new TypeError("Expected projection path is not a local JSON Pointer."),
715
- options.operationId,
716
- options.eventName,
717
- );
718
- }
719
- const encodedSegments = expectedPath.slice(2).split("/");
720
- const decodedSegments = encodedSegments.map((segment) =>
721
- segment.replaceAll("~1", "/").replaceAll("~0", "~"),
722
- );
723
- finalArrayKeyword = decodedSegments.at(-2);
724
- for (const segment of decodedSegments) {
725
- const visitedReferences = new WeakSet<Record<string, unknown>>();
726
- while (isJsonSchemaNode(target) && !Object.hasOwn(target, segment)) {
727
- if (visitedReferences.has(target) || typeof target.$ref !== "string") return undefined;
728
- visitedReferences.add(target);
729
- target = resolveLocalJsonSchemaReference(root, target.$ref, expectedPath, options);
730
- }
731
- if (!target || typeof target !== "object" || !Object.hasOwn(target, segment))
732
- return undefined;
733
- finalContainer = target;
734
- finalSegment = segment;
735
- target = Reflect.get(target, segment);
736
- }
737
- }
738
- if (!isJsonSchemaNode(target)) return undefined;
739
-
740
- const visited = new WeakSet<Record<string, unknown>>();
741
- const findMarker = (candidate: Record<string, unknown>): Record<string, unknown> | undefined => {
742
- if (visited.has(candidate)) return undefined;
743
- visited.add(candidate);
744
- const markerId = candidate[OUTPUT_TEXT_TRUST_PROJECTION_MARKER_KEY];
745
- if (markerId === expectedMarkerId && markers.has(candidate)) return candidate;
746
- if (typeof candidate.$ref === "string") {
747
- const referenced = findMarker(
748
- resolveLocalJsonSchemaReference(root, candidate.$ref, expectedPath, options),
749
- );
750
- if (referenced) return referenced;
751
- }
752
- return undefined;
753
- };
754
- const exact = findMarker(target);
755
- if (exact) return exact;
756
- if (
757
- finalSegment !== undefined &&
758
- /^[0-9]+$/.test(finalSegment) &&
759
- finalArrayKeyword !== undefined &&
760
- Array.isArray(finalContainer)
761
- ) {
762
- const index = Number(finalSegment);
763
- const findNestedProjectedIndex = (
764
- candidate: Record<string, unknown>,
765
- ): Record<string, unknown> | undefined => {
766
- const nestedBranches = candidate[finalArrayKeyword];
767
- if (Array.isArray(nestedBranches) && isJsonSchemaNode(nestedBranches[index])) {
768
- const nested = findMarker(nestedBranches[index]);
769
- if (nested) return nested;
770
- }
771
- for (const keyword of ARRAY_SCHEMA_KEYWORDS) {
772
- const wrappers = candidate[keyword];
773
- if (!Array.isArray(wrappers)) continue;
774
- for (const wrapper of wrappers) {
775
- if (!isJsonSchemaNode(wrapper)) continue;
776
- const nested = findNestedProjectedIndex(wrapper);
777
- if (nested) return nested;
778
- }
779
- }
780
- return undefined;
781
- };
782
- for (const wrapper of finalContainer) {
783
- if (!isJsonSchemaNode(wrapper)) continue;
784
- const nested = findNestedProjectedIndex(wrapper);
785
- if (nested) return nested;
786
- }
67
+ function zodJsonSchema(schema: ZodType): JsonValue {
68
+ const jsonSchema = toJsonValue(z.toJSONSchema(schema));
69
+ if (jsonSchema === undefined) {
70
+ throw new TypeError("z.toJSONSchema() returned a non-JSON value");
787
71
  }
788
- return undefined;
789
- }
790
-
791
- function collectProjectedJsonSchemaNodes(root: Record<string, unknown>): ProjectedJsonSchemaNode[] {
792
- const nodes: ProjectedJsonSchemaNode[] = [];
793
- const visited = new WeakSet<Record<string, unknown>>();
794
- const collect = (schema: Record<string, unknown>, path: string): void => {
795
- if (visited.has(schema)) return;
796
- visited.add(schema);
797
- nodes.push({ path, schema });
798
- for (const [childPath, child] of projectedJsonSchemaChildEntries(schema, path, true)) {
799
- collect(child, childPath);
800
- }
801
- };
802
- collect(root, "#");
803
- return nodes;
804
- }
805
-
806
- function projectedJsonSchemaChildren(
807
- schema: Record<string, unknown>,
808
- includeDefinitions: boolean,
809
- ): Record<string, unknown>[] {
810
- return [...projectedJsonSchemaChildEntries(schema, "#", includeDefinitions)].map(
811
- ([, child]) => child,
812
- );
813
- }
814
-
815
- function* projectedJsonSchemaChildEntries(
816
- schema: Record<string, unknown>,
817
- path: string,
818
- includeDefinitions: boolean,
819
- ): Generator<readonly [string, Record<string, unknown>]> {
820
- for (const keyword of SINGLE_SCHEMA_KEYWORDS) {
821
- const child = schema[keyword];
822
- if (isJsonSchemaNode(child)) yield [appendJsonSchemaPath(path, keyword), child];
823
- if (keyword === "items" && Array.isArray(child)) {
824
- for (const [index, item] of child.entries()) {
825
- if (isJsonSchemaNode(item)) {
826
- yield [appendJsonSchemaPath(appendJsonSchemaPath(path, keyword), index), item];
827
- }
828
- }
829
- }
830
- }
831
- for (const keyword of ARRAY_SCHEMA_KEYWORDS) {
832
- const children = schema[keyword];
833
- if (!Array.isArray(children)) continue;
834
- for (const [index, child] of children.entries()) {
835
- if (isJsonSchemaNode(child)) {
836
- yield [appendJsonSchemaPath(appendJsonSchemaPath(path, keyword), index), child];
837
- }
838
- }
839
- }
840
- for (const keyword of MAP_SCHEMA_KEYWORDS) {
841
- if (!includeDefinitions && (keyword === "$defs" || keyword === "definitions")) continue;
842
- const children = schema[keyword];
843
- if (!isJsonSchemaNode(children)) continue;
844
- for (const [key, child] of Object.entries(children)) {
845
- if (isJsonSchemaNode(child)) {
846
- yield [appendJsonSchemaPath(appendJsonSchemaPath(path, keyword), key), child];
847
- }
848
- }
849
- }
850
- }
851
-
852
- function resolveLocalJsonSchemaReference(
853
- root: Record<string, unknown>,
854
- reference: string,
855
- schemaPath: string,
856
- options: DescribeSchemaOptions,
857
- ): Record<string, unknown> {
858
- let target: unknown = root;
859
- if (reference !== "#") {
860
- if (!reference.startsWith("#/")) {
861
- throw new OutputTextTrustProjectionError(
862
- schemaPath,
863
- new TypeError(`Unsupported non-local JSON Schema reference: ${reference}`),
864
- options.operationId,
865
- options.eventName,
866
- );
867
- }
868
- for (const encodedSegment of reference.slice(2).split("/")) {
869
- const segment = encodedSegment.replaceAll("~1", "/").replaceAll("~0", "~");
870
- if (!target || typeof target !== "object" || !Object.hasOwn(target, segment)) {
871
- throw new OutputTextTrustProjectionError(
872
- schemaPath,
873
- new TypeError(`Unresolvable local JSON Schema reference: ${reference}`),
874
- options.operationId,
875
- options.eventName,
876
- );
877
- }
878
- target = Reflect.get(target, segment);
879
- }
880
- }
881
- if (!isJsonSchemaNode(target)) {
882
- throw new OutputTextTrustProjectionError(
883
- schemaPath,
884
- new TypeError(`JSON Schema reference does not resolve to a schema object: ${reference}`),
885
- options.operationId,
886
- options.eventName,
887
- );
888
- }
889
- return target;
890
- }
891
-
892
- function isJsonSchemaNode(value: unknown): value is Record<string, unknown> {
893
- return value !== null && typeof value === "object" && !Array.isArray(value);
894
- }
895
-
896
- function requireNodePath(
897
- nodes: readonly ProjectedJsonSchemaNode[],
898
- target: Record<string, unknown>,
899
- options: DescribeSchemaOptions,
900
- ): string {
901
- const path = nodes.find(({ schema }) => schema === target)?.path;
902
- if (path !== undefined) return path;
903
- throw new OutputTextTrustProjectionError(
904
- "#",
905
- new TypeError("Projected JSON Schema node is missing from the collected node index."),
906
- options.operationId,
907
- options.eventName,
908
- );
909
- }
910
-
911
- function appendJsonSchemaPath(path: string, segment: string | number): string {
912
- return `${path}/${String(segment).replaceAll("~", "~0").replaceAll("/", "~1")}`;
913
- }
914
-
915
- function jsonSchemaPath(path: readonly (string | number)[]): string {
916
- if (path.length === 0) return "$";
917
- return `#/${path
918
- .map((segment) => String(segment).replaceAll("~", "~0").replaceAll("/", "~1"))
919
- .join("/")}`;
920
- }
921
-
922
- function projectedJsonSchemaPath(path: readonly (string | number)[]): string {
923
- return path.length === 0 ? "#" : jsonSchemaPath(path);
72
+ return jsonSchema;
924
73
  }
925
74
 
926
75
  function getSchemaTypeName(schema: SchemaLike): string | undefined {