@cueai/omni-reader-mcp 1.1.3 → 1.2.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 +137 -85
- package/dist/artifact-store.d.ts +58 -0
- package/dist/artifact-store.js +449 -5
- package/dist/capabilities.d.ts +92 -0
- package/dist/capabilities.js +123 -0
- package/dist/constants.d.ts +34 -1
- package/dist/constants.js +39 -1
- package/dist/cube-client.d.ts +3 -0
- package/dist/cube-client.js +141 -9
- package/dist/cursor.d.ts +12 -0
- package/dist/cursor.js +89 -9
- package/dist/operation-journal.d.ts +19 -5
- package/dist/operation-journal.js +435 -73
- package/dist/operation-manager.d.ts +7 -2
- package/dist/operation-manager.js +215 -16
- package/dist/protocol.d.ts +16 -3
- package/dist/protocol.js +25 -1
- package/dist/remote-client.d.ts +4 -2
- package/dist/remote-client.js +155 -13
- package/dist/result-bundle.d.ts +21 -0
- package/dist/result-bundle.js +320 -0
- package/dist/result-contract.d.ts +398 -4
- package/dist/result-contract.js +169 -11
- package/dist/server.d.ts +17 -0
- package/dist/server.js +45 -1
- package/dist/tools.d.ts +7 -3
- package/dist/tools.js +77 -1
- package/package.json +1 -1
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { OmniBridgeError } from "./errors.js";
|
|
3
|
+
export const READER_CAPABILITIES_PROTOCOL = "omni.reader_capabilities.v1";
|
|
4
|
+
// Closed consumer-side schema for `omni.reader_capabilities.v1` (D2-D item 4).
|
|
5
|
+
// Objects/arrays are closed: profile names and every tuple member are exact
|
|
6
|
+
// literals, `details` is the exact ordered tuple ["grounded", "layout"], and a
|
|
7
|
+
// profile is advertised atomically — never assembled by cross-producting
|
|
8
|
+
// independent lists. Direct-profile literal values mirror the pinned
|
|
9
|
+
// cube-mcp `omni.cube_direct_profiles.v1` accepted set
|
|
10
|
+
// (contracts/cube-mcp/omni-capabilities/v1); the URL profile belongs to the
|
|
11
|
+
// URL-operation control plane per the same contract README.
|
|
12
|
+
const directProfileSchema = z
|
|
13
|
+
.object({
|
|
14
|
+
profile: z.literal("omni.direct_grounding.v1"),
|
|
15
|
+
grant_protocol: z.literal("omni.parse_grant.v3"),
|
|
16
|
+
stream_protocol: z.literal("omni.granted_parse_stream.v2"),
|
|
17
|
+
operation_protocol: z.literal("omni.direct_operation.v2"),
|
|
18
|
+
settlement_protocol: z.literal("omni.grant_settlement.v4"),
|
|
19
|
+
release_protocol: z.literal("omni.release_decision.v2"),
|
|
20
|
+
settlement_journal_protocol: z.literal("omni.direct_settlement_journal.v2"),
|
|
21
|
+
usage_protocol: z.literal("omni_parse_usage.v2"),
|
|
22
|
+
billing_protocol: z.literal("omni_billing.v2"),
|
|
23
|
+
bridge_protocol: z.literal("omni.local_bridge_tools.v3"),
|
|
24
|
+
bundle_protocol: z.literal("omni.result_bundle.v1"),
|
|
25
|
+
grounding_schema: z.literal("omni.grounding.v1"),
|
|
26
|
+
details: z.tuple([z.literal("grounded"), z.literal("layout")]),
|
|
27
|
+
max_result_bytes: z.literal(67108864),
|
|
28
|
+
})
|
|
29
|
+
.strict();
|
|
30
|
+
const urlProfileSchema = z
|
|
31
|
+
.object({
|
|
32
|
+
profile: z.literal("omni.url_grounding.v1"),
|
|
33
|
+
operation_protocol: z.literal("omni.url_operation.v3"),
|
|
34
|
+
usage_protocol: z.literal("omni_parse_usage.v2"),
|
|
35
|
+
billing_protocol: z.literal("omni_billing.v2"),
|
|
36
|
+
bridge_protocol: z.literal("omni.local_bridge_tools.v3"),
|
|
37
|
+
bundle_protocol: z.literal("omni.result_bundle.v1"),
|
|
38
|
+
grounding_schema: z.literal("omni.grounding.v1"),
|
|
39
|
+
details: z.tuple([z.literal("grounded"), z.literal("layout")]),
|
|
40
|
+
max_result_bytes: z.literal(16777216),
|
|
41
|
+
})
|
|
42
|
+
.strict();
|
|
43
|
+
const readerCapabilitiesSchema = z
|
|
44
|
+
.object({
|
|
45
|
+
protocol_version: z.literal(READER_CAPABILITIES_PROTOCOL),
|
|
46
|
+
expires_at: z.string().datetime({ offset: true }),
|
|
47
|
+
direct_profiles: z.array(directProfileSchema),
|
|
48
|
+
url_profiles: z.array(urlProfileSchema),
|
|
49
|
+
})
|
|
50
|
+
.strict();
|
|
51
|
+
function invalidCapabilities() {
|
|
52
|
+
return new OmniBridgeError({
|
|
53
|
+
code: "INVALID_READER_CAPABILITIES",
|
|
54
|
+
message: "The reader capability advertisement is invalid or expired.",
|
|
55
|
+
failureScope: "service",
|
|
56
|
+
operationCreated: false,
|
|
57
|
+
fileUploaded: false,
|
|
58
|
+
parserStarted: false,
|
|
59
|
+
billed: false,
|
|
60
|
+
contentReleased: false,
|
|
61
|
+
retryable: false,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
function unsupportedDetail() {
|
|
65
|
+
return new OmniBridgeError({
|
|
66
|
+
code: "UNSUPPORTED_DETAIL",
|
|
67
|
+
message: "This Omni service does not support the requested output detail.",
|
|
68
|
+
failureScope: "service",
|
|
69
|
+
userAction: "Use plain Markdown output for this source.",
|
|
70
|
+
operationCreated: false,
|
|
71
|
+
fileUploaded: false,
|
|
72
|
+
parserStarted: false,
|
|
73
|
+
billed: false,
|
|
74
|
+
contentReleased: false,
|
|
75
|
+
retryable: false,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
export function parseReaderCapabilities(value, now) {
|
|
79
|
+
let parsed;
|
|
80
|
+
try {
|
|
81
|
+
parsed = readerCapabilitiesSchema.parse(value);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
throw invalidCapabilities();
|
|
85
|
+
}
|
|
86
|
+
// A capability is only cachable up to its exact parsed expiry; an expired or
|
|
87
|
+
// unparseable `expires_at` advertisement is invalid at negotiation time.
|
|
88
|
+
const expiresAt = new Date(parsed.expires_at);
|
|
89
|
+
if (Number.isNaN(expiresAt.getTime()) ||
|
|
90
|
+
expiresAt.getTime() <= now.getTime()) {
|
|
91
|
+
throw invalidCapabilities();
|
|
92
|
+
}
|
|
93
|
+
// Profiles are advertised atomically; duplicate profile names are invalid.
|
|
94
|
+
const seen = new Set();
|
|
95
|
+
for (const profile of parsed.direct_profiles) {
|
|
96
|
+
if (seen.has(profile.profile))
|
|
97
|
+
throw invalidCapabilities();
|
|
98
|
+
seen.add(profile.profile);
|
|
99
|
+
}
|
|
100
|
+
for (const profile of parsed.url_profiles) {
|
|
101
|
+
if (seen.has(profile.profile))
|
|
102
|
+
throw invalidCapabilities();
|
|
103
|
+
seen.add(profile.profile);
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
protocol_version: parsed.protocol_version,
|
|
107
|
+
expires_at: expiresAt,
|
|
108
|
+
direct_profiles: parsed.direct_profiles,
|
|
109
|
+
url_profiles: parsed.url_profiles,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
export function selectDirectProfile(value, detail) {
|
|
113
|
+
const profile = value.direct_profiles.find((candidate) => candidate.details.includes(detail));
|
|
114
|
+
if (profile === undefined)
|
|
115
|
+
throw unsupportedDetail();
|
|
116
|
+
return profile;
|
|
117
|
+
}
|
|
118
|
+
export function selectUrlProfile(value, detail) {
|
|
119
|
+
const profile = value.url_profiles.find((candidate) => candidate.details.includes(detail));
|
|
120
|
+
if (profile === undefined)
|
|
121
|
+
throw unsupportedDetail();
|
|
122
|
+
return profile;
|
|
123
|
+
}
|
package/dist/constants.d.ts
CHANGED
|
@@ -6,9 +6,42 @@ export declare const CUBE_GRANT_PROTOCOL_VERSION = "omni.parse_grant.v1";
|
|
|
6
6
|
export declare const GRANTED_STREAM_PROTOCOL_VERSION = "omni.granted_parse_stream.v1";
|
|
7
7
|
export declare const DEFAULT_CUBE_BASE_URL = "https://mcp.cuecue.cn";
|
|
8
8
|
export declare const DEFAULT_IIIS_GRANTED_BASE_URL = "https://cubefile.ai.iiis.co:9443/omni/granted/";
|
|
9
|
-
export declare const BRIDGE_RELEASE_VERSION = "1.
|
|
9
|
+
export declare const BRIDGE_RELEASE_VERSION = "1.2.0";
|
|
10
10
|
export declare const REMOTE_OMNI_MCP_URL = "https://mcp.cuecue.cn/api/omni-reader/mcp/";
|
|
11
11
|
export declare const FOREGROUND_BUDGET_MS = 15000;
|
|
12
12
|
export declare const STATUS_LONG_POLL_MAX_MS = 20000;
|
|
13
13
|
export declare const STATUS_POLL_AFTER_SECONDS = 5;
|
|
14
14
|
export declare const DELIVERY_TTL_SECONDS = 600;
|
|
15
|
+
export declare const CUBE_CAPABILITIES_PATH = "/api/omni-reader/capabilities/v1";
|
|
16
|
+
export declare const REMOTE_CAPABILITIES_CUSTOM_FIELD = "cue.omni-reader";
|
|
17
|
+
export declare const CAPABILITIES_PROTOCOL_VERSION = "omni.reader_capabilities.v1";
|
|
18
|
+
export declare const CAPABILITIES_ENABLED_ENV = "OMNI_CAPABILITIES_ENABLED";
|
|
19
|
+
export declare const CAPABILITIES_TTL_SECONDS_ENV = "OMNI_CAPABILITIES_TTL_SECONDS";
|
|
20
|
+
export declare const CAPABILITIES_DEFAULT_TTL_SECONDS = 300;
|
|
21
|
+
export declare const DIRECT_GROUNDING_PROFILE: {
|
|
22
|
+
readonly profile: "omni.direct_grounding.v1";
|
|
23
|
+
readonly grant_protocol: "omni.parse_grant.v3";
|
|
24
|
+
readonly stream_protocol: "omni.granted_parse_stream.v2";
|
|
25
|
+
readonly operation_protocol: "omni.direct_operation.v2";
|
|
26
|
+
readonly settlement_protocol: "omni.grant_settlement.v4";
|
|
27
|
+
readonly release_protocol: "omni.release_decision.v2";
|
|
28
|
+
readonly settlement_journal_protocol: "omni.direct_settlement_journal.v2";
|
|
29
|
+
readonly usage_protocol: "omni_parse_usage.v2";
|
|
30
|
+
readonly billing_protocol: "omni_billing.v2";
|
|
31
|
+
readonly bridge_protocol: "omni.local_bridge_tools.v3";
|
|
32
|
+
readonly bundle_protocol: "omni.result_bundle.v1";
|
|
33
|
+
readonly grounding_schema: "omni.grounding.v1";
|
|
34
|
+
readonly details: readonly ["grounded", "layout"];
|
|
35
|
+
readonly max_result_bytes: 67108864;
|
|
36
|
+
};
|
|
37
|
+
export declare const URL_GROUNDING_PROFILE: {
|
|
38
|
+
readonly profile: "omni.url_grounding.v1";
|
|
39
|
+
readonly operation_protocol: "omni.url_operation.v3";
|
|
40
|
+
readonly usage_protocol: "omni_parse_usage.v2";
|
|
41
|
+
readonly billing_protocol: "omni_billing.v2";
|
|
42
|
+
readonly bridge_protocol: "omni.local_bridge_tools.v3";
|
|
43
|
+
readonly bundle_protocol: "omni.result_bundle.v1";
|
|
44
|
+
readonly grounding_schema: "omni.grounding.v1";
|
|
45
|
+
readonly details: readonly ["grounded", "layout"];
|
|
46
|
+
readonly max_result_bytes: 16777216;
|
|
47
|
+
};
|
package/dist/constants.js
CHANGED
|
@@ -6,9 +6,47 @@ export const CUBE_GRANT_PROTOCOL_VERSION = "omni.parse_grant.v1";
|
|
|
6
6
|
export const GRANTED_STREAM_PROTOCOL_VERSION = "omni.granted_parse_stream.v1";
|
|
7
7
|
export const DEFAULT_CUBE_BASE_URL = "https://mcp.cuecue.cn";
|
|
8
8
|
export const DEFAULT_IIIS_GRANTED_BASE_URL = "https://cubefile.ai.iiis.co:9443/omni/granted/";
|
|
9
|
-
export const BRIDGE_RELEASE_VERSION = "1.
|
|
9
|
+
export const BRIDGE_RELEASE_VERSION = "1.2.0";
|
|
10
10
|
export const REMOTE_OMNI_MCP_URL = "https://mcp.cuecue.cn/api/omni-reader/mcp/";
|
|
11
11
|
export const FOREGROUND_BUDGET_MS = 15_000;
|
|
12
12
|
export const STATUS_LONG_POLL_MAX_MS = 20_000;
|
|
13
13
|
export const STATUS_POLL_AFTER_SECONDS = 5;
|
|
14
14
|
export const DELIVERY_TTL_SECONDS = 600;
|
|
15
|
+
export const CUBE_CAPABILITIES_PATH = "/api/omni-reader/capabilities/v1";
|
|
16
|
+
export const REMOTE_CAPABILITIES_CUSTOM_FIELD = "cue.omni-reader";
|
|
17
|
+
// ── omni.reader_capabilities.v1 advertisement (D2-D item 4) ────────────────────────────────
|
|
18
|
+
// The direct profile's 14 exact literal fields mirror the pinned cube-mcp accepted set
|
|
19
|
+
// (contracts/cube-mcp/omni-capabilities/v1); the URL profile's 9 fields belong to the
|
|
20
|
+
// URL-operation control plane. Profiles are advertised atomically — never assembled by
|
|
21
|
+
// cross-producting independent lists.
|
|
22
|
+
export const CAPABILITIES_PROTOCOL_VERSION = "omni.reader_capabilities.v1";
|
|
23
|
+
export const CAPABILITIES_ENABLED_ENV = "OMNI_CAPABILITIES_ENABLED";
|
|
24
|
+
export const CAPABILITIES_TTL_SECONDS_ENV = "OMNI_CAPABILITIES_TTL_SECONDS";
|
|
25
|
+
export const CAPABILITIES_DEFAULT_TTL_SECONDS = 300;
|
|
26
|
+
export const DIRECT_GROUNDING_PROFILE = {
|
|
27
|
+
profile: "omni.direct_grounding.v1",
|
|
28
|
+
grant_protocol: "omni.parse_grant.v3",
|
|
29
|
+
stream_protocol: "omni.granted_parse_stream.v2",
|
|
30
|
+
operation_protocol: "omni.direct_operation.v2",
|
|
31
|
+
settlement_protocol: "omni.grant_settlement.v4",
|
|
32
|
+
release_protocol: "omni.release_decision.v2",
|
|
33
|
+
settlement_journal_protocol: "omni.direct_settlement_journal.v2",
|
|
34
|
+
usage_protocol: "omni_parse_usage.v2",
|
|
35
|
+
billing_protocol: "omni_billing.v2",
|
|
36
|
+
bridge_protocol: "omni.local_bridge_tools.v3",
|
|
37
|
+
bundle_protocol: "omni.result_bundle.v1",
|
|
38
|
+
grounding_schema: "omni.grounding.v1",
|
|
39
|
+
details: ["grounded", "layout"],
|
|
40
|
+
max_result_bytes: 67108864,
|
|
41
|
+
};
|
|
42
|
+
export const URL_GROUNDING_PROFILE = {
|
|
43
|
+
profile: "omni.url_grounding.v1",
|
|
44
|
+
operation_protocol: "omni.url_operation.v3",
|
|
45
|
+
usage_protocol: "omni_parse_usage.v2",
|
|
46
|
+
billing_protocol: "omni_billing.v2",
|
|
47
|
+
bridge_protocol: "omni.local_bridge_tools.v3",
|
|
48
|
+
bundle_protocol: "omni.result_bundle.v1",
|
|
49
|
+
grounding_schema: "omni.grounding.v1",
|
|
50
|
+
details: ["grounded", "layout"],
|
|
51
|
+
max_result_bytes: 16777216,
|
|
52
|
+
};
|
package/dist/cube-client.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BRIDGE_RELEASE_VERSION } from "./constants.js";
|
|
2
|
+
import { type ReaderCapabilitiesV1 } from "./capabilities.js";
|
|
2
3
|
import { OperationJournal } from "./operation-journal.js";
|
|
3
4
|
declare const BRIDGE_PACKAGE = "@cueai/omni-reader-mcp";
|
|
4
5
|
export interface GrantRequestInput {
|
|
@@ -7,6 +8,7 @@ export interface GrantRequestInput {
|
|
|
7
8
|
readonly fileExtension: string;
|
|
8
9
|
readonly noStore: boolean;
|
|
9
10
|
readonly output: "markdown";
|
|
11
|
+
readonly detail?: "grounded" | "layout";
|
|
10
12
|
}
|
|
11
13
|
export interface GrantedOperation {
|
|
12
14
|
readonly clientRequestId: string;
|
|
@@ -43,6 +45,7 @@ export declare function createClientRequestId(): string;
|
|
|
43
45
|
export declare class CubeGrantClient {
|
|
44
46
|
#private;
|
|
45
47
|
constructor(options: CubeGrantClientOptions);
|
|
48
|
+
getCapabilities(signal?: AbortSignal): Promise<ReaderCapabilitiesV1>;
|
|
46
49
|
createGrant(input: GrantRequestInput, clientRequestId: string, signal?: AbortSignal, options?: {
|
|
47
50
|
journal?: boolean;
|
|
48
51
|
}): Promise<GrantedOperation>;
|
package/dist/cube-client.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import { BRIDGE_RELEASE_VERSION, CUBE_GRANT_PROTOCOL_VERSION, DEFAULT_CUBE_BASE_URL, MAX_FILE_BYTES, } from "./constants.js";
|
|
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";
|
|
4
5
|
import { OmniBridgeError } from "./errors.js";
|
|
5
6
|
const GRANT_PATH = "/api/omni-reader/direct-upload/v1/parse-grants";
|
|
6
7
|
const BRIDGE_PACKAGE = "@cueai/omni-reader-mcp";
|
|
@@ -19,6 +20,28 @@ const grantResponseSchema = z
|
|
|
19
20
|
protocol_version: z.literal(CUBE_GRANT_PROTOCOL_VERSION),
|
|
20
21
|
})
|
|
21
22
|
.strict();
|
|
23
|
+
// Closed v3 grant response: the exact tuple of the selected direct profile is
|
|
24
|
+
// required before the upload phase may start. The tuple is re-checked against
|
|
25
|
+
// the profile after schema parsing so a drifting server cannot slip through.
|
|
26
|
+
const grantResponseV3Schema = z
|
|
27
|
+
.object({
|
|
28
|
+
grant_id: z.string().min(1),
|
|
29
|
+
operation_id: z.string().min(1),
|
|
30
|
+
parse_grant: z.string().min(1),
|
|
31
|
+
operation_token: z.string().min(1),
|
|
32
|
+
upload_url: z
|
|
33
|
+
.string()
|
|
34
|
+
.url()
|
|
35
|
+
.refine((value) => new URL(value).protocol === "https:"),
|
|
36
|
+
expires_at: z.string().datetime({ offset: true }),
|
|
37
|
+
max_bytes: z.literal(MAX_FILE_BYTES),
|
|
38
|
+
protocol_version: z.literal("omni.parse_grant.v3"),
|
|
39
|
+
stream_protocol_version: z.literal("omni.granted_parse_stream.v2"),
|
|
40
|
+
bundle_protocol_version: z.literal("omni.result_bundle.v1"),
|
|
41
|
+
detail: z.enum(["grounded", "layout"]),
|
|
42
|
+
approved_result_max_bytes: z.literal(67108864),
|
|
43
|
+
})
|
|
44
|
+
.strict();
|
|
22
45
|
function bridgeError(code, message, retryable) {
|
|
23
46
|
return new OmniBridgeError({
|
|
24
47
|
code,
|
|
@@ -31,17 +54,18 @@ function bridgeError(code, message, retryable) {
|
|
|
31
54
|
retryable,
|
|
32
55
|
});
|
|
33
56
|
}
|
|
34
|
-
function grantRequestBody(input) {
|
|
57
|
+
function grantRequestBody(input, profile) {
|
|
35
58
|
if (!Number.isSafeInteger(input.contentLength) ||
|
|
36
59
|
input.contentLength < 0 ||
|
|
37
60
|
input.contentLength > MAX_FILE_BYTES ||
|
|
38
61
|
!/^[^\s;/]+\/[^\s;]+$/u.test(input.contentType) ||
|
|
39
62
|
!/^\.[a-z0-9]{1,10}$/u.test(input.fileExtension) ||
|
|
40
63
|
typeof input.noStore !== "boolean" ||
|
|
41
|
-
input.output !== "markdown"
|
|
64
|
+
input.output !== "markdown" ||
|
|
65
|
+
(input.detail !== undefined && input.detail !== "grounded" && input.detail !== "layout")) {
|
|
42
66
|
throw bridgeError("INVALID_GRANT_REQUEST", "The local file metadata is invalid for a parse grant.", false);
|
|
43
67
|
}
|
|
44
|
-
|
|
68
|
+
const base = {
|
|
45
69
|
content_length: input.contentLength,
|
|
46
70
|
content_type: input.contentType,
|
|
47
71
|
file_extension: input.fileExtension,
|
|
@@ -52,6 +76,32 @@ function grantRequestBody(input) {
|
|
|
52
76
|
version: BRIDGE_RELEASE_VERSION,
|
|
53
77
|
},
|
|
54
78
|
};
|
|
79
|
+
if (profile === undefined)
|
|
80
|
+
return base;
|
|
81
|
+
return {
|
|
82
|
+
...base,
|
|
83
|
+
protocol_version: profile.grant_protocol,
|
|
84
|
+
stream_protocol_version: profile.stream_protocol,
|
|
85
|
+
operation_protocol_version: profile.operation_protocol,
|
|
86
|
+
settlement_protocol_version: profile.settlement_protocol,
|
|
87
|
+
release_protocol_version: profile.release_protocol,
|
|
88
|
+
settlement_journal_protocol_version: profile.settlement_journal_protocol,
|
|
89
|
+
usage_schema_version: profile.usage_protocol,
|
|
90
|
+
billing_contract_version: profile.billing_protocol,
|
|
91
|
+
grounding_schema_version: profile.grounding_schema,
|
|
92
|
+
bundle_protocol_version: profile.bundle_protocol,
|
|
93
|
+
detail: input.detail,
|
|
94
|
+
approved_result_max_bytes: profile.max_result_bytes,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function requireV3GrantTuple(parsed, profile, detail) {
|
|
98
|
+
if (parsed.protocol_version !== profile.grant_protocol ||
|
|
99
|
+
parsed.stream_protocol_version !== profile.stream_protocol ||
|
|
100
|
+
parsed.bundle_protocol_version !== profile.bundle_protocol ||
|
|
101
|
+
parsed.detail !== detail ||
|
|
102
|
+
parsed.approved_result_max_bytes !== profile.max_result_bytes) {
|
|
103
|
+
throw bridgeError("CUBE_PROTOCOL_ERROR", "Cube returned a parse grant outside the negotiated representation.", false);
|
|
104
|
+
}
|
|
55
105
|
}
|
|
56
106
|
export function grantRequestHash(body) {
|
|
57
107
|
return `sha256:${createHash("sha256")
|
|
@@ -86,7 +136,9 @@ export class CubeGrantClient {
|
|
|
86
136
|
#journal;
|
|
87
137
|
#apiKey;
|
|
88
138
|
#grantUrl;
|
|
139
|
+
#capabilitiesUrl;
|
|
89
140
|
#fetch;
|
|
141
|
+
#capabilitiesCache;
|
|
90
142
|
constructor(options) {
|
|
91
143
|
this.#journal = options.journal;
|
|
92
144
|
this.#apiKey =
|
|
@@ -101,13 +153,79 @@ export class CubeGrantClient {
|
|
|
101
153
|
throw bridgeError("INSECURE_CUBE_BASE_URL", "The Cube control endpoint must use HTTPS without embedded credentials.", false);
|
|
102
154
|
}
|
|
103
155
|
this.#grantUrl = new URL(GRANT_PATH, baseUrl).toString();
|
|
156
|
+
this.#capabilitiesUrl = new URL(CUBE_CAPABILITIES_PATH, baseUrl).toString();
|
|
104
157
|
this.#fetch = options.fetchImpl ?? fetch;
|
|
105
158
|
}
|
|
159
|
+
// Preflight for any non-text grant request (D2-D item 4): authenticated
|
|
160
|
+
// HTTPS GET of the capabilities endpoint, body validated through the closed
|
|
161
|
+
// `omni.reader_capabilities.v1` schema, and cached only until the exact
|
|
162
|
+
// parsed `expires_at`. The cache is keyed by this client's fixed
|
|
163
|
+
// capabilities endpoint; a different endpoint means a different client.
|
|
164
|
+
async getCapabilities(signal) {
|
|
165
|
+
if (this.#apiKey.length === 0) {
|
|
166
|
+
throw bridgeError("MISSING_CUE_API_KEY", "Set CUE_API_KEY before using local Omni document parsing.", false);
|
|
167
|
+
}
|
|
168
|
+
const now = Date.now();
|
|
169
|
+
const cached = this.#capabilitiesCache;
|
|
170
|
+
if (cached !== undefined && cached.expiresAt > now) {
|
|
171
|
+
return cached.value;
|
|
172
|
+
}
|
|
173
|
+
let response;
|
|
174
|
+
try {
|
|
175
|
+
response = await this.#fetch(this.#capabilitiesUrl, {
|
|
176
|
+
method: "GET",
|
|
177
|
+
headers: {
|
|
178
|
+
authorization: `Bearer ${this.#apiKey}`,
|
|
179
|
+
accept: "application/json",
|
|
180
|
+
},
|
|
181
|
+
signal,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
if (signal?.aborted) {
|
|
186
|
+
throw bridgeError("CAPABILITIES_REQUEST_CANCELED", "The capabilities request was canceled.", false);
|
|
187
|
+
}
|
|
188
|
+
throw bridgeError("CUBE_UNAVAILABLE", "Cube could not provide reader capabilities. Retry the same request later.", true);
|
|
189
|
+
}
|
|
190
|
+
if (!response.ok) {
|
|
191
|
+
throw responseError(response.status);
|
|
192
|
+
}
|
|
193
|
+
let capabilities;
|
|
194
|
+
try {
|
|
195
|
+
capabilities = parseReaderCapabilities(await response.json(), new Date(now));
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
throw bridgeError("CUBE_PROTOCOL_ERROR", "Cube returned invalid reader capability profiles.", false);
|
|
199
|
+
}
|
|
200
|
+
this.#capabilitiesCache = {
|
|
201
|
+
expiresAt: capabilities.expires_at.getTime(),
|
|
202
|
+
value: capabilities,
|
|
203
|
+
};
|
|
204
|
+
return capabilities;
|
|
205
|
+
}
|
|
206
|
+
// The exact direct profile for a non-text detail, obtained through the
|
|
207
|
+
// authenticated capabilities preflight and selected atomically from the
|
|
208
|
+
// closed advertisement; missing or mismatched profiles throw
|
|
209
|
+
// UNSUPPORTED_DETAIL before any grant request is constructed.
|
|
210
|
+
async #directProfileFor(detail, signal) {
|
|
211
|
+
const capabilities = await this.getCapabilities(signal);
|
|
212
|
+
return selectDirectProfile(capabilities, detail);
|
|
213
|
+
}
|
|
106
214
|
async createGrant(input, clientRequestId, signal, options = {}) {
|
|
107
215
|
if (this.#apiKey.length === 0) {
|
|
108
216
|
throw bridgeError("MISSING_CUE_API_KEY", "Set CUE_API_KEY before using local Omni document parsing.", false);
|
|
109
217
|
}
|
|
110
|
-
|
|
218
|
+
// D2-D Task 14: for any non-text grant the exact direct profile is
|
|
219
|
+
// obtained and selected BEFORE the request is constructed or journal state
|
|
220
|
+
// is written; without one exact compatible profile the request fails
|
|
221
|
+
// closed with UNSUPPORTED_DETAIL and no unknown-field request is sent.
|
|
222
|
+
const detail = input.detail === "grounded" || input.detail === "layout"
|
|
223
|
+
? input.detail
|
|
224
|
+
: undefined;
|
|
225
|
+
const profile = detail === undefined
|
|
226
|
+
? undefined
|
|
227
|
+
: await this.#directProfileFor(detail, signal);
|
|
228
|
+
const body = grantRequestBody(input, profile);
|
|
111
229
|
const serializedBody = JSON.stringify(body);
|
|
112
230
|
const requestHash = grantRequestHash(body);
|
|
113
231
|
if (options.journal !== false) {
|
|
@@ -135,12 +253,26 @@ export class CubeGrantClient {
|
|
|
135
253
|
if (!response.ok) {
|
|
136
254
|
throw responseError(response.status);
|
|
137
255
|
}
|
|
256
|
+
// The v3 response tuple is required before this promise resolves: the
|
|
257
|
+
// upload phase can only start after the negotiated representation is
|
|
258
|
+
// confirmed by the server.
|
|
138
259
|
let parsed;
|
|
139
|
-
|
|
140
|
-
|
|
260
|
+
if (profile !== undefined) {
|
|
261
|
+
try {
|
|
262
|
+
parsed = grantResponseV3Schema.parse(await response.json());
|
|
263
|
+
}
|
|
264
|
+
catch {
|
|
265
|
+
throw bridgeError("CUBE_PROTOCOL_ERROR", "Cube returned a parse grant outside the negotiated representation.", false);
|
|
266
|
+
}
|
|
267
|
+
requireV3GrantTuple(parsed, profile, detail);
|
|
141
268
|
}
|
|
142
|
-
|
|
143
|
-
|
|
269
|
+
else {
|
|
270
|
+
try {
|
|
271
|
+
parsed = grantResponseSchema.parse(await response.json());
|
|
272
|
+
}
|
|
273
|
+
catch {
|
|
274
|
+
throw bridgeError("CUBE_PROTOCOL_ERROR", "Cube returned an invalid parse grant response.", false);
|
|
275
|
+
}
|
|
144
276
|
}
|
|
145
277
|
if (options.journal !== false) {
|
|
146
278
|
await this.#journal.markGrantIssued(clientRequestId, {
|
package/dist/cursor.d.ts
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
|
+
import { GROUNDING_SCHEMA_VERSION, RESULT_BUNDLE_PROTOCOL_VERSION } from "./protocol.js";
|
|
1
2
|
export interface CursorPayload {
|
|
2
3
|
readonly resultId: string;
|
|
3
4
|
readonly offset: number;
|
|
4
5
|
readonly expiresAt: string;
|
|
5
6
|
}
|
|
7
|
+
export interface BundleCursorPayload {
|
|
8
|
+
readonly resultId: string;
|
|
9
|
+
readonly part: "content" | "grounding";
|
|
10
|
+
readonly detail: "grounded" | "layout";
|
|
11
|
+
readonly groundingSchemaVersion: typeof GROUNDING_SCHEMA_VERSION;
|
|
12
|
+
readonly bundleProtocolVersion: typeof RESULT_BUNDLE_PROTOCOL_VERSION;
|
|
13
|
+
readonly offset: number;
|
|
14
|
+
readonly expiresAt: string;
|
|
15
|
+
}
|
|
6
16
|
export interface CursorCodecOptions {
|
|
7
17
|
readonly now?: () => Date;
|
|
8
18
|
}
|
|
@@ -11,4 +21,6 @@ export declare class CursorCodec {
|
|
|
11
21
|
constructor(key: Uint8Array, options?: CursorCodecOptions);
|
|
12
22
|
encode(payload: CursorPayload): string;
|
|
13
23
|
decode(cursor: string): CursorPayload;
|
|
24
|
+
encodeBundle(payload: BundleCursorPayload): string;
|
|
25
|
+
decodeBundle(cursor: string): BundleCursorPayload;
|
|
14
26
|
}
|
package/dist/cursor.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
2
2
|
import { OmniBridgeError } from "./errors.js";
|
|
3
|
+
import { BUNDLE_CURSOR_VERSION, GROUNDING_SCHEMA_VERSION, RESULT_BUNDLE_PROTOCOL_VERSION, } from "./protocol.js";
|
|
3
4
|
const CURSOR_VERSION = 1;
|
|
4
5
|
const BASE64URL_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
5
6
|
const RESULT_ID_PATTERN = /^result_[A-Za-z0-9_-]{16,64}$/;
|
|
@@ -34,6 +35,8 @@ export class CursorCodec {
|
|
|
34
35
|
this.#key = Buffer.from(key);
|
|
35
36
|
this.#now = options.now ?? (() => new Date());
|
|
36
37
|
}
|
|
38
|
+
// v1 cursors remain the legacy text artifact cursor; a v2 payload is
|
|
39
|
+
// rejected here so a bundle cursor can never read a text result.
|
|
37
40
|
encode(payload) {
|
|
38
41
|
this.#validatePayload(payload);
|
|
39
42
|
const encoded = Buffer.from(JSON.stringify({
|
|
@@ -46,19 +49,59 @@ export class CursorCodec {
|
|
|
46
49
|
return `cursor_${encoded}.${signature}`;
|
|
47
50
|
}
|
|
48
51
|
decode(cursor) {
|
|
49
|
-
|
|
52
|
+
const encoded = this.#unseal(cursor);
|
|
53
|
+
let value;
|
|
54
|
+
try {
|
|
55
|
+
value = JSON.parse(decodeBase64Url(encoded).toString("utf8"));
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
if (error instanceof OmniBridgeError)
|
|
59
|
+
throw error;
|
|
50
60
|
throw cursorError("INVALID_RESULT_CURSOR", "The result cursor is invalid.");
|
|
51
61
|
}
|
|
52
|
-
|
|
53
|
-
if (components.length !== 2) {
|
|
62
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
54
63
|
throw cursorError("INVALID_RESULT_CURSOR", "The result cursor is invalid.");
|
|
55
64
|
}
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
65
|
+
const record = value;
|
|
66
|
+
if (Object.keys(record).sort().join(",") !== "expiresAt,offset,resultId,version"
|
|
67
|
+
|| record.version !== CURSOR_VERSION
|
|
68
|
+
|| typeof record.resultId !== "string"
|
|
69
|
+
|| !RESULT_ID_PATTERN.test(record.resultId)
|
|
70
|
+
|| typeof record.offset !== "number"
|
|
71
|
+
|| !Number.isSafeInteger(record.offset)
|
|
72
|
+
|| record.offset < 0
|
|
73
|
+
|| typeof record.expiresAt !== "string"
|
|
74
|
+
|| !Number.isFinite(Date.parse(record.expiresAt))) {
|
|
60
75
|
throw cursorError("INVALID_RESULT_CURSOR", "The result cursor is invalid.");
|
|
61
76
|
}
|
|
77
|
+
if (Date.parse(record.expiresAt) <= this.#now().getTime()) {
|
|
78
|
+
throw cursorError("RESULT_CURSOR_EXPIRED", "The result cursor has expired.");
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
resultId: record.resultId,
|
|
82
|
+
offset: record.offset,
|
|
83
|
+
expiresAt: record.expiresAt,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
// v2 cursors carry the exact bound field set; a v1 payload is rejected so
|
|
87
|
+
// a legacy text cursor can never select a bundle part.
|
|
88
|
+
encodeBundle(payload) {
|
|
89
|
+
this.#validateBundlePayload(payload);
|
|
90
|
+
const encoded = Buffer.from(JSON.stringify({
|
|
91
|
+
version: BUNDLE_CURSOR_VERSION,
|
|
92
|
+
resultId: payload.resultId,
|
|
93
|
+
part: payload.part,
|
|
94
|
+
detail: payload.detail,
|
|
95
|
+
groundingSchemaVersion: payload.groundingSchemaVersion,
|
|
96
|
+
bundleProtocolVersion: payload.bundleProtocolVersion,
|
|
97
|
+
offset: payload.offset,
|
|
98
|
+
expiresAt: payload.expiresAt,
|
|
99
|
+
}), "utf8").toString("base64url");
|
|
100
|
+
const signature = createHmac("sha256", this.#key).update(encoded).digest("base64url");
|
|
101
|
+
return `cursor_${encoded}.${signature}`;
|
|
102
|
+
}
|
|
103
|
+
decodeBundle(cursor) {
|
|
104
|
+
const encoded = this.#unseal(cursor);
|
|
62
105
|
let value;
|
|
63
106
|
try {
|
|
64
107
|
value = JSON.parse(decodeBase64Url(encoded).toString("utf8"));
|
|
@@ -72,10 +115,15 @@ export class CursorCodec {
|
|
|
72
115
|
throw cursorError("INVALID_RESULT_CURSOR", "The result cursor is invalid.");
|
|
73
116
|
}
|
|
74
117
|
const record = value;
|
|
75
|
-
if (Object.keys(record).sort().join(",") !==
|
|
76
|
-
|
|
118
|
+
if (Object.keys(record).sort().join(",") !==
|
|
119
|
+
"bundleProtocolVersion,detail,expiresAt,groundingSchemaVersion,offset,part,resultId,version"
|
|
120
|
+
|| record.version !== BUNDLE_CURSOR_VERSION
|
|
77
121
|
|| typeof record.resultId !== "string"
|
|
78
122
|
|| !RESULT_ID_PATTERN.test(record.resultId)
|
|
123
|
+
|| (record.part !== "content" && record.part !== "grounding")
|
|
124
|
+
|| (record.detail !== "grounded" && record.detail !== "layout")
|
|
125
|
+
|| record.groundingSchemaVersion !== GROUNDING_SCHEMA_VERSION
|
|
126
|
+
|| record.bundleProtocolVersion !== RESULT_BUNDLE_PROTOCOL_VERSION
|
|
79
127
|
|| typeof record.offset !== "number"
|
|
80
128
|
|| !Number.isSafeInteger(record.offset)
|
|
81
129
|
|| record.offset < 0
|
|
@@ -88,10 +136,30 @@ export class CursorCodec {
|
|
|
88
136
|
}
|
|
89
137
|
return {
|
|
90
138
|
resultId: record.resultId,
|
|
139
|
+
part: record.part,
|
|
140
|
+
detail: record.detail,
|
|
141
|
+
groundingSchemaVersion: record.groundingSchemaVersion,
|
|
142
|
+
bundleProtocolVersion: record.bundleProtocolVersion,
|
|
91
143
|
offset: record.offset,
|
|
92
144
|
expiresAt: record.expiresAt,
|
|
93
145
|
};
|
|
94
146
|
}
|
|
147
|
+
#unseal(cursor) {
|
|
148
|
+
if (typeof cursor !== "string" || cursor.length > 2048 || !cursor.startsWith("cursor_")) {
|
|
149
|
+
throw cursorError("INVALID_RESULT_CURSOR", "The result cursor is invalid.");
|
|
150
|
+
}
|
|
151
|
+
const components = cursor.slice("cursor_".length).split(".");
|
|
152
|
+
if (components.length !== 2) {
|
|
153
|
+
throw cursorError("INVALID_RESULT_CURSOR", "The result cursor is invalid.");
|
|
154
|
+
}
|
|
155
|
+
const [encoded, encodedSignature] = components;
|
|
156
|
+
const signature = decodeBase64Url(encodedSignature);
|
|
157
|
+
const expected = createHmac("sha256", this.#key).update(encoded).digest();
|
|
158
|
+
if (signature.length !== expected.length || !timingSafeEqual(signature, expected)) {
|
|
159
|
+
throw cursorError("INVALID_RESULT_CURSOR", "The result cursor is invalid.");
|
|
160
|
+
}
|
|
161
|
+
return encoded;
|
|
162
|
+
}
|
|
95
163
|
#validatePayload(payload) {
|
|
96
164
|
if (!RESULT_ID_PATTERN.test(payload.resultId)
|
|
97
165
|
|| !Number.isSafeInteger(payload.offset)
|
|
@@ -100,4 +168,16 @@ export class CursorCodec {
|
|
|
100
168
|
throw new Error("cursor payload is invalid");
|
|
101
169
|
}
|
|
102
170
|
}
|
|
171
|
+
#validateBundlePayload(payload) {
|
|
172
|
+
if (!RESULT_ID_PATTERN.test(payload.resultId)
|
|
173
|
+
|| (payload.part !== "content" && payload.part !== "grounding")
|
|
174
|
+
|| (payload.detail !== "grounded" && payload.detail !== "layout")
|
|
175
|
+
|| payload.groundingSchemaVersion !== GROUNDING_SCHEMA_VERSION
|
|
176
|
+
|| payload.bundleProtocolVersion !== RESULT_BUNDLE_PROTOCOL_VERSION
|
|
177
|
+
|| !Number.isSafeInteger(payload.offset)
|
|
178
|
+
|| payload.offset < 0
|
|
179
|
+
|| !Number.isFinite(Date.parse(payload.expiresAt))) {
|
|
180
|
+
throw new Error("bundle cursor payload is invalid");
|
|
181
|
+
}
|
|
182
|
+
}
|
|
103
183
|
}
|