@sparkvault/sdk-mobile 6.0.2 → 7.0.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/README.md +5 -5
- package/dist/account.d.ts +4 -3
- package/dist/account.js +16 -11
- package/dist/account.js.map +1 -1
- package/dist/app-sdk/email-address.d.ts +37 -0
- package/dist/app-sdk/email-address.js +91 -0
- package/dist/app-sdk/email-address.js.map +1 -0
- package/dist/app-sdk/encoding.d.ts +64 -0
- package/dist/app-sdk/encoding.js +245 -0
- package/dist/app-sdk/encoding.js.map +1 -0
- package/dist/app-sdk/entropy-contract.d.ts +33 -0
- package/dist/app-sdk/entropy-contract.js +51 -0
- package/dist/app-sdk/entropy-contract.js.map +1 -0
- package/dist/app-sdk/filename.d.ts +93 -0
- package/dist/app-sdk/filename.js +161 -0
- package/dist/app-sdk/filename.js.map +1 -0
- package/dist/app-sdk/forge-url.d.ts +107 -0
- package/dist/app-sdk/forge-url.js +222 -0
- package/dist/app-sdk/forge-url.js.map +1 -0
- package/dist/app-sdk/ids.d.ts +48 -0
- package/dist/app-sdk/ids.js +65 -0
- package/dist/app-sdk/ids.js.map +1 -0
- package/dist/app-sdk/ingot-access-log.d.ts +29 -0
- package/dist/app-sdk/ingot-access-log.js +49 -0
- package/dist/app-sdk/ingot-access-log.js.map +1 -0
- package/dist/app-sdk/phone.d.ts +37 -0
- package/dist/app-sdk/phone.js +134 -0
- package/dist/app-sdk/phone.js.map +1 -0
- package/dist/app-sdk/platform-domains.d.ts +41 -0
- package/dist/app-sdk/platform-domains.js +82 -0
- package/dist/app-sdk/platform-domains.js.map +1 -0
- package/dist/app-sdk/request-path.d.ts +49 -0
- package/dist/app-sdk/request-path.js +81 -0
- package/dist/app-sdk/request-path.js.map +1 -0
- package/dist/app-sdk/transient.d.ts +23 -0
- package/dist/app-sdk/transient.js +29 -0
- package/dist/app-sdk/transient.js.map +1 -0
- package/dist/app-sdk/vat-refusal.d.ts +66 -0
- package/dist/app-sdk/vat-refusal.js +69 -0
- package/dist/app-sdk/vat-refusal.js.map +1 -0
- package/dist/billing.d.ts +9 -17
- package/dist/billing.js +14 -17
- package/dist/billing.js.map +1 -1
- package/dist/config.d.ts +7 -1
- package/dist/config.js +10 -17
- package/dist/config.js.map +1 -1
- package/dist/encoding.d.ts +30 -4
- package/dist/encoding.js +89 -83
- package/dist/encoding.js.map +1 -1
- package/dist/entropy.d.ts +25 -4
- package/dist/entropy.js +62 -15
- package/dist/entropy.js.map +1 -1
- package/dist/entropy.test-d.d.ts +40 -0
- package/dist/entropy.test-d.js +12 -0
- package/dist/entropy.test-d.js.map +1 -0
- package/dist/errors.d.ts +67 -17
- package/dist/errors.js +70 -27
- package/dist/errors.js.map +1 -1
- package/dist/folders.js +8 -7
- package/dist/folders.js.map +1 -1
- package/dist/http.d.ts +57 -15
- package/dist/http.js +125 -138
- package/dist/http.js.map +1 -1
- package/dist/identity-dialog.js +4 -30
- package/dist/identity-dialog.js.map +1 -1
- package/dist/identity-input.d.ts +39 -0
- package/dist/identity-input.js +59 -0
- package/dist/identity-input.js.map +1 -0
- package/dist/index.d.ts +6 -5
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/ingots.d.ts +51 -18
- package/dist/ingots.js +132 -122
- package/dist/ingots.js.map +1 -1
- package/dist/mutex.d.ts +1 -2
- package/dist/mutex.js +0 -6
- package/dist/mutex.js.map +1 -1
- package/dist/notify.d.ts +7 -2
- package/dist/notify.js +12 -15
- package/dist/notify.js.map +1 -1
- package/dist/notify.test-d.d.ts +3 -2
- package/dist/notify.test-d.js +1 -1
- package/dist/products/identity.d.ts +5 -0
- package/dist/products/identity.js +40 -35
- package/dist/products/identity.js.map +1 -1
- package/dist/request-path.d.ts +32 -0
- package/dist/request-path.js +35 -0
- package/dist/request-path.js.map +1 -0
- package/dist/sparks.js +16 -15
- package/dist/sparks.js.map +1 -1
- package/dist/tus.d.ts +19 -2
- package/dist/tus.js +86 -35
- package/dist/tus.js.map +1 -1
- package/dist/types.d.ts +167 -15
- package/dist/users.d.ts +12 -5
- package/dist/users.js +27 -19
- package/dist/users.js.map +1 -1
- package/dist/validation.d.ts +20 -5
- package/dist/validation.js +29 -27
- package/dist/validation.js.map +1 -1
- package/dist/vaults.d.ts +34 -83
- package/dist/vaults.js +64 -44
- package/dist/vaults.js.map +1 -1
- package/dist/vaults.test-d.d.ts +22 -0
- package/dist/vaults.test-d.js +14 -0
- package/dist/vaults.test-d.js.map +1 -0
- package/package.json +4 -2
- package/src/account.ts +16 -11
- package/src/app-sdk/email-address.js +94 -0
- package/src/app-sdk/encoding.js +250 -0
- package/src/app-sdk/entropy-contract.js +56 -0
- package/src/app-sdk/filename.js +174 -0
- package/src/app-sdk/forge-url.js +224 -0
- package/src/app-sdk/ids.js +71 -0
- package/src/app-sdk/ingot-access-log.js +51 -0
- package/src/app-sdk/phone.js +139 -0
- package/src/app-sdk/platform-domains.js +99 -0
- package/src/app-sdk/request-path.js +81 -0
- package/src/app-sdk/transient.js +30 -0
- package/src/app-sdk/vat-refusal.js +74 -0
- package/src/billing.ts +16 -25
- package/src/config.ts +16 -23
- package/src/encoding.ts +91 -103
- package/src/entropy.test-d.ts +47 -0
- package/src/entropy.ts +86 -20
- package/src/errors.ts +93 -33
- package/src/folders.ts +8 -7
- package/src/http.ts +145 -150
- package/src/identity-dialog.tsx +4 -37
- package/src/identity-input.ts +70 -0
- package/src/index.ts +11 -3
- package/src/ingots.ts +173 -159
- package/src/mutex.ts +1 -8
- package/src/notify.test-d.ts +3 -1
- package/src/notify.ts +19 -17
- package/src/products/identity.ts +45 -39
- package/src/request-path.ts +45 -0
- package/src/sparks.ts +16 -13
- package/src/tus.ts +90 -37
- package/src/types.ts +165 -15
- package/src/users.ts +27 -17
- package/src/validation.ts +30 -24
- package/src/vaults.test-d.ts +29 -0
- package/src/vaults.ts +87 -153
- package/dist/platform-domains.generated.d.ts +0 -2
- package/dist/platform-domains.generated.js +0 -16
- package/dist/platform-domains.generated.js.map +0 -1
- package/src/platform-domains.generated.ts +0 -17
package/src/ingots.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
|
+
import { readForgeTransferUrl } from './app-sdk/forge-url.js';
|
|
2
|
+
import { INGOT_ACCESS_EVENT_TYPES, readIngotAccess } from './app-sdk/ingot-access-log.js';
|
|
1
3
|
import type { ResolvedMobileConfig } from './config.js';
|
|
2
4
|
import {
|
|
5
|
+
IngotNameHeldError,
|
|
3
6
|
IngotNotReadyError,
|
|
4
7
|
isRetryableDownloadError,
|
|
5
8
|
SparkVaultMobileError,
|
|
6
9
|
SparkVaultValidationError,
|
|
7
10
|
} from './errors.js';
|
|
8
11
|
import type { MobileHttpClient } from './http.js';
|
|
9
|
-
import { MobileTusUploader } from './tus.js';
|
|
12
|
+
import { allowsHost, MobileTusUploader } from './tus.js';
|
|
10
13
|
import type {
|
|
11
14
|
DebugLogger,
|
|
12
15
|
HashCheckResponse,
|
|
@@ -16,6 +19,7 @@ import type {
|
|
|
16
19
|
IngotInvite,
|
|
17
20
|
IngotSharingConfig,
|
|
18
21
|
ListIngotsResponse,
|
|
22
|
+
FolderContentsResponse,
|
|
19
23
|
IngotUploadSession,
|
|
20
24
|
MobileFileDownloader,
|
|
21
25
|
UploadAbortBehavior,
|
|
@@ -24,7 +28,26 @@ import type {
|
|
|
24
28
|
UploadTransferPolicy,
|
|
25
29
|
UploadTransport,
|
|
26
30
|
} from './types.js';
|
|
27
|
-
import { validateIngotId, validateVaultId } from './validation.js';
|
|
31
|
+
import { validateIngotId, validateResourceId, validateVaultId } from './validation.js';
|
|
32
|
+
import { apiPath } from './request-path.js';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* What the server does when a create's name is already held in the target
|
|
36
|
+
* folder (case-insensitive). The server decides on the holder it read before
|
|
37
|
+
* writing anything, so a caller that must not replace content says so here:
|
|
38
|
+
* - `upsert` (the server default): the upload replaces the holder's content.
|
|
39
|
+
* - `complete_pending`: a holder whose own upload never finalized is completed
|
|
40
|
+
* (`created: false`); a published holder is refused with
|
|
41
|
+
* `IngotNameHeldError`, untouched.
|
|
42
|
+
* - `refuse`: any holder, its upload finished or not, is refused with
|
|
43
|
+
* `IngotNameHeldError`, untouched; the create only ever mints.
|
|
44
|
+
* - `new_name`: the holder is never touched; the ingot is minted under the
|
|
45
|
+
* first free numbered name the server picks ("report (2).pdf"), returned
|
|
46
|
+
* as the result's `name`.
|
|
47
|
+
*/
|
|
48
|
+
export const NAME_CONFLICT_POLICIES = ['upsert', 'complete_pending', 'refuse', 'new_name'] as const;
|
|
49
|
+
|
|
50
|
+
export type NameConflictPolicy = (typeof NAME_CONFLICT_POLICIES)[number];
|
|
28
51
|
|
|
29
52
|
export interface UploadOptions {
|
|
30
53
|
vaultId: string;
|
|
@@ -55,14 +78,19 @@ export interface UploadOptions {
|
|
|
55
78
|
export interface UploadFromUriOptions extends UploadOptions {
|
|
56
79
|
fileUri: string;
|
|
57
80
|
fileSize: number;
|
|
81
|
+
/** What a name already held in the target folder does; omitted means the server's `upsert`. */
|
|
82
|
+
onNameConflict?: NameConflictPolicy;
|
|
58
83
|
/**
|
|
59
|
-
* Invoked once the
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
84
|
+
* Invoked once the server has placed the upload and before any Forge
|
|
85
|
+
* session exists: `created` is true when it minted the ingot and false when
|
|
86
|
+
* it staged the upload onto the row holding the name (as `onNameConflict`
|
|
87
|
+
* allowed), and `name` is the name the ingot is stored under (the server's
|
|
88
|
+
* numbered name under `new_name`). The placement has already been written
|
|
89
|
+
* by then, so this hook observes; a caller that must not replace content
|
|
90
|
+
* says so through `onNameConflict`. A throw here still stops the upload
|
|
91
|
+
* before a byte moves and propagates as-is.
|
|
64
92
|
*/
|
|
65
|
-
onIngotCreated?: (ingot: { ingotId: string; created: boolean }) => void;
|
|
93
|
+
onIngotCreated?: (ingot: { ingotId: string; created: boolean; name: string }) => void;
|
|
66
94
|
/**
|
|
67
95
|
* Invoked once the Forge tus session exists and before the first byte is
|
|
68
96
|
* sent, so the caller can persist it and `resumeUpload` after a failed
|
|
@@ -86,7 +114,8 @@ export interface UploadFromUriOptions extends UploadOptions {
|
|
|
86
114
|
debug?: DebugLogger;
|
|
87
115
|
}
|
|
88
116
|
|
|
89
|
-
|
|
117
|
+
/** A replace addresses its ingot by id: no name is placed, so the create-only hooks do not apply. */
|
|
118
|
+
export interface ReplaceFromUriOptions extends Omit<UploadFromUriOptions, 'onNameConflict' | 'onIngotCreated'> {
|
|
90
119
|
ingotId: string;
|
|
91
120
|
}
|
|
92
121
|
|
|
@@ -98,7 +127,6 @@ export interface ResumeUploadOptions {
|
|
|
98
127
|
fileUri: string;
|
|
99
128
|
fileSize: number;
|
|
100
129
|
name: string;
|
|
101
|
-
contentType: string;
|
|
102
130
|
/** The persisted tus session handed to `onSessionCreated`. */
|
|
103
131
|
session: { uploadUrl: string; istk: string };
|
|
104
132
|
/** See `UploadTransport`; default `foreground`. */
|
|
@@ -161,17 +189,20 @@ export interface CreateUploadOptions {
|
|
|
161
189
|
folderId?: string;
|
|
162
190
|
/** Vault-keyed plaintext HMAC (43-char base64url); see UploadOptions.contentHmac. */
|
|
163
191
|
contentHmac?: string;
|
|
192
|
+
/** What a name already held in the target folder does; omitted means the server's `upsert`. */
|
|
193
|
+
onNameConflict?: NameConflictPolicy;
|
|
164
194
|
}
|
|
165
195
|
|
|
166
196
|
export interface CreateUploadResult {
|
|
167
197
|
ingotId: string;
|
|
168
198
|
/** Forge tus endpoint plus upload-session token (ISTK) for the resumable upload. */
|
|
169
199
|
forgeUrl: string;
|
|
200
|
+
/** The name the ingot is stored under: the server's numbered name under `new_name`. */
|
|
170
201
|
name?: string;
|
|
171
202
|
sizeBytes?: number;
|
|
172
203
|
/**
|
|
173
|
-
* Whether the server minted this ingot (true) or
|
|
174
|
-
* row
|
|
204
|
+
* Whether the server minted this ingot (true) or staged the upload onto
|
|
205
|
+
* the row already holding the name (false). Absent when the server did not say.
|
|
175
206
|
*/
|
|
176
207
|
created?: boolean;
|
|
177
208
|
}
|
|
@@ -196,29 +227,23 @@ interface IngotUploadInitResponse {
|
|
|
196
227
|
created?: boolean;
|
|
197
228
|
}
|
|
198
229
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
actor_id?: string;
|
|
213
|
-
identity?: string;
|
|
214
|
-
ip_address?: string;
|
|
215
|
-
user_agent?: string;
|
|
216
|
-
timestamp?: number;
|
|
217
|
-
created_at?: number;
|
|
230
|
+
/** An ingot audit entry in the server's public shape. */
|
|
231
|
+
interface IngotAuditEntry {
|
|
232
|
+
timestamp: number;
|
|
233
|
+
event_type: string;
|
|
234
|
+
actor_id?: string | null;
|
|
235
|
+
actor_type?: string;
|
|
236
|
+
actor_display?: string;
|
|
237
|
+
ip_address?: string | null;
|
|
238
|
+
user_agent?: string | null;
|
|
239
|
+
metadata?: { identity?: string | null; [key: string]: unknown } | null;
|
|
240
|
+
encrypted?: boolean;
|
|
241
|
+
vault_id?: string;
|
|
242
|
+
ingot_id?: string;
|
|
218
243
|
}
|
|
219
244
|
|
|
220
245
|
interface IngotAuditLogsResponse {
|
|
221
|
-
entries?:
|
|
246
|
+
entries?: IngotAuditEntry[];
|
|
222
247
|
next_cursor?: string;
|
|
223
248
|
}
|
|
224
249
|
|
|
@@ -243,6 +268,28 @@ function toTypedDownloadError(err: unknown): unknown {
|
|
|
243
268
|
return err;
|
|
244
269
|
}
|
|
245
270
|
|
|
271
|
+
/**
|
|
272
|
+
* A create refused over its holder (`complete_pending` over a published one,
|
|
273
|
+
* `refuse` over any) answers 409 CONFLICT with `details.reason === 'NAME_HELD'`
|
|
274
|
+
* and the holder the server read. Every other 409 on the create (a folder
|
|
275
|
+
* being deleted, a cross-manager name) carries no holder and stays a plain
|
|
276
|
+
* `SparkVaultMobileError`.
|
|
277
|
+
*/
|
|
278
|
+
function toTypedCreateError(err: unknown): unknown {
|
|
279
|
+
if (!(err instanceof SparkVaultMobileError) || err.statusCode !== 409 || err.details?.reason !== 'NAME_HELD') {
|
|
280
|
+
return err;
|
|
281
|
+
}
|
|
282
|
+
const { ingot_id: ingotId, status, size_bytes: sizeBytes } = err.details;
|
|
283
|
+
if (typeof ingotId !== 'string' || typeof status !== 'string' || typeof sizeBytes !== 'number') {
|
|
284
|
+
return err;
|
|
285
|
+
}
|
|
286
|
+
return new IngotNameHeldError(err.message, {
|
|
287
|
+
holder: { ingotId, status, sizeBytes },
|
|
288
|
+
details: err.details,
|
|
289
|
+
meta: err.meta,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
246
293
|
export class MobileIngotsClient {
|
|
247
294
|
// Server-side finalization (encryption) of a just-uploaded ingot runs
|
|
248
295
|
// asynchronously and scales with file size, so a large video can sit in
|
|
@@ -280,19 +327,20 @@ export class MobileIngotsClient {
|
|
|
280
327
|
folder: options.folder,
|
|
281
328
|
folderId: options.folderId,
|
|
282
329
|
contentHmac: options.contentHmac,
|
|
330
|
+
onNameConflict: options.onNameConflict,
|
|
283
331
|
});
|
|
284
332
|
const created = init.created ?? true;
|
|
333
|
+
const name = init.name ?? options.name;
|
|
285
334
|
|
|
286
|
-
// The
|
|
287
|
-
//
|
|
288
|
-
//
|
|
289
|
-
options.onIngotCreated?.({ ingotId: init.ingotId, created });
|
|
335
|
+
// The placement is written but no Forge session exists yet: a throw from
|
|
336
|
+
// the hook stops the upload before a byte moves and reaches the caller
|
|
337
|
+
// untouched.
|
|
338
|
+
options.onIngotCreated?.({ ingotId: init.ingotId, created, name });
|
|
290
339
|
|
|
291
340
|
await this.tus.uploadFromUri({
|
|
292
341
|
fileUri: options.fileUri,
|
|
293
342
|
fileSize: options.fileSize,
|
|
294
|
-
filename:
|
|
295
|
-
contentType: options.contentType,
|
|
343
|
+
filename: name,
|
|
296
344
|
forgeUrl: init.forgeUrl,
|
|
297
345
|
onSessionCreated: session => options.onSessionCreated?.({ ...session, ingotId: init.ingotId, created }),
|
|
298
346
|
transport: options.transport,
|
|
@@ -307,7 +355,7 @@ export class MobileIngotsClient {
|
|
|
307
355
|
|
|
308
356
|
return {
|
|
309
357
|
ingot_id: init.ingotId,
|
|
310
|
-
name
|
|
358
|
+
name,
|
|
311
359
|
size_bytes: init.sizeBytes ?? options.fileSize,
|
|
312
360
|
storage_location: 's3',
|
|
313
361
|
// A server that does not report it minted the row (the upsert is the
|
|
@@ -326,7 +374,9 @@ export class MobileIngotsClient {
|
|
|
326
374
|
*
|
|
327
375
|
* A `TusUploadError` with `sessionLost` means Forge no longer has the
|
|
328
376
|
* session (expired or terminated); the caller drops it and starts over
|
|
329
|
-
* with `uploadFromUri`.
|
|
377
|
+
* with `uploadFromUri`. A session `uploadUrl` that is not an upload session
|
|
378
|
+
* on an allowed transfer host is refused (SparkVaultValidationError) before
|
|
379
|
+
* the ISTK or a byte is sent anywhere.
|
|
330
380
|
*/
|
|
331
381
|
async resumeUpload(options: ResumeUploadOptions): Promise<UploadResult> {
|
|
332
382
|
validateVaultId(options.vaultId);
|
|
@@ -339,7 +389,6 @@ export class MobileIngotsClient {
|
|
|
339
389
|
fileUri: options.fileUri,
|
|
340
390
|
fileSize: options.fileSize,
|
|
341
391
|
filename: options.name,
|
|
342
|
-
contentType: options.contentType,
|
|
343
392
|
resume: options.session,
|
|
344
393
|
transport: options.transport,
|
|
345
394
|
transferPolicy: options.transferPolicy,
|
|
@@ -366,6 +415,8 @@ export class MobileIngotsClient {
|
|
|
366
415
|
* longer has it. Lets a caller reconcile sessions on relaunch — a session
|
|
367
416
|
* at `offset === length` only needs its ingot verified, a lost one needs a
|
|
368
417
|
* fresh `uploadFromUri` — without spending an upload slot to find out.
|
|
418
|
+
* An `uploadUrl` that is not an upload session on an allowed transfer host
|
|
419
|
+
* is refused (SparkVaultValidationError) before the ISTK is sent anywhere.
|
|
369
420
|
*/
|
|
370
421
|
probeUploadSession(
|
|
371
422
|
session: { uploadUrl: string; istk: string }
|
|
@@ -379,6 +430,8 @@ export class MobileIngotsClient {
|
|
|
379
430
|
* the partial transfer's billing instead of waiting for expiry. Resolves
|
|
380
431
|
* once the session is gone, including when Forge no longer had it
|
|
381
432
|
* (404/410); any other failure rejects so the caller can retry later.
|
|
433
|
+
* An `uploadUrl` that is not an upload session on an allowed transfer host
|
|
434
|
+
* is refused (SparkVaultValidationError) before the ISTK is sent anywhere.
|
|
382
435
|
*/
|
|
383
436
|
terminateUploadSession(session: { uploadUrl: string; istk: string }): Promise<void> {
|
|
384
437
|
return this.tus.terminateSession(session);
|
|
@@ -406,11 +459,14 @@ export class MobileIngotsClient {
|
|
|
406
459
|
if (options.contentHmac) {
|
|
407
460
|
body.content_hmac = options.contentHmac;
|
|
408
461
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
462
|
+
if (options.onNameConflict) {
|
|
463
|
+
body.on_name_conflict = options.onNameConflict;
|
|
464
|
+
}
|
|
465
|
+
const response = await this.http
|
|
466
|
+
.post<IngotUploadInitResponse>(apiPath`/vaults/${options.vaultId}/ingots`, body, { vat: options.vat })
|
|
467
|
+
.catch((err: unknown) => {
|
|
468
|
+
throw toTypedCreateError(err);
|
|
469
|
+
});
|
|
414
470
|
|
|
415
471
|
if (!response.data.ingot_id || !response.data.forge_url) {
|
|
416
472
|
throw new SparkVaultMobileError('Server returned invalid response for ingot creation');
|
|
@@ -435,7 +491,7 @@ export class MobileIngotsClient {
|
|
|
435
491
|
}
|
|
436
492
|
|
|
437
493
|
const response = await this.http.put<IngotUploadInitResponse>(
|
|
438
|
-
`/vaults/${vaultId}/ingots/${ingotId}`,
|
|
494
|
+
apiPath`/vaults/${vaultId}/ingots/${ingotId}`,
|
|
439
495
|
{
|
|
440
496
|
name,
|
|
441
497
|
content_type: contentType,
|
|
@@ -453,7 +509,6 @@ export class MobileIngotsClient {
|
|
|
453
509
|
fileUri,
|
|
454
510
|
fileSize,
|
|
455
511
|
filename: name,
|
|
456
|
-
contentType,
|
|
457
512
|
forgeUrl: response.data.forge_url,
|
|
458
513
|
// A replace never mints an ingot: the row is the caller's existing one.
|
|
459
514
|
onSessionCreated: session => options.onSessionCreated?.({ ...session, ingotId, created: false }),
|
|
@@ -490,7 +545,7 @@ export class MobileIngotsClient {
|
|
|
490
545
|
): Promise<HashCheckResponse> {
|
|
491
546
|
validateVaultId(vaultId);
|
|
492
547
|
const response = await this.http.post<HashCheckResponse>(
|
|
493
|
-
`/vaults/${vaultId}/ingots/hash-check`,
|
|
548
|
+
apiPath`/vaults/${vaultId}/ingots/hash-check`,
|
|
494
549
|
{ hashes },
|
|
495
550
|
{ vat }
|
|
496
551
|
);
|
|
@@ -500,7 +555,7 @@ export class MobileIngotsClient {
|
|
|
500
555
|
async rename(vaultId: string, ingotId: string, vat: string, name: string): Promise<Ingot> {
|
|
501
556
|
validateVaultId(vaultId);
|
|
502
557
|
validateIngotId(ingotId);
|
|
503
|
-
const response = await this.http.patch<Ingot>(`/vaults/${vaultId}/ingots/${ingotId}`, { name }, { vat });
|
|
558
|
+
const response = await this.http.patch<Ingot>(apiPath`/vaults/${vaultId}/ingots/${ingotId}`, { name }, { vat });
|
|
504
559
|
return response.data;
|
|
505
560
|
}
|
|
506
561
|
|
|
@@ -510,44 +565,37 @@ export class MobileIngotsClient {
|
|
|
510
565
|
options?: { limit?: number; cursor?: string }
|
|
511
566
|
): Promise<ListIngotsResponse> {
|
|
512
567
|
validateVaultId(vaultId);
|
|
513
|
-
const
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
const query = params.toString();
|
|
518
|
-
const response = await this.http.get<ListIngotsResponse>(
|
|
519
|
-
query ? `/vaults/${vaultId}/ingots?${query}` : `/vaults/${vaultId}/ingots`,
|
|
520
|
-
{ vat }
|
|
521
|
-
);
|
|
568
|
+
const response = await this.http.get<ListIngotsResponse>(apiPath`/vaults/${vaultId}/ingots`, {
|
|
569
|
+
vat,
|
|
570
|
+
query: { limit: options?.limit || undefined, cursor: options?.cursor },
|
|
571
|
+
});
|
|
522
572
|
return response.data;
|
|
523
573
|
}
|
|
524
574
|
|
|
575
|
+
/**
|
|
576
|
+
* List a folder, or the vault root, whole: the folder, its breadcrumb, its
|
|
577
|
+
* direct subfolders and every ingot directly in it, in one response.
|
|
578
|
+
*/
|
|
525
579
|
async listContents(
|
|
526
580
|
vaultId: string,
|
|
527
581
|
vat: string,
|
|
528
582
|
options?: {
|
|
529
583
|
folder_id?: string | null;
|
|
530
|
-
|
|
531
|
-
limit?: number;
|
|
584
|
+
/** Ingot sort field (name | size | content_type | created | access_count | bandwidth); `created` by default */
|
|
532
585
|
sort_by?: string;
|
|
586
|
+
/** `desc` by default */
|
|
533
587
|
sort_order?: 'asc' | 'desc';
|
|
534
|
-
type?: string;
|
|
535
588
|
}
|
|
536
|
-
): Promise<
|
|
589
|
+
): Promise<FolderContentsResponse> {
|
|
537
590
|
validateVaultId(vaultId);
|
|
538
|
-
const
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
const query = params.toString();
|
|
547
|
-
const response = await this.http.get<ListIngotsResponse>(
|
|
548
|
-
query ? `/vaults/${vaultId}/contents?${query}` : `/vaults/${vaultId}/contents`,
|
|
549
|
-
{ vat }
|
|
550
|
-
);
|
|
591
|
+
const response = await this.http.get<FolderContentsResponse>(apiPath`/vaults/${vaultId}/contents`, {
|
|
592
|
+
vat,
|
|
593
|
+
query: {
|
|
594
|
+
folder_id: options?.folder_id,
|
|
595
|
+
sort_by: options?.sort_by,
|
|
596
|
+
sort_order: options?.sort_order,
|
|
597
|
+
},
|
|
598
|
+
});
|
|
551
599
|
return response.data;
|
|
552
600
|
}
|
|
553
601
|
|
|
@@ -566,35 +614,36 @@ export class MobileIngotsClient {
|
|
|
566
614
|
}
|
|
567
615
|
): Promise<ListIngotsResponse> {
|
|
568
616
|
validateVaultId(vaultId);
|
|
569
|
-
const
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
{ vat }
|
|
583
|
-
);
|
|
617
|
+
const response = await this.http.get<ListIngotsResponse>(apiPath`/vaults/${vaultId}/ingots/search`, {
|
|
618
|
+
vat,
|
|
619
|
+
query: {
|
|
620
|
+
q: options?.q,
|
|
621
|
+
file_type: options?.file_type === 'all' ? undefined : options?.file_type,
|
|
622
|
+
folder_id: options?.folder_id,
|
|
623
|
+
min_size: options?.min_size,
|
|
624
|
+
max_size: options?.max_size,
|
|
625
|
+
limit: options?.limit || undefined,
|
|
626
|
+
cursor: options?.cursor,
|
|
627
|
+
sort_order: options?.sort_order,
|
|
628
|
+
},
|
|
629
|
+
});
|
|
584
630
|
return response.data;
|
|
585
631
|
}
|
|
586
632
|
|
|
587
633
|
async get(vaultId: string, ingotId: string, vat: string, options?: { consistent?: boolean }): Promise<Ingot> {
|
|
588
634
|
validateVaultId(vaultId);
|
|
589
635
|
validateIngotId(ingotId);
|
|
590
|
-
const response = await this.http.get<Ingot>(`/vaults/${vaultId}/ingots/${ingotId}
|
|
636
|
+
const response = await this.http.get<Ingot>(apiPath`/vaults/${vaultId}/ingots/${ingotId}`, {
|
|
637
|
+
vat,
|
|
638
|
+
query: { consistent: options?.consistent ? 'true' : undefined },
|
|
639
|
+
});
|
|
591
640
|
return response.data;
|
|
592
641
|
}
|
|
593
642
|
|
|
594
643
|
async delete(vaultId: string, ingotId: string, vat: string): Promise<void> {
|
|
595
644
|
validateVaultId(vaultId);
|
|
596
645
|
validateIngotId(ingotId);
|
|
597
|
-
await this.http.delete(`/vaults/${vaultId}/ingots/${ingotId}`, { vat });
|
|
646
|
+
await this.http.delete(apiPath`/vaults/${vaultId}/ingots/${ingotId}`, { vat });
|
|
598
647
|
}
|
|
599
648
|
|
|
600
649
|
/**
|
|
@@ -610,7 +659,7 @@ export class MobileIngotsClient {
|
|
|
610
659
|
|
|
611
660
|
const response = await this.http
|
|
612
661
|
.post<{ download_url?: string; content_version?: string | null }>(
|
|
613
|
-
`/vaults/${vaultId}/ingots/${ingotId}/download`,
|
|
662
|
+
apiPath`/vaults/${vaultId}/ingots/${ingotId}/download`,
|
|
614
663
|
undefined,
|
|
615
664
|
{ vat }
|
|
616
665
|
)
|
|
@@ -719,7 +768,7 @@ export class MobileIngotsClient {
|
|
|
719
768
|
validateVaultId(vaultId);
|
|
720
769
|
validateIngotId(ingotId);
|
|
721
770
|
const response = await this.http.get<IngotSharingConfig>(
|
|
722
|
-
`/vaults/${vaultId}/ingots/${ingotId}/sharing`,
|
|
771
|
+
apiPath`/vaults/${vaultId}/ingots/${ingotId}/sharing`,
|
|
723
772
|
{ vat }
|
|
724
773
|
);
|
|
725
774
|
return response.data;
|
|
@@ -737,7 +786,7 @@ export class MobileIngotsClient {
|
|
|
737
786
|
validateVaultId(vaultId);
|
|
738
787
|
validateIngotId(ingotId);
|
|
739
788
|
const response = await this.http.put<IngotSharingConfig>(
|
|
740
|
-
`/vaults/${vaultId}/ingots/${ingotId}/sharing`,
|
|
789
|
+
apiPath`/vaults/${vaultId}/ingots/${ingotId}/sharing`,
|
|
741
790
|
config,
|
|
742
791
|
{ vat }
|
|
743
792
|
);
|
|
@@ -754,7 +803,7 @@ export class MobileIngotsClient {
|
|
|
754
803
|
validateVaultId(vaultId);
|
|
755
804
|
validateIngotId(ingotId);
|
|
756
805
|
const response = await this.http.post<IngotInvite>(
|
|
757
|
-
`/vaults/${vaultId}/ingots/${ingotId}/sharing/invite`,
|
|
806
|
+
apiPath`/vaults/${vaultId}/ingots/${ingotId}/sharing/invite`,
|
|
758
807
|
{ identity, expires_in_seconds: options.expiresInSeconds },
|
|
759
808
|
{ vat }
|
|
760
809
|
);
|
|
@@ -764,7 +813,8 @@ export class MobileIngotsClient {
|
|
|
764
813
|
async revokeInvite(vaultId: string, ingotId: string, vat: string, inviteId: string): Promise<void> {
|
|
765
814
|
validateVaultId(vaultId);
|
|
766
815
|
validateIngotId(ingotId);
|
|
767
|
-
|
|
816
|
+
validateResourceId(inviteId, 'invite ID', 'inviteId');
|
|
817
|
+
await this.http.delete(apiPath`/vaults/${vaultId}/ingots/${ingotId}/sharing/invite/${inviteId}`, { vat });
|
|
768
818
|
}
|
|
769
819
|
|
|
770
820
|
async getAccessLogs(
|
|
@@ -775,22 +825,25 @@ export class MobileIngotsClient {
|
|
|
775
825
|
): Promise<{ logs: IngotAccessLog[]; next_cursor?: string }> {
|
|
776
826
|
validateVaultId(vaultId);
|
|
777
827
|
validateIngotId(ingotId);
|
|
778
|
-
const params = new URLSearchParams();
|
|
779
|
-
if (options?.limit) params.append('limit', options.limit.toString());
|
|
780
|
-
if (options?.cursor) params.append('cursor', options.cursor);
|
|
781
|
-
|
|
782
|
-
const query = params.toString();
|
|
783
828
|
const response = await this.http.get<IngotAuditLogsResponse>(
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
829
|
+
apiPath`/vaults/${vaultId}/ingots/${ingotId}/audit-logs`,
|
|
830
|
+
{
|
|
831
|
+
vat,
|
|
832
|
+
query: {
|
|
833
|
+
limit: options?.limit || undefined,
|
|
834
|
+
cursor: options?.cursor,
|
|
835
|
+
event_types: INGOT_ACCESS_EVENT_TYPES.join(','),
|
|
836
|
+
},
|
|
837
|
+
}
|
|
788
838
|
);
|
|
789
|
-
const
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
839
|
+
const logs: IngotAccessLog[] = [];
|
|
840
|
+
for (const entry of response.data.entries ?? []) {
|
|
841
|
+
const access = readIngotAccess(entry);
|
|
842
|
+
if (access) {
|
|
843
|
+
logs.push({ log_id: String(access.timestamp), ...access });
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
return { logs, next_cursor: response.data.next_cursor };
|
|
794
847
|
}
|
|
795
848
|
|
|
796
849
|
async getAccessStats(vaultId: string, ingotId: string, vat: string): Promise<IngotAccessStats> {
|
|
@@ -817,8 +870,7 @@ export class MobileIngotsClient {
|
|
|
817
870
|
*
|
|
818
871
|
* The bytes are already fully uploaded and the ingot record exists before
|
|
819
872
|
* this runs, so this NEVER re-creates or re-uploads anything - it only polls
|
|
820
|
-
* status. A `
|
|
821
|
-
* caller should surface). A still-`processing` ingot after the poll budget
|
|
873
|
+
* status. A still-`uploading` ingot after the poll budget
|
|
822
874
|
* is returned as success, not thrown: the transport completed and the ingot
|
|
823
875
|
* exists, so treating it as a failure would make the caller re-run the whole
|
|
824
876
|
* upload and mint a DUPLICATE ingot (re-streaming the entire file and
|
|
@@ -835,10 +887,6 @@ export class MobileIngotsClient {
|
|
|
835
887
|
let ingot = await this.get(vaultId, ingotId, vat);
|
|
836
888
|
|
|
837
889
|
while (ingot.status !== 'active') {
|
|
838
|
-
if (ingot.status === 'failed') {
|
|
839
|
-
throw new SparkVaultMobileError('Upload failed: server encryption error');
|
|
840
|
-
}
|
|
841
|
-
|
|
842
890
|
const remaining = deadline - Date.now();
|
|
843
891
|
if (remaining <= 0) {
|
|
844
892
|
debug?.log(
|
|
@@ -856,28 +904,6 @@ export class MobileIngotsClient {
|
|
|
856
904
|
return ingot;
|
|
857
905
|
}
|
|
858
906
|
|
|
859
|
-
private toAccessLog(entry: IngotAuditLogEntry): IngotAccessLog {
|
|
860
|
-
const eventType = entry.event_type ?? entry.action ?? '';
|
|
861
|
-
const action = eventType.includes('download') ? 'download' : 'view';
|
|
862
|
-
const actor = entry.actor;
|
|
863
|
-
return {
|
|
864
|
-
log_id: entry.log_id ?? entry.event_id ?? entry.id ?? String(entry.timestamp ?? entry.created_at ?? ''),
|
|
865
|
-
action,
|
|
866
|
-
identity:
|
|
867
|
-
actor?.email ??
|
|
868
|
-
entry.actor_email ??
|
|
869
|
-
entry.identity ??
|
|
870
|
-
actor?.id ??
|
|
871
|
-
actor?.user_id ??
|
|
872
|
-
actor?.api_key_id ??
|
|
873
|
-
entry.actor_id ??
|
|
874
|
-
'Unknown',
|
|
875
|
-
ip_address: entry.ip_address ?? '',
|
|
876
|
-
user_agent: entry.user_agent ?? '',
|
|
877
|
-
timestamp: entry.timestamp ?? entry.created_at ?? 0,
|
|
878
|
-
};
|
|
879
|
-
}
|
|
880
|
-
|
|
881
907
|
private async verifyDownloadedFile(
|
|
882
908
|
fileDownloader: MobileFileDownloader,
|
|
883
909
|
preferredUri: string,
|
|
@@ -953,26 +979,14 @@ export class MobileIngotsClient {
|
|
|
953
979
|
});
|
|
954
980
|
}
|
|
955
981
|
|
|
982
|
+
/** Refuse a download link that is not a Forge decrypt URL on a trusted host (forge-url's rule). */
|
|
956
983
|
private validateDownloadUrl(downloadUrl: string): void {
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
}
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
if (url.protocol !== 'https:') {
|
|
965
|
-
throw new SparkVaultValidationError('Invalid download URL from server', {
|
|
966
|
-
reason: 'non_https_download_url',
|
|
967
|
-
});
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
const allowed = this.config.allowedDownloadHostPatterns.some(pattern => pattern.test(url.hostname));
|
|
971
|
-
if (!allowed) {
|
|
972
|
-
throw new SparkVaultValidationError('Invalid download URL from server', {
|
|
973
|
-
reason: 'download_host_not_allowed',
|
|
974
|
-
hostname: url.hostname,
|
|
975
|
-
});
|
|
984
|
+
const read = readForgeTransferUrl(downloadUrl, {
|
|
985
|
+
operation: 'decrypt',
|
|
986
|
+
isAllowedHost: allowsHost(this.config.allowedDownloadHostPatterns),
|
|
987
|
+
});
|
|
988
|
+
if (!read.ok) {
|
|
989
|
+
throw new SparkVaultValidationError('Invalid download URL from server', { reason: read.reason });
|
|
976
990
|
}
|
|
977
991
|
}
|
|
978
992
|
|
package/src/mutex.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
+
/** Runs one operation at a time; callers arriving while it runs share its result. */
|
|
1
2
|
export interface DedupMutex<T> {
|
|
2
3
|
dedupe(fn: () => Promise<T>): Promise<T>;
|
|
3
|
-
isRunning(): boolean;
|
|
4
|
-
reset(): void;
|
|
5
4
|
}
|
|
6
5
|
|
|
7
6
|
export function createDedupMutex<T>(): DedupMutex<T> {
|
|
@@ -16,11 +15,5 @@ export function createDedupMutex<T>(): DedupMutex<T> {
|
|
|
16
15
|
}
|
|
17
16
|
return current;
|
|
18
17
|
},
|
|
19
|
-
isRunning(): boolean {
|
|
20
|
-
return current !== null;
|
|
21
|
-
},
|
|
22
|
-
reset(): void {
|
|
23
|
-
current = null;
|
|
24
|
-
},
|
|
25
18
|
};
|
|
26
19
|
}
|
package/src/notify.test-d.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* which would put these assertions exactly where the compiler cannot see them.
|
|
12
12
|
*
|
|
13
13
|
* The row shape is shared verbatim with `@sparkvault/sdk-js`, whose own contract
|
|
14
|
-
* file asserts the same
|
|
14
|
+
* file asserts the same four members — both must move together, and the mirrored
|
|
15
15
|
* server source is `apps/api/src/notify/inbox.js` (`projectRow`).
|
|
16
16
|
*/
|
|
17
17
|
|
|
@@ -39,4 +39,6 @@ export type NotifyWireContract = [
|
|
|
39
39
|
// The pointer is withheld on a recalled row and absent on a cleartext one, so
|
|
40
40
|
// it is optional in both directions and must be guarded before use.
|
|
41
41
|
Assert<Equals<NotificationRow['sparklink_code'], string | undefined>>,
|
|
42
|
+
// The URL the pointer opens travels with it: absent exactly when the pointer is.
|
|
43
|
+
Assert<Equals<NotificationRow['pointer_url'], string | undefined>>,
|
|
42
44
|
];
|