Skip to content

Commit 7598a01

Browse files
committed
h
1 parent a002c38 commit 7598a01

3 files changed

Lines changed: 137 additions & 1 deletion

File tree

worker/src/incidents.mjs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
// Note d'incidente dell'automa della Coerenza.
2+
//
3+
// Perche' esistono: quando una promessa registra una violazione, il numero NON si tocca —
4+
// l'automa serve a misurare anche contro di noi, e la promessa P2 e' proprio "contatori
5+
// onesti". Ma il numero da solo non dice cosa e' successo. Queste note vivono ACCANTO al
6+
// numero: l'attestazione EAS del giorno le mette nel campo `evidenceURI`, quindi la
7+
// spiegazione finisce on-chain insieme al conteggio, per sempre.
8+
//
9+
// Regole, per non trasformarle in scuse: descrivono i fatti e la causa, non riclassificano
10+
// nulla, e non pretendono di annullare la violazione. Vengono scritte dopo, mai prima
11+
// (una manutenzione ANNUNCIATA richiederebbe una promessa nuova, pre-registrata).
12+
13+
export const INCIDENTS = {
14+
"2026-08-22": {
15+
day: "2026-08-22",
16+
promise: "P1",
17+
violations: 1,
18+
observations_that_day: "~144 (one probe every 10 minutes, as the promise declares)",
19+
what_happened:
20+
"The operator published a Vercel CDN routing rule intended to serve the unpaid 402 " +
21+
"challenge from the edge, in order to cut serverless CPU cost. The rewrite to an " +
22+
"external destination did not work: /api/x402/attestation answered HTTP 404 instead " +
23+
"of the 402 challenge. The rule was rolled back about three minutes later.",
24+
detected_by:
25+
"Our own coherence automaton, at 2026-08-22T15:40:15Z. Not by a customer, not by a " +
26+
"third party. The failed observation was recorded and kept.",
27+
impact:
28+
"Any unpaid request in that window received a 404 rather than a payment challenge. " +
29+
"A client of the paid endpoint could not have distinguished this from a real outage, " +
30+
"which is exactly why it counts as a violation.",
31+
what_we_did_not_do:
32+
"We did not edit the counter, and we did not reclassify the observation as an " +
33+
"operator test. Promise P1 is a hash-pinned file with no maintenance exemption; " +
34+
"inventing one after seeing the data would break promise P2 (honest counters) and " +
35+
"would make the whole meter worthless.",
36+
root_cause:
37+
"A single dropped character in the provider dashboard's destination-URL field. The " +
38+
"rewrite target was saved as /x402/atestation instead of /x402/attestation, so the " +
39+
"request reached our edge worker on a path it does not serve and the worker returned " +
40+
"its own 404. The mechanism was never broken; the address had a typo. The field was " +
41+
"later observed to drop a character a second time, which is how it was identified.",
42+
resolution:
43+
"Same day. Root cause found by having the worker echo the path it was receiving, the " +
44+
"address corrected, and the change proven on a throwaway path that is not under any " +
45+
"promise before being applied to the real one. As of 2026-08-22T16:28Z the intended " +
46+
"behaviour is live and verified: an unpaid request to /api/x402/attestation receives " +
47+
"the byte-identical 402 challenge from the edge, and a request carrying a payment " +
48+
"header reaches the normal paid pipeline.",
49+
prevention:
50+
"No routing rule is published on a path under promise before being proven on a path " +
51+
"that is not. Vercel's 'Test Rules' only confirms that a path matches a rule — it " +
52+
"does not prove the action produces the intended response. That distinction caused this. " +
53+
"The destination field is now read back and compared against the intended string before " +
54+
"any rule is saved.",
55+
promise_file: "https://gblin.digital/promises/P1-attestation-uptime.json",
56+
},
57+
};
58+
59+
export function incidentFor(day) {
60+
return INCIDENTS[day] || null;
61+
}
62+
63+
export function incidentResponse(day) {
64+
const inc = incidentFor(day);
65+
if (!inc) return new Response(JSON.stringify({ error: "no incident recorded for that day", day }), {
66+
status: 404, headers: { "content-type": "application/json" },
67+
});
68+
return new Response(JSON.stringify({
69+
about: "Incident note attached to the on-chain coherence attestation for this day. " +
70+
"It explains a violation; it does not cancel it.",
71+
...inc,
72+
}, null, 2), { status: 200, headers: { "content-type": "application/json", "cache-control": "public, max-age=3600", "access-control-allow-origin": "*" } });
73+
}

worker/src/index.js

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ import { catalogTick, catalogReport, catalogFull, observatoryPage, observatoryJs
3030
// su loro invito. Zero costo: 1 lettura + 1 firma per tick; niente chain.
3131
// Secret WITNESS_KEY assente → disattivato in silenzio (fail-safe).
3232
import { witnessTick, witnessIndex, witnessLatestNote, witnessAddCheckpoint, witnessHistory, witnessDiscoverLogs, witnessConfiguredLogs, WITNESSED_LOGS } from "./witness.mjs";
33+
import { x402StaticChallenge } from "./x402-challenge.mjs";
34+
import { incidentFor, incidentResponse } from "./incidents.mjs";
3335
import { sealAction, getReceipt, rlogStatus, demoAllowed, treeRoot, signedCheckpoint, proofFor, verifyReceipt, anchorConsistency, consistencyProof, leaves, pushToWitnesses, witnessState, PROVENANCE_LEVELS, RLOG_ORIGIN } from "./rlog.mjs";
3436

3537
const GBLIN = "0x36C81d7E1966310F305eA637e761Cf77F90852f0";
@@ -513,7 +515,12 @@ async function coherenceAttestClosedDay(env) {
513515
d.obs,
514516
keptBps,
515517
d.obs - d.kept,
516-
p.file,
518+
// Se il giorno ha violazioni e ne abbiamo scritto la nota, l'evidenza on-chain
519+
// punta alla NOTA (che rimanda alla promessa), non solo al file della promessa:
520+
// cosi' il conteggio e la spiegazione viaggiano insieme e per sempre.
521+
d.obs - d.kept > 0 && incidentFor(day)
522+
? `https://gblin-mcp.gblin-mcp-worker.workers.dev/coherence/incident/${day}`
523+
: p.file,
517524
]);
518525

519526
try {
@@ -1378,6 +1385,20 @@ export default {
13781385
witnesses: await witnessState(env),
13791386
}, 200, { "cache-control": "public, max-age=60" });
13801387
}
1388+
// Sfida x402 anonima servita dal bordo (vedi x402-challenge.mjs). Ci arriva riscritta
1389+
// da una Project Routing Rule di Vercel quando la richiesta NON porta pagamento.
1390+
// Note d'incidente della Coerenza: /coherence/incident/<AAAA-MM-GG>
1391+
if (url.pathname.startsWith("/coherence/incident/") && request.method === "GET") {
1392+
return incidentResponse(url.pathname.split("/").pop());
1393+
}
1394+
// Vercel, riscrivendo verso un URL esterno, inoltra il PERCORSO ORIGINALE della
1395+
// richiesta, non quello scritto nella destinazione. Quindi rispondiamo sia sul nostro
1396+
// percorso sia su quello pubblico della webapp. (Scoperto il 22/08 con un 404 del Worker
1397+
// che sembrava un 404 di Vercel: il corpo era il nostro, 46 byte.)
1398+
if ((url.pathname === "/x402/attestation" || url.pathname === "/api/x402/attestation" || url.pathname === "/prova-bordo-x402")
1399+
&& (request.method === "GET" || request.method === "HEAD")) {
1400+
return x402StaticChallenge(request);
1401+
}
13811402
if (url.pathname === "/log/checkpoint" && request.method === "GET") {
13821403
const st = await rlogStatus(env);
13831404
if (!st.checkpoint) return json({ error: "log empty or not armed", origin: st.origin, verifier_key: st.verifier_key }, 404);

worker/src/x402-challenge.mjs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
// Sfida x402 ANONIMA servita dal bordo, per non far partire una funzione Vercel.
2+
//
3+
// Perche': misurato il 22/08/2026, il 91 per cento della CPU fatturata su Vercel (7h10m su
4+
// 7h53m, contro 4h incluse) e' il middleware x402 che risponde 402 a crawler e sonde —
5+
// ~7.400 volte al giorno. Il middleware gira PRIMA della cache CDN, quindi nessuna cache lo
6+
// riduce, e su Vercel il 402 non e' uno stato cacheabile.
7+
//
8+
// Come ci arriva il traffico: una Project Routing Rule di Vercel riscrive verso qui le
9+
// richieste a /api/x402/attestation CHE NON PORTANO un header di pagamento. Chi paga non
10+
// matcha la regola e prosegue sul percorso normale su Vercel, dove la verifica del pagamento
11+
// resta l'unica autorita' che muove denaro.
12+
//
13+
// VINCOLO: questi byte sono indicizzati dal Bazaar di Coinbase e sono la fixture golden in
14+
// GBLIN_WEBAPP/test/x402-golden/. Se cambia la sfida sulla webapp va rigenerata anche qui,
15+
// altrimenti pubblichiamo due termini diversi. `node verify.mjs` in quella cartella se ne accorge.
16+
// Generato da GBLIN_WEBAPP/test/x402-golden/attestation.json.json il 2026-08-22.
17+
18+
const BODY = "{\"x402Version\":2,\"error\":\"Payment required\",\"resource\":{\"url\":\"https://gblin.digital/api/x402/attestation\",\"description\":\"EIP-712-signed risk attestation, verifiable OFFLINE in one step \u2014 no trust in this server required. Perishable (10-min) proof of the BTC/ETH risk regime (calm | elevated | crash) from GBLIN's on-chain Crash Shield on Base. Bought daily by a third-party ERC-8004 agent as a pinned input of its decision rule until 16 Aug 2026 (see /receipts). FREE sample: GET /api/x402/attestation-sample. Free verifier: verify_risk_attestation in @gblin-protocol/mcp-server.\",\"mimeType\":\"application/json\"},\"accepts\":[{\"scheme\":\"exact\",\"network\":\"eip155:8453\",\"amount\":\"3000\",\"asset\":\"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913\",\"payTo\":\"0x0ebA5d314F4f5Dcb7A094953Fa9311a45172dd1B\",\"maxTimeoutSeconds\":300,\"extra\":{\"name\":\"USD Coin\",\"version\":\"2\"}}],\"extensions\":{\"bazaar\":{\"info\":{\"input\":{\"type\":\"http\",\"queryParams\":{},\"method\":\"GET\"},\"output\":{\"type\":\"json\",\"example\":{\"attestation\":{\"regime\":\"calm\",\"regime_code\":0,\"risk_posture\":\"risk_on\",\"severity_pct\":0,\"severity_bps\":0,\"defensive_cash_pct\":10,\"shield_active\":false,\"block_number\":34567890,\"issued_at\":1747600000,\"expires_at\":1747600600,\"ttl_seconds\":600,\"basket_hash\":\"0xabcd\u2026\",\"chain_id\":8453,\"contract\":\"0x36C81d7E1966310F305eA637e761Cf77F90852f0\"},\"attestation_id\":\"0x9f1c\u2026\",\"signature\":\"0x\u2026 (present when attestor key configured, else null)\",\"attestor\":\"0x\u2026 (published GBLIN attestor, else null)\",\"signed\":false,\"verify\":{\"free_mcp_tool\":\"npx @gblin-protocol/mcp-server \u2192 verify_risk_attestation\"}}}},\"schema\":{\"$schema\":\"https://json-schema.org/draft/2020-12/schema\",\"type\":\"object\",\"properties\":{\"input\":{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\",\"const\":\"http\"},\"method\":{\"type\":\"string\",\"enum\":[\"GET\"]},\"queryParams\":{\"type\":\"object\",\"properties\":{},\"required\":[]}},\"required\":[\"type\",\"method\"],\"additionalProperties\":false},\"output\":{\"type\":\"object\",\"properties\":{\"type\":{\"type\":\"string\"},\"example\":{\"type\":\"object\"}},\"required\":[\"type\"]}},\"required\":[\"input\"]}}},\"gblin_info\":{\"docs\":\"https://gblin.digital/api/x402/llms.txt\",\"free_market_risk_regime\":\"https://gblin-mcp.gblin-mcp-worker.workers.dev/regime\",\"x402_uptime_observatory\":\"https://gblin-mcp.gblin-mcp-worker.workers.dev/observatory\",\"note\":\"Reading is free; only the paid resource above requires payment.\"}}";
19+
const PAYMENT_REQUIRED = "eyJ4NDAyVmVyc2lvbiI6MiwiZXJyb3IiOiJQYXltZW50IHJlcXVpcmVkIiwicmVzb3VyY2UiOnsidXJsIjoiaHR0cHM6Ly9nYmxpbi5kaWdpdGFsL2FwaS94NDAyL2F0dGVzdGF0aW9uIiwiZGVzY3JpcHRpb24iOiJFSVAtNzEyLXNpZ25lZCByaXNrIGF0dGVzdGF0aW9uLCB2ZXJpZmlhYmxlIE9GRkxJTkUgaW4gb25lIHN0ZXAg4oCUIG5vIHRydXN0IGluIHRoaXMgc2VydmVyIHJlcXVpcmVkLiBQZXJpc2hhYmxlICgxMC1taW4pIHByb29mIG9mIHRoZSBCVEMvRVRIIHJpc2sgcmVnaW1lIChjYWxtIHwgZWxldmF0ZWQgfCBjcmFzaCkgZnJvbSBHQkxJTidzIG9uLWNoYWluIENyYXNoIFNoaWVsZCBvbiBCYXNlLiBCb3VnaHQgZGFpbHkgYnkgYSB0aGlyZC1wYXJ0eSBFUkMtODAwNCBhZ2VudCBhcyBhIHBpbm5lZCBpbnB1dCBvZiBpdHMgZGVjaXNpb24gcnVsZSB1bnRpbCAxNiBBdWcgMjAyNiAoc2VlIC9yZWNlaXB0cykuIEZSRUUgc2FtcGxlOiBHRVQgL2FwaS94NDAyL2F0dGVzdGF0aW9uLXNhbXBsZS4gRnJlZSB2ZXJpZmllcjogdmVyaWZ5X3Jpc2tfYXR0ZXN0YXRpb24gaW4gQGdibGluLXByb3RvY29sL21jcC1zZXJ2ZXIuIiwibWltZVR5cGUiOiJhcHBsaWNhdGlvbi9qc29uIn0sImFjY2VwdHMiOlt7InNjaGVtZSI6ImV4YWN0IiwibmV0d29yayI6ImVpcDE1NTo4NDUzIiwiYW1vdW50IjoiMzAwMCIsImFzc2V0IjoiMHg4MzM1ODlmQ0Q2ZURiNkUwOGY0YzdDMzJENGY3MWI1NGJkQTAyOTEzIiwicGF5VG8iOiIweDBlYkE1ZDMxNEY0ZjVEY2I3QTA5NDk1M0ZhOTMxMWE0NTE3MmRkMUIiLCJtYXhUaW1lb3V0U2Vjb25kcyI6MzAwLCJleHRyYSI6eyJuYW1lIjoiVVNEIENvaW4iLCJ2ZXJzaW9uIjoiMiJ9fV0sImV4dGVuc2lvbnMiOnsiYmF6YWFyIjp7ImluZm8iOnsiaW5wdXQiOnsidHlwZSI6Imh0dHAiLCJxdWVyeVBhcmFtcyI6e30sIm1ldGhvZCI6IkdFVCJ9LCJvdXRwdXQiOnsidHlwZSI6Impzb24iLCJleGFtcGxlIjp7ImF0dGVzdGF0aW9uIjp7InJlZ2ltZSI6ImNhbG0iLCJyZWdpbWVfY29kZSI6MCwicmlza19wb3N0dXJlIjoicmlza19vbiIsInNldmVyaXR5X3BjdCI6MCwic2V2ZXJpdHlfYnBzIjowLCJkZWZlbnNpdmVfY2FzaF9wY3QiOjEwLCJzaGllbGRfYWN0aXZlIjpmYWxzZSwiYmxvY2tfbnVtYmVyIjozNDU2Nzg5MCwiaXNzdWVkX2F0IjoxNzQ3NjAwMDAwLCJleHBpcmVzX2F0IjoxNzQ3NjAwNjAwLCJ0dGxfc2Vjb25kcyI6NjAwLCJiYXNrZXRfaGFzaCI6IjB4YWJjZOKApiIsImNoYWluX2lkIjo4NDUzLCJjb250cmFjdCI6IjB4MzZDODFkN0UxOTY2MzEwRjMwNWVBNjM3ZTc2MUNmNzdGOTA4NTJmMCJ9LCJhdHRlc3RhdGlvbl9pZCI6IjB4OWYxY+KApiIsInNpZ25hdHVyZSI6IjB44oCmIChwcmVzZW50IHdoZW4gYXR0ZXN0b3Iga2V5IGNvbmZpZ3VyZWQsIGVsc2UgbnVsbCkiLCJhdHRlc3RvciI6IjB44oCmIChwdWJsaXNoZWQgR0JMSU4gYXR0ZXN0b3IsIGVsc2UgbnVsbCkiLCJzaWduZWQiOmZhbHNlLCJ2ZXJpZnkiOnsiZnJlZV9tY3BfdG9vbCI6Im5weCBAZ2JsaW4tcHJvdG9jb2wvbWNwLXNlcnZlciDihpIgdmVyaWZ5X3Jpc2tfYXR0ZXN0YXRpb24ifX19fSwic2NoZW1hIjp7IiRzY2hlbWEiOiJodHRwczovL2pzb24tc2NoZW1hLm9yZy9kcmFmdC8yMDIwLTEyL3NjaGVtYSIsInR5cGUiOiJvYmplY3QiLCJwcm9wZXJ0aWVzIjp7ImlucHV0Ijp7InR5cGUiOiJvYmplY3QiLCJwcm9wZXJ0aWVzIjp7InR5cGUiOnsidHlwZSI6InN0cmluZyIsImNvbnN0IjoiaHR0cCJ9LCJtZXRob2QiOnsidHlwZSI6InN0cmluZyIsImVudW0iOlsiR0VUIl19LCJxdWVyeVBhcmFtcyI6eyJ0eXBlIjoib2JqZWN0IiwicHJvcGVydGllcyI6e30sInJlcXVpcmVkIjpbXX19LCJyZXF1aXJlZCI6WyJ0eXBlIiwibWV0aG9kIl0sImFkZGl0aW9uYWxQcm9wZXJ0aWVzIjpmYWxzZX0sIm91dHB1dCI6eyJ0eXBlIjoib2JqZWN0IiwicHJvcGVydGllcyI6eyJ0eXBlIjp7InR5cGUiOiJzdHJpbmcifSwiZXhhbXBsZSI6eyJ0eXBlIjoib2JqZWN0In19LCJyZXF1aXJlZCI6WyJ0eXBlIl19fSwicmVxdWlyZWQiOlsiaW5wdXQiXX19fX0=";
20+
21+
export function x402StaticChallenge(request) {
22+
// Rete di sicurezza: se per qualunque motivo qui arrivasse una richiesta CON pagamento,
23+
// NON rispondiamo la sfida — sarebbe un pagante respinto. Meglio dirlo chiaramente.
24+
const paid = request.headers.get("x-payment") || request.headers.get("payment-signature");
25+
if (paid) {
26+
return new Response(JSON.stringify({
27+
error: "this edge path serves the unpaid challenge only; a request carrying payment must reach the origin",
28+
}), { status: 421, headers: { "content-type": "application/json", "cache-control": "no-store" } });
29+
}
30+
return new Response(BODY, {
31+
status: 402,
32+
headers: {
33+
"content-type": "application/json",
34+
"payment-required": PAYMENT_REQUIRED,
35+
// La sfida e' deterministica: si puo' cachare al bordo. TTL corto per poterla
36+
// cambiare in fretta se cambiano prezzo o payTo.
37+
"cache-control": "public, max-age=60, s-maxage=300",
38+
"x-gblin-edge-challenge": "1",
39+
"access-control-allow-origin": "*",
40+
},
41+
});
42+
}

0 commit comments

Comments
 (0)