@@ -18,7 +18,7 @@ app.use(express.json({ limit: "2mb" }));
1818// ---- basic CORS (no dependency)
1919app . 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
4545const 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.)
4949const PRIV_PEM_B64 = process . env . RECEIPT_SIGNING_PRIVATE_KEY_PEM_B64 || "" ;
5050const 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
5361const SERVICE_NAME = process . env . SERVICE_NAME || "commandlayer-runtime" ;
@@ -86,15 +94,16 @@ const ALLOW_FETCH_HOSTS = (process.env.ALLOW_FETCH_HOSTS || "")
8694const 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.
9197const VERIFY_SCHEMA_CACHED_ONLY = String ( process . env . VERIFY_SCHEMA_CACHED_ONLY || "1" ) === "1" ;
9298
9399// Prewarm knobs
94100const PREWARM_MAX_VERBS = Number ( process . env . PREWARM_MAX_VERBS || 25 ) ;
95101const PREWARM_TOTAL_BUDGET_MS = Number ( process . env . PREWARM_TOTAL_BUDGET_MS || 12000 ) ;
96102const 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+
98107function nowIso ( ) {
99108 return new Date ( ) . toISOString ( ) ;
100109}
@@ -105,8 +114,12 @@ function randId(prefix = "trace_") {
105114
106115function 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
112125function 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+
118144function 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
731759function 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
936967app . 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
9791014app . 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
9931029app . 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
10051042app . 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// -----------------------
10191057app . 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// -----------------------
10541091app . 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