@sparkvault/sdk-mobile 6.0.2 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/dist/account.d.ts +4 -3
- package/dist/account.js +16 -11
- package/dist/account.js.map +1 -1
- package/dist/app-sdk/email-address.d.ts +37 -0
- package/dist/app-sdk/email-address.js +91 -0
- package/dist/app-sdk/email-address.js.map +1 -0
- package/dist/app-sdk/encoding.d.ts +64 -0
- package/dist/app-sdk/encoding.js +245 -0
- package/dist/app-sdk/encoding.js.map +1 -0
- package/dist/app-sdk/entropy-contract.d.ts +33 -0
- package/dist/app-sdk/entropy-contract.js +51 -0
- package/dist/app-sdk/entropy-contract.js.map +1 -0
- package/dist/app-sdk/filename.d.ts +93 -0
- package/dist/app-sdk/filename.js +161 -0
- package/dist/app-sdk/filename.js.map +1 -0
- package/dist/app-sdk/forge-url.d.ts +107 -0
- package/dist/app-sdk/forge-url.js +222 -0
- package/dist/app-sdk/forge-url.js.map +1 -0
- package/dist/app-sdk/ids.d.ts +48 -0
- package/dist/app-sdk/ids.js +65 -0
- package/dist/app-sdk/ids.js.map +1 -0
- package/dist/app-sdk/ingot-access-log.d.ts +29 -0
- package/dist/app-sdk/ingot-access-log.js +49 -0
- package/dist/app-sdk/ingot-access-log.js.map +1 -0
- package/dist/app-sdk/phone.d.ts +37 -0
- package/dist/app-sdk/phone.js +134 -0
- package/dist/app-sdk/phone.js.map +1 -0
- package/dist/app-sdk/platform-domains.d.ts +41 -0
- package/dist/app-sdk/platform-domains.js +82 -0
- package/dist/app-sdk/platform-domains.js.map +1 -0
- package/dist/app-sdk/request-path.d.ts +49 -0
- package/dist/app-sdk/request-path.js +81 -0
- package/dist/app-sdk/request-path.js.map +1 -0
- package/dist/app-sdk/transient.d.ts +23 -0
- package/dist/app-sdk/transient.js +29 -0
- package/dist/app-sdk/transient.js.map +1 -0
- package/dist/app-sdk/vat-refusal.d.ts +66 -0
- package/dist/app-sdk/vat-refusal.js +69 -0
- package/dist/app-sdk/vat-refusal.js.map +1 -0
- package/dist/billing.d.ts +9 -17
- package/dist/billing.js +14 -17
- package/dist/billing.js.map +1 -1
- package/dist/config.d.ts +7 -1
- package/dist/config.js +10 -17
- package/dist/config.js.map +1 -1
- package/dist/encoding.d.ts +30 -4
- package/dist/encoding.js +89 -83
- package/dist/encoding.js.map +1 -1
- package/dist/entropy.d.ts +25 -4
- package/dist/entropy.js +62 -15
- package/dist/entropy.js.map +1 -1
- package/dist/entropy.test-d.d.ts +40 -0
- package/dist/entropy.test-d.js +12 -0
- package/dist/entropy.test-d.js.map +1 -0
- package/dist/errors.d.ts +67 -17
- package/dist/errors.js +70 -27
- package/dist/errors.js.map +1 -1
- package/dist/folders.js +8 -7
- package/dist/folders.js.map +1 -1
- package/dist/http.d.ts +57 -15
- package/dist/http.js +125 -138
- package/dist/http.js.map +1 -1
- package/dist/identity-dialog.js +4 -30
- package/dist/identity-dialog.js.map +1 -1
- package/dist/identity-input.d.ts +39 -0
- package/dist/identity-input.js +59 -0
- package/dist/identity-input.js.map +1 -0
- package/dist/index.d.ts +6 -5
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/ingots.d.ts +51 -18
- package/dist/ingots.js +132 -122
- package/dist/ingots.js.map +1 -1
- package/dist/mutex.d.ts +1 -2
- package/dist/mutex.js +0 -6
- package/dist/mutex.js.map +1 -1
- package/dist/notify.d.ts +7 -2
- package/dist/notify.js +12 -15
- package/dist/notify.js.map +1 -1
- package/dist/notify.test-d.d.ts +3 -2
- package/dist/notify.test-d.js +1 -1
- package/dist/products/identity.d.ts +5 -0
- package/dist/products/identity.js +40 -35
- package/dist/products/identity.js.map +1 -1
- package/dist/request-path.d.ts +32 -0
- package/dist/request-path.js +35 -0
- package/dist/request-path.js.map +1 -0
- package/dist/sparks.js +16 -15
- package/dist/sparks.js.map +1 -1
- package/dist/tus.d.ts +19 -2
- package/dist/tus.js +86 -35
- package/dist/tus.js.map +1 -1
- package/dist/types.d.ts +167 -15
- package/dist/users.d.ts +12 -5
- package/dist/users.js +27 -19
- package/dist/users.js.map +1 -1
- package/dist/validation.d.ts +20 -5
- package/dist/validation.js +29 -27
- package/dist/validation.js.map +1 -1
- package/dist/vaults.d.ts +34 -83
- package/dist/vaults.js +64 -44
- package/dist/vaults.js.map +1 -1
- package/dist/vaults.test-d.d.ts +22 -0
- package/dist/vaults.test-d.js +14 -0
- package/dist/vaults.test-d.js.map +1 -0
- package/package.json +4 -2
- package/src/account.ts +16 -11
- package/src/app-sdk/email-address.js +94 -0
- package/src/app-sdk/encoding.js +250 -0
- package/src/app-sdk/entropy-contract.js +56 -0
- package/src/app-sdk/filename.js +174 -0
- package/src/app-sdk/forge-url.js +224 -0
- package/src/app-sdk/ids.js +71 -0
- package/src/app-sdk/ingot-access-log.js +51 -0
- package/src/app-sdk/phone.js +139 -0
- package/src/app-sdk/platform-domains.js +99 -0
- package/src/app-sdk/request-path.js +81 -0
- package/src/app-sdk/transient.js +30 -0
- package/src/app-sdk/vat-refusal.js +74 -0
- package/src/billing.ts +16 -25
- package/src/config.ts +16 -23
- package/src/encoding.ts +91 -103
- package/src/entropy.test-d.ts +47 -0
- package/src/entropy.ts +86 -20
- package/src/errors.ts +93 -33
- package/src/folders.ts +8 -7
- package/src/http.ts +145 -150
- package/src/identity-dialog.tsx +4 -37
- package/src/identity-input.ts +70 -0
- package/src/index.ts +11 -3
- package/src/ingots.ts +173 -159
- package/src/mutex.ts +1 -8
- package/src/notify.test-d.ts +3 -1
- package/src/notify.ts +19 -17
- package/src/products/identity.ts +45 -39
- package/src/request-path.ts +45 -0
- package/src/sparks.ts +16 -13
- package/src/tus.ts +90 -37
- package/src/types.ts +165 -15
- package/src/users.ts +27 -17
- package/src/validation.ts +30 -24
- package/src/vaults.test-d.ts +29 -0
- package/src/vaults.ts +87 -153
- package/dist/platform-domains.generated.d.ts +0 -2
- package/dist/platform-domains.generated.js +0 -16
- package/dist/platform-domains.generated.js.map +0 -1
- package/src/platform-domains.generated.ts +0 -17
package/src/notify.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { MobileHttpClient } from './http.js';
|
|
2
|
+
import { apiPath } from './request-path.js';
|
|
3
|
+
import { validateResourceId } from './validation.js';
|
|
2
4
|
|
|
3
5
|
/** Inbox views the feed can be filtered to. `all` excludes archived rows. */
|
|
4
6
|
export type NotificationInboxState = 'all' | 'unseen' | 'unread' | 'archived';
|
|
@@ -34,10 +36,15 @@ export interface NotificationRow {
|
|
|
34
36
|
* The grant to open. Present only while a live reveal stands behind it — a
|
|
35
37
|
* SEALED (secure) send that has not been recalled. A cleartext send delivers its
|
|
36
38
|
* content inline in the channel and mints no grant; a recalled row's grant is
|
|
37
|
-
* revoked and the pointer is withheld rather than echoed.
|
|
38
|
-
* a URL from it.
|
|
39
|
+
* revoked and the pointer is withheld rather than echoed.
|
|
39
40
|
*/
|
|
40
41
|
sparklink_code?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Where the grant opens: the URL every Notify channel carries for it, built by
|
|
44
|
+
* the server. Present exactly when `sparklink_code` is; open this rather than
|
|
45
|
+
* building a URL from the code.
|
|
46
|
+
*/
|
|
47
|
+
pointer_url?: string;
|
|
41
48
|
/**
|
|
42
49
|
* True only when there is a secure part left to reveal — exactly when
|
|
43
50
|
* `sparklink_code` is present. `false` does NOT mean "cleartext row": a recalled
|
|
@@ -150,21 +157,15 @@ export class MobileNotifyClient {
|
|
|
150
157
|
|
|
151
158
|
/** The caller's own inbox feed (newest first). */
|
|
152
159
|
async getInbox(options: ListNotificationsOptions = {}): Promise<ListNotificationsResponse> {
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
if (options.cursor) params.set('cursor', options.cursor);
|
|
157
|
-
|
|
158
|
-
const query = params.toString();
|
|
159
|
-
const response = await this.http.get<ListNotificationsResponse>(
|
|
160
|
-
query ? `/me/notifications?${query}` : '/me/notifications'
|
|
161
|
-
);
|
|
160
|
+
const response = await this.http.get<ListNotificationsResponse>(apiPath`/me/notifications`, {
|
|
161
|
+
query: { state: options.state, limit: options.limit || undefined, cursor: options.cursor },
|
|
162
|
+
});
|
|
162
163
|
return response.data;
|
|
163
164
|
}
|
|
164
165
|
|
|
165
166
|
/** Bounded unseen badge count for the caller's own inbox. */
|
|
166
167
|
async getUnreadCount(): Promise<UnreadCountResponse> {
|
|
167
|
-
const response = await this.http.get<UnreadCountResponse>(
|
|
168
|
+
const response = await this.http.get<UnreadCountResponse>(apiPath`/me/notifications/unread-count`);
|
|
168
169
|
return response.data;
|
|
169
170
|
}
|
|
170
171
|
|
|
@@ -176,8 +177,9 @@ export class MobileNotifyClient {
|
|
|
176
177
|
notificationId: string,
|
|
177
178
|
options: MarkNotificationStateOptions
|
|
178
179
|
): Promise<MarkNotificationStateResponse> {
|
|
180
|
+
validateResourceId(notificationId, 'notification ID', 'notificationId');
|
|
179
181
|
const response = await this.http.post<MarkNotificationStateResponse>(
|
|
180
|
-
`/me/notifications/${
|
|
182
|
+
apiPath`/me/notifications/${notificationId}/state`,
|
|
181
183
|
{ created_at: options.createdAt, state: options.state }
|
|
182
184
|
);
|
|
183
185
|
return response.data;
|
|
@@ -198,7 +200,7 @@ export class MobileNotifyClient {
|
|
|
198
200
|
if (options.cursor) body.cursor = options.cursor;
|
|
199
201
|
|
|
200
202
|
const response = await this.http.post<MarkAllNotificationsStateResponse>(
|
|
201
|
-
|
|
203
|
+
apiPath`/me/notifications/state`,
|
|
202
204
|
body
|
|
203
205
|
);
|
|
204
206
|
return response.data;
|
|
@@ -232,7 +234,7 @@ export class MobileNotifyClient {
|
|
|
232
234
|
|
|
233
235
|
/** Register/refresh this device's Expo push token under the session user. */
|
|
234
236
|
async registerDevice(pushToken: string): Promise<RegisterDeviceResponse> {
|
|
235
|
-
const response = await this.http.post<RegisterDeviceResponse>(
|
|
237
|
+
const response = await this.http.post<RegisterDeviceResponse>(apiPath`/me/notifications/devices`, {
|
|
236
238
|
push_token: pushToken,
|
|
237
239
|
});
|
|
238
240
|
return response.data;
|
|
@@ -244,7 +246,7 @@ export class MobileNotifyClient {
|
|
|
244
246
|
* itself, so the caller never reimplements that derivation.
|
|
245
247
|
*/
|
|
246
248
|
async unregisterDeviceToken(pushToken: string): Promise<void> {
|
|
247
|
-
await this.http.delete(
|
|
249
|
+
await this.http.delete(apiPath`/me/notifications/devices`, { body: { token: pushToken } });
|
|
248
250
|
}
|
|
249
251
|
|
|
250
252
|
/**
|
|
@@ -252,6 +254,6 @@ export class MobileNotifyClient {
|
|
|
252
254
|
* Use `unregisterDeviceToken` when the caller holds the token itself.
|
|
253
255
|
*/
|
|
254
256
|
async unregisterDevice(tokenHash: string): Promise<void> {
|
|
255
|
-
await this.http.delete(`/me/notifications/devices/${
|
|
257
|
+
await this.http.delete(apiPath`/me/notifications/devices/${tokenHash}`);
|
|
256
258
|
}
|
|
257
259
|
}
|
package/src/products/identity.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { identityUrl, type ResolvedMobileConfig } from '../config.js';
|
|
2
2
|
import { base64UrlToBytes, base64UrlToUtf8, utf8ToBytes } from '../encoding.js';
|
|
3
3
|
import { SparkVaultAuthenticationError, SparkVaultMobileError, SparkVaultValidationError } from '../errors.js';
|
|
4
|
+
import { inferIdentityType, normalizeIdentifier } from '../identity-input.js';
|
|
5
|
+
import { apiPath, type ApiPath } from '../request-path.js';
|
|
4
6
|
import type {
|
|
5
7
|
ApiErrorBody,
|
|
6
8
|
BackupSendResponse,
|
|
@@ -105,7 +107,7 @@ export class MobileIdentityClient {
|
|
|
105
107
|
}
|
|
106
108
|
|
|
107
109
|
getJwksUri(): string {
|
|
108
|
-
return identityUrl(this.config,
|
|
110
|
+
return identityUrl(this.config, apiPath`/.well-known/jwks.json`);
|
|
109
111
|
}
|
|
110
112
|
|
|
111
113
|
async getConfig(): Promise<IdentityConfig> {
|
|
@@ -133,7 +135,7 @@ export class MobileIdentityClient {
|
|
|
133
135
|
const now = Date.now();
|
|
134
136
|
const expired = now - this.jwksCachedAt > JWKS_CACHE_TTL_MS;
|
|
135
137
|
if (!this.jwksCache || expired || options.forceRefresh) {
|
|
136
|
-
this.jwksCache = this.getIdentity<IdentityJwks>(
|
|
138
|
+
this.jwksCache = this.getIdentity<IdentityJwks>(apiPath`/.well-known/jwks.json`);
|
|
137
139
|
this.jwksCachedAt = now;
|
|
138
140
|
}
|
|
139
141
|
|
|
@@ -158,25 +160,18 @@ export class MobileIdentityClient {
|
|
|
158
160
|
throw new SparkVaultValidationError('Identity token is required', { field: 'token' });
|
|
159
161
|
}
|
|
160
162
|
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const [headerPart, payloadPart, signaturePart] = parts;
|
|
167
|
-
let header: JwtHeader;
|
|
168
|
-
let claims: IdentityTokenClaims;
|
|
169
|
-
|
|
163
|
+
const claims = this.decodeIdentityTokenClaims(token);
|
|
164
|
+
const [headerPart, payloadPart, signaturePart] = token.split('.');
|
|
165
|
+
let parsedHeader: unknown = null;
|
|
170
166
|
try {
|
|
171
|
-
|
|
172
|
-
const parsedClaims = JSON.parse(base64UrlToUtf8(payloadPart)) as unknown;
|
|
173
|
-
if (!this.isIdentityTokenClaims(parsedClaims)) {
|
|
174
|
-
throw new Error('Invalid claims');
|
|
175
|
-
}
|
|
176
|
-
claims = parsedClaims;
|
|
167
|
+
parsedHeader = JSON.parse(base64UrlToUtf8(headerPart));
|
|
177
168
|
} catch {
|
|
169
|
+
// Refused below with every other unusable header.
|
|
170
|
+
}
|
|
171
|
+
if (typeof parsedHeader !== 'object' || parsedHeader === null || Array.isArray(parsedHeader)) {
|
|
178
172
|
throw new SparkVaultValidationError('Invalid identity token payload');
|
|
179
173
|
}
|
|
174
|
+
const header = parsedHeader as JwtHeader;
|
|
180
175
|
|
|
181
176
|
if (header.alg !== 'EdDSA') {
|
|
182
177
|
throw new SparkVaultValidationError('Unsupported identity token algorithm', { alg: header.alg });
|
|
@@ -258,7 +253,7 @@ export class MobileIdentityClient {
|
|
|
258
253
|
kindling: string;
|
|
259
254
|
expires_at?: number;
|
|
260
255
|
retry_after?: number;
|
|
261
|
-
}>(
|
|
256
|
+
}>(apiPath`/otp/send`, {
|
|
262
257
|
recipient: identity,
|
|
263
258
|
method: request.method,
|
|
264
259
|
});
|
|
@@ -331,7 +326,7 @@ export class MobileIdentityClient {
|
|
|
331
326
|
* identity result. A wrong code throws.
|
|
332
327
|
*/
|
|
333
328
|
async submitSecondFactor(request: SubmitSecondFactorRequest): Promise<IdentityVerifyResult> {
|
|
334
|
-
const result = await this.postIdentity<IdentityVerificationResponse>(
|
|
329
|
+
const result = await this.postIdentity<IdentityVerificationResponse>(apiPath`/second-factor/verify`, {
|
|
335
330
|
ticket: request.ticket,
|
|
336
331
|
code: request.code,
|
|
337
332
|
});
|
|
@@ -367,7 +362,7 @@ export class MobileIdentityClient {
|
|
|
367
362
|
async checkPasskeyStatus(identity: string, identityType?: IdentityType): Promise<{ hasPasskey: boolean }> {
|
|
368
363
|
try {
|
|
369
364
|
const result = await this.postIdentity<{ has_passkey: boolean }>(
|
|
370
|
-
|
|
365
|
+
apiPath`/passkey/check`,
|
|
371
366
|
this.identityLookupBody(identity, identityType)
|
|
372
367
|
);
|
|
373
368
|
return { hasPasskey: Boolean(result.has_passkey) };
|
|
@@ -377,7 +372,7 @@ export class MobileIdentityClient {
|
|
|
377
372
|
}
|
|
378
373
|
|
|
379
374
|
async getPasskeyAuthOptions(_identity?: string, _identityType?: IdentityType): Promise<PasskeyAuthChallenge> {
|
|
380
|
-
return this.postIdentity<PasskeyAuthChallenge>(
|
|
375
|
+
return this.postIdentity<PasskeyAuthChallenge>(apiPath`/passkey/verify`, {});
|
|
381
376
|
}
|
|
382
377
|
|
|
383
378
|
async completePasskeyAuthentication(
|
|
@@ -385,7 +380,7 @@ export class MobileIdentityClient {
|
|
|
385
380
|
session: PasskeySession
|
|
386
381
|
): Promise<IdentityVerifyResult> {
|
|
387
382
|
const result = await this.postIdentity<IdentityPasskeyCompletionResponse>(
|
|
388
|
-
|
|
383
|
+
apiPath`/passkey/verify/complete`,
|
|
389
384
|
{ session, credential }
|
|
390
385
|
);
|
|
391
386
|
|
|
@@ -422,7 +417,7 @@ export class MobileIdentityClient {
|
|
|
422
417
|
const normalized = this.normalizeIdentity(identity, identityType);
|
|
423
418
|
const identityBody = identityType === 'phone' ? { phone: normalized } : { email: normalized };
|
|
424
419
|
|
|
425
|
-
return this.postIdentity<PasskeyRegisterChallenge>(
|
|
420
|
+
return this.postIdentity<PasskeyRegisterChallenge>(apiPath`/passkey/register`, {
|
|
426
421
|
...identityBody,
|
|
427
422
|
registration_token: params.registrationToken,
|
|
428
423
|
device_name: params.deviceName ?? this.config.userAgent,
|
|
@@ -434,7 +429,7 @@ export class MobileIdentityClient {
|
|
|
434
429
|
session: PasskeySession
|
|
435
430
|
): Promise<{ success: boolean; credential_id: string }> {
|
|
436
431
|
const result = await this.postIdentity<IdentityPasskeyCompletionResponse>(
|
|
437
|
-
|
|
432
|
+
apiPath`/passkey/register/complete`,
|
|
438
433
|
{ session, credential }
|
|
439
434
|
);
|
|
440
435
|
|
|
@@ -464,7 +459,7 @@ export class MobileIdentityClient {
|
|
|
464
459
|
* Authorized by the login's verification token. Best-effort.
|
|
465
460
|
*/
|
|
466
461
|
async dismissRecommendation(params: { verificationToken: string; key: RecommendationKey }): Promise<void> {
|
|
467
|
-
await this.postIdentity(
|
|
462
|
+
await this.postIdentity(apiPath`/recommendations/dismiss`, {
|
|
468
463
|
verification_token: params.verificationToken,
|
|
469
464
|
key: params.key
|
|
470
465
|
});
|
|
@@ -482,10 +477,10 @@ export class MobileIdentityClient {
|
|
|
482
477
|
value: string;
|
|
483
478
|
method?: 'email' | 'sms' | 'voice';
|
|
484
479
|
}): Promise<BackupSendResponse> {
|
|
485
|
-
return this.postIdentity<BackupSendResponse>(
|
|
480
|
+
return this.postIdentity<BackupSendResponse>(apiPath`/backup-verification/send`, {
|
|
486
481
|
verification_token: params.verificationToken,
|
|
487
482
|
type: params.type,
|
|
488
|
-
value: params.value,
|
|
483
|
+
value: this.normalizeIdentity(params.value, params.type),
|
|
489
484
|
...(params.method ? { method: params.method } : {}),
|
|
490
485
|
});
|
|
491
486
|
}
|
|
@@ -498,7 +493,7 @@ export class MobileIdentityClient {
|
|
|
498
493
|
sessionId: string;
|
|
499
494
|
code: string;
|
|
500
495
|
}): Promise<BackupVerifyResponse> {
|
|
501
|
-
return this.postIdentity<BackupVerifyResponse>(
|
|
496
|
+
return this.postIdentity<BackupVerifyResponse>(apiPath`/backup-verification/verify`, {
|
|
502
497
|
session_id: params.sessionId,
|
|
503
498
|
code: params.code,
|
|
504
499
|
});
|
|
@@ -506,10 +501,10 @@ export class MobileIdentityClient {
|
|
|
506
501
|
|
|
507
502
|
private async verifyOtpWithRetryErrors(request: VerifyOtpRequest): Promise<IdentityVerificationResponse> {
|
|
508
503
|
try {
|
|
509
|
-
return await this.postIdentity<IdentityVerificationResponse>(
|
|
504
|
+
return await this.postIdentity<IdentityVerificationResponse>(apiPath`/otp/verify`, {
|
|
510
505
|
kindling: request.kindling,
|
|
511
506
|
pin: request.pin,
|
|
512
|
-
recipient: request.recipient
|
|
507
|
+
recipient: this.normalizeIdentity(request.recipient, inferIdentityType(request.recipient)),
|
|
513
508
|
});
|
|
514
509
|
} catch (err) {
|
|
515
510
|
const apiError = err as SparkVaultMobileError & {
|
|
@@ -535,7 +530,7 @@ export class MobileIdentityClient {
|
|
|
535
530
|
}
|
|
536
531
|
|
|
537
532
|
private startConfigLoad(): Promise<IdentityConfig> {
|
|
538
|
-
const request = this.getIdentity<IdentityConfig>(
|
|
533
|
+
const request = this.getIdentity<IdentityConfig>(apiPath`/config`).catch((error: unknown) => {
|
|
539
534
|
this.configCache = null;
|
|
540
535
|
throw error;
|
|
541
536
|
});
|
|
@@ -582,7 +577,7 @@ export class MobileIdentityClient {
|
|
|
582
577
|
}
|
|
583
578
|
}
|
|
584
579
|
|
|
585
|
-
private async getIdentity<T>(path:
|
|
580
|
+
private async getIdentity<T>(path: ApiPath): Promise<T> {
|
|
586
581
|
const response = await this.config.fetch(identityUrl(this.config, path), {
|
|
587
582
|
method: 'GET',
|
|
588
583
|
headers: this.identityHeaders(),
|
|
@@ -605,7 +600,7 @@ export class MobileIdentityClient {
|
|
|
605
600
|
}
|
|
606
601
|
|
|
607
602
|
private async postIdentity<T>(
|
|
608
|
-
path:
|
|
603
|
+
path: ApiPath,
|
|
609
604
|
body: Record<string, unknown>,
|
|
610
605
|
options: { bearerToken?: string } = {}
|
|
611
606
|
): Promise<T> {
|
|
@@ -647,17 +642,28 @@ export class MobileIdentityClient {
|
|
|
647
642
|
}
|
|
648
643
|
|
|
649
644
|
private identityLookupBody(identity: string, identityType?: IdentityType): Record<string, string> {
|
|
650
|
-
const
|
|
651
|
-
const normalized = this.normalizeIdentity(identity,
|
|
652
|
-
return
|
|
645
|
+
const type = identityType ?? inferIdentityType(identity);
|
|
646
|
+
const normalized = this.normalizeIdentity(identity, type);
|
|
647
|
+
return type === 'phone' ? { phone: normalized } : { email: normalized };
|
|
653
648
|
}
|
|
654
649
|
|
|
650
|
+
/**
|
|
651
|
+
* An identity as the Identity Product keys it, read as the given type: the
|
|
652
|
+
* sign-in dialog's rule, so a number or address resolves to the same identity
|
|
653
|
+
* through every method.
|
|
654
|
+
*/
|
|
655
655
|
private normalizeIdentity(identity: string, identityType: IdentityType): string {
|
|
656
|
-
|
|
657
|
-
if (!trimmed) {
|
|
656
|
+
if (!identity.trim()) {
|
|
658
657
|
throw new SparkVaultValidationError('Identity is required', { field: 'identity' });
|
|
659
658
|
}
|
|
660
|
-
|
|
659
|
+
const normalized = normalizeIdentifier(identity, identityType);
|
|
660
|
+
if (normalized === null) {
|
|
661
|
+
throw new SparkVaultValidationError(
|
|
662
|
+
identityType === 'phone' ? 'Invalid phone number' : 'Invalid email address',
|
|
663
|
+
{ field: 'identity' }
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
return normalized;
|
|
661
667
|
}
|
|
662
668
|
|
|
663
669
|
private selectJwk(jwks: IdentityJwks, kid?: string): { x: string } {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Request paths: the one way a module names a SparkVault route.
|
|
3
|
+
*
|
|
4
|
+
* Every request the SDK sends carries the caller's credentials (the session's
|
|
5
|
+
* Bearer token, plus a Vault Access Token on vault data), so a value that
|
|
6
|
+
* smuggled '/' or a dot-segment into a path would re-target that credentialed
|
|
7
|
+
* request at another route. `apiPath` closes that by construction: each
|
|
8
|
+
* interpolated value becomes exactly one percent-encoded segment, and a value
|
|
9
|
+
* that cannot be one segment is refused before any request leaves. The
|
|
10
|
+
* transport accepts only an `ApiPath`, so no module can name a route any other
|
|
11
|
+
* way.
|
|
12
|
+
*
|
|
13
|
+
* The rules are the platform's one copy, mirrored from
|
|
14
|
+
* `@sparkvault/app-sdk/request-path` into `./app-sdk/request-path.js`; this
|
|
15
|
+
* module only types them and raises this SDK's error.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { buildRequestPath, encodePathSegment, queryString } from './app-sdk/request-path.js';
|
|
19
|
+
import { SparkVaultValidationError } from './errors.js';
|
|
20
|
+
|
|
21
|
+
declare const API_PATH: unique symbol;
|
|
22
|
+
|
|
23
|
+
/** A route path whose every interpolated value is exactly one encoded segment. */
|
|
24
|
+
export type ApiPath = string & { readonly [API_PATH]: true };
|
|
25
|
+
|
|
26
|
+
/** Query parameters; `undefined`, `null` and `''` are left out of the query. */
|
|
27
|
+
export type QueryParams = Record<string, string | number | boolean | null | undefined>;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Build a route path from a template: `apiPath\`/vaults/${vaultId}\``.
|
|
31
|
+
*
|
|
32
|
+
* @throws SparkVaultValidationError when an interpolated value cannot be
|
|
33
|
+
* exactly one path segment ('', '.', '..', a non-string, or malformed UTF-16).
|
|
34
|
+
*/
|
|
35
|
+
export function apiPath(literals: TemplateStringsArray, ...values: string[]): ApiPath {
|
|
36
|
+
const path = buildRequestPath(literals, values);
|
|
37
|
+
if (path === null) {
|
|
38
|
+
throw new SparkVaultValidationError('A path value must be exactly one path segment', {
|
|
39
|
+
received: values.find(value => encodePathSegment(value) === null),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
return path as ApiPath;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export { queryString };
|
package/src/sparks.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { MobileHttpClient } from './http.js';
|
|
2
2
|
import type { ShareSparkResponse, SparkShareStatus, UnshareSparkResponse } from './types.js';
|
|
3
|
+
import { apiPath } from './request-path.js';
|
|
4
|
+
import { validateResourceId } from './validation.js';
|
|
3
5
|
|
|
4
6
|
export interface Spark {
|
|
5
7
|
spark_id: string;
|
|
@@ -55,23 +57,20 @@ export class MobileSparksClient {
|
|
|
55
57
|
}
|
|
56
58
|
|
|
57
59
|
async list(options: ListSparksOptions = {}): Promise<ListSparksResponse> {
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
if (options.cursor) params.set('cursor', options.cursor);
|
|
62
|
-
|
|
63
|
-
const query = params.toString();
|
|
64
|
-
const response = await this.http.get<ListSparksResponse>(query ? `/sparks?${query}` : '/sparks');
|
|
60
|
+
const response = await this.http.get<ListSparksResponse>(apiPath`/sparks`, {
|
|
61
|
+
query: { kindling: options.kindling, limit: options.limit || undefined, cursor: options.cursor },
|
|
62
|
+
});
|
|
65
63
|
return response.data;
|
|
66
64
|
}
|
|
67
65
|
|
|
68
66
|
async get(sparkId: string): Promise<SparkResponse> {
|
|
69
|
-
|
|
67
|
+
validateResourceId(sparkId, 'spark ID', 'sparkId');
|
|
68
|
+
const response = await this.http.get<SparkResponse>(apiPath`/sparks/${sparkId}`);
|
|
70
69
|
return response.data;
|
|
71
70
|
}
|
|
72
71
|
|
|
73
72
|
async create(options: CreateSparkOptions): Promise<CreateSparkResponse> {
|
|
74
|
-
const response = await this.http.post<CreateSparkResponse>(
|
|
73
|
+
const response = await this.http.post<CreateSparkResponse>(apiPath`/sparks`, {
|
|
75
74
|
payload: options.payload,
|
|
76
75
|
content_type: options.contentType,
|
|
77
76
|
ttl_minutes: options.ttlMinutes,
|
|
@@ -83,7 +82,8 @@ export class MobileSparksClient {
|
|
|
83
82
|
|
|
84
83
|
/** Burn a spark before its natural expiry. An already-burned spark resolves (the server returns 204); a missing spark rejects with a 404. */
|
|
85
84
|
async delete(sparkId: string): Promise<void> {
|
|
86
|
-
|
|
85
|
+
validateResourceId(sparkId, 'spark ID', 'sparkId');
|
|
86
|
+
await this.http.delete(apiPath`/sparks/${sparkId}`);
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
/**
|
|
@@ -92,13 +92,14 @@ export class MobileSparksClient {
|
|
|
92
92
|
* shared.
|
|
93
93
|
*/
|
|
94
94
|
async share(sparkId: string, options: ShareSparkOptions = {}): Promise<ShareSparkResponse> {
|
|
95
|
+
validateResourceId(sparkId, 'spark ID', 'sparkId');
|
|
95
96
|
const body: Record<string, unknown> = {};
|
|
96
97
|
if (options.visibility) body.visibility = options.visibility;
|
|
97
98
|
if (options.invites) body.invites = options.invites;
|
|
98
99
|
if (options.expiresInSeconds !== undefined) body.expires_in_seconds = options.expiresInSeconds;
|
|
99
100
|
|
|
100
101
|
const response = await this.http.patch<ShareSparkResponse>(
|
|
101
|
-
`/sparks/${
|
|
102
|
+
apiPath`/sparks/${sparkId}/share`,
|
|
102
103
|
body
|
|
103
104
|
);
|
|
104
105
|
return response.data;
|
|
@@ -106,16 +107,18 @@ export class MobileSparksClient {
|
|
|
106
107
|
|
|
107
108
|
/** Current sharing status for a spark. `shared` is false when no SparkLink exists. */
|
|
108
109
|
async getShare(sparkId: string): Promise<SparkShareStatus> {
|
|
110
|
+
validateResourceId(sparkId, 'spark ID', 'sparkId');
|
|
109
111
|
const response = await this.http.get<SparkShareStatus>(
|
|
110
|
-
`/sparks/${
|
|
112
|
+
apiPath`/sparks/${sparkId}/share`
|
|
111
113
|
);
|
|
112
114
|
return response.data;
|
|
113
115
|
}
|
|
114
116
|
|
|
115
117
|
/** Revoke a spark's SparkLink. Resolves whether or not a link existed. */
|
|
116
118
|
async unshare(sparkId: string): Promise<UnshareSparkResponse> {
|
|
119
|
+
validateResourceId(sparkId, 'spark ID', 'sparkId');
|
|
117
120
|
const response = await this.http.delete<UnshareSparkResponse>(
|
|
118
|
-
`/sparks/${
|
|
121
|
+
apiPath`/sparks/${sparkId}/share`
|
|
119
122
|
);
|
|
120
123
|
return response.data;
|
|
121
124
|
}
|