@emdash-cms/auth 0.0.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.
Files changed (48) hide show
  1. package/dist/adapters/kysely.d.mts +62 -0
  2. package/dist/adapters/kysely.d.mts.map +1 -0
  3. package/dist/adapters/kysely.mjs +379 -0
  4. package/dist/adapters/kysely.mjs.map +1 -0
  5. package/dist/authenticate-D5UgaoTH.d.mts +124 -0
  6. package/dist/authenticate-D5UgaoTH.d.mts.map +1 -0
  7. package/dist/authenticate-j5GayLXB.mjs +373 -0
  8. package/dist/authenticate-j5GayLXB.mjs.map +1 -0
  9. package/dist/index.d.mts +444 -0
  10. package/dist/index.d.mts.map +1 -0
  11. package/dist/index.mjs +728 -0
  12. package/dist/index.mjs.map +1 -0
  13. package/dist/oauth/providers/github.d.mts +12 -0
  14. package/dist/oauth/providers/github.d.mts.map +1 -0
  15. package/dist/oauth/providers/github.mjs +55 -0
  16. package/dist/oauth/providers/github.mjs.map +1 -0
  17. package/dist/oauth/providers/google.d.mts +7 -0
  18. package/dist/oauth/providers/google.d.mts.map +1 -0
  19. package/dist/oauth/providers/google.mjs +38 -0
  20. package/dist/oauth/providers/google.mjs.map +1 -0
  21. package/dist/passkey/index.d.mts +2 -0
  22. package/dist/passkey/index.mjs +3 -0
  23. package/dist/types-Bu4irX9A.d.mts +35 -0
  24. package/dist/types-Bu4irX9A.d.mts.map +1 -0
  25. package/dist/types-CiSNpRI9.mjs +60 -0
  26. package/dist/types-CiSNpRI9.mjs.map +1 -0
  27. package/dist/types-HtRc90Wi.d.mts +208 -0
  28. package/dist/types-HtRc90Wi.d.mts.map +1 -0
  29. package/package.json +72 -0
  30. package/src/adapters/kysely.ts +715 -0
  31. package/src/config.ts +214 -0
  32. package/src/index.ts +135 -0
  33. package/src/invite.ts +205 -0
  34. package/src/magic-link/index.ts +150 -0
  35. package/src/oauth/consumer.ts +324 -0
  36. package/src/oauth/providers/github.ts +68 -0
  37. package/src/oauth/providers/google.ts +34 -0
  38. package/src/oauth/types.ts +36 -0
  39. package/src/passkey/authenticate.ts +183 -0
  40. package/src/passkey/index.ts +27 -0
  41. package/src/passkey/register.ts +232 -0
  42. package/src/passkey/types.ts +120 -0
  43. package/src/rbac.test.ts +141 -0
  44. package/src/rbac.ts +205 -0
  45. package/src/signup.ts +210 -0
  46. package/src/tokens.test.ts +141 -0
  47. package/src/tokens.ts +238 -0
  48. package/src/types.ts +352 -0
@@ -0,0 +1,324 @@
1
+ /**
2
+ * OAuth consumer - "Login with X" functionality
3
+ */
4
+
5
+ import { sha256 } from "@oslojs/crypto/sha2";
6
+ import { encodeBase64urlNoPadding } from "@oslojs/encoding";
7
+ import { z } from "zod";
8
+
9
+ import type { AuthAdapter, User, RoleLevel } from "../types.js";
10
+ import { github, fetchGitHubEmail } from "./providers/github.js";
11
+ import { google } from "./providers/google.js";
12
+ import type { OAuthProvider, OAuthConfig, OAuthProfile, OAuthState } from "./types.js";
13
+
14
+ export { github, google };
15
+
16
+ export interface OAuthConsumerConfig {
17
+ baseUrl: string;
18
+ providers: {
19
+ github?: OAuthConfig;
20
+ google?: OAuthConfig;
21
+ };
22
+ /**
23
+ * Check if self-signup is allowed for this email domain
24
+ */
25
+ canSelfSignup?: (email: string) => Promise<{ allowed: boolean; role: RoleLevel } | null>;
26
+ }
27
+
28
+ /**
29
+ * Generate an OAuth authorization URL
30
+ */
31
+ export async function createAuthorizationUrl(
32
+ config: OAuthConsumerConfig,
33
+ providerName: "github" | "google",
34
+ stateStore: StateStore,
35
+ ): Promise<{ url: string; state: string }> {
36
+ const providerConfig = config.providers[providerName];
37
+ if (!providerConfig) {
38
+ throw new Error(`OAuth provider ${providerName} not configured`);
39
+ }
40
+
41
+ const provider = getProvider(providerName);
42
+ const state = generateState();
43
+ const redirectUri = `${config.baseUrl}/api/auth/oauth/${providerName}/callback`;
44
+
45
+ // Generate PKCE code verifier for providers that support it
46
+ const codeVerifier = generateCodeVerifier();
47
+ const codeChallenge = await generateCodeChallenge(codeVerifier);
48
+
49
+ // Store state for verification
50
+ await stateStore.set(state, {
51
+ provider: providerName,
52
+ redirectUri,
53
+ codeVerifier,
54
+ });
55
+
56
+ // Build authorization URL
57
+ const url = new URL(provider.authorizeUrl);
58
+ url.searchParams.set("client_id", providerConfig.clientId);
59
+ url.searchParams.set("redirect_uri", redirectUri);
60
+ url.searchParams.set("response_type", "code");
61
+ url.searchParams.set("scope", provider.scopes.join(" "));
62
+ url.searchParams.set("state", state);
63
+
64
+ // PKCE for all providers (GitHub has supported S256 since 2021)
65
+ url.searchParams.set("code_challenge", codeChallenge);
66
+ url.searchParams.set("code_challenge_method", "S256");
67
+
68
+ return { url: url.toString(), state };
69
+ }
70
+
71
+ /**
72
+ * Handle OAuth callback
73
+ */
74
+ export async function handleOAuthCallback(
75
+ config: OAuthConsumerConfig,
76
+ adapter: AuthAdapter,
77
+ providerName: "github" | "google",
78
+ code: string,
79
+ state: string,
80
+ stateStore: StateStore,
81
+ ): Promise<User> {
82
+ const providerConfig = config.providers[providerName];
83
+ if (!providerConfig) {
84
+ throw new Error(`OAuth provider ${providerName} not configured`);
85
+ }
86
+
87
+ // Verify state
88
+ const storedState = await stateStore.get(state);
89
+ if (!storedState || storedState.provider !== providerName) {
90
+ throw new OAuthError("invalid_state", "Invalid OAuth state");
91
+ }
92
+
93
+ // Delete state (single-use)
94
+ await stateStore.delete(state);
95
+
96
+ const provider = getProvider(providerName);
97
+
98
+ // Exchange code for tokens
99
+ const tokens = await exchangeCode(
100
+ provider,
101
+ providerConfig,
102
+ code,
103
+ storedState.redirectUri,
104
+ storedState.codeVerifier,
105
+ );
106
+
107
+ // Fetch user profile
108
+ const profile = await fetchProfile(provider, tokens.accessToken, providerName);
109
+
110
+ // Find or create user
111
+ return findOrCreateUser(config, adapter, providerName, profile);
112
+ }
113
+
114
+ /**
115
+ * Exchange authorization code for tokens
116
+ */
117
+ async function exchangeCode(
118
+ provider: OAuthProvider,
119
+ config: OAuthConfig,
120
+ code: string,
121
+ redirectUri: string,
122
+ codeVerifier?: string,
123
+ ): Promise<{ accessToken: string; idToken?: string }> {
124
+ const body = new URLSearchParams({
125
+ grant_type: "authorization_code",
126
+ code,
127
+ redirect_uri: redirectUri,
128
+ client_id: config.clientId,
129
+ client_secret: config.clientSecret,
130
+ });
131
+
132
+ if (codeVerifier) {
133
+ body.set("code_verifier", codeVerifier);
134
+ }
135
+
136
+ const response = await fetch(provider.tokenUrl, {
137
+ method: "POST",
138
+ headers: {
139
+ "Content-Type": "application/x-www-form-urlencoded",
140
+ Accept: "application/json",
141
+ },
142
+ body,
143
+ });
144
+
145
+ if (!response.ok) {
146
+ const error = await response.text();
147
+ throw new OAuthError("token_exchange_failed", `Token exchange failed: ${error}`);
148
+ }
149
+
150
+ const json: unknown = await response.json();
151
+ const data = z
152
+ .object({
153
+ access_token: z.string(),
154
+ id_token: z.string().optional(),
155
+ })
156
+ .parse(json);
157
+
158
+ return {
159
+ accessToken: data.access_token,
160
+ idToken: data.id_token,
161
+ };
162
+ }
163
+
164
+ /**
165
+ * Fetch user profile from OAuth provider
166
+ */
167
+ async function fetchProfile(
168
+ provider: OAuthProvider,
169
+ accessToken: string,
170
+ providerName: string,
171
+ ): Promise<OAuthProfile> {
172
+ if (!provider.userInfoUrl) {
173
+ throw new Error("Provider does not have userinfo URL");
174
+ }
175
+
176
+ const response = await fetch(provider.userInfoUrl, {
177
+ headers: {
178
+ Authorization: `Bearer ${accessToken}`,
179
+ Accept: "application/json",
180
+ },
181
+ });
182
+
183
+ if (!response.ok) {
184
+ throw new OAuthError("profile_fetch_failed", `Failed to fetch profile: ${response.status}`);
185
+ }
186
+
187
+ const data = await response.json();
188
+ const profile = provider.parseProfile(data);
189
+
190
+ // GitHub may not return email in main profile
191
+ if (providerName === "github" && !profile.email) {
192
+ profile.email = await fetchGitHubEmail(accessToken);
193
+ }
194
+
195
+ return profile;
196
+ }
197
+
198
+ /**
199
+ * Find existing user or create new one (with auto-linking)
200
+ */
201
+ async function findOrCreateUser(
202
+ config: OAuthConsumerConfig,
203
+ adapter: AuthAdapter,
204
+ providerName: string,
205
+ profile: OAuthProfile,
206
+ ): Promise<User> {
207
+ // Check if OAuth account already linked
208
+ const existingAccount = await adapter.getOAuthAccount(providerName, profile.id);
209
+ if (existingAccount) {
210
+ const user = await adapter.getUserById(existingAccount.userId);
211
+ if (!user) {
212
+ throw new OAuthError("user_not_found", "Linked user not found");
213
+ }
214
+ return user;
215
+ }
216
+
217
+ // Check if user with this email exists (auto-link)
218
+ // Only auto-link when the provider has verified the email to prevent
219
+ // account takeover via unverified email on a third-party provider
220
+ const existingUser = await adapter.getUserByEmail(profile.email);
221
+ if (existingUser) {
222
+ if (!profile.emailVerified) {
223
+ throw new OAuthError(
224
+ "signup_not_allowed",
225
+ "Cannot link account: email not verified by provider",
226
+ );
227
+ }
228
+ await adapter.createOAuthAccount({
229
+ provider: providerName,
230
+ providerAccountId: profile.id,
231
+ userId: existingUser.id,
232
+ });
233
+ return existingUser;
234
+ }
235
+
236
+ // Check if self-signup is allowed
237
+ if (config.canSelfSignup) {
238
+ const signup = await config.canSelfSignup(profile.email);
239
+ if (signup?.allowed) {
240
+ // Create new user
241
+ const user = await adapter.createUser({
242
+ email: profile.email,
243
+ name: profile.name,
244
+ avatarUrl: profile.avatarUrl,
245
+ role: signup.role,
246
+ emailVerified: profile.emailVerified,
247
+ });
248
+
249
+ // Link OAuth account
250
+ await adapter.createOAuthAccount({
251
+ provider: providerName,
252
+ providerAccountId: profile.id,
253
+ userId: user.id,
254
+ });
255
+
256
+ return user;
257
+ }
258
+ }
259
+
260
+ throw new OAuthError("signup_not_allowed", "Self-signup not allowed for this email domain");
261
+ }
262
+
263
+ function getProvider(name: "github" | "google"): OAuthProvider {
264
+ switch (name) {
265
+ case "github":
266
+ return github;
267
+ case "google":
268
+ return google;
269
+ }
270
+ }
271
+
272
+ // ============================================================================
273
+ // Helpers
274
+ // ============================================================================
275
+
276
+ /**
277
+ * Generate a random state string for OAuth CSRF protection
278
+ */
279
+ function generateState(): string {
280
+ const bytes = new Uint8Array(32);
281
+ crypto.getRandomValues(bytes);
282
+ return encodeBase64urlNoPadding(bytes);
283
+ }
284
+
285
+ function generateCodeVerifier(): string {
286
+ const bytes = new Uint8Array(32);
287
+ crypto.getRandomValues(bytes);
288
+ return encodeBase64urlNoPadding(bytes);
289
+ }
290
+
291
+ async function generateCodeChallenge(verifier: string): Promise<string> {
292
+ const bytes = new TextEncoder().encode(verifier);
293
+ const hash = sha256(bytes);
294
+ return encodeBase64urlNoPadding(hash);
295
+ }
296
+
297
+ // ============================================================================
298
+ // State storage interface
299
+ // ============================================================================
300
+
301
+ export interface StateStore {
302
+ set(state: string, data: OAuthState): Promise<void>;
303
+ get(state: string): Promise<OAuthState | null>;
304
+ delete(state: string): Promise<void>;
305
+ }
306
+
307
+ // ============================================================================
308
+ // Errors
309
+ // ============================================================================
310
+
311
+ export class OAuthError extends Error {
312
+ constructor(
313
+ public code:
314
+ | "invalid_state"
315
+ | "token_exchange_failed"
316
+ | "profile_fetch_failed"
317
+ | "user_not_found"
318
+ | "signup_not_allowed",
319
+ message: string,
320
+ ) {
321
+ super(message);
322
+ this.name = "OAuthError";
323
+ }
324
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * GitHub OAuth provider
3
+ */
4
+
5
+ import { z } from "zod";
6
+
7
+ import type { OAuthProvider, OAuthProfile } from "../types.js";
8
+
9
+ const gitHubUserSchema = z.object({
10
+ id: z.number(),
11
+ login: z.string(),
12
+ name: z.string().nullable(),
13
+ email: z.string().nullable(),
14
+ avatar_url: z.string(),
15
+ });
16
+
17
+ const gitHubEmailSchema = z.object({
18
+ email: z.string(),
19
+ primary: z.boolean(),
20
+ verified: z.boolean(),
21
+ });
22
+
23
+ export const github: OAuthProvider = {
24
+ name: "github",
25
+ authorizeUrl: "https://github.com/login/oauth/authorize",
26
+ tokenUrl: "https://github.com/login/oauth/access_token",
27
+ userInfoUrl: "https://api.github.com/user",
28
+ scopes: ["read:user", "user:email"],
29
+
30
+ parseProfile(data: unknown): OAuthProfile {
31
+ const user = gitHubUserSchema.parse(data);
32
+ return {
33
+ id: String(user.id),
34
+ email: user.email || "", // Will be fetched separately if needed
35
+ name: user.name,
36
+ avatarUrl: user.avatar_url,
37
+ emailVerified: true, // GitHub verifies emails
38
+ };
39
+ },
40
+ };
41
+
42
+ /**
43
+ * Fetch the user's primary email from GitHub
44
+ * (needed because email may not be returned in the basic user endpoint)
45
+ */
46
+ export async function fetchGitHubEmail(accessToken: string): Promise<string> {
47
+ const response = await fetch("https://api.github.com/user/emails", {
48
+ headers: {
49
+ Authorization: `Bearer ${accessToken}`,
50
+ Accept: "application/vnd.github+json",
51
+ "X-GitHub-Api-Version": "2022-11-28",
52
+ },
53
+ });
54
+
55
+ if (!response.ok) {
56
+ throw new Error(`Failed to fetch GitHub emails: ${response.status}`);
57
+ }
58
+
59
+ const json: unknown = await response.json();
60
+ const emails = z.array(gitHubEmailSchema).parse(json);
61
+ const primary = emails.find((e) => e.primary && e.verified);
62
+
63
+ if (!primary) {
64
+ throw new Error("No verified primary email found on GitHub account");
65
+ }
66
+
67
+ return primary.email;
68
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Google OAuth provider (using OIDC)
3
+ */
4
+
5
+ import { z } from "zod";
6
+
7
+ import type { OAuthProvider, OAuthProfile } from "../types.js";
8
+
9
+ const googleUserSchema = z.object({
10
+ sub: z.string(),
11
+ email: z.string(),
12
+ email_verified: z.boolean(),
13
+ name: z.string(),
14
+ picture: z.string(),
15
+ });
16
+
17
+ export const google: OAuthProvider = {
18
+ name: "google",
19
+ authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
20
+ tokenUrl: "https://oauth2.googleapis.com/token",
21
+ userInfoUrl: "https://openidconnect.googleapis.com/v1/userinfo",
22
+ scopes: ["openid", "email", "profile"],
23
+
24
+ parseProfile(data: unknown): OAuthProfile {
25
+ const user = googleUserSchema.parse(data);
26
+ return {
27
+ id: user.sub,
28
+ email: user.email,
29
+ name: user.name,
30
+ avatarUrl: user.picture,
31
+ emailVerified: user.email_verified,
32
+ };
33
+ },
34
+ };
@@ -0,0 +1,36 @@
1
+ /**
2
+ * OAuth types
3
+ */
4
+
5
+ export interface OAuthProfile {
6
+ id: string;
7
+ email: string;
8
+ name: string | null;
9
+ avatarUrl: string | null;
10
+ emailVerified: boolean;
11
+ }
12
+
13
+ export interface OAuthProvider {
14
+ name: string;
15
+ authorizeUrl: string;
16
+ tokenUrl: string;
17
+ userInfoUrl?: string;
18
+ scopes: string[];
19
+
20
+ /**
21
+ * Parse the user profile from the provider's response
22
+ */
23
+ parseProfile(data: unknown): OAuthProfile;
24
+ }
25
+
26
+ export interface OAuthConfig {
27
+ clientId: string;
28
+ clientSecret: string;
29
+ }
30
+
31
+ export interface OAuthState {
32
+ provider: string;
33
+ redirectUri: string;
34
+ codeVerifier?: string; // For PKCE
35
+ nonce?: string;
36
+ }
@@ -0,0 +1,183 @@
1
+ /**
2
+ * Passkey authentication (credential assertion)
3
+ *
4
+ * Based on oslo webauthn documentation:
5
+ * https://webauthn.oslojs.dev/examples/authentication
6
+ */
7
+
8
+ import {
9
+ verifyECDSASignature,
10
+ p256,
11
+ decodeSEC1PublicKey,
12
+ decodePKIXECDSASignature,
13
+ } from "@oslojs/crypto/ecdsa";
14
+ import { sha256 } from "@oslojs/crypto/sha2";
15
+ import { encodeBase64urlNoPadding, decodeBase64urlIgnorePadding } from "@oslojs/encoding";
16
+ import {
17
+ parseAuthenticatorData,
18
+ parseClientDataJSON,
19
+ ClientDataType,
20
+ createAssertionSignatureMessage,
21
+ } from "@oslojs/webauthn";
22
+
23
+ import { generateToken } from "../tokens.js";
24
+ import type { Credential, AuthAdapter, User } from "../types.js";
25
+ import type {
26
+ AuthenticationOptions,
27
+ AuthenticationResponse,
28
+ VerifiedAuthentication,
29
+ ChallengeStore,
30
+ PasskeyConfig,
31
+ } from "./types.js";
32
+
33
+ const CHALLENGE_TTL = 5 * 60 * 1000; // 5 minutes
34
+
35
+ /**
36
+ * Generate authentication options for signing in with a passkey
37
+ */
38
+ export async function generateAuthenticationOptions(
39
+ config: PasskeyConfig,
40
+ credentials: Credential[],
41
+ challengeStore: ChallengeStore,
42
+ ): Promise<AuthenticationOptions> {
43
+ const challenge = generateToken();
44
+
45
+ // Store challenge for verification
46
+ await challengeStore.set(challenge, {
47
+ type: "authentication",
48
+ expiresAt: Date.now() + CHALLENGE_TTL,
49
+ });
50
+
51
+ return {
52
+ challenge,
53
+ rpId: config.rpId,
54
+ timeout: 60000,
55
+ userVerification: "preferred",
56
+ allowCredentials:
57
+ credentials.length > 0
58
+ ? credentials.map((cred) => ({
59
+ type: "public-key" as const,
60
+ id: cred.id,
61
+ transports: cred.transports,
62
+ }))
63
+ : undefined, // Empty = allow any discoverable credential
64
+ };
65
+ }
66
+
67
+ /**
68
+ * Verify an authentication response
69
+ */
70
+ export async function verifyAuthenticationResponse(
71
+ config: PasskeyConfig,
72
+ response: AuthenticationResponse,
73
+ credential: Credential,
74
+ challengeStore: ChallengeStore,
75
+ ): Promise<VerifiedAuthentication> {
76
+ // Decode the response
77
+ const clientDataJSON = decodeBase64urlIgnorePadding(response.response.clientDataJSON);
78
+ const authenticatorData = decodeBase64urlIgnorePadding(response.response.authenticatorData);
79
+ const signature = decodeBase64urlIgnorePadding(response.response.signature);
80
+
81
+ // Parse client data
82
+ const clientData = parseClientDataJSON(clientDataJSON);
83
+
84
+ // Verify client data type
85
+ if (clientData.type !== ClientDataType.Get) {
86
+ throw new Error("Invalid client data type");
87
+ }
88
+
89
+ // Verify challenge - convert Uint8Array back to base64url string (no padding, matching stored format)
90
+ const challengeString = encodeBase64urlNoPadding(clientData.challenge);
91
+ const challengeData = await challengeStore.get(challengeString);
92
+ if (!challengeData) {
93
+ throw new Error("Challenge not found or expired");
94
+ }
95
+ if (challengeData.type !== "authentication") {
96
+ throw new Error("Invalid challenge type");
97
+ }
98
+ if (challengeData.expiresAt < Date.now()) {
99
+ await challengeStore.delete(challengeString);
100
+ throw new Error("Challenge expired");
101
+ }
102
+
103
+ // Delete challenge (single-use)
104
+ await challengeStore.delete(challengeString);
105
+
106
+ // Verify origin
107
+ if (clientData.origin !== config.origin) {
108
+ throw new Error(`Invalid origin: expected ${config.origin}, got ${clientData.origin}`);
109
+ }
110
+
111
+ // Parse authenticator data
112
+ const authData = parseAuthenticatorData(authenticatorData);
113
+
114
+ // Verify RP ID hash
115
+ if (!authData.verifyRelyingPartyIdHash(config.rpId)) {
116
+ throw new Error("Invalid RP ID hash");
117
+ }
118
+
119
+ // Verify flags
120
+ if (!authData.userPresent) {
121
+ throw new Error("User presence not verified");
122
+ }
123
+
124
+ // Verify counter (prevent replay attacks)
125
+ if (authData.signatureCounter !== 0 && authData.signatureCounter <= credential.counter) {
126
+ throw new Error("Invalid signature counter - possible cloned authenticator");
127
+ }
128
+
129
+ // Create the message that was signed
130
+ const signatureMessage = createAssertionSignatureMessage(authenticatorData, clientDataJSON);
131
+
132
+ // Ensure public key is a Uint8Array (may come as Buffer from some DB drivers)
133
+ const publicKeyBytes =
134
+ credential.publicKey instanceof Uint8Array
135
+ ? credential.publicKey
136
+ : new Uint8Array(credential.publicKey);
137
+
138
+ // Decode the stored SEC1-encoded public key and verify signature
139
+ // The signature from WebAuthn is DER-encoded (PKIX format)
140
+ const ecdsaPublicKey = decodeSEC1PublicKey(p256, publicKeyBytes);
141
+ const ecdsaSignature = decodePKIXECDSASignature(signature);
142
+ const hash = sha256(signatureMessage);
143
+ const signatureValid = verifyECDSASignature(ecdsaPublicKey, hash, ecdsaSignature);
144
+
145
+ if (!signatureValid) {
146
+ throw new Error("Invalid signature");
147
+ }
148
+
149
+ return {
150
+ credentialId: response.id,
151
+ newCounter: authData.signatureCounter,
152
+ };
153
+ }
154
+
155
+ /**
156
+ * Authenticate a user with a passkey
157
+ */
158
+ export async function authenticateWithPasskey(
159
+ config: PasskeyConfig,
160
+ adapter: AuthAdapter,
161
+ response: AuthenticationResponse,
162
+ challengeStore: ChallengeStore,
163
+ ): Promise<User> {
164
+ // Find the credential
165
+ const credential = await adapter.getCredentialById(response.id);
166
+ if (!credential) {
167
+ throw new Error("Credential not found");
168
+ }
169
+
170
+ // Verify the response
171
+ const verified = await verifyAuthenticationResponse(config, response, credential, challengeStore);
172
+
173
+ // Update counter
174
+ await adapter.updateCredentialCounter(verified.credentialId, verified.newCounter);
175
+
176
+ // Get the user
177
+ const user = await adapter.getUserById(credential.userId);
178
+ if (!user) {
179
+ throw new Error("User not found");
180
+ }
181
+
182
+ return user;
183
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Passkey authentication module
3
+ */
4
+
5
+ export type {
6
+ RegistrationOptions,
7
+ RegistrationResponse,
8
+ VerifiedRegistration,
9
+ AuthenticationOptions,
10
+ AuthenticationResponse,
11
+ VerifiedAuthentication,
12
+ ChallengeStore,
13
+ ChallengeData,
14
+ PasskeyConfig,
15
+ } from "./types.js";
16
+
17
+ export {
18
+ generateRegistrationOptions,
19
+ verifyRegistrationResponse,
20
+ registerPasskey,
21
+ } from "./register.js";
22
+
23
+ export {
24
+ generateAuthenticationOptions,
25
+ verifyAuthenticationResponse,
26
+ authenticateWithPasskey,
27
+ } from "./authenticate.js";