Commit Graph
120 Commits
Author SHA1 Message Date
Moheeb Zara bcbe86424d docs: HANDOFF deploy state for the audit follow-up (run 26141570221) 2026-05-19 21:37:42 -07:00
Moheeb Zara f2534323da security: enforce https on dues_payment_methods.url at the data layer
The url renders to members as a clickable href. https was enforced only
in the admin Zod action, but RLS lets an admin/board write the row
directly via PostgREST, bypassing Zod, with no DB constraint -- a
malicious admin could store a javascript: URL and XSS members who click.

- Migration 050: CHECK (url ~* '^https://') so the DB is the source of
  truth; integration-tested that the service path cannot store
  javascript:/http:.
- Member-read action filters with isSafeDuesUrl (defense-in-depth).
2026-05-19 21:35:49 -07:00
Moheeb Zara 5ce6b29d4a test: integration coverage for dues_payment_methods RLS and dispatcher skip
- dues_payment_methods RLS: member read, admin-only write, cross-space
  denial, 046 unverified-admin status gate (4 cases).
- notification dispatcher: a muted category becomes 'skipped' while
  billing always sends; no pref row sends (default on). First real
  exercise of the skip path (cron is gated off in prod).
- Fix integration/db.ts rowsAsUser: the SELECT must be the last psql -c
  statement or its output is swallowed (it was never used before).
- Dues card: hide "No active dues subscription" for spaces without
  Stripe, where it read as confusing.
2026-05-19 20:44:08 -07:00
Moheeb Zara 897ff99726 docs: HANDOFF deploy state for pass-57/58 (run 26139360768, ab8c22a) 2026-05-19 20:28:48 -07:00
Moheeb Zara ab8c22ae1d Add notification preferences and alternate dues payment options
Notification preferences (migration 048):
- Per-member opt-out of muteable categories (bookings, classes, forms,
  admin alerts); billing is always-on. Opt-out model, default on.
- Dispatcher reads prefs and marks muted outbox rows 'skipped' instead
  of sending; pure isMuted logic, fail-open on a prefs lookup error.
- notification_preferences table (default-deny RLS); /me toggles and
  self-view via validated service-client actions.

Alternate dues payment methods (migration 049):
- Admin sets external pay-here URLs per platform (PayPal/Zeffy/Venmo)
  in /settings; members see active ones on /me and pay off-platform;
  the treasurer reconciles manually (platform pre-tagged). https-only.
- dues_payment_methods table (SELECT space member, write admin/board).

/me visual pass:
- Restore the chip/preset profile editor (skills/interests/willing-to/
  affiliations); promote ChipInput and presets to shared modules.
- Gate the Stripe pay-dues button on whether the space has Stripe
  configured; show external links or a contact-admin note otherwise.
2026-05-19 20:26:32 -07:00
Moheeb Zara addedfdb0f docs: HANDOFF pass-56 closeout, recalibrated backlog for the next session
Adds a session-closeout subsection capturing the three-audit-pass
discipline that this phase set as a new high-bar (each repeat audit
on the "clean" change set found a real, deploy-blocking issue: the
RPC enumeration vector, the best-effort gap in helpers, the phishing
vector in user-controlled fields exposed by form_submission_admin).

Refreshes the remaining-work list to the post-Phase 4 priority. The
owner-gated end-to-end validation is still the single biggest gap.
Member preferences + in-app inbox are now the natural next product
work (Phase 5; preferences also doubles as the volume governor for
Phase 4 fan-outs). user_has_permission EXECUTE-TO-PUBLIC pre-existing
exposure recorded as a separate triage item with the constraint
written down (RLS policies depend on anon/authenticated EXECUTE, so
the lockdown can't just be copied from migration 047).
2026-05-19 18:33:15 -07:00
Moheeb Zara febd0a8c11 docs: HANDOFF pass-56 deploy state, run 26135623172 success
Marks the Phase 4 entry DEPLOYED. Deploy log confirms migration 047
applied (applying 047_members_with_permission.sql, REVOKE, GRANT).
Smoke clean: public 200, gated 307, Stripe webhook 400, cron 503 (no
regressions; cron-503 is the expected fails-safe with CRON_SECRET
unset). The new event types are wired but inert at the transport
layer until owner provisions Resend and the cron secret + crontab.
2026-05-19 18:30:15 -07:00
Moheeb Zara a61e34d98d docs: HANDOFF pass-56, record P4g phishing-defense audit fix
Renames the P4f entry to "first-audit fixes", adds a P4g entry for the
third-audit URL-strip defense, and bumps the test-count line to 572
(40 new cases). The commit list at the top of the section now ends at
09f293e.
2026-05-19 17:24:42 -07:00
Moheeb Zara 47e2b4614d docs: pass-56 second-audit touchups, lockdown note + style nit
DATABASE_SCHEMA: the migration 047 row now records the REVOKE/GRANT
lockdown, with one-line rationale for why it differs from
user_has_permission (RLS policies depend on anon/authenticated being
able to call user_has_permission, so locking it would break policy
evaluation; the new function has no such constraint).

HANDOFF: replaces "Next-most-leverage" with "Highest-priority" in my
pass-56 backlog header. "Leverage" is on the CLAUDE.md style-bans list.
2026-05-19 16:18:47 -07:00
Moheeb Zara 3dead56e5c docs: HANDOFF pass-56 addendum, audit findings + P4f hardening commit
Records the ultrathink audit and the P4f fixes (RPC lockdown + best-
effort enqueue wraps). Also flags pre-existing user_has_permission
EXECUTE TO PUBLIC for separate triage: narrower than the 047 case
(per-uid boolean, only useful within spaces the caller is in), and
locking it down requires careful work because RLS policies depend on
anon/authenticated being able to call it. Out of Phase 4 scope.

Updates the test-count line to 567 (35 new cases) covering the new
helper best-effort no-throw paths.
2026-05-19 16:13:04 -07:00
Moheeb Zara 6abac53380 docs: HANDOFF pass-56, Phase 4 notification breadth + migration 047
Adds an ARCHITECTURE section "Notification breadth: bookings, classes,
forms" describing each new event type, the triggering action, the
recipient rule, and the dedupe-key scheme. Updates Known Limitations #5
to reflect that booking + class + form notifications are no longer "not
wired yet."

API_REFERENCE: extends the Transactional notifications section with the
shared enqueue helper, every new event type, its trigger and recipient,
and the new members_with_permission RPC used for the forms.manage
fan-out.

DATABASE_SCHEMA + DB_SCHEMA_MAP: adds the migration 047 row and the new
helper function to the helper-functions list.

HANDOFF: pass-56 entry covers the four locked design forks, the four
sub-phase commits, test growth (532 -> 565, +33 cases), state (local,
inert post-deploy until owner provisions Resend + CRON_SECRET), the
known fan-out volume tradeoff, and the remaining backlog.
2026-05-19 15:32:39 -07:00
Moheeb Zara 792534ee7b docs: HANDOFF pass-55 — session closeout + prioritized backlog for the next session 2026-05-19 14:39:10 -07:00
Moheeb Zara 19dc6321be docs: fix doc drift from D/F/G work — migration 046, harness, subsystem-level facts
DATABASE_SCHEMA + DB_SCHEMA_MAP: 046 migration row (was missing — added
last turn but not table-rowed; CLAUDE.md doc-with-change rule).
README Features: Stripe recurring dues, transactional notifications,
member self-serve portal (Phase 1/2/3 — months of shipped product
spine were absent from the first thing a reader sees).
ARCHITECTURE: Known Limitations #1 now describes the DB-backed
integration harness (the previous 'no harness' claim was misleading);
Classes section notes the advisory-lock RPCs (045); Equipment notes
the GiST exclusion constraint + btree_gist (042); Door notes the
DNS-rebind defense (resolve-once, pin validated IP) + no-raw-reason
client policy + auth-param-aware redaction; Stripe webhook notes the
out-of-order monotonic period guard (D5), zero-decimal currency
handling (D7), and generic error responses (E3); Notifications notes
the per-space fair drain + per-attempt Idempotency-Key + .eq('status',
'pending') re-entrancy guard; Authorization adds the RLS-layer
privilege-eligible status gate (046).
2026-05-19 14:19:44 -07:00
Moheeb Zara 3703c2a76c docs: HANDOFF pass-54 — 046 deployed (run 26125372468), smoke clean 2026-05-19 14:08:56 -07:00
Moheeb Zara c00c82d796 docs: HANDOFF pass-54 — deferred RLS status-gate lifted (046), harness-proven, needs deploy 2026-05-19 14:04:55 -07:00
Moheeb Zara ee7f59f1f4 docs: fix ARCHITECTURE limitation #2 (Stripe IS integrated) + HANDOFF pass-53 (F4, 26 integration tests) 2026-05-19 13:26:46 -07:00
Moheeb Zara b0267bbfb6 docs: HANDOFF pass-53 state — harness pushed (run 26122719148) + F3 extension (21 integration tests) 2026-05-19 13:20:01 -07:00
Moheeb Zara 6403220c92 test(integration): DB-backed harness validating shipped SQL (045/042/044/043)
Root-cause fix for 'correctness depended on post-deploy manual audits'.
Separate psql-driven suite (pnpm test:integration, own vitest config,
integration/ outside __tests__ so the hermetic pnpm test gate is
untouched; self-skips without a DB). 18 tests run against real Postgres:
- 045 class_signup_tx/class_cancel_tx: RETURNS TABLE->array shape the
  actions destructure, capacity/waitlist/dedupe, and the per-session
  advisory lock under TWO concurrent connections (D11 proven, never
  over-enrolls).
- 042 equipment exclusion constraint (overlap 23P01; cancelled/adjacent
  ok). 044 self-change trigger (role + payment cols blocked; service
  bypass). 043 members_update WITH CHECK (cross-space move blocked).
Disproves the pass-52 flagged risk (signup rpc-shape). Non-runtime;
default pnpm test still 532/26 hermetic; build clean.
2026-05-19 13:01:16 -07:00
Moheeb Zara c2f5f2a726 docs: HANDOFF pass-52 — E1-E3 deployed (run 26088940576), smoke clean 2026-05-19 02:38:28 -07:00
Moheeb Zara 099a103ed5 docs: HANDOFF pass-52 — E1-E3 hardening + evidence-based SECURITY DEFINER deferral 2026-05-19 02:14:39 -07:00
Moheeb Zara b90debbd37 docs: HANDOFF pass-51 — D11/D12 deployed (run 26086312156), smoke clean 2026-05-19 01:45:01 -07:00
Moheeb Zara d5a282b642 docs: HANDOFF pass-51 — D1-D10 verified (D6 regression fixed), D11/D12 deferred P1s cleared 2026-05-19 01:33:32 -07:00
Moheeb Zara 705f03a299 fix(classes): atomic signup/cancel under per-session advisory lock (D11)
Deferred deeper-audit P1. signUpForClass (count->decide->insert) and
cancelMySignup (cancel->recount->promote) were non-atomic: concurrent
signups at the capacity boundary over-enrolled and concurrent cancels
double-promoted waitlisters (dynamic capacity can't be a DB constraint).
Migration 045 adds class_signup_tx/class_cancel_tx (SECURITY DEFINER,
pg_advisory_xact_lock per session) doing the decision+write atomically;
actions call the RPCs and keep all pre-checks. Removed the now-unused
effectiveCapacity/computeSignupStatus/pickPromotion imports +
isUniqueViolation (pure rules still live + tested in classes-logic).
528 tests; build clean.
2026-05-19 01:30:07 -07:00
Moheeb Zara 6568bc9c10 docs: HANDOFF pass-50 — D1-D10 deployed (run 26071186611), smoke clean 2026-05-18 18:49:39 -07:00
Moheeb Zara a7c3a08db9 docs: HANDOFF pass-50 — deeper audit + D1-D10 remediation 2026-05-18 18:45:43 -07:00
Moheeb Zara 785e8e0ef9 fix(rls): block member self-change of payment/dues columns (D9)
prevent_member_self_role_change covered role/status/space but not the
financial columns; RLS members_update lets a member UPDATE their own
row, so payment_status/dues_paid_until/last_paid_at/last_payment_at/
stripe_customer_id/payment_note/joined_at were self-settable via
PostgREST to forge a dues-good signal. Migration 044 adds them to the
blocked set; service-client (auth.uid() NULL) + privileged-on-another-
member writes still bypass (Stripe webhook unaffected). Schema + maps.
2026-05-18 18:41:35 -07:00
Moheeb Zara 26e54f1fcb fix(rls): members_update WITH CHECK mirrors USING (D8)
UPDATE policy was USING-only: a privileged caller could mutate a member
row into a space they don't administer (no post-image re-check).
Migration 043 adds the mirrored WITH CHECK. Self column escalation
(space_id/role/status) remains blocked by the trigger (D9). Schema +
DB maps updated.
2026-05-18 18:39:53 -07:00
Moheeb Zara a513ad0fab fix(equipment): DB-enforced reservation no-overlap (D1, P0)
reserveEquipment was check-then-insert with no DB guard: two concurrent
requests both passed the in-memory overlap check and double-booked a
machine. Migration 042 adds btree_gist + a GiST EXCLUDE constraint
(equipment_id =, tstzrange && ) WHERE status='reserved', making the DB
the arbiter; the action maps the 23P01 violation to the existing
friendly overlap message. App-side check kept as a fast pre-check.
Schema + DB maps updated.
2026-05-18 18:26:19 -07:00
Moheeb Zara 247e966446 docs: HANDOFF pass-49 — Phase 3 P0 fixes deployed (run 26068281306) 2026-05-18 17:21:48 -07:00
Moheeb Zara 997e2e1366 docs: HANDOFF pass-49 — Phase 3 post-deploy audit, 2 P0 fixed (deploy ASAP) 2026-05-18 17:01:47 -07:00
Moheeb Zara c8842c3f75 docs: HANDOFF pass-48 — Phase 3 deployed (run 26066604148), smoke clean 2026-05-18 16:34:28 -07:00
Moheeb Zara 83ff54b17c docs: Phase 3 portal — API_REFERENCE, ARCHITECTURE, DEPLOYMENT (email-change config), HANDOFF pass-48 (P3f) 2026-05-18 15:55:20 -07:00
Moheeb Zara 2c847befeb feat(me): my-payments self-view in the Membership tab (P3d)
New getMyPayments: payments SELECT is treasurer-scoped (RLS), so the
self-view uses the validated service client strictly scoped to the
caller's space_id + member_id (same convention as getMyBilling /
getMyNotifications). Read-only list (amount, date, platform, status)
in the Membership tab. Docs updated. 521 tests; build clean.
2026-05-18 15:50:34 -07:00
Moheeb Zara 0e54a0c7cd docs: HANDOFF pass-47 — full-codebase audit + P1 remediation (R1-R4) 2026-05-18 13:15:00 -07:00
Moheeb Zara 504586fdcd fix(security): vault integration secrets instead of plaintext config (R1)
PayPal client_secret (and any secret-named integration field) was stored
plaintext in integrations.config, unlike Stripe/door which use the
AES-256-GCM vault. A DB read or backup leaked it in cleartext.

- lib/secrets/vault.ts: canonical generic storeSecret/readSecret +
  isSecretConfigField (the proven Stripe vault logic, generalized).
- saveIntegration: secret-named fields go to the vault, only a *_ref is
  kept in config; blank submit preserves the ref (write-only like
  Stripe); legacy plaintext auto-migrates into the vault on next save.
- paypal/sync reads client_secret from the vault, transitional fallback
  to legacy plaintext until the next save.
- Docs updated (API_REFERENCE, ARCHITECTURE 10.3). 502 tests.
2026-05-18 13:08:17 -07:00
Moheeb Zara 7844c262f8 docs: HANDOFF pass-46 — Phase 2 deployed (run 25990457904), smoke clean 2026-05-17 05:12:09 -07:00
Moheeb Zara f1bae44afd fix(notifications): P2 extreme-audit P1 fixes before deploy
Independent audit verdict was NOT SAFE; all findings fixed:
- dispatcher Idempotency-Key is now per-attempt (${id}:${attempts}),
  not row id alone, so a transient-failure response cached by Resend
  cannot permanently suppress every retry and silently lose an email.
- enqueueDues is fully try/catch wrapped so a notifications-table or
  render failure can never throw into / wedge the Stripe money path.
- dues_lapsed dedupe falls back to the subscription id when periodEnd
  is null (canceled/deleted subs), so future lapses no longer collapse
  to one key and get suppressed. New test; suite 499.
- documented the dispatcher's once/min concurrency assumption.
2026-05-17 05:06:02 -07:00
Moheeb Zara 7eb83676e7 feat(notifications): P2e member self-view on /me + docs
getMyNotifications (service-client self-view, mirrors getMyBilling) +
a read-only Notifications section on /me. Docs updated in the same
change: ARCHITECTURE (Phase 2 subsystem + limitation #5), API_REFERENCE
(notifications section, fixed the stale no-auth-middleware claim),
DATABASE_SCHEMA + DB_SCHEMA_MAP (migration 041 + table), DEPLOYMENT
(env vars + dispatcher crontab), HANDOFF pass-45.
2026-05-17 04:58:46 -07:00
Moheeb Zara dc76a86949 docs: HANDOFF pass-44 — Stripe Phase 1 deployed; webhook routing P0 fixed + redeployed 2026-05-17 03:59:21 -07:00
Moheeb Zara 64ce21e9a0 docs: HANDOFF pass-43 (Stripe Phase 1 deep-audited, P0/P1 fixed, deploy-ready) 2026-05-17 03:46:46 -07:00
Moheeb Zara ef9b0dc64f docs: P1g Stripe dues — API_REFERENCE + ARCHITECTURE + HANDOFF pass-42 (Phase 1 complete) 2026-05-17 03:39:51 -07:00
Moheeb Zara 0a11d36de1 docs: HANDOFF pass-41 (Stripe P1c-P1e built, review gate before deploy) 2026-05-17 03:11:35 -07:00
Moheeb Zara 289fa2d991 docs: HANDOFF pass-40 (Stripe dues P1a+P1b done; P1c-P1g plan) 2026-05-17 03:00:37 -07:00
Moheeb Zara 521df696a2 feat(billing): P1a Stripe schema — payment_platform 'stripe' + member_billing + webhook ledger
Migration 040 (mirrored in schema.sql Section 25 + the CREATE TYPE
list): ALTER TYPE payment_platform ADD VALUE 'stripe' (only safe path;
not used as a literal in the same migration); member_billing (member<->
Stripe customer/subscription/status/period; RLS SELECT = admin/board/
treasurer, NO client write policy — webhook/service-client only);
stripe_webhook_events idempotency ledger (PK = Stripe event id, service-
only, no policy). Per-space OWN keys: secrets->vault, config->
integrations.config. types + DATABASE_SCHEMA + DB_SCHEMA_MAP.
2026-05-17 02:57:58 -07:00
Moheeb Zara fe8696bbc3 docs: HANDOFF pass-39 deploy-state (backlog complete, deployed run 25987172786) 2026-05-17 02:33:23 -07:00
Moheeb Zara 41193d3f68 docs: HANDOFF pass-39 (backlog complete; product spine is the next thread) 2026-05-17 02:03:01 -07:00
Moheeb Zara d3481129a8 docs: HANDOFF pass-38 (mobile card tables; bulk/a11y remaining) 2026-05-17 01:54:55 -07:00
Moheeb Zara 51f733414b docs: HANDOFF pass-37 (dashboard attention + cached perms; remaining queued) 2026-05-17 01:37:35 -07:00
Moheeb Zara e7463c0b95 docs: HANDOFF pass-36 (F1/F2 + UX pack part 1) 2026-05-17 01:24:48 -07:00
Moheeb Zara 363e2a667f docs: HANDOFF pass-35 clean checkpoint + next-session kickoff prompt 2026-05-17 00:59:56 -07:00