@fonderie/auth 1.2.0 → 1.3.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.
@@ -35,6 +35,7 @@ __export(middlewares_exports, {
35
35
  module.exports = __toCommonJS(middlewares_exports);
36
36
 
37
37
  // src/services/jwt.ts
38
+ var import_node_crypto = require("crypto");
38
39
  var import_jsonwebtoken = __toESM(require("jsonwebtoken"), 1);
39
40
  function verifyToken(token, config) {
40
41
  try {
@@ -254,9 +255,44 @@ var UserModel = class {
254
255
  }
255
256
  };
256
257
 
258
+ // src/models/session.model.ts
259
+ var SessionModel = class {
260
+ constructor(store) {
261
+ this.store = store;
262
+ }
263
+ store;
264
+ async create(userId, token, expiresAt, sid) {
265
+ await this.store.query(
266
+ `INSERT INTO fonderie_sessions (user_id, token, expires_at, sid)
267
+ VALUES ($1, $2, $3, $4)
268
+ ON CONFLICT (token) DO NOTHING`,
269
+ [userId, token, expiresAt, sid ?? null]
270
+ );
271
+ }
272
+ async delete(token) {
273
+ await this.store.query(`DELETE FROM fonderie_sessions WHERE token = $1`, [token]);
274
+ }
275
+ async exists(token) {
276
+ const rows = await this.store.query(
277
+ `SELECT id FROM fonderie_sessions WHERE token = $1 AND expires_at > now()`,
278
+ [token]
279
+ );
280
+ return rows.length > 0;
281
+ }
282
+ // Liveness check for session-bound access tokens (by the sid claim).
283
+ async aliveBySid(sid) {
284
+ const rows = await this.store.query(
285
+ `SELECT id FROM fonderie_sessions WHERE sid = $1 AND expires_at > now()`,
286
+ [sid]
287
+ );
288
+ return rows.length > 0;
289
+ }
290
+ };
291
+
257
292
  // src/middlewares/session.ts
258
293
  function withSession(store, config) {
259
294
  const users = new UserModel(store);
295
+ const sessions = new SessionModel(store);
260
296
  return async (ctx, next) => {
261
297
  const token = extractToken(ctx.request);
262
298
  if (!token) {
@@ -266,6 +302,9 @@ function withSession(store, config) {
266
302
  if (!payload || payload.type !== "access") {
267
303
  return next();
268
304
  }
305
+ if (payload.sid && !await sessions.aliveBySid(payload.sid)) {
306
+ return next();
307
+ }
269
308
  const user = await users.findById(payload.sub);
270
309
  if (!user || user.suspended || user.deletedAt) {
271
310
  return next();
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/middlewares/index.ts","../../src/services/jwt.ts","../../src/models/user.model.ts","../../src/middlewares/session.ts","../../src/middlewares/require-auth.ts"],"sourcesContent":["import { withSession } from './session';\nimport { requireAuth } from './require-auth';\n\nconst middlewares = {\n\trequireAuth,\n\twithSession,\n};\n\nexport default middlewares;\n","import jwt, { type SignOptions } from 'jsonwebtoken';\n\nimport type { IAuthConfig } from '../config';\n\nexport interface TokenPair {\n\taccessToken: string;\n\trefreshToken: string;\n}\n\nexport interface IAccessPayload {\n\tsub: string; // userId\n\ttype: 'access';\n\tloginMethod: 'email' | 'phone' | 'google';\n\tphoneVerified: boolean;\n\tmfaPending?: boolean;\n}\n\nexport interface IRefreshPayload {\n\tsub: string;\n\ttype: 'refresh';\n\tloginMethod: 'email' | 'phone' | 'google';\n\tphoneVerified: boolean;\n}\n\nexport interface ITokenOptions {\n\tloginMethod: 'email' | 'phone' | 'google';\n\tphoneVerified?: boolean;\n}\n\nexport function issueMfaPendingToken(\n\tuserId: string,\n\tconfig: IAuthConfig,\n\tloginMethod: 'email' | 'phone',\n): string {\n\treturn jwt.sign(\n\t\t{\n\t\t\tsub: userId,\n\t\t\ttype: 'access',\n\t\t\tloginMethod,\n\t\t\tphoneVerified: false,\n\t\t\tmfaPending: true,\n\t\t} satisfies IAccessPayload,\n\t\tconfig.jwtSecret,\n\t\t{ expiresIn: '5m' },\n\t);\n}\n\nexport function issueTokenPair(\n\tuserId: string,\n\tconfig: IAuthConfig,\n\toptions: ITokenOptions,\n): TokenPair {\n\tconst duration = config.sessionDuration ?? '7d';\n\tconst loginMethod = options.loginMethod;\n\tconst phoneVerified = options.phoneVerified ?? false;\n\n\tconst accessToken = jwt.sign(\n\t\t{ sub: userId, type: 'access', loginMethod, phoneVerified } satisfies IAccessPayload,\n\t\tconfig.jwtSecret,\n\t\t{ expiresIn: '24h' },\n\t);\n\n\tconst refreshToken = jwt.sign(\n\t\t{ sub: userId, type: 'refresh', loginMethod, phoneVerified } satisfies IRefreshPayload,\n\t\tconfig.jwtSecret,\n\t\t{ expiresIn: duration } as SignOptions,\n\t);\n\n\treturn { accessToken, refreshToken };\n}\n\nexport function refreshTokenExpiry(token: string): Date {\n\tconst decoded = jwt.decode(token) as { exp?: number } | null;\n\treturn decoded?.exp\n\t\t? new Date(decoded.exp * 1000)\n\t\t: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);\n}\n\nexport function verifyToken(\n\ttoken: string,\n\tconfig: IAuthConfig,\n): IAccessPayload | IRefreshPayload | null {\n\ttry {\n\t\treturn jwt.verify(token, config.jwtSecret) as IAccessPayload | IRefreshPayload;\n\t} catch {\n\t\treturn null;\n\t}\n}\n","import type { IStoreAdapter } from '@fonderie/store';\nimport type { IUser } from '../types';\n\nexport interface IUserUpdateFields {\n\tfirstName?: string;\n\tlastName?: string;\n\tphoneNumber?: string;\n\tavatarUrl?: string;\n\tlocale?: string;\n\ttimezone?: string;\n\tpreferences?: unknown;\n}\n\nconst USER_COLUMNS = `\n\tid,\n\temail,\n\tpassword_hash AS \"passwordHash\",\n\tfirst_name AS \"firstName\",\n\tlast_name AS \"lastName\",\n\tphone,\n\tprofile_image_url AS \"profileImageUrl\",\n\tlocale,\n\ttimezone,\n\tis_active AS \"isActive\",\n\tlast_login AS \"lastLogin\",\n\tpreferences,\n\tsuspended,\n\twhitelist,\n\tip_whitelist AS \"ipWhitelist\",\n\tmfa_enabled AS \"mfaEnabled\",\n\tmfa_secret AS \"mfaSecret\",\n\temail_verified_at AS \"emailVerifiedAt\",\n\tdeleted_at AS \"deletedAt\",\n\tcreated_at AS \"createdAt\",\n\tupdated_at AS \"updatedAt\"\n`;\n\nexport class UserModel {\n\tconstructor(private store: IStoreAdapter) {}\n\n\tasync findById(id: string): Promise<IUser | null> {\n\t\tconst [row] = await this.store.query<IUser>(\n\t\t\t`SELECT ${USER_COLUMNS} FROM fonderie_users WHERE id = $1 AND deleted_at IS NULL`,\n\t\t\t[id],\n\t\t);\n\t\treturn row ?? null;\n\t}\n\n\tasync findByEmail(email: string): Promise<IUser | null> {\n\t\tconst [row] = await this.store.query<IUser>(\n\t\t\t`SELECT ${USER_COLUMNS} FROM fonderie_users WHERE email = $1 AND deleted_at IS NULL`,\n\t\t\t[email],\n\t\t);\n\t\treturn row ?? null;\n\t}\n\n\tasync findByPhone(phone: string): Promise<IUser | null> {\n\t\tconst [row] = await this.store.query<IUser>(\n\t\t\t`SELECT ${USER_COLUMNS} FROM fonderie_users WHERE phone = $1 AND deleted_at IS NULL`,\n\t\t\t[phone],\n\t\t);\n\t\treturn row ?? null;\n\t}\n\n\tasync findOrCreateByPhone(\n\t\tphone: string,\n\t\tfirstName: string | null = null,\n\t\tlastName: string | null = null,\n\t): Promise<{ id: string }> {\n\t\tconst [row] = await this.store.query<{ id: string }>(\n\t\t\t`INSERT INTO fonderie_users (phone, first_name, last_name)\n\t\t\tVALUES ($1, $2, $3)\n\t\t\tON CONFLICT (phone) DO UPDATE\n\t\t\tSET first_name = COALESCE(EXCLUDED.first_name, fonderie_users.first_name),\n\t\t\t last_name = COALESCE(EXCLUDED.last_name, fonderie_users.last_name),\n\t\t\t updated_at = now()\n\t\t\tRETURNING id`,\n\t\t\t[phone, firstName, lastName],\n\t\t);\n\t\treturn row!;\n\t}\n\n\tasync create(\n\t\temail: string,\n\t\tpasswordHash: string,\n\t\tfirstName: string | null,\n\t\tlastName: string | null,\n\t): Promise<{ id: string } | null> {\n\t\tconst [row] = await this.store.query<{ id: string }>(\n\t\t\t`INSERT INTO fonderie_users (email, password_hash, first_name, last_name)\n\t\t\tVALUES ($1, $2, $3, $4)\n\t\t\tRETURNING id`,\n\t\t\t[email.toLowerCase().trim(), passwordHash, firstName, lastName],\n\t\t);\n\t\treturn row ?? null;\n\t}\n\n\tasync update(id: string, fields: IUserUpdateFields): Promise<{ id: string } | null> {\n\t\tconst columnMap: Record<string, string> = {\n\t\t\tfirstName: 'first_name',\n\t\t\tlastName: 'last_name',\n\t\t\tphoneNumber: 'phone',\n\t\t\tavatarUrl: 'profile_image_url',\n\t\t\tlocale: 'locale',\n\t\t\ttimezone: 'timezone',\n\t\t\tpreferences: 'preferences',\n\t\t};\n\n\t\tconst sets: string[] = [];\n\t\tconst values: unknown[] = [];\n\n\t\tfor (const [key, col] of Object.entries(columnMap)) {\n\t\t\tif ((fields as Record<string, unknown>)[key] !== undefined) {\n\t\t\t\tvalues.push((fields as Record<string, unknown>)[key]);\n\t\t\t\tsets.push(`${col} = $${values.length}`);\n\t\t\t}\n\t\t}\n\n\t\tvalues.push(id);\n\t\tconst [row] = await this.store.query<{ id: string }>(\n\t\t\t`UPDATE fonderie_users SET ${sets.join(', ')}, updated_at = now() WHERE id = $${values.length} AND deleted_at IS NULL RETURNING id`,\n\t\t\tvalues,\n\t\t);\n\t\treturn row ?? null;\n\t}\n\n\tasync updatePassword(id: string, passwordHash: string): Promise<void> {\n\t\tawait this.store.query(`UPDATE fonderie_users SET password_hash = $1 WHERE id = $2`, [\n\t\t\tpasswordHash,\n\t\t\tid,\n\t\t]);\n\t}\n\n\tasync markEmailVerified(id: string): Promise<void> {\n\t\tawait this.store.query(\n\t\t\t`UPDATE fonderie_users SET email_verified_at = now(), updated_at = now() WHERE id = $1`,\n\t\t\t[id],\n\t\t);\n\t}\n\n\tasync softDelete(id: string): Promise<void> {\n\t\tawait this.store.query(\n\t\t\t`UPDATE fonderie_users SET deleted_at = now(), updated_at = now() WHERE id = $1`,\n\t\t\t[id],\n\t\t);\n\t}\n\n\tasync saveMfaSecret(id: string, secret: string): Promise<void> {\n\t\tawait this.store.query(`UPDATE fonderie_users SET mfa_secret = $1 WHERE id = $2`, [secret, id]);\n\t}\n\n\tasync saveMfaPendingSecret(id: string, secret: string): Promise<void> {\n\t\tawait this.store.query(\n\t\t\t`UPDATE fonderie_users\n\t\t\t SET mfa_secret_pending = $1, mfa_secret_pending_expires_at = now() + interval '15 minutes'\n\t\t\t WHERE id = $2`,\n\t\t\t[secret, id],\n\t\t);\n\t}\n\n\tasync getMfaPendingSecret(id: string): Promise<string | null> {\n\t\tconst [row] = await this.store.query<{ mfa_secret_pending: string | null }>(\n\t\t\t`SELECT mfa_secret_pending FROM fonderie_users\n\t\t\t WHERE id = $1 AND mfa_secret_pending_expires_at > now()`,\n\t\t\t[id],\n\t\t);\n\t\treturn row?.mfa_secret_pending ?? null;\n\t}\n\n\tasync confirmMfaSecret(id: string): Promise<void> {\n\t\tawait this.store.query(\n\t\t\t`UPDATE fonderie_users\n\t\t\t SET mfa_secret = mfa_secret_pending,\n\t\t\t mfa_secret_pending = NULL,\n\t\t\t mfa_secret_pending_expires_at = NULL,\n\t\t\t mfa_enabled = true,\n\t\t\t updated_at = now()\n\t\t\t WHERE id = $1`,\n\t\t\t[id],\n\t\t);\n\t}\n\n\tasync enableMfa(id: string): Promise<void> {\n\t\tawait this.store.query(`UPDATE fonderie_users SET mfa_enabled = true WHERE id = $1`, [id]);\n\t}\n\n\tasync disableMfa(id: string): Promise<void> {\n\t\tawait this.store.query(\n\t\t\t`UPDATE fonderie_users SET mfa_enabled = false, mfa_secret = NULL, updated_at = now() WHERE id = $1`,\n\t\t\t[id],\n\t\t);\n\t}\n\n\tasync updateEmail(id: string, email: string): Promise<void> {\n\t\tawait this.store.query(\n\t\t\t`UPDATE fonderie_users SET email = $1, email_verified_at = NULL, updated_at = now() WHERE id = $2`,\n\t\t\t[email.toLowerCase().trim(), id],\n\t\t);\n\t}\n\n\tasync updatePhone(id: string, phone: string): Promise<void> {\n\t\tawait this.store.query(\n\t\t\t`UPDATE fonderie_users SET phone = $1, updated_at = now() WHERE id = $2`,\n\t\t\t[phone, id],\n\t\t);\n\t}\n\n\tasync updatePreferences(\n\t\tid: string,\n\t\tfields: {\n\t\t\tlocale?: string;\n\t\t\ttimezone?: string;\n\t\t\tpatch?: Record<string, unknown>;\n\t\t},\n\t): Promise<{ id: string } | null> {\n\t\tconst sets: string[] = [];\n\t\tconst values: unknown[] = [];\n\n\t\tif (fields.locale !== undefined) {\n\t\t\tvalues.push(fields.locale);\n\t\t\tsets.push(`locale = $${values.length}`);\n\t\t}\n\t\tif (fields.timezone !== undefined) {\n\t\t\tvalues.push(fields.timezone);\n\t\t\tsets.push(`timezone = $${values.length}`);\n\t\t}\n\t\tif (fields.patch && Object.keys(fields.patch).length > 0) {\n\t\t\tvalues.push(JSON.stringify(fields.patch));\n\t\t\tsets.push(`preferences = preferences || $${values.length}::jsonb`);\n\t\t}\n\n\t\tif (sets.length === 0) return null;\n\n\t\tvalues.push(id);\n\t\tconst [row] = await this.store.query<{ id: string }>(\n\t\t\t`UPDATE fonderie_users SET ${sets.join(', ')}, updated_at = now() WHERE id = $${values.length} AND deleted_at IS NULL RETURNING id`,\n\t\t\tvalues,\n\t\t);\n\t\treturn row ?? null;\n\t}\n\n\tasync getMfaSecret(id: string): Promise<string | null> {\n\t\tconst [row] = await this.store.query<{ mfa_secret: string | null }>(\n\t\t\t`SELECT mfa_secret FROM fonderie_users WHERE id = $1`,\n\t\t\t[id],\n\t\t);\n\t\treturn row?.mfa_secret ?? null;\n\t}\n\n\tasync upsertByProvider(\n\t\temail: string,\n\t\tprovider: string,\n\t\tproviderId: string,\n\t): Promise<{ id: string } | null> {\n\t\tconst [row] = await this.store.query<{ id: string }>(\n\t\t\t`INSERT INTO fonderie_users (email, email_verified_at, provider, provider_id)\n\t\t\tVALUES ($1, now(), $2, $3)\n\t\t\tON CONFLICT (email) DO UPDATE\n\t\t\tSET provider = $2, provider_id = $3\n\t\t\tRETURNING id`,\n\t\t\t[email, provider, providerId],\n\t\t);\n\t\treturn row ?? null;\n\t}\n}\n","import type { Middleware } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IAuthConfig } from '../config';\nimport { verifyToken } from '../services/jwt';\nimport type { IAccessPayload } from '../services/jwt';\nimport { UserModel } from '../models/user.model';\n\n// Reads the Bearer token or session cookie, populates ctx.user\n// Does NOT reject — anonymous requests pass through\nexport function withSession(store: IStoreAdapter, config: IAuthConfig): Middleware {\n\tconst users = new UserModel(store);\n\n\treturn async (ctx, next) => {\n\t\tconst token = extractToken(ctx.request);\n\t\tif (!token) {\n\t\t\treturn next();\n\t\t}\n\n\t\tconst payload = verifyToken(token, config);\n\t\tif (!payload || payload.type !== 'access') {\n\t\t\treturn next();\n\t\t}\n\n\t\tconst user = await users.findById(payload.sub);\n\t\tif (!user || user.suspended || user.deletedAt) {\n\t\t\treturn next();\n\t\t}\n\n\t\tObject.assign(ctx, {\n\t\t\tuser: {\n\t\t\t\t...user,\n\t\t\t\tloginMethod: payload.loginMethod ?? 'email',\n\t\t\t\tphoneVerified: payload.phoneVerified ?? false,\n\t\t\t\tmfaPending: (payload as IAccessPayload).mfaPending ?? false,\n\t\t\t},\n\t\t});\n\n\t\treturn next();\n\t};\n}\n\nfunction extractToken(request: Request): string | null {\n\tconst auth = request.headers.get('authorization');\n\tif (auth?.startsWith('Bearer ')) {\n\t\treturn auth.slice(7);\n\t}\n\n\tconst cookie = request.headers.get('cookie') ?? '';\n\tconst match = cookie.match(/(?:^|;\\s*)access_token=([^;]+)/);\n\treturn match?.[1] ?? null;\n}\n","export { requireAuth, requireAnyAuth } from '@fonderie/core/middlewares';\n"],"mappings":"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","names":["jwt"]}
1
+ {"version":3,"sources":["../../src/middlewares/index.ts","../../src/services/jwt.ts","../../src/models/user.model.ts","../../src/models/session.model.ts","../../src/middlewares/session.ts","../../src/middlewares/require-auth.ts"],"sourcesContent":["import { withSession } from './session';\nimport { requireAuth } from './require-auth';\n\nconst middlewares = {\n\trequireAuth,\n\twithSession,\n};\n\nexport default middlewares;\n","import { randomUUID } from 'node:crypto';\nimport jwt, { type SignOptions } from 'jsonwebtoken';\n\nimport type { IAuthConfig } from '../config';\n\nexport interface TokenPair {\n\taccessToken: string;\n\trefreshToken: string;\n\t// Server-side session id both tokens are bound to. Store it on the\n\t// fonderie_sessions row — deleting that row revokes the access token\n\t// immediately, not just the refresh token.\n\tsid: string;\n}\n\nexport interface IAccessPayload {\n\tsub: string; // userId\n\ttype: 'access';\n\tloginMethod: 'email' | 'phone' | 'google';\n\tphoneVerified: boolean;\n\tmfaPending?: boolean;\n\t// Absent only on legacy tokens issued before session binding and on\n\t// short-lived mfaPending tokens (no session exists yet at that point).\n\tsid?: string;\n}\n\nexport interface IRefreshPayload {\n\tsub: string;\n\ttype: 'refresh';\n\tloginMethod: 'email' | 'phone' | 'google';\n\tphoneVerified: boolean;\n\tsid?: string;\n}\n\nexport interface ITokenOptions {\n\tloginMethod: 'email' | 'phone' | 'google';\n\tphoneVerified?: boolean;\n}\n\nexport function issueMfaPendingToken(\n\tuserId: string,\n\tconfig: IAuthConfig,\n\tloginMethod: 'email' | 'phone',\n): string {\n\treturn jwt.sign(\n\t\t{\n\t\t\tsub: userId,\n\t\t\ttype: 'access',\n\t\t\tloginMethod,\n\t\t\tphoneVerified: false,\n\t\t\tmfaPending: true,\n\t\t} satisfies IAccessPayload,\n\t\tconfig.jwtSecret,\n\t\t{ expiresIn: '5m' },\n\t);\n}\n\nexport function issueTokenPair(\n\tuserId: string,\n\tconfig: IAuthConfig,\n\toptions: ITokenOptions,\n): TokenPair {\n\tconst duration = config.sessionDuration ?? '7d';\n\tconst accessDuration = config.accessTokenDuration ?? '24h';\n\tconst loginMethod = options.loginMethod;\n\tconst phoneVerified = options.phoneVerified ?? false;\n\tconst sid = randomUUID();\n\n\tconst accessToken = jwt.sign(\n\t\t{ sub: userId, type: 'access', loginMethod, phoneVerified, sid } satisfies IAccessPayload,\n\t\tconfig.jwtSecret,\n\t\t{ expiresIn: accessDuration } as SignOptions,\n\t);\n\n\tconst refreshToken = jwt.sign(\n\t\t{ sub: userId, type: 'refresh', loginMethod, phoneVerified, sid } satisfies IRefreshPayload,\n\t\tconfig.jwtSecret,\n\t\t{ expiresIn: duration } as SignOptions,\n\t);\n\n\treturn { accessToken, refreshToken, sid };\n}\n\nexport function refreshTokenExpiry(token: string): Date {\n\tconst decoded = jwt.decode(token) as { exp?: number } | null;\n\treturn decoded?.exp\n\t\t? new Date(decoded.exp * 1000)\n\t\t: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);\n}\n\nexport function verifyToken(\n\ttoken: string,\n\tconfig: IAuthConfig,\n): IAccessPayload | IRefreshPayload | null {\n\ttry {\n\t\treturn jwt.verify(token, config.jwtSecret) as IAccessPayload | IRefreshPayload;\n\t} catch {\n\t\treturn null;\n\t}\n}\n","import type { IStoreAdapter } from '@fonderie/store';\nimport type { IUser } from '../types';\n\nexport interface IUserUpdateFields {\n\tfirstName?: string;\n\tlastName?: string;\n\tphoneNumber?: string;\n\tavatarUrl?: string;\n\tlocale?: string;\n\ttimezone?: string;\n\tpreferences?: unknown;\n}\n\nconst USER_COLUMNS = `\n\tid,\n\temail,\n\tpassword_hash AS \"passwordHash\",\n\tfirst_name AS \"firstName\",\n\tlast_name AS \"lastName\",\n\tphone,\n\tprofile_image_url AS \"profileImageUrl\",\n\tlocale,\n\ttimezone,\n\tis_active AS \"isActive\",\n\tlast_login AS \"lastLogin\",\n\tpreferences,\n\tsuspended,\n\twhitelist,\n\tip_whitelist AS \"ipWhitelist\",\n\tmfa_enabled AS \"mfaEnabled\",\n\tmfa_secret AS \"mfaSecret\",\n\temail_verified_at AS \"emailVerifiedAt\",\n\tdeleted_at AS \"deletedAt\",\n\tcreated_at AS \"createdAt\",\n\tupdated_at AS \"updatedAt\"\n`;\n\nexport class UserModel {\n\tconstructor(private store: IStoreAdapter) {}\n\n\tasync findById(id: string): Promise<IUser | null> {\n\t\tconst [row] = await this.store.query<IUser>(\n\t\t\t`SELECT ${USER_COLUMNS} FROM fonderie_users WHERE id = $1 AND deleted_at IS NULL`,\n\t\t\t[id],\n\t\t);\n\t\treturn row ?? null;\n\t}\n\n\tasync findByEmail(email: string): Promise<IUser | null> {\n\t\tconst [row] = await this.store.query<IUser>(\n\t\t\t`SELECT ${USER_COLUMNS} FROM fonderie_users WHERE email = $1 AND deleted_at IS NULL`,\n\t\t\t[email],\n\t\t);\n\t\treturn row ?? null;\n\t}\n\n\tasync findByPhone(phone: string): Promise<IUser | null> {\n\t\tconst [row] = await this.store.query<IUser>(\n\t\t\t`SELECT ${USER_COLUMNS} FROM fonderie_users WHERE phone = $1 AND deleted_at IS NULL`,\n\t\t\t[phone],\n\t\t);\n\t\treturn row ?? null;\n\t}\n\n\tasync findOrCreateByPhone(\n\t\tphone: string,\n\t\tfirstName: string | null = null,\n\t\tlastName: string | null = null,\n\t): Promise<{ id: string }> {\n\t\tconst [row] = await this.store.query<{ id: string }>(\n\t\t\t`INSERT INTO fonderie_users (phone, first_name, last_name)\n\t\t\tVALUES ($1, $2, $3)\n\t\t\tON CONFLICT (phone) DO UPDATE\n\t\t\tSET first_name = COALESCE(EXCLUDED.first_name, fonderie_users.first_name),\n\t\t\t last_name = COALESCE(EXCLUDED.last_name, fonderie_users.last_name),\n\t\t\t updated_at = now()\n\t\t\tRETURNING id`,\n\t\t\t[phone, firstName, lastName],\n\t\t);\n\t\treturn row!;\n\t}\n\n\tasync create(\n\t\temail: string,\n\t\tpasswordHash: string,\n\t\tfirstName: string | null,\n\t\tlastName: string | null,\n\t): Promise<{ id: string } | null> {\n\t\tconst [row] = await this.store.query<{ id: string }>(\n\t\t\t`INSERT INTO fonderie_users (email, password_hash, first_name, last_name)\n\t\t\tVALUES ($1, $2, $3, $4)\n\t\t\tRETURNING id`,\n\t\t\t[email.toLowerCase().trim(), passwordHash, firstName, lastName],\n\t\t);\n\t\treturn row ?? null;\n\t}\n\n\tasync update(id: string, fields: IUserUpdateFields): Promise<{ id: string } | null> {\n\t\tconst columnMap: Record<string, string> = {\n\t\t\tfirstName: 'first_name',\n\t\t\tlastName: 'last_name',\n\t\t\tphoneNumber: 'phone',\n\t\t\tavatarUrl: 'profile_image_url',\n\t\t\tlocale: 'locale',\n\t\t\ttimezone: 'timezone',\n\t\t\tpreferences: 'preferences',\n\t\t};\n\n\t\tconst sets: string[] = [];\n\t\tconst values: unknown[] = [];\n\n\t\tfor (const [key, col] of Object.entries(columnMap)) {\n\t\t\tif ((fields as Record<string, unknown>)[key] !== undefined) {\n\t\t\t\tvalues.push((fields as Record<string, unknown>)[key]);\n\t\t\t\tsets.push(`${col} = $${values.length}`);\n\t\t\t}\n\t\t}\n\n\t\tvalues.push(id);\n\t\tconst [row] = await this.store.query<{ id: string }>(\n\t\t\t`UPDATE fonderie_users SET ${sets.join(', ')}, updated_at = now() WHERE id = $${values.length} AND deleted_at IS NULL RETURNING id`,\n\t\t\tvalues,\n\t\t);\n\t\treturn row ?? null;\n\t}\n\n\tasync updatePassword(id: string, passwordHash: string): Promise<void> {\n\t\tawait this.store.query(`UPDATE fonderie_users SET password_hash = $1 WHERE id = $2`, [\n\t\t\tpasswordHash,\n\t\t\tid,\n\t\t]);\n\t}\n\n\tasync markEmailVerified(id: string): Promise<void> {\n\t\tawait this.store.query(\n\t\t\t`UPDATE fonderie_users SET email_verified_at = now(), updated_at = now() WHERE id = $1`,\n\t\t\t[id],\n\t\t);\n\t}\n\n\tasync softDelete(id: string): Promise<void> {\n\t\tawait this.store.query(\n\t\t\t`UPDATE fonderie_users SET deleted_at = now(), updated_at = now() WHERE id = $1`,\n\t\t\t[id],\n\t\t);\n\t}\n\n\tasync saveMfaSecret(id: string, secret: string): Promise<void> {\n\t\tawait this.store.query(`UPDATE fonderie_users SET mfa_secret = $1 WHERE id = $2`, [secret, id]);\n\t}\n\n\tasync saveMfaPendingSecret(id: string, secret: string): Promise<void> {\n\t\tawait this.store.query(\n\t\t\t`UPDATE fonderie_users\n\t\t\t SET mfa_secret_pending = $1, mfa_secret_pending_expires_at = now() + interval '15 minutes'\n\t\t\t WHERE id = $2`,\n\t\t\t[secret, id],\n\t\t);\n\t}\n\n\tasync getMfaPendingSecret(id: string): Promise<string | null> {\n\t\tconst [row] = await this.store.query<{ mfa_secret_pending: string | null }>(\n\t\t\t`SELECT mfa_secret_pending FROM fonderie_users\n\t\t\t WHERE id = $1 AND mfa_secret_pending_expires_at > now()`,\n\t\t\t[id],\n\t\t);\n\t\treturn row?.mfa_secret_pending ?? null;\n\t}\n\n\tasync confirmMfaSecret(id: string): Promise<void> {\n\t\tawait this.store.query(\n\t\t\t`UPDATE fonderie_users\n\t\t\t SET mfa_secret = mfa_secret_pending,\n\t\t\t mfa_secret_pending = NULL,\n\t\t\t mfa_secret_pending_expires_at = NULL,\n\t\t\t mfa_enabled = true,\n\t\t\t updated_at = now()\n\t\t\t WHERE id = $1`,\n\t\t\t[id],\n\t\t);\n\t}\n\n\tasync enableMfa(id: string): Promise<void> {\n\t\tawait this.store.query(`UPDATE fonderie_users SET mfa_enabled = true WHERE id = $1`, [id]);\n\t}\n\n\tasync disableMfa(id: string): Promise<void> {\n\t\tawait this.store.query(\n\t\t\t`UPDATE fonderie_users SET mfa_enabled = false, mfa_secret = NULL, updated_at = now() WHERE id = $1`,\n\t\t\t[id],\n\t\t);\n\t}\n\n\tasync updateEmail(id: string, email: string): Promise<void> {\n\t\tawait this.store.query(\n\t\t\t`UPDATE fonderie_users SET email = $1, email_verified_at = NULL, updated_at = now() WHERE id = $2`,\n\t\t\t[email.toLowerCase().trim(), id],\n\t\t);\n\t}\n\n\tasync updatePhone(id: string, phone: string): Promise<void> {\n\t\tawait this.store.query(\n\t\t\t`UPDATE fonderie_users SET phone = $1, updated_at = now() WHERE id = $2`,\n\t\t\t[phone, id],\n\t\t);\n\t}\n\n\tasync updatePreferences(\n\t\tid: string,\n\t\tfields: {\n\t\t\tlocale?: string;\n\t\t\ttimezone?: string;\n\t\t\tpatch?: Record<string, unknown>;\n\t\t},\n\t): Promise<{ id: string } | null> {\n\t\tconst sets: string[] = [];\n\t\tconst values: unknown[] = [];\n\n\t\tif (fields.locale !== undefined) {\n\t\t\tvalues.push(fields.locale);\n\t\t\tsets.push(`locale = $${values.length}`);\n\t\t}\n\t\tif (fields.timezone !== undefined) {\n\t\t\tvalues.push(fields.timezone);\n\t\t\tsets.push(`timezone = $${values.length}`);\n\t\t}\n\t\tif (fields.patch && Object.keys(fields.patch).length > 0) {\n\t\t\tvalues.push(JSON.stringify(fields.patch));\n\t\t\tsets.push(`preferences = preferences || $${values.length}::jsonb`);\n\t\t}\n\n\t\tif (sets.length === 0) return null;\n\n\t\tvalues.push(id);\n\t\tconst [row] = await this.store.query<{ id: string }>(\n\t\t\t`UPDATE fonderie_users SET ${sets.join(', ')}, updated_at = now() WHERE id = $${values.length} AND deleted_at IS NULL RETURNING id`,\n\t\t\tvalues,\n\t\t);\n\t\treturn row ?? null;\n\t}\n\n\tasync getMfaSecret(id: string): Promise<string | null> {\n\t\tconst [row] = await this.store.query<{ mfa_secret: string | null }>(\n\t\t\t`SELECT mfa_secret FROM fonderie_users WHERE id = $1`,\n\t\t\t[id],\n\t\t);\n\t\treturn row?.mfa_secret ?? null;\n\t}\n\n\tasync upsertByProvider(\n\t\temail: string,\n\t\tprovider: string,\n\t\tproviderId: string,\n\t): Promise<{ id: string } | null> {\n\t\tconst [row] = await this.store.query<{ id: string }>(\n\t\t\t`INSERT INTO fonderie_users (email, email_verified_at, provider, provider_id)\n\t\t\tVALUES ($1, now(), $2, $3)\n\t\t\tON CONFLICT (email) DO UPDATE\n\t\t\tSET provider = $2, provider_id = $3\n\t\t\tRETURNING id`,\n\t\t\t[email, provider, providerId],\n\t\t);\n\t\treturn row ?? null;\n\t}\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nexport class SessionModel {\n\tconstructor(private store: IStoreAdapter) {}\n\n\tasync create(userId: string, token: string, expiresAt: Date, sid?: string): Promise<void> {\n\t\tawait this.store.query(\n\t\t\t`INSERT INTO fonderie_sessions (user_id, token, expires_at, sid)\n\t\t\tVALUES ($1, $2, $3, $4)\n\t\t\tON CONFLICT (token) DO NOTHING`,\n\t\t\t[userId, token, expiresAt, sid ?? null],\n\t\t);\n\t}\n\n\tasync delete(token: string): Promise<void> {\n\t\tawait this.store.query(`DELETE FROM fonderie_sessions WHERE token = $1`, [token]);\n\t}\n\n\tasync exists(token: string): Promise<boolean> {\n\t\tconst rows = await this.store.query<{ id: string }>(\n\t\t\t`SELECT id FROM fonderie_sessions WHERE token = $1 AND expires_at > now()`,\n\t\t\t[token],\n\t\t);\n\t\treturn rows.length > 0;\n\t}\n\n\t// Liveness check for session-bound access tokens (by the sid claim).\n\tasync aliveBySid(sid: string): Promise<boolean> {\n\t\tconst rows = await this.store.query<{ id: string }>(\n\t\t\t`SELECT id FROM fonderie_sessions WHERE sid = $1 AND expires_at > now()`,\n\t\t\t[sid],\n\t\t);\n\t\treturn rows.length > 0;\n\t}\n}\n","import type { Middleware } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IAuthConfig } from '../config';\nimport { verifyToken } from '../services/jwt';\nimport type { IAccessPayload } from '../services/jwt';\nimport { UserModel } from '../models/user.model';\nimport { SessionModel } from '../models/session.model';\n\n// Reads the Bearer token or session cookie, populates ctx.user\n// Does NOT reject — anonymous requests pass through\nexport function withSession(store: IStoreAdapter, config: IAuthConfig): Middleware {\n\tconst users = new UserModel(store);\n\tconst sessions = new SessionModel(store);\n\n\treturn async (ctx, next) => {\n\t\tconst token = extractToken(ctx.request);\n\t\tif (!token) {\n\t\t\treturn next();\n\t\t}\n\n\t\tconst payload = verifyToken(token, config);\n\t\tif (!payload || payload.type !== 'access') {\n\t\t\treturn next();\n\t\t}\n\n\t\t// Revocation: access tokens are bound to their refresh session via the\n\t\t// sid claim. Logout, rotation, and password change delete that row, so\n\t\t// the access token dies with it instead of living out its JWT expiry.\n\t\t// Tokens without a sid pass through: short-lived mfaPending tokens\n\t\t// (no session exists yet) and legacy tokens from before session\n\t\t// binding, which age out within one accessTokenDuration of deploy.\n\t\tif (payload.sid && !(await sessions.aliveBySid(payload.sid))) {\n\t\t\treturn next();\n\t\t}\n\n\t\tconst user = await users.findById(payload.sub);\n\t\tif (!user || user.suspended || user.deletedAt) {\n\t\t\treturn next();\n\t\t}\n\n\t\tObject.assign(ctx, {\n\t\t\tuser: {\n\t\t\t\t...user,\n\t\t\t\tloginMethod: payload.loginMethod ?? 'email',\n\t\t\t\tphoneVerified: payload.phoneVerified ?? false,\n\t\t\t\tmfaPending: (payload as IAccessPayload).mfaPending ?? false,\n\t\t\t},\n\t\t});\n\n\t\treturn next();\n\t};\n}\n\nfunction extractToken(request: Request): string | null {\n\tconst auth = request.headers.get('authorization');\n\tif (auth?.startsWith('Bearer ')) {\n\t\treturn auth.slice(7);\n\t}\n\n\tconst cookie = request.headers.get('cookie') ?? '';\n\tconst match = cookie.match(/(?:^|;\\s*)access_token=([^;]+)/);\n\treturn match?.[1] ?? null;\n}\n","export { requireAuth, requireAnyAuth } from '@fonderie/core/middlewares';\n"],"mappings":"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","names":["jwt"]}
@@ -1,5 +1,5 @@
1
1
  import * as _fonderie_core from '@fonderie/core';
2
- import { w as withSession } from '../session-C54ksYE8.cjs';
2
+ import { w as withSession } from '../session-DbtCnbiG.cjs';
3
3
  import '@fonderie/store';
4
4
  import '@fonderie/rate-limit';
5
5
 
@@ -1,5 +1,5 @@
1
1
  import * as _fonderie_core from '@fonderie/core';
2
- import { w as withSession } from '../session-C54ksYE8.js';
2
+ import { w as withSession } from '../session-DbtCnbiG.js';
3
3
  import '@fonderie/store';
4
4
  import '@fonderie/rate-limit';
5
5
 
@@ -1,4 +1,5 @@
1
1
  // src/services/jwt.ts
2
+ import { randomUUID } from "crypto";
2
3
  import jwt from "jsonwebtoken";
3
4
  function verifyToken(token, config) {
4
5
  try {
@@ -218,9 +219,44 @@ var UserModel = class {
218
219
  }
219
220
  };
220
221
 
222
+ // src/models/session.model.ts
223
+ var SessionModel = class {
224
+ constructor(store) {
225
+ this.store = store;
226
+ }
227
+ store;
228
+ async create(userId, token, expiresAt, sid) {
229
+ await this.store.query(
230
+ `INSERT INTO fonderie_sessions (user_id, token, expires_at, sid)
231
+ VALUES ($1, $2, $3, $4)
232
+ ON CONFLICT (token) DO NOTHING`,
233
+ [userId, token, expiresAt, sid ?? null]
234
+ );
235
+ }
236
+ async delete(token) {
237
+ await this.store.query(`DELETE FROM fonderie_sessions WHERE token = $1`, [token]);
238
+ }
239
+ async exists(token) {
240
+ const rows = await this.store.query(
241
+ `SELECT id FROM fonderie_sessions WHERE token = $1 AND expires_at > now()`,
242
+ [token]
243
+ );
244
+ return rows.length > 0;
245
+ }
246
+ // Liveness check for session-bound access tokens (by the sid claim).
247
+ async aliveBySid(sid) {
248
+ const rows = await this.store.query(
249
+ `SELECT id FROM fonderie_sessions WHERE sid = $1 AND expires_at > now()`,
250
+ [sid]
251
+ );
252
+ return rows.length > 0;
253
+ }
254
+ };
255
+
221
256
  // src/middlewares/session.ts
222
257
  function withSession(store, config) {
223
258
  const users = new UserModel(store);
259
+ const sessions = new SessionModel(store);
224
260
  return async (ctx, next) => {
225
261
  const token = extractToken(ctx.request);
226
262
  if (!token) {
@@ -230,6 +266,9 @@ function withSession(store, config) {
230
266
  if (!payload || payload.type !== "access") {
231
267
  return next();
232
268
  }
269
+ if (payload.sid && !await sessions.aliveBySid(payload.sid)) {
270
+ return next();
271
+ }
233
272
  const user = await users.findById(payload.sub);
234
273
  if (!user || user.suspended || user.deletedAt) {
235
274
  return next();
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/services/jwt.ts","../../src/models/user.model.ts","../../src/middlewares/session.ts","../../src/middlewares/require-auth.ts","../../src/middlewares/index.ts"],"sourcesContent":["import jwt, { type SignOptions } from 'jsonwebtoken';\n\nimport type { IAuthConfig } from '../config';\n\nexport interface TokenPair {\n\taccessToken: string;\n\trefreshToken: string;\n}\n\nexport interface IAccessPayload {\n\tsub: string; // userId\n\ttype: 'access';\n\tloginMethod: 'email' | 'phone' | 'google';\n\tphoneVerified: boolean;\n\tmfaPending?: boolean;\n}\n\nexport interface IRefreshPayload {\n\tsub: string;\n\ttype: 'refresh';\n\tloginMethod: 'email' | 'phone' | 'google';\n\tphoneVerified: boolean;\n}\n\nexport interface ITokenOptions {\n\tloginMethod: 'email' | 'phone' | 'google';\n\tphoneVerified?: boolean;\n}\n\nexport function issueMfaPendingToken(\n\tuserId: string,\n\tconfig: IAuthConfig,\n\tloginMethod: 'email' | 'phone',\n): string {\n\treturn jwt.sign(\n\t\t{\n\t\t\tsub: userId,\n\t\t\ttype: 'access',\n\t\t\tloginMethod,\n\t\t\tphoneVerified: false,\n\t\t\tmfaPending: true,\n\t\t} satisfies IAccessPayload,\n\t\tconfig.jwtSecret,\n\t\t{ expiresIn: '5m' },\n\t);\n}\n\nexport function issueTokenPair(\n\tuserId: string,\n\tconfig: IAuthConfig,\n\toptions: ITokenOptions,\n): TokenPair {\n\tconst duration = config.sessionDuration ?? '7d';\n\tconst loginMethod = options.loginMethod;\n\tconst phoneVerified = options.phoneVerified ?? false;\n\n\tconst accessToken = jwt.sign(\n\t\t{ sub: userId, type: 'access', loginMethod, phoneVerified } satisfies IAccessPayload,\n\t\tconfig.jwtSecret,\n\t\t{ expiresIn: '24h' },\n\t);\n\n\tconst refreshToken = jwt.sign(\n\t\t{ sub: userId, type: 'refresh', loginMethod, phoneVerified } satisfies IRefreshPayload,\n\t\tconfig.jwtSecret,\n\t\t{ expiresIn: duration } as SignOptions,\n\t);\n\n\treturn { accessToken, refreshToken };\n}\n\nexport function refreshTokenExpiry(token: string): Date {\n\tconst decoded = jwt.decode(token) as { exp?: number } | null;\n\treturn decoded?.exp\n\t\t? new Date(decoded.exp * 1000)\n\t\t: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);\n}\n\nexport function verifyToken(\n\ttoken: string,\n\tconfig: IAuthConfig,\n): IAccessPayload | IRefreshPayload | null {\n\ttry {\n\t\treturn jwt.verify(token, config.jwtSecret) as IAccessPayload | IRefreshPayload;\n\t} catch {\n\t\treturn null;\n\t}\n}\n","import type { IStoreAdapter } from '@fonderie/store';\nimport type { IUser } from '../types';\n\nexport interface IUserUpdateFields {\n\tfirstName?: string;\n\tlastName?: string;\n\tphoneNumber?: string;\n\tavatarUrl?: string;\n\tlocale?: string;\n\ttimezone?: string;\n\tpreferences?: unknown;\n}\n\nconst USER_COLUMNS = `\n\tid,\n\temail,\n\tpassword_hash AS \"passwordHash\",\n\tfirst_name AS \"firstName\",\n\tlast_name AS \"lastName\",\n\tphone,\n\tprofile_image_url AS \"profileImageUrl\",\n\tlocale,\n\ttimezone,\n\tis_active AS \"isActive\",\n\tlast_login AS \"lastLogin\",\n\tpreferences,\n\tsuspended,\n\twhitelist,\n\tip_whitelist AS \"ipWhitelist\",\n\tmfa_enabled AS \"mfaEnabled\",\n\tmfa_secret AS \"mfaSecret\",\n\temail_verified_at AS \"emailVerifiedAt\",\n\tdeleted_at AS \"deletedAt\",\n\tcreated_at AS \"createdAt\",\n\tupdated_at AS \"updatedAt\"\n`;\n\nexport class UserModel {\n\tconstructor(private store: IStoreAdapter) {}\n\n\tasync findById(id: string): Promise<IUser | null> {\n\t\tconst [row] = await this.store.query<IUser>(\n\t\t\t`SELECT ${USER_COLUMNS} FROM fonderie_users WHERE id = $1 AND deleted_at IS NULL`,\n\t\t\t[id],\n\t\t);\n\t\treturn row ?? null;\n\t}\n\n\tasync findByEmail(email: string): Promise<IUser | null> {\n\t\tconst [row] = await this.store.query<IUser>(\n\t\t\t`SELECT ${USER_COLUMNS} FROM fonderie_users WHERE email = $1 AND deleted_at IS NULL`,\n\t\t\t[email],\n\t\t);\n\t\treturn row ?? null;\n\t}\n\n\tasync findByPhone(phone: string): Promise<IUser | null> {\n\t\tconst [row] = await this.store.query<IUser>(\n\t\t\t`SELECT ${USER_COLUMNS} FROM fonderie_users WHERE phone = $1 AND deleted_at IS NULL`,\n\t\t\t[phone],\n\t\t);\n\t\treturn row ?? null;\n\t}\n\n\tasync findOrCreateByPhone(\n\t\tphone: string,\n\t\tfirstName: string | null = null,\n\t\tlastName: string | null = null,\n\t): Promise<{ id: string }> {\n\t\tconst [row] = await this.store.query<{ id: string }>(\n\t\t\t`INSERT INTO fonderie_users (phone, first_name, last_name)\n\t\t\tVALUES ($1, $2, $3)\n\t\t\tON CONFLICT (phone) DO UPDATE\n\t\t\tSET first_name = COALESCE(EXCLUDED.first_name, fonderie_users.first_name),\n\t\t\t last_name = COALESCE(EXCLUDED.last_name, fonderie_users.last_name),\n\t\t\t updated_at = now()\n\t\t\tRETURNING id`,\n\t\t\t[phone, firstName, lastName],\n\t\t);\n\t\treturn row!;\n\t}\n\n\tasync create(\n\t\temail: string,\n\t\tpasswordHash: string,\n\t\tfirstName: string | null,\n\t\tlastName: string | null,\n\t): Promise<{ id: string } | null> {\n\t\tconst [row] = await this.store.query<{ id: string }>(\n\t\t\t`INSERT INTO fonderie_users (email, password_hash, first_name, last_name)\n\t\t\tVALUES ($1, $2, $3, $4)\n\t\t\tRETURNING id`,\n\t\t\t[email.toLowerCase().trim(), passwordHash, firstName, lastName],\n\t\t);\n\t\treturn row ?? null;\n\t}\n\n\tasync update(id: string, fields: IUserUpdateFields): Promise<{ id: string } | null> {\n\t\tconst columnMap: Record<string, string> = {\n\t\t\tfirstName: 'first_name',\n\t\t\tlastName: 'last_name',\n\t\t\tphoneNumber: 'phone',\n\t\t\tavatarUrl: 'profile_image_url',\n\t\t\tlocale: 'locale',\n\t\t\ttimezone: 'timezone',\n\t\t\tpreferences: 'preferences',\n\t\t};\n\n\t\tconst sets: string[] = [];\n\t\tconst values: unknown[] = [];\n\n\t\tfor (const [key, col] of Object.entries(columnMap)) {\n\t\t\tif ((fields as Record<string, unknown>)[key] !== undefined) {\n\t\t\t\tvalues.push((fields as Record<string, unknown>)[key]);\n\t\t\t\tsets.push(`${col} = $${values.length}`);\n\t\t\t}\n\t\t}\n\n\t\tvalues.push(id);\n\t\tconst [row] = await this.store.query<{ id: string }>(\n\t\t\t`UPDATE fonderie_users SET ${sets.join(', ')}, updated_at = now() WHERE id = $${values.length} AND deleted_at IS NULL RETURNING id`,\n\t\t\tvalues,\n\t\t);\n\t\treturn row ?? null;\n\t}\n\n\tasync updatePassword(id: string, passwordHash: string): Promise<void> {\n\t\tawait this.store.query(`UPDATE fonderie_users SET password_hash = $1 WHERE id = $2`, [\n\t\t\tpasswordHash,\n\t\t\tid,\n\t\t]);\n\t}\n\n\tasync markEmailVerified(id: string): Promise<void> {\n\t\tawait this.store.query(\n\t\t\t`UPDATE fonderie_users SET email_verified_at = now(), updated_at = now() WHERE id = $1`,\n\t\t\t[id],\n\t\t);\n\t}\n\n\tasync softDelete(id: string): Promise<void> {\n\t\tawait this.store.query(\n\t\t\t`UPDATE fonderie_users SET deleted_at = now(), updated_at = now() WHERE id = $1`,\n\t\t\t[id],\n\t\t);\n\t}\n\n\tasync saveMfaSecret(id: string, secret: string): Promise<void> {\n\t\tawait this.store.query(`UPDATE fonderie_users SET mfa_secret = $1 WHERE id = $2`, [secret, id]);\n\t}\n\n\tasync saveMfaPendingSecret(id: string, secret: string): Promise<void> {\n\t\tawait this.store.query(\n\t\t\t`UPDATE fonderie_users\n\t\t\t SET mfa_secret_pending = $1, mfa_secret_pending_expires_at = now() + interval '15 minutes'\n\t\t\t WHERE id = $2`,\n\t\t\t[secret, id],\n\t\t);\n\t}\n\n\tasync getMfaPendingSecret(id: string): Promise<string | null> {\n\t\tconst [row] = await this.store.query<{ mfa_secret_pending: string | null }>(\n\t\t\t`SELECT mfa_secret_pending FROM fonderie_users\n\t\t\t WHERE id = $1 AND mfa_secret_pending_expires_at > now()`,\n\t\t\t[id],\n\t\t);\n\t\treturn row?.mfa_secret_pending ?? null;\n\t}\n\n\tasync confirmMfaSecret(id: string): Promise<void> {\n\t\tawait this.store.query(\n\t\t\t`UPDATE fonderie_users\n\t\t\t SET mfa_secret = mfa_secret_pending,\n\t\t\t mfa_secret_pending = NULL,\n\t\t\t mfa_secret_pending_expires_at = NULL,\n\t\t\t mfa_enabled = true,\n\t\t\t updated_at = now()\n\t\t\t WHERE id = $1`,\n\t\t\t[id],\n\t\t);\n\t}\n\n\tasync enableMfa(id: string): Promise<void> {\n\t\tawait this.store.query(`UPDATE fonderie_users SET mfa_enabled = true WHERE id = $1`, [id]);\n\t}\n\n\tasync disableMfa(id: string): Promise<void> {\n\t\tawait this.store.query(\n\t\t\t`UPDATE fonderie_users SET mfa_enabled = false, mfa_secret = NULL, updated_at = now() WHERE id = $1`,\n\t\t\t[id],\n\t\t);\n\t}\n\n\tasync updateEmail(id: string, email: string): Promise<void> {\n\t\tawait this.store.query(\n\t\t\t`UPDATE fonderie_users SET email = $1, email_verified_at = NULL, updated_at = now() WHERE id = $2`,\n\t\t\t[email.toLowerCase().trim(), id],\n\t\t);\n\t}\n\n\tasync updatePhone(id: string, phone: string): Promise<void> {\n\t\tawait this.store.query(\n\t\t\t`UPDATE fonderie_users SET phone = $1, updated_at = now() WHERE id = $2`,\n\t\t\t[phone, id],\n\t\t);\n\t}\n\n\tasync updatePreferences(\n\t\tid: string,\n\t\tfields: {\n\t\t\tlocale?: string;\n\t\t\ttimezone?: string;\n\t\t\tpatch?: Record<string, unknown>;\n\t\t},\n\t): Promise<{ id: string } | null> {\n\t\tconst sets: string[] = [];\n\t\tconst values: unknown[] = [];\n\n\t\tif (fields.locale !== undefined) {\n\t\t\tvalues.push(fields.locale);\n\t\t\tsets.push(`locale = $${values.length}`);\n\t\t}\n\t\tif (fields.timezone !== undefined) {\n\t\t\tvalues.push(fields.timezone);\n\t\t\tsets.push(`timezone = $${values.length}`);\n\t\t}\n\t\tif (fields.patch && Object.keys(fields.patch).length > 0) {\n\t\t\tvalues.push(JSON.stringify(fields.patch));\n\t\t\tsets.push(`preferences = preferences || $${values.length}::jsonb`);\n\t\t}\n\n\t\tif (sets.length === 0) return null;\n\n\t\tvalues.push(id);\n\t\tconst [row] = await this.store.query<{ id: string }>(\n\t\t\t`UPDATE fonderie_users SET ${sets.join(', ')}, updated_at = now() WHERE id = $${values.length} AND deleted_at IS NULL RETURNING id`,\n\t\t\tvalues,\n\t\t);\n\t\treturn row ?? null;\n\t}\n\n\tasync getMfaSecret(id: string): Promise<string | null> {\n\t\tconst [row] = await this.store.query<{ mfa_secret: string | null }>(\n\t\t\t`SELECT mfa_secret FROM fonderie_users WHERE id = $1`,\n\t\t\t[id],\n\t\t);\n\t\treturn row?.mfa_secret ?? null;\n\t}\n\n\tasync upsertByProvider(\n\t\temail: string,\n\t\tprovider: string,\n\t\tproviderId: string,\n\t): Promise<{ id: string } | null> {\n\t\tconst [row] = await this.store.query<{ id: string }>(\n\t\t\t`INSERT INTO fonderie_users (email, email_verified_at, provider, provider_id)\n\t\t\tVALUES ($1, now(), $2, $3)\n\t\t\tON CONFLICT (email) DO UPDATE\n\t\t\tSET provider = $2, provider_id = $3\n\t\t\tRETURNING id`,\n\t\t\t[email, provider, providerId],\n\t\t);\n\t\treturn row ?? null;\n\t}\n}\n","import type { Middleware } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IAuthConfig } from '../config';\nimport { verifyToken } from '../services/jwt';\nimport type { IAccessPayload } from '../services/jwt';\nimport { UserModel } from '../models/user.model';\n\n// Reads the Bearer token or session cookie, populates ctx.user\n// Does NOT reject — anonymous requests pass through\nexport function withSession(store: IStoreAdapter, config: IAuthConfig): Middleware {\n\tconst users = new UserModel(store);\n\n\treturn async (ctx, next) => {\n\t\tconst token = extractToken(ctx.request);\n\t\tif (!token) {\n\t\t\treturn next();\n\t\t}\n\n\t\tconst payload = verifyToken(token, config);\n\t\tif (!payload || payload.type !== 'access') {\n\t\t\treturn next();\n\t\t}\n\n\t\tconst user = await users.findById(payload.sub);\n\t\tif (!user || user.suspended || user.deletedAt) {\n\t\t\treturn next();\n\t\t}\n\n\t\tObject.assign(ctx, {\n\t\t\tuser: {\n\t\t\t\t...user,\n\t\t\t\tloginMethod: payload.loginMethod ?? 'email',\n\t\t\t\tphoneVerified: payload.phoneVerified ?? false,\n\t\t\t\tmfaPending: (payload as IAccessPayload).mfaPending ?? false,\n\t\t\t},\n\t\t});\n\n\t\treturn next();\n\t};\n}\n\nfunction extractToken(request: Request): string | null {\n\tconst auth = request.headers.get('authorization');\n\tif (auth?.startsWith('Bearer ')) {\n\t\treturn auth.slice(7);\n\t}\n\n\tconst cookie = request.headers.get('cookie') ?? '';\n\tconst match = cookie.match(/(?:^|;\\s*)access_token=([^;]+)/);\n\treturn match?.[1] ?? null;\n}\n","export { requireAuth, requireAnyAuth } from '@fonderie/core/middlewares';\n","import { withSession } from './session';\nimport { requireAuth } from './require-auth';\n\nconst middlewares = {\n\trequireAuth,\n\twithSession,\n};\n\nexport default middlewares;\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/services/jwt.ts","../../src/models/user.model.ts","../../src/models/session.model.ts","../../src/middlewares/session.ts","../../src/middlewares/require-auth.ts","../../src/middlewares/index.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport jwt, { type SignOptions } from 'jsonwebtoken';\n\nimport type { IAuthConfig } from '../config';\n\nexport interface TokenPair {\n\taccessToken: string;\n\trefreshToken: string;\n\t// Server-side session id both tokens are bound to. Store it on the\n\t// fonderie_sessions row — deleting that row revokes the access token\n\t// immediately, not just the refresh token.\n\tsid: string;\n}\n\nexport interface IAccessPayload {\n\tsub: string; // userId\n\ttype: 'access';\n\tloginMethod: 'email' | 'phone' | 'google';\n\tphoneVerified: boolean;\n\tmfaPending?: boolean;\n\t// Absent only on legacy tokens issued before session binding and on\n\t// short-lived mfaPending tokens (no session exists yet at that point).\n\tsid?: string;\n}\n\nexport interface IRefreshPayload {\n\tsub: string;\n\ttype: 'refresh';\n\tloginMethod: 'email' | 'phone' | 'google';\n\tphoneVerified: boolean;\n\tsid?: string;\n}\n\nexport interface ITokenOptions {\n\tloginMethod: 'email' | 'phone' | 'google';\n\tphoneVerified?: boolean;\n}\n\nexport function issueMfaPendingToken(\n\tuserId: string,\n\tconfig: IAuthConfig,\n\tloginMethod: 'email' | 'phone',\n): string {\n\treturn jwt.sign(\n\t\t{\n\t\t\tsub: userId,\n\t\t\ttype: 'access',\n\t\t\tloginMethod,\n\t\t\tphoneVerified: false,\n\t\t\tmfaPending: true,\n\t\t} satisfies IAccessPayload,\n\t\tconfig.jwtSecret,\n\t\t{ expiresIn: '5m' },\n\t);\n}\n\nexport function issueTokenPair(\n\tuserId: string,\n\tconfig: IAuthConfig,\n\toptions: ITokenOptions,\n): TokenPair {\n\tconst duration = config.sessionDuration ?? '7d';\n\tconst accessDuration = config.accessTokenDuration ?? '24h';\n\tconst loginMethod = options.loginMethod;\n\tconst phoneVerified = options.phoneVerified ?? false;\n\tconst sid = randomUUID();\n\n\tconst accessToken = jwt.sign(\n\t\t{ sub: userId, type: 'access', loginMethod, phoneVerified, sid } satisfies IAccessPayload,\n\t\tconfig.jwtSecret,\n\t\t{ expiresIn: accessDuration } as SignOptions,\n\t);\n\n\tconst refreshToken = jwt.sign(\n\t\t{ sub: userId, type: 'refresh', loginMethod, phoneVerified, sid } satisfies IRefreshPayload,\n\t\tconfig.jwtSecret,\n\t\t{ expiresIn: duration } as SignOptions,\n\t);\n\n\treturn { accessToken, refreshToken, sid };\n}\n\nexport function refreshTokenExpiry(token: string): Date {\n\tconst decoded = jwt.decode(token) as { exp?: number } | null;\n\treturn decoded?.exp\n\t\t? new Date(decoded.exp * 1000)\n\t\t: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);\n}\n\nexport function verifyToken(\n\ttoken: string,\n\tconfig: IAuthConfig,\n): IAccessPayload | IRefreshPayload | null {\n\ttry {\n\t\treturn jwt.verify(token, config.jwtSecret) as IAccessPayload | IRefreshPayload;\n\t} catch {\n\t\treturn null;\n\t}\n}\n","import type { IStoreAdapter } from '@fonderie/store';\nimport type { IUser } from '../types';\n\nexport interface IUserUpdateFields {\n\tfirstName?: string;\n\tlastName?: string;\n\tphoneNumber?: string;\n\tavatarUrl?: string;\n\tlocale?: string;\n\ttimezone?: string;\n\tpreferences?: unknown;\n}\n\nconst USER_COLUMNS = `\n\tid,\n\temail,\n\tpassword_hash AS \"passwordHash\",\n\tfirst_name AS \"firstName\",\n\tlast_name AS \"lastName\",\n\tphone,\n\tprofile_image_url AS \"profileImageUrl\",\n\tlocale,\n\ttimezone,\n\tis_active AS \"isActive\",\n\tlast_login AS \"lastLogin\",\n\tpreferences,\n\tsuspended,\n\twhitelist,\n\tip_whitelist AS \"ipWhitelist\",\n\tmfa_enabled AS \"mfaEnabled\",\n\tmfa_secret AS \"mfaSecret\",\n\temail_verified_at AS \"emailVerifiedAt\",\n\tdeleted_at AS \"deletedAt\",\n\tcreated_at AS \"createdAt\",\n\tupdated_at AS \"updatedAt\"\n`;\n\nexport class UserModel {\n\tconstructor(private store: IStoreAdapter) {}\n\n\tasync findById(id: string): Promise<IUser | null> {\n\t\tconst [row] = await this.store.query<IUser>(\n\t\t\t`SELECT ${USER_COLUMNS} FROM fonderie_users WHERE id = $1 AND deleted_at IS NULL`,\n\t\t\t[id],\n\t\t);\n\t\treturn row ?? null;\n\t}\n\n\tasync findByEmail(email: string): Promise<IUser | null> {\n\t\tconst [row] = await this.store.query<IUser>(\n\t\t\t`SELECT ${USER_COLUMNS} FROM fonderie_users WHERE email = $1 AND deleted_at IS NULL`,\n\t\t\t[email],\n\t\t);\n\t\treturn row ?? null;\n\t}\n\n\tasync findByPhone(phone: string): Promise<IUser | null> {\n\t\tconst [row] = await this.store.query<IUser>(\n\t\t\t`SELECT ${USER_COLUMNS} FROM fonderie_users WHERE phone = $1 AND deleted_at IS NULL`,\n\t\t\t[phone],\n\t\t);\n\t\treturn row ?? null;\n\t}\n\n\tasync findOrCreateByPhone(\n\t\tphone: string,\n\t\tfirstName: string | null = null,\n\t\tlastName: string | null = null,\n\t): Promise<{ id: string }> {\n\t\tconst [row] = await this.store.query<{ id: string }>(\n\t\t\t`INSERT INTO fonderie_users (phone, first_name, last_name)\n\t\t\tVALUES ($1, $2, $3)\n\t\t\tON CONFLICT (phone) DO UPDATE\n\t\t\tSET first_name = COALESCE(EXCLUDED.first_name, fonderie_users.first_name),\n\t\t\t last_name = COALESCE(EXCLUDED.last_name, fonderie_users.last_name),\n\t\t\t updated_at = now()\n\t\t\tRETURNING id`,\n\t\t\t[phone, firstName, lastName],\n\t\t);\n\t\treturn row!;\n\t}\n\n\tasync create(\n\t\temail: string,\n\t\tpasswordHash: string,\n\t\tfirstName: string | null,\n\t\tlastName: string | null,\n\t): Promise<{ id: string } | null> {\n\t\tconst [row] = await this.store.query<{ id: string }>(\n\t\t\t`INSERT INTO fonderie_users (email, password_hash, first_name, last_name)\n\t\t\tVALUES ($1, $2, $3, $4)\n\t\t\tRETURNING id`,\n\t\t\t[email.toLowerCase().trim(), passwordHash, firstName, lastName],\n\t\t);\n\t\treturn row ?? null;\n\t}\n\n\tasync update(id: string, fields: IUserUpdateFields): Promise<{ id: string } | null> {\n\t\tconst columnMap: Record<string, string> = {\n\t\t\tfirstName: 'first_name',\n\t\t\tlastName: 'last_name',\n\t\t\tphoneNumber: 'phone',\n\t\t\tavatarUrl: 'profile_image_url',\n\t\t\tlocale: 'locale',\n\t\t\ttimezone: 'timezone',\n\t\t\tpreferences: 'preferences',\n\t\t};\n\n\t\tconst sets: string[] = [];\n\t\tconst values: unknown[] = [];\n\n\t\tfor (const [key, col] of Object.entries(columnMap)) {\n\t\t\tif ((fields as Record<string, unknown>)[key] !== undefined) {\n\t\t\t\tvalues.push((fields as Record<string, unknown>)[key]);\n\t\t\t\tsets.push(`${col} = $${values.length}`);\n\t\t\t}\n\t\t}\n\n\t\tvalues.push(id);\n\t\tconst [row] = await this.store.query<{ id: string }>(\n\t\t\t`UPDATE fonderie_users SET ${sets.join(', ')}, updated_at = now() WHERE id = $${values.length} AND deleted_at IS NULL RETURNING id`,\n\t\t\tvalues,\n\t\t);\n\t\treturn row ?? null;\n\t}\n\n\tasync updatePassword(id: string, passwordHash: string): Promise<void> {\n\t\tawait this.store.query(`UPDATE fonderie_users SET password_hash = $1 WHERE id = $2`, [\n\t\t\tpasswordHash,\n\t\t\tid,\n\t\t]);\n\t}\n\n\tasync markEmailVerified(id: string): Promise<void> {\n\t\tawait this.store.query(\n\t\t\t`UPDATE fonderie_users SET email_verified_at = now(), updated_at = now() WHERE id = $1`,\n\t\t\t[id],\n\t\t);\n\t}\n\n\tasync softDelete(id: string): Promise<void> {\n\t\tawait this.store.query(\n\t\t\t`UPDATE fonderie_users SET deleted_at = now(), updated_at = now() WHERE id = $1`,\n\t\t\t[id],\n\t\t);\n\t}\n\n\tasync saveMfaSecret(id: string, secret: string): Promise<void> {\n\t\tawait this.store.query(`UPDATE fonderie_users SET mfa_secret = $1 WHERE id = $2`, [secret, id]);\n\t}\n\n\tasync saveMfaPendingSecret(id: string, secret: string): Promise<void> {\n\t\tawait this.store.query(\n\t\t\t`UPDATE fonderie_users\n\t\t\t SET mfa_secret_pending = $1, mfa_secret_pending_expires_at = now() + interval '15 minutes'\n\t\t\t WHERE id = $2`,\n\t\t\t[secret, id],\n\t\t);\n\t}\n\n\tasync getMfaPendingSecret(id: string): Promise<string | null> {\n\t\tconst [row] = await this.store.query<{ mfa_secret_pending: string | null }>(\n\t\t\t`SELECT mfa_secret_pending FROM fonderie_users\n\t\t\t WHERE id = $1 AND mfa_secret_pending_expires_at > now()`,\n\t\t\t[id],\n\t\t);\n\t\treturn row?.mfa_secret_pending ?? null;\n\t}\n\n\tasync confirmMfaSecret(id: string): Promise<void> {\n\t\tawait this.store.query(\n\t\t\t`UPDATE fonderie_users\n\t\t\t SET mfa_secret = mfa_secret_pending,\n\t\t\t mfa_secret_pending = NULL,\n\t\t\t mfa_secret_pending_expires_at = NULL,\n\t\t\t mfa_enabled = true,\n\t\t\t updated_at = now()\n\t\t\t WHERE id = $1`,\n\t\t\t[id],\n\t\t);\n\t}\n\n\tasync enableMfa(id: string): Promise<void> {\n\t\tawait this.store.query(`UPDATE fonderie_users SET mfa_enabled = true WHERE id = $1`, [id]);\n\t}\n\n\tasync disableMfa(id: string): Promise<void> {\n\t\tawait this.store.query(\n\t\t\t`UPDATE fonderie_users SET mfa_enabled = false, mfa_secret = NULL, updated_at = now() WHERE id = $1`,\n\t\t\t[id],\n\t\t);\n\t}\n\n\tasync updateEmail(id: string, email: string): Promise<void> {\n\t\tawait this.store.query(\n\t\t\t`UPDATE fonderie_users SET email = $1, email_verified_at = NULL, updated_at = now() WHERE id = $2`,\n\t\t\t[email.toLowerCase().trim(), id],\n\t\t);\n\t}\n\n\tasync updatePhone(id: string, phone: string): Promise<void> {\n\t\tawait this.store.query(\n\t\t\t`UPDATE fonderie_users SET phone = $1, updated_at = now() WHERE id = $2`,\n\t\t\t[phone, id],\n\t\t);\n\t}\n\n\tasync updatePreferences(\n\t\tid: string,\n\t\tfields: {\n\t\t\tlocale?: string;\n\t\t\ttimezone?: string;\n\t\t\tpatch?: Record<string, unknown>;\n\t\t},\n\t): Promise<{ id: string } | null> {\n\t\tconst sets: string[] = [];\n\t\tconst values: unknown[] = [];\n\n\t\tif (fields.locale !== undefined) {\n\t\t\tvalues.push(fields.locale);\n\t\t\tsets.push(`locale = $${values.length}`);\n\t\t}\n\t\tif (fields.timezone !== undefined) {\n\t\t\tvalues.push(fields.timezone);\n\t\t\tsets.push(`timezone = $${values.length}`);\n\t\t}\n\t\tif (fields.patch && Object.keys(fields.patch).length > 0) {\n\t\t\tvalues.push(JSON.stringify(fields.patch));\n\t\t\tsets.push(`preferences = preferences || $${values.length}::jsonb`);\n\t\t}\n\n\t\tif (sets.length === 0) return null;\n\n\t\tvalues.push(id);\n\t\tconst [row] = await this.store.query<{ id: string }>(\n\t\t\t`UPDATE fonderie_users SET ${sets.join(', ')}, updated_at = now() WHERE id = $${values.length} AND deleted_at IS NULL RETURNING id`,\n\t\t\tvalues,\n\t\t);\n\t\treturn row ?? null;\n\t}\n\n\tasync getMfaSecret(id: string): Promise<string | null> {\n\t\tconst [row] = await this.store.query<{ mfa_secret: string | null }>(\n\t\t\t`SELECT mfa_secret FROM fonderie_users WHERE id = $1`,\n\t\t\t[id],\n\t\t);\n\t\treturn row?.mfa_secret ?? null;\n\t}\n\n\tasync upsertByProvider(\n\t\temail: string,\n\t\tprovider: string,\n\t\tproviderId: string,\n\t): Promise<{ id: string } | null> {\n\t\tconst [row] = await this.store.query<{ id: string }>(\n\t\t\t`INSERT INTO fonderie_users (email, email_verified_at, provider, provider_id)\n\t\t\tVALUES ($1, now(), $2, $3)\n\t\t\tON CONFLICT (email) DO UPDATE\n\t\t\tSET provider = $2, provider_id = $3\n\t\t\tRETURNING id`,\n\t\t\t[email, provider, providerId],\n\t\t);\n\t\treturn row ?? null;\n\t}\n}\n","import type { IStoreAdapter } from '@fonderie/store';\n\nexport class SessionModel {\n\tconstructor(private store: IStoreAdapter) {}\n\n\tasync create(userId: string, token: string, expiresAt: Date, sid?: string): Promise<void> {\n\t\tawait this.store.query(\n\t\t\t`INSERT INTO fonderie_sessions (user_id, token, expires_at, sid)\n\t\t\tVALUES ($1, $2, $3, $4)\n\t\t\tON CONFLICT (token) DO NOTHING`,\n\t\t\t[userId, token, expiresAt, sid ?? null],\n\t\t);\n\t}\n\n\tasync delete(token: string): Promise<void> {\n\t\tawait this.store.query(`DELETE FROM fonderie_sessions WHERE token = $1`, [token]);\n\t}\n\n\tasync exists(token: string): Promise<boolean> {\n\t\tconst rows = await this.store.query<{ id: string }>(\n\t\t\t`SELECT id FROM fonderie_sessions WHERE token = $1 AND expires_at > now()`,\n\t\t\t[token],\n\t\t);\n\t\treturn rows.length > 0;\n\t}\n\n\t// Liveness check for session-bound access tokens (by the sid claim).\n\tasync aliveBySid(sid: string): Promise<boolean> {\n\t\tconst rows = await this.store.query<{ id: string }>(\n\t\t\t`SELECT id FROM fonderie_sessions WHERE sid = $1 AND expires_at > now()`,\n\t\t\t[sid],\n\t\t);\n\t\treturn rows.length > 0;\n\t}\n}\n","import type { Middleware } from '@fonderie/core';\nimport type { IStoreAdapter } from '@fonderie/store';\n\nimport type { IAuthConfig } from '../config';\nimport { verifyToken } from '../services/jwt';\nimport type { IAccessPayload } from '../services/jwt';\nimport { UserModel } from '../models/user.model';\nimport { SessionModel } from '../models/session.model';\n\n// Reads the Bearer token or session cookie, populates ctx.user\n// Does NOT reject — anonymous requests pass through\nexport function withSession(store: IStoreAdapter, config: IAuthConfig): Middleware {\n\tconst users = new UserModel(store);\n\tconst sessions = new SessionModel(store);\n\n\treturn async (ctx, next) => {\n\t\tconst token = extractToken(ctx.request);\n\t\tif (!token) {\n\t\t\treturn next();\n\t\t}\n\n\t\tconst payload = verifyToken(token, config);\n\t\tif (!payload || payload.type !== 'access') {\n\t\t\treturn next();\n\t\t}\n\n\t\t// Revocation: access tokens are bound to their refresh session via the\n\t\t// sid claim. Logout, rotation, and password change delete that row, so\n\t\t// the access token dies with it instead of living out its JWT expiry.\n\t\t// Tokens without a sid pass through: short-lived mfaPending tokens\n\t\t// (no session exists yet) and legacy tokens from before session\n\t\t// binding, which age out within one accessTokenDuration of deploy.\n\t\tif (payload.sid && !(await sessions.aliveBySid(payload.sid))) {\n\t\t\treturn next();\n\t\t}\n\n\t\tconst user = await users.findById(payload.sub);\n\t\tif (!user || user.suspended || user.deletedAt) {\n\t\t\treturn next();\n\t\t}\n\n\t\tObject.assign(ctx, {\n\t\t\tuser: {\n\t\t\t\t...user,\n\t\t\t\tloginMethod: payload.loginMethod ?? 'email',\n\t\t\t\tphoneVerified: payload.phoneVerified ?? false,\n\t\t\t\tmfaPending: (payload as IAccessPayload).mfaPending ?? false,\n\t\t\t},\n\t\t});\n\n\t\treturn next();\n\t};\n}\n\nfunction extractToken(request: Request): string | null {\n\tconst auth = request.headers.get('authorization');\n\tif (auth?.startsWith('Bearer ')) {\n\t\treturn auth.slice(7);\n\t}\n\n\tconst cookie = request.headers.get('cookie') ?? '';\n\tconst match = cookie.match(/(?:^|;\\s*)access_token=([^;]+)/);\n\treturn match?.[1] ?? null;\n}\n","export { requireAuth, requireAnyAuth } from '@fonderie/core/middlewares';\n","import { withSession } from './session';\nimport { requireAuth } from './require-auth';\n\nconst middlewares = {\n\trequireAuth,\n\twithSession,\n};\n\nexport default middlewares;\n"],"mappings":"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","names":[]}
@@ -40,6 +40,7 @@ type AuthMessageKey = (typeof MESSAGE_KEYS)[keyof typeof MESSAGE_KEYS];
40
40
  interface IAuthConfig extends IAuthSecrets, IAuthRuntimeConfig {
41
41
  secureCookies?: boolean;
42
42
  rateLimit?: IAuthRateLimitConfig | false;
43
+ accessTokenDuration?: string;
43
44
  providers: ('email' | 'phone' | 'google' | 'github')[];
44
45
  appName?: string;
45
46
  resolve?: (ctx: {
@@ -40,6 +40,7 @@ type AuthMessageKey = (typeof MESSAGE_KEYS)[keyof typeof MESSAGE_KEYS];
40
40
  interface IAuthConfig extends IAuthSecrets, IAuthRuntimeConfig {
41
41
  secureCookies?: boolean;
42
42
  rateLimit?: IAuthRateLimitConfig | false;
43
+ accessTokenDuration?: string;
43
44
  providers: ('email' | 'phone' | 'google' | 'github')[];
44
45
  appName?: string;
45
46
  resolve?: (ctx: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fonderie/auth",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Drop-in auth for SaaS — email/password, phone OTP, Google OAuth, stateless JWT sessions, TOTP-based MFA with backup codes, and self-service password recovery.",
5
5
  "keywords": [
6
6
  "fonderie-js",