ncc-06-js 0.4.1 → 0.5.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/DOCS.md CHANGED
@@ -7,10 +7,10 @@
7
7
  These functions help build, parse, and verify NCC-02 service records (kind `30059`).
8
8
 
9
9
  ### `buildNcc02ServiceRecord(options)`
10
- - **Purpose**: create a signed service record containing `d`, `u`, `k`, and `exp`.
10
+ - **Purpose**: asynchronously create a signed service record containing `d`, `u`, `k`, `exp`, and optional privacy metadata.
11
11
  - **Example**:
12
12
  ```js
13
- const event = buildNcc02ServiceRecord({
13
+ const event = await buildNcc02ServiceRecord({
14
14
  secretKey,
15
15
  serviceId: 'relay',
16
16
  endpoint: 'wss://127.0.0.1:7447',
@@ -0,0 +1,31 @@
1
+ import { fileURLToPath, URL } from 'node:url';
2
+ import { FlatCompat } from '@eslint/eslintrc';
3
+ import globals from 'globals';
4
+ import pkg from '@eslint/js';
5
+
6
+ const { configs } = pkg;
7
+ const __dirname = fileURLToPath(new URL('.', import.meta.url));
8
+ const compat = new FlatCompat({ baseDirectory: __dirname, recommendedConfig: configs.recommended });
9
+ const envGlobals = {
10
+ ...globals.es2020,
11
+ ...globals.node
12
+ };
13
+
14
+ export default [
15
+ ...compat.extends('eslint:recommended'),
16
+ {
17
+ languageOptions: {
18
+ ecmaVersion: 2020,
19
+ sourceType: 'module',
20
+ globals: envGlobals
21
+ },
22
+ rules: {
23
+ 'no-unused-vars': [
24
+ 'warn',
25
+ {
26
+ argsIgnorePattern: '^_'
27
+ }
28
+ ]
29
+ }
30
+ }
31
+ ];
package/index.d.ts CHANGED
@@ -17,9 +17,11 @@ declare module 'ncc-06-js' {
17
17
  expirySeconds?: number;
18
18
  kind?: number;
19
19
  createdAt?: number;
20
+ isPrivate?: boolean;
21
+ privateRecipients?: string[];
20
22
  }
21
23
 
22
- export function buildNcc02ServiceRecord(options: BuildNcc02Options): NostrEvent;
24
+ export function buildNcc02ServiceRecord(options: BuildNcc02Options): Promise<NostrEvent>;
23
25
  export function parseNcc02Tags(event: NostrEvent): Record<string, string | undefined>;
24
26
  export interface ValidateNcc02Options {
25
27
  expectedAuthor?: string;
@@ -259,4 +261,4 @@ declare module 'ncc-06-js' {
259
261
  ncc02ExpectedKey?: string;
260
262
  }
261
263
  export function buildClientConfig(options: ClientConfigOptions): ClientConfig;
262
- }
264
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ncc-06-js",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "Reusable NCC-06 discovery helpers for multimodal service identities.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -10,13 +10,15 @@
10
10
  "lint": "eslint . --ext .js"
11
11
  },
12
12
  "dependencies": {
13
- "ncc-02-js": "^0.3.0",
14
- "ncc-05-js": "^1.1.14",
13
+ "ncc-02-js": "^0.5.0",
14
+ "ncc-05-js": "^1.2.0",
15
15
  "nostr-tools": "^2.19.4",
16
16
  "selfsigned": "^5.4.0",
17
17
  "ws": "^8.18.3"
18
18
  },
19
19
  "devDependencies": {
20
+ "@eslint/eslintrc": "^2.0.3",
21
+ "@eslint/js": "^9.39.2",
20
22
  "@types/node": "^25.0.3",
21
23
  "eslint": "^9.39.2",
22
24
  "typescript": "^5.9.3"
@@ -3,6 +3,7 @@ import os from 'os';
3
3
  const IPV4_PRIORITY = 10;
4
4
  const IPV6_PRIORITY = 20;
5
5
  const ONION_PRIORITY = 30;
6
+ const SECURE_PROTOCOLS = new Set(['wss', 'https', 'tls', 'tcps']);
6
7
 
7
8
  /**
8
9
  * Build a list of external endpoints that the operator wants to publish.
@@ -43,22 +44,24 @@ export async function buildExternalEndpoints({
43
44
  if (ipv6?.enabled) {
44
45
  const address = detectGlobalIPv6();
45
46
  if (address) {
46
- const protocol = ipv6.protocol || 'ws';
47
- const port = ipv6.port || (protocol === 'wss' ? wssPort : wsPort);
47
+ const protocol = (ipv6.protocol || 'ws').toLowerCase();
48
+ const secure = isSecureProtocol(protocol);
49
+ const port = ipv6.port || (secure ? wssPort : wsPort);
48
50
  const url = `${protocol}://[${address}]:${port}`;
49
51
  addEndpoint({
50
52
  url,
51
53
  priority: IPV6_PRIORITY,
52
54
  family: 'ipv6',
53
55
  protocol,
54
- k: protocol === 'wss' ? ncc02ExpectedKey : undefined
56
+ k: secure ? ncc02ExpectedKey : undefined
55
57
  });
56
58
  }
57
59
  }
58
60
 
59
61
  if (ipv4?.enabled) {
60
- const protocol = ipv4.protocol || 'wss';
61
- const port = ipv4.port || (protocol === 'wss' ? wssPort : wsPort);
62
+ const protocol = (ipv4.protocol || 'wss').toLowerCase();
63
+ const secure = isSecureProtocol(protocol);
64
+ const port = ipv4.port || (secure ? wssPort : wsPort);
62
65
  const address = ipv4.address || (await getPublicIPv4({ sources: ipv4.publicSources ?? publicIpv4Sources }));
63
66
  if (address) {
64
67
  addEndpoint({
@@ -66,7 +69,7 @@ export async function buildExternalEndpoints({
66
69
  priority: IPV4_PRIORITY,
67
70
  family: 'ipv4',
68
71
  protocol,
69
- k: protocol === 'wss' ? ncc02ExpectedKey : undefined
72
+ k: secure ? ncc02ExpectedKey : undefined
70
73
  });
71
74
  }
72
75
  }
@@ -126,13 +129,19 @@ export async function getPublicIPv4({ sources = ['https://api.ipify.org?format=j
126
129
  if (match) {
127
130
  return match[0];
128
131
  }
129
- } catch (err) {
132
+ } catch {
130
133
  continue;
131
134
  }
132
135
  }
133
136
  return null;
134
137
  }
135
138
 
139
+ function isSecureProtocol(protocol) {
140
+ if (!protocol) return false;
141
+ const normalized = protocol.toLowerCase();
142
+ return SECURE_PROTOCOLS.has(normalized) || (normalized.endsWith('s') && normalized !== 'ws');
143
+ }
144
+
136
145
  export function normalizeRelayUrl(url) {
137
146
  if (!url) return '';
138
147
  let normalized = url.trim();
@@ -152,5 +161,3 @@ export function normalizeRelays(relays) {
152
161
  .map(normalizeRelayUrl);
153
162
  return [...new Set(normalized)];
154
163
  }
155
-
156
-
package/src/keys.js CHANGED
@@ -1,6 +1,8 @@
1
- import { generateSecretKey, getPublicKey } from 'nostr-tools/pure';
1
+ import { generateSecretKey, getPublicKey as getPk } from 'nostr-tools/pure';
2
2
  import { nip19 } from 'nostr-tools';
3
3
 
4
+ export const getPublicKey = getPk;
5
+
4
6
  /**
5
7
  * Generate a deterministic keypair, returning all common formats.
6
8
  */
package/src/ncc02.js CHANGED
@@ -1,39 +1,37 @@
1
- import { finalizeEvent, getPublicKey, validateEvent, verifyEvent } from 'nostr-tools/pure';
1
+ import { NCC02Builder, verifyNCC02Event } from 'ncc-02-js';
2
2
 
3
- const DEFAULT_KIND = 30059;
3
+ const DEFAULT_EXPIRY_SECONDS = 14 * 24 * 60 * 60;
4
4
 
5
- /**
6
- * Build an NCC-02 service record event.
7
- */
8
- export function buildNcc02ServiceRecord({
5
+ function ensureSecretKey(key) {
6
+ if (!key) {
7
+ throw new Error('secretKey is required');
8
+ }
9
+ return key;
10
+ }
11
+
12
+ export async function buildNcc02ServiceRecord({
9
13
  secretKey,
10
14
  serviceId,
11
15
  endpoint,
12
16
  fingerprint,
13
- expirySeconds = 14 * 24 * 60 * 60,
14
- createdAt,
15
- kind = DEFAULT_KIND
17
+ expirySeconds = DEFAULT_EXPIRY_SECONDS,
18
+ isPrivate = false,
19
+ privateRecipients
16
20
  }) {
17
- if (!secretKey) {
18
- throw new Error('secretKey is required');
21
+ ensureSecretKey(secretKey);
22
+ if (!serviceId) {
23
+ throw new Error('serviceId is required');
19
24
  }
20
- const timestamp = createdAt ?? Math.floor(Date.now() / 1000);
21
- const expiresAt = timestamp + Number(expirySeconds);
22
- const tags = [
23
- ['d', serviceId],
24
- ['exp', expiresAt.toString()]
25
- ];
26
- if (endpoint) tags.push(['u', endpoint]);
27
- if (fingerprint) tags.push(['k', fingerprint]);
28
-
29
- const event = {
30
- kind,
31
- pubkey: getPublicKey(secretKey),
32
- created_at: timestamp,
33
- tags,
34
- content: ''
35
- };
36
- return finalizeEvent(event, secretKey);
25
+ const builder = new NCC02Builder(secretKey);
26
+ const expiryDays = Number(expirySeconds) / (24 * 60 * 60);
27
+ return builder.createServiceRecord({
28
+ serviceId,
29
+ endpoint,
30
+ fingerprint,
31
+ expiryDays,
32
+ isPrivate,
33
+ privateRecipients
34
+ });
37
35
  }
38
36
 
39
37
  export function parseNcc02Tags(event) {
@@ -44,10 +42,10 @@ export function parseNcc02Tags(event) {
44
42
  }
45
43
 
46
44
  export function validateNcc02(event, { expectedAuthor, expectedD, now, allowExpired = false } = {}) {
47
- if (!event || event.kind !== DEFAULT_KIND) {
45
+ if (!event || event.kind !== 30059) {
48
46
  return false;
49
47
  }
50
- if (!validateEvent(event) || !verifyEvent(event)) {
48
+ if (!verifyNCC02Event(event)) {
51
49
  return false;
52
50
  }
53
51
  const tags = parseNcc02Tags(event);
package/src/ncc05.js CHANGED
@@ -21,7 +21,7 @@ export function parseLocatorPayload(content) {
21
21
  }
22
22
  try {
23
23
  return JSON.parse(content);
24
- } catch (err) {
24
+ } catch {
25
25
  return null;
26
26
  }
27
27
  }
package/src/protocol.js CHANGED
@@ -12,7 +12,7 @@ export function parseNostrMessage(messageString) {
12
12
  return null;
13
13
  }
14
14
  return payload;
15
- } catch (error) {
15
+ } catch {
16
16
  return null;
17
17
  }
18
18
  }
package/src/resolver.js CHANGED
@@ -1,4 +1,3 @@
1
- import WebSocket from 'ws';
2
1
  import { SimplePool } from 'nostr-tools';
3
2
  import { NCC05Resolver } from 'ncc-05-js';
4
3
  import { validateNcc02, parseNcc02Tags } from './ncc02.js';
@@ -194,4 +193,3 @@ async function defaultResolveLocator({
194
193
  }
195
194
  }
196
195
  }
197
-
@@ -1,17 +1,4 @@
1
- import { DEFAULT_TTL_SECONDS } from './ncc05.js';
2
- import { getExpectedK } from './k.js';
3
-
4
- const DEFAULT_TOR_CONTROL = {
5
- enabled: false,
6
- host: '127.0.0.1',
7
- port: 9051,
8
- password: '',
9
- servicePort: 80,
10
- serviceFile: './onion-service.json',
11
- timeout: 5000
12
- };
13
-
14
- import { normalizeRelayUrl, normalizeRelays } from './external-endpoints.js';
1
+ import { normalizeRelayUrl } from './external-endpoints.js';
15
2
 
16
3
  const RELAY_MODE_PUBLIC = 'public';
17
4
  const RELAY_MODE_PRIVATE = 'private';
package/src/tls.js CHANGED
@@ -32,15 +32,22 @@ export async function ensureSelfSignedCert({
32
32
  return { type: 2, value: name };
33
33
  });
34
34
 
35
- const generated = selfsigned.generate(attrs, {
35
+ const generated = await selfsigned.generate(attrs, {
36
36
  algorithm: 'rsa',
37
37
  keySize: 2048,
38
38
  days: 365,
39
39
  extensions: [{ name: 'subjectAltName', altNames: altNameObjects }]
40
40
  });
41
41
 
42
- fs.writeFileSync(keyPath, generated.private, 'utf-8');
43
- fs.writeFileSync(certPath, generated.cert, 'utf-8');
42
+ const privateKey = generated.private || generated.privateKey;
43
+ const certificate = generated.cert || generated.certificate;
44
+
45
+ if (!privateKey || !certificate) {
46
+ throw new Error(`Self-signed cert generation failed. Keys returned: ${Object.keys(generated).join(', ')}`);
47
+ }
48
+
49
+ fs.writeFileSync(keyPath, privateKey, 'utf-8');
50
+ fs.writeFileSync(certPath, certificate, 'utf-8');
44
51
 
45
52
  return { keyPath, certPath };
46
53
  }
@@ -3,14 +3,17 @@ import { strict as assert } from 'assert';
3
3
  import { buildNcc02ServiceRecord, parseNcc02Tags, validateNcc02 } from '../src/ncc02.js';
4
4
  import { generateKeypair } from '../src/keys.js';
5
5
 
6
- test('builds and validates NCC-02 service record', () => {
6
+ test('builds and validates NCC-02 service record', async () => {
7
7
  const { secretKey, publicKey } = generateKeypair();
8
- const serviceEvent = buildNcc02ServiceRecord({
8
+ const recipients = ['cipher-text-1', 'cipher-text-2'];
9
+ const serviceEvent = await buildNcc02ServiceRecord({
9
10
  secretKey,
10
11
  serviceId: 'relay',
11
12
  endpoint: 'wss://127.0.0.1:7447',
12
13
  fingerprint: 'TESTKEY:resolver',
13
- expirySeconds: 60
14
+ expirySeconds: 60,
15
+ isPrivate: true,
16
+ privateRecipients: recipients
14
17
  });
15
18
 
16
19
  assert.equal(serviceEvent.pubkey, publicKey);
@@ -18,5 +21,8 @@ test('builds and validates NCC-02 service record', () => {
18
21
  assert.equal(tags.d, 'relay');
19
22
  assert.equal(tags.u, 'wss://127.0.0.1:7447');
20
23
  assert.equal(tags.k, 'TESTKEY:resolver');
24
+ assert.equal(tags.private, 'true');
25
+ const privateTagCount = serviceEvent.tags.filter(t => t[0] === 'privateRecipients').length;
26
+ assert.strictEqual(privateTagCount, recipients.length);
21
27
  assert.ok(validateNcc02(serviceEvent, { expectedAuthor: publicKey, expectedD: 'relay' }));
22
28
  });