mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
orderDecryptors compared candidate `priority` before the `preferLegacy` flag.
The signer registers its nip44 candidate at an earlier priority (-20) than
nip04 (-10), so priority always decided the order and preferLegacy was dead
code — every kind-4 DM tried nip44 first, which fails on the `?iv=` NIP-04
ciphertext ("invalid base64: Unknown letter ?") before falling back to nip04.
That doubled the serialized extension round-trips per message on cold inbox
load and spammed the console.
Move the legacy preference (and it already had the explicit-hint check) above
the priority comparison so kind-4 messages attempt nip04 first. Gift-wrap
(preferGiftWrap) and same-scheme priority ordering are unchanged.
Regression: tests/dm-decryptor-order.test.mjs.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
742 lines
19 KiB
JavaScript
742 lines
19 KiB
JavaScript
// js/dmDecryptor.js
|
|
|
|
import { normalizeActorKey } from "./nostr/watchHistory.js";
|
|
import { HEX64_REGEX } from "./utils/hex.js";
|
|
|
|
|
|
|
|
const SUPPORTED_KINDS = new Set([4, 1059]);
|
|
|
|
const GIFT_WRAP_KIND = 1059;
|
|
|
|
function cloneEvent(event) {
|
|
if (!event || typeof event !== "object") {
|
|
return null;
|
|
}
|
|
|
|
const cloned = { ...event };
|
|
if (Array.isArray(event.tags)) {
|
|
cloned.tags = event.tags.map((tag) =>
|
|
Array.isArray(tag) ? [...tag] : tag,
|
|
);
|
|
}
|
|
|
|
return cloned;
|
|
}
|
|
|
|
function normalizeCreatedAt(value) {
|
|
const numeric = Number(value);
|
|
if (!Number.isFinite(numeric) || numeric <= 0) {
|
|
return null;
|
|
}
|
|
|
|
return Math.floor(numeric);
|
|
}
|
|
|
|
function normalizeInnerMessage(event) {
|
|
if (!event || typeof event !== "object") {
|
|
return null;
|
|
}
|
|
|
|
const normalized = cloneEvent(event) || {};
|
|
normalized.content = typeof event.content === "string" ? event.content : "";
|
|
|
|
if (Number.isFinite(event.kind)) {
|
|
normalized.kind = event.kind;
|
|
}
|
|
|
|
const createdAt = normalizeCreatedAt(event.created_at);
|
|
if (createdAt !== null) {
|
|
normalized.created_at = createdAt;
|
|
} else if ("created_at" in normalized) {
|
|
delete normalized.created_at;
|
|
}
|
|
|
|
if (Array.isArray(event.tags)) {
|
|
normalized.tags = event.tags.map((tag) =>
|
|
Array.isArray(tag) ? [...tag] : tag,
|
|
);
|
|
}
|
|
|
|
return normalized;
|
|
}
|
|
|
|
function normalizeHex(candidate) {
|
|
if (typeof candidate !== "string") {
|
|
return "";
|
|
}
|
|
|
|
const trimmed = candidate.trim().toLowerCase();
|
|
if (!HEX64_REGEX.test(trimmed)) {
|
|
return "";
|
|
}
|
|
|
|
return trimmed;
|
|
}
|
|
|
|
function normalizeScheme(scheme) {
|
|
if (typeof scheme !== "string") {
|
|
return "";
|
|
}
|
|
|
|
const normalized = scheme.trim().toLowerCase();
|
|
if (!normalized) {
|
|
return "";
|
|
}
|
|
|
|
if (normalized === "nip44_v2" || normalized === "nip44-v2") {
|
|
return "nip44_v2";
|
|
}
|
|
if (normalized === "nip44" || normalized === "nip-44") {
|
|
return "nip44";
|
|
}
|
|
if (normalized === "nip04" || normalized === "nip-04") {
|
|
return "nip04";
|
|
}
|
|
return normalized;
|
|
}
|
|
|
|
function parseEncryptionHints(rawTags) {
|
|
const tags = Array.isArray(rawTags) ? rawTags : [];
|
|
const algorithms = [];
|
|
|
|
for (const tag of tags) {
|
|
if (!Array.isArray(tag) || tag.length < 2) {
|
|
continue;
|
|
}
|
|
if (tag[0] !== "encrypted") {
|
|
continue;
|
|
}
|
|
|
|
for (let index = 1; index < tag.length; index += 1) {
|
|
const value = typeof tag[index] === "string" ? tag[index].trim() : "";
|
|
if (!value) {
|
|
continue;
|
|
}
|
|
|
|
const parts = value.split(/\s+/).filter(Boolean);
|
|
for (const part of parts) {
|
|
const normalized = normalizeScheme(part);
|
|
if (normalized && !algorithms.includes(normalized)) {
|
|
algorithms.push(normalized);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return { algorithms };
|
|
}
|
|
|
|
function collectRelayHints(tag) {
|
|
const hints = [];
|
|
if (!Array.isArray(tag)) {
|
|
return hints;
|
|
}
|
|
|
|
for (let index = 2; index < tag.length; index += 1) {
|
|
const candidate = typeof tag[index] === "string" ? tag[index].trim() : "";
|
|
if (candidate) {
|
|
hints.push(candidate);
|
|
}
|
|
}
|
|
|
|
return hints;
|
|
}
|
|
|
|
function collectRecipients(rawTags) {
|
|
const tags = Array.isArray(rawTags) ? rawTags : [];
|
|
const recipients = [];
|
|
const seen = new Map();
|
|
|
|
for (const tag of tags) {
|
|
if (!Array.isArray(tag) || tag[0] !== "p") {
|
|
continue;
|
|
}
|
|
|
|
const pubkey = normalizeHex(tag[1]);
|
|
if (!pubkey) {
|
|
continue;
|
|
}
|
|
|
|
const hints = collectRelayHints(tag);
|
|
|
|
if (seen.has(pubkey)) {
|
|
const existing = seen.get(pubkey);
|
|
for (const hint of hints) {
|
|
if (!existing.relayHints.includes(hint)) {
|
|
existing.relayHints.push(hint);
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
|
|
const recipient = {
|
|
pubkey,
|
|
relayHints: hints,
|
|
role: "recipient",
|
|
};
|
|
seen.set(pubkey, recipient);
|
|
recipients.push(recipient);
|
|
}
|
|
|
|
return recipients;
|
|
}
|
|
|
|
function ensureDecryptCandidates(rawCandidates) {
|
|
if (!Array.isArray(rawCandidates)) {
|
|
return [];
|
|
}
|
|
|
|
return rawCandidates
|
|
.map((candidate) => {
|
|
if (!candidate || typeof candidate !== "object") {
|
|
return null;
|
|
}
|
|
|
|
const decrypt = candidate.decrypt;
|
|
if (typeof decrypt !== "function") {
|
|
return null;
|
|
}
|
|
|
|
const scheme = normalizeScheme(candidate.scheme) || "";
|
|
const supportsGiftWrap = candidate.supportsGiftWrap === true || scheme.startsWith("nip44");
|
|
const priority = Number.isFinite(candidate.priority) ? candidate.priority : 0;
|
|
|
|
return {
|
|
scheme: scheme || "",
|
|
decrypt,
|
|
priority,
|
|
supportsGiftWrap,
|
|
source: candidate.source || "",
|
|
};
|
|
})
|
|
.filter(Boolean);
|
|
}
|
|
|
|
function orderDecryptors(candidates, hints, { preferGiftWrap = false, preferLegacy = false } = {}) {
|
|
if (!candidates.length) {
|
|
return [];
|
|
}
|
|
|
|
const algorithms = Array.isArray(hints?.algorithms) ? hints.algorithms : [];
|
|
const hasHints = algorithms.length > 0;
|
|
const algorithmRanks = new Map();
|
|
algorithms.forEach((algorithm, index) => {
|
|
if (!algorithmRanks.has(algorithm)) {
|
|
algorithmRanks.set(algorithm, index);
|
|
}
|
|
});
|
|
|
|
const baseSchemeRank = (scheme) => {
|
|
if (scheme === "nip44_v2" || scheme === "nip44") {
|
|
return 0;
|
|
}
|
|
if (scheme === "nip04") {
|
|
return 5;
|
|
}
|
|
return 10;
|
|
};
|
|
|
|
const desiredSchemeRank = (scheme) => {
|
|
if (algorithmRanks.has(scheme)) {
|
|
return algorithmRanks.get(scheme);
|
|
}
|
|
if (scheme === "nip44" && algorithmRanks.has("nip44_v2")) {
|
|
return algorithmRanks.get("nip44_v2");
|
|
}
|
|
if (scheme === "nip44_v2" && algorithmRanks.has("nip44")) {
|
|
return algorithmRanks.get("nip44");
|
|
}
|
|
return baseSchemeRank(scheme);
|
|
};
|
|
|
|
return [...candidates].sort((a, b) => {
|
|
// Explicit per-event encryption hints (the "encrypted" tag) always win, even
|
|
// for legacy kind-4 events: if the sender told us the scheme, honor it.
|
|
if (hasHints) {
|
|
const aHinted = desiredSchemeRank(a.scheme);
|
|
const bHinted = desiredSchemeRank(b.scheme);
|
|
if (aHinted !== bHinted) {
|
|
return aHinted - bHinted;
|
|
}
|
|
}
|
|
|
|
// Legacy (kind 4) default: with no explicit hint, kind-4 DMs are NIP-04, so
|
|
// try nip04 first and don't burn a (serialized, slow) nip-07 round-trip on
|
|
// nip44. This MUST outrank `priority` below — the signer registers its nip44
|
|
// candidate at a lower (earlier) priority than nip04, so leaving priority to
|
|
// decide made every kind-4 message attempt nip44 first (guaranteed "invalid
|
|
// base64" on the ?iv= ciphertext) before falling back to nip04. Only flips
|
|
// the nip04-vs-nip44 default; an explicit hint above already took over.
|
|
if (preferLegacy && !hasHints) {
|
|
const aL = a.scheme === "nip04" ? 0 : 1;
|
|
const bL = b.scheme === "nip04" ? 0 : 1;
|
|
if (aL !== bL) {
|
|
return aL - bL;
|
|
}
|
|
}
|
|
|
|
if (a.priority !== b.priority) {
|
|
return a.priority - b.priority;
|
|
}
|
|
|
|
if (preferGiftWrap) {
|
|
const aWrapScore = a.supportsGiftWrap ? 0 : 1;
|
|
const bWrapScore = b.supportsGiftWrap ? 0 : 1;
|
|
if (aWrapScore !== bWrapScore) {
|
|
return aWrapScore - bWrapScore;
|
|
}
|
|
}
|
|
|
|
const aRank = desiredSchemeRank(a.scheme);
|
|
const bRank = desiredSchemeRank(b.scheme);
|
|
if (aRank !== bRank) {
|
|
return aRank - bRank;
|
|
}
|
|
|
|
return 0;
|
|
});
|
|
}
|
|
|
|
function deriveDirection(actorPubkey, senderPubkey, recipients) {
|
|
const normalizedActor = normalizeHex(actorPubkey);
|
|
if (!normalizedActor) {
|
|
return "unknown";
|
|
}
|
|
|
|
if (normalizedActor === normalizeHex(senderPubkey)) {
|
|
return "outgoing";
|
|
}
|
|
|
|
if (Array.isArray(recipients)) {
|
|
for (const recipient of recipients) {
|
|
if (normalizeHex(recipient?.pubkey) === normalizedActor) {
|
|
return "incoming";
|
|
}
|
|
}
|
|
}
|
|
|
|
return "unknown";
|
|
}
|
|
|
|
function buildDecryptResult({
|
|
ok,
|
|
event,
|
|
message,
|
|
plaintext,
|
|
recipients,
|
|
senderPubkey,
|
|
actorPubkey,
|
|
decryptor,
|
|
scheme,
|
|
envelope = null,
|
|
errors = [],
|
|
}) {
|
|
const normalizedScheme = normalizeScheme(scheme || decryptor?.scheme || "");
|
|
const messageTimestamp =
|
|
message && Number.isFinite(message.created_at) ? message.created_at : null;
|
|
const eventTimestamp =
|
|
event && Number.isFinite(event.created_at) ? event.created_at : null;
|
|
const timestamp =
|
|
messageTimestamp ?? eventTimestamp ?? Date.now() / 1000;
|
|
|
|
return {
|
|
ok,
|
|
event: cloneEvent(event),
|
|
message: message ? cloneEvent(message) : null,
|
|
plaintext: typeof plaintext === "string" ? plaintext : null,
|
|
recipients: Array.isArray(recipients) ? recipients : [],
|
|
sender: senderPubkey
|
|
? {
|
|
pubkey: senderPubkey,
|
|
relayHints: [],
|
|
role: "sender",
|
|
}
|
|
: null,
|
|
actorPubkey: normalizeHex(actorPubkey),
|
|
decryptor: decryptor
|
|
? {
|
|
scheme: normalizeScheme(decryptor.scheme) || normalizedScheme || "",
|
|
source: decryptor.source || "",
|
|
}
|
|
: { scheme: normalizedScheme || "", source: "" },
|
|
scheme: normalizedScheme,
|
|
encryption_scheme: normalizedScheme,
|
|
envelope,
|
|
direction: deriveDirection(actorPubkey, senderPubkey, recipients),
|
|
timestamp: Number.isFinite(timestamp) ? timestamp : Date.now() / 1000,
|
|
errors,
|
|
};
|
|
}
|
|
|
|
function parseEventJson(serialized, stage) {
|
|
if (typeof serialized !== "string" || !serialized.trim()) {
|
|
const error = new Error("Decrypted payload was empty.");
|
|
error.code = "empty-payload";
|
|
error.stage = stage;
|
|
throw error;
|
|
}
|
|
|
|
try {
|
|
const parsed = JSON.parse(serialized);
|
|
if (!parsed || typeof parsed !== "object") {
|
|
const error = new Error("Decrypted payload was not a valid JSON event.");
|
|
error.code = "invalid-json";
|
|
error.stage = stage;
|
|
throw error;
|
|
}
|
|
return parsed;
|
|
} catch (error) {
|
|
const failure = new Error("Failed to parse decrypted payload as JSON.");
|
|
failure.code = "json-parse-failed";
|
|
failure.stage = stage;
|
|
failure.cause = error;
|
|
throw failure;
|
|
}
|
|
}
|
|
|
|
async function decryptGiftWrap(event, decryptors, actorPubkey) {
|
|
const ciphertext = typeof event?.content === "string" ? event.content : "";
|
|
const wrapPubkey = normalizeHex(event?.pubkey);
|
|
|
|
if (!ciphertext || !wrapPubkey) {
|
|
return buildDecryptResult({
|
|
ok: false,
|
|
event,
|
|
actorPubkey,
|
|
errors: [
|
|
{
|
|
stage: "wrap",
|
|
error: new Error("Gift wrap event is missing ciphertext or pubkey."),
|
|
},
|
|
],
|
|
});
|
|
}
|
|
|
|
const errors = [];
|
|
|
|
const attemptUnwrap = async (decryptor) => {
|
|
try {
|
|
const sealSerialized = await decryptor.decrypt(wrapPubkey, ciphertext, {
|
|
event,
|
|
stage: "wrap",
|
|
});
|
|
const seal = parseEventJson(sealSerialized, "wrap");
|
|
|
|
const sealCiphertext =
|
|
typeof seal?.content === "string" ? seal.content : "";
|
|
const sealPubkey = normalizeHex(seal?.pubkey);
|
|
if (!sealCiphertext || !sealPubkey) {
|
|
const error = new Error(
|
|
"Gift wrap seal is missing ciphertext or pubkey.",
|
|
);
|
|
error.code = "invalid-seal";
|
|
error.stage = "seal";
|
|
throw error;
|
|
}
|
|
|
|
const rumorSerialized = await decryptor.decrypt(
|
|
sealPubkey,
|
|
sealCiphertext,
|
|
{
|
|
event: seal,
|
|
stage: "seal",
|
|
},
|
|
);
|
|
const rumor = parseEventJson(rumorSerialized, "rumor");
|
|
const normalizedRumor = normalizeInnerMessage(rumor);
|
|
|
|
const plaintext =
|
|
typeof normalizedRumor?.content === "string"
|
|
? normalizedRumor.content
|
|
: "";
|
|
const senderPubkey = normalizeHex(normalizedRumor?.pubkey) || sealPubkey;
|
|
const recipients = collectRecipients(normalizedRumor?.tags);
|
|
|
|
return buildDecryptResult({
|
|
ok: true,
|
|
event,
|
|
message: normalizedRumor,
|
|
plaintext,
|
|
recipients,
|
|
senderPubkey,
|
|
actorPubkey,
|
|
decryptor,
|
|
scheme: decryptor.scheme || "nip44",
|
|
envelope: {
|
|
wrap: cloneEvent(event),
|
|
seal: cloneEvent(seal),
|
|
},
|
|
});
|
|
} catch (error) {
|
|
throw {
|
|
scheme: decryptor.scheme || "",
|
|
source: decryptor.source || "",
|
|
stage: error?.stage || "wrap",
|
|
error,
|
|
};
|
|
}
|
|
};
|
|
|
|
try {
|
|
return await Promise.any(decryptors.map(attemptUnwrap));
|
|
} catch (aggregateError) {
|
|
const aggregatedErrors = aggregateError.errors || [];
|
|
errors.push(...aggregatedErrors);
|
|
}
|
|
|
|
return buildDecryptResult({
|
|
ok: false,
|
|
event,
|
|
actorPubkey,
|
|
errors,
|
|
});
|
|
}
|
|
|
|
function raceOrdered(promises) {
|
|
if (!promises || promises.length === 0) {
|
|
return Promise.reject(new AggregateError([], "No promises to race"));
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
let pendingCount = promises.length;
|
|
const errors = new Array(promises.length);
|
|
const results = new Array(promises.length);
|
|
const states = new Array(promises.length).fill("pending");
|
|
let scanIndex = 0;
|
|
|
|
promises.forEach((p, i) => {
|
|
Promise.resolve(p).then(
|
|
(val) => {
|
|
states[i] = "fulfilled";
|
|
results[i] = val;
|
|
|
|
if (i === scanIndex) {
|
|
resolve(val);
|
|
}
|
|
},
|
|
(err) => {
|
|
states[i] = "rejected";
|
|
errors[i] = err;
|
|
pendingCount--;
|
|
|
|
if (i === scanIndex) {
|
|
while (scanIndex < promises.length) {
|
|
if (states[scanIndex] === "fulfilled") {
|
|
resolve(results[scanIndex]);
|
|
return;
|
|
}
|
|
if (states[scanIndex] === "rejected") {
|
|
scanIndex++;
|
|
} else {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (pendingCount === 0 && scanIndex >= promises.length) {
|
|
reject(new AggregateError(errors, "All promises rejected"));
|
|
}
|
|
},
|
|
);
|
|
});
|
|
});
|
|
}
|
|
|
|
async function decryptLegacyDm(event, decryptors, actorPubkey) {
|
|
const ciphertext = typeof event?.content === "string" ? event.content : "";
|
|
const senderPubkey = normalizeHex(event?.pubkey);
|
|
const hints = parseEncryptionHints(event?.tags);
|
|
const recipients = collectRecipients(event?.tags);
|
|
const normalizedActor = normalizeHex(actorPubkey);
|
|
|
|
if (!ciphertext || !senderPubkey) {
|
|
return buildDecryptResult({
|
|
ok: false,
|
|
event,
|
|
actorPubkey,
|
|
errors: [
|
|
{
|
|
stage: "content",
|
|
error: new Error("Direct message is missing ciphertext or pubkey."),
|
|
},
|
|
],
|
|
});
|
|
}
|
|
|
|
const ordered = orderDecryptors(decryptors, hints, { preferLegacy: true });
|
|
const errors = [];
|
|
|
|
const remoteCandidates = [];
|
|
const seenRemotes = new Set();
|
|
let selfCandidate = null;
|
|
const registerRemote = (candidate) => {
|
|
const normalized = normalizeHex(candidate);
|
|
if (!normalized || seenRemotes.has(normalized)) {
|
|
return;
|
|
}
|
|
seenRemotes.add(normalized);
|
|
// A NIP-04 conversation key is derived with the *other* party, so trying the
|
|
// actor's own key is normally a wasted (slow, serialized) extension
|
|
// round-trip. Defer self to the end so we only attempt it as a fallback —
|
|
// but keep it, because note-to-self DMs have no other counterparty.
|
|
if (normalizedActor && normalized === normalizedActor) {
|
|
selfCandidate = normalized;
|
|
return;
|
|
}
|
|
remoteCandidates.push(normalized);
|
|
};
|
|
|
|
if (normalizedActor && normalizedActor === senderPubkey) {
|
|
for (const recipient of recipients) {
|
|
registerRemote(recipient?.pubkey);
|
|
}
|
|
}
|
|
|
|
registerRemote(senderPubkey);
|
|
|
|
for (const recipient of recipients) {
|
|
registerRemote(recipient?.pubkey);
|
|
}
|
|
|
|
if (selfCandidate) {
|
|
remoteCandidates.push(selfCandidate);
|
|
}
|
|
|
|
const attemptDecryption = async (decryptor, remotePubkey) => {
|
|
try {
|
|
const plaintext = await decryptor.decrypt(remotePubkey, ciphertext, {
|
|
event,
|
|
stage: "content",
|
|
remotePubkey,
|
|
});
|
|
|
|
if (typeof plaintext === "string") {
|
|
const resolvedScheme =
|
|
normalizeScheme(hints.algorithms?.[0]) ||
|
|
normalizeScheme(decryptor.scheme) ||
|
|
"";
|
|
return buildDecryptResult({
|
|
ok: true,
|
|
event,
|
|
message: {
|
|
...cloneEvent(event),
|
|
content: plaintext,
|
|
},
|
|
plaintext,
|
|
recipients,
|
|
senderPubkey,
|
|
actorPubkey,
|
|
decryptor,
|
|
scheme: resolvedScheme,
|
|
});
|
|
}
|
|
throw new Error("Decrypted content was not a valid string.");
|
|
} catch (error) {
|
|
throw {
|
|
scheme: decryptor.scheme || "",
|
|
source: decryptor.source || "",
|
|
stage: "content",
|
|
remotePubkey,
|
|
error,
|
|
};
|
|
}
|
|
};
|
|
|
|
// Try candidates sequentially and STOP at the first success. Racing every
|
|
// (decryptor x remote) combination in parallel looked fast, but the nip-07
|
|
// extension serializes calls — so racing N combinations just queued N slow
|
|
// extension round-trips per message (the legacy DM history was ~12 extension
|
|
// calls per message). With nip04 ordered first and the remote party first, the
|
|
// first attempt normally succeeds, so this is ~1 call per message with no
|
|
// worse latency-to-first-success on the serialized extension.
|
|
for (const decryptor of ordered) {
|
|
for (const remotePubkey of remoteCandidates) {
|
|
try {
|
|
return await attemptDecryption(decryptor, remotePubkey);
|
|
} catch (failure) {
|
|
errors.push(failure);
|
|
}
|
|
}
|
|
}
|
|
|
|
return buildDecryptResult({
|
|
ok: false,
|
|
event,
|
|
actorPubkey,
|
|
errors,
|
|
});
|
|
}
|
|
|
|
export async function decryptDM(event, context = {}) {
|
|
if (!event || typeof event !== "object") {
|
|
return {
|
|
ok: false,
|
|
event: null,
|
|
actorPubkey: normalizeHex(context?.actorPubkey),
|
|
errors: [
|
|
{
|
|
stage: "input",
|
|
error: new Error("A valid event is required for DM decryption."),
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
const kind = Number.isFinite(event.kind) ? event.kind : null;
|
|
if (!SUPPORTED_KINDS.has(kind)) {
|
|
return {
|
|
ok: false,
|
|
event: cloneEvent(event),
|
|
actorPubkey: normalizeHex(context?.actorPubkey),
|
|
errors: [
|
|
{
|
|
stage: "input",
|
|
error: new Error("Unsupported DM event kind."),
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
const actorPubkey = normalizeActorKey(context?.actorPubkey);
|
|
const decryptors = ensureDecryptCandidates(context?.decryptors);
|
|
|
|
if (!decryptors.length) {
|
|
return {
|
|
ok: false,
|
|
event: cloneEvent(event),
|
|
actorPubkey,
|
|
errors: [
|
|
{
|
|
stage: "decryptor",
|
|
error: new Error("No decryptors are available for DM payloads."),
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
if (kind === GIFT_WRAP_KIND) {
|
|
const giftWrapDecryptors = orderDecryptors(decryptors, null, {
|
|
preferGiftWrap: true,
|
|
}).filter((candidate) => candidate.supportsGiftWrap);
|
|
|
|
if (!giftWrapDecryptors.length) {
|
|
return {
|
|
ok: false,
|
|
event: cloneEvent(event),
|
|
actorPubkey,
|
|
errors: [
|
|
{
|
|
stage: "decryptor",
|
|
error: new Error("Gift wrap events require a NIP-44 decryptor."),
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
return decryptGiftWrap(event, giftWrapDecryptors, actorPubkey);
|
|
}
|
|
|
|
return decryptLegacyDm(event, decryptors, actorPubkey);
|
|
}
|