@cueai/omni-reader-mcp 1.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.
- package/README.md +49 -0
- package/dist/artifact-store.d.ts +65 -0
- package/dist/artifact-store.js +720 -0
- package/dist/cli/agent-config.d.ts +28 -0
- package/dist/cli/agent-config.js +335 -0
- package/dist/cli/clean.d.ts +9 -0
- package/dist/cli/clean.js +93 -0
- package/dist/cli/doctor.d.ts +17 -0
- package/dist/cli/doctor.js +157 -0
- package/dist/cli/setup.d.ts +8 -0
- package/dist/cli/setup.js +59 -0
- package/dist/constants.d.ts +7 -0
- package/dist/constants.js +7 -0
- package/dist/cube-client.d.ts +48 -0
- package/dist/cube-client.js +161 -0
- package/dist/cursor.d.ts +14 -0
- package/dist/cursor.js +101 -0
- package/dist/errors.d.ts +25 -0
- package/dist/errors.js +26 -0
- package/dist/iiis-client.d.ts +43 -0
- package/dist/iiis-client.js +694 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +174 -0
- package/dist/multipart-body.d.ts +12 -0
- package/dist/multipart-body.js +90 -0
- package/dist/operation-journal.d.ts +28 -0
- package/dist/operation-journal.js +351 -0
- package/dist/path-security.d.ts +22 -0
- package/dist/path-security.js +240 -0
- package/dist/progress.d.ts +4 -0
- package/dist/progress.js +3 -0
- package/dist/protocol.d.ts +39 -0
- package/dist/protocol.js +27 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.js +10 -0
- package/dist/tools.d.ts +24 -0
- package/dist/tools.js +224 -0
- package/package.json +35 -0
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { GRANTED_STREAM_PROTOCOL_VERSION, RESULT_CHUNK_MAX_BYTES, } from "./constants.js";
|
|
3
|
+
import { OmniBridgeError } from "./errors.js";
|
|
4
|
+
import { createMultipartBody } from "./multipart-body.js";
|
|
5
|
+
import { NOOP_PROGRESS } from "./progress.js";
|
|
6
|
+
class TransportFailure extends Error {
|
|
7
|
+
}
|
|
8
|
+
const ACTIVE_STATUSES = new Set(["CLAIMED", "UPLOADING", "PROCESSING", "SETTLING"]);
|
|
9
|
+
const TERMINAL_STATUSES = new Set([
|
|
10
|
+
"EXPIRED",
|
|
11
|
+
"SETTLEMENT_DENIED",
|
|
12
|
+
"FAILED",
|
|
13
|
+
"CANCELED",
|
|
14
|
+
"DELIVERY_EXPIRED",
|
|
15
|
+
"DELIVERED",
|
|
16
|
+
]);
|
|
17
|
+
const RECOVERABLE_SETTLEMENT_ERRORS = new Set([
|
|
18
|
+
"SETTLEMENT_IN_PROGRESS",
|
|
19
|
+
"SETTLEMENT_RETRYABLE",
|
|
20
|
+
"SETTLEMENT_JOURNAL_UNAVAILABLE",
|
|
21
|
+
"OPERATION_STORE_UNAVAILABLE",
|
|
22
|
+
"RESULT_STORAGE_UNAVAILABLE",
|
|
23
|
+
"CUBE_CONTROL_UNAVAILABLE",
|
|
24
|
+
]);
|
|
25
|
+
const BASE64URL_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
26
|
+
const SHA256_PATTERN = /^sha256:[0-9a-f]{64}$/;
|
|
27
|
+
function bridgeError(code, message, facts = {}) {
|
|
28
|
+
return new OmniBridgeError({
|
|
29
|
+
code,
|
|
30
|
+
message,
|
|
31
|
+
fileUploaded: facts.fileUploaded ?? false,
|
|
32
|
+
billed: facts.billed ?? false,
|
|
33
|
+
contentReleased: facts.contentReleased ?? false,
|
|
34
|
+
retryable: facts.retryable ?? false,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
function recoveryFacts(value) {
|
|
38
|
+
return {
|
|
39
|
+
fileUploaded: value?.fileUploaded ?? false,
|
|
40
|
+
billed: value?.billed ?? false,
|
|
41
|
+
contentReleased: value?.contentReleased ?? false,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function mergeRecoveryFacts(left, right) {
|
|
45
|
+
return {
|
|
46
|
+
fileUploaded: left.fileUploaded || right.fileUploaded === true,
|
|
47
|
+
billed: left.billed || right.billed === true,
|
|
48
|
+
contentReleased: left.contentReleased || right.contentReleased === true,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function errorWithRecoveryFacts(error, facts) {
|
|
52
|
+
return bridgeError(error.code, error.message, {
|
|
53
|
+
fileUploaded: error.fileUploaded || facts.fileUploaded,
|
|
54
|
+
billed: error.billed || facts.billed,
|
|
55
|
+
contentReleased: error.contentReleased || facts.contentReleased,
|
|
56
|
+
retryable: error.retryable,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
function resultStorageUnavailable(facts) {
|
|
60
|
+
return bridgeError("RESULT_STORAGE_UNAVAILABLE", "Released result delivery did not complete.", { ...facts, retryable: true });
|
|
61
|
+
}
|
|
62
|
+
function canceledAfterUpload() {
|
|
63
|
+
return bridgeError("CANCELED_AFTER_UPLOAD_STARTED", "The parse operation was canceled after upload started.", { fileUploaded: true });
|
|
64
|
+
}
|
|
65
|
+
function throwIfCanceled(input) {
|
|
66
|
+
if (input.signal?.aborted)
|
|
67
|
+
throw canceledAfterUpload();
|
|
68
|
+
}
|
|
69
|
+
function operationUrl(input, suffix = "") {
|
|
70
|
+
const base = new URL(input.uploadUrl);
|
|
71
|
+
if (base.protocol !== "https:" || base.username !== "" || base.password !== "") {
|
|
72
|
+
throw bridgeError("INSECURE_IIIS_URL", "The IIIS upload endpoint must use credential-free HTTPS.");
|
|
73
|
+
}
|
|
74
|
+
return new URL(`operations/${encodeURIComponent(input.operationId)}${suffix}`, base).toString();
|
|
75
|
+
}
|
|
76
|
+
function operationHeaders(input) {
|
|
77
|
+
return { authorization: `Bearer ${input.operationToken}` };
|
|
78
|
+
}
|
|
79
|
+
function isRecord(value) {
|
|
80
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
81
|
+
}
|
|
82
|
+
function safeInteger(value, minimum = 0) {
|
|
83
|
+
return typeof value === "number"
|
|
84
|
+
&& Number.isSafeInteger(value)
|
|
85
|
+
&& value >= minimum;
|
|
86
|
+
}
|
|
87
|
+
function validateProtocol(event) {
|
|
88
|
+
if (event.protocol_version !== GRANTED_STREAM_PROTOCOL_VERSION) {
|
|
89
|
+
throw bridgeError("PROTOCOL_MISMATCH", "IIIS returned an unsupported protocol version.");
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function progressValue(event) {
|
|
93
|
+
validateProtocol(event);
|
|
94
|
+
if (!safeInteger(event.done) || !safeInteger(event.total)) {
|
|
95
|
+
throw bridgeError("INVALID_PROGRESS_EVENT", "IIIS returned an invalid progress event.");
|
|
96
|
+
}
|
|
97
|
+
return {
|
|
98
|
+
done: event.done,
|
|
99
|
+
total: event.total,
|
|
100
|
+
message: String(event.message ?? event.stage ?? "Parsing"),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function monotonicProgress(sink) {
|
|
104
|
+
let last = 0;
|
|
105
|
+
return {
|
|
106
|
+
async report(progress, total, message) {
|
|
107
|
+
const next = Math.min(total, Math.max(last, progress));
|
|
108
|
+
last = next;
|
|
109
|
+
await sink.report(next, total, message);
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
async function errorFromResponse(response, input) {
|
|
114
|
+
try {
|
|
115
|
+
const body = await response.json();
|
|
116
|
+
if (isRecord(body) && typeof body.code === "string" && typeof body.message === "string") {
|
|
117
|
+
return bridgeError(body.code, body.message, {
|
|
118
|
+
fileUploaded: body.file_uploaded === true,
|
|
119
|
+
billed: body.billed === true,
|
|
120
|
+
contentReleased: body.content_released === true,
|
|
121
|
+
retryable: body.retryable === true,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
throwIfCanceled(input);
|
|
127
|
+
// Never expose untrusted response text.
|
|
128
|
+
}
|
|
129
|
+
return bridgeError("IIIS_UNAVAILABLE", "IIIS could not complete the parse request.", {
|
|
130
|
+
retryable: response.status >= 500,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
async function protocolJson(response, input, description) {
|
|
134
|
+
try {
|
|
135
|
+
return await response.json();
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
throwIfCanceled(input);
|
|
139
|
+
throw bridgeError("PROTOCOL_MISMATCH", `IIIS returned invalid ${description} JSON.`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
async function* sseEvents(response) {
|
|
143
|
+
if (response.body === null)
|
|
144
|
+
throw new TransportFailure("missing SSE body");
|
|
145
|
+
const reader = response.body.getReader();
|
|
146
|
+
const decoder = new TextDecoder();
|
|
147
|
+
let buffered = "";
|
|
148
|
+
try {
|
|
149
|
+
while (true) {
|
|
150
|
+
const { done, value } = await reader.read();
|
|
151
|
+
buffered += decoder.decode(value, { stream: !done }).replace(/\r\n/g, "\n");
|
|
152
|
+
let boundary;
|
|
153
|
+
while ((boundary = buffered.indexOf("\n\n")) >= 0) {
|
|
154
|
+
const block = buffered.slice(0, boundary);
|
|
155
|
+
buffered = buffered.slice(boundary + 2);
|
|
156
|
+
if (block.startsWith(":"))
|
|
157
|
+
continue;
|
|
158
|
+
const data = block.split("\n")
|
|
159
|
+
.filter((line) => line.startsWith("data:"))
|
|
160
|
+
.map((line) => line.slice(5).trimStart())
|
|
161
|
+
.join("\n");
|
|
162
|
+
if (data.length === 0)
|
|
163
|
+
continue;
|
|
164
|
+
let parsed;
|
|
165
|
+
try {
|
|
166
|
+
parsed = JSON.parse(data);
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
throw bridgeError("INVALID_SSE_EVENT", "IIIS returned an invalid progress event.");
|
|
170
|
+
}
|
|
171
|
+
if (!isRecord(parsed)) {
|
|
172
|
+
throw bridgeError("INVALID_SSE_EVENT", "IIIS returned an invalid progress event.");
|
|
173
|
+
}
|
|
174
|
+
yield parsed;
|
|
175
|
+
}
|
|
176
|
+
if (done) {
|
|
177
|
+
if (buffered.trim().length !== 0)
|
|
178
|
+
throw new TransportFailure("truncated SSE event");
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
if (error instanceof OmniBridgeError || error instanceof TransportFailure)
|
|
185
|
+
throw error;
|
|
186
|
+
throw new TransportFailure("SSE disconnected");
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
reader.releaseLock();
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
export class IiisClient {
|
|
193
|
+
#fetch;
|
|
194
|
+
#pollIntervalMs;
|
|
195
|
+
#maxPolls;
|
|
196
|
+
constructor(options = {}) {
|
|
197
|
+
this.#fetch = options.fetchImpl ?? fetch;
|
|
198
|
+
this.#pollIntervalMs = Math.max(0, options.pollIntervalMs ?? 250);
|
|
199
|
+
this.#maxPolls = Math.max(1, options.maxPolls ?? 240);
|
|
200
|
+
}
|
|
201
|
+
async uploadAndWait(input) {
|
|
202
|
+
const progress = monotonicProgress(input.progress ?? NOOP_PROGRESS);
|
|
203
|
+
try {
|
|
204
|
+
return await this.#uploadOnce(input, progress);
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
if (error instanceof OmniBridgeError) {
|
|
208
|
+
if (error.fileUploaded
|
|
209
|
+
&& error.retryable
|
|
210
|
+
&& RECOVERABLE_SETTLEMENT_ERRORS.has(error.code)) {
|
|
211
|
+
throwIfCanceled(input);
|
|
212
|
+
return this.#recover(input, false, progress, recoveryFacts(error));
|
|
213
|
+
}
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
216
|
+
throwIfCanceled(input);
|
|
217
|
+
return this.#recover(input, true, progress);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
async downloadResult(input) {
|
|
221
|
+
throwIfCanceled(input);
|
|
222
|
+
const confirmedFacts = recoveryFacts({
|
|
223
|
+
fileUploaded: true,
|
|
224
|
+
billed: true,
|
|
225
|
+
contentReleased: true,
|
|
226
|
+
});
|
|
227
|
+
try {
|
|
228
|
+
await input.retention.reset();
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
try {
|
|
232
|
+
await input.retention.abort();
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
// Preserve the primary reset failure and its confirmed release facts.
|
|
236
|
+
}
|
|
237
|
+
if (input.signal?.aborted) {
|
|
238
|
+
throw errorWithRecoveryFacts(canceledAfterUpload(), confirmedFacts);
|
|
239
|
+
}
|
|
240
|
+
if (error instanceof OmniBridgeError) {
|
|
241
|
+
throw errorWithRecoveryFacts(error, confirmedFacts);
|
|
242
|
+
}
|
|
243
|
+
throw resultStorageUnavailable(confirmedFacts);
|
|
244
|
+
}
|
|
245
|
+
let response;
|
|
246
|
+
try {
|
|
247
|
+
response = await this.#fetch(operationUrl(input, "/result"), {
|
|
248
|
+
headers: operationHeaders(input),
|
|
249
|
+
signal: input.signal,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
if (input.signal?.aborted) {
|
|
254
|
+
throw errorWithRecoveryFacts(canceledAfterUpload(), confirmedFacts);
|
|
255
|
+
}
|
|
256
|
+
throw bridgeError("IIIS_UNAVAILABLE", "IIIS result recovery failed.", {
|
|
257
|
+
...confirmedFacts,
|
|
258
|
+
retryable: true,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
if (!response.ok) {
|
|
262
|
+
throw errorWithRecoveryFacts(await errorFromResponse(response, input), confirmedFacts);
|
|
263
|
+
}
|
|
264
|
+
const declared = Number(response.headers.get("content-length"));
|
|
265
|
+
if (!safeInteger(declared)) {
|
|
266
|
+
throw bridgeError("RESULT_INTEGRITY_FAILED", "The recovered result length is invalid.", {
|
|
267
|
+
...confirmedFacts,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
const start = {
|
|
271
|
+
operationId: input.operationId,
|
|
272
|
+
resultBytes: declared,
|
|
273
|
+
mediaType: response.headers.get("content-type") ?? "application/octet-stream",
|
|
274
|
+
source: "recovery",
|
|
275
|
+
};
|
|
276
|
+
const hash = createHash("sha256");
|
|
277
|
+
let received = 0;
|
|
278
|
+
let completed = false;
|
|
279
|
+
try {
|
|
280
|
+
await input.retention.begin(start);
|
|
281
|
+
if (response.body === null) {
|
|
282
|
+
if (declared !== 0)
|
|
283
|
+
throw new TransportFailure("missing result body");
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
const reader = response.body.getReader();
|
|
287
|
+
try {
|
|
288
|
+
while (true) {
|
|
289
|
+
const { done, value } = await reader.read();
|
|
290
|
+
if (done)
|
|
291
|
+
break;
|
|
292
|
+
throwIfCanceled(input);
|
|
293
|
+
received += value.byteLength;
|
|
294
|
+
if (received > declared) {
|
|
295
|
+
throw bridgeError("RESULT_INTEGRITY_FAILED", "The recovered result exceeded its declared length.", {
|
|
296
|
+
...confirmedFacts,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
hash.update(value);
|
|
300
|
+
await input.retention.write(value);
|
|
301
|
+
throwIfCanceled(input);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
finally {
|
|
305
|
+
reader.releaseLock();
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (received !== declared) {
|
|
309
|
+
throw bridgeError("RESULT_INTEGRITY_FAILED", "The recovered result length is invalid.", {
|
|
310
|
+
...confirmedFacts,
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
const metadata = {
|
|
314
|
+
...start,
|
|
315
|
+
resultDigest: `sha256:${hash.digest("hex")}`,
|
|
316
|
+
};
|
|
317
|
+
await input.retention.complete(metadata);
|
|
318
|
+
throwIfCanceled(input);
|
|
319
|
+
completed = true;
|
|
320
|
+
return metadata;
|
|
321
|
+
}
|
|
322
|
+
catch (error) {
|
|
323
|
+
if (!completed) {
|
|
324
|
+
try {
|
|
325
|
+
await input.retention.abort();
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
// Preserve the primary recovery failure and its confirmed release facts.
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if (input.signal?.aborted) {
|
|
332
|
+
throw errorWithRecoveryFacts(canceledAfterUpload(), confirmedFacts);
|
|
333
|
+
}
|
|
334
|
+
if (error instanceof OmniBridgeError) {
|
|
335
|
+
throw errorWithRecoveryFacts(error, confirmedFacts);
|
|
336
|
+
}
|
|
337
|
+
throw resultStorageUnavailable(confirmedFacts);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
async ack(input) {
|
|
341
|
+
throwIfCanceled(input);
|
|
342
|
+
let response;
|
|
343
|
+
try {
|
|
344
|
+
response = await this.#fetch(operationUrl(input, "/ack"), {
|
|
345
|
+
method: "POST",
|
|
346
|
+
headers: operationHeaders(input),
|
|
347
|
+
signal: input.signal,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
catch {
|
|
351
|
+
throwIfCanceled(input);
|
|
352
|
+
throw bridgeError("IIIS_UNAVAILABLE", "IIIS could not acknowledge the retained result.", {
|
|
353
|
+
fileUploaded: true,
|
|
354
|
+
billed: true,
|
|
355
|
+
contentReleased: true,
|
|
356
|
+
retryable: true,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
if (response.status !== 204)
|
|
360
|
+
throw await errorFromResponse(response, input);
|
|
361
|
+
}
|
|
362
|
+
async #uploadOnce(input, progress) {
|
|
363
|
+
const body = createMultipartBody(input.openedFile, {
|
|
364
|
+
signal: input.signal,
|
|
365
|
+
onProgress: async (bytes) => {
|
|
366
|
+
const scaled = input.openedFile.size === 0
|
|
367
|
+
? 40
|
|
368
|
+
: Math.floor(bytes * 40 / input.openedFile.size);
|
|
369
|
+
await progress.report(scaled, 100, `Uploading ${bytes}/${input.openedFile.size} bytes`);
|
|
370
|
+
},
|
|
371
|
+
});
|
|
372
|
+
let response;
|
|
373
|
+
try {
|
|
374
|
+
response = await this.#fetch(input.uploadUrl, {
|
|
375
|
+
method: "POST",
|
|
376
|
+
headers: {
|
|
377
|
+
authorization: `Bearer ${input.parseGrant}`,
|
|
378
|
+
"content-type": body.contentType,
|
|
379
|
+
"content-length": String(body.contentLength),
|
|
380
|
+
},
|
|
381
|
+
body: body.stream,
|
|
382
|
+
signal: input.signal,
|
|
383
|
+
duplex: "half",
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
catch (error) {
|
|
387
|
+
if (error instanceof OmniBridgeError)
|
|
388
|
+
throw error;
|
|
389
|
+
if (input.signal?.aborted) {
|
|
390
|
+
throw bridgeError("CANCELED_BEFORE_UPLOAD_COMPLETE", "The upload was canceled.");
|
|
391
|
+
}
|
|
392
|
+
throw new TransportFailure("upload transport failed");
|
|
393
|
+
}
|
|
394
|
+
if (!response.ok)
|
|
395
|
+
throw await errorFromResponse(response, input);
|
|
396
|
+
return this.#consumeReleasedSse(input, response, progress);
|
|
397
|
+
}
|
|
398
|
+
async #consumeReleasedSse(input, response, progress) {
|
|
399
|
+
let released;
|
|
400
|
+
let completedMetadata;
|
|
401
|
+
let offset = 0;
|
|
402
|
+
let complete = false;
|
|
403
|
+
let streamFacts = recoveryFacts();
|
|
404
|
+
const hash = createHash("sha256");
|
|
405
|
+
try {
|
|
406
|
+
for await (const event of sseEvents(response)) {
|
|
407
|
+
throwIfCanceled(input);
|
|
408
|
+
if (complete) {
|
|
409
|
+
const code = event.type === "result_chunk" ? "INVALID_RESULT_CHUNK" : "INVALID_RESULT_EVENT_ORDER";
|
|
410
|
+
throw bridgeError(code, "IIIS sent an event after result completion.", {
|
|
411
|
+
fileUploaded: true,
|
|
412
|
+
billed: true,
|
|
413
|
+
contentReleased: true,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
if (event.type === "progress") {
|
|
417
|
+
const parsed = progressValue(event);
|
|
418
|
+
const scaled = parsed.total > 0
|
|
419
|
+
? 40 + Math.floor(Math.min(parsed.done / parsed.total, 1) * 60)
|
|
420
|
+
: 40;
|
|
421
|
+
await progress.report(scaled, 100, parsed.message);
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
if (event.type === "error") {
|
|
425
|
+
throw bridgeError(String(event.code ?? "IIIS_PARSE_FAILED"), String(event.message ?? "IIIS parsing failed."), {
|
|
426
|
+
fileUploaded: event.file_uploaded === true,
|
|
427
|
+
billed: event.billed === true,
|
|
428
|
+
contentReleased: event.content_released === true,
|
|
429
|
+
retryable: event.retryable === true,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
if (event.type === "released") {
|
|
433
|
+
validateProtocol(event);
|
|
434
|
+
if (released !== undefined
|
|
435
|
+
|| event.operation_id !== input.operationId
|
|
436
|
+
|| event.billed !== true
|
|
437
|
+
|| event.content_released !== true
|
|
438
|
+
|| !safeInteger(event.result_bytes)
|
|
439
|
+
|| typeof event.media_type !== "string"
|
|
440
|
+
|| event.media_type.length === 0
|
|
441
|
+
|| typeof event.result_digest !== "string"
|
|
442
|
+
|| !SHA256_PATTERN.test(event.result_digest)) {
|
|
443
|
+
throw bridgeError("INVALID_RELEASE_EVENT", "IIIS returned an invalid release event.");
|
|
444
|
+
}
|
|
445
|
+
released = {
|
|
446
|
+
resultBytes: event.result_bytes,
|
|
447
|
+
mediaType: event.media_type,
|
|
448
|
+
resultDigest: event.result_digest,
|
|
449
|
+
};
|
|
450
|
+
streamFacts = recoveryFacts({
|
|
451
|
+
fileUploaded: true,
|
|
452
|
+
billed: true,
|
|
453
|
+
contentReleased: true,
|
|
454
|
+
});
|
|
455
|
+
await input.retention.begin({
|
|
456
|
+
operationId: input.operationId,
|
|
457
|
+
resultBytes: released.resultBytes,
|
|
458
|
+
mediaType: released.mediaType,
|
|
459
|
+
source: "sse",
|
|
460
|
+
});
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
if (event.type === "result_chunk") {
|
|
464
|
+
validateProtocol(event);
|
|
465
|
+
if (released === undefined) {
|
|
466
|
+
throw bridgeError("RESULT_BEFORE_RELEASE", "IIIS sent result bytes before billing release.");
|
|
467
|
+
}
|
|
468
|
+
const encoded = event.data;
|
|
469
|
+
if (event.operation_id !== input.operationId
|
|
470
|
+
|| !safeInteger(event.offset)
|
|
471
|
+
|| !safeInteger(event.decoded_bytes, 1)
|
|
472
|
+
|| event.decoded_bytes > RESULT_CHUNK_MAX_BYTES
|
|
473
|
+
|| typeof encoded !== "string"
|
|
474
|
+
|| !BASE64URL_PATTERN.test(encoded)) {
|
|
475
|
+
if (safeInteger(event.decoded_bytes, 1) && event.decoded_bytes > RESULT_CHUNK_MAX_BYTES) {
|
|
476
|
+
throw bridgeError("RESULT_CHUNK_TOO_LARGE", "IIIS returned an oversized result chunk.");
|
|
477
|
+
}
|
|
478
|
+
throw bridgeError("INVALID_RESULT_CHUNK", "IIIS returned an invalid result chunk.");
|
|
479
|
+
}
|
|
480
|
+
const decoded = Buffer.from(encoded, "base64url");
|
|
481
|
+
if (decoded.toString("base64url") !== encoded
|
|
482
|
+
|| event.offset !== offset
|
|
483
|
+
|| event.decoded_bytes !== decoded.length
|
|
484
|
+
|| offset + decoded.length > released.resultBytes) {
|
|
485
|
+
throw bridgeError("INVALID_RESULT_CHUNK", "IIIS returned an invalid result chunk.");
|
|
486
|
+
}
|
|
487
|
+
hash.update(decoded);
|
|
488
|
+
await input.retention.write(decoded);
|
|
489
|
+
offset += decoded.length;
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
if (event.type === "result_complete") {
|
|
493
|
+
validateProtocol(event);
|
|
494
|
+
if (released === undefined
|
|
495
|
+
|| event.operation_id !== input.operationId
|
|
496
|
+
|| !safeInteger(event.result_bytes)
|
|
497
|
+
|| event.result_bytes !== released.resultBytes
|
|
498
|
+
|| typeof event.result_digest !== "string"
|
|
499
|
+
|| event.result_digest !== released.resultDigest
|
|
500
|
+
|| offset !== released.resultBytes) {
|
|
501
|
+
throw bridgeError("RESULT_INTEGRITY_FAILED", "IIIS result metadata did not match.");
|
|
502
|
+
}
|
|
503
|
+
const computedDigest = `sha256:${hash.digest("hex")}`;
|
|
504
|
+
if (computedDigest !== released.resultDigest) {
|
|
505
|
+
throw bridgeError("RESULT_INTEGRITY_FAILED", "IIIS result verification failed.");
|
|
506
|
+
}
|
|
507
|
+
const metadata = {
|
|
508
|
+
operationId: input.operationId,
|
|
509
|
+
resultBytes: released.resultBytes,
|
|
510
|
+
mediaType: released.mediaType,
|
|
511
|
+
resultDigest: released.resultDigest,
|
|
512
|
+
source: "sse",
|
|
513
|
+
};
|
|
514
|
+
await input.retention.complete(metadata);
|
|
515
|
+
throwIfCanceled(input);
|
|
516
|
+
completedMetadata = metadata;
|
|
517
|
+
complete = true;
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
throw bridgeError("INVALID_SSE_EVENT", "IIIS returned an unsupported progress event.");
|
|
521
|
+
}
|
|
522
|
+
if (completedMetadata !== undefined)
|
|
523
|
+
return completedMetadata;
|
|
524
|
+
throw new TransportFailure("SSE ended before completion");
|
|
525
|
+
}
|
|
526
|
+
catch (error) {
|
|
527
|
+
const facts = error instanceof OmniBridgeError
|
|
528
|
+
? mergeRecoveryFacts(streamFacts, recoveryFacts(error))
|
|
529
|
+
: streamFacts;
|
|
530
|
+
try {
|
|
531
|
+
await input.retention.abort();
|
|
532
|
+
}
|
|
533
|
+
catch {
|
|
534
|
+
// Preserve the primary delivery failure and its confirmed release facts.
|
|
535
|
+
}
|
|
536
|
+
if (input.signal?.aborted) {
|
|
537
|
+
throw errorWithRecoveryFacts(canceledAfterUpload(), facts);
|
|
538
|
+
}
|
|
539
|
+
if (error instanceof OmniBridgeError) {
|
|
540
|
+
throw errorWithRecoveryFacts(error, facts);
|
|
541
|
+
}
|
|
542
|
+
if (facts.contentReleased) {
|
|
543
|
+
throw resultStorageUnavailable(facts);
|
|
544
|
+
}
|
|
545
|
+
throw error;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
async #status(input) {
|
|
549
|
+
throwIfCanceled(input);
|
|
550
|
+
let response;
|
|
551
|
+
try {
|
|
552
|
+
response = await this.#fetch(operationUrl(input), {
|
|
553
|
+
headers: operationHeaders(input),
|
|
554
|
+
signal: input.signal,
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
catch {
|
|
558
|
+
throwIfCanceled(input);
|
|
559
|
+
throw bridgeError("IIIS_UNAVAILABLE", "IIIS operation status is unavailable.", { retryable: true });
|
|
560
|
+
}
|
|
561
|
+
if (!response.ok)
|
|
562
|
+
throw await errorFromResponse(response, input);
|
|
563
|
+
const value = await protocolJson(response, input, "operation status");
|
|
564
|
+
if (!isRecord(value)
|
|
565
|
+
|| value.protocol_version !== GRANTED_STREAM_PROTOCOL_VERSION
|
|
566
|
+
|| value.operation_id !== input.operationId
|
|
567
|
+
|| typeof value.status !== "string"
|
|
568
|
+
|| typeof value.parser_started !== "boolean"
|
|
569
|
+
|| typeof value.file_uploaded !== "boolean"
|
|
570
|
+
|| typeof value.billed !== "boolean"
|
|
571
|
+
|| typeof value.content_released !== "boolean"
|
|
572
|
+
|| typeof value.retryable !== "boolean"
|
|
573
|
+
|| !(value.expires_at === null || typeof value.expires_at === "string")) {
|
|
574
|
+
throw bridgeError("PROTOCOL_MISMATCH", "IIIS returned an invalid operation status.");
|
|
575
|
+
}
|
|
576
|
+
return value;
|
|
577
|
+
}
|
|
578
|
+
async #events(input, afterSequence) {
|
|
579
|
+
throwIfCanceled(input);
|
|
580
|
+
let response;
|
|
581
|
+
try {
|
|
582
|
+
response = await this.#fetch(operationUrl(input, `/events?after_sequence=${afterSequence}`), { headers: operationHeaders(input), signal: input.signal });
|
|
583
|
+
}
|
|
584
|
+
catch {
|
|
585
|
+
throwIfCanceled(input);
|
|
586
|
+
throw bridgeError("IIIS_UNAVAILABLE", "IIIS operation events are unavailable.", { retryable: true });
|
|
587
|
+
}
|
|
588
|
+
if (!response.ok)
|
|
589
|
+
throw await errorFromResponse(response, input);
|
|
590
|
+
const value = await protocolJson(response, input, "operation events");
|
|
591
|
+
if (!isRecord(value)
|
|
592
|
+
|| value.protocol_version !== GRANTED_STREAM_PROTOCOL_VERSION
|
|
593
|
+
|| value.operation_id !== input.operationId
|
|
594
|
+
|| typeof value.status !== "string"
|
|
595
|
+
|| value.after_sequence !== afterSequence
|
|
596
|
+
|| !safeInteger(value.next_sequence)
|
|
597
|
+
|| value.next_sequence < afterSequence
|
|
598
|
+
|| !Array.isArray(value.events)) {
|
|
599
|
+
throw bridgeError("PROTOCOL_MISMATCH", "IIIS returned an invalid operation event page.");
|
|
600
|
+
}
|
|
601
|
+
let expected = afterSequence + 1;
|
|
602
|
+
const events = [];
|
|
603
|
+
for (const raw of value.events) {
|
|
604
|
+
if (!isRecord(raw) || raw.sequence !== expected || raw.type !== "progress") {
|
|
605
|
+
throw bridgeError("EVENT_CURSOR_EXPIRED", "IIIS returned a non-contiguous operation event page.");
|
|
606
|
+
}
|
|
607
|
+
progressValue(raw);
|
|
608
|
+
events.push(raw);
|
|
609
|
+
expected += 1;
|
|
610
|
+
}
|
|
611
|
+
const expectedNext = events.length === 0 ? afterSequence : expected - 1;
|
|
612
|
+
if (value.next_sequence !== expectedNext) {
|
|
613
|
+
throw bridgeError("EVENT_CURSOR_EXPIRED", "IIIS returned an invalid operation event cursor.");
|
|
614
|
+
}
|
|
615
|
+
return { status: value.status, nextSequence: value.next_sequence, events };
|
|
616
|
+
}
|
|
617
|
+
async #recover(input, mayRetryUpload, progress, initialFacts = recoveryFacts()) {
|
|
618
|
+
let eventCursor = 0;
|
|
619
|
+
let parserStartedReported = false;
|
|
620
|
+
let facts = initialFacts;
|
|
621
|
+
for (let poll = 0; poll < this.#maxPolls; poll += 1) {
|
|
622
|
+
try {
|
|
623
|
+
throwIfCanceled(input);
|
|
624
|
+
const status = await this.#status(input);
|
|
625
|
+
facts = mergeRecoveryFacts(facts, {
|
|
626
|
+
fileUploaded: status.file_uploaded,
|
|
627
|
+
billed: status.billed,
|
|
628
|
+
contentReleased: status.content_released,
|
|
629
|
+
});
|
|
630
|
+
if (status.parser_started && !parserStartedReported) {
|
|
631
|
+
await progress.report(45, 100, "Parser started");
|
|
632
|
+
parserStartedReported = true;
|
|
633
|
+
}
|
|
634
|
+
if (status.status === "ISSUED" && mayRetryUpload && Date.parse(input.expiresAt) > Date.now()) {
|
|
635
|
+
try {
|
|
636
|
+
return await this.#uploadOnce(input, progress);
|
|
637
|
+
}
|
|
638
|
+
catch (error) {
|
|
639
|
+
if (error instanceof OmniBridgeError)
|
|
640
|
+
throw error;
|
|
641
|
+
throwIfCanceled(input);
|
|
642
|
+
mayRetryUpload = false;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
else if (status.status === "RELEASED") {
|
|
646
|
+
return await this.downloadResult(input);
|
|
647
|
+
}
|
|
648
|
+
else if (ACTIVE_STATUSES.has(status.status)) {
|
|
649
|
+
const page = await this.#events(input, eventCursor);
|
|
650
|
+
for (const event of page.events) {
|
|
651
|
+
const parsed = progressValue(event);
|
|
652
|
+
const scaled = parsed.total > 0
|
|
653
|
+
? 40 + Math.floor(Math.min(parsed.done / parsed.total, 1) * 60)
|
|
654
|
+
: 40;
|
|
655
|
+
await progress.report(scaled, 100, parsed.message);
|
|
656
|
+
}
|
|
657
|
+
eventCursor = page.nextSequence;
|
|
658
|
+
}
|
|
659
|
+
else if (TERMINAL_STATUSES.has(status.status)) {
|
|
660
|
+
throw bridgeError(status.status, `The IIIS operation ended with status ${status.status}.`, {
|
|
661
|
+
fileUploaded: status.file_uploaded,
|
|
662
|
+
billed: status.billed,
|
|
663
|
+
contentReleased: status.content_released,
|
|
664
|
+
retryable: status.retryable,
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
else if (status.status !== "ISSUED") {
|
|
668
|
+
throw bridgeError("PROTOCOL_MISMATCH", "IIIS returned an unsupported operation status.");
|
|
669
|
+
}
|
|
670
|
+
if (this.#pollIntervalMs > 0) {
|
|
671
|
+
await new Promise((resolve) => setTimeout(resolve, this.#pollIntervalMs));
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
catch (error) {
|
|
675
|
+
if (error instanceof OmniBridgeError)
|
|
676
|
+
throw errorWithRecoveryFacts(error, facts);
|
|
677
|
+
if (input.signal?.aborted) {
|
|
678
|
+
throw errorWithRecoveryFacts(canceledAfterUpload(), facts);
|
|
679
|
+
}
|
|
680
|
+
if (facts.contentReleased) {
|
|
681
|
+
throw resultStorageUnavailable(facts);
|
|
682
|
+
}
|
|
683
|
+
throw error;
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
if (input.signal?.aborted) {
|
|
687
|
+
throw errorWithRecoveryFacts(canceledAfterUpload(), facts);
|
|
688
|
+
}
|
|
689
|
+
throw bridgeError("OPERATION_RECOVERY_TIMEOUT", "Timed out while recovering the IIIS operation.", {
|
|
690
|
+
...facts,
|
|
691
|
+
retryable: true,
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
}
|