signet-protocol 1.11.0 → 1.11.2

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
@@ -27,23 +27,36 @@ Signet solves this without centralised data collection. A professional verifies
27
27
  npm install signet-protocol
28
28
  ```
29
29
 
30
- Display a verification badge for any Nostr user (Level 1 — a weekend to integrate):
30
+ Requires Node ≥22. ESM only (no CommonJS `require` export).
31
+
32
+ Display a verification badge for any Nostr user (Level 1: a weekend to integrate):
31
33
 
32
34
  ```typescript
33
- import { computeBadge, buildBadgeFilters, meetsMinimumTier } from 'signet-protocol';
35
+ import { computeBadge, buildBadgeFilters, fetchFromRelay, meetsMinimumTier } from 'signet-protocol';
34
36
 
35
37
  const filters = buildBadgeFilters(['<hex-pubkey>']);
36
- const events = await fetchFromRelay(filters);
37
- const badge = computeBadge('<hex-pubkey>', events, { verifySignatures: true });
38
+ const events = await fetchFromRelay('wss://relay.example.com', filters);
39
+ const badge = await computeBadge('<hex-pubkey>', events, { verifySignatures: true });
38
40
 
39
41
  // badge.tier => 3, badge.score => 106, badge.displayLabel => "Verified (Tier 3)"
42
+ // (illustrative values; badge.score is a Level 1 approximation, see the notes below)
40
43
  if (meetsMinimumTier(badge, 2)) {
41
44
  // allow posting in verified-only community
42
45
  }
43
46
  ```
44
47
 
48
+ `fetchFromRelay(relayUrl, filters)` is the package's own minimal relay client and returns `Promise<NostrEvent[]>`. Any other Nostr client works too: pass `buildBadgeFilters` output as the REQ filters and hand the resulting events to `computeBadge`. `computeBadge` is async and returns `Promise<BadgeInfo>`.
49
+
50
+ Three things to know before you copy this:
51
+
52
+ - `verifySignatures` defaults to `false`. A call such as `computeBadge(pubkey, events)` checks no signatures and trusts every event it is given, so keep `{ verifySignatures: true }` unless the events were already verified. `fetchFromRelay` drops events with an invalid signature or id by default; events from any other source are not checked for you.
53
+ - `badge.score` is on the 0-200 Signet IQ scale but is a Level 1 approximation of `computeTrustScore(...).score`: it ignores account age and identity bridges and applies none of the per-type caps (1 professional verification, 3 in-person vouches, 5 online vouches). Use `computeTrustScore` when you need the full score.
54
+ - `computeBadge` does not check who issued a Tier 3 or Tier 4 credential, and `buildBadgeFilters` does not fetch verifier or revocation events. Any key can sign an event that carries `tier: 3`, and it will count. To require that the issuer is an activated, unrevoked verifier, fetch the verifier credentials, their vouches and revocations yourself, apply `checkCrossVerification(...).activated` and `isVerifierRevoked(...)`, and drop other issuers' Tier 3 and Tier 4 credentials before calling `computeBadge`. [llms.txt](llms.txt) has the steps.
55
+
45
56
  See [Signet in 5 Minutes](docs/signet-in-5-minutes.md) for a full developer overview, or the [full flow example](examples/full-flow.ts) for all 4 tiers, trust scoring, policies, and verifier lifecycle.
46
57
 
58
+ For LLM and agent tooling, [llms.txt](llms.txt) is a concise API overview and [llms-full.txt](llms-full.txt) adds the full protocol and voting specifications and the developer guides in one file. Both ship in the npm package, as do `spec/`, `examples/` and the four `docs/` pages linked from this README and `llms.txt` (`signet-in-5-minutes.md`, `implementation-levels.md`, `private-vault-v1.md` and `bot-ownership-v1.md`), so those relative links resolve inside `node_modules/signet-protocol/`. Other files under `docs/`, `legal/` and `tests/` stay in the repository only.
59
+
47
60
  ## Protocol
48
61
 
49
62
  The full specification is at [`spec/protocol.md`](spec/protocol.md).
@@ -62,9 +75,8 @@ All identity attestations use a single NIP-VA kind (31000), differentiated by `t
62
75
  | 31000 | `revocation` | Verifier Revocation | Community confirms challenge, revokes verifier |
63
76
  | 31000 | `identity-bridge` | Identity Bridge | Link two keypairs via ring signature |
64
77
  | 31000 | `delegation` | Delegation | Guardian or agent delegation with scoped permission |
65
- | 30482 | — | Election | Voting extension: define an election |
66
- | 30483 | — | Ballot | Voting extension: cast an anonymous ballot |
67
- | 30484 | — | Election Result | Voting extension: publish tallied result |
78
+
79
+ Voting extension: kinds 30482 (election), 30483 (ballot) and 30484 (election result) are specified in [`spec/voting.md`](spec/voting.md) and implemented in the separate [`nostr-anon-vote`](https://github.com/forgesworn/nostr-anon-vote) package, not in `signet-protocol`.
68
80
 
69
81
  ### Crypto Stack
70
82
 
@@ -73,8 +85,8 @@ Layer 1: Schnorr (secp256k1) — zero new dependencies
73
85
  Credential signing, Merkle selective disclosure,
74
86
  ring signatures for issuer privacy, MuSig2
75
87
 
76
- Layer 2: Bulletproofs (secp256k1) — age range proofs
77
- ~700 byte proofs, no trusted setup
88
+ Layer 2: Pedersen range proofs (secp256k1): age range proofs
89
+ via @forgesworn/range-proof, no trusted setup
78
90
 
79
91
  Layer 3: General-purpose ZK (future, if needed)
80
92
  Complex threshold proofs, recursive composition
@@ -82,7 +94,7 @@ Layer 3: General-purpose ZK (future, if needed)
82
94
 
83
95
  ## Status
84
96
 
85
- **v1.0.0** — spec complete, TypeScript library implemented, reference apps functional. Seeking community feedback.
97
+ **Status:** spec complete, TypeScript library implemented, reference apps functional. Seeking community feedback.
86
98
 
87
99
  All identity attestations use kind 31000 (NIP-VA, Verifiable Attestations), differentiated by `type` tag.
88
100
 
@@ -0,0 +1,43 @@
1
+ # Draft bot ownership profile
2
+
3
+ Unstable. Import from `signet-protocol/experimental`; nothing here is exported
4
+ from the package root, and any of it may change without a deprecation period.
5
+
6
+ This unreleased profile uses nostr-attestations kind 31000 with type
7
+ `bot-ownership`. Its replaceable d-tag is `bot-ownership:<bot pubkey>`, p-tag is
8
+ the bot, and the event author is the selected owner persona. It cannot replace
9
+ a credential or vouch. Ownership never grants the bot the owner's trust.
10
+
11
+ A live claim has `valid_from` equal to `created_at`, matching `valid_to` and
12
+ `expiration`, and content `{ "v": 1, "label": "…" }` (100 characters maximum).
13
+ Labels refuse C0/C1 controls, DEL, zero-width and bidi marks (LRM, RLM, ALM),
14
+ line/paragraph separators and bidi embeddings, overrides and isolates, lone
15
+ surrogates, and every other Unicode format or default-ignorable code point,
16
+ except the narrow in-context forms emoji sequences need: the zero-width
17
+ joiner between emoji elements, the VS15/VS16 variation selectors right after
18
+ an emoji or a keycap base, and the England/Scotland/Wales RGI subdivision
19
+ flag tag sequences (no other tag sequence, and no hidden tag payload).
20
+ The event supplies principal, agent, issue date and expiry without requiring a
21
+ raw-hash signature: ordinary NIP-46 sign_event works on hardware. Revocation is
22
+ the existing same-address attestation with `status=revoked` and empty content.
23
+
24
+ Builders return unsigned events and never publish. Product integration must ask
25
+ for creation consent every time, sign with the chosen persona, and keep the
26
+ signed claim private until publication is explicitly chosen. Hardware-derived
27
+ bots must be registered on the device; this module does not register them.
28
+
29
+ Default expiry is 30 days, permitted creation range 1–90 days. Readers apply the
30
+ earliest of signed expiry, 90 days and an optional stricter per-bot ceiling,
31
+ with five minutes of clock tolerance. Lapsed is different from revoked.
32
+ Renewal becomes due with ten days left; retry at most daily. The scheduling
33
+ helper never signs: local policy and hardware approvals remain in force.
34
+
35
+ Consumers pin the expected owner and bot, verify signatures, and select the
36
+ newest same-address event while retaining their previously seen high-water mark.
37
+ A stale ownership claim must not resurrect a newer revocation. This module
38
+ validates one event; it does not implement relay freshness or fetch ordering.
39
+ `readBotOwnershipSync` applies identical validation for synchronous storage and
40
+ transport codecs; `readBotOwnership` is the asynchronous form of the same check.
41
+
42
+ Protocol review, app lifecycle/UI, isolated bots contacts, downstream adoption,
43
+ and hardware acceptance are still required before release.
@@ -0,0 +1,235 @@
1
+ # Signet Implementation Levels
2
+
3
+ A guide for Nostr client developers integrating the Signet identity protocol. There are three progressive integration levels. Start at Level 1 and add depth as your use case demands.
4
+
5
+ ## Summary
6
+
7
+ | Level | Effort | Event Kinds | New Crypto | What Users Get |
8
+ |-------|--------|-------------|------------|----------------|
9
+ | 1 — Read Trust Badges | ~1 weekend | 31000 (credential, vouch) | None | Tier badges and Signet Score on profiles |
10
+ | 2 — Issue Vouches | A few days | 31000, 30078 | None | Peer vouching, Tier 2 networks, policy checks |
11
+ | 3 — Full Protocol | Weeks to months | 31000, 30078, 30482-30484 | Merkle, Pedersen range proofs, ring signatures | Professional verification, ZK proofs, elections |
12
+
13
+ ---
14
+
15
+ ## Level 1 — Read Trust Badges
16
+
17
+ **Effort:** A weekend
18
+ **Event kinds:** 31000 (attestation — filter by `type` tag: `credential`, `vouch`)
19
+ **New crypto required:** None — Nostr already verifies Schnorr signatures
20
+
21
+ This is the NIP-05 equivalent of Signet integration. You read one event kind (filtered by type tag), call one function, and display a badge. No cryptographic work on your part beyond what your Nostr client already does.
22
+
23
+ ### What to build
24
+
25
+ 1. Subscribe to kind `31000` events with `type` tags `credential` and `vouch` for the pubkeys you want to display.
26
+ 2. Pass the events to `computeBadge()`.
27
+ 3. Show the result on the user's profile.
28
+
29
+ ### Installation
30
+
31
+ ```bash
32
+ npm install signet-protocol
33
+ ```
34
+
35
+ ### Code example
36
+
37
+ ```typescript
38
+ import { computeBadge, buildBadgeFilters, fetchFromRelay } from 'signet-protocol';
39
+
40
+ const pubkey = '<hex-pubkey>';
41
+
42
+ // 1. Fetch events from a relay (fetchFromRelay is the package's own client; any Nostr client works)
43
+ const filters = buildBadgeFilters([pubkey]);
44
+ // The filters request kind 31000 with credential and vouch type tags.
45
+ const events = await fetchFromRelay('wss://relay.example.com', filters);
46
+
47
+ // 2. Compute badge from the events you receive
48
+ const badge = await computeBadge(pubkey, events, { verifySignatures: true }); // async: returns Promise<BadgeInfo>
49
+
50
+ // 3. Display
51
+ if (badge.isVerified) {
52
+ console.log(badge.tierLabel, badge.score); // replace with your own rendering
53
+ }
54
+ ```
55
+
56
+ The `badge` object includes:
57
+
58
+ - `badge.isVerified` — boolean, true if the pubkey has any Signet verification
59
+ - `badge.tier` — number 1–4, the highest verified tier
60
+ - `badge.tierLabel` — human-readable string (e.g. `"Vouched"`, `"Verified"`, `"Verified (Child Safety)"`)
61
+ - `badge.score`: integer 0–200 on the Signet IQ scale (100 = government standard), derived from credentials and vouches only. It is a Level 1 approximation of `computeTrustScore(...).score`: it ignores account age and identity bridges and applies no per-type caps
62
+
63
+ ### Trust tiers at a glance
64
+
65
+ | Tier | Meaning |
66
+ |------|---------|
67
+ | 1 | Self-declared — the user has made identity claims |
68
+ | 2 | Web-of-trust — other users have vouched for this identity |
69
+ | 3 | Professional adult — verified by a recognised professional body |
70
+ | 4 | Professional adult + child safeguarding — extended professional verification |
71
+
72
+ ### What you do NOT need to handle at this level
73
+
74
+ - Generating or validating zero-knowledge proofs
75
+ - Nullifier computation
76
+ - Merkle trees
77
+ - Any cryptographic primitives beyond standard Nostr signature verification
78
+
79
+ Two things Level 1 does not do for you:
80
+
81
+ - Signature checking is opt-in. `verifySignatures` defaults to `false`, so `computeBadge(pubkey, events)` with no options trusts every event it is given. Keep `{ verifySignatures: true }` unless the events were already verified (`fetchFromRelay` drops events with an invalid signature or id by default; events from other sources are not checked for you).
82
+ - Issuer standing is not checked. `computeBadge` does not look at who signed a Tier 3 or Tier 4 credential, and `buildBadgeFilters` fetches no verifier or revocation events, so any key can sign an event that carries `tier: 3` and it will count. To require an activated, unrevoked verifier, fetch the verifier credentials, their vouches and revocations yourself, apply `checkCrossVerification(...).activated` and `isVerifierRevoked(...)`, and drop other issuers' Tier 3 and Tier 4 credentials before calling `computeBadge` (see `llms.txt`).
83
+
84
+ ---
85
+
86
+ ## Level 2 — Issue Vouches
87
+
88
+ **Effort:** A few days
89
+ **Event kinds:** 31000 (attestation — credential, vouch types), 30078 (policy)
90
+ **New crypto required:** None
91
+
92
+ Level 2 adds the ability for your users to vouch for each other and for your client to respect community policies. This is the viral layer of the protocol — peer vouching is what builds Tier 2 trust networks and creates network effects across clients.
93
+
94
+ ### What to build
95
+
96
+ Everything from Level 1, plus:
97
+
98
+ 1. UI for a user to select a contact and publish a kind `31000` vouch attestation (with `['type', 'vouch']` tag).
99
+ 2. Read kind `30078` (NIP-78 app-specific data) policies from community relays and check whether a pubkey meets the policy threshold before allowing sensitive actions.
100
+
101
+ ### Creating a vouch
102
+
103
+ ```typescript
104
+ import { createVouch } from 'signet-protocol';
105
+
106
+ // createVouch takes the voucher's private key and a VouchParams object.
107
+ // It returns a signed Nostr event ready to publish.
108
+ const vouchEvent = await createVouch(myPrivkey, {
109
+ subjectPubkey: theirPubkey,
110
+ method: 'in-person',
111
+ context: 'I have met this person in person and verified their identity.',
112
+ voucherTier: 2,
113
+ voucherScore: 106,
114
+ });
115
+
116
+ // Publish to your relay as a standard Nostr event.
117
+ await publishEvent(vouchEvent);
118
+ ```
119
+
120
+ ### Parsing incoming vouches
121
+
122
+ ```typescript
123
+ import { parseVouch, countQualifyingVouches, hasEnoughVouches } from 'signet-protocol';
124
+
125
+ // Parse a raw Nostr event into a structured vouch object
126
+ const vouch = parseVouch(rawEvent);
127
+
128
+ // Count how many qualifying vouches a pubkey has received
129
+ const count = countQualifyingVouches(allVouchEvents, pubkey);
130
+
131
+ // Check whether a pubkey clears the threshold required for Tier 2
132
+ const qualifies = hasEnoughVouches(allVouchEvents, pubkey, 3);
133
+ ```
134
+
135
+ Reference: `src/vouches.ts`
136
+
137
+ ### Checking policy compliance
138
+
139
+ Policies (kind `30078`, NIP-78) let community operators define trust thresholds for specific actions — for example, requiring a minimum score of 40 before a user can post to a paid relay, or requiring Tier 2 before joining a group.
140
+
141
+ ```typescript
142
+ import { parsePolicy, checkPolicyCompliance, PolicyChecker } from 'signet-protocol';
143
+
144
+ // Parse a policy event
145
+ const policy = parsePolicy(policyEvent);
146
+
147
+ // One-shot check: does this user meet the policy?
148
+ const result = checkPolicyCompliance(policy, userTier, userScore);
149
+ if (!result.allowed) {
150
+ showError(`This action requires Tier ${result.requiredTier} or higher.`);
151
+ }
152
+
153
+ // For repeated checks across many pubkeys, use PolicyChecker (more efficient)
154
+ const checker = new PolicyChecker(policyEvent);
155
+ const adultResult = checker.checkAdult(userTier, userScore);
156
+ ```
157
+
158
+ Reference: `src/policies.ts`
159
+
160
+ ### What you do NOT need to handle at this level
161
+
162
+ - Zero-knowledge proofs of any kind
163
+ - Professional body verification flows
164
+ - Two-credential ceremonies
165
+ - Nullifiers or document hashing
166
+
167
+ ---
168
+
169
+ ## Level 3 — Full Protocol
170
+
171
+ **Effort:** Weeks to months
172
+ **Event kinds:** 31000 (all attestation types), 30078 (policies), 30482-30484 (voting)
173
+ **New crypto required:** Merkle trees, Pedersen range proofs (age range proofs), linkable ring signatures
174
+
175
+ Level 3 is full reference implementation. This is appropriate for clients that want to act as professional verifiers, run elections, or issue and receive Tier 3/4 credentials with zero-knowledge proofs. Elections themselves (kinds 30482–30484) live in the separate `nostr-anon-vote` package, not in `signet-protocol`.
176
+
177
+ ### What to build (beyond Level 2)
178
+
179
+ - **Two-credential ceremony:** Professional verifiers issue a Natural Person credential (kind `31000` with `type: credential`, nullifier and Merkle root) and an anonymous Persona credential simultaneously. The Natural Person credential binds to a document-based nullifier (`SHA-256(docType || country || docNumber || salt)`) to prevent duplicate identity claims. The Persona credential reveals only an age range, not the full identity.
180
+ - **Merkle selective disclosure:** Verified attributes are stored as private leaves of a Merkle tree. Only the root is published on-chain. Users disclose individual leaves with inclusion proofs when required.
181
+ - **Age range proofs:** Pedersen range proofs prove that a user is within an age range without revealing their exact date of birth.
182
+ - **Professional verifier onboarding:** Verifier accounts (kind `31000` with `type: verifier`) are backed by recognised professional bodies (law societies, medical boards, notary commissions). The protocol has no central authority — trust anchors are these external bodies.
183
+ - **Ring signatures and elections:** The voting extension (kinds `30482`–`30484`) uses linkable ring signatures for anonymous voting with double-spend prevention. It is specified in `spec/voting.md` and implemented in the separate `nostr-anon-vote` package; `signet-protocol` ships only the linkable ring signature primitives (`lsagSign`, `lsagVerify`, `computeKeyImage`).
184
+ - **Guardian delegation:** Delegation attestations (kind `31000` with `type: delegation`) with scopes (`full`, `activity-approval`, `content-management`, `contact-approval`) support family structures and guardian-linked sub-accounts.
185
+ - **Revocation and lifecycle:** Revocation attestations (kind `31000` with `type: revocation`) and credential chains using `supersedes`/`superseded-by` tags handle name changes, document renewal, and tier upgrades.
186
+ - **Identity bridging:** Identity bridge attestations (kind `31000` with `type: identity-bridge`) link verified identities across protocols or jurisdictions.
187
+ - **Anomaly detection:** The library includes heuristics for detecting suspicious vouching patterns (e.g. vouch rings, rapid bulk vouching).
188
+
189
+ ### Key modules in signet-protocol
190
+
191
+ All modules are re-exported from the package root. You can also import them directly for tree-shaking.
192
+
193
+ | Module | Source | Responsibility |
194
+ |--------|--------|----------------|
195
+ | Credentials | `src/credentials.ts` | Issue, parse, verify credentials including Merkle roots and nullifiers |
196
+ | Ring signatures | `src/ring-signature.ts` | Ring signature generation and verification (via `@forgesworn/ring-sig`) |
197
+ | Range proofs | `src/range-proof.ts` | Pedersen range proofs for age verification (via `@forgesworn/range-proof`) |
198
+ | Merkle trees | `src/merkle.ts` | Build attribute trees, generate and verify inclusion proofs |
199
+ | Signet Score | `src/trust-score.ts` | Weighted signal aggregation, 0–200 Signet Score computation |
200
+ | Anomaly detection | `src/anomaly.ts` | Detect vouch ring attacks and other manipulation patterns |
201
+ | Jurisdictions | `jurisdiction-kit` | Jurisdiction-specific rules for document types and professional bodies |
202
+
203
+ All modules are re-exported from the package root (`signet-protocol`). Import from the root entry point:
204
+
205
+ ### Event kind reference
206
+
207
+ All Signet identity attestations use a single generic kind, differentiated by `type` tag:
208
+
209
+ | Kind | Type Tag | Name | Description |
210
+ |------|----------|------|-------------|
211
+ | 31000 | `credential` | Credential | Identity claim with optional Merkle root and nullifier |
212
+ | 31000 | `vouch` | Vouch | Peer attestation of another pubkey's identity |
213
+ | 31000 | `verifier` | Verifier | Professional verifier registration |
214
+ | 31000 | `challenge` | Challenge | Challenge against a verifier's legitimacy |
215
+ | 31000 | `revocation` | Revocation | Credential or vouch revocation |
216
+ | 31000 | `identity-bridge` | Identity Bridge | Cross-protocol or cross-jurisdiction identity link |
217
+ | 31000 | `delegation` | Delegation | Guardian or organisational delegation with scopes |
218
+ | 30078 | — | Policy | Community trust threshold definitions (NIP-78) |
219
+ | 30482 | — | Election | Voting election definition (separate NIP) |
220
+ | 30483 | — | Ballot | Anonymous signed ballot |
221
+ | 30484 | — | Election Result | Tallied election result |
222
+
223
+ **Note:** Kind 31000 is the NIP-VA (Verifiable Attestations) kind. The `type` tag differentiates attestation types within the single kind.
224
+
225
+ ### Where to start
226
+
227
+ The `signet-protocol` npm package contains everything. The protocol specification at `spec/protocol.md` is the authoritative source of truth for all behaviour.
228
+
229
+ ---
230
+
231
+ ## Choosing your level
232
+
233
+ Start at Level 1 unless you have a specific reason to go further. Most social clients only need Level 1 — displaying trust context on profiles. Level 2 is appropriate if your community has a peer moderation culture. Level 3 is for infrastructure builders: relay operators, professional verification portals, and election platforms.
234
+
235
+ You can ship Level 1 in a weekend and upgrade later. The three levels are strictly additive — nothing you build at Level 1 needs to be undone to reach Level 2 or 3.
@@ -0,0 +1,104 @@
1
+ # Private vault checkpoint v1 (staged, not released)
2
+
3
+ Unstable. Import from `signet-protocol/experimental`; nothing here is exported
4
+ from the package root, and any of it may change without a deprecation period.
5
+
6
+ Keys derive flat from the tree root using `vaultPurpose`; index means rotation.
7
+ Legacy Natural Person rails remain permanently readable. No writer is switched
8
+ until both the app and Sapwood can restore the new dataset.
9
+
10
+ The existing v2 AES-GCM/NIP-44 envelope is retained. Vault keys wrap content keys
11
+ and sign only control events below 12 KiB of UTF-8 content. Every chunk is sealed
12
+ to the vault key but signed by an installation device key. A checkpoint authorises
13
+ that device and lists exact chunk event IDs, encrypted-content SHA-256 hashes,
14
+ byte lengths and order. The complete plaintext SHA-256 is the revision. Recovery discovers and merges all
15
+ device heads rather than selecting one global latest snapshot, so concurrent
16
+ offline writers cannot replace each other. The reader fails closed beyond 16
17
+ heads instead of dropping devices; head compaction is future work. Readers
18
+ verify signatures, scope, bounds, sequence, every chunk and the revision before
19
+ returning any data. Missing chunks never mean an empty dataset.
20
+
21
+ All events use existing kind 30078. Each installation retains its own checkpoint under
22
+ `vaultCheckpointTag(author, publisherDevicePubkey)`;
23
+ chunks use a content-derived tag and are fetched by exact event ID. This reuses
24
+ the existing event kind rather than allocating a new kind. Content-addressed
25
+ chunk replacement cannot alter an already signed checkpoint reference. No public
26
+ tags name the dataset or connect device chunks to the vault author. Routing tags
27
+ are opaque, not secret: knowing a vault pubkey lets an observer compute its tag.
28
+
29
+ Head discovery reads only kind-30078 events by the vault author whose single
30
+ d-tag has the checkpoint shape (32 lowercase hex characters); other events by
31
+ the vault key are ignored (by the relay reader and again by recovery) before
32
+ any limit applies and are never decrypted. The
33
+ shape is necessary, not sufficient, because the publisher is encrypted: the
34
+ vault key must sign no other kind-30078 event with a 32-hex d-tag, or that
35
+ event is read as a malformed head and the read is unusable.
36
+
37
+ Checkpoints dated more than `VAULT_CLOCK_TOLERANCE_SECONDS` (300) after the
38
+ reader's clock are rejected; every reader takes `now` as a parameter. A device
39
+ whose clock ran ahead therefore cannot outrank its own later honest heads, but
40
+ its future-dated head is ignored until the clock catches up. Of two events with
41
+ the same created_at under one tag, the lower event ID is the newer (NIP-01).
42
+
43
+ Checkpoint plaintext shape is exported as `VaultCheckpoint`. It contains purpose,
44
+ rotation, sequence, revision, authorised device pubkeys and ordered chunk refs.
45
+ `nextRotation` is optional and, when present, exactly `rotation + 1`; any other
46
+ value makes the checkpoint invalid. Recovery begins at zero. A rotation
47
+ must not become canonical before the new copy is fetched, decrypted and verified.
48
+
49
+ Rotation is a revocation boundary. Recovery reads only the newest rotation:
50
+ the first rotation n whose successor n + 1 holds no authentic checkpoint-shaped
51
+ event. Before reading rotation n it always probes rotation n + 1 (one extra
52
+ resolve and query). Once n + 1 exists, rotation n is never queried, opened,
53
+ counted or merged, so a holder of a retired key can neither inject or tombstone
54
+ data (backdated or not, under any publisher tag) nor block recovery with
55
+ malformed, over-cap or junk-tag events, nor hide the successor by overwriting
56
+ the pointer head. The newest rotation is read strictly; a pointer there to a
57
+ rotation with no authentic checkpoint is damage (`unusable`). The rotating
58
+ writer must carry every head's state into the new rotation before publishing
59
+ there; nothing written under an older rotation is recovered. Sequence floors
60
+ apply to the rotation that is read.
61
+
62
+ A rotation revokes only a holder of THAT rotation's key, not the tree root: a
63
+ holder of the root key can derive every rotation regardless. A caller that
64
+ walks forward and finds every relay withholding the true newest rotation's
65
+ successor cannot tell that from a vault that never rotated further, so any
66
+ caller tracking the highest rotation it has already reached must treat a walk
67
+ that lands below it as a rollback, not as ready.
68
+
69
+ Rotation write order matters. The rotating writer first publishes the merged
70
+ state under rotation n + 1 (every chunk, then the checkpoint) and only then
71
+ declares `nextRotation` in rotation n. A pointer published first makes reads
72
+ `unusable` until rotation n + 1 is complete, because a pointer to a rotation
73
+ with no authentic checkpoint is damage. Once any authentic checkpoint-shaped
74
+ event exists under rotation n + 1, rotation n is never read: if that event (or
75
+ its chunks) is broken, the read is fail-closed `unusable` with no fallback to
76
+ rotation n. Recovery walks at most 32 rotations (`MAX_ROTATION_HOPS`); a vault
77
+ rotated more often reads `unusable`.
78
+
79
+ Restore results distinguish absent, unavailable, unusable and ready. Legacy may
80
+ be the canonical source only when the vault is absent, never merely because a
81
+ relay is offline or a new checkpoint fails to decrypt. A previously observed
82
+ sequence is a rollback floor. A fresh device cannot detect a relay withholding
83
+ all newer state without another trusted source; do not claim otherwise.
84
+
85
+ `createVaultRelayReader` queries at most eight `wss://` relays (plain `ws://`
86
+ only for `localhost` and `127.0.0.1`, matching `RelayClient`). A checkpoint
87
+ query, empty or not, completes only when every queried relay returned EOSE and
88
+ none failed: one relay's "nothing" is not evidence of absence, and one relay's
89
+ heads may omit a device head that another relay holds. Otherwise the query
90
+ throws a `VaultRelayError` whose `failedRelays` names the relays that failed,
91
+ and recovery returns `{ state: 'unavailable', failedRelays }`. A permanently
92
+ dead configured relay therefore keeps reads `unavailable` until it is removed
93
+ from the relay set; that is deliberate, since `unavailable` is safe and a false
94
+ `absent` is not. A chunk fetch by exact ID succeeds once any relay returns it
95
+ (its hash is pinned by the checkpoint); "not found" again needs every relay.
96
+ Each relay's events are re-checked against the requested author, kind, ID and
97
+ d-tag, checkpoint queries also drop events without a checkpoint-shaped d-tag,
98
+ and duplicates are removed, all before a per-relay cap of 128; a relay over the
99
+ cap counts as failed.
100
+
101
+ The SDK provides stateless read/validation and forward rotation traversal.
102
+ Writer scheduling, dataset import and durable canonical markers belong to
103
+ consumers. The candidate Signet app implements scheduling and confirmed-copy
104
+ state; this does not establish equivalent Sapwood behaviour.
@@ -0,0 +1,181 @@
1
+ # Signet in 5 Minutes
2
+
3
+ Signet is an open-source, decentralised identity verification protocol for Nostr. It lets users prove claims
4
+ about who they are — human, professional, adult, organisation — without exposing personal data. Verification
5
+ is anchored to professional bodies (law societies, medical boards, notary commissions) rather than any central
6
+ authority. Trust accumulates as a continuous Signet Score (0–200, where 100 represents the current government standard) assembled from weighted signals, and is expressed as a tier badge any Nostr client can display.
7
+
8
+
9
+ ## How Trust Flows
10
+
11
+ ```
12
+ Person
13
+ |
14
+ | signs a kind 31000 self-declaration (`type: credential`)
15
+ v
16
+ Tier 1 — Self-declared
17
+ |
18
+ | three or more peers sign kind 31000 vouches (`type: vouch`)
19
+ v
20
+ Tier 2 — Web-of-trust
21
+ |
22
+ | licensed professional issues kind 31000 credential (`type: credential`)
23
+ | with zero-knowledge age proof and document nullifier
24
+ v
25
+ Tier 3 — Professionally verified (adult)
26
+ |
27
+ | same ceremony, scope extended to child supervision
28
+ v
29
+ Tier 4 — Professionally verified (adult + child)
30
+ |
31
+ v
32
+ Trust signals accumulated (professional verif, vouches, account age, identity bridges)
33
+ |
34
+ v
35
+ Signet Score computed (0-200, where 100 = government standard)
36
+ |
37
+ v
38
+ Badge displayed by client e.g. "Verified (Tier 3) — Signet Score 106"
39
+ ```
40
+
41
+
42
+ ## The Four Tiers
43
+
44
+ | Tier | Name | Who issues | What it proves | Score contribution |
45
+ |------|-----------------------------|-----------------------------------------|-----------------------------------------------------|--------------------|
46
+ | 1 | Self-declared | Subject themselves | "I exist and I own this key" | — |
47
+ | 2 | Web-of-trust | Peers (kind 31000, `type: vouch`) | Enough known people can vouch for this identity | +2–8 per vouch |
48
+ | 3 | Professionally verified | Licensed professional (kind 31000, `type: credential`) | Government document seen; adult age confirmed | +40 |
49
+ | 4 | Professional + child safety | Licensed professional (kind 31000, `type: credential`) | Tier 3 plus authorisation to interact with minors | +40 |
50
+
51
+ Tier 2 requires at least 3 vouches from Tier 2+ peers (configurable per community via kind 30078 (NIP-78) policy).
52
+ Tiers 3 and 4 are issued in a two-credential ceremony: one Natural Person credential (carries document
53
+ nullifier and Merkle root) and one anonymous Persona credential (carries age range only, no identifying data).
54
+
55
+
56
+ ## Three Implementation Levels
57
+
58
+ ### Level 1 — Display badges (a weekend)
59
+
60
+ Read kind 31000 (`type: credential` and `type: vouch`) from relays, call `computeBadge`, show a label.
61
+ No cryptography required beyond Schnorr signature verification (optional).
62
+
63
+ ### Level 2 — Issue vouches (a few days)
64
+
65
+ Add kind 31000 (`type: vouch`) vouch issuance. Requires Schnorr signing, relay write access, and basic
66
+ kind 30078 (NIP-78) policy parsing so your client respects community verification thresholds.
67
+
68
+ ### Level 3 — Full protocol (weeks)
69
+
70
+ Issue professional credentials, manage verifier lifecycle (kind 31000 types: verifier, challenge, revocation), build
71
+ guardian delegation (kind 31000, `type: delegation`), support credential chains (supersedes / superseded-by),
72
+ and integrate the voting extension (kinds 30482–30484). The voting extension is specified in `spec/voting.md` and implemented in the separate `nostr-anon-vote` package, not in `signet-protocol`; this package ships only the linkable ring signature primitives it builds on (`lsagSign`, `lsagVerify`, `computeKeyImage`).
73
+
74
+
75
+ ## How Verification Works (Tier 3 / 4)
76
+
77
+ ```
78
+ Subject Verifier
79
+ | |
80
+ |--- presents document -->|
81
+ | |
82
+ | Verifier computes nullifier:
83
+ | SHA-256(docType || country || docNumber || salt)
84
+ | (prevents duplicate identity; document never stored)
85
+ | |
86
+ | Verifier builds Merkle tree:
87
+ | leaves = verified attributes (name, DOB, address, ...)
88
+ | only root is published on-chain
89
+ | |
90
+ | Verifier creates zero-knowledge age range proof
91
+ | (proves "subject is 18+" without revealing birth date)
92
+ | |
93
+ | Two credentials signed and published:
94
+ | kind 31000 Natural Person (nullifier + Merkle root, type: credential)
95
+ | kind 31000 Persona (age range only, anonymous, type: credential)
96
+ | |
97
+ v v
98
+ Both events published to Nostr relays
99
+ ```
100
+
101
+ Professional verifiers are themselves credentialed (kind 31000, `type: verifier`) and require cross-verification
102
+ by two other licensed professionals before becoming active. A challenge / revocation mechanism
103
+ (kind 31000 types: `challenge` and `revocation`) allows the community to remove a fraudulent verifier.
104
+
105
+
106
+ ## Event Kinds Reference
107
+
108
+ | Kind | Type tag | Purpose |
109
+ |-------|-------------------|----------------------------------------------------|
110
+ | 31000 | `credential` | Verification credential for any tier |
111
+ | 31000 | `vouch` | Peer attestation (Tier 2 building block) |
112
+ | 30078 | — | Policy: community verification requirements (NIP-78) |
113
+ | 31000 | `verifier` | Professional verifier credential |
114
+ | 31000 | `challenge` | Report a suspected fraudulent verifier |
115
+ | 31000 | `revocation` | Remove a verifier after threshold confirmations |
116
+ | 31000 | `identity-bridge` | Link two keypairs via ring signature |
117
+ | 31000 | `delegation` | Guardian / agent delegation with scoped permission |
118
+ | 30482 | — | Voting extension: define an election |
119
+ | 30483 | — | Voting extension: cast an anonymous ballot |
120
+ | 30484 | — | Voting extension: publish tallied result |
121
+
122
+ Kinds 30482–30484 are specified in `spec/voting.md` and implemented in the separate `nostr-anon-vote` package, not in `signet-protocol`.
123
+
124
+
125
+ ## Integrating at Level 1
126
+
127
+ Install the library:
128
+
129
+ ```
130
+ npm install signet-protocol
131
+ ```
132
+
133
+ Fetch events from a relay, then call `computeBadge`:
134
+
135
+ ```typescript
136
+ import { computeBadge, buildBadgeFilters, fetchFromRelay, meetsMinimumTier } from 'signet-protocol';
137
+ import type { NostrEvent } from 'signet-protocol';
138
+
139
+ // 1. Build relay filters for one or more pubkeys
140
+ const pubkeys = ['<hex-pubkey>'];
141
+ const filters = buildBadgeFilters(pubkeys);
142
+ // filters = [{ kinds: [31000], '#d': ['credential:<hex-pubkey>', 'vouch:<hex-pubkey>'] }]
143
+
144
+ // 2. Fetch events from your relay (fetchFromRelay is the package's own client; any Nostr client works)
145
+ const events: NostrEvent[] = await fetchFromRelay('wss://relay.example.com', filters);
146
+
147
+ // 3. Compute the badge
148
+ const badge = await computeBadge(pubkeys[0], events, { verifySignatures: true });
149
+
150
+ // badge.displayLabel => "Verified (Tier 3)"
151
+ // badge.score => 106 (illustrative; see the score note below)
152
+ // badge.tier => 3
153
+ // badge.vouchCount => 5
154
+
155
+ // 4. Gate a feature on minimum tier
156
+ if (meetsMinimumTier(badge, 2)) {
157
+ // allow posting in a verified-only community
158
+ }
159
+ ```
160
+
161
+ `computeBadge` accepts any mix of kind 31000 (`type: credential` and `type: vouch`) events. It handles expiry, deduplication
162
+ (one vouch counted per voucher pubkey), and optional Schnorr signature verification. The result is
163
+ a plain object — no rendering assumptions are made.
164
+
165
+ - `verifySignatures` defaults to `false`. `computeBadge(pubkey, events)` with no options checks no signatures, so keep `{ verifySignatures: true }` unless the events were already verified. `fetchFromRelay` drops events with an invalid signature or id by default; events from any other source are not checked for you.
166
+ - `badge.score` is on the 0–200 Signet IQ scale but is a Level 1 approximation of `computeTrustScore(...).score`: it ignores account age and identity bridges and applies no per-type caps (1 professional verification, 3 in-person vouches, 5 online vouches). Use `computeTrustScore` for the full score.
167
+ - `computeBadge` does not check who issued a Tier 3 or Tier 4 credential, and `buildBadgeFilters` fetches no verifier or revocation events. Any key can sign an event that carries `tier: 3` and it will count. To require an activated, unrevoked verifier, fetch the verifier credentials, their vouches and revocations yourself, apply `checkCrossVerification(...).activated` and `isVerifierRevoked(...)`, and drop other issuers' Tier 3 and Tier 4 credentials before calling `computeBadge` (see `llms.txt`).
168
+
169
+ The `buildBadgeFilters` helper returns a standard Nostr REQ filter array ready for use with any
170
+ relay client library.
171
+
172
+
173
+ ## Further Reading
174
+
175
+ - Full protocol specification: `spec/protocol.md`
176
+ - Voting extension: `spec/voting.md`
177
+ - Level 1 badge library: `src/badge.ts`
178
+ - Signet Score details: `src/trust-score.ts`
179
+ - Full working example: `examples/full-flow.ts`
180
+ - Jurisdiction-specific examples: `examples/jurisdictions/`
181
+ - "Signet me" peer verification: `examples/signet-me.ts`