Skip to content

Commit 19c2cfb

Browse files
claudemastermanas805
authored andcommitted
feat(api): populate claim_url on every 201 anon provision response (DOG-21)
DOG-21 — marketing landing page L106-109 promises "the agent surfaces the claim card directly in chat" with a clickable claim_url, but on the live 201 response /db/new (and every sibling) returned `claim_url: null`. Agents had to hand-construct `https://api.instanode.dev/start?t=<upgrade_jwt>` from the upgrade_jwt field — pushing composition responsibility into every vendor integration and producing different broken links per integration. Fix: emit `claim_url` alongside `upgrade` / `upgrade_jwt` at every 201 anonymous provision site. Same URL semantically (the /start?t=<jwt> page handles both claim and upgrade flows); distinct field signals intent so an agent can render a "Claim with email" CTA without parsing the upgrade URL from upgrade_jwt. Surfaces covered (rule 22): - db.go dedup branch + fresh-201 (2 sites) - cache.go dedup branch + fresh-201 (2 sites) - nosql.go dedup branch + fresh-201 (2 sites) - queue.go dedup branch + fresh-201 (2 sites) - vector.go dedup branch + fresh-201 (2 sites) - webhook.go dedup branch + fresh-201 (2 sites) - storage.go dedup branch + fresh-201 (2 sites) Total: 14 anon-provision emit sites — all carry claim_url. - openapi.go all 7 response schemas + ErrorResponse description widened to cover the 201-anon case (the pre-fix doc had it only on 402 recycle-gate, which is the documentation gap that produced the original report). What did NOT change: - upgrade / upgrade_jwt fields unchanged — back-compat preserved - Same URL as upgrade — no new endpoint, no new minting - Authenticated paths unchanged (no anon claim flow there) Coverage block: Symptom: claim_url:null on 201 /db/new despite marketing promise Enumeration: rg -n '"upgrade":[[:space:]]+upgradeURL,' internal/handlers/ Sites found: 14 (7 dedup + 7 fresh-201) Sites touched: 14 / 14 Coverage test: TestDOG21_ClaimURLEmittedOnEveryAnonProvision iterates the live anonProvisionHandlerFiles registry (rule 18 — registry- iterating, not hand-typed) and asserts claim_url count >= upgrade_jwt count per file; TestDOG21_OpenAPISchemaDocumentsClaimURL asserts ErrorResponse.claim_url description reflects the new 201 emit (rule 22 surface checklist). Live verified: pre-merge: see DOG-21 in personal-dogfood-log.md ("claim_url: null in the response"). post-merge curl below. Local gate: - go build ./... PASS - go vet ./... PASS - go test -short -run TestOpenAPI ./internal/handlers/ PASS (all 18 pass) - go test -short -run TestDOG21 ./internal/handlers/ PASS - go test -short -run Recycle ./internal/handlers/ PASS Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 7dbed94 commit 19c2cfb

9 files changed

Lines changed: 124 additions & 8 deletions

File tree

internal/handlers/cache.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ func (h *CacheHandler) NewCache(c *fiber.Ctx) error {
172172
"note": limitExceededNote(upgradeURL, existing.ExpiresAt.Time),
173173
"upgrade": upgradeURL,
174174
"upgrade_jwt": jwtToken,
175+
"claim_url": upgradeURL, // DOG-21: see db.go
175176
}
176177
setInternalURL(dedupResp, existing.Tier, connectionURL, "redis")
177178
if existing.KeyPrefix.String != "" {
@@ -289,6 +290,7 @@ func (h *CacheHandler) NewCache(c *fiber.Ctx) error {
289290
"note": upgradeNote(upgradeURL),
290291
"upgrade": upgradeURL,
291292
"upgrade_jwt": jwtToken,
293+
"claim_url": upgradeURL, // DOG-21: see dedup branch above
292294
}
293295
// T19 P0-2 (BugHunt 2026-05-20): emit top-level expires_at for
294296
// shape parity with storage/webhook responses; see db.go for rationale.

internal/handlers/db.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,12 @@ func (h *DBHandler) NewDB(c *fiber.Ctx) error {
208208
"note": limitExceededNote(upgradeURL, existing.ExpiresAt.Time),
209209
"upgrade": upgradeURL,
210210
"upgrade_jwt": jwtToken,
211+
// DOG-21 (QA 2026-05-29): emit claim_url alongside upgrade
212+
// so agents have a labelled link for the email-claim flow.
213+
// Same URL (the /start?t=<jwt> page handles both claim and
214+
// upgrade); distinct field signals intent to the calling
215+
// agent, matching the documented OpenAPI response schema.
216+
"claim_url": upgradeURL,
211217
}
212218
setInternalURL(dedupResp, existing.Tier, connectionURL, "postgres")
213219
return respondOK(c, dedupResp)
@@ -331,6 +337,7 @@ func (h *DBHandler) NewDB(c *fiber.Ctx) error {
331337
"note": upgradeNote(upgradeURL),
332338
"upgrade": upgradeURL,
333339
"upgrade_jwt": jwtToken,
340+
"claim_url": upgradeURL, // DOG-21: see dedup branch above
334341
}
335342
// T19 P0-2 (BugHunt 2026-05-20): unify the TTL contract across all
336343
// provisioning endpoints — storage/webhook already emit a top-level
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package handlers
2+
3+
// dog21_claim_url_test.go — DOG-21 regression.
4+
//
5+
// Every anonymous-tier 201 provision response (db/cache/nosql/queue/
6+
// vector/webhook/storage) MUST emit a top-level `claim_url` alongside
7+
// `upgrade` / `upgrade_jwt`. Pre-fix the field was documented on the
8+
// 402 recycle-gate envelope but absent on the 201, so agents that
9+
// wanted to surface a claim CTA had to hand-construct the URL from
10+
// upgrade_jwt — breaking the contract across vendor integrations.
11+
//
12+
// Source-level regression so we don't need a live Postgres/Redis to
13+
// pin the contract. The full integration test path lives in
14+
// existing service tests (provarms_test.go etc.).
15+
16+
import (
17+
"os"
18+
"path/filepath"
19+
"strings"
20+
"testing"
21+
22+
"github.com/stretchr/testify/assert"
23+
"github.com/stretchr/testify/require"
24+
)
25+
26+
// anonProvisionHandlerFiles lists every file that emits a 201 anonymous
27+
// provision response. Iterating this list (rule 18 — registry not
28+
// hand-typed assertions) means a new service handler added to the same
29+
// shape automatically gets the same coverage requirement.
30+
var anonProvisionHandlerFiles = []string{
31+
"db.go",
32+
"cache.go",
33+
"nosql.go",
34+
"queue.go",
35+
"vector.go",
36+
"webhook.go",
37+
"storage.go",
38+
}
39+
40+
// TestDOG21_ClaimURLEmittedOnEveryAnonProvision asserts every anon
41+
// provision handler file emits at least one `claim_url` field. The
42+
// number of expected emissions per file is at least the number of
43+
// `upgrade_jwt` emissions in that file (one per code path: the
44+
// fingerprint-dedup branch and the fresh-201 branch each emit one).
45+
func TestDOG21_ClaimURLEmittedOnEveryAnonProvision(t *testing.T) {
46+
for _, fname := range anonProvisionHandlerFiles {
47+
t.Run(fname, func(t *testing.T) {
48+
path := filepath.Join(".", fname)
49+
raw, err := os.ReadFile(path)
50+
require.NoError(t, err, "must read handler file %s", path)
51+
src := string(raw)
52+
53+
// Every site that emits upgrade_jwt is an anon-201 site. The
54+
// pre-fix code emitted upgrade_jwt without claim_url. The fix
55+
// requires claim_url at every such site.
56+
jwtCount := strings.Count(src, "upgrade_jwt")
57+
claimCount := strings.Count(src, "claim_url")
58+
59+
require.Greater(t, jwtCount, 0,
60+
"DOG-21: %s should still have anon-provision code paths that emit upgrade_jwt", fname)
61+
assert.GreaterOrEqual(t, claimCount, jwtCount,
62+
"DOG-21: %s emits upgrade_jwt %d time(s) but claim_url only %d time(s) — every 201 anon response must carry claim_url",
63+
fname, jwtCount, claimCount)
64+
})
65+
}
66+
}
67+
68+
// TestDOG21_OpenAPISchemaDocumentsClaimURLOnEveryAnonResponse pins
69+
// the rule-22 surface checklist: every provision-response schema in
70+
// openapi.go must describe the new claim_url field.
71+
func TestDOG21_OpenAPISchemaDocumentsClaimURL(t *testing.T) {
72+
raw, err := os.ReadFile("openapi.go")
73+
require.NoError(t, err)
74+
src := string(raw)
75+
76+
// At least 7 claim_url description lines (one per service schema +
77+
// the ErrorResponse schema = 8 minimum). We use 7 as the floor
78+
// because counting in openapi.go is fragile against future renames;
79+
// 7 catches the "added to only some schemas" failure mode without
80+
// flaking on cosmetic JSON splitting.
81+
claimURLDescCount := strings.Count(src, `"claim_url":`)
82+
assert.GreaterOrEqual(t, claimURLDescCount, 7,
83+
"DOG-21: openapi.go must document claim_url across all 7 provision-response schemas + ErrorResponse (rule 22 surface checklist)")
84+
85+
// The widened ErrorResponse description must call out the new 201 emit
86+
// surface so an agent reading the doc knows to expect claim_url on
87+
// success too, not only the recycle-gate 402.
88+
assert.Contains(t, src, "ALSO emitted on every successful 201 anonymous provision",
89+
"DOG-21: ErrorResponse.claim_url description must reflect the 201-anon emit (the documentation gap that caused the original report)")
90+
}

internal/handlers/nosql.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ func (h *NoSQLHandler) NewNoSQL(c *fiber.Ctx) error {
171171
"note": limitExceededNote(upgradeURL, existing.ExpiresAt.Time),
172172
"upgrade": upgradeURL,
173173
"upgrade_jwt": jwtToken,
174+
"claim_url": upgradeURL, // DOG-21: see db.go
174175
}
175176
setInternalURL(dedupResp, existing.Tier, connectionURL, "mongodb")
176177
return respondOK(c, dedupResp)
@@ -285,6 +286,7 @@ func (h *NoSQLHandler) NewNoSQL(c *fiber.Ctx) error {
285286
"note": upgradeNote(upgradeURL),
286287
"upgrade": upgradeURL,
287288
"upgrade_jwt": jwtToken,
289+
"claim_url": upgradeURL, // DOG-21: see dedup branch above
288290
}
289291
// T19 P0-2 (BugHunt 2026-05-20): emit top-level expires_at for
290292
// shape parity with storage/webhook responses; see db.go for rationale.

internal/handlers/openapi.go

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2644,7 +2644,8 @@ const openAPISpec = `{
26442644
"warning": { "type": "string", "description": "Present only when the resource is already over its storage limit at provision time — accompanied by the X-Instant-Notice: storage_limit_reached response header." },
26452645
"note": { "type": "string" },
26462646
"upgrade_jwt": { "type": "string", "description": "Anonymous-tier only. Signed JWT the agent can POST to /claim with an email to convert the anonymous resource into a claimed (authenticated) one — no need to string-parse the upgrade URL. Absent on authenticated provisions." },
2647-
"upgrade": { "type": "string", "format": "uri", "description": "Anonymous-tier only. Pre-baked GET /start?t=<upgrade_jwt> URL the agent can hand to the user to drive the dashboard claim flow." }
2647+
"upgrade": { "type": "string", "format": "uri", "description": "Anonymous-tier only. Pre-baked GET /start?t=<upgrade_jwt> URL the agent can hand to the user to drive the dashboard claim flow." },
2648+
"claim_url": { "type": "string", "format": "uri", "description": "Anonymous-tier only. DOG-21: same URL as 'upgrade', but distinct field signals the email-claim flow to the calling agent (vs upgrade-to-paid). Agents can render this as a claim CTA directly without constructing the URL from upgrade_jwt." }
26482649
}
26492650
},
26502651
"VectorProvisionRequest": {
@@ -2677,7 +2678,8 @@ const openAPISpec = `{
26772678
"warning": { "type": "string", "description": "Present only when the resource is already over its storage limit at provision time — accompanied by the X-Instant-Notice: storage_limit_reached response header." },
26782679
"note": { "type": "string" },
26792680
"upgrade_jwt": { "type": "string", "description": "Anonymous-tier only. Signed JWT the agent can POST to /claim with an email. Absent on authenticated provisions." },
2680-
"upgrade": { "type": "string", "format": "uri", "description": "Anonymous-tier only. Pre-baked GET /start?t=<upgrade_jwt> URL for the dashboard claim flow." }
2681+
"upgrade": { "type": "string", "format": "uri", "description": "Anonymous-tier only. Pre-baked GET /start?t=<upgrade_jwt> URL for the dashboard claim flow." },
2682+
"claim_url": { "type": "string", "format": "uri", "description": "Anonymous-tier only. DOG-21: same URL as 'upgrade', but distinct field signals the email-claim flow to the calling agent (vs upgrade-to-paid). Agents can render this as a claim CTA directly without constructing the URL from upgrade_jwt." }
26812683
}
26822684
},
26832685
"CacheProvisionResponse": {
@@ -2699,7 +2701,8 @@ const openAPISpec = `{
26992701
"warning": { "type": "string", "description": "Present only when the resource is already over its storage limit at provision time — accompanied by the X-Instant-Notice: storage_limit_reached response header." },
27002702
"note": { "type": "string" },
27012703
"upgrade_jwt": { "type": "string", "description": "Anonymous-tier only. Signed JWT the agent can POST to /claim with an email. Absent on authenticated provisions." },
2702-
"upgrade": { "type": "string", "format": "uri", "description": "Anonymous-tier only. Pre-baked GET /start?t=<upgrade_jwt> URL for the dashboard claim flow." }
2704+
"upgrade": { "type": "string", "format": "uri", "description": "Anonymous-tier only. Pre-baked GET /start?t=<upgrade_jwt> URL for the dashboard claim flow." },
2705+
"claim_url": { "type": "string", "format": "uri", "description": "Anonymous-tier only. DOG-21: same URL as 'upgrade', but distinct field signals the email-claim flow to the calling agent (vs upgrade-to-paid). Agents can render this as a claim CTA directly without constructing the URL from upgrade_jwt." }
27032706
}
27042707
},
27052708
"NoSQLProvisionResponse": {
@@ -2720,7 +2723,8 @@ const openAPISpec = `{
27202723
"warning": { "type": "string", "description": "Present only when the resource is already over its storage limit at provision time — accompanied by the X-Instant-Notice: storage_limit_reached response header." },
27212724
"note": { "type": "string" },
27222725
"upgrade_jwt": { "type": "string", "description": "Anonymous-tier only. Signed JWT the agent can POST to /claim with an email. Absent on authenticated provisions." },
2723-
"upgrade": { "type": "string", "format": "uri", "description": "Anonymous-tier only. Pre-baked GET /start?t=<upgrade_jwt> URL for the dashboard claim flow." }
2726+
"upgrade": { "type": "string", "format": "uri", "description": "Anonymous-tier only. Pre-baked GET /start?t=<upgrade_jwt> URL for the dashboard claim flow." },
2727+
"claim_url": { "type": "string", "format": "uri", "description": "Anonymous-tier only. DOG-21: same URL as 'upgrade', but distinct field signals the email-claim flow to the calling agent (vs upgrade-to-paid). Agents can render this as a claim CTA directly without constructing the URL from upgrade_jwt." }
27242728
}
27252729
},
27262730
"QueueProvisionResponse": {
@@ -2754,7 +2758,8 @@ const openAPISpec = `{
27542758
"dedicated": { "type": "boolean", "description": "True when the resource was provisioned on dedicated (single-tenant) infrastructure rather than the shared pool." },
27552759
"note": { "type": "string" },
27562760
"upgrade_jwt": { "type": "string", "description": "Anonymous-tier only. Signed JWT the agent can POST to /claim with an email. Absent on authenticated provisions." },
2757-
"upgrade": { "type": "string", "format": "uri", "description": "Anonymous-tier only. Pre-baked GET /start?t=<upgrade_jwt> URL for the dashboard claim flow." }
2761+
"upgrade": { "type": "string", "format": "uri", "description": "Anonymous-tier only. Pre-baked GET /start?t=<upgrade_jwt> URL for the dashboard claim flow." },
2762+
"claim_url": { "type": "string", "format": "uri", "description": "Anonymous-tier only. DOG-21: same URL as 'upgrade', but distinct field signals the email-claim flow to the calling agent (vs upgrade-to-paid). Agents can render this as a claim CTA directly without constructing the URL from upgrade_jwt." }
27582763
}
27592764
},
27602765
"WebhookProvisionResponse": {
@@ -2772,7 +2777,8 @@ const openAPISpec = `{
27722777
"expires_at": { "type": "string", "format": "date-time" },
27732778
"note": { "type": "string" },
27742779
"upgrade_jwt": { "type": "string", "description": "Anonymous-tier only. Signed JWT the agent can POST to /claim with an email. Absent on authenticated provisions." },
2775-
"upgrade": { "type": "string", "format": "uri", "description": "Anonymous-tier only. Pre-baked GET /start?t=<upgrade_jwt> URL for the dashboard claim flow." }
2780+
"upgrade": { "type": "string", "format": "uri", "description": "Anonymous-tier only. Pre-baked GET /start?t=<upgrade_jwt> URL for the dashboard claim flow." },
2781+
"claim_url": { "type": "string", "format": "uri", "description": "Anonymous-tier only. DOG-21: same URL as 'upgrade', but distinct field signals the email-claim flow to the calling agent (vs upgrade-to-paid). Agents can render this as a claim CTA directly without constructing the URL from upgrade_jwt." }
27762782
}
27772783
},
27782784
"StorageProvisionResponse": {
@@ -2802,7 +2808,8 @@ const openAPISpec = `{
28022808
"note": { "type": "string", "description": "Anonymous-tier upgrade hint emitted on the 201 happy path (T19 P1-5, BugHunt 2026-05-20). Was previously undocumented; the schema only listed credentials_note which only appears on the dedup path." },
28032809
"credentials_note": { "type": "string", "description": "Present only on the rate-limited anonymous dedup response, where access_key_id/secret_access_key are NOT re-emitted (the secret is minted once at provision time and never stored)." },
28042810
"upgrade_jwt": { "type": "string", "description": "Anonymous-tier only. Signed JWT the agent can POST to /claim with an email. Absent on authenticated provisions." },
2805-
"upgrade": { "type": "string", "format": "uri", "description": "Anonymous-tier only. Pre-baked GET /start?t=<upgrade_jwt> URL for the dashboard claim flow." }
2811+
"upgrade": { "type": "string", "format": "uri", "description": "Anonymous-tier only. Pre-baked GET /start?t=<upgrade_jwt> URL for the dashboard claim flow." },
2812+
"claim_url": { "type": "string", "format": "uri", "description": "Anonymous-tier only. DOG-21: same URL as 'upgrade', but distinct field signals the email-claim flow to the calling agent (vs upgrade-to-paid). Agents can render this as a claim CTA directly without constructing the URL from upgrade_jwt." }
28062813
}
28072814
},
28082815
"DeployItem": {
@@ -3193,7 +3200,7 @@ const openAPISpec = `{
31933200
"retry_after_seconds": { "type": ["integer", "null"], "description": "Seconds the agent should wait before retrying. null on 4xx (no retry — fix the request). int on transient 5xx: 30 for 503, 60 for 429, 10 for 502/504. For 429/502/503/504 the same value is also set in the Retry-After HTTP header." },
31943201
"agent_action": { "type": "string", "description": "Optional. A sentence the calling agent should surface verbatim to the human user — e.g. 'Tell the user they've hit the hobby tier storage limit (500MB). Have them upgrade at https://instanode.dev/pricing to provision more storage.' Present on quota walls, invalid-token errors, permission-denied errors, expired-resource errors, tier-gate errors, AND on plumbing 5xx (where it falls back to a generic 'email support with this request_id' sentence)." },
31953202
"upgrade_url": { "type": "string", "format": "uri", "description": "Optional. Where the user can resolve the error — typically the pricing/upgrade page for quota walls and the login page for token errors. Present whenever following the URL would clear the error." },
3196-
"claim_url": { "type": "string", "format": "uri", "description": "Optional. Present specifically on error='free_tier_recycle_requires_claim' (402 from /db/new, /cache/new, /nosql/new, /queue/new, /storage/new, /webhook/new): the URL the anonymous caller should visit to claim their existing resources with email before they can provision again. Distinct from upgrade_url — claim_url is about identity (anonymous → claimed), upgrade_url is about tier (claimed → paid). Both may be present on the same envelope." }
3203+
"claim_url": { "type": "string", "format": "uri", "description": "Optional. Present on error='free_tier_recycle_requires_claim' (402 from /db/new, /cache/new, /nosql/new, /queue/new, /storage/new, /webhook/new, /vector/new): the URL the anonymous caller should visit to claim their existing resources with email before they can provision again. DOG-21 (QA 2026-05-29): ALSO emitted on every successful 201 anonymous provision response under each service's response schema — agents can surface a claim CTA on first provision instead of waiting for the recycle gate. Distinct from upgrade_url — claim_url is about identity (anonymous → claimed), upgrade_url is about tier (claimed → paid). Both may be present on the same envelope." }
31973204
},
31983205
"required": ["ok", "error", "message", "retry_after_seconds"]
31993206
},

internal/handlers/queue.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@ func (h *QueueHandler) NewQueue(c *fiber.Ctx) error {
251251
"note": limitExceededNote(upgradeURL, existing.ExpiresAt.Time),
252252
"upgrade": upgradeURL,
253253
"upgrade_jwt": jwtToken,
254+
"claim_url": upgradeURL, // DOG-21: see db.go
254255
}
255256
setInternalURL(dedupResp, existing.Tier, connectionURL, "queue")
256257
return respondOK(c, dedupResp)
@@ -382,6 +383,7 @@ func (h *QueueHandler) NewQueue(c *fiber.Ctx) error {
382383
"note": upgradeNote(upgradeURL),
383384
"upgrade": upgradeURL,
384385
"upgrade_jwt": jwtToken,
386+
"claim_url": upgradeURL, // DOG-21: see dedup branch above
385387
}
386388
// MR-P0-5: when isolated creds are minted, surface them. Tenant clients
387389
// pass nats_jwt + nats_nkey to nats.UserJWTAndSeed(), or write the

internal/handlers/storage.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,7 @@ func (h *StorageHandler) NewStorage(c *fiber.Ctx) error {
232232
"note": limitExceededNote(upgradeURL, existing.ExpiresAt.Time),
233233
"upgrade": upgradeURL,
234234
"upgrade_jwt": jwtToken,
235+
"claim_url": upgradeURL, // DOG-21: see db.go
235236
"expires_at": existing.ExpiresAt.Time.Format(time.RFC3339),
236237
}
237238
// P2-05: the S3 prefix is recoverable from the persisted
@@ -389,6 +390,7 @@ func (h *StorageHandler) NewStorage(c *fiber.Ctx) error {
389390
resp["note"] = upgradeNote(upgradeURL)
390391
resp["upgrade"] = upgradeURL
391392
resp["upgrade_jwt"] = jwtToken
393+
resp["claim_url"] = upgradeURL // DOG-21: see dedup branch above
392394
resp["expires_at"] = expiresAt.Format(time.RFC3339)
393395
resp["limits"] = h.storageAnonymousLimits()
394396
return respondCreated(c, resp)

internal/handlers/vector.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,7 @@ func (h *VectorHandler) NewVector(c *fiber.Ctx) error {
318318
"note": limitExceededNote(upgradeURL, existing.ExpiresAt.Time),
319319
"upgrade": upgradeURL,
320320
"upgrade_jwt": jwtToken,
321+
"claim_url": upgradeURL, // DOG-21: see db.go
321322
}
322323
setInternalURL(dedupResp, existing.Tier, connectionURL, "postgres")
323324
return respondOK(c, dedupResp)
@@ -428,6 +429,7 @@ func (h *VectorHandler) NewVector(c *fiber.Ctx) error {
428429
"note": upgradeNote(upgradeURL),
429430
"upgrade": upgradeURL,
430431
"upgrade_jwt": jwtToken,
432+
"claim_url": upgradeURL, // DOG-21: see dedup branch above
431433
}
432434
// T19 P0-2 (BugHunt 2026-05-20): emit top-level expires_at for
433435
// shape parity with storage/webhook responses; see db.go for rationale.

0 commit comments

Comments
 (0)