Move every caller and route to /v1

Commerce is called at /v1, the indexer at /v1/indexer, and the operator
serves /v1/clusters. The console's unused fleet helpers called a /api
base nothing serves; they are gone. The legacy Quart env files named an
app that no longer exists.

Claude-Session: https://claude.ai/code/session_011i2AG9nwpiKUCVxAdDzYuU
This commit is contained in:
2026-09-29 18:34:58 -07:00
parent d0507e0a03
commit 4c917f849b
20 changed files with 134 additions and 195 deletions
+4 -4
View File
@@ -451,7 +451,7 @@ X-RateLimit-Reset: 1706400000`}</code>
-H "Content-Type: application/json" \\
-H "X-API-Key: YOUR_API_KEY" \\
-d '{
"url": "https://myapp.com/api/webhooks/bootnode",
"url": "https://myapp.com/webhooks/bootnode",
"chain": "ethereum",
"event_type": "address_activity",
"config": {
@@ -460,7 +460,7 @@ X-RateLimit-Reset: 1706400000`}</code>
}'`}
response={`{
"id": "wh_a1b2c3d4e5f6",
"url": "https://myapp.com/api/webhooks/bootnode",
"url": "https://myapp.com/webhooks/bootnode",
"chain": "ethereum",
"event_type": "address_activity",
"config": {
@@ -486,7 +486,7 @@ X-RateLimit-Reset: 1706400000`}</code>
"webhooks": [
{
"id": "wh_a1b2c3d4e5f6",
"url": "https://myapp.com/api/webhooks/bootnode",
"url": "https://myapp.com/webhooks/bootnode",
"chain": "ethereum",
"event_type": "address_activity",
"status": "active",
@@ -515,7 +515,7 @@ X-RateLimit-Reset: 1706400000`}</code>
}'`}
response={`{
"id": "wh_a1b2c3d4e5f6",
"url": "https://myapp.com/api/webhooks/bootnode",
"url": "https://myapp.com/webhooks/bootnode",
"chain": "ethereum",
"event_type": "address_activity",
"status": "paused",
+2 -2
View File
@@ -147,7 +147,7 @@ async function monitorWallet(address: string) {
"X-API-Key": process.env.BOOTNODE_API_KEY!,
},
body: JSON.stringify({
url: "https://myapp.com/api/webhooks/wallet-activity",
url: "https://myapp.com/webhooks/wallet-activity",
chain: "ethereum",
event_type: "address_activity",
config: {
@@ -163,7 +163,7 @@ async function monitorWallet(address: string) {
}
// 2. Handle incoming events (Next.js API route)
// app/api/webhooks/wallet-activity/route.ts
// app/webhooks/wallet-activity/route.ts
import { NextRequest, NextResponse } from "next/server";
import crypto from "crypto";
+1 -1
View File
@@ -86,7 +86,7 @@ console.log("Smart wallet:", wallet.address);
// Webhooks
const webhook = await client.webhooks.create({
url: "https://myapp.com/api/webhooks",
url: "https://myapp.com/webhooks",
chain: "ethereum",
eventType: "address_activity",
config: { addresses: ["0xd8dA...6045"] },
+1 -1
View File
@@ -276,7 +276,7 @@ export async function sendGaslessTransfer({
sponsored transaction:
</p>
<pre className="overflow-x-auto rounded-lg bg-zinc-950 p-4 text-sm text-zinc-100">
<code>{`// app/api/wallet/create/route.ts
<code>{`// app/wallet/create/route.ts
import { NextRequest, NextResponse } from "next/server";
const BOOTNODE_API_KEY = process.env.BOOTNODE_API_KEY!;
@@ -74,7 +74,7 @@ export default function WebhooksQuickstartPage() {
-H "Content-Type: application/json" \\
-H "X-API-Key: YOUR_API_KEY" \\
-d '{
"url": "https://myapp.com/api/webhooks/bootnode",
"url": "https://myapp.com/webhooks/bootnode",
"chain": "ethereum",
"event_type": "address_activity",
"config": {
@@ -89,7 +89,7 @@ export default function WebhooksQuickstartPage() {
<pre className="overflow-x-auto rounded-lg bg-zinc-950 p-4 text-sm text-zinc-100">
<code>{`{
"id": "wh_a1b2c3d4e5f6",
"url": "https://myapp.com/api/webhooks/bootnode",
"url": "https://myapp.com/webhooks/bootnode",
"chain": "ethereum",
"event_type": "address_activity",
"config": {
@@ -135,7 +135,7 @@ const SIGNING_SECRET = process.env.WEBHOOK_SIGNING_SECRET!;
// IMPORTANT: Use raw body for signature verification
app.post(
"/api/webhooks/bootnode",
"/webhooks/bootnode",
express.raw({ type: "application/json" }),
(req, res) => {
const signature = req.headers["x-bootnode-signature"] as string;
+2 -2
View File
@@ -333,7 +333,7 @@ export default function MPCProductPage() {
<CardContent className="p-0">
<pre className="overflow-x-auto rounded-lg bg-zinc-950 p-6 text-sm text-zinc-100">
<code>{`# Create a distributed wallet
curl -X POST https://mpc.yourorg.com/api/v1/wallets \\
curl -X POST https://mpc.yourorg.com/v1/wallets \\
-H "Authorization: Bearer \$TOKEN" \\
-d '{"name": "treasury", "chains": ["ETH", "BTC", "SOL"]}'
@@ -350,7 +350,7 @@ curl -X POST https://mpc.yourorg.com/api/v1/wallets \\
}
# Sign an Ethereum transaction
curl -X POST https://mpc.yourorg.com/api/v1/sign \\
curl -X POST https://mpc.yourorg.com/v1/sign \\
-H "Authorization: Bearer \$TOKEN" \\
-d '{
"wallet_id": "w_8f3a...",
-100
View File
@@ -1,4 +1,3 @@
import { getApiUrl } from "@/lib/api"
// DigitalOcean Kubernetes regions and Lux network configuration
// Used by the fleet management UI for cluster provisioning and monitoring.
@@ -57,102 +56,3 @@ export const NETWORK_CONFIGS: Record<LuxNetwork, LuxNetworkConfig> = {
};
export const NETWORK_NAMES: LuxNetwork[] = ["mainnet", "testnet", "devnet"] as const;
// ---------------------------------------------------------------------------
// API Client Helpers
// ---------------------------------------------------------------------------
// One resolver for every caller (lib/api.ts), host-derived at runtime.
const API_BASE = getApiUrl();
function authHeaders(): HeadersInit {
const headers: HeadersInit = { "Content-Type": "application/json" };
if (typeof window !== "undefined") {
const token = localStorage.getItem("token");
if (token) {
headers["Authorization"] = `Bearer ${token}`;
}
}
return headers;
}
async function request<T = unknown>(path: string, init?: RequestInit): Promise<T> {
const res = await fetch(`${API_BASE}${path}`, {
...init,
headers: { ...authHeaders(), ...init?.headers },
});
if (!res.ok) {
const body = await res.text().catch(() => "");
throw new Error(`${res.status} ${res.statusText}: ${body}`);
}
// 204 No Content
if (res.status === 204) return undefined as T;
return res.json() as Promise<T>;
}
/** List all fleets, optionally filtered by cluster. */
export async function fetchFleets(clusterId?: string) {
const qs = clusterId ? `?clusterId=${encodeURIComponent(clusterId)}` : "";
return request(`/api/fleets${qs}`);
}
/** Get a single fleet by cluster + network. */
export async function fetchFleet(clusterId: string, network: LuxNetwork) {
return request(`/api/fleets/${encodeURIComponent(clusterId)}/${network}`);
}
/** Create a new fleet. */
export async function createFleet(params: Record<string, unknown>) {
return request("/api/fleets", {
method: "POST",
body: JSON.stringify(params),
});
}
/** Update an existing fleet. */
export async function updateFleet(
clusterId: string,
network: LuxNetwork,
params: Record<string, unknown>,
) {
return request(`/api/fleets/${encodeURIComponent(clusterId)}/${network}`, {
method: "PUT",
body: JSON.stringify(params),
});
}
/** Delete a fleet. */
export async function deleteFleet(clusterId: string, network: LuxNetwork) {
return request(`/api/fleets/${encodeURIComponent(clusterId)}/${network}`, {
method: "DELETE",
});
}
/** Scale a fleet to the given replica count. */
export async function scaleFleet(
clusterId: string,
network: LuxNetwork,
replicas: number,
) {
return request(`/api/fleets/${encodeURIComponent(clusterId)}/${network}/scale`, {
method: "POST",
body: JSON.stringify({ replicas }),
});
}
/** Restart all nodes in a fleet. */
export async function restartFleet(clusterId: string, network: LuxNetwork) {
return request(`/api/fleets/${encodeURIComponent(clusterId)}/${network}/restart`, {
method: "POST",
});
}
/** Fetch aggregate stats across all fleets. */
export async function fetchStats() {
return request("/api/stats");
}
/** Fetch available Lux networks. */
export async function fetchNetworks() {
return request("/api/networks");
}
+50
View File
@@ -0,0 +1,50 @@
// Every path bootnode serves or calls is `/v1/...`; auth is `/v1/iam/*`.
//
// Run: `pnpm test`.
//
// Walks the first-party source of every component in this repo — the Python
// API, the console, the operator, the fleet service — for an `/api/` path
// written as a literal or appended to a base (`"/api/…"`, `` `${base}/api/…` ``,
// `f"{url}/api/…"`). The one address allowed is the Kubernetes API
// (`/api/v1/namespaces/…`), which is not ours. Tests are skipped: the fleet
// suite requests `/api/…` on purpose, to prove it 404s.
import { strict as assert } from "node:assert"
import { readdirSync, readFileSync, statSync } from "node:fs"
import { join, relative } from "node:path"
import { describe, it } from "node:test"
import { fileURLToPath } from "node:url"
const ROOT = join(fileURLToPath(new URL(".", import.meta.url)), "..", "..")
const DIRS = ["api/bootnode", "api/handlers", "api/store", "api/middleware", "console/app", "console/components", "console/lib", "operator/src", "fleet", "cmd"]
const SOURCE = /\.(py|go|rs|ts|tsx|js|mjs)$/
const TEST = /(\.test\.tsx?|_test\.go|\/test_[^/]*\.py)$/
const PATH = /['"`(}]\/api\//
const KUBERNETES = /\/api\/v1\/namespaces/
function walk(dir: string, out: string[] = []): string[] {
for (const entry of readdirSync(dir)) {
if (entry === "node_modules" || entry === "__pycache__") continue
const full = join(dir, entry)
if (statSync(full).isDirectory()) walk(full, out)
else out.push(full)
}
return out
}
describe("the /v1 prefix", () => {
it("no first-party source names an /api/ path", () => {
const hits: string[] = []
for (const dir of DIRS) {
for (const file of walk(join(ROOT, dir))) {
if (!SOURCE.test(file) || TEST.test(file)) continue
readFileSync(file, "utf8")
.split("\n")
.forEach((line, i) => {
if (PATH.test(line) && !KUBERNETES.test(line)) hits.push(`${relative(ROOT, file)}:${i + 1}: ${line.trim()}`)
})
}
}
assert.deepEqual(hits, [])
})
})