@everystack/auth 0.1.0
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 +256 -0
- package/package.json +104 -0
- package/src/api.ts +65 -0
- package/src/cff/index.ts +157 -0
- package/src/client/index.tsx +139 -0
- package/src/context.ts +30 -0
- package/src/cookies.ts +93 -0
- package/src/index.ts +526 -0
- package/src/jwt/hmac-node.ts +11 -0
- package/src/jwt/hmac-web.ts +17 -0
- package/src/jwt/hmac.ts +10 -0
- package/src/jwt/index.ts +182 -0
- package/src/jwt/jwt.sql +177 -0
- package/src/oauth/index.ts +7 -0
- package/src/oauth/providers/apple.ts +43 -0
- package/src/oauth/providers/google.ts +49 -0
- package/src/oauth/types.ts +17 -0
- package/src/oauth/verify.ts +193 -0
- package/src/oauth.ts +6 -0
- package/src/passwords.ts +70 -0
- package/src/postgres.ts +504 -0
- package/src/refresh.ts +204 -0
- package/src/response.ts +50 -0
- package/src/schema.ts +224 -0
- package/src/sql.ts +261 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @everystack/auth — JWT authentication handlers and password hashing.
|
|
3
|
+
*
|
|
4
|
+
* Two modes:
|
|
5
|
+
* - 'node' (default): bcrypt for passwords (cost via BCRYPT_COST env, default 10), in-house HS256 for JWTs
|
|
6
|
+
* - 'postgres': pgcrypto bcrypt (cost 10) for passwords, pgcrypto HMAC for JWTs
|
|
7
|
+
*
|
|
8
|
+
* Both modes produce byte-compatible bcrypt hashes and HS256 JWTs — credentials
|
|
9
|
+
* and tokens are interchangeable across modes.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* import { createAuthHandlers, hashPassword, verifyPassword } from '@everystack/auth';
|
|
13
|
+
*
|
|
14
|
+
* // Node mode (default):
|
|
15
|
+
* const auth = createAuthHandlers(db, { users, profiles }, jwtSecret);
|
|
16
|
+
*
|
|
17
|
+
* // Postgres mode (requires pgcrypto + auth SQL functions):
|
|
18
|
+
* const auth = createAuthHandlers(db, { users, profiles }, jwtSecret, { mode: 'postgres' });
|
|
19
|
+
*
|
|
20
|
+
* With DB-backed refresh tokens (recommended for production):
|
|
21
|
+
* import { sessions } from '@everystack/auth/schema';
|
|
22
|
+
*
|
|
23
|
+
* const auth = createAuthHandlers(db, { users, profiles, sessions }, jwtSecret);
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { createJwt } from './jwt';
|
|
27
|
+
import { hmacNode } from './jwt/hmac-node';
|
|
28
|
+
import { and, eq } from 'drizzle-orm';
|
|
29
|
+
import {
|
|
30
|
+
createRefreshToken,
|
|
31
|
+
rotateRefreshToken,
|
|
32
|
+
revokeAllRefreshTokens,
|
|
33
|
+
TokenError,
|
|
34
|
+
} from './refresh';
|
|
35
|
+
import type { OAuthProvider } from './oauth';
|
|
36
|
+
export type { OAuthProvider, OAuthUserInfo } from './oauth';
|
|
37
|
+
export { createAppleProvider, createGoogleProvider } from './oauth';
|
|
38
|
+
import { createPostgresAuthHandlers } from './postgres';
|
|
39
|
+
import { getRefreshFromCookies } from './cookies';
|
|
40
|
+
import { authResponse, signoutResponse } from './response';
|
|
41
|
+
import type { CookieConfig } from './response';
|
|
42
|
+
|
|
43
|
+
const TOKEN_EXPIRY_SEC = 3600; // 1h
|
|
44
|
+
const REFRESH_EXPIRY_SEC = 7 * 24 * 3600; // 7d
|
|
45
|
+
|
|
46
|
+
export interface TokenPayload {
|
|
47
|
+
sub: string;
|
|
48
|
+
email: string;
|
|
49
|
+
role: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface AuthHandlers {
|
|
53
|
+
signup: (request: Request) => Promise<Response>;
|
|
54
|
+
signin: (request: Request) => Promise<Response>;
|
|
55
|
+
refresh: (request: Request) => Promise<Response>;
|
|
56
|
+
signout: (request: Request) => Promise<Response>;
|
|
57
|
+
oauth?: (request: Request) => Promise<Response>;
|
|
58
|
+
verifyToken: (token: string) => Promise<TokenPayload | null>;
|
|
59
|
+
revokeRefreshTokens: (userId: string) => Promise<void>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface AuthOptions {
|
|
63
|
+
mode?: 'node' | 'postgres';
|
|
64
|
+
/** postgres.js sql client — required when mode is 'postgres' */
|
|
65
|
+
sql?: any;
|
|
66
|
+
oauth?: { providers: OAuthProvider[] };
|
|
67
|
+
/** When set, auth responses also set HttpOnly cookies. `true` uses defaults. */
|
|
68
|
+
cookies?: CookieConfig;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export {
|
|
72
|
+
hashPassword,
|
|
73
|
+
verifyPassword,
|
|
74
|
+
BCRYPT_COST,
|
|
75
|
+
BCRYPT_MAX_BYTES,
|
|
76
|
+
} from './passwords';
|
|
77
|
+
import { hashPassword, verifyPassword } from './passwords';
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Create JWT auth handlers for signup, signin, refresh, and token verification.
|
|
81
|
+
*
|
|
82
|
+
* When `options.mode` is `'postgres'`, delegates password hashing and JWT signing
|
|
83
|
+
* to PostgreSQL functions via pgcrypto. Requires auth SQL functions from `@everystack/auth/sql`.
|
|
84
|
+
*
|
|
85
|
+
* When `schema.sessions` is provided, refresh tokens are stored as
|
|
86
|
+
* SHA-256 hashes in auth.sessions with one-time-use rotation.
|
|
87
|
+
* When omitted, refresh tokens are stateless JWTs (original behavior).
|
|
88
|
+
*
|
|
89
|
+
* @param db - Drizzle database instance
|
|
90
|
+
* @param schema - Object with `users`, `profiles`, and optional `sessions`/`credentials`/`identities` table references
|
|
91
|
+
* @param jwtSecret - Secret key for JWT signing (Uint8Array)
|
|
92
|
+
* @param options - Mode ('node' | 'postgres') and OAuth config
|
|
93
|
+
*/
|
|
94
|
+
export function createAuthHandlers(
|
|
95
|
+
db: any,
|
|
96
|
+
schema: { users: any; profiles: any; sessions?: any; credentials?: any; identities?: any },
|
|
97
|
+
jwtSecret: Uint8Array,
|
|
98
|
+
options?: AuthOptions
|
|
99
|
+
): AuthHandlers {
|
|
100
|
+
if (options?.mode === 'postgres') {
|
|
101
|
+
return createPostgresAuthHandlers(db, schema, jwtSecret, options);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const { users, profiles } = schema;
|
|
105
|
+
|
|
106
|
+
const accessJwt = createJwt({
|
|
107
|
+
secret: jwtSecret,
|
|
108
|
+
hmac: hmacNode,
|
|
109
|
+
ttlSec: TOKEN_EXPIRY_SEC,
|
|
110
|
+
});
|
|
111
|
+
const refreshJwt = createJwt({
|
|
112
|
+
secret: jwtSecret,
|
|
113
|
+
hmac: hmacNode,
|
|
114
|
+
ttlSec: REFRESH_EXPIRY_SEC,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
async function signToken(payload: TokenPayload): Promise<string> {
|
|
118
|
+
return accessJwt.sign(payload as unknown as Record<string, unknown>);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function signRefreshToken(payload: { sub: string }): Promise<string> {
|
|
122
|
+
return refreshJwt.sign(payload as unknown as Record<string, unknown>);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
verifyToken: async (token: string): Promise<TokenPayload | null> => {
|
|
127
|
+
const claims = await accessJwt.verify(token);
|
|
128
|
+
return (claims as TokenPayload | null) ?? null;
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
signup: async (request: Request): Promise<Response> => {
|
|
132
|
+
const { email, password, username, displayName } = await request.json();
|
|
133
|
+
|
|
134
|
+
if (!email || !password || !username || !displayName) {
|
|
135
|
+
return Response.json(
|
|
136
|
+
{ message: 'email, password, username, and displayName are required' },
|
|
137
|
+
{ status: 400 }
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const existing = await db
|
|
142
|
+
.select()
|
|
143
|
+
.from(users)
|
|
144
|
+
.where(eq(users.email, email))
|
|
145
|
+
.limit(1);
|
|
146
|
+
if (existing.length > 0) {
|
|
147
|
+
return Response.json(
|
|
148
|
+
{ message: 'Email already registered' },
|
|
149
|
+
{ status: 409 }
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const passwordHash = await hashPassword(password);
|
|
154
|
+
const { user, profile } = await db.transaction(async (tx: any) => {
|
|
155
|
+
const [u] = await tx.insert(users).values({ email, passwordHash }).returning();
|
|
156
|
+
const [p] = await tx.insert(profiles).values({ id: u.id, username, displayName }).returning();
|
|
157
|
+
return { user: u, profile: p };
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const token = await signToken({
|
|
161
|
+
sub: user.id,
|
|
162
|
+
email: user.email,
|
|
163
|
+
role: user.role ?? 'authenticated',
|
|
164
|
+
});
|
|
165
|
+
const refreshToken = schema.sessions
|
|
166
|
+
? (await createRefreshToken(db, schema.sessions, user.id)).token
|
|
167
|
+
: await signRefreshToken({ sub: user.id });
|
|
168
|
+
|
|
169
|
+
return authResponse(
|
|
170
|
+
{
|
|
171
|
+
token,
|
|
172
|
+
refreshToken,
|
|
173
|
+
user: {
|
|
174
|
+
id: user.id,
|
|
175
|
+
email: user.email,
|
|
176
|
+
username: profile.username,
|
|
177
|
+
displayName: profile.displayName,
|
|
178
|
+
role: user.role ?? 'authenticated',
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
201,
|
|
182
|
+
options?.cookies,
|
|
183
|
+
);
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
signin: async (request: Request): Promise<Response> => {
|
|
187
|
+
const { email, password } = await request.json();
|
|
188
|
+
|
|
189
|
+
if (!email || !password) {
|
|
190
|
+
return Response.json(
|
|
191
|
+
{ message: 'email and password are required' },
|
|
192
|
+
{ status: 400 }
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const [user] = await db
|
|
197
|
+
.select()
|
|
198
|
+
.from(users)
|
|
199
|
+
.where(eq(users.email, email))
|
|
200
|
+
.limit(1);
|
|
201
|
+
if (!user) {
|
|
202
|
+
return Response.json(
|
|
203
|
+
{ message: 'Invalid email or password' },
|
|
204
|
+
{ status: 401 }
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const valid = await verifyPassword(password, user.passwordHash);
|
|
209
|
+
if (!valid) {
|
|
210
|
+
return Response.json(
|
|
211
|
+
{ message: 'Invalid email or password' },
|
|
212
|
+
{ status: 401 }
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const [profile] = await db
|
|
217
|
+
.select()
|
|
218
|
+
.from(profiles)
|
|
219
|
+
.where(eq(profiles.id, user.id))
|
|
220
|
+
.limit(1);
|
|
221
|
+
|
|
222
|
+
const token = await signToken({
|
|
223
|
+
sub: user.id,
|
|
224
|
+
email: user.email,
|
|
225
|
+
role: user.role ?? 'authenticated',
|
|
226
|
+
});
|
|
227
|
+
const refreshToken = schema.sessions
|
|
228
|
+
? (await createRefreshToken(db, schema.sessions, user.id)).token
|
|
229
|
+
: await signRefreshToken({ sub: user.id });
|
|
230
|
+
|
|
231
|
+
return authResponse(
|
|
232
|
+
{
|
|
233
|
+
token,
|
|
234
|
+
refreshToken,
|
|
235
|
+
user: {
|
|
236
|
+
id: user.id,
|
|
237
|
+
email: user.email,
|
|
238
|
+
username: profile?.username,
|
|
239
|
+
displayName: profile?.displayName,
|
|
240
|
+
role: user.role ?? 'authenticated',
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
200,
|
|
244
|
+
options?.cookies,
|
|
245
|
+
);
|
|
246
|
+
},
|
|
247
|
+
|
|
248
|
+
refresh: async (request: Request): Promise<Response> => {
|
|
249
|
+
// Read refresh token from JSON body (mobile) or cookie (web SSR)
|
|
250
|
+
let refreshToken: string | undefined;
|
|
251
|
+
try {
|
|
252
|
+
const body = await request.clone().json();
|
|
253
|
+
refreshToken = body?.refreshToken;
|
|
254
|
+
} catch {
|
|
255
|
+
// Empty body is fine — fall through to cookie
|
|
256
|
+
}
|
|
257
|
+
if (!refreshToken) {
|
|
258
|
+
refreshToken = getRefreshFromCookies(request.headers.get('cookie')) ?? undefined;
|
|
259
|
+
}
|
|
260
|
+
if (!refreshToken) {
|
|
261
|
+
return Response.json(
|
|
262
|
+
{ message: 'refreshToken is required' },
|
|
263
|
+
{ status: 400 }
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (schema.sessions) {
|
|
268
|
+
// DB-backed rotation path
|
|
269
|
+
try {
|
|
270
|
+
const result = await rotateRefreshToken(db, schema.sessions, refreshToken);
|
|
271
|
+
|
|
272
|
+
const [user] = await db
|
|
273
|
+
.select()
|
|
274
|
+
.from(users)
|
|
275
|
+
.where(eq(users.id, result.userId))
|
|
276
|
+
.limit(1);
|
|
277
|
+
if (!user) {
|
|
278
|
+
await revokeAllRefreshTokens(db, schema.sessions, result.userId);
|
|
279
|
+
return Response.json({ message: 'User not found' }, { status: 401 });
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const token = await signToken({
|
|
283
|
+
sub: user.id,
|
|
284
|
+
email: user.email,
|
|
285
|
+
role: user.role ?? 'authenticated',
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
return authResponse(
|
|
289
|
+
{ token, refreshToken: result.token },
|
|
290
|
+
200,
|
|
291
|
+
options?.cookies,
|
|
292
|
+
);
|
|
293
|
+
} catch (err) {
|
|
294
|
+
if (err instanceof TokenError) {
|
|
295
|
+
return Response.json(
|
|
296
|
+
{ message: 'Invalid or expired refresh token' },
|
|
297
|
+
{ status: 401 }
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
throw err;
|
|
301
|
+
}
|
|
302
|
+
} else {
|
|
303
|
+
// Legacy JWT-based path
|
|
304
|
+
try {
|
|
305
|
+
const payload = await refreshJwt.verify(refreshToken);
|
|
306
|
+
if (!payload) {
|
|
307
|
+
return Response.json({ message: 'Invalid refresh token' }, { status: 401 });
|
|
308
|
+
}
|
|
309
|
+
const userId = payload.sub as string;
|
|
310
|
+
|
|
311
|
+
const [user] = await db
|
|
312
|
+
.select()
|
|
313
|
+
.from(users)
|
|
314
|
+
.where(eq(users.id, userId))
|
|
315
|
+
.limit(1);
|
|
316
|
+
if (!user) {
|
|
317
|
+
return Response.json({ message: 'User not found' }, { status: 401 });
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const token = await signToken({
|
|
321
|
+
sub: user.id,
|
|
322
|
+
email: user.email,
|
|
323
|
+
role: user.role ?? 'authenticated',
|
|
324
|
+
});
|
|
325
|
+
const newRefreshToken = await signRefreshToken({ sub: user.id });
|
|
326
|
+
|
|
327
|
+
return authResponse(
|
|
328
|
+
{ token, refreshToken: newRefreshToken },
|
|
329
|
+
200,
|
|
330
|
+
options?.cookies,
|
|
331
|
+
);
|
|
332
|
+
} catch {
|
|
333
|
+
return Response.json(
|
|
334
|
+
{ message: 'Invalid or expired refresh token' },
|
|
335
|
+
{ status: 401 }
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
|
|
341
|
+
oauth: options?.oauth?.providers.length
|
|
342
|
+
? async (request: Request): Promise<Response> => {
|
|
343
|
+
if (!schema.credentials || !schema.identities) {
|
|
344
|
+
return Response.json(
|
|
345
|
+
{ message: 'OAuth not configured: credentials and identities tables required in schema' },
|
|
346
|
+
{ status: 500 }
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
let body: { provider?: string; token?: string };
|
|
351
|
+
try {
|
|
352
|
+
body = await request.json();
|
|
353
|
+
} catch {
|
|
354
|
+
return Response.json(
|
|
355
|
+
{ message: 'Invalid request body' },
|
|
356
|
+
{ status: 400 }
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const { provider: providerName, token } = body;
|
|
361
|
+
if (!providerName || !token) {
|
|
362
|
+
return Response.json(
|
|
363
|
+
{ message: 'provider and token are required' },
|
|
364
|
+
{ status: 400 }
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const provider = options!.oauth!.providers.find(
|
|
369
|
+
(p) => p.name === providerName
|
|
370
|
+
);
|
|
371
|
+
if (!provider) {
|
|
372
|
+
return Response.json(
|
|
373
|
+
{ message: `Unsupported provider: ${providerName}` },
|
|
374
|
+
{ status: 400 }
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const userInfo = await provider.verify(token);
|
|
379
|
+
if (!userInfo) {
|
|
380
|
+
return Response.json(
|
|
381
|
+
{ message: 'Token verification failed' },
|
|
382
|
+
{ status: 401 }
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Look up existing OAuth credential by (type='oauth', credentialId=providerUid)
|
|
387
|
+
// Join credentials → identities to get userId
|
|
388
|
+
const existingResults = await db
|
|
389
|
+
.select({
|
|
390
|
+
credentialId: schema.credentials.id,
|
|
391
|
+
userId: schema.identities.userId,
|
|
392
|
+
})
|
|
393
|
+
.from(schema.credentials)
|
|
394
|
+
.innerJoin(schema.identities, eq(schema.credentials.identityId, schema.identities.id))
|
|
395
|
+
.where(and(
|
|
396
|
+
eq(schema.credentials.type, 'oauth'),
|
|
397
|
+
eq(schema.credentials.credentialId, userInfo.uid),
|
|
398
|
+
))
|
|
399
|
+
.limit(1);
|
|
400
|
+
|
|
401
|
+
const existingAccount = existingResults[0];
|
|
402
|
+
|
|
403
|
+
if (existingAccount) {
|
|
404
|
+
// Existing linked account — sign in the associated user
|
|
405
|
+
const [existingUser] = await db
|
|
406
|
+
.select()
|
|
407
|
+
.from(users)
|
|
408
|
+
.where(eq(users.id, existingAccount.userId))
|
|
409
|
+
.limit(1);
|
|
410
|
+
|
|
411
|
+
const [profile] = await db
|
|
412
|
+
.select()
|
|
413
|
+
.from(profiles)
|
|
414
|
+
.where(eq(profiles.id, existingAccount.userId))
|
|
415
|
+
.limit(1);
|
|
416
|
+
|
|
417
|
+
const accessToken = await signToken({
|
|
418
|
+
sub: existingUser.id,
|
|
419
|
+
email: existingUser.email,
|
|
420
|
+
role: existingUser.role ?? 'authenticated',
|
|
421
|
+
});
|
|
422
|
+
const refreshToken = schema.sessions
|
|
423
|
+
? (await createRefreshToken(db, schema.sessions, existingUser.id)).token
|
|
424
|
+
: await signRefreshToken({ sub: existingUser.id });
|
|
425
|
+
|
|
426
|
+
return authResponse(
|
|
427
|
+
{
|
|
428
|
+
token: accessToken,
|
|
429
|
+
refreshToken,
|
|
430
|
+
user: {
|
|
431
|
+
id: existingUser.id,
|
|
432
|
+
email: existingUser.email,
|
|
433
|
+
username: profile?.username,
|
|
434
|
+
displayName: profile?.displayName,
|
|
435
|
+
avatarUrl: profile?.avatarUrl,
|
|
436
|
+
role: existingUser.role ?? 'authenticated',
|
|
437
|
+
},
|
|
438
|
+
},
|
|
439
|
+
200,
|
|
440
|
+
options?.cookies,
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// New user — create account + identity + OAuth credential
|
|
445
|
+
const displayName = [userInfo.firstName, userInfo.lastName]
|
|
446
|
+
.filter(Boolean)
|
|
447
|
+
.join(' ') || userInfo.email.split('@')[0];
|
|
448
|
+
const username = userInfo.email.split('@')[0];
|
|
449
|
+
|
|
450
|
+
const { user, profile } = await db.transaction(async (tx: any) => {
|
|
451
|
+
const [u] = await tx
|
|
452
|
+
.insert(users)
|
|
453
|
+
.values({ email: userInfo.email, passwordHash: null })
|
|
454
|
+
.returning();
|
|
455
|
+
|
|
456
|
+
const [ident] = await tx
|
|
457
|
+
.insert(schema.identities)
|
|
458
|
+
.values({
|
|
459
|
+
type: 'user',
|
|
460
|
+
userId: u.id,
|
|
461
|
+
name: userInfo.email,
|
|
462
|
+
})
|
|
463
|
+
.returning();
|
|
464
|
+
|
|
465
|
+
await tx
|
|
466
|
+
.insert(schema.credentials)
|
|
467
|
+
.values({
|
|
468
|
+
type: 'oauth',
|
|
469
|
+
identityId: ident.id,
|
|
470
|
+
credentialId: userInfo.uid,
|
|
471
|
+
name: providerName,
|
|
472
|
+
metadata: { email: userInfo.email },
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
const [p] = await tx
|
|
476
|
+
.insert(profiles)
|
|
477
|
+
.values({
|
|
478
|
+
id: u.id,
|
|
479
|
+
username,
|
|
480
|
+
displayName,
|
|
481
|
+
avatarUrl: userInfo.avatarUrl || null,
|
|
482
|
+
})
|
|
483
|
+
.returning();
|
|
484
|
+
|
|
485
|
+
return { user: u, profile: p };
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
const accessToken = await signToken({
|
|
489
|
+
sub: user.id,
|
|
490
|
+
email: user.email,
|
|
491
|
+
role: user.role ?? 'authenticated',
|
|
492
|
+
});
|
|
493
|
+
const refreshToken = schema.sessions
|
|
494
|
+
? (await createRefreshToken(db, schema.sessions, user.id)).token
|
|
495
|
+
: await signRefreshToken({ sub: user.id });
|
|
496
|
+
|
|
497
|
+
return authResponse(
|
|
498
|
+
{
|
|
499
|
+
token: accessToken,
|
|
500
|
+
refreshToken,
|
|
501
|
+
user: {
|
|
502
|
+
id: user.id,
|
|
503
|
+
email: user.email,
|
|
504
|
+
username: profile.username,
|
|
505
|
+
displayName: profile.displayName,
|
|
506
|
+
avatarUrl: profile.avatarUrl,
|
|
507
|
+
role: user.role ?? 'authenticated',
|
|
508
|
+
},
|
|
509
|
+
},
|
|
510
|
+
200,
|
|
511
|
+
options?.cookies,
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
: undefined,
|
|
515
|
+
|
|
516
|
+
signout: async (_request: Request): Promise<Response> => {
|
|
517
|
+
return signoutResponse(options?.cookies);
|
|
518
|
+
},
|
|
519
|
+
|
|
520
|
+
revokeRefreshTokens: async (userId: string): Promise<void> => {
|
|
521
|
+
if (schema.sessions) {
|
|
522
|
+
await revokeAllRefreshTokens(db, schema.sessions, userId);
|
|
523
|
+
}
|
|
524
|
+
},
|
|
525
|
+
};
|
|
526
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { createHmac } from 'node:crypto';
|
|
2
|
+
import type { HmacFn } from './hmac';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* HMAC-SHA256 via node:crypto. Synchronous under the hood; wrapped in a
|
|
6
|
+
* resolved promise to satisfy the async HmacFn contract.
|
|
7
|
+
*/
|
|
8
|
+
export const hmacNode: HmacFn = async (key, message) => {
|
|
9
|
+
const mac = createHmac('sha256', key).update(message).digest();
|
|
10
|
+
return new Uint8Array(mac.buffer, mac.byteOffset, mac.byteLength);
|
|
11
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { HmacFn } from './hmac';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* HMAC-SHA256 via Web Crypto (SubtleCrypto). Works in browsers, Workers,
|
|
5
|
+
* Deno, Bun, and Node 20+ (where SubtleCrypto is exposed on globalThis.crypto).
|
|
6
|
+
*/
|
|
7
|
+
export const hmacWeb: HmacFn = async (key, message) => {
|
|
8
|
+
const cryptoKey = await crypto.subtle.importKey(
|
|
9
|
+
'raw',
|
|
10
|
+
key as BufferSource,
|
|
11
|
+
{ name: 'HMAC', hash: 'SHA-256' },
|
|
12
|
+
false,
|
|
13
|
+
['sign']
|
|
14
|
+
);
|
|
15
|
+
const sig = await crypto.subtle.sign('HMAC', cryptoKey, message as BufferSource);
|
|
16
|
+
return new Uint8Array(sig);
|
|
17
|
+
};
|
package/src/jwt/hmac.ts
ADDED