fix(claim): polish anonymous→hobby conversion flow (4 friction findings)#9
Merged
Merged
Conversation
Four small fixes surfaced when walking an agent through the claim flow end-to-end on 2026-05-11. None are bugs in behaviour — they're each a place where the agent or end-user has to do extra work the platform should be doing for them. #13 Provision response 'note' field said "14-day trial, then $9/mo" which contradicts the project memo: anonymous (24h TTL) is the trial; hobby/pro/team are paid from day one. Now reads "Works for 24h free. Claim to keep — from $9/mo: <url>". #14 Upgrade URLs pointed at instant.dev/start — leftover from the domain rename. Now instanode.dev/start across all 28 emission sites + every error/upgrade-link copy string. #15 /claim/preview was implemented but undocumented; agents reading openapi.json had no way to discover the no-side-effect probe ("what would I attach to a team if I claimed?"). Added the path entry + ClaimPreviewResponse schema describing the resources array. #16 Anonymous provision responses emitted 'upgrade' as a full URL like 'https://instanode.dev/start?t=<jwt>'. Agents had to string-strip the JWT out with sed to pass it back to /claim. Now also emits 'upgrade_jwt' alongside 'upgrade' on every dedup-path response (db, cache, nosql, queue, storage, webhook). ClaimRequest schema description points agents at the new field. Tests: TestOpenAPI_ClaimPreviewEndpointDocumented — guards #15 TestOpenAPI_ClaimRequestDocumentsUpgradeJWT — guards #16 Existing TestOpenAPISpecParses catches any JSON-quote regression. Live verification on v2.0.3-claim-polish in prod: POST /db/new → note: "...Claim to keep — from $9/mo: https://instanode.dev/start?t=..." upgrade: "https://instanode.dev/start?t=eyJhbGc..." upgrade_jwt: "eyJhbGc..." (raw JWT, no parsing needed) GET /openapi.json | jq .paths."/claim/preview" → present Out of scope (separate friction worth its own PR): the fresh-success response path (newly provisioned, not dedup) doesn't emit the upgrade field at all — only the note string with URL embedded. Will fix separately to keep this diff focused on the four points raised. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3 tasks
mastermanas805
added a commit
that referenced
this pull request
May 11, 2026
#10) Earlier PRs (#4, #6, #9) shipped OpenAPI schema tests but lacked handler-level behavior tests. Code reviewers flagged the gap — a schema test catches "field documented" regressions but misses "field actually emitted" or "input actually parsed" regressions. This PR backfills behavior tests for four shipped behaviors: 1. upgradeNote / limitExceededNote copy (PR #9, friction #13) TestUpgradeNote_DoesNotMentionTrial — 2 sub-cases TestLimitExceededNote_DoesNotMentionTrial — 4 sub-cases Guards: no "14-day trial" framing, contains "Claim to keep" + "$9/mo", no instant.dev/start leakage. 2. POST /api/v1/whoami (PR #6, friction #9) TestWhoami_NoTokenReturns401 — 401 on missing bearer TestWhoami_ReturnsIdentityForAuthedRequest — 200 with uid/tid claims; plan_tier enrichment when DB hit Test app now wires /api/v1/whoami so this and future tests can hit it through the full RequireAuth middleware. 3. POST /deploy/new env_vars JSON parsing (PR #4, friction #11) TestDeployNew_EnvVarsJSON_Parsed_Into_InitEnv — valid JSON merges into deployment.EnvVars; underscore-prefixed keys silently stripped (_secret never leaks) TestDeployNew_EnvVarsInvalidJSON_Returns400 — malformed JSON returns 400 error="invalid_env_vars" (not a generic 500) Includes a multipartDeployBody helper that other deploy tests can reuse without colliding with stack_test.go's name. 4. upgrade_jwt in provisioning responses (PR #9, friction #16) TestAnonymousProvisionEmitsUpgradeJWT_OnDedup — dedup response includes raw upgrade_jwt JWT (no parsing) alongside the legacy upgrade URL; the two presentations of the same token must not drift. Skips cleanly when local test DB schema lags (env column). All 10 new test cases pass against postgres:16-alpine + redis:7-alpine. Total run time <1s. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3 tasks
mastermanas805
added a commit
that referenced
this pull request
May 11, 2026
…11) Friction #17 from the claim-flow walkthrough: PR #9 added upgrade_jwt to the dedup response path but not the fresh-success (newly provisioned) path. Agents that hit a new fingerprint got the URL only inside the note string, having to regex it back out — exactly the friction upgrade_jwt was meant to remove. Adds upgrade + upgrade_jwt to the anonymous fresh-success response in: - db.go (line 257 area) - cache.go (line 257 area) - nosql.go (line 245 area) - queue.go (line 251 area) - webhook.go (line 241 area) storage.go already emitted both fields from its fresh path; left unchanged. Authenticated fresh-success paths (in *.go newDB/Cache/etc authenticated branches) do not need upgrade fields — there's nothing to claim because the team already exists. Tests: TestAnonymousProvisionEmitsUpgradeJWT_OnFreshSuccess Asserts a 201 fresh response carries both fields, that upgrade_jwt is a bare JWT (no scheme), and that upgrade is a /start?t=<jwt> URL. Live verification on v2.0.4-fresh-success-upgrade in prod: $ curl -H "X-E2E-Test-Token: <secret>" \ -H "X-E2E-Source-IP: 192.0.2.117" \ -X POST https://api.instanode.dev/db/new -d '{}' → 201 (fresh, not 200 dedup) tier: "anonymous" note: "Works for 24h free. Claim to keep — from $9/mo: ..." upgrade: "https://instanode.dev/start?t=ey..." upgrade_jwt: "ey..." (bare JWT, no parsing needed) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
mastermanas805
added a commit
that referenced
this pull request
May 11, 2026
The dashboard's BillingPage + AppShell read `tier` and `email`
from /api/v1/whoami. Until this commit the handler only returned
`plan_tier` (under a different field name) and never returned the
email at all. As a result:
- The sidebar in /app showed '—' as the tier
- The 'aanya@example.com' avatar tooltip was either blank or the
fixture default
- Billing-page banners that depend on tier worked only because the
BillingPage hits /api/v1/billing separately
Found by running the end-to-end real-customer flow test:
POST /db/new → anonymous Postgres token
POST /claim {jwt,email} → team + session_token
GET /api/v1/whoami → ok:true, tier:null,
email:null ← bug
GET /api/v1/billing → ok:true, tier:hobby ← correct
This commit:
- Adds `tier` to the response (alias of `plan_tier`; kept both for
backward compat — some agents already key off `plan_tier`)
- Adds `email` by looking up the user record via models.GetUserByID
on the auth'd user_id from the JWT
go build clean. The OpenAPI `/api/v1/whoami` description already
includes `team_id + plan_tier` per friction-point #9; the new
fields are additive.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
mastermanas805
added a commit
that referenced
this pull request
May 17, 2026
Wave 4 of the 2026-05-17 bug hunt — P2 contract/security/doc fixes. Auth / session security: - CanonicalResourceURLFor + dpop.requestCanonicalURL no longer derive the canonical host from client-settable X-Forwarded-Host/-Proto headers — the RFC 8707 audience check and RFC 9449 DPoP htu check now resolve the host from API_PUBLIC_URL or the compiled default only. - OptionalAuth now runs the JTI-revocation check RequireAuth does; a revoked token drops the credential and continues anonymous. - signSessionJWT + the impersonation mint now stamp `aud` (canonical resource URL) so the opt-in audience check actually applies to session JWTs. plans.Registry instead of hardcoded limits (convention #3): - dbAnonymousLimits / vectorAnonymousLimits / webhookAnonLimits / queueAnonymousLimits are now handler methods reading plans.Registry. Quota / storage: - cache.StorageBytes raised the 1000-key SCAN cap to 200k with COUNT 500 and a truncation warning — no longer silently under-counts a tenant's quota. - quota.LimitBytes is the single MB->bytes conversion point (MiB, matching CheckStorageQuota); resourceToMap used SI 1_000_000 and under-stated the wall ~4.8%. Updated resource_limits_test.go to MiB. API contract / OpenAPI: - Provision response schemas (db/vector/cache/nosql/queue/webhook/storage) gained id/name/env/env_override_reason; upgrade_url renamed to `upgrade` to match what the handlers actually emit. - Recycle-gate 402 routed through the canonical ErrorResponse envelope (request_id + retry_after_seconds + claim_url) via respondRecycleGate. Billing: - ChangePlanAPI rejects self-serve downgrades (target rank <= current rank) with 400 + mailto-support agent_action — enforces the no-self-serve- downgrade policy. - Added billing_inr_drift_test.go: anti-drift guard for monthlyAmountINRForTier against plans.yaml prices. Stacks / lifecycle: - Redeploy + UpdateEnv reject stacks in `deleting` status with 409. - POST /stacks/new now accepts an optional validated `env` form field (defaults to EnvDefault) — matches the resource/deploy env contract. - Confirmed-deletion teardown failure now returns deletion_status=confirmed_teardown_pending instead of a false "torn down". Doc-comment / model: - 403-vs-404 doc drift corrected in resource_family.go, resource_metrics.go, backup.go (code already returns 404). - Custom domain: cert_ready documented as the terminal state; the never- implemented cert_ready -> live transition dropped from the docs and the `live` constant marked back-compat-only. Skipped: #9 (unknown /api/v1/* -> 401 not 404) — left as-is; 401 for an unauthenticated caller on any path is a deliberate posture (do not reveal route existence). Pre-existing unrelated failure TestAdminList_AdminUserSees200 is not touched by this change. go build ./... and go vet ./... clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
mastermanas805
added a commit
that referenced
this pull request
May 17, 2026
…ning Round-3 P2 remediation across deploy/stack/webhook/auth surfaces. 1. deploy.go Redeploy now rejects a deployment in a terminal status (expired/deleted/stopped) with 409 + error code `deployment_not_redeployable` — redeploying one would resurrect an over-TTL/over-cap workload. New models.IsDeploymentTerminal + DeployStatusStopped const. 2. stack.go Redeploy re-runs the per-tier deployments_apps cap check when the stack is NOT in an active (slot-occupying) status — a failed/stopped stack flipping back to `building` could take a team to cap+1. New models.IsStackActive. 3. stack.go empty-env vault fallback changed from "production" to models.EnvDefault (development) at both new + redeploy sites — convention #11: a no-env legacy stack must not silently read production secrets. 4. deploy_teardown_reconciler.go increments a new metrics.DeployTeardownMarkFailed counter when MarkDeploymentTornDown fails — a persistently stuck row is now alertable in NR, not a silent log. 5. auth.go findOrCreateUserGitHub now matches an existing account by email (GetUserByEmail) and links github_id via new models.LinkGitHubID instead of forking a new team/user — mirrors findOrCreateUserGoogle and rejects takeover of an account already linked to a different GitHub ID. The /user/emails fallback now filters on Verified && Primary. 6. (already correct) models.CreateUser already routes email through NormalizeEmail at the write boundary — every OAuth/magic-link/claim call site is covered. No change needed; verified. 7. webhook.go receive_url is now built from a fixed server-controlled base (new webhookReceiveBaseURL: API_PUBLIC_URL / compiled-in base; c.BaseURL() only as a non-production dev fallback) instead of the client-controllable Host header. The URL is encrypted + persisted, so a client-settable host was a persistence-poisoning vector. 8. webhook.go Receive + ListRequests reject any non-webhook resource token with 404 — GetResourceByToken selects by token only, so a postgres/redis token previously passed. 9. auth.go GoogleAuthURL drops the impossible url.Parse-error 500 branch (the argument is a compile-time constant) — matches GoogleStart. Regression tests: models/redeploy_guard_test.go (IsDeploymentTerminal, IsStackActive), models/link_github_id_test.go (LinkGitHubID), handlers/webhook_receive_base_url_test.go (#7), handlers/p2_roundup_test.go (#1 error code, #4 metric, #9 GoogleAuthURL), and a wrong-resource-type case appended to handlers/webhook_test.go (#8). go build ./... and go vet ./... pass. New no-DB regression tests pass; the DB/Redis-backed suites require a test Postgres/Redis (unavailable in this environment). 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.
Summary
Four small fixes surfaced when walking an agent through the claim flow end-to-end on 2026-05-11. None are bugs — each is a place where the platform was making the agent or end-user do extra work.
Live verification on v2.0.3-claim-polish in prod
```
$ curl -X POST https://api.instanode.dev/db/new -d '{}'
note: "Returning your existing resource. Expires in 20h 43m.
Claim to keep — from $9/mo: https://instanode.dev/start?t=..."
upgrade: "https://instanode.dev/start?t=eyJhbGc..."
upgrade_jwt: "eyJhbGc..."
$ curl https://api.instanode.dev/openapi.json | jq '.paths."/claim/preview".get.summary'
"Preview which resources a claim would attach"
```
Test plan
Out of scope
Fresh-success responses (newly provisioned, not dedup) don't emit `upgrade` at all — only the note string with URL embedded. Will fix in a separate PR to keep this diff focused.
🤖 Generated with Claude Code