Skip to content

Commit 13cf091

Browse files
authored
Update server.mjs
1 parent c1d517d commit 13cf091

File tree

1 file changed

+75
-43
lines changed

1 file changed

+75
-43
lines changed

server.mjs

Lines changed: 75 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ app.use(express.json({ limit: "2mb" }));
1818
// ---- basic CORS (no dependency)
1919
app.use((req, res, next) => {
2020
res.setHeader("Access-Control-Allow-Origin", "*");
21-
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
21+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Debug-Token");
2222
res.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS");
2323
if (req.method === "OPTIONS") return res.status(204).end();
2424
next();
@@ -44,10 +44,18 @@ const SIGNER_ID =
4444

4545
const SIGNER_KID = process.env.SIGNER_KID || "v1";
4646

47-
// NOTE: runtime-core expects PEM text, not base64.
48-
// We accept base64 envs and decode to PEM.
47+
// NOTE: runtime-core expects PEM text.
48+
// We accept EITHER raw PEM OR base64(PEM). (Railway envs vary.)
4949
const PRIV_PEM_B64 = process.env.RECEIPT_SIGNING_PRIVATE_KEY_PEM_B64 || "";
5050
const PUB_PEM_B64 = process.env.RECEIPT_SIGNING_PUBLIC_KEY_PEM_B64 || "";
51+
const PRIV_PEM_RAW = process.env.RECEIPT_SIGNING_PRIVATE_KEY_PEM || "";
52+
const PUB_PEM_RAW = process.env.RECEIPT_SIGNING_PUBLIC_KEY_PEM || "";
53+
54+
// Optional: allow a baked-in public key fallback for /verify when env isn't set.
55+
// (You provided this pubkey; safe to embed since it's public.)
56+
const EMBEDDED_PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----
57+
MCowBQYDK2VwAyEA7Vkkmt6R02Iltp/+i3D5mraZyvLjfuTSVB33KwfzQC8=
58+
-----END PUBLIC KEY-----`;
5159

5260
// ---- service identity / discovery
5361
const SERVICE_NAME = process.env.SERVICE_NAME || "commandlayer-runtime";
@@ -86,15 +94,16 @@ const ALLOW_FETCH_HOSTS = (process.env.ALLOW_FETCH_HOSTS || "")
8694
const VERIFY_MAX_MS = Number(process.env.VERIFY_MAX_MS || 30000);
8795

8896
// CRITICAL: edge-safe schema verify behavior
89-
// If true, /verify?schema=1 will NEVER compile or fetch; it will only validate if cached,
90-
// otherwise it returns 202 and queues warm.
9197
const VERIFY_SCHEMA_CACHED_ONLY = String(process.env.VERIFY_SCHEMA_CACHED_ONLY || "1") === "1";
9298

9399
// Prewarm knobs
94100
const PREWARM_MAX_VERBS = Number(process.env.PREWARM_MAX_VERBS || 25);
95101
const PREWARM_TOTAL_BUDGET_MS = Number(process.env.PREWARM_TOTAL_BUDGET_MS || 12000);
96102
const PREWARM_PER_VERB_BUDGET_MS = Number(process.env.PREWARM_PER_VERB_BUDGET_MS || 5000);
97103

104+
// Debug gating (do NOT leave debug open in prod)
105+
const DEBUG_TOKEN = process.env.DEBUG_TOKEN || ""; // if set, /debug/* requires header X-Debug-Token
106+
98107
function nowIso() {
99108
return new Date().toISOString();
100109
}
@@ -105,8 +114,12 @@ function randId(prefix = "trace_") {
105114

106115
function pemFromB64(b64) {
107116
if (!b64) return null;
108-
const pem = Buffer.from(b64, "base64").toString("utf8");
109-
return pem.includes("BEGIN") ? pem : null;
117+
try {
118+
const pem = Buffer.from(String(b64).trim(), "base64").toString("utf8");
119+
return pem.includes("BEGIN") ? pem : null;
120+
} catch {
121+
return null;
122+
}
110123
}
111124

112125
function normalizePem(text) {
@@ -115,6 +128,19 @@ function normalizePem(text) {
115128
return pem.includes("BEGIN") ? pem : null;
116129
}
117130

131+
function pemFromEnv({ b64, raw, fallback = null } = {}) {
132+
const pemDirect = normalizePem(raw);
133+
if (pemDirect) return pemDirect;
134+
const pemDecoded = pemFromB64(b64);
135+
if (pemDecoded) return pemDecoded;
136+
const pemFallback = normalizePem(fallback);
137+
return pemFallback || null;
138+
}
139+
140+
const PRIV_PEM = pemFromEnv({ b64: PRIV_PEM_B64, raw: PRIV_PEM_RAW, fallback: null });
141+
// Prefer env pubkey; fallback to embedded (public) key
142+
const PUB_PEM = pemFromEnv({ b64: PUB_PEM_B64, raw: PUB_PEM_RAW, fallback: EMBEDDED_PUBLIC_KEY_PEM });
143+
118144
function enabled(verb) {
119145
return ENABLED_VERBS.includes(verb);
120146
}
@@ -131,6 +157,15 @@ function requireBody(req, res) {
131157
return true;
132158
}
133159

160+
function requireDebug(req, res) {
161+
if (!DEBUG_TOKEN) return true; // local/dev
162+
if (req.headers["x-debug-token"] !== DEBUG_TOKEN) {
163+
res.status(403).json({ ok: false, error: "forbidden" });
164+
return false;
165+
}
166+
return true;
167+
}
168+
134169
// -----------------------
135170
// SSRF guard for fetch()
136171
// -----------------------
@@ -336,11 +371,7 @@ async function getValidatorForVerb(verb) {
336371
}
337372

338373
const schema = await fetchJsonWithTimeout(url, SCHEMA_FETCH_TIMEOUT_MS);
339-
const validate = await withTimeout(
340-
ajv.compileAsync(schema),
341-
SCHEMA_VALIDATE_BUDGET_MS,
342-
"ajv_compile_budget_exceeded"
343-
);
374+
const validate = await withTimeout(ajv.compileAsync(schema), SCHEMA_VALIDATE_BUDGET_MS, "ajv_compile_budget_exceeded");
344375
validatorCache.set(verb, { compiledAt: Date.now(), validate });
345376
return validate;
346377
})().finally(() => inflightValidator.delete(verb));
@@ -425,14 +456,13 @@ function makeReceipt({ x402, trace, result, status = "success", error = null, de
425456

426457
if (actor) receipt.metadata.actor = actor;
427458

428-
const privPem = pemFromB64(PRIV_PEM_B64);
429-
if (!privPem) throw new Error("Missing RECEIPT_SIGNING_PRIVATE_KEY_PEM_B64");
459+
if (!PRIV_PEM) throw new Error("Missing signing private key (set RECEIPT_SIGNING_PRIVATE_KEY_PEM or _PEM_B64)");
430460

431461
receipt = signReceiptEd25519Sha256(receipt, {
432462
signer_id: SIGNER_ID,
433463
kid: SIGNER_KID,
434464
canonical: CANONICAL_ID_SORTED_KEYS_V1,
435-
privateKeyPem: privPem,
465+
privateKeyPem: PRIV_PEM,
436466
});
437467

438468
return receipt;
@@ -467,7 +497,6 @@ async function doFetch(body) {
467497
if (done) break;
468498
received += value.byteLength;
469499
if (received > FETCH_MAX_BYTES) {
470-
// FIX: cancel stream once we decide to truncate
471500
try {
472501
await reader.cancel();
473502
} catch {}
@@ -710,8 +739,7 @@ function doExplain(body) {
710739

711740
let explanation = "";
712741
if (audience === "novice") {
713-
explanation =
714-
`**${subject}** are like “tamper-proof receipts” for agent actions.\n\n` + core.map((s) => `- ${s}`).join("\n");
742+
explanation = `**${subject}** are like “tamper-proof receipts” for agent actions.\n\n` + core.map((s) => `- ${s}`).join("\n");
715743
} else {
716744
explanation =
717745
`**${subject}** are cryptographically verifiable execution artifacts that bind intent (verb+version), semantics (schema), and output into a signed proof.\n\n` +
@@ -729,8 +757,10 @@ function doExplain(body) {
729757
}
730758

731759
function doAnalyze(body) {
732-
const input = String(body?.input ?? "");
733-
if (!input.trim()) throw new Error("analyze.input required (string)");
760+
// Accept both input as string OR {content:"..."} to avoid "[object Object]" failures.
761+
const raw = body?.input;
762+
const input = typeof raw === "string" ? raw : String(raw?.content ?? "");
763+
if (!String(input).trim()) throw new Error("analyze.input required (string or {content})");
734764
const goal = String(body?.goal ?? "").trim();
735765
const hints = Array.isArray(body?.hints) ? body.hints.map(String) : [];
736766
const lines = input.split(/\r?\n/).filter((l) => l.trim() !== "");
@@ -832,12 +862,13 @@ async function handleVerb(verb, req, res) {
832862

833863
const started = Date.now();
834864

835-
// Schema legality: only include parent_trace_id if it's a non-empty string
865+
// Trace: honor inbound trace_id if present
866+
const inboundTraceId = typeof req.body?.trace?.trace_id === "string" ? req.body.trace.trace_id.trim() : "";
836867
const rawParent = req.body?.trace?.parent_trace_id ?? req.body?.x402?.extras?.parent_trace_id ?? null;
837868
const parentTraceId = typeof rawParent === "string" && rawParent.trim().length ? rawParent.trim() : null;
838869

839870
const trace = {
840-
trace_id: randId("trace_"),
871+
trace_id: inboundTraceId || randId("trace_"),
841872
...(parentTraceId ? { parent_trace_id: parentTraceId } : {}),
842873
started_at: nowIso(),
843874
completed_at: null,
@@ -927,13 +958,14 @@ app.get("/health", (req, res) => {
927958
port: PORT,
928959
enabled_verbs: ENABLED_VERBS,
929960
signer_id: SIGNER_ID,
930-
signer_ok: !!pemFromB64(PRIV_PEM_B64),
961+
signer_ok: !!PRIV_PEM,
931962
time: nowIso(),
932963
})
933964
);
934965
});
935966

936967
app.get("/debug/env", (req, res) => {
968+
if (!requireDebug(req, res)) return;
937969
res.json({
938970
ok: true,
939971
node: process.version,
@@ -942,9 +974,12 @@ app.get("/debug/env", (req, res) => {
942974
enabled_verbs: ENABLED_VERBS,
943975
signer_id: SIGNER_ID,
944976
signer_kid: SIGNER_KID,
945-
signer_ok: !!pemFromB64(PRIV_PEM_B64),
977+
signer_ok: !!PRIV_PEM,
946978
has_priv_b64: !!PRIV_PEM_B64,
979+
has_priv_pem: !!PRIV_PEM_RAW,
947980
has_pub_b64: !!PUB_PEM_B64,
981+
has_pub_pem: !!PUB_PEM_RAW,
982+
using_embedded_pubkey_fallback: !pemFromEnv({ b64: PUB_PEM_B64, raw: PUB_PEM_RAW, fallback: null }) && !!PUB_PEM,
948983
verifier_ens_name: VERIFIER_ENS_NAME || null,
949984
ens_pubkey_text_key: ENS_PUBKEY_TEXT_KEY,
950985
has_rpc: hasRpc(),
@@ -977,6 +1012,7 @@ app.get("/debug/env", (req, res) => {
9771012
});
9781013

9791014
app.get("/debug/enskey", async (req, res) => {
1015+
if (!requireDebug(req, res)) return;
9801016
const refresh = String(req.query.refresh || "0") === "1";
9811017
const out = await fetchEnsPubkeyPem({ refresh });
9821018
res.json({
@@ -991,6 +1027,7 @@ app.get("/debug/enskey", async (req, res) => {
9911027
});
9921028

9931029
app.get("/debug/schemafetch", (req, res) => {
1030+
if (!requireDebug(req, res)) return;
9941031
const verb = String(req.query.verb || "").trim();
9951032
if (!verb) return res.status(400).json({ ok: false, error: "missing verb" });
9961033
const url = receiptSchemaUrlForVerb(verb);
@@ -1003,6 +1040,7 @@ app.get("/debug/schemafetch", (req, res) => {
10031040
});
10041041

10051042
app.get("/debug/validators", (req, res) => {
1043+
if (!requireDebug(req, res)) return;
10061044
res.json({
10071045
ok: true,
10081046
cached: Array.from(validatorCache.keys()),
@@ -1017,14 +1055,14 @@ app.get("/debug/validators", (req, res) => {
10171055
// EDGE-SAFE prewarm: responds immediately, warms AFTER response
10181056
// -----------------------
10191057
app.post("/debug/prewarm", (req, res) => {
1058+
if (!requireDebug(req, res)) return;
10201059
const verbs = Array.isArray(req.body?.verbs) ? req.body.verbs : [];
10211060
const cleaned = verbs
10221061
.map((v) => String(v || "").trim())
10231062
.filter(Boolean)
10241063
.slice(0, PREWARM_MAX_VERBS);
10251064

10261065
const supported = cleaned.filter((v) => handlers[v]);
1027-
10281066
for (const v of supported) warmQueue.add(v);
10291067

10301068
res.json({
@@ -1047,9 +1085,8 @@ for (const v of Object.keys(handlers)) {
10471085

10481086
// -----------------------
10491087
// verify endpoint (schema validation + ENS pubkey)
1050-
// - schema=1 (default off) is EDGE-SAFE:
1051-
// if VERIFY_SCHEMA_CACHED_ONLY=1, it only validates if cached; otherwise returns 202 and queues warm.
1052-
// - ens=1 resolves pubkey from ENS (still bounded by VERIFY_MAX_MS)
1088+
// - schema=1 is EDGE-SAFE when VERIFY_SCHEMA_CACHED_ONLY=1
1089+
// - ens=1 resolves pubkey from ENS (bounded)
10531090
// -----------------------
10541091
app.post("/verify", async (req, res) => {
10551092
const work = (async () => {
@@ -1083,17 +1120,15 @@ app.post("/verify", async (req, res) => {
10831120
return fail(400, "missing metadata.proof.signature_b64 or hash_sha256");
10841121
}
10851122

1086-
// 1) pick pubkey (env -> optional ENS)
1087-
let pubPem = pemFromB64(PUB_PEM_B64);
1088-
let pubSrc = pubPem ? "env-b64" : null;
1123+
// 1) pick pubkey: env/raw -> env/b64 -> embedded -> optional ENS override
1124+
let pubPem = PUB_PEM;
1125+
let pubSrc = pubPem ? (PUB_PEM_RAW ? "env-pem" : PUB_PEM_B64 ? "env-b64" : "embedded") : null;
10891126

10901127
if (wantEns) {
10911128
const ensOut = await fetchEnsPubkeyPem({ refresh });
10921129
if (ensOut.ok && ensOut.pem) {
10931130
pubPem = ensOut.pem;
10941131
pubSrc = "ens";
1095-
} else if (!pubPem) {
1096-
pubSrc = null;
10971132
}
10981133
}
10991134

@@ -1108,20 +1143,19 @@ app.post("/verify", async (req, res) => {
11081143

11091144
const sigOk = !!v.ok;
11101145

1111-
// FIX: tighten ternary formatting to avoid parsing weirdness
11121146
const sigErr = pubPem
11131147
? sigOk
11141148
? null
11151149
: (v.reason || "verify failed")
1116-
: "no public key available (set RECEIPT_SIGNING_PUBLIC_KEY_PEM_B64 or pass ens=1 with ETH_RPC_URL)";
1150+
: "no public key available (set RECEIPT_SIGNING_PUBLIC_KEY_PEM/_PEM_B64 or pass ens=1 with ETH_RPC_URL)";
11171151

1118-
// FIX: Hash match truthfulness
1119-
// - If signature is valid, hash match is implicitly true (it verified).
1120-
// - If signature is invalid and reason is "hash_mismatch", it's false.
1152+
// Truthful hash reporting:
1153+
// - If signature verifies, hash match is true.
1154+
// - If verifier says hash_mismatch, it's false.
11211155
// - Otherwise unknown (null).
11221156
const hashMatches = sigOk ? true : (v.reason === "hash_mismatch" ? false : null);
11231157

1124-
// FIX: We are NOT recomputing hash here; keep null unless you add an explicit helper.
1158+
// We are not recomputing hash here (keep null unless runtime-core exposes a helper)
11251159
const recomputed = null;
11261160

11271161
// 3) schema validation (edge-safe)
@@ -1156,9 +1190,7 @@ app.post("/verify", async (req, res) => {
11561190
});
11571191
} else {
11581192
try {
1159-
const validate = VERIFY_SCHEMA_CACHED_ONLY
1160-
? validatorCache.get(verb)?.validate
1161-
: await getValidatorForVerb(verb);
1193+
const validate = VERIFY_SCHEMA_CACHED_ONLY ? validatorCache.get(verb)?.validate : await getValidatorForVerb(verb);
11621194

11631195
if (!validate) {
11641196
schemaOk = false;
@@ -1176,7 +1208,7 @@ app.post("/verify", async (req, res) => {
11761208
}
11771209

11781210
return res.json({
1179-
ok: hashMatches === true && sigOk === true && schemaOk === true,
1211+
ok: (hashMatches === true) && (sigOk === true) && (schemaOk === true),
11801212
checks: { schema_valid: schemaOk, hash_matches: hashMatches, signature_valid: sigOk },
11811213
values: {
11821214
verb: receipt?.x402?.verb ?? null,

0 commit comments

Comments
 (0)