ncc-02-js 0.2.4 → 0.3.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/README.md CHANGED
@@ -6,7 +6,7 @@ This library provides tools for service owners to publish records and for client
6
6
 
7
7
  ## Features
8
8
 
9
- - **Service Discovery**: Resolve Kind 30059 service records.
9
+ - **Service Discovery**: Resolve Kind 30059 service records for both public and private services.
10
10
  - **Verification**: Built-in signature and expiry validation.
11
11
  - **Trust Policy**: Support for third-party attestations (Kind 30060) and revocations (Kind 30061).
12
12
  - **Security**: Cross-validation of subject and service identifiers to prevent impersonation.
@@ -26,7 +26,7 @@ npm install ncc-02-js
26
26
  import { NCC02Resolver } from 'ncc-02-js';
27
27
 
28
28
  // Initialize with relay URLs and optional trusted CA pubkeys
29
- const resolver = new NCC02Resolver(['wss://relay.damus.io'], {
29
+ const resolver = new NCC02Resolver(['wss://192.0.2.1:443'], {
30
30
  trustedCAPubkeys: ['npub1...'] // Trusted third-party certifiers
31
31
  });
32
32
 
@@ -36,7 +36,11 @@ try {
36
36
  requireAttestation: true,
37
37
  minLevel: 'verified' // 'self', 'verified', 'hardened'
38
38
  });
39
- console.log('Resolved endpoint:', service.endpoint);
39
+ if(service.endpoint) {
40
+ console.log('Resolved endpoint:', service.endpoint);
41
+ } else {
42
+ console.log('Resolved private service, use NCC-05 for endpoint discovery.');
43
+ }
40
44
  } catch (err) {
41
45
  console.error('Resolution failed:', err.code, err.message);
42
46
  }
@@ -50,7 +54,7 @@ import { NCC02Builder } from 'ncc-02-js';
50
54
  // Initialize with private key (hex)
51
55
  const builder = new NCC02Builder(privateKey);
52
56
 
53
- // Example 1: IP-based Service
57
+ // Example 1: Public IP-based Service
54
58
  const event = builder.createServiceRecord({
55
59
  serviceId: 'media',
56
60
  endpoint: 'https://203.0.113.45:8443',
@@ -58,10 +62,9 @@ const event = builder.createServiceRecord({
58
62
  expiryDays: 14
59
63
  });
60
64
 
61
- // Example 2: Tor Onion Service
62
- const onionEvent = builder.createServiceRecord({
65
+ // Example 2: Private / Invite-Only Service
66
+ const privateEvent = builder.createServiceRecord({
63
67
  serviceId: 'wallet',
64
- endpoint: 'tcp://vww6ybal4bd7szmgncyruucpgfkqahzddi37ktceo3ah7ngmcopnpyyd.onion:80',
65
68
  fingerprint: 'sha256:fingerprint',
66
69
  expiryDays: 7
67
70
  });
@@ -109,7 +112,7 @@ The library follows a fail-closed principle. If a policy requirement is not met
109
112
  - `options.minLevel`: Minimum trust level required.
110
113
 
111
114
  ### `NCC02Builder(privateKey)`
112
- - `createServiceRecord({ serviceId, endpoint, fingerprint, expiryDays })`
115
+ - `createServiceRecord({ serviceId, endpoint?, fingerprint?, expiryDays? })`
113
116
  - `createAttestation({ subjectPubkey, serviceId, serviceEventId, level, validDays })`
114
117
  - `createRevocation({ attestationId, reason })`
115
118
 
package/dist/index.cjs CHANGED
@@ -2509,25 +2509,24 @@ var NCC02Builder = class {
2509
2509
  * Creates a signed Service Record (Kind 30059).
2510
2510
  * @param {Object} options
2511
2511
  * @param {string} options.serviceId - The 'd' tag identifier.
2512
- * @param {string} options.endpoint - The 'u' tag URI.
2513
- * @param {string} options.fingerprint - The 'k' tag fingerprint.
2512
+ * @param {string} [options.endpoint] - The 'u' tag URI.
2513
+ * @param {string} [options.fingerprint] - The 'k' tag fingerprint.
2514
2514
  * @param {number} [options.expiryDays=14] - Expiry in days.
2515
2515
  */
2516
2516
  createServiceRecord(options) {
2517
2517
  const { serviceId, endpoint, fingerprint, expiryDays = 14 } = options;
2518
2518
  if (!serviceId) throw new Error("serviceId (d tag) is required");
2519
- if (!endpoint) throw new Error("endpoint (u tag) is required");
2520
- if (!fingerprint) throw new Error("fingerprint (k tag) is required");
2521
2519
  const expiry = Math.floor(Date.now() / 1e3) + expiryDays * 24 * 60 * 60;
2520
+ const tags = [
2521
+ ["d", serviceId],
2522
+ ["exp", expiry.toString()]
2523
+ ];
2524
+ if (endpoint) tags.push(["u", endpoint]);
2525
+ if (fingerprint) tags.push(["k", fingerprint]);
2522
2526
  const event = {
2523
2527
  kind: KINDS.SERVICE_RECORD,
2524
2528
  created_at: Math.floor(Date.now() / 1e3),
2525
- tags: [
2526
- ["d", serviceId],
2527
- ["u", endpoint],
2528
- ["k", fingerprint],
2529
- ["exp", expiry.toString()]
2530
- ],
2529
+ tags,
2531
2530
  content: `NCC-02 Service Record for ${serviceId}`,
2532
2531
  pubkey: this.pk
2533
2532
  };
@@ -7679,7 +7678,10 @@ var NCC02Resolver = class {
7679
7678
  * @param {string[]} relays - List of relay URLs.
7680
7679
  * @param {Object} [options={}]
7681
7680
  * @param {SimplePool} [options.pool] - Shared SimplePool instance.
7682
- * @param {string[]} [options.trustedCAPubkeys=[]] - List of trusted CA pubkeys.
7681
+ * @param {string[]} [options.trustedCAPubkeys=[]] - List of trusted CA pubkeys (hex or npub).
7682
+ * These are the ONLY pubkeys whose attestation signatures (Kind 30060) will be accepted
7683
+ * by the resolver. This allows you to define your own web of trust or rely on specific
7684
+ * community auditors. If empty, all attestations are ignored (effectively disabling attestation checks).
7683
7685
  */
7684
7686
  constructor(relays, options = {}) {
7685
7687
  if (!Array.isArray(relays)) {
@@ -7749,8 +7751,11 @@ var NCC02Resolver = class {
7749
7751
  }
7750
7752
  const serviceTags = Object.fromEntries(serviceEvent.tags);
7751
7753
  const now2 = Math.floor(Date.now() / 1e3);
7752
- if (!serviceTags.u || !serviceTags.k || !serviceTags.exp) {
7753
- throw new NCC02Error("MALFORMED_RECORD", "Service record is missing required tags (u, k, or exp)");
7754
+ if (!serviceTags.exp) {
7755
+ throw new NCC02Error("MALFORMED_RECORD", "Service record is missing required tag (exp)");
7756
+ }
7757
+ if (serviceTags.u && (serviceTags.u.startsWith("wss://") || serviceTags.u.startsWith("https://")) && !serviceTags.k) {
7758
+ throw new NCC02Error("MALFORMED_RECORD", "Service record with 'https' or 'wss' endpoint must have a 'k' tag");
7754
7759
  }
7755
7760
  const exp = parseInt(serviceTags.exp);
7756
7761
  if (isNaN(exp)) {
package/dist/index.mjs CHANGED
@@ -2475,25 +2475,24 @@ var NCC02Builder = class {
2475
2475
  * Creates a signed Service Record (Kind 30059).
2476
2476
  * @param {Object} options
2477
2477
  * @param {string} options.serviceId - The 'd' tag identifier.
2478
- * @param {string} options.endpoint - The 'u' tag URI.
2479
- * @param {string} options.fingerprint - The 'k' tag fingerprint.
2478
+ * @param {string} [options.endpoint] - The 'u' tag URI.
2479
+ * @param {string} [options.fingerprint] - The 'k' tag fingerprint.
2480
2480
  * @param {number} [options.expiryDays=14] - Expiry in days.
2481
2481
  */
2482
2482
  createServiceRecord(options) {
2483
2483
  const { serviceId, endpoint, fingerprint, expiryDays = 14 } = options;
2484
2484
  if (!serviceId) throw new Error("serviceId (d tag) is required");
2485
- if (!endpoint) throw new Error("endpoint (u tag) is required");
2486
- if (!fingerprint) throw new Error("fingerprint (k tag) is required");
2487
2485
  const expiry = Math.floor(Date.now() / 1e3) + expiryDays * 24 * 60 * 60;
2486
+ const tags = [
2487
+ ["d", serviceId],
2488
+ ["exp", expiry.toString()]
2489
+ ];
2490
+ if (endpoint) tags.push(["u", endpoint]);
2491
+ if (fingerprint) tags.push(["k", fingerprint]);
2488
2492
  const event = {
2489
2493
  kind: KINDS.SERVICE_RECORD,
2490
2494
  created_at: Math.floor(Date.now() / 1e3),
2491
- tags: [
2492
- ["d", serviceId],
2493
- ["u", endpoint],
2494
- ["k", fingerprint],
2495
- ["exp", expiry.toString()]
2496
- ],
2495
+ tags,
2497
2496
  content: `NCC-02 Service Record for ${serviceId}`,
2498
2497
  pubkey: this.pk
2499
2498
  };
@@ -7645,7 +7644,10 @@ var NCC02Resolver = class {
7645
7644
  * @param {string[]} relays - List of relay URLs.
7646
7645
  * @param {Object} [options={}]
7647
7646
  * @param {SimplePool} [options.pool] - Shared SimplePool instance.
7648
- * @param {string[]} [options.trustedCAPubkeys=[]] - List of trusted CA pubkeys.
7647
+ * @param {string[]} [options.trustedCAPubkeys=[]] - List of trusted CA pubkeys (hex or npub).
7648
+ * These are the ONLY pubkeys whose attestation signatures (Kind 30060) will be accepted
7649
+ * by the resolver. This allows you to define your own web of trust or rely on specific
7650
+ * community auditors. If empty, all attestations are ignored (effectively disabling attestation checks).
7649
7651
  */
7650
7652
  constructor(relays, options = {}) {
7651
7653
  if (!Array.isArray(relays)) {
@@ -7715,8 +7717,11 @@ var NCC02Resolver = class {
7715
7717
  }
7716
7718
  const serviceTags = Object.fromEntries(serviceEvent.tags);
7717
7719
  const now2 = Math.floor(Date.now() / 1e3);
7718
- if (!serviceTags.u || !serviceTags.k || !serviceTags.exp) {
7719
- throw new NCC02Error("MALFORMED_RECORD", "Service record is missing required tags (u, k, or exp)");
7720
+ if (!serviceTags.exp) {
7721
+ throw new NCC02Error("MALFORMED_RECORD", "Service record is missing required tag (exp)");
7722
+ }
7723
+ if (serviceTags.u && (serviceTags.u.startsWith("wss://") || serviceTags.u.startsWith("https://")) && !serviceTags.k) {
7724
+ throw new NCC02Error("MALFORMED_RECORD", "Service record with 'https' or 'wss' endpoint must have a 'k' tag");
7720
7725
  }
7721
7726
  const exp = parseInt(serviceTags.exp);
7722
7727
  if (isNaN(exp)) {
package/dist/models.d.ts CHANGED
@@ -22,14 +22,14 @@ export class NCC02Builder {
22
22
  * Creates a signed Service Record (Kind 30059).
23
23
  * @param {Object} options
24
24
  * @param {string} options.serviceId - The 'd' tag identifier.
25
- * @param {string} options.endpoint - The 'u' tag URI.
26
- * @param {string} options.fingerprint - The 'k' tag fingerprint.
25
+ * @param {string} [options.endpoint] - The 'u' tag URI.
26
+ * @param {string} [options.fingerprint] - The 'k' tag fingerprint.
27
27
  * @param {number} [options.expiryDays=14] - Expiry in days.
28
28
  */
29
29
  createServiceRecord(options: {
30
30
  serviceId: string;
31
- endpoint: string;
32
- fingerprint: string;
31
+ endpoint?: string;
32
+ fingerprint?: string;
33
33
  expiryDays?: number;
34
34
  }): import("nostr-tools/core").VerifiedEvent;
35
35
  /**
@@ -13,8 +13,8 @@ export class NCC02Error extends Error {
13
13
  }
14
14
  /**
15
15
  * @typedef {Object} ResolvedService
16
- * @property {string} endpoint
17
- * @property {string} fingerprint
16
+ * @property {string|undefined} endpoint
17
+ * @property {string|undefined} fingerprint
18
18
  * @property {number} expiry
19
19
  * @property {any[]} attestations
20
20
  * @property {string} eventId
@@ -29,7 +29,10 @@ export class NCC02Resolver {
29
29
  * @param {string[]} relays - List of relay URLs.
30
30
  * @param {Object} [options={}]
31
31
  * @param {SimplePool} [options.pool] - Shared SimplePool instance.
32
- * @param {string[]} [options.trustedCAPubkeys=[]] - List of trusted CA pubkeys.
32
+ * @param {string[]} [options.trustedCAPubkeys=[]] - List of trusted CA pubkeys (hex or npub).
33
+ * These are the ONLY pubkeys whose attestation signatures (Kind 30060) will be accepted
34
+ * by the resolver. This allows you to define your own web of trust or rely on specific
35
+ * community auditors. If empty, all attestations are ignored (effectively disabling attestation checks).
33
36
  */
34
37
  constructor(relays: string[], options?: {
35
38
  pool?: SimplePool;
@@ -84,8 +87,8 @@ export class NCC02Resolver {
84
87
  verifyEndpoint(resolved: ResolvedService, actualFingerprint: string): boolean;
85
88
  }
86
89
  export type ResolvedService = {
87
- endpoint: string;
88
- fingerprint: string;
90
+ endpoint: string | undefined;
91
+ fingerprint: string | undefined;
89
92
  expiry: number;
90
93
  attestations: any[];
91
94
  eventId: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ncc-02-js",
3
- "version": "0.2.4",
3
+ "version": "0.3.0",
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/models.js CHANGED
@@ -27,26 +27,26 @@ export class NCC02Builder {
27
27
  * Creates a signed Service Record (Kind 30059).
28
28
  * @param {Object} options
29
29
  * @param {string} options.serviceId - The 'd' tag identifier.
30
- * @param {string} options.endpoint - The 'u' tag URI.
31
- * @param {string} options.fingerprint - The 'k' tag fingerprint.
30
+ * @param {string} [options.endpoint] - The 'u' tag URI.
31
+ * @param {string} [options.fingerprint] - The 'k' tag fingerprint.
32
32
  * @param {number} [options.expiryDays=14] - Expiry in days.
33
33
  */
34
34
  createServiceRecord(options) {
35
35
  const { serviceId, endpoint, fingerprint, expiryDays = 14 } = options;
36
36
  if (!serviceId) throw new Error('serviceId (d tag) is required');
37
- if (!endpoint) throw new Error('endpoint (u tag) is required');
38
- if (!fingerprint) throw new Error('fingerprint (k tag) is required');
39
37
 
40
38
  const expiry = Math.floor(Date.now() / 1000) + (expiryDays * 24 * 60 * 60);
39
+ const tags = [
40
+ ['d', serviceId],
41
+ ['exp', expiry.toString()]
42
+ ];
43
+ if(endpoint) tags.push(['u', endpoint]);
44
+ if(fingerprint) tags.push(['k', fingerprint]);
45
+
41
46
  const event = {
42
47
  kind: KINDS.SERVICE_RECORD,
43
48
  created_at: Math.floor(Date.now() / 1000),
44
- tags: [
45
- ['d', serviceId],
46
- ['u', endpoint],
47
- ['k', fingerprint],
48
- ['exp', expiry.toString()]
49
- ],
49
+ tags: tags,
50
50
  content: `NCC-02 Service Record for ${serviceId}`,
51
51
  pubkey: this.pk
52
52
  };
package/src/resolver.js CHANGED
@@ -19,8 +19,8 @@ export class NCC02Error extends Error {
19
19
 
20
20
  /**
21
21
  * @typedef {Object} ResolvedService
22
- * @property {string} endpoint
23
- * @property {string} fingerprint
22
+ * @property {string|undefined} endpoint
23
+ * @property {string|undefined} fingerprint
24
24
  * @property {number} expiry
25
25
  * @property {any[]} attestations
26
26
  * @property {string} eventId
@@ -36,11 +36,14 @@ export class NCC02Resolver {
36
36
  * @param {string[]} relays - List of relay URLs.
37
37
  * @param {Object} [options={}]
38
38
  * @param {SimplePool} [options.pool] - Shared SimplePool instance.
39
- * @param {string[]} [options.trustedCAPubkeys=[]] - List of trusted CA pubkeys.
39
+ * @param {string[]} [options.trustedCAPubkeys=[]] - List of trusted CA pubkeys (hex or npub).
40
+ * These are the ONLY pubkeys whose attestation signatures (Kind 30060) will be accepted
41
+ * by the resolver. This allows you to define your own web of trust or rely on specific
42
+ * community auditors. If empty, all attestations are ignored (effectively disabling attestation checks).
40
43
  */
41
44
  constructor(relays, options = {}) {
42
45
  if (!Array.isArray(relays)) {
43
- throw new Error("NCC02Resolver expects an array of relay URLs.");
46
+ throw new Error('NCC02Resolver expects an array of relay URLs.');
44
47
  }
45
48
  this.relays = relays;
46
49
  this.pool = options.pool || new SimplePool();
@@ -54,16 +57,16 @@ export class NCC02Resolver {
54
57
  * @returns {Promise<import('nostr-tools').Event[]>}
55
58
  */
56
59
  async _query(filter) {
57
- return new Promise((resolve) => {
58
- /** @type {import('nostr-tools').Event[]} */
59
- const events = [];
60
- // subscribeMany(relays, filters, callbacks)
61
- // @ts-ignore - subscribeMany filters parameter type mismatch with simple Object
62
- const sub = this.pool.subscribeMany(this.relays, [filter], {
63
- onevent(e) { events.push(e); },
64
- oneose() { sub.close(); resolve(events); }
65
- });
60
+ return new Promise((resolve) => {
61
+ /** @type {import('nostr-tools').Event[]} */
62
+ const events = [];
63
+ // subscribeMany(relays, filters, callbacks)
64
+ // @ts-ignore - subscribeMany filters parameter type mismatch with simple Object
65
+ const sub = this.pool.subscribeMany(this.relays, [filter], {
66
+ onevent(e) { events.push(e); },
67
+ oneose() { sub.close(); resolve(events); }
66
68
  });
69
+ });
67
70
  }
68
71
 
69
72
  /**
@@ -114,8 +117,13 @@ export class NCC02Resolver {
114
117
  const now = Math.floor(Date.now() / 1000);
115
118
 
116
119
  // Security Fix: exp is REQUIRED by NCC-02 spec
117
- if (!serviceTags.u || !serviceTags.k || !serviceTags.exp) {
118
- throw new NCC02Error('MALFORMED_RECORD', 'Service record is missing required tags (u, k, or exp)');
120
+ if (!serviceTags.exp) {
121
+ throw new NCC02Error('MALFORMED_RECORD', 'Service record is missing required tag (exp)');
122
+ }
123
+
124
+ // 'k' is required for TLS-based endpoints
125
+ if (serviceTags.u && (serviceTags.u.startsWith('wss://') || serviceTags.u.startsWith('https://')) && !serviceTags.k) {
126
+ throw new NCC02Error('MALFORMED_RECORD', 'Service record with \'https\' or \'wss\' endpoint must have a \'k\' tag');
119
127
  }
120
128
 
121
129
  const exp = parseInt(serviceTags.exp);