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 CHANGED
@@ -48,39 +48,107 @@ try {
48
48
  }
49
49
  ```
50
50
 
51
- ### 2. Publish a Service Record
52
- ...
53
- ### Trust Model & Security
51
+ ### 2. Publish Service Records and Attestations
54
52
 
55
- ### Trust Levels
56
- - `self`: Asserted by the service owner (default if no attestation).
57
- - `verified`: Attested by a trusted third party.
58
- - `hardened`: Attested by a third party with stricter verification (e.g., physical proof or long-term history).
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
- ### Resolution Optimization
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
- ### Threat Model
64
- ...
65
- ### API Reference
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) Existing `nostr-tools` SimplePool.
70
- - `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.
71
119
 
72
120
  #### `resolve(pubkey, serviceId, options)`
73
- - `options.requireAttestation`: Fails if no trusted attestation is found.
74
- - `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.
75
128
 
76
129
  #### `close()`
77
- Closes WebSocket connections to all relays. If the resolver was initialized with an external `pool`, this will *not* close the pool; it only untracks the relays for this instance. If the resolver created its own internal pool, it will close it entirely.
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
- ### `NCC02Builder(privateKey)`
81
- - `createServiceRecord({ serviceId, endpoint?, fingerprint?, expiryDays? })`
82
- - `createAttestation({ subjectPubkey, serviceId, serviceEventId, level, validDays })`
83
- - `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`.
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} 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) {
@@ -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
- * @throws {NCC02Error} If verification or policy checks fail.
7732
- * @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.
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 serviceEvents;
7828
+ let serviceEvent;
7741
7829
  try {
7742
- serviceEvents = await this._query({
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 (!serviceEvents || !serviceEvents.length) {
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
- const validAttestations = [];
7776
- if (requireAttestation || minLevel === "verified" || minLevel === "hardened") {
7777
- let attestations;
7778
- let revocations;
7779
- try {
7780
- [attestations, revocations] = await Promise.all([
7781
- this._query({ kinds: [KINDS.ATTESTATION], "#e": [serviceEvent.id] }),
7782
- this._query({ kinds: [KINDS.REVOCATION] })
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 {string | undefined} actual
7818
- * @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
7819
7887
  */
7820
- _isLevelSufficient(actual, required) {
7821
- const levels = { "self": 0, "verified": 1, "hardened": 2 };
7822
- const actualVal = actual ? levels[actual] ?? -1 : -1;
7823
- const requiredVal = levels[required] ?? 0;
7824
- 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 };
7825
7924
  }
7826
7925
  /**
7827
- * @param {any} att
7828
- * @param {any} tags
7829
- * @param {any[]} revocations
7926
+ * @param {any[]} revocations
7927
+ * @returns {Record<string, any[]>}
7830
7928
  */
7831
- _isAttestationValid(att, tags, revocations) {
7832
- if (!verifyEvent2(att)) return false;
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
- for (const rev of revocations) {
7843
- const revTags = Object.fromEntries(rev.tags);
7844
- if (revTags.e === att.id && rev.pubkey === att.pubkey) {
7845
- if (verifyEvent2(rev)) {
7846
- return false;
7847
- }
7848
- }
7849
- }
7850
- 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;
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 {ResolvedService} resolved - The object returned by resolve().
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: