@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,43 @@
1
+ /**
2
+ * accountability.ts — witness accountability (Phase 7). Makes witness COLLUSION
3
+ * ACCOUNTABLE (never prevented): when co-signatures by the SAME witness over the SAME
4
+ * (logId, treeSize) with DIFFERENT roots are CO-LOCATED at one observer, it forms a
5
+ * non-repudiable `WitnessEquivocationProof` naming that witness.
6
+ *
7
+ * THE CO-LOCATION PRECONDITION (the honest boundary — same class as the operator monitor):
8
+ * a proof forms only if BOTH divergent co-signatures reach this corpus. A witness co-signs
9
+ * honestly via its durable store (which never co-signs two roots at one size), so the two
10
+ * divergent co-signatures come from a COMPROMISED key / faulty store and are observed by
11
+ * DIFFERENT relying parties (e.g. two victims that pollinated the same witness at the same
12
+ * size and got different roots) or via gossip. If a colluder never lets its two
13
+ * observations meet at one extractor, NO proof forms. Absence of a proof proves nothing.
14
+ * Quorum intersection (k > n/2) guarantees a successful two-quorum split SHARES a
15
+ * double-signer — but extraction still requires both quorums' co-signatures to co-locate.
16
+ *
17
+ * SPENDS NEVER DEPEND ON THIS — it is the same liveness-only, opportunistic aggregation as
18
+ * the operator equivocation monitor. The sole spend-time safety control stays
19
+ * verifyWitnessedTreeHead at quorum >= 2 fail-closed.
20
+ */
21
+ import { type WitnessConflictStore, type WitnessCosigStore, type WitnessCosignature, type WitnessEquivocationProof } from '@kyaki/core';
22
+ export interface WitnessAccountabilityConfig {
23
+ /** Durable corpus of observed witness co-signatures (keyed so two divergent roots coexist). */
24
+ observed: WitnessCosigStore;
25
+ /** Durable store of detected proofs (named, faulty witnesses). */
26
+ conflicts: WitnessConflictStore;
27
+ }
28
+ export declare class WitnessAccountabilityMonitor {
29
+ private readonly cfg;
30
+ constructor(cfg: WitnessAccountabilityConfig);
31
+ /**
32
+ * Ingest one observed witness co-signature (the store verifies it before recording) and,
33
+ * if it now completes a same-witness / same-size / different-root pair, form + persist the
34
+ * proof. Returns the proof iff one is available for that witness (newly formed or already
35
+ * recorded). Idempotent — re-observing the same co-signature never duplicates or errors.
36
+ */
37
+ observe(cosig: WitnessCosignature): Promise<WitnessEquivocationProof | undefined>;
38
+ /** Re-scan every observed witness and persist any equivocation proof. Returns the proofs held. */
39
+ sweep(): Promise<WitnessEquivocationProof[]>;
40
+ /** A witness's proof, re-detected from the corpus (and persisted) or read back from store. */
41
+ private detectFor;
42
+ }
43
+ //# sourceMappingURL=accountability.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"accountability.d.ts","sourceRoot":"","sources":["../src/accountability.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EAEL,KAAK,oBAAoB,EAAE,KAAK,iBAAiB,EAAE,KAAK,kBAAkB,EAAE,KAAK,wBAAwB,EAC1G,MAAM,aAAa,CAAC;AAErB,MAAM,WAAW,2BAA2B;IAC1C,+FAA+F;IAC/F,QAAQ,EAAE,iBAAiB,CAAC;IAC5B,kEAAkE;IAClE,SAAS,EAAE,oBAAoB,CAAC;CACjC;AAED,qBAAa,4BAA4B;IAC3B,OAAO,CAAC,QAAQ,CAAC,GAAG;gBAAH,GAAG,EAAE,2BAA2B;IAE7D;;;;;OAKG;IACG,OAAO,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,wBAAwB,GAAG,SAAS,CAAC;IASvF,kGAAkG;IAC5F,KAAK,IAAI,OAAO,CAAC,wBAAwB,EAAE,CAAC;IASlD,8FAA8F;YAChF,SAAS;CAUxB"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * accountability.ts — witness accountability (Phase 7). Makes witness COLLUSION
3
+ * ACCOUNTABLE (never prevented): when co-signatures by the SAME witness over the SAME
4
+ * (logId, treeSize) with DIFFERENT roots are CO-LOCATED at one observer, it forms a
5
+ * non-repudiable `WitnessEquivocationProof` naming that witness.
6
+ *
7
+ * THE CO-LOCATION PRECONDITION (the honest boundary — same class as the operator monitor):
8
+ * a proof forms only if BOTH divergent co-signatures reach this corpus. A witness co-signs
9
+ * honestly via its durable store (which never co-signs two roots at one size), so the two
10
+ * divergent co-signatures come from a COMPROMISED key / faulty store and are observed by
11
+ * DIFFERENT relying parties (e.g. two victims that pollinated the same witness at the same
12
+ * size and got different roots) or via gossip. If a colluder never lets its two
13
+ * observations meet at one extractor, NO proof forms. Absence of a proof proves nothing.
14
+ * Quorum intersection (k > n/2) guarantees a successful two-quorum split SHARES a
15
+ * double-signer — but extraction still requires both quorums' co-signatures to co-locate.
16
+ *
17
+ * SPENDS NEVER DEPEND ON THIS — it is the same liveness-only, opportunistic aggregation as
18
+ * the operator equivocation monitor. The sole spend-time safety control stays
19
+ * verifyWitnessedTreeHead at quorum >= 2 fail-closed.
20
+ */
21
+ import { detectWitnessEquivocation, verifyWitnessCosignature, verifyWitnessEquivocation, } from '@kyaki/core';
22
+ export class WitnessAccountabilityMonitor {
23
+ cfg;
24
+ constructor(cfg) {
25
+ this.cfg = cfg;
26
+ }
27
+ /**
28
+ * Ingest one observed witness co-signature (the store verifies it before recording) and,
29
+ * if it now completes a same-witness / same-size / different-root pair, form + persist the
30
+ * proof. Returns the proof iff one is available for that witness (newly formed or already
31
+ * recorded). Idempotent — re-observing the same co-signature never duplicates or errors.
32
+ */
33
+ async observe(cosig) {
34
+ // Verify-before-record at the monitor TOO (defense in depth — a non-authentic co-signature
35
+ // must never reach the corpus, where it could PK-squat and shadow a genuine root). The store
36
+ // also re-verifies, so neither layer can be the single point that lets junk in.
37
+ if (!cosig || !verifyWitnessCosignature(cosig))
38
+ return undefined;
39
+ await this.cfg.observed.record(cosig);
40
+ return this.detectFor(cosig.witnessId);
41
+ }
42
+ /** Re-scan every observed witness and persist any equivocation proof. Returns the proofs held. */
43
+ async sweep() {
44
+ const out = [];
45
+ for (const w of await this.cfg.observed.witnesses()) {
46
+ const p = await this.detectFor(w);
47
+ if (p)
48
+ out.push(p);
49
+ }
50
+ return out;
51
+ }
52
+ /** A witness's proof, re-detected from the corpus (and persisted) or read back from store. */
53
+ async detectFor(witnessId) {
54
+ const cosigs = await this.cfg.observed.byWitness(witnessId);
55
+ const proof = detectWitnessEquivocation(cosigs, witnessId);
56
+ if (proof && verifyWitnessEquivocation(proof, witnessId)) {
57
+ await this.cfg.conflicts.record(proof); // monotone, idempotent
58
+ return proof;
59
+ }
60
+ // No fresh pair (or only one root seen so far) ⇒ surface any already-recorded proof.
61
+ return this.cfg.conflicts.forWitness(witnessId);
62
+ }
63
+ }
64
+ //# sourceMappingURL=accountability.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"accountability.js","sourceRoot":"","sources":["../src/accountability.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EACL,yBAAyB,EAAE,wBAAwB,EAAE,yBAAyB,GAE/E,MAAM,aAAa,CAAC;AASrB,MAAM,OAAO,4BAA4B;IACV;IAA7B,YAA6B,GAAgC;QAAhC,QAAG,GAAH,GAAG,CAA6B;IAAG,CAAC;IAEjE;;;;;OAKG;IACH,KAAK,CAAC,OAAO,CAAC,KAAyB;QACrC,2FAA2F;QAC3F,6FAA6F;QAC7F,gFAAgF;QAChF,IAAI,CAAC,KAAK,IAAI,CAAC,wBAAwB,CAAC,KAAK,CAAC;YAAE,OAAO,SAAS,CAAC;QACjE,MAAM,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACtC,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;IACzC,CAAC;IAED,kGAAkG;IAClG,KAAK,CAAC,KAAK;QACT,MAAM,GAAG,GAA+B,EAAE,CAAC;QAC3C,KAAK,MAAM,CAAC,IAAI,MAAM,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,SAAS,EAAE,EAAE,CAAC;YACpD,MAAM,CAAC,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;YAClC,IAAI,CAAC;gBAAE,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACrB,CAAC;QACD,OAAO,GAAG,CAAC;IACb,CAAC;IAED,8FAA8F;IACtF,KAAK,CAAC,SAAS,CAAC,SAAiB;QACvC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;QAC5D,MAAM,KAAK,GAAG,yBAAyB,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;QAC3D,IAAI,KAAK,IAAI,yBAAyB,CAAC,KAAK,EAAE,SAAS,CAAC,EAAE,CAAC;YACzD,MAAM,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAG,uBAAuB;YACjE,OAAO,KAAK,CAAC;QACf,CAAC;QACD,qFAAqF;QACrF,OAAO,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC;IAClD,CAAC;CACF"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * adapters/monitor-http.ts — the WitnessMonitor's HTTP edge.
3
+ *
4
+ * One read route, `GET /conflict`, serving the durable EquivocationProof (the klaxon):
5
+ * - 200 { version, proof } — a detected, self-verifying proof. The CONSUMER MUST
6
+ * re-verify it with verifyEquivocation(proof, PINNED_OPERATOR_DID); this endpoint is
7
+ * an UNTRUSTED aggregator — a 200 status is not evidence, the proof bytes are.
8
+ * - 204 — no conflict OBSERVED by this monitor (NOT "no fork
9
+ * exists" — absence proves nothing; detection is liveness-bounded).
10
+ * - 500 { error } — a monitor-internal fault.
11
+ *
12
+ * Mirrors witness-http hardening: a thrown store/DB fault becomes a WRITTEN response,
13
+ * never a hung socket; the served body is a single fixed-shape proof, not unbounded data.
14
+ */
15
+ import { type Server } from 'node:http';
16
+ import type { ConflictStore } from '@kyaki/core';
17
+ /** Bumped if the wire shape of the /conflict body changes (consumer compatibility). */
18
+ export declare const CONFLICT_WIRE_VERSION = 1;
19
+ export interface MonitorHttpRequest {
20
+ method: string;
21
+ path: string;
22
+ }
23
+ export interface MonitorHttpResponse {
24
+ status: number;
25
+ body: unknown;
26
+ }
27
+ export type MonitorHttpHandler = (req: MonitorHttpRequest) => Promise<MonitorHttpResponse>;
28
+ export declare function createMonitorHttpHandler(conflicts: ConflictStore): MonitorHttpHandler;
29
+ /** Bind the pure handler to node:http. No request body is read (read-only routes). */
30
+ export declare function createMonitorServer(conflicts: ConflictStore): Server;
31
+ //# sourceMappingURL=monitor-http.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"monitor-http.d.ts","sourceRoot":"","sources":["../../src/adapters/monitor-http.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAAoC,KAAK,MAAM,EAAE,MAAM,WAAW,CAAC;AAC1E,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,uFAAuF;AACvF,eAAO,MAAM,qBAAqB,IAAI,CAAC;AAEvC,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd;AACD,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,OAAO,CAAC;CACf;AACD,MAAM,MAAM,kBAAkB,GAAG,CAAC,GAAG,EAAE,kBAAkB,KAAK,OAAO,CAAC,mBAAmB,CAAC,CAAC;AAE3F,wBAAgB,wBAAwB,CAAC,SAAS,EAAE,aAAa,GAAG,kBAAkB,CAarF;AAED,sFAAsF;AACtF,wBAAgB,mBAAmB,CAAC,SAAS,EAAE,aAAa,GAAG,MAAM,CAsBpE"}
@@ -0,0 +1,59 @@
1
+ /**
2
+ * adapters/monitor-http.ts — the WitnessMonitor's HTTP edge.
3
+ *
4
+ * One read route, `GET /conflict`, serving the durable EquivocationProof (the klaxon):
5
+ * - 200 { version, proof } — a detected, self-verifying proof. The CONSUMER MUST
6
+ * re-verify it with verifyEquivocation(proof, PINNED_OPERATOR_DID); this endpoint is
7
+ * an UNTRUSTED aggregator — a 200 status is not evidence, the proof bytes are.
8
+ * - 204 — no conflict OBSERVED by this monitor (NOT "no fork
9
+ * exists" — absence proves nothing; detection is liveness-bounded).
10
+ * - 500 { error } — a monitor-internal fault.
11
+ *
12
+ * Mirrors witness-http hardening: a thrown store/DB fault becomes a WRITTEN response,
13
+ * never a hung socket; the served body is a single fixed-shape proof, not unbounded data.
14
+ */
15
+ import { createServer as nodeCreateServer } from 'node:http';
16
+ /** Bumped if the wire shape of the /conflict body changes (consumer compatibility). */
17
+ export const CONFLICT_WIRE_VERSION = 1;
18
+ export function createMonitorHttpHandler(conflicts) {
19
+ return async (req) => {
20
+ if (req.method === 'GET' && req.path === '/conflict') {
21
+ try {
22
+ const proof = await conflicts.latest();
23
+ if (!proof)
24
+ return { status: 204, body: undefined };
25
+ return { status: 200, body: { version: CONFLICT_WIRE_VERSION, proof } };
26
+ }
27
+ catch {
28
+ return { status: 500, body: { error: 'INTERNAL' } };
29
+ }
30
+ }
31
+ return { status: 404, body: { error: 'NOT_FOUND' } };
32
+ };
33
+ }
34
+ /** Bind the pure handler to node:http. No request body is read (read-only routes). */
35
+ export function createMonitorServer(conflicts) {
36
+ const handle = createMonitorHttpHandler(conflicts);
37
+ return nodeCreateServer((req, res) => {
38
+ req.on('error', () => { });
39
+ res.on('error', () => { });
40
+ void (async () => {
41
+ const path = (req.url ?? '/').split('?')[0] ?? '/';
42
+ try {
43
+ const result = await handle({ method: req.method ?? 'GET', path });
44
+ if (result.status === 204) {
45
+ res.writeHead(204);
46
+ res.end();
47
+ return;
48
+ }
49
+ res.writeHead(result.status, { 'Content-Type': 'application/json' });
50
+ res.end(JSON.stringify(result.body));
51
+ }
52
+ catch {
53
+ res.writeHead(500, { 'Content-Type': 'application/json' });
54
+ res.end(JSON.stringify({ error: 'INTERNAL' }));
55
+ }
56
+ })();
57
+ });
58
+ }
59
+ //# sourceMappingURL=monitor-http.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"monitor-http.js","sourceRoot":"","sources":["../../src/adapters/monitor-http.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAAE,YAAY,IAAI,gBAAgB,EAAe,MAAM,WAAW,CAAC;AAG1E,uFAAuF;AACvF,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC;AAYvC,MAAM,UAAU,wBAAwB,CAAC,SAAwB;IAC/D,OAAO,KAAK,EAAE,GAAG,EAAE,EAAE;QACnB,IAAI,GAAG,CAAC,MAAM,KAAK,KAAK,IAAI,GAAG,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;YACrD,IAAI,CAAC;gBACH,MAAM,KAAK,GAAG,MAAM,SAAS,CAAC,MAAM,EAAE,CAAC;gBACvC,IAAI,CAAC,KAAK;oBAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;gBACpD,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,OAAO,EAAE,qBAAqB,EAAE,KAAK,EAAE,EAAE,CAAC;YAC1E,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,EAAE,CAAC;YACtD,CAAC;QACH,CAAC;QACD,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,EAAE,CAAC;IACvD,CAAC,CAAC;AACJ,CAAC;AAED,sFAAsF;AACtF,MAAM,UAAU,mBAAmB,CAAC,SAAwB;IAC1D,MAAM,MAAM,GAAG,wBAAwB,CAAC,SAAS,CAAC,CAAC;IACnD,OAAO,gBAAgB,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;QACnC,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QAC1B,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;QAC1B,KAAK,CAAC,KAAK,IAAI,EAAE;YACf,MAAM,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC;YACnD,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,IAAI,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;gBACnE,IAAI,MAAM,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;oBAC1B,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;oBACnB,GAAG,CAAC,GAAG,EAAE,CAAC;oBACV,OAAO;gBACT,CAAC;gBACD,GAAG,CAAC,SAAS,CAAC,MAAM,CAAC,MAAM,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,CAAC,CAAC;gBACrE,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;YACvC,CAAC;YAAC,MAAM,CAAC;gBACP,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE,CAAC,CAAC;gBAC3D,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC;YACjD,CAAC;QACH,CAAC,CAAC,EAAE,CAAC;IACP,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,62 @@
1
+ /**
2
+ * adapters/witness-http.ts — the witness's HTTP edge (a transport, not authority).
3
+ *
4
+ * A pure `(req) => Promise<res>` handler plus a node:http binding, mirroring
5
+ * @kyaki/api's adapter — but HARDENED per the design review, because a witness is a
6
+ * public-facing party an operator does not control:
7
+ * - a request-body BYTE CAP in the binding (413) so an unbounded POST can't
8
+ * exhaust heap before parsing;
9
+ * - a consistency-proof LENGTH CAP in the handler (400) — a real proof is
10
+ * O(log n) (≤ 64 nodes covers 2^64 entries), so a longer one is junk/abuse;
11
+ * - a try/catch around dispatch so a thrown WITNESS_* error becomes a WRITTEN
12
+ * response with the right status, never a hung socket / TCP reset;
13
+ * - an error TAXONOMY that keeps the smoking gun loud: an operator-attributable
14
+ * CONFLICT (equivocation / non-monotonic / non-consistent fork) is 409, an
15
+ * input fault (bad signature / missing or mis-shaped proof / wrong log) is 400,
16
+ * and only an unexpected internal fault is 500.
17
+ *
18
+ * NOTE (documented out-of-scope, per review): /cosign is unauthenticated. Any
19
+ * co-signature a replayer obtains is honest (the witness only ever endorses the
20
+ * operator's own signed heads), but advancing the anchor and the write path should
21
+ * be gated to the operator in production (private network / mTLS / a signed request
22
+ * envelope). The byte cap blunts the worst abuse; rate-limiting is a deployment concern.
23
+ */
24
+ import { type Server } from 'node:http';
25
+ import type { ConsistencyProof, SignedTreeHead, WitnessCosignature, WitnessHead } from '@kyaki/core';
26
+ /** The witness surface the adapter needs (satisfied by DurableWitness). */
27
+ export interface WitnessLike {
28
+ cosign(sth: SignedTreeHead, consistencyProof?: ConsistencyProof): Promise<WitnessCosignature>;
29
+ head(): Promise<WitnessHead | undefined>;
30
+ /** The operator-signed STH endorsed at `treeSize` (latest if omitted) — the gossip
31
+ * source a WitnessMonitor pulls via `GET /sth`. */
32
+ signedHead(treeSize?: number): Promise<SignedTreeHead | undefined>;
33
+ /** This witness's endorsement (its endorsed STH + its own co-signature) at `treeSize`
34
+ * (latest if omitted) — what a relying party pulls via `GET /cosig` to POLLINATE the
35
+ * head it was served (confirm a quorum of trusted witnesses co-signed the exact root). */
36
+ endorsement(treeSize?: number): Promise<{
37
+ sth: SignedTreeHead;
38
+ cosignature: WitnessCosignature;
39
+ } | undefined>;
40
+ }
41
+ export interface HttpRequest {
42
+ method: string;
43
+ path: string;
44
+ query?: Record<string, string | undefined>;
45
+ body?: unknown;
46
+ }
47
+ export interface HttpResponse {
48
+ status: number;
49
+ body: unknown;
50
+ }
51
+ export type HttpHandler = (req: HttpRequest) => Promise<HttpResponse>;
52
+ /**
53
+ * Routes:
54
+ * POST /cosign { sth, consistencyProof? } → 200 WitnessCosignature
55
+ * · 400 input fault · 409 operator conflict
56
+ * · 400 PROOF_TOO_LARGE
57
+ * GET /head → 200 { head: WitnessHead | null }
58
+ */
59
+ export declare function createWitnessHttpHandler(witness: WitnessLike): HttpHandler;
60
+ /** Bind the pure handler to node:http, enforcing the request-body byte cap. */
61
+ export declare function createWitnessServer(witness: WitnessLike): Server;
62
+ //# sourceMappingURL=witness-http.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"witness-http.d.ts","sourceRoot":"","sources":["../../src/adapters/witness-http.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,OAAO,EAAoC,KAAK,MAAM,EAAE,MAAM,WAAW,CAAC;AAC1E,OAAO,KAAK,EAAE,gBAAgB,EAAE,cAAc,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAErG,2EAA2E;AAC3E,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,GAAG,EAAE,cAAc,EAAE,gBAAgB,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;IAC9F,IAAI,IAAI,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CAAC;IACzC;wDACoD;IACpD,UAAU,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,CAAC;IACnE;;+FAE2F;IAC3F,WAAW,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,GAAG,EAAE,cAAc,CAAC;QAAC,WAAW,EAAE,kBAAkB,CAAA;KAAE,GAAG,SAAS,CAAC,CAAC;CAC/G;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IAC3C,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,OAAO,CAAC;CACf;AAED,MAAM,MAAM,WAAW,GAAG,CAAC,GAAG,EAAE,WAAW,KAAK,OAAO,CAAC,YAAY,CAAC,CAAC;AAiCtE;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,WAAW,GAAG,WAAW,CA6E1E;AAED,+EAA+E;AAC/E,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,WAAW,GAAG,MAAM,CA+DhE"}
@@ -0,0 +1,212 @@
1
+ /**
2
+ * adapters/witness-http.ts — the witness's HTTP edge (a transport, not authority).
3
+ *
4
+ * A pure `(req) => Promise<res>` handler plus a node:http binding, mirroring
5
+ * @kyaki/api's adapter — but HARDENED per the design review, because a witness is a
6
+ * public-facing party an operator does not control:
7
+ * - a request-body BYTE CAP in the binding (413) so an unbounded POST can't
8
+ * exhaust heap before parsing;
9
+ * - a consistency-proof LENGTH CAP in the handler (400) — a real proof is
10
+ * O(log n) (≤ 64 nodes covers 2^64 entries), so a longer one is junk/abuse;
11
+ * - a try/catch around dispatch so a thrown WITNESS_* error becomes a WRITTEN
12
+ * response with the right status, never a hung socket / TCP reset;
13
+ * - an error TAXONOMY that keeps the smoking gun loud: an operator-attributable
14
+ * CONFLICT (equivocation / non-monotonic / non-consistent fork) is 409, an
15
+ * input fault (bad signature / missing or mis-shaped proof / wrong log) is 400,
16
+ * and only an unexpected internal fault is 500.
17
+ *
18
+ * NOTE (documented out-of-scope, per review): /cosign is unauthenticated. Any
19
+ * co-signature a replayer obtains is honest (the witness only ever endorses the
20
+ * operator's own signed heads), but advancing the anchor and the write path should
21
+ * be gated to the operator in production (private network / mTLS / a signed request
22
+ * envelope). The byte cap blunts the worst abuse; rate-limiting is a deployment concern.
23
+ */
24
+ import { createServer as nodeCreateServer } from 'node:http';
25
+ /** A real Merkle consistency proof is O(log n); anything longer is junk or abuse. */
26
+ const MAX_PROOF_NODES = 64;
27
+ /** Honest STH + proof is well under 1 KiB; cap the raw body far below heap-risk. */
28
+ const MAX_BODY_BYTES = 64 * 1024;
29
+ /** Operator-attributable conflicts → 409 (the alarm); input faults → 400. */
30
+ const OPERATOR_CONFLICTS = new Set([
31
+ 'WITNESS_NON_MONOTONIC', 'WITNESS_EQUIVOCATION', 'WITNESS_CONSISTENCY_INVALID',
32
+ 'WITNESS_STORE_NON_MONOTONIC', 'WITNESS_STORE_EQUIVOCATION',
33
+ ]);
34
+ const INPUT_FAULTS = new Set([
35
+ 'WITNESS_STH_SIGNATURE_INVALID', 'WITNESS_CONSISTENCY_REQUIRED',
36
+ 'WITNESS_CONSISTENCY_SIZE_MISMATCH', 'WITNESS_WRONG_LOG',
37
+ ]);
38
+ /** The machine code is the message up to the first ':' or space. */
39
+ function codeOf(err) {
40
+ const msg = err instanceof Error ? err.message : String(err);
41
+ return msg.split(/[:\s]/)[0] ?? 'INTERNAL';
42
+ }
43
+ function statusForCode(code) {
44
+ if (INPUT_FAULTS.has(code))
45
+ return 400;
46
+ if (OPERATOR_CONFLICTS.has(code))
47
+ return 409;
48
+ // Any OTHER integrity-prefixed code (e.g. a store backstop like WITNESS_STORE_LOG_MISMATCH,
49
+ // or a future WITNESS_*/COSIG_* fault) is operator-attributable — surface it LOUD as a 409,
50
+ // never let it fall through to 500 where the client would misread it as transport/liveness.
51
+ if (code.startsWith('WITNESS_') || code.startsWith('COSIG_'))
52
+ return 409;
53
+ return 500;
54
+ }
55
+ /**
56
+ * Routes:
57
+ * POST /cosign { sth, consistencyProof? } → 200 WitnessCosignature
58
+ * · 400 input fault · 409 operator conflict
59
+ * · 400 PROOF_TOO_LARGE
60
+ * GET /head → 200 { head: WitnessHead | null }
61
+ */
62
+ export function createWitnessHttpHandler(witness) {
63
+ return async (req) => {
64
+ const { method, path } = req;
65
+ if (method === 'POST' && path === '/cosign') {
66
+ const body = (req.body ?? {});
67
+ if (!body.sth) {
68
+ return { status: 400, body: { error: 'MISSING_STH' } };
69
+ }
70
+ if (body.consistencyProof && Array.isArray(body.consistencyProof.proof)
71
+ && body.consistencyProof.proof.length > MAX_PROOF_NODES) {
72
+ return { status: 400, body: { error: 'PROOF_TOO_LARGE', max: MAX_PROOF_NODES } };
73
+ }
74
+ try {
75
+ const cosignature = await witness.cosign(body.sth, body.consistencyProof);
76
+ return { status: 200, body: cosignature };
77
+ }
78
+ catch (err) {
79
+ const code = codeOf(err);
80
+ const status = statusForCode(code);
81
+ // A 500 is an unexpected internal fault — surface a generic body, never a hang.
82
+ return { status, body: { error: status === 500 ? 'INTERNAL' : code } };
83
+ }
84
+ }
85
+ if (method === 'GET' && path === '/head') {
86
+ try {
87
+ const head = await witness.head();
88
+ return { status: 200, body: { head: head ?? null } };
89
+ }
90
+ catch {
91
+ // Keep the "no thrown error escapes the handler" invariant uniform across routes —
92
+ // a store/DB fault in head() becomes a written 500, not an unhandled rejection for
93
+ // an in-process caller composing the pure handler without the node:http binding.
94
+ return { status: 500, body: { error: 'INTERNAL' } };
95
+ }
96
+ }
97
+ if (method === 'GET' && path === '/sth') {
98
+ // The gossip read a WitnessMonitor pulls. `?size=` selects a specific endorsed
99
+ // size (for cross-witness same-size comparison); omitted ⇒ the latest endorsed STH.
100
+ const raw = req.query?.['size'];
101
+ let treeSize;
102
+ if (raw !== undefined && raw !== '') {
103
+ treeSize = Number(raw);
104
+ if (!Number.isInteger(treeSize) || treeSize < 0) {
105
+ return { status: 400, body: { error: 'BAD_SIZE' } };
106
+ }
107
+ }
108
+ try {
109
+ const sth = await witness.signedHead(treeSize);
110
+ return { status: 200, body: { sth: sth ?? null } };
111
+ }
112
+ catch {
113
+ return { status: 500, body: { error: 'INTERNAL' } };
114
+ }
115
+ }
116
+ if (method === 'GET' && path === '/cosig') {
117
+ // The relying-party POLLINATION read: this witness's endorsement (STH + its own
118
+ // co-signature) at `?size=`. A victim pulls this DIRECTLY from each trusted witness
119
+ // to confirm the head it was served — so the operator cannot withhold/cherry-pick.
120
+ const raw = req.query?.['size'];
121
+ let treeSize;
122
+ if (raw !== undefined && raw !== '') {
123
+ treeSize = Number(raw);
124
+ if (!Number.isInteger(treeSize) || treeSize < 0) {
125
+ return { status: 400, body: { error: 'BAD_SIZE' } };
126
+ }
127
+ }
128
+ try {
129
+ const e = await witness.endorsement(treeSize);
130
+ return { status: 200, body: { sth: e?.sth ?? null, cosignature: e?.cosignature ?? null } };
131
+ }
132
+ catch {
133
+ return { status: 500, body: { error: 'INTERNAL' } };
134
+ }
135
+ }
136
+ return { status: 404, body: { error: 'NOT_FOUND' } };
137
+ };
138
+ }
139
+ /** Bind the pure handler to node:http, enforcing the request-body byte cap. */
140
+ export function createWitnessServer(witness) {
141
+ const handle = createWitnessHttpHandler(witness);
142
+ return nodeCreateServer((req, res) => {
143
+ // A public-facing endpoint an operator does not control: a client that resets the
144
+ // connection mid-body makes the request stream emit 'error', which Node re-throws as
145
+ // an uncaught exception (crashing the witness) if unhandled. Swallow it on both streams.
146
+ req.on('error', () => { });
147
+ res.on('error', () => { });
148
+ const chunks = [];
149
+ let total = 0;
150
+ let aborted = false;
151
+ const fail = (status, error) => {
152
+ // Send the response and close the connection; the `aborted` flag stops further
153
+ // buffering. We do NOT req.destroy() here — destroying the socket before the body
154
+ // flushes can RST the connection and lose the response (e.g. the 413).
155
+ aborted = true;
156
+ res.writeHead(status, { 'Content-Type': 'application/json', 'Connection': 'close' });
157
+ res.end(JSON.stringify({ error }));
158
+ };
159
+ req.on('data', (c) => {
160
+ if (aborted)
161
+ return;
162
+ total += c.length;
163
+ if (total > MAX_BODY_BYTES) {
164
+ fail(413, 'PAYLOAD_TOO_LARGE');
165
+ return;
166
+ }
167
+ chunks.push(c);
168
+ });
169
+ req.on('end', () => {
170
+ if (aborted)
171
+ return;
172
+ const fail500 = () => {
173
+ try {
174
+ res.writeHead(500, { 'Content-Type': 'application/json' });
175
+ res.end(JSON.stringify({ error: 'INTERNAL' }));
176
+ }
177
+ catch { /* already responded */ }
178
+ };
179
+ void (async () => {
180
+ const raw = Buffer.concat(chunks).toString('utf8');
181
+ let body;
182
+ try {
183
+ body = raw ? JSON.parse(raw) : undefined;
184
+ }
185
+ catch {
186
+ res.writeHead(400, { 'Content-Type': 'application/json' });
187
+ res.end(JSON.stringify({ error: 'INVALID_JSON' }));
188
+ return;
189
+ }
190
+ // Parse path + query WITHOUT new URL(): a raw target like `//` makes new URL() THROW
191
+ // ('Invalid URL'), which — before the dispatch try — would escape this voided async
192
+ // IIFE as an unhandled rejection, hanging the socket (and crashing under the default
193
+ // --unhandled-rejections=throw). split + URLSearchParams never throw on a bad target.
194
+ const [rawPath, qs] = (req.url ?? '/').split('?');
195
+ const query = {};
196
+ for (const [k, v] of new URLSearchParams(qs ?? ''))
197
+ query[k] = v;
198
+ try {
199
+ const result = await handle({ method: req.method ?? 'GET', path: rawPath || '/', query, body });
200
+ res.writeHead(result.status, { 'Content-Type': 'application/json' });
201
+ res.end(JSON.stringify(result.body));
202
+ }
203
+ catch {
204
+ // The pure handler maps every expected rejection itself; anything escaping
205
+ // here is a genuine internal fault — still write a body, never hang.
206
+ fail500();
207
+ }
208
+ })().catch(fail500); // belt-and-suspenders: no throw can leave the socket hung
209
+ });
210
+ });
211
+ }
212
+ //# sourceMappingURL=witness-http.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"witness-http.js","sourceRoot":"","sources":["../../src/adapters/witness-http.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,101 @@
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 { type ConsistencyProof, type EndorsedHead, type Identity, type SignedTreeHead, type WitnessCosignature, type WitnessHead, type WitnessStateStore } from '@kyaki/core';
28
+ export declare class DurableWitness {
29
+ private readonly keys;
30
+ private readonly logId;
31
+ private readonly store;
32
+ private readonly mutex;
33
+ private constructor();
34
+ /**
35
+ * Build a witness pinned to one operator log. The durable store is the SOLE
36
+ * authority for the witness's last head — it is read under lock on every cosign,
37
+ * so there is no in-memory baseline to go stale. `create()` is the only
38
+ * constructor so a caller cannot bypass the store with a hand-seeded head.
39
+ */
40
+ static create(keys: Identity, logId: string, store: WitnessStateStore): Promise<DurableWitness>;
41
+ get id(): string;
42
+ /** The witness's durable last-co-signed head. The operator builds its next
43
+ * consistency proof from THIS (reading storage, never a cache) so an honest
44
+ * extension is never spuriously rejected for a size mismatch. */
45
+ head(): Promise<WitnessHead | undefined>;
46
+ /**
47
+ * Co-sign `sth` iff it is an append-only extension of the witness's durable head.
48
+ * Throws a named WITNESS_* error on rejection (the caller maps it to a status):
49
+ * - WITNESS_WRONG_LOG — sth is for a log this witness does not watch;
50
+ * - WITNESS_STH_SIGNATURE_INVALID / _CONSISTENCY_REQUIRED / _SIZE_MISMATCH
51
+ * — input faults (the proof is missing/wrong shape);
52
+ * - WITNESS_NON_MONOTONIC / _EQUIVOCATION / _CONSISTENCY_INVALID
53
+ * — operator-attributable conflicts (a fork attempt).
54
+ */
55
+ cosign(sth: SignedTreeHead, consistencyProof?: ConsistencyProof, now?: Date): Promise<WitnessCosignature>;
56
+ /** The operator-signed STH this witness endorsed at `treeSize` (its latest endorsed
57
+ * STH if omitted), read straight from the durable store — the gossip source a
58
+ * WitnessMonitor pulls to detect cross-witness equivocation. */
59
+ signedHead(treeSize?: number): Promise<SignedTreeHead | undefined>;
60
+ /**
61
+ * This witness's OWN endorsement at `treeSize` (its latest if omitted): the
62
+ * operator-signed STH it endorsed AND its co-signature over it. The co-signature is
63
+ * re-derived deterministically from the retained STH — its body is a pure function of
64
+ * (witnessId, logId, treeSize, rootHash), so this needs no extra stored state and is
65
+ * byte-identical to the one emitted at cosign time.
66
+ *
67
+ * This is what relying-party POLLINATION pulls DIRECTLY from each trusted witness: the
68
+ * victim asks the witnesses themselves "did you co-sign this exact root at this size?",
69
+ * so the operator cannot withhold or cherry-pick which witnesses endorsed which root,
70
+ * and the witness-signed co-signature cannot be forged by a man-in-the-middle.
71
+ * undefined if this witness retained no endorsement at that size.
72
+ */
73
+ endorsement(treeSize?: number): Promise<{
74
+ sth: SignedTreeHead;
75
+ cosignature: WitnessCosignature;
76
+ } | undefined>;
77
+ }
78
+ /**
79
+ * An in-memory `WitnessStateStore` for single-process tests and demos. It serializes
80
+ * `cosign` via a promise chain (mirroring the durable per-log lock) and applies the
81
+ * same monotonic + equivocation backstop the durable store does. It is NOT durable —
82
+ * it forgets on restart, so the cross-restart guarantee requires a durable store
83
+ * (`@kyaki/postgres` `PgWitnessStore`). Use it to exercise the witness LOGIC.
84
+ */
85
+ export declare class MemoryWitnessStore implements WitnessStateStore {
86
+ private current;
87
+ private cosignature;
88
+ /** Append-only operator-signed STHs keyed by tree size (FIRST-write-wins, mirroring
89
+ * the durable kya_endorsed_sth ON CONFLICT DO NOTHING). Retained so a past size
90
+ * stays serveable for gossip after the witness advances beyond it. */
91
+ private readonly endorsed;
92
+ private chain;
93
+ load(): Promise<WitnessHead | undefined>;
94
+ /** The co-signature co-committed with the current head (for parity with the
95
+ * durable store, which persists both in one transaction). */
96
+ latestCosignature(): Promise<WitnessCosignature | undefined>;
97
+ /** The endorsed operator STH at `treeSize` (latest if omitted). undefined if none. */
98
+ signedHead(treeSize?: number): Promise<SignedTreeHead | undefined>;
99
+ cosign(decide: (prior: WitnessHead | undefined) => EndorsedHead): Promise<WitnessCosignature>;
100
+ }
101
+ //# sourceMappingURL=durable-witness.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"durable-witness.d.ts","sourceRoot":"","sources":["../src/durable-witness.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,EAEL,KAAK,gBAAgB,EAAE,KAAK,YAAY,EAAE,KAAK,QAAQ,EAAE,KAAK,cAAc,EAC5E,KAAK,kBAAkB,EAAE,KAAK,WAAW,EAAE,KAAK,iBAAiB,EAClE,MAAM,aAAa,CAAC;AAErB,qBAAa,cAAc;IAIvB,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,KAAK;IALxB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAoB;IAE1C,OAAO;IAMP;;;;;OAKG;WACU,MAAM,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,cAAc,CAAC;IAIrG,IAAI,EAAE,IAAI,MAAM,CAEf;IAED;;sEAEkE;IAC5D,IAAI,IAAI,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC;IAI9C;;;;;;;;OAQG;IACG,MAAM,CAAC,GAAG,EAAE,cAAc,EAAE,gBAAgB,CAAC,EAAE,gBAAgB,EAAE,GAAG,CAAC,EAAE,IAAI,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAwB/G;;qEAEiE;IAC3D,UAAU,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC;IAIxE;;;;;;;;;;;;OAYG;IACG,WAAW,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,GAAG,EAAE,cAAc,CAAC;QAAC,WAAW,EAAE,kBAAkB,CAAA;KAAE,GAAG,SAAS,CAAC;CAKpH;AAED;;;;;;GAMG;AACH,qBAAa,kBAAmB,YAAW,iBAAiB;IAC1D,OAAO,CAAC,OAAO,CAA0B;IACzC,OAAO,CAAC,WAAW,CAAiC;IACpD;;2EAEuE;IACvE,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAqC;IAC9D,OAAO,CAAC,KAAK,CAAuC;IAE9C,IAAI,IAAI,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC;IAI9C;kEAC8D;IACxD,iBAAiB,IAAI,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC;IAIlE,sFAAsF;IAChF,UAAU,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC;IAOxE,MAAM,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,WAAW,GAAG,SAAS,KAAK,YAAY,GAAG,OAAO,CAAC,kBAAkB,CAAC;CAiC9F"}