@sparkvault/sdk 1.24.1 → 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 CHANGED
@@ -4,6 +4,16 @@
4
4
  export interface SparkVaultConfig {
5
5
  /** Account ID for Identity operations */
6
6
  accountId: string;
7
+ /** Core API origin. Defaults to https://api.sparkvault.com. A trailing /v1 is accepted and normalized. */
8
+ apiBaseUrl?: string;
9
+ /** Identity App API base URL. Defaults to https://api.sparkvault.com/v1/apps/identity. */
10
+ identityBaseUrl?: string;
11
+ /** Static SparkVault Core access token for authenticated API calls. */
12
+ accessToken?: string;
13
+ /** Dynamic SparkVault Core access token provider for authenticated API calls. */
14
+ getAccessToken?: () => string | null | Promise<string | null>;
15
+ /** Optional API key for server-side SDK use. */
16
+ apiKey?: string;
7
17
  /** Request timeout in milliseconds (default: 30000) */
8
18
  timeout?: number;
9
19
  /**
@@ -15,14 +25,23 @@ export interface SparkVaultConfig {
15
25
  preloadConfig?: boolean;
16
26
  /** Enable backdrop blur on dialogs (default: true) */
17
27
  backdropBlur?: boolean;
28
+ /**
29
+ * Optional host allowlist for backend-issued ingot download URLs.
30
+ * When omitted, the SDK preserves the existing HTTPS-only validation behavior.
31
+ */
32
+ allowedDownloadHostPatterns?: RegExp[];
18
33
  }
19
34
  export interface ResolvedConfig {
20
35
  accountId: string;
21
36
  timeout: number;
22
37
  apiBaseUrl: string;
23
38
  identityBaseUrl: string;
39
+ accessToken?: string;
40
+ getAccessToken?: () => string | null | Promise<string | null>;
41
+ apiKey?: string;
24
42
  preloadConfig: boolean;
25
43
  backdropBlur: boolean;
44
+ allowedDownloadHostPatterns?: RegExp[];
26
45
  }
27
46
  export declare function resolveConfig(config: SparkVaultConfig): ResolvedConfig;
28
47
  export declare function validateConfig(config: SparkVaultConfig): void;
@@ -0,0 +1,18 @@
1
+ import type { ResolvedConfig } from './config';
2
+ export interface HealthCheckOptions {
3
+ timeout?: number;
4
+ }
5
+ export interface HealthCheckResult {
6
+ online: boolean;
7
+ status: string;
8
+ httpStatus?: number;
9
+ checkedAt: number;
10
+ error?: string;
11
+ }
12
+ export declare class HealthModule {
13
+ private readonly config;
14
+ constructor(config: ResolvedConfig);
15
+ check(options?: HealthCheckOptions): Promise<HealthCheckResult>;
16
+ isOnline(options?: HealthCheckOptions): Promise<boolean>;
17
+ private readStatus;
18
+ }
package/dist/http.d.ts CHANGED
@@ -19,6 +19,8 @@ export interface ApiResponse<T = unknown> {
19
19
  export declare class HttpClient {
20
20
  private readonly config;
21
21
  constructor(config: ResolvedConfig);
22
+ private getDefaultHeaders;
23
+ private getAccessToken;
22
24
  request<T = unknown>(path: string, options?: RequestOptions): Promise<ApiResponse<T>>;
23
25
  /**
24
26
  * Execute the actual HTTP request (internal implementation)
@@ -31,6 +33,7 @@ export declare class HttpClient {
31
33
  private isRetryableError;
32
34
  private parseResponse;
33
35
  private createErrorFromResponse;
36
+ private unwrapApiResponse;
34
37
  get<T = unknown>(path: string, options?: Omit<RequestOptions, 'method' | 'body'>): Promise<ApiResponse<T>>;
35
38
  post<T = unknown>(path: string, body?: unknown, options?: Omit<RequestOptions, 'method'>): Promise<ApiResponse<T>>;
36
39
  put<T = unknown>(path: string, body?: unknown, options?: Omit<RequestOptions, 'method'>): Promise<ApiResponse<T>>;
package/dist/index.d.ts CHANGED
@@ -4,6 +4,16 @@
4
4
  interface SparkVaultConfig {
5
5
  /** Account ID for Identity operations */
6
6
  accountId: string;
7
+ /** Core API origin. Defaults to https://api.sparkvault.com. A trailing /v1 is accepted and normalized. */
8
+ apiBaseUrl?: string;
9
+ /** Identity App API base URL. Defaults to https://api.sparkvault.com/v1/apps/identity. */
10
+ identityBaseUrl?: string;
11
+ /** Static SparkVault Core access token for authenticated API calls. */
12
+ accessToken?: string;
13
+ /** Dynamic SparkVault Core access token provider for authenticated API calls. */
14
+ getAccessToken?: () => string | null | Promise<string | null>;
15
+ /** Optional API key for server-side SDK use. */
16
+ apiKey?: string;
7
17
  /** Request timeout in milliseconds (default: 30000) */
8
18
  timeout?: number;
9
19
  /**
@@ -15,14 +25,41 @@ interface SparkVaultConfig {
15
25
  preloadConfig?: boolean;
16
26
  /** Enable backdrop blur on dialogs (default: true) */
17
27
  backdropBlur?: boolean;
28
+ /**
29
+ * Optional host allowlist for backend-issued ingot download URLs.
30
+ * When omitted, the SDK preserves the existing HTTPS-only validation behavior.
31
+ */
32
+ allowedDownloadHostPatterns?: RegExp[];
18
33
  }
19
34
  interface ResolvedConfig {
20
35
  accountId: string;
21
36
  timeout: number;
22
37
  apiBaseUrl: string;
23
38
  identityBaseUrl: string;
39
+ accessToken?: string;
40
+ getAccessToken?: () => string | null | Promise<string | null>;
41
+ apiKey?: string;
24
42
  preloadConfig: boolean;
25
43
  backdropBlur: boolean;
44
+ allowedDownloadHostPatterns?: RegExp[];
45
+ }
46
+
47
+ interface HealthCheckOptions {
48
+ timeout?: number;
49
+ }
50
+ interface HealthCheckResult {
51
+ online: boolean;
52
+ status: string;
53
+ httpStatus?: number;
54
+ checkedAt: number;
55
+ error?: string;
56
+ }
57
+ declare class HealthModule {
58
+ private readonly config;
59
+ constructor(config: ResolvedConfig);
60
+ check(options?: HealthCheckOptions): Promise<HealthCheckResult>;
61
+ isOnline(options?: HealthCheckOptions): Promise<boolean>;
62
+ private readStatus;
26
63
  }
27
64
 
28
65
  /**
@@ -272,6 +309,8 @@ interface ApiResponse<T = unknown> {
272
309
  declare class HttpClient {
273
310
  private readonly config;
274
311
  constructor(config: ResolvedConfig);
312
+ private getDefaultHeaders;
313
+ private getAccessToken;
275
314
  request<T = unknown>(path: string, options?: RequestOptions): Promise<ApiResponse<T>>;
276
315
  /**
277
316
  * Execute the actual HTTP request (internal implementation)
@@ -284,6 +323,7 @@ declare class HttpClient {
284
323
  private isRetryableError;
285
324
  private parseResponse;
286
325
  private createErrorFromResponse;
326
+ private unwrapApiResponse;
287
327
  get<T = unknown>(path: string, options?: Omit<RequestOptions, 'method' | 'body'>): Promise<ApiResponse<T>>;
288
328
  post<T = unknown>(path: string, body?: unknown, options?: Omit<RequestOptions, 'method'>): Promise<ApiResponse<T>>;
289
329
  put<T = unknown>(path: string, body?: unknown, options?: Omit<RequestOptions, 'method'>): Promise<ApiResponse<T>>;
@@ -324,16 +364,14 @@ interface Vault {
324
364
  interface UnsealedVault {
325
365
  /** Vault ID */
326
366
  id: string;
327
- /** Vault name */
328
- name: string;
329
- /** Vault Access Token (short-lived session) */
367
+ /** Vault Access Token (short-lived session, required for all ingot operations) */
330
368
  vatToken: string;
369
+ /** VAT issued-at timestamp (Unix seconds) */
370
+ issuedAt: number;
331
371
  /** VAT expiry timestamp (Unix seconds) */
332
372
  expiresAt: number;
333
- /** Number of ingots in vault */
334
- ingotCount: number;
335
- /** Total storage used in bytes */
336
- storageBytes: number;
373
+ /** VAT lifetime in seconds */
374
+ ttlSeconds: number;
337
375
  }
338
376
  interface VaultSummary {
339
377
  /** Vault ID */
@@ -360,8 +398,18 @@ interface Ingot {
360
398
  size: number;
361
399
  /** Creation timestamp (Unix seconds) */
362
400
  createdAt: number;
363
- /** Ingot type: 'standard' or 'structured' */
364
- type: 'standard' | 'structured';
401
+ }
402
+ interface CreatedIngot {
403
+ /** Ingot ID */
404
+ id: string;
405
+ /** Ingot name */
406
+ name: string;
407
+ /** Content type (MIME) */
408
+ contentType: string;
409
+ /** Size in bytes */
410
+ size: number;
411
+ /** Forge upload session expiry (Unix seconds) */
412
+ uploadExpiresAt: number;
365
413
  }
366
414
  interface UploadIngotOptions {
367
415
  /** File to upload */
@@ -370,6 +418,8 @@ interface UploadIngotOptions {
370
418
  name?: string;
371
419
  /** Content type (auto-detected if not provided) */
372
420
  contentType?: string;
421
+ /** Progress callback for Forge/TUS upload */
422
+ onProgress?: (bytesUploaded: number, bytesTotal: number) => void;
373
423
  }
374
424
 
375
425
  /**
@@ -503,6 +553,7 @@ interface UploadCallable {
503
553
  close(): void;
504
554
  }
505
555
  declare class VaultsModule {
556
+ private readonly config;
506
557
  private readonly http;
507
558
  private readonly uploadModule;
508
559
  /**
@@ -562,7 +613,7 @@ declare class VaultsModule {
562
613
  * name: 'document.pdf'
563
614
  * });
564
615
  */
565
- uploadIngot(vault: UnsealedVault, options: UploadIngotOptions): Promise<Ingot>;
616
+ uploadIngot(vault: UnsealedVault, options: UploadIngotOptions): Promise<CreatedIngot>;
566
617
  /**
567
618
  * Download an ingot from an unsealed vault.
568
619
  *
@@ -588,6 +639,7 @@ declare class VaultsModule {
588
639
  deleteIngot(vault: UnsealedVault, ingotId: string): Promise<void>;
589
640
  private validateCreateOptions;
590
641
  private validateUploadOptions;
642
+ private validateDownloadUrl;
591
643
  }
592
644
 
593
645
  /**
@@ -667,6 +719,8 @@ declare class SparkVault {
667
719
  readonly identity: IdentityModule;
668
720
  /** Persistent encrypted storage module */
669
721
  readonly vaults: VaultsModule;
722
+ /** API availability checks */
723
+ readonly health: HealthModule;
670
724
  private readonly config;
671
725
  private constructor();
672
726
  /**
@@ -692,4 +746,4 @@ declare global {
692
746
  }
693
747
 
694
748
  export { AuthenticationError, AuthorizationError, NetworkError, PopupBlockedError, SparkVault, SparkVaultError, TimeoutError, UserCancelledError, ValidationError, SparkVault as default, logger, setDebugMode };
695
- export type { AuthMethod, CreateVaultOptions, AttachOptions as IdentityAttachOptions, Ingot, SparkVaultConfig, Theme, TokenClaims, UnsealedVault, UploadAttachOptions, UploadBranding, UploadIngotOptions, UploadOptions, UploadProgress, UploadResult, Vault, VaultSummary, VaultUploadConfig, VerifyOptions, VerifyResult };
749
+ export type { AuthMethod, CreateVaultOptions, CreatedIngot, HealthCheckOptions, HealthCheckResult, AttachOptions as IdentityAttachOptions, Ingot, SparkVaultConfig, Theme, TokenClaims, UnsealedVault, UploadAttachOptions, UploadBranding, UploadIngotOptions, UploadOptions, UploadProgress, UploadResult, Vault, VaultSummary, VaultUploadConfig, VerifyOptions, VerifyResult };
@@ -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 data = await this.parseResponse(response);
263
+ const parsed = await this.parseResponse(response);
232
264
  if (!response.ok) {
233
- throw this.createErrorFromResponse(response.status, data);
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 message = (typeof record.message === 'string' ? record.message : null) ??
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 record.code === 'string' ? record.code : 'api_error';
294
- const details = typeof record.details === 'object' && record.details !== null
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
  *
@@ -9553,6 +9658,7 @@ class VaultUploadModule {
9553
9658
  */
9554
9659
  class VaultsModule {
9555
9660
  constructor(config, http) {
9661
+ this.config = config;
9556
9662
  this.http = http;
9557
9663
  this.uploadModule = new VaultUploadModule(config);
9558
9664
  // Create callable that also has attach/close methods
@@ -9600,13 +9706,15 @@ class VaultsModule {
9600
9706
  }
9601
9707
  const cleanId = vaultId.startsWith('vlt_') ? vaultId : `vlt_${vaultId}`;
9602
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
+ }
9603
9712
  return {
9604
9713
  id: response.data.vault_id,
9605
- name: response.data.name,
9606
- vatToken: response.data.vat_token,
9714
+ vatToken: response.data.vat,
9715
+ issuedAt: response.data.issued_at,
9607
9716
  expiresAt: response.data.expires_at,
9608
- ingotCount: response.data.ingot_count,
9609
- storageBytes: response.data.storage_bytes,
9717
+ ttlSeconds: response.data.ttl_seconds,
9610
9718
  };
9611
9719
  }
9612
9720
  /**
@@ -9651,22 +9759,26 @@ class VaultsModule {
9651
9759
  */
9652
9760
  async uploadIngot(vault, options) {
9653
9761
  this.validateUploadOptions(options);
9654
- const name = options.name ?? (options.file instanceof File ? options.file.name : 'unnamed');
9655
- const contentType = options.contentType ?? options.file.type ?? 'application/octet-stream';
9762
+ const requestedName = options.name ?? getBlobName(options.file);
9763
+ const requestedContentType = options.contentType ?? options.file.type ?? 'application/octet-stream';
9656
9764
  const createResponse = await this.http.post(`/v1/vaults/${vault.id}/ingots`, {
9657
- name,
9658
- content_type: contentType,
9765
+ name: requestedName,
9766
+ content_type: requestedContentType,
9659
9767
  size_bytes: options.file.size,
9660
9768
  }, {
9661
- headers: { Authorization: `Bearer ${vault.vatToken}` },
9769
+ headers: { 'X-Vault-Access-Token': vault.vatToken },
9662
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);
9663
9776
  return {
9664
- id: createResponse.data.ingot_id,
9665
- name: createResponse.data.name,
9666
- contentType: createResponse.data.content_type,
9667
- size: createResponse.data.size_bytes,
9668
- createdAt: createResponse.data.created_at,
9669
- 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,
9670
9782
  };
9671
9783
  }
9672
9784
  /**
@@ -9678,10 +9790,14 @@ class VaultsModule {
9678
9790
  */
9679
9791
  async downloadIngot(vault, ingotId) {
9680
9792
  const cleanId = ingotId.startsWith('ing_') ? ingotId : `ing_${ingotId}`;
9681
- return this.http.requestRaw(`/v1/vaults/${vault.id}/ingots/${cleanId}/download`, {
9682
- method: 'POST',
9683
- 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 },
9684
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);
9685
9801
  }
9686
9802
  /**
9687
9803
  * List all ingots in an unsealed vault.
@@ -9692,7 +9808,7 @@ class VaultsModule {
9692
9808
  */
9693
9809
  async listIngots(vault) {
9694
9810
  const response = await this.http.get(`/v1/vaults/${vault.id}/ingots`, {
9695
- headers: { Authorization: `Bearer ${vault.vatToken}` },
9811
+ headers: { 'X-Vault-Access-Token': vault.vatToken },
9696
9812
  });
9697
9813
  return response.data.ingots.map((i) => ({
9698
9814
  id: i.ingot_id,
@@ -9700,7 +9816,6 @@ class VaultsModule {
9700
9816
  contentType: i.content_type,
9701
9817
  size: i.size_bytes,
9702
9818
  createdAt: i.created_at,
9703
- type: i.ingot_type,
9704
9819
  }));
9705
9820
  }
9706
9821
  /**
@@ -9712,7 +9827,7 @@ class VaultsModule {
9712
9827
  async deleteIngot(vault, ingotId) {
9713
9828
  const cleanId = ingotId.startsWith('ing_') ? ingotId : `ing_${ingotId}`;
9714
9829
  await this.http.delete(`/v1/vaults/${vault.id}/ingots/${cleanId}`, {
9715
- headers: { Authorization: `Bearer ${vault.vatToken}` },
9830
+ headers: { 'X-Vault-Access-Token': vault.vatToken },
9716
9831
  });
9717
9832
  }
9718
9833
  validateCreateOptions(options) {
@@ -9731,6 +9846,117 @@ class VaultsModule {
9731
9846
  throw new ValidationError('File cannot be empty');
9732
9847
  }
9733
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
+ }
9734
9960
  }
9735
9961
 
9736
9962
  /**
@@ -9917,6 +10143,7 @@ class SparkVault {
9917
10143
  const http = new HttpClient(this.config);
9918
10144
  this.identity = new IdentityModule(this.config);
9919
10145
  this.vaults = new VaultsModule(this.config, http);
10146
+ this.health = new HealthModule(this.config);
9920
10147
  }
9921
10148
  /**
9922
10149
  * Initialize the SparkVault SDK.