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.mjs
CHANGED
|
@@ -2464,12 +2464,73 @@ var KINDS = {
|
|
|
2464
2464
|
};
|
|
2465
2465
|
var NCC02Builder = class {
|
|
2466
2466
|
/**
|
|
2467
|
-
* @param {string | Uint8Array}
|
|
2467
|
+
* @param {string | Uint8Array | NostrSigner} signer - Raw private key or asynchronous signer.
|
|
2468
2468
|
*/
|
|
2469
|
-
constructor(
|
|
2470
|
-
if (!
|
|
2471
|
-
this.
|
|
2472
|
-
this.
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
7698
|
-
|
|
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
|
|
7934
|
+
let serviceEvent;
|
|
7707
7935
|
try {
|
|
7708
|
-
|
|
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 (!
|
|
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
|
-
|
|
7742
|
-
|
|
7743
|
-
|
|
7744
|
-
|
|
7745
|
-
|
|
7746
|
-
|
|
7747
|
-
|
|
7748
|
-
|
|
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 {
|
|
7784
|
-
* @param {
|
|
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
|
-
|
|
7787
|
-
const
|
|
7788
|
-
|
|
7789
|
-
|
|
7790
|
-
|
|
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}
|
|
7794
|
-
* @
|
|
7795
|
-
|
|
8038
|
+
* @param {any[]} revocations
|
|
8039
|
+
* @returns {Record<string, any[]>}
|
|
8040
|
+
*/
|
|
8041
|
+
/**
|
|
8042
|
+
* @param {any[]} revocations
|
|
8043
|
+
* @returns {Record<string, any[]>}
|
|
7796
8044
|
*/
|
|
7797
|
-
|
|
7798
|
-
|
|
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
|
-
|
|
7809
|
-
|
|
7810
|
-
|
|
7811
|
-
|
|
7812
|
-
|
|
7813
|
-
|
|
7814
|
-
|
|
7815
|
-
}
|
|
7816
|
-
|
|
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 {
|
|
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}
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}):
|
|
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
|
-
}):
|
|
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
|
+
};
|