@sparkvault/sdk-mobile 5.2.1 → 5.2.3
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/config.d.ts +20 -1
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -1
- package/dist/errors.d.ts +56 -3
- package/dist/errors.js +56 -2
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/ingots.d.ts +101 -1
- package/dist/ingots.js +83 -2
- package/dist/ingots.js.map +1 -1
- package/dist/notify.d.ts +17 -4
- package/dist/notify.js.map +1 -1
- package/dist/notify.test-d.d.ts +31 -0
- package/dist/notify.test-d.js +18 -0
- package/dist/notify.test-d.js.map +1 -0
- package/dist/tus.d.ts +132 -2
- package/dist/tus.js +546 -25
- package/dist/tus.js.map +1 -1
- package/dist/types.d.ts +124 -0
- package/package.json +1 -1
- package/src/config.ts +23 -0
- package/src/errors.ts +86 -3
- package/src/index.ts +6 -0
- package/src/ingots.ts +155 -2
- package/src/notify.test-d.ts +42 -0
- package/src/notify.ts +17 -4
- package/src/tus.ts +717 -48
- package/src/types.ts +128 -0
package/src/ingots.ts
CHANGED
|
@@ -16,9 +16,13 @@ import type {
|
|
|
16
16
|
IngotInvite,
|
|
17
17
|
IngotSharingConfig,
|
|
18
18
|
ListIngotsResponse,
|
|
19
|
+
IngotUploadSession,
|
|
19
20
|
MobileFileDownloader,
|
|
21
|
+
UploadAbortBehavior,
|
|
20
22
|
UploadProgressCallback,
|
|
21
23
|
UploadResult,
|
|
24
|
+
UploadTransferPolicy,
|
|
25
|
+
UploadTransport,
|
|
22
26
|
} from './types.js';
|
|
23
27
|
import { validateIngotId, validateVaultId } from './validation.js';
|
|
24
28
|
|
|
@@ -51,6 +55,34 @@ export interface UploadOptions {
|
|
|
51
55
|
export interface UploadFromUriOptions extends UploadOptions {
|
|
52
56
|
fileUri: string;
|
|
53
57
|
fileSize: number;
|
|
58
|
+
/**
|
|
59
|
+
* Invoked once the API has minted (or, via its same-name upsert, handed
|
|
60
|
+
* back) the ingot and before any Forge session exists. `created: false`
|
|
61
|
+
* means the bytes about to be sent will REPLACE an existing ingot's content
|
|
62
|
+
* once they finalize; a caller that must never overwrite throws here, and
|
|
63
|
+
* the throw propagates as-is with nothing sent and no session to clean up.
|
|
64
|
+
*/
|
|
65
|
+
onIngotCreated?: (ingot: { ingotId: string; created: boolean }) => void;
|
|
66
|
+
/**
|
|
67
|
+
* Invoked once the Forge tus session exists and before the first byte is
|
|
68
|
+
* sent, so the caller can persist it and `resumeUpload` after a failed
|
|
69
|
+
* attempt or a relaunch instead of re-sending bytes Forge already holds.
|
|
70
|
+
*/
|
|
71
|
+
onSessionCreated?: (session: IngotUploadSession) => void;
|
|
72
|
+
/** See `UploadTransport`; default `foreground`. */
|
|
73
|
+
transport?: UploadTransport;
|
|
74
|
+
/** Network policy for the OS transfer engine; see `UploadTransferPolicy`. Default: both allowed. */
|
|
75
|
+
transferPolicy?: UploadTransferPolicy;
|
|
76
|
+
/**
|
|
77
|
+
* What an abort does to the Forge session; see `UploadAbortBehavior`.
|
|
78
|
+
* Default `terminate` (a user cancel: Forge drops the chunks). `keep`
|
|
79
|
+
* (a pause) leaves the session on Forge so the caller can `resumeUpload`
|
|
80
|
+
* the one it was handed via `onSessionCreated` from the server offset;
|
|
81
|
+
* the caller then owns that session and is expected to resume or
|
|
82
|
+
* terminate it. The error is the same either way: the pre-flight
|
|
83
|
+
* `upload_cancelled` or the tus `cancelled`-phase `TusUploadError`.
|
|
84
|
+
*/
|
|
85
|
+
abortBehavior?: UploadAbortBehavior;
|
|
54
86
|
debug?: DebugLogger;
|
|
55
87
|
}
|
|
56
88
|
|
|
@@ -58,6 +90,31 @@ export interface ReplaceFromUriOptions extends UploadFromUriOptions {
|
|
|
58
90
|
ingotId: string;
|
|
59
91
|
}
|
|
60
92
|
|
|
93
|
+
export interface ResumeUploadOptions {
|
|
94
|
+
vaultId: string;
|
|
95
|
+
vat: string;
|
|
96
|
+
/** The ingot the session was created for (`UploadResult.ingot_id`). */
|
|
97
|
+
ingotId: string;
|
|
98
|
+
fileUri: string;
|
|
99
|
+
fileSize: number;
|
|
100
|
+
name: string;
|
|
101
|
+
contentType: string;
|
|
102
|
+
/** The persisted tus session handed to `onSessionCreated`. */
|
|
103
|
+
session: { uploadUrl: string; istk: string };
|
|
104
|
+
/** See `UploadTransport`; default `foreground`. */
|
|
105
|
+
transport?: UploadTransport;
|
|
106
|
+
/** Network policy for the OS transfer engine; see `UploadTransferPolicy`. Default: both allowed. */
|
|
107
|
+
transferPolicy?: UploadTransferPolicy;
|
|
108
|
+
onProgress?: UploadProgressCallback;
|
|
109
|
+
abortSignal?: AbortSignal;
|
|
110
|
+
/**
|
|
111
|
+
* See `UploadFromUriOptions.abortBehavior`. `keep` leaves `session` on
|
|
112
|
+
* Forge across the abort so the same session can be resumed again later.
|
|
113
|
+
*/
|
|
114
|
+
abortBehavior?: UploadAbortBehavior;
|
|
115
|
+
debug?: DebugLogger;
|
|
116
|
+
}
|
|
117
|
+
|
|
61
118
|
export interface DownloadToFileOptions {
|
|
62
119
|
vaultId: string;
|
|
63
120
|
ingotId: string;
|
|
@@ -109,6 +166,11 @@ export interface CreateUploadResult {
|
|
|
109
166
|
forgeUrl: string;
|
|
110
167
|
name?: string;
|
|
111
168
|
sizeBytes?: number;
|
|
169
|
+
/**
|
|
170
|
+
* Whether the server minted this ingot (true) or handed back an existing
|
|
171
|
+
* row via its same-name upsert (false). Absent when the server did not say.
|
|
172
|
+
*/
|
|
173
|
+
created?: boolean;
|
|
112
174
|
}
|
|
113
175
|
|
|
114
176
|
export interface CreateDownloadLinkOptions {
|
|
@@ -127,6 +189,7 @@ interface IngotUploadInitResponse {
|
|
|
127
189
|
forge_url: string;
|
|
128
190
|
name?: string;
|
|
129
191
|
size_bytes?: number;
|
|
192
|
+
created?: boolean;
|
|
130
193
|
}
|
|
131
194
|
|
|
132
195
|
interface IngotAuditLogEntry {
|
|
@@ -214,6 +277,12 @@ export class MobileIngotsClient {
|
|
|
214
277
|
folderId: options.folderId,
|
|
215
278
|
contentHmac: options.contentHmac,
|
|
216
279
|
});
|
|
280
|
+
const created = init.created ?? true;
|
|
281
|
+
|
|
282
|
+
// The ingot row exists but no Forge session does: a caller that refuses
|
|
283
|
+
// to replace an existing ingot (created: false) aborts here, before a
|
|
284
|
+
// byte moves, and its error reaches it untouched.
|
|
285
|
+
options.onIngotCreated?.({ ingotId: init.ingotId, created });
|
|
217
286
|
|
|
218
287
|
await this.tus.uploadFromUri({
|
|
219
288
|
fileUri: options.fileUri,
|
|
@@ -221,21 +290,96 @@ export class MobileIngotsClient {
|
|
|
221
290
|
filename: options.name,
|
|
222
291
|
contentType: options.contentType,
|
|
223
292
|
forgeUrl: init.forgeUrl,
|
|
293
|
+
onSessionCreated: session => options.onSessionCreated?.({ ...session, ingotId: init.ingotId, created }),
|
|
294
|
+
transport: options.transport,
|
|
295
|
+
transferPolicy: options.transferPolicy,
|
|
224
296
|
onProgress: options.onProgress,
|
|
225
297
|
abortSignal: options.abortSignal,
|
|
298
|
+
abortBehavior: options.abortBehavior,
|
|
226
299
|
debug: options.debug,
|
|
227
300
|
});
|
|
228
301
|
|
|
229
|
-
await this.verifyIngotActive(options.vaultId, init.ingotId, options.vat, options.debug);
|
|
302
|
+
const ingot = await this.verifyIngotActive(options.vaultId, init.ingotId, options.vat, options.debug);
|
|
230
303
|
|
|
231
304
|
return {
|
|
232
305
|
ingot_id: init.ingotId,
|
|
233
306
|
name: init.name ?? options.name,
|
|
234
307
|
size_bytes: init.sizeBytes ?? options.fileSize,
|
|
235
308
|
storage_location: 's3',
|
|
309
|
+
// A server that does not report it minted the row (the upsert is the
|
|
310
|
+
// only other outcome, and it always says so).
|
|
311
|
+
created,
|
|
312
|
+
status: ingot.status,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Continue an interrupted upload from the tus session `uploadFromUri`
|
|
318
|
+
* handed to `onSessionCreated`. No ingot is created: the record already
|
|
319
|
+
* exists from the original call, and the session is bound to it — Forge is
|
|
320
|
+
* asked (HEAD) where the transfer stands and only the missing bytes are
|
|
321
|
+
* sent, then the ingot is verified active exactly as a fresh upload is.
|
|
322
|
+
*
|
|
323
|
+
* A `TusUploadError` with `sessionLost` means Forge no longer has the
|
|
324
|
+
* session (expired or terminated); the caller drops it and starts over
|
|
325
|
+
* with `uploadFromUri`.
|
|
326
|
+
*/
|
|
327
|
+
async resumeUpload(options: ResumeUploadOptions): Promise<UploadResult> {
|
|
328
|
+
validateVaultId(options.vaultId);
|
|
329
|
+
validateIngotId(options.ingotId);
|
|
330
|
+
if (options.abortSignal?.aborted) {
|
|
331
|
+
throw new SparkVaultMobileError('Upload cancelled', { code: 'upload_cancelled' });
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
await this.tus.uploadFromUri({
|
|
335
|
+
fileUri: options.fileUri,
|
|
336
|
+
fileSize: options.fileSize,
|
|
337
|
+
filename: options.name,
|
|
338
|
+
contentType: options.contentType,
|
|
339
|
+
resume: options.session,
|
|
340
|
+
transport: options.transport,
|
|
341
|
+
transferPolicy: options.transferPolicy,
|
|
342
|
+
onProgress: options.onProgress,
|
|
343
|
+
abortSignal: options.abortSignal,
|
|
344
|
+
abortBehavior: options.abortBehavior,
|
|
345
|
+
debug: options.debug,
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
const ingot = await this.verifyIngotActive(options.vaultId, options.ingotId, options.vat, options.debug);
|
|
349
|
+
|
|
350
|
+
return {
|
|
351
|
+
ingot_id: options.ingotId,
|
|
352
|
+
name: options.name,
|
|
353
|
+
size_bytes: options.fileSize,
|
|
354
|
+
storage_location: 's3',
|
|
355
|
+
created: false,
|
|
356
|
+
status: ingot.status,
|
|
236
357
|
};
|
|
237
358
|
}
|
|
238
359
|
|
|
360
|
+
/**
|
|
361
|
+
* Where a persisted tus session stands on Forge, or `null` when Forge no
|
|
362
|
+
* longer has it. Lets a caller reconcile sessions on relaunch — a session
|
|
363
|
+
* at `offset === length` only needs its ingot verified, a lost one needs a
|
|
364
|
+
* fresh `uploadFromUri` — without spending an upload slot to find out.
|
|
365
|
+
*/
|
|
366
|
+
probeUploadSession(
|
|
367
|
+
session: { uploadUrl: string; istk: string }
|
|
368
|
+
): Promise<{ offset: number; length: number; chunkSize: number } | null> {
|
|
369
|
+
return this.tus.probeSession(session);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Release a tus session the caller kept on Forge (`abortBehavior: 'keep'`)
|
|
374
|
+
* and has since abandoned, so Forge drops the chunks it holds and settles
|
|
375
|
+
* the partial transfer's billing instead of waiting for expiry. Resolves
|
|
376
|
+
* once the session is gone, including when Forge no longer had it
|
|
377
|
+
* (404/410); any other failure rejects so the caller can retry later.
|
|
378
|
+
*/
|
|
379
|
+
terminateUploadSession(session: { uploadUrl: string; istk: string }): Promise<void> {
|
|
380
|
+
return this.tus.terminateSession(session);
|
|
381
|
+
}
|
|
382
|
+
|
|
239
383
|
/**
|
|
240
384
|
* Initialize an ingot record and obtain the Forge resumable-upload endpoint
|
|
241
385
|
* (with its upload-session token). The canonical first step of every upload;
|
|
@@ -273,6 +417,7 @@ export class MobileIngotsClient {
|
|
|
273
417
|
forgeUrl: response.data.forge_url,
|
|
274
418
|
name: response.data.name,
|
|
275
419
|
sizeBytes: response.data.size_bytes,
|
|
420
|
+
created: typeof response.data.created === 'boolean' ? response.data.created : undefined,
|
|
276
421
|
};
|
|
277
422
|
}
|
|
278
423
|
|
|
@@ -306,18 +451,26 @@ export class MobileIngotsClient {
|
|
|
306
451
|
filename: name,
|
|
307
452
|
contentType,
|
|
308
453
|
forgeUrl: response.data.forge_url,
|
|
454
|
+
// A replace never mints an ingot: the row is the caller's existing one.
|
|
455
|
+
onSessionCreated: session => options.onSessionCreated?.({ ...session, ingotId, created: false }),
|
|
456
|
+
transport: options.transport,
|
|
457
|
+
transferPolicy: options.transferPolicy,
|
|
309
458
|
onProgress,
|
|
310
459
|
abortSignal,
|
|
460
|
+
abortBehavior: options.abortBehavior,
|
|
311
461
|
debug,
|
|
312
462
|
});
|
|
313
463
|
|
|
314
|
-
await this.verifyIngotActive(vaultId, response.data.ingot_id, vat, debug);
|
|
464
|
+
const ingot = await this.verifyIngotActive(vaultId, response.data.ingot_id, vat, debug);
|
|
315
465
|
|
|
316
466
|
return {
|
|
317
467
|
ingot_id: response.data.ingot_id,
|
|
318
468
|
name,
|
|
319
469
|
size_bytes: fileSize,
|
|
320
470
|
storage_location: 's3',
|
|
471
|
+
// A replace never mints an ingot: the row is the caller's existing one.
|
|
472
|
+
created: false,
|
|
473
|
+
status: ingot.status,
|
|
321
474
|
};
|
|
322
475
|
}
|
|
323
476
|
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Notify wire-contract assertions — COMPILE-TIME ONLY.
|
|
3
|
+
*
|
|
4
|
+
* `NotificationRow` has no runtime, so a unit test can never catch it drifting
|
|
5
|
+
* from the server: the transpiler strips it and every assertion passes. This file
|
|
6
|
+
* is the gate instead — `tsc --noEmit` (`pnpm typecheck`, which CI runs on every
|
|
7
|
+
* push and again on every SDK publish) fails the moment the published row stops
|
|
8
|
+
* matching what `apps/api` actually emits.
|
|
9
|
+
*
|
|
10
|
+
* It is deliberately NOT a `*.test.ts`: that suffix is excluded from `tsconfig`,
|
|
11
|
+
* which would put these assertions exactly where the compiler cannot see them.
|
|
12
|
+
*
|
|
13
|
+
* The row shape is shared verbatim with `@sparkvault/sdk-js`, whose own contract
|
|
14
|
+
* file asserts the same three members — both must move together, and the mirrored
|
|
15
|
+
* server source is `apps/api/src/notify/inbox.js` (`projectRow`).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { NotificationRow } from './notify.js';
|
|
19
|
+
|
|
20
|
+
/** Fails to compile unless `T` resolves to exactly `true`. */
|
|
21
|
+
type Assert<T extends true> = T;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Invariant type equality — `A` and `B` must be the SAME type, not merely mutually
|
|
25
|
+
* assignable. Assignability would accept an optional member where the wire always
|
|
26
|
+
* carries one, which is the precise drift these assertions exist to catch.
|
|
27
|
+
*/
|
|
28
|
+
type Equals<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2
|
|
29
|
+
? true
|
|
30
|
+
: false;
|
|
31
|
+
|
|
32
|
+
export type NotifyWireContract = [
|
|
33
|
+
// `recalled` is `Boolean(row.recalled_at)`, so it is always on the wire: typing
|
|
34
|
+
// it optional would let the app skip the "no longer available" branch and fall
|
|
35
|
+
// through to a reveal that opens nothing.
|
|
36
|
+
Assert<Equals<NotificationRow['recalled'], boolean>>,
|
|
37
|
+
// `locked` is `!recalled && Boolean(row.sparklink_code)` — always present.
|
|
38
|
+
Assert<Equals<NotificationRow['locked'], boolean>>,
|
|
39
|
+
// The pointer is withheld on a recalled row and absent on a cleartext one, so
|
|
40
|
+
// it is optional in both directions and must be guarded before use.
|
|
41
|
+
Assert<Equals<NotificationRow['sparklink_code'], string | undefined>>,
|
|
42
|
+
];
|
package/src/notify.ts
CHANGED
|
@@ -31,13 +31,26 @@ export interface NotificationRow {
|
|
|
31
31
|
read_at: number | null;
|
|
32
32
|
archived_at: number | null;
|
|
33
33
|
/**
|
|
34
|
-
* The grant to open. Present only
|
|
35
|
-
*
|
|
36
|
-
*
|
|
34
|
+
* The grant to open. Present only while a live reveal stands behind it — a
|
|
35
|
+
* SEALED (secure) send that has not been recalled. A cleartext send delivers its
|
|
36
|
+
* content inline in the channel and mints no grant; a recalled row's grant is
|
|
37
|
+
* revoked and the pointer is withheld rather than echoed. Guard before building
|
|
38
|
+
* a URL from it.
|
|
37
39
|
*/
|
|
38
40
|
sparklink_code?: string;
|
|
39
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* True only when there is a secure part left to reveal — exactly when
|
|
43
|
+
* `sparklink_code` is present. `false` does NOT mean "cleartext row": a recalled
|
|
44
|
+
* row is unlocked too, with nothing behind it. Branch on `recalled` before
|
|
45
|
+
* treating an unlocked row as plain text.
|
|
46
|
+
*/
|
|
40
47
|
locked: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* The sender took this notification back. The grant is revoked and
|
|
50
|
+
* `sparklink_code` is withheld, so render "no longer available" rather than a
|
|
51
|
+
* tap-to-reveal that opens nothing.
|
|
52
|
+
*/
|
|
53
|
+
recalled: boolean;
|
|
41
54
|
thread_id?: string;
|
|
42
55
|
}
|
|
43
56
|
|