najm-auth 1.1.20 → 1.1.21

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.
@@ -83,6 +83,8 @@ interface AuthClientConfig {
83
83
  interface AuthEventMap {
84
84
  login: AuthUser;
85
85
  logout: null;
86
+ /** Emitted when server-side logout invalidation fails (state was already cleared) */
87
+ logoutError: unknown;
86
88
  tokenRefresh: null;
87
89
  sessionExpired: null;
88
90
  stateChange: AuthState;
@@ -1,4 +1,4 @@
1
- import { F as FetchClient, a as AuthClientConfig, d as AuthUser, c as AuthState, e as AuthEvent, f as AuthEventHandler } from './FetchClient-aatsED1a.js';
1
+ import { F as FetchClient, a as AuthClientConfig, d as AuthUser, c as AuthState, e as AuthEvent, f as AuthEventHandler } from './FetchClient-DadnuVF7.js';
2
2
 
3
3
  interface HydrateSession {
4
4
  user: AuthUser | null;
@@ -1,6 +1,6 @@
1
- export { H as HydrateSession, N as NajmAuthClient, c as createAuthClient } from '../NajmAuthClient-FCwA8LIg.js';
2
- import { D as DecodedToken, T as TabSyncMessage, S as SyncPayload } from '../FetchClient-aatsED1a.js';
3
- export { a as AuthClientConfig, A as AuthError, e as AuthEvent, f as AuthEventHandler, b as AuthEventMap, c as AuthState, d as AuthUser, F as FetchClient, i as RequestOptions, R as RetryConfig, g as ServerResponse, h as TokenPair } from '../FetchClient-aatsED1a.js';
1
+ export { H as HydrateSession, N as NajmAuthClient, c as createAuthClient } from '../NajmAuthClient-CZHKl4ri.js';
2
+ import { D as DecodedToken, T as TabSyncMessage, S as SyncPayload } from '../FetchClient-DadnuVF7.js';
3
+ export { a as AuthClientConfig, A as AuthError, e as AuthEvent, f as AuthEventHandler, b as AuthEventMap, c as AuthState, d as AuthUser, F as FetchClient, i as RequestOptions, R as RetryConfig, g as ServerResponse, h as TokenPair } from '../FetchClient-DadnuVF7.js';
4
4
 
5
5
  /**
6
6
  * Decode a JWT token payload without verification.
@@ -265,13 +265,14 @@ var NajmAuthClient = class _NajmAuthClient {
265
265
  return res.data;
266
266
  }
267
267
  async logout() {
268
- try {
269
- await this.api.post(`${this.prefix}/logout`);
270
- } catch {
271
- }
272
268
  this.resetState();
273
269
  this.tabSync?.broadcastLogout();
274
270
  this.emit("logout", null);
271
+ try {
272
+ await this.api.post(`${this.prefix}/logout`);
273
+ } catch (err) {
274
+ this.emit("logoutError", err);
275
+ }
275
276
  }
276
277
  async refresh() {
277
278
  if (this.refreshFailures >= _NajmAuthClient.MAX_REFRESH_FAILURES) {
@@ -1,8 +1,8 @@
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 } from '../../NajmAuthClient-FCwA8LIg.js';
5
- import { c as AuthState, d as AuthUser, A as AuthError, e as AuthEvent, b as AuthEventMap } from '../../FetchClient-aatsED1a.js';
4
+ import { N as NajmAuthClient, H as HydrateSession } from '../../NajmAuthClient-CZHKl4ri.js';
5
+ import { c as AuthState, d as AuthUser, A as AuthError, e as AuthEvent, b as AuthEventMap } from '../../FetchClient-DadnuVF7.js';
6
6
 
7
7
  interface AuthProviderProps {
8
8
  client: NajmAuthClient;
@@ -91,6 +91,8 @@ interface UseRegisterReturn {
91
91
  declare function useRegister(opts?: UseRegisterOptions): UseRegisterReturn;
92
92
 
93
93
  interface UseLogoutOptions {
94
+ /** URL to redirect to after logout (uses window.location for full page reload) */
95
+ redirectTo?: string;
94
96
  onSuccess?: () => void;
95
97
  onError?: (error: Error) => void;
96
98
  }
@@ -317,9 +319,9 @@ interface ProtectedProps {
317
319
  role?: string;
318
320
  /** Require a specific permission */
319
321
  permission?: string;
320
- /** Shown while session is loading */
322
+ /** Shown while session is loading (defaults to fallback) */
321
323
  loadingFallback?: ReactNode;
322
- /** Shown when access is denied */
324
+ /** Shown when access is denied (defaults to loadingFallback) */
323
325
  fallback?: ReactNode;
324
326
  }
325
327
  /**
@@ -338,6 +340,36 @@ interface ProtectedProps {
338
340
  */
339
341
  declare function Protected({ children, redirectTo, onUnauthenticated, role, permission, loadingFallback, fallback, }: ProtectedProps): react_jsx_runtime.JSX.Element;
340
342
 
343
+ interface AuthGateProps {
344
+ children: ReactNode;
345
+ /** Full-page loader shown while resolving auth on hard reload */
346
+ loader?: ReactNode;
347
+ /** Where to redirect when unauthenticated (uses window.location for full reload) */
348
+ redirectTo?: string;
349
+ /** Callback when unauthenticated (overrides redirectTo) */
350
+ onUnauthenticated?: () => void;
351
+ /** Required role — renders `denied` if user lacks it */
352
+ role?: string;
353
+ /** Required permission — renders `denied` if user lacks it */
354
+ permission?: string;
355
+ /** Shown when role/permission check fails (defaults to null) */
356
+ denied?: ReactNode;
357
+ }
358
+ /**
359
+ * Full-page authentication gate.
360
+ *
361
+ * **Key difference from hooks**: AuthGate never renders `children` until
362
+ * auth is confirmed. No conditional rendering bugs, no layout shift,
363
+ * no flash of protected content.
364
+ *
365
+ * **With SSR hydration**: If `AuthProvider` was hydrated with `initialSession`,
366
+ * the gate opens immediately on first render — no loader flash.
367
+ *
368
+ * **Without SSR hydration** (client-side nav, hard reload without initialSession):
369
+ * Shows `loader` while attempting a token refresh, then either opens or redirects.
370
+ */
371
+ declare function AuthGate({ children, loader, redirectTo, onUnauthenticated, role, permission, denied, }: AuthGateProps): react_jsx_runtime.JSX.Element;
372
+
341
373
  interface UserNameProps {
342
374
  /** Optional fallback shown when no user is loaded */
343
375
  fallback?: string;
@@ -492,4 +524,4 @@ interface RedirectToLoginProps {
492
524
  */
493
525
  declare function RedirectToLogin({ to, preserveFrom }: RedirectToLoginProps): any;
494
526
 
495
- export { type AuthEventEntry, AuthLoading, AuthProvider, Can, IfAuth, LoginButton, PermissionList, Protected, RedirectToLogin, Role, type SessionStatus, SignOutButton, SignedIn, SignedOut, UserAvatar, UserEmail, UserName, UserRole, useAuth, useAuthClient, useAuthEvent, useAuthEvents, useChangePassword, useForgotPassword, useLogin, useLogout, usePermissions, useRegister, useResetPassword, useSession, useUser };
527
+ export { type AuthEventEntry, AuthGate, AuthLoading, AuthProvider, Can, IfAuth, LoginButton, PermissionList, Protected, RedirectToLogin, Role, type SessionStatus, SignOutButton, SignedIn, SignedOut, UserAvatar, UserEmail, UserName, UserRole, useAuth, useAuthClient, useAuthEvent, useAuthEvents, useChangePassword, useForgotPassword, useLogin, useLogout, usePermissions, useRegister, useResetPassword, useSession, useUser };
@@ -152,6 +152,10 @@ function useLogout(opts) {
152
152
  setIsLoading(true);
153
153
  try {
154
154
  await client.logout();
155
+ if (opts?.redirectTo && typeof window !== "undefined") {
156
+ window.location.href = opts.redirectTo;
157
+ return;
158
+ }
155
159
  opts?.onSuccess?.();
156
160
  } catch (err) {
157
161
  const e = err instanceof Error ? err : new Error(String(err));
@@ -159,7 +163,7 @@ function useLogout(opts) {
159
163
  } finally {
160
164
  setIsLoading(false);
161
165
  }
162
- }, [client, opts?.onSuccess, opts?.onError]);
166
+ }, [client, opts?.onSuccess, opts?.onError, opts?.redirectTo]);
163
167
  return { logout, isLoading };
164
168
  }
165
169
  __name(useLogout, "useLogout");
@@ -276,6 +280,7 @@ import { useEffect as useEffect3, useRef as useRef4, useState as useState8 } fro
276
280
  var ALL_EVENTS = [
277
281
  "login",
278
282
  "logout",
283
+ "logoutError",
279
284
  "tokenRefresh",
280
285
  "sessionExpired",
281
286
  "stateChange",
@@ -372,51 +377,98 @@ function Protected({
372
377
  onUnauthenticated,
373
378
  role,
374
379
  permission,
375
- loadingFallback = null,
376
- fallback = null
380
+ loadingFallback,
381
+ fallback
377
382
  }) {
383
+ const resolvedLoading = loadingFallback ?? fallback ?? null;
384
+ const resolvedFallback = fallback ?? loadingFallback ?? null;
378
385
  const { isLoading, isAuthenticated } = useSession({
379
386
  required: true,
380
387
  redirectTo,
381
388
  onUnauthenticated
382
389
  });
383
390
  const { can, hasRole } = usePermissions();
384
- if (isLoading) return /* @__PURE__ */ jsx8(Fragment7, { children: loadingFallback });
385
- if (!isAuthenticated) return /* @__PURE__ */ jsx8(Fragment7, { children: fallback });
386
- if (role && !hasRole(role)) return /* @__PURE__ */ jsx8(Fragment7, { children: fallback });
387
- if (permission && !can(permission)) return /* @__PURE__ */ jsx8(Fragment7, { children: fallback });
391
+ if (isLoading) return /* @__PURE__ */ jsx8(Fragment7, { children: resolvedLoading });
392
+ if (!isAuthenticated) return /* @__PURE__ */ jsx8(Fragment7, { children: resolvedFallback });
393
+ if (role && !hasRole(role)) return /* @__PURE__ */ jsx8(Fragment7, { children: resolvedFallback });
394
+ if (permission && !can(permission)) return /* @__PURE__ */ jsx8(Fragment7, { children: resolvedFallback });
388
395
  return /* @__PURE__ */ jsx8(Fragment7, { children });
389
396
  }
390
397
  __name(Protected, "Protected");
391
398
 
392
- // src/client/react/UserName.tsx
399
+ // src/client/react/AuthGate.tsx
400
+ import { useEffect as useEffect4, useSyncExternalStore as useSyncExternalStore2 } from "react";
393
401
  import { Fragment as Fragment8, jsx as jsx9 } from "react/jsx-runtime";
402
+ function AuthGate({
403
+ children,
404
+ loader = null,
405
+ redirectTo,
406
+ onUnauthenticated,
407
+ role,
408
+ permission,
409
+ denied = null
410
+ }) {
411
+ const client = useAuthClient();
412
+ const state = useSyncExternalStore2(
413
+ (cb) => client.subscribe(cb),
414
+ () => client.getState(),
415
+ () => client.getState()
416
+ );
417
+ const hydrated = client.isHydrated();
418
+ useEffect4(() => {
419
+ if (hydrated) return;
420
+ client.refresh().then(() => client.fetchUser()).catch(() => {
421
+ });
422
+ }, [client, hydrated]);
423
+ if (!hydrated && !state.isAuthenticated) {
424
+ return /* @__PURE__ */ jsx9(Fragment8, { children: loader });
425
+ }
426
+ if (!state.isAuthenticated) {
427
+ if (onUnauthenticated) {
428
+ onUnauthenticated();
429
+ } else if (redirectTo && typeof window !== "undefined") {
430
+ window.location.href = redirectTo;
431
+ }
432
+ return /* @__PURE__ */ jsx9(Fragment8, { children: loader });
433
+ }
434
+ if (role && !client.hasRole(role)) {
435
+ return /* @__PURE__ */ jsx9(Fragment8, { children: denied });
436
+ }
437
+ if (permission && !client.can(permission)) {
438
+ return /* @__PURE__ */ jsx9(Fragment8, { children: denied });
439
+ }
440
+ return /* @__PURE__ */ jsx9(Fragment8, { children });
441
+ }
442
+ __name(AuthGate, "AuthGate");
443
+
444
+ // src/client/react/UserName.tsx
445
+ import { Fragment as Fragment9, jsx as jsx10 } from "react/jsx-runtime";
394
446
  function UserName({ fallback = "" }) {
395
447
  const user = useUser();
396
- if (!user) return /* @__PURE__ */ jsx9(Fragment8, { children: fallback });
448
+ if (!user) return /* @__PURE__ */ jsx10(Fragment9, { children: fallback });
397
449
  const name = user.name ?? user.email ?? fallback;
398
- return /* @__PURE__ */ jsx9(Fragment8, { children: name });
450
+ return /* @__PURE__ */ jsx10(Fragment9, { children: name });
399
451
  }
400
452
  __name(UserName, "UserName");
401
453
 
402
454
  // src/client/react/UserEmail.tsx
403
- import { Fragment as Fragment9, jsx as jsx10 } from "react/jsx-runtime";
455
+ import { Fragment as Fragment10, jsx as jsx11 } from "react/jsx-runtime";
404
456
  function UserEmail({ fallback = "" }) {
405
457
  const user = useUser();
406
- return /* @__PURE__ */ jsx10(Fragment9, { children: user?.email ?? fallback });
458
+ return /* @__PURE__ */ jsx11(Fragment10, { children: user?.email ?? fallback });
407
459
  }
408
460
  __name(UserEmail, "UserEmail");
409
461
 
410
462
  // src/client/react/UserRole.tsx
411
- import { Fragment as Fragment10, jsx as jsx11 } from "react/jsx-runtime";
463
+ import { Fragment as Fragment11, jsx as jsx12 } from "react/jsx-runtime";
412
464
  function UserRole({ fallback = "" }) {
413
465
  const { roles } = usePermissions();
414
- return /* @__PURE__ */ jsx11(Fragment10, { children: roles[0] ?? fallback });
466
+ return /* @__PURE__ */ jsx12(Fragment11, { children: roles[0] ?? fallback });
415
467
  }
416
468
  __name(UserRole, "UserRole");
417
469
 
418
470
  // src/client/react/UserAvatar.tsx
419
- import { jsx as jsx12 } from "react/jsx-runtime";
471
+ import { jsx as jsx13 } from "react/jsx-runtime";
420
472
  function UserAvatar({ size = 32, className, style, alt }) {
421
473
  const user = useUser();
422
474
  const baseStyle = {
@@ -439,7 +491,7 @@ function UserAvatar({ size = 32, className, style, alt }) {
439
491
  const initial = label ? label[0].toUpperCase() : "?";
440
492
  const altText = alt ?? label ?? "User avatar";
441
493
  if (src) {
442
- return /* @__PURE__ */ jsx12(
494
+ return /* @__PURE__ */ jsx13(
443
495
  "img",
444
496
  {
445
497
  src,
@@ -451,7 +503,7 @@ function UserAvatar({ size = 32, className, style, alt }) {
451
503
  }
452
504
  );
453
505
  }
454
- return /* @__PURE__ */ jsx12(
506
+ return /* @__PURE__ */ jsx13(
455
507
  "span",
456
508
  {
457
509
  role: "img",
@@ -465,11 +517,11 @@ function UserAvatar({ size = 32, className, style, alt }) {
465
517
  __name(UserAvatar, "UserAvatar");
466
518
 
467
519
  // src/client/react/PermissionList.tsx
468
- import { Fragment as Fragment11, jsx as jsx13 } from "react/jsx-runtime";
520
+ import { Fragment as Fragment12, jsx as jsx14 } from "react/jsx-runtime";
469
521
  function PermissionList({ children, fallback = null }) {
470
522
  const { permissions } = usePermissions();
471
- if (permissions.length === 0) return /* @__PURE__ */ jsx13(Fragment11, { children: fallback });
472
- return /* @__PURE__ */ jsx13(Fragment11, { children: permissions.map((perm, i) => children(perm, i)) });
523
+ if (permissions.length === 0) return /* @__PURE__ */ jsx14(Fragment12, { children: fallback });
524
+ return /* @__PURE__ */ jsx14(Fragment12, { children: permissions.map((perm, i) => children(perm, i)) });
473
525
  }
474
526
  __name(PermissionList, "PermissionList");
475
527
 
@@ -508,9 +560,9 @@ function LoginButton({ href = "/login", children }) {
508
560
  __name(LoginButton, "LoginButton");
509
561
 
510
562
  // src/client/react/RedirectToLogin.tsx
511
- import { useEffect as useEffect4 } from "react";
563
+ import { useEffect as useEffect5 } from "react";
512
564
  function RedirectToLogin({ to = "/login", preserveFrom = true }) {
513
- useEffect4(() => {
565
+ useEffect5(() => {
514
566
  if (typeof window === "undefined") return;
515
567
  const target = preserveFrom ? `${to}?from=${encodeURIComponent(window.location.pathname + window.location.search)}` : to;
516
568
  window.location.href = target;
@@ -519,6 +571,7 @@ function RedirectToLogin({ to = "/login", preserveFrom = true }) {
519
571
  }
520
572
  __name(RedirectToLogin, "RedirectToLogin");
521
573
  export {
574
+ AuthGate,
522
575
  AuthLoading,
523
576
  AuthProvider,
524
577
  Can,
@@ -1,4 +1,4 @@
1
- import { d as AuthUser, F as FetchClient } from '../../FetchClient-aatsED1a.js';
1
+ import { d as AuthUser, F as FetchClient } from '../../FetchClient-DadnuVF7.js';
2
2
  import * as next_server from 'next/server';
3
3
 
4
4
  interface GetServerSessionOptions {
@@ -115,19 +115,53 @@ interface GetSessionConfig {
115
115
  */
116
116
  authPrefix?: string;
117
117
  /**
118
- * Cookie name to check before making the network call.
118
+ * Refresh token cookie name to check before making the network call.
119
119
  * If absent we skip and return null immediately.
120
120
  * Default: 'refreshToken'
121
121
  */
122
122
  cookieName?: string;
123
+ /**
124
+ * Session cookie name (default: 'najm.session').
125
+ * When present and valid, skips the /auth/me fetch entirely.
126
+ */
127
+ sessionCookieName?: string;
128
+ /**
129
+ * Secret used to verify the session cookie HMAC signature.
130
+ * Must match the `jwt.accessSecret` used by the auth plugin.
131
+ * Falls back to NAJM_SESSION_SECRET or JWT_ACCESS_SECRET env vars.
132
+ */
133
+ sessionSecret?: string;
134
+ /**
135
+ * Error handling mode:
136
+ * - 'nullable' (default): returns null on any failure
137
+ * - 'strict': throws typed errors for debugging
138
+ */
139
+ mode?: 'nullable' | 'strict';
140
+ }
141
+ declare class NoSessionError extends Error {
142
+ readonly code = "NO_SESSION";
143
+ constructor(message?: string);
144
+ }
145
+ declare class AuthConfigError extends Error {
146
+ readonly code = "AUTH_CONFIG_ERROR";
147
+ constructor(message: string);
148
+ }
149
+ declare class AuthTransportError extends Error {
150
+ readonly status?: number;
151
+ readonly code = "AUTH_TRANSPORT_ERROR";
152
+ constructor(message: string, status?: number);
123
153
  }
124
154
  /**
125
155
  * Resolve the current session inside a Next.js Server Component, Route Handler,
126
- * or Server Action. Returns `null` if the user is not authenticated.
156
+ * or Server Action.
157
+ *
158
+ * **Fast path**: If a signed `najm.session` cookie exists and is valid,
159
+ * returns the session instantly with zero network calls.
160
+ *
161
+ * **Fallback**: Checks the refresh token cookie and calls `/auth/me`.
127
162
  *
128
163
  * @example
129
164
  * ```tsx
130
- * // app/layout.tsx
131
165
  * import { getSession } from 'najm-auth/client/server';
132
166
  *
133
167
  * export default async function RootLayout({ children }) {
@@ -163,4 +197,53 @@ interface WithAuthProps<P> {
163
197
  */
164
198
  declare function withAuth<P extends Record<string, unknown> = Record<string, unknown>>(Page: (args: WithAuthProps<P>) => Promise<unknown> | unknown, options?: WithAuthOptions): (props: P) => Promise<unknown>;
165
199
 
166
- export { type GetSessionConfig, type ServerSession, type WithAuthOptions, type WithAuthProps, createServerClient, getServerSession, getSession, withAuth, withAuthMiddleware };
200
+ interface DefineAuthConfig {
201
+ /** API base URL (default: '/api') */
202
+ apiBaseURL?: string;
203
+ /** Auth prefix appended to apiBaseURL (default: '/auth') */
204
+ authPrefix?: string;
205
+ /** Route to redirect unauthenticated users (default: '/login') */
206
+ loginRoute?: string;
207
+ /** Route to redirect after login (default: '/dashboard') */
208
+ afterLoginRoute?: string;
209
+ /** Routes that are always public (glob patterns) */
210
+ publicRoutes?: string[];
211
+ /** Routes that require authentication (glob patterns) */
212
+ protectedRoutes?: string[];
213
+ /** Routes restricted to specific roles: { '/admin/:path*': ['admin'] } */
214
+ roleRoutes?: Record<string, string[]>;
215
+ /** Refresh token cookie name (default: 'refreshToken') */
216
+ cookieName?: string;
217
+ /** Session cookie name (default: 'najm.session') */
218
+ sessionCookieName?: string;
219
+ /** Secret for verifying session cookie HMAC. Falls back to env vars. */
220
+ sessionSecret?: string;
221
+ /** Next.js middleware matcher (default: exclude _next, favicon, api) */
222
+ matcher?: string[];
223
+ }
224
+ interface AuthKit {
225
+ /** Resolve session — reads signed cookie first (instant), falls back to /auth/me */
226
+ getSession: (opts?: Pick<GetSessionConfig, 'mode'>) => Promise<ServerSession | null>;
227
+ /** Require session — throws if unauthenticated */
228
+ requireSession: () => Promise<ServerSession>;
229
+ /** Generated Next.js middleware function */
230
+ middleware: (request: Request) => Promise<Response>;
231
+ /** Next.js middleware config with matcher */
232
+ config: {
233
+ matcher: string[];
234
+ };
235
+ /**
236
+ * Protect a server component — redirects to loginRoute if unauthenticated.
237
+ * Passes session to the wrapped component.
238
+ */
239
+ protect: <P extends Record<string, unknown> = Record<string, unknown>>(Page: (args: {
240
+ session: ServerSession;
241
+ children?: unknown;
242
+ } & P) => Promise<unknown> | unknown, options?: {
243
+ role?: string;
244
+ permission?: string;
245
+ }) => (props: P) => Promise<unknown>;
246
+ }
247
+ declare function defineAuth(authConfig?: DefineAuthConfig): AuthKit;
248
+
249
+ export { AuthConfigError, type AuthKit, AuthTransportError, type DefineAuthConfig, type GetSessionConfig, NoSessionError, type ServerSession, type WithAuthOptions, type WithAuthProps, createServerClient, defineAuth, getServerSession, getSession, withAuth, withAuthMiddleware };
@@ -1,5 +1,199 @@
1
1
  var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
2
3
  var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // src/client/server/getSession.ts
13
+ var getSession_exports = {};
14
+ __export(getSession_exports, {
15
+ AuthConfigError: () => AuthConfigError,
16
+ AuthTransportError: () => AuthTransportError,
17
+ NoSessionError: () => NoSessionError,
18
+ getSession: () => getSession
19
+ });
20
+ import { createHmac, timingSafeEqual } from "crypto";
21
+ function defaultBaseURL() {
22
+ const explicit = typeof process !== "undefined" ? process.env.NAJM_AUTH_BASE_URL : void 0;
23
+ if (explicit) return explicit;
24
+ const origin = typeof process !== "undefined" ? process.env.NEXT_PUBLIC_API_URL ?? `http://localhost:${process.env.PORT ?? 3e3}` : "http://localhost:3000";
25
+ return `${origin.replace(/\/$/, "")}/api`;
26
+ }
27
+ function getSessionSecret(config) {
28
+ return config.sessionSecret ?? (typeof process !== "undefined" ? process.env.NAJM_SESSION_SECRET : void 0) ?? (typeof process !== "undefined" ? process.env.JWT_ACCESS_SECRET : void 0);
29
+ }
30
+ function verifyHmac(payload, signature, secret) {
31
+ const expected = createHmac("sha256", secret).update(payload).digest("base64url");
32
+ if (expected.length !== signature.length) return false;
33
+ try {
34
+ return timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
35
+ } catch {
36
+ return false;
37
+ }
38
+ }
39
+ function parseSessionCookie(raw, secret) {
40
+ const lastDot = raw.lastIndexOf(".");
41
+ if (lastDot === -1) return null;
42
+ const payload = raw.substring(0, lastDot);
43
+ const signature = raw.substring(lastDot + 1);
44
+ if (!verifyHmac(payload, signature, secret)) return null;
45
+ try {
46
+ const decoded = decodeURIComponent(payload);
47
+ const data = JSON.parse(decoded);
48
+ if (Date.now() - data.iat > SESSION_COOKIE_MAX_AGE_MS) return null;
49
+ const { user, roles, permissions } = data;
50
+ return {
51
+ user,
52
+ roles: roles ?? (user.role ? [user.role] : void 0),
53
+ permissions: permissions ?? void 0
54
+ };
55
+ } catch {
56
+ return null;
57
+ }
58
+ }
59
+ function buildSession(body) {
60
+ if (!body?.data) return null;
61
+ const { roles, permissions, ...user } = body.data;
62
+ return {
63
+ user,
64
+ roles: roles ?? (user.role ? [user.role] : void 0),
65
+ permissions: permissions ?? user.permissions
66
+ };
67
+ }
68
+ async function getSession(config = {}) {
69
+ const cookieName = config.cookieName ?? "refreshToken";
70
+ const sessionCookieName = config.sessionCookieName ?? "najm.session";
71
+ const baseURL = config.baseURL ?? defaultBaseURL();
72
+ const prefix = config.authPrefix ?? "/auth";
73
+ const strict = config.mode === "strict";
74
+ let cookieHeader = "";
75
+ let sessionCookieValue;
76
+ let hasRefreshCookie = false;
77
+ try {
78
+ const mod = await import("next/headers");
79
+ const cookieStore = await mod.cookies();
80
+ const sessionCookie = cookieStore.get(sessionCookieName);
81
+ if (sessionCookie) sessionCookieValue = sessionCookie.value;
82
+ if (typeof cookieStore.get === "function") {
83
+ hasRefreshCookie = !!cookieStore.get(cookieName);
84
+ }
85
+ cookieHeader = cookieStore.getAll().map((c) => `${c.name}=${c.value}`).join("; ");
86
+ } catch (err) {
87
+ if (strict) throw new AuthConfigError("Failed to read cookies from Next.js headers()");
88
+ return null;
89
+ }
90
+ if (sessionCookieValue) {
91
+ const secret = getSessionSecret(config);
92
+ if (secret) {
93
+ const session = parseSessionCookie(sessionCookieValue, secret);
94
+ if (session) return session;
95
+ }
96
+ }
97
+ if (!hasRefreshCookie) {
98
+ if (strict) throw new NoSessionError("No refresh token cookie");
99
+ return null;
100
+ }
101
+ if (!cookieHeader) {
102
+ if (strict) throw new NoSessionError("Empty cookie header");
103
+ return null;
104
+ }
105
+ try {
106
+ const res = await fetch(`${baseURL}${prefix}/me`, {
107
+ headers: { Cookie: cookieHeader, Accept: "application/json" },
108
+ cache: "no-store"
109
+ });
110
+ if (!res.ok) {
111
+ if (strict) throw new AuthTransportError(`/auth/me returned ${res.status}`, res.status);
112
+ return null;
113
+ }
114
+ const body = await res.json();
115
+ const session = buildSession(body);
116
+ if (!session && strict) throw new NoSessionError("No user data in /auth/me response");
117
+ return session;
118
+ } catch (err) {
119
+ if (err instanceof NoSessionError || err instanceof AuthTransportError) throw err;
120
+ if (strict) throw new AuthTransportError(`Failed to fetch /auth/me: ${err.message}`);
121
+ return null;
122
+ }
123
+ }
124
+ var NoSessionError, AuthConfigError, AuthTransportError, SESSION_COOKIE_MAX_AGE_MS;
125
+ var init_getSession = __esm({
126
+ "src/client/server/getSession.ts"() {
127
+ NoSessionError = class extends Error {
128
+ static {
129
+ __name(this, "NoSessionError");
130
+ }
131
+ code = "NO_SESSION";
132
+ constructor(message = "No active session") {
133
+ super(message);
134
+ this.name = "NoSessionError";
135
+ }
136
+ };
137
+ AuthConfigError = class extends Error {
138
+ static {
139
+ __name(this, "AuthConfigError");
140
+ }
141
+ code = "AUTH_CONFIG_ERROR";
142
+ constructor(message) {
143
+ super(message);
144
+ this.name = "AuthConfigError";
145
+ }
146
+ };
147
+ AuthTransportError = class extends Error {
148
+ constructor(message, status) {
149
+ super(message);
150
+ this.status = status;
151
+ this.name = "AuthTransportError";
152
+ }
153
+ static {
154
+ __name(this, "AuthTransportError");
155
+ }
156
+ code = "AUTH_TRANSPORT_ERROR";
157
+ };
158
+ __name(defaultBaseURL, "defaultBaseURL");
159
+ __name(getSessionSecret, "getSessionSecret");
160
+ __name(verifyHmac, "verifyHmac");
161
+ SESSION_COOKIE_MAX_AGE_MS = 3e5;
162
+ __name(parseSessionCookie, "parseSessionCookie");
163
+ __name(buildSession, "buildSession");
164
+ __name(getSession, "getSession");
165
+ }
166
+ });
167
+
168
+ // src/client/permissions.ts
169
+ var permissions_exports = {};
170
+ __export(permissions_exports, {
171
+ hasAnyRole: () => hasAnyRole,
172
+ hasRole: () => hasRole,
173
+ matchPermission: () => matchPermission
174
+ });
175
+ function matchPermission(userPermissions, required) {
176
+ if (userPermissions.includes("*:*")) return true;
177
+ if (userPermissions.includes(required)) return true;
178
+ const sep = required.indexOf(":");
179
+ if (sep === -1) return false;
180
+ const action = required.slice(0, sep);
181
+ const resource = required.slice(sep + 1);
182
+ return userPermissions.includes(`${action}:*`) || userPermissions.includes(`*:${resource}`);
183
+ }
184
+ function hasRole(userRoles, role) {
185
+ return userRoles.includes(role);
186
+ }
187
+ function hasAnyRole(userRoles, roles) {
188
+ return roles.some((r) => userRoles.includes(r));
189
+ }
190
+ var init_permissions = __esm({
191
+ "src/client/permissions.ts"() {
192
+ __name(matchPermission, "matchPermission");
193
+ __name(hasRole, "hasRole");
194
+ __name(hasAnyRole, "hasAnyRole");
195
+ }
196
+ });
3
197
 
4
198
  // src/client/server/getServerSession.ts
5
199
  async function getServerSession(opts) {
@@ -233,63 +427,12 @@ function findMatchingRoles(pathname, roleRoutes) {
233
427
  }
234
428
  __name(findMatchingRoles, "findMatchingRoles");
235
429
 
236
- // src/client/server/getSession.ts
237
- function defaultBaseURL() {
238
- const explicit = typeof process !== "undefined" ? process.env.NAJM_AUTH_BASE_URL : void 0;
239
- if (explicit) return explicit;
240
- const origin = typeof process !== "undefined" ? process.env.NEXT_PUBLIC_API_URL ?? `http://localhost:${process.env.PORT ?? 3e3}` : "http://localhost:3000";
241
- return `${origin.replace(/\/$/, "")}/api`;
242
- }
243
- __name(defaultBaseURL, "defaultBaseURL");
244
- async function getSession(config = {}) {
245
- const cookieName = config.cookieName ?? "refreshToken";
246
- const baseURL = config.baseURL ?? defaultBaseURL();
247
- const prefix = config.authPrefix ?? "/auth";
248
- let cookieHeader = "";
249
- try {
250
- const mod = await import("next/headers");
251
- const cookieStore = await mod.cookies();
252
- if (typeof cookieStore.get === "function" && !cookieStore.get(cookieName)) {
253
- return null;
254
- }
255
- cookieHeader = cookieStore.getAll().map((c) => `${c.name}=${c.value}`).join("; ");
256
- } catch {
257
- return null;
258
- }
259
- if (!cookieHeader) return null;
260
- try {
261
- const res = await fetch(`${baseURL}${prefix}/me`, {
262
- headers: { Cookie: cookieHeader, Accept: "application/json" },
263
- cache: "no-store"
264
- });
265
- if (!res.ok) return null;
266
- const body = await res.json();
267
- if (!body?.data) return null;
268
- const { roles, permissions, ...user } = body.data;
269
- return {
270
- user,
271
- roles: roles ?? (user.role ? [user.role] : void 0),
272
- permissions: permissions ?? user.permissions
273
- };
274
- } catch {
275
- return null;
276
- }
277
- }
278
- __name(getSession, "getSession");
279
-
280
- // src/client/permissions.ts
281
- function matchPermission(userPermissions, required) {
282
- if (userPermissions.includes("*:*")) return true;
283
- if (userPermissions.includes(required)) return true;
284
- const sep = required.indexOf(":");
285
- if (sep === -1) return false;
286
- const action = required.slice(0, sep);
287
- const resource = required.slice(sep + 1);
288
- return userPermissions.includes(`${action}:*`) || userPermissions.includes(`*:${resource}`);
289
- }
290
- __name(matchPermission, "matchPermission");
430
+ // src/client/server/index.ts
431
+ init_getSession();
291
432
 
292
433
  // src/client/server/withAuth.ts
434
+ init_getSession();
435
+ init_permissions();
293
436
  function withAuth(Page, options = {}) {
294
437
  const { redirectTo = "/login", role, permission, ...sessionConfig } = options;
295
438
  return /* @__PURE__ */ __name(async function ProtectedPage(props) {
@@ -316,8 +459,157 @@ function withAuth(Page, options = {}) {
316
459
  }, "ProtectedPage");
317
460
  }
318
461
  __name(withAuth, "withAuth");
462
+
463
+ // src/client/server/defineAuth.ts
464
+ function matchesAny2(pathname, patterns) {
465
+ return patterns.some((p) => matchPattern2(pathname, p));
466
+ }
467
+ __name(matchesAny2, "matchesAny");
468
+ function matchPattern2(pathname, pattern) {
469
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
470
+ const regex = escaped.replace(/\/:[^/]+\*/g, "(?:/.*)?").replace(/\/\\\*$/g, "(?:/.*)?").replace(/\\\*/g, "(?:/.*)?").replace(/\//g, "\\/");
471
+ return new RegExp(`^${regex}$`).test(pathname);
472
+ }
473
+ __name(matchPattern2, "matchPattern");
474
+ function cookieRegex2(name) {
475
+ return new RegExp(`(?:^|;\\s*)${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}=[^;]`);
476
+ }
477
+ __name(cookieRegex2, "cookieRegex");
478
+ function findMatchingRoles2(pathname, roleRoutes) {
479
+ for (const [pattern, roles] of Object.entries(roleRoutes)) {
480
+ if (matchPattern2(pathname, pattern)) return roles;
481
+ }
482
+ return null;
483
+ }
484
+ __name(findMatchingRoles2, "findMatchingRoles");
485
+ function defineAuth(authConfig = {}) {
486
+ const {
487
+ apiBaseURL = "/api",
488
+ authPrefix = "/auth",
489
+ loginRoute = "/login",
490
+ publicRoutes = [],
491
+ protectedRoutes = [],
492
+ roleRoutes = {},
493
+ cookieName = "refreshToken",
494
+ sessionCookieName = "najm.session",
495
+ sessionSecret,
496
+ matcher = ["/((?!_next/static|_next/image|favicon.ico|api).*)"]
497
+ } = authConfig;
498
+ const sessionConfig = {
499
+ baseURL: void 0,
500
+ // resolved at call time from env/defaults
501
+ authPrefix,
502
+ cookieName,
503
+ sessionCookieName,
504
+ sessionSecret
505
+ };
506
+ const getSession2 = /* @__PURE__ */ __name(async (opts) => {
507
+ const { getSession: resolveSession } = await Promise.resolve().then(() => (init_getSession(), getSession_exports));
508
+ return resolveSession({ ...sessionConfig, ...opts });
509
+ }, "getSession");
510
+ const requireSession = /* @__PURE__ */ __name(async () => {
511
+ const sessionModule = await Promise.resolve().then(() => (init_getSession(), getSession_exports));
512
+ const {
513
+ getSession: resolveSession,
514
+ NoSessionError: NoSessionError2,
515
+ AuthTransportError: AuthTransportError2
516
+ } = sessionModule;
517
+ try {
518
+ const session = await resolveSession({ ...sessionConfig, mode: "strict" });
519
+ return session;
520
+ } catch (err) {
521
+ if (err instanceof NoSessionError2) {
522
+ const { redirect } = await import("next/navigation");
523
+ redirect(loginRoute);
524
+ }
525
+ if (err instanceof AuthTransportError2 && (err.status === 401 || err.status === 403)) {
526
+ const { redirect } = await import("next/navigation");
527
+ redirect(loginRoute);
528
+ }
529
+ throw err;
530
+ }
531
+ }, "requireSession");
532
+ const middleware = /* @__PURE__ */ __name(async (request) => {
533
+ const { NextResponse } = await import("next/server");
534
+ const url = new URL(request.url);
535
+ const pathname = url.pathname;
536
+ if (matchesAny2(pathname, publicRoutes)) {
537
+ return NextResponse.next();
538
+ }
539
+ const isProtected = protectedRoutes.length === 0 || matchesAny2(pathname, protectedRoutes);
540
+ if (!isProtected) return NextResponse.next();
541
+ const cookie = request.headers.get("cookie") ?? "";
542
+ const hasToken = cookieRegex2(cookieName).test(cookie);
543
+ if (!hasToken) {
544
+ const loginUrl = new URL(loginRoute, request.url);
545
+ loginUrl.searchParams.set("from", pathname);
546
+ return NextResponse.redirect(loginUrl);
547
+ }
548
+ const requiredRoles = findMatchingRoles2(pathname, roleRoutes);
549
+ if (requiredRoles) {
550
+ const verifyURL = `${url.origin}${apiBaseURL}${authPrefix}/me`;
551
+ try {
552
+ const res = await fetch(verifyURL, {
553
+ headers: { Cookie: cookie, Accept: "application/json" }
554
+ });
555
+ if (!res.ok) {
556
+ const loginUrl = new URL(loginRoute, request.url);
557
+ loginUrl.searchParams.set("from", pathname);
558
+ return NextResponse.redirect(loginUrl);
559
+ }
560
+ const body = await res.json();
561
+ const userRole = body?.data?.role;
562
+ if (!userRole || !requiredRoles.includes(userRole)) {
563
+ return new NextResponse(null, { status: 403 });
564
+ }
565
+ } catch {
566
+ const loginUrl = new URL(loginRoute, request.url);
567
+ loginUrl.searchParams.set("from", pathname);
568
+ return NextResponse.redirect(loginUrl);
569
+ }
570
+ }
571
+ return NextResponse.next();
572
+ }, "middleware");
573
+ const protect = /* @__PURE__ */ __name((Page, options) => {
574
+ return /* @__PURE__ */ __name(async function ProtectedPage(props) {
575
+ const session = await getSession2();
576
+ if (!session) {
577
+ const { redirect } = await import("next/navigation");
578
+ redirect(loginRoute);
579
+ }
580
+ if (options?.role) {
581
+ const userRoles = session.roles ?? (session.user.role ? [session.user.role] : []);
582
+ if (!userRoles.includes(options.role)) {
583
+ const { redirect } = await import("next/navigation");
584
+ redirect(loginRoute);
585
+ }
586
+ }
587
+ if (options?.permission) {
588
+ const { matchPermission: matchPermission2 } = await Promise.resolve().then(() => (init_permissions(), permissions_exports));
589
+ const perms = session.permissions ?? session.user.permissions ?? [];
590
+ if (!matchPermission2(perms, options.permission)) {
591
+ const { redirect } = await import("next/navigation");
592
+ redirect(loginRoute);
593
+ }
594
+ }
595
+ return Page({ session, ...props });
596
+ }, "ProtectedPage");
597
+ }, "protect");
598
+ return {
599
+ getSession: getSession2,
600
+ requireSession,
601
+ middleware,
602
+ config: { matcher },
603
+ protect
604
+ };
605
+ }
606
+ __name(defineAuth, "defineAuth");
319
607
  export {
608
+ AuthConfigError,
609
+ AuthTransportError,
610
+ NoSessionError,
320
611
  createServerClient,
612
+ defineAuth,
321
613
  getServerSession,
322
614
  getSession,
323
615
  withAuth,
package/dist/index.d.ts CHANGED
@@ -31,6 +31,19 @@ interface LockoutConfig {
31
31
  /** Lockout duration (e.g. '15m') */
32
32
  duration: string;
33
33
  }
34
+ /**
35
+ * Session cookie cache configuration.
36
+ * A short-lived, HMAC-signed cookie that caches user/roles/permissions for
37
+ * instant SSR reads (skips the /auth/me round-trip).
38
+ */
39
+ interface SessionCookieConfig {
40
+ /** Cookie name (default: 'najm.session') */
41
+ name: string;
42
+ /** Max age in seconds (default: 300 = 5 min) */
43
+ maxAge: number;
44
+ /** Secret used to HMAC-sign the cookie. Defaults to jwt.accessSecret. */
45
+ secret?: string;
46
+ }
34
47
  /**
35
48
  * Complete auth plugin configuration (internal)
36
49
  */
@@ -51,6 +64,8 @@ interface AuthConfig {
51
64
  registrationMode: 'active' | 'pending';
52
65
  /** Per-account lockout settings */
53
66
  lockout: LockoutConfig;
67
+ /** Session cookie cache settings */
68
+ session: SessionCookieConfig;
54
69
  }
55
70
  /**
56
71
  * Auth plugin configuration options
@@ -88,6 +103,8 @@ type AuthPluginConfig = {
88
103
  registrationMode?: 'active' | 'pending';
89
104
  /** Per-account lockout settings */
90
105
  lockout?: Partial<LockoutConfig>;
106
+ /** Session cookie cache settings (optional — sensible defaults applied) */
107
+ session?: Partial<SessionCookieConfig>;
91
108
  /** Optional config forwarded to validation() dependency */
92
109
  validation?: ValidationPluginConfig;
93
110
  /** Optional config forwarded to rateLimit() dependency */
@@ -332,15 +349,45 @@ declare class EncryptionService {
332
349
  comparePassword(password: string, hashedPassword: string): Promise<boolean>;
333
350
  }
334
351
 
352
+ interface SessionCookieData {
353
+ user: {
354
+ id: string;
355
+ email: string;
356
+ name?: string | null;
357
+ role?: string;
358
+ };
359
+ roles: string[];
360
+ permissions: string[];
361
+ /** Epoch ms when the cookie was written */
362
+ iat: number;
363
+ }
335
364
  declare class CookieManager {
336
365
  private config;
337
366
  private cookieService;
338
367
  private get cookieName();
368
+ private get sessionCookieName();
369
+ private get sessionMaxAge();
370
+ private get sessionSecret();
339
371
  setRefreshToken(refreshToken: string): void;
340
372
  clearRefreshToken(): void;
341
373
  getRefreshToken(): string | undefined;
342
374
  hasRefreshToken(): boolean;
343
375
  getCookieName(): string;
376
+ /**
377
+ * Write a signed session cookie containing user data, roles, and permissions.
378
+ * The cookie is HMAC-signed with the access secret so it is tamper-proof
379
+ * but readable without a database query. Short TTL (5 min) ensures freshness.
380
+ */
381
+ setSessionCookie(data: Omit<SessionCookieData, 'iat'>): void;
382
+ /**
383
+ * Read and verify the session cookie. Returns null if missing, expired,
384
+ * or signature verification fails.
385
+ */
386
+ getSessionCookie(): SessionCookieData | null;
387
+ /**
388
+ * Clear the session cookie (on logout, password change, etc.)
389
+ */
390
+ clearSessionCookie(): void;
344
391
  }
345
392
 
346
393
  interface UserWithPermissions extends Omit<User, 'password'> {
@@ -847,6 +894,11 @@ declare class AuthService {
847
894
  constructor(tokenService: TokenService, userService: UserService, userValidator: UserValidator, cookieManager: CookieManager, i18nService: I18nService, emailService: EmailService);
848
895
  private isLockoutActive;
849
896
  private nextLockoutUntil;
897
+ /**
898
+ * Decode JWT payload without verification (token was just generated by us).
899
+ * Used to extract accurate roles/permissions for the session cookie cache.
900
+ */
901
+ private decodeAccessToken;
850
902
  registerUser(body: CreateUserDto): Promise<SanitizedUser>;
851
903
  loginUser(body: LoginDto): Promise<TokenPair & {
852
904
  user: SanitizedUser;
@@ -865,6 +917,12 @@ declare class AuthService {
865
917
  /**
866
918
  * Get current user — prefer access token (no cookie rotation risk),
867
919
  * fall back to cookie when no Authorization header is present.
920
+ *
921
+ * Refreshes the session cookie cache only when authoritative roles/permissions
922
+ * are available (from the access token JWT). When called via the refresh-cookie
923
+ * fallback, we cannot reliably populate permissions without a DB round-trip,
924
+ * so we leave the existing cache alone — it will be refreshed by the next
925
+ * login/refresh cycle.
868
926
  */
869
927
  getMe(authorization?: string): Promise<SanitizedUser & {
870
928
  language: string;
@@ -1836,4 +1894,4 @@ declare const authSeed: (config: AuthSeedConfig) => Record<string, SeedEntry>;
1836
1894
  */
1837
1895
  declare function seedAuthData(config: SeedAuthDataConfig): Promise<SeedAuthDataResult>;
1838
1896
 
1839
- export { AUTH_CONFIG, en as AUTH_EN, AUTH_LOCALES, AUTH_MODULE, AUTH_PERMISSIONS, AUTH_ROLE, AUTH_SCHEMA, AUTH_SUPPORTED_LANGUAGES, AUTH_USER, type AssignPermissionDto, type AssignRoleDto, type AssignRoleParams, type AuthConfig, AuthController, AuthGuard, type AuthPluginConfig, AuthQueries, AuthResolver, type AuthSchema, type AuthSeedConfig, AuthService, type AuthUser, Can, CanCreate, CanDelete, CanList, CanRead, CanUpdate, type ChainableGuard, type ChangePasswordDto, type CheckPermissionDto, type ConfiguredOwnership, type ConfirmResetPasswordDto, CookieManager, type CreatePermissionDto, type CreateRoleDto, type CreateTokenDto, type CreateUserDto, type DefineRolesOptions, type EmailParam, EncryptionService, type JwtConfig, type JwtPayload, type LanguageParam, type LoginDto, NewPermission, NewRoleEntity, NewUser, Owned, type OwnedMethods, type OwnershipConfig, type OwnershipProvider, type OwnershipRule, OwnershipToken, type OwnershipTokenOptions, Permission, PermissionController, PermissionGuard, type PermissionIdParam, PermissionRepository, PermissionService, PermissionValidator, Policy, ROLES, ROLE_GROUPS, type RefreshTokenDto, type ResetPasswordDto, type ResourceAccessor, type ResourceGuards, type ResourceGuardsOptions, type RevokeTokenDto, Role, RoleController, RoleEntity, RoleGuard, type RoleIdParam, type RoleInput, RolePermission, RoleRepository, RoleService, type RoleType, RoleValidator, type SanitizedUser, ScopeContext, type ScopeResult, type SeedAuthDataConfig, type SeedAuthDataResult, type SeedUserConfig, TOKEN_STATUS, TOKEN_TYPE, type TokenIdParam, type TokenPair, TokenRepository, TokenService, USER_STATUS, type UpdatePermissionDto, type UpdateRoleDto, type UpdateTokenDto, type UpdateUserDto, User, UserController, type UserIdInParam, type UserIdParam, UserRepository, UserService, UserValidator, type UserWithPermissions, type VerifyTokenDto, assignPermissionDto, assignRoleDto, assignRoleParams, auth$1 as auth, authSeed, avatarsPath, calculateAge, calculateYearsOfExperience, changePasswordDto, checkPermissionDto, clean, configureOwnership, confirmResetPasswordDto, createPermissionDto, createRoleDto, createTokenDto, createUserDto, defineRoles, emailParam, formatDate, getAuthLocale, getAvatarFile, isAdmin, isAdministrator, isAuth, isEmpty, isFile, isPath, join, languageParam, loginDto, own, parseSchema, permissionIdParam, pickProps, refreshTokenDto, resetPasswordDto, revokeTokenDto, roleIdParam, seedAuthData, setConfiguredCookieName, tokenIdParam, updatePermissionDto, updateRoleDto, updateTokenDto, updateUserDto, userIdInParam, userIdParam, verifyTokenDto, where };
1897
+ export { AUTH_CONFIG, en as AUTH_EN, AUTH_LOCALES, AUTH_MODULE, AUTH_PERMISSIONS, AUTH_ROLE, AUTH_SCHEMA, AUTH_SUPPORTED_LANGUAGES, AUTH_USER, type AssignPermissionDto, type AssignRoleDto, type AssignRoleParams, type AuthConfig, AuthController, AuthGuard, type AuthPluginConfig, AuthQueries, AuthResolver, type AuthSchema, type AuthSeedConfig, AuthService, type AuthUser, Can, CanCreate, CanDelete, CanList, CanRead, CanUpdate, type ChainableGuard, type ChangePasswordDto, type CheckPermissionDto, type ConfiguredOwnership, type ConfirmResetPasswordDto, CookieManager, type CreatePermissionDto, type CreateRoleDto, type CreateTokenDto, type CreateUserDto, type DefineRolesOptions, type EmailParam, EncryptionService, type JwtConfig, type JwtPayload, type LanguageParam, type LoginDto, NewPermission, NewRoleEntity, NewUser, Owned, type OwnedMethods, type OwnershipConfig, type OwnershipProvider, type OwnershipRule, OwnershipToken, type OwnershipTokenOptions, Permission, PermissionController, PermissionGuard, type PermissionIdParam, PermissionRepository, PermissionService, PermissionValidator, Policy, ROLES, ROLE_GROUPS, type RefreshTokenDto, type ResetPasswordDto, type ResourceAccessor, type ResourceGuards, type ResourceGuardsOptions, type RevokeTokenDto, Role, RoleController, RoleEntity, RoleGuard, type RoleIdParam, type RoleInput, RolePermission, RoleRepository, RoleService, type RoleType, RoleValidator, type SanitizedUser, ScopeContext, type ScopeResult, type SeedAuthDataConfig, type SeedAuthDataResult, type SeedUserConfig, type SessionCookieData, TOKEN_STATUS, TOKEN_TYPE, type TokenIdParam, type TokenPair, TokenRepository, TokenService, USER_STATUS, type UpdatePermissionDto, type UpdateRoleDto, type UpdateTokenDto, type UpdateUserDto, User, UserController, type UserIdInParam, type UserIdParam, UserRepository, UserService, UserValidator, type UserWithPermissions, type VerifyTokenDto, assignPermissionDto, assignRoleDto, assignRoleParams, auth$1 as auth, authSeed, avatarsPath, calculateAge, calculateYearsOfExperience, changePasswordDto, checkPermissionDto, clean, configureOwnership, confirmResetPasswordDto, createPermissionDto, createRoleDto, createTokenDto, createUserDto, defineRoles, emailParam, formatDate, getAuthLocale, getAvatarFile, isAdmin, isAdministrator, isAuth, isEmpty, isFile, isPath, join, languageParam, loginDto, own, parseSchema, permissionIdParam, pickProps, refreshTokenDto, resetPasswordDto, revokeTokenDto, roleIdParam, seedAuthData, setConfiguredCookieName, tokenIdParam, updatePermissionDto, updateRoleDto, updateTokenDto, updateUserDto, userIdInParam, userIdParam, verifyTokenDto, where };
package/dist/index.js CHANGED
@@ -1183,6 +1183,18 @@ var CookieManager = class CookieManager2 {
1183
1183
  get cookieName() {
1184
1184
  return this.config.refreshCookieName || "refreshToken";
1185
1185
  }
1186
+ get sessionCookieName() {
1187
+ return this.config.session.name;
1188
+ }
1189
+ get sessionMaxAge() {
1190
+ return this.config.session.maxAge;
1191
+ }
1192
+ get sessionSecret() {
1193
+ return this.config.session.secret || this.config.jwt.accessSecret;
1194
+ }
1195
+ // =========================================================================
1196
+ // Refresh Token Cookie
1197
+ // =========================================================================
1186
1198
  setRefreshToken(refreshToken) {
1187
1199
  const maxAge = timestring(this.config.jwt.refreshExpiresIn, "s");
1188
1200
  this.cookieService.set(this.cookieName, refreshToken, { maxAge });
@@ -1199,6 +1211,47 @@ var CookieManager = class CookieManager2 {
1199
1211
  getCookieName() {
1200
1212
  return this.cookieName;
1201
1213
  }
1214
+ // =========================================================================
1215
+ // Session Cookie Cache (HMAC-signed, short-lived)
1216
+ // =========================================================================
1217
+ /**
1218
+ * Write a signed session cookie containing user data, roles, and permissions.
1219
+ * The cookie is HMAC-signed with the access secret so it is tamper-proof
1220
+ * but readable without a database query. Short TTL (5 min) ensures freshness.
1221
+ */
1222
+ setSessionCookie(data) {
1223
+ const payload = { ...data, iat: Date.now() };
1224
+ this.cookieService.setSigned(this.sessionCookieName, JSON.stringify(payload), this.sessionSecret, {
1225
+ maxAge: this.sessionMaxAge,
1226
+ httpOnly: true,
1227
+ sameSite: "Lax",
1228
+ path: "/"
1229
+ });
1230
+ }
1231
+ /**
1232
+ * Read and verify the session cookie. Returns null if missing, expired,
1233
+ * or signature verification fails.
1234
+ */
1235
+ getSessionCookie() {
1236
+ const raw = this.cookieService.getSigned(this.sessionCookieName, this.sessionSecret);
1237
+ if (!raw)
1238
+ return null;
1239
+ try {
1240
+ const data = JSON.parse(raw);
1241
+ const age = Date.now() - data.iat;
1242
+ if (age > this.sessionMaxAge * 1e3)
1243
+ return null;
1244
+ return data;
1245
+ } catch {
1246
+ return null;
1247
+ }
1248
+ }
1249
+ /**
1250
+ * Clear the session cookie (on logout, password change, etc.)
1251
+ */
1252
+ clearSessionCookie() {
1253
+ this.cookieService.delete(this.sessionCookieName);
1254
+ }
1202
1255
  };
1203
1256
  __decorate8([
1204
1257
  Inject4(AUTH_CONFIG),
@@ -1647,6 +1700,7 @@ TokenService = __decorate10([
1647
1700
 
1648
1701
  // src/auth/AuthService.ts
1649
1702
  import timestring3 from "timestring";
1703
+ import jwt2 from "jsonwebtoken";
1650
1704
  var __decorate11 = function(decorators, target, key, desc) {
1651
1705
  var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
1652
1706
  if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
@@ -1697,6 +1751,21 @@ var AuthService = class AuthService2 {
1697
1751
  const durationMs = timestring3(this.config.lockout.duration, "ms");
1698
1752
  return new Date(Date.now() + durationMs).toISOString();
1699
1753
  }
1754
+ /**
1755
+ * Decode JWT payload without verification (token was just generated by us).
1756
+ * Used to extract accurate roles/permissions for the session cookie cache.
1757
+ */
1758
+ decodeAccessToken(accessToken) {
1759
+ try {
1760
+ const decoded = jwt2.decode(accessToken);
1761
+ return {
1762
+ roles: decoded?.roles ?? [],
1763
+ permissions: decoded?.permissions ?? []
1764
+ };
1765
+ } catch {
1766
+ return { roles: [], permissions: [] };
1767
+ }
1768
+ }
1700
1769
  async registerUser(body) {
1701
1770
  return await this.userService.create(body);
1702
1771
  }
@@ -1733,17 +1802,33 @@ var AuthService = class AuthService2 {
1733
1802
  this.cookieManager.setRefreshToken(data.refreshToken);
1734
1803
  await this.userService.updateLastLogin(user.id);
1735
1804
  const { password: _, failedLoginAttempts: __, lockoutUntil: ___, ...sanitized } = user;
1805
+ const { roles, permissions } = this.decodeAccessToken(data.accessToken);
1806
+ this.cookieManager.setSessionCookie({
1807
+ user: { id: sanitized.id, email: sanitized.email, name: sanitized.name, role: sanitized.role },
1808
+ roles,
1809
+ permissions
1810
+ });
1736
1811
  return { ...data, user: sanitized };
1737
1812
  }
1738
1813
  async refreshTokens() {
1739
1814
  const data = await this.tokenService.refreshTokens();
1740
1815
  this.cookieManager.setRefreshToken(data.refreshToken);
1816
+ const user = await this.tokenService.getUser(`Bearer ${data.accessToken}`);
1817
+ if (user) {
1818
+ const { roles, permissions } = this.decodeAccessToken(data.accessToken);
1819
+ this.cookieManager.setSessionCookie({
1820
+ user: { id: user.id, email: user.email, name: user.name, role: user.role },
1821
+ roles,
1822
+ permissions
1823
+ });
1824
+ }
1741
1825
  return data;
1742
1826
  }
1743
1827
  async logoutUser(userId, authorization) {
1744
1828
  await this.userValidator.checkUserExists(userId);
1745
1829
  await this.tokenService.logout(userId, authorization);
1746
1830
  this.cookieManager.clearRefreshToken();
1831
+ this.cookieManager.clearSessionCookie();
1747
1832
  return { data: null, message: this.t("auth.success.logout") };
1748
1833
  }
1749
1834
  async getUserProfile(userData) {
@@ -1762,16 +1847,37 @@ var AuthService = class AuthService2 {
1762
1847
  /**
1763
1848
  * Get current user — prefer access token (no cookie rotation risk),
1764
1849
  * fall back to cookie when no Authorization header is present.
1850
+ *
1851
+ * Refreshes the session cookie cache only when authoritative roles/permissions
1852
+ * are available (from the access token JWT). When called via the refresh-cookie
1853
+ * fallback, we cannot reliably populate permissions without a DB round-trip,
1854
+ * so we leave the existing cache alone — it will be refreshed by the next
1855
+ * login/refresh cycle.
1765
1856
  */
1766
1857
  async getMe(authorization) {
1858
+ let result;
1859
+ let cachePayload = null;
1767
1860
  if (authorization) {
1768
1861
  const user = await this.tokenService.getUser(authorization);
1769
1862
  if (user) {
1770
1863
  const lang = this.i18nService.getCurrentLanguage();
1771
- return { ...user, language: lang };
1864
+ result = { ...user, language: lang };
1865
+ const token = authorization.replace(/^Bearer\s+/i, "");
1866
+ cachePayload = this.decodeAccessToken(token);
1867
+ } else {
1868
+ result = await this.getUserFromCookie();
1772
1869
  }
1870
+ } else {
1871
+ result = await this.getUserFromCookie();
1872
+ }
1873
+ if (cachePayload) {
1874
+ this.cookieManager.setSessionCookie({
1875
+ user: { id: result.id, email: result.email, name: result.name, role: result.role },
1876
+ roles: cachePayload.roles,
1877
+ permissions: cachePayload.permissions
1878
+ });
1773
1879
  }
1774
- return this.getUserFromCookie();
1880
+ return result;
1775
1881
  }
1776
1882
  async forgotPassword(email2) {
1777
1883
  const user = await this.userService.findByEmail(email2);
@@ -1803,6 +1909,7 @@ var AuthService = class AuthService2 {
1803
1909
  await this.tokenService.invalidateUserAccessTokens(userId);
1804
1910
  await this.tokenService.revokeToken(userId);
1805
1911
  this.cookieManager.clearRefreshToken();
1912
+ this.cookieManager.clearSessionCookie();
1806
1913
  return { message: this.t("success.passwordChanged") };
1807
1914
  }
1808
1915
  async resetPassword(token, newPassword) {
@@ -1812,6 +1919,7 @@ var AuthService = class AuthService2 {
1812
1919
  await this.tokenService.invalidateUserAccessTokens(userId);
1813
1920
  await this.tokenService.revokeToken(userId);
1814
1921
  this.cookieManager.clearRefreshToken();
1922
+ this.cookieManager.clearSessionCookie();
1815
1923
  return { message: this.t("success.passwordReset") };
1816
1924
  }
1817
1925
  };
@@ -4038,6 +4146,12 @@ var mergeConfig = /* @__PURE__ */ __name((config) => {
4038
4146
  lockout: {
4039
4147
  maxAttempts: config?.lockout?.maxAttempts ?? 5,
4040
4148
  duration: config?.lockout?.duration ?? "15m"
4149
+ },
4150
+ session: {
4151
+ name: config?.session?.name ?? "najm.session",
4152
+ maxAge: config?.session?.maxAge ?? 300,
4153
+ secret: config?.session?.secret
4154
+ // fallback to jwt.accessSecret at use site
4041
4155
  }
4042
4156
  };
4043
4157
  if (!finalConfig.jwt.accessSecret) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "najm-auth",
3
- "version": "1.1.20",
3
+ "version": "1.1.21",
4
4
  "description": "Authentication and authorization library for najm framework",
5
5
  "type": "module",
6
6
  "files": [