najm-auth 3.4.0 → 4.0.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
@@ -97,11 +97,11 @@ FRONTEND_URL=https://app.example.com
97
97
  GOOGLE_CLIENT_ID=<google-web-client-id>
98
98
  GOOGLE_CLIENT_SECRET=<google-web-client-secret>
99
99
  # Optional for a split frontend/API deployment. Otherwise FRONTEND_URL is used.
100
- GOOGLE_CALLBACK_URL=https://app.example.com/api/auth/oauth/google/callback
101
- # Optional GitHub sign-in
102
- GITHUB_CLIENT_ID=<github-oauth-app-client-id>
103
- GITHUB_CLIENT_SECRET=<github-oauth-app-client-secret>
104
- GITHUB_CALLBACK_URL=https://app.example.com/api/auth/oauth/github/callback
100
+ GOOGLE_CALLBACK_URL=https://app.example.com/api/auth/oauth/google/callback
101
+ # Optional GitHub sign-in
102
+ GITHUB_CLIENT_ID=<github-oauth-app-client-id>
103
+ GITHUB_CLIENT_SECRET=<github-oauth-app-client-secret>
104
+ GITHUB_CALLBACK_URL=https://app.example.com/api/auth/oauth/github/callback
105
105
  ```
106
106
 
107
107
  > ⚠️ **Security:** Generate secrets with `openssl rand -base64 32`
@@ -156,7 +156,7 @@ auth({
156
156
  }
157
157
  }
158
158
 
159
- // Optional external identity providers
159
+ // Optional external identity providers
160
160
  oauth?: {
161
161
  google?: true | {
162
162
  clientId?: string // Or GOOGLE_CLIENT_ID
@@ -166,17 +166,17 @@ auth({
166
166
  errorRedirectPath?: string // Default: /login
167
167
  allowSignup?: boolean // Default: true
168
168
  autoLinkVerifiedEmail?: boolean // Default: false
169
- allowedHostedDomains?: string[] // Validates the Google hd claim
170
- }
171
- github?: true | {
172
- clientId?: string // Or GITHUB_CLIENT_ID
173
- clientSecret?: string // Or GITHUB_CLIENT_SECRET
174
- callbackUrl?: string // Or GITHUB_CALLBACK_URL
175
- frontendCallbackPath?: string // Default: /auth/oauth/callback
176
- errorRedirectPath?: string // Default: /login
177
- allowSignup?: boolean // Default: true
178
- autoLinkVerifiedEmail?: boolean // Default: false
179
- }
169
+ allowedHostedDomains?: string[] // Validates the Google hd claim
170
+ }
171
+ github?: true | {
172
+ clientId?: string // Or GITHUB_CLIENT_ID
173
+ clientSecret?: string // Or GITHUB_CLIENT_SECRET
174
+ callbackUrl?: string // Or GITHUB_CALLBACK_URL
175
+ frontendCallbackPath?: string // Default: /auth/oauth/callback
176
+ errorRedirectPath?: string // Default: /login
177
+ allowSignup?: boolean // Default: true
178
+ autoLinkVerifiedEmail?: boolean // Default: false
179
+ }
180
180
  }
181
181
 
182
182
  // Dependencies (forwarded to plugins)
@@ -346,22 +346,22 @@ import { GoogleLoginButton } from 'najm-auth/client/react';
346
346
  Google accounts are keyed by Google's stable `sub` claim. If an existing Najm
347
347
  user has the same email but is not linked, sign-in fails with
348
348
  `oauth_account_link_required` by default. After password login, call
349
- `client.linkOAuthAccount('google')` to prove control of both accounts. Setting
350
- `autoLinkVerifiedEmail: true` opts into verified-email linking.
351
-
352
- ### GitHub Sign-In
353
-
354
- Enable GitHub with `oauth: { github: true }`. Credentials come from
355
- `GITHUB_CLIENT_ID` and `GITHUB_CLIENT_SECRET`; the callback defaults to
356
- `${FRONTEND_URL}/api/auth/oauth/github/callback`. Register that exact callback
357
- on the GitHub OAuth App, and set `GITHUB_CALLBACK_URL` only for a split-origin
358
- deployment. GitHub login uses authorization code plus PKCE, requests
359
- `user:email`, requires a verified primary email, and keys the durable provider
360
- link by GitHub's numeric user ID.
361
-
362
- The client exposes `loginWithGitHub()`, `useGitHubLogin()`, and the headless
363
- `GitHubLoginButton`; the generic `linkOAuthAccount('github')` method links an
364
- authenticated Najm user.
349
+ `client.linkOAuthAccount('google')` to prove control of both accounts. Setting
350
+ `autoLinkVerifiedEmail: true` opts into verified-email linking.
351
+
352
+ ### GitHub Sign-In
353
+
354
+ Enable GitHub with `oauth: { github: true }`. Credentials come from
355
+ `GITHUB_CLIENT_ID` and `GITHUB_CLIENT_SECRET`; the callback defaults to
356
+ `${FRONTEND_URL}/api/auth/oauth/github/callback`. Register that exact callback
357
+ on the GitHub OAuth App, and set `GITHUB_CALLBACK_URL` only for a split-origin
358
+ deployment. GitHub login uses authorization code plus PKCE, requests
359
+ `user:email`, requires a verified primary email, and keys the durable provider
360
+ link by GitHub's numeric user ID.
361
+
362
+ The client exposes `loginWithGitHub()`, `useGitHubLogin()`, and the headless
363
+ `GitHubLoginButton`; the generic `linkOAuthAccount('github')` method links an
364
+ authenticated Najm user.
365
365
 
366
366
  ### Admin Routes (all require `@isAdmin()`)
367
367
 
@@ -664,7 +664,7 @@ credential_setup_requirements
664
664
  Existing databases must generate and run a migration after upgrading so the
665
665
  new `oauth_accounts`, `credential_setup_sessions`, and
666
666
  `credential_setup_requirements` tables exist. Custom `AuthSchema` objects may
667
- omit `oauthAccounts` while OAuth is disabled, but provider configuration fails
667
+ omit `oauthAccounts` while OAuth is disabled, but provider configuration fails
668
668
  fast unless the custom schema supplies it. Both credential-setup tables are
669
669
  required of a custom schema, because the setup flow is always mounted.
670
670
 
@@ -849,6 +849,31 @@ Next.js 16 requires the exported Proxy `config` to be a statically analyzable
849
849
  object literal. Turbopack rejects `export const config = auth.config`, so the
850
850
  matcher is the one integration value that cannot be composed at runtime.
851
851
 
852
+ When a Proxy generates request-scoped headers for the downstream render, pass
853
+ only the overrides as the optional second argument. Najm merges them over the
854
+ incoming request and preserves them when session recovery replaces the cookie.
855
+ The application still owns any matching response header:
856
+
857
+ ```typescript
858
+ export default async function proxy(request: Request) {
859
+ const nonce = btoa(globalThis.crypto.randomUUID());
860
+ const policy = `script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`;
861
+ const response = await auth.proxy(request, {
862
+ requestHeaders: {
863
+ 'content-security-policy': policy,
864
+ 'x-nonce': nonce,
865
+ },
866
+ });
867
+ response.headers.set('Content-Security-Policy', policy);
868
+ return response;
869
+ }
870
+ ```
871
+
872
+ Authentication reads the original request. `requestHeaders` controls only what
873
+ the successful Next.js render receives. Attempts to override `cookie` or
874
+ `authorization` fail closed; only Najm's validated recovery path may replace
875
+ the cookie after it authorizes the recovered session.
876
+
852
877
  ```typescript
853
878
  // src/app/api/[...route]/route.ts
854
879
  import { handle } from 'najm-core';
@@ -1052,8 +1077,9 @@ throw new HttpError(403, 'Insufficient permissions for this action');
1052
1077
  - Login uses a dummy password hash for missing users to reduce timing leaks.
1053
1078
  - Forgot-password responses avoid email enumeration.
1054
1079
  - Auth routes register `najm-rate` and ship route-level brute-force limits.
1055
- - Session cookies are signed and short-lived; server auth resolution checks
1056
- their session version.
1080
+ - Session cookies are signed, short-lived, and bound to their refresh-token
1081
+ family; server auth resolution checks both the session version and positive
1082
+ family liveness.
1057
1083
  - Expired signed sessions recover through authoritative, non-rotating refresh
1058
1084
  validation; middleware verifies the reissued HMAC before using its claims.
1059
1085
  - Server-side recovery sends only the configured refresh cookie and accepts
@@ -1092,17 +1118,20 @@ errors cannot change the authentication result.
1092
1118
 
1093
1119
  ### Password Reset Tokens
1094
1120
 
1095
- ⚠️ **Current behavior:** Reset tokens use JWT expiry (default 1h) for single-use validation. To add database-backed single-use tokens:
1121
+ Reset and invite links are signed JWTs whose `jti` is stored in the configured
1122
+ cache with the same expiry. `verifyResetToken()` atomically compares and
1123
+ deletes that value, so exactly one concurrent caller can consume a link and a
1124
+ stale link cannot delete the value for a newer one.
1096
1125
 
1097
- ```typescript
1098
- // In AuthService.resetPassword():
1099
- async resetPassword(token: string, newPassword: string) {
1100
- const userId = this.tokenService.verifyResetToken(token);
1101
- // ... update password ...
1102
- // Blacklist the reset token to prevent reuse
1103
- await this.tokenService.blacklistCurrentToken(token);
1104
- }
1105
- ```
1126
+ `AuthService.resetPassword()` validates the replacement password before
1127
+ consumption. Once consumed, a token stays consumed even if the later user
1128
+ mutation fails; restoring it would make the link replayable, so the user must
1129
+ request a new one.
1130
+
1131
+ The built-in memory and Redis drivers implement the required atomic primitive.
1132
+ A custom cache driver may omit `compareAndDelete()` for compatibility with
1133
+ unrelated cache usage, but reset and invite consumption then fails closed. Do
1134
+ not emulate this operation with separate `get()` and `del()` calls.
1106
1135
 
1107
1136
  ### Purpose-Bound Credential Setup
1108
1137
 
@@ -1152,8 +1181,9 @@ consuming it; `cancel()` revokes it and clears the cookie.
1152
1181
  ### Session Management
1153
1182
 
1154
1183
  - Sessions are multi-device: the token table stores one refresh row per login session (keyed by a unique `tokenFamily`), so a user can stay logged in on several devices at once. Logout and rotation are scoped to the current session; password change/reset revoke every session
1184
+ - Revocation changes the refresh row to a durable `revoked` tombstone until its original expiry. Active-session reads and rotations require `status = active`, so losing Redis cannot revive a logged-out database session; expired tombstones are removed by normal session cleanup
1155
1185
  - A stale refresh token presented after the 120-second rotation grace window revokes only that session's family as reuse protection
1156
- - The signed session cookie is accepted for up to its configured TTL (5 minutes by default) without a database or revocation-cache read
1186
+ - The signed session cookie is accepted on the fast path only while Redis positively identifies its family as live and owned by the same user; an unknown cache state falls back to authoritative refresh-row recovery
1157
1187
  - Use `@RateLimit` on logout for DDoS protection
1158
1188
 
1159
1189
  ### Token Blacklist
@@ -1177,8 +1207,24 @@ consuming it; `cancel()` revokes it and clears the cookie.
1177
1207
  ```bash
1178
1208
  bun run test # Run all tests
1179
1209
  bun run test:auth # Run auth tests only
1210
+ bun run --cwd packages/najm-auth test:real-infra # Opt-in PostgreSQL + Redis races
1211
+ bun packages/najm-auth/integration/mailpit-forgot-password/run.ts # Loopback Redis + Mailpit HTTP acceptance
1180
1212
  ```
1181
1213
 
1214
+ The real-infrastructure suite runs only with `NAJM_AUTH_REAL_INFRA=1`. Supply
1215
+ loopback-only `NAJM_AUTH_REAL_POSTGRES_URL` and `NAJM_AUTH_REAL_REDIS_URL` (or
1216
+ the conventional `DATABASE_URL` and `REDIS_URL`). It creates and drops its own
1217
+ randomly named PostgreSQL database and cleans only its unique Redis key prefix;
1218
+ remote endpoints fail before either service is touched.
1219
+
1220
+ The Mailpit acceptance runner requires Redis on `127.0.0.1:6399`, Mailpit SMTP
1221
+ on `127.0.0.1:1025`, and the Mailpit API on `127.0.0.1:8025` by default. It
1222
+ boots the real auth plugin over HTTP with an ephemeral SQLite fixture, proves
1223
+ ignored fields and spoofed forwarding headers cannot buy more reset emails,
1224
+ and removes only its run-specific messages and Redis keys. The three endpoints
1225
+ can be changed with the `NAJM_AUTH_MAILPIT_*` variables, but non-loopback
1226
+ values fail before the fixture is created.
1227
+
1182
1228
  Test files include:
1183
1229
  - `schema.test.ts` — Schema exports validation
1184
1230
  - `auth.test.ts` — Authentication flow
@@ -3,6 +3,19 @@ import { S as SessionRecoveryFailure } from '../sessionRecovery-D5Fa0yZ1.js';
3
3
  export { a as SessionRecoveryErrorDetails, b as SessionRecoveryFailureReason } from '../sessionRecovery-D5Fa0yZ1.js';
4
4
 
5
5
  type ProxySessionMode = 'optimistic' | 'authoritative';
6
+ /**
7
+ * Per-request headers an application proxy needs to expose to the downstream
8
+ * Next.js render. This is primarily useful for request-scoped values such as a
9
+ * Content Security Policy nonce.
10
+ *
11
+ * Values are merged over the incoming request headers. Authentication always
12
+ * evaluates the original request, so these overrides cannot change which
13
+ * principal or cookie Najm authorizes. `authorization` and `cookie` are
14
+ * rejected here; only Najm's validated recovery path may replace the cookie.
15
+ */
16
+ interface AuthProxyOptions {
17
+ requestHeaders?: HeadersInit;
18
+ }
6
19
  interface AuthMiddlewareConfig {
7
20
  /** Routes that require authentication (glob patterns) */
8
21
  protectedRoutes?: string[];
@@ -73,6 +86,6 @@ interface AuthMiddlewareConfig {
73
86
  * };
74
87
  * ```
75
88
  */
76
- declare function withAuthMiddleware(config: AuthMiddlewareConfig): (request: Request) => Promise<next_server.NextResponse<unknown>>;
89
+ declare function withAuthMiddleware(config: AuthMiddlewareConfig): (request: Request, options?: AuthProxyOptions) => Promise<next_server.NextResponse<unknown>>;
77
90
 
78
- export { type AuthMiddlewareConfig, type ProxySessionMode, SessionRecoveryFailure, withAuthMiddleware };
91
+ export { type AuthMiddlewareConfig, type AuthProxyOptions, type ProxySessionMode, SessionRecoveryFailure, withAuthMiddleware };
@@ -19,6 +19,7 @@ function parseSessionCookiePayload(payload, maxAgeSeconds = DEFAULT_SESSION_MAX_
19
19
  if (!isRecord(data) || !isValidUser(data.user)) return null;
20
20
  if (!isStringArray(data.roles) || !isStringArray(data.permissions)) return null;
21
21
  if (!Number.isInteger(data.sessionVersion) || data.sessionVersion < 0) return null;
22
+ if (typeof data.tokenFamily !== "string" || !data.tokenFamily) return null;
22
23
  if (!Number.isFinite(data.iat) || !Number.isInteger(data.iat) || data.iat <= 0) return null;
23
24
  const issuedAt = data.iat;
24
25
  if (issuedAt > now + MAX_CLOCK_SKEW_MS) return null;
@@ -28,6 +29,7 @@ function parseSessionCookiePayload(payload, maxAgeSeconds = DEFAULT_SESSION_MAX_
28
29
  roles: [...data.roles],
29
30
  permissions: [...data.permissions],
30
31
  sessionVersion: data.sessionVersion,
32
+ tokenFamily: data.tokenFamily,
31
33
  iat: issuedAt
32
34
  };
33
35
  } catch {
@@ -353,8 +355,25 @@ function withAuthMiddleware(config) {
353
355
  } = config;
354
356
  const verifyAlways = proxySessionMode === void 0 ? legacyVerifyAlways : proxySessionMode === "authoritative";
355
357
  const resolvedInternalRecoveryURL = resolveInternalRecoveryURL(internalRecoveryURL);
356
- return /* @__PURE__ */ __name(async function middleware(request) {
358
+ return /* @__PURE__ */ __name(async function middleware(request, options = {}) {
357
359
  const { NextResponse } = await import("next/server");
360
+ const downstreamHeaders = new Headers(request.headers);
361
+ if (options.requestHeaders) {
362
+ const overrides = new Headers(options.requestHeaders);
363
+ for (const identityHeader of ["authorization", "cookie"]) {
364
+ if (overrides.has(identityHeader)) {
365
+ throw new TypeError(
366
+ `Auth proxy requestHeaders cannot override ${identityHeader}`
367
+ );
368
+ }
369
+ }
370
+ overrides.forEach((value, key) => {
371
+ downstreamHeaders.set(key, value);
372
+ });
373
+ }
374
+ const continueRequest = /* @__PURE__ */ __name(() => NextResponse.next({
375
+ request: { headers: downstreamHeaders }
376
+ }), "continueRequest");
358
377
  const redirectToLogin = /* @__PURE__ */ __name((returnPath2, clearCookies) => {
359
378
  const loginUrl = new URL(loginRoute, request.url);
360
379
  loginUrl.searchParams.set("from", returnPath2);
@@ -371,10 +390,10 @@ function withAuthMiddleware(config) {
371
390
  const pathname = url.pathname;
372
391
  const returnPath = `${url.pathname}${url.search}`;
373
392
  if (matchesAny(pathname, publicRoutes)) {
374
- return NextResponse.next();
393
+ return continueRequest();
375
394
  }
376
395
  const isProtected = protectedRoutes.length === 0 || matchesAny(pathname, protectedRoutes);
377
- if (!isProtected) return NextResponse.next();
396
+ if (!isProtected) return continueRequest();
378
397
  const cookie = request.headers.get("cookie") ?? "";
379
398
  const sessionCookie = readCookieValue(cookie, sessionCookieName);
380
399
  const secret = resolveSessionSecret(sessionSecret);
@@ -420,16 +439,15 @@ function withAuthMiddleware(config) {
420
439
  return forbidden;
421
440
  }
422
441
  if (recovery?.status === "recovered") {
423
- const requestHeaders = new Headers(request.headers);
424
- requestHeaders.set(
442
+ downstreamHeaders.set(
425
443
  "cookie",
426
444
  replaceCookieValue(cookie, sessionCookieName, recovery.sessionCookieValue)
427
445
  );
428
- const response = NextResponse.next({ request: { headers: requestHeaders } });
446
+ const response = continueRequest();
429
447
  response.headers.append("Set-Cookie", recovery.setCookie);
430
448
  return response;
431
449
  }
432
- return NextResponse.next();
450
+ return continueRequest();
433
451
  }, "middleware");
434
452
  }
435
453
  __name(withAuthMiddleware, "withAuthMiddleware");
@@ -1,6 +1,6 @@
1
1
  import { A as AuthUser, R as RetryConfig } from '../../types-CI2t8wpJ.js';
2
2
  import { F as FetchClient, N as NajmAuthClient } from '../../NajmAuthClient-BygfBCpF.js';
3
- import { ProxySessionMode } from '../edge.js';
3
+ import { AuthProxyOptions, ProxySessionMode } from '../edge.js';
4
4
  export { withAuthMiddleware } from '../edge.js';
5
5
  import { G as GetSessionConfig, S as ServerSession$1 } from '../../getSession-DBX8XpsR.js';
6
6
  export { A as AuthConfigError, a as AuthTransportError, N as NoSessionError, g as getSession } from '../../getSession-DBX8XpsR.js';
@@ -244,9 +244,9 @@ interface AuthKit {
244
244
  */
245
245
  requireRole: (roles: string[]) => Promise<ServerSession$1>;
246
246
  /** Generated Next.js 16 Proxy function. */
247
- proxy: (request: Request) => Promise<Response>;
247
+ proxy: (request: Request, options?: AuthProxyOptions) => Promise<Response>;
248
248
  /** @deprecated Next.js 16 renamed Middleware to Proxy. Use `proxy`. */
249
- middleware: (request: Request) => Promise<Response>;
249
+ middleware: (request: Request, options?: AuthProxyOptions) => Promise<Response>;
250
250
  /** @deprecated Next.js 16 requires a static config literal in `proxy.ts`. */
251
251
  config: {
252
252
  matcher: string[];
@@ -298,4 +298,4 @@ interface SafeRedirectOptions {
298
298
  */
299
299
  declare function getSafeRedirectPath(value: string | string[] | undefined | null, options?: SafeRedirectOptions | string): string;
300
300
 
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 };
301
+ export { type AuthCookiePersistenceOptions, type AuthKit, AuthProxyOptions, 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 };
@@ -22,6 +22,7 @@ function parseSessionCookiePayload(payload, maxAgeSeconds = DEFAULT_SESSION_MAX_
22
22
  if (!isRecord(data) || !isValidUser(data.user)) return null;
23
23
  if (!isStringArray(data.roles) || !isStringArray(data.permissions)) return null;
24
24
  if (!Number.isInteger(data.sessionVersion) || data.sessionVersion < 0) return null;
25
+ if (typeof data.tokenFamily !== "string" || !data.tokenFamily) return null;
25
26
  if (!Number.isFinite(data.iat) || !Number.isInteger(data.iat) || data.iat <= 0) return null;
26
27
  const issuedAt = data.iat;
27
28
  if (issuedAt > now + MAX_CLOCK_SKEW_MS) return null;
@@ -31,6 +32,7 @@ function parseSessionCookiePayload(payload, maxAgeSeconds = DEFAULT_SESSION_MAX_
31
32
  roles: [...data.roles],
32
33
  permissions: [...data.permissions],
33
34
  sessionVersion: data.sessionVersion,
35
+ tokenFamily: data.tokenFamily,
34
36
  iat: issuedAt
35
37
  };
36
38
  } catch {
@@ -764,8 +766,25 @@ function withAuthMiddleware(config) {
764
766
  } = config;
765
767
  const verifyAlways = proxySessionMode === void 0 ? legacyVerifyAlways : proxySessionMode === "authoritative";
766
768
  const resolvedInternalRecoveryURL = resolveInternalRecoveryURL(internalRecoveryURL);
767
- return /* @__PURE__ */ __name(async function middleware(request) {
769
+ return /* @__PURE__ */ __name(async function middleware(request, options = {}) {
768
770
  const { NextResponse } = await import("next/server");
771
+ const downstreamHeaders = new Headers(request.headers);
772
+ if (options.requestHeaders) {
773
+ const overrides = new Headers(options.requestHeaders);
774
+ for (const identityHeader of ["authorization", "cookie"]) {
775
+ if (overrides.has(identityHeader)) {
776
+ throw new TypeError(
777
+ `Auth proxy requestHeaders cannot override ${identityHeader}`
778
+ );
779
+ }
780
+ }
781
+ overrides.forEach((value, key) => {
782
+ downstreamHeaders.set(key, value);
783
+ });
784
+ }
785
+ const continueRequest = /* @__PURE__ */ __name(() => NextResponse.next({
786
+ request: { headers: downstreamHeaders }
787
+ }), "continueRequest");
769
788
  const redirectToLogin = /* @__PURE__ */ __name((returnPath2, clearCookies) => {
770
789
  const loginUrl = new URL(loginRoute, request.url);
771
790
  loginUrl.searchParams.set("from", returnPath2);
@@ -782,10 +801,10 @@ function withAuthMiddleware(config) {
782
801
  const pathname = url.pathname;
783
802
  const returnPath = `${url.pathname}${url.search}`;
784
803
  if (matchesAny(pathname, publicRoutes)) {
785
- return NextResponse.next();
804
+ return continueRequest();
786
805
  }
787
806
  const isProtected = protectedRoutes.length === 0 || matchesAny(pathname, protectedRoutes);
788
- if (!isProtected) return NextResponse.next();
807
+ if (!isProtected) return continueRequest();
789
808
  const cookie = request.headers.get("cookie") ?? "";
790
809
  const sessionCookie = readCookieValue(cookie, sessionCookieName);
791
810
  const secret = resolveSessionSecret(sessionSecret);
@@ -831,16 +850,15 @@ function withAuthMiddleware(config) {
831
850
  return forbidden;
832
851
  }
833
852
  if (recovery?.status === "recovered") {
834
- const requestHeaders = new Headers(request.headers);
835
- requestHeaders.set(
853
+ downstreamHeaders.set(
836
854
  "cookie",
837
855
  replaceCookieValue(cookie, sessionCookieName, recovery.sessionCookieValue)
838
856
  );
839
- const response = NextResponse.next({ request: { headers: requestHeaders } });
857
+ const response = continueRequest();
840
858
  response.headers.append("Set-Cookie", recovery.setCookie);
841
859
  return response;
842
860
  }
843
- return NextResponse.next();
861
+ return continueRequest();
844
862
  }, "middleware");
845
863
  }
846
864
  __name(withAuthMiddleware, "withAuthMiddleware");