csoai-gspc-mcp 0.2.0 → 0.2.2
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/README.md +25 -17
- package/gspc-tools.json +23 -0
- package/index.mjs +153 -18
- package/package.json +4 -3
- package/paid-tools.json +0 -15
- package/verify-card.mjs +29 -7
package/README.md
CHANGED
|
@@ -23,45 +23,53 @@ are reported as two labelled numbers and never reconciled.
|
|
|
23
23
|
| `get_root` | GET `https://councilof.ai/root.json`. Three states: VALID / UNREACHABLE / UNCHECKABLE. Separate from GSPC. Never a certificate. |
|
|
24
24
|
| `get_card` | GET one card-v0 leaf by sha256. VALID / INVALID (not a leaf) / UNCHECKABLE (fetch failed). A 404 leaf is INVALID, not UNCHECKABLE. |
|
|
25
25
|
| `verify_inclusion` | GET `/api/proof?sha=`. VALID (included) / INVALID (not a leaf) / UNCHECKABLE (proof endpoint unreachable). |
|
|
26
|
+
| `x402_trust` | Latest x402 catalog trust snapshot: counts of correct challenges and phantom resources. A 402 is a challenge, not delivery. |
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
Eight free tools above; four metered ones below. `tools/list` returns all twelve, and
|
|
28
29
|
`wired-tools.test.mjs` fails if a listed tool does not run or a running tool is not listed.
|
|
29
30
|
|
|
30
|
-
The same
|
|
31
|
+
The same eight free tools, from the same definitions file
|
|
31
32
|
(`functions/mcp/gspc-tools.json`), are served over HTTP at
|
|
32
33
|
`https://councilof.ai/mcp` (streamable HTTP, JSON-RPC 2.0 POST). Use whichever
|
|
33
34
|
transport your client speaks; the contracts are identical.
|
|
34
35
|
|
|
35
|
-
### The
|
|
36
|
+
### The four x402-metered tools
|
|
36
37
|
|
|
37
38
|
| tool | route | free path |
|
|
38
39
|
|---|---|---|
|
|
39
40
|
| `commission_card` | `/api/request-attestation` | — (a payment never mints a MEASURED cell) |
|
|
40
41
|
| `art50_marking_evidence` | `/api/art50/marking-evidence` | `preview: true` |
|
|
41
42
|
| `rwa_evidence` | `/api/rwa/evidence` | `preview: true` (unsigned state) |
|
|
42
|
-
| `witness_hash` | `/api/witness` | — |
|
|
43
43
|
| `receipts_batch` | `/api/receipts/batch` | `preview: true` (count, span, roots, batch sha256) |
|
|
44
44
|
|
|
45
45
|
Payment travels as the **`x_payment` argument**, not as a transport header — so stdio carries these
|
|
46
46
|
exactly as the HTTP door does. Up to 0.1.1 this README said the opposite ("stdio has no payment header to
|
|
47
47
|
forward"); that was a statement about the transport, and it was wrong about the mechanism. The server
|
|
48
|
-
forwards your `x_payment` verbatim as the `X-PAYMENT` header on one request to `councilof.ai
|
|
49
|
-
|
|
48
|
+
forwards your `x_payment` verbatim as the `X-PAYMENT` header on one request to `councilof.ai`. It never
|
|
49
|
+
authenticates, signs or invents a receipt; it only classifies the opaque response's receipt shape.
|
|
50
|
+
Settlement is the route's job, fail-closed.
|
|
50
51
|
|
|
51
|
-
|
|
52
|
+
Top-level statuses describe delivery, not settlement:
|
|
52
53
|
|
|
53
54
|
- **`PAYMENT_REQUIRED`** — the route answered 402. The full challenge (`accepts[]`, the `PAYMENT-REQUIRED`
|
|
54
|
-
header) comes back as `structuredContent`.
|
|
55
|
-
|
|
55
|
+
header) comes back as `structuredContent`. With no `x_payment`, nothing was charged by that request. If
|
|
56
|
+
an authorization was presented, settlement remains `UNCONFIRMED`; inspect before signing or retrying.
|
|
57
|
+
A challenge is an answer, not a failure.
|
|
58
|
+
- **`DELIVERED`** — the route answered 2xx and returned a deliverable. Inspect `delivery_kind`:
|
|
59
|
+
`PREVIEW_OR_FREE`, `DELIVERED_SETTLEMENT_UNCONFIRMED`, `DELIVERED_RECEIPT_GAP`, or
|
|
60
|
+
`DELIVERED_WITH_ROUTE_RECEIPT`. `receipt_state: PRESENT_UNVERIFIED` means a JWS-shaped receipt was
|
|
61
|
+
present in the route's opaque response; this wrapper has not verified it.
|
|
56
62
|
- **`NOT_DEPLOYED`** — the route answered 404 on this origin. Said plainly, never a fabricated result.
|
|
57
|
-
- **`UNREACHABLE`** / **`BAD_ARGUMENTS`** — the call could not be made.
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
- **`UNREACHABLE`** / **`BAD_ARGUMENTS`** — the call could not be made. After an authorization is
|
|
64
|
+
presented, a transport failure makes delivery and settlement unknown; never retry blindly.
|
|
65
|
+
|
|
66
|
+
The package does not infer settlement from a challenge, a 2xx, or its own request. It reports delivery
|
|
67
|
+
and the route's settlement evidence separately: a 402 is `PAYMENT_REQUIRED`, a 2xx is `DELIVERED`,
|
|
68
|
+
and a transport failure remains `UNREACHABLE`. A settle echo is `REPORTED_BY_ROUTE`, not independent
|
|
69
|
+
chain verification; a missing or unreadable signed receipt is a named gap and never a silent success.
|
|
70
|
+
`settlement_state` is exactly `NOT_REQUESTED`, `UNCONFIRMED`, or `REPORTED_BY_ROUTE`.
|
|
71
|
+
`receipt_state` on a delivered result is exactly `NOT_REQUESTED`, `ABSENT`, `MISSING`, `UNREADABLE`,
|
|
72
|
+
or `PRESENT_UNVERIFIED`.
|
|
65
73
|
|
|
66
74
|
Every paid deliverable is measurement, not certification; no tool on either transport carries a trust
|
|
67
75
|
label; amounts appear only inside a 402 challenge.
|
package/gspc-tools.json
CHANGED
|
@@ -114,6 +114,29 @@
|
|
|
114
114
|
],
|
|
115
115
|
"additionalProperties": false
|
|
116
116
|
}
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
"name": "x402_trust",
|
|
120
|
+
"description": "GET the latest x402 catalog trust snapshot: counts of how many catalogued x402 resources open a correct 402 challenge vs how many are phantom on the wire. Counts only by doctrine — host details withheld; a 402 is NOT delivery; measurement, never certification.",
|
|
121
|
+
"inputSchema": {
|
|
122
|
+
"type": "object",
|
|
123
|
+
"properties": {},
|
|
124
|
+
"additionalProperties": false
|
|
125
|
+
},
|
|
126
|
+
"outputSchema": {
|
|
127
|
+
"type": "object",
|
|
128
|
+
"properties": {
|
|
129
|
+
"counts": {
|
|
130
|
+
"type": "object"
|
|
131
|
+
},
|
|
132
|
+
"headline": {
|
|
133
|
+
"type": "string"
|
|
134
|
+
},
|
|
135
|
+
"as_of": {
|
|
136
|
+
"type": "string"
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
117
140
|
}
|
|
118
141
|
]
|
|
119
142
|
}
|
package/index.mjs
CHANGED
|
@@ -32,7 +32,9 @@ import { createInterface } from "node:readline";
|
|
|
32
32
|
import { readFileSync, existsSync } from "node:fs";
|
|
33
33
|
import { fileURLToPath } from "node:url";
|
|
34
34
|
|
|
35
|
-
const
|
|
35
|
+
const PACKAGE = JSON.parse(readFileSync(fileURLToPath(new URL("./package.json", import.meta.url)), "utf8"));
|
|
36
|
+
const VERSION = PACKAGE.version;
|
|
37
|
+
if (typeof VERSION !== "string" || !VERSION) throw new Error("package.json has no valid version");
|
|
36
38
|
const ORIGIN = process.env.GSPC_ORIGIN || "https://councilof.ai";
|
|
37
39
|
const FETCH_TIMEOUT_MS = 15000;
|
|
38
40
|
|
|
@@ -64,8 +66,9 @@ const FREE_TOOLS = JSON.parse(readFileSync(TOOLS_PATH, "utf8")).tools;
|
|
|
64
66
|
*
|
|
65
67
|
* Payment travels as the `x_payment` TOOL ARGUMENT, not as a transport header, so stdio carries
|
|
66
68
|
* these exactly as the HTTP door does: the argument is forwarded verbatim as the X-PAYMENT header
|
|
67
|
-
* on one same-origin request
|
|
68
|
-
* Settlement is the route's job, fail-closed.
|
|
69
|
+
* on one same-origin request. This server never authenticates, signs or invents a receipt; it only
|
|
70
|
+
* classifies the opaque response's receipt shape. Settlement is the route's job, fail-closed.
|
|
71
|
+
* Without `x_payment` the tool returns the route's own
|
|
69
72
|
* 402 challenge, which is an answer and not a failure — and `preview` is free where a tool offers it.
|
|
70
73
|
*/
|
|
71
74
|
const PAID_TOOLS_PATH = firstExisting([
|
|
@@ -368,6 +371,29 @@ async function verifyInclusion(args) {
|
|
|
368
371
|
}
|
|
369
372
|
}
|
|
370
373
|
|
|
374
|
+
/**
|
|
375
|
+
* Mirror the HTTP MCP tool's public x402-trust contract. The snapshot is the
|
|
376
|
+
* canonical measured artefact; this package delegates to it instead of
|
|
377
|
+
* copying counts or manufacturing a trust verdict locally.
|
|
378
|
+
*/
|
|
379
|
+
async function x402Trust() {
|
|
380
|
+
const path = "/interop/x402-trust/latest.json";
|
|
381
|
+
try {
|
|
382
|
+
const d = await fetchJson(path);
|
|
383
|
+
return {
|
|
384
|
+
state: "VALID",
|
|
385
|
+
source: `${ORIGIN}${path}`,
|
|
386
|
+
kind: d.kind ?? null,
|
|
387
|
+
as_of: d.as_of ?? null,
|
|
388
|
+
counts: d.counts ?? null,
|
|
389
|
+
headline: d.headline ?? null,
|
|
390
|
+
not_a_certification: true,
|
|
391
|
+
};
|
|
392
|
+
} catch (e) {
|
|
393
|
+
return { ...unreachable(path, e), state: "UNREACHABLE" };
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
371
397
|
/* ---------------------------------------------------------------- paid tools */
|
|
372
398
|
|
|
373
399
|
const PAID_DOCTRINE =
|
|
@@ -413,12 +439,6 @@ function buildPaidRequest(name, args) {
|
|
|
413
439
|
u.searchParams.set("asset", str("asset"));
|
|
414
440
|
if (flag("preview")) u.searchParams.set("preview", "1");
|
|
415
441
|
break;
|
|
416
|
-
case "witness_hash":
|
|
417
|
-
if (!str("sha256") && !str("url")) return { error: "sha256 or url is required" };
|
|
418
|
-
if (str("sha256")) u.searchParams.set("sha256", str("sha256").toLowerCase());
|
|
419
|
-
if (str("url")) u.searchParams.set("url", str("url"));
|
|
420
|
-
if (str("label")) u.searchParams.set("label", str("label"));
|
|
421
|
-
break;
|
|
422
442
|
case "receipts_batch":
|
|
423
443
|
if (!str("from")) return { error: "from is required (ISO-8601)" };
|
|
424
444
|
u.searchParams.set("from", str("from"));
|
|
@@ -431,17 +451,73 @@ function buildPaidRequest(name, args) {
|
|
|
431
451
|
return { url: u.toString(), init: { method, headers, ...(body ? { body } : {}) }, route };
|
|
432
452
|
}
|
|
433
453
|
|
|
454
|
+
function inspectReceipt(paymentResponse) {
|
|
455
|
+
if (!paymentResponse) return "ABSENT";
|
|
456
|
+
try {
|
|
457
|
+
const normalized = paymentResponse.replace(/-/g, "+").replace(/_/g, "/");
|
|
458
|
+
const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "=");
|
|
459
|
+
const decoded = JSON.parse(new TextDecoder().decode(Uint8Array.from(atob(padded), (c) => c.charCodeAt(0))));
|
|
460
|
+
const receipt = decoded?.extensions?.["offer-receipt"]?.info?.receipt;
|
|
461
|
+
return receipt &&
|
|
462
|
+
typeof receipt === "object" &&
|
|
463
|
+
!Array.isArray(receipt) &&
|
|
464
|
+
receipt.format === "jws" &&
|
|
465
|
+
typeof receipt.signature === "string" &&
|
|
466
|
+
/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(receipt.signature)
|
|
467
|
+
? "PRESENT_UNVERIFIED"
|
|
468
|
+
: "MISSING";
|
|
469
|
+
} catch {
|
|
470
|
+
return "UNREADABLE";
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function settlementFields(paymentPresented, paymentResponse) {
|
|
475
|
+
if (paymentResponse) {
|
|
476
|
+
return {
|
|
477
|
+
settlement_state: "REPORTED_BY_ROUTE",
|
|
478
|
+
payment_response_header: paymentResponse,
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
if (paymentPresented) return { settlement_state: "UNCONFIRMED" };
|
|
482
|
+
return { settlement_state: "NOT_REQUESTED", nothing_charged: true };
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function deliveryFields(paymentPresented, paymentResponse) {
|
|
486
|
+
const receiptState = paymentResponse
|
|
487
|
+
? inspectReceipt(paymentResponse)
|
|
488
|
+
: paymentPresented
|
|
489
|
+
? "ABSENT"
|
|
490
|
+
: "NOT_REQUESTED";
|
|
491
|
+
if (!paymentPresented) {
|
|
492
|
+
return { delivery_kind: "PREVIEW_OR_FREE", receipt_state: receiptState };
|
|
493
|
+
}
|
|
494
|
+
if (!paymentResponse) {
|
|
495
|
+
return { delivery_kind: "DELIVERED_SETTLEMENT_UNCONFIRMED", receipt_state: receiptState };
|
|
496
|
+
}
|
|
497
|
+
if (receiptState === "PRESENT_UNVERIFIED") {
|
|
498
|
+
return { delivery_kind: "DELIVERED_WITH_ROUTE_RECEIPT", receipt_state: receiptState };
|
|
499
|
+
}
|
|
500
|
+
return {
|
|
501
|
+
delivery_kind: "DELIVERED_RECEIPT_GAP",
|
|
502
|
+
receipt_state: receiptState,
|
|
503
|
+
receipt_gap:
|
|
504
|
+
"The route reported settlement, but its opaque response did not carry a readable offer-receipt JWS. Inspect the route, chain and facilitator; do not retry blindly.",
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
|
|
434
508
|
/**
|
|
435
509
|
* Forward one paid tool to its route and report what came back, in the route's own words.
|
|
436
510
|
* 402 is PAYMENT_REQUIRED and not an error; 404 is NOT_DEPLOYED and never a fabricated result.
|
|
437
511
|
*/
|
|
438
512
|
async function callPaidTool(name, args) {
|
|
439
513
|
const tool = PAID_BY_NAME.get(name);
|
|
514
|
+
const paymentPresented = typeof args.x_payment === "string" && args.x_payment.trim().length > 0;
|
|
440
515
|
const base = {
|
|
441
516
|
tool: name,
|
|
442
517
|
route: tool.csoai.route,
|
|
443
518
|
sku: tool.csoai.sku,
|
|
444
519
|
rail: tool.csoai.rail,
|
|
520
|
+
payment_presented: paymentPresented,
|
|
445
521
|
doctrine: PAID_DOCTRINE,
|
|
446
522
|
not_a_certification: true,
|
|
447
523
|
};
|
|
@@ -456,11 +532,30 @@ async function callPaidTool(name, args) {
|
|
|
456
532
|
...base,
|
|
457
533
|
status: "UNREACHABLE",
|
|
458
534
|
reason: e instanceof Error ? e.message : String(e),
|
|
459
|
-
|
|
535
|
+
delivery_state: "UNKNOWN",
|
|
536
|
+
...settlementFields(paymentPresented),
|
|
537
|
+
note: paymentPresented
|
|
538
|
+
? "The route could not be fetched. Delivery and settlement are unknown; inspect the wallet, chain and facilitator before signing or retrying."
|
|
539
|
+
: "The route could not be fetched. No payment authorization was presented, and no result is invented.",
|
|
460
540
|
};
|
|
461
541
|
}
|
|
462
542
|
|
|
463
|
-
|
|
543
|
+
let text;
|
|
544
|
+
try {
|
|
545
|
+
text = await res.text();
|
|
546
|
+
} catch {
|
|
547
|
+
return {
|
|
548
|
+
...base,
|
|
549
|
+
status: "UNREADABLE_RESPONSE",
|
|
550
|
+
http_status: res.status,
|
|
551
|
+
reason: "the evidence route response could not be read",
|
|
552
|
+
delivery_state: "UNKNOWN",
|
|
553
|
+
...settlementFields(paymentPresented),
|
|
554
|
+
note: paymentPresented
|
|
555
|
+
? "Delivery and settlement are unknown; inspect the wallet, chain and facilitator before signing or retrying."
|
|
556
|
+
: "No payment authorization was presented, and no result is invented.",
|
|
557
|
+
};
|
|
558
|
+
}
|
|
464
559
|
let body = null;
|
|
465
560
|
try {
|
|
466
561
|
body = text ? JSON.parse(text) : null;
|
|
@@ -475,19 +570,57 @@ async function callPaidTool(name, args) {
|
|
|
475
570
|
http_status: 402,
|
|
476
571
|
payment_required: body,
|
|
477
572
|
payment_required_header: res.headers.get("payment-required"),
|
|
573
|
+
delivery_state: "NOT_DELIVERED",
|
|
574
|
+
...settlementFields(paymentPresented),
|
|
478
575
|
note:
|
|
479
|
-
"A challenge is an answer, not a failure.
|
|
480
|
-
|
|
576
|
+
"A challenge is an answer, not a failure. " +
|
|
577
|
+
(paymentPresented
|
|
578
|
+
? "A payment authorization was presented, but this response does not prove settlement. Inspect the wallet, chain and facilitator before signing or retrying."
|
|
579
|
+
: "Pay from your own wallet against accepts[] and call again with x_payment. No payment authorization was presented; nothing was charged by this request."),
|
|
481
580
|
};
|
|
482
581
|
}
|
|
483
582
|
if (res.status === 404) {
|
|
484
|
-
return {
|
|
583
|
+
return {
|
|
584
|
+
...base,
|
|
585
|
+
status: "NOT_DEPLOYED",
|
|
586
|
+
http_status: 404,
|
|
587
|
+
body,
|
|
588
|
+
delivery_state: "NOT_DELIVERED",
|
|
589
|
+
...settlementFields(paymentPresented),
|
|
590
|
+
note: paymentPresented
|
|
591
|
+
? `${tool.csoai.route} is not on ${ORIGIN}. Settlement is unconfirmed; inspect the wallet, chain and facilitator before signing or retrying.`
|
|
592
|
+
: `${tool.csoai.route} is not on ${ORIGIN}. No payment authorization was presented; nothing was charged by this request.`,
|
|
593
|
+
};
|
|
485
594
|
}
|
|
486
595
|
if (res.ok) {
|
|
487
596
|
const settle = res.headers.get("x-payment-response");
|
|
488
|
-
return {
|
|
597
|
+
return {
|
|
598
|
+
...base,
|
|
599
|
+
status: "DELIVERED",
|
|
600
|
+
http_status: res.status,
|
|
601
|
+
body,
|
|
602
|
+
deliverable: body,
|
|
603
|
+
delivery_state: "DELIVERED",
|
|
604
|
+
...settlementFields(paymentPresented, settle),
|
|
605
|
+
...deliveryFields(paymentPresented, settle),
|
|
606
|
+
payment_response_header: settle,
|
|
607
|
+
...(settle ? { x_payment_response: settle } : {}),
|
|
608
|
+
};
|
|
489
609
|
}
|
|
490
|
-
|
|
610
|
+
const settle = res.headers.get("x-payment-response");
|
|
611
|
+
return {
|
|
612
|
+
...base,
|
|
613
|
+
status: `HTTP_${res.status}`,
|
|
614
|
+
http_status: res.status,
|
|
615
|
+
body,
|
|
616
|
+
delivery_state: "NOT_DELIVERED",
|
|
617
|
+
...settlementFields(paymentPresented, settle),
|
|
618
|
+
note: settle
|
|
619
|
+
? "Settlement was reported by the route; inspect the receipt before retrying."
|
|
620
|
+
: paymentPresented
|
|
621
|
+
? "Settlement is unconfirmed; inspect the wallet, chain and facilitator before signing or retrying."
|
|
622
|
+
: "No payment authorization was presented; nothing was charged by this request.",
|
|
623
|
+
};
|
|
491
624
|
}
|
|
492
625
|
|
|
493
626
|
const HANDLERS = {
|
|
@@ -498,6 +631,7 @@ const HANDLERS = {
|
|
|
498
631
|
get_root: getRoot,
|
|
499
632
|
get_card: getCard,
|
|
500
633
|
verify_inclusion: verifyInclusion,
|
|
634
|
+
x402_trust: x402Trust,
|
|
501
635
|
};
|
|
502
636
|
|
|
503
637
|
/* ----------------------------------------------------------------- transport */
|
|
@@ -539,13 +673,14 @@ function summaryLine(name, payload) {
|
|
|
539
673
|
return `${payload.state ?? "?"} — card-v0 leaf ${String(payload.sha256 || "").slice(0, 16) || "?"}.`;
|
|
540
674
|
case "verify_inclusion":
|
|
541
675
|
return `${payload.state ?? "?"} — inclusion against live merkle.`;
|
|
676
|
+
case "x402_trust":
|
|
677
|
+
return `${payload.state ?? "?"} — ${payload.headline || "catalog trust counts"}.`;
|
|
542
678
|
case "commission_card":
|
|
543
679
|
case "art50_marking_evidence":
|
|
544
680
|
case "rwa_evidence":
|
|
545
|
-
case "witness_hash":
|
|
546
681
|
case "receipts_batch":
|
|
547
682
|
return `${payload.status ?? "?"} — ${payload.route ?? name}${
|
|
548
|
-
payload.status === "PAYMENT_REQUIRED" ? "; nothing charged" : ""
|
|
683
|
+
payload.status === "PAYMENT_REQUIRED" && payload.payment_presented === false ? "; nothing charged" : ""
|
|
549
684
|
}${payload.reason ? " — " + payload.reason : ""}. ${PAID_DOCTRINE}.`;
|
|
550
685
|
default:
|
|
551
686
|
return name;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "csoai-gspc-mcp",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "Stdio MCP server for
|
|
3
|
+
"version": "0.2.2",
|
|
4
|
+
"description": "Stdio MCP server for Council of AI's GSPC board and signed measurement evidence. Twelve tools: eight free readers and four x402-metered evidence tools. Measurement only, never certification. Zero runtime dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"csoai-gspc-mcp": "index.mjs"
|
|
@@ -50,5 +50,6 @@
|
|
|
50
50
|
},
|
|
51
51
|
"engines": {
|
|
52
52
|
"node": ">=20"
|
|
53
|
-
}
|
|
53
|
+
},
|
|
54
|
+
"mcpName": "io.github.CSOAI-ORG/gspc"
|
|
54
55
|
}
|
package/paid-tools.json
CHANGED
|
@@ -49,21 +49,6 @@
|
|
|
49
49
|
},
|
|
50
50
|
"csoai": { "paid": true, "rail": "x402", "route": "/api/rwa/evidence", "sku": "rwa_evidence", "free_preview": "preview=true", "deliverable": "one card-v0 leaf, surface public.notice, kind csoai.eater.xrpl-issuer/0.1", "deployed_by": "live on councilof.ai (verified 2026-09-03)" }
|
|
51
51
|
},
|
|
52
|
-
{
|
|
53
|
-
"name": "witness_hash",
|
|
54
|
-
"description": "PAID (x402 or CSOAI LTD invoice). Witness a SHA-256 digest — 'attest what you're shown' — via https://councilof.ai/api/witness: one public.notice leaf (csoai.witness.hash/0.1) in the next hourly signed public root, an RFC-3161 timestamp over the digest, and the root's Rekor + OpenTimestamps anchors. Hash-only: name a digest, or a public URL fetched once (robots honoured, never past a login, paywall or bot check → UNCHECKABLE, nothing charged). Attests existence of the digest at the root's as_of — nothing about content, legality or provenance; a self-signed card carries no legal presumption. Measurement, not certification. Status is free at /api/witness/status. Without x_payment the tool returns the 402 challenge. If the route is not deployed on this origin the tool says NOT_DEPLOYED.",
|
|
55
|
-
"inputSchema": {
|
|
56
|
-
"type": "object",
|
|
57
|
-
"properties": {
|
|
58
|
-
"sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$", "description": "The digest to witness (64 lowercase hex). Or pass url." },
|
|
59
|
-
"url": { "type": "string", "description": "Optional public https URL, fetched once with our UA, hashed, never stored." },
|
|
60
|
-
"label": { "type": "string", "description": "Optional label (≤120 chars, no verdict words); appears on the leaf." },
|
|
61
|
-
"x_payment": { "type": "string", "description": "The X-PAYMENT header value signed against accepts[] from the previous 402. Omit to receive the challenge." }
|
|
62
|
-
},
|
|
63
|
-
"additionalProperties": false
|
|
64
|
-
},
|
|
65
|
-
"csoai": { "paid": true, "rail": "x402-or-invoice", "route": "/api/witness", "sku": "witness_hash", "free_status": "/api/witness/status?sha256=", "deliverable": "one public.notice leaf in the next hourly root + RFC-3161 reply + Rekor/OTS anchors", "deployed_by": "live on councilof.ai (verified 2026-09-03)" }
|
|
66
|
-
},
|
|
67
52
|
{
|
|
68
53
|
"name": "receipts_batch",
|
|
69
54
|
"description": "PAID (x402 or CSOAI LTD invoice). A historical batch of the estate's measurement receipts via https://councilof.ai/api/receipts/batch: every signed card-v0 leaf whose as_of falls in [from,to] (≤200), each with its Merkle inclusion path and the public root(s) that carried it, plus the root index for the window and one signed manifest card citing the batch sha256. preview=true is free and returns count, span, root count and the sha256 of the exact bytes the paid call returns. Recent leaves are free at /root.json, /cards/ and /api/proof?sha= — the batch sells assembly across history, never a conclusion. No settlement-receipt stream exists (/api/receipts/latest is UNPUBLISHED) and this tool never claims one. Without x_payment the tool returns the 402 challenge.",
|
package/verify-card.mjs
CHANGED
|
@@ -30,8 +30,13 @@ const ORIGIN = "https://councilof.ai";
|
|
|
30
30
|
// that the file is self-consistent — anyone can alter a body and sign it with a key they
|
|
31
31
|
// just generated. Authenticity requires pinning to the key published in our DID document:
|
|
32
32
|
// did:web:csoai.org#card-attestation-1
|
|
33
|
+
const CARD_KEY_ID = "did:web:csoai.org#card-attestation-1";
|
|
33
34
|
const PINNED_PUBKEY_HEX =
|
|
34
35
|
"d4cb0eaa16d5f50bf7633a36aa34fe09a55e124b9316ded2abdb122bb9c37e38";
|
|
36
|
+
const PINNED_DID_KEYS = Object.freeze({
|
|
37
|
+
[CARD_KEY_ID]: PINNED_PUBKEY_HEX,
|
|
38
|
+
"did:web:csoai.org#board-attestation-1": "9367cf59be9cb72bbc9796adf056201ec1c58adfeaa13f83b2c5b754d6c20170",
|
|
39
|
+
});
|
|
35
40
|
|
|
36
41
|
// Fields whose values are floats in our schema. See "THE HONEST LIMIT" above.
|
|
37
42
|
const FLOAT_FIELDS = new Set(["accuracy", "ci_low", "ci_high", "recall", "precision", "f1"]);
|
|
@@ -142,18 +147,35 @@ function jcsString(s) {
|
|
|
142
147
|
export async function verifyCard(card) {
|
|
143
148
|
if (!card || typeof card !== "object" || !card.body)
|
|
144
149
|
return { state: "UNCHECKABLE", reason: "not a card: no body" };
|
|
145
|
-
if (!card.
|
|
146
|
-
return { state: "UNCHECKABLE", reason: "missing
|
|
150
|
+
if (!card.signature || !card.id)
|
|
151
|
+
return { state: "UNCHECKABLE", reason: "missing signature or id" };
|
|
152
|
+
const hasPubkey = typeof card.pubkey === "string" && !!card.pubkey;
|
|
153
|
+
const hasDid = typeof card.did === "string" && !!card.did;
|
|
154
|
+
if (!hasPubkey && !hasDid)
|
|
155
|
+
return { state: "UNCHECKABLE", reason: "card names neither an inline public key nor a DID key reference" };
|
|
156
|
+
if (hasPubkey && hasDid)
|
|
157
|
+
return { state: "UNCHECKABLE", reason: "card names two key authorities; verifier will not guess" };
|
|
147
158
|
|
|
148
|
-
|
|
149
|
-
|
|
159
|
+
let keyHex;
|
|
160
|
+
let keyId;
|
|
161
|
+
if (hasDid) {
|
|
162
|
+
keyHex = PINNED_DID_KEYS[card.did];
|
|
163
|
+
keyId = card.did;
|
|
164
|
+
if (!keyHex)
|
|
165
|
+
return { state: "UNCHECKABLE", reason: `DID key reference ${card.did} is not pinned by this verifier` };
|
|
166
|
+
} else {
|
|
167
|
+
if (card.pubkey !== PINNED_PUBKEY_HEX)
|
|
168
|
+
return { state: "INVALID", reason: "pubkey is not the published card-attestation key" };
|
|
169
|
+
keyHex = card.pubkey;
|
|
170
|
+
keyId = CARD_KEY_ID;
|
|
171
|
+
}
|
|
150
172
|
|
|
151
173
|
let preimage;
|
|
152
174
|
try {
|
|
153
175
|
// Preimage-rule dispatch (roadmap item 1): absent = legacy CPython v1; "jcs-rfc8785" = JCS v2.
|
|
154
176
|
// Never re-sign v1 cards — the verifier dispatches on the field.
|
|
155
177
|
const rule = card.preimage_rule || card.canon || "cpython-v1";
|
|
156
|
-
const fn = rule === "jcs-rfc8785" ? canonicalJcs : canonical;
|
|
178
|
+
const fn = rule === "jcs-rfc8785" || rule === "sha256(canonical body)" ? canonicalJcs : canonical;
|
|
157
179
|
preimage = new TextEncoder().encode(fn(card.body));
|
|
158
180
|
} catch (e) {
|
|
159
181
|
return { state: "UNCHECKABLE", reason: `cannot canonicalise: ${e.message}` };
|
|
@@ -165,13 +187,13 @@ export async function verifyCard(card) {
|
|
|
165
187
|
|
|
166
188
|
let key;
|
|
167
189
|
try {
|
|
168
|
-
key = await crypto.subtle.importKey("raw", unhex(
|
|
190
|
+
key = await crypto.subtle.importKey("raw", unhex(keyHex), "Ed25519", false, ["verify"]);
|
|
169
191
|
} catch {
|
|
170
192
|
return { state: "UNCHECKABLE", reason: "this runtime has no Ed25519 (needs Node 19+)" };
|
|
171
193
|
}
|
|
172
194
|
const ok = await crypto.subtle.verify("Ed25519", key, unhex(card.signature), preimage);
|
|
173
195
|
return ok
|
|
174
|
-
? { state: "VALID", id: card.id, axis: card.body.axis }
|
|
196
|
+
? { state: "VALID", id: card.id, axis: card.body.axis, keyId }
|
|
175
197
|
: { state: "INVALID", reason: "signature does not verify under the pinned key" };
|
|
176
198
|
}
|
|
177
199
|
|