@vonosan/auth 0.2.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.
Files changed (61) hide show
  1. package/dist/__tests__/passkey.test.d.ts +11 -0
  2. package/dist/__tests__/passkey.test.d.ts.map +1 -0
  3. package/dist/__tests__/passkey.test.js +87 -0
  4. package/dist/__tests__/passkey.test.js.map +1 -0
  5. package/dist/composables/useAuth.d.ts +43 -0
  6. package/dist/composables/useAuth.d.ts.map +1 -0
  7. package/dist/composables/useAuth.js +133 -0
  8. package/dist/composables/useAuth.js.map +1 -0
  9. package/dist/composables/usePasskey.d.ts +72 -0
  10. package/dist/composables/usePasskey.d.ts.map +1 -0
  11. package/dist/composables/usePasskey.js +289 -0
  12. package/dist/composables/usePasskey.js.map +1 -0
  13. package/dist/index.d.ts +29 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +37 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/lib/jwt.d.ts +30 -0
  18. package/dist/lib/jwt.d.ts.map +1 -0
  19. package/dist/lib/jwt.js +43 -0
  20. package/dist/lib/jwt.js.map +1 -0
  21. package/dist/lib/otp.d.ts +23 -0
  22. package/dist/lib/otp.d.ts.map +1 -0
  23. package/dist/lib/otp.js +50 -0
  24. package/dist/lib/otp.js.map +1 -0
  25. package/dist/lib/passkey.d.ts +139 -0
  26. package/dist/lib/passkey.d.ts.map +1 -0
  27. package/dist/lib/passkey.js +401 -0
  28. package/dist/lib/passkey.js.map +1 -0
  29. package/dist/lib/password.d.ts +20 -0
  30. package/dist/lib/password.d.ts.map +1 -0
  31. package/dist/lib/password.js +77 -0
  32. package/dist/lib/password.js.map +1 -0
  33. package/dist/middleware/auth.middleware.d.ts +50 -0
  34. package/dist/middleware/auth.middleware.d.ts.map +1 -0
  35. package/dist/middleware/auth.middleware.js +194 -0
  36. package/dist/middleware/auth.middleware.js.map +1 -0
  37. package/dist/passkey-schema.d.ts +375 -0
  38. package/dist/passkey-schema.d.ts.map +1 -0
  39. package/dist/passkey-schema.js +63 -0
  40. package/dist/passkey-schema.js.map +1 -0
  41. package/dist/routes/auth.routes.d.ts +16 -0
  42. package/dist/routes/auth.routes.d.ts.map +1 -0
  43. package/dist/routes/auth.routes.js +81 -0
  44. package/dist/routes/auth.routes.js.map +1 -0
  45. package/dist/routes/passkey.routes.d.ts +16 -0
  46. package/dist/routes/passkey.routes.d.ts.map +1 -0
  47. package/dist/routes/passkey.routes.js +127 -0
  48. package/dist/routes/passkey.routes.js.map +1 -0
  49. package/dist/schema.d.ts +547 -0
  50. package/dist/schema.d.ts.map +1 -0
  51. package/dist/schema.js +81 -0
  52. package/dist/schema.js.map +1 -0
  53. package/dist/service/auth.service.d.ts +73 -0
  54. package/dist/service/auth.service.d.ts.map +1 -0
  55. package/dist/service/auth.service.js +249 -0
  56. package/dist/service/auth.service.js.map +1 -0
  57. package/dist/service/passkey.service.d.ts +65 -0
  58. package/dist/service/passkey.service.d.ts.map +1 -0
  59. package/dist/service/passkey.service.js +202 -0
  60. package/dist/service/passkey.service.js.map +1 -0
  61. package/package.json +49 -0
@@ -0,0 +1,73 @@
1
+ /**
2
+ * ──────────────────────────────────────────────────────────────────
3
+ * 🏢 Company Name: Bonifade Technologies
4
+ * 👨‍💻 Developer: Bowofade Oyerinde
5
+ * 🐙 GitHub: oyenet1
6
+ * 📅 Created Date: 2026-04-05
7
+ * 🔄 Updated Date: 2026-04-05
8
+ * ──────────────────────────────────────────────────────────────────
9
+ */
10
+ import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js';
11
+ import { accounts } from '../schema.js';
12
+ export interface AuthTokens {
13
+ accessToken: string;
14
+ refreshToken: string;
15
+ sessionId: string;
16
+ }
17
+ export interface RegisterInput {
18
+ email: string;
19
+ password: string;
20
+ username?: string;
21
+ }
22
+ /**
23
+ * AuthService — handles all authentication flows.
24
+ *
25
+ * Inject via constructor with a Drizzle db instance and JWT secret.
26
+ */
27
+ export declare class AuthService {
28
+ private readonly db;
29
+ private readonly jwtSecret;
30
+ constructor(db: PostgresJsDatabase<typeof import('../schema.js')>, jwtSecret: string);
31
+ /**
32
+ * Register a new account with email + password.
33
+ * Assigns the 'user' role by default.
34
+ */
35
+ register(email: string, password: string, username?: string): Promise<{
36
+ id: string;
37
+ email: string;
38
+ }>;
39
+ /**
40
+ * Authenticate with email + password.
41
+ * Creates a session and returns access + refresh tokens.
42
+ */
43
+ login(email: string, password: string, ip?: string, userAgent?: string): Promise<AuthTokens>;
44
+ /**
45
+ * Exchange a refresh token for a new access + refresh token pair.
46
+ * Rotates the refresh token (old session deleted, new one created).
47
+ */
48
+ refresh(refreshToken: string): Promise<AuthTokens>;
49
+ /**
50
+ * Invalidate a session by ID.
51
+ */
52
+ logout(sessionId: string): Promise<void>;
53
+ /**
54
+ * Generate a 6-digit OTP for password reset.
55
+ * Stores the hash; returns the raw OTP so the caller can send it via email.
56
+ */
57
+ forgotPassword(email: string): Promise<string>;
58
+ /**
59
+ * Verify OTP and update the account password.
60
+ * Invalidates all existing sessions after reset.
61
+ */
62
+ resetPassword(email: string, otp: string, newPassword: string): Promise<void>;
63
+ /**
64
+ * Find or create an account for OAuth providers (Google, GitHub, etc.).
65
+ * Returns the account and whether it was newly created.
66
+ */
67
+ findOrCreateOAuthAccount(_provider: string, _providerId: string, email: string, name: string): Promise<{
68
+ account: typeof accounts.$inferSelect;
69
+ isNew: boolean;
70
+ }>;
71
+ private _createSession;
72
+ }
73
+ //# sourceMappingURL=auth.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.service.d.ts","sourceRoot":"","sources":["../../src/service/auth.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAIjE,OAAO,EAAE,QAAQ,EAAmC,MAAM,cAAc,CAAA;AAOxE,MAAM,WAAW,UAAU;IACzB,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB;AAID;;;;GAIG;AACH,qBAAa,WAAW;IAEpB,OAAO,CAAC,QAAQ,CAAC,EAAE;IACnB,OAAO,CAAC,QAAQ,CAAC,SAAS;gBADT,EAAE,EAAE,kBAAkB,CAAC,cAAc,cAAc,CAAC,CAAC,EACrD,SAAS,EAAE,MAAM;IAKpC;;;OAGG;IACG,QAAQ,CACZ,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,QAAQ,CAAC,EAAE,MAAM,GAChB,OAAO,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IA+BzC;;;OAGG;IACG,KAAK,CACT,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,EAAE,CAAC,EAAE,MAAM,EACX,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,UAAU,CAAC;IAyBtB;;;OAGG;IACG,OAAO,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IA8CxD;;OAEG;IACG,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAO9C;;;OAGG;IACG,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAwCpD;;;OAGG;IACG,aAAa,CACjB,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,MAAM,EACX,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,IAAI,CAAC;IAkDhB;;;OAGG;IACG,wBAAwB,CAC5B,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,GACX,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,QAAQ,CAAC,YAAY,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAE,CAAC;YAiCvD,cAAc;CA6B7B"}
@@ -0,0 +1,249 @@
1
+ /**
2
+ * ──────────────────────────────────────────────────────────────────
3
+ * 🏢 Company Name: Bonifade Technologies
4
+ * 👨‍💻 Developer: Bowofade Oyerinde
5
+ * 🐙 GitHub: oyenet1
6
+ * 📅 Created Date: 2026-04-05
7
+ * 🔄 Updated Date: 2026-04-05
8
+ * ──────────────────────────────────────────────────────────────────
9
+ */
10
+ import { eq, and, gt } from 'drizzle-orm';
11
+ import { HTTPException } from 'hono/http-exception';
12
+ import { generateId } from 'vonosan/server';
13
+ import { Logger } from 'vonosan/server';
14
+ import { accounts, authSessions, verificationCodes } from '../schema.js';
15
+ import { hashPassword, verifyPassword } from '../lib/password.js';
16
+ import { signAccessToken, signRefreshToken, verifyToken } from '../lib/jwt.js';
17
+ import { generateOtp, hashOtp, verifyOtp } from '../lib/otp.js';
18
+ // ─── AuthService ─────────────────────────────────────────────────────────────
19
+ /**
20
+ * AuthService — handles all authentication flows.
21
+ *
22
+ * Inject via constructor with a Drizzle db instance and JWT secret.
23
+ */
24
+ export class AuthService {
25
+ db;
26
+ jwtSecret;
27
+ constructor(db, jwtSecret) {
28
+ this.db = db;
29
+ this.jwtSecret = jwtSecret;
30
+ }
31
+ // ─── register ──────────────────────────────────────────────────────────────
32
+ /**
33
+ * Register a new account with email + password.
34
+ * Assigns the 'user' role by default.
35
+ */
36
+ async register(email, password, username) {
37
+ const existing = await this.db
38
+ .select({ id: accounts.id })
39
+ .from(accounts)
40
+ .where(eq(accounts.email, email.toLowerCase()))
41
+ .limit(1);
42
+ if (existing.length > 0) {
43
+ throw new HTTPException(409, { message: 'An account with this email already exists' });
44
+ }
45
+ const id = generateId();
46
+ const passwordHash = await hashPassword(password);
47
+ const resolvedUsername = username ?? email.split('@')[0];
48
+ await this.db.insert(accounts).values({
49
+ id,
50
+ email: email.toLowerCase(),
51
+ username: resolvedUsername,
52
+ password_hash: passwordHash,
53
+ status: 'active',
54
+ current_role: 'user',
55
+ language: 'en',
56
+ });
57
+ Logger.info('[auth] Account registered', { id, email });
58
+ return { id, email: email.toLowerCase() };
59
+ }
60
+ // ─── login ─────────────────────────────────────────────────────────────────
61
+ /**
62
+ * Authenticate with email + password.
63
+ * Creates a session and returns access + refresh tokens.
64
+ */
65
+ async login(email, password, ip, userAgent) {
66
+ const [account] = await this.db
67
+ .select()
68
+ .from(accounts)
69
+ .where(eq(accounts.email, email.toLowerCase()))
70
+ .limit(1);
71
+ if (!account || !account.password_hash) {
72
+ throw new HTTPException(401, { message: 'Invalid email or password' });
73
+ }
74
+ if (account.status !== 'active') {
75
+ throw new HTTPException(403, { message: 'Account is suspended' });
76
+ }
77
+ const valid = await verifyPassword(password, account.password_hash);
78
+ if (!valid) {
79
+ throw new HTTPException(401, { message: 'Invalid email or password' });
80
+ }
81
+ return this._createSession(account, ip, userAgent);
82
+ }
83
+ // ─── refresh ───────────────────────────────────────────────────────────────
84
+ /**
85
+ * Exchange a refresh token for a new access + refresh token pair.
86
+ * Rotates the refresh token (old session deleted, new one created).
87
+ */
88
+ async refresh(refreshToken) {
89
+ const payload = await verifyToken(refreshToken, this.jwtSecret);
90
+ if (!payload) {
91
+ throw new HTTPException(401, { message: 'Invalid or expired refresh token' });
92
+ }
93
+ if (!payload.sub) {
94
+ throw new HTTPException(401, { message: 'Invalid refresh token subject' });
95
+ }
96
+ // Verify session exists
97
+ const tokenHash = await hashOtp(refreshToken);
98
+ const [session] = await this.db
99
+ .select()
100
+ .from(authSessions)
101
+ .where(and(eq(authSessions.account_id, payload.sub), eq(authSessions.token_hash, tokenHash), gt(authSessions.expires_at, new Date())))
102
+ .limit(1);
103
+ if (!session) {
104
+ throw new HTTPException(401, { message: 'Session not found or expired' });
105
+ }
106
+ // Delete old session
107
+ await this.db.delete(authSessions).where(eq(authSessions.id, session.id));
108
+ const [account] = await this.db
109
+ .select()
110
+ .from(accounts)
111
+ .where(eq(accounts.id, payload.sub))
112
+ .limit(1);
113
+ if (!account || account.status !== 'active') {
114
+ throw new HTTPException(401, { message: 'Account not found or inactive' });
115
+ }
116
+ return this._createSession(account);
117
+ }
118
+ // ─── logout ────────────────────────────────────────────────────────────────
119
+ /**
120
+ * Invalidate a session by ID.
121
+ */
122
+ async logout(sessionId) {
123
+ await this.db.delete(authSessions).where(eq(authSessions.id, sessionId));
124
+ Logger.info('[auth] Session deleted', { sessionId });
125
+ }
126
+ // ─── forgotPassword ────────────────────────────────────────────────────────
127
+ /**
128
+ * Generate a 6-digit OTP for password reset.
129
+ * Stores the hash; returns the raw OTP so the caller can send it via email.
130
+ */
131
+ async forgotPassword(email) {
132
+ const [account] = await this.db
133
+ .select({ id: accounts.id })
134
+ .from(accounts)
135
+ .where(eq(accounts.email, email.toLowerCase()))
136
+ .limit(1);
137
+ if (!account) {
138
+ // Don't reveal whether the email exists
139
+ return generateOtp();
140
+ }
141
+ const otp = generateOtp();
142
+ const codeHash = await hashOtp(otp);
143
+ const expiresAt = new Date(Date.now() + 15 * 60 * 1000); // 15 minutes
144
+ // Remove any existing codes for this account
145
+ await this.db
146
+ .delete(verificationCodes)
147
+ .where(and(eq(verificationCodes.account_id, account.id), eq(verificationCodes.type, 'password_reset')));
148
+ await this.db.insert(verificationCodes).values({
149
+ id: generateId(),
150
+ account_id: account.id,
151
+ code_hash: codeHash,
152
+ type: 'password_reset',
153
+ expires_at: expiresAt,
154
+ });
155
+ Logger.info('[auth] Password reset OTP generated', { accountId: account.id });
156
+ return otp;
157
+ }
158
+ // ─── resetPassword ─────────────────────────────────────────────────────────
159
+ /**
160
+ * Verify OTP and update the account password.
161
+ * Invalidates all existing sessions after reset.
162
+ */
163
+ async resetPassword(email, otp, newPassword) {
164
+ const [account] = await this.db
165
+ .select()
166
+ .from(accounts)
167
+ .where(eq(accounts.email, email.toLowerCase()))
168
+ .limit(1);
169
+ if (!account) {
170
+ throw new HTTPException(400, { message: 'Invalid reset request' });
171
+ }
172
+ const [code] = await this.db
173
+ .select()
174
+ .from(verificationCodes)
175
+ .where(and(eq(verificationCodes.account_id, account.id), eq(verificationCodes.type, 'password_reset'), gt(verificationCodes.expires_at, new Date())))
176
+ .limit(1);
177
+ if (!code) {
178
+ throw new HTTPException(400, { message: 'OTP expired or not found' });
179
+ }
180
+ const valid = await verifyOtp(otp, code.code_hash);
181
+ if (!valid) {
182
+ throw new HTTPException(400, { message: 'Invalid OTP' });
183
+ }
184
+ const newHash = await hashPassword(newPassword);
185
+ await this.db
186
+ .update(accounts)
187
+ .set({ password_hash: newHash, updated_at: new Date() })
188
+ .where(eq(accounts.id, account.id));
189
+ // Delete the used code
190
+ await this.db.delete(verificationCodes).where(eq(verificationCodes.id, code.id));
191
+ // Invalidate all sessions
192
+ await this.db.delete(authSessions).where(eq(authSessions.account_id, account.id));
193
+ Logger.info('[auth] Password reset complete', { accountId: account.id });
194
+ }
195
+ // ─── findOrCreateOAuthAccount ──────────────────────────────────────────────
196
+ /**
197
+ * Find or create an account for OAuth providers (Google, GitHub, etc.).
198
+ * Returns the account and whether it was newly created.
199
+ */
200
+ async findOrCreateOAuthAccount(_provider, _providerId, email, name) {
201
+ const [existing] = await this.db
202
+ .select()
203
+ .from(accounts)
204
+ .where(eq(accounts.email, email.toLowerCase()))
205
+ .limit(1);
206
+ if (existing) {
207
+ return { account: existing, isNew: false };
208
+ }
209
+ const id = generateId();
210
+ const username = name.toLowerCase().replace(/\s+/g, '_') + '_' + id.slice(0, 6);
211
+ const [created] = await this.db
212
+ .insert(accounts)
213
+ .values({
214
+ id,
215
+ email: email.toLowerCase(),
216
+ username,
217
+ password_hash: null,
218
+ status: 'active',
219
+ current_role: 'user',
220
+ language: 'en',
221
+ })
222
+ .returning();
223
+ Logger.info('[auth] OAuth account created', { id, email, provider: _provider });
224
+ return { account: created, isNew: true };
225
+ }
226
+ // ─── Private helpers ───────────────────────────────────────────────────────
227
+ async _createSession(account, ip, userAgent) {
228
+ const payload = {
229
+ sub: account.id,
230
+ email: account.email,
231
+ role: account.current_role,
232
+ };
233
+ const accessToken = await signAccessToken(payload, this.jwtSecret);
234
+ const refreshToken = await signRefreshToken(payload, this.jwtSecret);
235
+ const sessionId = generateId();
236
+ const tokenHash = await hashOtp(refreshToken);
237
+ const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days
238
+ await this.db.insert(authSessions).values({
239
+ id: sessionId,
240
+ account_id: account.id,
241
+ token_hash: tokenHash,
242
+ ip: ip ?? null,
243
+ user_agent: userAgent ?? null,
244
+ expires_at: expiresAt,
245
+ });
246
+ return { accessToken, refreshToken, sessionId };
247
+ }
248
+ }
249
+ //# sourceMappingURL=auth.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.service.js","sourceRoot":"","sources":["../../src/service/auth.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,aAAa,CAAA;AAEzC,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AACnD,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAA;AAC3C,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAA;AACvC,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;AACxE,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AACjE,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAC9E,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAA;AAgB/D,gFAAgF;AAEhF;;;;GAIG;AACH,MAAM,OAAO,WAAW;IAEH;IACA;IAFnB,YACmB,EAAqD,EACrD,SAAiB;QADjB,OAAE,GAAF,EAAE,CAAmD;QACrD,cAAS,GAAT,SAAS,CAAQ;IACjC,CAAC;IAEJ,8EAA8E;IAE9E;;;OAGG;IACH,KAAK,CAAC,QAAQ,CACZ,KAAa,EACb,QAAgB,EAChB,QAAiB;QAEjB,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,EAAE;aAC3B,MAAM,CAAC,EAAE,EAAE,EAAE,QAAQ,CAAC,EAAE,EAAE,CAAC;aAC3B,IAAI,CAAC,QAAQ,CAAC;aACd,KAAK,CAAC,EAAE,CAAC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC;aAC9C,KAAK,CAAC,CAAC,CAAC,CAAA;QAEX,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACxB,MAAM,IAAI,aAAa,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,2CAA2C,EAAE,CAAC,CAAA;QACxF,CAAC;QAED,MAAM,EAAE,GAAG,UAAU,EAAE,CAAA;QACvB,MAAM,YAAY,GAAG,MAAM,YAAY,CAAC,QAAQ,CAAC,CAAA;QACjD,MAAM,gBAAgB,GAAG,QAAQ,IAAI,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAA;QAExD,MAAM,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC;YACpC,EAAE;YACF,KAAK,EAAE,KAAK,CAAC,WAAW,EAAE;YAC1B,QAAQ,EAAE,gBAAgB;YAC1B,aAAa,EAAE,YAAY;YAC3B,MAAM,EAAE,QAAQ;YAChB,YAAY,EAAE,MAAM;YACpB,QAAQ,EAAE,IAAI;SACf,CAAC,CAAA;QAEF,MAAM,CAAC,IAAI,CAAC,2BAA2B,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,CAAA;QACvD,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,CAAC,WAAW,EAAE,EAAE,CAAA;IAC3C,CAAC;IAED,8EAA8E;IAE9E;;;OAGG;IACH,KAAK,CAAC,KAAK,CACT,KAAa,EACb,QAAgB,EAChB,EAAW,EACX,SAAkB;QAElB,MAAM,CAAC,OAAO,CAAC,GAAG,MAAM,IAAI,CAAC,EAAE;aAC5B,MAAM,EAAE;aACR,IAAI,CAAC,QAAQ,CAAC;aACd,KAAK,CAAC,EAAE,CAAC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC;aAC9C,KAAK,CAAC,CAAC,CAAC,CAAA;QAEX,IAAI,CAAC,OAAO,IAAI,CAAC,OAAO,CAAC,aAAa,EAAE,CAAC;YACvC,MAAM,IAAI,aAAa,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,2BAA2B,EAAE,CAAC,CAAA;QACxE,CAAC;QAED,IAAI,OAAO,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;YAChC,MAAM,IAAI,aAAa,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,sBAAsB,EAAE,CAAC,CAAA;QACnE,CAAC;QAED,MAAM,KAAK,GAAG,MAAM,cAAc,CAAC,QAAQ,EAAE,OAAO,CAAC,aAAa,CAAC,CAAA;QACnE,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,MAAM,IAAI,aAAa,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,2BAA2B,EAAE,CAAC,CAAA;QACxE,CAAC;QAED,OAAO,IAAI,CAAC,cAAc,CAAC,OAAO,EAAE,EAAE,EAAE,SAAS,CAAC,CAAA;IACpD,CAAC;IAED,8EAA8E;IAE9E;;;OAGG;IACH,KAAK,CAAC,OAAO,CAAC,YAAoB;QAChC,MAAM,OAAO,GAAG,MAAM,WAAW,CAAC,YAAY,EAAE,IAAI,CAAC,SAAS,CAAC,CAAA;QAC/D,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,MAAM,IAAI,aAAa,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,kCAAkC,EAAE,CAAC,CAAA;QAC/E,CAAC;QAED,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC;YACjB,MAAM,IAAI,aAAa,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,+BAA+B,EAAE,CAAC,CAAA;QAC5E,CAAC;QAED,wBAAwB;QACxB,MAAM,SAAS,GAAG,MAAM,OAAO,CAAC,YAAY,CAAC,CAAA;QAC7C,MAAM,CAAC,OAAO,CAAC,GAAG,MAAM,IAAI,CAAC,EAAE;aAC5B,MAAM,EAAE;aACR,IAAI,CAAC,YAAY,CAAC;aAClB,KAAK,CACJ,GAAG,CACD,EAAE,CAAC,YAAY,CAAC,UAAU,EAAE,OAAO,CAAC,GAAG,CAAC,EACxC,EAAE,CAAC,YAAY,CAAC,UAAU,EAAE,SAAS,CAAC,EACtC,EAAE,CAAC,YAAY,CAAC,UAAU,EAAE,IAAI,IAAI,EAAE,CAAC,CACxC,CACF;aACA,KAAK,CAAC,CAAC,CAAC,CAAA;QAEX,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,MAAM,IAAI,aAAa,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,8BAA8B,EAAE,CAAC,CAAA;QAC3E,CAAC;QAED,qBAAqB;QACrB,MAAM,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,EAAE,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC,CAAA;QAEzE,MAAM,CAAC,OAAO,CAAC,GAAG,MAAM,IAAI,CAAC,EAAE;aAC5B,MAAM,EAAE;aACR,IAAI,CAAC,QAAQ,CAAC;aACd,KAAK,CAAC,EAAE,CAAC,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC;aACnC,KAAK,CAAC,CAAC,CAAC,CAAA;QAEX,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,MAAM,KAAK,QAAQ,EAAE,CAAC;YAC5C,MAAM,IAAI,aAAa,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,+BAA+B,EAAE,CAAC,CAAA;QAC5E,CAAC;QAED,OAAO,IAAI,CAAC,cAAc,CAAC,OAAO,CAAC,CAAA;IACrC,CAAC;IAED,8EAA8E;IAE9E;;OAEG;IACH,KAAK,CAAC,MAAM,CAAC,SAAiB;QAC5B,MAAM,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,EAAE,EAAE,SAAS,CAAC,CAAC,CAAA;QACxE,MAAM,CAAC,IAAI,CAAC,wBAAwB,EAAE,EAAE,SAAS,EAAE,CAAC,CAAA;IACtD,CAAC;IAED,8EAA8E;IAE9E;;;OAGG;IACH,KAAK,CAAC,cAAc,CAAC,KAAa;QAChC,MAAM,CAAC,OAAO,CAAC,GAAG,MAAM,IAAI,CAAC,EAAE;aAC5B,MAAM,CAAC,EAAE,EAAE,EAAE,QAAQ,CAAC,EAAE,EAAE,CAAC;aAC3B,IAAI,CAAC,QAAQ,CAAC;aACd,KAAK,CAAC,EAAE,CAAC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC;aAC9C,KAAK,CAAC,CAAC,CAAC,CAAA;QAEX,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,wCAAwC;YACxC,OAAO,WAAW,EAAE,CAAA;QACtB,CAAC;QAED,MAAM,GAAG,GAAG,WAAW,EAAE,CAAA;QACzB,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,CAAA;QACnC,MAAM,SAAS,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAA,CAAC,aAAa;QAErE,6CAA6C;QAC7C,MAAM,IAAI,CAAC,EAAE;aACV,MAAM,CAAC,iBAAiB,CAAC;aACzB,KAAK,CACJ,GAAG,CACD,EAAE,CAAC,iBAAiB,CAAC,UAAU,EAAE,OAAO,CAAC,EAAE,CAAC,EAC5C,EAAE,CAAC,iBAAiB,CAAC,IAAI,EAAE,gBAAgB,CAAC,CAC7C,CACF,CAAA;QAEH,MAAM,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAC,MAAM,CAAC;YAC7C,EAAE,EAAE,UAAU,EAAE;YAChB,UAAU,EAAE,OAAO,CAAC,EAAE;YACtB,SAAS,EAAE,QAAQ;YACnB,IAAI,EAAE,gBAAgB;YACtB,UAAU,EAAE,SAAS;SACtB,CAAC,CAAA;QAEF,MAAM,CAAC,IAAI,CAAC,qCAAqC,EAAE,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,EAAE,CAAC,CAAA;QAC7E,OAAO,GAAG,CAAA;IACZ,CAAC;IAED,8EAA8E;IAE9E;;;OAGG;IACH,KAAK,CAAC,aAAa,CACjB,KAAa,EACb,GAAW,EACX,WAAmB;QAEnB,MAAM,CAAC,OAAO,CAAC,GAAG,MAAM,IAAI,CAAC,EAAE;aAC5B,MAAM,EAAE;aACR,IAAI,CAAC,QAAQ,CAAC;aACd,KAAK,CAAC,EAAE,CAAC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC;aAC9C,KAAK,CAAC,CAAC,CAAC,CAAA;QAEX,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,MAAM,IAAI,aAAa,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,uBAAuB,EAAE,CAAC,CAAA;QACpE,CAAC;QAED,MAAM,CAAC,IAAI,CAAC,GAAG,MAAM,IAAI,CAAC,EAAE;aACzB,MAAM,EAAE;aACR,IAAI,CAAC,iBAAiB,CAAC;aACvB,KAAK,CACJ,GAAG,CACD,EAAE,CAAC,iBAAiB,CAAC,UAAU,EAAE,OAAO,CAAC,EAAE,CAAC,EAC5C,EAAE,CAAC,iBAAiB,CAAC,IAAI,EAAE,gBAAgB,CAAC,EAC5C,EAAE,CAAC,iBAAiB,CAAC,UAAU,EAAE,IAAI,IAAI,EAAE,CAAC,CAC7C,CACF;aACA,KAAK,CAAC,CAAC,CAAC,CAAA;QAEX,IAAI,CAAC,IAAI,EAAE,CAAC;YACV,MAAM,IAAI,aAAa,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,0BAA0B,EAAE,CAAC,CAAA;QACvE,CAAC;QAED,MAAM,KAAK,GAAG,MAAM,SAAS,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,CAAA;QAClD,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,MAAM,IAAI,aAAa,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,aAAa,EAAE,CAAC,CAAA;QAC1D,CAAC;QAED,MAAM,OAAO,GAAG,MAAM,YAAY,CAAC,WAAW,CAAC,CAAA;QAE/C,MAAM,IAAI,CAAC,EAAE;aACV,MAAM,CAAC,QAAQ,CAAC;aAChB,GAAG,CAAC,EAAE,aAAa,EAAE,OAAO,EAAE,UAAU,EAAE,IAAI,IAAI,EAAE,EAAE,CAAC;aACvD,KAAK,CAAC,EAAE,CAAC,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC,CAAA;QAErC,uBAAuB;QACvB,MAAM,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,iBAAiB,CAAC,EAAE,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC,CAAA;QAEhF,0BAA0B;QAC1B,MAAM,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,UAAU,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC,CAAA;QAEjF,MAAM,CAAC,IAAI,CAAC,gCAAgC,EAAE,EAAE,SAAS,EAAE,OAAO,CAAC,EAAE,EAAE,CAAC,CAAA;IAC1E,CAAC;IAED,8EAA8E;IAE9E;;;OAGG;IACH,KAAK,CAAC,wBAAwB,CAC5B,SAAiB,EACjB,WAAmB,EACnB,KAAa,EACb,IAAY;QAEZ,MAAM,CAAC,QAAQ,CAAC,GAAG,MAAM,IAAI,CAAC,EAAE;aAC7B,MAAM,EAAE;aACR,IAAI,CAAC,QAAQ,CAAC;aACd,KAAK,CAAC,EAAE,CAAC,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC;aAC9C,KAAK,CAAC,CAAC,CAAC,CAAA;QAEX,IAAI,QAAQ,EAAE,CAAC;YACb,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,CAAA;QAC5C,CAAC;QAED,MAAM,EAAE,GAAG,UAAU,EAAE,CAAA;QACvB,MAAM,QAAQ,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,GAAG,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;QAE/E,MAAM,CAAC,OAAO,CAAC,GAAG,MAAM,IAAI,CAAC,EAAE;aAC5B,MAAM,CAAC,QAAQ,CAAC;aAChB,MAAM,CAAC;YACN,EAAE;YACF,KAAK,EAAE,KAAK,CAAC,WAAW,EAAE;YAC1B,QAAQ;YACR,aAAa,EAAE,IAAI;YACnB,MAAM,EAAE,QAAQ;YAChB,YAAY,EAAE,MAAM;YACpB,QAAQ,EAAE,IAAI;SACf,CAAC;aACD,SAAS,EAAE,CAAA;QAEd,MAAM,CAAC,IAAI,CAAC,8BAA8B,EAAE,EAAE,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC,CAAA;QAC/E,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,CAAA;IAC1C,CAAC;IAED,8EAA8E;IAEtE,KAAK,CAAC,cAAc,CAC1B,OAAqC,EACrC,EAAW,EACX,SAAkB;QAElB,MAAM,OAAO,GAAG;YACd,GAAG,EAAE,OAAO,CAAC,EAAE;YACf,KAAK,EAAE,OAAO,CAAC,KAAK;YACpB,IAAI,EAAE,OAAO,CAAC,YAAY;SAC3B,CAAA;QAED,MAAM,WAAW,GAAG,MAAM,eAAe,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,CAAA;QAClE,MAAM,YAAY,GAAG,MAAM,gBAAgB,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,CAAA;QAEpE,MAAM,SAAS,GAAG,UAAU,EAAE,CAAA;QAC9B,MAAM,SAAS,GAAG,MAAM,OAAO,CAAC,YAAY,CAAC,CAAA;QAC7C,MAAM,SAAS,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAA,CAAC,SAAS;QAE1E,MAAM,IAAI,CAAC,EAAE,CAAC,MAAM,CAAC,YAAY,CAAC,CAAC,MAAM,CAAC;YACxC,EAAE,EAAE,SAAS;YACb,UAAU,EAAE,OAAO,CAAC,EAAE;YACtB,UAAU,EAAE,SAAS;YACrB,EAAE,EAAE,EAAE,IAAI,IAAI;YACd,UAAU,EAAE,SAAS,IAAI,IAAI;YAC7B,UAAU,EAAE,SAAS;SACtB,CAAC,CAAA;QAEF,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,SAAS,EAAE,CAAA;IACjD,CAAC;CACF"}
@@ -0,0 +1,65 @@
1
+ /**
2
+ * ──────────────────────────────────────────────────────────────────
3
+ * 🏢 Company Name: Bonifade Technologies
4
+ * 👨‍💻 Developer: Bowofade Oyerinde
5
+ * 🐙 GitHub: oyenet1
6
+ * 📅 Created Date: 2026-04-05
7
+ * 🔄 Updated Date: 2026-04-05
8
+ * ──────────────────────────────────────────────────────────────────
9
+ */
10
+ import { type RegistrationResponse, type AuthenticationResponse } from '../lib/passkey.js';
11
+ export interface PasskeyServiceConfig {
12
+ rpId: string;
13
+ rpName: string;
14
+ origin: string;
15
+ jwtSecret: string;
16
+ }
17
+ export declare class PasskeyService {
18
+ private readonly db;
19
+ private readonly config;
20
+ constructor(db: {
21
+ select: (...args: unknown[]) => unknown;
22
+ insert: (...args: unknown[]) => unknown;
23
+ update: (...args: unknown[]) => unknown;
24
+ delete: (...args: unknown[]) => unknown;
25
+ query: Record<string, unknown>;
26
+ }, config: PasskeyServiceConfig);
27
+ /**
28
+ * Begin passkey registration — generates a challenge and returns
29
+ * PublicKeyCredentialCreationOptions for the client.
30
+ */
31
+ beginRegistration(accountId: string, userName: string, displayName: string): Promise<Record<string, unknown>>;
32
+ /**
33
+ * Finish passkey registration — verifies the authenticator response
34
+ * and stores the credential.
35
+ */
36
+ finishRegistration(accountId: string, response: RegistrationResponse, credentialName?: string): Promise<{
37
+ credentialId: string;
38
+ deviceType: "platform" | "cross-platform";
39
+ }>;
40
+ /**
41
+ * Begin passkey authentication — generates a challenge.
42
+ * If accountId is provided, only that account's credentials are allowed.
43
+ * If null, any registered credential is allowed (usernameless flow).
44
+ */
45
+ beginAuthentication(accountId?: string): Promise<Record<string, unknown>>;
46
+ /**
47
+ * Finish passkey authentication — verifies the authenticator response
48
+ * and returns JWT tokens.
49
+ */
50
+ finishAuthentication(response: AuthenticationResponse): Promise<{
51
+ accountId: string;
52
+ accessToken: string;
53
+ refreshToken: string;
54
+ userVerified: boolean;
55
+ }>;
56
+ /** List all passkeys for an account */
57
+ listCredentials(accountId: string): Promise<any>;
58
+ /** Rename a passkey */
59
+ renameCredential(credentialId: string, accountId: string, name: string): Promise<void>;
60
+ /** Delete a passkey */
61
+ deleteCredential(credentialId: string, accountId: string): Promise<void>;
62
+ private getAndDeleteChallenge;
63
+ private cleanExpiredChallenges;
64
+ }
65
+ //# sourceMappingURL=passkey.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"passkey.service.d.ts","sourceRoot":"","sources":["../../src/service/passkey.service.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,EAQL,KAAK,oBAAoB,EACzB,KAAK,sBAAsB,EAC5B,MAAM,mBAAmB,CAAA;AAM1B,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,MAAM,CAAA;CAClB;AAID,qBAAa,cAAc;IAEvB,OAAO,CAAC,QAAQ,CAAC,EAAE;IAOnB,OAAO,CAAC,QAAQ,CAAC,MAAM;gBAPN,EAAE,EAAE;QACnB,MAAM,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAA;QACvC,MAAM,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAA;QACvC,MAAM,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAA;QACvC,MAAM,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAA;QACvC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAC/B,EACgB,MAAM,EAAE,oBAAoB;IAK/C;;;OAGG;IACG,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM;IAgChF;;;OAGG;IACG,kBAAkB,CACtB,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,oBAAoB,EAC9B,cAAc,CAAC,EAAE,MAAM;;;;IA0CzB;;;;OAIG;IACG,mBAAmB,CAAC,SAAS,CAAC,EAAE,MAAM;IAsC5C;;;OAGG;IACG,oBAAoB,CAAC,QAAQ,EAAE,sBAAsB;;;;;;IAsE3D,uCAAuC;IACjC,eAAe,CAAC,SAAS,EAAE,MAAM;IAMvC,uBAAuB;IACjB,gBAAgB,CAAC,YAAY,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM;IAW5E,uBAAuB;IACjB,gBAAgB,CAAC,YAAY,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;YAWhD,qBAAqB;YAgCrB,sBAAsB;CAKrC"}
@@ -0,0 +1,202 @@
1
+ /**
2
+ * ──────────────────────────────────────────────────────────────────
3
+ * 🏢 Company Name: Bonifade Technologies
4
+ * 👨‍💻 Developer: Bowofade Oyerinde
5
+ * 🐙 GitHub: oyenet1
6
+ * 📅 Created Date: 2026-04-05
7
+ * 🔄 Updated Date: 2026-04-05
8
+ * ──────────────────────────────────────────────────────────────────
9
+ */
10
+ import { eq, and, lt } from 'drizzle-orm';
11
+ import { generateChallenge, buildRegistrationOptions, buildAuthenticationOptions, verifyRegistration, verifyAuthentication, bufferToBase64url, base64urlToBuffer, } from '../lib/passkey.js';
12
+ import { passkeyCredentials, passkeyChallenges } from '../passkey-schema.js';
13
+ import { signAccessToken, signRefreshToken } from '../lib/jwt.js';
14
+ // ─── PasskeyService ───────────────────────────────────────────────────────────
15
+ export class PasskeyService {
16
+ db;
17
+ config;
18
+ constructor(db, config) {
19
+ this.db = db;
20
+ this.config = config;
21
+ }
22
+ // ── Registration ────────────────────────────────────────────────────────────
23
+ /**
24
+ * Begin passkey registration — generates a challenge and returns
25
+ * PublicKeyCredentialCreationOptions for the client.
26
+ */
27
+ async beginRegistration(accountId, userName, displayName) {
28
+ // Clean up expired challenges
29
+ await this.cleanExpiredChallenges();
30
+ const { challenge, expiresAt } = generateChallenge();
31
+ // Store challenge in DB
32
+ await this.db.insert(passkeyChallenges).values({
33
+ challenge,
34
+ type: 'registration',
35
+ account_id: accountId,
36
+ expires_at: new Date(expiresAt),
37
+ });
38
+ const userIdBytes = new TextEncoder().encode(accountId);
39
+ const userId = bufferToBase64url(userIdBytes);
40
+ return buildRegistrationOptions({
41
+ rpId: this.config.rpId,
42
+ rpName: this.config.rpName,
43
+ userId,
44
+ userName,
45
+ userDisplayName: displayName,
46
+ challenge,
47
+ authenticatorSelection: {
48
+ residentKey: 'preferred',
49
+ userVerification: 'preferred',
50
+ authenticatorAttachment: 'platform',
51
+ },
52
+ });
53
+ }
54
+ /**
55
+ * Finish passkey registration — verifies the authenticator response
56
+ * and stores the credential.
57
+ */
58
+ async finishRegistration(accountId, response, credentialName) {
59
+ // Retrieve and validate challenge
60
+ const challengeRecord = await this.getAndDeleteChallenge(response.response.clientDataJSON, 'registration');
61
+ if (!challengeRecord) {
62
+ throw new Error('Invalid or expired challenge');
63
+ }
64
+ if (challengeRecord.account_id !== accountId) {
65
+ throw new Error('Challenge account mismatch');
66
+ }
67
+ // Verify the registration response
68
+ const verified = await verifyRegistration(response, challengeRecord.challenge, this.config.origin, this.config.rpId);
69
+ // Store the credential
70
+ await this.db.insert(passkeyCredentials).values({
71
+ account_id: accountId,
72
+ credential_id: verified.credentialId,
73
+ public_key: verified.publicKey,
74
+ sign_count: verified.signCount,
75
+ device_type: verified.deviceType,
76
+ backed_up: verified.backedUp,
77
+ aaguid: verified.aaguid,
78
+ transports: verified.transports.join(','),
79
+ name: credentialName ?? `Passkey ${new Date().toLocaleDateString()}`,
80
+ });
81
+ return { credentialId: verified.credentialId, deviceType: verified.deviceType };
82
+ }
83
+ // ── Authentication ──────────────────────────────────────────────────────────
84
+ /**
85
+ * Begin passkey authentication — generates a challenge.
86
+ * If accountId is provided, only that account's credentials are allowed.
87
+ * If null, any registered credential is allowed (usernameless flow).
88
+ */
89
+ async beginAuthentication(accountId) {
90
+ await this.cleanExpiredChallenges();
91
+ const { challenge, expiresAt } = generateChallenge();
92
+ await this.db.insert(passkeyChallenges).values({
93
+ challenge,
94
+ type: 'authentication',
95
+ account_id: accountId ?? null,
96
+ expires_at: new Date(expiresAt),
97
+ });
98
+ // If accountId provided, include their credentials as allowCredentials
99
+ let allowCredentials = [];
100
+ if (accountId) {
101
+ const creds = await this.db.select()
102
+ .from(passkeyCredentials)
103
+ .where(eq(passkeyCredentials.account_id, accountId));
104
+ allowCredentials = creds.map((c) => ({
105
+ id: c.credential_id,
106
+ type: 'public-key',
107
+ transports: c.transports ? c.transports.split(',') : undefined,
108
+ }));
109
+ }
110
+ return buildAuthenticationOptions({
111
+ rpId: this.config.rpId,
112
+ challenge,
113
+ userVerification: 'preferred',
114
+ allowCredentials,
115
+ });
116
+ }
117
+ /**
118
+ * Finish passkey authentication — verifies the authenticator response
119
+ * and returns JWT tokens.
120
+ */
121
+ async finishAuthentication(response) {
122
+ // Retrieve and validate challenge
123
+ const challengeRecord = await this.getAndDeleteChallenge(response.response.clientDataJSON, 'authentication');
124
+ if (!challengeRecord) {
125
+ throw new Error('Invalid or expired challenge');
126
+ }
127
+ // Find the credential by ID
128
+ const [cred] = await this.db.select()
129
+ .from(passkeyCredentials)
130
+ .where(eq(passkeyCredentials.credential_id, response.id))
131
+ .limit(1);
132
+ if (!cred) {
133
+ throw new Error('Credential not found');
134
+ }
135
+ // Verify the authentication response
136
+ const verified = await verifyAuthentication(response, challengeRecord.challenge, this.config.origin, this.config.rpId, {
137
+ credentialId: cred.credential_id,
138
+ publicKey: cred.public_key,
139
+ signCount: cred.sign_count,
140
+ deviceType: cred.device_type,
141
+ backedUp: cred.backed_up,
142
+ transports: [],
143
+ aaguid: '',
144
+ });
145
+ // Update sign count and last_used_at
146
+ await this.db.update(passkeyCredentials)
147
+ .set({
148
+ sign_count: verified.newSignCount,
149
+ last_used_at: new Date(),
150
+ updated_at: new Date(),
151
+ })
152
+ .where(eq(passkeyCredentials.credential_id, cred.credential_id));
153
+ // Issue JWT tokens
154
+ const payload = { accountId: cred.account_id, email: '' };
155
+ const accessToken = await signAccessToken(payload, this.config.jwtSecret);
156
+ const refreshToken = await signRefreshToken(payload, this.config.jwtSecret);
157
+ return {
158
+ accountId: cred.account_id,
159
+ accessToken,
160
+ refreshToken,
161
+ userVerified: verified.userVerified,
162
+ };
163
+ }
164
+ // ── Credential management ───────────────────────────────────────────────────
165
+ /** List all passkeys for an account */
166
+ async listCredentials(accountId) {
167
+ return this.db.select()
168
+ .from(passkeyCredentials)
169
+ .where(eq(passkeyCredentials.account_id, accountId));
170
+ }
171
+ /** Rename a passkey */
172
+ async renameCredential(credentialId, accountId, name) {
173
+ await this.db.update(passkeyCredentials)
174
+ .set({ name, updated_at: new Date() })
175
+ .where(and(eq(passkeyCredentials.credential_id, credentialId), eq(passkeyCredentials.account_id, accountId)));
176
+ }
177
+ /** Delete a passkey */
178
+ async deleteCredential(credentialId, accountId) {
179
+ await this.db.delete(passkeyCredentials).where(and(eq(passkeyCredentials.credential_id, credentialId), eq(passkeyCredentials.account_id, accountId)));
180
+ }
181
+ // ── Helpers ─────────────────────────────────────────────────────────────────
182
+ async getAndDeleteChallenge(clientDataJSONBase64, type) {
183
+ // Decode challenge from clientDataJSON
184
+ const bytes = base64urlToBuffer(clientDataJSONBase64);
185
+ const clientData = JSON.parse(new TextDecoder().decode(bytes));
186
+ const [record] = await this.db.select()
187
+ .from(passkeyChallenges)
188
+ .where(and(eq(passkeyChallenges.challenge, clientData.challenge), eq(passkeyChallenges.type, type)))
189
+ .limit(1);
190
+ if (!record)
191
+ return null;
192
+ if (record.expires_at < new Date())
193
+ return null;
194
+ // Delete challenge (single-use)
195
+ await this.db.delete(passkeyChallenges).where(eq(passkeyChallenges.id, record.id));
196
+ return record;
197
+ }
198
+ async cleanExpiredChallenges() {
199
+ await this.db.delete(passkeyChallenges).where(lt(passkeyChallenges.expires_at, new Date()));
200
+ }
201
+ }
202
+ //# sourceMappingURL=passkey.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"passkey.service.js","sourceRoot":"","sources":["../../src/service/passkey.service.ts"],"names":[],"mappings":"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"}