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.
- package/LICENSE +21 -0
- package/README.md +112 -0
- package/dist/anomaly.d.ts +42 -0
- package/dist/anomaly.d.ts.map +1 -0
- package/dist/anomaly.js +209 -0
- package/dist/anomaly.js.map +1 -0
- package/dist/badge.d.ts +56 -0
- package/dist/badge.d.ts.map +1 -0
- package/dist/badge.js +171 -0
- package/dist/badge.js.map +1 -0
- package/dist/bonds.d.ts +39 -0
- package/dist/bonds.d.ts.map +1 -0
- package/dist/bonds.js +149 -0
- package/dist/bonds.js.map +1 -0
- package/dist/challenges.d.ts +18 -0
- package/dist/challenges.d.ts.map +1 -0
- package/dist/challenges.js +145 -0
- package/dist/challenges.js.map +1 -0
- package/dist/cold-call.d.ts +74 -0
- package/dist/cold-call.d.ts.map +1 -0
- package/dist/cold-call.js +176 -0
- package/dist/cold-call.js.map +1 -0
- package/dist/compliance.d.ts +82 -0
- package/dist/compliance.d.ts.map +1 -0
- package/dist/compliance.js +478 -0
- package/dist/compliance.js.map +1 -0
- package/dist/connections.d.ts +63 -0
- package/dist/connections.d.ts.map +1 -0
- package/dist/connections.js +170 -0
- package/dist/connections.js.map +1 -0
- package/dist/constants.d.ts +86 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +124 -0
- package/dist/constants.js.map +1 -0
- package/dist/credentials.d.ts +190 -0
- package/dist/credentials.d.ts.map +1 -0
- package/dist/credentials.js +686 -0
- package/dist/credentials.js.map +1 -0
- package/dist/crypto.d.ts +27 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +75 -0
- package/dist/crypto.js.map +1 -0
- package/dist/errors.d.ts +17 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +29 -0
- package/dist/errors.js.map +1 -0
- package/dist/i18n.d.ts +98 -0
- package/dist/i18n.d.ts.map +1 -0
- package/dist/i18n.js +1118 -0
- package/dist/i18n.js.map +1 -0
- package/dist/identity-bridge.d.ts +52 -0
- package/dist/identity-bridge.d.ts.map +1 -0
- package/dist/identity-bridge.js +228 -0
- package/dist/identity-bridge.js.map +1 -0
- package/dist/identity-tree.d.ts +47 -0
- package/dist/identity-tree.d.ts.map +1 -0
- package/dist/identity-tree.js +69 -0
- package/dist/identity-tree.js.map +1 -0
- package/dist/index.d.ts +55 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +86 -0
- package/dist/index.js.map +1 -0
- package/dist/key-derivation.d.ts +43 -0
- package/dist/key-derivation.d.ts.map +1 -0
- package/dist/key-derivation.js +212 -0
- package/dist/key-derivation.js.map +1 -0
- package/dist/lsag.d.ts +23 -0
- package/dist/lsag.d.ts.map +1 -0
- package/dist/lsag.js +35 -0
- package/dist/lsag.js.map +1 -0
- package/dist/merkle.d.ts +19 -0
- package/dist/merkle.d.ts.map +1 -0
- package/dist/merkle.js +155 -0
- package/dist/merkle.js.map +1 -0
- package/dist/policies.d.ts +22 -0
- package/dist/policies.d.ts.map +1 -0
- package/dist/policies.js +123 -0
- package/dist/policies.js.map +1 -0
- package/dist/range-proof.d.ts +6 -0
- package/dist/range-proof.d.ts.map +1 -0
- package/dist/range-proof.js +45 -0
- package/dist/range-proof.js.map +1 -0
- package/dist/relay.d.ts +106 -0
- package/dist/relay.d.ts.map +1 -0
- package/dist/relay.js +336 -0
- package/dist/relay.js.map +1 -0
- package/dist/ring-signature.d.ts +35 -0
- package/dist/ring-signature.d.ts.map +1 -0
- package/dist/ring-signature.js +56 -0
- package/dist/ring-signature.js.map +1 -0
- package/dist/shamir.d.ts +55 -0
- package/dist/shamir.d.ts.map +1 -0
- package/dist/shamir.js +253 -0
- package/dist/shamir.js.map +1 -0
- package/dist/signet-words.d.ts +42 -0
- package/dist/signet-words.d.ts.map +1 -0
- package/dist/signet-words.js +82 -0
- package/dist/signet-words.js.map +1 -0
- package/dist/store.d.ts +65 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +290 -0
- package/dist/store.js.map +1 -0
- package/dist/trust-score.d.ts +9 -0
- package/dist/trust-score.d.ts.map +1 -0
- package/dist/trust-score.js +186 -0
- package/dist/trust-score.js.map +1 -0
- package/dist/types.d.ts +358 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +15 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +11 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +21 -0
- package/dist/utils.js.map +1 -0
- package/dist/validation.d.ts +33 -0
- package/dist/validation.d.ts.map +1 -0
- package/dist/validation.js +312 -0
- package/dist/validation.js.map +1 -0
- package/dist/verifiers.d.ts +18 -0
- package/dist/verifiers.d.ts.map +1 -0
- package/dist/verifiers.js +118 -0
- package/dist/verifiers.js.map +1 -0
- package/dist/vouches.d.ts +14 -0
- package/dist/vouches.d.ts.map +1 -0
- package/dist/vouches.js +103 -0
- package/dist/vouches.js.map +1 -0
- package/package.json +76 -0
- package/src/anomaly.ts +307 -0
- package/src/badge.ts +208 -0
- package/src/bonds.ts +203 -0
- package/src/challenges.ts +187 -0
- package/src/cold-call.ts +238 -0
- package/src/compliance.ts +612 -0
- package/src/connections.ts +216 -0
- package/src/constants.ts +146 -0
- package/src/credentials.ts +908 -0
- package/src/crypto.ts +85 -0
- package/src/errors.ts +31 -0
- package/src/i18n.ts +1347 -0
- package/src/identity-bridge.ts +262 -0
- package/src/identity-tree.ts +90 -0
- package/src/index.ts +452 -0
- package/src/lsag.ts +53 -0
- package/src/merkle.ts +176 -0
- package/src/policies.ts +154 -0
- package/src/range-proof.ts +66 -0
- package/src/relay.ts +433 -0
- package/src/ring-signature.ts +76 -0
- package/src/signet-words.ts +122 -0
- package/src/store.ts +336 -0
- package/src/trust-score.ts +208 -0
- package/src/types.ts +482 -0
- package/src/utils.ts +20 -0
- package/src/validation.ts +391 -0
- package/src/verifiers.ts +156 -0
- 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"}
|
package/dist/anomaly.js
ADDED
|
@@ -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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"}
|
package/dist/badge.d.ts
ADDED
|
@@ -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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"}
|