@mountra/mountra-sdk 0.1.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/dist/index.cjs ADDED
@@ -0,0 +1,751 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
+ DEFAULT_S3_MULTIPART_PART_SIZE_BYTES: () => DEFAULT_S3_MULTIPART_PART_SIZE_BYTES,
24
+ InMemoryJumboChunkCheckpointStore: () => InMemoryJumboChunkCheckpointStore,
25
+ JUMBO_CHUNK_THRESHOLD_BYTES: () => JUMBO_CHUNK_THRESHOLD_BYTES,
26
+ LocalStorageJumboChunkCheckpointStore: () => LocalStorageJumboChunkCheckpointStore,
27
+ MAX_S3_MULTIPART_PARTS: () => MAX_S3_MULTIPART_PARTS,
28
+ MIN_S3_MULTIPART_PART_SIZE_BYTES: () => MIN_S3_MULTIPART_PART_SIZE_BYTES,
29
+ MountraApiError: () => MountraApiError,
30
+ MountraClient: () => MountraClient,
31
+ MountraSDK: () => MountraClient,
32
+ S3MultipartUploader: () => S3MultipartUploader,
33
+ createMountraClient: () => createMountraClient,
34
+ isJumboChunk: () => isJumboChunk,
35
+ sha256Hex: () => sha256Hex,
36
+ splitFileIntoChunks: () => splitFileIntoChunks
37
+ });
38
+ module.exports = __toCommonJS(src_exports);
39
+
40
+ // src/upload.ts
41
+ var JUMBO_CHUNK_THRESHOLD_BYTES = 128 * 1024 * 1024;
42
+ var MIN_S3_MULTIPART_PART_SIZE_BYTES = 5 * 1024 * 1024;
43
+ var DEFAULT_S3_MULTIPART_PART_SIZE_BYTES = 16 * 1024 * 1024;
44
+ var MAX_S3_MULTIPART_PARTS = 1e4;
45
+ var MountraApiError = class extends Error {
46
+ status;
47
+ code;
48
+ body;
49
+ constructor(message, input) {
50
+ super(message);
51
+ this.name = "MountraApiError";
52
+ this.status = input.status;
53
+ this.code = input.code;
54
+ this.body = input.body;
55
+ }
56
+ };
57
+ var memoryCheckpointRecords = /* @__PURE__ */ new Map();
58
+ function parseJumboCheckpoint(value) {
59
+ if (!isRecord(value) || value.version !== 1 || typeof value.storageKey !== "string") {
60
+ return null;
61
+ }
62
+ if (typeof value.chunkIndex !== "number" || typeof value.chunkSize !== "number" || typeof value.chunkSha256 !== "string" || typeof value.partSize !== "number" || typeof value.uploadId !== "string" || !Array.isArray(value.parts)) {
63
+ return null;
64
+ }
65
+ const parts = [];
66
+ for (const part of value.parts) {
67
+ if (!isRecord(part)) {
68
+ return null;
69
+ }
70
+ if (typeof part.partNumber !== "number" || typeof part.etag !== "string" || typeof part.size !== "number" || typeof part.sha256 !== "string") {
71
+ return null;
72
+ }
73
+ parts.push({
74
+ partNumber: part.partNumber,
75
+ etag: part.etag,
76
+ size: part.size,
77
+ sha256: part.sha256
78
+ });
79
+ }
80
+ return {
81
+ version: 1,
82
+ storageKey: value.storageKey,
83
+ chunkIndex: value.chunkIndex,
84
+ chunkSize: value.chunkSize,
85
+ chunkSha256: value.chunkSha256,
86
+ partSize: value.partSize,
87
+ uploadId: value.uploadId,
88
+ completed: value.completed === true,
89
+ completing: value.completing === true,
90
+ parts
91
+ };
92
+ }
93
+ var InMemoryJumboChunkCheckpointStore = class {
94
+ records = /* @__PURE__ */ new Map();
95
+ async load(key) {
96
+ return this.records.get(key) ?? null;
97
+ }
98
+ async save(key, checkpoint) {
99
+ this.records.set(key, {
100
+ ...checkpoint,
101
+ parts: checkpoint.parts.map((part) => ({ ...part }))
102
+ });
103
+ }
104
+ async remove(key) {
105
+ this.records.delete(key);
106
+ }
107
+ };
108
+ var LocalStorageJumboChunkCheckpointStore = class {
109
+ prefix;
110
+ constructor(prefix = "mountra-sdk:jumbo:") {
111
+ this.prefix = prefix;
112
+ }
113
+ async load(key) {
114
+ const storageKey = this.storageKey(key);
115
+ const raw = this.getStorage()?.getItem(storageKey) ?? memoryCheckpointRecords.get(storageKey);
116
+ if (!raw) {
117
+ return null;
118
+ }
119
+ try {
120
+ return parseJumboCheckpoint(JSON.parse(raw));
121
+ } catch {
122
+ await this.remove(key);
123
+ return null;
124
+ }
125
+ }
126
+ async save(key, checkpoint) {
127
+ const storageKey = this.storageKey(key);
128
+ const serialized = JSON.stringify(checkpoint);
129
+ const storage = this.getStorage();
130
+ if (storage) {
131
+ storage.setItem(storageKey, serialized);
132
+ } else {
133
+ memoryCheckpointRecords.set(storageKey, serialized);
134
+ }
135
+ }
136
+ async remove(key) {
137
+ const storageKey = this.storageKey(key);
138
+ this.getStorage()?.removeItem(storageKey);
139
+ memoryCheckpointRecords.delete(storageKey);
140
+ }
141
+ storageKey(key) {
142
+ return `${this.prefix}${key}`;
143
+ }
144
+ getStorage() {
145
+ try {
146
+ return globalThis.localStorage ?? null;
147
+ } catch {
148
+ return null;
149
+ }
150
+ }
151
+ };
152
+ var S3MultipartUploader = class {
153
+ bucket;
154
+ client;
155
+ constructor(input) {
156
+ const bucket = input.bucket.trim();
157
+ if (!bucket) {
158
+ throw new TypeError("S3 bucket is required");
159
+ }
160
+ this.bucket = bucket;
161
+ this.client = input.client;
162
+ }
163
+ createMultipartUpload(input) {
164
+ return this.client.createMultipartUpload({
165
+ bucket: this.bucket,
166
+ key: input.storageKey,
167
+ contentType: input.contentType,
168
+ signal: input.signal
169
+ });
170
+ }
171
+ uploadPart(input) {
172
+ return this.client.uploadPart({
173
+ bucket: this.bucket,
174
+ key: input.storageKey,
175
+ uploadId: input.uploadId,
176
+ partNumber: input.partNumber,
177
+ body: input.body,
178
+ signal: input.signal
179
+ });
180
+ }
181
+ completeMultipartUpload(input) {
182
+ return this.client.completeMultipartUpload({
183
+ bucket: this.bucket,
184
+ key: input.storageKey,
185
+ uploadId: input.uploadId,
186
+ parts: input.parts,
187
+ signal: input.signal
188
+ });
189
+ }
190
+ abortMultipartUpload(input) {
191
+ if (!this.client.abortMultipartUpload) {
192
+ return Promise.resolve();
193
+ }
194
+ return this.client.abortMultipartUpload({
195
+ bucket: this.bucket,
196
+ key: input.storageKey,
197
+ uploadId: input.uploadId,
198
+ signal: input.signal
199
+ });
200
+ }
201
+ };
202
+ function assertPositiveInteger(value, name) {
203
+ if (!Number.isSafeInteger(value) || value <= 0) {
204
+ throw new RangeError(`${name} must be a positive safe integer`);
205
+ }
206
+ }
207
+ function assertMultipartPartSize(value) {
208
+ assertPositiveInteger(value, "jumboPartSize");
209
+ if (value < MIN_S3_MULTIPART_PART_SIZE_BYTES) {
210
+ throw new RangeError(
211
+ `jumboPartSize must be at least ${MIN_S3_MULTIPART_PART_SIZE_BYTES} bytes for S3 Multipart uploads`
212
+ );
213
+ }
214
+ }
215
+ function normalizeBaseUrl(value) {
216
+ const trimmed = value.trim().replace(/\/+$/, "");
217
+ if (!trimmed) {
218
+ throw new TypeError("baseUrl is required");
219
+ }
220
+ return trimmed;
221
+ }
222
+ function normalizeApiPrefix(value) {
223
+ const trimmed = value.trim();
224
+ if (!trimmed || trimmed === "/") {
225
+ return "";
226
+ }
227
+ return `/${trimmed.replace(/^\/+|\/+$/g, "")}`;
228
+ }
229
+ function getWorkspaceBody(clientSelector, requestSelector) {
230
+ const workspaceCuid = requestSelector.workspaceCuid ?? clientSelector.workspaceCuid;
231
+ const workspaceId = requestSelector.workspaceId ?? clientSelector.workspaceId;
232
+ if (workspaceCuid === void 0 && workspaceId === void 0) {
233
+ throw new TypeError("workspaceCuid or workspaceId is required");
234
+ }
235
+ return {
236
+ ...workspaceCuid !== void 0 ? { workspace_cuid: workspaceCuid } : {},
237
+ ...workspaceId !== void 0 ? { workspace_id: workspaceId } : {}
238
+ };
239
+ }
240
+ function throwIfAborted(signal) {
241
+ if (!signal?.aborted) {
242
+ return;
243
+ }
244
+ throw new DOMException("The upload was aborted", "AbortError");
245
+ }
246
+ function isRecord(value) {
247
+ return typeof value === "object" && value !== null;
248
+ }
249
+ function parseApiErrorBody(body) {
250
+ if (!isRecord(body)) {
251
+ return {};
252
+ }
253
+ return {
254
+ message: typeof body.error === "string" ? body.error : void 0,
255
+ code: typeof body.code === "string" ? body.code : void 0
256
+ };
257
+ }
258
+ function ensurePrepareResponse(value) {
259
+ if (!isRecord(value) || typeof value.upload_id !== "string") {
260
+ throw new TypeError("Mountra prepare response is invalid");
261
+ }
262
+ if (!Array.isArray(value.upload_targets)) {
263
+ throw new TypeError("Mountra prepare response has no upload targets");
264
+ }
265
+ return value;
266
+ }
267
+ function ensureFinishResponse(value) {
268
+ if (!isRecord(value) || typeof value.upload_id !== "string") {
269
+ throw new TypeError("Mountra finish response is invalid");
270
+ }
271
+ return value;
272
+ }
273
+ function ensureUploadStatusResponse(value) {
274
+ if (!isRecord(value) || typeof value.upload_id !== "string") {
275
+ throw new TypeError("Mountra upload status response is invalid");
276
+ }
277
+ if (!Array.isArray(value.chunks)) {
278
+ throw new TypeError("Mountra upload status response has no chunk states");
279
+ }
280
+ return value;
281
+ }
282
+ function splitFileIntoChunks(file, chunkCount = 1) {
283
+ assertPositiveInteger(chunkCount, "chunkCount");
284
+ if (file.size === 0) {
285
+ return [{ index: 0, offset: 0, size: 0, blob: file.slice(0, 0) }];
286
+ }
287
+ if (chunkCount > file.size) {
288
+ throw new RangeError("chunkCount cannot be greater than the file size in bytes");
289
+ }
290
+ const chunkSize = Math.ceil(file.size / chunkCount);
291
+ const chunks = [];
292
+ for (let index = 0; index < chunkCount; index += 1) {
293
+ const offset = index * chunkSize;
294
+ const end = Math.min(offset + chunkSize, file.size);
295
+ chunks.push({
296
+ index,
297
+ offset,
298
+ size: end - offset,
299
+ blob: file.slice(offset, end)
300
+ });
301
+ }
302
+ return chunks;
303
+ }
304
+ function isJumboChunk(size) {
305
+ return size > JUMBO_CHUNK_THRESHOLD_BYTES;
306
+ }
307
+ async function sha256Hex(blob) {
308
+ const subtle = globalThis.crypto?.subtle;
309
+ if (!subtle) {
310
+ throw new Error("Web Crypto SHA-256 is not available in this environment");
311
+ }
312
+ const digest = await subtle.digest("SHA-256", await blob.arrayBuffer());
313
+ return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
314
+ }
315
+ var MountraClient = class {
316
+ baseUrl;
317
+ apiPrefix;
318
+ clientSelector;
319
+ token;
320
+ headers;
321
+ fetchImpl;
322
+ jumboChunkUploader;
323
+ jumboChunkCheckpointStore;
324
+ jumboPartSize;
325
+ constructor(options) {
326
+ this.baseUrl = normalizeBaseUrl(options.baseUrl);
327
+ this.apiPrefix = normalizeApiPrefix(options.apiPrefix ?? "/api");
328
+ this.clientSelector = {
329
+ workspaceCuid: options.workspaceCuid,
330
+ workspaceId: options.workspaceId
331
+ };
332
+ this.token = options.token;
333
+ this.headers = options.headers;
334
+ this.jumboChunkUploader = options.jumboChunkUploader;
335
+ this.jumboChunkCheckpointStore = options.jumboChunkCheckpointStore ?? new LocalStorageJumboChunkCheckpointStore();
336
+ this.jumboPartSize = options.jumboPartSize ?? DEFAULT_S3_MULTIPART_PART_SIZE_BYTES;
337
+ assertMultipartPartSize(this.jumboPartSize);
338
+ const fetchImpl = options.fetch ?? globalThis.fetch?.bind(globalThis);
339
+ if (!fetchImpl) {
340
+ throw new Error("fetch is not available; provide fetch in MountraClientOptions");
341
+ }
342
+ this.fetchImpl = fetchImpl;
343
+ }
344
+ async prepareUpload(file, options) {
345
+ const chunks = splitFileIntoChunks(file, options.chunkCount ?? 1);
346
+ const chunkHashes = await Promise.all(chunks.map((chunk) => sha256Hex(chunk.blob)));
347
+ const contentHash = await sha256Hex(file);
348
+ const chunkMethod = chunks.length === 1 ? "none" : "fix";
349
+ const contentType = options.contentType ?? ("type" in file ? file.type : void 0);
350
+ throwIfAborted(options.signal);
351
+ const response = ensurePrepareResponse(
352
+ await this.requestJson("/upload/prepare", {
353
+ ...getWorkspaceBody(this.clientSelector, options),
354
+ path: options.path,
355
+ size: file.size,
356
+ content_hash: contentHash,
357
+ chunks: chunks.map((chunk, index) => ({
358
+ chunk_index: chunk.index,
359
+ size: chunk.size,
360
+ sha256: chunkHashes[index]
361
+ })),
362
+ chunk_method: chunkMethod,
363
+ ...contentType ? { content_type: contentType } : {},
364
+ ...options.nameConflictStrategy ? { name_conflict_strategy: options.nameConflictStrategy } : {}
365
+ }, options.signal)
366
+ );
367
+ const context = {
368
+ uploadId: response.upload_id,
369
+ path: response.path,
370
+ fileSize: file.size,
371
+ contentHash,
372
+ chunkMethod: response.chunk_method,
373
+ chunks: chunks.map((chunk, index) => ({
374
+ index: chunk.index,
375
+ offset: chunk.offset,
376
+ size: chunk.size,
377
+ sha256: chunkHashes[index]
378
+ })),
379
+ ...contentType ? { contentType } : {}
380
+ };
381
+ await options.onSessionPrepared?.(context);
382
+ return { response, chunks, chunkHashes, contentHash, context };
383
+ }
384
+ async uploadFile(file, options) {
385
+ const prepared = await this.prepareUpload(file, options);
386
+ return this.uploadPreparedChunks(file, prepared.context, prepared.chunks, prepared.response.upload_targets, options);
387
+ }
388
+ /** Generic upload entry point. It selects PUT or S3 Multipart per chunk. */
389
+ async upload(file, options) {
390
+ return this.uploadFile(file, options);
391
+ }
392
+ async getUploadStatus(uploadId, selector = {}, signal) {
393
+ if (!uploadId.trim()) {
394
+ throw new TypeError("uploadId is required");
395
+ }
396
+ return ensureUploadStatusResponse(
397
+ await this.requestJson("/upload/status", {
398
+ ...getWorkspaceBody(this.clientSelector, selector),
399
+ upload_id: uploadId
400
+ }, signal)
401
+ );
402
+ }
403
+ /**
404
+ * Resume a previously prepared session. The caller must restore the same
405
+ * file/blob separately; this context only describes how that file was cut.
406
+ */
407
+ async resumeUpload(file, context, options = {}) {
408
+ this.assertUploadContext(context, file);
409
+ const actualChunkHashes = await Promise.all(
410
+ context.chunks.map((chunk) => sha256Hex(file.slice(chunk.offset, chunk.offset + chunk.size)))
411
+ );
412
+ if (actualChunkHashes.some(
413
+ (hash, index) => hash !== context.chunks[index]?.sha256
414
+ ) || await sha256Hex(file) !== context.contentHash) {
415
+ throw new Error("The restored file does not match the upload context hashes");
416
+ }
417
+ const chunks = context.chunks.map((chunk) => ({
418
+ index: chunk.index,
419
+ offset: chunk.offset,
420
+ size: chunk.size,
421
+ blob: file.slice(chunk.offset, chunk.offset + chunk.size)
422
+ }));
423
+ const status = await this.getUploadStatus(context.uploadId, options, options.signal);
424
+ if (status.upload_id !== context.uploadId || status.size !== context.fileSize) {
425
+ throw new Error("Mountra upload status does not match the saved upload context");
426
+ }
427
+ if (status.chunks.length !== context.chunks.length) {
428
+ throw new Error("Mountra upload status has a different chunk layout");
429
+ }
430
+ for (const [index, chunk] of status.chunks.entries()) {
431
+ const savedChunk = context.chunks[index];
432
+ if (chunk.chunk_index !== savedChunk.index || chunk.offset !== savedChunk.offset || chunk.size !== savedChunk.size || chunk.sha256 !== savedChunk.sha256) {
433
+ throw new Error("Mountra upload status has a different chunk layout");
434
+ }
435
+ }
436
+ return this.uploadPreparedChunks(file, context, chunks, status.chunks, options);
437
+ }
438
+ async uploadPreparedChunks(file, context, chunks, targets, options) {
439
+ const orderedTargets = [...targets].sort((left, right) => left.chunk_index - right.chunk_index);
440
+ if (orderedTargets.length !== chunks.length) {
441
+ throw new Error(
442
+ `Mountra returned ${orderedTargets.length} upload targets for ${chunks.length} chunks`
443
+ );
444
+ }
445
+ let uploadedBytes = 0;
446
+ let uploadedChunks = 0;
447
+ const jumboCheckpointKeys = /* @__PURE__ */ new Set();
448
+ for (const [position, target] of orderedTargets.entries()) {
449
+ const chunk = chunks[target.chunk_index];
450
+ if (!chunk || target.chunk_index !== position) {
451
+ throw new Error("Mountra upload targets must contain contiguous chunk indexes");
452
+ }
453
+ throwIfAborted(options.signal);
454
+ if (isJumboChunk(chunk.size)) {
455
+ const checkpointKey = this.buildJumboCheckpointKey(context, target, options);
456
+ jumboCheckpointKeys.add(checkpointKey);
457
+ if (target.upload_required) {
458
+ await this.uploadJumboChunk(
459
+ chunk,
460
+ target,
461
+ context,
462
+ checkpointKey,
463
+ options,
464
+ uploadedBytes,
465
+ uploadedChunks,
466
+ orderedTargets.length
467
+ );
468
+ }
469
+ } else if (target.upload_required) {
470
+ if (!target.put_url) {
471
+ throw new Error(`Mountra upload target ${target.chunk_index} has no signed URL`);
472
+ }
473
+ const response = await this.fetchImpl(target.put_url, {
474
+ method: target.put_method ?? "PUT",
475
+ headers: target.put_headers ?? {},
476
+ body: chunk.blob,
477
+ ...options.signal ? { signal: options.signal } : {}
478
+ });
479
+ if (!response.ok) {
480
+ throw new MountraApiError(
481
+ `Signed upload for chunk ${target.chunk_index} failed with HTTP ${response.status}`,
482
+ { status: response.status }
483
+ );
484
+ }
485
+ }
486
+ uploadedBytes += chunk.size;
487
+ uploadedChunks += 1;
488
+ options.onProgress?.({
489
+ phase: "uploading",
490
+ uploadedBytes,
491
+ totalBytes: file.size,
492
+ uploadedChunks,
493
+ totalChunks: orderedTargets.length
494
+ });
495
+ }
496
+ throwIfAborted(options.signal);
497
+ options.onProgress?.({
498
+ phase: "finishing",
499
+ uploadedBytes: file.size,
500
+ totalBytes: file.size,
501
+ uploadedChunks: orderedTargets.length,
502
+ totalChunks: orderedTargets.length
503
+ });
504
+ const finished = ensureFinishResponse(
505
+ await this.requestJson(
506
+ "/upload/finish",
507
+ {
508
+ ...getWorkspaceBody(this.clientSelector, options),
509
+ upload_id: context.uploadId
510
+ },
511
+ options.signal
512
+ )
513
+ );
514
+ if (finished.finish_status !== "completed") {
515
+ throw new Error(`Mountra upload did not finish successfully: ${finished.finish_status}`);
516
+ }
517
+ await Promise.all(
518
+ [...jumboCheckpointKeys].map(
519
+ (checkpointKey) => this.jumboChunkCheckpointStore.remove(checkpointKey)
520
+ )
521
+ );
522
+ return finished;
523
+ }
524
+ buildJumboCheckpointKey(context, target, selector) {
525
+ const workspaceCuid = selector.workspaceCuid ?? this.clientSelector.workspaceCuid ?? "";
526
+ const workspaceId = selector.workspaceId ?? this.clientSelector.workspaceId ?? "";
527
+ return encodeURIComponent(
528
+ [
529
+ this.baseUrl,
530
+ workspaceCuid,
531
+ workspaceId,
532
+ target.storage_key,
533
+ target.chunk_index,
534
+ context.chunks[target.chunk_index]?.sha256 ?? ""
535
+ ].join("|")
536
+ );
537
+ }
538
+ async uploadJumboChunk(chunk, target, context, checkpointKey, options, baseUploadedBytes, uploadedChunks, totalChunks) {
539
+ const uploader = this.jumboChunkUploader;
540
+ if (!uploader) {
541
+ throw new Error(
542
+ `Chunk ${target.chunk_index} is larger than 128 MiB; configure jumboChunkUploader to use S3 Multipart uploads`
543
+ );
544
+ }
545
+ const totalParts = Math.ceil(chunk.size / this.jumboPartSize);
546
+ if (totalParts > MAX_S3_MULTIPART_PARTS) {
547
+ throw new RangeError(
548
+ `Jumbo chunk requires ${totalParts} parts, exceeding S3's ${MAX_S3_MULTIPART_PARTS}-part limit`
549
+ );
550
+ }
551
+ let checkpoint = await this.jumboChunkCheckpointStore.load(checkpointKey);
552
+ if (checkpoint && !this.isCompatibleJumboCheckpoint(checkpoint, chunk, target, context)) {
553
+ await this.jumboChunkCheckpointStore.remove(checkpointKey);
554
+ checkpoint = null;
555
+ }
556
+ if (!checkpoint) {
557
+ const created = await uploader.createMultipartUpload({
558
+ storageKey: target.storage_key,
559
+ contentType: context.contentType,
560
+ signal: options.signal
561
+ });
562
+ if (!created.uploadId.trim()) {
563
+ throw new Error(`S3 Multipart create returned no upload id for chunk ${target.chunk_index}`);
564
+ }
565
+ checkpoint = {
566
+ version: 1,
567
+ storageKey: target.storage_key,
568
+ chunkIndex: target.chunk_index,
569
+ chunkSize: chunk.size,
570
+ chunkSha256: context.chunks[target.chunk_index]?.sha256 ?? "",
571
+ partSize: this.jumboPartSize,
572
+ uploadId: created.uploadId,
573
+ completed: false,
574
+ completing: false,
575
+ parts: []
576
+ };
577
+ await this.jumboChunkCheckpointStore.save(checkpointKey, checkpoint);
578
+ }
579
+ if (checkpoint.completed) {
580
+ return;
581
+ }
582
+ if (checkpoint.completing) {
583
+ const status = await this.getUploadStatus(context.uploadId, options, options.signal);
584
+ const statusChunk = status.chunks.find(
585
+ (candidate) => candidate.chunk_index === target.chunk_index
586
+ );
587
+ if (statusChunk?.status === "uploaded") {
588
+ checkpoint.completed = true;
589
+ checkpoint.completing = false;
590
+ await this.jumboChunkCheckpointStore.save(checkpointKey, checkpoint);
591
+ return;
592
+ }
593
+ checkpoint.completing = false;
594
+ await this.jumboChunkCheckpointStore.save(checkpointKey, checkpoint);
595
+ }
596
+ const parts = new Map(checkpoint.parts.map((part) => [part.partNumber, part]));
597
+ let uploadedBytes = 0;
598
+ let uploadedParts = 0;
599
+ for (let partNumber = 1; partNumber <= totalParts; partNumber += 1) {
600
+ throwIfAborted(options.signal);
601
+ const start = (partNumber - 1) * this.jumboPartSize;
602
+ const partBlob = chunk.blob.slice(start, Math.min(start + this.jumboPartSize, chunk.size));
603
+ const partHash = await sha256Hex(partBlob);
604
+ const savedPart = parts.get(partNumber);
605
+ const part = savedPart && savedPart.size === partBlob.size && savedPart.sha256 === partHash && savedPart.etag ? savedPart : await this.uploadJumboPart(
606
+ uploader,
607
+ target.storage_key,
608
+ checkpoint.uploadId,
609
+ partNumber,
610
+ partBlob,
611
+ partHash,
612
+ options.signal
613
+ );
614
+ parts.set(partNumber, part);
615
+ checkpoint.parts = [...parts.values()].sort(
616
+ (left, right) => left.partNumber - right.partNumber
617
+ );
618
+ await this.jumboChunkCheckpointStore.save(checkpointKey, checkpoint);
619
+ uploadedBytes += partBlob.size;
620
+ uploadedParts += 1;
621
+ options.onProgress?.({
622
+ phase: "uploading",
623
+ uploadedBytes: baseUploadedBytes + uploadedBytes,
624
+ totalBytes: context.fileSize,
625
+ uploadedChunks,
626
+ totalChunks,
627
+ uploadedParts,
628
+ totalParts
629
+ });
630
+ }
631
+ checkpoint.completing = true;
632
+ await this.jumboChunkCheckpointStore.save(checkpointKey, checkpoint);
633
+ try {
634
+ await uploader.completeMultipartUpload({
635
+ storageKey: target.storage_key,
636
+ uploadId: checkpoint.uploadId,
637
+ parts: checkpoint.parts.map(({ partNumber, etag }) => ({ partNumber, etag })),
638
+ signal: options.signal
639
+ });
640
+ } catch (error) {
641
+ checkpoint.completing = false;
642
+ await this.jumboChunkCheckpointStore.save(checkpointKey, checkpoint);
643
+ throw error;
644
+ }
645
+ checkpoint.completed = true;
646
+ checkpoint.completing = false;
647
+ await this.jumboChunkCheckpointStore.save(checkpointKey, checkpoint);
648
+ }
649
+ async uploadJumboPart(uploader, storageKey, uploadId, partNumber, body, sha256, signal) {
650
+ const result = await uploader.uploadPart({
651
+ storageKey,
652
+ uploadId,
653
+ partNumber,
654
+ body,
655
+ signal
656
+ });
657
+ if (!result.etag.trim()) {
658
+ throw new Error(`S3 Multipart upload returned no ETag for part ${partNumber}`);
659
+ }
660
+ return {
661
+ partNumber,
662
+ etag: result.etag,
663
+ size: body.size,
664
+ sha256
665
+ };
666
+ }
667
+ isCompatibleJumboCheckpoint(checkpoint, chunk, target, context) {
668
+ return checkpoint.version === 1 && checkpoint.storageKey === target.storage_key && checkpoint.chunkIndex === target.chunk_index && checkpoint.chunkSize === chunk.size && checkpoint.chunkSha256 === (context.chunks[target.chunk_index]?.sha256 ?? "") && checkpoint.partSize === this.jumboPartSize && checkpoint.uploadId.trim().length > 0;
669
+ }
670
+ assertUploadContext(context, file) {
671
+ if (!context.uploadId.trim()) {
672
+ throw new TypeError("upload context uploadId is required");
673
+ }
674
+ if (context.fileSize !== file.size) {
675
+ throw new Error("The restored file does not match the upload context size");
676
+ }
677
+ if (!Array.isArray(context.chunks) || context.chunks.length === 0) {
678
+ throw new TypeError("upload context must contain chunks");
679
+ }
680
+ let expectedOffset = 0;
681
+ for (const [index, chunk] of context.chunks.entries()) {
682
+ if (chunk.index !== index || chunk.offset !== expectedOffset || chunk.offset < 0 || chunk.size < 0 || chunk.offset + chunk.size > file.size || typeof chunk.sha256 !== "string") {
683
+ throw new TypeError("upload context contains an invalid chunk layout");
684
+ }
685
+ expectedOffset += chunk.size;
686
+ }
687
+ }
688
+ async finishUpload(uploadId, selector = {}) {
689
+ if (!uploadId.trim()) {
690
+ throw new TypeError("uploadId is required");
691
+ }
692
+ return ensureFinishResponse(
693
+ await this.requestJson("/upload/finish", {
694
+ ...getWorkspaceBody(this.clientSelector, selector),
695
+ upload_id: uploadId
696
+ })
697
+ );
698
+ }
699
+ endpoint(path) {
700
+ return `${this.baseUrl}${this.apiPrefix}${path}`;
701
+ }
702
+ async requestJson(path, body, signal) {
703
+ const headers = new Headers(this.headers);
704
+ headers.set("Content-Type", "application/json");
705
+ const token = typeof this.token === "function" ? await this.token() : this.token;
706
+ if (token) {
707
+ headers.set("Authorization", `Bearer ${token}`);
708
+ }
709
+ const response = await this.fetchImpl(this.endpoint(path), {
710
+ method: "POST",
711
+ headers,
712
+ body: JSON.stringify(body),
713
+ ...signal ? { signal } : {}
714
+ });
715
+ let responseBody;
716
+ try {
717
+ responseBody = await response.json();
718
+ } catch {
719
+ responseBody = void 0;
720
+ }
721
+ if (!response.ok) {
722
+ const error = parseApiErrorBody(responseBody);
723
+ throw new MountraApiError(
724
+ error.message ?? `Mountra API request failed with HTTP ${response.status}`,
725
+ { status: response.status, code: error.code, body: responseBody }
726
+ );
727
+ }
728
+ return responseBody;
729
+ }
730
+ };
731
+ function createMountraClient(options) {
732
+ return new MountraClient(options);
733
+ }
734
+ // Annotate the CommonJS export names for ESM import in node:
735
+ 0 && (module.exports = {
736
+ DEFAULT_S3_MULTIPART_PART_SIZE_BYTES,
737
+ InMemoryJumboChunkCheckpointStore,
738
+ JUMBO_CHUNK_THRESHOLD_BYTES,
739
+ LocalStorageJumboChunkCheckpointStore,
740
+ MAX_S3_MULTIPART_PARTS,
741
+ MIN_S3_MULTIPART_PART_SIZE_BYTES,
742
+ MountraApiError,
743
+ MountraClient,
744
+ MountraSDK,
745
+ S3MultipartUploader,
746
+ createMountraClient,
747
+ isJumboChunk,
748
+ sha256Hex,
749
+ splitFileIntoChunks
750
+ });
751
+ //# sourceMappingURL=index.cjs.map