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.
@@ -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. The JSON submission travels as the
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 BFF re-canonicalises and re-hashes each bundle; a `contentHash`
61
- * mismatch between the JSON ref and the recomputed hash of the bytes
62
- * is rejected with a deterministic error.
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
- * Bytes never persist on antpath storage; the dashboard BFF uploads
65
- * them straight to Anthropic Files for the lifetime of the run.
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. The JSON submission travels as the
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 BFF re-canonicalises and re-hashes each bundle; a `contentHash`
92
- * mismatch between the JSON ref and the recomputed hash of the bytes
93
- * is rejected with a deterministic error.
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
- * Bytes never persist on antpath storage; the dashboard BFF uploads
96
- * them straight to Anthropic Files for the lifetime of the run.
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
- if (!Array.isArray(bundles) || bundles.length === 0) {
100
- throw new Error("submitRunFlatMultipart: bundles must be a non-empty array");
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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"}