@revealui/auth 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.
Files changed (87) hide show
  1. package/README.md +58 -34
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/react/index.d.ts +4 -0
  4. package/dist/react/index.d.ts.map +1 -1
  5. package/dist/react/index.js +2 -0
  6. package/dist/react/useMFA.d.ts +83 -0
  7. package/dist/react/useMFA.d.ts.map +1 -0
  8. package/dist/react/useMFA.js +182 -0
  9. package/dist/react/usePasskey.d.ts +88 -0
  10. package/dist/react/usePasskey.d.ts.map +1 -0
  11. package/dist/react/usePasskey.js +203 -0
  12. package/dist/react/useSession.d.ts.map +1 -1
  13. package/dist/react/useSession.js +16 -5
  14. package/dist/react/useSignIn.d.ts +9 -3
  15. package/dist/react/useSignIn.d.ts.map +1 -1
  16. package/dist/react/useSignIn.js +32 -10
  17. package/dist/react/useSignOut.d.ts.map +1 -1
  18. package/dist/react/useSignUp.d.ts +1 -0
  19. package/dist/react/useSignUp.d.ts.map +1 -1
  20. package/dist/react/useSignUp.js +25 -9
  21. package/dist/server/auth.d.ts +2 -0
  22. package/dist/server/auth.d.ts.map +1 -1
  23. package/dist/server/auth.js +93 -5
  24. package/dist/server/brute-force.d.ts +10 -1
  25. package/dist/server/brute-force.d.ts.map +1 -1
  26. package/dist/server/brute-force.js +46 -23
  27. package/dist/server/errors.d.ts +4 -0
  28. package/dist/server/errors.d.ts.map +1 -1
  29. package/dist/server/errors.js +8 -0
  30. package/dist/server/index.d.ts +17 -6
  31. package/dist/server/index.d.ts.map +1 -1
  32. package/dist/server/index.js +12 -5
  33. package/dist/server/magic-link.d.ts +52 -0
  34. package/dist/server/magic-link.d.ts.map +1 -0
  35. package/dist/server/magic-link.js +111 -0
  36. package/dist/server/mfa.d.ts +87 -0
  37. package/dist/server/mfa.d.ts.map +1 -0
  38. package/dist/server/mfa.js +263 -0
  39. package/dist/server/oauth.d.ts +86 -0
  40. package/dist/server/oauth.d.ts.map +1 -0
  41. package/dist/server/oauth.js +355 -0
  42. package/dist/server/passkey.d.ts +132 -0
  43. package/dist/server/passkey.d.ts.map +1 -0
  44. package/dist/server/passkey.js +257 -0
  45. package/dist/server/password-reset.d.ts +32 -6
  46. package/dist/server/password-reset.d.ts.map +1 -1
  47. package/dist/server/password-reset.js +116 -47
  48. package/dist/server/password-validation.d.ts.map +1 -1
  49. package/dist/server/providers/github.d.ts +14 -0
  50. package/dist/server/providers/github.d.ts.map +1 -0
  51. package/dist/server/providers/github.js +89 -0
  52. package/dist/server/providers/google.d.ts +11 -0
  53. package/dist/server/providers/google.d.ts.map +1 -0
  54. package/dist/server/providers/google.js +69 -0
  55. package/dist/server/providers/vercel.d.ts +11 -0
  56. package/dist/server/providers/vercel.d.ts.map +1 -0
  57. package/dist/server/providers/vercel.js +63 -0
  58. package/dist/server/rate-limit.d.ts +10 -1
  59. package/dist/server/rate-limit.d.ts.map +1 -1
  60. package/dist/server/rate-limit.js +61 -43
  61. package/dist/server/session.d.ts +48 -1
  62. package/dist/server/session.d.ts.map +1 -1
  63. package/dist/server/session.js +126 -7
  64. package/dist/server/signed-cookie.d.ts +32 -0
  65. package/dist/server/signed-cookie.d.ts.map +1 -0
  66. package/dist/server/signed-cookie.js +67 -0
  67. package/dist/server/storage/database.d.ts +10 -1
  68. package/dist/server/storage/database.d.ts.map +1 -1
  69. package/dist/server/storage/database.js +43 -5
  70. package/dist/server/storage/in-memory.d.ts +4 -0
  71. package/dist/server/storage/in-memory.d.ts.map +1 -1
  72. package/dist/server/storage/in-memory.js +16 -6
  73. package/dist/server/storage/index.d.ts +11 -3
  74. package/dist/server/storage/index.d.ts.map +1 -1
  75. package/dist/server/storage/index.js +18 -4
  76. package/dist/server/storage/interface.d.ts +11 -1
  77. package/dist/server/storage/interface.d.ts.map +1 -1
  78. package/dist/server/storage/interface.js +1 -1
  79. package/dist/types.d.ts +23 -8
  80. package/dist/types.d.ts.map +1 -1
  81. package/dist/types.js +2 -2
  82. package/dist/utils/database.d.ts.map +1 -1
  83. package/dist/utils/database.js +12 -2
  84. package/dist/utils/token.d.ts +9 -1
  85. package/dist/utils/token.d.ts.map +1 -1
  86. package/dist/utils/token.js +9 -1
  87. package/package.json +26 -8
@@ -0,0 +1,355 @@
1
+ /**
2
+ * OAuth Core — State Management + User Upsert
3
+ *
4
+ * CSRF state: signed cookie using HMAC-SHA256 over a base64url payload.
5
+ * Provider dispatch: routes to Google / GitHub / Vercel provider modules.
6
+ * User upsert: links OAuth identities to local users via oauth_accounts table.
7
+ */
8
+ import crypto from 'node:crypto';
9
+ import { logger } from '@revealui/core/observability/logger';
10
+ import { getClient } from '@revealui/db/client';
11
+ import { oauthAccounts, users } from '@revealui/db/schema';
12
+ import { and, eq } from 'drizzle-orm';
13
+ import { OAuthAccountConflictError } from './errors.js';
14
+ import * as github from './providers/github.js';
15
+ import * as google from './providers/google.js';
16
+ import * as vercel from './providers/vercel.js';
17
+ // =============================================================================
18
+ // CSRF State
19
+ // =============================================================================
20
+ /**
21
+ * Generate a signed OAuth state token.
22
+ *
23
+ * State encodes provider + redirectTo + nonce as base64url JSON.
24
+ * Cookie value is `<state>.<hmac>` — the HMAC is over the state string
25
+ * using REVEALUI_SECRET, providing CSRF protection without a DB table.
26
+ */
27
+ export function generateOAuthState(provider, redirectTo) {
28
+ const nonce = crypto.randomBytes(16).toString('hex');
29
+ const payload = JSON.stringify({ provider, redirectTo, nonce });
30
+ const state = Buffer.from(payload).toString('base64url');
31
+ const secret = process.env.REVEALUI_SECRET;
32
+ if (!secret) {
33
+ throw new Error('REVEALUI_SECRET is required for OAuth state signing. ' +
34
+ 'Set it in your environment variables.');
35
+ }
36
+ const hmac = crypto.createHmac('sha256', secret).update(state).digest('hex');
37
+ return { state, cookieValue: `${state}.${hmac}` };
38
+ }
39
+ /**
40
+ * Verify a signed OAuth state token from the callback.
41
+ *
42
+ * Returns the decoded provider + redirectTo if valid, null otherwise.
43
+ */
44
+ export function verifyOAuthState(state, cookieValue) {
45
+ if (!(state && cookieValue))
46
+ return null;
47
+ const dotIdx = cookieValue.lastIndexOf('.');
48
+ if (dotIdx === -1)
49
+ return null;
50
+ const storedState = cookieValue.substring(0, dotIdx);
51
+ const storedHmac = cookieValue.substring(dotIdx + 1);
52
+ // State from query param must match what's in the cookie (timing-safe)
53
+ if (storedState.length !== state.length ||
54
+ !crypto.timingSafeEqual(Buffer.from(storedState), Buffer.from(state))) {
55
+ return null;
56
+ }
57
+ const secret = process.env.REVEALUI_SECRET;
58
+ if (!secret) {
59
+ throw new Error('REVEALUI_SECRET is required for OAuth state verification. ' +
60
+ 'Set it in your environment variables.');
61
+ }
62
+ const expectedHmac = crypto.createHmac('sha256', secret).update(state).digest('hex');
63
+ // Both are hex-encoded SHA-256 HMACs — must be exactly 64 hex characters.
64
+ // Reject wrong-length inputs immediately; do NOT pad (padding enables forged matches
65
+ // where a short storedHmac is zero-padded to collide with the expected hash).
66
+ if (storedHmac.length !== 64 || expectedHmac.length !== 64)
67
+ return null;
68
+ try {
69
+ if (!crypto.timingSafeEqual(Buffer.from(storedHmac, 'hex'), Buffer.from(expectedHmac, 'hex'))) {
70
+ return null;
71
+ }
72
+ }
73
+ catch {
74
+ return null;
75
+ }
76
+ try {
77
+ const parsed = JSON.parse(Buffer.from(state, 'base64url').toString());
78
+ return { provider: parsed.provider, redirectTo: parsed.redirectTo };
79
+ }
80
+ catch {
81
+ return null;
82
+ }
83
+ }
84
+ // =============================================================================
85
+ // Provider Dispatch
86
+ // =============================================================================
87
+ const PROVIDERS = ['google', 'github', 'vercel'];
88
+ function isProvider(p) {
89
+ return PROVIDERS.includes(p);
90
+ }
91
+ function getClientId(provider) {
92
+ const map = {
93
+ google: process.env.GOOGLE_CLIENT_ID,
94
+ github: process.env.GITHUB_CLIENT_ID,
95
+ vercel: process.env.VERCEL_CLIENT_ID,
96
+ };
97
+ const id = map[provider];
98
+ if (!id)
99
+ throw new Error(`Missing client ID for provider: ${provider}`);
100
+ return id;
101
+ }
102
+ export function buildAuthUrl(provider, redirectUri, state) {
103
+ if (!isProvider(provider))
104
+ throw new Error(`Unknown provider: ${provider}`);
105
+ const clientId = getClientId(provider);
106
+ const builders = {
107
+ google: google.buildAuthUrl,
108
+ github: github.buildAuthUrl,
109
+ vercel: vercel.buildAuthUrl,
110
+ };
111
+ return builders[provider](clientId, redirectUri, state);
112
+ }
113
+ export async function exchangeCode(provider, code, redirectUri) {
114
+ if (!isProvider(provider))
115
+ throw new Error(`Unknown provider: ${provider}`);
116
+ const exchangers = {
117
+ google: google.exchangeCode,
118
+ github: github.exchangeCode,
119
+ vercel: vercel.exchangeCode,
120
+ };
121
+ return exchangers[provider](code, redirectUri);
122
+ }
123
+ export async function fetchProviderUser(provider, accessToken) {
124
+ if (!isProvider(provider))
125
+ throw new Error(`Unknown provider: ${provider}`);
126
+ const fetchers = {
127
+ google: google.fetchUser,
128
+ github: github.fetchUser,
129
+ vercel: vercel.fetchUser,
130
+ };
131
+ return fetchers[provider](accessToken);
132
+ }
133
+ // =============================================================================
134
+ // User Upsert
135
+ // =============================================================================
136
+ /**
137
+ * Find or create a local user for the given OAuth identity.
138
+ *
139
+ * Flow:
140
+ * 1. Look up oauth_accounts by (provider, providerUserId) → get userId
141
+ * 2. If found: refresh metadata + return user
142
+ * 3. If not found: check users by email → link if match
143
+ * 4. If no match: create new user (role: 'admin', no password)
144
+ * 5. Insert oauth_accounts row
145
+ */
146
+ export async function upsertOAuthUser(provider, providerUser) {
147
+ const db = getClient();
148
+ // 1. Check for existing linked account
149
+ const [existingAccount] = await db
150
+ .select()
151
+ .from(oauthAccounts)
152
+ .where(and(eq(oauthAccounts.provider, provider), eq(oauthAccounts.providerUserId, providerUser.id)))
153
+ .limit(1);
154
+ if (existingAccount) {
155
+ // Refresh provider metadata (name/email/avatar may have changed)
156
+ await db
157
+ .update(oauthAccounts)
158
+ .set({
159
+ providerEmail: providerUser.email,
160
+ providerName: providerUser.name,
161
+ providerAvatarUrl: providerUser.avatarUrl,
162
+ updatedAt: new Date(),
163
+ })
164
+ .where(eq(oauthAccounts.id, existingAccount.id));
165
+ const [user] = await db
166
+ .select()
167
+ .from(users)
168
+ .where(eq(users.id, existingAccount.userId))
169
+ .limit(1);
170
+ if (!user) {
171
+ logger.error(`oauth_accounts row ${existingAccount.id} references missing user ${existingAccount.userId}`);
172
+ throw new Error('OAuth account references a deleted user');
173
+ }
174
+ return user;
175
+ }
176
+ // 2. Check for existing user by email — BLOCK auto-linking
177
+ // If an account with this email already exists but was not linked via OAuth,
178
+ // reject the login. Auto-linking is an account takeover vector: an attacker
179
+ // who controls a provider email instantly owns the existing account.
180
+ // Users must link providers explicitly from an authenticated session
181
+ // via linkOAuthAccount().
182
+ let userId;
183
+ let isNewUser = false;
184
+ if (providerUser.email) {
185
+ const [existingUser] = await db
186
+ .select()
187
+ .from(users)
188
+ .where(eq(users.email, providerUser.email))
189
+ .limit(1);
190
+ if (existingUser) {
191
+ throw new OAuthAccountConflictError(providerUser.email);
192
+ }
193
+ isNewUser = true;
194
+ userId = crypto.randomUUID();
195
+ }
196
+ else {
197
+ isNewUser = true;
198
+ userId = crypto.randomUUID();
199
+ }
200
+ // 3. Create user if none found
201
+ if (isNewUser) {
202
+ await db.insert(users).values({
203
+ id: userId,
204
+ name: providerUser.name,
205
+ email: providerUser.email,
206
+ avatarUrl: providerUser.avatarUrl,
207
+ password: null,
208
+ role: 'user',
209
+ status: 'active',
210
+ });
211
+ }
212
+ // 4. Insert oauth_accounts link
213
+ await db.insert(oauthAccounts).values({
214
+ id: crypto.randomUUID(),
215
+ userId,
216
+ provider,
217
+ providerUserId: providerUser.id,
218
+ providerEmail: providerUser.email,
219
+ providerName: providerUser.name,
220
+ providerAvatarUrl: providerUser.avatarUrl,
221
+ });
222
+ const [user] = await db.select().from(users).where(eq(users.id, userId)).limit(1);
223
+ if (!user)
224
+ throw new Error('Failed to fetch upserted OAuth user');
225
+ return user;
226
+ }
227
+ // =============================================================================
228
+ // Explicit Account Linking
229
+ // =============================================================================
230
+ /**
231
+ * Link an OAuth provider to an existing authenticated user.
232
+ *
233
+ * Unlike upsertOAuthUser(), this function requires the caller to be
234
+ * authenticated and explicitly requests the link. This is safe because
235
+ * the user has already proven ownership of the local account via their
236
+ * session.
237
+ *
238
+ * @param userId - The authenticated user's ID (from session)
239
+ * @param provider - OAuth provider name
240
+ * @param providerUser - Profile returned by the OAuth provider
241
+ * @returns The linked user
242
+ * @throws Error if the provider account is already linked to a different user
243
+ */
244
+ export async function linkOAuthAccount(userId, provider, providerUser) {
245
+ const db = getClient();
246
+ // 1. Check if this provider identity is already linked to ANY user
247
+ const [existingLink] = await db
248
+ .select()
249
+ .from(oauthAccounts)
250
+ .where(and(eq(oauthAccounts.provider, provider), eq(oauthAccounts.providerUserId, providerUser.id)))
251
+ .limit(1);
252
+ if (existingLink) {
253
+ if (existingLink.userId === userId) {
254
+ // Already linked to this user — refresh metadata and return
255
+ await db
256
+ .update(oauthAccounts)
257
+ .set({
258
+ providerEmail: providerUser.email,
259
+ providerName: providerUser.name,
260
+ providerAvatarUrl: providerUser.avatarUrl,
261
+ updatedAt: new Date(),
262
+ })
263
+ .where(eq(oauthAccounts.id, existingLink.id));
264
+ const [user] = await db.select().from(users).where(eq(users.id, userId)).limit(1);
265
+ if (!user)
266
+ throw new Error('Authenticated user not found in database');
267
+ return user;
268
+ }
269
+ // Linked to a different user — cannot steal the identity
270
+ throw new Error('This provider account is already linked to another user. Unlink it from the other account first.');
271
+ }
272
+ // 2. Check the authenticated user exists
273
+ const [user] = await db.select().from(users).where(eq(users.id, userId)).limit(1);
274
+ if (!user)
275
+ throw new Error('Authenticated user not found in database');
276
+ // 3. Check if this user already has a link for this provider (different provider account)
277
+ const [existingProviderLink] = await db
278
+ .select()
279
+ .from(oauthAccounts)
280
+ .where(and(eq(oauthAccounts.userId, userId), eq(oauthAccounts.provider, provider)))
281
+ .limit(1);
282
+ if (existingProviderLink) {
283
+ throw new Error(`You already have a ${provider} account linked. Unlink it first to connect a different one.`);
284
+ }
285
+ // 4. Create the link
286
+ await db.insert(oauthAccounts).values({
287
+ id: crypto.randomUUID(),
288
+ userId,
289
+ provider,
290
+ providerUserId: providerUser.id,
291
+ providerEmail: providerUser.email,
292
+ providerName: providerUser.name,
293
+ providerAvatarUrl: providerUser.avatarUrl,
294
+ });
295
+ logger.info(`Linked ${provider} account to user ${userId}`);
296
+ return user;
297
+ }
298
+ /**
299
+ * Unlink an OAuth provider from a user's account.
300
+ *
301
+ * Safety: refuses to unlink the last auth method (if user has no password
302
+ * and this is their only OAuth link, unlinking would lock them out).
303
+ *
304
+ * @param userId - The authenticated user's ID
305
+ * @param provider - The provider to unlink
306
+ * @throws Error if unlinking would leave the user with no authentication method
307
+ */
308
+ export async function unlinkOAuthAccount(userId, provider) {
309
+ const db = getClient();
310
+ // 1. Find the link to remove
311
+ const [link] = await db
312
+ .select()
313
+ .from(oauthAccounts)
314
+ .where(and(eq(oauthAccounts.userId, userId), eq(oauthAccounts.provider, provider)))
315
+ .limit(1);
316
+ if (!link) {
317
+ throw new Error(`No ${provider} account is linked to your account`);
318
+ }
319
+ // 2. Safety check: ensure user won't be locked out
320
+ const [user] = await db.select().from(users).where(eq(users.id, userId)).limit(1);
321
+ if (!user)
322
+ throw new Error('User not found');
323
+ const allLinks = await db
324
+ .select({ id: oauthAccounts.id })
325
+ .from(oauthAccounts)
326
+ .where(eq(oauthAccounts.userId, userId));
327
+ const hasPassword = !!user.password;
328
+ const otherLinksCount = allLinks.length - 1;
329
+ if (!hasPassword && otherLinksCount === 0) {
330
+ throw new Error('Cannot unlink your only sign-in method. Set a password first, or link another provider.');
331
+ }
332
+ // 3. Delete the link
333
+ await db
334
+ .delete(oauthAccounts)
335
+ .where(and(eq(oauthAccounts.userId, userId), eq(oauthAccounts.provider, provider)));
336
+ logger.info(`Unlinked ${provider} account from user ${userId}`);
337
+ }
338
+ /**
339
+ * Get all linked OAuth providers for a user.
340
+ *
341
+ * @param userId - The user's ID
342
+ * @returns Array of linked provider info (provider name, email, avatar)
343
+ */
344
+ export async function getLinkedProviders(userId) {
345
+ const db = getClient();
346
+ const links = await db
347
+ .select({
348
+ provider: oauthAccounts.provider,
349
+ providerEmail: oauthAccounts.providerEmail,
350
+ providerName: oauthAccounts.providerName,
351
+ })
352
+ .from(oauthAccounts)
353
+ .where(eq(oauthAccounts.userId, userId));
354
+ return links;
355
+ }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * WebAuthn Passkey Module
3
+ *
4
+ * Implements passkey registration, authentication, and management using
5
+ * @simplewebauthn/server v13. Passkeys enable passwordless authentication
6
+ * via biometrics, security keys, or platform authenticators.
7
+ *
8
+ * @see https://simplewebauthn.dev/
9
+ */
10
+ import type { AuthenticationResponseJSON, PublicKeyCredentialCreationOptionsJSON, PublicKeyCredentialRequestOptionsJSON, RegistrationResponseJSON, VerifiedRegistrationResponse, WebAuthnCredential } from '@simplewebauthn/server';
11
+ export interface PasskeyConfig {
12
+ /** Maximum passkeys per user (default: 10) */
13
+ maxPasskeysPerUser: number;
14
+ /** Challenge TTL in ms (default: 5 minutes) */
15
+ challengeTtlMs: number;
16
+ /** Relying Party ID — domain name (default: 'localhost') */
17
+ rpId: string;
18
+ /** Relying Party name — user-visible (default: 'RevealUI') */
19
+ rpName: string;
20
+ /** Expected origin(s) for verification (default: 'http://localhost:4000') */
21
+ origin: string | string[];
22
+ }
23
+ export declare function configurePasskey(overrides: Partial<PasskeyConfig>): void;
24
+ export declare function resetPasskeyConfig(): void;
25
+ /**
26
+ * Generate WebAuthn registration options for a user.
27
+ *
28
+ * The returned object should be passed to the browser's navigator.credentials.create().
29
+ * The challenge is embedded in the options and should be stored server-side
30
+ * for verification.
31
+ *
32
+ * @param userId - User's database ID
33
+ * @param userEmail - User's email (used as userName in WebAuthn)
34
+ * @param existingCredentialIds - Credential IDs to exclude (prevent re-registration)
35
+ */
36
+ export declare function generateRegistrationChallenge(userId: string, userEmail: string, existingCredentialIds?: string[]): Promise<PublicKeyCredentialCreationOptionsJSON>;
37
+ /**
38
+ * Verify a WebAuthn registration response from the browser.
39
+ *
40
+ * @param response - The RegistrationResponseJSON from the browser
41
+ * @param expectedChallenge - The challenge from generateRegistrationChallenge
42
+ * @param expectedOrigin - Override origin (defaults to config.origin)
43
+ * @returns Verified registration data including credential info
44
+ * @throws If verification fails
45
+ */
46
+ export declare function verifyRegistration(response: RegistrationResponseJSON, expectedChallenge: string, expectedOrigin?: string | string[]): Promise<VerifiedRegistrationResponse>;
47
+ /**
48
+ * Store a verified passkey credential in the database.
49
+ *
50
+ * Enforces the per-user passkey limit before insertion.
51
+ *
52
+ * @param userId - User's database ID
53
+ * @param credential - Verified credential from verifyRegistration
54
+ * @param deviceName - Optional user-friendly name (e.g., "MacBook Pro Touch ID")
55
+ * @returns The stored passkey record
56
+ */
57
+ export declare function storePasskey(userId: string, credential: {
58
+ id: string;
59
+ publicKey: Uint8Array;
60
+ counter: number;
61
+ transports?: string[];
62
+ aaguid?: string;
63
+ backedUp?: boolean;
64
+ }, deviceName?: string): Promise<{
65
+ id: string;
66
+ userId: string;
67
+ credentialId: string;
68
+ deviceName: string | null;
69
+ createdAt: Date;
70
+ }>;
71
+ /**
72
+ * Generate WebAuthn authentication options.
73
+ *
74
+ * The returned object should be passed to the browser's navigator.credentials.get().
75
+ *
76
+ * @param allowCredentials - Optional list of credential IDs to allow
77
+ */
78
+ export declare function generateAuthenticationChallenge(allowCredentials?: {
79
+ id: string;
80
+ transports?: string[];
81
+ }[]): Promise<PublicKeyCredentialRequestOptionsJSON>;
82
+ /**
83
+ * Verify a WebAuthn authentication response and update the credential counter.
84
+ *
85
+ * @param response - The AuthenticationResponseJSON from the browser
86
+ * @param credential - The stored credential (id, publicKey, counter)
87
+ * @param expectedChallenge - The challenge from generateAuthenticationChallenge
88
+ * @param expectedOrigin - Override origin (defaults to config.origin)
89
+ * @returns Verification result with new counter value
90
+ */
91
+ export declare function verifyAuthentication(response: AuthenticationResponseJSON, credential: WebAuthnCredential, expectedChallenge: string, expectedOrigin?: string | string[]): Promise<{
92
+ verified: boolean;
93
+ newCounter: number;
94
+ }>;
95
+ /**
96
+ * List all passkeys for a user (safe for client — excludes publicKey and counter).
97
+ */
98
+ export declare function listPasskeys(userId: string): Promise<{
99
+ id: string;
100
+ credentialId: string;
101
+ deviceName: string | null;
102
+ backedUp: boolean;
103
+ createdAt: Date;
104
+ lastUsedAt: Date | null;
105
+ }[]>;
106
+ /**
107
+ * Delete a passkey. Blocks deletion if it's the user's last sign-in method.
108
+ *
109
+ * @param userId - User's database ID
110
+ * @param passkeyId - The passkey record ID to delete
111
+ * @throws If this is the user's only sign-in method
112
+ */
113
+ export declare function deletePasskey(userId: string, passkeyId: string): Promise<void>;
114
+ /**
115
+ * Rename a passkey's device name.
116
+ *
117
+ * @param userId - User's database ID
118
+ * @param passkeyId - The passkey record ID to rename
119
+ * @param name - New friendly name
120
+ */
121
+ export declare function renamePasskey(userId: string, passkeyId: string, name: string): Promise<void>;
122
+ /**
123
+ * Count a user's sign-in credentials (passkeys + password).
124
+ *
125
+ * @param userId - User's database ID
126
+ * @returns Passkey count and whether user has a password set
127
+ */
128
+ export declare function countUserCredentials(userId: string): Promise<{
129
+ passkeyCount: number;
130
+ hasPassword: boolean;
131
+ }>;
132
+ //# sourceMappingURL=passkey.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"passkey.d.ts","sourceRoot":"","sources":["../../src/server/passkey.ts"],"names":[],"mappings":"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"}