From 172fb7dae806134164cf5aef4fca7aa0261eedcd Mon Sep 17 00:00:00 2001 From: FlowmemoryAI <283694809+FlowmemoryAI@users.noreply.github.com> Date: Wed, 13 May 2026 17:32:50 -0500 Subject: [PATCH] Expose browser-safe control plane endpoints --- services/control-plane/src/server.ts | 46 ++++++++++++++----- .../control-plane/test/control-plane.test.ts | 38 +++++++++++++++ 2 files changed, 73 insertions(+), 11 deletions(-) diff --git a/services/control-plane/src/server.ts b/services/control-plane/src/server.ts index 08185c96..84f4a9b2 100644 --- a/services/control-plane/src/server.ts +++ b/services/control-plane/src/server.ts @@ -1,4 +1,4 @@ -import { createServer } from "node:http"; +import { createServer, type ServerResponse } from "node:http"; import { fileURLToPath } from "node:url"; import { dispatchJsonRpc } from "./json-rpc.ts"; @@ -9,6 +9,22 @@ interface ServerOptions { port: number; } +const jsonHeaders = { + "access-control-allow-headers": "content-type", + "access-control-allow-methods": "GET,POST,OPTIONS", + "access-control-allow-origin": "*", + "content-type": "application/json", +}; + +function jsonResult(response: ReturnType): unknown { + return Array.isArray(response) ? response : response?.result ?? response; +} + +function writeJson(res: ServerResponse, statusCode: number, body: unknown): void { + res.writeHead(statusCode, jsonHeaders); + res.end(`${JSON.stringify(body)}\n`); +} + function parseArgs(args: string[]): ServerOptions { const options: ServerOptions = { host: "127.0.0.1", @@ -44,16 +60,26 @@ function parseArgs(args: string[]): ServerOptions { export function startControlPlaneServer(options: ServerOptions): ReturnType { const state = loadControlPlaneState(); const server = createServer((req, res) => { + if (req.method === "OPTIONS") { + res.writeHead(204, jsonHeaders); + res.end(); + return; + } + if (req.method === "GET" && req.url === "/health") { const response = dispatchJsonRpc({ jsonrpc: "2.0", id: "health", method: "health" }, { state }); - res.writeHead(200, { "content-type": "application/json" }); - res.end(`${JSON.stringify(Array.isArray(response) ? response : response?.result ?? response)}\n`); + writeJson(res, 200, jsonResult(response)); + return; + } + + if (req.method === "GET" && req.url === "/state") { + const response = dispatchJsonRpc({ jsonrpc: "2.0", id: "state", method: "devnet_state" }, { state }); + writeJson(res, 200, jsonResult(response)); return; } if (req.method !== "POST" || req.url !== "/rpc") { - res.writeHead(404, { "content-type": "application/json" }); - res.end(JSON.stringify({ error: "not found" })); + writeJson(res, 404, { error: "not found" }); return; } @@ -67,15 +93,13 @@ export function startControlPlaneServer(options: ServerOptions): ReturnType { @@ -170,3 +172,39 @@ test("smoke client queries the complete local lifecycle surface", () => { assert.equal(smoke.methodCount, 31); assert.ok((smoke.responseSchemas as string[]).includes("flowmemory.control_plane.raw_json.v0")); }); + +test("HTTP server exposes browser-safe health and state endpoints", async () => { + const server = startControlPlaneServer({ host: "127.0.0.1", port: 0 }); + + try { + await once(server, "listening"); + const address = server.address(); + assert.equal(typeof address, "object"); + assert.notEqual(address, null); + const port = address?.port; + + const health = await fetch(`http://127.0.0.1:${port}/health`, { + headers: { Origin: "http://127.0.0.1:5173" }, + }); + assert.equal(health.status, 200); + assert.equal(health.headers.get("access-control-allow-origin"), "*"); + assert.equal((await health.json()).status, "ok"); + + const state = await fetch(`http://127.0.0.1:${port}/state`, { + headers: { Origin: "http://127.0.0.1:5173" }, + }); + assert.equal(state.status, 200); + assert.equal(state.headers.get("access-control-allow-origin"), "*"); + assert.equal((await state.json()).schema, "flowmemory.control_plane.devnet_state.v0"); + } finally { + await new Promise((resolve, reject) => { + server.close((error) => { + if (error) { + reject(error); + return; + } + resolve(); + }); + }); + } +});