signet-protocol 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +112 -0
  3. package/dist/anomaly.d.ts +42 -0
  4. package/dist/anomaly.d.ts.map +1 -0
  5. package/dist/anomaly.js +209 -0
  6. package/dist/anomaly.js.map +1 -0
  7. package/dist/badge.d.ts +56 -0
  8. package/dist/badge.d.ts.map +1 -0
  9. package/dist/badge.js +171 -0
  10. package/dist/badge.js.map +1 -0
  11. package/dist/bonds.d.ts +39 -0
  12. package/dist/bonds.d.ts.map +1 -0
  13. package/dist/bonds.js +149 -0
  14. package/dist/bonds.js.map +1 -0
  15. package/dist/challenges.d.ts +18 -0
  16. package/dist/challenges.d.ts.map +1 -0
  17. package/dist/challenges.js +145 -0
  18. package/dist/challenges.js.map +1 -0
  19. package/dist/cold-call.d.ts +74 -0
  20. package/dist/cold-call.d.ts.map +1 -0
  21. package/dist/cold-call.js +176 -0
  22. package/dist/cold-call.js.map +1 -0
  23. package/dist/compliance.d.ts +82 -0
  24. package/dist/compliance.d.ts.map +1 -0
  25. package/dist/compliance.js +478 -0
  26. package/dist/compliance.js.map +1 -0
  27. package/dist/connections.d.ts +63 -0
  28. package/dist/connections.d.ts.map +1 -0
  29. package/dist/connections.js +170 -0
  30. package/dist/connections.js.map +1 -0
  31. package/dist/constants.d.ts +86 -0
  32. package/dist/constants.d.ts.map +1 -0
  33. package/dist/constants.js +124 -0
  34. package/dist/constants.js.map +1 -0
  35. package/dist/credentials.d.ts +190 -0
  36. package/dist/credentials.d.ts.map +1 -0
  37. package/dist/credentials.js +686 -0
  38. package/dist/credentials.js.map +1 -0
  39. package/dist/crypto.d.ts +27 -0
  40. package/dist/crypto.d.ts.map +1 -0
  41. package/dist/crypto.js +75 -0
  42. package/dist/crypto.js.map +1 -0
  43. package/dist/errors.d.ts +17 -0
  44. package/dist/errors.d.ts.map +1 -0
  45. package/dist/errors.js +29 -0
  46. package/dist/errors.js.map +1 -0
  47. package/dist/i18n.d.ts +98 -0
  48. package/dist/i18n.d.ts.map +1 -0
  49. package/dist/i18n.js +1118 -0
  50. package/dist/i18n.js.map +1 -0
  51. package/dist/identity-bridge.d.ts +52 -0
  52. package/dist/identity-bridge.d.ts.map +1 -0
  53. package/dist/identity-bridge.js +228 -0
  54. package/dist/identity-bridge.js.map +1 -0
  55. package/dist/identity-tree.d.ts +47 -0
  56. package/dist/identity-tree.d.ts.map +1 -0
  57. package/dist/identity-tree.js +69 -0
  58. package/dist/identity-tree.js.map +1 -0
  59. package/dist/index.d.ts +55 -0
  60. package/dist/index.d.ts.map +1 -0
  61. package/dist/index.js +86 -0
  62. package/dist/index.js.map +1 -0
  63. package/dist/key-derivation.d.ts +43 -0
  64. package/dist/key-derivation.d.ts.map +1 -0
  65. package/dist/key-derivation.js +212 -0
  66. package/dist/key-derivation.js.map +1 -0
  67. package/dist/lsag.d.ts +23 -0
  68. package/dist/lsag.d.ts.map +1 -0
  69. package/dist/lsag.js +35 -0
  70. package/dist/lsag.js.map +1 -0
  71. package/dist/merkle.d.ts +19 -0
  72. package/dist/merkle.d.ts.map +1 -0
  73. package/dist/merkle.js +155 -0
  74. package/dist/merkle.js.map +1 -0
  75. package/dist/policies.d.ts +22 -0
  76. package/dist/policies.d.ts.map +1 -0
  77. package/dist/policies.js +123 -0
  78. package/dist/policies.js.map +1 -0
  79. package/dist/range-proof.d.ts +6 -0
  80. package/dist/range-proof.d.ts.map +1 -0
  81. package/dist/range-proof.js +45 -0
  82. package/dist/range-proof.js.map +1 -0
  83. package/dist/relay.d.ts +106 -0
  84. package/dist/relay.d.ts.map +1 -0
  85. package/dist/relay.js +336 -0
  86. package/dist/relay.js.map +1 -0
  87. package/dist/ring-signature.d.ts +35 -0
  88. package/dist/ring-signature.d.ts.map +1 -0
  89. package/dist/ring-signature.js +56 -0
  90. package/dist/ring-signature.js.map +1 -0
  91. package/dist/shamir.d.ts +55 -0
  92. package/dist/shamir.d.ts.map +1 -0
  93. package/dist/shamir.js +253 -0
  94. package/dist/shamir.js.map +1 -0
  95. package/dist/signet-words.d.ts +42 -0
  96. package/dist/signet-words.d.ts.map +1 -0
  97. package/dist/signet-words.js +82 -0
  98. package/dist/signet-words.js.map +1 -0
  99. package/dist/store.d.ts +65 -0
  100. package/dist/store.d.ts.map +1 -0
  101. package/dist/store.js +290 -0
  102. package/dist/store.js.map +1 -0
  103. package/dist/trust-score.d.ts +9 -0
  104. package/dist/trust-score.d.ts.map +1 -0
  105. package/dist/trust-score.js +186 -0
  106. package/dist/trust-score.js.map +1 -0
  107. package/dist/types.d.ts +358 -0
  108. package/dist/types.d.ts.map +1 -0
  109. package/dist/types.js +15 -0
  110. package/dist/types.js.map +1 -0
  111. package/dist/utils.d.ts +11 -0
  112. package/dist/utils.d.ts.map +1 -0
  113. package/dist/utils.js +21 -0
  114. package/dist/utils.js.map +1 -0
  115. package/dist/validation.d.ts +33 -0
  116. package/dist/validation.d.ts.map +1 -0
  117. package/dist/validation.js +312 -0
  118. package/dist/validation.js.map +1 -0
  119. package/dist/verifiers.d.ts +18 -0
  120. package/dist/verifiers.d.ts.map +1 -0
  121. package/dist/verifiers.js +118 -0
  122. package/dist/verifiers.js.map +1 -0
  123. package/dist/vouches.d.ts +14 -0
  124. package/dist/vouches.d.ts.map +1 -0
  125. package/dist/vouches.js +103 -0
  126. package/dist/vouches.js.map +1 -0
  127. package/package.json +76 -0
  128. package/src/anomaly.ts +307 -0
  129. package/src/badge.ts +208 -0
  130. package/src/bonds.ts +203 -0
  131. package/src/challenges.ts +187 -0
  132. package/src/cold-call.ts +238 -0
  133. package/src/compliance.ts +612 -0
  134. package/src/connections.ts +216 -0
  135. package/src/constants.ts +146 -0
  136. package/src/credentials.ts +908 -0
  137. package/src/crypto.ts +85 -0
  138. package/src/errors.ts +31 -0
  139. package/src/i18n.ts +1347 -0
  140. package/src/identity-bridge.ts +262 -0
  141. package/src/identity-tree.ts +90 -0
  142. package/src/index.ts +452 -0
  143. package/src/lsag.ts +53 -0
  144. package/src/merkle.ts +176 -0
  145. package/src/policies.ts +154 -0
  146. package/src/range-proof.ts +66 -0
  147. package/src/relay.ts +433 -0
  148. package/src/ring-signature.ts +76 -0
  149. package/src/signet-words.ts +122 -0
  150. package/src/store.ts +336 -0
  151. package/src/trust-score.ts +208 -0
  152. package/src/types.ts +482 -0
  153. package/src/utils.ts +20 -0
  154. package/src/validation.ts +391 -0
  155. package/src/verifiers.ts +156 -0
  156. package/src/vouches.ts +141 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ForgeSworn
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,112 @@
1
+ # Signet
2
+
3
+ **Decentralised identity verification for Nostr.**
4
+
5
+ Signet is an open protocol that enables users to prove claims about their identity — age, parenthood, professional status — using zero-knowledge proofs, without revealing personal data or relying on a central authority.
6
+
7
+ ## What It Does
8
+
9
+ - **4 verification tiers**: self-declared → web-of-trust → professional adult → professional adult+child
10
+ - **Signet IQ** (0-200, where 100 = government standard): weighted by professional verification > in-person vouches > online vouches > account age
11
+ - **Verifier anti-corruption**: 6 layers of accountability for professional verifiers
12
+ - **ZKP age proofs**: prove "child aged 8-12" without revealing exact date of birth
13
+ - **Blue checkmarks for Nostr**: decentralised, cryptographic social proof for everyone
14
+ - **Community policies**: any relay, client, or community sets their own minimum verification requirements
15
+
16
+ ## Why
17
+
18
+ Nostr has no identity layer. Anyone can claim to be anyone. This matters for child safety (a predator can create a fake child account), for trust (no way to prove "I met this person"), and for regulation (age verification laws are coming worldwide).
19
+
20
+ Signet solves this without centralised data collection. A professional verifies your identity in person. A ZKP credential attests to the result. No personal data is stored. No database to breach.
21
+
22
+ ## Quick Start
23
+
24
+ ```bash
25
+ npm install signet-protocol
26
+ ```
27
+
28
+ Display a verification badge for any Nostr user (Level 1 — a weekend to integrate):
29
+
30
+ ```typescript
31
+ import { computeBadge, buildBadgeFilters, meetsMinimumTier } from 'signet-protocol';
32
+
33
+ const filters = buildBadgeFilters(['<hex-pubkey>']);
34
+ const events = await fetchFromRelay(filters);
35
+ const badge = computeBadge('<hex-pubkey>', events, { verifySignatures: true });
36
+
37
+ // badge.tier => 3, badge.score => 106, badge.displayLabel => "Verified (Tier 3)"
38
+ if (meetsMinimumTier(badge, 2)) {
39
+ // allow posting in verified-only community
40
+ }
41
+ ```
42
+
43
+ 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.
44
+
45
+ ## Protocol
46
+
47
+ The full specification is at [`spec/protocol.md`](spec/protocol.md).
48
+
49
+ ### Event Kinds
50
+
51
+ All identity attestations use a single NIP-VA kind (31000), differentiated by `type` tag:
52
+
53
+ | Kind | Type Tag | Name | Purpose |
54
+ |------|----------|------|---------|
55
+ | 31000 | `credential` | Verification Credential | Attests to a subject's verification tier |
56
+ | 31000 | `vouch` | Vouch Attestation | Peer vouch for another user |
57
+ | 30078 | — | Community Verification Policy | Minimum verification requirements (NIP-78) |
58
+ | 31000 | `verifier` | Verifier Credential | Professional declares verifier status |
59
+ | 31000 | `challenge` | Verifier Challenge | Challenge a verifier's legitimacy |
60
+ | 31000 | `revocation` | Verifier Revocation | Community confirms challenge, revokes verifier |
61
+ | 31000 | `identity-bridge` | Identity Bridge | Link two keypairs via ring signature |
62
+ | 31000 | `delegation` | Delegation | Guardian or agent delegation with scoped permission |
63
+ | 30482 | — | Election | Voting extension: define an election |
64
+ | 30483 | — | Ballot | Voting extension: cast an anonymous ballot |
65
+ | 30484 | — | Election Result | Voting extension: publish tallied result |
66
+
67
+ ### Crypto Stack
68
+
69
+ ```
70
+ Layer 1: Schnorr (secp256k1) — zero new dependencies
71
+ Credential signing, Merkle selective disclosure,
72
+ ring signatures for issuer privacy, MuSig2
73
+
74
+ Layer 2: Bulletproofs (secp256k1) — age range proofs
75
+ ~700 byte proofs, no trusted setup
76
+
77
+ Layer 3: General-purpose ZK (future, if needed)
78
+ Complex threshold proofs, recursive composition
79
+ ```
80
+
81
+ ## Status
82
+
83
+ **v0.1.0** — spec complete, TypeScript library implemented, reference apps functional. Seeking community feedback.
84
+
85
+ Kind numbers are placeholders pending NIP allocation.
86
+
87
+ ## Implementation
88
+
89
+ This repo includes:
90
+ - `src/` — TypeScript protocol library (`signet-protocol` on npm)
91
+
92
+ Signet is a standalone protocol. Any Nostr client can implement it independently.
93
+
94
+ ## Regulatory Compatibility
95
+
96
+ Signet is designed to satisfy identity verification requirements across jurisdictions:
97
+
98
+ | Regulation | Position |
99
+ |------------|----------|
100
+ | **UK Online Safety Act** | Tier 4 exceeds Ofcom's "highly effective age assurance" standard |
101
+ | **US COPPA** | No PII collected — exceeds FTC's March 2026 flexibility policy |
102
+ | **EU eIDAS 2.0** | Compatible with selective disclosure direction |
103
+ | **ISO/IEC 27566-1:2025** | Compatible with outcomes-based age assurance framework |
104
+ | **Australia under-16 ban** | Proves age range without central verification |
105
+
106
+ ## Contributing
107
+
108
+ Feedback, NIP discussion, and contributions are welcome. Open an issue or submit a PR.
109
+
110
+ ## Licence
111
+
112
+ MIT
@@ -0,0 +1,42 @@
1
+ import type { NostrEvent } from './types.js';
2
+ /** Types of anomaly flags */
3
+ export type AnomalyType = 'volume' | 'temporal' | 'geographic' | 'pattern';
4
+ /** A detected anomaly */
5
+ export interface AnomalyFlag {
6
+ type: AnomalyType;
7
+ verifierPubkey: string;
8
+ severity: 'low' | 'medium' | 'high';
9
+ description: string;
10
+ evidence: {
11
+ metric: string;
12
+ value: number;
13
+ threshold: number;
14
+ };
15
+ }
16
+ /** Anomaly detection configuration */
17
+ export interface AnomalyConfig {
18
+ /** Max credentials per week before flagging (default: 20) */
19
+ maxWeeklyVolume: number;
20
+ /** Max credentials per hour before flagging (default: 5) */
21
+ maxHourlyVolume: number;
22
+ /** Max percentage of credentials in a single foreign jurisdiction (default: 30) */
23
+ maxForeignJurisdictionPercent: number;
24
+ /** Min time between consecutive credentials in seconds (default: 300 = 5 min) */
25
+ minTimeBetweenCredentials: number;
26
+ /** Volume multiplier vs network average to flag (default: 5) */
27
+ volumeMultiplierThreshold: number;
28
+ }
29
+ /**
30
+ * Analyze a verifier's issuance patterns for anomalies.
31
+ *
32
+ * @param verifierPubkey - The verifier to analyze
33
+ * @param allCredentials - All credential events (kind 31000, type: credential) in the store
34
+ * @param verifierCredential - The verifier's own credential (kind 31000, type: verifier)
35
+ * @param config - Detection thresholds
36
+ */
37
+ export declare function detectAnomalies(verifierPubkey: string, allCredentials: NostrEvent[], verifierCredential?: NostrEvent, config?: Partial<AnomalyConfig>): AnomalyFlag[];
38
+ /**
39
+ * Get a summary of all flagged verifiers from a set of credentials.
40
+ */
41
+ export declare function scanForAnomalies(allCredentials: NostrEvent[], verifierCredentials: NostrEvent[], config?: Partial<AnomalyConfig>): Map<string, AnomalyFlag[]>;
42
+ //# sourceMappingURL=anomaly.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"anomaly.d.ts","sourceRoot":"","sources":["../src/anomaly.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,6BAA6B;AAC7B,MAAM,MAAM,WAAW,GAAG,QAAQ,GAAG,UAAU,GAAG,YAAY,GAAG,SAAS,CAAC;AAE3E,yBAAyB;AACzB,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,WAAW,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;IACpC,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE;QACR,MAAM,EAAE,MAAM,CAAC;QACf,KAAK,EAAE,MAAM,CAAC;QACd,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;CACH;AAED,sCAAsC;AACtC,MAAM,WAAW,aAAa;IAC5B,6DAA6D;IAC7D,eAAe,EAAE,MAAM,CAAC;IACxB,4DAA4D;IAC5D,eAAe,EAAE,MAAM,CAAC;IACxB,mFAAmF;IACnF,6BAA6B,EAAE,MAAM,CAAC;IACtC,iFAAiF;IACjF,yBAAyB,EAAE,MAAM,CAAC;IAClC,gEAAgE;IAChE,yBAAyB,EAAE,MAAM,CAAC;CACnC;AAUD;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,cAAc,EAAE,MAAM,EACtB,cAAc,EAAE,UAAU,EAAE,EAC5B,kBAAkB,CAAC,EAAE,UAAU,EAC/B,MAAM,GAAE,OAAO,CAAC,aAAa,CAAM,GAClC,WAAW,EAAE,CA6Bf;AA+LD;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,cAAc,EAAE,UAAU,EAAE,EAC5B,mBAAmB,EAAE,UAAU,EAAE,EACjC,MAAM,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,GAC9B,GAAG,CAAC,MAAM,EAAE,WAAW,EAAE,CAAC,CAoB5B"}
@@ -0,0 +1,209 @@
1
+ // Verifier Anomaly Detection
2
+ // Statistical analysis of verifier issuance patterns
3
+ // Layer 3 of the anti-corruption framework
4
+ import { ATTESTATION_KIND, ATTESTATION_TYPES } from './constants.js';
5
+ import { getTagValue } from './validation.js';
6
+ const DEFAULT_CONFIG = {
7
+ maxWeeklyVolume: 20,
8
+ maxHourlyVolume: 5,
9
+ maxForeignJurisdictionPercent: 30,
10
+ minTimeBetweenCredentials: 300,
11
+ volumeMultiplierThreshold: 5,
12
+ };
13
+ /**
14
+ * Analyze a verifier's issuance patterns for anomalies.
15
+ *
16
+ * @param verifierPubkey - The verifier to analyze
17
+ * @param allCredentials - All credential events (kind 31000, type: credential) in the store
18
+ * @param verifierCredential - The verifier's own credential (kind 31000, type: verifier)
19
+ * @param config - Detection thresholds
20
+ */
21
+ export function detectAnomalies(verifierPubkey, allCredentials, verifierCredential, config = {}) {
22
+ const cfg = { ...DEFAULT_CONFIG, ...config };
23
+ const flags = [];
24
+ // Get this verifier's issued credentials
25
+ const issued = allCredentials.filter((e) => e.kind === ATTESTATION_KIND && getTagValue(e, 'type') === ATTESTATION_TYPES.CREDENTIAL && e.pubkey === verifierPubkey);
26
+ if (issued.length === 0)
27
+ return flags;
28
+ // Sort by time
29
+ const sorted = [...issued].sort((a, b) => a.created_at - b.created_at);
30
+ // --- Volume Analysis ---
31
+ flags.push(...analyzeVolume(verifierPubkey, sorted, allCredentials, cfg));
32
+ // --- Temporal Analysis ---
33
+ flags.push(...analyzeTemporal(verifierPubkey, sorted, cfg));
34
+ // --- Geographic Analysis ---
35
+ if (verifierCredential) {
36
+ flags.push(...analyzeGeographic(verifierPubkey, sorted, verifierCredential, cfg));
37
+ }
38
+ // --- Pattern Analysis ---
39
+ flags.push(...analyzePatterns(verifierPubkey, sorted));
40
+ return flags;
41
+ }
42
+ /** Volume anomaly detection */
43
+ function analyzeVolume(verifierPubkey, issued, allCredentials, cfg) {
44
+ const flags = [];
45
+ const now = Math.floor(Date.now() / 1000);
46
+ const oneWeek = 7 * 24 * 60 * 60;
47
+ const oneHour = 60 * 60;
48
+ // Weekly volume
49
+ const weeklyCount = issued.filter((e) => e.created_at > now - oneWeek).length;
50
+ if (weeklyCount > cfg.maxWeeklyVolume) {
51
+ flags.push({
52
+ type: 'volume',
53
+ verifierPubkey,
54
+ severity: weeklyCount > cfg.maxWeeklyVolume * 3 ? 'high' : 'medium',
55
+ description: `Issued ${weeklyCount} credentials this week (threshold: ${cfg.maxWeeklyVolume})`,
56
+ evidence: { metric: 'weekly_volume', value: weeklyCount, threshold: cfg.maxWeeklyVolume },
57
+ });
58
+ }
59
+ // Hourly volume (burst detection)
60
+ const hourlyCount = issued.filter((e) => e.created_at > now - oneHour).length;
61
+ if (hourlyCount > cfg.maxHourlyVolume) {
62
+ flags.push({
63
+ type: 'volume',
64
+ verifierPubkey,
65
+ severity: 'high',
66
+ description: `Issued ${hourlyCount} credentials in the last hour (threshold: ${cfg.maxHourlyVolume})`,
67
+ evidence: { metric: 'hourly_volume', value: hourlyCount, threshold: cfg.maxHourlyVolume },
68
+ });
69
+ }
70
+ // Compare to network average
71
+ const allVerifiers = new Set(allCredentials.filter((e) => e.kind === ATTESTATION_KIND && getTagValue(e, 'type') === ATTESTATION_TYPES.CREDENTIAL).map((e) => e.pubkey));
72
+ if (allVerifiers.size > 1) {
73
+ const totalCredentials = allCredentials.filter((e) => e.kind === ATTESTATION_KIND && getTagValue(e, 'type') === ATTESTATION_TYPES.CREDENTIAL && e.created_at > now - oneWeek).length;
74
+ const networkAvg = totalCredentials / allVerifiers.size;
75
+ if (networkAvg > 0 && weeklyCount > networkAvg * cfg.volumeMultiplierThreshold) {
76
+ flags.push({
77
+ type: 'volume',
78
+ verifierPubkey,
79
+ severity: 'high',
80
+ description: `Weekly volume ${weeklyCount} is ${(weeklyCount / networkAvg).toFixed(1)}x the network average of ${networkAvg.toFixed(1)}`,
81
+ evidence: {
82
+ metric: 'volume_vs_average',
83
+ value: weeklyCount / networkAvg,
84
+ threshold: cfg.volumeMultiplierThreshold,
85
+ },
86
+ });
87
+ }
88
+ }
89
+ return flags;
90
+ }
91
+ /** Temporal anomaly detection */
92
+ function analyzeTemporal(verifierPubkey, sorted, cfg) {
93
+ const flags = [];
94
+ if (sorted.length < 2)
95
+ return flags;
96
+ // Check minimum time between consecutive credentials
97
+ let rapidCount = 0;
98
+ let minGap = Infinity;
99
+ for (let i = 1; i < sorted.length; i++) {
100
+ const gap = sorted[i].created_at - sorted[i - 1].created_at;
101
+ if (gap < cfg.minTimeBetweenCredentials) {
102
+ rapidCount++;
103
+ }
104
+ if (gap < minGap)
105
+ minGap = gap;
106
+ }
107
+ if (rapidCount > 0) {
108
+ flags.push({
109
+ type: 'temporal',
110
+ verifierPubkey,
111
+ severity: rapidCount > 5 ? 'high' : 'medium',
112
+ description: `${rapidCount} credential pairs issued less than ${cfg.minTimeBetweenCredentials}s apart (min gap: ${minGap}s). Suggests rubber-stamping, not in-person verification.`,
113
+ evidence: { metric: 'rapid_issuance_count', value: rapidCount, threshold: 0 },
114
+ });
115
+ }
116
+ return flags;
117
+ }
118
+ /** Geographic anomaly detection */
119
+ function analyzeGeographic(verifierPubkey, issued, verifierCredential, cfg) {
120
+ const flags = [];
121
+ const verifierJurisdiction = getTagValue(verifierCredential, 'jurisdiction');
122
+ if (!verifierJurisdiction)
123
+ return flags;
124
+ // Count jurisdictions in issued credentials
125
+ const jurisdictionCounts = new Map();
126
+ for (const cred of issued) {
127
+ const j = getTagValue(cred, 'jurisdiction') || 'unknown';
128
+ jurisdictionCounts.set(j, (jurisdictionCounts.get(j) || 0) + 1);
129
+ }
130
+ // Check for high foreign jurisdiction percentage
131
+ for (const [jurisdiction, count] of jurisdictionCounts) {
132
+ if (jurisdiction === verifierJurisdiction)
133
+ continue;
134
+ const percent = (count / issued.length) * 100;
135
+ if (percent > cfg.maxForeignJurisdictionPercent) {
136
+ flags.push({
137
+ type: 'geographic',
138
+ verifierPubkey,
139
+ severity: percent > 60 ? 'high' : 'medium',
140
+ description: `${percent.toFixed(0)}% of credentials (${count}/${issued.length}) issued in ${jurisdiction}, but verifier is registered in ${verifierJurisdiction}`,
141
+ evidence: {
142
+ metric: 'foreign_jurisdiction_percent',
143
+ value: percent,
144
+ threshold: cfg.maxForeignJurisdictionPercent,
145
+ },
146
+ });
147
+ }
148
+ }
149
+ return flags;
150
+ }
151
+ /** Pattern anomaly detection */
152
+ function analyzePatterns(verifierPubkey, sorted) {
153
+ const flags = [];
154
+ if (sorted.length < 5)
155
+ return flags;
156
+ // Check for repeated subjects (same person verified multiple times)
157
+ const subjectCounts = new Map();
158
+ for (const cred of sorted) {
159
+ const subject = getTagValue(cred, 'd') || '';
160
+ subjectCounts.set(subject, (subjectCounts.get(subject) || 0) + 1);
161
+ }
162
+ const duplicates = Array.from(subjectCounts.entries()).filter(([, count]) => count > 1);
163
+ if (duplicates.length > 0) {
164
+ const totalDupes = duplicates.reduce((sum, [, count]) => sum + count - 1, 0);
165
+ flags.push({
166
+ type: 'pattern',
167
+ verifierPubkey,
168
+ severity: 'low',
169
+ description: `${duplicates.length} subjects verified multiple times (${totalDupes} extra verifications)`,
170
+ evidence: { metric: 'duplicate_subjects', value: duplicates.length, threshold: 0 },
171
+ });
172
+ }
173
+ // Check for disproportionate tier 4 issuance
174
+ let tier4Count = 0;
175
+ for (const cred of sorted) {
176
+ if (getTagValue(cred, 'tier') === '4')
177
+ tier4Count++;
178
+ }
179
+ const tier4Percent = (tier4Count / sorted.length) * 100;
180
+ if (tier4Count > 3 && tier4Percent > 80) {
181
+ flags.push({
182
+ type: 'pattern',
183
+ verifierPubkey,
184
+ severity: 'medium',
185
+ description: `${tier4Percent.toFixed(0)}% of credentials are Tier 4 (${tier4Count}/${sorted.length}). Unusually high child verification rate.`,
186
+ evidence: { metric: 'tier4_percent', value: tier4Percent, threshold: 80 },
187
+ });
188
+ }
189
+ return flags;
190
+ }
191
+ /**
192
+ * Get a summary of all flagged verifiers from a set of credentials.
193
+ */
194
+ export function scanForAnomalies(allCredentials, verifierCredentials, config) {
195
+ const results = new Map();
196
+ // Get unique verifier pubkeys from credentials
197
+ const verifiers = new Set(allCredentials
198
+ .filter((e) => e.kind === ATTESTATION_KIND && getTagValue(e, 'type') === ATTESTATION_TYPES.CREDENTIAL)
199
+ .map((e) => e.pubkey));
200
+ for (const pubkey of verifiers) {
201
+ const verifierCred = verifierCredentials.find((c) => c.pubkey === pubkey);
202
+ const flags = detectAnomalies(pubkey, allCredentials, verifierCred, config);
203
+ if (flags.length > 0) {
204
+ results.set(pubkey, flags);
205
+ }
206
+ }
207
+ return results;
208
+ }
209
+ //# sourceMappingURL=anomaly.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"anomaly.js","sourceRoot":"","sources":["../src/anomaly.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,56 @@
1
+ import type { NostrEvent, SignetTier } from './types.js';
2
+ export interface BadgeInfo {
3
+ /** The pubkey this badge describes */
4
+ pubkey: string;
5
+ /** Highest verification tier (1-4) */
6
+ tier: SignetTier;
7
+ /** Human-readable tier label */
8
+ tierLabel: string;
9
+ /** Signet Score (0-200) */
10
+ score: number;
11
+ /** Whether the user has any valid credentials */
12
+ isVerified: boolean;
13
+ /** Short display string, e.g. "Verified (Tier 3)" */
14
+ displayLabel: string;
15
+ /** Number of valid credentials */
16
+ credentialCount: number;
17
+ /** Number of valid vouches */
18
+ vouchCount: number;
19
+ }
20
+ export type TrustLevel = 'unverified' | 'self-declared' | 'vouched' | 'professional' | 'professional-child';
21
+ /**
22
+ * Compute badge info for a pubkey from their credentials and vouches.
23
+ * This is the main entry point for Level 1 integration.
24
+ *
25
+ * @param pubkey - The Nostr pubkey to compute a badge for
26
+ * @param events - All relevant kind 31000 (`type: credential` and `type: vouch`) events
27
+ * @param options - Optional configuration
28
+ * @returns Badge info for display
29
+ */
30
+ export declare function computeBadge(pubkey: string, events: NostrEvent[], options?: {
31
+ verifySignatures?: boolean;
32
+ now?: number;
33
+ }): Promise<BadgeInfo>;
34
+ /**
35
+ * Get the trust level classification for a badge.
36
+ */
37
+ export declare function getTrustLevel(badge: BadgeInfo): TrustLevel;
38
+ /**
39
+ * Check if a pubkey has at least the required tier.
40
+ * Useful for gating access to features or communities.
41
+ */
42
+ export declare function meetsMinimumTier(badge: BadgeInfo, minTier: SignetTier): boolean;
43
+ /**
44
+ * Filter a set of events to only those relevant for a given pubkey.
45
+ * Useful when you have a mixed bag of events from a relay query.
46
+ */
47
+ export declare function filterEventsForPubkey(pubkey: string, events: NostrEvent[]): NostrEvent[];
48
+ /**
49
+ * Build a Nostr filter for fetching badge-relevant events for one or more pubkeys.
50
+ * Returns filters suitable for use with REQ messages.
51
+ */
52
+ export declare function buildBadgeFilters(pubkeys: string[]): Array<{
53
+ kinds: number[];
54
+ '#d': string[];
55
+ }>;
56
+ //# sourceMappingURL=badge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"badge.d.ts","sourceRoot":"","sources":["../src/badge.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAIzD,MAAM,WAAW,SAAS;IACxB,sCAAsC;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,IAAI,EAAE,UAAU,CAAC;IACjB,gCAAgC;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,2BAA2B;IAC3B,KAAK,EAAE,MAAM,CAAC;IACd,iDAAiD;IACjD,UAAU,EAAE,OAAO,CAAC;IACpB,qDAAqD;IACrD,YAAY,EAAE,MAAM,CAAC;IACrB,kCAAkC;IAClC,eAAe,EAAE,MAAM,CAAC;IACxB,8BAA8B;IAC9B,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,UAAU,GAAG,YAAY,GAAG,eAAe,GAAG,SAAS,GAAG,cAAc,GAAG,oBAAoB,CAAC;AAkB5G;;;;;;;;GAQG;AACH,wBAAsB,YAAY,CAChC,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,UAAU,EAAE,EACpB,OAAO,CAAC,EAAE;IAAE,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GACrD,OAAO,CAAC,SAAS,CAAC,CAiGpB;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,GAAG,UAAU,CAG1D;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,UAAU,GAAG,OAAO,CAE/E;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,UAAU,EAAE,CAYxF;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,KAAK,CAAC;IAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAS/F"}
package/dist/badge.js ADDED
@@ -0,0 +1,171 @@
1
+ // Level 1 Badge Display Library
2
+ // Minimal drop-in module for Nostr clients to display Signet trust badges.
3
+ // Requires only kind 31000 (type: credential and type: vouch) and Schnorr verification.
4
+ import { ATTESTATION_KIND, ATTESTATION_TYPES, TRUST_WEIGHTS, MAX_TRUST_SCORE } from './constants.js';
5
+ import { verifyEvent } from './crypto.js';
6
+ import { getTagValue } from './validation.js';
7
+ const TIER_LABELS = {
8
+ 1: 'Self-declared',
9
+ 2: 'Web-of-trust',
10
+ 3: 'Verified',
11
+ 4: 'Verified (Child Safety)',
12
+ };
13
+ const TIER_TO_TRUST_LEVEL = {
14
+ 1: 'self-declared',
15
+ 2: 'vouched',
16
+ 3: 'professional',
17
+ 4: 'professional-child',
18
+ };
19
+ // --- Core Functions ---
20
+ /**
21
+ * Compute badge info for a pubkey from their credentials and vouches.
22
+ * This is the main entry point for Level 1 integration.
23
+ *
24
+ * @param pubkey - The Nostr pubkey to compute a badge for
25
+ * @param events - All relevant kind 31000 (`type: credential` and `type: vouch`) events
26
+ * @param options - Optional configuration
27
+ * @returns Badge info for display
28
+ */
29
+ export async function computeBadge(pubkey, events, options) {
30
+ const now = options?.now ?? Math.floor(Date.now() / 1000);
31
+ const verify = options?.verifySignatures ?? false;
32
+ let highestTier = 1;
33
+ let rawScore = 0;
34
+ let credentialCount = 0;
35
+ let hasAnyCredential = false;
36
+ // Process credentials
37
+ for (const event of events) {
38
+ if (event.kind !== ATTESTATION_KIND)
39
+ continue;
40
+ if (getTagValue(event, 'type') !== ATTESTATION_TYPES.CREDENTIAL)
41
+ continue;
42
+ const dTag = getTagValue(event, 'd') || '';
43
+ const pTag = getTagValue(event, 'p');
44
+ let subject;
45
+ if (dTag.startsWith('assertion:') && pTag) {
46
+ subject = pTag;
47
+ }
48
+ else {
49
+ subject = dTag.startsWith('credential:') ? dTag.slice('credential:'.length) : dTag;
50
+ }
51
+ if (subject !== pubkey)
52
+ continue;
53
+ // Check expiry — NaN must be treated as expired (not perpetually valid)
54
+ const expires = getTagValue(event, 'expiration');
55
+ if (expires) {
56
+ const exp = parseInt(expires, 10);
57
+ if (isNaN(exp) || exp < now)
58
+ continue;
59
+ }
60
+ // Optional signature verification
61
+ if (verify && !await verifyEvent(event))
62
+ continue;
63
+ hasAnyCredential = true;
64
+ credentialCount++;
65
+ const rawTier = parseInt(getTagValue(event, 'tier') || '1', 10);
66
+ const tier = (rawTier >= 1 && rawTier <= 4 ? rawTier : 1);
67
+ if (tier > highestTier)
68
+ highestTier = tier;
69
+ const verificationType = getTagValue(event, 'verification-type');
70
+ if (verificationType === 'professional') {
71
+ rawScore += TRUST_WEIGHTS.PROFESSIONAL_VERIFICATION;
72
+ }
73
+ }
74
+ // Process vouches
75
+ const vouchersSeen = new Set();
76
+ let vouchCount = 0;
77
+ for (const event of events) {
78
+ if (event.kind !== ATTESTATION_KIND)
79
+ continue;
80
+ if (getTagValue(event, 'type') !== ATTESTATION_TYPES.VOUCH)
81
+ continue;
82
+ const dTag = getTagValue(event, 'd') || '';
83
+ const subject = dTag.startsWith('vouch:') ? dTag.slice('vouch:'.length) : dTag;
84
+ if (subject !== pubkey)
85
+ continue;
86
+ // One vouch per voucher
87
+ if (vouchersSeen.has(event.pubkey))
88
+ continue;
89
+ vouchersSeen.add(event.pubkey);
90
+ if (verify && !await verifyEvent(event))
91
+ continue;
92
+ vouchCount++;
93
+ const method = getTagValue(event, 'method');
94
+ const rawVoucherScore = parseInt(getTagValue(event, 'voucher-score') || '50', 10);
95
+ const voucherScore = isNaN(rawVoucherScore) ? 50 : Math.max(0, Math.min(rawVoucherScore, MAX_TRUST_SCORE));
96
+ const multiplier = voucherScore / MAX_TRUST_SCORE;
97
+ if (method === 'in-person') {
98
+ rawScore += TRUST_WEIGHTS.IN_PERSON_VOUCH * multiplier;
99
+ }
100
+ else {
101
+ rawScore += TRUST_WEIGHTS.ONLINE_VOUCH * multiplier;
102
+ }
103
+ }
104
+ const score = Math.min(Math.round(rawScore), MAX_TRUST_SCORE);
105
+ const tierLabel = TIER_LABELS[highestTier];
106
+ let displayLabel;
107
+ if (!hasAnyCredential && vouchCount === 0) {
108
+ displayLabel = 'Unverified';
109
+ }
110
+ else {
111
+ displayLabel = `${tierLabel} (Tier ${highestTier})`;
112
+ }
113
+ return {
114
+ pubkey,
115
+ tier: highestTier,
116
+ tierLabel,
117
+ score,
118
+ isVerified: hasAnyCredential,
119
+ displayLabel,
120
+ credentialCount,
121
+ vouchCount,
122
+ };
123
+ }
124
+ /**
125
+ * Get the trust level classification for a badge.
126
+ */
127
+ export function getTrustLevel(badge) {
128
+ if (!badge.isVerified && badge.vouchCount === 0)
129
+ return 'unverified';
130
+ return TIER_TO_TRUST_LEVEL[badge.tier];
131
+ }
132
+ /**
133
+ * Check if a pubkey has at least the required tier.
134
+ * Useful for gating access to features or communities.
135
+ */
136
+ export function meetsMinimumTier(badge, minTier) {
137
+ return badge.isVerified && badge.tier >= minTier;
138
+ }
139
+ /**
140
+ * Filter a set of events to only those relevant for a given pubkey.
141
+ * Useful when you have a mixed bag of events from a relay query.
142
+ */
143
+ export function filterEventsForPubkey(pubkey, events) {
144
+ return events.filter(event => {
145
+ if (event.kind !== ATTESTATION_KIND)
146
+ return false;
147
+ const eventType = getTagValue(event, 'type');
148
+ if (eventType !== ATTESTATION_TYPES.CREDENTIAL && eventType !== ATTESTATION_TYPES.VOUCH) {
149
+ return false;
150
+ }
151
+ const dTag = getTagValue(event, 'd') || '';
152
+ // Strip type prefix from d-tag to get subject
153
+ const subject = dTag.includes(':') ? dTag.slice(dTag.indexOf(':') + 1) : dTag;
154
+ return subject === pubkey;
155
+ });
156
+ }
157
+ /**
158
+ * Build a Nostr filter for fetching badge-relevant events for one or more pubkeys.
159
+ * Returns filters suitable for use with REQ messages.
160
+ */
161
+ export function buildBadgeFilters(pubkeys) {
162
+ // With the generic attestation kind, d-tags are now prefixed with the type
163
+ const dTags = pubkeys.flatMap(pk => [`credential:${pk}`, `vouch:${pk}`]);
164
+ return [
165
+ {
166
+ kinds: [ATTESTATION_KIND],
167
+ '#d': dTags,
168
+ },
169
+ ];
170
+ }
171
+ //# sourceMappingURL=badge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"badge.js","sourceRoot":"","sources":["../src/badge.ts"],"names":[],"mappings":"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"}