@verax-ai/body 0.3.0 → 0.4.0
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 +7 -2
- package/dist/approve-cli.d.ts +22 -1
- package/dist/approve-cli.js +209 -22
- package/dist/auth.d.ts +9 -1
- package/dist/auth.js +12 -2
- package/dist/body-log.d.ts +14 -0
- package/dist/body-log.js +61 -0
- package/dist/cli.d.ts +23 -1
- package/dist/cli.js +227 -94
- package/dist/config.d.ts +26 -0
- package/dist/config.js +47 -0
- package/dist/demo.d.ts +2 -0
- package/dist/demo.js +8 -2
- package/dist/desktop-parent.d.ts +19 -0
- package/dist/desktop-parent.js +44 -0
- package/dist/desktop.d.ts +140 -11
- package/dist/desktop.js +931 -103
- package/dist/doctor.d.ts +2 -0
- package/dist/doctor.js +94 -11
- package/dist/health-extras.d.ts +4 -0
- package/dist/health-extras.js +14 -0
- package/dist/init-local.d.ts +25 -1
- package/dist/init-local.js +151 -54
- package/dist/install.d.ts +745 -0
- package/dist/install.js +5738 -0
- package/dist/keys.d.ts +6 -0
- package/dist/keys.js +43 -20
- package/dist/main.js +11 -1
- package/dist/no-bypass-scan.js +3 -2
- package/dist/reconcile-cli.js +3 -1
- package/dist/server.d.ts +14 -1
- package/dist/server.js +125 -3
- package/dist/tools/audit.d.ts +5 -2
- package/dist/tools/audit.js +12 -2
- package/dist/tools/memory.js +41 -6
- package/dist/tools/message.js +38 -9
- package/dist/unlock.d.ts +5 -2
- package/dist/unlock.js +6 -1
- package/dist/verify-cli.js +135 -15
- package/dist/wiring.js +1 -1
- package/dist/witness.js +154 -24
- package/package.json +7 -3
package/dist/verify-cli.js
CHANGED
|
@@ -15,16 +15,33 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { readFileSync } from "node:fs";
|
|
17
17
|
import { verifyLedger } from "@verax-ai/proxy";
|
|
18
|
+
import { directoryAccess, unreadableSentence } from "./install.js";
|
|
18
19
|
export const EX_VERIFY_FAILED = 1;
|
|
19
20
|
function usage() {
|
|
20
21
|
return [
|
|
21
|
-
"usage: verax verify <stateDir> [--key <public.pem>] [--json]",
|
|
22
|
+
"usage: verax verify <stateDir> [--key <public.pem>] [--effect-key <public.pem>] [--witness-key <public.pem>] [--checkpoint-key <public.pem>] [--json]",
|
|
22
23
|
"",
|
|
23
24
|
" <stateDir> the directory holding decisions.jsonl and effects.jsonl",
|
|
24
|
-
" --key <file> verify against a public key you hold,
|
|
25
|
-
" the records carry. This is the
|
|
26
|
-
" 'these files agree with each other'
|
|
27
|
-
" were signed by the key I was given'.",
|
|
25
|
+
" --key <file> verify decision records against a public key you hold,",
|
|
26
|
+
" instead of the one the records carry. This is the",
|
|
27
|
+
" difference between 'these files agree with each other'",
|
|
28
|
+
" and 'these files were signed by the key I was given'.",
|
|
29
|
+
" --effect-key <file>",
|
|
30
|
+
" verify self effect rows against a public key you hold,",
|
|
31
|
+
" instead of one key taken from the first self row. Same",
|
|
32
|
+
" distinction as --key, for the effect signer. When this",
|
|
33
|
+
" is set and the ledger has a same-org row, --witness-key",
|
|
34
|
+
" is required too.",
|
|
35
|
+
" --witness-key <file>",
|
|
36
|
+
" verify same-org effect rows against a public key you",
|
|
37
|
+
" hold, instead of one key taken from the first same-org",
|
|
38
|
+
" row. Agreement is not trust, the same as --key. When",
|
|
39
|
+
" this is set and the ledger has a self row, --effect-key",
|
|
40
|
+
" is required too.",
|
|
41
|
+
" --checkpoint-key <file>",
|
|
42
|
+
" verify every checkpoint row against a public key you",
|
|
43
|
+
" hold, instead of one key taken from the checkpoint",
|
|
44
|
+
" file. Same distinction as --key, for the witness.",
|
|
28
45
|
" --json machine-readable result on stdout",
|
|
29
46
|
"",
|
|
30
47
|
"Exit code is 0 when the ledger verifies and 1 when it does not.",
|
|
@@ -44,6 +61,44 @@ export function renderVerify(r) {
|
|
|
44
61
|
? "the key carried in these files"
|
|
45
62
|
: "no key"}`);
|
|
46
63
|
lines.push(` ${r.trust.note}`);
|
|
64
|
+
lines.push(`effects with ${r.effectTrust.source === "pinned"
|
|
65
|
+
? "a key you supplied"
|
|
66
|
+
: r.effectTrust.source === "in-ledger"
|
|
67
|
+
? "the key carried in these files"
|
|
68
|
+
: "no key"}`);
|
|
69
|
+
lines.push(` ${r.effectTrust.note}`);
|
|
70
|
+
const witnessTrust = r.witnessTrust ?? {
|
|
71
|
+
source: "none",
|
|
72
|
+
publicKeyPem: null,
|
|
73
|
+
note: "no same-org effects, so no witness key was used",
|
|
74
|
+
};
|
|
75
|
+
lines.push(`same-org with ${witnessTrust.source === "pinned"
|
|
76
|
+
? "a key you supplied"
|
|
77
|
+
: witnessTrust.source === "in-ledger"
|
|
78
|
+
? "the key carried in these files"
|
|
79
|
+
: "no key"}`);
|
|
80
|
+
lines.push(` ${witnessTrust.note}`);
|
|
81
|
+
lines.push(`checkpoints with ${r.checkpointTrust.source === "pinned"
|
|
82
|
+
? "a key you supplied"
|
|
83
|
+
: r.checkpointTrust.source === "in-ledger"
|
|
84
|
+
? "the key carried in these files"
|
|
85
|
+
: "no key"}`);
|
|
86
|
+
lines.push(` ${r.checkpointTrust.note}`);
|
|
87
|
+
lines.push(r.index.line);
|
|
88
|
+
lines.push(r.effectCompleteness);
|
|
89
|
+
if (r.tail.checkpoint) {
|
|
90
|
+
const head = r.tail.checkpoint.chainHeadHash ?? "(no head hash)";
|
|
91
|
+
const holds = r.tail.checkpoint.ledgerHoldsRecord === null
|
|
92
|
+
? "no head hash to look up"
|
|
93
|
+
: r.tail.checkpoint.ledgerHoldsRecord
|
|
94
|
+
? "ledger holds that record"
|
|
95
|
+
: "ledger does not hold that record";
|
|
96
|
+
const covered = r.tail.checkpoint.receiptCount === null
|
|
97
|
+
? "no record count"
|
|
98
|
+
: `${r.tail.checkpoint.receiptCount} record(s)`;
|
|
99
|
+
lines.push(`checkpoint newest covers ${covered}, head ${head}, ${holds}`);
|
|
100
|
+
}
|
|
101
|
+
lines.push(r.tail.line);
|
|
47
102
|
if (r.problems.length > 0) {
|
|
48
103
|
lines.push("");
|
|
49
104
|
lines.push("problems:");
|
|
@@ -64,28 +119,93 @@ export async function runVerify(argv, out = (s) => process.stdout.write(`${s}\n`
|
|
|
64
119
|
}
|
|
65
120
|
const json = args.includes("--json");
|
|
66
121
|
let publicKeyPem;
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
122
|
+
let effectPublicKeyPem;
|
|
123
|
+
let witnessPublicKeyPem;
|
|
124
|
+
let checkpointPublicKeyPem;
|
|
125
|
+
const readKeyFlag = (flag) => {
|
|
126
|
+
const at = args.indexOf(flag);
|
|
127
|
+
if (at === -1)
|
|
128
|
+
return { ok: true };
|
|
129
|
+
const path = args[at + 1];
|
|
70
130
|
if (!path || path.startsWith("-")) {
|
|
71
|
-
out(
|
|
72
|
-
return
|
|
131
|
+
out(`verify: ${flag} needs a file path`);
|
|
132
|
+
return { ok: false };
|
|
73
133
|
}
|
|
74
134
|
try {
|
|
75
|
-
|
|
135
|
+
const pem = readFileSync(path, "utf8");
|
|
136
|
+
if (pem.trim() === "") {
|
|
137
|
+
out(`verify-key-empty: ${flag}`);
|
|
138
|
+
return { ok: false };
|
|
139
|
+
}
|
|
140
|
+
args.splice(at, 2);
|
|
141
|
+
return { ok: true, pem };
|
|
76
142
|
}
|
|
77
143
|
catch {
|
|
78
144
|
out(`verify: cannot read key file ${path}`);
|
|
79
|
-
return
|
|
145
|
+
return { ok: false };
|
|
80
146
|
}
|
|
81
|
-
|
|
82
|
-
|
|
147
|
+
};
|
|
148
|
+
const recordKey = readKeyFlag("--key");
|
|
149
|
+
if (!recordKey.ok)
|
|
150
|
+
return EX_VERIFY_FAILED;
|
|
151
|
+
publicKeyPem = recordKey.pem;
|
|
152
|
+
const effectKey = readKeyFlag("--effect-key");
|
|
153
|
+
if (!effectKey.ok)
|
|
154
|
+
return EX_VERIFY_FAILED;
|
|
155
|
+
effectPublicKeyPem = effectKey.pem;
|
|
156
|
+
const witnessKey = readKeyFlag("--witness-key");
|
|
157
|
+
if (!witnessKey.ok)
|
|
158
|
+
return EX_VERIFY_FAILED;
|
|
159
|
+
witnessPublicKeyPem = witnessKey.pem;
|
|
160
|
+
const checkpointKey = readKeyFlag("--checkpoint-key");
|
|
161
|
+
if (!checkpointKey.ok)
|
|
162
|
+
return EX_VERIFY_FAILED;
|
|
163
|
+
checkpointPublicKeyPem = checkpointKey.pem;
|
|
83
164
|
const dir = args.find((a) => !a.startsWith("-"));
|
|
84
165
|
if (!dir) {
|
|
85
166
|
out(usage());
|
|
86
167
|
return EX_VERIFY_FAILED;
|
|
87
168
|
}
|
|
88
|
-
|
|
169
|
+
if (directoryAccess(dir) === "unreadable") {
|
|
170
|
+
out(unreadableSentence(dir, "verify"));
|
|
171
|
+
return 77;
|
|
172
|
+
}
|
|
173
|
+
let result;
|
|
174
|
+
try {
|
|
175
|
+
result = await verifyLedger(dir, {
|
|
176
|
+
...(publicKeyPem ? { publicKeyPem } : {}),
|
|
177
|
+
...(effectPublicKeyPem ? { effectPublicKeyPem } : {}),
|
|
178
|
+
...(witnessPublicKeyPem ? { witnessPublicKeyPem } : {}),
|
|
179
|
+
...(checkpointPublicKeyPem ? { checkpointPublicKeyPem } : {}),
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
const problem = err instanceof Error ? err.message : "ledger could not be read";
|
|
184
|
+
const failed = {
|
|
185
|
+
ok: false,
|
|
186
|
+
directory: dir,
|
|
187
|
+
decisions: 0,
|
|
188
|
+
effects: 0,
|
|
189
|
+
signaturesValid: 0,
|
|
190
|
+
signaturesInvalid: 0,
|
|
191
|
+
chainBreakAt: null,
|
|
192
|
+
effectsBound: 0,
|
|
193
|
+
effectsOrphaned: 0,
|
|
194
|
+
trust: { source: "none", publicKeyPem: null, note: problem },
|
|
195
|
+
effectTrust: { source: "none", publicKeyPem: null, note: problem },
|
|
196
|
+
witnessTrust: { source: "none", publicKeyPem: null, note: problem },
|
|
197
|
+
checkpointTrust: { source: "none", publicKeyPem: null, note: problem },
|
|
198
|
+
index: { present: false, missing: 0, line: "index: none (cannot check for removed records)" },
|
|
199
|
+
effectCompleteness: "effect completeness was not checked",
|
|
200
|
+
tail: {
|
|
201
|
+
line: "tail: no checkpoint; removing the newest records with their effects is not detectable from these files",
|
|
202
|
+
checkpoint: null,
|
|
203
|
+
},
|
|
204
|
+
problems: [problem],
|
|
205
|
+
};
|
|
206
|
+
out(json ? JSON.stringify(failed, null, 2) : renderVerify(failed));
|
|
207
|
+
return EX_VERIFY_FAILED;
|
|
208
|
+
}
|
|
89
209
|
out(json ? JSON.stringify(result, null, 2) : renderVerify(result));
|
|
90
210
|
return result.ok ? 0 : EX_VERIFY_FAILED;
|
|
91
211
|
}
|
package/dist/wiring.js
CHANGED
|
@@ -66,7 +66,7 @@ export function createBodyServices(opts) {
|
|
|
66
66
|
: {}),
|
|
67
67
|
};
|
|
68
68
|
};
|
|
69
|
-
registry.set("audit.explain", async (call) => auditExplain(call, ledger, await explainOpts()));
|
|
69
|
+
registry.set("audit.explain", async (call, principal) => auditExplain(call, ledger, await explainOpts(), { principal, stateDir: opts.stateDir }));
|
|
70
70
|
registry.set("message.read", (call, principal) => messageRead(call, opts.stateDir, principal));
|
|
71
71
|
registry.set("message.send", (call, principal, ref) => messageSend(call, opts.stateDir, ref ?? "", principal));
|
|
72
72
|
registry.set("spend", (call, _principal, ref) => spendAuthorize(call, ref ?? ""));
|
package/dist/witness.js
CHANGED
|
@@ -3,9 +3,12 @@ import { appendFileSync, existsSync, mkdirSync, readFileSync, unlinkSync } from
|
|
|
3
3
|
import { createServer } from "node:http";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { writeFileAtomic } from "./atomic-write.js";
|
|
6
|
-
import { buildCheckpointClaims, checkpointHash, signCheckpoint, totalsFromDecisionRecords, } from "@cedulon/checkpoint";
|
|
7
|
-
import { decisionRecordHash } from "@cedulon/core";
|
|
6
|
+
import { buildCheckpointClaims, checkpointHash, signCheckpoint, totalsFromDecisionRecords, verifyCheckpoint, verifyCheckpointUnderPin, } from "@cedulon/checkpoint";
|
|
7
|
+
import { canonical, decisionRecordHash } from "@cedulon/core";
|
|
8
|
+
import { coseFromHex, decodeCoseSign1, verifyCoseSign1 } from "@cedulon/cose";
|
|
9
|
+
import { verifyEffectExtract } from "@cedulon/effect-extract";
|
|
8
10
|
import { checkpointsPath, listPieceFiles, signEffectAttestation } from "@verax-ai/proxy";
|
|
11
|
+
import { assertEd25519PublicKey } from "./keys.js";
|
|
9
12
|
import { pidAlive } from "./unlock.js";
|
|
10
13
|
const WITNESS_CLASS = "same-org";
|
|
11
14
|
function listenPath(stateDir) {
|
|
@@ -24,19 +27,23 @@ export function loadOrCreateWitnessKeys(stateDir) {
|
|
|
24
27
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
25
28
|
const priv = join(dir, "witness.private.pem");
|
|
26
29
|
const pub = join(dir, "witness.public.pem");
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
const pair = existsSync(priv) && existsSync(pub)
|
|
31
|
+
? { privateKeyPem: readFileSync(priv, "utf8"), publicKeyPem: readFileSync(pub, "utf8") }
|
|
32
|
+
: existsSync(priv) || existsSync(pub)
|
|
33
|
+
? null
|
|
34
|
+
: (() => {
|
|
35
|
+
const { publicKey, privateKey } = generateKeyPairSync("ed25519");
|
|
36
|
+
const created = {
|
|
37
|
+
publicKeyPem: publicKey.export({ type: "spki", format: "pem" }).toString(),
|
|
38
|
+
privateKeyPem: privateKey.export({ type: "pkcs8", format: "pem" }).toString(),
|
|
39
|
+
};
|
|
40
|
+
writePemAtomic(priv, created.privateKeyPem);
|
|
41
|
+
writePemAtomic(pub, created.publicKeyPem);
|
|
42
|
+
return created;
|
|
43
|
+
})();
|
|
44
|
+
if (!pair)
|
|
31
45
|
throw new Error("witness-keys-partial");
|
|
32
|
-
|
|
33
|
-
const { publicKey, privateKey } = generateKeyPairSync("ed25519");
|
|
34
|
-
const pair = {
|
|
35
|
-
publicKeyPem: publicKey.export({ type: "spki", format: "pem" }).toString(),
|
|
36
|
-
privateKeyPem: privateKey.export({ type: "pkcs8", format: "pem" }).toString(),
|
|
37
|
-
};
|
|
38
|
-
writePemAtomic(priv, pair.privateKeyPem);
|
|
39
|
-
writePemAtomic(pub, pair.publicKeyPem);
|
|
46
|
+
assertEd25519PublicKey(pair.publicKeyPem, "witness");
|
|
40
47
|
return pair;
|
|
41
48
|
}
|
|
42
49
|
export function readWitnessListen(stateDir) {
|
|
@@ -122,21 +129,61 @@ function loadDecisions(stateDir) {
|
|
|
122
129
|
}
|
|
123
130
|
return out;
|
|
124
131
|
}
|
|
132
|
+
/** Last checkpoint hash this process recorded. A truncated file is compared with it. */
|
|
133
|
+
function recordedCheckpointHash(stateDir) {
|
|
134
|
+
let text;
|
|
135
|
+
try {
|
|
136
|
+
text = readFileSync(statusPath(stateDir), "utf8");
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
let found = null;
|
|
142
|
+
for (const line of text.split("\n")) {
|
|
143
|
+
if (line === "")
|
|
144
|
+
continue;
|
|
145
|
+
try {
|
|
146
|
+
const row = JSON.parse(line);
|
|
147
|
+
if (typeof row.checkpointHash === "string" && row.checkpointHash !== "")
|
|
148
|
+
found = row.checkpointHash;
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
// A torn status line is not a hash.
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return found;
|
|
155
|
+
}
|
|
125
156
|
function lastCheckpointHash(stateDir) {
|
|
157
|
+
const path = checkpointsPath(stateDir);
|
|
158
|
+
let text;
|
|
126
159
|
try {
|
|
127
|
-
|
|
128
|
-
if (text === "")
|
|
129
|
-
return null;
|
|
130
|
-
const lines = text.split("\n").filter((l) => l !== "");
|
|
131
|
-
const last = JSON.parse(lines[lines.length - 1] ?? "{}");
|
|
132
|
-
if (typeof last.coseHex !== "string" || !last.claims)
|
|
133
|
-
return null;
|
|
134
|
-
return checkpointHash(last);
|
|
160
|
+
text = readFileSync(path, "utf8");
|
|
135
161
|
}
|
|
136
162
|
catch (err) {
|
|
137
163
|
if (err.code === "ENOENT")
|
|
138
164
|
return null;
|
|
139
|
-
|
|
165
|
+
throw new Error("checkpoint-unreadable");
|
|
166
|
+
}
|
|
167
|
+
const trimmed = text.trim();
|
|
168
|
+
if (trimmed === "") {
|
|
169
|
+
// A missing file starts a chain. Zero bytes or whitespace are not that file.
|
|
170
|
+
// They also are not the hash recorded when the last checkpoint was written.
|
|
171
|
+
const recorded = recordedCheckpointHash(stateDir);
|
|
172
|
+
if (recorded === null || !trimmed.includes(recorded))
|
|
173
|
+
throw new Error("checkpoint-unreadable");
|
|
174
|
+
}
|
|
175
|
+
const lines = trimmed.split("\n").filter((line) => line !== "");
|
|
176
|
+
try {
|
|
177
|
+
const last = JSON.parse(lines[lines.length - 1] ?? "");
|
|
178
|
+
if (!last || typeof last !== "object" || typeof last.coseHex !== "string" || !last.claims) {
|
|
179
|
+
throw new Error("checkpoint-unreadable");
|
|
180
|
+
}
|
|
181
|
+
return checkpointHash(last);
|
|
182
|
+
}
|
|
183
|
+
catch (err) {
|
|
184
|
+
if (err instanceof Error && err.message === "checkpoint-unreadable")
|
|
185
|
+
throw err;
|
|
186
|
+
throw new Error("checkpoint-unreadable");
|
|
140
187
|
}
|
|
141
188
|
}
|
|
142
189
|
function signWindowCheckpoint(stateDir, window, keys) {
|
|
@@ -146,6 +193,11 @@ function signWindowCheckpoint(stateDir, window, keys) {
|
|
|
146
193
|
const claims = buildCheckpointClaims(window.epoch, inWindow, startMs, endMs, lastCheckpointHash(stateDir), totalsFromDecisionRecords, (row) => decisionRecordHash(row));
|
|
147
194
|
const signed = signCheckpoint(claims, keys.privateKeyPem, keys.publicKeyPem);
|
|
148
195
|
appendFileSync(checkpointsPath(stateDir), `${JSON.stringify(signed)}\n`, { encoding: "utf8" });
|
|
196
|
+
appendStatus(stateDir, {
|
|
197
|
+
atMs: Date.now(),
|
|
198
|
+
result: "checkpoint",
|
|
199
|
+
checkpointHash: checkpointHash(signed),
|
|
200
|
+
});
|
|
149
201
|
return signed;
|
|
150
202
|
}
|
|
151
203
|
export async function requestWitnessCheckpoint(stateDir, window) {
|
|
@@ -167,12 +219,75 @@ export async function requestWitnessCheckpoint(stateDir, window) {
|
|
|
167
219
|
const body = (await res.json());
|
|
168
220
|
if (typeof body.coseHex !== "string" || !body.claims)
|
|
169
221
|
return null;
|
|
222
|
+
if (!checkpointMatchesListenKey(body, listen.publicKeyPem)) {
|
|
223
|
+
appendStatus(stateDir, {
|
|
224
|
+
atMs: Date.now(),
|
|
225
|
+
result: "checkpoint-refused",
|
|
226
|
+
reason: "witness-key-mismatch",
|
|
227
|
+
});
|
|
228
|
+
return null;
|
|
229
|
+
}
|
|
170
230
|
return body;
|
|
171
231
|
}
|
|
172
232
|
catch {
|
|
173
233
|
return null;
|
|
174
234
|
}
|
|
175
235
|
}
|
|
236
|
+
/** The listen file's key, not whichever key answered on the port. */
|
|
237
|
+
function checkpointMatchesListenKey(body, publicKeyPem) {
|
|
238
|
+
if (typeof body.coseHex !== "string" || body.coseHex === "" || !body.claims)
|
|
239
|
+
return false;
|
|
240
|
+
if (typeof body.publicKeyPem !== "string" || !pemSame(body.publicKeyPem, publicKeyPem))
|
|
241
|
+
return false;
|
|
242
|
+
try {
|
|
243
|
+
return verifyCheckpointUnderPin(body, publicKeyPem) && verifyCheckpoint(body, publicKeyPem);
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
function pemSame(left, right) {
|
|
250
|
+
return left.replace(/\r\n/g, "\n").trim() === right.replace(/\r\n/g, "\n").trim();
|
|
251
|
+
}
|
|
252
|
+
/** The listen file's key, not whichever key answered on the port. */
|
|
253
|
+
function answerMatchesListenKey(body, row, resultHash, publicKeyPem) {
|
|
254
|
+
const receipt = body.receipt;
|
|
255
|
+
const coseHex = body.attestation?.coseHex;
|
|
256
|
+
if (!receipt || typeof coseHex !== "string" || coseHex === "")
|
|
257
|
+
return false;
|
|
258
|
+
if (typeof receipt.publicKeyPem !== "string" || !pemSame(receipt.publicKeyPem, publicKeyPem))
|
|
259
|
+
return false;
|
|
260
|
+
const clean = {
|
|
261
|
+
ref: row.ref,
|
|
262
|
+
effectHash: row.effectHash,
|
|
263
|
+
effectClass: row.effectClass,
|
|
264
|
+
timestampMs: row.timestampMs,
|
|
265
|
+
...(row.actor !== undefined ? { actor: row.actor } : {}),
|
|
266
|
+
};
|
|
267
|
+
const expected = canonical({
|
|
268
|
+
ref: clean.ref,
|
|
269
|
+
effectHash: clean.effectHash,
|
|
270
|
+
witnessClass: WITNESS_CLASS,
|
|
271
|
+
resultHash: resultHash ?? null,
|
|
272
|
+
});
|
|
273
|
+
try {
|
|
274
|
+
const decoded = decodeCoseSign1(coseFromHex(coseHex));
|
|
275
|
+
const payload = JSON.parse(Buffer.from(decoded.payload).toString("utf8"));
|
|
276
|
+
if (canonical(payload) !== expected)
|
|
277
|
+
return false;
|
|
278
|
+
if (!verifyCoseSign1(coseFromHex(coseHex), publicKeyPem, "application/json"))
|
|
279
|
+
return false;
|
|
280
|
+
if (!verifyEffectExtract(receipt, publicKeyPem))
|
|
281
|
+
return false;
|
|
282
|
+
const signedRow = receipt.body.effects[0];
|
|
283
|
+
if (signedRow == null || canonical(signedRow) !== canonical(clean))
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
176
291
|
export async function requestWitnessSign(stateDir, row, resultHash) {
|
|
177
292
|
const listen = readWitnessListen(stateDir);
|
|
178
293
|
if (!listen || !pidAlive(listen.pid)) {
|
|
@@ -213,6 +328,15 @@ export async function requestWitnessSign(stateDir, row, resultHash) {
|
|
|
213
328
|
});
|
|
214
329
|
return null;
|
|
215
330
|
}
|
|
331
|
+
if (!answerMatchesListenKey(body, row, resultHash, listen.publicKeyPem)) {
|
|
332
|
+
appendStatus(stateDir, {
|
|
333
|
+
atMs: Date.now(),
|
|
334
|
+
ref: row.ref,
|
|
335
|
+
result: "self-fallback",
|
|
336
|
+
reason: "witness-key-mismatch",
|
|
337
|
+
});
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
216
340
|
appendStatus(stateDir, { atMs: Date.now(), ref: row.ref, result: "signed", pid: listen.pid });
|
|
217
341
|
return {
|
|
218
342
|
witnessClass: WITNESS_CLASS,
|
|
@@ -283,7 +407,13 @@ export async function runWitness(stateDir) {
|
|
|
283
407
|
}
|
|
284
408
|
send(res, 404, { error: "not-found" });
|
|
285
409
|
}
|
|
286
|
-
catch {
|
|
410
|
+
catch (err) {
|
|
411
|
+
const reason = err instanceof Error ? err.message : "";
|
|
412
|
+
if (reason === "checkpoint-unreadable") {
|
|
413
|
+
appendStatus(stateDir, { atMs: Date.now(), result: "checkpoint-refused", reason });
|
|
414
|
+
send(res, 409, { error: reason });
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
287
417
|
send(res, 400, { error: "bad-request" });
|
|
288
418
|
}
|
|
289
419
|
})();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@verax-ai/body",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -21,9 +21,13 @@
|
|
|
21
21
|
"dist"
|
|
22
22
|
],
|
|
23
23
|
"dependencies": {
|
|
24
|
+
"@cedulon/checkpoint": "0.13.1",
|
|
25
|
+
"@cedulon/core": "0.13.1",
|
|
26
|
+
"@cedulon/cose": "0.13.1",
|
|
27
|
+
"@cedulon/effect-extract": "0.13.1",
|
|
24
28
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
25
|
-
"@verax-ai/inventory": "^0.
|
|
26
|
-
"@verax-ai/proxy": "^0.
|
|
29
|
+
"@verax-ai/inventory": "^0.4.0",
|
|
30
|
+
"@verax-ai/proxy": "^0.4.0",
|
|
27
31
|
"jose": "^6.2.11"
|
|
28
32
|
},
|
|
29
33
|
"description": "VERAX body: an MCP server that decides, records and proves what an agent did.",
|