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 +2 -2
- package/eslint.config.js +31 -0
- package/index.d.ts +4 -2
- package/package.json +5 -3
- package/src/external-endpoints.js +16 -9
- package/src/keys.js +3 -1
- package/src/ncc02.js +28 -30
- package/src/ncc05.js +1 -1
- package/src/protocol.js +1 -1
- package/src/resolver.js +0 -2
- package/src/sidecar-config.js +1 -14
- package/src/tls.js +10 -3
- package/test/ncc02.test.js +9 -3
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
|
|
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',
|
package/eslint.config.js
ADDED
|
@@ -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.
|
|
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.
|
|
14
|
-
"ncc-05-js": "^1.
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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 {
|
|
1
|
+
import { NCC02Builder, verifyNCC02Event } from 'ncc-02-js';
|
|
2
2
|
|
|
3
|
-
const
|
|
3
|
+
const DEFAULT_EXPIRY_SECONDS = 14 * 24 * 60 * 60;
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
15
|
-
|
|
17
|
+
expirySeconds = DEFAULT_EXPIRY_SECONDS,
|
|
18
|
+
isPrivate = false,
|
|
19
|
+
privateRecipients
|
|
16
20
|
}) {
|
|
17
|
-
|
|
18
|
-
|
|
21
|
+
ensureSecretKey(secretKey);
|
|
22
|
+
if (!serviceId) {
|
|
23
|
+
throw new Error('serviceId is required');
|
|
19
24
|
}
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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 !==
|
|
45
|
+
if (!event || event.kind !== 30059) {
|
|
48
46
|
return false;
|
|
49
47
|
}
|
|
50
|
-
if (!
|
|
48
|
+
if (!verifyNCC02Event(event)) {
|
|
51
49
|
return false;
|
|
52
50
|
}
|
|
53
51
|
const tags = parseNcc02Tags(event);
|
package/src/ncc05.js
CHANGED
package/src/protocol.js
CHANGED
package/src/resolver.js
CHANGED
package/src/sidecar-config.js
CHANGED
|
@@ -1,17 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
43
|
-
|
|
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
|
}
|
package/test/ncc02.test.js
CHANGED
|
@@ -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
|
|
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
|
});
|