Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 54 additions & 49 deletions src/lib/docs/console-api-endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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<string>", 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<string>", 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<object>", 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 <token>`" },
],
Expand Down Expand Up @@ -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 <token>`.",
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.

Expand Down Expand Up @@ -944,20 +944,32 @@ const jwt = jwtResp.data.token;`,
responseStatus: "200 OK",
responseFields: [
{ field: "services.<name>.ready_replicas", type: "number", description: "Replicas currently passing readiness checks" },
{ field: "services.<name>.total_replicas", type: "number", description: "Replicas desired" },
{ field: "services.<name>.forwarded_ports", type: "array", description: "Provider-side port forwards" },
{ field: "services.<name>.ips", type: "array<string>", description: "Public IPs (when leasing IP endpoints)" },
{ field: "services.<name>.restart_count", type: "number", description: "Aggregate restart count for the service" },
{ field: "services.<name>.available_replicas", type: "number", description: "Replicas marked available by the Kubernetes controller" },
{ field: "services.<name>.replicas", type: "number", description: "Current replicas" },
{ field: "services.<name>.total", type: "number", description: "Desired replicas" },
{ field: "services.<name>.uris", type: "array<string>", description: "Accessible URIs for the service (when leasing endpoints)" },
{ field: "forwarded_ports.<name>", type: "array<object>", description: "Provider-side port forwards, keyed by service name (top-level — not nested under `services`)" },
{ field: "ips.<name>", type: "array<object>", 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: [
Expand All @@ -967,24 +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";
import { CertificateValidator } from "@akashnetwork/chain-sdk";
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";

const agent = new https.Agent({
rejectUnauthorized: false,
checkServerIdentity: (_host, cert) =>
CertificateValidator.validateProviderCert(cert, providerAddress),
});
// \`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();`,
},
Expand Down Expand Up @@ -1023,26 +1037,23 @@ 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"`,
},
{
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));`,
},
Expand Down Expand Up @@ -1082,25 +1093,21 @@ 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"`,
},
{
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())));`,
},
Expand Down Expand Up @@ -1139,7 +1146,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 =
Expand All @@ -1148,11 +1157,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));`,
},
Expand Down
Loading