@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.
Files changed (156) 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/transfer-retry.d.ts +298 -0
  36. package/dist/app-sdk/transfer-retry.js +535 -0
  37. package/dist/app-sdk/transfer-retry.js.map +1 -0
  38. package/dist/app-sdk/transient.d.ts +23 -0
  39. package/dist/app-sdk/transient.js +29 -0
  40. package/dist/app-sdk/transient.js.map +1 -0
  41. package/dist/app-sdk/upload-publication.d.ts +81 -0
  42. package/dist/app-sdk/upload-publication.js +93 -0
  43. package/dist/app-sdk/upload-publication.js.map +1 -0
  44. package/dist/app-sdk/vat-refusal.d.ts +66 -0
  45. package/dist/app-sdk/vat-refusal.js +69 -0
  46. package/dist/app-sdk/vat-refusal.js.map +1 -0
  47. package/dist/billing.d.ts +9 -17
  48. package/dist/billing.js +14 -17
  49. package/dist/billing.js.map +1 -1
  50. package/dist/config.d.ts +33 -1
  51. package/dist/config.js +23 -17
  52. package/dist/config.js.map +1 -1
  53. package/dist/encoding.d.ts +30 -4
  54. package/dist/encoding.js +89 -83
  55. package/dist/encoding.js.map +1 -1
  56. package/dist/entropy.d.ts +25 -4
  57. package/dist/entropy.js +62 -15
  58. package/dist/entropy.js.map +1 -1
  59. package/dist/entropy.test-d.d.ts +40 -0
  60. package/dist/entropy.test-d.js +12 -0
  61. package/dist/entropy.test-d.js.map +1 -0
  62. package/dist/errors.d.ts +105 -21
  63. package/dist/errors.js +74 -28
  64. package/dist/errors.js.map +1 -1
  65. package/dist/folders.js +8 -7
  66. package/dist/folders.js.map +1 -1
  67. package/dist/http.d.ts +57 -15
  68. package/dist/http.js +125 -138
  69. package/dist/http.js.map +1 -1
  70. package/dist/identity-dialog.js +4 -30
  71. package/dist/identity-dialog.js.map +1 -1
  72. package/dist/identity-input.d.ts +39 -0
  73. package/dist/identity-input.js +59 -0
  74. package/dist/identity-input.js.map +1 -0
  75. package/dist/index.d.ts +8 -7
  76. package/dist/index.js +5 -4
  77. package/dist/index.js.map +1 -1
  78. package/dist/ingots.d.ts +77 -32
  79. package/dist/ingots.js +152 -136
  80. package/dist/ingots.js.map +1 -1
  81. package/dist/mutex.d.ts +1 -2
  82. package/dist/mutex.js +0 -6
  83. package/dist/mutex.js.map +1 -1
  84. package/dist/notify.d.ts +7 -2
  85. package/dist/notify.js +12 -15
  86. package/dist/notify.js.map +1 -1
  87. package/dist/notify.test-d.d.ts +3 -2
  88. package/dist/notify.test-d.js +1 -1
  89. package/dist/products/identity.d.ts +5 -0
  90. package/dist/products/identity.js +40 -35
  91. package/dist/products/identity.js.map +1 -1
  92. package/dist/request-path.d.ts +32 -0
  93. package/dist/request-path.js +35 -0
  94. package/dist/request-path.js.map +1 -0
  95. package/dist/sparks.js +16 -15
  96. package/dist/sparks.js.map +1 -1
  97. package/dist/tus.d.ts +198 -16
  98. package/dist/tus.js +816 -327
  99. package/dist/tus.js.map +1 -1
  100. package/dist/types.d.ts +183 -23
  101. package/dist/users.d.ts +12 -5
  102. package/dist/users.js +27 -19
  103. package/dist/users.js.map +1 -1
  104. package/dist/validation.d.ts +20 -5
  105. package/dist/validation.js +29 -27
  106. package/dist/validation.js.map +1 -1
  107. package/dist/vaults.d.ts +34 -83
  108. package/dist/vaults.js +64 -44
  109. package/dist/vaults.js.map +1 -1
  110. package/dist/vaults.test-d.d.ts +22 -0
  111. package/dist/vaults.test-d.js +14 -0
  112. package/dist/vaults.test-d.js.map +1 -0
  113. package/package.json +4 -2
  114. package/src/account.ts +16 -11
  115. package/src/app-sdk/email-address.js +94 -0
  116. package/src/app-sdk/encoding.js +250 -0
  117. package/src/app-sdk/entropy-contract.js +56 -0
  118. package/src/app-sdk/filename.js +174 -0
  119. package/src/app-sdk/forge-url.js +224 -0
  120. package/src/app-sdk/ids.js +71 -0
  121. package/src/app-sdk/ingot-access-log.js +51 -0
  122. package/src/app-sdk/phone.js +139 -0
  123. package/src/app-sdk/platform-domains.js +99 -0
  124. package/src/app-sdk/request-path.js +81 -0
  125. package/src/app-sdk/transfer-retry.js +563 -0
  126. package/src/app-sdk/transient.js +30 -0
  127. package/src/app-sdk/upload-publication.js +102 -0
  128. package/src/app-sdk/vat-refusal.js +74 -0
  129. package/src/billing.ts +16 -25
  130. package/src/config.ts +56 -23
  131. package/src/encoding.ts +91 -103
  132. package/src/entropy.test-d.ts +47 -0
  133. package/src/entropy.ts +86 -20
  134. package/src/errors.ts +135 -38
  135. package/src/folders.ts +8 -7
  136. package/src/http.ts +145 -150
  137. package/src/identity-dialog.tsx +4 -37
  138. package/src/identity-input.ts +70 -0
  139. package/src/index.ts +13 -3
  140. package/src/ingots.ts +225 -180
  141. package/src/mutex.ts +1 -8
  142. package/src/notify.test-d.ts +3 -1
  143. package/src/notify.ts +19 -17
  144. package/src/products/identity.ts +45 -39
  145. package/src/request-path.ts +45 -0
  146. package/src/sparks.ts +16 -13
  147. package/src/tus.ts +1054 -408
  148. package/src/types.ts +181 -23
  149. package/src/users.ts +27 -17
  150. package/src/validation.ts +30 -24
  151. package/src/vaults.test-d.ts +29 -0
  152. package/src/vaults.ts +87 -153
  153. package/dist/platform-domains.generated.d.ts +0 -2
  154. package/dist/platform-domains.generated.js +0 -16
  155. package/dist/platform-domains.generated.js.map +0 -1
  156. package/src/platform-domains.generated.ts +0 -17
package/src/types.ts CHANGED
@@ -5,7 +5,9 @@ export type PoolState = 'ok' | 'notice' | 'warning' | 'exhausted';
5
5
  export interface PoolStatus {
6
6
  used_gb: number;
7
7
  limit_gb: number | null;
8
+ /** Used fraction of the pool: 0.85 is 85% full, 1 is full (above 1 when over). */
8
9
  pct: number;
10
+ /** The server's severity for the pool, from the canonical thresholds; display keys off it. */
9
11
  state: PoolState;
10
12
  }
11
13
 
@@ -137,6 +139,12 @@ export interface RemoveMemberResponse {
137
139
  user_id: string;
138
140
  removed_at: number;
139
141
  dvaks_revoked: number;
142
+ /**
143
+ * True when this removal ends the subscription: the member renewed the last
144
+ * license it renews, and it was not already set to end. It ends at the
145
+ * period end.
146
+ */
147
+ wind_down: boolean;
140
148
  }
141
149
 
142
150
  export interface ResendInviteResponse {
@@ -177,7 +185,7 @@ export interface SparkVaultSubscription {
177
185
  renewal_date: number | null;
178
186
  renewal_amount_usd: string;
179
187
  seats: {
180
- /** Cycle-unique billed counts (flex billing: what the renewal invoices). */
188
+ /** Licenses the subscription renews with — what the next renewal bills. */
181
189
  full: number;
182
190
  viewer: number;
183
191
  licensed: number;
@@ -187,6 +195,18 @@ export interface SparkVaultSubscription {
187
195
  assigned_viewer: number;
188
196
  /** Roster members with no license (no access until licensed; free). */
189
197
  unlicensed_members: number;
198
+ /** Licenses paid for the current billing period (a removed license stays
199
+ * paid until the renewal). */
200
+ paid: { full: number; viewer: number };
201
+ /** Paid licenses nobody holds — assignable at no charge until the renewal. */
202
+ available: { full: number; viewer: number };
203
+ /**
204
+ * Licenses are prepaid per period on a web subscription: a removed one
205
+ * stays paid until the renewal, free to give to someone else until then.
206
+ * False on a store subscription (its one license lands on a member by
207
+ * itself) and on an account with no web subscription.
208
+ */
209
+ prepaid: boolean;
190
210
  };
191
211
  blocks: {
192
212
  /** Effective capacity blocks (this subscription plus any add-on rail). */
@@ -204,6 +224,21 @@ export interface SparkVaultSubscription {
204
224
  /** Native in-app purchase store channel for receipt validation. */
205
225
  export type IapPlatform = 'ios' | 'android';
206
226
 
227
+ /** The store rail a validated purchase is billed on. */
228
+ export type IapChannel = 'appstore' | 'play';
229
+
230
+ /**
231
+ * POST /v1/billing/iap/validate — whether Core provisioned the purchase: a
232
+ * provisioning status, not a subscription (read the subscription again with
233
+ * `billing.getSubscription()` to show what changed). A purchase the store
234
+ * reports as not entitling (expired, refunded, revoked or not yet paid) is
235
+ * answered, not refused, and provisions nothing; every other refusal is an
236
+ * error. `addon` marks a capacity add-on stacked on the license.
237
+ */
238
+ export type IapValidation =
239
+ | { provisioned: true; channel: IapChannel; addon?: true }
240
+ | { provisioned: false; reason: 'not_entitling' };
241
+
207
242
  export type IdentityType = 'email' | 'phone';
208
243
  export type VerifiedIdentityType = IdentityType | 'social';
209
244
 
@@ -542,9 +577,104 @@ export interface UnsealVaultResponse {
542
577
  ttl_seconds: number;
543
578
  }
544
579
 
580
+ /** One page of GET /v1/vaults. */
545
581
  export interface ListVaultsResponse {
546
582
  vaults: Vault[];
547
- total: number;
583
+ count: number;
584
+ has_more: boolean;
585
+ next_cursor?: string;
586
+ }
587
+
588
+ /** A vault's public sharing policy (GET /v1/vaults/{id}/sharing). */
589
+ export interface VaultSharingConfig {
590
+ vault_id: string;
591
+ public_sharing_enabled: boolean;
592
+ all_ingots_public: boolean;
593
+ session_length_seconds: number | null;
594
+ enabled_at: number | null;
595
+ }
596
+
597
+ /** The sharing policy a caller sets when enabling or updating vault sharing. */
598
+ export interface VaultSharingOptions {
599
+ all_ingots_public?: boolean;
600
+ session_length_seconds?: number;
601
+ }
602
+
603
+ export interface EnableVaultSharingResponse {
604
+ vault_id: string;
605
+ public_sharing_enabled: boolean;
606
+ all_ingots_public: boolean;
607
+ session_length_seconds: number;
608
+ enabled_at: number;
609
+ }
610
+
611
+ export interface DisableVaultSharingResponse {
612
+ vault_id: string;
613
+ public_sharing_enabled: false;
614
+ disabled_at: number;
615
+ shares_revoked: number;
616
+ }
617
+
618
+ export interface UpdateVaultSharingResponse {
619
+ vault_id: string;
620
+ public_sharing_enabled: boolean;
621
+ all_ingots_public: boolean;
622
+ session_length_seconds: number;
623
+ updated_at: number;
624
+ }
625
+
626
+ /** How long a vault keeps its access log (GET /v1/vaults/{id}/access-log-retention). */
627
+ export interface AccessLogRetention {
628
+ vault_id: string;
629
+ access_log_retention_seconds: number;
630
+ }
631
+
632
+ export interface UpdateAccessLogRetentionResponse extends AccessLogRetention {
633
+ updated_at: number;
634
+ }
635
+
636
+ /** A vault's upload portal and widget settings (GET /v1/vaults/{id}/upload). */
637
+ export interface UploadConfig {
638
+ vault_id: string;
639
+ upload_portal_enabled: boolean;
640
+ upload_portal_enabled_at: number | null;
641
+ upload_widget_enabled: boolean;
642
+ upload_widget_enabled_at: number | null;
643
+ max_size_bytes: number | null;
644
+ notification_email: string | null;
645
+ portal_url?: string;
646
+ }
647
+
648
+ export interface UploadPortalEnabled {
649
+ vault_id: string;
650
+ upload_portal_enabled: true;
651
+ enabled_at: number;
652
+ portal_url: string;
653
+ }
654
+
655
+ export interface UploadWidgetEnabled {
656
+ vault_id: string;
657
+ upload_widget_enabled: true;
658
+ enabled_at: number;
659
+ }
660
+
661
+ export interface UploadPortalDisabled {
662
+ vault_id: string;
663
+ upload_portal_enabled: false;
664
+ disabled_at: number;
665
+ }
666
+
667
+ export interface UploadWidgetDisabled {
668
+ vault_id: string;
669
+ upload_widget_enabled: false;
670
+ disabled_at: number;
671
+ }
672
+
673
+ export interface UploadConfigUpdate {
674
+ vault_id: string;
675
+ max_size_bytes: number;
676
+ notification_email: string | null;
677
+ updated_at: number;
548
678
  }
549
679
 
550
680
  export interface Ingot {
@@ -553,7 +683,8 @@ export interface Ingot {
553
683
  content_type: string;
554
684
  size_bytes: number;
555
685
  storage_location: 'dynamodb' | 's3';
556
- status?: 'uploading' | 'encrypting' | 'active' | 'failed';
686
+ /** `uploading` until Forge activates the upload, then `active`. */
687
+ status?: 'uploading' | 'active';
557
688
  folder_id?: string | null;
558
689
  created_at: number;
559
690
  accessed_at?: number;
@@ -586,24 +717,36 @@ export interface ListIngotsResponse {
586
717
  next_cursor?: string;
587
718
  }
588
719
 
720
+ /**
721
+ * GET /vaults/:id/contents: one folder whole, in one response — the folder
722
+ * (null at the vault root), the trail from the root to it, its direct
723
+ * subfolders (pinned first) and every ingot directly in it.
724
+ */
725
+ export interface FolderContentsResponse {
726
+ folder: { folder_id: string; name: string; parent_id: string | null } | null;
727
+ breadcrumb: Array<{ folder_id: string | null; name: string }>;
728
+ folders: Folder[];
729
+ ingots: Ingot[];
730
+ }
731
+
589
732
  export interface UploadResult {
590
733
  ingot_id: string;
591
734
  name: string;
592
735
  size_bytes: number;
593
736
  storage_location: 's3';
594
737
  /**
595
- * Whether this upload minted the ingot (true) or was handed an existing row
596
- * by the server's same-name upsert / a resumed session (false). A caller
597
- * that discards its ingot on a failure path needs this to tell "mine to
598
- * discard" from "someone else's live row".
738
+ * Whether this upload minted the ingot (true) or went into an existing row
739
+ * (false): the one holding the name, as the create's name-conflict policy
740
+ * allowed, a replace, or a resumed session. A caller that discards its
741
+ * ingot on a failure path needs this to tell "mine to discard" from
742
+ * "someone else's live row".
599
743
  */
600
744
  created?: boolean;
601
745
  /**
602
- * The ingot status the upload last observed: `active` when the server
603
- * confirmed activation. Anything else means the transfer completed but the
604
- * activation poll budget ran out first - the ingot usually activates
605
- * moments later, but a caller recording "backed up" must not take that on
606
- * faith (resume the session later; Forge re-confirms finalization).
746
+ * The ingot status the upload confirmed: `active`, the server's read of
747
+ * the published ingot. An upload whose activation the poll budget could
748
+ * not confirm never resolves: it rejects with a `TusUploadError` whose
749
+ * `retryLater` is set.
607
750
  */
608
751
  status?: Ingot['status'];
609
752
  }
@@ -623,9 +766,19 @@ export interface IngotInvite {
623
766
  created_at: number;
624
767
  }
625
768
 
769
+ /**
770
+ * An ingot access-log entry: a download that delivered the file, a SparkLink
771
+ * open of it included. The server records an access as the download that
772
+ * delivered it, so `download` is the one action.
773
+ */
626
774
  export interface IngotAccessLog {
775
+ /**
776
+ * The entry's time (Unix seconds) as a string. The server issues no entry id,
777
+ * so entries in the same second share it.
778
+ */
627
779
  log_id: string;
628
- action: 'download' | 'view';
780
+ action: 'download';
781
+ /** Who accessed: a member's email, an API key's name, or a SparkLink visitor's identity */
629
782
  identity: string;
630
783
  ip_address: string;
631
784
  user_agent: string;
@@ -638,18 +791,14 @@ export interface IngotAccessStats {
638
791
  last_accessed_at?: number;
639
792
  }
640
793
 
794
+ /** A folder as every folder route answers it (Core's FolderRepository.toPublicFormat). */
641
795
  export interface Folder {
642
796
  folder_id: string;
643
- vault_id: string;
644
797
  name: string;
645
798
  parent_id: string | null;
646
- is_system: boolean;
647
799
  pinned: boolean;
648
800
  created_at: number;
649
801
  updated_at?: number;
650
- path?: string;
651
- depth?: number;
652
- ingot_count?: number;
653
802
  }
654
803
 
655
804
  export interface CreateFolderRequest {
@@ -663,9 +812,9 @@ export interface UpdateFolderRequest {
663
812
  pinned?: boolean;
664
813
  }
665
814
 
815
+ /** GET /v1/vaults/{id}/folders: the vault's whole folder list. */
666
816
  export interface ListFoldersResponse {
667
817
  folders: Folder[];
668
- total: number;
669
818
  }
670
819
 
671
820
  export interface FolderBreadcrumbItem {
@@ -716,14 +865,23 @@ export interface MobileFileReader {
716
865
 
717
866
  /**
718
867
  * A tus upload session as Forge created it: the session URL, the per-upload
719
- * token every request on it must carry, and the chunk geometry Forge fixed
720
- * for it. Persist it to resume the upload after a failed attempt or a
721
- * relaunch (`ingots.resumeUpload`) instead of re-sending bytes Forge holds.
868
+ * token every request on it must carry, the chunk geometry Forge fixed for
869
+ * it, and when it lapses. Persist it to resume the upload after a failed
870
+ * attempt or a relaunch (`ingots.resumeUpload`) instead of re-sending bytes
871
+ * Forge holds.
722
872
  */
723
873
  export interface TusSessionInfo {
724
874
  uploadUrl: string;
725
875
  istk: string;
726
876
  chunkSize: number;
877
+ /**
878
+ * When the session lapses (epoch ms, Forge's `Upload-Expires`), or null
879
+ * when Forge named no expiry. Forge slides it forward on every committed
880
+ * chunk, so a caller that persists the session keeps this current through
881
+ * the upload's `onSessionExpiry`; past it, Forge refuses every request to
882
+ * the session (`isSessionExpired`).
883
+ */
884
+ expiresAt: number | null;
727
885
  }
728
886
 
729
887
  /**
@@ -919,7 +1077,7 @@ export type SparkShareStatus =
919
1077
  link_code: string;
920
1078
  visibility: string;
921
1079
  invites: string[];
922
- /** Single-use lifecycle: 'active' → 'consumed' (opened once) | 'revoked'. */
1080
+ /** Single-use lifecycle: 'active' → 'consumed' (opened once). */
923
1081
  status: string;
924
1082
  expires_at?: number;
925
1083
  created_at?: number;
package/src/users.ts CHANGED
@@ -7,15 +7,17 @@ import type {
7
7
  RemoveMemberResponse,
8
8
  ResendInviteResponse,
9
9
  } from './types.js';
10
+ import { apiPath } from './request-path.js';
11
+ import { validateResourceId } from './validation.js';
10
12
 
11
13
  /**
12
14
  * Organization team client (member admins only — the API gates these on the
13
15
  * admin role and returns 403 otherwise).
14
16
  *
15
- * Licensing is a billing action (flex: nothing charges mid-cycle — the renewal
16
- * bills each cycle's unique license holders), so the only seat-affecting
17
- * operation exposed here is `invite`, which defaults new members to
18
- * `unlicensed` and never charges. Assigning or changing a paid license is
17
+ * Licensing is a billing action (licenses are prepaid: assigning or upgrading
18
+ * one charges the rest of the current billing period at once), so the only
19
+ * seat-affecting operation exposed here is `invite`, which defaults new members
20
+ * to `unlicensed` and never charges. Assigning or changing a paid license is
19
21
  * intentionally absent — clients route that to the web dashboard so native
20
22
  * apps never initiate an out-of-band purchase.
21
23
  */
@@ -28,33 +30,41 @@ export class MobileUsersClient {
28
30
 
29
31
  /** List organization members (active and invited), with optional search/filter. */
30
32
  async list(options: ListMembersOptions = {}): Promise<ListMembersResponse> {
31
- const params = new URLSearchParams();
32
- if (options.search) params.set('search', options.search);
33
- if (options.status) params.set('status', options.status);
34
- if (typeof options.limit === 'number') params.set('limit', String(options.limit));
35
- if (options.cursor) params.set('cursor', options.cursor);
36
- const query = params.toString();
37
- const response = await this.http.get<ListMembersResponse>(
38
- query ? `/users?${query}` : '/users'
39
- );
33
+ const response = await this.http.get<ListMembersResponse>(apiPath`/users`, {
34
+ query: {
35
+ search: options.search,
36
+ status: options.status,
37
+ limit: typeof options.limit === 'number' ? options.limit : undefined,
38
+ cursor: options.cursor,
39
+ },
40
+ });
40
41
  return response.data;
41
42
  }
42
43
 
43
44
  /** Invite a coworker by email. Defaults to an unlicensed seat (no charge). */
44
45
  async invite(request: InviteMemberRequest): Promise<InviteMemberResponse> {
45
- const response = await this.http.post<InviteMemberResponse>('/users/invite', request);
46
+ const response = await this.http.post<InviteMemberResponse>(apiPath`/users/invite`, request);
46
47
  return response.data;
47
48
  }
48
49
 
49
- /** Remove a member and revoke their DVAKs. Access ends immediately; the current cycle still bills them, and they stop counting next cycle. */
50
+ /**
51
+ * Remove a member and revoke their DVAKs. Access ends immediately. A license
52
+ * they held stays paid until the end of the billing period — nothing is
53
+ * refunded — and is not renewed; until then it can be given to someone else
54
+ * at no charge. `wind_down` is true when this removal ends the
55
+ * subscription — theirs was the last license it renews, and it was not
56
+ * already set to end: it ends at the period end.
57
+ */
50
58
  async remove(userId: string): Promise<RemoveMemberResponse> {
51
- const response = await this.http.delete<RemoveMemberResponse>(`/users/${userId}`);
59
+ validateResourceId(userId, 'user ID', 'userId');
60
+ const response = await this.http.delete<RemoveMemberResponse>(apiPath`/users/${userId}`);
52
61
  return response.data;
53
62
  }
54
63
 
55
64
  /** Resend a pending invitation email (resets the 7-day expiry). */
56
65
  async resendInvite(userId: string): Promise<ResendInviteResponse> {
57
- const response = await this.http.post<ResendInviteResponse>(`/users/${userId}/resend-invite`);
66
+ validateResourceId(userId, 'user ID', 'userId');
67
+ const response = await this.http.post<ResendInviteResponse>(apiPath`/users/${userId}/resend-invite`);
58
68
  return response.data;
59
69
  }
60
70
  }
package/src/validation.ts CHANGED
@@ -1,28 +1,32 @@
1
+ import { isResourceId } from './app-sdk/ids.js';
1
2
  import { SparkVaultValidationError } from './errors.js';
3
+ import { SAVED_FILENAME_FALLBACK, toStorableFilename } from './app-sdk/filename.js';
2
4
 
3
- export function isSafeId(id: string): boolean {
4
- if (!id || typeof id !== 'string') return false;
5
- if (id.includes('/') || id.includes('\\') || id.includes('..') || id.includes('\0')) return false;
6
- if (id.length < 8 || id.length > 128) return false;
7
- return /^[a-zA-Z0-9_-]+$/.test(id);
5
+ /** Whether a value is a well-formed SparkVault resource id (the platform's one rule). */
6
+ export { isResourceId as isSafeId };
7
+
8
+ /**
9
+ * Refuse a value that is not a SparkVault resource id before it names a route.
10
+ *
11
+ * @param label - How the message names the id, e.g. 'spark ID'.
12
+ * @param field - The argument's name, reported in the error details.
13
+ */
14
+ export function validateResourceId(value: unknown, label: string, field: string): void {
15
+ if (!isResourceId(value)) {
16
+ throw new SparkVaultValidationError(`Invalid ${label} format`, { [field]: value });
17
+ }
8
18
  }
9
19
 
10
20
  export function validateVaultId(vaultId: string): void {
11
- if (!isSafeId(vaultId)) {
12
- throw new SparkVaultValidationError('Invalid vault ID format', { vaultId });
13
- }
21
+ validateResourceId(vaultId, 'vault ID', 'vaultId');
14
22
  }
15
23
 
16
24
  export function validateIngotId(ingotId: string): void {
17
- if (!isSafeId(ingotId)) {
18
- throw new SparkVaultValidationError('Invalid ingot ID format', { ingotId });
19
- }
25
+ validateResourceId(ingotId, 'ingot ID', 'ingotId');
20
26
  }
21
27
 
22
28
  export function validateFolderId(folderId: string): void {
23
- if (!folderId || typeof folderId !== 'string' || !folderId.startsWith('fld_') || !isSafeId(folderId)) {
24
- throw new SparkVaultValidationError('Invalid folder ID format', { folderId });
25
- }
29
+ validateResourceId(folderId, 'folder ID', 'folderId');
26
30
  }
27
31
 
28
32
  /** Windows reserved filenames that cannot be used. */
@@ -33,17 +37,19 @@ const WINDOWS_RESERVED_NAMES = new Set([
33
37
  ]);
34
38
 
35
39
  /**
36
- * Sanitize a filename to prevent path traversal and other security issues:
37
- * removes null bytes and control characters, strips path traversal sequences,
38
- * replaces directory separators, strips leading dots (hidden files), and
39
- * prefixes Windows reserved names. Falls back to 'file' when nothing remains.
40
+ * The file name to store or save text under, by the platform's one file-name
41
+ * rule (@sparkvault/app-sdk/filename, mirrored into ./app-sdk/filename.js):
42
+ * path separators, control characters and the bidi controls that disguise an
43
+ * extension (`invoice<U+202E>fdp.exe` shows as `invoiceexe.pdf`) become `_`,
44
+ * and so do the characters SparkVault refuses in a name (: * ? " < > |), so a
45
+ * name derived from a note's title uploads. With every separator gone no name
46
+ * can climb a directory, so a `..` inside a name is kept. On top of the rule,
47
+ * for the device's file system: leading dots are stripped (a hidden file) and
48
+ * Windows reserved names are prefixed. Falls back to the platform's one
49
+ * fallback name (SAVED_FILENAME_FALLBACK) when nothing remains.
40
50
  */
41
51
  export function sanitizeFilename(filename: string): string {
42
- let sanitized = filename
43
- .replace(/\0/g, '')
44
- .replace(/[\x00-\x1f\x7f]/g, '')
45
- .replace(/\.\./g, '')
46
- .replace(/[\/\\]/g, '_')
52
+ let sanitized = toStorableFilename(filename)
47
53
  .replace(/^\.+/, '')
48
54
  .trim();
49
55
 
@@ -52,5 +58,5 @@ export function sanitizeFilename(filename: string): string {
52
58
  sanitized = `_${sanitized}`;
53
59
  }
54
60
 
55
- return sanitized || 'file';
61
+ return sanitized || SAVED_FILENAME_FALLBACK;
56
62
  }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Vault and folder wire-contract assertions — COMPILE-TIME ONLY.
3
+ *
4
+ * These types have no runtime, so a unit test can never catch them drifting from
5
+ * the server: the transpiler strips them and every assertion passes. `tsc
6
+ * --noEmit` (`pnpm typecheck`) is the gate instead. It is deliberately NOT a
7
+ * `*.test.ts`, which tsconfig excludes.
8
+ *
9
+ * Mirrored server sources: apps/api/src/folders/list.js (listFolders answers
10
+ * `{ folders }`, the whole list and nothing else) and apps/api/src/vaults/list.js
11
+ * (one page: `{ vaults, count, has_more, next_cursor? }`).
12
+ */
13
+
14
+ import type { ListFoldersResponse, ListVaultsResponse } from './types.js';
15
+
16
+ /** Fails to compile unless `T` resolves to exactly `true`. */
17
+ type Assert<T extends true> = T;
18
+
19
+ /** Invariant type equality: the SAME type, not merely mutually assignable. */
20
+ type Equals<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2
21
+ ? true
22
+ : false;
23
+
24
+ export type VaultsWireContract = [
25
+ // A folder listing carries the folders and nothing else: a `total` the server
26
+ // never sends would read as undefined wherever a caller trusted it.
27
+ Assert<Equals<keyof ListFoldersResponse, 'folders'>>,
28
+ Assert<Equals<keyof ListVaultsResponse, 'vaults' | 'count' | 'has_more' | 'next_cursor'>>,
29
+ ];