@sparkvault/sdk-mobile 1.0.2 → 1.0.4

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/src/index.ts CHANGED
@@ -65,7 +65,7 @@ export {
65
65
  MobileBillingClient,
66
66
  } from './billing.js';
67
67
  export type {
68
- BillingBalance,
68
+ PortalSession,
69
69
  } from './billing.js';
70
70
  export {
71
71
  MobileTusUploader,
@@ -85,6 +85,9 @@ export {
85
85
  utf8ToBytes,
86
86
  } from './encoding.js';
87
87
  export {
88
+ gateErrorFromBody,
89
+ PlanRequiredError,
90
+ QuotaExceededError,
88
91
  SparkVaultAuthenticationError,
89
92
  SparkVaultAuthorizationError,
90
93
  SparkVaultMobileError,
package/src/tus.ts CHANGED
@@ -1,7 +1,12 @@
1
1
  import type { ResolvedMobileConfig } from './config.js';
2
2
  import { base64EncodeUtf8, base64ToBytes } from './encoding.js';
3
- import { SparkVaultValidationError, TusUploadError } from './errors.js';
4
- import type { DebugLogger, MobileFileReader, UploadProgressCallback } from './types.js';
3
+ import {
4
+ gateErrorFromBody,
5
+ SparkVaultMobileError,
6
+ SparkVaultValidationError,
7
+ TusUploadError,
8
+ } from './errors.js';
9
+ import type { ApiErrorBody, DebugLogger, MobileFileReader, UploadProgressCallback } from './types.js';
5
10
 
6
11
  export interface ParsedForgeUrl {
7
12
  baseUrl: string;
@@ -47,6 +52,21 @@ export function parseForgeUrl(forgeUrl: string): ParsedForgeUrl {
47
52
  };
48
53
  }
49
54
 
55
+ /**
56
+ * Parse a Forge error response body (JSON text) into the shared ApiErrorBody
57
+ * shape so gate mapping can read its `error.code` / `error.details.resource`.
58
+ * Returns an empty body on non-JSON text, which simply matches no gate.
59
+ */
60
+ function parseErrorBody(text: string): ApiErrorBody {
61
+ if (!text) return {};
62
+ try {
63
+ const parsed: unknown = JSON.parse(text);
64
+ return parsed && typeof parsed === 'object' ? (parsed as ApiErrorBody) : {};
65
+ } catch {
66
+ return {};
67
+ }
68
+ }
69
+
50
70
  function getErrorName(err: unknown): string {
51
71
  return err instanceof Error ? err.name : 'UnknownError';
52
72
  }
@@ -119,6 +139,15 @@ async function createTusUpload(
119
139
 
120
140
  if (!response.ok) {
121
141
  const errorText = await response.text().catch(() => '');
142
+
143
+ // A 402 here is a billable gate hit mid-create (e.g. storage/bandwidth pool
144
+ // exhausted). Route it through the same typed-gate mapping as the JSON HTTP
145
+ // path so callers handle subscribe / add-on UX off one set of error types.
146
+ const gateError = gateErrorFromBody(response.status, parseErrorBody(errorText));
147
+ if (gateError) {
148
+ throw gateError;
149
+ }
150
+
122
151
  throw new TusUploadError(`TUS create failed: ${response.status} - ${errorText}`, {
123
152
  httpStatus: response.status,
124
153
  filename,
@@ -191,6 +220,11 @@ function uploadChunk(
191
220
  const newOffset = offsetHeader ? parseInt(offsetHeader, 10) : offset + chunkSize;
192
221
  finish(() => resolve(newOffset));
193
222
  } else {
223
+ const gateError = gateErrorFromBody(xhr.status, parseErrorBody(xhr.responseText));
224
+ if (gateError) {
225
+ finish(() => reject(gateError));
226
+ return;
227
+ }
194
228
  finish(() => reject(new TusUploadError(`Chunk upload failed: ${xhr.status} - ${xhr.responseText.substring(0, 200)}`, {
195
229
  httpStatus: xhr.status,
196
230
  phase: 'upload',
@@ -287,6 +321,10 @@ export class MobileTusUploader {
287
321
  }
288
322
 
289
323
  if (err instanceof TusUploadError) throw err;
324
+ // Typed gate errors (PlanRequiredError / QuotaExceededError) must reach
325
+ // the caller intact so the subscribe / add-on UX fires; never bury them
326
+ // in a generic TusUploadError.
327
+ if (err instanceof SparkVaultMobileError) throw err;
290
328
  throw TusUploadError.fromError(error, {
291
329
  filename: options.filename,
292
330
  fileSize: options.fileSize,
package/src/types.ts CHANGED
@@ -1,9 +1,31 @@
1
+ /** Utilization state shared by storage, bandwidth, and Identity pools. */
2
+ export type PoolState = 'ok' | 'notice' | 'warning' | 'exhausted';
3
+
4
+ /** Live usage for a single capacity pool. `limit_gb` is null on unlimited plans. */
5
+ export interface PoolStatus {
6
+ used_gb: number;
7
+ limit_gb: number | null;
8
+ pct: number;
9
+ state: PoolState;
10
+ }
11
+
12
+ /** Bandwidth pool additionally reports when the metered cycle resets. */
13
+ export interface BandwidthPoolStatus extends PoolStatus {
14
+ cycle_resets_at: number | null;
15
+ }
16
+
17
+ /** Storage + bandwidth pool snapshot; rides responses on `meta.pools`. */
18
+ export interface SubscriptionPools {
19
+ storage: PoolStatus;
20
+ bandwidth: BandwidthPoolStatus;
21
+ }
22
+
1
23
  export interface ApiMeta {
2
24
  request_id: string;
3
25
  response_ms: number;
4
26
  timestamp: number;
5
27
  api_version: string;
6
- balance?: string;
28
+ pools?: SubscriptionPools;
7
29
  quota?: {
8
30
  limit: number;
9
31
  used: number;
@@ -48,12 +70,49 @@ export interface SparkVaultAccount {
48
70
  organization_name: string;
49
71
  email: string;
50
72
  status: 'active' | 'suspended';
51
- balance: string;
52
73
  logo_url?: string;
53
74
  logo_dark_url?: string;
54
75
  created_at: number;
55
76
  }
56
77
 
78
+ /** Identity attempts usage against the purchased tier cap. */
79
+ export interface SubscriptionIdentityStatus {
80
+ attempts_cap: number | null;
81
+ attempts_used: number;
82
+ soft_cap: number | null;
83
+ state: PoolState;
84
+ }
85
+
86
+ /**
87
+ * GET /v1/billing/subscription — the account's current plan, licensed vs. used
88
+ * seats, capacity blocks, live pool usage, and Identity attempts. Drives the
89
+ * subscription / usage view. `renewal_amount_usd` is a formatted dollars string;
90
+ * `renewal_date` is an epoch (seconds) or null when there is no active plan.
91
+ */
92
+ export interface SparkVaultSubscription {
93
+ plan: 'subscription' | 'internal' | 'unsubscribed';
94
+ unlimited: boolean;
95
+ subscription_status: string | null;
96
+ past_due: boolean;
97
+ renewal_date: number | null;
98
+ renewal_amount_usd: string;
99
+ seats: {
100
+ full: number;
101
+ viewer: number;
102
+ licensed: number;
103
+ used: number;
104
+ };
105
+ blocks: {
106
+ storage: number;
107
+ bandwidth: number;
108
+ };
109
+ pools: SubscriptionPools;
110
+ identity: SubscriptionIdentityStatus;
111
+ }
112
+
113
+ /** Native in-app purchase store channel for receipt validation. */
114
+ export type IapPlatform = 'ios' | 'android';
115
+
57
116
  export type IdentityType = 'email' | 'phone';
58
117
  export type VerifiedIdentityType = IdentityType | 'social';
59
118
 
@@ -118,6 +177,17 @@ export interface IdentityTokenClaims {
118
177
  method: string;
119
178
  }
120
179
 
180
+ /**
181
+ * A server-decided post-login nudge. Computed once per login (default mode);
182
+ * clients act on the steps they support, in order.
183
+ */
184
+ export type Recommendation =
185
+ | { type: 'passkey' }
186
+ | { type: 'backup_identifier'; missingType: 'email' | 'phone' };
187
+
188
+ /** Canonical dismiss key for a recommendation (suppresses it server-side 30d). */
189
+ export type RecommendationKey = 'passkey' | 'backup_email' | 'backup_phone';
190
+
121
191
  export interface IdentityVerifyResult {
122
192
  token: string;
123
193
  identity: string;
@@ -126,6 +196,27 @@ export interface IdentityVerifyResult {
126
196
  redirect?: string;
127
197
  jwksUri: string;
128
198
  jwks?: IdentityJwks;
199
+ /** Ordered post-login nudges (default-mode logins only). */
200
+ recommendations?: Recommendation[];
201
+ }
202
+
203
+ /** Response to POST /backup-verification/send. */
204
+ export interface BackupSendResponse {
205
+ /** Opaque pending-attach session id (present when a code was sent). */
206
+ session_id?: string;
207
+ /** Delivery channel actually used. */
208
+ method?: 'email' | 'sms' | 'voice';
209
+ /** Code expiry (Unix seconds). */
210
+ expires_at?: number;
211
+ /** True when the identifier is already on the identity — no code was sent. */
212
+ already_verified?: boolean;
213
+ }
214
+
215
+ /** Response to POST /backup-verification/verify. */
216
+ export interface BackupVerifyResponse {
217
+ verified: boolean;
218
+ type: 'email' | 'phone';
219
+ value: string;
129
220
  }
130
221
 
131
222
  export interface SendTotpRequest {