fix(onboarding,brevo): /start always 302 + Brevo 401 carries correct agent_action (API-5/6)#172
Merged
Merged
Conversation
…agent_action (API-5/6)
== API-5 (P2) — GET /start ALWAYS 302s ==
Per CLAUDE.md 'Live API surface': '/start (302 → dashboard /claim?t=jwt)'.
Previously, an invalid/missing/expired token surfaced the raw JSON envelope
'{"ok":false,"error":"invalid_token"}' with HTTP 400. /start URLs land
in agents' terminal logs ('upgrade: https://api.instanode.dev/start?t=...');
when a user copy-pastes a stale one into a browser, they see naked JSON
instead of a friendly recovery flow.
Fix: pass the token through verbatim and let the dashboard's ClaimPage
handle every validation case (expired / unrecognised / already-claimed /
empty). The JTI lookup now happens once at /claim time where it's actually
load-bearing — the platform side no longer wastes a DB lookup on every
drive-by /start hit. landing_viewed metric still increments so the funnel
pivot stays measurable.
Edge cases:
- Missing ?t — 302 to /claim with no t= query, dashboard renders empty state.
- Garbage token — 302 to /claim?t=garbage, dashboard renders 'invalid token'.
- Unknown JTI — 302 to /claim?t=<jwt>, dashboard does the lookup.
- Happy path — unchanged (302 to /claim?t=<jwt>).
== API-6 (P2) — Brevo 401 agent_action targets operator, not user ==
POST /webhooks/brevo/<bogus-secret> returned 401 with the canonical
'unauthorized' error class — and the canonical agent_action for that class
('Tell the user their INSTANODE_TOKEN is missing or invalid. Have them log
in at https://instanode.dev/login...'). The Brevo webhook is unrelated to
user auth: an agent reading the error envelope would tell the user to log
in for a new INSTANODE_TOKEN, which is uselessly wrong. The actual incident
is an OPERATOR-side Brevo dashboard config drift.
Fix: introduce a new error class 'brevo_secret_mismatch' with operator-
targeted agent_action that tells the caller to verify the Brevo dashboard
webhook URL contains the configured BREVO_WEBHOOK_SECRET. HTTP status stays
401 — only the error CODE + agent_action copy change. Existing operator
alerting that pivots off metrics.BrevoWebhookEventsTotal{result="unauthorized"}
is unaffected.
== Multi-surface checklist (memory rule 22) ==
| Surface | Touched | Why |
|---|---|---|
| api/internal/handlers/openapi.go | Yes | /start contract changed from {302,400} to {302} only; spec updated to reflect always-302 + 't' parameter now optional |
| api/internal/handlers/helpers.go | Yes | New 'brevo_secret_mismatch' entry in codeToAgentAction map |
| content/llms.txt | No | /start was already documented as 302 — the dashboard-renders-error behaviour is the only new contract, and it's not a customer/agent-facing change (every agent that follows the 302 already lands at the dashboard regardless) |
| METRICS-CATALOG.md | No | No new metrics |
| dashboard | No | ClaimPage already handles the 'no/expired token' UI state |
== Test coverage ==
- TestResidualStartLanding_MissingToken_RedirectsToClaim — no t= → 302
- TestResidualStartLanding_GarbageToken_StillRedirects — invalid JWT → 302
- TestResidualStartLanding_UnknownJTI_StillRedirects — valid sig + unknown JTI → 302
- TestResidualStartLanding_HappyPath_Redirects — unchanged
- TestBrevoTxWebhook_SecretMismatch_AgentActionMentionsBrevo — pins API-6:
error code is brevo_secret_mismatch, agent_action mentions Brevo, does NOT
carry INSTANODE_TOKEN or the user-login recovery script
== Four-pass deploy ritual (memory rule 23) ==
- [x] Local build + vet + new tests green
- [ ] CI green (this PR)
- [ ] Auto-deploy via deploy.yml on merge
- [ ] Live verify: curl https://api.instanode.dev/healthz | jq .commit_id == merge SHA
== QA evidence ==
- /tmp/qa-session/API/start_bogus.txt + start_bogus_headers.txt — pre-fix 400 JSON
- /tmp/qa-session/API/brevo_bogus.json — pre-fix INSTANODE_TOKEN agent_action
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…_action to U3 contract Follow-up to the round-1 build-and-test failures: 1. brevo_secret_mismatch agent_action — reworded to comply with U3 contract enforced by TestAgentActionContract: must open with 'Tell the user', contain a full https://instanode.dev/ URL, and stay under 280 chars. New copy: Tell the user this is a Brevo-webhook config mismatch, not their auth. Operators must verify the Brevo dashboard webhook URL matches the configured BREVO_WEBHOOK_SECRET — see https://instanode.dev/docs/email. Still routes operators to the correct fix (Brevo dashboard webhook URL, not the user-login flow), but in the standard agent-action voice. 210 chars. 2. Existing /start tests aligned with the API-5 always-302 contract: - TestStartLanding_AlreadyClaimedRedirectsToDashboardWithFlag — now asserts 302 to /claim?t=, no longer asserts the already_claimed=true flag (dashboard handles the already-claimed UI now). - TestStartLanding_MissingTokenReturns400 — now asserts 302 with no t= query. - TestStartLanding_UnknownJTI_400 — now asserts 302 to /claim?t=. - TestOnboarding_GetStart_ExpiredJWT_Returns400LinkExpired — 302 to /claim. - TestOnboarding_GetStart_TamperedJWT_Returns400InvalidLink — 302 to /claim. - TestStartLanding_NoToken_Returns400 — 302 to /claim (no t=). - TestStartLanding_TamperedJWT_Returns400 — 302 to /claim?t=. - TestStartLanding_AlreadyClaimed_Returns302 — 302 to /claim?t= (no flag). - TestOnboarding_JWTWithFutureIssuedAt_Returns400 — 302 to /claim?t=. Test function NAMES kept verbatim for grep stability; ASSERTIONS updated to the new always-bounce contract per CLAUDE.md 'Live API surface' line. 3. TestBrevoTxWebhook_SecretMismatch_AgentActionMentionsBrevo — tightened the negative assertion to match the rephrased operator copy ('log in at https://instanode.dev/login to mint a new one' is the load-bearing piece we must NOT carry). Co-Authored-By: Claude Opus 4.7 (1M context) <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.
Two QA findings from the 2026-05-29 round (
/tmp/qa-session/shared/INBOX.md).API-5 (P2) —
GET /startalways 302sPer CLAUDE.md 'Live API surface':
'/start (302 → dashboard /claim?t=jwt)'. Previously, an invalid/missing/expired token surfaced the raw JSON envelope{"ok":false,"error":"invalid_token"}with HTTP 400./startURLs land in agents' terminal logs (upgrade: https://api.instanode.dev/start?t=...); when a user copy-pastes a stale one into a browser, they see naked JSON instead of a friendly recovery flow.Fix: pass the token through verbatim and let the dashboard's ClaimPage handle every validation case (expired / unrecognised / already-claimed / empty). The JTI lookup now happens once at
/claimtime where it's actually load-bearing — no more DB lookups on every drive-by/starthit.landing_viewedmetric still increments so the funnel pivot stays measurable.Cases:
?t→ 302 to/claimwith not=query, dashboard renders empty state./claim?t=garbage, dashboard renders 'invalid token'./claim?t=<jwt>, dashboard does the lookup./claim?t=<jwt>).API-6 (P2) — Brevo 401 agent_action targets operator
POST /webhooks/brevo/<bogus-secret>returned 401 with the canonicalunauthorizederror class — and the canonicalagent_actionfor that class (Tell the user their INSTANODE_TOKEN is missing or invalid. Have them log in at https://instanode.dev/login...). The Brevo webhook is unrelated to user auth: an agent reading the error envelope would tell the user to log in for a new INSTANODE_TOKEN, which is uselessly wrong. The actual incident is an operator-side Brevo dashboard config drift.Fix: new error class
brevo_secret_mismatchwith operator-targetedagent_actionthat tells the caller to verify the Brevo dashboard webhook URL contains the configuredBREVO_WEBHOOK_SECRET. HTTP status stays 401 — only the error CODE +agent_actioncopy change. Existing operator alerting that pivots offmetrics.BrevoWebhookEventsTotal{result="unauthorized"}is unaffected.Multi-surface checklist (memory rule 22)
api/internal/handlers/openapi.go/startcontract changed from {302,400} to {302} only;tnow optionalapi/internal/handlers/helpers.gobrevo_secret_mismatchagent_actioncontent/llms.txt/startwas already documented as 302; the dashboard-renders-error change is not customer/agent-facingMETRICS-CATALOG.mdTest coverage
TestResidualStartLanding_MissingToken_RedirectsToClaimTestResidualStartLanding_GarbageToken_StillRedirectsTestResidualStartLanding_UnknownJTI_StillRedirectsTestResidualStartLanding_HappyPath_Redirects— unchangedTestBrevoTxWebhook_SecretMismatch_AgentActionMentionsBrevo— pins API-6: error code isbrevo_secret_mismatch, body mentions Brevo, does NOT carryINSTANODE_TOKENor the user-login recovery scriptQA evidence
/tmp/qa-session/API/start_bogus.txt,start_bogus_headers.txt— pre-fix 400 JSON/tmp/qa-session/API/brevo_bogus.json— pre-fixINSTANODE_TOKENagent_action🤖 Generated with Claude Code