@sparkvault/sdk-mobile 1.0.5 → 3.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.
package/dist/sparks.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { MobileHttpClient } from './http.js';
2
+ import type { ShareSparkResponse, SparkShareStatus, UnshareSparkResponse } from './types.js';
2
3
  export interface Spark {
3
4
  spark_id: string;
4
5
  status: string;
@@ -30,10 +31,30 @@ export interface CreateSparkOptions {
30
31
  export interface CreateSparkResponse {
31
32
  spark_id: string;
32
33
  }
34
+ export interface ShareSparkOptions {
35
+ /** Link visibility. Defaults to public server-side. */
36
+ visibility?: 'public' | 'authenticated' | 'invite_only';
37
+ /** For invite_only visibility: identities granted access (server allows max 1). */
38
+ invites?: string[];
39
+ /** Link expiration TTL in seconds (server minimum 60, capped at the spark's own expiry). */
40
+ expiresInSeconds?: number;
41
+ }
33
42
  export declare class MobileSparksClient {
34
43
  private readonly http;
35
44
  constructor(http: MobileHttpClient);
36
45
  list(options?: ListSparksOptions): Promise<ListSparksResponse>;
37
46
  get(sparkId: string): Promise<SparkResponse>;
38
47
  create(options: CreateSparkOptions): Promise<CreateSparkResponse>;
48
+ /** Burn a spark before its natural expiry. An already-burned spark resolves (the server returns 204); a missing spark rejects with a 404. */
49
+ delete(sparkId: string): Promise<void>;
50
+ /**
51
+ * Create (or return the existing) public SparkLink for a spark, yielding a
52
+ * shareable `https://x.sv/...` URL. Only active, non-expired sparks can be
53
+ * shared.
54
+ */
55
+ share(sparkId: string, options?: ShareSparkOptions): Promise<ShareSparkResponse>;
56
+ /** Current sharing status for a spark. `shared` is false when no SparkLink exists. */
57
+ getShare(sparkId: string): Promise<SparkShareStatus>;
58
+ /** Revoke a spark's SparkLink. Resolves whether or not a link existed. */
59
+ unshare(sparkId: string): Promise<UnshareSparkResponse>;
39
60
  }
package/dist/sparks.js CHANGED
@@ -28,5 +28,35 @@ export class MobileSparksClient {
28
28
  });
29
29
  return response.data;
30
30
  }
31
+ /** Burn a spark before its natural expiry. An already-burned spark resolves (the server returns 204); a missing spark rejects with a 404. */
32
+ async delete(sparkId) {
33
+ await this.http.delete(`/sparks/${encodeURIComponent(sparkId)}`);
34
+ }
35
+ /**
36
+ * Create (or return the existing) public SparkLink for a spark, yielding a
37
+ * shareable `https://x.sv/...` URL. Only active, non-expired sparks can be
38
+ * shared.
39
+ */
40
+ async share(sparkId, options = {}) {
41
+ const body = {};
42
+ if (options.visibility)
43
+ body.visibility = options.visibility;
44
+ if (options.invites)
45
+ body.invites = options.invites;
46
+ if (options.expiresInSeconds !== undefined)
47
+ body.expires_in_seconds = options.expiresInSeconds;
48
+ const response = await this.http.patch(`/sparks/${encodeURIComponent(sparkId)}/share`, body);
49
+ return response.data;
50
+ }
51
+ /** Current sharing status for a spark. `shared` is false when no SparkLink exists. */
52
+ async getShare(sparkId) {
53
+ const response = await this.http.get(`/sparks/${encodeURIComponent(sparkId)}/share`);
54
+ return response.data;
55
+ }
56
+ /** Revoke a spark's SparkLink. Resolves whether or not a link existed. */
57
+ async unshare(sparkId) {
58
+ const response = await this.http.delete(`/sparks/${encodeURIComponent(sparkId)}/share`);
59
+ return response.data;
60
+ }
31
61
  }
32
62
  //# sourceMappingURL=sparks.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"sparks.js","sourceRoot":"","sources":["../src/sparks.ts"],"names":[],"mappings":"AAuCA,MAAM,OAAO,kBAAkB;IAG7B,YAAY,IAAsB;QAChC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,UAA6B,EAAE;QACxC,MAAM,MAAM,GAAG,IAAI,eAAe,EAAE,CAAC;QACrC,IAAI,OAAO,CAAC,QAAQ;YAAE,MAAM,CAAC,GAAG,CAAC,UAAU,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;QAC/D,IAAI,OAAO,CAAC,KAAK;YAAE,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;QAC9D,IAAI,OAAO,CAAC,MAAM;YAAE,MAAM,CAAC,GAAG,CAAC,QAAQ,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;QAEzD,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,EAAE,CAAC;QAChC,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,CAAqB,KAAK,CAAC,CAAC,CAAC,WAAW,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;QACjG,OAAO,QAAQ,CAAC,IAAI,CAAC;IACvB,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,OAAe;QACvB,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,CAAgB,WAAW,kBAAkB,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QAC9F,OAAO,QAAQ,CAAC,IAAI,CAAC;IACvB,CAAC;IAED,KAAK,CAAC,MAAM,CAAC,OAA2B;QACtC,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,IAAI,CAAsB,SAAS,EAAE;YACpE,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,YAAY,EAAE,OAAO,CAAC,WAAW;YACjC,WAAW,EAAE,OAAO,CAAC,UAAU;YAC/B,QAAQ,EAAE,OAAO,CAAC,QAAQ;YAC1B,IAAI,EAAE,OAAO,CAAC,IAAI;SACnB,CAAC,CAAC;QACH,OAAO,QAAQ,CAAC,IAAI,CAAC;IACvB,CAAC;CACF"}
1
+ {"version":3,"file":"sparks.js","sourceRoot":"","sources":["../src/sparks.ts"],"names":[],"mappings":"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"}
package/dist/types.d.ts CHANGED
@@ -201,12 +201,6 @@ export interface RequestPinResponse {
201
201
  retry_after?: number;
202
202
  is_signup: boolean;
203
203
  }
204
- export interface PinVerifyRequest {
205
- kindling: string;
206
- pin: string;
207
- recipient: string;
208
- organization_name?: string;
209
- }
210
204
  export interface PinVerifyLoginResponse {
211
205
  access_token: string;
212
206
  refresh_token: string;
@@ -221,9 +215,10 @@ export interface PinVerifySignupPendingResponse {
221
215
  expires_at: number;
222
216
  }
223
217
  /**
224
- * Returned by verifyPin when the PIN was correct but the account has 2FA
225
- * enabled: the caller must collect a second factor and call
226
- * completeSecondFactorPin({ ticket, code }) to finish the login.
218
+ * Variant of the Core exchange result indicating the account has 2FA enabled:
219
+ * the caller must collect a second factor via
220
+ * `products.identity.submitSecondFactor({ ticket, code })` and re-exchange the
221
+ * resulting Identity token via `account.exchangeIdentityToken`.
227
222
  */
228
223
  export interface PinVerifySecondFactorPendingResponse {
229
224
  verified: false;
@@ -403,7 +398,7 @@ export interface IngotSharingConfig {
403
398
  export interface IngotInvite {
404
399
  invite_id: string;
405
400
  identity: string;
406
- identity_type: 'email';
401
+ identity_type: 'email' | 'phone';
407
402
  created_at: number;
408
403
  }
409
404
  export interface IngotAccessLog {
@@ -510,3 +505,37 @@ export interface DebugLogger {
510
505
  export interface UploadProgressCallback {
511
506
  (bytesUploaded: number, bytesTotal: number): void;
512
507
  }
508
+ /** Result of sharing a spark — the created (or already-existing) SparkLink. */
509
+ export interface ShareSparkResponse {
510
+ shared: true;
511
+ /** Public `https://x.sv/{code}` URL for the SparkLink. */
512
+ share_url: string;
513
+ link_code: string;
514
+ visibility: string;
515
+ invites?: string[];
516
+ expires_at?: number;
517
+ created_at?: number;
518
+ /** True when a SparkLink already existed and was returned as-is. */
519
+ already_shared?: boolean;
520
+ }
521
+ /** Sharing status for a spark. */
522
+ export type SparkShareStatus = {
523
+ shared: false;
524
+ spark_id: string;
525
+ } | {
526
+ shared: true;
527
+ spark_id: string;
528
+ share_url: string;
529
+ link_code: string;
530
+ visibility: string;
531
+ invites: string[];
532
+ max_uses?: number | null;
533
+ use_count: number;
534
+ expires_at?: number;
535
+ created_at?: number;
536
+ };
537
+ /** Result of unsharing a spark (deleting its SparkLink). */
538
+ export interface UnshareSparkResponse {
539
+ shared: false;
540
+ message: string;
541
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sparkvault/sdk-mobile",
3
- "version": "1.0.5",
3
+ "version": "3.0.0",
4
4
  "description": "Mobile SDK for SparkVault Identity, vault, folder, and ingot workflows",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
package/src/account.ts ADDED
@@ -0,0 +1,152 @@
1
+ import type { ResolvedMobileConfig } from './config.js';
2
+ import { SparkVaultValidationError } from './errors.js';
3
+ import type { MobileHttpClient } from './http.js';
4
+ import type {
5
+ CompleteSignupRequest,
6
+ CompleteSignupResponse,
7
+ IdentitySessionResponse,
8
+ PinVerifyResponse,
9
+ SparkVaultAccount,
10
+ SparkVaultUser,
11
+ } from './types.js';
12
+
13
+ /**
14
+ * First-party SparkVault Core session, profile, and account client. Exchanges a
15
+ * verified Identity token for an app session and owns the authenticated
16
+ * profile/account surface.
17
+ */
18
+ export class MobileAccountClient {
19
+ private readonly config: ResolvedMobileConfig;
20
+ private readonly http: MobileHttpClient;
21
+
22
+ constructor(config: ResolvedMobileConfig, http: MobileHttpClient) {
23
+ this.config = config;
24
+ this.http = http;
25
+ }
26
+
27
+ async exchangeIdentityToken(identityToken: string): Promise<PinVerifyResponse> {
28
+ const sessionResponse = await this.http.post<IdentitySessionResponse>(
29
+ '/auth/identity/verify',
30
+ { token: identityToken },
31
+ { skipAuth: true }
32
+ );
33
+
34
+ const session = sessionResponse.data;
35
+ if (session.user) {
36
+ await this.storeSession(session);
37
+ return {
38
+ access_token: session.access_token,
39
+ refresh_token: session.refresh_token,
40
+ token_type: session.token_type ?? 'Bearer',
41
+ expires_in: session.expires_in ?? 3600,
42
+ user: session.user,
43
+ };
44
+ }
45
+
46
+ return {
47
+ verified: true,
48
+ needs_signup_info: true,
49
+ signup_token: session.access_token,
50
+ expires_at: Math.floor(Date.now() / 1000) + (session.expires_in ?? 3600),
51
+ };
52
+ }
53
+
54
+ async completeSignup(
55
+ signupToken: string,
56
+ request: CompleteSignupRequest
57
+ ): Promise<CompleteSignupResponse> {
58
+ const response = await this.http.post<CompleteSignupResponse>(
59
+ '/auth/complete-signup',
60
+ request,
61
+ {
62
+ skipAuth: true,
63
+ headers: { Authorization: `Bearer ${signupToken}` },
64
+ }
65
+ );
66
+
67
+ await this.storeSession(response.data);
68
+ return response.data;
69
+ }
70
+
71
+ async signup(params: {
72
+ signup_token: string;
73
+ organization_name: string;
74
+ full_name?: string;
75
+ }): Promise<CompleteSignupResponse> {
76
+ return this.completeSignup(params.signup_token, {
77
+ organization_name: params.organization_name,
78
+ full_name: params.full_name,
79
+ });
80
+ }
81
+
82
+ async getProfile(): Promise<SparkVaultUser> {
83
+ const response = await this.http.get<SparkVaultUser>('/profile');
84
+ if (this.config.tokenStorage.setUser) {
85
+ await this.config.tokenStorage.setUser(response.data);
86
+ }
87
+ return response.data;
88
+ }
89
+
90
+ async updateProfile(updates: { name?: string }): Promise<SparkVaultUser> {
91
+ const response = await this.http.put<SparkVaultUser>('/profile', updates);
92
+ if (this.config.tokenStorage.setUser) {
93
+ await this.config.tokenStorage.setUser(response.data);
94
+ }
95
+ return response.data;
96
+ }
97
+
98
+ async getAccount(): Promise<SparkVaultAccount> {
99
+ const response = await this.http.get<SparkVaultAccount>('/account');
100
+ if (this.config.tokenStorage.setAccount) {
101
+ await this.config.tokenStorage.setAccount(response.data);
102
+ }
103
+ return response.data;
104
+ }
105
+
106
+ async revokeRefreshToken(refreshToken?: string): Promise<void> {
107
+ const token = refreshToken ?? await this.config.tokenStorage.getRefreshToken();
108
+ if (!token) return;
109
+
110
+ await this.http.post('/auth/logout', { refresh_token: token }).catch(err => {
111
+ this.config.logger.warn('Logout API call failed', { error: String(err) });
112
+ });
113
+ }
114
+
115
+ async logout(options: { revoke?: boolean; clearTokens?: boolean } = {}): Promise<void> {
116
+ if (options.revoke !== false) {
117
+ await this.revokeRefreshToken();
118
+ }
119
+ if (options.clearTokens !== false) {
120
+ await this.config.tokenStorage.clearTokens();
121
+ }
122
+ }
123
+
124
+ private async storeSession(session: {
125
+ access_token: string;
126
+ refresh_token: string;
127
+ user?: SparkVaultUser | null;
128
+ account?: SparkVaultAccount | null;
129
+ }): Promise<void> {
130
+ if (!session.access_token || !session.refresh_token) {
131
+ throw new SparkVaultValidationError('Invalid session response from SparkVault');
132
+ }
133
+
134
+ if (this.config.tokenStorage.setTokens) {
135
+ await this.config.tokenStorage.setTokens(session.access_token, session.refresh_token);
136
+ } else {
137
+ await Promise.all([
138
+ this.config.tokenStorage.setAccessToken(session.access_token),
139
+ this.config.tokenStorage.setRefreshToken(session.refresh_token),
140
+ ]);
141
+ }
142
+
143
+ const writes: Promise<void>[] = [];
144
+ if (session.user && this.config.tokenStorage.setUser) {
145
+ writes.push(this.config.tokenStorage.setUser(session.user));
146
+ }
147
+ if (session.account && this.config.tokenStorage.setAccount) {
148
+ writes.push(this.config.tokenStorage.setAccount(session.account));
149
+ }
150
+ await Promise.all(writes);
151
+ }
152
+ }
package/src/client.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { MobileAuthClient } from './auth.js';
1
+ import { MobileAccountClient } from './account.js';
2
2
  import { MobileBillingClient } from './billing.js';
3
3
  import { resolveMobileConfig, type SparkVaultMobileConfig, type ResolvedMobileConfig } from './config.js';
4
4
  import { MobileEntropyClient } from './entropy.js';
@@ -6,13 +6,15 @@ import { MobileFoldersClient } from './folders.js';
6
6
  import { MobileHealthClient } from './health.js';
7
7
  import { MobileHttpClient, type AuthEventListener } from './http.js';
8
8
  import { MobileIngotsClient } from './ingots.js';
9
+ import { MobileProductsClient } from './products/index.js';
9
10
  import { MobilePushTokensClient } from './push-tokens.js';
10
11
  import { MobileSparksClient } from './sparks.js';
11
12
  import { MobileVaultsClient } from './vaults.js';
12
13
 
13
14
  export class SparkVaultMobile {
14
15
  readonly config: ResolvedMobileConfig;
15
- readonly auth: MobileAuthClient;
16
+ readonly products: MobileProductsClient;
17
+ readonly account: MobileAccountClient;
16
18
  readonly vaults: MobileVaultsClient;
17
19
  readonly ingots: MobileIngotsClient;
18
20
  readonly folders: MobileFoldersClient;
@@ -26,7 +28,8 @@ export class SparkVaultMobile {
26
28
  constructor(config: SparkVaultMobileConfig) {
27
29
  this.config = resolveMobileConfig(config);
28
30
  this.http = new MobileHttpClient(this.config);
29
- this.auth = new MobileAuthClient(this.config, this.http);
31
+ this.products = new MobileProductsClient(this.config);
32
+ this.account = new MobileAccountClient(this.config, this.http);
30
33
  this.vaults = new MobileVaultsClient(this.http);
31
34
  this.ingots = new MobileIngotsClient(this.config, this.http);
32
35
  this.folders = new MobileFoldersClient(this.http);
@@ -37,7 +40,7 @@ export class SparkVaultMobile {
37
40
  this.billing = new MobileBillingClient(this.http);
38
41
 
39
42
  if (this.config.preloadIdentityConfig) {
40
- this.auth.preloadConfig();
43
+ this.products.identity.preloadConfig();
41
44
  }
42
45
  }
43
46
 
@@ -111,7 +111,7 @@ export function SparkVaultIdentityDialog({
111
111
  setIdentity(initialIdentity ?? '');
112
112
  setIdentityType(initialIdentityType ?? 'email');
113
113
 
114
- client.auth.getConfig()
114
+ client.products.identity.getConfig()
115
115
  .then((nextConfig) => {
116
116
  if (cancelled || !isActiveSession(sessionRef, sessionId)) return;
117
117
  setConfig(nextConfig);
@@ -156,7 +156,7 @@ export function SparkVaultIdentityDialog({
156
156
  }, [expiresAt, step]);
157
157
 
158
158
  const finishWithToken = useCallback(async (result: IdentityVerifyResult, sessionId: number) => {
159
- const jwks = await client.auth.getJwks();
159
+ const jwks = await client.products.identity.getJwks();
160
160
  if (!isActiveSession(sessionRef, sessionId)) return;
161
161
  await onSuccessRef.current({ ...result, jwks });
162
162
  }, [client]);
@@ -186,7 +186,7 @@ export function SparkVaultIdentityDialog({
186
186
  setError('');
187
187
  setSelectedMethod(method);
188
188
  try {
189
- const response = await client.auth.sendTotp({
189
+ const response = await client.products.identity.sendTotp({
190
190
  identity: targetIdentity,
191
191
  identityType: targetIdentityType,
192
192
  method: totpMethod,
@@ -225,7 +225,7 @@ export function SparkVaultIdentityDialog({
225
225
  }
226
226
  if (!isActiveSession(sessionRef, sessionId)) return;
227
227
 
228
- const challenge = await client.auth.getPasskeyAuthOptions();
228
+ const challenge = await client.products.identity.getPasskeyAuthOptions();
229
229
  if (!isActiveSession(sessionRef, sessionId)) return;
230
230
  const credential = await passkeyProvider.authenticate(challenge.options);
231
231
  if (!isActiveSession(sessionRef, sessionId)) return;
@@ -233,7 +233,7 @@ export function SparkVaultIdentityDialog({
233
233
  throw new Error('Passkey authentication was cancelled.');
234
234
  }
235
235
 
236
- const result = await client.auth.completePasskeyAuthentication(credential, challenge.session);
236
+ const result = await client.products.identity.completePasskeyAuthentication(credential, challenge.session);
237
237
  await finishWithToken(result, sessionId);
238
238
  } catch (err) {
239
239
  if (!isActiveSession(sessionRef, sessionId)) return;
@@ -304,7 +304,7 @@ export function SparkVaultIdentityDialog({
304
304
  setIsBusy(true);
305
305
  setError('');
306
306
  try {
307
- const result = await client.auth.verifyTotp({
307
+ const result = await client.products.identity.verifyTotp({
308
308
  kindling,
309
309
  pin,
310
310
  recipient: identity,
@@ -341,7 +341,7 @@ export function SparkVaultIdentityDialog({
341
341
  setError('');
342
342
  setStep('loading');
343
343
 
344
- client.auth.getConfig()
344
+ client.products.identity.getConfig()
345
345
  .then((nextConfig) => {
346
346
  if (!isActiveSession(sessionRef, sessionId)) return;
347
347
  setConfig(nextConfig);
package/src/index.ts CHANGED
@@ -11,8 +11,12 @@ export type {
11
11
  AuthEventListener,
12
12
  } from './http.js';
13
13
  export {
14
- MobileAuthClient,
15
- } from './auth.js';
14
+ MobileProductsClient,
15
+ MobileIdentityClient,
16
+ } from './products/index.js';
17
+ export {
18
+ MobileAccountClient,
19
+ } from './account.js';
16
20
  export {
17
21
  MobileVaultsClient,
18
22
  } from './vaults.js';
@@ -28,6 +32,10 @@ export type {
28
32
  UploadOptions,
29
33
  UploadFromUriOptions,
30
34
  ReplaceFromUriOptions,
35
+ CreateUploadOptions,
36
+ CreateUploadResult,
37
+ CreateDownloadLinkOptions,
38
+ CreateDownloadLinkResult,
31
39
  } from './ingots.js';
32
40
  export {
33
41
  MobileHealthClient,
@@ -44,6 +52,7 @@ export type {
44
52
  CreateSparkResponse,
45
53
  ListSparksOptions,
46
54
  ListSparksResponse,
55
+ ShareSparkOptions,
47
56
  Spark,
48
57
  SparkResponse,
49
58
  } from './sparks.js';
package/src/ingots.ts CHANGED
@@ -65,6 +65,49 @@ export interface DownloadToFileResult {
65
65
  sizeBytes: number;
66
66
  }
67
67
 
68
+ export interface AddInviteOptions {
69
+ /** Invite lifetime in seconds. Omit to inherit the link's existing expiry. */
70
+ expiresInSeconds?: number;
71
+ }
72
+
73
+ export interface CreateUploadOptions {
74
+ vaultId: string;
75
+ vat: string;
76
+ name: string;
77
+ contentType: string;
78
+ sizeBytes: number;
79
+ /**
80
+ * Slash-delimited folder path within the vault (e.g. "Accounts/001xx").
81
+ * Missing path segments are find-or-created. Omitted means the vault root.
82
+ */
83
+ folder?: string;
84
+ /**
85
+ * Existing folder id to upload into (e.g. "fld_xxx"). Use when the target
86
+ * folder is already known by id; mutually exclusive with `folder` (path).
87
+ * Omitted means the vault root.
88
+ */
89
+ folderId?: string;
90
+ }
91
+
92
+ export interface CreateUploadResult {
93
+ ingotId: string;
94
+ /** Forge tus endpoint plus upload-session token (ISTK) for the resumable upload. */
95
+ forgeUrl: string;
96
+ name?: string;
97
+ sizeBytes?: number;
98
+ }
99
+
100
+ export interface CreateDownloadLinkOptions {
101
+ vaultId: string;
102
+ ingotId: string;
103
+ vat: string;
104
+ }
105
+
106
+ export interface CreateDownloadLinkResult {
107
+ /** Short-lived signed download URL, validated against the allowed-host policy. */
108
+ downloadUrl: string;
109
+ }
110
+
68
111
  interface IngotUploadInitResponse {
69
112
  ingot_id: string;
70
113
  forge_url: string;
@@ -120,7 +163,7 @@ export class MobileIngotsClient {
120
163
  throw new SparkVaultMobileError('Upload cancelled', { code: 'upload_cancelled' });
121
164
  }
122
165
 
123
- const init = await this.createIngotUpload({
166
+ const init = await this.createUpload({
124
167
  vaultId: options.vaultId,
125
168
  vat: options.vat,
126
169
  name: options.name,
@@ -135,22 +178,59 @@ export class MobileIngotsClient {
135
178
  fileSize: options.fileSize,
136
179
  filename: options.name,
137
180
  contentType: options.contentType,
138
- forgeUrl: init.forge_url,
181
+ forgeUrl: init.forgeUrl,
139
182
  onProgress: options.onProgress,
140
183
  abortSignal: options.abortSignal,
141
184
  debug: options.debug,
142
185
  });
143
186
 
144
- await this.verifyIngotActive(options.vaultId, init.ingot_id, options.vat);
187
+ await this.verifyIngotActive(options.vaultId, init.ingotId, options.vat);
145
188
 
146
189
  return {
147
- ingot_id: init.ingot_id,
190
+ ingot_id: init.ingotId,
148
191
  name: init.name ?? options.name,
149
- size_bytes: init.size_bytes ?? options.fileSize,
192
+ size_bytes: init.sizeBytes ?? options.fileSize,
150
193
  storage_location: 's3',
151
194
  };
152
195
  }
153
196
 
197
+ /**
198
+ * Initialize an ingot record and obtain the Forge resumable-upload endpoint
199
+ * (with its upload-session token). The canonical first step of every upload;
200
+ * `uploadFromUri` calls this before streaming bytes via tus.
201
+ */
202
+ async createUpload(options: CreateUploadOptions): Promise<CreateUploadResult> {
203
+ validateVaultId(options.vaultId);
204
+ const body: Record<string, unknown> = {
205
+ name: options.name,
206
+ content_type: options.contentType,
207
+ size_bytes: options.sizeBytes,
208
+ };
209
+ // folder_path (find-or-create) and folder_id (existing folder) are
210
+ // mutually exclusive server-side; a path takes precedence when both given.
211
+ if (options.folder) {
212
+ body.folder_path = options.folder;
213
+ } else if (options.folderId) {
214
+ body.folder_id = options.folderId;
215
+ }
216
+ const response = await this.http.post<IngotUploadInitResponse>(
217
+ `/vaults/${options.vaultId}/ingots`,
218
+ body,
219
+ { vat: options.vat }
220
+ );
221
+
222
+ if (!response.data.ingot_id || !response.data.forge_url) {
223
+ throw new SparkVaultMobileError('Server returned invalid response for ingot creation');
224
+ }
225
+
226
+ return {
227
+ ingotId: response.data.ingot_id,
228
+ forgeUrl: response.data.forge_url,
229
+ name: response.data.name,
230
+ sizeBytes: response.data.size_bytes,
231
+ };
232
+ }
233
+
154
234
  async replaceFromUri(options: ReplaceFromUriOptions): Promise<UploadResult> {
155
235
  const { vaultId, ingotId, vat, name, contentType, fileUri, fileSize, onProgress, abortSignal, debug } = options;
156
236
  validateVaultId(vaultId);
@@ -289,11 +369,14 @@ export class MobileIngotsClient {
289
369
  await this.http.delete(`/vaults/${vaultId}/ingots/${ingotId}`, { vat });
290
370
  }
291
371
 
292
- private async getDownloadUrl(
293
- vaultId: string,
294
- ingotId: string,
295
- vat: string
296
- ): Promise<string> {
372
+ /**
373
+ * Mint a fresh signed download URL for an ingot, validated against the
374
+ * allowed-host policy. The canonical first step of every download;
375
+ * `downloadToFile` calls this once per attempt because signed URLs are
376
+ * short-lived.
377
+ */
378
+ async createDownloadLink(options: CreateDownloadLinkOptions): Promise<CreateDownloadLinkResult> {
379
+ const { vaultId, ingotId, vat } = options;
297
380
  validateVaultId(vaultId);
298
381
  validateIngotId(ingotId);
299
382
 
@@ -309,7 +392,7 @@ export class MobileIngotsClient {
309
392
  }
310
393
 
311
394
  this.validateDownloadUrl(downloadUrl);
312
- return downloadUrl;
395
+ return { downloadUrl };
313
396
  }
314
397
 
315
398
  async downloadToFile(options: DownloadToFileOptions): Promise<DownloadToFileResult> {
@@ -343,7 +426,7 @@ export class MobileIngotsClient {
343
426
  try {
344
427
  // Fresh signed URL on every attempt — server URLs are short-lived,
345
428
  // and retrying with a stale URL would just fail again.
346
- const downloadUrl = await this.getDownloadUrl(vaultId, ingotId, vat);
429
+ const { downloadUrl } = await this.createDownloadLink({ vaultId, ingotId, vat });
347
430
  const result = await fileDownloader.download(downloadUrl, fileUri, {
348
431
  timeoutMs: options.timeoutMs ?? this.config.fileTransferTimeoutMs,
349
432
  abortSignal,
@@ -424,13 +507,13 @@ export class MobileIngotsClient {
424
507
  ingotId: string,
425
508
  vat: string,
426
509
  identity: string,
427
- identityType: 'email' = 'email'
510
+ options: AddInviteOptions = {}
428
511
  ): Promise<IngotInvite> {
429
512
  validateVaultId(vaultId);
430
513
  validateIngotId(ingotId);
431
514
  const response = await this.http.post<IngotInvite>(
432
515
  `/vaults/${vaultId}/ingots/${ingotId}/sharing/invite`,
433
- { identity, identity_type: identityType },
516
+ { identity, expires_in_seconds: options.expiresInSeconds },
434
517
  { vat }
435
518
  );
436
519
  return response.data;
@@ -487,40 +570,6 @@ export class MobileIngotsClient {
487
570
  );
488
571
  }
489
572
 
490
- private async createIngotUpload(options: {
491
- vaultId: string;
492
- vat: string;
493
- name: string;
494
- contentType: string;
495
- sizeBytes: number;
496
- folder?: string;
497
- folderId?: string;
498
- }): Promise<IngotUploadInitResponse> {
499
- const body: Record<string, unknown> = {
500
- name: options.name,
501
- content_type: options.contentType,
502
- size_bytes: options.sizeBytes,
503
- };
504
- // folder_path (find-or-create) and folder_id (existing folder) are
505
- // mutually exclusive server-side; a path takes precedence when both given.
506
- if (options.folder) {
507
- body.folder_path = options.folder;
508
- } else if (options.folderId) {
509
- body.folder_id = options.folderId;
510
- }
511
- const response = await this.http.post<IngotUploadInitResponse>(
512
- `/vaults/${options.vaultId}/ingots`,
513
- body,
514
- { vat: options.vat }
515
- );
516
-
517
- if (!response.data.ingot_id || !response.data.forge_url) {
518
- throw new SparkVaultMobileError('Server returned invalid response for ingot creation');
519
- }
520
-
521
- return response.data;
522
- }
523
-
524
573
  private async verifyIngotActive(vaultId: string, ingotId: string, vat: string): Promise<Ingot> {
525
574
  let ingot: Ingot | null = null;
526
575