@sparkvault/sdk 1.24.0 → 1.24.2
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/config.d.ts +19 -0
- package/dist/health.d.ts +18 -0
- package/dist/http.d.ts +3 -0
- package/dist/identity/api.d.ts +13 -8
- package/dist/index.d.ts +65 -11
- package/dist/sparkvault.cjs.js +291 -53
- package/dist/sparkvault.cjs.js.map +1 -1
- package/dist/sparkvault.esm.js +291 -53
- package/dist/sparkvault.esm.js.map +1 -1
- package/dist/sparkvault.js +1 -1
- package/dist/sparkvault.js.map +1 -1
- package/dist/vaults/index.d.ts +5 -3
- package/dist/vaults/types.d.ts +28 -16
- package/package.json +1 -1
package/dist/sparkvault.esm.js
CHANGED
|
@@ -66,14 +66,25 @@ class PopupBlockedError extends SparkVaultError {
|
|
|
66
66
|
*/
|
|
67
67
|
const API_URL = 'https://api.sparkvault.com';
|
|
68
68
|
const IDENTITY_URL = 'https://api.sparkvault.com/v1/apps/identity';
|
|
69
|
+
function normalizeApiBaseUrl(url) {
|
|
70
|
+
const trimmed = url.replace(/\/+$/, '');
|
|
71
|
+
return trimmed.endsWith('/v1') ? trimmed.slice(0, -3) : trimmed;
|
|
72
|
+
}
|
|
73
|
+
function normalizeBaseUrl(url) {
|
|
74
|
+
return url.replace(/\/+$/, '');
|
|
75
|
+
}
|
|
69
76
|
function resolveConfig(config) {
|
|
70
77
|
return {
|
|
71
78
|
accountId: config.accountId,
|
|
72
79
|
timeout: config.timeout ?? 30000,
|
|
73
|
-
apiBaseUrl: API_URL,
|
|
74
|
-
identityBaseUrl: IDENTITY_URL,
|
|
80
|
+
apiBaseUrl: normalizeApiBaseUrl(config.apiBaseUrl ?? API_URL),
|
|
81
|
+
identityBaseUrl: normalizeBaseUrl(config.identityBaseUrl ?? IDENTITY_URL),
|
|
82
|
+
accessToken: config.accessToken,
|
|
83
|
+
getAccessToken: config.getAccessToken,
|
|
84
|
+
apiKey: config.apiKey,
|
|
75
85
|
preloadConfig: config.preloadConfig !== false, // Default: true
|
|
76
86
|
backdropBlur: config.backdropBlur !== false, // Default: true
|
|
87
|
+
allowedDownloadHostPatterns: config.allowedDownloadHostPatterns,
|
|
77
88
|
};
|
|
78
89
|
}
|
|
79
90
|
function validateConfig(config) {
|
|
@@ -190,6 +201,31 @@ class HttpClient {
|
|
|
190
201
|
constructor(config) {
|
|
191
202
|
this.config = config;
|
|
192
203
|
}
|
|
204
|
+
async getDefaultHeaders(headers, hasJsonBody) {
|
|
205
|
+
const requestHeaders = {
|
|
206
|
+
Accept: 'application/json',
|
|
207
|
+
...headers,
|
|
208
|
+
};
|
|
209
|
+
if (hasJsonBody) {
|
|
210
|
+
requestHeaders['Content-Type'] = 'application/json';
|
|
211
|
+
}
|
|
212
|
+
if (!requestHeaders.Authorization) {
|
|
213
|
+
const token = await this.getAccessToken();
|
|
214
|
+
if (token) {
|
|
215
|
+
requestHeaders.Authorization = `Bearer ${token}`;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (this.config.apiKey && !requestHeaders['X-API-Key']) {
|
|
219
|
+
requestHeaders['X-API-Key'] = this.config.apiKey;
|
|
220
|
+
}
|
|
221
|
+
return requestHeaders;
|
|
222
|
+
}
|
|
223
|
+
async getAccessToken() {
|
|
224
|
+
if (this.config.getAccessToken) {
|
|
225
|
+
return this.config.getAccessToken();
|
|
226
|
+
}
|
|
227
|
+
return this.config.accessToken ?? null;
|
|
228
|
+
}
|
|
193
229
|
async request(path, options = {}) {
|
|
194
230
|
const { retry } = options;
|
|
195
231
|
// Per CLAUDE.md §7: Wrap with retry logic if enabled
|
|
@@ -209,11 +245,7 @@ class HttpClient {
|
|
|
209
245
|
async executeRequest(path, options) {
|
|
210
246
|
const { method = 'GET', headers = {}, body, timeout = this.config.timeout, } = options;
|
|
211
247
|
const url = `${this.config.apiBaseUrl}${path}`;
|
|
212
|
-
const requestHeaders =
|
|
213
|
-
'Content-Type': 'application/json',
|
|
214
|
-
Accept: 'application/json',
|
|
215
|
-
...headers,
|
|
216
|
-
};
|
|
248
|
+
const requestHeaders = await this.getDefaultHeaders(headers, body !== undefined);
|
|
217
249
|
const controller = new AbortController();
|
|
218
250
|
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
219
251
|
try {
|
|
@@ -224,12 +256,12 @@ class HttpClient {
|
|
|
224
256
|
signal: controller.signal,
|
|
225
257
|
});
|
|
226
258
|
clearTimeout(timeoutId);
|
|
227
|
-
const
|
|
259
|
+
const parsed = await this.parseResponse(response);
|
|
228
260
|
if (!response.ok) {
|
|
229
|
-
throw this.createErrorFromResponse(response.status,
|
|
261
|
+
throw this.createErrorFromResponse(response.status, parsed);
|
|
230
262
|
}
|
|
231
263
|
return {
|
|
232
|
-
data,
|
|
264
|
+
data: this.unwrapApiResponse(parsed),
|
|
233
265
|
status: response.status,
|
|
234
266
|
headers: response.headers,
|
|
235
267
|
};
|
|
@@ -283,13 +315,23 @@ class HttpClient {
|
|
|
283
315
|
// Safely extract error fields with runtime type checking
|
|
284
316
|
const errorData = typeof data === 'object' && data !== null ? data : {};
|
|
285
317
|
const record = errorData;
|
|
286
|
-
const
|
|
318
|
+
const nestedError = typeof record.error === 'object' && record.error !== null
|
|
319
|
+
? record.error
|
|
320
|
+
: null;
|
|
321
|
+
const message = (typeof nestedError?.message === 'string' ? nestedError.message : null) ??
|
|
322
|
+
(typeof record.message === 'string' ? record.message : null) ??
|
|
287
323
|
(typeof record.error === 'string' ? record.error : null) ??
|
|
288
324
|
'Request failed';
|
|
289
|
-
const code = typeof
|
|
290
|
-
|
|
325
|
+
const code = (typeof nestedError?.code === 'string' ? nestedError.code : null) ??
|
|
326
|
+
(typeof record.code === 'string' ? record.code : null) ??
|
|
327
|
+
'api_error';
|
|
328
|
+
const nestedDetails = typeof nestedError?.details === 'object' && nestedError.details !== null
|
|
329
|
+
? nestedError.details
|
|
330
|
+
: undefined;
|
|
331
|
+
const topLevelDetails = typeof record.details === 'object' && record.details !== null
|
|
291
332
|
? record.details
|
|
292
333
|
: undefined;
|
|
334
|
+
const details = nestedDetails ?? topLevelDetails;
|
|
293
335
|
switch (status) {
|
|
294
336
|
case 400:
|
|
295
337
|
return new ValidationError(message, details);
|
|
@@ -301,6 +343,15 @@ class HttpClient {
|
|
|
301
343
|
return new SparkVaultError(message, code, status, details);
|
|
302
344
|
}
|
|
303
345
|
}
|
|
346
|
+
unwrapApiResponse(data) {
|
|
347
|
+
if (typeof data === 'object' && data !== null) {
|
|
348
|
+
const record = data;
|
|
349
|
+
if ('data' in record && ('meta' in record || 'error' in record)) {
|
|
350
|
+
return record.data;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return data;
|
|
354
|
+
}
|
|
304
355
|
get(path, options) {
|
|
305
356
|
return this.request(path, { ...options, method: 'GET' });
|
|
306
357
|
}
|
|
@@ -338,10 +389,7 @@ class HttpClient {
|
|
|
338
389
|
async executeRequestRaw(path, options) {
|
|
339
390
|
const { method = 'GET', headers = {}, body, timeout = this.config.timeout, } = options;
|
|
340
391
|
const url = `${this.config.apiBaseUrl}${path}`;
|
|
341
|
-
const requestHeaders =
|
|
342
|
-
'Content-Type': 'application/json',
|
|
343
|
-
...headers,
|
|
344
|
-
};
|
|
392
|
+
const requestHeaders = await this.getDefaultHeaders(headers, body !== undefined);
|
|
345
393
|
const controller = new AbortController();
|
|
346
394
|
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
347
395
|
try {
|
|
@@ -375,6 +423,63 @@ class HttpClient {
|
|
|
375
423
|
}
|
|
376
424
|
}
|
|
377
425
|
|
|
426
|
+
class HealthModule {
|
|
427
|
+
constructor(config) {
|
|
428
|
+
this.config = config;
|
|
429
|
+
}
|
|
430
|
+
async check(options = {}) {
|
|
431
|
+
const checkedAt = Math.floor(Date.now() / 1000);
|
|
432
|
+
const controller = new AbortController();
|
|
433
|
+
const timeoutId = setTimeout(() => controller.abort(), options.timeout ?? this.config.timeout);
|
|
434
|
+
try {
|
|
435
|
+
const response = await fetch(`${this.config.apiBaseUrl}/health`, {
|
|
436
|
+
method: 'GET',
|
|
437
|
+
headers: { Accept: 'application/json' },
|
|
438
|
+
signal: controller.signal,
|
|
439
|
+
});
|
|
440
|
+
return {
|
|
441
|
+
online: response.ok,
|
|
442
|
+
status: response.ok ? await this.readStatus(response) : 'unhealthy',
|
|
443
|
+
httpStatus: response.status,
|
|
444
|
+
checkedAt,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
catch (err) {
|
|
448
|
+
return {
|
|
449
|
+
online: false,
|
|
450
|
+
status: 'unreachable',
|
|
451
|
+
checkedAt,
|
|
452
|
+
error: err instanceof Error ? err.message : String(err),
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
finally {
|
|
456
|
+
clearTimeout(timeoutId);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
async isOnline(options = {}) {
|
|
460
|
+
const result = await this.check(options);
|
|
461
|
+
return result.online;
|
|
462
|
+
}
|
|
463
|
+
async readStatus(response) {
|
|
464
|
+
try {
|
|
465
|
+
const body = await response.json();
|
|
466
|
+
const data = typeof body === 'object' && body !== null && 'data' in body
|
|
467
|
+
? body.data
|
|
468
|
+
: body;
|
|
469
|
+
if (typeof data === 'object' && data !== null) {
|
|
470
|
+
const status = data.status;
|
|
471
|
+
if (typeof status === 'string' && status) {
|
|
472
|
+
return status;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
catch {
|
|
477
|
+
return response.ok ? 'healthy' : 'unhealthy';
|
|
478
|
+
}
|
|
479
|
+
return response.ok ? 'healthy' : 'unhealthy';
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
378
483
|
/**
|
|
379
484
|
* SparkVault API Base
|
|
380
485
|
*
|
|
@@ -846,14 +951,19 @@ class IdentityApi extends SparkVaultApi {
|
|
|
846
951
|
return this.configCache !== null;
|
|
847
952
|
}
|
|
848
953
|
/**
|
|
849
|
-
* Check if an email has registered passkeys
|
|
954
|
+
* Check if an identity (email or phone) has registered passkeys.
|
|
850
955
|
*
|
|
851
956
|
* Returns:
|
|
852
|
-
* -
|
|
853
|
-
*
|
|
957
|
+
* - identity_valid: identity passes per-type validity check
|
|
958
|
+
* (email: MX lookup; phone: E.164 format)
|
|
959
|
+
* - has_passkey: whether any passkeys are registered for this identity
|
|
960
|
+
* (cross-tenant — single global RP)
|
|
961
|
+
* - email_valid: legacy alias for identity_valid, present for backward
|
|
962
|
+
* compatibility with older SDK versions
|
|
854
963
|
*/
|
|
855
|
-
async checkPasskey(email) {
|
|
856
|
-
|
|
964
|
+
async checkPasskey(identity, identityType = 'email') {
|
|
965
|
+
const body = identityType === 'phone' ? { phone: identity } : { email: identity };
|
|
966
|
+
return this.request('POST', '/passkey/check', body);
|
|
857
967
|
}
|
|
858
968
|
/**
|
|
859
969
|
* Build auth context params for API requests (OIDC/simple mode).
|
|
@@ -898,12 +1008,13 @@ class IdentityApi extends SparkVaultApi {
|
|
|
898
1008
|
});
|
|
899
1009
|
}
|
|
900
1010
|
/**
|
|
901
|
-
* Start passkey registration
|
|
1011
|
+
* Start passkey registration for an identity (email or phone).
|
|
902
1012
|
*/
|
|
903
|
-
async startPasskeyRegister(email) {
|
|
1013
|
+
async startPasskeyRegister(identity, identityType = 'email') {
|
|
1014
|
+
const body = identityType === 'phone' ? { phone: identity } : { email: identity };
|
|
904
1015
|
// Backend returns { options: PublicKeyCredentialCreationOptions, session: {...} }
|
|
905
1016
|
// Extract and flatten to match PasskeyChallengeResponse
|
|
906
|
-
const response = await this.request('POST', '/passkey/register',
|
|
1017
|
+
const response = await this.request('POST', '/passkey/register', body);
|
|
907
1018
|
return {
|
|
908
1019
|
challenge: response.options.challenge,
|
|
909
1020
|
rpId: response.options.rp.id,
|
|
@@ -933,12 +1044,13 @@ class IdentityApi extends SparkVaultApi {
|
|
|
933
1044
|
});
|
|
934
1045
|
}
|
|
935
1046
|
/**
|
|
936
|
-
* Start passkey verification
|
|
1047
|
+
* Start passkey verification for an identity (email or phone).
|
|
937
1048
|
*/
|
|
938
|
-
async startPasskeyVerify(email, authContext) {
|
|
1049
|
+
async startPasskeyVerify(identity, identityType = 'email', authContext) {
|
|
1050
|
+
const identityField = identityType === 'phone' ? { phone: identity } : { email: identity };
|
|
939
1051
|
// Backend returns { options: PublicKeyCredentialRequestOptions, session: {...} }
|
|
940
1052
|
// Extract and flatten to match PasskeyChallengeResponse
|
|
941
|
-
const response = await this.request('POST', '/passkey/verify', {
|
|
1053
|
+
const response = await this.request('POST', '/passkey/verify', { ...identityField, ...this.buildAuthContextParams(authContext) });
|
|
942
1054
|
return {
|
|
943
1055
|
challenge: response.options.challenge,
|
|
944
1056
|
rpId: response.options.rpId,
|
|
@@ -1345,10 +1457,12 @@ class PasskeyHandler {
|
|
|
1345
1457
|
*/
|
|
1346
1458
|
async checkPasskey() {
|
|
1347
1459
|
try {
|
|
1348
|
-
const response = await this.api.checkPasskey(this.state.recipient);
|
|
1460
|
+
const response = await this.api.checkPasskey(this.state.recipient, this.state.identityType);
|
|
1349
1461
|
this.state.setPasskeyStatus(response.has_passkey);
|
|
1462
|
+
// Prefer identity_valid; fall back to email_valid for older backends.
|
|
1463
|
+
const valid = response.identity_valid ?? response.email_valid;
|
|
1350
1464
|
return {
|
|
1351
|
-
emailValid:
|
|
1465
|
+
emailValid: valid,
|
|
1352
1466
|
hasPasskey: response.has_passkey,
|
|
1353
1467
|
};
|
|
1354
1468
|
}
|
|
@@ -1380,12 +1494,14 @@ class PasskeyHandler {
|
|
|
1380
1494
|
*/
|
|
1381
1495
|
async openPasskeyPopup(mode) {
|
|
1382
1496
|
return new Promise((resolve) => {
|
|
1383
|
-
// Build popup URL
|
|
1497
|
+
// Build popup URL. Pass identity as `email` or `phone` (mutually
|
|
1498
|
+
// exclusive) so the popup can render and POST the correct field.
|
|
1384
1499
|
const params = new URLSearchParams({
|
|
1385
1500
|
mode,
|
|
1386
|
-
email: this.state.recipient,
|
|
1387
1501
|
origin: window.location.origin,
|
|
1388
1502
|
});
|
|
1503
|
+
const identityParam = this.state.identityType === 'phone' ? 'phone' : 'email';
|
|
1504
|
+
params.set(identityParam, this.state.recipient);
|
|
1389
1505
|
// Add auth context for OIDC/simple mode flows
|
|
1390
1506
|
const authContext = this.state.authContext;
|
|
1391
1507
|
if (authContext?.authRequestId) {
|
|
@@ -9538,6 +9654,7 @@ class VaultUploadModule {
|
|
|
9538
9654
|
*/
|
|
9539
9655
|
class VaultsModule {
|
|
9540
9656
|
constructor(config, http) {
|
|
9657
|
+
this.config = config;
|
|
9541
9658
|
this.http = http;
|
|
9542
9659
|
this.uploadModule = new VaultUploadModule(config);
|
|
9543
9660
|
// Create callable that also has attach/close methods
|
|
@@ -9585,13 +9702,15 @@ class VaultsModule {
|
|
|
9585
9702
|
}
|
|
9586
9703
|
const cleanId = vaultId.startsWith('vlt_') ? vaultId : `vlt_${vaultId}`;
|
|
9587
9704
|
const response = await this.http.post(`/v1/vaults/${cleanId}/unseal`, { vmk });
|
|
9705
|
+
if (!response.data.vat) {
|
|
9706
|
+
throw new ValidationError('Unseal response did not include a vault access token');
|
|
9707
|
+
}
|
|
9588
9708
|
return {
|
|
9589
9709
|
id: response.data.vault_id,
|
|
9590
|
-
|
|
9591
|
-
|
|
9710
|
+
vatToken: response.data.vat,
|
|
9711
|
+
issuedAt: response.data.issued_at,
|
|
9592
9712
|
expiresAt: response.data.expires_at,
|
|
9593
|
-
|
|
9594
|
-
storageBytes: response.data.storage_bytes,
|
|
9713
|
+
ttlSeconds: response.data.ttl_seconds,
|
|
9595
9714
|
};
|
|
9596
9715
|
}
|
|
9597
9716
|
/**
|
|
@@ -9636,22 +9755,26 @@ class VaultsModule {
|
|
|
9636
9755
|
*/
|
|
9637
9756
|
async uploadIngot(vault, options) {
|
|
9638
9757
|
this.validateUploadOptions(options);
|
|
9639
|
-
const
|
|
9640
|
-
const
|
|
9758
|
+
const requestedName = options.name ?? getBlobName(options.file);
|
|
9759
|
+
const requestedContentType = options.contentType ?? options.file.type ?? 'application/octet-stream';
|
|
9641
9760
|
const createResponse = await this.http.post(`/v1/vaults/${vault.id}/ingots`, {
|
|
9642
|
-
name,
|
|
9643
|
-
content_type:
|
|
9761
|
+
name: requestedName,
|
|
9762
|
+
content_type: requestedContentType,
|
|
9644
9763
|
size_bytes: options.file.size,
|
|
9645
9764
|
}, {
|
|
9646
|
-
headers: {
|
|
9765
|
+
headers: { 'X-Vault-Access-Token': vault.vatToken },
|
|
9647
9766
|
});
|
|
9767
|
+
const created = createResponse.data;
|
|
9768
|
+
if (!created.forge_url) {
|
|
9769
|
+
throw new ValidationError('Upload endpoint did not return a Forge upload URL');
|
|
9770
|
+
}
|
|
9771
|
+
await uploadBlobWithTus(options.file, created.forge_url, created.name, created.content_type, options.onProgress);
|
|
9648
9772
|
return {
|
|
9649
|
-
id:
|
|
9650
|
-
name:
|
|
9651
|
-
contentType:
|
|
9652
|
-
size:
|
|
9653
|
-
|
|
9654
|
-
type: createResponse.data.ingot_type,
|
|
9773
|
+
id: created.ingot_id,
|
|
9774
|
+
name: created.name,
|
|
9775
|
+
contentType: created.content_type,
|
|
9776
|
+
size: created.size_bytes,
|
|
9777
|
+
uploadExpiresAt: created.expires_at,
|
|
9655
9778
|
};
|
|
9656
9779
|
}
|
|
9657
9780
|
/**
|
|
@@ -9663,10 +9786,14 @@ class VaultsModule {
|
|
|
9663
9786
|
*/
|
|
9664
9787
|
async downloadIngot(vault, ingotId) {
|
|
9665
9788
|
const cleanId = ingotId.startsWith('ing_') ? ingotId : `ing_${ingotId}`;
|
|
9666
|
-
|
|
9667
|
-
|
|
9668
|
-
headers: { Authorization: `Bearer ${vault.vatToken}` },
|
|
9789
|
+
const response = await this.http.post(`/v1/vaults/${vault.id}/ingots/${cleanId}/download`, undefined, {
|
|
9790
|
+
headers: { 'X-Vault-Access-Token': vault.vatToken },
|
|
9669
9791
|
});
|
|
9792
|
+
if (!response.data.download_url) {
|
|
9793
|
+
throw new ValidationError('Download endpoint did not return a download URL');
|
|
9794
|
+
}
|
|
9795
|
+
this.validateDownloadUrl(response.data.download_url);
|
|
9796
|
+
return fetchBlobWithTimeout(response.data.download_url);
|
|
9670
9797
|
}
|
|
9671
9798
|
/**
|
|
9672
9799
|
* List all ingots in an unsealed vault.
|
|
@@ -9677,7 +9804,7 @@ class VaultsModule {
|
|
|
9677
9804
|
*/
|
|
9678
9805
|
async listIngots(vault) {
|
|
9679
9806
|
const response = await this.http.get(`/v1/vaults/${vault.id}/ingots`, {
|
|
9680
|
-
headers: {
|
|
9807
|
+
headers: { 'X-Vault-Access-Token': vault.vatToken },
|
|
9681
9808
|
});
|
|
9682
9809
|
return response.data.ingots.map((i) => ({
|
|
9683
9810
|
id: i.ingot_id,
|
|
@@ -9685,7 +9812,6 @@ class VaultsModule {
|
|
|
9685
9812
|
contentType: i.content_type,
|
|
9686
9813
|
size: i.size_bytes,
|
|
9687
9814
|
createdAt: i.created_at,
|
|
9688
|
-
type: i.ingot_type,
|
|
9689
9815
|
}));
|
|
9690
9816
|
}
|
|
9691
9817
|
/**
|
|
@@ -9697,7 +9823,7 @@ class VaultsModule {
|
|
|
9697
9823
|
async deleteIngot(vault, ingotId) {
|
|
9698
9824
|
const cleanId = ingotId.startsWith('ing_') ? ingotId : `ing_${ingotId}`;
|
|
9699
9825
|
await this.http.delete(`/v1/vaults/${vault.id}/ingots/${cleanId}`, {
|
|
9700
|
-
headers: {
|
|
9826
|
+
headers: { 'X-Vault-Access-Token': vault.vatToken },
|
|
9701
9827
|
});
|
|
9702
9828
|
}
|
|
9703
9829
|
validateCreateOptions(options) {
|
|
@@ -9716,6 +9842,117 @@ class VaultsModule {
|
|
|
9716
9842
|
throw new ValidationError('File cannot be empty');
|
|
9717
9843
|
}
|
|
9718
9844
|
}
|
|
9845
|
+
validateDownloadUrl(downloadUrl) {
|
|
9846
|
+
let parsed;
|
|
9847
|
+
try {
|
|
9848
|
+
parsed = new URL(downloadUrl);
|
|
9849
|
+
}
|
|
9850
|
+
catch {
|
|
9851
|
+
throw new ValidationError('Invalid download URL from server');
|
|
9852
|
+
}
|
|
9853
|
+
if (parsed.protocol !== 'https:') {
|
|
9854
|
+
throw new ValidationError('Download URL must use HTTPS');
|
|
9855
|
+
}
|
|
9856
|
+
const allowedHosts = this.config.allowedDownloadHostPatterns;
|
|
9857
|
+
if (!allowedHosts?.length) {
|
|
9858
|
+
return;
|
|
9859
|
+
}
|
|
9860
|
+
if (!allowedHosts.some(pattern => pattern.test(parsed.hostname))) {
|
|
9861
|
+
throw new ValidationError('Invalid download URL from server');
|
|
9862
|
+
}
|
|
9863
|
+
}
|
|
9864
|
+
}
|
|
9865
|
+
function getBlobName(file) {
|
|
9866
|
+
if (typeof File !== 'undefined' && file instanceof File && file.name) {
|
|
9867
|
+
return file.name;
|
|
9868
|
+
}
|
|
9869
|
+
return 'unnamed';
|
|
9870
|
+
}
|
|
9871
|
+
function base64Encode(value) {
|
|
9872
|
+
return btoa(unescape(encodeURIComponent(value)));
|
|
9873
|
+
}
|
|
9874
|
+
async function uploadBlobWithTus(file, forgeUrl, filename, contentType, onProgress) {
|
|
9875
|
+
if (typeof XMLHttpRequest === 'undefined') {
|
|
9876
|
+
throw new ValidationError('XMLHttpRequest is required for browser ingot uploads');
|
|
9877
|
+
}
|
|
9878
|
+
const tusVersion = '1.0.0';
|
|
9879
|
+
const defaultChunkSize = 50 * 1024 * 1024;
|
|
9880
|
+
const url = new URL(forgeUrl);
|
|
9881
|
+
const istk = url.searchParams.get('istk');
|
|
9882
|
+
const endpoint = `${url.origin}${url.pathname}`;
|
|
9883
|
+
if (!istk) {
|
|
9884
|
+
throw new ValidationError('Forge upload URL is missing ISTK');
|
|
9885
|
+
}
|
|
9886
|
+
const createResponse = await fetch(endpoint, {
|
|
9887
|
+
method: 'POST',
|
|
9888
|
+
headers: {
|
|
9889
|
+
'Tus-Resumable': tusVersion,
|
|
9890
|
+
'Upload-Length': String(file.size),
|
|
9891
|
+
'Upload-Metadata': `filename ${base64Encode(filename)},filetype ${base64Encode(contentType)}`,
|
|
9892
|
+
'X-ISTK': istk,
|
|
9893
|
+
},
|
|
9894
|
+
});
|
|
9895
|
+
if (!createResponse.ok) {
|
|
9896
|
+
const errorText = await createResponse.text();
|
|
9897
|
+
throw new Error(`Failed to create upload session: ${createResponse.status} ${errorText}`);
|
|
9898
|
+
}
|
|
9899
|
+
const location = createResponse.headers.get('Location');
|
|
9900
|
+
const chunkSizeHeader = createResponse.headers.get('X-Chunk-Size');
|
|
9901
|
+
const chunkSize = chunkSizeHeader ? parseInt(chunkSizeHeader, 10) : defaultChunkSize;
|
|
9902
|
+
if (!location) {
|
|
9903
|
+
throw new Error('Server did not return upload location');
|
|
9904
|
+
}
|
|
9905
|
+
const uploadUrl = location.startsWith('http') ? location : `${url.origin}${location}`;
|
|
9906
|
+
let offset = 0;
|
|
9907
|
+
while (offset < file.size) {
|
|
9908
|
+
const chunkStart = offset;
|
|
9909
|
+
const chunkEnd = Math.min(offset + chunkSize, file.size);
|
|
9910
|
+
const chunk = file.slice(chunkStart, chunkEnd);
|
|
9911
|
+
offset = await uploadChunkWithProgress(uploadUrl, chunk, chunkStart, file.size, tusVersion, istk, onProgress);
|
|
9912
|
+
}
|
|
9913
|
+
}
|
|
9914
|
+
function uploadChunkWithProgress(uploadUrl, chunk, chunkStart, totalSize, tusVersion, istk, onProgress) {
|
|
9915
|
+
return new Promise((resolve, reject) => {
|
|
9916
|
+
const xhr = new XMLHttpRequest();
|
|
9917
|
+
xhr.upload.onprogress = (event) => {
|
|
9918
|
+
if (event.lengthComputable) {
|
|
9919
|
+
onProgress?.(chunkStart + event.loaded, totalSize);
|
|
9920
|
+
}
|
|
9921
|
+
};
|
|
9922
|
+
xhr.onload = () => {
|
|
9923
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
9924
|
+
const newOffsetHeader = xhr.getResponseHeader('Upload-Offset');
|
|
9925
|
+
const newOffset = newOffsetHeader ? parseInt(newOffsetHeader, 10) : chunkStart + chunk.size;
|
|
9926
|
+
resolve(newOffset);
|
|
9927
|
+
}
|
|
9928
|
+
else {
|
|
9929
|
+
reject(new Error(`Chunk upload failed with status ${xhr.status}`));
|
|
9930
|
+
}
|
|
9931
|
+
};
|
|
9932
|
+
xhr.onerror = () => reject(new Error('Chunk upload failed'));
|
|
9933
|
+
xhr.ontimeout = () => reject(new Error('Chunk upload timed out'));
|
|
9934
|
+
xhr.onabort = () => reject(new Error('Upload cancelled'));
|
|
9935
|
+
xhr.open('PATCH', uploadUrl);
|
|
9936
|
+
xhr.setRequestHeader('Tus-Resumable', tusVersion);
|
|
9937
|
+
xhr.setRequestHeader('Upload-Offset', String(chunkStart));
|
|
9938
|
+
xhr.setRequestHeader('Content-Type', 'application/offset+octet-stream');
|
|
9939
|
+
xhr.setRequestHeader('X-ISTK', istk);
|
|
9940
|
+
xhr.send(chunk);
|
|
9941
|
+
});
|
|
9942
|
+
}
|
|
9943
|
+
async function fetchBlobWithTimeout(url, timeout = 300000) {
|
|
9944
|
+
const controller = new AbortController();
|
|
9945
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
9946
|
+
try {
|
|
9947
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
9948
|
+
if (!response.ok) {
|
|
9949
|
+
throw new Error(`Download failed with status ${response.status}`);
|
|
9950
|
+
}
|
|
9951
|
+
return response.blob();
|
|
9952
|
+
}
|
|
9953
|
+
finally {
|
|
9954
|
+
clearTimeout(timeoutId);
|
|
9955
|
+
}
|
|
9719
9956
|
}
|
|
9720
9957
|
|
|
9721
9958
|
/**
|
|
@@ -9902,6 +10139,7 @@ class SparkVault {
|
|
|
9902
10139
|
const http = new HttpClient(this.config);
|
|
9903
10140
|
this.identity = new IdentityModule(this.config);
|
|
9904
10141
|
this.vaults = new VaultsModule(this.config, http);
|
|
10142
|
+
this.health = new HealthModule(this.config);
|
|
9905
10143
|
}
|
|
9906
10144
|
/**
|
|
9907
10145
|
* Initialize the SparkVault SDK.
|