ncc-02-js 0.3.1 → 0.5.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/dist/index.cjs CHANGED
@@ -34,6 +34,12 @@ __export(index_exports, {
34
34
  NCC02Builder: () => NCC02Builder,
35
35
  NCC02Error: () => NCC02Error,
36
36
  NCC02Resolver: () => NCC02Resolver,
37
+ collectPrivateRecipients: () => collectPrivateRecipients,
38
+ decryptPrivateRecipient: () => decryptPrivateRecipient,
39
+ encryptPrivateRecipients: () => encryptPrivateRecipients,
40
+ isExpired: () => isExpired,
41
+ isPrivateRecipientAuthorized: () => isPrivateRecipientAuthorized,
42
+ parsePrivateFlag: () => parsePrivateFlag,
37
43
  verifyNCC02Event: () => verifyNCC02Event
38
44
  });
39
45
  module.exports = __toCommonJS(index_exports);
@@ -2498,12 +2504,73 @@ var KINDS = {
2498
2504
  };
2499
2505
  var NCC02Builder = class {
2500
2506
  /**
2501
- * @param {string | Uint8Array} privateKey - The private key to sign events with.
2507
+ * @param {string | Uint8Array | NostrSigner} signer - Raw private key or asynchronous signer.
2502
2508
  */
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);
2509
+ constructor(signer) {
2510
+ if (!signer) throw new Error("Signer or private key is required");
2511
+ this.signer = this._normalizeSigner(signer);
2512
+ this._pubkeyPromise = this.signer.getPublicKey();
2513
+ this._pubkey = void 0;
2514
+ }
2515
+ async _getPublicKey() {
2516
+ if (!this._pubkey) {
2517
+ this._pubkey = await this._pubkeyPromise;
2518
+ }
2519
+ return this._pubkey;
2520
+ }
2521
+ /**
2522
+ * @param {any} event
2523
+ */
2524
+ async _finalizeEvent(event) {
2525
+ const pubkey = await this._getPublicKey();
2526
+ const eventWithPubkey = { ...event, pubkey };
2527
+ const signed = await this.signer.signEvent(eventWithPubkey);
2528
+ if (!signed || typeof signed.id !== "string" || typeof signed.sig !== "string") {
2529
+ throw new Error("Signer must return a signed event with id and sig");
2530
+ }
2531
+ return signed;
2532
+ }
2533
+ /**
2534
+ * @param {any} signer
2535
+ * @returns {NostrSigner}
2536
+ */
2537
+ _normalizeSigner(signer) {
2538
+ if (typeof signer === "string" || signer instanceof Uint8Array) {
2539
+ const privateKey = typeof signer === "string" ? hexToBytes2(signer) : signer;
2540
+ const pubkey = getPublicKey(privateKey);
2541
+ return {
2542
+ getPublicKey: async () => pubkey,
2543
+ /** @param {any} event */
2544
+ signEvent: async (event) => {
2545
+ const clonedEvent = {
2546
+ ...event,
2547
+ tags: Array.isArray(event.tags) ? event.tags.map((tag) => [...tag]) : []
2548
+ };
2549
+ return finalizeEvent(clonedEvent, privateKey);
2550
+ }
2551
+ };
2552
+ }
2553
+ if (typeof signer === "object" && signer !== null) {
2554
+ if (typeof signer.getPublicKey === "function" && typeof signer.signEvent === "function") {
2555
+ return {
2556
+ getPublicKey: async () => {
2557
+ const pubkey = await signer.getPublicKey();
2558
+ if (typeof pubkey !== "string") throw new Error("Signer.getPublicKey must return a hex string");
2559
+ return pubkey;
2560
+ },
2561
+ /** @param {any} event */
2562
+ signEvent: async (event) => {
2563
+ const signed = await signer.signEvent(event);
2564
+ if (!signed || typeof signed.id !== "string" || typeof signed.sig !== "string") {
2565
+ throw new Error("Signer.signEvent must return a signed event");
2566
+ }
2567
+ return signed;
2568
+ },
2569
+ decryptEvent: typeof signer.decryptEvent === "function" ? signer.decryptEvent.bind(signer) : void 0
2570
+ };
2571
+ }
2572
+ }
2573
+ throw new Error("Unsupported signer provided to NCC02Builder");
2507
2574
  }
2508
2575
  /**
2509
2576
  * Creates a signed Service Record (Kind 30059).
@@ -2512,25 +2579,35 @@ var NCC02Builder = class {
2512
2579
  * @param {string} [options.endpoint] - The 'u' tag URI.
2513
2580
  * @param {string} [options.fingerprint] - The 'k' tag fingerprint.
2514
2581
  * @param {number} [options.expiryDays=14] - Expiry in days.
2582
+ * @param {boolean} [options.isPrivate=false] - Whether the service is private (adds required `private` tag).
2583
+ * @param {string[]} [options.privateRecipients] - Optional encrypted ciphertexts for authorized recipients.
2515
2584
  */
2516
- createServiceRecord(options) {
2517
- const { serviceId, endpoint, fingerprint, expiryDays = 14 } = options;
2585
+ async createServiceRecord(options) {
2586
+ const { serviceId, endpoint, fingerprint, expiryDays = 14, isPrivate = false, privateRecipients } = options;
2518
2587
  if (!serviceId) throw new Error("serviceId (d tag) is required");
2588
+ if (typeof isPrivate !== "boolean") throw new Error("isPrivate must be a boolean value");
2519
2589
  const expiry = Math.floor(Date.now() / 1e3) + expiryDays * 24 * 60 * 60;
2520
2590
  const tags = [
2521
2591
  ["d", serviceId],
2522
2592
  ["exp", expiry.toString()]
2523
2593
  ];
2594
+ tags.push(["private", isPrivate ? "true" : "false"]);
2524
2595
  if (endpoint) tags.push(["u", endpoint]);
2525
2596
  if (fingerprint) tags.push(["k", fingerprint]);
2597
+ if (privateRecipients) {
2598
+ if (!Array.isArray(privateRecipients)) throw new Error("privateRecipients must be an array");
2599
+ privateRecipients.forEach((cipher) => {
2600
+ if (typeof cipher !== "string") throw new Error("privateRecipients entries must be strings");
2601
+ tags.push(["privateRecipients", cipher]);
2602
+ });
2603
+ }
2526
2604
  const event = {
2527
2605
  kind: KINDS.SERVICE_RECORD,
2528
2606
  created_at: Math.floor(Date.now() / 1e3),
2529
2607
  tags,
2530
- content: `NCC-02 Service Record for ${serviceId}`,
2531
- pubkey: this.pk
2608
+ content: `NCC-02 Service Record for ${serviceId}`
2532
2609
  };
2533
- return finalizeEvent(event, this.sk);
2610
+ return this._finalizeEvent(event);
2534
2611
  }
2535
2612
  /**
2536
2613
  * Creates a signed Certificate Attestation (Kind 30060).
@@ -2541,7 +2618,7 @@ var NCC02Builder = class {
2541
2618
  * @param {string} [options.level='verified'] - The 'lvl' tag level.
2542
2619
  * @param {number} [options.validDays=30] - Validity in days.
2543
2620
  */
2544
- createAttestation(options) {
2621
+ async createAttestation(options) {
2545
2622
  const { subjectPubkey, serviceId, serviceEventId, level = "verified", validDays = 30 } = options;
2546
2623
  if (!subjectPubkey) throw new Error("subjectPubkey is required");
2547
2624
  if (!serviceId) throw new Error("serviceId is required");
@@ -2560,10 +2637,9 @@ var NCC02Builder = class {
2560
2637
  ["nbf", now2.toString()],
2561
2638
  ["exp", expiry.toString()]
2562
2639
  ],
2563
- content: "NCC-02 Attestation",
2564
- pubkey: this.pk
2640
+ content: "NCC-02 Attestation"
2565
2641
  };
2566
- return finalizeEvent(event, this.sk);
2642
+ return this._finalizeEvent(event);
2567
2643
  }
2568
2644
  /**
2569
2645
  * Creates a signed Revocation (Kind 30061).
@@ -2571,7 +2647,7 @@ var NCC02Builder = class {
2571
2647
  * @param {string} options.attestationId - The 'e' tag referencing the attestation.
2572
2648
  * @param {string} [options.reason=''] - Optional reason.
2573
2649
  */
2574
- createRevocation(options) {
2650
+ async createRevocation(options) {
2575
2651
  const { attestationId, reason = "" } = options;
2576
2652
  if (!attestationId) throw new Error("attestationId (e tag) is required");
2577
2653
  const tags = [["e", attestationId]];
@@ -2580,15 +2656,22 @@ var NCC02Builder = class {
2580
2656
  kind: KINDS.REVOCATION,
2581
2657
  created_at: Math.floor(Date.now() / 1e3),
2582
2658
  tags,
2583
- content: "NCC-02 Revocation",
2584
- pubkey: this.pk
2659
+ content: "NCC-02 Revocation"
2585
2660
  };
2586
- return finalizeEvent(event, this.sk);
2661
+ return this._finalizeEvent(event);
2587
2662
  }
2588
2663
  };
2589
2664
  function verifyNCC02Event(event) {
2590
2665
  return verifyEvent(event);
2591
2666
  }
2667
+ function isExpired(event) {
2668
+ if (!event || !Array.isArray(event.tags)) return false;
2669
+ const expTag = event.tags.find((tag) => tag[0] === "exp");
2670
+ if (!expTag) return false;
2671
+ const expiry = parseInt(expTag[1], 10);
2672
+ if (Number.isNaN(expiry)) return false;
2673
+ return expiry <= Math.floor(Date.now() / 1e3);
2674
+ }
2592
2675
 
2593
2676
  // node_modules/@scure/base/lib/esm/index.js
2594
2677
  function assertNumber(n) {
@@ -7660,6 +7743,136 @@ async function validateEvent22(event, url, method, body) {
7660
7743
  return true;
7661
7744
  }
7662
7745
 
7746
+ // src/privacy.js
7747
+ var PRIVATE_TAG = "private";
7748
+ var PRIVATE_RECIPIENTS_TAG = "privateRecipients";
7749
+ var HEX_REGEX = /^[0-9a-f]{64}$/i;
7750
+ function normalizeHexPubkey(value) {
7751
+ if (typeof value !== "string") {
7752
+ throw new Error("Pubkey must be a string");
7753
+ }
7754
+ const lower = value.toLowerCase();
7755
+ if (HEX_REGEX.test(lower)) {
7756
+ return lower;
7757
+ }
7758
+ try {
7759
+ const decoded = nip19_exports.decode(value);
7760
+ if (decoded.type === "npub" && typeof decoded.data === "string") {
7761
+ return decoded.data.toLowerCase();
7762
+ }
7763
+ } catch {
7764
+ }
7765
+ throw new Error("Unsupported pubkey format");
7766
+ }
7767
+ function toNpub(hexPubkey) {
7768
+ return nip19_exports.npubEncode(hexPubkey);
7769
+ }
7770
+ function toUint8ArrayKey(key) {
7771
+ if (typeof key === "string") {
7772
+ return hexToBytes2(key);
7773
+ }
7774
+ if (key instanceof Uint8Array) {
7775
+ return key;
7776
+ }
7777
+ throw new Error("Private key must be a hex string or Uint8Array");
7778
+ }
7779
+ function createNip44Encryptor(owner) {
7780
+ if (typeof owner === "string" || owner instanceof Uint8Array) {
7781
+ const ownerKeyBytes = toUint8ArrayKey(owner);
7782
+ return async (recipientHex, plaintext) => {
7783
+ const conversationKey = nip44_exports.getConversationKey(ownerKeyBytes, recipientHex);
7784
+ return nip44_exports.encrypt(plaintext, conversationKey);
7785
+ };
7786
+ }
7787
+ if (owner && typeof owner === "object") {
7788
+ if (typeof owner.nip44Encrypt === "function") {
7789
+ const encryptFn = owner.nip44Encrypt.bind(owner);
7790
+ return (recipientHex, plaintext) => Promise.resolve(encryptFn(recipientHex, plaintext));
7791
+ }
7792
+ if (owner.nip04 && typeof owner.nip04.encrypt === "function") {
7793
+ const encryptFn = owner.nip04.encrypt.bind(owner.nip04);
7794
+ return (recipientHex, plaintext) => Promise.resolve(encryptFn(recipientHex, plaintext));
7795
+ }
7796
+ }
7797
+ throw new Error("Unsupported owner signer; must be private key or NIP-44/NIP-04 capable signer");
7798
+ }
7799
+ function createNip44Decryptor(recipient) {
7800
+ if (typeof recipient === "string" || recipient instanceof Uint8Array) {
7801
+ const recipientKeyBytes = toUint8ArrayKey(recipient);
7802
+ return async (ownerHex, ciphertext) => {
7803
+ const conversationKey = nip44_exports.getConversationKey(recipientKeyBytes, ownerHex);
7804
+ return nip44_exports.decrypt(ciphertext, conversationKey);
7805
+ };
7806
+ }
7807
+ if (recipient && typeof recipient === "object") {
7808
+ if (typeof recipient.nip44Decrypt === "function") {
7809
+ const decryptFn = recipient.nip44Decrypt.bind(recipient);
7810
+ return (ownerHex, ciphertext) => Promise.resolve(decryptFn(ownerHex, ciphertext));
7811
+ }
7812
+ if (recipient.nip04 && typeof recipient.nip04.decrypt === "function") {
7813
+ const decryptFn = recipient.nip04.decrypt.bind(recipient.nip04);
7814
+ return (ownerHex, ciphertext) => Promise.resolve(decryptFn(ownerHex, ciphertext));
7815
+ }
7816
+ }
7817
+ throw new Error("Unsupported recipient signer; must be private key or NIP-44/NIP-04 capable signer");
7818
+ }
7819
+ async function resolveRecipientPubkey(recipient) {
7820
+ if (typeof recipient === "string" || recipient instanceof Uint8Array) {
7821
+ return normalizeHexPubkey(getPublicKey(toUint8ArrayKey(recipient)));
7822
+ }
7823
+ if (recipient && typeof recipient.getPublicKey === "function") {
7824
+ const pubkey = await recipient.getPublicKey();
7825
+ return normalizeHexPubkey(pubkey);
7826
+ }
7827
+ throw new Error("Recipient must provide a private key or a NIP signer with getPublicKey()");
7828
+ }
7829
+ function parsePrivateFlag(tags) {
7830
+ if (!Array.isArray(tags)) return null;
7831
+ const tag = tags.find((t) => Array.isArray(t) && t[0] === PRIVATE_TAG);
7832
+ if (!tag || typeof tag[1] !== "string") return null;
7833
+ const normalized = tag[1].toLowerCase();
7834
+ if (normalized === "true") return true;
7835
+ if (normalized === "false") return false;
7836
+ return null;
7837
+ }
7838
+ function collectPrivateRecipients(tags) {
7839
+ if (!Array.isArray(tags)) return [];
7840
+ return tags.filter((t) => Array.isArray(t) && t[0] === PRIVATE_RECIPIENTS_TAG && typeof t[1] === "string").map((t) => t[1]);
7841
+ }
7842
+ async function encryptPrivateRecipients(ownerPrivateKey, recipients) {
7843
+ if (!Array.isArray(recipients)) {
7844
+ throw new Error("recipients must be an array of pubkeys");
7845
+ }
7846
+ const encryptor = createNip44Encryptor(ownerPrivateKey);
7847
+ const encrypted = [];
7848
+ for (const recipient of recipients) {
7849
+ const recipientHex = normalizeHexPubkey(recipient);
7850
+ const recipientNpub = toNpub(recipientHex);
7851
+ encrypted.push(await encryptor(recipientHex, recipientNpub));
7852
+ }
7853
+ return encrypted;
7854
+ }
7855
+ async function decryptPrivateRecipient(ciphertext, ownerPubkey, recipientPrivateKey) {
7856
+ const ownerHex = normalizeHexPubkey(ownerPubkey);
7857
+ const decryptor = createNip44Decryptor(recipientPrivateKey);
7858
+ return decryptor(ownerHex, ciphertext);
7859
+ }
7860
+ async function isPrivateRecipientAuthorized(privateRecipients, ownerPubkey, recipientPrivateKey) {
7861
+ if (!Array.isArray(privateRecipients) || privateRecipients.length === 0) return false;
7862
+ const ownerHex = normalizeHexPubkey(ownerPubkey);
7863
+ const recipientPubkey = await resolveRecipientPubkey(recipientPrivateKey);
7864
+ const expectedNpub = toNpub(normalizeHexPubkey(recipientPubkey));
7865
+ const decryptor = createNip44Decryptor(recipientPrivateKey);
7866
+ for (const ciphertext of privateRecipients) {
7867
+ try {
7868
+ const decrypted = await decryptor(ownerHex, ciphertext);
7869
+ if (decrypted === expectedNpub) return true;
7870
+ } catch {
7871
+ }
7872
+ }
7873
+ return false;
7874
+ }
7875
+
7663
7876
  // src/resolver.js
7664
7877
  var NCC02Error = class extends Error {
7665
7878
  /**
@@ -7719,6 +7932,27 @@ var NCC02Resolver = class {
7719
7932
  });
7720
7933
  });
7721
7934
  }
7935
+ /**
7936
+ * Returns the first event sorted by freshness (newest created_at, tie broken by id).
7937
+ * @param {import('nostr-tools').Event[]} events
7938
+ * @returns {import('nostr-tools').Event|null}
7939
+ */
7940
+ _freshestEvent(events) {
7941
+ if (!events || !events.length) return null;
7942
+ return events.sort((a, b) => {
7943
+ if (b.created_at !== a.created_at) return b.created_at - a.created_at;
7944
+ return a.id.localeCompare(b.id);
7945
+ })[0];
7946
+ }
7947
+ /**
7948
+ * Query helper that returns only the freshest event matching the filter.
7949
+ * @param {import('nostr-tools').Filter} filter
7950
+ * @returns {Promise<import('nostr-tools').Event | null>}
7951
+ */
7952
+ async _queryFreshest(filter) {
7953
+ const events = await this._query(filter);
7954
+ return this._freshestEvent(events);
7955
+ }
7722
7956
  /**
7723
7957
  * Resolves a service for a given pubkey and service identifier.
7724
7958
  *
@@ -7728,8 +7962,8 @@ var NCC02Resolver = class {
7728
7962
  * @param {boolean} [options.requireAttestation=false] - If true, fails if no trusted attestation is found.
7729
7963
  * @param {string} [options.minLevel=null] - Minimum trust level ('self', 'verified', 'hardened').
7730
7964
  * @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.
7965
+ * @throws {NCC02Error} If verification or policy checks fail.
7966
+ * @returns {Promise<ServiceStatus>} The service status including trust metadata.
7733
7967
  */
7734
7968
  async resolve(pubkey, serviceId, options = {}) {
7735
7969
  const {
@@ -7737,9 +7971,9 @@ var NCC02Resolver = class {
7737
7971
  minLevel = null,
7738
7972
  standard = "nostr-service-trust-v0.1"
7739
7973
  } = options;
7740
- let serviceEvents;
7974
+ let serviceEvent;
7741
7975
  try {
7742
- serviceEvents = await this._query({
7976
+ serviceEvent = await this._queryFreshest({
7743
7977
  kinds: [KINDS.SERVICE_RECORD],
7744
7978
  authors: [pubkey],
7745
7979
  "#d": [serviceId]
@@ -7747,18 +7981,18 @@ var NCC02Resolver = class {
7747
7981
  } catch (err) {
7748
7982
  throw new NCC02Error("RELAY_ERROR", `Failed to query relay for ${serviceId}`, err);
7749
7983
  }
7750
- if (!serviceEvents || !serviceEvents.length) {
7984
+ if (!serviceEvent) {
7751
7985
  throw new NCC02Error("NOT_FOUND", `No service record found for ${serviceId}`);
7752
7986
  }
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
7987
  if (!verifyEvent2(serviceEvent)) {
7758
7988
  throw new NCC02Error("INVALID_SIGNATURE", "Service record signature verification failed");
7759
7989
  }
7760
7990
  const serviceTags = Object.fromEntries(serviceEvent.tags);
7761
7991
  const now2 = Math.floor(Date.now() / 1e3);
7992
+ const privateFlag = parsePrivateFlag(serviceEvent.tags);
7993
+ if (privateFlag === null) {
7994
+ throw new NCC02Error("MALFORMED_RECORD", "Service record is missing required tag (private)");
7995
+ }
7762
7996
  if (!serviceTags.exp) {
7763
7997
  throw new NCC02Error("MALFORMED_RECORD", "Service record is missing required tag (exp)");
7764
7998
  }
@@ -7772,88 +8006,137 @@ var NCC02Resolver = class {
7772
8006
  if (exp < now2) {
7773
8007
  throw new NCC02Error("EXPIRED", "Service record has expired");
7774
8008
  }
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
- }
8009
+ let trustData;
8010
+ try {
8011
+ trustData = await this._buildTrustData(serviceEvent, { pubkey, serviceId, standard, minLevel });
8012
+ } catch (err) {
8013
+ throw new NCC02Error("RELAY_ERROR", "Failed to query relay for attestations/revocations", err);
8014
+ }
8015
+ if (requireAttestation && trustData.validAttestations.length === 0) {
8016
+ throw new NCC02Error("POLICY_FAILURE", `No trusted attestations meet the required policy for ${serviceId}`);
7806
8017
  }
7807
8018
  return {
7808
8019
  endpoint: serviceTags.u,
7809
8020
  fingerprint: serviceTags.k,
7810
8021
  expiry: exp,
7811
- attestations: validAttestations,
8022
+ attestations: trustData.validAttestations,
8023
+ attestationCount: trustData.validAttestations.length,
8024
+ isRevoked: trustData.isRevoked,
8025
+ isPrivate: privateFlag,
8026
+ privateRecipients: collectPrivateRecipients(serviceEvent.tags),
7812
8027
  eventId: serviceEvent.id,
7813
- pubkey: serviceEvent.pubkey
8028
+ pubkey: serviceEvent.pubkey,
8029
+ serviceEvent
7814
8030
  };
7815
8031
  }
7816
8032
  /**
7817
- * @param {string | undefined} actual
7818
- * @param {string} required
8033
+ * @param {any} serviceEvent
8034
+ * @param {Object} options
8035
+ * @param {string} options.pubkey
8036
+ * @param {string} options.serviceId
8037
+ * @param {string|null} options.standard
8038
+ * @param {string|null} options.minLevel
7819
8039
  */
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;
8040
+ async _buildTrustData(serviceEvent, options) {
8041
+ const attestations = await this._query({
8042
+ kinds: [KINDS.ATTESTATION],
8043
+ "#e": [serviceEvent.id]
8044
+ });
8045
+ const attestationIds = attestations.map((att) => att.id);
8046
+ let revocations = [];
8047
+ if (attestationIds.length) {
8048
+ revocations = await this._query({
8049
+ kinds: [KINDS.REVOCATION],
8050
+ "#e": attestationIds
8051
+ });
8052
+ }
8053
+ const revocationIndex = this._groupValidRevocations(revocations);
8054
+ const validAttestations = [];
8055
+ let isRevoked = false;
8056
+ for (const att of attestations) {
8057
+ if (!this.trustedCAPubkeys.has(att.pubkey)) continue;
8058
+ const attTags = Object.fromEntries(att.tags);
8059
+ if (attTags.subj !== options.pubkey) continue;
8060
+ if (attTags.srv !== options.serviceId) continue;
8061
+ if (options.standard && attTags.std !== options.standard) continue;
8062
+ const { valid, revoked } = this._evaluateAttestation(att, attTags, revocationIndex[att.id]);
8063
+ if (revoked) {
8064
+ isRevoked = true;
8065
+ continue;
8066
+ }
8067
+ if (!valid) continue;
8068
+ if (options.minLevel && !this._isLevelSufficient(attTags.lvl, options.minLevel)) continue;
8069
+ validAttestations.push({
8070
+ pubkey: att.pubkey,
8071
+ level: attTags.lvl,
8072
+ eventId: att.id
8073
+ });
8074
+ }
8075
+ return { validAttestations, isRevoked };
7825
8076
  }
7826
8077
  /**
7827
- * @param {any} att
7828
- * @param {any} tags
7829
- * @param {any[]} revocations
8078
+ * @param {any[]} revocations
8079
+ * @returns {Record<string, any[]>}
8080
+ */
8081
+ /**
8082
+ * @param {any[]} revocations
8083
+ * @returns {Record<string, any[]>}
7830
8084
  */
7831
- _isAttestationValid(att, tags, revocations) {
7832
- if (!verifyEvent2(att)) return false;
8085
+ _groupValidRevocations(revocations) {
8086
+ const indexed = {};
8087
+ for (const rev of revocations) {
8088
+ if (!verifyEvent2(rev)) continue;
8089
+ const tags = Object.fromEntries(rev.tags);
8090
+ const targetId = tags.e;
8091
+ if (!targetId) continue;
8092
+ if (!indexed[targetId]) indexed[targetId] = [];
8093
+ indexed[targetId].push(rev);
8094
+ }
8095
+ return indexed;
8096
+ }
8097
+ /**
8098
+ * @param {any} att
8099
+ * @param {Record<string, string>} tags
8100
+ * @param {any[]} revocations
8101
+ */
8102
+ /**
8103
+ * @param {any} att
8104
+ * @param {Record<string, string>} tags
8105
+ * @param {any[]} [revocations]
8106
+ */
8107
+ _evaluateAttestation(att, tags, revocations = []) {
8108
+ for (const rev of revocations) {
8109
+ if (rev.pubkey === att.pubkey) {
8110
+ return { valid: false, revoked: true };
8111
+ }
8112
+ }
8113
+ if (!verifyEvent2(att)) return { valid: false, revoked: false };
7833
8114
  const now2 = Math.floor(Date.now() / 1e3);
7834
8115
  if (tags.nbf) {
7835
- const nbf = parseInt(tags.nbf);
7836
- if (isNaN(nbf) || nbf > now2) return false;
8116
+ const nbf = parseInt(tags.nbf, 10);
8117
+ if (isNaN(nbf) || nbf > now2) return { valid: false, revoked: false };
7837
8118
  }
7838
8119
  if (tags.exp) {
7839
- const exp = parseInt(tags.exp);
7840
- if (isNaN(exp) || exp < now2) return false;
8120
+ const exp = parseInt(tags.exp, 10);
8121
+ if (isNaN(exp) || exp < now2) return { valid: false, revoked: false };
7841
8122
  }
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;
8123
+ return { valid: true, revoked: false };
8124
+ }
8125
+ /**
8126
+ * @param {string | undefined} actual
8127
+ * @param {string} required
8128
+ */
8129
+ _isLevelSufficient(actual, required) {
8130
+ const levels = { "self": 0, "verified": 1, "hardened": 2 };
8131
+ const actualVal = actual ? levels[actual] ?? -1 : -1;
8132
+ const requiredVal = levels[required] ?? 0;
8133
+ return actualVal >= requiredVal;
7851
8134
  }
7852
8135
  /**
7853
8136
  * Verifies that the actual fingerprint found during transport-level connection
7854
8137
  * matches the one declared in the signed service record.
7855
8138
  *
7856
- * @param {ResolvedService} resolved - The object returned by resolve().
8139
+ * @param {ServiceStatus} resolved - The object returned by resolve().
7857
8140
  * @param {string} actualFingerprint - The fingerprint obtained from the service.
7858
8141
  * @returns {boolean}
7859
8142
  */
@@ -7911,6 +8194,12 @@ var MockRelay = class {
7911
8194
  NCC02Builder,
7912
8195
  NCC02Error,
7913
8196
  NCC02Resolver,
8197
+ collectPrivateRecipients,
8198
+ decryptPrivateRecipient,
8199
+ encryptPrivateRecipients,
8200
+ isExpired,
8201
+ isPrivateRecipientAuthorized,
8202
+ parsePrivateFlag,
7914
8203
  verifyNCC02Event
7915
8204
  });
7916
8205
  /*! Bundled license information:
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export * from "./models.js";
2
2
  export * from "./resolver.js";
3
3
  export * from "./mockRelay.js";
4
+ export * from "./privacy.js";