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/README.md +93 -21
- package/dist/index.cjs +375 -86
- package/dist/index.d.ts +1 -0
- package/dist/index.mjs +369 -86
- package/dist/models.d.ts +33 -7
- package/dist/privacy.d.ts +44 -0
- package/dist/resolver.d.ts +78 -14
- package/package.json +1 -1
- package/src/index.js +1 -0
- package/src/models.js +117 -22
- package/src/privacy.js +217 -0
- package/src/resolver.js +162 -88
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}
|
|
2507
|
+
* @param {string | Uint8Array | NostrSigner} signer - Raw private key or asynchronous signer.
|
|
2502
2508
|
*/
|
|
2503
|
-
constructor(
|
|
2504
|
-
if (!
|
|
2505
|
-
this.
|
|
2506
|
-
this.
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
7732
|
-
|
|
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
|
|
7974
|
+
let serviceEvent;
|
|
7741
7975
|
try {
|
|
7742
|
-
|
|
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 (!
|
|
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
|
-
|
|
7776
|
-
|
|
7777
|
-
|
|
7778
|
-
|
|
7779
|
-
|
|
7780
|
-
|
|
7781
|
-
|
|
7782
|
-
|
|
7783
|
-
]);
|
|
7784
|
-
} catch (err) {
|
|
7785
|
-
throw new NCC02Error("RELAY_ERROR", "Failed to query relay for attestations/revocations", err);
|
|
7786
|
-
}
|
|
7787
|
-
for (const att of attestations) {
|
|
7788
|
-
if (this.trustedCAPubkeys.has(att.pubkey)) {
|
|
7789
|
-
const attTags = Object.fromEntries(att.tags);
|
|
7790
|
-
if (attTags.subj !== pubkey) continue;
|
|
7791
|
-
if (attTags.srv !== serviceId) continue;
|
|
7792
|
-
if (standard && attTags.std !== standard) continue;
|
|
7793
|
-
if (minLevel && !this._isLevelSufficient(attTags.lvl, minLevel)) continue;
|
|
7794
|
-
if (this._isAttestationValid(att, attTags, revocations)) {
|
|
7795
|
-
validAttestations.push({
|
|
7796
|
-
pubkey: att.pubkey,
|
|
7797
|
-
level: attTags.lvl,
|
|
7798
|
-
eventId: att.id
|
|
7799
|
-
});
|
|
7800
|
-
}
|
|
7801
|
-
}
|
|
7802
|
-
}
|
|
7803
|
-
if (requireAttestation && validAttestations.length === 0) {
|
|
7804
|
-
throw new NCC02Error("POLICY_FAILURE", `No trusted attestations meet the required policy for ${serviceId}`);
|
|
7805
|
-
}
|
|
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 {
|
|
7818
|
-
* @param {
|
|
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
|
-
|
|
7821
|
-
const
|
|
7822
|
-
|
|
7823
|
-
|
|
7824
|
-
|
|
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}
|
|
7828
|
-
* @
|
|
7829
|
-
|
|
8078
|
+
* @param {any[]} revocations
|
|
8079
|
+
* @returns {Record<string, any[]>}
|
|
8080
|
+
*/
|
|
8081
|
+
/**
|
|
8082
|
+
* @param {any[]} revocations
|
|
8083
|
+
* @returns {Record<string, any[]>}
|
|
7830
8084
|
*/
|
|
7831
|
-
|
|
7832
|
-
|
|
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
|
-
|
|
7843
|
-
|
|
7844
|
-
|
|
7845
|
-
|
|
7846
|
-
|
|
7847
|
-
|
|
7848
|
-
|
|
7849
|
-
}
|
|
7850
|
-
|
|
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 {
|
|
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