najm-auth 3.1.5 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -766,13 +766,14 @@ auth({
766
766
 
767
767
  ## Next.js App Router Structure
768
768
 
769
- Every App Router application keeps the same three files. Copying more than this
769
+ Every App Router application keeps the same four files. Copying more than this
770
770
  between apps means logic that belongs in the package has leaked into them.
771
771
 
772
772
  ```text
773
773
  src/lib/auth.ts defineAuth() configuration — browser, server, and proxy safe
774
774
  src/lib/session.ts one createReactServerAuth() instance for Server Components
775
- src/proxy.ts imports auth.ts only, and exports auth.middleware
775
+ src/proxy.ts exports auth.proxy plus Next's required static matcher
776
+ src/app/api/[...route]/route.ts binds the server through auth.routeHandlers()
776
777
  ```
777
778
 
778
779
  ```typescript
@@ -786,6 +787,7 @@ export const auth = defineAuth({
786
787
  publicRoutes: ['/', '/login'],
787
788
  protectedRoutes: ['/dashboard/:path*', '/admin/:path*'],
788
789
  roleRoutes: { '/admin/:path*': ['admin'] },
790
+ proxySessionMode: 'optimistic',
789
791
  });
790
792
  ```
791
793
 
@@ -804,10 +806,33 @@ export const serverAuth = createReactServerAuth(auth);
804
806
  // src/proxy.ts
805
807
  import { auth } from './lib/auth';
806
808
 
807
- export default auth.middleware;
808
- export const config = auth.config;
809
+ export default auth.proxy;
810
+ export const config = {
811
+ matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
812
+ };
809
813
  ```
810
814
 
815
+ Next.js 16 requires the exported Proxy `config` to be a statically analyzable
816
+ object literal. Turbopack rejects `export const config = auth.config`, so the
817
+ matcher is the one integration value that cannot be composed at runtime.
818
+
819
+ ```typescript
820
+ // src/app/api/[...route]/route.ts
821
+ import { handle } from 'najm-core';
822
+ import server from '@app/server';
823
+
824
+ import { auth } from '../../../lib/auth';
825
+
826
+ const handlers = auth.routeHandlers(handle(server));
827
+ export const { GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS } = handlers;
828
+ ```
829
+
830
+ `auth.routeHandlers()` applies the remember-me lifecycle to login, refresh,
831
+ credential setup, and logout for every supported Next.js verb. It automatically
832
+ uses the refresh and signed-session cookie names from `defineAuth()`; an app only
833
+ passes an option when it intentionally customizes behavior, such as
834
+ `{ rememberCookieName: 'school.remember' }`.
835
+
811
836
  ### Why `session.ts` exists
812
837
 
813
838
  A Next.js page is not one function. The root layout, each nested layout, and the
@@ -862,7 +887,7 @@ the same reason in mirror image:
862
887
  | the `createReactServerAuth()` module | server only | browser, Edge |
863
888
 
864
889
  `auth.client` and `auth.api` are what Client Components call, and
865
- `auth.middleware` is what the Edge proxy calls, so the `defineAuth()` module is
890
+ `auth.proxy` is what the Edge proxy calls, so the `defineAuth()` module is
866
891
  always in the browser and Edge graphs. The adapter must never be. Putting both
867
892
  in one file puts the adapter everywhere `auth` already is, and the `browser`
868
893
  export condition — which exists precisely to catch this — resolves to a module
@@ -910,13 +935,19 @@ into a silently anonymous page.
910
935
  |---|---|
911
936
  | `loginRoute`, `forbiddenRoute`, route matchers, `roleRoutes` | when to redirect where |
912
937
  | cookie names, `apiBaseURL`, `authPrefix`, recovery URL | request memoization |
913
- | `refreshThreshold`, `tabSync`, `verifyAlways` | strict vs optional semantics |
938
+ | `refreshThreshold`, `tabSync`, `proxySessionMode` | strict vs optional semantics |
914
939
  | — | `session.roles` / `user.role` fallback |
915
940
  | — | error classification |
916
941
 
917
- If a new app has to copy anything beyond the three files above, that logic
942
+ If a new app has to copy anything beyond the four files above, that logic
918
943
  belongs in the package instead.
919
944
 
945
+ `proxySessionMode: 'optimistic'` is the default and locally verifies the signed
946
+ snapshot, matching Next.js guidance that Proxy is an optimistic routing boundary.
947
+ Use `'authoritative'` only when every protected navigation must also validate
948
+ refresh-session state. The older `verifyAlways` option and `auth.middleware`
949
+ property remain as deprecated compatibility aliases.
950
+
920
951
  ### What a new app must prove
921
952
 
922
953
  At its real Next.js production boundary, not with mocks:
@@ -999,8 +1030,10 @@ throw new HttpError(403, 'Insufficient permissions for this action');
999
1030
  when their public reverse-proxy origin is not reachable from the app process.
1000
1031
  - `onRecoveryFailure` exposes structured, secret-free recovery diagnostics
1001
1032
  without logging anything by default.
1002
- - `verifyAlways` forces that authoritative check on every protected request;
1003
- the default bounds cached role/status staleness to `session.maxAge`.
1033
+ - `proxySessionMode: 'authoritative'` forces that check on every protected
1034
+ request; the default `'optimistic'` mode bounds cached role/status staleness
1035
+ to `session.maxAge`. The deprecated `verifyAlways` flag maps to the same
1036
+ behavior for existing applications.
1004
1037
 
1005
1038
  ### Next.js 16 Reverse-Proxy Recovery
1006
1039
 
@@ -2,6 +2,7 @@ import * as next_server from 'next/server';
2
2
  import { S as SessionRecoveryFailure } from '../sessionRecovery-D5Fa0yZ1.js';
3
3
  export { a as SessionRecoveryErrorDetails, b as SessionRecoveryFailureReason } from '../sessionRecovery-D5Fa0yZ1.js';
4
4
 
5
+ type ProxySessionMode = 'optimistic' | 'authoritative';
5
6
  interface AuthMiddlewareConfig {
6
7
  /** Routes that require authentication (glob patterns) */
7
8
  protectedRoutes?: string[];
@@ -28,8 +29,15 @@ interface AuthMiddlewareConfig {
28
29
  /**
29
30
  * Force authoritative refresh-session validation on every protected request.
30
31
  * This reissues the signed session cookie without rotating refresh tokens.
32
+ * @deprecated Use `proxySessionMode: 'authoritative'` instead.
31
33
  */
32
34
  verifyAlways?: boolean;
35
+ /**
36
+ * How Proxy handles an otherwise valid signed session snapshot.
37
+ * `optimistic` verifies it locally; `authoritative` also checks refresh state.
38
+ * Defaults to `optimistic`.
39
+ */
40
+ proxySessionMode?: ProxySessionMode;
33
41
  /**
34
42
  * Session-recovery endpoint. Relative values resolve against the request
35
43
  * origin. Defaults to `${apiBaseURL}${authPrefix}/session/recover`.
@@ -67,4 +75,4 @@ interface AuthMiddlewareConfig {
67
75
  */
68
76
  declare function withAuthMiddleware(config: AuthMiddlewareConfig): (request: Request) => Promise<next_server.NextResponse<unknown>>;
69
77
 
70
- export { type AuthMiddlewareConfig, SessionRecoveryFailure, withAuthMiddleware };
78
+ export { type AuthMiddlewareConfig, type ProxySessionMode, SessionRecoveryFailure, withAuthMiddleware };
@@ -345,11 +345,13 @@ function withAuthMiddleware(config) {
345
345
  sessionCookieName = "najm.session",
346
346
  sessionSecret,
347
347
  sessionMaxAge,
348
- verifyAlways = false,
348
+ verifyAlways: legacyVerifyAlways = false,
349
+ proxySessionMode,
349
350
  recoveryURL,
350
351
  internalRecoveryURL,
351
352
  onRecoveryFailure
352
353
  } = config;
354
+ const verifyAlways = proxySessionMode === void 0 ? legacyVerifyAlways : proxySessionMode === "authoritative";
353
355
  const resolvedInternalRecoveryURL = resolveInternalRecoveryURL(internalRecoveryURL);
354
356
  return /* @__PURE__ */ __name(async function middleware(request) {
355
357
  const { NextResponse } = await import("next/server");
@@ -1,5 +1,6 @@
1
1
  import { A as AuthUser, R as RetryConfig } from '../../types-BaSfgxqE.js';
2
2
  import { F as FetchClient, N as NajmAuthClient } from '../../NajmAuthClient-ZtXTIUSF.js';
3
+ import { ProxySessionMode } from '../edge.js';
3
4
  export { withAuthMiddleware } from '../edge.js';
4
5
  import { G as GetSessionConfig, S as ServerSession$1 } from '../../getSession-BthP85UA.js';
5
6
  export { A as AuthConfigError, a as AuthTransportError, N as NoSessionError, g as getSession } from '../../getSession-BthP85UA.js';
@@ -88,6 +89,68 @@ interface WithAuthProps<P> {
88
89
  */
89
90
  declare function withAuth<P extends Record<string, unknown> = Record<string, unknown>>(Page: (args: WithAuthProps<P>) => Promise<unknown> | unknown, options?: WithAuthOptions): (props: P) => Promise<unknown>;
90
91
 
92
+ type AuthRouteHandler<Args extends unknown[] = []> = (request: Request, ...args: Args) => Response | Promise<Response>;
93
+ interface NextAuthRouteHandlers<Args extends unknown[] = []> {
94
+ GET: AuthRouteHandler<Args>;
95
+ POST: AuthRouteHandler<Args>;
96
+ PUT: AuthRouteHandler<Args>;
97
+ PATCH: AuthRouteHandler<Args>;
98
+ DELETE: AuthRouteHandler<Args>;
99
+ HEAD: AuthRouteHandler<Args>;
100
+ OPTIONS: AuthRouteHandler<Args>;
101
+ }
102
+ interface AuthCookiePersistenceOptions {
103
+ /**
104
+ * Cookies whose lifetime this rewrites. Defaults to the two najm-auth issues.
105
+ * Anything not named here is passed through untouched.
106
+ */
107
+ authCookieNames?: string[];
108
+ /** Where the one-bit choice is stored. Defaults to `najm.remember`. */
109
+ rememberCookieName?: string;
110
+ /** How long a remembered choice lasts. Defaults to 7 days. */
111
+ maxAgeSeconds?: number;
112
+ /** Paths whose JSON body carries `rememberMe`. Defaults to `/api/auth/login`. */
113
+ loginPaths?: string[];
114
+ /** Paths that end a session and clear the choice. Defaults to `/api/auth/logout`. */
115
+ logoutPaths?: string[];
116
+ /** Paths that reissue cookies and must reapply the stored choice. */
117
+ refreshPaths?: string[];
118
+ /**
119
+ * Paths that finish credential setup. The stored choice is cleared there:
120
+ * the login it was recorded for never produced a session.
121
+ */
122
+ setupCompletionPaths?: string[];
123
+ /**
124
+ * Recognizes a response that has *not* issued a usable session because the
125
+ * user must still set up credentials.
126
+ *
127
+ * Najm's own setup response is recognized without this — supply it only to
128
+ * cover an application-specific shape. Such a response may carry auth
129
+ * cookies anyway, and persisting them would leave a half-authenticated
130
+ * browser that skips the setup step on reload. Returning `true` replaces
131
+ * them with deletions and clears the stored choice.
132
+ */
133
+ isSetupResponse?: (payload: unknown) => boolean;
134
+ }
135
+ /**
136
+ * Strips the lifetime attributes so the browser drops the cookie when it closes.
137
+ *
138
+ * Only the named auth cookies are touched — rewriting an unrelated `Set-Cookie`
139
+ * from the same response would be a silent side effect on someone else's state.
140
+ */
141
+ declare function makeSessionCookie(setCookie: string, authCookieNames?: string[]): string;
142
+ /**
143
+ * Wraps a request handler so the auth cookies it issues match the user's
144
+ * "remember me" choice.
145
+ *
146
+ * ```ts
147
+ * // app/api/[...route]/route.ts
148
+ * const handler = withAuthCookiePersistence((req) => server.fetch(req));
149
+ * export { handler as GET, handler as POST };
150
+ * ```
151
+ */
152
+ declare function withAuthCookiePersistence<Args extends unknown[] = []>(handler: AuthRouteHandler<Args>, options?: AuthCookiePersistenceOptions): AuthRouteHandler<Args>;
153
+
91
154
  interface DefineAuthConfig {
92
155
  /** API base URL (default: '/api') */
93
156
  apiBaseURL?: string;
@@ -138,13 +201,22 @@ interface DefineAuthConfig {
138
201
  recoveryURL?: string | false;
139
202
  /** Loopback-only recovery endpoint for self-hosted reverse-proxy setups. */
140
203
  internalRecoveryURL?: string;
141
- /** Next.js middleware matcher (default: exclude _next, favicon, api) */
204
+ /**
205
+ * Next.js middleware matcher (default: exclude _next, favicon, api).
206
+ * @deprecated Next.js 16 requires a static matcher literal in `proxy.ts`.
207
+ */
142
208
  matcher?: string[];
143
209
  /**
144
210
  * Force authoritative refresh-session validation on every protected request.
145
211
  * Recovery reissues the signed cookie without rotating refresh tokens.
212
+ * @deprecated Use `proxySessionMode: 'authoritative'` instead.
146
213
  */
147
214
  verifyAlways?: boolean;
215
+ /**
216
+ * How Proxy handles a valid signed session snapshot. Defaults to `optimistic`.
217
+ * API and server authorization remain authoritative in either mode.
218
+ */
219
+ proxySessionMode?: ProxySessionMode;
148
220
  /** Secret-free diagnostic hook for failed server or proxy recovery. */
149
221
  onRecoveryFailure?: (failure: SessionRecoveryFailure) => void;
150
222
  }
@@ -171,12 +243,19 @@ interface AuthKit {
171
243
  * ```
172
244
  */
173
245
  requireRole: (roles: string[]) => Promise<ServerSession$1>;
174
- /** Generated Next.js middleware function */
246
+ /** Generated Next.js 16 Proxy function. */
247
+ proxy: (request: Request) => Promise<Response>;
248
+ /** @deprecated Next.js 16 renamed Middleware to Proxy. Use `proxy`. */
175
249
  middleware: (request: Request) => Promise<Response>;
176
- /** Next.js middleware config with matcher */
250
+ /** @deprecated Next.js 16 requires a static config literal in `proxy.ts`. */
177
251
  config: {
178
252
  matcher: string[];
179
253
  };
254
+ /**
255
+ * Bind a Web Request handler to every Next.js Route Handler verb and apply
256
+ * Najm's login, refresh, setup, and logout cookie lifecycle consistently.
257
+ */
258
+ routeHandlers: <Args extends unknown[] = []>(handler: AuthRouteHandler<Args>, options?: AuthCookiePersistenceOptions) => NextAuthRouteHandlers<Args>;
180
259
  /**
181
260
  * Protect a server component — redirects to loginRoute if unauthenticated.
182
261
  * Passes session to the wrapped component.
@@ -219,57 +298,4 @@ interface SafeRedirectOptions {
219
298
  */
220
299
  declare function getSafeRedirectPath(value: string | string[] | undefined | null, options?: SafeRedirectOptions | string): string;
221
300
 
222
- type RequestHandler = (request: Request) => Promise<Response>;
223
- interface AuthCookiePersistenceOptions {
224
- /**
225
- * Cookies whose lifetime this rewrites. Defaults to the two najm-auth issues.
226
- * Anything not named here is passed through untouched.
227
- */
228
- authCookieNames?: string[];
229
- /** Where the one-bit choice is stored. Defaults to `najm.remember`. */
230
- rememberCookieName?: string;
231
- /** How long a remembered choice lasts. Defaults to 7 days. */
232
- maxAgeSeconds?: number;
233
- /** Paths whose JSON body carries `rememberMe`. Defaults to `/api/auth/login`. */
234
- loginPaths?: string[];
235
- /** Paths that end a session and clear the choice. Defaults to `/api/auth/logout`. */
236
- logoutPaths?: string[];
237
- /** Paths that reissue cookies and must reapply the stored choice. */
238
- refreshPaths?: string[];
239
- /**
240
- * Paths that finish credential setup. The stored choice is cleared there:
241
- * the login it was recorded for never produced a session.
242
- */
243
- setupCompletionPaths?: string[];
244
- /**
245
- * Recognizes a response that has *not* issued a usable session because the
246
- * user must still set up credentials.
247
- *
248
- * Najm's own setup response is recognized without this — supply it only to
249
- * cover an application-specific shape. Such a response may carry auth
250
- * cookies anyway, and persisting them would leave a half-authenticated
251
- * browser that skips the setup step on reload. Returning `true` replaces
252
- * them with deletions and clears the stored choice.
253
- */
254
- isSetupResponse?: (payload: unknown) => boolean;
255
- }
256
- /**
257
- * Strips the lifetime attributes so the browser drops the cookie when it closes.
258
- *
259
- * Only the named auth cookies are touched — rewriting an unrelated `Set-Cookie`
260
- * from the same response would be a silent side effect on someone else's state.
261
- */
262
- declare function makeSessionCookie(setCookie: string, authCookieNames?: string[]): string;
263
- /**
264
- * Wraps a request handler so the auth cookies it issues match the user's
265
- * "remember me" choice.
266
- *
267
- * ```ts
268
- * // app/api/[...route]/route.ts
269
- * const handler = withAuthCookiePersistence((req) => server.fetch(req));
270
- * export { handler as GET, handler as POST };
271
- * ```
272
- */
273
- declare function withAuthCookiePersistence(handler: RequestHandler, options?: AuthCookiePersistenceOptions): RequestHandler;
274
-
275
- export { type AuthCookiePersistenceOptions, type AuthKit, type DefineAuthConfig, GetSessionConfig, type SafeRedirectOptions, ServerSession$1 as ServerSession, SessionRecoveryFailure, type WithAuthOptions, type WithAuthProps, createServerClient, defineAuth, getSafeRedirectPath, getServerSession, makeSessionCookie, withAuth, withAuthCookiePersistence };
301
+ export { type AuthCookiePersistenceOptions, type AuthKit, type AuthRouteHandler, type DefineAuthConfig, GetSessionConfig, type NextAuthRouteHandlers, ProxySessionMode, type SafeRedirectOptions, ServerSession$1 as ServerSession, SessionRecoveryFailure, type WithAuthOptions, type WithAuthProps, createServerClient, defineAuth, getSafeRedirectPath, getServerSession, makeSessionCookie, withAuth, withAuthCookiePersistence };
@@ -756,11 +756,13 @@ function withAuthMiddleware(config) {
756
756
  sessionCookieName = "najm.session",
757
757
  sessionSecret,
758
758
  sessionMaxAge,
759
- verifyAlways = false,
759
+ verifyAlways: legacyVerifyAlways = false,
760
+ proxySessionMode,
760
761
  recoveryURL,
761
762
  internalRecoveryURL,
762
763
  onRecoveryFailure
763
764
  } = config;
765
+ const verifyAlways = proxySessionMode === void 0 ? legacyVerifyAlways : proxySessionMode === "authoritative";
764
766
  const resolvedInternalRecoveryURL = resolveInternalRecoveryURL(internalRecoveryURL);
765
767
  return /* @__PURE__ */ __name(async function middleware(request) {
766
768
  const { NextResponse } = await import("next/server");
@@ -1442,157 +1444,6 @@ function attachReactServerInternals(kit, internals) {
1442
1444
  }
1443
1445
  __name(attachReactServerInternals, "attachReactServerInternals");
1444
1446
 
1445
- // src/client/server/defineAuth.ts
1446
- function defineAuth(authConfig = {}) {
1447
- const {
1448
- apiBaseURL = "/api",
1449
- authPrefix = "/auth",
1450
- loginRoute = "/login",
1451
- forbiddenRoute = "/forbidden",
1452
- publicRoutes = [],
1453
- protectedRoutes = [],
1454
- roleRoutes = {},
1455
- cookieName: cookieName2 = "refreshToken",
1456
- sessionCookieName = "najm.session",
1457
- sessionSecret,
1458
- sessionMaxAge,
1459
- recoveryURL,
1460
- internalRecoveryURL,
1461
- matcher = ["/((?!_next/static|_next/image|favicon.ico|api).*)"],
1462
- verifyAlways = false,
1463
- onRecoveryFailure,
1464
- refreshThreshold,
1465
- tabSync,
1466
- channelName,
1467
- timeout,
1468
- retry
1469
- } = authConfig;
1470
- const sessionConfig = {
1471
- baseURL: apiBaseURL,
1472
- authPrefix,
1473
- cookieName: cookieName2,
1474
- sessionCookieName,
1475
- sessionSecret,
1476
- sessionMaxAge,
1477
- recoveryURL,
1478
- internalRecoveryURL,
1479
- onRecoveryFailure
1480
- };
1481
- let _client = null;
1482
- const getClient = /* @__PURE__ */ __name(() => {
1483
- if (_client) return _client;
1484
- _client = createAuthClient({
1485
- baseURL: apiBaseURL,
1486
- authPrefix,
1487
- refreshThreshold,
1488
- tabSync,
1489
- channelName,
1490
- timeout,
1491
- retry
1492
- });
1493
- return _client;
1494
- }, "getClient");
1495
- const getSession2 = /* @__PURE__ */ __name(async (opts) => {
1496
- const { getSession: resolveSession } = await Promise.resolve().then(() => (init_getSession(), getSession_exports));
1497
- return resolveSession({ ...sessionConfig, ...opts });
1498
- }, "getSession");
1499
- const resolveSessionOutcome2 = /* @__PURE__ */ __name(async () => {
1500
- const { resolveSessionOutcome: resolve } = await Promise.resolve().then(() => (init_getSession(), getSession_exports));
1501
- return resolve(sessionConfig);
1502
- }, "resolveSessionOutcome");
1503
- const requireSession = /* @__PURE__ */ __name(async () => {
1504
- const outcome = await resolveSessionOutcome2();
1505
- if (outcome.status === "authenticated") return outcome.session;
1506
- if (redirectsToLogin(outcome)) {
1507
- const { redirect } = await import("next/navigation");
1508
- redirect(loginRoute);
1509
- }
1510
- throw outcome.error;
1511
- }, "requireSession");
1512
- const requireRole = /* @__PURE__ */ __name(async (roles) => {
1513
- const session = await requireSession();
1514
- if (!heldRoles(session).some((role) => roles.includes(role))) {
1515
- const { redirect } = await import("next/navigation");
1516
- redirect(forbiddenRoute);
1517
- }
1518
- return session;
1519
- }, "requireRole");
1520
- const middleware = withAuthMiddleware({
1521
- protectedRoutes,
1522
- publicRoutes,
1523
- loginRoute,
1524
- roleRoutes,
1525
- cookieName: cookieName2,
1526
- apiBaseURL,
1527
- authPrefix,
1528
- sessionCookieName,
1529
- sessionSecret,
1530
- sessionMaxAge,
1531
- recoveryURL,
1532
- internalRecoveryURL,
1533
- verifyAlways,
1534
- onRecoveryFailure
1535
- });
1536
- const protect = /* @__PURE__ */ __name((Page, options) => {
1537
- return /* @__PURE__ */ __name(async function ProtectedPage(props) {
1538
- const session = await getSession2();
1539
- if (!session) {
1540
- const { redirect } = await import("next/navigation");
1541
- redirect(loginRoute);
1542
- }
1543
- if (options?.role) {
1544
- if (!heldRoles(session).includes(options.role)) {
1545
- const { redirect } = await import("next/navigation");
1546
- redirect(forbiddenRoute);
1547
- }
1548
- }
1549
- if (options?.permission) {
1550
- const { matchPermission: matchPermission2 } = await Promise.resolve().then(() => (init_permissions(), permissions_exports));
1551
- const perms = session.permissions ?? session.user.permissions ?? [];
1552
- if (!matchPermission2(perms, options.permission)) {
1553
- const { redirect } = await import("next/navigation");
1554
- redirect(forbiddenRoute);
1555
- }
1556
- }
1557
- return Page({ session, ...props });
1558
- }, "ProtectedPage");
1559
- }, "protect");
1560
- return attachReactServerInternals({
1561
- get client() {
1562
- return getClient();
1563
- },
1564
- get api() {
1565
- return getClient().api;
1566
- },
1567
- getSession: getSession2,
1568
- requireSession,
1569
- requireRole,
1570
- middleware,
1571
- config: { matcher },
1572
- protect
1573
- }, { resolveSessionOutcome: resolveSessionOutcome2, loginRoute, forbiddenRoute });
1574
- }
1575
- __name(defineAuth, "defineAuth");
1576
-
1577
- // src/client/server/safeRedirect.ts
1578
- var DEFAULT_BLOCKED_PREFIXES = ["/api", "/login", "/_next"];
1579
- var ASSET_EXTENSIONS = /\.(?:css|gif|ico|jpe?g|js|json|map|png|svg|webmanifest|webp)$/i;
1580
- function getSafeRedirectPath(value, options = {}) {
1581
- const {
1582
- fallback = "/dashboard",
1583
- blockedPrefixes = DEFAULT_BLOCKED_PREFIXES
1584
- } = typeof options === "string" ? { fallback: options } : options;
1585
- const path = Array.isArray(value) ? value[0] : value;
1586
- if (!path || !path.startsWith("/") || // Protocol-relative: the browser treats `//host/x` as off-site.
1587
- path.startsWith("//") || // A backslash is normalized to a forward slash by some browsers, so
1588
- // `/\evil.test` is another way to spell the case above.
1589
- path.startsWith("/\\") || blockedPrefixes.some((prefix) => path.startsWith(prefix)) || ASSET_EXTENSIONS.test(path.split("?")[0] ?? path)) {
1590
- return fallback;
1591
- }
1592
- return path;
1593
- }
1594
- __name(getSafeRedirectPath, "getSafeRedirectPath");
1595
-
1596
1447
  // src/client/server/authCookiePersistence.ts
1597
1448
  var DEFAULTS = {
1598
1449
  authCookieNames: ["refreshToken", "najm.session"],
@@ -1742,9 +1593,9 @@ function withAuthCookiePersistence(handler, options = {}) {
1742
1593
  statusText: response.statusText
1743
1594
  });
1744
1595
  }, "applyAction");
1745
- return async (request) => {
1596
+ return async (request, ...args) => {
1746
1597
  let action = await resolveAction(request);
1747
- const response = await handler(request);
1598
+ const response = await handler(request, ...args);
1748
1599
  if (!response.ok) return response;
1749
1600
  if (action?.type === "apply" && loginPaths.includes(new URL(request.url).pathname)) {
1750
1601
  const payload = await response.clone().json().catch(() => null);
@@ -1761,6 +1612,176 @@ function withAuthCookiePersistence(handler, options = {}) {
1761
1612
  };
1762
1613
  }
1763
1614
  __name(withAuthCookiePersistence, "withAuthCookiePersistence");
1615
+
1616
+ // src/client/server/defineAuth.ts
1617
+ function defineAuth(authConfig = {}) {
1618
+ const {
1619
+ apiBaseURL = "/api",
1620
+ authPrefix = "/auth",
1621
+ loginRoute = "/login",
1622
+ forbiddenRoute = "/forbidden",
1623
+ publicRoutes = [],
1624
+ protectedRoutes = [],
1625
+ roleRoutes = {},
1626
+ cookieName: cookieName2 = "refreshToken",
1627
+ sessionCookieName = "najm.session",
1628
+ sessionSecret,
1629
+ sessionMaxAge,
1630
+ recoveryURL,
1631
+ internalRecoveryURL,
1632
+ matcher = ["/((?!_next/static|_next/image|favicon.ico|api).*)"],
1633
+ verifyAlways,
1634
+ proxySessionMode,
1635
+ onRecoveryFailure,
1636
+ refreshThreshold,
1637
+ tabSync,
1638
+ channelName,
1639
+ timeout,
1640
+ retry
1641
+ } = authConfig;
1642
+ const sessionConfig = {
1643
+ baseURL: apiBaseURL,
1644
+ authPrefix,
1645
+ cookieName: cookieName2,
1646
+ sessionCookieName,
1647
+ sessionSecret,
1648
+ sessionMaxAge,
1649
+ recoveryURL,
1650
+ internalRecoveryURL,
1651
+ onRecoveryFailure
1652
+ };
1653
+ let _client = null;
1654
+ const getClient = /* @__PURE__ */ __name(() => {
1655
+ if (_client) return _client;
1656
+ _client = createAuthClient({
1657
+ baseURL: apiBaseURL,
1658
+ authPrefix,
1659
+ refreshThreshold,
1660
+ tabSync,
1661
+ channelName,
1662
+ timeout,
1663
+ retry
1664
+ });
1665
+ return _client;
1666
+ }, "getClient");
1667
+ const getSession2 = /* @__PURE__ */ __name(async (opts) => {
1668
+ const { getSession: resolveSession } = await Promise.resolve().then(() => (init_getSession(), getSession_exports));
1669
+ return resolveSession({ ...sessionConfig, ...opts });
1670
+ }, "getSession");
1671
+ const resolveSessionOutcome2 = /* @__PURE__ */ __name(async () => {
1672
+ const { resolveSessionOutcome: resolve } = await Promise.resolve().then(() => (init_getSession(), getSession_exports));
1673
+ return resolve(sessionConfig);
1674
+ }, "resolveSessionOutcome");
1675
+ const requireSession = /* @__PURE__ */ __name(async () => {
1676
+ const outcome = await resolveSessionOutcome2();
1677
+ if (outcome.status === "authenticated") return outcome.session;
1678
+ if (redirectsToLogin(outcome)) {
1679
+ const { redirect } = await import("next/navigation");
1680
+ redirect(loginRoute);
1681
+ }
1682
+ throw outcome.error;
1683
+ }, "requireSession");
1684
+ const requireRole = /* @__PURE__ */ __name(async (roles) => {
1685
+ const session = await requireSession();
1686
+ if (!heldRoles(session).some((role) => roles.includes(role))) {
1687
+ const { redirect } = await import("next/navigation");
1688
+ redirect(forbiddenRoute);
1689
+ }
1690
+ return session;
1691
+ }, "requireRole");
1692
+ const middleware = withAuthMiddleware({
1693
+ protectedRoutes,
1694
+ publicRoutes,
1695
+ loginRoute,
1696
+ roleRoutes,
1697
+ cookieName: cookieName2,
1698
+ apiBaseURL,
1699
+ authPrefix,
1700
+ sessionCookieName,
1701
+ sessionSecret,
1702
+ sessionMaxAge,
1703
+ recoveryURL,
1704
+ internalRecoveryURL,
1705
+ verifyAlways,
1706
+ proxySessionMode,
1707
+ onRecoveryFailure
1708
+ });
1709
+ const routeHandlers = /* @__PURE__ */ __name((handler, options = {}) => {
1710
+ const persistentHandler = withAuthCookiePersistence(handler, {
1711
+ ...options,
1712
+ authCookieNames: options.authCookieNames ?? [cookieName2, sessionCookieName]
1713
+ });
1714
+ return {
1715
+ GET: persistentHandler,
1716
+ POST: persistentHandler,
1717
+ PUT: persistentHandler,
1718
+ PATCH: persistentHandler,
1719
+ DELETE: persistentHandler,
1720
+ HEAD: persistentHandler,
1721
+ OPTIONS: persistentHandler
1722
+ };
1723
+ }, "routeHandlers");
1724
+ const protect = /* @__PURE__ */ __name((Page, options) => {
1725
+ return /* @__PURE__ */ __name(async function ProtectedPage(props) {
1726
+ const session = await getSession2();
1727
+ if (!session) {
1728
+ const { redirect } = await import("next/navigation");
1729
+ redirect(loginRoute);
1730
+ }
1731
+ if (options?.role) {
1732
+ if (!heldRoles(session).includes(options.role)) {
1733
+ const { redirect } = await import("next/navigation");
1734
+ redirect(forbiddenRoute);
1735
+ }
1736
+ }
1737
+ if (options?.permission) {
1738
+ const { matchPermission: matchPermission2 } = await Promise.resolve().then(() => (init_permissions(), permissions_exports));
1739
+ const perms = session.permissions ?? session.user.permissions ?? [];
1740
+ if (!matchPermission2(perms, options.permission)) {
1741
+ const { redirect } = await import("next/navigation");
1742
+ redirect(forbiddenRoute);
1743
+ }
1744
+ }
1745
+ return Page({ session, ...props });
1746
+ }, "ProtectedPage");
1747
+ }, "protect");
1748
+ return attachReactServerInternals({
1749
+ get client() {
1750
+ return getClient();
1751
+ },
1752
+ get api() {
1753
+ return getClient().api;
1754
+ },
1755
+ getSession: getSession2,
1756
+ requireSession,
1757
+ requireRole,
1758
+ proxy: middleware,
1759
+ middleware,
1760
+ config: { matcher },
1761
+ routeHandlers,
1762
+ protect
1763
+ }, { resolveSessionOutcome: resolveSessionOutcome2, loginRoute, forbiddenRoute });
1764
+ }
1765
+ __name(defineAuth, "defineAuth");
1766
+
1767
+ // src/client/server/safeRedirect.ts
1768
+ var DEFAULT_BLOCKED_PREFIXES = ["/api", "/login", "/_next"];
1769
+ var ASSET_EXTENSIONS = /\.(?:css|gif|ico|jpe?g|js|json|map|png|svg|webmanifest|webp)$/i;
1770
+ function getSafeRedirectPath(value, options = {}) {
1771
+ const {
1772
+ fallback = "/dashboard",
1773
+ blockedPrefixes = DEFAULT_BLOCKED_PREFIXES
1774
+ } = typeof options === "string" ? { fallback: options } : options;
1775
+ const path = Array.isArray(value) ? value[0] : value;
1776
+ if (!path || !path.startsWith("/") || // Protocol-relative: the browser treats `//host/x` as off-site.
1777
+ path.startsWith("//") || // A backslash is normalized to a forward slash by some browsers, so
1778
+ // `/\evil.test` is another way to spell the case above.
1779
+ path.startsWith("/\\") || blockedPrefixes.some((prefix) => path.startsWith(prefix)) || ASSET_EXTENSIONS.test(path.split("?")[0] ?? path)) {
1780
+ return fallback;
1781
+ }
1782
+ return path;
1783
+ }
1784
+ __name(getSafeRedirectPath, "getSafeRedirectPath");
1764
1785
  export {
1765
1786
  AuthConfigError,
1766
1787
  AuthTransportError,
package/dist/index.d.ts CHANGED
@@ -705,6 +705,7 @@ declare class UserValidator {
705
705
  * Check if user exists by email
706
706
  */
707
707
  checkUserExistsByEmail(email: string): Promise<{
708
+ password: string;
708
709
  id: string;
709
710
  name: string;
710
711
  createdAt: string;
@@ -713,9 +714,8 @@ declare class UserValidator {
713
714
  emailVerified: boolean;
714
715
  phone: string;
715
716
  phoneVerified: boolean;
716
- password: string;
717
717
  image: string;
718
- status: "active" | "inactive" | "pending";
718
+ status: "active" | "pending" | "inactive";
719
719
  roleId: string;
720
720
  lastLogin: string;
721
721
  failedLoginAttempts: number;
@@ -727,6 +727,7 @@ declare class UserValidator {
727
727
  * Check if email exists in database
728
728
  */
729
729
  checkEmailExists(email: string): Promise<{
730
+ password: string;
730
731
  id: string;
731
732
  name: string;
732
733
  createdAt: string;
@@ -735,9 +736,8 @@ declare class UserValidator {
735
736
  emailVerified: boolean;
736
737
  phone: string;
737
738
  phoneVerified: boolean;
738
- password: string;
739
739
  image: string;
740
- status: "active" | "inactive" | "pending";
740
+ status: "active" | "pending" | "inactive";
741
741
  roleId: string;
742
742
  lastLogin: string;
743
743
  failedLoginAttempts: number;
@@ -1260,8 +1260,8 @@ declare const createUserDto: z.ZodObject<{
1260
1260
  emailVerified: z.ZodDefault<z.ZodBoolean>;
1261
1261
  status: z.ZodOptional<z.ZodEnum<{
1262
1262
  active: "active";
1263
- inactive: "inactive";
1264
1263
  pending: "pending";
1264
+ inactive: "inactive";
1265
1265
  }>>;
1266
1266
  }, z.core.$strip>;
1267
1267
  declare const updateUserDto: z.ZodObject<{
@@ -1273,8 +1273,8 @@ declare const updateUserDto: z.ZodObject<{
1273
1273
  emailVerified: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
1274
1274
  status: z.ZodOptional<z.ZodOptional<z.ZodEnum<{
1275
1275
  active: "active";
1276
- inactive: "inactive";
1277
1276
  pending: "pending";
1277
+ inactive: "inactive";
1278
1278
  }>>>;
1279
1279
  }, z.core.$strip>;
1280
1280
  declare const registerDto: z.ZodObject<{
@@ -1655,6 +1655,7 @@ declare class AuthController {
1655
1655
  registerUser(body: RegisterDto): Promise<SanitizedUser>;
1656
1656
  loginUser(body: LoginDto): Promise<LoginResult>;
1657
1657
  inviteUser(body: InviteUserDto): Promise<Omit<{
1658
+ password: string;
1658
1659
  id: string;
1659
1660
  name: string;
1660
1661
  createdAt: string;
@@ -1663,9 +1664,8 @@ declare class AuthController {
1663
1664
  emailVerified: boolean;
1664
1665
  phone: string;
1665
1666
  phoneVerified: boolean;
1666
- password: string;
1667
1667
  image: string;
1668
- status: "active" | "inactive" | "pending";
1668
+ status: "active" | "pending" | "inactive";
1669
1669
  roleId: string;
1670
1670
  lastLogin: string;
1671
1671
  failedLoginAttempts: number;
@@ -1688,6 +1688,7 @@ declare class AuthController {
1688
1688
  message: string;
1689
1689
  }>;
1690
1690
  userProfile(authorization?: string): Promise<Omit<{
1691
+ password: string;
1691
1692
  id: string;
1692
1693
  name: string;
1693
1694
  createdAt: string;
@@ -1696,9 +1697,8 @@ declare class AuthController {
1696
1697
  emailVerified: boolean;
1697
1698
  phone: string;
1698
1699
  phoneVerified: boolean;
1699
- password: string;
1700
1700
  image: string;
1701
- status: "active" | "inactive" | "pending";
1701
+ status: "active" | "pending" | "inactive";
1702
1702
  roleId: string;
1703
1703
  lastLogin: string;
1704
1704
  failedLoginAttempts: number;
@@ -235,7 +235,7 @@ declare const usersTable: drizzle_orm_pg_core.PgTableWithColumns<{
235
235
  tableName: "users";
236
236
  dataType: "string";
237
237
  columnType: "PgEnumColumn";
238
- data: "active" | "inactive" | "pending";
238
+ data: "active" | "pending" | "inactive";
239
239
  driverParam: string;
240
240
  notNull: false;
241
241
  hasDefault: true;
@@ -1308,7 +1308,7 @@ declare const authSchema: {
1308
1308
  tableName: "users";
1309
1309
  dataType: "string";
1310
1310
  columnType: "PgEnumColumn";
1311
- data: "active" | "inactive" | "pending";
1311
+ data: "active" | "pending" | "inactive";
1312
1312
  driverParam: string;
1313
1313
  notNull: false;
1314
1314
  hasDefault: true;
@@ -252,7 +252,7 @@ declare const usersTable: drizzle_orm_sqlite_core.SQLiteTableWithColumns<{
252
252
  tableName: "users";
253
253
  dataType: "string";
254
254
  columnType: "SQLiteText";
255
- data: "active" | "inactive" | "pending";
255
+ data: "active" | "pending" | "inactive";
256
256
  driverParam: string;
257
257
  notNull: false;
258
258
  hasDefault: true;
@@ -265,7 +265,7 @@ declare const usersTable: drizzle_orm_sqlite_core.SQLiteTableWithColumns<{
265
265
  generated: undefined;
266
266
  }, {}, {
267
267
  length: number;
268
- $type: "active" | "inactive" | "pending";
268
+ $type: "active" | "pending" | "inactive";
269
269
  }>;
270
270
  roleId: drizzle_orm_sqlite_core.SQLiteColumn<{
271
271
  name: "role_id";
@@ -1476,7 +1476,7 @@ declare const authSchema: {
1476
1476
  tableName: "users";
1477
1477
  dataType: "string";
1478
1478
  columnType: "SQLiteText";
1479
- data: "active" | "inactive" | "pending";
1479
+ data: "active" | "pending" | "inactive";
1480
1480
  driverParam: string;
1481
1481
  notNull: false;
1482
1482
  hasDefault: true;
@@ -1489,7 +1489,7 @@ declare const authSchema: {
1489
1489
  generated: undefined;
1490
1490
  }, {}, {
1491
1491
  length: number;
1492
- $type: "active" | "inactive" | "pending";
1492
+ $type: "active" | "pending" | "inactive";
1493
1493
  }>;
1494
1494
  roleId: drizzle_orm_sqlite_core.SQLiteColumn<{
1495
1495
  name: "role_id";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "najm-auth",
3
- "version": "3.1.5",
3
+ "version": "3.2.0",
4
4
  "description": "Authentication and authorization library for najm framework",
5
5
  "type": "module",
6
6
  "files": [