@sparkvault/sdk 1.24.0 → 1.24.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
  *
@@ -850,14 +955,19 @@ class IdentityApi extends SparkVaultApi {
850
955
  return this.configCache !== null;
851
956
  }
852
957
  /**
853
- * Check if an email has registered passkeys and validate email domain
958
+ * Check if an identity (email or phone) has registered passkeys.
854
959
  *
855
960
  * Returns:
856
- * - email_valid: whether the email domain has valid MX records
857
- * - has_passkey: whether any passkeys are registered (only meaningful if email_valid)
961
+ * - identity_valid: identity passes per-type validity check
962
+ * (email: MX lookup; phone: E.164 format)
963
+ * - has_passkey: whether any passkeys are registered for this identity
964
+ * (cross-tenant — single global RP)
965
+ * - email_valid: legacy alias for identity_valid, present for backward
966
+ * compatibility with older SDK versions
858
967
  */
859
- async checkPasskey(email) {
860
- return this.request('POST', '/passkey/check', { email });
968
+ async checkPasskey(identity, identityType = 'email') {
969
+ const body = identityType === 'phone' ? { phone: identity } : { email: identity };
970
+ return this.request('POST', '/passkey/check', body);
861
971
  }
862
972
  /**
863
973
  * Build auth context params for API requests (OIDC/simple mode).
@@ -902,12 +1012,13 @@ class IdentityApi extends SparkVaultApi {
902
1012
  });
903
1013
  }
904
1014
  /**
905
- * Start passkey registration
1015
+ * Start passkey registration for an identity (email or phone).
906
1016
  */
907
- async startPasskeyRegister(email) {
1017
+ async startPasskeyRegister(identity, identityType = 'email') {
1018
+ const body = identityType === 'phone' ? { phone: identity } : { email: identity };
908
1019
  // Backend returns { options: PublicKeyCredentialCreationOptions, session: {...} }
909
1020
  // Extract and flatten to match PasskeyChallengeResponse
910
- const response = await this.request('POST', '/passkey/register', { email });
1021
+ const response = await this.request('POST', '/passkey/register', body);
911
1022
  return {
912
1023
  challenge: response.options.challenge,
913
1024
  rpId: response.options.rp.id,
@@ -937,12 +1048,13 @@ class IdentityApi extends SparkVaultApi {
937
1048
  });
938
1049
  }
939
1050
  /**
940
- * Start passkey verification
1051
+ * Start passkey verification for an identity (email or phone).
941
1052
  */
942
- async startPasskeyVerify(email, authContext) {
1053
+ async startPasskeyVerify(identity, identityType = 'email', authContext) {
1054
+ const identityField = identityType === 'phone' ? { phone: identity } : { email: identity };
943
1055
  // Backend returns { options: PublicKeyCredentialRequestOptions, session: {...} }
944
1056
  // Extract and flatten to match PasskeyChallengeResponse
945
- const response = await this.request('POST', '/passkey/verify', { email, ...this.buildAuthContextParams(authContext) });
1057
+ const response = await this.request('POST', '/passkey/verify', { ...identityField, ...this.buildAuthContextParams(authContext) });
946
1058
  return {
947
1059
  challenge: response.options.challenge,
948
1060
  rpId: response.options.rpId,
@@ -1349,10 +1461,12 @@ class PasskeyHandler {
1349
1461
  */
1350
1462
  async checkPasskey() {
1351
1463
  try {
1352
- const response = await this.api.checkPasskey(this.state.recipient);
1464
+ const response = await this.api.checkPasskey(this.state.recipient, this.state.identityType);
1353
1465
  this.state.setPasskeyStatus(response.has_passkey);
1466
+ // Prefer identity_valid; fall back to email_valid for older backends.
1467
+ const valid = response.identity_valid ?? response.email_valid;
1354
1468
  return {
1355
- emailValid: response.email_valid,
1469
+ emailValid: valid,
1356
1470
  hasPasskey: response.has_passkey,
1357
1471
  };
1358
1472
  }
@@ -1384,12 +1498,14 @@ class PasskeyHandler {
1384
1498
  */
1385
1499
  async openPasskeyPopup(mode) {
1386
1500
  return new Promise((resolve) => {
1387
- // Build popup URL
1501
+ // Build popup URL. Pass identity as `email` or `phone` (mutually
1502
+ // exclusive) so the popup can render and POST the correct field.
1388
1503
  const params = new URLSearchParams({
1389
1504
  mode,
1390
- email: this.state.recipient,
1391
1505
  origin: window.location.origin,
1392
1506
  });
1507
+ const identityParam = this.state.identityType === 'phone' ? 'phone' : 'email';
1508
+ params.set(identityParam, this.state.recipient);
1393
1509
  // Add auth context for OIDC/simple mode flows
1394
1510
  const authContext = this.state.authContext;
1395
1511
  if (authContext?.authRequestId) {
@@ -9542,6 +9658,7 @@ class VaultUploadModule {
9542
9658
  */
9543
9659
  class VaultsModule {
9544
9660
  constructor(config, http) {
9661
+ this.config = config;
9545
9662
  this.http = http;
9546
9663
  this.uploadModule = new VaultUploadModule(config);
9547
9664
  // Create callable that also has attach/close methods
@@ -9589,13 +9706,15 @@ class VaultsModule {
9589
9706
  }
9590
9707
  const cleanId = vaultId.startsWith('vlt_') ? vaultId : `vlt_${vaultId}`;
9591
9708
  const response = await this.http.post(`/v1/vaults/${cleanId}/unseal`, { vmk });
9709
+ if (!response.data.vat) {
9710
+ throw new ValidationError('Unseal response did not include a vault access token');
9711
+ }
9592
9712
  return {
9593
9713
  id: response.data.vault_id,
9594
- name: response.data.name,
9595
- vatToken: response.data.vat_token,
9714
+ vatToken: response.data.vat,
9715
+ issuedAt: response.data.issued_at,
9596
9716
  expiresAt: response.data.expires_at,
9597
- ingotCount: response.data.ingot_count,
9598
- storageBytes: response.data.storage_bytes,
9717
+ ttlSeconds: response.data.ttl_seconds,
9599
9718
  };
9600
9719
  }
9601
9720
  /**
@@ -9640,22 +9759,26 @@ class VaultsModule {
9640
9759
  */
9641
9760
  async uploadIngot(vault, options) {
9642
9761
  this.validateUploadOptions(options);
9643
- const name = options.name ?? (options.file instanceof File ? options.file.name : 'unnamed');
9644
- 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';
9645
9764
  const createResponse = await this.http.post(`/v1/vaults/${vault.id}/ingots`, {
9646
- name,
9647
- content_type: contentType,
9765
+ name: requestedName,
9766
+ content_type: requestedContentType,
9648
9767
  size_bytes: options.file.size,
9649
9768
  }, {
9650
- headers: { Authorization: `Bearer ${vault.vatToken}` },
9769
+ headers: { 'X-Vault-Access-Token': vault.vatToken },
9651
9770
  });
9771
+ const created = createResponse.data;
9772
+ if (!created.forge_url) {
9773
+ throw new ValidationError('Upload endpoint did not return a Forge upload URL');
9774
+ }
9775
+ await uploadBlobWithTus(options.file, created.forge_url, created.name, created.content_type, options.onProgress);
9652
9776
  return {
9653
- id: createResponse.data.ingot_id,
9654
- name: createResponse.data.name,
9655
- contentType: createResponse.data.content_type,
9656
- size: createResponse.data.size_bytes,
9657
- createdAt: createResponse.data.created_at,
9658
- type: createResponse.data.ingot_type,
9777
+ id: created.ingot_id,
9778
+ name: created.name,
9779
+ contentType: created.content_type,
9780
+ size: created.size_bytes,
9781
+ uploadExpiresAt: created.expires_at,
9659
9782
  };
9660
9783
  }
9661
9784
  /**
@@ -9667,10 +9790,14 @@ class VaultsModule {
9667
9790
  */
9668
9791
  async downloadIngot(vault, ingotId) {
9669
9792
  const cleanId = ingotId.startsWith('ing_') ? ingotId : `ing_${ingotId}`;
9670
- return this.http.requestRaw(`/v1/vaults/${vault.id}/ingots/${cleanId}/download`, {
9671
- method: 'POST',
9672
- headers: { Authorization: `Bearer ${vault.vatToken}` },
9793
+ const response = await this.http.post(`/v1/vaults/${vault.id}/ingots/${cleanId}/download`, undefined, {
9794
+ headers: { 'X-Vault-Access-Token': vault.vatToken },
9673
9795
  });
9796
+ if (!response.data.download_url) {
9797
+ throw new ValidationError('Download endpoint did not return a download URL');
9798
+ }
9799
+ this.validateDownloadUrl(response.data.download_url);
9800
+ return fetchBlobWithTimeout(response.data.download_url);
9674
9801
  }
9675
9802
  /**
9676
9803
  * List all ingots in an unsealed vault.
@@ -9681,7 +9808,7 @@ class VaultsModule {
9681
9808
  */
9682
9809
  async listIngots(vault) {
9683
9810
  const response = await this.http.get(`/v1/vaults/${vault.id}/ingots`, {
9684
- headers: { Authorization: `Bearer ${vault.vatToken}` },
9811
+ headers: { 'X-Vault-Access-Token': vault.vatToken },
9685
9812
  });
9686
9813
  return response.data.ingots.map((i) => ({
9687
9814
  id: i.ingot_id,
@@ -9689,7 +9816,6 @@ class VaultsModule {
9689
9816
  contentType: i.content_type,
9690
9817
  size: i.size_bytes,
9691
9818
  createdAt: i.created_at,
9692
- type: i.ingot_type,
9693
9819
  }));
9694
9820
  }
9695
9821
  /**
@@ -9701,7 +9827,7 @@ class VaultsModule {
9701
9827
  async deleteIngot(vault, ingotId) {
9702
9828
  const cleanId = ingotId.startsWith('ing_') ? ingotId : `ing_${ingotId}`;
9703
9829
  await this.http.delete(`/v1/vaults/${vault.id}/ingots/${cleanId}`, {
9704
- headers: { Authorization: `Bearer ${vault.vatToken}` },
9830
+ headers: { 'X-Vault-Access-Token': vault.vatToken },
9705
9831
  });
9706
9832
  }
9707
9833
  validateCreateOptions(options) {
@@ -9720,6 +9846,117 @@ class VaultsModule {
9720
9846
  throw new ValidationError('File cannot be empty');
9721
9847
  }
9722
9848
  }
9849
+ validateDownloadUrl(downloadUrl) {
9850
+ let parsed;
9851
+ try {
9852
+ parsed = new URL(downloadUrl);
9853
+ }
9854
+ catch {
9855
+ throw new ValidationError('Invalid download URL from server');
9856
+ }
9857
+ if (parsed.protocol !== 'https:') {
9858
+ throw new ValidationError('Download URL must use HTTPS');
9859
+ }
9860
+ const allowedHosts = this.config.allowedDownloadHostPatterns;
9861
+ if (!allowedHosts?.length) {
9862
+ return;
9863
+ }
9864
+ if (!allowedHosts.some(pattern => pattern.test(parsed.hostname))) {
9865
+ throw new ValidationError('Invalid download URL from server');
9866
+ }
9867
+ }
9868
+ }
9869
+ function getBlobName(file) {
9870
+ if (typeof File !== 'undefined' && file instanceof File && file.name) {
9871
+ return file.name;
9872
+ }
9873
+ return 'unnamed';
9874
+ }
9875
+ function base64Encode(value) {
9876
+ return btoa(unescape(encodeURIComponent(value)));
9877
+ }
9878
+ async function uploadBlobWithTus(file, forgeUrl, filename, contentType, onProgress) {
9879
+ if (typeof XMLHttpRequest === 'undefined') {
9880
+ throw new ValidationError('XMLHttpRequest is required for browser ingot uploads');
9881
+ }
9882
+ const tusVersion = '1.0.0';
9883
+ const defaultChunkSize = 50 * 1024 * 1024;
9884
+ const url = new URL(forgeUrl);
9885
+ const istk = url.searchParams.get('istk');
9886
+ const endpoint = `${url.origin}${url.pathname}`;
9887
+ if (!istk) {
9888
+ throw new ValidationError('Forge upload URL is missing ISTK');
9889
+ }
9890
+ const createResponse = await fetch(endpoint, {
9891
+ method: 'POST',
9892
+ headers: {
9893
+ 'Tus-Resumable': tusVersion,
9894
+ 'Upload-Length': String(file.size),
9895
+ 'Upload-Metadata': `filename ${base64Encode(filename)},filetype ${base64Encode(contentType)}`,
9896
+ 'X-ISTK': istk,
9897
+ },
9898
+ });
9899
+ if (!createResponse.ok) {
9900
+ const errorText = await createResponse.text();
9901
+ throw new Error(`Failed to create upload session: ${createResponse.status} ${errorText}`);
9902
+ }
9903
+ const location = createResponse.headers.get('Location');
9904
+ const chunkSizeHeader = createResponse.headers.get('X-Chunk-Size');
9905
+ const chunkSize = chunkSizeHeader ? parseInt(chunkSizeHeader, 10) : defaultChunkSize;
9906
+ if (!location) {
9907
+ throw new Error('Server did not return upload location');
9908
+ }
9909
+ const uploadUrl = location.startsWith('http') ? location : `${url.origin}${location}`;
9910
+ let offset = 0;
9911
+ while (offset < file.size) {
9912
+ const chunkStart = offset;
9913
+ const chunkEnd = Math.min(offset + chunkSize, file.size);
9914
+ const chunk = file.slice(chunkStart, chunkEnd);
9915
+ offset = await uploadChunkWithProgress(uploadUrl, chunk, chunkStart, file.size, tusVersion, istk, onProgress);
9916
+ }
9917
+ }
9918
+ function uploadChunkWithProgress(uploadUrl, chunk, chunkStart, totalSize, tusVersion, istk, onProgress) {
9919
+ return new Promise((resolve, reject) => {
9920
+ const xhr = new XMLHttpRequest();
9921
+ xhr.upload.onprogress = (event) => {
9922
+ if (event.lengthComputable) {
9923
+ onProgress?.(chunkStart + event.loaded, totalSize);
9924
+ }
9925
+ };
9926
+ xhr.onload = () => {
9927
+ if (xhr.status >= 200 && xhr.status < 300) {
9928
+ const newOffsetHeader = xhr.getResponseHeader('Upload-Offset');
9929
+ const newOffset = newOffsetHeader ? parseInt(newOffsetHeader, 10) : chunkStart + chunk.size;
9930
+ resolve(newOffset);
9931
+ }
9932
+ else {
9933
+ reject(new Error(`Chunk upload failed with status ${xhr.status}`));
9934
+ }
9935
+ };
9936
+ xhr.onerror = () => reject(new Error('Chunk upload failed'));
9937
+ xhr.ontimeout = () => reject(new Error('Chunk upload timed out'));
9938
+ xhr.onabort = () => reject(new Error('Upload cancelled'));
9939
+ xhr.open('PATCH', uploadUrl);
9940
+ xhr.setRequestHeader('Tus-Resumable', tusVersion);
9941
+ xhr.setRequestHeader('Upload-Offset', String(chunkStart));
9942
+ xhr.setRequestHeader('Content-Type', 'application/offset+octet-stream');
9943
+ xhr.setRequestHeader('X-ISTK', istk);
9944
+ xhr.send(chunk);
9945
+ });
9946
+ }
9947
+ async function fetchBlobWithTimeout(url, timeout = 300000) {
9948
+ const controller = new AbortController();
9949
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
9950
+ try {
9951
+ const response = await fetch(url, { signal: controller.signal });
9952
+ if (!response.ok) {
9953
+ throw new Error(`Download failed with status ${response.status}`);
9954
+ }
9955
+ return response.blob();
9956
+ }
9957
+ finally {
9958
+ clearTimeout(timeoutId);
9959
+ }
9723
9960
  }
9724
9961
 
9725
9962
  /**
@@ -9906,6 +10143,7 @@ class SparkVault {
9906
10143
  const http = new HttpClient(this.config);
9907
10144
  this.identity = new IdentityModule(this.config);
9908
10145
  this.vaults = new VaultsModule(this.config, http);
10146
+ this.health = new HealthModule(this.config);
9909
10147
  }
9910
10148
  /**
9911
10149
  * Initialize the SparkVault SDK.