antpath 0.10.4 → 0.10.7
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/dist/_shared/operations.d.ts +85 -10
- package/dist/_shared/operations.js +148 -12
- package/dist/_shared/runtime-types.d.ts +47 -0
- package/dist/asset-upload.d.ts +85 -0
- package/dist/asset-upload.js +124 -0
- package/dist/asset-upload.js.map +1 -0
- package/dist/cli.mjs +111 -3
- package/dist/cli.mjs.sha256 +1 -1
- package/dist/client.d.ts +90 -1
- package/dist/client.js +199 -8
- package/dist/client.js.map +1 -1
- package/dist/file.d.ts +117 -0
- package/dist/file.js +287 -0
- package/dist/file.js.map +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/skill.d.ts +29 -0
- package/dist/skill.js +38 -1
- package/dist/skill.js.map +1 -1
- package/package.json +3 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { HttpClient } from "./http.js";
|
|
2
2
|
import type { RunUnit } from "./run-unit.js";
|
|
3
|
-
import type { Output, Run, RunEvent, SignedOutputLink, Skill, WhoAmI } from "./runtime-types.js";
|
|
3
|
+
import type { AgentsMdRecord, FileRecord, Output, Run, RunEvent, SignedOutputLink, Skill, WhoAmI } from "./runtime-types.js";
|
|
4
4
|
import type { PlatformFlatRunSubmissionInput, PlatformRunSubmissionInput } from "./submission.js";
|
|
5
5
|
/**
|
|
6
6
|
* The single source of truth for SDK<->BFF transport. The SDK class
|
|
@@ -52,22 +52,32 @@ export declare function downloadRunArchive(http: HttpClient, runId: string): Pro
|
|
|
52
52
|
export declare function submitRunFlat(http: HttpClient, request: PlatformFlatRunSubmissionInput): Promise<Run>;
|
|
53
53
|
/**
|
|
54
54
|
* Multipart variant of `submitRunFlat` for runs that carry transient
|
|
55
|
-
* (per-run) skill bundles
|
|
56
|
-
* `submission` part; each `TransientSkillRef.slot` in
|
|
57
|
-
* `request.submission.skills` MUST be mirrored by exactly one
|
|
58
|
-
* `skill:<slot>` part with the bundle bytes.
|
|
55
|
+
* (per-run) skill bundles and/or transient AgentsMd content.
|
|
59
56
|
*
|
|
60
|
-
* The
|
|
61
|
-
*
|
|
62
|
-
*
|
|
57
|
+
* The JSON submission travels as the `submission` part; each
|
|
58
|
+
* `TransientSkillRef.slot` in `request.submission.skills` MUST be
|
|
59
|
+
* mirrored by exactly one `skill:<slot>` part with the bundle bytes.
|
|
60
|
+
* Each `TransientAgentsMdRef.slot` in `request.submission.agentsMd`
|
|
61
|
+
* MUST be mirrored by exactly one `agentsmd:<slot>` part with the
|
|
62
|
+
* markdown text.
|
|
63
63
|
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
64
|
+
* The BFF re-canonicalises and re-hashes each bundle/file; a
|
|
65
|
+
* `contentHash` mismatch is rejected with a deterministic error.
|
|
66
|
+
*
|
|
67
|
+
* At least one of `bundles` or `agentsMdParts` must be non-empty.
|
|
66
68
|
*/
|
|
67
69
|
export declare function submitRunFlatMultipart(http: HttpClient, request: PlatformFlatRunSubmissionInput, bundles: ReadonlyArray<{
|
|
68
70
|
readonly slot: string;
|
|
69
71
|
readonly bytes: Uint8Array;
|
|
70
72
|
readonly filename: string;
|
|
73
|
+
}>, agentsMdParts?: ReadonlyArray<{
|
|
74
|
+
readonly slot: string;
|
|
75
|
+
readonly content: string;
|
|
76
|
+
readonly filename: string;
|
|
77
|
+
}>, fileParts?: ReadonlyArray<{
|
|
78
|
+
readonly slot: string;
|
|
79
|
+
readonly bytes: Uint8Array;
|
|
80
|
+
readonly filename: string;
|
|
71
81
|
}>): Promise<Run>;
|
|
72
82
|
/**
|
|
73
83
|
* Upload a workspace skill bundle as a zip blob. The dashboard BFF runs
|
|
@@ -106,3 +116,68 @@ export declare function findSkillByHash(http: HttpClient, args: {
|
|
|
106
116
|
* indexed by-hash route is reserved for `uploadIfChanged`.
|
|
107
117
|
*/
|
|
108
118
|
export declare function findSkillByName(http: HttpClient, name: string): Promise<Skill | null>;
|
|
119
|
+
/**
|
|
120
|
+
* Upload a workspace AgentsMd file as a markdown string. The BFF
|
|
121
|
+
* canonicalises the content into a deterministic zip with AGENTS.md at
|
|
122
|
+
* root and runs the two-phase pending → ready upload.
|
|
123
|
+
*/
|
|
124
|
+
export declare function createAgentsMd(http: HttpClient, args: {
|
|
125
|
+
readonly name: string;
|
|
126
|
+
readonly content: string;
|
|
127
|
+
}): Promise<AgentsMdRecord>;
|
|
128
|
+
export declare function listAgentsMd(http: HttpClient): Promise<readonly AgentsMdRecord[]>;
|
|
129
|
+
export declare function getAgentsMd(http: HttpClient, agentsMdId: string): Promise<AgentsMdRecord>;
|
|
130
|
+
export declare function deleteAgentsMd(http: HttpClient, agentsMdId: string): Promise<void>;
|
|
131
|
+
/**
|
|
132
|
+
* Upload a workspace File as a zip bundle. The BFF canonicalises the
|
|
133
|
+
* content and runs the two-phase pending → ready upload.
|
|
134
|
+
*/
|
|
135
|
+
export declare function createFile(http: HttpClient, args: {
|
|
136
|
+
readonly name: string;
|
|
137
|
+
readonly bytes: Uint8Array;
|
|
138
|
+
}): Promise<FileRecord>;
|
|
139
|
+
export declare function listFiles(http: HttpClient): Promise<readonly FileRecord[]>;
|
|
140
|
+
export declare function getFile(http: HttpClient, fileId: string): Promise<FileRecord>;
|
|
141
|
+
export declare function deleteFile(http: HttpClient, fileId: string): Promise<void>;
|
|
142
|
+
/** Payload returned by the BFF's upload-init endpoint. */
|
|
143
|
+
export interface AssetUploadInitResult {
|
|
144
|
+
readonly assetId: string;
|
|
145
|
+
readonly storagePath: string;
|
|
146
|
+
readonly tusUrl: string;
|
|
147
|
+
readonly tusToken: string;
|
|
148
|
+
readonly expiresAt: string;
|
|
149
|
+
readonly uploadHeaders: Readonly<Record<string, string>>;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Initialise a chunked asset upload session. Calls the BFF's
|
|
153
|
+
* `POST /api/assets/upload-init` endpoint, which:
|
|
154
|
+
*
|
|
155
|
+
* 1. Inserts a `state='pending'` row in the appropriate table.
|
|
156
|
+
* 2. Returns a TUS URL + short-lived token the SDK will use to drive
|
|
157
|
+
* `tus-js-client` directly against Supabase Storage.
|
|
158
|
+
*
|
|
159
|
+
* The caller holds the `assetId` and `storagePath` for the subsequent
|
|
160
|
+
* finalize call.
|
|
161
|
+
*/
|
|
162
|
+
export declare function initAssetUpload(http: HttpClient, input: {
|
|
163
|
+
readonly kind: "skill" | "file";
|
|
164
|
+
readonly name: string;
|
|
165
|
+
readonly sizeBytes: number;
|
|
166
|
+
readonly hash: string;
|
|
167
|
+
}): Promise<AssetUploadInitResult>;
|
|
168
|
+
/**
|
|
169
|
+
* Finalise a chunked asset upload session. Calls the BFF's
|
|
170
|
+
* `POST /api/assets/finalize` endpoint, which:
|
|
171
|
+
*
|
|
172
|
+
* 1. Downloads the assembled bytes from Supabase Storage.
|
|
173
|
+
* 2. Verifies `sha256(bytes) === hash`.
|
|
174
|
+
* 3. Transitions the row `pending → ready`.
|
|
175
|
+
*
|
|
176
|
+
* Throws if the server returns a non-OK response (e.g. `hash_mismatch`).
|
|
177
|
+
*/
|
|
178
|
+
export declare function finalizeAssetUpload(http: HttpClient, input: {
|
|
179
|
+
readonly assetId: string;
|
|
180
|
+
readonly kind: "skill" | "file";
|
|
181
|
+
readonly hash: string;
|
|
182
|
+
readonly storagePath: string;
|
|
183
|
+
}): Promise<void>;
|
|
@@ -83,21 +83,26 @@ export async function submitRunFlat(http, request) {
|
|
|
83
83
|
}
|
|
84
84
|
/**
|
|
85
85
|
* Multipart variant of `submitRunFlat` for runs that carry transient
|
|
86
|
-
* (per-run) skill bundles
|
|
87
|
-
* `submission` part; each `TransientSkillRef.slot` in
|
|
88
|
-
* `request.submission.skills` MUST be mirrored by exactly one
|
|
89
|
-
* `skill:<slot>` part with the bundle bytes.
|
|
86
|
+
* (per-run) skill bundles and/or transient AgentsMd content.
|
|
90
87
|
*
|
|
91
|
-
* The
|
|
92
|
-
*
|
|
93
|
-
*
|
|
88
|
+
* The JSON submission travels as the `submission` part; each
|
|
89
|
+
* `TransientSkillRef.slot` in `request.submission.skills` MUST be
|
|
90
|
+
* mirrored by exactly one `skill:<slot>` part with the bundle bytes.
|
|
91
|
+
* Each `TransientAgentsMdRef.slot` in `request.submission.agentsMd`
|
|
92
|
+
* MUST be mirrored by exactly one `agentsmd:<slot>` part with the
|
|
93
|
+
* markdown text.
|
|
94
94
|
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
95
|
+
* The BFF re-canonicalises and re-hashes each bundle/file; a
|
|
96
|
+
* `contentHash` mismatch is rejected with a deterministic error.
|
|
97
|
+
*
|
|
98
|
+
* At least one of `bundles` or `agentsMdParts` must be non-empty.
|
|
97
99
|
*/
|
|
98
|
-
export async function submitRunFlatMultipart(http, request, bundles) {
|
|
99
|
-
|
|
100
|
-
|
|
100
|
+
export async function submitRunFlatMultipart(http, request, bundles, agentsMdParts, fileParts) {
|
|
101
|
+
const hasBundles = Array.isArray(bundles) && bundles.length > 0;
|
|
102
|
+
const hasAgentsMd = Array.isArray(agentsMdParts) && agentsMdParts.length > 0;
|
|
103
|
+
const hasFiles = Array.isArray(fileParts) && fileParts.length > 0;
|
|
104
|
+
if (!hasBundles && !hasAgentsMd && !hasFiles) {
|
|
105
|
+
throw new Error("submitRunFlatMultipart: bundles, agentsMdParts, or fileParts must be non-empty");
|
|
101
106
|
}
|
|
102
107
|
const form = new FormData();
|
|
103
108
|
// Submission rides as a typed JSON Blob so the BFF reads
|
|
@@ -116,6 +121,30 @@ export async function submitRunFlatMultipart(http, request, bundles) {
|
|
|
116
121
|
const blob = toBlob(bundle.bytes, "application/zip");
|
|
117
122
|
form.append(`skill:${bundle.slot}`, blob, bundle.filename);
|
|
118
123
|
}
|
|
124
|
+
for (const part of agentsMdParts ?? []) {
|
|
125
|
+
if (typeof part.slot !== "string" || !part.slot) {
|
|
126
|
+
throw new Error("submitRunFlatMultipart: each agentsMd part must have a non-empty slot id");
|
|
127
|
+
}
|
|
128
|
+
const partKey = `agentsmd:${part.slot}`;
|
|
129
|
+
if (seen.has(partKey)) {
|
|
130
|
+
throw new Error(`submitRunFlatMultipart: duplicate agentsMd slot "${part.slot}"`);
|
|
131
|
+
}
|
|
132
|
+
seen.add(partKey);
|
|
133
|
+
const blob = new Blob([part.content], { type: "text/plain" });
|
|
134
|
+
form.append(partKey, blob, part.filename);
|
|
135
|
+
}
|
|
136
|
+
for (const part of fileParts ?? []) {
|
|
137
|
+
if (typeof part.slot !== "string" || !part.slot) {
|
|
138
|
+
throw new Error("submitRunFlatMultipart: each file part must have a non-empty slot id");
|
|
139
|
+
}
|
|
140
|
+
const partKey = `file:${part.slot}`;
|
|
141
|
+
if (seen.has(partKey)) {
|
|
142
|
+
throw new Error(`submitRunFlatMultipart: duplicate file slot "${part.slot}"`);
|
|
143
|
+
}
|
|
144
|
+
seen.add(partKey);
|
|
145
|
+
const blob = toBlob(part.bytes, "application/zip");
|
|
146
|
+
form.append(partKey, blob, part.filename);
|
|
147
|
+
}
|
|
119
148
|
return http.request("/api/runs", {
|
|
120
149
|
method: "POST",
|
|
121
150
|
body: form
|
|
@@ -183,6 +212,80 @@ export async function findSkillByName(http, name) {
|
|
|
183
212
|
const skills = await listSkills(http);
|
|
184
213
|
return skills.find((skill) => skill.name === name) ?? null;
|
|
185
214
|
}
|
|
215
|
+
// ===========================================================================
|
|
216
|
+
// AgentsMd (workspace_files kind='agentsmd') operations
|
|
217
|
+
// ===========================================================================
|
|
218
|
+
/**
|
|
219
|
+
* Upload a workspace AgentsMd file as a markdown string. The BFF
|
|
220
|
+
* canonicalises the content into a deterministic zip with AGENTS.md at
|
|
221
|
+
* root and runs the two-phase pending → ready upload.
|
|
222
|
+
*/
|
|
223
|
+
export async function createAgentsMd(http, args) {
|
|
224
|
+
const form = new FormData();
|
|
225
|
+
form.append("name", args.name);
|
|
226
|
+
form.append("content", new Blob([args.content], { type: "text/plain" }), "AGENTS.md");
|
|
227
|
+
const result = await http.request("/api/agentsmd", { method: "POST", body: form });
|
|
228
|
+
return unwrapAgentsMd(result);
|
|
229
|
+
}
|
|
230
|
+
export async function listAgentsMd(http) {
|
|
231
|
+
const result = await http.request("/api/agentsmd");
|
|
232
|
+
if (Array.isArray(result)) {
|
|
233
|
+
return result;
|
|
234
|
+
}
|
|
235
|
+
return result.agentsMd;
|
|
236
|
+
}
|
|
237
|
+
export async function getAgentsMd(http, agentsMdId) {
|
|
238
|
+
const result = await http.request(`/api/agentsmd/${encodeURIComponent(agentsMdId)}`);
|
|
239
|
+
return unwrapAgentsMd(result);
|
|
240
|
+
}
|
|
241
|
+
export async function deleteAgentsMd(http, agentsMdId) {
|
|
242
|
+
await http.request(`/api/agentsmd/${encodeURIComponent(agentsMdId)}`, {
|
|
243
|
+
method: "DELETE"
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
function unwrapAgentsMd(result) {
|
|
247
|
+
if (result && typeof result === "object" && "agentsMd" in result) {
|
|
248
|
+
return result.agentsMd;
|
|
249
|
+
}
|
|
250
|
+
return result;
|
|
251
|
+
}
|
|
252
|
+
// ===========================================================================
|
|
253
|
+
// File (workspace_files kind='file') operations
|
|
254
|
+
// ===========================================================================
|
|
255
|
+
/**
|
|
256
|
+
* Upload a workspace File as a zip bundle. The BFF canonicalises the
|
|
257
|
+
* content and runs the two-phase pending → ready upload.
|
|
258
|
+
*/
|
|
259
|
+
export async function createFile(http, args) {
|
|
260
|
+
const form = new FormData();
|
|
261
|
+
form.append("name", args.name);
|
|
262
|
+
const blob = toBlob(args.bytes, "application/zip");
|
|
263
|
+
form.append("bundle", blob, `${args.name}.zip`);
|
|
264
|
+
const result = await http.request("/api/files", { method: "POST", body: form });
|
|
265
|
+
return unwrapFile(result);
|
|
266
|
+
}
|
|
267
|
+
export async function listFiles(http) {
|
|
268
|
+
const result = await http.request("/api/files");
|
|
269
|
+
if (Array.isArray(result)) {
|
|
270
|
+
return result;
|
|
271
|
+
}
|
|
272
|
+
return result.files;
|
|
273
|
+
}
|
|
274
|
+
export async function getFile(http, fileId) {
|
|
275
|
+
const result = await http.request(`/api/files/${encodeURIComponent(fileId)}`);
|
|
276
|
+
return unwrapFile(result);
|
|
277
|
+
}
|
|
278
|
+
export async function deleteFile(http, fileId) {
|
|
279
|
+
await http.request(`/api/files/${encodeURIComponent(fileId)}`, {
|
|
280
|
+
method: "DELETE"
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
function unwrapFile(result) {
|
|
284
|
+
if (result && typeof result === "object" && "file" in result) {
|
|
285
|
+
return result.file;
|
|
286
|
+
}
|
|
287
|
+
return result;
|
|
288
|
+
}
|
|
186
289
|
function unwrapSkill(result) {
|
|
187
290
|
if (result && typeof result === "object" && "skill" in result) {
|
|
188
291
|
return result.skill;
|
|
@@ -206,4 +309,37 @@ function toBlob(input, contentType) {
|
|
|
206
309
|
function hasRun(value) {
|
|
207
310
|
return Boolean(value && typeof value === "object" && "run" in value);
|
|
208
311
|
}
|
|
312
|
+
/**
|
|
313
|
+
* Initialise a chunked asset upload session. Calls the BFF's
|
|
314
|
+
* `POST /api/assets/upload-init` endpoint, which:
|
|
315
|
+
*
|
|
316
|
+
* 1. Inserts a `state='pending'` row in the appropriate table.
|
|
317
|
+
* 2. Returns a TUS URL + short-lived token the SDK will use to drive
|
|
318
|
+
* `tus-js-client` directly against Supabase Storage.
|
|
319
|
+
*
|
|
320
|
+
* The caller holds the `assetId` and `storagePath` for the subsequent
|
|
321
|
+
* finalize call.
|
|
322
|
+
*/
|
|
323
|
+
export async function initAssetUpload(http, input) {
|
|
324
|
+
return http.request("/api/assets/upload-init", {
|
|
325
|
+
method: "POST",
|
|
326
|
+
body: JSON.stringify(input)
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Finalise a chunked asset upload session. Calls the BFF's
|
|
331
|
+
* `POST /api/assets/finalize` endpoint, which:
|
|
332
|
+
*
|
|
333
|
+
* 1. Downloads the assembled bytes from Supabase Storage.
|
|
334
|
+
* 2. Verifies `sha256(bytes) === hash`.
|
|
335
|
+
* 3. Transitions the row `pending → ready`.
|
|
336
|
+
*
|
|
337
|
+
* Throws if the server returns a non-OK response (e.g. `hash_mismatch`).
|
|
338
|
+
*/
|
|
339
|
+
export async function finalizeAssetUpload(http, input) {
|
|
340
|
+
await http.request("/api/assets/finalize", {
|
|
341
|
+
method: "POST",
|
|
342
|
+
body: JSON.stringify(input)
|
|
343
|
+
});
|
|
344
|
+
}
|
|
209
345
|
//# sourceMappingURL=operations.js.map
|
|
@@ -122,6 +122,53 @@ export interface Skill {
|
|
|
122
122
|
readonly deletedAt?: string | null;
|
|
123
123
|
readonly [key: string]: unknown;
|
|
124
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* Wire-level record for a workspace AgentsMd file as returned by the BFF.
|
|
127
|
+
* Mirrors `PublicWorkspaceFile` from the dashboard service layer.
|
|
128
|
+
*/
|
|
129
|
+
export interface AgentsMdRecord {
|
|
130
|
+
readonly id: string;
|
|
131
|
+
readonly kind?: "agentsmd";
|
|
132
|
+
readonly name: string;
|
|
133
|
+
readonly state: "pending" | "ready";
|
|
134
|
+
readonly hash?: string | null;
|
|
135
|
+
readonly sizeBytes?: number | null;
|
|
136
|
+
readonly fileCount?: number | null;
|
|
137
|
+
readonly manifest?: ReadonlyArray<{
|
|
138
|
+
readonly path: string;
|
|
139
|
+
readonly size: number;
|
|
140
|
+
readonly mode: number;
|
|
141
|
+
}>;
|
|
142
|
+
readonly createdAt?: string;
|
|
143
|
+
readonly updatedAt?: string;
|
|
144
|
+
readonly finalizedAt?: string | null;
|
|
145
|
+
readonly deletedAt?: string | null;
|
|
146
|
+
readonly [key: string]: unknown;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Wire-level record for a workspace File as returned by the BFF.
|
|
150
|
+
* Mirrors `PublicWorkspaceFile` from the dashboard service layer
|
|
151
|
+
* with kind='file' and `f_*` ids.
|
|
152
|
+
*/
|
|
153
|
+
export interface FileRecord {
|
|
154
|
+
readonly id: string;
|
|
155
|
+
readonly kind?: "file";
|
|
156
|
+
readonly name: string;
|
|
157
|
+
readonly state: "pending" | "ready";
|
|
158
|
+
readonly hash?: string | null;
|
|
159
|
+
readonly sizeBytes?: number | null;
|
|
160
|
+
readonly fileCount?: number | null;
|
|
161
|
+
readonly manifest?: ReadonlyArray<{
|
|
162
|
+
readonly path: string;
|
|
163
|
+
readonly size: number;
|
|
164
|
+
readonly mode: number;
|
|
165
|
+
}>;
|
|
166
|
+
readonly createdAt?: string;
|
|
167
|
+
readonly updatedAt?: string;
|
|
168
|
+
readonly finalizedAt?: string | null;
|
|
169
|
+
readonly deletedAt?: string | null;
|
|
170
|
+
readonly [key: string]: unknown;
|
|
171
|
+
}
|
|
125
172
|
/**
|
|
126
173
|
* Full submission as the SDK and CLI assemble it before posting. The
|
|
127
174
|
* `template` is the SDK-level ResolvedTemplate (or a JSON-encoded one);
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase C: Chunked upload protocol for large assets.
|
|
3
|
+
*
|
|
4
|
+
* Two upload strategies:
|
|
5
|
+
*
|
|
6
|
+
* - **small** (≤ 6 MiB): existing single-request multipart POST to
|
|
7
|
+
* `/api/skills` or `/api/files`. No change to the existing path.
|
|
8
|
+
* - **chunked** (> 6 MiB): three-step TUS resumable upload:
|
|
9
|
+
* 1. `POST /api/assets/upload-init` — BFF mints a pending row +
|
|
10
|
+
* returns TUS URL + short-lived token.
|
|
11
|
+
* 2. Drive `tus-js-client` with `chunkSize: 6 MiB` against the
|
|
12
|
+
* Supabase TUS endpoint.
|
|
13
|
+
* 3. `POST /api/assets/finalize` — BFF verifies sha256 of the
|
|
14
|
+
* assembled bytes and transitions the row pending → ready.
|
|
15
|
+
*
|
|
16
|
+
* The threshold (6 MiB) matches the Supabase TUS requirement exactly:
|
|
17
|
+
* their endpoint requires chunk sizes of exactly 6 MiB for all chunks
|
|
18
|
+
* except the final one.
|
|
19
|
+
*
|
|
20
|
+
* `AgentsMd.upload` is NOT wired through chunked — a single markdown
|
|
21
|
+
* file will never exceed 6 MiB. Only `Skill.upload` and `File.upload`
|
|
22
|
+
* use the strategy chooser.
|
|
23
|
+
*/
|
|
24
|
+
/** Exact threshold in bytes: 6 MiB. */
|
|
25
|
+
export declare const CHUNKED_UPLOAD_THRESHOLD_BYTES: number;
|
|
26
|
+
/** Exact chunk size: 6 MiB. Supabase TUS requires this exactly. */
|
|
27
|
+
export declare const TUS_CHUNK_SIZE: number;
|
|
28
|
+
/**
|
|
29
|
+
* Choose the upload strategy based on bundle size.
|
|
30
|
+
*
|
|
31
|
+
* - `<= 6 MiB` → `"small"` — existing single-request multipart POST.
|
|
32
|
+
* - `> 6 MiB` → `"chunked"` — three-step TUS resumable upload.
|
|
33
|
+
*
|
|
34
|
+
* The boundary is inclusive at 6 MiB: a bundle of exactly 6,291,456 bytes
|
|
35
|
+
* takes the small path; one byte more takes the chunked path.
|
|
36
|
+
*/
|
|
37
|
+
export declare function chooseUploadStrategy(sizeBytes: number): "small" | "chunked";
|
|
38
|
+
/**
|
|
39
|
+
* Arguments for `uploadChunked`. The caller provides the bundle bytes,
|
|
40
|
+
* the TUS URL, the token, and the advisory hash that was computed
|
|
41
|
+
* before the upload session was opened (so the finalize endpoint can
|
|
42
|
+
* verify integrity without re-downloading).
|
|
43
|
+
*/
|
|
44
|
+
export interface UploadChunkedArgs {
|
|
45
|
+
/** The raw bytes to upload (canonical zip). */
|
|
46
|
+
readonly bundle: Uint8Array;
|
|
47
|
+
/** The TUS endpoint URL returned by the BFF's upload-init endpoint. */
|
|
48
|
+
readonly tusUrl: string;
|
|
49
|
+
/**
|
|
50
|
+
* Short-lived token returned by upload-init. Placed in the
|
|
51
|
+
* `Authorization` header of every TUS request so Supabase
|
|
52
|
+
* Storage accepts the upload.
|
|
53
|
+
*/
|
|
54
|
+
readonly tusToken: string;
|
|
55
|
+
/**
|
|
56
|
+
* Additional per-upload headers returned by the BFF (e.g.
|
|
57
|
+
* `x-upsert`, `content-type`). These travel verbatim on every TUS
|
|
58
|
+
* PATCH request.
|
|
59
|
+
*/
|
|
60
|
+
readonly uploadHeaders?: Readonly<Record<string, string>>;
|
|
61
|
+
/**
|
|
62
|
+
* Advisory `sha256:<hex>` hash of the bundle bytes. Verified
|
|
63
|
+
* BEFORE any bytes are pushed to the TUS endpoint — a mismatch
|
|
64
|
+
* throws early so we never PATCH bytes that don't match what was
|
|
65
|
+
* declared to the BFF during init.
|
|
66
|
+
*/
|
|
67
|
+
readonly hash: string;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Drive `tus-js-client` to upload `bundle` in 6 MiB chunks.
|
|
71
|
+
*
|
|
72
|
+
* Pre-upload steps:
|
|
73
|
+
* 1. Verify `sha256(bundle) === args.hash`. Throws if they don't match.
|
|
74
|
+
*
|
|
75
|
+
* Upload:
|
|
76
|
+
* 2. Build a `tus.Upload` with `chunkSize: TUS_CHUNK_SIZE`, the
|
|
77
|
+
* supplied auth header, and the extra headers the BFF returned.
|
|
78
|
+
* 3. Await the upload completion via a Promise wrapper around the
|
|
79
|
+
* `onSuccess` / `onError` callbacks.
|
|
80
|
+
*
|
|
81
|
+
* The function does NOT call the BFF finalize endpoint — that is the
|
|
82
|
+
* caller's responsibility (so the caller can attach extra metadata like
|
|
83
|
+
* `assetId`, `kind`, etc.).
|
|
84
|
+
*/
|
|
85
|
+
export declare function uploadChunked(args: UploadChunkedArgs): Promise<void>;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase C: Chunked upload protocol for large assets.
|
|
3
|
+
*
|
|
4
|
+
* Two upload strategies:
|
|
5
|
+
*
|
|
6
|
+
* - **small** (≤ 6 MiB): existing single-request multipart POST to
|
|
7
|
+
* `/api/skills` or `/api/files`. No change to the existing path.
|
|
8
|
+
* - **chunked** (> 6 MiB): three-step TUS resumable upload:
|
|
9
|
+
* 1. `POST /api/assets/upload-init` — BFF mints a pending row +
|
|
10
|
+
* returns TUS URL + short-lived token.
|
|
11
|
+
* 2. Drive `tus-js-client` with `chunkSize: 6 MiB` against the
|
|
12
|
+
* Supabase TUS endpoint.
|
|
13
|
+
* 3. `POST /api/assets/finalize` — BFF verifies sha256 of the
|
|
14
|
+
* assembled bytes and transitions the row pending → ready.
|
|
15
|
+
*
|
|
16
|
+
* The threshold (6 MiB) matches the Supabase TUS requirement exactly:
|
|
17
|
+
* their endpoint requires chunk sizes of exactly 6 MiB for all chunks
|
|
18
|
+
* except the final one.
|
|
19
|
+
*
|
|
20
|
+
* `AgentsMd.upload` is NOT wired through chunked — a single markdown
|
|
21
|
+
* file will never exceed 6 MiB. Only `Skill.upload` and `File.upload`
|
|
22
|
+
* use the strategy chooser.
|
|
23
|
+
*/
|
|
24
|
+
import * as tus from "tus-js-client";
|
|
25
|
+
/** Exact threshold in bytes: 6 MiB. */
|
|
26
|
+
export const CHUNKED_UPLOAD_THRESHOLD_BYTES = 6 * 1024 * 1024;
|
|
27
|
+
/** Exact chunk size: 6 MiB. Supabase TUS requires this exactly. */
|
|
28
|
+
export const TUS_CHUNK_SIZE = 6 * 1024 * 1024;
|
|
29
|
+
/**
|
|
30
|
+
* Choose the upload strategy based on bundle size.
|
|
31
|
+
*
|
|
32
|
+
* - `<= 6 MiB` → `"small"` — existing single-request multipart POST.
|
|
33
|
+
* - `> 6 MiB` → `"chunked"` — three-step TUS resumable upload.
|
|
34
|
+
*
|
|
35
|
+
* The boundary is inclusive at 6 MiB: a bundle of exactly 6,291,456 bytes
|
|
36
|
+
* takes the small path; one byte more takes the chunked path.
|
|
37
|
+
*/
|
|
38
|
+
export function chooseUploadStrategy(sizeBytes) {
|
|
39
|
+
return sizeBytes <= CHUNKED_UPLOAD_THRESHOLD_BYTES ? "small" : "chunked";
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Drive `tus-js-client` to upload `bundle` in 6 MiB chunks.
|
|
43
|
+
*
|
|
44
|
+
* Pre-upload steps:
|
|
45
|
+
* 1. Verify `sha256(bundle) === args.hash`. Throws if they don't match.
|
|
46
|
+
*
|
|
47
|
+
* Upload:
|
|
48
|
+
* 2. Build a `tus.Upload` with `chunkSize: TUS_CHUNK_SIZE`, the
|
|
49
|
+
* supplied auth header, and the extra headers the BFF returned.
|
|
50
|
+
* 3. Await the upload completion via a Promise wrapper around the
|
|
51
|
+
* `onSuccess` / `onError` callbacks.
|
|
52
|
+
*
|
|
53
|
+
* The function does NOT call the BFF finalize endpoint — that is the
|
|
54
|
+
* caller's responsibility (so the caller can attach extra metadata like
|
|
55
|
+
* `assetId`, `kind`, etc.).
|
|
56
|
+
*/
|
|
57
|
+
export async function uploadChunked(args) {
|
|
58
|
+
// ── 1. Pre-upload hash verification ────────────────────────────────────
|
|
59
|
+
const recomputed = await computeSha256Hex(args.bundle);
|
|
60
|
+
const expected = args.hash.startsWith("sha256:")
|
|
61
|
+
? args.hash.slice("sha256:".length)
|
|
62
|
+
: args.hash;
|
|
63
|
+
if (recomputed !== expected) {
|
|
64
|
+
throw new Error(`uploadChunked: bundle hash mismatch — computed sha256:${recomputed} ` +
|
|
65
|
+
`but init-session declared ${args.hash}. Bytes do not match the advisory hash; ` +
|
|
66
|
+
`aborting to avoid uploading corrupted data.`);
|
|
67
|
+
}
|
|
68
|
+
// ── 2. Build tus.Upload and drive the upload ────────────────────────────
|
|
69
|
+
await new Promise((resolve, reject) => {
|
|
70
|
+
const headers = {
|
|
71
|
+
Authorization: `Bearer ${args.tusToken}`,
|
|
72
|
+
...(args.uploadHeaders ?? {})
|
|
73
|
+
};
|
|
74
|
+
// tus-js-client accepts a Buffer in Node environments.
|
|
75
|
+
const file = Buffer.from(args.bundle.buffer, args.bundle.byteOffset, args.bundle.byteLength);
|
|
76
|
+
const upload = new tus.Upload(file, {
|
|
77
|
+
endpoint: args.tusUrl,
|
|
78
|
+
chunkSize: TUS_CHUNK_SIZE,
|
|
79
|
+
headers,
|
|
80
|
+
// Do not retry automatically — the caller (Skill.upload / File.upload)
|
|
81
|
+
// owns retry semantics. retryDelays: null means "fail fast on error".
|
|
82
|
+
retryDelays: null,
|
|
83
|
+
// Disable fingerprint-based resumption — we don't persist state
|
|
84
|
+
// across process restarts in Phase C (scoped out per plan).
|
|
85
|
+
storeFingerprintForResuming: false,
|
|
86
|
+
onSuccess() {
|
|
87
|
+
resolve();
|
|
88
|
+
},
|
|
89
|
+
onError(err) {
|
|
90
|
+
reject(err);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
upload.start();
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// Internal helpers
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
/**
|
|
100
|
+
* Compute SHA-256 over `bytes` and return the lowercase hex digest.
|
|
101
|
+
* Uses Web Crypto when available (Node 18+ / browser), falls back to
|
|
102
|
+
* nothing — the SDK already requires Web Crypto for `hashSkillBundle`.
|
|
103
|
+
*/
|
|
104
|
+
async function computeSha256Hex(bytes) {
|
|
105
|
+
const subtle = globalThis.crypto?.subtle;
|
|
106
|
+
if (!subtle) {
|
|
107
|
+
throw new Error("uploadChunked: globalThis.crypto.subtle is not available; " +
|
|
108
|
+
"Node 18+ or a Web-Crypto-capable runtime is required");
|
|
109
|
+
}
|
|
110
|
+
const copy = new Uint8Array(bytes.byteLength);
|
|
111
|
+
copy.set(bytes);
|
|
112
|
+
const digest = await subtle.digest("SHA-256", copy.buffer);
|
|
113
|
+
return bufferToHex(digest);
|
|
114
|
+
}
|
|
115
|
+
function bufferToHex(buffer) {
|
|
116
|
+
const view = new Uint8Array(buffer);
|
|
117
|
+
let out = "";
|
|
118
|
+
for (let i = 0; i < view.length; i++) {
|
|
119
|
+
const byte = view[i];
|
|
120
|
+
out += byte.toString(16).padStart(2, "0");
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=asset-upload.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"asset-upload.js","sourceRoot":"","sources":["../src/asset-upload.ts"],"names":[],"mappings":"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"}
|