@agentguard-run/spend 0.15.7 → 0.15.9

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.
Files changed (61) hide show
  1. package/README.md +61 -1
  2. package/dist/anchor/index.d.ts +35 -0
  3. package/dist/anchor/index.d.ts.map +1 -0
  4. package/dist/anchor/index.js +82 -0
  5. package/dist/anchor/index.js.map +1 -0
  6. package/dist/anchor/opentimestamps.d.ts +86 -0
  7. package/dist/anchor/opentimestamps.d.ts.map +1 -0
  8. package/dist/anchor/opentimestamps.js +239 -0
  9. package/dist/anchor/opentimestamps.js.map +1 -0
  10. package/dist/anchor/store.d.ts +29 -0
  11. package/dist/anchor/store.d.ts.map +1 -0
  12. package/dist/anchor/store.js +130 -0
  13. package/dist/anchor/store.js.map +1 -0
  14. package/dist/anchor/types.d.ts +84 -0
  15. package/dist/anchor/types.d.ts.map +1 -0
  16. package/dist/anchor/types.js +28 -0
  17. package/dist/anchor/types.js.map +1 -0
  18. package/dist/cli/anchor.d.ts +10 -0
  19. package/dist/cli/anchor.d.ts.map +1 -0
  20. package/dist/cli/anchor.js +177 -0
  21. package/dist/cli/anchor.js.map +1 -0
  22. package/dist/cli/colors.d.ts +1 -0
  23. package/dist/cli/colors.d.ts.map +1 -1
  24. package/dist/cli/colors.js +4 -0
  25. package/dist/cli/colors.js.map +1 -1
  26. package/dist/cli/main.d.ts.map +1 -1
  27. package/dist/cli/main.js +10 -0
  28. package/dist/cli/main.js.map +1 -1
  29. package/dist/cli/sample.d.ts +43 -0
  30. package/dist/cli/sample.d.ts.map +1 -0
  31. package/dist/cli/sample.js +154 -0
  32. package/dist/cli/sample.js.map +1 -0
  33. package/dist/cli/serve.d.ts +1 -1
  34. package/dist/cli/serve.d.ts.map +1 -1
  35. package/dist/cli/serve.js +10 -4
  36. package/dist/cli/serve.js.map +1 -1
  37. package/dist/cli/verify.d.ts.map +1 -1
  38. package/dist/cli/verify.js +75 -0
  39. package/dist/cli/verify.js.map +1 -1
  40. package/dist/dashboard/aggregate.d.ts +2 -0
  41. package/dist/dashboard/aggregate.d.ts.map +1 -1
  42. package/dist/dashboard/aggregate.js.map +1 -1
  43. package/dist/decision-log.d.ts +28 -0
  44. package/dist/decision-log.d.ts.map +1 -1
  45. package/dist/decision-log.js +34 -0
  46. package/dist/decision-log.js.map +1 -1
  47. package/dist/index.d.ts +5 -2
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +28 -4
  50. package/dist/index.js.map +1 -1
  51. package/dist/samples/flows.d.ts +28 -0
  52. package/dist/samples/flows.d.ts.map +1 -0
  53. package/dist/samples/flows.js +151 -0
  54. package/dist/samples/flows.js.map +1 -0
  55. package/dist/samples/verifier-html.d.ts +29 -0
  56. package/dist/samples/verifier-html.d.ts.map +1 -0
  57. package/dist/samples/verifier-html.js +188 -0
  58. package/dist/samples/verifier-html.js.map +1 -0
  59. package/package.json +7 -2
  60. package/src/bindings/bindings-enforce.test.ts +97 -0
  61. package/src/dashboard/aggregate.ts +8 -1
@@ -0,0 +1,188 @@
1
+ "use strict";
2
+ /**
3
+ * Standalone, self-verifying HTML for a sample receipt.
4
+ *
5
+ * The generated page embeds the receipt and, on load, re-derives the entry hash
6
+ * from the receipt's canonical JSON (SubtleCrypto SHA-256) and verifies the
7
+ * Ed25519 signature (SubtleCrypto Ed25519, where supported) — entirely
8
+ * in-browser, no network. If the browser lacks WebCrypto Ed25519, the hash
9
+ * chain is still checked and the page links to agentguard.run/verify with the
10
+ * receipt prefilled for signature verification.
11
+ *
12
+ * The in-browser canonicalJson MUST match src/decision-log.ts canonicalJson so
13
+ * the recomputed hash equals the signed entryHash.
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.buildSampleVerifierHtml = buildSampleVerifierHtml;
17
+ function buildSampleVerifierHtml(data) {
18
+ // Embed as JSON in a script tag (safe against </script> injection).
19
+ const payload = JSON.stringify(data).replace(/</g, '\\u003c');
20
+ return `<!doctype html>
21
+ <html lang="en"><head><meta charset="utf-8" />
22
+ <title>AgentGuard Sample Receipt · ${escapeHtml(data.flow)}</title>
23
+ <meta name="viewport" content="width=device-width,initial-scale=1" />
24
+ <style>
25
+ :root { --bg:#07101E; --surface:#0C1828; --border:rgba(42,188,180,0.22); --teal:#2abcb4; --text:#EAF2F6; --muted:#8AA0B3; --green:#4ade80; --red:#FF6B6B; --amber:#F1C40F; }
26
+ * { box-sizing:border-box; margin:0; padding:0; }
27
+ body { background:var(--bg); color:var(--text); font-family:-apple-system,system-ui,'DM Sans',sans-serif; line-height:1.5; padding:32px 16px; }
28
+ .wrap { max-width:820px; margin:0 auto; }
29
+ .brand { display:flex; align-items:center; gap:10px; margin-bottom:8px; }
30
+ .hex { width:22px; height:22px; background:var(--teal); color:var(--bg); display:inline-flex; align-items:center; justify-content:center; font-weight:800; border-radius:4px; }
31
+ h1 { font-size:20px; font-weight:800; }
32
+ .sub { color:var(--muted); font-size:13px; margin-bottom:22px; }
33
+ .card { background:var(--surface); border:1px solid var(--border); border-radius:10px; padding:20px 22px; margin-bottom:18px; }
34
+ .card h2 { font-size:11px; letter-spacing:.14em; text-transform:uppercase; color:var(--teal); margin-bottom:14px; font-family:ui-monospace,monospace; }
35
+ .row { display:flex; justify-content:space-between; gap:16px; padding:7px 0; border-bottom:1px solid rgba(255,255,255,.06); font-size:14px; }
36
+ .row:last-child { border-bottom:none; }
37
+ .row .k { color:var(--muted); }
38
+ .row .v { font-family:ui-monospace,monospace; text-align:right; word-break:break-all; }
39
+ .check { display:flex; align-items:flex-start; gap:10px; padding:9px 0; font-size:14px; }
40
+ .check .dot { width:18px; flex:0 0 18px; }
41
+ .ok { color:var(--green); } .bad { color:var(--red); } .warn { color:var(--amber); } .pending { color:var(--muted); }
42
+ pre { background:#050b14; border:1px solid rgba(255,255,255,.08); border-radius:8px; padding:14px; overflow-x:auto; font-size:12px; color:#cfe; }
43
+ a { color:var(--teal); }
44
+ .foot { color:var(--muted); font-size:12px; text-align:center; margin-top:24px; }
45
+ .badge { display:inline-block; font-family:ui-monospace,monospace; font-size:12px; padding:4px 10px; border-radius:4px; border:1px solid var(--border); }
46
+ </style></head>
47
+ <body><div class="wrap">
48
+ <div class="brand"><span class="hex">&#9187;</span><h1>AgentGuard&#174; Sample Receipt</h1></div>
49
+ <div class="sub">Flow: <b>${escapeHtml(data.flow)}</b> &middot; Self-verifying, offline. This is exactly what your counterparty would receive.</div>
50
+
51
+ <div class="card">
52
+ <h2>Verification</h2>
53
+ <div id="verdict" class="badge pending">verifying&hellip;</div>
54
+ <div id="checks" style="margin-top:12px"></div>
55
+ <div id="fallback" style="margin-top:12px; display:none" class="sub"></div>
56
+ </div>
57
+
58
+ <div class="card">
59
+ <h2>Receipt summary</h2>
60
+ <div id="summary"></div>
61
+ </div>
62
+
63
+ <div class="card">
64
+ <h2>Raw signed entry</h2>
65
+ <pre id="raw"></pre>
66
+ </div>
67
+
68
+ <div class="foot">
69
+ Signature scheme: Ed25519 over SHA-256 of canonical JSON. Zero data plane &mdash; no prompt or completion content is present.<br/>
70
+ <a href="https://agentguard.run/verify">agentguard.run/verify</a> &middot; <a href="https://www.npmjs.com/package/@agentguard-run/spend">npm: @agentguard-run/spend</a>
71
+ </div>
72
+ </div>
73
+
74
+ <script type="application/json" id="agentguard-receipt">${payload}</script>
75
+ <script>
76
+ (function () {
77
+ var DATA = JSON.parse(document.getElementById('agentguard-receipt').textContent);
78
+ var entry = DATA.entry;
79
+
80
+ // canonicalJson MUST match src/decision-log.ts exactly.
81
+ function canonicalJson(value) {
82
+ if (value === null || typeof value !== 'object') {
83
+ if (typeof value === 'number' && !isFinite(value)) throw new Error('non-finite');
84
+ return JSON.stringify(value);
85
+ }
86
+ if (Array.isArray(value)) return '[' + value.map(canonicalJson).join(',') + ']';
87
+ var keys = Object.keys(value).sort();
88
+ return '{' + keys.map(function (k) { return JSON.stringify(k) + ':' + canonicalJson(value[k]); }).join(',') + '}';
89
+ }
90
+ function hexToBytes(hex) {
91
+ var out = new Uint8Array(hex.length / 2);
92
+ for (var i = 0; i < out.length; i++) out[i] = parseInt(hex.substr(i * 2, 2), 16);
93
+ return out;
94
+ }
95
+ function bytesToHex(buf) {
96
+ var b = new Uint8Array(buf), s = '';
97
+ for (var i = 0; i < b.length; i++) s += b[i].toString(16).padStart(2, '0');
98
+ return s;
99
+ }
100
+ function esc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) { return ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c]; }); }
101
+
102
+ var checksEl = document.getElementById('checks');
103
+ var results = [];
104
+ function addCheck(state, text) {
105
+ results.push(state);
106
+ var cls = state === true ? 'ok' : state === 'pending' ? 'pending' : state === 'warn' ? 'warn' : 'bad';
107
+ var mark = state === true ? '&#10003;' : state === 'pending' ? '&#8987;' : state === 'warn' ? '&#9888;' : '&#10007;';
108
+ checksEl.innerHTML += '<div class="check ' + cls + '"><span class="dot">' + mark + '</span><span>' + text + '</span></div>';
109
+ }
110
+
111
+ function renderSummary() {
112
+ var d = entry.decision || {};
113
+ var oc = d.outcomeReceipt || {};
114
+ var rows = [
115
+ ['Flow', DATA.flow],
116
+ ['Decision', d.action],
117
+ ['Provider', d.provider],
118
+ ['Model (requested)', d.modelRequested],
119
+ ['Model (resolved)', d.modelResolved],
120
+ ['Policy', d.policyId + ' v' + d.policyVersion],
121
+ ['Delegated scope', (oc.delegatedScope || []).join(', ')],
122
+ ['Capability tier', oc.capabilityTier],
123
+ ['Transaction reference', oc.transactionReference],
124
+ ['Amount', oc.amountCents != null ? ('$' + (oc.amountCents / 100).toFixed(2) + ' ' + (oc.currency || '')) : ''],
125
+ ['Signer fingerprint', entry.signerFingerprint]
126
+ ];
127
+ document.getElementById('summary').innerHTML = rows.filter(function (r) { return r[1]; }).map(function (r) {
128
+ return '<div class="row"><span class="k">' + esc(r[0]) + '</span><span class="v">' + esc(r[1]) + '</span></div>';
129
+ }).join('');
130
+ document.getElementById('raw').textContent = JSON.stringify(entry, null, 2);
131
+ }
132
+
133
+ function setVerdict() {
134
+ var el = document.getElementById('verdict');
135
+ var anyBad = results.indexOf(false) !== -1;
136
+ var anyPending = results.indexOf('pending') !== -1;
137
+ if (anyBad) { el.textContent = '✗ verification FAILED'; el.className = 'badge bad'; el.style.borderColor = 'var(--red)'; }
138
+ else if (anyPending) { el.textContent = '⚠ hash chain valid; signature not checked here'; el.className = 'badge warn'; el.style.borderColor = 'var(--amber)'; }
139
+ else { el.textContent = '✓ receipt verified'; el.className = 'badge ok'; el.style.borderColor = 'var(--green)'; }
140
+ }
141
+
142
+ function showFallback() {
143
+ var fb = document.getElementById('fallback');
144
+ fb.style.display = 'block';
145
+ fb.innerHTML = 'This browser lacks WebCrypto Ed25519, so the signature was not checked locally. ' +
146
+ 'Verify the signature at <a href="https://agentguard.run/verify" target="_blank" rel="noopener">agentguard.run/verify</a> ' +
147
+ 'or run <code>agentguard verify</code> on the receipt.json.';
148
+ }
149
+
150
+ (async function run() {
151
+ renderSummary();
152
+ // 1) Genesis linkage
153
+ var isGenesis = entry.previousHash === '0'.repeat(64);
154
+ addCheck(isGenesis ? true : 'warn', 'previousHash is ' + (isGenesis ? 'genesis (chain start)' : 'a prior entry hash'));
155
+
156
+ // 2) Entry hash recomputation
157
+ var payload = { sequence: entry.sequence, decision: entry.decision, previousHash: entry.previousHash, signerFingerprint: entry.signerFingerprint };
158
+ var enc = new TextEncoder().encode(canonicalJson(payload));
159
+ var digest = await crypto.subtle.digest('SHA-256', enc);
160
+ var recomputed = bytesToHex(digest);
161
+ var hashOk = recomputed === entry.entryHash;
162
+ addCheck(hashOk, 'entry hash matches canonical JSON (SHA-256)' + (hashOk ? '' : ' &mdash; MISMATCH'));
163
+
164
+ // 3) Ed25519 signature (WebCrypto where available)
165
+ var sigChecked = false;
166
+ try {
167
+ var pub = await crypto.subtle.importKey('raw', hexToBytes(DATA.publicKeyHex), { name: 'Ed25519' }, false, ['verify']);
168
+ var okSig = await crypto.subtle.verify('Ed25519', pub, hexToBytes(entry.signature), hexToBytes(entry.entryHash));
169
+ addCheck(okSig, 'Ed25519 signature ' + (okSig ? 'valid for the embedded public key' : 'INVALID'));
170
+ sigChecked = true;
171
+ } catch (e) {
172
+ addCheck('pending', 'Ed25519 signature not checked (browser lacks WebCrypto Ed25519)');
173
+ }
174
+ if (!sigChecked) showFallback();
175
+ setVerdict();
176
+ })().catch(function (e) {
177
+ addCheck(false, 'verifier error: ' + esc(e && e.message || e));
178
+ setVerdict();
179
+ });
180
+ })();
181
+ </script>
182
+ </body></html>
183
+ `;
184
+ }
185
+ function escapeHtml(value) {
186
+ return String(value == null ? '' : value).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
187
+ }
188
+ //# sourceMappingURL=verifier-html.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"verifier-html.js","sourceRoot":"","sources":["../../src/samples/verifier-html.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;GAYG;;AAiBH,0DAuKC;AAvKD,SAAgB,uBAAuB,CAAC,IAAwB;IAC9D,oEAAoE;IACpE,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC;IAC9D,OAAO;;qCAE4B,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;8BA2B5B,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;0DAyBO,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6GhE,CAAC;AACF,CAAC;AAED,SAAS,UAAU,CAAC,KAAa;IAC/B,OAAO,MAAM,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,CAAC,CAAC,EAAE,EAAE,CAClE,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC,CAAC,CAAY,CAAA,CACvF,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentguard-run/spend",
3
- "version": "0.15.7",
3
+ "version": "0.15.9",
4
4
  "description": "All terminology and labels used in AgentGuard materials are descriptive of software functionality only, not legal definitions or guarantees of compliance. Terms like receipt, audit log, evidence, audit trail, and attestation refer solely to cryptographically-signed records produced by the software. Full functional-use disclaimer in README.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -198,10 +198,11 @@
198
198
  "@aws-sdk/client-bedrock-runtime": ">=3.600.0",
199
199
  "@aws-sdk/client-kms": ">=3.600.0",
200
200
  "@langchain/core": ">=0.3.0",
201
+ "ai": ">=6.0.0",
201
202
  "ioredis": ">=5.0.0",
202
203
  "openai": ">=5.0.0",
203
204
  "pg": ">=8.10.0",
204
- "ai": ">=6.0.0"
205
+ "opentimestamps": ">=0.4.0"
205
206
  },
206
207
  "peerDependenciesMeta": {
207
208
  "openai": {
@@ -227,10 +228,14 @@
227
228
  },
228
229
  "ai": {
229
230
  "optional": true
231
+ },
232
+ "opentimestamps": {
233
+ "optional": true
230
234
  }
231
235
  },
232
236
  "devDependencies": {
233
237
  "@types/node": "^20.0.0",
238
+ "opentimestamps": "^0.4.9",
234
239
  "typescript": "^5.0.0"
235
240
  },
236
241
  "engines": {
@@ -0,0 +1,97 @@
1
+ import test from 'node:test';
2
+ import { strict as assert } from 'assert';
3
+ import { withSpendGuard, AgentGuardBlockedError } from '../spend-guard';
4
+ import { withSpendGuardAnthropic } from './anthropic';
5
+ import { withSpendGuardBedrock } from './bedrock';
6
+ import { InMemorySpendStore } from '../store-memory';
7
+ import { InMemoryDecisionLogStore } from '../decision-log';
8
+ import { clearCostOverrides, setCostOverride } from '../cost-table';
9
+ import type { SpendPolicy } from '../types';
10
+
11
+ const scope = { tenantId: 'enforce-test' };
12
+
13
+ // A per_call cap of 1c that blocks: any priced call (>1c) is denied. Enforcement
14
+ // is a hard deterministic limit — identical semantics across every binding.
15
+ function blockPolicy(): SpendPolicy {
16
+ return {
17
+ id: 'enforce-block',
18
+ name: 'enforce block',
19
+ scope,
20
+ caps: [{ window: 'per_call', amountCents: 1, action: 'block' }],
21
+ mode: 'enforce',
22
+ version: 1,
23
+ effectiveFrom: '2026-07-16T00:00:00.000Z',
24
+ };
25
+ }
26
+
27
+ function harness() {
28
+ clearCostOverrides();
29
+ // Deterministic priced models per provider family.
30
+ setCostOverride('gpt-price-test', { inputCentsPerKtok: 100, outputCentsPerKtok: 100 });
31
+ setCostOverride('claude-price-test', { inputCentsPerKtok: 100, outputCentsPerKtok: 100 });
32
+ setCostOverride('amazon.price-test', { inputCentsPerKtok: 100, outputCentsPerKtok: 100 });
33
+ return {
34
+ spendStore: new InMemorySpendStore(),
35
+ logStore: new InMemoryDecisionLogStore(),
36
+ tokenEstimator: () => 1000,
37
+ };
38
+ }
39
+
40
+ test('OpenAI binding: priced call over the cap is BLOCKED and provider never called', async () => {
41
+ const cfg = harness();
42
+ let providerCalled = false;
43
+ const client = withSpendGuard(
44
+ { chat: { completions: { create: async () => { providerCalled = true; return {}; } } } },
45
+ { policy: blockPolicy(), scope, config: cfg },
46
+ );
47
+ await assert.rejects(
48
+ () => client.chat.completions.create({ model: 'gpt-price-test', messages: [{ content: 'x' }], max_tokens: 1000 }),
49
+ (e) => e instanceof AgentGuardBlockedError && e.decision.action === 'block' && e.decision.projectedCents === 200,
50
+ );
51
+ assert.equal(providerCalled, false, 'block prevents the provider dispatch');
52
+ });
53
+
54
+ test('Anthropic binding: priced call over the cap is BLOCKED', async () => {
55
+ const cfg = harness();
56
+ let providerCalled = false;
57
+ const client = withSpendGuardAnthropic(
58
+ { messages: { create: async () => { providerCalled = true; return {}; } } },
59
+ { policy: blockPolicy(), scope, config: cfg },
60
+ );
61
+ await assert.rejects(
62
+ () => client.messages.create({ model: 'claude-price-test', messages: [{ role: 'user', content: 'x' }], max_tokens: 1000 }),
63
+ (e) => e instanceof AgentGuardBlockedError && e.decision.projectedCents === 200,
64
+ );
65
+ assert.equal(providerCalled, false);
66
+ });
67
+
68
+ test('Bedrock binding: priced InvokeModel call over the cap is BLOCKED', async () => {
69
+ const cfg = harness();
70
+ let providerCalled = false;
71
+ class InvokeModelCommand { constructor(public input: Record<string, unknown>) {} }
72
+ const client = withSpendGuardBedrock(
73
+ { send: async () => { providerCalled = true; return {}; } },
74
+ { policy: blockPolicy(), scope, config: cfg },
75
+ );
76
+ await assert.rejects(
77
+ () => client.send(new InvokeModelCommand({
78
+ modelId: 'amazon.price-test',
79
+ body: JSON.stringify({ max_tokens: 1000, messages: [{ role: 'user', content: [{ type: 'text', text: 'x' }] }] }),
80
+ })),
81
+ (e) => e instanceof AgentGuardBlockedError && e.decision.projectedCents === 200,
82
+ );
83
+ assert.equal(providerCalled, false);
84
+ });
85
+
86
+ test('non-InvokeModel Bedrock commands pass through ungated (cost/enforce only on inference)', async () => {
87
+ const cfg = harness();
88
+ let passed = false;
89
+ class ListFoundationModelsCommand { constructor(public input: Record<string, unknown>) {} }
90
+ const client = withSpendGuardBedrock(
91
+ { send: async () => { passed = true; return { models: [] }; } },
92
+ { policy: blockPolicy(), scope, config: cfg },
93
+ );
94
+ const res = await client.send(new ListFoundationModelsCommand({}));
95
+ assert.deepEqual(res, { models: [] });
96
+ assert.equal(passed, true);
97
+ });
@@ -99,7 +99,14 @@ export interface CfoDashboard {
99
99
  byProvider: BucketSpend[];
100
100
  byBuilderCode: BucketSpend[];
101
101
  scopeKeyBreakdown: BucketSpend[]; // best-effort from triggeredScopeKey (may be sparse)
102
- integrity: { verified: boolean; entries: number; reason?: string; sequence?: number };
102
+ integrity: {
103
+ verified: boolean;
104
+ entries: number;
105
+ reason?: string;
106
+ sequence?: number;
107
+ /** Completeness signal: missing per-signer sequence numbers (possible omitted actions). */
108
+ sequenceGaps?: number[];
109
+ };
103
110
  }
104
111
 
105
112
  function topBuckets(map: Map<string, BucketSpend>, limit = 50): BucketSpend[] {