Add data-transfer opt-out flow (Supabase + SendGrid) - #9
Open
RubenSousaDinis wants to merge 13 commits into
Open
Add data-transfer opt-out flow (Supabase + SendGrid)#9RubenSousaDinis wants to merge 13 commits into
RubenSousaDinis wants to merge 13 commits into
Conversation
Talent Protocol is shutting down; builders can opt out of having their data carried over to the successor company. talent-api already implements the flow end-to-end but requires a server-side X-API-KEY on every request, so this adds thin proxy routes (/api/opt-out/request, /confirm, /status) that hold the key server-side and relay talent-api's status/body verbatim, plus an injectable-fetch, never-throwing client lib (src/lib/opt-out.ts) and two new route builders for the upcoming pages. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ates Fixes final-review findings: proxy routes now forward the visitor's IP (first x-forwarded-for hop) upstream as X-Client-IP so talent-api's rate limiter keys per visitor instead of this app's shared egress IPs; the confirm page no longer tells visitors with a transient failure (429/502/503/timeout) that their link expired, giving 'unavailable' its own retry-capable state distinct from genuine 'invalid' (422) results; requestOptOut gets a dedicated 'rate-limited' variant for 429 with its own copy; async result panels on both pages get role="status" for screen readers; plus a few trivial UX polish items (reset-to-form affordance, clearing stale results on validation errors, consistent font-mono email styling). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
firstName, to, and confirmUrl were interpolated raw into the HTML mail part. firstName comes from records.name (free text from a legacy DB export), so any &, <, >, ", or ' there could produce malformed HTML and break the confirm link. Add a local escapeHtml helper and apply it only to the html branch; the text/plain branch stays raw since escaping it would show users literal entity sequences. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Address task-3 review findings: race post-validation work against an 8s deadline that resolves to the same success body, so a stalled Supabase/SendGrid call (unlike a fast connection-refused) can no longer let a platform timeout distinguish a matched email from an unmatched one. Treat an unparseable last_sent_at as within cooldown (fail closed, not open). Switch test isolation to resetAllMocks and restore the 400 body assertions. Fix a stale comment in supabase-admin.ts left over from an earlier insertOptOut contract. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- test/opt-out-api-routes.test.ts: replace the hardcoded 2026-09-13 expires_at default in optOutRow() with a value relative to now, so the fixture stops silently flipping three tests to the expired-token branch after that date. Also fixes the now-flaky re-mint test this uncovered, by giving its `existing` row an explicitly past expires_at instead of relying on two same-millisecond Date.now() calls to differ. - src/lib/opt-out.ts, src/app/data-opt-out/page.tsx: rewrite comments that still described the deleted talent-api proxy module to describe the current Supabase + SendGrid backend instead. - README.md: correct the "Ground rules" line that claimed zero server-side secrets/state; the temporary opt-out flow holds SUPABASE_SECRET_KEY, SENDGRID_API_KEY, and real opt-out row state. - supabase/schema.sql: bring the file back in sync with the live project — lowercase-enforcing check constraints and plain-email indexes on records/extra_emails to match the actual eq. query shape. - src/app/api/opt-out/request/route.ts: pin the function-duration assumption with an explicit `maxDuration = 30` and reword the deadline comment to reference it instead of an unstated platform limit. - src/lib/opt-out.ts: document why the 429 branch is not dead code (a planned Vercel Firewall rate rule on /api/opt-out reaches it), per review guidance not to prune it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The data-transfer opt-out flow (/data-opt-out and its confirm/[token]
subtree) is reached from an email, not app navigation. Showing the Open
Builder Score navbar/footer there put an unfamiliar product brand and
distracting links in front of someone deciding whether to opt out —
exactly the shape people are trained to read as phishing.
A nested layout can't remove markup the root layout renders directly
around {children}, and giving these two routes their own root layout
would mean dropping app/layout.tsx as the app's single root, which is
far more invasive than warranted here. Instead, SiteChrome gates on
pathname in the one place Header/Footer were already being rendered:
normal routes keep the existing shell, and the opt-out routes get a
bare shell with just the Talent wordmark, keeping the app's fonts,
colors, and theme tokens untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
optout.talentprotocol.com now points at this Vercel project, so it was serving the whole Open Builder Score app. Add a proxy.ts that, only on that exact host, allows the /data-opt-out pages, /api/opt-out/* routes, and the static assets they need, redirects a bare / to the opt-out form on the same host (so a half-remembered URL lands on the form instead of bouncing away from it), and sends everything else off to talentprotocol.com with a non-cacheable 307. Every other host is untouched. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fix the ready state's email/text concatenation bug with explicit {' '}
spacing (transform-agnostic, not a CSS margin patch), lift the email out
of the sentence flow into its own prominent block, and raise the primary
sentence's contrast/size while keeping the "can't be undone" caveat
present but secondary. Same spacing pattern fixed in already-confirmed
and confirmed where it recurred.
Give the confirmed state a "Go to talentprotocol.com" button and a
10-second auto-redirect with a visible, ticking countdown. The interval
lives in a framework-free startRedirectCountdown helper so it's unit
testable without a component-render harness (none exists in this repo).
Screen readers get exactly two announcements — the initial notice and a
3-second final warning — never one per tick.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Public opt-out flow for the Talent Protocol shutdown. A recipient of the transfer notice visits
/data-opt-out, types their email, receives a tokenized confirmation email, clicks through, and presses an explicit Confirm opt-out button. Confirmed opt-outs are excluded from the data handed to the acquiring company.Backed by a dedicated Supabase database (
records+extra_emailsexported from talent-api,opt_outswritten here) and SendGrid for the confirmation email. Pages are bare and Talent-branded — no app chrome — since recipients arrive from an email and shouldn't meet unfamiliar navigation while making a consent decision.Design properties
{"success":true}whether or not the email matches, including on cooldown, already-confirmed, insert races, send failures, upstream outages, and an internal 8s deadline. It never reveals whether an address is in the transferring cohort.SUPABASE_SECRET_KEY/SENDGRID_API_KEYare read only in route handlers, never logged, neverNEXT_PUBLIC_. RLS is deny-all so only the service key reaches the data.Deployment prerequisites
SUPABASE_SECRET_KEYandSENDGRID_API_KEYin the Vercel project (SUPABASE_URLoptional; defaults to the project URL)/api/opt-out— this is the only abuse control; without it an attacker can drive unbounded SendGrid sendssupabase/schema.sqlapplied (already done on the live project)Test plan
npm run test— 485/485 across 31 files; typecheck and lint clean on all touched files🤖 Generated with Claude Code