@lifeaitools/clauth 1.30.7 → 1.30.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli/commands/serve.js
CHANGED
|
@@ -572,16 +572,21 @@ function makeWriteToken() {
|
|
|
572
572
|
};
|
|
573
573
|
}
|
|
574
574
|
|
|
575
|
-
function validateWriteToken(req, writeSession) {
|
|
576
|
-
if (!writeSession?.token || Date.now() > writeSession.expiresAt) return false;
|
|
577
|
-
const header = req.headers["x-clauth-write-token"] || req.headers.authorization;
|
|
578
|
-
const token = Array.isArray(header) ? header[0] : header;
|
|
579
|
-
if (!token) return false;
|
|
575
|
+
function validateWriteToken(req, writeSession) {
|
|
576
|
+
if (!writeSession?.token || Date.now() > writeSession.expiresAt) return false;
|
|
577
|
+
const header = req.headers["x-clauth-write-token"] || req.headers.authorization;
|
|
578
|
+
const token = Array.isArray(header) ? header[0] : header;
|
|
579
|
+
if (!token) return false;
|
|
580
580
|
const supplied = String(token).startsWith("Bearer ") ? String(token).slice(7) : String(token);
|
|
581
581
|
const a = Buffer.from(supplied);
|
|
582
|
-
const b = Buffer.from(writeSession.token);
|
|
583
|
-
return a.length === b.length && crypto.timingSafeEqual(a, b);
|
|
584
|
-
}
|
|
582
|
+
const b = Buffer.from(writeSession.token);
|
|
583
|
+
return a.length === b.length && crypto.timingSafeEqual(a, b);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
export function supervisorRequiresWriteToken(port = getSupervisorPort(), env = process.env) {
|
|
587
|
+
if (port !== getSupervisorPort()) return true;
|
|
588
|
+
return env.CLAUTH_SUPERVISOR_REQUIRE_WRITE_TOKEN === "1";
|
|
589
|
+
}
|
|
585
590
|
|
|
586
591
|
// ── PID helpers ──────────────────────────────────────────────
|
|
587
592
|
function readPid() {
|
|
@@ -3167,13 +3172,29 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
3167
3172
|
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
3168
3173
|
"Access-Control-Allow-Headers": "Content-Type, Authorization, Mcp-Session-Id, mcp-protocol-version, mcp-session-id",
|
|
3169
3174
|
};
|
|
3170
|
-
const NO_BROWSER_CORS = {
|
|
3171
|
-
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
3172
|
-
"Access-Control-Allow-Headers": "Content-Type, Authorization, Mcp-Session-Id, mcp-protocol-version, mcp-session-id",
|
|
3173
|
-
};
|
|
3174
|
-
const studioDebugRuntime = createStudioDebugRuntime({ port, logFile: LOG_FILE });
|
|
3175
|
-
|
|
3176
|
-
|
|
3175
|
+
const NO_BROWSER_CORS = {
|
|
3176
|
+
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
|
|
3177
|
+
"Access-Control-Allow-Headers": "Content-Type, Authorization, Mcp-Session-Id, mcp-protocol-version, mcp-session-id",
|
|
3178
|
+
};
|
|
3179
|
+
const studioDebugRuntime = createStudioDebugRuntime({ port, logFile: LOG_FILE });
|
|
3180
|
+
const isSupervisorPort = port === getSupervisorPort();
|
|
3181
|
+
const supervisorTestNoToken = process.env.CLAUTH_SUPERVISOR_TEST_NO_TOKEN === "1";
|
|
3182
|
+
|
|
3183
|
+
function hasSupervisorWrite(req) {
|
|
3184
|
+
if (validateWriteToken(req, writeSession)) return true;
|
|
3185
|
+
if (!isSupervisorPort) return false;
|
|
3186
|
+
return supervisorTestNoToken || !supervisorRequiresWriteToken(port);
|
|
3187
|
+
}
|
|
3188
|
+
|
|
3189
|
+
function rejectSupervisorWrite(res) {
|
|
3190
|
+
res.writeHead(403, { "Content-Type": "application/json", ...CORS });
|
|
3191
|
+
return res.end(JSON.stringify({
|
|
3192
|
+
error: "write_token_required",
|
|
3193
|
+
hint: "Unlock clauth writes, unset CLAUTH_SUPERVISOR_REQUIRE_WRITE_TOKEN for localhost supervisor operations, or set CLAUTH_SUPERVISOR_TEST_NO_TOKEN=1 for temporary localhost-only supervisor tests.",
|
|
3194
|
+
}));
|
|
3195
|
+
}
|
|
3196
|
+
|
|
3197
|
+
// ── MCP SSE session tracking ──────────────────────────────
|
|
3177
3198
|
const sseSessions = new Map(); // sessionId → { res, initialized }
|
|
3178
3199
|
let sseCounter = 0;
|
|
3179
3200
|
|
|
@@ -3641,19 +3662,13 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
3641
3662
|
}
|
|
3642
3663
|
|
|
3643
3664
|
if (method === "POST" && reqPath === "/v1/plugins/rescan") {
|
|
3644
|
-
if (!
|
|
3645
|
-
res.writeHead(403, { "Content-Type": "application/json", ...CORS });
|
|
3646
|
-
return res.end(JSON.stringify({ error: "write_token_required" }));
|
|
3647
|
-
}
|
|
3665
|
+
if (!hasSupervisorWrite(req)) return rejectSupervisorWrite(res);
|
|
3648
3666
|
return ok(res, discoverPlugins());
|
|
3649
3667
|
}
|
|
3650
3668
|
|
|
3651
3669
|
const pluginEnableMatch = reqPath.match(/^\/v1\/plugins\/([^/]+)\/(enable|disable|test|promote)$/);
|
|
3652
3670
|
if (method === "POST" && pluginEnableMatch) {
|
|
3653
|
-
if (!
|
|
3654
|
-
res.writeHead(403, { "Content-Type": "application/json", ...CORS });
|
|
3655
|
-
return res.end(JSON.stringify({ error: "write_token_required" }));
|
|
3656
|
-
}
|
|
3671
|
+
if (!hasSupervisorWrite(req)) return rejectSupervisorWrite(res);
|
|
3657
3672
|
const pluginId = decodeURIComponent(pluginEnableMatch[1]);
|
|
3658
3673
|
const op = pluginEnableMatch[2];
|
|
3659
3674
|
const result = op === "enable"
|
|
@@ -3671,10 +3686,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
3671
3686
|
|
|
3672
3687
|
const surfaceActionMatch = reqPath.match(/^\/v1\/surfaces\/([^/]+)\/actions$/);
|
|
3673
3688
|
if (method === "POST" && surfaceActionMatch) {
|
|
3674
|
-
if (!
|
|
3675
|
-
res.writeHead(403, { "Content-Type": "application/json", ...CORS });
|
|
3676
|
-
return res.end(JSON.stringify({ error: "write_token_required" }));
|
|
3677
|
-
}
|
|
3689
|
+
if (!hasSupervisorWrite(req)) return rejectSupervisorWrite(res);
|
|
3678
3690
|
let body;
|
|
3679
3691
|
try { body = await readBody(req); } catch {
|
|
3680
3692
|
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
@@ -3695,10 +3707,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
3695
3707
|
|
|
3696
3708
|
const tunnelRoutesMatch = reqPath.match(/^\/v1\/tunnels\/([^/]+)\/routes$/);
|
|
3697
3709
|
if (method === "POST" && tunnelRoutesMatch) {
|
|
3698
|
-
if (!
|
|
3699
|
-
res.writeHead(403, { "Content-Type": "application/json", ...CORS });
|
|
3700
|
-
return res.end(JSON.stringify({ error: "write_token_required" }));
|
|
3701
|
-
}
|
|
3710
|
+
if (!hasSupervisorWrite(req)) return rejectSupervisorWrite(res);
|
|
3702
3711
|
let body;
|
|
3703
3712
|
try { body = await readBody(req); } catch {
|
|
3704
3713
|
res.writeHead(400, { "Content-Type": "application/json", ...CORS });
|
|
@@ -3709,10 +3718,7 @@ function createServer(initPassword, whitelist, port, tunnelHostnameInit = null,
|
|
|
3709
3718
|
|
|
3710
3719
|
const tunnelRouteDeleteMatch = reqPath.match(/^\/v1\/tunnels\/([^/]+)\/routes\/([^/]+)$/);
|
|
3711
3720
|
if (method === "DELETE" && tunnelRouteDeleteMatch) {
|
|
3712
|
-
if (!
|
|
3713
|
-
res.writeHead(403, { "Content-Type": "application/json", ...CORS });
|
|
3714
|
-
return res.end(JSON.stringify({ error: "write_token_required" }));
|
|
3715
|
-
}
|
|
3721
|
+
if (!hasSupervisorWrite(req)) return rejectSupervisorWrite(res);
|
|
3716
3722
|
return ok(res, removeTunnelRoute(decodeURIComponent(tunnelRouteDeleteMatch[1]), decodeURIComponent(tunnelRouteDeleteMatch[2]), "localhost"));
|
|
3717
3723
|
}
|
|
3718
3724
|
|
|
@@ -372,10 +372,22 @@ export function runSurfaceAction(id, action, actor = "localhost") {
|
|
|
372
372
|
const port = surface.port === "auto" || !surface.port ? 0 : surface.port;
|
|
373
373
|
return operation(action, { surface_id: id }, surface, { ok: true, state: "candidate_testing", port, private: true, public_route: false, evidence: ["candidate surfaces bind localhost only"] }, actor);
|
|
374
374
|
}
|
|
375
|
-
const command = action === "stop" ? surface.stop : action === "start" ? surface.start : surface.restart;
|
|
376
375
|
if (surface.lifecycle_owner === "plugin") {
|
|
377
376
|
return operation(action, { surface_id: id }, surface, { ok: true, state: "delegated_to_plugin_adapter", evidence: ["plugin lifecycle owner retained"] }, actor);
|
|
378
377
|
}
|
|
378
|
+
if (action === "promote" || action === "rollback") {
|
|
379
|
+
return operation(action, { surface_id: id }, surface, {
|
|
380
|
+
ok: false,
|
|
381
|
+
state: "unsupported_surface_action",
|
|
382
|
+
reason: `${action}_is_plugin_candidate_lifecycle`,
|
|
383
|
+
evidence: ["surface action did not execute a process command"],
|
|
384
|
+
}, actor);
|
|
385
|
+
}
|
|
386
|
+
let command = null;
|
|
387
|
+
if (action === "stop") command = surface.stop;
|
|
388
|
+
else if (action === "start") command = surface.start;
|
|
389
|
+
else if (action === "restart") command = surface.restart;
|
|
390
|
+
else if (action === "reconcile") command = surface.enabled === false ? surface.stop : (surface.restart || surface.start);
|
|
379
391
|
if (!command || command.length === 0) {
|
|
380
392
|
return operation(action, { surface_id: id }, surface, { ok: false, state: "command_missing" }, actor);
|
|
381
393
|
}
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
supervisorHealth,
|
|
18
18
|
validatePluginManifest,
|
|
19
19
|
} from "./supervisor-registry.js";
|
|
20
|
+
import { supervisorRequiresWriteToken } from "./commands/serve.js";
|
|
20
21
|
|
|
21
22
|
function withTempSupervisor(fn) {
|
|
22
23
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "clauth-supervisor-"));
|
|
@@ -154,6 +155,34 @@ test("surface actions use dedicated clauth PM2 home and keep CodeFlow observe-on
|
|
|
154
155
|
assert.equal(supervisorHealth().surfaces, 2);
|
|
155
156
|
}));
|
|
156
157
|
|
|
158
|
+
test("surface promote and rollback never fall through to restart commands", () => withTempSupervisor((root) => {
|
|
159
|
+
const managed = path.join(root, "managed");
|
|
160
|
+
process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = managed;
|
|
161
|
+
process.env.CLAUTH_USER_PLUGIN_ROOTS = path.join(root, "user");
|
|
162
|
+
writePlugin(root, "managed", "demo", baseManifest("demo", {
|
|
163
|
+
surfaces: [{
|
|
164
|
+
id: "demo-surface",
|
|
165
|
+
name: "Demo surface",
|
|
166
|
+
health: "http://127.0.0.1:3333/health",
|
|
167
|
+
restart: [process.execPath, "--version"],
|
|
168
|
+
}],
|
|
169
|
+
}));
|
|
170
|
+
discoverPlugins();
|
|
171
|
+
|
|
172
|
+
for (const action of ["promote", "rollback"]) {
|
|
173
|
+
const receipt = runSurfaceAction("demo:demo-surface", action);
|
|
174
|
+
assert.equal(receipt.resulting_state.ok, false);
|
|
175
|
+
assert.equal(receipt.resulting_state.state, "unsupported_surface_action");
|
|
176
|
+
assert.equal(receipt.resulting_state.evidence[0], "surface action did not execute a process command");
|
|
177
|
+
}
|
|
178
|
+
}));
|
|
179
|
+
|
|
180
|
+
test("supervisor write-token policy is temporarily relaxed only for the localhost supervisor port", () => {
|
|
181
|
+
assert.equal(supervisorRequiresWriteToken(52439, {}), false);
|
|
182
|
+
assert.equal(supervisorRequiresWriteToken(52439, { CLAUTH_SUPERVISOR_REQUIRE_WRITE_TOKEN: "1" }), true);
|
|
183
|
+
assert.equal(supervisorRequiresWriteToken(52437, {}), true);
|
|
184
|
+
});
|
|
185
|
+
|
|
157
186
|
test("tunnel route add and remove produce reversible operation receipts", () => withTempSupervisor((root) => {
|
|
158
187
|
process.env.CLAUTH_MANAGED_PLUGIN_ROOTS = path.join(root, "managed");
|
|
159
188
|
process.env.CLAUTH_USER_PLUGIN_ROOTS = path.join(root, "user");
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|