From 3ce0793f3383c1e37702f8c93f39e4ba5e1e9fdc Mon Sep 17 00:00:00 2001 From: Maxime Beauchamp <15185355+baktun14@users.noreply.github.com> Date: Mon, 25 May 2026 22:13:16 -0500 Subject: [PATCH 1/2] fix(docs): correct Console API reference accuracy issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Stripe-style Console API reference was generated from Swagger with AI assistance and contained several hallucinated or incorrect claims. Fixes verified against console2 zod schemas and the chain-sdk source: - Drop the fake `import { CertificateValidator } from "@akashnetwork/chain-sdk"` from all 4 provider lease-ops JS snippets (status, logs, kubeevents, shell). The package doesn't export that symbol, and even if it did, `CertificateValidator.validate()` is async — it can't run inside Node's synchronous `checkServerIdentity` callback. Replace with `rejectUnauthorized: false` and a comment pointing to the preface. - Rewrite the TLS/cert pinning prose to describe the real pinning mechanism: CN must be the provider's bech32 wallet address, and the chain's `MsgCreateCertificate` record for `(address, serial number)` must match the leaf cert fingerprint. Link to the provider-proxy `CertificateValidator` for a reference implementation. - POST /v1/create-jwt-token: response status 200 OK -> 201 Created (jwt-token.router.ts returns 201). - GET /lease/{dseq}/{gseq}/{oseq}/status: rewrite `responseFields` and `responseExample`. `forwarded_ports` and `ips` are top-level keyed by service name (not nested under services); `restart_count` and `total_replicas` don't exist; the response includes `name`, `available`, `total`, `uris`, `observed_generation`, `replicas`, `updated_replicas`, `available_replicas` per service. - Add `hostname-migrate` and `ip-migrate` to the JWT scope enumeration in both spots (chain-sdk's `AccessScope` type lists 9 values; docs listed 7). --- src/lib/docs/console-api-endpoints.ts | 83 +++++++++++++-------------- 1 file changed, 41 insertions(+), 42 deletions(-) diff --git a/src/lib/docs/console-api-endpoints.ts b/src/lib/docs/console-api-endpoints.ts index c97ba8a9b..fa440a3a6 100644 --- a/src/lib/docs/console-api-endpoints.ts +++ b/src/lib/docs/console-api-endpoints.ts @@ -826,7 +826,7 @@ const { data } = await res.json();`, "The Console API and the provider share a JWT-based access model for lease-scoped operations (logs, events, status, shell). Mint a short-lived token from the Console API, then call the provider directly with it.", bodyMd: `**TTL**: tokens are short-lived (default 1800 s in the Console UI). There is no refresh endpoint — re-call \`POST /v1/create-jwt-token\` to extend lifetime. -**Scope**: grant the narrowest set you need. Missing scope → \`401\` from the provider. Valid values: \`send-manifest\`, \`get-manifest\`, \`logs\`, \`shell\`, \`events\`, \`status\`, \`restart\`. +**Scope**: grant the narrowest set you need. Missing scope → \`401\` from the provider. Valid values: \`send-manifest\`, \`get-manifest\`, \`logs\`, \`shell\`, \`events\`, \`status\`, \`restart\`, \`hostname-migrate\`, \`ip-migrate\`. **Spec**: JWT payload follows [AEP-64](https://akash.network/roadmap/aep-64/). The provider validates it the same way regardless of who minted it. @@ -848,10 +848,10 @@ const { data } = await res.json();`, { field: "x-api-key", location: "header", type: "string", required: true, description: "Your API key" }, { field: "data.ttl", location: "body", type: "number", required: true, description: "Token TTL in seconds (Console UI default: `1800`)" }, { field: "data.leases.access", location: "body", type: "enum", required: true, description: "One of `full`, `scoped`, or `granular`" }, - { field: "data.leases.scope", location: "body", type: "array", required: false, description: "Required when `access` is `scoped`. Any of `send-manifest`, `get-manifest`, `logs`, `shell`, `events`, `status`, `restart`" }, + { field: "data.leases.scope", location: "body", type: "array", required: false, description: "Required when `access` is `scoped`. Any of `send-manifest`, `get-manifest`, `logs`, `shell`, `events`, `status`, `restart`, `hostname-migrate`, `ip-migrate`" }, { field: "data.leases.permissions", location: "body", type: "array", required: false, description: "Required when `access` is `granular`. Per-provider, per-deployment rules" }, ], - responseStatus: "200 OK", + responseStatus: "201 Created", responseFields: [ { field: "data.token", type: "string (JWT)", description: "Bearer token to send to provider endpoints as `Authorization: Bearer `" }, ], @@ -913,7 +913,7 @@ const jwt = jwtResp.data.token;`, title: "Provider-side lease endpoints", description: "These endpoints are served by the **provider**, not by `console-api.akash.network`. Resolve the provider's `hostUri` via `GET /v1/providers/{address}` (network-data API), then call the provider directly with the JWT in `Authorization: Bearer `.", - bodyMd: `**TLS / cert pinning**: provider certificates are self-signed against the provider's on-chain wallet address — browsers will reject them. Server-side, use a custom HTTPS agent that pins the leaf cert against the provider's address (\`@akashnetwork/chain-sdk\`'s \`CertificateValidator\` does this). In a browser, route through a provider-proxy service. + bodyMd: `**TLS / cert pinning**: provider certificates are self-signed against the provider's on-chain wallet address — browsers will reject them. Server-side, you have to look the cert up on-chain: the CN must be the provider's bech32 wallet address, and the chain's \`MsgCreateCertificate\` record for that \`(address, serial number)\` must match the leaf cert's fingerprint. That lookup is a chain query, so it has to happen **after** the TLS handshake — Node's \`checkServerIdentity\` is synchronous and can't \`await\`. The canonical pattern (used by Console) is to disable Node's default verification, then validate the peer cert asynchronously against the chain. See the [provider-proxy \`CertificateValidator\`](https://github.com/akash-network/console/tree/main/apps/provider-proxy/src/services/CertificateValidator) for a reference implementation. In a browser, route through a provider-proxy service. **\`events\` vs \`kubeevents\` gotcha**: Console UI accepts \`events\` as a path component and rewrites it to \`kubeevents\` client-side. The wire path on the provider is **always \`kubeevents\`** — use that directly to avoid surprises. @@ -944,20 +944,32 @@ const jwt = jwtResp.data.token;`, responseStatus: "200 OK", responseFields: [ { field: "services..ready_replicas", type: "number", description: "Replicas currently passing readiness checks" }, - { field: "services..total_replicas", type: "number", description: "Replicas desired" }, - { field: "services..forwarded_ports", type: "array", description: "Provider-side port forwards" }, - { field: "services..ips", type: "array", description: "Public IPs (when leasing IP endpoints)" }, - { field: "services..restart_count", type: "number", description: "Aggregate restart count for the service" }, + { field: "services..available_replicas", type: "number", description: "Replicas marked available by the Kubernetes controller" }, + { field: "services..replicas", type: "number", description: "Current replicas" }, + { field: "services..total", type: "number", description: "Desired replicas" }, + { field: "services..uris", type: "array", description: "Accessible URIs for the service (when leasing endpoints)" }, + { field: "forwarded_ports.", type: "array", description: "Provider-side port forwards, keyed by service name (top-level — not nested under `services`)" }, + { field: "ips.", type: "array", description: "Public IPs assigned to the service, keyed by service name (top-level, when leasing IP endpoints)" }, ], responseExample: `{ "services": { "web": { + "name": "web", + "available": 1, + "total": 1, + "uris": ["example.com"], + "observed_generation": 1, + "replicas": 1, + "updated_replicas": 1, "ready_replicas": 1, - "total_replicas": 1, - "forwarded_ports": [], - "ips": [], - "restart_count": 0 + "available_replicas": 1 } + }, + "forwarded_ports": { + "web": [{ "host": "example.com", "port": 80, "externalPort": 30000, "available": 1 }] + }, + "ips": { + "web": [{ "IP": "1.2.3.4", "Port": 80, "ExternalPort": 30000, "Protocol": "tcp" }] } }`, notes: [ @@ -974,13 +986,12 @@ curl "https://\${HOSTURI#https://}/lease/\${DSEQ}/\${GSEQ}/\${OSEQ}/status" \\ { language: "javascript", code: `import https from "https"; -import { CertificateValidator } from "@akashnetwork/chain-sdk"; -const agent = new https.Agent({ - rejectUnauthorized: false, - checkServerIdentity: (_host, cert) => - CertificateValidator.validateProviderCert(cert, providerAddress), -}); +// \`rejectUnauthorized: false\` skips Node's hostname check (provider certs +// are self-signed against the provider's on-chain wallet). For production, +// validate the peer cert against the chain asynchronously — see the +// provider-endpoints preface. +const agent = new https.Agent({ rejectUnauthorized: false }); const res = await fetch( \`\${hostUri}/lease/\${dseq}/\${gseq}/\${oseq}/status\`, @@ -1031,18 +1042,13 @@ websocat "wss://\${HOSTURI#https://}/lease/\${DSEQ}/\${GSEQ}/\${OSEQ}/logs?follo language: "javascript", code: `import WebSocket from "ws"; import https from "https"; -import { CertificateValidator } from "@akashnetwork/chain-sdk"; + +// \`rejectUnauthorized: false\` skips Node's hostname check; see preface. +const agent = new https.Agent({ rejectUnauthorized: false }); const ws = new WebSocket( \`wss://\${hostUri.replace(/^https?:\\/\\//, "")}/lease/\${dseq}/\${gseq}/\${oseq}/logs?follow=true\`, - { - headers: { Authorization: \`Bearer \${jwt}\` }, - agent: new https.Agent({ - rejectUnauthorized: false, - checkServerIdentity: (_host, cert) => - CertificateValidator.validateProviderCert(cert, providerAddress), - }), - }, + { headers: { Authorization: \`Bearer \${jwt}\` }, agent }, ); ws.on("message", (chunk) => process.stdout.write(chunk));`, }, @@ -1089,18 +1095,13 @@ ws.on("message", (chunk) => process.stdout.write(chunk));`, language: "javascript", code: `import WebSocket from "ws"; import https from "https"; -import { CertificateValidator } from "@akashnetwork/chain-sdk"; + +// \`rejectUnauthorized: false\` skips Node's hostname check; see preface. +const agent = new https.Agent({ rejectUnauthorized: false }); const ws = new WebSocket( \`wss://\${hostUri.replace(/^https?:\\/\\//, "")}/lease/\${dseq}/\${gseq}/\${oseq}/kubeevents\`, - { - headers: { Authorization: \`Bearer \${jwt}\` }, - agent: new https.Agent({ - rejectUnauthorized: false, - checkServerIdentity: (_host, cert) => - CertificateValidator.validateProviderCert(cert, providerAddress), - }), - }, + { headers: { Authorization: \`Bearer \${jwt}\` }, agent }, ); ws.on("message", (chunk) => console.log(JSON.parse(chunk.toString())));`, }, @@ -1139,7 +1140,9 @@ ws.on("message", (chunk) => console.log(JSON.parse(chunk.toString())));`, language: "javascript", code: `import WebSocket from "ws"; import https from "https"; -import { CertificateValidator } from "@akashnetwork/chain-sdk"; + +// \`rejectUnauthorized: false\` skips Node's hostname check; see preface. +const agent = new https.Agent({ rejectUnauthorized: false }); const cmd = Buffer.from(JSON.stringify(["/bin/sh"])).toString("base64"); const url = @@ -1148,11 +1151,7 @@ const url = const ws = new WebSocket(url, { headers: { Authorization: \`Bearer \${jwt}\` }, - agent: new https.Agent({ - rejectUnauthorized: false, - checkServerIdentity: (_host, cert) => - CertificateValidator.validateProviderCert(cert, providerAddress), - }), + agent, }); ws.on("message", (frame) => process.stdout.write(frame));`, }, From 49deb8ac3cd595358a7c5d3c0b672f97bbdcc329 Mon Sep 17 00:00:00 2001 From: Maxime Beauchamp <15185355+baktun14@users.noreply.github.com> Date: Mon, 25 May 2026 22:24:48 -0500 Subject: [PATCH 2/2] fix(docs): make provider-endpoint snippets actually runnable The provider-side snippets in the lease-ops section claimed to bypass TLS verification, but as written they would have failed at runtime: - JS GET /lease/.../status used `await fetch(url, { agent })`. Node's global `fetch` is built on undici and silently ignores `https.Agent`, so the request would have failed with `SELF_SIGNED_CERT_IN_CHAIN`. Switch to undici's `fetch` + `Agent` + `dispatcher`, which actually controls TLS. (The WSS snippets use the `ws` package, which does accept `agent`, so they're left as-is.) - Bash snippets for status / logs / kubeevents lacked `-k` / `--insecure`, so curl/websocat would reject the self-signed provider cert. Add the flags inline with a one-line comment. --- src/lib/docs/console-api-endpoints.ts | 30 ++++++++++++++++----------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/lib/docs/console-api-endpoints.ts b/src/lib/docs/console-api-endpoints.ts index fa440a3a6..0824295c5 100644 --- a/src/lib/docs/console-api-endpoints.ts +++ b/src/lib/docs/console-api-endpoints.ts @@ -979,23 +979,26 @@ const jwt = jwtResp.data.token;`, codeSnippets: [ { language: "bash", - code: `# --cacert / -k handling omitted — see provider preface for cert pinning. -curl "https://\${HOSTURI#https://}/lease/\${DSEQ}/\${GSEQ}/\${OSEQ}/status" \\ + code: `# -k skips TLS verification (provider certs are self-signed against the +# on-chain wallet). For production, validate the cert against the chain +# out-of-band — see the provider-endpoints preface. +curl -k "https://\${HOSTURI#https://}/lease/\${DSEQ}/\${GSEQ}/\${OSEQ}/status" \\ -H "Authorization: Bearer $JWT"`, }, { language: "javascript", - code: `import https from "https"; + code: `// Node's global \`fetch\` is built on undici and ignores \`https.Agent\` — +// you have to pass an undici \`dispatcher\` to control TLS verification. +import { fetch, Agent } from "undici"; -// \`rejectUnauthorized: false\` skips Node's hostname check (provider certs -// are self-signed against the provider's on-chain wallet). For production, -// validate the peer cert against the chain asynchronously — see the -// provider-endpoints preface. -const agent = new https.Agent({ rejectUnauthorized: false }); +// \`connect.rejectUnauthorized: false\` lets the request complete past the +// self-signed provider cert. For production, validate the peer cert against +// the chain asynchronously — see the provider-endpoints preface. +const dispatcher = new Agent({ connect: { rejectUnauthorized: false } }); const res = await fetch( \`\${hostUri}/lease/\${dseq}/\${gseq}/\${oseq}/status\`, - { headers: { Authorization: \`Bearer \${jwt}\` }, agent }, + { headers: { Authorization: \`Bearer \${jwt}\` }, dispatcher }, ); const status = await res.json();`, }, @@ -1034,8 +1037,10 @@ const status = await res.json();`, codeSnippets: [ { language: "bash", - code: `# websocat 1.13+ supports --insecure for self-signed certs. -websocat "wss://\${HOSTURI#https://}/lease/\${DSEQ}/\${GSEQ}/\${OSEQ}/logs?follow=true&tail=200" \\ + code: `# --insecure skips TLS verification (provider certs are self-signed). +# Requires websocat 1.13+. See provider-endpoints preface for production +# cert validation. +websocat --insecure "wss://\${HOSTURI#https://}/lease/\${DSEQ}/\${GSEQ}/\${OSEQ}/logs?follow=true&tail=200" \\ -H "Authorization: Bearer $JWT"`, }, { @@ -1088,7 +1093,8 @@ ws.on("message", (chunk) => process.stdout.write(chunk));`, codeSnippets: [ { language: "bash", - code: `websocat "wss://\${HOSTURI#https://}/lease/\${DSEQ}/\${GSEQ}/\${OSEQ}/kubeevents" \\ + code: `# --insecure skips TLS verification (provider certs are self-signed). +websocat --insecure "wss://\${HOSTURI#https://}/lease/\${DSEQ}/\${GSEQ}/\${OSEQ}/kubeevents" \\ -H "Authorization: Bearer $JWT"`, }, {