You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
docs(openapi): add /auth/exchange — closes the gap that hid 2026-05-30 outage (#203)
* docs(openapi): add /auth/exchange — closes contract gap that hid 2026-05-30 outage
The 2026-05-30 prod-login outage chained 3 failures along /auth/exchange
(client missing, preflight rejected, ACAC missing). Even with the new
contract-CI gate (api #202), the bug class wasn't catchable because
/auth/exchange was literally NOT in the OpenAPI spec — agents and the
typed-client codegen pipeline had no contract to enforce.
Adds the path with the full CORS contract documented in the description
so any future regression to the headers / preflight rules is visible in
the diff that breaks the snapshot-drift gate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(openapi): drop redeploy_requires_name from DeployRequest description
That error code was emitted by an unreachable defence-in-depth arm that
landed in api#201 and was removed before merge (requireName already
rejects empty/whitespace name upstream of the redeploy branch). The
DeployRequest description still mentioned it; tidied + clarified the
upstream-rejection path.
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: internal/handlers/openapi.go
+14-1Lines changed: 14 additions & 1 deletion
Original file line number
Diff line number
Diff line change
@@ -1623,6 +1623,19 @@ const openAPISpec = `{
1623
1623
}
1624
1624
}
1625
1625
},
1626
+
"/auth/exchange": {
1627
+
"post": {
1628
+
"summary": "Exchange the AUTH-004 bridge cookie for a session JWT",
1629
+
"description": "Final leg of the AUTH-004 cross-origin sign-in handshake. The /auth/email/callback and /auth/github/callback handlers set a short-lived HttpOnly auth_exchange_cookie and 302 to https://instanode.dev/login/callback?signed_in=1. The dashboard then makes a credentials:include POST to this endpoint. CORS contract: response MUST include Access-Control-Allow-Origin: https://instanode.dev AND Access-Control-Allow-Credentials: true — the browser blocks the read otherwise. Request MUST be a CORS-simple POST (no custom headers like Accept: application/json), since adding one forces a preflight that PreflightAllowlist may reject. Returns 200 + the bearer JWT (24h, HS256, aud=https://api.instanode.dev) on success. The 2026-05-29 to 2026-05-30 prod-login outage chained three failures along this exact endpoint — documenting it here so any future regression is catchable by the cross-stack contract gate (api PR #202).",
1630
+
"requestBody": { "required": false, "description": "No body. The bridge cookie travels in the Cookie header via credentials:include." },
1631
+
"responses": {
1632
+
"200": { "description": "Cookie verified; JWT minted", "content": { "application/json": { "schema": { "type": "object", "required": ["ok", "token"], "properties": { "ok": { "type": "boolean" }, "token": { "type": "string", "description": "Session JWT — store in localStorage and send as Authorization: Bearer for /api/v1/* calls" } } } } } },
"summary": "Consume a magic link, mint a session JWT, 302 to <return_to>",
@@ -3090,7 +3103,7 @@ const openAPISpec = `{
3090
3103
"notify_webhook": { "type": "string", "description": "Optional https:// URL fired by POST when the deploy reaches a terminal state (status='healthy' or 'failed'). Lets callers subscribe instead of polling GET /deploy/:id. Rejected with 400 + agent_action if the URL is not https, the hostname is unresolvable, or resolves to a private/loopback/link-local/CGNAT IP (SSRF protection). Payload shape: { event: 'deploy.healthy' | 'deploy.failed', deploy_id, app_id, url, commit_id, build_time, duration_s, error_message? }. 2xx → notify_state='sent'; 4xx → 'failed' (no retry — user URL is broken); 5xx/network → up to 3 retries, then 'failed'." },
3091
3104
"notify_webhook_secret": { "type": "string", "description": "Optional HMAC-SHA256 signing key. When set, every dispatch includes an X-InstaNode-Signature: sha256=<hex(hmac(secret, body))> header. Stored AES-256-GCM encrypted; plaintext never leaves the request. Omit to dispatch without a signature header." },
3092
3105
"ttl_policy": { "type": "string", "enum": ["auto_24h", "permanent"], "description": "Wave FIX-J. Sets the deploy's lifecycle. 'auto_24h' (default for new deploys) means the deploy auto-expires 24h from creation; the response's agent_action sentence tells the LLM the three explicit routes to keep it permanent. 'permanent' opts the deploy out of TTL up front — useful for production deploys where the agent already knows the user wants it kept. Anonymous tier is FORCED to auto_24h regardless of caller intent. Team-wide default can be flipped via PATCH /api/v1/team/settings." },
3093
-
"redeploy": { "type": "boolean", "default": false, "description": "When true with a matching 'name', replace the existing deployment in place (same app_id + URL, same provider_id) instead of minting a fresh one. The platform looks up the team's most-recent non-terminal deployment whose env_vars._name matches the supplied 'name' (scoped to the resolved 'env'), then routes through the same compute path as POST /deploy/:id/redeploy. Closes the agent-UX gap (2026-05-30): multiple /deploy/new calls for the same logical app used to fan out into N distinct URLs because there was no way to upsert by name. Truthy values: 'true', '1', 'yes' (case-insensitive); anything else is false. Errors: 400 redeploy_requires_name when 'name' is empty; 404 no_existing_deployment_to_redeploy when no live row matches (omit 'redeploy' to create a new deployment, or call GET /api/v1/deployments first to discover the id); 409 not_ready when the matching row exists but has no provider_id yet (initial build still running). Default false: leaving the field absent keeps the legacy fan-out behaviour." }
3106
+
"redeploy": { "type": "boolean", "default": false, "description": "When true with a matching 'name', replace the existing deployment in place (same app_id + URL, same provider_id) instead of minting a fresh one. The platform looks up the team's most-recent non-terminal deployment whose env_vars._name matches the supplied 'name' (scoped to the resolved 'env'), then routes through the same compute path as POST /deploy/:id/redeploy. Closes the agent-UX gap (2026-05-30): multiple /deploy/new calls for the same logical app used to fan out into N distinct URLs because there was no way to upsert by name. Truthy values: 'true', '1', 'yes' (case-insensitive); anything else is false. Errors: 404 no_existing_deployment_to_redeploy when no live row matches (note: an empty 'name' is rejected upstream by the standard name_required check, before this flag is even consulted) (omit 'redeploy' to create a new deployment, or call GET /api/v1/deployments first to discover the id); 409 not_ready when the matching row exists but has no provider_id yet (initial build still running). Default false: leaving the field absent keeps the legacy fan-out behaviour." }
Copy file name to clipboardExpand all lines: openapi.snapshot.json
+45-1Lines changed: 45 additions & 1 deletion
Original file line number
Diff line number
Diff line change
@@ -839,7 +839,7 @@
839
839
},
840
840
"redeploy": {
841
841
"default": false,
842
-
"description": "When true with a matching 'name', replace the existing deployment in place (same app_id + URL, same provider_id) instead of minting a fresh one. The platform looks up the team's most-recent non-terminal deployment whose env_vars._name matches the supplied 'name' (scoped to the resolved 'env'), then routes through the same compute path as POST /deploy/:id/redeploy. Closes the agent-UX gap (2026-05-30): multiple /deploy/new calls for the same logical app used to fan out into N distinct URLs because there was no way to upsert by name. Truthy values: 'true', '1', 'yes' (case-insensitive); anything else is false. Errors: 400 redeploy_requires_name when 'name' is empty; 404 no_existing_deployment_to_redeploy when no live row matches (omit 'redeploy' to create a new deployment, or call GET /api/v1/deployments first to discover the id); 409 not_ready when the matching row exists but has no provider_id yet (initial build still running). Default false: leaving the field absent keeps the legacy fan-out behaviour.",
842
+
"description": "When true with a matching 'name', replace the existing deployment in place (same app_id + URL, same provider_id) instead of minting a fresh one. The platform looks up the team's most-recent non-terminal deployment whose env_vars._name matches the supplied 'name' (scoped to the resolved 'env'), then routes through the same compute path as POST /deploy/:id/redeploy. Closes the agent-UX gap (2026-05-30): multiple /deploy/new calls for the same logical app used to fan out into N distinct URLs because there was no way to upsert by name. Truthy values: 'true', '1', 'yes' (case-insensitive); anything else is false. Errors: 404 no_existing_deployment_to_redeploy when no live row matches (note: an empty 'name' is rejected upstream by the standard name_required check, before this flag is even consulted) (omit 'redeploy' to create a new deployment, or call GET /api/v1/deployments first to discover the id); 409 not_ready when the matching row exists but has no provider_id yet (initial build still running). Default false: leaving the field absent keeps the legacy fan-out behaviour.",
843
843
"type": "boolean"
844
844
},
845
845
"resource_bindings": {
@@ -8084,6 +8084,50 @@
8084
8084
"summary": "Send a passwordless magic-link sign-in email"
8085
8085
}
8086
8086
},
8087
+
"/auth/exchange": {
8088
+
"post": {
8089
+
"description": "Final leg of the AUTH-004 cross-origin sign-in handshake. The /auth/email/callback and /auth/github/callback handlers set a short-lived HttpOnly auth_exchange_cookie and 302 to https://instanode.dev/login/callback?signed_in=1. The dashboard then makes a credentials:include POST to this endpoint. CORS contract: response MUST include Access-Control-Allow-Origin: https://instanode.dev AND Access-Control-Allow-Credentials: true — the browser blocks the read otherwise. Request MUST be a CORS-simple POST (no custom headers like Accept: application/json), since adding one forces a preflight that PreflightAllowlist may reject. Returns 200 + the bearer JWT (24h, HS256, aud=https://api.instanode.dev) on success. The 2026-05-29 to 2026-05-30 prod-login outage chained three failures along this exact endpoint — documenting it here so any future regression is catchable by the cross-stack contract gate (api PR #202).",
8090
+
"requestBody": {
8091
+
"description": "No body. The bridge cookie travels in the Cookie header via credentials:include.",
8092
+
"required": false
8093
+
},
8094
+
"responses": {
8095
+
"200": {
8096
+
"content": {
8097
+
"application/json": {
8098
+
"schema": {
8099
+
"properties": {
8100
+
"ok": {
8101
+
"type": "boolean"
8102
+
},
8103
+
"token": {
8104
+
"description": "Session JWT — store in localStorage and send as Authorization: Bearer for /api/v1/* calls",
"description": "Cookie present but signature invalid or aud mismatch"
8123
+
},
8124
+
"503": {
8125
+
"description": "JWT signing failed (downstream)"
8126
+
}
8127
+
},
8128
+
"summary": "Exchange the AUTH-004 bridge cookie for a session JWT"
8129
+
}
8130
+
},
8087
8131
"/auth/github": {
8088
8132
"post": {
8089
8133
"description": "Programmatic / SPA flow. Body: {\"code\":\"<github-oauth-code>\"}. Returns 200 with a 24h session JWT plus user/team ids. Returns 503 oauth_not_configured when GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET are not set in the environment.",
0 commit comments