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