@ovixa/auth-client 0.2.0 → 0.3.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/CHANGELOG.md +26 -7
- package/dist/{chunk-WON3EB4B.js → chunk-YFMPRWGI.js} +2 -2
- package/dist/{chunk-5ZWKDQQM.js → chunk-ZUTZYJDF.js} +347 -289
- package/dist/chunk-ZUTZYJDF.js.map +1 -0
- package/dist/index.d.ts +178 -150
- package/dist/index.js +1 -1
- package/dist/middleware/astro.js +2 -2
- package/dist/middleware/express.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-5ZWKDQQM.js.map +0 -1
- /package/dist/{chunk-WON3EB4B.js.map → chunk-YFMPRWGI.js.map} +0 -0
|
@@ -442,284 +442,6 @@ var OvixaAuth = class {
|
|
|
442
442
|
url.searchParams.set("redirect_uri", options.redirectUri);
|
|
443
443
|
return url.toString();
|
|
444
444
|
}
|
|
445
|
-
// ============================================================
|
|
446
|
-
// WebAuthn / Passkey Methods
|
|
447
|
-
// ============================================================
|
|
448
|
-
/**
|
|
449
|
-
* Get registration options for creating a new passkey.
|
|
450
|
-
*
|
|
451
|
-
* The user must be logged in to register a passkey. Use the returned options
|
|
452
|
-
* with navigator.credentials.create() to create the credential.
|
|
453
|
-
*
|
|
454
|
-
* @param options - Registration options with access token
|
|
455
|
-
* @returns Registration options to pass to navigator.credentials.create()
|
|
456
|
-
* @throws {OvixaAuthError} If request fails
|
|
457
|
-
*
|
|
458
|
-
* @example
|
|
459
|
-
* ```typescript
|
|
460
|
-
* // User must be logged in
|
|
461
|
-
* const options = await auth.getPasskeyRegistrationOptions({
|
|
462
|
-
* accessToken: session.accessToken,
|
|
463
|
-
* });
|
|
464
|
-
*
|
|
465
|
-
* // Create the credential using the browser API
|
|
466
|
-
* const credential = await navigator.credentials.create({
|
|
467
|
-
* publicKey: {
|
|
468
|
-
* ...options,
|
|
469
|
-
* challenge: base64UrlToArrayBuffer(options.challenge),
|
|
470
|
-
* user: {
|
|
471
|
-
* ...options.user,
|
|
472
|
-
* id: new TextEncoder().encode(options.user.id),
|
|
473
|
-
* },
|
|
474
|
-
* excludeCredentials: options.excludeCredentials.map(c => ({
|
|
475
|
-
* ...c,
|
|
476
|
-
* id: base64UrlToArrayBuffer(c.id),
|
|
477
|
-
* })),
|
|
478
|
-
* },
|
|
479
|
-
* });
|
|
480
|
-
*
|
|
481
|
-
* // Verify the registration with the server
|
|
482
|
-
* await auth.verifyPasskeyRegistration({
|
|
483
|
-
* accessToken: session.accessToken,
|
|
484
|
-
* registration: formatRegistrationResponse(credential),
|
|
485
|
-
* deviceName: 'My MacBook',
|
|
486
|
-
* });
|
|
487
|
-
* ```
|
|
488
|
-
*/
|
|
489
|
-
async getPasskeyRegistrationOptions(options) {
|
|
490
|
-
const url = `${this.config.authUrl}/webauthn/register/options`;
|
|
491
|
-
try {
|
|
492
|
-
const response = await fetch(url, {
|
|
493
|
-
method: "POST",
|
|
494
|
-
headers: {
|
|
495
|
-
"Content-Type": "application/json",
|
|
496
|
-
Authorization: `Bearer ${options.accessToken}`
|
|
497
|
-
},
|
|
498
|
-
body: JSON.stringify({
|
|
499
|
-
realm_id: this.config.realmId
|
|
500
|
-
})
|
|
501
|
-
});
|
|
502
|
-
if (!response.ok) {
|
|
503
|
-
await this.handleErrorResponse(response);
|
|
504
|
-
}
|
|
505
|
-
return await response.json();
|
|
506
|
-
} catch (error) {
|
|
507
|
-
if (error instanceof OvixaAuthError) {
|
|
508
|
-
throw error;
|
|
509
|
-
}
|
|
510
|
-
if (error instanceof Error) {
|
|
511
|
-
throw new OvixaAuthError(`Network error: ${error.message}`, "NETWORK_ERROR");
|
|
512
|
-
}
|
|
513
|
-
throw new OvixaAuthError("Request failed", "REQUEST_FAILED");
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
/**
|
|
517
|
-
* Verify a passkey registration and store the credential.
|
|
518
|
-
*
|
|
519
|
-
* Call this after navigator.credentials.create() succeeds to complete the
|
|
520
|
-
* registration on the server.
|
|
521
|
-
*
|
|
522
|
-
* @param options - Verification options with registration response
|
|
523
|
-
* @returns Registration response with credential ID
|
|
524
|
-
* @throws {OvixaAuthError} If verification fails
|
|
525
|
-
*
|
|
526
|
-
* @example
|
|
527
|
-
* ```typescript
|
|
528
|
-
* const result = await auth.verifyPasskeyRegistration({
|
|
529
|
-
* accessToken: session.accessToken,
|
|
530
|
-
* registration: {
|
|
531
|
-
* id: credential.id,
|
|
532
|
-
* rawId: arrayBufferToBase64Url(credential.rawId),
|
|
533
|
-
* type: 'public-key',
|
|
534
|
-
* response: {
|
|
535
|
-
* clientDataJSON: arrayBufferToBase64Url(credential.response.clientDataJSON),
|
|
536
|
-
* attestationObject: arrayBufferToBase64Url(credential.response.attestationObject),
|
|
537
|
-
* transports: credential.response.getTransports?.(),
|
|
538
|
-
* },
|
|
539
|
-
* },
|
|
540
|
-
* deviceName: 'My MacBook',
|
|
541
|
-
* });
|
|
542
|
-
*
|
|
543
|
-
* console.log('Passkey registered:', result.credential_id);
|
|
544
|
-
* ```
|
|
545
|
-
*/
|
|
546
|
-
async verifyPasskeyRegistration(options) {
|
|
547
|
-
const url = `${this.config.authUrl}/webauthn/register/verify`;
|
|
548
|
-
const body = {
|
|
549
|
-
realm_id: this.config.realmId,
|
|
550
|
-
registration: options.registration
|
|
551
|
-
};
|
|
552
|
-
if (options.deviceName) {
|
|
553
|
-
body.device_name = options.deviceName;
|
|
554
|
-
}
|
|
555
|
-
try {
|
|
556
|
-
const response = await fetch(url, {
|
|
557
|
-
method: "POST",
|
|
558
|
-
headers: {
|
|
559
|
-
"Content-Type": "application/json",
|
|
560
|
-
Authorization: `Bearer ${options.accessToken}`
|
|
561
|
-
},
|
|
562
|
-
body: JSON.stringify(body)
|
|
563
|
-
});
|
|
564
|
-
if (!response.ok) {
|
|
565
|
-
await this.handleErrorResponse(response);
|
|
566
|
-
}
|
|
567
|
-
return await response.json();
|
|
568
|
-
} catch (error) {
|
|
569
|
-
if (error instanceof OvixaAuthError) {
|
|
570
|
-
throw error;
|
|
571
|
-
}
|
|
572
|
-
if (error instanceof Error) {
|
|
573
|
-
throw new OvixaAuthError(`Network error: ${error.message}`, "NETWORK_ERROR");
|
|
574
|
-
}
|
|
575
|
-
throw new OvixaAuthError("Request failed", "REQUEST_FAILED");
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
/**
|
|
579
|
-
* Get authentication options for signing in with a passkey.
|
|
580
|
-
*
|
|
581
|
-
* Use the returned options with navigator.credentials.get() to authenticate.
|
|
582
|
-
*
|
|
583
|
-
* @param options - Optional email for allowCredentials hint
|
|
584
|
-
* @returns Authentication options to pass to navigator.credentials.get()
|
|
585
|
-
* @throws {OvixaAuthError} If request fails
|
|
586
|
-
*
|
|
587
|
-
* @example
|
|
588
|
-
* ```typescript
|
|
589
|
-
* // Option 1: Discoverable credentials (no email needed)
|
|
590
|
-
* const options = await auth.getPasskeyAuthenticationOptions();
|
|
591
|
-
*
|
|
592
|
-
* // Option 2: Provide email for allowCredentials hint
|
|
593
|
-
* const options = await auth.getPasskeyAuthenticationOptions({
|
|
594
|
-
* email: 'user@example.com',
|
|
595
|
-
* });
|
|
596
|
-
*
|
|
597
|
-
* // Authenticate using the browser API
|
|
598
|
-
* const credential = await navigator.credentials.get({
|
|
599
|
-
* publicKey: {
|
|
600
|
-
* ...options,
|
|
601
|
-
* challenge: base64UrlToArrayBuffer(options.challenge),
|
|
602
|
-
* allowCredentials: options.allowCredentials.map(c => ({
|
|
603
|
-
* ...c,
|
|
604
|
-
* id: base64UrlToArrayBuffer(c.id),
|
|
605
|
-
* })),
|
|
606
|
-
* },
|
|
607
|
-
* });
|
|
608
|
-
*
|
|
609
|
-
* // Verify and get tokens
|
|
610
|
-
* const tokens = await auth.verifyPasskeyAuthentication({
|
|
611
|
-
* authentication: formatAuthenticationResponse(credential),
|
|
612
|
-
* });
|
|
613
|
-
* ```
|
|
614
|
-
*/
|
|
615
|
-
async getPasskeyAuthenticationOptions(options) {
|
|
616
|
-
const url = `${this.config.authUrl}/webauthn/authenticate/options`;
|
|
617
|
-
const body = {
|
|
618
|
-
realm_id: this.config.realmId
|
|
619
|
-
};
|
|
620
|
-
if (options?.email) {
|
|
621
|
-
body.email = options.email;
|
|
622
|
-
}
|
|
623
|
-
try {
|
|
624
|
-
const response = await fetch(url, {
|
|
625
|
-
method: "POST",
|
|
626
|
-
headers: {
|
|
627
|
-
"Content-Type": "application/json"
|
|
628
|
-
},
|
|
629
|
-
body: JSON.stringify(body)
|
|
630
|
-
});
|
|
631
|
-
if (!response.ok) {
|
|
632
|
-
await this.handleErrorResponse(response);
|
|
633
|
-
}
|
|
634
|
-
return await response.json();
|
|
635
|
-
} catch (error) {
|
|
636
|
-
if (error instanceof OvixaAuthError) {
|
|
637
|
-
throw error;
|
|
638
|
-
}
|
|
639
|
-
if (error instanceof Error) {
|
|
640
|
-
throw new OvixaAuthError(`Network error: ${error.message}`, "NETWORK_ERROR");
|
|
641
|
-
}
|
|
642
|
-
throw new OvixaAuthError("Request failed", "REQUEST_FAILED");
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
/**
|
|
646
|
-
* Verify a passkey authentication and get tokens.
|
|
647
|
-
*
|
|
648
|
-
* Call this after navigator.credentials.get() succeeds to complete the
|
|
649
|
-
* authentication and receive access/refresh tokens.
|
|
650
|
-
*
|
|
651
|
-
* @param options - Verification options with authentication response
|
|
652
|
-
* @returns Token response with access and refresh tokens
|
|
653
|
-
* @throws {OvixaAuthError} If verification fails
|
|
654
|
-
*
|
|
655
|
-
* @example
|
|
656
|
-
* ```typescript
|
|
657
|
-
* const tokens = await auth.verifyPasskeyAuthentication({
|
|
658
|
-
* authentication: {
|
|
659
|
-
* id: credential.id,
|
|
660
|
-
* rawId: arrayBufferToBase64Url(credential.rawId),
|
|
661
|
-
* type: 'public-key',
|
|
662
|
-
* response: {
|
|
663
|
-
* clientDataJSON: arrayBufferToBase64Url(credential.response.clientDataJSON),
|
|
664
|
-
* authenticatorData: arrayBufferToBase64Url(credential.response.authenticatorData),
|
|
665
|
-
* signature: arrayBufferToBase64Url(credential.response.signature),
|
|
666
|
-
* userHandle: credential.response.userHandle
|
|
667
|
-
* ? arrayBufferToBase64Url(credential.response.userHandle)
|
|
668
|
-
* : null,
|
|
669
|
-
* },
|
|
670
|
-
* },
|
|
671
|
-
* });
|
|
672
|
-
*
|
|
673
|
-
* // Use the tokens
|
|
674
|
-
* console.log('Logged in!', tokens.access_token);
|
|
675
|
-
* ```
|
|
676
|
-
*/
|
|
677
|
-
async verifyPasskeyAuthentication(options) {
|
|
678
|
-
const url = `${this.config.authUrl}/webauthn/authenticate/verify`;
|
|
679
|
-
const body = {
|
|
680
|
-
realm_id: this.config.realmId,
|
|
681
|
-
authentication: options.authentication
|
|
682
|
-
};
|
|
683
|
-
try {
|
|
684
|
-
const response = await fetch(url, {
|
|
685
|
-
method: "POST",
|
|
686
|
-
headers: {
|
|
687
|
-
"Content-Type": "application/json"
|
|
688
|
-
},
|
|
689
|
-
body: JSON.stringify(body)
|
|
690
|
-
});
|
|
691
|
-
if (!response.ok) {
|
|
692
|
-
await this.handleErrorResponse(response);
|
|
693
|
-
}
|
|
694
|
-
return await response.json();
|
|
695
|
-
} catch (error) {
|
|
696
|
-
if (error instanceof OvixaAuthError) {
|
|
697
|
-
throw error;
|
|
698
|
-
}
|
|
699
|
-
if (error instanceof Error) {
|
|
700
|
-
throw new OvixaAuthError(`Network error: ${error.message}`, "NETWORK_ERROR");
|
|
701
|
-
}
|
|
702
|
-
throw new OvixaAuthError("Request failed", "REQUEST_FAILED");
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
/**
|
|
706
|
-
* Handle error response from the server.
|
|
707
|
-
* @throws {OvixaAuthError} Always throws with appropriate error details
|
|
708
|
-
*/
|
|
709
|
-
async handleErrorResponse(response) {
|
|
710
|
-
const errorBody = await response.json().catch(() => ({}));
|
|
711
|
-
const errorData = errorBody;
|
|
712
|
-
let errorCode;
|
|
713
|
-
let errorMessage;
|
|
714
|
-
if (typeof errorData.error === "object" && errorData.error) {
|
|
715
|
-
errorCode = errorData.error.code || this.mapHttpStatusToErrorCode(response.status);
|
|
716
|
-
errorMessage = errorData.error.message || "Request failed";
|
|
717
|
-
} else {
|
|
718
|
-
errorCode = this.mapHttpStatusToErrorCode(response.status);
|
|
719
|
-
errorMessage = typeof errorData.error === "string" ? errorData.error : "Request failed";
|
|
720
|
-
}
|
|
721
|
-
throw new OvixaAuthError(errorMessage, errorCode, response.status);
|
|
722
|
-
}
|
|
723
445
|
/**
|
|
724
446
|
* Exchange an OAuth authorization code for tokens.
|
|
725
447
|
*
|
|
@@ -961,6 +683,37 @@ var OvixaAuth = class {
|
|
|
961
683
|
}
|
|
962
684
|
return new OvixaAuthAdmin(this.config);
|
|
963
685
|
}
|
|
686
|
+
/**
|
|
687
|
+
* Get the WebAuthn API interface for passkey operations.
|
|
688
|
+
*
|
|
689
|
+
* The WebAuthn namespace provides methods for registering and authenticating
|
|
690
|
+
* with passkeys, as well as managing existing passkeys.
|
|
691
|
+
*
|
|
692
|
+
* @returns OvixaAuthWebAuthn interface for passkey operations
|
|
693
|
+
*
|
|
694
|
+
* @example
|
|
695
|
+
* ```typescript
|
|
696
|
+
* const auth = new OvixaAuth({
|
|
697
|
+
* authUrl: 'https://auth.ovixa.io',
|
|
698
|
+
* realmId: 'your-realm',
|
|
699
|
+
* });
|
|
700
|
+
*
|
|
701
|
+
* // Register a passkey
|
|
702
|
+
* const options = await auth.webauthn.getRegistrationOptions({ accessToken });
|
|
703
|
+
*
|
|
704
|
+
* // Authenticate with passkey
|
|
705
|
+
* const authOptions = await auth.webauthn.getAuthenticationOptions();
|
|
706
|
+
*
|
|
707
|
+
* // List passkeys
|
|
708
|
+
* const { passkeys } = await auth.webauthn.listPasskeys({ accessToken });
|
|
709
|
+
*
|
|
710
|
+
* // Delete a passkey
|
|
711
|
+
* await auth.webauthn.deletePasskey({ accessToken, credentialId });
|
|
712
|
+
* ```
|
|
713
|
+
*/
|
|
714
|
+
get webauthn() {
|
|
715
|
+
return new OvixaAuthWebAuthn(this.config);
|
|
716
|
+
}
|
|
964
717
|
};
|
|
965
718
|
var OvixaAuthAdmin = class {
|
|
966
719
|
config;
|
|
@@ -1051,6 +804,293 @@ var OvixaAuthAdmin = class {
|
|
|
1051
804
|
}
|
|
1052
805
|
}
|
|
1053
806
|
};
|
|
807
|
+
var OvixaAuthWebAuthn = class {
|
|
808
|
+
config;
|
|
809
|
+
constructor(config) {
|
|
810
|
+
this.config = config;
|
|
811
|
+
}
|
|
812
|
+
/**
|
|
813
|
+
* Get registration options for creating a new passkey.
|
|
814
|
+
*
|
|
815
|
+
* The user must be logged in to register a passkey. Use the returned options
|
|
816
|
+
* with navigator.credentials.create() to create the credential.
|
|
817
|
+
*
|
|
818
|
+
* @param options - Registration options with access token
|
|
819
|
+
* @returns Registration options to pass to navigator.credentials.create()
|
|
820
|
+
* @throws {OvixaAuthError} If request fails
|
|
821
|
+
*/
|
|
822
|
+
async getRegistrationOptions(options) {
|
|
823
|
+
const url = `${this.config.authUrl}/webauthn/register/options`;
|
|
824
|
+
try {
|
|
825
|
+
const response = await fetch(url, {
|
|
826
|
+
method: "POST",
|
|
827
|
+
headers: {
|
|
828
|
+
"Content-Type": "application/json",
|
|
829
|
+
Authorization: `Bearer ${options.accessToken}`
|
|
830
|
+
},
|
|
831
|
+
body: JSON.stringify({
|
|
832
|
+
realm_id: this.config.realmId
|
|
833
|
+
})
|
|
834
|
+
});
|
|
835
|
+
if (!response.ok) {
|
|
836
|
+
await this.handleErrorResponse(response);
|
|
837
|
+
}
|
|
838
|
+
return await response.json();
|
|
839
|
+
} catch (error) {
|
|
840
|
+
if (error instanceof OvixaAuthError) {
|
|
841
|
+
throw error;
|
|
842
|
+
}
|
|
843
|
+
if (error instanceof Error) {
|
|
844
|
+
throw new OvixaAuthError(`Network error: ${error.message}`, "NETWORK_ERROR");
|
|
845
|
+
}
|
|
846
|
+
throw new OvixaAuthError("Request failed", "REQUEST_FAILED");
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
850
|
+
* Verify a passkey registration and store the credential.
|
|
851
|
+
*
|
|
852
|
+
* Call this after navigator.credentials.create() succeeds to complete the
|
|
853
|
+
* registration on the server.
|
|
854
|
+
*
|
|
855
|
+
* @param options - Verification options with registration response
|
|
856
|
+
* @returns Registration response with credential ID
|
|
857
|
+
* @throws {OvixaAuthError} If verification fails
|
|
858
|
+
*/
|
|
859
|
+
async verifyRegistration(options) {
|
|
860
|
+
const url = `${this.config.authUrl}/webauthn/register/verify`;
|
|
861
|
+
const body = {
|
|
862
|
+
realm_id: this.config.realmId,
|
|
863
|
+
registration: options.registration
|
|
864
|
+
};
|
|
865
|
+
if (options.deviceName) {
|
|
866
|
+
body.device_name = options.deviceName;
|
|
867
|
+
}
|
|
868
|
+
try {
|
|
869
|
+
const response = await fetch(url, {
|
|
870
|
+
method: "POST",
|
|
871
|
+
headers: {
|
|
872
|
+
"Content-Type": "application/json",
|
|
873
|
+
Authorization: `Bearer ${options.accessToken}`
|
|
874
|
+
},
|
|
875
|
+
body: JSON.stringify(body)
|
|
876
|
+
});
|
|
877
|
+
if (!response.ok) {
|
|
878
|
+
await this.handleErrorResponse(response);
|
|
879
|
+
}
|
|
880
|
+
return await response.json();
|
|
881
|
+
} catch (error) {
|
|
882
|
+
if (error instanceof OvixaAuthError) {
|
|
883
|
+
throw error;
|
|
884
|
+
}
|
|
885
|
+
if (error instanceof Error) {
|
|
886
|
+
throw new OvixaAuthError(`Network error: ${error.message}`, "NETWORK_ERROR");
|
|
887
|
+
}
|
|
888
|
+
throw new OvixaAuthError("Request failed", "REQUEST_FAILED");
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* Get authentication options for signing in with a passkey.
|
|
893
|
+
*
|
|
894
|
+
* Use the returned options with navigator.credentials.get() to authenticate.
|
|
895
|
+
*
|
|
896
|
+
* @param options - Optional email for allowCredentials hint
|
|
897
|
+
* @returns Authentication options to pass to navigator.credentials.get()
|
|
898
|
+
* @throws {OvixaAuthError} If request fails
|
|
899
|
+
*/
|
|
900
|
+
async getAuthenticationOptions(options) {
|
|
901
|
+
const url = `${this.config.authUrl}/webauthn/authenticate/options`;
|
|
902
|
+
const body = {
|
|
903
|
+
realm_id: this.config.realmId
|
|
904
|
+
};
|
|
905
|
+
if (options?.email) {
|
|
906
|
+
body.email = options.email;
|
|
907
|
+
}
|
|
908
|
+
try {
|
|
909
|
+
const response = await fetch(url, {
|
|
910
|
+
method: "POST",
|
|
911
|
+
headers: {
|
|
912
|
+
"Content-Type": "application/json"
|
|
913
|
+
},
|
|
914
|
+
body: JSON.stringify(body)
|
|
915
|
+
});
|
|
916
|
+
if (!response.ok) {
|
|
917
|
+
await this.handleErrorResponse(response);
|
|
918
|
+
}
|
|
919
|
+
return await response.json();
|
|
920
|
+
} catch (error) {
|
|
921
|
+
if (error instanceof OvixaAuthError) {
|
|
922
|
+
throw error;
|
|
923
|
+
}
|
|
924
|
+
if (error instanceof Error) {
|
|
925
|
+
throw new OvixaAuthError(`Network error: ${error.message}`, "NETWORK_ERROR");
|
|
926
|
+
}
|
|
927
|
+
throw new OvixaAuthError("Request failed", "REQUEST_FAILED");
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
/**
|
|
931
|
+
* Verify a passkey authentication and get tokens.
|
|
932
|
+
*
|
|
933
|
+
* Call this after navigator.credentials.get() succeeds to complete the
|
|
934
|
+
* authentication and receive access/refresh tokens.
|
|
935
|
+
*
|
|
936
|
+
* @param options - Verification options with authentication response
|
|
937
|
+
* @returns Token response with access and refresh tokens
|
|
938
|
+
* @throws {OvixaAuthError} If verification fails
|
|
939
|
+
*/
|
|
940
|
+
async authenticate(options) {
|
|
941
|
+
const url = `${this.config.authUrl}/webauthn/authenticate/verify`;
|
|
942
|
+
const body = {
|
|
943
|
+
realm_id: this.config.realmId,
|
|
944
|
+
authentication: options.authentication
|
|
945
|
+
};
|
|
946
|
+
try {
|
|
947
|
+
const response = await fetch(url, {
|
|
948
|
+
method: "POST",
|
|
949
|
+
headers: {
|
|
950
|
+
"Content-Type": "application/json"
|
|
951
|
+
},
|
|
952
|
+
body: JSON.stringify(body)
|
|
953
|
+
});
|
|
954
|
+
if (!response.ok) {
|
|
955
|
+
await this.handleErrorResponse(response);
|
|
956
|
+
}
|
|
957
|
+
return await response.json();
|
|
958
|
+
} catch (error) {
|
|
959
|
+
if (error instanceof OvixaAuthError) {
|
|
960
|
+
throw error;
|
|
961
|
+
}
|
|
962
|
+
if (error instanceof Error) {
|
|
963
|
+
throw new OvixaAuthError(`Network error: ${error.message}`, "NETWORK_ERROR");
|
|
964
|
+
}
|
|
965
|
+
throw new OvixaAuthError("Request failed", "REQUEST_FAILED");
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
/**
|
|
969
|
+
* List all passkeys for the authenticated user.
|
|
970
|
+
*
|
|
971
|
+
* @param options - Options with access token
|
|
972
|
+
* @returns List of passkeys
|
|
973
|
+
* @throws {OvixaAuthError} If request fails
|
|
974
|
+
*
|
|
975
|
+
* @example
|
|
976
|
+
* ```typescript
|
|
977
|
+
* const { passkeys } = await auth.webauthn.listPasskeys({ accessToken });
|
|
978
|
+
* for (const passkey of passkeys) {
|
|
979
|
+
* console.log(passkey.device_name, passkey.created_at);
|
|
980
|
+
* }
|
|
981
|
+
* ```
|
|
982
|
+
*/
|
|
983
|
+
async listPasskeys(options) {
|
|
984
|
+
const url = `${this.config.authUrl}/webauthn/passkeys`;
|
|
985
|
+
try {
|
|
986
|
+
const response = await fetch(url, {
|
|
987
|
+
method: "POST",
|
|
988
|
+
headers: {
|
|
989
|
+
"Content-Type": "application/json",
|
|
990
|
+
Authorization: `Bearer ${options.accessToken}`
|
|
991
|
+
},
|
|
992
|
+
body: JSON.stringify({
|
|
993
|
+
realm_id: this.config.realmId
|
|
994
|
+
})
|
|
995
|
+
});
|
|
996
|
+
if (!response.ok) {
|
|
997
|
+
await this.handleErrorResponse(response);
|
|
998
|
+
}
|
|
999
|
+
return await response.json();
|
|
1000
|
+
} catch (error) {
|
|
1001
|
+
if (error instanceof OvixaAuthError) {
|
|
1002
|
+
throw error;
|
|
1003
|
+
}
|
|
1004
|
+
if (error instanceof Error) {
|
|
1005
|
+
throw new OvixaAuthError(`Network error: ${error.message}`, "NETWORK_ERROR");
|
|
1006
|
+
}
|
|
1007
|
+
throw new OvixaAuthError("Request failed", "REQUEST_FAILED");
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
/**
|
|
1011
|
+
* Delete a passkey for the authenticated user.
|
|
1012
|
+
*
|
|
1013
|
+
* @param options - Options with access token and credential ID
|
|
1014
|
+
* @returns Success response
|
|
1015
|
+
* @throws {OvixaAuthError} If deletion fails
|
|
1016
|
+
*
|
|
1017
|
+
* @example
|
|
1018
|
+
* ```typescript
|
|
1019
|
+
* await auth.webauthn.deletePasskey({
|
|
1020
|
+
* accessToken,
|
|
1021
|
+
* credentialId: 'credential-id-to-delete',
|
|
1022
|
+
* });
|
|
1023
|
+
* ```
|
|
1024
|
+
*/
|
|
1025
|
+
async deletePasskey(options) {
|
|
1026
|
+
const url = `${this.config.authUrl}/webauthn/passkeys/${encodeURIComponent(options.credentialId)}`;
|
|
1027
|
+
try {
|
|
1028
|
+
const response = await fetch(url, {
|
|
1029
|
+
method: "DELETE",
|
|
1030
|
+
headers: {
|
|
1031
|
+
"Content-Type": "application/json",
|
|
1032
|
+
Authorization: `Bearer ${options.accessToken}`
|
|
1033
|
+
},
|
|
1034
|
+
body: JSON.stringify({
|
|
1035
|
+
realm_id: this.config.realmId
|
|
1036
|
+
})
|
|
1037
|
+
});
|
|
1038
|
+
if (!response.ok) {
|
|
1039
|
+
await this.handleErrorResponse(response);
|
|
1040
|
+
}
|
|
1041
|
+
return await response.json();
|
|
1042
|
+
} catch (error) {
|
|
1043
|
+
if (error instanceof OvixaAuthError) {
|
|
1044
|
+
throw error;
|
|
1045
|
+
}
|
|
1046
|
+
if (error instanceof Error) {
|
|
1047
|
+
throw new OvixaAuthError(`Network error: ${error.message}`, "NETWORK_ERROR");
|
|
1048
|
+
}
|
|
1049
|
+
throw new OvixaAuthError("Request failed", "REQUEST_FAILED");
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
/**
|
|
1053
|
+
* Handle error response from the server.
|
|
1054
|
+
* @throws {OvixaAuthError} Always throws with appropriate error details
|
|
1055
|
+
*/
|
|
1056
|
+
async handleErrorResponse(response) {
|
|
1057
|
+
const errorBody = await response.json().catch(() => ({}));
|
|
1058
|
+
const errorData = errorBody;
|
|
1059
|
+
let errorCode;
|
|
1060
|
+
let errorMessage;
|
|
1061
|
+
if (typeof errorData.error === "object" && errorData.error) {
|
|
1062
|
+
errorCode = errorData.error.code || this.mapHttpStatusToErrorCode(response.status);
|
|
1063
|
+
errorMessage = errorData.error.message || "Request failed";
|
|
1064
|
+
} else {
|
|
1065
|
+
errorCode = this.mapHttpStatusToErrorCode(response.status);
|
|
1066
|
+
errorMessage = typeof errorData.error === "string" ? errorData.error : "Request failed";
|
|
1067
|
+
}
|
|
1068
|
+
throw new OvixaAuthError(errorMessage, errorCode, response.status);
|
|
1069
|
+
}
|
|
1070
|
+
/**
|
|
1071
|
+
* Map HTTP status codes to error codes.
|
|
1072
|
+
*/
|
|
1073
|
+
mapHttpStatusToErrorCode(status) {
|
|
1074
|
+
switch (status) {
|
|
1075
|
+
case 400:
|
|
1076
|
+
return "BAD_REQUEST";
|
|
1077
|
+
case 401:
|
|
1078
|
+
return "UNAUTHORIZED";
|
|
1079
|
+
case 403:
|
|
1080
|
+
return "FORBIDDEN";
|
|
1081
|
+
case 404:
|
|
1082
|
+
return "NOT_FOUND";
|
|
1083
|
+
case 429:
|
|
1084
|
+
return "RATE_LIMITED";
|
|
1085
|
+
case 500:
|
|
1086
|
+
case 502:
|
|
1087
|
+
case 503:
|
|
1088
|
+
return "SERVER_ERROR";
|
|
1089
|
+
default:
|
|
1090
|
+
return "UNKNOWN_ERROR";
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
};
|
|
1054
1094
|
function arrayBufferToBase64Url(buffer) {
|
|
1055
1095
|
const bytes = new Uint8Array(buffer);
|
|
1056
1096
|
let binary = "";
|
|
@@ -1070,6 +1110,19 @@ function base64UrlToArrayBuffer(base64url) {
|
|
|
1070
1110
|
return bytes.buffer;
|
|
1071
1111
|
}
|
|
1072
1112
|
function prepareRegistrationOptions(options) {
|
|
1113
|
+
const excludeCredentials = Array.isArray(options.excludeCredentials) ? options.excludeCredentials : [];
|
|
1114
|
+
const decodedExcludeCredentials = [];
|
|
1115
|
+
for (const cred of excludeCredentials) {
|
|
1116
|
+
try {
|
|
1117
|
+
decodedExcludeCredentials.push({
|
|
1118
|
+
id: base64UrlToArrayBuffer(cred.id),
|
|
1119
|
+
type: cred.type,
|
|
1120
|
+
transports: cred.transports
|
|
1121
|
+
});
|
|
1122
|
+
} catch {
|
|
1123
|
+
console.warn("[ovixa/auth-client] Skipping malformed credential ID:", cred.id);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1073
1126
|
return {
|
|
1074
1127
|
challenge: base64UrlToArrayBuffer(options.challenge),
|
|
1075
1128
|
rp: options.rp,
|
|
@@ -1079,25 +1132,30 @@ function prepareRegistrationOptions(options) {
|
|
|
1079
1132
|
displayName: options.user.displayName
|
|
1080
1133
|
},
|
|
1081
1134
|
pubKeyCredParams: options.pubKeyCredParams,
|
|
1082
|
-
excludeCredentials:
|
|
1083
|
-
id: base64UrlToArrayBuffer(cred.id),
|
|
1084
|
-
type: cred.type,
|
|
1085
|
-
transports: cred.transports
|
|
1086
|
-
})),
|
|
1135
|
+
excludeCredentials: decodedExcludeCredentials,
|
|
1087
1136
|
authenticatorSelection: options.authenticatorSelection,
|
|
1088
1137
|
timeout: options.timeout,
|
|
1089
1138
|
attestation: options.attestation
|
|
1090
1139
|
};
|
|
1091
1140
|
}
|
|
1092
1141
|
function prepareAuthenticationOptions(options) {
|
|
1142
|
+
const allowCredentials = Array.isArray(options.allowCredentials) ? options.allowCredentials : [];
|
|
1143
|
+
const decodedAllowCredentials = [];
|
|
1144
|
+
for (const cred of allowCredentials) {
|
|
1145
|
+
try {
|
|
1146
|
+
decodedAllowCredentials.push({
|
|
1147
|
+
id: base64UrlToArrayBuffer(cred.id),
|
|
1148
|
+
type: cred.type,
|
|
1149
|
+
transports: cred.transports
|
|
1150
|
+
});
|
|
1151
|
+
} catch {
|
|
1152
|
+
console.warn("[ovixa/auth-client] Skipping malformed credential ID:", cred.id);
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1093
1155
|
return {
|
|
1094
1156
|
challenge: base64UrlToArrayBuffer(options.challenge),
|
|
1095
1157
|
rpId: options.rpId,
|
|
1096
|
-
allowCredentials:
|
|
1097
|
-
id: base64UrlToArrayBuffer(cred.id),
|
|
1098
|
-
type: cred.type,
|
|
1099
|
-
transports: cred.transports
|
|
1100
|
-
})),
|
|
1158
|
+
allowCredentials: decodedAllowCredentials,
|
|
1101
1159
|
userVerification: options.userVerification,
|
|
1102
1160
|
timeout: options.timeout
|
|
1103
1161
|
};
|
|
@@ -1142,4 +1200,4 @@ export {
|
|
|
1142
1200
|
formatRegistrationResponse,
|
|
1143
1201
|
formatAuthenticationResponse
|
|
1144
1202
|
};
|
|
1145
|
-
//# sourceMappingURL=chunk-
|
|
1203
|
+
//# sourceMappingURL=chunk-ZUTZYJDF.js.map
|