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