@agenticstechnologies/verify 0.0.0-stage → 1.0.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.
- package/LICENSE +17 -0
- package/README.md +152 -2
- package/bin/verify.mjs +133 -0
- package/package.json +32 -4
- package/src/bundle.mjs +364 -0
- package/src/cleared.d.ts +11 -0
- package/src/cleared.mjs +120 -0
- package/src/evidence.mjs +112 -0
- package/src/index.d.ts +32 -0
- package/src/index.mjs +8 -0
- package/src/merkle.mjs +150 -0
- package/src/record.mjs +440 -0
- package/src/solana.mjs +235 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
https://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
Copyright 2026 Agentics, Inc.
|
|
6
|
+
|
|
7
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
you may not use this file except in compliance with the License.
|
|
9
|
+
You may obtain a copy of the License at
|
|
10
|
+
|
|
11
|
+
https://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
|
|
13
|
+
Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
See the License for the specific language governing permissions and
|
|
17
|
+
limitations under the License.
|
package/README.md
CHANGED
|
@@ -1,3 +1,153 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @agenticstechnologies/verify
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Offline checks for an Agentics evidence export. The tool recomputes receipt
|
|
4
|
+
leaves and the `prev_hash` chain from the JSON file. It does not call
|
|
5
|
+
Agentics, and it does not trust the `verification` summary inside the file.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g @agenticstechnologies/verify
|
|
11
|
+
agentics-verify bundle.json
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The package binary is `agentics-verify`. Pass `-` to read JSON from stdin, and `--help` to print usage.
|
|
15
|
+
|
|
16
|
+
## Version bump policy
|
|
17
|
+
|
|
18
|
+
Versions follow semver. A fix increments the patch number, a backwards-compatible addition increments the minor number, and a breaking change increments the major number. Update `version` in this package's `package.json` in the same commit as the change. Publishing uses the founder's npm token and does not run on merge.
|
|
19
|
+
|
|
20
|
+
`scripts/verify-evidence.mjs` is a thin wrapper around the same leaf and chain
|
|
21
|
+
code. From a checkout of this repo:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
node scripts/verify-evidence.mjs bundle.json
|
|
25
|
+
node packages/agentics-verify/bin/verify.mjs bundle.json
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The package binary is `agentics-verify` (`./bin/verify.mjs`). Pass `-` to read
|
|
29
|
+
JSON from stdin.
|
|
30
|
+
|
|
31
|
+
## Exit codes
|
|
32
|
+
|
|
33
|
+
| Code | Meaning |
|
|
34
|
+
| --- | --- |
|
|
35
|
+
| 0 | Leaf and chain match. Merkle, when present, folds. A devnet or testnet memo, when present, matches or the public RPC was skipped. |
|
|
36
|
+
| 1 | A check failed, or the file is not JSON. The reason is on stderr. |
|
|
37
|
+
| 2 | No bundle path was given. |
|
|
38
|
+
|
|
39
|
+
A passing package run prints three lines:
|
|
40
|
+
|
|
41
|
+
```text
|
|
42
|
+
pass checked 2 receipt(s)
|
|
43
|
+
merkle not-present batch/merkle fields are not in this export
|
|
44
|
+
solana not-present tx/cluster are not in this export
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`not-present` means that block was skipped. It is not a failure. `warn` (RPC
|
|
48
|
+
timeout or a cluster this tool does not query) is also not a failure.
|
|
49
|
+
|
|
50
|
+
## What is checked
|
|
51
|
+
|
|
52
|
+
**Leaf and chain.** Same algorithm as `lib/gateway/proof.ts` `leafHash` and
|
|
53
|
+
the date-range export from `GET /api/proof/export`:
|
|
54
|
+
|
|
55
|
+
```text
|
|
56
|
+
sha256Hex(JSON.stringify(canonicalize(canonical minus {receipt_id, batch_id})))
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`canonicalize` sorts object keys and drops null and undefined. Each receipt
|
|
60
|
+
after the first must set `prev_hash` to the previous `receipt_hash`. The first
|
|
61
|
+
`prev_hash` must be 64 hex characters. `verification.truncated: true` fails
|
|
62
|
+
the file, including a single-receipt proof bundle.
|
|
63
|
+
|
|
64
|
+
If a receipt sets `text_cleared_at`, the stored hash covers the cleared fields.
|
|
65
|
+
Older leaves still check every other canonical field by trying the historical
|
|
66
|
+
outcome labels in memory. The label is not printed. An older outcome that is
|
|
67
|
+
not in that list, and every version 4 receipt, must carry `uncleared_digest`.
|
|
68
|
+
Version 4 checks the leaf with `reason`, `target`, `outcome`, and
|
|
69
|
+
`error_message` removed against that digest. An older unlisted outcome checks
|
|
70
|
+
the leaf with `outcome` removed. Without the digest those leaves fail. The
|
|
71
|
+
chain link is unchanged. A changed amount, agent, or `prev_hash` still fails.
|
|
72
|
+
|
|
73
|
+
A proof bundle from `GET /api/proof/bundle` has one `receipt` plus
|
|
74
|
+
`receipt_hash` instead of `receipts`. The leaf is recomputed the same way.
|
|
75
|
+
`chain` is checked as hash links (oldest-first or the newest-first order
|
|
76
|
+
`chainFor` returns). Ancestor bodies are not in that bundle, so only the
|
|
77
|
+
included receipt is re-hashed.
|
|
78
|
+
|
|
79
|
+
**Merkle.** When the export includes a proof, the fold is the sorted-pair
|
|
80
|
+
Merkle in `netlify/edge-functions/lib/ledger.js` (`hashPair` hashes the two
|
|
81
|
+
digests in sorted order, an odd node is duplicated, an empty list is null).
|
|
82
|
+
That is the same fold `lib/gateway/proof.ts` uses for a batch. It is tree rule
|
|
83
|
+
`v1`, and every batch anchored before v2 stays v1.
|
|
84
|
+
|
|
85
|
+
**Tree rule v2 (1.0.0).** A claim with `tree_rule: "v2"` uses RFC 9162
|
|
86
|
+
section 2.1 (RFC 6962) over the 32 bytes of each leaf:
|
|
87
|
+
|
|
88
|
+
- leaf = SHA-256(0x00 || leaf)
|
|
89
|
+
- node = SHA-256(0x01 || left || right), in position order
|
|
90
|
+
- an odd last node is promoted unchanged
|
|
91
|
+
- a one-leaf root is its leaf hash
|
|
92
|
+
|
|
93
|
+
A v2 proof is the RFC audit path, so it also needs `index` (or `leaf_index`)
|
|
94
|
+
and `leaf_count` (or `tree_size`). Without them the claim fails rather than
|
|
95
|
+
guessing. With no `tree_rule`, the claim is v1. Any other value is refused,
|
|
96
|
+
never folded as v1. `merkleRootV2`, `merkleProofV2`, `verifyProofV2`,
|
|
97
|
+
`leafHashV2` and `treeRuleOf` are exported.
|
|
98
|
+
|
|
99
|
+
Recognized fields, and no others:
|
|
100
|
+
|
|
101
|
+
- `merkle.siblings` or `merkle.proof`, with `merkle.root` or `merkle.merkle_root`
|
|
102
|
+
- `merkle.leaves` with a root (full leaf list, receipt-hash order)
|
|
103
|
+
- the same fields on a receipt row's `merkle` object
|
|
104
|
+
- `batches[].merkle_root` with `batches[].leaves` or `batches[].siblings`
|
|
105
|
+
- `tree_rule` and `leaf_count` on any of the above (v2 only)
|
|
106
|
+
|
|
107
|
+
The proof bundle published by `bundle()` uses `merkle.siblings` and
|
|
108
|
+
`merkle.root`. If none of these fields are present, the line is
|
|
109
|
+
`merkle not-present`. A root with neither siblings nor a leaf list fails:
|
|
110
|
+
the export claimed a root and this tool cannot recompute it.
|
|
111
|
+
|
|
112
|
+
**Solana memo.** Only when the bundle has both a transaction id and a cluster
|
|
113
|
+
(`anchor.txid` and `anchor.chain` or `anchor.cluster`, or top-level `tx` and
|
|
114
|
+
`cluster`). Devnet (`solana-devnet`, `devnet`, Solscan `?cluster=devnet`) uses
|
|
115
|
+
`https://api.devnet.solana.com`. Testnet uses `https://api.testnet.solana.com`.
|
|
116
|
+
The public `getTransaction` response is read for a Memo program instruction
|
|
117
|
+
(`MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr`) and compared to the recomputed
|
|
118
|
+
Merkle root, or to `anchor.memo` when the export has no root. No private key
|
|
119
|
+
is read and nothing is signed. Mainnet is not queried. If `anchor.memo` is
|
|
120
|
+
present and a Merkle root was recomputed, the two must match before any
|
|
121
|
+
network read; a missing transaction id still skips the public RPC. If the RPC
|
|
122
|
+
does not answer within 8 seconds, the line is `solana warn` and the process
|
|
123
|
+
still exits 0 when every other check passed.
|
|
124
|
+
|
|
125
|
+
## Agent record
|
|
126
|
+
|
|
127
|
+
`GET /api/record/:handle/export` is a signed record (`typ` `agentics.record.v1`).
|
|
128
|
+
Check it with the public keys from `https://agentics.you/.well-known/jwks.json`.
|
|
129
|
+
The tool does not call Agentics.
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
node packages/agentics-verify/bin/verify.mjs record.json --jwks jwks.json
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Exit 0 prints the handle, the label, and the counts. Exit 1 is `fail`, the
|
|
136
|
+
reason code, and a sentence. A changed count, a changed score, or a flipped
|
|
137
|
+
signature byte fails. A record does not expire by clock. A previous signing
|
|
138
|
+
key still verifies old exports while it remains in the published key set.
|
|
139
|
+
`New, no record yet` is a real label: the file had no settlement and no claim.
|
|
140
|
+
|
|
141
|
+
## Tests
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
node --test packages/agentics-verify/test/verify.test.mjs
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
The fixture `test/fixtures/evidence-export.json` is an unmodified
|
|
148
|
+
`agentics.evidence.v1` export. Tampering with `receipt_hash` or a leaf field
|
|
149
|
+
exits 1.
|
|
150
|
+
|
|
151
|
+
## License
|
|
152
|
+
|
|
153
|
+
Apache-2.0 — © Agentics Technologies LLC.
|
package/bin/verify.mjs
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Offline check of an Agentics evidence export.
|
|
3
|
+
//
|
|
4
|
+
// agentics-verify bundle.json
|
|
5
|
+
// agentics-verify - # JSON on stdin
|
|
6
|
+
//
|
|
7
|
+
// Exit 0 when the leaf and prev_hash checks pass, any included Merkle proof
|
|
8
|
+
// folds, and a devnet/testnet memo (if present) matches or the RPC is skipped.
|
|
9
|
+
// Exit 1 when a check fails. Exit 2 when no path is given.
|
|
10
|
+
|
|
11
|
+
import fs from "node:fs";
|
|
12
|
+
import { verifyBundle } from "../src/bundle.mjs";
|
|
13
|
+
import { isRecordBundle, verifyRecord } from "../src/record.mjs";
|
|
14
|
+
|
|
15
|
+
function statusLine(label, part) {
|
|
16
|
+
return `${label} ${part.status}${part.reason ? " " + part.reason : ""}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const USAGE = "usage: agentics-verify <bundle.json | -> [--jwks jwks.json]";
|
|
20
|
+
|
|
21
|
+
function packageVersion() {
|
|
22
|
+
return JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function printHelp() {
|
|
26
|
+
console.log([
|
|
27
|
+
"agentics-verify — check an Agentics evidence export offline",
|
|
28
|
+
"",
|
|
29
|
+
USAGE,
|
|
30
|
+
"",
|
|
31
|
+
" agentics-verify bundle.json",
|
|
32
|
+
" agentics-verify - --jwks jwks.json",
|
|
33
|
+
"",
|
|
34
|
+
"Exit 0 when the checks pass. Exit 1 when a check fails. Exit 2 when no file is given.",
|
|
35
|
+
].join("\n"));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const args = process.argv.slice(2);
|
|
39
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
40
|
+
printHelp();
|
|
41
|
+
process.exit(0);
|
|
42
|
+
}
|
|
43
|
+
if (args.includes("--version") || args.includes("-v")) {
|
|
44
|
+
console.log("agentics-verify " + packageVersion());
|
|
45
|
+
process.exit(0);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let jwksPath = null;
|
|
49
|
+
const positional = [];
|
|
50
|
+
for (let i = 0; i < args.length; i++) {
|
|
51
|
+
if (args[i] === "--jwks") {
|
|
52
|
+
jwksPath = args[i + 1] ?? "";
|
|
53
|
+
i += 1;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
positional.push(args[i]);
|
|
57
|
+
}
|
|
58
|
+
const arg = positional[0];
|
|
59
|
+
if (!arg) {
|
|
60
|
+
console.error(USAGE);
|
|
61
|
+
process.exit(2);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let text;
|
|
65
|
+
try {
|
|
66
|
+
text = arg === "-" ? fs.readFileSync(0, "utf8") : fs.readFileSync(arg, "utf8");
|
|
67
|
+
} catch (e) {
|
|
68
|
+
console.error("cannot read bundle:", e instanceof Error ? e.message : String(e));
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let bundle;
|
|
73
|
+
try {
|
|
74
|
+
bundle = JSON.parse(text);
|
|
75
|
+
} catch (e) {
|
|
76
|
+
console.error("invalid JSON:", e instanceof Error ? e.message : String(e));
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (isRecordBundle(bundle)) {
|
|
81
|
+
if (!jwksPath) {
|
|
82
|
+
console.error("fail jwks required pass --jwks with the keys from /.well-known/jwks.json");
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
let jwksText;
|
|
86
|
+
try {
|
|
87
|
+
jwksText = fs.readFileSync(jwksPath, "utf8");
|
|
88
|
+
} catch (e) {
|
|
89
|
+
console.error("cannot read jwks:", e instanceof Error ? e.message : String(e));
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
let jwks;
|
|
93
|
+
try {
|
|
94
|
+
jwks = JSON.parse(jwksText);
|
|
95
|
+
} catch (e) {
|
|
96
|
+
console.error("invalid jwks JSON:", e instanceof Error ? e.message : String(e));
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
const checked = await verifyRecord(bundle, jwks);
|
|
100
|
+
if (!checked.ok) {
|
|
101
|
+
console.error(`fail ${checked.reason}${checked.message ? " " + checked.message : ""}`);
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
const record = checked.record ?? {};
|
|
105
|
+
console.log(`pass record ${checked.handle}`);
|
|
106
|
+
console.log(`label ${record.label}`);
|
|
107
|
+
console.log(`paid on time ${record.paid_on_time}`);
|
|
108
|
+
console.log(`claims filed ${record.claims_filed}`);
|
|
109
|
+
console.log(`claims paid ${record.claims_paid}`);
|
|
110
|
+
console.log(`sellers ${record.distinct_sellers}`);
|
|
111
|
+
console.log(`age ${record.age_days == null ? "none" : record.age_days + " days"}`);
|
|
112
|
+
process.exit(0);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const result = await verifyBundle(bundle);
|
|
116
|
+
const merkleLine = statusLine("merkle", result.merkle);
|
|
117
|
+
const solanaLine = statusLine("solana", result.solana);
|
|
118
|
+
|
|
119
|
+
if (result.ok) {
|
|
120
|
+
console.log(`pass checked ${result.checked} receipt(s)`);
|
|
121
|
+
console.log(merkleLine);
|
|
122
|
+
if (result.solana.status === "warn") console.error(solanaLine);
|
|
123
|
+
else console.log(solanaLine);
|
|
124
|
+
process.exit(0);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
console.error(`fail checked ${result.checked}`);
|
|
128
|
+
for (const f of result.failures) {
|
|
129
|
+
console.error(`- ${f.reason} hash=${f.receipt_hash} prev=${f.prev_hash}`);
|
|
130
|
+
}
|
|
131
|
+
console.error(merkleLine);
|
|
132
|
+
console.error(solanaLine);
|
|
133
|
+
process.exit(1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,34 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agenticstechnologies/verify",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Offline verifier for Agentics evidence export bundles. Recomputes receipt leaves and the prev_hash chain, and folds a Merkle proof when the export includes one.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"author": "Agentics Technologies LLC",
|
|
7
|
+
"homepage": "https://agentics.you/docs/",
|
|
8
|
+
"bugs": {
|
|
9
|
+
"url": "https://agentics.you/contact/",
|
|
10
|
+
"email": "support@agentics.you"
|
|
11
|
+
},
|
|
12
|
+
"type": "module",
|
|
13
|
+
"bin": {
|
|
14
|
+
"agentics-verify": "./bin/verify.mjs"
|
|
15
|
+
},
|
|
16
|
+
"types": "./src/index.d.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./src/index.d.ts",
|
|
20
|
+
"import": "./src/index.mjs"
|
|
21
|
+
},
|
|
22
|
+
"./cleared": {
|
|
23
|
+
"types": "./src/cleared.d.ts",
|
|
24
|
+
"import": "./src/cleared.mjs"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"scripts": {
|
|
28
|
+
"test": "node --test test/verify.test.mjs"
|
|
29
|
+
},
|
|
30
|
+
"engines": { "node": ">=18" },
|
|
31
|
+
"files": ["bin", "src", "README.md", "LICENSE"],
|
|
32
|
+
"publishConfig": { "access": "public" },
|
|
33
|
+
"keywords": ["agentics", "evidence", "receipts", "merkle", "verify"]
|
|
34
|
+
}
|