@warlock.js/auth 4.15.0 → 5.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 (44) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/esm/commands/auth-cleanup-command.d.mts +1 -1
  3. package/esm/commands/auth-cleanup-command.d.mts.map +1 -1
  4. package/esm/commands/auth-purge-never-expiring-command.d.mts +1 -1
  5. package/esm/commands/auth-purge-never-expiring-command.d.mts.map +1 -1
  6. package/esm/commands/jwt-secret-generator-command.d.mts +1 -1
  7. package/esm/commands/jwt-secret-generator-command.d.mts.map +1 -1
  8. package/esm/contracts/index.d.mts +1 -1
  9. package/esm/contracts/types.d.mts +15 -2
  10. package/esm/contracts/types.d.mts.map +1 -1
  11. package/esm/contracts/types.mjs.map +1 -1
  12. package/esm/index.d.mts +3 -3
  13. package/esm/index.mjs +3 -3
  14. package/esm/middleware/auth.middleware.d.mts +8 -1
  15. package/esm/middleware/auth.middleware.d.mts.map +1 -1
  16. package/esm/middleware/auth.middleware.mjs +53 -37
  17. package/esm/middleware/auth.middleware.mjs.map +1 -1
  18. package/esm/middleware/login-throttle.middleware.mjs +1 -1
  19. package/esm/middleware/login-throttle.middleware.mjs.map +1 -1
  20. package/esm/models/access-token/access-token.model.d.mts +35 -5
  21. package/esm/models/access-token/access-token.model.d.mts.map +1 -1
  22. package/esm/models/auth.model.d.mts +1 -5
  23. package/esm/models/auth.model.d.mts.map +1 -1
  24. package/esm/models/index.d.mts +1 -1
  25. package/esm/models/index.d.mts.map +1 -1
  26. package/esm/models/refresh-token/refresh-token.model.d.mts +106 -4
  27. package/esm/models/refresh-token/refresh-token.model.d.mts.map +1 -1
  28. package/esm/services/auth-config.mjs +4 -0
  29. package/esm/services/auth-config.mjs.map +1 -1
  30. package/esm/services/auth.service.d.mts +11 -9
  31. package/esm/services/auth.service.d.mts.map +1 -1
  32. package/esm/services/auth.service.mjs +68 -41
  33. package/esm/services/auth.service.mjs.map +1 -1
  34. package/esm/services/index.d.mts +1 -1
  35. package/esm/services/index.mjs +1 -1
  36. package/esm/services/jwt.d.mts +21 -1
  37. package/esm/services/jwt.d.mts.map +1 -1
  38. package/esm/services/jwt.mjs +51 -2
  39. package/esm/services/jwt.mjs.map +1 -1
  40. package/esm/utils/auth-error-codes.d.mts +11 -1
  41. package/esm/utils/auth-error-codes.d.mts.map +1 -1
  42. package/esm/utils/auth-error-codes.mjs +9 -0
  43. package/esm/utils/auth-error-codes.mjs.map +1 -1
  44. package/package.json +9 -9
@@ -15,7 +15,58 @@ import { Model } from "@warlock.js/cascade";
15
15
  * });
16
16
  * }
17
17
  */
18
- declare const refreshTokenSchema: any;
18
+ declare const refreshTokenSchema: import("@warlock.js/seal").ObjectValidator<{
19
+ token: import("@warlock.js/seal").StringValidator & import("@warlock.js/seal").StandardSchemaV1<string, string>;
20
+ user_id: import("@warlock.js/seal").ScalarValidator & import("@warlock.js/seal").StandardSchemaV1<string | number | boolean, string | number | boolean>;
21
+ user_type: import("@warlock.js/seal").StringValidator & import("@warlock.js/seal").StandardSchemaV1<string, string>;
22
+ family_id: import("@warlock.js/seal").StringValidator & import("@warlock.js/seal").StandardSchemaV1<string, string>;
23
+ expires_at: import("@warlock.js/seal").DateValidator & import("@warlock.js/seal").StandardSchemaV1<Date, Date>;
24
+ last_used_at: import("@warlock.js/seal").DateValidator & import("@warlock.js/seal").StandardSchemaV1<Date, Date> & {
25
+ hasDefault: true;
26
+ };
27
+ revoked_at: import("@warlock.js/seal").DateValidator & import("@warlock.js/seal").StandardSchemaV1<Date, Date> & {
28
+ isOptional: true;
29
+ };
30
+ device_info: import("@warlock.js/seal").RecordValidator & {
31
+ valueValidator: import("@warlock.js/seal").AnyValidator & import("@warlock.js/seal").StandardSchemaV1<any, any>;
32
+ } & import("@warlock.js/seal").StandardSchemaV1<Record<string, any>, Record<string, any>> & {
33
+ isOptional: true;
34
+ };
35
+ }> & import("@warlock.js/seal").StandardSchemaV1<import("@warlock.js/seal").InferInputObjectShape<{
36
+ token: import("@warlock.js/seal").StringValidator & import("@warlock.js/seal").StandardSchemaV1<string, string>;
37
+ user_id: import("@warlock.js/seal").ScalarValidator & import("@warlock.js/seal").StandardSchemaV1<string | number | boolean, string | number | boolean>;
38
+ user_type: import("@warlock.js/seal").StringValidator & import("@warlock.js/seal").StandardSchemaV1<string, string>;
39
+ family_id: import("@warlock.js/seal").StringValidator & import("@warlock.js/seal").StandardSchemaV1<string, string>;
40
+ expires_at: import("@warlock.js/seal").DateValidator & import("@warlock.js/seal").StandardSchemaV1<Date, Date>;
41
+ last_used_at: import("@warlock.js/seal").DateValidator & import("@warlock.js/seal").StandardSchemaV1<Date, Date> & {
42
+ hasDefault: true;
43
+ };
44
+ revoked_at: import("@warlock.js/seal").DateValidator & import("@warlock.js/seal").StandardSchemaV1<Date, Date> & {
45
+ isOptional: true;
46
+ };
47
+ device_info: import("@warlock.js/seal").RecordValidator & {
48
+ valueValidator: import("@warlock.js/seal").AnyValidator & import("@warlock.js/seal").StandardSchemaV1<any, any>;
49
+ } & import("@warlock.js/seal").StandardSchemaV1<Record<string, any>, Record<string, any>> & {
50
+ isOptional: true;
51
+ };
52
+ }>, import("@warlock.js/seal").InferInputObjectShape<{
53
+ token: import("@warlock.js/seal").StringValidator & import("@warlock.js/seal").StandardSchemaV1<string, string>;
54
+ user_id: import("@warlock.js/seal").ScalarValidator & import("@warlock.js/seal").StandardSchemaV1<string | number | boolean, string | number | boolean>;
55
+ user_type: import("@warlock.js/seal").StringValidator & import("@warlock.js/seal").StandardSchemaV1<string, string>;
56
+ family_id: import("@warlock.js/seal").StringValidator & import("@warlock.js/seal").StandardSchemaV1<string, string>;
57
+ expires_at: import("@warlock.js/seal").DateValidator & import("@warlock.js/seal").StandardSchemaV1<Date, Date>;
58
+ last_used_at: import("@warlock.js/seal").DateValidator & import("@warlock.js/seal").StandardSchemaV1<Date, Date> & {
59
+ hasDefault: true;
60
+ };
61
+ revoked_at: import("@warlock.js/seal").DateValidator & import("@warlock.js/seal").StandardSchemaV1<Date, Date> & {
62
+ isOptional: true;
63
+ };
64
+ device_info: import("@warlock.js/seal").RecordValidator & {
65
+ valueValidator: import("@warlock.js/seal").AnyValidator & import("@warlock.js/seal").StandardSchemaV1<any, any>;
66
+ } & import("@warlock.js/seal").StandardSchemaV1<Record<string, any>, Record<string, any>> & {
67
+ isOptional: true;
68
+ };
69
+ }>>;
19
70
  /**
20
71
  * Extra attributes captured when a refresh token is issued.
21
72
  */
@@ -36,7 +87,58 @@ type RefreshTokenIssueOptions = {
36
87
  */
37
88
  declare class RefreshToken extends Model {
38
89
  static table: string;
39
- static schema: any;
90
+ static schema: import("@warlock.js/seal").ObjectValidator<{
91
+ token: import("@warlock.js/seal").StringValidator & import("@warlock.js/seal").StandardSchemaV1<string, string>;
92
+ user_id: import("@warlock.js/seal").ScalarValidator & import("@warlock.js/seal").StandardSchemaV1<string | number | boolean, string | number | boolean>;
93
+ user_type: import("@warlock.js/seal").StringValidator & import("@warlock.js/seal").StandardSchemaV1<string, string>;
94
+ family_id: import("@warlock.js/seal").StringValidator & import("@warlock.js/seal").StandardSchemaV1<string, string>;
95
+ expires_at: import("@warlock.js/seal").DateValidator & import("@warlock.js/seal").StandardSchemaV1<Date, Date>;
96
+ last_used_at: import("@warlock.js/seal").DateValidator & import("@warlock.js/seal").StandardSchemaV1<Date, Date> & {
97
+ hasDefault: true;
98
+ };
99
+ revoked_at: import("@warlock.js/seal").DateValidator & import("@warlock.js/seal").StandardSchemaV1<Date, Date> & {
100
+ isOptional: true;
101
+ };
102
+ device_info: import("@warlock.js/seal").RecordValidator & {
103
+ valueValidator: import("@warlock.js/seal").AnyValidator & import("@warlock.js/seal").StandardSchemaV1<any, any>;
104
+ } & import("@warlock.js/seal").StandardSchemaV1<Record<string, any>, Record<string, any>> & {
105
+ isOptional: true;
106
+ };
107
+ }> & import("@warlock.js/seal").StandardSchemaV1<import("@warlock.js/seal").InferInputObjectShape<{
108
+ token: import("@warlock.js/seal").StringValidator & import("@warlock.js/seal").StandardSchemaV1<string, string>;
109
+ user_id: import("@warlock.js/seal").ScalarValidator & import("@warlock.js/seal").StandardSchemaV1<string | number | boolean, string | number | boolean>;
110
+ user_type: import("@warlock.js/seal").StringValidator & import("@warlock.js/seal").StandardSchemaV1<string, string>;
111
+ family_id: import("@warlock.js/seal").StringValidator & import("@warlock.js/seal").StandardSchemaV1<string, string>;
112
+ expires_at: import("@warlock.js/seal").DateValidator & import("@warlock.js/seal").StandardSchemaV1<Date, Date>;
113
+ last_used_at: import("@warlock.js/seal").DateValidator & import("@warlock.js/seal").StandardSchemaV1<Date, Date> & {
114
+ hasDefault: true;
115
+ };
116
+ revoked_at: import("@warlock.js/seal").DateValidator & import("@warlock.js/seal").StandardSchemaV1<Date, Date> & {
117
+ isOptional: true;
118
+ };
119
+ device_info: import("@warlock.js/seal").RecordValidator & {
120
+ valueValidator: import("@warlock.js/seal").AnyValidator & import("@warlock.js/seal").StandardSchemaV1<any, any>;
121
+ } & import("@warlock.js/seal").StandardSchemaV1<Record<string, any>, Record<string, any>> & {
122
+ isOptional: true;
123
+ };
124
+ }>, import("@warlock.js/seal").InferInputObjectShape<{
125
+ token: import("@warlock.js/seal").StringValidator & import("@warlock.js/seal").StandardSchemaV1<string, string>;
126
+ user_id: import("@warlock.js/seal").ScalarValidator & import("@warlock.js/seal").StandardSchemaV1<string | number | boolean, string | number | boolean>;
127
+ user_type: import("@warlock.js/seal").StringValidator & import("@warlock.js/seal").StandardSchemaV1<string, string>;
128
+ family_id: import("@warlock.js/seal").StringValidator & import("@warlock.js/seal").StandardSchemaV1<string, string>;
129
+ expires_at: import("@warlock.js/seal").DateValidator & import("@warlock.js/seal").StandardSchemaV1<Date, Date>;
130
+ last_used_at: import("@warlock.js/seal").DateValidator & import("@warlock.js/seal").StandardSchemaV1<Date, Date> & {
131
+ hasDefault: true;
132
+ };
133
+ revoked_at: import("@warlock.js/seal").DateValidator & import("@warlock.js/seal").StandardSchemaV1<Date, Date> & {
134
+ isOptional: true;
135
+ };
136
+ device_info: import("@warlock.js/seal").RecordValidator & {
137
+ valueValidator: import("@warlock.js/seal").AnyValidator & import("@warlock.js/seal").StandardSchemaV1<any, any>;
138
+ } & import("@warlock.js/seal").StandardSchemaV1<Record<string, any>, Record<string, any>> & {
139
+ isOptional: true;
140
+ };
141
+ }>>;
40
142
  /** Token family this row belongs to (rotation / replay grouping). */
41
143
  get familyId(): string;
42
144
  /**
@@ -78,7 +180,7 @@ declare class RefreshToken extends Model {
78
180
  /**
79
181
  * Persist a freshly-signed refresh token for the user.
80
182
  */
81
- static issue(user: Auth, token: string, options: RefreshTokenIssueOptions): any;
183
+ static issue(user: Auth, token: string, options: RefreshTokenIssueOptions): Promise<RefreshToken>;
82
184
  /**
83
185
  * Find a refresh-token row by its raw token string.
84
186
  */
@@ -91,7 +193,7 @@ declare class RefreshToken extends Model {
91
193
  /**
92
194
  * Delete a specific refresh token belonging to the user.
93
195
  */
94
- static deleteForUser(user: Auth, token: string): any;
196
+ static deleteForUser(user: Auth, token: string): Promise<number>;
95
197
  /**
96
198
  * Active, unexpired sessions for the user, newest first.
97
199
  */
@@ -1 +1 @@
1
- {"version":3,"file":"refresh-token.model.d.mts","names":[],"sources":["../../../../../../../../auth/src/models/refresh-token/refresh-token.model.ts"],"mappings":";;;;;;;AAkBA;;;;AASE;AAKF;;;;;cAda,kBAAA;;;;KAcD,wBAAA;EACV,QAAA;EACA,SAAA;EACA,UAAA,GAAa,UAAU;AAAA;;;;;;;;;;;cAaZ,YAAA,SAAqB,KAAA;EAAA,OAClB,KAAA;EAAA,OAEA,MAAA;EAwIyB;EAAA,IArI5B,QAAA;EAqImC;;;;;;;;;;EAAA,IAvHnC,SAAA;EApBqB;;;;EAAA,IAgCrB,YAAA;EA7BG;EAAA,IAkCH,SAAA;EAjBA;EAAA,IAsBA,OAAA;EALA;;;EAYE,MAAA,IAAU,OAAA;EAWV;;;;;;;EAAA,cAAA,IAAkB,OAAA;EAqByB;;;EAP3C,UAAA,IAAc,OAAA;EA2Be;;;EAAA,OApB5B,KAAA,CAAM,IAAA,EAAM,IAAA,EAAM,KAAA,UAAe,OAAA,EAAS,wBAAA;EA4B9B;;;EAAA,OARZ,WAAA,CAAY,KAAA,WAAgB,OAAA,CAAQ,YAAA;EAepC;;;;EAAA,OAPA,WAAA,CAAY,IAAA,EAAM,IAAA,EAAM,KAAA,WAAgB,OAAA,CAAQ,YAAA;EAchC;;;EAAA,OAPhB,aAAA,CAAc,IAAA,EAAM,IAAA,EAAM,KAAA;EAuBpB;;;EAAA,OAhBN,SAAA,CAAU,IAAA,EAAM,IAAA,GAAO,OAAA,CAAQ,YAAA;EAgBS;;;;;;;;EAAA,OAAlC,YAAA,CAAa,IAAA,EAAM,IAAA,GAAO,OAAA,CAAQ,YAAA;EA+BG;;;;EAAA,OAfrC,YAAA,CAAa,QAAA,WAAmB,OAAA,CAAQ,YAAA;EAmDnB;;;;;EAAA,OApCrB,UAAA,CAAW,IAAA,EAAM,IAAA,EAAM,GAAA,WAAc,OAAA;EAmDK;;;;;EAAA,OA/B1C,YAAA,IAAgB,OAAA,CAAQ,YAAA;;;;;;;SAgBxB,iBAAA,IAAqB,OAAA,CAAQ,YAAA;;;;;;;;;;SAe7B,kBAAA,IAAsB,OAAA,CAAQ,YAAA;AAAA"}
1
+ {"version":3,"file":"refresh-token.model.d.mts","names":[],"sources":["../../../../../../../../auth/src/models/refresh-token/refresh-token.model.ts"],"mappings":";;;;;;;AAkBA;;;;;;;;;;cAAa,kBAAA,6BAAkB,eAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAcnB,wBAAA;EACV,QAAA;EACA,SAAA;EACA,UAAA,GAAa,UAAU;AAAA;;;;;;;;;;;cAaZ,YAAA,SAAqB,KAAA;EAAA,OAClB,KAAA;EAAA,OAEA,MAAA,6BAAM,eAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;MAGT,QAAA;;;;;;;;;;;MAcA,SAAA;;;AApCb;;MAgDa,YAAA;EA7CY;EAAA,IAkDZ,SAAA;EAnDX;EAAA,IAwDW,OAAA;EAvDE;;AAAU;EA8DV,MAAA,IAAU,OAAA;EAjDC;;;;;;;EA4DX,cAAA,IAAkB,OAAA;;;;EAclB,UAAA,IAAc,OAAA;;;;SAOb,KAAA,CAAM,IAAA,EAAM,IAAA,EAAM,KAAA,UAAe,OAAA,EAAS,wBAAA,GAAwB,OAAA,CAAA,YAAA;;;;SAoBlE,WAAA,CAAY,KAAA,WAAgB,OAAA,CAAQ,YAAA;;;;;SAQpC,WAAA,CAAY,IAAA,EAAM,IAAA,EAAM,KAAA,WAAgB,OAAA,CAAQ,YAAA;;;;SAOhD,aAAA,CAAc,IAAA,EAAM,IAAA,EAAM,KAAA,WAAa,OAAA;EAnC3B;;;EAAA,OA0CZ,SAAA,CAAU,IAAA,EAAM,IAAA,GAAO,OAAA,CAAQ,YAAA;EAtBK;;;;;;;;EAAA,OAsC9B,YAAA,CAAa,IAAA,EAAM,IAAA,GAAO,OAAA,CAAQ,YAAA;EAhBjB;;;;EAAA,OAgCjB,YAAA,CAAa,QAAA,WAAmB,OAAA,CAAQ,YAAA;EAAR;;;;;EAAA,OAehC,UAAA,CAAW,IAAA,EAAM,IAAA,EAAM,GAAA,WAAc,OAAA;EAoChB;;;;;EAAA,OAhBrB,YAAA,IAAgB,OAAA,CAAQ,YAAA;EA9LZ;;;;;;EAAA,OA8MZ,iBAAA,IAAqB,OAAA,CAAQ,YAAA;;;;;;;;;;SAe7B,kBAAA,IAAsB,OAAA,CAAQ,YAAA;AAAA"}
@@ -4,6 +4,7 @@ import ms from "ms";
4
4
 
5
5
  //#region ../auth/src/services/auth-config.ts
6
6
  const warnedLegacyKeys = /* @__PURE__ */ new Set();
7
+ const permitAuthentication = () => true;
7
8
  /**
8
9
  * `ms` accepts anything at runtime and answers `undefined` for a string it
9
10
  * cannot parse (`"30dayz"`), a *formatted string* for a number (`2592000` ⇒
@@ -70,6 +71,9 @@ function resolve(newKey, legacyKey, fallback) {
70
71
  * config and the legacy `auth.jwt.*` shape both resolve the same way.
71
72
  */
72
73
  const authConfig = {
74
+ canAuthenticate: async (user) => {
75
+ return config.key("auth.canAuthenticate", permitAuthentication)(user);
76
+ },
73
77
  accessToken: {
74
78
  /** Signing secret (legacy: `auth.jwt.secret`). Throws if neither is set. */
75
79
  secret: () => {
@@ -1 +1 @@
1
- {"version":3,"file":"auth-config.mjs","names":[],"sources":["../../../../../../../auth/src/services/auth-config.ts"],"sourcesContent":["import { config } from \"@warlock.js/core\";\r\nimport { log } from \"@warlock.js/logger\";\r\nimport { type Algorithm } from \"fast-jwt\";\r\nimport ms from \"ms\";\r\nimport type { LogoutWithoutTokenBehavior } from \"../contracts/types\";\r\n\r\nconst warnedLegacyKeys = new Set<string>();\r\n\r\n/**\r\n * `ms` accepts anything at runtime and answers `undefined` for a string it\r\n * cannot parse (`\"30dayz\"`), a *formatted string* for a number (`2592000` ⇒\r\n * `\"43m\"`), and throws for `\"\"`. Its published types claim the far narrower\r\n * `(value: ms.StringValue) => number`, which is exactly what let an\r\n * `as ms.StringValue` cast smuggle arbitrary config text past the compiler and\r\n * hand `undefined` to the JWT signer. Declare the honest runtime signature once,\r\n * here, so every caller is forced to deal with the `undefined`.\r\n */\r\nconst parseMs = ms as unknown as (value: unknown) => number | string | undefined;\r\n\r\n/**\r\n * Turn a configured lifetime into a positive number of milliseconds, or throw\r\n * naming the key.\r\n *\r\n * A token lifetime is a security boundary, so an unusable value is a hard error\r\n * rather than a silent fallback: substituting a default would trade one\r\n * unintended lifetime for another, just as quietly — and quiet is the whole\r\n * problem. Every non-positive, non-finite or non-numeric parse is rejected:\r\n *\r\n * - `\"30dayz\"` / `\"thirty days\"` ⇒ `undefined` ⇒ a JWT signed with **no `exp`\r\n * claim** and an `Invalid Date` written to the token row.\r\n * - `\"0d\"` ⇒ parses cleanly to `0`, which `fast-jwt` treats as falsy and also\r\n * emits with **no `exp` claim** — it survives any guard that only rejects\r\n * `undefined`. `\"-1h\"` signs an already-expired token.\r\n * - `2592000` (a bare number) ⇒ `ms` *formats* it as `\"43m\"`, a string, which\r\n * then turns `Date.now() + expiresIn` into an `Invalid Date`.\r\n */\r\nfunction parseDuration(key: string, raw: unknown): number {\r\n let parsed: number | string | undefined;\r\n\r\n try {\r\n parsed = parseMs(raw);\r\n } catch {\r\n // ms throws (rather than returning undefined) for \"\" and non-strings\r\n parsed = undefined;\r\n }\r\n\r\n if (typeof parsed !== \"number\" || !Number.isFinite(parsed) || parsed <= 0) {\r\n throw new Error(\r\n `auth.${key}: ${JSON.stringify(raw)} is not a valid ms duration — ` +\r\n `use a positive duration string such as \"1h\", \"7d\", or NO_EXPIRATION.`,\r\n );\r\n }\r\n\r\n return parsed;\r\n}\r\n\r\n/** Access-token lifetime used when nothing is configured. */\r\nconst DEFAULT_ACCESS_TOKEN_EXPIRES_IN = \"1h\";\r\n\r\n/**\r\n * Resolve an auth setting, preferring the new `auth.accessToken.*` /\r\n * `auth.refreshToken.*` key and falling back to the deprecated `auth.jwt.*`\r\n * shape — warning once per legacy key. Returns `fallback` when neither is set.\r\n *\r\n * This is the backward-compatible shim that lets existing `auth.jwt.*` configs\r\n * keep working after the config split.\r\n */\r\nfunction resolve<T>(newKey: string, legacyKey: string, fallback?: T): T {\r\n const fromNew = config.key(`auth.${newKey}`);\r\n\r\n if (fromNew !== undefined && fromNew !== null) {\r\n return fromNew as T;\r\n }\r\n\r\n const fromLegacy = config.key(`auth.${legacyKey}`);\r\n\r\n if (fromLegacy !== undefined && fromLegacy !== null) {\r\n if (!warnedLegacyKeys.has(legacyKey)) {\r\n warnedLegacyKeys.add(legacyKey);\r\n log.warn(\"auth\", \"config-deprecation\", `auth.${legacyKey} is deprecated — use auth.${newKey}`);\r\n }\r\n\r\n return fromLegacy as T;\r\n }\r\n\r\n return fallback as T;\r\n}\r\n\r\n/**\r\n * Typed, backward-compatible access to auth configuration. The service and the\r\n * jwt signer read configuration exclusively through here, so the new split\r\n * config and the legacy `auth.jwt.*` shape both resolve the same way.\r\n */\r\nexport const authConfig = {\r\n accessToken: {\r\n /** Signing secret (legacy: `auth.jwt.secret`). Throws if neither is set. */\r\n secret: (): string => {\r\n const secret = resolve<string | undefined>(\"accessToken.secret\", \"jwt.secret\");\r\n\r\n if (!secret) {\r\n throw new Error(\"auth: no JWT secret configured — set `auth.accessToken.secret`.\");\r\n }\r\n\r\n return secret;\r\n },\r\n /** Signing algorithm (legacy: `auth.jwt.algorithm`). */\r\n algorithm: (): Algorithm => resolve(\"accessToken.algorithm\", \"jwt.algorithm\", \"HS256\"),\r\n /** Lifetime as an `ms`-string (legacy: `auth.jwt.expiresIn`). */\r\n expiresIn: (): string | undefined => resolve(\"accessToken.expiresIn\", \"jwt.expiresIn\"),\r\n /**\r\n * Lifetime in milliseconds — the only form a token issuer may use. Defaults\r\n * to 1 hour when unset; throws naming the key when the configured value is\r\n * not a positive `ms` duration. See {@link parseDuration}.\r\n */\r\n expiresInMs: (): number =>\r\n parseDuration(\r\n \"accessToken.expiresIn\",\r\n authConfig.accessToken.expiresIn() ?? DEFAULT_ACCESS_TOKEN_EXPIRES_IN,\r\n ),\r\n },\r\n refreshToken: {\r\n /** Separate refresh secret (legacy: `auth.jwt.refresh.secret`); empty ⇒ fall back to the access secret. */\r\n secret: (): string | undefined => resolve(\"refreshToken.secret\", \"jwt.refresh.secret\"),\r\n /** Whether refresh tokens are enabled (legacy: `auth.jwt.refresh.enabled`). */\r\n enabled: (): boolean => resolve(\"refreshToken.enabled\", \"jwt.refresh.enabled\", true),\r\n /** Lifetime as an `ms`-string (legacy: `auth.jwt.refresh.expiresIn`). */\r\n expiresIn: (): string => resolve(\"refreshToken.expiresIn\", \"jwt.refresh.expiresIn\", \"7d\"),\r\n /**\r\n * Lifetime in milliseconds — the only form a token issuer may use. Throws\r\n * naming the key when the configured value is not a positive `ms` duration.\r\n */\r\n expiresInMs: (): number =>\r\n parseDuration(\"refreshToken.expiresIn\", authConfig.refreshToken.expiresIn()),\r\n /** Rotate-on-use (legacy: `auth.jwt.refresh.rotation`). */\r\n rotation: (): boolean => resolve(\"refreshToken.rotation\", \"jwt.refresh.rotation\", true),\r\n /** Max active tokens per user (legacy: `auth.jwt.refresh.maxPerUser`). */\r\n maxPerUser: (): number => resolve(\"refreshToken.maxPerUser\", \"jwt.refresh.maxPerUser\", 5),\r\n /** Logout-without-token behavior (legacy: `auth.jwt.refresh.logoutWithoutToken`). */\r\n logoutWithoutToken: (): LogoutWithoutTokenBehavior =>\r\n resolve(\"refreshToken.logoutWithoutToken\", \"jwt.refresh.logoutWithoutToken\", \"revoke-all\"),\r\n },\r\n};\r\n"],"mappings":";;;;;AAMA,MAAM,mCAAmB,IAAI,IAAY;;;;;;;;;;AAWzC,MAAM,UAAU;;;;;;;;;;;;;;;;;;AAmBhB,SAAS,cAAc,KAAa,KAAsB;CACxD,IAAI;CAEJ,IAAI;EACF,SAAS,QAAQ,GAAG;CACtB,QAAQ;EAEN,SAAS;CACX;CAEA,IAAI,OAAO,WAAW,YAAY,CAAC,OAAO,SAAS,MAAM,KAAK,UAAU,GACtE,MAAM,IAAI,MACR,QAAQ,IAAI,IAAI,KAAK,UAAU,GAAG,EAAE,mGAEtC;CAGF,OAAO;AACT;;AAGA,MAAM,kCAAkC;;;;;;;;;AAUxC,SAAS,QAAW,QAAgB,WAAmB,UAAiB;CACtE,MAAM,UAAU,OAAO,IAAI,QAAQ,QAAQ;CAE3C,IAAI,YAAY,UAAa,YAAY,MACvC,OAAO;CAGT,MAAM,aAAa,OAAO,IAAI,QAAQ,WAAW;CAEjD,IAAI,eAAe,UAAa,eAAe,MAAM;EACnD,IAAI,CAAC,iBAAiB,IAAI,SAAS,GAAG;GACpC,iBAAiB,IAAI,SAAS;GAC9B,IAAI,KAAK,QAAQ,sBAAsB,QAAQ,UAAU,4BAA4B,QAAQ;EAC/F;EAEA,OAAO;CACT;CAEA,OAAO;AACT;;;;;;AAOA,MAAa,aAAa;CACxB,aAAa;;EAEX,cAAsB;GACpB,MAAM,SAAS,QAA4B,sBAAsB,YAAY;GAE7E,IAAI,CAAC,QACH,MAAM,IAAI,MAAM,iEAAiE;GAGnF,OAAO;EACT;;EAEA,iBAA4B,QAAQ,yBAAyB,iBAAiB,OAAO;;EAErF,iBAAqC,QAAQ,yBAAyB,eAAe;;;;;;EAMrF,mBACE,cACE,yBACA,WAAW,YAAY,UAAU,KAAK,+BACxC;CACJ;CACA,cAAc;;EAEZ,cAAkC,QAAQ,uBAAuB,oBAAoB;;EAErF,eAAwB,QAAQ,wBAAwB,uBAAuB,IAAI;;EAEnF,iBAAyB,QAAQ,0BAA0B,yBAAyB,IAAI;;;;;EAKxF,mBACE,cAAc,0BAA0B,WAAW,aAAa,UAAU,CAAC;;EAE7E,gBAAyB,QAAQ,yBAAyB,wBAAwB,IAAI;;EAEtF,kBAA0B,QAAQ,2BAA2B,0BAA0B,CAAC;;EAExF,0BACE,QAAQ,mCAAmC,kCAAkC,YAAY;CAC7F;AACF"}
1
+ {"version":3,"file":"auth-config.mjs","names":[],"sources":["../../../../../../../auth/src/services/auth-config.ts"],"sourcesContent":["import { config } from \"@warlock.js/core\";\r\nimport { log } from \"@warlock.js/logger\";\r\nimport { type Algorithm } from \"fast-jwt\";\r\nimport ms from \"ms\";\r\nimport type { CanAuthenticate, LogoutWithoutTokenBehavior } from \"../contracts/types\";\nimport type { Auth } from \"../models/auth.model\";\n\nconst warnedLegacyKeys = new Set<string>();\n\nconst permitAuthentication: CanAuthenticate = (): boolean => true;\n\r\n/**\r\n * `ms` accepts anything at runtime and answers `undefined` for a string it\r\n * cannot parse (`\"30dayz\"`), a *formatted string* for a number (`2592000` ⇒\r\n * `\"43m\"`), and throws for `\"\"`. Its published types claim the far narrower\r\n * `(value: ms.StringValue) => number`, which is exactly what let an\r\n * `as ms.StringValue` cast smuggle arbitrary config text past the compiler and\r\n * hand `undefined` to the JWT signer. Declare the honest runtime signature once,\r\n * here, so every caller is forced to deal with the `undefined`.\r\n */\r\nconst parseMs = ms as unknown as (value: unknown) => number | string | undefined;\r\n\r\n/**\r\n * Turn a configured lifetime into a positive number of milliseconds, or throw\r\n * naming the key.\r\n *\r\n * A token lifetime is a security boundary, so an unusable value is a hard error\r\n * rather than a silent fallback: substituting a default would trade one\r\n * unintended lifetime for another, just as quietly — and quiet is the whole\r\n * problem. Every non-positive, non-finite or non-numeric parse is rejected:\r\n *\r\n * - `\"30dayz\"` / `\"thirty days\"` ⇒ `undefined` ⇒ a JWT signed with **no `exp`\r\n * claim** and an `Invalid Date` written to the token row.\r\n * - `\"0d\"` ⇒ parses cleanly to `0`, which `fast-jwt` treats as falsy and also\r\n * emits with **no `exp` claim** — it survives any guard that only rejects\r\n * `undefined`. `\"-1h\"` signs an already-expired token.\r\n * - `2592000` (a bare number) ⇒ `ms` *formats* it as `\"43m\"`, a string, which\r\n * then turns `Date.now() + expiresIn` into an `Invalid Date`.\r\n */\r\nfunction parseDuration(key: string, raw: unknown): number {\r\n let parsed: number | string | undefined;\r\n\r\n try {\r\n parsed = parseMs(raw);\r\n } catch {\r\n // ms throws (rather than returning undefined) for \"\" and non-strings\r\n parsed = undefined;\r\n }\r\n\r\n if (typeof parsed !== \"number\" || !Number.isFinite(parsed) || parsed <= 0) {\r\n throw new Error(\r\n `auth.${key}: ${JSON.stringify(raw)} is not a valid ms duration — ` +\r\n `use a positive duration string such as \"1h\", \"7d\", or NO_EXPIRATION.`,\r\n );\r\n }\r\n\r\n return parsed;\r\n}\r\n\r\n/** Access-token lifetime used when nothing is configured. */\r\nconst DEFAULT_ACCESS_TOKEN_EXPIRES_IN = \"1h\";\r\n\r\n/**\r\n * Resolve an auth setting, preferring the new `auth.accessToken.*` /\r\n * `auth.refreshToken.*` key and falling back to the deprecated `auth.jwt.*`\r\n * shape — warning once per legacy key. Returns `fallback` when neither is set.\r\n *\r\n * This is the backward-compatible shim that lets existing `auth.jwt.*` configs\r\n * keep working after the config split.\r\n */\r\nfunction resolve<T>(newKey: string, legacyKey: string, fallback?: T): T {\r\n const fromNew = config.key(`auth.${newKey}`);\r\n\r\n if (fromNew !== undefined && fromNew !== null) {\r\n return fromNew as T;\r\n }\r\n\r\n const fromLegacy = config.key(`auth.${legacyKey}`);\r\n\r\n if (fromLegacy !== undefined && fromLegacy !== null) {\r\n if (!warnedLegacyKeys.has(legacyKey)) {\r\n warnedLegacyKeys.add(legacyKey);\r\n log.warn(\"auth\", \"config-deprecation\", `auth.${legacyKey} is deprecated — use auth.${newKey}`);\r\n }\r\n\r\n return fromLegacy as T;\r\n }\r\n\r\n return fallback as T;\r\n}\r\n\r\n/**\r\n * Typed, backward-compatible access to auth configuration. The service and the\r\n * jwt signer read configuration exclusively through here, so the new split\r\n * config and the legacy `auth.jwt.*` shape both resolve the same way.\r\n */\r\nexport const authConfig = {\n canAuthenticate: async (user: Auth): Promise<boolean> => {\n const canAuthenticate = config.key<CanAuthenticate>(\n \"auth.canAuthenticate\",\n permitAuthentication,\n );\n\n return canAuthenticate(user);\n },\n accessToken: {\n /** Signing secret (legacy: `auth.jwt.secret`). Throws if neither is set. */\r\n secret: (): string => {\r\n const secret = resolve<string | undefined>(\"accessToken.secret\", \"jwt.secret\");\r\n\r\n if (!secret) {\r\n throw new Error(\"auth: no JWT secret configured — set `auth.accessToken.secret`.\");\r\n }\r\n\r\n return secret;\r\n },\r\n /** Signing algorithm (legacy: `auth.jwt.algorithm`). */\r\n algorithm: (): Algorithm => resolve(\"accessToken.algorithm\", \"jwt.algorithm\", \"HS256\"),\r\n /** Lifetime as an `ms`-string (legacy: `auth.jwt.expiresIn`). */\r\n expiresIn: (): string | undefined => resolve(\"accessToken.expiresIn\", \"jwt.expiresIn\"),\r\n /**\r\n * Lifetime in milliseconds — the only form a token issuer may use. Defaults\r\n * to 1 hour when unset; throws naming the key when the configured value is\r\n * not a positive `ms` duration. See {@link parseDuration}.\r\n */\r\n expiresInMs: (): number =>\r\n parseDuration(\r\n \"accessToken.expiresIn\",\r\n authConfig.accessToken.expiresIn() ?? DEFAULT_ACCESS_TOKEN_EXPIRES_IN,\r\n ),\r\n },\r\n refreshToken: {\r\n /** Separate refresh secret (legacy: `auth.jwt.refresh.secret`); empty ⇒ fall back to the access secret. */\r\n secret: (): string | undefined => resolve(\"refreshToken.secret\", \"jwt.refresh.secret\"),\r\n /** Whether refresh tokens are enabled (legacy: `auth.jwt.refresh.enabled`). */\r\n enabled: (): boolean => resolve(\"refreshToken.enabled\", \"jwt.refresh.enabled\", true),\r\n /** Lifetime as an `ms`-string (legacy: `auth.jwt.refresh.expiresIn`). */\r\n expiresIn: (): string => resolve(\"refreshToken.expiresIn\", \"jwt.refresh.expiresIn\", \"7d\"),\r\n /**\r\n * Lifetime in milliseconds — the only form a token issuer may use. Throws\r\n * naming the key when the configured value is not a positive `ms` duration.\r\n */\r\n expiresInMs: (): number =>\r\n parseDuration(\"refreshToken.expiresIn\", authConfig.refreshToken.expiresIn()),\r\n /** Rotate-on-use (legacy: `auth.jwt.refresh.rotation`). */\r\n rotation: (): boolean => resolve(\"refreshToken.rotation\", \"jwt.refresh.rotation\", true),\r\n /** Max active tokens per user (legacy: `auth.jwt.refresh.maxPerUser`). */\r\n maxPerUser: (): number => resolve(\"refreshToken.maxPerUser\", \"jwt.refresh.maxPerUser\", 5),\r\n /** Logout-without-token behavior (legacy: `auth.jwt.refresh.logoutWithoutToken`). */\r\n logoutWithoutToken: (): LogoutWithoutTokenBehavior =>\r\n resolve(\"refreshToken.logoutWithoutToken\", \"jwt.refresh.logoutWithoutToken\", \"revoke-all\"),\r\n },\r\n};\r\n"],"mappings":";;;;;AAOA,MAAM,mCAAmB,IAAI,IAAY;AAEzC,MAAM,6BAAuD;;;;;;;;;;AAW7D,MAAM,UAAU;;;;;;;;;;;;;;;;;;AAmBhB,SAAS,cAAc,KAAa,KAAsB;CACxD,IAAI;CAEJ,IAAI;EACF,SAAS,QAAQ,GAAG;CACtB,QAAQ;EAEN,SAAS;CACX;CAEA,IAAI,OAAO,WAAW,YAAY,CAAC,OAAO,SAAS,MAAM,KAAK,UAAU,GACtE,MAAM,IAAI,MACR,QAAQ,IAAI,IAAI,KAAK,UAAU,GAAG,EAAE,mGAEtC;CAGF,OAAO;AACT;;AAGA,MAAM,kCAAkC;;;;;;;;;AAUxC,SAAS,QAAW,QAAgB,WAAmB,UAAiB;CACtE,MAAM,UAAU,OAAO,IAAI,QAAQ,QAAQ;CAE3C,IAAI,YAAY,UAAa,YAAY,MACvC,OAAO;CAGT,MAAM,aAAa,OAAO,IAAI,QAAQ,WAAW;CAEjD,IAAI,eAAe,UAAa,eAAe,MAAM;EACnD,IAAI,CAAC,iBAAiB,IAAI,SAAS,GAAG;GACpC,iBAAiB,IAAI,SAAS;GAC9B,IAAI,KAAK,QAAQ,sBAAsB,QAAQ,UAAU,4BAA4B,QAAQ;EAC/F;EAEA,OAAO;CACT;CAEA,OAAO;AACT;;;;;;AAOA,MAAa,aAAa;CACxB,iBAAiB,OAAO,SAAiC;EAMvD,OALwB,OAAO,IAC7B,wBACA,oBAGmB,CAAC,CAAC,IAAI;CAC7B;CACA,aAAa;;EAEX,cAAsB;GACpB,MAAM,SAAS,QAA4B,sBAAsB,YAAY;GAE7E,IAAI,CAAC,QACH,MAAM,IAAI,MAAM,iEAAiE;GAGnF,OAAO;EACT;;EAEA,iBAA4B,QAAQ,yBAAyB,iBAAiB,OAAO;;EAErF,iBAAqC,QAAQ,yBAAyB,eAAe;;;;;;EAMrF,mBACE,cACE,yBACA,WAAW,YAAY,UAAU,KAAK,+BACxC;CACJ;CACA,cAAc;;EAEZ,cAAkC,QAAQ,uBAAuB,oBAAoB;;EAErF,eAAwB,QAAQ,wBAAwB,uBAAuB,IAAI;;EAEnF,iBAAyB,QAAQ,0BAA0B,yBAAyB,IAAI;;;;;EAKxF,mBACE,cAAc,0BAA0B,WAAW,aAAa,UAAU,CAAC;;EAE7E,gBAAyB,QAAQ,yBAAyB,wBAAwB,IAAI;;EAEtF,kBAA0B,QAAQ,2BAA2B,0BAA0B,CAAC;;EAExF,0BACE,QAAQ,mCAAmC,kCAAkC,YAAY;CAC7F;AACF"}
@@ -1,6 +1,6 @@
1
1
  import { Auth } from "../models/auth.model.mjs";
2
2
  import { AccessToken } from "../models/access-token/access-token.model.mjs";
3
- import { AccessTokenOutput, DeviceInfo, LoginResult, TokenPair } from "../contracts/types.mjs";
3
+ import { AccessTokenOutput, AuthCredentials, DeviceInfo, LoginResult, TokenPair } from "../contracts/types.mjs";
4
4
  import { RefreshToken } from "../models/refresh-token/refresh-token.model.mjs";
5
5
  import { ChildModel } from "@warlock.js/cascade";
6
6
 
@@ -20,22 +20,24 @@ declare class AuthService {
20
20
  /**
21
21
  * Build the default access-token JWT payload from a user.
22
22
  */
23
- buildAccessTokenPayload(user: Auth): {
24
- id: any;
25
- userType: string;
26
- created_at: number;
27
- };
23
+ buildAccessTokenPayload(user: Auth): Record<string, unknown>;
24
+ /** One policy seam keeps account-state checks identical across all auth paths. */
25
+ canAuthenticate(user: Auth): Promise<boolean>;
26
+ private assertCanAuthenticate;
27
+ private issueAccessToken;
28
28
  /**
29
29
  * Sign + persist an access token for the user and return the token with its
30
30
  * expiry. The expiry is computed locally from `expiresIn` rather than by
31
31
  * re-verifying the token we just signed.
32
32
  */
33
- generateAccessToken(user: Auth, payload?: any): Promise<AccessTokenOutput>;
33
+ generateAccessToken(user: Auth, payload?: Record<string, unknown>): Promise<AccessTokenOutput>;
34
+ private issueRefreshToken;
34
35
  /**
35
36
  * Sign + persist a refresh token for the user (enforcing the per-user cap
36
37
  * first). Resolves to `undefined` when refresh tokens are disabled in config.
37
38
  */
38
39
  createRefreshToken(user: Auth, deviceInfo?: DeviceInfo): Promise<RefreshToken | undefined>;
40
+ private issueTokenPair;
39
41
  /**
40
42
  * Issue both an access and a refresh token, emitting the creation events.
41
43
  */
@@ -58,12 +60,12 @@ declare class AuthService {
58
60
  * Resolve a user by credentials, verifying the password. Returns `null` on a
59
61
  * missing user or a wrong password, emitting `login.failed` either way.
60
62
  */
61
- attemptLogin<T extends Auth>(Model: ChildModel<T>, data: any): Promise<T | null>;
63
+ attemptLogin<T extends Auth>(Model: ChildModel<T>, data: AuthCredentials): Promise<T | null>;
62
64
  /**
63
65
  * Full login flow: validate credentials, issue tokens, emit events. Returns
64
66
  * the user + token pair on success, `null` on failure.
65
67
  */
66
- login<T extends Auth>(Model: ChildModel<T>, credentials: any, deviceInfo?: DeviceInfo): Promise<LoginResult<T> | null>;
68
+ login<T extends Auth>(Model: ChildModel<T>, credentials: AuthCredentials, deviceInfo?: DeviceInfo): Promise<LoginResult<T> | null>;
67
69
  /**
68
70
  * Log a user out.
69
71
  *
@@ -1 +1 @@
1
- {"version":3,"file":"auth.service.d.mts","names":[],"sources":["../../../../../../../auth/src/services/auth.service.ts"],"mappings":";;;;;;;cAWM,WAAA;;;;AALiD;;;cAYzC,gBAAA;EA2B2B;;;EAAA,YApB3B,iBAAA;EAyCG;;;EAlCR,uBAAA,CAAwB,IAAA,EAAM,IAAA;;;;;EA8F1B;;;;;EAjFE,mBAAA,CAAoB,IAAA,EAAM,IAAA,EAAM,OAAA,SAAgB,OAAA,CAAQ,iBAAA;EA4JpB;;;;EAzIpC,kBAAA,CACX,IAAA,EAAM,IAAA,EACN,UAAA,GAAa,UAAA,GACZ,OAAA,CAAQ,YAAA;EAiKF;;;EArII,eAAA,CAAgB,IAAA,EAAM,IAAA,EAAM,UAAA,GAAa,UAAA,GAAa,OAAA,CAAQ,SAAA;EAwIxE;;;;;EA5GU,aAAA,CACX,kBAAA,UACA,UAAA,GAAa,UAAA,GACZ,OAAA,CAAQ,SAAA;EA2KqC;;;EA/GnC,cAAA,CAAe,aAAA,UAAuB,cAAA,WAAyB,OAAA;EA+HlC;;;EAxH7B,YAAA,CAAa,QAAA,WAAmB,OAAA;EA4K5B;;;;EApKJ,YAAA,WAAuB,IAAA,EAAM,KAAA,EAAO,UAAA,CAAW,CAAA,GAAI,IAAA,QAAY,OAAA,CAAQ,CAAA;EAwMxC;;;;EA9K/B,KAAA,WAAgB,IAAA,EAC3B,KAAA,EAAO,UAAA,CAAW,CAAA,GAClB,WAAA,OACA,UAAA,GAAa,UAAA,GACZ,OAAA,CAAQ,WAAA,CAAY,CAAA;EAvMhB;;;;;;;;;;EAqOM,MAAA,CAAO,IAAA,EAAM,IAAA,EAAM,WAAA,WAAsB,YAAA,YAAwB,OAAA;EAxNT;;;EAqPxD,iBAAA,CAAkB,IAAA,EAAM,IAAA,EAAM,KAAA,WAAgB,OAAA;EAhO5C;;;EAuOF,qBAAA,CAAsB,IAAA,EAAM,IAAA,GAAO,OAAA;EA1MnC;;;EAiNA,kBAAA,CAAmB,IAAA,EAAM,IAAA,EAAM,KAAA,WAAgB,OAAA;EAjNnB;;;;;EA0N5B,eAAA,CAAgB,IAAA,EAAM,IAAA,GAAO,OAAA;EA5LxC;;;EA2MW,iBAAA,CAAkB,QAAA,WAAmB,OAAA;EA9ItB;;;;;EAyJf,oBAAA,IAAwB,OAAA;EA1IxB;;;;;;;;;;EAkKA,uBAAA,IAA2B,OAAA;IACtC,YAAA,EAAc,WAAA;IACd,aAAA,EAAe,YAAA;EAAA;EAzIf;;;;;;;;EAyJW,wBAAA,IAA4B,OAAA;IACvC,YAAA;IACA,aAAA;EAAA;EA1H4E;;;EA4IjE,iBAAA,CAAkB,IAAA,EAAM,IAAA,GAAO,OAAA,CAAQ,YAAA;AAAA;AAAA,cAKzC,WAAA,EAAW,WAAoB"}
1
+ {"version":3,"file":"auth.service.d.mts","names":[],"sources":["../../../../../../../auth/src/services/auth.service.ts"],"mappings":";;;;;;;cAiBM,WAAA;;;;AALiD;;;cAYzC,gBAAA;EAcgC;;;EAAA,YAPhC,iBAAA;EAiDA;;;EA1CL,uBAAA,CAAwB,IAAA,EAAM,IAAA,GAAO,MAAA;EAmF7B;EA1EF,eAAA,CAAgB,IAAA,EAAM,IAAA,GAAO,OAAA;EAAA,QAI5B,qBAAA;EAAA,QAMA,gBAAA;EAmGwC;;;;;EA9EzC,mBAAA,CACX,IAAA,EAAM,IAAA,EACN,OAAA,GAAU,MAAA,oBACT,OAAA,CAAQ,iBAAA;EAAA,QAMG,iBAAA;EAoK+B;;;;EApIhC,kBAAA,CACX,IAAA,EAAM,IAAA,EACN,UAAA,GAAa,UAAA,GACZ,OAAA,CAAQ,YAAA;EAAA,QAQG,cAAA;EAoIX;;;EA1GU,eAAA,CAAgB,IAAA,EAAM,IAAA,EAAM,UAAA,GAAa,UAAA,GAAa,OAAA,CAAQ,SAAA;EA4I5D;;;;;EAjIF,aAAA,CACX,kBAAA,UACA,UAAA,GAAa,UAAA,GACZ,OAAA,CAAQ,SAAA;EA8JmE;;;EApFjE,cAAA,CAAe,aAAA,UAAuB,cAAA,WAAyB,OAAA;EAwH5B;;;EAjHnC,YAAA,CAAa,QAAA,WAAmB,OAAA;EAiIH;;;;EAzH7B,YAAA,WAAuB,IAAA,EAClC,KAAA,EAAO,UAAA,CAAW,CAAA,GAClB,IAAA,EAAM,eAAA,GACL,OAAA,CAAQ,CAAA;EAwK6B;;;;EAxI3B,KAAA,WAAgB,IAAA,EAC3B,KAAA,EAAO,UAAA,CAAW,CAAA,GAClB,WAAA,EAAa,eAAA,EACb,UAAA,GAAa,UAAA,GACZ,OAAA,CAAQ,WAAA,CAAY,CAAA;EA0K4B;;;;;;;;;;EA5ItC,MAAA,CAAO,IAAA,EAAM,IAAA,EAAM,WAAA,WAAsB,YAAA,YAAwB,OAAA;EAzRpC;;;EAsT7B,iBAAA,CAAkB,IAAA,EAAM,IAAA,EAAM,KAAA,WAAgB,OAAA;EAtRnD;;;EA6RK,qBAAA,CAAsB,IAAA,EAAM,IAAA,GAAO,OAAA;EA3R7C;;;EAkSU,kBAAA,CAAmB,IAAA,EAAM,IAAA,EAAM,KAAA,WAAgB,OAAA;EA3PpD;;;;;EAoQK,eAAA,CAAgB,IAAA,EAAM,IAAA,GAAO,OAAA;EA1P5B;;;EAyQD,iBAAA,CAAkB,QAAA,WAAmB,OAAA;EA/OI;;;;;EA0PzC,oBAAA,IAAwB,OAAA;EA7OtB;;;;;;;;;;EAqQF,uBAAA,IAA2B,OAAA;IACtC,YAAA,EAAc,WAAA;IACd,aAAA,EAAe,YAAA;EAAA;EA5KR;;;;;;;;EA4LI,wBAAA,IAA4B,OAAA;IACvC,YAAA;IACA,aAAA;EAAA;EA3JA;;;EA6KW,iBAAA,CAAkB,IAAA,EAAM,IAAA,GAAO,OAAA,CAAQ,YAAA;AAAA;AAAA,cAKzC,WAAA,EAAW,WAAoB"}
@@ -4,8 +4,8 @@ import { RefreshToken } from "../models/refresh-token/refresh-token.model.mjs";
4
4
  import "../models/refresh-token/index.mjs";
5
5
  import { authConfig } from "./auth-config.mjs";
6
6
  import { authEvents } from "./auth-events.mjs";
7
- import { jwt } from "./jwt.mjs";
8
- import { config, hashPassword, verifyPassword } from "@warlock.js/core";
7
+ import { isInvalidCredentialError, jwt } from "./jwt.mjs";
8
+ import { ForbiddenError, config, hashPassword, verifyPassword } from "@warlock.js/core";
9
9
  import { Random } from "@mongez/reinforcements";
10
10
 
11
11
  //#region ../auth/src/services/auth.service.ts
@@ -35,12 +35,14 @@ var AuthService = class {
35
35
  created_at: Date.now()
36
36
  };
37
37
  }
38
- /**
39
- * Sign + persist an access token for the user and return the token with its
40
- * expiry. The expiry is computed locally from `expiresIn` rather than by
41
- * re-verifying the token we just signed.
42
- */
43
- async generateAccessToken(user, payload) {
38
+ /** One policy seam keeps account-state checks identical across all auth paths. */
39
+ async canAuthenticate(user) {
40
+ return authConfig.canAuthenticate(user);
41
+ }
42
+ async assertCanAuthenticate(user) {
43
+ if (!await this.canAuthenticate(user)) throw new ForbiddenError("This user cannot authenticate.");
44
+ }
45
+ async issueAccessToken(user, payload) {
44
46
  const data = payload || this.buildAccessTokenPayload(user);
45
47
  const expiresIn = authConfig.accessToken.expiresInMs();
46
48
  const token = await jwt.generate(data, { expiresIn });
@@ -52,10 +54,15 @@ var AuthService = class {
52
54
  };
53
55
  }
54
56
  /**
55
- * Sign + persist a refresh token for the user (enforcing the per-user cap
56
- * first). Resolves to `undefined` when refresh tokens are disabled in config.
57
+ * Sign + persist an access token for the user and return the token with its
58
+ * expiry. The expiry is computed locally from `expiresIn` rather than by
59
+ * re-verifying the token we just signed.
57
60
  */
58
- async createRefreshToken(user, deviceInfo) {
61
+ async generateAccessToken(user, payload) {
62
+ await this.assertCanAuthenticate(user);
63
+ return this.issueAccessToken(user, payload);
64
+ }
65
+ async issueRefreshToken(user, deviceInfo) {
59
66
  if (!authConfig.refreshToken.enabled()) return;
60
67
  const expiresIn = authConfig.refreshToken.expiresInMs();
61
68
  const familyId = deviceInfo?.familyId || Random.string(32);
@@ -74,11 +81,17 @@ var AuthService = class {
74
81
  });
75
82
  }
76
83
  /**
77
- * Issue both an access and a refresh token, emitting the creation events.
84
+ * Sign + persist a refresh token for the user (enforcing the per-user cap
85
+ * first). Resolves to `undefined` when refresh tokens are disabled in config.
78
86
  */
79
- async createTokenPair(user, deviceInfo) {
80
- const accessToken = await this.generateAccessToken(user, deviceInfo?.payload);
81
- const refreshToken = await this.createRefreshToken(user, deviceInfo);
87
+ async createRefreshToken(user, deviceInfo) {
88
+ if (!authConfig.refreshToken.enabled()) return;
89
+ await this.assertCanAuthenticate(user);
90
+ return this.issueRefreshToken(user, deviceInfo);
91
+ }
92
+ async issueTokenPair(user, deviceInfo) {
93
+ const accessToken = await this.issueAccessToken(user, deviceInfo?.payload);
94
+ const refreshToken = await this.issueRefreshToken(user, deviceInfo);
82
95
  const tokenPair = {
83
96
  accessToken,
84
97
  refreshToken: refreshToken ? {
@@ -91,38 +104,48 @@ var AuthService = class {
91
104
  return tokenPair;
92
105
  }
93
106
  /**
107
+ * Issue both an access and a refresh token, emitting the creation events.
108
+ */
109
+ async createTokenPair(user, deviceInfo) {
110
+ await this.assertCanAuthenticate(user);
111
+ return this.issueTokenPair(user, deviceInfo);
112
+ }
113
+ /**
94
114
  * Exchange a refresh token for a new token pair, with rotation + replay
95
115
  * detection. A concurrent reuse of the same token loses the atomic revoke and
96
116
  * is treated as a breach — the whole family is revoked and the request fails.
97
117
  */
98
118
  async refreshTokens(refreshTokenString, deviceInfo) {
119
+ let decoded;
99
120
  try {
100
- const decoded = await jwt.verifyRefreshToken(refreshTokenString);
101
- if (!decoded) return null;
102
- const refreshToken = await this.refreshTokenModel.findByToken(refreshTokenString);
103
- if (!refreshToken?.isValid) {
104
- if (refreshToken) await this.revokeTokenFamily(refreshToken.familyId);
105
- return null;
106
- }
107
- const UserModel = config.key(`auth.userType.${decoded.userType}`);
108
- if (!UserModel) return null;
109
- const user = await UserModel.find(decoded.userId);
110
- if (!user) return null;
111
- if (authConfig.refreshToken.rotation()) {
112
- if (!await refreshToken.revokeIfActive()) {
113
- await this.revokeTokenFamily(refreshToken.familyId);
114
- return null;
115
- }
116
- } else await refreshToken.markAsUsed();
117
- const newTokenPair = await this.createTokenPair(user, {
118
- ...deviceInfo,
119
- familyId: refreshToken.familyId
120
- });
121
- authEvents.emit("token.refreshed", user, newTokenPair, refreshToken);
122
- return newTokenPair;
123
- } catch {
121
+ decoded = await jwt.verifyRefreshToken(refreshTokenString);
122
+ } catch (error) {
123
+ if (isInvalidCredentialError(error)) return null;
124
+ throw error;
125
+ }
126
+ if (!decoded) return null;
127
+ const refreshToken = await this.refreshTokenModel.findByToken(refreshTokenString);
128
+ if (!refreshToken?.isValid) {
129
+ if (refreshToken) await this.revokeTokenFamily(refreshToken.familyId);
124
130
  return null;
125
131
  }
132
+ const UserModel = config.key(`auth.userType.${decoded.userType}`);
133
+ if (!UserModel) throw new Error(`User type ${decoded.userType} is unknown type.`);
134
+ const user = await UserModel.find(decoded.userId);
135
+ if (!user) return null;
136
+ if (!await this.canAuthenticate(user)) return null;
137
+ if (authConfig.refreshToken.rotation()) {
138
+ if (!await refreshToken.revokeIfActive()) {
139
+ await this.revokeTokenFamily(refreshToken.familyId);
140
+ return null;
141
+ }
142
+ } else await refreshToken.markAsUsed();
143
+ const newTokenPair = await this.issueTokenPair(user, {
144
+ ...deviceInfo,
145
+ familyId: refreshToken.familyId
146
+ });
147
+ authEvents.emit("token.refreshed", user, newTokenPair, refreshToken);
148
+ return newTokenPair;
126
149
  }
127
150
  /**
128
151
  * Verify a plaintext password against a stored hash.
@@ -152,6 +175,10 @@ var AuthService = class {
152
175
  authEvents.emit("login.failed", otherData, "Invalid password");
153
176
  return null;
154
177
  }
178
+ if (!await this.canAuthenticate(user)) {
179
+ authEvents.emit("login.failed", otherData, "Authentication not allowed");
180
+ return null;
181
+ }
155
182
  return user;
156
183
  }
157
184
  /**
@@ -163,9 +190,9 @@ var AuthService = class {
163
190
  if (!user) return null;
164
191
  if (!authConfig.refreshToken.enabled()) return {
165
192
  user,
166
- tokens: { accessToken: await this.generateAccessToken(user, deviceInfo?.payload) }
193
+ tokens: { accessToken: await this.issueAccessToken(user, deviceInfo?.payload) }
167
194
  };
168
- const tokens = await this.createTokenPair(user, deviceInfo);
195
+ const tokens = await this.issueTokenPair(user, deviceInfo);
169
196
  authEvents.emit("login.success", user, tokens, deviceInfo);
170
197
  return {
171
198
  user,
@@ -1 +1 @@
1
- {"version":3,"file":"auth.service.mjs","names":[],"sources":["../../../../../../../auth/src/services/auth.service.ts"],"sourcesContent":["import { Random } from \"@mongez/reinforcements\";\r\nimport type { ChildModel } from \"@warlock.js/cascade\";\r\nimport { config, hashPassword, verifyPassword } from \"@warlock.js/core\";\r\nimport type { AccessTokenOutput, DeviceInfo, LoginResult, TokenPair } from \"../contracts/types\";\r\nimport { AccessToken } from \"../models/access-token\";\r\nimport type { Auth } from \"../models/auth.model\";\r\nimport { RefreshToken } from \"../models/refresh-token\";\r\nimport { authConfig } from \"./auth-config\";\r\nimport { authEvents } from \"./auth-events\";\r\nimport { jwt } from \"./jwt\";\r\n\r\nclass AuthService {\r\n /**\r\n * Resolve the active access-token model — the package default, or a subclass\r\n * an app registered under `config.auth.accessToken.model` (e.g. to add a\r\n * tenant column). The service never references the concrete class directly so\r\n * an override is a pure config change.\r\n */\r\n private get accessTokenModel(): typeof AccessToken {\r\n return config.key(\"auth.accessToken.model\", AccessToken);\r\n }\r\n\r\n /**\r\n * Resolve the active refresh-token model (default or registered override).\r\n */\r\n private get refreshTokenModel(): typeof RefreshToken {\r\n return config.key(\"auth.refreshToken.model\", RefreshToken);\r\n }\r\n\r\n /**\r\n * Build the default access-token JWT payload from a user.\r\n */\r\n public buildAccessTokenPayload(user: Auth) {\r\n return {\r\n id: user.id,\r\n userType: user.userType,\r\n created_at: Date.now(),\r\n };\r\n }\r\n\r\n /**\r\n * Sign + persist an access token for the user and return the token with its\r\n * expiry. The expiry is computed locally from `expiresIn` rather than by\r\n * re-verifying the token we just signed.\r\n */\r\n public async generateAccessToken(user: Auth, payload?: any): Promise<AccessTokenOutput> {\r\n const data = payload || this.buildAccessTokenPayload(user);\r\n // Validated before anything is signed or persisted: an unusable lifetime\r\n // throws naming the config key rather than minting a token whose expiry\r\n // nobody chose (defaults to 1h when unset).\r\n const expiresIn = authConfig.accessToken.expiresInMs();\r\n\r\n const token = await jwt.generate(data, { expiresIn });\r\n const expiresAt = new Date(Date.now() + expiresIn);\r\n\r\n await this.accessTokenModel.issue(user, token, expiresAt);\r\n\r\n return { token, expiresAt: expiresAt.toISOString() };\r\n }\r\n\r\n /**\r\n * Sign + persist a refresh token for the user (enforcing the per-user cap\r\n * first). Resolves to `undefined` when refresh tokens are disabled in config.\r\n */\r\n public async createRefreshToken(\r\n user: Auth,\r\n deviceInfo?: DeviceInfo,\r\n ): Promise<RefreshToken | undefined> {\r\n if (!authConfig.refreshToken.enabled()) return;\r\n\r\n // Validate the lifetime first — before the per-user cap is enforced, before\r\n // anything is signed — so a bad value can never revoke a user's oldest\r\n // session on its way to failing.\r\n const expiresIn = authConfig.refreshToken.expiresInMs();\r\n\r\n const familyId = deviceInfo?.familyId || Random.string(32);\r\n\r\n const payload = {\r\n userId: user.id,\r\n userType: user.userType,\r\n familyId,\r\n };\r\n\r\n const expiresAt = new Date(Date.now() + expiresIn).toISOString();\r\n\r\n await this.refreshTokenModel.enforceMax(user, authConfig.refreshToken.maxPerUser());\r\n\r\n const token = await jwt.generateRefreshToken(payload, { expiresIn });\r\n\r\n return this.refreshTokenModel.issue(user, token, { familyId, expiresAt, deviceInfo });\r\n }\r\n\r\n /**\r\n * Issue both an access and a refresh token, emitting the creation events.\r\n */\r\n public async createTokenPair(user: Auth, deviceInfo?: DeviceInfo): Promise<TokenPair> {\r\n const accessToken = await this.generateAccessToken(user, deviceInfo?.payload);\r\n const refreshToken = await this.createRefreshToken(user, deviceInfo);\r\n\r\n const tokenPair: TokenPair = {\r\n accessToken,\r\n refreshToken: refreshToken\r\n ? {\r\n token: refreshToken.get(\"token\"),\r\n expiresAt: refreshToken.get(\"expires_at\"),\r\n }\r\n : undefined,\r\n };\r\n\r\n authEvents.emit(\"token.created\", user, tokenPair);\r\n\r\n if (refreshToken) {\r\n authEvents.emit(\"session.created\", user, refreshToken, deviceInfo);\r\n }\r\n\r\n return tokenPair;\r\n }\r\n\r\n /**\r\n * Exchange a refresh token for a new token pair, with rotation + replay\r\n * detection. A concurrent reuse of the same token loses the atomic revoke and\r\n * is treated as a breach — the whole family is revoked and the request fails.\r\n */\r\n public async refreshTokens(\r\n refreshTokenString: string,\r\n deviceInfo?: DeviceInfo,\r\n ): Promise<TokenPair | null> {\r\n try {\r\n const decoded = await jwt.verifyRefreshToken<{\r\n userId: number;\r\n userType: string;\r\n familyId: string;\r\n }>(refreshTokenString);\r\n\r\n if (!decoded) return null;\r\n\r\n const refreshToken = await this.refreshTokenModel.findByToken(refreshTokenString);\r\n\r\n if (!refreshToken?.isValid) {\r\n // Already-invalid token presented → likely a replayed (rotated) token.\r\n if (refreshToken) {\r\n await this.revokeTokenFamily(refreshToken.familyId);\r\n }\r\n\r\n return null;\r\n }\r\n\r\n const UserModel = config.key(`auth.userType.${decoded.userType}`);\r\n\r\n if (!UserModel) return null;\r\n\r\n const user = (await UserModel.find(decoded.userId)) as Auth | null;\r\n\r\n if (!user) return null;\r\n\r\n const rotationEnabled = authConfig.refreshToken.rotation();\r\n\r\n if (rotationEnabled) {\r\n const won = await refreshToken.revokeIfActive();\r\n\r\n if (!won) {\r\n // A concurrent request already rotated this token (reuse / replay).\r\n await this.revokeTokenFamily(refreshToken.familyId);\r\n\r\n return null;\r\n }\r\n } else {\r\n await refreshToken.markAsUsed();\r\n }\r\n\r\n const newTokenPair = await this.createTokenPair(user, {\r\n ...deviceInfo,\r\n familyId: refreshToken.familyId,\r\n });\r\n\r\n authEvents.emit(\"token.refreshed\", user, newTokenPair, refreshToken);\r\n\r\n return newTokenPair;\r\n } catch {\r\n return null;\r\n }\r\n }\r\n\r\n /**\r\n * Verify a plaintext password against a stored hash.\r\n */\r\n public async verifyPassword(plainPassword: string, hashedPassword: string): Promise<boolean> {\r\n return verifyPassword(plainPassword, hashedPassword);\r\n }\r\n\r\n /**\r\n * Hash a plaintext password.\r\n */\r\n public async hashPassword(password: string): Promise<string> {\r\n return hashPassword(password);\r\n }\r\n\r\n /**\r\n * Resolve a user by credentials, verifying the password. Returns `null` on a\r\n * missing user or a wrong password, emitting `login.failed` either way.\r\n */\r\n public async attemptLogin<T extends Auth>(Model: ChildModel<T>, data: any): Promise<T | null> {\r\n const { password, ...otherData } = data;\r\n\r\n authEvents.emit(\"login.attempt\", otherData);\r\n\r\n const user = (await Model.first(otherData)) as T | null;\r\n\r\n if (!user) {\r\n authEvents.emit(\"login.failed\", otherData, \"User not found\");\r\n\r\n return null;\r\n }\r\n\r\n if (!(await this.verifyPassword(password, user.string(\"password\")!))) {\r\n authEvents.emit(\"login.failed\", otherData, \"Invalid password\");\r\n\r\n return null;\r\n }\r\n\r\n return user;\r\n }\r\n\r\n /**\r\n * Full login flow: validate credentials, issue tokens, emit events. Returns\r\n * the user + token pair on success, `null` on failure.\r\n */\r\n public async login<T extends Auth>(\r\n Model: ChildModel<T>,\r\n credentials: any,\r\n deviceInfo?: DeviceInfo,\r\n ): Promise<LoginResult<T> | null> {\r\n const user = await this.attemptLogin(Model, credentials);\r\n\r\n if (!user) {\r\n return null;\r\n }\r\n\r\n if (!authConfig.refreshToken.enabled()) {\r\n const accessToken = await this.generateAccessToken(user, deviceInfo?.payload);\r\n\r\n return { user, tokens: { accessToken } };\r\n }\r\n\r\n const tokens = await this.createTokenPair(user, deviceInfo);\r\n\r\n authEvents.emit(\"login.success\", user, tokens, deviceInfo);\r\n\r\n return { user, tokens } as LoginResult<T>;\r\n }\r\n\r\n /**\r\n * Log a user out.\r\n *\r\n * @param accessToken - access token string to revoke (optional)\r\n * @param refreshToken - refresh token string to revoke (optional)\r\n *\r\n * When no refresh token is supplied, `config.auth.refreshToken.logoutWithoutToken`\r\n * decides the behavior: `\"revoke-all\"` (default, fail-safe) revokes every\r\n * refresh token; `\"error\"` requires the caller to pass one.\r\n */\r\n public async logout(user: Auth, accessToken?: string, refreshToken?: string): Promise<void> {\r\n if (accessToken) {\r\n await this.removeAccessToken(user, accessToken);\r\n }\r\n\r\n if (refreshToken) {\r\n const token = await this.refreshTokenModel.findForUser(user, refreshToken);\r\n\r\n if (token) {\r\n await token.revoke();\r\n authEvents.emit(\"session.destroyed\", user, token);\r\n }\r\n } else {\r\n const behavior = authConfig.refreshToken.logoutWithoutToken();\r\n\r\n if (behavior === \"error\") {\r\n throw new Error(\"Refresh token required for logout\");\r\n }\r\n\r\n await this.revokeAllTokens(user);\r\n authEvents.emit(\"logout.failsafe\", user);\r\n }\r\n\r\n authEvents.emit(\"logout\", user);\r\n }\r\n\r\n /**\r\n * Remove a specific access token belonging to the user.\r\n */\r\n public async removeAccessToken(user: Auth, token: string): Promise<void> {\r\n await this.accessTokenModel.deleteForUser(user, token);\r\n }\r\n\r\n /**\r\n * Remove every access token belonging to the user.\r\n */\r\n public async removeAllAccessTokens(user: Auth): Promise<void> {\r\n await this.accessTokenModel.deleteAllForUser(user);\r\n }\r\n\r\n /**\r\n * Remove a specific refresh token belonging to the user.\r\n */\r\n public async removeRefreshToken(user: Auth, token: string): Promise<void> {\r\n await this.refreshTokenModel.deleteForUser(user, token);\r\n }\r\n\r\n /**\r\n * Revoke every active refresh token for the user and delete their access\r\n * tokens — \"log out of all devices\". Revocation is a single bulk update; an\r\n * event fires per revoked token.\r\n */\r\n public async revokeAllTokens(user: Auth): Promise<void> {\r\n const revokedTokens = await this.refreshTokenModel.revokeAllFor(user);\r\n\r\n for (const token of revokedTokens) {\r\n authEvents.emit(\"token.revoked\", user, token);\r\n }\r\n\r\n await this.removeAllAccessTokens(user);\r\n\r\n authEvents.emit(\"logout.all\", user);\r\n }\r\n\r\n /**\r\n * Revoke an entire token family — rotation breach containment.\r\n */\r\n public async revokeTokenFamily(familyId: string): Promise<void> {\r\n const revokedTokens = await this.refreshTokenModel.revokeFamily(familyId);\r\n\r\n authEvents.emit(\"token.familyRevoked\", familyId, revokedTokens);\r\n }\r\n\r\n /**\r\n * Delete expired tokens (refresh + access). Emits `token.expired` per refresh\r\n * token and `cleanup.completed` with the refresh count. Drives the\r\n * `auth.cleanup` CLI command.\r\n */\r\n public async cleanupExpiredTokens(): Promise<number> {\r\n const expiredTokens = await this.refreshTokenModel.purgeExpired();\r\n\r\n for (const token of expiredTokens) {\r\n authEvents.emit(\"token.expired\", token);\r\n }\r\n\r\n await this.accessTokenModel.purgeExpired();\r\n\r\n authEvents.emit(\"cleanup.completed\", expiredTokens.length);\r\n\r\n return expiredTokens.length;\r\n }\r\n\r\n /**\r\n * Find every persisted token — access and refresh — that can never retire\r\n * itself: an unusable `expires_at`, or a token carrying no `exp` claim.\r\n *\r\n * This is the remediation read for the pre-4.12.0 `expiresIn` defect (#25).\r\n * Nothing else surfaces these rows: `auth.cleanup` selects `expires_at < now`,\r\n * which an `Invalid Date` never satisfies, and a row whose date column is\r\n * perfectly fine can still hold a token with no deadline in it. Read-only —\r\n * pair with {@link purgeNeverExpiringTokens} to act on the answer.\r\n */\r\n public async findNeverExpiringTokens(): Promise<{\r\n accessTokens: AccessToken[];\r\n refreshTokens: RefreshToken[];\r\n }> {\r\n return {\r\n accessTokens: await this.accessTokenModel.findNeverExpiring(),\r\n refreshTokens: await this.refreshTokenModel.findNeverExpiring(),\r\n };\r\n }\r\n\r\n /**\r\n * Revoke every never-expiring token by deleting its row, emitting\r\n * `token.revoked` per refresh token. Drives `warlock auth.purge-never-expiring`.\r\n *\r\n * Access tokens first: deleting one is immediate revocation, while a refresh\r\n * token left in place for a moment can only mint an access token through a\r\n * verifier that now rejects it for the missing `exp`.\r\n */\r\n public async purgeNeverExpiringTokens(): Promise<{\r\n accessTokens: number;\r\n refreshTokens: number;\r\n }> {\r\n const accessTokens = await this.accessTokenModel.purgeNeverExpiring();\r\n const refreshTokens = await this.refreshTokenModel.purgeNeverExpiring();\r\n\r\n // `token.expired` (the event `auth.cleanup` already emits for a removed\r\n // refresh token) rather than `token.revoked`, which carries a loaded `Auth`\r\n // this batch path has no reason to fetch a user row for.\r\n for (const token of refreshTokens) {\r\n authEvents.emit(\"token.expired\", token);\r\n }\r\n\r\n return { accessTokens: accessTokens.length, refreshTokens: refreshTokens.length };\r\n }\r\n\r\n /**\r\n * Active, unexpired sessions for the user, newest first.\r\n */\r\n public async getActiveSessions(user: Auth): Promise<RefreshToken[]> {\r\n return this.refreshTokenModel.activeFor(user);\r\n }\r\n}\r\n\r\nexport const authService = new AuthService();\r\n"],"mappings":"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"}
1
+ {"version":3,"file":"auth.service.mjs","names":[],"sources":["../../../../../../../auth/src/services/auth.service.ts"],"sourcesContent":["import { Random } from \"@mongez/reinforcements\";\r\nimport type { ChildModel } from \"@warlock.js/cascade\";\r\nimport { config, ForbiddenError, hashPassword, verifyPassword } from \"@warlock.js/core\";\nimport type {\n AccessTokenOutput,\n AuthCredentials,\n DeviceInfo,\n LoginResult,\n TokenPair,\n} from \"../contracts/types\";\nimport { AccessToken } from \"../models/access-token\";\nimport type { Auth } from \"../models/auth.model\";\nimport { RefreshToken } from \"../models/refresh-token\";\nimport { authConfig } from \"./auth-config\";\nimport { authEvents } from \"./auth-events\";\nimport { isInvalidCredentialError, jwt } from \"./jwt\";\n\nclass AuthService {\n /**\r\n * Resolve the active access-token model — the package default, or a subclass\r\n * an app registered under `config.auth.accessToken.model` (e.g. to add a\r\n * tenant column). The service never references the concrete class directly so\r\n * an override is a pure config change.\r\n */\r\n private get accessTokenModel(): typeof AccessToken {\r\n return config.key(\"auth.accessToken.model\", AccessToken);\r\n }\r\n\r\n /**\r\n * Resolve the active refresh-token model (default or registered override).\r\n */\r\n private get refreshTokenModel(): typeof RefreshToken {\r\n return config.key(\"auth.refreshToken.model\", RefreshToken);\r\n }\r\n\r\n /**\r\n * Build the default access-token JWT payload from a user.\r\n */\r\n public buildAccessTokenPayload(user: Auth): Record<string, unknown> {\n return {\r\n id: user.id,\r\n userType: user.userType,\r\n created_at: Date.now(),\r\n };\r\n }\n\n /** One policy seam keeps account-state checks identical across all auth paths. */\n public async canAuthenticate(user: Auth): Promise<boolean> {\n return authConfig.canAuthenticate(user);\n }\n\n private async assertCanAuthenticate(user: Auth): Promise<void> {\n if (!(await this.canAuthenticate(user))) {\n throw new ForbiddenError(\"This user cannot authenticate.\");\n }\n }\n\n private async issueAccessToken(\n user: Auth,\n payload?: Record<string, unknown>,\n ): Promise<AccessTokenOutput> {\n const data = payload || this.buildAccessTokenPayload(user);\n // Validate before signing so an unusable lifetime cannot mint an immortal token.\n const expiresIn = authConfig.accessToken.expiresInMs();\n\n const token = await jwt.generate(data, { expiresIn });\n const expiresAt = new Date(Date.now() + expiresIn);\n\n await this.accessTokenModel.issue(user, token, expiresAt);\n\n return { token, expiresAt: expiresAt.toISOString() };\n }\n\r\n /**\r\n * Sign + persist an access token for the user and return the token with its\r\n * expiry. The expiry is computed locally from `expiresIn` rather than by\r\n * re-verifying the token we just signed.\r\n */\r\n public async generateAccessToken(\n user: Auth,\n payload?: Record<string, unknown>,\n ): Promise<AccessTokenOutput> {\n await this.assertCanAuthenticate(user);\n\n return this.issueAccessToken(user, payload);\n }\n\r\n private async issueRefreshToken(\n user: Auth,\n deviceInfo?: DeviceInfo,\n ): Promise<RefreshToken | undefined> {\n if (!authConfig.refreshToken.enabled()) return;\r\n\r\n // Validate the lifetime first — before the per-user cap is enforced, before\r\n // anything is signed — so a bad value can never revoke a user's oldest\r\n // session on its way to failing.\r\n const expiresIn = authConfig.refreshToken.expiresInMs();\r\n\r\n const familyId = deviceInfo?.familyId || Random.string(32);\r\n\r\n const payload = {\r\n userId: user.id,\r\n userType: user.userType,\r\n familyId,\r\n };\r\n\r\n const expiresAt = new Date(Date.now() + expiresIn).toISOString();\r\n\r\n await this.refreshTokenModel.enforceMax(user, authConfig.refreshToken.maxPerUser());\r\n\r\n const token = await jwt.generateRefreshToken(payload, { expiresIn });\r\n\r\n return this.refreshTokenModel.issue(user, token, { familyId, expiresAt, deviceInfo });\n }\n\n /**\n * Sign + persist a refresh token for the user (enforcing the per-user cap\n * first). Resolves to `undefined` when refresh tokens are disabled in config.\n */\n public async createRefreshToken(\n user: Auth,\n deviceInfo?: DeviceInfo,\n ): Promise<RefreshToken | undefined> {\n if (!authConfig.refreshToken.enabled()) return;\n\n await this.assertCanAuthenticate(user);\n\n return this.issueRefreshToken(user, deviceInfo);\n }\n\n private async issueTokenPair(user: Auth, deviceInfo?: DeviceInfo): Promise<TokenPair> {\n const accessToken = await this.issueAccessToken(user, deviceInfo?.payload);\n const refreshToken = await this.issueRefreshToken(user, deviceInfo);\n\n const tokenPair: TokenPair = {\n accessToken,\n refreshToken: refreshToken\n ? {\n token: refreshToken.get(\"token\"),\n expiresAt: refreshToken.get(\"expires_at\"),\n }\n : undefined,\n };\n\n authEvents.emit(\"token.created\", user, tokenPair);\n\n if (refreshToken) {\n authEvents.emit(\"session.created\", user, refreshToken, deviceInfo);\n }\n\n return tokenPair;\n }\n\r\n /**\r\n * Issue both an access and a refresh token, emitting the creation events.\r\n */\r\n public async createTokenPair(user: Auth, deviceInfo?: DeviceInfo): Promise<TokenPair> {\n await this.assertCanAuthenticate(user);\n\n return this.issueTokenPair(user, deviceInfo);\n }\n\r\n /**\r\n * Exchange a refresh token for a new token pair, with rotation + replay\r\n * detection. A concurrent reuse of the same token loses the atomic revoke and\r\n * is treated as a breach — the whole family is revoked and the request fails.\r\n */\r\n public async refreshTokens(\n refreshTokenString: string,\n deviceInfo?: DeviceInfo,\n ): Promise<TokenPair | null> {\n let decoded:\n | {\n userId: number;\n userType: string;\n familyId: string;\n }\n | null;\n\n try {\n decoded = await jwt.verifyRefreshToken<{\n userId: number;\n userType: string;\n familyId: string;\n }>(refreshTokenString);\n } catch (error) {\n if (isInvalidCredentialError(error)) return null;\n\n throw error;\n }\n\n if (!decoded) return null;\n\r\n const refreshToken = await this.refreshTokenModel.findByToken(refreshTokenString);\n\n if (!refreshToken?.isValid) {\n // An already-invalid token may be a replay of a rotated credential.\n if (refreshToken) {\n await this.revokeTokenFamily(refreshToken.familyId);\n }\n\n return null;\n }\n\n const UserModel = config.key(`auth.userType.${decoded.userType}`);\n\n if (!UserModel) {\n throw new Error(`User type ${decoded.userType} is unknown type.`);\n }\n\n const user = (await UserModel.find(decoded.userId)) as Auth | null;\n\n if (!user) return null;\n\n if (!(await this.canAuthenticate(user))) return null;\n\n const rotationEnabled = authConfig.refreshToken.rotation();\n\n if (rotationEnabled) {\n const won = await refreshToken.revokeIfActive();\n\r\n if (!won) {\n // A concurrent request already rotated this token.\n await this.revokeTokenFamily(refreshToken.familyId);\n\n return null;\n }\n } else {\n await refreshToken.markAsUsed();\n }\n\n const newTokenPair = await this.issueTokenPair(user, {\n ...deviceInfo,\n familyId: refreshToken.familyId,\n });\n\n authEvents.emit(\"token.refreshed\", user, newTokenPair, refreshToken);\n\n return newTokenPair;\n }\n\r\n /**\r\n * Verify a plaintext password against a stored hash.\r\n */\r\n public async verifyPassword(plainPassword: string, hashedPassword: string): Promise<boolean> {\r\n return verifyPassword(plainPassword, hashedPassword);\r\n }\r\n\r\n /**\r\n * Hash a plaintext password.\r\n */\r\n public async hashPassword(password: string): Promise<string> {\r\n return hashPassword(password);\r\n }\r\n\r\n /**\r\n * Resolve a user by credentials, verifying the password. Returns `null` on a\r\n * missing user or a wrong password, emitting `login.failed` either way.\r\n */\r\n public async attemptLogin<T extends Auth>(\n Model: ChildModel<T>,\n data: AuthCredentials,\n ): Promise<T | null> {\n const { password, ...otherData } = data;\r\n\r\n authEvents.emit(\"login.attempt\", otherData);\r\n\r\n const user = (await Model.first(otherData)) as T | null;\r\n\r\n if (!user) {\r\n authEvents.emit(\"login.failed\", otherData, \"User not found\");\r\n\r\n return null;\r\n }\r\n\r\n if (!(await this.verifyPassword(password, user.string(\"password\")!))) {\n authEvents.emit(\"login.failed\", otherData, \"Invalid password\");\n\n return null;\n }\n\n if (!(await this.canAuthenticate(user))) {\n authEvents.emit(\"login.failed\", otherData, \"Authentication not allowed\");\n\n return null;\n }\n\n return user;\n }\r\n\r\n /**\r\n * Full login flow: validate credentials, issue tokens, emit events. Returns\r\n * the user + token pair on success, `null` on failure.\r\n */\r\n public async login<T extends Auth>(\r\n Model: ChildModel<T>,\r\n credentials: AuthCredentials,\n deviceInfo?: DeviceInfo,\r\n ): Promise<LoginResult<T> | null> {\r\n const user = await this.attemptLogin(Model, credentials);\r\n\r\n if (!user) {\r\n return null;\r\n }\r\n\r\n if (!authConfig.refreshToken.enabled()) {\n const accessToken = await this.issueAccessToken(user, deviceInfo?.payload);\n\n return { user, tokens: { accessToken } };\n }\n\n const tokens = await this.issueTokenPair(user, deviceInfo);\n\r\n authEvents.emit(\"login.success\", user, tokens, deviceInfo);\r\n\r\n return { user, tokens };\n }\r\n\r\n /**\r\n * Log a user out.\r\n *\r\n * @param accessToken - access token string to revoke (optional)\r\n * @param refreshToken - refresh token string to revoke (optional)\r\n *\r\n * When no refresh token is supplied, `config.auth.refreshToken.logoutWithoutToken`\r\n * decides the behavior: `\"revoke-all\"` (default, fail-safe) revokes every\r\n * refresh token; `\"error\"` requires the caller to pass one.\r\n */\r\n public async logout(user: Auth, accessToken?: string, refreshToken?: string): Promise<void> {\r\n if (accessToken) {\r\n await this.removeAccessToken(user, accessToken);\r\n }\r\n\r\n if (refreshToken) {\r\n const token = await this.refreshTokenModel.findForUser(user, refreshToken);\r\n\r\n if (token) {\r\n await token.revoke();\r\n authEvents.emit(\"session.destroyed\", user, token);\r\n }\r\n } else {\r\n const behavior = authConfig.refreshToken.logoutWithoutToken();\r\n\r\n if (behavior === \"error\") {\r\n throw new Error(\"Refresh token required for logout\");\r\n }\r\n\r\n await this.revokeAllTokens(user);\r\n authEvents.emit(\"logout.failsafe\", user);\r\n }\r\n\r\n authEvents.emit(\"logout\", user);\r\n }\r\n\r\n /**\r\n * Remove a specific access token belonging to the user.\r\n */\r\n public async removeAccessToken(user: Auth, token: string): Promise<void> {\r\n await this.accessTokenModel.deleteForUser(user, token);\r\n }\r\n\r\n /**\r\n * Remove every access token belonging to the user.\r\n */\r\n public async removeAllAccessTokens(user: Auth): Promise<void> {\r\n await this.accessTokenModel.deleteAllForUser(user);\r\n }\r\n\r\n /**\r\n * Remove a specific refresh token belonging to the user.\r\n */\r\n public async removeRefreshToken(user: Auth, token: string): Promise<void> {\r\n await this.refreshTokenModel.deleteForUser(user, token);\r\n }\r\n\r\n /**\r\n * Revoke every active refresh token for the user and delete their access\r\n * tokens — \"log out of all devices\". Revocation is a single bulk update; an\r\n * event fires per revoked token.\r\n */\r\n public async revokeAllTokens(user: Auth): Promise<void> {\r\n const revokedTokens = await this.refreshTokenModel.revokeAllFor(user);\r\n\r\n for (const token of revokedTokens) {\r\n authEvents.emit(\"token.revoked\", user, token);\r\n }\r\n\r\n await this.removeAllAccessTokens(user);\r\n\r\n authEvents.emit(\"logout.all\", user);\r\n }\r\n\r\n /**\r\n * Revoke an entire token family — rotation breach containment.\r\n */\r\n public async revokeTokenFamily(familyId: string): Promise<void> {\r\n const revokedTokens = await this.refreshTokenModel.revokeFamily(familyId);\r\n\r\n authEvents.emit(\"token.familyRevoked\", familyId, revokedTokens);\r\n }\r\n\r\n /**\r\n * Delete expired tokens (refresh + access). Emits `token.expired` per refresh\r\n * token and `cleanup.completed` with the refresh count. Drives the\r\n * `auth.cleanup` CLI command.\r\n */\r\n public async cleanupExpiredTokens(): Promise<number> {\r\n const expiredTokens = await this.refreshTokenModel.purgeExpired();\r\n\r\n for (const token of expiredTokens) {\r\n authEvents.emit(\"token.expired\", token);\r\n }\r\n\r\n await this.accessTokenModel.purgeExpired();\r\n\r\n authEvents.emit(\"cleanup.completed\", expiredTokens.length);\r\n\r\n return expiredTokens.length;\r\n }\r\n\r\n /**\r\n * Find every persisted token — access and refresh — that can never retire\r\n * itself: an unusable `expires_at`, or a token carrying no `exp` claim.\r\n *\r\n * This is the remediation read for the pre-4.12.0 `expiresIn` defect (#25).\r\n * Nothing else surfaces these rows: `auth.cleanup` selects `expires_at < now`,\r\n * which an `Invalid Date` never satisfies, and a row whose date column is\r\n * perfectly fine can still hold a token with no deadline in it. Read-only —\r\n * pair with {@link purgeNeverExpiringTokens} to act on the answer.\r\n */\r\n public async findNeverExpiringTokens(): Promise<{\r\n accessTokens: AccessToken[];\r\n refreshTokens: RefreshToken[];\r\n }> {\r\n return {\r\n accessTokens: await this.accessTokenModel.findNeverExpiring(),\r\n refreshTokens: await this.refreshTokenModel.findNeverExpiring(),\r\n };\r\n }\r\n\r\n /**\r\n * Revoke every never-expiring token by deleting its row, emitting\r\n * `token.revoked` per refresh token. Drives `warlock auth.purge-never-expiring`.\r\n *\r\n * Access tokens first: deleting one is immediate revocation, while a refresh\r\n * token left in place for a moment can only mint an access token through a\r\n * verifier that now rejects it for the missing `exp`.\r\n */\r\n public async purgeNeverExpiringTokens(): Promise<{\r\n accessTokens: number;\r\n refreshTokens: number;\r\n }> {\r\n const accessTokens = await this.accessTokenModel.purgeNeverExpiring();\r\n const refreshTokens = await this.refreshTokenModel.purgeNeverExpiring();\r\n\r\n // `token.expired` (the event `auth.cleanup` already emits for a removed\r\n // refresh token) rather than `token.revoked`, which carries a loaded `Auth`\r\n // this batch path has no reason to fetch a user row for.\r\n for (const token of refreshTokens) {\r\n authEvents.emit(\"token.expired\", token);\r\n }\r\n\r\n return { accessTokens: accessTokens.length, refreshTokens: refreshTokens.length };\r\n }\r\n\r\n /**\r\n * Active, unexpired sessions for the user, newest first.\r\n */\r\n public async getActiveSessions(user: Auth): Promise<RefreshToken[]> {\r\n return this.refreshTokenModel.activeFor(user);\r\n }\r\n}\r\n\r\nexport const authService = new AuthService();\r\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
1
  import { AuthEventCallback, AuthEventName, AuthEventPayloads, authEvents } from "./auth-events.mjs";
2
2
  import { authService } from "./auth.service.mjs";
3
3
  import { generateJWTSecret } from "./generate-jwt-secret.mjs";
4
- import { TokenType, jwt } from "./jwt.mjs";
4
+ import { TokenType, TokenTypeError, isInvalidCredentialError, jwt } from "./jwt.mjs";
@@ -1,5 +1,5 @@
1
1
  import { authEvents } from "./auth-events.mjs";
2
- import { jwt } from "./jwt.mjs";
2
+ import { TokenTypeError, isInvalidCredentialError, jwt } from "./jwt.mjs";
3
3
  import { authService } from "./auth.service.mjs";
4
4
  import { generateJWTSecret } from "./generate-jwt-secret.mjs";
5
5
 
@@ -1,3 +1,4 @@
1
+ import { AuthErrorCodes } from "../utils/auth-error-codes.mjs";
1
2
  import { SignerOptions, VerifierOptions } from "fast-jwt";
2
3
 
3
4
  //#region ../auth/src/services/jwt.d.ts
@@ -10,6 +11,25 @@ import { SignerOptions, VerifierOptions } from "fast-jwt";
10
11
  * type is always rejected.
11
12
  */
12
13
  type TokenType = "access" | "refresh";
14
+ /**
15
+ * Only coded credential failures become authentication misses. Plain errors,
16
+ * including missing-secret configuration failures, propagate to the central
17
+ * server-error path instead of reading as "everyone's token is bad".
18
+ */
19
+ declare function isInvalidCredentialError(error: unknown): boolean;
20
+ /**
21
+ * A `tokenType` claim that does not match what the caller expects. This IS a
22
+ * bad credential (an access-token cookie can only hold a refresh token
23
+ * through an app bug) — but unlike `fast-jwt`'s own rejections it was
24
+ * previously a plain `Error`, unclassifiable by a caller that wants to answer
25
+ * 401 without string-matching a message that could reword on any release.
26
+ * `code` mirrors how `fast-jwt`'s `TokenError` carries its own code, so a
27
+ * caller can classify both through `isInvalidCredentialError` above.
28
+ */
29
+ declare class TokenTypeError extends Error {
30
+ readonly code = AuthErrorCodes.InvalidTokenType;
31
+ constructor(expected: TokenType, actual: string);
32
+ }
13
33
  declare const jwt: {
14
34
  /**
15
35
  * Generate a new JWT token for the user.
@@ -59,5 +79,5 @@ declare const jwt: {
59
79
  }): Promise<T>;
60
80
  };
61
81
  //#endregion
62
- export { TokenType, jwt };
82
+ export { TokenType, TokenTypeError, isInvalidCredentialError, jwt };
63
83
  //# sourceMappingURL=jwt.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"jwt.d.mts","names":[],"sources":["../../../../../../../auth/src/services/jwt.ts"],"mappings":";;;;;AAkBA;;;;AAAqB;AA8CrB;KA9CY,SAAA;AAAA,cA8CC,GAAA;EAMG;;;;;IAAA,GAAA;IAAA,SAAA;IAAA,GAAA;EAAA,IAKT,aAAA;IAAkB,GAAA;EAAA,IACpB,OAAA;EAoBE;;;;;sBAPS,KAAA;IACC,GAAA;IAAA,UAAA;IAAA,cAAA;IAAA,GAAA;EAAA,IAMV,eAAA;IAAoB,GAAA;EAAA,IACtB,OAAA,CAAQ,CAAA;EAmCI;;;;IAhBD,GAAA;IAAA,SAAA;IAAA,SAAA;IAAA,GAAA;EAAA,IAMT,aAAA;IAAkB,GAAA;EAAA,IACpB,OAAA;EAgBO;;;kCARgB,KAAA;IACX,GAAA;IAAA,UAAA;IAAA,cAAA;IAAA,GAAA;EAAA,IAMV,eAAA;IAAoB,GAAA;EAAA,IACtB,OAAA,CAAQ,CAAA;AAAA"}
1
+ {"version":3,"file":"jwt.d.mts","names":[],"sources":["../../../../../../../auth/src/services/jwt.ts"],"mappings":";;;;;;AAmBA;;;;AAAqB;AAoCrB;KApCY,SAAA;;;AAoC2C;AAevD;;iBAfgB,wBAAA,CAAyB,KAAc;;;;;;;;;;cAe1C,cAAA,SAAuB,KAAA;EAAA,SACzB,IAAA,GAAI,cAAA,CAAA,gBAAA;cAED,QAAA,EAAU,SAAA,EAAW,MAAA;AAAA;AAAA,cA+CtB,GAAA;EA/CoC;AA+CjD;;;;IAMgB,GAAA;IAAA,SAAA;IAAA,GAAA;EAAA,IAKT,aAAA;IAAkB,GAAA;EAAA,IACpB,OAAA;EAAA;;;;;sBAaW,KAAA;IACC,GAAA;IAAA,UAAA;IAAA,cAAA;IAAA,GAAA;EAAA,IAMV,eAAA;IAAoB,GAAA;EAAA,IACtB,OAAA,CAAQ,CAAA;EAmBG;;;;IAAA,GAAA;IAAA,SAAA;IAAA,SAAA;IAAA,GAAA;EAAA,IAMT,aAAA;IAAkB,GAAA;EAAA,IACpB,OAAA;EASY;;;kCADW,KAAA;IACX,GAAA;IAAA,UAAA;IAAA,cAAA;IAAA,GAAA;EAAA,IAMV,eAAA;IAAoB,GAAA;EAAA,IACtB,OAAA,CAAQ,CAAA;AAAA"}