najm-auth 3.1.4 → 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
@@ -272,9 +272,13 @@ login, `AuthSessionService.establish()`, Google OAuth (which redirects with
272
272
  recovery — so verified-email OAuth linking cannot skip it. Marking a new
273
273
  requirement also revokes the user's current sessions.
274
274
 
275
- `withAuthCookiePersistence` recognizes the setup response on its own: it drops
276
- any session cookies the response carried, clears the remembered preference, and
277
- leaves the opaque setup cookie alone.
275
+ `withAuthCookiePersistence` recognizes logout and setup boundaries on its own.
276
+ After a successful logout it drops stale auth-cookie issuances and guarantees
277
+ exactly one deletion for each configured auth cookie. It preserves a valid
278
+ upstream deletion (including a custom cookie path), or synthesizes a canonical
279
+ deletion when one is missing. A setup response gets the same auth-cookie
280
+ deletions, clears the remembered preference, and leaves the opaque setup cookie
281
+ alone.
278
282
 
279
283
  ### Google Sign-In
280
284
 
@@ -721,7 +725,7 @@ limits are active when `auth()` is registered.
721
725
  | Route | Limit | Window | Key Strategy |
722
726
  |-------|-------|--------|--------------|
723
727
  | `POST /auth/register` | 5 | 15 minutes | IP |
724
- | `POST /auth/login` | 8 | 10 minutes | IP + hashed normalized identity |
728
+ | `POST /auth/login` | 8 | 10 minutes | IP + hashed normalized identity |
725
729
  | `POST /auth/refresh` | 15 | 15 minutes | Cookie fingerprint |
726
730
  | `POST /auth/session/recover` | 120 | 1 minute | Cookie fingerprint |
727
731
  | `POST /auth/logout` | 10 | 15 minutes | User ID |
@@ -729,27 +733,27 @@ limits are active when `auth()` is registered.
729
733
  | `POST /auth/forgot-password` | 3 | 15 minutes | IP |
730
734
  | `POST /auth/reset-password` | 5 | 15 minutes | IP |
731
735
 
732
- ### Customizing Rate Limits
733
-
734
- The login route has strict environment overrides. Values are read when the
735
- server imports `najm-auth`, so restart the process after changing them. Invalid
736
- values fail startup rather than silently weakening the limiter.
737
-
738
- ```bash
739
- NAJM_AUTH_LOGIN_RATE_LIMIT_ENABLED=true
740
- NAJM_AUTH_LOGIN_RATE_LIMIT=8
741
- NAJM_AUTH_LOGIN_RATE_WINDOW=10m
742
- ```
743
-
744
- `NAJM_AUTH_LOGIN_RATE_LIMIT_ENABLED=false` disables only the login-route
745
- limiter. Keep it enabled on public production deployments; a shorter window is
746
- the safer setting for a disposable production-built demo.
747
-
748
- The generic plugin configuration remains available for global limits and skip
749
- rules:
750
-
751
- ```typescript
752
- auth({
736
+ ### Customizing Rate Limits
737
+
738
+ The login route has strict environment overrides. Values are read when the
739
+ server imports `najm-auth`, so restart the process after changing them. Invalid
740
+ values fail startup rather than silently weakening the limiter.
741
+
742
+ ```bash
743
+ NAJM_AUTH_LOGIN_RATE_LIMIT_ENABLED=true
744
+ NAJM_AUTH_LOGIN_RATE_LIMIT=8
745
+ NAJM_AUTH_LOGIN_RATE_WINDOW=10m
746
+ ```
747
+
748
+ `NAJM_AUTH_LOGIN_RATE_LIMIT_ENABLED=false` disables only the login-route
749
+ limiter. Keep it enabled on public production deployments; a shorter window is
750
+ the safer setting for a disposable production-built demo.
751
+
752
+ The generic plugin configuration remains available for global limits and skip
753
+ rules:
754
+
755
+ ```typescript
756
+ auth({
753
757
  rateLimit: {
754
758
  keyGenerator: 'ip', // or 'user', 'api-key', 'user+ip'
755
759
  defaultWindow: '10m',
@@ -762,13 +766,14 @@ auth({
762
766
 
763
767
  ## Next.js App Router Structure
764
768
 
765
- 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
766
770
  between apps means logic that belongs in the package has leaked into them.
767
771
 
768
772
  ```text
769
773
  src/lib/auth.ts defineAuth() configuration — browser, server, and proxy safe
770
774
  src/lib/session.ts one createReactServerAuth() instance for Server Components
771
- 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()
772
777
  ```
773
778
 
774
779
  ```typescript
@@ -782,6 +787,7 @@ export const auth = defineAuth({
782
787
  publicRoutes: ['/', '/login'],
783
788
  protectedRoutes: ['/dashboard/:path*', '/admin/:path*'],
784
789
  roleRoutes: { '/admin/:path*': ['admin'] },
790
+ proxySessionMode: 'optimistic',
785
791
  });
786
792
  ```
787
793
 
@@ -800,10 +806,33 @@ export const serverAuth = createReactServerAuth(auth);
800
806
  // src/proxy.ts
801
807
  import { auth } from './lib/auth';
802
808
 
803
- export default auth.middleware;
804
- export const config = auth.config;
809
+ export default auth.proxy;
810
+ export const config = {
811
+ matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
812
+ };
805
813
  ```
806
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
+
807
836
  ### Why `session.ts` exists
808
837
 
809
838
  A Next.js page is not one function. The root layout, each nested layout, and the
@@ -858,7 +887,7 @@ the same reason in mirror image:
858
887
  | the `createReactServerAuth()` module | server only | browser, Edge |
859
888
 
860
889
  `auth.client` and `auth.api` are what Client Components call, and
861
- `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
862
891
  always in the browser and Edge graphs. The adapter must never be. Putting both
863
892
  in one file puts the adapter everywhere `auth` already is, and the `browser`
864
893
  export condition — which exists precisely to catch this — resolves to a module
@@ -906,13 +935,19 @@ into a silently anonymous page.
906
935
  |---|---|
907
936
  | `loginRoute`, `forbiddenRoute`, route matchers, `roleRoutes` | when to redirect where |
908
937
  | cookie names, `apiBaseURL`, `authPrefix`, recovery URL | request memoization |
909
- | `refreshThreshold`, `tabSync`, `verifyAlways` | strict vs optional semantics |
938
+ | `refreshThreshold`, `tabSync`, `proxySessionMode` | strict vs optional semantics |
910
939
  | — | `session.roles` / `user.role` fallback |
911
940
  | — | error classification |
912
941
 
913
- 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
914
943
  belongs in the package instead.
915
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
+
916
951
  ### What a new app must prove
917
952
 
918
953
  At its real Next.js production boundary, not with mocks:
@@ -995,8 +1030,10 @@ throw new HttpError(403, 'Insufficient permissions for this action');
995
1030
  when their public reverse-proxy origin is not reachable from the app process.
996
1031
  - `onRecoveryFailure` exposes structured, secret-free recovery diagnostics
997
1032
  without logging anything by default.
998
- - `verifyAlways` forces that authoritative check on every protected request;
999
- 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.
1000
1037
 
1001
1038
  ### Next.js 16 Reverse-Proxy Recovery
1002
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` strips them
252
- * 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,6 +1444,175 @@ function attachReactServerInternals(kit, internals) {
1442
1444
  }
1443
1445
  __name(attachReactServerInternals, "attachReactServerInternals");
1444
1446
 
1447
+ // src/client/server/authCookiePersistence.ts
1448
+ var DEFAULTS = {
1449
+ authCookieNames: ["refreshToken", "najm.session"],
1450
+ rememberCookieName: "najm.remember",
1451
+ maxAgeSeconds: 7 * 24 * 60 * 60,
1452
+ loginPaths: ["/api/auth/login"],
1453
+ logoutPaths: ["/api/auth/logout"],
1454
+ refreshPaths: ["/api/auth/refresh", "/api/auth/session/recover"],
1455
+ setupCompletionPaths: ["/api/auth/credential-setup/change"]
1456
+ };
1457
+ function isNajmSetupResponse(payload) {
1458
+ if (typeof payload !== "object" || payload === null) return false;
1459
+ const body = payload;
1460
+ if (body.nextStep === "credential_setup") return true;
1461
+ const data = body.data;
1462
+ return typeof data === "object" && data !== null && data.nextStep === "credential_setup";
1463
+ }
1464
+ __name(isNajmSetupResponse, "isNajmSetupResponse");
1465
+ function cookieValue(header, name) {
1466
+ for (const part of header.split(";")) {
1467
+ const separator = part.indexOf("=");
1468
+ if (separator < 0) continue;
1469
+ if (part.slice(0, separator).trim() !== name) continue;
1470
+ return part.slice(separator + 1).trim();
1471
+ }
1472
+ return void 0;
1473
+ }
1474
+ __name(cookieValue, "cookieValue");
1475
+ function cookieName(setCookie) {
1476
+ const separator = setCookie.indexOf("=");
1477
+ return separator < 0 ? "" : setCookie.slice(0, separator).trim();
1478
+ }
1479
+ __name(cookieName, "cookieName");
1480
+ function makeSessionCookie(setCookie, authCookieNames = DEFAULTS.authCookieNames) {
1481
+ if (!authCookieNames.includes(cookieName(setCookie))) return setCookie;
1482
+ return setCookie.split(";").filter((part) => !/^\s*(?:expires|max-age)=/i.test(part)).join(";");
1483
+ }
1484
+ __name(makeSessionCookie, "makeSessionCookie");
1485
+ function isDeletionCookie(setCookie) {
1486
+ if (/^[^=]+=\s*(?:;|$)/.test(setCookie)) return true;
1487
+ if (/(?:^|;)\s*max-age=0(?:;|$)/i.test(setCookie)) return true;
1488
+ const expires = /(?:^|;)\s*expires=([^;]+)/i.exec(setCookie)?.[1];
1489
+ return expires ? new Date(expires).getTime() <= Date.now() : false;
1490
+ }
1491
+ __name(isDeletionCookie, "isDeletionCookie");
1492
+ function rememberCookie(name, mode, secure, maxAgeSeconds) {
1493
+ const attributes = [
1494
+ `${name}=${mode === "persistent" ? "1" : "0"}`,
1495
+ "Path=/",
1496
+ "HttpOnly",
1497
+ "SameSite=Lax"
1498
+ ];
1499
+ if (secure) attributes.push("Secure");
1500
+ if (mode === "persistent") attributes.push(`Max-Age=${maxAgeSeconds}`);
1501
+ return attributes.join("; ");
1502
+ }
1503
+ __name(rememberCookie, "rememberCookie");
1504
+ function clearedRememberCookie(name, secure) {
1505
+ return [
1506
+ `${name}=`,
1507
+ "Path=/",
1508
+ "HttpOnly",
1509
+ "SameSite=Lax",
1510
+ ...secure ? ["Secure"] : [],
1511
+ "Max-Age=0"
1512
+ ].join("; ");
1513
+ }
1514
+ __name(clearedRememberCookie, "clearedRememberCookie");
1515
+ function clearedAuthCookie(name, secure) {
1516
+ return [
1517
+ `${name}=`,
1518
+ "Path=/",
1519
+ "HttpOnly",
1520
+ "SameSite=Lax",
1521
+ ...secure ? ["Secure"] : [],
1522
+ "Expires=Thu, 01 Jan 1970 00:00:00 GMT",
1523
+ "Max-Age=0"
1524
+ ].join("; ");
1525
+ }
1526
+ __name(clearedAuthCookie, "clearedAuthCookie");
1527
+ function withAuthCookiePersistence(handler, options = {}) {
1528
+ const {
1529
+ authCookieNames = DEFAULTS.authCookieNames,
1530
+ rememberCookieName = DEFAULTS.rememberCookieName,
1531
+ maxAgeSeconds = DEFAULTS.maxAgeSeconds,
1532
+ loginPaths = DEFAULTS.loginPaths,
1533
+ logoutPaths = DEFAULTS.logoutPaths,
1534
+ refreshPaths = DEFAULTS.refreshPaths,
1535
+ setupCompletionPaths = DEFAULTS.setupCompletionPaths,
1536
+ isSetupResponse
1537
+ } = options;
1538
+ const resolveAction = /* @__PURE__ */ __name(async (request) => {
1539
+ const { pathname } = new URL(request.url);
1540
+ if (loginPaths.includes(pathname)) {
1541
+ const body = await request.clone().json().catch(() => null);
1542
+ return {
1543
+ type: "apply",
1544
+ mode: body?.rememberMe === true ? "persistent" : "session"
1545
+ };
1546
+ }
1547
+ if (logoutPaths.includes(pathname)) return { type: "clear" };
1548
+ if (setupCompletionPaths.includes(pathname)) return { type: "clear" };
1549
+ if (refreshPaths.includes(pathname)) {
1550
+ const remembered = cookieValue(
1551
+ request.headers.get("cookie") ?? "",
1552
+ rememberCookieName
1553
+ );
1554
+ if (remembered === "0") return { type: "apply", mode: "session" };
1555
+ if (remembered === "1") return { type: "apply", mode: "persistent" };
1556
+ }
1557
+ return null;
1558
+ }, "resolveAction");
1559
+ const applyAction = /* @__PURE__ */ __name((response, action, secure) => {
1560
+ const headers = new Headers(response.headers);
1561
+ const setCookies = headers.getSetCookie();
1562
+ headers.delete("set-cookie");
1563
+ const clearedAuthCookies = /* @__PURE__ */ new Set();
1564
+ for (const setCookie of setCookies) {
1565
+ const name = cookieName(setCookie);
1566
+ const isAuthCookie = authCookieNames.includes(name);
1567
+ if ((action.type === "clear" || action.type === "setup") && isAuthCookie) {
1568
+ if (isDeletionCookie(setCookie) && !clearedAuthCookies.has(name)) {
1569
+ headers.append("set-cookie", setCookie);
1570
+ clearedAuthCookies.add(name);
1571
+ }
1572
+ continue;
1573
+ }
1574
+ headers.append(
1575
+ "set-cookie",
1576
+ action.type === "apply" && action.mode === "session" ? makeSessionCookie(setCookie, authCookieNames) : setCookie
1577
+ );
1578
+ }
1579
+ if (action.type === "clear" || action.type === "setup") {
1580
+ for (const name of authCookieNames) {
1581
+ if (!clearedAuthCookies.has(name)) {
1582
+ headers.append("set-cookie", clearedAuthCookie(name, secure));
1583
+ }
1584
+ }
1585
+ }
1586
+ headers.append(
1587
+ "set-cookie",
1588
+ action.type === "clear" || action.type === "setup" ? clearedRememberCookie(rememberCookieName, secure) : rememberCookie(rememberCookieName, action.mode, secure, maxAgeSeconds)
1589
+ );
1590
+ return new Response(response.body, {
1591
+ headers,
1592
+ status: response.status,
1593
+ statusText: response.statusText
1594
+ });
1595
+ }, "applyAction");
1596
+ return async (request, ...args) => {
1597
+ let action = await resolveAction(request);
1598
+ const response = await handler(request, ...args);
1599
+ if (!response.ok) return response;
1600
+ if (action?.type === "apply" && loginPaths.includes(new URL(request.url).pathname)) {
1601
+ const payload = await response.clone().json().catch(() => null);
1602
+ if (isNajmSetupResponse(payload) || isSetupResponse?.(payload)) {
1603
+ action = { type: "setup" };
1604
+ }
1605
+ }
1606
+ if (!action) return response;
1607
+ return applyAction(
1608
+ response,
1609
+ action,
1610
+ new URL(request.url).protocol === "https:"
1611
+ );
1612
+ };
1613
+ }
1614
+ __name(withAuthCookiePersistence, "withAuthCookiePersistence");
1615
+
1445
1616
  // src/client/server/defineAuth.ts
1446
1617
  function defineAuth(authConfig = {}) {
1447
1618
  const {
@@ -1459,7 +1630,8 @@ function defineAuth(authConfig = {}) {
1459
1630
  recoveryURL,
1460
1631
  internalRecoveryURL,
1461
1632
  matcher = ["/((?!_next/static|_next/image|favicon.ico|api).*)"],
1462
- verifyAlways = false,
1633
+ verifyAlways,
1634
+ proxySessionMode,
1463
1635
  onRecoveryFailure,
1464
1636
  refreshThreshold,
1465
1637
  tabSync,
@@ -1531,8 +1703,24 @@ function defineAuth(authConfig = {}) {
1531
1703
  recoveryURL,
1532
1704
  internalRecoveryURL,
1533
1705
  verifyAlways,
1706
+ proxySessionMode,
1534
1707
  onRecoveryFailure
1535
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");
1536
1724
  const protect = /* @__PURE__ */ __name((Page, options) => {
1537
1725
  return /* @__PURE__ */ __name(async function ProtectedPage(props) {
1538
1726
  const session = await getSession2();
@@ -1567,8 +1755,10 @@ function defineAuth(authConfig = {}) {
1567
1755
  getSession: getSession2,
1568
1756
  requireSession,
1569
1757
  requireRole,
1758
+ proxy: middleware,
1570
1759
  middleware,
1571
1760
  config: { matcher },
1761
+ routeHandlers,
1572
1762
  protect
1573
1763
  }, { resolveSessionOutcome: resolveSessionOutcome2, loginRoute, forbiddenRoute });
1574
1764
  }
@@ -1592,149 +1782,6 @@ function getSafeRedirectPath(value, options = {}) {
1592
1782
  return path;
1593
1783
  }
1594
1784
  __name(getSafeRedirectPath, "getSafeRedirectPath");
1595
-
1596
- // src/client/server/authCookiePersistence.ts
1597
- var DEFAULTS = {
1598
- authCookieNames: ["refreshToken", "najm.session"],
1599
- rememberCookieName: "najm.remember",
1600
- maxAgeSeconds: 7 * 24 * 60 * 60,
1601
- loginPaths: ["/api/auth/login"],
1602
- logoutPaths: ["/api/auth/logout"],
1603
- refreshPaths: ["/api/auth/refresh", "/api/auth/session/recover"],
1604
- setupCompletionPaths: ["/api/auth/credential-setup/change"]
1605
- };
1606
- function isNajmSetupResponse(payload) {
1607
- if (typeof payload !== "object" || payload === null) return false;
1608
- const body = payload;
1609
- if (body.nextStep === "credential_setup") return true;
1610
- const data = body.data;
1611
- return typeof data === "object" && data !== null && data.nextStep === "credential_setup";
1612
- }
1613
- __name(isNajmSetupResponse, "isNajmSetupResponse");
1614
- function cookieValue(header, name) {
1615
- for (const part of header.split(";")) {
1616
- const separator = part.indexOf("=");
1617
- if (separator < 0) continue;
1618
- if (part.slice(0, separator).trim() !== name) continue;
1619
- return part.slice(separator + 1).trim();
1620
- }
1621
- return void 0;
1622
- }
1623
- __name(cookieValue, "cookieValue");
1624
- function cookieName(setCookie) {
1625
- const separator = setCookie.indexOf("=");
1626
- return separator < 0 ? "" : setCookie.slice(0, separator).trim();
1627
- }
1628
- __name(cookieName, "cookieName");
1629
- function makeSessionCookie(setCookie, authCookieNames = DEFAULTS.authCookieNames) {
1630
- if (!authCookieNames.includes(cookieName(setCookie))) return setCookie;
1631
- return setCookie.split(";").filter((part) => !/^\s*(?:expires|max-age)=/i.test(part)).join(";");
1632
- }
1633
- __name(makeSessionCookie, "makeSessionCookie");
1634
- function isDeletionCookie(setCookie) {
1635
- if (/^[^=]+=\s*(?:;|$)/.test(setCookie)) return true;
1636
- if (/(?:^|;)\s*max-age=0(?:;|$)/i.test(setCookie)) return true;
1637
- const expires = /(?:^|;)\s*expires=([^;]+)/i.exec(setCookie)?.[1];
1638
- return expires ? new Date(expires).getTime() <= Date.now() : false;
1639
- }
1640
- __name(isDeletionCookie, "isDeletionCookie");
1641
- function rememberCookie(name, mode, secure, maxAgeSeconds) {
1642
- const attributes = [
1643
- `${name}=${mode === "persistent" ? "1" : "0"}`,
1644
- "Path=/",
1645
- "HttpOnly",
1646
- "SameSite=Lax"
1647
- ];
1648
- if (secure) attributes.push("Secure");
1649
- if (mode === "persistent") attributes.push(`Max-Age=${maxAgeSeconds}`);
1650
- return attributes.join("; ");
1651
- }
1652
- __name(rememberCookie, "rememberCookie");
1653
- function clearedRememberCookie(name, secure) {
1654
- return [
1655
- `${name}=`,
1656
- "Path=/",
1657
- "HttpOnly",
1658
- "SameSite=Lax",
1659
- ...secure ? ["Secure"] : [],
1660
- "Max-Age=0"
1661
- ].join("; ");
1662
- }
1663
- __name(clearedRememberCookie, "clearedRememberCookie");
1664
- function withAuthCookiePersistence(handler, options = {}) {
1665
- const {
1666
- authCookieNames = DEFAULTS.authCookieNames,
1667
- rememberCookieName = DEFAULTS.rememberCookieName,
1668
- maxAgeSeconds = DEFAULTS.maxAgeSeconds,
1669
- loginPaths = DEFAULTS.loginPaths,
1670
- logoutPaths = DEFAULTS.logoutPaths,
1671
- refreshPaths = DEFAULTS.refreshPaths,
1672
- setupCompletionPaths = DEFAULTS.setupCompletionPaths,
1673
- isSetupResponse
1674
- } = options;
1675
- const resolveAction = /* @__PURE__ */ __name(async (request) => {
1676
- const { pathname } = new URL(request.url);
1677
- if (loginPaths.includes(pathname)) {
1678
- const body = await request.clone().json().catch(() => null);
1679
- return {
1680
- type: "apply",
1681
- mode: body?.rememberMe === true ? "persistent" : "session"
1682
- };
1683
- }
1684
- if (logoutPaths.includes(pathname)) return { type: "clear" };
1685
- if (setupCompletionPaths.includes(pathname)) return { type: "clear" };
1686
- if (refreshPaths.includes(pathname)) {
1687
- const remembered = cookieValue(
1688
- request.headers.get("cookie") ?? "",
1689
- rememberCookieName
1690
- );
1691
- if (remembered === "0") return { type: "apply", mode: "session" };
1692
- if (remembered === "1") return { type: "apply", mode: "persistent" };
1693
- }
1694
- return null;
1695
- }, "resolveAction");
1696
- const applyAction = /* @__PURE__ */ __name((response, action, secure) => {
1697
- const headers = new Headers(response.headers);
1698
- const setCookies = headers.getSetCookie();
1699
- headers.delete("set-cookie");
1700
- for (const setCookie of setCookies) {
1701
- if (action.type === "setup" && authCookieNames.includes(cookieName(setCookie)) && !isDeletionCookie(setCookie)) {
1702
- continue;
1703
- }
1704
- headers.append(
1705
- "set-cookie",
1706
- action.type === "apply" && action.mode === "session" ? makeSessionCookie(setCookie, authCookieNames) : setCookie
1707
- );
1708
- }
1709
- headers.append(
1710
- "set-cookie",
1711
- action.type === "clear" || action.type === "setup" ? clearedRememberCookie(rememberCookieName, secure) : rememberCookie(rememberCookieName, action.mode, secure, maxAgeSeconds)
1712
- );
1713
- return new Response(response.body, {
1714
- headers,
1715
- status: response.status,
1716
- statusText: response.statusText
1717
- });
1718
- }, "applyAction");
1719
- return async (request) => {
1720
- let action = await resolveAction(request);
1721
- const response = await handler(request);
1722
- if (!response.ok) return response;
1723
- if (action?.type === "apply" && loginPaths.includes(new URL(request.url).pathname)) {
1724
- const payload = await response.clone().json().catch(() => null);
1725
- if (isNajmSetupResponse(payload) || isSetupResponse?.(payload)) {
1726
- action = { type: "setup" };
1727
- }
1728
- }
1729
- if (!action) return response;
1730
- return applyAction(
1731
- response,
1732
- action,
1733
- new URL(request.url).protocol === "https:"
1734
- );
1735
- };
1736
- }
1737
- __name(withAuthCookiePersistence, "withAuthCookiePersistence");
1738
1785
  export {
1739
1786
  AuthConfigError,
1740
1787
  AuthTransportError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "najm-auth",
3
- "version": "3.1.4",
3
+ "version": "3.2.0",
4
4
  "description": "Authentication and authorization library for najm framework",
5
5
  "type": "module",
6
6
  "files": [