fix: emit upgrade + upgrade_jwt on anonymous fresh-success responses#11
Merged
Merged
Conversation
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>
7 tasks
mastermanas805
added a commit
that referenced
this pull request
May 17, 2026
P1-A: fingerprint daily provision cap (5/day) was bypassable by mixing service types — the dedup-recovery query was type-scoped, so an abuser could provision 5x EACH service type/day. It also ignored `env`, leaking a `production` resource to a `development` caller (defeats migration 026 / convention #11). models.GetActiveResourceByFingerprintType now takes an `env` arg and filters on it; added models.GetActiveResourceByFingerprint as a cross-service fallback. All 7 call sites (db, cache, nosql, queue, storage, webhook, vector) now: pass env, and on no-same-type-match do a cross-service check — if any anonymous resource exists for the fingerprint+env the cap is spent, return 429 instead of provisioning. P1-B: anonymous /storage/new never enforced the tier byte cap (advertised 10MB anon cap unenforced). Added models.SumStorageBytesByFingerprintAndType and a quota check on the anonymous storage path mirroring the authenticated path, scoped to the fingerprint. Lags real usage by one worker scanner tick (storage_bytes is worker-populated); fails open. P1-C: expired anonymous webhooks kept accepting payloads forever — Receive checked status but not expires_at, and each receive re-extended the Redis-list TTL. Receive and ListRequests now return 410 Gone when resource.ExpiresAt is valid and in the past. P1-D: nosql provisioning advertised `connections: <tier limit>` as a per-token guarantee, but MongoDB enforces no per-user connection cap. The authenticated response now exposes connections_informational + connections_note instead of a false enforced-limit claim. P1-E: Redis ACL username (api local backend) was derived from the 8-char token prefix, so two tokens sharing 8 hex chars collided on one ACL user (SETUSER overwrite). aclUsername now uses the FULL token, matching the key-prefix; added legacyACLUsername helper so an old-scheme user is still locatable for deletion. 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
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 hitting a brand-new fingerprint got the URL only inside the note text, having to regex it back out.
Adds `upgrade` + `upgrade_jwt` to the anonymous fresh-success response in db / cache / nosql / queue / webhook. storage.go already had them. Authenticated fresh-success paths don't need them (nothing to claim).
Live verification (before this PR)
```
$ curl -H "X-E2E-Test-Token: "
-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)
```
Test plan
🤖 Generated with Claude Code