@kyaki/witness 0.1.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.
Files changed (46) hide show
  1. package/dist/accountability.d.ts +43 -0
  2. package/dist/accountability.d.ts.map +1 -0
  3. package/dist/accountability.js +64 -0
  4. package/dist/accountability.js.map +1 -0
  5. package/dist/adapters/monitor-http.d.ts +31 -0
  6. package/dist/adapters/monitor-http.d.ts.map +1 -0
  7. package/dist/adapters/monitor-http.js +59 -0
  8. package/dist/adapters/monitor-http.js.map +1 -0
  9. package/dist/adapters/witness-http.d.ts +62 -0
  10. package/dist/adapters/witness-http.d.ts.map +1 -0
  11. package/dist/adapters/witness-http.js +212 -0
  12. package/dist/adapters/witness-http.js.map +1 -0
  13. package/dist/durable-witness.d.ts +101 -0
  14. package/dist/durable-witness.d.ts.map +1 -0
  15. package/dist/durable-witness.js +179 -0
  16. package/dist/durable-witness.js.map +1 -0
  17. package/dist/fan-out.d.ts +20 -0
  18. package/dist/fan-out.d.ts.map +1 -0
  19. package/dist/fan-out.js +30 -0
  20. package/dist/fan-out.js.map +1 -0
  21. package/dist/index.d.ts +17 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +17 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/monitor.d.ts +83 -0
  26. package/dist/monitor.d.ts.map +1 -0
  27. package/dist/monitor.js +133 -0
  28. package/dist/monitor.js.map +1 -0
  29. package/dist/pollination.d.ts +109 -0
  30. package/dist/pollination.d.ts.map +1 -0
  31. package/dist/pollination.js +170 -0
  32. package/dist/pollination.js.map +1 -0
  33. package/dist/ssrf.d.ts +20 -0
  34. package/dist/ssrf.d.ts.map +1 -0
  35. package/dist/ssrf.js +205 -0
  36. package/dist/ssrf.js.map +1 -0
  37. package/package.json +33 -0
  38. package/src/accountability.ts +72 -0
  39. package/src/adapters/monitor-http.ts +69 -0
  40. package/src/adapters/witness-http.ts +235 -0
  41. package/src/durable-witness.ts +190 -0
  42. package/src/fan-out.ts +34 -0
  43. package/src/index.ts +28 -0
  44. package/src/monitor.ts +159 -0
  45. package/src/pollination.ts +229 -0
  46. package/src/ssrf.ts +190 -0
@@ -0,0 +1,179 @@
1
+ /**
2
+ * durable-witness.ts — the DURABLE, independent witness party.
3
+ *
4
+ * `@kyaki/core`'s `Witness` is the verification ENGINE, but its remembered head is
5
+ * in-process: it forgets on restart, so it provides no cross-restart rollback
6
+ * defense and cannot be horizontally scaled. `DurableWitness` closes that — it is
7
+ * the leg of "anchored AND externally witnessed" the moat rests on.
8
+ *
9
+ * The cross-party guarantee lives in TWO places working together:
10
+ * 1. PERSIST-BEFORE-EMIT — a co-signature is returned only AFTER the head it
11
+ * endorses is durably committed. Releasing a co-sig before recording it would
12
+ * let the witness forget, after a crash, that it had endorsed head N and later
13
+ * co-sign a forked N′ — exactly the equivocation it exists to prevent.
14
+ * 2. VERIFY-UNDER-LOCK against the DURABLE head — the append-only consistency
15
+ * check (which the store cannot perform; it has no Merkle state) runs inside
16
+ * the store's per-log lock against the head read FROM STORAGE, never an
17
+ * in-memory cache. So two instances sharing one store can never co-sign a fork
18
+ * from stale local state. In-process requests are additionally serialized per
19
+ * log by a KeyedMutex so two concurrent /cosign calls cannot interleave across
20
+ * the store await.
21
+ *
22
+ * What this does NOT defend (honest boundary, enforced at the relying party by
23
+ * `verifyWitnessedTreeHead`): a single witness does not defeat a general cross-party
24
+ * split-view, nor a compromised/colluding witness key — those need quorum ≥ 2 under
25
+ * disjoint control and, ultimately, witness-to-witness gossip (a documented follow-up).
26
+ */
27
+ import { KeyedMutex, buildWitnessCosignature, checkWitnessAdvance, } from '@kyaki/core';
28
+ export class DurableWitness {
29
+ keys;
30
+ logId;
31
+ store;
32
+ mutex = new KeyedMutex();
33
+ constructor(keys, logId, store) {
34
+ this.keys = keys;
35
+ this.logId = logId;
36
+ this.store = store;
37
+ }
38
+ /**
39
+ * Build a witness pinned to one operator log. The durable store is the SOLE
40
+ * authority for the witness's last head — it is read under lock on every cosign,
41
+ * so there is no in-memory baseline to go stale. `create()` is the only
42
+ * constructor so a caller cannot bypass the store with a hand-seeded head.
43
+ */
44
+ static async create(keys, logId, store) {
45
+ return new DurableWitness(keys, logId, store);
46
+ }
47
+ get id() {
48
+ return this.keys.did;
49
+ }
50
+ /** The witness's durable last-co-signed head. The operator builds its next
51
+ * consistency proof from THIS (reading storage, never a cache) so an honest
52
+ * extension is never spuriously rejected for a size mismatch. */
53
+ async head() {
54
+ return this.store.load();
55
+ }
56
+ /**
57
+ * Co-sign `sth` iff it is an append-only extension of the witness's durable head.
58
+ * Throws a named WITNESS_* error on rejection (the caller maps it to a status):
59
+ * - WITNESS_WRONG_LOG — sth is for a log this witness does not watch;
60
+ * - WITNESS_STH_SIGNATURE_INVALID / _CONSISTENCY_REQUIRED / _SIZE_MISMATCH
61
+ * — input faults (the proof is missing/wrong shape);
62
+ * - WITNESS_NON_MONOTONIC / _EQUIVOCATION / _CONSISTENCY_INVALID
63
+ * — operator-attributable conflicts (a fork attempt).
64
+ */
65
+ async cosign(sth, consistencyProof, now) {
66
+ if (sth.logId !== this.logId) {
67
+ throw new Error(`WITNESS_WRONG_LOG: this witness is pinned to ${this.logId}, not ${sth.logId}`);
68
+ }
69
+ const at = (now ?? new Date()).toISOString();
70
+ return this.mutex.runExclusive(this.logId, () => this.store.cosign((prior) => {
71
+ // The append-only check runs HERE — inside the store's per-log lock, against
72
+ // the DURABLE prior head. This is the load-bearing placement: a stale cache
73
+ // is never the baseline, so a scaled-out witness cannot co-sign a fork.
74
+ checkWitnessAdvance(prior, sth, consistencyProof);
75
+ const head = { logId: sth.logId, treeSize: sth.treeSize, rootHash: sth.rootHash, at };
76
+ const cosignature = buildWitnessCosignature(this.keys, sth);
77
+ // Retain the OPERATOR-signed STH (verbatim) + the consistency proof we just
78
+ // verified, so this witness can later serve the exact head it endorsed at THIS
79
+ // size for gossip — even after advancing far beyond it. checkWitnessAdvance
80
+ // already validated sth's operator signature before we reach here.
81
+ const endorsed = { head, cosignature, endorsedSth: sth };
82
+ if (consistencyProof)
83
+ endorsed.prefixProof = consistencyProof;
84
+ return endorsed;
85
+ }));
86
+ }
87
+ /** The operator-signed STH this witness endorsed at `treeSize` (its latest endorsed
88
+ * STH if omitted), read straight from the durable store — the gossip source a
89
+ * WitnessMonitor pulls to detect cross-witness equivocation. */
90
+ async signedHead(treeSize) {
91
+ return this.store.signedHead(treeSize);
92
+ }
93
+ /**
94
+ * This witness's OWN endorsement at `treeSize` (its latest if omitted): the
95
+ * operator-signed STH it endorsed AND its co-signature over it. The co-signature is
96
+ * re-derived deterministically from the retained STH — its body is a pure function of
97
+ * (witnessId, logId, treeSize, rootHash), so this needs no extra stored state and is
98
+ * byte-identical to the one emitted at cosign time.
99
+ *
100
+ * This is what relying-party POLLINATION pulls DIRECTLY from each trusted witness: the
101
+ * victim asks the witnesses themselves "did you co-sign this exact root at this size?",
102
+ * so the operator cannot withhold or cherry-pick which witnesses endorsed which root,
103
+ * and the witness-signed co-signature cannot be forged by a man-in-the-middle.
104
+ * undefined if this witness retained no endorsement at that size.
105
+ */
106
+ async endorsement(treeSize) {
107
+ const sth = await this.store.signedHead(treeSize);
108
+ if (!sth)
109
+ return undefined;
110
+ return { sth, cosignature: buildWitnessCosignature(this.keys, sth) };
111
+ }
112
+ }
113
+ /**
114
+ * An in-memory `WitnessStateStore` for single-process tests and demos. It serializes
115
+ * `cosign` via a promise chain (mirroring the durable per-log lock) and applies the
116
+ * same monotonic + equivocation backstop the durable store does. It is NOT durable —
117
+ * it forgets on restart, so the cross-restart guarantee requires a durable store
118
+ * (`@kyaki/postgres` `PgWitnessStore`). Use it to exercise the witness LOGIC.
119
+ */
120
+ export class MemoryWitnessStore {
121
+ current;
122
+ cosignature;
123
+ /** Append-only operator-signed STHs keyed by tree size (FIRST-write-wins, mirroring
124
+ * the durable kya_endorsed_sth ON CONFLICT DO NOTHING). Retained so a past size
125
+ * stays serveable for gossip after the witness advances beyond it. */
126
+ endorsed = new Map();
127
+ chain = Promise.resolve();
128
+ async load() {
129
+ return this.current ? { ...this.current } : undefined;
130
+ }
131
+ /** The co-signature co-committed with the current head (for parity with the
132
+ * durable store, which persists both in one transaction). */
133
+ async latestCosignature() {
134
+ return this.cosignature ? { ...this.cosignature } : undefined;
135
+ }
136
+ /** The endorsed operator STH at `treeSize` (latest if omitted). undefined if none. */
137
+ async signedHead(treeSize) {
138
+ const size = treeSize ?? (this.endorsed.size > 0 ? Math.max(...this.endorsed.keys()) : undefined);
139
+ if (size === undefined)
140
+ return undefined;
141
+ const sth = this.endorsed.get(size);
142
+ return sth ? { ...sth } : undefined;
143
+ }
144
+ cosign(decide) {
145
+ const run = this.chain.then(() => {
146
+ const prior = this.current ? { ...this.current } : undefined;
147
+ const { head, cosignature, endorsedSth } = decide(prior); // throws ⇒ rejection, state untouched
148
+ // Parity with PgWitnessStore: the head and the co-signature it endorses must name the
149
+ // same log (a mis-targeted decide() from a non-tree composer fails closed here too).
150
+ if (head.logId !== cosignature.logId) {
151
+ throw new Error('WITNESS_STORE_LOG_MISMATCH: head/cosig logId disagree');
152
+ }
153
+ // The retained STH must be the very head being endorsed (same size+root), or a
154
+ // monitor would serve an STH that does not match the co-signature.
155
+ if (endorsedSth.treeSize !== head.treeSize || endorsedSth.rootHash !== head.rootHash) {
156
+ throw new Error('WITNESS_STORE_STH_MISMATCH: endorsed STH does not match the head');
157
+ }
158
+ // Defense-in-depth backstop (the store cannot check Merkle consistency, but it
159
+ // can refuse a non-monotonic or same-size-equivocating head outright).
160
+ if (prior) {
161
+ if (head.treeSize < prior.treeSize) {
162
+ throw new Error('WITNESS_STORE_NON_MONOTONIC: refusing a head smaller than the durable one');
163
+ }
164
+ if (head.treeSize === prior.treeSize && head.rootHash !== prior.rootHash) {
165
+ throw new Error('WITNESS_STORE_EQUIVOCATION: a different root at an already-recorded size');
166
+ }
167
+ }
168
+ this.current = { ...head }; // co-commit: head + co-signature + endorsed STH together
169
+ this.cosignature = { ...cosignature };
170
+ if (!this.endorsed.has(endorsedSth.treeSize))
171
+ this.endorsed.set(endorsedSth.treeSize, { ...endorsedSth });
172
+ return cosignature;
173
+ });
174
+ // Keep the chain alive after a rejection so one bad request can't wedge the lock.
175
+ this.chain = run.then(() => undefined, () => undefined);
176
+ return run;
177
+ }
178
+ }
179
+ //# sourceMappingURL=durable-witness.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"durable-witness.js","sourceRoot":"","sources":["../src/durable-witness.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * fan-out.ts — bounded-concurrency fan-out over a list (RULES §5.12 / §5.16).
3
+ *
4
+ * Pulls every item concurrently but never more than `concurrency` in flight, so a large
5
+ * pinned source set cannot open unbounded sockets or buffer unbounded heap in a single tick
6
+ * (a relying-party / monitor self-DoS). Per-item settle (`Promise.allSettled`): one
7
+ * rejecting or slow item never rejects the whole call, and worst-case wall-clock is one
8
+ * timeout PER BATCH, not the sum. Shared by the WitnessMonitor gossip poll and relying-party
9
+ * pollination so the two stay symmetric (the same bound the monitor was hardened to has to
10
+ * hold for the victim's pull too).
11
+ */
12
+ /** Default in-flight cap. A handful of concurrent pulls is plenty for a witness/clearinghouse
13
+ * set; the bound exists to stop a large set from exhausting sockets/FDs, not to maximize throughput. */
14
+ export declare const POLL_CONCURRENCY = 8;
15
+ /**
16
+ * Run `fn` over every item, at most `concurrency` in flight at a time. Never rejects — each
17
+ * item's outcome is returned as a settled result in input order, so callers classify per item.
18
+ */
19
+ export declare function boundedFanOut<S, T>(items: readonly S[], fn: (item: S, index: number) => Promise<T>, concurrency?: number): Promise<PromiseSettledResult<T>[]>;
20
+ //# sourceMappingURL=fan-out.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fan-out.d.ts","sourceRoot":"","sources":["../src/fan-out.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH;yGACyG;AACzG,eAAO,MAAM,gBAAgB,IAAI,CAAC;AAElC;;;GAGG;AACH,wBAAsB,aAAa,CAAC,CAAC,EAAE,CAAC,EACtC,KAAK,EAAE,SAAS,CAAC,EAAE,EACnB,EAAE,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,EAC1C,WAAW,GAAE,MAAyB,GACrC,OAAO,CAAC,oBAAoB,CAAC,CAAC,CAAC,EAAE,CAAC,CASpC"}
@@ -0,0 +1,30 @@
1
+ /**
2
+ * fan-out.ts — bounded-concurrency fan-out over a list (RULES §5.12 / §5.16).
3
+ *
4
+ * Pulls every item concurrently but never more than `concurrency` in flight, so a large
5
+ * pinned source set cannot open unbounded sockets or buffer unbounded heap in a single tick
6
+ * (a relying-party / monitor self-DoS). Per-item settle (`Promise.allSettled`): one
7
+ * rejecting or slow item never rejects the whole call, and worst-case wall-clock is one
8
+ * timeout PER BATCH, not the sum. Shared by the WitnessMonitor gossip poll and relying-party
9
+ * pollination so the two stay symmetric (the same bound the monitor was hardened to has to
10
+ * hold for the victim's pull too).
11
+ */
12
+ /** Default in-flight cap. A handful of concurrent pulls is plenty for a witness/clearinghouse
13
+ * set; the bound exists to stop a large set from exhausting sockets/FDs, not to maximize throughput. */
14
+ export const POLL_CONCURRENCY = 8;
15
+ /**
16
+ * Run `fn` over every item, at most `concurrency` in flight at a time. Never rejects — each
17
+ * item's outcome is returned as a settled result in input order, so callers classify per item.
18
+ */
19
+ export async function boundedFanOut(items, fn, concurrency = POLL_CONCURRENCY) {
20
+ const width = Number.isInteger(concurrency) && concurrency > 0 ? concurrency : POLL_CONCURRENCY;
21
+ const out = new Array(items.length);
22
+ for (let i = 0; i < items.length; i += width) {
23
+ const batch = items.slice(i, i + width);
24
+ const settled = await Promise.allSettled(batch.map((s, j) => fn(s, i + j)));
25
+ for (let j = 0; j < settled.length; j++)
26
+ out[i + j] = settled[j];
27
+ }
28
+ return out;
29
+ }
30
+ //# sourceMappingURL=fan-out.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fan-out.js","sourceRoot":"","sources":["../src/fan-out.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH;yGACyG;AACzG,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC;AAElC;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,KAAmB,EACnB,EAA0C,EAC1C,cAAsB,gBAAgB;IAEtC,MAAM,KAAK,GAAG,MAAM,CAAC,SAAS,CAAC,WAAW,CAAC,IAAI,WAAW,GAAG,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,gBAAgB,CAAC;IAChG,MAAM,GAAG,GAA8B,IAAI,KAAK,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IAC/D,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,IAAI,KAAK,EAAE,CAAC;QAC7C,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,CAAC;QACxC,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QAC5E,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE;YAAE,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAE,CAAC;IACpE,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @kyaki/witness — the independent, durable witness party.
3
+ *
4
+ * A separate party from the operator: it depends ONLY on @kyaki/core (it can never
5
+ * reach the operator's audit log), holds its own key, and remembers the last head
6
+ * it endorsed OUT OF BAND in its own WitnessStateStore — so it refuses to
7
+ * contradict that head even across its own restart or horizontal scaling. This is
8
+ * the cross-party rollback defense the Mandate Transparency Log rests on.
9
+ */
10
+ export { DurableWitness, MemoryWitnessStore } from './durable-witness.js';
11
+ export { createWitnessHttpHandler, createWitnessServer, type WitnessLike, type HttpHandler, type HttpRequest, type HttpResponse, } from './adapters/witness-http.js';
12
+ export { WitnessMonitor, HttpGossipSource, type WitnessGossipSource, type WitnessMonitorConfig, type PollResult, } from './monitor.js';
13
+ export { assertSafeUrl, safeFetchJson, isPrivateIp, type SsrfPolicy } from './ssrf.js';
14
+ export { pollinate, HttpPollinationSource, type PollinationSource, type PollinationResult, type PollinateOptions, } from './pollination.js';
15
+ export { WitnessAccountabilityMonitor, type WitnessAccountabilityConfig } from './accountability.js';
16
+ export { createMonitorHttpHandler, createMonitorServer, CONFLICT_WIRE_VERSION, type MonitorHttpHandler, type MonitorHttpRequest, type MonitorHttpResponse, } from './adapters/monitor-http.js';
17
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1E,OAAO,EACL,wBAAwB,EAAE,mBAAmB,EAC7C,KAAK,WAAW,EAAE,KAAK,WAAW,EAAE,KAAK,WAAW,EAAE,KAAK,YAAY,GACxE,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,cAAc,EAAE,gBAAgB,EAChC,KAAK,mBAAmB,EAAE,KAAK,oBAAoB,EAAE,KAAK,UAAU,GACrE,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,WAAW,EAAE,KAAK,UAAU,EAAE,MAAM,WAAW,CAAC;AACvF,OAAO,EACL,SAAS,EAAE,qBAAqB,EAChC,KAAK,iBAAiB,EAAE,KAAK,iBAAiB,EAAE,KAAK,gBAAgB,GACtE,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,4BAA4B,EAAE,KAAK,2BAA2B,EAAE,MAAM,qBAAqB,CAAC;AACrG,OAAO,EACL,wBAAwB,EAAE,mBAAmB,EAAE,qBAAqB,EACpE,KAAK,kBAAkB,EAAE,KAAK,kBAAkB,EAAE,KAAK,mBAAmB,GAC3E,MAAM,4BAA4B,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @kyaki/witness — the independent, durable witness party.
3
+ *
4
+ * A separate party from the operator: it depends ONLY on @kyaki/core (it can never
5
+ * reach the operator's audit log), holds its own key, and remembers the last head
6
+ * it endorsed OUT OF BAND in its own WitnessStateStore — so it refuses to
7
+ * contradict that head even across its own restart or horizontal scaling. This is
8
+ * the cross-party rollback defense the Mandate Transparency Log rests on.
9
+ */
10
+ export { DurableWitness, MemoryWitnessStore } from './durable-witness.js';
11
+ export { createWitnessHttpHandler, createWitnessServer, } from './adapters/witness-http.js';
12
+ export { WitnessMonitor, HttpGossipSource, } from './monitor.js';
13
+ export { assertSafeUrl, safeFetchJson, isPrivateIp } from './ssrf.js';
14
+ export { pollinate, HttpPollinationSource, } from './pollination.js';
15
+ export { WitnessAccountabilityMonitor } from './accountability.js';
16
+ export { createMonitorHttpHandler, createMonitorServer, CONFLICT_WIRE_VERSION, } from './adapters/monitor-http.js';
17
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,EAAE,cAAc,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1E,OAAO,EACL,wBAAwB,EAAE,mBAAmB,GAE9C,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,cAAc,EAAE,gBAAgB,GAEjC,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,WAAW,EAAmB,MAAM,WAAW,CAAC;AACvF,OAAO,EACL,SAAS,EAAE,qBAAqB,GAEjC,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,4BAA4B,EAAoC,MAAM,qBAAqB,CAAC;AACrG,OAAO,EACL,wBAAwB,EAAE,mBAAmB,EAAE,qBAAqB,GAErE,MAAM,4BAA4B,CAAC"}
@@ -0,0 +1,83 @@
1
+ /**
2
+ * monitor.ts — the WitnessMonitor: an INDEPENDENT aggregator that gossips operator-
3
+ * signed STHs from a config-pinned witness set and surfaces a portable EquivocationProof
4
+ * when two witnesses disagree at one tree size.
5
+ *
6
+ * TRUST MODEL (read this — the monitor needs NO trust): it only ever relays
7
+ * self-verifying artifacts. Every STH it ingests is re-checked with verifySignedTreeHead
8
+ * against the operator key before it is retained, and every proof it emits is a pair of
9
+ * operator-signed STHs any third party re-verifies with verifyEquivocation against the
10
+ * pinned operator DID. A malicious monitor therefore cannot FABRICATE a false alarm
11
+ * (it cannot forge the operator's signature) — it can only WITHHOLD one. That makes the
12
+ * monitor a LIVENESS party, not a safety party.
13
+ *
14
+ * WHAT IT DOES AND DOES NOT CLOSE (§9 honesty — do not overstate):
15
+ * - It produces an irrefutable proof WHEN two conflicting operator-signed heads at one
16
+ * size both reach its corpus. The actual SAFETY control against a split-view remains
17
+ * verifyWitnessedTreeHead at quorum ≥ 2 (fail-closed at the relying party).
18
+ * - Detection is best-effort and operator-suppressible: an operator that never lets the
19
+ * monitor observe both forks (targeted split-view), or DDoSes/partitions it, yields no
20
+ * proof. ABSENCE OF A PROOF PROVES NOTHING.
21
+ * - SPENDS NEVER DEPEND ON THE MONITOR. It is read-only gossip + detection; the spend
22
+ * path (submit/approve) never calls it.
23
+ */
24
+ import { type ConflictStore, type EquivocationProof, type ObservedSthStore, type SignedTreeHead } from '@kyaki/core';
25
+ import { type SsrfPolicy } from './ssrf.js';
26
+ /** A gossip source the monitor pulls STHs from. Satisfied in-process by `DurableWitness`
27
+ * (it has `id` + `signedHead`) and over the wire by `HttpGossipSource`. */
28
+ export interface WitnessGossipSource {
29
+ /** The witness DID — used to attribute and key the observed-STH corpus. */
30
+ readonly id?: string;
31
+ /** The operator-signed STH the witness endorsed at `treeSize` (its latest if omitted). */
32
+ signedHead(treeSize?: number): Promise<SignedTreeHead | undefined>;
33
+ }
34
+ /** Outcome of one gossip round. `proof` is set the first round a conflict is detected
35
+ * (it is then durably recorded and re-served by ConflictStore). `unreachable` lists
36
+ * sources that errored — a LIVENESS signal, never blocking. */
37
+ export interface PollResult {
38
+ observed: number;
39
+ proof?: EquivocationProof;
40
+ unreachable: string[];
41
+ }
42
+ export interface WitnessMonitorConfig {
43
+ /** The operator log this monitor watches (the pinned operator DID). REQUIRED. */
44
+ logId: string;
45
+ /** The operator-INDEPENDENT, config-pinned witness set to gossip. */
46
+ sources: WitnessGossipSource[];
47
+ /** Durable observed-STH corpus (the detection window). */
48
+ observed: ObservedSthStore;
49
+ /** Durable klaxon store for detected proofs. */
50
+ conflicts: ConflictStore;
51
+ }
52
+ export declare class WitnessMonitor {
53
+ private readonly cfg;
54
+ constructor(cfg: WitnessMonitorConfig);
55
+ /** One gossip round: pull each source's latest head, back-fill each source at the
56
+ * sizes the others reached (so a same-size fork across witnesses is observable),
57
+ * retain only authentic operator STHs, then run same-size detection over the corpus
58
+ * and durably record any proof. Returns the proof the first round it forms.
59
+ *
60
+ * Idempotent and append-only: re-running never loses or fabricates evidence. Source
61
+ * errors are tolerated (collected in `unreachable`) — detection is liveness, not safety. */
62
+ poll(): Promise<PollResult>;
63
+ /** Verify-before-retain: only an authentic STH for the watched log enters the corpus,
64
+ * so junk/forged input can never form a pair. Returns whether it was NEWLY recorded
65
+ * (a duplicate re-observation returns false), so the caller counts distinct heads. */
66
+ private retain;
67
+ /** Bounded-concurrency fan-out over sources; never throws (per-source settle).
68
+ * Delegates to the shared `boundedFanOut` so the monitor and relying-party pollination
69
+ * enforce the SAME in-flight cap (POLL_CONCURRENCY). */
70
+ private fanOut;
71
+ }
72
+ /**
73
+ * HttpGossipSource — pulls a witness's endorsed STHs over the wire via `GET /sth?size`,
74
+ * through the SSRF-safe fetch. `id` is the witness DID (for corpus attribution).
75
+ */
76
+ export declare class HttpGossipSource implements WitnessGossipSource {
77
+ private readonly baseUrl;
78
+ readonly id: string;
79
+ private readonly ssrf;
80
+ constructor(baseUrl: string, id: string, ssrf: SsrfPolicy);
81
+ signedHead(treeSize?: number): Promise<SignedTreeHead | undefined>;
82
+ }
83
+ //# sourceMappingURL=monitor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"monitor.d.ts","sourceRoot":"","sources":["../src/monitor.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,OAAO,EAEL,KAAK,aAAa,EAAE,KAAK,iBAAiB,EAAE,KAAK,gBAAgB,EAAE,KAAK,cAAc,EACvF,MAAM,aAAa,CAAC;AAErB,OAAO,EAAiB,KAAK,UAAU,EAAE,MAAM,WAAW,CAAC;AAE3D;4EAC4E;AAC5E,MAAM,WAAW,mBAAmB;IAClC,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC;IACrB,0FAA0F;IAC1F,UAAU,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,CAAC;CACpE;AAED;;gEAEgE;AAChE,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,iBAAiB,CAAC;IAC1B,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAMD,MAAM,WAAW,oBAAoB;IACnC,iFAAiF;IACjF,KAAK,EAAE,MAAM,CAAC;IACd,qEAAqE;IACrE,OAAO,EAAE,mBAAmB,EAAE,CAAC;IAC/B,0DAA0D;IAC1D,QAAQ,EAAE,gBAAgB,CAAC;IAC3B,gDAAgD;IAChD,SAAS,EAAE,aAAa,CAAC;CAC1B;AAED,qBAAa,cAAc;IACb,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAAH,GAAG,EAAE,oBAAoB;IAEtD;;;;;;iGAM6F;IACvF,IAAI,IAAI,OAAO,CAAC,UAAU,CAAC;IAkDjC;;2FAEuF;YACzE,MAAM;IAKpB;;6DAEyD;IACzD,OAAO,CAAC,MAAM;CAMf;AAED;;;GAGG;AACH,qBAAa,gBAAiB,YAAW,mBAAmB;IAExD,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,QAAQ,CAAC,EAAE,EAAE,MAAM;IACnB,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAFJ,OAAO,EAAE,MAAM,EACvB,EAAE,EAAE,MAAM,EACF,IAAI,EAAE,UAAU;IAG7B,UAAU,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC;CAKzE"}
@@ -0,0 +1,133 @@
1
+ /**
2
+ * monitor.ts — the WitnessMonitor: an INDEPENDENT aggregator that gossips operator-
3
+ * signed STHs from a config-pinned witness set and surfaces a portable EquivocationProof
4
+ * when two witnesses disagree at one tree size.
5
+ *
6
+ * TRUST MODEL (read this — the monitor needs NO trust): it only ever relays
7
+ * self-verifying artifacts. Every STH it ingests is re-checked with verifySignedTreeHead
8
+ * against the operator key before it is retained, and every proof it emits is a pair of
9
+ * operator-signed STHs any third party re-verifies with verifyEquivocation against the
10
+ * pinned operator DID. A malicious monitor therefore cannot FABRICATE a false alarm
11
+ * (it cannot forge the operator's signature) — it can only WITHHOLD one. That makes the
12
+ * monitor a LIVENESS party, not a safety party.
13
+ *
14
+ * WHAT IT DOES AND DOES NOT CLOSE (§9 honesty — do not overstate):
15
+ * - It produces an irrefutable proof WHEN two conflicting operator-signed heads at one
16
+ * size both reach its corpus. The actual SAFETY control against a split-view remains
17
+ * verifyWitnessedTreeHead at quorum ≥ 2 (fail-closed at the relying party).
18
+ * - Detection is best-effort and operator-suppressible: an operator that never lets the
19
+ * monitor observe both forks (targeted split-view), or DDoSes/partitions it, yields no
20
+ * proof. ABSENCE OF A PROOF PROVES NOTHING.
21
+ * - SPENDS NEVER DEPEND ON THE MONITOR. It is read-only gossip + detection; the spend
22
+ * path (submit/approve) never calls it.
23
+ */
24
+ import { detectEquivocation, verifyEquivocation, verifySignedTreeHead, } from '@kyaki/core';
25
+ import { boundedFanOut } from './fan-out.js';
26
+ import { safeFetchJson } from './ssrf.js';
27
+ /** Cap on distinct sizes back-filled per round, so a hostile source advertising a huge
28
+ * size cannot blow up the per-round fan-out. */
29
+ const MAX_BACKFILL_SIZES = 64;
30
+ export class WitnessMonitor {
31
+ cfg;
32
+ constructor(cfg) {
33
+ this.cfg = cfg;
34
+ }
35
+ /** One gossip round: pull each source's latest head, back-fill each source at the
36
+ * sizes the others reached (so a same-size fork across witnesses is observable),
37
+ * retain only authentic operator STHs, then run same-size detection over the corpus
38
+ * and durably record any proof. Returns the proof the first round it forms.
39
+ *
40
+ * Idempotent and append-only: re-running never loses or fabricates evidence. Source
41
+ * errors are tolerated (collected in `unreachable`) — detection is liveness, not safety. */
42
+ async poll() {
43
+ const { logId, sources, observed, conflicts } = this.cfg;
44
+ const unreachable = new Set();
45
+ const pulled = new Set(); // `${i}:${size}` already fetched this round (skip re-pulls)
46
+ let observedCount = 0; // counts DISTINCT newly-recorded heads, not re-observations
47
+ // Pass 1: latest head from every source.
48
+ const latests = await this.fanOut(sources, (s) => s.signedHead());
49
+ const sizes = new Set();
50
+ for (let i = 0; i < sources.length; i++) {
51
+ const r = latests[i];
52
+ const label = sources[i].id ?? `source#${i}`;
53
+ if (r.status === 'rejected') {
54
+ unreachable.add(label);
55
+ continue;
56
+ }
57
+ if (!r.value)
58
+ continue;
59
+ pulled.add(`${i}:${r.value.treeSize}`);
60
+ sizes.add(r.value.treeSize);
61
+ if (await this.retain(label, r.value))
62
+ observedCount++;
63
+ }
64
+ // Pass 2: back-fill each source at the sizes OTHER sources reached (skipping a
65
+ // (source, size) already pulled in pass 1) — this is what makes a same-size fork
66
+ // (W1@n on fork A, W2@n on fork B) land as two corpus rows for detection.
67
+ const backfillSizes = [...sizes].sort((a, b) => a - b).slice(0, MAX_BACKFILL_SIZES);
68
+ for (const size of backfillSizes) {
69
+ const targets = sources
70
+ .map((s, i) => ({ s, i }))
71
+ .filter(({ i }) => !pulled.has(`${i}:${size}`));
72
+ if (targets.length === 0)
73
+ continue;
74
+ const got = await this.fanOut(targets.map((t) => t.s), (s) => s.signedHead(size));
75
+ for (let j = 0; j < targets.length; j++) {
76
+ const { i } = targets[j];
77
+ const r = got[j];
78
+ const label = sources[i].id ?? `source#${i}`;
79
+ if (r.status === 'rejected') {
80
+ unreachable.add(label);
81
+ continue;
82
+ }
83
+ if (!r.value)
84
+ continue;
85
+ pulled.add(`${i}:${size}`);
86
+ if (await this.retain(label, r.value))
87
+ observedCount++;
88
+ }
89
+ }
90
+ // Detect over the deduped corpus; a proof is SELF-verifying, but re-check before
91
+ // persisting (belt and suspenders — the store must never hold a non-proof).
92
+ const proof = detectEquivocation(await observed.distinct(), logId);
93
+ if (proof && verifyEquivocation(proof, logId)) {
94
+ await conflicts.record(proof);
95
+ return { observed: observedCount, proof, unreachable: [...unreachable] };
96
+ }
97
+ return { observed: observedCount, unreachable: [...unreachable] };
98
+ }
99
+ /** Verify-before-retain: only an authentic STH for the watched log enters the corpus,
100
+ * so junk/forged input can never form a pair. Returns whether it was NEWLY recorded
101
+ * (a duplicate re-observation returns false), so the caller counts distinct heads. */
102
+ async retain(witnessId, sth) {
103
+ if (sth.logId !== this.cfg.logId || !verifySignedTreeHead(sth))
104
+ return false;
105
+ return this.cfg.observed.record(witnessId, sth);
106
+ }
107
+ /** Bounded-concurrency fan-out over sources; never throws (per-source settle).
108
+ * Delegates to the shared `boundedFanOut` so the monitor and relying-party pollination
109
+ * enforce the SAME in-flight cap (POLL_CONCURRENCY). */
110
+ fanOut(sources, fn) {
111
+ return boundedFanOut(sources, (s) => fn(s));
112
+ }
113
+ }
114
+ /**
115
+ * HttpGossipSource — pulls a witness's endorsed STHs over the wire via `GET /sth?size`,
116
+ * through the SSRF-safe fetch. `id` is the witness DID (for corpus attribution).
117
+ */
118
+ export class HttpGossipSource {
119
+ baseUrl;
120
+ id;
121
+ ssrf;
122
+ constructor(baseUrl, id, ssrf) {
123
+ this.baseUrl = baseUrl;
124
+ this.id = id;
125
+ this.ssrf = ssrf;
126
+ }
127
+ async signedHead(treeSize) {
128
+ const url = treeSize === undefined ? `${this.baseUrl}/sth` : `${this.baseUrl}/sth?size=${treeSize}`;
129
+ const body = (await safeFetchJson(url, { method: 'GET' }, this.ssrf));
130
+ return body?.sth ?? undefined;
131
+ }
132
+ }
133
+ //# sourceMappingURL=monitor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"monitor.js","sourceRoot":"","sources":["../src/monitor.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,109 @@
1
+ /**
2
+ * pollination.ts — relying-party STH pollination (Phase 6).
3
+ *
4
+ * The one sound defense against a TARGETED split-view — an operator that shows a victim a
5
+ * root B@n it never shows any honest witness. The monitor cannot see B (it never enters the
6
+ * witnessed world), and quorum verification over the operator's OWN co-signature bundle can
7
+ * be cherry-picked by the operator. Pollination closes it by having the VICTIM pull each
8
+ * trusted witness's co-signature DIRECTLY (operator-independent, SSRF-safe) and run the
9
+ * audited `verifyWitnessedTreeHead`, FAIL-CLOSED: unless >= quorum distinct trusted witnesses
10
+ * co-signed the EXACT served root, the victim refuses.
11
+ *
12
+ * Why it is sound: a witness co-signature is WITNESS-signed over (logId, treeSize, rootHash),
13
+ * so neither a MITM nor the operator can forge "W endorsed B". The victim asks the witnesses
14
+ * themselves, so the operator cannot turn suppression into a false ACCEPT — if it withholds the
15
+ * witness responses the victim simply FAILS CLOSED (detection of the contradiction requires the
16
+ * victim to actually reach >= quorum honest witnesses directly). It does NOT defeat COLLUSION
17
+ * (>= quorum witnesses lying together — quorum >= 2 under disjoint control remains the only
18
+ * in-band lever). And SPENDS NEVER DEPEND ON WITNESS LIVENESS: pollination is an ADDITIONAL
19
+ * relying-party fail-closed check layered on quorum, not the kernel's safety gate; an
20
+ * unreachable witness is reported and simply does not count toward the quorum.
21
+ *
22
+ * DISPLAY ↔ VERDICT (load-bearing, do not weaken): `confirmed` is taken SOLELY from the audited
23
+ * `verifyWitnessedTreeHead` — never a hand-ported quorum rule. The display fields
24
+ * (matched/revoked/unreachable) are a MIRROR of that verdict's own accounting, gated on the
25
+ * SAME pre-count refusals the verifier applies (the policy.logId pin, the quorum-validity bound,
26
+ * and STH authenticity). So `matched` can never be reported under a basis the verdict already
27
+ * rejected — `matched >= quorum` if and only if `confirmed` (see the gate below). The two were
28
+ * once allowed to diverge on a malformed policy (a §4 over-claim caught by the Phase-6 review);
29
+ * that is now closed by computing the display under the verifier's exact gates.
30
+ */
31
+ import { type SignedTreeHead, type WitnessCosignature, type WitnessPolicy } from '@kyaki/core';
32
+ import { type SsrfPolicy } from './ssrf.js';
33
+ /** A trusted witness a relying party pulls a co-signature from. Satisfied in-process by
34
+ * `DurableWitness` (it has `id` + `endorsement`) and over the wire by `HttpPollinationSource`.
35
+ * `id` SHOULD be set (both built-in implementations set it): a source may only contribute the
36
+ * endorsement of the witness it claims to be — see the identity binding in `pollinate`. */
37
+ export interface PollinationSource {
38
+ /** The witness DID — must be in the relying party's `trustedWitnesses` to count, and must
39
+ * match the `witnessId` of the co-signature this source returns. */
40
+ readonly id?: string;
41
+ /** This witness's endorsement (its endorsed STH + its own co-signature) at `treeSize`.
42
+ * MUST be self-bounding (its own timeout) for a hard liveness guarantee; `pollinate` also
43
+ * applies a backstop per-source timeout so a hung source can never wedge the whole call. */
44
+ endorsement(treeSize?: number): Promise<{
45
+ sth: SignedTreeHead;
46
+ cosignature: WitnessCosignature;
47
+ } | undefined>;
48
+ }
49
+ export interface PollinationResult {
50
+ /** Fail-closed verdict: did >= quorum DISTINCT trusted, non-revoked witnesses co-sign the
51
+ * EXACT (logId, treeSize, rootHash) served? Driven SOLELY by `verifyWitnessedTreeHead`.
52
+ *
53
+ * What `confirmed=true` MEANS — and does NOT: a quorum of the relying party's OWN trusted set
54
+ * co-signed the exact served root. It is NOT proof of honesty: COLLUSION of >= quorum trusted
55
+ * witnesses is out of scope (raise the bar only with quorum >= 2 under disjoint control, or
56
+ * threshold signatures). `confirmed=false` with witnesses `unreachable` is INCONCLUSIVE
57
+ * (a liveness gap), not proof of a fork — distinguish it from `confirmed=false` with all
58
+ * witnesses reached (corroboration genuinely refused). */
59
+ confirmed: boolean;
60
+ /** Distinct trusted, NON-REVOKED witnesses whose co-signature authentically binds the served head.
61
+ * Counted exactly as the verdict counts — a revoked witness is never included, and on any input
62
+ * the verdict rejects pre-count (wrong operator / invalid quorum / non-authentic STH) `matched`
63
+ * is 0, so it can never contradict `confirmed` (e.g. show matched >= quorum while confirmed=false). */
64
+ matched: number;
65
+ /** Size of the trusted-witness set (the ORIGINAL allowlist; `revoked` is reported separately so the
66
+ * UI can render "N of T, R revoked" transparently rather than silently shrinking the denominator). */
67
+ total: number;
68
+ /** Distinct trusted witnesses whose co-signature bound the head but were NOT counted because they
69
+ * are revoked (Phase 7 v1b) — a transparency signal, never part of the quorum. */
70
+ revoked: number;
71
+ /** Distinct TRUSTED witnesses that could not be reached and did not end up matched or revoked —
72
+ * a LIVENESS signal (they never count toward the quorum). Keyed to the trusted set and excludes
73
+ * any witness already counted, so matched + revoked + unreachable <= total (a coherent partition);
74
+ * a non-trusted source that failed is not surfaced here (it could never have counted anyway). */
75
+ unreachable: string[];
76
+ }
77
+ /** Optional knobs for `pollinate`. */
78
+ export interface PollinateOptions {
79
+ /** Backstop timeout (ms) applied to EACH source pull, so a hung source (e.g. an in-process
80
+ * durable witness blocked on a store lock) can never wedge the whole call — the "worst-case
81
+ * one timeout" liveness guarantee is a property of `pollinate`, not delegated to the source
82
+ * type. Set above an HTTP source's own SSRF timeout so it only bites a genuinely hung source.
83
+ * Default 10000. */
84
+ sourceTimeoutMs?: number;
85
+ }
86
+ /**
87
+ * Pollinate: pull each trusted witness's co-signature at the served head's size and confirm,
88
+ * fail-closed, that >= quorum distinct trusted witnesses co-signed the EXACT (logId, treeSize,
89
+ * rootHash) the relying party was served. The verdict is the audited `verifyWitnessedTreeHead`
90
+ * (never a hand-ported quorum rule). An unreachable witness is reported and never makes the
91
+ * check pass. Liveness-independent: the fan-out is concurrency-bounded and per-source
92
+ * timeout-bounded, so neither a large pinned witness set nor a hung source can DoS the victim,
93
+ * and a pollination failure never blocks a spend (it is an additional relying-party check).
94
+ */
95
+ export declare function pollinate(servedSth: SignedTreeHead, sources: readonly PollinationSource[], policy: WitnessPolicy, opts?: PollinateOptions): Promise<PollinationResult>;
96
+ /** Pulls a witness's endorsement over the wire via `GET /cosig?size`, SSRF-guarded.
97
+ * The load-bearing object is the returned `cosignature` (witness-signed over the exact tuple);
98
+ * the returned `sth` is informational and not re-trusted by `pollinate`. */
99
+ export declare class HttpPollinationSource implements PollinationSource {
100
+ private readonly baseUrl;
101
+ readonly id: string;
102
+ private readonly ssrf;
103
+ constructor(baseUrl: string, id: string, ssrf: SsrfPolicy);
104
+ endorsement(treeSize?: number): Promise<{
105
+ sth: SignedTreeHead;
106
+ cosignature: WitnessCosignature;
107
+ } | undefined>;
108
+ }
109
+ //# sourceMappingURL=pollination.d.ts.map