@sparkvault/sdk-mobile 1.0.5 → 3.1.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/README.md +6 -5
- package/dist/account.d.ts +31 -0
- package/dist/account.js +106 -0
- package/dist/account.js.map +1 -0
- package/dist/client.d.ts +4 -2
- package/dist/client.js +5 -3
- package/dist/client.js.map +1 -1
- package/dist/identity-dialog.js +7 -7
- package/dist/identity-dialog.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/ingots.d.ts +52 -3
- package/dist/ingots.js +48 -30
- package/dist/ingots.js.map +1 -1
- package/dist/{auth.d.ts → products/identity.d.ts} +13 -35
- package/dist/{auth.js → products/identity.js} +42 -160
- package/dist/products/identity.js.map +1 -0
- package/dist/products/index.d.ts +12 -0
- package/dist/products/index.js +13 -0
- package/dist/products/index.js.map +1 -0
- package/dist/sparks.d.ts +21 -0
- package/dist/sparks.js +30 -0
- package/dist/sparks.js.map +1 -1
- package/dist/types.d.ts +39 -10
- package/package.json +1 -1
- package/src/account.ts +152 -0
- package/src/client.ts +7 -4
- package/src/identity-dialog.tsx +7 -7
- package/src/index.ts +11 -2
- package/src/ingots.ts +97 -48
- package/src/{auth.ts → products/identity.ts} +50 -218
- package/src/products/index.ts +17 -0
- package/src/sparks.ts +49 -0
- package/src/types.ts +44 -11
- package/dist/auth.js.map +0 -1
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
|
package/dist/sparks.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sparks.js","sourceRoot":"","sources":["../src/sparks.ts"],"names":[],"mappings":"
|
|
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
|
-
*
|
|
225
|
-
*
|
|
226
|
-
*
|
|
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
|
+
/** Single-use lifecycle: 'active' → 'consumed' (opened once) | 'revoked'. */
|
|
533
|
+
status: string;
|
|
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
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 {
|
|
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
|
|
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.
|
|
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.
|
|
43
|
+
this.products.identity.preloadConfig();
|
|
41
44
|
}
|
|
42
45
|
}
|
|
43
46
|
|
package/src/identity-dialog.tsx
CHANGED
|
@@ -111,7 +111,7 @@ export function SparkVaultIdentityDialog({
|
|
|
111
111
|
setIdentity(initialIdentity ?? '');
|
|
112
112
|
setIdentityType(initialIdentityType ?? 'email');
|
|
113
113
|
|
|
114
|
-
client.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
15
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
187
|
+
await this.verifyIngotActive(options.vaultId, init.ingotId, options.vat);
|
|
145
188
|
|
|
146
189
|
return {
|
|
147
|
-
ingot_id: init.
|
|
190
|
+
ingot_id: init.ingotId,
|
|
148
191
|
name: init.name ?? options.name,
|
|
149
|
-
size_bytes: init.
|
|
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
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
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.
|
|
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
|
-
|
|
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,
|
|
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
|
|