Skip to content

API sweep + cron pass: billing keys, Stripe webhook, AI not-configured, child savings, unsent email reported sent - #619

Merged
NewWorldVenture merged 7 commits into
mainfrom
claude/bubaly-repo-connect-45d8k6
Sep 27, 2026
Merged

NewWorldVenture merged 7 commits into
mainfrom
claude/bubaly-repo-connect-45d8k6

Conversation

@NewWorldVenture

Copy link
Copy Markdown
Owner

What this is

This covers Register B's API rows, which nobody had claimed.

scripts/api-audit finds every method of every app/api/**/route.ts on disk: 155 route methods in 147 files. It calls each one against a local production build and a local Supabase with all migrations through 0458, as four callers:

  • signed out
  • a child member
  • a non-admin parent
  • a super-admin parent

Each call sends the input a careless or hostile client would: no body, {}, and a body that isn't JSON. That is 1,008 calls in total. The summariser sorts each response into FAIL, BLOCKED (an honest 503 saying "not set up"), REVIEW or OK.

  • First sweep: 24 FAIL.
  • After these fixes: 0 FAIL, 30 BLOCKED (no AI, Stripe, push, GIF or Twilio keys in this build), 11 REVIEW (all read; each is public by design or deliberately reads an optional body as empty) and 114 OK.
  • No admin route answered a child or a non-admin parent, no cron route ran without its secret, and no response leaked internal detail.

Fixes (each has a test that fails against the old code)

ID Sev What was wrong Fix
API-SWEEP-01 Med With no Google credentials, "Connect Google" on the calendar threw and showed a blank 500. Returns the parent to /dashboard/calendar?gcal=not_configured with a translated toast.
API-SWEEP-02 High Super Admin → Stripe Setup saves the secret key, but only checkout used it. Portal, cancel and change-plan read STRIPE_SECRET_KEY only. With no key anywhere, all four answered 500, and checkout built its Stripe client before reading the body. All four resolve the key the same way: Setup first, then the environment variable. With no key they return 503 "billing isn't set up". Checkout now reads the body first.
API-SWEEP-03 High The Stripe webhook read STRIPE_WEBHOOK_SECRET only and called getStripe() just to verify the signature. With the keys held only in Stripe Setup, every real event was refused as "signature invalid". Uses the Setup secret first, then the environment variable, and Stripe's keyless static verifier. Tested with real signatures.
API-SWEEP-04 Med With no AI key, 13 AI routes returned 500 or 502 "temporarily unavailable". health/coach checked only the environment, so it missed a key saved in Admin → AI Engine. Each route calls isAIConfigured() before the provider. A guard test enforces this for every provider caller except three deliberate fallbacks.
API-SWEEP-05 High /api/ai/savings answered a child with advice built from the family's transactions, budgets, bills and subscriptions. 403 for anyone who isn't a manager, before any read. This matches the finance service and the privacy export.

Recorded as open, not fixed here (API-SWEEP-06, High):

  • At the database level, transactions, budgets, bills and subscriptions_tracked are readable by any family member, children included, and subscriptions_tracked is writable by any member.
  • The app treats finances as adults-only in three places.
  • Many readers (home, wallet hub, voting, briefing) depend on these tables, so tightening this needs an owner decision and a migration.

finalaudit.md

  • 142 API rows move from NOT STARTED to 🔄 IN PROGRESS, not PASS. Each row records the sweep's per-caller statuses, its verdict and any fix. What remains is a valid request, its side effects and the response shape.
  • The 11 rows already in progress keep their text, with the sweep result appended.
  • Two routes had no row, and I added them.
  • Register B now reads: 14,228 items, 13,688 NOT STARTED, 525 IN PROGRESS.
  • PRODUCTION READY: NO.

Checks

  • tsc clean and lint clean on the changed files; the i18n gate is clean.
  • Full unit suite: 21,484 pass. The only failures are the two Node-24-only tests (node-version-is-pinned, stream-cancellation-runtime); this container runs Node 22.
  • Re-sweeping the fixed build gives 0 FAIL.

🤖 Generated with Claude Code

https://claude.ai/code/session_01776xJyhVe8xJQvTGeNfT9T


Generated by Claude Code

scripts/api-audit calls every method of every app/api route (155 in 147
files) against a local production build, as signed out, a child, a parent
and a super admin, with no body, an empty JSON object and a body that is
not JSON. First sweep: 24 FAIL. After these fixes: 0 FAIL, 30 BLOCKED
(an honest 503 for a provider this build has no key for), 11 reviewed.

- Google Calendar connect: with no Google credentials the link threw a
  blank 500; it now returns the parent to the calendar with a toast.
- Billing: portal, cancel and change-plan read STRIPE_SECRET_KEY alone,
  ignoring the key Super Admin -> Stripe Setup saves and checkout uses.
  All four now resolve it the same way and answer 503 "billing isn't set
  up" with no key; checkout reads the body before building a client.
- Stripe webhook: read STRIPE_WEBHOOK_SECRET alone and built a client
  with getStripe() just to verify, so with the key in Stripe Setup every
  real event was answered "signature invalid". It now uses the Setup
  secret first and Stripe's keyless static verifier.
- AI routes: nine let the provider's "not configured" throw become a
  500, four a 502 "temporarily unavailable"; each now asks
  isAIConfigured() first. health/coach checked only the environment and
  missed a key saved in Admin -> AI Engine.
- /api/ai/savings answered a child with advice built from the family's
  transactions, budgets, bills and subscriptions, which the finance
  service and the privacy export keep for adults. Now 403.

Each fix has a test that fails against the old code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01776xJyhVe8xJQvTGeNfT9T
142 API rows move from NOT STARTED to IN PROGRESS with the sweep's
per-caller statuses, verdict and fix; the eleven already in progress get
the result appended; two routes the inventory missed are added. Register
B: 14,228 items, 13,688 NOT STARTED, 525 IN PROGRESS. Records the open
High finding API-SWEEP-06: finance tables are readable by a child at the
database while the app treats them as adults-only. PRODUCTION READY: NO.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01776xJyhVe8xJQvTGeNfT9T
@vercel

vercel Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
bubaly Ignored Ignored Sep 27, 2026 11:09pm UTC

Request Review

Running every scheduled job on a local build, /api/cron/weekly-digest
reported {"sent":4} with no mail provider. sendEmail/sendReactEmail
answer { ok: true, skipped: true } without RESEND_API_KEY, a contract
the contact form relies on, so each caller that read only `ok` turned
"nothing sent" into "sent": the invite, welcome, referral and admin
re-send flows told the person their email had gone, and the weekly
digest and chore reminders counted skips as sends.

Each caller now reads `skipped`: the four person-facing flows answer
their existing "couldn't send" message (the referral rolls back its
record), and the crons count a skip as a skip. A guard holds every
caller that reads the result to read `skipped` too; a behavioural test
drives the invite route through the real lib/email.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01776xJyhVe8xJQvTGeNfT9T
Every job in vercel.json run on a local build with and without its
secret: 27/27 refuse without it (401) and run with it (200, zero
failures). 25 JOB rows move to IN PROGRESS, one is appended, and the
unregistered /api/cron/library-feeds is added. Register B: 14,229 items,
13,663 NOT STARTED, 551 IN PROGRESS. PRODUCTION READY: NO.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01776xJyhVe8xJQvTGeNfT9T
@NewWorldVenture NewWorldVenture changed the title API sweep: every app/api route as four callers; billing keys, Stripe webhook, AI not-configured, child savings API sweep + cron pass: billing keys, Stripe webhook, AI not-configured, child savings, unsent email reported sent Sep 27, 2026
@NewWorldVenture
NewWorldVenture marked this pull request as ready for review September 27, 2026 22:40
…t keeps both top entries and recounts Register B

Both sides added a dated entry at the top (the MAIN reconciliation, the
API sweep) and both edited the Session B counts. The rows merged
cleanly, so the header is recounted from them: 14,229 = 13,663 NOT
STARTED + 535 IN PROGRESS + 21 FIXED + PASS + 9 BLOCKED + 1 FAIL.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DXw2nu25BjyRfA6Fg3YiMS

Copy link
Copy Markdown
Owner Author

I pushed a merge of main (877d9a5e: #620, #622) into this branch as 558ff300. My #622 had edited the same finalaudit.md header, which is why this PR showed a conflict. It is a merge commit, so no history was rewritten, and nothing outside finalaudit.md needed resolving.

How the conflict was resolved:

Checks on the merged tree:

If you push again, please pull first.


Generated by Claude Code

…unted from rows

#623 moved sixteen public-site MAIN rows to FIXED + PASS. The rows merged
cleanly; the header is recounted: 14,229 = 13,663 NOT STARTED + 519 IN
PROGRESS + 37 FIXED + PASS + 9 BLOCKED + 1 FAIL (0.26%).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DXw2nu25BjyRfA6Fg3YiMS
@NewWorldVenture
NewWorldVenture merged commit 3ff8150 into main Sep 27, 2026
7 checks passed
NewWorldVenture pushed a commit that referenced this pull request Sep 27, 2026
…folded

#619 and this branch fixed the same routes from two sweeps. Main's
versions are kept where they overlap, because they cover this branch's
cases and more: billing reads the key saved in Stripe Setup (half of
PAY-002) and answers "billing isn't set up" with a 503; the Google link
answers gcal=not_configured and now also logs the missing variable, as
this branch wanted. This branch's tests hold the merged routes to the
same statuses, with the merged messages.

finalaudit: rows both sweeps wrote keep main's cells with this branch's
evidence in Notes; the two routes both registered keep main's IDs, with
this branch's (API-B5A3C675E789, API-4E51E2B204A7) folded in as aliases.
Recounted: 14,229 = 13,663 NOT STARTED + 520 IN PROGRESS + 37 FIXED +
PASS + 9 BLOCKED + 0 FAIL. A note under PAY-002 records what #619 fixed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01776xJyhVe8xJQvTGeNfT9T
NewWorldVenture pushed a commit that referenced this pull request Sep 28, 2026
#619's API-SWEEP-04 checks isAIConfigured() before the provider call in
the relationship and weekly-briefing routes; B11's P-25 answers a failure
during the call with 503. Both are kept, and the two route tests mock
both helpers. P-25's list of eighteen further routes is withdrawn in
finalaudit.md, superseded by API-SWEEP-04.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRY21ZKsFrfB3qtoP972A4
NewWorldVenture pushed a commit that referenced this pull request Sep 28, 2026
…24 to P-26 are B11's, so B14's findings are P-27 to P-31

B11 (#624) recorded its own P-24, P-25 and P-26 while B14 ran. B14's five
findings are renumbered P-27 to P-31 in its section, its batch row and
the three MAIN rows that cite them; the code and tests carry no P number.
Both sections are kept, B11 first. Register B recounted from its rows:
14,229 = 13,663 / 511 / 45 / 9 / 1 (0.32%).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DXw2nu25BjyRfA6Fg3YiMS
NewWorldVenture pushed a commit that referenced this pull request Sep 28, 2026
…fixes kept

#619 ran the same API sweep and cron pass as this branch, in parallel, and
landed first. Where both fixed the same defect (Google connect with no
credentials, billing's Stripe key source, the webhook secret, unsent mail
reported as sent), main's version is kept: every overlapping route is main's
file. This branch's duplicate fixes are dropped, as is its Google test, whose
case main's covers with a better answer (`gcal=not_configured`).

What this branch still adds:
- SEC-001: 0459, its probe, the E2E spec and the CSP change (unchanged).
- API-SWEEP-08: /api/email/invite evaluated its family-wide limit on the
  member's session, which rate_limit_hit (0179) refuses, so every invite
  answered 429. The limit now runs on the service client, and main's invite
  test gets the matching mock export.
- Driven tests that pass against main's fixes, kept as extra coverage: the
  no-key answer for all four billing routes, invite/welcome/referral/admin
  re-send, the no-provider digest counts, and the rate-limit key guard.

finalaudit.md: main's API/JOB rows and sweep section are kept, and
API-SWEEP-08 is added to its table. This branch's duplicate SWEEP-001..003 and
PAY-002 findings are removed (main's tally stands: FIXED 122). The top entry
records the duplication, SEC-001, the page-board reconciliation and the Next 16
re-crawl. Register B, recounted: 14,229 = 13,180 + 1,003 + 37 + 9 + 0 FAIL.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
NewWorldVenture pushed a commit that referenced this pull request Sep 28, 2026
…ions agree

session_01776xJyhVe8xJQvTGeNfT9T merged main into this PR branch at 78eac27
while this session merged main into its later head. Both kept main's route
fixes. Taken from theirs: the Google connect route's log line naming the
missing variable and this branch's Google test, which asserts it; and the
API rows, which keep main's cells with this branch's sweep evidence folded
into Notes, with the two duplicate route rows folded in as aliases. Kept from
this side: the top entry, the headers and everything after 78eac27 (SEC-001,
API-SWEEP-08, the page-board reconciliation, the Next 16 re-crawl).

Notes that cited this branch's retired IDs now name main's: SWEEP-001 is
API-SWEEP-01, 002 is 02, 003 is 07 and 004 is 08. Register B recounted:
14,229 = 13,180 + 1,003 + 37 + 9 + 0 FAIL. The full unit suite passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
NewWorldVenture pushed a commit that referenced this pull request Sep 28, 2026
… already has it

ab77291 merged main at 8f6155a into this branch at ff08933. This side had
merged the same main, plus everything after 78eac27. No code differs. In
finalaudit.md, its only extra lines are this branch's own superseded sweep
entry and older row text, so this side's ledger is kept whole.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KP9rt5rVQ9jDMpZp2xBy3K
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.

2 participants