ncc-02-js 0.3.0 → 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 CHANGED
@@ -43,78 +43,112 @@ try {
43
43
  }
44
44
  } catch (err) {
45
45
  console.error('Resolution failed:', err.code, err.message);
46
+ } finally {
47
+ resolver.close(); // Clean up WebSocket connections
46
48
  }
47
49
  ```
48
50
 
49
- ### 2. Publish a Service Record
51
+ ### 2. Publish Service Records and Attestations
50
52
 
51
53
  ```javascript
52
54
  import { NCC02Builder } from 'ncc-02-js';
53
55
 
54
- // Initialize with private key (hex)
55
56
  const builder = new NCC02Builder(privateKey);
56
57
 
57
- // Example 1: Public IP-based Service
58
- const event = builder.createServiceRecord({
59
- serviceId: 'media',
60
- endpoint: 'https://203.0.113.45:8443',
61
- fingerprint: 'sha256:fingerprint',
62
- expiryDays: 14
63
- });
64
-
65
- // Example 2: Private / Invite-Only Service
66
- const privateEvent = builder.createServiceRecord({
67
- serviceId: 'wallet',
68
- fingerprint: 'sha256:fingerprint',
58
+ const serviceRecord = await builder.createServiceRecord({
59
+ serviceId: 'api',
60
+ endpoint: 'https://service.example.com',
61
+ fingerprint: '<spki fingerprint>',
69
62
  expiryDays: 7
70
63
  });
71
- // publish events to relays...
72
- ```
73
-
74
- ### 3. Issue an Attestation (CA)
75
64
 
76
- ```javascript
77
- const caBuilder = new NCC02Builder(caPrivateKey);
78
- const attestation = caBuilder.createAttestation({
79
- subjectPubkey: 'npub1...', // The service owner being certified
80
- serviceId: 'media',
81
- serviceEventId: serviceRecordEventId,
65
+ const attestation = await builder.createAttestation({
66
+ subjectPubkey: ownerPubkey,
67
+ serviceId: 'api',
68
+ serviceEventId: serviceRecord.id,
82
69
  level: 'verified',
83
70
  validDays: 30
84
71
  });
72
+
73
+ await builder.createRevocation({
74
+ attestationId: attestation.id,
75
+ reason: 'Key rotation'
76
+ });
85
77
  ```
86
78
 
87
- ## Trust Model & Security
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`.
88
80
 
89
- ### Trust Levels
90
- - `self`: Asserted by the service owner (default if no attestation).
91
- - `verified`: Attested by a trusted third party.
92
- - `hardened`: Attested by a third party with stricter verification (e.g., physical proof or long-term history).
81
+ ### 3. Trust Model & Security
93
82
 
94
- ### Threat Model
95
- - **Endpoint Impersonation**: Prevented by binding the endpoint URI to a public key fingerprint (`k` tag).
96
- - **Man-in-the-Middle (MITM)**: Mitigated via cryptographic pinning of transport-level keys.
97
- - **Stale Records**: Limited by required expiry (`exp`) and support for revocations.
98
- - **Relay Censorship**: Mitigated by querying multiple relays (implemented via `SimplePool`).
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.
99
84
 
100
- ### Fail-Closed Design
101
- The library follows a fail-closed principle. If a policy requirement is not met (e.g., `requireAttestation: true` but no valid attestation is found), it throws an `NCC02Error` rather than returning a partially verified record.
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
90
+
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.
92
+
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.
102
112
 
103
113
  ## API Reference
104
114
 
105
115
  ### `NCC02Resolver(relays, options)`
106
116
  - `relays`: Array of relay URLs.
107
- - `options.pool`: (Optional) Existing `nostr-tools` SimplePool.
108
- - `options.trustedCAPubkeys`: (Optional) Array of pubkeys trusted to issue attestations.
117
+ - `options.pool`: (Optional) Shared `nostr-tools` `SimplePool`.
118
+ - `options.trustedCAPubkeys`: Optional array of certifier pubkeys that may issue trusted attestations.
109
119
 
110
120
  #### `resolve(pubkey, serviceId, options)`
111
- - `options.requireAttestation`: Fails if no trusted attestation is found.
112
- - `options.minLevel`: Minimum trust level required.
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.
128
+
129
+ #### `close()`
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.
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.
113
147
 
114
- ### `NCC02Builder(privateKey)`
115
- - `createServiceRecord({ serviceId, endpoint?, fingerprint?, expiryDays? })`
116
- - `createAttestation({ subjectPubkey, serviceId, serviceEventId, level, validDays })`
117
- - `createRevocation({ attestationId, reason })`
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`.
118
152
 
119
153
  ## License
120
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} privateKey - The private key to sign events with.
2502
+ * @param {string | Uint8Array | NostrSigner} signer - Raw private key or asynchronous signer.
2502
2503
  */
2503
- constructor(privateKey) {
2504
- if (!privateKey) throw new Error("Private key is required");
2505
- this.sk = typeof privateKey === "string" ? hexToBytes2(privateKey) : privateKey;
2506
- this.pk = getPublicKey(this.sk);
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 finalizeEvent(event, this.sk);
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 finalizeEvent(event, this.sk);
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 finalizeEvent(event, this.sk);
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) {
@@ -7692,6 +7759,14 @@ var NCC02Resolver = class {
7692
7759
  this.ownsPool = !options.pool;
7693
7760
  this.trustedCAPubkeys = new Set(options.trustedCAPubkeys || []);
7694
7761
  }
7762
+ /**
7763
+ * Closes the connection to the relays if the pool is owned by this resolver.
7764
+ */
7765
+ close() {
7766
+ if (this.ownsPool && this.pool) {
7767
+ this.pool.close(this.relays);
7768
+ }
7769
+ }
7695
7770
  /**
7696
7771
  * Internal query helper using SimplePool.subscribeMany (since list() is deprecated).
7697
7772
  * @param {import('nostr-tools').Filter} filter
@@ -7711,6 +7786,27 @@ var NCC02Resolver = class {
7711
7786
  });
7712
7787
  });
7713
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
+ }
7714
7810
  /**
7715
7811
  * Resolves a service for a given pubkey and service identifier.
7716
7812
  *
@@ -7720,8 +7816,8 @@ var NCC02Resolver = class {
7720
7816
  * @param {boolean} [options.requireAttestation=false] - If true, fails if no trusted attestation is found.
7721
7817
  * @param {string} [options.minLevel=null] - Minimum trust level ('self', 'verified', 'hardened').
7722
7818
  * @param {string} [options.standard='nostr-service-trust-v0.1'] - Expected trust standard.
7723
- * @throws {NCC02Error} If verification or policy checks fail.
7724
- * @returns {Promise<ResolvedService>} The verified service details.
7819
+ * @throws {NCC02Error} If verification or policy checks fail.
7820
+ * @returns {Promise<ServiceStatus>} The service status including trust metadata.
7725
7821
  */
7726
7822
  async resolve(pubkey, serviceId, options = {}) {
7727
7823
  const {
@@ -7729,9 +7825,9 @@ var NCC02Resolver = class {
7729
7825
  minLevel = null,
7730
7826
  standard = "nostr-service-trust-v0.1"
7731
7827
  } = options;
7732
- let serviceEvents;
7828
+ let serviceEvent;
7733
7829
  try {
7734
- serviceEvents = await this._query({
7830
+ serviceEvent = await this._queryFreshest({
7735
7831
  kinds: [KINDS.SERVICE_RECORD],
7736
7832
  authors: [pubkey],
7737
7833
  "#d": [serviceId]
@@ -7739,13 +7835,9 @@ var NCC02Resolver = class {
7739
7835
  } catch (err) {
7740
7836
  throw new NCC02Error("RELAY_ERROR", `Failed to query relay for ${serviceId}`, err);
7741
7837
  }
7742
- if (!serviceEvents || !serviceEvents.length) {
7838
+ if (!serviceEvent) {
7743
7839
  throw new NCC02Error("NOT_FOUND", `No service record found for ${serviceId}`);
7744
7840
  }
7745
- const serviceEvent = serviceEvents.sort((a, b) => {
7746
- if (b.created_at !== a.created_at) return b.created_at - a.created_at;
7747
- return a.id.localeCompare(b.id);
7748
- })[0];
7749
7841
  if (!verifyEvent2(serviceEvent)) {
7750
7842
  throw new NCC02Error("INVALID_SIGNATURE", "Service record signature verification failed");
7751
7843
  }
@@ -7764,86 +7856,135 @@ var NCC02Resolver = class {
7764
7856
  if (exp < now2) {
7765
7857
  throw new NCC02Error("EXPIRED", "Service record has expired");
7766
7858
  }
7767
- let attestations;
7768
- let revocations;
7859
+ let trustData;
7769
7860
  try {
7770
- [attestations, revocations] = await Promise.all([
7771
- this._query({ kinds: [KINDS.ATTESTATION], "#e": [serviceEvent.id] }),
7772
- this._query({ kinds: [KINDS.REVOCATION] })
7773
- ]);
7861
+ trustData = await this._buildTrustData(serviceEvent, { pubkey, serviceId, standard, minLevel });
7774
7862
  } catch (err) {
7775
7863
  throw new NCC02Error("RELAY_ERROR", "Failed to query relay for attestations/revocations", err);
7776
7864
  }
7777
- const validAttestations = [];
7778
- for (const att of attestations) {
7779
- if (this.trustedCAPubkeys.has(att.pubkey)) {
7780
- const attTags = Object.fromEntries(att.tags);
7781
- if (attTags.subj !== pubkey) continue;
7782
- if (attTags.srv !== serviceId) continue;
7783
- if (standard && attTags.std !== standard) continue;
7784
- if (minLevel && !this._isLevelSufficient(attTags.lvl, minLevel)) continue;
7785
- if (this._isAttestationValid(att, attTags, revocations)) {
7786
- validAttestations.push({
7787
- pubkey: att.pubkey,
7788
- level: attTags.lvl,
7789
- eventId: att.id
7790
- });
7791
- }
7792
- }
7793
- }
7794
- if (requireAttestation && validAttestations.length === 0) {
7865
+ if (requireAttestation && trustData.validAttestations.length === 0) {
7795
7866
  throw new NCC02Error("POLICY_FAILURE", `No trusted attestations meet the required policy for ${serviceId}`);
7796
7867
  }
7797
7868
  return {
7798
7869
  endpoint: serviceTags.u,
7799
7870
  fingerprint: serviceTags.k,
7800
7871
  expiry: exp,
7801
- attestations: validAttestations,
7872
+ attestations: trustData.validAttestations,
7873
+ attestationCount: trustData.validAttestations.length,
7874
+ isRevoked: trustData.isRevoked,
7802
7875
  eventId: serviceEvent.id,
7803
- pubkey: serviceEvent.pubkey
7876
+ pubkey: serviceEvent.pubkey,
7877
+ serviceEvent
7804
7878
  };
7805
7879
  }
7806
7880
  /**
7807
- * @param {string | undefined} actual
7808
- * @param {string} required
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
7809
7887
  */
7810
- _isLevelSufficient(actual, required) {
7811
- const levels = { "self": 0, "verified": 1, "hardened": 2 };
7812
- const actualVal = actual ? levels[actual] ?? -1 : -1;
7813
- const requiredVal = levels[required] ?? 0;
7814
- return actualVal >= requiredVal;
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 };
7924
+ }
7925
+ /**
7926
+ * @param {any[]} revocations
7927
+ * @returns {Record<string, any[]>}
7928
+ */
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;
7815
7944
  }
7816
7945
  /**
7817
- * @param {any} att
7818
- * @param {any} tags
7819
- * @param {any[]} revocations
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]
7820
7954
  */
7821
- _isAttestationValid(att, tags, revocations) {
7822
- if (!verifyEvent2(att)) return false;
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 };
7823
7962
  const now2 = Math.floor(Date.now() / 1e3);
7824
7963
  if (tags.nbf) {
7825
- const nbf = parseInt(tags.nbf);
7826
- 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 };
7827
7966
  }
7828
7967
  if (tags.exp) {
7829
- const exp = parseInt(tags.exp);
7830
- if (isNaN(exp) || exp < now2) return false;
7831
- }
7832
- for (const rev of revocations) {
7833
- const revTags = Object.fromEntries(rev.tags);
7834
- if (revTags.e === att.id && rev.pubkey === att.pubkey) {
7835
- if (verifyEvent2(rev)) {
7836
- return false;
7837
- }
7838
- }
7968
+ const exp = parseInt(tags.exp, 10);
7969
+ if (isNaN(exp) || exp < now2) return { valid: false, revoked: false };
7839
7970
  }
7840
- return true;
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;
7841
7982
  }
7842
7983
  /**
7843
7984
  * Verifies that the actual fingerprint found during transport-level connection
7844
7985
  * matches the one declared in the signed service record.
7845
7986
  *
7846
- * @param {ResolvedService} resolved - The object returned by resolve().
7987
+ * @param {ServiceStatus} resolved - The object returned by resolve().
7847
7988
  * @param {string} actualFingerprint - The fingerprint obtained from the service.
7848
7989
  * @returns {boolean}
7849
7990
  */
@@ -7901,6 +8042,7 @@ var MockRelay = class {
7901
8042
  NCC02Builder,
7902
8043
  NCC02Error,
7903
8044
  NCC02Resolver,
8045
+ isExpired,
7904
8046
  verifyNCC02Event
7905
8047
  });
7906
8048
  /*! Bundled license information: