@warlock.js/auth 4.1.14 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +44 -0
- package/LICENSE +21 -0
- package/README.md +78 -37
- package/esm/commands/auth-cleanup-command.d.mts +1 -1
- package/esm/commands/auth-cleanup-command.d.mts.map +1 -1
- package/esm/commands/auth-cleanup-command.mjs +1 -1
- package/esm/commands/auth-cleanup-command.mjs.map +1 -1
- package/esm/commands/jwt-secret-generator-command.d.mts +1 -1
- package/esm/commands/jwt-secret-generator-command.d.mts.map +1 -1
- package/esm/commands/jwt-secret-generator-command.mjs +1 -1
- package/esm/commands/jwt-secret-generator-command.mjs.map +1 -1
- package/esm/contracts/auth-contract.d.mts +2 -2
- package/esm/contracts/auth-contract.d.mts.map +1 -1
- package/esm/contracts/index.d.mts +1 -1
- package/esm/contracts/types.d.mts +105 -76
- package/esm/contracts/types.d.mts.map +1 -1
- package/esm/contracts/types.mjs +2 -2
- package/esm/contracts/types.mjs.map +1 -1
- package/esm/index.d.mts +6 -5
- package/esm/index.mjs +4 -3
- package/esm/middleware/auth.middleware.d.mts +1 -1
- package/esm/middleware/auth.middleware.d.mts.map +1 -1
- package/esm/middleware/auth.middleware.mjs +9 -9
- package/esm/middleware/auth.middleware.mjs.map +1 -1
- package/esm/middleware/index.d.mts +2 -0
- package/esm/middleware/index.mjs +1 -0
- package/esm/middleware/login-throttle.middleware.d.mts +116 -0
- package/esm/middleware/login-throttle.middleware.d.mts.map +1 -0
- package/esm/middleware/login-throttle.middleware.mjs +105 -0
- package/esm/middleware/login-throttle.middleware.mjs.map +1 -0
- package/esm/models/access-token/access-token.model.d.mts +47 -5
- package/esm/models/access-token/access-token.model.d.mts.map +1 -1
- package/esm/models/access-token/access-token.model.mjs +70 -4
- package/esm/models/access-token/access-token.model.mjs.map +1 -1
- package/esm/models/access-token/index.d.mts +1 -1
- package/esm/models/access-token/index.mjs +1 -1
- package/esm/models/access-token/migration.mjs +2 -3
- package/esm/models/access-token/migration.mjs.map +1 -1
- package/esm/models/auth.model.d.mts +2 -2
- package/esm/models/auth.model.d.mts.map +1 -1
- package/esm/models/auth.model.mjs +1 -1
- package/esm/models/auth.model.mjs.map +1 -1
- package/esm/models/index.d.mts +4 -3
- package/esm/models/index.d.mts.map +1 -1
- package/esm/models/index.mjs +3 -3
- package/esm/models/index.mjs.map +1 -1
- package/esm/models/refresh-token/index.d.mts +1 -1
- package/esm/models/refresh-token/index.mjs +1 -1
- package/esm/models/refresh-token/migration.mjs +2 -1
- package/esm/models/refresh-token/migration.mjs.map +1 -1
- package/esm/models/refresh-token/refresh-token.model.d.mts +94 -16
- package/esm/models/refresh-token/refresh-token.model.d.mts.map +1 -1
- package/esm/models/refresh-token/refresh-token.model.mjs +152 -13
- package/esm/models/refresh-token/refresh-token.model.mjs.map +1 -1
- package/esm/services/auth-config.mjs +63 -0
- package/esm/services/auth-config.mjs.map +1 -0
- package/esm/services/auth-events.d.mts +2 -2
- package/esm/services/auth-events.d.mts.map +1 -1
- package/esm/services/auth-events.mjs +7 -2
- package/esm/services/auth-events.mjs.map +1 -1
- package/esm/services/auth.service.d.mts +48 -30
- package/esm/services/auth.service.d.mts.map +1 -1
- package/esm/services/auth.service.mjs +88 -102
- package/esm/services/auth.service.mjs.map +1 -1
- package/esm/services/generate-jwt-secret.d.mts +1 -1
- package/esm/services/generate-jwt-secret.d.mts.map +1 -1
- package/esm/services/generate-jwt-secret.mjs +4 -4
- package/esm/services/generate-jwt-secret.mjs.map +1 -1
- package/esm/services/index.d.mts +1 -1
- package/esm/services/jwt.d.mts +13 -4
- package/esm/services/jwt.d.mts.map +1 -1
- package/esm/services/jwt.mjs +30 -11
- package/esm/services/jwt.mjs.map +1 -1
- package/esm/utils/auth-error-codes.d.mts +9 -2
- package/esm/utils/auth-error-codes.d.mts.map +1 -1
- package/esm/utils/auth-error-codes.mjs +7 -1
- package/esm/utils/auth-error-codes.mjs.map +1 -1
- package/llms-full.txt +269 -20
- package/llms.txt +2 -0
- package/package.json +9 -11
- package/skills/auth-basics/SKILL.md +8 -7
- package/skills/customize-token-storage/SKILL.md +112 -0
- package/skills/customize-user-type/SKILL.md +7 -2
- package/skills/handle-login-and-logout/SKILL.md +1 -1
- package/skills/manage-tokens/SKILL.md +9 -9
- package/skills/overview/SKILL.md +7 -1
- package/skills/protect-routes/SKILL.md +1 -0
- package/skills/run-auth-commands/SKILL.md +1 -1
- package/skills/throttle-login-attempts/SKILL.md +116 -0
- package/cjs/index.cjs +0 -807
- package/cjs/index.cjs.map +0 -1
package/esm/models/index.mjs
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { AccessToken } from "./access-token/access-token.model.mjs";
|
|
1
|
+
import { AccessToken, accessTokenSchema } from "./access-token/access-token.model.mjs";
|
|
2
2
|
import "./access-token/index.mjs";
|
|
3
|
-
import { RefreshToken } from "./refresh-token/refresh-token.model.mjs";
|
|
3
|
+
import { RefreshToken, refreshTokenSchema } from "./refresh-token/refresh-token.model.mjs";
|
|
4
4
|
import "./refresh-token/index.mjs";
|
|
5
5
|
import { AccessTokenMigration } from "./access-token/migration.mjs";
|
|
6
6
|
import { RefreshTokenMigration } from "./refresh-token/migration.mjs";
|
|
7
7
|
import { Auth } from "./auth.model.mjs";
|
|
8
8
|
|
|
9
|
-
//#region
|
|
9
|
+
//#region ../@warlock.js/auth/src/models/index.ts
|
|
10
10
|
const authMigrations = [AccessTokenMigration, RefreshTokenMigration];
|
|
11
11
|
|
|
12
12
|
//#endregion
|
package/esm/models/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../../../../../../@warlock.js/auth/src/models/index.ts"],"sourcesContent":["import { AccessTokenMigration } from \"./access-token/migration\";\r\nimport { RefreshTokenMigration } from \"./refresh-token/migration\";\r\n\r\nexport * from \"./access-token\";\r\nexport * from \"./auth.model\";\r\nexport * from \"./refresh-token\";\r\n\r\nexport const authMigrations = [AccessTokenMigration, RefreshTokenMigration];\r\n"],"mappings":";;;;;;;;;AAOA,MAAa,iBAAiB,CAAC,sBAAsB,qBAAqB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import { RefreshToken } from "./refresh-token.model.mjs";
|
|
1
|
+
import { RefreshToken, RefreshTokenIssueOptions, refreshTokenSchema } from "./refresh-token.model.mjs";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { RefreshToken } from "./refresh-token.model.mjs";
|
|
2
2
|
import { migrate } from "@warlock.js/cascade";
|
|
3
3
|
|
|
4
|
-
//#region
|
|
4
|
+
//#region ../@warlock.js/auth/src/models/refresh-token/migration.ts
|
|
5
5
|
const RefreshTokenMigration = migrate(RefreshToken, {
|
|
6
6
|
name: "refreshToken",
|
|
7
7
|
up() {
|
|
@@ -15,6 +15,7 @@ const RefreshTokenMigration = migrate(RefreshToken, {
|
|
|
15
15
|
this.timestamp("last_used_at").nullable();
|
|
16
16
|
this.timestamp("revoked_at").nullable();
|
|
17
17
|
this.json("device_info").nullable();
|
|
18
|
+
this.index(["user_id", "user_type"]);
|
|
18
19
|
this.timestamps();
|
|
19
20
|
},
|
|
20
21
|
down() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"migration.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"migration.mjs","names":[],"sources":["../../../../../../../../@warlock.js/auth/src/models/refresh-token/migration.ts"],"sourcesContent":["import { migrate } from \"@warlock.js/cascade\";\nimport { RefreshToken } from \"./refresh-token.model\";\n\nexport const RefreshTokenMigration = migrate(RefreshToken, {\n name: \"refreshToken\",\n up() {\n // Create table\n this.createTableIfNotExists();\n\n // Primary key\n this.primaryUuid();\n\n // Token fields\n this.text(\"token\").unique();\n this.uuid(\"user_id\").index();\n this.string(\"user_type\", 50).nullable();\n this.text(\"family_id\").index().nullable();\n this.timestamp(\"expires_at\").index().nullable();\n this.timestamp(\"last_used_at\").nullable();\n this.timestamp(\"revoked_at\").nullable();\n this.json(\"device_info\").nullable();\n\n // Composite index for the per-user active-session lookups\n // (revokeAllFor / enforceMax / activeFor all filter on user_id + user_type).\n this.index([\"user_id\", \"user_type\"]);\n\n // Timestamps\n this.timestamps();\n },\n down() {\n this.dropTableIfExists();\n },\n});\n"],"mappings":";;;;AAGA,MAAa,wBAAwB,QAAQ,cAAc;CACzD,MAAM;CACN,KAAK;EAEH,KAAK,uBAAuB;EAG5B,KAAK,YAAY;EAGjB,KAAK,KAAK,OAAO,CAAC,CAAC,OAAO;EAC1B,KAAK,KAAK,SAAS,CAAC,CAAC,MAAM;EAC3B,KAAK,OAAO,aAAa,EAAE,CAAC,CAAC,SAAS;EACtC,KAAK,KAAK,WAAW,CAAC,CAAC,MAAM,CAAC,CAAC,SAAS;EACxC,KAAK,UAAU,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC,SAAS;EAC9C,KAAK,UAAU,cAAc,CAAC,CAAC,SAAS;EACxC,KAAK,UAAU,YAAY,CAAC,CAAC,SAAS;EACtC,KAAK,KAAK,aAAa,CAAC,CAAC,SAAS;EAIlC,KAAK,MAAM,CAAC,WAAW,WAAW,CAAC;EAGnC,KAAK,WAAW;CAClB;CACA,OAAO;EACL,KAAK,kBAAkB;CACzB;AACF,CAAC"}
|
|
@@ -1,36 +1,114 @@
|
|
|
1
|
+
import { Auth } from "../auth.model.mjs";
|
|
2
|
+
import { DeviceInfo } from "../../contracts/types.mjs";
|
|
1
3
|
import { Model } from "@warlock.js/cascade";
|
|
2
4
|
|
|
3
|
-
//#region
|
|
5
|
+
//#region ../@warlock.js/auth/src/models/refresh-token/refresh-token.model.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* Seal schema for the persisted refresh-token record. Exported so an override
|
|
8
|
+
* can spread it and add columns (e.g. a tenant key) without re-declaring the
|
|
9
|
+
* base shape:
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* export class AppRefreshToken extends RefreshToken {
|
|
13
|
+
* public static schema = refreshTokenSchema.extend({
|
|
14
|
+
* organization_id: v.string().required(),
|
|
15
|
+
* });
|
|
16
|
+
* }
|
|
17
|
+
*/
|
|
18
|
+
declare const refreshTokenSchema: any;
|
|
19
|
+
/**
|
|
20
|
+
* Extra attributes captured when a refresh token is issued.
|
|
21
|
+
*/
|
|
22
|
+
type RefreshTokenIssueOptions = {
|
|
23
|
+
familyId: string;
|
|
24
|
+
expiresAt: string;
|
|
25
|
+
deviceInfo?: DeviceInfo;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Persisted refresh-token record + the data layer for refresh tokens.
|
|
29
|
+
*
|
|
30
|
+
* **Role.** Owns every refresh-token read, write, and lifecycle transition.
|
|
31
|
+
* The auth service drives refresh-token state exclusively through this model's
|
|
32
|
+
* named statics and instance methods, so it never hard-codes a column name —
|
|
33
|
+
* which is what makes a snake/camel mismatch (the historical `userId` bug)
|
|
34
|
+
* structurally impossible and lets an override rename or add columns by
|
|
35
|
+
* extending this class and registering it under `config.auth.refreshToken.model`.
|
|
36
|
+
*/
|
|
4
37
|
declare class RefreshToken extends Model {
|
|
38
|
+
static table: string;
|
|
39
|
+
static schema: any;
|
|
40
|
+
/** Token family this row belongs to (rotation / replay grouping). */
|
|
41
|
+
get familyId(): string;
|
|
42
|
+
/** Whether the token's `expires_at` is in the past. */
|
|
43
|
+
get isExpired(): boolean;
|
|
44
|
+
/** Whether the token has been revoked. */
|
|
45
|
+
get isRevoked(): boolean;
|
|
46
|
+
/** Whether the token is still usable (not expired and not revoked). */
|
|
47
|
+
get isValid(): boolean;
|
|
5
48
|
/**
|
|
6
|
-
*
|
|
49
|
+
* Unconditionally stamp `revoked_at` on this token.
|
|
7
50
|
*/
|
|
8
|
-
|
|
51
|
+
revoke(): Promise<this>;
|
|
9
52
|
/**
|
|
10
|
-
*
|
|
53
|
+
* Atomically revoke this token ONLY if it is still active. Resolves to `true`
|
|
54
|
+
* when this call performed the revoke, `false` when a concurrent request had
|
|
55
|
+
* already revoked it — the win/lose signal that powers rotation replay
|
|
56
|
+
* detection. Uses a conditional UPDATE so two concurrent rotations of the
|
|
57
|
+
* same token can never both succeed.
|
|
11
58
|
*/
|
|
12
|
-
|
|
59
|
+
revokeIfActive(): Promise<boolean>;
|
|
13
60
|
/**
|
|
14
|
-
*
|
|
61
|
+
* Touch `last_used_at` without revoking — the non-rotating refresh path.
|
|
15
62
|
*/
|
|
16
|
-
|
|
63
|
+
markAsUsed(): Promise<void>;
|
|
17
64
|
/**
|
|
18
|
-
*
|
|
65
|
+
* Persist a freshly-signed refresh token for the user.
|
|
19
66
|
*/
|
|
20
|
-
|
|
67
|
+
static issue(user: Auth, token: string, options: RefreshTokenIssueOptions): any;
|
|
21
68
|
/**
|
|
22
|
-
*
|
|
69
|
+
* Find a refresh-token row by its raw token string.
|
|
23
70
|
*/
|
|
24
|
-
|
|
71
|
+
static findByToken(token: string): Promise<RefreshToken | null>;
|
|
25
72
|
/**
|
|
26
|
-
*
|
|
73
|
+
* Find a refresh token scoped to a user — used by logout so a caller can only
|
|
74
|
+
* revoke a token that actually belongs to them.
|
|
27
75
|
*/
|
|
28
|
-
|
|
76
|
+
static findForUser(user: Auth, token: string): Promise<RefreshToken | null>;
|
|
29
77
|
/**
|
|
30
|
-
*
|
|
78
|
+
* Delete a specific refresh token belonging to the user.
|
|
31
79
|
*/
|
|
32
|
-
|
|
80
|
+
static deleteForUser(user: Auth, token: string): any;
|
|
81
|
+
/**
|
|
82
|
+
* Active, unexpired sessions for the user, newest first.
|
|
83
|
+
*/
|
|
84
|
+
static activeFor(user: Auth): Promise<RefreshToken[]>;
|
|
85
|
+
/**
|
|
86
|
+
* Revoke every still-active refresh token for the user, returning the rows
|
|
87
|
+
* that were revoked so the caller can emit a per-token event for each.
|
|
88
|
+
*
|
|
89
|
+
* Rows are fetched BEFORE they are revoked: a bulk `findAndUpdate` keyed on
|
|
90
|
+
* `revoked_at: null` would re-query the same predicate after the update and
|
|
91
|
+
* match nothing, returning an empty set.
|
|
92
|
+
*/
|
|
93
|
+
static revokeAllFor(user: Auth): Promise<RefreshToken[]>;
|
|
94
|
+
/**
|
|
95
|
+
* Revoke every still-active token in a family (rotation breach containment),
|
|
96
|
+
* returning the revoked rows (fetched before revocation — see `revokeAllFor`).
|
|
97
|
+
*/
|
|
98
|
+
static revokeFamily(familyId: string): Promise<RefreshToken[]>;
|
|
99
|
+
/**
|
|
100
|
+
* Revoke the oldest active tokens so at most `max - 1` remain — making room
|
|
101
|
+
* for the about-to-be-issued one. Bounded by `max`, so the per-row loop is
|
|
102
|
+
* small.
|
|
103
|
+
*/
|
|
104
|
+
static enforceMax(user: Auth, max: number): Promise<void>;
|
|
105
|
+
/**
|
|
106
|
+
* Hard-delete every expired refresh token, returning the deleted rows so the
|
|
107
|
+
* caller can emit a per-token event. Runs from the `auth.cleanup` CLI command
|
|
108
|
+
* (a cold batch path).
|
|
109
|
+
*/
|
|
110
|
+
static purgeExpired(): Promise<RefreshToken[]>;
|
|
33
111
|
}
|
|
34
112
|
//#endregion
|
|
35
|
-
export { RefreshToken };
|
|
113
|
+
export { RefreshToken, RefreshTokenIssueOptions, refreshTokenSchema };
|
|
36
114
|
//# sourceMappingURL=refresh-token.model.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"refresh-token.model.d.mts","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"refresh-token.model.d.mts","names":[],"sources":["../../../../../../../../@warlock.js/auth/src/models/refresh-token/refresh-token.model.ts"],"mappings":";;;;;;;AAiBA;;;;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;EAuHyB;EAAA,IApH5B,QAAA;EAoHmC;EAAA,IA/GnC,SAAA;EA+HyC;EAAA,IAtHzC,SAAA;EAqI8C;EAAA,IAhI9C,OAAA;EAoJyB;;;EA7IvB,MAAA,IAAU,OAAA;EAhCS;;;;;;;EA2CnB,cAAA,IAAkB,OAAA;EAXR;;;EAyBV,UAAA,IAAc,OAAA;EAAA;;;EAAA,OAOb,KAAA,CAAM,IAAA,EAAM,IAAA,EAAM,KAAA,UAAe,OAAA,EAAS,wBAAA;EAAxB;;;EAAA,OAoBlB,WAAA,CAAY,KAAA,WAAgB,OAAA,CAAQ,YAAA;EAAxB;;;;EAAA,OAQZ,WAAA,CAAY,IAAA,EAAM,IAAA,EAAM,KAAA,WAAgB,OAAA,CAAQ,YAAA;EAApC;;;EAAA,OAOZ,aAAA,CAAc,IAAA,EAAM,IAAA,EAAM,KAAA;EAA1B;;;EAAA,OAOA,SAAA,CAAU,IAAA,EAAM,IAAA,GAAO,OAAA,CAAQ,YAAA;EAA/B;;;;;;;;EAAA,OAgBM,YAAA,CAAa,IAAA,EAAM,IAAA,GAAO,OAAA,CAAQ,YAAA;EAAA;;;;EAAA,OAgBlC,YAAA,CAAa,QAAA,WAAmB,OAAA,CAAQ,YAAA;EAexC;;;;;EAAA,OAAA,UAAA,CAAW,IAAA,EAAM,IAAA,EAAM,GAAA,WAAc,OAAA;EAoBrB;;;AAAoB;;EAApB,OAAhB,YAAA,IAAgB,OAAA,CAAQ,YAAA;AAAA"}
|
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
import { Model } from "@warlock.js/cascade";
|
|
2
2
|
import { v } from "@warlock.js/seal";
|
|
3
3
|
|
|
4
|
-
//#region
|
|
4
|
+
//#region ../@warlock.js/auth/src/models/refresh-token/refresh-token.model.ts
|
|
5
|
+
/**
|
|
6
|
+
* Seal schema for the persisted refresh-token record. Exported so an override
|
|
7
|
+
* can spread it and add columns (e.g. a tenant key) without re-declaring the
|
|
8
|
+
* base shape:
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* export class AppRefreshToken extends RefreshToken {
|
|
12
|
+
* public static schema = refreshTokenSchema.extend({
|
|
13
|
+
* organization_id: v.string().required(),
|
|
14
|
+
* });
|
|
15
|
+
* }
|
|
16
|
+
*/
|
|
5
17
|
const refreshTokenSchema = v.object({
|
|
6
18
|
token: v.string().required(),
|
|
7
19
|
user_id: v.scalar().required(),
|
|
@@ -12,6 +24,16 @@ const refreshTokenSchema = v.object({
|
|
|
12
24
|
revoked_at: v.date().optional(),
|
|
13
25
|
device_info: v.record(v.any()).optional()
|
|
14
26
|
});
|
|
27
|
+
/**
|
|
28
|
+
* Persisted refresh-token record + the data layer for refresh tokens.
|
|
29
|
+
*
|
|
30
|
+
* **Role.** Owns every refresh-token read, write, and lifecycle transition.
|
|
31
|
+
* The auth service drives refresh-token state exclusively through this model's
|
|
32
|
+
* named statics and instance methods, so it never hard-codes a column name —
|
|
33
|
+
* which is what makes a snake/camel mismatch (the historical `userId` bug)
|
|
34
|
+
* structurally impossible and lets an override rename or add columns by
|
|
35
|
+
* extending this class and registering it under `config.auth.refreshToken.model`.
|
|
36
|
+
*/
|
|
15
37
|
var RefreshToken = class extends Model {
|
|
16
38
|
static {
|
|
17
39
|
this.table = "refresh_tokens";
|
|
@@ -19,40 +41,157 @@ var RefreshToken = class extends Model {
|
|
|
19
41
|
static {
|
|
20
42
|
this.schema = refreshTokenSchema;
|
|
21
43
|
}
|
|
22
|
-
/**
|
|
23
|
-
|
|
24
|
-
|
|
44
|
+
/** Token family this row belongs to (rotation / replay grouping). */
|
|
45
|
+
get familyId() {
|
|
46
|
+
return this.get("family_id");
|
|
47
|
+
}
|
|
48
|
+
/** Whether the token's `expires_at` is in the past. */
|
|
25
49
|
get isExpired() {
|
|
26
50
|
const expiresAt = this.get("expires_at");
|
|
27
51
|
if (!expiresAt) return false;
|
|
28
52
|
return /* @__PURE__ */ new Date() > new Date(expiresAt);
|
|
29
53
|
}
|
|
30
|
-
/**
|
|
31
|
-
* Check if token is revoked
|
|
32
|
-
*/
|
|
54
|
+
/** Whether the token has been revoked. */
|
|
33
55
|
get isRevoked() {
|
|
34
56
|
return !!this.get("revoked_at");
|
|
35
57
|
}
|
|
36
|
-
/**
|
|
37
|
-
* Check if token is valid (not expired and not revoked)
|
|
38
|
-
*/
|
|
58
|
+
/** Whether the token is still usable (not expired and not revoked). */
|
|
39
59
|
get isValid() {
|
|
40
60
|
return !this.isExpired && !this.isRevoked;
|
|
41
61
|
}
|
|
42
62
|
/**
|
|
43
|
-
*
|
|
63
|
+
* Unconditionally stamp `revoked_at` on this token.
|
|
44
64
|
*/
|
|
45
65
|
async revoke() {
|
|
46
66
|
return this.merge({ revoked_at: /* @__PURE__ */ new Date() }).save();
|
|
47
67
|
}
|
|
48
68
|
/**
|
|
49
|
-
*
|
|
69
|
+
* Atomically revoke this token ONLY if it is still active. Resolves to `true`
|
|
70
|
+
* when this call performed the revoke, `false` when a concurrent request had
|
|
71
|
+
* already revoked it — the win/lose signal that powers rotation replay
|
|
72
|
+
* detection. Uses a conditional UPDATE so two concurrent rotations of the
|
|
73
|
+
* same token can never both succeed.
|
|
74
|
+
*/
|
|
75
|
+
async revokeIfActive() {
|
|
76
|
+
return await this.constructor.atomic({
|
|
77
|
+
id: this.id,
|
|
78
|
+
revoked_at: null
|
|
79
|
+
}, { $set: { revoked_at: /* @__PURE__ */ new Date() } }) > 0;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Touch `last_used_at` without revoking — the non-rotating refresh path.
|
|
50
83
|
*/
|
|
51
84
|
async markAsUsed() {
|
|
52
85
|
await this.merge({ last_used_at: /* @__PURE__ */ new Date() }).save();
|
|
53
86
|
}
|
|
87
|
+
/**
|
|
88
|
+
* Persist a freshly-signed refresh token for the user.
|
|
89
|
+
*/
|
|
90
|
+
static issue(user, token, options) {
|
|
91
|
+
return this.create({
|
|
92
|
+
token,
|
|
93
|
+
user_id: user.id,
|
|
94
|
+
user_type: user.userType,
|
|
95
|
+
family_id: options.familyId,
|
|
96
|
+
expires_at: options.expiresAt,
|
|
97
|
+
device_info: options.deviceInfo ? {
|
|
98
|
+
userAgent: options.deviceInfo.userAgent,
|
|
99
|
+
ip: options.deviceInfo.ip,
|
|
100
|
+
deviceId: options.deviceInfo.deviceId
|
|
101
|
+
} : void 0
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Find a refresh-token row by its raw token string.
|
|
106
|
+
*/
|
|
107
|
+
static findByToken(token) {
|
|
108
|
+
return this.first({ token });
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Find a refresh token scoped to a user — used by logout so a caller can only
|
|
112
|
+
* revoke a token that actually belongs to them.
|
|
113
|
+
*/
|
|
114
|
+
static findForUser(user, token) {
|
|
115
|
+
return this.first({
|
|
116
|
+
token,
|
|
117
|
+
user_id: user.id
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Delete a specific refresh token belonging to the user.
|
|
122
|
+
*/
|
|
123
|
+
static deleteForUser(user, token) {
|
|
124
|
+
return this.delete({
|
|
125
|
+
token,
|
|
126
|
+
user_id: user.id
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Active, unexpired sessions for the user, newest first.
|
|
131
|
+
*/
|
|
132
|
+
static activeFor(user) {
|
|
133
|
+
return this.query().where({
|
|
134
|
+
user_id: user.id,
|
|
135
|
+
user_type: user.userType,
|
|
136
|
+
revoked_at: null
|
|
137
|
+
}).where("expires_at", ">", /* @__PURE__ */ new Date()).orderBy("created_at", "desc").get();
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Revoke every still-active refresh token for the user, returning the rows
|
|
141
|
+
* that were revoked so the caller can emit a per-token event for each.
|
|
142
|
+
*
|
|
143
|
+
* Rows are fetched BEFORE they are revoked: a bulk `findAndUpdate` keyed on
|
|
144
|
+
* `revoked_at: null` would re-query the same predicate after the update and
|
|
145
|
+
* match nothing, returning an empty set.
|
|
146
|
+
*/
|
|
147
|
+
static async revokeAllFor(user) {
|
|
148
|
+
const tokens = await this.query().where({
|
|
149
|
+
user_id: user.id,
|
|
150
|
+
user_type: user.userType,
|
|
151
|
+
revoked_at: null
|
|
152
|
+
}).get();
|
|
153
|
+
for (const token of tokens) await token.revoke();
|
|
154
|
+
return tokens;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Revoke every still-active token in a family (rotation breach containment),
|
|
158
|
+
* returning the revoked rows (fetched before revocation — see `revokeAllFor`).
|
|
159
|
+
*/
|
|
160
|
+
static async revokeFamily(familyId) {
|
|
161
|
+
const tokens = await this.query().where({
|
|
162
|
+
family_id: familyId,
|
|
163
|
+
revoked_at: null
|
|
164
|
+
}).get();
|
|
165
|
+
for (const token of tokens) await token.revoke();
|
|
166
|
+
return tokens;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Revoke the oldest active tokens so at most `max - 1` remain — making room
|
|
170
|
+
* for the about-to-be-issued one. Bounded by `max`, so the per-row loop is
|
|
171
|
+
* small.
|
|
172
|
+
*/
|
|
173
|
+
static async enforceMax(user, max) {
|
|
174
|
+
const activeTokens = await this.query().where({
|
|
175
|
+
user_id: user.id,
|
|
176
|
+
user_type: user.userType,
|
|
177
|
+
revoked_at: null
|
|
178
|
+
}).orderBy("created_at", "asc").get();
|
|
179
|
+
if (activeTokens.length < max) return;
|
|
180
|
+
const tokensToRevoke = activeTokens.slice(0, activeTokens.length - max + 1);
|
|
181
|
+
for (const token of tokensToRevoke) await token.revoke();
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Hard-delete every expired refresh token, returning the deleted rows so the
|
|
185
|
+
* caller can emit a per-token event. Runs from the `auth.cleanup` CLI command
|
|
186
|
+
* (a cold batch path).
|
|
187
|
+
*/
|
|
188
|
+
static async purgeExpired() {
|
|
189
|
+
const expiredTokens = await this.query().where("expires_at", "<", /* @__PURE__ */ new Date()).get();
|
|
190
|
+
for (const token of expiredTokens) await token.destroy();
|
|
191
|
+
return expiredTokens;
|
|
192
|
+
}
|
|
54
193
|
};
|
|
55
194
|
|
|
56
195
|
//#endregion
|
|
57
|
-
export { RefreshToken };
|
|
196
|
+
export { RefreshToken, refreshTokenSchema };
|
|
58
197
|
//# sourceMappingURL=refresh-token.model.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"refresh-token.model.mjs","names":[],"sources":["../../../../../../../@warlock.js/auth/src/models/refresh-token/refresh-token.model.ts"],"sourcesContent":["import { Model } from \"@warlock.js/cascade\";\nimport { v } from \"@warlock.js/seal\";\n\nconst refreshTokenSchema = v.object({\n token: v.string().required(),\n user_id: v.scalar().required(),\n user_type: v.string().required(),\n family_id: v.string().required(),\n expires_at: v.date().required(),\n last_used_at: v.date().default(() => new Date()),\n revoked_at: v.date().optional(),\n device_info: v.record(v.any()).optional(),\n});\n\nexport class RefreshToken extends Model {\n /**\n * {@inheritDoc}\n */\n public static table = \"refresh_tokens\";\n\n /**\n * {@inheritDoc}\n */\n public static schema = refreshTokenSchema;\n\n /**\n * Check if token is expired\n */\n public get isExpired(): boolean {\n const expiresAt = this.get(\"expires_at\");\n if (!expiresAt) return false;\n return new Date() > new Date(expiresAt);\n }\n\n /**\n * Check if token is revoked\n */\n public get isRevoked(): boolean {\n return !!this.get(\"revoked_at\");\n }\n\n /**\n * Check if token is valid (not expired and not revoked)\n */\n public get isValid(): boolean {\n return !this.isExpired && !this.isRevoked;\n }\n\n /**\n * Revoke this token\n */\n public async revoke(): Promise<this> {\n return this.merge({ revoked_at: new Date() }).save();\n }\n\n /**\n * Mark token as used (update last_used_at)\n */\n public async markAsUsed(): Promise<void> {\n await this.merge({ last_used_at: new Date() }).save();\n }\n}\n"],"mappings":";;;;AAGA,MAAM,qBAAqB,EAAE,OAAO;CAClC,OAAO,EAAE,OAAO,EAAE,SAAS;CAC3B,SAAS,EAAE,OAAO,EAAE,SAAS;CAC7B,WAAW,EAAE,OAAO,EAAE,SAAS;CAC/B,WAAW,EAAE,OAAO,EAAE,SAAS;CAC/B,YAAY,EAAE,KAAK,EAAE,SAAS;CAC9B,cAAc,EAAE,KAAK,EAAE,8BAAc,IAAI,KAAK,CAAC;CAC/C,YAAY,EAAE,KAAK,EAAE,SAAS;CAC9B,aAAa,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,SAAS;AAC1C,CAAC;AAED,IAAa,eAAb,cAAkC,MAAM;;eAIhB;;;gBAKC;;;;;CAKvB,IAAW,YAAqB;EAC9B,MAAM,YAAY,KAAK,IAAI,YAAY;EACvC,IAAI,CAAC,WAAW,OAAO;EACvB,uBAAO,IAAI,KAAK,IAAI,IAAI,KAAK,SAAS;CACxC;;;;CAKA,IAAW,YAAqB;EAC9B,OAAO,CAAC,CAAC,KAAK,IAAI,YAAY;CAChC;;;;CAKA,IAAW,UAAmB;EAC5B,OAAO,CAAC,KAAK,aAAa,CAAC,KAAK;CAClC;;;;CAKA,MAAa,SAAwB;EACnC,OAAO,KAAK,MAAM,EAAE,4BAAY,IAAI,KAAK,EAAE,CAAC,EAAE,KAAK;CACrD;;;;CAKA,MAAa,aAA4B;EACvC,MAAM,KAAK,MAAM,EAAE,8BAAc,IAAI,KAAK,EAAE,CAAC,EAAE,KAAK;CACtD;AACF"}
|
|
1
|
+
{"version":3,"file":"refresh-token.model.mjs","names":[],"sources":["../../../../../../../../@warlock.js/auth/src/models/refresh-token/refresh-token.model.ts"],"sourcesContent":["import { Model } from \"@warlock.js/cascade\";\nimport { v } from \"@warlock.js/seal\";\nimport type { DeviceInfo } from \"../../contracts/types\";\nimport type { Auth } from \"../auth.model\";\n\n/**\n * Seal schema for the persisted refresh-token record. Exported so an override\n * can spread it and add columns (e.g. a tenant key) without re-declaring the\n * base shape:\n *\n * @example\n * export class AppRefreshToken extends RefreshToken {\n * public static schema = refreshTokenSchema.extend({\n * organization_id: v.string().required(),\n * });\n * }\n */\nexport const refreshTokenSchema = v.object({\n token: v.string().required(),\n user_id: v.scalar().required(),\n user_type: v.string().required(),\n family_id: v.string().required(),\n expires_at: v.date().required(),\n last_used_at: v.date().default(() => new Date()),\n revoked_at: v.date().optional(),\n device_info: v.record(v.any()).optional(),\n});\n\n/**\n * Extra attributes captured when a refresh token is issued.\n */\nexport type RefreshTokenIssueOptions = {\n familyId: string;\n expiresAt: string;\n deviceInfo?: DeviceInfo;\n};\n\n/**\n * Persisted refresh-token record + the data layer for refresh tokens.\n *\n * **Role.** Owns every refresh-token read, write, and lifecycle transition.\n * The auth service drives refresh-token state exclusively through this model's\n * named statics and instance methods, so it never hard-codes a column name —\n * which is what makes a snake/camel mismatch (the historical `userId` bug)\n * structurally impossible and lets an override rename or add columns by\n * extending this class and registering it under `config.auth.refreshToken.model`.\n */\nexport class RefreshToken extends Model {\n public static table = \"refresh_tokens\";\n\n public static schema = refreshTokenSchema;\n\n /** Token family this row belongs to (rotation / replay grouping). */\n public get familyId(): string {\n return this.get(\"family_id\");\n }\n\n /** Whether the token's `expires_at` is in the past. */\n public get isExpired(): boolean {\n const expiresAt = this.get(\"expires_at\");\n\n if (!expiresAt) return false;\n\n return new Date() > new Date(expiresAt);\n }\n\n /** Whether the token has been revoked. */\n public get isRevoked(): boolean {\n return !!this.get(\"revoked_at\");\n }\n\n /** Whether the token is still usable (not expired and not revoked). */\n public get isValid(): boolean {\n return !this.isExpired && !this.isRevoked;\n }\n\n /**\n * Unconditionally stamp `revoked_at` on this token.\n */\n public async revoke(): Promise<this> {\n return this.merge({ revoked_at: new Date() }).save();\n }\n\n /**\n * Atomically revoke this token ONLY if it is still active. Resolves to `true`\n * when this call performed the revoke, `false` when a concurrent request had\n * already revoked it — the win/lose signal that powers rotation replay\n * detection. Uses a conditional UPDATE so two concurrent rotations of the\n * same token can never both succeed.\n */\n public async revokeIfActive(): Promise<boolean> {\n const modelClass = this.constructor as typeof RefreshToken;\n\n const revokedCount = await modelClass.atomic(\n { id: this.id, revoked_at: null },\n { $set: { revoked_at: new Date() } },\n );\n\n return revokedCount > 0;\n }\n\n /**\n * Touch `last_used_at` without revoking — the non-rotating refresh path.\n */\n public async markAsUsed(): Promise<void> {\n await this.merge({ last_used_at: new Date() }).save();\n }\n\n /**\n * Persist a freshly-signed refresh token for the user.\n */\n public static issue(user: Auth, token: string, options: RefreshTokenIssueOptions) {\n return this.create({\n token,\n user_id: user.id,\n user_type: user.userType,\n family_id: options.familyId,\n expires_at: options.expiresAt,\n device_info: options.deviceInfo\n ? {\n userAgent: options.deviceInfo.userAgent,\n ip: options.deviceInfo.ip,\n deviceId: options.deviceInfo.deviceId,\n }\n : undefined,\n });\n }\n\n /**\n * Find a refresh-token row by its raw token string.\n */\n public static findByToken(token: string): Promise<RefreshToken | null> {\n return this.first({ token });\n }\n\n /**\n * Find a refresh token scoped to a user — used by logout so a caller can only\n * revoke a token that actually belongs to them.\n */\n public static findForUser(user: Auth, token: string): Promise<RefreshToken | null> {\n return this.first({ token, user_id: user.id });\n }\n\n /**\n * Delete a specific refresh token belonging to the user.\n */\n public static deleteForUser(user: Auth, token: string) {\n return this.delete({ token, user_id: user.id });\n }\n\n /**\n * Active, unexpired sessions for the user, newest first.\n */\n public static activeFor(user: Auth): Promise<RefreshToken[]> {\n return this.query()\n .where({ user_id: user.id, user_type: user.userType, revoked_at: null })\n .where(\"expires_at\", \">\", new Date())\n .orderBy(\"created_at\", \"desc\")\n .get();\n }\n\n /**\n * Revoke every still-active refresh token for the user, returning the rows\n * that were revoked so the caller can emit a per-token event for each.\n *\n * Rows are fetched BEFORE they are revoked: a bulk `findAndUpdate` keyed on\n * `revoked_at: null` would re-query the same predicate after the update and\n * match nothing, returning an empty set.\n */\n public static async revokeAllFor(user: Auth): Promise<RefreshToken[]> {\n const tokens = await this.query()\n .where({ user_id: user.id, user_type: user.userType, revoked_at: null })\n .get();\n\n for (const token of tokens) {\n await token.revoke();\n }\n\n return tokens;\n }\n\n /**\n * Revoke every still-active token in a family (rotation breach containment),\n * returning the revoked rows (fetched before revocation — see `revokeAllFor`).\n */\n public static async revokeFamily(familyId: string): Promise<RefreshToken[]> {\n const tokens = await this.query().where({ family_id: familyId, revoked_at: null }).get();\n\n for (const token of tokens) {\n await token.revoke();\n }\n\n return tokens;\n }\n\n /**\n * Revoke the oldest active tokens so at most `max - 1` remain — making room\n * for the about-to-be-issued one. Bounded by `max`, so the per-row loop is\n * small.\n */\n public static async enforceMax(user: Auth, max: number): Promise<void> {\n const activeTokens = await this.query()\n .where({ user_id: user.id, user_type: user.userType, revoked_at: null })\n .orderBy(\"created_at\", \"asc\")\n .get();\n\n if (activeTokens.length < max) return;\n\n const tokensToRevoke = activeTokens.slice(0, activeTokens.length - max + 1);\n\n for (const token of tokensToRevoke) {\n await token.revoke();\n }\n }\n\n /**\n * Hard-delete every expired refresh token, returning the deleted rows so the\n * caller can emit a per-token event. Runs from the `auth.cleanup` CLI command\n * (a cold batch path).\n */\n public static async purgeExpired(): Promise<RefreshToken[]> {\n const expiredTokens = await this.query().where(\"expires_at\", \"<\", new Date()).get();\n\n for (const token of expiredTokens) {\n await token.destroy();\n }\n\n return expiredTokens;\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { config } from "@warlock.js/core";
|
|
2
|
+
import { log } from "@warlock.js/logger";
|
|
3
|
+
|
|
4
|
+
//#region ../@warlock.js/auth/src/services/auth-config.ts
|
|
5
|
+
const warnedLegacyKeys = /* @__PURE__ */ new Set();
|
|
6
|
+
/**
|
|
7
|
+
* Resolve an auth setting, preferring the new `auth.accessToken.*` /
|
|
8
|
+
* `auth.refreshToken.*` key and falling back to the deprecated `auth.jwt.*`
|
|
9
|
+
* shape — warning once per legacy key. Returns `fallback` when neither is set.
|
|
10
|
+
*
|
|
11
|
+
* This is the backward-compatible shim that lets existing `auth.jwt.*` configs
|
|
12
|
+
* keep working after the config split.
|
|
13
|
+
*/
|
|
14
|
+
function resolve(newKey, legacyKey, fallback) {
|
|
15
|
+
const fromNew = config.key(`auth.${newKey}`);
|
|
16
|
+
if (fromNew !== void 0 && fromNew !== null) return fromNew;
|
|
17
|
+
const fromLegacy = config.key(`auth.${legacyKey}`);
|
|
18
|
+
if (fromLegacy !== void 0 && fromLegacy !== null) {
|
|
19
|
+
if (!warnedLegacyKeys.has(legacyKey)) {
|
|
20
|
+
warnedLegacyKeys.add(legacyKey);
|
|
21
|
+
log.warn("auth", "config-deprecation", `auth.${legacyKey} is deprecated — use auth.${newKey}`);
|
|
22
|
+
}
|
|
23
|
+
return fromLegacy;
|
|
24
|
+
}
|
|
25
|
+
return fallback;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Typed, backward-compatible access to auth configuration. The service and the
|
|
29
|
+
* jwt signer read configuration exclusively through here, so the new split
|
|
30
|
+
* config and the legacy `auth.jwt.*` shape both resolve the same way.
|
|
31
|
+
*/
|
|
32
|
+
const authConfig = {
|
|
33
|
+
accessToken: {
|
|
34
|
+
/** Signing secret (legacy: `auth.jwt.secret`). Throws if neither is set. */
|
|
35
|
+
secret: () => {
|
|
36
|
+
const secret = resolve("accessToken.secret", "jwt.secret");
|
|
37
|
+
if (!secret) throw new Error("auth: no JWT secret configured — set `auth.accessToken.secret`.");
|
|
38
|
+
return secret;
|
|
39
|
+
},
|
|
40
|
+
/** Signing algorithm (legacy: `auth.jwt.algorithm`). */
|
|
41
|
+
algorithm: () => resolve("accessToken.algorithm", "jwt.algorithm", "HS256"),
|
|
42
|
+
/** Lifetime as an `ms`-string (legacy: `auth.jwt.expiresIn`). */
|
|
43
|
+
expiresIn: () => resolve("accessToken.expiresIn", "jwt.expiresIn")
|
|
44
|
+
},
|
|
45
|
+
refreshToken: {
|
|
46
|
+
/** Separate refresh secret (legacy: `auth.jwt.refresh.secret`); empty ⇒ fall back to the access secret. */
|
|
47
|
+
secret: () => resolve("refreshToken.secret", "jwt.refresh.secret"),
|
|
48
|
+
/** Whether refresh tokens are enabled (legacy: `auth.jwt.refresh.enabled`). */
|
|
49
|
+
enabled: () => resolve("refreshToken.enabled", "jwt.refresh.enabled", true),
|
|
50
|
+
/** Lifetime as an `ms`-string (legacy: `auth.jwt.refresh.expiresIn`). */
|
|
51
|
+
expiresIn: () => resolve("refreshToken.expiresIn", "jwt.refresh.expiresIn", "7d"),
|
|
52
|
+
/** Rotate-on-use (legacy: `auth.jwt.refresh.rotation`). */
|
|
53
|
+
rotation: () => resolve("refreshToken.rotation", "jwt.refresh.rotation", true),
|
|
54
|
+
/** Max active tokens per user (legacy: `auth.jwt.refresh.maxPerUser`). */
|
|
55
|
+
maxPerUser: () => resolve("refreshToken.maxPerUser", "jwt.refresh.maxPerUser", 5),
|
|
56
|
+
/** Logout-without-token behavior (legacy: `auth.jwt.refresh.logoutWithoutToken`). */
|
|
57
|
+
logoutWithoutToken: () => resolve("refreshToken.logoutWithoutToken", "jwt.refresh.logoutWithoutToken", "revoke-all")
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
//#endregion
|
|
62
|
+
export { authConfig };
|
|
63
|
+
//# sourceMappingURL=auth-config.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-config.mjs","names":[],"sources":["../../../../../../../@warlock.js/auth/src/services/auth-config.ts"],"sourcesContent":["import { config } from \"@warlock.js/core\";\nimport { log } from \"@warlock.js/logger\";\nimport { type Algorithm } from \"fast-jwt\";\nimport type { LogoutWithoutTokenBehavior } from \"../contracts/types\";\n\nconst warnedLegacyKeys = new Set<string>();\n\n/**\n * Resolve an auth setting, preferring the new `auth.accessToken.*` /\n * `auth.refreshToken.*` key and falling back to the deprecated `auth.jwt.*`\n * shape — warning once per legacy key. Returns `fallback` when neither is set.\n *\n * This is the backward-compatible shim that lets existing `auth.jwt.*` configs\n * keep working after the config split.\n */\nfunction resolve<T>(newKey: string, legacyKey: string, fallback?: T): T {\n const fromNew = config.key(`auth.${newKey}`);\n\n if (fromNew !== undefined && fromNew !== null) {\n return fromNew as T;\n }\n\n const fromLegacy = config.key(`auth.${legacyKey}`);\n\n if (fromLegacy !== undefined && fromLegacy !== null) {\n if (!warnedLegacyKeys.has(legacyKey)) {\n warnedLegacyKeys.add(legacyKey);\n log.warn(\"auth\", \"config-deprecation\", `auth.${legacyKey} is deprecated — use auth.${newKey}`);\n }\n\n return fromLegacy as T;\n }\n\n return fallback as T;\n}\n\n/**\n * Typed, backward-compatible access to auth configuration. The service and the\n * jwt signer read configuration exclusively through here, so the new split\n * config and the legacy `auth.jwt.*` shape both resolve the same way.\n */\nexport const authConfig = {\n accessToken: {\n /** Signing secret (legacy: `auth.jwt.secret`). Throws if neither is set. */\n secret: (): string => {\n const secret = resolve<string | undefined>(\"accessToken.secret\", \"jwt.secret\");\n\n if (!secret) {\n throw new Error(\"auth: no JWT secret configured — set `auth.accessToken.secret`.\");\n }\n\n return secret;\n },\n /** Signing algorithm (legacy: `auth.jwt.algorithm`). */\n algorithm: (): Algorithm => resolve(\"accessToken.algorithm\", \"jwt.algorithm\", \"HS256\"),\n /** Lifetime as an `ms`-string (legacy: `auth.jwt.expiresIn`). */\n expiresIn: (): string | undefined => resolve(\"accessToken.expiresIn\", \"jwt.expiresIn\"),\n },\n refreshToken: {\n /** Separate refresh secret (legacy: `auth.jwt.refresh.secret`); empty ⇒ fall back to the access secret. */\n secret: (): string | undefined => resolve(\"refreshToken.secret\", \"jwt.refresh.secret\"),\n /** Whether refresh tokens are enabled (legacy: `auth.jwt.refresh.enabled`). */\n enabled: (): boolean => resolve(\"refreshToken.enabled\", \"jwt.refresh.enabled\", true),\n /** Lifetime as an `ms`-string (legacy: `auth.jwt.refresh.expiresIn`). */\n expiresIn: (): string => resolve(\"refreshToken.expiresIn\", \"jwt.refresh.expiresIn\", \"7d\"),\n /** Rotate-on-use (legacy: `auth.jwt.refresh.rotation`). */\n rotation: (): boolean => resolve(\"refreshToken.rotation\", \"jwt.refresh.rotation\", true),\n /** Max active tokens per user (legacy: `auth.jwt.refresh.maxPerUser`). */\n maxPerUser: (): number => resolve(\"refreshToken.maxPerUser\", \"jwt.refresh.maxPerUser\", 5),\n /** Logout-without-token behavior (legacy: `auth.jwt.refresh.logoutWithoutToken`). */\n logoutWithoutToken: (): LogoutWithoutTokenBehavior =>\n resolve(\"refreshToken.logoutWithoutToken\", \"jwt.refresh.logoutWithoutToken\", \"revoke-all\"),\n },\n};\n"],"mappings":";;;;AAKA,MAAM,mCAAmB,IAAI,IAAY;;;;;;;;;AAUzC,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;CACvF;CACA,cAAc;;EAEZ,cAAkC,QAAQ,uBAAuB,oBAAoB;;EAErF,eAAwB,QAAQ,wBAAwB,uBAAuB,IAAI;;EAEnF,iBAAyB,QAAQ,0BAA0B,yBAAyB,IAAI;;EAExF,gBAAyB,QAAQ,yBAAyB,wBAAwB,IAAI;;EAEtF,kBAA0B,QAAQ,2BAA2B,0BAA0B,CAAC;;EAExF,0BACE,QAAQ,mCAAmC,kCAAkC,YAAY;CAC7F;AACF"}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { RefreshToken } from "../models/refresh-token/refresh-token.model.mjs";
|
|
2
1
|
import { Auth } from "../models/auth.model.mjs";
|
|
3
2
|
import { DeviceInfo, TokenPair } from "../contracts/types.mjs";
|
|
3
|
+
import { RefreshToken } from "../models/refresh-token/refresh-token.model.mjs";
|
|
4
4
|
import { EventSubscription } from "@mongez/events";
|
|
5
5
|
|
|
6
|
-
//#region
|
|
6
|
+
//#region ../@warlock.js/auth/src/services/auth-events.d.ts
|
|
7
7
|
/**
|
|
8
8
|
* Auth event payload types
|
|
9
9
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-events.d.mts","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"auth-events.d.mts","names":[],"sources":["../../../../../../../@warlock.js/auth/src/services/auth-events.ts"],"mappings":";;;;;;;;;KASY,iBAAA;EAEV,eAAA,GAAkB,IAAA,EAAM,IAAA,EAAM,SAAA,EAAW,SAAA,EAAW,UAAA,GAAa,UAAA;EACjE,cAAA,GAAiB,WAAA;IAAe,KAAA;IAAgB,QAAA;EAAA,GAAqB,MAAA;EACrE,eAAA,GAAkB,WAAA;IAAe,KAAA;IAAgB,QAAA;EAAA;EAGjD,MAAA,GAAS,IAAA,EAAM,IAAA;EACf,YAAA,GAAe,IAAA,EAAM,IAAA;EACrB,iBAAA,GAAoB,IAAA,EAAM,IAAA;EAG1B,eAAA,GAAkB,IAAA,EAAM,IAAA,EAAM,SAAA,EAAW,SAAA;EACzC,iBAAA,GAAoB,IAAA,EAAM,IAAA,EAAM,YAAA,EAAc,SAAA,EAAW,eAAA,EAAiB,YAAA;EAC1E,eAAA,GAAkB,IAAA,EAAM,IAAA,EAAM,KAAA,EAAO,YAAA;EACrC,eAAA,GAAkB,KAAA,EAAO,YAAA;EACzB,qBAAA,GAAwB,QAAA,UAAkB,MAAA,EAAQ,YAAA;EAGlD,kBAAA,GAAqB,IAAA,EAAM,IAAA;EAC3B,yBAAA,GAA4B,IAAA,EAAM,IAAA,EAAM,UAAA;EACxC,gBAAA,GAAmB,IAAA,EAAM,IAAA;EAGzB,iBAAA,GAAoB,IAAA,EAAM,IAAA,EAAM,YAAA,EAAc,YAAA,EAAc,UAAA,GAAa,UAAA;EACzE,mBAAA,GAAsB,IAAA,EAAM,IAAA,EAAM,YAAA,EAAc,YAAA;EAGhD,mBAAA,GAAsB,YAAA;AAAA;;;;KAMZ,aAAA,SAAsB,iBAAiB;;;;KAKvC,iBAAA,WAA4B,aAAA,QACnC,IAAA,EAAM,iBAAA,CAAkB,CAAA,aACjB,OAAA;;;;;;;;;;;;;;;;;;;cAyBC,UAAA;EAzDe;;;eA6Db,aAAA,EAAa,KAAA,EAAS,CAAA,EAAC,QAAA,EAAY,iBAAA,CAAkB,CAAA,IAAK,iBAAA;EA1DzC;;;sBAiEV,aAAA,EAAa,KAAA,EAAS,CAAA,EAAC,QAAA,EAAY,iBAAA,CAAkB,CAAA,IAAK,iBAAA;EAhEpD;;;iBAuEX,aAAA,EAAa,KAAA,EAAS,CAAA,KAAC,IAAA,EAAW,iBAAA,CAAkB,CAAA;EAvEO;;;oBAuFxD,aAAA,EAAa,KAAA,EAAS,CAAA,KAAC,IAAA,EAAW,iBAAA,CAAkB,CAAA;EAtFxC;;;;EACL;;;cAmGb,aAAA;AAAA"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { log } from "@warlock.js/logger";
|
|
1
2
|
import events from "@mongez/events";
|
|
2
3
|
|
|
3
|
-
//#region
|
|
4
|
+
//#region ../@warlock.js/auth/src/services/auth-events.ts
|
|
4
5
|
/**
|
|
5
6
|
* Event namespace prefix for auth events
|
|
6
7
|
*/
|
|
@@ -40,7 +41,11 @@ const authEvents = {
|
|
|
40
41
|
* Emit an auth event
|
|
41
42
|
*/
|
|
42
43
|
emit(event, ...args) {
|
|
43
|
-
|
|
44
|
+
try {
|
|
45
|
+
events.trigger(AUTH_EVENT_PREFIX + event, ...args);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
log.error("auth", "event", error);
|
|
48
|
+
}
|
|
44
49
|
},
|
|
45
50
|
/**
|
|
46
51
|
* Emit an auth event (alias for `emit`)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-events.mjs","names":[],"sources":["
|
|
1
|
+
{"version":3,"file":"auth-events.mjs","names":[],"sources":["../../../../../../../@warlock.js/auth/src/services/auth-events.ts"],"sourcesContent":["import events, { type EventSubscription } from \"@mongez/events\";\nimport { log } from \"@warlock.js/logger\";\nimport type { DeviceInfo, TokenPair } from \"../contracts/types\";\nimport type { Auth } from \"../models/auth.model\";\nimport type { RefreshToken } from \"../models/refresh-token\";\n\n/**\n * Auth event payload types\n */\nexport type AuthEventPayloads = {\n // Login events\n \"login.success\": [user: Auth, tokenPair: TokenPair, deviceInfo?: DeviceInfo];\n \"login.failed\": [credentials: { email?: string; username?: string }, reason: string];\n \"login.attempt\": [credentials: { email?: string; username?: string }];\n\n // Logout events\n logout: [user: Auth];\n \"logout.all\": [user: Auth];\n \"logout.failsafe\": [user: Auth];\n\n // Token events\n \"token.created\": [user: Auth, tokenPair: TokenPair];\n \"token.refreshed\": [user: Auth, newTokenPair: TokenPair, oldRefreshToken: RefreshToken];\n \"token.revoked\": [user: Auth, token: RefreshToken];\n \"token.expired\": [token: RefreshToken];\n \"token.familyRevoked\": [familyId: string, tokens: RefreshToken[]];\n\n // Password events\n \"password.changed\": [user: Auth];\n \"password.resetRequested\": [user: Auth, resetToken: string];\n \"password.reset\": [user: Auth];\n\n // Session events\n \"session.created\": [user: Auth, refreshToken: RefreshToken, deviceInfo?: DeviceInfo];\n \"session.destroyed\": [user: Auth, refreshToken: RefreshToken];\n\n // Cleanup events\n \"cleanup.completed\": [expiredCount: number];\n};\n\n/**\n * Auth event names\n */\nexport type AuthEventName = keyof AuthEventPayloads;\n\n/**\n * Callback type for a specific event\n */\nexport type AuthEventCallback<T extends AuthEventName> = (\n ...args: AuthEventPayloads[T]\n) => void | Promise<void>;\n\n/**\n * Event namespace prefix for auth events\n */\nconst AUTH_EVENT_PREFIX = \"auth.\";\n\n/**\n * Type-safe auth events manager\n *\n * @example\n * ```typescript\n * // Subscribe to events with full autocomplete\n * authEvents.on(\"login.success\", (user, tokenPair, deviceInfo) => {\n * console.log(`User ${user.id} logged in`);\n * });\n *\n * authEvents.on(\"token.refreshed\", (user, newPair, oldToken) => {\n * console.log(`Token refreshed for user ${user.id}`);\n * });\n *\n * // Trigger events\n * authEvents.emit(\"login.success\", user, tokenPair, deviceInfo);\n * ```\n */\nexport const authEvents = {\n /**\n * Subscribe to an auth event\n */\n on<T extends AuthEventName>(event: T, callback: AuthEventCallback<T>): EventSubscription {\n return events.subscribe(AUTH_EVENT_PREFIX + event, callback as Function);\n },\n\n /**\n * Subscribe to an auth event (alias for `on`)\n */\n subscribe<T extends AuthEventName>(event: T, callback: AuthEventCallback<T>): EventSubscription {\n return this.on(event, callback);\n },\n\n /**\n * Emit an auth event\n */\n emit<T extends AuthEventName>(event: T, ...args: AuthEventPayloads[T]): void {\n try {\n events.trigger(AUTH_EVENT_PREFIX + event, ...args);\n } catch (error) {\n // Auth events are fire-and-forget notifications: a throwing listener must\n // never break the flow that emitted it (e.g. turn a completed login into a\n // 500). Log and swallow. NOTE: this guards synchronous listener throws;\n // async listener rejections and \"later listeners still run\" need a fix in\n // @mongez/events (tracked separately).\n log.error(\"auth\", \"event\", error);\n }\n },\n\n /**\n * Emit an auth event (alias for `emit`)\n */\n trigger<T extends AuthEventName>(event: T, ...args: AuthEventPayloads[T]): void {\n this.emit(event, ...args);\n },\n\n /**\n * Unsubscribe from all auth events\n */\n unsubscribeAll(): void {\n events.unsubscribeNamespace(AUTH_EVENT_PREFIX.slice(0, -1));\n },\n\n /**\n * Unsubscribe from a specific auth event\n */\n off(event?: AuthEventName): void {\n if (event) {\n events.unsubscribe(AUTH_EVENT_PREFIX + event);\n } else {\n this.unsubscribeAll();\n }\n },\n};\n"],"mappings":";;;;;;;AAuDA,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;AAoB1B,MAAa,aAAa;;;;CAIxB,GAA4B,OAAU,UAAmD;EACvF,OAAO,OAAO,UAAU,oBAAoB,OAAO,QAAoB;CACzE;;;;CAKA,UAAmC,OAAU,UAAmD;EAC9F,OAAO,KAAK,GAAG,OAAO,QAAQ;CAChC;;;;CAKA,KAA8B,OAAU,GAAG,MAAkC;EAC3E,IAAI;GACF,OAAO,QAAQ,oBAAoB,OAAO,GAAG,IAAI;EACnD,SAAS,OAAO;GAMd,IAAI,MAAM,QAAQ,SAAS,KAAK;EAClC;CACF;;;;CAKA,QAAiC,OAAU,GAAG,MAAkC;EAC9E,KAAK,KAAK,OAAO,GAAG,IAAI;CAC1B;;;;CAKA,iBAAuB;EACrB,OAAO,qBAAqB,kBAAkB,MAAM,GAAG,EAAE,CAAC;CAC5D;;;;CAKA,IAAI,OAA6B;EAC/B,IAAI,OACF,OAAO,YAAY,oBAAoB,KAAK;OAE5C,KAAK,eAAe;CAExB;AACF"}
|