@mrgnw/anahtar 0.0.6

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.
@@ -0,0 +1,187 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ export function sqliteAdapter(db, options = {}) {
3
+ const p = options.tablePrefix ?? 'auth_';
4
+ const t = {
5
+ users: `${p}users`,
6
+ sessions: `${p}sessions`,
7
+ otpCodes: `${p}otp_codes`,
8
+ passkeys: `${p}passkeys`,
9
+ challenges: `${p}challenges`
10
+ };
11
+ return {
12
+ init() {
13
+ db.pragma('journal_mode = WAL');
14
+ db.pragma('foreign_keys = ON');
15
+ db.exec(`
16
+ CREATE TABLE IF NOT EXISTS ${t.users} (
17
+ id TEXT PRIMARY KEY,
18
+ email TEXT UNIQUE NOT NULL,
19
+ skip_passkey_prompt INTEGER DEFAULT 0,
20
+ created_at INTEGER DEFAULT (unixepoch())
21
+ );
22
+ CREATE TABLE IF NOT EXISTS ${t.sessions} (
23
+ id TEXT PRIMARY KEY,
24
+ user_id TEXT NOT NULL REFERENCES ${t.users}(id),
25
+ expires_at INTEGER NOT NULL,
26
+ created_at INTEGER DEFAULT (unixepoch())
27
+ );
28
+ CREATE TABLE IF NOT EXISTS ${t.otpCodes} (
29
+ id TEXT PRIMARY KEY,
30
+ email TEXT NOT NULL,
31
+ code TEXT NOT NULL,
32
+ attempts INTEGER NOT NULL DEFAULT 0,
33
+ expires_at INTEGER NOT NULL,
34
+ created_at INTEGER DEFAULT (unixepoch())
35
+ );
36
+ CREATE TABLE IF NOT EXISTS ${t.passkeys} (
37
+ id TEXT PRIMARY KEY,
38
+ user_id TEXT NOT NULL REFERENCES ${t.users}(id),
39
+ credential_id TEXT UNIQUE NOT NULL,
40
+ public_key BLOB NOT NULL,
41
+ counter INTEGER NOT NULL DEFAULT 0,
42
+ transports TEXT,
43
+ name TEXT,
44
+ created_at INTEGER DEFAULT (unixepoch())
45
+ );
46
+ CREATE TABLE IF NOT EXISTS ${t.challenges} (
47
+ challenge TEXT PRIMARY KEY,
48
+ user_id TEXT NOT NULL,
49
+ expires_at INTEGER NOT NULL,
50
+ created_at INTEGER DEFAULT (unixepoch())
51
+ );
52
+ `);
53
+ },
54
+ getUserByEmail(email) {
55
+ const row = db
56
+ .prepare(`SELECT id, email, skip_passkey_prompt, created_at FROM ${t.users} WHERE email = ?`)
57
+ .get(email);
58
+ if (!row)
59
+ return null;
60
+ return {
61
+ id: row.id,
62
+ email: row.email,
63
+ skipPasskeyPrompt: row.skip_passkey_prompt === 1,
64
+ createdAt: row.created_at
65
+ };
66
+ },
67
+ createUser(email) {
68
+ const id = randomUUID();
69
+ db.prepare(`INSERT INTO ${t.users} (id, email) VALUES (?, ?)`).run(id, email);
70
+ return {
71
+ id,
72
+ email,
73
+ skipPasskeyPrompt: false,
74
+ createdAt: Math.floor(Date.now() / 1000)
75
+ };
76
+ },
77
+ setSkipPasskeyPrompt(userId, skip) {
78
+ db.prepare(`UPDATE ${t.users} SET skip_passkey_prompt = ? WHERE id = ?`).run(skip ? 1 : 0, userId);
79
+ },
80
+ createSession(tokenHash, userId, expiresAt) {
81
+ db.prepare(`INSERT INTO ${t.sessions} (id, user_id, expires_at) VALUES (?, ?, ?)`).run(tokenHash, userId, expiresAt);
82
+ },
83
+ getSession(tokenHash) {
84
+ const row = db
85
+ .prepare(`SELECT s.id, s.user_id, s.expires_at, u.email
86
+ FROM ${t.sessions} s
87
+ JOIN ${t.users} u ON u.id = s.user_id
88
+ WHERE s.id = ?`)
89
+ .get(tokenHash);
90
+ if (!row)
91
+ return null;
92
+ return {
93
+ id: row.id,
94
+ userId: row.user_id,
95
+ expiresAt: row.expires_at,
96
+ email: row.email
97
+ };
98
+ },
99
+ deleteSession(tokenHash) {
100
+ db.prepare(`DELETE FROM ${t.sessions} WHERE id = ?`).run(tokenHash);
101
+ },
102
+ storeOTP(email, id, code, expiresAt) {
103
+ db.prepare(`INSERT INTO ${t.otpCodes} (id, email, code, expires_at) VALUES (?, ?, ?, ?)`).run(id, email, code, expiresAt);
104
+ },
105
+ getLatestOTP(email) {
106
+ const row = db
107
+ .prepare(`SELECT id, email, code, attempts, expires_at FROM ${t.otpCodes} WHERE email = ? ORDER BY created_at DESC LIMIT 1`)
108
+ .get(email);
109
+ if (!row)
110
+ return null;
111
+ return {
112
+ id: row.id,
113
+ email: row.email,
114
+ code: row.code,
115
+ attempts: row.attempts,
116
+ expiresAt: row.expires_at
117
+ };
118
+ },
119
+ updateOTPAttempts(id, attempts) {
120
+ db.prepare(`UPDATE ${t.otpCodes} SET attempts = ? WHERE id = ?`).run(attempts, id);
121
+ },
122
+ deleteOTP(id) {
123
+ db.prepare(`DELETE FROM ${t.otpCodes} WHERE id = ?`).run(id);
124
+ },
125
+ deleteOTPsForEmail(email) {
126
+ db.prepare(`DELETE FROM ${t.otpCodes} WHERE email = ?`).run(email);
127
+ },
128
+ storeChallenge(challenge, userId, expiresAt) {
129
+ db.prepare(`DELETE FROM ${t.challenges} WHERE expires_at < ?`).run(Date.now());
130
+ db.prepare(`INSERT INTO ${t.challenges} (challenge, user_id, expires_at) VALUES (?, ?, ?)`).run(challenge, userId, expiresAt);
131
+ },
132
+ consumeChallenge(challenge) {
133
+ const row = db.prepare(`SELECT user_id, expires_at FROM ${t.challenges} WHERE challenge = ?`).get(challenge);
134
+ if (!row)
135
+ return null;
136
+ db.prepare(`DELETE FROM ${t.challenges} WHERE challenge = ?`).run(challenge);
137
+ if (row.expires_at < Date.now())
138
+ return null;
139
+ return { userId: row.user_id };
140
+ },
141
+ getPasskeyByCredentialId(credentialId) {
142
+ const row = db
143
+ .prepare(`SELECT p.id, p.user_id, p.credential_id, p.public_key, p.counter, p.transports, p.name, p.created_at, u.email
144
+ FROM ${t.passkeys} p
145
+ JOIN ${t.users} u ON u.id = p.user_id
146
+ WHERE p.credential_id = ?`)
147
+ .get(credentialId);
148
+ if (!row)
149
+ return null;
150
+ return {
151
+ id: row.id,
152
+ userId: row.user_id,
153
+ credentialId: row.credential_id,
154
+ publicKey: new Uint8Array(row.public_key),
155
+ counter: row.counter,
156
+ transports: row.transports,
157
+ name: row.name,
158
+ createdAt: row.created_at,
159
+ email: row.email
160
+ };
161
+ },
162
+ getUserPasskeys(userId) {
163
+ const rows = db
164
+ .prepare(`SELECT id, credential_id, public_key, counter, transports, name, created_at FROM ${t.passkeys} WHERE user_id = ?`)
165
+ .all(userId);
166
+ return rows.map((row) => ({
167
+ id: row.id,
168
+ credentialId: row.credential_id,
169
+ publicKey: new Uint8Array(row.public_key),
170
+ counter: row.counter,
171
+ transports: row.transports,
172
+ name: row.name,
173
+ createdAt: row.created_at
174
+ }));
175
+ },
176
+ storePasskey(passkey) {
177
+ db.prepare(`INSERT INTO ${t.passkeys} (id, user_id, credential_id, public_key, counter, transports, name) VALUES (?, ?, ?, ?, ?, ?, ?)`).run(passkey.id, passkey.userId, passkey.credentialId, Buffer.from(passkey.publicKey), passkey.counter, passkey.transports, passkey.name);
178
+ },
179
+ updatePasskeyCounter(id, counter) {
180
+ db.prepare(`UPDATE ${t.passkeys} SET counter = ? WHERE id = ?`).run(counter, id);
181
+ },
182
+ deletePasskey(id, userId) {
183
+ const result = db.prepare(`DELETE FROM ${t.passkeys} WHERE id = ? AND user_id = ?`).run(id, userId);
184
+ return result.changes > 0;
185
+ }
186
+ };
187
+ }
@@ -0,0 +1 @@
1
+ export declare function guessDeviceName(ua?: string): string;
package/dist/device.js ADDED
@@ -0,0 +1,36 @@
1
+ export function guessDeviceName(ua) {
2
+ const s = ua ?? (typeof navigator !== 'undefined' ? navigator.userAgent : '');
3
+ if (!s)
4
+ return 'Passkey';
5
+ const browser = /Edg\//i.test(s)
6
+ ? 'Edge'
7
+ : /OPR\//i.test(s)
8
+ ? 'Opera'
9
+ : /Chrome\//i.test(s)
10
+ ? 'Chrome'
11
+ : /Safari\//i.test(s) && !/Chrome/i.test(s)
12
+ ? 'Safari'
13
+ : /Firefox\//i.test(s)
14
+ ? 'Firefox'
15
+ : null;
16
+ const os = /Windows/i.test(s)
17
+ ? 'Windows'
18
+ : /Mac OS|Macintosh/i.test(s)
19
+ ? 'macOS'
20
+ : /Android/i.test(s)
21
+ ? 'Android'
22
+ : /iPhone|iPad|iPod/i.test(s)
23
+ ? 'iOS'
24
+ : /Linux/i.test(s)
25
+ ? 'Linux'
26
+ : /CrOS/i.test(s)
27
+ ? 'ChromeOS'
28
+ : null;
29
+ if (browser && os)
30
+ return `${browser} on ${os}`;
31
+ if (browser)
32
+ return browser;
33
+ if (os)
34
+ return os;
35
+ return 'Passkey';
36
+ }
@@ -0,0 +1,12 @@
1
+ import type { AuthConfig } from './types.js';
2
+ export { resolveConfig } from './config.js';
3
+ export { guessDeviceName } from './device.js';
4
+ export type { AuthConfig, AuthDB, AuthUser, FullPasskeyRecord, MaybePromise, NewPasskey, OTPRecord, OtpResult, PasskeyRecord, ResolvedConfig, SessionRecord } from './types.js';
5
+ export declare function createAuth(config: AuthConfig): Promise<{
6
+ handle: import("@sveltejs/kit").Handle;
7
+ handlers: {
8
+ GET: (event: import("@sveltejs/kit").RequestEvent) => Promise<Response>;
9
+ POST: (event: import("@sveltejs/kit").RequestEvent) => Promise<Response>;
10
+ };
11
+ config: import("./types.js").ResolvedConfig;
12
+ }>;
package/dist/index.js ADDED
@@ -0,0 +1,14 @@
1
+ import { resolveConfig } from './config.js';
2
+ import { createHandle } from './kit/handle.js';
3
+ import { createHandlers } from './kit/handlers.js';
4
+ export { resolveConfig } from './config.js';
5
+ export { guessDeviceName } from './device.js';
6
+ export async function createAuth(config) {
7
+ const resolved = resolveConfig(config);
8
+ await config.db.init();
9
+ return {
10
+ handle: createHandle(resolved),
11
+ handlers: createHandlers(resolved),
12
+ config: resolved
13
+ };
14
+ }
@@ -0,0 +1,3 @@
1
+ import type { Handle } from '@sveltejs/kit';
2
+ import type { ResolvedConfig } from '../types.js';
3
+ export declare function createHandle(config: ResolvedConfig): Handle;
@@ -0,0 +1,18 @@
1
+ import { validateSession } from '../session.js';
2
+ export function createHandle(config) {
3
+ return async ({ event, resolve }) => {
4
+ const token = event.cookies.get(config.cookie);
5
+ if (!token) {
6
+ event.locals.user = null;
7
+ return resolve(event);
8
+ }
9
+ const result = await validateSession(config.db, token);
10
+ if (!result) {
11
+ event.cookies.delete(config.cookie, { path: '/' });
12
+ event.locals.user = null;
13
+ return resolve(event);
14
+ }
15
+ event.locals.user = result.user;
16
+ return resolve(event);
17
+ };
18
+ }
@@ -0,0 +1,8 @@
1
+ import type { RequestEvent } from '@sveltejs/kit';
2
+ import type { ResolvedConfig } from '../types.js';
3
+ type RouteHandler = (event: RequestEvent) => Promise<Response>;
4
+ export declare function createHandlers(config: ResolvedConfig): {
5
+ GET: RouteHandler;
6
+ POST: RouteHandler;
7
+ };
8
+ export {};
@@ -0,0 +1,183 @@
1
+ import { json } from '@sveltejs/kit';
2
+ import { generateOTP, verifyOTP } from '../otp.js';
3
+ import { generateAuthenticationChallenge, generateRegistrationChallenge, removePasskey, verifyAuthenticationResponse, verifyRegistrationResponse } from '../passkey.js';
4
+ import { createSession, invalidateSession, validateSession } from '../session.js';
5
+ const SESSION_MAX_AGE_SECONDS = 30 * 24 * 60 * 60;
6
+ function requireAuth(event) {
7
+ const user = event.locals.user;
8
+ if (!user)
9
+ return json({ error: 'Not authenticated' }, { status: 401 });
10
+ return user;
11
+ }
12
+ export function createHandlers(config) {
13
+ const maxAge = Math.floor(config.sessionDuration / 1000);
14
+ function cookieOpts(event) {
15
+ return {
16
+ httpOnly: true,
17
+ secure: event.url.protocol === 'https:',
18
+ sameSite: 'lax',
19
+ path: '/',
20
+ maxAge
21
+ };
22
+ }
23
+ const routes = {
24
+ start: {
25
+ method: 'POST',
26
+ handler: async (event) => {
27
+ const body = await event.request.json().catch(() => null);
28
+ if (!body || typeof body.email !== 'string' || !body.email.includes('@')) {
29
+ return json({ error: 'Invalid email' }, { status: 400 });
30
+ }
31
+ const { code } = await generateOTP(config.db, body.email, config);
32
+ await config.onSendOTP(body.email, code);
33
+ return json({ success: true });
34
+ }
35
+ },
36
+ verify: {
37
+ method: 'POST',
38
+ handler: async (event) => {
39
+ const body = await event.request.json().catch(() => null);
40
+ if (!body || typeof body.email !== 'string' || typeof body.code !== 'string') {
41
+ return json({ error: 'Invalid input' }, { status: 400 });
42
+ }
43
+ const otp = await verifyOTP(config.db, body.email, body.code, config);
44
+ if (!otp.ok) {
45
+ const messages = {
46
+ invalid: 'Invalid code. Please try again.',
47
+ expired: 'Code expired. Please request a new one.',
48
+ rate_limited: 'Too many attempts. Please request a new code.'
49
+ };
50
+ return json({ error: messages[otp.error] }, { status: otp.error === 'rate_limited' ? 429 : 400 });
51
+ }
52
+ let user = await config.db.getUserByEmail(body.email);
53
+ if (!user) {
54
+ user = await config.db.createUser(body.email);
55
+ }
56
+ const session = await createSession(config.db, user.id, config);
57
+ event.cookies.set(config.cookie, session.sessionToken, cookieOpts(event));
58
+ const passkeys = await config.db.getUserPasskeys(user.id);
59
+ return json({
60
+ user: { id: user.id, email: user.email },
61
+ hasPasskey: passkeys.length > 0,
62
+ skipPasskeyPrompt: user.skipPasskeyPrompt
63
+ });
64
+ }
65
+ },
66
+ logout: {
67
+ method: 'POST',
68
+ handler: async (event) => {
69
+ const token = event.cookies.get(config.cookie);
70
+ if (token) {
71
+ const result = await validateSession(config.db, token);
72
+ if (result) {
73
+ await invalidateSession(config.db, result.session.id);
74
+ }
75
+ event.cookies.delete(config.cookie, { path: '/' });
76
+ }
77
+ return json({ ok: true });
78
+ }
79
+ },
80
+ 'passkey/login-start': {
81
+ method: 'GET',
82
+ handler: async (event) => {
83
+ const options = await generateAuthenticationChallenge(config.db, event.url);
84
+ return json(options);
85
+ }
86
+ },
87
+ 'passkey/login-finish': {
88
+ method: 'POST',
89
+ handler: async (event) => {
90
+ const body = await event.request.json().catch(() => null);
91
+ if (!body)
92
+ return json({ error: 'Invalid input' }, { status: 400 });
93
+ const result = await verifyAuthenticationResponse(config.db, body, event.url);
94
+ if (!result)
95
+ return json({ error: 'Authentication failed' }, { status: 401 });
96
+ const session = await createSession(config.db, result.user.id, config);
97
+ event.cookies.set(config.cookie, session.sessionToken, cookieOpts(event));
98
+ return json({ user: result.user });
99
+ }
100
+ },
101
+ 'passkey/register-start': {
102
+ method: 'POST',
103
+ handler: async (event) => {
104
+ const user = requireAuth(event);
105
+ if (user instanceof Response)
106
+ return user;
107
+ const options = await generateRegistrationChallenge(config.db, user, event.url, config);
108
+ return json(options);
109
+ }
110
+ },
111
+ 'passkey/register-finish': {
112
+ method: 'POST',
113
+ handler: async (event) => {
114
+ const user = requireAuth(event);
115
+ if (user instanceof Response)
116
+ return user;
117
+ const body = await event.request.json().catch(() => null);
118
+ if (!body)
119
+ return json({ error: 'Invalid input' }, { status: 400 });
120
+ const { name, ...response } = body;
121
+ const passkeyName = typeof name === 'string' && name.trim() ? name.trim() : null;
122
+ const success = await verifyRegistrationResponse(config.db, user.id, response, event.url, passkeyName);
123
+ if (!success)
124
+ return json({ error: 'Passkey registration failed' }, { status: 400 });
125
+ return json({ success: true });
126
+ }
127
+ },
128
+ 'passkey/remove': {
129
+ method: 'POST',
130
+ handler: async (event) => {
131
+ const user = requireAuth(event);
132
+ if (user instanceof Response)
133
+ return user;
134
+ const body = await event.request.json().catch(() => null);
135
+ if (!body || typeof body.passkeyId !== 'string') {
136
+ return json({ error: 'Invalid input' }, { status: 400 });
137
+ }
138
+ const success = await removePasskey(config.db, body.passkeyId, user.id);
139
+ if (!success)
140
+ return json({ error: 'Passkey not found' }, { status: 404 });
141
+ return json({ success: true });
142
+ }
143
+ },
144
+ 'skip-passkey': {
145
+ method: 'POST',
146
+ handler: async (event) => {
147
+ const user = requireAuth(event);
148
+ if (user instanceof Response)
149
+ return user;
150
+ await config.db.setSkipPasskeyPrompt(user.id, true);
151
+ return json({ success: true });
152
+ }
153
+ }
154
+ };
155
+ function getRoute(event) {
156
+ const path = event.params.path;
157
+ if (typeof path === 'string')
158
+ return path;
159
+ return null;
160
+ }
161
+ return {
162
+ GET: async (event) => {
163
+ const path = getRoute(event);
164
+ if (!path)
165
+ return json({ error: 'Not found' }, { status: 404 });
166
+ const route = routes[path];
167
+ if (!route || route.method !== 'GET') {
168
+ return json({ error: 'Not found' }, { status: 404 });
169
+ }
170
+ return route.handler(event);
171
+ },
172
+ POST: async (event) => {
173
+ const path = getRoute(event);
174
+ if (!path)
175
+ return json({ error: 'Not found' }, { status: 404 });
176
+ const route = routes[path];
177
+ if (!route || route.method !== 'POST') {
178
+ return json({ error: 'Not found' }, { status: 404 });
179
+ }
180
+ return route.handler(event);
181
+ }
182
+ };
183
+ }
package/dist/otp.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ import type { AuthDB, OtpResult, ResolvedConfig } from './types.js';
2
+ export declare function generateOTP(db: AuthDB, email: string, config: ResolvedConfig): Promise<{
3
+ id: string;
4
+ code: string;
5
+ }>;
6
+ export declare function verifyOTP(db: AuthDB, email: string, code: string, config: ResolvedConfig): Promise<OtpResult>;
package/dist/otp.js ADDED
@@ -0,0 +1,35 @@
1
+ import { randomInt, randomUUID } from 'node:crypto';
2
+ export async function generateOTP(db, email, config) {
3
+ await db.deleteOTPsForEmail(email);
4
+ const id = randomUUID();
5
+ const max = 10 ** config.otpLength;
6
+ const min = 10 ** (config.otpLength - 1);
7
+ const code = String(randomInt(min, max));
8
+ const expiresAt = Date.now() + config.otpExpiry;
9
+ await db.storeOTP(email, id, code, expiresAt);
10
+ return { id, code };
11
+ }
12
+ export async function verifyOTP(db, email, code, config) {
13
+ const row = await db.getLatestOTP(email);
14
+ if (!row)
15
+ return { ok: false, error: 'invalid' };
16
+ if (row.expiresAt < Date.now()) {
17
+ await db.deleteOTP(row.id);
18
+ return { ok: false, error: 'expired' };
19
+ }
20
+ if (row.attempts >= config.otpMaxAttempts) {
21
+ await db.deleteOTP(row.id);
22
+ return { ok: false, error: 'rate_limited', attemptsLeft: 0 };
23
+ }
24
+ if (row.code !== code) {
25
+ const newAttempts = row.attempts + 1;
26
+ await db.updateOTPAttempts(row.id, newAttempts);
27
+ if (newAttempts >= config.otpMaxAttempts) {
28
+ await db.deleteOTP(row.id);
29
+ return { ok: false, error: 'rate_limited', attemptsLeft: 0 };
30
+ }
31
+ return { ok: false, error: 'invalid' };
32
+ }
33
+ await db.deleteOTP(row.id);
34
+ return { ok: true };
35
+ }
@@ -0,0 +1,20 @@
1
+ import type { AuthenticationResponseJSON, RegistrationResponseJSON } from '@simplewebauthn/server';
2
+ import type { AuthDB, ResolvedConfig } from './types.js';
3
+ export declare function getWebAuthnConfig(requestUrl: URL): {
4
+ rpID: string;
5
+ origin: string;
6
+ };
7
+ export declare function generateRegistrationChallenge(db: AuthDB, user: {
8
+ id: string;
9
+ email: string;
10
+ }, requestUrl: URL, config: ResolvedConfig): Promise<import("@simplewebauthn/server").PublicKeyCredentialCreationOptionsJSON>;
11
+ export declare function verifyRegistrationResponse(db: AuthDB, userId: string, response: RegistrationResponseJSON, requestUrl: URL, name?: string | null): Promise<boolean>;
12
+ export declare function generateAuthenticationChallenge(db: AuthDB, requestUrl: URL): Promise<import("@simplewebauthn/server").PublicKeyCredentialRequestOptionsJSON>;
13
+ export declare function verifyAuthenticationResponse(db: AuthDB, response: AuthenticationResponseJSON, requestUrl: URL): Promise<{
14
+ user: {
15
+ id: string;
16
+ email: string;
17
+ };
18
+ } | null>;
19
+ export declare function getUserPasskeys(db: AuthDB, userId: string): Promise<import("./types.js").PasskeyRecord[]>;
20
+ export declare function removePasskey(db: AuthDB, passkeyId: string, userId: string): Promise<boolean>;
@@ -0,0 +1,112 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { generateAuthenticationOptions, generateRegistrationOptions, verifyAuthenticationResponse as verifyAuthResponse, verifyRegistrationResponse as verifyRegResponse } from '@simplewebauthn/server';
3
+ const CHALLENGE_EXPIRY_MS = 5 * 60 * 1000;
4
+ export function getWebAuthnConfig(requestUrl) {
5
+ const envOrigin = process.env.ORIGIN;
6
+ if (envOrigin) {
7
+ const url = new URL(envOrigin);
8
+ return { rpID: url.hostname, origin: url.origin };
9
+ }
10
+ return { rpID: requestUrl.hostname, origin: requestUrl.origin };
11
+ }
12
+ export async function generateRegistrationChallenge(db, user, requestUrl, config) {
13
+ const { rpID } = getWebAuthnConfig(requestUrl);
14
+ const existingPasskeys = await db.getUserPasskeys(user.id);
15
+ const excludeCredentials = existingPasskeys.map((pk) => ({
16
+ id: pk.credentialId,
17
+ transports: pk.transports ? JSON.parse(pk.transports) : undefined
18
+ }));
19
+ const options = await generateRegistrationOptions({
20
+ rpName: config.rpName,
21
+ rpID,
22
+ userName: user.email,
23
+ userID: new TextEncoder().encode(user.id),
24
+ authenticatorSelection: {
25
+ residentKey: 'required',
26
+ userVerification: 'preferred'
27
+ },
28
+ excludeCredentials
29
+ });
30
+ await db.storeChallenge(options.challenge, user.id, Date.now() + CHALLENGE_EXPIRY_MS);
31
+ return options;
32
+ }
33
+ export async function verifyRegistrationResponse(db, userId, response, requestUrl, name = null) {
34
+ const { rpID, origin } = getWebAuthnConfig(requestUrl);
35
+ const challenge = JSON.parse(Buffer.from(response.response.clientDataJSON, 'base64url').toString()).challenge;
36
+ const stored = await db.consumeChallenge(challenge);
37
+ if (!stored || stored.userId !== userId)
38
+ return false;
39
+ try {
40
+ const verification = await verifyRegResponse({
41
+ response,
42
+ expectedChallenge: challenge,
43
+ expectedOrigin: origin,
44
+ expectedRPID: rpID
45
+ });
46
+ if (!verification.verified || !verification.registrationInfo)
47
+ return false;
48
+ const { credential } = verification.registrationInfo;
49
+ await db.storePasskey({
50
+ id: randomUUID(),
51
+ userId,
52
+ credentialId: credential.id,
53
+ publicKey: new Uint8Array(credential.publicKey),
54
+ counter: credential.counter,
55
+ transports: response.response.transports ? JSON.stringify(response.response.transports) : null,
56
+ name
57
+ });
58
+ return true;
59
+ }
60
+ catch {
61
+ return false;
62
+ }
63
+ }
64
+ export async function generateAuthenticationChallenge(db, requestUrl) {
65
+ const { rpID } = getWebAuthnConfig(requestUrl);
66
+ const options = await generateAuthenticationOptions({
67
+ rpID,
68
+ allowCredentials: [],
69
+ userVerification: 'preferred'
70
+ });
71
+ await db.storeChallenge(options.challenge, 'anonymous', Date.now() + CHALLENGE_EXPIRY_MS);
72
+ return options;
73
+ }
74
+ export async function verifyAuthenticationResponse(db, response, requestUrl) {
75
+ const { rpID, origin } = getWebAuthnConfig(requestUrl);
76
+ const passkey = await db.getPasskeyByCredentialId(response.id);
77
+ if (!passkey)
78
+ return null;
79
+ const challenge = JSON.parse(Buffer.from(response.response.clientDataJSON, 'base64url').toString()).challenge;
80
+ const stored = await db.consumeChallenge(challenge);
81
+ if (!stored)
82
+ return null;
83
+ try {
84
+ const verification = await verifyAuthResponse({
85
+ response,
86
+ expectedChallenge: challenge,
87
+ expectedOrigin: origin,
88
+ expectedRPID: rpID,
89
+ credential: {
90
+ id: passkey.credentialId,
91
+ publicKey: new Uint8Array(passkey.publicKey),
92
+ counter: passkey.counter,
93
+ transports: passkey.transports ? JSON.parse(passkey.transports) : undefined
94
+ }
95
+ });
96
+ if (!verification.verified)
97
+ return null;
98
+ await db.updatePasskeyCounter(passkey.id, verification.authenticationInfo.newCounter);
99
+ return {
100
+ user: { id: passkey.userId, email: passkey.email }
101
+ };
102
+ }
103
+ catch {
104
+ return null;
105
+ }
106
+ }
107
+ export async function getUserPasskeys(db, userId) {
108
+ return db.getUserPasskeys(userId);
109
+ }
110
+ export async function removePasskey(db, passkeyId, userId) {
111
+ return db.deletePasskey(passkeyId, userId);
112
+ }
@@ -0,0 +1,16 @@
1
+ import type { AuthDB, ResolvedConfig } from './types.js';
2
+ export declare function createSession(db: AuthDB, userId: string, config: ResolvedConfig): Promise<{
3
+ sessionToken: string;
4
+ expiresAt: number;
5
+ }>;
6
+ export declare function validateSession(db: AuthDB, token: string): Promise<{
7
+ user: {
8
+ id: string;
9
+ email: string;
10
+ };
11
+ session: {
12
+ id: string;
13
+ expiresAt: number;
14
+ };
15
+ } | null>;
16
+ export declare function invalidateSession(db: AuthDB, sessionId: string): Promise<void>;