@sparkvault/sdk-mobile 1.0.0 → 1.0.2

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/src/ingots.ts CHANGED
@@ -26,6 +26,12 @@ export interface UploadOptions {
26
26
  * Missing path segments are find-or-created. Omitted means the vault root.
27
27
  */
28
28
  folder?: string;
29
+ /**
30
+ * Existing folder id to upload into (e.g. "fld_xxx"). Use when the target
31
+ * folder is already known by id; mutually exclusive with `folder` (path).
32
+ * Omitted means the vault root.
33
+ */
34
+ folderId?: string;
29
35
  onProgress?: UploadProgressCallback;
30
36
  abortSignal?: AbortSignal;
31
37
  }
@@ -89,8 +95,6 @@ interface IngotAuditLogEntry {
89
95
 
90
96
  interface IngotAuditLogsResponse {
91
97
  entries?: IngotAuditLogEntry[];
92
- logs?: IngotAuditLogEntry[];
93
- cursor?: string;
94
98
  next_cursor?: string;
95
99
  }
96
100
 
@@ -123,6 +127,7 @@ export class MobileIngotsClient {
123
127
  contentType: options.contentType,
124
128
  sizeBytes: options.fileSize,
125
129
  folder: options.folder,
130
+ folderId: options.folderId,
126
131
  });
127
132
 
128
133
  await this.tus.uploadFromUri({
@@ -456,10 +461,10 @@ export class MobileIngotsClient {
456
461
  : `/vaults/${vaultId}/ingots/${ingotId}/audit-logs`,
457
462
  { vat }
458
463
  );
459
- const entries = response.data.entries ?? response.data.logs ?? [];
464
+ const entries = response.data.entries ?? [];
460
465
  return {
461
466
  logs: entries.map(entry => this.toAccessLog(entry)),
462
- next_cursor: response.data.cursor ?? response.data.next_cursor,
467
+ next_cursor: response.data.next_cursor,
463
468
  };
464
469
  }
465
470
 
@@ -489,14 +494,19 @@ export class MobileIngotsClient {
489
494
  contentType: string;
490
495
  sizeBytes: number;
491
496
  folder?: string;
497
+ folderId?: string;
492
498
  }): Promise<IngotUploadInitResponse> {
493
499
  const body: Record<string, unknown> = {
494
500
  name: options.name,
495
501
  content_type: options.contentType,
496
502
  size_bytes: options.sizeBytes,
497
503
  };
504
+ // folder_path (find-or-create) and folder_id (existing folder) are
505
+ // mutually exclusive server-side; a path takes precedence when both given.
498
506
  if (options.folder) {
499
507
  body.folder_path = options.folder;
508
+ } else if (options.folderId) {
509
+ body.folder_id = options.folderId;
500
510
  }
501
511
  const response = await this.http.post<IngotUploadInitResponse>(
502
512
  `/vaults/${options.vaultId}/ingots`,
@@ -0,0 +1,17 @@
1
+ // GENERATED FILE — do not edit by hand.
2
+ // Source of truth: packages/app-sdk/src/platform-domains.js
3
+ // Regenerate with: pnpm --filter @sparkvault/sdk-mobile build
4
+
5
+ export const SPARKVAULT_ROOT_DOMAIN = "sparkvault.com";
6
+
7
+ export const SPARKVAULT_PLATFORM_SV_HOSTS: readonly string[] = Object.freeze([
8
+ "auth.sv",
9
+ "x.sv",
10
+ "files.sv",
11
+ "file.sv",
12
+ "send.sv",
13
+ "spark.sv",
14
+ "by.sv",
15
+ "at.sv",
16
+ "db.sv",
17
+ ]);
package/src/tus.ts CHANGED
@@ -73,6 +73,12 @@ function ensureNotAborted(signal?: AbortSignal): void {
73
73
  }
74
74
 
75
75
  function toArrayBuffer(bytes: Uint8Array): ArrayBuffer {
76
+ // base64ToBytes returns exactly-sized buffers, so the chunk hot path is a
77
+ // zero-copy unwrap; copy only when the view spans part of a larger buffer.
78
+ // Aliasing is safe: chunks are sent then discarded, never mutated.
79
+ if (bytes.byteOffset === 0 && bytes.byteLength === bytes.buffer.byteLength) {
80
+ return bytes.buffer as ArrayBuffer;
81
+ }
76
82
  const copy = new Uint8Array(bytes.byteLength);
77
83
  copy.set(bytes);
78
84
  return copy.buffer;
@@ -223,10 +229,11 @@ export class MobileTusUploader {
223
229
  const debug = options.debug;
224
230
  const cleanUri = options.fileUri.split('#')[0];
225
231
  let bytesUploaded = 0;
232
+ let uploadUrl: string | null = null;
226
233
 
227
234
  try {
228
235
  debug?.log(`Starting TUS upload for ${options.filename}`);
229
- const uploadUrl = await createTusUpload(
236
+ uploadUrl = await createTusUpload(
230
237
  this.config,
231
238
  parsed,
232
239
  options.fileSize,
@@ -267,6 +274,18 @@ export class MobileTusUploader {
267
274
  } catch (err) {
268
275
  const error = err instanceof Error ? err : new Error(String(err));
269
276
  debug?.error(`Upload failed: ${getErrorMessage(error)}`);
277
+
278
+ // A cancelled session must be terminated server-side: the tus DELETE is
279
+ // what lets Forge remove already-written chunk objects (otherwise they
280
+ // are orphaned in S3 — nothing else cleans them up) and settle
281
+ // partial-transfer billing, matching the web client's abort semantics.
282
+ const cancelled =
283
+ options.abortSignal?.aborted === true ||
284
+ (err instanceof TusUploadError && err.phase === 'cancelled');
285
+ if (cancelled && uploadUrl) {
286
+ this.terminateUpload(uploadUrl, parsed.istk, debug);
287
+ }
288
+
270
289
  if (err instanceof TusUploadError) throw err;
271
290
  throw TusUploadError.fromError(error, {
272
291
  filename: options.filename,
@@ -276,6 +295,28 @@ export class MobileTusUploader {
276
295
  }
277
296
  }
278
297
 
298
+ /**
299
+ * Best-effort tus termination after a cancelled upload. Fire-and-forget so
300
+ * cancel UX stays instant and the cancellation error still propagates — a
301
+ * failed DELETE only means the orphaned session waits for server-side
302
+ * expiry. Deliberately NOT bound to the (already aborted) upload signal,
303
+ * which would kill the DELETE before it left the device.
304
+ */
305
+ private terminateUpload(uploadUrl: string, istk: string, debug?: DebugLogger): void {
306
+ void this.config
307
+ .fetch(uploadUrl, {
308
+ method: 'DELETE',
309
+ headers: {
310
+ 'Tus-Resumable': TUS_VERSION,
311
+ 'X-ISTK': istk,
312
+ },
313
+ timeoutMs: this.config.tusPostTimeoutMs,
314
+ })
315
+ .catch(err => {
316
+ debug?.log(`TUS termination after cancel failed: ${getErrorMessage(err)}`);
317
+ });
318
+ }
319
+
279
320
  private async readChunk(
280
321
  fileReader: MobileFileReader,
281
322
  fileUri: string,
package/src/validation.ts CHANGED
@@ -25,13 +25,20 @@ export function validateFolderId(folderId: string): void {
25
25
  }
26
26
  }
27
27
 
28
+ /** Windows reserved filenames that cannot be used. */
29
+ const WINDOWS_RESERVED_NAMES = new Set([
30
+ 'CON', 'PRN', 'AUX', 'NUL',
31
+ 'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9',
32
+ 'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9',
33
+ ]);
34
+
35
+ /**
36
+ * Sanitize a filename to prevent path traversal and other security issues:
37
+ * removes null bytes and control characters, strips path traversal sequences,
38
+ * replaces directory separators, strips leading dots (hidden files), and
39
+ * prefixes Windows reserved names. Falls back to 'file' when nothing remains.
40
+ */
28
41
  export function sanitizeFilename(filename: string): string {
29
- const reservedNames = new Set([
30
- 'CON', 'PRN', 'AUX', 'NUL',
31
- 'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9',
32
- 'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9',
33
- ]);
34
-
35
42
  let sanitized = filename
36
43
  .replace(/\0/g, '')
37
44
  .replace(/[\x00-\x1f\x7f]/g, '')
@@ -41,7 +48,7 @@ export function sanitizeFilename(filename: string): string {
41
48
  .trim();
42
49
 
43
50
  const baseName = sanitized.split('.')[0].toUpperCase();
44
- if (reservedNames.has(baseName)) {
51
+ if (WINDOWS_RESERVED_NAMES.has(baseName)) {
45
52
  sanitized = `_${sanitized}`;
46
53
  }
47
54
 
package/src/vaults.ts CHANGED
@@ -257,20 +257,6 @@ export class MobileVaultsClient {
257
257
  return response.data;
258
258
  }
259
259
 
260
- /**
261
- * @deprecated Use enableUploadPortal() or enableUploadWidget().
262
- */
263
- async enableUpload(vaultId: string, vmk: string): Promise<UploadPortalEnabled> {
264
- return this.enableUploadPortal(vaultId, vmk);
265
- }
266
-
267
- /**
268
- * @deprecated Use disableUploadPortal() or disableUploadWidget().
269
- */
270
- async disableUpload(vaultId: string): Promise<UploadPortalDisabled> {
271
- return this.disableUploadPortal(vaultId);
272
- }
273
-
274
260
  async updateUploadConfig(
275
261
  vaultId: string,
276
262
  config: {