Skip to content

feat: Auth always enable in all environments - #102

Merged
johlju merged 22 commits into
viscalyx:mainfrom
johlju:f/auth-build-time-module-swap
Apr 26, 2026
Merged

feat: Auth always enable in all environments#102
johlju merged 22 commits into
viscalyx:mainfrom
johlju:f/auth-build-time-module-swap

Conversation

@johlju

@johlju johlju commented Apr 25, 2026

Copy link
Copy Markdown
Member

Description

Screenshots (if applicable)

Related Issues

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Refactoring (no functional changes)
  • Performance improvement (improves performance without changing functionality)
  • Dependency update (updating libraries or tools)

Testing

  • npm run check passes locally
  • All existing tests still pass
  • Manual testing completed
  • UI tested on desktop and mobile (if applicable)

Checklist

  • Documentation updated as needed

Checklist

  • Code follows the project style guidelines (Biome)
  • Tests added/updated as needed
  • Self-review of code completed
  • Comments added for complex logic
  • No hardcoded strings (use translations if i18n is added)

This change is Reviewable

johlju and others added 6 commits April 25, 2026 17:07
… OIDC client settings

Co-authored-by: Copilot <copilot@github.com>
…gets, enhancing auth handling and security settings
…emove legacy auth handling

Co-authored-by: Copilot <copilot@github.com>
@coderabbitai

coderabbitai Bot commented Apr 25, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a7971b1f-2216-4664-8044-c5433e4aefc3

📥 Commits

Reviewing files that changed from the base of the PR and between 605c9a1 and 36b1441.

📒 Files selected for processing (3)
  • .github/workflows/integration-tests.yml
  • docs/auth-how-it-works.md
  • next.config.ts
✅ Files skipped from review due to trivial changes (1)
  • docs/auth-how-it-works.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • next.config.ts

Walkthrough

Authentication becomes always-on and build-target driven: runtime AUTH_ENABLED/NEXT_PUBLIC_AUTH_ENABLED paths removed, insecure-issuer allowance moved to build-time BUILD_TARGET constants, OIDC config is required at startup, and auth flows/cookie/security behavior use build-target flags.

Changes

Cohort / File(s) Summary
Build-target constants system
lib/runtime/build-target.ts, lib/runtime/build-target.local-prod.ts, lib/runtime/build-target.prod.ts
Add three build-target modules exporting BUILD_TARGET, ALLOW_INSECURE_OIDC_ISSUER, USE_INSECURE_COOKIE, USE_DEV_CSP with target-specific values for dev/local-prod/prod.
Build configuration & test aliases
next.config.ts, vitest.config.ts
Introduce BUILD_TARGET resolution, enforce valid production build target, alias @/lib/runtime/build-target to target impl via webpack; Vitest aliases to dev impl for tests.
Scripts & env files
package.json, .env.development, .env.example, .env.prodlike
Add build:local-prod; set BUILD_TARGET in build/start scripts; remove AUTH_ENABLED/NEXT_PUBLIC_AUTH_ENABLED and AUTH_OIDC_ALLOW_INSECURE_ISSUER from env examples; update .env.prodlike with local Keycloak client/audience and cookie password.
Auth core refactor
lib/auth/config.ts, lib/auth/oidc.ts, lib/auth/mcp-token.ts, lib/auth/session.ts, lib/auth/login-state.ts, lib/auth/csrf.ts
Remove enabled/auth-disabled branches; require OIDC env vars or throw AuthConfigError; use build-time ALLOW_INSECURE_OIDC_ISSUER and USE_INSECURE_COOKIE; change verifyMcpBearerToken to always return VerifiedMcpToken.
Auth API routes
app/api/auth/login/route.ts, app/api/auth/callback/route.ts, app/api/auth/logout/route.ts, app/api/auth/me/route.ts
Eliminate auth-disabled short-circuits; routes always execute normal auth/session/OIDC flows and return canonical responses.
Frontend & middleware
components/AuthMenu.tsx, proxy.ts
Remove client-side NEXT_PUBLIC_AUTH_ENABLED gating and authDisabled state; always call /api/auth/me; proxy always strips impersonation headers and enforces auth; CSP selection uses USE_DEV_CSP.
Actor resolution & MCP
lib/requirements/auth.ts, lib/mcp/http.ts
Drop 'headers' actor source and header-based actor resolution; request actors derived from session or verified JWT; attachVerifiedActor always invoked after token verification.
Keycloak realm & docs
dev/keycloak/realm-kravhantering-dev.json, docs/*, cspell.jsonc
Add kravhantering-local client and protocol mappers; update multiple docs to describe always-on auth and build-target contract; add spell-check terms.
Integration & Playwright
tests/integration/global-setup.ts, tests/integration/auth-login.spec.ts, playwright*.config.ts, .github/workflows/integration-tests.yml
Global setup seeds authenticated storageState via Keycloak logins; new login E2E spec; Playwright configs use localhost, inject Origin/X-Requested-With, persist storageState; CI workflow brings up/down local IdP and waits for discovery.
Tests & test tooling
tests/unit/*, tests/quality/*, vitest.setup.ts, tests/unit/build-target.test.ts, tests/unit/playwright-global-setup.test.ts
Remove auth-disabled test branches; seed OIDC env in vitest.setup; update test helpers to attach verified actors; add tests for build-target constants and global-setup helpers.
CI quality checks
.github/workflows/quality-checks.yml
Add PR comment checklist entry for "Prod build-target contract" (npm run verify:prod-bundle).

Sequence Diagram(s)

sequenceDiagram
  participant Browser as Client (Browser)
  participant Proxy as proxy.ts
  participant App as Next.js server (/api/auth/login, /api/auth/callback, /api/auth/me)
  participant IdP as Keycloak (OIDC)

  Browser->>Proxy: GET /api/auth/login
  Proxy->>App: forward /api/auth/login (headers stripped)
  App->>IdP: Redirect to Keycloak (auth request, PKCE/state)
  Browser->>IdP: Follow redirect, authenticate (user submits credentials)
  IdP->>Browser: Redirect to /api/auth/callback with code
  Browser->>Proxy: GET /api/auth/callback (headers stripped)
  Proxy->>App: forward /api/auth/callback
  App->>IdP: Exchange code for tokens, validate issuer/audience
  App->>App: Create iron-session cookie, attach verified actor
  App->>Browser: 302 redirect to app with session cookie set
  Browser->>Proxy: GET /api/auth/me
  Proxy->>App: forward /api/auth/me
  App->>Browser: return authenticated user JSON
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.95% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The PR description consists almost entirely of unchecked template boilerplate with minimal content; it includes only a reference to issue #100 but lacks any substantive explanation of changes, testing notes, or implementation details. Provide a concise summary of the implementation approach (e.g., build-time module-swapping strategy) and document which testing steps were completed or remain outstanding.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: Auth always enable in all environments' accurately describes the main objective—making authentication always-on across all build targets—which is the primary focus of the changeset.
Linked Issues check ✅ Passed The PR fully implements issue #100 objectives: introduces build-target module-swapping via next.config.ts [#100], refactors all auth-flag reads to build-time constants [#100], removes runtime auth-toggle capabilities [#100], updates tests and CI verification [#100], and documents the build-contract model [#100].
Out of Scope Changes check ✅ Passed Changes align with issue #100 scope: build-target constants, module-swapping infrastructure, codebase refactoring, test updates, documentation, and CI setup for verification. No unrelated feature additions, refactorings, or unplanned improvements are present.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


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 and usage tips.

@codecov

codecov Bot commented Apr 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 90.00000% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 57.31%. Comparing base (30449fa) to head (36b1441).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
lib/auth/oidc.ts 0.00% 2 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #102      +/-   ##
==========================================
- Coverage   57.35%   57.31%   -0.04%     
==========================================
  Files         257      260       +3     
  Lines       17013    16967      -46     
  Branches     6441     6519      +78     
==========================================
- Hits         9757     9725      -32     
+ Misses       7143     7129      -14     
  Partials      113      113              
Files with missing lines Coverage Δ
app/api/auth/callback/route.ts 96.21% <ø> (+0.68%) ⬆️
app/api/auth/login/route.ts 0.00% <ø> (ø)
app/api/auth/logout/route.ts 100.00% <100.00%> (+2.43%) ⬆️
app/api/auth/me/route.ts 100.00% <ø> (ø)
components/AuthMenu.tsx 77.77% <100.00%> (+0.70%) ⬆️
lib/auth/config.ts 87.71% <ø> (-6.26%) ⬇️
lib/auth/csrf.ts 95.55% <100.00%> (ø)
lib/auth/login-state.ts 0.00% <ø> (ø)
lib/auth/mcp-token.ts 100.00% <100.00%> (ø)
lib/auth/session.ts 84.12% <ø> (ø)
... and 6 more

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@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: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lib/requirements/auth.ts (1)

31-37: ⚠️ Potential issue | 🟡 Minor

Stale doc comment references the removed header-trust path.

The JSDoc on hsaId still says it's null for "header-trust" actors, but the header-derived path was removed in this PR (the 'headers' value was dropped from ActorSource on Line 25). Please drop that mention so the doc matches the new identity model.

📝 Suggested doc tweak
   /**
    * HSA-id when the actor was authenticated and a verified `employeeHsaId`
-   * claim was present. `null` for anonymous, header-trust, or MCP actors
-   * whose token did not carry the claim. MCP service-account tokens carry
-   * the synthetic `mcp-client:<client_id>` value here.
+   * claim was present. `null` for anonymous actors and for MCP actors whose
+   * token did not carry the claim. MCP service-account tokens carry the
+   * synthetic `mcp-client:<client_id>` value here.
    */
   hsaId: string | null
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/requirements/auth.ts` around lines 31 - 37, The JSDoc for hsaId
incorrectly mentions "header-trust" actors which no longer exist after removing
the 'headers' variant from ActorSource; update the comment on the hsaId field in
lib/requirements/auth.ts to remove the "header-trust" reference so it matches
the new identity model (keep notes about null for anonymous or MCP actors and
the synthetic `mcp-client:<client_id>` value for MCP service-account tokens).
🧹 Nitpick comments (15)
dev/keycloak/realm-kravhantering-dev.json (2)

129-179: Update auth developer workflow docs for the new kravhantering-local client.

A new realm client (kravhantering-local, port 3001, secret local-kc-app-secret) was added for the prodlike target, but docs/auth-developer-workflow.md (and/or docs/auth-how-it-works.md) doesn't appear to mention it. Developers running the local-prod / prodlike flow against this realm need to know which client/secret to point at.

Based on learnings: "Update docs/auth-developer-workflow.md whenever a change affects local Keycloak setup".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dev/keycloak/realm-kravhantering-dev.json` around lines 129 - 179, The docs
lack mention of the new Keycloak client "kravhantering-local" (clientId:
kravhantering-local, secret: local-kc-app-secret, port: 3001) added for the
prodlike/local-prod flow; update docs/auth-developer-workflow.md (and optionally
docs/auth-how-it-works.md) to document this client: where to configure redirect
URIs (http://localhost:3001/api/auth/callback and
http://127.0.0.1:3001/api/auth/callback), webOrigins, the client secret, and
that this client is used for the prodlike target when running locally so
developers know which clientId/secret and port to point their local app at.

129-179: Consider hardening the prod-bundle verifier against kravhantering-local / local-kc-app-secret leaks.

scripts/verify-prod-bundle.mjs already lists kravhantering-app and dev-only as forbidden tokens to catch dev artifacts leaking into the prod bundle. The new local-prod-only client id (kravhantering-local) and its secret (local-kc-app-secret) won't match either pattern. If any code path ever statically embeds these (e.g., a fallback constant), the verifier won't catch it. Adding them to FORBIDDEN keeps the contract tight.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dev/keycloak/realm-kravhantering-dev.json` around lines 129 - 179, The
prod-bundle verifier doesn't currently block the new local client id/secret from
the Keycloak JSON; update the FORBIDDEN list in scripts/verify-prod-bundle.mjs
to include "kravhantering-local" and "local-kc-app-secret" so the verifier will
catch any accidental static embedding of those dev-only values; ensure the
additions are added to the same FORBIDDEN constant/array that already contains
"kravhantering-app" and "dev-only" and run the verifier tests.
lib/runtime/build-target.prod.ts (1)

16-26: Optional: use literal types for stronger compile-time DCE narrowing.

Annotating these as : boolean widens away the literal false, so TypeScript itself cannot narrow if (USE_DEV_CSP) { ... } to never in consumers. Webpack constant-folding still eliminates the dead branches at bundle time (which is what verify-prod-bundle checks), but stronger TS narrowing would catch dead-code-elimination regressions during type-checking, not just at build-verify time.

📝 Suggested narrowing
-export const BUILD_TARGET: BuildTarget = 'prod'
+export const BUILD_TARGET = 'prod' as const satisfies BuildTarget
@@
-export const ALLOW_INSECURE_OIDC_ISSUER: boolean = false
+export const ALLOW_INSECURE_OIDC_ISSUER = false as const
@@
-export const USE_INSECURE_COOKIE: boolean = false
+export const USE_INSECURE_COOKIE = false as const
@@
-export const USE_DEV_CSP: boolean = false
+export const USE_DEV_CSP = false as const

Note: this would need to be applied symmetrically to the other build-target files so the alias-swap module shape stays compatible across targets.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/runtime/build-target.prod.ts` around lines 16 - 26, The constants
ALLOW_INSECURE_OIDC_ISSUER, USE_INSECURE_COOKIE, and USE_DEV_CSP are annotated
as boolean which widens them away from the literal false; change their types to
the literal false (e.g., export const USE_DEV_CSP: false = false) or use const
assertions so TypeScript narrows branches at compile time (leave BUILD_TARGET as
'prod'); apply the same literal-typing pattern symmetrically across the other
build-target files so the module shape remains compatible.
.github/workflows/quality-checks.yml (1)

106-107: The npm run dotenv:check script exists and does cover both paths, but has different exclusions than the workflow actions.

The npm script (dotenv:check in package.json) correctly runs dotenv-linter on both root (.) and .devcontainer --recursive. However, it applies specific exclusions (.env.sqlserver, .env.local, .env.*.local at root; .devcontainer/.env, .devcontainer/elevated/.env in devcontainer) that are not explicitly configured in the workflow actions, meaning running locally may not reproduce the exact same linting behavior as the workflow.

Not introduced by this PR—flagging only because the new entry was added next to it. Consider aligning exclusion patterns between the npm script and the workflow actions to ensure consistency between local development checks and CI/CD validation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/quality-checks.yml around lines 106 - 107, The workflow's
dotenv-linter action (entry named 'dotenv-linter' running "npm run
dotenv:check") and the package.json script 'dotenv:check' use different
exclusion patterns so CI and local runs can diverge; update either the workflow
action or the 'dotenv:check' npm script so they use the exact same dotenv-linter
exclude globs (e.g., root exclusions like .env.sqlserver, .env.local,
.env.*.local and devcontainer exclusions like .devcontainer/.env and
.devcontainer/elevated/.env) — locate the 'dotenv:check' script in package.json
and the 'dotenv-linter' entry in the workflow (name: 'dotenv-linter') and make
their exclusion flags identical to ensure parity between local and CI linting.
tests/unit/mcp-token.test.ts (1)

83-197: Inconsistent cleanup: stray enabled: true left in other getAuthConfig mocks.

The first test (Line 31-42) was correctly trimmed to drop enabled from the mocked AuthConfig, matching the new contract where enabled no longer exists. However, the remaining tests in this file still set enabled: true on the mock return values (Lines 85, 108, 134, 164, 193). These references are now meaningless and easy to misread as if the auth-enabled flag still has effect. Consider removing them for consistency.

♻️ Proposed cleanup
   it('accepts a real HSA-id in employeeHsaId', async () => {
     getAuthConfigMock.mockReturnValue({
-      enabled: true,
       issuerUrl: 'https://issuer.example.com',
       apiAudience: 'kravhantering-app',
     })

(Apply the same removal at Lines 108, 134, 164, and 193.)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/mcp-token.test.ts` around lines 83 - 197, The tests still include
the obsolete enabled property in getAuthConfigMock.mockReturnValue calls,
causing inconsistent mocks; remove enabled: true from every
getAuthConfigMock.mockReturnValue usage in this test file (used in the blocks
that set issuerUrl/apiAudience in the verifyMcpBearerToken tests and the
security audit events beforeEach) so the mocked AuthConfig matches the new
contract—look for getAuthConfigMock.mockReturnValue({...}) in the tests around
the verifyMcpBearerToken specs and the beforeEach and delete the enabled: true
entries.
vitest.config.ts (1)

71-90: Move the @/lib/runtime/build-target alias before the @ alias to ensure more specific rules are matched first.

Vite evaluates aliases in declaration order (first match wins). The @ alias at line 74 matches any import starting with @/ (including @/lib/runtime/build-target), so the more specific alias below it is never consulted. Today both resolve to the same file, but if the explicit alias is later pointed to a different file (e.g., a test stub or dev variant), the swap will silently fail, defeating the stated intent.

♻️ Proposed reorder
   resolve: {
     alias: {
-      // Module path mapping (equivalent to Jest's moduleNameMapper)
-      '@': path.resolve(__dirname, '.'),
       // Always resolve the build-target to the dev implementation in tests.
       // Webpack aliases do not apply to vitest; this explicit alias ensures
       // tests never accidentally use the local-prod or prod frozen constants.
       '@/lib/runtime/build-target': path.resolve(
         __dirname,
         'lib/runtime/build-target.ts',
       ),
+      // Module path mapping (equivalent to Jest's moduleNameMapper)
+      '@': path.resolve(__dirname, '.'),
       '@viscalyx/developer-mode-core': path.resolve(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vitest.config.ts` around lines 71 - 90, The alias ordering causes the general
'@' alias to shadow the more specific '@/lib/runtime/build-target' rule; move
the '@/lib/runtime/build-target' entry so it appears before the '@' alias in the
resolve.alias object in vitest.config.ts so the specific alias is matched first
(ensure the keys '@/lib/runtime/build-target' and '@' are reordered
accordingly).
tests/unit/auth-me-route.test.ts (1)

7-34: Optional cleanup: Remove unused getAuthConfig mock from test setup.

The app/api/auth/me/route.ts handler imports and uses only getSession() and isSignedIn(). It never calls or imports getAuthConfig(). Remove the unused mock definition, the vi.mock('@/lib/auth/config', ...) block, and the getAuthConfigMock.mockReturnValue({}) calls from both test cases to keep the test file aligned with the route's actual dependencies.

♻️ Proposed cleanup
 import { beforeEach, describe, expect, it, vi } from 'vitest'

-const getAuthConfigMock = vi.fn()
 const getSessionMock = vi.fn()
 const isSignedInMock = vi.fn()

-vi.mock('@/lib/auth/config', () => ({
-  getAuthConfig: () => getAuthConfigMock(),
-}))
-
 vi.mock('@/lib/auth/session', () => ({
   getSession: () => getSessionMock(),
   isSignedIn: (...args: unknown[]) => isSignedInMock(...args),
 }))

 import { GET } from '@/app/api/auth/me/route'

 describe('auth me route', () => {
   beforeEach(() => {
     vi.clearAllMocks()
   })

   it('returns unauthenticated responses with no-store caching', async () => {
-    getAuthConfigMock.mockReturnValue({})
     getSessionMock.mockResolvedValue({})
     isSignedInMock.mockReturnValue(false)

@@ it('returns authenticated responses with no-store caching', async () => {
-    getAuthConfigMock.mockReturnValue({})
     getSessionMock.mockResolvedValue({
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/auth-me-route.test.ts` around lines 7 - 34, Remove the unused
getAuthConfig mock and its uses: delete the vi.mock('@/lib/auth/config', ...)
block and remove any getAuthConfigMock.mockReturnValue({}) calls in the tests
for the GET handler; keep only the mocks for getSession and isSignedIn
(getSessionMock, isSignedInMock) which are actually used by the GET function in
app/api/auth/me/route. Ensure tests still call GET() and assert headers/body
unchanged after removing getAuthConfig-related setup.
playwright.config.ts (1)

62-73: Normalize the Origin header derived from PLAYWRIGHT_BASE_URL.

Origin is built by reusing PLAYWRIGHT_BASE_URL verbatim. If a user sets PLAYWRIGHT_BASE_URL=http://localhost:3000/ (trailing slash) or with a path, the Origin header value becomes non-canonical and the same-origin check in lib/auth/csrf.ts (which compares URL origins) will start rejecting mutating requests in their local Playwright runs. Fine for the default value, but a footgun the moment someone overrides the env var.

🛡️ Suggested hardening
-    baseURL: process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:3000',
+    baseURL: new URL(
+      process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:3000',
+    ).origin,
@@
-    extraHTTPHeaders: {
-      Origin: process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:3000',
-      'X-Requested-With': 'XMLHttpRequest',
-    },
+    extraHTTPHeaders: {
+      Origin: new URL(
+        process.env.PLAYWRIGHT_BASE_URL || 'http://localhost:3000',
+      ).origin,
+      'X-Requested-With': 'XMLHttpRequest',
+    },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@playwright.config.ts` around lines 62 - 73, Normalize the Origin header by
deriving it from the env var using URL parsing instead of reusing
PLAYWRIGHT_BASE_URL verbatim: compute an origin value via new
URL(process.env.PLAYWRIGHT_BASE_URL).origin (with a try/catch and fallback to
'http://localhost:3000') and use that for extraHTTPHeaders. Update the
extraHTTPHeaders Origin assignment (and keep baseURL as is) so Origin is the
canonical origin string; reference extraHTTPHeaders, PLAYWRIGHT_BASE_URL, and
baseURL and ensure this matches the same-origin checks in lib/auth/csrf.ts.
tests/unit/build-target.test.ts (1)

67-74: Consider broadening the “no process.env” check.

The regex /process\.env\.[A-Z_]/ only catches dotted access. A future regression that uses bracket notation (process.env['ALLOW_INSECURE_OIDC_ISSUER']) or aliases the process.env object first would slip past this guard, while still violating the “no process.env reads” contract documented at the top of lib/runtime/build-target.prod.ts.

♻️ Tighter assertion
-    // Match actual property accesses like process.env.FOO, not prose mentions.
-    expect(src).not.toMatch(/process\.env\.[A-Z_]/)
+    // Match actual reads of process.env in any access form, not prose mentions.
+    // Strip line-comments and block-comments first so doc-text mentions don't
+    // trip the assertion.
+    const code = src
+      .replace(/\/\*[\s\S]*?\*\//g, '')
+      .replace(/(^|[^:])\/\/.*$/gm, '$1')
+    expect(code).not.toMatch(/process\s*\.\s*env\s*\.\s*[A-Z_]/)
+    expect(code).not.toMatch(/process\s*\.\s*env\s*\[/)
+    expect(code).not.toMatch(/process\s*\[\s*['"]env['"]\s*\]/)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/unit/build-target.test.ts` around lines 67 - 74, The test "contains no
process.env reads" currently only detects dotted access and misses bracket
notation and aliases; update the assertion in tests/unit/build-target.test.ts to
use a broader pattern that matches process.env followed by either a dot property
access, a bracketed string property access, or any bare "process.env" occurrence
(which would indicate aliasing/destructuring), and assert that the source of
lib/runtime/build-target.prod.ts does not match that broader pattern so bracket
notation and aliasing regressions are caught.
scripts/verify-prod-bundle.mjs (2)

41-115: Consider extracting the prod-export contract check for unit testing.

The Part 1 logic (extras / missing / value-mismatch comparison against EXPECTED_PROD_EXPORTS) is pure and is exactly the kind of "deterministic script logic" the repository guideline asks to keep covered. Pulling the comparison into an exported helper (e.g. compareProdExports(actual, expected) returning { extras, missing, mismatched }) would make it trivial to assert its three failure modes from Vitest without spawning Node or touching the filesystem.

As per coding guidelines: "Treat deterministic script logic as production code and keep changed files at >= 85% coverage" and "Prefer extracting pure functions from CLI wrappers so behavior is easy to test".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/verify-prod-bundle.mjs` around lines 41 - 115, The verification logic
in verifyBuildTargetProd is pure and should be extracted into a testable helper;
create and export a function (e.g. compareProdExports(actualExports, expected =
EXPECTED_PROD_EXPORTS)) that returns an object like { extras, missing,
mismatched } where mismatched lists keys with differing values, then refactor
verifyBuildTargetProd to call this helper and use its results to set failed/log
messages; keep the existing EXPECTED_PROD_EXPORTS constant and ensure the new
function is exported so Vitest unit tests can assert the three failure modes
without filesystem or Node import side effects.

152-175: Harden the grep invocation: use fixed-string mode and avoid shell quoting.

JSON.stringify(token) only handles JSON escaping; the resulting double-quoted bash word still expands $, backtick, and \, so adding any future token containing those would silently misbehave. Likewise, grep without -F interprets the token as a basic regex — fine for the current alphanumeric-and-hyphen tokens but brittle long term. Switching to spawnSync with array args plus grep -F -r removes both footguns at once.

🛡️ Suggested change
-import { execSync } from 'node:child_process'
+import { spawnSync } from 'node:child_process'
@@
-  for (const token of FORBIDDEN) {
-    for (const dir of [SERVER_DIR, STATIC_DIR]) {
-      let output = ''
-      try {
-        output = execSync(
-          `grep -r --include="*.js" -l ${JSON.stringify(token)} ${JSON.stringify(dir)}`,
-          { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] },
-        ).trim()
-      } catch {
-        // grep exits 1 when nothing is found — that is the success case here.
-        output = ''
-      }
-
-      if (output.length > 0) {
+  for (const token of FORBIDDEN) {
+    for (const dir of [SERVER_DIR, STATIC_DIR]) {
+      const result = spawnSync(
+        'grep',
+        ['-rFl', '--include=*.js', '--', token, dir],
+        { encoding: 'utf8' },
+      )
+      // grep exits 1 when nothing is found — that is the success case here.
+      // Anything other than 0 or 1 is a real failure.
+      if (result.status !== 0 && result.status !== 1) {
+        console.error(
+          `[verify-prod-bundle] grep failed (exit ${result.status}): ${result.stderr}`,
+        )
+        failed = true
+        continue
+      }
+      const output = (result.stdout ?? '').trim()
+      if (output.length > 0) {
         console.error(
           `[verify-prod-bundle] FORBIDDEN token "${token}" found in bundle:`,
         )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/verify-prod-bundle.mjs` around lines 152 - 175, The grep invocation
is brittle and uses shell quoting via JSON.stringify and regex mode; replace
execSync with child_process.spawnSync to call grep directly and pass args as an
array including '-F' and '-r' and '--include=*.js' plus the token as a separate
argument, iterating over FORBIDDEN and [SERVER_DIR, STATIC_DIR] as before; treat
exit status 1 as "not found" (no output) and on exit code 0 read stdout (use
encoding: 'utf8') to populate output, preserving the same logging and setting of
failed when output is non-empty so token hits are reported.
playwright.prodlike.config.ts (1)

29-53: LGTM — webServer and storageState now consistently target the always-auth prodlike posture.

Switching to start:prodlike and pinning storageState to test-results/auth/admin.json correctly aligns with the always-on auth contract. The new comment block makes the dev-IdP prerequisite explicit.

One nit (defer-able): if a contributor runs with PLAYWRIGHT_SKIP_WEBSERVER and globalSetup hasn't populated test-results/auth/admin.json, every spec fails with an opaque ENOENT. Consider adding a guard in tests/integration/global-setup.ts (out of scope here) that surfaces a clearer "run npm run idp:up and rerun" message — verifying that's already covered would close the loop.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@playwright.prodlike.config.ts` around lines 29 - 53, Add a guard in the
global setup to check for the pinned storage state file and surface a clear
error when it's missing: in tests/integration/global-setup.ts (the globalSetup
function), detect when process.env.PLAYWRIGHT_SKIP_WEBSERVER is truthy and
verify the file 'test-results/auth/admin.json' exists before proceeding; if the
file is not found, throw or log an explicit error that tells the contributor to
run the dev IdP (e.g., "run `npm run idp:up` and re-run tests") so specs don't
fail with an opaque ENOENT.
docs/auth-how-it-works.md (1)

28-31: Minor sentence-flow nit on Line 28.

The sentence reads proxy.ts is the front door. Auth is always on, so it: allows public paths, redirects .... The colon mid-sentence after "so it:" inside running prose is awkward — consider a small reflow such as "Auth is always on, so the proxy allows public paths, redirects unauthenticated browser page requests to /api/auth/login, returns 401 for unauthenticated API requests, and requires a Bearer header for /api/mcp." Purely cosmetic.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/auth-how-it-works.md` around lines 28 - 31, Reflow the sentence in
docs/auth-how-it-works.md to remove the awkward mid-sentence colon and make it
run smoothly; replace the two-sentence/colon form that starts with "proxy.ts is
the front door. Auth is always on, so it:" with a single sentence like "Auth is
always on, so the proxy (`proxy.ts`) allows public paths, redirects
unauthenticated browser page requests to `/api/auth/login`, returns `401` for
unauthenticated API requests, and requires a Bearer header for `/api/mcp`."
Ensure the reference to proxy.ts and the endpoint paths remain accurate.
lib/requirements/auth.ts (1)

175-208: Confirm the intent of calling getAuthConfig() for its side effect.

getAuthConfig() is invoked purely to assert config is present (Line 183), then its return value is discarded. After the first request the result is cached, so subsequent calls are cheap, but the pattern is subtle — a future reader could easily delete the line as "dead code". Consider either capturing/using the result or extracting a named helper (e.g. assertAuthConfigLoaded()) so the intent is unmistakable. Functionally fine as-is.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/requirements/auth.ts` around lines 175 - 208, The call to getAuthConfig()
in getActorContextFromSession is currently only for its side effect (ensuring
auth env vars) and looks like dead code; make the intent explicit by replacing
the bare call with either (a) capture and use its result (e.g., const authConfig
= getAuthConfig(); /* use authConfig or pass to helper */) or (b) extract a
clearly named helper like assertAuthConfigLoaded() and call that from
getActorContextFromSession so readers know the call is intentional and won’t
remove it; update the code paths that rely on auth config accordingly
(references: getActorContextFromSession, getAuthConfig, consider adding
assertAuthConfigLoaded).
package.json (1)

20-20: Redundant outer dotenv -e .env.prodlike -- wrapper before npm run build:local-prod.

build:local-prod already wraps next build with dotenv -e .env.prodlike -- cross-env ..., so the outer dotenv on the build half of start:prodlike is duplicative and just adds startup overhead. The second dotenv (after &&) is still needed for next start.

♻️ Proposed simplification
-    "start:prodlike": "dotenv -e .env.prodlike -- npm run build:local-prod && dotenv -e .env.prodlike -- cross-env NODE_ENV=production BUILD_TARGET=local-prod next start --hostname 0.0.0.0 --port 3001",
+    "start:prodlike": "npm run build:local-prod && dotenv -e .env.prodlike -- cross-env NODE_ENV=production BUILD_TARGET=local-prod next start --hostname 0.0.0.0 --port 3001",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` at line 20, The start:prodlike npm script redundantly prepends
dotenv -e .env.prodlike -- to npm run build:local-prod even though
build:local-prod already applies dotenv; remove the outer dotenv wrapper so
start:prodlike simply runs npm run build:local-prod && dotenv -e .env.prodlike
-- cross-env NODE_ENV=production BUILD_TARGET=local-prod next start --hostname
0.0.0.0 --port 3001, leaving the second dotenv before next start intact; update
the "start:prodlike" entry in package.json and verify the script name
"build:local-prod" remains unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/quality-checks.yml:
- Around line 63-65: Add an explicit prod build step before the "Verify prod
build-target contract" step so that the verify:prod-bundle script can run its
second security check against actual build artifacts; specifically, insert a
step that runs "npm run build" (or your production build script) prior to
invoking the npm script "verify:prod-bundle" referenced in the step named
"Verify prod build-target contract" so that the grepping of .next/server and
.next/static is not skipped.

In `@app/api/auth/logout/route.ts`:
- Around line 32-35: The GET handler currently just returns
createLocalRedirect(request, cfg.postLogoutRedirectUri) and must also clear the
user's session to avoid leaving a valid cookie; modify the exported GET function
to call await getSession(request) (or the existing session getter used by the
POST flow), await session.destroy() when a session exists, then proceed to
createLocalRedirect with cfg.postLogoutRedirectUri; reference the GET handler,
getAuthConfig, getSession/session.destroy, and createLocalRedirect so reviewers
can find and mirror the POST handler's logout steps.

In `@docs/arkitekturbeskrivning-kravhantering.md`:
- Around line 1198-1201: Update the misleading doc wording to reference the
actual compile-time exported constant ALLOW_INSECURE_OIDC_ISSUER (from
`@/lib/runtime/build-target`) instead of the old env-var name
AUTH_OIDC_ALLOW_INSECURE_ISSUER; change the fail-closed bullet in
docs/arkitekturbeskrivning-kravhantering.md to state that
ALLOW_INSECURE_OIDC_ISSUER is a build-target compile-time constant (true only in
dev/local-prod) and no longer an environment variable, and make the same
consistent replacement and wording check in docs/auth-how-it-works.md so both
docs refer to the exported symbol and the compile-time semantics.

In `@docs/auth-developer-workflow.md`:
- Around line 9-11: The wording omits the local-prod build target: update the
sentence that mentions "Keycloak in dev, the real OIDC provider in deployed
environments" to explicitly include the local-prod target (which also uses the
local Keycloak at http://localhost:8080) so readers don't assume start:prodlike
uses a real provider; reference the build target name "local-prod" and the env
var "AUTH_OIDC_ALLOW_INSECURE_ISSUER" in the revised sentence so it’s clear
local-prod points to the local Keycloak and that insecure issuer usage is
configurable.

In `@next.config.ts`:
- Around line 11-26: The code lets an empty-string BUILD_TARGET slip through;
change how buildTarget is derived and validated so empty or whitespace-only
values are treated as unset: read the raw env (e.g., rawTarget =
process.env.BUILD_TARGET), normalize with trim() and treat '' as undefined, then
compute buildTarget = normalized ?? (isProduction ? undefined : 'dev'); update
the subsequent checks that currently use truthiness (the production guard and
the whitelist guard) to explicitly test for undefined (e.g., if (isProduction &&
buildTarget === undefined) and if (buildTarget !== undefined &&
!['dev','local-prod','prod'].includes(buildTarget))) so an empty BUILD_TARGET
triggers the Unknown BUILD_TARGET error and resolvedBuildTarget logic no longer
receives a blank string.

In `@package.json`:
- Line 23: The "check" npm script currently runs "verify:prod-bundle" which
silently skips the bundle grep when .next/server and .next/static are absent,
causing false positives; update either the package.json "check" pipeline to
ensure a prod build is present (prepend or guarantee "npm run build" before
"verify:prod-bundle") or modify scripts/verify-prod-bundle.mjs to assert
presence of .next/server and .next/static (or fail after Part 1 /
build-target.prod.ts validation) so the process exits nonzero when artifacts are
missing; reference the "check" script, verify-prod-bundle.mjs, and
build-target.prod.ts when applying the change.

In `@tests/integration/global-setup.ts`:
- Around line 117-138: The success log inside globalSetup that reports a stored
session for each spec is using console.warn; change that one to console.info (or
console.log) so only the skip branch uses console.warn. Locate the loop over
ROLES in globalSetup and replace the console.warn call that logs `[playwright
global-setup] Stored ${spec.role} session at ${spec.filePath}` with console.info
(or console.log), keeping the skip message and its console.warn unchanged; no
other behavior should change.
- Around line 64-80: The regex used to extract the Keycloak form action
(actionMatch from loginHtml using
/<form[^>]*id="kc-form-login"[^>]*action="([^"]+)"/i) is order-dependent and can
fail if action appears before id; update the extraction to be order-agnostic by
either parsing loginHtml with an HTML/DOM parser (e.g.,
querySelector('form#kc-form-login').getAttribute('action')) or replace the regex
with one that matches both attributes in any order (ensure it still decodes
using decodeHtmlEntities into formAction); update the logic around
actionMatch/loginHtml/formAction accordingly so the code reliably finds the form
action regardless of attribute ordering.

---

Outside diff comments:
In `@lib/requirements/auth.ts`:
- Around line 31-37: The JSDoc for hsaId incorrectly mentions "header-trust"
actors which no longer exist after removing the 'headers' variant from
ActorSource; update the comment on the hsaId field in lib/requirements/auth.ts
to remove the "header-trust" reference so it matches the new identity model
(keep notes about null for anonymous or MCP actors and the synthetic
`mcp-client:<client_id>` value for MCP service-account tokens).

---

Nitpick comments:
In @.github/workflows/quality-checks.yml:
- Around line 106-107: The workflow's dotenv-linter action (entry named
'dotenv-linter' running "npm run dotenv:check") and the package.json script
'dotenv:check' use different exclusion patterns so CI and local runs can
diverge; update either the workflow action or the 'dotenv:check' npm script so
they use the exact same dotenv-linter exclude globs (e.g., root exclusions like
.env.sqlserver, .env.local, .env.*.local and devcontainer exclusions like
.devcontainer/.env and .devcontainer/elevated/.env) — locate the 'dotenv:check'
script in package.json and the 'dotenv-linter' entry in the workflow (name:
'dotenv-linter') and make their exclusion flags identical to ensure parity
between local and CI linting.

In `@dev/keycloak/realm-kravhantering-dev.json`:
- Around line 129-179: The docs lack mention of the new Keycloak client
"kravhantering-local" (clientId: kravhantering-local, secret:
local-kc-app-secret, port: 3001) added for the prodlike/local-prod flow; update
docs/auth-developer-workflow.md (and optionally docs/auth-how-it-works.md) to
document this client: where to configure redirect URIs
(http://localhost:3001/api/auth/callback and
http://127.0.0.1:3001/api/auth/callback), webOrigins, the client secret, and
that this client is used for the prodlike target when running locally so
developers know which clientId/secret and port to point their local app at.
- Around line 129-179: The prod-bundle verifier doesn't currently block the new
local client id/secret from the Keycloak JSON; update the FORBIDDEN list in
scripts/verify-prod-bundle.mjs to include "kravhantering-local" and
"local-kc-app-secret" so the verifier will catch any accidental static embedding
of those dev-only values; ensure the additions are added to the same FORBIDDEN
constant/array that already contains "kravhantering-app" and "dev-only" and run
the verifier tests.

In `@docs/auth-how-it-works.md`:
- Around line 28-31: Reflow the sentence in docs/auth-how-it-works.md to remove
the awkward mid-sentence colon and make it run smoothly; replace the
two-sentence/colon form that starts with "proxy.ts is the front door. Auth is
always on, so it:" with a single sentence like "Auth is always on, so the proxy
(`proxy.ts`) allows public paths, redirects unauthenticated browser page
requests to `/api/auth/login`, returns `401` for unauthenticated API requests,
and requires a Bearer header for `/api/mcp`." Ensure the reference to proxy.ts
and the endpoint paths remain accurate.

In `@lib/requirements/auth.ts`:
- Around line 175-208: The call to getAuthConfig() in getActorContextFromSession
is currently only for its side effect (ensuring auth env vars) and looks like
dead code; make the intent explicit by replacing the bare call with either (a)
capture and use its result (e.g., const authConfig = getAuthConfig(); /* use
authConfig or pass to helper */) or (b) extract a clearly named helper like
assertAuthConfigLoaded() and call that from getActorContextFromSession so
readers know the call is intentional and won’t remove it; update the code paths
that rely on auth config accordingly (references: getActorContextFromSession,
getAuthConfig, consider adding assertAuthConfigLoaded).

In `@lib/runtime/build-target.prod.ts`:
- Around line 16-26: The constants ALLOW_INSECURE_OIDC_ISSUER,
USE_INSECURE_COOKIE, and USE_DEV_CSP are annotated as boolean which widens them
away from the literal false; change their types to the literal false (e.g.,
export const USE_DEV_CSP: false = false) or use const assertions so TypeScript
narrows branches at compile time (leave BUILD_TARGET as 'prod'); apply the same
literal-typing pattern symmetrically across the other build-target files so the
module shape remains compatible.

In `@package.json`:
- Line 20: The start:prodlike npm script redundantly prepends dotenv -e
.env.prodlike -- to npm run build:local-prod even though build:local-prod
already applies dotenv; remove the outer dotenv wrapper so start:prodlike simply
runs npm run build:local-prod && dotenv -e .env.prodlike -- cross-env
NODE_ENV=production BUILD_TARGET=local-prod next start --hostname 0.0.0.0 --port
3001, leaving the second dotenv before next start intact; update the
"start:prodlike" entry in package.json and verify the script name
"build:local-prod" remains unchanged.

In `@playwright.config.ts`:
- Around line 62-73: Normalize the Origin header by deriving it from the env var
using URL parsing instead of reusing PLAYWRIGHT_BASE_URL verbatim: compute an
origin value via new URL(process.env.PLAYWRIGHT_BASE_URL).origin (with a
try/catch and fallback to 'http://localhost:3000') and use that for
extraHTTPHeaders. Update the extraHTTPHeaders Origin assignment (and keep
baseURL as is) so Origin is the canonical origin string; reference
extraHTTPHeaders, PLAYWRIGHT_BASE_URL, and baseURL and ensure this matches the
same-origin checks in lib/auth/csrf.ts.

In `@playwright.prodlike.config.ts`:
- Around line 29-53: Add a guard in the global setup to check for the pinned
storage state file and surface a clear error when it's missing: in
tests/integration/global-setup.ts (the globalSetup function), detect when
process.env.PLAYWRIGHT_SKIP_WEBSERVER is truthy and verify the file
'test-results/auth/admin.json' exists before proceeding; if the file is not
found, throw or log an explicit error that tells the contributor to run the dev
IdP (e.g., "run `npm run idp:up` and re-run tests") so specs don't fail with an
opaque ENOENT.

In `@scripts/verify-prod-bundle.mjs`:
- Around line 41-115: The verification logic in verifyBuildTargetProd is pure
and should be extracted into a testable helper; create and export a function
(e.g. compareProdExports(actualExports, expected = EXPECTED_PROD_EXPORTS)) that
returns an object like { extras, missing, mismatched } where mismatched lists
keys with differing values, then refactor verifyBuildTargetProd to call this
helper and use its results to set failed/log messages; keep the existing
EXPECTED_PROD_EXPORTS constant and ensure the new function is exported so Vitest
unit tests can assert the three failure modes without filesystem or Node import
side effects.
- Around line 152-175: The grep invocation is brittle and uses shell quoting via
JSON.stringify and regex mode; replace execSync with child_process.spawnSync to
call grep directly and pass args as an array including '-F' and '-r' and
'--include=*.js' plus the token as a separate argument, iterating over FORBIDDEN
and [SERVER_DIR, STATIC_DIR] as before; treat exit status 1 as "not found" (no
output) and on exit code 0 read stdout (use encoding: 'utf8') to populate
output, preserving the same logging and setting of failed when output is
non-empty so token hits are reported.

In `@tests/unit/auth-me-route.test.ts`:
- Around line 7-34: Remove the unused getAuthConfig mock and its uses: delete
the vi.mock('@/lib/auth/config', ...) block and remove any
getAuthConfigMock.mockReturnValue({}) calls in the tests for the GET handler;
keep only the mocks for getSession and isSignedIn (getSessionMock,
isSignedInMock) which are actually used by the GET function in
app/api/auth/me/route. Ensure tests still call GET() and assert headers/body
unchanged after removing getAuthConfig-related setup.

In `@tests/unit/build-target.test.ts`:
- Around line 67-74: The test "contains no process.env reads" currently only
detects dotted access and misses bracket notation and aliases; update the
assertion in tests/unit/build-target.test.ts to use a broader pattern that
matches process.env followed by either a dot property access, a bracketed string
property access, or any bare "process.env" occurrence (which would indicate
aliasing/destructuring), and assert that the source of
lib/runtime/build-target.prod.ts does not match that broader pattern so bracket
notation and aliasing regressions are caught.

In `@tests/unit/mcp-token.test.ts`:
- Around line 83-197: The tests still include the obsolete enabled property in
getAuthConfigMock.mockReturnValue calls, causing inconsistent mocks; remove
enabled: true from every getAuthConfigMock.mockReturnValue usage in this test
file (used in the blocks that set issuerUrl/apiAudience in the
verifyMcpBearerToken tests and the security audit events beforeEach) so the
mocked AuthConfig matches the new contract—look for
getAuthConfigMock.mockReturnValue({...}) in the tests around the
verifyMcpBearerToken specs and the beforeEach and delete the enabled: true
entries.

In `@vitest.config.ts`:
- Around line 71-90: The alias ordering causes the general '@' alias to shadow
the more specific '@/lib/runtime/build-target' rule; move the
'@/lib/runtime/build-target' entry so it appears before the '@' alias in the
resolve.alias object in vitest.config.ts so the specific alias is matched first
(ensure the keys '@/lib/runtime/build-target' and '@' are reordered
accordingly).
🪄 Autofix (Beta)

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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e3763b4d-15bb-4ee0-ba8e-19bae9743167

📥 Commits

Reviewing files that changed from the base of the PR and between 30449fa and 54dea15.

📒 Files selected for processing (51)
  • .env.development
  • .env.example
  • .env.prodlike
  • .github/workflows/quality-checks.yml
  • app/api/auth/callback/route.ts
  • app/api/auth/login/route.ts
  • app/api/auth/logout/route.ts
  • app/api/auth/me/route.ts
  • components/AuthMenu.tsx
  • cspell.jsonc
  • dev/keycloak/realm-kravhantering-dev.json
  • docs/arkitekturbeskrivning-kravhantering.md
  • docs/auth-developer-workflow.md
  • docs/auth-how-it-works.md
  • docs/mcp-server-contributor-guide.md
  • lib/auth/config.ts
  • lib/auth/csrf.ts
  • lib/auth/login-state.ts
  • lib/auth/mcp-token.ts
  • lib/auth/oidc.ts
  • lib/auth/session.ts
  • lib/mcp/http.ts
  • lib/requirements/auth.ts
  • lib/runtime/build-target.local-prod.ts
  • lib/runtime/build-target.prod.ts
  • lib/runtime/build-target.ts
  • next.config.ts
  • package.json
  • playwright.config.ts
  • playwright.guide.config.ts
  • playwright.prodlike.config.ts
  • proxy.ts
  • scripts/verify-prod-bundle.mjs
  • tests/integration/auth-login.spec.ts
  • tests/integration/global-setup.ts
  • tests/quality/functional.test.ts
  • tests/unit/auth-callback-audit.test.ts
  • tests/unit/auth-config.test.ts
  • tests/unit/auth-logout-audit.test.ts
  • tests/unit/auth-me-route.test.ts
  • tests/unit/auth-session-diagnostics.test.ts
  • tests/unit/auth-session.test.ts
  • tests/unit/build-target.test.ts
  • tests/unit/mcp-http.test.ts
  • tests/unit/mcp-token.test.ts
  • tests/unit/proxy.test.ts
  • tests/unit/requirements-auth-session-errors.test.ts
  • tests/unit/requirements-auth.test.ts
  • tests/unit/requirements-service.test.ts
  • vitest.config.ts
  • vitest.setup.ts
💤 Files with no reviewable changes (8)
  • app/api/auth/login/route.ts
  • app/api/auth/callback/route.ts
  • tests/unit/auth-callback-audit.test.ts
  • .env.development
  • tests/unit/auth-logout-audit.test.ts
  • app/api/auth/me/route.ts
  • tests/unit/auth-session.test.ts
  • tests/unit/auth-session-diagnostics.test.ts

Comment thread .github/workflows/quality-checks.yml Outdated
Comment thread app/api/auth/logout/route.ts
Comment thread docs/arkitekturbeskrivning-kravhantering.md Outdated
Comment thread docs/auth-developer-workflow.md Outdated
Comment thread next.config.ts
Comment thread package.json Outdated
Comment thread tests/integration/global-setup.ts
Comment thread tests/integration/global-setup.ts

@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: 1

🧹 Nitpick comments (3)
docs/auth-how-it-works.md (1)

298-306: Consider adding explicit rebuild semantics for build-target auth constants.

This section is correct, but add one sentence that changing build target/auth behavior requires a rebuild (not runtime env toggling) to avoid operational confusion.

Based on learnings: Do not leave auth behaviour changes documented only in work documents; ensure they are reflected in docs/auth-how-it-works.md or docs/auth-developer-workflow.md.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/auth-how-it-works.md` around lines 298 - 306, Add a single clarifying
sentence after the paragraph about the "insecure-issuer allowance" and
"build-target constant" that states these auth-related build-target constants
are baked into the build and changing them requires rebuilding (not toggling
runtime env vars); reference the "insecure-issuer" and "build-target constant"
terms and, for context, mention the "local-prod" / "npm run start:prodlike"
scenario so readers understand this is a build-time change rather than a runtime
toggle.
tests/integration/global-setup.ts (1)

175-180: Catch-all error wrapping can mislead on auth-level failures.

Every failure from loginAndSaveStorageState (e.g., wrong password producing a non-2xx Keycloak response, or /api/auth/me reporting authenticated=false) is rewrapped with "Make sure the IdP is running … and the dev server is reachable at ${baseUrl}". The underlying message is appended, but the headline points contributors at infrastructure when the real cause may be credentials or session wiring. Consider only adding the reachability hint for connection-class failures (e.g., ECONNREFUSED, fetch errors), or splitting the message so the inner reason is the headline.

next.config.ts (1)

38-43: Optional: tighten the buildTarget cast and skip unused path computation in dev.

Two small polish items, both non-blocking:

  1. buildTarget as 'dev' | 'local-prod' | 'prod' (line 38) is safe given the preceding throws, but TS doesn't narrow through Array.prototype.includes on a string[]. A as const tuple + Array.includes typed as readonly BuildTarget[] (or a small type guard) would let TS narrow without a cast and would keep the suffix logic in sync if a new target is added.
  2. buildTargetModulePath is computed on every config load, but only consumed when resolvedBuildTarget !== 'dev'. Hoisting the fileURLToPath/new URL work into the if branch in webpack() (or a lazy getter) avoids the wasted resolution for next dev.
♻️ Sketch
-const resolvedBuildTarget = buildTarget as 'dev' | 'local-prod' | 'prod'
-const buildTargetSuffix =
-  resolvedBuildTarget === 'dev' ? '' : `.${resolvedBuildTarget}`
-const buildTargetModulePath = fileURLToPath(
-  new URL(`./lib/runtime/build-target${buildTargetSuffix}.ts`, import.meta.url),
-)
+const BUILD_TARGETS = ['dev', 'local-prod', 'prod'] as const
+type BuildTarget = (typeof BUILD_TARGETS)[number]
+const isBuildTarget = (v: unknown): v is BuildTarget =>
+  typeof v === 'string' && (BUILD_TARGETS as readonly string[]).includes(v)
+// (replace the validation block with a single `!isBuildTarget(buildTarget)` check)
+const resolvedBuildTarget: BuildTarget = isBuildTarget(buildTarget)
+  ? buildTarget
+  : 'dev'

…and move the fileURLToPath(...) call inside the webpack() if (resolvedBuildTarget !== 'dev') branch.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@next.config.ts` around lines 38 - 43, Tighten the build target typing and
avoid computing the module path in dev: replace the broad `buildTarget as 'dev'
| 'local-prod' | 'prod'` cast by using a const tuple (e.g. readonly
BuildTargets) with Array.includes typed to narrow or add a small type guard so
`resolvedBuildTarget` is properly inferred without an unsafe cast; then move the
`fileURLToPath(new URL(...))` work that produces `buildTargetModulePath` out of
the top-level and into the `webpack()` branch (or a lazy getter) so the
`fileURLToPath`/`new URL` call only runs when `resolvedBuildTarget !== 'dev'`
and `buildTargetSuffix` logic remains consistent with the typed targets.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/integration-tests.yml:
- Around line 65-80: Update project docs to explicitly state that the
integration-tests GitHub Actions workflow requires a running local Keycloak for
both matrix legs (dev and prodlike) so CI consumers know the hard dependency; in
the workflow steps named "Start local IdP service" and "Wait for IdP to be
ready" replace the inline curl retry loop (curl -sf
http://localhost:8080/realms/kravhantering-dev/.well-known/openid-configuration)
with either a Docker-level healthcheck and use docker compose up -d --wait or at
minimum add --max-time 5 to each curl attempt to bound hangs, and mention the
idp:up / idp:down commands and the new CI requirement in the documentation text
so local and CI setup are consistent.

---

Nitpick comments:
In `@docs/auth-how-it-works.md`:
- Around line 298-306: Add a single clarifying sentence after the paragraph
about the "insecure-issuer allowance" and "build-target constant" that states
these auth-related build-target constants are baked into the build and changing
them requires rebuilding (not toggling runtime env vars); reference the
"insecure-issuer" and "build-target constant" terms and, for context, mention
the "local-prod" / "npm run start:prodlike" scenario so readers understand this
is a build-time change rather than a runtime toggle.

In `@next.config.ts`:
- Around line 38-43: Tighten the build target typing and avoid computing the
module path in dev: replace the broad `buildTarget as 'dev' | 'local-prod' |
'prod'` cast by using a const tuple (e.g. readonly BuildTargets) with
Array.includes typed to narrow or add a small type guard so
`resolvedBuildTarget` is properly inferred without an unsafe cast; then move the
`fileURLToPath(new URL(...))` work that produces `buildTargetModulePath` out of
the top-level and into the `webpack()` branch (or a lazy getter) so the
`fileURLToPath`/`new URL` call only runs when `resolvedBuildTarget !== 'dev'`
and `buildTargetSuffix` logic remains consistent with the typed targets.
🪄 Autofix (Beta)

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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1dbd503b-60d7-47b6-9d61-9efbe0ead9c4

📥 Commits

Reviewing files that changed from the base of the PR and between 54dea15 and 605c9a1.

📒 Files selected for processing (19)
  • .github/workflows/integration-tests.yml
  • .github/workflows/quality-checks.yml
  • app/api/auth/logout/route.ts
  • cspell.jsonc
  • docs/arkitekturbeskrivning-kravhantering.md
  • docs/auth-developer-workflow.md
  • docs/auth-how-it-works.md
  • lib/requirements/auth.ts
  • lib/runtime/build-target.prod.ts
  • next.config.ts
  • package.json
  • playwright.config.ts
  • playwright.prodlike.config.ts
  • tests/integration/global-setup.ts
  • tests/unit/auth-logout-audit.test.ts
  • tests/unit/auth-me-route.test.ts
  • tests/unit/build-target.test.ts
  • tests/unit/mcp-token.test.ts
  • tests/unit/playwright-global-setup.test.ts
💤 Files with no reviewable changes (1)
  • tests/unit/auth-me-route.test.ts
✅ Files skipped from review due to trivial changes (2)
  • cspell.jsonc
  • tests/unit/auth-logout-audit.test.ts
🚧 Files skipped from review as they are similar to previous changes (10)
  • app/api/auth/logout/route.ts
  • lib/runtime/build-target.prod.ts
  • .github/workflows/quality-checks.yml
  • tests/unit/build-target.test.ts
  • tests/unit/mcp-token.test.ts
  • package.json
  • playwright.prodlike.config.ts
  • docs/arkitekturbeskrivning-kravhantering.md
  • playwright.config.ts
  • docs/auth-developer-workflow.md

Comment thread .github/workflows/integration-tests.yml
@johlju

johlju commented Apr 26, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Apr 26, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@johlju
johlju merged commit d73f223 into viscalyx:main Apr 26, 2026
7 checks passed
@johlju
johlju deleted the f/auth-build-time-module-swap branch April 26, 2026 07:52
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.

Plan: Compile-time AUTH_ENABLE removal (module-alias swapping)

1 participant