Skip to content

feat: add anonymous sessions support - #2797

Open
tusharpandey13 wants to merge 1 commit into
auth0:mainfrom
tusharpandey13:feat/anonymous-sessions
Open

feat: add anonymous sessions support#2797
tusharpandey13 wants to merge 1 commit into
auth0:mainfrom
tusharpandey13:feat/anonymous-sessions

Conversation

@tusharpandey13

@tusharpandey13 tusharpandey13 commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Description

Adds anonymous sessions to @auth0/nextjs-auth0: pre-login sessions backed by Auth0-issued access tokens. Applications can attach identity and metadata to a visitor before authentication and link that anonymous session at login. The surface is entirely additive and gated behind an anonymousSession.enabled flag — existing applications are unaffected.

What's included

Server API (Auth0Client)

  • getAnonymousSession() — read the current anonymous session. App Router zero-arg form and Pages Router req form. Returns null when there is no session, the feature is disabled, or the cookie is malformed/expired (never throws for those).
  • createAnonymousSession() — create and persist a fresh session. Zero-arg Server Action form and req/res Route Handler form.
  • anonymousSession config block ({ enabled, cookie: { name, sameSite, secure } }) which mounts three routes: GET /auth/anonymous-session, POST /auth/anonymous-session/update, POST /auth/anonymous-session/logout, with NEXT_PUBLIC_* route overrides.

Client API

  • useAnonymousSession() hook returning { anonymous, isLoading, error, invalidate }.
  • Auth0Provider anonymousSession / anonymousSessionRoute props for SSR cache seeding (no loading flash).

Behavior

  • Renewal state machine: valid access token returned as-is; expired access token renewed from the session token in a request/response context; expired session token triggers silent recovery (a fresh session). Read-only Server Component context defers renewal.
  • Metadata updates delegate the merge to the authorization server; the 1KB cap is enforced on the UTF-8 byte length.
  • Typed AnonymousSessionError with a code→HTTP-status mapping (401 invalid_client; 403 feature_not_enabled / unauthorized_client; 500 server_error; 400 otherwise). session_expired / invalid_session_token are silently recovered.

Security

  • Session-token fixation mitigation (three layers): session_token is a reserved authorize parameter (caller-supplied values are stripped), the injected token is sourced only from the SDK's own encrypted cookie, and it is bound to the CSRF-protected transaction state via anonymousSessionLinked.
  • auth0_anon cookie is HttpOnly, Secure by default, JWE-encrypted, with no-store cache headers. Chunked-cookie fragments are cleared on logout.

Testing

  • 117 dedicated anonymous-session tests across server, client, hook, provider, and type suites; full repository suite green (1757 tests), tsc and eslint clean.
  • Coverage spans the full design test matrix (T1–T8, FR-1..FR-18) including the three-layer fixation mitigation.
  • Key regression guards (route dispatch, session-token retention on renewal, UTF-8 metadata cap) were verified to fail if the corresponding fix is reverted.

Documentation

  • New docs/anonymous-sessions.md (enabling, server/client usage, metadata, error handling, security, limitations) and a README link.

Compatibility

Additive and minor-versionable. With anonymousSession unset, no routes are mounted, the methods short-circuit, and no new network calls occur.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added configurable anonymous sessions, including creation, retrieval, renewal, metadata updates, logout, and recovery.
    • Added support across server APIs, client hooks, middleware, App Router, and Pages Router.
    • Anonymous sessions can persist securely and link to interactive login flows.
    • Added typed session data, configuration options, and error handling.
  • Documentation

    • Added comprehensive anonymous-session documentation, configuration guidance, API references, examples, limitations, and security notes.
  • Tests

    • Added extensive coverage for session lifecycle, routing, cookies, recovery, metadata validation, errors, and login integration.

Introduces pre-login anonymous sessions backed by Auth0-issued access
tokens, letting applications attach identity and metadata to a visitor
before they authenticate and link that session at login.

Server API (Auth0Client):
- getAnonymousSession() — read the current anonymous session (App Router
  zero-arg and Pages Router req forms); returns null when absent, disabled,
  or the cookie is malformed/expired.
- createAnonymousSession() — create and persist a fresh session (zero-arg
  Server Action and req/res Route Handler forms).
- anonymousSession config block ({ enabled, cookie: { name, sameSite,
  secure } }) mounts three routes: GET /auth/anonymous-session,
  POST /auth/anonymous-session/update, POST /auth/anonymous-session/logout,
  with NEXT_PUBLIC_* route overrides.

Client API:
- useAnonymousSession() hook ({ anonymous, isLoading, error, invalidate }).
- Auth0Provider anonymousSession / anonymousSessionRoute props for SSR
  cache seeding (no loading flash).

Behavior:
- Renewal state machine: valid access token returned as-is; expired access
  token renewed from the session token in a request/response context;
  expired session token triggers silent recovery (new session). Read-only
  Server Component context defers renewal (D7).
- Metadata updates delegate the merge to the authorization server; 1KB cap
  enforced on the UTF-8 byte length.
- Typed AnonymousSessionError with code→HTTP-status mapping (401
  invalid_client; 403 feature_not_enabled/unauthorized_client; 500
  server_error; 400 otherwise); session_expired/invalid_session_token are
  silently recovered.

Security:
- Session-token fixation mitigation (SEC-1): session_token is a reserved
  authorize parameter (caller values stripped), injected only from the
  SDK's own encrypted cookie, and bound to the CSRF-protected transaction
  state via anonymousSessionLinked.
- auth0_anon cookie is HttpOnly, Secure by default, JWE-encrypted, with
  no-store cache headers; chunked-cookie fragments are cleared on logout.

Additive and fully gated: with anonymousSession unset no routes are
mounted, the methods short-circuit, and no new network calls occur.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@tusharpandey13
tusharpandey13 requested a review from a team as a code owner August 5, 2026 11:02
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 89.74359% with 72 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.73%. Comparing base (bd7c012) to head (eeba1dd).
⚠️ Report is 215 commits behind head on main.

Files with missing lines Patch % Lines
src/server/auth-client.ts 92.81% 37 Missing ⚠️
src/server/client.ts 34.00% 33 Missing ⚠️
src/client/hooks/use-anonymous-session.ts 97.72% 0 Missing and 1 partial ⚠️
src/client/index.ts 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2797      +/-   ##
==========================================
- Coverage   88.36%   87.73%   -0.63%     
==========================================
  Files          77       84       +7     
  Lines       10297    12166    +1869     
  Branches     2145     2511     +366     
==========================================
+ Hits         9099    10674    +1575     
- Misses       1154     1445     +291     
- Partials       44       47       +3     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.


// Loading state (no data yet, no error)
return {
anonymous: data ?? null,
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Anonymous sessions

Layer / File(s) Summary
Contracts and public APIs
src/types/anonymous-session.ts, src/errors/anonymous-session-errors.ts, src/utils/anonymous-session-constants.ts, src/server/client.ts, src/test/defaults.ts, src/server/transaction-store.ts
Adds anonymous-session types, error mapping, cookie constants, configurable routes, transaction state, and public creation and retrieval APIs.
Server lifecycle and route handling
src/server/auth-client.ts
Adds encrypted chunked-cookie storage, token creation and renewal, metadata updates, logout, route handlers, recovery, error responses, and login-session linking.
Client hook and provider integration
src/client/hooks/use-anonymous-session.ts, src/client/providers/auth0-provider.tsx, src/client/index.ts
Adds the SWR-based useAnonymousSession hook and provider cache seeding for anonymous-session data.
Lifecycle and contract validation
src/server/*.test.ts, src/client/**/*.test.*, src/types/anonymous-session.test.ts
Adds coverage for lifecycle flows, cookie behavior, metadata limits, recovery, security constraints, route dispatch, client state, and public APIs.
Anonymous-session documentation
README.md, docs/anonymous-sessions.md
Documents setup, routes, server and client APIs, metadata, logout, recovery, security properties, types, and usage examples.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Auth0Provider
  participant useAnonymousSession
  participant AuthClient
  participant Auth0
  Client->>Auth0Provider: Render with optional anonymousSession
  Auth0Provider->>useAnonymousSession: Seed SWR cache
  Client->>useAnonymousSession: Request anonymous session
  useAnonymousSession->>AuthClient: Fetch session route
  AuthClient->>Auth0: Create or renew session token
  Auth0-->>AuthClient: Return token response
  AuthClient-->>useAnonymousSession: Return session JSON
  useAnonymousSession-->>Client: Expose session and loading/error state
Loading

Possibly related PRs

  • auth0/nextjs-auth0#2608: Both changes extend AuthClient route dispatch and default route or transaction infrastructure.

Suggested reviewers: amitsingh05667

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the primary change: adding anonymous session support.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

🟡 Minor comments (13)
src/client/providers/auth0-provider.test.tsx-165-167 (1)

165-167: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Both env-var tests restore process.env incorrectly. Each finally block assigns the saved value back to process.env. If the variable was unset before the test, the saved value is undefined and Node coerces the assignment to the string "undefined". The variable then stays truthy for every later test in the same worker, and the provider resolves the affected route key to "undefined". Delete the key when the saved value is undefined.

  • src/client/providers/auth0-provider.test.tsx#L165-L167: replace the assignment with a conditional delete for NEXT_PUBLIC_PROFILE_ROUTE.
  • src/client/providers/auth0-provider.test.tsx#L182-L184: replace the assignment with a conditional delete for NEXT_PUBLIC_ANONYMOUS_SESSION_ROUTE.
💚 Proposed fix for both sites
       } finally {
-        process.env.NEXT_PUBLIC_PROFILE_ROUTE = originalEnv;
+        if (originalEnv === undefined) {
+          delete process.env.NEXT_PUBLIC_PROFILE_ROUTE;
+        } else {
+          process.env.NEXT_PUBLIC_PROFILE_ROUTE = originalEnv;
+        }
       }
       } finally {
-        process.env.NEXT_PUBLIC_ANONYMOUS_SESSION_ROUTE = originalEnv;
+        if (originalEnv === undefined) {
+          delete process.env.NEXT_PUBLIC_ANONYMOUS_SESSION_ROUTE;
+        } else {
+          process.env.NEXT_PUBLIC_ANONYMOUS_SESSION_ROUTE = originalEnv;
+        }
       }

Alternatively, use vi.stubEnv with vi.unstubAllEnvs() in an afterEach, which handles the unset case correctly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/client/providers/auth0-provider.test.tsx` around lines 165 - 167, Update
both environment restoration finally blocks in auth0-provider.test.tsx: for
NEXT_PUBLIC_PROFILE_ROUTE at lines 165-167 and
NEXT_PUBLIC_ANONYMOUS_SESSION_ROUTE at lines 182-184, delete the corresponding
process.env key when its saved value is undefined; otherwise restore the saved
value normally.
docs/anonymous-sessions.md-25-25 (1)

25-25: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix the broken table-of-contents link.

Line 25 points to #ending-an-anonymous-session, but the document defines ## Logging Out. Change the link to #logging-out or rename the heading.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/anonymous-sessions.md` at line 25, Update the table-of-contents entry
for “Ending an Anonymous Session” to target the document’s existing “Logging
Out” heading anchor, `#logging-out`, keeping the heading unchanged.

Source: Linters/SAST tools

docs/anonymous-sessions.md-164-166 (1)

164-166: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Check error.code instead of instanceof AnonymousSessionError.

The imported error class is documented as an Error with a code, and the other examples handle anonymous errors through the code field. Update both catch blocks to dispatch on error.code instead of relying on the error-class instance check.

Also applies to: 411-413

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/anonymous-sessions.md` around lines 164 - 166, Update both catch blocks
in the anonymous-session examples to dispatch using the caught error’s code
field instead of instanceof AnonymousSessionError. Preserve the existing
success-false response and error-code handling, and remove the class-instance
check from both locations.

Source: Coding guidelines

src/types/anonymous-session.test.ts-162-244 (1)

162-244: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

These tests assert on local literals, not on SDK behavior, but their names claim otherwise.

Test T1.1 at lines 164-180 is named "AnonymousSession.id must be extracted from JWT sub claim (not assigned)". It builds a plain object, copies mockJWT.sub into extractedId, and asserts that extractedId equals the literal it was just assigned. No SDK function runs. Test T2.1 at lines 220-243 decodes a hardcoded token inline and asserts the sub. It also never calls toPublicSession.

The tests at lines 182-202 assign a field to an AnonymousSession literal and then assert that the field holds the assigned value. These are compile-time type checks written as runtime assertions.

The real coverage of the id-from-sub contract already exists at src/server/auth-client.anonymous-routes.test.ts lines 818-843, where createAnonymousSession runs and session.id is compared to the decoded sub. Remove the tautological tests, or rename them so they do not imply that this file verifies the extraction logic. The current names create false confidence in the coverage of a security-relevant contract.

Note also the empty catch at lines 236-238. It discards the binding e and can hide a decode failure.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/types/anonymous-session.test.ts` around lines 162 - 244, Remove the
tautological runtime tests in the “Type definitions” and “JWT claim extraction
verification” blocks, since they do not invoke SDK behavior; retain only
meaningful type coverage or rename tests to describe type/literal validation
without claiming JWT extraction. Do not duplicate the contract already covered
by createAnonymousSession in the existing route tests, and remove the unused
catch binding in the inline decode logic if that test remains.
src/server/auth-client.anonymous-routes.test.ts-365-392 (1)

365-392: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

The assertion cannot demonstrate the behavior in the test name.

The test is named "Unmentioned keys preserved". The cookie payload carries { a: 1, b: 2, c: 3 } and the update sends { a: 99 }. The default MSW handler at lines 46-60 returns metadata: body.metadata || {}, so the mocked response contains only { a: 99 }. Keys b and c are never returned. The single assertion expect(body.metadata).toHaveProperty("a") passes without proving preservation.

Either override the handler to simulate the merge, as test T3.3 does at lines 270-304, and then assert b and c, or rename the test to describe what it verifies.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/auth-client.anonymous-routes.test.ts` around lines 365 - 392,
Update the T3.5 test around handleUpdateAnonymousSession to actually verify
unmentioned metadata preservation: override the MSW handler like T3.3 so the
response merges the existing cookie metadata with the request, then assert that
b and c remain present alongside the updated a value. Keep the “Unmentioned keys
preserved” name only if these preservation assertions are added.
src/server/client.ts-511-516 (1)

511-516: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

The anonymousSession option doc is wrong about disabled behavior in both declarations. The same sentence, "when disabled, routes are not mounted and methods return null", is duplicated on both option interfaces. getAnonymousSession returns null when disabled, but createAnonymousSession throws AnonymousSessionError("unauthorized_client") (src/server/auth-client.ts lines 2762-2767) because its return type is non-nullable. A consumer who follows this doc writes a null check and receives an unhandled throw.

  • src/server/client.ts#L511-L516: replace the "methods return null" sentence with the split behavior — getAnonymousSession returns null, createAnonymousSession throws AnonymousSessionError with code unauthorized_client.
  • src/server/auth-client.ts#L385-L390: apply the identical correction to the AuthClientOptions.anonymousSession doc block.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/client.ts` around lines 511 - 516, The anonymousSession
documentation incorrectly says all methods return null when disabled. Update the
documentation blocks for anonymousSession in src/server/client.ts lines 511-516
and src/server/auth-client.ts lines 385-390 identically: state that
getAnonymousSession returns null, while createAnonymousSession throws
AnonymousSessionError with code unauthorized_client.
src/server/auth-client.ts-3113-3118 (1)

3113-3118: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Use parseJsonBody(req) for the anonymous request body.

This handler calls req.json() directly instead of the shared JSON parser used by nearby POST handlers, and it parses before the metadata size check. Keep parsing behind parseJsonBody(req) so the body limit and parser behavior apply consistently.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/auth-client.ts` around lines 3113 - 3118, Update the anonymous
request handler to obtain its body through the shared parseJsonBody(req) helper
instead of calling req.json() directly. Preserve the invalid-request response
for parse failures and ensure parsing occurs through the helper before applying
the metadata size check, matching nearby POST handlers.
src/server/auth-client.test.ts-4559-4560 (1)

4559-4560: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add coverage for anonymousSessionLinked: true reaching onCallback.

src/server/anonymous-session.flow.test.ts:626-655 records the transaction-state flag being set, but no test asserts onCallback receives anonymousSessionLinked: true. Add the positive flow case in src/server/auth-client.test.ts or an equivalent callback-flow test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/auth-client.test.ts` around lines 4559 - 4560, Add a positive
callback-flow test near the existing auth-client transaction-state cases, using
the relevant onCallback test setup, that starts with anonymousSessionLinked set
to true and asserts onCallback receives anonymousSessionLinked: true. Keep the
existing false-case coverage unchanged and verify the flag propagates through
the callback payload.
src/server/client.ts-903-913 (1)

903-913: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix the Prettier failure on the reqCookies type annotation.

The Lint Code check fails at line 913. Prettier wants the union collapsed.

🎨 Proposed formatting fix
-    let reqCookies:
-      | RequestCookies
-      | import("./cookies.js").ReadonlyRequestCookies;
+    let reqCookies: RequestCookies | import("./cookies.js").ReadonlyRequestCookies;

Run npx prettier --write src/server/client.ts to apply the exact formatting the check expects.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/client.ts` around lines 903 - 913, Update the reqCookies type
annotation in the client request-cookie initialization to use Prettier’s
collapsed union formatting. Preserve the existing RequestCookies and
ReadonlyRequestCookies types and runtime branching behavior.

Source: Linters/SAST tools

src/server/client.test.ts-1424-1436 (1)

1424-1436: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

The test does not pass the anonymous-session config it claims to test.

The title states "Auth0Client can be instantiated with anonymous session config", but the options object omits anonymousSession. The test passes today and would keep passing if the constructor rejected that option entirely.

Add the config so the assertion matches the title.

💚 Proposed fix
     const testClient = new Auth0Client({
       domain: "test.auth0.com",
       clientId: "test-id",
       clientSecret: "test-secret",
-      secret: "test-secret-32-bytes-minimum-1234567890ab"
+      secret: "test-secret-32-bytes-minimum-1234567890ab",
+      anonymousSession: {
+        enabled: true,
+        cookie: { name: "custom_anon", sameSite: "strict", secure: true }
+      }
     });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/client.test.ts` around lines 1424 - 1436, Update the Auth0Client
instantiation in the “C2/C3: Auth0Client can be instantiated with anonymous
session config” test to include the required anonymousSession configuration, so
the test exercises acceptance of that option while preserving the existing
instance and method assertions.
src/server/auth-client.ts-3265-3284 (1)

3265-3284: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

The catch path diverges from the success path in three ways.

Compare lines 3267-3283 with the success path at lines 3244-3264:

  • The response omits headers: { "content-type": "application/json" }. Next.js then serves the JSON string as text/plain;charset=UTF-8. A client that checks the content type before parsing fails.
  • The response omits addCacheControlHeadersForSession(res). The success path marks the logout response no-store; this path leaves it cacheable.
  • The caught error is discarded without a log. The success path logs the Auth0 call failure at line 3238. Here an unexpected failure in cookie decryption or cookie deletion is completely silent, which makes the path undiagnosable in production.

Make the two paths consistent.

🐛 Proposed fix
     } catch (err) {
+      console.error("Anonymous logout handler error:", err);
       // Even on error, attempt to clear the cookie (and its chunks).
       const res = new NextResponse(JSON.stringify({ ok: true }), {
-        status: 200
+        status: 200,
+        headers: { "content-type": "application/json" }
       });
       deleteChunkedCookie(
         this.anonymousCookieName,
         req.cookies,
         res.cookies,
         false,
         {
           path: this.anonymousCookieOptions.path,
           domain: this.anonymousCookieOptions.domain,
           secure: this.anonymousCookieOptions.secure,
           sameSite: this.anonymousCookieOptions.sameSite,
           httpOnly: this.anonymousCookieOptions.httpOnly
         }
       );
+      addCacheControlHeadersForSession(res);
       return res;
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/auth-client.ts` around lines 3265 - 3284, Update the catch path in
the logout flow to match the success path: create the JSON response with the
application/json content type, apply addCacheControlHeadersForSession to mark it
no-store, and log the caught error using the same failure-logging approach as
the success path before clearing cookies and returning the response.
src/server/client.ts-944-964 (1)

944-964: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Guard the (undefined, res) argument combination.

The implementation validates only one direction. If a caller passes req without res, it throws a clear TypeError. If a caller passes res without req, normalizedReq is undefined, control falls into the App Router branch, and res is silently ignored. The cookie is then written to the next/headers store instead of the caller's response, or cookies() throws an opaque error outside request scope.

getAccessToken guards the mirror case at lines 1083-1087. Apply the same guard here.

🐛 Proposed guard
     let reqCookies: RequestCookies;
     let resCookies: ResponseCookies;
     if (normalizedReq) {
       if (!res) {
         throw new TypeError(
           "createAnonymousSession(req, res): The 'res' argument is missing. Both 'req' and 'res' must be provided together for Route Handler or Pages Router usage."
         );
       }
       reqCookies =
         normalizedReq instanceof NextRequest
           ? normalizedReq.cookies
           : (this.createRequestCookies(normalizedReq) as RequestCookies);
       resCookies = res.cookies;
     } else {
+      if (res !== undefined) {
+        throw new TypeError(
+          "createAnonymousSession(req, res): The 'req' argument is missing. Both 'req' and 'res' must be provided together for Route Handler or Pages Router usage."
+        );
+      }
       // Server Action (App Router): next/headers cookies() is writable here.
       const cookieStore = await cookies();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/client.ts` around lines 944 - 964, Update createAnonymousSession’s
request-context validation around resolveRequestContext to reject the
(undefined, res) combination before entering the App Router cookies() branch.
Mirror the existing getAccessToken guard so a provided response without a
request throws the same clear TypeError, while preserving the current behavior
for valid request/response pairs and Server Actions.
src/server/auth-client.ts-644-644 (1)

644-644: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

path ignores NEXT_PUBLIC_BASE_PATH.

anonymousCookieOptions.path is hard-coded to "/". Every other cookie in the SDK honors the base path. src/server/client.ts line 605-609 resolves the session cookie path as options.session?.cookie?.path ?? process.env.AUTH0_COOKIE_PATH ?? basePath ?? "/", and the transaction cookie does the same at line 622.

Under a Next.js basePath deployment the session cookie is scoped to /app while the anonymous cookie is scoped to /. The anonymous cookie is then sent on requests to sibling applications on the same host.

Resolve the path the same way the other cookies do.

♻️ Proposed fix
     this.anonymousCookieOptions = {
       httpOnly: true,
       secure: anonConfig.cookie?.secure ?? true,
       sameSite: anonConfig.cookie?.sameSite ?? "lax",
-      path: "/"
+      path: process.env.NEXT_PUBLIC_BASE_PATH ?? "/"
     };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/auth-client.ts` at line 644, Update anonymousCookieOptions.path in
the authentication client to resolve the cookie path using the same precedence
as the session and transaction cookies: configured anonymous cookie path,
AUTH0_COOKIE_PATH, the Next.js basePath, then "/". Reuse the existing base-path
resolution symbols and preserve the current anonymous cookie behavior otherwise.
🧹 Nitpick comments (16)
src/client/providers/auth0-provider.test.tsx (1)

38-47: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

These tests cannot fail; assert the seeded SWR value.

expect(container).toBeTruthy() is true for any rendered container. The "seeds SWR cache" test and all four "Route resolution" tests pass even if you delete the fallback construction in auth0-provider.tsx entirely. The suite therefore does not verify FR-8.

Render a consumer that reads the key and assert the seeded value appears without a fetch.

💚 Example of a failing-capable assertion
import useSWR from "swr";

function AnonProbe() {
  const { data } = useSWR<AnonymousSession | null>("/auth/anonymous-session");
  return <span data-testid="anon-id">{data?.id ?? "none"}</span>;
}

it("FR-8: seeds the SWR cache for the default anonymous-session key", () => {
  const { getByTestId } = render(
    <Auth0Provider anonymousSession={mockAnonymousSession}>
      <AnonProbe />
    </Auth0Provider>
  );
  expect(getByTestId("anon-id").textContent).toBe(mockAnonymousSession.id);
});

Apply the same pattern with a custom anonymousSessionRoute to prove the key resolution.

Also applies to: 120-151

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/client/providers/auth0-provider.test.tsx` around lines 38 - 47, Update
the FR-8 test and all route-resolution tests around Auth0Provider to render an
SWR consumer for the configured anonymous-session key, then assert that it
displays the seeded mock session value without relying on container truthiness.
Cover both the default key and a custom anonymousSessionRoute so the tests fail
when fallback construction or key resolution is removed.
src/client/hooks/use-anonymous-session.test.ts (2)

109-136: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a test for NEXT_PUBLIC_ANONYMOUS_SESSION_ROUTE.

The hook resolves the route from options, then the env var, then the default. The suite covers options and the default only. The env-var branch is untested. The provider test file already covers the equivalent branch at its Lines 170-185.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/client/hooks/use-anonymous-session.test.ts` around lines 109 - 136, Add a
test alongside the route-resolution cases in useAnonymousSession.test.ts that
sets NEXT_PUBLIC_ANONYMOUS_SESSION_ROUTE, renders useAnonymousSession without a
route option, and verifies useSWR receives the environment-defined route.
Restore or isolate the environment variable after the test so existing default
and custom-route tests remain unaffected.

23-25: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Restore global.fetch after each test.

Three tests assign global.fetch = vi.fn() at Lines 170, 195, and 221. vi.clearAllMocks() clears call history but does not restore the original global. The last stub stays installed for the rest of the run. Use vi.stubGlobal and unstub in an afterEach.

♻️ Proposed change
-import { beforeEach, describe, expect, it, vi } from "vitest";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
   beforeEach(() => {
     vi.clearAllMocks();
   });
+
+  afterEach(() => {
+    vi.unstubAllGlobals();
+  });

Then replace each global.fetch = vi.fn().mockResolvedValue({...}) with vi.stubGlobal("fetch", vi.fn().mockResolvedValue({...})).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/client/hooks/use-anonymous-session.test.ts` around lines 23 - 25, Update
the use-anonymous-session test setup so mocked fetch is restored after each test
instead of relying on vi.clearAllMocks(), which only resets call history.
Replace the direct global.fetch assignments in the affected tests with
vi.stubGlobal("fetch", ...) and add an afterEach in
use-anonymous-session.test.ts to unstub the global, keeping the existing mock
responses and test behavior unchanged.
src/client/providers/auth0-provider.tsx (1)

57-73: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the anonymous-session route resolution into a shared helper.

Lines 58-62 duplicate the exact resolution order used in src/client/hooks/use-anonymous-session.ts Lines 33-37. The SWR cache key must match between the two, or the seeded fallback is never read and the loading flash returns with no error. Two independent copies of the same precedence chain can drift.

Also note the coupling this creates for consumers: if an application passes anonymousSessionRoute to Auth0Provider but omits route in useAnonymousSession (or the reverse), the keys differ and seeding silently fails. Document this or derive both from one source.

♻️ Proposed shared helper

Add to src/utils/pathUtils.ts (or a client-side route helper):

export const resolveAnonymousSessionKey = (route?: string) =>
  normalizeWithBasePath(
    route ||
      process.env.NEXT_PUBLIC_ANONYMOUS_SESSION_ROUTE ||
      "/auth/anonymous-session"
  );

Then in this file:

-  // Resolve anonymous session route
-  const anonKey = normalizeWithBasePath(
-    anonymousSessionRoute ||
-      process.env.NEXT_PUBLIC_ANONYMOUS_SESSION_ROUTE ||
-      "/auth/anonymous-session"
-  );
+  // Resolve anonymous session route
+  const anonKey = resolveAnonymousSessionKey(anonymousSessionRoute);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/client/providers/auth0-provider.tsx` around lines 57 - 73, Extract the
anonymous-session route precedence logic into a shared helper such as
resolveAnonymousSessionKey, then replace the local anonKey calculation in the
Auth0 provider and the equivalent resolution in useAnonymousSession with that
helper. Ensure both consumers use identical keys, and document or enforce that
explicitly supplied routes must match when configuring Auth0Provider and
useAnonymousSession.
src/client/hooks/use-anonymous-session.ts (2)

40-56: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Include the HTTP status in the thrown error.

The fetcher throws one generic message for every non-OK response. Callers cannot distinguish 401, 429, and 500. Attach the status to the error so consumers can react.

♻️ Proposed change
   >(route, (...args) =>
     fetch(...args).then((res) => {
       if (!res.ok) {
-        throw new Error("Failed to load anonymous session");
+        const err = new Error(
+          `Failed to load anonymous session (status ${res.status})`
+        ) as Error & { status?: number };
+        err.status = res.status;
+        throw err;
       }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/client/hooks/use-anonymous-session.ts` around lines 40 - 56, Update the
fetcher inside useAnonymousSession so the non-OK branch includes the response
status in the thrown error instead of always using the same message. Keep the
existing useSWR and 204/200 handling unchanged, and adjust the error creation in
the fetch(...).then((res) => ...) path so callers of useAnonymousSession can
distinguish statuses like 401, 429, and 500 from the thrown Error.

44-45: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use a single-string fetcher signature for string SWR keys.

For a string key, SWR passes that string as the first fetcher argument. (...args) => fetch(...args) works here, but fetcher: (url: string) => ... is clearer and matches the typed key.
[false_positives_maybe_suggest_optional_refactor]

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/client/hooks/use-anonymous-session.ts` around lines 44 - 45, Update the
fetcher associated with the string SWR key in the anonymous session hook to
accept a single typed url string and pass it to fetch, replacing the variadic
args signature while preserving the existing response handling.
src/server/auth-client.anonymous-routes.test.ts (3)

1108-1110: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the static import for decrypt.

encrypt is already imported statically from ./cookies.js on line 18. The dynamic await import("../server/cookies.js") resolves to the same module through a longer path, and it repeats at lines 1171-1173. Add decrypt to the static import.

♻️ Proposed change
-import { encrypt } from "./cookies.js";
+import { decrypt, encrypt } from "./cookies.js";
-      const decrypted = await (
-        await import("../server/cookies.js")
-      ).decrypt<AnonymousCookiePayload>(renewedCookieValue, secret);
+      const decrypted = await decrypt<AnonymousCookiePayload>(
+        renewedCookieValue,
+        secret
+      );

Apply the same change at lines 1171-1173.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/auth-client.anonymous-routes.test.ts` around lines 1108 - 1110,
Update the static cookies import in the anonymous-route tests to include decrypt
alongside encrypt, then replace both dynamic await import calls around the
renewed cookie assertions with the statically imported decrypt function,
preserving the existing generic payload and arguments.

22-120: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the duplicated test scaffolding into a shared helper.

Lines 22-36 (createMockJWT), lines 44-78 (the MSW server setup), lines 88-110 (the client beforeEach), and lines 112-120 (createSessionCookie) are duplicated almost verbatim in src/server/anonymous-session.flow.test.ts lines 22-120. The only differences are the mock subject (anon@uuid-1234 against anon@uuid-9999) and the fallback session-token prefix. Move the helpers and the handler factory into a shared module under src/test/. Each suite then keeps only its own overrides.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/auth-client.anonymous-routes.test.ts` around lines 22 - 120,
Extract the duplicated createMockJWT, MSW handler/server setup, AuthClient
initialization, and createSessionCookie scaffolding into a shared helper under
src/test/. Update both test suites to consume the shared factory, while
preserving each suite’s mock subject and fallback session-token prefix through
explicit overrides; leave suite-specific configuration in the individual tests.

582-599: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The test name contradicts the test body.

The name states "Auth0 logout 5xx throws error (no 5xx swallow)". The comment on lines 594-595 states that handleAnonymousLogout swallows 5xx by design. The test only exercises anonymousLogoutRequest. Rename the test to describe the network method, for example "anonymousLogoutRequest throws on Auth0 5xx". Consider adding a second case that asserts handleAnonymousLogout still returns 200 and clears the cookie on a 5xx, which is the behavior the comment describes but no test covers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/auth-client.anonymous-routes.test.ts` around lines 582 - 599,
Rename the test case around anonymousLogoutRequest to state that the network
method throws on an Auth0 5xx response, removing the contradictory “no 5xx
swallow” wording. Optionally add a separate test for handleAnonymousLogout that
verifies a 5xx response still returns 200 and clears the cookie.
src/server/anonymous-session.flow.test.ts (2)

123-191: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse the created cookie in Step 2 to make the lifecycle continuous.

Step 1 creates a session and asserts that set-cookie is present. Step 2 then discards that cookie and builds a hand-crafted readPayload with a different session_token. The test title claims a "Create → Read → Update → Logout" flow, but the create step and the read step are not linked. Extract the auth0_anon value from createRes.cookies and send it in the read request. The test then covers the real round trip.

♻️ Proposed change to link Step 1 and Step 2
       // Extract cookies from response (in real flow, client would send these back)
       const setCookieHeader = createRes.headers.get("set-cookie");
       expect(setCookieHeader).toBeTruthy();
 
       // Step 2: Read session (cookie already set in browser)
-      const now = Math.floor(Date.now() / 1000);
-      const readPayload: AnonymousCookiePayload = {
-        session_token: "session-123",
-        access_token: createMockJWT("anon@uuid-9999"),
-        expires_at: now + 3600,
-        session_expires_at: now + 2592000
-      };
-      const readEncrypted = await createSessionCookie(readPayload, secret);
+      const readEncrypted = createRes.cookies.get("auth0_anon")!.value;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/anonymous-session.flow.test.ts` around lines 123 - 191, Update the
comprehensive lifecycle test so Step 2 reuses the auth0_anon cookie created by
createAnonymousSession in createRes.cookies instead of constructing a separate
readPayload and encrypted cookie. Send that extracted cookie in the read
request, preserving the existing read, update, and logout assertions while
linking the flow to the session created in Step 1.

926-936: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Replace the try/catch assertion with rejects.toMatchObject.

If createAnonymousSession resolves on the second call, the catch block never runs and the e.code assertion is skipped without failing the test. The direct form always asserts. The sibling test at src/server/auth-client.anonymous-routes.test.ts lines 991-993 already uses this form.

♻️ Proposed change
       await expect(
         (client as any).createAnonymousSession(req.cookies, res.cookies)
-      ).rejects.toThrow();
-
-      // Verify the error is an AnonymousSessionError with code invalid_client
-      try {
-        await (client as any).createAnonymousSession(req.cookies, res.cookies);
-      } catch (e: any) {
-        expect(e.code).toBe("invalid_client");
-      }
+      ).rejects.toMatchObject({ code: "invalid_client" });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/anonymous-session.flow.test.ts` around lines 926 - 936, Replace
the try/catch assertion around createAnonymousSession with a single
rejects.toMatchObject assertion that verifies code is "invalid_client"; retain
the rejection expectation so a resolved promise fails the test.
src/types/anonymous-session.test.ts (1)

272-303: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Annotate the config objects with AnonymousSessionConfig.

The suite is named "Config validation", but each object is an untyped literal. AnonymousSessionConfig is not imported in this file. A field rename or a removed property in src/types/anonymous-session.ts would not fail these tests. Import the type and annotate each literal, so the compiler checks the shape.

♻️ Proposed change
 import {
   isRecoverableAnonymousError,
   type AnonymousCookiePayload,
-  type AnonymousSession
+  type AnonymousSession,
+  type AnonymousSessionConfig
 } from "./anonymous-session.js";
     it("T8.1: AnonymousSessionConfig has enabled flag", () => {
-      const config = {
+      const config: AnonymousSessionConfig = {
         enabled: false
       };
       expect(config.enabled).toBe(false);
     });
 
     it("T8.3: Cookie name override in config", () => {
-      const config = {
+      const config: AnonymousSessionConfig = {
         enabled: true,
         cookie: { name: "custom_anon" }
       };
       expect(config.cookie?.name).toBe("custom_anon");
     });

Apply the same annotation to the tests at lines 288-302.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/types/anonymous-session.test.ts` around lines 272 - 303, Import
AnonymousSessionConfig in the test file and annotate each config literal in the
“Config validation” cases, including the enabled, cookie name, sameSite, and
secure override tests, so TypeScript validates their fields against the
configuration type.
src/errors/index.ts (1)

78-82: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reconsider exporting the two mapping helpers publicly.

AnonymousSessionError belongs in the public surface so consumers can branch on error.code. getStatusForAnonymousError and mapAnonymousErrorCode are SDK-internal helpers used only by src/server/auth-client.ts. Exporting them commits the SDK to their signatures under semver.

Keep them internal unless consumers need them.

♻️ Proposed narrowing of the public surface
 export {
-  AnonymousSessionError,
-  getStatusForAnonymousError,
-  mapAnonymousErrorCode
+  AnonymousSessionError
 } from "./anonymous-session-errors.js";

src/server/auth-client.ts already imports the helpers directly from ../errors/anonymous-session-errors.js, so no internal call site breaks.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/errors/index.ts` around lines 78 - 82, Update the exports in the errors
barrel to expose only AnonymousSessionError; remove getStatusForAnonymousError
and mapAnonymousErrorCode from the public re-export while leaving their direct
internal imports and implementations unchanged.
src/server/auth-client.ts (2)

3132-3141: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Call readAnonymousCookie instead of repeating the decrypt.

Lines 3133-3136 duplicate readAnonymousCookie (lines 2809-2822) exactly: the same getChunkedCookie call, the same decrypt<AnonymousCookiePayload> call, the same secret. handleAnonymousLogout repeats it a third time at lines 3217-3229.

Keep one decrypt path for the anonymous cookie so any future change to the read logic applies everywhere.

♻️ Proposed refactor
       // Step 2: Read current session to get session_token (DESIGN §5.I4: requires active session)
-      const current = getChunkedCookie(this.anonymousCookieName, req.cookies);
-      const decrypted = current
-        ? await decrypt<AnonymousCookiePayload>(current, this.secret)
-        : null;
+      const payload = await this.readAnonymousCookie(req.cookies);
 
-      if (!decrypted?.payload?.session_token) {
+      if (!payload?.session_token) {
         // No active session → return 400 (cannot present session_token to Auth0)
         return this.anonymousErrorResponse("invalid_session_token", 400);
       }

Update the two later references to decrypted.payload.session_token at lines 3151 and 3160 to payload.session_token.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/auth-client.ts` around lines 3132 - 3141, Replace the duplicated
cookie retrieval and decryption in the surrounding anonymous-session flow with
the existing readAnonymousCookie method, preserving its current error and null
handling. Use the returned payload variable for the session_token references
instead of decrypted.payload.session_token, and apply the same shared read path
in handleAnonymousLogout.

451-451: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the write-only anonymousSessionConfig field.

The constructor stores anonConfig, but the code already uses the flattened fields and extracted values (anonymousSessionEnabled, anonymousCookieName, anonymousCookieOptions).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/auth-client.ts` at line 451, Remove the unused private
anonymousSessionConfig field from the class and stop assigning the constructor’s
anonConfig to it; retain the existing flattened fields anonymousSessionEnabled,
anonymousCookieName, and anonymousCookieOptions.
src/utils/anonymous-session-constants.ts (1)

1-1: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Move transferCookies out of the constants module.

This module holds four plain constants. Adding transferCookies forces a next/server.js import on every consumer of those constants, including client-side code that only needs DEFAULT_ANONYMOUS_SESSION_COOKIE_NAME.

Place the helper in a server-only module, for example src/server/cookies.ts, which already owns setChunkedCookie and deleteChunkedCookie.

Also applies to: 22-26

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/anonymous-session-constants.ts` at line 1, Move transferCookies out
of anonymous-session-constants so src/utils/anonymous-session-constants.ts
remains plain constant exports and no longer imports NextResponse from
next/server.js. Put transferCookies in the server-only cookies module alongside
setChunkedCookie and deleteChunkedCookie, and update any callers to import it
from that server helper instead of the constants module.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/anonymous-sessions.md`:
- Around line 212-216: Update the createAnonymousSession() documentation in the
Return Value section to state that it returns null when anonymous sessions are
disabled, while preserving the documented AnonymousSessionError cases for
authorization failures and authorization-server errors. Align this wording with
the auth-client.ts contract, configuration table, and “Never throws” statement.
- Around line 60-65: Update the client examples in the affected sections to use
NEXT_PUBLIC_ANONYMOUS_SESSION_UPDATE_ROUTE and
NEXT_PUBLIC_ANONYMOUS_SESSION_LOGOUT_ROUTE, falling back to their documented
default paths when unset, instead of hardcoded routes. Apply this consistently
to every update and logout request example while preserving the existing route
behavior.

In `@src/server/anonymous-session.flow.test.ts`:
- Around line 1109-1112: Update all three startInteractiveLogin calls in
src/server/anonymous-session.flow.test.ts:1109-1112, 1127-1133, and 1184-1187 to
pass req as the second positional argument rather than inside the options
object. In the first site, assert the location header excludes
should-not-inject; in the second, assert it excludes attacker-injected; leave
the third focused on exercising the no-cookie guard path.
- Around line 626-669: Update the SEC-1 T6.1 and T6.2 tests to inspect the
decrypted transaction cookie rather than relying on the redirect location. Reuse
the file’s existing decrypt helper and transaction-cookie symbols to assert
anonymousSessionLinked is true for the injected-session case and absent or false
when no session exists; retain only assertions relevant to each test.

In `@src/server/auth-client.ts`:
- Around line 2643-2663: The read-only branches in resolveAnonymousSession must
not propagate malformed access-token errors: wrap both toPublicSession(state)
calls in src/server/auth-client.ts lines 2643-2663 so undecodable payloads
return null, while leaving the writable renewal and creation paths unchanged.
Make no code change in src/server/client.ts lines 880-919; re-verify its
never-throws documentation remains accurate.
- Around line 3120-3130: Update the metadata validation in the handler before
the JSON serialization and byte-size check to accept only non-null plain
objects, rejecting strings, arrays, numbers, and other non-object values with
the existing anonymous error response. Preserve the current size-limit
validation for valid metadata objects and ensure the value forwarded to
toCookiePayload remains compatible with AnonymousCookiePayload.metadata.
- Around line 2681-2687: Replace the inline renewedPayload construction in the
renew flow with the existing toCookiePayload helper, passing the renewal
response and prior state as required by its contract. Preserve the renew
behavior while allowing rotated session_token and server-merged metadata from
the response, matching the existing update handler usage.
- Around line 640-645: Update persistAnonymousCookie to set
anonymousCookieOptions.maxAge immediately before calling setChunkedCookie, using
Math.max(0, expiration - this.epoch()) so the cookie lifetime matches the
encrypted payload’s session_expires_at. Preserve the existing cookie options and
chunking behavior.
- Around line 780-800: Update the anonymous-route dispatch around
handleGetAnonymousSession, handleUpdateAnonymousSession, and
handleAnonymousLogout so matching paths are routed regardless of
anonymousSessionEnabled. Keep method and pathname checks intact, allowing each
handler’s existing disabled-feature guard to return 404 while preserving normal
behavior when the feature is enabled.
- Around line 903-923: Update the anonymous-session cookie lookup in the
handleLogin flow around anonymousSessionLinked so it can read cookies when req
is unavailable, using the request-less next/headers access supported by the
server-component/action path. Preserve request-based cookie reading when req
exists, and ensure the session token is injected and anonymousSessionLinked is
set for programmatic startInteractiveLogin calls as well.
- Around line 2938-2956: Update the anonymous JSON-POST authentication flows
around the request logic at both auth-client.ts sites 2938-2956 and 2995-3023:
apply the callable ClientAuth returned by getClientAuth() explicitly so client
credentials, assertions, and mTLS authentication are attached instead of
filtering only object entries. Ensure the logout flow propagates a 401
invalid_client response rather than treating it as successful.

---

Minor comments:
In `@docs/anonymous-sessions.md`:
- Line 25: Update the table-of-contents entry for “Ending an Anonymous Session”
to target the document’s existing “Logging Out” heading anchor, `#logging-out`,
keeping the heading unchanged.
- Around line 164-166: Update both catch blocks in the anonymous-session
examples to dispatch using the caught error’s code field instead of instanceof
AnonymousSessionError. Preserve the existing success-false response and
error-code handling, and remove the class-instance check from both locations.

In `@src/client/providers/auth0-provider.test.tsx`:
- Around line 165-167: Update both environment restoration finally blocks in
auth0-provider.test.tsx: for NEXT_PUBLIC_PROFILE_ROUTE at lines 165-167 and
NEXT_PUBLIC_ANONYMOUS_SESSION_ROUTE at lines 182-184, delete the corresponding
process.env key when its saved value is undefined; otherwise restore the saved
value normally.

In `@src/server/auth-client.anonymous-routes.test.ts`:
- Around line 365-392: Update the T3.5 test around handleUpdateAnonymousSession
to actually verify unmentioned metadata preservation: override the MSW handler
like T3.3 so the response merges the existing cookie metadata with the request,
then assert that b and c remain present alongside the updated a value. Keep the
“Unmentioned keys preserved” name only if these preservation assertions are
added.

In `@src/server/auth-client.test.ts`:
- Around line 4559-4560: Add a positive callback-flow test near the existing
auth-client transaction-state cases, using the relevant onCallback test setup,
that starts with anonymousSessionLinked set to true and asserts onCallback
receives anonymousSessionLinked: true. Keep the existing false-case coverage
unchanged and verify the flag propagates through the callback payload.

In `@src/server/auth-client.ts`:
- Around line 3113-3118: Update the anonymous request handler to obtain its body
through the shared parseJsonBody(req) helper instead of calling req.json()
directly. Preserve the invalid-request response for parse failures and ensure
parsing occurs through the helper before applying the metadata size check,
matching nearby POST handlers.
- Around line 3265-3284: Update the catch path in the logout flow to match the
success path: create the JSON response with the application/json content type,
apply addCacheControlHeadersForSession to mark it no-store, and log the caught
error using the same failure-logging approach as the success path before
clearing cookies and returning the response.
- Line 644: Update anonymousCookieOptions.path in the authentication client to
resolve the cookie path using the same precedence as the session and transaction
cookies: configured anonymous cookie path, AUTH0_COOKIE_PATH, the Next.js
basePath, then "/". Reuse the existing base-path resolution symbols and preserve
the current anonymous cookie behavior otherwise.

In `@src/server/client.test.ts`:
- Around line 1424-1436: Update the Auth0Client instantiation in the “C2/C3:
Auth0Client can be instantiated with anonymous session config” test to include
the required anonymousSession configuration, so the test exercises acceptance of
that option while preserving the existing instance and method assertions.

In `@src/server/client.ts`:
- Around line 511-516: The anonymousSession documentation incorrectly says all
methods return null when disabled. Update the documentation blocks for
anonymousSession in src/server/client.ts lines 511-516 and
src/server/auth-client.ts lines 385-390 identically: state that
getAnonymousSession returns null, while createAnonymousSession throws
AnonymousSessionError with code unauthorized_client.
- Around line 903-913: Update the reqCookies type annotation in the client
request-cookie initialization to use Prettier’s collapsed union formatting.
Preserve the existing RequestCookies and ReadonlyRequestCookies types and
runtime branching behavior.
- Around line 944-964: Update createAnonymousSession’s request-context
validation around resolveRequestContext to reject the (undefined, res)
combination before entering the App Router cookies() branch. Mirror the existing
getAccessToken guard so a provided response without a request throws the same
clear TypeError, while preserving the current behavior for valid
request/response pairs and Server Actions.

In `@src/types/anonymous-session.test.ts`:
- Around line 162-244: Remove the tautological runtime tests in the “Type
definitions” and “JWT claim extraction verification” blocks, since they do not
invoke SDK behavior; retain only meaningful type coverage or rename tests to
describe type/literal validation without claiming JWT extraction. Do not
duplicate the contract already covered by createAnonymousSession in the existing
route tests, and remove the unused catch binding in the inline decode logic if
that test remains.

---

Nitpick comments:
In `@src/client/hooks/use-anonymous-session.test.ts`:
- Around line 109-136: Add a test alongside the route-resolution cases in
useAnonymousSession.test.ts that sets NEXT_PUBLIC_ANONYMOUS_SESSION_ROUTE,
renders useAnonymousSession without a route option, and verifies useSWR receives
the environment-defined route. Restore or isolate the environment variable after
the test so existing default and custom-route tests remain unaffected.
- Around line 23-25: Update the use-anonymous-session test setup so mocked fetch
is restored after each test instead of relying on vi.clearAllMocks(), which only
resets call history. Replace the direct global.fetch assignments in the affected
tests with vi.stubGlobal("fetch", ...) and add an afterEach in
use-anonymous-session.test.ts to unstub the global, keeping the existing mock
responses and test behavior unchanged.

In `@src/client/hooks/use-anonymous-session.ts`:
- Around line 40-56: Update the fetcher inside useAnonymousSession so the non-OK
branch includes the response status in the thrown error instead of always using
the same message. Keep the existing useSWR and 204/200 handling unchanged, and
adjust the error creation in the fetch(...).then((res) => ...) path so callers
of useAnonymousSession can distinguish statuses like 401, 429, and 500 from the
thrown Error.
- Around line 44-45: Update the fetcher associated with the string SWR key in
the anonymous session hook to accept a single typed url string and pass it to
fetch, replacing the variadic args signature while preserving the existing
response handling.

In `@src/client/providers/auth0-provider.test.tsx`:
- Around line 38-47: Update the FR-8 test and all route-resolution tests around
Auth0Provider to render an SWR consumer for the configured anonymous-session
key, then assert that it displays the seeded mock session value without relying
on container truthiness. Cover both the default key and a custom
anonymousSessionRoute so the tests fail when fallback construction or key
resolution is removed.

In `@src/client/providers/auth0-provider.tsx`:
- Around line 57-73: Extract the anonymous-session route precedence logic into a
shared helper such as resolveAnonymousSessionKey, then replace the local anonKey
calculation in the Auth0 provider and the equivalent resolution in
useAnonymousSession with that helper. Ensure both consumers use identical keys,
and document or enforce that explicitly supplied routes must match when
configuring Auth0Provider and useAnonymousSession.

In `@src/errors/index.ts`:
- Around line 78-82: Update the exports in the errors barrel to expose only
AnonymousSessionError; remove getStatusForAnonymousError and
mapAnonymousErrorCode from the public re-export while leaving their direct
internal imports and implementations unchanged.

In `@src/server/anonymous-session.flow.test.ts`:
- Around line 123-191: Update the comprehensive lifecycle test so Step 2 reuses
the auth0_anon cookie created by createAnonymousSession in createRes.cookies
instead of constructing a separate readPayload and encrypted cookie. Send that
extracted cookie in the read request, preserving the existing read, update, and
logout assertions while linking the flow to the session created in Step 1.
- Around line 926-936: Replace the try/catch assertion around
createAnonymousSession with a single rejects.toMatchObject assertion that
verifies code is "invalid_client"; retain the rejection expectation so a
resolved promise fails the test.

In `@src/server/auth-client.anonymous-routes.test.ts`:
- Around line 1108-1110: Update the static cookies import in the anonymous-route
tests to include decrypt alongside encrypt, then replace both dynamic await
import calls around the renewed cookie assertions with the statically imported
decrypt function, preserving the existing generic payload and arguments.
- Around line 22-120: Extract the duplicated createMockJWT, MSW handler/server
setup, AuthClient initialization, and createSessionCookie scaffolding into a
shared helper under src/test/. Update both test suites to consume the shared
factory, while preserving each suite’s mock subject and fallback session-token
prefix through explicit overrides; leave suite-specific configuration in the
individual tests.
- Around line 582-599: Rename the test case around anonymousLogoutRequest to
state that the network method throws on an Auth0 5xx response, removing the
contradictory “no 5xx swallow” wording. Optionally add a separate test for
handleAnonymousLogout that verifies a 5xx response still returns 200 and clears
the cookie.

In `@src/server/auth-client.ts`:
- Around line 3132-3141: Replace the duplicated cookie retrieval and decryption
in the surrounding anonymous-session flow with the existing readAnonymousCookie
method, preserving its current error and null handling. Use the returned payload
variable for the session_token references instead of
decrypted.payload.session_token, and apply the same shared read path in
handleAnonymousLogout.
- Line 451: Remove the unused private anonymousSessionConfig field from the
class and stop assigning the constructor’s anonConfig to it; retain the existing
flattened fields anonymousSessionEnabled, anonymousCookieName, and
anonymousCookieOptions.

In `@src/types/anonymous-session.test.ts`:
- Around line 272-303: Import AnonymousSessionConfig in the test file and
annotate each config literal in the “Config validation” cases, including the
enabled, cookie name, sameSite, and secure override tests, so TypeScript
validates their fields against the configuration type.

In `@src/utils/anonymous-session-constants.ts`:
- Line 1: Move transferCookies out of anonymous-session-constants so
src/utils/anonymous-session-constants.ts remains plain constant exports and no
longer imports NextResponse from next/server.js. Put transferCookies in the
server-only cookies module alongside setChunkedCookie and deleteChunkedCookie,
and update any callers to import it from that server helper instead of the
constants module.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 09342321-2d59-4bc3-861c-cc60877bda65

📥 Commits

Reviewing files that changed from the base of the PR and between ce2ac00 and eeba1dd.

📒 Files selected for processing (21)
  • README.md
  • docs/anonymous-sessions.md
  • src/client/hooks/use-anonymous-session.test.ts
  • src/client/hooks/use-anonymous-session.ts
  • src/client/index.ts
  • src/client/providers/auth0-provider.test.tsx
  • src/client/providers/auth0-provider.tsx
  • src/errors/anonymous-session-errors.ts
  • src/errors/index.ts
  • src/server/anonymous-session.flow.test.ts
  • src/server/auth-client.anonymous-routes.test.ts
  • src/server/auth-client.test.ts
  • src/server/auth-client.ts
  • src/server/client.test.ts
  • src/server/client.ts
  • src/server/transaction-store.ts
  • src/test/defaults.ts
  • src/types/anonymous-session.test.ts
  • src/types/anonymous-session.ts
  • src/types/index.ts
  • src/utils/anonymous-session-constants.ts

Comment on lines +60 to +65
Three environment variables allow you to customize the feature routes:

```env
NEXT_PUBLIC_ANONYMOUS_SESSION_ROUTE=/auth/anonymous-session
NEXT_PUBLIC_ANONYMOUS_SESSION_UPDATE_ROUTE=/auth/anonymous-session/update
NEXT_PUBLIC_ANONYMOUS_SESSION_LOGOUT_ROUTE=/auth/anonymous-session/logout

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Use the configured update and logout routes.

The documentation defines NEXT_PUBLIC_ANONYMOUS_SESSION_UPDATE_ROUTE and NEXT_PUBLIC_ANONYMOUS_SESSION_LOGOUT_ROUTE, but the client examples use literal default paths. Custom route values will make these requests target the wrong endpoints. Read the public environment variables with the documented defaults, or state that callers must replace these paths.

Also applies to: 310-313, 387-389, 666-672

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/anonymous-sessions.md` around lines 60 - 65, Update the client examples
in the affected sections to use NEXT_PUBLIC_ANONYMOUS_SESSION_UPDATE_ROUTE and
NEXT_PUBLIC_ANONYMOUS_SESSION_LOGOUT_ROUTE, falling back to their documented
default paths when unset, instead of hardcoded routes. Apply this consistently
to every update and logout request example while preserving the existing route
behavior.

Comment on lines +212 to +216
**Return Value**: Returns `AnonymousSession`. Throws `AnonymousSessionError` if:

- The feature is disabled
- The client is not authorized for anonymous sessions
- The authorization server encounters an error

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Align the disabled-feature contract.

The supplied src/server/auth-client.ts contract states that disabled anonymous-session methods return null. This section says createAnonymousSession() throws when the feature is disabled. That also conflicts with the configuration table and the “Never throws” statement at Line 141. Document the actual behavior consistently.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/anonymous-sessions.md` around lines 212 - 216, Update the
createAnonymousSession() documentation in the Return Value section to state that
it returns null when anonymous sessions are disabled, while preserving the
documented AnonymousSessionError cases for authorization failures and
authorization-server errors. Align this wording with the auth-client.ts
contract, configuration table, and “Never throws” statement.

Comment on lines +626 to +669
it("SEC-1 T6.1: Transaction state binding records anonymousSessionLinked flag", async () => {
// Layer 3 of SEC-1: transaction state binding.
// Verify that when a session is injected, the flag is set in transaction state.
// This prevents swapped-cookie attacks at callback time.

const now = Math.floor(Date.now() / 1000);
const anonPayload: AnonymousCookiePayload = {
session_token: "session-bound",
access_token: createMockJWT("anon@uuid-9999"),
expires_at: now + 3600,
session_expires_at: now + 2592000
};
const encrypted = await createSessionCookie(anonPayload, secret);

const req = new NextRequest(new URL("http://localhost:3000/auth/login"), {
headers: { cookie: `auth0_anon=${encrypted}` }
});

const result = await (client as any).startInteractiveLogin(
{ returnTo: "/" },
req
);

// After startInteractiveLogin, the transaction state should have anonymousSessionLinked=true
// This is verified at callback time to prevent cookie-swap attacks
const location = result.headers.get("location");
expect(location).toContain("session_token=session-bound");
});

it("SEC-1 T6.2: No session at login → anonymousSessionLinked flag false", async () => {
// When no anon session exists, flag must be false so callback knows
// not to apply migration logic.

const req = new NextRequest(new URL("http://localhost:3000/auth/login"));

const result = await (client as any).startInteractiveLogin(
{ returnTo: "/" },
req
);

// No session_token in URL since no cookie
const location = result.headers.get("location");
expect(location).not.toContain("session_token=");
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Assert anonymousSessionLinked in the transaction state, not the location header.

Test T6.1 is named "Transaction state binding records anonymousSessionLinked flag", and T6.2 is named "No session at login → anonymousSessionLinked flag false". Neither test reads the transaction state. T6.1 asserts only that the location header contains session_token=session-bound, which duplicates the assertions in the tests at lines 536-558 and 597-624. The Layer 3 defense therefore has no coverage.

Decrypt the transaction cookie from the response and assert the flag. The tests in this file already decrypt cookies with decrypt from ./cookies.js, so the helper is available.

💚 Proposed assertion for T6.1
-      // After startInteractiveLogin, the transaction state should have anonymousSessionLinked=true
-      // This is verified at callback time to prevent cookie-swap attacks
-      const location = result.headers.get("location");
-      expect(location).toContain("session_token=session-bound");
+      // The transaction state must record the binding flag; this is checked at
+      // callback time to prevent cookie-swap attacks.
+      const txnCookie = result.cookies
+        .getAll()
+        .find((c: any) => c.name.startsWith("__txn_"));
+      expect(txnCookie).toBeDefined();
+      const txn = await decrypt<TransactionState>(txnCookie!.value, secret);
+      expect(txn?.payload.anonymousSessionLinked).toBe(true);

Import the extra symbols at the top of the file:

-import { encrypt } from "./cookies.js";
+import { decrypt, encrypt } from "./cookies.js";
+import type { TransactionState } from "./transaction-store.js";

Apply the mirrored assertion in T6.2, expecting the flag to be absent or false.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/anonymous-session.flow.test.ts` around lines 626 - 669, Update the
SEC-1 T6.1 and T6.2 tests to inspect the decrypted transaction cookie rather
than relying on the redirect location. Reuse the file’s existing decrypt helper
and transaction-cookie symbols to assert anonymousSessionLinked is true for the
injected-session case and absent or false when no session exists; retain only
assertions relevant to each test.

Comment on lines +1109 to +1112
const result = await (disabledClient as any).startInteractiveLogin({
req,
returnTo: "/"
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Three startInteractiveLogin calls pass req inside the options object instead of as the second argument. The signature is startInteractiveLogin(options, req?). When req is placed in the options object, the second parameter is undefined, the guard this.anonymousSessionEnabled && req is false, and the anonymous-cookie injection never runs. All three tests then pass without exercising the behavior their names describe. The calls at lines 550-553, 581-587, 616-619, and 644-647 use the correct positional form.

  • src/server/anonymous-session.flow.test.ts#L1109-L1112: move req to the second argument, then assert that the location header does not contain should-not-inject, so the test proves the disabled feature suppresses injection.
  • src/server/anonymous-session.flow.test.ts#L1127-L1133: move req to the second argument, then assert that the location header does not contain attacker-injected, so the test proves Layer 1 stripping.
  • src/server/anonymous-session.flow.test.ts#L1184-L1187: move req to the second argument, so the no-cookie path runs through the same guard the other tests use.
📍 Affects 1 file
  • src/server/anonymous-session.flow.test.ts#L1109-L1112 (this comment)
  • src/server/anonymous-session.flow.test.ts#L1127-L1133
  • src/server/anonymous-session.flow.test.ts#L1184-L1187
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/anonymous-session.flow.test.ts` around lines 1109 - 1112, Update
all three startInteractiveLogin calls in
src/server/anonymous-session.flow.test.ts:1109-1112, 1127-1133, and 1184-1187 to
pass req as the second positional argument rather than inside the options
object. In the first site, assert the location header excludes
should-not-inject; in the second, assert it excludes attacker-injected; leave
the third focused on exercising the no-cookie guard path.

Comment thread src/server/auth-client.ts
Comment on lines +640 to +645
this.anonymousCookieOptions = {
httpOnly: true,
secure: anonConfig.cookie?.secure ?? true,
sameSite: anonConfig.cookie?.sameSite ?? "lax",
path: "/"
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Confirm the anonymous cookie options never receive a maxAge.
set -euo pipefail

# Locate every read and write of the anonymous cookie options.
rg -nP -C 6 'anonymousCookieOptions' --type=ts

# Confirm setChunkedCookie forwards maxAge from its options argument.
ast-grep run --pattern $'export function setChunkedCookie($$$) { $$$ }' --lang typescript src/server/cookies.ts

Repository: auth0/nextjs-auth0

Length of output: 156


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Files containing anonymousCookieOptions or setChunkedCookie:\n'
rg -n 'anonymousCookieOptions|setChunkedCookie|anonymousSessionConfig|anonymousCookieName' --glob '*.{ts,tsx}' || true

printf '\nFile outline for auth-client.ts max matches:\n'
wc -l src/server/auth-client.ts
ast-grep outline src/server/auth-client.ts --view compact || true

printf '\nRelevant auth-client.ts sections:\n'
sed -n '600,655p' src/server/auth-client.ts
sed -n '2760,2800p' src/server/auth-client.ts
sed -n '3200,3290p' src/server/auth-client.ts

printf '\nCookies.ts outline and relevant section:\n'
wc -l src/server/cookies.ts
ast-grep outline src/server/cookies.ts --view compact || true
sed -n '1,260p' src/server/cookies.ts

Repository: auth0/nextjs-auth0

Length of output: 217


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import pathlib, re

for path in pathlib.Path('src/server').glob('*.ts'):
    text = path.read_text(errors='replace')
    if 'anonymousCookieOptions' in text or 'setChunkedCookie' in text:
        print(f'\n--- {path} ---')
        for i, line in enumerate(text.splitlines(), 1):
            if 'anonymousCookieOptions' in line or 'setChunkedCookie' in line:
                start=max(1,i-8); end=min(len(text.splitlines()), i+12)
                lines=text.splitlines()
                print(f'@@ {start}-{end}')
                for j in range(start,end+1):
                    print(f'{j:4}: {lines[j-1]}')
PY

printf '\nSearch for maxAge mentions in src/server:\n'
rg -n 'maxAge|max-age|max_age|cookie_options|CookieOptions' src/server --glob '*.{ts,tsx}' || true

Repository: auth0/nextjs-auth0

Length of output: 50374


Set maxAge on the anonymous cookie to match its payload lifetime.

anonymousCookieOptions has no maxAge, and setChunkedCookie forwards cookie options unchanged, so the encrypted cookie is browser-session scoped. The encrypted payload sets JWE.exp to session_expires_at, so the anonymous session is lost when the browser closes. Add maxAge: Math.max(0, expiration - this.epoch()) from persistAnonymousCookie before calling setChunkedCookie.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/auth-client.ts` around lines 640 - 645, Update
persistAnonymousCookie to set anonymousCookieOptions.maxAge immediately before
calling setChunkedCookie, using Math.max(0, expiration - this.epoch()) so the
cookie lifetime matches the encrypted payload’s session_expires_at. Preserve the
existing cookie options and chunking behavior.

Comment thread src/server/auth-client.ts
Comment on lines +903 to +923
// SEC-1 three-layer fixation mitigation: inject anonymous session token if present
// Layer 1 (reserved-param stripping) has already happened via mergeAuthorizationParamsIntoSearchParams()
// Layer 2 (own-cookie sourcing) and Layer 3 (transaction state binding) below
let anonymousSessionLinked = false;
if (this.anonymousSessionEnabled && req) {
try {
const anonCookie = await this.readAnonymousCookie(req.cookies);
if (anonCookie?.session_token) {
// Session token exists in own cookie → safe to inject (Layer 2)
authorizationParams.set(
RESERVED_SESSION_TOKEN_PARAM,
anonCookie.session_token
);
anonymousSessionLinked = true; // Flag for transaction state binding (Layer 3)
}
} catch (err) {
// Log error but don't fail login over anon cookie issue
console.error("Error reading anonymous session cookie:", err);
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find every caller of AuthClient.startInteractiveLogin and check whether a request is passed.
set -euo pipefail

rg -nP -C 5 '\bstartInteractiveLogin\s*\(' --type=ts -g '!**/*.test.ts'

Repository: auth0/nextjs-auth0

Length of output: 156


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== repo files around auth-client/client =="
fd -a 'auth-client\.ts$|client\.ts$' . | sed 's#^\./##' | head -50

echo "== git status =="
git status --short | head -50

echo "== grep startInteractiveLogin across tracked files =="
git ls-files | xargs rg -n 'startInteractiveLogin\s*\(' || true

echo "== grep handleLogin and startInteractiveLogin in server/client files =="
rg -n 'function startInteractiveLogin|startInteractiveLogin\(|handleLogin\s*\(' --glob '*.ts' --glob '*.tsx' . | head -100

Repository: auth0/nextjs-auth0

Length of output: 11990


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== src/server/auth-client.ts startInteractiveLogin/handleLogin section =="
sed -n '820,1040p' src/server/auth-client.ts | nl -ba -v820

echo "== src/server/client.ts public client startInteractiveLogin =="
sed -n '1560,1605p' src/server/client.ts | nl -ba -v1560

echo "== src/server/auth-client.ts anonymousSessionLinked references =="
rg -n 'anonymousSessionLinked|RESERVED_SESSION_TOKEN_PARAM' src/server/auth-client.ts -C 3

echo "== src/server/client.ts exports =="
sed -n '1,120p' src/server/client.ts | nl -ba -v1

Repository: auth0/nextjs-auth0

Length of output: 270


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== src/server/auth-client.ts startInteractiveLogin/handleLogin section =="
sed -n '820,1040p' src/server/auth-client.ts

echo "== src/server/client.ts public client startInteractiveLogin =="
sed -n '1560,1605p' src/server/client.ts

echo "== src/server/auth-client.ts anonymousSessionLinked references =="
rg -n 'anonymousSessionLinked|RESERVED_SESSION_TOKEN_PARAM' src/server/auth-client.ts -C 3

echo "== src/server/client.ts relevant exports =="
sed -n '1,120p' src/server/client.ts

Repository: auth0/nextjs-auth0

Length of output: 17432


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== AnonymousSessionConfig / startsInteractiveLogin docs =="
rg -n 'anonymousSession|startInteractiveLogin|onCallback' --glob '*.md' --glob '*.ts' --glob '*.tsx' | head -300

echo "== anonymous flow tests around startInteractiveLogin =="
sed -n '520,685p' src/server/anonymous-session.flow.test.ts

Repository: auth0/nextjs-auth0

Length of output: 280


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== AnonymousSessionConfig / startInteractiveLogin docs =="
rg -n 'anonymousSession|startInteractiveLogin|onCallback' --glob '*.md' --glob '*.ts' --glob '*.tsx' . | head -300 || true

echo "== anonymous flow tests around startInteractiveLogin =="
python3 - <<'PY'
from pathlib import Path
p=Path('src/server/anonymous-session.flow.test.ts')
text=p.read_text()
for start in range(0, len(text), 100_000):
    idx=text.find('client.startInteractiveLogin', start)
    while idx != -1:
        line=text[:idx].count('\n')+1
        if line in (550,581,616,644):
            print(f"-- context line {line} --")
            lines=text.splitlines()
            for i in range(max(1,line-25), min(len(lines),line+45)+1):
                print(f"{i:5}: {lines[i-1]}")
        idx=text.find('client.startInteractiveLogin', idx+1)
PY

Repository: auth0/nextjs-auth0

Length of output: 21141


Include the request when reading anonymous-session cookies.

handleLogin() passes the NextRequest, so /auth/login links anonymous sessions. Auth0Client.startInteractiveLogin() calls authClient.startInteractiveLogin(options) from a Server Component/Action path with no req, so anonymous-session linking stays false and the migration hook will not fire. Use the available next/headers request-less cookie access here, or document that programmatic startInteractiveLogin() does not link anonymous sessions.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/auth-client.ts` around lines 903 - 923, Update the
anonymous-session cookie lookup in the handleLogin flow around
anonymousSessionLinked so it can read cookies when req is unavailable, using the
request-less next/headers access supported by the server-component/action path.
Preserve request-based cookie reading when req exists, and ensure the session
token is injected and anonymousSessionLinked is set for programmatic
startInteractiveLogin calls as well.

Comment thread src/server/auth-client.ts
Comment on lines +2643 to +2663
if (state.expires_at > now) {
// Access token still valid → return session, no renewal needed (T1.3)
return this.toPublicSession(state);
}

// Access token is expired; check if we can renew
if (resCookies && state.session_expires_at > now) {
// Session token valid, can write cookie → renew access token (T1.4)
return await this.renewAccessToken(state, reqCookies, resCookies);
}

// Session token is also expired; can we write cookies?
if (resCookies) {
// Session expired + can write → create fresh session silently (T1.5, T3.6, FR-12)
return await this.createAndPersist(reqCookies, resCookies);
}

// Can't write cookie (Server Component read-only context) → defer renewal (D7, T1.6)
// Return the decrypted session as-is; renewal will happen on next route handler call
return this.toPublicSession(state);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

One throwing call breaks the documented never-throws read contract. toPublicSession throws AnonymousSessionError("invalid_session_token") when the access token inside a successfully decrypted cookie cannot be decoded or its sub lacks the anon@ prefix. resolveAnonymousSession calls it on both read-path branches, so the throw propagates all the way to the public reader, which documents that it never throws for a malformed cookie and is intended for Server Components.

  • src/server/auth-client.ts#L2643-L2663: wrap the two toPublicSession(state) calls on the read path so an undecodable payload returns null, matching how decrypt already handles a malformed cookie. Leave the writable paths throwing.
  • src/server/client.ts#L880-L919: no code change is needed once the root cause is fixed; re-verify that the doc claim at lines 882-883 then holds.
📍 Affects 2 files
  • src/server/auth-client.ts#L2643-L2663 (this comment)
  • src/server/client.ts#L880-L919
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/auth-client.ts` around lines 2643 - 2663, The read-only branches
in resolveAnonymousSession must not propagate malformed access-token errors:
wrap both toPublicSession(state) calls in src/server/auth-client.ts lines
2643-2663 so undecodable payloads return null, while leaving the writable
renewal and creation paths unchanged. Make no code change in
src/server/client.ts lines 880-919; re-verify its never-throws documentation
remains accurate.

Comment thread src/server/auth-client.ts
Comment on lines +2681 to +2687
const renewedPayload: AnonymousCookiePayload = {
session_token: state.session_token, // Keep original session token
access_token: res.access_token, // New access token
expires_at: this.epoch() + res.expires_in,
session_expires_at: this.epoch() + res.session_expires_in,
metadata: state.metadata // Preserve metadata
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reuse toCookiePayload instead of rebuilding the payload inline.

This block duplicates toCookiePayload (lines 2874-2893) with two behavioral differences:

  • Line 2682 always keeps state.session_token. toCookiePayload uses res.session_token ?? priorSessionToken. If Auth0 ever rotates the session token on a renew response, this path discards the new handle and keeps the stale one. Every later renewal then fails.
  • Line 2686 always keeps state.metadata. toCookiePayload prefers res.metadata, the server-merged value. Metadata updated server-side is dropped on renew.

The update handler at line 3158 already routes through toCookiePayload. Route the renew path through it too, so one function owns the response-to-payload contract.

♻️ Proposed refactor
-      const renewedPayload: AnonymousCookiePayload = {
-        session_token: state.session_token, // Keep original session token
-        access_token: res.access_token, // New access token
-        expires_at: this.epoch() + res.expires_in,
-        session_expires_at: this.epoch() + res.session_expires_in,
-        metadata: state.metadata // Preserve metadata
-      };
+      // toCookiePayload owns the response-to-payload contract: it carries the
+      // prior session handle forward when the server omits it, and prefers
+      // server-merged metadata over the locally cached copy.
+      const renewedPayload = this.toCookiePayload(
+        res,
+        state.session_token,
+        state.metadata
+      );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const renewedPayload: AnonymousCookiePayload = {
session_token: state.session_token, // Keep original session token
access_token: res.access_token, // New access token
expires_at: this.epoch() + res.expires_in,
session_expires_at: this.epoch() + res.session_expires_in,
metadata: state.metadata // Preserve metadata
};
// toCookiePayload owns the response-to-payload contract: it carries the
// prior session handle forward when the server omits it, and prefers
// server-merged metadata over the locally cached copy.
const renewedPayload = this.toCookiePayload(
res,
state.session_token,
state.metadata
);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/auth-client.ts` around lines 2681 - 2687, Replace the inline
renewedPayload construction in the renew flow with the existing toCookiePayload
helper, passing the renewal response and prior state as required by its
contract. Preserve the renew behavior while allowing rotated session_token and
server-merged metadata from the response, matching the existing update handler
usage.

Comment thread src/server/auth-client.ts
Comment on lines +2938 to +2956
if (clientAuth && typeof clientAuth === "object") {
for (const [key, value] of Object.entries(
clientAuth as Record<string, unknown>
)) {
if (typeof value === "string") {
headersObj[key] = value;
}
}
}

const res = await this.fetch(requestUrl, {
method: "POST",
headers: headersObj,
body: JSON.stringify({
client_id: this.clientMetadata.client_id,
...body
}),
signal: httpOpts.signal
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | 🏗️ Heavy lift

🧩 Analysis chain

🌐 Web query:

oauth4webapi 3.x ClientAuth is a function ClientSecretPost returns callable

💡 Result:

In the oauth4webapi library, ClientSecretPost is a function that returns a ClientAuth object [1][2]. ClientAuth is a type alias for a function (a callable) that performs client authentication during an OAuth request [2]. Its signature is: (as: AuthorizationServer, client: Client, body: URLSearchParams, headers: Headers) => void | Promise [2] When you call ClientSecretPost(clientSecret), it returns this callable function, which, when invoked by the library during an OAuth flow (such as authorizationCodeGrantRequest), injects the client_id and client_secret into the HTTP request body as application/x-www-form-urlencoded parameters [1][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== package files =="
fd -a 'package.json|pnpm-lock.yaml|package-lock.json|yarn.lock|bun.lockb|bun.lock' . | sed 's#^\./##' | head -50

echo
echo "== relevant files =="
fd -a 'auth-client.ts' . | sed 's#^\./##'

echo
echo "== dependency versions =="
for f in package.json pnpm-lock.yaml package-lock.json yarn.lock bun.lock; do
  if [ -f "$f" ]; then
    echo "--- $f ---"
    if [ "$f" = "package.json" ]; then
      node -e "const p=require('./$f'); console.log(JSON.stringify({dependencies:p.dependencies, devDependencies:p.devDependencies, peerDependencies:p.peerDependencies}, null, 2))" | rg -n "oauth4webapi|nextjs-auth0|typescript|node" || true
    else
      rg -n "oauth4webapi|`@auth0/nextjs-auth0`" "$f" | head -100 || true
    fi
  fi
done

echo
echo "== auth-client outline around anon =="
file="$(fd 'auth-client.ts' . | head -1)"
if [ -n "${file:-}" ]; then
  ast-grep outline "$file" --match anonymous | head -200 || true
  echo "--- lines 2850-3040 ---"
  sed -n '2850,3040p' "$file" | nl -ba -v2850
fi

Repository: auth0/nextjs-auth0

Length of output: 1630


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== lines 2850-3045 of src/server/auth-client.ts =="
sed -n '2850,3045p' src/server/auth-client.ts | awk '{printf "%6d\t%s\n", NR+2849, $0}'

echo
echo "== getCientAuth definition/usages =="
rg -n "getClientAuth|anonymous/token|anonymous/logout|clientAuth|ClientAuth|ClientSecretPost|assertion" src/server/auth-client.ts

echo
echo "== inspect type import source if local copy exists =="
fd -a 'node_modules/oauth4webapi|oauth4webapi' . | sed 's#^\./##' | head -20 || true

Repository: auth0/nextjs-auth0

Length of output: 9663


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== lines 3480-3535: getClientAuth implementation =="
sed -n '3480,3535p' src/server/auth-client.ts | awk '{printf "%6d\t%s\n", NR+3479, $0}'

echo
echo "== nearby getServerClient usage with getClientAuth =="
sed -n '1280,1355p' src/server/auth-client.ts | awk '{printf "%6d\t%s\n", NR+1279, $0}'

echo
echo "== auth-client imports =="
sed -n '1,120p' src/server/auth-client.ts | awk '{printf "%6d\t%s\n", NR, $0}'

echo
echo "== package lock for oauth4webapi type/export metadata, if present =="
sed -n '1980,2010p' pnpm-lock.yaml

Repository: auth0/nextjs-auth0

Length of output: 11255


🌐 Web query:

oauth4webapi 3.8.6 source ClientAuth callable ClientSecretPost PrivateKeyJwt TLSClientAuth

💡 Result:

In the oauth4webapi library (version 3.8.6), ClientAuth is a type alias for a callable function used to implement client authentication methods when interacting with an Authorization Server [1][2][3]. The ClientAuth function signature is defined as [2][3]: (as: AuthorizationServer, client: Client, body: URLSearchParams, headers: Headers) => void | Promise This callable is invoked by authentication request functions (such as authorizationCodeGrantRequest or clientCredentialsGrantRequest) to dynamically modify the outgoing request's body or headers to include the necessary authentication credentials [1][2][3]. The library provides several built-in functions that return a ClientAuth instance [4]: ClientSecretPost(clientSecret: string): ClientAuth This method sends the client_id and client_secret as application/x-www-form-urlencoded body parameters [5][2]. PrivateKeyJwt(clientPrivateKey: CryptoKey | PrivateKey, options?: ModifyAssertionOptions): ClientAuth This method sends the client_id, client_assertion_type, and a signed client_assertion (JWT) as application/x-www-form-urlencoded body parameters, utilizing digital signatures for authentication [6][2]. TlsClientAuth: ClientAuth This method sends only the client_id as a body parameter and relies on mutual TLS (mTLS) for authentication, which must be configured externally through a custom fetch implementation (typically using an agent with a key and certificate) [7][8][3]. These implementations provide a flexible mechanism to inject authentication material into OAuth 2.0 requests while maintaining adherence to various RFC-specified authentication methods [2][3].

Citations:


Fix the anonymous Auth0 client authentication path. getClientAuth() returns oauth4webapi’s callable ClientAuth, but typeof clientAuth === "object" skips callable credentials, so /anonymous/token and /anonymous/logout do not include client_id/client_secret, client assertion, or mTLS auth. Attach credentials explicitly on these JSON-POST endpoints, then surface 401 invalid_client for logout instead of treating it as success.

📍 Affects 1 file
  • src/server/auth-client.ts#L2938-L2956 (this comment)
  • src/server/auth-client.ts#L2995-L3023
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/auth-client.ts` around lines 2938 - 2956, Update the anonymous
JSON-POST authentication flows around the request logic at both auth-client.ts
sites 2938-2956 and 2995-3023: apply the callable ClientAuth returned by
getClientAuth() explicitly so client credentials, assertions, and mTLS
authentication are attached instead of filtering only object entries. Ensure the
logout flow propagates a 401 invalid_client response rather than treating it as
successful.

Comment thread src/server/auth-client.ts
Comment on lines +3120 to +3130
const { metadata } = body;

// Step 1: Validate metadata size
// (Before network call, per FR-15; client-side optimization)
// FR-15: cap is on UTF-8 byte length, not UTF-16 code-unit count, so
// multibyte metadata cannot bypass the limit. TextEncoder is Edge-safe.
const metadataStr = JSON.stringify(metadata ?? {});
const metadataBytes = new TextEncoder().encode(metadataStr).length;
if (metadataBytes > METADATA_SIZE_LIMIT_BYTES) {
return this.anonymousErrorResponse("metadata_too_large", 400);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Validate that metadata is a plain object.

The handler checks only the serialized byte size. A client can POST {"metadata": "text"}, {"metadata": [1,2]}, or {"metadata": 42}. Each passes the 1KB check and is forwarded to Auth0 unchanged.

The value then flows through toCookiePayload into AnonymousCookiePayload.metadata and out through toPublicSession into AnonymousSession.metadata. Both are declared Record<string, unknown>. A string, array, or number breaks that contract for every consumer, including the client hook, with no type error to catch it.

Reject non-object values before the size check.

🐛 Proposed validation
       const { metadata } = body;
 
+      // metadata must be a plain object: AnonymousCookiePayload.metadata and
+      // AnonymousSession.metadata are both declared Record<string, unknown>.
+      if (
+        metadata !== undefined &&
+        (typeof metadata !== "object" ||
+          metadata === null ||
+          Array.isArray(metadata))
+      ) {
+        return this.anonymousErrorResponse("invalid_request", 400);
+      }
+
       // Step 1: Validate metadata size
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const { metadata } = body;
// Step 1: Validate metadata size
// (Before network call, per FR-15; client-side optimization)
// FR-15: cap is on UTF-8 byte length, not UTF-16 code-unit count, so
// multibyte metadata cannot bypass the limit. TextEncoder is Edge-safe.
const metadataStr = JSON.stringify(metadata ?? {});
const metadataBytes = new TextEncoder().encode(metadataStr).length;
if (metadataBytes > METADATA_SIZE_LIMIT_BYTES) {
return this.anonymousErrorResponse("metadata_too_large", 400);
}
const { metadata } = body;
// metadata must be a plain object: AnonymousCookiePayload.metadata and
// AnonymousSession.metadata are both declared Record<string, unknown>.
if (
metadata !== undefined &&
(typeof metadata !== "object" ||
metadata === null ||
Array.isArray(metadata))
) {
return this.anonymousErrorResponse("invalid_request", 400);
}
// Step 1: Validate metadata size
// (Before network call, per FR-15; client-side optimization)
// FR-15: cap is on UTF-8 byte length, not UTF-16 code-unit count, so
// multibyte metadata cannot bypass the limit. TextEncoder is Edge-safe.
const metadataStr = JSON.stringify(metadata ?? {});
const metadataBytes = new TextEncoder().encode(metadataStr).length;
if (metadataBytes > METADATA_SIZE_LIMIT_BYTES) {
return this.anonymousErrorResponse("metadata_too_large", 400);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/auth-client.ts` around lines 3120 - 3130, Update the metadata
validation in the handler before the JSON serialization and byte-size check to
accept only non-null plain objects, rejecting strings, arrays, numbers, and
other non-object values with the existing anonymous error response. Preserve the
current size-limit validation for valid metadata objects and ensure the value
forwarded to toCookiePayload remains compatible with
AnonymousCookiePayload.metadata.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants