signet-protocol 1.11.1 → 1.11.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,253 @@
1
+ // Example: Full "Signet Me" Flow
2
+ // Profile creation → backup → connection → verification words
3
+ //
4
+ // Run: npx tsx examples/signet-me.ts
5
+
6
+ import {
7
+ generateMnemonic,
8
+ validateMnemonic,
9
+ mnemonicToEntropy,
10
+ entropyToMnemonic,
11
+ createSignetIdentity,
12
+ deriveAdditionalPersona,
13
+ splitSecret,
14
+ reconstructSecret,
15
+ shareToWords,
16
+ wordsToShare,
17
+ createQRPayload,
18
+ serializeQRPayload,
19
+ parseQRPayload,
20
+ computeSharedSecret,
21
+ createConnection,
22
+ getSignetWords,
23
+ verifySignetWords,
24
+ formatSignetWords,
25
+ getSignetDisplay,
26
+ } from '../src/index.js';
27
+ import { bytesToHex } from '@noble/hashes/utils.js';
28
+
29
+ // ─────────────────────────────────────────────────────────────────────────────
30
+ // Step 1: Alice creates her identity
31
+ // ─────────────────────────────────────────────────────────────────────────────
32
+
33
+ console.log('═══════════════════════════════════════════════════');
34
+ console.log(' STEP 1: Profile Creation');
35
+ console.log('═══════════════════════════════════════════════════\n');
36
+
37
+ const aliceMnemonic = generateMnemonic();
38
+ const aliceIdentity = createSignetIdentity(aliceMnemonic);
39
+ const alice = {
40
+ privateKey: bytesToHex(aliceIdentity.naturalPerson.identity.privateKey),
41
+ publicKey: bytesToHex(aliceIdentity.naturalPerson.identity.publicKey),
42
+ };
43
+
44
+ console.log('Alice creates a new Signet identity:\n');
45
+ console.log(` Mnemonic: ${aliceMnemonic}`);
46
+ console.log(` Public Key: ${alice.publicKey.slice(0, 16)}...`);
47
+ console.log(` Valid: ${validateMnemonic(aliceMnemonic)}`);
48
+
49
+ // Alice also derives a child persona
50
+ const aliceChildPersona = deriveAdditionalPersona(aliceIdentity.root, 'child', 0);
51
+ const aliceChild = {
52
+ publicKey: bytesToHex(aliceChildPersona.identity.publicKey),
53
+ };
54
+ console.log(`\n Child's Key: ${aliceChild.publicKey.slice(0, 16)}... (persona: child)`);
55
+ aliceIdentity.root.destroy();
56
+
57
+ // ─────────────────────────────────────────────────────────────────────────────
58
+ // Step 2: Alice backs up her seed using Shamir's Secret Sharing
59
+ // ─────────────────────────────────────────────────────────────────────────────
60
+
61
+ console.log('\n═══════════════════════════════════════════════════');
62
+ console.log(' STEP 2: Shared Backup (Shamir 2-of-3)');
63
+ console.log('═══════════════════════════════════════════════════\n');
64
+
65
+ const entropy = mnemonicToEntropy(aliceMnemonic);
66
+ const shares = splitSecret(entropy, 2, 3);
67
+
68
+ console.log('Alice splits her seed into 3 shares (any 2 reconstruct):\n');
69
+ for (const share of shares) {
70
+ const words = shareToWords(share);
71
+ console.log(` Share ${share.id}: ${words.slice(0, 4).join(' ')}... (${words.length} words)`);
72
+ }
73
+
74
+ // Verify reconstruction with shares 1 and 3 (skipping share 2)
75
+ const reconstructed = reconstructSecret(
76
+ [wordsToShare(shareToWords(shares[0])), wordsToShare(shareToWords(shares[2]))],
77
+ 2
78
+ );
79
+ const reconstructedMnemonic = entropyToMnemonic(reconstructed);
80
+ console.log(`\n Reconstructed from shares 1 + 3: ${reconstructedMnemonic === aliceMnemonic ? 'SUCCESS ✓' : 'FAILED ✗'}`);
81
+
82
+ // ─────────────────────────────────────────────────────────────────────────────
83
+ // Step 3: Bob creates his identity
84
+ // ─────────────────────────────────────────────────────────────────────────────
85
+
86
+ console.log('\n═══════════════════════════════════════════════════');
87
+ console.log(' STEP 3: Bob Creates His Identity');
88
+ console.log('═══════════════════════════════════════════════════\n');
89
+
90
+ const bobMnemonic = generateMnemonic();
91
+ const bobIdentity = createSignetIdentity(bobMnemonic);
92
+ const bob = {
93
+ privateKey: bytesToHex(bobIdentity.naturalPerson.identity.privateKey),
94
+ publicKey: bytesToHex(bobIdentity.naturalPerson.identity.publicKey),
95
+ };
96
+ const bobChildPersona = deriveAdditionalPersona(bobIdentity.root, 'child', 0);
97
+ const bobChild = {
98
+ publicKey: bytesToHex(bobChildPersona.identity.publicKey),
99
+ };
100
+ bobIdentity.root.destroy();
101
+
102
+ console.log(`Bob's identity:`);
103
+ console.log(` Public Key: ${bob.publicKey.slice(0, 16)}...`);
104
+ console.log(` Child's Key: ${bobChild.publicKey.slice(0, 16)}...`);
105
+
106
+ // ─────────────────────────────────────────────────────────────────────────────
107
+ // Step 4: Alice and Bob meet in person — QR code exchange
108
+ // ─────────────────────────────────────────────────────────────────────────────
109
+
110
+ console.log('\n═══════════════════════════════════════════════════');
111
+ console.log(' STEP 4: In-Person Connection (QR Exchange)');
112
+ console.log('═══════════════════════════════════════════════════\n');
113
+
114
+ // Alice shows her QR code with selected contact info
115
+ const aliceQR = createQRPayload(alice.publicKey, {
116
+ name: 'Alice',
117
+ mobile: '+44 7700 900000',
118
+ childPubkeys: [aliceChild.publicKey],
119
+ });
120
+
121
+ // Bob shows his QR code
122
+ const bobQR = createQRPayload(bob.publicKey, {
123
+ name: 'Bob',
124
+ mobile: '+44 7700 900001',
125
+ address: '42 Oak Lane, London',
126
+ childPubkeys: [bobChild.publicKey],
127
+ });
128
+
129
+ console.log('Alice and Bob meet at a local meetup. They scan each other\'s QR codes.\n');
130
+
131
+ // Simulate the QR scan — each party scans the other's serialized payload
132
+ const aliceScannedBob = parseQRPayload(serializeQRPayload(bobQR));
133
+ const bobScannedAlice = parseQRPayload(serializeQRPayload(aliceQR));
134
+
135
+ // Each creates a connection
136
+ const aliceConnection = createConnection(alice.privateKey, aliceScannedBob, {
137
+ name: 'Alice',
138
+ mobile: '+44 7700 900000',
139
+ childPubkeys: [aliceChild.publicKey],
140
+ });
141
+
142
+ const bobConnection = createConnection(bob.privateKey, bobScannedAlice, {
143
+ name: 'Bob',
144
+ mobile: '+44 7700 900001',
145
+ address: '42 Oak Lane, London',
146
+ childPubkeys: [bobChild.publicKey],
147
+ });
148
+
149
+ console.log(`Alice → Bob shared secret: ${aliceConnection.sharedSecret.slice(0, 16)}...`);
150
+ console.log(`Bob → Alice shared secret: ${bobConnection.sharedSecret.slice(0, 16)}...`);
151
+ console.log(`Secrets match: ${aliceConnection.sharedSecret === bobConnection.sharedSecret ? 'YES ✓' : 'NO ✗'}`);
152
+ console.log(`\nAlice sees Bob's info:`);
153
+ console.log(` Name: ${aliceConnection.theirInfo.name}`);
154
+ console.log(` Mobile: ${aliceConnection.theirInfo.mobile}`);
155
+ console.log(` Kids: ${aliceConnection.theirInfo.childPubkeys?.length} child key(s)`);
156
+ console.log(`\nBob sees Alice's info:`);
157
+ console.log(` Name: ${bobConnection.theirInfo.name}`);
158
+ console.log(` Mobile: ${bobConnection.theirInfo.mobile}`);
159
+ console.log(` Kids: ${bobConnection.theirInfo.childPubkeys?.length} child key(s)`);
160
+
161
+ // ─────────────────────────────────────────────────────────────────────────────
162
+ // Step 5: Months later — Bob calls Alice asking for money
163
+ // ─────────────────────────────────────────────────────────────────────────────
164
+
165
+ console.log('\n═══════════════════════════════════════════════════');
166
+ console.log(' STEP 5: "Signet Me" — Phone Verification');
167
+ console.log('═══════════════════════════════════════════════════\n');
168
+
169
+ console.log('Six months later, Bob calls Alice:');
170
+ console.log('"Hey Alice, it\'s Bob. I\'m in a tight spot — can you send');
171
+ console.log(' some cash to my new bank account?"');
172
+ console.log('');
173
+ console.log('Alice: "Sure, no problem. Signet me first."\n');
174
+
175
+ // Both open each other's profiles at the same time
176
+ const now = Date.now();
177
+
178
+ const aliceSees = getSignetDisplay(aliceConnection.sharedSecret, now);
179
+ const bobSees = getSignetDisplay(bobConnection.sharedSecret, now);
180
+
181
+ console.log('Alice opens Bob\'s profile and sees:');
182
+ console.log(` My Signet: ${aliceSees.formatted}`);
183
+ console.log(` Expires in: ${aliceSees.expiresIn.toFixed(0)}s`);
184
+ console.log('');
185
+ console.log('Bob opens Alice\'s profile and sees:');
186
+ console.log(` My Signet: ${bobSees.formatted}`);
187
+ console.log(` Expires in: ${bobSees.expiresIn.toFixed(0)}s`);
188
+ console.log('');
189
+ console.log(`Words match: ${aliceSees.formatted === bobSees.formatted ? 'YES ✓' : 'NO ✗'}`);
190
+
191
+ // Bob reads the words to Alice
192
+ const bobReadsWords = getSignetWords(bobConnection.sharedSecret, now);
193
+ const aliceVerifies = verifySignetWords(aliceConnection.sharedSecret, bobReadsWords, now);
194
+
195
+ console.log(`\nBob reads: "${formatSignetWords(bobReadsWords)}"`);
196
+ console.log(`Alice verifies: ${aliceVerifies ? 'MATCH ✓ — It\'s really Bob!' : 'NO MATCH ✗ — Impostor!'}`);
197
+
198
+ // ─────────────────────────────────────────────────────────────────────────────
199
+ // Step 6: Demonstrating word rotation
200
+ // ─────────────────────────────────────────────────────────────────────────────
201
+
202
+ console.log('\n═══════════════════════════════════════════════════');
203
+ console.log(' STEP 6: Word Rotation');
204
+ console.log('═══════════════════════════════════════════════════\n');
205
+
206
+ console.log('Words rotate every 30 seconds:\n');
207
+ for (let i = 0; i < 4; i++) {
208
+ const futureTime = now + i * 30_000;
209
+ const words = getSignetWords(aliceConnection.sharedSecret, futureTime);
210
+ const label = i === 0 ? 'Now ' : `+${i * 30}s `;
211
+ console.log(` ${label} ${formatSignetWords(words)}`);
212
+ }
213
+
214
+ // ─────────────────────────────────────────────────────────────────────────────
215
+ // Step 7: Bank scenario
216
+ // ─────────────────────────────────────────────────────────────────────────────
217
+
218
+ console.log('\n═══════════════════════════════════════════════════');
219
+ console.log(' STEP 7: Bank Verification');
220
+ console.log('═══════════════════════════════════════════════════\n');
221
+
222
+ // The bank has a Nostr keypair too
223
+ const bankMnemonic = generateMnemonic();
224
+ const bankIdentity = createSignetIdentity(bankMnemonic);
225
+ const bank = {
226
+ privateKey: bytesToHex(bankIdentity.naturalPerson.identity.privateKey),
227
+ publicKey: bytesToHex(bankIdentity.naturalPerson.identity.publicKey),
228
+ };
229
+ bankIdentity.root.destroy();
230
+
231
+ // Alice visits the bank and scans their QR code
232
+ const bankQR = createQRPayload(bank.publicKey, { name: 'High Street Bank' });
233
+ const aliceBankConnection = createConnection(alice.privateKey, parseQRPayload(serializeQRPayload(bankQR)), {
234
+ name: 'Alice',
235
+ });
236
+
237
+ // Bank also stores the connection
238
+ const bankAliceSecret = computeSharedSecret(bank.privateKey, alice.publicKey);
239
+
240
+ console.log('"The bank" calls Alice about suspicious activity.\n');
241
+ console.log('Alice: "Read me my signet."\n');
242
+
243
+ const bankNow = Date.now();
244
+ const bankWords = getSignetWords(bankAliceSecret, bankNow);
245
+ const aliceBankWords = getSignetWords(aliceBankConnection.sharedSecret, bankNow);
246
+
247
+ console.log(`Bank agent reads: ${formatSignetWords(bankWords)}`);
248
+ console.log(`Alice sees: ${formatSignetWords(aliceBankWords)}`);
249
+ console.log(`Match: ${formatSignetWords(bankWords) === formatSignetWords(aliceBankWords) ? 'YES ✓ — It\'s really the bank.' : 'NO ✗ — Scam call!'}`);
250
+
251
+ console.log('\n═══════════════════════════════════════════════════');
252
+ console.log(' Done. Your signet is your proof.');
253
+ console.log('═══════════════════════════════════════════════════');
package/llms-full.txt CHANGED
@@ -24,6 +24,7 @@ const events = await fetchFromRelay('wss://relay.example.com', filters);
24
24
  // Compute badge (async: returns Promise<BadgeInfo>)
25
25
  const badge = await computeBadge('<hex-pubkey>', events, { verifySignatures: true });
26
26
  // badge.tier => 3, badge.score => 106, badge.displayLabel => "Verified (Tier 3)"
27
+ // (illustrative values; badge.score is a Level 1 approximation, see "Trust & Badges")
27
28
 
28
29
  // Gate features on verification level
29
30
  if (meetsMinimumTier(badge, 2)) {
@@ -31,6 +32,8 @@ if (meetsMinimumTier(badge, 2)) {
31
32
  }
32
33
  ```
33
34
 
35
+ `verifySignatures` defaults to `false` in `computeBadge`, so keep `{ verifySignatures: true }` in copied code unless the events were already verified. `computeBadge` also does not check who issued a Tier 3 or Tier 4 credential; see "Who may issue Tier 3 and Tier 4" before gating on a professional tier.
36
+
34
37
  `fetchFromRelay(relayUrl: string, filters: NostrFilter[]): Promise<NostrEvent[]>` connects, collects events until EOSE and disconnects. Non-localhost relay URLs must be `wss://`. Any other Nostr client works equally well: pass the `buildBadgeFilters` output as REQ filters and give the returned events to `computeBadge`.
35
38
 
36
39
  ## Key Concepts
@@ -48,13 +51,16 @@ Conventions used throughout:
48
51
  - Of the `create*` functions documented in this file, the event-signing ones (credentials, vouches, policies, verifiers, challenges, revocations, identity bridges, guardian delegations) are async and return a signed `Promise<NostrEvent>` (with `id` and `sig`). The matching `build*Event` functions return an unsigned `UnsignedEvent`. The documented `create*` helpers that are not event signers return what is stated beside them: `createAgeRangeProof` (synchronous, `RangeProof`), `createSignetIdentity` / `createSignetIdentityFromNsec` (synchronous, `SignetIdentity`) and `createLinkageProof` (synchronous, `LinkageProof`). The package root exports other `create*` functions that this file does not cover (for example `createTwoCredentialCeremony`, `createBondProof`); check their return types in `src/` rather than assuming the event-signer shape.
49
52
  - Tiers are `SignetTier` (`1 | 2 | 3 | 4`). Timestamps (`expiresAt`, `occurredAt`, `accountCreatedAt`) are unix seconds.
50
53
  - Throws use the `SignetError` hierarchy (`SignetValidationError`, `SignetCryptoError`).
54
+ - Types are exported from the package root as type-only exports (use `import type { NostrEvent } from 'signet-protocol'`; they have no runtime value): `NostrEvent` (`{ id, pubkey, created_at, kind, tags: string[][], content, sig }`), `UnsignedEvent` (the same without `id` and `sig`), `NostrFilter` (`{ ids?, authors?, kinds?, '#d'?, '#p'?, '#L'?, '#l'?, since?, until?, limit? }`), `BadgeInfo` and `RangeProof`. The error classes `SignetError`, `SignetValidationError` and `SignetCryptoError` are exported from the root as ordinary values you can use with `instanceof`. `src/errors.ts` also defines `SignetVotingError`, but it is not exported from the root and nothing in this package throws it.
55
+ - Arguments named `events`, `credentials`, `vouches`, `bridges`, `revocations`, `verifierCredentials` and `confirmationEvents` are raw signed `NostrEvent[]` as returned by a relay, not parsed objects (`ParsedCredential`, `ParsedVouch` and so on). The functions read the tags themselves and skip events of the wrong kind or `type`. Unless a signature check is stated beside a function, it does not verify signatures: verify first, or fetch with `fetchFromRelay`, whose `RelayClient` drops events with an invalid signature or id by default.
56
+ - Issuer standing is not checked by the Level 1 and scoring functions. See "Who may issue Tier 3 and Tier 4" below.
51
57
 
52
58
  ### Credentials (kind 31000, type: credential)
53
59
  - `createSelfDeclaredCredential(privateKey, scope = 'adult', expiresAt?)`: Tier 1. `scope` is `'adult' | 'adult+child'`.
54
60
  - `createPeerVouchedCredential(issuerPrivateKey, subjectPubkey, opts)`: Tier 2. `opts: { assertionEventId: string; assertionRelay?: string; expiresAt?: number }`. `assertionEventId` is the subject's Tier 1 event.
55
61
  - `createProfessionalCredential(verifierPrivateKey, subjectPubkey, opts)`: Tier 3. `opts: { assertionEventId: string; profession: string; jurisdiction: string; actualAge: number; assertionRelay?: string; expiresAt?: number; occurredAt?: number; proofBlob?: string }`. Generates an `'18+'` range proof from `actualAge` into the event content.
56
62
  - `createChildSafetyCredential(verifierPrivateKey, subjectPubkey, opts)`: Tier 4. Same `opts` as Tier 3 plus `ageRange: string` (the range proved, e.g. `'8-12'`).
57
- - `verifyCredential(event)`: `Promise<{ signatureValid: boolean; structureValid: boolean; expired: boolean; errors: string[] }>`
63
+ - `verifyCredential(event: NostrEvent)`: `Promise<{ signatureValid: boolean; structureValid: boolean; expired: boolean; errors: string[] }>`. Checks the event's own Schnorr signature, tag structure (for example a Tier 3 or Tier 4 credential must carry `verification-type: professional` and a `profession` tag) and expiry. It does not look at who signed it: a valid result does not mean the issuer is an activated, unrevoked verifier. See "Who may issue Tier 3 and Tier 4".
58
64
  - `parseCredential(event)`: `ParsedCredential | null`
59
65
 
60
66
  `ageRange` format everywhere: `'<min>-<max>'` inclusive (e.g. `'8-12'`, `'13-17'`) or `'<min>+'` (e.g. `'18+'`, upper bound 150). Anything else fails to parse.
@@ -63,16 +69,26 @@ Default credential expiry is two years (`DEFAULT_CREDENTIAL_EXPIRY_SECONDS`) whe
63
69
 
64
70
  ### Vouches (kind 31000, type: vouch)
65
71
  - `createVouch(voucherPrivateKey, params)`: vouch for another user. `params: { subjectPubkey: string; method: 'in-person' | 'online'; voucherTier: SignetTier; voucherScore: number; context?: string; occurredAt?: number }`. `voucherScore` is the voucher's own Signet IQ (0–200).
66
- - `hasEnoughVouches(vouches, subjectPubkey, threshold = 3, minVoucherTier = 2)`: `boolean`. `true` when the number of qualifying vouches (one per voucher, voucher tier ≥ `minVoucherTier`) reaches `threshold`. `countQualifyingVouches(vouches, subjectPubkey, minVoucherTier?)` returns the count.
72
+ - `hasEnoughVouches(vouches: NostrEvent[], subjectPubkey: string, threshold = 3, minVoucherTier: SignetTier = 2)`: `boolean`. `vouches` is raw kind 31000 vouch events (`NostrEvent[]`), not `ParsedVouch` objects. `true` when the number of qualifying vouches (one per voucher, `voucher-tier` tag ≥ `minVoucherTier`) reaches `threshold`. It does not verify signatures, and the voucher tier is the self-reported tag, not looked up. `countQualifyingVouches(vouches: NostrEvent[], subjectPubkey: string, minVoucherTier?: SignetTier)` returns the count.
67
73
 
68
74
  ### Trust & Badges
69
- - `computeBadge(pubkey, events, opts?)`: `Promise<BadgeInfo>`. `opts: { verifySignatures?: boolean (default false); now?: number }`. `BadgeInfo: { pubkey, tier, tierLabel, score, isVerified, displayLabel, credentialCount, vouchCount, entityType? }`.
70
- - `buildBadgeFilters(pubkeys: string[])`: `Array<{ kinds: number[]; '#d': string[] }>`. One filter for kind 31000 with d-tags `credential:<pubkey>` and `vouch:<pubkey>` for each pubkey.
75
+ - `computeBadge(pubkey: string, events: NostrEvent[], opts?)`: `Promise<BadgeInfo>`. `opts: { verifySignatures?: boolean (default false); now?: number; isTrustedIssuer?: (issuerPubkey: string) => boolean }`. `BadgeInfo: { pubkey, tier, tierLabel, score, isVerified, displayLabel, credentialCount, vouchCount, entityType? }`. `verifySignatures` defaults to `false`: with the default, `computeBadge` does not check any signature, so a call that omits it trusts every event it is given. Pass `{ verifySignatures: true }` unless the events came from `fetchFromRelay` or another source that has already dropped events with an invalid signature or id. `computeBadge` reads the `tier`, `verification-type`, `expiration`, `method` and `voucher-score` tags. It does not run `validateCredential`. It checks who signed a credential only when `isTrustedIssuer` is given: the predicate is called with `event.pubkey` of each credential that has `tier` 3 or 4 or `verification-type: professional`, and a credential it rejects is skipped (it adds nothing to the tier, score or `credentialCount`). Without the predicate, issuer standing is not enforced (see "Who may issue Tier 3 and Tier 4"). With `verifySignatures: true`, a vouch whose signature fails does not take its voucher's one-vouch slot.
76
+ - `buildBadgeFilters(pubkeys: string[])`: `Array<{ kinds: number[]; '#d': string[] }>`. One filter for kind 31000 with d-tags `credential:<pubkey>` and `vouch:<pubkey>` for each pubkey. It fetches no verifier, challenge or revocation events, so the events it returns cannot be used to check issuer standing.
71
77
  - `meetsMinimumTier(badge: BadgeInfo, minTier: SignetTier)`: `boolean`. `true` only when `badge.isVerified` and `badge.tier >= minTier`.
72
- - `computeTrustScore(subjectPubkey, credentials, vouches, accountCreatedAt?, bridges?)`: `TrustScoreBreakdown` (`{ score, tier, professionalVerifications, inPersonVouches, onlineVouches, accountAgeDays, signals }`). This is the full Signet IQ. It does not verify signatures.
78
+ - `computeTrustScore(subjectPubkey: string, credentials: NostrEvent[], vouches: NostrEvent[], accountCreatedAt?: number, bridges?: NostrEvent[])`: `TrustScoreBreakdown` (`{ score, tier, professionalVerifications, inPersonVouches, onlineVouches, accountAgeDays, signals }`). This is the full Signet IQ. `credentials`, `vouches` and `bridges` are raw kind 31000 events (`NostrEvent[]`), not parsed objects. It does not verify signatures and, like `computeBadge`, does not check issuer standing. `bridges` are matched by `event.pubkey === subjectPubkey` and are not ring-signature verified here: run `verifyIdentityBridge` first.
73
79
 
74
80
  `badge.score` is on the same 0–200 scale and uses the same weights as `computeTrustScore(...).score` (professional verification 80, in-person vouch up to 16, online vouch up to 4, each vouch scaled by the voucher's score ÷ 200), but it is a Level 1 approximation, not the full IQ: `computeBadge` ignores account age and identity bridges and does not apply the per-type caps (1 professional, 3 in-person vouches, 5 online vouches). Use `computeTrustScore` when you need the full IQ.
75
81
 
82
+ ### Who may issue Tier 3 and Tier 4
83
+
84
+ The library does not enforce issuer standing in `computeTrustScore`, `verifyCredential` or `countChallengeConfirmations`, and `computeBadge` enforces it only through the `isTrustedIssuer` option. A kind 31000 `type: credential` event that carries `tier: 3` (or 4) and `verification-type: professional` counts toward the tier and the score whichever key signed it, including a key that has never published a verifier credential, and `verifySignatures: true` only proves the signer wrote the event. `buildBadgeFilters` fetches only `credential:` and `vouch:` events, so a badge built from its output cannot tell a licensed verifier from any other key. The protocol requires issuers of Tier 3 and Tier 4 to be activated, unrevoked verifiers (see `spec/protocol.md`); your client has to decide which issuers pass and give `computeBadge` that decision:
85
+
86
+ 1. Fetch the candidate issuer's `type: verifier` credential, the vouches for that issuer, the verifier credentials of those vouchers, and any `type: revocation` events (all kind 31000). `buildBadgeFilters` does not fetch these, so build your own filters.
87
+ 2. Treat an issuer as acceptable only when `checkCrossVerification(issuerPubkey, vouches, verifierCredentials, { revocations, isActiveVerifier }).activated` is `true` and `isVerifierRevoked(issuerPubkey, revocations)` is `false`.
88
+ 3. Call `computeBadge(pubkey, events, { verifySignatures: true, isTrustedIssuer: (pk) => accepted.has(pk) })`, where `accepted` holds the issuers that passed step 2.
89
+
90
+ Voucher activation is recursive (a voucher is active only if its own vouchers are), so `checkCrossVerification` takes it as the `isActiveVerifier` predicate rather than resolving the chain. Start from verifiers your client already accepts. Without `isActiveVerifier` and `revocations`, a voucher only needs a verifier credential with a `profession` tag.
91
+
76
92
  ### Policies (kind 30078, NIP-78)
77
93
  - `createPolicy(operatorPrivateKey, params)`: community verification requirements. `params: { communityId: string; adultMinTier: SignetTier; childMinTier: SignetTier; enforcement: 'client' | 'relay' | 'both'; minScore?: number; modMinTier?: SignetTier; verifierBond?: number; revocationThreshold?: number; description?: string }`. The d-tag is `signet:policy:<communityId>`.
78
94
  - `new PolicyChecker(policyEvent: NostrEvent)`: throws `SignetValidationError('Invalid policy event')` if the event is not a valid policy. Methods: `getPolicy(): ParsedPolicy`, `checkAdult(tier, score)`, `checkChild(tier, score)`, `checkModerator(tier, score)`. Each check returns `PolicyCheckResult`: `{ allowed: boolean; reason?: string; requiredTier: SignetTier; actualTier: SignetTier; requiredScore?: number; actualScore?: number }`.
@@ -80,20 +96,20 @@ Default credential expiry is two years (`DEFAULT_CREDENTIAL_EXPIRY_SECONDS`) whe
80
96
 
81
97
  ### Verifiers (kind 31000, type: verifier)
82
98
  - `createVerifierCredential(privateKey, params)`: register as professional verifier. `params: { profession: string; jurisdiction: string; licenceHash: string; professionalBody: string; statement?: string; bondProof?: BondProof }`.
83
- - `checkCrossVerification(verifierPubkey, vouches, verifierCredentials)`: activation check. Returns `{ activated: boolean; vouchCount: number; professions: string[]; errors: string[] }`.
84
- - `isVerifierRevoked(verifierPubkey, revocations)`: `boolean`
99
+ - `checkCrossVerification(verifierPubkey: string, vouches: NostrEvent[], verifierCredentials: NostrEvent[], options?: { revocations?: NostrEvent[]; isActiveVerifier?: (voucherPubkey: string) => boolean })`: activation check. `vouches` is kind 31000 vouch events (`NostrEvent[]`) and `verifierCredentials` is kind 31000 `type: verifier` events (`NostrEvent[]`), not parsed objects. Returns `{ activated: boolean; vouchCount: number; professions: string[]; errors: string[] }`. `activated` is `true` when at least `VERIFIER_ACTIVATION.MIN_VOUCHES` vouches for `verifierPubkey` come from distinct vouchers that each appear in `verifierCredentials` with a `profession` tag, and those vouchers cover at least `VERIFIER_ACTIVATION.MIN_PROFESSIONS` different professions. It does not verify signatures. The spec also requires each voucher to be an active, non-revoked verifier: pass `options.revocations` to skip any voucher `isVerifierRevoked` reports as revoked, and `options.isActiveVerifier` to skip vouchers your client does not treat as activated. Without these options neither is checked.
100
+ - `isVerifierRevoked(verifierPubkey: string, revocations: NostrEvent[])`: `boolean`. `true` when any kind 31000 `type: revocation` event in `revocations` targets `verifierPubkey`. It does not verify signatures or check who signed the revocation.
85
101
 
86
102
  ### Challenges and Revocations (kind 31000, type: challenge / revocation)
87
103
  - `createChallenge(reporterPrivateKey, params)`: `Promise<NostrEvent>`. `params: { verifierPubkey: string; reason: 'anomalous-volume' | 'registry-mismatch' | 'fraudulent-attestation' | 'licence-revoked' | 'other'; evidenceType: string; reporterTier: SignetTier; evidence: string; occurredAt?: number }`. `evidence` becomes the event content.
88
104
  - `createRevocation(authorityPrivateKey, params)`: `Promise<NostrEvent>`. `params: { verifierPubkey: string; challengeEventId: string; confirmations: number; bondAction: 'slashed' | 'returned' | 'held'; scope: 'full' | 'partial'; effectiveAt: number; summary: string }`.
89
- - `countChallengeConfirmations(challengeEventId, confirmationEvents, credentialEvents)`: `number`
105
+ - `countChallengeConfirmations(challengeEventId: string, confirmationEvents: NostrEvent[], credentialEvents: NostrEvent[])`: `number`. All arrays are raw `NostrEvent[]`. Counts distinct confirmation signers whose `challenge` tag equals `challengeEventId` and who hold a credential event with a `tier` tag of 3 or higher in `credentialEvents`. It does not verify signatures or the issuers of those credentials.
90
106
  - `hasReachedRevocationThreshold(confirmations, threshold = 5)`: `boolean`
91
107
  - `parseChallenge(event)` / `parseRevocation(event)`: parsed object or `null`.
92
108
 
93
109
  ### Identity Bridge (kind 31000, type: identity-bridge)
94
- - `createIdentityBridge(anonPrivateKey, realPrivateKey, ring, signerIndex, ringMinTier, opts?)`: link anonymous account to verified identity via ring signature. `ring: string[]` of pubkeys (at least 5, `MIN_BRIDGE_RING_SIZE`); `opts: { occurredAt?: number }`. Signed by the anonymous key.
95
- - `selectDecoyRing(verifiedPubkeys, realPubkey, ringSize = 5)`: `{ ring: string[]; signerIndex: number }`
96
- - `verifyIdentityBridge(event, opts?)`: `Promise<boolean>`. `opts: { maxAgeSeconds?: number }` (default 24 hours; `0` disables the freshness check).
110
+ - `createIdentityBridge(anonPrivateKey: string, realPrivateKey: string, ring: string[], signerIndex: number, ringMinTier: SignetTier, opts?)`: `Promise<NostrEvent>`. Links an anonymous account to a verified identity via ring signature. `ring: string[]` is the anonymity set of verified pubkeys (at least 5, `MIN_BRIDGE_RING_SIZE`) and must contain the public key of `realPrivateKey`. `signerIndex` is the zero-based position in `ring` of that real public key (`selectDecoyRing` returns it for you); a wrong index makes ring signing throw (`Private key does not match ring member at signerIndex`). `ringMinTier` is the lowest verification tier among the ring members, which you assert yourself: it is written to the event's `ring-min-tier` tag and scales the bridge's contribution to `computeTrustScore` (`TRUST_WEIGHTS.IDENTITY_BRIDGE` of 50 × `ringMinTier` ÷ 4). Neither `createIdentityBridge` nor `verifyIdentityBridge` checks that the ring members hold that tier. `opts: { occurredAt?: number }`. Signed by the anonymous key.
111
+ - `selectDecoyRing(verifiedPubkeys: string[], realPubkey: string, ringSize = 5)`: `{ ring: string[]; signerIndex: number }`. Picks decoys from `verifiedPubkeys` (`realPubkey` is filtered out of the pool if present) and places `realPubkey` at a random position. Throws if the pool has fewer than `ringSize - 1` other pubkeys.
112
+ - `verifyIdentityBridge(event: NostrEvent, opts?)`: `Promise<boolean>`. `opts: { maxAgeSeconds?: number }` (default 24 hours; `0` disables the freshness check).
97
113
 
98
114
  ### ZKP Age Proofs
99
115
  - `createAgeRangeProof(age: number, ageRange: string, subjectPubkey?: string)`: `RangeProof` (synchronous). `subjectPubkey` binds the proof to one credential.
@@ -124,6 +140,8 @@ The voting extension (kinds 30482–30484) is specified in `spec/voting.md` and
124
140
 
125
141
  ## Optional
126
142
 
143
+ These paths ship in the npm package (`spec/`, `examples/`, and the `docs/` pages named below and in the experimental section above), so the relative links resolve inside `node_modules/signet-protocol/`. The examples import from the package's own `src/` directory; in your own code import from `signet-protocol`.
144
+
127
145
  - [llms-full.txt](llms-full.txt): llms.txt plus the full protocol and voting specifications and the developer guides in one file
128
146
  - [Full protocol specification](spec/protocol.md)
129
147
  - [Voting extension specification](spec/voting.md)
@@ -503,6 +521,8 @@ Minimum requirement for verifier activation:
503
521
  - At least 2 different professions represented
504
522
  - Vouchers must themselves be active, non-revoked verifiers
505
523
 
524
+ Activation is recursive: whether a voucher is active depends on its own vouchers. A client therefore resolves it from a set of verifiers it already accepts and cannot derive it from a single verifier's vouches alone. A client counting vouches (for activation or for a score) MUST check each vouch's signature before treating that voucher as seen, so that an invalid event carrying a voucher's pubkey cannot displace the voucher's valid vouch.
525
+
506
526
  ### Layer 3 — Rate Limiting / Anomaly Detection
507
527
 
508
528
  All credentials are public Nostr events. Issuance volume is visible to the entire network.
@@ -3406,7 +3426,7 @@ kind 30078 (NIP-78) policy parsing so your client respects community verificatio
3406
3426
 
3407
3427
  Issue professional credentials, manage verifier lifecycle (kind 31000 types: verifier, challenge, revocation), build
3408
3428
  guardian delegation (kind 31000, `type: delegation`), support credential chains (supersedes / superseded-by),
3409
- and integrate the voting extension (kinds 30482–30484).
3429
+ 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`).
3410
3430
 
3411
3431
 
3412
3432
  ## How Verification Works (Tier 3 / 4)
@@ -3456,6 +3476,8 @@ by two other licensed professionals before becoming active. A challenge / revoca
3456
3476
  | 30483 | — | Voting extension: cast an anonymous ballot |
3457
3477
  | 30484 | — | Voting extension: publish tallied result |
3458
3478
 
3479
+ Kinds 30482–30484 are specified in `spec/voting.md` and implemented in the separate `nostr-anon-vote` package, not in `signet-protocol`.
3480
+
3459
3481
 
3460
3482
  ## Integrating at Level 1
3461
3483
 
@@ -3483,7 +3505,7 @@ const events: NostrEvent[] = await fetchFromRelay('wss://relay.example.com', fil
3483
3505
  const badge = await computeBadge(pubkeys[0], events, { verifySignatures: true });
3484
3506
 
3485
3507
  // badge.displayLabel => "Verified (Tier 3)"
3486
- // badge.score => 106
3508
+ // badge.score => 106 (illustrative; see the score note below)
3487
3509
  // badge.tier => 3
3488
3510
  // badge.vouchCount => 5
3489
3511
 
@@ -3497,6 +3519,10 @@ if (meetsMinimumTier(badge, 2)) {
3497
3519
  (one vouch counted per voucher pubkey), and optional Schnorr signature verification. The result is
3498
3520
  a plain object — no rendering assumptions are made.
3499
3521
 
3522
+ - `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.
3523
+ - `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.
3524
+ - `computeBadge` checks who issued a Tier 3 or Tier 4 credential only when you pass `isTrustedIssuer`, and `buildBadgeFilters` does not fetch verifier or revocation events. Without the option, 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, decide which issuers pass with `checkCrossVerification(...).activated` and `isVerifierRevoked(...)`, and pass `{ isTrustedIssuer: (pk) => accepted.has(pk) }` to `computeBadge` (see `llms.txt`).
3525
+
3500
3526
  The `buildBadgeFilters` helper returns a standard Nostr REQ filter array ready for use with any
3501
3527
  relay client library.
3502
3528
 
@@ -3551,19 +3577,21 @@ npm install signet-protocol
3551
3577
  ### Code example
3552
3578
 
3553
3579
  ```typescript
3554
- import { computeBadge, buildBadgeFilters } from 'signet-protocol';
3580
+ import { computeBadge, buildBadgeFilters, fetchFromRelay } from 'signet-protocol';
3581
+
3582
+ const pubkey = '<hex-pubkey>';
3555
3583
 
3556
- // 1. Fetch events from relay
3584
+ // 1. Fetch events from a relay (fetchFromRelay is the package's own client; any Nostr client works)
3557
3585
  const filters = buildBadgeFilters([pubkey]);
3558
- // Send a REQ message to your relay with these filters.
3559
3586
  // The filters request kind 31000 with credential and vouch type tags.
3587
+ const events = await fetchFromRelay('wss://relay.example.com', filters);
3560
3588
 
3561
3589
  // 2. Compute badge from the events you receive
3562
- const badge = await computeBadge(pubkey, events); // async: returns Promise<BadgeInfo>
3590
+ const badge = await computeBadge(pubkey, events, { verifySignatures: true }); // async: returns Promise<BadgeInfo>
3563
3591
 
3564
3592
  // 3. Display
3565
3593
  if (badge.isVerified) {
3566
- showBadge(badge.tierLabel, badge.score);
3594
+ console.log(badge.tierLabel, badge.score); // replace with your own rendering
3567
3595
  }
3568
3596
  ```
3569
3597
 
@@ -3590,6 +3618,11 @@ The `badge` object includes:
3590
3618
  - Merkle trees
3591
3619
  - Any cryptographic primitives beyond standard Nostr signature verification
3592
3620
 
3621
+ Two things Level 1 does not do for you:
3622
+
3623
+ - 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).
3624
+ - Issuer standing is opt-in. `computeBadge` checks who issued a Tier 3 or Tier 4 credential only when you pass `isTrustedIssuer`, and `buildBadgeFilters` does not fetch verifier or revocation events. Without the option, 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, decide which issuers pass with `checkCrossVerification(...).activated` and `isVerifierRevoked(...)`, and pass `{ isTrustedIssuer: (pk) => accepted.has(pk) }` to `computeBadge` (see `llms.txt`).
3625
+
3593
3626
  ---
3594
3627
 
3595
3628
  ## Level 2 — Issue Vouches
@@ -3681,7 +3714,7 @@ Reference: `src/policies.ts`
3681
3714
  **Event kinds:** 31000 (all attestation types), 30078 (policies), 30482-30484 (voting)
3682
3715
  **New crypto required:** Merkle trees, Pedersen range proofs (age range proofs), linkable ring signatures
3683
3716
 
3684
- 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.
3717
+ 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`.
3685
3718
 
3686
3719
  ### What to build (beyond Level 2)
3687
3720
 
@@ -3689,7 +3722,7 @@ Level 3 is full reference implementation. This is appropriate for clients that w
3689
3722
  - **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.
3690
3723
  - **Age range proofs:** Pedersen range proofs prove that a user is within an age range without revealing their exact date of birth.
3691
3724
  - **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.
3692
- - **Ring signatures and elections:** The voting extension (kinds `30482`–`30484`) uses linkable ring signatures for anonymous voting with double-spend prevention.
3725
+ - **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`).
3693
3726
  - **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.
3694
3727
  - **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.
3695
3728
  - **Identity bridging:** Identity bridge attestations (kind `31000` with `type: identity-bridge`) link verified identities across protocols or jurisdictions.
package/llms.txt CHANGED
@@ -24,6 +24,7 @@ const events = await fetchFromRelay('wss://relay.example.com', filters);
24
24
  // Compute badge (async: returns Promise<BadgeInfo>)
25
25
  const badge = await computeBadge('<hex-pubkey>', events, { verifySignatures: true });
26
26
  // badge.tier => 3, badge.score => 106, badge.displayLabel => "Verified (Tier 3)"
27
+ // (illustrative values; badge.score is a Level 1 approximation, see "Trust & Badges")
27
28
 
28
29
  // Gate features on verification level
29
30
  if (meetsMinimumTier(badge, 2)) {
@@ -31,6 +32,8 @@ if (meetsMinimumTier(badge, 2)) {
31
32
  }
32
33
  ```
33
34
 
35
+ `verifySignatures` defaults to `false` in `computeBadge`, so keep `{ verifySignatures: true }` in copied code unless the events were already verified. `computeBadge` also does not check who issued a Tier 3 or Tier 4 credential; see "Who may issue Tier 3 and Tier 4" before gating on a professional tier.
36
+
34
37
  `fetchFromRelay(relayUrl: string, filters: NostrFilter[]): Promise<NostrEvent[]>` connects, collects events until EOSE and disconnects. Non-localhost relay URLs must be `wss://`. Any other Nostr client works equally well: pass the `buildBadgeFilters` output as REQ filters and give the returned events to `computeBadge`.
35
38
 
36
39
  ## Key Concepts
@@ -48,13 +51,16 @@ Conventions used throughout:
48
51
  - Of the `create*` functions documented in this file, the event-signing ones (credentials, vouches, policies, verifiers, challenges, revocations, identity bridges, guardian delegations) are async and return a signed `Promise<NostrEvent>` (with `id` and `sig`). The matching `build*Event` functions return an unsigned `UnsignedEvent`. The documented `create*` helpers that are not event signers return what is stated beside them: `createAgeRangeProof` (synchronous, `RangeProof`), `createSignetIdentity` / `createSignetIdentityFromNsec` (synchronous, `SignetIdentity`) and `createLinkageProof` (synchronous, `LinkageProof`). The package root exports other `create*` functions that this file does not cover (for example `createTwoCredentialCeremony`, `createBondProof`); check their return types in `src/` rather than assuming the event-signer shape.
49
52
  - Tiers are `SignetTier` (`1 | 2 | 3 | 4`). Timestamps (`expiresAt`, `occurredAt`, `accountCreatedAt`) are unix seconds.
50
53
  - Throws use the `SignetError` hierarchy (`SignetValidationError`, `SignetCryptoError`).
54
+ - Types are exported from the package root as type-only exports (use `import type { NostrEvent } from 'signet-protocol'`; they have no runtime value): `NostrEvent` (`{ id, pubkey, created_at, kind, tags: string[][], content, sig }`), `UnsignedEvent` (the same without `id` and `sig`), `NostrFilter` (`{ ids?, authors?, kinds?, '#d'?, '#p'?, '#L'?, '#l'?, since?, until?, limit? }`), `BadgeInfo` and `RangeProof`. The error classes `SignetError`, `SignetValidationError` and `SignetCryptoError` are exported from the root as ordinary values you can use with `instanceof`. `src/errors.ts` also defines `SignetVotingError`, but it is not exported from the root and nothing in this package throws it.
55
+ - Arguments named `events`, `credentials`, `vouches`, `bridges`, `revocations`, `verifierCredentials` and `confirmationEvents` are raw signed `NostrEvent[]` as returned by a relay, not parsed objects (`ParsedCredential`, `ParsedVouch` and so on). The functions read the tags themselves and skip events of the wrong kind or `type`. Unless a signature check is stated beside a function, it does not verify signatures: verify first, or fetch with `fetchFromRelay`, whose `RelayClient` drops events with an invalid signature or id by default.
56
+ - Issuer standing is not checked by the Level 1 and scoring functions. See "Who may issue Tier 3 and Tier 4" below.
51
57
 
52
58
  ### Credentials (kind 31000, type: credential)
53
59
  - `createSelfDeclaredCredential(privateKey, scope = 'adult', expiresAt?)`: Tier 1. `scope` is `'adult' | 'adult+child'`.
54
60
  - `createPeerVouchedCredential(issuerPrivateKey, subjectPubkey, opts)`: Tier 2. `opts: { assertionEventId: string; assertionRelay?: string; expiresAt?: number }`. `assertionEventId` is the subject's Tier 1 event.
55
61
  - `createProfessionalCredential(verifierPrivateKey, subjectPubkey, opts)`: Tier 3. `opts: { assertionEventId: string; profession: string; jurisdiction: string; actualAge: number; assertionRelay?: string; expiresAt?: number; occurredAt?: number; proofBlob?: string }`. Generates an `'18+'` range proof from `actualAge` into the event content.
56
62
  - `createChildSafetyCredential(verifierPrivateKey, subjectPubkey, opts)`: Tier 4. Same `opts` as Tier 3 plus `ageRange: string` (the range proved, e.g. `'8-12'`).
57
- - `verifyCredential(event)`: `Promise<{ signatureValid: boolean; structureValid: boolean; expired: boolean; errors: string[] }>`
63
+ - `verifyCredential(event: NostrEvent)`: `Promise<{ signatureValid: boolean; structureValid: boolean; expired: boolean; errors: string[] }>`. Checks the event's own Schnorr signature, tag structure (for example a Tier 3 or Tier 4 credential must carry `verification-type: professional` and a `profession` tag) and expiry. It does not look at who signed it: a valid result does not mean the issuer is an activated, unrevoked verifier. See "Who may issue Tier 3 and Tier 4".
58
64
  - `parseCredential(event)`: `ParsedCredential | null`
59
65
 
60
66
  `ageRange` format everywhere: `'<min>-<max>'` inclusive (e.g. `'8-12'`, `'13-17'`) or `'<min>+'` (e.g. `'18+'`, upper bound 150). Anything else fails to parse.
@@ -63,16 +69,26 @@ Default credential expiry is two years (`DEFAULT_CREDENTIAL_EXPIRY_SECONDS`) whe
63
69
 
64
70
  ### Vouches (kind 31000, type: vouch)
65
71
  - `createVouch(voucherPrivateKey, params)`: vouch for another user. `params: { subjectPubkey: string; method: 'in-person' | 'online'; voucherTier: SignetTier; voucherScore: number; context?: string; occurredAt?: number }`. `voucherScore` is the voucher's own Signet IQ (0–200).
66
- - `hasEnoughVouches(vouches, subjectPubkey, threshold = 3, minVoucherTier = 2)`: `boolean`. `true` when the number of qualifying vouches (one per voucher, voucher tier ≥ `minVoucherTier`) reaches `threshold`. `countQualifyingVouches(vouches, subjectPubkey, minVoucherTier?)` returns the count.
72
+ - `hasEnoughVouches(vouches: NostrEvent[], subjectPubkey: string, threshold = 3, minVoucherTier: SignetTier = 2)`: `boolean`. `vouches` is raw kind 31000 vouch events (`NostrEvent[]`), not `ParsedVouch` objects. `true` when the number of qualifying vouches (one per voucher, `voucher-tier` tag ≥ `minVoucherTier`) reaches `threshold`. It does not verify signatures, and the voucher tier is the self-reported tag, not looked up. `countQualifyingVouches(vouches: NostrEvent[], subjectPubkey: string, minVoucherTier?: SignetTier)` returns the count.
67
73
 
68
74
  ### Trust & Badges
69
- - `computeBadge(pubkey, events, opts?)`: `Promise<BadgeInfo>`. `opts: { verifySignatures?: boolean (default false); now?: number }`. `BadgeInfo: { pubkey, tier, tierLabel, score, isVerified, displayLabel, credentialCount, vouchCount, entityType? }`.
70
- - `buildBadgeFilters(pubkeys: string[])`: `Array<{ kinds: number[]; '#d': string[] }>`. One filter for kind 31000 with d-tags `credential:<pubkey>` and `vouch:<pubkey>` for each pubkey.
75
+ - `computeBadge(pubkey: string, events: NostrEvent[], opts?)`: `Promise<BadgeInfo>`. `opts: { verifySignatures?: boolean (default false); now?: number; isTrustedIssuer?: (issuerPubkey: string) => boolean }`. `BadgeInfo: { pubkey, tier, tierLabel, score, isVerified, displayLabel, credentialCount, vouchCount, entityType? }`. `verifySignatures` defaults to `false`: with the default, `computeBadge` does not check any signature, so a call that omits it trusts every event it is given. Pass `{ verifySignatures: true }` unless the events came from `fetchFromRelay` or another source that has already dropped events with an invalid signature or id. `computeBadge` reads the `tier`, `verification-type`, `expiration`, `method` and `voucher-score` tags. It does not run `validateCredential`. It checks who signed a credential only when `isTrustedIssuer` is given: the predicate is called with `event.pubkey` of each credential that has `tier` 3 or 4 or `verification-type: professional`, and a credential it rejects is skipped (it adds nothing to the tier, score or `credentialCount`). Without the predicate, issuer standing is not enforced (see "Who may issue Tier 3 and Tier 4"). With `verifySignatures: true`, a vouch whose signature fails does not take its voucher's one-vouch slot.
76
+ - `buildBadgeFilters(pubkeys: string[])`: `Array<{ kinds: number[]; '#d': string[] }>`. One filter for kind 31000 with d-tags `credential:<pubkey>` and `vouch:<pubkey>` for each pubkey. It fetches no verifier, challenge or revocation events, so the events it returns cannot be used to check issuer standing.
71
77
  - `meetsMinimumTier(badge: BadgeInfo, minTier: SignetTier)`: `boolean`. `true` only when `badge.isVerified` and `badge.tier >= minTier`.
72
- - `computeTrustScore(subjectPubkey, credentials, vouches, accountCreatedAt?, bridges?)`: `TrustScoreBreakdown` (`{ score, tier, professionalVerifications, inPersonVouches, onlineVouches, accountAgeDays, signals }`). This is the full Signet IQ. It does not verify signatures.
78
+ - `computeTrustScore(subjectPubkey: string, credentials: NostrEvent[], vouches: NostrEvent[], accountCreatedAt?: number, bridges?: NostrEvent[])`: `TrustScoreBreakdown` (`{ score, tier, professionalVerifications, inPersonVouches, onlineVouches, accountAgeDays, signals }`). This is the full Signet IQ. `credentials`, `vouches` and `bridges` are raw kind 31000 events (`NostrEvent[]`), not parsed objects. It does not verify signatures and, like `computeBadge`, does not check issuer standing. `bridges` are matched by `event.pubkey === subjectPubkey` and are not ring-signature verified here: run `verifyIdentityBridge` first.
73
79
 
74
80
  `badge.score` is on the same 0–200 scale and uses the same weights as `computeTrustScore(...).score` (professional verification 80, in-person vouch up to 16, online vouch up to 4, each vouch scaled by the voucher's score ÷ 200), but it is a Level 1 approximation, not the full IQ: `computeBadge` ignores account age and identity bridges and does not apply the per-type caps (1 professional, 3 in-person vouches, 5 online vouches). Use `computeTrustScore` when you need the full IQ.
75
81
 
82
+ ### Who may issue Tier 3 and Tier 4
83
+
84
+ The library does not enforce issuer standing in `computeTrustScore`, `verifyCredential` or `countChallengeConfirmations`, and `computeBadge` enforces it only through the `isTrustedIssuer` option. A kind 31000 `type: credential` event that carries `tier: 3` (or 4) and `verification-type: professional` counts toward the tier and the score whichever key signed it, including a key that has never published a verifier credential, and `verifySignatures: true` only proves the signer wrote the event. `buildBadgeFilters` fetches only `credential:` and `vouch:` events, so a badge built from its output cannot tell a licensed verifier from any other key. The protocol requires issuers of Tier 3 and Tier 4 to be activated, unrevoked verifiers (see `spec/protocol.md`); your client has to decide which issuers pass and give `computeBadge` that decision:
85
+
86
+ 1. Fetch the candidate issuer's `type: verifier` credential, the vouches for that issuer, the verifier credentials of those vouchers, and any `type: revocation` events (all kind 31000). `buildBadgeFilters` does not fetch these, so build your own filters.
87
+ 2. Treat an issuer as acceptable only when `checkCrossVerification(issuerPubkey, vouches, verifierCredentials, { revocations, isActiveVerifier }).activated` is `true` and `isVerifierRevoked(issuerPubkey, revocations)` is `false`.
88
+ 3. Call `computeBadge(pubkey, events, { verifySignatures: true, isTrustedIssuer: (pk) => accepted.has(pk) })`, where `accepted` holds the issuers that passed step 2.
89
+
90
+ Voucher activation is recursive (a voucher is active only if its own vouchers are), so `checkCrossVerification` takes it as the `isActiveVerifier` predicate rather than resolving the chain. Start from verifiers your client already accepts. Without `isActiveVerifier` and `revocations`, a voucher only needs a verifier credential with a `profession` tag.
91
+
76
92
  ### Policies (kind 30078, NIP-78)
77
93
  - `createPolicy(operatorPrivateKey, params)`: community verification requirements. `params: { communityId: string; adultMinTier: SignetTier; childMinTier: SignetTier; enforcement: 'client' | 'relay' | 'both'; minScore?: number; modMinTier?: SignetTier; verifierBond?: number; revocationThreshold?: number; description?: string }`. The d-tag is `signet:policy:<communityId>`.
78
94
  - `new PolicyChecker(policyEvent: NostrEvent)`: throws `SignetValidationError('Invalid policy event')` if the event is not a valid policy. Methods: `getPolicy(): ParsedPolicy`, `checkAdult(tier, score)`, `checkChild(tier, score)`, `checkModerator(tier, score)`. Each check returns `PolicyCheckResult`: `{ allowed: boolean; reason?: string; requiredTier: SignetTier; actualTier: SignetTier; requiredScore?: number; actualScore?: number }`.
@@ -80,20 +96,20 @@ Default credential expiry is two years (`DEFAULT_CREDENTIAL_EXPIRY_SECONDS`) whe
80
96
 
81
97
  ### Verifiers (kind 31000, type: verifier)
82
98
  - `createVerifierCredential(privateKey, params)`: register as professional verifier. `params: { profession: string; jurisdiction: string; licenceHash: string; professionalBody: string; statement?: string; bondProof?: BondProof }`.
83
- - `checkCrossVerification(verifierPubkey, vouches, verifierCredentials)`: activation check. Returns `{ activated: boolean; vouchCount: number; professions: string[]; errors: string[] }`.
84
- - `isVerifierRevoked(verifierPubkey, revocations)`: `boolean`
99
+ - `checkCrossVerification(verifierPubkey: string, vouches: NostrEvent[], verifierCredentials: NostrEvent[], options?: { revocations?: NostrEvent[]; isActiveVerifier?: (voucherPubkey: string) => boolean })`: activation check. `vouches` is kind 31000 vouch events (`NostrEvent[]`) and `verifierCredentials` is kind 31000 `type: verifier` events (`NostrEvent[]`), not parsed objects. Returns `{ activated: boolean; vouchCount: number; professions: string[]; errors: string[] }`. `activated` is `true` when at least `VERIFIER_ACTIVATION.MIN_VOUCHES` vouches for `verifierPubkey` come from distinct vouchers that each appear in `verifierCredentials` with a `profession` tag, and those vouchers cover at least `VERIFIER_ACTIVATION.MIN_PROFESSIONS` different professions. It does not verify signatures. The spec also requires each voucher to be an active, non-revoked verifier: pass `options.revocations` to skip any voucher `isVerifierRevoked` reports as revoked, and `options.isActiveVerifier` to skip vouchers your client does not treat as activated. Without these options neither is checked.
100
+ - `isVerifierRevoked(verifierPubkey: string, revocations: NostrEvent[])`: `boolean`. `true` when any kind 31000 `type: revocation` event in `revocations` targets `verifierPubkey`. It does not verify signatures or check who signed the revocation.
85
101
 
86
102
  ### Challenges and Revocations (kind 31000, type: challenge / revocation)
87
103
  - `createChallenge(reporterPrivateKey, params)`: `Promise<NostrEvent>`. `params: { verifierPubkey: string; reason: 'anomalous-volume' | 'registry-mismatch' | 'fraudulent-attestation' | 'licence-revoked' | 'other'; evidenceType: string; reporterTier: SignetTier; evidence: string; occurredAt?: number }`. `evidence` becomes the event content.
88
104
  - `createRevocation(authorityPrivateKey, params)`: `Promise<NostrEvent>`. `params: { verifierPubkey: string; challengeEventId: string; confirmations: number; bondAction: 'slashed' | 'returned' | 'held'; scope: 'full' | 'partial'; effectiveAt: number; summary: string }`.
89
- - `countChallengeConfirmations(challengeEventId, confirmationEvents, credentialEvents)`: `number`
105
+ - `countChallengeConfirmations(challengeEventId: string, confirmationEvents: NostrEvent[], credentialEvents: NostrEvent[])`: `number`. All arrays are raw `NostrEvent[]`. Counts distinct confirmation signers whose `challenge` tag equals `challengeEventId` and who hold a credential event with a `tier` tag of 3 or higher in `credentialEvents`. It does not verify signatures or the issuers of those credentials.
90
106
  - `hasReachedRevocationThreshold(confirmations, threshold = 5)`: `boolean`
91
107
  - `parseChallenge(event)` / `parseRevocation(event)`: parsed object or `null`.
92
108
 
93
109
  ### Identity Bridge (kind 31000, type: identity-bridge)
94
- - `createIdentityBridge(anonPrivateKey, realPrivateKey, ring, signerIndex, ringMinTier, opts?)`: link anonymous account to verified identity via ring signature. `ring: string[]` of pubkeys (at least 5, `MIN_BRIDGE_RING_SIZE`); `opts: { occurredAt?: number }`. Signed by the anonymous key.
95
- - `selectDecoyRing(verifiedPubkeys, realPubkey, ringSize = 5)`: `{ ring: string[]; signerIndex: number }`
96
- - `verifyIdentityBridge(event, opts?)`: `Promise<boolean>`. `opts: { maxAgeSeconds?: number }` (default 24 hours; `0` disables the freshness check).
110
+ - `createIdentityBridge(anonPrivateKey: string, realPrivateKey: string, ring: string[], signerIndex: number, ringMinTier: SignetTier, opts?)`: `Promise<NostrEvent>`. Links an anonymous account to a verified identity via ring signature. `ring: string[]` is the anonymity set of verified pubkeys (at least 5, `MIN_BRIDGE_RING_SIZE`) and must contain the public key of `realPrivateKey`. `signerIndex` is the zero-based position in `ring` of that real public key (`selectDecoyRing` returns it for you); a wrong index makes ring signing throw (`Private key does not match ring member at signerIndex`). `ringMinTier` is the lowest verification tier among the ring members, which you assert yourself: it is written to the event's `ring-min-tier` tag and scales the bridge's contribution to `computeTrustScore` (`TRUST_WEIGHTS.IDENTITY_BRIDGE` of 50 × `ringMinTier` ÷ 4). Neither `createIdentityBridge` nor `verifyIdentityBridge` checks that the ring members hold that tier. `opts: { occurredAt?: number }`. Signed by the anonymous key.
111
+ - `selectDecoyRing(verifiedPubkeys: string[], realPubkey: string, ringSize = 5)`: `{ ring: string[]; signerIndex: number }`. Picks decoys from `verifiedPubkeys` (`realPubkey` is filtered out of the pool if present) and places `realPubkey` at a random position. Throws if the pool has fewer than `ringSize - 1` other pubkeys.
112
+ - `verifyIdentityBridge(event: NostrEvent, opts?)`: `Promise<boolean>`. `opts: { maxAgeSeconds?: number }` (default 24 hours; `0` disables the freshness check).
97
113
 
98
114
  ### ZKP Age Proofs
99
115
  - `createAgeRangeProof(age: number, ageRange: string, subjectPubkey?: string)`: `RangeProof` (synchronous). `subjectPubkey` binds the proof to one credential.
@@ -124,6 +140,8 @@ The voting extension (kinds 30482–30484) is specified in `spec/voting.md` and
124
140
 
125
141
  ## Optional
126
142
 
143
+ These paths ship in the npm package (`spec/`, `examples/`, and the `docs/` pages named below and in the experimental section above), so the relative links resolve inside `node_modules/signet-protocol/`. The examples import from the package's own `src/` directory; in your own code import from `signet-protocol`.
144
+
127
145
  - [llms-full.txt](llms-full.txt): llms.txt plus the full protocol and voting specifications and the developer guides in one file
128
146
  - [Full protocol specification](spec/protocol.md)
129
147
  - [Voting extension specification](spec/voting.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "signet-protocol",
3
- "version": "1.11.1",
3
+ "version": "1.11.3",
4
4
  "description": "Decentralised identity verification protocol for Nostr",
5
5
  "homepage": "https://github.com/forgesworn/signet#readme",
6
6
  "type": "module",
@@ -19,6 +19,12 @@
19
19
  "files": [
20
20
  "dist",
21
21
  "src",
22
+ "spec",
23
+ "examples",
24
+ "docs/signet-in-5-minutes.md",
25
+ "docs/implementation-levels.md",
26
+ "docs/private-vault-v1.md",
27
+ "docs/bot-ownership-v1.md",
22
28
  "llms.txt",
23
29
  "llms-full.txt"
24
30
  ],