@common-agent/reputations 0.0.0-stage → 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.
- package/LICENSE +202 -0
- package/LICENSE-UNICODE +39 -0
- package/NOTICE +9 -0
- package/README.md +38 -2
- package/dist/casefolding-15.1.0.json +1539 -0
- package/dist/heads.d.ts +184 -0
- package/dist/heads.js +358 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +28 -0
- package/dist/ledger.d.ts +92 -0
- package/dist/ledger.js +157 -0
- package/dist/nda-claims.d.ts +141 -0
- package/dist/nda-claims.js +168 -0
- package/dist/nda-record.d.ts +86 -0
- package/dist/nda-record.js +83 -0
- package/dist/pool.d.ts +59 -0
- package/dist/pool.js +77 -0
- package/dist/ports.d.ts +6 -0
- package/dist/ports.js +1 -0
- package/dist/report.d.ts +176 -0
- package/dist/report.js +147 -0
- package/dist/score.d.ts +110 -0
- package/dist/score.js +129 -0
- package/dist/signing.d.ts +150 -0
- package/dist/signing.js +299 -0
- package/package.json +53 -4
package/dist/heads.d.ts
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { type ReportSigner } from "./signing.js";
|
|
2
|
+
export declare const HEADS_TAG = "fdbk-heads-v1";
|
|
3
|
+
export declare const COSIGN_TAG = "fdbk-heads-cosign-v1";
|
|
4
|
+
export declare const WITNESSES_TAG = "fdbk-witnesses-v1";
|
|
5
|
+
/**
|
|
6
|
+
* A subject's canonical caseless form (Unicode D145, at 15.1.0): NFD, full
|
|
7
|
+
* case folding (the C and F entries of CaseFolding.txt), NFD again, as
|
|
8
|
+
* Personal Agent Naming §2 compares handles.
|
|
9
|
+
*/
|
|
10
|
+
export declare function foldSubject(subject: string): string;
|
|
11
|
+
/** The slot key: the SHA-256 of the UTF-8 of the folded subject, in lowercase hex. */
|
|
12
|
+
export declare const subjectKey: (subject: string) => string;
|
|
13
|
+
/** What a slot holds: a subject's head, or the tombstone of an erased subject. */
|
|
14
|
+
export type Slot = {
|
|
15
|
+
key: string;
|
|
16
|
+
subject: string;
|
|
17
|
+
seq: number;
|
|
18
|
+
head_hash: string;
|
|
19
|
+
after_erasure?: string;
|
|
20
|
+
} | {
|
|
21
|
+
key: string;
|
|
22
|
+
erased_at: string;
|
|
23
|
+
};
|
|
24
|
+
export interface Head {
|
|
25
|
+
subject: string;
|
|
26
|
+
seq: number;
|
|
27
|
+
head_hash: string;
|
|
28
|
+
after_erasure?: string;
|
|
29
|
+
}
|
|
30
|
+
export interface Tombstone {
|
|
31
|
+
key: string;
|
|
32
|
+
erased_at: string;
|
|
33
|
+
}
|
|
34
|
+
/** The object a slot's digest is taken over. */
|
|
35
|
+
export declare function slotBody(slot: Slot): Record<string, unknown>;
|
|
36
|
+
/** A filled slot's digest: SHA-256 over 0x00 and the JCS of what it holds. */
|
|
37
|
+
export declare const slotDigest: (slot: Slot) => Buffer;
|
|
38
|
+
/** EMPTY[h]: the digest of an empty subtree of height h. */
|
|
39
|
+
export declare const EMPTY: Buffer[];
|
|
40
|
+
/** The slots of a tree, one per subject. Two entries for one slot are refused. */
|
|
41
|
+
export declare function slotsOf(heads: readonly Head[], tombstones?: readonly Tombstone[]): Map<string, Slot>;
|
|
42
|
+
/**
|
|
43
|
+
* The sparse Merkle tree over a set of slots, built once: the slots sorted by
|
|
44
|
+
* key, each key converted to bytes once, and the digest of every subtree the
|
|
45
|
+
* build visits kept, so the root and any slot's proof are read from the
|
|
46
|
+
* cache. The build splits the sorted slots by one bit per level, so it costs
|
|
47
|
+
* a hash per subtree that holds a slot, and a proof costs one walk of 256
|
|
48
|
+
* levels plus, for an absent key beside a lone slot, one climb from that
|
|
49
|
+
* slot.
|
|
50
|
+
*/
|
|
51
|
+
export declare class SparseTree {
|
|
52
|
+
private readonly keys;
|
|
53
|
+
private readonly slots;
|
|
54
|
+
private readonly cache;
|
|
55
|
+
readonly root: string;
|
|
56
|
+
constructor(slots: Map<string, Slot> | readonly Slot[]);
|
|
57
|
+
/** The first index in [lo, hi) whose key has bit `depth` set. */
|
|
58
|
+
private split;
|
|
59
|
+
/** A lone slot's digest as the root of a subtree at `depth`: its leaf, climbed through empty siblings. */
|
|
60
|
+
private lone;
|
|
61
|
+
/** The digest of the subtree at `depth` holding the slots [lo, hi). */
|
|
62
|
+
private node;
|
|
63
|
+
/** The slot at a key, or null. */
|
|
64
|
+
get(keyHex: string): Slot | null;
|
|
65
|
+
/** The proof for one slot: each sibling's digest from the slot up to the root, null for an empty subtree. */
|
|
66
|
+
proof(keyHex: string): Array<string | null>;
|
|
67
|
+
}
|
|
68
|
+
/** The root of the tree, in lowercase hex. */
|
|
69
|
+
export declare const smtRoot: (slots: Map<string, Slot>) => string;
|
|
70
|
+
/** The proof for one slot: each sibling's digest from the slot up to the root, null for an empty subtree. */
|
|
71
|
+
export declare const smtProof: (slots: Map<string, Slot>, keyHex: string) => Array<string | null>;
|
|
72
|
+
/** Fold a proof from a slot (null for an empty one) up to a root. */
|
|
73
|
+
export declare function rootFromProof(keyHex: string, slot: Slot | null, siblings: ReadonlyArray<string | null>): string;
|
|
74
|
+
export interface Commitment {
|
|
75
|
+
bureau: string;
|
|
76
|
+
seq: number;
|
|
77
|
+
prev: string | null;
|
|
78
|
+
issued_at: string;
|
|
79
|
+
root: string;
|
|
80
|
+
subjects: number;
|
|
81
|
+
tombstones: number;
|
|
82
|
+
}
|
|
83
|
+
export interface CommitmentEnvelope {
|
|
84
|
+
heads: Commitment;
|
|
85
|
+
bureau_key: string;
|
|
86
|
+
bureau_kid: string;
|
|
87
|
+
bureau_sig: string;
|
|
88
|
+
}
|
|
89
|
+
/** SHA-256, in lowercase hex, of a commitment's signed bytes: the next one's `prev`, and what a witness co-signs. */
|
|
90
|
+
export declare const commitmentHash: (c: Commitment) => string;
|
|
91
|
+
/** The commitment over these slots, after `prev` (null for the first). */
|
|
92
|
+
export declare function commitmentOf(x: {
|
|
93
|
+
bureau: string;
|
|
94
|
+
issuedAt: string;
|
|
95
|
+
slots: Map<string, Slot>;
|
|
96
|
+
prev: Commitment | null;
|
|
97
|
+
root?: string;
|
|
98
|
+
}): Commitment;
|
|
99
|
+
/** The commitment signed as a report is (§8.1.1), under `fdbk-heads-v1`. */
|
|
100
|
+
export declare function signCommitment(c: Commitment, signer: ReportSigner & {
|
|
101
|
+
kid?: string;
|
|
102
|
+
}): CommitmentEnvelope;
|
|
103
|
+
/** Whether `sig` (padded base64) is `key`'s Ed25519 signature over `bytes`; `key` padded base64 or an nkey. */
|
|
104
|
+
export declare function verifiesUnder(bytes: Uint8Array, key: string, sig: string): boolean;
|
|
105
|
+
/** Every tombstone in a commitment is dated no later than the commitment. */
|
|
106
|
+
export declare function erasuresDated(c: Pick<Commitment, "issued_at">, tombstones: readonly Tombstone[]): "ok" | string;
|
|
107
|
+
/** A commitment follows another: the next sequence number, and `prev` naming the previous one's signed bytes. */
|
|
108
|
+
export declare function followsFrom(prev: Commitment, next: Commitment): "ok" | string;
|
|
109
|
+
/**
|
|
110
|
+
* Every slot of the earlier commitment carried forward: a head stays or
|
|
111
|
+
* moves up its own log unless a tombstone replaces it; a tombstone stays, or
|
|
112
|
+
* gives way to a head that names the erasure in `after_erasure`; no slot
|
|
113
|
+
* simply empties.
|
|
114
|
+
*/
|
|
115
|
+
export declare function carriesForward(prevSlots: Map<string, Slot>, slots: Map<string, Slot>): "ok" | string;
|
|
116
|
+
export interface Witness {
|
|
117
|
+
id: string;
|
|
118
|
+
key: string;
|
|
119
|
+
kind: "cosigner" | "log";
|
|
120
|
+
}
|
|
121
|
+
export interface Cosignature {
|
|
122
|
+
witness: string;
|
|
123
|
+
kid?: string;
|
|
124
|
+
sig: string;
|
|
125
|
+
}
|
|
126
|
+
/** What a co-signer signs: the bureau, the seq, and the commitment's hash. */
|
|
127
|
+
export declare const cosignedObject: (c: Commitment) => {
|
|
128
|
+
bureau: string;
|
|
129
|
+
commitment: string;
|
|
130
|
+
seq: number;
|
|
131
|
+
};
|
|
132
|
+
/** A co-signature by one witness's signer. */
|
|
133
|
+
export declare function cosign(c: Commitment, witness: string, signer: ReportSigner): Cosignature;
|
|
134
|
+
/** The commitment a change of the witness list takes effect after: null before any commitment. */
|
|
135
|
+
export type ChangeAfter = {
|
|
136
|
+
seq: number;
|
|
137
|
+
commitment: string;
|
|
138
|
+
} | null;
|
|
139
|
+
/**
|
|
140
|
+
* Whether a co-signer may co-sign a commitment (rule 1, Agent Reputations
|
|
141
|
+
* 0.11.0-draft): the one that extends the last it co-signed for that bureau;
|
|
142
|
+
* or, for its first, where its record starts. A witness named before the
|
|
143
|
+
* first commitment (`start` null) starts with seq 1 and no prev; one that
|
|
144
|
+
* joined through a change starts after the commitment the change names in
|
|
145
|
+
* `after`, and never co-signs that one or any before it. So it never
|
|
146
|
+
* co-signs two commitments with one seq.
|
|
147
|
+
*/
|
|
148
|
+
export declare function witnessCosigns(last: Commitment | null, c: Commitment, start?: ChangeAfter): "ok" | string;
|
|
149
|
+
/** Every co-signer the key set names has co-signed the commitment. */
|
|
150
|
+
export declare function cosignedByAll(c: Commitment, cosignatures: readonly Cosignature[], witnesses: readonly Witness[]): "ok" | string;
|
|
151
|
+
/** The SHA-256 in lowercase hex of the JCS of a witness list: what a change names as `previous`. */
|
|
152
|
+
export declare const witnessListHash: (list: readonly Witness[]) => string;
|
|
153
|
+
export interface WitnessChange {
|
|
154
|
+
bureau: string;
|
|
155
|
+
previous: string;
|
|
156
|
+
/** The commitment current when the change takes effect (0.11.0-draft): the old list is named for it and every one before, the new list for every one after. */
|
|
157
|
+
after: ChangeAfter;
|
|
158
|
+
witnesses: Witness[];
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* A change of the witness list holds when it is this bureau's, changes the
|
|
162
|
+
* list the consumer pinned, adds or removes exactly one witness (§4.1.1: a
|
|
163
|
+
* new witness joins in one change and an old one leaves in a later one),
|
|
164
|
+
* names in `after` a commitment the bureau published and every witness on
|
|
165
|
+
* the list it replaces co-signed, never before the `after` of the change
|
|
166
|
+
* before it (`prior`), and every co-signer on the list it changes signed it.
|
|
167
|
+
* `published` maps a seq to the commitment and its co-signatures.
|
|
168
|
+
*/
|
|
169
|
+
export declare function witnessChangeSigned(pinned: readonly Witness[], change: WitnessChange, signatures: readonly Cosignature[], opts?: {
|
|
170
|
+
bureau?: string;
|
|
171
|
+
published?: ReadonlyMap<number, {
|
|
172
|
+
commitment: Commitment;
|
|
173
|
+
cosignatures: readonly Cosignature[];
|
|
174
|
+
}>;
|
|
175
|
+
prior?: ChangeAfter;
|
|
176
|
+
}): "ok" | string;
|
|
177
|
+
/** The witnesses named for a commitment's seq: the pinned list moved along every change that takes effect before it. */
|
|
178
|
+
export declare function witnessesFor(pinned: readonly Witness[], changes: ReadonlyArray<{
|
|
179
|
+
change: WitnessChange;
|
|
180
|
+
}>, seq: number): readonly Witness[];
|
|
181
|
+
/** Where a witness's record starts: null for one named before the first commitment, the `after` of the change that added it otherwise, undefined for one never named. */
|
|
182
|
+
export declare function witnessStart(pinned: readonly Witness[], changes: ReadonlyArray<{
|
|
183
|
+
change: WitnessChange;
|
|
184
|
+
}>, id: string): ChangeAfter | undefined;
|
package/dist/heads.js
ADDED
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The commitment over every subject's log (Agent Reputations §4.1.1), for a
|
|
3
|
+
* bureau that keeps one log per subject: the sparse Merkle tree of height 256
|
|
4
|
+
* with one slot per subject, keyed by the subject's canonical caseless form;
|
|
5
|
+
* the chained commitment over its root, signed under `fdbk-heads-v1`; the
|
|
6
|
+
* witnesses' co-signatures under `fdbk-heads-cosign-v1` and their rules; the
|
|
7
|
+
* changes to the witness list under `fdbk-witnesses-v1`; proofs of a slot;
|
|
8
|
+
* and what a later commitment may change of an earlier one.
|
|
9
|
+
*
|
|
10
|
+
* Pure: the bureau supplies the heads, the tombstones and the signing key, and
|
|
11
|
+
* this decides the bytes.
|
|
12
|
+
* `casefolding-15.1.0.json` beside this file is Unicode data under the
|
|
13
|
+
* Unicode License V3 (LICENSE-UNICODE beside it), a pinned copy of the
|
|
14
|
+
* standard's conformance file.
|
|
15
|
+
*/
|
|
16
|
+
import { createHash } from "node:crypto";
|
|
17
|
+
import { USER_KEY_RE, decodeSignature, encodeSignature, rawFromBase64Key, rawUserKey, verifyRaw } from "@common-agent/crypto";
|
|
18
|
+
import CASEFOLDING from "./casefolding-15.1.0.json" with { type: "json" };
|
|
19
|
+
import { jcs, kidOf, signedBytes } from "./signing.js";
|
|
20
|
+
export const HEADS_TAG = "fdbk-heads-v1";
|
|
21
|
+
export const COSIGN_TAG = "fdbk-heads-cosign-v1";
|
|
22
|
+
export const WITNESSES_TAG = "fdbk-witnesses-v1";
|
|
23
|
+
// ── the slot key (§4.1.1, the tree) ─────────────────────────────────────────
|
|
24
|
+
const FOLD = CASEFOLDING.fold;
|
|
25
|
+
/**
|
|
26
|
+
* A subject's canonical caseless form (Unicode D145, at 15.1.0): NFD, full
|
|
27
|
+
* case folding (the C and F entries of CaseFolding.txt), NFD again, as
|
|
28
|
+
* Personal Agent Naming §2 compares handles.
|
|
29
|
+
*/
|
|
30
|
+
export function foldSubject(subject) {
|
|
31
|
+
const table = FOLD;
|
|
32
|
+
let out = "";
|
|
33
|
+
for (const ch of subject.normalize("NFD")) {
|
|
34
|
+
const hex = ch.codePointAt(0).toString(16).toUpperCase().padStart(4, "0");
|
|
35
|
+
const to = table[hex];
|
|
36
|
+
out += to ? String.fromCodePoint(...to.split(" ").map((h) => parseInt(h, 16))) : ch;
|
|
37
|
+
}
|
|
38
|
+
return out.normalize("NFD");
|
|
39
|
+
}
|
|
40
|
+
/** The slot key: the SHA-256 of the UTF-8 of the folded subject, in lowercase hex. */
|
|
41
|
+
export const subjectKey = (subject) => createHash("sha256").update(foldSubject(subject), "utf8").digest("hex");
|
|
42
|
+
const isTombstone = (s) => "erased_at" in s;
|
|
43
|
+
/** The object a slot's digest is taken over. */
|
|
44
|
+
export function slotBody(slot) {
|
|
45
|
+
if (isTombstone(slot))
|
|
46
|
+
return { erased_at: slot.erased_at, key: slot.key };
|
|
47
|
+
return { ...(slot.after_erasure ? { after_erasure: slot.after_erasure } : {}), head_hash: slot.head_hash, key: slot.key, seq: slot.seq, subject: slot.subject };
|
|
48
|
+
}
|
|
49
|
+
const H = (...parts) => {
|
|
50
|
+
const h = createHash("sha256");
|
|
51
|
+
for (const p of parts)
|
|
52
|
+
h.update(p);
|
|
53
|
+
return h.digest();
|
|
54
|
+
};
|
|
55
|
+
const ONE = Buffer.from([1]);
|
|
56
|
+
/** A filled slot's digest: SHA-256 over 0x00 and the JCS of what it holds. */
|
|
57
|
+
export const slotDigest = (slot) => H(Buffer.from([0]), Buffer.from(jcs(slotBody(slot)), "utf8"));
|
|
58
|
+
/** EMPTY[h]: the digest of an empty subtree of height h. */
|
|
59
|
+
export const EMPTY = [Buffer.alloc(32)];
|
|
60
|
+
for (let i = 1; i <= 256; i++)
|
|
61
|
+
EMPTY.push(H(ONE, EMPTY[i - 1], EMPTY[i - 1]));
|
|
62
|
+
const bitOf = (key, i) => (key[i >> 3] >> (7 - (i & 7))) & 1;
|
|
63
|
+
/** The slots of a tree, one per subject. Two entries for one slot are refused. */
|
|
64
|
+
export function slotsOf(heads, tombstones = []) {
|
|
65
|
+
const slots = new Map();
|
|
66
|
+
const put = (slot) => {
|
|
67
|
+
if (slots.has(slot.key))
|
|
68
|
+
throw new Error(`two entries for the slot ${slot.key}`);
|
|
69
|
+
slots.set(slot.key, slot);
|
|
70
|
+
};
|
|
71
|
+
for (const h of heads)
|
|
72
|
+
put({ ...h, key: subjectKey(h.subject) });
|
|
73
|
+
for (const t of tombstones)
|
|
74
|
+
put({ key: t.key, erased_at: t.erased_at });
|
|
75
|
+
return slots;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The sparse Merkle tree over a set of slots, built once: the slots sorted by
|
|
79
|
+
* key, each key converted to bytes once, and the digest of every subtree the
|
|
80
|
+
* build visits kept, so the root and any slot's proof are read from the
|
|
81
|
+
* cache. The build splits the sorted slots by one bit per level, so it costs
|
|
82
|
+
* a hash per subtree that holds a slot, and a proof costs one walk of 256
|
|
83
|
+
* levels plus, for an absent key beside a lone slot, one climb from that
|
|
84
|
+
* slot.
|
|
85
|
+
*/
|
|
86
|
+
export class SparseTree {
|
|
87
|
+
keys;
|
|
88
|
+
slots;
|
|
89
|
+
cache = new Map();
|
|
90
|
+
root;
|
|
91
|
+
constructor(slots) {
|
|
92
|
+
const list = [...(slots instanceof Map ? slots.values() : slots)].sort((x, y) => (x.key < y.key ? -1 : x.key > y.key ? 1 : 0));
|
|
93
|
+
this.slots = list;
|
|
94
|
+
this.keys = list.map((x) => Buffer.from(x.key, "hex"));
|
|
95
|
+
this.root = this.node(0, 0, list.length).toString("hex");
|
|
96
|
+
}
|
|
97
|
+
/** The first index in [lo, hi) whose key has bit `depth` set. */
|
|
98
|
+
split(depth, lo, hi) {
|
|
99
|
+
let a = lo;
|
|
100
|
+
let b = hi;
|
|
101
|
+
while (a < b) {
|
|
102
|
+
const m = (a + b) >> 1;
|
|
103
|
+
if (bitOf(this.keys[m], depth))
|
|
104
|
+
b = m;
|
|
105
|
+
else
|
|
106
|
+
a = m + 1;
|
|
107
|
+
}
|
|
108
|
+
return a;
|
|
109
|
+
}
|
|
110
|
+
/** A lone slot's digest as the root of a subtree at `depth`: its leaf, climbed through empty siblings. */
|
|
111
|
+
lone(i, depth) {
|
|
112
|
+
let cur = slotDigest(this.slots[i]);
|
|
113
|
+
for (let h = 0; h < 256 - depth; h++)
|
|
114
|
+
cur = bitOf(this.keys[i], 255 - h) ? H(ONE, EMPTY[h], cur) : H(ONE, cur, EMPTY[h]);
|
|
115
|
+
return cur;
|
|
116
|
+
}
|
|
117
|
+
/** The digest of the subtree at `depth` holding the slots [lo, hi). */
|
|
118
|
+
node(depth, lo, hi) {
|
|
119
|
+
if (lo === hi)
|
|
120
|
+
return EMPTY[256 - depth];
|
|
121
|
+
const k = `${depth}:${lo}:${hi}`;
|
|
122
|
+
const hit = this.cache.get(k);
|
|
123
|
+
if (hit)
|
|
124
|
+
return hit;
|
|
125
|
+
let d;
|
|
126
|
+
if (hi - lo === 1)
|
|
127
|
+
d = this.lone(lo, depth);
|
|
128
|
+
else {
|
|
129
|
+
const m = this.split(depth, lo, hi);
|
|
130
|
+
d = H(ONE, this.node(depth + 1, lo, m), this.node(depth + 1, m, hi));
|
|
131
|
+
}
|
|
132
|
+
this.cache.set(k, d);
|
|
133
|
+
return d;
|
|
134
|
+
}
|
|
135
|
+
/** The slot at a key, or null. */
|
|
136
|
+
get(keyHex) {
|
|
137
|
+
let a = 0;
|
|
138
|
+
let b = this.slots.length;
|
|
139
|
+
while (a < b) {
|
|
140
|
+
const m = (a + b) >> 1;
|
|
141
|
+
if (this.slots[m].key < keyHex)
|
|
142
|
+
a = m + 1;
|
|
143
|
+
else
|
|
144
|
+
b = m;
|
|
145
|
+
}
|
|
146
|
+
return a < this.slots.length && this.slots[a].key === keyHex ? this.slots[a] : null;
|
|
147
|
+
}
|
|
148
|
+
/** The proof for one slot: each sibling's digest from the slot up to the root, null for an empty subtree. */
|
|
149
|
+
proof(keyHex) {
|
|
150
|
+
const key = Buffer.from(keyHex, "hex");
|
|
151
|
+
const topDown = [];
|
|
152
|
+
let lo = 0;
|
|
153
|
+
let hi = this.slots.length;
|
|
154
|
+
for (let depth = 0; depth < 256; depth++) {
|
|
155
|
+
const m = this.split(depth, lo, hi);
|
|
156
|
+
const [same, other] = bitOf(key, depth) ? [[m, hi], [lo, m]] : [[lo, m], [m, hi]];
|
|
157
|
+
topDown.push(other[0] === other[1] ? null : this.node(depth + 1, other[0], other[1]).toString("hex"));
|
|
158
|
+
[lo, hi] = same;
|
|
159
|
+
}
|
|
160
|
+
return topDown.reverse();
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/** The root of the tree, in lowercase hex. */
|
|
164
|
+
export const smtRoot = (slots) => new SparseTree(slots).root;
|
|
165
|
+
/** The proof for one slot: each sibling's digest from the slot up to the root, null for an empty subtree. */
|
|
166
|
+
export const smtProof = (slots, keyHex) => new SparseTree(slots).proof(keyHex);
|
|
167
|
+
/** Fold a proof from a slot (null for an empty one) up to a root. */
|
|
168
|
+
export function rootFromProof(keyHex, slot, siblings) {
|
|
169
|
+
const key = Buffer.from(keyHex, "hex");
|
|
170
|
+
let cur = slot ? slotDigest(slot) : EMPTY[0];
|
|
171
|
+
for (let h = 0; h < 256; h++) {
|
|
172
|
+
const sb = siblings[h];
|
|
173
|
+
const sib = sb === null || sb === undefined ? EMPTY[h] : Buffer.from(sb, "hex");
|
|
174
|
+
cur = bitOf(key, 255 - h) ? H(ONE, sib, cur) : H(ONE, cur, sib);
|
|
175
|
+
}
|
|
176
|
+
return cur.toString("hex");
|
|
177
|
+
}
|
|
178
|
+
/** SHA-256, in lowercase hex, of a commitment's signed bytes: the next one's `prev`, and what a witness co-signs. */
|
|
179
|
+
export const commitmentHash = (c) => createHash("sha256").update(signedBytes(HEADS_TAG, c)).digest("hex");
|
|
180
|
+
/** The commitment over these slots, after `prev` (null for the first). */
|
|
181
|
+
export function commitmentOf(x) {
|
|
182
|
+
const values = [...x.slots.values()];
|
|
183
|
+
return {
|
|
184
|
+
bureau: x.bureau,
|
|
185
|
+
seq: x.prev ? x.prev.seq + 1 : 1,
|
|
186
|
+
prev: x.prev ? commitmentHash(x.prev) : null,
|
|
187
|
+
issued_at: x.issuedAt,
|
|
188
|
+
// The root of a tree the caller already built over these slots, or this one's.
|
|
189
|
+
root: x.root ?? smtRoot(x.slots),
|
|
190
|
+
subjects: values.filter((s) => !isTombstone(s)).length,
|
|
191
|
+
tombstones: values.filter(isTombstone).length,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
/** The commitment signed as a report is (§8.1.1), under `fdbk-heads-v1`. */
|
|
195
|
+
export function signCommitment(c, signer) {
|
|
196
|
+
const sig = encodeSignature(signer.sign(signedBytes(HEADS_TAG, c)));
|
|
197
|
+
const kid = kidOf(signer.key);
|
|
198
|
+
return { heads: c, bureau_key: signer.key, bureau_kid: kid, bureau_sig: sig };
|
|
199
|
+
}
|
|
200
|
+
/** Whether `sig` (padded base64) is `key`'s Ed25519 signature over `bytes`; `key` padded base64 or an nkey. */
|
|
201
|
+
export function verifiesUnder(bytes, key, sig) {
|
|
202
|
+
const raw = USER_KEY_RE.test(key) ? rawUserKey(key) : rawFromBase64Key(key);
|
|
203
|
+
return verifyRaw(raw, bytes, decodeSignature(sig, "base64"));
|
|
204
|
+
}
|
|
205
|
+
/** Every tombstone in a commitment is dated no later than the commitment. */
|
|
206
|
+
export function erasuresDated(c, tombstones) {
|
|
207
|
+
for (const t of tombstones)
|
|
208
|
+
if (Date.parse(t.erased_at) > Date.parse(c.issued_at))
|
|
209
|
+
return `the erasure of ${t.key.slice(0, 12)}… is dated after the commitment`;
|
|
210
|
+
return "ok";
|
|
211
|
+
}
|
|
212
|
+
/** A commitment follows another: the next sequence number, and `prev` naming the previous one's signed bytes. */
|
|
213
|
+
export function followsFrom(prev, next) {
|
|
214
|
+
if (next.seq !== prev.seq + 1)
|
|
215
|
+
return `the sequence number is ${next.seq}, after ${prev.seq}`;
|
|
216
|
+
if (next.prev !== commitmentHash(prev))
|
|
217
|
+
return "prev does not name the previous commitment";
|
|
218
|
+
return "ok";
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Every slot of the earlier commitment carried forward: a head stays or
|
|
222
|
+
* moves up its own log unless a tombstone replaces it; a tombstone stays, or
|
|
223
|
+
* gives way to a head that names the erasure in `after_erasure`; no slot
|
|
224
|
+
* simply empties.
|
|
225
|
+
*/
|
|
226
|
+
export function carriesForward(prevSlots, slots) {
|
|
227
|
+
for (const [key, was] of prevSlots) {
|
|
228
|
+
const now = slots.get(key);
|
|
229
|
+
if (!now)
|
|
230
|
+
return `the slot ${key.slice(0, 12)}… emptied without a tombstone`;
|
|
231
|
+
if (isTombstone(was)) {
|
|
232
|
+
if (isTombstone(now) ? now.erased_at !== was.erased_at : now.after_erasure !== was.erased_at)
|
|
233
|
+
return `the erased slot ${key.slice(0, 12)}… came back without naming its erasure`;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (isTombstone(now))
|
|
237
|
+
continue;
|
|
238
|
+
if ((now.after_erasure ?? null) !== (was.after_erasure ?? null))
|
|
239
|
+
return `the slot ${key.slice(0, 12)}… changed its erasure`;
|
|
240
|
+
if (now.seq < was.seq)
|
|
241
|
+
return `the head of ${was.subject} went back from ${was.seq} to ${now.seq}`;
|
|
242
|
+
if (now.seq === was.seq && now.head_hash !== was.head_hash)
|
|
243
|
+
return `the head of ${was.subject} at ${was.seq} changed its hash`;
|
|
244
|
+
}
|
|
245
|
+
return "ok";
|
|
246
|
+
}
|
|
247
|
+
/** What a co-signer signs: the bureau, the seq, and the commitment's hash. */
|
|
248
|
+
export const cosignedObject = (c) => ({ bureau: c.bureau, commitment: commitmentHash(c), seq: c.seq });
|
|
249
|
+
/** A co-signature by one witness's signer. */
|
|
250
|
+
export function cosign(c, witness, signer) {
|
|
251
|
+
return { witness, sig: encodeSignature(signer.sign(signedBytes(COSIGN_TAG, cosignedObject(c)))) };
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Whether a co-signer may co-sign a commitment (rule 1, Agent Reputations
|
|
255
|
+
* 0.11.0-draft): the one that extends the last it co-signed for that bureau;
|
|
256
|
+
* or, for its first, where its record starts. A witness named before the
|
|
257
|
+
* first commitment (`start` null) starts with seq 1 and no prev; one that
|
|
258
|
+
* joined through a change starts after the commitment the change names in
|
|
259
|
+
* `after`, and never co-signs that one or any before it. So it never
|
|
260
|
+
* co-signs two commitments with one seq.
|
|
261
|
+
*/
|
|
262
|
+
export function witnessCosigns(last, c, start = null) {
|
|
263
|
+
if (!last && start === null)
|
|
264
|
+
return c.seq === 1 && c.prev === null ? "ok" : "the witness co-signs a bureau's first commitment first";
|
|
265
|
+
if (!last && start) {
|
|
266
|
+
if (c.seq <= start.seq)
|
|
267
|
+
return "a joining witness never co-signs the commitment its change names or one before it";
|
|
268
|
+
if (c.seq !== start.seq + 1 || c.prev !== start.commitment)
|
|
269
|
+
return "a joining witness first co-signs the commitment after the one its change names";
|
|
270
|
+
return "ok";
|
|
271
|
+
}
|
|
272
|
+
if (!last)
|
|
273
|
+
return "the witness has no record";
|
|
274
|
+
if (c.bureau !== last.bureau)
|
|
275
|
+
return "another bureau";
|
|
276
|
+
if (c.seq !== last.seq + 1 || c.prev !== commitmentHash(last))
|
|
277
|
+
return "it does not extend the last commitment the witness co-signed";
|
|
278
|
+
return "ok";
|
|
279
|
+
}
|
|
280
|
+
/** Every co-signer the key set names has co-signed the commitment. */
|
|
281
|
+
export function cosignedByAll(c, cosignatures, witnesses) {
|
|
282
|
+
const bytes = signedBytes(COSIGN_TAG, cosignedObject(c));
|
|
283
|
+
for (const w of witnesses.filter((x) => x.kind === "cosigner")) {
|
|
284
|
+
const s = cosignatures.find((x) => x.witness === w.id);
|
|
285
|
+
if (!s || !verifiesUnder(bytes, w.key, s.sig))
|
|
286
|
+
return `${w.id} has not co-signed it`;
|
|
287
|
+
}
|
|
288
|
+
return "ok";
|
|
289
|
+
}
|
|
290
|
+
/** The SHA-256 in lowercase hex of the JCS of a witness list: what a change names as `previous`. */
|
|
291
|
+
export const witnessListHash = (list) => createHash("sha256").update(jcs(list), "utf8").digest("hex");
|
|
292
|
+
/**
|
|
293
|
+
* A change of the witness list holds when it is this bureau's, changes the
|
|
294
|
+
* list the consumer pinned, adds or removes exactly one witness (§4.1.1: a
|
|
295
|
+
* new witness joins in one change and an old one leaves in a later one),
|
|
296
|
+
* names in `after` a commitment the bureau published and every witness on
|
|
297
|
+
* the list it replaces co-signed, never before the `after` of the change
|
|
298
|
+
* before it (`prior`), and every co-signer on the list it changes signed it.
|
|
299
|
+
* `published` maps a seq to the commitment and its co-signatures.
|
|
300
|
+
*/
|
|
301
|
+
export function witnessChangeSigned(pinned, change, signatures, opts = {}) {
|
|
302
|
+
if (opts.bureau !== undefined && change.bureau !== opts.bureau)
|
|
303
|
+
return "it is another bureau's change";
|
|
304
|
+
if (change.previous !== witnessListHash(pinned))
|
|
305
|
+
return "it does not change the list the consumer pinned";
|
|
306
|
+
if (!("after" in change))
|
|
307
|
+
return "it does not name the commitment it takes effect after";
|
|
308
|
+
const after = change.after;
|
|
309
|
+
const prior = opts.prior ?? null;
|
|
310
|
+
if (after !== null) {
|
|
311
|
+
if (typeof after !== "object" || Object.keys(after).sort().join() !== "commitment,seq" || !Number.isInteger(after.seq) || after.seq < 1)
|
|
312
|
+
return "its after is not a seq and a commitment hash";
|
|
313
|
+
const c = opts.published?.get(after.seq);
|
|
314
|
+
if (!c || commitmentHash(c.commitment) !== after.commitment)
|
|
315
|
+
return "its after names a commitment the bureau did not publish";
|
|
316
|
+
if (cosignedByAll(c.commitment, c.cosignatures, pinned) !== "ok")
|
|
317
|
+
return "the witnesses it replaces have not all co-signed the commitment it names";
|
|
318
|
+
if (prior !== null && after.seq < prior.seq)
|
|
319
|
+
return "it takes effect before the change before it";
|
|
320
|
+
}
|
|
321
|
+
else if (prior !== null)
|
|
322
|
+
return "it takes effect before the change before it";
|
|
323
|
+
const id = (w) => `${w.id}|${w.key}|${w.kind}`;
|
|
324
|
+
const before = new Set(pinned.map(id));
|
|
325
|
+
const afterList = new Set(change.witnesses.map(id));
|
|
326
|
+
const moved = [...before].filter((w) => !afterList.has(w)).length + [...afterList].filter((w) => !before.has(w)).length;
|
|
327
|
+
if (moved !== 1)
|
|
328
|
+
return "a change adds or removes exactly one witness";
|
|
329
|
+
const bytes = signedBytes(WITNESSES_TAG, change);
|
|
330
|
+
for (const w of pinned.filter((x) => x.kind === "cosigner")) {
|
|
331
|
+
const sg = signatures.find((x) => x.witness === w.id);
|
|
332
|
+
if (!sg || !verifiesUnder(bytes, w.key, sg.sig))
|
|
333
|
+
return `${w.id}, on the list it changes, has not signed it`;
|
|
334
|
+
}
|
|
335
|
+
return "ok";
|
|
336
|
+
}
|
|
337
|
+
/** The witnesses named for a commitment's seq: the pinned list moved along every change that takes effect before it. */
|
|
338
|
+
export function witnessesFor(pinned, changes, seq) {
|
|
339
|
+
let list = pinned;
|
|
340
|
+
for (const { change } of changes) {
|
|
341
|
+
if (change.after !== null && change.after.seq >= seq)
|
|
342
|
+
break;
|
|
343
|
+
list = change.witnesses;
|
|
344
|
+
}
|
|
345
|
+
return list;
|
|
346
|
+
}
|
|
347
|
+
/** Where a witness's record starts: null for one named before the first commitment, the `after` of the change that added it otherwise, undefined for one never named. */
|
|
348
|
+
export function witnessStart(pinned, changes, id) {
|
|
349
|
+
let list = pinned;
|
|
350
|
+
if (list.some((w) => w.id === id))
|
|
351
|
+
return null;
|
|
352
|
+
for (const { change } of changes) {
|
|
353
|
+
if (!list.some((w) => w.id === id) && change.witnesses.some((w) => w.id === id))
|
|
354
|
+
return change.after;
|
|
355
|
+
list = change.witnesses;
|
|
356
|
+
}
|
|
357
|
+
return undefined;
|
|
358
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Agent Reputations core (https://agentreputations.com).
|
|
3
|
+
*
|
|
4
|
+
* The scoring formula and its priors (score.ts), the pool of §7.1 over the
|
|
5
|
+
* standard's ledger events (pool.ts), the pure rules of the bureau's ledger
|
|
6
|
+
* (ledger.ts), the report as data (report.ts), signing and verifying a
|
|
7
|
+
* report (signing.ts), and the pure parts of a claim of breach of an Agent
|
|
8
|
+
* NDA (nda-claims.ts). A bureau's process, its database and its signing key
|
|
9
|
+
* are the host's, which uses this core.
|
|
10
|
+
*
|
|
11
|
+
* This core asks the platform for nothing and calls no other standard. It
|
|
12
|
+
* learns about engagements only from the evidence the bureau records.
|
|
13
|
+
*/
|
|
14
|
+
/** The standard and the version this core is written against. */
|
|
15
|
+
export declare const REPUTATIONS_SPEC: {
|
|
16
|
+
readonly id: "reputations";
|
|
17
|
+
readonly name: "Agent Reputations";
|
|
18
|
+
readonly version: "0.6.0-draft";
|
|
19
|
+
readonly url: "https://agentreputations.com/spec.html";
|
|
20
|
+
};
|
|
21
|
+
export { OUTCOME_VALUES, VALUE_OUTCOMES, UNSCORED_OUTCOMES, FORMULA_VERSION, HALF_LIFE_DAYS, UNPROVEN_THRESHOLD, K_VALUE, decayWeight, eventWeight, scorePool, scoreWeighted, portfolioPrior, checkPrior, UNIFORM_PRIOR, MAX_PRIOR_WEIGHT, type Prior, type ScoredEvent, type QualityScore, } from "./score.js";
|
|
22
|
+
export { poolFromEvents, NO_WORK, type LedgerEvent, type PoolEntry } from "./pool.js";
|
|
23
|
+
export { GENESIS, SOURCE_RANK, enrollmentFromActs, poolFromEntries, entryHashOf, verifyChainEntries, LEDGER_ACTIONS, type LedgerAction, type LedgerEntry, type EnrollmentInfo, type PoolResult, } from "./ledger.js";
|
|
24
|
+
export { REPORT_SIG_PREFIX, ENROLL_TAG, WITHDRAW_TAG, VALUE_TAG, enrollmentActText, valueJudgmentText, REPORT_TTL_MS, capabilityState, capabilityBlock, factsBlock, assembleReport, assembleCapabilityReport, type CapabilityState, type CapabilityBlock, type FactsBlock, type Report, type SignedReport, type CapabilityReport, } from "./report.js";
|
|
25
|
+
export { REPORT_TAG, ORG_REPORT_TAG, REPORT_KEY_USE, CLOCK_GRACE_SECONDS, jcs, signedBytes, kidOf, keySetFor, rawKeyFromNkey, keyFromRaw, seedSigner, signReport, inexactInteger, verifyReport, verifyReportText, repeatedMember, type KeySet, type KeySetEntry, type ReportSigner, type ReportEnvelope, type VerifyOptions, type ExpectedAnswer, type VerifyResult, } from "./signing.js";
|
|
26
|
+
export { NDA_CLAIM_TAG, DISCLOSURE_KINDS, FILING_PERIOD_DAYS, claimSignedBytes, claimDigest, claimProblems, claimSignedBy, nkeyOf, signedByKey, saltedAnchorHash, withinClaimWindow, amendmentKey, NDA_ANSWER_TAG, NDA_ACK_TAG, NDA_WITHDRAW_TAG, MAX_ANSWER_CHARS, ndaActBytes, UPHOLD_BASES, RESOLUTIONS, OPEN_PERIOD_DAYS, RECORD_PERIOD_DAYS, MIN_RECORD_PERIOD_DAYS, MIN_FILING_PERIOD_DAYS, resolutionRefusal, claimStanding, type DisclosureKind, type NdaClaim, type NdaAnswer, type NdaAck, type NdaWithdrawal, type UpholdBasis, type Resolution, type ClaimState, } from "./nda-claims.js";
|
|
27
|
+
export { ndaRecord, filedCounts, type ClaimView, type NdaFiledCounts, type NdaRecord, type NdaRecordUpheld } from "./nda-record.js";
|
|
28
|
+
export { HEADS_TAG, COSIGN_TAG, WITNESSES_TAG, EMPTY, foldSubject, subjectKey, slotBody, slotDigest, slotsOf, SparseTree, smtRoot, smtProof, rootFromProof, commitmentHash, commitmentOf, signCommitment, verifiesUnder, erasuresDated, followsFrom, carriesForward, cosignedObject, cosign, witnessCosigns, cosignedByAll, witnessListHash, witnessChangeSigned, witnessesFor, witnessStart, type ChangeAfter, type Slot, type Head, type Tombstone, type Commitment, type CommitmentEnvelope, type Witness, type Cosignature, type WitnessChange, } from "./heads.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Agent Reputations core (https://agentreputations.com).
|
|
3
|
+
*
|
|
4
|
+
* The scoring formula and its priors (score.ts), the pool of §7.1 over the
|
|
5
|
+
* standard's ledger events (pool.ts), the pure rules of the bureau's ledger
|
|
6
|
+
* (ledger.ts), the report as data (report.ts), signing and verifying a
|
|
7
|
+
* report (signing.ts), and the pure parts of a claim of breach of an Agent
|
|
8
|
+
* NDA (nda-claims.ts). A bureau's process, its database and its signing key
|
|
9
|
+
* are the host's, which uses this core.
|
|
10
|
+
*
|
|
11
|
+
* This core asks the platform for nothing and calls no other standard. It
|
|
12
|
+
* learns about engagements only from the evidence the bureau records.
|
|
13
|
+
*/
|
|
14
|
+
/** The standard and the version this core is written against. */
|
|
15
|
+
export const REPUTATIONS_SPEC = {
|
|
16
|
+
id: "reputations",
|
|
17
|
+
name: "Agent Reputations",
|
|
18
|
+
version: "0.6.0-draft",
|
|
19
|
+
url: "https://agentreputations.com/spec.html",
|
|
20
|
+
};
|
|
21
|
+
export { OUTCOME_VALUES, VALUE_OUTCOMES, UNSCORED_OUTCOMES, FORMULA_VERSION, HALF_LIFE_DAYS, UNPROVEN_THRESHOLD, K_VALUE, decayWeight, eventWeight, scorePool, scoreWeighted, portfolioPrior, checkPrior, UNIFORM_PRIOR, MAX_PRIOR_WEIGHT, } from "./score.js";
|
|
22
|
+
export { poolFromEvents, NO_WORK } from "./pool.js";
|
|
23
|
+
export { GENESIS, SOURCE_RANK, enrollmentFromActs, poolFromEntries, entryHashOf, verifyChainEntries, LEDGER_ACTIONS, } from "./ledger.js";
|
|
24
|
+
export { REPORT_SIG_PREFIX, ENROLL_TAG, WITHDRAW_TAG, VALUE_TAG, enrollmentActText, valueJudgmentText, REPORT_TTL_MS, capabilityState, capabilityBlock, factsBlock, assembleReport, assembleCapabilityReport, } from "./report.js";
|
|
25
|
+
export { REPORT_TAG, ORG_REPORT_TAG, REPORT_KEY_USE, CLOCK_GRACE_SECONDS, jcs, signedBytes, kidOf, keySetFor, rawKeyFromNkey, keyFromRaw, seedSigner, signReport, inexactInteger, verifyReport, verifyReportText, repeatedMember, } from "./signing.js";
|
|
26
|
+
export { NDA_CLAIM_TAG, DISCLOSURE_KINDS, FILING_PERIOD_DAYS, claimSignedBytes, claimDigest, claimProblems, claimSignedBy, nkeyOf, signedByKey, saltedAnchorHash, withinClaimWindow, amendmentKey, NDA_ANSWER_TAG, NDA_ACK_TAG, NDA_WITHDRAW_TAG, MAX_ANSWER_CHARS, ndaActBytes, UPHOLD_BASES, RESOLUTIONS, OPEN_PERIOD_DAYS, RECORD_PERIOD_DAYS, MIN_RECORD_PERIOD_DAYS, MIN_FILING_PERIOD_DAYS, resolutionRefusal, claimStanding, } from "./nda-claims.js";
|
|
27
|
+
export { ndaRecord, filedCounts } from "./nda-record.js";
|
|
28
|
+
export { HEADS_TAG, COSIGN_TAG, WITNESSES_TAG, EMPTY, foldSubject, subjectKey, slotBody, slotDigest, slotsOf, SparseTree, smtRoot, smtProof, rootFromProof, commitmentHash, commitmentOf, signCommitment, verifiesUnder, erasuresDated, followsFrom, carriesForward, cosignedObject, cosign, witnessCosigns, cosignedByAll, witnessListHash, witnessChangeSigned, witnessesFor, witnessStart, } from "./heads.js";
|