@cueai/omni-reader-mcp 1.5.5 → 1.7.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 +106 -46
- package/dist/artifact-store.d.ts +2 -0
- package/dist/artifact-store.js +45 -10
- package/dist/capabilities.d.ts +129 -2
- package/dist/capabilities.js +122 -19
- package/dist/cli/agent-config.js +2 -2
- package/dist/cli/arguments.d.ts +8 -4
- package/dist/cli/arguments.js +62 -7
- package/dist/cli/config-inspection.d.ts +59 -0
- package/dist/cli/config-inspection.js +307 -0
- package/dist/cli/doctor.d.ts +7 -0
- package/dist/cli/doctor.js +37 -2
- package/dist/cli/setup.js +13 -2
- package/dist/constants.d.ts +6 -1
- package/dist/constants.js +9 -4
- package/dist/cube-client.d.ts +4 -1
- package/dist/cube-client.js +286 -32
- package/dist/cursor.d.ts +4 -0
- package/dist/cursor.js +11 -13
- package/dist/errors.d.ts +1 -0
- package/dist/errors.js +15 -0
- package/dist/iiis-client.d.ts +33 -2
- package/dist/iiis-client.js +368 -40
- package/dist/index.js +10 -1
- package/dist/operation-journal.d.ts +17 -1
- package/dist/operation-journal.js +260 -15
- package/dist/operation-manager.d.ts +6 -2
- package/dist/operation-manager.js +448 -89
- package/dist/path-normalization.d.ts +5 -0
- package/dist/path-normalization.js +25 -0
- package/dist/path-security.d.ts +2 -1
- package/dist/path-security.js +49 -28
- package/dist/protocol.d.ts +10 -2
- package/dist/protocol.js +23 -7
- package/dist/remote-client.js +3 -13
- package/dist/result-contract.d.ts +76 -32
- package/dist/result-contract.js +121 -5
- package/dist/task-runtime.d.ts +3 -2
- package/dist/task-runtime.js +2 -2
- package/dist/tools.d.ts +4 -0
- package/dist/tools.js +41 -31
- package/package.json +1 -1
|
@@ -1,13 +1,23 @@
|
|
|
1
|
+
import type { DirectProfile } from "./capabilities.js";
|
|
1
2
|
import { type RepresentationIntent } from "./protocol.js";
|
|
2
3
|
export type JournalState = "CREATED" | "GRANT_PENDING" | "GRANT_ISSUED" | "UPLOADING" | "PROCESSING" | "RESULT_READY" | "ACK_PENDING" | "CLEANUP_PENDING" | "COMPLETED" | "FAILED" | "CANCELED" | "EXPIRED";
|
|
3
4
|
export type JournalProgressUnit = "page" | "sheet" | "slide" | "frame" | "segment";
|
|
4
5
|
export type JournalCleanupState = "not_created" | "in_use" | "pending" | "deleted";
|
|
5
6
|
export type JournalDeliveryState = "not_created" | "pending" | "deleted_after_ack";
|
|
7
|
+
export type JournalResultDelivery = "auto" | "artifact";
|
|
6
8
|
export interface JournalProgress {
|
|
7
9
|
readonly unit: JournalProgressUnit;
|
|
8
10
|
readonly completed: number;
|
|
9
11
|
readonly total: number;
|
|
10
12
|
}
|
|
13
|
+
export interface JournalBillingFacts {
|
|
14
|
+
readonly creditsCharged: number;
|
|
15
|
+
readonly creditsRemaining: number;
|
|
16
|
+
}
|
|
17
|
+
export interface JournalSettlementFacts {
|
|
18
|
+
readonly directProfile: DirectProfile | null;
|
|
19
|
+
readonly billing: JournalBillingFacts;
|
|
20
|
+
}
|
|
11
21
|
export type JournalSourceKind = "local" | "url";
|
|
12
22
|
export type JournalDetail = "text" | "grounded" | "layout";
|
|
13
23
|
export type JournalGroundingSchemaVersion = "none" | "omni.grounding.v1";
|
|
@@ -22,6 +32,9 @@ export interface JournalRecord {
|
|
|
22
32
|
readonly detail: JournalDetail;
|
|
23
33
|
readonly groundingSchemaVersion: JournalGroundingSchemaVersion;
|
|
24
34
|
readonly bundleProtocolVersion: JournalBundleProtocolVersion;
|
|
35
|
+
readonly resultDeliveryEffective: JournalResultDelivery;
|
|
36
|
+
readonly directProfile: DirectProfile | null;
|
|
37
|
+
readonly billing: JournalBillingFacts | null;
|
|
25
38
|
readonly operationId: string | null;
|
|
26
39
|
readonly operationToken: string | null;
|
|
27
40
|
readonly uploadUrl: string | null;
|
|
@@ -85,9 +98,12 @@ export declare class OperationJournal {
|
|
|
85
98
|
constructor(options?: OperationJournalOptions);
|
|
86
99
|
requestIdentityHmac(canonicalIdentityJson: string): Promise<string>;
|
|
87
100
|
sourceLocatorHmac(sourceLocator: string): Promise<string>;
|
|
88
|
-
beginIntent(clientRequestId: string, canonicalIdentityJson: string, sourceKind?: JournalSourceKind, sourceLocator?: string | null, representation?: RepresentationIntent): Promise<JournalRecord>;
|
|
101
|
+
beginIntent(clientRequestId: string, canonicalIdentityJson: string, sourceKind?: JournalSourceKind, sourceLocator?: string | null, representation?: RepresentationIntent, resultDelivery?: JournalResultDelivery): Promise<JournalRecord>;
|
|
89
102
|
migrateLegacyRecord(clientRequestId: string, canonicalIdentityJson: string, sourceLocator?: string | null): Promise<JournalRecord | null>;
|
|
90
103
|
transition(clientRequestId: string, expectedState: JournalState, nextState: JournalState, patch: JournalPatch): Promise<JournalRecord>;
|
|
104
|
+
strengthenResultDelivery(clientRequestId: string, requested: JournalResultDelivery): Promise<JournalRecord>;
|
|
105
|
+
bindDirectProfile(clientRequestId: string, directProfile: DirectProfile): Promise<JournalRecord>;
|
|
106
|
+
bindSettlementFacts(clientRequestId: string, facts: JournalSettlementFacts): Promise<JournalRecord>;
|
|
91
107
|
loadByRequestId(clientRequestId: string): Promise<JournalRecord | null>;
|
|
92
108
|
loadByOperationId(operationId: string): Promise<JournalRecord | null>;
|
|
93
109
|
loadLatestByRequestIdentityHmac(requestIdentityHmac: string): Promise<JournalRecord | null>;
|
|
@@ -4,7 +4,7 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { OmniBridgeError } from "./errors.js";
|
|
6
6
|
import { GROUNDING_SCHEMA_VERSION, RESULT_BUNDLE_PROTOCOL_VERSION, normalizeRepresentation, } from "./protocol.js";
|
|
7
|
-
// D2-D item 4-6: v3 records persist keyed identities instead of unkeyed
|
|
7
|
+
// D2-D item 4-6: v3/v4 records persist keyed identities instead of unkeyed
|
|
8
8
|
// request/source hashes. requestIdentityHmac and sourceLocatorHmac are
|
|
9
9
|
// lowercase full hex of HMAC-SHA-256 over the journal secret with the exact
|
|
10
10
|
// domain prefixes below; they are never returned in tool output and never
|
|
@@ -43,6 +43,15 @@ const PERSISTED_V3_KEYS = [
|
|
|
43
43
|
"resultExpiresAt",
|
|
44
44
|
"errorCode",
|
|
45
45
|
];
|
|
46
|
+
const PERSISTED_V4_KEYS = [
|
|
47
|
+
...PERSISTED_V3_KEYS,
|
|
48
|
+
"result_delivery_effective",
|
|
49
|
+
];
|
|
50
|
+
const PERSISTED_V5_KEYS = [
|
|
51
|
+
...PERSISTED_V4_KEYS,
|
|
52
|
+
"direct_profile",
|
|
53
|
+
"billing",
|
|
54
|
+
];
|
|
46
55
|
const JOURNAL_STATES = new Set([
|
|
47
56
|
"CREATED",
|
|
48
57
|
"GRANT_PENDING",
|
|
@@ -81,6 +90,7 @@ const DELIVERY_STATES = new Set([
|
|
|
81
90
|
"pending",
|
|
82
91
|
"deleted_after_ack",
|
|
83
92
|
]);
|
|
93
|
+
const RESULT_DELIVERIES = new Set(["auto", "artifact"]);
|
|
84
94
|
const RECORD_FILE_PATTERN = /^[0-9a-f]{64}(?:\.issued)?\.json$/u;
|
|
85
95
|
const LOCK_STALE_MS = 30_000;
|
|
86
96
|
const HEX_64_PATTERN = /^[0-9a-f]{64}$/u;
|
|
@@ -255,6 +265,73 @@ function parseVersionThreeRecord(value) {
|
|
|
255
265
|
}
|
|
256
266
|
return value;
|
|
257
267
|
}
|
|
268
|
+
function parseVersionFourRecord(value) {
|
|
269
|
+
const keys = Object.keys(value);
|
|
270
|
+
if (keys.length !== PERSISTED_V4_KEYS.length ||
|
|
271
|
+
PERSISTED_V4_KEYS.some((key) => !(key in value)) ||
|
|
272
|
+
value.version !== 4 ||
|
|
273
|
+
typeof value.result_delivery_effective !== "string" ||
|
|
274
|
+
!RESULT_DELIVERIES.has(value.result_delivery_effective)) {
|
|
275
|
+
throw new Error("version-4 record fields are invalid");
|
|
276
|
+
}
|
|
277
|
+
const { result_delivery_effective, ...versionThreeFields } = value;
|
|
278
|
+
const validated = parseVersionThreeRecord({
|
|
279
|
+
...versionThreeFields,
|
|
280
|
+
version: 3,
|
|
281
|
+
});
|
|
282
|
+
return {
|
|
283
|
+
...validated,
|
|
284
|
+
version: 4,
|
|
285
|
+
result_delivery_effective: result_delivery_effective,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
function isBillingDirectProfile(value) {
|
|
289
|
+
return value === "omni.direct_text_billing.v1"
|
|
290
|
+
|| value === "omni.direct_grounding_billing.v1";
|
|
291
|
+
}
|
|
292
|
+
function isPersistedBilling(value) {
|
|
293
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
const billing = value;
|
|
297
|
+
return Object.keys(billing).length === 2
|
|
298
|
+
&& "credits_charged" in billing
|
|
299
|
+
&& "credits_remaining" in billing
|
|
300
|
+
&& typeof billing.credits_charged === "number"
|
|
301
|
+
&& Number.isFinite(billing.credits_charged)
|
|
302
|
+
&& billing.credits_charged >= 0
|
|
303
|
+
&& typeof billing.credits_remaining === "number"
|
|
304
|
+
&& Number.isFinite(billing.credits_remaining)
|
|
305
|
+
&& billing.credits_remaining >= 0;
|
|
306
|
+
}
|
|
307
|
+
function parseVersionFiveRecord(value) {
|
|
308
|
+
const keys = Object.keys(value);
|
|
309
|
+
if (keys.length !== PERSISTED_V5_KEYS.length
|
|
310
|
+
|| PERSISTED_V5_KEYS.some((key) => !(key in value))
|
|
311
|
+
|| value.version !== 5
|
|
312
|
+
|| !(value.direct_profile === null
|
|
313
|
+
|| isBillingDirectProfile(value.direct_profile))
|
|
314
|
+
|| !(value.billing === null || isPersistedBilling(value.billing))) {
|
|
315
|
+
throw new Error("version-5 record fields are invalid");
|
|
316
|
+
}
|
|
317
|
+
const { direct_profile, billing, ...versionFourFields } = value;
|
|
318
|
+
const validated = parseVersionFourRecord({
|
|
319
|
+
...versionFourFields,
|
|
320
|
+
version: 4,
|
|
321
|
+
});
|
|
322
|
+
if ((validated.sourceKind === "url" && direct_profile !== null)
|
|
323
|
+
|| (validated.sourceKind === "local"
|
|
324
|
+
&& billing !== null
|
|
325
|
+
&& direct_profile === null)) {
|
|
326
|
+
throw new Error("version-5 settlement facts are invalid");
|
|
327
|
+
}
|
|
328
|
+
return {
|
|
329
|
+
...validated,
|
|
330
|
+
version: 5,
|
|
331
|
+
direct_profile: direct_profile,
|
|
332
|
+
billing: billing,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
258
335
|
function parsePersistedRecord(value) {
|
|
259
336
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
260
337
|
throw new Error("record is not an object");
|
|
@@ -266,12 +343,16 @@ function parsePersistedRecord(value) {
|
|
|
266
343
|
return parseVersionTwoRecord(record);
|
|
267
344
|
if (record.version === 3)
|
|
268
345
|
return parseVersionThreeRecord(record);
|
|
346
|
+
if (record.version === 4)
|
|
347
|
+
return parseVersionFourRecord(record);
|
|
348
|
+
if (record.version === 5)
|
|
349
|
+
return parseVersionFiveRecord(record);
|
|
269
350
|
throw new Error("record version is invalid");
|
|
270
351
|
}
|
|
271
352
|
function migrateInMemory(stored) {
|
|
272
|
-
if (stored.version === 3) {
|
|
353
|
+
if (stored.version === 3 || stored.version === 4 || stored.version === 5) {
|
|
273
354
|
return {
|
|
274
|
-
version:
|
|
355
|
+
version: 5,
|
|
275
356
|
clientRequestId: stored.clientRequestId,
|
|
276
357
|
requestIdentityHmac: stored.requestIdentityHmac,
|
|
277
358
|
sourceLocatorHmac: stored.sourceLocatorHmac,
|
|
@@ -282,6 +363,16 @@ function migrateInMemory(stored) {
|
|
|
282
363
|
detail: stored.detail,
|
|
283
364
|
groundingSchemaVersion: stored.groundingSchemaVersion,
|
|
284
365
|
bundleProtocolVersion: stored.bundleProtocolVersion,
|
|
366
|
+
resultDeliveryEffective: stored.version === 4 || stored.version === 5
|
|
367
|
+
? stored.result_delivery_effective
|
|
368
|
+
: "auto",
|
|
369
|
+
directProfile: stored.version === 5 ? stored.direct_profile : null,
|
|
370
|
+
billing: stored.version === 5 && stored.billing !== null
|
|
371
|
+
? {
|
|
372
|
+
creditsCharged: stored.billing.credits_charged,
|
|
373
|
+
creditsRemaining: stored.billing.credits_remaining,
|
|
374
|
+
}
|
|
375
|
+
: null,
|
|
285
376
|
operationId: stored.operationId,
|
|
286
377
|
operationToken: stored.operationToken,
|
|
287
378
|
state: stored.state,
|
|
@@ -308,7 +399,7 @@ function migrateInMemory(stored) {
|
|
|
308
399
|
// are recomputed only when safe canonical source facts reconstruct the
|
|
309
400
|
// stored unkeyed requestHash (see #reconstructLegacy).
|
|
310
401
|
return {
|
|
311
|
-
version:
|
|
402
|
+
version: 5,
|
|
312
403
|
clientRequestId: stored.clientRequestId,
|
|
313
404
|
requestIdentityHmac: null,
|
|
314
405
|
sourceLocatorHmac: null,
|
|
@@ -319,6 +410,9 @@ function migrateInMemory(stored) {
|
|
|
319
410
|
detail: "text",
|
|
320
411
|
groundingSchemaVersion: "none",
|
|
321
412
|
bundleProtocolVersion: "none",
|
|
413
|
+
resultDeliveryEffective: "auto",
|
|
414
|
+
directProfile: null,
|
|
415
|
+
billing: null,
|
|
322
416
|
operationId: stored.operationId,
|
|
323
417
|
operationToken: stored.operationToken,
|
|
324
418
|
state: stored.state,
|
|
@@ -341,7 +435,7 @@ function migrateInMemory(stored) {
|
|
|
341
435
|
};
|
|
342
436
|
}
|
|
343
437
|
return {
|
|
344
|
-
version:
|
|
438
|
+
version: 5,
|
|
345
439
|
clientRequestId: stored.clientRequestId,
|
|
346
440
|
requestIdentityHmac: null,
|
|
347
441
|
sourceLocatorHmac: null,
|
|
@@ -352,6 +446,9 @@ function migrateInMemory(stored) {
|
|
|
352
446
|
detail: "text",
|
|
353
447
|
groundingSchemaVersion: "none",
|
|
354
448
|
bundleProtocolVersion: "none",
|
|
449
|
+
resultDeliveryEffective: "auto",
|
|
450
|
+
directProfile: null,
|
|
451
|
+
billing: null,
|
|
355
452
|
operationId: stored.operationId,
|
|
356
453
|
operationToken: stored.operationToken,
|
|
357
454
|
state: stored.state,
|
|
@@ -407,6 +504,10 @@ function assertStableTransition(current, updated) {
|
|
|
407
504
|
throw bridgeError("JOURNAL_IDENTITY_CONFLICT", "A stable local operation identifier cannot be replaced.", false);
|
|
408
505
|
}
|
|
409
506
|
}
|
|
507
|
+
if (current.directProfile !== null
|
|
508
|
+
&& current.directProfile !== updated.directProfile) {
|
|
509
|
+
throw bridgeError("JOURNAL_DIRECT_PROFILE_MISMATCH", "The direct billing profile cannot be replaced.", false);
|
|
510
|
+
}
|
|
410
511
|
if (current.operationToken !== null &&
|
|
411
512
|
current.operationToken !== updated.operationToken &&
|
|
412
513
|
!(updated.operationToken === null && TERMINAL_STATES.has(updated.state))) {
|
|
@@ -428,12 +529,28 @@ function assertStableTransition(current, updated) {
|
|
|
428
529
|
throw bridgeError("JOURNAL_FACT_REGRESSION", "A confirmed local operation fact cannot be reverted.", false);
|
|
429
530
|
}
|
|
430
531
|
}
|
|
532
|
+
if (current.resultDeliveryEffective === "artifact" &&
|
|
533
|
+
updated.resultDeliveryEffective !== "artifact") {
|
|
534
|
+
throw bridgeError("JOURNAL_RESULT_DELIVERY_REGRESSION", "Artifact result delivery cannot be weakened to automatic delivery.", false);
|
|
535
|
+
}
|
|
431
536
|
}
|
|
432
537
|
function hmacEqual(left, right) {
|
|
433
538
|
if (left.length !== right.length)
|
|
434
539
|
return false;
|
|
435
540
|
return timingSafeEqual(Buffer.from(left, "hex"), Buffer.from(right, "hex"));
|
|
436
541
|
}
|
|
542
|
+
function assertJournalBilling(billing) {
|
|
543
|
+
if (!Number.isFinite(billing.creditsCharged)
|
|
544
|
+
|| billing.creditsCharged < 0
|
|
545
|
+
|| !Number.isFinite(billing.creditsRemaining)
|
|
546
|
+
|| billing.creditsRemaining < 0) {
|
|
547
|
+
throw bridgeError("JOURNAL_BILLING_INVALID", "The authoritative billing facts are invalid.", false);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
function billingEqual(left, right) {
|
|
551
|
+
return left.creditsCharged === right.creditsCharged
|
|
552
|
+
&& left.creditsRemaining === right.creditsRemaining;
|
|
553
|
+
}
|
|
437
554
|
export class OperationJournal {
|
|
438
555
|
#rootDirectory;
|
|
439
556
|
#now;
|
|
@@ -451,19 +568,22 @@ export class OperationJournal {
|
|
|
451
568
|
async sourceLocatorHmac(sourceLocator) {
|
|
452
569
|
return this.#hmac(SOURCE_LOCATOR_DOMAIN, sourceLocator);
|
|
453
570
|
}
|
|
454
|
-
//
|
|
571
|
+
// v5 intent creation. canonicalIdentityJson is the exact canonical
|
|
455
572
|
// serialization of {source_kind, source_facts} (which includes the
|
|
456
573
|
// normalized representation tuple); the journal derives the keyed identity
|
|
457
574
|
// from it and persists only the HMAC, never the payload or any unkeyed hash.
|
|
458
|
-
async beginIntent(clientRequestId, canonicalIdentityJson, sourceKind = "local", sourceLocator = null, representation = TEXT_REPRESENTATION) {
|
|
575
|
+
async beginIntent(clientRequestId, canonicalIdentityJson, sourceKind = "local", sourceLocator = null, representation = TEXT_REPRESENTATION, resultDelivery = "auto") {
|
|
459
576
|
validateClientRequestId(clientRequestId);
|
|
577
|
+
if (!RESULT_DELIVERIES.has(resultDelivery)) {
|
|
578
|
+
throw bridgeError("INVALID_RESULT_DELIVERY", "The result delivery mode is invalid.", false);
|
|
579
|
+
}
|
|
460
580
|
const existing = await this.#readRecord(clientRequestId);
|
|
461
581
|
if (existing !== null) {
|
|
462
582
|
return this.#requireMatchingRequest(clientRequestId, canonicalIdentityJson, sourceLocator, existing);
|
|
463
583
|
}
|
|
464
584
|
const createdAt = this.#now().toISOString();
|
|
465
585
|
const created = {
|
|
466
|
-
version:
|
|
586
|
+
version: 5,
|
|
467
587
|
clientRequestId,
|
|
468
588
|
requestIdentityHmac: await this.#hmac(REQUEST_IDENTITY_DOMAIN, canonicalIdentityJson),
|
|
469
589
|
sourceLocatorHmac: sourceLocator === null
|
|
@@ -476,6 +596,9 @@ export class OperationJournal {
|
|
|
476
596
|
detail: representation.detail,
|
|
477
597
|
groundingSchemaVersion: representation.groundingSchemaVersion,
|
|
478
598
|
bundleProtocolVersion: representation.bundleProtocolVersion,
|
|
599
|
+
resultDeliveryEffective: resultDelivery,
|
|
600
|
+
directProfile: null,
|
|
601
|
+
billing: null,
|
|
479
602
|
operationId: null,
|
|
480
603
|
operationToken: null,
|
|
481
604
|
state: "CREATED",
|
|
@@ -507,7 +630,7 @@ export class OperationJournal {
|
|
|
507
630
|
}
|
|
508
631
|
// Migrate a legacy v1/v2 record using the caller's safe canonical source
|
|
509
632
|
// facts. When the facts reconstruct the stored unkeyed requestHash, the
|
|
510
|
-
// record migrates to
|
|
633
|
+
// record migrates to v4 (durably) with domain-separated HMACs and keeps its
|
|
511
634
|
// state; otherwise it becomes a terminal source-free recovery failure and
|
|
512
635
|
// never copies the unkeyed hashes. Returns null when no record exists.
|
|
513
636
|
async migrateLegacyRecord(clientRequestId, canonicalIdentityJson, sourceLocator = null) {
|
|
@@ -515,8 +638,9 @@ export class OperationJournal {
|
|
|
515
638
|
const stored = await this.#readRecord(clientRequestId);
|
|
516
639
|
if (stored === null)
|
|
517
640
|
return null;
|
|
518
|
-
if (stored.version === 3)
|
|
641
|
+
if (stored.version === 3 || stored.version === 4 || stored.version === 5) {
|
|
519
642
|
return this.#recordToPublic(clientRequestId, stored);
|
|
643
|
+
}
|
|
520
644
|
const expected = `sha256:${createHash("sha256").update(canonicalIdentityJson, "utf8").digest("hex")}`;
|
|
521
645
|
if (stored.requestHash === expected) {
|
|
522
646
|
const migrated = await this.#reconstructLegacy(stored, canonicalIdentityJson, sourceLocator);
|
|
@@ -559,6 +683,105 @@ export class OperationJournal {
|
|
|
559
683
|
return updated;
|
|
560
684
|
});
|
|
561
685
|
}
|
|
686
|
+
async strengthenResultDelivery(clientRequestId, requested) {
|
|
687
|
+
validateClientRequestId(clientRequestId);
|
|
688
|
+
if (!RESULT_DELIVERIES.has(requested)) {
|
|
689
|
+
throw bridgeError("INVALID_RESULT_DELIVERY", "The result delivery mode is invalid.", false);
|
|
690
|
+
}
|
|
691
|
+
return this.#withRecordLock(clientRequestId, async () => {
|
|
692
|
+
const stored = await this.#readRecord(clientRequestId);
|
|
693
|
+
if (stored === null) {
|
|
694
|
+
throw bridgeError("JOURNAL_RECORD_NOT_FOUND", "The local operation journal record is missing.", true);
|
|
695
|
+
}
|
|
696
|
+
const current = await this.#recordToPublic(clientRequestId, stored);
|
|
697
|
+
if (requested === "auto" || current.resultDeliveryEffective === "artifact") {
|
|
698
|
+
return current;
|
|
699
|
+
}
|
|
700
|
+
const updated = {
|
|
701
|
+
...current,
|
|
702
|
+
resultDeliveryEffective: "artifact",
|
|
703
|
+
updatedAt: this.#now().toISOString(),
|
|
704
|
+
};
|
|
705
|
+
assertStableTransition(current, updated);
|
|
706
|
+
await this.#replaceRecord(await this.#persistedRecord(updated), this.#recordPath(clientRequestId));
|
|
707
|
+
await unlink(this.#legacyIssuedRecordPath(clientRequestId)).catch(() => undefined);
|
|
708
|
+
return updated;
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
async bindDirectProfile(clientRequestId, directProfile) {
|
|
712
|
+
validateClientRequestId(clientRequestId);
|
|
713
|
+
if (!isBillingDirectProfile(directProfile)) {
|
|
714
|
+
throw bridgeError("JOURNAL_DIRECT_PROFILE_MISMATCH", "The direct billing profile does not match the operation source.", false);
|
|
715
|
+
}
|
|
716
|
+
return this.#withRecordLock(clientRequestId, async () => {
|
|
717
|
+
const stored = await this.#readRecord(clientRequestId);
|
|
718
|
+
if (stored === null) {
|
|
719
|
+
throw bridgeError("JOURNAL_RECORD_NOT_FOUND", "The local operation journal record is missing.", true);
|
|
720
|
+
}
|
|
721
|
+
const current = await this.#recordToPublic(clientRequestId, stored);
|
|
722
|
+
if (current.sourceKind !== "local") {
|
|
723
|
+
throw bridgeError("JOURNAL_DIRECT_PROFILE_MISMATCH", "The direct billing profile does not match the operation source.", false);
|
|
724
|
+
}
|
|
725
|
+
if (current.directProfile !== null) {
|
|
726
|
+
if (current.directProfile !== directProfile) {
|
|
727
|
+
throw bridgeError("JOURNAL_DIRECT_PROFILE_MISMATCH", "The direct billing profile cannot be replaced.", false);
|
|
728
|
+
}
|
|
729
|
+
return current;
|
|
730
|
+
}
|
|
731
|
+
const updated = {
|
|
732
|
+
...current,
|
|
733
|
+
directProfile,
|
|
734
|
+
updatedAt: this.#now().toISOString(),
|
|
735
|
+
};
|
|
736
|
+
assertStableTransition(current, updated);
|
|
737
|
+
await this.#replaceRecord(await this.#persistedRecord(updated), this.#recordPath(clientRequestId));
|
|
738
|
+
await unlink(this.#legacyIssuedRecordPath(clientRequestId)).catch(() => undefined);
|
|
739
|
+
return updated;
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
async bindSettlementFacts(clientRequestId, facts) {
|
|
743
|
+
validateClientRequestId(clientRequestId);
|
|
744
|
+
assertJournalBilling(facts.billing);
|
|
745
|
+
return this.#withRecordLock(clientRequestId, async () => {
|
|
746
|
+
const stored = await this.#readRecord(clientRequestId);
|
|
747
|
+
if (stored === null) {
|
|
748
|
+
throw bridgeError("JOURNAL_RECORD_NOT_FOUND", "The local operation journal record is missing.", true);
|
|
749
|
+
}
|
|
750
|
+
const current = await this.#recordToPublic(clientRequestId, stored);
|
|
751
|
+
const profileMatchesSource = current.sourceKind === "url"
|
|
752
|
+
? facts.directProfile === null
|
|
753
|
+
: isBillingDirectProfile(facts.directProfile);
|
|
754
|
+
if (!profileMatchesSource) {
|
|
755
|
+
throw bridgeError("JOURNAL_DIRECT_PROFILE_MISMATCH", "The direct billing profile does not match the operation source.", false);
|
|
756
|
+
}
|
|
757
|
+
if (current.billing !== null) {
|
|
758
|
+
if (current.directProfile !== facts.directProfile) {
|
|
759
|
+
throw bridgeError("JOURNAL_DIRECT_PROFILE_MISMATCH", "The direct billing profile cannot be replaced.", false);
|
|
760
|
+
}
|
|
761
|
+
if (!billingEqual(current.billing, facts.billing)) {
|
|
762
|
+
throw bridgeError("JOURNAL_BILLING_MISMATCH", "The authoritative billing facts cannot be replaced.", false);
|
|
763
|
+
}
|
|
764
|
+
return current;
|
|
765
|
+
}
|
|
766
|
+
if (current.directProfile !== null
|
|
767
|
+
&& current.directProfile !== facts.directProfile) {
|
|
768
|
+
throw bridgeError("JOURNAL_DIRECT_PROFILE_MISMATCH", "The direct billing profile cannot be replaced.", false);
|
|
769
|
+
}
|
|
770
|
+
const updated = {
|
|
771
|
+
...current,
|
|
772
|
+
directProfile: facts.directProfile,
|
|
773
|
+
billing: {
|
|
774
|
+
creditsCharged: facts.billing.creditsCharged,
|
|
775
|
+
creditsRemaining: facts.billing.creditsRemaining,
|
|
776
|
+
},
|
|
777
|
+
updatedAt: this.#now().toISOString(),
|
|
778
|
+
};
|
|
779
|
+
assertStableTransition(current, updated);
|
|
780
|
+
await this.#replaceRecord(await this.#persistedRecord(updated), this.#recordPath(clientRequestId));
|
|
781
|
+
await unlink(this.#legacyIssuedRecordPath(clientRequestId)).catch(() => undefined);
|
|
782
|
+
return updated;
|
|
783
|
+
});
|
|
784
|
+
}
|
|
562
785
|
async loadByRequestId(clientRequestId) {
|
|
563
786
|
validateClientRequestId(clientRequestId);
|
|
564
787
|
const persisted = await this.#readRecord(clientRequestId);
|
|
@@ -592,7 +815,7 @@ export class OperationJournal {
|
|
|
592
815
|
// the journal receives, so the keyed handle is derived over that value.
|
|
593
816
|
const createdAt = this.#now().toISOString();
|
|
594
817
|
const created = {
|
|
595
|
-
version:
|
|
818
|
+
version: 5,
|
|
596
819
|
clientRequestId,
|
|
597
820
|
requestIdentityHmac: await this.#hmac(REQUEST_IDENTITY_DOMAIN, requestHash),
|
|
598
821
|
sourceLocatorHmac: null,
|
|
@@ -603,6 +826,9 @@ export class OperationJournal {
|
|
|
603
826
|
detail: "text",
|
|
604
827
|
groundingSchemaVersion: "none",
|
|
605
828
|
bundleProtocolVersion: "none",
|
|
829
|
+
resultDeliveryEffective: "auto",
|
|
830
|
+
directProfile: null,
|
|
831
|
+
billing: null,
|
|
606
832
|
operationId: null,
|
|
607
833
|
operationToken: null,
|
|
608
834
|
state: "CREATED",
|
|
@@ -698,7 +924,7 @@ export class OperationJournal {
|
|
|
698
924
|
}
|
|
699
925
|
async #persistedRecord(record) {
|
|
700
926
|
return {
|
|
701
|
-
version:
|
|
927
|
+
version: 5,
|
|
702
928
|
clientRequestId: record.clientRequestId,
|
|
703
929
|
// A legacy record persisted before reconstruction (for example a
|
|
704
930
|
// source-free recovery failure) gets the deterministic keyed handle of
|
|
@@ -711,6 +937,14 @@ export class OperationJournal {
|
|
|
711
937
|
detail: record.detail,
|
|
712
938
|
groundingSchemaVersion: record.groundingSchemaVersion,
|
|
713
939
|
bundleProtocolVersion: record.bundleProtocolVersion,
|
|
940
|
+
result_delivery_effective: record.resultDeliveryEffective,
|
|
941
|
+
direct_profile: record.directProfile,
|
|
942
|
+
billing: record.billing === null
|
|
943
|
+
? null
|
|
944
|
+
: {
|
|
945
|
+
credits_charged: record.billing.creditsCharged,
|
|
946
|
+
credits_remaining: record.billing.creditsRemaining,
|
|
947
|
+
},
|
|
714
948
|
operationId: record.operationId,
|
|
715
949
|
operationToken: record.operationToken === null
|
|
716
950
|
? null
|
|
@@ -736,7 +970,7 @@ export class OperationJournal {
|
|
|
736
970
|
}
|
|
737
971
|
async #persistedFromInMemory(record) {
|
|
738
972
|
return {
|
|
739
|
-
version:
|
|
973
|
+
version: 5,
|
|
740
974
|
clientRequestId: record.clientRequestId,
|
|
741
975
|
requestIdentityHmac: record.requestIdentityHmac === null
|
|
742
976
|
? await this.#hmac(REQUEST_IDENTITY_DOMAIN, "")
|
|
@@ -746,6 +980,14 @@ export class OperationJournal {
|
|
|
746
980
|
detail: record.detail,
|
|
747
981
|
groundingSchemaVersion: record.groundingSchemaVersion,
|
|
748
982
|
bundleProtocolVersion: record.bundleProtocolVersion,
|
|
983
|
+
result_delivery_effective: record.resultDeliveryEffective,
|
|
984
|
+
direct_profile: record.directProfile,
|
|
985
|
+
billing: record.billing === null
|
|
986
|
+
? null
|
|
987
|
+
: {
|
|
988
|
+
credits_charged: record.billing.creditsCharged,
|
|
989
|
+
credits_remaining: record.billing.creditsRemaining,
|
|
990
|
+
},
|
|
749
991
|
operationId: record.operationId,
|
|
750
992
|
operationToken: record.operationToken,
|
|
751
993
|
state: record.state,
|
|
@@ -784,6 +1026,9 @@ export class OperationJournal {
|
|
|
784
1026
|
detail: migrated.detail,
|
|
785
1027
|
groundingSchemaVersion: migrated.groundingSchemaVersion,
|
|
786
1028
|
bundleProtocolVersion: migrated.bundleProtocolVersion,
|
|
1029
|
+
resultDeliveryEffective: migrated.resultDeliveryEffective,
|
|
1030
|
+
directProfile: migrated.directProfile,
|
|
1031
|
+
billing: migrated.billing,
|
|
787
1032
|
operationId: migrated.operationId,
|
|
788
1033
|
operationToken,
|
|
789
1034
|
uploadUrl: migrated.uploadUrl,
|
|
@@ -819,7 +1064,7 @@ export class OperationJournal {
|
|
|
819
1064
|
return migrated;
|
|
820
1065
|
}
|
|
821
1066
|
async #requireMatchingRequest(clientRequestId, canonicalIdentityJson, sourceLocator, persisted) {
|
|
822
|
-
if (persisted.version === 3) {
|
|
1067
|
+
if (persisted.version === 3 || persisted.version === 4 || persisted.version === 5) {
|
|
823
1068
|
const expected = await this.#hmacExisting(REQUEST_IDENTITY_DOMAIN, canonicalIdentityJson);
|
|
824
1069
|
if (!hmacEqual(persisted.requestIdentityHmac, expected)) {
|
|
825
1070
|
throw bridgeError("IDEMPOTENCY_COLLISION", "This local operation request identifier is already bound to different metadata.", false);
|
|
@@ -833,7 +1078,7 @@ export class OperationJournal {
|
|
|
833
1078
|
throw bridgeError("IDEMPOTENCY_COLLISION", "This local operation request identifier is already bound to different metadata.", false);
|
|
834
1079
|
}
|
|
835
1080
|
async #requireMatchingCubeRequest(clientRequestId, requestHash, persisted) {
|
|
836
|
-
if (persisted.version === 3) {
|
|
1081
|
+
if (persisted.version === 3 || persisted.version === 4 || persisted.version === 5) {
|
|
837
1082
|
const expected = await this.#hmacExisting(REQUEST_IDENTITY_DOMAIN, requestHash);
|
|
838
1083
|
if (!hmacEqual(persisted.requestIdentityHmac, expected)) {
|
|
839
1084
|
throw bridgeError("IDEMPOTENCY_COLLISION", "This local operation request identifier is already bound to different metadata.", false);
|
|
@@ -3,7 +3,7 @@ import type { CubeGrantClient } from "./cube-client.js";
|
|
|
3
3
|
import type { IiisClient, ResultRetentionSink } from "./iiis-client.js";
|
|
4
4
|
import type { JournalPatch, JournalRecord, JournalState, OperationJournal } from "./operation-journal.js";
|
|
5
5
|
import { type OpenAllowedFileOptions, type OpenedAllowedFile } from "./path-security.js";
|
|
6
|
-
import { type RepresentationIntent } from "./protocol.js";
|
|
6
|
+
import { type RepresentationIntent, type ResultDelivery } from "./protocol.js";
|
|
7
7
|
import type { RemoteOmniClient } from "./remote-client.js";
|
|
8
8
|
import { type ParseResult } from "./result-contract.js";
|
|
9
9
|
export interface SubmitOperationInput {
|
|
@@ -12,6 +12,7 @@ export interface SubmitOperationInput {
|
|
|
12
12
|
readonly clientRequestId: string;
|
|
13
13
|
readonly signal?: AbortSignal;
|
|
14
14
|
readonly context?: unknown;
|
|
15
|
+
readonly resultDelivery?: ResultDelivery;
|
|
15
16
|
readonly representation?: RepresentationIntent;
|
|
16
17
|
}
|
|
17
18
|
export interface OperationDriverUpdate {
|
|
@@ -38,8 +39,9 @@ export interface OperationManagerDriver {
|
|
|
38
39
|
result?(record: JournalRecord, signal: AbortSignal): Promise<ParseResult | void>;
|
|
39
40
|
}
|
|
40
41
|
export interface OperationManagerOptions {
|
|
41
|
-
readonly journal: Pick<OperationJournal, "beginIntent" | "migrateLegacyRecord" | "requestIdentityHmac" | "sourceLocatorHmac" | "transition" | "loadByRequestId" | "loadByOperationId" | "loadLatestByRequestIdentityHmac" | "loadLatestBySourceLocatorHmac" | "listRecoverable">;
|
|
42
|
+
readonly journal: Pick<OperationJournal, "beginIntent" | "bindSettlementFacts" | "migrateLegacyRecord" | "requestIdentityHmac" | "sourceLocatorHmac" | "transition" | "loadByRequestId" | "loadByOperationId" | "loadLatestByRequestIdentityHmac" | "loadLatestBySourceLocatorHmac" | "listRecoverable" | "strengthenResultDelivery">;
|
|
42
43
|
readonly driver: OperationManagerDriver;
|
|
44
|
+
readonly presentResult?: (record: JournalRecord, canonicalResult: ParseResult) => Promise<ParseResult>;
|
|
43
45
|
readonly now?: () => number;
|
|
44
46
|
readonly sleep?: (milliseconds: number) => Promise<void>;
|
|
45
47
|
}
|
|
@@ -61,6 +63,8 @@ export interface LocalRetention extends ResultRetentionSink {
|
|
|
61
63
|
}
|
|
62
64
|
export interface LocalArtifactStore {
|
|
63
65
|
createRetention(): LocalRetention;
|
|
66
|
+
preflightWrite?(): Promise<void>;
|
|
67
|
+
mintReadCursor?(resultId: string, byteOffset: number): Promise<string>;
|
|
64
68
|
read(resultId: string, cursor?: string, maxBytes?: number): Promise<ArtifactReadResult>;
|
|
65
69
|
readBundleDescriptor?(resultId: string): Promise<BundleLocalResult | null>;
|
|
66
70
|
}
|