@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,168 @@
1
+ <script lang="ts">
2
+ import { onDestroy } from 'svelte';
3
+
4
+ interface Props {
5
+ countdownSeconds?: number;
6
+ onRegister: () => Promise<void>;
7
+ onSkip: () => void;
8
+ }
9
+
10
+ let { countdownSeconds = 3, onRegister, onSkip }: Props = $props();
11
+
12
+ let countdown = $state(countdownSeconds);
13
+ let failed = $state(false);
14
+ let registering = $state(false);
15
+
16
+ let interval = setInterval(() => {
17
+ countdown -= 1;
18
+ if (countdown <= 0) {
19
+ clearInterval(interval);
20
+ triggerRegistration();
21
+ }
22
+ }, 1000);
23
+
24
+ onDestroy(() => {
25
+ if (interval) clearInterval(interval);
26
+ });
27
+
28
+ async function triggerRegistration() {
29
+ if (registering) return;
30
+ registering = true;
31
+ try {
32
+ await onRegister();
33
+ } catch {
34
+ failed = true;
35
+ } finally {
36
+ registering = false;
37
+ }
38
+ }
39
+
40
+ let circumference = 2 * Math.PI * 40;
41
+ let dashOffset = $derived(circumference * (1 - countdown / countdownSeconds));
42
+ </script>
43
+
44
+ <div class="anahtar-passkey-prompt">
45
+ <div class="anahtar-passkey-ring">
46
+ <svg viewBox="0 0 100 100" class="anahtar-passkey-ring-svg">
47
+ <circle cx="50" cy="50" r="40" fill="none" stroke="var(--anahtar-border, #d1d5db)" stroke-width="4" />
48
+ <circle
49
+ cx="50" cy="50" r="40"
50
+ fill="none"
51
+ stroke="var(--anahtar-primary, #3b82f6)"
52
+ stroke-width="4"
53
+ stroke-linecap="round"
54
+ stroke-dasharray={circumference}
55
+ stroke-dashoffset={dashOffset}
56
+ class="anahtar-passkey-progress"
57
+ />
58
+ </svg>
59
+ <div class="anahtar-passkey-icon">
60
+ <svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" class="anahtar-key-pulse">
61
+ <circle cx="7.5" cy="15.5" r="5.5"/>
62
+ <path d="m11.5 12 4-4"/>
63
+ <path d="m15 7 2 2"/>
64
+ <path d="m17.5 4.5 2 2"/>
65
+ </svg>
66
+ </div>
67
+ </div>
68
+
69
+ {#if !failed}
70
+ <p class="anahtar-passkey-title">Making you a passkey</p>
71
+ <p class="anahtar-passkey-subtitle">for easier login</p>
72
+ <button onclick={onSkip} class="anahtar-passkey-skip">Skip</button>
73
+ {:else}
74
+ <p class="anahtar-passkey-title">Set up a passkey?</p>
75
+ <button onclick={triggerRegistration} class="anahtar-passkey-add" disabled={registering}>
76
+ Add passkey
77
+ </button>
78
+ <button onclick={onSkip} class="anahtar-passkey-skip">Maybe later</button>
79
+ {/if}
80
+ </div>
81
+
82
+ <style>
83
+ .anahtar-passkey-prompt {
84
+ display: flex;
85
+ flex-direction: column;
86
+ align-items: center;
87
+ }
88
+
89
+ .anahtar-passkey-ring {
90
+ position: relative;
91
+ width: 7rem;
92
+ height: 7rem;
93
+ margin-bottom: 1.5rem;
94
+ }
95
+
96
+ .anahtar-passkey-ring-svg {
97
+ width: 100%;
98
+ height: 100%;
99
+ transform: rotate(-90deg);
100
+ }
101
+
102
+ .anahtar-passkey-progress {
103
+ transition: stroke-dashoffset 1s linear;
104
+ }
105
+
106
+ .anahtar-passkey-icon {
107
+ position: absolute;
108
+ inset: 0;
109
+ display: flex;
110
+ align-items: center;
111
+ justify-content: center;
112
+ }
113
+
114
+ @keyframes key-pulse {
115
+ 0%, 100% { filter: drop-shadow(0 0 0 transparent); opacity: 0.8; }
116
+ 50% { filter: drop-shadow(0 0 8px var(--anahtar-primary, #3b82f6)); opacity: 1; }
117
+ }
118
+
119
+ .anahtar-key-pulse {
120
+ animation: key-pulse 2s ease-in-out infinite;
121
+ }
122
+
123
+ .anahtar-passkey-title {
124
+ font-size: 0.875rem;
125
+ font-weight: 500;
126
+ margin-bottom: 0.25rem;
127
+ }
128
+
129
+ .anahtar-passkey-subtitle {
130
+ font-size: 0.875rem;
131
+ opacity: 0.6;
132
+ margin-bottom: 1.5rem;
133
+ }
134
+
135
+ .anahtar-passkey-add {
136
+ width: 100%;
137
+ padding: 0.5rem;
138
+ font-size: 0.875rem;
139
+ font-weight: 500;
140
+ border-radius: 0.375rem;
141
+ background: var(--anahtar-primary, #3b82f6);
142
+ color: var(--anahtar-primary-fg, #fff);
143
+ border: none;
144
+ cursor: pointer;
145
+ margin-bottom: 0.75rem;
146
+ }
147
+
148
+ .anahtar-passkey-add:hover {
149
+ opacity: 0.9;
150
+ }
151
+
152
+ .anahtar-passkey-add:disabled {
153
+ opacity: 0.5;
154
+ }
155
+
156
+ .anahtar-passkey-skip {
157
+ font-size: 0.75rem;
158
+ opacity: 0.6;
159
+ background: none;
160
+ border: none;
161
+ cursor: pointer;
162
+ color: inherit;
163
+ }
164
+
165
+ .anahtar-passkey-skip:hover {
166
+ opacity: 1;
167
+ }
168
+ </style>
@@ -0,0 +1,8 @@
1
+ interface Props {
2
+ countdownSeconds?: number;
3
+ onRegister: () => Promise<void>;
4
+ onSkip: () => void;
5
+ }
6
+ declare const PasskeyPrompt: import("svelte").Component<Props, {}, "">;
7
+ type PasskeyPrompt = ReturnType<typeof PasskeyPrompt>;
8
+ export default PasskeyPrompt;
@@ -0,0 +1,4 @@
1
+ export { guessDeviceName } from '../device.js';
2
+ export { default as AuthFlow } from './AuthFlow.svelte';
3
+ export { default as OtpInput } from './OtpInput.svelte';
4
+ export { default as PasskeyPrompt } from './PasskeyPrompt.svelte';
@@ -0,0 +1,4 @@
1
+ export { guessDeviceName } from '../device.js';
2
+ export { default as AuthFlow } from './AuthFlow.svelte';
3
+ export { default as OtpInput } from './OtpInput.svelte';
4
+ export { default as PasskeyPrompt } from './PasskeyPrompt.svelte';
@@ -0,0 +1,2 @@
1
+ import type { AuthConfig, ResolvedConfig } from './types.js';
2
+ export declare function resolveConfig(config: AuthConfig): ResolvedConfig;
package/dist/config.js ADDED
@@ -0,0 +1,15 @@
1
+ const DEFAULTS = {
2
+ tablePrefix: 'auth_',
3
+ cookie: 'session',
4
+ sessionDuration: 30 * 24 * 60 * 60 * 1000,
5
+ otpExpiry: 30 * 60 * 1000,
6
+ otpLength: 5,
7
+ otpMaxAttempts: 5,
8
+ rpName: 'anahtar',
9
+ };
10
+ export function resolveConfig(config) {
11
+ return {
12
+ ...DEFAULTS,
13
+ ...config,
14
+ };
15
+ }
@@ -0,0 +1,22 @@
1
+ import type { AuthDB } from '../types.js';
2
+ interface D1Database {
3
+ prepare(sql: string): D1PreparedStatement;
4
+ exec(sql: string): Promise<unknown>;
5
+ }
6
+ interface D1PreparedStatement {
7
+ bind(...values: unknown[]): D1PreparedStatement;
8
+ first<T = Record<string, unknown>>(): Promise<T | null>;
9
+ all<T = Record<string, unknown>>(): Promise<{
10
+ results: T[];
11
+ }>;
12
+ run(): Promise<{
13
+ meta: {
14
+ changes: number;
15
+ };
16
+ }>;
17
+ }
18
+ interface D1AdapterOptions {
19
+ tablePrefix?: string;
20
+ }
21
+ export declare function d1Adapter(db: D1Database, options?: D1AdapterOptions): AuthDB;
22
+ export {};
package/dist/db/d1.js ADDED
@@ -0,0 +1,202 @@
1
+ export function d1Adapter(db, options = {}) {
2
+ const p = options.tablePrefix ?? 'auth_';
3
+ const t = {
4
+ users: `${p}users`,
5
+ sessions: `${p}sessions`,
6
+ otpCodes: `${p}otp_codes`,
7
+ passkeys: `${p}passkeys`,
8
+ challenges: `${p}challenges`
9
+ };
10
+ return {
11
+ async init() {
12
+ await db
13
+ .prepare(`CREATE TABLE IF NOT EXISTS ${t.users} (
14
+ id TEXT PRIMARY KEY, email TEXT UNIQUE NOT NULL,
15
+ skip_passkey_prompt INTEGER DEFAULT 0, created_at INTEGER DEFAULT (unixepoch())
16
+ )`)
17
+ .run();
18
+ await db
19
+ .prepare(`CREATE TABLE IF NOT EXISTS ${t.sessions} (
20
+ id TEXT PRIMARY KEY, user_id TEXT NOT NULL REFERENCES ${t.users}(id),
21
+ expires_at INTEGER NOT NULL, created_at INTEGER DEFAULT (unixepoch())
22
+ )`)
23
+ .run();
24
+ await db
25
+ .prepare(`CREATE TABLE IF NOT EXISTS ${t.otpCodes} (
26
+ id TEXT PRIMARY KEY, email TEXT NOT NULL, code TEXT NOT NULL,
27
+ attempts INTEGER NOT NULL DEFAULT 0, expires_at INTEGER NOT NULL,
28
+ created_at INTEGER DEFAULT (unixepoch())
29
+ )`)
30
+ .run();
31
+ await db
32
+ .prepare(`CREATE TABLE IF NOT EXISTS ${t.passkeys} (
33
+ id TEXT PRIMARY KEY, user_id TEXT NOT NULL REFERENCES ${t.users}(id),
34
+ credential_id TEXT UNIQUE NOT NULL, public_key BLOB NOT NULL,
35
+ counter INTEGER NOT NULL DEFAULT 0, transports TEXT, name TEXT,
36
+ created_at INTEGER DEFAULT (unixepoch())
37
+ )`)
38
+ .run();
39
+ await db
40
+ .prepare(`CREATE TABLE IF NOT EXISTS ${t.challenges} (
41
+ challenge TEXT PRIMARY KEY, user_id TEXT NOT NULL,
42
+ expires_at INTEGER NOT NULL, created_at INTEGER DEFAULT (unixepoch())
43
+ )`)
44
+ .run();
45
+ },
46
+ async getUserByEmail(email) {
47
+ const row = await db
48
+ .prepare(`SELECT id, email, skip_passkey_prompt, created_at FROM ${t.users} WHERE email = ?`)
49
+ .bind(email)
50
+ .first();
51
+ if (!row)
52
+ return null;
53
+ return {
54
+ id: row.id,
55
+ email: row.email,
56
+ skipPasskeyPrompt: row.skip_passkey_prompt === 1,
57
+ createdAt: row.created_at
58
+ };
59
+ },
60
+ async createUser(email) {
61
+ const id = crypto.randomUUID();
62
+ await db.prepare(`INSERT INTO ${t.users} (id, email) VALUES (?, ?)`).bind(id, email).run();
63
+ return {
64
+ id,
65
+ email,
66
+ skipPasskeyPrompt: false,
67
+ createdAt: Math.floor(Date.now() / 1000)
68
+ };
69
+ },
70
+ async setSkipPasskeyPrompt(userId, skip) {
71
+ await db
72
+ .prepare(`UPDATE ${t.users} SET skip_passkey_prompt = ? WHERE id = ?`)
73
+ .bind(skip ? 1 : 0, userId)
74
+ .run();
75
+ },
76
+ async createSession(tokenHash, userId, expiresAt) {
77
+ await db
78
+ .prepare(`INSERT INTO ${t.sessions} (id, user_id, expires_at) VALUES (?, ?, ?)`)
79
+ .bind(tokenHash, userId, expiresAt)
80
+ .run();
81
+ },
82
+ async getSession(tokenHash) {
83
+ const row = await db
84
+ .prepare(`SELECT s.id, s.user_id, s.expires_at, u.email
85
+ FROM ${t.sessions} s
86
+ JOIN ${t.users} u ON u.id = s.user_id
87
+ WHERE s.id = ?`)
88
+ .bind(tokenHash)
89
+ .first();
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
+ async deleteSession(tokenHash) {
100
+ await db.prepare(`DELETE FROM ${t.sessions} WHERE id = ?`).bind(tokenHash).run();
101
+ },
102
+ async storeOTP(email, id, code, expiresAt) {
103
+ await db
104
+ .prepare(`INSERT INTO ${t.otpCodes} (id, email, code, expires_at) VALUES (?, ?, ?, ?)`)
105
+ .bind(id, email, code, expiresAt)
106
+ .run();
107
+ },
108
+ async getLatestOTP(email) {
109
+ const row = await db
110
+ .prepare(`SELECT id, email, code, attempts, expires_at FROM ${t.otpCodes} WHERE email = ? ORDER BY created_at DESC LIMIT 1`)
111
+ .bind(email)
112
+ .first();
113
+ if (!row)
114
+ return null;
115
+ return {
116
+ id: row.id,
117
+ email: row.email,
118
+ code: row.code,
119
+ attempts: row.attempts,
120
+ expiresAt: row.expires_at
121
+ };
122
+ },
123
+ async updateOTPAttempts(id, attempts) {
124
+ await db.prepare(`UPDATE ${t.otpCodes} SET attempts = ? WHERE id = ?`).bind(attempts, id).run();
125
+ },
126
+ async deleteOTP(id) {
127
+ await db.prepare(`DELETE FROM ${t.otpCodes} WHERE id = ?`).bind(id).run();
128
+ },
129
+ async deleteOTPsForEmail(email) {
130
+ await db.prepare(`DELETE FROM ${t.otpCodes} WHERE email = ?`).bind(email).run();
131
+ },
132
+ async storeChallenge(challenge, userId, expiresAt) {
133
+ await db.prepare(`DELETE FROM ${t.challenges} WHERE expires_at < ?`).bind(Date.now()).run();
134
+ await db
135
+ .prepare(`INSERT INTO ${t.challenges} (challenge, user_id, expires_at) VALUES (?, ?, ?)`)
136
+ .bind(challenge, userId, expiresAt)
137
+ .run();
138
+ },
139
+ async consumeChallenge(challenge) {
140
+ const row = await db
141
+ .prepare(`SELECT user_id, expires_at FROM ${t.challenges} WHERE challenge = ?`)
142
+ .bind(challenge)
143
+ .first();
144
+ if (!row)
145
+ return null;
146
+ await db.prepare(`DELETE FROM ${t.challenges} WHERE challenge = ?`).bind(challenge).run();
147
+ if (row.expires_at < Date.now())
148
+ return null;
149
+ return { userId: row.user_id };
150
+ },
151
+ async getPasskeyByCredentialId(credentialId) {
152
+ const row = await db
153
+ .prepare(`SELECT p.id, p.user_id, p.credential_id, p.public_key, p.counter, p.transports, p.name, p.created_at, u.email
154
+ FROM ${t.passkeys} p
155
+ JOIN ${t.users} u ON u.id = p.user_id
156
+ WHERE p.credential_id = ?`)
157
+ .bind(credentialId)
158
+ .first();
159
+ if (!row)
160
+ return null;
161
+ return {
162
+ id: row.id,
163
+ userId: row.user_id,
164
+ credentialId: row.credential_id,
165
+ publicKey: new Uint8Array(row.public_key),
166
+ counter: row.counter,
167
+ transports: row.transports,
168
+ name: row.name,
169
+ createdAt: row.created_at,
170
+ email: row.email
171
+ };
172
+ },
173
+ async getUserPasskeys(userId) {
174
+ const { results } = await db
175
+ .prepare(`SELECT id, credential_id, public_key, counter, transports, name, created_at FROM ${t.passkeys} WHERE user_id = ?`)
176
+ .bind(userId)
177
+ .all();
178
+ return results.map((row) => ({
179
+ id: row.id,
180
+ credentialId: row.credential_id,
181
+ publicKey: new Uint8Array(row.public_key),
182
+ counter: row.counter,
183
+ transports: row.transports,
184
+ name: row.name,
185
+ createdAt: row.created_at
186
+ }));
187
+ },
188
+ async storePasskey(passkey) {
189
+ await db
190
+ .prepare(`INSERT INTO ${t.passkeys} (id, user_id, credential_id, public_key, counter, transports, name) VALUES (?, ?, ?, ?, ?, ?, ?)`)
191
+ .bind(passkey.id, passkey.userId, passkey.credentialId, passkey.publicKey, passkey.counter, passkey.transports, passkey.name)
192
+ .run();
193
+ },
194
+ async updatePasskeyCounter(id, counter) {
195
+ await db.prepare(`UPDATE ${t.passkeys} SET counter = ? WHERE id = ?`).bind(counter, id).run();
196
+ },
197
+ async deletePasskey(id, userId) {
198
+ const result = await db.prepare(`DELETE FROM ${t.passkeys} WHERE id = ? AND user_id = ?`).bind(id, userId).run();
199
+ return result.meta.changes > 0;
200
+ }
201
+ };
202
+ }
@@ -0,0 +1,12 @@
1
+ import type { AuthDB } from '../types.js';
2
+ interface PgPool {
3
+ query(text: string, values?: unknown[]): Promise<{
4
+ rows: Record<string, unknown>[];
5
+ rowCount: number | null;
6
+ }>;
7
+ }
8
+ interface PostgresAdapterOptions {
9
+ tablePrefix?: string;
10
+ }
11
+ export declare function postgresAdapter(pool: PgPool, options?: PostgresAdapterOptions): AuthDB;
12
+ export {};
@@ -0,0 +1,204 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ export function postgresAdapter(pool, 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
+ async function queryOne(text, values) {
12
+ const { rows } = await pool.query(text, values);
13
+ return rows[0];
14
+ }
15
+ async function queryAll(text, values) {
16
+ const { rows } = await pool.query(text, values);
17
+ return rows;
18
+ }
19
+ return {
20
+ async init() {
21
+ await pool.query(`
22
+ CREATE TABLE IF NOT EXISTS ${t.users} (
23
+ id TEXT PRIMARY KEY,
24
+ email TEXT UNIQUE NOT NULL,
25
+ skip_passkey_prompt BOOLEAN DEFAULT FALSE,
26
+ created_at TIMESTAMPTZ DEFAULT NOW()
27
+ );
28
+ CREATE TABLE IF NOT EXISTS ${t.sessions} (
29
+ id TEXT PRIMARY KEY,
30
+ user_id TEXT NOT NULL REFERENCES ${t.users}(id),
31
+ expires_at BIGINT NOT NULL,
32
+ created_at TIMESTAMPTZ DEFAULT NOW()
33
+ );
34
+ CREATE TABLE IF NOT EXISTS ${t.otpCodes} (
35
+ id TEXT PRIMARY KEY,
36
+ email TEXT NOT NULL,
37
+ code TEXT NOT NULL,
38
+ attempts INTEGER NOT NULL DEFAULT 0,
39
+ expires_at BIGINT NOT NULL,
40
+ created_at TIMESTAMPTZ DEFAULT NOW()
41
+ );
42
+ CREATE TABLE IF NOT EXISTS ${t.passkeys} (
43
+ id TEXT PRIMARY KEY,
44
+ user_id TEXT NOT NULL REFERENCES ${t.users}(id),
45
+ credential_id TEXT UNIQUE NOT NULL,
46
+ public_key BYTEA NOT NULL,
47
+ counter INTEGER NOT NULL DEFAULT 0,
48
+ transports TEXT,
49
+ name TEXT,
50
+ created_at TIMESTAMPTZ DEFAULT NOW()
51
+ );
52
+ CREATE TABLE IF NOT EXISTS ${t.challenges} (
53
+ challenge TEXT PRIMARY KEY,
54
+ user_id TEXT NOT NULL,
55
+ expires_at BIGINT NOT NULL,
56
+ created_at TIMESTAMPTZ DEFAULT NOW()
57
+ );
58
+ `);
59
+ },
60
+ async getUserByEmail(email) {
61
+ const row = await queryOne(`SELECT id, email, skip_passkey_prompt, created_at FROM ${t.users} WHERE email = $1`, [email]);
62
+ if (!row)
63
+ return null;
64
+ return {
65
+ id: row.id,
66
+ email: row.email,
67
+ skipPasskeyPrompt: row.skip_passkey_prompt,
68
+ createdAt: new Date(row.created_at).getTime()
69
+ };
70
+ },
71
+ async createUser(email) {
72
+ const id = randomUUID();
73
+ await pool.query(`INSERT INTO ${t.users} (id, email) VALUES ($1, $2)`, [id, email]);
74
+ return {
75
+ id,
76
+ email,
77
+ skipPasskeyPrompt: false,
78
+ createdAt: Date.now()
79
+ };
80
+ },
81
+ async setSkipPasskeyPrompt(userId, skip) {
82
+ await pool.query(`UPDATE ${t.users} SET skip_passkey_prompt = $1 WHERE id = $2`, [skip, userId]);
83
+ },
84
+ async createSession(tokenHash, userId, expiresAt) {
85
+ await pool.query(`INSERT INTO ${t.sessions} (id, user_id, expires_at) VALUES ($1, $2, $3)`, [
86
+ tokenHash,
87
+ userId,
88
+ expiresAt
89
+ ]);
90
+ },
91
+ async getSession(tokenHash) {
92
+ const row = await queryOne(`SELECT s.id, s.user_id, s.expires_at, u.email
93
+ FROM ${t.sessions} s
94
+ JOIN ${t.users} u ON u.id = s.user_id
95
+ WHERE s.id = $1`, [tokenHash]);
96
+ if (!row)
97
+ return null;
98
+ return {
99
+ id: row.id,
100
+ userId: row.user_id,
101
+ expiresAt: Number(row.expires_at),
102
+ email: row.email
103
+ };
104
+ },
105
+ async deleteSession(tokenHash) {
106
+ await pool.query(`DELETE FROM ${t.sessions} WHERE id = $1`, [tokenHash]);
107
+ },
108
+ async storeOTP(email, id, code, expiresAt) {
109
+ await pool.query(`INSERT INTO ${t.otpCodes} (id, email, code, expires_at) VALUES ($1, $2, $3, $4)`, [
110
+ id,
111
+ email,
112
+ code,
113
+ expiresAt
114
+ ]);
115
+ },
116
+ async getLatestOTP(email) {
117
+ const row = await queryOne(`SELECT id, email, code, attempts, expires_at FROM ${t.otpCodes} WHERE email = $1 ORDER BY created_at DESC LIMIT 1`, [email]);
118
+ if (!row)
119
+ return null;
120
+ return {
121
+ id: row.id,
122
+ email: row.email,
123
+ code: row.code,
124
+ attempts: row.attempts,
125
+ expiresAt: Number(row.expires_at)
126
+ };
127
+ },
128
+ async updateOTPAttempts(id, attempts) {
129
+ await pool.query(`UPDATE ${t.otpCodes} SET attempts = $1 WHERE id = $2`, [attempts, id]);
130
+ },
131
+ async deleteOTP(id) {
132
+ await pool.query(`DELETE FROM ${t.otpCodes} WHERE id = $1`, [id]);
133
+ },
134
+ async deleteOTPsForEmail(email) {
135
+ await pool.query(`DELETE FROM ${t.otpCodes} WHERE email = $1`, [email]);
136
+ },
137
+ async storeChallenge(challenge, userId, expiresAt) {
138
+ await pool.query(`DELETE FROM ${t.challenges} WHERE expires_at < $1`, [Date.now()]);
139
+ await pool.query(`INSERT INTO ${t.challenges} (challenge, user_id, expires_at) VALUES ($1, $2, $3)`, [
140
+ challenge,
141
+ userId,
142
+ expiresAt
143
+ ]);
144
+ },
145
+ async consumeChallenge(challenge) {
146
+ const row = await queryOne(`SELECT user_id, expires_at FROM ${t.challenges} WHERE challenge = $1`, [challenge]);
147
+ if (!row)
148
+ return null;
149
+ await pool.query(`DELETE FROM ${t.challenges} WHERE challenge = $1`, [challenge]);
150
+ if (Number(row.expires_at) < Date.now())
151
+ return null;
152
+ return { userId: row.user_id };
153
+ },
154
+ async getPasskeyByCredentialId(credentialId) {
155
+ const row = await queryOne(`SELECT p.id, p.user_id, p.credential_id, p.public_key, p.counter, p.transports, p.name, p.created_at, u.email
156
+ FROM ${t.passkeys} p
157
+ JOIN ${t.users} u ON u.id = p.user_id
158
+ WHERE p.credential_id = $1`, [credentialId]);
159
+ if (!row)
160
+ return null;
161
+ return {
162
+ id: row.id,
163
+ userId: row.user_id,
164
+ credentialId: row.credential_id,
165
+ publicKey: new Uint8Array(row.public_key),
166
+ counter: row.counter,
167
+ transports: row.transports,
168
+ name: row.name,
169
+ createdAt: new Date(row.created_at).getTime(),
170
+ email: row.email
171
+ };
172
+ },
173
+ async getUserPasskeys(userId) {
174
+ const rows = await queryAll(`SELECT id, credential_id, public_key, counter, transports, name, created_at FROM ${t.passkeys} WHERE user_id = $1`, [userId]);
175
+ return rows.map((row) => ({
176
+ id: row.id,
177
+ credentialId: row.credential_id,
178
+ publicKey: new Uint8Array(row.public_key),
179
+ counter: row.counter,
180
+ transports: row.transports,
181
+ name: row.name,
182
+ createdAt: new Date(row.created_at).getTime()
183
+ }));
184
+ },
185
+ async storePasskey(passkey) {
186
+ await pool.query(`INSERT INTO ${t.passkeys} (id, user_id, credential_id, public_key, counter, transports, name) VALUES ($1, $2, $3, $4, $5, $6, $7)`, [
187
+ passkey.id,
188
+ passkey.userId,
189
+ passkey.credentialId,
190
+ Buffer.from(passkey.publicKey),
191
+ passkey.counter,
192
+ passkey.transports,
193
+ passkey.name
194
+ ]);
195
+ },
196
+ async updatePasskeyCounter(id, counter) {
197
+ await pool.query(`UPDATE ${t.passkeys} SET counter = $1 WHERE id = $2`, [counter, id]);
198
+ },
199
+ async deletePasskey(id, userId) {
200
+ const result = await pool.query(`DELETE FROM ${t.passkeys} WHERE id = $1 AND user_id = $2`, [id, userId]);
201
+ return (result.rowCount ?? 0) > 0;
202
+ }
203
+ };
204
+ }
@@ -0,0 +1,7 @@
1
+ import type Database from 'better-sqlite3';
2
+ import type { AuthDB } from '../types.js';
3
+ interface SqliteAdapterOptions {
4
+ tablePrefix?: string;
5
+ }
6
+ export declare function sqliteAdapter(db: Database.Database, options?: SqliteAdapterOptions): AuthDB;
7
+ export {};