Skip to content

Commit 4df22b1

Browse files
kenneivesclaude
andcommitted
feat(sdk): JS/TS Aggregator SDK — @agentgraph/trust (#118 complete)
The JS half of the aggregator SDK, peer to the Python client. Completes #118. - src/verify.js: standalone verifyEnvelope(env, jwks) — strips proof, JCS via canonicalize@2.1.0, SHA-256 raw digest, Ed25519 verify via node:crypto, freshness. - src/client.js: TrustClient.getAggregate/getContributions/checkRepo/getJwks/ verify/verifyEnvelope (global fetch, Node>=18). - 9 node:test cases incl LIVE-PROD verification: canonicalize@2.1.0 produces a byte-identical JCS preimage to Python rfc8785 + the server signature → valid=true, kid=trust-v2-2026. Cross-language byte-compat proven. - Single runtime dep (canonicalize); Ed25519 + tests use Node built-ins. NOTE: envelope §5 prose says JWS payload is hex(SHA-256), but the working server+verifiers sign the RAW 32-byte digest (live verify confirms). Spec-doc prose should be clarified to match the implementation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 2cd42e4 commit 4df22b1

7 files changed

Lines changed: 688 additions & 0 deletions

File tree

sdk/js/README.md

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# @agentgraph/trust
2+
3+
JavaScript/TypeScript SDK for **AgentGraph Trust Score v2** — signed,
4+
self-verifiable trust-score envelopes. This is the JS peer of the Python
5+
`agentgraph-sdk` verify module; both reproduce the server's
6+
JCS-canonical, Ed25519-over-SHA-256 verification **byte-for-byte**.
7+
8+
- Zero crypto deps: uses Node's built-in `node:crypto` for Ed25519.
9+
- One runtime dep: [`canonicalize`](https://www.npmjs.com/package/canonicalize)
10+
(RFC 8785 JCS — byte-matches Python's `rfc8785`).
11+
- Node >= 18 (uses the global `fetch`).
12+
13+
## Install
14+
15+
```bash
16+
npm install @agentgraph/trust
17+
```
18+
19+
## Trust Score v2 — signed, self-verifiable envelopes
20+
21+
Every v2 trust score is a signed envelope you can verify **without trusting our
22+
server** — fetch it, then check the Ed25519 signature against our published JWKS
23+
yourself.
24+
25+
```js
26+
import { TrustClient } from '@agentgraph/trust';
27+
28+
const client = new TrustClient('https://agentgraph.co');
29+
const did = 'did:web:agentgraph.co:agents:<id>';
30+
31+
// Signed envelope: score + per-source methodology breakdown + proof
32+
const env = await client.getAggregate(did);
33+
console.log(env.trust_score, env.contributions.map((c) => c.source));
34+
35+
// Verify it client-side (fetches JWKS, checks signature + freshness)
36+
const result = await client.verify(did);
37+
if (result.valid) { // true iff signature valid AND fresh
38+
console.log('verified:', result.kid);
39+
} else {
40+
console.log('NOT verified:', result.reason);
41+
}
42+
43+
// Scan any GitHub repo -> grade + findings + a verifiable envelope
44+
const scan = await client.checkRepo('owner', 'repo');
45+
if (scan.trust_envelope) {
46+
console.log(await client.verifyEnvelope(scan.trust_envelope));
47+
}
48+
```
49+
50+
### Standalone verification (no client)
51+
52+
`verifyEnvelope(envelope, jwks, { now })` only needs `canonicalize` plus
53+
`node:crypto`, reproducing the server's JCS-canonical, Ed25519-over-SHA-256
54+
check byte-for-byte.
55+
56+
```js
57+
import { verifyEnvelope } from '@agentgraph/trust';
58+
59+
const result = verifyEnvelope(envelope, jwks);
60+
// => { valid, signatureValid, fresh, kid, reason }
61+
```
62+
63+
It (1) strips the top-level `proof` key, (2) JCS-canonicalizes the rest,
64+
(3) SHA-256s it, (4) reads `kid` from the detached JWS header, (5) finds the
65+
matching `{ kty: 'OKP', crv: 'Ed25519', x }` key in the JWKS, (6) verifies the
66+
Ed25519 signature over the digest, then (7) checks
67+
`computed_at + freshness_ttl_seconds >= now`. `valid` is
68+
`signatureValid && fresh`.
69+
70+
## API
71+
72+
### `new TrustClient(baseUrl, { apiKey?, token?, timeout? })`
73+
74+
| Method | Description |
75+
|--------|-------------|
76+
| `getAggregate(did)` | Signed v2 envelope for a subject DID |
77+
| `getContributions(did)` | Just the methodology breakdown |
78+
| `checkRepo(owner, repo)` | Scan a GitHub repo -> grade + findings + envelope |
79+
| `getJwks()` | Issuer JWKS from `<baseUrl>/.well-known/jwks.json` |
80+
| `verifyEnvelope(env, { now? })` | Fetch JWKS + verify an envelope client-side |
81+
| `verify(did, { now? })` | `getAggregate` + `verifyEnvelope` in one call |
82+
83+
API base is `<baseUrl>/api/v1`; the JWKS is served outside that prefix.
84+
85+
### `verifyEnvelope(envelope, jwks, { now? })`
86+
87+
Returns `{ valid, signatureValid, fresh, kid, reason }`. `reason` is one of:
88+
`ok`, `missing or unsupported proof`, `malformed jws`,
89+
`no matching key in JWKS`, `signature invalid`, `envelope expired (stale)`.
90+
91+
Also exported: `envelopeDigest(envelope)` (raw 32-byte SHA-256 of the
92+
JCS-canonical, proof-stripped envelope), `isFresh(envelope, { now? })`,
93+
`PROOF_TYPE`.
94+
95+
## Verification / tests
96+
97+
```bash
98+
npm install
99+
npm test # node --test
100+
```
101+
102+
The test suite validates against **production**: it fetches the real JWKS and a
103+
real signed aggregate from `agentgraph.co` and asserts the JS verifier accepts
104+
it (`valid === true`, `kid === 'trust-v2-2026'`). A passing live check proves
105+
byte-compatible JCS canonicalization + Ed25519 with the Python/server side. If
106+
prod is unreachable it falls back to a pinned fixture captured from prod.
107+
108+
## Spec
109+
110+
[`docs/standards/trust-score-envelope-v2.0.md`](../../docs/standards/trust-score-envelope-v2.0.md)
111+
(§6 verification). MIT licensed.

sdk/js/index.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// @agentgraph/trust — JS/TS peer of the Python agentgraph-sdk Trust Score v2 surface.
2+
//
3+
// Standalone client-side verification of signed v2 trust-score envelopes
4+
// (JCS-canonical, proof-stripped, Ed25519 over SHA-256), plus a thin async API
5+
// client. Byte-compatible with the Python SDK and the server.
6+
7+
export { verifyEnvelope, envelopeDigest, isFresh, PROOF_TYPE } from './src/verify.js';
8+
export { TrustClient, AgentGraphError } from './src/client.js';

sdk/js/package-lock.json

Lines changed: 28 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sdk/js/package.json

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"name": "@agentgraph/trust",
3+
"version": "0.1.0",
4+
"description": "Client-side verification of AgentGraph Trust Score v2 signed envelopes (JCS-canonical, Ed25519-over-SHA-256). Peer of the Python agentgraph-sdk verify module.",
5+
"type": "module",
6+
"license": "MIT",
7+
"author": "AgentGraph",
8+
"homepage": "https://agentgraph.co",
9+
"main": "index.js",
10+
"exports": {
11+
".": "./index.js",
12+
"./verify": "./src/verify.js",
13+
"./client": "./src/client.js"
14+
},
15+
"files": [
16+
"index.js",
17+
"src/",
18+
"README.md"
19+
],
20+
"engines": {
21+
"node": ">=18"
22+
},
23+
"scripts": {
24+
"test": "node --test"
25+
},
26+
"dependencies": {
27+
"canonicalize": "2.1.0"
28+
},
29+
"keywords": [
30+
"agentgraph",
31+
"trust-score",
32+
"ed25519",
33+
"jcs",
34+
"rfc8785",
35+
"jws",
36+
"did",
37+
"verifiable"
38+
]
39+
}

sdk/js/src/client.js

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
// Async API client for the AgentGraph Trust Score v2 surface.
2+
//
3+
// Peer of the Python agentgraph_sdk.client.AgentGraphClient (Trust Score v2
4+
// methods). Uses the global `fetch` (Node 18+). API base is `<baseUrl>/api/v1`;
5+
// the JWKS lives outside that prefix at `<baseUrl>/.well-known/jwks.json`.
6+
7+
import { verifyEnvelope } from './verify.js';
8+
9+
/** Base error for AgentGraph API failures. */
10+
export class AgentGraphError extends Error {
11+
/**
12+
* @param {string} message
13+
* @param {number|null} [statusCode]
14+
*/
15+
constructor(message, statusCode = null) {
16+
super(message);
17+
this.name = 'AgentGraphError';
18+
this.statusCode = statusCode;
19+
}
20+
}
21+
22+
/**
23+
* Async client for the AgentGraph API (Trust Score v2 surface).
24+
*
25+
* @example
26+
* const client = new TrustClient('https://agentgraph.co');
27+
* const env = await client.getAggregate('did:web:agentgraph.co:agents:<id>');
28+
* const result = await client.verifyEnvelope(env);
29+
* if (result.valid) console.log('verified:', result.kid);
30+
*/
31+
export class TrustClient {
32+
/**
33+
* @param {string} baseUrl
34+
* @param {{apiKey?: string, token?: string, timeout?: number}} [opts]
35+
*/
36+
constructor(baseUrl, { apiKey, token, timeout = 30000 } = {}) {
37+
this.baseUrl = baseUrl.replace(/\/+$/, '');
38+
this._apiPrefix = `${this.baseUrl}/api/v1`;
39+
this._apiKey = apiKey ?? null;
40+
this._token = token ?? null;
41+
this._timeout = timeout;
42+
}
43+
44+
_headers() {
45+
const headers = {};
46+
if (this._apiKey) {
47+
headers['X-API-Key'] = this._apiKey;
48+
} else if (this._token) {
49+
headers['Authorization'] = `Bearer ${this._token}`;
50+
}
51+
return headers;
52+
}
53+
54+
/**
55+
* Low-level request against the API prefix (`<baseUrl>/api/v1`).
56+
* @param {string} method
57+
* @param {string} path
58+
* @param {{json?: object, params?: object}} [opts]
59+
*/
60+
async _request(method, path, { json, params } = {}) {
61+
let url = `${this._apiPrefix}${path}`;
62+
if (params) {
63+
const qs = new URLSearchParams();
64+
for (const [k, v] of Object.entries(params)) {
65+
if (v !== null && v !== undefined) qs.append(k, String(v));
66+
}
67+
const s = qs.toString();
68+
if (s) url += `?${s}`;
69+
}
70+
return this._fetch(method, url, json);
71+
}
72+
73+
async _fetch(method, url, json) {
74+
const headers = this._headers();
75+
const init = { method, headers };
76+
if (json !== undefined) {
77+
headers['Content-Type'] = 'application/json';
78+
init.body = JSON.stringify(json);
79+
}
80+
81+
let controller;
82+
let timer;
83+
if (this._timeout && typeof AbortController !== 'undefined') {
84+
controller = new AbortController();
85+
init.signal = controller.signal;
86+
timer = setTimeout(() => controller.abort(), this._timeout);
87+
}
88+
89+
let resp;
90+
try {
91+
resp = await fetch(url, init);
92+
} catch (err) {
93+
throw new AgentGraphError(`request failed: ${err.message}`, null);
94+
} finally {
95+
if (timer) clearTimeout(timer);
96+
}
97+
return this._parseResponse(resp);
98+
}
99+
100+
async _parseResponse(resp) {
101+
if (resp.status === 204) return null;
102+
const text = await resp.text();
103+
if (resp.status >= 400) {
104+
let detail = text;
105+
try {
106+
detail = JSON.parse(text).detail ?? text;
107+
} catch {
108+
// keep raw text
109+
}
110+
throw new AgentGraphError(String(detail) || `HTTP ${resp.status}`, resp.status);
111+
}
112+
if (!text) return null;
113+
return JSON.parse(text);
114+
}
115+
116+
// ------------------------------------------------------------------
117+
// Trust Score v2 — signed aggregate envelopes
118+
// ------------------------------------------------------------------
119+
120+
/**
121+
* Fetch the signed v2 trust-score envelope for a subject DID.
122+
* The envelope carries the score, a per-source methodology breakdown, and an
123+
* Ed25519 proof. Use `verify()` to check it client-side.
124+
* @param {string} subjectDid
125+
*/
126+
async getAggregate(subjectDid) {
127+
return this._request('GET', `/aggregate/${subjectDid}`);
128+
}
129+
130+
/**
131+
* Fetch just the methodology breakdown (contributions) for a subject.
132+
* @param {string} subjectDid
133+
*/
134+
async getContributions(subjectDid) {
135+
return this._request('GET', `/aggregate/${subjectDid}/contributions`);
136+
}
137+
138+
/**
139+
* Scan a GitHub repo and return its grade + findings + signed v2 envelope.
140+
* The `trust_envelope` field (when present) is verifiable via verifyEnvelope().
141+
* @param {string} owner
142+
* @param {string} repo
143+
*/
144+
async checkRepo(owner, repo) {
145+
return this._request('GET', `/public/scan/${owner}/${repo}`);
146+
}
147+
148+
/**
149+
* Fetch the issuer JWKS (RFC 7517) used to verify signed envelopes.
150+
* Served at `<baseUrl>/.well-known/jwks.json` (outside the API prefix).
151+
*/
152+
async getJwks() {
153+
return this._fetch('GET', `${this.baseUrl}/.well-known/jwks.json`);
154+
}
155+
156+
/**
157+
* Verify a signed envelope client-side against the issuer's JWKS.
158+
* Fetches the JWKS, then checks the Ed25519 signature + freshness WITHOUT
159+
* trusting this server's verdict — the whole point of the signed envelope.
160+
* @param {object} envelope
161+
* @param {{now?: Date}} [opts]
162+
* @returns {Promise<import('./verify.js').VerificationResult>}
163+
*/
164+
async verifyEnvelope(envelope, { now } = {}) {
165+
const jwks = await this.getJwks();
166+
return verifyEnvelope(envelope, jwks, { now });
167+
}
168+
169+
/**
170+
* Fetch a subject's envelope and verify it client-side in one call.
171+
* @param {string} subjectDid
172+
* @param {{now?: Date}} [opts]
173+
* @returns {Promise<import('./verify.js').VerificationResult>}
174+
*/
175+
async verify(subjectDid, { now } = {}) {
176+
const envelope = await this.getAggregate(subjectDid);
177+
return this.verifyEnvelope(envelope, { now });
178+
}
179+
}
180+
181+
export default TrustClient;

0 commit comments

Comments
 (0)