@zanii/witness 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -39,6 +39,7 @@ co-signing the root binds the witness to that exact history.
39
39
 
40
40
  ## Changelog
41
41
 
42
+ - **0.1.3** — k-of-n quorum policy, checkpoints as Ed25519 signed notes, equivocation evidence, regulator-scoped witnesses, batch co-signing across logs, time-token binding, uptime, lag dashboard, directory health checks.
42
43
  - **0.1.2** — now depends on `@zanii/core` 0.5 (protocol 1.2), so it shares one core with `@zanii/sdk` 0.5 instead of pulling in a second, older copy. No API change.
43
44
  - **0.1.0** — initial release: `createWitness`, `verifyCosignature`, `WitnessCosignature`.
44
45
 
package/dist/index.d.ts CHANGED
@@ -27,4 +27,5 @@ export declare function createWitness(keypair: {
27
27
  * Both signatures must be valid and both must commit to the same `(size, root)`.
28
28
  */
29
29
  export declare function verifyCosignature(c: WitnessCosignature, origin: SignedTreeHead): boolean;
30
+ export * from './network.js';
30
31
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAaA,OAAO,EAA2C,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAI3F,MAAM,WAAW,kBAAkB;IACjC,CAAC,EAAE,CAAC,CAAC;IACL,2DAA2D;IAC3D,gBAAgB,EAAE,MAAM,CAAC;IACzB,qFAAqF;IACrF,WAAW,EAAE,cAAc,CAAC;CAC7B;AAED,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,MAAM,CAAC,MAAM,EAAE,cAAc,EAAE,IAAI,CAAC,EAAE;QAAE,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,GAAG,kBAAkB,CAAC;CAC5F;AAED,8EAA8E;AAC9E,wBAAgB,aAAa,CAAC,OAAO,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,UAAU,CAAA;CAAE,GAAG,OAAO,CA+BvF;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,kBAAkB,EAAE,MAAM,EAAE,cAAc,GAAG,OAAO,CASxF"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAaA,OAAO,EAA2C,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAI3F,MAAM,WAAW,kBAAkB;IACjC,CAAC,EAAE,CAAC,CAAC;IACL,2DAA2D;IAC3D,gBAAgB,EAAE,MAAM,CAAC;IACzB,qFAAqF;IACrF,WAAW,EAAE,cAAc,CAAC;CAC7B;AAED,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,MAAM,CAAC,MAAM,EAAE,cAAc,EAAE,IAAI,CAAC,EAAE;QAAE,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,GAAG,kBAAkB,CAAC;CAC5F;AAED,8EAA8E;AAC9E,wBAAgB,aAAa,CAAC,OAAO,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,UAAU,CAAA;CAAE,GAAG,OAAO,CA+BvF;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,kBAAkB,EAAE,MAAM,EAAE,cAAc,GAAG,OAAO,CASxF;AACD,cAAc,cAAc,CAAC"}
package/dist/index.js CHANGED
@@ -57,4 +57,5 @@ export function verifyCosignature(c, origin) {
57
57
  c.cosignature.size === origin.size &&
58
58
  c.cosignature.root === origin.root);
59
59
  }
60
+ export * from './network.js';
60
61
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,128 @@
1
+ import { type SignedTreeHead } from '@zanii/core';
2
+ import { type Witness, type WitnessCosignature } from './index.js';
3
+ type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
4
+ /** A verifier's rule: accept a tree head only when at least `k` of these witnesses co-signed it. */
5
+ export declare function verifyQuorum(origin: SignedTreeHead, cosigs: WitnessCosignature[], policy: {
6
+ trusted: string[];
7
+ k: number;
8
+ }): {
9
+ ok: boolean;
10
+ witnesses: string[];
11
+ needed: number;
12
+ };
13
+ /** The checkpoint body: origin line, tree size, base64 root hash, each ending in a newline. */
14
+ export declare function checkpointBody(sth: SignedTreeHead, origin: string): string;
15
+ /** Key ID for an Ed25519 note key: the first 4 bytes of SHA-256(name, newline, 0x01, public key). */
16
+ export declare function noteKeyId(name: string, publicKey: Uint8Array): Buffer;
17
+ /** A signature line for a note: `— <name> <base64(key id || signature)>`. */
18
+ export declare function signNote(body: string, name: string, privateKey: Uint8Array): string;
19
+ /** The whole signed checkpoint: body, a blank line, then one signature line per signer. */
20
+ export declare function signedCheckpoint(sth: SignedTreeHead, origin: string, signers: Array<{
21
+ name: string;
22
+ privateKey: Uint8Array;
23
+ }>): string;
24
+ /** Names whose signature lines verify against the keys you trust. */
25
+ export declare function verifyCheckpoint(note: string, keys: Array<{
26
+ name: string;
27
+ publicKey: Uint8Array;
28
+ }>): {
29
+ body: null;
30
+ signers: string[];
31
+ } | {
32
+ body: string;
33
+ signers: string[];
34
+ };
35
+ /**
36
+ * Evidence that a witness signed two different roots at the same size for the same log:
37
+ * the basis for slashing a bonded witness. Returns null when the two do not conflict.
38
+ */
39
+ export declare function equivocationEvidence(a: WitnessCosignature, b: WitnessCosignature): {
40
+ type: string;
41
+ witness: string;
42
+ log: string;
43
+ size: number;
44
+ roots: string[];
45
+ evidence: WitnessCosignature[];
46
+ } | null;
47
+ /** A witness that only co-signs the logs it supervises, and keeps every co-signature it issued. */
48
+ export declare function regulatorWitness(keypair: {
49
+ did: string;
50
+ privateKey: Uint8Array;
51
+ }, supervisedLogs: string[]): {
52
+ did: string;
53
+ cosign(origin: SignedTreeHead, opts?: {
54
+ consistencyProof?: string[];
55
+ }): WitnessCosignature;
56
+ issued: () => WitnessCosignature[];
57
+ };
58
+ /** Co-sign checkpoints from several logs in one pass; failures are reported per item, never thrown. */
59
+ export declare function cosignMany(witness: Witness, items: Array<{
60
+ origin: SignedTreeHead;
61
+ consistencyProof?: string[];
62
+ }>): ({
63
+ log: string;
64
+ ok: true;
65
+ cosignature: WitnessCosignature;
66
+ error?: undefined;
67
+ } | {
68
+ log: string;
69
+ ok: false;
70
+ error: string;
71
+ cosignature?: undefined;
72
+ })[];
73
+ /**
74
+ * Bind a co-signature to an external time token (RFC 3161 TSA or Roughtime response, by hash):
75
+ * the trust root for "seen by time T" is that time source.
76
+ */
77
+ export declare function timeBoundCosignature(c: WitnessCosignature, token: {
78
+ source: string;
79
+ ts: string;
80
+ tokenHash: string;
81
+ }): {
82
+ time: {
83
+ source: string;
84
+ ts: string;
85
+ tokenHash: string;
86
+ };
87
+ v: 1;
88
+ witnessed_log_id: string;
89
+ cosignature: SignedTreeHead;
90
+ };
91
+ /** Uptime from co-signature times: share of expected slots covered and the longest silence. */
92
+ export declare function witnessUptime(times: string[], opts: {
93
+ from: string;
94
+ to: string;
95
+ everyMs: number;
96
+ }): {
97
+ slots: number;
98
+ covered: number;
99
+ uptimePermille: number;
100
+ maxGapMs: number;
101
+ };
102
+ /** For each log, the newest size each witness co-signed and how far it lags the newest co-signed size. */
103
+ export declare function witnessDashboard(cosigs: WitnessCosignature[]): {
104
+ log: string;
105
+ head: number;
106
+ witnesses: {
107
+ witness: string;
108
+ size: number;
109
+ lag: number;
110
+ }[];
111
+ }[];
112
+ /** Health of listed witnesses: GET `<url>/health` with a timeout; latency in ms when up. */
113
+ export declare function witnessHealth(directory: Array<{
114
+ did: string;
115
+ url: string;
116
+ operator?: string;
117
+ }>, opts?: {
118
+ fetchImpl?: FetchLike;
119
+ timeoutMs?: number;
120
+ }): Promise<{
121
+ up: boolean;
122
+ latencyMs: number | null;
123
+ did: string;
124
+ url: string;
125
+ operator?: string;
126
+ }[]>;
127
+ export {};
128
+ //# sourceMappingURL=network.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"network.d.ts","sourceRoot":"","sources":["../src/network.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Witnesses as a network: quorum policies for verifiers, checkpoints as signed notes
3
+ * (the text format transparency-log tooling uses), equivocation evidence for slashing,
4
+ * scoped regulator witnesses, batch co-signing across logs, time tokens, uptime and a
5
+ * dashboard, and a directory with health checks.
6
+ */
7
+ import { createHash } from 'node:crypto';
8
+ import { ed25519 } from '@noble/curves/ed25519';
9
+ import { hexToBytes } from '@noble/hashes/utils';
10
+ import { verifySTH } from '@zanii/core';
11
+ import { createWitness, verifyCosignature } from './index.js';
12
+ // ------------------------------------------------------------------------------ //
13
+ // Verifier policy
14
+ // ------------------------------------------------------------------------------ //
15
+ /** A verifier's rule: accept a tree head only when at least `k` of these witnesses co-signed it. */
16
+ export function verifyQuorum(origin, cosigs, policy) {
17
+ const by = new Set(cosigs.filter((c) => policy.trusted.includes(c.cosignature.log_id) && verifyCosignature(c, origin)).map((c) => c.cosignature.log_id));
18
+ return { ok: by.size >= policy.k, witnesses: [...by].sort(), needed: policy.k };
19
+ }
20
+ // ------------------------------------------------------------------------------ //
21
+ // Signed-note checkpoints
22
+ // ------------------------------------------------------------------------------ //
23
+ /** The checkpoint body: origin line, tree size, base64 root hash, each ending in a newline. */
24
+ export function checkpointBody(sth, origin) {
25
+ if (!origin || /\n/.test(origin))
26
+ throw new Error('witness: origin must be one non-empty line');
27
+ return `${origin}\n${sth.size}\n${Buffer.from(hexToBytes(sth.root.replace('sha256:', ''))).toString('base64')}\n`;
28
+ }
29
+ /** Key ID for an Ed25519 note key: the first 4 bytes of SHA-256(name, newline, 0x01, public key). */
30
+ export function noteKeyId(name, publicKey) {
31
+ return createHash('sha256').update(Buffer.concat([Buffer.from(`${name}\n`), Buffer.from([0x01]), Buffer.from(publicKey)])).digest().subarray(0, 4);
32
+ }
33
+ /** A signature line for a note: `— <name> <base64(key id || signature)>`. */
34
+ export function signNote(body, name, privateKey) {
35
+ const sig = ed25519.sign(new Uint8Array(Buffer.from(body)), privateKey);
36
+ return `— ${name} ${Buffer.concat([noteKeyId(name, ed25519.getPublicKey(privateKey)), Buffer.from(sig)]).toString('base64')}\n`;
37
+ }
38
+ /** The whole signed checkpoint: body, a blank line, then one signature line per signer. */
39
+ export function signedCheckpoint(sth, origin, signers) {
40
+ const body = checkpointBody(sth, origin);
41
+ return `${body}\n${signers.map((s) => signNote(body, s.name, s.privateKey)).join('')}`;
42
+ }
43
+ /** Names whose signature lines verify against the keys you trust. */
44
+ export function verifyCheckpoint(note, keys) {
45
+ const split = note.indexOf('\n\n');
46
+ if (split < 0)
47
+ return { body: null, signers: [] };
48
+ const body = note.slice(0, split + 1);
49
+ const signers = new Set();
50
+ for (const l of note.slice(split + 2).split('\n').filter(Boolean)) {
51
+ const m = /^— (\S+) (\S+)$/.exec(l);
52
+ if (!m)
53
+ continue;
54
+ const raw = Buffer.from(m[2], 'base64');
55
+ const k = keys.find((x) => x.name === m[1] && noteKeyId(x.name, x.publicKey).equals(raw.subarray(0, 4)));
56
+ try {
57
+ if (k && raw.length === 68 && ed25519.verify(new Uint8Array(raw.subarray(4)), new Uint8Array(Buffer.from(body)), k.publicKey))
58
+ signers.add(k.name);
59
+ }
60
+ catch {
61
+ // a malformed line is simply not counted
62
+ }
63
+ }
64
+ return { body, signers: [...signers].sort() };
65
+ }
66
+ // ------------------------------------------------------------------------------ //
67
+ // Accountability
68
+ // ------------------------------------------------------------------------------ //
69
+ /**
70
+ * Evidence that a witness signed two different roots at the same size for the same log:
71
+ * the basis for slashing a bonded witness. Returns null when the two do not conflict.
72
+ */
73
+ export function equivocationEvidence(a, b) {
74
+ const conflict = a.cosignature.log_id === b.cosignature.log_id && a.witnessed_log_id === b.witnessed_log_id && a.cosignature.size === b.cosignature.size && a.cosignature.root !== b.cosignature.root && verifySTH(a.cosignature) && verifySTH(b.cosignature);
75
+ return conflict ? { type: 'witness.equivocation', witness: a.cosignature.log_id, log: a.witnessed_log_id, size: a.cosignature.size, roots: [a.cosignature.root, b.cosignature.root].sort(), evidence: [a, b] } : null;
76
+ }
77
+ /** A witness that only co-signs the logs it supervises, and keeps every co-signature it issued. */
78
+ export function regulatorWitness(keypair, supervisedLogs) {
79
+ const inner = createWitness(keypair);
80
+ const issued = [];
81
+ return {
82
+ did: inner.did,
83
+ cosign(origin, opts) {
84
+ if (!supervisedLogs.includes(origin.log_id))
85
+ throw new Error(`witness: ${origin.log_id} is not a supervised log`);
86
+ const c = inner.cosign(origin, opts);
87
+ issued.push(c);
88
+ return c;
89
+ },
90
+ issued: () => [...issued],
91
+ };
92
+ }
93
+ /** Co-sign checkpoints from several logs in one pass; failures are reported per item, never thrown. */
94
+ export function cosignMany(witness, items) {
95
+ return items.map((i) => {
96
+ try {
97
+ return { log: i.origin.log_id, ok: true, cosignature: witness.cosign(i.origin, { consistencyProof: i.consistencyProof }) };
98
+ }
99
+ catch (err) {
100
+ return { log: i.origin.log_id, ok: false, error: err.message };
101
+ }
102
+ });
103
+ }
104
+ /**
105
+ * Bind a co-signature to an external time token (RFC 3161 TSA or Roughtime response, by hash):
106
+ * the trust root for "seen by time T" is that time source.
107
+ */
108
+ export function timeBoundCosignature(c, token) {
109
+ if (token.ts < c.cosignature.ts)
110
+ throw new Error('witness: the time token predates the checkpoint');
111
+ return { ...c, time: token };
112
+ }
113
+ // ------------------------------------------------------------------------------ //
114
+ // Operations
115
+ // ------------------------------------------------------------------------------ //
116
+ /** Uptime from co-signature times: share of expected slots covered and the longest silence. */
117
+ export function witnessUptime(times, opts) {
118
+ const from = Date.parse(opts.from);
119
+ const to = Date.parse(opts.to);
120
+ const slots = Math.max(1, Math.ceil((to - from) / opts.everyMs));
121
+ const covered = new Set(times.map((t) => Math.floor((Date.parse(t) - from) / opts.everyMs)).filter((i) => i >= 0 && i < slots));
122
+ const sorted = [from, ...times.map(Date.parse).filter((t) => t >= from && t <= to).sort((a, b) => a - b), to];
123
+ const maxGapMs = Math.max(...sorted.slice(1).map((t, i) => t - sorted[i]));
124
+ return { slots, covered: covered.size, uptimePermille: Math.round((covered.size / slots) * 1000), maxGapMs };
125
+ }
126
+ /** For each log, the newest size each witness co-signed and how far it lags the newest co-signed size. */
127
+ export function witnessDashboard(cosigs) {
128
+ const logs = new Map();
129
+ for (const c of cosigs) {
130
+ const m = logs.get(c.witnessed_log_id) ?? new Map();
131
+ m.set(c.cosignature.log_id, Math.max(m.get(c.cosignature.log_id) ?? 0, c.cosignature.size));
132
+ logs.set(c.witnessed_log_id, m);
133
+ }
134
+ return [...logs].map(([log, m]) => {
135
+ const head = Math.max(...m.values());
136
+ return { log, head, witnesses: [...m].map(([w, size]) => ({ witness: w, size, lag: head - size })).sort((a, b) => a.lag - b.lag) };
137
+ });
138
+ }
139
+ /** Health of listed witnesses: GET `<url>/health` with a timeout; latency in ms when up. */
140
+ export async function witnessHealth(directory, opts = {}) {
141
+ const f = opts.fetchImpl ?? ((u, i) => fetch(u, i));
142
+ return Promise.all(directory.map(async (w) => {
143
+ const t = performance.now();
144
+ try {
145
+ const res = await f(`${w.url.replace(/\/$/, '')}/health`, { signal: AbortSignal.timeout(opts.timeoutMs ?? 5000) });
146
+ return { ...w, up: res.ok, latencyMs: res.ok ? Math.round(performance.now() - t) : null };
147
+ }
148
+ catch {
149
+ return { ...w, up: false, latencyMs: null };
150
+ }
151
+ }));
152
+ }
153
+ //# sourceMappingURL=network.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"network.js","sourceRoot":"","sources":["../src/network.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zanii/witness",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "An independent co-signer for a Zanii transparency log. Verifies a checkpoint is append-only, then counter-signs it so others can trust that tree state without running their own monitor.",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://ledger.zanii.agency",
@@ -29,12 +29,13 @@
29
29
  "@types/node": "^26.1.0",
30
30
  "typescript": "^5.8.2",
31
31
  "vitest": "^3.0.9",
32
- "@zanii/sdk": "0.5.2",
33
- "@zanii/testing": "0.1.2"
32
+ "@zanii/sdk": "0.5.3",
33
+ "@zanii/testing": "0.1.3"
34
34
  },
35
35
  "dependencies": {
36
36
  "@noble/hashes": "^1.7.1",
37
- "@zanii/core": "^0.5.2"
37
+ "@noble/curves": "^1.8.1",
38
+ "@zanii/core": "^0.5.4"
38
39
  },
39
40
  "scripts": {
40
41
  "build": "tsc -p tsconfig.build.json",
package/src/index.ts CHANGED
@@ -81,3 +81,4 @@ export function verifyCosignature(c: WitnessCosignature, origin: SignedTreeHead)
81
81
  c.cosignature.root === origin.root
82
82
  );
83
83
  }
84
+ export * from './network.js';
package/src/network.ts ADDED
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Witnesses as a network: quorum policies for verifiers, checkpoints as signed notes
3
+ * (the text format transparency-log tooling uses), equivocation evidence for slashing,
4
+ * scoped regulator witnesses, batch co-signing across logs, time tokens, uptime and a
5
+ * dashboard, and a directory with health checks.
6
+ */
7
+ import { createHash } from 'node:crypto';
8
+ import { ed25519 } from '@noble/curves/ed25519';
9
+ import { hexToBytes } from '@noble/hashes/utils';
10
+ import { verifySTH, type SignedTreeHead } from '@zanii/core';
11
+ import { createWitness, verifyCosignature, type Witness, type WitnessCosignature } from './index.js';
12
+
13
+ type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;
14
+
15
+ // ------------------------------------------------------------------------------ //
16
+ // Verifier policy
17
+ // ------------------------------------------------------------------------------ //
18
+
19
+ /** A verifier's rule: accept a tree head only when at least `k` of these witnesses co-signed it. */
20
+ export function verifyQuorum(origin: SignedTreeHead, cosigs: WitnessCosignature[], policy: { trusted: string[]; k: number }) {
21
+ const by = new Set(cosigs.filter((c) => policy.trusted.includes(c.cosignature.log_id) && verifyCosignature(c, origin)).map((c) => c.cosignature.log_id));
22
+ return { ok: by.size >= policy.k, witnesses: [...by].sort(), needed: policy.k };
23
+ }
24
+
25
+ // ------------------------------------------------------------------------------ //
26
+ // Signed-note checkpoints
27
+ // ------------------------------------------------------------------------------ //
28
+
29
+ /** The checkpoint body: origin line, tree size, base64 root hash, each ending in a newline. */
30
+ export function checkpointBody(sth: SignedTreeHead, origin: string): string {
31
+ if (!origin || /\n/.test(origin)) throw new Error('witness: origin must be one non-empty line');
32
+ return `${origin}\n${sth.size}\n${Buffer.from(hexToBytes(sth.root.replace('sha256:', ''))).toString('base64')}\n`;
33
+ }
34
+
35
+ /** Key ID for an Ed25519 note key: the first 4 bytes of SHA-256(name, newline, 0x01, public key). */
36
+ export function noteKeyId(name: string, publicKey: Uint8Array): Buffer {
37
+ return createHash('sha256').update(Buffer.concat([Buffer.from(`${name}\n`), Buffer.from([0x01]), Buffer.from(publicKey)])).digest().subarray(0, 4);
38
+ }
39
+
40
+ /** A signature line for a note: `— <name> <base64(key id || signature)>`. */
41
+ export function signNote(body: string, name: string, privateKey: Uint8Array): string {
42
+ const sig = ed25519.sign(new Uint8Array(Buffer.from(body)), privateKey);
43
+ return `— ${name} ${Buffer.concat([noteKeyId(name, ed25519.getPublicKey(privateKey)), Buffer.from(sig)]).toString('base64')}\n`;
44
+ }
45
+
46
+ /** The whole signed checkpoint: body, a blank line, then one signature line per signer. */
47
+ export function signedCheckpoint(sth: SignedTreeHead, origin: string, signers: Array<{ name: string; privateKey: Uint8Array }>): string {
48
+ const body = checkpointBody(sth, origin);
49
+ return `${body}\n${signers.map((s) => signNote(body, s.name, s.privateKey)).join('')}`;
50
+ }
51
+
52
+ /** Names whose signature lines verify against the keys you trust. */
53
+ export function verifyCheckpoint(note: string, keys: Array<{ name: string; publicKey: Uint8Array }>) {
54
+ const split = note.indexOf('\n\n');
55
+ if (split < 0) return { body: null, signers: [] as string[] };
56
+ const body = note.slice(0, split + 1);
57
+ const signers = new Set<string>();
58
+ for (const l of note.slice(split + 2).split('\n').filter(Boolean)) {
59
+ const m = /^— (\S+) (\S+)$/.exec(l);
60
+ if (!m) continue;
61
+ const raw = Buffer.from(m[2]!, 'base64');
62
+ const k = keys.find((x) => x.name === m[1] && noteKeyId(x.name, x.publicKey).equals(raw.subarray(0, 4)));
63
+ try {
64
+ if (k && raw.length === 68 && ed25519.verify(new Uint8Array(raw.subarray(4)), new Uint8Array(Buffer.from(body)), k.publicKey)) signers.add(k.name);
65
+ } catch {
66
+ // a malformed line is simply not counted
67
+ }
68
+ }
69
+ return { body, signers: [...signers].sort() };
70
+ }
71
+
72
+ // ------------------------------------------------------------------------------ //
73
+ // Accountability
74
+ // ------------------------------------------------------------------------------ //
75
+
76
+ /**
77
+ * Evidence that a witness signed two different roots at the same size for the same log:
78
+ * the basis for slashing a bonded witness. Returns null when the two do not conflict.
79
+ */
80
+ export function equivocationEvidence(a: WitnessCosignature, b: WitnessCosignature) {
81
+ const conflict = a.cosignature.log_id === b.cosignature.log_id && a.witnessed_log_id === b.witnessed_log_id && a.cosignature.size === b.cosignature.size && a.cosignature.root !== b.cosignature.root && verifySTH(a.cosignature) && verifySTH(b.cosignature);
82
+ return conflict ? { type: 'witness.equivocation', witness: a.cosignature.log_id, log: a.witnessed_log_id, size: a.cosignature.size, roots: [a.cosignature.root, b.cosignature.root].sort(), evidence: [a, b] } : null;
83
+ }
84
+
85
+ /** A witness that only co-signs the logs it supervises, and keeps every co-signature it issued. */
86
+ export function regulatorWitness(keypair: { did: string; privateKey: Uint8Array }, supervisedLogs: string[]) {
87
+ const inner = createWitness(keypair);
88
+ const issued: WitnessCosignature[] = [];
89
+ return {
90
+ did: inner.did,
91
+ cosign(origin: SignedTreeHead, opts?: { consistencyProof?: string[] }) {
92
+ if (!supervisedLogs.includes(origin.log_id)) throw new Error(`witness: ${origin.log_id} is not a supervised log`);
93
+ const c = inner.cosign(origin, opts);
94
+ issued.push(c);
95
+ return c;
96
+ },
97
+ issued: () => [...issued],
98
+ };
99
+ }
100
+
101
+ /** Co-sign checkpoints from several logs in one pass; failures are reported per item, never thrown. */
102
+ export function cosignMany(witness: Witness, items: Array<{ origin: SignedTreeHead; consistencyProof?: string[] }>) {
103
+ return items.map((i) => {
104
+ try {
105
+ return { log: i.origin.log_id, ok: true as const, cosignature: witness.cosign(i.origin, { consistencyProof: i.consistencyProof }) };
106
+ } catch (err) {
107
+ return { log: i.origin.log_id, ok: false as const, error: (err as Error).message };
108
+ }
109
+ });
110
+ }
111
+
112
+ /**
113
+ * Bind a co-signature to an external time token (RFC 3161 TSA or Roughtime response, by hash):
114
+ * the trust root for "seen by time T" is that time source.
115
+ */
116
+ export function timeBoundCosignature(c: WitnessCosignature, token: { source: string; ts: string; tokenHash: string }) {
117
+ if (token.ts < c.cosignature.ts) throw new Error('witness: the time token predates the checkpoint');
118
+ return { ...c, time: token };
119
+ }
120
+
121
+ // ------------------------------------------------------------------------------ //
122
+ // Operations
123
+ // ------------------------------------------------------------------------------ //
124
+
125
+ /** Uptime from co-signature times: share of expected slots covered and the longest silence. */
126
+ export function witnessUptime(times: string[], opts: { from: string; to: string; everyMs: number }) {
127
+ const from = Date.parse(opts.from);
128
+ const to = Date.parse(opts.to);
129
+ const slots = Math.max(1, Math.ceil((to - from) / opts.everyMs));
130
+ const covered = new Set(times.map((t) => Math.floor((Date.parse(t) - from) / opts.everyMs)).filter((i) => i >= 0 && i < slots));
131
+ const sorted = [from, ...times.map(Date.parse).filter((t) => t >= from && t <= to).sort((a, b) => a - b), to];
132
+ const maxGapMs = Math.max(...sorted.slice(1).map((t, i) => t - sorted[i]!));
133
+ return { slots, covered: covered.size, uptimePermille: Math.round((covered.size / slots) * 1000), maxGapMs };
134
+ }
135
+
136
+ /** For each log, the newest size each witness co-signed and how far it lags the newest co-signed size. */
137
+ export function witnessDashboard(cosigs: WitnessCosignature[]) {
138
+ const logs = new Map<string, Map<string, number>>();
139
+ for (const c of cosigs) {
140
+ const m = logs.get(c.witnessed_log_id) ?? new Map<string, number>();
141
+ m.set(c.cosignature.log_id, Math.max(m.get(c.cosignature.log_id) ?? 0, c.cosignature.size));
142
+ logs.set(c.witnessed_log_id, m);
143
+ }
144
+ return [...logs].map(([log, m]) => {
145
+ const head = Math.max(...m.values());
146
+ return { log, head, witnesses: [...m].map(([w, size]) => ({ witness: w, size, lag: head - size })).sort((a, b) => a.lag - b.lag) };
147
+ });
148
+ }
149
+
150
+ /** Health of listed witnesses: GET `<url>/health` with a timeout; latency in ms when up. */
151
+ export async function witnessHealth(directory: Array<{ did: string; url: string; operator?: string }>, opts: { fetchImpl?: FetchLike; timeoutMs?: number } = {}) {
152
+ const f = opts.fetchImpl ?? ((u: string, i?: RequestInit) => fetch(u, i));
153
+ return Promise.all(directory.map(async (w) => {
154
+ const t = performance.now();
155
+ try {
156
+ const res = await f(`${w.url.replace(/\/$/, '')}/health`, { signal: AbortSignal.timeout(opts.timeoutMs ?? 5000) });
157
+ return { ...w, up: res.ok, latencyMs: res.ok ? Math.round(performance.now() - t) : null };
158
+ } catch {
159
+ return { ...w, up: false, latencyMs: null };
160
+ }
161
+ }));
162
+ }