@mnemom/mnemom 0.11.0 → 0.12.1

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.
@@ -0,0 +1,102 @@
1
+ /**
2
+ * `mnemom verify-card` — cards-as-primitive Phase 5 D1.
3
+ *
4
+ * Offline verifier for AAP attestation tokens + transparency-log
5
+ * inclusion proofs. Fetches public-key material from
6
+ * `<api>/v1/.well-known/jwks.json` once, caches under `~/.mnemom/cache/`,
7
+ * then verifies the attestation against the cached JWKS without any
8
+ * subsequent server round-trip beyond the data the verifier asks for.
9
+ *
10
+ * Usage:
11
+ * mnemom verify-card <agent_id> [--at <ISO>] [--card-kind alignment|protection]
12
+ * [--api https://api.mnemom.ai] [--strict]
13
+ * [--jwks-cache <path>] [--no-cache]
14
+ *
15
+ * Behavior:
16
+ * 1. Fetch JWKS from <api>/v1/.well-known/jwks.json (or load cached).
17
+ * 2. If --at present: fetch /v1/transparency/log/{id}?at=<ts>&card_kind=<k>.
18
+ * Else: fetch the agent's current A2A AgentCard at
19
+ * /v1/agents/{id}/a2a-agent-card and extract the embedded attestation.
20
+ * 3. Verify the JWS signature against the JWKS.
21
+ * 4. Fetch the transparency-log row by log_index for the freshly-computed
22
+ * inclusion proof, plus /v1/transparency/root for the signed root.
23
+ * 5. Verify the Merkle inclusion proof against the root.
24
+ * 6. Print a care-framed result.
25
+ *
26
+ * --strict mode bypasses the JWKS cache and exits non-zero on any
27
+ * verification gap (signature, expiry, proof, root signature).
28
+ *
29
+ * care-framing:rule-slug care-framed-output — stderr lines use the
30
+ * would-benefit-from vocabulary; stdout carries machine-readable
31
+ * verification status.
32
+ */
33
+ interface JwkPublic {
34
+ kty: "OKP";
35
+ crv: "Ed25519";
36
+ x: string;
37
+ kid: string;
38
+ alg?: string;
39
+ use?: string;
40
+ mnemom_ext?: {
41
+ status?: "active" | "retired";
42
+ activated_at?: string;
43
+ retired_at?: string;
44
+ rotation_reason?: string;
45
+ };
46
+ }
47
+ interface Jwks {
48
+ keys: JwkPublic[];
49
+ }
50
+ interface AttestationPayload {
51
+ typ: "AAP-Attestation/v1";
52
+ iss: string;
53
+ sub: string;
54
+ iat: number;
55
+ exp: number;
56
+ content_hash: string;
57
+ version: number;
58
+ composed_at: string;
59
+ card_kind: "alignment" | "protection";
60
+ smolt_id?: string;
61
+ historic_backfill?: true;
62
+ }
63
+ interface MerkleProof {
64
+ leaf_hash: string;
65
+ log_index: number;
66
+ tree_size: number;
67
+ hashes: {
68
+ sibling: string;
69
+ position: "left" | "right";
70
+ }[];
71
+ }
72
+ export interface VerifyCardOptions {
73
+ agentId: string;
74
+ at?: string;
75
+ cardKind?: "alignment" | "protection";
76
+ api?: string;
77
+ strict?: boolean;
78
+ jwksCache?: string;
79
+ noCache?: boolean;
80
+ }
81
+ export declare function verifyCardCommand(options: VerifyCardOptions): Promise<number>;
82
+ interface JwsVerification {
83
+ ok: boolean;
84
+ kid?: string;
85
+ payload?: AttestationPayload;
86
+ error?: string;
87
+ }
88
+ declare function verifyJws(jws: string, jwks: Jwks): Promise<JwsVerification>;
89
+ declare function verifyInclusionProof(proof: MerkleProof, expectedRoot: string): Promise<boolean>;
90
+ declare function internalHash(left: string, right: string): Promise<string>;
91
+ declare function base64urlDecode(s: string): Uint8Array;
92
+ declare function base64urlDecodeString(s: string): string;
93
+ declare function hexToBytes(hex: string): Uint8Array;
94
+ export declare const __testing: {
95
+ verifyInclusionProof: typeof verifyInclusionProof;
96
+ internalHash: typeof internalHash;
97
+ hexToBytes: typeof hexToBytes;
98
+ base64urlDecode: typeof base64urlDecode;
99
+ base64urlDecodeString: typeof base64urlDecodeString;
100
+ verifyJws: typeof verifyJws;
101
+ };
102
+ export {};
@@ -0,0 +1,287 @@
1
+ /**
2
+ * `mnemom verify-card` — cards-as-primitive Phase 5 D1.
3
+ *
4
+ * Offline verifier for AAP attestation tokens + transparency-log
5
+ * inclusion proofs. Fetches public-key material from
6
+ * `<api>/v1/.well-known/jwks.json` once, caches under `~/.mnemom/cache/`,
7
+ * then verifies the attestation against the cached JWKS without any
8
+ * subsequent server round-trip beyond the data the verifier asks for.
9
+ *
10
+ * Usage:
11
+ * mnemom verify-card <agent_id> [--at <ISO>] [--card-kind alignment|protection]
12
+ * [--api https://api.mnemom.ai] [--strict]
13
+ * [--jwks-cache <path>] [--no-cache]
14
+ *
15
+ * Behavior:
16
+ * 1. Fetch JWKS from <api>/v1/.well-known/jwks.json (or load cached).
17
+ * 2. If --at present: fetch /v1/transparency/log/{id}?at=<ts>&card_kind=<k>.
18
+ * Else: fetch the agent's current A2A AgentCard at
19
+ * /v1/agents/{id}/a2a-agent-card and extract the embedded attestation.
20
+ * 3. Verify the JWS signature against the JWKS.
21
+ * 4. Fetch the transparency-log row by log_index for the freshly-computed
22
+ * inclusion proof, plus /v1/transparency/root for the signed root.
23
+ * 5. Verify the Merkle inclusion proof against the root.
24
+ * 6. Print a care-framed result.
25
+ *
26
+ * --strict mode bypasses the JWKS cache and exits non-zero on any
27
+ * verification gap (signature, expiry, proof, root signature).
28
+ *
29
+ * care-framing:rule-slug care-framed-output — stderr lines use the
30
+ * would-benefit-from vocabulary; stdout carries machine-readable
31
+ * verification status.
32
+ */
33
+ import * as fs from "node:fs";
34
+ import * as path from "node:path";
35
+ import { MNEMOM_DIR } from "../lib/config.js";
36
+ const JWKS_CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
37
+ const DEFAULT_API = "https://api.mnemom.ai";
38
+ export async function verifyCardCommand(options) {
39
+ const api = (options.api ?? DEFAULT_API).replace(/\/+$/, "");
40
+ const cardKind = options.cardKind ?? "alignment";
41
+ const cachePath = options.jwksCache ?? path.join(MNEMOM_DIR, "cache", "aap-jwks.json");
42
+ let exitCode = 0;
43
+ const findings = [];
44
+ try {
45
+ const jwks = await loadJwks(api, cachePath, !options.strict && !options.noCache);
46
+ // Step 2: fetch the attestation + log row.
47
+ const target = await resolveTargetEntry(api, options.agentId, cardKind, options.at);
48
+ if (!target) {
49
+ process.stderr.write(`No attestation was integrated for ${options.agentId} (kind=${cardKind})${options.at ? ` at-or-before ${options.at}` : ""} — nothing to verify yet.\n`);
50
+ return options.strict ? 1 : 0;
51
+ }
52
+ // Step 3: verify the JWS signature against the JWKS.
53
+ const verification = await verifyJws(target.entry.signed_attestation, jwks);
54
+ if (!verification.ok) {
55
+ findings.push(`signature: ${verification.error}`);
56
+ exitCode = 1;
57
+ }
58
+ else {
59
+ findings.push(`signature: ok (kid=${verification.kid})`);
60
+ }
61
+ const payload = verification.payload;
62
+ // Step 4: fetch signed root.
63
+ const root = await fetchSignedRoot(api);
64
+ if (!root) {
65
+ findings.push("merkle: transparency log is empty (no root to verify against)");
66
+ }
67
+ else {
68
+ // Step 5: verify the Merkle inclusion proof.
69
+ const proofOk = await verifyInclusionProof(target.inclusion_proof, root.root_hash);
70
+ if (!proofOk) {
71
+ findings.push(`merkle: inclusion proof did not reconstruct root (log_index=${target.entry.log_index}, tree_size=${target.inclusion_proof.tree_size})`);
72
+ exitCode = 1;
73
+ }
74
+ else {
75
+ findings.push(`merkle: inclusion proof ok (log_index=${target.entry.log_index}, tree_size=${target.inclusion_proof.tree_size})`);
76
+ }
77
+ }
78
+ // Step 6: print result.
79
+ const result = {
80
+ ok: exitCode === 0,
81
+ agent_id: target.entry.agent_id,
82
+ card_kind: target.entry.card_kind,
83
+ content_hash: target.entry.content_hash,
84
+ version: target.entry.version,
85
+ composed_at: target.entry.composed_at,
86
+ integrated_time: target.entry.integrated_time,
87
+ signing_key_id: target.entry.signing_key_id,
88
+ historic_backfill: payload?.historic_backfill === true,
89
+ findings,
90
+ };
91
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
92
+ if (exitCode === 0) {
93
+ process.stderr.write(`Canonical posture verifies: ${target.entry.agent_id} @ version ${target.entry.version} composed ${target.entry.composed_at}.\n`);
94
+ }
95
+ else {
96
+ process.stderr.write(`Verification gaps surfaced — see findings above.\n`);
97
+ }
98
+ return exitCode;
99
+ }
100
+ catch (err) {
101
+ process.stderr.write(`verify-card couldn't complete — the underlying registry surfaced an error: ${err.message}\n`);
102
+ return 1;
103
+ }
104
+ }
105
+ // ── JWKS fetch + cache ───────────────────────────────────────────────
106
+ async function loadJwks(api, cachePath, useCache) {
107
+ if (useCache && fs.existsSync(cachePath)) {
108
+ try {
109
+ const stat = fs.statSync(cachePath);
110
+ if (Date.now() - stat.mtimeMs < JWKS_CACHE_TTL_MS) {
111
+ const raw = fs.readFileSync(cachePath, "utf8");
112
+ return JSON.parse(raw);
113
+ }
114
+ }
115
+ catch {
116
+ /* fall through to network */
117
+ }
118
+ }
119
+ const res = await fetch(`${api}/v1/.well-known/jwks.json`);
120
+ if (!res.ok) {
121
+ throw new Error(`JWKS fetch failed: ${res.status} ${await res.text()}`);
122
+ }
123
+ const jwks = (await res.json());
124
+ if (useCache) {
125
+ fs.mkdirSync(path.dirname(cachePath), { recursive: true });
126
+ fs.writeFileSync(cachePath, JSON.stringify(jwks, null, 2));
127
+ }
128
+ return jwks;
129
+ }
130
+ async function resolveTargetEntry(api, agentId, cardKind, at) {
131
+ if (at) {
132
+ const url = new URL(`${api}/v1/transparency/log/${encodeURIComponent(agentId)}`);
133
+ url.searchParams.set("at", at);
134
+ url.searchParams.set("card_kind", cardKind);
135
+ const res = await fetch(url.toString());
136
+ if (res.status === 404)
137
+ return null;
138
+ if (!res.ok) {
139
+ throw new Error(`transparency log at lookup failed: ${res.status}`);
140
+ }
141
+ return (await res.json());
142
+ }
143
+ // No --at: fetch the A2A AgentCard for live attestation, then locate
144
+ // the matching log entry by content_hash + version.
145
+ const a2aRes = await fetch(`${api}/v1/agents/${encodeURIComponent(agentId)}/a2a-agent-card`);
146
+ if (a2aRes.status === 404)
147
+ return null;
148
+ if (!a2aRes.ok) {
149
+ throw new Error(`A2A export fetch failed: ${a2aRes.status}`);
150
+ }
151
+ const card = (await a2aRes.json());
152
+ const attestation = card.extensions?.find((e) => e.uri === "https://aap.mnemom.ai/v1/attestation");
153
+ if (!attestation || typeof attestation.body?.token !== "string") {
154
+ throw new Error("A2A AgentCard exists but ships no AAP attestation extension — AAP_ATTESTATION_SIGNING_ENABLED may be off on the server.");
155
+ }
156
+ // Decode payload for content_hash + version to locate the log entry.
157
+ const segs = attestation.body.token.split(".");
158
+ if (segs.length !== 3) {
159
+ throw new Error("Embedded attestation is not a 3-part JWS.");
160
+ }
161
+ const payload = JSON.parse(base64urlDecodeString(segs[1]));
162
+ // Now fetch the log row matching this content_hash + version.
163
+ return resolveLatestLogEntry(api, agentId, cardKind, payload.composed_at);
164
+ }
165
+ async function resolveLatestLogEntry(api, agentId, cardKind, composedAt) {
166
+ const url = new URL(`${api}/v1/transparency/log/${encodeURIComponent(agentId)}`);
167
+ url.searchParams.set("at", composedAt);
168
+ url.searchParams.set("card_kind", cardKind);
169
+ const res = await fetch(url.toString());
170
+ if (res.status === 404)
171
+ return null;
172
+ if (!res.ok) {
173
+ throw new Error(`transparency log latest lookup failed: ${res.status}`);
174
+ }
175
+ return (await res.json());
176
+ }
177
+ async function fetchSignedRoot(api) {
178
+ const res = await fetch(`${api}/v1/transparency/root`);
179
+ if (!res.ok)
180
+ return null;
181
+ const body = (await res.json());
182
+ if (body.tree_size === 0)
183
+ return null;
184
+ return body;
185
+ }
186
+ async function verifyJws(jws, jwks) {
187
+ const parts = jws.split(".");
188
+ if (parts.length !== 3) {
189
+ return { ok: false, error: "JWS does not have three parts" };
190
+ }
191
+ let header;
192
+ let payload;
193
+ try {
194
+ header = JSON.parse(base64urlDecodeString(parts[0]));
195
+ payload = JSON.parse(base64urlDecodeString(parts[1]));
196
+ }
197
+ catch {
198
+ return { ok: false, error: "could not parse JWS header/payload" };
199
+ }
200
+ if (header.alg !== "EdDSA" || header.typ !== "AAP-Attestation/v1") {
201
+ return { ok: false, error: `unexpected header alg=${header.alg} typ=${header.typ}` };
202
+ }
203
+ const kid = header.kid ?? "";
204
+ const key = jwks.keys.find((k) => k.kid === kid);
205
+ if (!key) {
206
+ return { ok: false, error: `kid ${kid} not in JWKS` };
207
+ }
208
+ const publicKey = await crypto.subtle.importKey("jwk", { kty: key.kty, crv: key.crv, x: key.x, ext: true }, { name: "Ed25519" }, true, ["verify"]);
209
+ const signingInput = new TextEncoder().encode(`${parts[0]}.${parts[1]}`);
210
+ const sigBytes = base64urlDecode(parts[2]);
211
+ // BufferSource over a fresh ArrayBuffer keeps newer TS lib defs happy
212
+ // (older Uint8Array<ArrayBufferLike> trips a SharedArrayBuffer mismatch).
213
+ const sigBuf = new ArrayBuffer(sigBytes.length);
214
+ new Uint8Array(sigBuf).set(sigBytes);
215
+ const sigOk = await crypto.subtle.verify("Ed25519", publicKey, sigBuf, signingInput);
216
+ if (!sigOk) {
217
+ return { ok: false, kid, payload, error: "signature did not verify" };
218
+ }
219
+ const nowSecs = Math.floor(Date.now() / 1000);
220
+ if (nowSecs >= payload.exp) {
221
+ return {
222
+ ok: false,
223
+ kid,
224
+ payload,
225
+ error: `token expired at ${new Date(payload.exp * 1000).toISOString()} — the transparency log row still proves the historic posture but the embedded token is past its TTL.`,
226
+ };
227
+ }
228
+ return { ok: true, kid, payload };
229
+ }
230
+ // ── Merkle proof verification ────────────────────────────────────────
231
+ async function verifyInclusionProof(proof, expectedRoot) {
232
+ let current = proof.leaf_hash;
233
+ for (const step of proof.hashes) {
234
+ if (step.position === "right") {
235
+ current = await internalHash(current, step.sibling);
236
+ }
237
+ else {
238
+ current = await internalHash(step.sibling, current);
239
+ }
240
+ }
241
+ return current === expectedRoot;
242
+ }
243
+ async function internalHash(left, right) {
244
+ const leftBytes = hexToBytes(left);
245
+ const rightBytes = hexToBytes(right);
246
+ const buf = new Uint8Array(1 + leftBytes.length + rightBytes.length);
247
+ buf[0] = 0x01;
248
+ buf.set(leftBytes, 1);
249
+ buf.set(rightBytes, 1 + leftBytes.length);
250
+ const digest = await crypto.subtle.digest("SHA-256", buf);
251
+ return Array.from(new Uint8Array(digest))
252
+ .map((b) => b.toString(16).padStart(2, "0"))
253
+ .join("");
254
+ }
255
+ // ── Encoding helpers ─────────────────────────────────────────────────
256
+ function base64urlDecode(s) {
257
+ const padded = s.replace(/-/g, "+").replace(/_/g, "/") + "===".slice(0, (4 - (s.length % 4)) % 4);
258
+ const bin = atob(padded);
259
+ const out = new Uint8Array(bin.length);
260
+ for (let i = 0; i < bin.length; i++)
261
+ out[i] = bin.charCodeAt(i);
262
+ return out;
263
+ }
264
+ function base64urlDecodeString(s) {
265
+ return new TextDecoder().decode(base64urlDecode(s));
266
+ }
267
+ function hexToBytes(hex) {
268
+ if (hex.length % 2 !== 0) {
269
+ throw new Error(`hexToBytes: odd-length hex "${hex}"`);
270
+ }
271
+ const out = new Uint8Array(hex.length / 2);
272
+ for (let i = 0; i < out.length; i++) {
273
+ out[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
274
+ }
275
+ return out;
276
+ }
277
+ // Sentinel for tests that want to exercise individual helpers.
278
+ export const __testing = {
279
+ verifyInclusionProof,
280
+ internalHash,
281
+ hexToBytes,
282
+ base64urlDecode,
283
+ base64urlDecodeString,
284
+ verifyJws,
285
+ };
286
+ // Silence "MNEMOM_DIR unused" if the cache code path is taken in tests.
287
+ void MNEMOM_DIR;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * `mnemom webhooks ...` commands — Track 2 W3.5.
3
+ *
4
+ * mnemom webhooks list <org_id> — list endpoints
5
+ * mnemom webhooks get <org_id> <endpoint_id> — show one
6
+ * mnemom webhooks create <org_id> --url <u> [--events <list>]
7
+ * [--description <d>]
8
+ * mnemom webhooks update <org_id> <endpoint_id> [--url <u>]
9
+ * [--events <list>]
10
+ * [--active <bool>]
11
+ * mnemom webhooks delete <org_id> <endpoint_id> — remove endpoint
12
+ * mnemom webhooks rotate-secret <org_id> <endpoint_id> — mint new secret
13
+ * mnemom webhooks list-deliveries <org_id> [--endpoint <id>] [--limit <n>]
14
+ * mnemom webhooks redeliver <org_id> <delivery_id> — retry one delivery
15
+ * mnemom webhooks replay <org_id> <event_id> [--endpoint <id>...]
16
+ * mnemom webhooks trigger <org_id> <endpoint_id> — fire a test event
17
+ *
18
+ * Every mutation auto-generates a fresh `Idempotency-Key`. Re-running the
19
+ * same command does NOT replay the cache (you'd need to keep the key
20
+ * around for that — the curl/programmatic clients do); for the typical
21
+ * CLI ergonomic path, each invocation is its own logical mutation.
22
+ */
23
+ export declare function webhooksListCommand(orgId: string, opts?: {
24
+ json?: boolean;
25
+ }): Promise<void>;
26
+ export declare function webhooksGetCommand(orgId: string, endpointId: string, opts?: {
27
+ json?: boolean;
28
+ }): Promise<void>;
29
+ export declare function webhooksCreateCommand(orgId: string, opts?: {
30
+ url?: string;
31
+ events?: string;
32
+ description?: string;
33
+ json?: boolean;
34
+ }): Promise<void>;
35
+ export declare function webhooksUpdateCommand(orgId: string, endpointId: string, opts?: {
36
+ url?: string;
37
+ events?: string;
38
+ description?: string;
39
+ active?: string;
40
+ json?: boolean;
41
+ }): Promise<void>;
42
+ export declare function webhooksDeleteCommand(orgId: string, endpointId: string): Promise<void>;
43
+ export declare function webhooksRotateSecretCommand(orgId: string, endpointId: string, opts?: {
44
+ json?: boolean;
45
+ }): Promise<void>;
46
+ export declare function webhooksTriggerCommand(orgId: string, endpointId: string, opts?: {
47
+ json?: boolean;
48
+ }): Promise<void>;
49
+ export declare function webhooksListDeliveriesCommand(orgId: string, opts?: {
50
+ endpoint?: string;
51
+ limit?: string;
52
+ offset?: string;
53
+ json?: boolean;
54
+ }): Promise<void>;
55
+ export declare function webhooksRedeliverCommand(orgId: string, deliveryId: string, opts?: {
56
+ json?: boolean;
57
+ }): Promise<void>;
58
+ export declare function webhooksReplayCommand(orgId: string, eventId: string, opts?: {
59
+ endpoint?: string[];
60
+ json?: boolean;
61
+ }): Promise<void>;