@sparkvault/sdk-mobile 6.0.2 → 7.0.1
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/transfer-retry.d.ts +298 -0
- package/dist/app-sdk/transfer-retry.js +535 -0
- package/dist/app-sdk/transfer-retry.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/upload-publication.d.ts +81 -0
- package/dist/app-sdk/upload-publication.js +93 -0
- package/dist/app-sdk/upload-publication.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 +33 -1
- package/dist/config.js +23 -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 +105 -21
- package/dist/errors.js +74 -28
- 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 +8 -7
- package/dist/index.js +5 -4
- package/dist/index.js.map +1 -1
- package/dist/ingots.d.ts +77 -32
- package/dist/ingots.js +152 -136
- 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 +198 -16
- package/dist/tus.js +816 -327
- package/dist/tus.js.map +1 -1
- package/dist/types.d.ts +183 -23
- 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/transfer-retry.js +563 -0
- package/src/app-sdk/transient.js +30 -0
- package/src/app-sdk/upload-publication.js +102 -0
- package/src/app-sdk/vat-refusal.js +74 -0
- package/src/billing.ts +16 -25
- package/src/config.ts +56 -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 +135 -38
- 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 +13 -3
- package/src/ingots.ts +225 -180
- 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 +1054 -408
- package/src/types.ts +181 -23
- 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,16 @@
|
|
|
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,
|
|
10
|
+
TusUploadError,
|
|
7
11
|
} from './errors.js';
|
|
8
12
|
import type { MobileHttpClient } from './http.js';
|
|
9
|
-
import { MobileTusUploader } from './tus.js';
|
|
13
|
+
import { allowsHost, MobileTusUploader, type TusSessionProbe } from './tus.js';
|
|
10
14
|
import type {
|
|
11
15
|
DebugLogger,
|
|
12
16
|
HashCheckResponse,
|
|
@@ -16,6 +20,7 @@ import type {
|
|
|
16
20
|
IngotInvite,
|
|
17
21
|
IngotSharingConfig,
|
|
18
22
|
ListIngotsResponse,
|
|
23
|
+
FolderContentsResponse,
|
|
19
24
|
IngotUploadSession,
|
|
20
25
|
MobileFileDownloader,
|
|
21
26
|
UploadAbortBehavior,
|
|
@@ -24,7 +29,26 @@ import type {
|
|
|
24
29
|
UploadTransferPolicy,
|
|
25
30
|
UploadTransport,
|
|
26
31
|
} from './types.js';
|
|
27
|
-
import { validateIngotId, validateVaultId } from './validation.js';
|
|
32
|
+
import { validateIngotId, validateResourceId, validateVaultId } from './validation.js';
|
|
33
|
+
import { apiPath } from './request-path.js';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* What the server does when a create's name is already held in the target
|
|
37
|
+
* folder (case-insensitive). The server decides on the holder it read before
|
|
38
|
+
* writing anything, so a caller that must not replace content says so here:
|
|
39
|
+
* - `upsert` (the server default): the upload replaces the holder's content.
|
|
40
|
+
* - `complete_pending`: a holder whose own upload never finalized is completed
|
|
41
|
+
* (`created: false`); a published holder is refused with
|
|
42
|
+
* `IngotNameHeldError`, untouched.
|
|
43
|
+
* - `refuse`: any holder, its upload finished or not, is refused with
|
|
44
|
+
* `IngotNameHeldError`, untouched; the create only ever mints.
|
|
45
|
+
* - `new_name`: the holder is never touched; the ingot is minted under the
|
|
46
|
+
* first free numbered name the server picks ("report (2).pdf"), returned
|
|
47
|
+
* as the result's `name`.
|
|
48
|
+
*/
|
|
49
|
+
export const NAME_CONFLICT_POLICIES = ['upsert', 'complete_pending', 'refuse', 'new_name'] as const;
|
|
50
|
+
|
|
51
|
+
export type NameConflictPolicy = (typeof NAME_CONFLICT_POLICIES)[number];
|
|
28
52
|
|
|
29
53
|
export interface UploadOptions {
|
|
30
54
|
vaultId: string;
|
|
@@ -55,20 +79,31 @@ export interface UploadOptions {
|
|
|
55
79
|
export interface UploadFromUriOptions extends UploadOptions {
|
|
56
80
|
fileUri: string;
|
|
57
81
|
fileSize: number;
|
|
82
|
+
/** What a name already held in the target folder does; omitted means the server's `upsert`. */
|
|
83
|
+
onNameConflict?: NameConflictPolicy;
|
|
58
84
|
/**
|
|
59
|
-
* Invoked once the
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
85
|
+
* Invoked once the server has placed the upload and before any Forge
|
|
86
|
+
* session exists: `created` is true when it minted the ingot and false when
|
|
87
|
+
* it staged the upload onto the row holding the name (as `onNameConflict`
|
|
88
|
+
* allowed), and `name` is the name the ingot is stored under (the server's
|
|
89
|
+
* numbered name under `new_name`). The placement has already been written
|
|
90
|
+
* by then, so this hook observes; a caller that must not replace content
|
|
91
|
+
* says so through `onNameConflict`. A throw here still stops the upload
|
|
92
|
+
* before a byte moves and propagates as-is.
|
|
64
93
|
*/
|
|
65
|
-
onIngotCreated?: (ingot: { ingotId: string; created: boolean }) => void;
|
|
94
|
+
onIngotCreated?: (ingot: { ingotId: string; created: boolean; name: string }) => void;
|
|
66
95
|
/**
|
|
67
96
|
* Invoked once the Forge tus session exists and before the first byte is
|
|
68
97
|
* sent, so the caller can persist it and `resumeUpload` after a failed
|
|
69
98
|
* attempt or a relaunch instead of re-sending bytes Forge already holds.
|
|
70
99
|
*/
|
|
71
100
|
onSessionCreated?: (session: IngotUploadSession) => void;
|
|
101
|
+
/**
|
|
102
|
+
* Invoked whenever the session's expiry changes (see
|
|
103
|
+
* `TusUploadFromUriOptions.onSessionExpiry`): a caller that persists the
|
|
104
|
+
* session keeps its `expiresAt` current with it.
|
|
105
|
+
*/
|
|
106
|
+
onSessionExpiry?: (expiresAt: number | null) => void;
|
|
72
107
|
/** See `UploadTransport`; default `foreground`. */
|
|
73
108
|
transport?: UploadTransport;
|
|
74
109
|
/** Network policy for the OS transfer engine; see `UploadTransferPolicy`. Default: both allowed. */
|
|
@@ -86,7 +121,8 @@ export interface UploadFromUriOptions extends UploadOptions {
|
|
|
86
121
|
debug?: DebugLogger;
|
|
87
122
|
}
|
|
88
123
|
|
|
89
|
-
|
|
124
|
+
/** A replace addresses its ingot by id: no name is placed, so the create-only hooks do not apply. */
|
|
125
|
+
export interface ReplaceFromUriOptions extends Omit<UploadFromUriOptions, 'onNameConflict' | 'onIngotCreated'> {
|
|
90
126
|
ingotId: string;
|
|
91
127
|
}
|
|
92
128
|
|
|
@@ -98,9 +134,14 @@ export interface ResumeUploadOptions {
|
|
|
98
134
|
fileUri: string;
|
|
99
135
|
fileSize: number;
|
|
100
136
|
name: string;
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
137
|
+
/**
|
|
138
|
+
* The persisted tus session handed to `onSessionCreated`, with the expiry
|
|
139
|
+
* the caller keeps for it (`onSessionExpiry`): a session past it is
|
|
140
|
+
* refused as `sessionLost` before any request.
|
|
141
|
+
*/
|
|
142
|
+
session: { uploadUrl: string; istk: string; expiresAt?: number | null };
|
|
143
|
+
/** See `UploadFromUriOptions.onSessionExpiry`. */
|
|
144
|
+
onSessionExpiry?: (expiresAt: number | null) => void;
|
|
104
145
|
/** See `UploadTransport`; default `foreground`. */
|
|
105
146
|
transport?: UploadTransport;
|
|
106
147
|
/** Network policy for the OS transfer engine; see `UploadTransferPolicy`. Default: both allowed. */
|
|
@@ -161,17 +202,20 @@ export interface CreateUploadOptions {
|
|
|
161
202
|
folderId?: string;
|
|
162
203
|
/** Vault-keyed plaintext HMAC (43-char base64url); see UploadOptions.contentHmac. */
|
|
163
204
|
contentHmac?: string;
|
|
205
|
+
/** What a name already held in the target folder does; omitted means the server's `upsert`. */
|
|
206
|
+
onNameConflict?: NameConflictPolicy;
|
|
164
207
|
}
|
|
165
208
|
|
|
166
209
|
export interface CreateUploadResult {
|
|
167
210
|
ingotId: string;
|
|
168
211
|
/** Forge tus endpoint plus upload-session token (ISTK) for the resumable upload. */
|
|
169
212
|
forgeUrl: string;
|
|
213
|
+
/** The name the ingot is stored under: the server's numbered name under `new_name`. */
|
|
170
214
|
name?: string;
|
|
171
215
|
sizeBytes?: number;
|
|
172
216
|
/**
|
|
173
|
-
* Whether the server minted this ingot (true) or
|
|
174
|
-
* row
|
|
217
|
+
* Whether the server minted this ingot (true) or staged the upload onto
|
|
218
|
+
* the row already holding the name (false). Absent when the server did not say.
|
|
175
219
|
*/
|
|
176
220
|
created?: boolean;
|
|
177
221
|
}
|
|
@@ -196,29 +240,23 @@ interface IngotUploadInitResponse {
|
|
|
196
240
|
created?: boolean;
|
|
197
241
|
}
|
|
198
242
|
|
|
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;
|
|
243
|
+
/** An ingot audit entry in the server's public shape. */
|
|
244
|
+
interface IngotAuditEntry {
|
|
245
|
+
timestamp: number;
|
|
246
|
+
event_type: string;
|
|
247
|
+
actor_id?: string | null;
|
|
248
|
+
actor_type?: string;
|
|
249
|
+
actor_display?: string;
|
|
250
|
+
ip_address?: string | null;
|
|
251
|
+
user_agent?: string | null;
|
|
252
|
+
metadata?: { identity?: string | null; [key: string]: unknown } | null;
|
|
253
|
+
encrypted?: boolean;
|
|
254
|
+
vault_id?: string;
|
|
255
|
+
ingot_id?: string;
|
|
218
256
|
}
|
|
219
257
|
|
|
220
258
|
interface IngotAuditLogsResponse {
|
|
221
|
-
entries?:
|
|
259
|
+
entries?: IngotAuditEntry[];
|
|
222
260
|
next_cursor?: string;
|
|
223
261
|
}
|
|
224
262
|
|
|
@@ -243,6 +281,28 @@ function toTypedDownloadError(err: unknown): unknown {
|
|
|
243
281
|
return err;
|
|
244
282
|
}
|
|
245
283
|
|
|
284
|
+
/**
|
|
285
|
+
* A create refused over its holder (`complete_pending` over a published one,
|
|
286
|
+
* `refuse` over any) answers 409 CONFLICT with `details.reason === 'NAME_HELD'`
|
|
287
|
+
* and the holder the server read. Every other 409 on the create (a folder
|
|
288
|
+
* being deleted, a cross-manager name) carries no holder and stays a plain
|
|
289
|
+
* `SparkVaultMobileError`.
|
|
290
|
+
*/
|
|
291
|
+
function toTypedCreateError(err: unknown): unknown {
|
|
292
|
+
if (!(err instanceof SparkVaultMobileError) || err.statusCode !== 409 || err.details?.reason !== 'NAME_HELD') {
|
|
293
|
+
return err;
|
|
294
|
+
}
|
|
295
|
+
const { ingot_id: ingotId, status, size_bytes: sizeBytes } = err.details;
|
|
296
|
+
if (typeof ingotId !== 'string' || typeof status !== 'string' || typeof sizeBytes !== 'number') {
|
|
297
|
+
return err;
|
|
298
|
+
}
|
|
299
|
+
return new IngotNameHeldError(err.message, {
|
|
300
|
+
holder: { ingotId, status, sizeBytes },
|
|
301
|
+
details: err.details,
|
|
302
|
+
meta: err.meta,
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
246
306
|
export class MobileIngotsClient {
|
|
247
307
|
// Server-side finalization (encryption) of a just-uploaded ingot runs
|
|
248
308
|
// asynchronously and scales with file size, so a large video can sit in
|
|
@@ -280,21 +340,23 @@ export class MobileIngotsClient {
|
|
|
280
340
|
folder: options.folder,
|
|
281
341
|
folderId: options.folderId,
|
|
282
342
|
contentHmac: options.contentHmac,
|
|
343
|
+
onNameConflict: options.onNameConflict,
|
|
283
344
|
});
|
|
284
345
|
const created = init.created ?? true;
|
|
346
|
+
const name = init.name ?? options.name;
|
|
285
347
|
|
|
286
|
-
// The
|
|
287
|
-
//
|
|
288
|
-
//
|
|
289
|
-
options.onIngotCreated?.({ ingotId: init.ingotId, created });
|
|
348
|
+
// The placement is written but no Forge session exists yet: a throw from
|
|
349
|
+
// the hook stops the upload before a byte moves and reaches the caller
|
|
350
|
+
// untouched.
|
|
351
|
+
options.onIngotCreated?.({ ingotId: init.ingotId, created, name });
|
|
290
352
|
|
|
291
353
|
await this.tus.uploadFromUri({
|
|
292
354
|
fileUri: options.fileUri,
|
|
293
355
|
fileSize: options.fileSize,
|
|
294
|
-
filename:
|
|
295
|
-
contentType: options.contentType,
|
|
356
|
+
filename: name,
|
|
296
357
|
forgeUrl: init.forgeUrl,
|
|
297
358
|
onSessionCreated: session => options.onSessionCreated?.({ ...session, ingotId: init.ingotId, created }),
|
|
359
|
+
onSessionExpiry: options.onSessionExpiry,
|
|
298
360
|
transport: options.transport,
|
|
299
361
|
transferPolicy: options.transferPolicy,
|
|
300
362
|
onProgress: options.onProgress,
|
|
@@ -303,11 +365,15 @@ export class MobileIngotsClient {
|
|
|
303
365
|
debug: options.debug,
|
|
304
366
|
});
|
|
305
367
|
|
|
306
|
-
const ingot = await this.verifyIngotActive(
|
|
368
|
+
const ingot = await this.verifyIngotActive(
|
|
369
|
+
{ vaultId: options.vaultId, ingotId: init.ingotId, vat: options.vat },
|
|
370
|
+
{ filename: name, fileSize: options.fileSize },
|
|
371
|
+
options.debug
|
|
372
|
+
);
|
|
307
373
|
|
|
308
374
|
return {
|
|
309
375
|
ingot_id: init.ingotId,
|
|
310
|
-
name
|
|
376
|
+
name,
|
|
311
377
|
size_bytes: init.sizeBytes ?? options.fileSize,
|
|
312
378
|
storage_location: 's3',
|
|
313
379
|
// A server that does not report it minted the row (the upsert is the
|
|
@@ -325,8 +391,10 @@ export class MobileIngotsClient {
|
|
|
325
391
|
* sent, then the ingot is verified active exactly as a fresh upload is.
|
|
326
392
|
*
|
|
327
393
|
* A `TusUploadError` with `sessionLost` means Forge no longer has the
|
|
328
|
-
* session (
|
|
329
|
-
* with `uploadFromUri`.
|
|
394
|
+
* session (its expiry passed, or it was terminated); the caller drops it
|
|
395
|
+
* and starts over with `uploadFromUri`. A session `uploadUrl` that is not
|
|
396
|
+
* an upload session on an allowed transfer host is refused
|
|
397
|
+
* (SparkVaultValidationError) before the ISTK or a byte is sent anywhere.
|
|
330
398
|
*/
|
|
331
399
|
async resumeUpload(options: ResumeUploadOptions): Promise<UploadResult> {
|
|
332
400
|
validateVaultId(options.vaultId);
|
|
@@ -339,8 +407,8 @@ export class MobileIngotsClient {
|
|
|
339
407
|
fileUri: options.fileUri,
|
|
340
408
|
fileSize: options.fileSize,
|
|
341
409
|
filename: options.name,
|
|
342
|
-
contentType: options.contentType,
|
|
343
410
|
resume: options.session,
|
|
411
|
+
onSessionExpiry: options.onSessionExpiry,
|
|
344
412
|
transport: options.transport,
|
|
345
413
|
transferPolicy: options.transferPolicy,
|
|
346
414
|
onProgress: options.onProgress,
|
|
@@ -349,7 +417,11 @@ export class MobileIngotsClient {
|
|
|
349
417
|
debug: options.debug,
|
|
350
418
|
});
|
|
351
419
|
|
|
352
|
-
const ingot = await this.verifyIngotActive(
|
|
420
|
+
const ingot = await this.verifyIngotActive(
|
|
421
|
+
{ vaultId: options.vaultId, ingotId: options.ingotId, vat: options.vat },
|
|
422
|
+
{ filename: options.name, fileSize: options.fileSize },
|
|
423
|
+
options.debug
|
|
424
|
+
);
|
|
353
425
|
|
|
354
426
|
return {
|
|
355
427
|
ingot_id: options.ingotId,
|
|
@@ -366,10 +438,10 @@ export class MobileIngotsClient {
|
|
|
366
438
|
* longer has it. Lets a caller reconcile sessions on relaunch — a session
|
|
367
439
|
* at `offset === length` only needs its ingot verified, a lost one needs a
|
|
368
440
|
* fresh `uploadFromUri` — without spending an upload slot to find out.
|
|
441
|
+
* An `uploadUrl` that is not an upload session on an allowed transfer host
|
|
442
|
+
* is refused (SparkVaultValidationError) before the ISTK is sent anywhere.
|
|
369
443
|
*/
|
|
370
|
-
probeUploadSession(
|
|
371
|
-
session: { uploadUrl: string; istk: string }
|
|
372
|
-
): Promise<{ offset: number; length: number; chunkSize: number } | null> {
|
|
444
|
+
probeUploadSession(session: { uploadUrl: string; istk: string }): Promise<TusSessionProbe | null> {
|
|
373
445
|
return this.tus.probeSession(session);
|
|
374
446
|
}
|
|
375
447
|
|
|
@@ -379,6 +451,8 @@ export class MobileIngotsClient {
|
|
|
379
451
|
* the partial transfer's billing instead of waiting for expiry. Resolves
|
|
380
452
|
* once the session is gone, including when Forge no longer had it
|
|
381
453
|
* (404/410); any other failure rejects so the caller can retry later.
|
|
454
|
+
* An `uploadUrl` that is not an upload session on an allowed transfer host
|
|
455
|
+
* is refused (SparkVaultValidationError) before the ISTK is sent anywhere.
|
|
382
456
|
*/
|
|
383
457
|
terminateUploadSession(session: { uploadUrl: string; istk: string }): Promise<void> {
|
|
384
458
|
return this.tus.terminateSession(session);
|
|
@@ -406,11 +480,14 @@ export class MobileIngotsClient {
|
|
|
406
480
|
if (options.contentHmac) {
|
|
407
481
|
body.content_hmac = options.contentHmac;
|
|
408
482
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
483
|
+
if (options.onNameConflict) {
|
|
484
|
+
body.on_name_conflict = options.onNameConflict;
|
|
485
|
+
}
|
|
486
|
+
const response = await this.http
|
|
487
|
+
.post<IngotUploadInitResponse>(apiPath`/vaults/${options.vaultId}/ingots`, body, { vat: options.vat })
|
|
488
|
+
.catch((err: unknown) => {
|
|
489
|
+
throw toTypedCreateError(err);
|
|
490
|
+
});
|
|
414
491
|
|
|
415
492
|
if (!response.data.ingot_id || !response.data.forge_url) {
|
|
416
493
|
throw new SparkVaultMobileError('Server returned invalid response for ingot creation');
|
|
@@ -435,7 +512,7 @@ export class MobileIngotsClient {
|
|
|
435
512
|
}
|
|
436
513
|
|
|
437
514
|
const response = await this.http.put<IngotUploadInitResponse>(
|
|
438
|
-
`/vaults/${vaultId}/ingots/${ingotId}`,
|
|
515
|
+
apiPath`/vaults/${vaultId}/ingots/${ingotId}`,
|
|
439
516
|
{
|
|
440
517
|
name,
|
|
441
518
|
content_type: contentType,
|
|
@@ -453,10 +530,10 @@ export class MobileIngotsClient {
|
|
|
453
530
|
fileUri,
|
|
454
531
|
fileSize,
|
|
455
532
|
filename: name,
|
|
456
|
-
contentType,
|
|
457
533
|
forgeUrl: response.data.forge_url,
|
|
458
534
|
// A replace never mints an ingot: the row is the caller's existing one.
|
|
459
535
|
onSessionCreated: session => options.onSessionCreated?.({ ...session, ingotId, created: false }),
|
|
536
|
+
onSessionExpiry: options.onSessionExpiry,
|
|
460
537
|
transport: options.transport,
|
|
461
538
|
transferPolicy: options.transferPolicy,
|
|
462
539
|
onProgress,
|
|
@@ -465,7 +542,11 @@ export class MobileIngotsClient {
|
|
|
465
542
|
debug,
|
|
466
543
|
});
|
|
467
544
|
|
|
468
|
-
const ingot = await this.verifyIngotActive(
|
|
545
|
+
const ingot = await this.verifyIngotActive(
|
|
546
|
+
{ vaultId, ingotId: response.data.ingot_id, vat },
|
|
547
|
+
{ filename: name, fileSize },
|
|
548
|
+
debug
|
|
549
|
+
);
|
|
469
550
|
|
|
470
551
|
return {
|
|
471
552
|
ingot_id: response.data.ingot_id,
|
|
@@ -490,7 +571,7 @@ export class MobileIngotsClient {
|
|
|
490
571
|
): Promise<HashCheckResponse> {
|
|
491
572
|
validateVaultId(vaultId);
|
|
492
573
|
const response = await this.http.post<HashCheckResponse>(
|
|
493
|
-
`/vaults/${vaultId}/ingots/hash-check`,
|
|
574
|
+
apiPath`/vaults/${vaultId}/ingots/hash-check`,
|
|
494
575
|
{ hashes },
|
|
495
576
|
{ vat }
|
|
496
577
|
);
|
|
@@ -500,7 +581,7 @@ export class MobileIngotsClient {
|
|
|
500
581
|
async rename(vaultId: string, ingotId: string, vat: string, name: string): Promise<Ingot> {
|
|
501
582
|
validateVaultId(vaultId);
|
|
502
583
|
validateIngotId(ingotId);
|
|
503
|
-
const response = await this.http.patch<Ingot>(`/vaults/${vaultId}/ingots/${ingotId}`, { name }, { vat });
|
|
584
|
+
const response = await this.http.patch<Ingot>(apiPath`/vaults/${vaultId}/ingots/${ingotId}`, { name }, { vat });
|
|
504
585
|
return response.data;
|
|
505
586
|
}
|
|
506
587
|
|
|
@@ -510,44 +591,37 @@ export class MobileIngotsClient {
|
|
|
510
591
|
options?: { limit?: number; cursor?: string }
|
|
511
592
|
): Promise<ListIngotsResponse> {
|
|
512
593
|
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
|
-
);
|
|
594
|
+
const response = await this.http.get<ListIngotsResponse>(apiPath`/vaults/${vaultId}/ingots`, {
|
|
595
|
+
vat,
|
|
596
|
+
query: { limit: options?.limit || undefined, cursor: options?.cursor },
|
|
597
|
+
});
|
|
522
598
|
return response.data;
|
|
523
599
|
}
|
|
524
600
|
|
|
601
|
+
/**
|
|
602
|
+
* List a folder, or the vault root, whole: the folder, its breadcrumb, its
|
|
603
|
+
* direct subfolders and every ingot directly in it, in one response.
|
|
604
|
+
*/
|
|
525
605
|
async listContents(
|
|
526
606
|
vaultId: string,
|
|
527
607
|
vat: string,
|
|
528
608
|
options?: {
|
|
529
609
|
folder_id?: string | null;
|
|
530
|
-
|
|
531
|
-
limit?: number;
|
|
610
|
+
/** Ingot sort field (name | size | content_type | created | access_count | bandwidth); `created` by default */
|
|
532
611
|
sort_by?: string;
|
|
612
|
+
/** `desc` by default */
|
|
533
613
|
sort_order?: 'asc' | 'desc';
|
|
534
|
-
type?: string;
|
|
535
614
|
}
|
|
536
|
-
): Promise<
|
|
615
|
+
): Promise<FolderContentsResponse> {
|
|
537
616
|
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
|
-
);
|
|
617
|
+
const response = await this.http.get<FolderContentsResponse>(apiPath`/vaults/${vaultId}/contents`, {
|
|
618
|
+
vat,
|
|
619
|
+
query: {
|
|
620
|
+
folder_id: options?.folder_id,
|
|
621
|
+
sort_by: options?.sort_by,
|
|
622
|
+
sort_order: options?.sort_order,
|
|
623
|
+
},
|
|
624
|
+
});
|
|
551
625
|
return response.data;
|
|
552
626
|
}
|
|
553
627
|
|
|
@@ -566,35 +640,36 @@ export class MobileIngotsClient {
|
|
|
566
640
|
}
|
|
567
641
|
): Promise<ListIngotsResponse> {
|
|
568
642
|
validateVaultId(vaultId);
|
|
569
|
-
const
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
{ vat }
|
|
583
|
-
);
|
|
643
|
+
const response = await this.http.get<ListIngotsResponse>(apiPath`/vaults/${vaultId}/ingots/search`, {
|
|
644
|
+
vat,
|
|
645
|
+
query: {
|
|
646
|
+
q: options?.q,
|
|
647
|
+
file_type: options?.file_type === 'all' ? undefined : options?.file_type,
|
|
648
|
+
folder_id: options?.folder_id,
|
|
649
|
+
min_size: options?.min_size,
|
|
650
|
+
max_size: options?.max_size,
|
|
651
|
+
limit: options?.limit || undefined,
|
|
652
|
+
cursor: options?.cursor,
|
|
653
|
+
sort_order: options?.sort_order,
|
|
654
|
+
},
|
|
655
|
+
});
|
|
584
656
|
return response.data;
|
|
585
657
|
}
|
|
586
658
|
|
|
587
659
|
async get(vaultId: string, ingotId: string, vat: string, options?: { consistent?: boolean }): Promise<Ingot> {
|
|
588
660
|
validateVaultId(vaultId);
|
|
589
661
|
validateIngotId(ingotId);
|
|
590
|
-
const response = await this.http.get<Ingot>(`/vaults/${vaultId}/ingots/${ingotId}
|
|
662
|
+
const response = await this.http.get<Ingot>(apiPath`/vaults/${vaultId}/ingots/${ingotId}`, {
|
|
663
|
+
vat,
|
|
664
|
+
query: { consistent: options?.consistent ? 'true' : undefined },
|
|
665
|
+
});
|
|
591
666
|
return response.data;
|
|
592
667
|
}
|
|
593
668
|
|
|
594
669
|
async delete(vaultId: string, ingotId: string, vat: string): Promise<void> {
|
|
595
670
|
validateVaultId(vaultId);
|
|
596
671
|
validateIngotId(ingotId);
|
|
597
|
-
await this.http.delete(`/vaults/${vaultId}/ingots/${ingotId}`, { vat });
|
|
672
|
+
await this.http.delete(apiPath`/vaults/${vaultId}/ingots/${ingotId}`, { vat });
|
|
598
673
|
}
|
|
599
674
|
|
|
600
675
|
/**
|
|
@@ -610,7 +685,7 @@ export class MobileIngotsClient {
|
|
|
610
685
|
|
|
611
686
|
const response = await this.http
|
|
612
687
|
.post<{ download_url?: string; content_version?: string | null }>(
|
|
613
|
-
`/vaults/${vaultId}/ingots/${ingotId}/download`,
|
|
688
|
+
apiPath`/vaults/${vaultId}/ingots/${ingotId}/download`,
|
|
614
689
|
undefined,
|
|
615
690
|
{ vat }
|
|
616
691
|
)
|
|
@@ -719,7 +794,7 @@ export class MobileIngotsClient {
|
|
|
719
794
|
validateVaultId(vaultId);
|
|
720
795
|
validateIngotId(ingotId);
|
|
721
796
|
const response = await this.http.get<IngotSharingConfig>(
|
|
722
|
-
`/vaults/${vaultId}/ingots/${ingotId}/sharing`,
|
|
797
|
+
apiPath`/vaults/${vaultId}/ingots/${ingotId}/sharing`,
|
|
723
798
|
{ vat }
|
|
724
799
|
);
|
|
725
800
|
return response.data;
|
|
@@ -737,7 +812,7 @@ export class MobileIngotsClient {
|
|
|
737
812
|
validateVaultId(vaultId);
|
|
738
813
|
validateIngotId(ingotId);
|
|
739
814
|
const response = await this.http.put<IngotSharingConfig>(
|
|
740
|
-
`/vaults/${vaultId}/ingots/${ingotId}/sharing`,
|
|
815
|
+
apiPath`/vaults/${vaultId}/ingots/${ingotId}/sharing`,
|
|
741
816
|
config,
|
|
742
817
|
{ vat }
|
|
743
818
|
);
|
|
@@ -754,7 +829,7 @@ export class MobileIngotsClient {
|
|
|
754
829
|
validateVaultId(vaultId);
|
|
755
830
|
validateIngotId(ingotId);
|
|
756
831
|
const response = await this.http.post<IngotInvite>(
|
|
757
|
-
`/vaults/${vaultId}/ingots/${ingotId}/sharing/invite`,
|
|
832
|
+
apiPath`/vaults/${vaultId}/ingots/${ingotId}/sharing/invite`,
|
|
758
833
|
{ identity, expires_in_seconds: options.expiresInSeconds },
|
|
759
834
|
{ vat }
|
|
760
835
|
);
|
|
@@ -764,7 +839,8 @@ export class MobileIngotsClient {
|
|
|
764
839
|
async revokeInvite(vaultId: string, ingotId: string, vat: string, inviteId: string): Promise<void> {
|
|
765
840
|
validateVaultId(vaultId);
|
|
766
841
|
validateIngotId(ingotId);
|
|
767
|
-
|
|
842
|
+
validateResourceId(inviteId, 'invite ID', 'inviteId');
|
|
843
|
+
await this.http.delete(apiPath`/vaults/${vaultId}/ingots/${ingotId}/sharing/invite/${inviteId}`, { vat });
|
|
768
844
|
}
|
|
769
845
|
|
|
770
846
|
async getAccessLogs(
|
|
@@ -775,22 +851,25 @@ export class MobileIngotsClient {
|
|
|
775
851
|
): Promise<{ logs: IngotAccessLog[]; next_cursor?: string }> {
|
|
776
852
|
validateVaultId(vaultId);
|
|
777
853
|
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
854
|
const response = await this.http.get<IngotAuditLogsResponse>(
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
855
|
+
apiPath`/vaults/${vaultId}/ingots/${ingotId}/audit-logs`,
|
|
856
|
+
{
|
|
857
|
+
vat,
|
|
858
|
+
query: {
|
|
859
|
+
limit: options?.limit || undefined,
|
|
860
|
+
cursor: options?.cursor,
|
|
861
|
+
event_types: INGOT_ACCESS_EVENT_TYPES.join(','),
|
|
862
|
+
},
|
|
863
|
+
}
|
|
788
864
|
);
|
|
789
|
-
const
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
865
|
+
const logs: IngotAccessLog[] = [];
|
|
866
|
+
for (const entry of response.data.entries ?? []) {
|
|
867
|
+
const access = readIngotAccess(entry);
|
|
868
|
+
if (access) {
|
|
869
|
+
logs.push({ log_id: String(access.timestamp), ...access });
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
return { logs, next_cursor: response.data.next_cursor };
|
|
794
873
|
}
|
|
795
874
|
|
|
796
875
|
async getAccessStats(vaultId: string, ingotId: string, vat: string): Promise<IngotAccessStats> {
|
|
@@ -813,39 +892,39 @@ export class MobileIngotsClient {
|
|
|
813
892
|
}
|
|
814
893
|
|
|
815
894
|
/**
|
|
816
|
-
*
|
|
895
|
+
* Read a just-uploaded ingot until the server reports it `active`.
|
|
817
896
|
*
|
|
818
|
-
* The
|
|
819
|
-
*
|
|
820
|
-
*
|
|
821
|
-
*
|
|
822
|
-
*
|
|
823
|
-
*
|
|
824
|
-
*
|
|
825
|
-
*
|
|
897
|
+
* The upload resolved on Forge's receipt (a 2xx to the PATCH at the final
|
|
898
|
+
* offset, which Forge answers only once the ingot is published), so a read
|
|
899
|
+
* that is not `active` yet is a read lagging that publication, which the
|
|
900
|
+
* poll waits out. It never re-creates or re-uploads anything. A row still
|
|
901
|
+
* not `active` past the poll budget is never reported as a success: it
|
|
902
|
+
* throws a `TusUploadError` whose `retryLater` is set (every byte is
|
|
903
|
+
* received, publication is unconfirmed), so the caller resumes the session
|
|
904
|
+
* it persisted later, which asks Forge for the receipt again and sends no
|
|
905
|
+
* bytes, rather than recording a backup the vault may not hold.
|
|
826
906
|
*/
|
|
827
907
|
private async verifyIngotActive(
|
|
828
|
-
vaultId: string,
|
|
829
|
-
|
|
830
|
-
vat: string,
|
|
908
|
+
ingotRef: { vaultId: string; ingotId: string; vat: string },
|
|
909
|
+
upload: { filename: string; fileSize: number },
|
|
831
910
|
debug?: DebugLogger
|
|
832
911
|
): Promise<Ingot> {
|
|
912
|
+
const { vaultId, ingotId, vat } = ingotRef;
|
|
833
913
|
const deadline = Date.now() + MobileIngotsClient.INGOT_STATUS_POLL_BUDGET_MS;
|
|
834
914
|
let delay = MobileIngotsClient.INGOT_STATUS_POLL_INITIAL_MS;
|
|
835
915
|
let ingot = await this.get(vaultId, ingotId, vat);
|
|
836
916
|
|
|
837
917
|
while (ingot.status !== 'active') {
|
|
838
|
-
if (ingot.status === 'failed') {
|
|
839
|
-
throw new SparkVaultMobileError('Upload failed: server encryption error');
|
|
840
|
-
}
|
|
841
|
-
|
|
842
918
|
const remaining = deadline - Date.now();
|
|
843
919
|
if (remaining <= 0) {
|
|
844
920
|
debug?.log(
|
|
845
921
|
`Ingot ${ingotId} still '${ingot.status}' after ${MobileIngotsClient.INGOT_STATUS_POLL_BUDGET_MS}ms; ` +
|
|
846
|
-
'upload
|
|
922
|
+
'the upload is received and will be confirmed on a later attempt'
|
|
923
|
+
);
|
|
924
|
+
throw new TusUploadError(
|
|
925
|
+
`Upload received; the vault has not confirmed ingot ${ingotId} published yet (status ${ingot.status ?? 'unknown'})`,
|
|
926
|
+
{ ...upload, phase: 'finalize', retryLater: true }
|
|
847
927
|
);
|
|
848
|
-
return ingot;
|
|
849
928
|
}
|
|
850
929
|
|
|
851
930
|
await new Promise(resolve => setTimeout(resolve, Math.min(delay, remaining)));
|
|
@@ -856,28 +935,6 @@ export class MobileIngotsClient {
|
|
|
856
935
|
return ingot;
|
|
857
936
|
}
|
|
858
937
|
|
|
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
938
|
private async verifyDownloadedFile(
|
|
882
939
|
fileDownloader: MobileFileDownloader,
|
|
883
940
|
preferredUri: string,
|
|
@@ -953,26 +1010,14 @@ export class MobileIngotsClient {
|
|
|
953
1010
|
});
|
|
954
1011
|
}
|
|
955
1012
|
|
|
1013
|
+
/** Refuse a download link that is not a Forge decrypt URL on a trusted host (forge-url's rule). */
|
|
956
1014
|
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
|
-
});
|
|
1015
|
+
const read = readForgeTransferUrl(downloadUrl, {
|
|
1016
|
+
operation: 'decrypt',
|
|
1017
|
+
isAllowedHost: allowsHost(this.config.allowedDownloadHostPatterns),
|
|
1018
|
+
});
|
|
1019
|
+
if (!read.ok) {
|
|
1020
|
+
throw new SparkVaultValidationError('Invalid download URL from server', { reason: read.reason });
|
|
976
1021
|
}
|
|
977
1022
|
}
|
|
978
1023
|
|