From 1d58475a578430879bdaee77641c3aa23b7961c8 Mon Sep 17 00:00:00 2001 From: Chris Kehayias Date: Tue, 29 Sep 2026 18:22:35 -0400 Subject: [PATCH] chore: prepare the repo for public use - Add an MIT LICENSE; set license/repository/homepage/bugs in package.json and replace the README's "License: Private". - Replace the real-looking mpi.ministryplatform.com default with the your-instance.ministryplatform.com placeholder in .env.example and setup.ts, and stop setup offering the placeholder as the host default. - Remove internal working files: the dated deps-audit reports (now gitignored local copies; deps-known-issues.md is the durable record) and the open setup-check TODO. - Move the closed 2026-09-28 auth review record from .claude/TODO/ to docs/security/2026-09-28-auth-review.md and neutralize its instance-specific wording. - Repoint test comments that cited deleted TODO files, and reword "TODO security-*" tags as review items so they don't read as open work. - .gitignore: .claude/reports/, /out/, npm-debug.log*, OS/editor files. Co-Authored-By: Claude Opus 5.5 (1M context) --- ...igate-setup-check-stale-model-detection.md | 56 ---- .claude/commands/audit-deps.md | 6 +- .../port-security-review-2026-09-28.md | 2 +- .claude/references/deps-known-issues.md | 4 +- .claude/references/testing.md | 14 +- .claude/reports/deps-audit-2026-08-21-run2.md | 152 ---------- .claude/reports/deps-audit-2026-08-21.md | 259 ------------------ .claude/reports/deps-audit-2026-08-27.md | 159 ----------- .env.example | 4 +- .gitignore | 14 + LICENSE | 21 ++ README.md | 9 +- .../security/2026-09-28-auth-review.md | 12 +- package-lock.json | 1 + package.json | 9 + scripts/setup.ts | 7 +- src/auth.code-flow.test.ts | 2 +- src/auth.discovery-rebuild.test.ts | 2 +- src/auth.ip-address.test.ts | 2 +- src/auth.oidc-hardening.test.ts | 8 +- src/auth.origin-check.test.ts | 2 +- src/auth.rate-limit.test.ts | 2 +- src/auth.secret-guard.test.ts | 4 +- src/auth.session-config.test.ts | 2 +- src/auth.session-lifetime.test.ts | 2 +- src/auth.test.ts | 10 +- src/auth.user-id-cache.test.ts | 2 +- src/auth.user-oauth-tokens.test.ts | 2 +- src/components/contact-logs/actions.test.ts | 2 +- .../contact-logs/contact-logs.test.tsx | 3 +- .../contact-lookup-details/actions.test.ts | 4 +- src/services/contactLogService.test.ts | 2 +- 32 files changed, 96 insertions(+), 684 deletions(-) delete mode 100644 .claude/TODO/investigate-setup-check-stale-model-detection.md delete mode 100644 .claude/reports/deps-audit-2026-08-21-run2.md delete mode 100644 .claude/reports/deps-audit-2026-08-21.md delete mode 100644 .claude/reports/deps-audit-2026-08-27.md create mode 100644 LICENSE rename .claude/TODO/security-review-2026-09-28-index.md => docs/security/2026-09-28-auth-review.md (94%) diff --git a/.claude/TODO/investigate-setup-check-stale-model-detection.md b/.claude/TODO/investigate-setup-check-stale-model-detection.md deleted file mode 100644 index 579ba48d..00000000 --- a/.claude/TODO/investigate-setup-check-stale-model-detection.md +++ /dev/null @@ -1,56 +0,0 @@ -# TODO: `setup:check` cannot detect stale or never-regenerated Ministry Platform models - -**Created:** 2026-05-21 -**Severity:** Misleading — `setup:check` reports green when the on-disk models do not actually match the configured MP instance. -**Reported by:** Customer walkthrough #4, 2026-05-21. - -## Symptom - -When the interactive `npm run setup` flow fails at step 8 (type generation) — for example, because `MINISTRY_PLATFORM_CLIENT_SECRET` is wrong — the script leaves the **previously committed 603 generated model files on disk untouched**. Running `npm run setup:check` immediately afterward then reports: - -``` -6 passed, 2 warnings, 0 failed -``` - -…because the `setup:check` validation counts files in `src/lib/providers/ministry-platform/models/` and finds the expected number. It has no way to know: - -1. Those files were generated against a **different** MP instance (the repo's reference instance, not the customer's). -2. The customer's credentials never successfully produced a single generated file. -3. The build step "passed" against those stale models, so the customer's app will type-check against the wrong schema. - -A customer who hits this sequence sees a green `setup:check` and reasonably concludes their environment is healthy when it is not. - -## Why this matters - -- The whole point of `setup:check` is to be a quick "is this environment correctly configured" command. Right now it can be lying. -- The failure mode is silent: `setup:check` is green, build is green, `npm run dev` will probably even start. The customer only discovers the problem when their data shapes don't match the generated types — which may be far away from the cause in time and frustration. -- This compounds the type-gen error truncation issue (fixed in commit `b022be5`): even with the real OAuth error now visible, a customer who misses it and runs `setup:check` will be told everything is fine. - -## Possible approaches - -In rough order from least to most invasive: - -1. **Compare model directory mtime to `.env.local` mtime.** If `.env.local` is newer than every file in `models/`, flag a warning: "Environment changed since types were last generated — run `npm run mp:generate:models`". Cheap, doesn't make a network call. Doesn't catch the case where `.env.local` is wrong from the start. -2. **Look for a per-instance signature inside the models.** Embed the MP host (or a hash of it) into a generated `MODELS_PROVENANCE.json` at type-gen time, then have `setup:check` read it and verify it matches the current `.env.local`. Catches the cross-instance staleness case. Requires changes to the type generator too. -3. **Issue a single low-cost authenticated metadata call** in `setup:check` (e.g., `GET /tables?$top=1`) to confirm the credentials in `.env.local` actually work. Catches both "wrong credentials" and "credentials work but models are from a different instance" when combined with approach 2. Adds network dependency to `setup:check`. -4. **Record the result of the last `mp:generate:models` run.** Write a timestamp and exit code to a `.setup-state.json` (gitignored). `setup:check` reads it and warns if the last generate run failed or never ran. Doesn't catch out-of-band schema changes but does catch the customer's exact scenario. - -Approach 4 is the smallest change. Approach 2 is the most accurate. Approach 3 might be overkill for a check command. - -## How to verify a fix - -1. Clone the repo fresh (so committed models are present). -2. Configure `.env.local` with **deliberately wrong** `MINISTRY_PLATFORM_CLIENT_SECRET`. -3. Run `npm run setup` and observe type generation fail. -4. Run `npm run setup:check`. Today: green. After fix: should warn or fail with an explanation of which check triggered (mtime mismatch, provenance mismatch, network probe failure, or last-run state). - -## Out of scope - -- Don't change the **type generator** itself in this work unless approach 2 is chosen — keep the change scoped to the check command and (if needed) a small provenance file. -- Don't auto-regenerate from `setup:check`. The check should report, not mutate. - -## References - -- Commit `b022be5` — fixes the truncated type-gen error and the dishonest "Setup Complete!" summary. This TODO is the next layer of the same customer journey. -- `scripts/setup.ts` — `runSetupCheck` function and its dependents. -- `src/lib/providers/ministry-platform/scripts/generate-types.ts` — type generator (touch only if approach 2 is chosen). diff --git a/.claude/commands/audit-deps.md b/.claude/commands/audit-deps.md index ce23186b..9b316bec 100644 --- a/.claude/commands/audit-deps.md +++ b/.claude/commands/audit-deps.md @@ -219,7 +219,7 @@ For each approved major, one at a time: ## Phase 9 — Report, persist, commit -**Report** (in chat, and written to `.claude/reports/deps-audit-.md`): +**Report** (in chat, and written to `.claude/reports/deps-audit-.md`, which is gitignored: a local working copy, never committed — the durable record is `deps-known-issues.md`): ```markdown # Dependency Audit — @@ -263,8 +263,8 @@ longer applies. Keep dates on everything. - Separate commits per tier so a bisect is meaningful: - `fix(deps): resolve advisories via in-range updates` - `chore(deps): bump -> ` (one per major) - - `docs(deps): audit report ` -- Include the report path and the verification results in the commit body. + - `docs(deps): update known issues ` +- Include the report summary and the verification results in the commit body. - Sign off as configured in CLAUDE.md's commit conventions. - **Do not push and do not open a PR.** Tell the user the branch is ready. diff --git a/.claude/playbooks/port-security-review-2026-09-28.md b/.claude/playbooks/port-security-review-2026-09-28.md index e8727ee6..7651a061 100644 --- a/.claude/playbooks/port-security-review-2026-09-28.md +++ b/.claude/playbooks/port-security-review-2026-09-28.md @@ -511,7 +511,7 @@ git show e17c55e # SessionContextService rethrows Next control-flow er Reference docs in the upstream repo: -- `.claude/TODO/security-review-2026-09-28-index.md`: the review record (every item, decisions, and what was checked and held) +- `docs/security/2026-09-28-auth-review.md`: the review record (every item, decisions, and what was checked and held) - `docs/security/downstream-hardening-playbook.md` § 2026-09-29 follow-up: the human-readable summary - `docs/security/Additional_Security_Hardening.md`: the open policy options (revocation, MP login re-validation, role granularity) - `.claude/references/auth.md`: session lifetime, emergency sign-out, authorization policy, the per-process caveats diff --git a/.claude/references/deps-known-issues.md b/.claude/references/deps-known-issues.md index 37736d9f..7f805db3 100644 --- a/.claude/references/deps-known-issues.md +++ b/.claude/references/deps-known-issues.md @@ -3,8 +3,8 @@ State carried between `/audit-deps` runs so each audit starts from prior conclusions instead of re-deriving them. Every entry needs a date and a re-check trigger. -Last audit: **2026-08-27** — report at `.claude/reports/deps-audit-2026-08-27.md` -(prior: `deps-audit-2026-08-21-run2.md`, `deps-audit-2026-08-21.md`). +Last full audit: **2026-08-27** (prior: 2026-08-21, twice). Per-run reports are local +working files (`.claude/reports/`, gitignored); this file is the durable record. Advisory state re-verified **2026-09-12** (release docs pass — read-only, no upgrades applied): `npm audit` reports **0 vulnerabilities across 720 packages** diff --git a/.claude/references/testing.md b/.claude/references/testing.md index c7b7e558..082a96ed 100644 --- a/.claude/references/testing.md +++ b/.claude/references/testing.md @@ -845,19 +845,13 @@ production), `procedure.service.test.ts` (stored procedures can mutate data), an ## Deferred Issues -Defects and refactors found while testing are documented one-per-file in -`.claude/TODO/`, not fixed silently. **Every test-derived TODO is now closed** — +Defects and refactors found while testing were documented, not fixed silently. +**Every test-derived item is now closed** — filter injection via numeric IDs, the two unauthenticated `'use server'` actions, the missing authorization gate, the `SessionContextService` refactor, the N+1 lookup, the token lifetime, and the untested contact-log component. See -`.claude/docs/TestCoverage.md` §5 and §6 for each one. `.claude/TODO/` holds a -single open item, `investigate-setup-check-stale-model-detection.md`, which is a -setup-wizard issue with no bearing on the suite. - -Five test files still carry `Regression guard for .claude/TODO/.md` comments -naming TODO files that have since been deleted (`mp-filter-injection-numeric-ids.md`, -`n-plus-1-contact-log-types-lookup.md`, `contact-logs-component-untested.md`). -The assertions are correct; only the breadcrumbs are dangling. +`.claude/docs/TestCoverage.md` §5 and §6 for each one. The tests that pin those +fixes carry `Regression guard for …` comments naming the fix. Those assertions are now specifications rather than snapshots — `should NOT delete when the caller holds no security role` and `should permit editing a log diff --git a/.claude/reports/deps-audit-2026-08-21-run2.md b/.claude/reports/deps-audit-2026-08-21-run2.md deleted file mode 100644 index ece3e16f..00000000 --- a/.claude/reports/deps-audit-2026-08-21-run2.md +++ /dev/null @@ -1,152 +0,0 @@ -# Dependency Audit — 2026-08-21 (run 2) - -Baseline: audited tree = `package.json` as of `4446294` (clean working tree at start) -· node v24.18.0 · npm 11.16.0 - -> Second audit run on the same date. The earlier run's report is preserved at -> `.claude/reports/deps-audit-2026-08-21.md`; this file does not replace it. - -## Summary - -Zero advisories across every source checked — `npm audit` reports 0 findings over 583 -packages, and OSV returns 0 vulnerabilities for all 11 security-critical runtime and dev -packages at their installed versions. One in-range update was available and applied: -`next` and `eslint-config-next` 16.3.1 → 16.3.2. **The pre-announced critical Next.js -vulnerability is still unpatched** — 16.3.2 shipped today, but as a bug-fix backport, not -the security release. No major upgrade is actionable: all three re-verified as blocked or -deliberately held. Nothing needs a decision from you. - -## Security findings - -| Package | Advisory | Severity | Exposure here | Scope | Evidence | Action | -|---|---|---|---|---|---|---| -| — | none | — | — | — | `npm audit` = 0 findings / 583 pkgs; OSV = 0 vulns for `next`, `react`, `react-dom`, `better-auth`, `zod`, `dotenv`, `tsx`, `vitest`, `jsdom`, `esbuild`, `vite` | none | - -### Unpatched upstream — tracked, no action possible - -**`next` — pre-announced critical vulnerability.** Vercel -[announced](https://nextjs.org/blog/upcoming-nextjs-security-release-august-2026) -(published 2026-08-20) one **critical** severity vulnerability, with patches scheduled for -**2026-08-26**. - -The announcement names `16.3.2` and `15.5.24` as the patch versions. `16.3.2` **has already -shipped** (published 2026-08-21T09:36:39Z) — but its release notes are explicitly -bug-fixes-only and carry no security content: - -> This release is backporting bug fixes. It does **not** include all pending features/changes on canary. - -Six listed changes, all Turbopack / app-router / Turborepo-OIDC fixes. No CVE, no advisory, -and `15.5.24` does not exist (the `backport` dist-tag is still `15.5.23`). - -**Conclusion: 16.3.2 is not the security release, and upgrading to it does not address the -critical vulnerability.** The announced version numbering will presumably shift (16.3.3 / -15.5.24), or the advisory will land separately on 08-26. Installed `16.3.2` is the newest -available, so no action is possible today. Re-check on/after **2026-08-26**. - -### Not exploitable — do not "fix" - -**`better-auth` 2026 CVE cluster** (`CVE-2026-53513` SSRF CVSS 9.6, `CVE-2026-53516` OAuth -auto-link ATO, `CVE-2026-45337` deviceAuthorization, `CVE-2026-67336` insecure crypto -defaults) — re-verified this run, reason still holds on two independent grounds: - -1. All fixed in `1.6.11`+; installed is `1.7.1`. -2. The vulnerable plugins are not loaded. `src/lib/auth.ts` registers exactly - `genericOAuth` (line 77), `customSession` (line 164), `nextCookies` (line 186). - A grep of `src/` and `scripts/` for `oidcProvider`, `ssoPlugin`, - `deviceAuthorization`, `apiKey(`, `mcp(` returns **no matches**. - -OSV independently returns 0 vulns for `better-auth@1.7.1`. - -## Updates - -| Package | Current | Wanted | Latest | Tier | Risk | Notes | -|---|---|---|---|---|---|---| -| `next` | 16.3.1 | 16.3.2 | 16.3.2 | 1 (in-range) | Low | Applied. Bug-fix backport; **not** the security patch | -| `eslint-config-next` | 16.3.1 | 16.3.2 | 16.3.2 | 1 (in-range) | Low | Applied alongside `next` | -| `@types/node` | 24.13.3 | 24.13.3 | 26.2.0 | 4 (held) | — | Held one major below latest to match the Node 24.18.0 runtime | -| `eslint` | 9.39.5 | 9.39.5 | 10.8.1 | 3 (blocked) | — | Blocked upstream at the plugin layer — see below | -| `typescript` | 6.0.3 | 6.0.3 | 7.0.2 | 3 (blocked) | — | Blocked by typescript-eslint — see below | - -No Tier 2 range bumps were available: every other dependency is already at `latest`. - -### Blocked majors — re-verified this run, not inherited - -**`eslint` 10.x — blocked, and the prior diagnosis is now stronger.** All three plugins -that `eslint-config-next` pulls in are at their **latest published versions** and none -accepts ESLint 10: - -| Plugin | Latest | `eslint` peer range | -|---|---|---| -| `eslint-plugin-react` | 7.37.5 | `^3 \|\| … \|\| ^9.7` | -| `eslint-plugin-import` | 2.32.0 | `^2 \|\| … \|\| ^9` | -| `eslint-plugin-jsx-a11y` | 6.10.2 | `^3 \|\| … \|\| ^9` | - -This is not a stale pin in `eslint-config-next@16.3.2` (which depends on -`eslint-plugin-react@^7.37.0`, `eslint-plugin-import@^2.32.0`, -`eslint-plugin-jsx-a11y@^6.10.0`) — the plugins themselves have shipped **no** ESLint 10 -support at all. Nothing to wait on downstream; upstream must move first. - -*Correction to the prior run's note:* in the current tree these plugins resolve **hoisted -at top level** (`node_modules/eslint-plugin-react`), not nested under -`eslint-config-next/node_modules/`. The binding constraint is identical either way, but -check the resolved plugin versions rather than a fixed nested path. - -**`typescript` 7.x — blocked, unchanged.** `typescript-eslint@latest` (8.67.0) and -`@canary` (8.67.1-alpha.24) *both* still declare `typescript: >=4.8.4 <6.1.0`. There is no -9.x line. As established last run, the blocker is lint alone — build and tests pass on -TS 7 — because TypeScript 7.0 ships no compiler API and typescript-eslint requires one. -Upstream targets TS >= 7.1. - -**`@types/node` 26.x — held, unchanged.** Local runtime is still Node 24.18.0. Moving types -two majors ahead of the runtime would let `tsc` accept APIs that do not exist at runtime. - -## Applied - -`package.json` **unchanged** — no version range was modified. Lockfile only: - -``` -package-lock.json | 276 +++++++++++++------------------------- -1 file changed, 93 insertions(+), 183 deletions(-) -``` - -- `next` 16.3.1 → 16.3.2 (plus all 8 `@next/swc-*` platform binaries and `@next/env`) -- `eslint-config-next` 16.3.1 → 16.3.2, `@next/eslint-plugin-next` 16.3.1 → 16.3.2 -- Dedupe/hoist churn, no version regressions: `ajv@6.15.0` and - `json-schema-traverse@0.4.1` hoisted to top level; `whatwg-url` 16.0.1 → 17.1.0 hoisted - from `jsdom`; six `@emnapi`/`@napi-rs`/`@tybys` copies under the - `@tailwindcss/oxide-wasm32-wasi` optional tree pruned. - -Applied via `npm update` (in-range only). Plain `npm install` was a no-op — the lockfile -already satisfied `package.json`, so it held at 16.3.1. `npm audit fix` was not needed at -0 findings. `--force` was never used. - -## Verification - -- **build**: **pass** — `next build` (Turbopack), 16.3.2, compiled in 3.5s, TypeScript - check 1798ms, 7/7 static pages, 8 routes + proxy -- **lint**: **pass** — `eslint .`, exit 0, no warnings -- **tests**: **pass** — 21/21 files, **279/279** tests, 2.87s - -## Needs your decision - -Nothing. No major is in a state worth attempting — each blocker was re-verified above -against current upstream metadata. - -## Deferred / blocked - -| Item | Reason | Re-check trigger | -|---|---|---| -| `next` critical vulnerability | Advisory unpublished; 16.3.2 is a bug-fix backport, not the patch. Installed version is already the newest available | On/after **2026-08-26** | -| `eslint` 10.x | `eslint-plugin-react` / `-import` / `-jsx-a11y` at latest cap at `^9` | Any of the three ships ESLint 10 support | -| `typescript` 7.x | `typescript-eslint` latest + canary both cap at `<6.1.0`; TS 7.0 has no compiler API | typescript-eslint ships TS >= 7.1 support ([#10940](https://github.com/typescript-eslint/typescript-eslint/issues/10940)) | -| `@types/node` 26.x | Runtime is Node 24.18.0 | Runtime moves to 26 | - -### Pre-existing, unrelated to dependencies - -- `vitest.config.ts` CJS/ESM warning — Vite 8 warns the config uses ESM syntax while loaded - as CommonJS, and that `configLoader: 'native'` becomes the default in a future major. - Fix: rename to `vitest.config.mts`. Still present; not a blocker. -- `pkce: false` at `src/lib/auth.ts:102` — security posture item, not a dependency issue. - MP discovery advertises `S256`, so PKCE can likely be enabled. Needs its own change + test. -- `npm warn allow-scripts` for `esbuild@0.28.2` and `unrs-resolver@1.12.2` — npm 11 - install-script gating; both are expected native-binary postinstalls. Informational. diff --git a/.claude/reports/deps-audit-2026-08-21.md b/.claude/reports/deps-audit-2026-08-21.md deleted file mode 100644 index 5cd45ebe..00000000 --- a/.claude/reports/deps-audit-2026-08-21.md +++ /dev/null @@ -1,259 +0,0 @@ -# Dependency Audit — 2026-08-21 - -Baseline: `c77b15c` (clean tree) · node v24.18.0 · npm 11.16.0 -Audited tree = `package.json` as of `c77b15c`, verified via `npm ls --all` (no `invalid`/`missing`; -all `UNMET OPTIONAL` entries are cross-platform native binaries and are expected on Windows). - -## Summary - -Zero exploitable vulnerabilities. `npm audit` reports **0 findings** across 583 packages, and OSV -returns no advisories for any installed version of the 17 packages checked individually — including -every runtime-critical one. 32 direct dependencies were updated in-range (`npm update`, no -`package.json` range edits) and build, lint, and the full 279-test suite all pass. - -Four approved changes were then applied on top: **`openai` removed** (it was a declared runtime -dependency with zero usage anywhere in the repo), **`@types/node` realigned `^25` → `^24`** to match -the Node 24.18.0 runtime, and dev majors **`jsdom` 30**, **`chalk` 6**, and **`jest-dom` 7** (plus an -explicit `@testing-library/dom`). A fifth, **ESLint 10, was attempted and reverted** — see below. - -**ESLint 10 is blocked, contrary to the initial phase-5 assessment.** The peer ranges of -`eslint-config-next` and `typescript-eslint` do accept v10, but `eslint-config-next@16.3.1` *vendors* -`eslint-plugin-react`, `eslint-plugin-import`, and `eslint-plugin-jsx-a11y` under its own -`node_modules`, and all three cap `eslint` at `^9`. Installing v10 produced `ERESOLVE overriding peer -dependency` and `invalid:` in `npm ls`, and `eslint .` then failed outright: -`TypeError: Error while loading rule 'react/display-name': contextOrFilename.getFilename is not a -function` — `eslint-plugin-react` still calls the `context.getFilename()` API that v10 removed. Not -a single file could be linted. Reverted; lint is green again on 9.39.5. - -One forward-looking item, no action available: Next.js has **pre-announced a critical-severity -vulnerability** with patches (16.3.2 / 15.5.24) landing **2026-08-26**. We are on 16.3.1, the current -latest — there is nothing to upgrade to yet. Re-run this audit on or after Aug 26. - -## Security findings - -| Package | Advisory | Severity | Exposure here | Scope | Evidence | Action | -|---|---|---|---|---|---|---| -| `next` | Pre-announced, undisclosed (patch = 16.3.2, due 2026-08-26) | Critical (vendor-stated) | **Pending** — unknowable; advisory not published | runtime | [nextjs.org announcement, published 2026-08-20](https://nextjs.org/blog/upcoming-nextjs-security-release-august-2026); installed 16.3.1 is the latest 16.3.x | None available. Re-audit on/after 2026-08-26 and upgrade to 16.3.2 | -| — | `npm audit` | — | none | — | 0 findings, 583 packages audited | — | -| — | OSV (17 packages) | — | none | — | `next@16.3.1`, `react`/`react-dom@19.2.6`, `better-auth@1.7.1`, `zod@4.4.3`, `openai@6.38.0`, `dotenv@17.4.2`, `vitest@4.1.6`, `vite@8.1.4`, `esbuild@0.28.1`, `jsdom@29.1.1`, `tsx@4.22.1`, `postcss@8.5.23`, `tailwindcss@4.3.0`, `eslint@9.39.4`, `typescript@6.0.3`, `react-hook-form@7.76.0`, `lucide-react@1.16.0` — all clean | — | - -### Not exploitable — do not "fix" - -- **`better-auth` — the 2026 CVE cluster** (`CVE-2026-53513` SSRF/CVSS 9.6 in `@better-auth/sso`, - `CVE-2026-53516` OAuth auto-link account takeover, `CVE-2026-45337` `deviceAuthorization`, - `CVE-2026-67336` insecure crypto defaults in `oidcProvider`/`mcp`). Two independent reasons this - does not apply: **(a)** all are fixed in `1.6.11`+ and this repo runs `1.7.1`; **(b)** the - vulnerable plugins are not loaded. Re-verified against `src/lib/auth.ts` on 2026-08-21 — the - plugin list is exactly `genericOAuth`, `customSession`, `nextCookies`. A repo-wide grep for - `oidcProvider`, `mcp(`, `ssoPlugin`, `deviceAuthorization`, `apiKey(` matches nothing outside - comments. -- **`postcss` bundled inside `next`** — the 2 moderate findings recorded in the prior audit are - **gone as of `next@16.3.1`**. `npm audit` returns 0. This entry has been cleared from the - known-issues file. The `npm audit fix --force` ban stands regardless: it still resolves such - findings by downgrading `next` to 9.3.3. - -### Observations (not dependency vulnerabilities) - -- `src/lib/auth.ts` sets `pkce: false` on the `genericOAuth` provider. The adjacent code comment - notes MP discovery advertises `code_challenge_methods_supported: ["plain","S256"]`, so PKCE can - likely be enabled. OAuth 2.1 makes PKCE the default for good reason; enabling it removes - authorization-code interception risk. Out of scope for a dependency audit — worth a separate, - separately-tested change. -- `@types/node` is declared `^25` while the local runtime is **Node 24.18.0**. The `@types/node` - major tracks the Node major, so the type surface currently describes APIs the runtime may not - have. See "Needs your decision". - -## Updates - -### Applied — Tier 1 (in-range, no `package.json` change) - -| Package | Change | -|---|---| -| `@hookform/resolvers` | 5.2.2 → 5.9.1 | -| `@inquirer/prompts` | 8.4.3 → 8.6.0 | -| `@radix-ui/react-alert-dialog` | 1.1.15 → 1.1.23 | -| `@radix-ui/react-avatar` | 1.1.11 → 1.2.6 | -| `@radix-ui/react-checkbox` | 1.3.3 → 1.3.11 | -| `@radix-ui/react-dialog` | 1.1.15 → 1.1.23 | -| `@radix-ui/react-dropdown-menu` | 2.1.16 → 2.1.24 | -| `@radix-ui/react-label` | 2.1.8 → 2.1.15 | -| `@radix-ui/react-radio-group` | 1.3.8 → 1.4.7 | -| `@radix-ui/react-select` | 2.2.6 → 2.3.7 | -| `@radix-ui/react-slot` | 1.2.4 → 1.3.3 | -| `@radix-ui/react-switch` | 1.2.6 → 1.3.7 | -| `@radix-ui/react-tooltip` | 1.2.8 → 1.2.16 | -| `@tailwindcss/postcss` | 4.3.0 → 4.3.3 | -| `@tailwindcss/typography` | 0.5.19 → 0.5.20 | -| `@types/node` | 25.8.0 → 25.9.5 | -| `@types/react` | 19.2.14 → 19.2.18 | -| `@types/react-dom` | 19.2.3 → 19.2.4 | -| `@vitejs/plugin-react` | 6.0.2 → 6.1.0 | -| `@vitest/coverage-v8` | 4.1.6 → 4.1.11 | -| `autoprefixer` | 10.5.0 → 10.5.4 | -| `eslint` | 9.39.4 → 9.39.5 | -| `eslint-config-next` | 16.2.6 → **16.3.1** (now aligned with `next@16.3.1`; was a minor-line mismatch) | -| `lucide-react` | 1.16.0 → **1.33.0** (largest in-range jump; build + lint clean) | -| `openai` | 6.38.0 → 6.49.0 | -| `postcss` | 8.5.23 → 8.5.26 | -| `react` | 19.2.6 → 19.2.8 | -| `react-dom` | 19.2.6 → 19.2.8 | -| `react-hook-form` | 7.76.0 → 7.85.0 | -| `tailwindcss` | 4.3.0 → 4.3.3 | -| `tsx` | 4.22.1 → 4.23.12 | -| `vitest` | 4.1.6 → 4.1.11 | - -Transitively, `typescript-eslint` moved 8.59.3 → **8.67.0** (via `eslint-config-next@16.3.1`), which -is what unblocks ESLint 10 — see below. - -**Tier 2 (range bumps, non-major): none required.** Every non-major `latest` is already reachable -inside the declared `^` ranges, so `package.json` needed no edits at all. - -### Tier 3 — majors remaining - -| Package | Current | Latest | Scope | Verdict | -|---|---|---|---|---| -| `openai` | 6.49.0 | 7.5.0 | runtime (declared) | **Removed** — unused | -| `eslint` | 9.39.5 | 10.8.1 | dev | **Attempted → BLOCKED**, reverted | -| `@testing-library/jest-dom` | 6.9.1 | 7.0.1 | dev | **Applied** (+ explicit `@testing-library/dom`) | -| `jsdom` | 29.1.1 | 30.0.1 | dev | **Applied** | -| `chalk` | 5.6.2 | 6.0.0 | dev | **Applied** | -| `typescript` | 6.0.3 | 7.0.2 | dev/build | **Blocked** (unchanged) | -| `@types/node` | 25.9.5 | 26.2.0 | dev | **Realigned down to `^24.13.3`** to match the runtime | - -### Applied — Tier 3, second pass (approved 2026-08-21) - -`package.json` diff, verified green after each individual change: - -```diff - dependencies: -- "openai": "^6.32.0", - - devDependencies: -+ "@testing-library/dom": "^10.4.1", -- "@testing-library/jest-dom": "^6.9.1", -+ "@testing-library/jest-dom": "^7.0.1", -- "@types/node": "^25.5.0", -+ "@types/node": "^24.13.3", -- "chalk": "^5.6.2", -+ "chalk": "^6.0.0", -- "jsdom": "^29.0.0", -+ "jsdom": "^30.0.1", -``` - -- **`openai` removed.** Single-package footprint; `npm install` removed exactly 1 package. Build, - lint, and 279 tests green afterward — confirming nothing referenced it. -- **`@types/node` → `^24.13.3`.** Note `24.18.0` (the *Node* version) does not exist as an - `@types/node` release — the highest 24.x is **24.13.3**. Writing the unsatisfiable `^24.18.0` sent - the npm resolver into pathological backtracking (killed after ~870 CPU-seconds with no output) - rather than failing fast; the corrected range installed in under a second. Type-check is clean, so - nothing in the codebase depended on a Node 25-only API. -- **`jsdom` → 30.0.1.** 279/279 tests pass. -- **`chalk` → 6.0.0.** Verified by actually running the one consumer: `npm run setup:check` renders - colored output and completes all 8 checks. (That script reports `Missing: BETTER_AUTH_SECRET, - BETTER_AUTH_URL` — pre-existing and unrelated; per CLAUDE.md those have `NEXTAUTH_*` fallbacks, - which is what `.env.local` appears to use.) -- **`jest-dom` → 7.0.1** with `@testing-library/dom@^10.4.1` promoted from transitive to an explicit - `devDependency`, as v7 requires. No `invalid:` peers afterward. - -### Rejected — ESLint 10 (attempted, reverted) - -| Step | Result | -|---|---| -| `"eslint": "^10.8.1"` + `npm install` | `npm warn ERESOLVE overriding peer dependency` ×3; `npm error invalid: eslint@10.8.1` | -| `npm run lint` | **Hard failure.** `TypeError: Error while loading rule 'react/display-name': contextOrFilename.getFilename is not a function` at `eslint-config-next/node_modules/eslint-plugin-react/lib/util/version.js:31` | -| Revert | Range restored to `^9.39.2`, `npm install` re-resolved the lockfile (not `git checkout` — the lockfile also carries the changes being kept). `npm ls` clean, `eslint .` green. | - -The blocker is **not** `eslint-config-next` or `typescript-eslint` — both accept v10 in their declared -peers. It is the three ESLint plugins `eslint-config-next@16.3.1` vendors under its own -`node_modules`, each capped at `eslint: ^9`: - -| Vendored plugin | Declared `eslint` peer | -|---|---| -| `eslint-plugin-import` | `^2 \|\| ^3 \|\| ^4 \|\| ^5 \|\| ^6 \|\| ^7.2.0 \|\| ^8 \|\| ^9` | -| `eslint-plugin-jsx-a11y` | `^3 \|\| ^4 \|\| ^5 \|\| ^6 \|\| ^7 \|\| ^8 \|\| ^9` | -| `eslint-plugin-react` | `^3 \|\| ^4 \|\| ^5 \|\| ^6 \|\| ^7 \|\| ^8 \|\| ^9.7` | - -Checking only the top-level peer of `eslint-config-next` is not sufficient for this package — its -vendored plugin tree has to be checked too. Recorded in the known-issues file so the next run does -not retry it blindly. - -## Verification - -Full pass run after Tier 1, and again after each individual Tier 3 change. **Final tree:** - -- **build**: **pass** — `next build` (Turbopack), Next.js 16.3.1, TypeScript check clean, 8 routes emitted. -- **lint**: **pass** — `eslint .`, no output (zero errors, zero warnings). -- **tests**: **pass** — 21 files, **279/279 tests passed**. -- **audit**: **0 vulnerabilities**, 595 packages. -- **`npm ls`**: no `invalid:` peers. -- **`npm outdated`**: only the 3 documented holds remain (`@types/node` 26, `eslint` 10, `typescript` 7). - -Pre-existing, not introduced by this audit: `vitest run` emits a Vite 8 forward-compat warning that -`vitest.config.ts` uses ESM syntax while being loaded as CommonJS, and that `configLoader: 'native'` -will become the default in a future Vite major. Renaming to `vitest.config.mts` clears it. Not -applied — source change, outside this command autonomy contract. - -Also restored: `next-env.d.ts`. Running `next build` rewrites its route-type imports from -`./.next/dev/types/...` to `./.next/types/...`; `next dev` writes them back. That churn is an -artifact of verification, not a change, so it was reverted with `git checkout` to keep the diff -dependency-only. - -## Decisions taken (all four approved and acted on 2026-08-21) - -The four items below were presented for approval; all were approved. Outcomes are recorded above — -three applied as proposed, one (ESLint 10) attempted and reverted on hard evidence. - -1. **Remove `openai` — approved and applied.** It is declared in `dependencies` at `^6.32.0` but has **zero - references** anywhere: no `import`/`require` in `src/` or `scripts/`, no `OPENAI*` environment - variable, no reference in any config. The only mentions in the repo are `package.json` itself and - the `/audit-deps` command doc that lists it as security-critical. Carrying it means auditing and - patching a runtime dependency that ships nothing. Removing it also makes the major upgrade moot. - *Alternative if it is scaffolding for planned work:* move it to `devDependencies`, or bump to - `^7` now while nothing depends on the v6 API surface. - -2. **ESLint 9 → 10 — approved, attempted, BLOCKED** (see "Rejected — ESLint 10" above). The - reasoning below is preserved because it was the basis for approval, and because it shows exactly - where the pre-flight check fell short: it verified the top-level peers but not the plugin tree - `eslint-config-next` vendors. ESLint 9.39.5 is **deprecated upstream** — npm prints "This - version is no longer supported" on install. Both recorded blockers have cleared: - `typescript-eslint@8.67.0` now declares `eslint: ^8.57.0 || ^9.0.0 || ^10.0.0`, and - `eslint-config-next@16.3.1` declares `eslint: >=9.0.0`. Node 24.18.0 satisfies the ESLint 10 - engine range `^20.19.0 || ^22.13.0 || >=24`. Blast radius is small: this repo is already pure - flat config (`eslint.config.mjs`, one root file, no `.eslintrc*` anywhere), so the headline - breaking change — removal of the legacy eslintrc system — does not touch it. The one thing that - could surface new findings is the new JSX reference tracking in v10, which changes scope analysis - in `.tsx` files; that shows up immediately as lint output, and this is lint-only with no runtime - effect. - -3. **Dev-tooling majors — approved and applied (all three).** `jsdom` 29→30, `chalk` 5→6, - `@testing-library/jest-dom` 6→7. All three - are test/script-only and engine-compatible with Node 24.18.0. - - `jsdom` 30: engine bump to `^22.22.2 || ^24.15.0 || >=26`, plus `CSS.escape()`/`CSS.supports()` - and `getComputedStyle()` pixel-conversion fixes. Consumed only as `environment: 'jsdom'` in - `vitest.config.ts`; the `vitest` peer on it is `*`. - - `chalk` 6: requires Node ≥22, treats numeric `FORCE_COLOR` as an exact level. One consumer, - `scripts/setup.ts:19`, using basic color methods. - - `jest-dom` 7: requires Node ≥22 and makes `@testing-library/dom` an **explicit** peer. It is - currently only transitive (`@testing-library/react@16.3.2` → `@testing-library/dom@10.4.1`, - which satisfies `>=10 <11`), so this upgrade also means **adding `@testing-library/dom` to - `devDependencies`** — a new dependency, hence the approval. Consumed only via - `src/test-setup.ts:1`. - -4. **`@types/node` — approved: realigned to `^24` (resolved `24.13.3`), not `^26`.** The - `@types/node` major tracks the Node - major, and the local runtime is **Node 24.18.0** while the declared range is `^25`. The tree is - already a major *ahead* of its runtime, which lets `tsc` accept Node 25 APIs that Node 24 does - not have. Going to `^26` widens that gap. The `vitest` peer (`^20 || ^22 || >=24`) is satisfied - by any of them, so this is purely about type fidelity. I would align it to `^24` — but confirm - the deployment target Node version first, since CI/production may not be on 24. - -## Deferred / blocked - -- **`eslint` 10.8.1** — blocked by the ESLint plugins vendored inside `eslint-config-next@16.3.1` - (`eslint-plugin-react`, `eslint-plugin-import`, `eslint-plugin-jsx-a11y`, all capped at `^9`). - Verified by attempt, 2026-08-21. Re-check when `eslint-config-next` ships a release whose bundled - plugins accept ESLint 10 — in particular when `eslint-plugin-react` drops `context.getFilename()`. -- **`typescript` 7.0.2** — still blocked, re-verified 2026-08-21. `typescript-eslint@8.67.0` (latest) - caps its peer at `typescript: >=4.8.4 <6.1.0`. Re-check when typescript-eslint widens that range. -- **`next` 16.3.2** — not yet published. Re-check 2026-08-26. -- **`@types/node` 26** — held pending the runtime-version decision above. diff --git a/.claude/reports/deps-audit-2026-08-27.md b/.claude/reports/deps-audit-2026-08-27.md deleted file mode 100644 index 22500dac..00000000 --- a/.claude/reports/deps-audit-2026-08-27.md +++ /dev/null @@ -1,159 +0,0 @@ -# Dependency Audit — 2026-08-27 - -Baseline: `89436f0` · node v24.18.0 · npm 11.16.0 -Prior runs: [2026-08-21](deps-audit-2026-08-21.md), [2026-08-21 run 2](deps-audit-2026-08-21-run2.md) - -## Summary - -The Next.js critical security release that the last audit was waiting on **shipped on -2026-08-25**, one day ahead of its announced date. It patches **two critical unauthenticated -RCE vulnerabilities**. This repo was running the vulnerable `next@16.3.2`; it is now on -`16.3.3`, applied as an ordinary in-range update with no `package.json` change. - -Both vulnerabilities triage to **Not exploitable in this application** — but only because of -specific, verifiable properties of this codebase that a future commit could quietly remove. -Both re-check triggers are recorded in the known-issues file. - -Notably, **`npm audit` and OSV both reported zero findings for the vulnerable version**. The -advisory was only visible at the vendor source. A single-source audit would have declared this -repo clean while it was running a critical unauthenticated RCE. - -Six other in-range updates rode along. All three remaining majors stay blocked or held for -unchanged upstream reasons. Nothing needs your decision. - -## Security findings - -| Package | Advisory | Severity | Exposure here | Scope | Evidence | Action | -|---|---|---|---|---|---|---| -| `next` | [GHSA-2xp9-vwfh-vxw4](https://github.com/vercel/next.js/security/advisories/GHSA-2xp9-vwfh-vxw4) — AVIF image-optimization RCE via `libheif`/`sharp` | **Critical** | **Not exploitable** (3 independent reasons) | runtime | `next.config.ts` (no `images` config); 3 `` call sites; `public/` | **Patched anyway** → `16.3.3` | -| `next` | [CVE-2026-75604](https://www.cve.org/CVERecord?id=CVE-2026-75604) / [GHSA-p293-qw3h-jr36](https://github.com/vercel/next.js/security/advisories/GHSA-p293-qw3h-jr36) — Windows-hosted server RCE | **Critical** | **Not exploitable** (one of two preconditions fails) | runtime | no source `pages/`; no `getServerSideProps`/`getStaticProps`/`next/router` in `src/` | **Patched anyway** → `16.3.3` | - -Advisory published **2026-08-25**; `16.3.3` published 2026-08-25T15:32:19Z, `15.5.24` at -16:14:06Z. Source: [August 2026 Security Release](https://nextjs.org/blog/august-2026-security-release). - -### Why the scanners missed this - -| Source | Verdict on `next@16.3.2` | | -|---|---|---| -| `npm audit` | 0 vulnerabilities / 720 packages | ❌ missed | -| OSV (`api.osv.dev`) | 0 vulns | ❌ missed | -| Vendor blog / GHSA | 2 × critical RCE | ✅ caught | - -Neither advisory database had ingested the GHSA entries ~2 days after publication. Keep the -vendor-source step in this audit; it is the only thing that found the highest-severity issue -in the repo. - -### Not exploitable — do not "fix" - -**GHSA-2xp9-vwfh-vxw4 (AVIF RCE)** — requires Next.js to optimize an *attacker-controlled* -AVIF image. Three independent reasons that cannot happen here: - -1. **All three `` call sites pass `unoptimized`**, bypassing the Image Optimization API - entirely — `contact-lookup-results.tsx:95`, `contact-lookup-details.tsx:78`, - `layout/header.tsx:60`. -2. **`next.config.ts` declares no `images.remotePatterns` / `images.domains`** (the file is an - empty config). With an empty allowlist `/_next/image` rejects every remote URL, so an - attacker cannot point the optimizer at a hostile AVIF — this is what makes the MP-hosted - avatar URLs unreachable through the optimizer too. -3. **The only local file the optimizer could reach is `public/assets/icons/favicon.ico`** — - repo-committed, not attacker-controlled. - -Note `/_next/image` *is* unauthenticated by design: `src/proxy.ts:32` excludes it from the -protected matcher. That is correct and unchanged, but it means reasons 1–3 are the whole -defense — re-check them before enabling image optimization. - -**CVE-2026-75604 (Windows RCE)** — requires **both** preconditions: - -- *Windows filesystem* — **met.** Development runs on Windows 11. -- *Both Pages Router and App Router in use* — **not met.** App Router only (`src/app/`); no - source `pages/` directory, and no `getServerSideProps` / `getStaticProps` / `next/router` - anywhere in `src/`. The `./.next/server/pages` path found on disk is a build artifact Next.js - emits for internal error pages, not a Pages Router adoption. - -Because the OS half of the precondition *is* satisfied here, this one had a narrower margin -than the AVIF issue — introducing a single `pages/` route would have made it live. Patched. - -## Updates - -| Package | Current | Wanted | Latest | Tier | Risk | Notes | -|---|---|---|---|---|---|---| -| `next` | 16.3.2 | **16.3.3** | 16.3.3 | 1 | low | **Critical security fix.** In range of `^16.1.6` | -| `sharp` (transitive) | 0.35.3 | **0.35.4** | — | 1 | low | Pulled in by `next@16.3.3`; the AVIF-vulnerable dep | -| `eslint-config-next` | 16.3.2 | 16.3.3 | 16.3.3 | 1 | low | Tracks `next` | -| `better-auth` | 1.7.1 | 1.7.2 | 1.7.2 | 1 | low | Auth-critical; OSV clean at both versions | -| `lucide-react` | 1.33.0 | 1.34.0 | 1.34.0 | 1 | low | Icons | -| `react-hook-form` | 7.85.0 | 7.86.0 | 7.86.0 | 1 | low | | -| `@inquirer/prompts` | 8.6.0 | 8.7.0 | 8.7.0 | 1 | low | dev — setup wizard | -| `@types/react-dom` | 19.2.4 | 19.2.5 | 19.2.5 | 1 | low | dev — types | -| `@types/node` | 24.13.3 | 24.13.3 | 26.4.0 | 4 | — | **Held** at `^24` to match Node 24.18.0 runtime | -| `eslint` | 9.39.5 | 9.39.5 | 10.9.1 | 4 | — | **Blocked** — plugin peers cap at `^9` | -| `typescript` | 6.0.3 | 6.0.3 | 7.0.2 | 4 | — | **Blocked** — typescript-eslint has no TS 7 support | - -## Applied - -- `npm update` → the eight in-range bumps above. **`package.json` unchanged** — no version - range was modified, so nothing here required approval. -- `npm audit fix` (no `--force`) → no-op; 0 advisories in the registry data. -- `npm run deps:relock` → **required.** See below. - -`git diff --stat`: `package-lock.json` only, 451 insertions / 451 deletions. - -### The lockfile guard earned its keep again - -`npm install` alone reported "up to date" and applied nothing — it honors the existing -lockfile when it satisfies `package.json`. **`npm update` is what actually moves in-range -dependencies**, and it dedupes as a side effect. That dedupe re-broke the lockfile for Linux CI -in the exact documented pattern: - -``` -✗ package-lock.json does not match Linux resolution. - Missing entries that Linux needs (10): - + node_modules/eslint/node_modules/ajv - + node_modules/@eslint/eslintrc/node_modules/ajv (+ json-schema-traverse) - + node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/* (6 entries) - Entries Linux resolution does not produce (2): - - node_modules/ajv - - node_modules/json-schema-traverse -``` - -This is the `64f18f0` incident reproduced precisely — `ajv` hoisted to top level, the -`@emnapi/*` subtree pruned. `npm run deps:relock` fixed it; `npm run deps:verify` now passes. -Had this been committed unchecked, CI would have died at the install step before running a -single test. - -**Carry forward:** the audit's "apply safe updates" step must be `npm update`, and `npm update` -*always* requires a relock afterward on Windows. - -## Verification - -| Step | Result | -|---|---| -| `npm run deps:verify` | ✅ pass — lockfile tree matches Linux resolution | -| `npm run build` | ✅ pass — compiled in 6.3s, TypeScript 4.4s, 7/7 static pages, 8 routes | -| `npm run lint` | ✅ pass — clean, no warnings | -| `npm run test:run` | ✅ pass — **582/582 tests, 32/32 files**, 23.29s | - -## Needs your decision - -**Nothing.** No major upgrade is currently viable, and no in-range change required approval. - -## Deferred / blocked - -| Item | Status | Re-check trigger | -|---|---|---| -| `typescript` 7.x | Blocked, **re-confirmed**. typescript-eslint moved 8.67.0 → **8.68.0**; its `typescript` peer is *still* `>=4.8.4 <6.1.0`, and there is still **no 9.x line** (newest is `8.68.1-alpha.5`). TS 7 ships no compiler API; upstream targets TS ≥ 7.1. | typescript-eslint ships TS ≥ 7.1 support | -| `eslint` 10.x | Blocked, **re-confirmed**. `eslint-plugin-react@7.37.5` (`^9.7`), `eslint-plugin-import@2.32.0` (`^9`), `eslint-plugin-jsx-a11y@6.10.2` (`^9`) — all still at latest, all still capped at `^9`. `eslint-config-next@16.3.3` already permits the newest of each, so this is an upstream gap. **Partial movement:** `typescript-eslint@8.68.0` now accepts `^10.0.0`, so it is no longer part of the blockage — the three plugins are the sole remaining constraint. | Any of the three plugins ships ESLint 10 support | -| `@types/node` 26.x | Held by design at `^24.13.3` to match the Node 24.18.0 runtime. | Runtime moves off Node 24 | - -## Behavioral note - -`16.3.3` **disables AVIF optimization** as its mitigation, pending an upstream `libheif` fix. -No impact here — all `` uses are `unoptimized`. But if image optimization is enabled -later, AVIF output will not be produced until that upstream fix lands. - -## Still open (unchanged, not dependency blockers) - -- `pkce: false` in `src/lib/auth.ts` — MP discovery advertises `S256`; PKCE can likely be - enabled. Needs its own change + test. Raised 2026-08-21. -- `vitest.config.ts` CJS/ESM warning — still emitted on every test run. Fix: rename to - `vitest.config.mts`. Pre-existing. diff --git a/.env.example b/.env.example index a3ed19a5..9adea1e9 100644 --- a/.env.example +++ b/.env.example @@ -84,7 +84,7 @@ AUTH_TRUSTED_PROXIES= # trailing slash is stripped. The app refuses to start otherwise. MINISTRY_PLATFORM_CLIENT_ID=MPNext.API MINISTRY_PLATFORM_CLIENT_SECRET= -MINISTRY_PLATFORM_BASE_URL=https://mpi.ministryplatform.com/ministryplatformapi +MINISTRY_PLATFORM_BASE_URL=https://your-instance.ministryplatform.com/ministryplatformapi # ============================================================================= # Authorization @@ -125,7 +125,7 @@ MP_SECURITY_ROLES= # ============================================================================= # NEXT Public Keys # ============================================================================= -NEXT_PUBLIC_MINISTRY_PLATFORM_FILE_URL=https://mpi.ministryplatform.com/ministryplatformapi/files +NEXT_PUBLIC_MINISTRY_PLATFORM_FILE_URL=https://your-instance.ministryplatform.com/ministryplatformapi/files NEXT_PUBLIC_APP_NAME=MPNextApp # ============================================================================= diff --git a/.gitignore b/.gitignore index 705c8377..f46b4363 100644 --- a/.gitignore +++ b/.gitignore @@ -434,3 +434,17 @@ FodyWeavers.xsd # Playwright MCP snapshots/logs (hold real MP member data; never commit) .playwright-mcp/ + +# Claude Code /audit-deps run reports (local working copies; the durable record is .claude/references/deps-known-issues.md) +.claude/reports/ + +# Next.js static export output +/out/ + +# npm debug logs +npm-debug.log* + +# OS and editor files +.DS_Store +Thumbs.db +.idea/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..11b6ab16 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025-2026 MinistryPlatform-Community contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 4241a276..ba80b138 100644 --- a/README.md +++ b/README.md @@ -484,8 +484,7 @@ MPNext/ ├── .claude/ # Claude AI configuration │ ├── commands/ # Claude Code skills (/audit-deps, /release) │ ├── references/ # Documentation references -│ ├── playbooks/ # Porting playbooks -│ └── reports/ # Past dependency audit reports +│ └── playbooks/ # Porting playbooks ├── .githooks/ # pre-commit lockfile guard ├── .github/workflows/ # CI: tests, lint + tsc, build + prerender check, lockfile drift check ├── .github/dependabot.yml # Weekly SHA bumps for GitHub Actions @@ -815,7 +814,7 @@ Performs a complete review of dependencies: real vulnerability exposure, availab /audit-deps --no-verify # Skip the post-upgrade verification run ``` -> This command never reads or writes Ministry Platform data — it touches only local dependencies and report files. Past run reports are kept in `.claude/reports/`. +> This command never reads or writes Ministry Platform data — it touches only local dependencies and report files. Run reports go to `.claude/reports/`, which is gitignored; lasting findings are recorded in [Dependency Known Issues](.claude/references/deps-known-issues.md). ### `/release` - GitHub Release @@ -968,7 +967,7 @@ Also: **do not run `npm ci` while `next dev` is running.** It deletes `node_modu **Never run `npm audit fix --force`.** It still "fixes" bundled-dependency findings by downgrading `next` to a major version this codebase cannot run on. -Re-check with `/audit-deps`, which triages each finding for real exploitability rather than treating the raw count as the signal. Triaged vendor advisories that `npm audit` does not surface are tracked in [Dependency Known Issues](.claude/references/deps-known-issues.md); past run reports are kept in `.claude/reports/`. +Re-check with `/audit-deps`, which triages each finding for real exploitability rather than treating the raw count as the signal. Triaged vendor advisories that `npm audit` does not surface are tracked in [Dependency Known Issues](.claude/references/deps-known-issues.md). ## Contributing @@ -976,7 +975,7 @@ This project follows strict TypeScript conventions and code style. Please review ## License -Private +[MIT](LICENSE). Fork it, customize it, and run it for your church; keep the copyright and license notice in your copy. ## Support diff --git a/.claude/TODO/security-review-2026-09-28-index.md b/docs/security/2026-09-28-auth-review.md similarity index 94% rename from .claude/TODO/security-review-2026-09-28-index.md rename to docs/security/2026-09-28-auth-review.md index 138ca379..9cc9372d 100644 --- a/.claude/TODO/security-review-2026-09-28-index.md +++ b/docs/security/2026-09-28-auth-review.md @@ -1,16 +1,16 @@ -# Auth Security Review — 2026-09-28 (index) +# Auth Security Review — 2026-09-28 -**Status: closed 2026-09-29.** Every finding is fixed, closed by decision, or recorded as accepted/known in the docs listed below. This file is the review record; nothing here is open. +**Status: closed 2026-09-29.** Every finding is fixed, closed by decision, or recorded as accepted/known in the docs listed below. This file is the review record; nothing here is open. Item names such as `security-auth-test-gaps` are the review's finding IDs; test comments cite them as "review item …". **Scope:** every authentication and authorization path — better-auth 1.7.4 config and internals, the MP OIDC sign-in flow, sessions/cookies/secrets, the `/api/auth` catch-all and `proxy.ts`, server actions and the role gate, the MP client-credentials service account and HTTP client, the auth UI pages, security headers, setup/config/CI, and the security docs. **Method:** six parallel read-only reviewers (OAuth/OIDC, sessions, HTTP boundary, authorization, MP service-account client, client/config). Claims were checked against `node_modules` source and reproduced where possible with mock OIDC/MP servers and stubbed `fetch`. **No Ministry Platform calls were made.** Key claims were spot-checked again when consolidating. **Baseline:** `main` @ `ea2e0ad`. Next 16.3.5, better-auth 1.7.4, better-call 1.4.0. -**Result:** no Critical or High findings. The fixes from the 2026-09-12 and 2026-09-25 rounds (F-UPDATE-USER, F2, F1/F10/F11, F4, F5, F7, F9, F12, F3/F3b) all held against targeted bypass attempts. The biggest remaining risks are that **sign-out doesn't revoke a stateless session**, **the MP service account is over-privileged**, and **several configuration defaults fail open**. +**Result:** no Critical or High findings. The fixes from the 2026-09-12 and 2026-09-25 rounds (F-UPDATE-USER, F2, F1/F10/F11, F4, F5, F7, F9, F12, F3/F3b) all held against targeted bypass attempts. The biggest remaining risks are that **sign-out doesn't revoke a stateless session**, **an over-privileged MP service account would widen the impact of any flaw**, and **several configuration defaults fail open**. ## Medium -None open. The remaining parts of the three partly fixed Medium items live in [`docs/security/Additional_Security_Hardening.md`](../../docs/security/Additional_Security_Hardening.md): §1 sign-out revocation **accepted 2026-09-29**; §2 MP login re-validation and §3 role granularity (read/write split, MP rights) are future options that need a policy decision, not review work — §3's name-matching part is deferred. +None open. The remaining parts of the three partly fixed Medium items live in [`docs/security/Additional_Security_Hardening.md`](Additional_Security_Hardening.md): §1 sign-out revocation **accepted 2026-09-29**; §2 MP login re-validation and §3 role granularity (read/write split, MP rights) are future options that need a policy decision, not review work — §3's name-matching part is deferred. ## Fixed 2026-09-28 (branch `fix/security-review-2026-09-28-medium`) @@ -19,14 +19,14 @@ None open. The remaining parts of the three partly fixed Medium items live in [` - `security-gitignore-env-files` — `.env*` + `!.env.example`, `.vercel`; pre-commit refuses staged env files - `security-security-md-reporting-channel` — private vulnerability reporting, secret scanning, push protection and Dependabot security updates enabled; placeholder email removed - `security-sign-in-social-callbackurl-size-dos` — 4 KB body cap (declared and streamed); `callbackURL` string ≤ 2048 -- `security-service-account-over-privileged` — closed as not applicable (dev environment) +- `security-service-account-over-privileged` — closed as not applicable to the template: each deployment scopes its own Client User (see README § Data-access client) - `security-rate-limit-ip-resolution` — `AUTH_IP_ADDRESS_HEADERS` / `AUTH_TRUSTED_PROXIES` → `advanced.ipAddress`, validated at startup; per-host guidance in `.env.example` (429 UI handling stays with `security-signin-page-swallows-errors`) - `security-mp-logout-missing-id-token-hint` — `client_id` + `id_token_hint` sent; one shared `auth` per process (2026-09-29) so the hint is actually available; verified live against MP (no prompt). On another serverless instance MP prompts once — documented - `security-committed-claude-settings-sed` — `.claude/settings.local.json` untracked + gitignored; shared entries (no `sed:*`) moved to `.claude/settings.json` ## Fixed 2026-09-29 (branch `dev/security-review-2026-09-28`, wave 1 — child branches `security/a`…`security/g`) -TODO files deleted; their doc edits landed in the docs sweep below. +The per-finding working notes were deleted once fixed; their doc edits landed in the docs sweep below. - **MP HTTP client (a):** timeouts + `redirect: "error"` on every MP fetch; single-flight token refresh, token-response validation, 401 → refresh + one retry; `buildUrl` path guard; name-only error logging — `mp-fetch-timeouts-and-redirects`, `mp-token-cache-hardening` - **Provider services (b):** identifier/ID/GUID validation + per-segment encoding in File/Table/Procedure services; trusted sender for communications; procedure allowlist; `$ignorePermissions` dropped; codegen escaping — `file-service-path-traversal`, `info-provider-logging-and-codegen` diff --git a/package-lock.json b/package-lock.json index ceba7352..847b7ce6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,7 @@ "": { "name": "mp-next", "version": "0.2.0", + "license": "MIT", "dependencies": { "@heroicons/react": "^2.2.0", "@hookform/resolvers": "^5.2.2", diff --git a/package.json b/package.json index 2ba638ae..2d8bd327 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,15 @@ "name": "mp-next", "version": "0.2.0", "private": true, + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/MinistryPlatform-Community/MPNext.git" + }, + "homepage": "https://github.com/MinistryPlatform-Community/MPNext#readme", + "bugs": { + "url": "https://github.com/MinistryPlatform-Community/MPNext/issues" + }, "scripts": { "dev": "next dev", "build": "next build", diff --git a/scripts/setup.ts b/scripts/setup.ts index 72e7393f..3056110a 100644 --- a/scripts/setup.ts +++ b/scripts/setup.ts @@ -1095,9 +1095,10 @@ async function runInteractiveSetup(options: SetupOptions): Promise { const currentBaseUrl = currentEnv.get('MINISTRY_PLATFORM_BASE_URL') || ''; let currentHost = ''; if (currentBaseUrl) { - // Extract host from existing URL (e.g., https://mpi.ministryplatform.com/ministryplatformapi -> mpi.ministryplatform.com) + // Extract host from existing URL (e.g., https://your-instance.ministryplatform.com/ministryplatformapi -> your-instance.ministryplatform.com) const match = currentBaseUrl.match(/https?:\/\/([^/]+)/); - if (match) { + // The .env.example placeholder is not a real host; don't offer it as the default. + if (match && match[1] !== 'your-instance.ministryplatform.com') { currentHost = match[1]; } } @@ -1106,7 +1107,7 @@ async function runInteractiveSetup(options: SetupOptions): Promise { console.log(chalk.gray(' The OIDC, API, and File URLs will be derived from your MP host')); const mpHost = await input({ - message: 'Enter your Ministry Platform host (e.g., mpi.ministryplatform.com):', + message: 'Enter your Ministry Platform host (e.g., your-instance.ministryplatform.com):', default: currentHost || undefined, }); diff --git a/src/auth.code-flow.test.ts b/src/auth.code-flow.test.ts index 71c3b8b9..11e6b991 100644 --- a/src/auth.code-flow.test.ts +++ b/src/auth.code-flow.test.ts @@ -7,7 +7,7 @@ import { genericOAuth, type GenericOAuthConfig, type GenericOAuthOptions } from /** * End-to-end tests of `GET /api/auth/callback/ministry-platform` through the - * REAL `auth.handler` (TODO security-auth-test-gaps #2, #4, #5): + * REAL `auth.handler` (review item security-auth-test-gaps #2, #4, #5): * - state validation: a missing, mismatched, cookie-less, tampered or expired * state is refused before the code is ever redeemed; * - the code exchange (no PKCE verifier, since MP does not support PKCE); diff --git a/src/auth.discovery-rebuild.test.ts b/src/auth.discovery-rebuild.test.ts index f9d05460..04e43692 100644 --- a/src/auth.discovery-rebuild.test.ts +++ b/src/auth.discovery-rebuild.test.ts @@ -5,7 +5,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { toNextJsHandler } from 'better-auth/next-js'; /** - * TODO security-discovery-failure-no-retry: genericOAuth fetches MP's OIDC + * Review item security-discovery-failure-no-retry: genericOAuth fetches MP's OIDC * discovery document once per auth instance, with no retry, so one failed * fetch at cold start used to leave `/sign-in/social` answering * `404 PROVIDER_NOT_FOUND` until the process restarted. `selfHealingAuth` in diff --git a/src/auth.ip-address.test.ts b/src/auth.ip-address.test.ts index ff6e2180..0707ea54 100644 --- a/src/auth.ip-address.test.ts +++ b/src/auth.ip-address.test.ts @@ -4,7 +4,7 @@ import { betterAuth } from 'better-auth'; import { getIP, getIPFromHeader } from '@better-auth/core/utils/ip'; /** - * Guard for TODO security-rate-limit-ip-resolution: which client IP + * Guard for review item security-rate-limit-ip-resolution: which client IP * better-auth's rate limiter keys on is host-specific, so it is configured by * `AUTH_IP_ADDRESS_HEADERS` / `AUTH_TRUSTED_PROXIES` (see * `parseIpAddressOptions` in src/lib/auth.ts). Without it, clients whose IP diff --git a/src/auth.oidc-hardening.test.ts b/src/auth.oidc-hardening.test.ts index 2f77c521..8359a736 100644 --- a/src/auth.oidc-hardening.test.ts +++ b/src/auth.oidc-hardening.test.ts @@ -5,17 +5,17 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; /** * End-to-end guards for the 2026-09-28 OIDC hardening in src/lib/auth.ts: - * - `requireIdTokenVerification: true` (TODO + * - `requireIdTokenVerification: true` (review item * security-require-id-token-verification): a discovery document missing * `jwks_uri` or `issuer` must take the provider down, not leave it live with * id_token verification silently off. - * - id_token `exp` / `azp` checks in `getUserInfo` (TODO + * - id_token `exp` / `azp` checks in `getUserInfo` (review item * security-id-token-claim-checks-weak), through the real code flow with * genuinely signed tokens that better-auth's own verifier ACCEPTS. - * - `cookieCache.strategy: "jwe"` (TODO + * - `cookieCache.strategy: "jwe"` (review item * security-session-cookie-readable-jwt-strategy): `session_data` must not be * readable. - * - `/get-session` withholds `token`, `ipAddress`, `userAgent` (TODO + * - `/get-session` withholds `token`, `ipAddress`, `userAgent` (review item * security-client-data-overexposure). * * Each test re-imports `@/lib/auth` (Vitest builds a fresh instance per diff --git a/src/auth.origin-check.test.ts b/src/auth.origin-check.test.ts index 687fd32e..2b8af782 100644 --- a/src/auth.origin-check.test.ts +++ b/src/auth.origin-check.test.ts @@ -3,7 +3,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { betterAuth } from 'better-auth'; /** - * Server-side half of F3/F3b (TODO security-auth-test-gaps #1): better-auth's + * Server-side half of F3/F3b (review item security-auth-test-gaps #1): better-auth's * Origin / callbackURL check must be ON for the real `auth` instance. * * Vitest sets `NODE_ENV=test`, and better-auth derives diff --git a/src/auth.rate-limit.test.ts b/src/auth.rate-limit.test.ts index d0a7d5a1..e854aee4 100644 --- a/src/auth.rate-limit.test.ts +++ b/src/auth.rate-limit.test.ts @@ -3,7 +3,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { betterAuth } from 'better-auth'; /** - * Rate-limit IP resolution through the REAL auth options (TODO + * Rate-limit IP resolution through the REAL auth options (review item * security-auth-test-gaps #5). src/auth.ip-address.test.ts covers * `parseIpAddressOptions` and a bare better-auth instance; this suite proves * the app's own config wires `AUTH_IP_ADDRESS_HEADERS` / `AUTH_TRUSTED_PROXIES` diff --git a/src/auth.secret-guard.test.ts b/src/auth.secret-guard.test.ts index 7f41fb7d..0d6c426f 100644 --- a/src/auth.secret-guard.test.ts +++ b/src/auth.secret-guard.test.ts @@ -4,7 +4,7 @@ import path from 'node:path'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; /** - * Guard for `assertAuthEnvironment` in src/lib/auth.ts (TODO + * Guard for `assertAuthEnvironment` in src/lib/auth.ts (review item * security-auth-secret-fallback-and-test-flag). * * With no secret, better-auth 1.7.4 signs sessions with its PUBLIC default @@ -132,7 +132,7 @@ describe('assertAuthEnvironment at module load', () => { }); /** - * TODO security-auth-url-env-not-validated: the two auth-critical URLs are + * Review item security-auth-url-env-not-validated: the two auth-critical URLs are * validated at module load (src/lib/env.ts has the per-value cases). Unlike the * secret guard these run under Vitest too, so no exemption is switched off. */ diff --git a/src/auth.session-config.test.ts b/src/auth.session-config.test.ts index 2811b45c..aabe0671 100644 --- a/src/auth.session-config.test.ts +++ b/src/auth.session-config.test.ts @@ -6,7 +6,7 @@ import { betterAuth } from 'better-auth'; import { symmetricDecodeJWT } from 'better-auth/crypto'; /** - * Pins the session cookies the real `auth` instance actually sets (TODO + * Pins the session cookies the real `auth` instance actually sets (review item * security-auth-test-gaps #3), so a better-auth upgrade that changes stateless * defaults, or a config edit, cannot ship unnoticed: * - cookie names and prefix (`better-auth.*`, `__Secure-` over https); diff --git a/src/auth.session-lifetime.test.ts b/src/auth.session-lifetime.test.ts index 241c6c05..0359601c 100644 --- a/src/auth.session-lifetime.test.ts +++ b/src/auth.session-lifetime.test.ts @@ -6,7 +6,7 @@ import { betterAuth } from 'better-auth'; /** * Clock-walk guard for the session lifetime settings in src/lib/auth.ts - * (TODOs security-stateless-session-not-revocable and + * (review items security-stateless-session-not-revocable and * security-session-no-absolute-lifetime-or-mp-revalidation). * * Signs in through the REAL `auth` instance with the full authorization-code diff --git a/src/auth.test.ts b/src/auth.test.ts index 819fb72e..9f31120b 100644 --- a/src/auth.test.ts +++ b/src/auth.test.ts @@ -123,7 +123,7 @@ describe('Auth - enrichSessionUser', () => { }); /** - * TODO security-client-data-overexposure: `/get-session` must not hand page + * Review item security-client-data-overexposure: `/get-session` must not hand page * JS the raw session token (a bearer credential if a `bearer` plugin is ever * added) or the recorded IP / user agent. */ @@ -274,7 +274,7 @@ describe('Auth - enrichSessionUser', () => { }); /** - * TODO security-resolve-mp-user-id-logs-guid-and-no-negative-cache: the + * Review item security-resolve-mp-user-id-logs-guid-and-no-negative-cache: the * failure log is a structured event with no GUID, and never the error * message (an MP client error can carry the `$filter`, which holds the GUID). */ @@ -314,7 +314,7 @@ describe('Auth - enrichSessionUser', () => { }); /** - * Negative cache for `resolveMpUserId` (TODO + * Negative cache for `resolveMpUserId` (review item * security-resolve-mp-user-id-logs-guid-and-no-negative-cache). customSession * runs on every `/get-session`, so an uncached failure cost one MP query (and * one log line) per request. Fake `Date` only: the module-level cache compares @@ -611,7 +611,7 @@ describe('Auth - OAuth Configuration', () => { }); /** - * TODO security-get-user-info-robustness: `getUserInfo`'s contract is + * Review item security-get-user-info-robustness: `getUserInfo`'s contract is * "return null, never throw" (a throw is not caught by better-auth's callback * route), so every way the userinfo request can go wrong must come back as * null plus a structured log with no body and no token. @@ -719,7 +719,7 @@ describe('Auth - OAuth Configuration', () => { }); /** - * TODO security-id-token-claim-checks-weak: jose checks `exp` only when it + * Review item security-id-token-claim-checks-weak: jose checks `exp` only when it * is present, and nothing checks `azp` for a multi-audience token, so * `getUserInfo` does both (before spending a userinfo call). */ diff --git a/src/auth.user-id-cache.test.ts b/src/auth.user-id-cache.test.ts index e14138cc..665aeb76 100644 --- a/src/auth.user-id-cache.test.ts +++ b/src/auth.user-id-cache.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; /** - * TTL on the User_GUID → MP User_ID cache in src/lib/auth.ts (TODO + * TTL on the User_GUID → MP User_ID cache in src/lib/auth.ts (review item * security-session-no-absolute-lifetime-or-mp-revalidation). Before the TTL * the cache never expired, so `dp_Users` was never re-read for a user after * their first request on a process. diff --git a/src/auth.user-oauth-tokens.test.ts b/src/auth.user-oauth-tokens.test.ts index 317305cf..2cf4defb 100644 --- a/src/auth.user-oauth-tokens.test.ts +++ b/src/auth.user-oauth-tokens.test.ts @@ -4,7 +4,7 @@ import { describe, it, expect, vi } from 'vitest'; /** - * Guard for TODO security-unused-user-oauth-tokens-stored: the user's own MP + * Guard for review item security-unused-user-oauth-tokens-stored: the user's own MP * OAuth tokens are never used (all MP data access is the client-credentials * service account), so the app must not ask for a refresh token, must not put * the tokens in the browser (`account_data` cookie), and must not keep the diff --git a/src/components/contact-logs/actions.test.ts b/src/components/contact-logs/actions.test.ts index 3cc2de77..b323fd60 100644 --- a/src/components/contact-logs/actions.test.ts +++ b/src/components/contact-logs/actions.test.ts @@ -487,7 +487,7 @@ describe('contact-logs actions', () => { }); }); - // Regression guard for `.claude/TODO/mp-filter-injection-numeric-ids.md`. + // Regression guard for the numeric-ID `$filter` injection fix. // // These actions compile to POST endpoints, so a caller controls the payload's // shape as well as its values — a string reaches a `number` parameter. The old diff --git a/src/components/contact-logs/contact-logs.test.tsx b/src/components/contact-logs/contact-logs.test.tsx index ca270cdc..e19dbc22 100644 --- a/src/components/contact-logs/contact-logs.test.tsx +++ b/src/components/contact-logs/contact-logs.test.tsx @@ -15,8 +15,7 @@ import type { ContactLogDisplay } from "@/lib/dto"; * close the dialog or signal a refresh as if * it had succeeded * - * See `.claude/TODO/contact-logs-component-untested.md` (the original gap) and - * `.claude/references/testing.md`. + * See `.claude/references/testing.md`. */ const { diff --git a/src/components/contact-lookup-details/actions.test.ts b/src/components/contact-lookup-details/actions.test.ts index 9f348dea..1b8c749a 100644 --- a/src/components/contact-lookup-details/actions.test.ts +++ b/src/components/contact-lookup-details/actions.test.ts @@ -232,7 +232,7 @@ describe('contact-lookup-details actions', () => { }); }); - // Regression guard for `.claude/TODO/n-plus-1-contact-log-types-lookup.md`. + // Regression guard for the N+1 contact-log-types lookup fix. // `getContactLogTypes()` used to be called inside the `logs.map()` callback, so // the same small lookup table was refetched once per typed log. The call-count // assertions below are the whole point — the pre-existing tests mocked the call @@ -318,7 +318,7 @@ describe('contact-lookup-details actions', () => { }); }); - // Regression guard for `.claude/TODO/mp-filter-injection-numeric-ids.md`. This + // Regression guard for the numeric-ID `$filter` injection fix. This // is the second reachable entry point into // `ContactLogService.getContactLogsByContactId` and carried the same // ineffective `!contactId || contactId <= 0` guard. diff --git a/src/services/contactLogService.test.ts b/src/services/contactLogService.test.ts index c05e25a9..50bc100c 100644 --- a/src/services/contactLogService.test.ts +++ b/src/services/contactLogService.test.ts @@ -710,7 +710,7 @@ describe('ContactLogService', () => { }); }); - // Regression guard for `.claude/TODO/mp-filter-injection-numeric-ids.md`. + // Regression guard for the numeric-ID `$filter` injection fix. // // Every method here declares `number`, but that annotation is erased at // runtime and server actions compile to POST endpoints whose payload shape the