@cueai/omni-reader-mcp 1.6.0 → 1.7.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.
@@ -1,8 +1,8 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { z } from "zod";
3
- import { BRIDGE_RELEASE_VERSION, CUBE_CAPABILITIES_PATH, CUBE_GRANT_PROTOCOL_VERSION, DEFAULT_CUBE_BASE_URL, MAX_FILE_BYTES, } from "./constants.js";
4
- import { parseReaderCapabilities, selectDirectProfile, } from "./capabilities.js";
5
- import { OmniBridgeError } from "./errors.js";
3
+ import { BRIDGE_RELEASE_VERSION, CAPABILITIES_V2_PATH, CUBE_CAPABILITIES_PATH, CUBE_GRANT_PROTOCOL_VERSION, DEFAULT_CUBE_BASE_URL, MAX_FILE_BYTES, } from "./constants.js";
4
+ import { DIRECT_GROUNDING_BILLING_PROFILE, DIRECT_TEXT_BILLING_PROFILE, parseReaderCapabilities, parseReaderCapabilitiesV2, selectDirectProfile, selectDirectProfileV2, } from "./capabilities.js";
5
+ import { OmniBridgeError, unsupportedDetailError, } from "./errors.js";
6
6
  const GRANT_PATH = "/api/omni-reader/direct-upload/v1/parse-grants";
7
7
  const BRIDGE_PACKAGE = "@cueai/omni-reader-mcp";
8
8
  // Legacy grant response for UNPROFILED (detail-less) requests. cube-mcp serves
@@ -48,6 +48,16 @@ const bridgeUpgradeRequiredSchema = z
48
48
  retryable: z.literal(false),
49
49
  })
50
50
  .strict();
51
+ const unsupportedDetailSchema = z
52
+ .object({
53
+ code: z.literal("UNSUPPORTED_DETAIL"),
54
+ message: z.literal("The declared capability profile is not supported by this control plane."),
55
+ file_uploaded: z.literal(false),
56
+ billed: z.literal(false),
57
+ content_released: z.literal(false),
58
+ retryable: z.literal(false),
59
+ })
60
+ .strict();
51
61
  // Closed v3 grant response: the exact tuple of the selected direct profile is
52
62
  // required before the upload phase may start. The tuple is re-checked against
53
63
  // the profile after schema parsing so a drifting server cannot slip through.
@@ -70,6 +80,90 @@ const grantResponseV3Schema = z
70
80
  approved_result_max_bytes: z.literal(67108864),
71
81
  })
72
82
  .strict();
83
+ const grantResponseV4Schema = z
84
+ .object({
85
+ grant_id: z.string().min(1),
86
+ operation_id: z.string().min(1),
87
+ parse_grant: z.string().min(1),
88
+ operation_token: z.string().min(1),
89
+ upload_url: z
90
+ .string()
91
+ .url()
92
+ .refine((value) => new URL(value).protocol === "https:"),
93
+ expires_at: z.string().datetime({ offset: true }),
94
+ max_bytes: z.literal(MAX_FILE_BYTES),
95
+ protocol_version: z.literal("omni.parse_grant.v4"),
96
+ })
97
+ .strict();
98
+ const textRepresentationV4Schema = z
99
+ .object({
100
+ kind: z.literal("text"),
101
+ media_type: z.literal("text/markdown; charset=utf-8"),
102
+ usage_schema_version: z.literal("omni_parse_usage.v2"),
103
+ billing_contract_version: z.literal("omni_billing.v2"),
104
+ })
105
+ .strict();
106
+ const bundleRepresentationV4Schema = z
107
+ .object({
108
+ kind: z.literal("result_bundle"),
109
+ media_type: z.literal("application/vnd.cue.omni-result-bundle+json; version=1"),
110
+ bundle_protocol_version: z.literal("omni.result_bundle.v1"),
111
+ grounding_schema_version: z.literal("omni.grounding.v1"),
112
+ usage_schema_version: z.literal("omni_parse_usage.v2"),
113
+ billing_contract_version: z.literal("omni_billing.v2"),
114
+ })
115
+ .strict();
116
+ const signedGrantV4BaseShape = {
117
+ protocol_version: z.literal("omni.parse_grant.v4"),
118
+ iss: z.string().min(1).max(64),
119
+ aud: z.literal("omni-reader-l1"),
120
+ sub: z.string().regex(/^sha256:[a-f0-9]{64}$/u),
121
+ jti: z.string().regex(/^pg_[A-Za-z0-9_-]+$/u),
122
+ grant_id: z.string().regex(/^pg_[A-Za-z0-9_-]+$/u),
123
+ operation_id: z.string().regex(/^op_[A-Za-z0-9_-]+$/u),
124
+ usage_event_id: z.string().regex(/^[a-f0-9]{32}$/u),
125
+ request_id: z.string().max(128).regex(/^req_[A-Za-z0-9_-]+$/u),
126
+ stream_protocol: z.literal("omni.granted_parse_stream.v3"),
127
+ operation_protocol: z.literal("omni.direct_operation.v3"),
128
+ settlement_protocol: z.literal("omni.grant_settlement.v5"),
129
+ release_protocol: z.literal("omni.release_decision.v3"),
130
+ settlement_journal_protocol: z.literal("omni.direct_settlement_journal.v3"),
131
+ usage_schema_version: z.literal("omni_parse_usage.v2"),
132
+ billing_contract_version: z.literal("omni_billing.v2"),
133
+ bridge_protocol: z.literal("omni.local_bridge_tools.v5"),
134
+ method: z.literal("POST"),
135
+ path: z.literal("/omni/granted/parse_stream"),
136
+ content_length: z.number().int().min(1).max(MAX_FILE_BYTES),
137
+ content_type: z.string().min(1).max(128),
138
+ file_extension: z.string().regex(/^\.[a-z0-9]{1,10}$/u),
139
+ no_store: z.boolean(),
140
+ output: z.enum(["markdown", "hypertext", "chunks"]),
141
+ max_bytes: z.literal(MAX_FILE_BYTES),
142
+ nonce: z.string().min(32).max(256),
143
+ iat: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
144
+ nbf: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
145
+ exp: z.number().int().min(1).max(Number.MAX_SAFE_INTEGER),
146
+ };
147
+ const signedGrantV4Schema = z.discriminatedUnion("profile", [
148
+ z
149
+ .object({
150
+ ...signedGrantV4BaseShape,
151
+ profile: z.literal(DIRECT_TEXT_BILLING_PROFILE.profile),
152
+ detail: z.literal("text"),
153
+ representation: textRepresentationV4Schema,
154
+ approved_result_max_bytes: z.literal(DIRECT_TEXT_BILLING_PROFILE.max_result_bytes),
155
+ })
156
+ .strict(),
157
+ z
158
+ .object({
159
+ ...signedGrantV4BaseShape,
160
+ profile: z.literal(DIRECT_GROUNDING_BILLING_PROFILE.profile),
161
+ detail: z.enum(["grounded", "layout"]),
162
+ representation: bundleRepresentationV4Schema,
163
+ approved_result_max_bytes: z.literal(DIRECT_GROUNDING_BILLING_PROFILE.max_result_bytes),
164
+ })
165
+ .strict(),
166
+ ]);
73
167
  function bridgeError(code, message, retryable, options = {}) {
74
168
  return new OmniBridgeError({
75
169
  code,
@@ -81,6 +175,7 @@ function bridgeError(code, message, retryable, options = {}) {
81
175
  contentReleased: false,
82
176
  retryable,
83
177
  ...(options.failureScope === undefined ? {} : { failureScope: options.failureScope }),
178
+ ...(options.sourceKind === undefined ? {} : { sourceKind: options.sourceKind }),
84
179
  ...(options.userAction === undefined ? {} : { userAction: options.userAction }),
85
180
  ...(options.retryAfter === undefined ? {} : { retryAfter: options.retryAfter }),
86
181
  });
@@ -125,6 +220,19 @@ function grantRequestBody(input, profile) {
125
220
  approved_result_max_bytes: profile.max_result_bytes,
126
221
  };
127
222
  }
223
+ function v4GrantRequestBody(input, profile, detail, clientRequestId) {
224
+ const base = grantRequestBody(input);
225
+ const requestId = `req_${createHash("sha256")
226
+ .update(clientRequestId, "utf8")
227
+ .digest("base64url")
228
+ .slice(0, 22)}`;
229
+ return {
230
+ ...base,
231
+ profile,
232
+ detail,
233
+ request_id: requestId,
234
+ };
235
+ }
128
236
  function requireV3GrantTuple(parsed, profile, detail) {
129
237
  if (parsed.protocol_version !== profile.grant_protocol ||
130
238
  parsed.stream_protocol_version !== profile.stream_protocol ||
@@ -134,6 +242,56 @@ function requireV3GrantTuple(parsed, profile, detail) {
134
242
  throw bridgeError("CUBE_PROTOCOL_ERROR", "Cube returned a parse grant outside the negotiated representation.", false);
135
243
  }
136
244
  }
245
+ function parseSignedGrantV4(parseGrant) {
246
+ const segments = parseGrant.split(".");
247
+ if (segments.length !== 3 ||
248
+ segments.some((segment) => segment.length === 0 || !/^[A-Za-z0-9_-]+$/u.test(segment))) {
249
+ throw new Error("invalid compact JWT");
250
+ }
251
+ const payload = segments[1];
252
+ const decoded = Buffer.from(payload, "base64url");
253
+ if (decoded.length === 0 || decoded.toString("base64url") !== payload) {
254
+ throw new Error("noncanonical JWT payload");
255
+ }
256
+ const value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(decoded));
257
+ return signedGrantV4Schema.parse(value);
258
+ }
259
+ function requireV4GrantBinding(parsed, profile, detail, body) {
260
+ let claims;
261
+ try {
262
+ claims = parseSignedGrantV4(parsed.parse_grant);
263
+ }
264
+ catch {
265
+ throw bridgeError("CUBE_PROTOCOL_ERROR", "Cube returned a parse grant outside the negotiated representation.", false);
266
+ }
267
+ const expiresAt = Date.parse(parsed.expires_at);
268
+ const expiryMatches = expiresAt % 1000 === 0 && claims.exp === expiresAt / 1000;
269
+ if (claims.jti !== parsed.grant_id ||
270
+ claims.grant_id !== parsed.grant_id ||
271
+ claims.operation_id !== parsed.operation_id ||
272
+ claims.request_id !== body.request_id ||
273
+ claims.protocol_version !== profile.grant_protocol ||
274
+ claims.profile !== profile.profile ||
275
+ claims.detail !== detail ||
276
+ claims.approved_result_max_bytes !== profile.max_result_bytes ||
277
+ claims.stream_protocol !== profile.stream_protocol ||
278
+ claims.operation_protocol !== profile.operation_protocol ||
279
+ claims.settlement_protocol !== profile.settlement_protocol ||
280
+ claims.release_protocol !== profile.release_protocol ||
281
+ claims.settlement_journal_protocol !== profile.settlement_journal_protocol ||
282
+ claims.usage_schema_version !== profile.usage_protocol ||
283
+ claims.billing_contract_version !== profile.billing_protocol ||
284
+ claims.bridge_protocol !== profile.bridge_protocol ||
285
+ claims.content_length !== body.content_length ||
286
+ claims.content_type !== body.content_type ||
287
+ claims.file_extension !== body.file_extension ||
288
+ claims.no_store !== body.no_store ||
289
+ claims.output !== body.output ||
290
+ claims.max_bytes !== MAX_FILE_BYTES ||
291
+ !expiryMatches) {
292
+ throw bridgeError("CUBE_PROTOCOL_ERROR", "Cube returned a parse grant outside the negotiated representation.", false);
293
+ }
294
+ }
137
295
  export function grantRequestHash(body) {
138
296
  return `sha256:${createHash("sha256")
139
297
  .update(JSON.stringify(body), "utf8")
@@ -144,24 +302,38 @@ export function createClientRequestId() {
144
302
  }
145
303
  function responseError(status) {
146
304
  if (status === 401) {
147
- return bridgeError("INVALID_CUE_API_KEY", "The Cue API Key is invalid or expired.", false);
305
+ return bridgeError("INVALID_CUE_API_KEY", "The Cue API Key is invalid or expired.", false, { failureScope: "authentication", sourceKind: "local" });
148
306
  }
149
307
  if (status === 403) {
150
- return bridgeError("OMNI_NOT_ENTITLED", "This Cue account is not entitled to use Omni Reader.", false);
308
+ return bridgeError("OMNI_NOT_ENTITLED", "This Cue account is not entitled to use Omni Reader.", false, { failureScope: "authentication", sourceKind: "local" });
151
309
  }
152
310
  if (status === 402) {
153
- return bridgeError("INSUFFICIENT_BALANCE", "The Cue account does not have enough balance to start this parse.", false);
311
+ return bridgeError("INSUFFICIENT_BALANCE", "The Cue account does not have enough balance to start this parse.", false, { failureScope: "billing", sourceKind: "local" });
154
312
  }
155
313
  if (status === 409) {
156
- return bridgeError("IDEMPOTENCY_COLLISION", "This grant request identifier is already bound to different metadata.", false);
314
+ return bridgeError("IDEMPOTENCY_COLLISION", "This grant request identifier is already bound to different metadata.", false, { failureScope: "operation", sourceKind: "local" });
157
315
  }
158
316
  if (status === 404) {
159
- return bridgeError("DIRECT_UPLOAD_DISABLED", "Direct local-file parsing is not enabled on this Omni service.", false);
317
+ return bridgeError("DIRECT_UPLOAD_UNAVAILABLE", "The Omni service's direct-upload route is unavailable.", false, {
318
+ failureScope: "local_capability",
319
+ sourceKind: "local",
320
+ userAction: "Do not infer an account restriction; HTTP 403 is the entitlement signal. Retry only after the service route is enabled, or parse an existing HTTP(S) source instead.",
321
+ });
160
322
  }
161
323
  if (status === 429) {
162
- return bridgeError("CUBE_BUSY", "Cube is temporarily busy. Retry this same grant request later.", true);
324
+ return bridgeError("CUBE_BUSY", "Cube is temporarily busy. Retry this same grant request later.", true, { failureScope: "service", sourceKind: "local" });
163
325
  }
164
- return bridgeError("CUBE_UNAVAILABLE", "Cube could not create the parse grant. Retry this same grant request later.", status >= 500);
326
+ return bridgeError("CUBE_UNAVAILABLE", "Cube could not create the parse grant. Retry this same grant request later.", status >= 500, { failureScope: "service", sourceKind: "local" });
327
+ }
328
+ function capabilitiesResponseError(status) {
329
+ if (status === 404) {
330
+ return bridgeError("DETAIL_CAPABILITIES_UNAVAILABLE", "This Omni service does not advertise grounded or layout parsing.", false, {
331
+ failureScope: "local_capability",
332
+ sourceKind: "local",
333
+ userAction: "Use text output only when plain Markdown is acceptable; text may retain headings, lists, and tables but does not include grounding or layout sidecars.",
334
+ });
335
+ }
336
+ return responseError(status);
165
337
  }
166
338
  function retryAfterSeconds(response) {
167
339
  const value = response.headers.get("retry-after");
@@ -171,6 +343,19 @@ function retryAfterSeconds(response) {
171
343
  return Number.isSafeInteger(seconds) && seconds > 0 ? seconds : undefined;
172
344
  }
173
345
  async function grantResponseError(response) {
346
+ if (response.status === 422 && !response.headers.has("retry-after")) {
347
+ let body;
348
+ try {
349
+ body = await response.json();
350
+ }
351
+ catch {
352
+ return responseError(response.status);
353
+ }
354
+ if (unsupportedDetailSchema.safeParse(body).success) {
355
+ return unsupportedDetailError("local");
356
+ }
357
+ return responseError(response.status);
358
+ }
174
359
  // The strict 426 marker is only recognized when there is exactly a 426 status,
175
360
  // no Retry-After, and a body matching the six frozen literal fields. Any drift
176
361
  // degrades through `responseError` to the generic nonretryable CUBE_UNAVAILABLE.
@@ -185,7 +370,8 @@ async function grantResponseError(response) {
185
370
  if (bridgeUpgradeRequiredSchema.safeParse(body).success) {
186
371
  return bridgeError("BRIDGE_UPGRADE_REQUIRED", "This Bridge release is not accepted for direct local-file parsing.", false, {
187
372
  failureScope: "service",
188
- userAction: `Upgrade to @cueai/omni-reader-mcp@${BRIDGE_RELEASE_VERSION} using the official Omni setup instructions, then retry the same local-file parse.`,
373
+ sourceKind: "local",
374
+ userAction: "Install the latest published @cueai/omni-reader-mcp release using the official Omni setup instructions, then retry once. If you are already running the latest published release, do not reinstall or retry; run doctor --json and ask the service operator to verify Bridge admission.",
189
375
  });
190
376
  }
191
377
  }
@@ -201,6 +387,7 @@ async function grantResponseError(response) {
201
387
  if (principalConcurrencyErrorSchema.safeParse(body).success) {
202
388
  return bridgeError("PRINCIPAL_CONCURRENCY_LIMIT", "The maximum number of concurrent parsing operations is already active.", true, {
203
389
  failureScope: "service",
390
+ sourceKind: "local",
204
391
  userAction: "Wait for retry_after seconds, then retry the same local-file parse.",
205
392
  retryAfter,
206
393
  });
@@ -212,9 +399,11 @@ export class CubeGrantClient {
212
399
  #journal;
213
400
  #apiKey;
214
401
  #grantUrl;
215
- #capabilitiesUrl;
402
+ #capabilitiesV1Url;
403
+ #capabilitiesV2Url;
216
404
  #fetch;
217
- #capabilitiesCache;
405
+ #capabilitiesV1Cache;
406
+ #capabilitiesV2Cache;
218
407
  constructor(options) {
219
408
  this.#journal = options.journal;
220
409
  this.#apiKey =
@@ -229,7 +418,8 @@ export class CubeGrantClient {
229
418
  throw bridgeError("INSECURE_CUBE_BASE_URL", "The Cube control endpoint must use HTTPS without embedded credentials.", false);
230
419
  }
231
420
  this.#grantUrl = new URL(GRANT_PATH, baseUrl).toString();
232
- this.#capabilitiesUrl = new URL(CUBE_CAPABILITIES_PATH, baseUrl).toString();
421
+ this.#capabilitiesV1Url = new URL(CUBE_CAPABILITIES_PATH, baseUrl).toString();
422
+ this.#capabilitiesV2Url = new URL(CAPABILITIES_V2_PATH, baseUrl).toString();
233
423
  this.#fetch = options.fetchImpl ?? fetch;
234
424
  }
235
425
  // Preflight for any non-text grant request (D2-D item 4): authenticated
@@ -242,13 +432,13 @@ export class CubeGrantClient {
242
432
  throw bridgeError("MISSING_CUE_API_KEY", "Set CUE_API_KEY before using local Omni document parsing.", false);
243
433
  }
244
434
  const now = Date.now();
245
- const cached = this.#capabilitiesCache;
435
+ const cached = this.#capabilitiesV1Cache;
246
436
  if (cached !== undefined && cached.expiresAt > now) {
247
437
  return cached.value;
248
438
  }
249
439
  let response;
250
440
  try {
251
- response = await this.#fetch(this.#capabilitiesUrl, {
441
+ response = await this.#fetch(this.#capabilitiesV1Url, {
252
442
  method: "GET",
253
443
  headers: {
254
444
  authorization: `Bearer ${this.#apiKey}`,
@@ -264,7 +454,7 @@ export class CubeGrantClient {
264
454
  throw bridgeError("CUBE_UNAVAILABLE", "Cube could not provide reader capabilities. Retry the same request later.", true);
265
455
  }
266
456
  if (!response.ok) {
267
- throw responseError(response.status);
457
+ throw capabilitiesResponseError(response.status);
268
458
  }
269
459
  let capabilities;
270
460
  try {
@@ -273,7 +463,57 @@ export class CubeGrantClient {
273
463
  catch {
274
464
  throw bridgeError("CUBE_PROTOCOL_ERROR", "Cube returned invalid reader capability profiles.", false);
275
465
  }
276
- this.#capabilitiesCache = {
466
+ this.#capabilitiesV1Cache = {
467
+ expiresAt: capabilities.expires_at.getTime(),
468
+ value: capabilities,
469
+ };
470
+ return capabilities;
471
+ }
472
+ async getCapabilitiesV2(signal) {
473
+ if (this.#apiKey.length === 0) {
474
+ throw bridgeError("MISSING_CUE_API_KEY", "Set CUE_API_KEY before using local Omni document parsing.", false);
475
+ }
476
+ const now = Date.now();
477
+ const cached = this.#capabilitiesV2Cache;
478
+ if (cached !== undefined && cached.expiresAt > now) {
479
+ return cached.value;
480
+ }
481
+ let response;
482
+ try {
483
+ response = await this.#fetch(this.#capabilitiesV2Url, {
484
+ method: "GET",
485
+ headers: {
486
+ authorization: `Bearer ${this.#apiKey}`,
487
+ accept: "application/json",
488
+ },
489
+ signal,
490
+ });
491
+ }
492
+ catch {
493
+ if (signal?.aborted) {
494
+ throw bridgeError("CAPABILITIES_REQUEST_CANCELED", "The capabilities request was canceled.", false);
495
+ }
496
+ throw bridgeError("CUBE_UNAVAILABLE", "Cube could not provide reader capabilities. Retry the same request later.", true);
497
+ }
498
+ if (response.status === 404) {
499
+ try {
500
+ await response.body?.cancel();
501
+ }
502
+ catch {
503
+ throw bridgeError("CUBE_UNAVAILABLE", "Cube could not release the capabilities response. Retry the same request later.", true);
504
+ }
505
+ return null;
506
+ }
507
+ if (!response.ok)
508
+ throw responseError(response.status);
509
+ let capabilities;
510
+ try {
511
+ capabilities = parseReaderCapabilitiesV2(await response.json(), new Date(now));
512
+ }
513
+ catch {
514
+ throw bridgeError("CUBE_PROTOCOL_ERROR", "Cube returned invalid reader capability profiles.", false);
515
+ }
516
+ this.#capabilitiesV2Cache = {
277
517
  expiresAt: capabilities.expires_at.getTime(),
278
518
  value: capabilities,
279
519
  };
@@ -285,23 +525,27 @@ export class CubeGrantClient {
285
525
  // UNSUPPORTED_DETAIL before any grant request is constructed.
286
526
  async #directProfileFor(detail, signal) {
287
527
  const capabilities = await this.getCapabilities(signal);
288
- return selectDirectProfile(capabilities, detail);
528
+ return selectDirectProfile(capabilities, detail, "local");
289
529
  }
290
530
  async createGrant(input, clientRequestId, signal, options = {}) {
291
531
  if (this.#apiKey.length === 0) {
292
532
  throw bridgeError("MISSING_CUE_API_KEY", "Set CUE_API_KEY before using local Omni document parsing.", false);
293
533
  }
294
- // D2-D Task 14: for any non-text grant the exact direct profile is
295
- // obtained and selected BEFORE the request is constructed or journal state
296
- // is written; without one exact compatible profile the request fails
297
- // closed with UNSUPPORTED_DETAIL and no unknown-field request is sent.
298
- const detail = input.detail === "grounded" || input.detail === "layout"
534
+ const requestedDetail = input.detail ?? "text";
535
+ const capabilitiesV2 = await this.getCapabilitiesV2(signal);
536
+ const profileV2 = capabilitiesV2 === null
537
+ ? undefined
538
+ : selectDirectProfileV2(capabilitiesV2, requestedDetail, "local");
539
+ const legacyDetail = input.detail === "grounded" || input.detail === "layout"
299
540
  ? input.detail
300
541
  : undefined;
301
- const profile = detail === undefined
302
- ? undefined
303
- : await this.#directProfileFor(detail, signal);
304
- const body = grantRequestBody(input, profile);
542
+ const profileV1 = capabilitiesV2 === null && legacyDetail !== undefined
543
+ ? await this.#directProfileFor(legacyDetail, signal)
544
+ : undefined;
545
+ const directProfile = profileV2?.profile ?? profileV1?.profile ?? null;
546
+ const body = profileV2 === undefined
547
+ ? grantRequestBody(input, profileV1)
548
+ : v4GrantRequestBody(input, profileV2, requestedDetail, clientRequestId);
305
549
  const serializedBody = JSON.stringify(body);
306
550
  const requestHash = grantRequestHash(body);
307
551
  if (options.journal !== false) {
@@ -329,18 +573,26 @@ export class CubeGrantClient {
329
573
  if (!response.ok) {
330
574
  throw await grantResponseError(response);
331
575
  }
332
- // The v3 response tuple is required before this promise resolves: the
333
- // upload phase can only start after the negotiated representation is
334
- // confirmed by the server.
576
+ // Profiled responses are accepted only for the exact negotiated grant
577
+ // generation. Legacy response meanings remain frozen.
335
578
  let parsed;
336
- if (profile !== undefined) {
579
+ if (profileV2 !== undefined) {
580
+ try {
581
+ parsed = grantResponseV4Schema.parse(await response.json());
582
+ }
583
+ catch {
584
+ throw bridgeError("CUBE_PROTOCOL_ERROR", "Cube returned a parse grant outside the negotiated representation.", false);
585
+ }
586
+ requireV4GrantBinding(parsed, profileV2, requestedDetail, body);
587
+ }
588
+ else if (profileV1 !== undefined) {
337
589
  try {
338
590
  parsed = grantResponseV3Schema.parse(await response.json());
339
591
  }
340
592
  catch {
341
593
  throw bridgeError("CUBE_PROTOCOL_ERROR", "Cube returned a parse grant outside the negotiated representation.", false);
342
594
  }
343
- requireV3GrantTuple(parsed, profile, detail);
595
+ requireV3GrantTuple(parsed, profileV1, legacyDetail);
344
596
  }
345
597
  else {
346
598
  try {
@@ -369,6 +621,8 @@ export class CubeGrantClient {
369
621
  expiresAt: parsed.expires_at,
370
622
  maxBytes: parsed.max_bytes,
371
623
  protocolVersion: parsed.protocol_version,
624
+ directProfile,
625
+ requestedDetail,
372
626
  };
373
627
  }
374
628
  }
package/dist/errors.d.ts CHANGED
@@ -54,3 +54,4 @@ export declare class OmniBridgeError extends Error {
54
54
  constructor(init: OmniBridgeErrorInit);
55
55
  toJSON(): OmniBridgeErrorPayload;
56
56
  }
57
+ export declare function unsupportedDetailError(sourceKind?: OmniSourceKind): OmniBridgeError;
package/dist/errors.js CHANGED
@@ -49,3 +49,18 @@ export class OmniBridgeError extends Error {
49
49
  };
50
50
  }
51
51
  }
52
+ export function unsupportedDetailError(sourceKind) {
53
+ return new OmniBridgeError({
54
+ code: "UNSUPPORTED_DETAIL",
55
+ message: "The requested parsing representation is not supported by this control plane.",
56
+ failureScope: "local_capability",
57
+ ...(sourceKind === undefined ? {} : { sourceKind }),
58
+ userAction: "Use text output only when Markdown without grounding/layout sidecars is acceptable; otherwise do not retry unchanged.",
59
+ operationCreated: false,
60
+ fileUploaded: false,
61
+ parserStarted: false,
62
+ billed: false,
63
+ contentReleased: false,
64
+ retryable: false,
65
+ });
66
+ }
@@ -1,3 +1,4 @@
1
+ import type { DirectProfile, RequestedDirectDetailV2 } from "./capabilities.js";
1
2
  import type { OpenedAllowedFile } from "./path-security.js";
2
3
  import { type ProgressSink } from "./progress.js";
3
4
  export interface ResultRetentionStart {
@@ -22,14 +23,22 @@ export interface IiisOperationInput {
22
23
  readonly operationToken: string;
23
24
  readonly uploadUrl?: string;
24
25
  readonly expiresAt?: string;
26
+ readonly directProfile?: DirectProfile | null;
27
+ readonly expectedDetail?: RequestedDirectDetailV2;
25
28
  readonly openedFile?: OpenedAllowedFile;
26
29
  readonly retention: ResultRetentionSink;
27
30
  readonly progress?: ProgressSink;
28
31
  readonly signal?: AbortSignal;
29
32
  }
33
+ export interface WireBillingFacts {
34
+ readonly credits_charged: number;
35
+ readonly credits_remaining: number;
36
+ }
30
37
  export interface ReleasedResult extends ReleasedMetadata {
38
+ readonly directProfile: DirectProfile | null;
39
+ readonly billing: WireBillingFacts | null;
31
40
  }
32
- export type IiisOperationStatus = "ISSUED" | "CLAIMED" | "UPLOADING" | "PROCESSING" | "SETTLING" | "RELEASED" | "EXPIRED" | "SETTLEMENT_DENIED" | "FAILED" | "CANCELED" | "DELIVERY_EXPIRED" | "DELIVERED";
41
+ export type IiisOperationStatus = "ISSUED" | "CLAIMED" | "UPLOADING" | "PROCESSING" | "SETTLING" | "RELEASED" | "EXPIRED" | "SETTLEMENT_DENIED" | "FAILED" | "UNSUPPORTED" | "CANCELED" | "DELIVERY_EXPIRED" | "DELIVERED";
33
42
  export interface IiisOperationSnapshot {
34
43
  readonly status: IiisOperationStatus;
35
44
  readonly parserStarted: boolean;
@@ -38,6 +47,8 @@ export interface IiisOperationSnapshot {
38
47
  readonly contentReleased: boolean;
39
48
  readonly retryable: boolean;
40
49
  readonly expiresAt: string | null;
50
+ readonly directProfile: DirectProfile | null;
51
+ readonly billing: WireBillingFacts | null;
41
52
  }
42
53
  export interface IiisClientOptions {
43
54
  readonly fetchImpl?: typeof fetch;
@@ -45,6 +56,25 @@ export interface IiisClientOptions {
45
56
  readonly maxPolls?: number;
46
57
  readonly operationBaseUrl?: string;
47
58
  }
59
+ interface ResultIdentity {
60
+ readonly digest: string;
61
+ readonly bytes: number;
62
+ readonly mediaType: string;
63
+ }
64
+ interface OperationStatus {
65
+ readonly protocol_version: string;
66
+ readonly operation_id: string;
67
+ readonly status: IiisOperationStatus;
68
+ readonly parser_started: boolean;
69
+ readonly file_uploaded: boolean;
70
+ readonly billed: boolean;
71
+ readonly content_released: boolean;
72
+ readonly retryable: boolean;
73
+ readonly expires_at: string | null;
74
+ readonly directProfile: DirectProfile | null;
75
+ readonly billing: WireBillingFacts | null;
76
+ readonly result: ResultIdentity | null;
77
+ }
48
78
  export declare class IiisClient {
49
79
  #private;
50
80
  constructor(options?: IiisClientOptions);
@@ -52,6 +82,7 @@ export declare class IiisClient {
52
82
  recoverAndWait(input: IiisOperationInput): Promise<ReleasedResult>;
53
83
  inspectOperation(input: IiisOperationInput): Promise<IiisOperationSnapshot>;
54
84
  cancelOperation(input: IiisOperationInput): Promise<IiisOperationSnapshot>;
55
- downloadResult(input: IiisOperationInput, progress?: ProgressSink): Promise<ReleasedResult>;
85
+ downloadResult(input: IiisOperationInput, progress?: ProgressSink, settledStatus?: OperationStatus): Promise<ReleasedResult>;
56
86
  ack(input: IiisOperationInput): Promise<void>;
57
87
  }
88
+ export {};