@homurajs/auth 26.10.14 → 26.10.15
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/README.md +108 -31
- package/dist/hono.d.ts +61 -15
- package/dist/hono.js +294 -19
- package/dist/index.d.ts +46 -2
- package/dist/index.js +323 -9
- package/dist/provider-Bf7d5mkl.d.ts +164 -0
- package/dist/provider-BkUYQ703.d.ts +157 -0
- package/dist/provider-DMtwLgGf.js +215 -0
- package/dist/provider-zTWN9myS.js +276 -0
- package/dist/tables.d.ts +850 -15
- package/dist/tables.js +101 -11
- package/dist/{manager-_5q5sluE.d.ts → types-C-SlEMFJ.d.ts} +86 -49
- package/dist/webauthn.d.ts +21 -0
- package/dist/webauthn.js +164 -0
- package/package.json +11 -3
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { _ as TokenResponse, c as SessionManagerConfig, d as RefreshTokenStore, f as createDrizzleSessionStore, g as OAuth2Endpoints, h as AuthUser, i as UserStore, l as SessionTokens, m as RefreshTokenRecord, n as LocalUser, o as SessionClaims, p as ConsumedToken, r as OAuth2LinkStore, s as SessionManager, t as IdentityLink, u as DrizzleDb } from "./types-C-SlEMFJ.js";
|
|
2
|
+
import { a as PasswordStore, c as IdentityResolverOptions, d as OAuth2Client, f as OAuth2ClientConfig, i as PasswordRecord, l as ResolveProfile, n as PasswordProviderOptions, o as IdentityNotLinkedError, r as UsernameTakenError, s as IdentityResolver, t as PasswordProvider, u as createDefaultIdentityResolver } from "./provider-BkUYQ703.js";
|
|
3
|
+
import { a as WebAuthProvider, c as WEBAUTHN_CREDENTIALS_TABLE, d as WebauthnCredentialStore, i as RegistrationResponse, l as createRestWebauthnCredentialStore, n as AuthenticationResponse, o as WebAuthProviderConfig, r as RegistrationOptions, s as RestWebauthnCredentialStoreOptions, t as AuthenticationOptions, u as createDrizzleWebauthnCredentialStore } from "./provider-Bf7d5mkl.js";
|
|
2
4
|
import { RestDb } from "@homurajs/db";
|
|
5
|
+
import { NodeSQLiteDatabase } from "drizzle-orm/node-sqlite";
|
|
6
|
+
import { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
3
7
|
//#region src/session/rest-store.d.ts
|
|
4
8
|
export declare const REFRESH_TOKENS_TABLE = "refresh_tokens";
|
|
5
9
|
interface RestSessionStoreOptions {
|
|
@@ -8,4 +12,44 @@ interface RestSessionStoreOptions {
|
|
|
8
12
|
}
|
|
9
13
|
export declare function createRestSessionStore(rest: RestDb, opts?: RestSessionStoreOptions): RefreshTokenStore;
|
|
10
14
|
//#endregion
|
|
11
|
-
|
|
15
|
+
//#region src/identity/user-store.d.ts
|
|
16
|
+
type DrizzleDb$3 = PostgresJsDatabase | NodeSQLiteDatabase;
|
|
17
|
+
export declare function createDrizzleUserStore(db: DrizzleDb$3, opts?: {
|
|
18
|
+
now?: () => number;
|
|
19
|
+
}): UserStore;
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/identity/oauth2-link-store.d.ts
|
|
22
|
+
type DrizzleDb$2 = PostgresJsDatabase | NodeSQLiteDatabase;
|
|
23
|
+
export declare function createDrizzleOAuth2LinkStore(db: DrizzleDb$2, opts?: {
|
|
24
|
+
now?: () => number;
|
|
25
|
+
}): OAuth2LinkStore;
|
|
26
|
+
//#endregion
|
|
27
|
+
//#region src/identity/user-rest-store.d.ts
|
|
28
|
+
interface RestUserStoreOptions {
|
|
29
|
+
table?: string;
|
|
30
|
+
now?: () => number;
|
|
31
|
+
}
|
|
32
|
+
export declare function createRestUserStore(rest: RestDb, opts?: RestUserStoreOptions): UserStore;
|
|
33
|
+
//#endregion
|
|
34
|
+
//#region src/identity/oauth2-link-rest-store.d.ts
|
|
35
|
+
interface RestOAuth2LinkStoreOptions {
|
|
36
|
+
table?: string;
|
|
37
|
+
now?: () => number;
|
|
38
|
+
}
|
|
39
|
+
export declare function createRestOAuth2LinkStore(rest: RestDb, opts?: RestOAuth2LinkStoreOptions): OAuth2LinkStore;
|
|
40
|
+
//#endregion
|
|
41
|
+
//#region src/password/drizzle-store.d.ts
|
|
42
|
+
type DrizzleDb$1 = PostgresJsDatabase | NodeSQLiteDatabase;
|
|
43
|
+
export declare function createDrizzlePasswordStore(db: DrizzleDb$1, opts?: {
|
|
44
|
+
now?: () => number;
|
|
45
|
+
}): PasswordStore;
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region src/password/rest-store.d.ts
|
|
48
|
+
export declare const PASSWORDS_TABLE = "passwords";
|
|
49
|
+
interface RestPasswordStoreOptions {
|
|
50
|
+
table?: string;
|
|
51
|
+
now?: () => number;
|
|
52
|
+
}
|
|
53
|
+
export declare function createRestPasswordStore(rest: RestDb, opts?: RestPasswordStoreOptions): PasswordStore;
|
|
54
|
+
//#endregion
|
|
55
|
+
export { type AuthUser, type AuthenticationOptions, type AuthenticationResponse, type ConsumedToken, type DrizzleDb, type IdentityLink, IdentityNotLinkedError, type IdentityResolver, type IdentityResolverOptions, type LocalUser, OAuth2Client, type OAuth2ClientConfig, type OAuth2Endpoints, type OAuth2LinkStore, PasswordProvider, type PasswordProviderOptions, type PasswordRecord, type PasswordStore, type RefreshTokenRecord, type RefreshTokenStore, type RegistrationOptions, type RegistrationResponse, type ResolveProfile, type RestOAuth2LinkStoreOptions, type RestPasswordStoreOptions, type RestSessionStoreOptions, type RestUserStoreOptions, type RestWebauthnCredentialStoreOptions, type SessionClaims, SessionManager, type SessionManagerConfig, type SessionTokens, type TokenResponse, type UserStore, UsernameTakenError, WEBAUTHN_CREDENTIALS_TABLE, WebAuthProvider, type WebAuthProviderConfig, type WebauthnCredentialStore, createDefaultIdentityResolver, createDrizzleSessionStore, createDrizzleWebauthnCredentialStore, createRestWebauthnCredentialStore };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { i as createDefaultIdentityResolver, n as UsernameTakenError, r as IdentityNotLinkedError, t as PasswordProvider } from "./provider-DMtwLgGf.js";
|
|
2
|
+
import { i as createDrizzleWebauthnCredentialStore, n as WEBAUTHN_CREDENTIALS_TABLE, r as createRestWebauthnCredentialStore, t as WebAuthProvider } from "./provider-zTWN9myS.js";
|
|
1
3
|
import { randomToken } from "@homurajs/core";
|
|
2
|
-
import { createHash, randomBytes } from "node:crypto";
|
|
4
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
3
5
|
import { SignJWT, jwtVerify } from "jose";
|
|
4
6
|
import { sql } from "drizzle-orm";
|
|
5
7
|
import { isSqlite } from "@homurajs/db";
|
|
@@ -191,11 +193,11 @@ var SessionManager = class {
|
|
|
191
193
|
};
|
|
192
194
|
//#endregion
|
|
193
195
|
//#region src/session/store.ts
|
|
194
|
-
async function exec(db, q) {
|
|
196
|
+
async function exec$3(db, q) {
|
|
195
197
|
if (isSqlite(db)) await db.run(q);
|
|
196
198
|
else await db.execute(q);
|
|
197
199
|
}
|
|
198
|
-
async function query(db, q) {
|
|
200
|
+
async function query$2(db, q) {
|
|
199
201
|
if (isSqlite(db)) return await db.all(q);
|
|
200
202
|
return await db.execute(q) ?? [];
|
|
201
203
|
}
|
|
@@ -207,12 +209,12 @@ function createDrizzleSessionStore(db, opts) {
|
|
|
207
209
|
const now = opts?.now ?? (() => Date.now());
|
|
208
210
|
return {
|
|
209
211
|
async create(r) {
|
|
210
|
-
await exec(db, sql`INSERT INTO refresh_tokens (token_hash, family_id, sid, sub, expires_at, profile)
|
|
212
|
+
await exec$3(db, sql`INSERT INTO refresh_tokens (token_hash, family_id, sid, sub, expires_at, profile)
|
|
211
213
|
VALUES (${r.tokenHash}, ${r.familyId}, ${r.sid}, ${r.sub}, ${r.expiresAt}, ${r.profile ?? null})`);
|
|
212
214
|
},
|
|
213
215
|
async consume(tokenHash) {
|
|
214
216
|
const ts = now();
|
|
215
|
-
const claimed = (await query(db, sql`UPDATE refresh_tokens
|
|
217
|
+
const claimed = (await query$2(db, sql`UPDATE refresh_tokens
|
|
216
218
|
SET used_at = ${ts}
|
|
217
219
|
WHERE token_hash = ${tokenHash} AND expires_at > ${ts} AND used_at IS NULL
|
|
218
220
|
RETURNING family_id, sid, sub, profile`))[0];
|
|
@@ -223,7 +225,7 @@ RETURNING family_id, sid, sub, profile`))[0];
|
|
|
223
225
|
profile: claimed.profile,
|
|
224
226
|
revoked: false
|
|
225
227
|
};
|
|
226
|
-
const row = (await query(db, sql`SELECT family_id, used_at FROM refresh_tokens WHERE token_hash = ${tokenHash}`))[0];
|
|
228
|
+
const row = (await query$2(db, sql`SELECT family_id, used_at FROM refresh_tokens WHERE token_hash = ${tokenHash}`))[0];
|
|
227
229
|
if (!row) return null;
|
|
228
230
|
if (row.used_at !== null) return {
|
|
229
231
|
familyId: row.family_id,
|
|
@@ -235,10 +237,10 @@ RETURNING family_id, sid, sub, profile`))[0];
|
|
|
235
237
|
return null;
|
|
236
238
|
},
|
|
237
239
|
async revokeFamily(familyId) {
|
|
238
|
-
await exec(db, sql`UPDATE refresh_tokens SET used_at = ${now()} WHERE family_id = ${familyId} AND used_at IS NULL`);
|
|
240
|
+
await exec$3(db, sql`UPDATE refresh_tokens SET used_at = ${now()} WHERE family_id = ${familyId} AND used_at IS NULL`);
|
|
239
241
|
},
|
|
240
242
|
async revokeUser(sub) {
|
|
241
|
-
await exec(db, sql`UPDATE refresh_tokens SET used_at = ${now()} WHERE sub = ${sub} AND used_at IS NULL`);
|
|
243
|
+
await exec$3(db, sql`UPDATE refresh_tokens SET used_at = ${now()} WHERE sub = ${sub} AND used_at IS NULL`);
|
|
242
244
|
}
|
|
243
245
|
};
|
|
244
246
|
}
|
|
@@ -306,4 +308,316 @@ function createRestSessionStore(rest, opts = {}) {
|
|
|
306
308
|
};
|
|
307
309
|
}
|
|
308
310
|
//#endregion
|
|
309
|
-
|
|
311
|
+
//#region src/identity/user-store.ts
|
|
312
|
+
/**
|
|
313
|
+
* Drizzle 双方言 UserStore —— users 表的 CRUD。
|
|
314
|
+
* 复用 session/store.ts 的 exec/query 模式(isSqlite 分支 + 原生 SQL)。
|
|
315
|
+
*
|
|
316
|
+
* ⚠ 列名必须与 tables.ts 的显式 snake_case 对齐(id/username/nickname/avatar_url/created_at/updated_at)。
|
|
317
|
+
*/
|
|
318
|
+
async function exec$2(db, q) {
|
|
319
|
+
if (isSqlite(db)) await db.run(q);
|
|
320
|
+
else await db.execute(q);
|
|
321
|
+
}
|
|
322
|
+
async function query$1(db, q) {
|
|
323
|
+
if (isSqlite(db)) return await db.all(q);
|
|
324
|
+
return await db.execute(q) ?? [];
|
|
325
|
+
}
|
|
326
|
+
function toUser$1(row) {
|
|
327
|
+
return {
|
|
328
|
+
id: row.id,
|
|
329
|
+
username: row.username,
|
|
330
|
+
nickname: row.nickname,
|
|
331
|
+
avatarUrl: row.avatar_url,
|
|
332
|
+
createdAt: Number(row.created_at),
|
|
333
|
+
updatedAt: Number(row.updated_at)
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
function createDrizzleUserStore(db, opts) {
|
|
337
|
+
const now = opts?.now ?? (() => Date.now());
|
|
338
|
+
return {
|
|
339
|
+
async findById(id) {
|
|
340
|
+
const rows = await query$1(db, sql`SELECT id, username, nickname, avatar_url, created_at, updated_at FROM users WHERE id = ${id}`);
|
|
341
|
+
return rows[0] ? toUser$1(rows[0]) : null;
|
|
342
|
+
},
|
|
343
|
+
async findByUsername(username) {
|
|
344
|
+
const rows = await query$1(db, sql`SELECT id, username, nickname, avatar_url, created_at, updated_at FROM users WHERE username = ${username}`);
|
|
345
|
+
return rows[0] ? toUser$1(rows[0]) : null;
|
|
346
|
+
},
|
|
347
|
+
async create(input) {
|
|
348
|
+
const id = randomUUID();
|
|
349
|
+
const ts = now();
|
|
350
|
+
await exec$2(db, sql`INSERT INTO users (id, username, nickname, avatar_url, created_at, updated_at)
|
|
351
|
+
VALUES (${id}, ${input.username ?? null}, ${input.nickname ?? null}, ${input.avatarUrl ?? null}, ${ts}, ${ts})`);
|
|
352
|
+
return {
|
|
353
|
+
id,
|
|
354
|
+
username: input.username ?? null,
|
|
355
|
+
nickname: input.nickname ?? null,
|
|
356
|
+
avatarUrl: input.avatarUrl ?? null,
|
|
357
|
+
createdAt: ts,
|
|
358
|
+
updatedAt: ts
|
|
359
|
+
};
|
|
360
|
+
},
|
|
361
|
+
async update(id, patch) {
|
|
362
|
+
const ts = now();
|
|
363
|
+
if (patch.nickname !== void 0 && patch.avatarUrl !== void 0) await exec$2(db, sql`UPDATE users SET nickname = ${patch.nickname}, avatar_url = ${patch.avatarUrl}, updated_at = ${ts} WHERE id = ${id}`);
|
|
364
|
+
else if (patch.nickname !== void 0) await exec$2(db, sql`UPDATE users SET nickname = ${patch.nickname}, updated_at = ${ts} WHERE id = ${id}`);
|
|
365
|
+
else if (patch.avatarUrl !== void 0) await exec$2(db, sql`UPDATE users SET avatar_url = ${patch.avatarUrl}, updated_at = ${ts} WHERE id = ${id}`);
|
|
366
|
+
else await exec$2(db, sql`UPDATE users SET updated_at = ${ts} WHERE id = ${id}`);
|
|
367
|
+
const rows = await query$1(db, sql`SELECT id, username, nickname, avatar_url, created_at, updated_at FROM users WHERE id = ${id}`);
|
|
368
|
+
if (!rows[0]) throw new Error("user 不存在: " + id);
|
|
369
|
+
return toUser$1(rows[0]);
|
|
370
|
+
},
|
|
371
|
+
async delete(id) {
|
|
372
|
+
await exec$2(db, sql`DELETE FROM users WHERE id = ${id}`);
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
//#endregion
|
|
377
|
+
//#region src/identity/oauth2-link-store.ts
|
|
378
|
+
/**
|
|
379
|
+
* Drizzle 双方言 OAuth2LinkStore —— oauth2_identity_links 表的 CRUD。
|
|
380
|
+
* identity resolver 在 OAuth2 callback 时用 findByProvider 查已有 link,
|
|
381
|
+
* JIT 模式时 create 新 link 并关联新建的 user。
|
|
382
|
+
*
|
|
383
|
+
* ⚠ 列名必须与 tables.ts 的显式 snake_case 对齐
|
|
384
|
+
* (id/user_id/provider/provider_sub/raw_profile/created_at)。
|
|
385
|
+
*/
|
|
386
|
+
async function exec$1(db, q) {
|
|
387
|
+
if (isSqlite(db)) await db.run(q);
|
|
388
|
+
else await db.execute(q);
|
|
389
|
+
}
|
|
390
|
+
async function query(db, q) {
|
|
391
|
+
if (isSqlite(db)) return await db.all(q);
|
|
392
|
+
return await db.execute(q) ?? [];
|
|
393
|
+
}
|
|
394
|
+
function toLink$1(row) {
|
|
395
|
+
return {
|
|
396
|
+
id: row.id,
|
|
397
|
+
userId: row.user_id,
|
|
398
|
+
provider: row.provider,
|
|
399
|
+
providerSub: row.provider_sub,
|
|
400
|
+
rawProfile: row.raw_profile,
|
|
401
|
+
createdAt: Number(row.created_at)
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
const SELECT_COLS = sql.raw("id, user_id, provider, provider_sub, raw_profile, created_at");
|
|
405
|
+
function createDrizzleOAuth2LinkStore(db, opts) {
|
|
406
|
+
const now = opts?.now ?? (() => Date.now());
|
|
407
|
+
return {
|
|
408
|
+
async findByProvider(provider, providerSub) {
|
|
409
|
+
const rows = await query(db, sql`SELECT ${SELECT_COLS} FROM oauth2_identity_links
|
|
410
|
+
WHERE provider = ${provider} AND provider_sub = ${providerSub}`);
|
|
411
|
+
return rows[0] ? toLink$1(rows[0]) : null;
|
|
412
|
+
},
|
|
413
|
+
async findByUserId(userId) {
|
|
414
|
+
return (await query(db, sql`SELECT ${SELECT_COLS} FROM oauth2_identity_links WHERE user_id = ${userId}`)).map(toLink$1);
|
|
415
|
+
},
|
|
416
|
+
async create(input) {
|
|
417
|
+
const id = randomUUID();
|
|
418
|
+
const ts = now();
|
|
419
|
+
await exec$1(db, sql`INSERT INTO oauth2_identity_links (id, user_id, provider, provider_sub, raw_profile, created_at)
|
|
420
|
+
VALUES (${id}, ${input.userId}, ${input.provider}, ${input.providerSub}, ${input.rawProfile ?? null}, ${ts})`);
|
|
421
|
+
return {
|
|
422
|
+
id,
|
|
423
|
+
userId: input.userId,
|
|
424
|
+
provider: input.provider,
|
|
425
|
+
providerSub: input.providerSub,
|
|
426
|
+
rawProfile: input.rawProfile ?? null,
|
|
427
|
+
createdAt: ts
|
|
428
|
+
};
|
|
429
|
+
},
|
|
430
|
+
async delete(id) {
|
|
431
|
+
await exec$1(db, sql`DELETE FROM oauth2_identity_links WHERE id = ${id}`);
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
function toUser(row) {
|
|
436
|
+
return {
|
|
437
|
+
id: row.id,
|
|
438
|
+
username: row.username,
|
|
439
|
+
nickname: row.nickname,
|
|
440
|
+
avatarUrl: row.avatar_url,
|
|
441
|
+
createdAt: Number(row.created_at),
|
|
442
|
+
updatedAt: Number(row.updated_at)
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
function createRestUserStore(rest, opts = {}) {
|
|
446
|
+
const t = rest.table(opts.table ?? "users");
|
|
447
|
+
const now = opts.now ?? (() => Date.now());
|
|
448
|
+
return {
|
|
449
|
+
async findById(id) {
|
|
450
|
+
const row = (await t.select({ id }, { limit: 1 }))[0];
|
|
451
|
+
return row ? toUser(row) : null;
|
|
452
|
+
},
|
|
453
|
+
async findByUsername(username) {
|
|
454
|
+
const row = (await t.select({ username }, { limit: 1 }))[0];
|
|
455
|
+
return row ? toUser(row) : null;
|
|
456
|
+
},
|
|
457
|
+
async create(input) {
|
|
458
|
+
const id = randomUUID();
|
|
459
|
+
const ts = now();
|
|
460
|
+
const row = (await t.insert({
|
|
461
|
+
id,
|
|
462
|
+
username: input.username ?? null,
|
|
463
|
+
nickname: input.nickname ?? null,
|
|
464
|
+
avatar_url: input.avatarUrl ?? null,
|
|
465
|
+
created_at: ts,
|
|
466
|
+
updated_at: ts
|
|
467
|
+
}))[0];
|
|
468
|
+
if (!row) throw new Error("insert failed");
|
|
469
|
+
return toUser(row);
|
|
470
|
+
},
|
|
471
|
+
async update(id, patch) {
|
|
472
|
+
const ts = now();
|
|
473
|
+
const row = (await t.update({
|
|
474
|
+
...patch.nickname !== void 0 ? { nickname: patch.nickname } : {},
|
|
475
|
+
...patch.avatarUrl !== void 0 ? { avatar_url: patch.avatarUrl } : {},
|
|
476
|
+
updated_at: ts
|
|
477
|
+
}, { id }))[0];
|
|
478
|
+
if (!row) throw new Error("user 不存在: " + id);
|
|
479
|
+
return toUser(row);
|
|
480
|
+
},
|
|
481
|
+
async delete(id) {
|
|
482
|
+
await t.remove({ id });
|
|
483
|
+
}
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
function toLink(row) {
|
|
487
|
+
return {
|
|
488
|
+
id: row.id,
|
|
489
|
+
userId: row.user_id,
|
|
490
|
+
provider: row.provider,
|
|
491
|
+
providerSub: row.provider_sub,
|
|
492
|
+
rawProfile: row.raw_profile,
|
|
493
|
+
createdAt: Number(row.created_at)
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
function createRestOAuth2LinkStore(rest, opts = {}) {
|
|
497
|
+
const t = rest.table(opts.table ?? "oauth2_identity_links");
|
|
498
|
+
const now = opts.now ?? (() => Date.now());
|
|
499
|
+
return {
|
|
500
|
+
async findByProvider(provider, providerSub) {
|
|
501
|
+
const row = (await t.select({
|
|
502
|
+
provider,
|
|
503
|
+
provider_sub: providerSub
|
|
504
|
+
}, { limit: 1 }))[0];
|
|
505
|
+
return row ? toLink(row) : null;
|
|
506
|
+
},
|
|
507
|
+
async findByUserId(userId) {
|
|
508
|
+
return (await t.select({ user_id: userId })).map(toLink);
|
|
509
|
+
},
|
|
510
|
+
async create(input) {
|
|
511
|
+
const id = randomUUID();
|
|
512
|
+
const ts = now();
|
|
513
|
+
const row = (await t.insert({
|
|
514
|
+
id,
|
|
515
|
+
user_id: input.userId,
|
|
516
|
+
provider: input.provider,
|
|
517
|
+
provider_sub: input.providerSub,
|
|
518
|
+
raw_profile: input.rawProfile ?? null,
|
|
519
|
+
created_at: ts
|
|
520
|
+
}))[0];
|
|
521
|
+
if (!row) throw new Error("insert failed");
|
|
522
|
+
return toLink(row);
|
|
523
|
+
},
|
|
524
|
+
async delete(id) {
|
|
525
|
+
await t.remove({ id });
|
|
526
|
+
}
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
//#endregion
|
|
530
|
+
//#region src/password/drizzle-store.ts
|
|
531
|
+
/**
|
|
532
|
+
* Drizzle 双方言 PasswordStore 实现。
|
|
533
|
+
*
|
|
534
|
+
* ⚠ 列名必须与 tables.ts 的显式 snake_case 对齐
|
|
535
|
+
* (user_id/password_hash/created_at/updated_at)。
|
|
536
|
+
*/
|
|
537
|
+
async function exec(db, q) {
|
|
538
|
+
if (isSqlite(db)) await db.run(q);
|
|
539
|
+
else await db.execute(q);
|
|
540
|
+
}
|
|
541
|
+
async function queryOne(db, q) {
|
|
542
|
+
if (isSqlite(db)) return (await db.all(q))[0] ?? null;
|
|
543
|
+
return (await db.execute(q))?.[0] ?? null;
|
|
544
|
+
}
|
|
545
|
+
function toRecord$1(row) {
|
|
546
|
+
return {
|
|
547
|
+
userId: row.user_id,
|
|
548
|
+
passwordHash: row.password_hash,
|
|
549
|
+
createdAt: Number(row.created_at),
|
|
550
|
+
updatedAt: Number(row.updated_at)
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
function createDrizzlePasswordStore(db, opts) {
|
|
554
|
+
const now = opts?.now ?? (() => Date.now());
|
|
555
|
+
return {
|
|
556
|
+
async findByUserId(userId) {
|
|
557
|
+
const row = await queryOne(db, sql`SELECT user_id, password_hash, created_at, updated_at FROM passwords WHERE user_id = ${userId}`);
|
|
558
|
+
return row ? toRecord$1(row) : null;
|
|
559
|
+
},
|
|
560
|
+
async create(input) {
|
|
561
|
+
const ts = now();
|
|
562
|
+
await exec(db, sql`INSERT INTO passwords (user_id, password_hash, created_at, updated_at)
|
|
563
|
+
VALUES (${input.userId}, ${input.passwordHash}, ${ts}, ${ts})`);
|
|
564
|
+
return {
|
|
565
|
+
userId: input.userId,
|
|
566
|
+
passwordHash: input.passwordHash,
|
|
567
|
+
createdAt: ts,
|
|
568
|
+
updatedAt: ts
|
|
569
|
+
};
|
|
570
|
+
},
|
|
571
|
+
async updateHash(userId, passwordHash) {
|
|
572
|
+
const ts = now();
|
|
573
|
+
await exec(db, sql`UPDATE passwords SET password_hash = ${passwordHash}, updated_at = ${ts} WHERE user_id = ${userId}`);
|
|
574
|
+
},
|
|
575
|
+
async delete(userId) {
|
|
576
|
+
await exec(db, sql`DELETE FROM passwords WHERE user_id = ${userId}`);
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
//#endregion
|
|
581
|
+
//#region src/password/rest-store.ts
|
|
582
|
+
const PASSWORDS_TABLE = "passwords";
|
|
583
|
+
function toRecord(row) {
|
|
584
|
+
return {
|
|
585
|
+
userId: row.user_id,
|
|
586
|
+
passwordHash: row.password_hash,
|
|
587
|
+
createdAt: Number(row.created_at),
|
|
588
|
+
updatedAt: Number(row.updated_at)
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
function createRestPasswordStore(rest, opts = {}) {
|
|
592
|
+
const t = rest.table(opts.table ?? "passwords");
|
|
593
|
+
const now = opts.now ?? (() => Date.now());
|
|
594
|
+
return {
|
|
595
|
+
async findByUserId(userId) {
|
|
596
|
+
const row = (await t.select({ user_id: userId }, { limit: 1 }))[0];
|
|
597
|
+
return row ? toRecord(row) : null;
|
|
598
|
+
},
|
|
599
|
+
async create(input) {
|
|
600
|
+
const ts = now();
|
|
601
|
+
const row = (await t.insert({
|
|
602
|
+
user_id: input.userId,
|
|
603
|
+
password_hash: input.passwordHash,
|
|
604
|
+
created_at: ts,
|
|
605
|
+
updated_at: ts
|
|
606
|
+
}))[0];
|
|
607
|
+
if (!row) throw new Error("insert failed");
|
|
608
|
+
return toRecord(row);
|
|
609
|
+
},
|
|
610
|
+
async updateHash(userId, passwordHash) {
|
|
611
|
+
const ts = now();
|
|
612
|
+
await t.update({
|
|
613
|
+
password_hash: passwordHash,
|
|
614
|
+
updated_at: ts
|
|
615
|
+
}, { user_id: userId });
|
|
616
|
+
},
|
|
617
|
+
async delete(userId) {
|
|
618
|
+
await t.remove({ user_id: userId });
|
|
619
|
+
}
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
//#endregion
|
|
623
|
+
export { IdentityNotLinkedError, OAuth2Client, PASSWORDS_TABLE, PasswordProvider, REFRESH_TOKENS_TABLE, SessionManager, UsernameTakenError, WEBAUTHN_CREDENTIALS_TABLE, WebAuthProvider, createDefaultIdentityResolver, createDrizzleOAuth2LinkStore, createDrizzlePasswordStore, createDrizzleSessionStore, createDrizzleUserStore, createDrizzleWebauthnCredentialStore, createRestOAuth2LinkStore, createRestPasswordStore, createRestSessionStore, createRestUserStore, createRestWebauthnCredentialStore };
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { a as WebauthnCredentialRecord, i as UserStore, n as LocalUser } from "./types-C-SlEMFJ.js";
|
|
2
|
+
import { TtlKv } from "@homurajs/core";
|
|
3
|
+
import { RestDb } from "@homurajs/db";
|
|
4
|
+
import { NodeSQLiteDatabase } from "drizzle-orm/node-sqlite";
|
|
5
|
+
import { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
6
|
+
//#region src/webauthn/credential-store.d.ts
|
|
7
|
+
interface WebauthnCredentialStore {
|
|
8
|
+
/** 按 credentialId 查;不存在返回 null */
|
|
9
|
+
findById(id: string): Promise<WebauthnCredentialRecord | null>;
|
|
10
|
+
/** 按 userId 查所有凭证 */
|
|
11
|
+
findByUserId(userId: string): Promise<WebauthnCredentialRecord[]>;
|
|
12
|
+
/** 创建凭证 */
|
|
13
|
+
create(input: {
|
|
14
|
+
id: string;
|
|
15
|
+
userId: string;
|
|
16
|
+
publicKey: string;
|
|
17
|
+
counter: number;
|
|
18
|
+
deviceType: string;
|
|
19
|
+
backedUp: number;
|
|
20
|
+
transports?: string | null;
|
|
21
|
+
displayName?: string | null;
|
|
22
|
+
}): Promise<WebauthnCredentialRecord>;
|
|
23
|
+
/** 更新 counter(验证成功后调用) */
|
|
24
|
+
updateCounter(id: string, counter: number): Promise<void>;
|
|
25
|
+
/** 删除凭证 */
|
|
26
|
+
delete(id: string): Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
//#endregion
|
|
29
|
+
//#region src/webauthn/credential-drizzle-store.d.ts
|
|
30
|
+
type DrizzleDb = PostgresJsDatabase | NodeSQLiteDatabase;
|
|
31
|
+
declare function createDrizzleWebauthnCredentialStore(db: DrizzleDb, opts?: {
|
|
32
|
+
now?: () => number;
|
|
33
|
+
}): WebauthnCredentialStore;
|
|
34
|
+
//#endregion
|
|
35
|
+
//#region src/webauthn/credential-rest-store.d.ts
|
|
36
|
+
declare const WEBAUTHN_CREDENTIALS_TABLE = "webauthn_credentials";
|
|
37
|
+
interface RestWebauthnCredentialStoreOptions {
|
|
38
|
+
table?: string;
|
|
39
|
+
now?: () => number;
|
|
40
|
+
}
|
|
41
|
+
declare function createRestWebauthnCredentialStore(rest: RestDb, opts?: RestWebauthnCredentialStoreOptions): WebauthnCredentialStore;
|
|
42
|
+
//#endregion
|
|
43
|
+
//#region src/webauthn/provider.d.ts
|
|
44
|
+
/**
|
|
45
|
+
* WebAuthn RP 配置。
|
|
46
|
+
* rpId 通常是域名(如 'app.example.com'),origin 是完整 origin(如 'https://app.example.com')。
|
|
47
|
+
*/
|
|
48
|
+
interface WebAuthProviderConfig {
|
|
49
|
+
rpId: string;
|
|
50
|
+
rpName: string;
|
|
51
|
+
/** 允许的 origin(如 ['https://app.example.com']) */
|
|
52
|
+
origin: string | string[];
|
|
53
|
+
credentialStore: WebauthnCredentialStore;
|
|
54
|
+
userStore: UserStore;
|
|
55
|
+
/** challenge 存储(TtlKv),TTL 5min */
|
|
56
|
+
challengeKv: TtlKv;
|
|
57
|
+
/** challenge TTL(默认 5min) */
|
|
58
|
+
challengeTtlMs?: number;
|
|
59
|
+
now?: () => number;
|
|
60
|
+
}
|
|
61
|
+
/** 注册 options(传给客户端 @simplewebauthn/browser 的 startRegistration) */
|
|
62
|
+
interface RegistrationOptions {
|
|
63
|
+
challenge: string;
|
|
64
|
+
rp: {
|
|
65
|
+
name: string;
|
|
66
|
+
id: string;
|
|
67
|
+
};
|
|
68
|
+
user: {
|
|
69
|
+
id: string;
|
|
70
|
+
name: string;
|
|
71
|
+
displayName: string;
|
|
72
|
+
};
|
|
73
|
+
pubKeyCredParams: Array<{
|
|
74
|
+
type: 'public-key';
|
|
75
|
+
alg: number;
|
|
76
|
+
}>;
|
|
77
|
+
timeout: number;
|
|
78
|
+
attestationType: string;
|
|
79
|
+
authenticatorSelection: {
|
|
80
|
+
userVerification: string;
|
|
81
|
+
residentKey: string;
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/** 认证 options(传给客户端 @simplewebauthn/browser 的 startAuthentication) */
|
|
85
|
+
interface AuthenticationOptions {
|
|
86
|
+
challenge: string;
|
|
87
|
+
timeout: number;
|
|
88
|
+
rpId: string;
|
|
89
|
+
allowCredentials: Array<{
|
|
90
|
+
type: 'public-key';
|
|
91
|
+
id: string;
|
|
92
|
+
transports?: string[];
|
|
93
|
+
}>;
|
|
94
|
+
userVerification: string;
|
|
95
|
+
}
|
|
96
|
+
/** 注册验证输入(客户端 attestation 响应) */
|
|
97
|
+
interface RegistrationResponse {
|
|
98
|
+
id: string;
|
|
99
|
+
rawId: string;
|
|
100
|
+
response: {
|
|
101
|
+
clientDataJSON: string;
|
|
102
|
+
attestationObject: string;
|
|
103
|
+
transports?: string[];
|
|
104
|
+
};
|
|
105
|
+
type: string;
|
|
106
|
+
clientExtensionResults?: Record<string, unknown>;
|
|
107
|
+
}
|
|
108
|
+
/** 认证验证输入(客户端 assertion 响应) */
|
|
109
|
+
interface AuthenticationResponse {
|
|
110
|
+
id: string;
|
|
111
|
+
rawId: string;
|
|
112
|
+
response: {
|
|
113
|
+
clientDataJSON: string;
|
|
114
|
+
authenticatorData: string;
|
|
115
|
+
signature: string;
|
|
116
|
+
userHandle?: string | null;
|
|
117
|
+
};
|
|
118
|
+
type: string;
|
|
119
|
+
clientExtensionResults?: Record<string, unknown>;
|
|
120
|
+
}
|
|
121
|
+
declare class WebAuthProvider {
|
|
122
|
+
private readonly config;
|
|
123
|
+
private readonly challengeTtl;
|
|
124
|
+
private readonly nowFn;
|
|
125
|
+
constructor(config: WebAuthProviderConfig);
|
|
126
|
+
/**
|
|
127
|
+
* 生成注册 options(已登录用户发起)。
|
|
128
|
+
* challenge 存入 TtlKv(key = userId:challengeType),TTL 5min。
|
|
129
|
+
*/
|
|
130
|
+
generateRegistrationOptions(userId: string): Promise<RegistrationOptions>;
|
|
131
|
+
/**
|
|
132
|
+
* 验证注册响应并存储凭证。
|
|
133
|
+
* challenge 一次性消费(take 语义)。
|
|
134
|
+
*/
|
|
135
|
+
verifyRegistration(userId: string, response: RegistrationResponse, displayName?: string): Promise<{
|
|
136
|
+
id: string;
|
|
137
|
+
}>;
|
|
138
|
+
/**
|
|
139
|
+
* 生成认证 options(未登录用户发起)。
|
|
140
|
+
* challenge 存入 TtlKv,返回 { options, challengeKey }——challengeKey 必须由调用方
|
|
141
|
+
* 在 verifyAuthentication 时回传,否则匿名(discoverable credential)流程无法定位 challenge:
|
|
142
|
+
* 匿名场景下 options 阶段还不知道是哪个用户/凭证,key 只能是 `anon:${random}`,
|
|
143
|
+
* 而 verify 阶段是按 response.id 反查凭证的,两条腿对不上同一个 key。
|
|
144
|
+
* 已知 userId 时 key 固定为 `${userId}:auth`,回传与否都能命中(向后兼容)。
|
|
145
|
+
*/
|
|
146
|
+
generateAuthenticationOptions(userId?: string): Promise<{
|
|
147
|
+
options: AuthenticationOptions;
|
|
148
|
+
challengeKey: string;
|
|
149
|
+
}>;
|
|
150
|
+
/**
|
|
151
|
+
* 验证认证响应,返回已验证的用户(供 session 创建)。
|
|
152
|
+
* challenge 一次性消费。
|
|
153
|
+
*
|
|
154
|
+
* @param challengeKey generateAuthenticationOptions 返回的 key。匿名流程**必须**回传;
|
|
155
|
+
* 已知 userId 的流程可省略(缺省按 `${cred.userId}:auth` 取)。
|
|
156
|
+
*/
|
|
157
|
+
verifyAuthentication(response: AuthenticationResponse, challengeKey?: string): Promise<LocalUser>;
|
|
158
|
+
/** 列出用户的所有凭证(管理用) */
|
|
159
|
+
listCredentials(userId: string): Promise<WebauthnCredentialRecord[]>;
|
|
160
|
+
/** 删除凭证 */
|
|
161
|
+
deleteCredential(userId: string, credentialId: string): Promise<void>;
|
|
162
|
+
}
|
|
163
|
+
//#endregion
|
|
164
|
+
export { WebAuthProvider as a, WEBAUTHN_CREDENTIALS_TABLE as c, WebauthnCredentialStore as d, RegistrationResponse as i, createRestWebauthnCredentialStore as l, AuthenticationResponse as n, WebAuthProviderConfig as o, RegistrationOptions as r, RestWebauthnCredentialStoreOptions as s, AuthenticationOptions as t, createDrizzleWebauthnCredentialStore as u };
|