ncc-02-js 0.2.5 → 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.
@@ -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
  };
@@ -7752,8 +7751,11 @@ var NCC02Resolver = class {
7752
7751
  }
7753
7752
  const serviceTags = Object.fromEntries(serviceEvent.tags);
7754
7753
  const now2 = Math.floor(Date.now() / 1e3);
7755
- if (!serviceTags.u || !serviceTags.k || !serviceTags.exp) {
7756
- 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");
7757
7759
  }
7758
7760
  const exp = parseInt(serviceTags.exp);
7759
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
  };
@@ -7718,8 +7717,11 @@ var NCC02Resolver = class {
7718
7717
  }
7719
7718
  const serviceTags = Object.fromEntries(serviceEvent.tags);
7720
7719
  const now2 = Math.floor(Date.now() / 1e3);
7721
- if (!serviceTags.u || !serviceTags.k || !serviceTags.exp) {
7722
- 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");
7723
7725
  }
7724
7726
  const exp = parseInt(serviceTags.exp);
7725
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
@@ -87,8 +87,8 @@ export class NCC02Resolver {
87
87
  verifyEndpoint(resolved: ResolvedService, actualFingerprint: string): boolean;
88
88
  }
89
89
  export type ResolvedService = {
90
- endpoint: string;
91
- fingerprint: string;
90
+ endpoint: string | undefined;
91
+ fingerprint: string | undefined;
92
92
  expiry: number;
93
93
  attestations: any[];
94
94
  eventId: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ncc-02-js",
3
- "version": "0.2.5",
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
@@ -43,7 +43,7 @@ export class NCC02Resolver {
43
43
  */
44
44
  constructor(relays, options = {}) {
45
45
  if (!Array.isArray(relays)) {
46
- throw new Error("NCC02Resolver expects an array of relay URLs.");
46
+ throw new Error('NCC02Resolver expects an array of relay URLs.');
47
47
  }
48
48
  this.relays = relays;
49
49
  this.pool = options.pool || new SimplePool();
@@ -57,16 +57,16 @@ export class NCC02Resolver {
57
57
  * @returns {Promise<import('nostr-tools').Event[]>}
58
58
  */
59
59
  async _query(filter) {
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); }
68
- });
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); }
69
68
  });
69
+ });
70
70
  }
71
71
 
72
72
  /**
@@ -117,8 +117,13 @@ export class NCC02Resolver {
117
117
  const now = Math.floor(Date.now() / 1000);
118
118
 
119
119
  // Security Fix: exp is REQUIRED by NCC-02 spec
120
- if (!serviceTags.u || !serviceTags.k || !serviceTags.exp) {
121
- 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');
122
127
  }
123
128
 
124
129
  const exp = parseInt(serviceTags.exp);