pazair-verify 1.7.0 → 1.8.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/README.md +15 -0
- package/package.json +1 -1
- package/src/fingerprint.js +85 -0
- package/src/index.d.ts +13 -0
- package/src/index.js +3 -0
package/README.md
CHANGED
|
@@ -60,6 +60,18 @@ const h = await checkHead('2026-10-10:3f9c…'); // the head yo
|
|
|
60
60
|
Nothing here asks the issuer whether its chain is fine. The chain is fetched as data and recomputed; the issuer's
|
|
61
61
|
`witness` tool is for the issuer's own count of witnesses, this is yours. SPEC section 14 has the formulas.
|
|
62
62
|
|
|
63
|
+
## Fingerprint: no two passes alike, provably
|
|
64
|
+
|
|
65
|
+
Every pass carries a fingerprint (SPEC section 15): a Reed-Solomon codeword over `sha256(origin, agent, first anchored
|
|
66
|
+
leaf)`. Any two agents differ in at least 55 of 64 symbols, a theorem, not a probability. Draw it, compare it:
|
|
67
|
+
|
|
68
|
+
```js
|
|
69
|
+
import { fingerprintOf, fingerprintDistance, fingerprintSvg, recordOf } from 'pazair-verify';
|
|
70
|
+
const me = await fingerprintOf({ origin: 'https://pazair.kulalabs.ch', agent: 'ag_xyz', first_leaf: doc.fingerprint.first_leaf });
|
|
71
|
+
fingerprintDistance(me, doc.fingerprint); // 0: the issuer's print is this agent's
|
|
72
|
+
const svg = fingerprintSvg(me, recordOf(doc.current)); // the same bytes the issuer serves as fingerprint.svg
|
|
73
|
+
```
|
|
74
|
+
|
|
63
75
|
## Lower level
|
|
64
76
|
|
|
65
77
|
```js
|
|
@@ -88,6 +100,9 @@ console.log(await verifyReceipt(receipt, { delivery }));
|
|
|
88
100
|
| `verifyMandate(receipt, mandate, { keys?, end? })` | Section 3.2: the receipt was bought within the principal's mandate (agent, currency, cap per order, before its end); returns `{ valid, covers, reasons }` |
|
|
89
101
|
| `verifyAward(receipt, tender, award, { keys?, qa? })` | Section 3.3: the receipt was paid under this award for this tender (buyer, awarded seller, budget, price or milestone, deadline, questions published before the award); returns `{ valid, covers, reasons }` |
|
|
90
102
|
| `receiptHash(receipt)` | The hash a parent receipt lists in `inputs` |
|
|
103
|
+
| `fingerprintOf({ origin, agent, first_leaf })` | Section 15: the seed and the 64-byte codeword of an agent's fingerprint; two agents differ in at least 55 symbols |
|
|
104
|
+
| `fingerprintDistance(a, b)` | How many of the 64 symbols differ: 0 the same agent, below 55 not a fingerprint |
|
|
105
|
+
| `fingerprintSvg(codeword, record?)` | The reference drawing, the same bytes for the same input; `recordOf(pass)` reads the record it grows with |
|
|
91
106
|
| `verifyPass(anchored, { keys? })` | signature, Merkle proof into the day's root; returns the anchors to check on Bitcoin and Stellar |
|
|
92
107
|
| `verifyProof(leaf, proof, root)` | a Merkle path |
|
|
93
108
|
| `leafOf(pass)`, `canonical(obj)`, `sha256hex(s)` | the building blocks of the spec |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pazair-verify",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "Verify Word Passes, receipts, Merkle proofs and Remember (the chain of daily roots, Ed25519 + ML-DSA-65): checked by anyone, for any issuer, without trusting it. Zero dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Fingerprint (SPEC section 15): a print no other agent can carry, provably.
|
|
2
|
+
// seed = sha256(origin "\n" agent "\n" first_leaf); the first 10 bytes are the message of a Reed-Solomon code over
|
|
3
|
+
// GF(256) with n = 64, k = 10, so any two prints differ in at least d = n - k + 1 = 55 of 64 symbols. Each symbol is
|
|
4
|
+
// one sector of the drawing. The record is drawn over it and never changes the identity.
|
|
5
|
+
import { sha256hex } from './index.js';
|
|
6
|
+
|
|
7
|
+
const N = 64, K = 10, POLY = 0x11d;
|
|
8
|
+
const EXP = new Uint8Array(512), LOG = new Uint8Array(256);
|
|
9
|
+
(() => { let x = 1; for (let i = 0; i < 255; i++) { EXP[i] = x; LOG[x] = i; x <<= 1; if (x & 256) x ^= POLY; } for (let i = 255; i < 512; i++) EXP[i] = EXP[i - 255]; })();
|
|
10
|
+
const mul = (a, b) => (a && b ? EXP[LOG[a] + LOG[b]] : 0);
|
|
11
|
+
/** Generator polynomial: product of (x - α^i) for i = 0 … n-k-1, lowest degree first. */
|
|
12
|
+
const GEN = (() => { let g = [1]; for (let i = 0; i < N - K; i++) { const r = EXP[i], ng = new Array(g.length + 1).fill(0); for (let j = 0; j < g.length; j++) { ng[j] ^= g[j]; ng[j + 1] ^= mul(g[j], r); } g = ng; } return g; })();
|
|
13
|
+
|
|
14
|
+
/** Systematic Reed-Solomon encoding: the first K bytes of `msg`, then N-K parity bytes. */
|
|
15
|
+
export function rsEncode(msg) {
|
|
16
|
+
const buf = new Uint8Array(N); buf.set(msg.slice(0, K));
|
|
17
|
+
for (let i = 0; i < K; i++) { const c = buf[i]; if (!c) continue; for (let j = 1; j < GEN.length; j++) buf[i + j] ^= mul(GEN[j], c); }
|
|
18
|
+
const out = new Uint8Array(N); out.set(msg.slice(0, K)); out.set(buf.subarray(K), K); return out;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const hex = (u) => [...u].map((b) => b.toString(16).padStart(2, '0')).join('');
|
|
22
|
+
const unhex = (h) => Uint8Array.from(h.match(/../g), (x) => parseInt(x, 16));
|
|
23
|
+
export const FINGERPRINT = Object.freeze({ n: N, k: K, d: N - K + 1, field: 'GF(256), polynomial 0x11d', generator: 'roots α^0 … α^53' });
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The fingerprint of an agent: `origin` is the issuer's https origin (section 2), `agent` its id, `first_leaf` the
|
|
27
|
+
* leaf of the agent's first anchored pass (the issuer serves it as `fingerprint.first_leaf`). Returns the seed and the
|
|
28
|
+
* codeword as lowercase hex.
|
|
29
|
+
*/
|
|
30
|
+
export async function fingerprintOf({ origin, agent, first_leaf }) {
|
|
31
|
+
for (const [k, v] of Object.entries({ origin, agent, first_leaf })) if (typeof v !== 'string' || !v) throw new TypeError(`fingerprintOf: ${k} must be a non-empty string`);
|
|
32
|
+
const seed = await sha256hex(`${origin.replace(/\/+$/, '')}\n${agent}\n${first_leaf}`);
|
|
33
|
+
return { seed, codeword: hex(rsEncode(unhex(seed))) };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** How many of the 64 symbols differ. For two different seeds this is never below 55. */
|
|
37
|
+
export function fingerprintDistance(a, b) {
|
|
38
|
+
const x = unhex(a.codeword ?? a), y = unhex(b.codeword ?? b);
|
|
39
|
+
if (x.length !== N || y.length !== N) throw new TypeError('fingerprintDistance: a codeword has 64 bytes');
|
|
40
|
+
let d = 0; for (let i = 0; i < N; i++) if (x[i] !== y[i]) d++; return d;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The record the drawing grows with, read from a pass (section 4). */
|
|
44
|
+
export const recordOf = (pass) => ({ delivered: Number(pass?.as_seller?.delivered ?? 0), buyers: Number(pass?.as_seller?.buyers ?? 0), badge: pass?.word?.badge ?? null, disputes_lost: Number(pass?.disputes_lost ?? 0) });
|
|
45
|
+
export const ridgesOf = (delivered) => Math.min(5 + Math.floor(Math.max(0, delivered) / 3), 26);
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The reference drawing as SVG: closed ridges around a core, like a finger. Sector j (symbol s): bits 0-4 say on which
|
|
49
|
+
* ridge the sector's ridge ending sits, bits 5-7 how far the ridges bulge there (smoothed over the neighbours). Five
|
|
50
|
+
* ridges to start, one more per three delivered orders; the two innermost form an open loop until five buyers; the badge
|
|
51
|
+
* is the gold outermost ridge; a lost dispute is a scar across the ridges. Same drawing for the same input, byte for byte.
|
|
52
|
+
*/
|
|
53
|
+
export function fingerprintSvg(codeword, record = { delivered: 0, buyers: 0, badge: null, disputes_lost: 0 }, { size = 360, ink = 'currentColor', gold = '#d1a04a', background = 'none' } = {}) {
|
|
54
|
+
const cw = unhex(codeword.codeword ?? codeword);
|
|
55
|
+
if (cw.length !== N) throw new TypeError('fingerprintSvg: a codeword has 64 bytes');
|
|
56
|
+
const W = 360, cx = W / 2, cy = W * 0.52, total = ridgesOf(record.delivered), gap = (W * 0.40) / (total + 1), r0 = W * 0.06;
|
|
57
|
+
const raw = [...cw].map((s) => ((s >> 5) - 3.5) / 3.5), sm = raw.map((_, j) => (raw[(j + N - 1) % N] + 2 * raw[j] + raw[(j + 1) % N]) / 4);
|
|
58
|
+
const bulgeAt = (th) => { const u = ((th / (Math.PI * 2)) * N + N) % N, j = Math.floor(u), f = (1 - Math.cos((u - j) * Math.PI)) / 2; return sm[j] * (1 - f) + sm[(j + 1) % N] * f; };
|
|
59
|
+
const p1 = (cw[0] / 255) * Math.PI * 2, p2 = (cw[1] / 255) * Math.PI * 2, e1 = 0.06 + (cw[2] / 255) * 0.08, e2 = 0.04 + (cw[3] / 255) * 0.06;
|
|
60
|
+
const radiusAt = (i, th) => (r0 + i * gap) * (1 + e1 * Math.cos(th - p1) * (i / total) + e2 * Math.cos(2 * th - p2)) * (1 + 0.06 * bulgeAt(th) * Math.min(1, i / 3));
|
|
61
|
+
const f1 = (x) => (Math.floor(x * 10 + 0.5) / 10).toFixed(1); // identical rounding in every implementation
|
|
62
|
+
const paths = [];
|
|
63
|
+
for (let i = 0; i < total; i++) {
|
|
64
|
+
const last = i === total - 1 && record.badge, closedCore = i < 2 ? record.buyers >= 5 : true;
|
|
65
|
+
const width = last ? 2.8 : 1.6 + ((cw[i % N] >> 6) & 3) * 0.2;
|
|
66
|
+
let d = '', pen = false;
|
|
67
|
+
for (let k = 0; k <= 256; k++) {
|
|
68
|
+
const th = (k / 256) * Math.PI * 2 - Math.PI / 2, sector = Math.floor((((th / (Math.PI * 2)) * N) + N) % N), s = cw[sector];
|
|
69
|
+
const ending = (s & 31) % total === i, openGap = !closedCore && th > Math.PI * 0.12 && th < Math.PI * 0.5;
|
|
70
|
+
const r = radiusAt(i, th), x = cx + Math.cos(th) * r * 0.84, y = cy + Math.sin(th) * r;
|
|
71
|
+
if (ending || openGap) { pen = false; continue; }
|
|
72
|
+
d += `${pen ? 'L' : 'M'}${f1(x)} ${f1(y)}`; pen = true;
|
|
73
|
+
}
|
|
74
|
+
paths.push(`<path d="${d}" stroke="${last ? gold : ink}" stroke-width="${f1(width)}"/>`);
|
|
75
|
+
}
|
|
76
|
+
// A lost dispute is a scar: a cut masked out of the ridges, so it shows on any background.
|
|
77
|
+
let scars = '';
|
|
78
|
+
for (let d = 0; d < record.disputes_lost; d++) {
|
|
79
|
+
const th = ((cw[(d * 11) % N] / 255) * Math.PI * 2) - Math.PI / 2, ra = r0 + gap * 2.5, rb = r0 + gap * Math.min(total - 0.5, 7.5);
|
|
80
|
+
scars += `<path d="M${f1(cx + Math.cos(th) * ra * 0.84)} ${f1(cy + Math.sin(th) * ra)}L${f1(cx + Math.cos(th + 0.08) * rb * 0.84)} ${f1(cy + Math.sin(th + 0.08) * rb)}" stroke="#000" stroke-width="7.2" stroke-linecap="round"/>`;
|
|
81
|
+
}
|
|
82
|
+
const mask = scars ? `<mask id="scars"><rect width="${W}" height="${W}" fill="#fff"/>${scars}</mask>` : '';
|
|
83
|
+
const desc = `Word Pass fingerprint: ${total} ridges from ${record.delivered} delivered orders; core ${record.buyers >= 5 ? 'closed' : 'open'} (${record.buyers} buyers); ${record.disputes_lost} disputes lost; ${record.badge ? 'badge ' + record.badge : 'no badge yet'}. Codeword ${hex(cw)}.`;
|
|
84
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${W} ${W}" width="${size}" height="${size}" role="img" aria-label="Word Pass fingerprint"><title>Word Pass fingerprint</title><desc>${desc}</desc>${background === 'none' ? '' : `<rect width="${W}" height="${W}" fill="${background}"/>`}${mask}<g fill="none" stroke-linecap="round" stroke-linejoin="round"${scars ? ' mask="url(#scars)"' : ''}>${paths.join('')}</g></svg>`;
|
|
85
|
+
}
|
package/src/index.d.ts
CHANGED
|
@@ -43,3 +43,16 @@ export function pqKid(publicKeyB64u: string): Promise<string>;
|
|
|
43
43
|
export type RememberCheck = { verdict: 'consistent' | 'inconsistent' | 'unreachable'; origin: string; say: string; checks: { document: { ed25519: boolean; ml_dsa_65: boolean | null; pq_kid_matches: boolean; kid_in_issuer_keys: boolean }; chain: { days: number; recomputed_head: string; document_head: string | null; head_matches: boolean; broken_at: string | null; published_heads_match: boolean }; links: { with_signature: number; ed25519_valid: number; ml_dsa_65_valid: number | null; failures: string[] }; ml_dsa_65_checked: boolean } | null };
|
|
44
44
|
export function checkRemember(origin?: string, opts?: { fetch?: typeof fetch }): Promise<RememberCheck>;
|
|
45
45
|
export function checkHead(kept: string, origin?: string, opts?: { fetch?: typeof fetch }): Promise<{ verdict: 'consistent' | 'inconsistent' | 'unreachable'; day?: string; kept?: string; recomputed?: string | null; published?: string | null; days_since?: number; say: string }>;
|
|
46
|
+
|
|
47
|
+
// Fingerprint (SPEC section 15): a print no other agent can carry, provably.
|
|
48
|
+
export const FINGERPRINT: { n: 64; k: 10; d: 55; field: string; generator: string };
|
|
49
|
+
export function rsEncode(msg: Uint8Array): Uint8Array;
|
|
50
|
+
export type Fingerprint = { seed: string; codeword: string };
|
|
51
|
+
export function fingerprintOf(input: { origin: string; agent: string; first_leaf: string }): Promise<Fingerprint>;
|
|
52
|
+
/** Symbols that differ, 0 … 64; never below 55 for two different seeds. */
|
|
53
|
+
export function fingerprintDistance(a: Fingerprint | string, b: Fingerprint | string): number;
|
|
54
|
+
export type FingerprintRecord = { delivered: number; buyers: number; badge: string | null; disputes_lost: number };
|
|
55
|
+
export function recordOf(pass: Record<string, unknown>): FingerprintRecord;
|
|
56
|
+
export function ridgesOf(delivered: number): number;
|
|
57
|
+
/** The reference drawing, the same bytes for the same input. */
|
|
58
|
+
export function fingerprintSvg(codeword: Fingerprint | string, record?: FingerprintRecord, opts?: { size?: number; ink?: string; gold?: string; background?: string }): string;
|
package/src/index.js
CHANGED
|
@@ -360,3 +360,6 @@ export function sayPass(p, by = p.issuer) {
|
|
|
360
360
|
|
|
361
361
|
// Remember: the chain of daily roots, checked without the issuer (SPEC section 14).
|
|
362
362
|
export { GENESIS, headOf, recomputeChain, verifyLink, verifyRememberDocument, checkRemember, checkHead, loadMlDsa, verifyMlDsa, pqKid } from './remember.js';
|
|
363
|
+
|
|
364
|
+
// Fingerprint (SPEC section 15): a print no other agent can carry, provably; the reference drawing.
|
|
365
|
+
export { FINGERPRINT, rsEncode, fingerprintOf, fingerprintDistance, fingerprintSvg, recordOf, ridgesOf } from './fingerprint.js';
|