@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,343 @@
1
+ import { admitManifest, CATALOG_STORE_PUBLIC_MESSAGES, verifyStoredRecord, } from "../runtime/index.js";
2
+ async function assertPublicError(adapter, run, code, message, forbidden) {
3
+ let caught;
4
+ try {
5
+ await run();
6
+ }
7
+ catch (error) {
8
+ caught = error;
9
+ }
10
+ adapter.ok(caught);
11
+ const error = caught;
12
+ adapter.equal(error.code, code);
13
+ adapter.deepEqual(error.details, {});
14
+ adapter.equal(error.retryable, false);
15
+ adapter.equal(error.message, message);
16
+ if (forbidden !== undefined) {
17
+ adapter.equal(JSON.stringify(error).includes(forbidden), false);
18
+ adapter.equal(String(error.message).includes(forbidden), false);
19
+ }
20
+ }
21
+ function attemptMutation(run) {
22
+ try {
23
+ run();
24
+ }
25
+ catch {
26
+ // Frozen results may reject mutation; mutable results must be detached.
27
+ }
28
+ }
29
+ class ConformanceGenerationStore {
30
+ #states = new Map();
31
+ async get(catalogId, issuer) {
32
+ return this.#states.get(`${catalogId}\0${issuer}`) ?? null;
33
+ }
34
+ async accept(catalogId, issuer, transition) {
35
+ const key = `${catalogId}\0${issuer}`;
36
+ const current = this.#states.get(key) ?? null;
37
+ if ((current?.revision ?? null) !== transition.expectedRevision)
38
+ return null;
39
+ this.#states.set(key, transition.next);
40
+ return transition.next;
41
+ }
42
+ }
43
+ /** Register the transport-independent CatalogStore contract against a factory. */
44
+ export function runRuntimeConformanceSuite(factory, options) {
45
+ const { testAdapter: adapter } = options;
46
+ const use = (name, run) => {
47
+ adapter.test(name, async () => {
48
+ const value = await factory();
49
+ try {
50
+ await run(value);
51
+ }
52
+ finally {
53
+ await value.dispose?.();
54
+ }
55
+ });
56
+ };
57
+ const useFault = (name, scenario, run) => {
58
+ adapter.test(name, async () => {
59
+ const value = await factory(scenario);
60
+ try {
61
+ await run(value);
62
+ }
63
+ finally {
64
+ await value.dispose?.();
65
+ }
66
+ });
67
+ };
68
+ use("catalog candidates are bounded, exact, and duplicate-free", async ({ store, fixture }) => {
69
+ adapter.deepEqual(await store.searchCandidates({
70
+ query: fixture.searchQuery,
71
+ api: fixture.api,
72
+ limit: 1,
73
+ }), fixture.expectedCandidates.slice(0, 1));
74
+ adapter.deepEqual(await store.searchCandidates({
75
+ query: fixture.searchQuery,
76
+ api: fixture.api,
77
+ limit: fixture.expectedCandidates.length,
78
+ }), fixture.expectedCandidates);
79
+ });
80
+ useFault("catalog rejects duplicate candidate transport rows", { fault: "duplicate-candidates" }, async ({ store, fixture }) => {
81
+ await assertPublicError(adapter, () => store.searchCandidates({
82
+ query: fixture.searchQuery,
83
+ api: fixture.api,
84
+ limit: fixture.expectedCandidates.length,
85
+ }), "RECORD_DIGEST_MISMATCH", CATALOG_STORE_PUBLIC_MESSAGES.searchTransportDuplicateRows);
86
+ });
87
+ useFault("catalog rejects malformed candidate transport rows", { fault: "candidate-malformed-operation-id" }, async ({ store, fixture }) => {
88
+ await assertPublicError(adapter, () => store.searchCandidates({
89
+ query: fixture.searchQuery,
90
+ api: fixture.api,
91
+ limit: 1,
92
+ }), "RECORD_DIGEST_MISMATCH", CATALOG_STORE_PUBLIC_MESSAGES.storedOperationIdentifierInvalid);
93
+ });
94
+ useFault("catalog rejects candidate transport rows with non-operation IDs", { fault: "candidate-non-operation-id" }, async ({ store, fixture }) => {
95
+ await assertPublicError(adapter, () => store.searchCandidates({
96
+ query: fixture.searchQuery,
97
+ api: fixture.api,
98
+ limit: 1,
99
+ }), "RECORD_DIGEST_MISMATCH", CATALOG_STORE_PUBLIC_MESSAGES.storedOperationIdentifierInvalid);
100
+ });
101
+ useFault("catalog rejects cross-API candidate transport rows", { fault: "candidate-cross-api" }, async ({ store, fixture }) => {
102
+ await assertPublicError(adapter, () => store.searchCandidates({
103
+ query: fixture.searchQuery,
104
+ api: fixture.api,
105
+ limit: 1,
106
+ }), "RECORD_DIGEST_MISMATCH", CATALOG_STORE_PUBLIC_MESSAGES.searchTransportOutsideRequestedApi);
107
+ });
108
+ useFault("catalog rejects duplicate manifest transport rows", { fault: "duplicate-manifest" }, async ({ store, fixture }) => {
109
+ await assertPublicError(adapter, () => store.getManifest(fixture.catalogId, fixture.releaseA), "MANIFEST_INVALID", CATALOG_STORE_PUBLIC_MESSAGES.manifestTransportAbsentOrAmbiguous);
110
+ });
111
+ useFault("catalog rejects duplicate operation transport rows", { fault: "duplicate-operation" }, async ({ store, fixture }) => {
112
+ await assertPublicError(adapter, () => store.getOperation(fixture.catalogId, fixture.releaseA, fixture.operationId), "RECORD_DIGEST_MISMATCH", CATALOG_STORE_PUBLIC_MESSAGES.operationRowAmbiguous);
113
+ });
114
+ useFault("catalog rejects duplicate schema transport rows", { fault: "duplicate-schemas" }, async ({ store, fixture }) => {
115
+ await assertPublicError(adapter, () => store.getSchemas(fixture.catalogId, fixture.releaseA, fixture.schemaIds), "RECORD_DIGEST_MISMATCH", CATALOG_STORE_PUBLIC_MESSAGES.schemaTransportTooManyRows);
116
+ });
117
+ const injectedDriverError = "conformance-private-driver-secret";
118
+ useFault("catalog redacts candidate driver failures", { fault: "driver-candidates", injectedDriverError }, async ({ store, fixture }) => {
119
+ await assertPublicError(adapter, () => store.searchCandidates({
120
+ query: fixture.searchQuery,
121
+ api: fixture.api,
122
+ limit: 1,
123
+ }), "INPUT_INVALID", CATALOG_STORE_PUBLIC_MESSAGES.searchExpressionInvalid, injectedDriverError);
124
+ });
125
+ useFault("catalog redacts manifest driver failures", { fault: "driver-manifest", injectedDriverError }, async ({ store, fixture }) => {
126
+ await assertPublicError(adapter, () => store.getManifest(fixture.catalogId, fixture.releaseA), "MANIFEST_INVALID", CATALOG_STORE_PUBLIC_MESSAGES.manifestTransportUnavailable, injectedDriverError);
127
+ });
128
+ useFault("catalog redacts operation driver failures", { fault: "driver-operation", injectedDriverError }, async ({ store, fixture }) => {
129
+ await assertPublicError(adapter, () => store.getOperation(fixture.catalogId, fixture.releaseA, fixture.operationId), "RECORD_DIGEST_MISMATCH", CATALOG_STORE_PUBLIC_MESSAGES.recordTransportUnavailable, injectedDriverError);
130
+ });
131
+ useFault("catalog redacts schema driver failures", { fault: "driver-schemas", injectedDriverError }, async ({ store, fixture }) => {
132
+ await assertPublicError(adapter, () => store.getSchemas(fixture.catalogId, fixture.releaseA, fixture.schemaIds), "RECORD_DIGEST_MISMATCH", CATALOG_STORE_PUBLIC_MESSAGES.recordTransportUnavailable, injectedDriverError);
133
+ });
134
+ use("catalog operations bind catalog and release", async ({ store, fixture }) => {
135
+ adapter.deepEqual(await store.getOperation(fixture.catalogId, fixture.releaseA, fixture.operationId), fixture.operationA);
136
+ adapter.deepEqual(await store.getOperation(fixture.catalogId, fixture.releaseB, fixture.operationId), fixture.operationB);
137
+ adapter.equal(await store.getOperation(fixture.catalogId, "missing-release", fixture.operationId), null);
138
+ adapter.equal(await store.getOperation(fixture.catalogId, fixture.releaseA, fixture.missingOperationId), null);
139
+ });
140
+ use("catalog schemas are batched, unique, ordered, and empty-safe", async ({ store, fixture }) => {
141
+ const firstSchemaId = fixture.schemaIds[0];
142
+ if (firstSchemaId === undefined)
143
+ throw new Error("Conformance fixture has no schema");
144
+ adapter.deepEqual(await store.getSchemas(fixture.catalogId, fixture.releaseA, [...fixture.schemaIds].reverse().concat(firstSchemaId)), fixture.schemasA);
145
+ adapter.deepEqual(await store.getSchemas(fixture.catalogId, fixture.releaseB, [...fixture.schemaIds].reverse()), fixture.schemasB);
146
+ adapter.deepEqual(await store.getSchemas(fixture.catalogId, fixture.releaseA, []), []);
147
+ });
148
+ use("catalog manifests admit and authenticate logical records", async ({ store, fixture }) => {
149
+ const admittedA = await admitManifest(await store.getManifest(fixture.catalogId, fixture.releaseA), fixture.trust, new ConformanceGenerationStore());
150
+ adapter.equal(admittedA.manifest.catalogId, fixture.catalogId);
151
+ adapter.equal(admittedA.manifest.releaseId, fixture.releaseA);
152
+ const operationA = await store.getOperation(fixture.catalogId, fixture.releaseA, fixture.operationId);
153
+ adapter.ok(operationA);
154
+ if (operationA === null)
155
+ throw new Error("Conformance operation A is absent");
156
+ adapter.deepEqual(await verifyStoredRecord(admittedA, operationA), {
157
+ ...fixture.operationA.record,
158
+ tags: [],
159
+ });
160
+ const schemasA = await store.getSchemas(fixture.catalogId, fixture.releaseA, fixture.schemaIds);
161
+ for (const [index, row] of schemasA.entries()) {
162
+ adapter.deepEqual(await verifyStoredRecord(admittedA, row), fixture.schemasA[index]?.record);
163
+ }
164
+ const admittedB = await admitManifest(await store.getManifest(fixture.catalogId, fixture.releaseB), fixture.trust, new ConformanceGenerationStore());
165
+ adapter.equal(admittedB.manifest.catalogId, fixture.catalogId);
166
+ adapter.equal(admittedB.manifest.releaseId, fixture.releaseB);
167
+ const operationB = await store.getOperation(fixture.catalogId, fixture.releaseB, fixture.operationId);
168
+ adapter.ok(operationB);
169
+ if (operationB === null)
170
+ throw new Error("Conformance operation B is absent");
171
+ adapter.deepEqual(await verifyStoredRecord(admittedB, operationB), {
172
+ ...fixture.operationB.record,
173
+ tags: [],
174
+ });
175
+ });
176
+ use("catalog verification rejects tampered logical content", async ({ store, fixture }) => {
177
+ const admitted = await admitManifest(await store.getManifest(fixture.catalogId, fixture.releaseA), fixture.trust, new ConformanceGenerationStore());
178
+ const operation = await store.getOperation(fixture.catalogId, fixture.releaseA, fixture.operationId);
179
+ adapter.ok(operation);
180
+ if (operation === null)
181
+ throw new Error("Conformance operation is absent");
182
+ await adapter.rejects(() => verifyStoredRecord(admitted, {
183
+ ...operation,
184
+ record: { ...operation.record, summary: "tampered" },
185
+ }), "RECORD_DIGEST_MISMATCH");
186
+ });
187
+ use("catalog identity and absence are enforced across every record kind", async ({ store, fixture }) => {
188
+ const wrongCatalog = "missing-catalog";
189
+ const missingRelease = "missing-release";
190
+ const missingSchema = "schema:conformance:#/components/schemas/Missing";
191
+ for (const [catalogId, releaseId] of [
192
+ [wrongCatalog, fixture.releaseA],
193
+ [fixture.catalogId, missingRelease],
194
+ ]) {
195
+ await adapter.rejects(() => store.getManifest(catalogId, releaseId), "MANIFEST_INVALID");
196
+ adapter.equal(await store.getOperation(catalogId, releaseId, fixture.operationId), null);
197
+ await adapter.rejects(() => store.getSchemas(catalogId, releaseId, fixture.schemaIds), "RECORD_DIGEST_MISMATCH");
198
+ }
199
+ await adapter.rejects(() => store.getSchemas(fixture.catalogId, fixture.releaseA, [
200
+ missingSchema,
201
+ ]), "RECORD_DIGEST_MISMATCH");
202
+ await assertPublicError(adapter, () => store.searchCandidates({ query: '"', api: fixture.api, limit: 1 }), "INPUT_INVALID", CATALOG_STORE_PUBLIC_MESSAGES.searchExpressionInvalid);
203
+ });
204
+ use("catalog rejects malformed caller identities", async ({ store, fixture }) => {
205
+ await assertPublicError(adapter, () => store.getOperation(fixture.catalogId, fixture.releaseA, "schema:conformance:not-an-operation"), "INPUT_INVALID", CATALOG_STORE_PUBLIC_MESSAGES.operationIdentityInvalid);
206
+ await assertPublicError(adapter, () => store.getSchemas(fixture.catalogId, fixture.releaseA, [
207
+ "operation:conformance:not-a-schema",
208
+ ]), "INPUT_INVALID", CATALOG_STORE_PUBLIC_MESSAGES.schemaIdentityInvalid);
209
+ });
210
+ use("catalog rejects non-string identity values without coercion", async ({ store, fixture }) => {
211
+ let coercions = 0;
212
+ const coercible = Object.freeze({
213
+ toString() {
214
+ coercions += 1;
215
+ return fixture.catalogId;
216
+ },
217
+ });
218
+ for (const [label, invalid] of [
219
+ ["number", 42],
220
+ ["null", null],
221
+ ["object", coercible],
222
+ ]) {
223
+ const attempts = [
224
+ [
225
+ () => store.getManifest(invalid, fixture.releaseA),
226
+ CATALOG_STORE_PUBLIC_MESSAGES.catalogIdentityInvalid,
227
+ ],
228
+ [
229
+ () => store.getManifest(fixture.catalogId, invalid),
230
+ CATALOG_STORE_PUBLIC_MESSAGES.releaseIdentityInvalid,
231
+ ],
232
+ [
233
+ () => store.searchCandidates({
234
+ query: fixture.searchQuery,
235
+ api: invalid,
236
+ limit: 1,
237
+ }),
238
+ CATALOG_STORE_PUBLIC_MESSAGES.catalogIdentityInvalid,
239
+ ],
240
+ [
241
+ () => store.getOperation(invalid, fixture.releaseA, fixture.operationId),
242
+ CATALOG_STORE_PUBLIC_MESSAGES.catalogIdentityInvalid,
243
+ ],
244
+ [
245
+ () => store.getOperation(fixture.catalogId, invalid, fixture.operationId),
246
+ CATALOG_STORE_PUBLIC_MESSAGES.releaseIdentityInvalid,
247
+ ],
248
+ [
249
+ () => store.getOperation(fixture.catalogId, fixture.releaseA, invalid),
250
+ CATALOG_STORE_PUBLIC_MESSAGES.operationIdentityInvalid,
251
+ ],
252
+ [
253
+ () => store.getSchemas(invalid, fixture.releaseA, fixture.schemaIds),
254
+ CATALOG_STORE_PUBLIC_MESSAGES.catalogIdentityInvalid,
255
+ ],
256
+ [
257
+ () => store.getSchemas(fixture.catalogId, invalid, fixture.schemaIds),
258
+ CATALOG_STORE_PUBLIC_MESSAGES.releaseIdentityInvalid,
259
+ ],
260
+ [
261
+ () => store.getSchemas(fixture.catalogId, fixture.releaseA, [
262
+ invalid,
263
+ ]),
264
+ CATALOG_STORE_PUBLIC_MESSAGES.schemaIdentityInvalid,
265
+ ],
266
+ ];
267
+ for (const [run, message] of attempts) {
268
+ await assertPublicError(adapter, run, "INPUT_INVALID", message);
269
+ }
270
+ adapter.equal(coercions, 0, `${label} was coerced`);
271
+ }
272
+ });
273
+ use("catalog snapshots only plain data-only search and schema requests", async ({ store, fixture }) => {
274
+ let searchGetterCalls = 0;
275
+ const changingSearch = {};
276
+ for (const [key, first, second] of [
277
+ ["query", fixture.searchQuery, '"'],
278
+ ["api", fixture.api, "other-api"],
279
+ ["limit", 1, 2],
280
+ ]) {
281
+ let current = first;
282
+ Object.defineProperty(changingSearch, key, {
283
+ enumerable: true,
284
+ get() {
285
+ searchGetterCalls += 1;
286
+ const result = current;
287
+ current = second;
288
+ return result;
289
+ },
290
+ });
291
+ }
292
+ const inheritedSearch = Object.create({
293
+ query: fixture.searchQuery,
294
+ api: fixture.api,
295
+ limit: 1,
296
+ });
297
+ for (const hostile of [changingSearch, inheritedSearch]) {
298
+ await assertPublicError(adapter, () => store.searchCandidates(hostile), "INPUT_INVALID", CATALOG_STORE_PUBLIC_MESSAGES.searchQueryInvalid);
299
+ }
300
+ adapter.equal(searchGetterCalls, 0);
301
+ let iteratorCalls = 0;
302
+ const nonPlain = Object.setPrototypeOf([...fixture.schemaIds], Object.create(Array.prototype));
303
+ const sparse = new Array(fixture.schemaIds.length);
304
+ const extraKey = [...fixture.schemaIds];
305
+ extraKey.extra = "untrusted";
306
+ const customIterator = [...fixture.schemaIds];
307
+ Object.defineProperty(customIterator, Symbol.iterator, {
308
+ enumerable: false,
309
+ value: function* () {
310
+ iteratorCalls += 1;
311
+ yield* fixture.schemaIds;
312
+ },
313
+ });
314
+ for (const hostile of [nonPlain, sparse, extraKey, customIterator]) {
315
+ await assertPublicError(adapter, () => store.getSchemas(fixture.catalogId, fixture.releaseA, hostile), "INPUT_INVALID", CATALOG_STORE_PUBLIC_MESSAGES.schemaRequestInvalid);
316
+ }
317
+ adapter.equal(iteratorCalls, 0);
318
+ });
319
+ use("catalog results are detached from subsequent reads", async ({ store, fixture }) => {
320
+ const operation = await store.getOperation(fixture.catalogId, fixture.releaseA, fixture.operationId);
321
+ if (operation === null)
322
+ throw new Error("Conformance operation is absent");
323
+ attemptMutation(() => {
324
+ operation.logicalDigest = "0".repeat(64);
325
+ });
326
+ attemptMutation(() => {
327
+ operation.record.summary = "mutated";
328
+ });
329
+ attemptMutation(() => {
330
+ operation.record.schemaIds.push("schema:conformance:#/components/schemas/Mutation");
331
+ });
332
+ const schemas = await store.getSchemas(fixture.catalogId, fixture.releaseA, fixture.schemaIds);
333
+ attemptMutation(() => {
334
+ schemas[0].logicalDigest = "0".repeat(64);
335
+ });
336
+ attemptMutation(() => {
337
+ const schema = schemas[0]?.record.schema;
338
+ schema.title = "mutated";
339
+ });
340
+ adapter.deepEqual(await store.getOperation(fixture.catalogId, fixture.releaseA, fixture.operationId), fixture.operationA);
341
+ adapter.deepEqual(await store.getSchemas(fixture.catalogId, fixture.releaseA, fixture.schemaIds), fixture.schemasA);
342
+ });
343
+ }
@@ -0,0 +1 @@
1
+ export * from "./compiler.ts";
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export * from "./compiler.js";
package/dist/load.d.ts ADDED
@@ -0,0 +1,31 @@
1
+ export interface OpenApiOperation {
2
+ operationId?: string;
3
+ summary?: string;
4
+ description?: string;
5
+ tags?: string[];
6
+ deprecated?: boolean;
7
+ parameters?: unknown[];
8
+ requestBody?: unknown;
9
+ [key: string]: unknown;
10
+ }
11
+ export interface OpenApiDoc {
12
+ openapi: string;
13
+ servers?: Array<{
14
+ url: string;
15
+ }>;
16
+ paths: Record<string, Record<string, OpenApiOperation>>;
17
+ components?: {
18
+ schemas?: Record<string, unknown>;
19
+ parameters?: Record<string, unknown>;
20
+ responses?: Record<string, unknown>;
21
+ };
22
+ [key: string]: unknown;
23
+ }
24
+ /**
25
+ * Reads an OpenAPI 3.x document from disk. Under Bun, YAML is parsed with
26
+ * Bun's native parser, which handles the 44 MB Microsoft Graph document in
27
+ * ~0.5s. Under Node, it falls back to the `yaml` package, which takes ~12.6s
28
+ * on the same document — acceptable for a compile step, and JSON specs skip
29
+ * YAML parsing entirely either way.
30
+ */
31
+ export declare function loadSpec(path: string): Promise<OpenApiDoc>;
package/dist/load.js ADDED
@@ -0,0 +1,20 @@
1
+ import { readFile } from "node:fs/promises";
2
+ /**
3
+ * Reads an OpenAPI 3.x document from disk. Under Bun, YAML is parsed with
4
+ * Bun's native parser, which handles the 44 MB Microsoft Graph document in
5
+ * ~0.5s. Under Node, it falls back to the `yaml` package, which takes ~12.6s
6
+ * on the same document — acceptable for a compile step, and JSON specs skip
7
+ * YAML parsing entirely either way.
8
+ */
9
+ export async function loadSpec(path) {
10
+ const text = await readFile(path, "utf8");
11
+ const doc = (path.endsWith(".json")
12
+ ? JSON.parse(text)
13
+ : typeof Bun !== "undefined"
14
+ ? Bun.YAML.parse(text)
15
+ : (await import("yaml")).parse(text));
16
+ if (!doc || typeof doc !== "object" || typeof doc.paths !== "object") {
17
+ throw new Error(`${path}: not an OpenAPI document (no "paths" object)`);
18
+ }
19
+ return doc;
20
+ }
@@ -0,0 +1,27 @@
1
+ import type { OpenApiDoc } from "./load.ts";
2
+ import type { OperationRecord } from "./types.ts";
3
+ /** The gatekeeper caps rendered descriptions at 600 characters. */
4
+ export declare const MAX_SUMMARY = 600;
5
+ /**
6
+ * Splits identifier-style text into lowercase words for fts indexing.
7
+ * fts5's unicode61 tokenizer already folds case but treats `sendMail` as one
8
+ * token, so `MATCH 'send mail'` misses it — this widens the indexed text with
9
+ * a word-split shadow so both the joined and split forms are searchable.
10
+ * Splits camelCase boundaries (including acronym runs: `getURLValue` →
11
+ * `get url value`), `. / _ - { }` separators, and whitespace, then
12
+ * lowercases and collapses runs of whitespace. Deduping words is not required.
13
+ */
14
+ export declare function splitWords(text: string): string;
15
+ /**
16
+ * Canonicalize the URI-fragment representation of an RFC 6901 pointer.
17
+ *
18
+ * The returned value is an internal, already URI-decoded physical address. It
19
+ * must not be fed through URI decoding a second time.
20
+ */
21
+ export declare function canonicalLocalPointer(ref: string): string;
22
+ /** Resolve an already canonicalized physical JSON Pointer address. */
23
+ export declare function resolveCanonicalPointer(doc: unknown, pointer: string): unknown;
24
+ /** Resolve a URI-decoded, one-pass RFC 6901 fragment through own properties only. */
25
+ export declare function resolveLocalPointer(doc: unknown, ref: string): unknown;
26
+ /** Extracts one thin record per operation. `$ref`s are stored, never resolved. */
27
+ export declare function extractOperations(doc: OpenApiDoc, api: string): OperationRecord[];
@@ -0,0 +1,231 @@
1
+ import { classifySafety, riskFor } from "./safety.js";
2
+ /** The gatekeeper caps rendered descriptions at 600 characters. */
3
+ export const MAX_SUMMARY = 600;
4
+ /**
5
+ * Splits identifier-style text into lowercase words for fts indexing.
6
+ * fts5's unicode61 tokenizer already folds case but treats `sendMail` as one
7
+ * token, so `MATCH 'send mail'` misses it — this widens the indexed text with
8
+ * a word-split shadow so both the joined and split forms are searchable.
9
+ * Splits camelCase boundaries (including acronym runs: `getURLValue` →
10
+ * `get url value`), `. / _ - { }` separators, and whitespace, then
11
+ * lowercases and collapses runs of whitespace. Deduping words is not required.
12
+ */
13
+ export function splitWords(text) {
14
+ return text
15
+ .replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2")
16
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
17
+ .split(/[._/\-{}\s]+/)
18
+ .filter(Boolean)
19
+ .join(" ")
20
+ .toLowerCase();
21
+ }
22
+ const HTTP_METHODS = new Set([
23
+ "get",
24
+ "post",
25
+ "put",
26
+ "patch",
27
+ "delete",
28
+ "head",
29
+ "options",
30
+ ]);
31
+ const FORBIDDEN_POINTER_KEYS = new Set([
32
+ "__proto__",
33
+ "prototype",
34
+ "constructor",
35
+ ]);
36
+ /**
37
+ * Canonicalize the URI-fragment representation of an RFC 6901 pointer.
38
+ *
39
+ * The returned value is an internal, already URI-decoded physical address. It
40
+ * must not be fed through URI decoding a second time.
41
+ */
42
+ export function canonicalLocalPointer(ref) {
43
+ if (ref === "")
44
+ return "#";
45
+ if (!ref.startsWith("#"))
46
+ throw new Error("local reference must be a JSON Pointer fragment");
47
+ let pointer;
48
+ try {
49
+ pointer = decodeURIComponent(ref.slice(1));
50
+ }
51
+ catch {
52
+ throw new Error("JSON Pointer contains an invalid URI escape");
53
+ }
54
+ if (pointer === "")
55
+ return "#";
56
+ if (!pointer.startsWith("/"))
57
+ throw new Error("local reference must be a JSON Pointer fragment");
58
+ const tokens = pointer.slice(1).split("/");
59
+ for (const rawToken of tokens) {
60
+ if (/(?:~(?:[^01]|$))/.test(rawToken))
61
+ throw new Error("JSON Pointer contains an invalid ~ escape");
62
+ const token = rawToken.replace(/~1/g, "/").replace(/~0/g, "~");
63
+ if (FORBIDDEN_POINTER_KEYS.has(token))
64
+ throw new Error("JSON Pointer contains a forbidden prototype key");
65
+ }
66
+ return `#/${tokens.join("/")}`;
67
+ }
68
+ /** Resolve an already canonicalized physical JSON Pointer address. */
69
+ export function resolveCanonicalPointer(doc, pointer) {
70
+ if (pointer === "#")
71
+ return doc;
72
+ if (!pointer.startsWith("#/"))
73
+ throw new Error("canonical pointer must be a JSON Pointer fragment");
74
+ let node = doc;
75
+ for (const rawToken of pointer.slice(2).split("/")) {
76
+ if (/(?:~(?:[^01]|$))/.test(rawToken)) {
77
+ throw new Error("JSON Pointer contains an invalid ~ escape");
78
+ }
79
+ const token = rawToken.replace(/~1/g, "/").replace(/~0/g, "~");
80
+ if (FORBIDDEN_POINTER_KEYS.has(token)) {
81
+ throw new Error("JSON Pointer contains a forbidden prototype key");
82
+ }
83
+ if (Array.isArray(node)) {
84
+ if (!/^(?:0|[1-9][0-9]*)$/.test(token)) {
85
+ throw new Error("JSON Pointer array index is not canonical");
86
+ }
87
+ const index = Number(token);
88
+ if (!Number.isSafeInteger(index) ||
89
+ index >= node.length ||
90
+ !Object.hasOwn(node, index)) {
91
+ throw new Error("JSON Pointer array index was not found");
92
+ }
93
+ node = node[index];
94
+ continue;
95
+ }
96
+ if (typeof node !== "object" ||
97
+ node === null ||
98
+ !Object.hasOwn(node, token)) {
99
+ throw new Error("JSON Pointer target was not found");
100
+ }
101
+ const descriptor = Object.getOwnPropertyDescriptor(node, token);
102
+ if (descriptor === undefined || !("value" in descriptor)) {
103
+ throw new Error("JSON Pointer target is not an own data property");
104
+ }
105
+ node = descriptor.value;
106
+ }
107
+ return node;
108
+ }
109
+ /** Resolve a URI-decoded, one-pass RFC 6901 fragment through own properties only. */
110
+ export function resolveLocalPointer(doc, ref) {
111
+ return resolveCanonicalPointer(doc, canonicalLocalPointer(ref));
112
+ }
113
+ function collectParams(doc, pathItem, op) {
114
+ const raw = [
115
+ ...(pathItem.parameters ?? []),
116
+ ...(op.parameters ?? []),
117
+ ];
118
+ // OpenAPI identifies a parameter by `(name, in)` and lets the operation
119
+ // override the path item's. Operation entries come second, so a plain
120
+ // last-write-wins keyed insert gives exactly that precedence — whereas
121
+ // concatenating would emit both and leave the caller two conflicting
122
+ // schemas for one parameter.
123
+ const out = new Map();
124
+ for (const entry of raw) {
125
+ const p = (typeof entry === "object" && entry !== null && "$ref" in entry
126
+ ? resolveLocalPointer(doc, entry.$ref)
127
+ : entry);
128
+ if (!p || typeof p.name !== "string")
129
+ continue;
130
+ const where = typeof p.in === "string" ? p.in : "query";
131
+ out.set(`${where}:${p.name}`, {
132
+ name: p.name,
133
+ in: where,
134
+ required: p.required === true,
135
+ schema: p.schema ?? { type: "string" },
136
+ });
137
+ }
138
+ return [...out.values()];
139
+ }
140
+ const NO_BODY = { ref: null, schemaJson: null, mediaType: null };
141
+ /**
142
+ * Captures an operation's request-body contract. A `$ref` is kept as a ref and
143
+ * resolved lazily from the schema store, which is the whole point of the thin
144
+ * artifact. Anything else — an inline schema, or a body under a non-JSON media
145
+ * type — is stored verbatim instead of dropped: a write whose body contract is
146
+ * missing is either unusable or, worse, executable with no validation at all.
147
+ */
148
+ function bodyOf(op) {
149
+ const rb = op.requestBody;
150
+ if (!rb)
151
+ return NO_BODY;
152
+ if (typeof rb.$ref === "string") {
153
+ return { ref: rb.$ref, schemaJson: null, mediaType: null };
154
+ }
155
+ const content = rb.content;
156
+ if (!content)
157
+ return NO_BODY;
158
+ // Prefer JSON, but fall back to whatever the operation actually declares —
159
+ // the media type travels with the schema so the server can set Content-Type.
160
+ const mediaType = "application/json" in content
161
+ ? "application/json"
162
+ : Object.keys(content)[0];
163
+ if (mediaType === undefined)
164
+ return NO_BODY;
165
+ const schema = content[mediaType]?.schema;
166
+ if (schema === undefined)
167
+ return NO_BODY;
168
+ const ref = schema.$ref;
169
+ return typeof ref === "string"
170
+ ? { ref, schemaJson: null, mediaType }
171
+ : { ref: null, schemaJson: JSON.stringify(schema), mediaType };
172
+ }
173
+ /** Extracts one thin record per operation. `$ref`s are stored, never resolved. */
174
+ export function extractOperations(doc, api) {
175
+ const serverUrl = doc.servers?.[0]?.url ?? "";
176
+ if (!serverUrl)
177
+ throw new Error("document declares no servers[0].url");
178
+ const seen = new Set();
179
+ const records = [];
180
+ for (const [path, pathItem] of Object.entries(doc.paths)) {
181
+ if (pathItem.servers !== undefined) {
182
+ throw new Error(`${path}: path-item-level "servers" overrides are not supported`);
183
+ }
184
+ for (const [method, op] of Object.entries(pathItem)) {
185
+ if (!HTTP_METHODS.has(method))
186
+ continue;
187
+ if (op.servers !== undefined) {
188
+ throw new Error(`${method.toUpperCase()} ${path}: operation-level "servers" overrides are not supported`);
189
+ }
190
+ const operationId = op.operationId;
191
+ if (typeof operationId !== "string" || operationId.length === 0) {
192
+ throw new Error(`${method.toUpperCase()} ${path}: missing operationId`);
193
+ }
194
+ const qualifiedId = `${api}:${operationId}`;
195
+ if (seen.has(qualifiedId)) {
196
+ throw new Error(`duplicate operationId: ${operationId}`);
197
+ }
198
+ seen.add(qualifiedId);
199
+ const upper = method.toUpperCase();
200
+ const body = bodyOf(op);
201
+ const safety = classifySafety(upper, path, operationId, api);
202
+ const summary = (op.summary ?? op.description ?? null)?.slice(0, MAX_SUMMARY);
203
+ records.push({
204
+ qualifiedId,
205
+ api,
206
+ operationId,
207
+ method: upper,
208
+ path,
209
+ safety,
210
+ risk: riskFor(safety, null, path),
211
+ operationType: typeof op["x-ms-docs-operation-type"] === "string"
212
+ ? op["x-ms-docs-operation-type"]
213
+ : null,
214
+ pageable: op["x-ms-pageable"] !== undefined,
215
+ deprecated: op.deprecated === true,
216
+ permissions: null,
217
+ permConfidence: null,
218
+ privilegeLevel: null,
219
+ summary: summary ?? null,
220
+ tags: Array.isArray(op.tags) ? op.tags.join(" ") : null,
221
+ paramsJson: JSON.stringify(collectParams(doc, pathItem, op)),
222
+ searchText: `${splitWords(operationId)} ${splitWords(path)}`,
223
+ bodyRef: body.ref,
224
+ bodySchemaJson: body.schemaJson,
225
+ bodyMediaType: body.mediaType,
226
+ serverUrl,
227
+ });
228
+ }
229
+ }
230
+ return records;
231
+ }