@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.
Files changed (148) hide show
  1. package/README.md +5 -5
  2. package/dist/account.d.ts +4 -3
  3. package/dist/account.js +16 -11
  4. package/dist/account.js.map +1 -1
  5. package/dist/app-sdk/email-address.d.ts +37 -0
  6. package/dist/app-sdk/email-address.js +91 -0
  7. package/dist/app-sdk/email-address.js.map +1 -0
  8. package/dist/app-sdk/encoding.d.ts +64 -0
  9. package/dist/app-sdk/encoding.js +245 -0
  10. package/dist/app-sdk/encoding.js.map +1 -0
  11. package/dist/app-sdk/entropy-contract.d.ts +33 -0
  12. package/dist/app-sdk/entropy-contract.js +51 -0
  13. package/dist/app-sdk/entropy-contract.js.map +1 -0
  14. package/dist/app-sdk/filename.d.ts +93 -0
  15. package/dist/app-sdk/filename.js +161 -0
  16. package/dist/app-sdk/filename.js.map +1 -0
  17. package/dist/app-sdk/forge-url.d.ts +107 -0
  18. package/dist/app-sdk/forge-url.js +222 -0
  19. package/dist/app-sdk/forge-url.js.map +1 -0
  20. package/dist/app-sdk/ids.d.ts +48 -0
  21. package/dist/app-sdk/ids.js +65 -0
  22. package/dist/app-sdk/ids.js.map +1 -0
  23. package/dist/app-sdk/ingot-access-log.d.ts +29 -0
  24. package/dist/app-sdk/ingot-access-log.js +49 -0
  25. package/dist/app-sdk/ingot-access-log.js.map +1 -0
  26. package/dist/app-sdk/phone.d.ts +37 -0
  27. package/dist/app-sdk/phone.js +134 -0
  28. package/dist/app-sdk/phone.js.map +1 -0
  29. package/dist/app-sdk/platform-domains.d.ts +41 -0
  30. package/dist/app-sdk/platform-domains.js +82 -0
  31. package/dist/app-sdk/platform-domains.js.map +1 -0
  32. package/dist/app-sdk/request-path.d.ts +49 -0
  33. package/dist/app-sdk/request-path.js +81 -0
  34. package/dist/app-sdk/request-path.js.map +1 -0
  35. package/dist/app-sdk/transient.d.ts +23 -0
  36. package/dist/app-sdk/transient.js +29 -0
  37. package/dist/app-sdk/transient.js.map +1 -0
  38. package/dist/app-sdk/vat-refusal.d.ts +66 -0
  39. package/dist/app-sdk/vat-refusal.js +69 -0
  40. package/dist/app-sdk/vat-refusal.js.map +1 -0
  41. package/dist/billing.d.ts +9 -17
  42. package/dist/billing.js +14 -17
  43. package/dist/billing.js.map +1 -1
  44. package/dist/config.d.ts +7 -1
  45. package/dist/config.js +10 -17
  46. package/dist/config.js.map +1 -1
  47. package/dist/encoding.d.ts +30 -4
  48. package/dist/encoding.js +89 -83
  49. package/dist/encoding.js.map +1 -1
  50. package/dist/entropy.d.ts +25 -4
  51. package/dist/entropy.js +62 -15
  52. package/dist/entropy.js.map +1 -1
  53. package/dist/entropy.test-d.d.ts +40 -0
  54. package/dist/entropy.test-d.js +12 -0
  55. package/dist/entropy.test-d.js.map +1 -0
  56. package/dist/errors.d.ts +67 -17
  57. package/dist/errors.js +70 -27
  58. package/dist/errors.js.map +1 -1
  59. package/dist/folders.js +8 -7
  60. package/dist/folders.js.map +1 -1
  61. package/dist/http.d.ts +57 -15
  62. package/dist/http.js +125 -138
  63. package/dist/http.js.map +1 -1
  64. package/dist/identity-dialog.js +4 -30
  65. package/dist/identity-dialog.js.map +1 -1
  66. package/dist/identity-input.d.ts +39 -0
  67. package/dist/identity-input.js +59 -0
  68. package/dist/identity-input.js.map +1 -0
  69. package/dist/index.d.ts +6 -5
  70. package/dist/index.js +4 -3
  71. package/dist/index.js.map +1 -1
  72. package/dist/ingots.d.ts +51 -18
  73. package/dist/ingots.js +132 -122
  74. package/dist/ingots.js.map +1 -1
  75. package/dist/mutex.d.ts +1 -2
  76. package/dist/mutex.js +0 -6
  77. package/dist/mutex.js.map +1 -1
  78. package/dist/notify.d.ts +7 -2
  79. package/dist/notify.js +12 -15
  80. package/dist/notify.js.map +1 -1
  81. package/dist/notify.test-d.d.ts +3 -2
  82. package/dist/notify.test-d.js +1 -1
  83. package/dist/products/identity.d.ts +5 -0
  84. package/dist/products/identity.js +40 -35
  85. package/dist/products/identity.js.map +1 -1
  86. package/dist/request-path.d.ts +32 -0
  87. package/dist/request-path.js +35 -0
  88. package/dist/request-path.js.map +1 -0
  89. package/dist/sparks.js +16 -15
  90. package/dist/sparks.js.map +1 -1
  91. package/dist/tus.d.ts +19 -2
  92. package/dist/tus.js +86 -35
  93. package/dist/tus.js.map +1 -1
  94. package/dist/types.d.ts +167 -15
  95. package/dist/users.d.ts +12 -5
  96. package/dist/users.js +27 -19
  97. package/dist/users.js.map +1 -1
  98. package/dist/validation.d.ts +20 -5
  99. package/dist/validation.js +29 -27
  100. package/dist/validation.js.map +1 -1
  101. package/dist/vaults.d.ts +34 -83
  102. package/dist/vaults.js +64 -44
  103. package/dist/vaults.js.map +1 -1
  104. package/dist/vaults.test-d.d.ts +22 -0
  105. package/dist/vaults.test-d.js +14 -0
  106. package/dist/vaults.test-d.js.map +1 -0
  107. package/package.json +4 -2
  108. package/src/account.ts +16 -11
  109. package/src/app-sdk/email-address.js +94 -0
  110. package/src/app-sdk/encoding.js +250 -0
  111. package/src/app-sdk/entropy-contract.js +56 -0
  112. package/src/app-sdk/filename.js +174 -0
  113. package/src/app-sdk/forge-url.js +224 -0
  114. package/src/app-sdk/ids.js +71 -0
  115. package/src/app-sdk/ingot-access-log.js +51 -0
  116. package/src/app-sdk/phone.js +139 -0
  117. package/src/app-sdk/platform-domains.js +99 -0
  118. package/src/app-sdk/request-path.js +81 -0
  119. package/src/app-sdk/transient.js +30 -0
  120. package/src/app-sdk/vat-refusal.js +74 -0
  121. package/src/billing.ts +16 -25
  122. package/src/config.ts +16 -23
  123. package/src/encoding.ts +91 -103
  124. package/src/entropy.test-d.ts +47 -0
  125. package/src/entropy.ts +86 -20
  126. package/src/errors.ts +93 -33
  127. package/src/folders.ts +8 -7
  128. package/src/http.ts +145 -150
  129. package/src/identity-dialog.tsx +4 -37
  130. package/src/identity-input.ts +70 -0
  131. package/src/index.ts +11 -3
  132. package/src/ingots.ts +173 -159
  133. package/src/mutex.ts +1 -8
  134. package/src/notify.test-d.ts +3 -1
  135. package/src/notify.ts +19 -17
  136. package/src/products/identity.ts +45 -39
  137. package/src/request-path.ts +45 -0
  138. package/src/sparks.ts +16 -13
  139. package/src/tus.ts +90 -37
  140. package/src/types.ts +165 -15
  141. package/src/users.ts +27 -17
  142. package/src/validation.ts +30 -24
  143. package/src/vaults.test-d.ts +29 -0
  144. package/src/vaults.ts +87 -153
  145. package/dist/platform-domains.generated.d.ts +0 -2
  146. package/dist/platform-domains.generated.js +0 -16
  147. package/dist/platform-domains.generated.js.map +0 -1
  148. package/src/platform-domains.generated.ts +0 -17
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: flex licensing has no seat limit, and
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 that is still being
119
- * processed server-side (status `uploading`/`encrypting`). Transient by
120
- * nature, but retrying immediately will not help — callers should back off
121
- * until the ingot becomes active.
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,6 +142,41 @@ 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;
@@ -259,8 +296,7 @@ export function isNetworkError(err: unknown): boolean {
259
296
  * per-upload token minted with the ingot (expired, consumed, or bound to a
260
297
  * different session) — not the vault session. Re-unsealing the vault cannot
261
298
  * fix it; the caller creates a new upload session (`POST /ingots`) instead.
262
- * Use `isVatExpiredError` / `isReadOnlyVaultError` to tell the API's VAT
263
- * rejections apart.
299
+ * Use `vatRefusalReason` to tell the API's VAT refusals apart.
264
300
  */
265
301
  export function isAuthError(err: unknown): boolean {
266
302
  return (
@@ -271,31 +307,51 @@ export function isAuthError(err: unknown): boolean {
271
307
  }
272
308
 
273
309
  /**
274
- * True when the API rejected the vault access token because it has expired
275
- * (`details.reason === 'VAT_EXPIRED'` on a 401 or 403 — an expired VAT
276
- * answers 403 for its first hour, then 401). Re-unsealing the vault mints a
277
- * fresh VAT and the same request can be retried as-is.
310
+ * The reason the API refused the vault access token, or null when the error
311
+ * is not a VAT refusal:
312
+ * - `VAT_INVALID` (401): no, a malformed, unknown or swept token, or one a
313
+ * since-revoked DVAK minted — unseal again. Never the login.
314
+ * - `VAT_EXPIRED` (403): the token outlived its TTL — unseal again and
315
+ * retry the same request as-is.
316
+ * - `VAT_READ_ONLY` (403): a View Only token on a write — no unseal fixes it.
317
+ * A 403 with no reason is a token for another vault or account; a 401 with no
318
+ * reason is the login itself.
319
+ */
320
+ export function vatRefusalReason(err: unknown): string | null {
321
+ if (!(err instanceof SparkVaultMobileError) || err instanceof TusUploadError) {
322
+ return null;
323
+ }
324
+ return readVatRefusalReason(err);
325
+ }
326
+
327
+ /**
328
+ * True when the API refused the vault access token because it has expired
329
+ * (403 `VAT_EXPIRED`). Re-unsealing the vault mints a fresh VAT and the same
330
+ * request can be retried as-is.
278
331
  */
279
332
  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
- );
333
+ return vatRefusalReason(err) === VAT_REFUSAL_REASONS.EXPIRED;
285
334
  }
286
335
 
287
336
  /**
288
- * True when the vault access token is valid but read-only
289
- * (`details.reason === 'VAT_READ_ONLY'` on a 403): the session was unsealed
290
- * with a read-only access level, so a write cannot succeed by retrying or by
291
- * re-unsealing the same way — the caller needs write access to the vault.
337
+ * True when the vault access token is valid but read-only (403
338
+ * `VAT_READ_ONLY`): the session was unsealed with a read-only access level, so
339
+ * a write cannot succeed by retrying or by re-unsealing the same way — the
340
+ * caller needs write access to the vault.
292
341
  */
293
342
  export function isReadOnlyVaultError(err: unknown): err is SparkVaultMobileError {
294
- return (
295
- err instanceof SparkVaultMobileError &&
296
- err.statusCode === 403 &&
297
- err.details?.reason === 'VAT_READ_ONLY'
298
- );
343
+ return vatRefusalReason(err) === VAT_REFUSAL_REASONS.READ_ONLY;
344
+ }
345
+
346
+ /**
347
+ * True for the API's 404 not-found answer: the resource it names does not
348
+ * exist (or is not this caller's to see). The only answer that says a vault,
349
+ * an ingot or a key's vault is missing; a rate limit, an outage or a refused
350
+ * session says nothing about it. Never true for a `TusUploadError`: Forge's
351
+ * 404 is an upload session that is gone, not a missing resource.
352
+ */
353
+ export function isNotFoundError(err: unknown): err is SparkVaultMobileError {
354
+ return err instanceof SparkVaultMobileError && !(err instanceof TusUploadError) && err.statusCode === 404;
299
355
  }
300
356
 
301
357
  /** True for a 429 rate-limit response — retry after a cooldown, never immediately. */
@@ -311,18 +367,22 @@ export function isIngotNotReadyError(err: unknown): err is IngotNotReadyError {
311
367
  return err instanceof IngotNotReadyError;
312
368
  }
313
369
 
370
+ /** Type guard for a create refused over the same-name ingot holding its name. */
371
+ export function isIngotNameHeldError(err: unknown): err is IngotNameHeldError {
372
+ return err instanceof IngotNameHeldError;
373
+ }
374
+
314
375
  /**
315
- * True when retrying a failed download can succeed. Validation failures and
316
- * deterministic 4xx responses always fail the same way again; everything else
317
- * (connectivity, 5xx, 408, 429) is worth another attempt.
376
+ * True when retrying a failed download can succeed. A validation failure always
377
+ * fails the same way again, and an HTTP refusal is retried only when the
378
+ * platform's retry rule (`isTransientHttpStatus`, mirrored from
379
+ * `@sparkvault/app-sdk/transient`) calls it transient; every other failure
380
+ * (connectivity, an answer that could not be read) is worth another attempt.
318
381
  */
319
382
  export function isRetryableDownloadError(err: unknown): boolean {
320
383
  if (err instanceof SparkVaultValidationError) return false;
321
384
  const status = err instanceof SparkVaultMobileError ? err.statusCode : undefined;
322
- if (typeof status === 'number' && status >= 400 && status < 500 && status !== 408 && status !== 429) {
323
- return false;
324
- }
325
- return true;
385
+ return typeof status !== 'number' || status < 400 || isTransientHttpStatus(status);
326
386
  }
327
387
 
328
388
  /**
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
  }