Severity: Low — batch of hardening/cleanup items from the launch-readiness audit. Split out if any grows.
Items
-
Enable leaked-password protection in Supabase Auth (security advisor: currently disabled) — checks passwords against HaveIBeenPwned.
-
No Stripe idempotencyKey on paymentIntents.create (src/app/api/portal/projects/checkout/route.ts:381-406) — a double-submit mints two intents + rows (bounded only by the 3/min limit). Add an idempotency key.
-
Observability gaps: webhook/poll metadata-mismatch events log-and-200 with no alerting surface, and absent metadata passes the cross-checks; no error-reporting service (Sentry/OTel) is wired at all (src/app/error.tsx:15 has only a scaffold comment). Wire error reporting + a mismatch alert.
-
Discount RPC surface: lookup_discount_code is granted to authenticated with no in-body guard and returns full code params incl. reserved/redeemed counts — a per-guess oracle (supabase/migrations/20260715_consume_discount_codes.sql); reserve_/restore_first_mix_discount retain the default EXECUTE TO PUBLIC and their identity guard is bypassed for anon (auth.uid() null passes). Bounded today (0 codes; dormant flag). Route lookup through the service client and revoke the first-mix RPCs to service_role only, matching the catalog trio.
-
Thin confirm route + error leakage: files/[fileId]/confirm/route.ts uses requireApiUser only (no state/uploader check, blind upload_status='uploaded'); files/route.ts:91 forwards the raw storage/PG error object to the client on 500. Tighten the guard; stop leaking internal error details.
-
Stripe API version drift: pinned 2026-03-25.dahlia is two releases behind the installed SDK's 2026-05-27.dahlia, and the as ApiVersion cast suppresses drift detection (src/lib/stripe/server.ts:16-20). Re-pin deliberately.
-
Cosmetic/config: dead footer newsletter form promising a welcome discount (no submit handler); STRIPE_RESTRICTED_KEY documented but unused; package.json still named tailwind-plus-studio with no engines field; contact page shows mix@nova-spatial.com while CONTACT_INBOX_TO=contact@nova-spatial.com; RESEND_FROM_EMAIL fallback is the wrong brand ("Atmos onboarding@resend.dev"); several handlers await request.json() unguarded (500 instead of 400 on malformed bodies); enforce_studio_only_archive lacks the service-role escape its sibling fences have; purge drains only 20 projects/day with mayHaveMore ignored (irrelevant at launch volume).
-
Orphan rows: no sweeper exists for stale pending project_files rows (register → abandoned PUT) or for registered-but-never-submitted comment-attachment objects. Harmless for /listen (filters upload_status='uploaded') but accumulates.
Severity: Low — batch of hardening/cleanup items from the launch-readiness audit. Split out if any grows.
Items
Enable leaked-password protection in Supabase Auth (security advisor: currently disabled) — checks passwords against HaveIBeenPwned.
No Stripe
idempotencyKeyonpaymentIntents.create(src/app/api/portal/projects/checkout/route.ts:381-406) — a double-submit mints two intents + rows (bounded only by the 3/min limit). Add an idempotency key.Observability gaps: webhook/poll metadata-mismatch events log-and-200 with no alerting surface, and absent metadata passes the cross-checks; no error-reporting service (Sentry/OTel) is wired at all (
src/app/error.tsx:15has only a scaffold comment). Wire error reporting + a mismatch alert.Discount RPC surface:
lookup_discount_codeis granted toauthenticatedwith no in-body guard and returns full code params incl. reserved/redeemed counts — a per-guess oracle (supabase/migrations/20260715_consume_discount_codes.sql);reserve_/restore_first_mix_discountretain the defaultEXECUTE TO PUBLICand their identity guard is bypassed for anon (auth.uid()null passes). Bounded today (0 codes; dormant flag). Route lookup through the service client and revoke the first-mix RPCs toservice_roleonly, matching the catalog trio.Thin
confirmroute + error leakage:files/[fileId]/confirm/route.tsusesrequireApiUseronly (no state/uploader check, blindupload_status='uploaded');files/route.ts:91forwards the raw storage/PG error object to the client on 500. Tighten the guard; stop leaking internal error details.Stripe API version drift: pinned
2026-03-25.dahliais two releases behind the installed SDK's2026-05-27.dahlia, and theas ApiVersioncast suppresses drift detection (src/lib/stripe/server.ts:16-20). Re-pin deliberately.Cosmetic/config: dead footer newsletter form promising a welcome discount (no submit handler);
STRIPE_RESTRICTED_KEYdocumented but unused;package.jsonstill namedtailwind-plus-studiowith noenginesfield; contact page showsmix@nova-spatial.comwhileCONTACT_INBOX_TO=contact@nova-spatial.com;RESEND_FROM_EMAILfallback is the wrong brand ("Atmos onboarding@resend.dev"); several handlersawait request.json()unguarded (500 instead of 400 on malformed bodies);enforce_studio_only_archivelacks the service-role escape its sibling fences have; purge drains only 20 projects/day withmayHaveMoreignored (irrelevant at launch volume).Orphan rows: no sweeper exists for stale
pendingproject_filesrows (register → abandoned PUT) or for registered-but-never-submitted comment-attachment objects. Harmless for/listen(filtersupload_status='uploaded') but accumulates.