@knitli/openapi-mcp 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/conformance/runtime-suite.js +38 -0
- package/dist/runtime/runtime.js +76 -24
- package/dist/runtime/serialize.js +7 -0
- package/dist/runtime/store.d.ts +5 -1
- package/dist/runtime/store.js +63 -4
- package/dist/runtime/types.d.ts +7 -0
- package/dist/sqlite/catalog-store.d.ts +1 -0
- package/dist/sqlite/catalog-store.js +3 -0
- package/dist/stdio/search.js +4 -0
- package/package.json +1 -1
|
@@ -111,6 +111,14 @@ export function runRuntimeConformanceSuite(factory, options) {
|
|
|
111
111
|
useFault("catalog rejects duplicate operation transport rows", { fault: "duplicate-operation" }, async ({ store, fixture }) => {
|
|
112
112
|
await assertPublicError(adapter, () => store.getOperation(fixture.catalogId, fixture.releaseA, fixture.operationId), "RECORD_DIGEST_MISMATCH", CATALOG_STORE_PUBLIC_MESSAGES.operationRowAmbiguous);
|
|
113
113
|
});
|
|
114
|
+
useFault("catalog rejects duplicate batched operation transport rows", { fault: "duplicate-operation" }, async ({ store, fixture }) => {
|
|
115
|
+
const getOperations = store.getOperations?.bind(store);
|
|
116
|
+
if (getOperations === undefined)
|
|
117
|
+
return;
|
|
118
|
+
await assertPublicError(adapter, () => getOperations(fixture.catalogId, fixture.releaseA, [
|
|
119
|
+
fixture.operationId,
|
|
120
|
+
]), "RECORD_DIGEST_MISMATCH", CATALOG_STORE_PUBLIC_MESSAGES.operationTransportTooManyRows);
|
|
121
|
+
});
|
|
114
122
|
useFault("catalog rejects duplicate schema transport rows", { fault: "duplicate-schemas" }, async ({ store, fixture }) => {
|
|
115
123
|
await assertPublicError(adapter, () => store.getSchemas(fixture.catalogId, fixture.releaseA, fixture.schemaIds), "RECORD_DIGEST_MISMATCH", CATALOG_STORE_PUBLIC_MESSAGES.schemaTransportTooManyRows);
|
|
116
124
|
});
|
|
@@ -128,6 +136,14 @@ export function runRuntimeConformanceSuite(factory, options) {
|
|
|
128
136
|
useFault("catalog redacts operation driver failures", { fault: "driver-operation", injectedDriverError }, async ({ store, fixture }) => {
|
|
129
137
|
await assertPublicError(adapter, () => store.getOperation(fixture.catalogId, fixture.releaseA, fixture.operationId), "RECORD_DIGEST_MISMATCH", CATALOG_STORE_PUBLIC_MESSAGES.recordTransportUnavailable, injectedDriverError);
|
|
130
138
|
});
|
|
139
|
+
useFault("catalog redacts batched operation driver failures", { fault: "driver-operation", injectedDriverError }, async ({ store, fixture }) => {
|
|
140
|
+
const getOperations = store.getOperations?.bind(store);
|
|
141
|
+
if (getOperations === undefined)
|
|
142
|
+
return;
|
|
143
|
+
await assertPublicError(adapter, () => getOperations(fixture.catalogId, fixture.releaseA, [
|
|
144
|
+
fixture.operationId,
|
|
145
|
+
]), "RECORD_DIGEST_MISMATCH", CATALOG_STORE_PUBLIC_MESSAGES.recordTransportUnavailable, injectedDriverError);
|
|
146
|
+
});
|
|
131
147
|
useFault("catalog redacts schema driver failures", { fault: "driver-schemas", injectedDriverError }, async ({ store, fixture }) => {
|
|
132
148
|
await assertPublicError(adapter, () => store.getSchemas(fixture.catalogId, fixture.releaseA, fixture.schemaIds), "RECORD_DIGEST_MISMATCH", CATALOG_STORE_PUBLIC_MESSAGES.recordTransportUnavailable, injectedDriverError);
|
|
133
149
|
});
|
|
@@ -137,6 +153,28 @@ export function runRuntimeConformanceSuite(factory, options) {
|
|
|
137
153
|
adapter.equal(await store.getOperation(fixture.catalogId, "missing-release", fixture.operationId), null);
|
|
138
154
|
adapter.equal(await store.getOperation(fixture.catalogId, fixture.releaseA, fixture.missingOperationId), null);
|
|
139
155
|
});
|
|
156
|
+
use("catalog batched operations match single reads and omit absent IDs", async ({ store, fixture }) => {
|
|
157
|
+
const getOperations = store.getOperations?.bind(store);
|
|
158
|
+
if (getOperations === undefined)
|
|
159
|
+
return;
|
|
160
|
+
adapter.deepEqual(await getOperations(fixture.catalogId, fixture.releaseA, [
|
|
161
|
+
fixture.missingOperationId,
|
|
162
|
+
fixture.operationId,
|
|
163
|
+
fixture.operationId,
|
|
164
|
+
]), [fixture.operationA]);
|
|
165
|
+
adapter.deepEqual(await getOperations(fixture.catalogId, fixture.releaseB, [
|
|
166
|
+
fixture.operationId,
|
|
167
|
+
]), [fixture.operationB]);
|
|
168
|
+
adapter.deepEqual(await getOperations(fixture.catalogId, "missing-release", [
|
|
169
|
+
fixture.operationId,
|
|
170
|
+
]), []);
|
|
171
|
+
adapter.deepEqual(await getOperations(fixture.catalogId, fixture.releaseA, []), []);
|
|
172
|
+
await assertPublicError(adapter, () => getOperations(fixture.catalogId, fixture.releaseA, [
|
|
173
|
+
"schema:conformance:not-an-operation",
|
|
174
|
+
]), "INPUT_INVALID", CATALOG_STORE_PUBLIC_MESSAGES.operationIdentityInvalid);
|
|
175
|
+
const sparse = new Array(1);
|
|
176
|
+
await assertPublicError(adapter, () => getOperations(fixture.catalogId, fixture.releaseA, sparse), "INPUT_INVALID", CATALOG_STORE_PUBLIC_MESSAGES.operationRequestInvalid);
|
|
177
|
+
});
|
|
140
178
|
use("catalog schemas are batched, unique, ordered, and empty-safe", async ({ store, fixture }) => {
|
|
141
179
|
const firstSchemaId = fixture.schemaIds[0];
|
|
142
180
|
if (firstSchemaId === undefined)
|
package/dist/runtime/runtime.js
CHANGED
|
@@ -27,6 +27,12 @@ function manifestKey(catalogId, releaseId) {
|
|
|
27
27
|
}
|
|
28
28
|
const maximumSearchReleases = 8;
|
|
29
29
|
const maximumInventorySchemaBatch = 128;
|
|
30
|
+
// Stores null record_json over maxRecordBytes, so one operation batch carries
|
|
31
|
+
// at most 16 MiB of raw row text; rows are then verified one at a time, so
|
|
32
|
+
// only one record's working copies are live at once. Sized for a Worker
|
|
33
|
+
// isolate. Schema batches keep the maxSchemaClosureBytes-derived bound
|
|
34
|
+
// because they serve closure resolution.
|
|
35
|
+
const OPERATION_BATCH_BYTE_BUDGET = 16 * 1024 * 1024;
|
|
30
36
|
// State churn may require reproof, but one search may spend no more than the
|
|
31
37
|
// same eight-release compatibility envelope used for manifest authentication.
|
|
32
38
|
const maximumCompleteReleaseProofs = maximumSearchReleases;
|
|
@@ -140,40 +146,86 @@ async function verifyCompleteRelease(store, authenticated, limits, searchBudget)
|
|
|
140
146
|
}
|
|
141
147
|
return bytes;
|
|
142
148
|
};
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
+
// Each response is bounded by its batch size times maxRecordBytes, and each
|
|
150
|
+
// request by the store's ID byte limit; one fixed JSON parameter keeps D1
|
|
151
|
+
// bindings flat.
|
|
152
|
+
const batchSizeFor = (byteBudget) => Math.max(1, Math.min(maximumInventorySchemaBatch, Math.floor(byteBudget / limits.maxRecordBytes)));
|
|
153
|
+
const operationBatchSize = batchSizeFor(OPERATION_BATCH_BYTE_BUDGET);
|
|
154
|
+
const schemaBatchSize = batchSizeFor(limits.maxSchemaClosureBytes);
|
|
155
|
+
const nextBatch = (ids, start, kind) => {
|
|
156
|
+
const batchSize = kind === "operation" ? operationBatchSize : schemaBatchSize;
|
|
157
|
+
const batch = [];
|
|
158
|
+
let requestBytes = 0;
|
|
159
|
+
for (let index = start; index < ids.length; index += 1) {
|
|
160
|
+
if (batch.length >= batchSize)
|
|
161
|
+
break;
|
|
162
|
+
const idBytes = encoder.encode(ids[index]).byteLength;
|
|
163
|
+
if (idBytes > limits.maxSchemaClosureBytes ||
|
|
164
|
+
(batch.length > 0 &&
|
|
165
|
+
requestBytes + idBytes > limits.maxSchemaClosureBytes))
|
|
166
|
+
break;
|
|
167
|
+
batch.push(ids[index]);
|
|
168
|
+
requestBytes += idBytes;
|
|
149
169
|
}
|
|
150
|
-
|
|
170
|
+
if (batch.length === 0) {
|
|
171
|
+
throw new OpenApiMcpError("RECORD_NOT_ADMITTED", `Release inventory ${kind} request exceeds its byte limit`);
|
|
172
|
+
}
|
|
173
|
+
return batch;
|
|
174
|
+
};
|
|
175
|
+
const operationRoots = [];
|
|
176
|
+
const admitOperation = (id, operation) => {
|
|
151
177
|
if (!operation.id.startsWith("operation:") || operation.id !== id) {
|
|
152
178
|
throw new OpenApiMcpError("RECORD_DIGEST_MISMATCH", "Release inventory operation identity does not match");
|
|
153
179
|
}
|
|
154
180
|
accountRecord(operation);
|
|
155
181
|
operationRoots.push([...new Set(operation.schemaIds)].sort());
|
|
182
|
+
};
|
|
183
|
+
const missingOperation = () => new OpenApiMcpError("RECORD_NOT_ADMITTED", "Release inventory operation is missing");
|
|
184
|
+
if (store.getOperations === undefined) {
|
|
185
|
+
for (const id of operationIds) {
|
|
186
|
+
chargeCompleteVerification(searchBudget, { work: 1, storeCalls: 1 });
|
|
187
|
+
const row = await store.getOperation(authenticated.manifest.catalogId, authenticated.manifest.releaseId, id);
|
|
188
|
+
if (row === null)
|
|
189
|
+
throw missingOperation();
|
|
190
|
+
admitOperation(id, await verifyStoredRecord(authenticated, row, limits));
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
let operationOffset = 0;
|
|
195
|
+
while (operationOffset < operationIds.length) {
|
|
196
|
+
const ids = nextBatch(operationIds, operationOffset, "operation");
|
|
197
|
+
operationOffset += ids.length;
|
|
198
|
+
chargeCompleteVerification(searchBudget, {
|
|
199
|
+
work: ids.length,
|
|
200
|
+
storeCalls: 1,
|
|
201
|
+
});
|
|
202
|
+
const result = await store.getOperations(authenticated.manifest.catalogId, authenticated.manifest.releaseId, ids);
|
|
203
|
+
const rows = snapshotRows(result, ids.length);
|
|
204
|
+
// Duplicate or unrequested rows are integrity failures, like an identity
|
|
205
|
+
// mismatch on the per-row path; absent rows are missing. Verify one row
|
|
206
|
+
// at a time: batching saves round-trips, not hashing.
|
|
207
|
+
const requested = new Set(ids);
|
|
208
|
+
const returned = new Map();
|
|
209
|
+
for (const row of rows) {
|
|
210
|
+
const record = await verifyStoredRecord(authenticated, row, limits);
|
|
211
|
+
if (!requested.has(record.id) || returned.has(record.id)) {
|
|
212
|
+
throw new OpenApiMcpError("RECORD_DIGEST_MISMATCH", "Release inventory operation rows are ambiguous");
|
|
213
|
+
}
|
|
214
|
+
returned.set(record.id, record);
|
|
215
|
+
}
|
|
216
|
+
for (const id of ids) {
|
|
217
|
+
const operation = returned.get(id);
|
|
218
|
+
if (operation === undefined)
|
|
219
|
+
throw missingOperation();
|
|
220
|
+
admitOperation(id, operation);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
156
223
|
}
|
|
157
|
-
const schemaBatchSize = Math.max(1, Math.min(maximumInventorySchemaBatch, Math.floor(limits.maxSchemaClosureBytes / limits.maxRecordBytes)));
|
|
158
224
|
const schemas = new Map();
|
|
159
225
|
let offset = 0;
|
|
160
226
|
while (offset < schemaIds.length) {
|
|
161
|
-
const ids =
|
|
162
|
-
|
|
163
|
-
while (offset < schemaIds.length && ids.length < schemaBatchSize) {
|
|
164
|
-
const id = schemaIds[offset];
|
|
165
|
-
const idBytes = encoder.encode(id).byteLength;
|
|
166
|
-
if (idBytes > limits.maxSchemaClosureBytes ||
|
|
167
|
-
(ids.length > 0 &&
|
|
168
|
-
requestBytes + idBytes > limits.maxSchemaClosureBytes))
|
|
169
|
-
break;
|
|
170
|
-
ids.push(id);
|
|
171
|
-
requestBytes += idBytes;
|
|
172
|
-
offset += 1;
|
|
173
|
-
}
|
|
174
|
-
if (ids.length === 0) {
|
|
175
|
-
throw new OpenApiMcpError("RECORD_NOT_ADMITTED", "Release inventory schema request exceeds its byte limit");
|
|
176
|
-
}
|
|
227
|
+
const ids = nextBatch(schemaIds, offset, "schema");
|
|
228
|
+
offset += ids.length;
|
|
177
229
|
chargeCompleteVerification(searchBudget, {
|
|
178
230
|
work: ids.length,
|
|
179
231
|
storeCalls: 1,
|
|
@@ -344,6 +344,13 @@ function validateSchema(schema, value, closure, depth, context) {
|
|
|
344
344
|
if (schema === false || !isObject(schema))
|
|
345
345
|
schemaFailure();
|
|
346
346
|
for (const key of Object.keys(schema)) {
|
|
347
|
+
// OpenAPI Specification Extensions (`^x-`, OpenAPI 3.1 §4.9) are legal on a Schema Object and
|
|
348
|
+
// carry no validation semantics, so ignore the key and never walk its value -- Cloudflare's
|
|
349
|
+
// API document puts `x-auditable: true` on parameter schemas, and the compiler copies
|
|
350
|
+
// component schemas verbatim. Every other unknown keyword still fails: one with validation
|
|
351
|
+
// semantics (`pattern`, `contains`) must not be silently skipped.
|
|
352
|
+
if (key.startsWith("x-"))
|
|
353
|
+
continue;
|
|
347
354
|
if (!supportedSchemaKeys.has(key))
|
|
348
355
|
schemaFailure();
|
|
349
356
|
}
|
package/dist/runtime/store.d.ts
CHANGED
|
@@ -28,6 +28,10 @@ export declare const CATALOG_STORE_PUBLIC_MESSAGES: Readonly<{
|
|
|
28
28
|
recordJsonInvalid: "Stored record JSON is invalid";
|
|
29
29
|
operationRowAmbiguous: "Stored operation row is ambiguous";
|
|
30
30
|
operationIdentifierMismatch: "Stored operation identifier does not match request";
|
|
31
|
+
operationRequestLimitExceeded: "Operation request exceeds its limit";
|
|
32
|
+
operationRequestInvalid: "Operation request is invalid";
|
|
33
|
+
operationTransportTooManyRows: "Stored operation transport returned too many rows";
|
|
34
|
+
operationRowsInvalid: "Stored operation rows are invalid";
|
|
31
35
|
schemaRequestLimitExceeded: "Schema request exceeds its limit";
|
|
32
36
|
schemaRequestInvalid: "Schema request is invalid";
|
|
33
37
|
schemaTransportTooManyRows: "Stored schema transport returned too many rows";
|
|
@@ -49,4 +53,4 @@ export interface D1CatalogDatabase {
|
|
|
49
53
|
prepare(sql: string): D1CatalogPreparedStatement;
|
|
50
54
|
}
|
|
51
55
|
/** Construct the Worker-safe D1 implementation using only fixed SQL statements. */
|
|
52
|
-
export declare function createD1CatalogStore(binding: D1CatalogDatabase, limits?: Partial<RuntimeLimits>): CatalogStore
|
|
56
|
+
export declare function createD1CatalogStore(binding: D1CatalogDatabase, limits?: Partial<RuntimeLimits>): Required<CatalogStore>;
|
package/dist/runtime/store.js
CHANGED
|
@@ -64,6 +64,24 @@ WHERE s.catalog_id = ? AND s.release_id = ?
|
|
|
64
64
|
AND r.format IN (4, 5) AND r.contract = 1
|
|
65
65
|
ORDER BY s.record_id COLLATE BINARY
|
|
66
66
|
LIMIT ?;`;
|
|
67
|
+
const GET_OPERATIONS_SQL = `WITH requested(record_id) AS (
|
|
68
|
+
SELECT DISTINCT value FROM json_each(?) WHERE typeof(value) = 'text'
|
|
69
|
+
)
|
|
70
|
+
SELECT
|
|
71
|
+
CASE WHEN typeof(o.record_id) = 'text' AND length(CAST(o.record_id AS BLOB)) <= ? THEN o.record_id ELSE NULL END AS record_id,
|
|
72
|
+
length(CAST(o.record_id AS BLOB)) AS record_id_bytes,
|
|
73
|
+
CASE WHEN typeof(o.logical_digest) = 'text' AND length(CAST(o.logical_digest AS BLOB)) <= ? THEN o.logical_digest ELSE NULL END AS logical_digest,
|
|
74
|
+
length(CAST(o.logical_digest AS BLOB)) AS logical_digest_bytes,
|
|
75
|
+
CASE WHEN typeof(o.record_json) = 'text' AND length(CAST(o.record_json AS BLOB)) <= ? THEN o.record_json ELSE NULL END AS record_json,
|
|
76
|
+
length(CAST(o.record_json AS BLOB)) AS record_json_bytes
|
|
77
|
+
FROM requested
|
|
78
|
+
JOIN operations AS o ON o.record_id = requested.record_id
|
|
79
|
+
JOIN release_metadata AS r
|
|
80
|
+
ON r.catalog_id = o.catalog_id AND r.release_id = o.release_id
|
|
81
|
+
WHERE o.catalog_id = ? AND o.release_id = ?
|
|
82
|
+
AND r.format IN (4, 5) AND r.contract = 1
|
|
83
|
+
ORDER BY o.record_id COLLATE BINARY
|
|
84
|
+
LIMIT ?;`;
|
|
67
85
|
const digestPattern = /^[0-9a-f]{64}$/;
|
|
68
86
|
const identifierPattern = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
69
87
|
const signaturePattern = /^[A-Za-z0-9_-]+$/;
|
|
@@ -104,6 +122,10 @@ export const CATALOG_STORE_PUBLIC_MESSAGES = Object.freeze({
|
|
|
104
122
|
recordJsonInvalid: "Stored record JSON is invalid",
|
|
105
123
|
operationRowAmbiguous: "Stored operation row is ambiguous",
|
|
106
124
|
operationIdentifierMismatch: "Stored operation identifier does not match request",
|
|
125
|
+
operationRequestLimitExceeded: "Operation request exceeds its limit",
|
|
126
|
+
operationRequestInvalid: "Operation request is invalid",
|
|
127
|
+
operationTransportTooManyRows: "Stored operation transport returned too many rows",
|
|
128
|
+
operationRowsInvalid: "Stored operation rows are invalid",
|
|
107
129
|
schemaRequestLimitExceeded: "Schema request exceeds its limit",
|
|
108
130
|
schemaRequestInvalid: "Schema request is invalid",
|
|
109
131
|
schemaTransportTooManyRows: "Stored schema transport returned too many rows",
|
|
@@ -400,6 +422,12 @@ function snapshotSearchQuery(value, limits) {
|
|
|
400
422
|
}
|
|
401
423
|
}
|
|
402
424
|
function snapshotSchemaIds(value, limits) {
|
|
425
|
+
return snapshotRecordIds(value, limits, requestedSchemaId, CATALOG_STORE_PUBLIC_MESSAGES.schemaRequestLimitExceeded, CATALOG_STORE_PUBLIC_MESSAGES.schemaRequestInvalid);
|
|
426
|
+
}
|
|
427
|
+
function snapshotOperationIds(value, limits) {
|
|
428
|
+
return snapshotRecordIds(value, limits, requestedOperationId, CATALOG_STORE_PUBLIC_MESSAGES.operationRequestLimitExceeded, CATALOG_STORE_PUBLIC_MESSAGES.operationRequestInvalid);
|
|
429
|
+
}
|
|
430
|
+
function snapshotRecordIds(value, limits, parse, limitMessage, invalidMessage) {
|
|
403
431
|
try {
|
|
404
432
|
if (!Array.isArray(value) ||
|
|
405
433
|
Object.getPrototypeOf(value) !== Array.prototype)
|
|
@@ -413,7 +441,7 @@ function snapshotSchemaIds(value, limits) {
|
|
|
413
441
|
length.value < 0)
|
|
414
442
|
throw new Error();
|
|
415
443
|
if (length.value > limits.maxManifestRecords)
|
|
416
|
-
throw input(
|
|
444
|
+
throw input(limitMessage);
|
|
417
445
|
const names = Object.getOwnPropertyNames(value);
|
|
418
446
|
if (names.length !== length.value + 1 || !names.includes("length"))
|
|
419
447
|
throw new Error();
|
|
@@ -423,10 +451,10 @@ function snapshotSchemaIds(value, limits) {
|
|
|
423
451
|
const id = Object.getOwnPropertyDescriptor(value, String(index));
|
|
424
452
|
if (!id?.enumerable || !("value" in id))
|
|
425
453
|
throw new Error();
|
|
426
|
-
const parsed =
|
|
454
|
+
const parsed = parse(id.value);
|
|
427
455
|
totalBytes += textEncoder.encode(parsed).byteLength;
|
|
428
456
|
if (totalBytes > limits.maxSchemaClosureBytes)
|
|
429
|
-
throw input(
|
|
457
|
+
throw input(limitMessage);
|
|
430
458
|
ids.push(parsed);
|
|
431
459
|
}
|
|
432
460
|
return ids;
|
|
@@ -434,7 +462,7 @@ function snapshotSchemaIds(value, limits) {
|
|
|
434
462
|
catch (error) {
|
|
435
463
|
if (error instanceof OpenApiMcpError)
|
|
436
464
|
throw error;
|
|
437
|
-
throw input(
|
|
465
|
+
throw input(invalidMessage);
|
|
438
466
|
}
|
|
439
467
|
}
|
|
440
468
|
async function d1All(database, sql, values, errorCode) {
|
|
@@ -540,6 +568,37 @@ class D1CatalogStore {
|
|
|
540
568
|
throw failure("RECORD_DIGEST_MISMATCH", CATALOG_STORE_PUBLIC_MESSAGES.operationIdentifierMismatch);
|
|
541
569
|
return row;
|
|
542
570
|
}
|
|
571
|
+
async getOperations(catalog, release, ids) {
|
|
572
|
+
const catalogIdValue = catalogId(catalog);
|
|
573
|
+
const releaseIdValue = releaseId(release);
|
|
574
|
+
const operationIds = snapshotOperationIds(ids, this.limits);
|
|
575
|
+
if (operationIds.length === 0)
|
|
576
|
+
return [];
|
|
577
|
+
const requested = new Set(operationIds);
|
|
578
|
+
const rows = await d1All(this.database, GET_OPERATIONS_SQL, [
|
|
579
|
+
canonicalJson([...requested].sort()),
|
|
580
|
+
operationIdBytes,
|
|
581
|
+
digestBytes,
|
|
582
|
+
this.limits.maxRecordBytes,
|
|
583
|
+
catalogIdValue,
|
|
584
|
+
releaseIdValue,
|
|
585
|
+
requested.size + 1,
|
|
586
|
+
], "RECORD_DIGEST_MISMATCH");
|
|
587
|
+
if (rows.length > requested.size)
|
|
588
|
+
throw failure("RECORD_DIGEST_MISMATCH", CATALOG_STORE_PUBLIC_MESSAGES.operationTransportTooManyRows);
|
|
589
|
+
// Unlike getSchemas, absent IDs are omitted rather than rejected, matching
|
|
590
|
+
// getOperation's null; callers decide whether absence is fatal.
|
|
591
|
+
const records = [];
|
|
592
|
+
let previous = "";
|
|
593
|
+
for (const value of rows) {
|
|
594
|
+
const row = decodeRecord(value, "operation", this.limits);
|
|
595
|
+
if (!requested.has(row.id) || row.id <= previous)
|
|
596
|
+
throw failure("RECORD_DIGEST_MISMATCH", CATALOG_STORE_PUBLIC_MESSAGES.operationRowsInvalid);
|
|
597
|
+
previous = row.id;
|
|
598
|
+
records.push(row);
|
|
599
|
+
}
|
|
600
|
+
return records;
|
|
601
|
+
}
|
|
543
602
|
async getSchemas(catalog, release, ids) {
|
|
544
603
|
const catalogIdValue = catalogId(catalog);
|
|
545
604
|
const releaseIdValue = releaseId(release);
|
package/dist/runtime/types.d.ts
CHANGED
|
@@ -167,6 +167,13 @@ export interface CatalogStore {
|
|
|
167
167
|
getManifest(catalogId: CatalogId, releaseId: ReleaseId): Promise<ManifestEnvelope>;
|
|
168
168
|
searchCandidates(query: SearchQuery): Promise<readonly CandidateRef[]>;
|
|
169
169
|
getOperation(catalogId: CatalogId, releaseId: ReleaseId, id: TypedOperationId): Promise<StoredRecord<OperationRecordV4> | null>;
|
|
170
|
+
/**
|
|
171
|
+
* Optional batched operation read used by complete-release verification.
|
|
172
|
+
* Returns the stored rows for the requested IDs; a missing ID is omitted
|
|
173
|
+
* (as `getOperation` returns null). Stores without it are read row by row.
|
|
174
|
+
* It has no response-size bound of its own; callers bound the batch.
|
|
175
|
+
*/
|
|
176
|
+
getOperations?(catalogId: CatalogId, releaseId: ReleaseId, ids: readonly TypedOperationId[]): Promise<readonly StoredRecord<OperationRecordV4>[]>;
|
|
170
177
|
getSchemas(catalogId: CatalogId, releaseId: ReleaseId, ids: readonly TypedSchemaId[]): Promise<readonly StoredRecord<SchemaRecordV4>[]>;
|
|
171
178
|
}
|
|
172
179
|
/** Immutable high-water data plus the release currently admitted for use. */
|
|
@@ -16,5 +16,6 @@ export declare class SqliteCatalogStore implements CatalogStore {
|
|
|
16
16
|
getManifest(catalog: CatalogId, release: ReleaseId): Promise<ManifestEnvelope>;
|
|
17
17
|
searchCandidates(query: SearchQuery): Promise<readonly import("../runtime/types.ts").CandidateRef[]>;
|
|
18
18
|
getOperation(catalog: CatalogId, release: ReleaseId, id: TypedOperationId): Promise<StoredRecord<OperationRecordV4> | null>;
|
|
19
|
+
getOperations(catalog: CatalogId, release: ReleaseId, ids: readonly TypedOperationId[]): Promise<readonly StoredRecord<OperationRecordV4>[]>;
|
|
19
20
|
getSchemas(catalog: CatalogId, release: ReleaseId, ids: readonly TypedSchemaId[]): Promise<readonly StoredRecord<SchemaRecordV4>[]>;
|
|
20
21
|
}
|
|
@@ -364,6 +364,9 @@ export class SqliteCatalogStore {
|
|
|
364
364
|
async getOperation(catalog, release, id) {
|
|
365
365
|
return await this.#v4Store().getOperation(catalog, release, id);
|
|
366
366
|
}
|
|
367
|
+
async getOperations(catalog, release, ids) {
|
|
368
|
+
return await this.#v4Store().getOperations(catalog, release, ids);
|
|
369
|
+
}
|
|
367
370
|
async getSchemas(catalog, release, ids) {
|
|
368
371
|
return await this.#v4Store().getSchemas(catalog, release, ids);
|
|
369
372
|
}
|
package/dist/stdio/search.js
CHANGED
|
@@ -30,6 +30,10 @@ export function createStdioSearchRuntime(catalogs, options) {
|
|
|
30
30
|
throw new OpenApiMcpError("UPSTREAM_ERROR");
|
|
31
31
|
},
|
|
32
32
|
};
|
|
33
|
+
// Batch only when every source store can; otherwise keep per-row reads.
|
|
34
|
+
// Every source was just checked, so `?? []` only fails closed as missing.
|
|
35
|
+
if (ordered.every((entry) => entry.store.getOperations !== undefined))
|
|
36
|
+
store.getOperations = async (catalog, release, ids) => (await storeFor(catalog, release).getOperations?.(catalog, release, ids)) ?? [];
|
|
33
37
|
return createRuntimeWithCandidateLookup({ ...options, limits, store }, async (query, tryChargeSource) => {
|
|
34
38
|
const batches = [];
|
|
35
39
|
let remaining = query.limit;
|
package/package.json
CHANGED