@sparkvault/sdk 1.23.9 → 1.24.1

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.
@@ -379,6 +379,153 @@ class HttpClient {
379
379
  }
380
380
  }
381
381
 
382
+ /**
383
+ * SparkVault API Base
384
+ *
385
+ * Shared lifecycle and HTTP plumbing for every internal API client in the
386
+ * SDK. Concrete clients (IdentityApi, UploadApi, ...) extend this and
387
+ * provide:
388
+ * - their own error factory via makeError()
389
+ * - their own typed endpoint methods that call rawRequest()
390
+ *
391
+ * Session lifecycle invariant: every user-facing session (a modal open, a
392
+ * verify flow, an upload widget invocation) starts with the owning Module
393
+ * calling resetSession(). close() aborts the current AbortController; the
394
+ * next resetSession() swaps in a fresh one. The Module is responsible for
395
+ * this; clients of the SDK never see any of it.
396
+ */
397
+ /** Default request timeout in milliseconds (30 seconds). */
398
+ const DEFAULT_TIMEOUT_MS = 30000;
399
+ class SparkVaultApi {
400
+ constructor(timeoutMs = DEFAULT_TIMEOUT_MS) {
401
+ /**
402
+ * Abort controller for the current session. Replaced by resetSession()
403
+ * at the start of every user-facing session — an aborted controller can
404
+ * never leak into the next session.
405
+ */
406
+ this.closeController = new AbortController();
407
+ this.timeoutMs = timeoutMs;
408
+ }
409
+ /** Abort all pending requests on this client. Called by the Module on close. */
410
+ abort() {
411
+ this.closeController.abort();
412
+ }
413
+ /** Whether the *current* session has been aborted. */
414
+ isAborted() {
415
+ return this.closeController.signal.aborted;
416
+ }
417
+ /** The current session's abort signal — for callers that need to wire it into XHR/etc. */
418
+ getAbortSignal() {
419
+ return this.closeController.signal;
420
+ }
421
+ /**
422
+ * Swap in a fresh AbortController. The Module calls this at the start of
423
+ * every user-facing session. Long-lived per-client state (e.g. config
424
+ * caches) is intentionally NOT cleared here.
425
+ */
426
+ resetSession() {
427
+ this.closeController = new AbortController();
428
+ }
429
+ /**
430
+ * Issue an HTTP request with timeout + session abort + standard error
431
+ * mapping. Subclasses build URLs and unwrap response envelopes on top.
432
+ *
433
+ * Returns the parsed JSON body (whatever shape it takes) plus the
434
+ * status code. Throws subclass-typed errors via makeError() for: aborted
435
+ * sessions, timeouts, network failures, and non-2xx responses.
436
+ */
437
+ async rawRequest(url, options) {
438
+ // Capture the current controller locally. If resetSession() swaps the
439
+ // field while this request is in flight (or in its catch block), the
440
+ // aborted-vs-timeout discrimination below stays accurate.
441
+ const localCloseController = this.closeController;
442
+ if (localCloseController.signal.aborted) {
443
+ throw this.makeError('Request cancelled', 'cancelled', 0);
444
+ }
445
+ const timeoutController = new AbortController();
446
+ const timeoutId = setTimeout(() => timeoutController.abort(), this.timeoutMs);
447
+ let combinedSignal;
448
+ let onCloseAbort = null;
449
+ if ('any' in AbortSignal) {
450
+ combinedSignal = AbortSignal.any([
451
+ timeoutController.signal,
452
+ localCloseController.signal,
453
+ ]);
454
+ }
455
+ else {
456
+ // Fallback for browsers without AbortSignal.any: link the close
457
+ // signal into the timeout controller manually. Listener is removed
458
+ // in the finally block so it doesn't leak across requests.
459
+ combinedSignal = timeoutController.signal;
460
+ onCloseAbort = () => timeoutController.abort();
461
+ localCloseController.signal.addEventListener('abort', onCloseAbort);
462
+ }
463
+ try {
464
+ // Only send Content-Type when there's actually a body — otherwise GETs
465
+ // with no payload would carry a misleading content-type header.
466
+ const headers = { 'Accept': 'application/json' };
467
+ if (options.body !== undefined)
468
+ headers['Content-Type'] = 'application/json';
469
+ if (options.headers)
470
+ Object.assign(headers, options.headers);
471
+ const response = await fetch(url, {
472
+ method: options.method,
473
+ headers,
474
+ body: options.body,
475
+ signal: combinedSignal,
476
+ });
477
+ const json = await response.json().catch(() => null);
478
+ if (!response.ok) {
479
+ const { message, code } = extractApiError(json);
480
+ throw this.makeError(message, code, response.status);
481
+ }
482
+ return { data: json, status: response.status };
483
+ }
484
+ catch (error) {
485
+ // Re-throw SparkVault errors as-is — they already carry the right code/status.
486
+ if (error instanceof SparkVaultError)
487
+ throw error;
488
+ // Distinguish session-close (cancelled) from timeout. Use the LOCAL
489
+ // controller captured at request start, not `this.closeController`.
490
+ if (error != null && typeof error === 'object' && 'name' in error && error.name === 'AbortError') {
491
+ if (localCloseController.signal.aborted) {
492
+ throw this.makeError('Request cancelled', 'cancelled', 0);
493
+ }
494
+ throw this.makeError(`Request timed out after ${this.timeoutMs}ms`, 'timeout', 0);
495
+ }
496
+ // Browser-level network failure: TypeError("Failed to fetch")
497
+ // (Chrome/Firefox) or TypeError("Load failed") (WebKit).
498
+ if (error instanceof TypeError) {
499
+ throw this.makeError('Unable to connect. Please check your connection and try again.', 'network_error', 0);
500
+ }
501
+ throw this.makeError(error instanceof Error ? error.message : 'An unexpected error occurred', 'unknown_error', 0);
502
+ }
503
+ finally {
504
+ clearTimeout(timeoutId);
505
+ if (onCloseAbort) {
506
+ localCloseController.signal.removeEventListener('abort', onCloseAbort);
507
+ }
508
+ }
509
+ }
510
+ }
511
+ /**
512
+ * Extract { message, code } from an API error body. The API wraps errors as
513
+ * `{ error: { code, message, details }, meta: {...} }`, but legacy/edge
514
+ * paths may flatten or use `{ error: 'string' }` instead.
515
+ */
516
+ function extractApiError(json) {
517
+ const root = (typeof json === 'object' && json !== null) ? json : {};
518
+ const errorObj = typeof root.error === 'object' && root.error !== null
519
+ ? root.error
520
+ : root;
521
+ const message = (typeof errorObj.message === 'string' ? errorObj.message : null) ??
522
+ (typeof errorObj.error === 'string' ? errorObj.error : null) ??
523
+ (typeof root.message === 'string' ? root.message : null) ??
524
+ 'Request failed';
525
+ const code = typeof errorObj.code === 'string' ? errorObj.code : 'api_error';
526
+ return { message, code };
527
+ }
528
+
382
529
  /**
383
530
  * CooldownTimer - Reusable countdown utility for resend buttons and rate limiting
384
531
  *
@@ -637,122 +784,30 @@ function generateState() {
637
784
  * Handles all HTTP communication with the Identity App endpoints.
638
785
  * Single responsibility: API calls only.
639
786
  */
640
- /** Default request timeout in milliseconds (30 seconds) */
641
- const DEFAULT_TIMEOUT_MS$1 = 30000;
642
- class IdentityApi {
643
- constructor(config, timeoutMs = DEFAULT_TIMEOUT_MS$1) {
787
+ class IdentityApi extends SparkVaultApi {
788
+ constructor(config, timeoutMs) {
789
+ super(timeoutMs);
644
790
  /** Cached config promise - allows preloading and deduplication */
645
791
  this.configCache = null;
646
- /**
647
- * Abort controller for cancelling all pending requests on close.
648
- * Replaced by resetSession() at the start of every user-facing session.
649
- */
650
- this.closeController = new AbortController();
651
792
  this.config = config;
652
- this.timeoutMs = timeoutMs;
653
- }
654
- /**
655
- * Abort all pending requests.
656
- * Call this when the renderer is closed to prevent orphaned requests.
657
- */
658
- abort() {
659
- this.closeController.abort();
660
- }
661
- /**
662
- * Check if the API has been aborted.
663
- */
664
- isAborted() {
665
- return this.closeController.signal.aborted;
666
- }
667
- /**
668
- * Reset the session lifecycle by swapping in a fresh AbortController.
669
- * The Module calls this at the start of every user-facing session so a
670
- * previously-closed session can never leak its aborted state into the
671
- * next one. The config cache is intentionally preserved across sessions
672
- * so preloadConfig() keeps making modal opens instant.
673
- */
674
- resetSession() {
675
- this.closeController = new AbortController();
676
793
  }
677
794
  get baseUrl() {
678
795
  return `${this.config.identityBaseUrl}/${this.config.accountId}`;
679
796
  }
797
+ makeError(message, code, statusCode) {
798
+ return new IdentityApiError(message, code, statusCode);
799
+ }
680
800
  async request(method, endpoint, body) {
681
- // Don't start new requests if already aborted (renderer closed)
682
- if (this.closeController.signal.aborted) {
683
- throw new IdentityApiError('Request cancelled', 'cancelled', 0);
684
- }
685
- const url = `${this.baseUrl}${endpoint}`;
686
- const headers = {
687
- 'Accept': 'application/json',
688
- };
689
- if (body) {
690
- headers['Content-Type'] = 'application/json';
691
- }
692
- // Create abort controller for request timeout
693
- const timeoutController = new AbortController();
694
- const timeoutId = setTimeout(() => timeoutController.abort(), this.timeoutMs);
695
- // Combine timeout and close signals - abort on either
696
- // Use AbortSignal.any if available, otherwise fall back to manual linking
697
- let combinedSignal;
698
- if ('any' in AbortSignal) {
699
- combinedSignal = AbortSignal.any([
700
- timeoutController.signal,
701
- this.closeController.signal,
702
- ]);
703
- }
704
- else {
705
- // Fallback for older browsers: link close signal to timeout controller
706
- combinedSignal = timeoutController.signal;
707
- const onClose = () => timeoutController.abort();
708
- this.closeController.signal.addEventListener('abort', onClose);
709
- }
710
- try {
711
- const response = await fetch(url, {
712
- method,
713
- headers,
714
- body: body ? JSON.stringify(body) : undefined,
715
- signal: combinedSignal,
716
- });
717
- const json = await response.json();
718
- if (!response.ok) {
719
- // API error responses have format: { error: { code, message, details }, meta: {...} }
720
- const errorData = json.error || {};
721
- const message = errorData.message || json.message || 'Request failed';
722
- const code = errorData.code || json.code || 'api_error';
723
- throw new IdentityApiError(message, code, response.status);
724
- }
725
- // API responses are wrapped in { data: ..., meta: ... }
726
- // Unwrap the data field
727
- return (json.data ?? json);
728
- }
729
- catch (error) {
730
- // Convert AbortError to a more descriptive error.
731
- // Check error.name directly (DOMException may not extend Error in all environments).
732
- if (error != null && typeof error === 'object' && 'name' in error && error.name === 'AbortError') {
733
- // Check which signal triggered the abort
734
- if (this.closeController.signal.aborted) {
735
- throw new IdentityApiError('Request cancelled', 'cancelled', 0);
736
- }
737
- throw new IdentityApiError('Request timed out', 'timeout', 0);
738
- }
739
- // Convert TypeError to a user-friendly network error.
740
- // WebKit (Safari/iOS) throws TypeError("Load failed"),
741
- // Chrome/Firefox throw TypeError("Failed to fetch")
742
- // when fetch() fails at the browser level (network error, CORS, DNS, etc.).
743
- if (error instanceof TypeError) {
744
- throw new IdentityApiError('Unable to connect. Please check your connection and try again.', 'network_error', 0);
745
- }
746
- // Re-throw IdentityApiError as-is (already has user-friendly message)
747
- if (error instanceof IdentityApiError) {
748
- throw error;
749
- }
750
- // Convert any other unexpected errors to a structured error
751
- throw new IdentityApiError(error instanceof Error ? error.message : 'An unexpected error occurred', 'unknown_error', 0);
752
- }
753
- finally {
754
- clearTimeout(timeoutId);
755
- }
801
+ const { data } = await this.rawRequest(`${this.baseUrl}${endpoint}`, {
802
+ method,
803
+ body: body ? JSON.stringify(body) : undefined,
804
+ });
805
+ // API responses are wrapped in `{ data: ..., meta: ... }`. Unwrap the
806
+ // data field if present; otherwise return the raw payload.
807
+ const unwrapped = (typeof data === 'object' && data !== null && 'data' in data)
808
+ ? data.data
809
+ : data;
810
+ return unwrapped;
756
811
  }
757
812
  /**
758
813
  * Fetch SDK configuration (branding, enabled methods).
@@ -795,14 +850,19 @@ class IdentityApi {
795
850
  return this.configCache !== null;
796
851
  }
797
852
  /**
798
- * Check if an email has registered passkeys and validate email domain
853
+ * Check if an identity (email or phone) has registered passkeys.
799
854
  *
800
855
  * Returns:
801
- * - email_valid: whether the email domain has valid MX records
802
- * - has_passkey: whether any passkeys are registered (only meaningful if email_valid)
856
+ * - identity_valid: identity passes per-type validity check
857
+ * (email: MX lookup; phone: E.164 format)
858
+ * - has_passkey: whether any passkeys are registered for this identity
859
+ * (cross-tenant — single global RP)
860
+ * - email_valid: legacy alias for identity_valid, present for backward
861
+ * compatibility with older SDK versions
803
862
  */
804
- async checkPasskey(email) {
805
- return this.request('POST', '/passkey/check', { email });
863
+ async checkPasskey(identity, identityType = 'email') {
864
+ const body = identityType === 'phone' ? { phone: identity } : { email: identity };
865
+ return this.request('POST', '/passkey/check', body);
806
866
  }
807
867
  /**
808
868
  * Build auth context params for API requests (OIDC/simple mode).
@@ -847,12 +907,13 @@ class IdentityApi {
847
907
  });
848
908
  }
849
909
  /**
850
- * Start passkey registration
910
+ * Start passkey registration for an identity (email or phone).
851
911
  */
852
- async startPasskeyRegister(email) {
912
+ async startPasskeyRegister(identity, identityType = 'email') {
913
+ const body = identityType === 'phone' ? { phone: identity } : { email: identity };
853
914
  // Backend returns { options: PublicKeyCredentialCreationOptions, session: {...} }
854
915
  // Extract and flatten to match PasskeyChallengeResponse
855
- const response = await this.request('POST', '/passkey/register', { email });
916
+ const response = await this.request('POST', '/passkey/register', body);
856
917
  return {
857
918
  challenge: response.options.challenge,
858
919
  rpId: response.options.rp.id,
@@ -882,12 +943,13 @@ class IdentityApi {
882
943
  });
883
944
  }
884
945
  /**
885
- * Start passkey verification
946
+ * Start passkey verification for an identity (email or phone).
886
947
  */
887
- async startPasskeyVerify(email, authContext) {
948
+ async startPasskeyVerify(identity, identityType = 'email', authContext) {
949
+ const identityField = identityType === 'phone' ? { phone: identity } : { email: identity };
888
950
  // Backend returns { options: PublicKeyCredentialRequestOptions, session: {...} }
889
951
  // Extract and flatten to match PasskeyChallengeResponse
890
- const response = await this.request('POST', '/passkey/verify', { email, ...this.buildAuthContextParams(authContext) });
952
+ const response = await this.request('POST', '/passkey/verify', { ...identityField, ...this.buildAuthContextParams(authContext) });
891
953
  return {
892
954
  challenge: response.options.challenge,
893
955
  rpId: response.options.rpId,
@@ -943,12 +1005,10 @@ class IdentityApi {
943
1005
  });
944
1006
  }
945
1007
  }
946
- class IdentityApiError extends Error {
1008
+ class IdentityApiError extends SparkVaultError {
947
1009
  constructor(message, code, statusCode) {
948
- super(message);
1010
+ super(message, code, statusCode);
949
1011
  this.name = 'IdentityApiError';
950
- this.code = code;
951
- this.statusCode = statusCode;
952
1012
  Object.setPrototypeOf(this, IdentityApiError.prototype);
953
1013
  }
954
1014
  }
@@ -1296,10 +1356,12 @@ class PasskeyHandler {
1296
1356
  */
1297
1357
  async checkPasskey() {
1298
1358
  try {
1299
- const response = await this.api.checkPasskey(this.state.recipient);
1359
+ const response = await this.api.checkPasskey(this.state.recipient, this.state.identityType);
1300
1360
  this.state.setPasskeyStatus(response.has_passkey);
1361
+ // Prefer identity_valid; fall back to email_valid for older backends.
1362
+ const valid = response.identity_valid ?? response.email_valid;
1301
1363
  return {
1302
- emailValid: response.email_valid,
1364
+ emailValid: valid,
1303
1365
  hasPasskey: response.has_passkey,
1304
1366
  };
1305
1367
  }
@@ -1331,12 +1393,14 @@ class PasskeyHandler {
1331
1393
  */
1332
1394
  async openPasskeyPopup(mode) {
1333
1395
  return new Promise((resolve) => {
1334
- // Build popup URL
1396
+ // Build popup URL. Pass identity as `email` or `phone` (mutually
1397
+ // exclusive) so the popup can render and POST the correct field.
1335
1398
  const params = new URLSearchParams({
1336
1399
  mode,
1337
- email: this.state.recipient,
1338
1400
  origin: window.location.origin,
1339
1401
  });
1402
+ const identityParam = this.state.identityType === 'phone' ? 'phone' : 'email';
1403
+ params.set(identityParam, this.state.recipient);
1340
1404
  // Add auth context for OIDC/simple mode flows
1341
1405
  const authContext = this.state.authContext;
1342
1406
  if (authContext?.authRequestId) {
@@ -6523,23 +6587,18 @@ class IdentityModule {
6523
6587
  *
6524
6588
  * API client for vault upload operations.
6525
6589
  */
6526
- /** Default request timeout in milliseconds (30 seconds) */
6527
- const DEFAULT_TIMEOUT_MS = 30000;
6528
6590
  /**
6529
- * Error class for upload API errors.
6591
+ * Error class for upload API errors. Inherits `code` and `statusCode` from
6592
+ * SparkVaultError — no duplicate `httpStatus` field.
6530
6593
  */
6531
6594
  class UploadApiError extends SparkVaultError {
6532
- constructor(message, code, httpStatus) {
6533
- super(message, code, httpStatus);
6534
- this.httpStatus = httpStatus;
6595
+ constructor(message, code, statusCode) {
6596
+ super(message, code, statusCode);
6535
6597
  this.name = 'UploadApiError';
6536
6598
  Object.setPrototypeOf(this, UploadApiError.prototype);
6537
6599
  }
6538
6600
  }
6539
- /**
6540
- * Runtime validation for VaultUploadInfoResponse.
6541
- * Validates required fields exist and have correct types.
6542
- */
6601
+ /** Runtime validation for VaultUploadInfoResponse. */
6543
6602
  function isVaultUploadInfoResponse(data) {
6544
6603
  if (typeof data !== 'object' || data === null)
6545
6604
  return false;
@@ -6555,52 +6614,30 @@ function isVaultUploadInfoResponse(data) {
6555
6614
  typeof d.encryption.algorithm === 'string' &&
6556
6615
  (d.forge_status === 'active' || d.forge_status === 'inactive'));
6557
6616
  }
6558
- /**
6559
- * Runtime validation for InitiateUploadResponse.
6560
- */
6617
+ /** Runtime validation for InitiateUploadResponse. */
6561
6618
  function isInitiateUploadResponse(data) {
6562
6619
  if (typeof data !== 'object' || data === null)
6563
6620
  return false;
6564
6621
  const d = data;
6565
6622
  return typeof d.forge_url === 'string' && typeof d.ingot_id === 'string';
6566
6623
  }
6567
- class UploadApi {
6624
+ class UploadApi extends SparkVaultApi {
6568
6625
  constructor(config) {
6569
- /**
6570
- * Abort controller for cancelling all pending requests on close.
6571
- * Replaced by resetSession() at the start of every user-facing session.
6572
- */
6573
- this.closeController = new AbortController();
6626
+ super(config.timeout);
6574
6627
  this.config = config;
6575
- this.timeoutMs = config.timeout ?? DEFAULT_TIMEOUT_MS;
6576
- }
6577
- /**
6578
- * Abort all pending requests.
6579
- * Call this when the renderer is closed to prevent orphaned requests.
6580
- */
6581
- abort() {
6582
- this.closeController.abort();
6583
- }
6584
- /**
6585
- * Check if the API has been aborted.
6586
- */
6587
- isAborted() {
6588
- return this.closeController.signal.aborted;
6589
6628
  }
6590
- /**
6591
- * Get the abort signal for external use (e.g., XHR uploads).
6592
- */
6593
- getAbortSignal() {
6594
- return this.closeController.signal;
6629
+ makeError(message, code, statusCode) {
6630
+ return new UploadApiError(message, code, statusCode);
6595
6631
  }
6596
6632
  /**
6597
- * Reset the session lifecycle by swapping in a fresh AbortController.
6598
- * The Module calls this at the start of every user-facing session so a
6599
- * previously-closed session can never leak its aborted state into the
6600
- * next one. (Long-lived caches on `this` are intentionally preserved.)
6633
+ * Issue a request and unwrap the standard `{ data, meta }` envelope.
6601
6634
  */
6602
- resetSession() {
6603
- this.closeController = new AbortController();
6635
+ async request(url, options) {
6636
+ const { data: raw, status } = await this.rawRequest(url, options);
6637
+ const data = (typeof raw === 'object' && raw !== null && 'data' in raw)
6638
+ ? raw.data
6639
+ : raw;
6640
+ return { data, status };
6604
6641
  }
6605
6642
  /**
6606
6643
  * Get vault upload info (public endpoint).
@@ -6650,85 +6687,6 @@ class UploadApi {
6650
6687
  ingotId: response.data.ingot_id,
6651
6688
  };
6652
6689
  }
6653
- /**
6654
- * Internal request method with timeout handling and error context.
6655
- */
6656
- async request(url, options) {
6657
- // Don't start new requests if already aborted (renderer closed)
6658
- if (this.closeController.signal.aborted) {
6659
- throw new UploadApiError('Request cancelled', 'cancelled', 0);
6660
- }
6661
- const timeoutController = new AbortController();
6662
- const timeoutId = setTimeout(() => timeoutController.abort(), this.timeoutMs);
6663
- // Combine timeout and close signals - abort on either
6664
- let combinedSignal;
6665
- if ('any' in AbortSignal) {
6666
- combinedSignal = AbortSignal.any([
6667
- timeoutController.signal,
6668
- this.closeController.signal,
6669
- ]);
6670
- }
6671
- else {
6672
- // Fallback for older browsers: link close signal to timeout controller
6673
- combinedSignal = timeoutController.signal;
6674
- const onClose = () => timeoutController.abort();
6675
- this.closeController.signal.addEventListener('abort', onClose);
6676
- }
6677
- try {
6678
- const response = await fetch(url, {
6679
- method: options.method,
6680
- headers: {
6681
- 'Content-Type': 'application/json',
6682
- 'Accept': 'application/json',
6683
- },
6684
- body: options.body,
6685
- signal: combinedSignal,
6686
- });
6687
- const json = await response.json();
6688
- if (!response.ok) {
6689
- // Safely extract error fields with runtime type checking
6690
- const errorData = typeof json === 'object' && json !== null ? json : {};
6691
- const errorObj = typeof errorData.error === 'object' && errorData.error !== null
6692
- ? errorData.error
6693
- : errorData;
6694
- const message = (typeof errorObj.message === 'string' ? errorObj.message : null) ??
6695
- (typeof errorObj.error === 'string' ? errorObj.error : null) ??
6696
- 'Request failed';
6697
- const code = typeof errorObj.code === 'string' ? errorObj.code : 'api_error';
6698
- throw new UploadApiError(message, code, response.status);
6699
- }
6700
- // API responses are wrapped in { data: ..., meta: ... }
6701
- const data = typeof json === 'object' && json !== null && 'data' in json
6702
- ? json.data
6703
- : json;
6704
- return { data, status: response.status };
6705
- }
6706
- catch (error) {
6707
- // Re-throw SparkVault errors as-is
6708
- if (error instanceof SparkVaultError) {
6709
- throw error;
6710
- }
6711
- // Convert AbortError to appropriate error
6712
- if (error instanceof DOMException && error.name === 'AbortError') {
6713
- // Check which signal triggered the abort
6714
- if (this.closeController.signal.aborted) {
6715
- throw new UploadApiError('Request cancelled', 'cancelled', 0);
6716
- }
6717
- throw new TimeoutError(`Request timed out after ${this.timeoutMs}ms`);
6718
- }
6719
- // Network errors with context
6720
- if (error instanceof TypeError) {
6721
- throw new NetworkError(`Network request failed: ${error.message}`, { url, method: options.method });
6722
- }
6723
- // Unknown errors - preserve message
6724
- throw new NetworkError(error instanceof Error
6725
- ? `Request failed: ${error.message}`
6726
- : 'Request failed: Unknown error');
6727
- }
6728
- finally {
6729
- clearTimeout(timeoutId);
6730
- }
6731
- }
6732
6690
  }
6733
6691
 
6734
6692
  /**
@@ -9321,25 +9279,25 @@ class UploadRenderer {
9321
9279
  title: 'Upload Widget Not Enabled',
9322
9280
  message: 'This vault has not enabled the embedded upload widget. Enable it on the vault\'s settings page in the SparkVault dashboard to start accepting uploads here.',
9323
9281
  code: 'UPLOAD_SOURCE_DISABLED',
9324
- httpStatus: error.httpStatus,
9282
+ statusCode: error.statusCode,
9325
9283
  });
9326
9284
  }
9327
- else if (error.httpStatus === 404) {
9285
+ else if (error.statusCode === 404) {
9328
9286
  this.setState({
9329
9287
  view: 'error',
9330
9288
  title: 'Upload Page Not Found',
9331
9289
  message: 'The requested upload endpoint could not be located. This may occur if the upload link has expired or was entered incorrectly.',
9332
9290
  code: 'NOT_FOUND',
9333
- httpStatus: 404,
9291
+ statusCode: 404,
9334
9292
  });
9335
9293
  }
9336
- else if (error.httpStatus === 402) {
9294
+ else if (error.statusCode === 402) {
9337
9295
  this.setState({
9338
9296
  view: 'error',
9339
9297
  title: 'Service Unavailable',
9340
9298
  message: 'This upload endpoint has been temporarily disabled due to an account billing issue. Please contact the organization\'s administrator.',
9341
9299
  code: 'PAYMENT_REQUIRED',
9342
- httpStatus: 402,
9300
+ statusCode: 402,
9343
9301
  });
9344
9302
  }
9345
9303
  else {
@@ -9348,7 +9306,7 @@ class UploadRenderer {
9348
9306
  title: 'Upload Failed',
9349
9307
  message: error.message,
9350
9308
  code: error.code,
9351
- httpStatus: error.httpStatus,
9309
+ statusCode: error.statusCode,
9352
9310
  });
9353
9311
  }
9354
9312
  this.callbacks.onError(error);