After commit 90acc35 issued the cookie for $0 signups too, the "payment"
framing was wrong — there's no payment in a $0 signup. The cookie is
about bridging the gap between signup-form submit and email verify, not
about payment specifically.
Changes:
- setPaymentBridgeCookie → setSignupBridgeCookie
- getPaymentBridgeMember → getSignupBridgeMember
- Cookie wire name payment-bridge → signup-bridge
- JWT scope payment_bridge → signup_bridge
Touches both /api/helcim/subscription (signup activation) and
/api/helcim/initialize-payment (paid Helcim checkout) which both consume
the cookie. In-flight signup sessions started before this lands will
need to re-submit the form (cookie name mismatch); cutover hasn't
happened yet, so the only impact is local dev sessions.
When a /join submitter's email matches a pending/selected/invited
PreRegistration, mark the pre-reg as accepted and link memberId to the
new Member. Prevents the same person from appearing as both an active
member and an unaccepted pre-registrant. Silent — no email, no UI.
Adds the PreRegistration mock to helcim-customer and free-signup-flow
test suites, since both invoke the customer handler at runtime.
Free ($0) signups need the same short-lived bridge cookie as paid signups
so /api/helcim/subscription can identify the member during activation
without a verified auth session. Drops the contributionAmount > 0 guard
that broke free-tier activation in the same flow.
Replaces the per-file inviteToSlack helpers with a single auto-flag
call. Self-serve activation paths now check for pre-existing workspace
membership (silent on miss) instead of attempting an admin-only invite.
- helcim/subscription.post.js: removed local inviteToSlack; both
free- and paid-tier activation branches now call the helper, then
notifyNewMember with the canonical 'manual_invitation_required' arg.
- members/create.post.js: same shape — helper + canonical notify arg.
- invite/accept.post.js (free-tier branch): added the helper call after
member creation. Free-tier had no prior Slack call (audit confirmed);
paid-tier remains untouched and activates via the Helcim webhook.
Admin-created and CSV-imported members intentionally do NOT call the
helper — admins flip the flag manually after sending the invite.
Test stub for autoFlagPreExistingSlackAccess added to server setup.
Helcim returns next-charge as `dateBilling` on POST /subscriptions, but the
two CREATE sites were reading `subscription.nextBillingDate`, leaving
`member.nextBillingDate` empty after every signup and free→paid upgrade.
The lazy refresh in subscription.get.js (which already accepts both shapes)
masked it on next account-page load, so renders eventually populated — but
the success response we returned to the client also had `nextBillingDate:
undefined`. Mirror the GET-side resolution at both CREATE sites: prefer
`dateBilling`, fall back to `nextBillingDate`. Existing Number.isNaN guard
unchanged; defensively rejects malformed strings from either field.
Helcim refuses paymentType:'verify' for cards already saved on a
customer ("A new card must be entered for saving the payment method"),
breaking every "Complete Payment" retry after a partial-failed signup.
Add GET /api/helcim/existing-card and short-circuit HelcimPay verify in
useMemberPayment + payment-setup.vue when a saved card is found, going
straight to subscription creation. The two existence-check fetches run
in parallel with get-or-create-customer so no extra round-trip latency
in the common path.
Follow-up to 208638e. Code review surfaced a few real issues; this
commit addresses them.
- login.post.js now uses the new sendMagicLink util instead of
duplicating the jti/jwt/Resend/logActivity logic. Reduces 60 lines.
- sendMagicLink accepts an optional pre-loaded Member doc, skipping
the redundant findOne when the caller already has one. customer.post.js
passes the just-created/upgraded member, dropping signup from 3
Mongo round-trips to 1 (lookup is gone; jti burn remains).
- sendMagicLink now lowercases the email defensively so callers don't
have to remember.
- rateLimit.js: replaced an effectively-dead eviction line with a
probabilistic sweep (~1% of calls scan and evict keys whose newest
entry has aged out). Caps unbounded Map growth under random-key
spraying.
- reconcile-payments.post.js: 401/403/404 from Helcim now bails out
immediately instead of burning all 3 retry attempts; dry-run
summary filters via the same RECONCILABLE_STATUSES set as apply
mode so counts match.
- Deleted WHAT-comments and section banners per CLAUDE.md no-comment
rule. Kept genuine WHY-comments (validateBeforeSave rationale,
amount-IGNORED-for-tickets, sendConfirmation deliberately-omitted).
Tests: 758/760 passing (unchanged).
Day-of-launch deep-dive audit and remediation. 11 issues fixed across
security, correctness, and reliability. Tests: 698 → 758 passing
(+60), 0 failing, 2 skipped.
CRITICAL (security)
Fix#1 — HELCIM_API_TOKEN removed from runtimeConfig.public; dead
useHelcim.js deleted. Production token MUST BE ROTATED post-deploy
(was previously exposed in window.__NUXT__ payload).
Fix#2 — /api/helcim/customer gated with origin check + per-IP/email
rate limit + magic-link email verification (replaces unauthenticated
setAuthCookie). Adds payment-bridge token for paid-tier signup so
users can complete Helcim checkout before email verify. New utils:
server/utils/{magicLink,rateLimit}.js. UX: signup success copy now
prompts user to check email.
Fix#3 — /api/events/[id]/payment deleted (dead code with unauth
member-spoof bypass — processHelcimPayment was a permanent stub).
Removes processHelcimPayment export and eventPaymentSchema.
Fix#4 — /api/helcim/initialize-payment re-derives ticket amount
server-side via calculateTicketPrice and calculateSeriesTicketPrice.
Adds new series_ticket metadata type (was being shoved through
event_ticket with seriesId in metadata.eventId).
Fix#5 — /api/helcim/customer upgrades existing status:guest members
in place rather than rejecting with 409. Lowercases email at lookup;
preserves _id so prior event registrations stay linked.
HIGH (correctness / reliability)
Fix#6 — Daily reconciliation cron via Netlify scheduled function
(@daily). New: netlify.toml, netlify/functions/reconcile-payments.mjs,
server/api/internal/reconcile-payments.post.js. Shared-secret auth
via NUXT_RECONCILE_TOKEN env var. Inline 3-retry exponential backoff
on Helcim transactions API.
Fix#7 — validateBeforeSave: false on event subdoc saves (waitlist
endpoints) to dodge legacy location validators.
Fix#8 — /api/series/[id]/tickets/purchase always upserts a guest
Member when caller is unauthenticated, mirrors event-ticket flow
byte-for-byte. SeriesPassPurchase.vue adds guest-account hint and
client auth refresh on signedIn:true response.
Fix#9 — /api/members/cancel-subscription leaves status active per
ratified bylaws (was pending_payment). Adds lastCancelledAt audit
field on Member model. Indirectly fixes false-positive
detectStuckPendingPayment admin alert for cancelled members.
Fix#10 — /api/auth/verify uses validateBody with strict() Zod schema
(verifyMagicLinkSchema, max 2000 chars).
Fix#11 — 8 vitest cases for cancel-subscription handler (was
uncovered).
Specs and audit at docs/superpowers/specs/2026-04-25-fix-*.md and
docs/superpowers/plans/2026-04-25-launch-readiness-fixes.md.
LAUNCH_READINESS.md updated with new test count, 3 deploy-time
tasks (rotate Helcim token, set NUXT_RECONCILE_TOKEN, verify
Netlify scheduled function), and Fixed-2026-04-25 fix log.
After a paid subscription is created and the Member row is flipped to
active, fetches the newest paid transaction from Helcim and upserts a
Payment row. Passes paymentType from the chosen cadence and
sendConfirmation: true.
Wrapped in try/catch: a logging failure here never breaks subscription
creation — the reconcile-helcim-payments script will pick up any
misses on the next run.
Three related changes on /member/account:
1. Payment History section now renders when contributionAmount > 0 OR
past payments exist. Previously a paid member who switched to $0 lost
visibility of their own past charges.
2. New "Next charge: $X on DATE" row renders above the transaction list
when nextPaymentDate is available, using --candle dashed border.
3. server/api/helcim/subscription.get.js now reads dateBilling from
Helcim's GET response and handles data as either object or array.
Helcim's real shape is {data: {id, dateBilling, ...}} — the old code
expected {data: [{nextBillingDate}]} and returned empty strings, so
the Membership-card "Next payment" row never rendered for members
whose cached date was missing. subscription.post.js and
update-contribution.post.js have the same wrong field name in their
CREATE flows; left for a follow-up — the GET refresh masks it.
Manual edit-flow and admin-flow tests also recorded in
docs/LAUNCH_READINESS.md.
Persist nextBillingDate on subscription create/update; unset on
cancel or downgrade to free. Account page displays the cached
date and lazily refreshes from Helcim when the cached value is
within 24h of now (or missing).
POST /api/helcim/update-card updates the customer's default card, then
best-effort patches the active subscription payment method. Status-gated
to {active, pending_payment}; verifies the submitted cardToken is
attached to the member's helcimCustomerId via listHelcimCustomerCards.
On subscription PATCH 5xx we revert the customer default to the prior
card token; 4xx (schema rejection — cardToken is not a documented
subscription PATCH field) is tolerated since the customer default is
the authoritative billing driver.
Add GET /api/helcim/payment-history returning the authenticated
member's normalized Helcim card transactions (newest first, capped
at 50). Resolves helcimCustomerId -> customerCode via getHelcimCustomer
before calling listHelcimCustomerTransactions. Returns
{ transactions: [] } when the member has no helcimCustomerId, and
{ transactions: [], error: 'unavailable' } (HTTP 200) on Helcim
failures so the UI can render fallback copy.
Covered by unit tests at tests/server/api/helcim-payment-history.test.js
(auth, missing customer id, happy path, both Helcim failure paths,
missing customerCode).
Replace tier-based plan lookup with cadence-keyed lookup, compute
recurringAmount via getTierAmount, persist billingCadence on member.
Delete both manual-fallback blocks; Helcim failure now surfaces as 500.
Moves updateHelcimSubscription to the live-verified wrapped shape
(PATCH /subscriptions { subscriptions: [{ id, ...payload }] }), adds a prior-
status check so sendWelcomeEmail only fires on pending_payment to active
transitions, short-circuits get-or-create-customer when a valid
helcimCustomerId is already on file, and replaces member.save() Slack-status
writes with findByIdAndUpdate({ runValidators: false }) to avoid save-time
validator pitfalls.
Introduces /community-guidelines and /policies/{privacy,terms,[slug]} pages,
swaps the signup/invite checkbox from agreedToTerms to agreedToGuidelines,
adds Member.agreement.acceptedAt, and stamps the field when a Helcim
customer is created.
Adds schema-based input validation across helcim, events, members,
series, admin, and updates API endpoints. Removes the peer-support
debug test endpoint. Adds validation test coverage.
- Add centralized Zod schemas (server/utils/schemas.js) and validateBody
utility for all API endpoints
- Fix critical mass assignment in member creation: raw body no longer
passed to new Member(), only validated fields (email, name, circle,
contributionTier) are accepted
- Apply Zod validation to login, profile patch, event registration,
updates, verify-payment, and admin event creation endpoints
- Fix logout cookie flags to match login (httpOnly: true, secure
conditional on NODE_ENV)
- Delete unauthenticated test/debug endpoints (test-connection,
test-subscription, test-bot)
- Remove sensitive console.log statements from Helcim and member
endpoints
- Remove unused bcryptjs dependency
- Add 10MB file size limit on image uploads
- Use runtime config for JWT secret across all endpoints
- Add 38 validation tests (117 total, all passing)
Auth: Add requireAuth/requireAdmin guards with JWT cookie verification,
member status checks (suspended/cancelled = 403), and admin role
enforcement. Apply to all admin, upload, and payment endpoints. Add
role field to Member model.
CSRF: Double-submit cookie middleware with client plugin. Exempt
webhook and magic-link verify routes.
Headers: X-Content-Type-Options, X-Frame-Options, X-XSS-Protection,
Referrer-Policy, Permissions-Policy on all responses. HSTS and CSP
(Helcim/Cloudinary/Plausible sources) in production only.
Rate limiting: Auth 5/5min, payment 10/min, upload 10/min, general
100/min via rate-limiter-flexible, keyed by client IP.
XSS: DOMPurify sanitization on marked() output with tag/attr
allowlists. escapeHtml() utility for email template interpolation.
Anti-enumeration: Login returns identical response for existing and
non-existing emails. Remove 404 handling from login UI components.
Mass assignment: Remove helcimCustomerId from profile allowedFields.
Session: 7-day token expiry, refresh endpoint, httpOnly+secure cookies.
Environment: Validate required secrets on startup via server plugin.
Remove JWT_SECRET hardcoded fallback.