@tidblink/server 0.0.0-stage → 0.4.0-rc.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/LICENSE +201 -0
- package/README.md +1182 -2
- package/dist/audio.d.ts +72 -0
- package/dist/audio.d.ts.map +1 -0
- package/dist/audio.js +168 -0
- package/dist/audio.js.map +1 -0
- package/dist/backend-connection.d.ts +89 -0
- package/dist/backend-connection.d.ts.map +1 -0
- package/dist/backend-connection.js +300 -0
- package/dist/backend-connection.js.map +1 -0
- package/dist/backend-pool.d.ts +93 -0
- package/dist/backend-pool.d.ts.map +1 -0
- package/dist/backend-pool.js +419 -0
- package/dist/backend-pool.js.map +1 -0
- package/dist/client-tool-executor.d.ts +31 -0
- package/dist/client-tool-executor.d.ts.map +1 -0
- package/dist/client-tool-executor.js +190 -0
- package/dist/client-tool-executor.js.map +1 -0
- package/dist/client-tools.d.ts +21 -0
- package/dist/client-tools.d.ts.map +1 -0
- package/dist/client-tools.js +48 -0
- package/dist/client-tools.js.map +1 -0
- package/dist/connector-callback.d.ts +92 -0
- package/dist/connector-callback.d.ts.map +1 -0
- package/dist/connector-callback.js +155 -0
- package/dist/connector-callback.js.map +1 -0
- package/dist/connectors.d.ts +66 -0
- package/dist/connectors.d.ts.map +1 -0
- package/dist/connectors.js +71 -0
- package/dist/connectors.js.map +1 -0
- package/dist/control.d.ts +17 -0
- package/dist/control.d.ts.map +1 -0
- package/dist/control.js +52 -0
- package/dist/control.js.map +1 -0
- package/dist/errors.d.ts +103 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +115 -0
- package/dist/errors.js.map +1 -0
- package/dist/events.d.ts +158 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +110 -0
- package/dist/events.js.map +1 -0
- package/dist/files.d.ts +129 -0
- package/dist/files.d.ts.map +1 -0
- package/dist/files.js +388 -0
- package/dist/files.js.map +1 -0
- package/dist/history.d.ts +279 -0
- package/dist/history.d.ts.map +1 -0
- package/dist/history.js +91 -0
- package/dist/history.js.map +1 -0
- package/dist/http.d.ts +88 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +302 -0
- package/dist/http.js.map +1 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/organization.d.ts +87 -0
- package/dist/organization.d.ts.map +1 -0
- package/dist/organization.js +112 -0
- package/dist/organization.js.map +1 -0
- package/dist/realtime.d.ts +90 -0
- package/dist/realtime.d.ts.map +1 -0
- package/dist/realtime.js +111 -0
- package/dist/realtime.js.map +1 -0
- package/dist/session-connection.d.ts +144 -0
- package/dist/session-connection.d.ts.map +1 -0
- package/dist/session-connection.js +334 -0
- package/dist/session-connection.js.map +1 -0
- package/dist/session-send.d.ts +49 -0
- package/dist/session-send.d.ts.map +1 -0
- package/dist/session-send.js +179 -0
- package/dist/session-send.js.map +1 -0
- package/dist/session.d.ts +165 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +253 -0
- package/dist/session.js.map +1 -0
- package/dist/skills.d.ts +103 -0
- package/dist/skills.d.ts.map +1 -0
- package/dist/skills.js +98 -0
- package/dist/skills.js.map +1 -0
- package/dist/sse.d.ts +10 -0
- package/dist/sse.d.ts.map +1 -0
- package/dist/sse.js +50 -0
- package/dist/sse.js.map +1 -0
- package/dist/template-switch.d.ts +51 -0
- package/dist/template-switch.d.ts.map +1 -0
- package/dist/template-switch.js +81 -0
- package/dist/template-switch.js.map +1 -0
- package/dist/templates.d.ts +129 -0
- package/dist/templates.d.ts.map +1 -0
- package/dist/templates.js +92 -0
- package/dist/templates.js.map +1 -0
- package/dist/turn.d.ts +47 -0
- package/dist/turn.d.ts.map +1 -0
- package/dist/turn.js +210 -0
- package/dist/turn.js.map +1 -0
- package/dist/usage.d.ts +278 -0
- package/dist/usage.d.ts.map +1 -0
- package/dist/usage.js +22 -0
- package/dist/usage.js.map +1 -0
- package/dist/user.d.ts +55 -0
- package/dist/user.d.ts.map +1 -0
- package/dist/user.js +52 -0
- package/dist/user.js.map +1 -0
- package/examples/control-turn.mjs +53 -0
- package/examples/create-session.mjs +82 -0
- package/examples/http-connector.mjs +127 -0
- package/examples/lamp-backend.mjs +175 -0
- package/examples/prepare-realtime-connection.mjs +72 -0
- package/examples/read-usage.mjs +44 -0
- package/examples/send-message.mjs +48 -0
- package/examples/submit-audio.mjs +57 -0
- package/examples/upload-file.mjs +58 -0
- package/package.json +59 -4
package/dist/events.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { TiDBLinkError } from "./errors.js";
|
|
2
|
+
const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3
|
+
const isString = (value) => typeof value === "string";
|
|
4
|
+
/** Turn events name their durable event in `data.event`. */
|
|
5
|
+
const TURN_EVENTS = {
|
|
6
|
+
"agent.session.turn.started": ["turn_started"],
|
|
7
|
+
"agent.session.turn.output_text.snapshot": ["assistant_draft"],
|
|
8
|
+
"agent.session.turn.progress": ["progress"],
|
|
9
|
+
"agent.session.turn.item.added": ["assistant_message", "operation_step"],
|
|
10
|
+
"agent.session.turn.requires_action": ["client_tool_calls"],
|
|
11
|
+
"agent.session.turn.completed": ["turn_finished"],
|
|
12
|
+
"agent.session.turn.cancelled": ["turn_finished"],
|
|
13
|
+
"agent.session.turn.failed": ["turn_finished", "turn_error"],
|
|
14
|
+
};
|
|
15
|
+
const FINISHED_STATUS = {
|
|
16
|
+
"agent.session.turn.completed": "succeeded",
|
|
17
|
+
"agent.session.turn.cancelled": "interrupted",
|
|
18
|
+
"agent.session.turn.failed": "failed",
|
|
19
|
+
};
|
|
20
|
+
const isTextPayload = (payload) => isString(payload.text);
|
|
21
|
+
/** Whether a `client_tool_calls` payload has the fields the SDK types. */
|
|
22
|
+
export const isClientToolCallsPayload = (payload) => Array.isArray(payload.calls) &&
|
|
23
|
+
payload.calls.every((call) => isObject(call) &&
|
|
24
|
+
isString(call.callId) &&
|
|
25
|
+
isString(call.name) &&
|
|
26
|
+
isObject(call.arguments) &&
|
|
27
|
+
isString(call.deadlineAt) &&
|
|
28
|
+
(call.claimBy === undefined || isString(call.claimBy)));
|
|
29
|
+
const isKnownTurnPayload = (name, payload, type) => {
|
|
30
|
+
switch (name) {
|
|
31
|
+
case "progress":
|
|
32
|
+
case "assistant_draft":
|
|
33
|
+
return isTextPayload(payload);
|
|
34
|
+
case "assistant_message":
|
|
35
|
+
return isTextPayload(payload) && isString(payload.messageId);
|
|
36
|
+
case "operation_step":
|
|
37
|
+
return (isString(payload.operationId) &&
|
|
38
|
+
isString(payload.status) &&
|
|
39
|
+
isString(payload.label));
|
|
40
|
+
case "client_tool_calls":
|
|
41
|
+
return isClientToolCallsPayload(payload);
|
|
42
|
+
case "turn_finished":
|
|
43
|
+
return payload.status === FINISHED_STATUS[type];
|
|
44
|
+
case "turn_error":
|
|
45
|
+
return isString(payload.code) && isString(payload.message);
|
|
46
|
+
default:
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Narrow an event to one of the types the SDK knows. Returns `undefined` for
|
|
52
|
+
* any other type, and for a known type whose data lacks the fields the SDK
|
|
53
|
+
* types; such events are still delivered by the stream unchanged.
|
|
54
|
+
*/
|
|
55
|
+
export function asKnownSessionEvent(event) {
|
|
56
|
+
const { type, data } = event;
|
|
57
|
+
const turnEvents = TURN_EVENTS[type];
|
|
58
|
+
if (turnEvents) {
|
|
59
|
+
if (!isString(event.turnId) ||
|
|
60
|
+
!isString(data.event) ||
|
|
61
|
+
!turnEvents.includes(data.event) ||
|
|
62
|
+
!isObject(data.payload) ||
|
|
63
|
+
!isKnownTurnPayload(data.event, data.payload, type))
|
|
64
|
+
return undefined;
|
|
65
|
+
return event;
|
|
66
|
+
}
|
|
67
|
+
if (type === "agent.session.realtime.input_transcript.completed" ||
|
|
68
|
+
type === "agent.session.realtime.response.completed") {
|
|
69
|
+
const expected = type === "agent.session.realtime.input_transcript.completed"
|
|
70
|
+
? "input_transcript"
|
|
71
|
+
: "response";
|
|
72
|
+
return data.type === expected &&
|
|
73
|
+
isString(data.id) &&
|
|
74
|
+
isString(data.text) &&
|
|
75
|
+
isString(data.realtimeConnectionId)
|
|
76
|
+
? event
|
|
77
|
+
: undefined;
|
|
78
|
+
}
|
|
79
|
+
if (type === "agent.session.template_switch.succeeded" ||
|
|
80
|
+
type === "agent.session.template_switch.failed")
|
|
81
|
+
return isString(data.commandId) &&
|
|
82
|
+
isString(data.agentTemplateId) &&
|
|
83
|
+
typeof data.configurationEpoch === "number" &&
|
|
84
|
+
data.status === type.slice("agent.session.template_switch.".length)
|
|
85
|
+
? event
|
|
86
|
+
: undefined;
|
|
87
|
+
if (type === "agent.session.idle")
|
|
88
|
+
return isString(data.id) && (data.turnId === null || isString(data.turnId))
|
|
89
|
+
? event
|
|
90
|
+
: undefined;
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
/** Check one event envelope and attach its cursor. */
|
|
94
|
+
export function sessionEventFrom(value, cursor, options = {}) {
|
|
95
|
+
const requestId = options.requestId === undefined ? {} : { requestId: options.requestId };
|
|
96
|
+
const event = value;
|
|
97
|
+
if (!isString(cursor) ||
|
|
98
|
+
!cursor ||
|
|
99
|
+
!isObject(event) ||
|
|
100
|
+
!isString(event.type) ||
|
|
101
|
+
!event.type ||
|
|
102
|
+
!isString(event.sessionId) ||
|
|
103
|
+
(options.sessionId !== undefined && event.sessionId !== options.sessionId) ||
|
|
104
|
+
(event.turnId !== null && !isString(event.turnId)) ||
|
|
105
|
+
!isString(event.createdAt) ||
|
|
106
|
+
!isObject(event.data))
|
|
107
|
+
throw new TiDBLinkError("Session event has an invalid envelope", requestId);
|
|
108
|
+
return { ...event, cursor };
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=events.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"events.js","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"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"}
|
package/dist/files.d.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { HttpClient } from "./http.js";
|
|
2
|
+
import { OutcomeUnknownError } from "./errors.js";
|
|
3
|
+
import type { RequestOptions } from "./organization.js";
|
|
4
|
+
export interface UserFile {
|
|
5
|
+
fileId: string;
|
|
6
|
+
originalName: string;
|
|
7
|
+
status: "uploading" | "ready" | "failed";
|
|
8
|
+
byteSize: number | null;
|
|
9
|
+
sha256: string | null;
|
|
10
|
+
checksumVerification: "whole" | "part_only" | null;
|
|
11
|
+
contentType: string | null;
|
|
12
|
+
lastError: string | null;
|
|
13
|
+
createdAt: string;
|
|
14
|
+
updatedAt: string;
|
|
15
|
+
source: {
|
|
16
|
+
kind: "user_upload";
|
|
17
|
+
} | {
|
|
18
|
+
kind: "artifact_revision";
|
|
19
|
+
artifactId: string;
|
|
20
|
+
revisionId: string;
|
|
21
|
+
revisionNumber: number;
|
|
22
|
+
sessionId: string;
|
|
23
|
+
turnId: string | null;
|
|
24
|
+
operationId: string | null;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export interface FileUploadCapabilities {
|
|
28
|
+
protocol: "tidb-link.user-files.v2";
|
|
29
|
+
inlineThreshold: number;
|
|
30
|
+
maxUploadBytes: number;
|
|
31
|
+
checksum: "sha-256";
|
|
32
|
+
}
|
|
33
|
+
export interface CreateFileUploadInput extends RequestOptions {
|
|
34
|
+
idempotencyKey: string;
|
|
35
|
+
originalName: string;
|
|
36
|
+
byteSize: number;
|
|
37
|
+
sha256: string;
|
|
38
|
+
contentType: string | null;
|
|
39
|
+
}
|
|
40
|
+
export interface UploadFileInput extends RequestOptions {
|
|
41
|
+
/** Persist with the original input before upload; reuse only for that exact file. */
|
|
42
|
+
idempotencyKey: string;
|
|
43
|
+
originalName: string;
|
|
44
|
+
bytes: Uint8Array;
|
|
45
|
+
contentType?: string | null;
|
|
46
|
+
}
|
|
47
|
+
export interface FileUpload {
|
|
48
|
+
uploadId: string;
|
|
49
|
+
file: UserFile;
|
|
50
|
+
mode: "tidb-link" | "s3";
|
|
51
|
+
expectedChecksumVerification: "whole";
|
|
52
|
+
expiresAt: string;
|
|
53
|
+
contentUrl?: string;
|
|
54
|
+
uploadUrl?: {
|
|
55
|
+
method: "PUT";
|
|
56
|
+
url: string;
|
|
57
|
+
headers: Record<string, string>;
|
|
58
|
+
expiresAt: string;
|
|
59
|
+
};
|
|
60
|
+
partSize?: number;
|
|
61
|
+
totalParts?: number;
|
|
62
|
+
}
|
|
63
|
+
export interface FileUploadStatus {
|
|
64
|
+
uploadId: string;
|
|
65
|
+
file: UserFile;
|
|
66
|
+
mode: "tidb-link" | "s3" | "initializing";
|
|
67
|
+
state: "active" | "completing" | "aborting" | "completed" | "aborted";
|
|
68
|
+
expiresAt: string;
|
|
69
|
+
}
|
|
70
|
+
export interface FileUploadPart {
|
|
71
|
+
number: number;
|
|
72
|
+
url: string;
|
|
73
|
+
size: number;
|
|
74
|
+
headers: Record<string, string>;
|
|
75
|
+
expiresAt: string;
|
|
76
|
+
}
|
|
77
|
+
export interface FileListOptions extends RequestOptions {
|
|
78
|
+
history?: boolean;
|
|
79
|
+
limit?: number;
|
|
80
|
+
cursor?: string;
|
|
81
|
+
}
|
|
82
|
+
export interface FilePage {
|
|
83
|
+
files: UserFile[];
|
|
84
|
+
nextCursor: string | null;
|
|
85
|
+
}
|
|
86
|
+
export interface FileDownloadGrant {
|
|
87
|
+
url: string;
|
|
88
|
+
expiresAt: string;
|
|
89
|
+
byteSize: number;
|
|
90
|
+
sha256: string;
|
|
91
|
+
}
|
|
92
|
+
export type FileContent = {
|
|
93
|
+
type: "bytes";
|
|
94
|
+
bytes: Uint8Array;
|
|
95
|
+
contentType: string;
|
|
96
|
+
} | {
|
|
97
|
+
type: "download";
|
|
98
|
+
grant: FileDownloadGrant;
|
|
99
|
+
};
|
|
100
|
+
export interface UploadedFilePart {
|
|
101
|
+
number: number;
|
|
102
|
+
etag: string;
|
|
103
|
+
sha256: string;
|
|
104
|
+
}
|
|
105
|
+
export declare class FileUploadOutcomeUnknownError extends OutcomeUnknownError {
|
|
106
|
+
readonly idempotencyKey: string;
|
|
107
|
+
readonly uploadId: string | undefined;
|
|
108
|
+
constructor(idempotencyKey: string, uploadId: string | undefined, cause: OutcomeUnknownError);
|
|
109
|
+
}
|
|
110
|
+
export declare class UserFiles {
|
|
111
|
+
#private;
|
|
112
|
+
constructor(http: HttpClient);
|
|
113
|
+
capabilities(options?: RequestOptions): Promise<FileUploadCapabilities>;
|
|
114
|
+
createUpload(input: CreateFileUploadInput): Promise<FileUpload>;
|
|
115
|
+
getUpload(uploadId: string, options?: RequestOptions): Promise<FileUploadStatus>;
|
|
116
|
+
uploadContent(uploadId: string, bytes: Uint8Array, options?: RequestOptions): Promise<UserFile>;
|
|
117
|
+
presignPart(uploadId: string, number: number, sha256: string, options?: RequestOptions): Promise<FileUploadPart>;
|
|
118
|
+
uploadPart(part: FileUploadPart, bytes: Uint8Array, options?: RequestOptions): Promise<UploadedFilePart>;
|
|
119
|
+
private transfer;
|
|
120
|
+
completeUpload(uploadId: string, parts?: UploadedFilePart[], options?: RequestOptions): Promise<UserFile>;
|
|
121
|
+
cancelUpload(uploadId: string, options?: RequestOptions): Promise<UserFile>;
|
|
122
|
+
upload(input: UploadFileInput): Promise<UserFile>;
|
|
123
|
+
list(options?: FileListOptions): Promise<FilePage>;
|
|
124
|
+
get(fileId: string, options?: RequestOptions): Promise<UserFile>;
|
|
125
|
+
getContent(fileId: string, options?: RequestOptions): Promise<FileContent>;
|
|
126
|
+
downloadFromGrant(grant: FileDownloadGrant, options?: RequestOptions): Promise<Uint8Array>;
|
|
127
|
+
download(fileId: string, options?: RequestOptions): Promise<Uint8Array>;
|
|
128
|
+
}
|
|
129
|
+
//# sourceMappingURL=files.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"files.d.ts","sourceRoot":"","sources":["../src/files.ts"],"names":[],"mappings":"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"}
|
package/dist/files.js
ADDED
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import { requestOptions, HttpClient, requireKnownKeys } from "./http.js";
|
|
2
|
+
import { OutcomeUnknownError, TiDBLinkError, TiDBLinkApiError, ConnectionError, } from "./errors.js";
|
|
3
|
+
export class FileUploadOutcomeUnknownError extends OutcomeUnknownError {
|
|
4
|
+
idempotencyKey;
|
|
5
|
+
uploadId;
|
|
6
|
+
constructor(idempotencyKey, uploadId, cause) {
|
|
7
|
+
super("File upload outcome is unknown; recover the original upload before transferring again", { ...(cause.requestId ? { requestId: cause.requestId } : {}), cause });
|
|
8
|
+
this.idempotencyKey = idempotencyKey;
|
|
9
|
+
this.uploadId = uploadId;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
const uploadPath = (id) => `/drive/uploads/${encodeURIComponent(id)}`;
|
|
13
|
+
const digest = async (bytes) => {
|
|
14
|
+
const value = new Uint8Array(await crypto.subtle.digest("SHA-256", Uint8Array.from(bytes)));
|
|
15
|
+
return {
|
|
16
|
+
hex: Array.from(value, (byte) => byte.toString(16).padStart(2, "0")).join(""),
|
|
17
|
+
base64: btoa(String.fromCharCode(...value)),
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
const fileReceipt = (value, requestId) => {
|
|
21
|
+
if (!value?.file?.fileId ||
|
|
22
|
+
!["uploading", "ready", "failed"].includes(value.file.status)) {
|
|
23
|
+
throw new OutcomeUnknownError("File operation returned no usable File; read the upload state", { requestId });
|
|
24
|
+
}
|
|
25
|
+
return value.file;
|
|
26
|
+
};
|
|
27
|
+
const requireReady = (file, byteSize, sha256) => {
|
|
28
|
+
if (file.status !== "ready" ||
|
|
29
|
+
file.byteSize !== byteSize ||
|
|
30
|
+
file.sha256 !== sha256 ||
|
|
31
|
+
file.checksumVerification !== "whole") {
|
|
32
|
+
throw new TiDBLinkError("File is not ready with the expected complete size and checksum");
|
|
33
|
+
}
|
|
34
|
+
return file;
|
|
35
|
+
};
|
|
36
|
+
const requireActiveGrant = (expiresAt) => {
|
|
37
|
+
if (!Number.isFinite(Date.parse(expiresAt)) ||
|
|
38
|
+
Date.parse(expiresAt) <= Date.now())
|
|
39
|
+
throw new TiDBLinkApiError({
|
|
40
|
+
status: 410,
|
|
41
|
+
code: "file_transfer_expired",
|
|
42
|
+
message: "File transfer credential expired; obtain a fresh credential for the original file",
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
const transferUrl = (url) => {
|
|
46
|
+
if (!["http:", "https:"].includes(url.protocol) ||
|
|
47
|
+
url.username ||
|
|
48
|
+
url.password ||
|
|
49
|
+
url.hash)
|
|
50
|
+
throw new TypeError("File transfer URL must be HTTP(S) without embedded credentials or a fragment");
|
|
51
|
+
return url;
|
|
52
|
+
};
|
|
53
|
+
export class UserFiles {
|
|
54
|
+
#http;
|
|
55
|
+
constructor(http) {
|
|
56
|
+
this.#http = http;
|
|
57
|
+
}
|
|
58
|
+
async capabilities(options) {
|
|
59
|
+
options = requestOptions(options);
|
|
60
|
+
return (await this.#http.get("/drive/upload-capabilities", options)).capabilities;
|
|
61
|
+
}
|
|
62
|
+
async createUpload(input) {
|
|
63
|
+
requireKnownKeys(input, [
|
|
64
|
+
"idempotencyKey",
|
|
65
|
+
"originalName",
|
|
66
|
+
"byteSize",
|
|
67
|
+
"sha256",
|
|
68
|
+
"contentType",
|
|
69
|
+
"signal",
|
|
70
|
+
"requestId",
|
|
71
|
+
], "File upload");
|
|
72
|
+
if (typeof input.idempotencyKey !== "string" ||
|
|
73
|
+
!/^[\x21-\x7e]{1,255}$/.test(input.idempotencyKey))
|
|
74
|
+
throw new TypeError("File upload requires a stable idempotencyKey (1-255 visible ASCII characters)");
|
|
75
|
+
if (typeof input.originalName !== "string" ||
|
|
76
|
+
!input.originalName ||
|
|
77
|
+
input.originalName.length > 255 ||
|
|
78
|
+
!Number.isSafeInteger(input.byteSize) ||
|
|
79
|
+
input.byteSize < 0 ||
|
|
80
|
+
!/^[a-f0-9]{64}$/.test(input.sha256) ||
|
|
81
|
+
!(input.contentType === null ||
|
|
82
|
+
(typeof input.contentType === "string" &&
|
|
83
|
+
input.contentType.length <= 255)))
|
|
84
|
+
throw new TypeError("File upload requires a name, nonnegative byteSize, complete SHA-256 and nullable contentType");
|
|
85
|
+
const { idempotencyKey, signal, requestId, ...body } = input;
|
|
86
|
+
const result = await this.#http.postResponse("/drive/uploads", body, {
|
|
87
|
+
idempotencyKey,
|
|
88
|
+
outcomeUnknown: true,
|
|
89
|
+
...(signal ? { signal } : {}),
|
|
90
|
+
...(requestId ? { requestId } : {}),
|
|
91
|
+
});
|
|
92
|
+
const upload = result.data?.upload;
|
|
93
|
+
if (!upload?.uploadId ||
|
|
94
|
+
upload.file?.fileId !== upload.uploadId ||
|
|
95
|
+
!["tidb-link", "s3"].includes(upload.mode) ||
|
|
96
|
+
!Number.isFinite(Date.parse(upload.expiresAt)) ||
|
|
97
|
+
upload.expectedChecksumVerification !== "whole")
|
|
98
|
+
throw new OutcomeUnknownError("File creation returned no usable upload; replay the same creation identity", { requestId: result.requestId });
|
|
99
|
+
return upload;
|
|
100
|
+
}
|
|
101
|
+
async getUpload(uploadId, options) {
|
|
102
|
+
options = requestOptions(options);
|
|
103
|
+
return (await this.#http.get(uploadPath(uploadId), options)).upload;
|
|
104
|
+
}
|
|
105
|
+
async uploadContent(uploadId, bytes, options) {
|
|
106
|
+
options = requestOptions(options);
|
|
107
|
+
const checksum = await digest(bytes);
|
|
108
|
+
const { response, requestId } = await this.#http.rawResponse(`${uploadPath(uploadId)}/content`, {
|
|
109
|
+
...options,
|
|
110
|
+
method: "PUT",
|
|
111
|
+
body: Uint8Array.from(bytes),
|
|
112
|
+
headers: {
|
|
113
|
+
"content-type": "application/octet-stream",
|
|
114
|
+
"content-length": String(bytes.byteLength),
|
|
115
|
+
"content-digest": `sha-256=:${checksum.base64}:`,
|
|
116
|
+
},
|
|
117
|
+
outcomeUnknown: true,
|
|
118
|
+
});
|
|
119
|
+
try {
|
|
120
|
+
return fileReceipt(await response.json(), requestId);
|
|
121
|
+
}
|
|
122
|
+
catch (cause) {
|
|
123
|
+
if (cause instanceof OutcomeUnknownError)
|
|
124
|
+
throw cause;
|
|
125
|
+
throw new OutcomeUnknownError("Inline transfer returned no usable File; read the upload state", { requestId, cause });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
async presignPart(uploadId, number, sha256, options) {
|
|
129
|
+
options = requestOptions(options);
|
|
130
|
+
if (!Number.isSafeInteger(number) ||
|
|
131
|
+
number < 1 ||
|
|
132
|
+
!/^[a-f0-9]{64}$/.test(sha256))
|
|
133
|
+
throw new TypeError("A part requires a positive number and SHA-256");
|
|
134
|
+
const result = await this.#http.postResponse(`${uploadPath(uploadId)}/parts/${number}/presign`, { sha256 }, { ...options, outcomeUnknown: true });
|
|
135
|
+
const part = result.data?.part;
|
|
136
|
+
if (part?.number !== number ||
|
|
137
|
+
!Number.isSafeInteger(part.size) ||
|
|
138
|
+
part.size < 0 ||
|
|
139
|
+
!part.url ||
|
|
140
|
+
!part.headers ||
|
|
141
|
+
!Number.isFinite(Date.parse(part.expiresAt)))
|
|
142
|
+
throw new OutcomeUnknownError("Part signing returned no usable grant; reconcile before transferring", { requestId: result.requestId });
|
|
143
|
+
return part;
|
|
144
|
+
}
|
|
145
|
+
async uploadPart(part, bytes, options) {
|
|
146
|
+
options = requestOptions(options);
|
|
147
|
+
if (bytes.byteLength !== part.size)
|
|
148
|
+
throw new TypeError("Part bytes do not match the signed size");
|
|
149
|
+
const checksum = await digest(bytes);
|
|
150
|
+
const signedChecksum = new Headers(part.headers).get("x-amz-checksum-sha256");
|
|
151
|
+
if (signedChecksum !== checksum.base64)
|
|
152
|
+
throw new TypeError("Part bytes do not match the signed checksum");
|
|
153
|
+
const response = await this.transfer(part.url, part.expiresAt, {
|
|
154
|
+
method: "PUT",
|
|
155
|
+
headers: { ...part.headers, "content-length": String(bytes.byteLength) },
|
|
156
|
+
body: Uint8Array.from(bytes),
|
|
157
|
+
...(options?.signal ? { signal: options.signal } : {}),
|
|
158
|
+
});
|
|
159
|
+
const etag = response.headers.get("etag");
|
|
160
|
+
await response.body?.cancel();
|
|
161
|
+
if (!etag)
|
|
162
|
+
throw new OutcomeUnknownError("Part transfer returned no ETag; reconcile before completing");
|
|
163
|
+
return { number: part.number, etag, sha256: checksum.hex };
|
|
164
|
+
}
|
|
165
|
+
async transfer(url, expiresAt, init) {
|
|
166
|
+
requireActiveGrant(expiresAt);
|
|
167
|
+
const destination = transferUrl(this.#http.resolveUrl(url));
|
|
168
|
+
init.signal?.throwIfAborted();
|
|
169
|
+
let response;
|
|
170
|
+
try {
|
|
171
|
+
response = await this.#http.fetch(destination, { ...init, redirect: "error" });
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
throw new OutcomeUnknownError("File transfer outcome is unknown; inspect the original upload");
|
|
175
|
+
}
|
|
176
|
+
if (!response.ok) {
|
|
177
|
+
await response.body?.cancel();
|
|
178
|
+
if (response.status >= 500 || response.status === 408)
|
|
179
|
+
throw new OutcomeUnknownError("File transfer outcome is unknown; inspect the original upload");
|
|
180
|
+
throw new TiDBLinkApiError({
|
|
181
|
+
status: response.status,
|
|
182
|
+
code: "file_transfer_failed",
|
|
183
|
+
message: "File byte transfer was refused",
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
return response;
|
|
187
|
+
}
|
|
188
|
+
async completeUpload(uploadId, parts, options) {
|
|
189
|
+
options = requestOptions(options);
|
|
190
|
+
const result = await this.#http.postResponse(`${uploadPath(uploadId)}/complete`, parts === undefined ? {} : { parts }, { ...options, outcomeUnknown: true });
|
|
191
|
+
return fileReceipt(result.data, result.requestId);
|
|
192
|
+
}
|
|
193
|
+
async cancelUpload(uploadId, options) {
|
|
194
|
+
options = requestOptions(options);
|
|
195
|
+
const result = await this.#http.deleteResponse(uploadPath(uploadId), { ...options, outcomeUnknown: true });
|
|
196
|
+
return fileReceipt(result.data, result.requestId);
|
|
197
|
+
}
|
|
198
|
+
async upload(input) {
|
|
199
|
+
requireKnownKeys(input, [
|
|
200
|
+
"idempotencyKey",
|
|
201
|
+
"originalName",
|
|
202
|
+
"bytes",
|
|
203
|
+
"contentType",
|
|
204
|
+
"signal",
|
|
205
|
+
"requestId",
|
|
206
|
+
], "File upload");
|
|
207
|
+
if (!(input.bytes instanceof Uint8Array))
|
|
208
|
+
throw new TypeError("File bytes must be a Uint8Array");
|
|
209
|
+
const { bytes: source, ...metadata } = input;
|
|
210
|
+
const bytes = Uint8Array.from(source);
|
|
211
|
+
const checksum = await digest(bytes);
|
|
212
|
+
const options = {
|
|
213
|
+
...(input.signal ? { signal: input.signal } : {}),
|
|
214
|
+
...(input.requestId ? { requestId: input.requestId } : {}),
|
|
215
|
+
};
|
|
216
|
+
let uploadId;
|
|
217
|
+
try {
|
|
218
|
+
const upload = await this.createUpload({
|
|
219
|
+
...metadata,
|
|
220
|
+
contentType: input.contentType ?? null,
|
|
221
|
+
byteSize: bytes.byteLength,
|
|
222
|
+
sha256: checksum.hex,
|
|
223
|
+
});
|
|
224
|
+
uploadId = upload.uploadId;
|
|
225
|
+
if (upload.file.status === "ready")
|
|
226
|
+
return requireReady(upload.file, bytes.byteLength, checksum.hex);
|
|
227
|
+
requireActiveGrant(upload.expiresAt);
|
|
228
|
+
let parts;
|
|
229
|
+
if (upload.mode === "tidb-link") {
|
|
230
|
+
if (upload.uploadUrl) {
|
|
231
|
+
const grant = upload.uploadUrl;
|
|
232
|
+
if (grant.method !== "PUT" ||
|
|
233
|
+
new Headers(grant.headers).get("content-digest") !==
|
|
234
|
+
`sha-256=:${checksum.base64}:`)
|
|
235
|
+
throw new TiDBLinkError("Inline upload grant does not match the complete file checksum");
|
|
236
|
+
const response = await this.transfer(grant.url, grant.expiresAt, {
|
|
237
|
+
method: "PUT",
|
|
238
|
+
headers: {
|
|
239
|
+
...grant.headers,
|
|
240
|
+
"content-length": String(bytes.byteLength),
|
|
241
|
+
},
|
|
242
|
+
body: bytes,
|
|
243
|
+
...(input.signal ? { signal: input.signal } : {}),
|
|
244
|
+
});
|
|
245
|
+
await response.body?.cancel();
|
|
246
|
+
}
|
|
247
|
+
else
|
|
248
|
+
await this.uploadContent(uploadId, bytes, options);
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
const { partSize, totalParts } = upload;
|
|
252
|
+
if (!partSize ||
|
|
253
|
+
!Number.isSafeInteger(partSize) ||
|
|
254
|
+
!Number.isSafeInteger(totalParts) ||
|
|
255
|
+
totalParts !== Math.ceil(bytes.byteLength / partSize))
|
|
256
|
+
throw new TiDBLinkError("Multipart upload plan does not cover the complete file");
|
|
257
|
+
parts = [];
|
|
258
|
+
for (let number = 1; number <= totalParts; number++) {
|
|
259
|
+
const partBytes = bytes.subarray((number - 1) * partSize, number * partSize);
|
|
260
|
+
const part = await this.presignPart(uploadId, number, (await digest(partBytes)).hex, options);
|
|
261
|
+
parts.push(await this.uploadPart(part, partBytes, options));
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return requireReady(await this.completeUpload(uploadId, parts, options), bytes.byteLength, checksum.hex);
|
|
265
|
+
}
|
|
266
|
+
catch (cause) {
|
|
267
|
+
if (cause instanceof OutcomeUnknownError)
|
|
268
|
+
throw new FileUploadOutcomeUnknownError(metadata.idempotencyKey, uploadId, cause);
|
|
269
|
+
throw cause;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
list(options = {}) {
|
|
273
|
+
requireKnownKeys(options, ["history", "limit", "cursor", "signal", "requestId"], "File list options");
|
|
274
|
+
const query = new URLSearchParams();
|
|
275
|
+
if (options.history !== undefined)
|
|
276
|
+
query.set("history", String(options.history));
|
|
277
|
+
if (options.limit !== undefined)
|
|
278
|
+
query.set("limit", String(options.limit));
|
|
279
|
+
if (options.cursor !== undefined)
|
|
280
|
+
query.set("cursor", options.cursor);
|
|
281
|
+
return this.#http.get(`/files${query.size ? `?${query}` : ""}`, options);
|
|
282
|
+
}
|
|
283
|
+
async get(fileId, options) {
|
|
284
|
+
options = requestOptions(options);
|
|
285
|
+
return (await this.#http.get(`/files/${encodeURIComponent(fileId)}`, options)).file;
|
|
286
|
+
}
|
|
287
|
+
async getContent(fileId, options) {
|
|
288
|
+
options = requestOptions(options);
|
|
289
|
+
const { response, requestId } = await this.#http.rawResponse(`/files/${encodeURIComponent(fileId)}/content`, options);
|
|
290
|
+
const contentType = response.headers.get("content-type") ?? "application/octet-stream";
|
|
291
|
+
try {
|
|
292
|
+
if (contentType.split(";")[0]?.trim().toLowerCase() ===
|
|
293
|
+
"application/vnd.tidb-link.file-download+json") {
|
|
294
|
+
const grant = await response.json();
|
|
295
|
+
if (!grant?.url ||
|
|
296
|
+
!Number.isSafeInteger(grant.byteSize) ||
|
|
297
|
+
grant.byteSize < 0 ||
|
|
298
|
+
!/^[a-f0-9]{64}$/.test(grant.sha256) ||
|
|
299
|
+
!Number.isFinite(Date.parse(grant.expiresAt)))
|
|
300
|
+
throw new TiDBLinkError("File content returned no usable download grant", { requestId });
|
|
301
|
+
return { type: "download", grant };
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
type: "bytes",
|
|
305
|
+
bytes: new Uint8Array(await response.arrayBuffer()),
|
|
306
|
+
contentType,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
catch (cause) {
|
|
310
|
+
if (cause instanceof TiDBLinkError)
|
|
311
|
+
throw cause;
|
|
312
|
+
throw new ConnectionError("File content could not be read completely", {
|
|
313
|
+
requestId,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
async downloadFromGrant(grant, options) {
|
|
318
|
+
options = requestOptions(options);
|
|
319
|
+
requireActiveGrant(grant.expiresAt);
|
|
320
|
+
let destination = transferUrl(this.#http.resolveUrl(grant.url));
|
|
321
|
+
options?.signal?.throwIfAborted();
|
|
322
|
+
let response;
|
|
323
|
+
try {
|
|
324
|
+
response = await this.#http.fetch(destination, {
|
|
325
|
+
redirect: "manual",
|
|
326
|
+
...(options?.signal ? { signal: options.signal } : {}),
|
|
327
|
+
});
|
|
328
|
+
if ([301, 302, 303, 307, 308].includes(response.status)) {
|
|
329
|
+
const location = response.headers.get("location");
|
|
330
|
+
await response.body?.cancel();
|
|
331
|
+
if (!location)
|
|
332
|
+
throw new TiDBLinkError("File download redirect omitted its destination");
|
|
333
|
+
destination = transferUrl(new URL(location, destination));
|
|
334
|
+
if (destination.protocol !== "https:")
|
|
335
|
+
throw new TiDBLinkError("File download redirect requires HTTPS");
|
|
336
|
+
response = await this.#http.fetch(destination, {
|
|
337
|
+
redirect: "error",
|
|
338
|
+
...(options?.signal ? { signal: options.signal } : {}),
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
catch (cause) {
|
|
343
|
+
if (cause instanceof TiDBLinkError)
|
|
344
|
+
throw cause;
|
|
345
|
+
throw new ConnectionError("File download could not be reached");
|
|
346
|
+
}
|
|
347
|
+
if (!response.ok) {
|
|
348
|
+
await response.body?.cancel();
|
|
349
|
+
throw new TiDBLinkApiError({
|
|
350
|
+
status: response.status,
|
|
351
|
+
code: "file_download_failed",
|
|
352
|
+
message: "File download was refused; obtain a fresh credential if it expired",
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
let bytes;
|
|
356
|
+
try {
|
|
357
|
+
bytes = new Uint8Array(await response.arrayBuffer());
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
throw new ConnectionError("File download ended before the complete body arrived");
|
|
361
|
+
}
|
|
362
|
+
if (bytes.byteLength !== grant.byteSize ||
|
|
363
|
+
(await digest(bytes)).hex !== grant.sha256)
|
|
364
|
+
throw new TiDBLinkError("Downloaded file does not match its complete size and checksum");
|
|
365
|
+
return bytes;
|
|
366
|
+
}
|
|
367
|
+
async download(fileId, options) {
|
|
368
|
+
options = requestOptions(options);
|
|
369
|
+
const file = await this.get(fileId, options);
|
|
370
|
+
if (file.status !== "ready" ||
|
|
371
|
+
file.byteSize === null ||
|
|
372
|
+
file.sha256 === null ||
|
|
373
|
+
file.checksumVerification !== "whole")
|
|
374
|
+
throw new TiDBLinkError("Only a ready, whole-file verified File can be downloaded");
|
|
375
|
+
const content = await this.getContent(fileId, options);
|
|
376
|
+
if (content.type === "download") {
|
|
377
|
+
if (content.grant.byteSize !== file.byteSize ||
|
|
378
|
+
content.grant.sha256 !== file.sha256)
|
|
379
|
+
throw new TiDBLinkError("Download grant does not match the selected File");
|
|
380
|
+
return this.downloadFromGrant(content.grant, options);
|
|
381
|
+
}
|
|
382
|
+
if (content.bytes.byteLength !== file.byteSize ||
|
|
383
|
+
(await digest(content.bytes)).hex !== file.sha256)
|
|
384
|
+
throw new TiDBLinkError("Downloaded file does not match its complete size and checksum");
|
|
385
|
+
return content.bytes;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
//# sourceMappingURL=files.js.map
|