ncc-02-js 0.2.4 → 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 +11 -8
- package/dist/index.cjs +18 -13
- package/dist/index.mjs +18 -13
- package/dist/models.d.ts +4 -4
- package/dist/resolver.d.ts +8 -5
- package/package.json +1 -1
- package/src/models.js +10 -10
- package/src/resolver.js +23 -15
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.
|
|
@@ -26,7 +26,7 @@ npm install ncc-02-js
|
|
|
26
26
|
import { NCC02Resolver } from 'ncc-02-js';
|
|
27
27
|
|
|
28
28
|
// Initialize with relay URLs and optional trusted CA pubkeys
|
|
29
|
-
const resolver = new NCC02Resolver(['wss://
|
|
29
|
+
const resolver = new NCC02Resolver(['wss://192.0.2.1:443'], {
|
|
30
30
|
trustedCAPubkeys: ['npub1...'] // Trusted third-party certifiers
|
|
31
31
|
});
|
|
32
32
|
|
|
@@ -36,7 +36,11 @@ try {
|
|
|
36
36
|
requireAttestation: true,
|
|
37
37
|
minLevel: 'verified' // 'self', 'verified', 'hardened'
|
|
38
38
|
});
|
|
39
|
-
|
|
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:
|
|
62
|
-
const
|
|
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
|
|
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
|
};
|
|
@@ -7679,7 +7678,10 @@ var NCC02Resolver = class {
|
|
|
7679
7678
|
* @param {string[]} relays - List of relay URLs.
|
|
7680
7679
|
* @param {Object} [options={}]
|
|
7681
7680
|
* @param {SimplePool} [options.pool] - Shared SimplePool instance.
|
|
7682
|
-
* @param {string[]} [options.trustedCAPubkeys=[]] - List of trusted CA pubkeys.
|
|
7681
|
+
* @param {string[]} [options.trustedCAPubkeys=[]] - List of trusted CA pubkeys (hex or npub).
|
|
7682
|
+
* These are the ONLY pubkeys whose attestation signatures (Kind 30060) will be accepted
|
|
7683
|
+
* by the resolver. This allows you to define your own web of trust or rely on specific
|
|
7684
|
+
* community auditors. If empty, all attestations are ignored (effectively disabling attestation checks).
|
|
7683
7685
|
*/
|
|
7684
7686
|
constructor(relays, options = {}) {
|
|
7685
7687
|
if (!Array.isArray(relays)) {
|
|
@@ -7749,8 +7751,11 @@ var NCC02Resolver = class {
|
|
|
7749
7751
|
}
|
|
7750
7752
|
const serviceTags = Object.fromEntries(serviceEvent.tags);
|
|
7751
7753
|
const now2 = Math.floor(Date.now() / 1e3);
|
|
7752
|
-
if (!serviceTags.
|
|
7753
|
-
throw new NCC02Error("MALFORMED_RECORD", "Service record is missing required
|
|
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");
|
|
7754
7759
|
}
|
|
7755
7760
|
const exp = parseInt(serviceTags.exp);
|
|
7756
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
|
};
|
|
@@ -7645,7 +7644,10 @@ var NCC02Resolver = class {
|
|
|
7645
7644
|
* @param {string[]} relays - List of relay URLs.
|
|
7646
7645
|
* @param {Object} [options={}]
|
|
7647
7646
|
* @param {SimplePool} [options.pool] - Shared SimplePool instance.
|
|
7648
|
-
* @param {string[]} [options.trustedCAPubkeys=[]] - List of trusted CA pubkeys.
|
|
7647
|
+
* @param {string[]} [options.trustedCAPubkeys=[]] - List of trusted CA pubkeys (hex or npub).
|
|
7648
|
+
* These are the ONLY pubkeys whose attestation signatures (Kind 30060) will be accepted
|
|
7649
|
+
* by the resolver. This allows you to define your own web of trust or rely on specific
|
|
7650
|
+
* community auditors. If empty, all attestations are ignored (effectively disabling attestation checks).
|
|
7649
7651
|
*/
|
|
7650
7652
|
constructor(relays, options = {}) {
|
|
7651
7653
|
if (!Array.isArray(relays)) {
|
|
@@ -7715,8 +7717,11 @@ var NCC02Resolver = class {
|
|
|
7715
7717
|
}
|
|
7716
7718
|
const serviceTags = Object.fromEntries(serviceEvent.tags);
|
|
7717
7719
|
const now2 = Math.floor(Date.now() / 1e3);
|
|
7718
|
-
if (!serviceTags.
|
|
7719
|
-
throw new NCC02Error("MALFORMED_RECORD", "Service record is missing required
|
|
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");
|
|
7720
7725
|
}
|
|
7721
7726
|
const exp = parseInt(serviceTags.exp);
|
|
7722
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
|
|
32
|
-
fingerprint
|
|
31
|
+
endpoint?: string;
|
|
32
|
+
fingerprint?: string;
|
|
33
33
|
expiryDays?: number;
|
|
34
34
|
}): import("nostr-tools/core").VerifiedEvent;
|
|
35
35
|
/**
|
package/dist/resolver.d.ts
CHANGED
|
@@ -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
|
|
@@ -29,7 +29,10 @@ export class NCC02Resolver {
|
|
|
29
29
|
* @param {string[]} relays - List of relay URLs.
|
|
30
30
|
* @param {Object} [options={}]
|
|
31
31
|
* @param {SimplePool} [options.pool] - Shared SimplePool instance.
|
|
32
|
-
* @param {string[]} [options.trustedCAPubkeys=[]] - List of trusted CA pubkeys.
|
|
32
|
+
* @param {string[]} [options.trustedCAPubkeys=[]] - List of trusted CA pubkeys (hex or npub).
|
|
33
|
+
* These are the ONLY pubkeys whose attestation signatures (Kind 30060) will be accepted
|
|
34
|
+
* by the resolver. This allows you to define your own web of trust or rely on specific
|
|
35
|
+
* community auditors. If empty, all attestations are ignored (effectively disabling attestation checks).
|
|
33
36
|
*/
|
|
34
37
|
constructor(relays: string[], options?: {
|
|
35
38
|
pool?: SimplePool;
|
|
@@ -84,8 +87,8 @@ export class NCC02Resolver {
|
|
|
84
87
|
verifyEndpoint(resolved: ResolvedService, actualFingerprint: string): boolean;
|
|
85
88
|
}
|
|
86
89
|
export type ResolvedService = {
|
|
87
|
-
endpoint: string;
|
|
88
|
-
fingerprint: string;
|
|
90
|
+
endpoint: string | undefined;
|
|
91
|
+
fingerprint: string | undefined;
|
|
89
92
|
expiry: number;
|
|
90
93
|
attestations: any[];
|
|
91
94
|
eventId: string;
|
package/package.json
CHANGED
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
|
|
@@ -36,11 +36,14 @@ export class NCC02Resolver {
|
|
|
36
36
|
* @param {string[]} relays - List of relay URLs.
|
|
37
37
|
* @param {Object} [options={}]
|
|
38
38
|
* @param {SimplePool} [options.pool] - Shared SimplePool instance.
|
|
39
|
-
* @param {string[]} [options.trustedCAPubkeys=[]] - List of trusted CA pubkeys.
|
|
39
|
+
* @param {string[]} [options.trustedCAPubkeys=[]] - List of trusted CA pubkeys (hex or npub).
|
|
40
|
+
* These are the ONLY pubkeys whose attestation signatures (Kind 30060) will be accepted
|
|
41
|
+
* by the resolver. This allows you to define your own web of trust or rely on specific
|
|
42
|
+
* community auditors. If empty, all attestations are ignored (effectively disabling attestation checks).
|
|
40
43
|
*/
|
|
41
44
|
constructor(relays, options = {}) {
|
|
42
45
|
if (!Array.isArray(relays)) {
|
|
43
|
-
|
|
46
|
+
throw new Error('NCC02Resolver expects an array of relay URLs.');
|
|
44
47
|
}
|
|
45
48
|
this.relays = relays;
|
|
46
49
|
this.pool = options.pool || new SimplePool();
|
|
@@ -54,16 +57,16 @@ export class NCC02Resolver {
|
|
|
54
57
|
* @returns {Promise<import('nostr-tools').Event[]>}
|
|
55
58
|
*/
|
|
56
59
|
async _query(filter) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
});
|
|
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); }
|
|
66
68
|
});
|
|
69
|
+
});
|
|
67
70
|
}
|
|
68
71
|
|
|
69
72
|
/**
|
|
@@ -114,8 +117,13 @@ export class NCC02Resolver {
|
|
|
114
117
|
const now = Math.floor(Date.now() / 1000);
|
|
115
118
|
|
|
116
119
|
// Security Fix: exp is REQUIRED by NCC-02 spec
|
|
117
|
-
if (!serviceTags.
|
|
118
|
-
throw new NCC02Error('MALFORMED_RECORD', 'Service record is missing required
|
|
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');
|
|
119
127
|
}
|
|
120
128
|
|
|
121
129
|
const exp = parseInt(serviceTags.exp);
|