@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/dist/auth.d.ts +6 -0
- package/dist/auth.js +22 -31
- package/dist/auth.js.map +1 -1
- package/dist/config.js +8 -4
- package/dist/config.js.map +1 -1
- package/dist/encoding.js +15 -5
- package/dist/encoding.js.map +1 -1
- package/dist/http.js +15 -2
- package/dist/http.js.map +1 -1
- package/dist/identity-dialog.js +3 -4
- package/dist/identity-dialog.js.map +1 -1
- package/dist/ingots.d.ts +6 -0
- package/dist/ingots.js +8 -2
- package/dist/ingots.js.map +1 -1
- package/dist/platform-domains.generated.d.ts +2 -0
- package/dist/platform-domains.generated.js +16 -0
- package/dist/platform-domains.generated.js.map +1 -0
- package/dist/tus.d.ts +8 -0
- package/dist/tus.js +38 -1
- package/dist/tus.js.map +1 -1
- package/dist/validation.d.ts +6 -0
- package/dist/validation.js +13 -6
- package/dist/validation.js.map +1 -1
- package/dist/vaults.d.ts +0 -8
- package/dist/vaults.js +0 -12
- package/dist/vaults.js.map +1 -1
- package/package.json +3 -2
- package/src/auth.ts +23 -32
- package/src/config.ts +12 -4
- package/src/encoding.ts +15 -5
- package/src/http.ts +13 -2
- package/src/identity-dialog.tsx +3 -4
- package/src/ingots.ts +14 -4
- package/src/platform-domains.generated.ts +17 -0
- package/src/tus.ts +42 -1
- package/src/validation.ts +14 -7
- package/src/vaults.ts +0 -14
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 ??
|
|
464
|
+
const entries = response.data.entries ?? [];
|
|
460
465
|
return {
|
|
461
466
|
logs: entries.map(entry => this.toAccessLog(entry)),
|
|
462
|
-
next_cursor: response.data.
|
|
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
|
-
|
|
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 (
|
|
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: {
|