@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.
@@ -66,14 +66,25 @@ class PopupBlockedError extends SparkVaultError {
66
66
  */
67
67
  const API_URL = 'https://api.sparkvault.com';
68
68
  const IDENTITY_URL = 'https://api.sparkvault.com/v1/apps/identity';
69
+ function normalizeApiBaseUrl(url) {
70
+ const trimmed = url.replace(/\/+$/, '');
71
+ return trimmed.endsWith('/v1') ? trimmed.slice(0, -3) : trimmed;
72
+ }
73
+ function normalizeBaseUrl(url) {
74
+ return url.replace(/\/+$/, '');
75
+ }
69
76
  function resolveConfig(config) {
70
77
  return {
71
78
  accountId: config.accountId,
72
79
  timeout: config.timeout ?? 30000,
73
- apiBaseUrl: API_URL,
74
- identityBaseUrl: IDENTITY_URL,
80
+ apiBaseUrl: normalizeApiBaseUrl(config.apiBaseUrl ?? API_URL),
81
+ identityBaseUrl: normalizeBaseUrl(config.identityBaseUrl ?? IDENTITY_URL),
82
+ accessToken: config.accessToken,
83
+ getAccessToken: config.getAccessToken,
84
+ apiKey: config.apiKey,
75
85
  preloadConfig: config.preloadConfig !== false, // Default: true
76
86
  backdropBlur: config.backdropBlur !== false, // Default: true
87
+ allowedDownloadHostPatterns: config.allowedDownloadHostPatterns,
77
88
  };
78
89
  }
79
90
  function validateConfig(config) {
@@ -190,6 +201,31 @@ class HttpClient {
190
201
  constructor(config) {
191
202
  this.config = config;
192
203
  }
204
+ async getDefaultHeaders(headers, hasJsonBody) {
205
+ const requestHeaders = {
206
+ Accept: 'application/json',
207
+ ...headers,
208
+ };
209
+ if (hasJsonBody) {
210
+ requestHeaders['Content-Type'] = 'application/json';
211
+ }
212
+ if (!requestHeaders.Authorization) {
213
+ const token = await this.getAccessToken();
214
+ if (token) {
215
+ requestHeaders.Authorization = `Bearer ${token}`;
216
+ }
217
+ }
218
+ if (this.config.apiKey && !requestHeaders['X-API-Key']) {
219
+ requestHeaders['X-API-Key'] = this.config.apiKey;
220
+ }
221
+ return requestHeaders;
222
+ }
223
+ async getAccessToken() {
224
+ if (this.config.getAccessToken) {
225
+ return this.config.getAccessToken();
226
+ }
227
+ return this.config.accessToken ?? null;
228
+ }
193
229
  async request(path, options = {}) {
194
230
  const { retry } = options;
195
231
  // Per CLAUDE.md §7: Wrap with retry logic if enabled
@@ -209,11 +245,7 @@ class HttpClient {
209
245
  async executeRequest(path, options) {
210
246
  const { method = 'GET', headers = {}, body, timeout = this.config.timeout, } = options;
211
247
  const url = `${this.config.apiBaseUrl}${path}`;
212
- const requestHeaders = {
213
- 'Content-Type': 'application/json',
214
- Accept: 'application/json',
215
- ...headers,
216
- };
248
+ const requestHeaders = await this.getDefaultHeaders(headers, body !== undefined);
217
249
  const controller = new AbortController();
218
250
  const timeoutId = setTimeout(() => controller.abort(), timeout);
219
251
  try {
@@ -224,12 +256,12 @@ class HttpClient {
224
256
  signal: controller.signal,
225
257
  });
226
258
  clearTimeout(timeoutId);
227
- const data = await this.parseResponse(response);
259
+ const parsed = await this.parseResponse(response);
228
260
  if (!response.ok) {
229
- throw this.createErrorFromResponse(response.status, data);
261
+ throw this.createErrorFromResponse(response.status, parsed);
230
262
  }
231
263
  return {
232
- data,
264
+ data: this.unwrapApiResponse(parsed),
233
265
  status: response.status,
234
266
  headers: response.headers,
235
267
  };
@@ -283,13 +315,23 @@ class HttpClient {
283
315
  // Safely extract error fields with runtime type checking
284
316
  const errorData = typeof data === 'object' && data !== null ? data : {};
285
317
  const record = errorData;
286
- const message = (typeof record.message === 'string' ? record.message : null) ??
318
+ const nestedError = typeof record.error === 'object' && record.error !== null
319
+ ? record.error
320
+ : null;
321
+ const message = (typeof nestedError?.message === 'string' ? nestedError.message : null) ??
322
+ (typeof record.message === 'string' ? record.message : null) ??
287
323
  (typeof record.error === 'string' ? record.error : null) ??
288
324
  'Request failed';
289
- const code = typeof record.code === 'string' ? record.code : 'api_error';
290
- const details = typeof record.details === 'object' && record.details !== null
325
+ const code = (typeof nestedError?.code === 'string' ? nestedError.code : null) ??
326
+ (typeof record.code === 'string' ? record.code : null) ??
327
+ 'api_error';
328
+ const nestedDetails = typeof nestedError?.details === 'object' && nestedError.details !== null
329
+ ? nestedError.details
330
+ : undefined;
331
+ const topLevelDetails = typeof record.details === 'object' && record.details !== null
291
332
  ? record.details
292
333
  : undefined;
334
+ const details = nestedDetails ?? topLevelDetails;
293
335
  switch (status) {
294
336
  case 400:
295
337
  return new ValidationError(message, details);
@@ -301,6 +343,15 @@ class HttpClient {
301
343
  return new SparkVaultError(message, code, status, details);
302
344
  }
303
345
  }
346
+ unwrapApiResponse(data) {
347
+ if (typeof data === 'object' && data !== null) {
348
+ const record = data;
349
+ if ('data' in record && ('meta' in record || 'error' in record)) {
350
+ return record.data;
351
+ }
352
+ }
353
+ return data;
354
+ }
304
355
  get(path, options) {
305
356
  return this.request(path, { ...options, method: 'GET' });
306
357
  }
@@ -338,10 +389,7 @@ class HttpClient {
338
389
  async executeRequestRaw(path, options) {
339
390
  const { method = 'GET', headers = {}, body, timeout = this.config.timeout, } = options;
340
391
  const url = `${this.config.apiBaseUrl}${path}`;
341
- const requestHeaders = {
342
- 'Content-Type': 'application/json',
343
- ...headers,
344
- };
392
+ const requestHeaders = await this.getDefaultHeaders(headers, body !== undefined);
345
393
  const controller = new AbortController();
346
394
  const timeoutId = setTimeout(() => controller.abort(), timeout);
347
395
  try {
@@ -375,6 +423,63 @@ class HttpClient {
375
423
  }
376
424
  }
377
425
 
426
+ class HealthModule {
427
+ constructor(config) {
428
+ this.config = config;
429
+ }
430
+ async check(options = {}) {
431
+ const checkedAt = Math.floor(Date.now() / 1000);
432
+ const controller = new AbortController();
433
+ const timeoutId = setTimeout(() => controller.abort(), options.timeout ?? this.config.timeout);
434
+ try {
435
+ const response = await fetch(`${this.config.apiBaseUrl}/health`, {
436
+ method: 'GET',
437
+ headers: { Accept: 'application/json' },
438
+ signal: controller.signal,
439
+ });
440
+ return {
441
+ online: response.ok,
442
+ status: response.ok ? await this.readStatus(response) : 'unhealthy',
443
+ httpStatus: response.status,
444
+ checkedAt,
445
+ };
446
+ }
447
+ catch (err) {
448
+ return {
449
+ online: false,
450
+ status: 'unreachable',
451
+ checkedAt,
452
+ error: err instanceof Error ? err.message : String(err),
453
+ };
454
+ }
455
+ finally {
456
+ clearTimeout(timeoutId);
457
+ }
458
+ }
459
+ async isOnline(options = {}) {
460
+ const result = await this.check(options);
461
+ return result.online;
462
+ }
463
+ async readStatus(response) {
464
+ try {
465
+ const body = await response.json();
466
+ const data = typeof body === 'object' && body !== null && 'data' in body
467
+ ? body.data
468
+ : body;
469
+ if (typeof data === 'object' && data !== null) {
470
+ const status = data.status;
471
+ if (typeof status === 'string' && status) {
472
+ return status;
473
+ }
474
+ }
475
+ }
476
+ catch {
477
+ return response.ok ? 'healthy' : 'unhealthy';
478
+ }
479
+ return response.ok ? 'healthy' : 'unhealthy';
480
+ }
481
+ }
482
+
378
483
  /**
379
484
  * SparkVault API Base
380
485
  *
@@ -846,14 +951,19 @@ class IdentityApi extends SparkVaultApi {
846
951
  return this.configCache !== null;
847
952
  }
848
953
  /**
849
- * Check if an email has registered passkeys and validate email domain
954
+ * Check if an identity (email or phone) has registered passkeys.
850
955
  *
851
956
  * Returns:
852
- * - email_valid: whether the email domain has valid MX records
853
- * - has_passkey: whether any passkeys are registered (only meaningful if email_valid)
957
+ * - identity_valid: identity passes per-type validity check
958
+ * (email: MX lookup; phone: E.164 format)
959
+ * - has_passkey: whether any passkeys are registered for this identity
960
+ * (cross-tenant — single global RP)
961
+ * - email_valid: legacy alias for identity_valid, present for backward
962
+ * compatibility with older SDK versions
854
963
  */
855
- async checkPasskey(email) {
856
- return this.request('POST', '/passkey/check', { email });
964
+ async checkPasskey(identity, identityType = 'email') {
965
+ const body = identityType === 'phone' ? { phone: identity } : { email: identity };
966
+ return this.request('POST', '/passkey/check', body);
857
967
  }
858
968
  /**
859
969
  * Build auth context params for API requests (OIDC/simple mode).
@@ -898,12 +1008,13 @@ class IdentityApi extends SparkVaultApi {
898
1008
  });
899
1009
  }
900
1010
  /**
901
- * Start passkey registration
1011
+ * Start passkey registration for an identity (email or phone).
902
1012
  */
903
- async startPasskeyRegister(email) {
1013
+ async startPasskeyRegister(identity, identityType = 'email') {
1014
+ const body = identityType === 'phone' ? { phone: identity } : { email: identity };
904
1015
  // Backend returns { options: PublicKeyCredentialCreationOptions, session: {...} }
905
1016
  // Extract and flatten to match PasskeyChallengeResponse
906
- const response = await this.request('POST', '/passkey/register', { email });
1017
+ const response = await this.request('POST', '/passkey/register', body);
907
1018
  return {
908
1019
  challenge: response.options.challenge,
909
1020
  rpId: response.options.rp.id,
@@ -933,12 +1044,13 @@ class IdentityApi extends SparkVaultApi {
933
1044
  });
934
1045
  }
935
1046
  /**
936
- * Start passkey verification
1047
+ * Start passkey verification for an identity (email or phone).
937
1048
  */
938
- async startPasskeyVerify(email, authContext) {
1049
+ async startPasskeyVerify(identity, identityType = 'email', authContext) {
1050
+ const identityField = identityType === 'phone' ? { phone: identity } : { email: identity };
939
1051
  // Backend returns { options: PublicKeyCredentialRequestOptions, session: {...} }
940
1052
  // Extract and flatten to match PasskeyChallengeResponse
941
- const response = await this.request('POST', '/passkey/verify', { email, ...this.buildAuthContextParams(authContext) });
1053
+ const response = await this.request('POST', '/passkey/verify', { ...identityField, ...this.buildAuthContextParams(authContext) });
942
1054
  return {
943
1055
  challenge: response.options.challenge,
944
1056
  rpId: response.options.rpId,
@@ -1345,10 +1457,12 @@ class PasskeyHandler {
1345
1457
  */
1346
1458
  async checkPasskey() {
1347
1459
  try {
1348
- const response = await this.api.checkPasskey(this.state.recipient);
1460
+ const response = await this.api.checkPasskey(this.state.recipient, this.state.identityType);
1349
1461
  this.state.setPasskeyStatus(response.has_passkey);
1462
+ // Prefer identity_valid; fall back to email_valid for older backends.
1463
+ const valid = response.identity_valid ?? response.email_valid;
1350
1464
  return {
1351
- emailValid: response.email_valid,
1465
+ emailValid: valid,
1352
1466
  hasPasskey: response.has_passkey,
1353
1467
  };
1354
1468
  }
@@ -1380,12 +1494,14 @@ class PasskeyHandler {
1380
1494
  */
1381
1495
  async openPasskeyPopup(mode) {
1382
1496
  return new Promise((resolve) => {
1383
- // Build popup URL
1497
+ // Build popup URL. Pass identity as `email` or `phone` (mutually
1498
+ // exclusive) so the popup can render and POST the correct field.
1384
1499
  const params = new URLSearchParams({
1385
1500
  mode,
1386
- email: this.state.recipient,
1387
1501
  origin: window.location.origin,
1388
1502
  });
1503
+ const identityParam = this.state.identityType === 'phone' ? 'phone' : 'email';
1504
+ params.set(identityParam, this.state.recipient);
1389
1505
  // Add auth context for OIDC/simple mode flows
1390
1506
  const authContext = this.state.authContext;
1391
1507
  if (authContext?.authRequestId) {
@@ -9538,6 +9654,7 @@ class VaultUploadModule {
9538
9654
  */
9539
9655
  class VaultsModule {
9540
9656
  constructor(config, http) {
9657
+ this.config = config;
9541
9658
  this.http = http;
9542
9659
  this.uploadModule = new VaultUploadModule(config);
9543
9660
  // Create callable that also has attach/close methods
@@ -9585,13 +9702,15 @@ class VaultsModule {
9585
9702
  }
9586
9703
  const cleanId = vaultId.startsWith('vlt_') ? vaultId : `vlt_${vaultId}`;
9587
9704
  const response = await this.http.post(`/v1/vaults/${cleanId}/unseal`, { vmk });
9705
+ if (!response.data.vat) {
9706
+ throw new ValidationError('Unseal response did not include a vault access token');
9707
+ }
9588
9708
  return {
9589
9709
  id: response.data.vault_id,
9590
- name: response.data.name,
9591
- vatToken: response.data.vat_token,
9710
+ vatToken: response.data.vat,
9711
+ issuedAt: response.data.issued_at,
9592
9712
  expiresAt: response.data.expires_at,
9593
- ingotCount: response.data.ingot_count,
9594
- storageBytes: response.data.storage_bytes,
9713
+ ttlSeconds: response.data.ttl_seconds,
9595
9714
  };
9596
9715
  }
9597
9716
  /**
@@ -9636,22 +9755,26 @@ class VaultsModule {
9636
9755
  */
9637
9756
  async uploadIngot(vault, options) {
9638
9757
  this.validateUploadOptions(options);
9639
- const name = options.name ?? (options.file instanceof File ? options.file.name : 'unnamed');
9640
- const contentType = options.contentType ?? options.file.type ?? 'application/octet-stream';
9758
+ const requestedName = options.name ?? getBlobName(options.file);
9759
+ const requestedContentType = options.contentType ?? options.file.type ?? 'application/octet-stream';
9641
9760
  const createResponse = await this.http.post(`/v1/vaults/${vault.id}/ingots`, {
9642
- name,
9643
- content_type: contentType,
9761
+ name: requestedName,
9762
+ content_type: requestedContentType,
9644
9763
  size_bytes: options.file.size,
9645
9764
  }, {
9646
- headers: { Authorization: `Bearer ${vault.vatToken}` },
9765
+ headers: { 'X-Vault-Access-Token': vault.vatToken },
9647
9766
  });
9767
+ const created = createResponse.data;
9768
+ if (!created.forge_url) {
9769
+ throw new ValidationError('Upload endpoint did not return a Forge upload URL');
9770
+ }
9771
+ await uploadBlobWithTus(options.file, created.forge_url, created.name, created.content_type, options.onProgress);
9648
9772
  return {
9649
- id: createResponse.data.ingot_id,
9650
- name: createResponse.data.name,
9651
- contentType: createResponse.data.content_type,
9652
- size: createResponse.data.size_bytes,
9653
- createdAt: createResponse.data.created_at,
9654
- type: createResponse.data.ingot_type,
9773
+ id: created.ingot_id,
9774
+ name: created.name,
9775
+ contentType: created.content_type,
9776
+ size: created.size_bytes,
9777
+ uploadExpiresAt: created.expires_at,
9655
9778
  };
9656
9779
  }
9657
9780
  /**
@@ -9663,10 +9786,14 @@ class VaultsModule {
9663
9786
  */
9664
9787
  async downloadIngot(vault, ingotId) {
9665
9788
  const cleanId = ingotId.startsWith('ing_') ? ingotId : `ing_${ingotId}`;
9666
- return this.http.requestRaw(`/v1/vaults/${vault.id}/ingots/${cleanId}/download`, {
9667
- method: 'POST',
9668
- headers: { Authorization: `Bearer ${vault.vatToken}` },
9789
+ const response = await this.http.post(`/v1/vaults/${vault.id}/ingots/${cleanId}/download`, undefined, {
9790
+ headers: { 'X-Vault-Access-Token': vault.vatToken },
9669
9791
  });
9792
+ if (!response.data.download_url) {
9793
+ throw new ValidationError('Download endpoint did not return a download URL');
9794
+ }
9795
+ this.validateDownloadUrl(response.data.download_url);
9796
+ return fetchBlobWithTimeout(response.data.download_url);
9670
9797
  }
9671
9798
  /**
9672
9799
  * List all ingots in an unsealed vault.
@@ -9677,7 +9804,7 @@ class VaultsModule {
9677
9804
  */
9678
9805
  async listIngots(vault) {
9679
9806
  const response = await this.http.get(`/v1/vaults/${vault.id}/ingots`, {
9680
- headers: { Authorization: `Bearer ${vault.vatToken}` },
9807
+ headers: { 'X-Vault-Access-Token': vault.vatToken },
9681
9808
  });
9682
9809
  return response.data.ingots.map((i) => ({
9683
9810
  id: i.ingot_id,
@@ -9685,7 +9812,6 @@ class VaultsModule {
9685
9812
  contentType: i.content_type,
9686
9813
  size: i.size_bytes,
9687
9814
  createdAt: i.created_at,
9688
- type: i.ingot_type,
9689
9815
  }));
9690
9816
  }
9691
9817
  /**
@@ -9697,7 +9823,7 @@ class VaultsModule {
9697
9823
  async deleteIngot(vault, ingotId) {
9698
9824
  const cleanId = ingotId.startsWith('ing_') ? ingotId : `ing_${ingotId}`;
9699
9825
  await this.http.delete(`/v1/vaults/${vault.id}/ingots/${cleanId}`, {
9700
- headers: { Authorization: `Bearer ${vault.vatToken}` },
9826
+ headers: { 'X-Vault-Access-Token': vault.vatToken },
9701
9827
  });
9702
9828
  }
9703
9829
  validateCreateOptions(options) {
@@ -9716,6 +9842,117 @@ class VaultsModule {
9716
9842
  throw new ValidationError('File cannot be empty');
9717
9843
  }
9718
9844
  }
9845
+ validateDownloadUrl(downloadUrl) {
9846
+ let parsed;
9847
+ try {
9848
+ parsed = new URL(downloadUrl);
9849
+ }
9850
+ catch {
9851
+ throw new ValidationError('Invalid download URL from server');
9852
+ }
9853
+ if (parsed.protocol !== 'https:') {
9854
+ throw new ValidationError('Download URL must use HTTPS');
9855
+ }
9856
+ const allowedHosts = this.config.allowedDownloadHostPatterns;
9857
+ if (!allowedHosts?.length) {
9858
+ return;
9859
+ }
9860
+ if (!allowedHosts.some(pattern => pattern.test(parsed.hostname))) {
9861
+ throw new ValidationError('Invalid download URL from server');
9862
+ }
9863
+ }
9864
+ }
9865
+ function getBlobName(file) {
9866
+ if (typeof File !== 'undefined' && file instanceof File && file.name) {
9867
+ return file.name;
9868
+ }
9869
+ return 'unnamed';
9870
+ }
9871
+ function base64Encode(value) {
9872
+ return btoa(unescape(encodeURIComponent(value)));
9873
+ }
9874
+ async function uploadBlobWithTus(file, forgeUrl, filename, contentType, onProgress) {
9875
+ if (typeof XMLHttpRequest === 'undefined') {
9876
+ throw new ValidationError('XMLHttpRequest is required for browser ingot uploads');
9877
+ }
9878
+ const tusVersion = '1.0.0';
9879
+ const defaultChunkSize = 50 * 1024 * 1024;
9880
+ const url = new URL(forgeUrl);
9881
+ const istk = url.searchParams.get('istk');
9882
+ const endpoint = `${url.origin}${url.pathname}`;
9883
+ if (!istk) {
9884
+ throw new ValidationError('Forge upload URL is missing ISTK');
9885
+ }
9886
+ const createResponse = await fetch(endpoint, {
9887
+ method: 'POST',
9888
+ headers: {
9889
+ 'Tus-Resumable': tusVersion,
9890
+ 'Upload-Length': String(file.size),
9891
+ 'Upload-Metadata': `filename ${base64Encode(filename)},filetype ${base64Encode(contentType)}`,
9892
+ 'X-ISTK': istk,
9893
+ },
9894
+ });
9895
+ if (!createResponse.ok) {
9896
+ const errorText = await createResponse.text();
9897
+ throw new Error(`Failed to create upload session: ${createResponse.status} ${errorText}`);
9898
+ }
9899
+ const location = createResponse.headers.get('Location');
9900
+ const chunkSizeHeader = createResponse.headers.get('X-Chunk-Size');
9901
+ const chunkSize = chunkSizeHeader ? parseInt(chunkSizeHeader, 10) : defaultChunkSize;
9902
+ if (!location) {
9903
+ throw new Error('Server did not return upload location');
9904
+ }
9905
+ const uploadUrl = location.startsWith('http') ? location : `${url.origin}${location}`;
9906
+ let offset = 0;
9907
+ while (offset < file.size) {
9908
+ const chunkStart = offset;
9909
+ const chunkEnd = Math.min(offset + chunkSize, file.size);
9910
+ const chunk = file.slice(chunkStart, chunkEnd);
9911
+ offset = await uploadChunkWithProgress(uploadUrl, chunk, chunkStart, file.size, tusVersion, istk, onProgress);
9912
+ }
9913
+ }
9914
+ function uploadChunkWithProgress(uploadUrl, chunk, chunkStart, totalSize, tusVersion, istk, onProgress) {
9915
+ return new Promise((resolve, reject) => {
9916
+ const xhr = new XMLHttpRequest();
9917
+ xhr.upload.onprogress = (event) => {
9918
+ if (event.lengthComputable) {
9919
+ onProgress?.(chunkStart + event.loaded, totalSize);
9920
+ }
9921
+ };
9922
+ xhr.onload = () => {
9923
+ if (xhr.status >= 200 && xhr.status < 300) {
9924
+ const newOffsetHeader = xhr.getResponseHeader('Upload-Offset');
9925
+ const newOffset = newOffsetHeader ? parseInt(newOffsetHeader, 10) : chunkStart + chunk.size;
9926
+ resolve(newOffset);
9927
+ }
9928
+ else {
9929
+ reject(new Error(`Chunk upload failed with status ${xhr.status}`));
9930
+ }
9931
+ };
9932
+ xhr.onerror = () => reject(new Error('Chunk upload failed'));
9933
+ xhr.ontimeout = () => reject(new Error('Chunk upload timed out'));
9934
+ xhr.onabort = () => reject(new Error('Upload cancelled'));
9935
+ xhr.open('PATCH', uploadUrl);
9936
+ xhr.setRequestHeader('Tus-Resumable', tusVersion);
9937
+ xhr.setRequestHeader('Upload-Offset', String(chunkStart));
9938
+ xhr.setRequestHeader('Content-Type', 'application/offset+octet-stream');
9939
+ xhr.setRequestHeader('X-ISTK', istk);
9940
+ xhr.send(chunk);
9941
+ });
9942
+ }
9943
+ async function fetchBlobWithTimeout(url, timeout = 300000) {
9944
+ const controller = new AbortController();
9945
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
9946
+ try {
9947
+ const response = await fetch(url, { signal: controller.signal });
9948
+ if (!response.ok) {
9949
+ throw new Error(`Download failed with status ${response.status}`);
9950
+ }
9951
+ return response.blob();
9952
+ }
9953
+ finally {
9954
+ clearTimeout(timeoutId);
9955
+ }
9719
9956
  }
9720
9957
 
9721
9958
  /**
@@ -9902,6 +10139,7 @@ class SparkVault {
9902
10139
  const http = new HttpClient(this.config);
9903
10140
  this.identity = new IdentityModule(this.config);
9904
10141
  this.vaults = new VaultsModule(this.config, http);
10142
+ this.health = new HealthModule(this.config);
9905
10143
  }
9906
10144
  /**
9907
10145
  * Initialize the SparkVault SDK.