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 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
- ```javascript
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
- ### Threat Model
95
- - **Endpoint Impersonation**: Prevented by binding the endpoint URI to a public key fingerprint (`k` tag).
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
- ### Fail-Closed Design
101
- The library follows a fail-closed principle. If a policy requirement is not met (e.g., `requireAttestation: true` but no valid attestation is found), it throws an `NCC02Error` rather than returning a partially verified record.
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
- for (const att of attestations) {
7779
- if (this.trustedCAPubkeys.has(att.pubkey)) {
7780
- const attTags = Object.fromEntries(att.tags);
7781
- if (attTags.subj !== pubkey) continue;
7782
- if (attTags.srv !== serviceId) continue;
7783
- if (standard && attTags.std !== standard) continue;
7784
- if (minLevel && !this._isLevelSufficient(attTags.lvl, minLevel)) continue;
7785
- if (this._isAttestationValid(att, attTags, revocations)) {
7786
- validAttestations.push({
7787
- pubkey: att.pubkey,
7788
- level: attTags.lvl,
7789
- eventId: att.id
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
- if (requireAttestation && validAttestations.length === 0) {
7795
- throw new NCC02Error("POLICY_FAILURE", `No trusted attestations meet the required policy for ${serviceId}`);
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
- for (const att of attestations) {
7745
- if (this.trustedCAPubkeys.has(att.pubkey)) {
7746
- const attTags = Object.fromEntries(att.tags);
7747
- if (attTags.subj !== pubkey) continue;
7748
- if (attTags.srv !== serviceId) continue;
7749
- if (standard && attTags.std !== standard) continue;
7750
- if (minLevel && !this._isLevelSufficient(attTags.lvl, minLevel)) continue;
7751
- if (this._isAttestationValid(att, attTags, revocations)) {
7752
- validAttestations.push({
7753
- pubkey: att.pubkey,
7754
- level: attTags.lvl,
7755
- eventId: att.id
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
- if (requireAttestation && validAttestations.length === 0) {
7761
- throw new NCC02Error("POLICY_FAILURE", `No trusted attestations meet the required policy for ${serviceId}`);
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,
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ncc-02-js",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Nostr-native service discovery and trust implementation (NCC-02)",
5
5
  "main": "dist/index.cjs",
6
6
  "module": "dist/index.mjs",
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
- for (const att of attestations) {
150
- if (this.trustedCAPubkeys.has(att.pubkey)) {
151
- const attTags = Object.fromEntries(att.tags);
152
-
153
- // Cross-validate subject, service ID, and standard
154
- if (attTags.subj !== pubkey) continue;
155
- if (attTags.srv !== serviceId) continue;
156
- if (standard && attTags.std !== standard) continue;
157
-
158
- // Trust Level Filtering
159
- if (minLevel && !this._isLevelSufficient(attTags.lvl, minLevel)) continue;
160
-
161
- if (this._isAttestationValid(att, attTags, revocations)) {
162
- validAttestations.push({
163
- pubkey: att.pubkey,
164
- level: attTags.lvl,
165
- eventId: att.id
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
- if (requireAttestation && validAttestations.length === 0) {
172
- throw new NCC02Error('POLICY_FAILURE', `No trusted attestations meet the required policy for ${serviceId}`);
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 {