@fonderie/auth 1.0.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.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +51 -0
  3. package/dist/dtos/user.cjs +66 -0
  4. package/dist/dtos/user.cjs.map +1 -0
  5. package/dist/dtos/user.d.cts +24 -0
  6. package/dist/dtos/user.d.ts +24 -0
  7. package/dist/dtos/user.js +41 -0
  8. package/dist/dtos/user.js.map +1 -0
  9. package/dist/index.cjs +1952 -0
  10. package/dist/index.cjs.map +1 -0
  11. package/dist/index.d.cts +22 -0
  12. package/dist/index.d.ts +22 -0
  13. package/dist/index.js +1915 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/middlewares/index.cjs +303 -0
  16. package/dist/middlewares/index.cjs.map +1 -0
  17. package/dist/middlewares/index.d.cts +10 -0
  18. package/dist/middlewares/index.d.ts +10 -0
  19. package/dist/middlewares/index.js +270 -0
  20. package/dist/middlewares/index.js.map +1 -0
  21. package/dist/migrations/index.js +7 -0
  22. package/dist/migrations/index.js.map +1 -0
  23. package/dist/migrations/sql/001_auth.sql +73 -0
  24. package/dist/migrations/sql/002_phone_auth.sql +20 -0
  25. package/dist/migrations/sql/003_phone_registration_name.sql +4 -0
  26. package/dist/migrations/sql/004_drop_phone_verif_name_cols.sql +5 -0
  27. package/dist/migrations/sql/005_phone_verification_user_id.sql +6 -0
  28. package/dist/migrations/sql/006_drop_phone_verified_at.sql +1 -0
  29. package/dist/migrations/sql/007_email_verif_user_id_pk.sql +14 -0
  30. package/dist/migrations/sql/008_password_reset_pin.sql +4 -0
  31. package/dist/migrations/sql/009_password_reset_created_at.sql +1 -0
  32. package/dist/migrations/sql/010_mfa_pending_secret.sql +3 -0
  33. package/dist/migrations/sql/011_mfa_backup_codes.sql +10 -0
  34. package/dist/migrations/sql/012_drop_skills.sql +1 -0
  35. package/dist/session-CLD1WPJs.d.cts +41 -0
  36. package/dist/session-CLD1WPJs.d.ts +41 -0
  37. package/dist/types.cjs +19 -0
  38. package/dist/types.cjs.map +1 -0
  39. package/dist/types.d.cts +52 -0
  40. package/dist/types.d.ts +52 -0
  41. package/dist/types.js +1 -0
  42. package/dist/types.js.map +1 -0
  43. package/package.json +100 -0
@@ -0,0 +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"]}
@@ -0,0 +1,10 @@
1
+ import * as _fonderie_core from '@fonderie/core';
2
+ import { w as withSession } from '../session-CLD1WPJs.cjs';
3
+ import '@fonderie/store';
4
+
5
+ declare const middlewares: {
6
+ requireAuth: _fonderie_core.Middleware;
7
+ withSession: typeof withSession;
8
+ };
9
+
10
+ export { middlewares as default };
@@ -0,0 +1,10 @@
1
+ import * as _fonderie_core from '@fonderie/core';
2
+ import { w as withSession } from '../session-CLD1WPJs.js';
3
+ import '@fonderie/store';
4
+
5
+ declare const middlewares: {
6
+ requireAuth: _fonderie_core.Middleware;
7
+ withSession: typeof withSession;
8
+ };
9
+
10
+ export { middlewares as default };
@@ -0,0 +1,270 @@
1
+ // src/services/jwt.ts
2
+ import jwt from "jsonwebtoken";
3
+ function verifyToken(token, config) {
4
+ try {
5
+ return jwt.verify(token, config.jwtSecret);
6
+ } catch {
7
+ return null;
8
+ }
9
+ }
10
+
11
+ // src/models/user.model.ts
12
+ var USER_COLUMNS = `
13
+ id,
14
+ email,
15
+ password_hash AS "passwordHash",
16
+ first_name AS "firstName",
17
+ last_name AS "lastName",
18
+ phone,
19
+ profile_image_url AS "profileImageUrl",
20
+ locale,
21
+ timezone,
22
+ is_active AS "isActive",
23
+ last_login AS "lastLogin",
24
+ preferences,
25
+ suspended,
26
+ whitelist,
27
+ ip_whitelist AS "ipWhitelist",
28
+ mfa_enabled AS "mfaEnabled",
29
+ mfa_secret AS "mfaSecret",
30
+ email_verified_at AS "emailVerifiedAt",
31
+ deleted_at AS "deletedAt",
32
+ created_at AS "createdAt",
33
+ updated_at AS "updatedAt"
34
+ `;
35
+ var UserModel = class {
36
+ constructor(store) {
37
+ this.store = store;
38
+ }
39
+ store;
40
+ async findById(id) {
41
+ const [row] = await this.store.query(
42
+ `SELECT ${USER_COLUMNS} FROM fonderie_users WHERE id = $1 AND deleted_at IS NULL`,
43
+ [id]
44
+ );
45
+ return row ?? null;
46
+ }
47
+ async findByEmail(email) {
48
+ const [row] = await this.store.query(
49
+ `SELECT ${USER_COLUMNS} FROM fonderie_users WHERE email = $1 AND deleted_at IS NULL`,
50
+ [email]
51
+ );
52
+ return row ?? null;
53
+ }
54
+ async findByPhone(phone) {
55
+ const [row] = await this.store.query(
56
+ `SELECT ${USER_COLUMNS} FROM fonderie_users WHERE phone = $1 AND deleted_at IS NULL`,
57
+ [phone]
58
+ );
59
+ return row ?? null;
60
+ }
61
+ async findOrCreateByPhone(phone, firstName = null, lastName = null) {
62
+ const [row] = await this.store.query(
63
+ `INSERT INTO fonderie_users (phone, first_name, last_name)
64
+ VALUES ($1, $2, $3)
65
+ ON CONFLICT (phone) DO UPDATE
66
+ SET first_name = COALESCE(EXCLUDED.first_name, fonderie_users.first_name),
67
+ last_name = COALESCE(EXCLUDED.last_name, fonderie_users.last_name),
68
+ updated_at = now()
69
+ RETURNING id`,
70
+ [phone, firstName, lastName]
71
+ );
72
+ return row;
73
+ }
74
+ async create(email, passwordHash, firstName, lastName) {
75
+ const [row] = await this.store.query(
76
+ `INSERT INTO fonderie_users (email, password_hash, first_name, last_name)
77
+ VALUES ($1, $2, $3, $4)
78
+ RETURNING id`,
79
+ [email.toLowerCase().trim(), passwordHash, firstName, lastName]
80
+ );
81
+ return row ?? null;
82
+ }
83
+ async update(id, fields) {
84
+ const columnMap = {
85
+ firstName: "first_name",
86
+ lastName: "last_name",
87
+ phoneNumber: "phone",
88
+ avatarUrl: "profile_image_url",
89
+ locale: "locale",
90
+ timezone: "timezone",
91
+ preferences: "preferences"
92
+ };
93
+ const sets = [];
94
+ const values = [];
95
+ for (const [key, col] of Object.entries(columnMap)) {
96
+ if (fields[key] !== void 0) {
97
+ values.push(fields[key]);
98
+ sets.push(`${col} = $${values.length}`);
99
+ }
100
+ }
101
+ values.push(id);
102
+ const [row] = await this.store.query(
103
+ `UPDATE fonderie_users SET ${sets.join(", ")}, updated_at = now() WHERE id = $${values.length} AND deleted_at IS NULL RETURNING id`,
104
+ values
105
+ );
106
+ return row ?? null;
107
+ }
108
+ async updatePassword(id, passwordHash) {
109
+ await this.store.query(`UPDATE fonderie_users SET password_hash = $1 WHERE id = $2`, [
110
+ passwordHash,
111
+ id
112
+ ]);
113
+ }
114
+ async markEmailVerified(id) {
115
+ await this.store.query(
116
+ `UPDATE fonderie_users SET email_verified_at = now(), updated_at = now() WHERE id = $1`,
117
+ [id]
118
+ );
119
+ }
120
+ async softDelete(id) {
121
+ await this.store.query(
122
+ `UPDATE fonderie_users SET deleted_at = now(), updated_at = now() WHERE id = $1`,
123
+ [id]
124
+ );
125
+ }
126
+ async saveMfaSecret(id, secret) {
127
+ await this.store.query(`UPDATE fonderie_users SET mfa_secret = $1 WHERE id = $2`, [secret, id]);
128
+ }
129
+ async saveMfaPendingSecret(id, secret) {
130
+ await this.store.query(
131
+ `UPDATE fonderie_users
132
+ SET mfa_secret_pending = $1, mfa_secret_pending_expires_at = now() + interval '15 minutes'
133
+ WHERE id = $2`,
134
+ [secret, id]
135
+ );
136
+ }
137
+ async getMfaPendingSecret(id) {
138
+ const [row] = await this.store.query(
139
+ `SELECT mfa_secret_pending FROM fonderie_users
140
+ WHERE id = $1 AND mfa_secret_pending_expires_at > now()`,
141
+ [id]
142
+ );
143
+ return row?.mfa_secret_pending ?? null;
144
+ }
145
+ async confirmMfaSecret(id) {
146
+ await this.store.query(
147
+ `UPDATE fonderie_users
148
+ SET mfa_secret = mfa_secret_pending,
149
+ mfa_secret_pending = NULL,
150
+ mfa_secret_pending_expires_at = NULL,
151
+ mfa_enabled = true,
152
+ updated_at = now()
153
+ WHERE id = $1`,
154
+ [id]
155
+ );
156
+ }
157
+ async enableMfa(id) {
158
+ await this.store.query(`UPDATE fonderie_users SET mfa_enabled = true WHERE id = $1`, [id]);
159
+ }
160
+ async disableMfa(id) {
161
+ await this.store.query(
162
+ `UPDATE fonderie_users SET mfa_enabled = false, mfa_secret = NULL, updated_at = now() WHERE id = $1`,
163
+ [id]
164
+ );
165
+ }
166
+ async updateEmail(id, email) {
167
+ await this.store.query(
168
+ `UPDATE fonderie_users SET email = $1, email_verified_at = NULL, updated_at = now() WHERE id = $2`,
169
+ [email.toLowerCase().trim(), id]
170
+ );
171
+ }
172
+ async updatePhone(id, phone) {
173
+ await this.store.query(
174
+ `UPDATE fonderie_users SET phone = $1, updated_at = now() WHERE id = $2`,
175
+ [phone, id]
176
+ );
177
+ }
178
+ async updatePreferences(id, fields) {
179
+ const sets = [];
180
+ const values = [];
181
+ if (fields.locale !== void 0) {
182
+ values.push(fields.locale);
183
+ sets.push(`locale = $${values.length}`);
184
+ }
185
+ if (fields.timezone !== void 0) {
186
+ values.push(fields.timezone);
187
+ sets.push(`timezone = $${values.length}`);
188
+ }
189
+ if (fields.patch && Object.keys(fields.patch).length > 0) {
190
+ values.push(JSON.stringify(fields.patch));
191
+ sets.push(`preferences = preferences || $${values.length}::jsonb`);
192
+ }
193
+ if (sets.length === 0) return null;
194
+ values.push(id);
195
+ const [row] = await this.store.query(
196
+ `UPDATE fonderie_users SET ${sets.join(", ")}, updated_at = now() WHERE id = $${values.length} AND deleted_at IS NULL RETURNING id`,
197
+ values
198
+ );
199
+ return row ?? null;
200
+ }
201
+ async getMfaSecret(id) {
202
+ const [row] = await this.store.query(
203
+ `SELECT mfa_secret FROM fonderie_users WHERE id = $1`,
204
+ [id]
205
+ );
206
+ return row?.mfa_secret ?? null;
207
+ }
208
+ async upsertByProvider(email, provider, providerId) {
209
+ const [row] = await this.store.query(
210
+ `INSERT INTO fonderie_users (email, email_verified_at, provider, provider_id)
211
+ VALUES ($1, now(), $2, $3)
212
+ ON CONFLICT (email) DO UPDATE
213
+ SET provider = $2, provider_id = $3
214
+ RETURNING id`,
215
+ [email, provider, providerId]
216
+ );
217
+ return row ?? null;
218
+ }
219
+ };
220
+
221
+ // src/middlewares/session.ts
222
+ function withSession(store, config) {
223
+ const users = new UserModel(store);
224
+ return async (ctx, next) => {
225
+ const token = extractToken(ctx.request);
226
+ if (!token) {
227
+ return next();
228
+ }
229
+ const payload = verifyToken(token, config);
230
+ if (!payload || payload.type !== "access") {
231
+ return next();
232
+ }
233
+ const user = await users.findById(payload.sub);
234
+ if (!user || user.suspended || user.deletedAt) {
235
+ return next();
236
+ }
237
+ Object.assign(ctx, {
238
+ user: {
239
+ ...user,
240
+ loginMethod: payload.loginMethod ?? "email",
241
+ phoneVerified: payload.phoneVerified ?? false,
242
+ mfaPending: payload.mfaPending ?? false
243
+ }
244
+ });
245
+ return next();
246
+ };
247
+ }
248
+ function extractToken(request) {
249
+ const auth = request.headers.get("authorization");
250
+ if (auth?.startsWith("Bearer ")) {
251
+ return auth.slice(7);
252
+ }
253
+ const cookie = request.headers.get("cookie") ?? "";
254
+ const match = cookie.match(/(?:^|;\s*)access_token=([^;]+)/);
255
+ return match?.[1] ?? null;
256
+ }
257
+
258
+ // src/middlewares/require-auth.ts
259
+ import { requireAuth, requireAnyAuth } from "@fonderie/core/middlewares";
260
+
261
+ // src/middlewares/index.ts
262
+ var middlewares = {
263
+ requireAuth,
264
+ withSession
265
+ };
266
+ var middlewares_default = middlewares;
267
+ export {
268
+ middlewares_default as default
269
+ };
270
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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":[]}
@@ -0,0 +1,7 @@
1
+ // src/migrations/index.ts
2
+ import { createMigrationsPath } from "@fonderie/store";
3
+ var getMigrationsPath = () => createMigrationsPath(import.meta.url);
4
+ export {
5
+ getMigrationsPath
6
+ };
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/migrations/index.ts"],"sourcesContent":["import { createMigrationsPath } from '@fonderie/store';\n\nexport const getMigrationsPath = (): string => createMigrationsPath(import.meta.url);\n"],"mappings":";AAAA,SAAS,4BAA4B;AAE9B,IAAM,oBAAoB,MAAc,qBAAqB,YAAY,GAAG;","names":[]}
@@ -0,0 +1,73 @@
1
+ -- ── fonderie_users ───────────────────────────────────────────────
2
+ CREATE TABLE IF NOT EXISTS fonderie_users (
3
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
4
+ email TEXT NOT NULL UNIQUE,
5
+ password_hash TEXT,
6
+ first_name TEXT,
7
+ last_name TEXT,
8
+ phone TEXT,
9
+ profile_image_url TEXT,
10
+ locale TEXT NOT NULL DEFAULT 'en-US',
11
+ timezone TEXT NOT NULL DEFAULT 'UTC',
12
+ provider TEXT,
13
+ provider_id TEXT,
14
+ is_active BOOLEAN NOT NULL DEFAULT true,
15
+ last_login TIMESTAMPTZ,
16
+ preferences JSONB NOT NULL DEFAULT '{"notifications":{"email":true,"inApp":true,"sms":false,"push":false},"emailDigest":"immediate","dateFormat":"MM/DD/YYYY","timeFormat":"hh:mm A"}',
17
+ suspended BOOLEAN NOT NULL DEFAULT false,
18
+ whitelist BOOLEAN NOT NULL DEFAULT false,
19
+ ip_whitelist JSONB NOT NULL DEFAULT '[]',
20
+ mfa_enabled BOOLEAN NOT NULL DEFAULT false,
21
+ mfa_secret TEXT,
22
+ email_verified_at TIMESTAMPTZ,
23
+ deleted_at TIMESTAMPTZ,
24
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
25
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
26
+ );
27
+
28
+ CREATE INDEX IF NOT EXISTS idx_fonderie_users_email ON fonderie_users (email);
29
+
30
+ -- ── fonderie_email_verifications ─────────────────────────────────
31
+ CREATE TABLE IF NOT EXISTS fonderie_email_verifications (
32
+ token TEXT PRIMARY KEY,
33
+ user_id UUID NOT NULL REFERENCES fonderie_users(id) ON DELETE CASCADE,
34
+ expires_at TIMESTAMPTZ NOT NULL,
35
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
36
+ );
37
+
38
+ -- ── fonderie_password_resets ─────────────────────────────────────
39
+ CREATE TABLE IF NOT EXISTS fonderie_password_resets (
40
+ user_id UUID PRIMARY KEY REFERENCES fonderie_users(id) ON DELETE CASCADE,
41
+ token TEXT NOT NULL UNIQUE,
42
+ expires_at TIMESTAMPTZ NOT NULL,
43
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
44
+ );
45
+
46
+ CREATE INDEX IF NOT EXISTS idx_fonderie_password_resets_token ON fonderie_password_resets (token);
47
+
48
+ -- ── fonderie_sessions ────────────────────────────────────────────
49
+ CREATE TABLE IF NOT EXISTS fonderie_sessions (
50
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
51
+ user_id UUID NOT NULL REFERENCES fonderie_users(id) ON DELETE CASCADE,
52
+ token TEXT NOT NULL UNIQUE,
53
+ user_agent TEXT,
54
+ ip_address TEXT,
55
+ expires_at TIMESTAMPTZ NOT NULL,
56
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
57
+ );
58
+
59
+ CREATE INDEX IF NOT EXISTS idx_fonderie_sessions_user_id ON fonderie_sessions (user_id);
60
+ CREATE INDEX IF NOT EXISTS idx_fonderie_sessions_token ON fonderie_sessions (token);
61
+ CREATE INDEX IF NOT EXISTS idx_fonderie_sessions_expires_at ON fonderie_sessions (expires_at);
62
+
63
+ -- ── fonderie_mfa_challenges ──────────────────────────────────────
64
+ CREATE TABLE IF NOT EXISTS fonderie_mfa_challenges (
65
+ token TEXT PRIMARY KEY,
66
+ user_id UUID NOT NULL REFERENCES fonderie_users(id) ON DELETE CASCADE,
67
+ expires_at TIMESTAMPTZ NOT NULL,
68
+ used_at TIMESTAMPTZ,
69
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
70
+ );
71
+
72
+ CREATE INDEX IF NOT EXISTS idx_fonderie_mfa_challenges_user_id ON fonderie_mfa_challenges (user_id);
73
+ CREATE INDEX IF NOT EXISTS idx_fonderie_mfa_challenges_expires_at ON fonderie_mfa_challenges (expires_at);
@@ -0,0 +1,20 @@
1
+ -- Make email nullable to support phone-only users
2
+ ALTER TABLE fonderie_users ALTER COLUMN email DROP NOT NULL;
3
+
4
+ -- Track when phone was last verified via OTP
5
+ ALTER TABLE fonderie_users ADD COLUMN IF NOT EXISTS
6
+ phone_verified_at TIMESTAMPTZ;
7
+
8
+ -- Unique constraint on phone (NULLs are distinct in PostgreSQL, so existing
9
+ -- rows with NULL phone are unaffected)
10
+ ALTER TABLE fonderie_users
11
+ ADD CONSTRAINT fonderie_users_phone_unique UNIQUE (phone);
12
+
13
+ -- ── fonderie_phone_verifications ─────────────────────────────────
14
+ -- One pending OTP per phone number at a time (phone is the PK)
15
+ CREATE TABLE IF NOT EXISTS fonderie_phone_verifications (
16
+ phone TEXT PRIMARY KEY,
17
+ otp TEXT NOT NULL,
18
+ expires_at TIMESTAMPTZ NOT NULL,
19
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
20
+ );
@@ -0,0 +1,4 @@
1
+ -- No longer needed: first_name/last_name are written directly to
2
+ -- fonderie_users on registration, so they never need to travel through
3
+ -- the verifications table.
4
+ SELECT 1;
@@ -0,0 +1,5 @@
1
+ -- Drop columns added speculatively in 003 — user is created immediately
2
+ -- on registration so names never need to be carried in this table.
3
+ ALTER TABLE fonderie_phone_verifications
4
+ DROP COLUMN IF EXISTS first_name,
5
+ DROP COLUMN IF EXISTS last_name;
@@ -0,0 +1,6 @@
1
+ -- Link phone verifications to a specific user so verify can be done by JWT alone
2
+ ALTER TABLE fonderie_phone_verifications
3
+ ADD COLUMN IF NOT EXISTS user_id UUID REFERENCES fonderie_users(id) ON DELETE CASCADE;
4
+
5
+ CREATE INDEX IF NOT EXISTS idx_phone_verifications_user_id
6
+ ON fonderie_phone_verifications (user_id);
@@ -0,0 +1 @@
1
+ ALTER TABLE fonderie_users DROP COLUMN IF EXISTS phone_verified_at;
@@ -0,0 +1,14 @@
1
+ -- Swap primary key from token to user_id so concurrent users can share the
2
+ -- same 6-digit PIN without colliding. Deduplicate first (keep newest row per
3
+ -- user) in case stale data exists, then promote user_id to PK.
4
+
5
+ DELETE FROM fonderie_email_verifications e1
6
+ USING fonderie_email_verifications e2
7
+ WHERE e1.user_id = e2.user_id
8
+ AND e1.created_at < e2.created_at;
9
+
10
+ ALTER TABLE fonderie_email_verifications
11
+ DROP CONSTRAINT fonderie_email_verifications_pkey;
12
+
13
+ ALTER TABLE fonderie_email_verifications
14
+ ADD PRIMARY KEY (user_id);
@@ -0,0 +1,4 @@
1
+ ALTER TABLE fonderie_password_resets RENAME COLUMN token TO pin;
2
+
3
+ DROP INDEX IF EXISTS idx_fonderie_password_resets_token;
4
+ CREATE INDEX IF NOT EXISTS idx_fonderie_password_resets_pin ON fonderie_password_resets (pin);
@@ -0,0 +1 @@
1
+ ALTER TABLE fonderie_password_resets ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ NOT NULL DEFAULT now();
@@ -0,0 +1,3 @@
1
+ ALTER TABLE fonderie_users
2
+ ADD COLUMN IF NOT EXISTS mfa_secret_pending TEXT,
3
+ ADD COLUMN IF NOT EXISTS mfa_secret_pending_expires_at TIMESTAMPTZ;
@@ -0,0 +1,10 @@
1
+ CREATE TABLE IF NOT EXISTS fonderie_mfa_backup_codes (
2
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
3
+ user_id UUID NOT NULL REFERENCES fonderie_users(id) ON DELETE CASCADE,
4
+ code_hash TEXT NOT NULL,
5
+ used_at TIMESTAMPTZ,
6
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
7
+ );
8
+
9
+ CREATE INDEX IF NOT EXISTS idx_fonderie_mfa_backup_codes_user_id
10
+ ON fonderie_mfa_backup_codes (user_id);
@@ -0,0 +1 @@
1
+ ALTER TABLE fonderie_users DROP COLUMN IF EXISTS skills;
@@ -0,0 +1,41 @@
1
+ import { Middleware } from '@fonderie/core';
2
+ import { IStoreAdapter } from '@fonderie/store';
3
+
4
+ interface IAuthSecrets {
5
+ jwtSecret: string;
6
+ google?: {
7
+ clientId: string;
8
+ clientSecret: string;
9
+ redirectUri: string;
10
+ };
11
+ }
12
+ interface IAuthRuntimeConfig {
13
+ sessionDuration?: string;
14
+ verificationCooldown?: number;
15
+ mfa?: boolean;
16
+ requireVerification?: boolean;
17
+ }
18
+ declare const AUTH_CONFIG_KEYS: Record<keyof IAuthRuntimeConfig, string>;
19
+ declare const MESSAGE_KEYS: {
20
+ readonly emailRegistration: "email-registration";
21
+ readonly emailVerification: "email-verification";
22
+ readonly passwordReset: "password-reset";
23
+ readonly phoneOtp: "phone-otp";
24
+ readonly mfaEnabled: "mfa-enabled";
25
+ readonly mfaDisabled: "mfa-disabled";
26
+ readonly mfaBackupCodesRegenerated: "mfa-backup-codes-regenerated";
27
+ readonly emailChanged: "email-changed";
28
+ readonly phoneChanged: "phone-changed";
29
+ };
30
+ type AuthMessageKey = (typeof MESSAGE_KEYS)[keyof typeof MESSAGE_KEYS];
31
+ interface IAuthConfig extends IAuthSecrets, IAuthRuntimeConfig {
32
+ providers: ('email' | 'phone' | 'google' | 'github')[];
33
+ appName?: string;
34
+ resolve?: (ctx: {
35
+ meta: Record<string, unknown>;
36
+ }) => Partial<IAuthRuntimeConfig>;
37
+ }
38
+
39
+ declare function withSession(store: IStoreAdapter, config: IAuthConfig): Middleware;
40
+
41
+ export { AUTH_CONFIG_KEYS as A, type IAuthConfig as I, MESSAGE_KEYS as M, type AuthMessageKey as a, type IAuthRuntimeConfig as b, type IAuthSecrets as c, withSession as w };
@@ -0,0 +1,41 @@
1
+ import { Middleware } from '@fonderie/core';
2
+ import { IStoreAdapter } from '@fonderie/store';
3
+
4
+ interface IAuthSecrets {
5
+ jwtSecret: string;
6
+ google?: {
7
+ clientId: string;
8
+ clientSecret: string;
9
+ redirectUri: string;
10
+ };
11
+ }
12
+ interface IAuthRuntimeConfig {
13
+ sessionDuration?: string;
14
+ verificationCooldown?: number;
15
+ mfa?: boolean;
16
+ requireVerification?: boolean;
17
+ }
18
+ declare const AUTH_CONFIG_KEYS: Record<keyof IAuthRuntimeConfig, string>;
19
+ declare const MESSAGE_KEYS: {
20
+ readonly emailRegistration: "email-registration";
21
+ readonly emailVerification: "email-verification";
22
+ readonly passwordReset: "password-reset";
23
+ readonly phoneOtp: "phone-otp";
24
+ readonly mfaEnabled: "mfa-enabled";
25
+ readonly mfaDisabled: "mfa-disabled";
26
+ readonly mfaBackupCodesRegenerated: "mfa-backup-codes-regenerated";
27
+ readonly emailChanged: "email-changed";
28
+ readonly phoneChanged: "phone-changed";
29
+ };
30
+ type AuthMessageKey = (typeof MESSAGE_KEYS)[keyof typeof MESSAGE_KEYS];
31
+ interface IAuthConfig extends IAuthSecrets, IAuthRuntimeConfig {
32
+ providers: ('email' | 'phone' | 'google' | 'github')[];
33
+ appName?: string;
34
+ resolve?: (ctx: {
35
+ meta: Record<string, unknown>;
36
+ }) => Partial<IAuthRuntimeConfig>;
37
+ }
38
+
39
+ declare function withSession(store: IStoreAdapter, config: IAuthConfig): Middleware;
40
+
41
+ export { AUTH_CONFIG_KEYS as A, type IAuthConfig as I, MESSAGE_KEYS as M, type AuthMessageKey as a, type IAuthRuntimeConfig as b, type IAuthSecrets as c, withSession as w };
package/dist/types.cjs ADDED
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __copyProps = (to, from, except, desc) => {
7
+ if (from && typeof from === "object" || typeof from === "function") {
8
+ for (let key of __getOwnPropNames(from))
9
+ if (!__hasOwnProp.call(to, key) && key !== except)
10
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
11
+ }
12
+ return to;
13
+ };
14
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
15
+
16
+ // src/types.ts
17
+ var types_exports = {};
18
+ module.exports = __toCommonJS(types_exports);
19
+ //# sourceMappingURL=types.cjs.map