11// server.mjs
22import express from "express" ;
3- import { signReceiptEd25519Sha256 , verifyReceiptEd25519Sha256 , CANONICAL_ID_SORTED_KEYS_V1 } from "@commandlayer/runtime-core" ;
43import crypto from "crypto" ;
54import Ajv from "ajv" ;
65import addFormats from "ajv-formats" ;
76import { ethers } from "ethers" ;
87import net from "net" ;
98
9+ import {
10+ signReceiptEd25519Sha256 ,
11+ verifyReceiptEd25519Sha256 ,
12+ CANONICAL_ID_SORTED_KEYS_V1
13+ } from "@commandlayer/runtime-core" ;
14+
1015const app = express ( ) ;
1116app . use ( express . json ( { limit : "2mb" } ) ) ;
1217
@@ -22,12 +27,17 @@ app.use((req, res, next) => {
2227const PORT = Number ( process . env . PORT || 8080 ) ;
2328
2429// ---- runtime config
25- const ENABLED_VERBS = ( process . env . ENABLED_VERBS || "fetch,describe,format,clean,parse,summarize,convert,explain,analyze,classify" )
30+ const ENABLED_VERBS = ( process . env . ENABLED_VERBS ||
31+ "fetch,describe,format,clean,parse,summarize,convert,explain,analyze,classify" )
2632 . split ( "," )
2733 . map ( ( s ) => s . trim ( ) )
2834 . filter ( Boolean ) ;
2935
30- const SIGNER_ID = process . env . RECEIPT_SIGNER_ID || process . env . ENS_NAME || "runtime" ;
36+ const SIGNER_ID = process . env . RECEIPT_SIGNER_ID || process . env . ENS_NAME || "runtime.commandlayer.eth" ;
37+ const SIGNER_KID = process . env . SIGNER_KID || "v1" ;
38+
39+ // NOTE: runtime-core expects PEM text, not base64.
40+ // We accept base64 envs (as you already do) and decode to PEM.
3141const PRIV_PEM_B64 = process . env . RECEIPT_SIGNING_PRIVATE_KEY_PEM_B64 || "" ;
3242const PUB_PEM_B64 = process . env . RECEIPT_SIGNING_PUBLIC_KEY_PEM_B64 || "" ;
3343
@@ -70,7 +80,7 @@ const VERIFY_MAX_MS = Number(process.env.VERIFY_MAX_MS || 30000);
7080// CRITICAL: edge-safe schema verify behavior
7181// If true, /verify?schema=1 will NEVER compile or fetch; it will only validate if cached,
7282// otherwise it returns 202 and queues warm.
73- // Default true (this is what prevents Railway edge 502s).
83+ // Default true (this is what prevents edge 502s).
7484const VERIFY_SCHEMA_CACHED_ONLY = String ( process . env . VERIFY_SCHEMA_CACHED_ONLY || "1" ) === "1" ;
7585
7686// Prewarm knobs
@@ -86,25 +96,6 @@ function randId(prefix = "trace_") {
8696 return prefix + crypto . randomBytes ( 6 ) . toString ( "hex" ) ;
8797}
8898
89- // Stable stringify (deterministic object key order)
90- function stableStringify ( value ) {
91- const seen = new WeakSet ( ) ;
92- const helper = ( v ) => {
93- if ( v === null || typeof v !== "object" ) return v ;
94- if ( seen . has ( v ) ) return "[Circular]" ;
95- seen . add ( v ) ;
96- if ( Array . isArray ( v ) ) return v . map ( helper ) ;
97- const out = { } ;
98- for ( const k of Object . keys ( v ) . sort ( ) ) out [ k ] = helper ( v [ k ] ) ;
99- return out ;
100- } ;
101- return JSON . stringify ( helper ( value ) ) ;
102- }
103-
104- function sha256Hex ( str ) {
105- return crypto . createHash ( "sha256" ) . update ( str ) . digest ( "hex" ) ;
106- }
107-
10899function pemFromB64 ( b64 ) {
109100 if ( ! b64 ) return null ;
110101 const pem = Buffer . from ( b64 , "base64" ) . toString ( "utf8" ) ;
@@ -117,27 +108,14 @@ function normalizePem(text) {
117108 return pem . includes ( "BEGIN" ) ? pem : null ;
118109}
119110
120- function signEd25519Base64 ( messageUtf8 ) {
121- const pem = pemFromB64 ( PRIV_PEM_B64 ) ;
122- if ( ! pem ) throw new Error ( "Missing RECEIPT_SIGNING_PRIVATE_KEY_PEM_B64" ) ;
123- const key = crypto . createPrivateKey ( pem ) ;
124- const sig = crypto . sign ( null , Buffer . from ( messageUtf8 , "utf8" ) , key ) ;
125- return sig . toString ( "base64" ) ;
126- }
127-
128- function verifyEd25519Base64 ( messageUtf8 , signatureB64 , pubPem ) {
129- const key = crypto . createPublicKey ( pubPem ) ;
130- return crypto . verify ( null , Buffer . from ( messageUtf8 , "utf8" ) , key , Buffer . from ( signatureB64 , "base64" ) ) ;
111+ function enabled ( verb ) {
112+ return ENABLED_VERBS . includes ( verb ) ;
131113}
132114
133115function makeError ( code , message , extra = { } ) {
134116 return { status : "error" , code, message, ...extra } ;
135117}
136118
137- function enabled ( verb ) {
138- return ENABLED_VERBS . includes ( verb ) ;
139- }
140-
141119function requireBody ( req , res ) {
142120 if ( ! req . body || typeof req . body !== "object" ) {
143121 res . status ( 400 ) . json ( makeError ( 400 , "Invalid JSON body" ) ) ;
@@ -411,10 +389,10 @@ function startWarmWorker() {
411389}
412390
413391// -----------------------
414- // receipts (receipt_id excluded from canonical hash )
392+ // receipts (runtime-core: single source of truth )
415393// -----------------------
416394function makeReceipt ( { x402, trace, result, status = "success" , error = null , delegation_result = null , actor = null } ) {
417- const receipt = {
395+ let receipt = {
418396 status,
419397 x402,
420398 trace,
@@ -424,8 +402,9 @@ function makeReceipt({ x402, trace, result, status = "success", error = null, de
424402 metadata : {
425403 proof : {
426404 alg : "ed25519-sha256" ,
427- canonical : "json-stringify" ,
405+ canonical : CANONICAL_ID_SORTED_KEYS_V1 ,
428406 signer_id : SIGNER_ID ,
407+ kid : SIGNER_KID ,
429408 hash_sha256 : null ,
430409 signature_b64 : null ,
431410 } ,
@@ -435,18 +414,15 @@ function makeReceipt({ x402, trace, result, status = "success", error = null, de
435414
436415 if ( actor ) receipt . metadata . actor = actor ;
437416
438- const unsigned = structuredClone ( receipt ) ;
439- unsigned . metadata . proof . hash_sha256 = "" ;
440- unsigned . metadata . proof . signature_b64 = "" ;
441- unsigned . metadata . receipt_id = "" ;
442-
443- const canonical = stableStringify ( unsigned ) ;
444- const hash = sha256Hex ( canonical ) ;
445- const sigB64 = signEd25519Base64 ( hash ) ;
417+ const privPem = pemFromB64 ( PRIV_PEM_B64 ) ;
418+ if ( ! privPem ) throw new Error ( "Missing RECEIPT_SIGNING_PRIVATE_KEY_PEM_B64" ) ;
446419
447- receipt . metadata . proof . hash_sha256 = hash ;
448- receipt . metadata . proof . signature_b64 = sigB64 ;
449- receipt . metadata . receipt_id = hash ;
420+ receipt = signReceiptEd25519Sha256 ( receipt , {
421+ signer_id : SIGNER_ID ,
422+ kid : SIGNER_KID ,
423+ canonical : CANONICAL_ID_SORTED_KEYS_V1 ,
424+ privateKeyPem : privPem ,
425+ } ) ;
450426
451427 return receipt ;
452428}
@@ -628,6 +604,10 @@ function doParse(body) {
628604 return result ;
629605}
630606
607+ function sha256HexUtf8 ( str ) {
608+ return crypto . createHash ( "sha256" ) . update ( String ( str ) , "utf8" ) . digest ( "hex" ) ;
609+ }
610+
631611function doSummarize ( body ) {
632612 const input = body ?. input || { } ;
633613 const content = String ( input . content ?? "" ) ;
@@ -645,7 +625,7 @@ function doSummarize(body) {
645625 }
646626 if ( ! summary ) summary = content . slice ( 0 , 400 ) . trim ( ) ;
647627
648- const srcHash = sha256Hex ( content ) ;
628+ const srcHash = sha256HexUtf8 ( content ) ;
649629 const cr = summary . length ? Number ( ( content . length / summary . length ) . toFixed ( 3 ) ) : 0 ;
650630
651631 return { summary, format : format === "markdown" ? "markdown" : "text" , compression_ratio : cr , source_hash : srcHash } ;
@@ -850,7 +830,10 @@ async function handleVerb(verb, req, res) {
850830 const x402 = req . body ?. x402 || { verb, version : "1.0.0" , entry : `x402://${ verb } agent.eth/${ verb } /v1.0.0` } ;
851831
852832 const callerTimeout = Number ( req . body ?. limits ?. timeout_ms || req . body ?. limits ?. max_latency_ms || 0 ) ;
853- const timeoutMs = Math . min ( SERVER_MAX_HANDLER_MS , callerTimeout && callerTimeout > 0 ? callerTimeout : SERVER_MAX_HANDLER_MS ) ;
833+ const timeoutMs = Math . min (
834+ SERVER_MAX_HANDLER_MS ,
835+ callerTimeout && callerTimeout > 0 ? callerTimeout : SERVER_MAX_HANDLER_MS
836+ ) ;
854837
855838 const work = Promise . resolve ( handlers [ verb ] ( req . body ) ) ;
856839 const result = timeoutMs
@@ -942,6 +925,7 @@ app.get("/debug/env", (req, res) => {
942925 service : process . env . RAILWAY_SERVICE_NAME || "runtime" ,
943926 enabled_verbs : ENABLED_VERBS ,
944927 signer_id : SIGNER_ID ,
928+ signer_kid : SIGNER_KID ,
945929 signer_ok : ! ! pemFromB64 ( PRIV_PEM_B64 ) ,
946930 has_priv_b64 : ! ! PRIV_PEM_B64 ,
947931 has_pub_b64 : ! ! PUB_PEM_B64 ,
@@ -952,7 +936,6 @@ app.get("/debug/env", (req, res) => {
952936 schema_fetch_timeout_ms : SCHEMA_FETCH_TIMEOUT_MS ,
953937 schema_validate_budget_ms : SCHEMA_VALIDATE_BUDGET_MS ,
954938 verify_schema_cached_only : VERIFY_SCHEMA_CACHED_ONLY ,
955-
956939 enable_ssrf_guard : ENABLE_SSRF_GUARD ,
957940 fetch_timeout_ms : FETCH_TIMEOUT_MS ,
958941 fetch_max_bytes : FETCH_MAX_BYTES ,
@@ -973,6 +956,7 @@ app.get("/debug/env", (req, res) => {
973956 service_version : SERVICE_VERSION ,
974957 api_version : API_VERSION ,
975958 canonical_base_url : CANONICAL_BASE ,
959+ canonical_id : CANONICAL_ID_SORTED_KEYS_V1 ,
976960 } ) ;
977961} ) ;
978962
@@ -1083,14 +1067,7 @@ app.post("/verify", async (req, res) => {
10831067 return fail ( 400 , "missing metadata.proof.signature_b64 or hash_sha256" ) ;
10841068 }
10851069
1086- const v = verifyReceiptEd25519Sha256 ( receipt , {
1087- publicKeyPemOrDer : pubPem ,
1088- allowedCanonicals : [ CANONICAL_ID_SORTED_KEYS_V1 ]
1089- } ) ;
1090-
1091- const hashMatches = v . ok ? true : ( v . reason === "bad_signature" ? true : ( v . reason === "hash_mismatch" ? false : false ) ) ;
1092- const recomputed = hashMatches ? proof . hash_sha256 : null ;
1093-
1070+ // 1) pick pubkey (env -> optional ENS)
10941071 let pubPem = pemFromB64 ( PUB_PEM_B64 ) ;
10951072 let pubSrc = pubPem ? "env-b64" : null ;
10961073
@@ -1104,18 +1081,25 @@ app.post("/verify", async (req, res) => {
11041081 }
11051082 }
11061083
1107- let sigOk = false ;
1108- let sigErr = null ;
1109-
1084+ // 2) verify (runtime-core canonical + hash + signature)
1085+ let v = { ok : false , reason : "no_public_key" } ;
11101086 if ( pubPem ) {
1111- sigOk = v . ok ;
1112- sigErr = v . ok ? null : ( v . reason || "verify failed" ) ;
1113- } else {
1114- sigOk = false ;
1115- sigErr = "no public key available (set RECEIPT_SIGNING_PUBLIC_KEY_PEM_B64 or pass ens=1 with ETH_RPC_URL)" ;
1087+ v = verifyReceiptEd25519Sha256 ( receipt , {
1088+ publicKeyPemOrDer : pubPem ,
1089+ allowedCanonicals : [ CANONICAL_ID_SORTED_KEYS_V1 ] ,
1090+ } ) ;
11161091 }
11171092
1118- // Schema validation (edge-safe)
1093+ const sigOk = ! ! v . ok ;
1094+ const sigErr =
1095+ pubPem ? ( sigOk ? null : ( v . reason || "verify failed" ) ) :
1096+ "no public key available (set RECEIPT_SIGNING_PUBLIC_KEY_PEM_B64 or pass ens=1 with ETH_RPC_URL)" ;
1097+
1098+ // Hash match: runtime-core reasons should already reflect canonical/hash mismatch
1099+ const hashMatches = sigOk ? true : ( v . reason === "hash_mismatch" ? false : true ) ;
1100+ const recomputed = hashMatches ? ( proof . hash_sha256 || null ) : null ;
1101+
1102+ // 3) schema validation (edge-safe)
11191103 let schemaOk = true ;
11201104 let schemaErrors = null ;
11211105
@@ -1126,7 +1110,6 @@ app.post("/verify", async (req, res) => {
11261110 if ( ! verb ) {
11271111 schemaErrors = [ { message : "missing receipt.x402.verb" } ] ;
11281112 } else if ( VERIFY_SCHEMA_CACHED_ONLY && ! hasValidatorCached ( verb ) ) {
1129- // Do NOT compile/fetch here; queue warm and return 202
11301113 warmQueue . add ( verb ) ;
11311114 startWarmWorker ( ) ;
11321115 schemaErrors = [ { message : "validator_not_warmed_yet" } ] ;
@@ -1148,9 +1131,7 @@ app.post("/verify", async (req, res) => {
11481131 } ) ;
11491132 } else {
11501133 try {
1151- const validate = VERIFY_SCHEMA_CACHED_ONLY
1152- ? validatorCache . get ( verb ) ?. validate
1153- : await getValidatorForVerb ( verb ) ;
1134+ const validate = VERIFY_SCHEMA_CACHED_ONLY ? validatorCache . get ( verb ) ?. validate : await getValidatorForVerb ( verb ) ;
11541135
11551136 if ( ! validate ) {
11561137 schemaOk = false ;
0 commit comments