@cueai/omni-reader-mcp 1.0.2 → 1.1.1

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 (48) hide show
  1. package/README.md +115 -26
  2. package/dist/artifact-store.d.ts +11 -0
  3. package/dist/artifact-store.js +94 -48
  4. package/dist/cli/agent-config.d.ts +29 -4
  5. package/dist/cli/agent-config.js +910 -107
  6. package/dist/cli/arguments.d.ts +32 -0
  7. package/dist/cli/arguments.js +120 -0
  8. package/dist/cli/doctor.d.ts +42 -1
  9. package/dist/cli/doctor.js +109 -36
  10. package/dist/cli/setup.d.ts +3 -0
  11. package/dist/cli/setup.js +103 -18
  12. package/dist/cli/uninstall.d.ts +6 -0
  13. package/dist/cli/uninstall.js +37 -0
  14. package/dist/constants.d.ts +7 -0
  15. package/dist/constants.js +7 -0
  16. package/dist/cube-client.d.ts +5 -3
  17. package/dist/cube-client.js +16 -11
  18. package/dist/cursor.js +2 -0
  19. package/dist/errors.d.ts +32 -1
  20. package/dist/errors.js +26 -1
  21. package/dist/iiis-client.d.ts +18 -4
  22. package/dist/iiis-client.js +194 -40
  23. package/dist/index.d.ts +3 -0
  24. package/dist/index.js +93 -32
  25. package/dist/multipart-body.js +2 -0
  26. package/dist/onboarding-policy.d.ts +10 -0
  27. package/dist/onboarding-policy.js +58 -0
  28. package/dist/operation-journal.d.ts +50 -1
  29. package/dist/operation-journal.js +473 -114
  30. package/dist/operation-manager.d.ts +75 -0
  31. package/dist/operation-manager.js +1324 -0
  32. package/dist/path-security.d.ts +1 -0
  33. package/dist/path-security.js +26 -6
  34. package/dist/progress.d.ts +6 -1
  35. package/dist/protocol.d.ts +26 -13
  36. package/dist/protocol.js +34 -10
  37. package/dist/remote-client.d.ts +17 -0
  38. package/dist/remote-client.js +233 -0
  39. package/dist/result-contract.d.ts +199 -0
  40. package/dist/result-contract.js +235 -0
  41. package/dist/server.js +21 -4
  42. package/dist/source.d.ts +8 -0
  43. package/dist/source.js +37 -0
  44. package/dist/task-runtime.d.ts +13 -0
  45. package/dist/task-runtime.js +94 -0
  46. package/dist/tools.d.ts +19 -1
  47. package/dist/tools.js +317 -112
  48. package/package.json +3 -3
@@ -1,13 +1,55 @@
1
1
  import { createCipheriv, createDecipheriv, createHash, randomBytes, randomUUID, } from "node:crypto";
2
- import { chmod, link, mkdir, open, readFile, unlink, } from "node:fs/promises";
2
+ import { chmod, link, mkdir, open, readFile, readdir, rename, rm, stat, unlink, } from "node:fs/promises";
3
3
  import { homedir } from "node:os";
4
4
  import path from "node:path";
5
5
  import { OmniBridgeError } from "./errors.js";
6
+ const JOURNAL_STATES = new Set([
7
+ "CREATED",
8
+ "GRANT_PENDING",
9
+ "GRANT_ISSUED",
10
+ "UPLOADING",
11
+ "PROCESSING",
12
+ "RESULT_READY",
13
+ "ACK_PENDING",
14
+ "CLEANUP_PENDING",
15
+ "COMPLETED",
16
+ "FAILED",
17
+ "CANCELED",
18
+ "EXPIRED",
19
+ ]);
20
+ const TERMINAL_STATES = new Set([
21
+ "COMPLETED",
22
+ "FAILED",
23
+ "CANCELED",
24
+ "EXPIRED",
25
+ ]);
26
+ const PROGRESS_UNITS = new Set([
27
+ "page",
28
+ "sheet",
29
+ "slide",
30
+ "frame",
31
+ "segment",
32
+ ]);
33
+ const CLEANUP_STATES = new Set([
34
+ "not_created",
35
+ "in_use",
36
+ "pending",
37
+ "deleted",
38
+ ]);
39
+ const DELIVERY_STATES = new Set([
40
+ "not_created",
41
+ "pending",
42
+ "deleted_after_ack",
43
+ ]);
44
+ const RECORD_FILE_PATTERN = /^[0-9a-f]{64}(?:\.issued)?\.json$/u;
45
+ const LOCK_STALE_MS = 30_000;
6
46
  function bridgeError(code, message, retryable) {
7
47
  return new OmniBridgeError({
8
48
  code,
9
49
  message,
50
+ operationCreated: false,
10
51
  fileUploaded: false,
52
+ parserStarted: false,
11
53
  billed: false,
12
54
  contentReleased: false,
13
55
  retryable,
@@ -28,40 +70,166 @@ function recordFileName(clientRequestId) {
28
70
  }
29
71
  function validateClientRequestId(clientRequestId) {
30
72
  if (!/^[A-Za-z0-9._:-]{1,128}$/u.test(clientRequestId)) {
31
- throw bridgeError("INVALID_CLIENT_REQUEST_ID", "The local grant request identifier is invalid.", false);
73
+ throw bridgeError("INVALID_CLIENT_REQUEST_ID", "The local operation request identifier is invalid.", false);
32
74
  }
33
75
  }
76
+ function isStringOrNull(value) {
77
+ return value === null || typeof value === "string";
78
+ }
34
79
  function isEncryptedToken(value) {
35
- if (value === null || typeof value !== "object") {
80
+ if (value === null || typeof value !== "object" || Array.isArray(value))
36
81
  return false;
37
- }
38
82
  const token = value;
39
83
  return (token.algorithm === "aes-256-gcm" &&
40
84
  typeof token.nonce === "string" &&
41
85
  typeof token.ciphertext === "string" &&
42
86
  typeof token.tag === "string");
43
87
  }
88
+ function isProgress(value) {
89
+ if (value === null || typeof value !== "object" || Array.isArray(value))
90
+ return false;
91
+ const progress = value;
92
+ return (typeof progress.unit === "string" &&
93
+ PROGRESS_UNITS.has(progress.unit) &&
94
+ Number.isSafeInteger(progress.completed) &&
95
+ Number.isSafeInteger(progress.total) &&
96
+ (progress.completed ?? -1) >= 0 &&
97
+ (progress.total ?? 0) > 0 &&
98
+ (progress.completed ?? 1) <= (progress.total ?? 0));
99
+ }
100
+ function parseLegacyRecord(value) {
101
+ if (value.version !== 1 ||
102
+ typeof value.clientRequestId !== "string" ||
103
+ typeof value.requestHash !== "string" ||
104
+ !isStringOrNull(value.operationId) ||
105
+ !(value.operationToken === null || isEncryptedToken(value.operationToken)) ||
106
+ !isStringOrNull(value.uploadUrl) ||
107
+ (value.state !== "GRANT_PENDING" && value.state !== "GRANT_ISSUED") ||
108
+ typeof value.createdAt !== "string" ||
109
+ !isStringOrNull(value.expiresAt)) {
110
+ throw new Error("legacy record fields are invalid");
111
+ }
112
+ return value;
113
+ }
114
+ function parseVersionTwoRecord(value) {
115
+ if (value.version !== 2 ||
116
+ typeof value.clientRequestId !== "string" ||
117
+ typeof value.requestHash !== "string" ||
118
+ !(value.sourceLocatorHash === undefined ||
119
+ isStringOrNull(value.sourceLocatorHash)) ||
120
+ !(value.sourceKind === undefined ||
121
+ value.sourceKind === "local" ||
122
+ value.sourceKind === "url") ||
123
+ !isStringOrNull(value.operationId) ||
124
+ !(value.operationToken === null || isEncryptedToken(value.operationToken)) ||
125
+ typeof value.state !== "string" ||
126
+ !JOURNAL_STATES.has(value.state) ||
127
+ typeof value.createdAt !== "string" ||
128
+ typeof value.updatedAt !== "string" ||
129
+ !isStringOrNull(value.expiresAt) ||
130
+ !isStringOrNull(value.stage) ||
131
+ !(value.progressPercent === undefined ||
132
+ (typeof value.progressPercent === "number" &&
133
+ Number.isFinite(value.progressPercent) &&
134
+ value.progressPercent >= 0 &&
135
+ value.progressPercent <= 100)) ||
136
+ !(value.progress === null || isProgress(value.progress)) ||
137
+ typeof value.fileUploaded !== "boolean" ||
138
+ typeof value.parserStarted !== "boolean" ||
139
+ typeof value.billed !== "boolean" ||
140
+ typeof value.contentReleased !== "boolean" ||
141
+ typeof value.processingCopy !== "string" ||
142
+ !CLEANUP_STATES.has(value.processingCopy) ||
143
+ typeof value.temporaryData !== "string" ||
144
+ !CLEANUP_STATES.has(value.temporaryData) ||
145
+ typeof value.deliveryResult !== "string" ||
146
+ !DELIVERY_STATES.has(value.deliveryResult) ||
147
+ !isStringOrNull(value.resultId) ||
148
+ !isStringOrNull(value.resultExpiresAt) ||
149
+ !isStringOrNull(value.errorCode)) {
150
+ throw new Error("version-2 record fields are invalid");
151
+ }
152
+ return {
153
+ ...value,
154
+ sourceLocatorHash: value.sourceLocatorHash ?? null,
155
+ sourceKind: value.sourceKind ?? "local",
156
+ progressPercent: value.progressPercent ?? 0,
157
+ };
158
+ }
44
159
  function parsePersistedRecord(value) {
45
- if (value === null || typeof value !== "object") {
160
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
46
161
  throw new Error("record is not an object");
47
162
  }
48
163
  const record = value;
49
- if (record.version !== 1 ||
50
- typeof record.clientRequestId !== "string" ||
51
- typeof record.requestHash !== "string" ||
52
- (record.operationId !== null && typeof record.operationId !== "string") ||
53
- (record.operationToken !== null && !isEncryptedToken(record.operationToken)) ||
54
- (record.uploadUrl !== null && typeof record.uploadUrl !== "string") ||
55
- (record.state !== "GRANT_PENDING" && record.state !== "GRANT_ISSUED") ||
56
- typeof record.createdAt !== "string" ||
57
- (record.expiresAt !== null && typeof record.expiresAt !== "string")) {
58
- throw new Error("record fields are invalid");
59
- }
60
- return record;
164
+ return record.version === 1 ? parseLegacyRecord(record) : parseVersionTwoRecord(record);
165
+ }
166
+ function migrateRecord(record) {
167
+ if (record.version === 2)
168
+ return record;
169
+ return {
170
+ version: 2,
171
+ clientRequestId: record.clientRequestId,
172
+ requestHash: record.requestHash,
173
+ sourceLocatorHash: null,
174
+ sourceKind: "local",
175
+ operationId: record.operationId,
176
+ operationToken: record.operationToken,
177
+ state: record.state,
178
+ createdAt: record.createdAt,
179
+ updatedAt: record.createdAt,
180
+ expiresAt: record.expiresAt,
181
+ stage: null,
182
+ progressPercent: 0,
183
+ progress: null,
184
+ fileUploaded: false,
185
+ parserStarted: false,
186
+ billed: false,
187
+ contentReleased: false,
188
+ processingCopy: "not_created",
189
+ temporaryData: "not_created",
190
+ deliveryResult: "not_created",
191
+ resultId: null,
192
+ resultExpiresAt: null,
193
+ errorCode: null,
194
+ };
61
195
  }
62
196
  async function closeQuietly(handle) {
63
197
  await handle?.close().catch(() => undefined);
64
198
  }
199
+ function sleep(milliseconds) {
200
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
201
+ }
202
+ function assertStableTransition(current, updated) {
203
+ for (const [before, after] of [
204
+ [current.operationId, updated.operationId],
205
+ [current.resultId, updated.resultId],
206
+ ]) {
207
+ if (before !== null && before !== after) {
208
+ throw bridgeError("JOURNAL_IDENTITY_CONFLICT", "A stable local operation identifier cannot be replaced.", false);
209
+ }
210
+ }
211
+ if (current.operationToken !== null &&
212
+ current.operationToken !== updated.operationToken &&
213
+ !(updated.operationToken === null && TERMINAL_STATES.has(updated.state))) {
214
+ throw bridgeError("JOURNAL_IDENTITY_CONFLICT", "A recoverable local operation token cannot be replaced.", false);
215
+ }
216
+ if (updated.progressPercent < current.progressPercent ||
217
+ (current.progress !== null && (updated.progress === null ||
218
+ updated.progress.unit !== current.progress.unit ||
219
+ updated.progress.completed < current.progress.completed))) {
220
+ throw bridgeError("JOURNAL_PROGRESS_REGRESSION", "Confirmed local operation progress cannot move backward.", false);
221
+ }
222
+ for (const [before, after] of [
223
+ [current.fileUploaded, updated.fileUploaded],
224
+ [current.parserStarted, updated.parserStarted],
225
+ [current.billed, updated.billed],
226
+ [current.contentReleased, updated.contentReleased],
227
+ ]) {
228
+ if (before && !after) {
229
+ throw bridgeError("JOURNAL_FACT_REGRESSION", "A confirmed local operation fact cannot be reverted.", false);
230
+ }
231
+ }
232
+ }
65
233
  export class OperationJournal {
66
234
  #rootDirectory;
67
235
  #now;
@@ -70,105 +238,262 @@ export class OperationJournal {
70
238
  this.#rootDirectory = options.rootDirectory ?? defaultRootDirectory();
71
239
  this.#now = options.now ?? (() => new Date());
72
240
  }
73
- async begin(clientRequestId, requestHash) {
241
+ async beginIntent(clientRequestId, requestHash, sourceKind = "local", sourceLocatorHash = null) {
74
242
  validateClientRequestId(clientRequestId);
75
243
  const existing = await this.#readRecord(clientRequestId);
76
244
  if (existing !== null) {
77
245
  return this.#requireMatchingRequest(clientRequestId, requestHash, existing);
78
246
  }
79
- const pending = {
80
- version: 1,
247
+ const createdAt = this.#now().toISOString();
248
+ const created = {
249
+ version: 2,
81
250
  clientRequestId,
82
251
  requestHash,
252
+ sourceLocatorHash,
253
+ sourceKind,
83
254
  operationId: null,
84
255
  operationToken: null,
85
- uploadUrl: null,
86
- state: "GRANT_PENDING",
87
- createdAt: this.#now().toISOString(),
256
+ state: "CREATED",
257
+ createdAt,
258
+ updatedAt: createdAt,
88
259
  expiresAt: null,
260
+ stage: null,
261
+ progressPercent: 0,
262
+ progress: null,
263
+ fileUploaded: false,
264
+ parserStarted: false,
265
+ billed: false,
266
+ contentReleased: false,
267
+ processingCopy: "not_created",
268
+ temporaryData: "not_created",
269
+ deliveryResult: "not_created",
270
+ resultId: null,
271
+ resultExpiresAt: null,
272
+ errorCode: null,
89
273
  };
90
- if (await this.#createRecord(pending, this.#pendingRecordPath(clientRequestId))) {
91
- return this.#publicRecord(pending, null);
274
+ if (await this.#createRecord(created, this.#recordPath(clientRequestId))) {
275
+ return this.#recordToPublic(clientRequestId, created);
92
276
  }
93
277
  const raced = await this.#readRecord(clientRequestId);
94
278
  if (raced === null) {
95
- throw bridgeError("JOURNAL_WRITE_FAILED", "The local grant journal could not be saved.", true);
279
+ throw bridgeError("JOURNAL_WRITE_FAILED", "The local operation journal could not be saved.", true);
96
280
  }
97
281
  return this.#requireMatchingRequest(clientRequestId, requestHash, raced);
98
282
  }
99
- async markGrantIssued(clientRequestId, fields) {
283
+ async transition(clientRequestId, expectedState, nextState, patch) {
284
+ validateClientRequestId(clientRequestId);
285
+ if (!JOURNAL_STATES.has(expectedState) || !JOURNAL_STATES.has(nextState)) {
286
+ throw bridgeError("INVALID_JOURNAL_STATE", "The local operation state is invalid.", false);
287
+ }
288
+ return this.#withRecordLock(clientRequestId, async () => {
289
+ const stored = await this.#readRecord(clientRequestId);
290
+ if (stored === null) {
291
+ throw bridgeError("JOURNAL_RECORD_NOT_FOUND", "The local operation journal record is missing.", true);
292
+ }
293
+ const current = await this.#recordToPublic(clientRequestId, stored);
294
+ if (current.state !== expectedState) {
295
+ throw bridgeError("JOURNAL_STATE_CONFLICT", "The local operation changed before this transition was saved.", true);
296
+ }
297
+ const updated = {
298
+ ...current,
299
+ ...patch,
300
+ ...(TERMINAL_STATES.has(nextState) ? { operationToken: null } : {}),
301
+ uploadUrl: null,
302
+ state: nextState,
303
+ updatedAt: this.#now().toISOString(),
304
+ };
305
+ assertStableTransition(current, updated);
306
+ await this.#replaceRecord(await this.#persistedRecord(updated), this.#recordPath(clientRequestId));
307
+ await unlink(this.#legacyIssuedRecordPath(clientRequestId)).catch(() => undefined);
308
+ return updated;
309
+ });
310
+ }
311
+ async loadByRequestId(clientRequestId) {
312
+ validateClientRequestId(clientRequestId);
100
313
  const persisted = await this.#readRecord(clientRequestId);
101
- if (persisted === null) {
102
- throw bridgeError("JOURNAL_RECORD_NOT_FOUND", "The local grant journal record is missing.", true);
314
+ return persisted === null ? null : this.#recordToPublic(clientRequestId, persisted);
315
+ }
316
+ async loadByOperationId(operationId) {
317
+ if (operationId.length === 0)
318
+ return null;
319
+ return (await this.#listRecords()).find((record) => record.operationId === operationId) ?? null;
320
+ }
321
+ async loadLatestByRequestHash(requestHash) {
322
+ return (await this.#listRecords())
323
+ .filter((record) => record.requestHash === requestHash)
324
+ .at(-1) ?? null;
325
+ }
326
+ async loadLatestBySourceLocatorHash(sourceLocatorHash) {
327
+ return (await this.#listRecords())
328
+ .filter((record) => record.sourceLocatorHash === sourceLocatorHash)
329
+ .at(-1) ?? null;
330
+ }
331
+ async listRecoverable() {
332
+ return (await this.#listRecords()).filter((record) => !TERMINAL_STATES.has(record.state));
333
+ }
334
+ async begin(clientRequestId, requestHash) {
335
+ const intent = await this.beginIntent(clientRequestId, requestHash);
336
+ if (intent.state !== "CREATED")
337
+ return intent;
338
+ try {
339
+ return await this.transition(clientRequestId, "CREATED", "GRANT_PENDING", {});
103
340
  }
104
- if (persisted.state === "GRANT_ISSUED") {
105
- return this.#requireMatchingGrant(clientRequestId, persisted, fields);
341
+ catch (error) {
342
+ if (!(error instanceof OmniBridgeError) || error.code !== "JOURNAL_STATE_CONFLICT")
343
+ throw error;
344
+ const raced = await this.loadByRequestId(clientRequestId);
345
+ if (raced === null || raced.requestHash !== requestHash)
346
+ throw error;
347
+ return raced;
106
348
  }
107
- const operationToken = await this.#encryptToken(clientRequestId, fields.operationToken);
108
- const issued = {
109
- ...persisted,
110
- operationId: fields.operationId,
111
- operationToken,
112
- uploadUrl: fields.uploadUrl,
113
- state: "GRANT_ISSUED",
114
- expiresAt: fields.expiresAt,
115
- };
116
- if (await this.#createRecord(issued, this.#issuedRecordPath(clientRequestId))) {
117
- return this.#publicRecord(issued, fields.operationToken);
349
+ }
350
+ async markGrantIssued(clientRequestId, fields) {
351
+ const current = await this.loadByRequestId(clientRequestId);
352
+ if (current === null) {
353
+ throw bridgeError("JOURNAL_RECORD_NOT_FOUND", "The local operation journal record is missing.", true);
118
354
  }
119
- const raced = await this.#readRecord(clientRequestId);
120
- if (raced === null || raced.state !== "GRANT_ISSUED") {
121
- throw bridgeError("JOURNAL_WRITE_FAILED", "The local grant journal could not be saved.", true);
355
+ if (current.state !== "GRANT_PENDING")
356
+ return this.#requireMatchingGrant(current, fields);
357
+ try {
358
+ return await this.transition(clientRequestId, "GRANT_PENDING", "GRANT_ISSUED", {
359
+ operationId: fields.operationId,
360
+ operationToken: fields.operationToken,
361
+ expiresAt: fields.expiresAt,
362
+ });
363
+ }
364
+ catch (error) {
365
+ if (!(error instanceof OmniBridgeError) || error.code !== "JOURNAL_STATE_CONFLICT")
366
+ throw error;
367
+ const raced = await this.loadByRequestId(clientRequestId);
368
+ if (raced === null)
369
+ throw error;
370
+ return this.#requireMatchingGrant(raced, fields);
122
371
  }
123
- return this.#requireMatchingGrant(clientRequestId, raced, fields);
124
372
  }
125
- async load(clientRequestId) {
126
- const persisted = await this.#readRecord(clientRequestId);
127
- if (persisted === null) {
128
- return null;
373
+ load(clientRequestId) {
374
+ return this.loadByRequestId(clientRequestId);
375
+ }
376
+ async #listRecords() {
377
+ let names;
378
+ try {
379
+ names = await readdir(this.#rootDirectory);
129
380
  }
130
- return this.#recordToPublic(clientRequestId, persisted);
381
+ catch (error) {
382
+ if (error.code === "ENOENT")
383
+ return [];
384
+ throw bridgeError("JOURNAL_READ_FAILED", "The local operation journal could not be read.", true);
385
+ }
386
+ const requestIds = new Set();
387
+ for (const name of names.filter((entry) => RECORD_FILE_PATTERN.test(entry))) {
388
+ const stored = await this.#readRecordFile(undefined, path.join(this.#rootDirectory, name));
389
+ if (stored !== null)
390
+ requestIds.add(stored.clientRequestId);
391
+ }
392
+ const records = [];
393
+ for (const requestId of requestIds) {
394
+ const record = await this.loadByRequestId(requestId);
395
+ if (record !== null)
396
+ records.push(record);
397
+ }
398
+ return records.sort((left, right) => left.createdAt.localeCompare(right.createdAt) ||
399
+ left.clientRequestId.localeCompare(right.clientRequestId));
131
400
  }
132
- async #recordToPublic(clientRequestId, persisted) {
401
+ async #persistedRecord(record) {
402
+ return {
403
+ version: 2,
404
+ clientRequestId: record.clientRequestId,
405
+ requestHash: record.requestHash,
406
+ sourceLocatorHash: record.sourceLocatorHash,
407
+ sourceKind: record.sourceKind,
408
+ operationId: record.operationId,
409
+ operationToken: record.operationToken === null
410
+ ? null
411
+ : await this.#encryptToken(record.clientRequestId, record.operationToken),
412
+ state: record.state,
413
+ createdAt: record.createdAt,
414
+ updatedAt: record.updatedAt,
415
+ expiresAt: record.expiresAt,
416
+ stage: record.stage,
417
+ progressPercent: record.progressPercent,
418
+ progress: record.progress,
419
+ fileUploaded: record.fileUploaded,
420
+ parserStarted: record.parserStarted,
421
+ billed: record.billed,
422
+ contentReleased: record.contentReleased,
423
+ processingCopy: record.processingCopy,
424
+ temporaryData: record.temporaryData,
425
+ deliveryResult: record.deliveryResult,
426
+ resultId: record.resultId,
427
+ resultExpiresAt: record.resultExpiresAt,
428
+ errorCode: record.errorCode,
429
+ };
430
+ }
431
+ async #recordToPublic(clientRequestId, stored) {
432
+ const persisted = migrateRecord(stored);
133
433
  const operationToken = persisted.operationToken === null
134
434
  ? null
135
435
  : await this.#decryptToken(clientRequestId, persisted.operationToken);
136
- return this.#publicRecord(persisted, operationToken);
436
+ return {
437
+ clientRequestId: persisted.clientRequestId,
438
+ requestHash: persisted.requestHash,
439
+ sourceLocatorHash: persisted.sourceLocatorHash,
440
+ sourceKind: persisted.sourceKind,
441
+ operationId: persisted.operationId,
442
+ operationToken,
443
+ uploadUrl: stored.version === 1 ? stored.uploadUrl : null,
444
+ state: persisted.state,
445
+ createdAt: persisted.createdAt,
446
+ updatedAt: persisted.updatedAt,
447
+ expiresAt: persisted.expiresAt,
448
+ stage: persisted.stage,
449
+ progressPercent: persisted.progressPercent,
450
+ progress: persisted.progress,
451
+ fileUploaded: persisted.fileUploaded,
452
+ parserStarted: persisted.parserStarted,
453
+ billed: persisted.billed,
454
+ contentReleased: persisted.contentReleased,
455
+ processingCopy: persisted.processingCopy,
456
+ temporaryData: persisted.temporaryData,
457
+ deliveryResult: persisted.deliveryResult,
458
+ resultId: persisted.resultId,
459
+ resultExpiresAt: persisted.resultExpiresAt,
460
+ errorCode: persisted.errorCode,
461
+ };
137
462
  }
138
463
  async #requireMatchingRequest(clientRequestId, requestHash, persisted) {
139
464
  if (persisted.requestHash !== requestHash) {
140
- throw bridgeError("IDEMPOTENCY_COLLISION", "This local grant request identifier is already bound to different metadata.", false);
465
+ throw bridgeError("IDEMPOTENCY_COLLISION", "This local operation request identifier is already bound to different metadata.", false);
141
466
  }
142
467
  return this.#recordToPublic(clientRequestId, persisted);
143
468
  }
144
- async #requireMatchingGrant(clientRequestId, persisted, fields) {
145
- const existing = await this.#recordToPublic(clientRequestId, persisted);
146
- if (existing.operationId !== fields.operationId ||
147
- existing.operationToken !== fields.operationToken ||
148
- existing.uploadUrl !== fields.uploadUrl ||
149
- existing.expiresAt !== fields.expiresAt) {
469
+ #requireMatchingGrant(current, fields) {
470
+ if (current.operationId !== fields.operationId ||
471
+ current.operationToken !== fields.operationToken ||
472
+ current.expiresAt !== fields.expiresAt) {
150
473
  throw bridgeError("JOURNAL_GRANT_CONFLICT", "Cube returned conflicting data for the same grant request.", false);
151
474
  }
152
- return existing;
475
+ return current;
153
476
  }
154
477
  async #ensureRoot() {
155
478
  await mkdir(this.#rootDirectory, { recursive: true, mode: 0o700 });
156
- if (process.platform !== "win32") {
479
+ if (process.platform !== "win32")
157
480
  await chmod(this.#rootDirectory, 0o700);
158
- }
159
481
  }
160
- #pendingRecordPath(clientRequestId) {
482
+ #recordPath(clientRequestId) {
161
483
  return path.join(this.#rootDirectory, recordFileName(clientRequestId));
162
484
  }
163
- #issuedRecordPath(clientRequestId) {
164
- return this.#pendingRecordPath(clientRequestId).replace(/\.json$/u, ".issued.json");
485
+ #legacyIssuedRecordPath(clientRequestId) {
486
+ return this.#recordPath(clientRequestId).replace(/\.json$/u, ".issued.json");
487
+ }
488
+ #lockPath(clientRequestId) {
489
+ return this.#recordPath(clientRequestId).replace(/\.json$/u, ".lock");
165
490
  }
166
491
  async #readRecord(clientRequestId) {
167
- const issued = await this.#readRecordFile(clientRequestId, this.#issuedRecordPath(clientRequestId));
168
- if (issued !== null) {
169
- return issued;
170
- }
171
- return this.#readRecordFile(clientRequestId, this.#pendingRecordPath(clientRequestId));
492
+ const current = await this.#readRecordFile(clientRequestId, this.#recordPath(clientRequestId));
493
+ if (current?.version === 2)
494
+ return current;
495
+ const legacyIssued = await this.#readRecordFile(clientRequestId, this.#legacyIssuedRecordPath(clientRequestId));
496
+ return legacyIssued ?? current;
172
497
  }
173
498
  async #readRecordFile(clientRequestId, recordPath) {
174
499
  let serialized;
@@ -176,20 +501,20 @@ export class OperationJournal {
176
501
  serialized = await readFile(recordPath, "utf8");
177
502
  }
178
503
  catch (error) {
179
- if (error.code === "ENOENT") {
504
+ if (error.code === "ENOENT")
180
505
  return null;
181
- }
182
- throw bridgeError("JOURNAL_READ_FAILED", "The local grant journal could not be read.", true);
506
+ throw bridgeError("JOURNAL_READ_FAILED", "The local operation journal could not be read.", true);
183
507
  }
184
508
  try {
185
509
  const persisted = parsePersistedRecord(JSON.parse(serialized));
186
- if (persisted.clientRequestId !== clientRequestId) {
510
+ if (clientRequestId !== undefined && persisted.clientRequestId !== clientRequestId) {
187
511
  throw new Error("record identity mismatch");
188
512
  }
513
+ validateClientRequestId(persisted.clientRequestId);
189
514
  return persisted;
190
515
  }
191
516
  catch {
192
- throw bridgeError("JOURNAL_CORRUPT", "The local grant journal is invalid.", false);
517
+ throw bridgeError("JOURNAL_CORRUPT", "The local operation journal is invalid.", false);
193
518
  }
194
519
  }
195
520
  async #createRecord(record, recordPath) {
@@ -203,42 +528,96 @@ export class OperationJournal {
203
528
  await handle.close();
204
529
  handle = undefined;
205
530
  await link(temporaryPath, recordPath);
206
- if (process.platform !== "win32") {
531
+ if (process.platform !== "win32")
207
532
  await chmod(recordPath, 0o600);
208
- }
209
533
  await this.#syncDirectory();
210
534
  return true;
211
535
  }
212
536
  catch (error) {
213
537
  await closeQuietly(handle);
214
- if (error.code === "EEXIST") {
538
+ if (error.code === "EEXIST")
215
539
  return false;
216
- }
217
- throw bridgeError("JOURNAL_WRITE_FAILED", "The local grant journal could not be saved.", true);
540
+ throw bridgeError("JOURNAL_WRITE_FAILED", "The local operation journal could not be saved.", true);
541
+ }
542
+ finally {
543
+ await unlink(temporaryPath).catch(() => undefined);
544
+ }
545
+ }
546
+ async #replaceRecord(record, recordPath) {
547
+ await this.#ensureRoot();
548
+ const temporaryPath = path.join(this.#rootDirectory, `.${path.basename(recordPath)}.${randomUUID()}.tmp`);
549
+ let handle;
550
+ try {
551
+ handle = await open(temporaryPath, "wx", 0o600);
552
+ await handle.writeFile(`${JSON.stringify(record)}\n`, "utf8");
553
+ await handle.sync();
554
+ await handle.close();
555
+ handle = undefined;
556
+ await rename(temporaryPath, recordPath);
557
+ if (process.platform !== "win32")
558
+ await chmod(recordPath, 0o600);
559
+ await this.#syncDirectory();
560
+ }
561
+ catch {
562
+ await closeQuietly(handle);
563
+ throw bridgeError("JOURNAL_WRITE_FAILED", "The local operation journal could not be saved.", true);
218
564
  }
219
565
  finally {
220
566
  await unlink(temporaryPath).catch(() => undefined);
221
567
  }
222
568
  }
569
+ async #withRecordLock(clientRequestId, action) {
570
+ await this.#ensureRoot();
571
+ const lockPath = this.#lockPath(clientRequestId);
572
+ for (let attempt = 0; attempt < 500; attempt += 1) {
573
+ try {
574
+ await mkdir(lockPath, { mode: 0o700 });
575
+ try {
576
+ return await action();
577
+ }
578
+ finally {
579
+ await rm(lockPath, { recursive: true, force: true });
580
+ }
581
+ }
582
+ catch (error) {
583
+ if (error.code !== "EEXIST")
584
+ throw error;
585
+ if (await this.#removeStaleLock(lockPath))
586
+ continue;
587
+ await sleep(10);
588
+ }
589
+ }
590
+ throw bridgeError("JOURNAL_LOCK_TIMEOUT", "The local operation journal remained busy.", true);
591
+ }
592
+ async #removeStaleLock(lockPath) {
593
+ try {
594
+ const lock = await stat(lockPath);
595
+ if (Date.now() - lock.mtimeMs <= LOCK_STALE_MS)
596
+ return false;
597
+ await rm(lockPath, { recursive: true, force: true });
598
+ return true;
599
+ }
600
+ catch (error) {
601
+ return error.code === "ENOENT";
602
+ }
603
+ }
223
604
  async #journalKey() {
224
605
  this.#keyPromise ??= this.#loadOrCreateKey();
225
606
  return this.#keyPromise;
226
607
  }
227
608
  async #existingJournalKey() {
228
- if (this.#keyPromise !== undefined) {
609
+ if (this.#keyPromise !== undefined)
229
610
  return this.#keyPromise;
230
- }
231
611
  const keyPath = path.join(this.#rootDirectory, "journal.key");
232
612
  try {
233
613
  const existing = await readFile(keyPath);
234
- if (existing.length !== 32) {
614
+ if (existing.length !== 32)
235
615
  throw new Error("invalid key length");
236
- }
237
616
  this.#keyPromise = Promise.resolve(existing);
238
617
  return existing;
239
618
  }
240
619
  catch {
241
- throw bridgeError("JOURNAL_KEY_INVALID", "The local grant journal key is missing or invalid.", false);
620
+ throw bridgeError("JOURNAL_KEY_INVALID", "The local operation journal key is missing or invalid.", false);
242
621
  }
243
622
  }
244
623
  async #loadOrCreateKey() {
@@ -246,14 +625,13 @@ export class OperationJournal {
246
625
  const keyPath = path.join(this.#rootDirectory, "journal.key");
247
626
  try {
248
627
  const existing = await readFile(keyPath);
249
- if (existing.length !== 32) {
628
+ if (existing.length !== 32)
250
629
  throw new Error("invalid key length");
251
- }
252
630
  return existing;
253
631
  }
254
632
  catch (error) {
255
633
  if (error.code !== "ENOENT") {
256
- throw bridgeError("JOURNAL_KEY_INVALID", "The local grant journal key is invalid.", false);
634
+ throw bridgeError("JOURNAL_KEY_INVALID", "The local operation journal key is invalid.", false);
257
635
  }
258
636
  }
259
637
  const key = randomBytes(32);
@@ -266,9 +644,8 @@ export class OperationJournal {
266
644
  await handle.close();
267
645
  handle = undefined;
268
646
  await link(temporaryPath, keyPath);
269
- if (process.platform !== "win32") {
647
+ if (process.platform !== "win32")
270
648
  await chmod(keyPath, 0o600);
271
- }
272
649
  await this.#syncDirectory();
273
650
  return key;
274
651
  }
@@ -276,11 +653,10 @@ export class OperationJournal {
276
653
  await closeQuietly(handle);
277
654
  if (error.code === "EEXIST") {
278
655
  const existing = await readFile(keyPath);
279
- if (existing.length === 32) {
656
+ if (existing.length === 32)
280
657
  return existing;
281
- }
282
658
  }
283
- throw bridgeError("JOURNAL_KEY_WRITE_FAILED", "The local grant journal key could not be saved.", true);
659
+ throw bridgeError("JOURNAL_KEY_WRITE_FAILED", "The local operation journal key could not be saved.", true);
284
660
  }
285
661
  finally {
286
662
  await unlink(temporaryPath).catch(() => undefined);
@@ -291,10 +667,7 @@ export class OperationJournal {
291
667
  const nonce = randomBytes(12);
292
668
  const cipher = createCipheriv("aes-256-gcm", key, nonce);
293
669
  cipher.setAAD(Buffer.from(clientRequestId, "utf8"));
294
- const ciphertext = Buffer.concat([
295
- cipher.update(operationToken, "utf8"),
296
- cipher.final(),
297
- ]);
670
+ const ciphertext = Buffer.concat([cipher.update(operationToken, "utf8"), cipher.final()]);
298
671
  return {
299
672
  algorithm: "aes-256-gcm",
300
673
  nonce: nonce.toString("base64url"),
@@ -314,28 +687,14 @@ export class OperationJournal {
314
687
  ]).toString("utf8");
315
688
  }
316
689
  catch (error) {
317
- if (error instanceof OmniBridgeError) {
690
+ if (error instanceof OmniBridgeError)
318
691
  throw error;
319
- }
320
692
  throw bridgeError("JOURNAL_TOKEN_DECRYPT_FAILED", "The local operation token could not be recovered.", false);
321
693
  }
322
694
  }
323
- #publicRecord(persisted, operationToken) {
324
- return {
325
- clientRequestId: persisted.clientRequestId,
326
- requestHash: persisted.requestHash,
327
- operationId: persisted.operationId,
328
- operationToken,
329
- uploadUrl: persisted.uploadUrl,
330
- state: persisted.state,
331
- createdAt: persisted.createdAt,
332
- expiresAt: persisted.expiresAt,
333
- };
334
- }
335
695
  async #syncDirectory() {
336
- if (process.platform === "win32") {
696
+ if (process.platform === "win32")
337
697
  return;
338
- }
339
698
  let directory;
340
699
  try {
341
700
  directory = await open(this.#rootDirectory, "r");