@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 +19 -0
- package/dist/health.d.ts +18 -0
- package/dist/http.d.ts +3 -0
- package/dist/index.d.ts +65 -11
- package/dist/sparkvault.cjs.js +265 -38
- package/dist/sparkvault.cjs.js.map +1 -1
- package/dist/sparkvault.esm.js +265 -38
- 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/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;
|
package/dist/health.d.ts
ADDED
|
@@ -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
|
|
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
|
-
/**
|
|
334
|
-
|
|
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
|
-
|
|
364
|
-
|
|
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<
|
|
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 };
|
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
|
*
|
|
@@ -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
|
-
|
|
9606
|
-
|
|
9714
|
+
vatToken: response.data.vat,
|
|
9715
|
+
issuedAt: response.data.issued_at,
|
|
9607
9716
|
expiresAt: response.data.expires_at,
|
|
9608
|
-
|
|
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
|
|
9655
|
-
const
|
|
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:
|
|
9765
|
+
name: requestedName,
|
|
9766
|
+
content_type: requestedContentType,
|
|
9659
9767
|
size_bytes: options.file.size,
|
|
9660
9768
|
}, {
|
|
9661
|
-
headers: {
|
|
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:
|
|
9665
|
-
name:
|
|
9666
|
-
contentType:
|
|
9667
|
-
size:
|
|
9668
|
-
|
|
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
|
-
|
|
9682
|
-
|
|
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: {
|
|
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: {
|
|
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.
|