@fonderie/auth 5.0.0 → 5.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -35,7 +35,7 @@ Also exports `toUserDTO`, `normalizeEmail`, and the full type surface
35
35
  You've shipped this plumbing before — auth, teams, billing, messaging —
36
36
  and the next project will ask for it again. Fonderie packages it once:
37
37
  plain TypeScript modules for
38
- [`@fonderie/core`](https://github.com/fonderie-js/sdk/tree/main/packages/core),
38
+ [`@fonderie/core`](https://github.com/fonderiejs/sdk/tree/main/packages/core),
39
39
  PostgreSQL-backed, self-hosted, MIT. No external control plane, no
40
40
  per-seat anything. Register the modules you need; skip the ones you don't.
41
41
 
@@ -43,7 +43,7 @@ per-seat anything. Register the modules you need; skip the ones you don't.
43
43
  other brick trusts the `ctx.user` this one establishes.
44
44
 
45
45
  Browse the whole set at
46
- [fonderie-js/sdk](https://github.com/fonderie-js/sdk) · follow
46
+ [fonderiejs/sdk](https://github.com/fonderiejs/sdk) · follow
47
47
  [@fonderiejs](https://x.com/fonderiejs)
48
48
 
49
49
  ## License
package/brain/outcomes.md CHANGED
@@ -131,6 +131,7 @@ Raw SQL ships in `node_modules/@fonderie/auth/dist/migrations/sql/` — read it
131
131
  | DELETE | `/users` | `requireAuth → verifyGate → user.deleteMe` |
132
132
  | GET | `/users` | `requireAuth → user.me` |
133
133
  | PUT | `/users/email` | `requireAuth → verifyGate → validate(updateEmailSchema) → user.updateEmail` |
134
+ | GET | `/users/export` | `requireAuth → user.exportMe` |
134
135
  | PUT | `/users/password` | `requireAuth → validate(changePasswordSchema) → user.changePassword` |
135
136
  | PUT | `/users/phone` | `requireAuth → verifyGate → validate(updatePhoneSchema) → user.updatePhone` |
136
137
  | PUT | `/users/preferences` | `requireAuth → verifyGate → validate(updatePreferencesSchema) → user.updatePreferences` |
@@ -63,10 +63,12 @@ interface IAuthConfig extends IAuthSecrets, IAuthRuntimeConfig {
63
63
  }) => Partial<IAuthRuntimeConfig>;
64
64
  routes?: Partial<Record<AuthRouteId, AuthRouteOverride>>;
65
65
  legacyVerify?: (plain: string, hash: string) => boolean | Promise<boolean>;
66
+ dataExportContributors?: IDataExportContributor[];
66
67
  }
67
68
 
68
69
  interface IAuthSecrets {
69
70
  jwtSecret: string;
71
+ mfaSecretKey?: string;
70
72
  google?: {
71
73
  clientId: string;
72
74
  clientSecret: string;
@@ -81,6 +83,11 @@ interface IAuthRuntimeConfig {
81
83
  requireVerification?: boolean;
82
84
  }
83
85
 
86
+ interface IDataExportContributor {
87
+ name: string;
88
+ collect: (userId: string) => Promise<unknown> | unknown;
89
+ }
90
+
84
91
  const AUTH_CONFIG_KEYS: { sessionDuration: string; verificationCooldown: string; mfa: string; requireVerification: string; }
85
92
 
86
93
  const MESSAGE_KEYS: { readonly emailRegistration: "email-registration"; readonly emailVerification: "email-verification"; readonly passwordReset: "password-reset"; readonly phoneOtp: "phone-otp"; readonly mfaEnabled: "mfa-enabled"; readonly mfaDisabled: "mfa-disabled"; readonly mfaBackupCodesRegenerated: "mfa-backup-codes-regenerated"; readonly emailChanged: "email-changed"; readonly phoneChanged: "phone-changed"; }
@@ -131,6 +138,19 @@ function normalizeEmailSafe(email: string): string | null
131
138
 
132
139
  function importUser(store: IStoreAdapter, user: IImportUser): Promise<{ id: string; }>
133
140
 
141
+ function purgeSoftDeletedUsers(store: IStoreAdapter, { olderThanDays }: IPurgeOptions): Promise<number>
142
+
143
+ function startUserRetention(store: IStoreAdapter, options: IUserRetentionScheduleOptions): { stop: () => void; }
144
+
145
+ interface IPurgeOptions {
146
+ olderThanDays: number;
147
+ }
148
+
149
+ interface IUserRetentionScheduleOptions extends IPurgeOptions {
150
+ intervalMs?: number;
151
+ onPurge?: (deleted: number) => void;
152
+ }
153
+
134
154
  interface IImportUser {
135
155
  id?: string;
136
156
  email: string;
@@ -32,8 +32,27 @@ var DEFAULT_PREFERENCES = {
32
32
  dateFormat: "MM/DD/YYYY",
33
33
  timeFormat: "hh:mm A"
34
34
  };
35
+ function cleanPreferences(prefs) {
36
+ const out = {};
37
+ if (typeof prefs.locale === "string") out.locale = prefs.locale;
38
+ if (typeof prefs.timezone === "string") out.timezone = prefs.timezone;
39
+ if (typeof prefs.emailDigest === "string") out.emailDigest = prefs.emailDigest;
40
+ if (typeof prefs.dateFormat === "string") out.dateFormat = prefs.dateFormat;
41
+ if (typeof prefs.timeFormat === "string") out.timeFormat = prefs.timeFormat;
42
+ return out;
43
+ }
44
+ function cleanNotifications(value) {
45
+ if (!value || typeof value !== "object") return {};
46
+ const out = {};
47
+ for (const key of ["email", "inApp", "sms", "push"]) {
48
+ const v = value[key];
49
+ if (typeof v === "boolean") out[key] = v;
50
+ }
51
+ return out;
52
+ }
35
53
  function toUserDTO(user, phoneVerified = false) {
36
54
  const prefs = user.preferences ?? {};
55
+ const cleaned = cleanPreferences(prefs);
37
56
  return {
38
57
  id: (0, import_core.stringOrEmpty)(user.id),
39
58
  email: (0, import_core.stringOrEmpty)(user.email),
@@ -45,9 +64,13 @@ function toUserDTO(user, phoneVerified = false) {
45
64
  lastLogin: user.lastLogin instanceof Date ? user.lastLogin.toISOString() : "",
46
65
  preferences: {
47
66
  ...DEFAULT_PREFERENCES,
48
- ...prefs,
49
- locale: user.locale || prefs.locale || "en-US",
50
- timezone: user.timezone || prefs.timezone || "UTC"
67
+ ...cleaned,
68
+ notifications: {
69
+ ...DEFAULT_PREFERENCES.notifications,
70
+ ...cleanNotifications(prefs.notifications)
71
+ },
72
+ locale: user.locale || cleaned.locale || "en-US",
73
+ timezone: user.timezone || cleaned.timezone || "UTC"
51
74
  },
52
75
  isEmailVerified: user.emailVerifiedAt !== null,
53
76
  isPhoneVerified: phoneVerified,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/dtos/user.ts"],"sourcesContent":["import { stringOrEmpty, booleanOrFalse } from '@fonderie/core';\n\nimport type { IUser, IUserPreferences } from '../types';\n\nexport interface IUserDTO {\n\tid: string;\n\temail: string;\n\tfirstName: string;\n\tlastName: string;\n\tphone: string;\n\tprofileImageUrl: string;\n\tisActive: boolean;\n\tlastLogin: string;\n\tpreferences: IUserPreferences;\n\tisEmailVerified: boolean;\n\tisPhoneVerified: boolean;\n\tmfaEnabled: boolean;\n\tsuspended: boolean;\n\twhitelist: boolean;\n\tipWhitelist: string[];\n\tcreatedAt: string;\n\tupdatedAt: string;\n}\n\nconst DEFAULT_PREFERENCES: IUserPreferences = {\n\tlocale: 'en-US',\n\ttimezone: 'UTC',\n\tnotifications: { email: true, inApp: true, sms: false, push: false },\n\temailDigest: 'immediate',\n\tdateFormat: 'MM/DD/YYYY',\n\ttimeFormat: 'hh:mm A',\n};\n\nexport function toUserDTO(user: IUser, phoneVerified = false): IUserDTO {\n\tconst prefs = user.preferences ?? ({} as IUserPreferences);\n\treturn {\n\t\tid: stringOrEmpty(user.id),\n\t\temail: stringOrEmpty(user.email),\n\t\tfirstName: stringOrEmpty(user.firstName),\n\t\tlastName: stringOrEmpty(user.lastName),\n\t\tphone: stringOrEmpty(user.phone),\n\t\tprofileImageUrl: stringOrEmpty(user.profileImageUrl),\n\t\tisActive: typeof user.isActive === 'boolean' ? user.isActive : true,\n\t\tlastLogin: user.lastLogin instanceof Date ? user.lastLogin.toISOString() : '',\n\t\tpreferences: {\n\t\t\t...DEFAULT_PREFERENCES,\n\t\t\t...prefs,\n\t\t\tlocale: user.locale || prefs.locale || 'en-US',\n\t\t\ttimezone: user.timezone || prefs.timezone || 'UTC',\n\t\t},\n\t\tisEmailVerified: user.emailVerifiedAt !== null,\n\t\tisPhoneVerified: phoneVerified,\n\t\tmfaEnabled: booleanOrFalse(user.mfaEnabled),\n\t\tsuspended: booleanOrFalse(user.suspended),\n\t\twhitelist: booleanOrFalse(user.whitelist),\n\t\tipWhitelist: Array.isArray(user.ipWhitelist) ? user.ipWhitelist : [],\n\t\tcreatedAt: user.createdAt instanceof Date ? user.createdAt.toISOString() : '',\n\t\tupdatedAt: user.updatedAt instanceof Date ? user.updatedAt.toISOString() : '',\n\t};\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,kBAA8C;AAwB9C,IAAM,sBAAwC;AAAA,EAC7C,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,eAAe,EAAE,OAAO,MAAM,OAAO,MAAM,KAAK,OAAO,MAAM,MAAM;AAAA,EACnE,aAAa;AAAA,EACb,YAAY;AAAA,EACZ,YAAY;AACb;AAEO,SAAS,UAAU,MAAa,gBAAgB,OAAiB;AACvE,QAAM,QAAQ,KAAK,eAAgB,CAAC;AACpC,SAAO;AAAA,IACN,QAAI,2BAAc,KAAK,EAAE;AAAA,IACzB,WAAO,2BAAc,KAAK,KAAK;AAAA,IAC/B,eAAW,2BAAc,KAAK,SAAS;AAAA,IACvC,cAAU,2BAAc,KAAK,QAAQ;AAAA,IACrC,WAAO,2BAAc,KAAK,KAAK;AAAA,IAC/B,qBAAiB,2BAAc,KAAK,eAAe;AAAA,IACnD,UAAU,OAAO,KAAK,aAAa,YAAY,KAAK,WAAW;AAAA,IAC/D,WAAW,KAAK,qBAAqB,OAAO,KAAK,UAAU,YAAY,IAAI;AAAA,IAC3E,aAAa;AAAA,MACZ,GAAG;AAAA,MACH,GAAG;AAAA,MACH,QAAQ,KAAK,UAAU,MAAM,UAAU;AAAA,MACvC,UAAU,KAAK,YAAY,MAAM,YAAY;AAAA,IAC9C;AAAA,IACA,iBAAiB,KAAK,oBAAoB;AAAA,IAC1C,iBAAiB;AAAA,IACjB,gBAAY,4BAAe,KAAK,UAAU;AAAA,IAC1C,eAAW,4BAAe,KAAK,SAAS;AAAA,IACxC,eAAW,4BAAe,KAAK,SAAS;AAAA,IACxC,aAAa,MAAM,QAAQ,KAAK,WAAW,IAAI,KAAK,cAAc,CAAC;AAAA,IACnE,WAAW,KAAK,qBAAqB,OAAO,KAAK,UAAU,YAAY,IAAI;AAAA,IAC3E,WAAW,KAAK,qBAAqB,OAAO,KAAK,UAAU,YAAY,IAAI;AAAA,EAC5E;AACD;","names":[]}
1
+ {"version":3,"sources":["../../src/dtos/user.ts"],"sourcesContent":["import { stringOrEmpty, booleanOrFalse } from '@fonderie/core';\n\nimport type { IUser, IUserPreferences } from '../types';\n\nexport interface IUserDTO {\n\tid: string;\n\temail: string;\n\tfirstName: string;\n\tlastName: string;\n\tphone: string;\n\tprofileImageUrl: string;\n\tisActive: boolean;\n\tlastLogin: string;\n\tpreferences: IUserPreferences;\n\tisEmailVerified: boolean;\n\tisPhoneVerified: boolean;\n\tmfaEnabled: boolean;\n\tsuspended: boolean;\n\twhitelist: boolean;\n\tipWhitelist: string[];\n\tcreatedAt: string;\n\tupdatedAt: string;\n}\n\nconst DEFAULT_PREFERENCES: IUserPreferences = {\n\tlocale: 'en-US',\n\ttimezone: 'UTC',\n\tnotifications: { email: true, inApp: true, sms: false, push: false },\n\temailDigest: 'immediate',\n\tdateFormat: 'MM/DD/YYYY',\n\ttimeFormat: 'hh:mm A',\n};\n\n// Historical rows may hold garbage preferences (the update schema accepted\n// `unknown` for four keys until it was typed). Only well-typed values\n// survive into the DTO, and notifications deep-merge over the defaults so a\n// partial stored object can't shrink the shape the client type promises.\nfunction cleanPreferences(prefs: IUserPreferences): Partial<IUserPreferences> {\n\tconst out: Partial<IUserPreferences> = {};\n\tif (typeof prefs.locale === 'string') out.locale = prefs.locale;\n\tif (typeof prefs.timezone === 'string') out.timezone = prefs.timezone;\n\tif (typeof prefs.emailDigest === 'string') out.emailDigest = prefs.emailDigest;\n\tif (typeof prefs.dateFormat === 'string') out.dateFormat = prefs.dateFormat;\n\tif (typeof prefs.timeFormat === 'string') out.timeFormat = prefs.timeFormat;\n\treturn out;\n}\n\nfunction cleanNotifications(value: unknown): Partial<IUserPreferences['notifications']> {\n\tif (!value || typeof value !== 'object') return {};\n\tconst out: Partial<IUserPreferences['notifications']> = {};\n\tfor (const key of ['email', 'inApp', 'sms', 'push'] as const) {\n\t\tconst v = (value as Record<string, unknown>)[key];\n\t\tif (typeof v === 'boolean') out[key] = v;\n\t}\n\treturn out;\n}\n\nexport function toUserDTO(user: IUser, phoneVerified = false): IUserDTO {\n\tconst prefs = user.preferences ?? ({} as IUserPreferences);\n\tconst cleaned = cleanPreferences(prefs);\n\treturn {\n\t\tid: stringOrEmpty(user.id),\n\t\temail: stringOrEmpty(user.email),\n\t\tfirstName: stringOrEmpty(user.firstName),\n\t\tlastName: stringOrEmpty(user.lastName),\n\t\tphone: stringOrEmpty(user.phone),\n\t\tprofileImageUrl: stringOrEmpty(user.profileImageUrl),\n\t\tisActive: typeof user.isActive === 'boolean' ? user.isActive : true,\n\t\tlastLogin: user.lastLogin instanceof Date ? user.lastLogin.toISOString() : '',\n\t\tpreferences: {\n\t\t\t...DEFAULT_PREFERENCES,\n\t\t\t...cleaned,\n\t\t\tnotifications: {\n\t\t\t\t...DEFAULT_PREFERENCES.notifications,\n\t\t\t\t...cleanNotifications(prefs.notifications),\n\t\t\t},\n\t\t\tlocale: user.locale || cleaned.locale || 'en-US',\n\t\t\ttimezone: user.timezone || cleaned.timezone || 'UTC',\n\t\t},\n\t\tisEmailVerified: user.emailVerifiedAt !== null,\n\t\tisPhoneVerified: phoneVerified,\n\t\tmfaEnabled: booleanOrFalse(user.mfaEnabled),\n\t\tsuspended: booleanOrFalse(user.suspended),\n\t\twhitelist: booleanOrFalse(user.whitelist),\n\t\tipWhitelist: Array.isArray(user.ipWhitelist) ? user.ipWhitelist : [],\n\t\tcreatedAt: user.createdAt instanceof Date ? user.createdAt.toISOString() : '',\n\t\tupdatedAt: user.updatedAt instanceof Date ? user.updatedAt.toISOString() : '',\n\t};\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,kBAA8C;AAwB9C,IAAM,sBAAwC;AAAA,EAC7C,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,eAAe,EAAE,OAAO,MAAM,OAAO,MAAM,KAAK,OAAO,MAAM,MAAM;AAAA,EACnE,aAAa;AAAA,EACb,YAAY;AAAA,EACZ,YAAY;AACb;AAMA,SAAS,iBAAiB,OAAoD;AAC7E,QAAM,MAAiC,CAAC;AACxC,MAAI,OAAO,MAAM,WAAW,SAAU,KAAI,SAAS,MAAM;AACzD,MAAI,OAAO,MAAM,aAAa,SAAU,KAAI,WAAW,MAAM;AAC7D,MAAI,OAAO,MAAM,gBAAgB,SAAU,KAAI,cAAc,MAAM;AACnE,MAAI,OAAO,MAAM,eAAe,SAAU,KAAI,aAAa,MAAM;AACjE,MAAI,OAAO,MAAM,eAAe,SAAU,KAAI,aAAa,MAAM;AACjE,SAAO;AACR;AAEA,SAAS,mBAAmB,OAA4D;AACvF,MAAI,CAAC,SAAS,OAAO,UAAU,SAAU,QAAO,CAAC;AACjD,QAAM,MAAkD,CAAC;AACzD,aAAW,OAAO,CAAC,SAAS,SAAS,OAAO,MAAM,GAAY;AAC7D,UAAM,IAAK,MAAkC,GAAG;AAChD,QAAI,OAAO,MAAM,UAAW,KAAI,GAAG,IAAI;AAAA,EACxC;AACA,SAAO;AACR;AAEO,SAAS,UAAU,MAAa,gBAAgB,OAAiB;AACvE,QAAM,QAAQ,KAAK,eAAgB,CAAC;AACpC,QAAM,UAAU,iBAAiB,KAAK;AACtC,SAAO;AAAA,IACN,QAAI,2BAAc,KAAK,EAAE;AAAA,IACzB,WAAO,2BAAc,KAAK,KAAK;AAAA,IAC/B,eAAW,2BAAc,KAAK,SAAS;AAAA,IACvC,cAAU,2BAAc,KAAK,QAAQ;AAAA,IACrC,WAAO,2BAAc,KAAK,KAAK;AAAA,IAC/B,qBAAiB,2BAAc,KAAK,eAAe;AAAA,IACnD,UAAU,OAAO,KAAK,aAAa,YAAY,KAAK,WAAW;AAAA,IAC/D,WAAW,KAAK,qBAAqB,OAAO,KAAK,UAAU,YAAY,IAAI;AAAA,IAC3E,aAAa;AAAA,MACZ,GAAG;AAAA,MACH,GAAG;AAAA,MACH,eAAe;AAAA,QACd,GAAG,oBAAoB;AAAA,QACvB,GAAG,mBAAmB,MAAM,aAAa;AAAA,MAC1C;AAAA,MACA,QAAQ,KAAK,UAAU,QAAQ,UAAU;AAAA,MACzC,UAAU,KAAK,YAAY,QAAQ,YAAY;AAAA,IAChD;AAAA,IACA,iBAAiB,KAAK,oBAAoB;AAAA,IAC1C,iBAAiB;AAAA,IACjB,gBAAY,4BAAe,KAAK,UAAU;AAAA,IAC1C,eAAW,4BAAe,KAAK,SAAS;AAAA,IACxC,eAAW,4BAAe,KAAK,SAAS;AAAA,IACxC,aAAa,MAAM,QAAQ,KAAK,WAAW,IAAI,KAAK,cAAc,CAAC;AAAA,IACnE,WAAW,KAAK,qBAAqB,OAAO,KAAK,UAAU,YAAY,IAAI;AAAA,IAC3E,WAAW,KAAK,qBAAqB,OAAO,KAAK,UAAU,YAAY,IAAI;AAAA,EAC5E;AACD;","names":[]}
package/dist/dtos/user.js CHANGED
@@ -8,8 +8,27 @@ var DEFAULT_PREFERENCES = {
8
8
  dateFormat: "MM/DD/YYYY",
9
9
  timeFormat: "hh:mm A"
10
10
  };
11
+ function cleanPreferences(prefs) {
12
+ const out = {};
13
+ if (typeof prefs.locale === "string") out.locale = prefs.locale;
14
+ if (typeof prefs.timezone === "string") out.timezone = prefs.timezone;
15
+ if (typeof prefs.emailDigest === "string") out.emailDigest = prefs.emailDigest;
16
+ if (typeof prefs.dateFormat === "string") out.dateFormat = prefs.dateFormat;
17
+ if (typeof prefs.timeFormat === "string") out.timeFormat = prefs.timeFormat;
18
+ return out;
19
+ }
20
+ function cleanNotifications(value) {
21
+ if (!value || typeof value !== "object") return {};
22
+ const out = {};
23
+ for (const key of ["email", "inApp", "sms", "push"]) {
24
+ const v = value[key];
25
+ if (typeof v === "boolean") out[key] = v;
26
+ }
27
+ return out;
28
+ }
11
29
  function toUserDTO(user, phoneVerified = false) {
12
30
  const prefs = user.preferences ?? {};
31
+ const cleaned = cleanPreferences(prefs);
13
32
  return {
14
33
  id: stringOrEmpty(user.id),
15
34
  email: stringOrEmpty(user.email),
@@ -21,9 +40,13 @@ function toUserDTO(user, phoneVerified = false) {
21
40
  lastLogin: user.lastLogin instanceof Date ? user.lastLogin.toISOString() : "",
22
41
  preferences: {
23
42
  ...DEFAULT_PREFERENCES,
24
- ...prefs,
25
- locale: user.locale || prefs.locale || "en-US",
26
- timezone: user.timezone || prefs.timezone || "UTC"
43
+ ...cleaned,
44
+ notifications: {
45
+ ...DEFAULT_PREFERENCES.notifications,
46
+ ...cleanNotifications(prefs.notifications)
47
+ },
48
+ locale: user.locale || cleaned.locale || "en-US",
49
+ timezone: user.timezone || cleaned.timezone || "UTC"
27
50
  },
28
51
  isEmailVerified: user.emailVerifiedAt !== null,
29
52
  isPhoneVerified: phoneVerified,
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/dtos/user.ts"],"sourcesContent":["import { stringOrEmpty, booleanOrFalse } from '@fonderie/core';\n\nimport type { IUser, IUserPreferences } from '../types';\n\nexport interface IUserDTO {\n\tid: string;\n\temail: string;\n\tfirstName: string;\n\tlastName: string;\n\tphone: string;\n\tprofileImageUrl: string;\n\tisActive: boolean;\n\tlastLogin: string;\n\tpreferences: IUserPreferences;\n\tisEmailVerified: boolean;\n\tisPhoneVerified: boolean;\n\tmfaEnabled: boolean;\n\tsuspended: boolean;\n\twhitelist: boolean;\n\tipWhitelist: string[];\n\tcreatedAt: string;\n\tupdatedAt: string;\n}\n\nconst DEFAULT_PREFERENCES: IUserPreferences = {\n\tlocale: 'en-US',\n\ttimezone: 'UTC',\n\tnotifications: { email: true, inApp: true, sms: false, push: false },\n\temailDigest: 'immediate',\n\tdateFormat: 'MM/DD/YYYY',\n\ttimeFormat: 'hh:mm A',\n};\n\nexport function toUserDTO(user: IUser, phoneVerified = false): IUserDTO {\n\tconst prefs = user.preferences ?? ({} as IUserPreferences);\n\treturn {\n\t\tid: stringOrEmpty(user.id),\n\t\temail: stringOrEmpty(user.email),\n\t\tfirstName: stringOrEmpty(user.firstName),\n\t\tlastName: stringOrEmpty(user.lastName),\n\t\tphone: stringOrEmpty(user.phone),\n\t\tprofileImageUrl: stringOrEmpty(user.profileImageUrl),\n\t\tisActive: typeof user.isActive === 'boolean' ? user.isActive : true,\n\t\tlastLogin: user.lastLogin instanceof Date ? user.lastLogin.toISOString() : '',\n\t\tpreferences: {\n\t\t\t...DEFAULT_PREFERENCES,\n\t\t\t...prefs,\n\t\t\tlocale: user.locale || prefs.locale || 'en-US',\n\t\t\ttimezone: user.timezone || prefs.timezone || 'UTC',\n\t\t},\n\t\tisEmailVerified: user.emailVerifiedAt !== null,\n\t\tisPhoneVerified: phoneVerified,\n\t\tmfaEnabled: booleanOrFalse(user.mfaEnabled),\n\t\tsuspended: booleanOrFalse(user.suspended),\n\t\twhitelist: booleanOrFalse(user.whitelist),\n\t\tipWhitelist: Array.isArray(user.ipWhitelist) ? user.ipWhitelist : [],\n\t\tcreatedAt: user.createdAt instanceof Date ? user.createdAt.toISOString() : '',\n\t\tupdatedAt: user.updatedAt instanceof Date ? user.updatedAt.toISOString() : '',\n\t};\n}\n"],"mappings":";AAAA,SAAS,eAAe,sBAAsB;AAwB9C,IAAM,sBAAwC;AAAA,EAC7C,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,eAAe,EAAE,OAAO,MAAM,OAAO,MAAM,KAAK,OAAO,MAAM,MAAM;AAAA,EACnE,aAAa;AAAA,EACb,YAAY;AAAA,EACZ,YAAY;AACb;AAEO,SAAS,UAAU,MAAa,gBAAgB,OAAiB;AACvE,QAAM,QAAQ,KAAK,eAAgB,CAAC;AACpC,SAAO;AAAA,IACN,IAAI,cAAc,KAAK,EAAE;AAAA,IACzB,OAAO,cAAc,KAAK,KAAK;AAAA,IAC/B,WAAW,cAAc,KAAK,SAAS;AAAA,IACvC,UAAU,cAAc,KAAK,QAAQ;AAAA,IACrC,OAAO,cAAc,KAAK,KAAK;AAAA,IAC/B,iBAAiB,cAAc,KAAK,eAAe;AAAA,IACnD,UAAU,OAAO,KAAK,aAAa,YAAY,KAAK,WAAW;AAAA,IAC/D,WAAW,KAAK,qBAAqB,OAAO,KAAK,UAAU,YAAY,IAAI;AAAA,IAC3E,aAAa;AAAA,MACZ,GAAG;AAAA,MACH,GAAG;AAAA,MACH,QAAQ,KAAK,UAAU,MAAM,UAAU;AAAA,MACvC,UAAU,KAAK,YAAY,MAAM,YAAY;AAAA,IAC9C;AAAA,IACA,iBAAiB,KAAK,oBAAoB;AAAA,IAC1C,iBAAiB;AAAA,IACjB,YAAY,eAAe,KAAK,UAAU;AAAA,IAC1C,WAAW,eAAe,KAAK,SAAS;AAAA,IACxC,WAAW,eAAe,KAAK,SAAS;AAAA,IACxC,aAAa,MAAM,QAAQ,KAAK,WAAW,IAAI,KAAK,cAAc,CAAC;AAAA,IACnE,WAAW,KAAK,qBAAqB,OAAO,KAAK,UAAU,YAAY,IAAI;AAAA,IAC3E,WAAW,KAAK,qBAAqB,OAAO,KAAK,UAAU,YAAY,IAAI;AAAA,EAC5E;AACD;","names":[]}
1
+ {"version":3,"sources":["../../src/dtos/user.ts"],"sourcesContent":["import { stringOrEmpty, booleanOrFalse } from '@fonderie/core';\n\nimport type { IUser, IUserPreferences } from '../types';\n\nexport interface IUserDTO {\n\tid: string;\n\temail: string;\n\tfirstName: string;\n\tlastName: string;\n\tphone: string;\n\tprofileImageUrl: string;\n\tisActive: boolean;\n\tlastLogin: string;\n\tpreferences: IUserPreferences;\n\tisEmailVerified: boolean;\n\tisPhoneVerified: boolean;\n\tmfaEnabled: boolean;\n\tsuspended: boolean;\n\twhitelist: boolean;\n\tipWhitelist: string[];\n\tcreatedAt: string;\n\tupdatedAt: string;\n}\n\nconst DEFAULT_PREFERENCES: IUserPreferences = {\n\tlocale: 'en-US',\n\ttimezone: 'UTC',\n\tnotifications: { email: true, inApp: true, sms: false, push: false },\n\temailDigest: 'immediate',\n\tdateFormat: 'MM/DD/YYYY',\n\ttimeFormat: 'hh:mm A',\n};\n\n// Historical rows may hold garbage preferences (the update schema accepted\n// `unknown` for four keys until it was typed). Only well-typed values\n// survive into the DTO, and notifications deep-merge over the defaults so a\n// partial stored object can't shrink the shape the client type promises.\nfunction cleanPreferences(prefs: IUserPreferences): Partial<IUserPreferences> {\n\tconst out: Partial<IUserPreferences> = {};\n\tif (typeof prefs.locale === 'string') out.locale = prefs.locale;\n\tif (typeof prefs.timezone === 'string') out.timezone = prefs.timezone;\n\tif (typeof prefs.emailDigest === 'string') out.emailDigest = prefs.emailDigest;\n\tif (typeof prefs.dateFormat === 'string') out.dateFormat = prefs.dateFormat;\n\tif (typeof prefs.timeFormat === 'string') out.timeFormat = prefs.timeFormat;\n\treturn out;\n}\n\nfunction cleanNotifications(value: unknown): Partial<IUserPreferences['notifications']> {\n\tif (!value || typeof value !== 'object') return {};\n\tconst out: Partial<IUserPreferences['notifications']> = {};\n\tfor (const key of ['email', 'inApp', 'sms', 'push'] as const) {\n\t\tconst v = (value as Record<string, unknown>)[key];\n\t\tif (typeof v === 'boolean') out[key] = v;\n\t}\n\treturn out;\n}\n\nexport function toUserDTO(user: IUser, phoneVerified = false): IUserDTO {\n\tconst prefs = user.preferences ?? ({} as IUserPreferences);\n\tconst cleaned = cleanPreferences(prefs);\n\treturn {\n\t\tid: stringOrEmpty(user.id),\n\t\temail: stringOrEmpty(user.email),\n\t\tfirstName: stringOrEmpty(user.firstName),\n\t\tlastName: stringOrEmpty(user.lastName),\n\t\tphone: stringOrEmpty(user.phone),\n\t\tprofileImageUrl: stringOrEmpty(user.profileImageUrl),\n\t\tisActive: typeof user.isActive === 'boolean' ? user.isActive : true,\n\t\tlastLogin: user.lastLogin instanceof Date ? user.lastLogin.toISOString() : '',\n\t\tpreferences: {\n\t\t\t...DEFAULT_PREFERENCES,\n\t\t\t...cleaned,\n\t\t\tnotifications: {\n\t\t\t\t...DEFAULT_PREFERENCES.notifications,\n\t\t\t\t...cleanNotifications(prefs.notifications),\n\t\t\t},\n\t\t\tlocale: user.locale || cleaned.locale || 'en-US',\n\t\t\ttimezone: user.timezone || cleaned.timezone || 'UTC',\n\t\t},\n\t\tisEmailVerified: user.emailVerifiedAt !== null,\n\t\tisPhoneVerified: phoneVerified,\n\t\tmfaEnabled: booleanOrFalse(user.mfaEnabled),\n\t\tsuspended: booleanOrFalse(user.suspended),\n\t\twhitelist: booleanOrFalse(user.whitelist),\n\t\tipWhitelist: Array.isArray(user.ipWhitelist) ? user.ipWhitelist : [],\n\t\tcreatedAt: user.createdAt instanceof Date ? user.createdAt.toISOString() : '',\n\t\tupdatedAt: user.updatedAt instanceof Date ? user.updatedAt.toISOString() : '',\n\t};\n}\n"],"mappings":";AAAA,SAAS,eAAe,sBAAsB;AAwB9C,IAAM,sBAAwC;AAAA,EAC7C,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,eAAe,EAAE,OAAO,MAAM,OAAO,MAAM,KAAK,OAAO,MAAM,MAAM;AAAA,EACnE,aAAa;AAAA,EACb,YAAY;AAAA,EACZ,YAAY;AACb;AAMA,SAAS,iBAAiB,OAAoD;AAC7E,QAAM,MAAiC,CAAC;AACxC,MAAI,OAAO,MAAM,WAAW,SAAU,KAAI,SAAS,MAAM;AACzD,MAAI,OAAO,MAAM,aAAa,SAAU,KAAI,WAAW,MAAM;AAC7D,MAAI,OAAO,MAAM,gBAAgB,SAAU,KAAI,cAAc,MAAM;AACnE,MAAI,OAAO,MAAM,eAAe,SAAU,KAAI,aAAa,MAAM;AACjE,MAAI,OAAO,MAAM,eAAe,SAAU,KAAI,aAAa,MAAM;AACjE,SAAO;AACR;AAEA,SAAS,mBAAmB,OAA4D;AACvF,MAAI,CAAC,SAAS,OAAO,UAAU,SAAU,QAAO,CAAC;AACjD,QAAM,MAAkD,CAAC;AACzD,aAAW,OAAO,CAAC,SAAS,SAAS,OAAO,MAAM,GAAY;AAC7D,UAAM,IAAK,MAAkC,GAAG;AAChD,QAAI,OAAO,MAAM,UAAW,KAAI,GAAG,IAAI;AAAA,EACxC;AACA,SAAO;AACR;AAEO,SAAS,UAAU,MAAa,gBAAgB,OAAiB;AACvE,QAAM,QAAQ,KAAK,eAAgB,CAAC;AACpC,QAAM,UAAU,iBAAiB,KAAK;AACtC,SAAO;AAAA,IACN,IAAI,cAAc,KAAK,EAAE;AAAA,IACzB,OAAO,cAAc,KAAK,KAAK;AAAA,IAC/B,WAAW,cAAc,KAAK,SAAS;AAAA,IACvC,UAAU,cAAc,KAAK,QAAQ;AAAA,IACrC,OAAO,cAAc,KAAK,KAAK;AAAA,IAC/B,iBAAiB,cAAc,KAAK,eAAe;AAAA,IACnD,UAAU,OAAO,KAAK,aAAa,YAAY,KAAK,WAAW;AAAA,IAC/D,WAAW,KAAK,qBAAqB,OAAO,KAAK,UAAU,YAAY,IAAI;AAAA,IAC3E,aAAa;AAAA,MACZ,GAAG;AAAA,MACH,GAAG;AAAA,MACH,eAAe;AAAA,QACd,GAAG,oBAAoB;AAAA,QACvB,GAAG,mBAAmB,MAAM,aAAa;AAAA,MAC1C;AAAA,MACA,QAAQ,KAAK,UAAU,QAAQ,UAAU;AAAA,MACzC,UAAU,KAAK,YAAY,QAAQ,YAAY;AAAA,IAChD;AAAA,IACA,iBAAiB,KAAK,oBAAoB;AAAA,IAC1C,iBAAiB;AAAA,IACjB,YAAY,eAAe,KAAK,UAAU;AAAA,IAC1C,WAAW,eAAe,KAAK,SAAS;AAAA,IACxC,WAAW,eAAe,KAAK,SAAS;AAAA,IACxC,aAAa,MAAM,QAAQ,KAAK,WAAW,IAAI,KAAK,cAAc,CAAC;AAAA,IACnE,WAAW,KAAK,qBAAqB,OAAO,KAAK,UAAU,YAAY,IAAI;AAAA,IAC3E,WAAW,KAAK,qBAAqB,OAAO,KAAK,UAAU,YAAY,IAAI;AAAA,EAC5E;AACD;","names":[]}