@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.cjs.js
CHANGED
|
@@ -70,14 +70,25 @@ class PopupBlockedError extends SparkVaultError {
|
|
|
70
70
|
*/
|
|
71
71
|
const API_URL = 'https://api.sparkvault.com';
|
|
72
72
|
const IDENTITY_URL = 'https://api.sparkvault.com/v1/apps/identity';
|
|
73
|
+
function normalizeApiBaseUrl(url) {
|
|
74
|
+
const trimmed = url.replace(/\/+$/, '');
|
|
75
|
+
return trimmed.endsWith('/v1') ? trimmed.slice(0, -3) : trimmed;
|
|
76
|
+
}
|
|
77
|
+
function normalizeBaseUrl(url) {
|
|
78
|
+
return url.replace(/\/+$/, '');
|
|
79
|
+
}
|
|
73
80
|
function resolveConfig(config) {
|
|
74
81
|
return {
|
|
75
82
|
accountId: config.accountId,
|
|
76
83
|
timeout: config.timeout ?? 30000,
|
|
77
|
-
apiBaseUrl: API_URL,
|
|
78
|
-
identityBaseUrl: IDENTITY_URL,
|
|
84
|
+
apiBaseUrl: normalizeApiBaseUrl(config.apiBaseUrl ?? API_URL),
|
|
85
|
+
identityBaseUrl: normalizeBaseUrl(config.identityBaseUrl ?? IDENTITY_URL),
|
|
86
|
+
accessToken: config.accessToken,
|
|
87
|
+
getAccessToken: config.getAccessToken,
|
|
88
|
+
apiKey: config.apiKey,
|
|
79
89
|
preloadConfig: config.preloadConfig !== false, // Default: true
|
|
80
90
|
backdropBlur: config.backdropBlur !== false, // Default: true
|
|
91
|
+
allowedDownloadHostPatterns: config.allowedDownloadHostPatterns,
|
|
81
92
|
};
|
|
82
93
|
}
|
|
83
94
|
function validateConfig(config) {
|
|
@@ -194,6 +205,31 @@ class HttpClient {
|
|
|
194
205
|
constructor(config) {
|
|
195
206
|
this.config = config;
|
|
196
207
|
}
|
|
208
|
+
async getDefaultHeaders(headers, hasJsonBody) {
|
|
209
|
+
const requestHeaders = {
|
|
210
|
+
Accept: 'application/json',
|
|
211
|
+
...headers,
|
|
212
|
+
};
|
|
213
|
+
if (hasJsonBody) {
|
|
214
|
+
requestHeaders['Content-Type'] = 'application/json';
|
|
215
|
+
}
|
|
216
|
+
if (!requestHeaders.Authorization) {
|
|
217
|
+
const token = await this.getAccessToken();
|
|
218
|
+
if (token) {
|
|
219
|
+
requestHeaders.Authorization = `Bearer ${token}`;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (this.config.apiKey && !requestHeaders['X-API-Key']) {
|
|
223
|
+
requestHeaders['X-API-Key'] = this.config.apiKey;
|
|
224
|
+
}
|
|
225
|
+
return requestHeaders;
|
|
226
|
+
}
|
|
227
|
+
async getAccessToken() {
|
|
228
|
+
if (this.config.getAccessToken) {
|
|
229
|
+
return this.config.getAccessToken();
|
|
230
|
+
}
|
|
231
|
+
return this.config.accessToken ?? null;
|
|
232
|
+
}
|
|
197
233
|
async request(path, options = {}) {
|
|
198
234
|
const { retry } = options;
|
|
199
235
|
// Per CLAUDE.md §7: Wrap with retry logic if enabled
|
|
@@ -213,11 +249,7 @@ class HttpClient {
|
|
|
213
249
|
async executeRequest(path, options) {
|
|
214
250
|
const { method = 'GET', headers = {}, body, timeout = this.config.timeout, } = options;
|
|
215
251
|
const url = `${this.config.apiBaseUrl}${path}`;
|
|
216
|
-
const requestHeaders =
|
|
217
|
-
'Content-Type': 'application/json',
|
|
218
|
-
Accept: 'application/json',
|
|
219
|
-
...headers,
|
|
220
|
-
};
|
|
252
|
+
const requestHeaders = await this.getDefaultHeaders(headers, body !== undefined);
|
|
221
253
|
const controller = new AbortController();
|
|
222
254
|
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
223
255
|
try {
|
|
@@ -228,12 +260,12 @@ class HttpClient {
|
|
|
228
260
|
signal: controller.signal,
|
|
229
261
|
});
|
|
230
262
|
clearTimeout(timeoutId);
|
|
231
|
-
const
|
|
263
|
+
const parsed = await this.parseResponse(response);
|
|
232
264
|
if (!response.ok) {
|
|
233
|
-
throw this.createErrorFromResponse(response.status,
|
|
265
|
+
throw this.createErrorFromResponse(response.status, parsed);
|
|
234
266
|
}
|
|
235
267
|
return {
|
|
236
|
-
data,
|
|
268
|
+
data: this.unwrapApiResponse(parsed),
|
|
237
269
|
status: response.status,
|
|
238
270
|
headers: response.headers,
|
|
239
271
|
};
|
|
@@ -287,13 +319,23 @@ class HttpClient {
|
|
|
287
319
|
// Safely extract error fields with runtime type checking
|
|
288
320
|
const errorData = typeof data === 'object' && data !== null ? data : {};
|
|
289
321
|
const record = errorData;
|
|
290
|
-
const
|
|
322
|
+
const nestedError = typeof record.error === 'object' && record.error !== null
|
|
323
|
+
? record.error
|
|
324
|
+
: null;
|
|
325
|
+
const message = (typeof nestedError?.message === 'string' ? nestedError.message : null) ??
|
|
326
|
+
(typeof record.message === 'string' ? record.message : null) ??
|
|
291
327
|
(typeof record.error === 'string' ? record.error : null) ??
|
|
292
328
|
'Request failed';
|
|
293
|
-
const code = typeof
|
|
294
|
-
|
|
329
|
+
const code = (typeof nestedError?.code === 'string' ? nestedError.code : null) ??
|
|
330
|
+
(typeof record.code === 'string' ? record.code : null) ??
|
|
331
|
+
'api_error';
|
|
332
|
+
const nestedDetails = typeof nestedError?.details === 'object' && nestedError.details !== null
|
|
333
|
+
? nestedError.details
|
|
334
|
+
: undefined;
|
|
335
|
+
const topLevelDetails = typeof record.details === 'object' && record.details !== null
|
|
295
336
|
? record.details
|
|
296
337
|
: undefined;
|
|
338
|
+
const details = nestedDetails ?? topLevelDetails;
|
|
297
339
|
switch (status) {
|
|
298
340
|
case 400:
|
|
299
341
|
return new ValidationError(message, details);
|
|
@@ -305,6 +347,15 @@ class HttpClient {
|
|
|
305
347
|
return new SparkVaultError(message, code, status, details);
|
|
306
348
|
}
|
|
307
349
|
}
|
|
350
|
+
unwrapApiResponse(data) {
|
|
351
|
+
if (typeof data === 'object' && data !== null) {
|
|
352
|
+
const record = data;
|
|
353
|
+
if ('data' in record && ('meta' in record || 'error' in record)) {
|
|
354
|
+
return record.data;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return data;
|
|
358
|
+
}
|
|
308
359
|
get(path, options) {
|
|
309
360
|
return this.request(path, { ...options, method: 'GET' });
|
|
310
361
|
}
|
|
@@ -342,10 +393,7 @@ class HttpClient {
|
|
|
342
393
|
async executeRequestRaw(path, options) {
|
|
343
394
|
const { method = 'GET', headers = {}, body, timeout = this.config.timeout, } = options;
|
|
344
395
|
const url = `${this.config.apiBaseUrl}${path}`;
|
|
345
|
-
const requestHeaders =
|
|
346
|
-
'Content-Type': 'application/json',
|
|
347
|
-
...headers,
|
|
348
|
-
};
|
|
396
|
+
const requestHeaders = await this.getDefaultHeaders(headers, body !== undefined);
|
|
349
397
|
const controller = new AbortController();
|
|
350
398
|
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
351
399
|
try {
|
|
@@ -379,6 +427,63 @@ class HttpClient {
|
|
|
379
427
|
}
|
|
380
428
|
}
|
|
381
429
|
|
|
430
|
+
class HealthModule {
|
|
431
|
+
constructor(config) {
|
|
432
|
+
this.config = config;
|
|
433
|
+
}
|
|
434
|
+
async check(options = {}) {
|
|
435
|
+
const checkedAt = Math.floor(Date.now() / 1000);
|
|
436
|
+
const controller = new AbortController();
|
|
437
|
+
const timeoutId = setTimeout(() => controller.abort(), options.timeout ?? this.config.timeout);
|
|
438
|
+
try {
|
|
439
|
+
const response = await fetch(`${this.config.apiBaseUrl}/health`, {
|
|
440
|
+
method: 'GET',
|
|
441
|
+
headers: { Accept: 'application/json' },
|
|
442
|
+
signal: controller.signal,
|
|
443
|
+
});
|
|
444
|
+
return {
|
|
445
|
+
online: response.ok,
|
|
446
|
+
status: response.ok ? await this.readStatus(response) : 'unhealthy',
|
|
447
|
+
httpStatus: response.status,
|
|
448
|
+
checkedAt,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
catch (err) {
|
|
452
|
+
return {
|
|
453
|
+
online: false,
|
|
454
|
+
status: 'unreachable',
|
|
455
|
+
checkedAt,
|
|
456
|
+
error: err instanceof Error ? err.message : String(err),
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
finally {
|
|
460
|
+
clearTimeout(timeoutId);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
async isOnline(options = {}) {
|
|
464
|
+
const result = await this.check(options);
|
|
465
|
+
return result.online;
|
|
466
|
+
}
|
|
467
|
+
async readStatus(response) {
|
|
468
|
+
try {
|
|
469
|
+
const body = await response.json();
|
|
470
|
+
const data = typeof body === 'object' && body !== null && 'data' in body
|
|
471
|
+
? body.data
|
|
472
|
+
: body;
|
|
473
|
+
if (typeof data === 'object' && data !== null) {
|
|
474
|
+
const status = data.status;
|
|
475
|
+
if (typeof status === 'string' && status) {
|
|
476
|
+
return status;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
catch {
|
|
481
|
+
return response.ok ? 'healthy' : 'unhealthy';
|
|
482
|
+
}
|
|
483
|
+
return response.ok ? 'healthy' : 'unhealthy';
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
382
487
|
/**
|
|
383
488
|
* SparkVault API Base
|
|
384
489
|
*
|
|
@@ -850,14 +955,19 @@ class IdentityApi extends SparkVaultApi {
|
|
|
850
955
|
return this.configCache !== null;
|
|
851
956
|
}
|
|
852
957
|
/**
|
|
853
|
-
* Check if an email has registered passkeys
|
|
958
|
+
* Check if an identity (email or phone) has registered passkeys.
|
|
854
959
|
*
|
|
855
960
|
* Returns:
|
|
856
|
-
* -
|
|
857
|
-
*
|
|
961
|
+
* - identity_valid: identity passes per-type validity check
|
|
962
|
+
* (email: MX lookup; phone: E.164 format)
|
|
963
|
+
* - has_passkey: whether any passkeys are registered for this identity
|
|
964
|
+
* (cross-tenant — single global RP)
|
|
965
|
+
* - email_valid: legacy alias for identity_valid, present for backward
|
|
966
|
+
* compatibility with older SDK versions
|
|
858
967
|
*/
|
|
859
|
-
async checkPasskey(email) {
|
|
860
|
-
|
|
968
|
+
async checkPasskey(identity, identityType = 'email') {
|
|
969
|
+
const body = identityType === 'phone' ? { phone: identity } : { email: identity };
|
|
970
|
+
return this.request('POST', '/passkey/check', body);
|
|
861
971
|
}
|
|
862
972
|
/**
|
|
863
973
|
* Build auth context params for API requests (OIDC/simple mode).
|
|
@@ -902,12 +1012,13 @@ class IdentityApi extends SparkVaultApi {
|
|
|
902
1012
|
});
|
|
903
1013
|
}
|
|
904
1014
|
/**
|
|
905
|
-
* Start passkey registration
|
|
1015
|
+
* Start passkey registration for an identity (email or phone).
|
|
906
1016
|
*/
|
|
907
|
-
async startPasskeyRegister(email) {
|
|
1017
|
+
async startPasskeyRegister(identity, identityType = 'email') {
|
|
1018
|
+
const body = identityType === 'phone' ? { phone: identity } : { email: identity };
|
|
908
1019
|
// Backend returns { options: PublicKeyCredentialCreationOptions, session: {...} }
|
|
909
1020
|
// Extract and flatten to match PasskeyChallengeResponse
|
|
910
|
-
const response = await this.request('POST', '/passkey/register',
|
|
1021
|
+
const response = await this.request('POST', '/passkey/register', body);
|
|
911
1022
|
return {
|
|
912
1023
|
challenge: response.options.challenge,
|
|
913
1024
|
rpId: response.options.rp.id,
|
|
@@ -937,12 +1048,13 @@ class IdentityApi extends SparkVaultApi {
|
|
|
937
1048
|
});
|
|
938
1049
|
}
|
|
939
1050
|
/**
|
|
940
|
-
* Start passkey verification
|
|
1051
|
+
* Start passkey verification for an identity (email or phone).
|
|
941
1052
|
*/
|
|
942
|
-
async startPasskeyVerify(email, authContext) {
|
|
1053
|
+
async startPasskeyVerify(identity, identityType = 'email', authContext) {
|
|
1054
|
+
const identityField = identityType === 'phone' ? { phone: identity } : { email: identity };
|
|
943
1055
|
// Backend returns { options: PublicKeyCredentialRequestOptions, session: {...} }
|
|
944
1056
|
// Extract and flatten to match PasskeyChallengeResponse
|
|
945
|
-
const response = await this.request('POST', '/passkey/verify', {
|
|
1057
|
+
const response = await this.request('POST', '/passkey/verify', { ...identityField, ...this.buildAuthContextParams(authContext) });
|
|
946
1058
|
return {
|
|
947
1059
|
challenge: response.options.challenge,
|
|
948
1060
|
rpId: response.options.rpId,
|
|
@@ -1349,10 +1461,12 @@ class PasskeyHandler {
|
|
|
1349
1461
|
*/
|
|
1350
1462
|
async checkPasskey() {
|
|
1351
1463
|
try {
|
|
1352
|
-
const response = await this.api.checkPasskey(this.state.recipient);
|
|
1464
|
+
const response = await this.api.checkPasskey(this.state.recipient, this.state.identityType);
|
|
1353
1465
|
this.state.setPasskeyStatus(response.has_passkey);
|
|
1466
|
+
// Prefer identity_valid; fall back to email_valid for older backends.
|
|
1467
|
+
const valid = response.identity_valid ?? response.email_valid;
|
|
1354
1468
|
return {
|
|
1355
|
-
emailValid:
|
|
1469
|
+
emailValid: valid,
|
|
1356
1470
|
hasPasskey: response.has_passkey,
|
|
1357
1471
|
};
|
|
1358
1472
|
}
|
|
@@ -1384,12 +1498,14 @@ class PasskeyHandler {
|
|
|
1384
1498
|
*/
|
|
1385
1499
|
async openPasskeyPopup(mode) {
|
|
1386
1500
|
return new Promise((resolve) => {
|
|
1387
|
-
// Build popup URL
|
|
1501
|
+
// Build popup URL. Pass identity as `email` or `phone` (mutually
|
|
1502
|
+
// exclusive) so the popup can render and POST the correct field.
|
|
1388
1503
|
const params = new URLSearchParams({
|
|
1389
1504
|
mode,
|
|
1390
|
-
email: this.state.recipient,
|
|
1391
1505
|
origin: window.location.origin,
|
|
1392
1506
|
});
|
|
1507
|
+
const identityParam = this.state.identityType === 'phone' ? 'phone' : 'email';
|
|
1508
|
+
params.set(identityParam, this.state.recipient);
|
|
1393
1509
|
// Add auth context for OIDC/simple mode flows
|
|
1394
1510
|
const authContext = this.state.authContext;
|
|
1395
1511
|
if (authContext?.authRequestId) {
|
|
@@ -9542,6 +9658,7 @@ class VaultUploadModule {
|
|
|
9542
9658
|
*/
|
|
9543
9659
|
class VaultsModule {
|
|
9544
9660
|
constructor(config, http) {
|
|
9661
|
+
this.config = config;
|
|
9545
9662
|
this.http = http;
|
|
9546
9663
|
this.uploadModule = new VaultUploadModule(config);
|
|
9547
9664
|
// Create callable that also has attach/close methods
|
|
@@ -9589,13 +9706,15 @@ class VaultsModule {
|
|
|
9589
9706
|
}
|
|
9590
9707
|
const cleanId = vaultId.startsWith('vlt_') ? vaultId : `vlt_${vaultId}`;
|
|
9591
9708
|
const response = await this.http.post(`/v1/vaults/${cleanId}/unseal`, { vmk });
|
|
9709
|
+
if (!response.data.vat) {
|
|
9710
|
+
throw new ValidationError('Unseal response did not include a vault access token');
|
|
9711
|
+
}
|
|
9592
9712
|
return {
|
|
9593
9713
|
id: response.data.vault_id,
|
|
9594
|
-
|
|
9595
|
-
|
|
9714
|
+
vatToken: response.data.vat,
|
|
9715
|
+
issuedAt: response.data.issued_at,
|
|
9596
9716
|
expiresAt: response.data.expires_at,
|
|
9597
|
-
|
|
9598
|
-
storageBytes: response.data.storage_bytes,
|
|
9717
|
+
ttlSeconds: response.data.ttl_seconds,
|
|
9599
9718
|
};
|
|
9600
9719
|
}
|
|
9601
9720
|
/**
|
|
@@ -9640,22 +9759,26 @@ class VaultsModule {
|
|
|
9640
9759
|
*/
|
|
9641
9760
|
async uploadIngot(vault, options) {
|
|
9642
9761
|
this.validateUploadOptions(options);
|
|
9643
|
-
const
|
|
9644
|
-
const
|
|
9762
|
+
const requestedName = options.name ?? getBlobName(options.file);
|
|
9763
|
+
const requestedContentType = options.contentType ?? options.file.type ?? 'application/octet-stream';
|
|
9645
9764
|
const createResponse = await this.http.post(`/v1/vaults/${vault.id}/ingots`, {
|
|
9646
|
-
name,
|
|
9647
|
-
content_type:
|
|
9765
|
+
name: requestedName,
|
|
9766
|
+
content_type: requestedContentType,
|
|
9648
9767
|
size_bytes: options.file.size,
|
|
9649
9768
|
}, {
|
|
9650
|
-
headers: {
|
|
9769
|
+
headers: { 'X-Vault-Access-Token': vault.vatToken },
|
|
9651
9770
|
});
|
|
9771
|
+
const created = createResponse.data;
|
|
9772
|
+
if (!created.forge_url) {
|
|
9773
|
+
throw new ValidationError('Upload endpoint did not return a Forge upload URL');
|
|
9774
|
+
}
|
|
9775
|
+
await uploadBlobWithTus(options.file, created.forge_url, created.name, created.content_type, options.onProgress);
|
|
9652
9776
|
return {
|
|
9653
|
-
id:
|
|
9654
|
-
name:
|
|
9655
|
-
contentType:
|
|
9656
|
-
size:
|
|
9657
|
-
|
|
9658
|
-
type: createResponse.data.ingot_type,
|
|
9777
|
+
id: created.ingot_id,
|
|
9778
|
+
name: created.name,
|
|
9779
|
+
contentType: created.content_type,
|
|
9780
|
+
size: created.size_bytes,
|
|
9781
|
+
uploadExpiresAt: created.expires_at,
|
|
9659
9782
|
};
|
|
9660
9783
|
}
|
|
9661
9784
|
/**
|
|
@@ -9667,10 +9790,14 @@ class VaultsModule {
|
|
|
9667
9790
|
*/
|
|
9668
9791
|
async downloadIngot(vault, ingotId) {
|
|
9669
9792
|
const cleanId = ingotId.startsWith('ing_') ? ingotId : `ing_${ingotId}`;
|
|
9670
|
-
|
|
9671
|
-
|
|
9672
|
-
headers: { Authorization: `Bearer ${vault.vatToken}` },
|
|
9793
|
+
const response = await this.http.post(`/v1/vaults/${vault.id}/ingots/${cleanId}/download`, undefined, {
|
|
9794
|
+
headers: { 'X-Vault-Access-Token': vault.vatToken },
|
|
9673
9795
|
});
|
|
9796
|
+
if (!response.data.download_url) {
|
|
9797
|
+
throw new ValidationError('Download endpoint did not return a download URL');
|
|
9798
|
+
}
|
|
9799
|
+
this.validateDownloadUrl(response.data.download_url);
|
|
9800
|
+
return fetchBlobWithTimeout(response.data.download_url);
|
|
9674
9801
|
}
|
|
9675
9802
|
/**
|
|
9676
9803
|
* List all ingots in an unsealed vault.
|
|
@@ -9681,7 +9808,7 @@ class VaultsModule {
|
|
|
9681
9808
|
*/
|
|
9682
9809
|
async listIngots(vault) {
|
|
9683
9810
|
const response = await this.http.get(`/v1/vaults/${vault.id}/ingots`, {
|
|
9684
|
-
headers: {
|
|
9811
|
+
headers: { 'X-Vault-Access-Token': vault.vatToken },
|
|
9685
9812
|
});
|
|
9686
9813
|
return response.data.ingots.map((i) => ({
|
|
9687
9814
|
id: i.ingot_id,
|
|
@@ -9689,7 +9816,6 @@ class VaultsModule {
|
|
|
9689
9816
|
contentType: i.content_type,
|
|
9690
9817
|
size: i.size_bytes,
|
|
9691
9818
|
createdAt: i.created_at,
|
|
9692
|
-
type: i.ingot_type,
|
|
9693
9819
|
}));
|
|
9694
9820
|
}
|
|
9695
9821
|
/**
|
|
@@ -9701,7 +9827,7 @@ class VaultsModule {
|
|
|
9701
9827
|
async deleteIngot(vault, ingotId) {
|
|
9702
9828
|
const cleanId = ingotId.startsWith('ing_') ? ingotId : `ing_${ingotId}`;
|
|
9703
9829
|
await this.http.delete(`/v1/vaults/${vault.id}/ingots/${cleanId}`, {
|
|
9704
|
-
headers: {
|
|
9830
|
+
headers: { 'X-Vault-Access-Token': vault.vatToken },
|
|
9705
9831
|
});
|
|
9706
9832
|
}
|
|
9707
9833
|
validateCreateOptions(options) {
|
|
@@ -9720,6 +9846,117 @@ class VaultsModule {
|
|
|
9720
9846
|
throw new ValidationError('File cannot be empty');
|
|
9721
9847
|
}
|
|
9722
9848
|
}
|
|
9849
|
+
validateDownloadUrl(downloadUrl) {
|
|
9850
|
+
let parsed;
|
|
9851
|
+
try {
|
|
9852
|
+
parsed = new URL(downloadUrl);
|
|
9853
|
+
}
|
|
9854
|
+
catch {
|
|
9855
|
+
throw new ValidationError('Invalid download URL from server');
|
|
9856
|
+
}
|
|
9857
|
+
if (parsed.protocol !== 'https:') {
|
|
9858
|
+
throw new ValidationError('Download URL must use HTTPS');
|
|
9859
|
+
}
|
|
9860
|
+
const allowedHosts = this.config.allowedDownloadHostPatterns;
|
|
9861
|
+
if (!allowedHosts?.length) {
|
|
9862
|
+
return;
|
|
9863
|
+
}
|
|
9864
|
+
if (!allowedHosts.some(pattern => pattern.test(parsed.hostname))) {
|
|
9865
|
+
throw new ValidationError('Invalid download URL from server');
|
|
9866
|
+
}
|
|
9867
|
+
}
|
|
9868
|
+
}
|
|
9869
|
+
function getBlobName(file) {
|
|
9870
|
+
if (typeof File !== 'undefined' && file instanceof File && file.name) {
|
|
9871
|
+
return file.name;
|
|
9872
|
+
}
|
|
9873
|
+
return 'unnamed';
|
|
9874
|
+
}
|
|
9875
|
+
function base64Encode(value) {
|
|
9876
|
+
return btoa(unescape(encodeURIComponent(value)));
|
|
9877
|
+
}
|
|
9878
|
+
async function uploadBlobWithTus(file, forgeUrl, filename, contentType, onProgress) {
|
|
9879
|
+
if (typeof XMLHttpRequest === 'undefined') {
|
|
9880
|
+
throw new ValidationError('XMLHttpRequest is required for browser ingot uploads');
|
|
9881
|
+
}
|
|
9882
|
+
const tusVersion = '1.0.0';
|
|
9883
|
+
const defaultChunkSize = 50 * 1024 * 1024;
|
|
9884
|
+
const url = new URL(forgeUrl);
|
|
9885
|
+
const istk = url.searchParams.get('istk');
|
|
9886
|
+
const endpoint = `${url.origin}${url.pathname}`;
|
|
9887
|
+
if (!istk) {
|
|
9888
|
+
throw new ValidationError('Forge upload URL is missing ISTK');
|
|
9889
|
+
}
|
|
9890
|
+
const createResponse = await fetch(endpoint, {
|
|
9891
|
+
method: 'POST',
|
|
9892
|
+
headers: {
|
|
9893
|
+
'Tus-Resumable': tusVersion,
|
|
9894
|
+
'Upload-Length': String(file.size),
|
|
9895
|
+
'Upload-Metadata': `filename ${base64Encode(filename)},filetype ${base64Encode(contentType)}`,
|
|
9896
|
+
'X-ISTK': istk,
|
|
9897
|
+
},
|
|
9898
|
+
});
|
|
9899
|
+
if (!createResponse.ok) {
|
|
9900
|
+
const errorText = await createResponse.text();
|
|
9901
|
+
throw new Error(`Failed to create upload session: ${createResponse.status} ${errorText}`);
|
|
9902
|
+
}
|
|
9903
|
+
const location = createResponse.headers.get('Location');
|
|
9904
|
+
const chunkSizeHeader = createResponse.headers.get('X-Chunk-Size');
|
|
9905
|
+
const chunkSize = chunkSizeHeader ? parseInt(chunkSizeHeader, 10) : defaultChunkSize;
|
|
9906
|
+
if (!location) {
|
|
9907
|
+
throw new Error('Server did not return upload location');
|
|
9908
|
+
}
|
|
9909
|
+
const uploadUrl = location.startsWith('http') ? location : `${url.origin}${location}`;
|
|
9910
|
+
let offset = 0;
|
|
9911
|
+
while (offset < file.size) {
|
|
9912
|
+
const chunkStart = offset;
|
|
9913
|
+
const chunkEnd = Math.min(offset + chunkSize, file.size);
|
|
9914
|
+
const chunk = file.slice(chunkStart, chunkEnd);
|
|
9915
|
+
offset = await uploadChunkWithProgress(uploadUrl, chunk, chunkStart, file.size, tusVersion, istk, onProgress);
|
|
9916
|
+
}
|
|
9917
|
+
}
|
|
9918
|
+
function uploadChunkWithProgress(uploadUrl, chunk, chunkStart, totalSize, tusVersion, istk, onProgress) {
|
|
9919
|
+
return new Promise((resolve, reject) => {
|
|
9920
|
+
const xhr = new XMLHttpRequest();
|
|
9921
|
+
xhr.upload.onprogress = (event) => {
|
|
9922
|
+
if (event.lengthComputable) {
|
|
9923
|
+
onProgress?.(chunkStart + event.loaded, totalSize);
|
|
9924
|
+
}
|
|
9925
|
+
};
|
|
9926
|
+
xhr.onload = () => {
|
|
9927
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
9928
|
+
const newOffsetHeader = xhr.getResponseHeader('Upload-Offset');
|
|
9929
|
+
const newOffset = newOffsetHeader ? parseInt(newOffsetHeader, 10) : chunkStart + chunk.size;
|
|
9930
|
+
resolve(newOffset);
|
|
9931
|
+
}
|
|
9932
|
+
else {
|
|
9933
|
+
reject(new Error(`Chunk upload failed with status ${xhr.status}`));
|
|
9934
|
+
}
|
|
9935
|
+
};
|
|
9936
|
+
xhr.onerror = () => reject(new Error('Chunk upload failed'));
|
|
9937
|
+
xhr.ontimeout = () => reject(new Error('Chunk upload timed out'));
|
|
9938
|
+
xhr.onabort = () => reject(new Error('Upload cancelled'));
|
|
9939
|
+
xhr.open('PATCH', uploadUrl);
|
|
9940
|
+
xhr.setRequestHeader('Tus-Resumable', tusVersion);
|
|
9941
|
+
xhr.setRequestHeader('Upload-Offset', String(chunkStart));
|
|
9942
|
+
xhr.setRequestHeader('Content-Type', 'application/offset+octet-stream');
|
|
9943
|
+
xhr.setRequestHeader('X-ISTK', istk);
|
|
9944
|
+
xhr.send(chunk);
|
|
9945
|
+
});
|
|
9946
|
+
}
|
|
9947
|
+
async function fetchBlobWithTimeout(url, timeout = 300000) {
|
|
9948
|
+
const controller = new AbortController();
|
|
9949
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
9950
|
+
try {
|
|
9951
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
9952
|
+
if (!response.ok) {
|
|
9953
|
+
throw new Error(`Download failed with status ${response.status}`);
|
|
9954
|
+
}
|
|
9955
|
+
return response.blob();
|
|
9956
|
+
}
|
|
9957
|
+
finally {
|
|
9958
|
+
clearTimeout(timeoutId);
|
|
9959
|
+
}
|
|
9723
9960
|
}
|
|
9724
9961
|
|
|
9725
9962
|
/**
|
|
@@ -9906,6 +10143,7 @@ class SparkVault {
|
|
|
9906
10143
|
const http = new HttpClient(this.config);
|
|
9907
10144
|
this.identity = new IdentityModule(this.config);
|
|
9908
10145
|
this.vaults = new VaultsModule(this.config, http);
|
|
10146
|
+
this.health = new HealthModule(this.config);
|
|
9909
10147
|
}
|
|
9910
10148
|
/**
|
|
9911
10149
|
* Initialize the SparkVault SDK.
|