@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/entropy.ts
CHANGED
|
@@ -1,18 +1,34 @@
|
|
|
1
|
-
import { base64ToBytes } from './encoding.js';
|
|
1
|
+
import { base64ToBytes } from './app-sdk/encoding.js';
|
|
2
|
+
import { ENTROPY_FORMATS, MAX_DRAW_BYTES, isEntropyFormat, maxBytesFor } from './app-sdk/entropy-contract.js';
|
|
2
3
|
import { SparkVaultMobileError, SparkVaultValidationError } from './errors.js';
|
|
3
4
|
import type { MobileHttpClient } from './http.js';
|
|
5
|
+
import { apiPath } from './request-path.js';
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
/** An output format the entropy endpoint serves (the platform's entropy contract). */
|
|
8
|
+
export type EntropyFormat =
|
|
9
|
+
| 'hex'
|
|
10
|
+
| 'base64'
|
|
11
|
+
| 'base64url'
|
|
12
|
+
| 'alphanumeric'
|
|
13
|
+
| 'alphanumeric-mixed'
|
|
14
|
+
| 'password'
|
|
15
|
+
| 'numeric'
|
|
16
|
+
| 'uuid'
|
|
17
|
+
| 'bytes';
|
|
6
18
|
|
|
7
19
|
export interface GenerateEntropyOptions {
|
|
20
|
+
/** Number of random bytes: 1–1024, or 1–512 for alphanumeric, alphanumeric-mixed and password. */
|
|
8
21
|
bytes: number;
|
|
22
|
+
/** Output format. Defaults to base64. */
|
|
9
23
|
format?: EntropyFormat;
|
|
10
24
|
}
|
|
11
25
|
|
|
12
|
-
|
|
13
|
-
|
|
26
|
+
/** The endpoint's answer: `value` is a number array for the `bytes` format and a string for every other. */
|
|
27
|
+
export interface EntropyResponse<Value extends string | number[] = string | number[]> {
|
|
28
|
+
value: Value;
|
|
14
29
|
num_bytes: number;
|
|
15
30
|
format?: EntropyFormat;
|
|
31
|
+
reference_id?: string;
|
|
16
32
|
}
|
|
17
33
|
|
|
18
34
|
export class MobileEntropyClient {
|
|
@@ -22,35 +38,85 @@ export class MobileEntropyClient {
|
|
|
22
38
|
this.http = http;
|
|
23
39
|
}
|
|
24
40
|
|
|
41
|
+
generate(options: GenerateEntropyOptions & { format: 'bytes' }): Promise<EntropyResponse<number[]>>;
|
|
42
|
+
generate(options: GenerateEntropyOptions & { format?: Exclude<EntropyFormat, 'bytes'> }): Promise<EntropyResponse<string>>;
|
|
43
|
+
generate(options: GenerateEntropyOptions): Promise<EntropyResponse>;
|
|
25
44
|
async generate(options: GenerateEntropyOptions): Promise<EntropyResponse> {
|
|
26
|
-
|
|
27
|
-
|
|
45
|
+
const format = options.format ?? 'base64';
|
|
46
|
+
validateEntropyRequest(options.bytes, format);
|
|
47
|
+
const response = await this.http.post<EntropyResponse>(apiPath`/entropy/generate`, {
|
|
28
48
|
num_bytes: options.bytes,
|
|
29
|
-
format
|
|
49
|
+
format,
|
|
30
50
|
});
|
|
31
51
|
return response.data;
|
|
32
52
|
}
|
|
33
53
|
|
|
34
54
|
async generateBytes(bytes: number): Promise<Uint8Array> {
|
|
35
55
|
const response = await this.generate({ bytes, format: 'base64' });
|
|
36
|
-
|
|
37
|
-
throw new SparkVaultMobileError('Entropy endpoint returned no value');
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const entropy = base64ToBytes(response.value);
|
|
41
|
-
if (entropy.length !== bytes) {
|
|
42
|
-
throw new SparkVaultMobileError(`Entropy length mismatch: expected ${bytes}, got ${entropy.length}`);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
return entropy;
|
|
56
|
+
return drawnBytes(response.value, bytes);
|
|
46
57
|
}
|
|
47
58
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
59
|
+
/**
|
|
60
|
+
* Random bytes for the signed-in client's own working values (a key's id, a
|
|
61
|
+
* secret's server share), from POST /v1/entropy/first-party: answered to an
|
|
62
|
+
* interactive SparkVault session only, never plan- or bandwidth-gated and
|
|
63
|
+
* never recorded as the account's usage. Values an integration asks for come
|
|
64
|
+
* from `generate` / `generateBytes`.
|
|
65
|
+
*
|
|
66
|
+
* @param bytes - 1 to the entropy contract's ceiling (MAX_DRAW_BYTES)
|
|
67
|
+
* @returns exactly `bytes` bytes
|
|
68
|
+
*/
|
|
69
|
+
async drawFirstParty(bytes: number): Promise<Uint8Array> {
|
|
70
|
+
if (!Number.isInteger(bytes) || bytes < 1 || bytes > MAX_DRAW_BYTES) {
|
|
71
|
+
throw new SparkVaultValidationError(`Entropy byte count must be an integer from 1 to ${MAX_DRAW_BYTES}`, {
|
|
51
72
|
field: 'bytes',
|
|
52
73
|
received: bytes,
|
|
53
74
|
});
|
|
54
75
|
}
|
|
76
|
+
const response = await this.http.post<{ value: unknown; num_bytes: number }>(apiPath`/entropy/first-party`, {
|
|
77
|
+
num_bytes: bytes,
|
|
78
|
+
});
|
|
79
|
+
return drawnBytes(response.data.value, bytes);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The bytes a draw answered as base64, refused unless they are exactly the
|
|
85
|
+
* bytes asked for, so a malformed answer fails the operation that asked for it.
|
|
86
|
+
*/
|
|
87
|
+
function drawnBytes(value: unknown, bytes: number): Uint8Array {
|
|
88
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
89
|
+
throw new SparkVaultMobileError('Entropy endpoint returned no value');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const entropy = base64ToBytes(value);
|
|
93
|
+
if (entropy === null) {
|
|
94
|
+
throw new SparkVaultMobileError('Entropy endpoint returned a value that is not base64');
|
|
95
|
+
}
|
|
96
|
+
if (entropy.length !== bytes) {
|
|
97
|
+
throw new SparkVaultMobileError(`Entropy length mismatch: expected ${bytes}, got ${entropy.length}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return entropy;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Refuse a request the endpoint would refuse, before it is sent: the format must
|
|
105
|
+
* be one the endpoint serves, and the byte count an integer from 1 to that
|
|
106
|
+
* format's ceiling.
|
|
107
|
+
*/
|
|
108
|
+
function validateEntropyRequest(bytes: number, format: EntropyFormat): void {
|
|
109
|
+
if (!isEntropyFormat(format)) {
|
|
110
|
+
throw new SparkVaultValidationError(`Entropy format must be one of: ${ENTROPY_FORMATS.join(', ')}`, {
|
|
111
|
+
field: 'format',
|
|
112
|
+
received: format,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
const max = maxBytesFor(format);
|
|
116
|
+
if (!Number.isInteger(bytes) || bytes < 1 || bytes > max) {
|
|
117
|
+
throw new SparkVaultValidationError(`Entropy byte count must be an integer from 1 to ${max} for the ${format} format`, {
|
|
118
|
+
field: 'bytes',
|
|
119
|
+
received: bytes,
|
|
120
|
+
});
|
|
55
121
|
}
|
|
56
122
|
}
|
package/src/errors.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { ApiErrorBody, ApiMeta, BackgroundTransferFailureKind } from './types.js';
|
|
2
|
+
import { isTransientHttpStatus } from './app-sdk/transient.js';
|
|
3
|
+
import { VAT_REFUSAL_REASONS, vatRefusalReason as readVatRefusalReason } from './app-sdk/vat-refusal.js';
|
|
2
4
|
|
|
3
|
-
// Only the byte pools hard-gate:
|
|
4
|
-
// Identity soft-gates through its own path.
|
|
5
|
+
// Only the byte pools hard-gate: licenses are assigned per member, never
|
|
6
|
+
// drawn from a pool, and Identity soft-gates through its own path.
|
|
5
7
|
type QuotaResource = 'storage' | 'bandwidth';
|
|
6
8
|
|
|
7
9
|
function isQuotaResource(value: unknown): value is QuotaResource {
|
|
@@ -115,10 +117,10 @@ export class SparkVaultTimeoutError extends SparkVaultMobileError {
|
|
|
115
117
|
}
|
|
116
118
|
|
|
117
119
|
/**
|
|
118
|
-
* Thrown when a download is requested against an ingot
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
120
|
+
* Thrown when a download is requested against an ingot whose upload Forge has
|
|
121
|
+
* not activated yet (status `uploading`). Transient by nature, but retrying
|
|
122
|
+
* immediately will not help — callers should back off until the ingot becomes
|
|
123
|
+
* active.
|
|
122
124
|
*/
|
|
123
125
|
export class IngotNotReadyError extends SparkVaultMobileError {
|
|
124
126
|
/** Server-reported ingot status at the time of the request, when known. */
|
|
@@ -140,32 +142,101 @@ export class IngotNotReadyError extends SparkVaultMobileError {
|
|
|
140
142
|
}
|
|
141
143
|
}
|
|
142
144
|
|
|
145
|
+
/** The ingot a create was refused over, as the server read it. */
|
|
146
|
+
export interface IngotNameHolder {
|
|
147
|
+
ingotId: string;
|
|
148
|
+
/**
|
|
149
|
+
* Lifecycle status of the holder: `active` when its upload finished,
|
|
150
|
+
* `uploading` when it has not (only a `refuse` create is refused over an
|
|
151
|
+
* unfinished holder; `complete_pending` completes one).
|
|
152
|
+
*/
|
|
153
|
+
status: string;
|
|
154
|
+
sizeBytes: number;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Thrown when a create is refused because an ingot already holds the name in
|
|
159
|
+
* the target folder (409 `details.reason === 'NAME_HELD'`): with
|
|
160
|
+
* `onNameConflict: 'complete_pending'` a published holder, with `'refuse'` any
|
|
161
|
+
* holder. The server decided before writing anything, so the holder is
|
|
162
|
+
* untouched; `holder` is the row it read, which lets the caller act on it
|
|
163
|
+
* (send its bytes into its own row by id, adopt it, or try another name)
|
|
164
|
+
* without a second read.
|
|
165
|
+
*/
|
|
166
|
+
export class IngotNameHeldError extends SparkVaultMobileError {
|
|
167
|
+
readonly holder: IngotNameHolder;
|
|
168
|
+
|
|
169
|
+
constructor(
|
|
170
|
+
message: string,
|
|
171
|
+
options: { holder: IngotNameHolder; details?: Record<string, unknown>; meta?: ApiMeta }
|
|
172
|
+
) {
|
|
173
|
+
super(message, { code: 'CONFLICT', statusCode: 409, details: options.details, meta: options.meta });
|
|
174
|
+
this.name = 'IngotNameHeldError';
|
|
175
|
+
this.holder = options.holder;
|
|
176
|
+
Object.setPrototypeOf(this, IngotNameHeldError.prototype);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
143
180
|
export class TusUploadError extends SparkVaultMobileError {
|
|
144
181
|
readonly cause: Error | null;
|
|
145
182
|
readonly filename: string | null;
|
|
146
183
|
readonly fileSize: number | null;
|
|
147
|
-
|
|
184
|
+
/**
|
|
185
|
+
* Where the upload stopped: `finalize` is after the transfer, when every
|
|
186
|
+
* byte was on Forge but the ingot was not confirmed published.
|
|
187
|
+
*/
|
|
188
|
+
readonly phase: 'create' | 'upload' | 'finalize' | 'network' | 'timeout' | 'cancelled' | 'stalled' | 'unknown';
|
|
148
189
|
/**
|
|
149
190
|
* True when Forge no longer has the session (it answered a status
|
|
150
|
-
* `isSessionGone` recognises
|
|
151
|
-
* cannot carry this file. The persisted session is worthless, so
|
|
152
|
-
* caller starts a fresh upload instead of retrying it.
|
|
191
|
+
* `isSessionGone` recognises, or the expiry it named has passed), or the
|
|
192
|
+
* session cannot carry this file. The persisted session is worthless, so
|
|
193
|
+
* the caller starts a fresh upload instead of retrying it.
|
|
153
194
|
*/
|
|
154
195
|
readonly sessionLost: boolean;
|
|
196
|
+
/**
|
|
197
|
+
* The `Retry-After` of the answer that ended the upload, in ms: the
|
|
198
|
+
* earliest a next attempt can expect another answer. Null when that answer
|
|
199
|
+
* named none, or no answer arrived.
|
|
200
|
+
*/
|
|
201
|
+
readonly retryAfterMs: number | null;
|
|
202
|
+
/**
|
|
203
|
+
* True when every byte is on Forge and the session is intact but the ingot
|
|
204
|
+
* was not confirmed published: the vault did not read it `active` within
|
|
205
|
+
* the activation poll budget, or Forge was still saving the file at the
|
|
206
|
+
* upload's idle limit (code `upload_received_publishing`; it finishes
|
|
207
|
+
* without this device). The caller retries later by resuming the session
|
|
208
|
+
* it persisted, which asks Forge for the receipt again and sends no bytes.
|
|
209
|
+
*/
|
|
210
|
+
readonly retryLater: boolean;
|
|
211
|
+
/**
|
|
212
|
+
* The code Core ended this upload with, as Forge named it. With code
|
|
213
|
+
* `upload_refused` a final refusal (`UPLOAD_POOL_FULL`: the account's
|
|
214
|
+
* storage pool cannot take it; `UPLOAD_SUPERSEDED`: a later upload replaced
|
|
215
|
+
* it), whose message is Core's own sentence and which no new upload of the
|
|
216
|
+
* same file remedies; with `sessionLost` any other code (`NOT_FOUND`: the
|
|
217
|
+
* file the upload was for is gone; `UPLOAD_PROTOCOL_RETIRED`; a 412's),
|
|
218
|
+
* which a fresh upload starts over from. Null when the answer named none.
|
|
219
|
+
*/
|
|
220
|
+
readonly coreCode: string | null;
|
|
155
221
|
|
|
156
222
|
constructor(
|
|
157
223
|
message: string,
|
|
158
224
|
options: {
|
|
159
225
|
cause?: Error | null;
|
|
160
226
|
httpStatus?: number | null;
|
|
227
|
+
/** The error's code when it names an outcome of its own; otherwise the HTTP status, or `tus_upload_error`. */
|
|
228
|
+
code?: string;
|
|
161
229
|
filename?: string | null;
|
|
162
230
|
fileSize?: number | null;
|
|
163
231
|
phase?: TusUploadError['phase'];
|
|
164
232
|
sessionLost?: boolean;
|
|
233
|
+
retryAfterMs?: number | null;
|
|
234
|
+
retryLater?: boolean;
|
|
235
|
+
coreCode?: string | null;
|
|
165
236
|
} = {}
|
|
166
237
|
) {
|
|
167
238
|
super(message, {
|
|
168
|
-
code: options.httpStatus ?? 'tus_upload_error',
|
|
239
|
+
code: options.code ?? options.httpStatus ?? 'tus_upload_error',
|
|
169
240
|
statusCode: options.httpStatus ?? undefined,
|
|
170
241
|
});
|
|
171
242
|
this.name = 'TusUploadError';
|
|
@@ -174,6 +245,9 @@ export class TusUploadError extends SparkVaultMobileError {
|
|
|
174
245
|
this.fileSize = options.fileSize ?? null;
|
|
175
246
|
this.phase = options.phase ?? 'unknown';
|
|
176
247
|
this.sessionLost = options.sessionLost ?? false;
|
|
248
|
+
this.retryAfterMs = options.retryAfterMs ?? null;
|
|
249
|
+
this.retryLater = options.retryLater ?? false;
|
|
250
|
+
this.coreCode = options.coreCode ?? null;
|
|
177
251
|
Object.setPrototypeOf(this, TusUploadError.prototype);
|
|
178
252
|
}
|
|
179
253
|
|
|
@@ -259,8 +333,7 @@ export function isNetworkError(err: unknown): boolean {
|
|
|
259
333
|
* per-upload token minted with the ingot (expired, consumed, or bound to a
|
|
260
334
|
* different session) — not the vault session. Re-unsealing the vault cannot
|
|
261
335
|
* fix it; the caller creates a new upload session (`POST /ingots`) instead.
|
|
262
|
-
* Use `
|
|
263
|
-
* rejections apart.
|
|
336
|
+
* Use `vatRefusalReason` to tell the API's VAT refusals apart.
|
|
264
337
|
*/
|
|
265
338
|
export function isAuthError(err: unknown): boolean {
|
|
266
339
|
return (
|
|
@@ -271,31 +344,51 @@ export function isAuthError(err: unknown): boolean {
|
|
|
271
344
|
}
|
|
272
345
|
|
|
273
346
|
/**
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
277
|
-
*
|
|
347
|
+
* The reason the API refused the vault access token, or null when the error
|
|
348
|
+
* is not a VAT refusal:
|
|
349
|
+
* - `VAT_INVALID` (401): no, a malformed, unknown or swept token, or one a
|
|
350
|
+
* since-revoked DVAK minted — unseal again. Never the login.
|
|
351
|
+
* - `VAT_EXPIRED` (403): the token outlived its TTL — unseal again and
|
|
352
|
+
* retry the same request as-is.
|
|
353
|
+
* - `VAT_READ_ONLY` (403): a View Only token on a write — no unseal fixes it.
|
|
354
|
+
* A 403 with no reason is a token for another vault or account; a 401 with no
|
|
355
|
+
* reason is the login itself.
|
|
356
|
+
*/
|
|
357
|
+
export function vatRefusalReason(err: unknown): string | null {
|
|
358
|
+
if (!(err instanceof SparkVaultMobileError) || err instanceof TusUploadError) {
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
return readVatRefusalReason(err);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* True when the API refused the vault access token because it has expired
|
|
366
|
+
* (403 `VAT_EXPIRED`). Re-unsealing the vault mints a fresh VAT and the same
|
|
367
|
+
* request can be retried as-is.
|
|
278
368
|
*/
|
|
279
369
|
export function isVatExpiredError(err: unknown): err is SparkVaultMobileError {
|
|
280
|
-
return (
|
|
281
|
-
err instanceof SparkVaultMobileError &&
|
|
282
|
-
(err.statusCode === 401 || err.statusCode === 403) &&
|
|
283
|
-
err.details?.reason === 'VAT_EXPIRED'
|
|
284
|
-
);
|
|
370
|
+
return vatRefusalReason(err) === VAT_REFUSAL_REASONS.EXPIRED;
|
|
285
371
|
}
|
|
286
372
|
|
|
287
373
|
/**
|
|
288
|
-
* True when the vault access token is valid but read-only
|
|
289
|
-
*
|
|
290
|
-
*
|
|
291
|
-
*
|
|
374
|
+
* True when the vault access token is valid but read-only (403
|
|
375
|
+
* `VAT_READ_ONLY`): the session was unsealed with a read-only access level, so
|
|
376
|
+
* a write cannot succeed by retrying or by re-unsealing the same way — the
|
|
377
|
+
* caller needs write access to the vault.
|
|
292
378
|
*/
|
|
293
379
|
export function isReadOnlyVaultError(err: unknown): err is SparkVaultMobileError {
|
|
294
|
-
return (
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
380
|
+
return vatRefusalReason(err) === VAT_REFUSAL_REASONS.READ_ONLY;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* True for the API's 404 not-found answer: the resource it names does not
|
|
385
|
+
* exist (or is not this caller's to see). The only answer that says a vault,
|
|
386
|
+
* an ingot or a key's vault is missing; a rate limit, an outage or a refused
|
|
387
|
+
* session says nothing about it. Never true for a `TusUploadError`: Forge's
|
|
388
|
+
* 404 is an upload session that is gone, not a missing resource.
|
|
389
|
+
*/
|
|
390
|
+
export function isNotFoundError(err: unknown): err is SparkVaultMobileError {
|
|
391
|
+
return err instanceof SparkVaultMobileError && !(err instanceof TusUploadError) && err.statusCode === 404;
|
|
299
392
|
}
|
|
300
393
|
|
|
301
394
|
/** True for a 429 rate-limit response — retry after a cooldown, never immediately. */
|
|
@@ -311,18 +404,22 @@ export function isIngotNotReadyError(err: unknown): err is IngotNotReadyError {
|
|
|
311
404
|
return err instanceof IngotNotReadyError;
|
|
312
405
|
}
|
|
313
406
|
|
|
407
|
+
/** Type guard for a create refused over the same-name ingot holding its name. */
|
|
408
|
+
export function isIngotNameHeldError(err: unknown): err is IngotNameHeldError {
|
|
409
|
+
return err instanceof IngotNameHeldError;
|
|
410
|
+
}
|
|
411
|
+
|
|
314
412
|
/**
|
|
315
|
-
* True when retrying a failed download can succeed.
|
|
316
|
-
*
|
|
317
|
-
*
|
|
413
|
+
* True when retrying a failed download can succeed. A validation failure always
|
|
414
|
+
* fails the same way again, and an HTTP refusal is retried only when the
|
|
415
|
+
* platform's retry rule (`isTransientHttpStatus`, mirrored from
|
|
416
|
+
* `@sparkvault/app-sdk/transient`) calls it transient; every other failure
|
|
417
|
+
* (connectivity, an answer that could not be read) is worth another attempt.
|
|
318
418
|
*/
|
|
319
419
|
export function isRetryableDownloadError(err: unknown): boolean {
|
|
320
420
|
if (err instanceof SparkVaultValidationError) return false;
|
|
321
421
|
const status = err instanceof SparkVaultMobileError ? err.statusCode : undefined;
|
|
322
|
-
|
|
323
|
-
return false;
|
|
324
|
-
}
|
|
325
|
-
return true;
|
|
422
|
+
return typeof status !== 'number' || status < 400 || isTransientHttpStatus(status);
|
|
326
423
|
}
|
|
327
424
|
|
|
328
425
|
/**
|
package/src/folders.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
UpdateFolderRequest,
|
|
8
8
|
} from './types.js';
|
|
9
9
|
import { validateFolderId, validateIngotId, validateVaultId } from './validation.js';
|
|
10
|
+
import { apiPath } from './request-path.js';
|
|
10
11
|
|
|
11
12
|
export class MobileFoldersClient {
|
|
12
13
|
private readonly http: MobileHttpClient;
|
|
@@ -17,14 +18,14 @@ export class MobileFoldersClient {
|
|
|
17
18
|
|
|
18
19
|
async list(vaultId: string, vat: string): Promise<ListFoldersResponse> {
|
|
19
20
|
validateVaultId(vaultId);
|
|
20
|
-
const response = await this.http.get<ListFoldersResponse>(`/vaults/${vaultId}/folders`, { vat });
|
|
21
|
+
const response = await this.http.get<ListFoldersResponse>(apiPath`/vaults/${vaultId}/folders`, { vat });
|
|
21
22
|
return response.data;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
async get(vaultId: string, folderId: string, vat: string): Promise<Folder> {
|
|
25
26
|
validateVaultId(vaultId);
|
|
26
27
|
validateFolderId(folderId);
|
|
27
|
-
const response = await this.http.get<Folder>(`/vaults/${vaultId}/folders/${folderId}`, { vat });
|
|
28
|
+
const response = await this.http.get<Folder>(apiPath`/vaults/${vaultId}/folders/${folderId}`, { vat });
|
|
28
29
|
return response.data;
|
|
29
30
|
}
|
|
30
31
|
|
|
@@ -32,7 +33,7 @@ export class MobileFoldersClient {
|
|
|
32
33
|
validateVaultId(vaultId);
|
|
33
34
|
validateFolderId(folderId);
|
|
34
35
|
const response = await this.http.get<Folder & { breadcrumb?: FolderBreadcrumbItem[] }>(
|
|
35
|
-
`/vaults/${vaultId}/folders/${folderId}`,
|
|
36
|
+
apiPath`/vaults/${vaultId}/folders/${folderId}`,
|
|
36
37
|
{ vat }
|
|
37
38
|
);
|
|
38
39
|
return response.data.breadcrumb ?? [];
|
|
@@ -40,14 +41,14 @@ export class MobileFoldersClient {
|
|
|
40
41
|
|
|
41
42
|
async create(vaultId: string, vat: string, data: CreateFolderRequest): Promise<Folder> {
|
|
42
43
|
validateVaultId(vaultId);
|
|
43
|
-
const response = await this.http.post<Folder>(`/vaults/${vaultId}/folders`, data, { vat });
|
|
44
|
+
const response = await this.http.post<Folder>(apiPath`/vaults/${vaultId}/folders`, data, { vat });
|
|
44
45
|
return response.data;
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
async update(vaultId: string, folderId: string, vat: string, data: UpdateFolderRequest): Promise<Folder> {
|
|
48
49
|
validateVaultId(vaultId);
|
|
49
50
|
validateFolderId(folderId);
|
|
50
|
-
const response = await this.http.put<Folder>(`/vaults/${vaultId}/folders/${folderId}`, data, { vat });
|
|
51
|
+
const response = await this.http.put<Folder>(apiPath`/vaults/${vaultId}/folders/${folderId}`, data, { vat });
|
|
51
52
|
return response.data;
|
|
52
53
|
}
|
|
53
54
|
|
|
@@ -62,7 +63,7 @@ export class MobileFoldersClient {
|
|
|
62
63
|
async delete(vaultId: string, folderId: string, vat: string): Promise<void> {
|
|
63
64
|
validateVaultId(vaultId);
|
|
64
65
|
validateFolderId(folderId);
|
|
65
|
-
await this.http.delete(`/vaults/${vaultId}/folders/${folderId}`, { vat });
|
|
66
|
+
await this.http.delete(apiPath`/vaults/${vaultId}/folders/${folderId}`, { vat });
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
async moveIngot(vaultId: string, ingotId: string, vat: string, folderId: string | null): Promise<void> {
|
|
@@ -71,6 +72,6 @@ export class MobileFoldersClient {
|
|
|
71
72
|
if (folderId !== null) {
|
|
72
73
|
validateFolderId(folderId);
|
|
73
74
|
}
|
|
74
|
-
await this.http.patch(`/vaults/${vaultId}/ingots/${ingotId}`, { folder_id: folderId }, { vat });
|
|
75
|
+
await this.http.patch(apiPath`/vaults/${vaultId}/ingots/${ingotId}`, { folder_id: folderId }, { vat });
|
|
75
76
|
}
|
|
76
77
|
}
|