ncc-02-js 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -47
- package/dist/index.cjs +36 -26
- package/dist/index.mjs +36 -26
- package/dist/resolver.d.ts +4 -0
- package/package.json +1 -1
- package/src/resolver.js +45 -32
package/README.md
CHANGED
|
@@ -43,64 +43,26 @@ try {
|
|
|
43
43
|
}
|
|
44
44
|
} catch (err) {
|
|
45
45
|
console.error('Resolution failed:', err.code, err.message);
|
|
46
|
+
} finally {
|
|
47
|
+
resolver.close(); // Clean up WebSocket connections
|
|
46
48
|
}
|
|
47
49
|
```
|
|
48
50
|
|
|
49
51
|
### 2. Publish a Service Record
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
import { NCC02Builder } from 'ncc-02-js';
|
|
53
|
-
|
|
54
|
-
// Initialize with private key (hex)
|
|
55
|
-
const builder = new NCC02Builder(privateKey);
|
|
56
|
-
|
|
57
|
-
// Example 1: Public IP-based Service
|
|
58
|
-
const event = builder.createServiceRecord({
|
|
59
|
-
serviceId: 'media',
|
|
60
|
-
endpoint: 'https://203.0.113.45:8443',
|
|
61
|
-
fingerprint: 'sha256:fingerprint',
|
|
62
|
-
expiryDays: 14
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
// Example 2: Private / Invite-Only Service
|
|
66
|
-
const privateEvent = builder.createServiceRecord({
|
|
67
|
-
serviceId: 'wallet',
|
|
68
|
-
fingerprint: 'sha256:fingerprint',
|
|
69
|
-
expiryDays: 7
|
|
70
|
-
});
|
|
71
|
-
// publish events to relays...
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
### 3. Issue an Attestation (CA)
|
|
75
|
-
|
|
76
|
-
```javascript
|
|
77
|
-
const caBuilder = new NCC02Builder(caPrivateKey);
|
|
78
|
-
const attestation = caBuilder.createAttestation({
|
|
79
|
-
subjectPubkey: 'npub1...', // The service owner being certified
|
|
80
|
-
serviceId: 'media',
|
|
81
|
-
serviceEventId: serviceRecordEventId,
|
|
82
|
-
level: 'verified',
|
|
83
|
-
validDays: 30
|
|
84
|
-
});
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
## Trust Model & Security
|
|
52
|
+
...
|
|
53
|
+
### Trust Model & Security
|
|
88
54
|
|
|
89
55
|
### Trust Levels
|
|
90
56
|
- `self`: Asserted by the service owner (default if no attestation).
|
|
91
57
|
- `verified`: Attested by a trusted third party.
|
|
92
58
|
- `hardened`: Attested by a third party with stricter verification (e.g., physical proof or long-term history).
|
|
93
59
|
|
|
94
|
-
###
|
|
95
|
-
-
|
|
96
|
-
- **Man-in-the-Middle (MITM)**: Mitigated via cryptographic pinning of transport-level keys.
|
|
97
|
-
- **Stale Records**: Limited by required expiry (`exp`) and support for revocations.
|
|
98
|
-
- **Relay Censorship**: Mitigated by querying multiple relays (implemented via `SimplePool`).
|
|
60
|
+
### Resolution Optimization
|
|
61
|
+
The resolver is designed to be network-efficient. It will only query for attestations (Kind 30060) and revocations (Kind 30061) if the provided policy requires them (e.g., when `requireAttestation` is set to `true` or a `minLevel` higher than `self` is requested).
|
|
99
62
|
|
|
100
|
-
###
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
## API Reference
|
|
63
|
+
### Threat Model
|
|
64
|
+
...
|
|
65
|
+
### API Reference
|
|
104
66
|
|
|
105
67
|
### `NCC02Resolver(relays, options)`
|
|
106
68
|
- `relays`: Array of relay URLs.
|
|
@@ -111,6 +73,10 @@ The library follows a fail-closed principle. If a policy requirement is not met
|
|
|
111
73
|
- `options.requireAttestation`: Fails if no trusted attestation is found.
|
|
112
74
|
- `options.minLevel`: Minimum trust level required.
|
|
113
75
|
|
|
76
|
+
#### `close()`
|
|
77
|
+
Closes WebSocket connections to all relays. If the resolver was initialized with an external `pool`, this will *not* close the pool; it only untracks the relays for this instance. If the resolver created its own internal pool, it will close it entirely.
|
|
78
|
+
|
|
79
|
+
|
|
114
80
|
### `NCC02Builder(privateKey)`
|
|
115
81
|
- `createServiceRecord({ serviceId, endpoint?, fingerprint?, expiryDays? })`
|
|
116
82
|
- `createAttestation({ subjectPubkey, serviceId, serviceEventId, level, validDays })`
|
package/dist/index.cjs
CHANGED
|
@@ -7692,6 +7692,14 @@ var NCC02Resolver = class {
|
|
|
7692
7692
|
this.ownsPool = !options.pool;
|
|
7693
7693
|
this.trustedCAPubkeys = new Set(options.trustedCAPubkeys || []);
|
|
7694
7694
|
}
|
|
7695
|
+
/**
|
|
7696
|
+
* Closes the connection to the relays if the pool is owned by this resolver.
|
|
7697
|
+
*/
|
|
7698
|
+
close() {
|
|
7699
|
+
if (this.ownsPool && this.pool) {
|
|
7700
|
+
this.pool.close(this.relays);
|
|
7701
|
+
}
|
|
7702
|
+
}
|
|
7695
7703
|
/**
|
|
7696
7704
|
* Internal query helper using SimplePool.subscribeMany (since list() is deprecated).
|
|
7697
7705
|
* @param {import('nostr-tools').Filter} filter
|
|
@@ -7764,35 +7772,37 @@ var NCC02Resolver = class {
|
|
|
7764
7772
|
if (exp < now2) {
|
|
7765
7773
|
throw new NCC02Error("EXPIRED", "Service record has expired");
|
|
7766
7774
|
}
|
|
7767
|
-
let attestations;
|
|
7768
|
-
let revocations;
|
|
7769
|
-
try {
|
|
7770
|
-
[attestations, revocations] = await Promise.all([
|
|
7771
|
-
this._query({ kinds: [KINDS.ATTESTATION], "#e": [serviceEvent.id] }),
|
|
7772
|
-
this._query({ kinds: [KINDS.REVOCATION] })
|
|
7773
|
-
]);
|
|
7774
|
-
} catch (err) {
|
|
7775
|
-
throw new NCC02Error("RELAY_ERROR", "Failed to query relay for attestations/revocations", err);
|
|
7776
|
-
}
|
|
7777
7775
|
const validAttestations = [];
|
|
7778
|
-
|
|
7779
|
-
|
|
7780
|
-
|
|
7781
|
-
|
|
7782
|
-
|
|
7783
|
-
|
|
7784
|
-
|
|
7785
|
-
|
|
7786
|
-
|
|
7787
|
-
|
|
7788
|
-
|
|
7789
|
-
|
|
7790
|
-
|
|
7776
|
+
if (requireAttestation || minLevel === "verified" || minLevel === "hardened") {
|
|
7777
|
+
let attestations;
|
|
7778
|
+
let revocations;
|
|
7779
|
+
try {
|
|
7780
|
+
[attestations, revocations] = await Promise.all([
|
|
7781
|
+
this._query({ kinds: [KINDS.ATTESTATION], "#e": [serviceEvent.id] }),
|
|
7782
|
+
this._query({ kinds: [KINDS.REVOCATION] })
|
|
7783
|
+
]);
|
|
7784
|
+
} catch (err) {
|
|
7785
|
+
throw new NCC02Error("RELAY_ERROR", "Failed to query relay for attestations/revocations", err);
|
|
7786
|
+
}
|
|
7787
|
+
for (const att of attestations) {
|
|
7788
|
+
if (this.trustedCAPubkeys.has(att.pubkey)) {
|
|
7789
|
+
const attTags = Object.fromEntries(att.tags);
|
|
7790
|
+
if (attTags.subj !== pubkey) continue;
|
|
7791
|
+
if (attTags.srv !== serviceId) continue;
|
|
7792
|
+
if (standard && attTags.std !== standard) continue;
|
|
7793
|
+
if (minLevel && !this._isLevelSufficient(attTags.lvl, minLevel)) continue;
|
|
7794
|
+
if (this._isAttestationValid(att, attTags, revocations)) {
|
|
7795
|
+
validAttestations.push({
|
|
7796
|
+
pubkey: att.pubkey,
|
|
7797
|
+
level: attTags.lvl,
|
|
7798
|
+
eventId: att.id
|
|
7799
|
+
});
|
|
7800
|
+
}
|
|
7791
7801
|
}
|
|
7792
7802
|
}
|
|
7793
|
-
|
|
7794
|
-
|
|
7795
|
-
|
|
7803
|
+
if (requireAttestation && validAttestations.length === 0) {
|
|
7804
|
+
throw new NCC02Error("POLICY_FAILURE", `No trusted attestations meet the required policy for ${serviceId}`);
|
|
7805
|
+
}
|
|
7796
7806
|
}
|
|
7797
7807
|
return {
|
|
7798
7808
|
endpoint: serviceTags.u,
|
package/dist/index.mjs
CHANGED
|
@@ -7658,6 +7658,14 @@ var NCC02Resolver = class {
|
|
|
7658
7658
|
this.ownsPool = !options.pool;
|
|
7659
7659
|
this.trustedCAPubkeys = new Set(options.trustedCAPubkeys || []);
|
|
7660
7660
|
}
|
|
7661
|
+
/**
|
|
7662
|
+
* Closes the connection to the relays if the pool is owned by this resolver.
|
|
7663
|
+
*/
|
|
7664
|
+
close() {
|
|
7665
|
+
if (this.ownsPool && this.pool) {
|
|
7666
|
+
this.pool.close(this.relays);
|
|
7667
|
+
}
|
|
7668
|
+
}
|
|
7661
7669
|
/**
|
|
7662
7670
|
* Internal query helper using SimplePool.subscribeMany (since list() is deprecated).
|
|
7663
7671
|
* @param {import('nostr-tools').Filter} filter
|
|
@@ -7730,35 +7738,37 @@ var NCC02Resolver = class {
|
|
|
7730
7738
|
if (exp < now2) {
|
|
7731
7739
|
throw new NCC02Error("EXPIRED", "Service record has expired");
|
|
7732
7740
|
}
|
|
7733
|
-
let attestations;
|
|
7734
|
-
let revocations;
|
|
7735
|
-
try {
|
|
7736
|
-
[attestations, revocations] = await Promise.all([
|
|
7737
|
-
this._query({ kinds: [KINDS.ATTESTATION], "#e": [serviceEvent.id] }),
|
|
7738
|
-
this._query({ kinds: [KINDS.REVOCATION] })
|
|
7739
|
-
]);
|
|
7740
|
-
} catch (err) {
|
|
7741
|
-
throw new NCC02Error("RELAY_ERROR", "Failed to query relay for attestations/revocations", err);
|
|
7742
|
-
}
|
|
7743
7741
|
const validAttestations = [];
|
|
7744
|
-
|
|
7745
|
-
|
|
7746
|
-
|
|
7747
|
-
|
|
7748
|
-
|
|
7749
|
-
|
|
7750
|
-
|
|
7751
|
-
|
|
7752
|
-
|
|
7753
|
-
|
|
7754
|
-
|
|
7755
|
-
|
|
7756
|
-
|
|
7742
|
+
if (requireAttestation || minLevel === "verified" || minLevel === "hardened") {
|
|
7743
|
+
let attestations;
|
|
7744
|
+
let revocations;
|
|
7745
|
+
try {
|
|
7746
|
+
[attestations, revocations] = await Promise.all([
|
|
7747
|
+
this._query({ kinds: [KINDS.ATTESTATION], "#e": [serviceEvent.id] }),
|
|
7748
|
+
this._query({ kinds: [KINDS.REVOCATION] })
|
|
7749
|
+
]);
|
|
7750
|
+
} catch (err) {
|
|
7751
|
+
throw new NCC02Error("RELAY_ERROR", "Failed to query relay for attestations/revocations", err);
|
|
7752
|
+
}
|
|
7753
|
+
for (const att of attestations) {
|
|
7754
|
+
if (this.trustedCAPubkeys.has(att.pubkey)) {
|
|
7755
|
+
const attTags = Object.fromEntries(att.tags);
|
|
7756
|
+
if (attTags.subj !== pubkey) continue;
|
|
7757
|
+
if (attTags.srv !== serviceId) continue;
|
|
7758
|
+
if (standard && attTags.std !== standard) continue;
|
|
7759
|
+
if (minLevel && !this._isLevelSufficient(attTags.lvl, minLevel)) continue;
|
|
7760
|
+
if (this._isAttestationValid(att, attTags, revocations)) {
|
|
7761
|
+
validAttestations.push({
|
|
7762
|
+
pubkey: att.pubkey,
|
|
7763
|
+
level: attTags.lvl,
|
|
7764
|
+
eventId: att.id
|
|
7765
|
+
});
|
|
7766
|
+
}
|
|
7757
7767
|
}
|
|
7758
7768
|
}
|
|
7759
|
-
|
|
7760
|
-
|
|
7761
|
-
|
|
7769
|
+
if (requireAttestation && validAttestations.length === 0) {
|
|
7770
|
+
throw new NCC02Error("POLICY_FAILURE", `No trusted attestations meet the required policy for ${serviceId}`);
|
|
7771
|
+
}
|
|
7762
7772
|
}
|
|
7763
7773
|
return {
|
|
7764
7774
|
endpoint: serviceTags.u,
|
package/dist/resolver.d.ts
CHANGED
|
@@ -42,6 +42,10 @@ export class NCC02Resolver {
|
|
|
42
42
|
pool: SimplePool;
|
|
43
43
|
ownsPool: boolean;
|
|
44
44
|
trustedCAPubkeys: Set<string>;
|
|
45
|
+
/**
|
|
46
|
+
* Closes the connection to the relays if the pool is owned by this resolver.
|
|
47
|
+
*/
|
|
48
|
+
close(): void;
|
|
45
49
|
/**
|
|
46
50
|
* Internal query helper using SimplePool.subscribeMany (since list() is deprecated).
|
|
47
51
|
* @param {import('nostr-tools').Filter} filter
|
package/package.json
CHANGED
package/src/resolver.js
CHANGED
|
@@ -51,6 +51,15 @@ export class NCC02Resolver {
|
|
|
51
51
|
this.trustedCAPubkeys = new Set(options.trustedCAPubkeys || []);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Closes the connection to the relays if the pool is owned by this resolver.
|
|
56
|
+
*/
|
|
57
|
+
close() {
|
|
58
|
+
if (this.ownsPool && this.pool) {
|
|
59
|
+
this.pool.close(this.relays);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
54
63
|
/**
|
|
55
64
|
* Internal query helper using SimplePool.subscribeMany (since list() is deprecated).
|
|
56
65
|
* @param {import('nostr-tools').Filter} filter
|
|
@@ -134,42 +143,46 @@ export class NCC02Resolver {
|
|
|
134
143
|
throw new NCC02Error('EXPIRED', 'Service record has expired');
|
|
135
144
|
}
|
|
136
145
|
|
|
137
|
-
let attestations;
|
|
138
|
-
let revocations;
|
|
139
|
-
try {
|
|
140
|
-
[attestations, revocations] = await Promise.all([
|
|
141
|
-
this._query({ kinds: [KINDS.ATTESTATION], '#e': [serviceEvent.id] }),
|
|
142
|
-
this._query({ kinds: [KINDS.REVOCATION] })
|
|
143
|
-
]);
|
|
144
|
-
} catch (err) {
|
|
145
|
-
throw new NCC02Error('RELAY_ERROR', 'Failed to query relay for attestations/revocations', err);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
146
|
const validAttestations = [];
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
147
|
+
|
|
148
|
+
// Optimization: Only fetch attestations if policy requires it
|
|
149
|
+
if (requireAttestation || minLevel === 'verified' || minLevel === 'hardened') {
|
|
150
|
+
let attestations;
|
|
151
|
+
let revocations;
|
|
152
|
+
try {
|
|
153
|
+
[attestations, revocations] = await Promise.all([
|
|
154
|
+
this._query({ kinds: [KINDS.ATTESTATION], '#e': [serviceEvent.id] }),
|
|
155
|
+
this._query({ kinds: [KINDS.REVOCATION] })
|
|
156
|
+
]);
|
|
157
|
+
} catch (err) {
|
|
158
|
+
throw new NCC02Error('RELAY_ERROR', 'Failed to query relay for attestations/revocations', err);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
for (const att of attestations) {
|
|
162
|
+
if (this.trustedCAPubkeys.has(att.pubkey)) {
|
|
163
|
+
const attTags = Object.fromEntries(att.tags);
|
|
164
|
+
|
|
165
|
+
// Cross-validate subject, service ID, and standard
|
|
166
|
+
if (attTags.subj !== pubkey) continue;
|
|
167
|
+
if (attTags.srv !== serviceId) continue;
|
|
168
|
+
if (standard && attTags.std !== standard) continue;
|
|
169
|
+
|
|
170
|
+
// Trust Level Filtering
|
|
171
|
+
if (minLevel && !this._isLevelSufficient(attTags.lvl, minLevel)) continue;
|
|
172
|
+
|
|
173
|
+
if (this._isAttestationValid(att, attTags, revocations)) {
|
|
174
|
+
validAttestations.push({
|
|
175
|
+
pubkey: att.pubkey,
|
|
176
|
+
level: attTags.lvl,
|
|
177
|
+
eventId: att.id
|
|
178
|
+
});
|
|
179
|
+
}
|
|
167
180
|
}
|
|
168
181
|
}
|
|
169
|
-
}
|
|
170
182
|
|
|
171
|
-
|
|
172
|
-
|
|
183
|
+
if (requireAttestation && validAttestations.length === 0) {
|
|
184
|
+
throw new NCC02Error('POLICY_FAILURE', `No trusted attestations meet the required policy for ${serviceId}`);
|
|
185
|
+
}
|
|
173
186
|
}
|
|
174
187
|
|
|
175
188
|
return {
|