@cueai/omni-reader-mcp 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +49 -0
- package/dist/artifact-store.d.ts +65 -0
- package/dist/artifact-store.js +720 -0
- package/dist/cli/agent-config.d.ts +28 -0
- package/dist/cli/agent-config.js +335 -0
- package/dist/cli/clean.d.ts +9 -0
- package/dist/cli/clean.js +93 -0
- package/dist/cli/doctor.d.ts +17 -0
- package/dist/cli/doctor.js +157 -0
- package/dist/cli/setup.d.ts +8 -0
- package/dist/cli/setup.js +59 -0
- package/dist/constants.d.ts +7 -0
- package/dist/constants.js +7 -0
- package/dist/cube-client.d.ts +48 -0
- package/dist/cube-client.js +161 -0
- package/dist/cursor.d.ts +14 -0
- package/dist/cursor.js +101 -0
- package/dist/errors.d.ts +25 -0
- package/dist/errors.js +26 -0
- package/dist/iiis-client.d.ts +43 -0
- package/dist/iiis-client.js +694 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +174 -0
- package/dist/multipart-body.d.ts +12 -0
- package/dist/multipart-body.js +90 -0
- package/dist/operation-journal.d.ts +28 -0
- package/dist/operation-journal.js +351 -0
- package/dist/path-security.d.ts +22 -0
- package/dist/path-security.js +240 -0
- package/dist/progress.d.ts +4 -0
- package/dist/progress.js +3 -0
- package/dist/protocol.d.ts +39 -0
- package/dist/protocol.js +27 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.js +10 -0
- package/dist/tools.d.ts +24 -0
- package/dist/tools.js +224 -0
- package/package.json +35 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { detectAgentTargets, parseAgentTarget, prepareAgentConfig, verifyPreparedAgentConfig, writePreparedAgentConfig, } from "./agent-config.js";
|
|
3
|
+
import { checkHealth } from "./doctor.js";
|
|
4
|
+
function parseExtraRoots(input, environment) {
|
|
5
|
+
const separator = environment.platform === "win32" ? ";" : ":";
|
|
6
|
+
const paths = environment.platform === "win32" ? path.win32 : path.posix;
|
|
7
|
+
return input.split(separator)
|
|
8
|
+
.map((value) => value.trim())
|
|
9
|
+
.filter((value) => value.length > 0)
|
|
10
|
+
.map((value) => {
|
|
11
|
+
const expanded = value === "~"
|
|
12
|
+
? environment.homeDirectory
|
|
13
|
+
: value.startsWith(`~${paths.sep}`)
|
|
14
|
+
? paths.join(environment.homeDirectory, value.slice(2))
|
|
15
|
+
: value;
|
|
16
|
+
if (!paths.isAbsolute(expanded)) {
|
|
17
|
+
throw new Error("Every additional allowed root must be an absolute path.");
|
|
18
|
+
}
|
|
19
|
+
return paths.normalize(expanded);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
export async function runSetup(options) {
|
|
23
|
+
const labels = { cursor: "Cursor", "claude-desktop": "Claude Desktop" };
|
|
24
|
+
const detected = await detectAgentTargets(options);
|
|
25
|
+
const detectedLabel = detected.length === 0
|
|
26
|
+
? "none"
|
|
27
|
+
: detected.map((target) => labels[target]).join(", ");
|
|
28
|
+
const selected = await options.ask(`Agent (Cursor / Claude Desktop / Other; Detected: ${detectedLabel}): `);
|
|
29
|
+
const target = parseAgentTarget(selected);
|
|
30
|
+
const extraRootInput = await options.ask("Additional allowed roots (optional): ");
|
|
31
|
+
const extraRoots = parseExtraRoots(extraRootInput, options);
|
|
32
|
+
const prepared = await prepareAgentConfig(target, extraRoots, options);
|
|
33
|
+
options.write(`Target: ${prepared.displayPath}\n`);
|
|
34
|
+
options.write("Before:\n");
|
|
35
|
+
options.write(`${JSON.stringify(prepared.before, null, 2)}\n`);
|
|
36
|
+
options.write("After:\n");
|
|
37
|
+
options.write(`${JSON.stringify(prepared.after, null, 2)}\n`);
|
|
38
|
+
const confirmation = (await options.ask("Apply this user-scope configuration? Type yes: ")).trim().toLowerCase();
|
|
39
|
+
if (confirmation !== "yes") {
|
|
40
|
+
options.write("No changes written.\n");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
await verifyPreparedAgentConfig(prepared);
|
|
44
|
+
const apiKey = options.env.CUE_API_KEY ?? "";
|
|
45
|
+
if (apiKey.length === 0) {
|
|
46
|
+
throw new Error("CUE_API_KEY is absent. Create one at https://cuecue.cn/hub/api-key and retry.");
|
|
47
|
+
}
|
|
48
|
+
options.write("Cue API Key: present\n");
|
|
49
|
+
await checkHealth(options.fetchImpl, apiKey);
|
|
50
|
+
if (prepared.configPath === undefined) {
|
|
51
|
+
options.write("Generic user-scope configuration:\n");
|
|
52
|
+
options.write(`${JSON.stringify({ mcpServers: { "omni-reader": prepared.entry } }, null, 2)}\n`);
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
await writePreparedAgentConfig(prepared);
|
|
56
|
+
options.write(`Wrote ${prepared.displayPath}\n`);
|
|
57
|
+
}
|
|
58
|
+
options.write("用 Omni 解析 ./report.pdf\n");
|
|
59
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const MAX_FILE_BYTES = 268435456;
|
|
2
|
+
export declare const INLINE_RESULT_MAX_BYTES = 65536;
|
|
3
|
+
export declare const ARTIFACT_TTL_MS: number;
|
|
4
|
+
export declare const RESULT_CHUNK_MAX_BYTES = 65536;
|
|
5
|
+
export declare const PROTOCOL_VERSION = "omni.direct_upload.v1";
|
|
6
|
+
export declare const GRANTED_STREAM_PROTOCOL_VERSION = "omni.granted_parse_stream.v1";
|
|
7
|
+
export declare const DEFAULT_CUBE_BASE_URL = "https://mcp.cuecue.cn";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const MAX_FILE_BYTES = 268435456;
|
|
2
|
+
export const INLINE_RESULT_MAX_BYTES = 65536;
|
|
3
|
+
export const ARTIFACT_TTL_MS = 24 * 60 * 60 * 1000;
|
|
4
|
+
export const RESULT_CHUNK_MAX_BYTES = 65536;
|
|
5
|
+
export const PROTOCOL_VERSION = "omni.direct_upload.v1";
|
|
6
|
+
export const GRANTED_STREAM_PROTOCOL_VERSION = "omni.granted_parse_stream.v1";
|
|
7
|
+
export const DEFAULT_CUBE_BASE_URL = "https://mcp.cuecue.cn";
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { OperationJournal } from "./operation-journal.js";
|
|
2
|
+
declare const BRIDGE_PACKAGE = "@cue/omni-reader-mcp";
|
|
3
|
+
declare const BRIDGE_VERSION = "1.0.0";
|
|
4
|
+
export interface GrantRequestInput {
|
|
5
|
+
readonly contentLength: number;
|
|
6
|
+
readonly contentType: string;
|
|
7
|
+
readonly fileExtension: string;
|
|
8
|
+
readonly noStore: boolean;
|
|
9
|
+
readonly output: "markdown";
|
|
10
|
+
}
|
|
11
|
+
export interface GrantedOperation {
|
|
12
|
+
readonly clientRequestId: string;
|
|
13
|
+
readonly requestHash: string;
|
|
14
|
+
readonly grantId: string;
|
|
15
|
+
readonly operationId: string;
|
|
16
|
+
readonly parseGrant: string;
|
|
17
|
+
readonly operationToken: string;
|
|
18
|
+
readonly uploadUrl: string;
|
|
19
|
+
readonly expiresAt: string;
|
|
20
|
+
readonly maxBytes: number;
|
|
21
|
+
readonly protocolVersion: string;
|
|
22
|
+
}
|
|
23
|
+
export interface CubeGrantClientOptions {
|
|
24
|
+
readonly journal: OperationJournal;
|
|
25
|
+
readonly apiKey?: string;
|
|
26
|
+
readonly environment?: Pick<NodeJS.ProcessEnv, "CUE_API_KEY">;
|
|
27
|
+
readonly baseUrl?: string;
|
|
28
|
+
readonly fetchImpl?: typeof fetch;
|
|
29
|
+
}
|
|
30
|
+
interface GrantRequestBody {
|
|
31
|
+
readonly content_length: number;
|
|
32
|
+
readonly content_type: string;
|
|
33
|
+
readonly file_extension: string;
|
|
34
|
+
readonly no_store: boolean;
|
|
35
|
+
readonly output: "markdown";
|
|
36
|
+
readonly bridge: {
|
|
37
|
+
readonly package: typeof BRIDGE_PACKAGE;
|
|
38
|
+
readonly version: typeof BRIDGE_VERSION;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export declare function grantRequestHash(body: GrantRequestBody): string;
|
|
42
|
+
export declare function createClientRequestId(): string;
|
|
43
|
+
export declare class CubeGrantClient {
|
|
44
|
+
#private;
|
|
45
|
+
constructor(options: CubeGrantClientOptions);
|
|
46
|
+
createGrant(input: GrantRequestInput, clientRequestId: string, signal?: AbortSignal): Promise<GrantedOperation>;
|
|
47
|
+
}
|
|
48
|
+
export {};
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { DEFAULT_CUBE_BASE_URL, MAX_FILE_BYTES, PROTOCOL_VERSION, } from "./constants.js";
|
|
4
|
+
import { OmniBridgeError } from "./errors.js";
|
|
5
|
+
const GRANT_PATH = "/api/omni-reader/direct-upload/v1/parse-grants";
|
|
6
|
+
const BRIDGE_PACKAGE = "@cue/omni-reader-mcp";
|
|
7
|
+
const BRIDGE_VERSION = "1.0.0";
|
|
8
|
+
const grantResponseSchema = z
|
|
9
|
+
.object({
|
|
10
|
+
grant_id: z.string().min(1),
|
|
11
|
+
operation_id: z.string().min(1),
|
|
12
|
+
parse_grant: z.string().min(1),
|
|
13
|
+
operation_token: z.string().min(1),
|
|
14
|
+
upload_url: z
|
|
15
|
+
.string()
|
|
16
|
+
.url()
|
|
17
|
+
.refine((value) => new URL(value).protocol === "https:"),
|
|
18
|
+
expires_at: z.string().datetime({ offset: true }),
|
|
19
|
+
max_bytes: z.literal(MAX_FILE_BYTES),
|
|
20
|
+
protocol_version: z.literal(PROTOCOL_VERSION),
|
|
21
|
+
})
|
|
22
|
+
.strict();
|
|
23
|
+
function bridgeError(code, message, retryable) {
|
|
24
|
+
return new OmniBridgeError({
|
|
25
|
+
code,
|
|
26
|
+
message,
|
|
27
|
+
fileUploaded: false,
|
|
28
|
+
billed: false,
|
|
29
|
+
contentReleased: false,
|
|
30
|
+
retryable,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
function grantRequestBody(input) {
|
|
34
|
+
if (!Number.isSafeInteger(input.contentLength) ||
|
|
35
|
+
input.contentLength < 0 ||
|
|
36
|
+
input.contentLength > MAX_FILE_BYTES ||
|
|
37
|
+
!/^[^\s;/]+\/[^\s;]+$/u.test(input.contentType) ||
|
|
38
|
+
!/^\.[a-z0-9]{1,10}$/u.test(input.fileExtension) ||
|
|
39
|
+
typeof input.noStore !== "boolean" ||
|
|
40
|
+
input.output !== "markdown") {
|
|
41
|
+
throw bridgeError("INVALID_GRANT_REQUEST", "The local file metadata is invalid for a parse grant.", false);
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
content_length: input.contentLength,
|
|
45
|
+
content_type: input.contentType,
|
|
46
|
+
file_extension: input.fileExtension,
|
|
47
|
+
no_store: input.noStore,
|
|
48
|
+
output: input.output,
|
|
49
|
+
bridge: {
|
|
50
|
+
package: BRIDGE_PACKAGE,
|
|
51
|
+
version: BRIDGE_VERSION,
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export function grantRequestHash(body) {
|
|
56
|
+
return `sha256:${createHash("sha256")
|
|
57
|
+
.update(JSON.stringify(body), "utf8")
|
|
58
|
+
.digest("hex")}`;
|
|
59
|
+
}
|
|
60
|
+
export function createClientRequestId() {
|
|
61
|
+
return randomUUID();
|
|
62
|
+
}
|
|
63
|
+
function responseError(status) {
|
|
64
|
+
if (status === 401) {
|
|
65
|
+
return bridgeError("INVALID_CUE_API_KEY", "The Cue API Key is invalid or expired.", false);
|
|
66
|
+
}
|
|
67
|
+
if (status === 403) {
|
|
68
|
+
return bridgeError("OMNI_NOT_ENTITLED", "This Cue account is not entitled to use Omni Reader.", false);
|
|
69
|
+
}
|
|
70
|
+
if (status === 402) {
|
|
71
|
+
return bridgeError("INSUFFICIENT_BALANCE", "The Cue account does not have enough balance to start this parse.", false);
|
|
72
|
+
}
|
|
73
|
+
if (status === 409) {
|
|
74
|
+
return bridgeError("IDEMPOTENCY_COLLISION", "This grant request identifier is already bound to different metadata.", false);
|
|
75
|
+
}
|
|
76
|
+
if (status === 404) {
|
|
77
|
+
return bridgeError("DIRECT_UPLOAD_DISABLED", "Direct local-file parsing is not enabled on this Omni service.", false);
|
|
78
|
+
}
|
|
79
|
+
if (status === 429) {
|
|
80
|
+
return bridgeError("CUBE_BUSY", "Cube is temporarily busy. Retry this same grant request later.", true);
|
|
81
|
+
}
|
|
82
|
+
return bridgeError("CUBE_UNAVAILABLE", "Cube could not create the parse grant. Retry this same grant request later.", status >= 500);
|
|
83
|
+
}
|
|
84
|
+
export class CubeGrantClient {
|
|
85
|
+
#journal;
|
|
86
|
+
#apiKey;
|
|
87
|
+
#grantUrl;
|
|
88
|
+
#fetch;
|
|
89
|
+
constructor(options) {
|
|
90
|
+
this.#journal = options.journal;
|
|
91
|
+
this.#apiKey =
|
|
92
|
+
options.apiKey ??
|
|
93
|
+
options.environment?.CUE_API_KEY ??
|
|
94
|
+
process.env.CUE_API_KEY ??
|
|
95
|
+
"";
|
|
96
|
+
const baseUrl = new URL(options.baseUrl ?? DEFAULT_CUBE_BASE_URL);
|
|
97
|
+
if (baseUrl.protocol !== "https:" ||
|
|
98
|
+
baseUrl.username.length > 0 ||
|
|
99
|
+
baseUrl.password.length > 0) {
|
|
100
|
+
throw bridgeError("INSECURE_CUBE_BASE_URL", "The Cube control endpoint must use HTTPS without embedded credentials.", false);
|
|
101
|
+
}
|
|
102
|
+
this.#grantUrl = new URL(GRANT_PATH, baseUrl).toString();
|
|
103
|
+
this.#fetch = options.fetchImpl ?? fetch;
|
|
104
|
+
}
|
|
105
|
+
async createGrant(input, clientRequestId, signal) {
|
|
106
|
+
if (this.#apiKey.length === 0) {
|
|
107
|
+
throw bridgeError("MISSING_CUE_API_KEY", "Set CUE_API_KEY before using local Omni document parsing.", false);
|
|
108
|
+
}
|
|
109
|
+
const body = grantRequestBody(input);
|
|
110
|
+
const serializedBody = JSON.stringify(body);
|
|
111
|
+
const requestHash = grantRequestHash(body);
|
|
112
|
+
await this.#journal.begin(clientRequestId, requestHash);
|
|
113
|
+
let response;
|
|
114
|
+
try {
|
|
115
|
+
response = await this.#fetch(this.#grantUrl, {
|
|
116
|
+
method: "POST",
|
|
117
|
+
headers: {
|
|
118
|
+
authorization: `Bearer ${this.#apiKey}`,
|
|
119
|
+
"content-type": "application/json",
|
|
120
|
+
"idempotency-key": clientRequestId,
|
|
121
|
+
},
|
|
122
|
+
body: serializedBody,
|
|
123
|
+
signal,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
if (signal?.aborted) {
|
|
128
|
+
throw bridgeError("GRANT_REQUEST_CANCELED", "The parse grant request was canceled.", false);
|
|
129
|
+
}
|
|
130
|
+
throw bridgeError("CUBE_UNAVAILABLE", "Cube could not create the parse grant. Retry this same grant request later.", true);
|
|
131
|
+
}
|
|
132
|
+
if (!response.ok) {
|
|
133
|
+
throw responseError(response.status);
|
|
134
|
+
}
|
|
135
|
+
let parsed;
|
|
136
|
+
try {
|
|
137
|
+
parsed = grantResponseSchema.parse(await response.json());
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
throw bridgeError("CUBE_PROTOCOL_ERROR", "Cube returned an invalid parse grant response.", false);
|
|
141
|
+
}
|
|
142
|
+
await this.#journal.markGrantIssued(clientRequestId, {
|
|
143
|
+
operationId: parsed.operation_id,
|
|
144
|
+
operationToken: parsed.operation_token,
|
|
145
|
+
uploadUrl: parsed.upload_url,
|
|
146
|
+
expiresAt: parsed.expires_at,
|
|
147
|
+
});
|
|
148
|
+
return {
|
|
149
|
+
clientRequestId,
|
|
150
|
+
requestHash,
|
|
151
|
+
grantId: parsed.grant_id,
|
|
152
|
+
operationId: parsed.operation_id,
|
|
153
|
+
parseGrant: parsed.parse_grant,
|
|
154
|
+
operationToken: parsed.operation_token,
|
|
155
|
+
uploadUrl: parsed.upload_url,
|
|
156
|
+
expiresAt: parsed.expires_at,
|
|
157
|
+
maxBytes: parsed.max_bytes,
|
|
158
|
+
protocolVersion: parsed.protocol_version,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|
package/dist/cursor.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface CursorPayload {
|
|
2
|
+
readonly resultId: string;
|
|
3
|
+
readonly offset: number;
|
|
4
|
+
readonly expiresAt: string;
|
|
5
|
+
}
|
|
6
|
+
export interface CursorCodecOptions {
|
|
7
|
+
readonly now?: () => Date;
|
|
8
|
+
}
|
|
9
|
+
export declare class CursorCodec {
|
|
10
|
+
#private;
|
|
11
|
+
constructor(key: Uint8Array, options?: CursorCodecOptions);
|
|
12
|
+
encode(payload: CursorPayload): string;
|
|
13
|
+
decode(cursor: string): CursorPayload;
|
|
14
|
+
}
|
package/dist/cursor.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { OmniBridgeError } from "./errors.js";
|
|
3
|
+
const CURSOR_VERSION = 1;
|
|
4
|
+
const BASE64URL_PATTERN = /^[A-Za-z0-9_-]+$/;
|
|
5
|
+
const RESULT_ID_PATTERN = /^result_[A-Za-z0-9_-]{16,64}$/;
|
|
6
|
+
function cursorError(code, message) {
|
|
7
|
+
return new OmniBridgeError({
|
|
8
|
+
code,
|
|
9
|
+
message,
|
|
10
|
+
fileUploaded: true,
|
|
11
|
+
billed: true,
|
|
12
|
+
contentReleased: true,
|
|
13
|
+
retryable: false,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
function decodeBase64Url(value) {
|
|
17
|
+
if (!BASE64URL_PATTERN.test(value)) {
|
|
18
|
+
throw cursorError("INVALID_RESULT_CURSOR", "The result cursor is invalid.");
|
|
19
|
+
}
|
|
20
|
+
const decoded = Buffer.from(value, "base64url");
|
|
21
|
+
if (decoded.toString("base64url") !== value) {
|
|
22
|
+
throw cursorError("INVALID_RESULT_CURSOR", "The result cursor is invalid.");
|
|
23
|
+
}
|
|
24
|
+
return decoded;
|
|
25
|
+
}
|
|
26
|
+
export class CursorCodec {
|
|
27
|
+
#key;
|
|
28
|
+
#now;
|
|
29
|
+
constructor(key, options = {}) {
|
|
30
|
+
if (key.byteLength !== 32)
|
|
31
|
+
throw new Error("cursor key must contain exactly 32 bytes");
|
|
32
|
+
this.#key = Buffer.from(key);
|
|
33
|
+
this.#now = options.now ?? (() => new Date());
|
|
34
|
+
}
|
|
35
|
+
encode(payload) {
|
|
36
|
+
this.#validatePayload(payload);
|
|
37
|
+
const encoded = Buffer.from(JSON.stringify({
|
|
38
|
+
version: CURSOR_VERSION,
|
|
39
|
+
resultId: payload.resultId,
|
|
40
|
+
offset: payload.offset,
|
|
41
|
+
expiresAt: payload.expiresAt,
|
|
42
|
+
}), "utf8").toString("base64url");
|
|
43
|
+
const signature = createHmac("sha256", this.#key).update(encoded).digest("base64url");
|
|
44
|
+
return `cursor_${encoded}.${signature}`;
|
|
45
|
+
}
|
|
46
|
+
decode(cursor) {
|
|
47
|
+
if (typeof cursor !== "string" || cursor.length > 2048 || !cursor.startsWith("cursor_")) {
|
|
48
|
+
throw cursorError("INVALID_RESULT_CURSOR", "The result cursor is invalid.");
|
|
49
|
+
}
|
|
50
|
+
const components = cursor.slice("cursor_".length).split(".");
|
|
51
|
+
if (components.length !== 2) {
|
|
52
|
+
throw cursorError("INVALID_RESULT_CURSOR", "The result cursor is invalid.");
|
|
53
|
+
}
|
|
54
|
+
const [encoded, encodedSignature] = components;
|
|
55
|
+
const signature = decodeBase64Url(encodedSignature);
|
|
56
|
+
const expected = createHmac("sha256", this.#key).update(encoded).digest();
|
|
57
|
+
if (signature.length !== expected.length || !timingSafeEqual(signature, expected)) {
|
|
58
|
+
throw cursorError("INVALID_RESULT_CURSOR", "The result cursor is invalid.");
|
|
59
|
+
}
|
|
60
|
+
let value;
|
|
61
|
+
try {
|
|
62
|
+
value = JSON.parse(decodeBase64Url(encoded).toString("utf8"));
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
if (error instanceof OmniBridgeError)
|
|
66
|
+
throw error;
|
|
67
|
+
throw cursorError("INVALID_RESULT_CURSOR", "The result cursor is invalid.");
|
|
68
|
+
}
|
|
69
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
70
|
+
throw cursorError("INVALID_RESULT_CURSOR", "The result cursor is invalid.");
|
|
71
|
+
}
|
|
72
|
+
const record = value;
|
|
73
|
+
if (Object.keys(record).sort().join(",") !== "expiresAt,offset,resultId,version"
|
|
74
|
+
|| record.version !== CURSOR_VERSION
|
|
75
|
+
|| typeof record.resultId !== "string"
|
|
76
|
+
|| !RESULT_ID_PATTERN.test(record.resultId)
|
|
77
|
+
|| typeof record.offset !== "number"
|
|
78
|
+
|| !Number.isSafeInteger(record.offset)
|
|
79
|
+
|| record.offset < 0
|
|
80
|
+
|| typeof record.expiresAt !== "string"
|
|
81
|
+
|| !Number.isFinite(Date.parse(record.expiresAt))) {
|
|
82
|
+
throw cursorError("INVALID_RESULT_CURSOR", "The result cursor is invalid.");
|
|
83
|
+
}
|
|
84
|
+
if (Date.parse(record.expiresAt) <= this.#now().getTime()) {
|
|
85
|
+
throw cursorError("RESULT_CURSOR_EXPIRED", "The result cursor has expired.");
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
resultId: record.resultId,
|
|
89
|
+
offset: record.offset,
|
|
90
|
+
expiresAt: record.expiresAt,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
#validatePayload(payload) {
|
|
94
|
+
if (!RESULT_ID_PATTERN.test(payload.resultId)
|
|
95
|
+
|| !Number.isSafeInteger(payload.offset)
|
|
96
|
+
|| payload.offset < 0
|
|
97
|
+
|| !Number.isFinite(Date.parse(payload.expiresAt))) {
|
|
98
|
+
throw new Error("cursor payload is invalid");
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface OmniBridgeErrorInit {
|
|
2
|
+
code: string;
|
|
3
|
+
message: string;
|
|
4
|
+
fileUploaded: boolean;
|
|
5
|
+
billed: boolean;
|
|
6
|
+
contentReleased: boolean;
|
|
7
|
+
retryable: boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface OmniBridgeErrorPayload {
|
|
10
|
+
code: string;
|
|
11
|
+
message: string;
|
|
12
|
+
file_uploaded: boolean;
|
|
13
|
+
billed: boolean;
|
|
14
|
+
content_released: boolean;
|
|
15
|
+
retryable: boolean;
|
|
16
|
+
}
|
|
17
|
+
export declare class OmniBridgeError extends Error {
|
|
18
|
+
readonly code: string;
|
|
19
|
+
readonly fileUploaded: boolean;
|
|
20
|
+
readonly billed: boolean;
|
|
21
|
+
readonly contentReleased: boolean;
|
|
22
|
+
readonly retryable: boolean;
|
|
23
|
+
constructor(init: OmniBridgeErrorInit);
|
|
24
|
+
toJSON(): OmniBridgeErrorPayload;
|
|
25
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export class OmniBridgeError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
fileUploaded;
|
|
4
|
+
billed;
|
|
5
|
+
contentReleased;
|
|
6
|
+
retryable;
|
|
7
|
+
constructor(init) {
|
|
8
|
+
super(init.message);
|
|
9
|
+
this.name = "OmniBridgeError";
|
|
10
|
+
this.code = init.code;
|
|
11
|
+
this.fileUploaded = init.fileUploaded;
|
|
12
|
+
this.billed = init.billed;
|
|
13
|
+
this.contentReleased = init.contentReleased;
|
|
14
|
+
this.retryable = init.retryable;
|
|
15
|
+
}
|
|
16
|
+
toJSON() {
|
|
17
|
+
return {
|
|
18
|
+
code: this.code,
|
|
19
|
+
message: this.message,
|
|
20
|
+
file_uploaded: this.fileUploaded,
|
|
21
|
+
billed: this.billed,
|
|
22
|
+
content_released: this.contentReleased,
|
|
23
|
+
retryable: this.retryable,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { OpenedAllowedFile } from "./path-security.js";
|
|
2
|
+
import { type ProgressSink } from "./progress.js";
|
|
3
|
+
export interface ResultRetentionStart {
|
|
4
|
+
readonly operationId: string;
|
|
5
|
+
readonly resultBytes: number;
|
|
6
|
+
readonly mediaType: string;
|
|
7
|
+
readonly source: "sse" | "recovery";
|
|
8
|
+
}
|
|
9
|
+
export interface ReleasedMetadata extends ResultRetentionStart {
|
|
10
|
+
readonly resultDigest: string;
|
|
11
|
+
}
|
|
12
|
+
export interface ResultRetentionSink {
|
|
13
|
+
reset(): Promise<void>;
|
|
14
|
+
begin(metadata: ResultRetentionStart): Promise<void>;
|
|
15
|
+
write(chunk: Uint8Array): Promise<void>;
|
|
16
|
+
complete(metadata: ReleasedMetadata): Promise<void>;
|
|
17
|
+
abort(): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
export interface IiisOperationInput {
|
|
20
|
+
readonly parseGrant: string;
|
|
21
|
+
readonly operationId: string;
|
|
22
|
+
readonly operationToken: string;
|
|
23
|
+
readonly uploadUrl: string;
|
|
24
|
+
readonly expiresAt: string;
|
|
25
|
+
readonly openedFile: OpenedAllowedFile;
|
|
26
|
+
readonly retention: ResultRetentionSink;
|
|
27
|
+
readonly progress?: ProgressSink;
|
|
28
|
+
readonly signal?: AbortSignal;
|
|
29
|
+
}
|
|
30
|
+
export interface ReleasedResult extends ReleasedMetadata {
|
|
31
|
+
}
|
|
32
|
+
export interface IiisClientOptions {
|
|
33
|
+
readonly fetchImpl?: typeof fetch;
|
|
34
|
+
readonly pollIntervalMs?: number;
|
|
35
|
+
readonly maxPolls?: number;
|
|
36
|
+
}
|
|
37
|
+
export declare class IiisClient {
|
|
38
|
+
#private;
|
|
39
|
+
constructor(options?: IiisClientOptions);
|
|
40
|
+
uploadAndWait(input: IiisOperationInput): Promise<ReleasedResult>;
|
|
41
|
+
downloadResult(input: IiisOperationInput): Promise<ReleasedResult>;
|
|
42
|
+
ack(input: IiisOperationInput): Promise<void>;
|
|
43
|
+
}
|