@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.
- package/dist/adapters/kysely.d.mts +62 -0
- package/dist/adapters/kysely.d.mts.map +1 -0
- package/dist/adapters/kysely.mjs +379 -0
- package/dist/adapters/kysely.mjs.map +1 -0
- package/dist/authenticate-D5UgaoTH.d.mts +124 -0
- package/dist/authenticate-D5UgaoTH.d.mts.map +1 -0
- package/dist/authenticate-j5GayLXB.mjs +373 -0
- package/dist/authenticate-j5GayLXB.mjs.map +1 -0
- package/dist/index.d.mts +444 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +728 -0
- package/dist/index.mjs.map +1 -0
- package/dist/oauth/providers/github.d.mts +12 -0
- package/dist/oauth/providers/github.d.mts.map +1 -0
- package/dist/oauth/providers/github.mjs +55 -0
- package/dist/oauth/providers/github.mjs.map +1 -0
- package/dist/oauth/providers/google.d.mts +7 -0
- package/dist/oauth/providers/google.d.mts.map +1 -0
- package/dist/oauth/providers/google.mjs +38 -0
- package/dist/oauth/providers/google.mjs.map +1 -0
- package/dist/passkey/index.d.mts +2 -0
- package/dist/passkey/index.mjs +3 -0
- package/dist/types-Bu4irX9A.d.mts +35 -0
- package/dist/types-Bu4irX9A.d.mts.map +1 -0
- package/dist/types-CiSNpRI9.mjs +60 -0
- package/dist/types-CiSNpRI9.mjs.map +1 -0
- package/dist/types-HtRc90Wi.d.mts +208 -0
- package/dist/types-HtRc90Wi.d.mts.map +1 -0
- package/package.json +72 -0
- package/src/adapters/kysely.ts +715 -0
- package/src/config.ts +214 -0
- package/src/index.ts +135 -0
- package/src/invite.ts +205 -0
- package/src/magic-link/index.ts +150 -0
- package/src/oauth/consumer.ts +324 -0
- package/src/oauth/providers/github.ts +68 -0
- package/src/oauth/providers/google.ts +34 -0
- package/src/oauth/types.ts +36 -0
- package/src/passkey/authenticate.ts +183 -0
- package/src/passkey/index.ts +27 -0
- package/src/passkey/register.ts +232 -0
- package/src/passkey/types.ts +120 -0
- package/src/rbac.test.ts +141 -0
- package/src/rbac.ts +205 -0
- package/src/signup.ts +210 -0
- package/src/tokens.test.ts +141 -0
- package/src/tokens.ts +238 -0
- package/src/types.ts +352 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { n as AuthAdapter } from "../types-HtRc90Wi.mjs";
|
|
2
|
+
import { Kysely } from "kysely";
|
|
3
|
+
|
|
4
|
+
//#region src/adapters/kysely.d.ts
|
|
5
|
+
interface AuthTables {
|
|
6
|
+
users: UserTable;
|
|
7
|
+
credentials: CredentialTable;
|
|
8
|
+
auth_tokens: AuthTokenTable;
|
|
9
|
+
oauth_accounts: OAuthAccountTable;
|
|
10
|
+
allowed_domains: AllowedDomainTable;
|
|
11
|
+
}
|
|
12
|
+
interface UserTable {
|
|
13
|
+
id: string;
|
|
14
|
+
email: string;
|
|
15
|
+
name: string | null;
|
|
16
|
+
avatar_url: string | null;
|
|
17
|
+
role: number;
|
|
18
|
+
email_verified: number;
|
|
19
|
+
disabled: number;
|
|
20
|
+
data: string | null;
|
|
21
|
+
created_at: string;
|
|
22
|
+
updated_at: string;
|
|
23
|
+
}
|
|
24
|
+
interface CredentialTable {
|
|
25
|
+
id: string;
|
|
26
|
+
user_id: string;
|
|
27
|
+
public_key: Uint8Array;
|
|
28
|
+
counter: number;
|
|
29
|
+
device_type: string;
|
|
30
|
+
backed_up: number;
|
|
31
|
+
transports: string | null;
|
|
32
|
+
name: string | null;
|
|
33
|
+
created_at: string;
|
|
34
|
+
last_used_at: string;
|
|
35
|
+
}
|
|
36
|
+
interface AuthTokenTable {
|
|
37
|
+
hash: string;
|
|
38
|
+
user_id: string | null;
|
|
39
|
+
email: string | null;
|
|
40
|
+
type: string;
|
|
41
|
+
role: number | null;
|
|
42
|
+
invited_by: string | null;
|
|
43
|
+
expires_at: string;
|
|
44
|
+
created_at: string;
|
|
45
|
+
}
|
|
46
|
+
interface OAuthAccountTable {
|
|
47
|
+
provider: string;
|
|
48
|
+
provider_account_id: string;
|
|
49
|
+
user_id: string;
|
|
50
|
+
created_at: string;
|
|
51
|
+
}
|
|
52
|
+
interface AllowedDomainTable {
|
|
53
|
+
domain: string;
|
|
54
|
+
default_role: number;
|
|
55
|
+
enabled: number;
|
|
56
|
+
created_at: string;
|
|
57
|
+
}
|
|
58
|
+
declare function createKyselyAdapter<T extends AuthTables>(db: Kysely<T>): AuthAdapter;
|
|
59
|
+
declare const AUTH_TABLES_SQL = "\n-- Users (no password_hash)\nCREATE TABLE IF NOT EXISTS users (\n id TEXT PRIMARY KEY,\n email TEXT UNIQUE NOT NULL,\n name TEXT,\n avatar_url TEXT,\n role INTEGER NOT NULL DEFAULT 10,\n email_verified INTEGER NOT NULL DEFAULT 0,\n disabled INTEGER NOT NULL DEFAULT 0,\n data TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\n\nCREATE INDEX IF NOT EXISTS idx_users_email ON users(email);\n\n-- Passkey credentials\nCREATE TABLE IF NOT EXISTS credentials (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n public_key BLOB NOT NULL,\n counter INTEGER NOT NULL DEFAULT 0,\n device_type TEXT NOT NULL,\n backed_up INTEGER NOT NULL DEFAULT 0,\n transports TEXT,\n name TEXT,\n created_at TEXT NOT NULL,\n last_used_at TEXT NOT NULL\n);\n\nCREATE INDEX IF NOT EXISTS idx_credentials_user ON credentials(user_id);\n\n-- Auth tokens (magic links, email verification, invites)\nCREATE TABLE IF NOT EXISTS auth_tokens (\n hash TEXT PRIMARY KEY,\n user_id TEXT REFERENCES users(id) ON DELETE CASCADE,\n email TEXT,\n type TEXT NOT NULL,\n role INTEGER,\n invited_by TEXT REFERENCES users(id),\n expires_at TEXT NOT NULL,\n created_at TEXT NOT NULL\n);\n\nCREATE INDEX IF NOT EXISTS idx_auth_tokens_email ON auth_tokens(email);\n\n-- OAuth accounts (external provider links)\nCREATE TABLE IF NOT EXISTS oauth_accounts (\n provider TEXT NOT NULL,\n provider_account_id TEXT NOT NULL,\n user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n created_at TEXT NOT NULL,\n PRIMARY KEY (provider, provider_account_id)\n);\n\nCREATE INDEX IF NOT EXISTS idx_oauth_accounts_user ON oauth_accounts(user_id);\n\n-- Allowed domains for self-signup\nCREATE TABLE IF NOT EXISTS allowed_domains (\n domain TEXT PRIMARY KEY,\n default_role INTEGER NOT NULL DEFAULT 20,\n enabled INTEGER NOT NULL DEFAULT 1,\n created_at TEXT NOT NULL\n);\n";
|
|
60
|
+
//#endregion
|
|
61
|
+
export { AUTH_TABLES_SQL, AuthTables, createKyselyAdapter };
|
|
62
|
+
//# sourceMappingURL=kysely.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kysely.d.mts","names":[],"sources":["../../src/adapters/kysely.ts"],"mappings":";;;;UA+BiB,UAAA;EAChB,KAAA,EAAO,SAAA;EACP,WAAA,EAAa,eAAA;EACb,WAAA,EAAa,cAAA;EACb,cAAA,EAAgB,iBAAA;EAChB,eAAA,EAAiB,kBAAA;AAAA;AAAA,UAGR,SAAA;EACT,EAAA;EACA,KAAA;EACA,IAAA;EACA,UAAA;EACA,IAAA;EACA,cAAA;EACA,QAAA;EACA,IAAA;EACA,UAAA;EACA,UAAA;AAAA;AAAA,UAGS,eAAA;EACT,EAAA;EACA,OAAA;EACA,UAAA,EAAY,UAAA;EACZ,OAAA;EACA,WAAA;EACA,SAAA;EACA,UAAA;EACA,IAAA;EACA,UAAA;EACA,YAAA;AAAA;AAAA,UAGS,cAAA;EACT,IAAA;EACA,OAAA;EACA,KAAA;EACA,IAAA;EACA,IAAA;EACA,UAAA;EACA,UAAA;EACA,UAAA;AAAA;AAAA,UAGS,iBAAA;EACT,QAAA;EACA,mBAAA;EACA,OAAA;EACA,UAAA;AAAA;AAAA,UAGS,kBAAA;EACT,MAAA;EACA,YAAA;EACA,OAAA;EACA,UAAA;AAAA;AAAA,iBAOe,mBAAA,WAA8B,UAAA,CAAA,CAAY,EAAA,EAAI,MAAA,CAAO,CAAA,IAAK,WAAA;AAAA,cA2iB7D,eAAA"}
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import { a as toDeviceType, n as Role, o as toRoleLevel, s as toTokenType } from "../types-CiSNpRI9.mjs";
|
|
2
|
+
import { ulid } from "ulidx";
|
|
3
|
+
|
|
4
|
+
//#region src/adapters/kysely.ts
|
|
5
|
+
function createKyselyAdapter(db) {
|
|
6
|
+
const kdb = db;
|
|
7
|
+
return {
|
|
8
|
+
async getUserById(id) {
|
|
9
|
+
const row = await kdb.selectFrom("users").selectAll().where("id", "=", id).executeTakeFirst();
|
|
10
|
+
return row ? rowToUser(row) : null;
|
|
11
|
+
},
|
|
12
|
+
async getUserByEmail(email) {
|
|
13
|
+
const row = await kdb.selectFrom("users").selectAll().where("email", "=", email.toLowerCase()).executeTakeFirst();
|
|
14
|
+
return row ? rowToUser(row) : null;
|
|
15
|
+
},
|
|
16
|
+
async createUser(user) {
|
|
17
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
18
|
+
const id = ulid();
|
|
19
|
+
const row = {
|
|
20
|
+
id,
|
|
21
|
+
email: user.email.toLowerCase(),
|
|
22
|
+
name: user.name ?? null,
|
|
23
|
+
avatar_url: user.avatarUrl ?? null,
|
|
24
|
+
role: user.role ?? Role.SUBSCRIBER,
|
|
25
|
+
email_verified: user.emailVerified ? 1 : 0,
|
|
26
|
+
disabled: 0,
|
|
27
|
+
data: user.data ? JSON.stringify(user.data) : null,
|
|
28
|
+
created_at: now,
|
|
29
|
+
updated_at: now
|
|
30
|
+
};
|
|
31
|
+
await kdb.insertInto("users").values(row).execute();
|
|
32
|
+
return {
|
|
33
|
+
id,
|
|
34
|
+
email: row.email,
|
|
35
|
+
name: user.name ?? null,
|
|
36
|
+
avatarUrl: user.avatarUrl ?? null,
|
|
37
|
+
role: toRoleLevel(row.role),
|
|
38
|
+
emailVerified: row.email_verified === 1,
|
|
39
|
+
disabled: false,
|
|
40
|
+
data: user.data ?? null,
|
|
41
|
+
createdAt: new Date(now),
|
|
42
|
+
updatedAt: new Date(now)
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
async updateUser(id, data) {
|
|
46
|
+
const update = { updated_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
47
|
+
if (data.email !== void 0) update.email = data.email.toLowerCase();
|
|
48
|
+
if (data.name !== void 0) update.name = data.name;
|
|
49
|
+
if (data.avatarUrl !== void 0) update.avatar_url = data.avatarUrl;
|
|
50
|
+
if (data.role !== void 0) update.role = data.role;
|
|
51
|
+
if (data.emailVerified !== void 0) update.email_verified = data.emailVerified ? 1 : 0;
|
|
52
|
+
if (data.disabled !== void 0) update.disabled = data.disabled ? 1 : 0;
|
|
53
|
+
if (data.data !== void 0) update.data = data.data ? JSON.stringify(data.data) : null;
|
|
54
|
+
await kdb.updateTable("users").set(update).where("id", "=", id).execute();
|
|
55
|
+
},
|
|
56
|
+
async deleteUser(id) {
|
|
57
|
+
await kdb.deleteFrom("users").where("id", "=", id).execute();
|
|
58
|
+
},
|
|
59
|
+
async countUsers() {
|
|
60
|
+
return (await kdb.selectFrom("users").select((eb) => eb.fn.countAll().as("count")).executeTakeFirstOrThrow()).count;
|
|
61
|
+
},
|
|
62
|
+
async getUsers(options) {
|
|
63
|
+
const limit = Math.min(options?.limit ?? 20, 100);
|
|
64
|
+
let query = kdb.selectFrom("users").leftJoin("credentials", "users.id", "credentials.user_id").selectAll("users").select((eb) => [eb.fn.count("credentials.id").as("credential_count"), eb.fn.max("credentials.last_used_at").as("last_login")]).groupBy("users.id").orderBy("users.created_at", "desc").limit(limit + 1);
|
|
65
|
+
if (options?.search) {
|
|
66
|
+
const searchPattern = `%${options.search}%`;
|
|
67
|
+
query = query.where((eb) => eb.or([eb("users.email", "like", searchPattern), eb("users.name", "like", searchPattern)]));
|
|
68
|
+
}
|
|
69
|
+
if (options?.role !== void 0) query = query.where("users.role", "=", options.role);
|
|
70
|
+
if (options?.cursor) {
|
|
71
|
+
const cursorUser = await kdb.selectFrom("users").select("created_at").where("id", "=", options.cursor).executeTakeFirst();
|
|
72
|
+
if (cursorUser) query = query.where("users.created_at", "<", cursorUser.created_at);
|
|
73
|
+
}
|
|
74
|
+
const rows = await query.execute();
|
|
75
|
+
const userIds = rows.slice(0, limit).map((r) => r.id);
|
|
76
|
+
const oauthAccounts = userIds.length > 0 ? await kdb.selectFrom("oauth_accounts").select(["user_id", "provider"]).where("user_id", "in", userIds).execute() : [];
|
|
77
|
+
const oauthByUser = /* @__PURE__ */ new Map();
|
|
78
|
+
for (const account of oauthAccounts) {
|
|
79
|
+
const providers = oauthByUser.get(account.user_id) ?? [];
|
|
80
|
+
providers.push(account.provider);
|
|
81
|
+
oauthByUser.set(account.user_id, providers);
|
|
82
|
+
}
|
|
83
|
+
const hasMore = rows.length > limit;
|
|
84
|
+
const items = rows.slice(0, limit).map((row) => ({
|
|
85
|
+
id: row.id,
|
|
86
|
+
email: row.email,
|
|
87
|
+
name: row.name,
|
|
88
|
+
avatarUrl: row.avatar_url,
|
|
89
|
+
role: toRoleLevel(row.role),
|
|
90
|
+
emailVerified: row.email_verified === 1,
|
|
91
|
+
disabled: row.disabled === 1,
|
|
92
|
+
data: row.data ? JSON.parse(row.data) : null,
|
|
93
|
+
createdAt: new Date(row.created_at),
|
|
94
|
+
updatedAt: new Date(row.updated_at),
|
|
95
|
+
lastLogin: row.last_login ? new Date(row.last_login) : null,
|
|
96
|
+
credentialCount: row.credential_count ?? 0,
|
|
97
|
+
oauthProviders: oauthByUser.get(row.id) ?? []
|
|
98
|
+
}));
|
|
99
|
+
return {
|
|
100
|
+
items,
|
|
101
|
+
nextCursor: hasMore ? items.at(-1)?.id : void 0
|
|
102
|
+
};
|
|
103
|
+
},
|
|
104
|
+
async getUserWithDetails(id) {
|
|
105
|
+
const user = await kdb.selectFrom("users").selectAll().where("id", "=", id).executeTakeFirst();
|
|
106
|
+
if (!user) return null;
|
|
107
|
+
const [credentials, oauthAccounts] = await Promise.all([kdb.selectFrom("credentials").selectAll().where("user_id", "=", id).orderBy("created_at", "desc").execute(), kdb.selectFrom("oauth_accounts").selectAll().where("user_id", "=", id).execute()]);
|
|
108
|
+
const lastLogin = credentials.reduce((latest, cred) => {
|
|
109
|
+
const lastUsed = new Date(cred.last_used_at);
|
|
110
|
+
return !latest || lastUsed > latest ? lastUsed : latest;
|
|
111
|
+
}, null);
|
|
112
|
+
return {
|
|
113
|
+
user: rowToUser(user),
|
|
114
|
+
credentials: credentials.map(rowToCredential),
|
|
115
|
+
oauthAccounts: oauthAccounts.map(rowToOAuthAccount),
|
|
116
|
+
lastLogin
|
|
117
|
+
};
|
|
118
|
+
},
|
|
119
|
+
async countAdmins() {
|
|
120
|
+
return (await kdb.selectFrom("users").select((eb) => eb.fn.countAll().as("count")).where("role", "=", Role.ADMIN).where("disabled", "=", 0).executeTakeFirstOrThrow()).count;
|
|
121
|
+
},
|
|
122
|
+
async getCredentialById(id) {
|
|
123
|
+
const row = await kdb.selectFrom("credentials").selectAll().where("id", "=", id).executeTakeFirst();
|
|
124
|
+
return row ? rowToCredential(row) : null;
|
|
125
|
+
},
|
|
126
|
+
async getCredentialsByUserId(userId) {
|
|
127
|
+
return (await kdb.selectFrom("credentials").selectAll().where("user_id", "=", userId).execute()).map(rowToCredential);
|
|
128
|
+
},
|
|
129
|
+
async createCredential(credential) {
|
|
130
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
131
|
+
const row = {
|
|
132
|
+
id: credential.id,
|
|
133
|
+
user_id: credential.userId,
|
|
134
|
+
public_key: credential.publicKey,
|
|
135
|
+
counter: credential.counter,
|
|
136
|
+
device_type: credential.deviceType,
|
|
137
|
+
backed_up: credential.backedUp ? 1 : 0,
|
|
138
|
+
transports: credential.transports.length > 0 ? JSON.stringify(credential.transports) : null,
|
|
139
|
+
name: credential.name ?? null,
|
|
140
|
+
created_at: now,
|
|
141
|
+
last_used_at: now
|
|
142
|
+
};
|
|
143
|
+
await kdb.insertInto("credentials").values(row).execute();
|
|
144
|
+
return {
|
|
145
|
+
id: credential.id,
|
|
146
|
+
userId: credential.userId,
|
|
147
|
+
publicKey: credential.publicKey,
|
|
148
|
+
counter: credential.counter,
|
|
149
|
+
deviceType: credential.deviceType,
|
|
150
|
+
backedUp: credential.backedUp,
|
|
151
|
+
transports: credential.transports,
|
|
152
|
+
name: credential.name ?? null,
|
|
153
|
+
createdAt: new Date(now),
|
|
154
|
+
lastUsedAt: new Date(now)
|
|
155
|
+
};
|
|
156
|
+
},
|
|
157
|
+
async updateCredentialCounter(id, counter) {
|
|
158
|
+
await kdb.updateTable("credentials").set({
|
|
159
|
+
counter,
|
|
160
|
+
last_used_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
161
|
+
}).where("id", "=", id).execute();
|
|
162
|
+
},
|
|
163
|
+
async updateCredentialName(id, name) {
|
|
164
|
+
await kdb.updateTable("credentials").set({ name }).where("id", "=", id).execute();
|
|
165
|
+
},
|
|
166
|
+
async deleteCredential(id) {
|
|
167
|
+
await kdb.deleteFrom("credentials").where("id", "=", id).execute();
|
|
168
|
+
},
|
|
169
|
+
async countCredentialsByUserId(userId) {
|
|
170
|
+
return (await kdb.selectFrom("credentials").select((eb) => eb.fn.countAll().as("count")).where("user_id", "=", userId).executeTakeFirstOrThrow()).count;
|
|
171
|
+
},
|
|
172
|
+
async createToken(token) {
|
|
173
|
+
const row = {
|
|
174
|
+
hash: token.hash,
|
|
175
|
+
user_id: token.userId ?? null,
|
|
176
|
+
email: token.email ?? null,
|
|
177
|
+
type: token.type,
|
|
178
|
+
role: token.role ?? null,
|
|
179
|
+
invited_by: token.invitedBy ?? null,
|
|
180
|
+
expires_at: token.expiresAt.toISOString(),
|
|
181
|
+
created_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
182
|
+
};
|
|
183
|
+
await kdb.insertInto("auth_tokens").values(row).execute();
|
|
184
|
+
},
|
|
185
|
+
async getToken(hash, type) {
|
|
186
|
+
const row = await kdb.selectFrom("auth_tokens").selectAll().where("hash", "=", hash).where("type", "=", type).executeTakeFirst();
|
|
187
|
+
return row ? rowToAuthToken(row) : null;
|
|
188
|
+
},
|
|
189
|
+
async deleteToken(hash) {
|
|
190
|
+
await kdb.deleteFrom("auth_tokens").where("hash", "=", hash).execute();
|
|
191
|
+
},
|
|
192
|
+
async deleteExpiredTokens() {
|
|
193
|
+
await kdb.deleteFrom("auth_tokens").where("expires_at", "<", (/* @__PURE__ */ new Date()).toISOString()).execute();
|
|
194
|
+
},
|
|
195
|
+
async getOAuthAccount(provider, providerAccountId) {
|
|
196
|
+
const row = await kdb.selectFrom("oauth_accounts").selectAll().where("provider", "=", provider).where("provider_account_id", "=", providerAccountId).executeTakeFirst();
|
|
197
|
+
return row ? rowToOAuthAccount(row) : null;
|
|
198
|
+
},
|
|
199
|
+
async getOAuthAccountsByUserId(userId) {
|
|
200
|
+
return (await kdb.selectFrom("oauth_accounts").selectAll().where("user_id", "=", userId).execute()).map(rowToOAuthAccount);
|
|
201
|
+
},
|
|
202
|
+
async createOAuthAccount(account) {
|
|
203
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
204
|
+
const row = {
|
|
205
|
+
provider: account.provider,
|
|
206
|
+
provider_account_id: account.providerAccountId,
|
|
207
|
+
user_id: account.userId,
|
|
208
|
+
created_at: now
|
|
209
|
+
};
|
|
210
|
+
await kdb.insertInto("oauth_accounts").values(row).execute();
|
|
211
|
+
return {
|
|
212
|
+
provider: account.provider,
|
|
213
|
+
providerAccountId: account.providerAccountId,
|
|
214
|
+
userId: account.userId,
|
|
215
|
+
createdAt: new Date(now)
|
|
216
|
+
};
|
|
217
|
+
},
|
|
218
|
+
async deleteOAuthAccount(provider, providerAccountId) {
|
|
219
|
+
await kdb.deleteFrom("oauth_accounts").where("provider", "=", provider).where("provider_account_id", "=", providerAccountId).execute();
|
|
220
|
+
},
|
|
221
|
+
async getAllowedDomain(domain) {
|
|
222
|
+
const row = await kdb.selectFrom("allowed_domains").selectAll().where("domain", "=", domain.toLowerCase()).executeTakeFirst();
|
|
223
|
+
return row ? rowToAllowedDomain(row) : null;
|
|
224
|
+
},
|
|
225
|
+
async getAllowedDomains() {
|
|
226
|
+
return (await kdb.selectFrom("allowed_domains").selectAll().execute()).map(rowToAllowedDomain);
|
|
227
|
+
},
|
|
228
|
+
async createAllowedDomain(domain, defaultRole) {
|
|
229
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
230
|
+
const row = {
|
|
231
|
+
domain: domain.toLowerCase(),
|
|
232
|
+
default_role: defaultRole,
|
|
233
|
+
enabled: 1,
|
|
234
|
+
created_at: now
|
|
235
|
+
};
|
|
236
|
+
await kdb.insertInto("allowed_domains").values(row).execute();
|
|
237
|
+
return {
|
|
238
|
+
domain: row.domain,
|
|
239
|
+
defaultRole,
|
|
240
|
+
enabled: true,
|
|
241
|
+
createdAt: new Date(now)
|
|
242
|
+
};
|
|
243
|
+
},
|
|
244
|
+
async updateAllowedDomain(domain, enabled, defaultRole) {
|
|
245
|
+
const update = { enabled: enabled ? 1 : 0 };
|
|
246
|
+
if (defaultRole !== void 0) update.default_role = defaultRole;
|
|
247
|
+
await kdb.updateTable("allowed_domains").set(update).where("domain", "=", domain.toLowerCase()).execute();
|
|
248
|
+
},
|
|
249
|
+
async deleteAllowedDomain(domain) {
|
|
250
|
+
await kdb.deleteFrom("allowed_domains").where("domain", "=", domain.toLowerCase()).execute();
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
function rowToUser(row) {
|
|
255
|
+
return {
|
|
256
|
+
id: row.id,
|
|
257
|
+
email: row.email,
|
|
258
|
+
name: row.name,
|
|
259
|
+
avatarUrl: row.avatar_url,
|
|
260
|
+
role: toRoleLevel(row.role),
|
|
261
|
+
emailVerified: row.email_verified === 1,
|
|
262
|
+
disabled: row.disabled === 1,
|
|
263
|
+
data: row.data ? JSON.parse(row.data) : null,
|
|
264
|
+
createdAt: new Date(row.created_at),
|
|
265
|
+
updatedAt: new Date(row.updated_at)
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
function rowToCredential(row) {
|
|
269
|
+
return {
|
|
270
|
+
id: row.id,
|
|
271
|
+
userId: row.user_id,
|
|
272
|
+
publicKey: row.public_key,
|
|
273
|
+
counter: row.counter,
|
|
274
|
+
deviceType: toDeviceType(row.device_type),
|
|
275
|
+
backedUp: row.backed_up === 1,
|
|
276
|
+
transports: row.transports ? JSON.parse(row.transports) : [],
|
|
277
|
+
name: row.name,
|
|
278
|
+
createdAt: new Date(row.created_at),
|
|
279
|
+
lastUsedAt: new Date(row.last_used_at)
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
function rowToAuthToken(row) {
|
|
283
|
+
return {
|
|
284
|
+
hash: row.hash,
|
|
285
|
+
userId: row.user_id,
|
|
286
|
+
email: row.email,
|
|
287
|
+
type: toTokenType(row.type),
|
|
288
|
+
role: row.role != null ? toRoleLevel(row.role) : null,
|
|
289
|
+
invitedBy: row.invited_by,
|
|
290
|
+
expiresAt: new Date(row.expires_at),
|
|
291
|
+
createdAt: new Date(row.created_at)
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
function rowToOAuthAccount(row) {
|
|
295
|
+
return {
|
|
296
|
+
provider: row.provider,
|
|
297
|
+
providerAccountId: row.provider_account_id,
|
|
298
|
+
userId: row.user_id,
|
|
299
|
+
createdAt: new Date(row.created_at)
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
function rowToAllowedDomain(row) {
|
|
303
|
+
return {
|
|
304
|
+
domain: row.domain,
|
|
305
|
+
defaultRole: toRoleLevel(row.default_role),
|
|
306
|
+
enabled: row.enabled === 1,
|
|
307
|
+
createdAt: new Date(row.created_at)
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
const AUTH_TABLES_SQL = `
|
|
311
|
+
-- Users (no password_hash)
|
|
312
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
313
|
+
id TEXT PRIMARY KEY,
|
|
314
|
+
email TEXT UNIQUE NOT NULL,
|
|
315
|
+
name TEXT,
|
|
316
|
+
avatar_url TEXT,
|
|
317
|
+
role INTEGER NOT NULL DEFAULT 10,
|
|
318
|
+
email_verified INTEGER NOT NULL DEFAULT 0,
|
|
319
|
+
disabled INTEGER NOT NULL DEFAULT 0,
|
|
320
|
+
data TEXT,
|
|
321
|
+
created_at TEXT NOT NULL,
|
|
322
|
+
updated_at TEXT NOT NULL
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
|
|
326
|
+
|
|
327
|
+
-- Passkey credentials
|
|
328
|
+
CREATE TABLE IF NOT EXISTS credentials (
|
|
329
|
+
id TEXT PRIMARY KEY,
|
|
330
|
+
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
331
|
+
public_key BLOB NOT NULL,
|
|
332
|
+
counter INTEGER NOT NULL DEFAULT 0,
|
|
333
|
+
device_type TEXT NOT NULL,
|
|
334
|
+
backed_up INTEGER NOT NULL DEFAULT 0,
|
|
335
|
+
transports TEXT,
|
|
336
|
+
name TEXT,
|
|
337
|
+
created_at TEXT NOT NULL,
|
|
338
|
+
last_used_at TEXT NOT NULL
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
CREATE INDEX IF NOT EXISTS idx_credentials_user ON credentials(user_id);
|
|
342
|
+
|
|
343
|
+
-- Auth tokens (magic links, email verification, invites)
|
|
344
|
+
CREATE TABLE IF NOT EXISTS auth_tokens (
|
|
345
|
+
hash TEXT PRIMARY KEY,
|
|
346
|
+
user_id TEXT REFERENCES users(id) ON DELETE CASCADE,
|
|
347
|
+
email TEXT,
|
|
348
|
+
type TEXT NOT NULL,
|
|
349
|
+
role INTEGER,
|
|
350
|
+
invited_by TEXT REFERENCES users(id),
|
|
351
|
+
expires_at TEXT NOT NULL,
|
|
352
|
+
created_at TEXT NOT NULL
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
CREATE INDEX IF NOT EXISTS idx_auth_tokens_email ON auth_tokens(email);
|
|
356
|
+
|
|
357
|
+
-- OAuth accounts (external provider links)
|
|
358
|
+
CREATE TABLE IF NOT EXISTS oauth_accounts (
|
|
359
|
+
provider TEXT NOT NULL,
|
|
360
|
+
provider_account_id TEXT NOT NULL,
|
|
361
|
+
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
362
|
+
created_at TEXT NOT NULL,
|
|
363
|
+
PRIMARY KEY (provider, provider_account_id)
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
CREATE INDEX IF NOT EXISTS idx_oauth_accounts_user ON oauth_accounts(user_id);
|
|
367
|
+
|
|
368
|
+
-- Allowed domains for self-signup
|
|
369
|
+
CREATE TABLE IF NOT EXISTS allowed_domains (
|
|
370
|
+
domain TEXT PRIMARY KEY,
|
|
371
|
+
default_role INTEGER NOT NULL DEFAULT 20,
|
|
372
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
373
|
+
created_at TEXT NOT NULL
|
|
374
|
+
);
|
|
375
|
+
`;
|
|
376
|
+
|
|
377
|
+
//#endregion
|
|
378
|
+
export { AUTH_TABLES_SQL, createKyselyAdapter };
|
|
379
|
+
//# sourceMappingURL=kysely.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"kysely.mjs","names":[],"sources":["../../src/adapters/kysely.ts"],"sourcesContent":["/**\n * Kysely database adapter for @emdashcms/auth\n */\n\nimport type { Kysely, Insertable, Selectable, Updateable } from \"kysely\";\nimport { ulid } from \"ulidx\";\n\nimport {\n\tRole,\n\ttoRoleLevel,\n\ttoDeviceType,\n\ttoTokenType,\n\ttype AuthAdapter,\n\ttype User,\n\ttype NewUser,\n\ttype UpdateUser,\n\ttype Credential,\n\ttype NewCredential,\n\ttype AuthToken,\n\ttype NewAuthToken,\n\ttype TokenType,\n\ttype OAuthAccount,\n\ttype NewOAuthAccount,\n\ttype AllowedDomain,\n\ttype RoleLevel,\n} from \"../types.js\";\n\n// ============================================================================\n// Database schema types\n// ============================================================================\n\nexport interface AuthTables {\n\tusers: UserTable;\n\tcredentials: CredentialTable;\n\tauth_tokens: AuthTokenTable;\n\toauth_accounts: OAuthAccountTable;\n\tallowed_domains: AllowedDomainTable;\n}\n\ninterface UserTable {\n\tid: string;\n\temail: string;\n\tname: string | null;\n\tavatar_url: string | null;\n\trole: number;\n\temail_verified: number;\n\tdisabled: number;\n\tdata: string | null;\n\tcreated_at: string;\n\tupdated_at: string;\n}\n\ninterface CredentialTable {\n\tid: string;\n\tuser_id: string;\n\tpublic_key: Uint8Array;\n\tcounter: number;\n\tdevice_type: string;\n\tbacked_up: number;\n\ttransports: string | null;\n\tname: string | null;\n\tcreated_at: string;\n\tlast_used_at: string;\n}\n\ninterface AuthTokenTable {\n\thash: string;\n\tuser_id: string | null;\n\temail: string | null;\n\ttype: string;\n\trole: number | null;\n\tinvited_by: string | null;\n\texpires_at: string;\n\tcreated_at: string;\n}\n\ninterface OAuthAccountTable {\n\tprovider: string;\n\tprovider_account_id: string;\n\tuser_id: string;\n\tcreated_at: string;\n}\n\ninterface AllowedDomainTable {\n\tdomain: string;\n\tdefault_role: number;\n\tenabled: number;\n\tcreated_at: string;\n}\n\n// ============================================================================\n// Adapter implementation\n// ============================================================================\n\nexport function createKyselyAdapter<T extends AuthTables>(db: Kysely<T>): AuthAdapter {\n\t// Type cast to work with generic Kysely instance\n\t// eslint-disable-next-line typescript-eslint(no-unsafe-type-assertion) -- generic Kysely<T extends AuthTables> narrowed to concrete AuthTables for internal queries\n\tconst kdb = db as unknown as Kysely<AuthTables>;\n\n\treturn {\n\t\t// ========================================================================\n\t\t// Users\n\t\t// ========================================================================\n\n\t\tasync getUserById(id: string): Promise<User | null> {\n\t\t\tconst row = await kdb.selectFrom(\"users\").selectAll().where(\"id\", \"=\", id).executeTakeFirst();\n\n\t\t\treturn row ? rowToUser(row) : null;\n\t\t},\n\n\t\tasync getUserByEmail(email: string): Promise<User | null> {\n\t\t\tconst row = await kdb\n\t\t\t\t.selectFrom(\"users\")\n\t\t\t\t.selectAll()\n\t\t\t\t.where(\"email\", \"=\", email.toLowerCase())\n\t\t\t\t.executeTakeFirst();\n\n\t\t\treturn row ? rowToUser(row) : null;\n\t\t},\n\n\t\tasync createUser(user: NewUser): Promise<User> {\n\t\t\tconst now = new Date().toISOString();\n\t\t\tconst id = ulid();\n\n\t\t\tconst row: Insertable<UserTable> = {\n\t\t\t\tid,\n\t\t\t\temail: user.email.toLowerCase(),\n\t\t\t\tname: user.name ?? null,\n\t\t\t\tavatar_url: user.avatarUrl ?? null,\n\t\t\t\trole: user.role ?? Role.SUBSCRIBER,\n\t\t\t\temail_verified: user.emailVerified ? 1 : 0,\n\t\t\t\tdisabled: 0,\n\t\t\t\tdata: user.data ? JSON.stringify(user.data) : null,\n\t\t\t\tcreated_at: now,\n\t\t\t\tupdated_at: now,\n\t\t\t};\n\n\t\t\tawait kdb.insertInto(\"users\").values(row).execute();\n\n\t\t\treturn {\n\t\t\t\tid,\n\t\t\t\temail: row.email,\n\t\t\t\tname: user.name ?? null,\n\t\t\t\tavatarUrl: user.avatarUrl ?? null,\n\t\t\t\trole: toRoleLevel(row.role),\n\t\t\t\temailVerified: row.email_verified === 1,\n\t\t\t\tdisabled: false,\n\t\t\t\tdata: user.data ?? null,\n\t\t\t\tcreatedAt: new Date(now),\n\t\t\t\tupdatedAt: new Date(now),\n\t\t\t};\n\t\t},\n\n\t\tasync updateUser(id: string, data: UpdateUser): Promise<void> {\n\t\t\tconst update: Updateable<UserTable> = {\n\t\t\t\tupdated_at: new Date().toISOString(),\n\t\t\t};\n\n\t\t\tif (data.email !== undefined) update.email = data.email.toLowerCase();\n\t\t\tif (data.name !== undefined) update.name = data.name;\n\t\t\tif (data.avatarUrl !== undefined) update.avatar_url = data.avatarUrl;\n\t\t\tif (data.role !== undefined) update.role = data.role;\n\t\t\tif (data.emailVerified !== undefined) update.email_verified = data.emailVerified ? 1 : 0;\n\t\t\tif (data.disabled !== undefined) update.disabled = data.disabled ? 1 : 0;\n\t\t\tif (data.data !== undefined) update.data = data.data ? JSON.stringify(data.data) : null;\n\n\t\t\tawait kdb.updateTable(\"users\").set(update).where(\"id\", \"=\", id).execute();\n\t\t},\n\n\t\tasync deleteUser(id: string): Promise<void> {\n\t\t\tawait kdb.deleteFrom(\"users\").where(\"id\", \"=\", id).execute();\n\t\t},\n\n\t\tasync countUsers(): Promise<number> {\n\t\t\tconst result = await kdb\n\t\t\t\t.selectFrom(\"users\")\n\t\t\t\t.select((eb) => eb.fn.countAll<number>().as(\"count\"))\n\t\t\t\t.executeTakeFirstOrThrow();\n\n\t\t\treturn result.count;\n\t\t},\n\n\t\tasync getUsers(options?: {\n\t\t\tsearch?: string;\n\t\t\trole?: number;\n\t\t\tcursor?: string;\n\t\t\tlimit?: number;\n\t\t}): Promise<{\n\t\t\titems: Array<\n\t\t\t\tUser & {\n\t\t\t\t\tlastLogin: Date | null;\n\t\t\t\t\tcredentialCount: number;\n\t\t\t\t\toauthProviders: string[];\n\t\t\t\t}\n\t\t\t>;\n\t\t\tnextCursor?: string;\n\t\t}> {\n\t\t\tconst limit = Math.min(options?.limit ?? 20, 100);\n\n\t\t\tlet query = kdb\n\t\t\t\t.selectFrom(\"users\")\n\t\t\t\t.leftJoin(\"credentials\", \"users.id\", \"credentials.user_id\")\n\t\t\t\t.selectAll(\"users\")\n\t\t\t\t.select((eb) => [\n\t\t\t\t\teb.fn.count<number>(\"credentials.id\").as(\"credential_count\"),\n\t\t\t\t\teb.fn.max(\"credentials.last_used_at\").as(\"last_login\"),\n\t\t\t\t])\n\t\t\t\t.groupBy(\"users.id\")\n\t\t\t\t.orderBy(\"users.created_at\", \"desc\")\n\t\t\t\t.limit(limit + 1);\n\n\t\t\t// Apply filters\n\t\t\tif (options?.search) {\n\t\t\t\tconst searchPattern = `%${options.search}%`;\n\t\t\t\tquery = query.where((eb) =>\n\t\t\t\t\teb.or([\n\t\t\t\t\t\teb(\"users.email\", \"like\", searchPattern),\n\t\t\t\t\t\teb(\"users.name\", \"like\", searchPattern),\n\t\t\t\t\t]),\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tif (options?.role !== undefined) {\n\t\t\t\tquery = query.where(\"users.role\", \"=\", options.role);\n\t\t\t}\n\n\t\t\tif (options?.cursor) {\n\t\t\t\t// Get the cursor user's created_at for pagination\n\t\t\t\tconst cursorUser = await kdb\n\t\t\t\t\t.selectFrom(\"users\")\n\t\t\t\t\t.select(\"created_at\")\n\t\t\t\t\t.where(\"id\", \"=\", options.cursor)\n\t\t\t\t\t.executeTakeFirst();\n\n\t\t\t\tif (cursorUser) {\n\t\t\t\t\tquery = query.where(\"users.created_at\", \"<\", cursorUser.created_at);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst rows = await query.execute();\n\n\t\t\t// Get OAuth providers for all users in this batch\n\t\t\tconst userIds = rows.slice(0, limit).map((r) => r.id);\n\t\t\tconst oauthAccounts =\n\t\t\t\tuserIds.length > 0\n\t\t\t\t\t? await kdb\n\t\t\t\t\t\t\t.selectFrom(\"oauth_accounts\")\n\t\t\t\t\t\t\t.select([\"user_id\", \"provider\"])\n\t\t\t\t\t\t\t.where(\"user_id\", \"in\", userIds)\n\t\t\t\t\t\t\t.execute()\n\t\t\t\t\t: [];\n\n\t\t\t// Group OAuth providers by user\n\t\t\tconst oauthByUser = new Map<string, string[]>();\n\t\t\tfor (const account of oauthAccounts) {\n\t\t\t\tconst providers = oauthByUser.get(account.user_id) ?? [];\n\t\t\t\tproviders.push(account.provider);\n\t\t\t\toauthByUser.set(account.user_id, providers);\n\t\t\t}\n\n\t\t\tconst hasMore = rows.length > limit;\n\t\t\tconst items = rows.slice(0, limit).map((row) => ({\n\t\t\t\tid: row.id,\n\t\t\t\temail: row.email,\n\t\t\t\tname: row.name,\n\t\t\t\tavatarUrl: row.avatar_url,\n\t\t\t\trole: toRoleLevel(row.role),\n\t\t\t\temailVerified: row.email_verified === 1,\n\t\t\t\tdisabled: row.disabled === 1,\n\t\t\t\tdata: row.data ? JSON.parse(row.data) : null,\n\t\t\t\tcreatedAt: new Date(row.created_at),\n\t\t\t\tupdatedAt: new Date(row.updated_at),\n\t\t\t\tlastLogin: row.last_login ? new Date(row.last_login) : null,\n\t\t\t\tcredentialCount: row.credential_count ?? 0,\n\t\t\t\toauthProviders: oauthByUser.get(row.id) ?? [],\n\t\t\t}));\n\n\t\t\treturn {\n\t\t\t\titems,\n\t\t\t\tnextCursor: hasMore ? items.at(-1)?.id : undefined,\n\t\t\t};\n\t\t},\n\n\t\tasync getUserWithDetails(id: string): Promise<{\n\t\t\tuser: User;\n\t\t\tcredentials: Credential[];\n\t\t\toauthAccounts: OAuthAccount[];\n\t\t\tlastLogin: Date | null;\n\t\t} | null> {\n\t\t\tconst user = await kdb\n\t\t\t\t.selectFrom(\"users\")\n\t\t\t\t.selectAll()\n\t\t\t\t.where(\"id\", \"=\", id)\n\t\t\t\t.executeTakeFirst();\n\n\t\t\tif (!user) return null;\n\n\t\t\tconst [credentials, oauthAccounts] = await Promise.all([\n\t\t\t\tkdb\n\t\t\t\t\t.selectFrom(\"credentials\")\n\t\t\t\t\t.selectAll()\n\t\t\t\t\t.where(\"user_id\", \"=\", id)\n\t\t\t\t\t.orderBy(\"created_at\", \"desc\")\n\t\t\t\t\t.execute(),\n\t\t\t\tkdb.selectFrom(\"oauth_accounts\").selectAll().where(\"user_id\", \"=\", id).execute(),\n\t\t\t]);\n\n\t\t\t// Find last login from most recent credential use\n\t\t\tconst lastLogin = credentials.reduce<Date | null>((latest, cred) => {\n\t\t\t\tconst lastUsed = new Date(cred.last_used_at);\n\t\t\t\treturn !latest || lastUsed > latest ? lastUsed : latest;\n\t\t\t}, null);\n\n\t\t\treturn {\n\t\t\t\tuser: rowToUser(user),\n\t\t\t\tcredentials: credentials.map(rowToCredential),\n\t\t\t\toauthAccounts: oauthAccounts.map(rowToOAuthAccount),\n\t\t\t\tlastLogin,\n\t\t\t};\n\t\t},\n\n\t\tasync countAdmins(): Promise<number> {\n\t\t\tconst result = await kdb\n\t\t\t\t.selectFrom(\"users\")\n\t\t\t\t.select((eb) => eb.fn.countAll<number>().as(\"count\"))\n\t\t\t\t.where(\"role\", \"=\", Role.ADMIN)\n\t\t\t\t.where(\"disabled\", \"=\", 0)\n\t\t\t\t.executeTakeFirstOrThrow();\n\n\t\t\treturn result.count;\n\t\t},\n\n\t\t// ========================================================================\n\t\t// Credentials\n\t\t// ========================================================================\n\n\t\tasync getCredentialById(id: string): Promise<Credential | null> {\n\t\t\tconst row = await kdb\n\t\t\t\t.selectFrom(\"credentials\")\n\t\t\t\t.selectAll()\n\t\t\t\t.where(\"id\", \"=\", id)\n\t\t\t\t.executeTakeFirst();\n\n\t\t\treturn row ? rowToCredential(row) : null;\n\t\t},\n\n\t\tasync getCredentialsByUserId(userId: string): Promise<Credential[]> {\n\t\t\tconst rows = await kdb\n\t\t\t\t.selectFrom(\"credentials\")\n\t\t\t\t.selectAll()\n\t\t\t\t.where(\"user_id\", \"=\", userId)\n\t\t\t\t.execute();\n\n\t\t\treturn rows.map(rowToCredential);\n\t\t},\n\n\t\tasync createCredential(credential: NewCredential): Promise<Credential> {\n\t\t\tconst now = new Date().toISOString();\n\n\t\t\tconst row: Insertable<CredentialTable> = {\n\t\t\t\tid: credential.id,\n\t\t\t\tuser_id: credential.userId,\n\t\t\t\tpublic_key: credential.publicKey,\n\t\t\t\tcounter: credential.counter,\n\t\t\t\tdevice_type: credential.deviceType,\n\t\t\t\tbacked_up: credential.backedUp ? 1 : 0,\n\t\t\t\ttransports: credential.transports.length > 0 ? JSON.stringify(credential.transports) : null,\n\t\t\t\tname: credential.name ?? null,\n\t\t\t\tcreated_at: now,\n\t\t\t\tlast_used_at: now,\n\t\t\t};\n\n\t\t\tawait kdb.insertInto(\"credentials\").values(row).execute();\n\n\t\t\treturn {\n\t\t\t\tid: credential.id,\n\t\t\t\tuserId: credential.userId,\n\t\t\t\tpublicKey: credential.publicKey,\n\t\t\t\tcounter: credential.counter,\n\t\t\t\tdeviceType: credential.deviceType,\n\t\t\t\tbackedUp: credential.backedUp,\n\t\t\t\ttransports: credential.transports,\n\t\t\t\tname: credential.name ?? null,\n\t\t\t\tcreatedAt: new Date(now),\n\t\t\t\tlastUsedAt: new Date(now),\n\t\t\t};\n\t\t},\n\n\t\tasync updateCredentialCounter(id: string, counter: number): Promise<void> {\n\t\t\tawait kdb\n\t\t\t\t.updateTable(\"credentials\")\n\t\t\t\t.set({\n\t\t\t\t\tcounter,\n\t\t\t\t\tlast_used_at: new Date().toISOString(),\n\t\t\t\t})\n\t\t\t\t.where(\"id\", \"=\", id)\n\t\t\t\t.execute();\n\t\t},\n\n\t\tasync updateCredentialName(id: string, name: string | null): Promise<void> {\n\t\t\tawait kdb.updateTable(\"credentials\").set({ name }).where(\"id\", \"=\", id).execute();\n\t\t},\n\n\t\tasync deleteCredential(id: string): Promise<void> {\n\t\t\tawait kdb.deleteFrom(\"credentials\").where(\"id\", \"=\", id).execute();\n\t\t},\n\n\t\tasync countCredentialsByUserId(userId: string): Promise<number> {\n\t\t\tconst result = await kdb\n\t\t\t\t.selectFrom(\"credentials\")\n\t\t\t\t.select((eb) => eb.fn.countAll<number>().as(\"count\"))\n\t\t\t\t.where(\"user_id\", \"=\", userId)\n\t\t\t\t.executeTakeFirstOrThrow();\n\n\t\t\treturn result.count;\n\t\t},\n\n\t\t// ========================================================================\n\t\t// Auth Tokens\n\t\t// ========================================================================\n\n\t\tasync createToken(token: NewAuthToken): Promise<void> {\n\t\t\tconst row: Insertable<AuthTokenTable> = {\n\t\t\t\thash: token.hash,\n\t\t\t\tuser_id: token.userId ?? null,\n\t\t\t\temail: token.email ?? null,\n\t\t\t\ttype: token.type,\n\t\t\t\trole: token.role ?? null,\n\t\t\t\tinvited_by: token.invitedBy ?? null,\n\t\t\t\texpires_at: token.expiresAt.toISOString(),\n\t\t\t\tcreated_at: new Date().toISOString(),\n\t\t\t};\n\n\t\t\tawait kdb.insertInto(\"auth_tokens\").values(row).execute();\n\t\t},\n\n\t\tasync getToken(hash: string, type: TokenType): Promise<AuthToken | null> {\n\t\t\tconst row = await kdb\n\t\t\t\t.selectFrom(\"auth_tokens\")\n\t\t\t\t.selectAll()\n\t\t\t\t.where(\"hash\", \"=\", hash)\n\t\t\t\t.where(\"type\", \"=\", type)\n\t\t\t\t.executeTakeFirst();\n\n\t\t\treturn row ? rowToAuthToken(row) : null;\n\t\t},\n\n\t\tasync deleteToken(hash: string): Promise<void> {\n\t\t\tawait kdb.deleteFrom(\"auth_tokens\").where(\"hash\", \"=\", hash).execute();\n\t\t},\n\n\t\tasync deleteExpiredTokens(): Promise<void> {\n\t\t\tawait kdb\n\t\t\t\t.deleteFrom(\"auth_tokens\")\n\t\t\t\t.where(\"expires_at\", \"<\", new Date().toISOString())\n\t\t\t\t.execute();\n\t\t},\n\n\t\t// ========================================================================\n\t\t// OAuth Accounts\n\t\t// ========================================================================\n\n\t\tasync getOAuthAccount(\n\t\t\tprovider: string,\n\t\t\tproviderAccountId: string,\n\t\t): Promise<OAuthAccount | null> {\n\t\t\tconst row = await kdb\n\t\t\t\t.selectFrom(\"oauth_accounts\")\n\t\t\t\t.selectAll()\n\t\t\t\t.where(\"provider\", \"=\", provider)\n\t\t\t\t.where(\"provider_account_id\", \"=\", providerAccountId)\n\t\t\t\t.executeTakeFirst();\n\n\t\t\treturn row ? rowToOAuthAccount(row) : null;\n\t\t},\n\n\t\tasync getOAuthAccountsByUserId(userId: string): Promise<OAuthAccount[]> {\n\t\t\tconst rows = await kdb\n\t\t\t\t.selectFrom(\"oauth_accounts\")\n\t\t\t\t.selectAll()\n\t\t\t\t.where(\"user_id\", \"=\", userId)\n\t\t\t\t.execute();\n\n\t\t\treturn rows.map(rowToOAuthAccount);\n\t\t},\n\n\t\tasync createOAuthAccount(account: NewOAuthAccount): Promise<OAuthAccount> {\n\t\t\tconst now = new Date().toISOString();\n\n\t\t\tconst row: Insertable<OAuthAccountTable> = {\n\t\t\t\tprovider: account.provider,\n\t\t\t\tprovider_account_id: account.providerAccountId,\n\t\t\t\tuser_id: account.userId,\n\t\t\t\tcreated_at: now,\n\t\t\t};\n\n\t\t\tawait kdb.insertInto(\"oauth_accounts\").values(row).execute();\n\n\t\t\treturn {\n\t\t\t\tprovider: account.provider,\n\t\t\t\tproviderAccountId: account.providerAccountId,\n\t\t\t\tuserId: account.userId,\n\t\t\t\tcreatedAt: new Date(now),\n\t\t\t};\n\t\t},\n\n\t\tasync deleteOAuthAccount(provider: string, providerAccountId: string): Promise<void> {\n\t\t\tawait kdb\n\t\t\t\t.deleteFrom(\"oauth_accounts\")\n\t\t\t\t.where(\"provider\", \"=\", provider)\n\t\t\t\t.where(\"provider_account_id\", \"=\", providerAccountId)\n\t\t\t\t.execute();\n\t\t},\n\n\t\t// ========================================================================\n\t\t// Allowed Domains\n\t\t// ========================================================================\n\n\t\tasync getAllowedDomain(domain: string): Promise<AllowedDomain | null> {\n\t\t\tconst row = await kdb\n\t\t\t\t.selectFrom(\"allowed_domains\")\n\t\t\t\t.selectAll()\n\t\t\t\t.where(\"domain\", \"=\", domain.toLowerCase())\n\t\t\t\t.executeTakeFirst();\n\n\t\t\treturn row ? rowToAllowedDomain(row) : null;\n\t\t},\n\n\t\tasync getAllowedDomains(): Promise<AllowedDomain[]> {\n\t\t\tconst rows = await kdb.selectFrom(\"allowed_domains\").selectAll().execute();\n\n\t\t\treturn rows.map(rowToAllowedDomain);\n\t\t},\n\n\t\tasync createAllowedDomain(domain: string, defaultRole: RoleLevel): Promise<AllowedDomain> {\n\t\t\tconst now = new Date().toISOString();\n\n\t\t\tconst row: Insertable<AllowedDomainTable> = {\n\t\t\t\tdomain: domain.toLowerCase(),\n\t\t\t\tdefault_role: defaultRole,\n\t\t\t\tenabled: 1,\n\t\t\t\tcreated_at: now,\n\t\t\t};\n\n\t\t\tawait kdb.insertInto(\"allowed_domains\").values(row).execute();\n\n\t\t\treturn {\n\t\t\t\tdomain: row.domain,\n\t\t\t\tdefaultRole,\n\t\t\t\tenabled: true,\n\t\t\t\tcreatedAt: new Date(now),\n\t\t\t};\n\t\t},\n\n\t\tasync updateAllowedDomain(\n\t\t\tdomain: string,\n\t\t\tenabled: boolean,\n\t\t\tdefaultRole?: RoleLevel,\n\t\t): Promise<void> {\n\t\t\tconst update: Updateable<AllowedDomainTable> = {\n\t\t\t\tenabled: enabled ? 1 : 0,\n\t\t\t};\n\n\t\t\tif (defaultRole !== undefined) {\n\t\t\t\tupdate.default_role = defaultRole;\n\t\t\t}\n\n\t\t\tawait kdb\n\t\t\t\t.updateTable(\"allowed_domains\")\n\t\t\t\t.set(update)\n\t\t\t\t.where(\"domain\", \"=\", domain.toLowerCase())\n\t\t\t\t.execute();\n\t\t},\n\n\t\tasync deleteAllowedDomain(domain: string): Promise<void> {\n\t\t\tawait kdb.deleteFrom(\"allowed_domains\").where(\"domain\", \"=\", domain.toLowerCase()).execute();\n\t\t},\n\t};\n}\n\n// ============================================================================\n// Row converters\n// ============================================================================\n\nfunction rowToUser(row: Selectable<UserTable>): User {\n\treturn {\n\t\tid: row.id,\n\t\temail: row.email,\n\t\tname: row.name,\n\t\tavatarUrl: row.avatar_url,\n\t\trole: toRoleLevel(row.role),\n\t\temailVerified: row.email_verified === 1,\n\t\tdisabled: row.disabled === 1,\n\t\tdata: row.data ? JSON.parse(row.data) : null,\n\t\tcreatedAt: new Date(row.created_at),\n\t\tupdatedAt: new Date(row.updated_at),\n\t};\n}\n\nfunction rowToCredential(row: Selectable<CredentialTable>): Credential {\n\treturn {\n\t\tid: row.id,\n\t\tuserId: row.user_id,\n\t\tpublicKey: row.public_key,\n\t\tcounter: row.counter,\n\t\tdeviceType: toDeviceType(row.device_type),\n\t\tbackedUp: row.backed_up === 1,\n\t\ttransports: row.transports ? JSON.parse(row.transports) : [],\n\t\tname: row.name,\n\t\tcreatedAt: new Date(row.created_at),\n\t\tlastUsedAt: new Date(row.last_used_at),\n\t};\n}\n\nfunction rowToAuthToken(row: Selectable<AuthTokenTable>): AuthToken {\n\treturn {\n\t\thash: row.hash,\n\t\tuserId: row.user_id,\n\t\temail: row.email,\n\t\ttype: toTokenType(row.type),\n\t\trole: row.role != null ? toRoleLevel(row.role) : null,\n\t\tinvitedBy: row.invited_by,\n\t\texpiresAt: new Date(row.expires_at),\n\t\tcreatedAt: new Date(row.created_at),\n\t};\n}\n\nfunction rowToOAuthAccount(row: Selectable<OAuthAccountTable>): OAuthAccount {\n\treturn {\n\t\tprovider: row.provider,\n\t\tproviderAccountId: row.provider_account_id,\n\t\tuserId: row.user_id,\n\t\tcreatedAt: new Date(row.created_at),\n\t};\n}\n\nfunction rowToAllowedDomain(row: Selectable<AllowedDomainTable>): AllowedDomain {\n\treturn {\n\t\tdomain: row.domain,\n\t\tdefaultRole: toRoleLevel(row.default_role),\n\t\tenabled: row.enabled === 1,\n\t\tcreatedAt: new Date(row.created_at),\n\t};\n}\n\n// ============================================================================\n// Migration SQL\n// ============================================================================\n\nexport const AUTH_TABLES_SQL = `\n-- Users (no password_hash)\nCREATE TABLE IF NOT EXISTS users (\n id TEXT PRIMARY KEY,\n email TEXT UNIQUE NOT NULL,\n name TEXT,\n avatar_url TEXT,\n role INTEGER NOT NULL DEFAULT 10,\n email_verified INTEGER NOT NULL DEFAULT 0,\n disabled INTEGER NOT NULL DEFAULT 0,\n data TEXT,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\n\nCREATE INDEX IF NOT EXISTS idx_users_email ON users(email);\n\n-- Passkey credentials\nCREATE TABLE IF NOT EXISTS credentials (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n public_key BLOB NOT NULL,\n counter INTEGER NOT NULL DEFAULT 0,\n device_type TEXT NOT NULL,\n backed_up INTEGER NOT NULL DEFAULT 0,\n transports TEXT,\n name TEXT,\n created_at TEXT NOT NULL,\n last_used_at TEXT NOT NULL\n);\n\nCREATE INDEX IF NOT EXISTS idx_credentials_user ON credentials(user_id);\n\n-- Auth tokens (magic links, email verification, invites)\nCREATE TABLE IF NOT EXISTS auth_tokens (\n hash TEXT PRIMARY KEY,\n user_id TEXT REFERENCES users(id) ON DELETE CASCADE,\n email TEXT,\n type TEXT NOT NULL,\n role INTEGER,\n invited_by TEXT REFERENCES users(id),\n expires_at TEXT NOT NULL,\n created_at TEXT NOT NULL\n);\n\nCREATE INDEX IF NOT EXISTS idx_auth_tokens_email ON auth_tokens(email);\n\n-- OAuth accounts (external provider links)\nCREATE TABLE IF NOT EXISTS oauth_accounts (\n provider TEXT NOT NULL,\n provider_account_id TEXT NOT NULL,\n user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,\n created_at TEXT NOT NULL,\n PRIMARY KEY (provider, provider_account_id)\n);\n\nCREATE INDEX IF NOT EXISTS idx_oauth_accounts_user ON oauth_accounts(user_id);\n\n-- Allowed domains for self-signup\nCREATE TABLE IF NOT EXISTS allowed_domains (\n domain TEXT PRIMARY KEY,\n default_role INTEGER NOT NULL DEFAULT 20,\n enabled INTEGER NOT NULL DEFAULT 1,\n created_at TEXT NOT NULL\n);\n`;\n"],"mappings":";;;;AA8FA,SAAgB,oBAA0C,IAA4B;CAGrF,MAAM,MAAM;AAEZ,QAAO;EAKN,MAAM,YAAY,IAAkC;GACnD,MAAM,MAAM,MAAM,IAAI,WAAW,QAAQ,CAAC,WAAW,CAAC,MAAM,MAAM,KAAK,GAAG,CAAC,kBAAkB;AAE7F,UAAO,MAAM,UAAU,IAAI,GAAG;;EAG/B,MAAM,eAAe,OAAqC;GACzD,MAAM,MAAM,MAAM,IAChB,WAAW,QAAQ,CACnB,WAAW,CACX,MAAM,SAAS,KAAK,MAAM,aAAa,CAAC,CACxC,kBAAkB;AAEpB,UAAO,MAAM,UAAU,IAAI,GAAG;;EAG/B,MAAM,WAAW,MAA8B;GAC9C,MAAM,uBAAM,IAAI,MAAM,EAAC,aAAa;GACpC,MAAM,KAAK,MAAM;GAEjB,MAAM,MAA6B;IAClC;IACA,OAAO,KAAK,MAAM,aAAa;IAC/B,MAAM,KAAK,QAAQ;IACnB,YAAY,KAAK,aAAa;IAC9B,MAAM,KAAK,QAAQ,KAAK;IACxB,gBAAgB,KAAK,gBAAgB,IAAI;IACzC,UAAU;IACV,MAAM,KAAK,OAAO,KAAK,UAAU,KAAK,KAAK,GAAG;IAC9C,YAAY;IACZ,YAAY;IACZ;AAED,SAAM,IAAI,WAAW,QAAQ,CAAC,OAAO,IAAI,CAAC,SAAS;AAEnD,UAAO;IACN;IACA,OAAO,IAAI;IACX,MAAM,KAAK,QAAQ;IACnB,WAAW,KAAK,aAAa;IAC7B,MAAM,YAAY,IAAI,KAAK;IAC3B,eAAe,IAAI,mBAAmB;IACtC,UAAU;IACV,MAAM,KAAK,QAAQ;IACnB,WAAW,IAAI,KAAK,IAAI;IACxB,WAAW,IAAI,KAAK,IAAI;IACxB;;EAGF,MAAM,WAAW,IAAY,MAAiC;GAC7D,MAAM,SAAgC,EACrC,6BAAY,IAAI,MAAM,EAAC,aAAa,EACpC;AAED,OAAI,KAAK,UAAU,OAAW,QAAO,QAAQ,KAAK,MAAM,aAAa;AACrE,OAAI,KAAK,SAAS,OAAW,QAAO,OAAO,KAAK;AAChD,OAAI,KAAK,cAAc,OAAW,QAAO,aAAa,KAAK;AAC3D,OAAI,KAAK,SAAS,OAAW,QAAO,OAAO,KAAK;AAChD,OAAI,KAAK,kBAAkB,OAAW,QAAO,iBAAiB,KAAK,gBAAgB,IAAI;AACvF,OAAI,KAAK,aAAa,OAAW,QAAO,WAAW,KAAK,WAAW,IAAI;AACvE,OAAI,KAAK,SAAS,OAAW,QAAO,OAAO,KAAK,OAAO,KAAK,UAAU,KAAK,KAAK,GAAG;AAEnF,SAAM,IAAI,YAAY,QAAQ,CAAC,IAAI,OAAO,CAAC,MAAM,MAAM,KAAK,GAAG,CAAC,SAAS;;EAG1E,MAAM,WAAW,IAA2B;AAC3C,SAAM,IAAI,WAAW,QAAQ,CAAC,MAAM,MAAM,KAAK,GAAG,CAAC,SAAS;;EAG7D,MAAM,aAA8B;AAMnC,WALe,MAAM,IACnB,WAAW,QAAQ,CACnB,QAAQ,OAAO,GAAG,GAAG,UAAkB,CAAC,GAAG,QAAQ,CAAC,CACpD,yBAAyB,EAEb;;EAGf,MAAM,SAAS,SAcZ;GACF,MAAM,QAAQ,KAAK,IAAI,SAAS,SAAS,IAAI,IAAI;GAEjD,IAAI,QAAQ,IACV,WAAW,QAAQ,CACnB,SAAS,eAAe,YAAY,sBAAsB,CAC1D,UAAU,QAAQ,CAClB,QAAQ,OAAO,CACf,GAAG,GAAG,MAAc,iBAAiB,CAAC,GAAG,mBAAmB,EAC5D,GAAG,GAAG,IAAI,2BAA2B,CAAC,GAAG,aAAa,CACtD,CAAC,CACD,QAAQ,WAAW,CACnB,QAAQ,oBAAoB,OAAO,CACnC,MAAM,QAAQ,EAAE;AAGlB,OAAI,SAAS,QAAQ;IACpB,MAAM,gBAAgB,IAAI,QAAQ,OAAO;AACzC,YAAQ,MAAM,OAAO,OACpB,GAAG,GAAG,CACL,GAAG,eAAe,QAAQ,cAAc,EACxC,GAAG,cAAc,QAAQ,cAAc,CACvC,CAAC,CACF;;AAGF,OAAI,SAAS,SAAS,OACrB,SAAQ,MAAM,MAAM,cAAc,KAAK,QAAQ,KAAK;AAGrD,OAAI,SAAS,QAAQ;IAEpB,MAAM,aAAa,MAAM,IACvB,WAAW,QAAQ,CACnB,OAAO,aAAa,CACpB,MAAM,MAAM,KAAK,QAAQ,OAAO,CAChC,kBAAkB;AAEpB,QAAI,WACH,SAAQ,MAAM,MAAM,oBAAoB,KAAK,WAAW,WAAW;;GAIrE,MAAM,OAAO,MAAM,MAAM,SAAS;GAGlC,MAAM,UAAU,KAAK,MAAM,GAAG,MAAM,CAAC,KAAK,MAAM,EAAE,GAAG;GACrD,MAAM,gBACL,QAAQ,SAAS,IACd,MAAM,IACL,WAAW,iBAAiB,CAC5B,OAAO,CAAC,WAAW,WAAW,CAAC,CAC/B,MAAM,WAAW,MAAM,QAAQ,CAC/B,SAAS,GACV,EAAE;GAGN,MAAM,8BAAc,IAAI,KAAuB;AAC/C,QAAK,MAAM,WAAW,eAAe;IACpC,MAAM,YAAY,YAAY,IAAI,QAAQ,QAAQ,IAAI,EAAE;AACxD,cAAU,KAAK,QAAQ,SAAS;AAChC,gBAAY,IAAI,QAAQ,SAAS,UAAU;;GAG5C,MAAM,UAAU,KAAK,SAAS;GAC9B,MAAM,QAAQ,KAAK,MAAM,GAAG,MAAM,CAAC,KAAK,SAAS;IAChD,IAAI,IAAI;IACR,OAAO,IAAI;IACX,MAAM,IAAI;IACV,WAAW,IAAI;IACf,MAAM,YAAY,IAAI,KAAK;IAC3B,eAAe,IAAI,mBAAmB;IACtC,UAAU,IAAI,aAAa;IAC3B,MAAM,IAAI,OAAO,KAAK,MAAM,IAAI,KAAK,GAAG;IACxC,WAAW,IAAI,KAAK,IAAI,WAAW;IACnC,WAAW,IAAI,KAAK,IAAI,WAAW;IACnC,WAAW,IAAI,aAAa,IAAI,KAAK,IAAI,WAAW,GAAG;IACvD,iBAAiB,IAAI,oBAAoB;IACzC,gBAAgB,YAAY,IAAI,IAAI,GAAG,IAAI,EAAE;IAC7C,EAAE;AAEH,UAAO;IACN;IACA,YAAY,UAAU,MAAM,GAAG,GAAG,EAAE,KAAK;IACzC;;EAGF,MAAM,mBAAmB,IAKf;GACT,MAAM,OAAO,MAAM,IACjB,WAAW,QAAQ,CACnB,WAAW,CACX,MAAM,MAAM,KAAK,GAAG,CACpB,kBAAkB;AAEpB,OAAI,CAAC,KAAM,QAAO;GAElB,MAAM,CAAC,aAAa,iBAAiB,MAAM,QAAQ,IAAI,CACtD,IACE,WAAW,cAAc,CACzB,WAAW,CACX,MAAM,WAAW,KAAK,GAAG,CACzB,QAAQ,cAAc,OAAO,CAC7B,SAAS,EACX,IAAI,WAAW,iBAAiB,CAAC,WAAW,CAAC,MAAM,WAAW,KAAK,GAAG,CAAC,SAAS,CAChF,CAAC;GAGF,MAAM,YAAY,YAAY,QAAqB,QAAQ,SAAS;IACnE,MAAM,WAAW,IAAI,KAAK,KAAK,aAAa;AAC5C,WAAO,CAAC,UAAU,WAAW,SAAS,WAAW;MAC/C,KAAK;AAER,UAAO;IACN,MAAM,UAAU,KAAK;IACrB,aAAa,YAAY,IAAI,gBAAgB;IAC7C,eAAe,cAAc,IAAI,kBAAkB;IACnD;IACA;;EAGF,MAAM,cAA+B;AAQpC,WAPe,MAAM,IACnB,WAAW,QAAQ,CACnB,QAAQ,OAAO,GAAG,GAAG,UAAkB,CAAC,GAAG,QAAQ,CAAC,CACpD,MAAM,QAAQ,KAAK,KAAK,MAAM,CAC9B,MAAM,YAAY,KAAK,EAAE,CACzB,yBAAyB,EAEb;;EAOf,MAAM,kBAAkB,IAAwC;GAC/D,MAAM,MAAM,MAAM,IAChB,WAAW,cAAc,CACzB,WAAW,CACX,MAAM,MAAM,KAAK,GAAG,CACpB,kBAAkB;AAEpB,UAAO,MAAM,gBAAgB,IAAI,GAAG;;EAGrC,MAAM,uBAAuB,QAAuC;AAOnE,WANa,MAAM,IACjB,WAAW,cAAc,CACzB,WAAW,CACX,MAAM,WAAW,KAAK,OAAO,CAC7B,SAAS,EAEC,IAAI,gBAAgB;;EAGjC,MAAM,iBAAiB,YAAgD;GACtE,MAAM,uBAAM,IAAI,MAAM,EAAC,aAAa;GAEpC,MAAM,MAAmC;IACxC,IAAI,WAAW;IACf,SAAS,WAAW;IACpB,YAAY,WAAW;IACvB,SAAS,WAAW;IACpB,aAAa,WAAW;IACxB,WAAW,WAAW,WAAW,IAAI;IACrC,YAAY,WAAW,WAAW,SAAS,IAAI,KAAK,UAAU,WAAW,WAAW,GAAG;IACvF,MAAM,WAAW,QAAQ;IACzB,YAAY;IACZ,cAAc;IACd;AAED,SAAM,IAAI,WAAW,cAAc,CAAC,OAAO,IAAI,CAAC,SAAS;AAEzD,UAAO;IACN,IAAI,WAAW;IACf,QAAQ,WAAW;IACnB,WAAW,WAAW;IACtB,SAAS,WAAW;IACpB,YAAY,WAAW;IACvB,UAAU,WAAW;IACrB,YAAY,WAAW;IACvB,MAAM,WAAW,QAAQ;IACzB,WAAW,IAAI,KAAK,IAAI;IACxB,YAAY,IAAI,KAAK,IAAI;IACzB;;EAGF,MAAM,wBAAwB,IAAY,SAAgC;AACzE,SAAM,IACJ,YAAY,cAAc,CAC1B,IAAI;IACJ;IACA,+BAAc,IAAI,MAAM,EAAC,aAAa;IACtC,CAAC,CACD,MAAM,MAAM,KAAK,GAAG,CACpB,SAAS;;EAGZ,MAAM,qBAAqB,IAAY,MAAoC;AAC1E,SAAM,IAAI,YAAY,cAAc,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,MAAM,MAAM,KAAK,GAAG,CAAC,SAAS;;EAGlF,MAAM,iBAAiB,IAA2B;AACjD,SAAM,IAAI,WAAW,cAAc,CAAC,MAAM,MAAM,KAAK,GAAG,CAAC,SAAS;;EAGnE,MAAM,yBAAyB,QAAiC;AAO/D,WANe,MAAM,IACnB,WAAW,cAAc,CACzB,QAAQ,OAAO,GAAG,GAAG,UAAkB,CAAC,GAAG,QAAQ,CAAC,CACpD,MAAM,WAAW,KAAK,OAAO,CAC7B,yBAAyB,EAEb;;EAOf,MAAM,YAAY,OAAoC;GACrD,MAAM,MAAkC;IACvC,MAAM,MAAM;IACZ,SAAS,MAAM,UAAU;IACzB,OAAO,MAAM,SAAS;IACtB,MAAM,MAAM;IACZ,MAAM,MAAM,QAAQ;IACpB,YAAY,MAAM,aAAa;IAC/B,YAAY,MAAM,UAAU,aAAa;IACzC,6BAAY,IAAI,MAAM,EAAC,aAAa;IACpC;AAED,SAAM,IAAI,WAAW,cAAc,CAAC,OAAO,IAAI,CAAC,SAAS;;EAG1D,MAAM,SAAS,MAAc,MAA4C;GACxE,MAAM,MAAM,MAAM,IAChB,WAAW,cAAc,CACzB,WAAW,CACX,MAAM,QAAQ,KAAK,KAAK,CACxB,MAAM,QAAQ,KAAK,KAAK,CACxB,kBAAkB;AAEpB,UAAO,MAAM,eAAe,IAAI,GAAG;;EAGpC,MAAM,YAAY,MAA6B;AAC9C,SAAM,IAAI,WAAW,cAAc,CAAC,MAAM,QAAQ,KAAK,KAAK,CAAC,SAAS;;EAGvE,MAAM,sBAAqC;AAC1C,SAAM,IACJ,WAAW,cAAc,CACzB,MAAM,cAAc,sBAAK,IAAI,MAAM,EAAC,aAAa,CAAC,CAClD,SAAS;;EAOZ,MAAM,gBACL,UACA,mBAC+B;GAC/B,MAAM,MAAM,MAAM,IAChB,WAAW,iBAAiB,CAC5B,WAAW,CACX,MAAM,YAAY,KAAK,SAAS,CAChC,MAAM,uBAAuB,KAAK,kBAAkB,CACpD,kBAAkB;AAEpB,UAAO,MAAM,kBAAkB,IAAI,GAAG;;EAGvC,MAAM,yBAAyB,QAAyC;AAOvE,WANa,MAAM,IACjB,WAAW,iBAAiB,CAC5B,WAAW,CACX,MAAM,WAAW,KAAK,OAAO,CAC7B,SAAS,EAEC,IAAI,kBAAkB;;EAGnC,MAAM,mBAAmB,SAAiD;GACzE,MAAM,uBAAM,IAAI,MAAM,EAAC,aAAa;GAEpC,MAAM,MAAqC;IAC1C,UAAU,QAAQ;IAClB,qBAAqB,QAAQ;IAC7B,SAAS,QAAQ;IACjB,YAAY;IACZ;AAED,SAAM,IAAI,WAAW,iBAAiB,CAAC,OAAO,IAAI,CAAC,SAAS;AAE5D,UAAO;IACN,UAAU,QAAQ;IAClB,mBAAmB,QAAQ;IAC3B,QAAQ,QAAQ;IAChB,WAAW,IAAI,KAAK,IAAI;IACxB;;EAGF,MAAM,mBAAmB,UAAkB,mBAA0C;AACpF,SAAM,IACJ,WAAW,iBAAiB,CAC5B,MAAM,YAAY,KAAK,SAAS,CAChC,MAAM,uBAAuB,KAAK,kBAAkB,CACpD,SAAS;;EAOZ,MAAM,iBAAiB,QAA+C;GACrE,MAAM,MAAM,MAAM,IAChB,WAAW,kBAAkB,CAC7B,WAAW,CACX,MAAM,UAAU,KAAK,OAAO,aAAa,CAAC,CAC1C,kBAAkB;AAEpB,UAAO,MAAM,mBAAmB,IAAI,GAAG;;EAGxC,MAAM,oBAA8C;AAGnD,WAFa,MAAM,IAAI,WAAW,kBAAkB,CAAC,WAAW,CAAC,SAAS,EAE9D,IAAI,mBAAmB;;EAGpC,MAAM,oBAAoB,QAAgB,aAAgD;GACzF,MAAM,uBAAM,IAAI,MAAM,EAAC,aAAa;GAEpC,MAAM,MAAsC;IAC3C,QAAQ,OAAO,aAAa;IAC5B,cAAc;IACd,SAAS;IACT,YAAY;IACZ;AAED,SAAM,IAAI,WAAW,kBAAkB,CAAC,OAAO,IAAI,CAAC,SAAS;AAE7D,UAAO;IACN,QAAQ,IAAI;IACZ;IACA,SAAS;IACT,WAAW,IAAI,KAAK,IAAI;IACxB;;EAGF,MAAM,oBACL,QACA,SACA,aACgB;GAChB,MAAM,SAAyC,EAC9C,SAAS,UAAU,IAAI,GACvB;AAED,OAAI,gBAAgB,OACnB,QAAO,eAAe;AAGvB,SAAM,IACJ,YAAY,kBAAkB,CAC9B,IAAI,OAAO,CACX,MAAM,UAAU,KAAK,OAAO,aAAa,CAAC,CAC1C,SAAS;;EAGZ,MAAM,oBAAoB,QAA+B;AACxD,SAAM,IAAI,WAAW,kBAAkB,CAAC,MAAM,UAAU,KAAK,OAAO,aAAa,CAAC,CAAC,SAAS;;EAE7F;;AAOF,SAAS,UAAU,KAAkC;AACpD,QAAO;EACN,IAAI,IAAI;EACR,OAAO,IAAI;EACX,MAAM,IAAI;EACV,WAAW,IAAI;EACf,MAAM,YAAY,IAAI,KAAK;EAC3B,eAAe,IAAI,mBAAmB;EACtC,UAAU,IAAI,aAAa;EAC3B,MAAM,IAAI,OAAO,KAAK,MAAM,IAAI,KAAK,GAAG;EACxC,WAAW,IAAI,KAAK,IAAI,WAAW;EACnC,WAAW,IAAI,KAAK,IAAI,WAAW;EACnC;;AAGF,SAAS,gBAAgB,KAA8C;AACtE,QAAO;EACN,IAAI,IAAI;EACR,QAAQ,IAAI;EACZ,WAAW,IAAI;EACf,SAAS,IAAI;EACb,YAAY,aAAa,IAAI,YAAY;EACzC,UAAU,IAAI,cAAc;EAC5B,YAAY,IAAI,aAAa,KAAK,MAAM,IAAI,WAAW,GAAG,EAAE;EAC5D,MAAM,IAAI;EACV,WAAW,IAAI,KAAK,IAAI,WAAW;EACnC,YAAY,IAAI,KAAK,IAAI,aAAa;EACtC;;AAGF,SAAS,eAAe,KAA4C;AACnE,QAAO;EACN,MAAM,IAAI;EACV,QAAQ,IAAI;EACZ,OAAO,IAAI;EACX,MAAM,YAAY,IAAI,KAAK;EAC3B,MAAM,IAAI,QAAQ,OAAO,YAAY,IAAI,KAAK,GAAG;EACjD,WAAW,IAAI;EACf,WAAW,IAAI,KAAK,IAAI,WAAW;EACnC,WAAW,IAAI,KAAK,IAAI,WAAW;EACnC;;AAGF,SAAS,kBAAkB,KAAkD;AAC5E,QAAO;EACN,UAAU,IAAI;EACd,mBAAmB,IAAI;EACvB,QAAQ,IAAI;EACZ,WAAW,IAAI,KAAK,IAAI,WAAW;EACnC;;AAGF,SAAS,mBAAmB,KAAoD;AAC/E,QAAO;EACN,QAAQ,IAAI;EACZ,aAAa,YAAY,IAAI,aAAa;EAC1C,SAAS,IAAI,YAAY;EACzB,WAAW,IAAI,KAAK,IAAI,WAAW;EACnC;;AAOF,MAAa,kBAAkB"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { T as User, c as DeviceType, n as AuthAdapter, o as AuthenticatorTransport, s as Credential } from "./types-HtRc90Wi.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/passkey/types.d.ts
|
|
4
|
+
interface RegistrationOptions {
|
|
5
|
+
challenge: string;
|
|
6
|
+
rp: {
|
|
7
|
+
name: string;
|
|
8
|
+
id: string;
|
|
9
|
+
};
|
|
10
|
+
user: {
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
displayName: string;
|
|
14
|
+
};
|
|
15
|
+
pubKeyCredParams: Array<{
|
|
16
|
+
type: "public-key";
|
|
17
|
+
alg: number;
|
|
18
|
+
}>;
|
|
19
|
+
timeout?: number;
|
|
20
|
+
attestation?: "none" | "indirect" | "direct";
|
|
21
|
+
authenticatorSelection?: {
|
|
22
|
+
authenticatorAttachment?: "platform" | "cross-platform";
|
|
23
|
+
residentKey?: "discouraged" | "preferred" | "required";
|
|
24
|
+
requireResidentKey?: boolean;
|
|
25
|
+
userVerification?: "discouraged" | "preferred" | "required";
|
|
26
|
+
};
|
|
27
|
+
excludeCredentials?: Array<{
|
|
28
|
+
type: "public-key";
|
|
29
|
+
id: string;
|
|
30
|
+
transports?: AuthenticatorTransport[];
|
|
31
|
+
}>;
|
|
32
|
+
}
|
|
33
|
+
interface RegistrationResponse {
|
|
34
|
+
id: string;
|
|
35
|
+
rawId: string;
|
|
36
|
+
type: "public-key";
|
|
37
|
+
response: {
|
|
38
|
+
clientDataJSON: string;
|
|
39
|
+
attestationObject: string;
|
|
40
|
+
transports?: AuthenticatorTransport[];
|
|
41
|
+
};
|
|
42
|
+
authenticatorAttachment?: "platform" | "cross-platform";
|
|
43
|
+
}
|
|
44
|
+
interface VerifiedRegistration {
|
|
45
|
+
credentialId: string;
|
|
46
|
+
publicKey: Uint8Array;
|
|
47
|
+
counter: number;
|
|
48
|
+
deviceType: DeviceType;
|
|
49
|
+
backedUp: boolean;
|
|
50
|
+
transports: AuthenticatorTransport[];
|
|
51
|
+
}
|
|
52
|
+
interface AuthenticationOptions {
|
|
53
|
+
challenge: string;
|
|
54
|
+
rpId: string;
|
|
55
|
+
timeout?: number;
|
|
56
|
+
userVerification?: "discouraged" | "preferred" | "required";
|
|
57
|
+
allowCredentials?: Array<{
|
|
58
|
+
type: "public-key";
|
|
59
|
+
id: string;
|
|
60
|
+
transports?: AuthenticatorTransport[];
|
|
61
|
+
}>;
|
|
62
|
+
}
|
|
63
|
+
interface AuthenticationResponse {
|
|
64
|
+
id: string;
|
|
65
|
+
rawId: string;
|
|
66
|
+
type: "public-key";
|
|
67
|
+
response: {
|
|
68
|
+
clientDataJSON: string;
|
|
69
|
+
authenticatorData: string;
|
|
70
|
+
signature: string;
|
|
71
|
+
userHandle?: string;
|
|
72
|
+
};
|
|
73
|
+
authenticatorAttachment?: "platform" | "cross-platform";
|
|
74
|
+
}
|
|
75
|
+
interface VerifiedAuthentication {
|
|
76
|
+
credentialId: string;
|
|
77
|
+
newCounter: number;
|
|
78
|
+
}
|
|
79
|
+
interface ChallengeStore {
|
|
80
|
+
set(challenge: string, data: ChallengeData): Promise<void>;
|
|
81
|
+
get(challenge: string): Promise<ChallengeData | null>;
|
|
82
|
+
delete(challenge: string): Promise<void>;
|
|
83
|
+
}
|
|
84
|
+
interface ChallengeData {
|
|
85
|
+
type: "registration" | "authentication";
|
|
86
|
+
userId?: string;
|
|
87
|
+
expiresAt: number;
|
|
88
|
+
}
|
|
89
|
+
interface PasskeyConfig {
|
|
90
|
+
rpName: string;
|
|
91
|
+
rpId: string;
|
|
92
|
+
origin: string;
|
|
93
|
+
}
|
|
94
|
+
//#endregion
|
|
95
|
+
//#region src/passkey/register.d.ts
|
|
96
|
+
/**
|
|
97
|
+
* Generate registration options for creating a new passkey
|
|
98
|
+
*/
|
|
99
|
+
declare function generateRegistrationOptions(config: PasskeyConfig, user: Pick<User, "id" | "email" | "name">, existingCredentials: Credential[], challengeStore: ChallengeStore): Promise<RegistrationOptions>;
|
|
100
|
+
/**
|
|
101
|
+
* Verify a registration response and extract credential data
|
|
102
|
+
*/
|
|
103
|
+
declare function verifyRegistrationResponse(config: PasskeyConfig, response: RegistrationResponse, challengeStore: ChallengeStore): Promise<VerifiedRegistration>;
|
|
104
|
+
/**
|
|
105
|
+
* Register a new passkey for a user
|
|
106
|
+
*/
|
|
107
|
+
declare function registerPasskey(adapter: AuthAdapter, userId: string, verified: VerifiedRegistration, name?: string): Promise<Credential>;
|
|
108
|
+
//#endregion
|
|
109
|
+
//#region src/passkey/authenticate.d.ts
|
|
110
|
+
/**
|
|
111
|
+
* Generate authentication options for signing in with a passkey
|
|
112
|
+
*/
|
|
113
|
+
declare function generateAuthenticationOptions(config: PasskeyConfig, credentials: Credential[], challengeStore: ChallengeStore): Promise<AuthenticationOptions>;
|
|
114
|
+
/**
|
|
115
|
+
* Verify an authentication response
|
|
116
|
+
*/
|
|
117
|
+
declare function verifyAuthenticationResponse(config: PasskeyConfig, response: AuthenticationResponse, credential: Credential, challengeStore: ChallengeStore): Promise<VerifiedAuthentication>;
|
|
118
|
+
/**
|
|
119
|
+
* Authenticate a user with a passkey
|
|
120
|
+
*/
|
|
121
|
+
declare function authenticateWithPasskey(config: PasskeyConfig, adapter: AuthAdapter, response: AuthenticationResponse, challengeStore: ChallengeStore): Promise<User>;
|
|
122
|
+
//#endregion
|
|
123
|
+
export { registerPasskey as a, AuthenticationResponse as c, PasskeyConfig as d, RegistrationOptions as f, VerifiedRegistration as h, generateRegistrationOptions as i, ChallengeData as l, VerifiedAuthentication as m, generateAuthenticationOptions as n, verifyRegistrationResponse as o, RegistrationResponse as p, verifyAuthenticationResponse as r, AuthenticationOptions as s, authenticateWithPasskey as t, ChallengeStore as u };
|
|
124
|
+
//# sourceMappingURL=authenticate-D5UgaoTH.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authenticate-D5UgaoTH.d.mts","names":[],"sources":["../src/passkey/types.ts","../src/passkey/register.ts","../src/passkey/authenticate.ts"],"mappings":";;;UAUiB,mBAAA;EAChB,SAAA;EACA,EAAA;IACC,IAAA;IACA,EAAA;EAAA;EAED,IAAA;IACC,EAAA;IACA,IAAA;IACA,WAAA;EAAA;EAED,gBAAA,EAAkB,KAAA;IACjB,IAAA;IACA,GAAA;EAAA;EAED,OAAA;EACA,WAAA;EACA,sBAAA;IACC,uBAAA;IACA,WAAA;IACA,kBAAA;IACA,gBAAA;EAAA;EAED,kBAAA,GAAqB,KAAA;IACpB,IAAA;IACA,EAAA;IACA,UAAA,GAAa,sBAAA;EAAA;AAAA;AAAA,UAIE,oBAAA;EAChB,EAAA;EACA,KAAA;EACA,IAAA;EACA,QAAA;IACC,cAAA;IACA,iBAAA;IACA,UAAA,GAAa,sBAAA;EAAA;EAEd,uBAAA;AAAA;AAAA,UAGgB,oBAAA;EAChB,YAAA;EACA,SAAA,EAAW,UAAA;EACX,OAAA;EACA,UAAA,EAAY,UAAA;EACZ,QAAA;EACA,UAAA,EAAY,sBAAA;AAAA;AAAA,UAOI,qBAAA;EAChB,SAAA;EACA,IAAA;EACA,OAAA;EACA,gBAAA;EACA,gBAAA,GAAmB,KAAA;IAClB,IAAA;IACA,EAAA;IACA,UAAA,GAAa,sBAAA;EAAA;AAAA;AAAA,UAIE,sBAAA;EAChB,EAAA;EACA,KAAA;EACA,IAAA;EACA,QAAA;IACC,cAAA;IACA,iBAAA;IACA,SAAA;IACA,UAAA;EAAA;EAED,uBAAA;AAAA;AAAA,UAGgB,sBAAA;EAChB,YAAA;EACA,UAAA;AAAA;AAAA,UAOgB,cAAA;EAChB,GAAA,CAAI,SAAA,UAAmB,IAAA,EAAM,aAAA,GAAgB,OAAA;EAC7C,GAAA,CAAI,SAAA,WAAoB,OAAA,CAAQ,aAAA;EAChC,MAAA,CAAO,SAAA,WAAoB,OAAA;AAAA;AAAA,UAGX,aAAA;EAChB,IAAA;EACA,MAAA;EACA,SAAA;AAAA;AAAA,UAOgB,aAAA;EAChB,MAAA;EACA,IAAA;EACA,MAAA;AAAA;;;;;;iBCjFqB,2BAAA,CACrB,MAAA,EAAQ,aAAA,EACR,IAAA,EAAM,IAAA,CAAK,IAAA,4BACX,mBAAA,EAAqB,UAAA,IACrB,cAAA,EAAgB,cAAA,GACd,OAAA,CAAQ,mBAAA;;;;iBA8CW,0BAAA,CACrB,MAAA,EAAQ,aAAA,EACR,QAAA,EAAU,oBAAA,EACV,cAAA,EAAgB,cAAA,GACd,OAAA,CAAQ,oBAAA;;;;iBA6GW,eAAA,CACrB,OAAA,EAAS,WAAA,EACT,MAAA,UACA,QAAA,EAAU,oBAAA,EACV,IAAA,YACE,OAAA,CAAQ,UAAA;;;;;;iBCzKW,6BAAA,CACrB,MAAA,EAAQ,aAAA,EACR,WAAA,EAAa,UAAA,IACb,cAAA,EAAgB,cAAA,GACd,OAAA,CAAQ,qBAAA;;;;iBA4BW,4BAAA,CACrB,MAAA,EAAQ,aAAA,EACR,QAAA,EAAU,sBAAA,EACV,UAAA,EAAY,UAAA,EACZ,cAAA,EAAgB,cAAA,GACd,OAAA,CAAQ,sBAAA;;;;iBAmFW,uBAAA,CACrB,MAAA,EAAQ,aAAA,EACR,OAAA,EAAS,WAAA,EACT,QAAA,EAAU,sBAAA,EACV,cAAA,EAAgB,cAAA,GACd,OAAA,CAAQ,IAAA"}
|