Skip to content

Commit 58f0653

Browse files
Add /api/v1/health endpoint with sui, walrus, and publisher probes (#40)
Returns version, network identifiers, package and registry ids, plus a probe result for each external dependency. Useful for hosting platforms with uptime checks (Vercel, Cloudflare, Better Uptime). Response is 200 when probes succeed, 503 when sui rpc fails or the package is unconfigured. Probes have a 4 second hard timeout so the endpoint stays cheap even when an external service is down. Cache-control is no-store because the underlying state is live. Co-authored-by: Vika Joestar <mvikaputrap7@gmail.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent e93dc93 commit 58f0653

1 file changed

Lines changed: 130 additions & 0 deletions

File tree

src/app/api/v1/health/route.ts

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import { NextResponse } from "next/server";
2+
import {
3+
PLINTH_FORM_REGISTRY_ID,
4+
PLINTH_NETWORK,
5+
PLINTH_PACKAGE_ID,
6+
suiClient,
7+
} from "@/lib/sui/client";
8+
import { WALRUS_AGGREGATOR_URL, WALRUS_NETWORK, WALRUS_PUBLISHER_URL } from "@/lib/walrus/config";
9+
10+
const VERSION = "0.1.0";
11+
const PROBE_TIMEOUT_MS = 4000;
12+
13+
type ProbeStatus = "ok" | "degraded" | "unconfigured" | "error";
14+
15+
interface ProbeResult {
16+
status: ProbeStatus;
17+
detail?: string;
18+
durationMs?: number;
19+
}
20+
21+
interface HealthResponse {
22+
status: "ok" | "degraded";
23+
version: string;
24+
network: {
25+
sui: string;
26+
walrus: string;
27+
};
28+
package: {
29+
id: string;
30+
formRegistryId: string;
31+
};
32+
checks: {
33+
sui: ProbeResult;
34+
walrus: ProbeResult;
35+
publisher: ProbeResult;
36+
};
37+
}
38+
39+
async function withTimeout<T>(p: Promise<T>, ms: number): Promise<T> {
40+
return new Promise<T>((resolve, reject) => {
41+
const t = setTimeout(() => reject(new Error(`timeout after ${ms}ms`)), ms);
42+
p.then(
43+
(v) => {
44+
clearTimeout(t);
45+
resolve(v);
46+
},
47+
(e) => {
48+
clearTimeout(t);
49+
reject(e);
50+
},
51+
);
52+
});
53+
}
54+
55+
async function probeSui(): Promise<ProbeResult> {
56+
if (!PLINTH_PACKAGE_ID || PLINTH_PACKAGE_ID.length === 0) {
57+
return { status: "unconfigured", detail: "NEXT_PUBLIC_PLINTH_PACKAGE_ID is empty" };
58+
}
59+
const start = Date.now();
60+
try {
61+
await withTimeout(suiClient.getLatestCheckpointSequenceNumber(), PROBE_TIMEOUT_MS);
62+
return { status: "ok", durationMs: Date.now() - start };
63+
} catch (e) {
64+
return {
65+
status: "error",
66+
detail: e instanceof Error ? e.message : "sui rpc error",
67+
durationMs: Date.now() - start,
68+
};
69+
}
70+
}
71+
72+
async function probeWalrus(): Promise<ProbeResult> {
73+
const start = Date.now();
74+
const url = `${WALRUS_AGGREGATOR_URL}/v1/api`;
75+
try {
76+
const controller = new AbortController();
77+
const timer = setTimeout(() => controller.abort(), PROBE_TIMEOUT_MS);
78+
const res = await fetch(url, { method: "GET", signal: controller.signal });
79+
clearTimeout(timer);
80+
if (res.ok || res.status === 404) {
81+
return { status: "ok", durationMs: Date.now() - start };
82+
}
83+
return {
84+
status: "degraded",
85+
detail: `aggregator returned HTTP ${res.status}`,
86+
durationMs: Date.now() - start,
87+
};
88+
} catch (e) {
89+
return {
90+
status: "error",
91+
detail: e instanceof Error ? e.message : "walrus aggregator error",
92+
durationMs: Date.now() - start,
93+
};
94+
}
95+
}
96+
97+
function probePublisher(): ProbeResult {
98+
if (!WALRUS_PUBLISHER_URL || WALRUS_PUBLISHER_URL.length === 0) {
99+
return {
100+
status: "unconfigured",
101+
detail: "NEXT_PUBLIC_WALRUS_PUBLISHER_URL is empty, write paths are disabled",
102+
};
103+
}
104+
return { status: "ok" };
105+
}
106+
107+
function overallStatus(checks: HealthResponse["checks"]): "ok" | "degraded" {
108+
for (const probe of Object.values(checks)) {
109+
if (probe.status === "error") return "degraded";
110+
}
111+
if (checks.sui.status === "unconfigured") return "degraded";
112+
return "ok";
113+
}
114+
115+
export async function GET() {
116+
const [sui, walrus] = await Promise.all([probeSui(), probeWalrus()]);
117+
const publisher = probePublisher();
118+
const checks: HealthResponse["checks"] = { sui, walrus, publisher };
119+
const body: HealthResponse = {
120+
status: overallStatus(checks),
121+
version: VERSION,
122+
network: { sui: PLINTH_NETWORK, walrus: WALRUS_NETWORK },
123+
package: { id: PLINTH_PACKAGE_ID, formRegistryId: PLINTH_FORM_REGISTRY_ID },
124+
checks,
125+
};
126+
return NextResponse.json(body, {
127+
status: body.status === "ok" ? 200 : 503,
128+
headers: { "cache-control": "no-store" },
129+
});
130+
}

0 commit comments

Comments
 (0)