@sparkvault/sdk-mobile 7.1.11 → 7.1.13
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 +2 -1
- package/dist/account.d.ts +0 -5
- package/dist/account.js +0 -9
- package/dist/account.js.map +1 -1
- package/dist/account.test-d.d.ts +26 -0
- package/dist/account.test-d.js +17 -0
- package/dist/account.test-d.js.map +1 -0
- package/dist/errors.d.ts +0 -3
- package/dist/errors.js +0 -7
- package/dist/errors.js.map +1 -1
- package/dist/http.d.ts +7 -13
- package/dist/http.js +7 -41
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/ingots.d.ts +17 -8
- package/dist/ingots.js +48 -26
- package/dist/ingots.js.map +1 -1
- package/dist/ingots.test-d.d.ts +11 -6
- package/dist/ingots.test-d.js +7 -4
- package/dist/ingots.test-d.js.map +1 -1
- package/dist/products/identity.d.ts +8 -5
- package/dist/products/identity.js +12 -29
- package/dist/products/identity.js.map +1 -1
- package/dist/sparks.d.ts +1 -2
- package/dist/sparks.js +0 -1
- package/dist/sparks.js.map +1 -1
- package/dist/tus.d.ts +0 -1
- package/dist/tus.js +1 -5
- package/dist/tus.js.map +1 -1
- package/dist/types.d.ts +27 -52
- package/dist/vaults.d.ts +1 -4
- package/dist/vaults.js +3 -3
- package/dist/vaults.js.map +1 -1
- package/dist/vaults.test-d.d.ts +7 -4
- package/dist/vaults.test-d.js +4 -2
- package/dist/vaults.test-d.js.map +1 -1
- package/package.json +1 -1
- package/src/account.test-d.ts +39 -0
- package/src/account.ts +0 -14
- package/src/errors.ts +0 -8
- package/src/http.ts +8 -50
- package/src/index.ts +0 -1
- package/src/ingots.test-d.ts +28 -5
- package/src/ingots.ts +56 -42
- package/src/products/identity.ts +16 -46
- package/src/sparks.ts +1 -3
- package/src/tus.ts +1 -6
- package/src/types.ts +27 -56
- package/src/vaults.test-d.ts +6 -3
- package/src/vaults.ts +4 -8
package/src/ingots.ts
CHANGED
|
@@ -121,8 +121,12 @@ export interface UploadFromUriOptions extends UploadOptions {
|
|
|
121
121
|
debug?: DebugLogger;
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
/**
|
|
125
|
-
|
|
124
|
+
/**
|
|
125
|
+
* A replace addresses its ingot by id: no name is placed and the ingot stays
|
|
126
|
+
* in its folder, so the create-only hooks and the folder target do not apply.
|
|
127
|
+
*/
|
|
128
|
+
export interface ReplaceFromUriOptions
|
|
129
|
+
extends Omit<UploadFromUriOptions, 'name' | 'folder' | 'folderId' | 'onNameConflict' | 'onIngotCreated'> {
|
|
126
130
|
ingotId: string;
|
|
127
131
|
/**
|
|
128
132
|
* A new name for the ingot, stored in the same request as the content.
|
|
@@ -195,11 +199,6 @@ export interface DownloadToFileResult {
|
|
|
195
199
|
sizeBytes: number;
|
|
196
200
|
}
|
|
197
201
|
|
|
198
|
-
export interface AddInviteOptions {
|
|
199
|
-
/** Invite lifetime in seconds. Omit to inherit the link's existing expiry. */
|
|
200
|
-
expiresInSeconds?: number;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
202
|
export interface CreateUploadOptions {
|
|
204
203
|
vaultId: string;
|
|
205
204
|
vat: string;
|
|
@@ -330,6 +329,22 @@ function publicationOf(ingot: Ingot): Pick<UploadResult, 'content_version'> {
|
|
|
330
329
|
return { content_version: ingot.content_version ?? null };
|
|
331
330
|
}
|
|
332
331
|
|
|
332
|
+
/** A response header's value by name, whatever case the adapter spelled it in; undefined when absent. */
|
|
333
|
+
function headerOf(headers: Record<string, string> | undefined, name: string): string | undefined {
|
|
334
|
+
return Object.entries(headers ?? {}).find(([header]) => header.toLowerCase() === name)?.[1];
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* The body length a response declared (`Content-Length`), or undefined when
|
|
339
|
+
* it declared none, or something that is not a non-negative whole number.
|
|
340
|
+
*/
|
|
341
|
+
function declaredLengthOf(headers: Record<string, string> | undefined): number | undefined {
|
|
342
|
+
const declared = headerOf(headers, 'content-length');
|
|
343
|
+
if (declared === undefined || !/^\d+$/.test(declared.trim())) return undefined;
|
|
344
|
+
const length = Number(declared);
|
|
345
|
+
return Number.isSafeInteger(length) ? length : undefined;
|
|
346
|
+
}
|
|
347
|
+
|
|
333
348
|
/**
|
|
334
349
|
* Forge's id for an upload: the last segment of its session URL, where
|
|
335
350
|
* resolveForgeSessionUrl placed the session Forge's create answered with.
|
|
@@ -411,7 +426,6 @@ export class MobileIngotsClient {
|
|
|
411
426
|
ingot_id: init.ingotId,
|
|
412
427
|
name,
|
|
413
428
|
size_bytes: init.sizeBytes ?? options.fileSize,
|
|
414
|
-
storage_location: 's3',
|
|
415
429
|
// A server that does not report it minted the row (the upsert is the
|
|
416
430
|
// only other outcome, and it always says so).
|
|
417
431
|
created,
|
|
@@ -464,7 +478,6 @@ export class MobileIngotsClient {
|
|
|
464
478
|
ingot_id: options.ingotId,
|
|
465
479
|
name: options.name,
|
|
466
480
|
size_bytes: options.fileSize,
|
|
467
|
-
storage_location: 's3',
|
|
468
481
|
created: false,
|
|
469
482
|
status: ingot.status,
|
|
470
483
|
...publicationOf(ingot),
|
|
@@ -600,7 +613,6 @@ export class MobileIngotsClient {
|
|
|
600
613
|
ingot_id: response.data.ingot_id,
|
|
601
614
|
name,
|
|
602
615
|
size_bytes: fileSize,
|
|
603
|
-
storage_location: 's3',
|
|
604
616
|
// A replace never mints an ingot: the row is the caller's existing one.
|
|
605
617
|
created: false,
|
|
606
618
|
status: ingot.status,
|
|
@@ -799,11 +811,11 @@ export class MobileIngotsClient {
|
|
|
799
811
|
result.uri,
|
|
800
812
|
fileUri,
|
|
801
813
|
result.sizeBytes,
|
|
802
|
-
expectedSizeBytes
|
|
814
|
+
expectedSizeBytes,
|
|
815
|
+
declaredLengthOf(result.headers)
|
|
803
816
|
);
|
|
804
817
|
// Bind cached metadata to the actual stream, never the earlier URL response.
|
|
805
|
-
const contentVersion =
|
|
806
|
-
.find(([name]) => name.toLowerCase() === 'x-content-version')?.[1] || null;
|
|
818
|
+
const contentVersion = headerOf(result.headers, 'x-content-version') || null;
|
|
807
819
|
if (options.expectedContentVersion && contentVersion && options.expectedContentVersion !== contentVersion) {
|
|
808
820
|
await this.deleteFileIfPresent(fileDownloader, verified.filePath);
|
|
809
821
|
throw new SparkVaultMobileError('File changed while downloading. Please try again.', {
|
|
@@ -868,18 +880,12 @@ export class MobileIngotsClient {
|
|
|
868
880
|
return response.data;
|
|
869
881
|
}
|
|
870
882
|
|
|
871
|
-
async addInvite(
|
|
872
|
-
vaultId: string,
|
|
873
|
-
ingotId: string,
|
|
874
|
-
vat: string,
|
|
875
|
-
identity: string,
|
|
876
|
-
options: AddInviteOptions = {}
|
|
877
|
-
): Promise<IngotInvite> {
|
|
883
|
+
async addInvite(vaultId: string, ingotId: string, vat: string, identity: string): Promise<IngotInvite> {
|
|
878
884
|
validateVaultId(vaultId);
|
|
879
885
|
validateIngotId(ingotId);
|
|
880
886
|
const response = await this.http.post<IngotInvite>(
|
|
881
887
|
apiPath`/vaults/${vaultId}/ingots/${ingotId}/sharing/invite`,
|
|
882
|
-
{ identity
|
|
888
|
+
{ identity },
|
|
883
889
|
{ vat }
|
|
884
890
|
);
|
|
885
891
|
return response.data;
|
|
@@ -925,19 +931,7 @@ export class MobileIngotsClient {
|
|
|
925
931
|
validateVaultId(vaultId);
|
|
926
932
|
validateIngotId(ingotId);
|
|
927
933
|
const ingot = await this.get(vaultId, ingotId, vat);
|
|
928
|
-
return {
|
|
929
|
-
total_downloads: ingot.access_count ?? 0,
|
|
930
|
-
last_accessed_at: ingot.last_accessed_at ?? ingot.accessed_at,
|
|
931
|
-
};
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
async purgeAccessLogs(vaultId: string, ingotId: string, vat: string): Promise<void> {
|
|
935
|
-
validateVaultId(vaultId);
|
|
936
|
-
validateIngotId(ingotId);
|
|
937
|
-
void vat;
|
|
938
|
-
throw new SparkVaultValidationError(
|
|
939
|
-
'Ingot audit logs are read-only. Configure vault access-log retention to expire old entries.'
|
|
940
|
-
);
|
|
934
|
+
return { total_downloads: ingot.access_count ?? 0 };
|
|
941
935
|
}
|
|
942
936
|
|
|
943
937
|
/**
|
|
@@ -992,12 +986,24 @@ export class MobileIngotsClient {
|
|
|
992
986
|
return ingot;
|
|
993
987
|
}
|
|
994
988
|
|
|
989
|
+
/**
|
|
990
|
+
* The staged file is the download only when its length is the file's. The
|
|
991
|
+
* caller's `expectedSizeBytes` (the ingot's `size_bytes`) is the length the
|
|
992
|
+
* vault lists, so a file of any other length is the wrong content for good
|
|
993
|
+
* (SparkVaultValidationError, never retried). Without it, the response's own
|
|
994
|
+
* `Content-Length` stands in: a body that ended cleanly short of it is a
|
|
995
|
+
* transfer some hop cut and reframed, not the file, so the file goes and the
|
|
996
|
+
* attempt fails as a transport failure does (`download_truncated`, retried
|
|
997
|
+
* on a fresh link). A response that declares no length can only be checked
|
|
998
|
+
* for being non-empty.
|
|
999
|
+
*/
|
|
995
1000
|
private async verifyDownloadedFile(
|
|
996
1001
|
fileDownloader: MobileFileDownloader,
|
|
997
1002
|
preferredUri: string,
|
|
998
1003
|
expectedUri: string,
|
|
999
1004
|
knownSizeBytes?: number,
|
|
1000
|
-
expectedSizeBytes?: number
|
|
1005
|
+
expectedSizeBytes?: number,
|
|
1006
|
+
declaredSizeBytes?: number
|
|
1001
1007
|
): Promise<DownloadToFileResult> {
|
|
1002
1008
|
const filePath = preferredUri || expectedUri;
|
|
1003
1009
|
const info = await fileDownloader.getInfo(filePath);
|
|
@@ -1011,14 +1017,22 @@ export class MobileIngotsClient {
|
|
|
1011
1017
|
throw new SparkVaultMobileError('Download failed - file is empty');
|
|
1012
1018
|
}
|
|
1013
1019
|
|
|
1014
|
-
if (expectedSizeBytes !== undefined && expectedSizeBytes > 0
|
|
1020
|
+
if (expectedSizeBytes !== undefined && expectedSizeBytes > 0) {
|
|
1021
|
+
if (sizeBytes !== expectedSizeBytes) {
|
|
1022
|
+
await this.deleteFileIfPresent(fileDownloader, filePath);
|
|
1023
|
+
// Deterministic server-content mismatch — retrying the same ingot will
|
|
1024
|
+
// yield the same bytes, so this must surface as a non-retryable validation
|
|
1025
|
+
// failure rather than a transient transport error.
|
|
1026
|
+
throw new SparkVaultValidationError('Download failed - file size did not match expected ingot size', {
|
|
1027
|
+
expectedSizeBytes,
|
|
1028
|
+
actualSizeBytes: sizeBytes,
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
} else if (declaredSizeBytes !== undefined && sizeBytes !== declaredSizeBytes) {
|
|
1015
1032
|
await this.deleteFileIfPresent(fileDownloader, filePath);
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
throw new SparkVaultValidationError('Download failed - file size did not match expected ingot size', {
|
|
1020
|
-
expectedSizeBytes,
|
|
1021
|
-
actualSizeBytes: sizeBytes,
|
|
1033
|
+
throw new SparkVaultMobileError('Download failed - the file is not the length the server declared', {
|
|
1034
|
+
code: 'download_truncated',
|
|
1035
|
+
details: { declaredSizeBytes, actualSizeBytes: sizeBytes },
|
|
1022
1036
|
});
|
|
1023
1037
|
}
|
|
1024
1038
|
|
package/src/products/identity.ts
CHANGED
|
@@ -20,9 +20,8 @@ import type {
|
|
|
20
20
|
PasskeyRegisterChallenge,
|
|
21
21
|
PasskeySession,
|
|
22
22
|
PasskeyStatus,
|
|
23
|
-
RequestPinRequest,
|
|
24
|
-
RequestPinResponse,
|
|
25
23
|
SendOtpRequest,
|
|
24
|
+
SendOtpResponse,
|
|
26
25
|
SparkVaultUser,
|
|
27
26
|
SubmitSecondFactorRequest,
|
|
28
27
|
VerifyOtpRequest,
|
|
@@ -127,10 +126,6 @@ export class MobileIdentityClient {
|
|
|
127
126
|
}
|
|
128
127
|
}
|
|
129
128
|
|
|
130
|
-
isConfigPreloaded(): boolean {
|
|
131
|
-
return this.configCache !== null;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
129
|
async getJwks(options: { forceRefresh?: boolean } = {}): Promise<IdentityJwks> {
|
|
135
130
|
const now = Date.now();
|
|
136
131
|
const expired = now - this.jwksCachedAt > JWKS_CACHE_TTL_MS;
|
|
@@ -247,31 +242,14 @@ export class MobileIdentityClient {
|
|
|
247
242
|
return key;
|
|
248
243
|
}
|
|
249
244
|
|
|
250
|
-
async sendOtp(request: SendOtpRequest): Promise<
|
|
245
|
+
async sendOtp(request: SendOtpRequest): Promise<SendOtpResponse> {
|
|
251
246
|
const identity = this.normalizeIdentity(request.identity, request.identityType);
|
|
252
|
-
const result = await this.postIdentity<{
|
|
253
|
-
kindling: string;
|
|
254
|
-
expires_at?: number;
|
|
255
|
-
retry_after?: number;
|
|
256
|
-
}>(apiPath`/otp/send`, {
|
|
247
|
+
const result = await this.postIdentity<SendOtpResponse>(apiPath`/otp/send`, {
|
|
257
248
|
recipient: identity,
|
|
258
249
|
method: request.method,
|
|
259
250
|
});
|
|
260
251
|
|
|
261
|
-
return {
|
|
262
|
-
kindling: result.kindling,
|
|
263
|
-
expires_at: result.expires_at ?? Math.floor(Date.now() / 1000) + 900,
|
|
264
|
-
retry_after: result.retry_after,
|
|
265
|
-
is_signup: false,
|
|
266
|
-
};
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
async requestPin(request: RequestPinRequest): Promise<RequestPinResponse> {
|
|
270
|
-
return this.sendOtp({
|
|
271
|
-
identity: request.email,
|
|
272
|
-
identityType: 'email',
|
|
273
|
-
method: 'email',
|
|
274
|
-
});
|
|
252
|
+
return { kindling: result.kindling, expires_at: result.expires_at };
|
|
275
253
|
}
|
|
276
254
|
|
|
277
255
|
async verifyOtp(request: VerifyOtpRequest): Promise<IdentityVerifyResult> {
|
|
@@ -371,7 +349,12 @@ export class MobileIdentityClient {
|
|
|
371
349
|
}
|
|
372
350
|
}
|
|
373
351
|
|
|
374
|
-
|
|
352
|
+
/**
|
|
353
|
+
* Start a discoverable passkey ceremony: the request names no identity, so
|
|
354
|
+
* the authenticator offers every passkey it holds for the Identity relying
|
|
355
|
+
* party, and the one chosen names its person (its user handle is the SVID).
|
|
356
|
+
*/
|
|
357
|
+
async getPasskeyAuthOptions(): Promise<PasskeyAuthChallenge> {
|
|
375
358
|
return this.postIdentity<PasskeyAuthChallenge>(apiPath`/passkey/verify`, {});
|
|
376
359
|
}
|
|
377
360
|
|
|
@@ -507,9 +490,7 @@ export class MobileIdentityClient {
|
|
|
507
490
|
recipient: this.normalizeIdentity(request.recipient, inferIdentityType(request.recipient)),
|
|
508
491
|
});
|
|
509
492
|
} catch (err) {
|
|
510
|
-
const apiError = err as SparkVaultMobileError
|
|
511
|
-
data?: { kindling?: string; expires_at?: number };
|
|
512
|
-
};
|
|
493
|
+
const apiError = err as SparkVaultMobileError;
|
|
513
494
|
const message = apiError.message || 'Verification failed';
|
|
514
495
|
const code = apiError.statusCode ?? apiError.code;
|
|
515
496
|
|
|
@@ -599,15 +580,10 @@ export class MobileIdentityClient {
|
|
|
599
580
|
return (data.data ?? data) as T;
|
|
600
581
|
}
|
|
601
582
|
|
|
602
|
-
private async postIdentity<T>(
|
|
603
|
-
path: ApiPath,
|
|
604
|
-
body: Record<string, unknown>,
|
|
605
|
-
options: { bearerToken?: string } = {}
|
|
606
|
-
): Promise<T> {
|
|
607
|
-
const headers = this.identityHeaders(options.bearerToken);
|
|
583
|
+
private async postIdentity<T>(path: ApiPath, body: Record<string, unknown>): Promise<T> {
|
|
608
584
|
const response = await this.config.fetch(identityUrl(this.config, path), {
|
|
609
585
|
method: 'POST',
|
|
610
|
-
headers,
|
|
586
|
+
headers: this.identityHeaders(),
|
|
611
587
|
body: JSON.stringify(body),
|
|
612
588
|
timeoutMs: this.config.timeoutMs,
|
|
613
589
|
});
|
|
@@ -615,30 +591,24 @@ export class MobileIdentityClient {
|
|
|
615
591
|
if (!response.ok) {
|
|
616
592
|
const errorBody = await response.json().catch(() => ({} as ApiErrorBody));
|
|
617
593
|
const message = errorBody.error?.message ?? errorBody.message ?? 'Identity verification failed';
|
|
618
|
-
|
|
594
|
+
throw new SparkVaultMobileError(message, {
|
|
619
595
|
code: errorBody.error?.code ?? response.status,
|
|
620
596
|
statusCode: response.status,
|
|
621
597
|
details: errorBody.error?.details,
|
|
622
598
|
meta: errorBody.meta,
|
|
623
599
|
});
|
|
624
|
-
(error as SparkVaultMobileError & { data?: unknown }).data = errorBody.data ?? errorBody;
|
|
625
|
-
throw error;
|
|
626
600
|
}
|
|
627
601
|
|
|
628
602
|
const data = await response.json();
|
|
629
603
|
return (data.data ?? data) as T;
|
|
630
604
|
}
|
|
631
605
|
|
|
632
|
-
private identityHeaders(
|
|
633
|
-
|
|
606
|
+
private identityHeaders(): Record<string, string> {
|
|
607
|
+
return {
|
|
634
608
|
'Content-Type': 'application/json',
|
|
635
609
|
Accept: 'application/json',
|
|
636
610
|
'User-Agent': this.config.userAgent,
|
|
637
611
|
};
|
|
638
|
-
if (bearerToken) {
|
|
639
|
-
headers.Authorization = `Bearer ${bearerToken}`;
|
|
640
|
-
}
|
|
641
|
-
return headers;
|
|
642
612
|
}
|
|
643
613
|
|
|
644
614
|
private identityLookupBody(identity: string, identityType?: IdentityType): Record<string, string> {
|
package/src/sparks.ts
CHANGED
|
@@ -3,10 +3,10 @@ import type { ShareSparkResponse, SparkShareStatus, UnshareSparkResponse } from
|
|
|
3
3
|
import { apiPath } from './request-path.js';
|
|
4
4
|
import { validateResourceId } from './validation.js';
|
|
5
5
|
|
|
6
|
+
/** A Spark as GET /sparks lists it: never its payload, which only `get` reads. */
|
|
6
7
|
export interface Spark {
|
|
7
8
|
spark_id: string;
|
|
8
9
|
status: string;
|
|
9
|
-
payload?: string;
|
|
10
10
|
content_type?: string;
|
|
11
11
|
}
|
|
12
12
|
|
|
@@ -37,7 +37,6 @@ export interface CreateSparkOptions {
|
|
|
37
37
|
*/
|
|
38
38
|
ttlMinutes?: number;
|
|
39
39
|
kindling?: string;
|
|
40
|
-
name?: string;
|
|
41
40
|
}
|
|
42
41
|
|
|
43
42
|
export interface CreateSparkResponse {
|
|
@@ -79,7 +78,6 @@ export class MobileSparksClient {
|
|
|
79
78
|
content_type: options.contentType,
|
|
80
79
|
ttl_minutes: options.ttlMinutes,
|
|
81
80
|
kindling: options.kindling,
|
|
82
|
-
name: options.name,
|
|
83
81
|
});
|
|
84
82
|
return response.data;
|
|
85
83
|
}
|
package/src/tus.ts
CHANGED
|
@@ -37,7 +37,6 @@ import type {
|
|
|
37
37
|
} from './types.js';
|
|
38
38
|
|
|
39
39
|
export interface ParsedForgeUrl {
|
|
40
|
-
baseUrl: string;
|
|
41
40
|
tusEndpoint: string;
|
|
42
41
|
istk: string;
|
|
43
42
|
}
|
|
@@ -169,11 +168,7 @@ export function parseForgeUrl(
|
|
|
169
168
|
reason: read.reason,
|
|
170
169
|
});
|
|
171
170
|
}
|
|
172
|
-
return {
|
|
173
|
-
baseUrl: read.origin,
|
|
174
|
-
tusEndpoint: read.endpoint,
|
|
175
|
-
istk: read.istk,
|
|
176
|
-
};
|
|
171
|
+
return { tusEndpoint: read.endpoint, istk: read.istk };
|
|
177
172
|
}
|
|
178
173
|
|
|
179
174
|
/**
|
package/src/types.ts
CHANGED
|
@@ -48,16 +48,9 @@ export interface ApiErrorBody {
|
|
|
48
48
|
details?: Record<string, unknown>;
|
|
49
49
|
};
|
|
50
50
|
message?: string;
|
|
51
|
-
data?: unknown;
|
|
52
51
|
meta?: ApiMeta;
|
|
53
52
|
}
|
|
54
53
|
|
|
55
|
-
export interface RateLimitInfo {
|
|
56
|
-
limit: number;
|
|
57
|
-
remaining: number;
|
|
58
|
-
reset: number;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
54
|
/** Account-scoped role. Owners and admins manage the organization and team. */
|
|
62
55
|
export type OrgRole = 'owner' | 'admin' | 'user';
|
|
63
56
|
|
|
@@ -282,7 +275,6 @@ export interface IdentityBranding {
|
|
|
282
275
|
companyName: string | null;
|
|
283
276
|
logoLight: string | null;
|
|
284
277
|
logoDark: string | null;
|
|
285
|
-
backdrop_blur?: boolean;
|
|
286
278
|
themeMode?: 'light' | 'dark';
|
|
287
279
|
}
|
|
288
280
|
|
|
@@ -380,52 +372,37 @@ export interface VerifyOtpRequest {
|
|
|
380
372
|
recipient: string;
|
|
381
373
|
}
|
|
382
374
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
organization_name?: string;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
export interface RequestPinResponse {
|
|
375
|
+
/** A code sent: the kindling its verify names, and when the code expires (Unix seconds). */
|
|
376
|
+
export interface SendOtpResponse {
|
|
389
377
|
kindling: string;
|
|
390
378
|
expires_at: number;
|
|
391
|
-
retry_after?: number;
|
|
392
|
-
is_signup: boolean;
|
|
393
379
|
}
|
|
394
380
|
|
|
395
381
|
export interface PinVerifyLoginResponse {
|
|
396
382
|
access_token: string;
|
|
397
383
|
refresh_token: string;
|
|
398
|
-
token_type: 'Bearer';
|
|
399
|
-
expires_in: number;
|
|
400
384
|
user: SparkVaultUser;
|
|
401
385
|
}
|
|
402
386
|
|
|
387
|
+
/**
|
|
388
|
+
* A verified identity with no SparkVault user yet: `signup_token` (an
|
|
389
|
+
* identity-only access token) authorizes `account.completeSignup`.
|
|
390
|
+
*/
|
|
403
391
|
export interface PinVerifySignupPendingResponse {
|
|
404
392
|
verified: true;
|
|
405
393
|
needs_signup_info: true;
|
|
406
394
|
signup_token: string;
|
|
407
|
-
expires_at: number;
|
|
408
395
|
}
|
|
409
396
|
|
|
410
397
|
/**
|
|
411
|
-
*
|
|
412
|
-
* the
|
|
413
|
-
* `
|
|
414
|
-
*
|
|
398
|
+
* The Core exchange of a verified Identity token. A second factor is collected
|
|
399
|
+
* at the Identity Product before its token exists (`products.identity.verifyOtp`
|
|
400
|
+
* throws for it, and `submitSecondFactor` finishes it), so the exchange is a
|
|
401
|
+
* login or a pending signup, never a second-factor challenge.
|
|
415
402
|
*/
|
|
416
|
-
export interface PinVerifySecondFactorPendingResponse {
|
|
417
|
-
verified: false;
|
|
418
|
-
second_factor_required: true;
|
|
419
|
-
/** Opaque pending-login handle to submit the second factor against. */
|
|
420
|
-
ticket: string;
|
|
421
|
-
/** Methods the user may use, e.g. ['authenticator','recovery_code']. */
|
|
422
|
-
methods: string[];
|
|
423
|
-
}
|
|
424
|
-
|
|
425
403
|
export type PinVerifyResponse =
|
|
426
404
|
| PinVerifyLoginResponse
|
|
427
|
-
| PinVerifySignupPendingResponse
|
|
428
|
-
| PinVerifySecondFactorPendingResponse;
|
|
405
|
+
| PinVerifySignupPendingResponse;
|
|
429
406
|
|
|
430
407
|
export interface SubmitSecondFactorRequest {
|
|
431
408
|
/** Pending-login ticket from a second_factor_required response. */
|
|
@@ -442,8 +419,6 @@ export interface CompleteSignupRequest {
|
|
|
442
419
|
export interface CompleteSignupResponse {
|
|
443
420
|
access_token: string;
|
|
444
421
|
refresh_token: string;
|
|
445
|
-
token_type?: 'Bearer';
|
|
446
|
-
expires_in?: number;
|
|
447
422
|
user: SparkVaultUser;
|
|
448
423
|
account: SparkVaultAccount;
|
|
449
424
|
}
|
|
@@ -454,10 +429,14 @@ export interface PasskeyAuthOptions {
|
|
|
454
429
|
challenge: string;
|
|
455
430
|
timeout: number;
|
|
456
431
|
rpId: string;
|
|
432
|
+
/**
|
|
433
|
+
* The person's credentials when the ceremony is scoped to an identifier:
|
|
434
|
+
* ids only, since a transport list would hand an unauthenticated probe a
|
|
435
|
+
* device fingerprint.
|
|
436
|
+
*/
|
|
457
437
|
allowCredentials?: Array<{
|
|
458
438
|
id: string;
|
|
459
439
|
type: 'public-key';
|
|
460
|
-
transports?: string[];
|
|
461
440
|
}>;
|
|
462
441
|
userVerification: 'required' | 'preferred' | 'discouraged';
|
|
463
442
|
}
|
|
@@ -523,15 +502,15 @@ export interface PasskeyCredential {
|
|
|
523
502
|
|
|
524
503
|
export interface PasskeyStatus {
|
|
525
504
|
has_passkey: boolean;
|
|
526
|
-
registered_at?: number;
|
|
527
|
-
last_used_at?: number;
|
|
528
505
|
}
|
|
529
506
|
|
|
507
|
+
/**
|
|
508
|
+
* POST /auth/identity/verify: a session for the verified identity's member, or,
|
|
509
|
+
* with `user` and `account` null, an identity-only token for a signup.
|
|
510
|
+
*/
|
|
530
511
|
export interface IdentitySessionResponse {
|
|
531
512
|
access_token: string;
|
|
532
513
|
refresh_token: string;
|
|
533
|
-
token_type?: 'Bearer';
|
|
534
|
-
expires_in?: number;
|
|
535
514
|
user: SparkVaultUser | null;
|
|
536
515
|
account: SparkVaultAccount | null;
|
|
537
516
|
}
|
|
@@ -549,18 +528,16 @@ export interface Vault {
|
|
|
549
528
|
unseal_count?: number;
|
|
550
529
|
}
|
|
551
530
|
|
|
531
|
+
/** A vault is created with its name alone; a description is set with `update`. */
|
|
552
532
|
export interface CreateVaultRequest {
|
|
553
533
|
name: string;
|
|
554
|
-
description?: string;
|
|
555
534
|
}
|
|
556
535
|
|
|
557
536
|
export interface CreateVaultResponse {
|
|
558
537
|
vault_id: string;
|
|
559
538
|
name: string;
|
|
560
|
-
description?: string;
|
|
561
539
|
vmk: string;
|
|
562
540
|
vmk_warning: string;
|
|
563
|
-
status: 'sealed';
|
|
564
541
|
created_at: number;
|
|
565
542
|
}
|
|
566
543
|
|
|
@@ -700,7 +677,6 @@ export interface Ingot {
|
|
|
700
677
|
name: string;
|
|
701
678
|
content_type: string;
|
|
702
679
|
size_bytes: number;
|
|
703
|
-
storage_location: 'dynamodb' | 's3';
|
|
704
680
|
/** `uploading` until Forge activates the upload, then `active`. */
|
|
705
681
|
status?: 'uploading' | 'active';
|
|
706
682
|
folder_id?: string | null;
|
|
@@ -712,10 +688,7 @@ export interface Ingot {
|
|
|
712
688
|
*/
|
|
713
689
|
managed_by?: string | null;
|
|
714
690
|
created_at: number;
|
|
715
|
-
accessed_at?: number;
|
|
716
|
-
last_accessed_at?: number;
|
|
717
691
|
access_count?: number;
|
|
718
|
-
expires_at?: number;
|
|
719
692
|
/**
|
|
720
693
|
* Client-computed, vault-keyed HMAC of the plaintext (43-char base64url),
|
|
721
694
|
* or null when the uploader did not supply one. Used for upload dedup and
|
|
@@ -736,9 +709,14 @@ export interface HashCheckResponse {
|
|
|
736
709
|
found: HashCheckMatch[];
|
|
737
710
|
}
|
|
738
711
|
|
|
712
|
+
/**
|
|
713
|
+
* One page of GET /vaults/:id/ingots or /ingots/search. `count` is this
|
|
714
|
+
* page's length: the server reports no total.
|
|
715
|
+
*/
|
|
739
716
|
export interface ListIngotsResponse {
|
|
740
717
|
ingots: Ingot[];
|
|
741
|
-
|
|
718
|
+
count: number;
|
|
719
|
+
has_more: boolean;
|
|
742
720
|
next_cursor?: string;
|
|
743
721
|
}
|
|
744
722
|
|
|
@@ -762,7 +740,6 @@ export interface UploadResult {
|
|
|
762
740
|
*/
|
|
763
741
|
name: string;
|
|
764
742
|
size_bytes: number;
|
|
765
|
-
storage_location: 's3';
|
|
766
743
|
/**
|
|
767
744
|
* Whether this upload minted the ingot (true) or went into an existing row
|
|
768
745
|
* (false): the one holding the name, as the create's name-conflict policy
|
|
@@ -842,8 +819,6 @@ export interface IngotAccessLog {
|
|
|
842
819
|
|
|
843
820
|
export interface IngotAccessStats {
|
|
844
821
|
total_downloads: number;
|
|
845
|
-
unique_visitors?: number;
|
|
846
|
-
last_accessed_at?: number;
|
|
847
822
|
}
|
|
848
823
|
|
|
849
824
|
/** A folder as every folder route answers it (Core's FolderRepository.toPublicFormat). */
|
|
@@ -886,16 +861,13 @@ export interface TokenStorageAdapter {
|
|
|
886
861
|
clearTokens(): Promise<void>;
|
|
887
862
|
getUser?<T = SparkVaultUser>(): Promise<T | null>;
|
|
888
863
|
setUser?<T = SparkVaultUser>(user: T): Promise<void>;
|
|
889
|
-
getAccount?<T = SparkVaultAccount>(): Promise<T | null>;
|
|
890
864
|
setAccount?<T = SparkVaultAccount>(account: T): Promise<void>;
|
|
891
|
-
clearAll?(): Promise<void>;
|
|
892
865
|
}
|
|
893
866
|
|
|
894
867
|
export type FetchLike = (
|
|
895
868
|
url: string,
|
|
896
869
|
options?: RequestInit & {
|
|
897
870
|
timeoutMs?: number;
|
|
898
|
-
responseType?: XMLHttpRequestResponseType;
|
|
899
871
|
}
|
|
900
872
|
) => Promise<Response>;
|
|
901
873
|
|
|
@@ -1106,7 +1078,6 @@ export interface MobileFileDownloader {
|
|
|
1106
1078
|
|
|
1107
1079
|
export interface DebugLogger {
|
|
1108
1080
|
log(message: string): void;
|
|
1109
|
-
warn(message: string): void;
|
|
1110
1081
|
error(message: string): void;
|
|
1111
1082
|
success(message: string): void;
|
|
1112
1083
|
}
|
package/src/vaults.test-d.ts
CHANGED
|
@@ -7,11 +7,13 @@
|
|
|
7
7
|
* `*.test.ts`, which tsconfig excludes.
|
|
8
8
|
*
|
|
9
9
|
* Mirrored server sources: apps/api/src/folders/list.js (listFolders answers
|
|
10
|
-
* `{ folders }`, the whole list and nothing else)
|
|
11
|
-
* (one page: `{ vaults, count, has_more, next_cursor? }`)
|
|
10
|
+
* `{ folders }`, the whole list and nothing else), apps/api/src/vaults/list.js
|
|
11
|
+
* (one page: `{ vaults, count, has_more, next_cursor? }`) and
|
|
12
|
+
* apps/api/src/vaults/create.js (createVault answers `{ vault_id, name, vmk,
|
|
13
|
+
* vmk_warning, created_at }`).
|
|
12
14
|
*/
|
|
13
15
|
|
|
14
|
-
import type { ListFoldersResponse, ListVaultsResponse } from './types.js';
|
|
16
|
+
import type { CreateVaultResponse, ListFoldersResponse, ListVaultsResponse } from './types.js';
|
|
15
17
|
|
|
16
18
|
/** Fails to compile unless `T` resolves to exactly `true`. */
|
|
17
19
|
type Assert<T extends true> = T;
|
|
@@ -26,4 +28,5 @@ export type VaultsWireContract = [
|
|
|
26
28
|
// never sends would read as undefined wherever a caller trusted it.
|
|
27
29
|
Assert<Equals<keyof ListFoldersResponse, 'folders'>>,
|
|
28
30
|
Assert<Equals<keyof ListVaultsResponse, 'vaults' | 'count' | 'has_more' | 'next_cursor'>>,
|
|
31
|
+
Assert<Equals<keyof CreateVaultResponse, 'vault_id' | 'name' | 'vmk' | 'vmk_warning' | 'created_at'>>,
|
|
29
32
|
];
|
package/src/vaults.ts
CHANGED
|
@@ -41,13 +41,9 @@ export class MobileVaultsClient {
|
|
|
41
41
|
return response.data;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
async list(options?: {
|
|
45
|
-
status?: 'sealed' | 'unsealed';
|
|
46
|
-
limit?: number;
|
|
47
|
-
cursor?: string;
|
|
48
|
-
}): Promise<ListVaultsResponse> {
|
|
44
|
+
async list(options?: { limit?: number; cursor?: string }): Promise<ListVaultsResponse> {
|
|
49
45
|
const response = await this.http.get<ListVaultsResponse>(apiPath`/vaults`, {
|
|
50
|
-
query: {
|
|
46
|
+
query: { limit: options?.limit || undefined, cursor: options?.cursor },
|
|
51
47
|
});
|
|
52
48
|
return response.data;
|
|
53
49
|
}
|
|
@@ -58,11 +54,11 @@ export class MobileVaultsClient {
|
|
|
58
54
|
* filtered down to nothing still carries a cursor. A failed page fails the
|
|
59
55
|
* listing: a vault picker must never show a silently partial account.
|
|
60
56
|
*/
|
|
61
|
-
async listAll(
|
|
57
|
+
async listAll(): Promise<Vault[]> {
|
|
62
58
|
const vaults: Vault[] = [];
|
|
63
59
|
let cursor: string | undefined;
|
|
64
60
|
do {
|
|
65
|
-
const page = await this.list({
|
|
61
|
+
const page = await this.list({ limit: VAULT_LIST_PAGE_LIMIT, cursor });
|
|
66
62
|
vaults.push(...page.vaults);
|
|
67
63
|
cursor = page.has_more ? page.next_cursor : undefined;
|
|
68
64
|
} while (cursor);
|