fix: proxy cloud web read-only API

This commit is contained in:
root
2026-05-22 08:58:25 +00:00
parent 6211333c1b
commit 744890847a
2 changed files with 94 additions and 1 deletions
+85
View File
@@ -308,6 +308,13 @@ const environment = process.env.HWLAB_ENVIRONMENT || "dev";
const runtimeKind = process.env.HWLAB_ARTIFACT_KIND || "health-placeholder";
const entrypoint = process.env.HWLAB_SERVICE_ENTRYPOINT || "";
const port = Number.parseInt(process.env.PORT || process.env.HWLAB_PORT || "8080", 10);
const cloudApiBaseUrl = process.env.HWLAB_API_BASE_URL || "";
const readOnlyRpcMethods = new Set([
"system.health",
"cloud.adapter.describe",
"audit.event.query",
"evidence.record.query"
]);
function sendJson(response, statusCode, body) {
const payload = JSON.stringify(body, null, 2);
@@ -340,6 +347,76 @@ function runNodeEntrypoint(file) {
});
}
async function readRequestBody(request) {
let body = "";
for await (const chunk of request) {
body += chunk;
}
return body;
}
function copyHeaders(headers) {
const result = {};
for (const [key, value] of Object.entries(headers)) {
if (value === undefined) continue;
result[key] = value;
}
return result;
}
async function proxyCloudApi(request, response, url) {
if (!cloudApiBaseUrl) {
sendJson(response, 503, {
error: "cloud_api_base_url_missing",
serviceId,
path: url.pathname
});
return;
}
let body = "";
if (request.method !== "GET") {
body = await readRequestBody(request);
}
if (url.pathname === "/json-rpc") {
try {
const envelope = body ? JSON.parse(body) : {};
if (!readOnlyRpcMethods.has(envelope.method)) {
sendJson(response, 403, {
error: "readonly_rpc_required",
serviceId,
method: envelope.method || null
});
return;
}
} catch (error) {
sendJson(response, 400, {
error: "invalid_json",
serviceId,
reason: error.message
});
return;
}
}
const target = new URL(url.pathname + url.search, cloudApiBaseUrl);
const upstream = await fetch(target, {
method: request.method,
headers: {
accept: request.headers.accept || "application/json",
"content-type": request.headers["content-type"] || "application/json"
},
body: request.method === "GET" ? undefined : body
});
const payload = Buffer.from(await upstream.arrayBuffer());
response.writeHead(upstream.status, {
...copyHeaders(Object.fromEntries(upstream.headers.entries())),
"content-length": payload.length
});
response.end(payload);
}
async function serveCloudWeb() {
const roots = ["/app/web/hwlab-cloud-web/dist", "/app/web/hwlab-cloud-web"];
const server = createServer(async (request, response) => {
@@ -349,6 +426,14 @@ async function serveCloudWeb() {
return;
}
if (
(request.method === "GET" && (url.pathname === "/v1" || url.pathname.startsWith("/v1/"))) ||
(request.method === "POST" && url.pathname === "/json-rpc")
) {
await proxyCloudApi(request, response, url);
return;
}
const relativePath = url.pathname === "/" ? "index.html" : url.pathname.slice(1);
for (const root of roots) {
const candidate = path.resolve(root, relativePath);
+9 -1
View File
@@ -19,7 +19,8 @@ for (const file of requiredFiles) {
const html = fs.readFileSync(path.resolve(rootDir, "index.html"), "utf8");
const app = fs.readFileSync(path.resolve(rootDir, "app.mjs"), "utf8");
const frontendSource = `${html}\n${app}`;
const artifactPublisher = fs.readFileSync(path.resolve(repoRoot, "scripts/dev-artifact-publish.mjs"), "utf8");
const frontendSource = `${html}\n${app}\n${artifactPublisher}`;
assert.match(html, /HWLAB DEV Console/);
assert.match(html, /M3 Diagnostics Console/);
@@ -48,6 +49,13 @@ assert.match(app, /callRpc\("system\.health"\)/);
assert.match(app, /callRpc\("cloud\.adapter\.describe"\)/);
assert.match(app, /callRpc\("audit\.event\.query"/);
assert.match(app, /callRpc\("evidence\.record\.query"/);
assert.match(artifactPublisher, /HWLAB_API_BASE_URL/);
assert.match(artifactPublisher, /readOnlyRpcMethods/);
assert.match(artifactPublisher, /"system\.health"/);
assert.match(artifactPublisher, /"cloud\.adapter\.describe"/);
assert.match(artifactPublisher, /"audit\.event\.query"/);
assert.match(artifactPublisher, /"evidence\.record\.query"/);
assert.match(artifactPublisher, /readonly_rpc_required/);
assert.match(app, /degraded/);
assert.match(app, /runtime\.serviceRoute\.join\(" -> "\)/);
assert.doesNotMatch(app, /callRpc\("hardware\./);