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.mjs CHANGED
@@ -2464,12 +2464,73 @@ var KINDS = {
2464
2464
  };
2465
2465
  var NCC02Builder = class {
2466
2466
  /**
2467
- * @param {string | Uint8Array} privateKey - The private key to sign events with.
2467
+ * @param {string | Uint8Array | NostrSigner} signer - Raw private key or asynchronous signer.
2468
2468
  */
2469
- constructor(privateKey) {
2470
- if (!privateKey) throw new Error("Private key is required");
2471
- this.sk = typeof privateKey === "string" ? hexToBytes2(privateKey) : privateKey;
2472
- this.pk = getPublicKey(this.sk);
2469
+ constructor(signer) {
2470
+ if (!signer) throw new Error("Signer or private key is required");
2471
+ this.signer = this._normalizeSigner(signer);
2472
+ this._pubkeyPromise = this.signer.getPublicKey();
2473
+ this._pubkey = void 0;
2474
+ }
2475
+ async _getPublicKey() {
2476
+ if (!this._pubkey) {
2477
+ this._pubkey = await this._pubkeyPromise;
2478
+ }
2479
+ return this._pubkey;
2480
+ }
2481
+ /**
2482
+ * @param {any} event
2483
+ */
2484
+ async _finalizeEvent(event) {
2485
+ const pubkey = await this._getPublicKey();
2486
+ const eventWithPubkey = { ...event, pubkey };
2487
+ const signed = await this.signer.signEvent(eventWithPubkey);
2488
+ if (!signed || typeof signed.id !== "string" || typeof signed.sig !== "string") {
2489
+ throw new Error("Signer must return a signed event with id and sig");
2490
+ }
2491
+ return signed;
2492
+ }
2493
+ /**
2494
+ * @param {any} signer
2495
+ * @returns {NostrSigner}
2496
+ */
2497
+ _normalizeSigner(signer) {
2498
+ if (typeof signer === "string" || signer instanceof Uint8Array) {
2499
+ const privateKey = typeof signer === "string" ? hexToBytes2(signer) : signer;
2500
+ const pubkey = getPublicKey(privateKey);
2501
+ return {
2502
+ getPublicKey: async () => pubkey,
2503
+ /** @param {any} event */
2504
+ signEvent: async (event) => {
2505
+ const clonedEvent = {
2506
+ ...event,
2507
+ tags: Array.isArray(event.tags) ? event.tags.map((tag) => [...tag]) : []
2508
+ };
2509
+ return finalizeEvent(clonedEvent, privateKey);
2510
+ }
2511
+ };
2512
+ }
2513
+ if (typeof signer === "object" && signer !== null) {
2514
+ if (typeof signer.getPublicKey === "function" && typeof signer.signEvent === "function") {
2515
+ return {
2516
+ getPublicKey: async () => {
2517
+ const pubkey = await signer.getPublicKey();
2518
+ if (typeof pubkey !== "string") throw new Error("Signer.getPublicKey must return a hex string");
2519
+ return pubkey;
2520
+ },
2521
+ /** @param {any} event */
2522
+ signEvent: async (event) => {
2523
+ const signed = await signer.signEvent(event);
2524
+ if (!signed || typeof signed.id !== "string" || typeof signed.sig !== "string") {
2525
+ throw new Error("Signer.signEvent must return a signed event");
2526
+ }
2527
+ return signed;
2528
+ },
2529
+ decryptEvent: typeof signer.decryptEvent === "function" ? signer.decryptEvent.bind(signer) : void 0
2530
+ };
2531
+ }
2532
+ }
2533
+ throw new Error("Unsupported signer provided to NCC02Builder");
2473
2534
  }
2474
2535
  /**
2475
2536
  * Creates a signed Service Record (Kind 30059).
@@ -2478,25 +2539,35 @@ var NCC02Builder = class {
2478
2539
  * @param {string} [options.endpoint] - The 'u' tag URI.
2479
2540
  * @param {string} [options.fingerprint] - The 'k' tag fingerprint.
2480
2541
  * @param {number} [options.expiryDays=14] - Expiry in days.
2542
+ * @param {boolean} [options.isPrivate=false] - Whether the service is private (adds required `private` tag).
2543
+ * @param {string[]} [options.privateRecipients] - Optional encrypted ciphertexts for authorized recipients.
2481
2544
  */
2482
- createServiceRecord(options) {
2483
- const { serviceId, endpoint, fingerprint, expiryDays = 14 } = options;
2545
+ async createServiceRecord(options) {
2546
+ const { serviceId, endpoint, fingerprint, expiryDays = 14, isPrivate = false, privateRecipients } = options;
2484
2547
  if (!serviceId) throw new Error("serviceId (d tag) is required");
2548
+ if (typeof isPrivate !== "boolean") throw new Error("isPrivate must be a boolean value");
2485
2549
  const expiry = Math.floor(Date.now() / 1e3) + expiryDays * 24 * 60 * 60;
2486
2550
  const tags = [
2487
2551
  ["d", serviceId],
2488
2552
  ["exp", expiry.toString()]
2489
2553
  ];
2554
+ tags.push(["private", isPrivate ? "true" : "false"]);
2490
2555
  if (endpoint) tags.push(["u", endpoint]);
2491
2556
  if (fingerprint) tags.push(["k", fingerprint]);
2557
+ if (privateRecipients) {
2558
+ if (!Array.isArray(privateRecipients)) throw new Error("privateRecipients must be an array");
2559
+ privateRecipients.forEach((cipher) => {
2560
+ if (typeof cipher !== "string") throw new Error("privateRecipients entries must be strings");
2561
+ tags.push(["privateRecipients", cipher]);
2562
+ });
2563
+ }
2492
2564
  const event = {
2493
2565
  kind: KINDS.SERVICE_RECORD,
2494
2566
  created_at: Math.floor(Date.now() / 1e3),
2495
2567
  tags,
2496
- content: `NCC-02 Service Record for ${serviceId}`,
2497
- pubkey: this.pk
2568
+ content: `NCC-02 Service Record for ${serviceId}`
2498
2569
  };
2499
- return finalizeEvent(event, this.sk);
2570
+ return this._finalizeEvent(event);
2500
2571
  }
2501
2572
  /**
2502
2573
  * Creates a signed Certificate Attestation (Kind 30060).
@@ -2507,7 +2578,7 @@ var NCC02Builder = class {
2507
2578
  * @param {string} [options.level='verified'] - The 'lvl' tag level.
2508
2579
  * @param {number} [options.validDays=30] - Validity in days.
2509
2580
  */
2510
- createAttestation(options) {
2581
+ async createAttestation(options) {
2511
2582
  const { subjectPubkey, serviceId, serviceEventId, level = "verified", validDays = 30 } = options;
2512
2583
  if (!subjectPubkey) throw new Error("subjectPubkey is required");
2513
2584
  if (!serviceId) throw new Error("serviceId is required");
@@ -2526,10 +2597,9 @@ var NCC02Builder = class {
2526
2597
  ["nbf", now2.toString()],
2527
2598
  ["exp", expiry.toString()]
2528
2599
  ],
2529
- content: "NCC-02 Attestation",
2530
- pubkey: this.pk
2600
+ content: "NCC-02 Attestation"
2531
2601
  };
2532
- return finalizeEvent(event, this.sk);
2602
+ return this._finalizeEvent(event);
2533
2603
  }
2534
2604
  /**
2535
2605
  * Creates a signed Revocation (Kind 30061).
@@ -2537,7 +2607,7 @@ var NCC02Builder = class {
2537
2607
  * @param {string} options.attestationId - The 'e' tag referencing the attestation.
2538
2608
  * @param {string} [options.reason=''] - Optional reason.
2539
2609
  */
2540
- createRevocation(options) {
2610
+ async createRevocation(options) {
2541
2611
  const { attestationId, reason = "" } = options;
2542
2612
  if (!attestationId) throw new Error("attestationId (e tag) is required");
2543
2613
  const tags = [["e", attestationId]];
@@ -2546,15 +2616,22 @@ var NCC02Builder = class {
2546
2616
  kind: KINDS.REVOCATION,
2547
2617
  created_at: Math.floor(Date.now() / 1e3),
2548
2618
  tags,
2549
- content: "NCC-02 Revocation",
2550
- pubkey: this.pk
2619
+ content: "NCC-02 Revocation"
2551
2620
  };
2552
- return finalizeEvent(event, this.sk);
2621
+ return this._finalizeEvent(event);
2553
2622
  }
2554
2623
  };
2555
2624
  function verifyNCC02Event(event) {
2556
2625
  return verifyEvent(event);
2557
2626
  }
2627
+ function isExpired(event) {
2628
+ if (!event || !Array.isArray(event.tags)) return false;
2629
+ const expTag = event.tags.find((tag) => tag[0] === "exp");
2630
+ if (!expTag) return false;
2631
+ const expiry = parseInt(expTag[1], 10);
2632
+ if (Number.isNaN(expiry)) return false;
2633
+ return expiry <= Math.floor(Date.now() / 1e3);
2634
+ }
2558
2635
 
2559
2636
  // node_modules/@scure/base/lib/esm/index.js
2560
2637
  function assertNumber(n) {
@@ -7626,6 +7703,136 @@ async function validateEvent22(event, url, method, body) {
7626
7703
  return true;
7627
7704
  }
7628
7705
 
7706
+ // src/privacy.js
7707
+ var PRIVATE_TAG = "private";
7708
+ var PRIVATE_RECIPIENTS_TAG = "privateRecipients";
7709
+ var HEX_REGEX = /^[0-9a-f]{64}$/i;
7710
+ function normalizeHexPubkey(value) {
7711
+ if (typeof value !== "string") {
7712
+ throw new Error("Pubkey must be a string");
7713
+ }
7714
+ const lower = value.toLowerCase();
7715
+ if (HEX_REGEX.test(lower)) {
7716
+ return lower;
7717
+ }
7718
+ try {
7719
+ const decoded = nip19_exports.decode(value);
7720
+ if (decoded.type === "npub" && typeof decoded.data === "string") {
7721
+ return decoded.data.toLowerCase();
7722
+ }
7723
+ } catch {
7724
+ }
7725
+ throw new Error("Unsupported pubkey format");
7726
+ }
7727
+ function toNpub(hexPubkey) {
7728
+ return nip19_exports.npubEncode(hexPubkey);
7729
+ }
7730
+ function toUint8ArrayKey(key) {
7731
+ if (typeof key === "string") {
7732
+ return hexToBytes2(key);
7733
+ }
7734
+ if (key instanceof Uint8Array) {
7735
+ return key;
7736
+ }
7737
+ throw new Error("Private key must be a hex string or Uint8Array");
7738
+ }
7739
+ function createNip44Encryptor(owner) {
7740
+ if (typeof owner === "string" || owner instanceof Uint8Array) {
7741
+ const ownerKeyBytes = toUint8ArrayKey(owner);
7742
+ return async (recipientHex, plaintext) => {
7743
+ const conversationKey = nip44_exports.getConversationKey(ownerKeyBytes, recipientHex);
7744
+ return nip44_exports.encrypt(plaintext, conversationKey);
7745
+ };
7746
+ }
7747
+ if (owner && typeof owner === "object") {
7748
+ if (typeof owner.nip44Encrypt === "function") {
7749
+ const encryptFn = owner.nip44Encrypt.bind(owner);
7750
+ return (recipientHex, plaintext) => Promise.resolve(encryptFn(recipientHex, plaintext));
7751
+ }
7752
+ if (owner.nip04 && typeof owner.nip04.encrypt === "function") {
7753
+ const encryptFn = owner.nip04.encrypt.bind(owner.nip04);
7754
+ return (recipientHex, plaintext) => Promise.resolve(encryptFn(recipientHex, plaintext));
7755
+ }
7756
+ }
7757
+ throw new Error("Unsupported owner signer; must be private key or NIP-44/NIP-04 capable signer");
7758
+ }
7759
+ function createNip44Decryptor(recipient) {
7760
+ if (typeof recipient === "string" || recipient instanceof Uint8Array) {
7761
+ const recipientKeyBytes = toUint8ArrayKey(recipient);
7762
+ return async (ownerHex, ciphertext) => {
7763
+ const conversationKey = nip44_exports.getConversationKey(recipientKeyBytes, ownerHex);
7764
+ return nip44_exports.decrypt(ciphertext, conversationKey);
7765
+ };
7766
+ }
7767
+ if (recipient && typeof recipient === "object") {
7768
+ if (typeof recipient.nip44Decrypt === "function") {
7769
+ const decryptFn = recipient.nip44Decrypt.bind(recipient);
7770
+ return (ownerHex, ciphertext) => Promise.resolve(decryptFn(ownerHex, ciphertext));
7771
+ }
7772
+ if (recipient.nip04 && typeof recipient.nip04.decrypt === "function") {
7773
+ const decryptFn = recipient.nip04.decrypt.bind(recipient.nip04);
7774
+ return (ownerHex, ciphertext) => Promise.resolve(decryptFn(ownerHex, ciphertext));
7775
+ }
7776
+ }
7777
+ throw new Error("Unsupported recipient signer; must be private key or NIP-44/NIP-04 capable signer");
7778
+ }
7779
+ async function resolveRecipientPubkey(recipient) {
7780
+ if (typeof recipient === "string" || recipient instanceof Uint8Array) {
7781
+ return normalizeHexPubkey(getPublicKey(toUint8ArrayKey(recipient)));
7782
+ }
7783
+ if (recipient && typeof recipient.getPublicKey === "function") {
7784
+ const pubkey = await recipient.getPublicKey();
7785
+ return normalizeHexPubkey(pubkey);
7786
+ }
7787
+ throw new Error("Recipient must provide a private key or a NIP signer with getPublicKey()");
7788
+ }
7789
+ function parsePrivateFlag(tags) {
7790
+ if (!Array.isArray(tags)) return null;
7791
+ const tag = tags.find((t) => Array.isArray(t) && t[0] === PRIVATE_TAG);
7792
+ if (!tag || typeof tag[1] !== "string") return null;
7793
+ const normalized = tag[1].toLowerCase();
7794
+ if (normalized === "true") return true;
7795
+ if (normalized === "false") return false;
7796
+ return null;
7797
+ }
7798
+ function collectPrivateRecipients(tags) {
7799
+ if (!Array.isArray(tags)) return [];
7800
+ return tags.filter((t) => Array.isArray(t) && t[0] === PRIVATE_RECIPIENTS_TAG && typeof t[1] === "string").map((t) => t[1]);
7801
+ }
7802
+ async function encryptPrivateRecipients(ownerPrivateKey, recipients) {
7803
+ if (!Array.isArray(recipients)) {
7804
+ throw new Error("recipients must be an array of pubkeys");
7805
+ }
7806
+ const encryptor = createNip44Encryptor(ownerPrivateKey);
7807
+ const encrypted = [];
7808
+ for (const recipient of recipients) {
7809
+ const recipientHex = normalizeHexPubkey(recipient);
7810
+ const recipientNpub = toNpub(recipientHex);
7811
+ encrypted.push(await encryptor(recipientHex, recipientNpub));
7812
+ }
7813
+ return encrypted;
7814
+ }
7815
+ async function decryptPrivateRecipient(ciphertext, ownerPubkey, recipientPrivateKey) {
7816
+ const ownerHex = normalizeHexPubkey(ownerPubkey);
7817
+ const decryptor = createNip44Decryptor(recipientPrivateKey);
7818
+ return decryptor(ownerHex, ciphertext);
7819
+ }
7820
+ async function isPrivateRecipientAuthorized(privateRecipients, ownerPubkey, recipientPrivateKey) {
7821
+ if (!Array.isArray(privateRecipients) || privateRecipients.length === 0) return false;
7822
+ const ownerHex = normalizeHexPubkey(ownerPubkey);
7823
+ const recipientPubkey = await resolveRecipientPubkey(recipientPrivateKey);
7824
+ const expectedNpub = toNpub(normalizeHexPubkey(recipientPubkey));
7825
+ const decryptor = createNip44Decryptor(recipientPrivateKey);
7826
+ for (const ciphertext of privateRecipients) {
7827
+ try {
7828
+ const decrypted = await decryptor(ownerHex, ciphertext);
7829
+ if (decrypted === expectedNpub) return true;
7830
+ } catch {
7831
+ }
7832
+ }
7833
+ return false;
7834
+ }
7835
+
7629
7836
  // src/resolver.js
7630
7837
  var NCC02Error = class extends Error {
7631
7838
  /**
@@ -7685,6 +7892,27 @@ var NCC02Resolver = class {
7685
7892
  });
7686
7893
  });
7687
7894
  }
7895
+ /**
7896
+ * Returns the first event sorted by freshness (newest created_at, tie broken by id).
7897
+ * @param {import('nostr-tools').Event[]} events
7898
+ * @returns {import('nostr-tools').Event|null}
7899
+ */
7900
+ _freshestEvent(events) {
7901
+ if (!events || !events.length) return null;
7902
+ return events.sort((a, b) => {
7903
+ if (b.created_at !== a.created_at) return b.created_at - a.created_at;
7904
+ return a.id.localeCompare(b.id);
7905
+ })[0];
7906
+ }
7907
+ /**
7908
+ * Query helper that returns only the freshest event matching the filter.
7909
+ * @param {import('nostr-tools').Filter} filter
7910
+ * @returns {Promise<import('nostr-tools').Event | null>}
7911
+ */
7912
+ async _queryFreshest(filter) {
7913
+ const events = await this._query(filter);
7914
+ return this._freshestEvent(events);
7915
+ }
7688
7916
  /**
7689
7917
  * Resolves a service for a given pubkey and service identifier.
7690
7918
  *
@@ -7694,8 +7922,8 @@ var NCC02Resolver = class {
7694
7922
  * @param {boolean} [options.requireAttestation=false] - If true, fails if no trusted attestation is found.
7695
7923
  * @param {string} [options.minLevel=null] - Minimum trust level ('self', 'verified', 'hardened').
7696
7924
  * @param {string} [options.standard='nostr-service-trust-v0.1'] - Expected trust standard.
7697
- * @throws {NCC02Error} If verification or policy checks fail.
7698
- * @returns {Promise<ResolvedService>} The verified service details.
7925
+ * @throws {NCC02Error} If verification or policy checks fail.
7926
+ * @returns {Promise<ServiceStatus>} The service status including trust metadata.
7699
7927
  */
7700
7928
  async resolve(pubkey, serviceId, options = {}) {
7701
7929
  const {
@@ -7703,9 +7931,9 @@ var NCC02Resolver = class {
7703
7931
  minLevel = null,
7704
7932
  standard = "nostr-service-trust-v0.1"
7705
7933
  } = options;
7706
- let serviceEvents;
7934
+ let serviceEvent;
7707
7935
  try {
7708
- serviceEvents = await this._query({
7936
+ serviceEvent = await this._queryFreshest({
7709
7937
  kinds: [KINDS.SERVICE_RECORD],
7710
7938
  authors: [pubkey],
7711
7939
  "#d": [serviceId]
@@ -7713,18 +7941,18 @@ var NCC02Resolver = class {
7713
7941
  } catch (err) {
7714
7942
  throw new NCC02Error("RELAY_ERROR", `Failed to query relay for ${serviceId}`, err);
7715
7943
  }
7716
- if (!serviceEvents || !serviceEvents.length) {
7944
+ if (!serviceEvent) {
7717
7945
  throw new NCC02Error("NOT_FOUND", `No service record found for ${serviceId}`);
7718
7946
  }
7719
- const serviceEvent = serviceEvents.sort((a, b) => {
7720
- if (b.created_at !== a.created_at) return b.created_at - a.created_at;
7721
- return a.id.localeCompare(b.id);
7722
- })[0];
7723
7947
  if (!verifyEvent2(serviceEvent)) {
7724
7948
  throw new NCC02Error("INVALID_SIGNATURE", "Service record signature verification failed");
7725
7949
  }
7726
7950
  const serviceTags = Object.fromEntries(serviceEvent.tags);
7727
7951
  const now2 = Math.floor(Date.now() / 1e3);
7952
+ const privateFlag = parsePrivateFlag(serviceEvent.tags);
7953
+ if (privateFlag === null) {
7954
+ throw new NCC02Error("MALFORMED_RECORD", "Service record is missing required tag (private)");
7955
+ }
7728
7956
  if (!serviceTags.exp) {
7729
7957
  throw new NCC02Error("MALFORMED_RECORD", "Service record is missing required tag (exp)");
7730
7958
  }
@@ -7738,88 +7966,137 @@ var NCC02Resolver = class {
7738
7966
  if (exp < now2) {
7739
7967
  throw new NCC02Error("EXPIRED", "Service record has expired");
7740
7968
  }
7741
- const validAttestations = [];
7742
- if (requireAttestation || minLevel === "verified" || minLevel === "hardened") {
7743
- let attestations;
7744
- let revocations;
7745
- try {
7746
- [attestations, revocations] = await Promise.all([
7747
- this._query({ kinds: [KINDS.ATTESTATION], "#e": [serviceEvent.id] }),
7748
- this._query({ kinds: [KINDS.REVOCATION] })
7749
- ]);
7750
- } catch (err) {
7751
- throw new NCC02Error("RELAY_ERROR", "Failed to query relay for attestations/revocations", err);
7752
- }
7753
- for (const att of attestations) {
7754
- if (this.trustedCAPubkeys.has(att.pubkey)) {
7755
- const attTags = Object.fromEntries(att.tags);
7756
- if (attTags.subj !== pubkey) continue;
7757
- if (attTags.srv !== serviceId) continue;
7758
- if (standard && attTags.std !== standard) continue;
7759
- if (minLevel && !this._isLevelSufficient(attTags.lvl, minLevel)) continue;
7760
- if (this._isAttestationValid(att, attTags, revocations)) {
7761
- validAttestations.push({
7762
- pubkey: att.pubkey,
7763
- level: attTags.lvl,
7764
- eventId: att.id
7765
- });
7766
- }
7767
- }
7768
- }
7769
- if (requireAttestation && validAttestations.length === 0) {
7770
- throw new NCC02Error("POLICY_FAILURE", `No trusted attestations meet the required policy for ${serviceId}`);
7771
- }
7969
+ let trustData;
7970
+ try {
7971
+ trustData = await this._buildTrustData(serviceEvent, { pubkey, serviceId, standard, minLevel });
7972
+ } catch (err) {
7973
+ throw new NCC02Error("RELAY_ERROR", "Failed to query relay for attestations/revocations", err);
7974
+ }
7975
+ if (requireAttestation && trustData.validAttestations.length === 0) {
7976
+ throw new NCC02Error("POLICY_FAILURE", `No trusted attestations meet the required policy for ${serviceId}`);
7772
7977
  }
7773
7978
  return {
7774
7979
  endpoint: serviceTags.u,
7775
7980
  fingerprint: serviceTags.k,
7776
7981
  expiry: exp,
7777
- attestations: validAttestations,
7982
+ attestations: trustData.validAttestations,
7983
+ attestationCount: trustData.validAttestations.length,
7984
+ isRevoked: trustData.isRevoked,
7985
+ isPrivate: privateFlag,
7986
+ privateRecipients: collectPrivateRecipients(serviceEvent.tags),
7778
7987
  eventId: serviceEvent.id,
7779
- pubkey: serviceEvent.pubkey
7988
+ pubkey: serviceEvent.pubkey,
7989
+ serviceEvent
7780
7990
  };
7781
7991
  }
7782
7992
  /**
7783
- * @param {string | undefined} actual
7784
- * @param {string} required
7993
+ * @param {any} serviceEvent
7994
+ * @param {Object} options
7995
+ * @param {string} options.pubkey
7996
+ * @param {string} options.serviceId
7997
+ * @param {string|null} options.standard
7998
+ * @param {string|null} options.minLevel
7785
7999
  */
7786
- _isLevelSufficient(actual, required) {
7787
- const levels = { "self": 0, "verified": 1, "hardened": 2 };
7788
- const actualVal = actual ? levels[actual] ?? -1 : -1;
7789
- const requiredVal = levels[required] ?? 0;
7790
- return actualVal >= requiredVal;
8000
+ async _buildTrustData(serviceEvent, options) {
8001
+ const attestations = await this._query({
8002
+ kinds: [KINDS.ATTESTATION],
8003
+ "#e": [serviceEvent.id]
8004
+ });
8005
+ const attestationIds = attestations.map((att) => att.id);
8006
+ let revocations = [];
8007
+ if (attestationIds.length) {
8008
+ revocations = await this._query({
8009
+ kinds: [KINDS.REVOCATION],
8010
+ "#e": attestationIds
8011
+ });
8012
+ }
8013
+ const revocationIndex = this._groupValidRevocations(revocations);
8014
+ const validAttestations = [];
8015
+ let isRevoked = false;
8016
+ for (const att of attestations) {
8017
+ if (!this.trustedCAPubkeys.has(att.pubkey)) continue;
8018
+ const attTags = Object.fromEntries(att.tags);
8019
+ if (attTags.subj !== options.pubkey) continue;
8020
+ if (attTags.srv !== options.serviceId) continue;
8021
+ if (options.standard && attTags.std !== options.standard) continue;
8022
+ const { valid, revoked } = this._evaluateAttestation(att, attTags, revocationIndex[att.id]);
8023
+ if (revoked) {
8024
+ isRevoked = true;
8025
+ continue;
8026
+ }
8027
+ if (!valid) continue;
8028
+ if (options.minLevel && !this._isLevelSufficient(attTags.lvl, options.minLevel)) continue;
8029
+ validAttestations.push({
8030
+ pubkey: att.pubkey,
8031
+ level: attTags.lvl,
8032
+ eventId: att.id
8033
+ });
8034
+ }
8035
+ return { validAttestations, isRevoked };
7791
8036
  }
7792
8037
  /**
7793
- * @param {any} att
7794
- * @param {any} tags
7795
- * @param {any[]} revocations
8038
+ * @param {any[]} revocations
8039
+ * @returns {Record<string, any[]>}
8040
+ */
8041
+ /**
8042
+ * @param {any[]} revocations
8043
+ * @returns {Record<string, any[]>}
7796
8044
  */
7797
- _isAttestationValid(att, tags, revocations) {
7798
- if (!verifyEvent2(att)) return false;
8045
+ _groupValidRevocations(revocations) {
8046
+ const indexed = {};
8047
+ for (const rev of revocations) {
8048
+ if (!verifyEvent2(rev)) continue;
8049
+ const tags = Object.fromEntries(rev.tags);
8050
+ const targetId = tags.e;
8051
+ if (!targetId) continue;
8052
+ if (!indexed[targetId]) indexed[targetId] = [];
8053
+ indexed[targetId].push(rev);
8054
+ }
8055
+ return indexed;
8056
+ }
8057
+ /**
8058
+ * @param {any} att
8059
+ * @param {Record<string, string>} tags
8060
+ * @param {any[]} revocations
8061
+ */
8062
+ /**
8063
+ * @param {any} att
8064
+ * @param {Record<string, string>} tags
8065
+ * @param {any[]} [revocations]
8066
+ */
8067
+ _evaluateAttestation(att, tags, revocations = []) {
8068
+ for (const rev of revocations) {
8069
+ if (rev.pubkey === att.pubkey) {
8070
+ return { valid: false, revoked: true };
8071
+ }
8072
+ }
8073
+ if (!verifyEvent2(att)) return { valid: false, revoked: false };
7799
8074
  const now2 = Math.floor(Date.now() / 1e3);
7800
8075
  if (tags.nbf) {
7801
- const nbf = parseInt(tags.nbf);
7802
- if (isNaN(nbf) || nbf > now2) return false;
8076
+ const nbf = parseInt(tags.nbf, 10);
8077
+ if (isNaN(nbf) || nbf > now2) return { valid: false, revoked: false };
7803
8078
  }
7804
8079
  if (tags.exp) {
7805
- const exp = parseInt(tags.exp);
7806
- if (isNaN(exp) || exp < now2) return false;
8080
+ const exp = parseInt(tags.exp, 10);
8081
+ if (isNaN(exp) || exp < now2) return { valid: false, revoked: false };
7807
8082
  }
7808
- for (const rev of revocations) {
7809
- const revTags = Object.fromEntries(rev.tags);
7810
- if (revTags.e === att.id && rev.pubkey === att.pubkey) {
7811
- if (verifyEvent2(rev)) {
7812
- return false;
7813
- }
7814
- }
7815
- }
7816
- return true;
8083
+ return { valid: true, revoked: false };
8084
+ }
8085
+ /**
8086
+ * @param {string | undefined} actual
8087
+ * @param {string} required
8088
+ */
8089
+ _isLevelSufficient(actual, required) {
8090
+ const levels = { "self": 0, "verified": 1, "hardened": 2 };
8091
+ const actualVal = actual ? levels[actual] ?? -1 : -1;
8092
+ const requiredVal = levels[required] ?? 0;
8093
+ return actualVal >= requiredVal;
7817
8094
  }
7818
8095
  /**
7819
8096
  * Verifies that the actual fingerprint found during transport-level connection
7820
8097
  * matches the one declared in the signed service record.
7821
8098
  *
7822
- * @param {ResolvedService} resolved - The object returned by resolve().
8099
+ * @param {ServiceStatus} resolved - The object returned by resolve().
7823
8100
  * @param {string} actualFingerprint - The fingerprint obtained from the service.
7824
8101
  * @returns {boolean}
7825
8102
  */
@@ -7876,6 +8153,12 @@ export {
7876
8153
  NCC02Builder,
7877
8154
  NCC02Error,
7878
8155
  NCC02Resolver,
8156
+ collectPrivateRecipients,
8157
+ decryptPrivateRecipient,
8158
+ encryptPrivateRecipients,
8159
+ isExpired,
8160
+ isPrivateRecipientAuthorized,
8161
+ parsePrivateFlag,
7879
8162
  verifyNCC02Event
7880
8163
  };
7881
8164
  /*! Bundled license information:
package/dist/models.d.ts CHANGED
@@ -3,6 +3,12 @@
3
3
  * @param {any} event
4
4
  */
5
5
  export function verifyNCC02Event(event: any): event is import("nostr-tools/core").VerifiedEvent;
6
+ /**
7
+ * Checks whether an NCC event has expired based on its 'exp' tag.
8
+ * @param {any} event
9
+ * @returns {boolean}
10
+ */
11
+ export function isExpired(event: any): boolean;
6
12
  export namespace KINDS {
7
13
  let SERVICE_RECORD: number;
8
14
  let ATTESTATION: number;
@@ -13,11 +19,22 @@ export namespace KINDS {
13
19
  */
14
20
  export class NCC02Builder {
15
21
  /**
16
- * @param {string | Uint8Array} privateKey - The private key to sign events with.
22
+ * @param {string | Uint8Array | NostrSigner} signer - Raw private key or asynchronous signer.
23
+ */
24
+ constructor(signer: string | Uint8Array | NostrSigner);
25
+ signer: NostrSigner;
26
+ _pubkeyPromise: Promise<string>;
27
+ _pubkey: string;
28
+ _getPublicKey(): Promise<string>;
29
+ /**
30
+ * @param {any} event
31
+ */
32
+ _finalizeEvent(event: any): Promise<any>;
33
+ /**
34
+ * @param {any} signer
35
+ * @returns {NostrSigner}
17
36
  */
18
- constructor(privateKey: string | Uint8Array);
19
- sk: Uint8Array<ArrayBufferLike>;
20
- pk: string;
37
+ _normalizeSigner(signer: any): NostrSigner;
21
38
  /**
22
39
  * Creates a signed Service Record (Kind 30059).
23
40
  * @param {Object} options
@@ -25,13 +42,17 @@ export class NCC02Builder {
25
42
  * @param {string} [options.endpoint] - The 'u' tag URI.
26
43
  * @param {string} [options.fingerprint] - The 'k' tag fingerprint.
27
44
  * @param {number} [options.expiryDays=14] - Expiry in days.
45
+ * @param {boolean} [options.isPrivate=false] - Whether the service is private (adds required `private` tag).
46
+ * @param {string[]} [options.privateRecipients] - Optional encrypted ciphertexts for authorized recipients.
28
47
  */
29
48
  createServiceRecord(options: {
30
49
  serviceId: string;
31
50
  endpoint?: string;
32
51
  fingerprint?: string;
33
52
  expiryDays?: number;
34
- }): import("nostr-tools/core").VerifiedEvent;
53
+ isPrivate?: boolean;
54
+ privateRecipients?: string[];
55
+ }): Promise<any>;
35
56
  /**
36
57
  * Creates a signed Certificate Attestation (Kind 30060).
37
58
  * @param {Object} options
@@ -47,7 +68,7 @@ export class NCC02Builder {
47
68
  serviceEventId: string;
48
69
  level?: string;
49
70
  validDays?: number;
50
- }): import("nostr-tools/core").VerifiedEvent;
71
+ }): Promise<any>;
51
72
  /**
52
73
  * Creates a signed Revocation (Kind 30061).
53
74
  * @param {Object} options
@@ -57,5 +78,10 @@ export class NCC02Builder {
57
78
  createRevocation(options: {
58
79
  attestationId: string;
59
80
  reason?: string;
60
- }): import("nostr-tools/core").VerifiedEvent;
81
+ }): Promise<any>;
61
82
  }
83
+ export type NostrSigner = {
84
+ getPublicKey: () => Promise<string>;
85
+ signEvent: (event: any) => Promise<any>;
86
+ decryptEvent?: (event: any) => Promise<any>;
87
+ };