@sparkvault/sdk 1.24.1 → 1.24.3
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 +20 -0
- package/dist/health.d.ts +18 -0
- package/dist/http.d.ts +3 -0
- package/dist/index.d.ts +66 -11
- package/dist/sparkvault.cjs.js +300 -39
- package/dist/sparkvault.cjs.js.map +1 -1
- package/dist/sparkvault.esm.js +300 -39
- 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
|
@@ -64,16 +64,32 @@ class PopupBlockedError extends SparkVaultError {
|
|
|
64
64
|
/**
|
|
65
65
|
* SparkVault SDK Configuration
|
|
66
66
|
*/
|
|
67
|
+
const DEFAULT_ALLOWED_DOWNLOAD_HOST_PATTERNS = [
|
|
68
|
+
/\.sparkvault\.(com|io)$/i,
|
|
69
|
+
/\.amazonaws\.com$/i,
|
|
70
|
+
/\.cloudfront\.net$/i,
|
|
71
|
+
];
|
|
67
72
|
const API_URL = 'https://api.sparkvault.com';
|
|
68
73
|
const IDENTITY_URL = 'https://api.sparkvault.com/v1/apps/identity';
|
|
74
|
+
function normalizeApiBaseUrl(url) {
|
|
75
|
+
const trimmed = url.replace(/\/+$/, '');
|
|
76
|
+
return trimmed.endsWith('/v1') ? trimmed.slice(0, -3) : trimmed;
|
|
77
|
+
}
|
|
78
|
+
function normalizeBaseUrl(url) {
|
|
79
|
+
return url.replace(/\/+$/, '');
|
|
80
|
+
}
|
|
69
81
|
function resolveConfig(config) {
|
|
70
82
|
return {
|
|
71
83
|
accountId: config.accountId,
|
|
72
84
|
timeout: config.timeout ?? 30000,
|
|
73
|
-
apiBaseUrl: API_URL,
|
|
74
|
-
identityBaseUrl: IDENTITY_URL,
|
|
85
|
+
apiBaseUrl: normalizeApiBaseUrl(config.apiBaseUrl ?? API_URL),
|
|
86
|
+
identityBaseUrl: normalizeBaseUrl(config.identityBaseUrl ?? IDENTITY_URL),
|
|
87
|
+
accessToken: config.accessToken,
|
|
88
|
+
getAccessToken: config.getAccessToken,
|
|
89
|
+
apiKey: config.apiKey,
|
|
75
90
|
preloadConfig: config.preloadConfig !== false, // Default: true
|
|
76
91
|
backdropBlur: config.backdropBlur !== false, // Default: true
|
|
92
|
+
allowedDownloadHostPatterns: config.allowedDownloadHostPatterns ?? DEFAULT_ALLOWED_DOWNLOAD_HOST_PATTERNS,
|
|
77
93
|
};
|
|
78
94
|
}
|
|
79
95
|
function validateConfig(config) {
|
|
@@ -190,6 +206,31 @@ class HttpClient {
|
|
|
190
206
|
constructor(config) {
|
|
191
207
|
this.config = config;
|
|
192
208
|
}
|
|
209
|
+
async getDefaultHeaders(headers, hasJsonBody) {
|
|
210
|
+
const requestHeaders = {
|
|
211
|
+
Accept: 'application/json',
|
|
212
|
+
...headers,
|
|
213
|
+
};
|
|
214
|
+
if (hasJsonBody) {
|
|
215
|
+
requestHeaders['Content-Type'] = 'application/json';
|
|
216
|
+
}
|
|
217
|
+
if (!requestHeaders.Authorization) {
|
|
218
|
+
const token = await this.getAccessToken();
|
|
219
|
+
if (token) {
|
|
220
|
+
requestHeaders.Authorization = `Bearer ${token}`;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (this.config.apiKey && !requestHeaders['X-API-Key']) {
|
|
224
|
+
requestHeaders['X-API-Key'] = this.config.apiKey;
|
|
225
|
+
}
|
|
226
|
+
return requestHeaders;
|
|
227
|
+
}
|
|
228
|
+
async getAccessToken() {
|
|
229
|
+
if (this.config.getAccessToken) {
|
|
230
|
+
return this.config.getAccessToken();
|
|
231
|
+
}
|
|
232
|
+
return this.config.accessToken ?? null;
|
|
233
|
+
}
|
|
193
234
|
async request(path, options = {}) {
|
|
194
235
|
const { retry } = options;
|
|
195
236
|
// Per CLAUDE.md §7: Wrap with retry logic if enabled
|
|
@@ -209,11 +250,7 @@ class HttpClient {
|
|
|
209
250
|
async executeRequest(path, options) {
|
|
210
251
|
const { method = 'GET', headers = {}, body, timeout = this.config.timeout, } = options;
|
|
211
252
|
const url = `${this.config.apiBaseUrl}${path}`;
|
|
212
|
-
const requestHeaders =
|
|
213
|
-
'Content-Type': 'application/json',
|
|
214
|
-
Accept: 'application/json',
|
|
215
|
-
...headers,
|
|
216
|
-
};
|
|
253
|
+
const requestHeaders = await this.getDefaultHeaders(headers, body !== undefined);
|
|
217
254
|
const controller = new AbortController();
|
|
218
255
|
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
219
256
|
try {
|
|
@@ -224,12 +261,12 @@ class HttpClient {
|
|
|
224
261
|
signal: controller.signal,
|
|
225
262
|
});
|
|
226
263
|
clearTimeout(timeoutId);
|
|
227
|
-
const
|
|
264
|
+
const parsed = await this.parseResponse(response);
|
|
228
265
|
if (!response.ok) {
|
|
229
|
-
throw this.createErrorFromResponse(response.status,
|
|
266
|
+
throw this.createErrorFromResponse(response.status, parsed);
|
|
230
267
|
}
|
|
231
268
|
return {
|
|
232
|
-
data,
|
|
269
|
+
data: this.unwrapApiResponse(parsed),
|
|
233
270
|
status: response.status,
|
|
234
271
|
headers: response.headers,
|
|
235
272
|
};
|
|
@@ -283,13 +320,23 @@ class HttpClient {
|
|
|
283
320
|
// Safely extract error fields with runtime type checking
|
|
284
321
|
const errorData = typeof data === 'object' && data !== null ? data : {};
|
|
285
322
|
const record = errorData;
|
|
286
|
-
const
|
|
323
|
+
const nestedError = typeof record.error === 'object' && record.error !== null
|
|
324
|
+
? record.error
|
|
325
|
+
: null;
|
|
326
|
+
const message = (typeof nestedError?.message === 'string' ? nestedError.message : null) ??
|
|
327
|
+
(typeof record.message === 'string' ? record.message : null) ??
|
|
287
328
|
(typeof record.error === 'string' ? record.error : null) ??
|
|
288
329
|
'Request failed';
|
|
289
|
-
const code = typeof
|
|
290
|
-
|
|
330
|
+
const code = (typeof nestedError?.code === 'string' ? nestedError.code : null) ??
|
|
331
|
+
(typeof record.code === 'string' ? record.code : null) ??
|
|
332
|
+
'api_error';
|
|
333
|
+
const nestedDetails = typeof nestedError?.details === 'object' && nestedError.details !== null
|
|
334
|
+
? nestedError.details
|
|
335
|
+
: undefined;
|
|
336
|
+
const topLevelDetails = typeof record.details === 'object' && record.details !== null
|
|
291
337
|
? record.details
|
|
292
338
|
: undefined;
|
|
339
|
+
const details = nestedDetails ?? topLevelDetails;
|
|
293
340
|
switch (status) {
|
|
294
341
|
case 400:
|
|
295
342
|
return new ValidationError(message, details);
|
|
@@ -301,6 +348,15 @@ class HttpClient {
|
|
|
301
348
|
return new SparkVaultError(message, code, status, details);
|
|
302
349
|
}
|
|
303
350
|
}
|
|
351
|
+
unwrapApiResponse(data) {
|
|
352
|
+
if (typeof data === 'object' && data !== null) {
|
|
353
|
+
const record = data;
|
|
354
|
+
if ('data' in record && ('meta' in record || 'error' in record)) {
|
|
355
|
+
return record.data;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return data;
|
|
359
|
+
}
|
|
304
360
|
get(path, options) {
|
|
305
361
|
return this.request(path, { ...options, method: 'GET' });
|
|
306
362
|
}
|
|
@@ -338,10 +394,7 @@ class HttpClient {
|
|
|
338
394
|
async executeRequestRaw(path, options) {
|
|
339
395
|
const { method = 'GET', headers = {}, body, timeout = this.config.timeout, } = options;
|
|
340
396
|
const url = `${this.config.apiBaseUrl}${path}`;
|
|
341
|
-
const requestHeaders =
|
|
342
|
-
'Content-Type': 'application/json',
|
|
343
|
-
...headers,
|
|
344
|
-
};
|
|
397
|
+
const requestHeaders = await this.getDefaultHeaders(headers, body !== undefined);
|
|
345
398
|
const controller = new AbortController();
|
|
346
399
|
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
347
400
|
try {
|
|
@@ -375,6 +428,63 @@ class HttpClient {
|
|
|
375
428
|
}
|
|
376
429
|
}
|
|
377
430
|
|
|
431
|
+
class HealthModule {
|
|
432
|
+
constructor(config) {
|
|
433
|
+
this.config = config;
|
|
434
|
+
}
|
|
435
|
+
async check(options = {}) {
|
|
436
|
+
const checkedAt = Math.floor(Date.now() / 1000);
|
|
437
|
+
const controller = new AbortController();
|
|
438
|
+
const timeoutId = setTimeout(() => controller.abort(), options.timeout ?? this.config.timeout);
|
|
439
|
+
try {
|
|
440
|
+
const response = await fetch(`${this.config.apiBaseUrl}/health`, {
|
|
441
|
+
method: 'GET',
|
|
442
|
+
headers: { Accept: 'application/json' },
|
|
443
|
+
signal: controller.signal,
|
|
444
|
+
});
|
|
445
|
+
return {
|
|
446
|
+
online: response.ok,
|
|
447
|
+
status: response.ok ? await this.readStatus(response) : 'unhealthy',
|
|
448
|
+
httpStatus: response.status,
|
|
449
|
+
checkedAt,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
catch (err) {
|
|
453
|
+
return {
|
|
454
|
+
online: false,
|
|
455
|
+
status: 'unreachable',
|
|
456
|
+
checkedAt,
|
|
457
|
+
error: err instanceof Error ? err.message : String(err),
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
finally {
|
|
461
|
+
clearTimeout(timeoutId);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
async isOnline(options = {}) {
|
|
465
|
+
const result = await this.check(options);
|
|
466
|
+
return result.online;
|
|
467
|
+
}
|
|
468
|
+
async readStatus(response) {
|
|
469
|
+
try {
|
|
470
|
+
const body = await response.json();
|
|
471
|
+
const data = typeof body === 'object' && body !== null && 'data' in body
|
|
472
|
+
? body.data
|
|
473
|
+
: body;
|
|
474
|
+
if (typeof data === 'object' && data !== null) {
|
|
475
|
+
const status = data.status;
|
|
476
|
+
if (typeof status === 'string' && status) {
|
|
477
|
+
return status;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
catch {
|
|
482
|
+
return response.ok ? 'healthy' : 'unhealthy';
|
|
483
|
+
}
|
|
484
|
+
return response.ok ? 'healthy' : 'unhealthy';
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
378
488
|
/**
|
|
379
489
|
* SparkVault API Base
|
|
380
490
|
*
|
|
@@ -9549,6 +9659,7 @@ class VaultUploadModule {
|
|
|
9549
9659
|
*/
|
|
9550
9660
|
class VaultsModule {
|
|
9551
9661
|
constructor(config, http) {
|
|
9662
|
+
this.config = config;
|
|
9552
9663
|
this.http = http;
|
|
9553
9664
|
this.uploadModule = new VaultUploadModule(config);
|
|
9554
9665
|
// Create callable that also has attach/close methods
|
|
@@ -9596,13 +9707,15 @@ class VaultsModule {
|
|
|
9596
9707
|
}
|
|
9597
9708
|
const cleanId = vaultId.startsWith('vlt_') ? vaultId : `vlt_${vaultId}`;
|
|
9598
9709
|
const response = await this.http.post(`/v1/vaults/${cleanId}/unseal`, { vmk });
|
|
9710
|
+
if (!response.data.vat) {
|
|
9711
|
+
throw new ValidationError('Unseal response did not include a vault access token');
|
|
9712
|
+
}
|
|
9599
9713
|
return {
|
|
9600
9714
|
id: response.data.vault_id,
|
|
9601
|
-
|
|
9602
|
-
|
|
9715
|
+
vatToken: response.data.vat,
|
|
9716
|
+
issuedAt: response.data.issued_at,
|
|
9603
9717
|
expiresAt: response.data.expires_at,
|
|
9604
|
-
|
|
9605
|
-
storageBytes: response.data.storage_bytes,
|
|
9718
|
+
ttlSeconds: response.data.ttl_seconds,
|
|
9606
9719
|
};
|
|
9607
9720
|
}
|
|
9608
9721
|
/**
|
|
@@ -9647,22 +9760,26 @@ class VaultsModule {
|
|
|
9647
9760
|
*/
|
|
9648
9761
|
async uploadIngot(vault, options) {
|
|
9649
9762
|
this.validateUploadOptions(options);
|
|
9650
|
-
const
|
|
9651
|
-
const
|
|
9763
|
+
const requestedName = options.name ?? getBlobName(options.file);
|
|
9764
|
+
const requestedContentType = options.contentType ?? options.file.type ?? 'application/octet-stream';
|
|
9652
9765
|
const createResponse = await this.http.post(`/v1/vaults/${vault.id}/ingots`, {
|
|
9653
|
-
name,
|
|
9654
|
-
content_type:
|
|
9766
|
+
name: requestedName,
|
|
9767
|
+
content_type: requestedContentType,
|
|
9655
9768
|
size_bytes: options.file.size,
|
|
9656
9769
|
}, {
|
|
9657
|
-
headers: {
|
|
9770
|
+
headers: { 'X-Vault-Access-Token': vault.vatToken },
|
|
9658
9771
|
});
|
|
9772
|
+
const created = createResponse.data;
|
|
9773
|
+
if (!created.forge_url) {
|
|
9774
|
+
throw new ValidationError('Upload endpoint did not return a Forge upload URL');
|
|
9775
|
+
}
|
|
9776
|
+
await uploadBlobWithTus(options.file, created.forge_url, created.name, created.content_type, options.onProgress);
|
|
9659
9777
|
return {
|
|
9660
|
-
id:
|
|
9661
|
-
name:
|
|
9662
|
-
contentType:
|
|
9663
|
-
size:
|
|
9664
|
-
|
|
9665
|
-
type: createResponse.data.ingot_type,
|
|
9778
|
+
id: created.ingot_id,
|
|
9779
|
+
name: created.name,
|
|
9780
|
+
contentType: created.content_type,
|
|
9781
|
+
size: created.size_bytes,
|
|
9782
|
+
uploadExpiresAt: created.expires_at,
|
|
9666
9783
|
};
|
|
9667
9784
|
}
|
|
9668
9785
|
/**
|
|
@@ -9674,10 +9791,27 @@ class VaultsModule {
|
|
|
9674
9791
|
*/
|
|
9675
9792
|
async downloadIngot(vault, ingotId) {
|
|
9676
9793
|
const cleanId = ingotId.startsWith('ing_') ? ingotId : `ing_${ingotId}`;
|
|
9677
|
-
|
|
9678
|
-
|
|
9679
|
-
|
|
9680
|
-
|
|
9794
|
+
let lastError = null;
|
|
9795
|
+
for (let attempt = 1; attempt <= DOWNLOAD_MAX_ATTEMPTS; attempt++) {
|
|
9796
|
+
try {
|
|
9797
|
+
const response = await this.http.post(`/v1/vaults/${vault.id}/ingots/${cleanId}/download`, undefined, {
|
|
9798
|
+
headers: { 'X-Vault-Access-Token': vault.vatToken },
|
|
9799
|
+
});
|
|
9800
|
+
if (!response.data.download_url) {
|
|
9801
|
+
throw new ValidationError('Download endpoint did not return a download URL');
|
|
9802
|
+
}
|
|
9803
|
+
this.validateDownloadUrl(response.data.download_url);
|
|
9804
|
+
return await fetchBlobWithTimeout(response.data.download_url);
|
|
9805
|
+
}
|
|
9806
|
+
catch (err) {
|
|
9807
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
9808
|
+
if (!isRetryableDownloadError(lastError) || attempt === DOWNLOAD_MAX_ATTEMPTS) {
|
|
9809
|
+
throw lastError;
|
|
9810
|
+
}
|
|
9811
|
+
await delayExponential(attempt);
|
|
9812
|
+
}
|
|
9813
|
+
}
|
|
9814
|
+
throw lastError ?? new Error('Download failed');
|
|
9681
9815
|
}
|
|
9682
9816
|
/**
|
|
9683
9817
|
* List all ingots in an unsealed vault.
|
|
@@ -9688,7 +9822,7 @@ class VaultsModule {
|
|
|
9688
9822
|
*/
|
|
9689
9823
|
async listIngots(vault) {
|
|
9690
9824
|
const response = await this.http.get(`/v1/vaults/${vault.id}/ingots`, {
|
|
9691
|
-
headers: {
|
|
9825
|
+
headers: { 'X-Vault-Access-Token': vault.vatToken },
|
|
9692
9826
|
});
|
|
9693
9827
|
return response.data.ingots.map((i) => ({
|
|
9694
9828
|
id: i.ingot_id,
|
|
@@ -9696,7 +9830,6 @@ class VaultsModule {
|
|
|
9696
9830
|
contentType: i.content_type,
|
|
9697
9831
|
size: i.size_bytes,
|
|
9698
9832
|
createdAt: i.created_at,
|
|
9699
|
-
type: i.ingot_type,
|
|
9700
9833
|
}));
|
|
9701
9834
|
}
|
|
9702
9835
|
/**
|
|
@@ -9708,7 +9841,7 @@ class VaultsModule {
|
|
|
9708
9841
|
async deleteIngot(vault, ingotId) {
|
|
9709
9842
|
const cleanId = ingotId.startsWith('ing_') ? ingotId : `ing_${ingotId}`;
|
|
9710
9843
|
await this.http.delete(`/v1/vaults/${vault.id}/ingots/${cleanId}`, {
|
|
9711
|
-
headers: {
|
|
9844
|
+
headers: { 'X-Vault-Access-Token': vault.vatToken },
|
|
9712
9845
|
});
|
|
9713
9846
|
}
|
|
9714
9847
|
validateCreateOptions(options) {
|
|
@@ -9727,6 +9860,133 @@ class VaultsModule {
|
|
|
9727
9860
|
throw new ValidationError('File cannot be empty');
|
|
9728
9861
|
}
|
|
9729
9862
|
}
|
|
9863
|
+
validateDownloadUrl(downloadUrl) {
|
|
9864
|
+
let parsed;
|
|
9865
|
+
try {
|
|
9866
|
+
parsed = new URL(downloadUrl);
|
|
9867
|
+
}
|
|
9868
|
+
catch {
|
|
9869
|
+
throw new ValidationError('Invalid download URL from server');
|
|
9870
|
+
}
|
|
9871
|
+
if (parsed.protocol !== 'https:') {
|
|
9872
|
+
throw new ValidationError('Download URL must use HTTPS');
|
|
9873
|
+
}
|
|
9874
|
+
const allowedHosts = this.config.allowedDownloadHostPatterns;
|
|
9875
|
+
if (!allowedHosts.some(pattern => pattern.test(parsed.hostname))) {
|
|
9876
|
+
throw new ValidationError('Invalid download URL from server');
|
|
9877
|
+
}
|
|
9878
|
+
}
|
|
9879
|
+
}
|
|
9880
|
+
function getBlobName(file) {
|
|
9881
|
+
if (typeof File !== 'undefined' && file instanceof File && file.name) {
|
|
9882
|
+
return file.name;
|
|
9883
|
+
}
|
|
9884
|
+
return 'unnamed';
|
|
9885
|
+
}
|
|
9886
|
+
function base64Encode(value) {
|
|
9887
|
+
return btoa(unescape(encodeURIComponent(value)));
|
|
9888
|
+
}
|
|
9889
|
+
async function uploadBlobWithTus(file, forgeUrl, filename, contentType, onProgress) {
|
|
9890
|
+
if (typeof XMLHttpRequest === 'undefined') {
|
|
9891
|
+
throw new ValidationError('XMLHttpRequest is required for browser ingot uploads');
|
|
9892
|
+
}
|
|
9893
|
+
const tusVersion = '1.0.0';
|
|
9894
|
+
const defaultChunkSize = 50 * 1024 * 1024;
|
|
9895
|
+
const url = new URL(forgeUrl);
|
|
9896
|
+
const istk = url.searchParams.get('istk');
|
|
9897
|
+
const endpoint = `${url.origin}${url.pathname}`;
|
|
9898
|
+
if (!istk) {
|
|
9899
|
+
throw new ValidationError('Forge upload URL is missing ISTK');
|
|
9900
|
+
}
|
|
9901
|
+
const createResponse = await fetch(endpoint, {
|
|
9902
|
+
method: 'POST',
|
|
9903
|
+
headers: {
|
|
9904
|
+
'Tus-Resumable': tusVersion,
|
|
9905
|
+
'Upload-Length': String(file.size),
|
|
9906
|
+
'Upload-Metadata': `filename ${base64Encode(filename)},filetype ${base64Encode(contentType)}`,
|
|
9907
|
+
'X-ISTK': istk,
|
|
9908
|
+
},
|
|
9909
|
+
});
|
|
9910
|
+
if (!createResponse.ok) {
|
|
9911
|
+
const errorText = await createResponse.text();
|
|
9912
|
+
throw new Error(`Failed to create upload session: ${createResponse.status} ${errorText}`);
|
|
9913
|
+
}
|
|
9914
|
+
const location = createResponse.headers.get('Location');
|
|
9915
|
+
const chunkSizeHeader = createResponse.headers.get('X-Chunk-Size');
|
|
9916
|
+
const chunkSize = chunkSizeHeader ? parseInt(chunkSizeHeader, 10) : defaultChunkSize;
|
|
9917
|
+
if (!location) {
|
|
9918
|
+
throw new Error('Server did not return upload location');
|
|
9919
|
+
}
|
|
9920
|
+
const uploadUrl = location.startsWith('http') ? location : `${url.origin}${location}`;
|
|
9921
|
+
let offset = 0;
|
|
9922
|
+
while (offset < file.size) {
|
|
9923
|
+
const chunkStart = offset;
|
|
9924
|
+
const chunkEnd = Math.min(offset + chunkSize, file.size);
|
|
9925
|
+
const chunk = file.slice(chunkStart, chunkEnd);
|
|
9926
|
+
offset = await uploadChunkWithProgress(uploadUrl, chunk, chunkStart, file.size, tusVersion, istk, onProgress);
|
|
9927
|
+
}
|
|
9928
|
+
}
|
|
9929
|
+
function uploadChunkWithProgress(uploadUrl, chunk, chunkStart, totalSize, tusVersion, istk, onProgress) {
|
|
9930
|
+
return new Promise((resolve, reject) => {
|
|
9931
|
+
const xhr = new XMLHttpRequest();
|
|
9932
|
+
xhr.upload.onprogress = (event) => {
|
|
9933
|
+
if (event.lengthComputable) {
|
|
9934
|
+
onProgress?.(chunkStart + event.loaded, totalSize);
|
|
9935
|
+
}
|
|
9936
|
+
};
|
|
9937
|
+
xhr.onload = () => {
|
|
9938
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
9939
|
+
const newOffsetHeader = xhr.getResponseHeader('Upload-Offset');
|
|
9940
|
+
const newOffset = newOffsetHeader ? parseInt(newOffsetHeader, 10) : chunkStart + chunk.size;
|
|
9941
|
+
resolve(newOffset);
|
|
9942
|
+
}
|
|
9943
|
+
else {
|
|
9944
|
+
reject(new Error(`Chunk upload failed with status ${xhr.status}`));
|
|
9945
|
+
}
|
|
9946
|
+
};
|
|
9947
|
+
xhr.onerror = () => reject(new Error('Chunk upload failed'));
|
|
9948
|
+
xhr.ontimeout = () => reject(new Error('Chunk upload timed out'));
|
|
9949
|
+
xhr.onabort = () => reject(new Error('Upload cancelled'));
|
|
9950
|
+
xhr.open('PATCH', uploadUrl);
|
|
9951
|
+
xhr.setRequestHeader('Tus-Resumable', tusVersion);
|
|
9952
|
+
xhr.setRequestHeader('Upload-Offset', String(chunkStart));
|
|
9953
|
+
xhr.setRequestHeader('Content-Type', 'application/offset+octet-stream');
|
|
9954
|
+
xhr.setRequestHeader('X-ISTK', istk);
|
|
9955
|
+
xhr.send(chunk);
|
|
9956
|
+
});
|
|
9957
|
+
}
|
|
9958
|
+
const DOWNLOAD_MAX_ATTEMPTS = 3;
|
|
9959
|
+
const DOWNLOAD_RETRY_BASE_MS = 500;
|
|
9960
|
+
function delayExponential(attempt) {
|
|
9961
|
+
return new Promise(resolve => setTimeout(resolve, DOWNLOAD_RETRY_BASE_MS * 2 ** (attempt - 1)));
|
|
9962
|
+
}
|
|
9963
|
+
function isRetryableDownloadError(err) {
|
|
9964
|
+
// Server contract / client-side validation errors aren't fixed by retrying.
|
|
9965
|
+
if (err instanceof ValidationError)
|
|
9966
|
+
return false;
|
|
9967
|
+
// Network errors carry a status code only when the SDK HTTP client attached
|
|
9968
|
+
// one; treat any explicitly-4xx response as a permanent failure.
|
|
9969
|
+
const status = err.statusCode;
|
|
9970
|
+
if (typeof status === 'number' && status >= 400 && status < 500 && status !== 408 && status !== 429) {
|
|
9971
|
+
return false;
|
|
9972
|
+
}
|
|
9973
|
+
return true;
|
|
9974
|
+
}
|
|
9975
|
+
async function fetchBlobWithTimeout(url, timeout = 300000) {
|
|
9976
|
+
const controller = new AbortController();
|
|
9977
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
9978
|
+
try {
|
|
9979
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
9980
|
+
if (!response.ok) {
|
|
9981
|
+
const error = new Error(`Download failed with status ${response.status}`);
|
|
9982
|
+
error.statusCode = response.status;
|
|
9983
|
+
throw error;
|
|
9984
|
+
}
|
|
9985
|
+
return response.blob();
|
|
9986
|
+
}
|
|
9987
|
+
finally {
|
|
9988
|
+
clearTimeout(timeoutId);
|
|
9989
|
+
}
|
|
9730
9990
|
}
|
|
9731
9991
|
|
|
9732
9992
|
/**
|
|
@@ -9913,6 +10173,7 @@ class SparkVault {
|
|
|
9913
10173
|
const http = new HttpClient(this.config);
|
|
9914
10174
|
this.identity = new IdentityModule(this.config);
|
|
9915
10175
|
this.vaults = new VaultsModule(this.config, http);
|
|
10176
|
+
this.health = new HealthModule(this.config);
|
|
9916
10177
|
}
|
|
9917
10178
|
/**
|
|
9918
10179
|
* Initialize the SparkVault SDK.
|