najm-auth 2.0.4 → 2.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -167,6 +167,7 @@ All routes are prefixed with `/auth` and auto-registered by the plugin.
167
167
  | `POST` | `/auth/register` | Register new user | None |
168
168
  | `POST` | `/auth/login` | Login with email/password | None |
169
169
  | `POST` | `/auth/refresh` | Refresh access token (cookie) | None (uses refresh cookie) |
170
+ | `POST` | `/auth/session/recover` | Reissue signed session without token rotation | Refresh cookie + recovery header |
170
171
  | `POST` | `/auth/logout` | Logout and revoke tokens | ✅ Required |
171
172
  | `GET` | `/auth/me` | Get current user profile | ✅ Required |
172
173
  | `POST` | `/auth/forgot-password` | Request password reset | None |
@@ -600,6 +601,7 @@ limits are active when `auth()` is registered.
600
601
  | `POST /auth/register` | 5 | 15 minutes | IP |
601
602
  | `POST /auth/login` | 5 | 15 minutes | IP |
602
603
  | `POST /auth/refresh` | 15 | 15 minutes | Cookie fingerprint |
604
+ | `POST /auth/session/recover` | 120 | 1 minute | Cookie fingerprint |
603
605
  | `POST /auth/logout` | 10 | 15 minutes | User ID |
604
606
  | `GET /auth/me` | 30 | 1 minute | User ID |
605
607
  | `POST /auth/forgot-password` | 3 | 15 minutes | IP |
@@ -675,8 +677,12 @@ throw new HttpError(403, 'Insufficient permissions for this action');
675
677
  - Login uses a dummy password hash for missing users to reduce timing leaks.
676
678
  - Forgot-password responses avoid email enumeration.
677
679
  - Auth routes register `najm-rate` and ship route-level brute-force limits.
678
- - Session cookies are signed, short-lived, and checked against session version
679
- invalidation.
680
+ - Session cookies are signed and short-lived; server auth resolution checks
681
+ their session version.
682
+ - Expired signed sessions recover through authoritative, non-rotating refresh
683
+ validation; middleware verifies the reissued HMAC before using its claims.
684
+ - `verifyAlways` forces that authoritative check on every protected request;
685
+ the default bounds cached role/status staleness to `session.maxAge`.
680
686
 
681
687
  ### Password Reset Tokens
682
688
 
@@ -11,6 +11,10 @@ interface AuthMiddlewareConfig {
11
11
  roleRoutes?: Record<string, string[]>;
12
12
  /** Refresh token cookie name (default: 'refreshToken') */
13
13
  cookieName?: string;
14
+ /** API base URL used by session recovery (default: '/api'). */
15
+ apiBaseURL?: string;
16
+ /** Auth endpoint prefix used by session recovery (default: '/auth'). */
17
+ authPrefix?: string;
14
18
  /** Signed session cookie name (default: 'najm.session') */
15
19
  sessionCookieName?: string;
16
20
  /** @deprecated Session cookies are verified locally at the Edge. */
@@ -20,10 +24,16 @@ interface AuthMiddlewareConfig {
20
24
  /** Must match the auth plugin's session.maxAge. Default: 300 seconds. */
21
25
  sessionMaxAge?: number;
22
26
  /**
23
- * Retained for compatibility. Every protected route now verifies the signed
24
- * session cookie locally, so enabling this never calls `/auth/me`.
27
+ * Force authoritative refresh-session validation on every protected request.
28
+ * This reissues the signed session cookie without rotating refresh tokens.
25
29
  */
26
30
  verifyAlways?: boolean;
31
+ /**
32
+ * Session-recovery endpoint. Relative values resolve against the request
33
+ * origin. Defaults to `${apiBaseURL}${authPrefix}/session/recover`.
34
+ * Set to false to disable automatic recovery.
35
+ */
36
+ recoveryURL?: string | false;
27
37
  }
28
38
  /**
29
39
  * Create a Next.js middleware function that protects routes based on auth state.
@@ -131,6 +131,99 @@ function isStringArray(value) {
131
131
  }
132
132
  __name(isStringArray, "isStringArray");
133
133
 
134
+ // src/client/sessionRecovery.ts
135
+ async function requestSessionRecovery(options) {
136
+ if (!isCookieName(options.refreshCookieName) || !isCookieName(options.sessionCookieName)) {
137
+ return { status: "unavailable" };
138
+ }
139
+ if (!isCookieValue(options.refreshCookieValue)) {
140
+ return { status: "invalid" };
141
+ }
142
+ if (!isSafeRecoveryEndpoint(options.endpoint)) {
143
+ return { status: "unavailable" };
144
+ }
145
+ let response;
146
+ try {
147
+ response = await fetch(options.endpoint, {
148
+ method: "POST",
149
+ headers: {
150
+ Accept: "application/json",
151
+ Cookie: `${options.refreshCookieName}=${options.refreshCookieValue}`,
152
+ "X-Najm-Session-Recovery": "1"
153
+ },
154
+ cache: "no-store",
155
+ redirect: "manual"
156
+ });
157
+ } catch {
158
+ return { status: "unavailable" };
159
+ }
160
+ if (!response.ok) {
161
+ const status = response.status;
162
+ return {
163
+ status: status === 400 || status === 401 || status === 403 ? "invalid" : "unavailable",
164
+ httpStatus: status
165
+ };
166
+ }
167
+ const setCookie = response.headers.get("set-cookie");
168
+ if (!setCookie) return { status: "unavailable", httpStatus: response.status };
169
+ const sessionCookieValue = readSetCookieValue(setCookie, options.sessionCookieName);
170
+ if (!sessionCookieValue) {
171
+ return { status: "unavailable", httpStatus: response.status };
172
+ }
173
+ const claims = await verifySessionCookie(sessionCookieValue, {
174
+ secret: options.sessionSecret,
175
+ maxAgeSeconds: options.sessionMaxAge
176
+ });
177
+ if (!claims) return { status: "unavailable", httpStatus: response.status };
178
+ return {
179
+ status: "recovered",
180
+ claims,
181
+ setCookie,
182
+ sessionCookieValue
183
+ };
184
+ }
185
+ __name(requestSessionRecovery, "requestSessionRecovery");
186
+ function authEndpoint(baseURL, authPrefix, suffix, requestURL) {
187
+ const normalizedBase = baseURL.replace(/\/+$/, "");
188
+ const normalizedPrefix = `/${authPrefix.replace(/^\/+|\/+$/g, "")}`;
189
+ const normalizedSuffix = `/${suffix.replace(/^\/+/, "")}`;
190
+ const value = `${normalizedBase}${normalizedPrefix}${normalizedSuffix}`;
191
+ return requestURL ? new URL(value, requestURL).toString() : value;
192
+ }
193
+ __name(authEndpoint, "authEndpoint");
194
+ function replaceCookieValue(cookieHeader, name, value) {
195
+ const parts = cookieHeader.split(";").map((part) => part.trim()).filter(Boolean).filter((part) => part.slice(0, part.indexOf("=")).trim() !== name);
196
+ parts.push(`${name}=${value}`);
197
+ return parts.join("; ");
198
+ }
199
+ __name(replaceCookieValue, "replaceCookieValue");
200
+ function readSetCookieValue(setCookie, name) {
201
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
202
+ const match = setCookie.match(new RegExp(`(?:^|,\\s*)${escaped}=([^;]*)`));
203
+ return match?.[1] || void 0;
204
+ }
205
+ __name(readSetCookieValue, "readSetCookieValue");
206
+ function isCookieName(value) {
207
+ return /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(value);
208
+ }
209
+ __name(isCookieName, "isCookieName");
210
+ function isCookieValue(value) {
211
+ return value.length > 0 && !/[\r\n;]/.test(value);
212
+ }
213
+ __name(isCookieValue, "isCookieValue");
214
+ function isSafeRecoveryEndpoint(value) {
215
+ try {
216
+ const url = new URL(value);
217
+ if (url.username || url.password) return false;
218
+ if (url.protocol === "https:") return true;
219
+ if (url.protocol !== "http:") return false;
220
+ return url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]" || url.hostname === "::1";
221
+ } catch {
222
+ return false;
223
+ }
224
+ }
225
+ __name(isSafeRecoveryEndpoint, "isSafeRecoveryEndpoint");
226
+
134
227
  // src/client/server/withAuthMiddleware.ts
135
228
  function withAuthMiddleware(config) {
136
229
  const {
@@ -139,26 +232,31 @@ function withAuthMiddleware(config) {
139
232
  loginRoute = "/login",
140
233
  roleRoutes = {},
141
234
  cookieName = "refreshToken",
235
+ apiBaseURL = "/api",
236
+ authPrefix = "/auth",
142
237
  sessionCookieName = "najm.session",
143
238
  sessionSecret,
144
239
  sessionMaxAge,
145
- verifyAlways = false
240
+ verifyAlways = false,
241
+ recoveryURL
146
242
  } = config;
147
- void verifyAlways;
148
243
  return /* @__PURE__ */ __name(async function middleware(request) {
149
244
  const { NextResponse } = await import("next/server");
150
- const redirectToLogin = /* @__PURE__ */ __name((pathname2, clearCookies) => {
245
+ const redirectToLogin = /* @__PURE__ */ __name((returnPath2, clearCookies) => {
151
246
  const loginUrl = new URL(loginRoute, request.url);
152
- loginUrl.searchParams.set("from", pathname2);
247
+ loginUrl.searchParams.set("from", returnPath2);
153
248
  const res = NextResponse.redirect(loginUrl);
154
- if (clearCookies) {
249
+ if (clearCookies.includes("refresh")) {
155
250
  res.cookies.delete(cookieName);
251
+ }
252
+ if (clearCookies.includes("session")) {
156
253
  res.cookies.delete(sessionCookieName);
157
254
  }
158
255
  return res;
159
256
  }, "redirectToLogin");
160
257
  const url = new URL(request.url);
161
258
  const pathname = url.pathname;
259
+ const returnPath = `${url.pathname}${url.search}`;
162
260
  if (matchesAny(pathname, publicRoutes)) {
163
261
  return NextResponse.next();
164
262
  }
@@ -167,19 +265,53 @@ function withAuthMiddleware(config) {
167
265
  const cookie = request.headers.get("cookie") ?? "";
168
266
  const sessionCookie = readCookieValue(cookie, sessionCookieName);
169
267
  const secret = resolveSessionSecret(sessionSecret);
170
- if (!sessionCookie || !secret) {
171
- return redirectToLogin(pathname, true);
268
+ if (!secret) {
269
+ return redirectToLogin(returnPath, ["session"]);
172
270
  }
173
- const session = await verifySessionCookie(sessionCookie, {
271
+ let session = sessionCookie ? await verifySessionCookie(sessionCookie, {
174
272
  secret,
175
273
  maxAgeSeconds: sessionMaxAge
176
- });
177
- if (!session) {
178
- return redirectToLogin(pathname, true);
274
+ }) : null;
275
+ let recovery = null;
276
+ if (!session || verifyAlways) {
277
+ const refreshCookie = readCookieValue(cookie, cookieName);
278
+ if (!refreshCookie || recoveryURL === false) {
279
+ return redirectToLogin(returnPath, ["refresh", "session"]);
280
+ }
281
+ const endpoint = recoveryURL ? new URL(recoveryURL, request.url).toString() : authEndpoint(apiBaseURL, authPrefix, "/session/recover", request.url);
282
+ recovery = await requestSessionRecovery({
283
+ endpoint,
284
+ refreshCookieName: cookieName,
285
+ refreshCookieValue: refreshCookie,
286
+ sessionCookieName,
287
+ sessionSecret: secret,
288
+ sessionMaxAge
289
+ });
290
+ if (recovery.status !== "recovered") {
291
+ return redirectToLogin(
292
+ returnPath,
293
+ recovery.status === "invalid" ? ["refresh", "session"] : ["session"]
294
+ );
295
+ }
296
+ session = recovery.claims;
179
297
  }
180
298
  const requiredRoles = findMatchingRoles(pathname, roleRoutes);
181
299
  if (requiredRoles && !session.roles.some((role) => requiredRoles.includes(role))) {
182
- return new NextResponse(null, { status: 403 });
300
+ const forbidden = new NextResponse(null, { status: 403 });
301
+ if (recovery?.status === "recovered") {
302
+ forbidden.headers.append("Set-Cookie", recovery.setCookie);
303
+ }
304
+ return forbidden;
305
+ }
306
+ if (recovery?.status === "recovered") {
307
+ const requestHeaders = new Headers(request.headers);
308
+ requestHeaders.set(
309
+ "cookie",
310
+ replaceCookieValue(cookie, sessionCookieName, recovery.sessionCookieValue)
311
+ );
312
+ const response = NextResponse.next({ request: { headers: requestHeaders } });
313
+ response.headers.append("Set-Cookie", recovery.setCookie);
314
+ return response;
183
315
  }
184
316
  return NextResponse.next();
185
317
  }, "middleware");
@@ -75,24 +75,14 @@ interface GetSessionConfig {
75
75
  * Defaults to `${NEXT_PUBLIC_API_URL || http://localhost:${PORT||3000}}/api`.
76
76
  */
77
77
  baseURL?: string;
78
- /**
79
- * Auth route prefix appended to baseURL (default: '/auth').
80
- */
78
+ /** Auth route prefix appended to baseURL (default: '/auth'). */
81
79
  authPrefix?: string;
82
- /**
83
- * Refresh token cookie name to check before making the network call.
84
- * If absent we skip and return null immediately.
85
- * Default: 'refreshToken'
86
- */
80
+ /** Refresh token cookie name (default: 'refreshToken'). */
87
81
  cookieName?: string;
88
- /**
89
- * Session cookie name (default: 'najm.session').
90
- * When present and valid, skips the /auth/me fetch entirely.
91
- */
82
+ /** Signed session cookie name (default: 'najm.session'). */
92
83
  sessionCookieName?: string;
93
84
  /**
94
85
  * Secret used to verify the session cookie HMAC signature.
95
- * Must match the `jwt.accessSecret` used by the auth plugin.
96
86
  * Falls back to NAJM_SESSION_SECRET or JWT_ACCESS_SECRET env vars.
97
87
  */
98
88
  sessionSecret?: string;
@@ -101,6 +91,11 @@ interface GetSessionConfig {
101
91
  * Must match the auth plugin's `session.maxAge`. Default: 300.
102
92
  */
103
93
  sessionMaxAge?: number;
94
+ /**
95
+ * Session-recovery endpoint. Defaults to
96
+ * `${baseURL}${authPrefix}/session/recover`. Set to false to disable fallback.
97
+ */
98
+ recoveryURL?: string | false;
104
99
  /**
105
100
  * Error handling mode:
106
101
  * - 'nullable' (default): returns null on any failure
@@ -122,29 +117,10 @@ declare class AuthTransportError extends Error {
122
117
  constructor(message: string, status?: number);
123
118
  }
124
119
  /**
125
- * Resolve the current session inside a Next.js Server Component, Route Handler,
126
- * or Server Action.
127
- *
128
- * **Fast path**: If a signed `najm.session` cookie exists and is valid,
129
- * returns the session instantly with zero network calls.
130
- *
131
- * **Fallback**: Checks the refresh token cookie and calls `/auth/me`.
132
- *
133
- * @example
134
- * ```tsx
135
- * import { getSession } from 'najm-auth/client/server';
136
- *
137
- * export default async function RootLayout({ children }) {
138
- * const session = await getSession();
139
- * return (
140
- * <html><body>
141
- * <AuthProvider client={authClient} initialSession={session}>
142
- * {children}
143
- * </AuthProvider>
144
- * </body></html>
145
- * );
146
- * }
147
- * ```
120
+ * Resolve a session in a Next.js Server Component, Route Handler, or Server
121
+ * Action. Recovery returns claims for the current render but cannot persist
122
+ * response cookies during Server Component rendering; middleware performs that
123
+ * persistence for protected navigation.
148
124
  */
149
125
  declare function getSession(config?: GetSessionConfig): Promise<ServerSession | null>;
150
126
 
@@ -200,11 +176,16 @@ interface DefineAuthConfig {
200
176
  sessionSecret?: string;
201
177
  /** Must match the auth plugin's session.maxAge. Default: 300 seconds. */
202
178
  sessionMaxAge?: number;
179
+ /**
180
+ * Session-recovery endpoint. Defaults to
181
+ * `${apiBaseURL}${authPrefix}/session/recover`; false disables recovery.
182
+ */
183
+ recoveryURL?: string | false;
203
184
  /** Next.js middleware matcher (default: exclude _next, favicon, api) */
204
185
  matcher?: string[];
205
186
  /**
206
- * Retained for compatibility. Every protected route verifies the signed
207
- * session cookie locally without an `/auth/me` request.
187
+ * Force authoritative refresh-session validation on every protected request.
188
+ * Recovery reissues the signed cookie without rotating refresh tokens.
208
189
  */
209
190
  verifyAlways?: boolean;
210
191
  }
@@ -218,7 +199,7 @@ interface AuthKit {
218
199
  readonly client: NajmAuthClient;
219
200
  /** Shortcut for `client.api` — the underlying FetchClient with auth attached. */
220
201
  readonly api: FetchClient;
221
- /** Resolve session — reads signed cookie first (instant), falls back to /auth/me */
202
+ /** Resolve session — signed-cookie first, then non-rotating recovery. */
222
203
  getSession: (opts?: Pick<GetSessionConfig, 'mode'>) => Promise<ServerSession | null>;
223
204
  /** Require session — throws if unauthenticated */
224
205
  requireSession: () => Promise<ServerSession>;
@@ -144,6 +144,104 @@ var init_sessionCookie = __esm({
144
144
  }
145
145
  });
146
146
 
147
+ // src/client/sessionRecovery.ts
148
+ async function requestSessionRecovery(options) {
149
+ if (!isCookieName(options.refreshCookieName) || !isCookieName(options.sessionCookieName)) {
150
+ return { status: "unavailable" };
151
+ }
152
+ if (!isCookieValue(options.refreshCookieValue)) {
153
+ return { status: "invalid" };
154
+ }
155
+ if (!isSafeRecoveryEndpoint(options.endpoint)) {
156
+ return { status: "unavailable" };
157
+ }
158
+ let response;
159
+ try {
160
+ response = await fetch(options.endpoint, {
161
+ method: "POST",
162
+ headers: {
163
+ Accept: "application/json",
164
+ Cookie: `${options.refreshCookieName}=${options.refreshCookieValue}`,
165
+ "X-Najm-Session-Recovery": "1"
166
+ },
167
+ cache: "no-store",
168
+ redirect: "manual"
169
+ });
170
+ } catch {
171
+ return { status: "unavailable" };
172
+ }
173
+ if (!response.ok) {
174
+ const status = response.status;
175
+ return {
176
+ status: status === 400 || status === 401 || status === 403 ? "invalid" : "unavailable",
177
+ httpStatus: status
178
+ };
179
+ }
180
+ const setCookie = response.headers.get("set-cookie");
181
+ if (!setCookie) return { status: "unavailable", httpStatus: response.status };
182
+ const sessionCookieValue = readSetCookieValue(setCookie, options.sessionCookieName);
183
+ if (!sessionCookieValue) {
184
+ return { status: "unavailable", httpStatus: response.status };
185
+ }
186
+ const claims = await verifySessionCookie(sessionCookieValue, {
187
+ secret: options.sessionSecret,
188
+ maxAgeSeconds: options.sessionMaxAge
189
+ });
190
+ if (!claims) return { status: "unavailable", httpStatus: response.status };
191
+ return {
192
+ status: "recovered",
193
+ claims,
194
+ setCookie,
195
+ sessionCookieValue
196
+ };
197
+ }
198
+ function authEndpoint(baseURL, authPrefix, suffix, requestURL) {
199
+ const normalizedBase = baseURL.replace(/\/+$/, "");
200
+ const normalizedPrefix = `/${authPrefix.replace(/^\/+|\/+$/g, "")}`;
201
+ const normalizedSuffix = `/${suffix.replace(/^\/+/, "")}`;
202
+ const value = `${normalizedBase}${normalizedPrefix}${normalizedSuffix}`;
203
+ return requestURL ? new URL(value, requestURL).toString() : value;
204
+ }
205
+ function replaceCookieValue(cookieHeader, name, value) {
206
+ const parts = cookieHeader.split(";").map((part) => part.trim()).filter(Boolean).filter((part) => part.slice(0, part.indexOf("=")).trim() !== name);
207
+ parts.push(`${name}=${value}`);
208
+ return parts.join("; ");
209
+ }
210
+ function readSetCookieValue(setCookie, name) {
211
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
212
+ const match = setCookie.match(new RegExp(`(?:^|,\\s*)${escaped}=([^;]*)`));
213
+ return match?.[1] || void 0;
214
+ }
215
+ function isCookieName(value) {
216
+ return /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(value);
217
+ }
218
+ function isCookieValue(value) {
219
+ return value.length > 0 && !/[\r\n;]/.test(value);
220
+ }
221
+ function isSafeRecoveryEndpoint(value) {
222
+ try {
223
+ const url = new URL(value);
224
+ if (url.username || url.password) return false;
225
+ if (url.protocol === "https:") return true;
226
+ if (url.protocol !== "http:") return false;
227
+ return url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]" || url.hostname === "::1";
228
+ } catch {
229
+ return false;
230
+ }
231
+ }
232
+ var init_sessionRecovery = __esm({
233
+ "src/client/sessionRecovery.ts"() {
234
+ init_sessionCookie();
235
+ __name(requestSessionRecovery, "requestSessionRecovery");
236
+ __name(authEndpoint, "authEndpoint");
237
+ __name(replaceCookieValue, "replaceCookieValue");
238
+ __name(readSetCookieValue, "readSetCookieValue");
239
+ __name(isCookieName, "isCookieName");
240
+ __name(isCookieValue, "isCookieValue");
241
+ __name(isSafeRecoveryEndpoint, "isSafeRecoveryEndpoint");
242
+ }
243
+ });
244
+
147
245
  // src/client/server/getSession.ts
148
246
  var getSession_exports = {};
149
247
  __export(getSession_exports, {
@@ -158,88 +256,77 @@ function defaultBaseURL() {
158
256
  const origin = typeof process !== "undefined" ? process.env.NEXT_PUBLIC_API_URL ?? `http://localhost:${process.env.PORT ?? 3e3}` : "http://localhost:3000";
159
257
  return `${origin.replace(/\/$/, "")}/api`;
160
258
  }
161
- function buildSession(body) {
162
- if (!body?.data) return null;
163
- const { roles, permissions, ...user } = body.data;
164
- return {
165
- user,
166
- roles: roles ?? (user.role ? [user.role] : void 0),
167
- permissions: permissions ?? user.permissions
168
- };
169
- }
170
259
  async function getSession(config = {}) {
171
260
  const cookieName = config.cookieName ?? "refreshToken";
172
261
  const sessionCookieName = config.sessionCookieName ?? "najm.session";
173
262
  const baseURL = config.baseURL ?? defaultBaseURL();
174
263
  const prefix = config.authPrefix ?? "/auth";
175
264
  const strict = config.mode === "strict";
176
- let cookieHeader = "";
177
265
  let sessionCookieValue;
178
- let hasRefreshCookie = false;
266
+ let refreshCookieValue;
179
267
  try {
180
268
  const mod = await import("next/headers");
181
269
  const cookieStore = await mod.cookies();
182
- const sessionCookie = cookieStore.get(sessionCookieName);
183
- if (sessionCookie) sessionCookieValue = sessionCookie.value;
184
- if (typeof cookieStore.get === "function") {
185
- hasRefreshCookie = !!cookieStore.get(cookieName);
186
- }
187
- cookieHeader = cookieStore.getAll().map((c) => `${c.name}=${c.value}`).join("; ");
188
- } catch (err) {
270
+ sessionCookieValue = cookieStore.get(sessionCookieName)?.value;
271
+ refreshCookieValue = cookieStore.get(cookieName)?.value;
272
+ } catch {
189
273
  if (strict) throw new AuthConfigError("Failed to read cookies from Next.js headers()");
190
274
  return null;
191
275
  }
192
- if (sessionCookieValue) {
193
- const secret = resolveSessionSecret(config.sessionSecret);
194
- if (!secret) {
195
- if (strict) throw new AuthConfigError("Session cookie secret is not configured");
196
- return null;
197
- }
276
+ const secret = resolveSessionSecret(config.sessionSecret);
277
+ if (sessionCookieValue && secret) {
198
278
  const claims = await verifySessionCookie(sessionCookieValue, {
199
279
  secret,
200
280
  maxAgeSeconds: config.sessionMaxAge
201
281
  });
202
- if (!claims) {
203
- if (strict) throw new NoSessionError("Invalid or expired session cookie");
204
- return null;
282
+ if (claims) {
283
+ return {
284
+ user: claims.user,
285
+ roles: claims.roles,
286
+ permissions: claims.permissions
287
+ };
205
288
  }
206
- return {
207
- user: claims.user,
208
- roles: claims.roles,
209
- permissions: claims.permissions
210
- };
211
289
  }
212
- if (!hasRefreshCookie) {
213
- if (strict) throw new NoSessionError("No refresh token cookie");
290
+ if (!secret) {
291
+ if (strict) throw new AuthConfigError("Session cookie secret is not configured");
214
292
  return null;
215
293
  }
216
- if (!cookieHeader) {
217
- if (strict) throw new NoSessionError("Empty cookie header");
294
+ if (!refreshCookieValue || config.recoveryURL === false) {
295
+ if (strict) throw new NoSessionError("No recoverable refresh session");
218
296
  return null;
219
297
  }
220
- try {
221
- const res = await fetch(`${baseURL}${prefix}/me`, {
222
- headers: { Cookie: cookieHeader, Accept: "application/json" },
223
- cache: "no-store"
224
- });
225
- if (!res.ok) {
226
- if (strict) throw new AuthTransportError(`/auth/me returned ${res.status}`, res.status);
227
- return null;
298
+ const endpoint = config.recoveryURL ? new URL(config.recoveryURL, baseURL).toString() : authEndpoint(baseURL, prefix, "/session/recover");
299
+ const recovery = await requestSessionRecovery({
300
+ endpoint,
301
+ refreshCookieName: cookieName,
302
+ refreshCookieValue,
303
+ sessionCookieName,
304
+ sessionSecret: secret,
305
+ sessionMaxAge: config.sessionMaxAge
306
+ });
307
+ if (recovery.status === "recovered") {
308
+ return {
309
+ user: recovery.claims.user,
310
+ roles: recovery.claims.roles,
311
+ permissions: recovery.claims.permissions
312
+ };
313
+ }
314
+ if (strict) {
315
+ if (recovery.status === "invalid") {
316
+ throw new NoSessionError("Refresh session is invalid or revoked");
228
317
  }
229
- const body = await res.json();
230
- const session = buildSession(body);
231
- if (!session && strict) throw new NoSessionError("No user data in /auth/me response");
232
- return session;
233
- } catch (err) {
234
- if (err instanceof NoSessionError || err instanceof AuthTransportError) throw err;
235
- if (strict) throw new AuthTransportError(`Failed to fetch /auth/me: ${err.message}`);
236
- return null;
318
+ throw new AuthTransportError(
319
+ "Session recovery endpoint was unavailable or returned an invalid session",
320
+ recovery.httpStatus
321
+ );
237
322
  }
323
+ return null;
238
324
  }
239
325
  var NoSessionError, AuthConfigError, AuthTransportError;
240
326
  var init_getSession = __esm({
241
327
  "src/client/server/getSession.ts"() {
242
328
  init_sessionCookie();
329
+ init_sessionRecovery();
243
330
  NoSessionError = class extends Error {
244
331
  static {
245
332
  __name(this, "NoSessionError");
@@ -272,7 +359,6 @@ var init_getSession = __esm({
272
359
  code = "AUTH_TRANSPORT_ERROR";
273
360
  };
274
361
  __name(defaultBaseURL, "defaultBaseURL");
275
- __name(buildSession, "buildSession");
276
362
  __name(getSession, "getSession");
277
363
  }
278
364
  });
@@ -468,6 +554,7 @@ __name(createServerClient, "createServerClient");
468
554
 
469
555
  // src/client/server/withAuthMiddleware.ts
470
556
  init_sessionCookie();
557
+ init_sessionRecovery();
471
558
  function withAuthMiddleware(config) {
472
559
  const {
473
560
  protectedRoutes = [],
@@ -475,26 +562,31 @@ function withAuthMiddleware(config) {
475
562
  loginRoute = "/login",
476
563
  roleRoutes = {},
477
564
  cookieName = "refreshToken",
565
+ apiBaseURL = "/api",
566
+ authPrefix = "/auth",
478
567
  sessionCookieName = "najm.session",
479
568
  sessionSecret,
480
569
  sessionMaxAge,
481
- verifyAlways = false
570
+ verifyAlways = false,
571
+ recoveryURL
482
572
  } = config;
483
- void verifyAlways;
484
573
  return /* @__PURE__ */ __name(async function middleware(request) {
485
574
  const { NextResponse } = await import("next/server");
486
- const redirectToLogin = /* @__PURE__ */ __name((pathname2, clearCookies) => {
575
+ const redirectToLogin = /* @__PURE__ */ __name((returnPath2, clearCookies) => {
487
576
  const loginUrl = new URL(loginRoute, request.url);
488
- loginUrl.searchParams.set("from", pathname2);
577
+ loginUrl.searchParams.set("from", returnPath2);
489
578
  const res = NextResponse.redirect(loginUrl);
490
- if (clearCookies) {
579
+ if (clearCookies.includes("refresh")) {
491
580
  res.cookies.delete(cookieName);
581
+ }
582
+ if (clearCookies.includes("session")) {
492
583
  res.cookies.delete(sessionCookieName);
493
584
  }
494
585
  return res;
495
586
  }, "redirectToLogin");
496
587
  const url = new URL(request.url);
497
588
  const pathname = url.pathname;
589
+ const returnPath = `${url.pathname}${url.search}`;
498
590
  if (matchesAny(pathname, publicRoutes)) {
499
591
  return NextResponse.next();
500
592
  }
@@ -503,19 +595,53 @@ function withAuthMiddleware(config) {
503
595
  const cookie = request.headers.get("cookie") ?? "";
504
596
  const sessionCookie = readCookieValue(cookie, sessionCookieName);
505
597
  const secret = resolveSessionSecret(sessionSecret);
506
- if (!sessionCookie || !secret) {
507
- return redirectToLogin(pathname, true);
598
+ if (!secret) {
599
+ return redirectToLogin(returnPath, ["session"]);
508
600
  }
509
- const session = await verifySessionCookie(sessionCookie, {
601
+ let session = sessionCookie ? await verifySessionCookie(sessionCookie, {
510
602
  secret,
511
603
  maxAgeSeconds: sessionMaxAge
512
- });
513
- if (!session) {
514
- return redirectToLogin(pathname, true);
604
+ }) : null;
605
+ let recovery = null;
606
+ if (!session || verifyAlways) {
607
+ const refreshCookie = readCookieValue(cookie, cookieName);
608
+ if (!refreshCookie || recoveryURL === false) {
609
+ return redirectToLogin(returnPath, ["refresh", "session"]);
610
+ }
611
+ const endpoint = recoveryURL ? new URL(recoveryURL, request.url).toString() : authEndpoint(apiBaseURL, authPrefix, "/session/recover", request.url);
612
+ recovery = await requestSessionRecovery({
613
+ endpoint,
614
+ refreshCookieName: cookieName,
615
+ refreshCookieValue: refreshCookie,
616
+ sessionCookieName,
617
+ sessionSecret: secret,
618
+ sessionMaxAge
619
+ });
620
+ if (recovery.status !== "recovered") {
621
+ return redirectToLogin(
622
+ returnPath,
623
+ recovery.status === "invalid" ? ["refresh", "session"] : ["session"]
624
+ );
625
+ }
626
+ session = recovery.claims;
515
627
  }
516
628
  const requiredRoles = findMatchingRoles(pathname, roleRoutes);
517
629
  if (requiredRoles && !session.roles.some((role) => requiredRoles.includes(role))) {
518
- return new NextResponse(null, { status: 403 });
630
+ const forbidden = new NextResponse(null, { status: 403 });
631
+ if (recovery?.status === "recovered") {
632
+ forbidden.headers.append("Set-Cookie", recovery.setCookie);
633
+ }
634
+ return forbidden;
635
+ }
636
+ if (recovery?.status === "recovered") {
637
+ const requestHeaders = new Headers(request.headers);
638
+ requestHeaders.set(
639
+ "cookie",
640
+ replaceCookieValue(cookie, sessionCookieName, recovery.sessionCookieValue)
641
+ );
642
+ const response = NextResponse.next({ request: { headers: requestHeaders } });
643
+ response.headers.append("Set-Cookie", recovery.setCookie);
644
+ return response;
519
645
  }
520
646
  return NextResponse.next();
521
647
  }, "middleware");
@@ -1049,6 +1175,7 @@ function defineAuth(authConfig = {}) {
1049
1175
  sessionCookieName = "najm.session",
1050
1176
  sessionSecret,
1051
1177
  sessionMaxAge,
1178
+ recoveryURL,
1052
1179
  matcher = ["/((?!_next/static|_next/image|favicon.ico|api).*)"],
1053
1180
  verifyAlways = false,
1054
1181
  refreshThreshold,
@@ -1064,7 +1191,8 @@ function defineAuth(authConfig = {}) {
1064
1191
  cookieName,
1065
1192
  sessionCookieName,
1066
1193
  sessionSecret,
1067
- sessionMaxAge
1194
+ sessionMaxAge,
1195
+ recoveryURL
1068
1196
  };
1069
1197
  let _client = null;
1070
1198
  const getClient = /* @__PURE__ */ __name(() => {
@@ -1112,9 +1240,12 @@ function defineAuth(authConfig = {}) {
1112
1240
  loginRoute,
1113
1241
  roleRoutes,
1114
1242
  cookieName,
1243
+ apiBaseURL,
1244
+ authPrefix,
1115
1245
  sessionCookieName,
1116
1246
  sessionSecret,
1117
1247
  sessionMaxAge,
1248
+ recoveryURL,
1118
1249
  verifyAlways
1119
1250
  });
1120
1251
  const protect = /* @__PURE__ */ __name((Page, options) => {
package/dist/index.d.ts CHANGED
@@ -9,6 +9,7 @@ import { User, NewUser, RoleEntity, NewRoleEntity, Permission, NewPermission, Ro
9
9
  export { NewOAuthAccount, NewRolePermission, NewToken, OAuthAccount, Token, authSchema, baseFields, oauthAccountsTable, permissionsTable, rolePermissionsTable, rolesTable, tokenStatusEnum, tokenTypeEnum, tokensTable, userStatusEnum, usersTable } from './schema/pg.js';
10
10
  import { CacheService } from 'najm-cache';
11
11
  import { z } from 'zod';
12
+ import { Context } from 'hono';
12
13
  import { GuardResult } from 'najm-guard';
13
14
  import 'drizzle-orm';
14
15
  import 'drizzle-orm/pg-core';
@@ -279,6 +280,7 @@ var auth = {
279
280
  passwordReset: "Password has been reset successfully",
280
281
  accountInviteSent: "Invitation sent successfully",
281
282
  tokenRefreshed: "Token refreshed successfully",
283
+ sessionRecovered: "Session recovered successfully",
282
284
  oauthLogin: "Google sign-in successful",
283
285
  oauthLinked: "Google account linked successfully"
284
286
  },
@@ -396,6 +398,7 @@ declare const AUTH_LOCALES: {
396
398
  passwordReset: string;
397
399
  accountInviteSent: string;
398
400
  tokenRefreshed: string;
401
+ sessionRecovered: string;
399
402
  oauthLogin: string;
400
403
  oauthLinked: string;
401
404
  };
@@ -932,7 +935,21 @@ declare class TokenService {
932
935
  * the active session. Reuse detection and revocation belong to the
933
936
  * rotation path only.
934
937
  */
938
+ private resolveRefreshSessionFromCookie;
935
939
  resolveUserFromCookie(): Promise<string>;
940
+ /**
941
+ * Resolve authoritative claims for signed-session recovery.
942
+ *
943
+ * This deliberately bypasses the 30-second user cache so status, role, and
944
+ * permission changes are reflected when the short session snapshot expires.
945
+ * It validates but never rotates or consumes the refresh token.
946
+ */
947
+ recoverSessionFromCookie(): Promise<{
948
+ user: any;
949
+ roles: any[];
950
+ permissions: any;
951
+ sessionVersion: number;
952
+ }>;
936
953
  getUser(auth: string): Promise<any>;
937
954
  getUserById(userId: string): Promise<any>;
938
955
  private hashToken;
@@ -1009,6 +1026,7 @@ declare class TokenService {
1009
1026
  accessTokenExpiresAt: number;
1010
1027
  refreshTokenExpiresAt: number;
1011
1028
  }>;
1029
+ private requireActiveRefreshUser;
1012
1030
  /** Revoke every refresh session for a user (password change/reset, logout-all). */
1013
1031
  revokeAllForUser(userId: string): Promise<any>;
1014
1032
  /** Revoke a single refresh session (one family). */
@@ -1242,6 +1260,14 @@ declare class AuthService {
1242
1260
  user: SanitizedUser;
1243
1261
  }>;
1244
1262
  refreshTokens(): Promise<TokenPair>;
1263
+ /**
1264
+ * Reissue the short-lived signed session snapshot from a fully validated
1265
+ * refresh session. This path never creates or returns access/refresh tokens
1266
+ * and never rotates the refresh family.
1267
+ */
1268
+ recoverSession(): Promise<{
1269
+ recovered: true;
1270
+ }>;
1245
1271
  logoutUser(userId: string, authorization?: string): Promise<{
1246
1272
  data: any;
1247
1273
  message: string;
@@ -1310,6 +1336,9 @@ declare class AuthController {
1310
1336
  emailSent: boolean;
1311
1337
  }>;
1312
1338
  refreshTokens(): Promise<TokenPair>;
1339
+ recoverSession(recoveryRequest: string | undefined, ctx: Context): Promise<{
1340
+ recovered: true;
1341
+ }>;
1313
1342
  logoutUser(userId: string, authorization?: string): Promise<{
1314
1343
  data: any;
1315
1344
  message: string;
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ var __export = (target, all) => {
6
6
  };
7
7
 
8
8
  // src/AuthPlugin.ts
9
- import { Err as Err10, plugin } from "najm-core";
9
+ import { Err as Err11, plugin } from "najm-core";
10
10
  import { cache } from "najm-cache";
11
11
 
12
12
  // src/auth.tokens.ts
@@ -431,9 +431,9 @@ CookieManager = __decorate2([
431
431
  ], CookieManager);
432
432
 
433
433
  // src/auth/AuthController.ts
434
- import { Controller } from "najm-core";
434
+ import { Controller, Err as Err9 } from "najm-core";
435
435
  import { Get, Post, ResMsg } from "najm-core";
436
- import { Body, User as User2, Headers } from "najm-core";
436
+ import { Body, User as User2, Headers, Ctx } from "najm-core";
437
437
 
438
438
  // src/auth/AuthService.ts
439
439
  import { Injectable as Injectable8, Inject as Inject8 } from "najm-core";
@@ -1638,7 +1638,7 @@ var TokenService = class TokenService2 {
1638
1638
  * the active session. Reuse detection and revocation belong to the
1639
1639
  * rotation path only.
1640
1640
  */
1641
- async resolveUserFromCookie() {
1641
+ async resolveRefreshSessionFromCookie() {
1642
1642
  const refreshToken = this.cookieManager.getRefreshToken();
1643
1643
  if (!refreshToken) {
1644
1644
  Err6(this.t("errors.refreshTokenMissing"));
@@ -1650,14 +1650,35 @@ var TokenService = class TokenService2 {
1650
1650
  }
1651
1651
  const presentedHash = this.hashToken(refreshToken);
1652
1652
  if (presentedHash === stored.token) {
1653
- return userId;
1653
+ return { userId, tokenFamily };
1654
1654
  }
1655
1655
  const canRecover = stored.previousHash && presentedHash === stored.previousHash && stored.previousValidUntil && new Date(stored.previousValidUntil).getTime() > Date.now() && !stored.previousUsedAt;
1656
1656
  if (canRecover) {
1657
- return userId;
1657
+ return { userId, tokenFamily };
1658
1658
  }
1659
1659
  Err6(this.t("errors.refreshTokenInvalid"));
1660
1660
  }
1661
+ async resolveUserFromCookie() {
1662
+ return (await this.resolveRefreshSessionFromCookie()).userId;
1663
+ }
1664
+ /**
1665
+ * Resolve authoritative claims for signed-session recovery.
1666
+ *
1667
+ * This deliberately bypasses the 30-second user cache so status, role, and
1668
+ * permission changes are reflected when the short session snapshot expires.
1669
+ * It validates but never rotates or consumes the refresh token.
1670
+ */
1671
+ async recoverSessionFromCookie() {
1672
+ const { userId, tokenFamily } = await this.resolveRefreshSessionFromCookie();
1673
+ const user = await this.requireActiveRefreshUser(userId, tokenFamily);
1674
+ const sessionVersion = await this.getUserSessionVersion(userId);
1675
+ return {
1676
+ user,
1677
+ roles: user.role ? [user.role] : [],
1678
+ permissions: Array.isArray(user.permissions) ? user.permissions : [],
1679
+ sessionVersion
1680
+ };
1681
+ }
1661
1682
  // ============ USER RETRIEVAL (MAIN METHOD) ============
1662
1683
  async getUser(auth2) {
1663
1684
  if (!auth2)
@@ -1820,6 +1841,7 @@ var TokenService = class TokenService2 {
1820
1841
  Err6(this.t("errors.refreshTokenInvalid"));
1821
1842
  }
1822
1843
  const presentedHash = this.hashToken(refreshToken);
1844
+ await this.requireActiveRefreshUser(userId, tokenFamily);
1823
1845
  if (presentedHash === stored.token) {
1824
1846
  return this.generateTokens(userId, tokenFamily);
1825
1847
  }
@@ -1834,6 +1856,14 @@ var TokenService = class TokenService2 {
1834
1856
  await this.revokeSuspectRefreshFamily(userId, tokenFamily);
1835
1857
  Err6(this.t("errors.refreshTokenInvalid"));
1836
1858
  }
1859
+ async requireActiveRefreshUser(userId, tokenFamily) {
1860
+ const user = await this.tokenRepository.getUser(userId);
1861
+ if (!user || user.status !== "active") {
1862
+ await this.revokeFamily(tokenFamily);
1863
+ Err6(this.t("errors.refreshTokenInvalid"));
1864
+ }
1865
+ return user;
1866
+ }
1837
1867
  /** Revoke every refresh session for a user (password change/reset, logout-all). */
1838
1868
  async revokeAllForUser(userId) {
1839
1869
  return this.tokenRepository.revokeAllForUser(userId);
@@ -2276,6 +2306,27 @@ var AuthService = class AuthService2 {
2276
2306
  const { userId: _userId, tokenFamily: _tokenFamily, roles: _roles, permissions: _permissions, sessionVersion: _sv, ...tokens } = generated;
2277
2307
  return tokens;
2278
2308
  }
2309
+ /**
2310
+ * Reissue the short-lived signed session snapshot from a fully validated
2311
+ * refresh session. This path never creates or returns access/refresh tokens
2312
+ * and never rotates the refresh family.
2313
+ */
2314
+ async recoverSession() {
2315
+ const recovered = await this.tokenService.recoverSessionFromCookie();
2316
+ this.cookieManager.setSessionCookie({
2317
+ user: {
2318
+ id: recovered.user.id,
2319
+ email: recovered.user.email,
2320
+ name: recovered.user.name,
2321
+ role: recovered.user.role ?? void 0,
2322
+ status: recovered.user.status ?? void 0
2323
+ },
2324
+ roles: recovered.roles,
2325
+ permissions: recovered.permissions,
2326
+ sessionVersion: recovered.sessionVersion
2327
+ });
2328
+ return { recovered: true };
2329
+ }
2279
2330
  async logoutUser(userId, authorization) {
2280
2331
  await this.tokenService.logout(userId, authorization);
2281
2332
  this.cookieManager.clearRefreshToken();
@@ -2604,6 +2655,14 @@ var AuthController = class AuthController2 {
2604
2655
  async refreshTokens() {
2605
2656
  return this.authService.refreshTokens();
2606
2657
  }
2658
+ async recoverSession(recoveryRequest, ctx) {
2659
+ if (recoveryRequest !== "1") {
2660
+ Err9("Invalid session recovery request", 400);
2661
+ }
2662
+ ctx.header("Cache-Control", "private, no-store");
2663
+ ctx.header("Vary", "Cookie");
2664
+ return this.authService.recoverSession();
2665
+ }
2607
2666
  async logoutUser(userId, authorization) {
2608
2667
  return this.authService.logoutUser(userId, authorization);
2609
2668
  }
@@ -2659,6 +2718,16 @@ __decorate15([
2659
2718
  __metadata15("design:paramtypes", []),
2660
2719
  __metadata15("design:returntype", Promise)
2661
2720
  ], AuthController.prototype, "refreshTokens", null);
2721
+ __decorate15([
2722
+ Post("/session/recover"),
2723
+ RateLimit({ limit: 120, window: "1m", key: cookieFingerprint() }),
2724
+ ResMsg("auth.success.sessionRecovered"),
2725
+ __param5(0, Headers("x-najm-session-recovery")),
2726
+ __param5(1, Ctx()),
2727
+ __metadata15("design:type", Function),
2728
+ __metadata15("design:paramtypes", [String, Object]),
2729
+ __metadata15("design:returntype", Promise)
2730
+ ], AuthController.prototype, "recoverSession", null);
2662
2731
  __decorate15([
2663
2732
  Post("/logout"),
2664
2733
  isAuth(),
@@ -3471,7 +3540,7 @@ import { Injectable as Injectable11 } from "najm-core";
3471
3540
  // src/permissions/PermissionValidator.ts
3472
3541
  import { Injectable as Injectable10 } from "najm-core";
3473
3542
  import { I18n as I18n7 } from "najm-i18n";
3474
- import { Err as Err9 } from "najm-core";
3543
+ import { Err as Err10 } from "najm-core";
3475
3544
  var __decorate21 = function(decorators, target, key, desc) {
3476
3545
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3477
3546
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -3500,7 +3569,7 @@ var PermissionValidator = class PermissionValidator2 {
3500
3569
  async checkPermissionExists(id) {
3501
3570
  const permission = await this.permissionRepository.getById(id);
3502
3571
  if (!permission) {
3503
- Err9(this.t("errors.notFound"), 404);
3572
+ Err10(this.t("errors.notFound"), 404);
3504
3573
  }
3505
3574
  return permission;
3506
3575
  }
@@ -3510,7 +3579,7 @@ var PermissionValidator = class PermissionValidator2 {
3510
3579
  async checkPermissionExistsByName(name) {
3511
3580
  const permission = await this.permissionRepository.getByName(name);
3512
3581
  if (!permission) {
3513
- Err9(this.t("errors.notFound"), 404);
3582
+ Err10(this.t("errors.notFound"), 404);
3514
3583
  }
3515
3584
  return permission;
3516
3585
  }
@@ -3522,7 +3591,7 @@ var PermissionValidator = class PermissionValidator2 {
3522
3591
  return;
3523
3592
  const existingPermission = await this.permissionRepository.getByName(name);
3524
3593
  if (existingPermission && existingPermission.id !== excludeId) {
3525
- Err9(this.t("errors.nameExists"), 409);
3594
+ Err10(this.t("errors.nameExists"), 409);
3526
3595
  }
3527
3596
  }
3528
3597
  /**
@@ -3545,7 +3614,7 @@ var PermissionValidator = class PermissionValidator2 {
3545
3614
  await this.checkPermissionExists(permissionId);
3546
3615
  const hasPermission = await this.permissionRepository.checkRoleHasPermission(roleId, permissionId);
3547
3616
  if (hasPermission) {
3548
- Err9(this.t("errors.roleAlreadyHasPermission"), 409);
3617
+ Err10(this.t("errors.roleAlreadyHasPermission"), 409);
3549
3618
  }
3550
3619
  }
3551
3620
  };
@@ -4594,6 +4663,7 @@ var en_default = {
4594
4663
  passwordReset: "Password has been reset successfully",
4595
4664
  accountInviteSent: "Invitation sent successfully",
4596
4665
  tokenRefreshed: "Token refreshed successfully",
4666
+ sessionRecovered: "Session recovered successfully",
4597
4667
  oauthLogin: "Google sign-in successful",
4598
4668
  oauthLinked: "Google account linked successfully"
4599
4669
  },
@@ -5010,7 +5080,7 @@ OAuthAccountService = __decorate29([
5010
5080
 
5011
5081
  // src/oauth/OAuthController.ts
5012
5082
  import { createHash as createHash4 } from "crypto";
5013
- import { Controller as Controller5, Ctx, Get as Get5, Post as Post5, Query as Query2, User as User5 } from "najm-core";
5083
+ import { Controller as Controller5, Ctx as Ctx2, Get as Get5, Post as Post5, Query as Query2, User as User5 } from "najm-core";
5014
5084
  import { RateLimit as RateLimit2 } from "najm-rate";
5015
5085
 
5016
5086
  // src/oauth/OAuthService.ts
@@ -5296,7 +5366,7 @@ var OAuthController = class OAuthController2 {
5296
5366
  __decorate32([
5297
5367
  Get5("/start"),
5298
5368
  RateLimit2({ limit: 20, window: "15m", key: "ip" }),
5299
- __param11(0, Ctx()),
5369
+ __param11(0, Ctx2()),
5300
5370
  __param11(1, Query2("returnTo")),
5301
5371
  __metadata32("design:type", Function),
5302
5372
  __metadata32("design:paramtypes", [Object, String]),
@@ -5305,7 +5375,7 @@ __decorate32([
5305
5375
  __decorate32([
5306
5376
  Get5("/callback"),
5307
5377
  RateLimit2({ limit: 20, window: "15m", key: callbackKey }),
5308
- __param11(0, Ctx()),
5378
+ __param11(0, Ctx2()),
5309
5379
  __param11(1, Query2("code")),
5310
5380
  __param11(2, Query2("state")),
5311
5381
  __param11(3, Query2("error")),
@@ -5360,9 +5430,9 @@ var resolveGoogleConfig = /* @__PURE__ */ __name((config) => {
5360
5430
  const clientId = google.clientId ?? process.env.GOOGLE_CLIENT_ID ?? "";
5361
5431
  const clientSecret = google.clientSecret ?? process.env.GOOGLE_CLIENT_SECRET ?? "";
5362
5432
  if (!clientId)
5363
- throw Err10.configRequired("auth.oauth.google", "GOOGLE_CLIENT_ID");
5433
+ throw Err11.configRequired("auth.oauth.google", "GOOGLE_CLIENT_ID");
5364
5434
  if (!clientSecret)
5365
- throw Err10.configRequired("auth.oauth.google", "GOOGLE_CLIENT_SECRET");
5435
+ throw Err11.configRequired("auth.oauth.google", "GOOGLE_CLIENT_SECRET");
5366
5436
  const frontendUrl = config?.frontendUrl ?? process.env.FRONTEND_URL ?? "http://localhost:3000";
5367
5437
  const callbackUrl = google.callbackUrl ?? process.env.GOOGLE_CALLBACK_URL ?? `${frontendUrl.replace(/\/$/, "")}/api/auth/oauth/google/callback`;
5368
5438
  let callback;
@@ -5421,10 +5491,10 @@ var resolveAuthConfig = /* @__PURE__ */ __name((config) => {
5421
5491
  }
5422
5492
  };
5423
5493
  if (!finalConfig.jwt.accessSecret) {
5424
- throw Err10.configRequired("auth", "JWT_ACCESS_SECRET");
5494
+ throw Err11.configRequired("auth", "JWT_ACCESS_SECRET");
5425
5495
  }
5426
5496
  if (!finalConfig.jwt.refreshSecret) {
5427
- throw Err10.configRequired("auth", "JWT_REFRESH_SECRET");
5497
+ throw Err11.configRequired("auth", "JWT_REFRESH_SECRET");
5428
5498
  }
5429
5499
  return finalConfig;
5430
5500
  }, "resolveAuthConfig");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "najm-auth",
3
- "version": "2.0.4",
3
+ "version": "2.0.5",
4
4
  "description": "Authentication and authorization library for najm framework",
5
5
  "type": "module",
6
6
  "files": [