ncc-06-js 0.2.3 → 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/DOCS.md +9 -8
- package/README.md +29 -29
- package/index.d.ts +40 -51
- package/package.json +6 -6
- package/src/external-endpoints.js +22 -0
- package/src/ncc02.js +27 -17
- package/src/resolver.js +39 -112
- package/src/selector.js +33 -14
- package/src/sidecar-config.js +58 -80
- package/test/resolver.test.js +21 -26
- package/test/selector.test.js +26 -1
- package/test/sidecar-config.test.js +19 -15
package/DOCS.md
CHANGED
|
@@ -43,28 +43,29 @@ Utilities for locator payload construction and evaluation.
|
|
|
43
43
|
|
|
44
44
|
## NCC-06 helpers
|
|
45
45
|
|
|
46
|
-
### `choosePreferredEndpoint(endpoints, { torPreferred?, expectedK? })`
|
|
47
|
-
- Applies NCC-06 policy:
|
|
46
|
+
### `choosePreferredEndpoint(endpoints, { torPreferred?, expectedK?, allowedProtocols? })`
|
|
47
|
+
- Applies NCC-06 policy: favors onion if `torPreferred`, filters by `allowedProtocols` (default: `['wss', 'ws']`), and validates `k` fingerprints for secure protocols (`wss`, `https`, `tls`, etc).
|
|
48
48
|
- Returns `{ endpoint?: NormalizedEndpoint, reason?: string, expected?, actual? }`.
|
|
49
49
|
|
|
50
50
|
### `resolveServiceEndpoint(options)`
|
|
51
51
|
- Orchestrates resolution by querying bootstrap relays, preferring NCC-05 locators, and falling back to NCC-02 records.
|
|
52
|
-
- **Options** include `bootstrapRelays`, `servicePubkey`, `serviceId`, `locatorId`, `expectedK`, `locatorSecretKey`, `torPreferred`, timeouts, and override hooks.
|
|
53
|
-
- **Returns** `{ endpoint, source,
|
|
52
|
+
- **Options** include `bootstrapRelays`, `servicePubkey`, `serviceId`, `locatorId`, `expectedK`, `locatorSecretKey`, `torPreferred`, `allowedProtocols`, timeouts, and override hooks.
|
|
53
|
+
- **Returns** `{ endpoint, source, serviceRecord, locatorPayload, selection }`.
|
|
54
54
|
|
|
55
55
|
## Sidecar config helpers
|
|
56
56
|
|
|
57
57
|
### `buildSidecarConfig(options)`
|
|
58
|
-
- Mirrors the example sidecar setup and derives `ncc02ExpectedKey`, `publishRelays`, and `torControl` from operator intent
|
|
58
|
+
- Mirrors the example sidecar setup and derives `ncc02ExpectedKey`, `publishRelays`, and `torControl` from operator intent.
|
|
59
|
+
- Supports `serviceUrl` and `serviceMode` as aliases for `relayUrl` and `relayMode`.
|
|
59
60
|
|
|
60
61
|
### `getRelayMode(config)`
|
|
61
|
-
- Returns the normalized `"public"` or `"private"` mode
|
|
62
|
+
- Returns the normalized `"public"` or `"private"` mode based on `relayMode` or `serviceMode`.
|
|
62
63
|
|
|
63
64
|
### `setRelayMode(config, mode)`
|
|
64
|
-
- Normalizes and writes `"public"` or `"private"` back into the config object
|
|
65
|
+
- Normalizes and writes `"public"` or `"private"` back into the config object (setting both `relayMode` and `serviceMode`).
|
|
65
66
|
|
|
66
67
|
### `buildClientConfig(options)`
|
|
67
|
-
- Rehydrates the minimal client config that the example resolver expects
|
|
68
|
+
- Rehydrates the minimal client config that the example resolver expects. Supports `serviceUrl` as alias for `relayUrl`.
|
|
68
69
|
|
|
69
70
|
## Key and TLS helpers
|
|
70
71
|
|
package/README.md
CHANGED
|
@@ -6,14 +6,13 @@ Reusable helpers extracted from the NCC-06 example relay, sidecar, and client im
|
|
|
6
6
|
|
|
7
7
|
- **NCC-02 builders & validators** (`buildNcc02ServiceRecord`, `parseNcc02Tags`, `validateNcc02`) that manage the `d`, `u`, `k`, and `exp` tags a service record must expose.
|
|
8
8
|
- **NCC-05 helpers** (`buildLocatorPayload`, `normalizeLocatorEndpoints`, `validateLocatorFreshness`) that assemble locator payloads, parse stored JSON, and enforce TTL/`updated_at` freshness rules.
|
|
9
|
-
- **Deterministic NCC-06 resolution** via `choosePreferredEndpoint` and `resolveServiceEndpoint
|
|
10
|
-
- **
|
|
11
|
-
- **
|
|
12
|
-
|
|
13
|
-
- **
|
|
14
|
-
|
|
15
|
-
- **
|
|
16
|
-
- **Relay mode helpers** (`getRelayMode`, `setRelayMode`) so you can portrait whether your relay is *public* (publishes NCC-05 locators) or *private* (only maintains an NCC-02 record without advertising endpoints).
|
|
9
|
+
- **Deterministic NCC-06 resolution** via `choosePreferredEndpoint` and `resolveServiceEndpoint`. It queries bootstrap relays, prefers fresh NCC-05 locators, verifies `k` fingerprints for any secure protocol (`wss://`, `https://`, etc.), and falls back to NCC-02 `u` values.
|
|
10
|
+
- **Service-Agnostic Helpers:** While originally built for relays, all helpers support generic `serviceUrl`, `serviceMode`, and custom `allowedProtocols`.
|
|
11
|
+
- **External endpoint helpers** (`buildExternalEndpoints`, `detectGlobalIPv6`, `getPublicIPv4`) so services can declare onion/IPv6/IPv4 reachability in a reproducible order.
|
|
12
|
+
...
|
|
13
|
+
- **Sidecar config helpers** (`buildSidecarConfig`, `buildClientConfig`) so you can reuse the same config generation logic. Supports `serviceUrl` and `serviceMode` aliases for broader application.
|
|
14
|
+
...
|
|
15
|
+
- **Relay & Service mode helpers** (`getRelayMode`, `setRelayMode`) so you can control whether your service is *public* (publishes NCC-05 locators) or *private`.
|
|
17
16
|
|
|
18
17
|
## Usage
|
|
19
18
|
|
|
@@ -23,36 +22,37 @@ Install directly from the repository (example workspace):
|
|
|
23
22
|
npm install ../ncc-06-js
|
|
24
23
|
```
|
|
25
24
|
|
|
26
|
-
|
|
25
|
+
### Resolving an HTTP API Service
|
|
27
26
|
|
|
28
27
|
```js
|
|
29
|
-
import {
|
|
30
|
-
resolveServiceEndpoint,
|
|
31
|
-
buildExternalEndpoints,
|
|
32
|
-
generateExpectedK
|
|
33
|
-
} from 'ncc-06-js';
|
|
34
|
-
|
|
35
|
-
const endpoints = await buildExternalEndpoints({
|
|
36
|
-
ipv4: { enabled: true, protocol: 'wss', address: '1.2.3.4', port: 7447 },
|
|
37
|
-
wsPort: 7000,
|
|
38
|
-
wssPort: 7447,
|
|
39
|
-
ncc02ExpectedKey: 'TESTKEY:relay-local-dev-1',
|
|
40
|
-
ensureOnionService
|
|
41
|
-
});
|
|
28
|
+
import { resolveServiceEndpoint } from 'ncc-06-js';
|
|
42
29
|
|
|
43
30
|
const resolution = await resolveServiceEndpoint({
|
|
44
|
-
bootstrapRelays: ['
|
|
31
|
+
bootstrapRelays: ['wss://relay.damus.io'],
|
|
45
32
|
servicePubkey: '...',
|
|
46
|
-
serviceId: '
|
|
47
|
-
locatorId: '
|
|
48
|
-
expectedK: '
|
|
49
|
-
|
|
33
|
+
serviceId: 'my-api',
|
|
34
|
+
locatorId: 'api-locator',
|
|
35
|
+
expectedK: '...', // SPKI fingerprint for HTTPS pinning
|
|
36
|
+
allowedProtocols: ['https', 'http'] // Override default [wss, ws]
|
|
50
37
|
});
|
|
51
38
|
|
|
52
|
-
console.log('Resolved endpoint:', resolution.endpoint);
|
|
39
|
+
console.log('Resolved API endpoint:', resolution.endpoint);
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Building a Service Config
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
import { buildSidecarConfig } from 'ncc-06-js';
|
|
46
|
+
|
|
47
|
+
const config = buildSidecarConfig({
|
|
48
|
+
secretKey: '...',
|
|
49
|
+
serviceUrl: 'https://api.example.com',
|
|
50
|
+
serviceId: 'my-api',
|
|
51
|
+
serviceMode: 'public'
|
|
52
|
+
});
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
-
The package exposes modular helpers so you can keep using your own transport stack while reusing
|
|
55
|
+
The package exposes modular helpers so you can keep using your own transport stack while reusing deterministic NCC-06 behaviour.
|
|
56
56
|
|
|
57
57
|
## Trust model
|
|
58
58
|
|
package/index.d.ts
CHANGED
|
@@ -34,7 +34,7 @@ declare module 'ncc-06-js' {
|
|
|
34
34
|
type?: string;
|
|
35
35
|
uri?: string;
|
|
36
36
|
value?: string;
|
|
37
|
-
protocol?:
|
|
37
|
+
protocol?: string;
|
|
38
38
|
family?: 'ipv4' | 'ipv6' | 'onion' | string;
|
|
39
39
|
priority?: number;
|
|
40
40
|
prio?: number;
|
|
@@ -65,6 +65,7 @@ declare module 'ncc-06-js' {
|
|
|
65
65
|
export interface SelectorOptions {
|
|
66
66
|
torPreferred?: boolean;
|
|
67
67
|
expectedK?: string;
|
|
68
|
+
allowedProtocols?: string[];
|
|
68
69
|
}
|
|
69
70
|
export interface SelectorResult {
|
|
70
71
|
endpoint: LocatorEndpoint | null;
|
|
@@ -78,6 +79,15 @@ declare module 'ncc-06-js' {
|
|
|
78
79
|
): SelectorResult;
|
|
79
80
|
export { normalizeLocatorEndpoints };
|
|
80
81
|
|
|
82
|
+
export interface ResolvedService {
|
|
83
|
+
endpoint?: string;
|
|
84
|
+
fingerprint?: string;
|
|
85
|
+
expiry: number;
|
|
86
|
+
attestations: any[];
|
|
87
|
+
eventId: string;
|
|
88
|
+
pubkey: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
81
91
|
export interface ResolverOptions {
|
|
82
92
|
bootstrapRelays: string[];
|
|
83
93
|
servicePubkey: string;
|
|
@@ -88,11 +98,8 @@ declare module 'ncc-06-js' {
|
|
|
88
98
|
locatorSecretKey?: string;
|
|
89
99
|
ncc05TimeoutMs?: number;
|
|
90
100
|
publicationRelayTimeoutMs?: number;
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
filter: Record<string, unknown>,
|
|
94
|
-
options?: { timeoutMs?: number }
|
|
95
|
-
) => Promise<NostrEvent[]>;
|
|
101
|
+
pool?: any;
|
|
102
|
+
ncc02Resolver?: any;
|
|
96
103
|
resolveLocator?: (options: {
|
|
97
104
|
bootstrapRelays: string[];
|
|
98
105
|
servicePubkey: string;
|
|
@@ -100,7 +107,6 @@ declare module 'ncc-06-js' {
|
|
|
100
107
|
locatorSecretKey?: string;
|
|
101
108
|
timeout?: number;
|
|
102
109
|
}) => Promise<LocatorPayload | null>;
|
|
103
|
-
throttle?: unknown;
|
|
104
110
|
now?: number;
|
|
105
111
|
}
|
|
106
112
|
export interface ResolverSelection {
|
|
@@ -113,7 +119,7 @@ declare module 'ncc-06-js' {
|
|
|
113
119
|
endpoint: string | null;
|
|
114
120
|
source: 'locator' | 'ncc02' | null;
|
|
115
121
|
locatorPayload: LocatorPayload | null;
|
|
116
|
-
|
|
122
|
+
serviceRecord: ResolvedService;
|
|
117
123
|
selection: ResolverSelection;
|
|
118
124
|
}
|
|
119
125
|
export function resolveServiceEndpoint(options: ResolverOptions): Promise<ResolverResult>;
|
|
@@ -179,14 +185,14 @@ declare module 'ncc-06-js' {
|
|
|
179
185
|
};
|
|
180
186
|
ipv4?: {
|
|
181
187
|
enabled?: boolean;
|
|
182
|
-
protocol?:
|
|
188
|
+
protocol?: string;
|
|
183
189
|
port?: number;
|
|
184
190
|
address?: string;
|
|
185
191
|
publicSources?: string[];
|
|
186
192
|
};
|
|
187
193
|
ipv6?: {
|
|
188
194
|
enabled?: boolean;
|
|
189
|
-
protocol?:
|
|
195
|
+
protocol?: string;
|
|
190
196
|
port?: number;
|
|
191
197
|
};
|
|
192
198
|
wsPort?: number;
|
|
@@ -198,76 +204,59 @@ declare module 'ncc-06-js' {
|
|
|
198
204
|
export function buildExternalEndpoints(options?: ExternalEndpointOptions): Promise<LocatorEndpoint[]>;
|
|
199
205
|
export function detectGlobalIPv6(): string | null;
|
|
200
206
|
export function getPublicIPv4(options?: { sources?: string[] }): Promise<string | null>;
|
|
207
|
+
export function normalizeRelayUrl(url: string): string;
|
|
208
|
+
export function normalizeRelays(relays: string[]): string[];
|
|
201
209
|
|
|
202
210
|
export interface SidecarConfigOptions {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
relayUrl: string;
|
|
211
|
+
secretKey: string;
|
|
212
|
+
serviceUrl?: string;
|
|
213
|
+
relayUrl?: string;
|
|
207
214
|
serviceId?: string;
|
|
208
215
|
locatorId?: string;
|
|
209
216
|
publicationRelays?: string[];
|
|
210
217
|
publishRelays?: string[];
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
torControl?: Record<string, unknown>;
|
|
214
|
-
externalEndpoints?: Record<string, unknown>;
|
|
215
|
-
k?: KConfig;
|
|
216
|
-
baseDir?: string;
|
|
218
|
+
persistPath?: string;
|
|
219
|
+
certPath?: string;
|
|
217
220
|
relayMode?: 'public' | 'private';
|
|
218
|
-
|
|
221
|
+
serviceMode?: 'public' | 'private';
|
|
219
222
|
}
|
|
220
223
|
export interface SidecarConfig {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
serviceNpub: string;
|
|
224
|
+
secretKey: string;
|
|
225
|
+
serviceUrl: string;
|
|
224
226
|
relayUrl: string;
|
|
225
227
|
serviceId: string;
|
|
226
228
|
locatorId: string;
|
|
227
229
|
publicationRelays: string[];
|
|
228
230
|
publishRelays: string[];
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
ncc02ExpectedKey: string;
|
|
232
|
-
ncc02ExpectedKeySource: string;
|
|
233
|
-
externalEndpoints: Record<string, unknown>;
|
|
234
|
-
torControl: Record<string, unknown>;
|
|
235
|
-
k: KConfig;
|
|
231
|
+
persistPath?: string;
|
|
232
|
+
certPath?: string;
|
|
236
233
|
relayMode: 'public' | 'private';
|
|
234
|
+
serviceMode: 'public' | 'private';
|
|
237
235
|
}
|
|
238
236
|
export function buildSidecarConfig(options: SidecarConfigOptions): SidecarConfig;
|
|
239
|
-
export function getRelayMode(config?: { relayMode?: string }): 'public' | 'private';
|
|
237
|
+
export function getRelayMode(config?: { relayMode?: string, serviceMode?: string }): 'public' | 'private';
|
|
240
238
|
export function setRelayMode(config?: Record<string, unknown>, mode: 'public' | 'private'): Record<string, unknown>;
|
|
241
239
|
|
|
242
240
|
export interface ClientConfigOptions {
|
|
243
|
-
relayUrl: string;
|
|
244
|
-
servicePubkey?: string;
|
|
245
|
-
serviceNpub?: string;
|
|
246
241
|
serviceIdentityUri?: string;
|
|
247
|
-
|
|
248
|
-
|
|
242
|
+
serviceNpub?: string;
|
|
243
|
+
servicePubkey?: string;
|
|
244
|
+
serviceUrl?: string;
|
|
245
|
+
relayUrl?: string;
|
|
249
246
|
publicationRelays?: string[];
|
|
250
|
-
staleFallbackSeconds?: number;
|
|
251
|
-
torPreferred?: boolean;
|
|
252
|
-
ncc05TimeoutMs?: number;
|
|
253
247
|
serviceId?: string;
|
|
254
248
|
locatorId?: string;
|
|
255
|
-
|
|
249
|
+
ncc02ExpectedKey?: string;
|
|
256
250
|
}
|
|
257
251
|
export interface ClientConfig {
|
|
258
|
-
relayUrl: string;
|
|
259
252
|
serviceIdentityUri: string;
|
|
260
253
|
servicePubkey: string;
|
|
261
|
-
|
|
254
|
+
serviceUrl: string;
|
|
255
|
+
relayUrl: string;
|
|
262
256
|
publicationRelays: string[];
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
ncc05TimeoutMs: number;
|
|
266
|
-
locatorSecretKey?: string;
|
|
267
|
-
locatorFriendPubkey?: string;
|
|
268
|
-
serviceId?: string;
|
|
269
|
-
locatorId?: string;
|
|
257
|
+
serviceId: string;
|
|
258
|
+
locatorId: string;
|
|
270
259
|
ncc02ExpectedKey?: string;
|
|
271
260
|
}
|
|
272
261
|
export function buildClientConfig(options: ClientConfigOptions): ClientConfig;
|
|
273
|
-
}
|
|
262
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ncc-06-js",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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",
|
|
@@ -10,15 +10,15 @@
|
|
|
10
10
|
"lint": "eslint . --ext .js"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"ncc-02-js": "^0.
|
|
14
|
-
"ncc-05-js": "^1.1.
|
|
13
|
+
"ncc-02-js": "^0.3.0",
|
|
14
|
+
"ncc-05-js": "^1.1.14",
|
|
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
|
-
"@types/node": "^
|
|
21
|
-
"eslint": "^
|
|
22
|
-
"typescript": "^5.
|
|
20
|
+
"@types/node": "^25.0.3",
|
|
21
|
+
"eslint": "^9.39.2",
|
|
22
|
+
"typescript": "^5.9.3"
|
|
23
23
|
}
|
|
24
24
|
}
|
|
@@ -132,3 +132,25 @@ export async function getPublicIPv4({ sources = ['https://api.ipify.org?format=j
|
|
|
132
132
|
}
|
|
133
133
|
return null;
|
|
134
134
|
}
|
|
135
|
+
|
|
136
|
+
export function normalizeRelayUrl(url) {
|
|
137
|
+
if (!url) return '';
|
|
138
|
+
let normalized = url.trim();
|
|
139
|
+
if (normalized.endsWith('/')) {
|
|
140
|
+
normalized = normalized.slice(0, -1);
|
|
141
|
+
}
|
|
142
|
+
if (!normalized.includes('://')) {
|
|
143
|
+
normalized = `wss://${normalized}`;
|
|
144
|
+
}
|
|
145
|
+
return normalized;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function normalizeRelays(relays) {
|
|
149
|
+
if (!Array.isArray(relays)) return [];
|
|
150
|
+
const normalized = relays
|
|
151
|
+
.filter(Boolean)
|
|
152
|
+
.map(normalizeRelayUrl);
|
|
153
|
+
return [...new Set(normalized)];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
|
package/src/ncc02.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { finalizeEvent, getPublicKey, validateEvent, verifyEvent } from 'nostr-tools/pure';
|
|
2
|
+
import { NCC02Builder } from 'ncc-02-js';
|
|
2
3
|
|
|
3
4
|
const DEFAULT_KIND = 30059;
|
|
4
5
|
|
|
@@ -19,22 +20,31 @@ export function buildNcc02ServiceRecord({
|
|
|
19
20
|
if (!secretKey) {
|
|
20
21
|
throw new Error('secretKey is required to build NCC-02 records');
|
|
21
22
|
}
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
23
|
+
|
|
24
|
+
const builder = new NCC02Builder(secretKey);
|
|
25
|
+
// NCC02Builder expects days.
|
|
26
|
+
const expiryDays = expirySeconds / (24 * 60 * 60);
|
|
27
|
+
|
|
28
|
+
const event = builder.createServiceRecord({
|
|
29
|
+
serviceId,
|
|
30
|
+
endpoint,
|
|
31
|
+
fingerprint,
|
|
32
|
+
expiryDays
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// If createdAt or kind override is needed, we must re-sign.
|
|
36
|
+
if (createdAt || kind !== DEFAULT_KIND) {
|
|
37
|
+
const template = {
|
|
38
|
+
...event,
|
|
39
|
+
created_at: createdAt ?? event.created_at,
|
|
40
|
+
kind: kind ?? event.kind,
|
|
41
|
+
id: undefined,
|
|
42
|
+
sig: undefined
|
|
43
|
+
};
|
|
44
|
+
return finalizeEvent(template, secretKey);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return event;
|
|
38
48
|
}
|
|
39
49
|
|
|
40
50
|
/**
|
|
@@ -70,4 +80,4 @@ export function validateNcc02(event, { expectedAuthor, expectedD, now, allowExpi
|
|
|
70
80
|
return false;
|
|
71
81
|
}
|
|
72
82
|
return true;
|
|
73
|
-
}
|
|
83
|
+
}
|
package/src/resolver.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import WebSocket from 'ws';
|
|
2
2
|
import { NCC05Resolver } from 'ncc-05-js';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { normalizeLocatorEndpoints, validateLocatorFreshness } from './ncc05.js';
|
|
3
|
+
import { NCC02Resolver } from 'ncc-02-js';
|
|
4
|
+
import { validateLocatorFreshness, normalizeLocatorEndpoints } from './ncc05.js';
|
|
6
5
|
import { choosePreferredEndpoint } from './selector.js';
|
|
7
6
|
|
|
8
7
|
const DEFAULT_RELAY_TIMEOUT_MS = 5000;
|
|
@@ -21,7 +20,8 @@ export async function resolveServiceEndpoint(options = {}) {
|
|
|
21
20
|
locatorSecretKey,
|
|
22
21
|
ncc05TimeoutMs = 5000,
|
|
23
22
|
publicationRelayTimeoutMs = DEFAULT_RELAY_TIMEOUT_MS,
|
|
24
|
-
|
|
23
|
+
pool, // Allow passing a shared pool
|
|
24
|
+
ncc02Resolver, // Injection for testing
|
|
25
25
|
resolveLocator,
|
|
26
26
|
now
|
|
27
27
|
} = options;
|
|
@@ -40,22 +40,25 @@ export async function resolveServiceEndpoint(options = {}) {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
const timestamp = now ?? Math.floor(Date.now() / 1000);
|
|
43
|
-
const fetchEvents = queryRelayEvents ?? defaultQueryRelayEvents;
|
|
44
43
|
const locatorResolver = resolveLocator ?? defaultResolveLocator;
|
|
45
44
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
45
|
+
// 1. Resolve NCC-02 Service Record using the library
|
|
46
|
+
let serviceRecord;
|
|
47
|
+
try {
|
|
48
|
+
const resolver = ncc02Resolver || new NCC02Resolver(bootstrapRelays, { pool });
|
|
49
|
+
serviceRecord = await resolver.resolve(servicePubkey, serviceId, {
|
|
50
|
+
// We can pass options if needed, e.g. minLevel
|
|
51
|
+
});
|
|
52
|
+
} catch (err) {
|
|
53
|
+
// Map library errors or rethrow?
|
|
54
|
+
// The library throws generic Error or NCC02Error.
|
|
55
|
+
// We can just let it propagate or wrap.
|
|
56
|
+
// Existing code threw "No valid NCC-02 service record available".
|
|
57
|
+
// We'll let the library error bubble up as it provides more detail.
|
|
58
|
+
throw err;
|
|
57
59
|
}
|
|
58
|
-
|
|
60
|
+
|
|
61
|
+
// 2. Resolve NCC-05 Locator
|
|
59
62
|
const locatorPayload = await locatorResolver({
|
|
60
63
|
bootstrapRelays,
|
|
61
64
|
servicePubkey,
|
|
@@ -64,8 +67,9 @@ export async function resolveServiceEndpoint(options = {}) {
|
|
|
64
67
|
timeout: ncc05TimeoutMs
|
|
65
68
|
});
|
|
66
69
|
|
|
70
|
+
// 3. Determine Endpoint
|
|
67
71
|
const selection = determineEndpoint({
|
|
68
|
-
|
|
72
|
+
serviceRecord,
|
|
69
73
|
locatorPayload,
|
|
70
74
|
expectedK,
|
|
71
75
|
torPreferred,
|
|
@@ -76,15 +80,20 @@ export async function resolveServiceEndpoint(options = {}) {
|
|
|
76
80
|
endpoint: selection.endpoint,
|
|
77
81
|
source: selection.source,
|
|
78
82
|
locatorPayload,
|
|
79
|
-
|
|
83
|
+
serviceRecord,
|
|
80
84
|
selection
|
|
81
85
|
};
|
|
82
86
|
}
|
|
83
87
|
|
|
84
|
-
function determineEndpoint({
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
+
function determineEndpoint({ serviceRecord, locatorPayload, expectedK, torPreferred, now }) {
|
|
89
|
+
// serviceRecord is { endpoint, fingerprint, expiry, attestations, ... }
|
|
90
|
+
// It is already validated (signature, expiry).
|
|
91
|
+
|
|
92
|
+
const ncc02Url = serviceRecord.endpoint;
|
|
93
|
+
const k = serviceRecord.fingerprint;
|
|
94
|
+
// expiry check was done by resolver, but we check if we need to?
|
|
95
|
+
// NCC02Resolver throws if expired. So we can assume it's fresh.
|
|
96
|
+
|
|
88
97
|
const result = {
|
|
89
98
|
endpoint: null,
|
|
90
99
|
source: null,
|
|
@@ -110,20 +119,22 @@ function determineEndpoint({ serviceEvent, locatorPayload, expectedK, torPreferr
|
|
|
110
119
|
result.evidence = selection;
|
|
111
120
|
}
|
|
112
121
|
|
|
113
|
-
if (ncc02Url
|
|
114
|
-
|
|
122
|
+
if (ncc02Url) {
|
|
123
|
+
const isSecure = ncc02Url.match(/^(wss|https|tls|tcps):\/\//) || (ncc02Url.includes('://') && ncc02Url.split(':')[0].endsWith('s'));
|
|
124
|
+
|
|
125
|
+
if (isSecure && expectedK && k && k !== expectedK) {
|
|
115
126
|
return {
|
|
116
127
|
endpoint: null,
|
|
117
128
|
source: 'ncc02',
|
|
118
129
|
reason: 'k-mismatch',
|
|
119
|
-
evidence: { expected: expectedK, actual:
|
|
130
|
+
evidence: { expected: expectedK, actual: k }
|
|
120
131
|
};
|
|
121
132
|
}
|
|
122
133
|
return {
|
|
123
134
|
endpoint: ncc02Url,
|
|
124
135
|
source: 'ncc02',
|
|
125
136
|
reason: 'fallback',
|
|
126
|
-
evidence: {
|
|
137
|
+
evidence: { endpoint: ncc02Url, fingerprint: k }
|
|
127
138
|
};
|
|
128
139
|
}
|
|
129
140
|
|
|
@@ -131,91 +142,6 @@ function determineEndpoint({ serviceEvent, locatorPayload, expectedK, torPreferr
|
|
|
131
142
|
return result;
|
|
132
143
|
}
|
|
133
144
|
|
|
134
|
-
function pickBestServiceRecord(events, now, expectedServiceId) {
|
|
135
|
-
const candidates = [];
|
|
136
|
-
for (const event of events) {
|
|
137
|
-
if (!validateNcc02(event, { now, expectedD: expectedServiceId })) {
|
|
138
|
-
continue;
|
|
139
|
-
}
|
|
140
|
-
candidates.push(event);
|
|
141
|
-
}
|
|
142
|
-
return candidates.sort((a, b) => {
|
|
143
|
-
if (b.created_at !== a.created_at) {
|
|
144
|
-
return b.created_at - a.created_at;
|
|
145
|
-
}
|
|
146
|
-
return b.id.localeCompare(a.id);
|
|
147
|
-
})[0] || null;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
async function defaultQueryRelayEvents(relays, filter, options = {}) {
|
|
151
|
-
const queries = relays.map(relay => queryRelayForEvents(relay, filter, options.timeoutMs));
|
|
152
|
-
const settled = await Promise.allSettled(queries);
|
|
153
|
-
return settled.reduce((acc, item) => {
|
|
154
|
-
if (item.status === 'fulfilled') {
|
|
155
|
-
return acc.concat(item.value);
|
|
156
|
-
}
|
|
157
|
-
return acc;
|
|
158
|
-
}, []);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function queryRelayForEvents(relayUrl, filter, timeoutMs = DEFAULT_RELAY_TIMEOUT_MS) {
|
|
162
|
-
return new Promise((resolve, reject) => {
|
|
163
|
-
const ws = new WebSocket(relayUrl);
|
|
164
|
-
const events = [];
|
|
165
|
-
const subId = `ncc06-${Math.random().toString(16).slice(2, 8)}`;
|
|
166
|
-
let settled = false;
|
|
167
|
-
const timer = setTimeout(() => {
|
|
168
|
-
if (!settled) {
|
|
169
|
-
settled = true;
|
|
170
|
-
ws.close();
|
|
171
|
-
resolve(events);
|
|
172
|
-
}
|
|
173
|
-
}, timeoutMs);
|
|
174
|
-
|
|
175
|
-
ws.onopen = () => {
|
|
176
|
-
ws.send(serializeNostrMessage(createReqMessage(subId, filter)));
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
ws.onmessage = raw => {
|
|
180
|
-
const message = parseNostrMessage(raw.data.toString());
|
|
181
|
-
if (!message) {
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
const [type, ...payload] = message;
|
|
185
|
-
if (type === 'EVENT') {
|
|
186
|
-
const [receivedSubId, event] = payload;
|
|
187
|
-
if (receivedSubId === subId) {
|
|
188
|
-
events.push(event);
|
|
189
|
-
}
|
|
190
|
-
} else if (type === 'EOSE') {
|
|
191
|
-
const [receivedSubId] = payload;
|
|
192
|
-
if (receivedSubId === subId && !settled) {
|
|
193
|
-
settled = true;
|
|
194
|
-
clearTimeout(timer);
|
|
195
|
-
ws.close();
|
|
196
|
-
resolve(events);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
};
|
|
200
|
-
|
|
201
|
-
ws.onerror = err => {
|
|
202
|
-
if (!settled) {
|
|
203
|
-
settled = true;
|
|
204
|
-
clearTimeout(timer);
|
|
205
|
-
reject(err);
|
|
206
|
-
}
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
ws.onclose = () => {
|
|
210
|
-
if (!settled) {
|
|
211
|
-
settled = true;
|
|
212
|
-
clearTimeout(timer);
|
|
213
|
-
resolve(events);
|
|
214
|
-
}
|
|
215
|
-
};
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
|
|
219
145
|
async function defaultResolveLocator({
|
|
220
146
|
bootstrapRelays,
|
|
221
147
|
servicePubkey,
|
|
@@ -236,3 +162,4 @@ async function defaultResolveLocator({
|
|
|
236
162
|
resolver.close();
|
|
237
163
|
}
|
|
238
164
|
}
|
|
165
|
+
|
package/src/selector.js
CHANGED
|
@@ -9,41 +9,60 @@ function findByFamily(list, family) {
|
|
|
9
9
|
return list.find(ep => ep.family === family);
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
function isSecureProtocol(protocol) {
|
|
13
|
+
return ['wss', 'https', 'tls', 'tcps'].includes(protocol) || (protocol && protocol.endsWith('s') && protocol !== 'ws');
|
|
14
|
+
}
|
|
15
|
+
|
|
12
16
|
/**
|
|
13
17
|
* Choose which endpoint to connect to based on NCC-06 policy.
|
|
14
18
|
* - prefers onion when torPreferred.
|
|
15
|
-
* -
|
|
19
|
+
* - filters by allowedProtocols (default: wss, ws).
|
|
20
|
+
* - validates 'k' for secure protocols.
|
|
16
21
|
*/
|
|
17
22
|
export function choosePreferredEndpoint(endpoints = [], options = {}) {
|
|
18
|
-
const {
|
|
23
|
+
const {
|
|
24
|
+
torPreferred = false,
|
|
25
|
+
expectedK,
|
|
26
|
+
allowedProtocols = ['wss', 'ws']
|
|
27
|
+
} = options;
|
|
28
|
+
|
|
19
29
|
const normalized = endpoints.length ? endpoints : [];
|
|
20
|
-
|
|
21
|
-
|
|
30
|
+
|
|
31
|
+
// Filter by allowed protocols
|
|
32
|
+
const candidates = normalized.filter(ep => allowedProtocols.includes(ep.protocol));
|
|
33
|
+
|
|
34
|
+
let selection = null;
|
|
22
35
|
|
|
23
|
-
|
|
36
|
+
// Tor Preference: Look for onion in candidates
|
|
24
37
|
if (torPreferred) {
|
|
25
|
-
|
|
38
|
+
selection = findByFamily(candidates, 'onion');
|
|
26
39
|
}
|
|
27
|
-
|
|
28
|
-
|
|
40
|
+
|
|
41
|
+
// Priority Fallback
|
|
42
|
+
if (!selection) {
|
|
43
|
+
selection = pickByPriority(candidates);
|
|
29
44
|
}
|
|
30
|
-
|
|
45
|
+
|
|
46
|
+
if (!selection) {
|
|
31
47
|
return { endpoint: null, reason: 'no-endpoint' };
|
|
32
48
|
}
|
|
33
|
-
|
|
34
|
-
|
|
49
|
+
|
|
50
|
+
// Security Validation
|
|
51
|
+
if (isSecureProtocol(selection.protocol)) {
|
|
52
|
+
if (!selection.k) {
|
|
35
53
|
return { endpoint: null, reason: 'missing-k' };
|
|
36
54
|
}
|
|
37
|
-
if (expectedK &&
|
|
55
|
+
if (expectedK && selection.k !== expectedK) {
|
|
38
56
|
return {
|
|
39
57
|
endpoint: null,
|
|
40
58
|
reason: 'k-mismatch',
|
|
41
59
|
expected: expectedK,
|
|
42
|
-
actual:
|
|
60
|
+
actual: selection.k
|
|
43
61
|
};
|
|
44
62
|
}
|
|
45
63
|
}
|
|
46
|
-
|
|
64
|
+
|
|
65
|
+
return { endpoint: selection };
|
|
47
66
|
}
|
|
48
67
|
|
|
49
68
|
export { normalizeLocatorEndpoints };
|
package/src/sidecar-config.js
CHANGED
|
@@ -11,6 +11,8 @@ const DEFAULT_TOR_CONTROL = {
|
|
|
11
11
|
timeout: 5000
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
+
import { normalizeRelayUrl, normalizeRelays } from './external-endpoints.js';
|
|
15
|
+
|
|
14
16
|
const RELAY_MODE_PUBLIC = 'public';
|
|
15
17
|
const RELAY_MODE_PRIVATE = 'private';
|
|
16
18
|
|
|
@@ -18,136 +20,112 @@ function normalizeRelayMode(mode) {
|
|
|
18
20
|
if (!mode) {
|
|
19
21
|
return RELAY_MODE_PUBLIC;
|
|
20
22
|
}
|
|
21
|
-
const value =
|
|
23
|
+
const value = mode.toLowerCase();
|
|
22
24
|
if (value !== RELAY_MODE_PUBLIC && value !== RELAY_MODE_PRIVATE) {
|
|
23
|
-
throw new Error(`relayMode must be "${RELAY_MODE_PUBLIC}" or "${RELAY_MODE_PRIVATE}"`);
|
|
25
|
+
throw new Error(`relayMode (or serviceMode) must be "${RELAY_MODE_PUBLIC}" or "${RELAY_MODE_PRIVATE}"`);
|
|
24
26
|
}
|
|
25
27
|
return value;
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
function uniqueList(
|
|
29
|
-
|
|
30
|
-
return items.filter(item => {
|
|
31
|
-
if (!item) return false;
|
|
32
|
-
const normalized = item.trim();
|
|
33
|
-
if (seen.has(normalized)) {
|
|
34
|
-
return false;
|
|
35
|
-
}
|
|
36
|
-
seen.add(normalized);
|
|
37
|
-
return true;
|
|
38
|
-
});
|
|
30
|
+
function uniqueList(arr) {
|
|
31
|
+
return [...new Set(arr.filter(Boolean))];
|
|
39
32
|
}
|
|
40
33
|
|
|
41
34
|
export function getRelayMode(config = {}) {
|
|
42
|
-
return normalizeRelayMode(config.relayMode);
|
|
35
|
+
return normalizeRelayMode(config.relayMode || config.serviceMode);
|
|
43
36
|
}
|
|
44
37
|
|
|
45
38
|
export function setRelayMode(config = {}, mode) {
|
|
46
39
|
const normalized = normalizeRelayMode(mode);
|
|
47
|
-
return { ...config, relayMode: normalized };
|
|
40
|
+
return { ...config, relayMode: normalized, serviceMode: normalized };
|
|
48
41
|
}
|
|
49
42
|
|
|
50
43
|
/**
|
|
51
|
-
* Build
|
|
44
|
+
* Build configuration for the NCC-06 Sidecar.
|
|
52
45
|
*/
|
|
53
46
|
export function buildSidecarConfig({
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
serviceNpub,
|
|
47
|
+
secretKey,
|
|
48
|
+
serviceUrl,
|
|
57
49
|
relayUrl,
|
|
58
50
|
serviceId = 'relay',
|
|
59
51
|
locatorId = 'relay-locator',
|
|
60
52
|
publicationRelays = [],
|
|
61
53
|
publishRelays,
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
torControl = DEFAULT_TOR_CONTROL,
|
|
65
|
-
externalEndpoints = {},
|
|
66
|
-
k = {},
|
|
67
|
-
baseDir = process.cwd(),
|
|
54
|
+
persistPath,
|
|
55
|
+
certPath,
|
|
68
56
|
relayMode,
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
if (!
|
|
72
|
-
throw new Error('
|
|
57
|
+
serviceMode
|
|
58
|
+
}) {
|
|
59
|
+
if (!secretKey) {
|
|
60
|
+
throw new Error('secretKey is required');
|
|
73
61
|
}
|
|
74
|
-
|
|
75
|
-
|
|
62
|
+
|
|
63
|
+
const primaryUrl = serviceUrl || relayUrl;
|
|
64
|
+
|
|
65
|
+
if (!primaryUrl) {
|
|
66
|
+
throw new Error('serviceUrl (or relayUrl) is required');
|
|
76
67
|
}
|
|
77
68
|
|
|
78
|
-
const
|
|
79
|
-
const normalizedPublicationRelays = uniqueList([
|
|
69
|
+
const normalizedPrimaryUrl = normalizeRelayUrl(primaryUrl);
|
|
70
|
+
const normalizedPublicationRelays = uniqueList([normalizedPrimaryUrl, ...publicationRelays]);
|
|
80
71
|
const normalizedPublishRelays = uniqueList([
|
|
81
|
-
|
|
72
|
+
normalizedPrimaryUrl,
|
|
82
73
|
...(publishRelays ?? normalizedPublicationRelays)
|
|
83
74
|
]);
|
|
84
|
-
|
|
85
|
-
const
|
|
75
|
+
|
|
76
|
+
const mode = relayMode || serviceMode;
|
|
77
|
+
const normalizedMode = normalizeRelayMode(mode);
|
|
86
78
|
|
|
87
79
|
return {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
relayUrl,
|
|
80
|
+
secretKey,
|
|
81
|
+
serviceUrl: normalizedPrimaryUrl,
|
|
82
|
+
relayUrl: normalizedPrimaryUrl, // Backward compatibility
|
|
92
83
|
serviceId,
|
|
93
84
|
locatorId,
|
|
94
85
|
publicationRelays: normalizedPublicationRelays,
|
|
95
86
|
publishRelays: normalizedPublishRelays,
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
externalEndpoints,
|
|
101
|
-
torControl,
|
|
102
|
-
k,
|
|
103
|
-
relayMode: normalizedRelayMode
|
|
87
|
+
persistPath,
|
|
88
|
+
certPath,
|
|
89
|
+
relayMode: normalizedMode,
|
|
90
|
+
serviceMode: normalizedMode // Alias
|
|
104
91
|
};
|
|
105
92
|
}
|
|
106
93
|
|
|
107
94
|
/**
|
|
108
|
-
* Build
|
|
95
|
+
* Build configuration for the NCC-06 Client.
|
|
109
96
|
*/
|
|
110
97
|
export function buildClientConfig({
|
|
111
|
-
relayUrl,
|
|
112
|
-
servicePubkey,
|
|
113
|
-
serviceNpub,
|
|
114
98
|
serviceIdentityUri,
|
|
115
|
-
|
|
116
|
-
|
|
99
|
+
serviceNpub, // Deprecated, but supported
|
|
100
|
+
servicePubkey,
|
|
101
|
+
serviceUrl,
|
|
102
|
+
relayUrl,
|
|
117
103
|
publicationRelays = [],
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
} = {}) {
|
|
125
|
-
if (!relayUrl) {
|
|
126
|
-
throw new Error('relayUrl is required');
|
|
127
|
-
}
|
|
128
|
-
const identityUri =
|
|
129
|
-
serviceIdentityUri || (serviceNpub ? `wss://${serviceNpub}` : undefined);
|
|
130
|
-
if (!identityUri) {
|
|
131
|
-
throw new Error('serviceIdentityUri or serviceNpub is required');
|
|
104
|
+
serviceId = 'relay',
|
|
105
|
+
locatorId = 'relay-locator',
|
|
106
|
+
ncc02ExpectedKey
|
|
107
|
+
}) {
|
|
108
|
+
if (!serviceIdentityUri && !serviceNpub) {
|
|
109
|
+
throw new Error('serviceIdentityUri (or serviceNpub) is required');
|
|
132
110
|
}
|
|
133
|
-
|
|
134
|
-
|
|
111
|
+
|
|
112
|
+
const primaryUrl = serviceUrl || relayUrl;
|
|
113
|
+
|
|
114
|
+
if (!primaryUrl) {
|
|
115
|
+
throw new Error('serviceUrl (or relayUrl) is required');
|
|
135
116
|
}
|
|
136
|
-
|
|
117
|
+
|
|
118
|
+
const normalizedPrimaryUrl = normalizeRelayUrl(primaryUrl);
|
|
119
|
+
const publicationList = uniqueList([normalizedPrimaryUrl, ...publicationRelays]);
|
|
137
120
|
|
|
138
121
|
return {
|
|
139
|
-
|
|
140
|
-
serviceIdentityUri: identityUri,
|
|
122
|
+
serviceIdentityUri: serviceIdentityUri || (serviceNpub ? `wss://${serviceNpub}` : null),
|
|
141
123
|
servicePubkey,
|
|
142
|
-
|
|
124
|
+
serviceUrl: normalizedPrimaryUrl,
|
|
125
|
+
relayUrl: normalizedPrimaryUrl, // Backward compatibility
|
|
143
126
|
publicationRelays: publicationList,
|
|
144
|
-
staleFallbackSeconds,
|
|
145
|
-
torPreferred,
|
|
146
|
-
ncc05TimeoutMs,
|
|
147
|
-
locatorSecretKey,
|
|
148
|
-
locatorFriendPubkey,
|
|
149
127
|
serviceId,
|
|
150
128
|
locatorId,
|
|
151
|
-
ncc02ExpectedKey
|
|
129
|
+
ncc02ExpectedKey
|
|
152
130
|
};
|
|
153
131
|
}
|
package/test/resolver.test.js
CHANGED
|
@@ -1,31 +1,23 @@
|
|
|
1
1
|
import { test } from 'node:test';
|
|
2
2
|
import { strict as assert } from 'assert';
|
|
3
3
|
import { resolveServiceEndpoint } from '../src/resolver.js';
|
|
4
|
-
import { buildNcc02ServiceRecord } from '../src/ncc02.js';
|
|
5
4
|
import { buildLocatorPayload } from '../src/ncc05.js';
|
|
6
5
|
import { generateKeypair } from '../src/keys.js';
|
|
7
6
|
|
|
8
7
|
const SERVICE_ID = 'relay';
|
|
9
8
|
const LOCATOR_ID = 'relay-locator';
|
|
10
9
|
|
|
11
|
-
function buildServiceEvent({ secretKey, serviceId, endpoint, fingerprint }) {
|
|
12
|
-
return buildNcc02ServiceRecord({
|
|
13
|
-
secretKey,
|
|
14
|
-
serviceId,
|
|
15
|
-
endpoint,
|
|
16
|
-
fingerprint,
|
|
17
|
-
expirySeconds: 60
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
|
-
|
|
21
10
|
test('resolver prefers NCC-05 endpoint when k matches', async () => {
|
|
22
|
-
const {
|
|
23
|
-
const
|
|
24
|
-
secretKey,
|
|
25
|
-
serviceId: SERVICE_ID,
|
|
11
|
+
const { publicKey } = generateKeypair();
|
|
12
|
+
const mockServiceRecord = {
|
|
26
13
|
endpoint: 'wss://fallback',
|
|
27
|
-
fingerprint: 'TESTKEY:match'
|
|
28
|
-
|
|
14
|
+
fingerprint: 'TESTKEY:match',
|
|
15
|
+
expiry: Math.floor(Date.now() / 1000) + 60,
|
|
16
|
+
attestations: [],
|
|
17
|
+
eventId: 'mock-id',
|
|
18
|
+
pubkey: publicKey
|
|
19
|
+
};
|
|
20
|
+
|
|
29
21
|
const locatorPayload = buildLocatorPayload({
|
|
30
22
|
ttl: 60,
|
|
31
23
|
endpoints: [
|
|
@@ -39,7 +31,7 @@ test('resolver prefers NCC-05 endpoint when k matches', async () => {
|
|
|
39
31
|
serviceId: SERVICE_ID,
|
|
40
32
|
locatorId: LOCATOR_ID,
|
|
41
33
|
expectedK: 'TESTKEY:match',
|
|
42
|
-
|
|
34
|
+
ncc02Resolver: { resolve: async () => mockServiceRecord },
|
|
43
35
|
resolveLocator: async () => locatorPayload
|
|
44
36
|
});
|
|
45
37
|
|
|
@@ -48,13 +40,16 @@ test('resolver prefers NCC-05 endpoint when k matches', async () => {
|
|
|
48
40
|
});
|
|
49
41
|
|
|
50
42
|
test('resolver falls back to NCC-02 when locator k mismatches', async () => {
|
|
51
|
-
const {
|
|
52
|
-
const
|
|
53
|
-
secretKey,
|
|
54
|
-
serviceId: SERVICE_ID,
|
|
43
|
+
const { publicKey } = generateKeypair();
|
|
44
|
+
const mockServiceRecord = {
|
|
55
45
|
endpoint: 'wss://fallback',
|
|
56
|
-
fingerprint: 'TESTKEY:match'
|
|
57
|
-
|
|
46
|
+
fingerprint: 'TESTKEY:match',
|
|
47
|
+
expiry: Math.floor(Date.now() / 1000) + 60,
|
|
48
|
+
attestations: [],
|
|
49
|
+
eventId: 'mock-id',
|
|
50
|
+
pubkey: publicKey
|
|
51
|
+
};
|
|
52
|
+
|
|
58
53
|
const locatorPayload = buildLocatorPayload({
|
|
59
54
|
ttl: 60,
|
|
60
55
|
endpoints: [
|
|
@@ -68,11 +63,11 @@ test('resolver falls back to NCC-02 when locator k mismatches', async () => {
|
|
|
68
63
|
serviceId: SERVICE_ID,
|
|
69
64
|
locatorId: LOCATOR_ID,
|
|
70
65
|
expectedK: 'TESTKEY:match',
|
|
71
|
-
|
|
66
|
+
ncc02Resolver: { resolve: async () => mockServiceRecord },
|
|
72
67
|
resolveLocator: async () => locatorPayload
|
|
73
68
|
});
|
|
74
69
|
|
|
75
70
|
assert.equal(result.endpoint, 'wss://fallback');
|
|
76
71
|
assert.equal(result.source, 'ncc02');
|
|
77
72
|
assert.equal(result.selection.reason, 'fallback');
|
|
78
|
-
});
|
|
73
|
+
});
|
package/test/selector.test.js
CHANGED
|
@@ -21,7 +21,32 @@ test('rejects mismatched k', () => {
|
|
|
21
21
|
|
|
22
22
|
test('returns missing k reason', () => {
|
|
23
23
|
const result = choosePreferredEndpoint([
|
|
24
|
-
{
|
|
24
|
+
{ protocol: 'wss', url: 'wss://secure', k: null }
|
|
25
25
|
]);
|
|
26
|
+
assert.equal(result.endpoint, null);
|
|
26
27
|
assert.equal(result.reason, 'missing-k');
|
|
27
28
|
});
|
|
29
|
+
|
|
30
|
+
test('supports custom allowedProtocols and validates https k', () => {
|
|
31
|
+
const endpoints = [
|
|
32
|
+
{ protocol: 'https', url: 'https://secure', k: 'key', priority: 1 },
|
|
33
|
+
{ protocol: 'http', url: 'http://insecure', priority: 2 }
|
|
34
|
+
];
|
|
35
|
+
const result = choosePreferredEndpoint(endpoints, {
|
|
36
|
+
allowedProtocols: ['https', 'http'],
|
|
37
|
+
expectedK: 'key'
|
|
38
|
+
});
|
|
39
|
+
assert.equal(result.endpoint.url, 'https://secure');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('rejects https with mismatched k', () => {
|
|
43
|
+
const endpoints = [
|
|
44
|
+
{ protocol: 'https', url: 'https://secure', k: 'wrong', priority: 1 }
|
|
45
|
+
];
|
|
46
|
+
const result = choosePreferredEndpoint(endpoints, {
|
|
47
|
+
allowedProtocols: ['https'],
|
|
48
|
+
expectedK: 'key'
|
|
49
|
+
});
|
|
50
|
+
assert.equal(result.endpoint, null);
|
|
51
|
+
assert.equal(result.reason, 'k-mismatch');
|
|
52
|
+
});
|
|
@@ -8,22 +8,26 @@ import {
|
|
|
8
8
|
} from '../src/sidecar-config.js';
|
|
9
9
|
|
|
10
10
|
test('buildSidecarConfig constructs expected fields based on inputs', () => {
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
relayUrl: 'ws://localhost:7000',
|
|
16
|
-
k: { mode: 'static', value: 'TESTKEY:abc' },
|
|
17
|
-
publicationRelays: ['ws://aux:7001'],
|
|
18
|
-
externalEndpoints: {
|
|
19
|
-
ipv4: { enabled: true, protocol: 'ws', address: '127.0.0.1', port: 7447 }
|
|
20
|
-
}
|
|
11
|
+
const config = buildSidecarConfig({
|
|
12
|
+
secretKey: 'hex',
|
|
13
|
+
relayUrl: 'wss://test',
|
|
14
|
+
relayMode: 'private'
|
|
21
15
|
});
|
|
16
|
+
assert.equal(config.relayUrl, 'wss://test');
|
|
17
|
+
assert.equal(config.serviceUrl, 'wss://test');
|
|
18
|
+
assert.equal(config.relayMode, 'private');
|
|
19
|
+
});
|
|
22
20
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
21
|
+
test('buildSidecarConfig supports serviceUrl and serviceMode aliases', () => {
|
|
22
|
+
const config = buildSidecarConfig({
|
|
23
|
+
secretKey: 'hex',
|
|
24
|
+
serviceUrl: 'https://api.example.com',
|
|
25
|
+
serviceMode: 'private'
|
|
26
|
+
});
|
|
27
|
+
assert.equal(config.serviceUrl, 'https://api.example.com');
|
|
28
|
+
assert.equal(config.relayUrl, 'https://api.example.com');
|
|
29
|
+
assert.equal(config.serviceMode, 'private');
|
|
30
|
+
assert.equal(config.relayMode, 'private');
|
|
27
31
|
});
|
|
28
32
|
|
|
29
33
|
test('buildClientConfig enforces identity URI and relays', () => {
|
|
@@ -31,7 +35,7 @@ test('buildClientConfig enforces identity URI and relays', () => {
|
|
|
31
35
|
relayUrl: 'ws://relay',
|
|
32
36
|
servicePubkey: 'pk',
|
|
33
37
|
serviceNpub: 'npub',
|
|
34
|
-
|
|
38
|
+
ncc02ExpectedKey: 'TESTKEY:abc',
|
|
35
39
|
publicationRelays: ['ws://relay', 'ws://aux']
|
|
36
40
|
});
|
|
37
41
|
|