@ovixa/auth-client 0.2.0 → 0.3.0
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 +19 -7
- package/dist/{chunk-5ZWKDQQM.js → chunk-5QEKSWV4.js} +319 -279
- package/dist/chunk-5QEKSWV4.js.map +1 -0
- package/dist/{chunk-WON3EB4B.js → chunk-IU57RXBC.js} +2 -2
- 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-IU57RXBC.js.map} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,18 +5,30 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
7
7
|
|
|
8
|
+
## [0.3.0] - 2026-01-30
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- `auth.webauthn.listPasskeys({ accessToken })` - List all passkeys for the authenticated user
|
|
13
|
+
- `auth.webauthn.deletePasskey({ accessToken, credentialId })` - Delete a specific passkey
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- **BREAKING**: Moved WebAuthn methods to `auth.webauthn` namespace
|
|
18
|
+
- `auth.getPasskeyRegistrationOptions()` → `auth.webauthn.getRegistrationOptions()`
|
|
19
|
+
- `auth.verifyPasskeyRegistration()` → `auth.webauthn.verifyRegistration()`
|
|
20
|
+
- `auth.getPasskeyAuthenticationOptions()` → `auth.webauthn.getAuthenticationOptions()`
|
|
21
|
+
- `auth.verifyPasskeyAuthentication()` → `auth.webauthn.authenticate()`
|
|
22
|
+
|
|
8
23
|
## [0.2.0] - 2026-01-29
|
|
9
24
|
|
|
10
25
|
### Added
|
|
11
26
|
|
|
12
27
|
- **WebAuthn/Passkey authentication support** - Passwordless authentication using FIDO2
|
|
13
|
-
- `auth.
|
|
14
|
-
- `auth.
|
|
15
|
-
- `auth.
|
|
16
|
-
- `auth.
|
|
17
|
-
- `auth.webauthn.getCredentials(realmId)` - List user's registered passkeys
|
|
18
|
-
- `auth.webauthn.deleteCredential(realmId, credentialId)` - Remove a passkey
|
|
19
|
-
- `auth.webauthn.updateCredentialName(realmId, credentialId, name)` - Rename a passkey
|
|
28
|
+
- `auth.getPasskeyRegistrationOptions({ accessToken })` - Get options to register a new passkey
|
|
29
|
+
- `auth.verifyPasskeyRegistration({ accessToken, registration, deviceName? })` - Complete passkey registration
|
|
30
|
+
- `auth.getPasskeyAuthenticationOptions({ email? })` - Get options to authenticate with passkey
|
|
31
|
+
- `auth.verifyPasskeyAuthentication({ authentication })` - Authenticate and receive tokens
|
|
20
32
|
|
|
21
33
|
## [0.1.3] - 2026-01-29
|
|
22
34
|
|
|
@@ -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 = "";
|
|
@@ -1142,4 +1182,4 @@ export {
|
|
|
1142
1182
|
formatRegistrationResponse,
|
|
1143
1183
|
formatAuthenticationResponse
|
|
1144
1184
|
};
|
|
1145
|
-
//# sourceMappingURL=chunk-
|
|
1185
|
+
//# sourceMappingURL=chunk-5QEKSWV4.js.map
|