najm-auth 2.0.13 → 2.0.15

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.
@@ -222,6 +222,14 @@ declare class NajmAuthClient {
222
222
  */
223
223
  hydrate(session: HydrateSession | null): void;
224
224
  isHydrated(): boolean;
225
+ /**
226
+ * A fresh client with the same config — unhydrated, and without tab sync.
227
+ *
228
+ * Server rendering needs one client per request. A single process serves
229
+ * every user, so the hydration latch on a shared client would otherwise pin
230
+ * every later render to the first request's session.
231
+ */
232
+ fork(): NajmAuthClient;
225
233
  on<K extends AuthEvent>(event: K, handler: AuthEventHandler<K>): void;
226
234
  off<K extends AuthEvent>(event: K, handler: AuthEventHandler<K>): void;
227
235
  subscribe(listener: (state: AuthState) => void): () => void;
@@ -1,5 +1,5 @@
1
- import { D as DecodedToken, T as TabSyncMessage, S as SyncPayload } from '../NajmAuthClient-0yzFb9CR.js';
2
- export { A as AuthClientConfig, a as AuthError, b as AuthEvent, c as AuthEventHandler, d as AuthEventMap, e as AuthState, f as AuthUser, F as FetchClient, H as HydrateSession, N as NajmAuthClient, O as OAuthLoginOptions, g as OAuthProvider, R as RequestOptions, h as RetryConfig, i as ServerResponse, j as TokenPair, k as createAuthClient } from '../NajmAuthClient-0yzFb9CR.js';
1
+ import { D as DecodedToken, T as TabSyncMessage, S as SyncPayload } from '../NajmAuthClient-Cn9bObLB.js';
2
+ export { A as AuthClientConfig, a as AuthError, b as AuthEvent, c as AuthEventHandler, d as AuthEventMap, e as AuthState, f as AuthUser, F as FetchClient, H as HydrateSession, N as NajmAuthClient, O as OAuthLoginOptions, g as OAuthProvider, R as RequestOptions, h as RetryConfig, i as ServerResponse, j as TokenPair, k as createAuthClient } from '../NajmAuthClient-Cn9bObLB.js';
3
3
 
4
4
  /**
5
5
  * Decode a JWT token payload without verification.
@@ -422,6 +422,16 @@ var NajmAuthClient = class _NajmAuthClient {
422
422
  isHydrated() {
423
423
  return this._hydrated;
424
424
  }
425
+ /**
426
+ * A fresh client with the same config — unhydrated, and without tab sync.
427
+ *
428
+ * Server rendering needs one client per request. A single process serves
429
+ * every user, so the hydration latch on a shared client would otherwise pin
430
+ * every later render to the first request's session.
431
+ */
432
+ fork() {
433
+ return new _NajmAuthClient({ ...this.config, tabSync: false });
434
+ }
425
435
  // =========================================================================
426
436
  // Events
427
437
  // =========================================================================
@@ -505,6 +515,7 @@ var NajmAuthClient = class _NajmAuthClient {
505
515
  }
506
516
  scheduleRefresh(decoded) {
507
517
  this.clearRefreshTimer();
518
+ if (typeof window === "undefined") return;
508
519
  const ttl = getTokenTTL(decoded);
509
520
  if (ttl <= 0) return;
510
521
  const delay = ttl * this.threshold * 1e3;
@@ -1,7 +1,7 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import * as react from 'react';
3
3
  import { ReactNode, CSSProperties, ReactElement } from 'react';
4
- import { N as NajmAuthClient, H as HydrateSession, e as AuthState, f as AuthUser, a as AuthError, O as OAuthLoginOptions, b as AuthEvent, d as AuthEventMap } from '../../NajmAuthClient-0yzFb9CR.js';
4
+ import { N as NajmAuthClient, H as HydrateSession, e as AuthState, f as AuthUser, a as AuthError, O as OAuthLoginOptions, b as AuthEvent, d as AuthEventMap } from '../../NajmAuthClient-Cn9bObLB.js';
5
5
 
6
6
  interface AuthProviderProps {
7
7
  client: NajmAuthClient;
@@ -3,7 +3,7 @@ var __defProp = Object.defineProperty;
3
3
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
4
4
 
5
5
  // src/client/react/AuthProvider.tsx
6
- import { useEffect, useRef } from "react";
6
+ import { useEffect, useRef, useState } from "react";
7
7
 
8
8
  // src/client/react/context.ts
9
9
  import { createContext, useContext } from "react";
@@ -49,12 +49,15 @@ function AuthProvider({
49
49
  initialSession,
50
50
  autoRefresh = true
51
51
  }) {
52
+ const [active] = useState(
53
+ () => typeof window === "undefined" && initialSession !== void 0 ? client.fork() : client
54
+ );
52
55
  const hydrated = useRef(false);
53
56
  if (!hydrated.current && initialSession !== void 0) {
54
- client.hydrate(initialSession);
57
+ active.hydrate(initialSession);
55
58
  hydrated.current = true;
56
59
  }
57
- return /* @__PURE__ */ jsxs(AuthClientContext.Provider, { value: client, children: [
60
+ return /* @__PURE__ */ jsxs(AuthClientContext.Provider, { value: active, children: [
58
61
  autoRefresh ? /* @__PURE__ */ jsx(AutoRefresh, {}) : null,
59
62
  children
60
63
  ] });
@@ -98,11 +101,11 @@ function useUser() {
98
101
  __name(useUser, "useUser");
99
102
 
100
103
  // src/client/react/useSession.ts
101
- import { useEffect as useEffect2, useRef as useRef2, useState } from "react";
104
+ import { useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
102
105
  function useSession(opts) {
103
106
  const client = useAuthClient();
104
107
  const state = useAuth();
105
- const [isLoading, setIsLoading] = useState(() => !client.isHydrated());
108
+ const [isLoading, setIsLoading] = useState2(() => !client.isHydrated());
106
109
  const didInit = useRef2(false);
107
110
  useEffect2(() => {
108
111
  if (didInit.current) return;
@@ -145,11 +148,11 @@ function usePermissions() {
145
148
  __name(usePermissions, "usePermissions");
146
149
 
147
150
  // src/client/react/useLogin.ts
148
- import { useCallback, useState as useState2 } from "react";
151
+ import { useCallback, useState as useState3 } from "react";
149
152
  function useLogin(opts) {
150
153
  const client = useAuthClient();
151
- const [isLoading, setIsLoading] = useState2(false);
152
- const [error, setError] = useState2(null);
154
+ const [isLoading, setIsLoading] = useState3(false);
155
+ const [error, setError] = useState3(null);
153
156
  const login = useCallback(async (credentials) => {
154
157
  setIsLoading(true);
155
158
  setError(null);
@@ -169,11 +172,11 @@ function useLogin(opts) {
169
172
  __name(useLogin, "useLogin");
170
173
 
171
174
  // src/client/react/useRegister.ts
172
- import { useCallback as useCallback2, useState as useState3 } from "react";
175
+ import { useCallback as useCallback2, useState as useState4 } from "react";
173
176
  function useRegister(opts) {
174
177
  const client = useAuthClient();
175
- const [isLoading, setIsLoading] = useState3(false);
176
- const [error, setError] = useState3(null);
178
+ const [isLoading, setIsLoading] = useState4(false);
179
+ const [error, setError] = useState4(null);
177
180
  const register = useCallback2(async (data) => {
178
181
  setIsLoading(true);
179
182
  setError(null);
@@ -193,10 +196,10 @@ function useRegister(opts) {
193
196
  __name(useRegister, "useRegister");
194
197
 
195
198
  // src/client/react/useLogout.ts
196
- import { useCallback as useCallback3, useState as useState4 } from "react";
199
+ import { useCallback as useCallback3, useState as useState5 } from "react";
197
200
  function useLogout(opts) {
198
201
  const client = useAuthClient();
199
- const [isLoading, setIsLoading] = useState4(false);
202
+ const [isLoading, setIsLoading] = useState5(false);
200
203
  const logout = useCallback3(async () => {
201
204
  setIsLoading(true);
202
205
  try {
@@ -218,12 +221,12 @@ function useLogout(opts) {
218
221
  __name(useLogout, "useLogout");
219
222
 
220
223
  // src/client/react/useForgotPassword.ts
221
- import { useCallback as useCallback4, useState as useState5 } from "react";
224
+ import { useCallback as useCallback4, useState as useState6 } from "react";
222
225
  function useForgotPassword(opts) {
223
226
  const client = useAuthClient();
224
- const [isLoading, setIsLoading] = useState5(false);
225
- const [error, setError] = useState5(null);
226
- const [isSuccess, setIsSuccess] = useState5(false);
227
+ const [isLoading, setIsLoading] = useState6(false);
228
+ const [error, setError] = useState6(null);
229
+ const [isSuccess, setIsSuccess] = useState6(false);
227
230
  const forgotPassword = useCallback4(async (data) => {
228
231
  setIsLoading(true);
229
232
  setError(null);
@@ -249,12 +252,12 @@ function useForgotPassword(opts) {
249
252
  __name(useForgotPassword, "useForgotPassword");
250
253
 
251
254
  // src/client/react/useResetPassword.ts
252
- import { useCallback as useCallback5, useState as useState6 } from "react";
255
+ import { useCallback as useCallback5, useState as useState7 } from "react";
253
256
  function useResetPassword(opts) {
254
257
  const client = useAuthClient();
255
- const [isLoading, setIsLoading] = useState6(false);
256
- const [error, setError] = useState6(null);
257
- const [isSuccess, setIsSuccess] = useState6(false);
258
+ const [isLoading, setIsLoading] = useState7(false);
259
+ const [error, setError] = useState7(null);
260
+ const [isSuccess, setIsSuccess] = useState7(false);
258
261
  const resetPassword = useCallback5(async (data) => {
259
262
  setIsLoading(true);
260
263
  setError(null);
@@ -280,12 +283,12 @@ function useResetPassword(opts) {
280
283
  __name(useResetPassword, "useResetPassword");
281
284
 
282
285
  // src/client/react/useChangePassword.ts
283
- import { useCallback as useCallback6, useState as useState7 } from "react";
286
+ import { useCallback as useCallback6, useState as useState8 } from "react";
284
287
  function useChangePassword(opts) {
285
288
  const client = useAuthClient();
286
- const [isLoading, setIsLoading] = useState7(false);
287
- const [error, setError] = useState7(null);
288
- const [isSuccess, setIsSuccess] = useState7(false);
289
+ const [isLoading, setIsLoading] = useState8(false);
290
+ const [error, setError] = useState8(null);
291
+ const [isSuccess, setIsSuccess] = useState8(false);
289
292
  const changePassword = useCallback6(async (data) => {
290
293
  setIsLoading(true);
291
294
  setError(null);
@@ -311,11 +314,11 @@ function useChangePassword(opts) {
311
314
  __name(useChangePassword, "useChangePassword");
312
315
 
313
316
  // src/client/react/useGoogleLogin.ts
314
- import { useCallback as useCallback7, useState as useState8 } from "react";
317
+ import { useCallback as useCallback7, useState as useState9 } from "react";
315
318
  function useGoogleLogin(options) {
316
319
  const client = useAuthClient();
317
- const [isRedirecting, setIsRedirecting] = useState8(false);
318
- const [error, setError] = useState8(null);
320
+ const [isRedirecting, setIsRedirecting] = useState9(false);
321
+ const [error, setError] = useState9(null);
319
322
  const fail = useCallback7((value) => {
320
323
  const next = value instanceof Error ? value : new Error(String(value));
321
324
  setIsRedirecting(false);
@@ -345,12 +348,12 @@ function useGoogleLogin(options) {
345
348
  __name(useGoogleLogin, "useGoogleLogin");
346
349
 
347
350
  // src/client/react/useOAuthCallback.ts
348
- import { useCallback as useCallback8, useRef as useRef3, useState as useState9 } from "react";
351
+ import { useCallback as useCallback8, useRef as useRef3, useState as useState10 } from "react";
349
352
  function useOAuthCallback(options) {
350
353
  const client = useAuthClient();
351
354
  const promise = useRef3(null);
352
- const [isLoading, setIsLoading] = useState9(false);
353
- const [error, setError] = useState9(null);
355
+ const [isLoading, setIsLoading] = useState10(false);
356
+ const [error, setError] = useState10(null);
354
357
  const complete = useCallback8(async () => {
355
358
  if (promise.current) return promise.current;
356
359
  setIsLoading(true);
@@ -388,7 +391,7 @@ function useAuthEvent(event, handler) {
388
391
  __name(useAuthEvent, "useAuthEvent");
389
392
 
390
393
  // src/client/react/useAuthEvents.ts
391
- import { useEffect as useEffect4, useRef as useRef5, useState as useState10 } from "react";
394
+ import { useEffect as useEffect4, useRef as useRef5, useState as useState11 } from "react";
392
395
  var ALL_EVENTS = [
393
396
  "login",
394
397
  "logout",
@@ -401,7 +404,7 @@ var ALL_EVENTS = [
401
404
  function useAuthEvents(opts) {
402
405
  const client = useAuthClient();
403
406
  const max = opts?.maxEntries ?? 100;
404
- const [entries, setEntries] = useState10([]);
407
+ const [entries, setEntries] = useState11([]);
405
408
  const maxRef = useRef5(max);
406
409
  maxRef.current = max;
407
410
  useEffect4(() => {
@@ -1,4 +1,4 @@
1
- import { f as AuthUser, F as FetchClient, N as NajmAuthClient, h as RetryConfig } from '../../NajmAuthClient-0yzFb9CR.js';
1
+ import { f as AuthUser, F as FetchClient, N as NajmAuthClient, h as RetryConfig } from '../../NajmAuthClient-Cn9bObLB.js';
2
2
  import { SessionRecoveryFailure } from '../edge.js';
3
3
  export { SessionRecoveryErrorDetails, SessionRecoveryFailureReason, withAuthMiddleware } from '../edge.js';
4
4
  import 'next/server';
@@ -169,6 +169,16 @@ interface DefineAuthConfig {
169
169
  loginRoute?: string;
170
170
  /** Route to redirect after login (default: '/dashboard') */
171
171
  afterLoginRoute?: string;
172
+ /**
173
+ * Where an *authenticated* user goes when their role is not allowed
174
+ * (default: '/forbidden').
175
+ *
176
+ * Distinct from `loginRoute` on purpose. Sending them to the login form says
177
+ * "prove who you are" to someone who already has; they log in again, land
178
+ * back on the same page, and get bounced again. A forbidden page is the only
179
+ * response that terminates.
180
+ */
181
+ forbiddenRoute?: string;
172
182
  /** Routes that are always public (glob patterns) */
173
183
  publicRoutes?: string[];
174
184
  /** Routes that require authentication (glob patterns) */
@@ -212,6 +222,15 @@ interface AuthKit {
212
222
  getSession: (opts?: Pick<GetSessionConfig, 'mode'>) => Promise<ServerSession | null>;
213
223
  /** Require session — throws if unauthenticated */
214
224
  requireSession: () => Promise<ServerSession>;
225
+ /**
226
+ * Require one of `roles` — redirects to `loginRoute` when unauthenticated and
227
+ * to `forbiddenRoute` when authenticated as the wrong role.
228
+ *
229
+ * ```ts
230
+ * const session = await auth.requireRole(['admin', 'operator']);
231
+ * ```
232
+ */
233
+ requireRole: (roles: string[]) => Promise<ServerSession>;
215
234
  /** Generated Next.js middleware function */
216
235
  middleware: (request: Request) => Promise<Response>;
217
236
  /** Next.js middleware config with matcher */
@@ -232,4 +251,78 @@ interface AuthKit {
232
251
  }
233
252
  declare function defineAuth(authConfig?: DefineAuthConfig): AuthKit;
234
253
 
235
- export { AuthConfigError, type AuthKit, AuthTransportError, type DefineAuthConfig, type GetSessionConfig, NoSessionError, type ServerSession, SessionRecoveryFailure, type WithAuthOptions, type WithAuthProps, createServerClient, defineAuth, getServerSession, getSession, withAuth };
254
+ interface SafeRedirectOptions {
255
+ /** Where to send anything rejected. Defaults to `/dashboard`. */
256
+ fallback?: string;
257
+ /**
258
+ * Path prefixes that are never a valid destination. Defaults to `/api`,
259
+ * `/login` and `/_next`.
260
+ *
261
+ * `/login` is on the list because bouncing back to it is the redirect loop
262
+ * this parameter causes most often: a user who just authenticated is sent
263
+ * straight back to the form they came from.
264
+ */
265
+ blockedPrefixes?: string[];
266
+ }
267
+ /**
268
+ * Reduces an untrusted `?next=` value to a path that is safe to redirect to.
269
+ *
270
+ * Only same-origin *paths* survive. An absolute URL is rejected outright rather
271
+ * than parsed and compared, because the comparison is where this goes wrong:
272
+ * `//evil.test` is a protocol-relative URL that browsers resolve off-site while
273
+ * a naive `startsWith('/')` check reads it as local. Anything that is not a
274
+ * single leading slash followed by a path is refused.
275
+ *
276
+ * ```ts
277
+ * redirect(getSafeRedirectPath(searchParams.next, { fallback: '/home' }));
278
+ * ```
279
+ */
280
+ declare function getSafeRedirectPath(value: string | string[] | undefined | null, options?: SafeRedirectOptions | string): string;
281
+
282
+ type RequestHandler = (request: Request) => Promise<Response>;
283
+ interface AuthCookiePersistenceOptions {
284
+ /**
285
+ * Cookies whose lifetime this rewrites. Defaults to the two najm-auth issues.
286
+ * Anything not named here is passed through untouched.
287
+ */
288
+ authCookieNames?: string[];
289
+ /** Where the one-bit choice is stored. Defaults to `najm.remember`. */
290
+ rememberCookieName?: string;
291
+ /** How long a remembered choice lasts. Defaults to 7 days. */
292
+ maxAgeSeconds?: number;
293
+ /** Paths whose JSON body carries `rememberMe`. Defaults to `/api/auth/login`. */
294
+ loginPaths?: string[];
295
+ /** Paths that end a session and clear the choice. Defaults to `/api/auth/logout`. */
296
+ logoutPaths?: string[];
297
+ /** Paths that reissue cookies and must reapply the stored choice. */
298
+ refreshPaths?: string[];
299
+ /**
300
+ * Recognizes a response that has *not* issued a usable session because the
301
+ * user must still set up credentials.
302
+ *
303
+ * Such a response may carry auth cookies anyway, and persisting them would
304
+ * leave a half-authenticated browser that skips the setup step on reload.
305
+ * Returning `true` strips them and clears the stored choice.
306
+ */
307
+ isSetupResponse?: (payload: unknown) => boolean;
308
+ }
309
+ /**
310
+ * Strips the lifetime attributes so the browser drops the cookie when it closes.
311
+ *
312
+ * Only the named auth cookies are touched — rewriting an unrelated `Set-Cookie`
313
+ * from the same response would be a silent side effect on someone else's state.
314
+ */
315
+ declare function makeSessionCookie(setCookie: string, authCookieNames?: string[]): string;
316
+ /**
317
+ * Wraps a request handler so the auth cookies it issues match the user's
318
+ * "remember me" choice.
319
+ *
320
+ * ```ts
321
+ * // app/api/[...route]/route.ts
322
+ * const handler = withAuthCookiePersistence((req) => server.fetch(req));
323
+ * export { handler as GET, handler as POST };
324
+ * ```
325
+ */
326
+ declare function withAuthCookiePersistence(handler: RequestHandler, options?: AuthCookiePersistenceOptions): RequestHandler;
327
+
328
+ export { AuthConfigError, type AuthCookiePersistenceOptions, type AuthKit, AuthTransportError, type DefineAuthConfig, type GetSessionConfig, NoSessionError, type SafeRedirectOptions, type ServerSession, SessionRecoveryFailure, type WithAuthOptions, type WithAuthProps, createServerClient, defineAuth, getSafeRedirectPath, getServerSession, getSession, makeSessionCookie, withAuth, withAuthCookiePersistence };
@@ -382,7 +382,7 @@ function requestOriginFromHeaders(headers) {
382
382
  }
383
383
  }
384
384
  async function getSession(config = {}) {
385
- const cookieName = config.cookieName ?? "refreshToken";
385
+ const cookieName2 = config.cookieName ?? "refreshToken";
386
386
  const sessionCookieName = config.sessionCookieName ?? "najm.session";
387
387
  const baseURL = config.baseURL ?? defaultBaseURL();
388
388
  const prefix = config.authPrefix ?? "/auth";
@@ -395,7 +395,7 @@ async function getSession(config = {}) {
395
395
  const mod = await import("next/headers");
396
396
  const cookieStore = await mod.cookies();
397
397
  sessionCookieValue = cookieStore.get(sessionCookieName)?.value;
398
- refreshCookieValue = cookieStore.get(cookieName)?.value;
398
+ refreshCookieValue = cookieStore.get(cookieName2)?.value;
399
399
  if (typeof mod.headers === "function") {
400
400
  requestOrigin = requestOriginFromHeaders(await mod.headers());
401
401
  }
@@ -434,7 +434,7 @@ async function getSession(config = {}) {
434
434
  endpoint,
435
435
  requestOrigin,
436
436
  allowLoopbackEndpoint: internalRecoveryURL !== void 0,
437
- refreshCookieName: cookieName,
437
+ refreshCookieName: cookieName2,
438
438
  refreshCookieValue,
439
439
  sessionCookieName,
440
440
  sessionSecret: secret,
@@ -700,7 +700,7 @@ function withAuthMiddleware(config) {
700
700
  publicRoutes = [],
701
701
  loginRoute = "/login",
702
702
  roleRoutes = {},
703
- cookieName = "refreshToken",
703
+ cookieName: cookieName2 = "refreshToken",
704
704
  apiBaseURL = "/api",
705
705
  authPrefix = "/auth",
706
706
  sessionCookieName = "najm.session",
@@ -719,7 +719,7 @@ function withAuthMiddleware(config) {
719
719
  loginUrl.searchParams.set("from", returnPath2);
720
720
  const res = NextResponse.redirect(loginUrl);
721
721
  if (clearCookies.includes("refresh")) {
722
- res.cookies.delete(cookieName);
722
+ res.cookies.delete(cookieName2);
723
723
  }
724
724
  if (clearCookies.includes("session")) {
725
725
  res.cookies.delete(sessionCookieName);
@@ -746,7 +746,7 @@ function withAuthMiddleware(config) {
746
746
  }) : null;
747
747
  let recovery = null;
748
748
  if (!session || verifyAlways) {
749
- const refreshCookie = readCookieValue(cookie, cookieName);
749
+ const refreshCookie = readCookieValue(cookie, cookieName2);
750
750
  if (!refreshCookie || recoveryURL === false && !resolvedInternalRecoveryURL) {
751
751
  return redirectToLogin(returnPath, ["refresh", "session"]);
752
752
  }
@@ -755,7 +755,7 @@ function withAuthMiddleware(config) {
755
755
  endpoint,
756
756
  requestOrigin: url.origin,
757
757
  allowLoopbackEndpoint: resolvedInternalRecoveryURL !== void 0,
758
- refreshCookieName: cookieName,
758
+ refreshCookieName: cookieName2,
759
759
  refreshCookieValue: refreshCookie,
760
760
  sessionCookieName,
761
761
  sessionSecret: secret,
@@ -1119,6 +1119,16 @@ var NajmAuthClient = class _NajmAuthClient {
1119
1119
  isHydrated() {
1120
1120
  return this._hydrated;
1121
1121
  }
1122
+ /**
1123
+ * A fresh client with the same config — unhydrated, and without tab sync.
1124
+ *
1125
+ * Server rendering needs one client per request. A single process serves
1126
+ * every user, so the hydration latch on a shared client would otherwise pin
1127
+ * every later render to the first request's session.
1128
+ */
1129
+ fork() {
1130
+ return new _NajmAuthClient({ ...this.config, tabSync: false });
1131
+ }
1122
1132
  // =========================================================================
1123
1133
  // Events
1124
1134
  // =========================================================================
@@ -1202,6 +1212,7 @@ var NajmAuthClient = class _NajmAuthClient {
1202
1212
  }
1203
1213
  scheduleRefresh(decoded) {
1204
1214
  this.clearRefreshTimer();
1215
+ if (typeof window === "undefined") return;
1205
1216
  const ttl = getTokenTTL(decoded);
1206
1217
  if (ttl <= 0) return;
1207
1218
  const delay = ttl * this.threshold * 1e3;
@@ -1313,10 +1324,11 @@ function defineAuth(authConfig = {}) {
1313
1324
  apiBaseURL = "/api",
1314
1325
  authPrefix = "/auth",
1315
1326
  loginRoute = "/login",
1327
+ forbiddenRoute = "/forbidden",
1316
1328
  publicRoutes = [],
1317
1329
  protectedRoutes = [],
1318
1330
  roleRoutes = {},
1319
- cookieName = "refreshToken",
1331
+ cookieName: cookieName2 = "refreshToken",
1320
1332
  sessionCookieName = "najm.session",
1321
1333
  sessionSecret,
1322
1334
  sessionMaxAge,
@@ -1334,7 +1346,7 @@ function defineAuth(authConfig = {}) {
1334
1346
  const sessionConfig = {
1335
1347
  baseURL: apiBaseURL,
1336
1348
  authPrefix,
1337
- cookieName,
1349
+ cookieName: cookieName2,
1338
1350
  sessionCookieName,
1339
1351
  sessionSecret,
1340
1352
  sessionMaxAge,
@@ -1382,12 +1394,21 @@ function defineAuth(authConfig = {}) {
1382
1394
  throw err;
1383
1395
  }
1384
1396
  }, "requireSession");
1397
+ const requireRole = /* @__PURE__ */ __name(async (roles) => {
1398
+ const session = await requireSession();
1399
+ const held = session.roles ?? (session.user.role ? [session.user.role] : []);
1400
+ if (!held.some((role) => roles.includes(role))) {
1401
+ const { redirect } = await import("next/navigation");
1402
+ redirect(forbiddenRoute);
1403
+ }
1404
+ return session;
1405
+ }, "requireRole");
1385
1406
  const middleware = withAuthMiddleware({
1386
1407
  protectedRoutes,
1387
1408
  publicRoutes,
1388
1409
  loginRoute,
1389
1410
  roleRoutes,
1390
- cookieName,
1411
+ cookieName: cookieName2,
1391
1412
  apiBaseURL,
1392
1413
  authPrefix,
1393
1414
  sessionCookieName,
@@ -1409,7 +1430,7 @@ function defineAuth(authConfig = {}) {
1409
1430
  const userRoles = session.roles ?? (session.user.role ? [session.user.role] : []);
1410
1431
  if (!userRoles.includes(options.role)) {
1411
1432
  const { redirect } = await import("next/navigation");
1412
- redirect(loginRoute);
1433
+ redirect(forbiddenRoute);
1413
1434
  }
1414
1435
  }
1415
1436
  if (options?.permission) {
@@ -1417,7 +1438,7 @@ function defineAuth(authConfig = {}) {
1417
1438
  const perms = session.permissions ?? session.user.permissions ?? [];
1418
1439
  if (!matchPermission2(perms, options.permission)) {
1419
1440
  const { redirect } = await import("next/navigation");
1420
- redirect(loginRoute);
1441
+ redirect(forbiddenRoute);
1421
1442
  }
1422
1443
  }
1423
1444
  return Page({ session, ...props });
@@ -1432,20 +1453,173 @@ function defineAuth(authConfig = {}) {
1432
1453
  },
1433
1454
  getSession: getSession2,
1434
1455
  requireSession,
1456
+ requireRole,
1435
1457
  middleware,
1436
1458
  config: { matcher },
1437
1459
  protect
1438
1460
  };
1439
1461
  }
1440
1462
  __name(defineAuth, "defineAuth");
1463
+
1464
+ // src/client/server/safeRedirect.ts
1465
+ var DEFAULT_BLOCKED_PREFIXES = ["/api", "/login", "/_next"];
1466
+ var ASSET_EXTENSIONS = /\.(?:css|gif|ico|jpe?g|js|json|map|png|svg|webmanifest|webp)$/i;
1467
+ function getSafeRedirectPath(value, options = {}) {
1468
+ const {
1469
+ fallback = "/dashboard",
1470
+ blockedPrefixes = DEFAULT_BLOCKED_PREFIXES
1471
+ } = typeof options === "string" ? { fallback: options } : options;
1472
+ const path = Array.isArray(value) ? value[0] : value;
1473
+ if (!path || !path.startsWith("/") || // Protocol-relative: the browser treats `//host/x` as off-site.
1474
+ path.startsWith("//") || // A backslash is normalized to a forward slash by some browsers, so
1475
+ // `/\evil.test` is another way to spell the case above.
1476
+ path.startsWith("/\\") || blockedPrefixes.some((prefix) => path.startsWith(prefix)) || ASSET_EXTENSIONS.test(path.split("?")[0] ?? path)) {
1477
+ return fallback;
1478
+ }
1479
+ return path;
1480
+ }
1481
+ __name(getSafeRedirectPath, "getSafeRedirectPath");
1482
+
1483
+ // src/client/server/authCookiePersistence.ts
1484
+ var DEFAULTS = {
1485
+ authCookieNames: ["refreshToken", "najm.session"],
1486
+ rememberCookieName: "najm.remember",
1487
+ maxAgeSeconds: 7 * 24 * 60 * 60,
1488
+ loginPaths: ["/api/auth/login"],
1489
+ logoutPaths: ["/api/auth/logout"],
1490
+ refreshPaths: ["/api/auth/refresh", "/api/auth/session/recover"]
1491
+ };
1492
+ function cookieValue(header, name) {
1493
+ for (const part of header.split(";")) {
1494
+ const separator = part.indexOf("=");
1495
+ if (separator < 0) continue;
1496
+ if (part.slice(0, separator).trim() !== name) continue;
1497
+ return part.slice(separator + 1).trim();
1498
+ }
1499
+ return void 0;
1500
+ }
1501
+ __name(cookieValue, "cookieValue");
1502
+ function cookieName(setCookie) {
1503
+ const separator = setCookie.indexOf("=");
1504
+ return separator < 0 ? "" : setCookie.slice(0, separator).trim();
1505
+ }
1506
+ __name(cookieName, "cookieName");
1507
+ function makeSessionCookie(setCookie, authCookieNames = DEFAULTS.authCookieNames) {
1508
+ if (!authCookieNames.includes(cookieName(setCookie))) return setCookie;
1509
+ return setCookie.split(";").filter((part) => !/^\s*(?:expires|max-age)=/i.test(part)).join(";");
1510
+ }
1511
+ __name(makeSessionCookie, "makeSessionCookie");
1512
+ function isDeletionCookie(setCookie) {
1513
+ if (/^[^=]+=\s*(?:;|$)/.test(setCookie)) return true;
1514
+ if (/(?:^|;)\s*max-age=0(?:;|$)/i.test(setCookie)) return true;
1515
+ const expires = /(?:^|;)\s*expires=([^;]+)/i.exec(setCookie)?.[1];
1516
+ return expires ? new Date(expires).getTime() <= Date.now() : false;
1517
+ }
1518
+ __name(isDeletionCookie, "isDeletionCookie");
1519
+ function rememberCookie(name, mode, secure, maxAgeSeconds) {
1520
+ const attributes = [
1521
+ `${name}=${mode === "persistent" ? "1" : "0"}`,
1522
+ "Path=/",
1523
+ "HttpOnly",
1524
+ "SameSite=Lax"
1525
+ ];
1526
+ if (secure) attributes.push("Secure");
1527
+ if (mode === "persistent") attributes.push(`Max-Age=${maxAgeSeconds}`);
1528
+ return attributes.join("; ");
1529
+ }
1530
+ __name(rememberCookie, "rememberCookie");
1531
+ function clearedRememberCookie(name, secure) {
1532
+ return [
1533
+ `${name}=`,
1534
+ "Path=/",
1535
+ "HttpOnly",
1536
+ "SameSite=Lax",
1537
+ ...secure ? ["Secure"] : [],
1538
+ "Max-Age=0"
1539
+ ].join("; ");
1540
+ }
1541
+ __name(clearedRememberCookie, "clearedRememberCookie");
1542
+ function withAuthCookiePersistence(handler, options = {}) {
1543
+ const {
1544
+ authCookieNames = DEFAULTS.authCookieNames,
1545
+ rememberCookieName = DEFAULTS.rememberCookieName,
1546
+ maxAgeSeconds = DEFAULTS.maxAgeSeconds,
1547
+ loginPaths = DEFAULTS.loginPaths,
1548
+ logoutPaths = DEFAULTS.logoutPaths,
1549
+ refreshPaths = DEFAULTS.refreshPaths,
1550
+ isSetupResponse
1551
+ } = options;
1552
+ const resolveAction = /* @__PURE__ */ __name(async (request) => {
1553
+ const { pathname } = new URL(request.url);
1554
+ if (loginPaths.includes(pathname)) {
1555
+ const body = await request.clone().json().catch(() => null);
1556
+ return {
1557
+ type: "apply",
1558
+ mode: body?.rememberMe === true ? "persistent" : "session"
1559
+ };
1560
+ }
1561
+ if (logoutPaths.includes(pathname)) return { type: "clear" };
1562
+ if (refreshPaths.includes(pathname)) {
1563
+ const remembered = cookieValue(
1564
+ request.headers.get("cookie") ?? "",
1565
+ rememberCookieName
1566
+ );
1567
+ if (remembered === "0") return { type: "apply", mode: "session" };
1568
+ if (remembered === "1") return { type: "apply", mode: "persistent" };
1569
+ }
1570
+ return null;
1571
+ }, "resolveAction");
1572
+ const applyAction = /* @__PURE__ */ __name((response, action, secure) => {
1573
+ const headers = new Headers(response.headers);
1574
+ const setCookies = headers.getSetCookie();
1575
+ headers.delete("set-cookie");
1576
+ for (const setCookie of setCookies) {
1577
+ if (action.type === "setup" && authCookieNames.includes(cookieName(setCookie)) && !isDeletionCookie(setCookie)) {
1578
+ continue;
1579
+ }
1580
+ headers.append(
1581
+ "set-cookie",
1582
+ action.type === "apply" && action.mode === "session" ? makeSessionCookie(setCookie, authCookieNames) : setCookie
1583
+ );
1584
+ }
1585
+ headers.append(
1586
+ "set-cookie",
1587
+ action.type === "clear" || action.type === "setup" ? clearedRememberCookie(rememberCookieName, secure) : rememberCookie(rememberCookieName, action.mode, secure, maxAgeSeconds)
1588
+ );
1589
+ return new Response(response.body, {
1590
+ headers,
1591
+ status: response.status,
1592
+ statusText: response.statusText
1593
+ });
1594
+ }, "applyAction");
1595
+ return async (request) => {
1596
+ let action = await resolveAction(request);
1597
+ const response = await handler(request);
1598
+ if (!response.ok) return response;
1599
+ if (action?.type === "apply" && isSetupResponse) {
1600
+ const payload = await response.clone().json().catch(() => null);
1601
+ if (isSetupResponse(payload)) action = { type: "setup" };
1602
+ }
1603
+ if (!action) return response;
1604
+ return applyAction(
1605
+ response,
1606
+ action,
1607
+ new URL(request.url).protocol === "https:"
1608
+ );
1609
+ };
1610
+ }
1611
+ __name(withAuthCookiePersistence, "withAuthCookiePersistence");
1441
1612
  export {
1442
1613
  AuthConfigError,
1443
1614
  AuthTransportError,
1444
1615
  NoSessionError,
1445
1616
  createServerClient,
1446
1617
  defineAuth,
1618
+ getSafeRedirectPath,
1447
1619
  getServerSession,
1448
1620
  getSession,
1621
+ makeSessionCookie,
1449
1622
  withAuth,
1623
+ withAuthCookiePersistence,
1450
1624
  withAuthMiddleware
1451
1625
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "najm-auth",
3
- "version": "2.0.13",
3
+ "version": "2.0.15",
4
4
  "description": "Authentication and authorization library for najm framework",
5
5
  "type": "module",
6
6
  "files": [
@@ -78,10 +78,10 @@
78
78
  "dependencies": {
79
79
  "bcryptjs": "^3.0.2",
80
80
  "najm-cookies": "^2.0.2",
81
- "najm-core": "^2.0.4",
81
+ "najm-core": "^2.0.5",
82
82
  "najm-database": "^2.0.3",
83
83
  "najm-guard": "^2.0.2",
84
- "najm-i18n": "^2.0.2",
84
+ "najm-i18n": "^2.0.3",
85
85
  "najm-cache": "^2.0.2",
86
86
  "najm-email": "^2.0.2",
87
87
  "najm-rate": "^2.0.2",