@warlock.js/auth 5.8.0 → 5.9.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 +14 -0
- package/esm/contracts/index.d.mts +1 -1
- package/esm/contracts/types.d.mts +29 -1
- package/esm/contracts/types.d.mts.map +1 -1
- package/esm/contracts/types.mjs.map +1 -1
- package/esm/index.d.mts +2 -2
- package/llms-full.txt +26 -0
- package/package.json +7 -7
- package/skills/protect-routes/SKILL.md +44 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,20 @@ All notable changes to `@warlock.js/auth` are documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). `@warlock.js/*` packages are released in lockstep — every package shares the same version number, so a version below may list only the changes that affected this package.
|
|
6
6
|
|
|
7
|
+
## 5.9.0 - 2026-09-13
|
|
8
|
+
|
|
9
|
+
_Released in lockstep with the `@warlock.js/*` family; no package-specific changes in 5.9.0._
|
|
10
|
+
|
|
11
|
+
## 5.8.0 - 2026-09-13
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- Guarded page routes can redirect a logged-out browser to a configurable login path instead of returning a raw JSON 401 — opt-in via `auth.pageAuth.loginPath` (API routes still return the JSON 401).
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- The `access_tokens` / `refresh_tokens` migrations now derive the `user_id` column type from the user model's primary key (via `foreignId`), so a fresh app with an integer-PK user can log in — the hardcoded `uuid` `user_id` previously failed the first login with no diagnostic.
|
|
20
|
+
|
|
7
21
|
## 5.5.0 - 2026-09-07
|
|
8
22
|
|
|
9
23
|
### Fixed
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { AccessTokenConfig, AccessTokenOutput, AuthConfigurations, AuthCredentials, CanAuthenticate, DeviceInfo, LegacyJwtConfig, LoginResult, LogoutWithoutTokenBehavior, NO_EXPIRATION, RefreshTokenConfig, TokenFrom, TokenPair } from "./types.mjs";
|
|
1
|
+
import { AccessTokenConfig, AccessTokenOutput, AuthConfigurations, AuthCredentials, CanAuthenticate, DeviceInfo, LegacyJwtConfig, LoginResult, LogoutWithoutTokenBehavior, NO_EXPIRATION, PageAuthConfig, RefreshTokenConfig, TokenFrom, TokenPair } from "./types.mjs";
|
|
2
2
|
import { Authenticable } from "./auth-contract.mjs";
|
|
@@ -134,6 +134,28 @@ type LegacyJwtConfig = {
|
|
|
134
134
|
logoutWithoutToken?: LogoutWithoutTokenBehavior;
|
|
135
135
|
};
|
|
136
136
|
};
|
|
137
|
+
/**
|
|
138
|
+
* Page-route auth-failure behavior. When a guarded PAGE route (a React-SSR page,
|
|
139
|
+
* `route.isPage`) rejects a logged-out browser, this decides whether it redirects
|
|
140
|
+
* to a login page instead of returning the API-style JSON 401.
|
|
141
|
+
*
|
|
142
|
+
* Read at runtime through `authConfig.pageAuth` (`auth.pageAuth.*`). API routes
|
|
143
|
+
* always keep the JSON 401 contract regardless of this setting.
|
|
144
|
+
*/
|
|
145
|
+
type PageAuthConfig = {
|
|
146
|
+
/**
|
|
147
|
+
* Login path a guarded page route redirects a logged-out browser to (e.g.
|
|
148
|
+
* `"/login"`). Unset ⇒ page-route auth failures keep the JSON 401, so the
|
|
149
|
+
* behavior is opt-in and backward-compatible.
|
|
150
|
+
*/
|
|
151
|
+
loginPath?: string;
|
|
152
|
+
/**
|
|
153
|
+
* Query parameter the redirect carries the originally-requested path in, so
|
|
154
|
+
* the login flow can send the user back where they were headed.
|
|
155
|
+
* @default "returnUrl"
|
|
156
|
+
*/
|
|
157
|
+
returnUrlParam?: string;
|
|
158
|
+
};
|
|
137
159
|
type AuthConfigurations = {
|
|
138
160
|
/**
|
|
139
161
|
* Define all user types — maps a user-type slug to its `Auth` model class so
|
|
@@ -155,6 +177,12 @@ type AuthConfigurations = {
|
|
|
155
177
|
* Refresh-token configuration (rotation, lifetime, cap, model override).
|
|
156
178
|
*/
|
|
157
179
|
refreshToken?: RefreshTokenConfig;
|
|
180
|
+
/**
|
|
181
|
+
* Page-route auth-failure behavior — set `loginPath` to redirect a logged-out
|
|
182
|
+
* browser on a guarded PAGE route to a login page instead of the JSON 401.
|
|
183
|
+
* Opt-in; API routes are unaffected. Read via `authConfig.pageAuth`.
|
|
184
|
+
*/
|
|
185
|
+
pageAuth?: PageAuthConfig;
|
|
158
186
|
/**
|
|
159
187
|
* @deprecated Use `accessToken` / `refreshToken`. Read via a backward-compatible shim.
|
|
160
188
|
*/
|
|
@@ -214,5 +242,5 @@ type LoginResult<UserType extends Auth> = {
|
|
|
214
242
|
tokens: TokenPair;
|
|
215
243
|
};
|
|
216
244
|
//#endregion
|
|
217
|
-
export { AccessTokenConfig, AccessTokenOutput, AuthConfigurations, AuthCredentials, CanAuthenticate, DeviceInfo, LegacyJwtConfig, LoginResult, LogoutWithoutTokenBehavior, NO_EXPIRATION, RefreshTokenConfig, TokenFrom, TokenPair };
|
|
245
|
+
export { AccessTokenConfig, AccessTokenOutput, AuthConfigurations, AuthCredentials, CanAuthenticate, DeviceInfo, LegacyJwtConfig, LoginResult, LogoutWithoutTokenBehavior, NO_EXPIRATION, PageAuthConfig, RefreshTokenConfig, TokenFrom, TokenPair };
|
|
218
246
|
//# sourceMappingURL=types.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../auth/src/contracts/types.ts"],"mappings":";;;;;;;;;;;AAyBA;;;;AAA0B;AAO1B;;;;AAAsC;AAGtC;;;;AAAqB;cAVR,aAAA;;;;;;KAOD,0BAAA;;KAGA,SAAA;AAMZ;AAAA,KAHY,eAAA,IAAmB,IAAA,EAAM,IAAA,eAAmB,OAAO;;KAGnD,eAAA,GAAkB,MAAM;EAClC,QAAQ;AAAA;;;;;;;KASE,iBAAA;EAeE;;;AAUH;AAMX;EAzBE,KAAA,UAAe,WAAA;;;;EAIf,MAAA;EA+BA;;;;EA1BA,SAAA,GAAY,SAAS;EAsDrB;;;AAA+C;AASjD;;;;;EArDE,SAAA;AAAA;;;;KAMU,kBAAA;EAsDR;;;;EAjDF,KAAA,UAAe,YAAA;EAoDkC;AAAA;
|
|
1
|
+
{"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../auth/src/contracts/types.ts"],"mappings":";;;;;;;;;;;AAyBA;;;;AAA0B;AAO1B;;;;AAAsC;AAGtC;;;;AAAqB;cAVR,aAAA;;;;;;KAOD,0BAAA;;KAGA,SAAA;AAMZ;AAAA,KAHY,eAAA,IAAmB,IAAA,EAAM,IAAA,eAAmB,OAAO;;KAGnD,eAAA,GAAkB,MAAM;EAClC,QAAQ;AAAA;;;;;;;KASE,iBAAA;EAeE;;;AAUH;AAMX;EAzBE,KAAA,UAAe,WAAA;;;;EAIf,MAAA;EA+BA;;;;EA1BA,SAAA,GAAY,SAAS;EAsDrB;;;AAA+C;AASjD;;;;;EArDE,SAAA;AAAA;;;;KAMU,kBAAA;EAsDR;;;;EAjDF,KAAA,UAAe,YAAA;EAoDkC;AAAA;AAYnD;;EA3DE,OAAA;EAiEA;AAMc;AAGhB;;EArEE,MAAA;EA2EiC;;;;;;;EAnEjC,SAAA;EA2FqB;;;;EAtFrB,QAAA;EAoEA;;;;EA/DA,UAAA;EAuEe;;;;EAlEf,kBAAA,GAAqB,0BAA0B;AAAA;AA4E1B;AAGvB;;;;AAQW;AAXY,KAnEX,eAAA;EACV,MAAA;EACA,SAAA,GAAY,SAAA;EACZ,SAAA;EACA,OAAA;IACE,MAAA;IACA,OAAA;IACA,SAAA;IACA,QAAA;IACA,UAAA;IACA,kBAAA,GAAqB,0BAA0B;EAAA;AAAA;;;;;;;;;KAYvC,cAAA;EAoGA;;;;;EA9FV,SAAA;EAgGiB;;;;;EA1FjB,cAAc;AAAA;AAAA,KAGJ,kBAAA;EAuFO;AAAA;;;EAlFjB,QAAA;IAAA,CACG,QAAA,WAAmB,UAAA,CAAW,IAAA;EAAA;;;;;EAMjC,eAAA,GAAkB,eAAA;;;;EAIlB,WAAA,GAAc,iBAAA;;;;EAId,YAAA,GAAe,kBAAA;;;;;;EAMf,QAAA,GAAW,cAAA;;;;EAIX,GAAA,GAAM,eAAA;AAAA;AAAA,KAGI,iBAAA;;;;EAIV,KAAA;;;;EAIA,SAAS;AAAA;;;;KAMC,SAAA;;;;EAIV,WAAA,EAAa,iBAAA;;;;EAIb,YAAA,GAAe,iBAAiB;AAAA;;;;KAMtB,UAAA;;;;EAIV,SAAA;;;;EAIA,EAAA;;;;EAIA,QAAA;;;;;EAKA,QAAA;;;;EAIA,OAAA,GAAU,MAAM;AAAA;AAAA,KAGN,WAAA,kBAA6B,IAAA;EACvC,IAAA,EAAM,QAAA;EACN,MAAA,EAAQ,SAAA;AAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.mjs","names":[],"sources":["../../../../../../../auth/src/contracts/types.ts"],"sourcesContent":["import { type ChildModel } from \"@warlock.js/cascade\";\r\nimport { type Algorithm } from \"fast-jwt\";\r\nimport type { AccessToken } from \"../models/access-token\";\r\nimport type { Auth } from \"../models/auth.model\";\r\nimport type { RefreshToken } from \"../models/refresh-token\";\r\n\r\n/**\r\n * Symbol to indicate no expiration for tokens\r\n * Use this when you explicitly want tokens to never expire\r\n *\r\n * @example\r\n * ```typescript\r\n * // src/config/auth.ts\r\n * import { NO_EXPIRATION, type AuthConfigurations } from \"@warlock.js/auth\";\r\n *\r\n * const authConfigurations: AuthConfigurations = {\r\n * accessToken: {\r\n * secret: env(\"JWT_SECRET\"),\r\n * expiresIn: NO_EXPIRATION, // Token expires within 100 years\r\n * },\r\n * };\r\n *\r\n * export default authConfigurations;\r\n * ```\r\n */\r\nexport const NO_EXPIRATION = \"100y\";\r\n\r\n/**\r\n * Behavior when logout is called without a refresh token\r\n * - \"revoke-all\": Revoke all refresh tokens for the user (secure default)\r\n * - \"error\": Return an error requiring the refresh token\r\n */\r\nexport type LogoutWithoutTokenBehavior = \"revoke-all\" | \"error\";\n\n/** The single request credential source an auth middleware instance accepts. */\nexport type TokenFrom = \"header\" | `cookie:${string}`;\n\n/** Application policy that decides whether a resolved user may authenticate. */\nexport type CanAuthenticate = (user: Auth) => boolean | Promise<boolean>;\n\n/** Password credentials plus the application-defined identity fields. */\nexport type AuthCredentials = Record<string, unknown> & {\n password: string;\n};\n\r\n/**\r\n * Access-token configuration.\r\n *\r\n * `secret` may also be supplied via the deprecated `jwt.secret` (resolved by a\r\n * backward-compatible shim); one of the two is required at runtime.\r\n */\r\nexport type AccessTokenConfig = {\r\n /**\r\n * Override the persisted access-token model — extend {@link AccessToken} to\r\n * add columns (e.g. a tenant key) or rename them, then register the subclass\r\n * here. Defaults to the package's `AccessToken`.\r\n */\r\n model?: typeof AccessToken;\r\n /**\r\n * Secret used to sign access tokens.\r\n */\r\n secret?: string;\r\n /**\r\n * JWT algorithm.\r\n * @default \"HS256\"\r\n */\r\n algorithm?: Algorithm;\r\n /**\r\n * Access-token lifetime — a duration string `ms` parses to a **positive**\r\n * number of milliseconds. A value `ms` cannot parse (`\"30dayz\"`), a\r\n * non-positive one (`\"0d\"`), or a bare number throws naming this key on the\r\n * first token issue: each of them would otherwise sign a token with no `exp`\r\n * claim at all.\r\n * @example \"1h\" or NO_EXPIRATION\r\n * @default \"1h\"\r\n */\r\n expiresIn?: string;\r\n};\r\n\r\n/**\r\n * Refresh-token configuration.\r\n */\r\nexport type RefreshTokenConfig = {\r\n /**\r\n * Override the persisted refresh-token model — extend {@link RefreshToken}.\r\n * Defaults to the package's `RefreshToken`.\r\n */\r\n model?: typeof RefreshToken;\r\n /**\r\n * Enable refresh tokens.\r\n * @default true\r\n */\r\n enabled?: boolean;\r\n /**\r\n * Separate secret for refresh tokens (recommended for security). Falls back\r\n * to the access-token secret when omitted.\r\n */\r\n secret?: string;\r\n /**\r\n * Refresh-token lifetime — a duration string `ms` parses to a **positive**\r\n * number of milliseconds; an unusable value throws naming this key on issue\r\n * (see {@link AccessTokenConfig.expiresIn}).\r\n * @example \"7d\" or \"1w\"\r\n * @default \"7d\"\r\n */\r\n expiresIn?: string;\r\n /**\r\n * Rotate the refresh token on each use (old token revoked, replay detected).\r\n * @default true\r\n */\r\n rotation?: boolean;\r\n /**\r\n * Maximum active refresh tokens per user; the oldest are revoked past it.\r\n * @default 5\r\n */\r\n maxPerUser?: number;\r\n /**\r\n * Behavior when logout is called without a refresh token.\r\n * @default \"revoke-all\"\r\n */\r\n logoutWithoutToken?: LogoutWithoutTokenBehavior;\r\n};\r\n\r\n/**\r\n * Legacy JWT configuration shape. Still honored by the resolver shim with a\r\n * deprecation warning.\r\n *\r\n * @deprecated Use the top-level `accessToken` / `refreshToken` blocks.\r\n */\r\nexport type LegacyJwtConfig = {\r\n secret?: string;\r\n algorithm?: Algorithm;\r\n expiresIn?: string;\r\n refresh?: {\r\n secret?: string;\r\n enabled?: boolean;\r\n expiresIn?: string;\r\n rotation?: boolean;\r\n maxPerUser?: number;\r\n logoutWithoutToken?: LogoutWithoutTokenBehavior;\r\n };\r\n};\r\n\r\nexport type AuthConfigurations = {\r\n /**\r\n * Define all user types — maps a user-type slug to its `Auth` model class so\r\n * tokens and the middleware can resolve the right model.\r\n */\r\n userType: {\n [userType: string]: ChildModel<Auth>;\n };\n /**\n * Decide whether a resolved user may authenticate or receive new tokens.\n * @default () => true\n */\n canAuthenticate?: CanAuthenticate;\n /**\r\n * Access-token configuration (secret, expiry, algorithm, model override).\r\n */\r\n accessToken?: AccessTokenConfig;\r\n /**\r\n * Refresh-token configuration (rotation, lifetime, cap, model override).\r\n */\r\n refreshToken?: RefreshTokenConfig;\r\n /**\r\n * @deprecated Use `accessToken` / `refreshToken`. Read via a backward-compatible shim.\r\n */\r\n jwt?: LegacyJwtConfig;\r\n};\r\n\r\nexport type AccessTokenOutput = {\r\n /**\r\n * JWT Token\r\n */\r\n token: string;\r\n /**\r\n * Exprie time in ISO format UTC time\r\n */\r\n expiresAt: string;\r\n};\r\n\r\n/**\r\n * Token pair returned after login or token refresh\r\n */\r\nexport type TokenPair = {\r\n /**\r\n * JWT access token (short-lived)\r\n */\r\n accessToken: AccessTokenOutput;\r\n /**\r\n * JWT refresh token (long-lived)\r\n */\r\n refreshToken?: AccessTokenOutput;\r\n};\r\n\r\n/**\r\n * Device information for session tracking\r\n */\r\nexport type DeviceInfo = {\r\n /**\r\n * User agent string from request\r\n */\r\n userAgent?: string;\r\n /**\r\n * Client IP address\r\n */\r\n ip?: string;\r\n /**\r\n * Optional device identifier\r\n */\r\n deviceId?: string;\r\n /**\r\n * Token family ID (for rotation tracking)\r\n * @internal\r\n */\r\n familyId?: string;\r\n /**\r\n * Access token payload\r\n */\r\n payload?: Record<string, unknown>;\n};\n\r\nexport type LoginResult<UserType extends Auth> = {\r\n user: UserType;\r\n tokens: TokenPair;\r\n};\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAyBA,MAAa,gBAAgB"}
|
|
1
|
+
{"version":3,"file":"types.mjs","names":[],"sources":["../../../../../../../auth/src/contracts/types.ts"],"sourcesContent":["import { type ChildModel } from \"@warlock.js/cascade\";\r\nimport { type Algorithm } from \"fast-jwt\";\r\nimport type { AccessToken } from \"../models/access-token\";\r\nimport type { Auth } from \"../models/auth.model\";\r\nimport type { RefreshToken } from \"../models/refresh-token\";\r\n\r\n/**\r\n * Symbol to indicate no expiration for tokens\r\n * Use this when you explicitly want tokens to never expire\r\n *\r\n * @example\r\n * ```typescript\r\n * // src/config/auth.ts\r\n * import { NO_EXPIRATION, type AuthConfigurations } from \"@warlock.js/auth\";\r\n *\r\n * const authConfigurations: AuthConfigurations = {\r\n * accessToken: {\r\n * secret: env(\"JWT_SECRET\"),\r\n * expiresIn: NO_EXPIRATION, // Token expires within 100 years\r\n * },\r\n * };\r\n *\r\n * export default authConfigurations;\r\n * ```\r\n */\r\nexport const NO_EXPIRATION = \"100y\";\r\n\r\n/**\r\n * Behavior when logout is called without a refresh token\r\n * - \"revoke-all\": Revoke all refresh tokens for the user (secure default)\r\n * - \"error\": Return an error requiring the refresh token\r\n */\r\nexport type LogoutWithoutTokenBehavior = \"revoke-all\" | \"error\";\r\n\r\n/** The single request credential source an auth middleware instance accepts. */\r\nexport type TokenFrom = \"header\" | `cookie:${string}`;\r\n\r\n/** Application policy that decides whether a resolved user may authenticate. */\r\nexport type CanAuthenticate = (user: Auth) => boolean | Promise<boolean>;\r\n\r\n/** Password credentials plus the application-defined identity fields. */\r\nexport type AuthCredentials = Record<string, unknown> & {\r\n password: string;\r\n};\r\n\r\n/**\r\n * Access-token configuration.\r\n *\r\n * `secret` may also be supplied via the deprecated `jwt.secret` (resolved by a\r\n * backward-compatible shim); one of the two is required at runtime.\r\n */\r\nexport type AccessTokenConfig = {\r\n /**\r\n * Override the persisted access-token model — extend {@link AccessToken} to\r\n * add columns (e.g. a tenant key) or rename them, then register the subclass\r\n * here. Defaults to the package's `AccessToken`.\r\n */\r\n model?: typeof AccessToken;\r\n /**\r\n * Secret used to sign access tokens.\r\n */\r\n secret?: string;\r\n /**\r\n * JWT algorithm.\r\n * @default \"HS256\"\r\n */\r\n algorithm?: Algorithm;\r\n /**\r\n * Access-token lifetime — a duration string `ms` parses to a **positive**\r\n * number of milliseconds. A value `ms` cannot parse (`\"30dayz\"`), a\r\n * non-positive one (`\"0d\"`), or a bare number throws naming this key on the\r\n * first token issue: each of them would otherwise sign a token with no `exp`\r\n * claim at all.\r\n * @example \"1h\" or NO_EXPIRATION\r\n * @default \"1h\"\r\n */\r\n expiresIn?: string;\r\n};\r\n\r\n/**\r\n * Refresh-token configuration.\r\n */\r\nexport type RefreshTokenConfig = {\r\n /**\r\n * Override the persisted refresh-token model — extend {@link RefreshToken}.\r\n * Defaults to the package's `RefreshToken`.\r\n */\r\n model?: typeof RefreshToken;\r\n /**\r\n * Enable refresh tokens.\r\n * @default true\r\n */\r\n enabled?: boolean;\r\n /**\r\n * Separate secret for refresh tokens (recommended for security). Falls back\r\n * to the access-token secret when omitted.\r\n */\r\n secret?: string;\r\n /**\r\n * Refresh-token lifetime — a duration string `ms` parses to a **positive**\r\n * number of milliseconds; an unusable value throws naming this key on issue\r\n * (see {@link AccessTokenConfig.expiresIn}).\r\n * @example \"7d\" or \"1w\"\r\n * @default \"7d\"\r\n */\r\n expiresIn?: string;\r\n /**\r\n * Rotate the refresh token on each use (old token revoked, replay detected).\r\n * @default true\r\n */\r\n rotation?: boolean;\r\n /**\r\n * Maximum active refresh tokens per user; the oldest are revoked past it.\r\n * @default 5\r\n */\r\n maxPerUser?: number;\r\n /**\r\n * Behavior when logout is called without a refresh token.\r\n * @default \"revoke-all\"\r\n */\r\n logoutWithoutToken?: LogoutWithoutTokenBehavior;\r\n};\r\n\r\n/**\r\n * Legacy JWT configuration shape. Still honored by the resolver shim with a\r\n * deprecation warning.\r\n *\r\n * @deprecated Use the top-level `accessToken` / `refreshToken` blocks.\r\n */\r\nexport type LegacyJwtConfig = {\r\n secret?: string;\r\n algorithm?: Algorithm;\r\n expiresIn?: string;\r\n refresh?: {\r\n secret?: string;\r\n enabled?: boolean;\r\n expiresIn?: string;\r\n rotation?: boolean;\r\n maxPerUser?: number;\r\n logoutWithoutToken?: LogoutWithoutTokenBehavior;\r\n };\r\n};\r\n\r\n/**\r\n * Page-route auth-failure behavior. When a guarded PAGE route (a React-SSR page,\r\n * `route.isPage`) rejects a logged-out browser, this decides whether it redirects\r\n * to a login page instead of returning the API-style JSON 401.\r\n *\r\n * Read at runtime through `authConfig.pageAuth` (`auth.pageAuth.*`). API routes\r\n * always keep the JSON 401 contract regardless of this setting.\r\n */\r\nexport type PageAuthConfig = {\r\n /**\r\n * Login path a guarded page route redirects a logged-out browser to (e.g.\r\n * `\"/login\"`). Unset ⇒ page-route auth failures keep the JSON 401, so the\r\n * behavior is opt-in and backward-compatible.\r\n */\r\n loginPath?: string;\r\n /**\r\n * Query parameter the redirect carries the originally-requested path in, so\r\n * the login flow can send the user back where they were headed.\r\n * @default \"returnUrl\"\r\n */\r\n returnUrlParam?: string;\r\n};\r\n\r\nexport type AuthConfigurations = {\r\n /**\r\n * Define all user types — maps a user-type slug to its `Auth` model class so\r\n * tokens and the middleware can resolve the right model.\r\n */\r\n userType: {\r\n [userType: string]: ChildModel<Auth>;\r\n };\r\n /**\r\n * Decide whether a resolved user may authenticate or receive new tokens.\r\n * @default () => true\r\n */\r\n canAuthenticate?: CanAuthenticate;\r\n /**\r\n * Access-token configuration (secret, expiry, algorithm, model override).\r\n */\r\n accessToken?: AccessTokenConfig;\r\n /**\r\n * Refresh-token configuration (rotation, lifetime, cap, model override).\r\n */\r\n refreshToken?: RefreshTokenConfig;\r\n /**\r\n * Page-route auth-failure behavior — set `loginPath` to redirect a logged-out\r\n * browser on a guarded PAGE route to a login page instead of the JSON 401.\r\n * Opt-in; API routes are unaffected. Read via `authConfig.pageAuth`.\r\n */\r\n pageAuth?: PageAuthConfig;\r\n /**\r\n * @deprecated Use `accessToken` / `refreshToken`. Read via a backward-compatible shim.\r\n */\r\n jwt?: LegacyJwtConfig;\r\n};\r\n\r\nexport type AccessTokenOutput = {\r\n /**\r\n * JWT Token\r\n */\r\n token: string;\r\n /**\r\n * Exprie time in ISO format UTC time\r\n */\r\n expiresAt: string;\r\n};\r\n\r\n/**\r\n * Token pair returned after login or token refresh\r\n */\r\nexport type TokenPair = {\r\n /**\r\n * JWT access token (short-lived)\r\n */\r\n accessToken: AccessTokenOutput;\r\n /**\r\n * JWT refresh token (long-lived)\r\n */\r\n refreshToken?: AccessTokenOutput;\r\n};\r\n\r\n/**\r\n * Device information for session tracking\r\n */\r\nexport type DeviceInfo = {\r\n /**\r\n * User agent string from request\r\n */\r\n userAgent?: string;\r\n /**\r\n * Client IP address\r\n */\r\n ip?: string;\r\n /**\r\n * Optional device identifier\r\n */\r\n deviceId?: string;\r\n /**\r\n * Token family ID (for rotation tracking)\r\n * @internal\r\n */\r\n familyId?: string;\r\n /**\r\n * Access token payload\r\n */\r\n payload?: Record<string, unknown>;\r\n};\r\n\r\nexport type LoginResult<UserType extends Auth> = {\r\n user: UserType;\r\n tokens: TokenPair;\r\n};\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAyBA,MAAa,gBAAgB"}
|
package/esm/index.d.mts
CHANGED
|
@@ -3,7 +3,7 @@ import { registerAuthPurgeNeverExpiringCommand } from "./commands/auth-purge-nev
|
|
|
3
3
|
import { registerJWTSecretGeneratorCommand } from "./commands/jwt-secret-generator-command.mjs";
|
|
4
4
|
import { Auth } from "./models/auth.model.mjs";
|
|
5
5
|
import { AccessToken, accessTokenSchema } from "./models/access-token/access-token.model.mjs";
|
|
6
|
-
import { AccessTokenConfig, AccessTokenOutput, AuthConfigurations, AuthCredentials, CanAuthenticate, DeviceInfo, LegacyJwtConfig, LoginResult, LogoutWithoutTokenBehavior, NO_EXPIRATION, RefreshTokenConfig, TokenFrom, TokenPair } from "./contracts/types.mjs";
|
|
6
|
+
import { AccessTokenConfig, AccessTokenOutput, AuthConfigurations, AuthCredentials, CanAuthenticate, DeviceInfo, LegacyJwtConfig, LoginResult, LogoutWithoutTokenBehavior, NO_EXPIRATION, PageAuthConfig, RefreshTokenConfig, TokenFrom, TokenPair } from "./contracts/types.mjs";
|
|
7
7
|
import { RefreshToken, RefreshTokenIssueOptions, refreshTokenSchema } from "./models/refresh-token/refresh-token.model.mjs";
|
|
8
8
|
import { Authenticable } from "./contracts/auth-contract.mjs";
|
|
9
9
|
import { authMiddleware } from "./middleware/auth.middleware.mjs";
|
|
@@ -15,4 +15,4 @@ import { generateJWTSecret } from "./services/generate-jwt-secret.mjs";
|
|
|
15
15
|
import { AuthErrorCodes } from "./utils/auth-error-codes.mjs";
|
|
16
16
|
import { TokenType, TokenTypeError, isInvalidCredentialError, jwt } from "./services/jwt.mjs";
|
|
17
17
|
import { isNeverExpiring, isUsableExpiry, tokenHasExpClaim } from "./utils/token-expiry.mjs";
|
|
18
|
-
export { AccessToken, AccessTokenConfig, AccessTokenOutput, Auth, AuthConfigurations, AuthCredentials, AuthErrorCodes, AuthEventCallback, AuthEventName, AuthEventPayloads, Authenticable, CanAuthenticate, DeviceInfo, LegacyJwtConfig, LoginResult, LoginThrottleIdentifier, LoginThrottleOptions, LogoutWithoutTokenBehavior, NO_EXPIRATION, RefreshToken, RefreshTokenConfig, RefreshTokenIssueOptions, TokenFrom, TokenPair, TokenType, TokenTypeError, accessTokenSchema, authEvents, authMiddleware, authMigrations, authService, generateJWTSecret, isInvalidCredentialError, isNeverExpiring, isUsableExpiry, jwt, loginThrottleMiddleware, refreshTokenSchema, registerAuthCleanupCommand, registerAuthPurgeNeverExpiringCommand, registerJWTSecretGeneratorCommand, tokenHasExpClaim };
|
|
18
|
+
export { AccessToken, AccessTokenConfig, AccessTokenOutput, Auth, AuthConfigurations, AuthCredentials, AuthErrorCodes, AuthEventCallback, AuthEventName, AuthEventPayloads, Authenticable, CanAuthenticate, DeviceInfo, LegacyJwtConfig, LoginResult, LoginThrottleIdentifier, LoginThrottleOptions, LogoutWithoutTokenBehavior, NO_EXPIRATION, PageAuthConfig, RefreshToken, RefreshTokenConfig, RefreshTokenIssueOptions, TokenFrom, TokenPair, TokenType, TokenTypeError, accessTokenSchema, authEvents, authMiddleware, authMigrations, authService, generateJWTSecret, isInvalidCredentialError, isNeverExpiring, isUsableExpiry, jwt, loginThrottleMiddleware, refreshTokenSchema, registerAuthCleanupCommand, registerAuthPurgeNeverExpiringCommand, registerJWTSecretGeneratorCommand, tokenHasExpClaim };
|
package/llms-full.txt
CHANGED
|
@@ -874,6 +874,32 @@ On failure, the middleware returns one of these 401 responses:
|
|
|
874
874
|
| `InvalidAccessToken` | Token doesn't verify (signature, missing/passed `exp`, wrong token type), has no DB row, or the row's `expires_at` has passed |
|
|
875
875
|
| `Unauthorized` | Token valid but user-type isn't in the allowed list |
|
|
876
876
|
|
|
877
|
+
## Page routes: redirect to login instead of the JSON 401
|
|
878
|
+
|
|
879
|
+
By default every auth failure — API or page — returns the JSON `401` above. That's right for an API client, but a logged-out human who *navigates* to a guarded page route gets a raw JSON error blob instead of a login screen.
|
|
880
|
+
|
|
881
|
+
Opt into a redirect with `auth.pageAuth` (5.8+). Set `loginPath` and a guarded **page** route (`route.isPage`) that a logged-out browser hits redirects to `loginPath?returnUrl=<original path>` instead of returning the JSON 401:
|
|
882
|
+
|
|
883
|
+
```ts title="src/config/auth.ts"
|
|
884
|
+
export default {
|
|
885
|
+
userType: { user: User },
|
|
886
|
+
pageAuth: {
|
|
887
|
+
loginPath: "/login", // set this to turn the redirect on
|
|
888
|
+
returnUrlParam: "returnUrl", // optional, this is the default
|
|
889
|
+
},
|
|
890
|
+
};
|
|
891
|
+
```
|
|
892
|
+
|
|
893
|
+
Behavior, precisely:
|
|
894
|
+
|
|
895
|
+
- **Page route + `loginPath` set** → `302` redirect to `loginPath?returnUrl=<encoded request path>`. The login flow reads `returnUrl` to send the user back where they were headed. If `loginPath` already carries a query string, the param is appended with `&` instead of `?`.
|
|
896
|
+
- **Page route + `loginPath` unset** → the JSON `401` is unchanged. The redirect is **opt-in**, so an app that hasn't configured `pageAuth` keeps exactly its current behavior.
|
|
897
|
+
- **API route** → always the JSON `401`, regardless of `pageAuth`. The API contract is never altered.
|
|
898
|
+
|
|
899
|
+
The page-vs-API signal is `request.route.isPage`. Config is typed as `PageAuthConfig` on `AuthConfigurations` (`loginPath?: string`, `returnUrlParam?: string`, default `"returnUrl"`); read at runtime through `authConfig.pageAuth`.
|
|
900
|
+
|
|
901
|
+
> The redirect approach may be revisited in a later version; this documents the shipped 5.8 behavior.
|
|
902
|
+
|
|
877
903
|
## Reading the user in a controller
|
|
878
904
|
|
|
879
905
|
```ts
|
package/package.json
CHANGED
|
@@ -12,12 +12,12 @@
|
|
|
12
12
|
"ms": "^2.1.3"
|
|
13
13
|
},
|
|
14
14
|
"peerDependencies": {
|
|
15
|
-
"@warlock.js/fs": "5.
|
|
16
|
-
"@warlock.js/cache": "5.
|
|
17
|
-
"@warlock.js/cascade": "5.
|
|
18
|
-
"@warlock.js/core": "5.
|
|
19
|
-
"@warlock.js/logger": "5.
|
|
20
|
-
"@warlock.js/seal": "5.
|
|
15
|
+
"@warlock.js/fs": "5.9.0",
|
|
16
|
+
"@warlock.js/cache": "5.9.0",
|
|
17
|
+
"@warlock.js/cascade": "5.9.0",
|
|
18
|
+
"@warlock.js/core": "5.9.0",
|
|
19
|
+
"@warlock.js/logger": "5.9.0",
|
|
20
|
+
"@warlock.js/seal": "5.9.0"
|
|
21
21
|
},
|
|
22
22
|
"repository": {
|
|
23
23
|
"type": "git",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
],
|
|
34
34
|
"author": "hassanzohdy",
|
|
35
35
|
"license": "MIT",
|
|
36
|
-
"version": "5.
|
|
36
|
+
"version": "5.9.0",
|
|
37
37
|
"type": "module",
|
|
38
38
|
"main": "./esm/index.mjs",
|
|
39
39
|
"module": "./esm/index.mjs",
|
|
@@ -58,6 +58,32 @@ On failure, the middleware returns one of these 401 responses:
|
|
|
58
58
|
| `InvalidAccessToken` | Token doesn't verify (signature, missing/passed `exp`, wrong token type), has no DB row, or the row's `expires_at` has passed |
|
|
59
59
|
| `Unauthorized` | Token valid but user-type isn't in the allowed list |
|
|
60
60
|
|
|
61
|
+
## Page routes: redirect to login instead of the JSON 401
|
|
62
|
+
|
|
63
|
+
By default every auth failure — API or page — returns the JSON `401` above. That's right for an API client, but a logged-out human who *navigates* to a guarded page route gets a raw JSON error blob instead of a login screen.
|
|
64
|
+
|
|
65
|
+
Opt into a redirect with `auth.pageAuth` (5.8+). Set `loginPath` and a guarded **page** route (`route.isPage`) that a logged-out browser hits redirects to `loginPath?returnUrl=<original path>` instead of returning the JSON 401:
|
|
66
|
+
|
|
67
|
+
```ts title="src/config/auth.ts"
|
|
68
|
+
export default {
|
|
69
|
+
userType: { user: User },
|
|
70
|
+
pageAuth: {
|
|
71
|
+
loginPath: "/login", // set this to turn the redirect on
|
|
72
|
+
returnUrlParam: "returnUrl", // optional, this is the default
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Behavior, precisely:
|
|
78
|
+
|
|
79
|
+
- **Page route + `loginPath` set** → `302` redirect to `loginPath?returnUrl=<encoded request path>`. The login flow reads `returnUrl` to send the user back where they were headed. If `loginPath` already carries a query string, the param is appended with `&` instead of `?`.
|
|
80
|
+
- **Page route + `loginPath` unset** → the JSON `401` is unchanged. The redirect is **opt-in**, so an app that hasn't configured `pageAuth` keeps exactly its current behavior.
|
|
81
|
+
- **API route** → always the JSON `401`, regardless of `pageAuth`. The API contract is never altered.
|
|
82
|
+
|
|
83
|
+
The page-vs-API signal is `request.route.isPage`. Config is typed as `PageAuthConfig` on `AuthConfigurations` (`loginPath?: string`, `returnUrlParam?: string`, default `"returnUrl"`); read at runtime through `authConfig.pageAuth`.
|
|
84
|
+
|
|
85
|
+
> `auth.pageAuth` is **not a second auth mechanism** — it is a convenience *adapter* over the same "not authenticated" outcome the gate already produces. It only changes the **representation** of that outcome on a page route (a browser-friendly login redirect) instead of the JSON `401`; the decision that the request is unauthenticated is unchanged. The sanctioned *generic* way to make authentication soft is the app-owned optional-auth middleware below — `pageAuth` is the built-in adapter for the common "redirect a logged-out human to /login" case, so most apps never need to hand-roll it.
|
|
86
|
+
|
|
61
87
|
## Reading the user in a controller
|
|
62
88
|
|
|
63
89
|
```ts
|
|
@@ -85,19 +111,31 @@ router.group({ prefix: "/admin", middleware: [authMiddleware("admin")] }, () =>
|
|
|
85
111
|
|
|
86
112
|
Every route inside the group is gated — the group's `middleware` array applies to each route in the callback. Cleaner than repeating the middleware on each route.
|
|
87
113
|
|
|
88
|
-
##
|
|
114
|
+
## Optional auth is an app-owned middleware (the sanctioned pattern)
|
|
89
115
|
|
|
90
|
-
There is no "hydrate `request.user` if a token is present, otherwise continue" mode
|
|
116
|
+
`authMiddleware` is a **hard gate** — it always requires a valid token and rejects when one is absent. There is deliberately no built-in "hydrate `request.user` if a token is present, otherwise continue" mode, because the *sanctioned generic pattern* for soft/optional auth is a small **app-owned optional-auth middleware**: resolve the user when a valid token is present, and otherwise leave the route to decide. The app owns it because the "what to do when absent" policy is the app's, not the framework's.
|
|
91
117
|
|
|
92
|
-
```ts
|
|
93
|
-
import { type
|
|
118
|
+
```ts title="src/app/middleware/optional-auth.middleware.ts"
|
|
119
|
+
import { type Middleware } from "@warlock.js/core";
|
|
120
|
+
import { jwt } from "@warlock.js/auth";
|
|
94
121
|
|
|
95
|
-
|
|
122
|
+
// The one sanctioned optional-auth shape: resolve if present, never reject.
|
|
123
|
+
// Middleware receives the context object ({ request, response }), like a handler.
|
|
124
|
+
export const optionalAuth: Middleware = async ({ request }) => {
|
|
96
125
|
const token = request.authorizationValue;
|
|
97
|
-
//
|
|
126
|
+
if (!token) return; // absent → continue anonymously; the route decides
|
|
127
|
+
try {
|
|
128
|
+
const decoded = await jwt.verify(token);
|
|
129
|
+
// hydrate request.user from your token/user model when valid
|
|
130
|
+
} catch {
|
|
131
|
+
// invalid token on an optional route → treat as anonymous, don't reject
|
|
132
|
+
}
|
|
133
|
+
// returning nothing (undefined) === continue to the handler
|
|
98
134
|
};
|
|
99
135
|
```
|
|
100
136
|
|
|
137
|
+
Wire it like any middleware (`{ middleware: [optionalAuth] }`), then branch on `request.user` in the controller. Use `authMiddleware` when the route must be gated; use this when the route is public but personalizes for a signed-in user. `auth.pageAuth` (above) is the built-in **adapter** over this same "unauthenticated" outcome for the common page-redirect case — it is not a competing mechanism.
|
|
138
|
+
|
|
101
139
|
## Custom error responses
|
|
102
140
|
|
|
103
141
|
The middleware uses the framework's `response.unauthorized({...})` shape. To override the response globally, hook the framework's error transformer to remap `AuthErrorCodes.*` codes.
|