ncc-06-js 0.2.0 → 0.2.2

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/.eslintrc.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "env": {
3
+ "node": true,
4
+ "es2020": true
5
+ },
6
+ "extends": "eslint:recommended",
7
+ "parserOptions": {
8
+ "ecmaVersion": 2020,
9
+ "sourceType": "module"
10
+ },
11
+ "rules": {
12
+ "no-unused-vars": [
13
+ "warn",
14
+ {
15
+ "argsIgnorePattern": "^_"
16
+ }
17
+ ]
18
+ }
19
+ }
package/DOCS.md CHANGED
@@ -52,6 +52,14 @@ Utilities for locator payload construction and evaluation.
52
52
  - **Options** include `bootstrapRelays`, `servicePubkey`, `serviceId`, `locatorId`, `expectedK`, `locatorSecretKey`, `torPreferred`, timeouts, and override hooks.
53
53
  - **Returns** `{ endpoint, source, serviceEvent, locatorPayload, selection }`.
54
54
 
55
+ ## Sidecar config helpers
56
+
57
+ ### `buildSidecarConfig(options)`
58
+ - Mirrors the example sidecar setup and derives `ncc02ExpectedKey`, `publishRelays`, and `torControl` from operator intent so you do not need to duplicate those scripts.
59
+
60
+ ### `buildClientConfig(options)`
61
+ - Rehydrates the minimal client config that the example resolver expects; dedupes publication relays, enforces `serviceIdentityUri`, and carries the `expectedK` for NCC-02 pinning.
62
+
55
63
  ## Key and TLS helpers
56
64
 
57
65
  ### `generateExpectedK`, `validateExpectedKFormat`
@@ -108,3 +116,7 @@ const delay = scheduleWithJitter(60000); // use this for republish timers
108
116
  ```
109
117
 
110
118
  The helpers are intentionally small, focused on NCC-06 policy, and rely on the calling code for transport/threading so they can be reused inside Node scripts, CLI tools, or downstream SDKs.
119
+
120
+ ## TypeScript definitions
121
+
122
+ `ncc-06-js` ships an `index.d.ts` file that mirrors the helpers documented above so TypeScript consumers receive accurate typings.
package/README.md CHANGED
@@ -11,6 +11,8 @@ Reusable helpers extracted from the NCC-06 example relay, sidecar, and client im
11
11
  - **Scheduling helpers** (`scheduleWithJitter`) for applying bounded jitter to recurring NCC-02/NCC-05 timers without ever publishing outside the declared window.
12
12
  - **TLS/key utilities** (`ensureSelfSignedCert`, `generateKeypair`, `toNpub`, `fromNsec`, `generateExpectedK`, `validateExpectedKFormat`) that mirror the key and fingerprint management used by the example sidecar.
13
13
  - **Lightweight protocol helpers** (`parseNostrMessage`, `serializeNostrMessage`, `createReqMessage`) for downstream code that wants to reuse the same framing logic as the example client.
14
+ - **Sidecar config helpers** (`buildSidecarConfig`, `buildClientConfig`) so you can reuse the same config generation logic the example sidecar/client rely on without copying the scripts.
15
+ - **TypeScript definitions** (`index.d.ts`) that describe every exported helper, making it easier to import from TypeScript consumers.
14
16
 
15
17
  ## Usage
16
18
 
package/index.d.ts ADDED
@@ -0,0 +1,269 @@
1
+ declare module 'ncc-06-js' {
2
+ export interface NostrEvent {
3
+ id: string;
4
+ pubkey: string;
5
+ created_at: number;
6
+ kind: number;
7
+ tags: string[][];
8
+ content: string;
9
+ sig: string;
10
+ }
11
+
12
+ export interface BuildNcc02Options {
13
+ secretKey: string;
14
+ serviceId: string;
15
+ endpoint: string;
16
+ fingerprint?: string | null;
17
+ expirySeconds?: number;
18
+ kind?: number;
19
+ createdAt?: number;
20
+ }
21
+
22
+ export function buildNcc02ServiceRecord(options: BuildNcc02Options): NostrEvent;
23
+ export function parseNcc02Tags(event: NostrEvent): Record<string, string | undefined>;
24
+ export interface ValidateNcc02Options {
25
+ expectedAuthor?: string;
26
+ expectedD?: string;
27
+ now?: number;
28
+ allowExpired?: boolean;
29
+ }
30
+ export function validateNcc02(event: NostrEvent, options?: ValidateNcc02Options): boolean;
31
+
32
+ export interface LocatorEndpoint {
33
+ url: string;
34
+ type?: string;
35
+ uri?: string;
36
+ value?: string;
37
+ protocol?: 'ws' | 'wss';
38
+ family?: 'ipv4' | 'ipv6' | 'onion' | string;
39
+ priority?: number;
40
+ prio?: number;
41
+ k?: string;
42
+ fingerprint?: string;
43
+ [key: string]: unknown;
44
+ }
45
+
46
+ export interface LocatorPayload {
47
+ ttl: number;
48
+ updated_at: number;
49
+ endpoints: LocatorEndpoint[];
50
+ }
51
+
52
+ export const DEFAULT_TTL_SECONDS: number;
53
+ export function buildLocatorPayload(options?: {
54
+ endpoints?: LocatorEndpoint[];
55
+ ttl?: number;
56
+ updatedAt?: number;
57
+ }): LocatorPayload;
58
+ export function parseLocatorPayload(content: string | null | undefined): LocatorPayload | null;
59
+ export function validateLocatorFreshness(
60
+ payload: LocatorPayload | null | undefined,
61
+ options?: { now?: number; allowStale?: boolean }
62
+ ): boolean;
63
+ export function normalizeLocatorEndpoints(endpoints?: LocatorEndpoint[]): LocatorEndpoint[];
64
+
65
+ export interface SelectorOptions {
66
+ torPreferred?: boolean;
67
+ expectedK?: string;
68
+ }
69
+ export interface SelectorResult {
70
+ endpoint: LocatorEndpoint | null;
71
+ reason?: string;
72
+ expected?: string;
73
+ actual?: string;
74
+ }
75
+ export function choosePreferredEndpoint(
76
+ endpoints?: LocatorEndpoint[],
77
+ options?: SelectorOptions
78
+ ): SelectorResult;
79
+ export { normalizeLocatorEndpoints };
80
+
81
+ export interface ResolverOptions {
82
+ bootstrapRelays: string[];
83
+ servicePubkey: string;
84
+ serviceId: string;
85
+ locatorId: string;
86
+ expectedK?: string;
87
+ torPreferred?: boolean;
88
+ locatorSecretKey?: string;
89
+ ncc05TimeoutMs?: number;
90
+ publicationRelayTimeoutMs?: number;
91
+ queryRelayEvents?: (
92
+ relays: string[],
93
+ filter: Record<string, unknown>,
94
+ options?: { timeoutMs?: number }
95
+ ) => Promise<NostrEvent[]>;
96
+ resolveLocator?: (options: {
97
+ bootstrapRelays: string[];
98
+ servicePubkey: string;
99
+ locatorId: string;
100
+ locatorSecretKey?: string;
101
+ timeout?: number;
102
+ }) => Promise<LocatorPayload | null>;
103
+ throttle?: unknown;
104
+ now?: number;
105
+ }
106
+ export interface ResolverSelection {
107
+ endpoint: string | null;
108
+ source: 'locator' | 'ncc02' | null;
109
+ reason: string;
110
+ evidence?: Record<string, unknown>;
111
+ }
112
+ export interface ResolverResult {
113
+ endpoint: string | null;
114
+ source: 'locator' | 'ncc02' | null;
115
+ locatorPayload: LocatorPayload | null;
116
+ serviceEvent: NostrEvent;
117
+ selection: ResolverSelection;
118
+ }
119
+ export function resolveServiceEndpoint(options: ResolverOptions): Promise<ResolverResult>;
120
+
121
+ export interface Protocol {
122
+ parseNostrMessage(messageString: string): unknown[] | null;
123
+ serializeNostrMessage(messageArray: unknown[]): string;
124
+ createReqMessage(subId: string, ...filters: unknown[]): unknown[];
125
+ }
126
+
127
+ export interface Keypair {
128
+ secretKey: string;
129
+ publicKey: string;
130
+ npub: string;
131
+ nsec: string;
132
+ }
133
+ export function generateKeypair(): Keypair;
134
+ export function toNpub(pubkey: string): string;
135
+ export function fromNpub(npub: string): string;
136
+ export function toNsec(secretKey: string): string;
137
+ export function fromNsec(nsec: string): string;
138
+
139
+ export type KMode = 'tls_spki' | 'static' | 'generate';
140
+ export interface KConfig {
141
+ mode?: KMode;
142
+ value?: string;
143
+ certPath?: string;
144
+ persistPath?: string;
145
+ externalEndpoints?: Record<string, unknown>;
146
+ }
147
+ export function generateExpectedK(options?: {
148
+ prefix?: string;
149
+ label?: string;
150
+ suffix?: string;
151
+ }): string;
152
+ export function validateExpectedKFormat(k: string): boolean;
153
+ export function computeKFromCertPem(pem: string): string;
154
+ export function getExpectedK(cfg?: { k?: KConfig; externalEndpoints?: Record<string, unknown> }, options?: {
155
+ baseDir?: string;
156
+ }): string;
157
+
158
+ export interface JitterResult {
159
+ baseMs: number;
160
+ jitterRatio?: number;
161
+ }
162
+ export function scheduleWithJitter(baseMs: number, jitterRatio?: number): number;
163
+
164
+ export interface SelfSignedCertificate {
165
+ keyPath: string;
166
+ certPath: string;
167
+ }
168
+ export interface EnsureCertOptions {
169
+ targetDir?: string;
170
+ keyFileName?: string;
171
+ certFileName?: string;
172
+ altNames?: string[];
173
+ }
174
+ export function ensureSelfSignedCert(options?: EnsureCertOptions): Promise<SelfSignedCertificate>;
175
+
176
+ export interface ExternalEndpointOptions {
177
+ tor?: {
178
+ enabled?: boolean;
179
+ };
180
+ ipv4?: {
181
+ enabled?: boolean;
182
+ protocol?: 'ws' | 'wss';
183
+ port?: number;
184
+ address?: string;
185
+ publicSources?: string[];
186
+ };
187
+ ipv6?: {
188
+ enabled?: boolean;
189
+ protocol?: 'ws' | 'wss';
190
+ port?: number;
191
+ };
192
+ wsPort?: number;
193
+ wssPort?: number;
194
+ ncc02ExpectedKey?: string;
195
+ ensureOnionService?: () => Promise<{ address: string; servicePort: number } | null>;
196
+ publicIpv4Sources?: string[];
197
+ }
198
+ export function buildExternalEndpoints(options?: ExternalEndpointOptions): Promise<LocatorEndpoint[]>;
199
+ export function detectGlobalIPv6(): string | null;
200
+ export function getPublicIPv4(options?: { sources?: string[] }): Promise<string | null>;
201
+
202
+ export interface SidecarConfigOptions {
203
+ serviceSk: string;
204
+ servicePk: string;
205
+ serviceNpub: string;
206
+ relayUrl: string;
207
+ serviceId?: string;
208
+ locatorId?: string;
209
+ publicationRelays?: string[];
210
+ publishRelays?: string[];
211
+ ncc02ExpSeconds?: number;
212
+ ncc05TtlSeconds?: number;
213
+ torControl?: Record<string, unknown>;
214
+ externalEndpoints?: Record<string, unknown>;
215
+ k?: KConfig;
216
+ baseDir?: string;
217
+ ncc02ExpectedKeySource?: string;
218
+ }
219
+ export interface SidecarConfig {
220
+ serviceSk: string;
221
+ servicePk: string;
222
+ serviceNpub: string;
223
+ relayUrl: string;
224
+ serviceId: string;
225
+ locatorId: string;
226
+ publicationRelays: string[];
227
+ publishRelays: string[];
228
+ ncc02ExpSeconds: number;
229
+ ncc05TtlSeconds: number;
230
+ ncc02ExpectedKey: string;
231
+ ncc02ExpectedKeySource: string;
232
+ externalEndpoints: Record<string, unknown>;
233
+ torControl: Record<string, unknown>;
234
+ k: KConfig;
235
+ }
236
+ export function buildSidecarConfig(options: SidecarConfigOptions): SidecarConfig;
237
+
238
+ export interface ClientConfigOptions {
239
+ relayUrl: string;
240
+ servicePubkey?: string;
241
+ serviceNpub?: string;
242
+ serviceIdentityUri?: string;
243
+ locatorSecretKey?: string;
244
+ locatorFriendPubkey?: string;
245
+ publicationRelays?: string[];
246
+ staleFallbackSeconds?: number;
247
+ torPreferred?: boolean;
248
+ ncc05TimeoutMs?: number;
249
+ serviceId?: string;
250
+ locatorId?: string;
251
+ expectedK?: string;
252
+ }
253
+ export interface ClientConfig {
254
+ relayUrl: string;
255
+ serviceIdentityUri: string;
256
+ servicePubkey: string;
257
+ serviceNpub: string;
258
+ publicationRelays: string[];
259
+ staleFallbackSeconds: number;
260
+ torPreferred: boolean;
261
+ ncc05TimeoutMs: number;
262
+ locatorSecretKey?: string;
263
+ locatorFriendPubkey?: string;
264
+ serviceId?: string;
265
+ locatorId?: string;
266
+ ncc02ExpectedKey?: string;
267
+ }
268
+ export function buildClientConfig(options: ClientConfigOptions): ClientConfig;
269
+ }
package/package.json CHANGED
@@ -1,22 +1,24 @@
1
1
  {
2
2
  "name": "ncc-06-js",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Reusable NCC-06 discovery helpers extracted from the example relay, sidecar, and client.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
7
+ "types": "index.d.ts",
7
8
  "scripts": {
8
9
  "test": "node --test",
9
10
  "lint": "eslint . --ext .js"
10
11
  },
11
12
  "dependencies": {
12
- "ncc-02-js": "^0.2.2",
13
- "ncc-05": "^1.1.8",
13
+ "ncc-02-js": "^0.2.3",
14
+ "ncc-05-js": "^1.1.9",
14
15
  "nostr-tools": "^2.19.4",
15
16
  "selfsigned": "^5.4.0",
16
17
  "ws": "^8.18.3"
17
18
  },
18
19
  "devDependencies": {
19
20
  "@types/node": "^20.7.2",
20
- "eslint": "^8.54.0"
21
+ "eslint": "^8.54.0",
22
+ "typescript": "^5.5.3"
21
23
  }
22
24
  }
@@ -76,7 +76,7 @@ export async function buildExternalEndpoints({
76
76
  if (a.priority !== b.priority) return a.priority - b.priority;
77
77
  return a.index - b.index;
78
78
  })
79
- .map(({ index, createdAt, ...endpoint }) => endpoint);
79
+ .map(({ index: _index, createdAt: _createdAt, ...endpoint }) => endpoint);
80
80
  }
81
81
 
82
82
  /**
package/src/index.js CHANGED
@@ -8,3 +8,4 @@ export * from './tls.js';
8
8
  export * from './k.js';
9
9
  export * from './external-endpoints.js';
10
10
  export * from './schedule.js';
11
+ export * from './sidecar-config.js';
package/src/resolver.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import WebSocket from 'ws';
2
- import { NCC05Resolver } from 'ncc-05';
2
+ import { NCC05Resolver } from 'ncc-05-js';
3
3
  import { parseNostrMessage, serializeNostrMessage, createReqMessage } from './protocol.js';
4
4
  import { validateNcc02, parseNcc02Tags } from './ncc02.js';
5
5
  import { normalizeLocatorEndpoints, validateLocatorFreshness } from './ncc05.js';
@@ -0,0 +1,127 @@
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
+ function uniqueList(items) {
15
+ const seen = new Set();
16
+ return items.filter(item => {
17
+ if (!item) return false;
18
+ const normalized = item.trim();
19
+ if (seen.has(normalized)) {
20
+ return false;
21
+ }
22
+ seen.add(normalized);
23
+ return true;
24
+ });
25
+ }
26
+
27
+ /**
28
+ * Build a deployable sidecar config object that mirrors the example's defaults.
29
+ */
30
+ export function buildSidecarConfig({
31
+ serviceSk,
32
+ servicePk,
33
+ serviceNpub,
34
+ relayUrl,
35
+ serviceId = 'relay',
36
+ locatorId = 'relay-locator',
37
+ publicationRelays = [],
38
+ publishRelays,
39
+ ncc02ExpSeconds = 14 * 24 * 60 * 60,
40
+ ncc05TtlSeconds = DEFAULT_TTL_SECONDS,
41
+ torControl = DEFAULT_TOR_CONTROL,
42
+ externalEndpoints = {},
43
+ k = {},
44
+ baseDir = process.cwd(),
45
+ ncc02ExpectedKeySource
46
+ } = {}) {
47
+ if (!serviceSk || !servicePk || !serviceNpub) {
48
+ throw new Error('service keypair must be provided');
49
+ }
50
+ if (!relayUrl) {
51
+ throw new Error('relayUrl is required');
52
+ }
53
+
54
+ const expectedKey = getExpectedK({ k, externalEndpoints }, { baseDir });
55
+ const normalizedPublicationRelays = uniqueList([relayUrl, ...publicationRelays]);
56
+ const normalizedPublishRelays = uniqueList([
57
+ relayUrl,
58
+ ...(publishRelays ?? normalizedPublicationRelays)
59
+ ]);
60
+ const keySource = ncc02ExpectedKeySource ?? k.mode ?? 'auto';
61
+
62
+ return {
63
+ serviceSk,
64
+ servicePk,
65
+ serviceNpub,
66
+ relayUrl,
67
+ serviceId,
68
+ locatorId,
69
+ publicationRelays: normalizedPublicationRelays,
70
+ publishRelays: normalizedPublishRelays,
71
+ ncc02ExpSeconds,
72
+ ncc05TtlSeconds,
73
+ ncc02ExpectedKey: expectedKey,
74
+ ncc02ExpectedKeySource: keySource,
75
+ externalEndpoints,
76
+ torControl,
77
+ k
78
+ };
79
+ }
80
+
81
+ /**
82
+ * Build a client config that matches the NCC-06 example expectations.
83
+ */
84
+ export function buildClientConfig({
85
+ relayUrl,
86
+ servicePubkey,
87
+ serviceNpub,
88
+ serviceIdentityUri,
89
+ locatorSecretKey,
90
+ locatorFriendPubkey,
91
+ publicationRelays = [],
92
+ staleFallbackSeconds = 600,
93
+ torPreferred = false,
94
+ ncc05TimeoutMs = 5000,
95
+ serviceId,
96
+ locatorId,
97
+ expectedK
98
+ } = {}) {
99
+ if (!relayUrl) {
100
+ throw new Error('relayUrl is required');
101
+ }
102
+ const identityUri =
103
+ serviceIdentityUri || (serviceNpub ? `wss://${serviceNpub}` : undefined);
104
+ if (!identityUri) {
105
+ throw new Error('serviceIdentityUri or serviceNpub is required');
106
+ }
107
+ if (!servicePubkey) {
108
+ throw new Error('servicePubkey is required');
109
+ }
110
+ const publicationList = uniqueList([relayUrl, ...publicationRelays]);
111
+
112
+ return {
113
+ relayUrl,
114
+ serviceIdentityUri: identityUri,
115
+ servicePubkey,
116
+ serviceNpub: serviceNpub ?? '',
117
+ publicationRelays: publicationList,
118
+ staleFallbackSeconds,
119
+ torPreferred,
120
+ ncc05TimeoutMs,
121
+ locatorSecretKey,
122
+ locatorFriendPubkey,
123
+ serviceId,
124
+ locatorId,
125
+ ncc02ExpectedKey: expectedK
126
+ };
127
+ }
@@ -46,7 +46,7 @@ test('detectGlobalIPv6 filters private addresses', () => {
46
46
 
47
47
  test('getPublicIPv4 returns from first reachable source', async () => {
48
48
  const original = global.fetch;
49
- global.fetch = async (url) => ({
49
+ global.fetch = async (_url) => ({
50
50
  ok: true,
51
51
  text: async () => '{"ip":"5.6.7.8"}'
52
52
  });
package/test/k.test.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { strict as assert } from 'assert';
2
2
  import { test } from 'node:test';
3
3
  import { computeKFromCertPem, getExpectedK } from '../src/k.js';
4
- import { readFileSync, existsSync, rmSync, mkdirSync } from 'fs';
4
+ import { readFileSync, rmSync, mkdirSync } from 'fs';
5
5
  import path from 'path';
6
6
  import os from 'os';
7
7
 
@@ -0,0 +1,36 @@
1
+ import { test } from 'node:test';
2
+ import { strict as assert } from 'assert';
3
+ import { buildSidecarConfig, buildClientConfig } from '../src/sidecar-config.js';
4
+
5
+ test('buildSidecarConfig constructs expected fields based on inputs', () => {
6
+ const cfg = buildSidecarConfig({
7
+ serviceSk: 'sk',
8
+ servicePk: 'pk',
9
+ serviceNpub: 'npub',
10
+ relayUrl: 'ws://localhost:7000',
11
+ k: { mode: 'static', value: 'TESTKEY:abc' },
12
+ publicationRelays: ['ws://aux:7001'],
13
+ externalEndpoints: {
14
+ ipv4: { enabled: true, protocol: 'ws', address: '127.0.0.1', port: 7447 }
15
+ }
16
+ });
17
+
18
+ assert.equal(cfg.serviceSk, 'sk');
19
+ assert.equal(cfg.ncc02ExpectedKey, 'TESTKEY:abc');
20
+ assert.deepEqual(cfg.publicationRelays[0], 'ws://localhost:7000');
21
+ assert.ok(cfg.publishRelays.length >= 1);
22
+ });
23
+
24
+ test('buildClientConfig enforces identity URI and relays', () => {
25
+ const client = buildClientConfig({
26
+ relayUrl: 'ws://relay',
27
+ servicePubkey: 'pk',
28
+ serviceNpub: 'npub',
29
+ expectedK: 'TESTKEY:abc',
30
+ publicationRelays: ['ws://relay', 'ws://aux']
31
+ });
32
+
33
+ assert.equal(client.serviceIdentityUri, 'wss://npub');
34
+ assert.equal(client.publicationRelays.length, 2);
35
+ assert.equal(client.ncc02ExpectedKey, 'TESTKEY:abc');
36
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "ES2020",
5
+ "moduleResolution": "node",
6
+ "allowSyntheticDefaultImports": true,
7
+ "esModuleInterop": true,
8
+ "resolveJsonModule": true,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "strict": true,
11
+ "skipLibCheck": true,
12
+ "checkJs": false,
13
+ "allowJs": true,
14
+ "noEmit": true,
15
+ "types": ["node"]
16
+ },
17
+ "include": ["src/**/*.js", "test/**/*.js"]
18
+ }