@knitli/openapi-mcp 0.0.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.
Files changed (116) hide show
  1. package/LICENSE-APACHE +202 -0
  2. package/LICENSE-MIT +21 -0
  3. package/README.md +362 -0
  4. package/dist/cli.d.ts +2 -0
  5. package/dist/cli.js +249 -0
  6. package/dist/compile.d.ts +14 -0
  7. package/dist/compile.js +132 -0
  8. package/dist/compiler.d.ts +13 -0
  9. package/dist/compiler.js +7 -0
  10. package/dist/conformance/fixtures.d.ts +24 -0
  11. package/dist/conformance/fixtures.js +119 -0
  12. package/dist/conformance/index.d.ts +2 -0
  13. package/dist/conformance/index.js +2 -0
  14. package/dist/conformance/runtime-suite.d.ts +31 -0
  15. package/dist/conformance/runtime-suite.js +343 -0
  16. package/dist/index.d.ts +1 -0
  17. package/dist/index.js +1 -0
  18. package/dist/load.d.ts +31 -0
  19. package/dist/load.js +20 -0
  20. package/dist/operations.d.ts +27 -0
  21. package/dist/operations.js +231 -0
  22. package/dist/permissions.d.ts +31 -0
  23. package/dist/permissions.js +89 -0
  24. package/dist/release/compile-release.d.ts +33 -0
  25. package/dist/release/compile-release.js +1620 -0
  26. package/dist/release/load-v4.d.ts +29 -0
  27. package/dist/release/load-v4.js +297 -0
  28. package/dist/release/manifest-builder.d.ts +19 -0
  29. package/dist/release/manifest-builder.js +28 -0
  30. package/dist/release/publish.d.ts +45 -0
  31. package/dist/release/publish.js +346 -0
  32. package/dist/release/reference-map.d.ts +53 -0
  33. package/dist/release/reference-map.js +347 -0
  34. package/dist/release/schema-v4.d.ts +5 -0
  35. package/dist/release/schema-v4.js +91 -0
  36. package/dist/runtime/action-permit.d.ts +11 -0
  37. package/dist/runtime/action-permit.js +1 -0
  38. package/dist/runtime/classify.d.ts +16 -0
  39. package/dist/runtime/classify.js +279 -0
  40. package/dist/runtime/credential-binding.d.ts +3 -0
  41. package/dist/runtime/credential-binding.js +101 -0
  42. package/dist/runtime/digest.d.ts +8 -0
  43. package/dist/runtime/digest.js +80 -0
  44. package/dist/runtime/errors.d.ts +14 -0
  45. package/dist/runtime/errors.js +15 -0
  46. package/dist/runtime/index.d.ts +20 -0
  47. package/dist/runtime/index.js +14 -0
  48. package/dist/runtime/manifest.d.ts +33 -0
  49. package/dist/runtime/manifest.js +489 -0
  50. package/dist/runtime/prepared-call.d.ts +39 -0
  51. package/dist/runtime/prepared-call.js +566 -0
  52. package/dist/runtime/references.d.ts +7 -0
  53. package/dist/runtime/references.js +197 -0
  54. package/dist/runtime/runtime.d.ts +25 -0
  55. package/dist/runtime/runtime.js +1272 -0
  56. package/dist/runtime/schema-keywords.d.ts +2 -0
  57. package/dist/runtime/schema-keywords.js +30 -0
  58. package/dist/runtime/schema-resolver.d.ts +10 -0
  59. package/dist/runtime/schema-resolver.js +237 -0
  60. package/dist/runtime/serialize.d.ts +12 -0
  61. package/dist/runtime/serialize.js +877 -0
  62. package/dist/runtime/store.d.ts +52 -0
  63. package/dist/runtime/store.js +582 -0
  64. package/dist/runtime/strict-json.d.ts +19 -0
  65. package/dist/runtime/strict-json.js +539 -0
  66. package/dist/runtime/types.d.ts +457 -0
  67. package/dist/runtime/types.js +1 -0
  68. package/dist/runtime/verify-record.d.ts +6 -0
  69. package/dist/runtime/verify-record.js +492 -0
  70. package/dist/runtime/versions.d.ts +63 -0
  71. package/dist/runtime/versions.js +86 -0
  72. package/dist/safety.d.ts +28 -0
  73. package/dist/safety.js +60 -0
  74. package/dist/schema.d.ts +5 -0
  75. package/dist/schema.js +52 -0
  76. package/dist/schemas.d.ts +9 -0
  77. package/dist/schemas.js +16 -0
  78. package/dist/sign.d.ts +20 -0
  79. package/dist/sign.js +55 -0
  80. package/dist/sqlite/auth.d.ts +33 -0
  81. package/dist/sqlite/auth.js +973 -0
  82. package/dist/sqlite/catalog-store.d.ts +20 -0
  83. package/dist/sqlite/catalog-store.js +370 -0
  84. package/dist/sqlite/destination-guard.d.ts +24 -0
  85. package/dist/sqlite/destination-guard.js +140 -0
  86. package/dist/sqlite/generation-store.d.ts +12 -0
  87. package/dist/sqlite/generation-store.js +721 -0
  88. package/dist/sqlite/guarded-fetch.d.ts +24 -0
  89. package/dist/sqlite/guarded-fetch.js +626 -0
  90. package/dist/sqlite/index.d.ts +5 -0
  91. package/dist/sqlite/index.js +5 -0
  92. package/dist/sqlite/oauth-resource.d.ts +2 -0
  93. package/dist/sqlite/oauth-resource.js +15 -0
  94. package/dist/sqlite/release-completion.d.ts +5 -0
  95. package/dist/sqlite/release-completion.js +80 -0
  96. package/dist/stdio/action-broker.d.ts +15 -0
  97. package/dist/stdio/action-broker.js +136 -0
  98. package/dist/stdio/authorizer.d.ts +30 -0
  99. package/dist/stdio/authorizer.js +484 -0
  100. package/dist/stdio/config.d.ts +77 -0
  101. package/dist/stdio/config.js +144 -0
  102. package/dist/stdio/exact-policy.d.ts +41 -0
  103. package/dist/stdio/exact-policy.js +361 -0
  104. package/dist/stdio/index.d.ts +6 -0
  105. package/dist/stdio/index.js +148 -0
  106. package/dist/stdio/render.d.ts +8 -0
  107. package/dist/stdio/render.js +253 -0
  108. package/dist/stdio/request-state.d.ts +18 -0
  109. package/dist/stdio/request-state.js +79 -0
  110. package/dist/stdio/search.d.ts +10 -0
  111. package/dist/stdio/search.js +76 -0
  112. package/dist/stdio/server.d.ts +29 -0
  113. package/dist/stdio/server.js +309 -0
  114. package/dist/types.d.ts +32 -0
  115. package/dist/types.js +1 -0
  116. package/package.json +131 -0
@@ -0,0 +1,1620 @@
1
+ import { createHash } from "node:crypto";
2
+ import { constants } from "node:fs";
3
+ import { mkdir, mkdtemp, open } from "node:fs/promises";
4
+ import { extname, join, resolve } from "node:path";
5
+ import { DatabaseSync } from "node:sqlite";
6
+ import { canonicalLocalPointer, resolveCanonicalPointer, splitWords, } from "../operations.js";
7
+ import { buildPermissionIndex, lookupPermissions, } from "../permissions.js";
8
+ import { sha256 } from "../runtime/digest.js";
9
+ import { admitManifest } from "../runtime/manifest.js";
10
+ import { parseTypedRecordId } from "../runtime/references.js";
11
+ import { schemaChildKind } from "../runtime/schema-keywords.js";
12
+ import { canonicalJson, canonicalJsonBounded, parseJsonStrict, } from "../runtime/strict-json.js";
13
+ import { parseOperationPathTemplateV4, verifyStoredRecord, } from "../runtime/verify-record.js";
14
+ import { DEFAULT_RUNTIME_LIMITS, MAX_OPERATION_TAG_BYTES, MAX_OPERATION_TAG_BYTES_TOTAL, MAX_OPERATION_TAGS, } from "../runtime/versions.js";
15
+ import { classifySafety, riskFor } from "../safety.js";
16
+ import { deriveReleasePublicKeyV4 } from "../sign.js";
17
+ import { DEFAULT_COMPILER_LIMITS, parseSpecBytesV4, readFileBoundedV4, } from "./load-v4.js";
18
+ import { buildManifestEnvelopeV4, buildReleaseManifestV4, } from "./manifest-builder.js";
19
+ import { capturePathIdentity, cleanupOwnedStage, pathIdentityFromStats, registerCompiledRelease, samePathIdentity, } from "./publish.js";
20
+ import { normalizeGraphUri, normalizeHttpsUri, ReferenceGraphV4, referenceGraphDigest, sourceUriFromLabel, } from "./reference-map.js";
21
+ import { createReleaseSchemaV4, populateOperationsFtsV4 } from "./schema-v4.js";
22
+ const COMPILER_VERSION = "0.1.0";
23
+ const HTTP_METHODS = [
24
+ "get",
25
+ "head",
26
+ "post",
27
+ "put",
28
+ "patch",
29
+ "delete",
30
+ "options",
31
+ "trace",
32
+ ];
33
+ function pathItemEntries(paths) {
34
+ return Object.entries(paths).filter(([path]) => !path.startsWith("x-"));
35
+ }
36
+ const PARAMETER_LOCATIONS = [
37
+ "path",
38
+ "query",
39
+ "header",
40
+ "cookie",
41
+ ];
42
+ const PARAMETER_STYLES = [
43
+ "matrix",
44
+ "label",
45
+ "form",
46
+ "simple",
47
+ "spaceDelimited",
48
+ "pipeDelimited",
49
+ "deepObject",
50
+ ];
51
+ const reservedComponentPrefix = "__openapi_mcp_v4_";
52
+ function encodePointerToken(value) {
53
+ return value.replace(/~/g, "~0").replace(/\//g, "~1");
54
+ }
55
+ function decodePointerToken(value) {
56
+ return value.replace(/~1/g, "/").replace(/~0/g, "~");
57
+ }
58
+ function isSchemaDescendantPath(tokens) {
59
+ let index = 0;
60
+ while (index < tokens.length) {
61
+ const kind = schemaChildKind(decodePointerToken(tokens[index]));
62
+ if (kind === "single") {
63
+ index += 1;
64
+ continue;
65
+ }
66
+ if (kind === "map") {
67
+ if (index + 1 >= tokens.length)
68
+ return false;
69
+ index += 2;
70
+ continue;
71
+ }
72
+ if (kind === "array") {
73
+ if (index + 1 >= tokens.length ||
74
+ !/^(?:0|[1-9][0-9]*)$/.test(decodePointerToken(tokens[index + 1])))
75
+ return false;
76
+ index += 2;
77
+ continue;
78
+ }
79
+ return false;
80
+ }
81
+ return true;
82
+ }
83
+ function exactObject(value, label) {
84
+ if (typeof value !== "object" || value === null || Array.isArray(value))
85
+ throw new Error(`${label} must be an object`);
86
+ return value;
87
+ }
88
+ function canonicalMediaType(value) {
89
+ const normalized = value.toLowerCase();
90
+ if (!/^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+$/.test(normalized)) {
91
+ throw new Error(`media type "${value}" is not a type/subtype without parameters`);
92
+ }
93
+ return normalized;
94
+ }
95
+ function canonicalOrigin(value) {
96
+ if (typeof value !== "string")
97
+ throw new Error("document declares no servers[0].url");
98
+ const url = new URL(value);
99
+ if (url.protocol !== "https:" ||
100
+ url.username ||
101
+ url.password ||
102
+ url.search ||
103
+ url.hash)
104
+ throw new Error("operation server must be credential-free HTTPS");
105
+ if (url.pathname !== "/")
106
+ throw new Error("operation server must be an exact HTTPS origin");
107
+ return url.origin;
108
+ }
109
+ function truncateSummary(value, maximum = 600) {
110
+ let end = 0;
111
+ for (const character of value) {
112
+ if (end + character.length > maximum)
113
+ break;
114
+ end += character.length;
115
+ }
116
+ return value.slice(0, end);
117
+ }
118
+ function normalizeOperationTags(value) {
119
+ if (value === undefined)
120
+ return [];
121
+ if (!Array.isArray(value) || value.length > MAX_OPERATION_TAGS)
122
+ throw new Error("operation tags are invalid");
123
+ const tags = [];
124
+ const seen = new Set();
125
+ let totalBytes = 0;
126
+ for (const tag of value) {
127
+ if (typeof tag !== "string" ||
128
+ tag.length === 0 ||
129
+ tag.length > MAX_OPERATION_TAG_BYTES)
130
+ throw new Error("operation tags are invalid");
131
+ const tagBytes = new TextEncoder().encode(tag).byteLength;
132
+ if (tagBytes > MAX_OPERATION_TAG_BYTES ||
133
+ totalBytes + tagBytes > MAX_OPERATION_TAG_BYTES_TOTAL ||
134
+ seen.has(tag))
135
+ throw new Error("operation tags are invalid");
136
+ seen.add(tag);
137
+ totalBytes += tagBytes;
138
+ tags.push(tag);
139
+ }
140
+ return tags;
141
+ }
142
+ function checkedRecordJson(record, limits) {
143
+ try {
144
+ return canonicalJsonBounded(record, {
145
+ maxBytes: limits.maxRecordBytes,
146
+ maxDepth: limits.maxJsonDepth,
147
+ maxNodes: limits.maxDocumentNodes,
148
+ });
149
+ }
150
+ catch (error) {
151
+ throw new Error("logical record exceeds record limit or cannot be canonicalized", { cause: error });
152
+ }
153
+ }
154
+ function stableSort(values, key) {
155
+ return values.sort((left, right) => key(left) < key(right) ? -1 : key(left) > key(right) ? 1 : 0);
156
+ }
157
+ class SchemaMaterializer {
158
+ api;
159
+ sourceUri;
160
+ rootDocument;
161
+ graph;
162
+ limits;
163
+ records = new Map();
164
+ #addresses = new Map();
165
+ #resources = new Map();
166
+ #resourcesByDocument = new Map();
167
+ #resourceRegistrations = [];
168
+ #maxSchemaReferenceWork;
169
+ #schemaReferenceWork = 0;
170
+ constructor(api, sourceUri, rootDocument, graph, limits) {
171
+ this.api = api;
172
+ this.sourceUri = sourceUri;
173
+ this.rootDocument = rootDocument;
174
+ this.graph = graph;
175
+ this.limits = limits;
176
+ const proportionalLimit = limits.maxSchemas >
177
+ Math.floor(Number.MAX_SAFE_INTEGER / limits.maxJsonDepth)
178
+ ? Number.MAX_SAFE_INTEGER
179
+ : limits.maxSchemas * limits.maxJsonDepth;
180
+ this.#maxSchemaReferenceWork = Math.min(limits.maxDocumentNodes, proportionalLimit);
181
+ this.#registerResource({
182
+ documentUri: sourceUri,
183
+ referenceBaseUri: sourceUri,
184
+ resourceUri: sourceUri,
185
+ resourcePointer: "#",
186
+ resourceValue: rootDocument,
187
+ pointer: "#",
188
+ document: rootDocument,
189
+ value: rootDocument,
190
+ });
191
+ }
192
+ #registerResource(resource) {
193
+ if (!this.#resources.has(resource.resourceUri))
194
+ this.#resourceRegistrations.push(resource.resourceUri);
195
+ this.#resources.set(resource.resourceUri, resource);
196
+ let documentResources = this.#resourcesByDocument.get(resource.documentUri);
197
+ if (!documentResources) {
198
+ documentResources = new Map();
199
+ this.#resourcesByDocument.set(resource.documentUri, documentResources);
200
+ }
201
+ documentResources.set(resource.resourcePointer, resource);
202
+ }
203
+ #scopeTarget(target) {
204
+ let nearest = target;
205
+ const documentResources = this.#resourcesByDocument.get(target.documentUri);
206
+ if (!documentResources)
207
+ return nearest;
208
+ let pointer = target.pointer;
209
+ while (true) {
210
+ const resource = documentResources.get(pointer);
211
+ if (resource &&
212
+ resource.resourcePointer.length > nearest.resourcePointer.length) {
213
+ nearest = {
214
+ ...resource,
215
+ pointer: target.pointer,
216
+ value: target.value,
217
+ };
218
+ }
219
+ if (pointer === "#")
220
+ break;
221
+ const separator = pointer.lastIndexOf("/");
222
+ pointer = separator <= 1 ? "#" : pointer.slice(0, separator);
223
+ }
224
+ return nearest;
225
+ }
226
+ #indexEnclosingResources(target) {
227
+ const tokens = target.pointer === "#" ? [] : target.pointer.slice(2).split("/");
228
+ if (tokens.length > this.limits.maxJsonDepth)
229
+ throw new Error("schema depth exceeds limit");
230
+ for (let length = 0; length <= tokens.length; length += 1) {
231
+ if (!isSchemaDescendantPath(tokens.slice(length)))
232
+ continue;
233
+ const pointer = length === 0 ? "#" : `#/${tokens.slice(0, length).join("/")}`;
234
+ const value = resolveCanonicalPointer(target.document, pointer);
235
+ if (typeof value !== "object" ||
236
+ value === null ||
237
+ Array.isArray(value) ||
238
+ !Object.hasOwn(value, "$id"))
239
+ continue;
240
+ const candidate = this.#scopeTarget({
241
+ documentUri: target.documentUri,
242
+ referenceBaseUri: target.documentUri,
243
+ resourceUri: target.documentUri,
244
+ resourcePointer: "#",
245
+ resourceValue: target.document,
246
+ pointer,
247
+ document: target.document,
248
+ value,
249
+ });
250
+ this.schemaAddress(value, candidate);
251
+ }
252
+ }
253
+ #fromResource(resource, fragment, schemaContext) {
254
+ const canonical = canonicalLocalPointer(fragment);
255
+ const pointer = `${resource.resourcePointer}${canonical.slice(1)}`;
256
+ const target = {
257
+ ...resource,
258
+ pointer,
259
+ value: resolveCanonicalPointer(resource.resourceValue, canonical),
260
+ };
261
+ if (schemaContext)
262
+ this.#indexEnclosingResources(target);
263
+ return schemaContext ? this.#scopeTarget(target) : target;
264
+ }
265
+ async resolveReference(rawRef, from, depth, schemaContext) {
266
+ if (Buffer.byteLength(rawRef) > this.limits.maxRefLength)
267
+ throw new Error("reference exceeds ref length limit");
268
+ if (depth > this.limits.maxJsonDepth)
269
+ throw new Error("reference depth exceeds limit");
270
+ const hash = rawRef.indexOf("#");
271
+ const documentPart = hash < 0 ? rawRef : rawRef.slice(0, hash);
272
+ const rawPointer = hash < 0 ? "#" : `#${rawRef.slice(hash + 1)}`;
273
+ const pointer = canonicalLocalPointer(rawPointer);
274
+ if (documentPart === "") {
275
+ return this.#fromResource(from, rawPointer, schemaContext);
276
+ }
277
+ const resourceUri = normalizeGraphUri(documentPart, from.referenceBaseUri);
278
+ const resource = this.#resources.get(resourceUri);
279
+ if (resource && (schemaContext || !resource.declaredResource))
280
+ return this.#fromResource(resource, rawPointer, schemaContext);
281
+ const external = await this.graph.resolve(rawRef, from.referenceBaseUri, depth);
282
+ const existing = this.#resources.get(external.uri);
283
+ if (existing && existing.document !== external.document)
284
+ throw new Error("embedded resource URI has conflicting physical identity");
285
+ if (!existing)
286
+ this.#registerResource({
287
+ documentUri: external.uri,
288
+ referenceBaseUri: external.uri,
289
+ resourceUri: external.uri,
290
+ resourcePointer: "#",
291
+ resourceValue: external.document,
292
+ pointer: "#",
293
+ document: external.document,
294
+ value: external.document,
295
+ });
296
+ const target = {
297
+ documentUri: external.uri,
298
+ referenceBaseUri: external.uri,
299
+ resourceUri: external.uri,
300
+ resourcePointer: "#",
301
+ resourceValue: external.document,
302
+ pointer,
303
+ document: external.document,
304
+ value: resolveCanonicalPointer(external.document, pointer),
305
+ };
306
+ if (schemaContext)
307
+ this.#indexEnclosingResources(target);
308
+ return schemaContext ? this.#scopeTarget(target) : target;
309
+ }
310
+ #rootComponentName(address) {
311
+ if (address.documentUri !== this.sourceUri)
312
+ return null;
313
+ const prefix = "#/components/schemas/";
314
+ if (!address.pointer.startsWith(prefix))
315
+ return null;
316
+ const tail = address.pointer.slice(prefix.length);
317
+ if (tail.includes("/"))
318
+ return null;
319
+ return tail.replace(/~1/g, "/").replace(/~0/g, "~");
320
+ }
321
+ async #idFor(address, authoredName) {
322
+ const componentName = authoredName ?? this.#rootComponentName(address);
323
+ if (componentName !== null && componentName !== undefined) {
324
+ if (componentName.startsWith(reservedComponentPrefix))
325
+ throw new Error("authored schema component uses the reserved v4 namespace");
326
+ const id = `schema:${this.api}:#/components/schemas/${encodePointerToken(componentName)}`;
327
+ const parsed = parseTypedRecordId(id);
328
+ if (!parsed.startsWith("schema:"))
329
+ throw new Error("schema identifier is invalid");
330
+ return parsed;
331
+ }
332
+ const digest = await sha256("knitli.openapi-mcp.schema-materialization-id.v1", {
333
+ api: this.api,
334
+ documentUri: address.documentUri,
335
+ pointer: address.pointer,
336
+ });
337
+ return `schema:${this.api}:#/components/schemas/${reservedComponentPrefix}${digest}`;
338
+ }
339
+ async materialize(address, authoredName, referenceDepth = 0) {
340
+ this.indexResources(address.value, address);
341
+ const id = await this.#idFor(address, authoredName);
342
+ const identity = `${address.documentUri}\0${address.pointer}`;
343
+ const prior = this.#addresses.get(id);
344
+ if (prior && prior !== identity)
345
+ throw new Error("synthetic schema identifier collision");
346
+ if (this.records.has(id))
347
+ return id;
348
+ if (this.records.size >= this.limits.maxSchemas)
349
+ throw new Error("Schemas exceed maxSchemas limit");
350
+ this.#addresses.set(id, identity);
351
+ this.records.set(id, { id, schema: Object.create(null) });
352
+ const schema = await this.#rewriteSubschema(address.value, address, 0, referenceDepth);
353
+ const record = { id, schema };
354
+ checkedRecordJson(record, this.limits);
355
+ this.records.set(id, record);
356
+ return id;
357
+ }
358
+ async materializeUse(address) {
359
+ if (typeof address.value === "boolean")
360
+ return this.materialize(address);
361
+ const object = exactObject(address.value, "schema use");
362
+ if (Object.keys(object).length === 1 && typeof object.$ref === "string") {
363
+ return this.materialize(await this.resolveReference(object.$ref, address, 1, true), undefined, 1);
364
+ }
365
+ return this.materialize(address);
366
+ }
367
+ schemaAddress(schema, address) {
368
+ if (schema.$id === undefined)
369
+ return address;
370
+ if (address.declaredResource &&
371
+ address.resourcePointer === address.pointer &&
372
+ address.resourceValue === schema)
373
+ return address;
374
+ if (typeof schema.$id !== "string")
375
+ throw new Error("schema $id must be a string");
376
+ const resourceUri = normalizeGraphUri(schema.$id, address.referenceBaseUri);
377
+ const resource = {
378
+ ...address,
379
+ referenceBaseUri: resourceUri,
380
+ resourceUri,
381
+ resourcePointer: address.pointer,
382
+ resourceValue: schema,
383
+ declaredResource: true,
384
+ };
385
+ const prior = this.#resources.get(resourceUri);
386
+ if (prior &&
387
+ (prior.documentUri !== address.documentUri ||
388
+ prior.pointer !== address.pointer))
389
+ throw new Error("schema $id duplicates an embedded resource URI");
390
+ this.#registerResource(resource);
391
+ return resource;
392
+ }
393
+ indexResources(value, address, depth = 0) {
394
+ if (depth > this.limits.maxJsonDepth)
395
+ throw new Error("schema depth exceeds limit");
396
+ if (typeof value === "boolean")
397
+ return;
398
+ const schema = exactObject(value, "schema");
399
+ const scoped = this.schemaAddress(schema, address);
400
+ for (const [key, child] of Object.entries(schema)) {
401
+ const kind = schemaChildKind(key);
402
+ if (kind === "single")
403
+ this.indexResources(child, {
404
+ ...scoped,
405
+ pointer: `${address.pointer}/${encodePointerToken(key)}`,
406
+ value: child,
407
+ }, depth + 1);
408
+ else if (kind === "map" &&
409
+ typeof child === "object" &&
410
+ child !== null &&
411
+ !Array.isArray(child))
412
+ for (const [name, nested] of Object.entries(child))
413
+ this.indexResources(nested, {
414
+ ...scoped,
415
+ pointer: `${address.pointer}/${encodePointerToken(key)}/${encodePointerToken(name)}`,
416
+ value: nested,
417
+ }, depth + 1);
418
+ else if (kind === "array" && Array.isArray(child))
419
+ child.forEach((nested, index) => {
420
+ this.indexResources(nested, {
421
+ ...scoped,
422
+ pointer: `${address.pointer}/${encodePointerToken(key)}/${index}`,
423
+ value: nested,
424
+ }, depth + 1);
425
+ });
426
+ }
427
+ }
428
+ #collectSchemaReferences(value, address, referenceDepth, output, depth = 0) {
429
+ if (depth > this.limits.maxJsonDepth)
430
+ throw new Error("schema depth exceeds limit");
431
+ if (typeof value === "boolean")
432
+ return;
433
+ const schema = exactObject(value, "schema");
434
+ const scoped = this.schemaAddress(schema, address);
435
+ if (Object.hasOwn(schema, "$ref")) {
436
+ if (typeof schema.$ref !== "string")
437
+ throw new Error("schema $ref must be a string");
438
+ const hash = schema.$ref.indexOf("#");
439
+ const documentPart = hash < 0 ? schema.$ref : schema.$ref.slice(0, hash);
440
+ if (this.#schemaReferenceWork >= this.#maxSchemaReferenceWork)
441
+ throw new Error("schema reference work exceeds limit");
442
+ this.#schemaReferenceWork += 1;
443
+ output.push({
444
+ rawRef: schema.$ref,
445
+ from: scoped,
446
+ referenceDepth: referenceDepth + 1,
447
+ resourceUri: documentPart === ""
448
+ ? undefined
449
+ : normalizeGraphUri(documentPart, scoped.referenceBaseUri),
450
+ });
451
+ }
452
+ for (const [key, child] of Object.entries(schema)) {
453
+ const kind = schemaChildKind(key);
454
+ const childAddress = {
455
+ ...scoped,
456
+ pointer: `${address.pointer}/${encodePointerToken(key)}`,
457
+ value: child,
458
+ };
459
+ if (kind === "single") {
460
+ this.#collectSchemaReferences(child, childAddress, referenceDepth, output, depth + 1);
461
+ }
462
+ else if (kind === "map" &&
463
+ typeof child === "object" &&
464
+ child !== null &&
465
+ !Array.isArray(child)) {
466
+ for (const [name, nested] of Object.entries(child))
467
+ this.#collectSchemaReferences(nested, {
468
+ ...childAddress,
469
+ pointer: `${childAddress.pointer}/${encodePointerToken(name)}`,
470
+ value: nested,
471
+ }, referenceDepth, output, depth + 1);
472
+ }
473
+ else if (kind === "array" && Array.isArray(child)) {
474
+ child.forEach((nested, index) => {
475
+ this.#collectSchemaReferences(nested, {
476
+ ...childAddress,
477
+ pointer: `${childAddress.pointer}/${index}`,
478
+ value: nested,
479
+ }, referenceDepth, output, depth + 1);
480
+ });
481
+ }
482
+ }
483
+ }
484
+ async preIndexResources(roots) {
485
+ const work = [];
486
+ const seenEdges = new Set();
487
+ const seenTargets = new Set();
488
+ const waitingByResource = new Map();
489
+ let workIndex = 0;
490
+ let registrationIndex = 0;
491
+ const enqueue = (references) => {
492
+ for (const reference of references) {
493
+ const identity = `${reference.from.documentUri}\0${reference.from.pointer}\0${reference.from.referenceBaseUri}\0${reference.rawRef}`;
494
+ if (seenEdges.has(identity))
495
+ continue;
496
+ seenEdges.add(identity);
497
+ work.push(reference);
498
+ }
499
+ };
500
+ const wakeRegisteredResources = () => {
501
+ while (registrationIndex < this.#resourceRegistrations.length) {
502
+ const uri = this.#resourceRegistrations[registrationIndex++];
503
+ const waiting = waitingByResource.get(uri);
504
+ if (!waiting)
505
+ continue;
506
+ waitingByResource.delete(uri);
507
+ work.push(...waiting);
508
+ }
509
+ };
510
+ for (const root of roots) {
511
+ const discovered = [];
512
+ this.indexResources(root.value, root);
513
+ this.#collectSchemaReferences(root.value, root, 0, discovered);
514
+ enqueue(discovered);
515
+ seenTargets.add(`${root.documentUri}\0${root.pointer}\0${root.referenceBaseUri}`);
516
+ }
517
+ wakeRegisteredResources();
518
+ while (workIndex < work.length) {
519
+ const reference = work[workIndex++];
520
+ let target;
521
+ try {
522
+ target = await this.resolveReference(reference.rawRef, reference.from, reference.referenceDepth, true);
523
+ }
524
+ catch (error) {
525
+ if (error instanceof Error &&
526
+ error.message ===
527
+ "external reference is absent from the reference map" &&
528
+ reference.resourceUri !== undefined) {
529
+ const waiting = waitingByResource.get(reference.resourceUri);
530
+ if (waiting)
531
+ waiting.push(reference);
532
+ else
533
+ waitingByResource.set(reference.resourceUri, [reference]);
534
+ continue;
535
+ }
536
+ throw error;
537
+ }
538
+ wakeRegisteredResources();
539
+ const identity = `${target.documentUri}\0${target.pointer}\0${target.referenceBaseUri}`;
540
+ if (seenTargets.has(identity))
541
+ continue;
542
+ seenTargets.add(identity);
543
+ this.indexResources(target.value, target);
544
+ wakeRegisteredResources();
545
+ const discovered = [];
546
+ this.#collectSchemaReferences(target.value, target, reference.referenceDepth, discovered);
547
+ enqueue(discovered);
548
+ }
549
+ const unresolved = waitingByResource.values().next().value?.[0];
550
+ if (unresolved) {
551
+ await this.resolveReference(unresolved.rawRef, unresolved.from, unresolved.referenceDepth, true);
552
+ throw new Error("unreachable reference pre-index state");
553
+ }
554
+ }
555
+ #opaqueJson(value) {
556
+ let canonical;
557
+ try {
558
+ canonical = canonicalJsonBounded(value, {
559
+ maxBytes: this.limits.maxRecordBytes,
560
+ maxDepth: this.limits.maxJsonDepth,
561
+ maxNodes: this.limits.maxDocumentNodes,
562
+ });
563
+ }
564
+ catch (error) {
565
+ throw new Error("schema annotation cannot be canonicalized", {
566
+ cause: error,
567
+ });
568
+ }
569
+ return parseJsonStrict(canonical, {
570
+ maxBytes: this.limits.maxRecordBytes,
571
+ maxDepth: this.limits.maxJsonDepth,
572
+ maxKeys: this.limits.maxDocumentKeys,
573
+ });
574
+ }
575
+ async #rewriteSchema(value, address, depth, referenceDepth) {
576
+ if (depth > this.limits.maxJsonDepth)
577
+ throw new Error("schema depth exceeds limit");
578
+ const object = exactObject(value, "schema");
579
+ const scopedAddress = this.schemaAddress(object, address);
580
+ for (const keyword of [
581
+ "$dynamicRef",
582
+ "$recursiveRef",
583
+ "$anchor",
584
+ "$dynamicAnchor",
585
+ ]) {
586
+ if (Object.hasOwn(object, keyword))
587
+ throw new Error(`${keyword} is unsupported`);
588
+ }
589
+ const properties = object.properties;
590
+ if (properties &&
591
+ Object.keys(exactObject(properties, "schema properties")).length >
592
+ this.limits.maxPropertiesPerSchema) {
593
+ throw new Error("Properties exceed maxPropertiesPerSchema limit");
594
+ }
595
+ const output = Object.create(null);
596
+ for (const [key, child] of Object.entries(object)) {
597
+ if (key === "$ref") {
598
+ if (typeof child !== "string")
599
+ throw new Error("schema $ref must be a string");
600
+ const target = await this.resolveReference(child, scopedAddress, referenceDepth + 1, true);
601
+ output.$ref = await this.materialize(target, undefined, referenceDepth + 1);
602
+ continue;
603
+ }
604
+ const kind = schemaChildKind(key);
605
+ const childAddress = {
606
+ ...scopedAddress,
607
+ pointer: `${address.pointer}/${encodePointerToken(key)}`,
608
+ value: child,
609
+ };
610
+ if (kind === "single") {
611
+ output[key] = await this.#rewriteSubschema(child, childAddress, depth + 1, referenceDepth);
612
+ }
613
+ else if (kind === "map") {
614
+ const rewritten = Object.create(null);
615
+ for (const [name, schema] of Object.entries(exactObject(child, `schema ${key}`)))
616
+ rewritten[name] = await this.#rewriteSubschema(schema, {
617
+ ...childAddress,
618
+ pointer: `${childAddress.pointer}/${encodePointerToken(name)}`,
619
+ value: schema,
620
+ }, depth + 1, referenceDepth);
621
+ output[key] = rewritten;
622
+ }
623
+ else if (kind === "array") {
624
+ if (!Array.isArray(child))
625
+ throw new Error(`schema ${key} must be an array`);
626
+ output[key] = await Promise.all(child.map((schema, index) => this.#rewriteSubschema(schema, {
627
+ ...childAddress,
628
+ pointer: `${childAddress.pointer}/${index}`,
629
+ value: schema,
630
+ }, depth + 1, referenceDepth)));
631
+ }
632
+ else
633
+ output[key] = this.#opaqueJson(child);
634
+ }
635
+ return output;
636
+ }
637
+ async #rewriteSubschema(value, address, depth, referenceDepth) {
638
+ if (typeof value === "boolean")
639
+ return value;
640
+ return this.#rewriteSchema(value, address, depth, referenceDepth);
641
+ }
642
+ }
643
+ async function resolveObject(value, address, materializer, depth = 0) {
644
+ let object = exactObject(value, "OpenAPI object");
645
+ let current = address;
646
+ const seen = new Set();
647
+ while (typeof object.$ref === "string") {
648
+ const target = await materializer.resolveReference(object.$ref, current, depth + 1, false);
649
+ const identity = `${target.documentUri}\0${target.pointer}`;
650
+ if (seen.has(identity))
651
+ throw new Error("object reference cycle is unsupported");
652
+ seen.add(identity);
653
+ current = target;
654
+ object = exactObject(target.value, "referenced OpenAPI object");
655
+ depth += 1;
656
+ if (depth > materializer.limits.maxJsonDepth)
657
+ throw new Error("reference depth exceeds limit");
658
+ }
659
+ return { object, address: current };
660
+ }
661
+ /**
662
+ * Resolves a Path Item reference without silently discarding adjacent fields.
663
+ * OpenAPI leaves duplicate fields undefined, so this compiler rejects them;
664
+ * disjoint fields retain the document address that defined each one.
665
+ */
666
+ async function resolvePathItem(value, address, materializer, depth = 0, seen = new Set()) {
667
+ const object = exactObject(value, "Path Item");
668
+ if (Object.hasOwn(object, "$ref") && typeof object.$ref !== "string")
669
+ throw new Error("Path Item $ref must be a string");
670
+ if (typeof object.$ref !== "string") {
671
+ return {
672
+ object,
673
+ address,
674
+ fieldOrigins: new Map(Object.keys(object).map((key) => [key, address])),
675
+ };
676
+ }
677
+ const target = await materializer.resolveReference(object.$ref, address, depth + 1, false);
678
+ const identity = `${target.documentUri}\0${target.pointer}`;
679
+ if (seen.has(identity))
680
+ throw new Error("Path Item reference cycle is unsupported");
681
+ seen.add(identity);
682
+ const referenced = await resolvePathItem(target.value, target, materializer, depth + 1, seen);
683
+ const merged = Object.assign(Object.create(null), referenced.object);
684
+ const fieldOrigins = new Map(referenced.fieldOrigins);
685
+ for (const [key, field] of Object.entries(object)) {
686
+ if (key === "$ref")
687
+ continue;
688
+ if (Object.hasOwn(merged, key))
689
+ throw new Error(`Path Item $ref has a conflicting sibling "${key}"`);
690
+ merged[key] = field;
691
+ fieldOrigins.set(key, address);
692
+ }
693
+ return { object: merged, address, fieldOrigins };
694
+ }
695
+ function schemaUseAddress(owner, address) {
696
+ const hasSchema = Object.hasOwn(owner, "schema");
697
+ const hasContent = Object.hasOwn(owner, "content");
698
+ if (hasSchema === hasContent)
699
+ throw new Error("parameter/header must declare exactly one of schema or content");
700
+ if (hasSchema) {
701
+ return {
702
+ kind: "schema",
703
+ address: {
704
+ ...address,
705
+ pointer: `${address.pointer}/schema`,
706
+ value: owner.schema,
707
+ },
708
+ };
709
+ }
710
+ const entries = Object.entries(exactObject(owner.content, "parameter content"));
711
+ if (entries.length !== 1)
712
+ throw new Error("parameter content must contain exactly one media type");
713
+ const [rawMediaType, media] = entries[0];
714
+ const mediaObject = exactObject(media, "parameter media type");
715
+ if (!Object.hasOwn(mediaObject, "schema"))
716
+ throw new Error("parameter content is missing a schema");
717
+ return {
718
+ kind: "content",
719
+ mediaType: canonicalMediaType(rawMediaType),
720
+ address: {
721
+ ...address,
722
+ pointer: `${address.pointer}/content/${encodePointerToken(rawMediaType)}/schema`,
723
+ value: mediaObject.schema,
724
+ },
725
+ };
726
+ }
727
+ async function schemaUse(owner, address, materializer) {
728
+ const located = schemaUseAddress(owner, address);
729
+ const schemaId = await materializer.materializeUse(located.address);
730
+ return located.kind === "schema"
731
+ ? { kind: "schema", schemaId }
732
+ : { kind: "content", mediaType: located.mediaType, schemaId };
733
+ }
734
+ async function collectParameterSchemaRoots(rawList, address, materializer, roots) {
735
+ if (rawList === undefined)
736
+ return;
737
+ if (!Array.isArray(rawList))
738
+ throw new Error("parameters must be an array");
739
+ if (rawList.length > materializer.limits.maxParametersPerOperation)
740
+ throw new Error("Parameters exceed maxParametersPerOperation limit");
741
+ for (const [index, raw] of rawList.entries()) {
742
+ const resolved = await resolveObject(raw, {
743
+ ...address,
744
+ pointer: `${address.pointer}/parameters/${index}`,
745
+ value: raw,
746
+ }, materializer);
747
+ roots.push(schemaUseAddress(resolved.object, resolved.address).address);
748
+ }
749
+ }
750
+ async function collectEncodingHeaderSchemaRoots(headers, address, materializer, roots) {
751
+ if (headers === undefined)
752
+ return;
753
+ for (const [rawName, rawHeader] of Object.entries(exactObject(headers, "encoding headers"))) {
754
+ const resolved = await resolveObject(rawHeader, {
755
+ ...address,
756
+ pointer: `${address.pointer}/${encodePointerToken(rawName)}`,
757
+ value: rawHeader,
758
+ }, materializer);
759
+ roots.push(schemaUseAddress(resolved.object, resolved.address).address);
760
+ }
761
+ }
762
+ async function collectRequestBodySchemaRoots(raw, address, materializer, roots) {
763
+ if (raw === undefined)
764
+ return;
765
+ const resolved = await resolveObject(raw, address, materializer);
766
+ const alternatives = Object.entries(exactObject(resolved.object.content, "request body content"));
767
+ if (alternatives.length === 0)
768
+ throw new Error("request body content must be nonempty");
769
+ for (const [rawMediaType, rawMedia] of alternatives) {
770
+ const media = exactObject(rawMedia, "request body media entry");
771
+ if (!Object.hasOwn(media, "schema"))
772
+ throw new Error("request body media entry missing schema");
773
+ const mediaAddress = {
774
+ ...resolved.address,
775
+ pointer: `${resolved.address.pointer}/content/${encodePointerToken(rawMediaType)}`,
776
+ value: media,
777
+ };
778
+ roots.push({
779
+ ...mediaAddress,
780
+ pointer: `${mediaAddress.pointer}/schema`,
781
+ value: media.schema,
782
+ });
783
+ if (media.encoding === undefined)
784
+ continue;
785
+ for (const [property, rawEncoding] of Object.entries(exactObject(media.encoding, "request body encoding"))) {
786
+ const encoding = exactObject(rawEncoding, "encoding entry");
787
+ await collectEncodingHeaderSchemaRoots(encoding.headers, {
788
+ ...mediaAddress,
789
+ pointer: `${mediaAddress.pointer}/encoding/${encodePointerToken(property)}/headers`,
790
+ value: encoding.headers,
791
+ }, materializer, roots);
792
+ }
793
+ }
794
+ }
795
+ async function collectOpenApiSchemaRoots(document, sourceUri, materializer) {
796
+ const root = document;
797
+ const sourceAddress = {
798
+ documentUri: sourceUri,
799
+ referenceBaseUri: sourceUri,
800
+ resourceUri: sourceUri,
801
+ resourcePointer: "#",
802
+ resourceValue: root,
803
+ pointer: "#",
804
+ document: root,
805
+ value: root,
806
+ };
807
+ const roots = [];
808
+ const componentSchemas = exactObject(document.components?.schemas ?? Object.create(null), "component schemas");
809
+ for (const [name, schema] of Object.entries(componentSchemas))
810
+ roots.push({
811
+ ...sourceAddress,
812
+ pointer: `#/components/schemas/${encodePointerToken(name)}`,
813
+ value: schema,
814
+ });
815
+ for (const [path, rawPathItem] of pathItemEntries(document.paths)) {
816
+ const pathAddress = {
817
+ ...sourceAddress,
818
+ pointer: `#/paths/${encodePointerToken(path)}`,
819
+ value: rawPathItem,
820
+ };
821
+ const resolvedPathItem = await resolvePathItem(rawPathItem, pathAddress, materializer);
822
+ const pathItem = resolvedPathItem.object;
823
+ await collectParameterSchemaRoots(pathItem.parameters, resolvedPathItem.fieldOrigins.get("parameters") ??
824
+ resolvedPathItem.address, materializer, roots);
825
+ for (const method of HTTP_METHODS) {
826
+ if (!Object.hasOwn(pathItem, method))
827
+ continue;
828
+ const operation = exactObject(pathItem[method], `${method.toUpperCase()} ${path}`);
829
+ const operationOrigin = resolvedPathItem.fieldOrigins.get(method) ?? resolvedPathItem.address;
830
+ const operationAddress = {
831
+ ...operationOrigin,
832
+ pointer: `${operationOrigin.pointer}/${method}`,
833
+ value: operation,
834
+ };
835
+ await collectParameterSchemaRoots(operation.parameters, operationAddress, materializer, roots);
836
+ await collectRequestBodySchemaRoots(operation.requestBody, {
837
+ ...operationAddress,
838
+ pointer: `${operationAddress.pointer}/requestBody`,
839
+ value: operation.requestBody,
840
+ }, materializer, roots);
841
+ }
842
+ }
843
+ return roots;
844
+ }
845
+ function defaultStyle(location) {
846
+ return location === "path" || location === "header" ? "simple" : "form";
847
+ }
848
+ async function normalizeParameter(raw, address, materializer) {
849
+ const resolved = await resolveObject(raw, address, materializer);
850
+ const object = resolved.object;
851
+ if (typeof object.name !== "string" ||
852
+ !PARAMETER_LOCATIONS.includes(object.in))
853
+ throw new Error("parameter name or location is invalid");
854
+ const location = object.in;
855
+ const style = object.style === undefined ? defaultStyle(location) : object.style;
856
+ if (!PARAMETER_STYLES.includes(style))
857
+ throw new Error("parameter style is invalid");
858
+ const allowedStyles = {
859
+ path: ["matrix", "label", "simple"],
860
+ query: ["form", "spaceDelimited", "pipeDelimited", "deepObject"],
861
+ header: ["simple"],
862
+ cookie: ["form"],
863
+ };
864
+ if (!allowedStyles[location].includes(style))
865
+ throw new Error("parameter style is invalid for its location");
866
+ const explode = object.explode === undefined ? style === "form" : object.explode;
867
+ if (typeof explode !== "boolean")
868
+ throw new Error("parameter explode is invalid");
869
+ if (location === "path" && object.required !== true)
870
+ throw new Error("path parameters must be required");
871
+ if (location !== "query" && object.allowReserved === true)
872
+ throw new Error("allowReserved is valid only for query parameters");
873
+ return {
874
+ name: object.name,
875
+ in: location,
876
+ required: object.required === true,
877
+ deprecated: object.deprecated === true,
878
+ style: style,
879
+ explode,
880
+ allowReserved: object.allowReserved === true,
881
+ value: await schemaUse(object, resolved.address, materializer),
882
+ };
883
+ }
884
+ async function normalizeParameters(path, pathItem, operation, pathItemAddress, operationAddress, materializer) {
885
+ const merged = new Map();
886
+ for (const [scope, rawList] of [
887
+ ["path", pathItem.parameters],
888
+ ["operation", operation.parameters],
889
+ ]) {
890
+ if (rawList === undefined)
891
+ continue;
892
+ if (!Array.isArray(rawList))
893
+ throw new Error(`${scope} parameters must be an array`);
894
+ if (rawList.length > materializer.limits.maxParametersPerOperation)
895
+ throw new Error("Parameters exceed maxParametersPerOperation limit");
896
+ const local = new Set();
897
+ for (let index = 0; index < rawList.length; index += 1) {
898
+ const parameter = await normalizeParameter(rawList[index], {
899
+ ...(scope === "path" ? pathItemAddress : operationAddress),
900
+ pointer: `${scope === "path" ? pathItemAddress.pointer : operationAddress.pointer}/parameters/${index}`,
901
+ value: rawList[index],
902
+ }, materializer);
903
+ const key = `${parameter.in}\0${parameter.name}`;
904
+ if (local.has(key))
905
+ throw new Error("duplicate parameter after normalization");
906
+ local.add(key);
907
+ merged.set(key, parameter);
908
+ }
909
+ }
910
+ const variables = new Set(parseOperationPathTemplateV4(path));
911
+ if (merged.size > materializer.limits.maxParametersPerOperation)
912
+ throw new Error("Parameters exceed maxParametersPerOperation limit");
913
+ const pathParameters = [...merged.values()].filter((parameter) => parameter.in === "path");
914
+ if (pathParameters.some((parameter) => !variables.has(parameter.name)) ||
915
+ pathParameters.length !== variables.size) {
916
+ throw new Error("path parameters must exactly match path-template variables");
917
+ }
918
+ return [...merged.values()].sort((left, right) => {
919
+ const location = PARAMETER_LOCATIONS.indexOf(left.in) -
920
+ PARAMETER_LOCATIONS.indexOf(right.in);
921
+ return (location || (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));
922
+ });
923
+ }
924
+ async function normalizeEncodingHeaders(headers, address, materializer) {
925
+ if (headers === undefined)
926
+ return [];
927
+ const output = [];
928
+ const seen = new Set();
929
+ for (const [rawName, rawHeader] of Object.entries(exactObject(headers, "encoding headers"))) {
930
+ const name = rawName.toLowerCase();
931
+ if (!/^[a-z0-9!#$%&'*+.^_`|~-]+$/.test(name) || seen.has(name))
932
+ throw new Error("encoding header name is invalid or duplicated");
933
+ seen.add(name);
934
+ const headerAddress = {
935
+ ...address,
936
+ pointer: `${address.pointer}/${encodePointerToken(rawName)}`,
937
+ value: rawHeader,
938
+ };
939
+ const resolved = await resolveObject(rawHeader, headerAddress, materializer);
940
+ output.push({
941
+ name,
942
+ required: resolved.object.required === true,
943
+ value: await schemaUse(resolved.object, resolved.address, materializer),
944
+ });
945
+ }
946
+ return stableSort(output, (header) => header.name);
947
+ }
948
+ async function normalizeRequestBody(raw, address, materializer) {
949
+ if (raw === undefined)
950
+ return null;
951
+ const resolved = await resolveObject(raw, address, materializer);
952
+ const alternatives = Object.entries(exactObject(resolved.object.content, "request body content"));
953
+ if (alternatives.length === 0)
954
+ throw new Error("request body content must be nonempty");
955
+ const seen = new Set();
956
+ const content = [];
957
+ for (const [rawMediaType, rawMedia] of alternatives) {
958
+ const mediaType = canonicalMediaType(rawMediaType);
959
+ if (seen.has(mediaType))
960
+ throw new Error("duplicate normalized request-body media type");
961
+ seen.add(mediaType);
962
+ const media = exactObject(rawMedia, "request body media entry");
963
+ if (!Object.hasOwn(media, "schema"))
964
+ throw new Error("request body media entry is missing a schema");
965
+ const mediaAddress = {
966
+ ...resolved.address,
967
+ pointer: `${resolved.address.pointer}/content/${encodePointerToken(rawMediaType)}`,
968
+ value: media,
969
+ };
970
+ const schemaId = await materializer.materializeUse({
971
+ ...mediaAddress,
972
+ pointer: `${mediaAddress.pointer}/schema`,
973
+ value: media.schema,
974
+ });
975
+ let encodingProperties = null;
976
+ if (media.encoding !== undefined) {
977
+ let schemaAddress = {
978
+ ...mediaAddress,
979
+ pointer: `${mediaAddress.pointer}/schema`,
980
+ value: media.schema,
981
+ };
982
+ let schemaObject = exactObject(schemaAddress.value, "request body root schema");
983
+ const visited = new Set();
984
+ while (typeof schemaObject.$ref === "string") {
985
+ schemaAddress = materializer.schemaAddress(schemaObject, schemaAddress);
986
+ schemaAddress = await materializer.resolveReference(schemaObject.$ref, schemaAddress, visited.size + 1, true);
987
+ const identity = `${schemaAddress.documentUri}\0${schemaAddress.pointer}`;
988
+ if (visited.has(identity))
989
+ throw new Error("request body root schema reference cycle is unsupported for encoding");
990
+ visited.add(identity);
991
+ schemaObject = exactObject(schemaAddress.value, "request body root schema");
992
+ }
993
+ if (schemaObject.type !== "object" ||
994
+ typeof schemaObject.properties !== "object" ||
995
+ schemaObject.properties === null ||
996
+ Array.isArray(schemaObject.properties)) {
997
+ throw new Error("request body encoding requires a resolved root object schema");
998
+ }
999
+ encodingProperties = schemaObject.properties;
1000
+ }
1001
+ const encodings = [];
1002
+ if (media.encoding !== undefined) {
1003
+ if (mediaType !== "multipart/form-data" &&
1004
+ mediaType !== "application/x-www-form-urlencoded")
1005
+ throw new Error("encoding is unsupported for this media type");
1006
+ for (const [property, rawEncoding] of Object.entries(exactObject(media.encoding, "request body encoding"))) {
1007
+ if (!encodingProperties || !Object.hasOwn(encodingProperties, property))
1008
+ throw new Error("encoding property is not an immediate root schema property");
1009
+ const encoding = exactObject(rawEncoding, "encoding entry");
1010
+ const style = encoding.style === undefined ? null : encoding.style;
1011
+ if (style !== null &&
1012
+ !PARAMETER_STYLES.includes(style))
1013
+ throw new Error("encoding style is invalid");
1014
+ const explode = encoding.explode === undefined ? null : encoding.explode;
1015
+ if (explode !== null && typeof explode !== "boolean")
1016
+ throw new Error("encoding explode is invalid");
1017
+ encodings.push({
1018
+ property,
1019
+ contentType: encoding.contentType === undefined
1020
+ ? null
1021
+ : canonicalMediaType(String(encoding.contentType)),
1022
+ style: style,
1023
+ explode,
1024
+ allowReserved: encoding.allowReserved === true,
1025
+ headers: await normalizeEncodingHeaders(encoding.headers, {
1026
+ ...mediaAddress,
1027
+ pointer: `${mediaAddress.pointer}/encoding/${encodePointerToken(property)}/headers`,
1028
+ value: encoding.headers,
1029
+ }, materializer),
1030
+ });
1031
+ }
1032
+ }
1033
+ content.push({
1034
+ mediaType,
1035
+ schemaId,
1036
+ encoding: stableSort(encodings, (entry) => entry.property),
1037
+ });
1038
+ }
1039
+ return {
1040
+ required: resolved.object.required === true,
1041
+ content: stableSort(content, (entry) => entry.mediaType),
1042
+ };
1043
+ }
1044
+ async function extractLogicalRecords(document, sourceUri, graph, api, allowedOrigins, limits, permissionIndex) {
1045
+ const root = document;
1046
+ const materializer = new SchemaMaterializer(api, sourceUri, root, graph, limits);
1047
+ const componentSchemas = exactObject(document.components?.schemas ?? Object.create(null), "component schemas");
1048
+ if (Object.keys(componentSchemas).length > limits.maxSchemas)
1049
+ throw new Error("Schemas exceed maxSchemas limit");
1050
+ const paths = pathItemEntries(document.paths);
1051
+ if (paths.length > limits.maxPaths)
1052
+ throw new Error("Paths exceed maxPaths limit");
1053
+ await materializer.preIndexResources(await collectOpenApiSchemaRoots(document, sourceUri, materializer));
1054
+ for (const [name, schema] of Object.entries(componentSchemas)) {
1055
+ await materializer.materialize({
1056
+ documentUri: sourceUri,
1057
+ referenceBaseUri: sourceUri,
1058
+ resourceUri: sourceUri,
1059
+ resourcePointer: "#",
1060
+ resourceValue: root,
1061
+ pointer: `#/components/schemas/${encodePointerToken(name)}`,
1062
+ document: root,
1063
+ value: schema,
1064
+ }, name);
1065
+ }
1066
+ const operations = [];
1067
+ const seen = new Set();
1068
+ const documentOrigin = canonicalOrigin(document.servers?.[0]?.url);
1069
+ if (!allowedOrigins.includes(documentOrigin))
1070
+ throw new Error("document origin is absent from allowedOrigins");
1071
+ for (const [path, rawPathItem] of paths) {
1072
+ const resolvedPathItem = await resolvePathItem(rawPathItem, {
1073
+ documentUri: sourceUri,
1074
+ referenceBaseUri: sourceUri,
1075
+ resourceUri: sourceUri,
1076
+ resourcePointer: "#",
1077
+ resourceValue: root,
1078
+ document: root,
1079
+ pointer: `#/paths/${encodePointerToken(path)}`,
1080
+ value: rawPathItem,
1081
+ }, materializer);
1082
+ const pathItem = resolvedPathItem.object;
1083
+ if (pathItem.servers !== undefined)
1084
+ throw new Error(`${path}: path-item server override is unsupported`);
1085
+ for (const method of HTTP_METHODS) {
1086
+ if (!Object.hasOwn(pathItem, method))
1087
+ continue;
1088
+ if (operations.length >= limits.maxOperations)
1089
+ throw new Error("Operations exceed maxOperations limit");
1090
+ const operation = exactObject(pathItem[method], "operation");
1091
+ if (operation.servers !== undefined)
1092
+ throw new Error(`${method.toUpperCase()} ${path}: operation server override is unsupported`);
1093
+ if (typeof operation.operationId !== "string")
1094
+ throw new Error(`${method.toUpperCase()} ${path}: operationId is invalid`);
1095
+ const id = `operation:${api}:${operation.operationId}`;
1096
+ const parsed = parseTypedRecordId(id);
1097
+ if (!parsed.startsWith("operation:"))
1098
+ throw new Error("operation identifier is invalid");
1099
+ if (seen.has(id))
1100
+ throw new Error("duplicate operationId");
1101
+ seen.add(id);
1102
+ const pathItemAddress = resolvedPathItem.fieldOrigins.get("parameters") ??
1103
+ resolvedPathItem.address;
1104
+ const operationOrigin = resolvedPathItem.fieldOrigins.get(method) ?? resolvedPathItem.address;
1105
+ const operationAddress = {
1106
+ ...operationOrigin,
1107
+ pointer: `${operationOrigin.pointer}/${method}`,
1108
+ value: operation,
1109
+ };
1110
+ const parameters = await normalizeParameters(path, pathItem, operation, pathItemAddress, operationAddress, materializer);
1111
+ const requestBody = await normalizeRequestBody(operation.requestBody, {
1112
+ ...operationAddress,
1113
+ pointer: `${operationAddress.pointer}/requestBody`,
1114
+ value: operation.requestBody,
1115
+ }, materializer);
1116
+ const schemaIds = new Set();
1117
+ for (const parameter of parameters)
1118
+ schemaIds.add(parameter.value.schemaId);
1119
+ for (const media of requestBody?.content ?? []) {
1120
+ schemaIds.add(media.schemaId);
1121
+ for (const encoding of media.encoding)
1122
+ for (const header of encoding.headers)
1123
+ schemaIds.add(header.value.schemaId);
1124
+ }
1125
+ const safety = classifySafety(method.toUpperCase(), path, operation.operationId, api);
1126
+ const permission = permissionIndex
1127
+ ? lookupPermissions(permissionIndex, path, method)
1128
+ : null;
1129
+ const record = {
1130
+ id: parsed,
1131
+ api,
1132
+ operationId: operation.operationId,
1133
+ method: method.toUpperCase(),
1134
+ path,
1135
+ origin: documentOrigin,
1136
+ summary: typeof operation.summary === "string"
1137
+ ? truncateSummary(operation.summary)
1138
+ : typeof operation.description === "string"
1139
+ ? truncateSummary(operation.description)
1140
+ : null,
1141
+ tags: normalizeOperationTags(operation.tags),
1142
+ deprecated: operation.deprecated === true,
1143
+ parameters,
1144
+ requestBody,
1145
+ schemaIds: [...schemaIds].sort(),
1146
+ advisory: {
1147
+ safety,
1148
+ risk: riskFor(safety, permission?.privilegeLevel ?? null, path),
1149
+ pageable: operation["x-ms-pageable"] !== undefined,
1150
+ operationType: typeof operation["x-ms-docs-operation-type"] === "string"
1151
+ ? operation["x-ms-docs-operation-type"]
1152
+ : null,
1153
+ permissions: permission?.permissions ?? null,
1154
+ permConfidence: permission?.confidence ?? null,
1155
+ privilegeLevel: permission?.privilegeLevel ?? null,
1156
+ },
1157
+ };
1158
+ checkedRecordJson(record, limits);
1159
+ operations.push(record);
1160
+ }
1161
+ }
1162
+ return { operations, schemas: [...materializer.records.values()] };
1163
+ }
1164
+ class EphemeralGenerationStore {
1165
+ #states = new Map();
1166
+ async get(catalogId, issuer) {
1167
+ const value = this.#states.get(`${catalogId}\0${issuer}`);
1168
+ return value ? structuredClone(value) : null;
1169
+ }
1170
+ async accept(catalogId, issuer, transition) {
1171
+ const key = `${catalogId}\0${issuer}`;
1172
+ const current = this.#states.get(key);
1173
+ if ((current?.revision ?? null) !== transition.expectedRevision)
1174
+ return null;
1175
+ const next = structuredClone(transition.next);
1176
+ this.#states.set(key, next);
1177
+ return structuredClone(next);
1178
+ }
1179
+ }
1180
+ async function requireStageDirectory(path, identity) {
1181
+ const current = await capturePathIdentity(path, "directory").catch(() => null);
1182
+ if (!current || !samePathIdentity(current, identity))
1183
+ throw new Error("compiler stage directory identity was lost");
1184
+ }
1185
+ async function requireStageFile(path, identity) {
1186
+ const current = await capturePathIdentity(path, "file").catch(() => null);
1187
+ if (!current || !samePathIdentity(current, identity))
1188
+ throw new Error("compiler stage file identity was lost");
1189
+ }
1190
+ /**
1191
+ * Portable Node/Bun has no openat-style API. The compiler therefore assumes
1192
+ * no hostile same-UID parent-namespace mutation before O_EXCL creates a leaf.
1193
+ * Pre/post parent checks limit that residual to an empty entry: bytes are
1194
+ * written only after the pinned handle and its pathname identity both match.
1195
+ */
1196
+ async function createOwnedStageFile(path, directory, directoryIdentity, contents, recordIdentity, afterOpen = () => { }) {
1197
+ if (typeof constants.O_NOFOLLOW !== "number" || constants.O_NOFOLLOW === 0)
1198
+ throw new Error("compiler staging requires O_NOFOLLOW support");
1199
+ await requireStageDirectory(directory, directoryIdentity);
1200
+ const handle = await open(path, constants.O_CREAT |
1201
+ constants.O_EXCL |
1202
+ constants.O_WRONLY |
1203
+ constants.O_NOFOLLOW, 0o600);
1204
+ try {
1205
+ const metadata = await handle.stat({ bigint: true });
1206
+ if (!metadata.isFile() || metadata.nlink !== 1n)
1207
+ throw new Error("compiler-created stage file is unsafe");
1208
+ const identity = pathIdentityFromStats(metadata);
1209
+ recordIdentity(identity);
1210
+ await afterOpen();
1211
+ await requireStageDirectory(directory, directoryIdentity);
1212
+ await requireStageFile(path, identity);
1213
+ await handle.writeFile(contents);
1214
+ await handle.sync();
1215
+ const after = await handle.stat({ bigint: true });
1216
+ if (!after.isFile() ||
1217
+ after.nlink !== 1n ||
1218
+ !samePathIdentity(pathIdentityFromStats(after), identity))
1219
+ throw new Error("compiler stage file identity was lost");
1220
+ await requireStageDirectory(directory, directoryIdentity);
1221
+ await requireStageFile(path, identity);
1222
+ }
1223
+ finally {
1224
+ await handle.close();
1225
+ }
1226
+ }
1227
+ async function readOwnedStageFileSnapshot(path, directory, directoryIdentity, fileIdentity, maxBytes, beforeRead = () => { }) {
1228
+ await requireStageDirectory(directory, directoryIdentity);
1229
+ await requireStageFile(path, fileIdentity);
1230
+ const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
1231
+ try {
1232
+ const before = await handle.stat({ bigint: true });
1233
+ if (!before.isFile() ||
1234
+ before.nlink !== 1n ||
1235
+ !samePathIdentity(pathIdentityFromStats(before), fileIdentity) ||
1236
+ before.size > BigInt(maxBytes))
1237
+ throw new Error("emitted SQLite identity or size is invalid");
1238
+ await requireStageDirectory(directory, directoryIdentity);
1239
+ await requireStageFile(path, fileIdentity);
1240
+ await beforeRead();
1241
+ const expectedBytes = Number(before.size);
1242
+ const bytes = new Uint8Array(expectedBytes);
1243
+ let offset = 0;
1244
+ while (offset < expectedBytes) {
1245
+ const { bytesRead } = await handle.read(bytes, offset, Math.min(64 * 1024, expectedBytes - offset), null);
1246
+ if (bytesRead === 0)
1247
+ throw new Error("emitted SQLite ended before its captured size");
1248
+ offset += bytesRead;
1249
+ }
1250
+ const growthProbe = new Uint8Array(1);
1251
+ const { bytesRead: extraBytes } = await handle.read(growthProbe, 0, 1, null);
1252
+ if (extraBytes !== 0)
1253
+ throw new Error("emitted SQLite grew beyond its captured size");
1254
+ const after = await handle.stat({ bigint: true });
1255
+ if (!after.isFile() ||
1256
+ after.nlink !== 1n ||
1257
+ !samePathIdentity(pathIdentityFromStats(after), fileIdentity) ||
1258
+ after.size !== before.size ||
1259
+ BigInt(bytes.byteLength) !== after.size)
1260
+ throw new Error("emitted SQLite identity or size changed during reread");
1261
+ await requireStageDirectory(directory, directoryIdentity);
1262
+ await requireStageFile(path, fileIdentity);
1263
+ return bytes;
1264
+ }
1265
+ finally {
1266
+ await handle.close();
1267
+ }
1268
+ }
1269
+ async function syncOwnedStageDirectory(path, identity) {
1270
+ await requireStageDirectory(path, identity);
1271
+ const handle = await open(path, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
1272
+ try {
1273
+ const metadata = await handle.stat({ bigint: true });
1274
+ if (!metadata.isDirectory() ||
1275
+ !samePathIdentity(pathIdentityFromStats(metadata), identity))
1276
+ throw new Error("compiler stage directory identity was lost");
1277
+ await handle.sync();
1278
+ await requireStageDirectory(path, identity);
1279
+ }
1280
+ finally {
1281
+ await handle.close();
1282
+ }
1283
+ }
1284
+ function sourceIdentity(options) {
1285
+ const hasUri = typeof options.sourceUri === "string";
1286
+ const hasLabel = typeof options.sourceLabel === "string";
1287
+ if (hasUri === hasLabel)
1288
+ throw new Error("exactly one source URI or source label is required");
1289
+ return hasUri
1290
+ ? normalizeHttpsUri(options.sourceUri)
1291
+ : sourceUriFromLabel(options.sourceLabel);
1292
+ }
1293
+ function assertIdentifiers(options) {
1294
+ const operationProbe = parseTypedRecordId(`operation:${options.catalogId}:probe`);
1295
+ if (!operationProbe.startsWith("operation:"))
1296
+ throw new Error("catalog ID is invalid");
1297
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(options.releaseId) ||
1298
+ options.releaseId === "." ||
1299
+ options.releaseId === "..")
1300
+ throw new Error("release ID is invalid");
1301
+ return {
1302
+ catalogId: options.catalogId,
1303
+ releaseId: options.releaseId,
1304
+ };
1305
+ }
1306
+ export async function compileReleaseWithCheckpoint(options, checkpoint = () => { }) {
1307
+ const sourceUri = sourceIdentity(options);
1308
+ const ids = assertIdentifiers(options);
1309
+ const limits = options.limits ?? DEFAULT_COMPILER_LIMITS;
1310
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(options.sourceRevision) ||
1311
+ options.sourceRevision === "." ||
1312
+ options.sourceRevision === "..") {
1313
+ throw new Error("source revision is invalid");
1314
+ }
1315
+ let stage;
1316
+ let stagePaths;
1317
+ let constructionDatabase;
1318
+ const stageOwnership = {};
1319
+ try {
1320
+ const rootBytes = await readFileBoundedV4(options.specPath, limits.maxSourceBytes, "source document").catch((error) => {
1321
+ if (error instanceof Error &&
1322
+ error.message.includes("exceeds byte limit"))
1323
+ throw new Error("aggregate source bytes exceed limit");
1324
+ throw new Error("source document could not be read");
1325
+ });
1326
+ const extension = extname(options.specPath).toLowerCase();
1327
+ const sourceMediaType = extension === ".json"
1328
+ ? "json"
1329
+ : extension === ".yaml" || extension === ".yml"
1330
+ ? "yaml"
1331
+ : null;
1332
+ if (sourceMediaType === null) {
1333
+ throw new Error("unsupported OpenAPI media type file extension");
1334
+ }
1335
+ const document = parseSpecBytesV4(rootBytes, sourceMediaType, limits);
1336
+ const graph = await ReferenceGraphV4.create({
1337
+ sourceUri,
1338
+ mapPath: options.referenceMapPath,
1339
+ referenceRoot: options.referenceRoot,
1340
+ resolver: options.referenceResolver,
1341
+ limits,
1342
+ rootBytes: rootBytes.byteLength,
1343
+ });
1344
+ let permissionIndex;
1345
+ if (options.permissionsPath) {
1346
+ let permissionsBytes;
1347
+ try {
1348
+ permissionsBytes = await readFileBoundedV4(options.permissionsPath, limits.maxSourceBytes, "permissions dataset");
1349
+ }
1350
+ catch (error) {
1351
+ if (error instanceof Error &&
1352
+ error.message === "permissions dataset exceeds byte limit")
1353
+ throw error;
1354
+ throw new Error("permissions dataset could not be read");
1355
+ }
1356
+ let permissionsText;
1357
+ try {
1358
+ permissionsText = new TextDecoder("utf-8", { fatal: true }).decode(permissionsBytes);
1359
+ }
1360
+ catch {
1361
+ throw new Error("permissions dataset is not valid UTF-8");
1362
+ }
1363
+ const dataset = parseJsonStrict(permissionsText, {
1364
+ maxBytes: limits.maxSourceBytes,
1365
+ maxDepth: limits.maxJsonDepth,
1366
+ maxKeys: limits.maxDocumentKeys,
1367
+ });
1368
+ permissionIndex = buildPermissionIndex(dataset);
1369
+ }
1370
+ const records = await extractLogicalRecords(document, sourceUri, graph, options.catalogId, options.allowedOrigins, limits, permissionIndex);
1371
+ const sourceContentSha256 = createHash("sha256")
1372
+ .update(rootBytes)
1373
+ .digest("hex");
1374
+ const graphDigest = await referenceGraphDigest(graph.map);
1375
+ await mkdir(options.outDir, { recursive: true });
1376
+ const outDir = resolve(options.outDir);
1377
+ stage = await mkdtemp(join(outDir, ".openapi-mcp-v5-"));
1378
+ stageOwnership.directory = await capturePathIdentity(stage, "directory");
1379
+ const paths = {
1380
+ directory: stage,
1381
+ sqlite: join(stage, `${options.releaseId}.sqlite`),
1382
+ signature: join(stage, `${options.releaseId}.manifest.sig`),
1383
+ manifest: join(stage, `${options.releaseId}.manifest.json`),
1384
+ };
1385
+ stagePaths = paths;
1386
+ const compiledAt = new Date().toISOString();
1387
+ await checkpoint("before-sqlite-created", paths);
1388
+ await requireStageDirectory(paths.directory, stageOwnership.directory);
1389
+ const database = new DatabaseSync(":memory:");
1390
+ constructionDatabase = database;
1391
+ try {
1392
+ database.exec("BEGIN IMMEDIATE");
1393
+ createReleaseSchemaV4(database);
1394
+ database
1395
+ .prepare(`INSERT INTO release_metadata (
1396
+ catalog_id, release_id, format, contract, generation, issuer, key_id,
1397
+ policy_id, allowed_origins_json, compiled_at, compiler_version,
1398
+ source_uri, source_revision, source_content_sha256,
1399
+ reference_graph_digest, manifest_json, signature_algorithm,
1400
+ signature_key_id, signature
1401
+ ) VALUES (?, ?, 5, 1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', 'Ed25519', ?, '')`)
1402
+ .run(ids.catalogId, ids.releaseId, options.generation, options.issuer, options.keyId, options.policyId, canonicalJson([...options.allowedOrigins].sort()), compiledAt, COMPILER_VERSION, sourceUri, options.sourceRevision, sourceContentSha256, graphDigest, options.keyId);
1403
+ const insertOperation = database.prepare(`INSERT INTO operations (
1404
+ catalog_id, release_id, record_id, record_json, logical_digest,
1405
+ api, operation_id, summary, path, search_text
1406
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
1407
+ for (const record of records.operations) {
1408
+ const json = checkedRecordJson(record, limits);
1409
+ const digest = await sha256("knitli.openapi-mcp.operation-record.v5", record);
1410
+ insertOperation.run(ids.catalogId, ids.releaseId, record.id, json, digest, record.api, record.operationId, record.summary, record.path, `${splitWords(record.operationId)} ${splitWords(record.path)}`);
1411
+ }
1412
+ const insertSchema = database.prepare(`INSERT INTO schemas (
1413
+ catalog_id, release_id, record_id, record_json, logical_digest
1414
+ ) VALUES (?, ?, ?, ?, ?)`);
1415
+ for (const record of records.schemas) {
1416
+ const json = checkedRecordJson(record, limits);
1417
+ const digest = await sha256("knitli.openapi-mcp.schema-record.v5", record);
1418
+ insertSchema.run(ids.catalogId, ids.releaseId, record.id, json, digest);
1419
+ }
1420
+ populateOperationsFtsV4(database);
1421
+ database.exec("COMMIT");
1422
+ }
1423
+ catch (error) {
1424
+ try {
1425
+ database.exec("ROLLBACK");
1426
+ }
1427
+ catch { }
1428
+ throw error;
1429
+ }
1430
+ const reread = database;
1431
+ let rows;
1432
+ rows = reread
1433
+ .prepare("SELECT record_id, record_json, logical_digest FROM operations UNION ALL SELECT record_id, record_json, logical_digest FROM schemas ORDER BY record_id")
1434
+ .all();
1435
+ const manifestRecords = Object.create(null);
1436
+ for (const row of rows) {
1437
+ const id = parseTypedRecordId(row.record_id);
1438
+ const record = parseJsonStrict(row.record_json, {
1439
+ maxBytes: limits.maxRecordBytes,
1440
+ maxDepth: limits.maxJsonDepth,
1441
+ maxKeys: limits.maxDocumentKeys,
1442
+ });
1443
+ const domain = id.startsWith("operation:")
1444
+ ? "knitli.openapi-mcp.operation-record.v5"
1445
+ : "knitli.openapi-mcp.schema-record.v5";
1446
+ const digest = await sha256(domain, record);
1447
+ if (digest !== row.logical_digest)
1448
+ throw new Error("reread logical record digest mismatch");
1449
+ manifestRecords[id] = digest;
1450
+ }
1451
+ const manifest = buildReleaseManifestV4({
1452
+ ...ids,
1453
+ generation: options.generation,
1454
+ issuer: options.issuer,
1455
+ keyId: options.keyId,
1456
+ policyId: options.policyId,
1457
+ allowedOrigins: options.allowedOrigins,
1458
+ compiledAt,
1459
+ compilerVersion: COMPILER_VERSION,
1460
+ sourceUri,
1461
+ sourceRevision: options.sourceRevision,
1462
+ sourceContentSha256,
1463
+ referenceGraphDigest: graphDigest,
1464
+ records: manifestRecords,
1465
+ });
1466
+ const envelope = buildManifestEnvelopeV4(manifest, options.privateKeyPem);
1467
+ const update = database;
1468
+ update
1469
+ .prepare(`UPDATE release_metadata SET manifest_json = ?, signature_algorithm = ?, signature_key_id = ?, signature = ? WHERE catalog_id = ? AND release_id = ?`)
1470
+ .run(envelope.manifestJson, envelope.signature.algorithm, envelope.signature.keyId, envelope.signature.signature, ids.catalogId, ids.releaseId);
1471
+ const metadataDatabase = database;
1472
+ const metadata = metadataDatabase
1473
+ .prepare("SELECT * FROM release_metadata WHERE catalog_id = ? AND release_id = ?")
1474
+ .get(ids.catalogId, ids.releaseId);
1475
+ if (!metadata)
1476
+ throw new Error("release metadata is absent after construction");
1477
+ const expectedMetadata = {
1478
+ catalog_id: manifest.catalogId,
1479
+ release_id: manifest.releaseId,
1480
+ format: manifest.format,
1481
+ contract: manifest.contract,
1482
+ generation: manifest.generation,
1483
+ issuer: manifest.issuer,
1484
+ key_id: manifest.keyId,
1485
+ policy_id: manifest.policyId,
1486
+ allowed_origins_json: canonicalJson([
1487
+ ...manifest.allowedOrigins,
1488
+ ]),
1489
+ compiled_at: manifest.compiledAt,
1490
+ compiler_version: manifest.compilerVersion,
1491
+ source_uri: manifest.source.uri,
1492
+ source_revision: manifest.source.revision,
1493
+ source_content_sha256: manifest.source.contentSha256,
1494
+ reference_graph_digest: manifest.source.referenceGraphDigest,
1495
+ manifest_json: envelope.manifestJson,
1496
+ signature_algorithm: envelope.signature.algorithm,
1497
+ signature_key_id: envelope.signature.keyId,
1498
+ signature: envelope.signature.signature,
1499
+ };
1500
+ for (const [key, expected] of Object.entries(expectedMetadata)) {
1501
+ if (metadata[key] !== expected)
1502
+ throw new Error(`release metadata field ${key} disagrees with the canonical manifest`);
1503
+ }
1504
+ const sqliteBytes = database.serialize();
1505
+ database.close();
1506
+ constructionDatabase = undefined;
1507
+ const signatureBytes = canonicalJson(envelope.signature);
1508
+ await createOwnedStageFile(paths.sqlite, paths.directory, stageOwnership.directory, sqliteBytes, (identity) => {
1509
+ stageOwnership.sqlite = identity;
1510
+ }, () => checkpoint("after-sqlite-opened", paths));
1511
+ await checkpoint("after-sqlite-emitted", paths);
1512
+ const emittedBytes = await readOwnedStageFileSnapshot(paths.sqlite, paths.directory, stageOwnership.directory, stageOwnership.sqlite, sqliteBytes.byteLength, () => checkpoint("before-sqlite-snapshot-read", paths)).catch(() => {
1513
+ throw new Error("emitted SQLite could not be reopened safely");
1514
+ });
1515
+ const emittedDatabase = new DatabaseSync(":memory:");
1516
+ const emittedStoredRows = [];
1517
+ try {
1518
+ emittedDatabase.deserialize(emittedBytes);
1519
+ emittedDatabase.exec("PRAGMA query_only = ON");
1520
+ const emittedRows = emittedDatabase
1521
+ .prepare("SELECT record_id, record_json, logical_digest FROM operations UNION ALL SELECT record_id, record_json, logical_digest FROM schemas ORDER BY record_id")
1522
+ .all();
1523
+ const emittedManifestRecords = Object.create(null);
1524
+ for (const row of emittedRows) {
1525
+ const id = parseTypedRecordId(row.record_id);
1526
+ let record;
1527
+ try {
1528
+ record = parseJsonStrict(row.record_json, {
1529
+ maxBytes: limits.maxRecordBytes,
1530
+ maxDepth: limits.maxJsonDepth,
1531
+ maxKeys: limits.maxDocumentKeys,
1532
+ });
1533
+ }
1534
+ catch {
1535
+ throw new Error("reread emitted record_json is invalid");
1536
+ }
1537
+ const domain = id.startsWith("operation:")
1538
+ ? "knitli.openapi-mcp.operation-record.v5"
1539
+ : "knitli.openapi-mcp.schema-record.v5";
1540
+ const digest = await sha256(domain, record);
1541
+ if (digest !== row.logical_digest)
1542
+ throw new Error("reread emitted record digest mismatch");
1543
+ emittedManifestRecords[id] = digest;
1544
+ emittedStoredRows.push({ id, logicalDigest: digest, record });
1545
+ }
1546
+ if (canonicalJson(emittedManifestRecords) !==
1547
+ canonicalJson(manifest.records))
1548
+ throw new Error("reread emitted manifest record map mismatch");
1549
+ const emittedMetadata = emittedDatabase
1550
+ .prepare("SELECT * FROM release_metadata WHERE catalog_id = ? AND release_id = ?")
1551
+ .get(ids.catalogId, ids.releaseId);
1552
+ if (!emittedMetadata)
1553
+ throw new Error("reread emitted release metadata is absent");
1554
+ for (const [key, expected] of Object.entries(expectedMetadata)) {
1555
+ if (emittedMetadata[key] !== expected)
1556
+ throw new Error(`reread emitted release metadata field ${key} disagrees with the canonical manifest`);
1557
+ }
1558
+ }
1559
+ finally {
1560
+ emittedDatabase.close();
1561
+ }
1562
+ await checkpoint("before-signature-created", paths);
1563
+ await createOwnedStageFile(paths.signature, paths.directory, stageOwnership.directory, signatureBytes, (identity) => {
1564
+ stageOwnership.signature = identity;
1565
+ }, () => checkpoint("after-signature-opened", paths));
1566
+ await checkpoint("before-manifest-created", paths);
1567
+ await createOwnedStageFile(paths.manifest, paths.directory, stageOwnership.directory, envelope.manifestJson, (identity) => {
1568
+ stageOwnership.manifest = identity;
1569
+ }, () => checkpoint("after-manifest-opened", paths));
1570
+ await syncOwnedStageDirectory(paths.directory, stageOwnership.directory);
1571
+ const admitted = await admitManifest(envelope, {
1572
+ releaseKeys: [
1573
+ {
1574
+ issuer: options.issuer,
1575
+ keyId: options.keyId,
1576
+ publicKey: deriveReleasePublicKeyV4(options.privateKeyPem),
1577
+ },
1578
+ ],
1579
+ rollbackKeys: [],
1580
+ }, new EphemeralGenerationStore(), DEFAULT_RUNTIME_LIMITS);
1581
+ if (canonicalJson(admitted.manifest) !==
1582
+ envelope.manifestJson)
1583
+ throw new Error("self-admitted manifest is not byte-identical");
1584
+ for (const row of emittedStoredRows)
1585
+ await verifyStoredRecord(admitted, row);
1586
+ const compiled = Object.freeze({
1587
+ manifest: admitted.manifest,
1588
+ envelope: Object.freeze({
1589
+ manifestJson: envelope.manifestJson,
1590
+ signature: Object.freeze({ ...envelope.signature }),
1591
+ }),
1592
+ paths: Object.freeze({ ...paths }),
1593
+ });
1594
+ const stageDigests = {
1595
+ sqlite: createHash("sha256").update(emittedBytes).digest("hex"),
1596
+ signature: createHash("sha256").update(signatureBytes).digest("hex"),
1597
+ manifest: createHash("sha256")
1598
+ .update(envelope.manifestJson)
1599
+ .digest("hex"),
1600
+ };
1601
+ const stageSizes = {
1602
+ sqlite: emittedBytes.byteLength,
1603
+ signature: Buffer.byteLength(signatureBytes),
1604
+ manifest: Buffer.byteLength(envelope.manifestJson),
1605
+ };
1606
+ return registerCompiledRelease(compiled, outDir, stageDigests, stageSizes, stageOwnership);
1607
+ }
1608
+ catch (error) {
1609
+ try {
1610
+ constructionDatabase?.close();
1611
+ }
1612
+ catch { }
1613
+ if (stagePaths)
1614
+ await cleanupOwnedStage(stagePaths, stageOwnership);
1615
+ throw error;
1616
+ }
1617
+ }
1618
+ export async function compileRelease(options) {
1619
+ return compileReleaseWithCheckpoint(options);
1620
+ }