ncc-02-js 0.3.1 → 0.4.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 +89 -21
- package/dist/index.cjs +217 -85
- package/dist/index.mjs +216 -85
- package/dist/models.d.ts +29 -7
- package/dist/resolver.d.ts +74 -14
- package/package.json +1 -1
- package/src/models.js +105 -21
- package/src/resolver.js +153 -88
package/README.md
CHANGED
|
@@ -48,39 +48,107 @@ try {
|
|
|
48
48
|
}
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
-
### 2. Publish
|
|
52
|
-
...
|
|
53
|
-
### Trust Model & Security
|
|
51
|
+
### 2. Publish Service Records and Attestations
|
|
54
52
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
53
|
+
```javascript
|
|
54
|
+
import { NCC02Builder } from 'ncc-02-js';
|
|
55
|
+
|
|
56
|
+
const builder = new NCC02Builder(privateKey);
|
|
57
|
+
|
|
58
|
+
const serviceRecord = await builder.createServiceRecord({
|
|
59
|
+
serviceId: 'api',
|
|
60
|
+
endpoint: 'https://service.example.com',
|
|
61
|
+
fingerprint: '<spki fingerprint>',
|
|
62
|
+
expiryDays: 7
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const attestation = await builder.createAttestation({
|
|
66
|
+
subjectPubkey: ownerPubkey,
|
|
67
|
+
serviceId: 'api',
|
|
68
|
+
serviceEventId: serviceRecord.id,
|
|
69
|
+
level: 'verified',
|
|
70
|
+
validDays: 30
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
await builder.createRevocation({
|
|
74
|
+
attestationId: attestation.id,
|
|
75
|
+
reason: 'Key rotation'
|
|
76
|
+
});
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The builder helpers emit the expected NCC-02 event kinds: Kind 30059 for service records, 30060 for attestations, and 30061 for revocations. `createServiceRecord` always includes the `d` and `exp` tags while optionally populating `u` and `k`. Attestations include `subj`, `srv`, `e`, `std`, `lvl`, `nbf`, and `exp`. Revocations only need the `e` tag and an optional reason. You can supply either a raw private key (hex string or `Uint8Array`) or a signer implementing `getPublicKey`/`signEvent`.
|
|
80
|
+
|
|
81
|
+
### 3. Trust Model & Security
|
|
82
|
+
|
|
83
|
+
The resolver treats the service owner’s pubkey as the root authority. Certificates and revocations are additive layers that you opt into via `requireAttestation` or `minLevel`. Validation failures surface as `NCC02Error` instances with codes such as `INVALID_SIGNATURE`, `EXPIRED`, or `POLICY_FAILURE`, helping clients react instead of defaulting to insecure fallbacks.
|
|
84
|
+
|
|
85
|
+
### 4. Resolution Optimization
|
|
86
|
+
|
|
87
|
+
To avoid unnecessary relay traffic, attestation and revocation feeds are only fetched when the resolver’s policy requests them. Keep expiries short and rotate fingerprints with the `k` tag to reduce the blast radius of compromised keys.
|
|
88
|
+
|
|
89
|
+
### 5. Threat Model
|
|
59
90
|
|
|
60
|
-
|
|
61
|
-
The resolver is designed to be network-efficient. It will only query for attestations (Kind 30060) and revocations (Kind 30061) if the provided policy requires them (e.g., when `requireAttestation` is set to `true` or a `minLevel` higher than `self` is requested).
|
|
91
|
+
The library defends against endpoint impersonation, MITM, stale records, and relay censorship by enforcing signed records, short expiries, revocation checks, and optional third-party attestations. It deliberately keeps scope focused on application-layer trust and does not replace TLS or browser PKI.
|
|
62
92
|
|
|
63
|
-
###
|
|
64
|
-
|
|
65
|
-
|
|
93
|
+
### 6. Testing with MockRelay
|
|
94
|
+
|
|
95
|
+
```javascript
|
|
96
|
+
import { MockRelay, NCC02Builder, NCC02Resolver } from 'ncc-02-js';
|
|
97
|
+
|
|
98
|
+
const mock = new MockRelay();
|
|
99
|
+
const builder = new NCC02Builder(privateKey);
|
|
100
|
+
const serviceRecord = await builder.createServiceRecord({ serviceId: 'api', endpoint: 'https://example', fingerprint: '<fp>' });
|
|
101
|
+
await mock.publish(serviceRecord);
|
|
102
|
+
|
|
103
|
+
const resolver = new NCC02Resolver(['mock://local'], { pool: mock });
|
|
104
|
+
await resolver.resolve(ownerPubkey, 'api');
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`MockRelay` mimics a Nostr relay by verifying signatures and honoring `kinds`, `authors`, `ids`, and `#tag` filters. It makes writing unit tests and integration suites deterministic without external dependencies.
|
|
108
|
+
|
|
109
|
+
### 7. Endpoint Verification Helpers
|
|
110
|
+
|
|
111
|
+
After resolving a service, call `resolver.verifyEndpoint(serviceStatus, actualFingerprint)` to ensure the runtime transport fingerprint matches the declared `k` tag. Use `verifyNCC02Event` when you ingest raw events and `isExpired` to quickly skip expired records before attempting network validation.
|
|
112
|
+
|
|
113
|
+
## API Reference
|
|
66
114
|
|
|
67
115
|
### `NCC02Resolver(relays, options)`
|
|
68
116
|
- `relays`: Array of relay URLs.
|
|
69
|
-
- `options.pool`: (Optional)
|
|
70
|
-
- `options.trustedCAPubkeys`:
|
|
117
|
+
- `options.pool`: (Optional) Shared `nostr-tools` `SimplePool`.
|
|
118
|
+
- `options.trustedCAPubkeys`: Optional array of certifier pubkeys that may issue trusted attestations.
|
|
71
119
|
|
|
72
120
|
#### `resolve(pubkey, serviceId, options)`
|
|
73
|
-
- `options.requireAttestation`:
|
|
74
|
-
- `options.minLevel`: Minimum
|
|
121
|
+
- `options.requireAttestation`: Reject if no trusted attestation is available.
|
|
122
|
+
- `options.minLevel`: Minimum `lvl` tag level (`self`, `verified`, `hardened`).
|
|
123
|
+
- `options.standard`: Expected `std` tag value (defaults to `nostr-service-trust-v0.1`).
|
|
124
|
+
- Returns `ServiceStatus` including `endpoint`, `fingerprint`, `expiry`, `attestations`, `attestationCount`, `isRevoked`, `eventId`, `pubkey`, and the raw `serviceEvent`.
|
|
125
|
+
|
|
126
|
+
#### `verifyEndpoint(resolved, actualFingerprint)`
|
|
127
|
+
- Compares the resolved fingerprint with the observed transport fingerprint for an additional rejection guard.
|
|
75
128
|
|
|
76
129
|
#### `close()`
|
|
77
|
-
Closes
|
|
130
|
+
- Closes relay subscriptions when the resolver owns the `SimplePool`.
|
|
131
|
+
|
|
132
|
+
### `NCC02Builder(signer)`
|
|
133
|
+
- `signer`: Hex private key, `Uint8Array`, or custom signer with `getPublicKey`/`signEvent`.
|
|
134
|
+
|
|
135
|
+
#### `createServiceRecord({ serviceId, endpoint?, fingerprint?, expiryDays? })`
|
|
136
|
+
- Returns a signed Kind 30059 event. `endpoint` maps to `u`, `fingerprint` to `k`, and `expiryDays` controls `exp`.
|
|
137
|
+
|
|
138
|
+
#### `createAttestation({ subjectPubkey, serviceId, serviceEventId, level?, validDays? })`
|
|
139
|
+
- Emits Kind 30060 with `subj`, `srv`, `e`, `std`, `lvl`, `nbf`, and `exp`.
|
|
140
|
+
|
|
141
|
+
#### `createRevocation({ attestationId, reason? })`
|
|
142
|
+
- Emits Kind 30061 pointing to the revoked attestation.
|
|
78
143
|
|
|
144
|
+
### `MockRelay`
|
|
145
|
+
- `publish(event)`: Verifies and stores the event (returns `true` if accepted).
|
|
146
|
+
- `query(filter)`: Filters stored events by `kinds`, `authors`, `ids`, and `#tag` criteria.
|
|
79
147
|
|
|
80
|
-
###
|
|
81
|
-
- `
|
|
82
|
-
- `
|
|
83
|
-
- `
|
|
148
|
+
### Helpers
|
|
149
|
+
- `verifyNCC02Event(event)`: Signature verification wrapper.
|
|
150
|
+
- `isExpired(event)`: Returns `true` when the `exp` tag is in the past.
|
|
151
|
+
- `KINDS`: Enum with `SERVICE_RECORD`, `ATTESTATION`, and `REVOCATION`.
|
|
84
152
|
|
|
85
153
|
## License
|
|
86
154
|
|
package/dist/index.cjs
CHANGED
|
@@ -34,6 +34,7 @@ __export(index_exports, {
|
|
|
34
34
|
NCC02Builder: () => NCC02Builder,
|
|
35
35
|
NCC02Error: () => NCC02Error,
|
|
36
36
|
NCC02Resolver: () => NCC02Resolver,
|
|
37
|
+
isExpired: () => isExpired,
|
|
37
38
|
verifyNCC02Event: () => verifyNCC02Event
|
|
38
39
|
});
|
|
39
40
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -2498,12 +2499,73 @@ var KINDS = {
|
|
|
2498
2499
|
};
|
|
2499
2500
|
var NCC02Builder = class {
|
|
2500
2501
|
/**
|
|
2501
|
-
* @param {string | Uint8Array}
|
|
2502
|
+
* @param {string | Uint8Array | NostrSigner} signer - Raw private key or asynchronous signer.
|
|
2502
2503
|
*/
|
|
2503
|
-
constructor(
|
|
2504
|
-
if (!
|
|
2505
|
-
this.
|
|
2506
|
-
this.
|
|
2504
|
+
constructor(signer) {
|
|
2505
|
+
if (!signer) throw new Error("Signer or private key is required");
|
|
2506
|
+
this.signer = this._normalizeSigner(signer);
|
|
2507
|
+
this._pubkeyPromise = this.signer.getPublicKey();
|
|
2508
|
+
this._pubkey = void 0;
|
|
2509
|
+
}
|
|
2510
|
+
async _getPublicKey() {
|
|
2511
|
+
if (!this._pubkey) {
|
|
2512
|
+
this._pubkey = await this._pubkeyPromise;
|
|
2513
|
+
}
|
|
2514
|
+
return this._pubkey;
|
|
2515
|
+
}
|
|
2516
|
+
/**
|
|
2517
|
+
* @param {any} event
|
|
2518
|
+
*/
|
|
2519
|
+
async _finalizeEvent(event) {
|
|
2520
|
+
const pubkey = await this._getPublicKey();
|
|
2521
|
+
const eventWithPubkey = { ...event, pubkey };
|
|
2522
|
+
const signed = await this.signer.signEvent(eventWithPubkey);
|
|
2523
|
+
if (!signed || typeof signed.id !== "string" || typeof signed.sig !== "string") {
|
|
2524
|
+
throw new Error("Signer must return a signed event with id and sig");
|
|
2525
|
+
}
|
|
2526
|
+
return signed;
|
|
2527
|
+
}
|
|
2528
|
+
/**
|
|
2529
|
+
* @param {any} signer
|
|
2530
|
+
* @returns {NostrSigner}
|
|
2531
|
+
*/
|
|
2532
|
+
_normalizeSigner(signer) {
|
|
2533
|
+
if (typeof signer === "string" || signer instanceof Uint8Array) {
|
|
2534
|
+
const privateKey = typeof signer === "string" ? hexToBytes2(signer) : signer;
|
|
2535
|
+
const pubkey = getPublicKey(privateKey);
|
|
2536
|
+
return {
|
|
2537
|
+
getPublicKey: async () => pubkey,
|
|
2538
|
+
/** @param {any} event */
|
|
2539
|
+
signEvent: async (event) => {
|
|
2540
|
+
const clonedEvent = {
|
|
2541
|
+
...event,
|
|
2542
|
+
tags: Array.isArray(event.tags) ? event.tags.map((tag) => [...tag]) : []
|
|
2543
|
+
};
|
|
2544
|
+
return finalizeEvent(clonedEvent, privateKey);
|
|
2545
|
+
}
|
|
2546
|
+
};
|
|
2547
|
+
}
|
|
2548
|
+
if (typeof signer === "object" && signer !== null) {
|
|
2549
|
+
if (typeof signer.getPublicKey === "function" && typeof signer.signEvent === "function") {
|
|
2550
|
+
return {
|
|
2551
|
+
getPublicKey: async () => {
|
|
2552
|
+
const pubkey = await signer.getPublicKey();
|
|
2553
|
+
if (typeof pubkey !== "string") throw new Error("Signer.getPublicKey must return a hex string");
|
|
2554
|
+
return pubkey;
|
|
2555
|
+
},
|
|
2556
|
+
/** @param {any} event */
|
|
2557
|
+
signEvent: async (event) => {
|
|
2558
|
+
const signed = await signer.signEvent(event);
|
|
2559
|
+
if (!signed || typeof signed.id !== "string" || typeof signed.sig !== "string") {
|
|
2560
|
+
throw new Error("Signer.signEvent must return a signed event");
|
|
2561
|
+
}
|
|
2562
|
+
return signed;
|
|
2563
|
+
},
|
|
2564
|
+
decryptEvent: typeof signer.decryptEvent === "function" ? signer.decryptEvent.bind(signer) : void 0
|
|
2565
|
+
};
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
throw new Error("Unsupported signer provided to NCC02Builder");
|
|
2507
2569
|
}
|
|
2508
2570
|
/**
|
|
2509
2571
|
* Creates a signed Service Record (Kind 30059).
|
|
@@ -2513,7 +2575,7 @@ var NCC02Builder = class {
|
|
|
2513
2575
|
* @param {string} [options.fingerprint] - The 'k' tag fingerprint.
|
|
2514
2576
|
* @param {number} [options.expiryDays=14] - Expiry in days.
|
|
2515
2577
|
*/
|
|
2516
|
-
createServiceRecord(options) {
|
|
2578
|
+
async createServiceRecord(options) {
|
|
2517
2579
|
const { serviceId, endpoint, fingerprint, expiryDays = 14 } = options;
|
|
2518
2580
|
if (!serviceId) throw new Error("serviceId (d tag) is required");
|
|
2519
2581
|
const expiry = Math.floor(Date.now() / 1e3) + expiryDays * 24 * 60 * 60;
|
|
@@ -2527,10 +2589,9 @@ var NCC02Builder = class {
|
|
|
2527
2589
|
kind: KINDS.SERVICE_RECORD,
|
|
2528
2590
|
created_at: Math.floor(Date.now() / 1e3),
|
|
2529
2591
|
tags,
|
|
2530
|
-
content: `NCC-02 Service Record for ${serviceId}
|
|
2531
|
-
pubkey: this.pk
|
|
2592
|
+
content: `NCC-02 Service Record for ${serviceId}`
|
|
2532
2593
|
};
|
|
2533
|
-
return
|
|
2594
|
+
return this._finalizeEvent(event);
|
|
2534
2595
|
}
|
|
2535
2596
|
/**
|
|
2536
2597
|
* Creates a signed Certificate Attestation (Kind 30060).
|
|
@@ -2541,7 +2602,7 @@ var NCC02Builder = class {
|
|
|
2541
2602
|
* @param {string} [options.level='verified'] - The 'lvl' tag level.
|
|
2542
2603
|
* @param {number} [options.validDays=30] - Validity in days.
|
|
2543
2604
|
*/
|
|
2544
|
-
createAttestation(options) {
|
|
2605
|
+
async createAttestation(options) {
|
|
2545
2606
|
const { subjectPubkey, serviceId, serviceEventId, level = "verified", validDays = 30 } = options;
|
|
2546
2607
|
if (!subjectPubkey) throw new Error("subjectPubkey is required");
|
|
2547
2608
|
if (!serviceId) throw new Error("serviceId is required");
|
|
@@ -2560,10 +2621,9 @@ var NCC02Builder = class {
|
|
|
2560
2621
|
["nbf", now2.toString()],
|
|
2561
2622
|
["exp", expiry.toString()]
|
|
2562
2623
|
],
|
|
2563
|
-
content: "NCC-02 Attestation"
|
|
2564
|
-
pubkey: this.pk
|
|
2624
|
+
content: "NCC-02 Attestation"
|
|
2565
2625
|
};
|
|
2566
|
-
return
|
|
2626
|
+
return this._finalizeEvent(event);
|
|
2567
2627
|
}
|
|
2568
2628
|
/**
|
|
2569
2629
|
* Creates a signed Revocation (Kind 30061).
|
|
@@ -2571,7 +2631,7 @@ var NCC02Builder = class {
|
|
|
2571
2631
|
* @param {string} options.attestationId - The 'e' tag referencing the attestation.
|
|
2572
2632
|
* @param {string} [options.reason=''] - Optional reason.
|
|
2573
2633
|
*/
|
|
2574
|
-
createRevocation(options) {
|
|
2634
|
+
async createRevocation(options) {
|
|
2575
2635
|
const { attestationId, reason = "" } = options;
|
|
2576
2636
|
if (!attestationId) throw new Error("attestationId (e tag) is required");
|
|
2577
2637
|
const tags = [["e", attestationId]];
|
|
@@ -2580,15 +2640,22 @@ var NCC02Builder = class {
|
|
|
2580
2640
|
kind: KINDS.REVOCATION,
|
|
2581
2641
|
created_at: Math.floor(Date.now() / 1e3),
|
|
2582
2642
|
tags,
|
|
2583
|
-
content: "NCC-02 Revocation"
|
|
2584
|
-
pubkey: this.pk
|
|
2643
|
+
content: "NCC-02 Revocation"
|
|
2585
2644
|
};
|
|
2586
|
-
return
|
|
2645
|
+
return this._finalizeEvent(event);
|
|
2587
2646
|
}
|
|
2588
2647
|
};
|
|
2589
2648
|
function verifyNCC02Event(event) {
|
|
2590
2649
|
return verifyEvent(event);
|
|
2591
2650
|
}
|
|
2651
|
+
function isExpired(event) {
|
|
2652
|
+
if (!event || !Array.isArray(event.tags)) return false;
|
|
2653
|
+
const expTag = event.tags.find((tag) => tag[0] === "exp");
|
|
2654
|
+
if (!expTag) return false;
|
|
2655
|
+
const expiry = parseInt(expTag[1], 10);
|
|
2656
|
+
if (Number.isNaN(expiry)) return false;
|
|
2657
|
+
return expiry <= Math.floor(Date.now() / 1e3);
|
|
2658
|
+
}
|
|
2592
2659
|
|
|
2593
2660
|
// node_modules/@scure/base/lib/esm/index.js
|
|
2594
2661
|
function assertNumber(n) {
|
|
@@ -7719,6 +7786,27 @@ var NCC02Resolver = class {
|
|
|
7719
7786
|
});
|
|
7720
7787
|
});
|
|
7721
7788
|
}
|
|
7789
|
+
/**
|
|
7790
|
+
* Returns the first event sorted by freshness (newest created_at, tie broken by id).
|
|
7791
|
+
* @param {import('nostr-tools').Event[]} events
|
|
7792
|
+
* @returns {import('nostr-tools').Event|null}
|
|
7793
|
+
*/
|
|
7794
|
+
_freshestEvent(events) {
|
|
7795
|
+
if (!events || !events.length) return null;
|
|
7796
|
+
return events.sort((a, b) => {
|
|
7797
|
+
if (b.created_at !== a.created_at) return b.created_at - a.created_at;
|
|
7798
|
+
return a.id.localeCompare(b.id);
|
|
7799
|
+
})[0];
|
|
7800
|
+
}
|
|
7801
|
+
/**
|
|
7802
|
+
* Query helper that returns only the freshest event matching the filter.
|
|
7803
|
+
* @param {import('nostr-tools').Filter} filter
|
|
7804
|
+
* @returns {Promise<import('nostr-tools').Event | null>}
|
|
7805
|
+
*/
|
|
7806
|
+
async _queryFreshest(filter) {
|
|
7807
|
+
const events = await this._query(filter);
|
|
7808
|
+
return this._freshestEvent(events);
|
|
7809
|
+
}
|
|
7722
7810
|
/**
|
|
7723
7811
|
* Resolves a service for a given pubkey and service identifier.
|
|
7724
7812
|
*
|
|
@@ -7728,8 +7816,8 @@ var NCC02Resolver = class {
|
|
|
7728
7816
|
* @param {boolean} [options.requireAttestation=false] - If true, fails if no trusted attestation is found.
|
|
7729
7817
|
* @param {string} [options.minLevel=null] - Minimum trust level ('self', 'verified', 'hardened').
|
|
7730
7818
|
* @param {string} [options.standard='nostr-service-trust-v0.1'] - Expected trust standard.
|
|
7731
|
-
|
|
7732
|
-
|
|
7819
|
+
* @throws {NCC02Error} If verification or policy checks fail.
|
|
7820
|
+
* @returns {Promise<ServiceStatus>} The service status including trust metadata.
|
|
7733
7821
|
*/
|
|
7734
7822
|
async resolve(pubkey, serviceId, options = {}) {
|
|
7735
7823
|
const {
|
|
@@ -7737,9 +7825,9 @@ var NCC02Resolver = class {
|
|
|
7737
7825
|
minLevel = null,
|
|
7738
7826
|
standard = "nostr-service-trust-v0.1"
|
|
7739
7827
|
} = options;
|
|
7740
|
-
let
|
|
7828
|
+
let serviceEvent;
|
|
7741
7829
|
try {
|
|
7742
|
-
|
|
7830
|
+
serviceEvent = await this._queryFreshest({
|
|
7743
7831
|
kinds: [KINDS.SERVICE_RECORD],
|
|
7744
7832
|
authors: [pubkey],
|
|
7745
7833
|
"#d": [serviceId]
|
|
@@ -7747,13 +7835,9 @@ var NCC02Resolver = class {
|
|
|
7747
7835
|
} catch (err) {
|
|
7748
7836
|
throw new NCC02Error("RELAY_ERROR", `Failed to query relay for ${serviceId}`, err);
|
|
7749
7837
|
}
|
|
7750
|
-
if (!
|
|
7838
|
+
if (!serviceEvent) {
|
|
7751
7839
|
throw new NCC02Error("NOT_FOUND", `No service record found for ${serviceId}`);
|
|
7752
7840
|
}
|
|
7753
|
-
const serviceEvent = serviceEvents.sort((a, b) => {
|
|
7754
|
-
if (b.created_at !== a.created_at) return b.created_at - a.created_at;
|
|
7755
|
-
return a.id.localeCompare(b.id);
|
|
7756
|
-
})[0];
|
|
7757
7841
|
if (!verifyEvent2(serviceEvent)) {
|
|
7758
7842
|
throw new NCC02Error("INVALID_SIGNATURE", "Service record signature verification failed");
|
|
7759
7843
|
}
|
|
@@ -7772,88 +7856,135 @@ var NCC02Resolver = class {
|
|
|
7772
7856
|
if (exp < now2) {
|
|
7773
7857
|
throw new NCC02Error("EXPIRED", "Service record has expired");
|
|
7774
7858
|
}
|
|
7775
|
-
|
|
7776
|
-
|
|
7777
|
-
|
|
7778
|
-
|
|
7779
|
-
|
|
7780
|
-
|
|
7781
|
-
|
|
7782
|
-
|
|
7783
|
-
]);
|
|
7784
|
-
} catch (err) {
|
|
7785
|
-
throw new NCC02Error("RELAY_ERROR", "Failed to query relay for attestations/revocations", err);
|
|
7786
|
-
}
|
|
7787
|
-
for (const att of attestations) {
|
|
7788
|
-
if (this.trustedCAPubkeys.has(att.pubkey)) {
|
|
7789
|
-
const attTags = Object.fromEntries(att.tags);
|
|
7790
|
-
if (attTags.subj !== pubkey) continue;
|
|
7791
|
-
if (attTags.srv !== serviceId) continue;
|
|
7792
|
-
if (standard && attTags.std !== standard) continue;
|
|
7793
|
-
if (minLevel && !this._isLevelSufficient(attTags.lvl, minLevel)) continue;
|
|
7794
|
-
if (this._isAttestationValid(att, attTags, revocations)) {
|
|
7795
|
-
validAttestations.push({
|
|
7796
|
-
pubkey: att.pubkey,
|
|
7797
|
-
level: attTags.lvl,
|
|
7798
|
-
eventId: att.id
|
|
7799
|
-
});
|
|
7800
|
-
}
|
|
7801
|
-
}
|
|
7802
|
-
}
|
|
7803
|
-
if (requireAttestation && validAttestations.length === 0) {
|
|
7804
|
-
throw new NCC02Error("POLICY_FAILURE", `No trusted attestations meet the required policy for ${serviceId}`);
|
|
7805
|
-
}
|
|
7859
|
+
let trustData;
|
|
7860
|
+
try {
|
|
7861
|
+
trustData = await this._buildTrustData(serviceEvent, { pubkey, serviceId, standard, minLevel });
|
|
7862
|
+
} catch (err) {
|
|
7863
|
+
throw new NCC02Error("RELAY_ERROR", "Failed to query relay for attestations/revocations", err);
|
|
7864
|
+
}
|
|
7865
|
+
if (requireAttestation && trustData.validAttestations.length === 0) {
|
|
7866
|
+
throw new NCC02Error("POLICY_FAILURE", `No trusted attestations meet the required policy for ${serviceId}`);
|
|
7806
7867
|
}
|
|
7807
7868
|
return {
|
|
7808
7869
|
endpoint: serviceTags.u,
|
|
7809
7870
|
fingerprint: serviceTags.k,
|
|
7810
7871
|
expiry: exp,
|
|
7811
|
-
attestations: validAttestations,
|
|
7872
|
+
attestations: trustData.validAttestations,
|
|
7873
|
+
attestationCount: trustData.validAttestations.length,
|
|
7874
|
+
isRevoked: trustData.isRevoked,
|
|
7812
7875
|
eventId: serviceEvent.id,
|
|
7813
|
-
pubkey: serviceEvent.pubkey
|
|
7876
|
+
pubkey: serviceEvent.pubkey,
|
|
7877
|
+
serviceEvent
|
|
7814
7878
|
};
|
|
7815
7879
|
}
|
|
7816
7880
|
/**
|
|
7817
|
-
* @param {
|
|
7818
|
-
* @param {
|
|
7881
|
+
* @param {any} serviceEvent
|
|
7882
|
+
* @param {Object} options
|
|
7883
|
+
* @param {string} options.pubkey
|
|
7884
|
+
* @param {string} options.serviceId
|
|
7885
|
+
* @param {string|null} options.standard
|
|
7886
|
+
* @param {string|null} options.minLevel
|
|
7819
7887
|
*/
|
|
7820
|
-
|
|
7821
|
-
const
|
|
7822
|
-
|
|
7823
|
-
|
|
7824
|
-
|
|
7888
|
+
async _buildTrustData(serviceEvent, options) {
|
|
7889
|
+
const attestations = await this._query({
|
|
7890
|
+
kinds: [KINDS.ATTESTATION],
|
|
7891
|
+
"#e": [serviceEvent.id]
|
|
7892
|
+
});
|
|
7893
|
+
const attestationIds = attestations.map((att) => att.id);
|
|
7894
|
+
let revocations = [];
|
|
7895
|
+
if (attestationIds.length) {
|
|
7896
|
+
revocations = await this._query({
|
|
7897
|
+
kinds: [KINDS.REVOCATION],
|
|
7898
|
+
"#e": attestationIds
|
|
7899
|
+
});
|
|
7900
|
+
}
|
|
7901
|
+
const revocationIndex = this._groupValidRevocations(revocations);
|
|
7902
|
+
const validAttestations = [];
|
|
7903
|
+
let isRevoked = false;
|
|
7904
|
+
for (const att of attestations) {
|
|
7905
|
+
if (!this.trustedCAPubkeys.has(att.pubkey)) continue;
|
|
7906
|
+
const attTags = Object.fromEntries(att.tags);
|
|
7907
|
+
if (attTags.subj !== options.pubkey) continue;
|
|
7908
|
+
if (attTags.srv !== options.serviceId) continue;
|
|
7909
|
+
if (options.standard && attTags.std !== options.standard) continue;
|
|
7910
|
+
const { valid, revoked } = this._evaluateAttestation(att, attTags, revocationIndex[att.id]);
|
|
7911
|
+
if (revoked) {
|
|
7912
|
+
isRevoked = true;
|
|
7913
|
+
continue;
|
|
7914
|
+
}
|
|
7915
|
+
if (!valid) continue;
|
|
7916
|
+
if (options.minLevel && !this._isLevelSufficient(attTags.lvl, options.minLevel)) continue;
|
|
7917
|
+
validAttestations.push({
|
|
7918
|
+
pubkey: att.pubkey,
|
|
7919
|
+
level: attTags.lvl,
|
|
7920
|
+
eventId: att.id
|
|
7921
|
+
});
|
|
7922
|
+
}
|
|
7923
|
+
return { validAttestations, isRevoked };
|
|
7825
7924
|
}
|
|
7826
7925
|
/**
|
|
7827
|
-
* @param {any}
|
|
7828
|
-
* @
|
|
7829
|
-
* @param {any[]} revocations
|
|
7926
|
+
* @param {any[]} revocations
|
|
7927
|
+
* @returns {Record<string, any[]>}
|
|
7830
7928
|
*/
|
|
7831
|
-
|
|
7832
|
-
|
|
7929
|
+
/**
|
|
7930
|
+
* @param {any[]} revocations
|
|
7931
|
+
* @returns {Record<string, any[]>}
|
|
7932
|
+
*/
|
|
7933
|
+
_groupValidRevocations(revocations) {
|
|
7934
|
+
const indexed = {};
|
|
7935
|
+
for (const rev of revocations) {
|
|
7936
|
+
if (!verifyEvent2(rev)) continue;
|
|
7937
|
+
const tags = Object.fromEntries(rev.tags);
|
|
7938
|
+
const targetId = tags.e;
|
|
7939
|
+
if (!targetId) continue;
|
|
7940
|
+
if (!indexed[targetId]) indexed[targetId] = [];
|
|
7941
|
+
indexed[targetId].push(rev);
|
|
7942
|
+
}
|
|
7943
|
+
return indexed;
|
|
7944
|
+
}
|
|
7945
|
+
/**
|
|
7946
|
+
* @param {any} att
|
|
7947
|
+
* @param {Record<string, string>} tags
|
|
7948
|
+
* @param {any[]} revocations
|
|
7949
|
+
*/
|
|
7950
|
+
/**
|
|
7951
|
+
* @param {any} att
|
|
7952
|
+
* @param {Record<string, string>} tags
|
|
7953
|
+
* @param {any[]} [revocations]
|
|
7954
|
+
*/
|
|
7955
|
+
_evaluateAttestation(att, tags, revocations = []) {
|
|
7956
|
+
for (const rev of revocations) {
|
|
7957
|
+
if (rev.pubkey === att.pubkey) {
|
|
7958
|
+
return { valid: false, revoked: true };
|
|
7959
|
+
}
|
|
7960
|
+
}
|
|
7961
|
+
if (!verifyEvent2(att)) return { valid: false, revoked: false };
|
|
7833
7962
|
const now2 = Math.floor(Date.now() / 1e3);
|
|
7834
7963
|
if (tags.nbf) {
|
|
7835
|
-
const nbf = parseInt(tags.nbf);
|
|
7836
|
-
if (isNaN(nbf) || nbf > now2) return false;
|
|
7964
|
+
const nbf = parseInt(tags.nbf, 10);
|
|
7965
|
+
if (isNaN(nbf) || nbf > now2) return { valid: false, revoked: false };
|
|
7837
7966
|
}
|
|
7838
7967
|
if (tags.exp) {
|
|
7839
|
-
const exp = parseInt(tags.exp);
|
|
7840
|
-
if (isNaN(exp) || exp < now2) return false;
|
|
7968
|
+
const exp = parseInt(tags.exp, 10);
|
|
7969
|
+
if (isNaN(exp) || exp < now2) return { valid: false, revoked: false };
|
|
7841
7970
|
}
|
|
7842
|
-
|
|
7843
|
-
|
|
7844
|
-
|
|
7845
|
-
|
|
7846
|
-
|
|
7847
|
-
|
|
7848
|
-
|
|
7849
|
-
}
|
|
7850
|
-
|
|
7971
|
+
return { valid: true, revoked: false };
|
|
7972
|
+
}
|
|
7973
|
+
/**
|
|
7974
|
+
* @param {string | undefined} actual
|
|
7975
|
+
* @param {string} required
|
|
7976
|
+
*/
|
|
7977
|
+
_isLevelSufficient(actual, required) {
|
|
7978
|
+
const levels = { "self": 0, "verified": 1, "hardened": 2 };
|
|
7979
|
+
const actualVal = actual ? levels[actual] ?? -1 : -1;
|
|
7980
|
+
const requiredVal = levels[required] ?? 0;
|
|
7981
|
+
return actualVal >= requiredVal;
|
|
7851
7982
|
}
|
|
7852
7983
|
/**
|
|
7853
7984
|
* Verifies that the actual fingerprint found during transport-level connection
|
|
7854
7985
|
* matches the one declared in the signed service record.
|
|
7855
7986
|
*
|
|
7856
|
-
* @param {
|
|
7987
|
+
* @param {ServiceStatus} resolved - The object returned by resolve().
|
|
7857
7988
|
* @param {string} actualFingerprint - The fingerprint obtained from the service.
|
|
7858
7989
|
* @returns {boolean}
|
|
7859
7990
|
*/
|
|
@@ -7911,6 +8042,7 @@ var MockRelay = class {
|
|
|
7911
8042
|
NCC02Builder,
|
|
7912
8043
|
NCC02Error,
|
|
7913
8044
|
NCC02Resolver,
|
|
8045
|
+
isExpired,
|
|
7914
8046
|
verifyNCC02Event
|
|
7915
8047
|
});
|
|
7916
8048
|
/*! Bundled license information:
|