@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,20 @@
1
+ import { type CatalogId, type CatalogStore, type ManifestEnvelope, type OperationRecordV4, type ReleaseId, type RuntimeLimits, type SchemaRecordV4, type SearchQuery, type StoredRecord, type TypedOperationId, type TypedSchemaId } from "../runtime/index.ts";
2
+ export interface LegacyV3CatalogIdentity {
3
+ readonly catalogId: CatalogId;
4
+ readonly releaseId: ReleaseId;
5
+ }
6
+ export interface SqliteCatalogStoreOptions {
7
+ readonly limits?: Partial<RuntimeLimits>;
8
+ readonly legacyIdentity?: LegacyV3CatalogIdentity;
9
+ }
10
+ /** Read-only v4/v5 store with an explicit inventory-only v3 migration mode. */
11
+ export declare class SqliteCatalogStore implements CatalogStore {
12
+ #private;
13
+ readonly legacyInventoryOnly: boolean;
14
+ constructor(path: string, options?: SqliteCatalogStoreOptions);
15
+ close(): void;
16
+ getManifest(catalog: CatalogId, release: ReleaseId): Promise<ManifestEnvelope>;
17
+ searchCandidates(query: SearchQuery): Promise<readonly import("../runtime/types.ts").CandidateRef[]>;
18
+ getOperation(catalog: CatalogId, release: ReleaseId, id: TypedOperationId): Promise<StoredRecord<OperationRecordV4> | null>;
19
+ getSchemas(catalog: CatalogId, release: ReleaseId, ids: readonly TypedSchemaId[]): Promise<readonly StoredRecord<SchemaRecordV4>[]>;
20
+ }
@@ -0,0 +1,370 @@
1
+ import { DatabaseSync } from "node:sqlite";
2
+ import { createD1CatalogStore, OpenApiMcpError, parseTypedRecordId, resolveRuntimeLimits, } from "../runtime/index.js";
3
+ import { parseJsonStrict } from "../runtime/strict-json.js";
4
+ import { MAX_SEARCH_QUERY_BYTES } from "../runtime/versions.js";
5
+ import { releaseFileIdentity, verifyReleaseCompletion, } from "./release-completion.js";
6
+ const SCHEMA_PROBE = `SELECT CASE
7
+ WHEN typeof(name) = 'text' AND name IN ('meta', 'release_metadata') THEN name
8
+ ELSE NULL
9
+ END AS name,
10
+ CASE
11
+ WHEN typeof(type) = 'text' AND type = 'table' THEN type
12
+ ELSE NULL
13
+ END AS type
14
+ FROM sqlite_schema
15
+ WHERE name IN ('meta', 'release_metadata')
16
+ ORDER BY name COLLATE BINARY
17
+ LIMIT 3;`;
18
+ const V3_VERSION_PROBE = `SELECT CASE
19
+ WHEN typeof(value) = 'text' AND length(CAST(value AS BLOB)) <= 1 THEN value
20
+ ELSE NULL
21
+ END AS value
22
+ FROM meta
23
+ WHERE key = ?
24
+ LIMIT 2;`;
25
+ const V4_VERSION_PROBE = `SELECT DISTINCT
26
+ CASE
27
+ WHEN typeof(format) = 'integer' AND length(CAST(format AS BLOB)) <= 1 THEN format
28
+ ELSE NULL
29
+ END AS format,
30
+ CASE
31
+ WHEN typeof(contract) = 'integer' AND length(CAST(contract AS BLOB)) <= 1 THEN contract
32
+ ELSE NULL
33
+ END AS contract
34
+ FROM release_metadata
35
+ ORDER BY format, contract
36
+ LIMIT 3;`;
37
+ const V3_SEARCH_SQL = `SELECT CASE
38
+ WHEN typeof(o.qualified_id) = 'text'
39
+ AND length(CAST(o.qualified_id AS BLOB)) <= ?
40
+ THEN o.qualified_id
41
+ ELSE NULL
42
+ END AS qualified_id
43
+ FROM operations_fts
44
+ JOIN operations AS o ON o.rowid = operations_fts.rowid
45
+ WHERE operations_fts MATCH ?
46
+ AND (? IS NULL OR o.api = ?)
47
+ ORDER BY bm25(operations_fts), o.qualified_id COLLATE BINARY
48
+ LIMIT ?;`;
49
+ const legacyQualifiedIdBytes = 641;
50
+ function unsupported(message) {
51
+ return new OpenApiMcpError("ARTIFACT_FORMAT_UNSUPPORTED", message);
52
+ }
53
+ function segment(value) {
54
+ try {
55
+ if (typeof value !== "string")
56
+ throw new Error();
57
+ parseTypedRecordId(`operation:${value}:x`);
58
+ return value;
59
+ }
60
+ catch {
61
+ throw unsupported("Catalog identity is invalid");
62
+ }
63
+ }
64
+ function snapshotLegacyIdentity(options) {
65
+ try {
66
+ const legacyIdentity = Object.getOwnPropertyDescriptor(options, "legacyIdentity");
67
+ if (legacyIdentity === undefined)
68
+ return undefined;
69
+ if (!legacyIdentity.enumerable || !("value" in legacyIdentity))
70
+ throw new Error();
71
+ if (legacyIdentity.value === undefined)
72
+ return undefined;
73
+ const value = legacyIdentity.value;
74
+ if (typeof value !== "object" || value === null || Array.isArray(value))
75
+ throw new Error();
76
+ const prototype = Object.getPrototypeOf(value);
77
+ if (prototype !== null && prototype !== Object.prototype)
78
+ throw new Error();
79
+ if (Object.getOwnPropertySymbols(value).length !== 0)
80
+ throw new Error();
81
+ const keys = Object.getOwnPropertyNames(value).sort();
82
+ if (keys.length !== 2 || keys[0] !== "catalogId" || keys[1] !== "releaseId")
83
+ throw new Error();
84
+ const catalogId = Object.getOwnPropertyDescriptor(value, "catalogId");
85
+ const releaseId = Object.getOwnPropertyDescriptor(value, "releaseId");
86
+ if (!catalogId?.enumerable ||
87
+ !("value" in catalogId) ||
88
+ !releaseId?.enumerable ||
89
+ !("value" in releaseId))
90
+ throw new Error();
91
+ return Object.freeze({
92
+ catalogId: segment(catalogId.value),
93
+ releaseId: segment(releaseId.value),
94
+ });
95
+ }
96
+ catch (error) {
97
+ if (error instanceof OpenApiMcpError)
98
+ throw error;
99
+ throw unsupported("Catalog identity is invalid");
100
+ }
101
+ }
102
+ function snapshotLegacySearchQuery(value, limits) {
103
+ try {
104
+ if (typeof value !== "object" || value === null || Array.isArray(value))
105
+ throw new Error();
106
+ const prototype = Object.getPrototypeOf(value);
107
+ if (prototype !== null && prototype !== Object.prototype)
108
+ throw new Error();
109
+ if (Object.getOwnPropertySymbols(value).length !== 0)
110
+ throw new Error();
111
+ const api = Object.getOwnPropertyDescriptor(value, "api");
112
+ const expected = api === undefined ? ["limit", "query"] : ["api", "limit", "query"];
113
+ const keys = Object.getOwnPropertyNames(value).sort();
114
+ if (keys.length !== expected.length ||
115
+ keys.some((key, index) => key !== expected[index]))
116
+ throw new Error();
117
+ const query = Object.getOwnPropertyDescriptor(value, "query");
118
+ const limit = Object.getOwnPropertyDescriptor(value, "limit");
119
+ if (!query?.enumerable ||
120
+ !("value" in query) ||
121
+ !limit?.enumerable ||
122
+ !("value" in limit) ||
123
+ (api !== undefined && (!api.enumerable || !("value" in api))))
124
+ throw new Error();
125
+ const queryValue = query.value;
126
+ if (typeof queryValue !== "string" ||
127
+ queryValue.length > MAX_SEARCH_QUERY_BYTES ||
128
+ new TextEncoder().encode(queryValue).byteLength >
129
+ MAX_SEARCH_QUERY_BYTES ||
130
+ queryValue.length === 0 ||
131
+ !Number.isSafeInteger(limit.value) ||
132
+ limit.value < 1 ||
133
+ limit.value > limits.maxSearchResults)
134
+ throw new OpenApiMcpError("INPUT_INVALID", "Search input is invalid");
135
+ let apiValue = null;
136
+ if (api !== undefined && api.value !== undefined) {
137
+ searchApi(api.value);
138
+ apiValue = api.value;
139
+ }
140
+ return {
141
+ query: queryValue,
142
+ api: apiValue,
143
+ limit: limit.value,
144
+ };
145
+ }
146
+ catch (error) {
147
+ if (error instanceof OpenApiMcpError)
148
+ throw error;
149
+ throw new OpenApiMcpError("INPUT_INVALID", "Search input is invalid");
150
+ }
151
+ }
152
+ function searchApi(value) {
153
+ try {
154
+ if (typeof value !== "string")
155
+ throw new Error();
156
+ parseTypedRecordId(`operation:${value}:x`);
157
+ }
158
+ catch {
159
+ throw new OpenApiMcpError("INPUT_INVALID", "Search API is invalid");
160
+ }
161
+ }
162
+ function snapshotSqliteRuntimeLimits(options) {
163
+ let overrides;
164
+ try {
165
+ if (typeof options !== "object" ||
166
+ options === null ||
167
+ Array.isArray(options))
168
+ throw new Error();
169
+ const limits = Object.getOwnPropertyDescriptor(options, "limits");
170
+ if (limits !== undefined) {
171
+ if (!limits.enumerable || !("value" in limits))
172
+ throw new Error();
173
+ overrides = limits.value;
174
+ }
175
+ }
176
+ catch {
177
+ throw new RangeError("Runtime limits overrides must be an exact plain data object");
178
+ }
179
+ return resolveRuntimeLimits(overrides);
180
+ }
181
+ function exactly(rows, name) {
182
+ return (rows.length === 1 &&
183
+ typeof rows[0] === "object" &&
184
+ rows[0] !== null &&
185
+ rows[0].name === name &&
186
+ rows[0].type === "table");
187
+ }
188
+ function isExecutableArtifact(rows) {
189
+ return (rows.length > 0 &&
190
+ rows.length <= 2 &&
191
+ rows.every((row) => typeof row === "object" &&
192
+ row !== null &&
193
+ (row.format === 4 ||
194
+ row.format === 5) &&
195
+ row.contract === 1));
196
+ }
197
+ function bunSqlite() {
198
+ return process.getBuiltinModule?.("bun:sqlite");
199
+ }
200
+ function openReadOnlyDatabase(path) {
201
+ const module = bunSqlite();
202
+ if (module !== undefined)
203
+ return new module.Database(path, { readonly: true });
204
+ return new DatabaseSync(path, { readOnly: true });
205
+ }
206
+ function all(database, sql, values = []) {
207
+ const statement = database.prepare(sql);
208
+ try {
209
+ return statement.all(...values);
210
+ }
211
+ finally {
212
+ statement.finalize?.();
213
+ }
214
+ }
215
+ function first(database, sql, values) {
216
+ const statement = database.prepare(sql);
217
+ try {
218
+ return statement.get(...values);
219
+ }
220
+ finally {
221
+ statement.finalize?.();
222
+ }
223
+ }
224
+ function closeDatabase(database) {
225
+ database.close(true);
226
+ }
227
+ function bridge(database) {
228
+ return {
229
+ prepare(sql) {
230
+ let values = [];
231
+ return {
232
+ bind(...next) {
233
+ values = next;
234
+ return this;
235
+ },
236
+ async all() {
237
+ return {
238
+ success: true,
239
+ results: all(database, sql, values),
240
+ };
241
+ },
242
+ async first() {
243
+ return first(database, sql, values) ?? null;
244
+ },
245
+ };
246
+ },
247
+ };
248
+ }
249
+ /** Read-only v4/v5 store with an explicit inventory-only v3 migration mode. */
250
+ export class SqliteCatalogStore {
251
+ legacyInventoryOnly;
252
+ #database;
253
+ #v4;
254
+ #legacyIdentity;
255
+ #limits;
256
+ #path;
257
+ #payloadIdentity;
258
+ constructor(path, options = {}) {
259
+ this.#path = path;
260
+ this.#limits = snapshotSqliteRuntimeLimits(options);
261
+ let database;
262
+ try {
263
+ database = openReadOnlyDatabase(path);
264
+ const tables = all(database, SCHEMA_PROBE);
265
+ if (exactly(tables, "release_metadata")) {
266
+ const versions = all(database, V4_VERSION_PROBE);
267
+ if (!isExecutableArtifact(versions))
268
+ throw unsupported("Artifact format unsupported");
269
+ this.legacyInventoryOnly = false;
270
+ this.#v4 = createD1CatalogStore(bridge(database), this.#limits);
271
+ }
272
+ else if (exactly(tables, "meta")) {
273
+ const versions = all(database, V3_VERSION_PROBE, ["format_version"]);
274
+ if (versions.length !== 1 ||
275
+ typeof versions[0] !== "object" ||
276
+ versions[0] === null ||
277
+ versions[0].value !== "3")
278
+ throw unsupported("Artifact format is unsupported");
279
+ this.#legacyIdentity = snapshotLegacyIdentity(options);
280
+ this.legacyInventoryOnly = true;
281
+ }
282
+ else
283
+ throw unsupported("Artifact format is unsupported");
284
+ this.#database = database;
285
+ }
286
+ catch (error) {
287
+ if (database !== undefined)
288
+ closeDatabase(database);
289
+ if (error instanceof OpenApiMcpError)
290
+ throw error;
291
+ throw unsupported("Artifact format is unsupported");
292
+ }
293
+ }
294
+ close() {
295
+ if (this.#database !== undefined)
296
+ closeDatabase(this.#database);
297
+ this.#database = undefined;
298
+ }
299
+ #v4Store() {
300
+ if (!this.#database)
301
+ throw unsupported("Catalog store is closed");
302
+ if (!this.#v4)
303
+ throw unsupported("v3 artifacts are inventory-only");
304
+ return this.#v4;
305
+ }
306
+ async getManifest(catalog, release) {
307
+ const envelope = await this.#v4Store().getManifest(catalog, release);
308
+ // The signed envelope's format controls completion, never unsigned SQL metadata.
309
+ if (parseJsonStrict(envelope.manifestJson, {
310
+ maxBytes: this.#limits.maxManifestBytes,
311
+ maxDepth: this.#limits.maxJsonDepth,
312
+ maxKeys: this.#limits.maxManifestRecords + 100,
313
+ }).format === 5) {
314
+ this.#payloadIdentity ??= releaseFileIdentity(this.#path);
315
+ verifyReleaseCompletion(this.#path, this.#payloadIdentity, envelope, this.#limits);
316
+ }
317
+ return envelope;
318
+ }
319
+ async searchCandidates(query) {
320
+ if (!this.legacyInventoryOnly)
321
+ return this.#v4Store().searchCandidates(query);
322
+ if (!this.#database)
323
+ throw unsupported("Catalog store is closed");
324
+ const legacyIdentity = this.#legacyIdentity;
325
+ if (!legacyIdentity)
326
+ throw unsupported("v3 inventory requires an explicit catalog identity");
327
+ const snapshot = snapshotLegacySearchQuery(query, this.#limits);
328
+ try {
329
+ const rows = all(this.#database, V3_SEARCH_SQL, [
330
+ legacyQualifiedIdBytes,
331
+ snapshot.query,
332
+ snapshot.api,
333
+ snapshot.api,
334
+ snapshot.limit,
335
+ ]);
336
+ if (rows.length > snapshot.limit)
337
+ throw new Error();
338
+ const seen = new Set();
339
+ return rows.map((row) => {
340
+ const qualified = typeof row === "object" && row !== null
341
+ ? row.qualified_id
342
+ : undefined;
343
+ if (typeof qualified !== "string")
344
+ throw new Error();
345
+ if (snapshot.api !== null && !qualified.startsWith(`${snapshot.api}:`))
346
+ throw new Error();
347
+ const parsed = parseTypedRecordId(`operation:${qualified}`);
348
+ if (!parsed.startsWith("operation:") || seen.has(parsed))
349
+ throw new Error();
350
+ seen.add(parsed);
351
+ return {
352
+ catalogId: legacyIdentity.catalogId,
353
+ releaseId: legacyIdentity.releaseId,
354
+ operationId: parsed,
355
+ };
356
+ });
357
+ }
358
+ catch (error) {
359
+ if (error instanceof OpenApiMcpError)
360
+ throw error;
361
+ throw new OpenApiMcpError("INPUT_INVALID", "Search expression is invalid");
362
+ }
363
+ }
364
+ async getOperation(catalog, release, id) {
365
+ return await this.#v4Store().getOperation(catalog, release, id);
366
+ }
367
+ async getSchemas(catalog, release, ids) {
368
+ return await this.#v4Store().getSchemas(catalog, release, ids);
369
+ }
370
+ }
@@ -0,0 +1,24 @@
1
+ import type { LookupFunction } from "node:net";
2
+ export interface DestinationGuardOptions {
3
+ readonly allowedOrigins: readonly string[];
4
+ /** Trusted resolver only; every returned address still passes public policy. */
5
+ readonly lookup?: (hostname: string, options: {
6
+ all: true;
7
+ verbatim: true;
8
+ }) => Promise<readonly {
9
+ address: string;
10
+ family: number;
11
+ }[]>;
12
+ readonly now?: () => number;
13
+ readonly ttlMs?: number;
14
+ }
15
+ export interface ApprovedDestination {
16
+ readonly origin: string;
17
+ readonly expiresAt: number;
18
+ readonly lookup: LookupFunction;
19
+ }
20
+ export declare class NodeDestinationGuard {
21
+ #private;
22
+ constructor(options: DestinationGuardOptions);
23
+ authorize(value: URL, signal?: AbortSignal): Promise<ApprovedDestination>;
24
+ }
@@ -0,0 +1,140 @@
1
+ import { lookup as systemLookup } from "node:dns/promises";
2
+ import { BlockList, isIP } from "node:net";
3
+ import { OpenApiMcpError } from "../runtime/errors.js";
4
+ const deniedV4 = new BlockList();
5
+ for (const [network, prefix] of [
6
+ ["0.0.0.0", 8],
7
+ ["10.0.0.0", 8],
8
+ ["100.64.0.0", 10],
9
+ ["127.0.0.0", 8],
10
+ ["169.254.0.0", 16],
11
+ ["172.16.0.0", 12],
12
+ ["192.0.0.0", 24],
13
+ ["192.0.2.0", 24],
14
+ ["192.88.99.0", 24],
15
+ ["192.168.0.0", 16],
16
+ ["198.18.0.0", 15],
17
+ ["198.51.100.0", 24],
18
+ ["203.0.113.0", 24],
19
+ ["224.0.0.0", 4],
20
+ ["240.0.0.0", 4],
21
+ ])
22
+ deniedV4.addSubnet(network, prefix, "ipv4");
23
+ const publicV6 = new BlockList();
24
+ publicV6.addSubnet("2000::", 3, "ipv6");
25
+ const deniedV6 = new BlockList();
26
+ for (const [network, prefix] of [
27
+ ["2001::", 23],
28
+ ["2001:db8::", 32],
29
+ ["2002::", 16],
30
+ ["3fff::", 20],
31
+ ])
32
+ deniedV6.addSubnet(network, prefix, "ipv6");
33
+ function denied() {
34
+ return new OpenApiMcpError("DESTINATION_DENIED", "Destination is not permitted");
35
+ }
36
+ /** Conservative IANA special-purpose policy, including all mapped/translation IPv6. */
37
+ function isPublic(address, family) {
38
+ if (isIP(address) !== family || address.includes("%"))
39
+ return false;
40
+ if (family === 4)
41
+ return !deniedV4.check(address, "ipv4");
42
+ return (family === 6 &&
43
+ publicV6.check(address, "ipv6") &&
44
+ !deniedV6.check(address, "ipv6"));
45
+ }
46
+ export class NodeDestinationGuard {
47
+ #origins;
48
+ #resolve;
49
+ #now;
50
+ #ttl;
51
+ constructor(options) {
52
+ this.#origins = new Set(options.allowedOrigins.map((value) => {
53
+ const url = new URL(value);
54
+ if (url.protocol !== "https:" ||
55
+ url.username ||
56
+ url.password ||
57
+ url.search ||
58
+ url.hash ||
59
+ url.pathname !== "/")
60
+ throw denied();
61
+ return url.origin;
62
+ }));
63
+ this.#resolve = options.lookup ?? systemLookup;
64
+ this.#now = options.now ?? Date.now;
65
+ this.#ttl = options.ttlMs ?? 30_000;
66
+ if (!Number.isSafeInteger(this.#ttl) || this.#ttl < 1 || this.#ttl > 30_000)
67
+ throw new RangeError("Destination TTL must only lower 30000ms");
68
+ }
69
+ async authorize(value, signal) {
70
+ const url = new URL(value.href);
71
+ if (url.protocol !== "https:" ||
72
+ url.username ||
73
+ url.password ||
74
+ !this.#origins.has(url.origin))
75
+ throw denied();
76
+ const hostname = url.hostname.replace(/^\[|\]$/g, "");
77
+ const issuedAt = this.#now();
78
+ if (!Number.isFinite(issuedAt))
79
+ throw denied();
80
+ const expiresAt = issuedAt + this.#ttl;
81
+ let answers;
82
+ let timeout;
83
+ let abort;
84
+ try {
85
+ if (signal?.aborted)
86
+ throw denied();
87
+ const family = isIP(hostname);
88
+ answers = family
89
+ ? [{ address: hostname, family }]
90
+ : await Promise.race([
91
+ this.#resolve(hostname, { all: true, verbatim: true }),
92
+ new Promise((_, reject) => {
93
+ timeout = setTimeout(() => reject(denied()), this.#ttl);
94
+ }),
95
+ new Promise((_, reject) => {
96
+ abort = () => reject(denied());
97
+ signal?.addEventListener("abort", abort, { once: true });
98
+ }),
99
+ ]);
100
+ }
101
+ catch {
102
+ throw denied();
103
+ }
104
+ finally {
105
+ clearTimeout(timeout);
106
+ if (abort)
107
+ signal?.removeEventListener("abort", abort);
108
+ }
109
+ if (!Array.isArray(answers) ||
110
+ answers.length === 0 ||
111
+ answers.length > 64 ||
112
+ this.#now() >= expiresAt)
113
+ throw denied();
114
+ const pinned = answers.map(({ address, family }) => {
115
+ if (!isPublic(address, family))
116
+ throw denied();
117
+ return Object.freeze({ address, family });
118
+ });
119
+ const lookup = (requested, options, callback) => {
120
+ const time = this.#now();
121
+ if (requested !== hostname ||
122
+ !Number.isFinite(time) ||
123
+ time >= expiresAt) {
124
+ callback(denied(), "", 0);
125
+ return;
126
+ }
127
+ const eligible = pinned.filter((answer) => !options.family || answer.family === Number(options.family));
128
+ const first = eligible[0];
129
+ if (!first) {
130
+ callback(denied(), "", 0);
131
+ return;
132
+ }
133
+ if (options.all)
134
+ callback(null, eligible.map((answer) => ({ ...answer })));
135
+ else
136
+ callback(null, first.address, first.family);
137
+ };
138
+ return Object.freeze({ origin: url.origin, expiresAt, lookup });
139
+ }
140
+ }
@@ -0,0 +1,12 @@
1
+ import type { CatalogId, GenerationState, GenerationStore, GenerationTransition } from "../runtime/types.ts";
2
+ /** Stable bounded failure when another process retains the generation mutex. */
3
+ export declare class GenerationStoreContentionError extends Error {
4
+ constructor();
5
+ }
6
+ /** Node-only durable, user-private compare-and-swap generation storage. */
7
+ export declare class FileGenerationStore implements GenerationStore {
8
+ #private;
9
+ constructor(path: string);
10
+ get(catalogId: CatalogId, issuer: string): Promise<GenerationState | null>;
11
+ accept(catalogId: CatalogId, issuer: string, transition: GenerationTransition): Promise<GenerationState | null>;
12
+ }