Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions .claude/commands/audit-deps.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ Know exactly what you may do without asking:

**Allowed without asking**
- Every read-only command (`npm ls`, `npm audit`, `npm outdated`, `npm view`, `git status`, `git diff`).
- `npm install` with **no package argument** (syncs the tree to the lockfile).
- `npm install` with **no package argument** (syncs the tree to the lockfile). If it changes
`package-lock.json`, regenerate it with `npm run deps:relock` and check it with
`npm run deps:verify` — a bare Windows `npm install`/`npm dedupe` must never produce the
committed lockfile (CLAUDE.md § Dependency Rule).
- **`npm update`** with no package argument — applies patch/minor updates inside the existing
ranges. This is the phase 6 workhorse; it never edits `package.json`.
- `npm run deps:relock` / `npm run deps:verify` — required after any `npm update`.
Expand Down Expand Up @@ -77,10 +80,13 @@ reading a single advisory.
2. `node --version` and `npm --version` — note them in the report; some advisories and
peer ranges are engine-dependent.
3. `npm ls --all 2>&1 | grep -iE "invalid|missing|UNMET"` — any hit means the installed
tree does not satisfy `package.json`. Run `npm install`, then re-check.
tree does not satisfy `package.json`. Run `npm install`, then re-check. If that changed
`package-lock.json`, run `npm run deps:relock` then `npm run deps:verify` before going on.
4. Confirm `package.json` and the lockfile agree. Prefer `npm ls` cleanliness plus
`npm install --package-lock-only --dry-run` (non-destructive). Reach for `npm ci` only
if you must, and warn the user first — it **deletes and rebuilds `node_modules`**.
if you must, and warn the user first — it **deletes and rebuilds `node_modules`**. Have
the user stop `next dev` before it runs: a running dev server locks a native `.node`
file, so `npm ci` aborts after the delete and leaves `node_modules` half-installed.
5. Only proceed once the tree matches `package.json`. State the baseline explicitly:
"audited tree = package.json as of \<commit sha\>".

Expand Down Expand Up @@ -208,13 +214,17 @@ recommendation. Then ask.

For each approved major, one at a time:

1. Edit the range in `package.json`, `npm install`. Watch for `ERESOLVE` and for
`invalid:` in `npm ls <pkg>` — either means surrounding tooling hasn't adopted it.
1. Edit the range in `package.json`, `npm install`, then `npm run deps:relock` and
`npm run deps:verify` (the bare install's lockfile is never the one you commit). Watch
for `ERESOLVE` and for `invalid:` in `npm ls <pkg>` — either means surrounding tooling
hasn't adopted it.
2. Apply the code changes the migration guide requires.
3. Run the full phase 7 verification.
4. **On failure, revert cleanly and completely:** restore the `package.json` range,
`git checkout -- package-lock.json`, `npm install`, then confirm `git status` is clean
for those files and the build passes again. Record the failure and its cause in the
for those files (if `npm install` rewrote the restored lockfile, discard that with
`git checkout -- package-lock.json` again rather than committing it — or relock and
verify) and the build passes again. Record the failure and its cause in the
known-issues file with today's date so the next run doesn't retry it blindly.

## Phase 9 — Report, persist, commit
Expand Down
4 changes: 3 additions & 1 deletion .claude/commands/release.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,10 @@ Create a GitHub release with auto-generated release notes from merged pull reque
- Apply any requested changes

6. **Create the release:**
- Run `gh release create <tag> --target main --title "<tag>" --notes "<notes>"`
- Run `gh release create <tag> --target <branch> --title "<tag>" --notes "<notes>"`, where `<branch>` is the `--target` argument (default `main`)
- Append `--draft` if the `--draft` argument was given, and `--prerelease` if `--prerelease` was given
- Use a HEREDOC for the notes body to handle multiline content
- If the release is a draft, the tag is not created until it is published — say so, and skip the tag-sync check in step 7
- Run `git fetch --tags` to sync the new tag locally

7. **Post-creation:**
Expand Down
9 changes: 7 additions & 2 deletions .claude/docs/TestCoverage.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# MPNext — Application & Unit Test Coverage Review

> **Historical snapshot.** This review describes commit `5bc505a` as of 2026-09-12 and is not kept up
> to date. The coverage gates that apply today are the thresholds in `vitest.config.mts`, documented in
> [Testing Reference](../references/testing.md) § Coverage; run `npm run test:coverage` for current figures.
> The `.claude/TODO/` folder it mentions no longer exists.

**Date:** 2026-09-12
**Reviewed commit:** `5bc505a` (branch `docs/release-readiness-refresh`)
**Scope:** whole application — `src/**` excluding generated MP models, codegen scripts and `src/components/ui/`
Expand Down Expand Up @@ -30,7 +35,7 @@ Three things matter more than the headline number:
|---|---|
| **Measurement was inflated ~2.2×.** With no explicit `coverage.include`, every file no test imported dropped out of the denominator. | **Fixed.** `vitest.config.mts` now sets an explicit `include`, plus per-glob `thresholds` that fail the run on regression. |
| **`testing.md` claimed 95.39% coverage** — not reproducible under any configuration. | **Fixed.** Rewritten against measured numbers, with the new mock patterns documented. |
| **Coverage was pointed away from the risk.** Two `'use server'` actions have no session check at all, and both sat at 100% line coverage. | **Documented, then fixed.** Every finding in §5 is closed — §5.1 (filter injection), §5.2/§5.3 (missing auth), §5.4/§5.5 (missing authz, duplicated User_ID lookup), §5.6 (N+1 lookup), §5.7 (token lifetime), §5.8 (tests asserting against a copy of the logic). No test-derived item remains in `.claude/TODO/`. |
| **Coverage was pointed away from the risk.** Two `'use server'` actions have no session check at all, and both sat at 100% line coverage. | **Documented, then fixed.** Every finding in §5 is closed — §5.1 (filter injection), §5.2/§5.3 (missing auth), §5.4/§5.5 (missing authz, duplicated User_ID lookup), §5.6 (N+1 lookup), §5.7 (token lifetime), §5.8 (tests asserting against a copy of the logic). No test-derived item remained in `.claude/TODO/` (since removed). |

The shape of the original problem is worth restating, because the new number does not make it go
away: **high coverage is not evidence of correctness.** The filter-injection path in §5.1 lived in a
Expand Down Expand Up @@ -176,7 +181,7 @@ safeguards), `contact-logs.tsx:387` and `user-menu.tsx:35`. None is worth the co

**This is the most important section.** Each item below was fully covered by passing tests and was
still wrong. The original coverage work **documented rather than fixed** them — one file per issue in
`.claude/TODO/` — and the fixed items have since been closed out by follow-up work; each carries a
`.claude/TODO/` (a folder that has since been removed) — and the fixed items have since been closed out by follow-up work; each carries a
regression test that would have caught the defect.

### 5.1 Numeric IDs are interpolated into MP filters unsanitized ✅ FIXED
Expand Down
2 changes: 1 addition & 1 deletion .claude/playbooks/port-downstream-hardening.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ grep -rn "console\.log\|console\.debug\|console\.info" src/ --include="*.ts" --i
grep -rn "callbackUrl" src/ | grep -i "location.href\|sanitize"
# F3b: the 2026-09-12 sanitizer only checked a leading `//` and `/\` — tab/CR/LF bypass it
grep -rqF 'startsWith("/\\")' src/ && echo "✗ F3b (weak leading-/\\ check)" || echo "✓ no weak check"
grep -rqF '\u001f' src/components/sign-in/ && echo "✓ control chars refused" || echo "✗ F3b (no control-char rule)"
grep -rqF '\u001f' src/components/sign-in/ src/app/signin/ 2>/dev/null && echo "✓ control chars refused" || echo "✗ F3b (no control-char rule)"

# F12: is ID-token sign-in refused? (live whenever discoveryUrl is set on better-auth >= 1.7)
grep -q "discoveryUrl" src/lib/auth.ts && ! grep -q "ID_TOKEN_SIGN_IN_DISABLED" src/lib/auth.ts \
Expand Down
8 changes: 8 additions & 0 deletions .claude/playbooks/port-mp-write-attribution.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Playbook: Port Ministry Platform Write Attribution Into This Repo

> **Superseded in part — read this first.**
>
> - **Anonymous writes are now refused upstream.** MPNext resolves the acting `User_ID` inside `AuthorizationService.requireSecurityRole` (`src/services/authorizationService.ts`, `hasSecurityRole`): writes go through `SessionContextService.getActingUserIdForWrite`, which still logs `mp.write.non_user`, but a `null` result is then refused as `no_mp_user`. The gate returns the `User_ID`, and services pass that as `$userId`. `CLAUDE.md` (Key Development Practice 10) requires this role gate on every service method that touches MP data. Outcome 4 below ("anonymous / unresolved writes do not throw") no longer describes upstream.
> - **If this fork has ported the security playbooks** ([`port-downstream-hardening.md`](port-downstream-hardening.md), [`port-security-review-2026-09-28.md`](port-security-review-2026-09-28.md)) or the [Downstream Hardening Playbook](../../docs/security/downstream-hardening-playbook.md), follow those where they disagree with this one. The session-side resolver and `SessionContextService` below are still the basis they build on.
> - **`.claude/references/ministryplatform.writeattribution.md` does not exist upstream.** The steps below that create and cite it are optional and fork-local; upstream documents attribution in `CLAUDE.md` and `.claude/references/auth.md`.
>
> The body below is kept as written for forks porting the original change.

You are Claude Code running in a repo that integrates with **Ministry Platform** (MP). Another team has closed a class of audit-trail gaps where MP write APIs were called without `$userId`, so every change recorded against MP's audit log was attributed to the OAuth integration account rather than the user who performed it. This playbook ports that fix into the repo you're in.

**Outcome you're driving toward**
Expand Down
2 changes: 1 addition & 1 deletion .claude/playbooks/port-security-review-2026-09-28.md
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ The service account holds `dataplatform/scopes/all`. Everything this client send
3. **Stored procedures are deny-all.** `ALLOWED_PROCEDURES = []` is built in. `new MPHelper({ allowedProcedures: ['api_MyChurch_Get_Stats'] })` enables names **per instance**, and each name is validated at construction. The service that calls a procedure builds its own helper with a fixed module-level list, so the name is reviewed next to its caller. **Never build that list from request input.** Before you merge, list every procedure this fork calls and ask the user to confirm each one.
4. **Trusted sender.** `createCommunication(content, sender: { authorUserId, fromContactId }, attachments?)` and `sendMessage(content, sender: { fromAddress }, attachments?)`. Author and From fields on the content object are ignored. The caller must be a service method that has already run `requireSecurityRole`, and it builds `sender` from the `User_ID` the gate returned plus a server-side lookup. **Never from form fields, action arguments or query strings.** The service account can send as anyone, so this is the only thing stopping a caller from impersonating the senior pastor. Also refuse CR/LF (C0 controls) in subjects and display names, which would be header injection.
5. **Remove `$ignorePermissions`** from the helper, provider and types. Requests already run as the admin-level service account, so the flag could only widen what they see. Where a caller-shaped params object is forwarded (upstream: `DomainService.getGlobalFilters`), forward an explicit allowlist (only `$userId`), so a smuggled `$ignorePermissions` is dropped even when an untyped caller sets it.
6. **Codegen escaping** (`scripts/generate-types.ts`). Emit MP metadata (table and column names, descriptions) through `JSON.stringify` so that `"`, `\` and control characters come out as a valid TS string literal. Hostile or odd metadata must not be able to inject code into generated models.
6. **Codegen escaping** (`src/lib/providers/ministry-platform/scripts/generate-types.ts`). Emit MP metadata (table and column names, descriptions) through `JSON.stringify` so that `"`, `\` and control characters come out as a valid TS string literal. Hostile or odd metadata must not be able to inject code into generated models.
7. **Log `errorName(error)` only** everywhere in these services.

**Tests.** Path traversal attempts (`../procs/x`, `%2e%2e`, `a/b`, arrays, objects) throw before any fetch. A non-allowlisted procedure throws. A sender missing or on the content object is refused or ignored. Upstream's `helper.wiring.test.ts` checks that `MPHelper` actually forwards `allowedProcedures` and `sender` to the provider.
Expand Down
Loading
Loading