2767 lines
79 KiB
JavaScript
2767 lines
79 KiB
JavaScript
var __defProp = Object.defineProperty;
|
|
var __export = (target, all) => {
|
|
for (var name in all)
|
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
};
|
|
|
|
// pure.ts
|
|
import { schnorr } from "@noble/curves/secp256k1";
|
|
import { bytesToHex as bytesToHex2 } from "@noble/hashes/utils";
|
|
|
|
// core.ts
|
|
var verifiedSymbol = Symbol("verified");
|
|
var isRecord = (obj) => obj instanceof Object;
|
|
function validateEvent(event) {
|
|
if (!isRecord(event))
|
|
return false;
|
|
if (typeof event.kind !== "number")
|
|
return false;
|
|
if (typeof event.content !== "string")
|
|
return false;
|
|
if (typeof event.created_at !== "number")
|
|
return false;
|
|
if (typeof event.pubkey !== "string")
|
|
return false;
|
|
if (!event.pubkey.match(/^[a-f0-9]{64}$/))
|
|
return false;
|
|
if (!Array.isArray(event.tags))
|
|
return false;
|
|
for (let i2 = 0; i2 < event.tags.length; i2++) {
|
|
let tag = event.tags[i2];
|
|
if (!Array.isArray(tag))
|
|
return false;
|
|
for (let j = 0; j < tag.length; j++) {
|
|
if (typeof tag[j] !== "string")
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
function sortEvents(events) {
|
|
return events.sort((a, b) => {
|
|
if (a.created_at !== b.created_at) {
|
|
return b.created_at - a.created_at;
|
|
}
|
|
return a.id.localeCompare(b.id);
|
|
});
|
|
}
|
|
|
|
// pure.ts
|
|
import { sha256 } from "@noble/hashes/sha256";
|
|
|
|
// utils.ts
|
|
var utils_exports = {};
|
|
__export(utils_exports, {
|
|
Queue: () => Queue,
|
|
QueueNode: () => QueueNode,
|
|
binarySearch: () => binarySearch,
|
|
bytesToHex: () => bytesToHex,
|
|
hexToBytes: () => hexToBytes,
|
|
insertEventIntoAscendingList: () => insertEventIntoAscendingList,
|
|
insertEventIntoDescendingList: () => insertEventIntoDescendingList,
|
|
normalizeURL: () => normalizeURL,
|
|
utf8Decoder: () => utf8Decoder,
|
|
utf8Encoder: () => utf8Encoder
|
|
});
|
|
import { bytesToHex, hexToBytes } from "@noble/hashes/utils";
|
|
var utf8Decoder = new TextDecoder("utf-8");
|
|
var utf8Encoder = new TextEncoder();
|
|
function normalizeURL(url) {
|
|
try {
|
|
if (url.indexOf("://") === -1)
|
|
url = "wss://" + url;
|
|
let p = new URL(url);
|
|
p.pathname = p.pathname.replace(/\/+/g, "/");
|
|
if (p.pathname.endsWith("/"))
|
|
p.pathname = p.pathname.slice(0, -1);
|
|
if (p.port === "80" && p.protocol === "ws:" || p.port === "443" && p.protocol === "wss:")
|
|
p.port = "";
|
|
p.searchParams.sort();
|
|
p.hash = "";
|
|
return p.toString();
|
|
} catch (e) {
|
|
throw new Error(`Invalid URL: ${url}`);
|
|
}
|
|
}
|
|
function insertEventIntoDescendingList(sortedArray, event) {
|
|
const [idx, found] = binarySearch(sortedArray, (b) => {
|
|
if (event.id === b.id)
|
|
return 0;
|
|
if (event.created_at === b.created_at)
|
|
return -1;
|
|
return b.created_at - event.created_at;
|
|
});
|
|
if (!found) {
|
|
sortedArray.splice(idx, 0, event);
|
|
}
|
|
return sortedArray;
|
|
}
|
|
function insertEventIntoAscendingList(sortedArray, event) {
|
|
const [idx, found] = binarySearch(sortedArray, (b) => {
|
|
if (event.id === b.id)
|
|
return 0;
|
|
if (event.created_at === b.created_at)
|
|
return -1;
|
|
return event.created_at - b.created_at;
|
|
});
|
|
if (!found) {
|
|
sortedArray.splice(idx, 0, event);
|
|
}
|
|
return sortedArray;
|
|
}
|
|
function binarySearch(arr, compare) {
|
|
let start = 0;
|
|
let end = arr.length - 1;
|
|
while (start <= end) {
|
|
const mid = Math.floor((start + end) / 2);
|
|
const cmp = compare(arr[mid]);
|
|
if (cmp === 0) {
|
|
return [mid, true];
|
|
}
|
|
if (cmp < 0) {
|
|
end = mid - 1;
|
|
} else {
|
|
start = mid + 1;
|
|
}
|
|
}
|
|
return [start, false];
|
|
}
|
|
var QueueNode = class {
|
|
value;
|
|
next = null;
|
|
prev = null;
|
|
constructor(message) {
|
|
this.value = message;
|
|
}
|
|
};
|
|
var Queue = class {
|
|
first;
|
|
last;
|
|
constructor() {
|
|
this.first = null;
|
|
this.last = null;
|
|
}
|
|
enqueue(value) {
|
|
const newNode = new QueueNode(value);
|
|
if (!this.last) {
|
|
this.first = newNode;
|
|
this.last = newNode;
|
|
} else if (this.last === this.first) {
|
|
this.last = newNode;
|
|
this.last.prev = this.first;
|
|
this.first.next = newNode;
|
|
} else {
|
|
newNode.prev = this.last;
|
|
this.last.next = newNode;
|
|
this.last = newNode;
|
|
}
|
|
return true;
|
|
}
|
|
dequeue() {
|
|
if (!this.first)
|
|
return null;
|
|
if (this.first === this.last) {
|
|
const target2 = this.first;
|
|
this.first = null;
|
|
this.last = null;
|
|
return target2.value;
|
|
}
|
|
const target = this.first;
|
|
this.first = target.next;
|
|
if (this.first) {
|
|
this.first.prev = null;
|
|
}
|
|
return target.value;
|
|
}
|
|
};
|
|
|
|
// pure.ts
|
|
var JS = class {
|
|
generateSecretKey() {
|
|
return schnorr.utils.randomPrivateKey();
|
|
}
|
|
getPublicKey(secretKey) {
|
|
return bytesToHex2(schnorr.getPublicKey(secretKey));
|
|
}
|
|
finalizeEvent(t, secretKey) {
|
|
const event = t;
|
|
event.pubkey = bytesToHex2(schnorr.getPublicKey(secretKey));
|
|
event.id = getEventHash(event);
|
|
event.sig = bytesToHex2(schnorr.sign(getEventHash(event), secretKey));
|
|
event[verifiedSymbol] = true;
|
|
return event;
|
|
}
|
|
verifyEvent(event) {
|
|
if (typeof event[verifiedSymbol] === "boolean")
|
|
return event[verifiedSymbol];
|
|
const hash = getEventHash(event);
|
|
if (hash !== event.id) {
|
|
event[verifiedSymbol] = false;
|
|
return false;
|
|
}
|
|
try {
|
|
const valid = schnorr.verify(event.sig, hash, event.pubkey);
|
|
event[verifiedSymbol] = valid;
|
|
return valid;
|
|
} catch (err) {
|
|
event[verifiedSymbol] = false;
|
|
return false;
|
|
}
|
|
}
|
|
};
|
|
function serializeEvent(evt) {
|
|
if (!validateEvent(evt))
|
|
throw new Error("can't serialize event with wrong or missing properties");
|
|
return JSON.stringify([0, evt.pubkey, evt.created_at, evt.kind, evt.tags, evt.content]);
|
|
}
|
|
function getEventHash(event) {
|
|
let eventHash = sha256(utf8Encoder.encode(serializeEvent(event)));
|
|
return bytesToHex2(eventHash);
|
|
}
|
|
var i = new JS();
|
|
var generateSecretKey = i.generateSecretKey;
|
|
var getPublicKey = i.getPublicKey;
|
|
var finalizeEvent = i.finalizeEvent;
|
|
var verifyEvent = i.verifyEvent;
|
|
|
|
// kinds.ts
|
|
var kinds_exports = {};
|
|
__export(kinds_exports, {
|
|
Application: () => Application,
|
|
BadgeAward: () => BadgeAward,
|
|
BadgeDefinition: () => BadgeDefinition,
|
|
BlockedRelaysList: () => BlockedRelaysList,
|
|
BookmarkList: () => BookmarkList,
|
|
Bookmarksets: () => Bookmarksets,
|
|
Calendar: () => Calendar,
|
|
CalendarEventRSVP: () => CalendarEventRSVP,
|
|
ChannelCreation: () => ChannelCreation,
|
|
ChannelHideMessage: () => ChannelHideMessage,
|
|
ChannelMessage: () => ChannelMessage,
|
|
ChannelMetadata: () => ChannelMetadata,
|
|
ChannelMuteUser: () => ChannelMuteUser,
|
|
ClassifiedListing: () => ClassifiedListing,
|
|
ClientAuth: () => ClientAuth,
|
|
CommunitiesList: () => CommunitiesList,
|
|
CommunityDefinition: () => CommunityDefinition,
|
|
CommunityPostApproval: () => CommunityPostApproval,
|
|
Contacts: () => Contacts,
|
|
CreateOrUpdateProduct: () => CreateOrUpdateProduct,
|
|
CreateOrUpdateStall: () => CreateOrUpdateStall,
|
|
Curationsets: () => Curationsets,
|
|
Date: () => Date2,
|
|
DirectMessageRelaysList: () => DirectMessageRelaysList,
|
|
DraftClassifiedListing: () => DraftClassifiedListing,
|
|
DraftLong: () => DraftLong,
|
|
Emojisets: () => Emojisets,
|
|
EncryptedDirectMessage: () => EncryptedDirectMessage,
|
|
EventDeletion: () => EventDeletion,
|
|
FileMetadata: () => FileMetadata,
|
|
FileServerPreference: () => FileServerPreference,
|
|
Followsets: () => Followsets,
|
|
GenericRepost: () => GenericRepost,
|
|
Genericlists: () => Genericlists,
|
|
GiftWrap: () => GiftWrap,
|
|
HTTPAuth: () => HTTPAuth,
|
|
Handlerinformation: () => Handlerinformation,
|
|
Handlerrecommendation: () => Handlerrecommendation,
|
|
Highlights: () => Highlights,
|
|
InterestsList: () => InterestsList,
|
|
Interestsets: () => Interestsets,
|
|
JobFeedback: () => JobFeedback,
|
|
JobRequest: () => JobRequest,
|
|
JobResult: () => JobResult,
|
|
Label: () => Label,
|
|
LightningPubRPC: () => LightningPubRPC,
|
|
LiveChatMessage: () => LiveChatMessage,
|
|
LiveEvent: () => LiveEvent,
|
|
LongFormArticle: () => LongFormArticle,
|
|
Metadata: () => Metadata,
|
|
Mutelist: () => Mutelist,
|
|
NWCWalletInfo: () => NWCWalletInfo,
|
|
NWCWalletRequest: () => NWCWalletRequest,
|
|
NWCWalletResponse: () => NWCWalletResponse,
|
|
NostrConnect: () => NostrConnect,
|
|
OpenTimestamps: () => OpenTimestamps,
|
|
Pinlist: () => Pinlist,
|
|
PrivateDirectMessage: () => PrivateDirectMessage,
|
|
ProblemTracker: () => ProblemTracker,
|
|
ProfileBadges: () => ProfileBadges,
|
|
PublicChatsList: () => PublicChatsList,
|
|
Reaction: () => Reaction,
|
|
RecommendRelay: () => RecommendRelay,
|
|
RelayList: () => RelayList,
|
|
Relaysets: () => Relaysets,
|
|
Report: () => Report,
|
|
Reporting: () => Reporting,
|
|
Repost: () => Repost,
|
|
Seal: () => Seal,
|
|
SearchRelaysList: () => SearchRelaysList,
|
|
ShortTextNote: () => ShortTextNote,
|
|
Time: () => Time,
|
|
UserEmojiList: () => UserEmojiList,
|
|
UserStatuses: () => UserStatuses,
|
|
Zap: () => Zap,
|
|
ZapGoal: () => ZapGoal,
|
|
ZapRequest: () => ZapRequest,
|
|
classifyKind: () => classifyKind,
|
|
isAddressableKind: () => isAddressableKind,
|
|
isEphemeralKind: () => isEphemeralKind,
|
|
isKind: () => isKind,
|
|
isRegularKind: () => isRegularKind,
|
|
isReplaceableKind: () => isReplaceableKind
|
|
});
|
|
function isRegularKind(kind) {
|
|
return 1e3 <= kind && kind < 1e4 || [1, 2, 4, 5, 6, 7, 8, 16, 40, 41, 42, 43, 44].includes(kind);
|
|
}
|
|
function isReplaceableKind(kind) {
|
|
return [0, 3].includes(kind) || 1e4 <= kind && kind < 2e4;
|
|
}
|
|
function isEphemeralKind(kind) {
|
|
return 2e4 <= kind && kind < 3e4;
|
|
}
|
|
function isAddressableKind(kind) {
|
|
return 3e4 <= kind && kind < 4e4;
|
|
}
|
|
function classifyKind(kind) {
|
|
if (isRegularKind(kind))
|
|
return "regular";
|
|
if (isReplaceableKind(kind))
|
|
return "replaceable";
|
|
if (isEphemeralKind(kind))
|
|
return "ephemeral";
|
|
if (isAddressableKind(kind))
|
|
return "parameterized";
|
|
return "unknown";
|
|
}
|
|
function isKind(event, kind) {
|
|
const kindAsArray = kind instanceof Array ? kind : [kind];
|
|
return validateEvent(event) && kindAsArray.includes(event.kind) || false;
|
|
}
|
|
var Metadata = 0;
|
|
var ShortTextNote = 1;
|
|
var RecommendRelay = 2;
|
|
var Contacts = 3;
|
|
var EncryptedDirectMessage = 4;
|
|
var EventDeletion = 5;
|
|
var Repost = 6;
|
|
var Reaction = 7;
|
|
var BadgeAward = 8;
|
|
var Seal = 13;
|
|
var PrivateDirectMessage = 14;
|
|
var GenericRepost = 16;
|
|
var ChannelCreation = 40;
|
|
var ChannelMetadata = 41;
|
|
var ChannelMessage = 42;
|
|
var ChannelHideMessage = 43;
|
|
var ChannelMuteUser = 44;
|
|
var OpenTimestamps = 1040;
|
|
var GiftWrap = 1059;
|
|
var FileMetadata = 1063;
|
|
var LiveChatMessage = 1311;
|
|
var ProblemTracker = 1971;
|
|
var Report = 1984;
|
|
var Reporting = 1984;
|
|
var Label = 1985;
|
|
var CommunityPostApproval = 4550;
|
|
var JobRequest = 5999;
|
|
var JobResult = 6999;
|
|
var JobFeedback = 7e3;
|
|
var ZapGoal = 9041;
|
|
var ZapRequest = 9734;
|
|
var Zap = 9735;
|
|
var Highlights = 9802;
|
|
var Mutelist = 1e4;
|
|
var Pinlist = 10001;
|
|
var RelayList = 10002;
|
|
var BookmarkList = 10003;
|
|
var CommunitiesList = 10004;
|
|
var PublicChatsList = 10005;
|
|
var BlockedRelaysList = 10006;
|
|
var SearchRelaysList = 10007;
|
|
var InterestsList = 10015;
|
|
var UserEmojiList = 10030;
|
|
var DirectMessageRelaysList = 10050;
|
|
var FileServerPreference = 10096;
|
|
var NWCWalletInfo = 13194;
|
|
var LightningPubRPC = 21e3;
|
|
var ClientAuth = 22242;
|
|
var NWCWalletRequest = 23194;
|
|
var NWCWalletResponse = 23195;
|
|
var NostrConnect = 24133;
|
|
var HTTPAuth = 27235;
|
|
var Followsets = 3e4;
|
|
var Genericlists = 30001;
|
|
var Relaysets = 30002;
|
|
var Bookmarksets = 30003;
|
|
var Curationsets = 30004;
|
|
var ProfileBadges = 30008;
|
|
var BadgeDefinition = 30009;
|
|
var Interestsets = 30015;
|
|
var CreateOrUpdateStall = 30017;
|
|
var CreateOrUpdateProduct = 30018;
|
|
var LongFormArticle = 30023;
|
|
var DraftLong = 30024;
|
|
var Emojisets = 30030;
|
|
var Application = 30078;
|
|
var LiveEvent = 30311;
|
|
var UserStatuses = 30315;
|
|
var ClassifiedListing = 30402;
|
|
var DraftClassifiedListing = 30403;
|
|
var Date2 = 31922;
|
|
var Time = 31923;
|
|
var Calendar = 31924;
|
|
var CalendarEventRSVP = 31925;
|
|
var Handlerrecommendation = 31989;
|
|
var Handlerinformation = 31990;
|
|
var CommunityDefinition = 34550;
|
|
|
|
// filter.ts
|
|
function matchFilter(filter, event) {
|
|
if (filter.ids && filter.ids.indexOf(event.id) === -1) {
|
|
return false;
|
|
}
|
|
if (filter.kinds && filter.kinds.indexOf(event.kind) === -1) {
|
|
return false;
|
|
}
|
|
if (filter.authors && filter.authors.indexOf(event.pubkey) === -1) {
|
|
return false;
|
|
}
|
|
for (let f in filter) {
|
|
if (f[0] === "#") {
|
|
let tagName = f.slice(1);
|
|
let values = filter[`#${tagName}`];
|
|
if (values && !event.tags.find(([t, v]) => t === f.slice(1) && values.indexOf(v) !== -1))
|
|
return false;
|
|
}
|
|
}
|
|
if (filter.since && event.created_at < filter.since)
|
|
return false;
|
|
if (filter.until && event.created_at > filter.until)
|
|
return false;
|
|
return true;
|
|
}
|
|
function matchFilters(filters, event) {
|
|
for (let i2 = 0; i2 < filters.length; i2++) {
|
|
if (matchFilter(filters[i2], event)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
function mergeFilters(...filters) {
|
|
let result = {};
|
|
for (let i2 = 0; i2 < filters.length; i2++) {
|
|
let filter = filters[i2];
|
|
Object.entries(filter).forEach(([property, values]) => {
|
|
if (property === "kinds" || property === "ids" || property === "authors" || property[0] === "#") {
|
|
result[property] = result[property] || [];
|
|
for (let v = 0; v < values.length; v++) {
|
|
let value = values[v];
|
|
if (!result[property].includes(value))
|
|
result[property].push(value);
|
|
}
|
|
}
|
|
});
|
|
if (filter.limit && (!result.limit || filter.limit > result.limit))
|
|
result.limit = filter.limit;
|
|
if (filter.until && (!result.until || filter.until > result.until))
|
|
result.until = filter.until;
|
|
if (filter.since && (!result.since || filter.since < result.since))
|
|
result.since = filter.since;
|
|
}
|
|
return result;
|
|
}
|
|
function getFilterLimit(filter) {
|
|
if (filter.ids && !filter.ids.length)
|
|
return 0;
|
|
if (filter.kinds && !filter.kinds.length)
|
|
return 0;
|
|
if (filter.authors && !filter.authors.length)
|
|
return 0;
|
|
for (const [key, value] of Object.entries(filter)) {
|
|
if (key[0] === "#" && Array.isArray(value) && !value.length)
|
|
return 0;
|
|
}
|
|
return Math.min(
|
|
Math.max(0, filter.limit ?? Infinity),
|
|
filter.ids?.length ?? Infinity,
|
|
filter.authors?.length && filter.kinds?.every((kind) => isReplaceableKind(kind)) ? filter.authors.length * filter.kinds.length : Infinity,
|
|
filter.authors?.length && filter.kinds?.every((kind) => isAddressableKind(kind)) && filter["#d"]?.length ? filter.authors.length * filter.kinds.length * filter["#d"].length : Infinity
|
|
);
|
|
}
|
|
|
|
// fakejson.ts
|
|
var fakejson_exports = {};
|
|
__export(fakejson_exports, {
|
|
getHex64: () => getHex64,
|
|
getInt: () => getInt,
|
|
getSubscriptionId: () => getSubscriptionId,
|
|
matchEventId: () => matchEventId,
|
|
matchEventKind: () => matchEventKind,
|
|
matchEventPubkey: () => matchEventPubkey
|
|
});
|
|
function getHex64(json, field) {
|
|
let len = field.length + 3;
|
|
let idx = json.indexOf(`"${field}":`) + len;
|
|
let s = json.slice(idx).indexOf(`"`) + idx + 1;
|
|
return json.slice(s, s + 64);
|
|
}
|
|
function getInt(json, field) {
|
|
let len = field.length;
|
|
let idx = json.indexOf(`"${field}":`) + len + 3;
|
|
let sliced = json.slice(idx);
|
|
let end = Math.min(sliced.indexOf(","), sliced.indexOf("}"));
|
|
return parseInt(sliced.slice(0, end), 10);
|
|
}
|
|
function getSubscriptionId(json) {
|
|
let idx = json.slice(0, 22).indexOf(`"EVENT"`);
|
|
if (idx === -1)
|
|
return null;
|
|
let pstart = json.slice(idx + 7 + 1).indexOf(`"`);
|
|
if (pstart === -1)
|
|
return null;
|
|
let start = idx + 7 + 1 + pstart;
|
|
let pend = json.slice(start + 1, 80).indexOf(`"`);
|
|
if (pend === -1)
|
|
return null;
|
|
let end = start + 1 + pend;
|
|
return json.slice(start + 1, end);
|
|
}
|
|
function matchEventId(json, id) {
|
|
return id === getHex64(json, "id");
|
|
}
|
|
function matchEventPubkey(json, pubkey) {
|
|
return pubkey === getHex64(json, "pubkey");
|
|
}
|
|
function matchEventKind(json, kind) {
|
|
return kind === getInt(json, "kind");
|
|
}
|
|
|
|
// nip42.ts
|
|
var nip42_exports = {};
|
|
__export(nip42_exports, {
|
|
makeAuthEvent: () => makeAuthEvent
|
|
});
|
|
function makeAuthEvent(relayURL, challenge) {
|
|
return {
|
|
kind: ClientAuth,
|
|
created_at: Math.floor(Date.now() / 1e3),
|
|
tags: [
|
|
["relay", relayURL],
|
|
["challenge", challenge]
|
|
],
|
|
content: ""
|
|
};
|
|
}
|
|
|
|
// helpers.ts
|
|
async function yieldThread() {
|
|
return new Promise((resolve) => {
|
|
const ch = new MessageChannel();
|
|
const handler = () => {
|
|
ch.port1.removeEventListener("message", handler);
|
|
resolve();
|
|
};
|
|
ch.port1.addEventListener("message", handler);
|
|
ch.port2.postMessage(0);
|
|
ch.port1.start();
|
|
});
|
|
}
|
|
var alwaysTrue = (t) => {
|
|
t[verifiedSymbol] = true;
|
|
return true;
|
|
};
|
|
|
|
// abstract-relay.ts
|
|
var SendingOnClosedConnection = class extends Error {
|
|
constructor(message, relay) {
|
|
super(`Tried to send message '${message} on a closed connection to ${relay}.`);
|
|
this.name = "SendingOnClosedConnection";
|
|
}
|
|
};
|
|
var AbstractRelay = class {
|
|
url;
|
|
_connected = false;
|
|
onclose = null;
|
|
onnotice = (msg) => console.debug(`NOTICE from ${this.url}: ${msg}`);
|
|
baseEoseTimeout = 4400;
|
|
connectionTimeout = 4400;
|
|
publishTimeout = 4400;
|
|
pingFrequency = 2e4;
|
|
pingTimeout = 2e4;
|
|
openSubs = /* @__PURE__ */ new Map();
|
|
enablePing;
|
|
connectionTimeoutHandle;
|
|
connectionPromise;
|
|
openCountRequests = /* @__PURE__ */ new Map();
|
|
openEventPublishes = /* @__PURE__ */ new Map();
|
|
ws;
|
|
incomingMessageQueue = new Queue();
|
|
queueRunning = false;
|
|
challenge;
|
|
authPromise;
|
|
serial = 0;
|
|
verifyEvent;
|
|
_WebSocket;
|
|
constructor(url, opts) {
|
|
this.url = normalizeURL(url);
|
|
this.verifyEvent = opts.verifyEvent;
|
|
this._WebSocket = opts.websocketImplementation || WebSocket;
|
|
this.enablePing = opts.enablePing;
|
|
}
|
|
static async connect(url, opts) {
|
|
const relay = new AbstractRelay(url, opts);
|
|
await relay.connect();
|
|
return relay;
|
|
}
|
|
closeAllSubscriptions(reason) {
|
|
for (let [_, sub] of this.openSubs) {
|
|
sub.close(reason);
|
|
}
|
|
this.openSubs.clear();
|
|
for (let [_, ep] of this.openEventPublishes) {
|
|
ep.reject(new Error(reason));
|
|
}
|
|
this.openEventPublishes.clear();
|
|
for (let [_, cr] of this.openCountRequests) {
|
|
cr.reject(new Error(reason));
|
|
}
|
|
this.openCountRequests.clear();
|
|
}
|
|
get connected() {
|
|
return this._connected;
|
|
}
|
|
async connect() {
|
|
if (this.connectionPromise)
|
|
return this.connectionPromise;
|
|
this.challenge = void 0;
|
|
this.authPromise = void 0;
|
|
this.connectionPromise = new Promise((resolve, reject) => {
|
|
this.connectionTimeoutHandle = setTimeout(() => {
|
|
reject("connection timed out");
|
|
this.connectionPromise = void 0;
|
|
this.onclose?.();
|
|
this.closeAllSubscriptions("relay connection timed out");
|
|
}, this.connectionTimeout);
|
|
try {
|
|
this.ws = new this._WebSocket(this.url);
|
|
} catch (err) {
|
|
clearTimeout(this.connectionTimeoutHandle);
|
|
reject(err);
|
|
return;
|
|
}
|
|
this.ws.onopen = () => {
|
|
clearTimeout(this.connectionTimeoutHandle);
|
|
this._connected = true;
|
|
if (this.enablePing) {
|
|
this.pingpong();
|
|
}
|
|
resolve();
|
|
};
|
|
this.ws.onerror = (ev) => {
|
|
clearTimeout(this.connectionTimeoutHandle);
|
|
reject(ev.message || "websocket error");
|
|
this._connected = false;
|
|
this.connectionPromise = void 0;
|
|
this.onclose?.();
|
|
this.closeAllSubscriptions("relay connection errored");
|
|
};
|
|
this.ws.onclose = (ev) => {
|
|
clearTimeout(this.connectionTimeoutHandle);
|
|
reject(ev.message || "websocket closed");
|
|
this._connected = false;
|
|
this.connectionPromise = void 0;
|
|
this.onclose?.();
|
|
this.closeAllSubscriptions("relay connection closed");
|
|
};
|
|
this.ws.onmessage = this._onmessage.bind(this);
|
|
});
|
|
return this.connectionPromise;
|
|
}
|
|
async waitForPingPong() {
|
|
return new Promise((res, err) => {
|
|
;
|
|
this.ws && this.ws.on && this.ws.on("pong", () => res(true)) || err("ws can't listen for pong");
|
|
this.ws && this.ws.ping && this.ws.ping();
|
|
});
|
|
}
|
|
async waitForDummyReq() {
|
|
return new Promise((resolve, _) => {
|
|
const sub = this.subscribe([{ ids: ["a".repeat(64)] }], {
|
|
oneose: () => {
|
|
sub.close();
|
|
resolve(true);
|
|
},
|
|
eoseTimeout: this.pingTimeout + 1e3
|
|
});
|
|
});
|
|
}
|
|
async pingpong() {
|
|
if (this.ws?.readyState === 1) {
|
|
const result = await Promise.any([
|
|
this.ws && this.ws.ping && this.ws.on ? this.waitForPingPong() : this.waitForDummyReq(),
|
|
new Promise((res) => setTimeout(() => res(false), this.pingTimeout))
|
|
]);
|
|
if (result) {
|
|
setTimeout(() => this.pingpong(), this.pingFrequency);
|
|
} else {
|
|
this.closeAllSubscriptions("pingpong timed out");
|
|
this._connected = false;
|
|
this.onclose?.();
|
|
this.ws?.close();
|
|
}
|
|
}
|
|
}
|
|
async runQueue() {
|
|
this.queueRunning = true;
|
|
while (true) {
|
|
if (false === this.handleNext()) {
|
|
break;
|
|
}
|
|
await yieldThread();
|
|
}
|
|
this.queueRunning = false;
|
|
}
|
|
handleNext() {
|
|
const json = this.incomingMessageQueue.dequeue();
|
|
if (!json) {
|
|
return false;
|
|
}
|
|
const subid = getSubscriptionId(json);
|
|
if (subid) {
|
|
const so = this.openSubs.get(subid);
|
|
if (!so) {
|
|
return;
|
|
}
|
|
const id = getHex64(json, "id");
|
|
const alreadyHave = so.alreadyHaveEvent?.(id);
|
|
so.receivedEvent?.(this, id);
|
|
if (alreadyHave) {
|
|
return;
|
|
}
|
|
}
|
|
try {
|
|
let data = JSON.parse(json);
|
|
switch (data[0]) {
|
|
case "EVENT": {
|
|
const so = this.openSubs.get(data[1]);
|
|
const event = data[2];
|
|
if (this.verifyEvent(event) && matchFilters(so.filters, event)) {
|
|
so.onevent(event);
|
|
}
|
|
return;
|
|
}
|
|
case "COUNT": {
|
|
const id = data[1];
|
|
const payload = data[2];
|
|
const cr = this.openCountRequests.get(id);
|
|
if (cr) {
|
|
cr.resolve(payload.count);
|
|
this.openCountRequests.delete(id);
|
|
}
|
|
return;
|
|
}
|
|
case "EOSE": {
|
|
const so = this.openSubs.get(data[1]);
|
|
if (!so)
|
|
return;
|
|
so.receivedEose();
|
|
return;
|
|
}
|
|
case "OK": {
|
|
const id = data[1];
|
|
const ok = data[2];
|
|
const reason = data[3];
|
|
const ep = this.openEventPublishes.get(id);
|
|
if (ep) {
|
|
clearTimeout(ep.timeout);
|
|
if (ok)
|
|
ep.resolve(reason);
|
|
else
|
|
ep.reject(new Error(reason));
|
|
this.openEventPublishes.delete(id);
|
|
}
|
|
return;
|
|
}
|
|
case "CLOSED": {
|
|
const id = data[1];
|
|
const so = this.openSubs.get(id);
|
|
if (!so)
|
|
return;
|
|
so.closed = true;
|
|
so.close(data[2]);
|
|
return;
|
|
}
|
|
case "NOTICE":
|
|
this.onnotice(data[1]);
|
|
return;
|
|
case "AUTH": {
|
|
this.challenge = data[1];
|
|
return;
|
|
}
|
|
}
|
|
} catch (err) {
|
|
return;
|
|
}
|
|
}
|
|
async send(message) {
|
|
if (!this.connectionPromise)
|
|
throw new SendingOnClosedConnection(message, this.url);
|
|
this.connectionPromise.then(() => {
|
|
this.ws?.send(message);
|
|
});
|
|
}
|
|
async auth(signAuthEvent) {
|
|
const challenge = this.challenge;
|
|
if (!challenge)
|
|
throw new Error("can't perform auth, no challenge was received");
|
|
if (this.authPromise)
|
|
return this.authPromise;
|
|
this.authPromise = new Promise(async (resolve, reject) => {
|
|
try {
|
|
let evt = await signAuthEvent(makeAuthEvent(this.url, challenge));
|
|
let timeout = setTimeout(() => {
|
|
let ep = this.openEventPublishes.get(evt.id);
|
|
if (ep) {
|
|
ep.reject(new Error("auth timed out"));
|
|
this.openEventPublishes.delete(evt.id);
|
|
}
|
|
}, this.publishTimeout);
|
|
this.openEventPublishes.set(evt.id, { resolve, reject, timeout });
|
|
this.send('["AUTH",' + JSON.stringify(evt) + "]");
|
|
} catch (err) {
|
|
console.warn("subscribe auth function failed:", err);
|
|
}
|
|
});
|
|
return this.authPromise;
|
|
}
|
|
async publish(event) {
|
|
const ret = new Promise((resolve, reject) => {
|
|
const timeout = setTimeout(() => {
|
|
const ep = this.openEventPublishes.get(event.id);
|
|
if (ep) {
|
|
ep.reject(new Error("publish timed out"));
|
|
this.openEventPublishes.delete(event.id);
|
|
}
|
|
}, this.publishTimeout);
|
|
this.openEventPublishes.set(event.id, { resolve, reject, timeout });
|
|
});
|
|
this.send('["EVENT",' + JSON.stringify(event) + "]");
|
|
return ret;
|
|
}
|
|
async count(filters, params) {
|
|
this.serial++;
|
|
const id = params?.id || "count:" + this.serial;
|
|
const ret = new Promise((resolve, reject) => {
|
|
this.openCountRequests.set(id, { resolve, reject });
|
|
});
|
|
this.send('["COUNT","' + id + '",' + JSON.stringify(filters).substring(1));
|
|
return ret;
|
|
}
|
|
subscribe(filters, params) {
|
|
const subscription = this.prepareSubscription(filters, params);
|
|
subscription.fire();
|
|
return subscription;
|
|
}
|
|
prepareSubscription(filters, params) {
|
|
this.serial++;
|
|
const id = params.id || (params.label ? params.label + ":" : "sub:") + this.serial;
|
|
const subscription = new Subscription(this, id, filters, params);
|
|
this.openSubs.set(id, subscription);
|
|
return subscription;
|
|
}
|
|
close() {
|
|
this.closeAllSubscriptions("relay connection closed by us");
|
|
this._connected = false;
|
|
this.onclose?.();
|
|
this.ws?.close();
|
|
}
|
|
_onmessage(ev) {
|
|
this.incomingMessageQueue.enqueue(ev.data);
|
|
if (!this.queueRunning) {
|
|
this.runQueue();
|
|
}
|
|
}
|
|
};
|
|
var Subscription = class {
|
|
relay;
|
|
id;
|
|
closed = false;
|
|
eosed = false;
|
|
filters;
|
|
alreadyHaveEvent;
|
|
receivedEvent;
|
|
onevent;
|
|
oneose;
|
|
onclose;
|
|
eoseTimeout;
|
|
eoseTimeoutHandle;
|
|
constructor(relay, id, filters, params) {
|
|
this.relay = relay;
|
|
this.filters = filters;
|
|
this.id = id;
|
|
this.alreadyHaveEvent = params.alreadyHaveEvent;
|
|
this.receivedEvent = params.receivedEvent;
|
|
this.eoseTimeout = params.eoseTimeout || relay.baseEoseTimeout;
|
|
this.oneose = params.oneose;
|
|
this.onclose = params.onclose;
|
|
this.onevent = params.onevent || ((event) => {
|
|
console.warn(
|
|
`onevent() callback not defined for subscription '${this.id}' in relay ${this.relay.url}. event received:`,
|
|
event
|
|
);
|
|
});
|
|
}
|
|
fire() {
|
|
this.relay.send('["REQ","' + this.id + '",' + JSON.stringify(this.filters).substring(1));
|
|
this.eoseTimeoutHandle = setTimeout(this.receivedEose.bind(this), this.eoseTimeout);
|
|
}
|
|
receivedEose() {
|
|
if (this.eosed)
|
|
return;
|
|
clearTimeout(this.eoseTimeoutHandle);
|
|
this.eosed = true;
|
|
this.oneose?.();
|
|
}
|
|
close(reason = "closed by caller") {
|
|
if (!this.closed && this.relay.connected) {
|
|
try {
|
|
this.relay.send('["CLOSE",' + JSON.stringify(this.id) + "]");
|
|
} catch (err) {
|
|
if (err instanceof SendingOnClosedConnection) {
|
|
} else {
|
|
throw err;
|
|
}
|
|
}
|
|
this.closed = true;
|
|
}
|
|
this.relay.openSubs.delete(this.id);
|
|
this.onclose?.(reason);
|
|
}
|
|
};
|
|
|
|
// relay.ts
|
|
var _WebSocket;
|
|
try {
|
|
_WebSocket = WebSocket;
|
|
} catch {
|
|
}
|
|
var Relay = class extends AbstractRelay {
|
|
constructor(url) {
|
|
super(url, { verifyEvent, websocketImplementation: _WebSocket });
|
|
}
|
|
static async connect(url) {
|
|
const relay = new Relay(url);
|
|
await relay.connect();
|
|
return relay;
|
|
}
|
|
};
|
|
|
|
// abstract-pool.ts
|
|
var AbstractSimplePool = class {
|
|
relays = /* @__PURE__ */ new Map();
|
|
seenOn = /* @__PURE__ */ new Map();
|
|
trackRelays = false;
|
|
verifyEvent;
|
|
enablePing;
|
|
trustedRelayURLs = /* @__PURE__ */ new Set();
|
|
_WebSocket;
|
|
constructor(opts) {
|
|
this.verifyEvent = opts.verifyEvent;
|
|
this._WebSocket = opts.websocketImplementation;
|
|
this.enablePing = opts.enablePing;
|
|
}
|
|
async ensureRelay(url, params) {
|
|
url = normalizeURL(url);
|
|
let relay = this.relays.get(url);
|
|
if (!relay) {
|
|
relay = new AbstractRelay(url, {
|
|
verifyEvent: this.trustedRelayURLs.has(url) ? alwaysTrue : this.verifyEvent,
|
|
websocketImplementation: this._WebSocket,
|
|
enablePing: this.enablePing
|
|
});
|
|
relay.onclose = () => {
|
|
this.relays.delete(url);
|
|
};
|
|
if (params?.connectionTimeout)
|
|
relay.connectionTimeout = params.connectionTimeout;
|
|
this.relays.set(url, relay);
|
|
}
|
|
await relay.connect();
|
|
return relay;
|
|
}
|
|
close(relays) {
|
|
relays.map(normalizeURL).forEach((url) => {
|
|
this.relays.get(url)?.close();
|
|
this.relays.delete(url);
|
|
});
|
|
}
|
|
subscribe(relays, filter, params) {
|
|
params.onauth = params.onauth || params.doauth;
|
|
const request = [];
|
|
for (let i2 = 0; i2 < relays.length; i2++) {
|
|
const url = normalizeURL(relays[i2]);
|
|
if (!request.find((r) => r.url === url)) {
|
|
request.push({ url, filter });
|
|
}
|
|
}
|
|
return this.subscribeMap(request, params);
|
|
}
|
|
subscribeMany(relays, filter, params) {
|
|
params.onauth = params.onauth || params.doauth;
|
|
const request = [];
|
|
const uniqUrls = [];
|
|
for (let i2 = 0; i2 < relays.length; i2++) {
|
|
const url = normalizeURL(relays[i2]);
|
|
if (uniqUrls.indexOf(url) === -1) {
|
|
uniqUrls.push(url);
|
|
request.push({ url, filter });
|
|
}
|
|
}
|
|
return this.subscribeMap(request, params);
|
|
}
|
|
subscribeMap(requests, params) {
|
|
params.onauth = params.onauth || params.doauth;
|
|
const grouped = /* @__PURE__ */ new Map();
|
|
for (const req of requests) {
|
|
const { url, filter } = req;
|
|
if (!grouped.has(url))
|
|
grouped.set(url, []);
|
|
grouped.get(url).push(filter);
|
|
}
|
|
const groupedRequests = Array.from(grouped.entries()).map(([url, filters]) => ({ url, filters }));
|
|
if (this.trackRelays) {
|
|
params.receivedEvent = (relay, id) => {
|
|
let set = this.seenOn.get(id);
|
|
if (!set) {
|
|
set = /* @__PURE__ */ new Set();
|
|
this.seenOn.set(id, set);
|
|
}
|
|
set.add(relay);
|
|
};
|
|
}
|
|
const _knownIds = /* @__PURE__ */ new Set();
|
|
const subs = [];
|
|
const eosesReceived = [];
|
|
let handleEose = (i2) => {
|
|
if (eosesReceived[i2])
|
|
return;
|
|
eosesReceived[i2] = true;
|
|
if (eosesReceived.filter((a) => a).length === requests.length) {
|
|
params.oneose?.();
|
|
handleEose = () => {
|
|
};
|
|
}
|
|
};
|
|
const closesReceived = [];
|
|
let handleClose = (i2, reason) => {
|
|
if (closesReceived[i2])
|
|
return;
|
|
handleEose(i2);
|
|
closesReceived[i2] = reason;
|
|
if (closesReceived.filter((a) => a).length === requests.length) {
|
|
params.onclose?.(closesReceived);
|
|
handleClose = () => {
|
|
};
|
|
}
|
|
};
|
|
const localAlreadyHaveEventHandler = (id) => {
|
|
if (params.alreadyHaveEvent?.(id)) {
|
|
return true;
|
|
}
|
|
const have = _knownIds.has(id);
|
|
_knownIds.add(id);
|
|
return have;
|
|
};
|
|
const allOpened = Promise.all(
|
|
groupedRequests.map(async ({ url, filters }, i2) => {
|
|
let relay;
|
|
try {
|
|
relay = await this.ensureRelay(url, {
|
|
connectionTimeout: params.maxWait ? Math.max(params.maxWait * 0.8, params.maxWait - 1e3) : void 0
|
|
});
|
|
} catch (err) {
|
|
handleClose(i2, err?.message || String(err));
|
|
return;
|
|
}
|
|
let subscription = relay.subscribe(filters, {
|
|
...params,
|
|
oneose: () => handleEose(i2),
|
|
onclose: (reason) => {
|
|
if (reason.startsWith("auth-required: ") && params.onauth) {
|
|
relay.auth(params.onauth).then(() => {
|
|
relay.subscribe(filters, {
|
|
...params,
|
|
oneose: () => handleEose(i2),
|
|
onclose: (reason2) => {
|
|
handleClose(i2, reason2);
|
|
},
|
|
alreadyHaveEvent: localAlreadyHaveEventHandler,
|
|
eoseTimeout: params.maxWait
|
|
});
|
|
}).catch((err) => {
|
|
handleClose(i2, `auth was required and attempted, but failed with: ${err}`);
|
|
});
|
|
} else {
|
|
handleClose(i2, reason);
|
|
}
|
|
},
|
|
alreadyHaveEvent: localAlreadyHaveEventHandler,
|
|
eoseTimeout: params.maxWait
|
|
});
|
|
subs.push(subscription);
|
|
})
|
|
);
|
|
return {
|
|
async close(reason) {
|
|
await allOpened;
|
|
subs.forEach((sub) => {
|
|
sub.close(reason);
|
|
});
|
|
}
|
|
};
|
|
}
|
|
subscribeEose(relays, filter, params) {
|
|
params.onauth = params.onauth || params.doauth;
|
|
const subcloser = this.subscribe(relays, filter, {
|
|
...params,
|
|
oneose() {
|
|
subcloser.close("closed automatically on eose");
|
|
}
|
|
});
|
|
return subcloser;
|
|
}
|
|
subscribeManyEose(relays, filter, params) {
|
|
params.onauth = params.onauth || params.doauth;
|
|
const subcloser = this.subscribeMany(relays, filter, {
|
|
...params,
|
|
oneose() {
|
|
subcloser.close("closed automatically on eose");
|
|
}
|
|
});
|
|
return subcloser;
|
|
}
|
|
async querySync(relays, filter, params) {
|
|
return new Promise(async (resolve) => {
|
|
const events = [];
|
|
this.subscribeEose(relays, filter, {
|
|
...params,
|
|
onevent(event) {
|
|
events.push(event);
|
|
},
|
|
onclose(_) {
|
|
resolve(events);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
async get(relays, filter, params) {
|
|
filter.limit = 1;
|
|
const events = await this.querySync(relays, filter, params);
|
|
events.sort((a, b) => b.created_at - a.created_at);
|
|
return events[0] || null;
|
|
}
|
|
publish(relays, event, options) {
|
|
return relays.map(normalizeURL).map(async (url, i2, arr) => {
|
|
if (arr.indexOf(url) !== i2) {
|
|
return Promise.reject("duplicate url");
|
|
}
|
|
let r = await this.ensureRelay(url);
|
|
return r.publish(event).catch(async (err) => {
|
|
if (err instanceof Error && err.message.startsWith("auth-required: ") && options?.onauth) {
|
|
await r.auth(options.onauth);
|
|
return r.publish(event);
|
|
}
|
|
throw err;
|
|
}).then((reason) => {
|
|
if (this.trackRelays) {
|
|
let set = this.seenOn.get(event.id);
|
|
if (!set) {
|
|
set = /* @__PURE__ */ new Set();
|
|
this.seenOn.set(event.id, set);
|
|
}
|
|
set.add(r);
|
|
}
|
|
return reason;
|
|
});
|
|
});
|
|
}
|
|
listConnectionStatus() {
|
|
const map = /* @__PURE__ */ new Map();
|
|
this.relays.forEach((relay, url) => map.set(url, relay.connected));
|
|
return map;
|
|
}
|
|
destroy() {
|
|
this.relays.forEach((conn) => conn.close());
|
|
this.relays = /* @__PURE__ */ new Map();
|
|
}
|
|
};
|
|
|
|
// pool.ts
|
|
var _WebSocket2;
|
|
try {
|
|
_WebSocket2 = WebSocket;
|
|
} catch {
|
|
}
|
|
var SimplePool = class extends AbstractSimplePool {
|
|
constructor(options) {
|
|
super({ verifyEvent, websocketImplementation: _WebSocket2, ...options });
|
|
}
|
|
};
|
|
|
|
// nip19.ts
|
|
var nip19_exports = {};
|
|
__export(nip19_exports, {
|
|
BECH32_REGEX: () => BECH32_REGEX,
|
|
Bech32MaxSize: () => Bech32MaxSize,
|
|
NostrTypeGuard: () => NostrTypeGuard,
|
|
decode: () => decode,
|
|
decodeNostrURI: () => decodeNostrURI,
|
|
encodeBytes: () => encodeBytes,
|
|
naddrEncode: () => naddrEncode,
|
|
neventEncode: () => neventEncode,
|
|
noteEncode: () => noteEncode,
|
|
nprofileEncode: () => nprofileEncode,
|
|
npubEncode: () => npubEncode,
|
|
nsecEncode: () => nsecEncode
|
|
});
|
|
import { bytesToHex as bytesToHex3, concatBytes, hexToBytes as hexToBytes2 } from "@noble/hashes/utils";
|
|
import { bech32 } from "@scure/base";
|
|
var NostrTypeGuard = {
|
|
isNProfile: (value) => /^nprofile1[a-z\d]+$/.test(value || ""),
|
|
isNEvent: (value) => /^nevent1[a-z\d]+$/.test(value || ""),
|
|
isNAddr: (value) => /^naddr1[a-z\d]+$/.test(value || ""),
|
|
isNSec: (value) => /^nsec1[a-z\d]{58}$/.test(value || ""),
|
|
isNPub: (value) => /^npub1[a-z\d]{58}$/.test(value || ""),
|
|
isNote: (value) => /^note1[a-z\d]+$/.test(value || ""),
|
|
isNcryptsec: (value) => /^ncryptsec1[a-z\d]+$/.test(value || "")
|
|
};
|
|
var Bech32MaxSize = 5e3;
|
|
var BECH32_REGEX = /[\x21-\x7E]{1,83}1[023456789acdefghjklmnpqrstuvwxyz]{6,}/;
|
|
function integerToUint8Array(number) {
|
|
const uint8Array = new Uint8Array(4);
|
|
uint8Array[0] = number >> 24 & 255;
|
|
uint8Array[1] = number >> 16 & 255;
|
|
uint8Array[2] = number >> 8 & 255;
|
|
uint8Array[3] = number & 255;
|
|
return uint8Array;
|
|
}
|
|
function decodeNostrURI(nip19code) {
|
|
try {
|
|
if (nip19code.startsWith("nostr:"))
|
|
nip19code = nip19code.substring(6);
|
|
return decode(nip19code);
|
|
} catch (_err) {
|
|
return { type: "invalid", data: null };
|
|
}
|
|
}
|
|
function decode(code) {
|
|
let { prefix, words } = bech32.decode(code, Bech32MaxSize);
|
|
let data = new Uint8Array(bech32.fromWords(words));
|
|
switch (prefix) {
|
|
case "nprofile": {
|
|
let tlv = parseTLV(data);
|
|
if (!tlv[0]?.[0])
|
|
throw new Error("missing TLV 0 for nprofile");
|
|
if (tlv[0][0].length !== 32)
|
|
throw new Error("TLV 0 should be 32 bytes");
|
|
return {
|
|
type: "nprofile",
|
|
data: {
|
|
pubkey: bytesToHex3(tlv[0][0]),
|
|
relays: tlv[1] ? tlv[1].map((d) => utf8Decoder.decode(d)) : []
|
|
}
|
|
};
|
|
}
|
|
case "nevent": {
|
|
let tlv = parseTLV(data);
|
|
if (!tlv[0]?.[0])
|
|
throw new Error("missing TLV 0 for nevent");
|
|
if (tlv[0][0].length !== 32)
|
|
throw new Error("TLV 0 should be 32 bytes");
|
|
if (tlv[2] && tlv[2][0].length !== 32)
|
|
throw new Error("TLV 2 should be 32 bytes");
|
|
if (tlv[3] && tlv[3][0].length !== 4)
|
|
throw new Error("TLV 3 should be 4 bytes");
|
|
return {
|
|
type: "nevent",
|
|
data: {
|
|
id: bytesToHex3(tlv[0][0]),
|
|
relays: tlv[1] ? tlv[1].map((d) => utf8Decoder.decode(d)) : [],
|
|
author: tlv[2]?.[0] ? bytesToHex3(tlv[2][0]) : void 0,
|
|
kind: tlv[3]?.[0] ? parseInt(bytesToHex3(tlv[3][0]), 16) : void 0
|
|
}
|
|
};
|
|
}
|
|
case "naddr": {
|
|
let tlv = parseTLV(data);
|
|
if (!tlv[0]?.[0])
|
|
throw new Error("missing TLV 0 for naddr");
|
|
if (!tlv[2]?.[0])
|
|
throw new Error("missing TLV 2 for naddr");
|
|
if (tlv[2][0].length !== 32)
|
|
throw new Error("TLV 2 should be 32 bytes");
|
|
if (!tlv[3]?.[0])
|
|
throw new Error("missing TLV 3 for naddr");
|
|
if (tlv[3][0].length !== 4)
|
|
throw new Error("TLV 3 should be 4 bytes");
|
|
return {
|
|
type: "naddr",
|
|
data: {
|
|
identifier: utf8Decoder.decode(tlv[0][0]),
|
|
pubkey: bytesToHex3(tlv[2][0]),
|
|
kind: parseInt(bytesToHex3(tlv[3][0]), 16),
|
|
relays: tlv[1] ? tlv[1].map((d) => utf8Decoder.decode(d)) : []
|
|
}
|
|
};
|
|
}
|
|
case "nsec":
|
|
return { type: prefix, data };
|
|
case "npub":
|
|
case "note":
|
|
return { type: prefix, data: bytesToHex3(data) };
|
|
default:
|
|
throw new Error(`unknown prefix ${prefix}`);
|
|
}
|
|
}
|
|
function parseTLV(data) {
|
|
let result = {};
|
|
let rest = data;
|
|
while (rest.length > 0) {
|
|
let t = rest[0];
|
|
let l = rest[1];
|
|
let v = rest.slice(2, 2 + l);
|
|
rest = rest.slice(2 + l);
|
|
if (v.length < l)
|
|
throw new Error(`not enough data to read on TLV ${t}`);
|
|
result[t] = result[t] || [];
|
|
result[t].push(v);
|
|
}
|
|
return result;
|
|
}
|
|
function nsecEncode(key) {
|
|
return encodeBytes("nsec", key);
|
|
}
|
|
function npubEncode(hex) {
|
|
return encodeBytes("npub", hexToBytes2(hex));
|
|
}
|
|
function noteEncode(hex) {
|
|
return encodeBytes("note", hexToBytes2(hex));
|
|
}
|
|
function encodeBech32(prefix, data) {
|
|
let words = bech32.toWords(data);
|
|
return bech32.encode(prefix, words, Bech32MaxSize);
|
|
}
|
|
function encodeBytes(prefix, bytes) {
|
|
return encodeBech32(prefix, bytes);
|
|
}
|
|
function nprofileEncode(profile) {
|
|
let data = encodeTLV({
|
|
0: [hexToBytes2(profile.pubkey)],
|
|
1: (profile.relays || []).map((url) => utf8Encoder.encode(url))
|
|
});
|
|
return encodeBech32("nprofile", data);
|
|
}
|
|
function neventEncode(event) {
|
|
let kindArray;
|
|
if (event.kind !== void 0) {
|
|
kindArray = integerToUint8Array(event.kind);
|
|
}
|
|
let data = encodeTLV({
|
|
0: [hexToBytes2(event.id)],
|
|
1: (event.relays || []).map((url) => utf8Encoder.encode(url)),
|
|
2: event.author ? [hexToBytes2(event.author)] : [],
|
|
3: kindArray ? [new Uint8Array(kindArray)] : []
|
|
});
|
|
return encodeBech32("nevent", data);
|
|
}
|
|
function naddrEncode(addr) {
|
|
let kind = new ArrayBuffer(4);
|
|
new DataView(kind).setUint32(0, addr.kind, false);
|
|
let data = encodeTLV({
|
|
0: [utf8Encoder.encode(addr.identifier)],
|
|
1: (addr.relays || []).map((url) => utf8Encoder.encode(url)),
|
|
2: [hexToBytes2(addr.pubkey)],
|
|
3: [new Uint8Array(kind)]
|
|
});
|
|
return encodeBech32("naddr", data);
|
|
}
|
|
function encodeTLV(tlv) {
|
|
let entries = [];
|
|
Object.entries(tlv).reverse().forEach(([t, vs]) => {
|
|
vs.forEach((v) => {
|
|
let entry = new Uint8Array(v.length + 2);
|
|
entry.set([parseInt(t)], 0);
|
|
entry.set([v.length], 1);
|
|
entry.set(v, 2);
|
|
entries.push(entry);
|
|
});
|
|
});
|
|
return concatBytes(...entries);
|
|
}
|
|
|
|
// references.ts
|
|
var mentionRegex = /\bnostr:((note|npub|naddr|nevent|nprofile)1\w+)\b|#\[(\d+)\]/g;
|
|
function parseReferences(evt) {
|
|
let references = [];
|
|
for (let ref of evt.content.matchAll(mentionRegex)) {
|
|
if (ref[2]) {
|
|
try {
|
|
let { type, data } = decode(ref[1]);
|
|
switch (type) {
|
|
case "npub": {
|
|
references.push({
|
|
text: ref[0],
|
|
profile: { pubkey: data, relays: [] }
|
|
});
|
|
break;
|
|
}
|
|
case "nprofile": {
|
|
references.push({
|
|
text: ref[0],
|
|
profile: data
|
|
});
|
|
break;
|
|
}
|
|
case "note": {
|
|
references.push({
|
|
text: ref[0],
|
|
event: { id: data, relays: [] }
|
|
});
|
|
break;
|
|
}
|
|
case "nevent": {
|
|
references.push({
|
|
text: ref[0],
|
|
event: data
|
|
});
|
|
break;
|
|
}
|
|
case "naddr": {
|
|
references.push({
|
|
text: ref[0],
|
|
address: data
|
|
});
|
|
break;
|
|
}
|
|
}
|
|
} catch (err) {
|
|
}
|
|
} else if (ref[3]) {
|
|
let idx = parseInt(ref[3], 10);
|
|
let tag = evt.tags[idx];
|
|
if (!tag)
|
|
continue;
|
|
switch (tag[0]) {
|
|
case "p": {
|
|
references.push({
|
|
text: ref[0],
|
|
profile: { pubkey: tag[1], relays: tag[2] ? [tag[2]] : [] }
|
|
});
|
|
break;
|
|
}
|
|
case "e": {
|
|
references.push({
|
|
text: ref[0],
|
|
event: { id: tag[1], relays: tag[2] ? [tag[2]] : [] }
|
|
});
|
|
break;
|
|
}
|
|
case "a": {
|
|
try {
|
|
let [kind, pubkey, identifier] = tag[1].split(":");
|
|
references.push({
|
|
text: ref[0],
|
|
address: {
|
|
identifier,
|
|
pubkey,
|
|
kind: parseInt(kind, 10),
|
|
relays: tag[2] ? [tag[2]] : []
|
|
}
|
|
});
|
|
} catch (err) {
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return references;
|
|
}
|
|
|
|
// nip04.ts
|
|
var nip04_exports = {};
|
|
__export(nip04_exports, {
|
|
decrypt: () => decrypt,
|
|
encrypt: () => encrypt
|
|
});
|
|
import { bytesToHex as bytesToHex4, randomBytes } from "@noble/hashes/utils";
|
|
import { secp256k1 } from "@noble/curves/secp256k1";
|
|
import { cbc } from "@noble/ciphers/aes";
|
|
import { base64 } from "@scure/base";
|
|
function encrypt(secretKey, pubkey, text) {
|
|
const privkey = secretKey instanceof Uint8Array ? bytesToHex4(secretKey) : secretKey;
|
|
const key = secp256k1.getSharedSecret(privkey, "02" + pubkey);
|
|
const normalizedKey = getNormalizedX(key);
|
|
let iv = Uint8Array.from(randomBytes(16));
|
|
let plaintext = utf8Encoder.encode(text);
|
|
let ciphertext = cbc(normalizedKey, iv).encrypt(plaintext);
|
|
let ctb64 = base64.encode(new Uint8Array(ciphertext));
|
|
let ivb64 = base64.encode(new Uint8Array(iv.buffer));
|
|
return `${ctb64}?iv=${ivb64}`;
|
|
}
|
|
function decrypt(secretKey, pubkey, data) {
|
|
const privkey = secretKey instanceof Uint8Array ? bytesToHex4(secretKey) : secretKey;
|
|
let [ctb64, ivb64] = data.split("?iv=");
|
|
let key = secp256k1.getSharedSecret(privkey, "02" + pubkey);
|
|
let normalizedKey = getNormalizedX(key);
|
|
let iv = base64.decode(ivb64);
|
|
let ciphertext = base64.decode(ctb64);
|
|
let plaintext = cbc(normalizedKey, iv).decrypt(ciphertext);
|
|
return utf8Decoder.decode(plaintext);
|
|
}
|
|
function getNormalizedX(key) {
|
|
return key.slice(1, 33);
|
|
}
|
|
|
|
// nip05.ts
|
|
var nip05_exports = {};
|
|
__export(nip05_exports, {
|
|
NIP05_REGEX: () => NIP05_REGEX,
|
|
isNip05: () => isNip05,
|
|
isValid: () => isValid,
|
|
queryProfile: () => queryProfile,
|
|
searchDomain: () => searchDomain,
|
|
useFetchImplementation: () => useFetchImplementation
|
|
});
|
|
var NIP05_REGEX = /^(?:([\w.+-]+)@)?([\w_-]+(\.[\w_-]+)+)$/;
|
|
var isNip05 = (value) => NIP05_REGEX.test(value || "");
|
|
var _fetch;
|
|
try {
|
|
_fetch = fetch;
|
|
} catch (_) {
|
|
null;
|
|
}
|
|
function useFetchImplementation(fetchImplementation) {
|
|
_fetch = fetchImplementation;
|
|
}
|
|
async function searchDomain(domain, query = "") {
|
|
try {
|
|
const url = `https://${domain}/.well-known/nostr.json?name=${query}`;
|
|
const res = await _fetch(url, { redirect: "manual" });
|
|
if (res.status !== 200) {
|
|
throw Error("Wrong response code");
|
|
}
|
|
const json = await res.json();
|
|
return json.names;
|
|
} catch (_) {
|
|
return {};
|
|
}
|
|
}
|
|
async function queryProfile(fullname) {
|
|
const match = fullname.match(NIP05_REGEX);
|
|
if (!match)
|
|
return null;
|
|
const [, name = "_", domain] = match;
|
|
try {
|
|
const url = `https://${domain}/.well-known/nostr.json?name=${name}`;
|
|
const res = await _fetch(url, { redirect: "manual" });
|
|
if (res.status !== 200) {
|
|
throw Error("Wrong response code");
|
|
}
|
|
const json = await res.json();
|
|
const pubkey = json.names[name];
|
|
return pubkey ? { pubkey, relays: json.relays?.[pubkey] } : null;
|
|
} catch (_e) {
|
|
return null;
|
|
}
|
|
}
|
|
async function isValid(pubkey, nip05) {
|
|
const res = await queryProfile(nip05);
|
|
return res ? res.pubkey === pubkey : false;
|
|
}
|
|
|
|
// nip10.ts
|
|
var nip10_exports = {};
|
|
__export(nip10_exports, {
|
|
parse: () => parse
|
|
});
|
|
function parse(event) {
|
|
const result = {
|
|
reply: void 0,
|
|
root: void 0,
|
|
mentions: [],
|
|
profiles: [],
|
|
quotes: []
|
|
};
|
|
let maybeParent;
|
|
let maybeRoot;
|
|
for (let i2 = event.tags.length - 1; i2 >= 0; i2--) {
|
|
const tag = event.tags[i2];
|
|
if (tag[0] === "e" && tag[1]) {
|
|
const [_, eTagEventId, eTagRelayUrl, eTagMarker, eTagAuthor] = tag;
|
|
const eventPointer = {
|
|
id: eTagEventId,
|
|
relays: eTagRelayUrl ? [eTagRelayUrl] : [],
|
|
author: eTagAuthor
|
|
};
|
|
if (eTagMarker === "root") {
|
|
result.root = eventPointer;
|
|
continue;
|
|
}
|
|
if (eTagMarker === "reply") {
|
|
result.reply = eventPointer;
|
|
continue;
|
|
}
|
|
if (eTagMarker === "mention") {
|
|
result.mentions.push(eventPointer);
|
|
continue;
|
|
}
|
|
if (!maybeParent) {
|
|
maybeParent = eventPointer;
|
|
} else {
|
|
maybeRoot = eventPointer;
|
|
}
|
|
result.mentions.push(eventPointer);
|
|
continue;
|
|
}
|
|
if (tag[0] === "q" && tag[1]) {
|
|
const [_, eTagEventId, eTagRelayUrl] = tag;
|
|
result.quotes.push({
|
|
id: eTagEventId,
|
|
relays: eTagRelayUrl ? [eTagRelayUrl] : []
|
|
});
|
|
}
|
|
if (tag[0] === "p" && tag[1]) {
|
|
result.profiles.push({
|
|
pubkey: tag[1],
|
|
relays: tag[2] ? [tag[2]] : []
|
|
});
|
|
continue;
|
|
}
|
|
}
|
|
if (!result.root) {
|
|
result.root = maybeRoot || maybeParent || result.reply;
|
|
}
|
|
if (!result.reply) {
|
|
result.reply = maybeParent || result.root;
|
|
}
|
|
;
|
|
[result.reply, result.root].forEach((ref) => {
|
|
if (!ref)
|
|
return;
|
|
let idx = result.mentions.indexOf(ref);
|
|
if (idx !== -1) {
|
|
result.mentions.splice(idx, 1);
|
|
}
|
|
if (ref.author) {
|
|
let author = result.profiles.find((p) => p.pubkey === ref.author);
|
|
if (author && author.relays) {
|
|
if (!ref.relays) {
|
|
ref.relays = [];
|
|
}
|
|
author.relays.forEach((url) => {
|
|
if (ref.relays?.indexOf(url) === -1)
|
|
ref.relays.push(url);
|
|
});
|
|
author.relays = ref.relays;
|
|
}
|
|
}
|
|
});
|
|
result.mentions.forEach((ref) => {
|
|
if (ref.author) {
|
|
let author = result.profiles.find((p) => p.pubkey === ref.author);
|
|
if (author && author.relays) {
|
|
if (!ref.relays) {
|
|
ref.relays = [];
|
|
}
|
|
author.relays.forEach((url) => {
|
|
if (ref.relays.indexOf(url) === -1)
|
|
ref.relays.push(url);
|
|
});
|
|
author.relays = ref.relays;
|
|
}
|
|
}
|
|
});
|
|
return result;
|
|
}
|
|
|
|
// nip11.ts
|
|
var nip11_exports = {};
|
|
__export(nip11_exports, {
|
|
fetchRelayInformation: () => fetchRelayInformation,
|
|
useFetchImplementation: () => useFetchImplementation2
|
|
});
|
|
var _fetch2;
|
|
try {
|
|
_fetch2 = fetch;
|
|
} catch {
|
|
}
|
|
function useFetchImplementation2(fetchImplementation) {
|
|
_fetch2 = fetchImplementation;
|
|
}
|
|
async function fetchRelayInformation(url) {
|
|
return await (await fetch(url.replace("ws://", "http://").replace("wss://", "https://"), {
|
|
headers: { Accept: "application/nostr+json" }
|
|
})).json();
|
|
}
|
|
|
|
// nip13.ts
|
|
var nip13_exports = {};
|
|
__export(nip13_exports, {
|
|
fastEventHash: () => fastEventHash,
|
|
getPow: () => getPow,
|
|
minePow: () => minePow
|
|
});
|
|
import { bytesToHex as bytesToHex5 } from "@noble/hashes/utils";
|
|
import { sha256 as sha2562 } from "@noble/hashes/sha256";
|
|
function getPow(hex) {
|
|
let count = 0;
|
|
for (let i2 = 0; i2 < 64; i2 += 8) {
|
|
const nibble = parseInt(hex.substring(i2, i2 + 8), 16);
|
|
if (nibble === 0) {
|
|
count += 32;
|
|
} else {
|
|
count += Math.clz32(nibble);
|
|
break;
|
|
}
|
|
}
|
|
return count;
|
|
}
|
|
function minePow(unsigned, difficulty) {
|
|
let count = 0;
|
|
const event = unsigned;
|
|
const tag = ["nonce", count.toString(), difficulty.toString()];
|
|
event.tags.push(tag);
|
|
while (true) {
|
|
const now2 = Math.floor(new Date().getTime() / 1e3);
|
|
if (now2 !== event.created_at) {
|
|
count = 0;
|
|
event.created_at = now2;
|
|
}
|
|
tag[1] = (++count).toString();
|
|
event.id = fastEventHash(event);
|
|
if (getPow(event.id) >= difficulty) {
|
|
break;
|
|
}
|
|
}
|
|
return event;
|
|
}
|
|
function fastEventHash(evt) {
|
|
return bytesToHex5(
|
|
sha2562(utf8Encoder.encode(JSON.stringify([0, evt.pubkey, evt.created_at, evt.kind, evt.tags, evt.content])))
|
|
);
|
|
}
|
|
|
|
// nip17.ts
|
|
var nip17_exports = {};
|
|
__export(nip17_exports, {
|
|
unwrapEvent: () => unwrapEvent2,
|
|
unwrapManyEvents: () => unwrapManyEvents2,
|
|
wrapEvent: () => wrapEvent2,
|
|
wrapManyEvents: () => wrapManyEvents2
|
|
});
|
|
|
|
// nip59.ts
|
|
var nip59_exports = {};
|
|
__export(nip59_exports, {
|
|
createRumor: () => createRumor,
|
|
createSeal: () => createSeal,
|
|
createWrap: () => createWrap,
|
|
unwrapEvent: () => unwrapEvent,
|
|
unwrapManyEvents: () => unwrapManyEvents,
|
|
wrapEvent: () => wrapEvent,
|
|
wrapManyEvents: () => wrapManyEvents
|
|
});
|
|
|
|
// nip44.ts
|
|
var nip44_exports = {};
|
|
__export(nip44_exports, {
|
|
decrypt: () => decrypt2,
|
|
encrypt: () => encrypt2,
|
|
getConversationKey: () => getConversationKey,
|
|
v2: () => v2
|
|
});
|
|
import { chacha20 } from "@noble/ciphers/chacha";
|
|
import { equalBytes } from "@noble/ciphers/utils";
|
|
import { secp256k1 as secp256k12 } from "@noble/curves/secp256k1";
|
|
import { extract as hkdf_extract, expand as hkdf_expand } from "@noble/hashes/hkdf";
|
|
import { hmac } from "@noble/hashes/hmac";
|
|
import { sha256 as sha2563 } from "@noble/hashes/sha256";
|
|
import { concatBytes as concatBytes2, randomBytes as randomBytes2 } from "@noble/hashes/utils";
|
|
import { base64 as base642 } from "@scure/base";
|
|
var minPlaintextSize = 1;
|
|
var maxPlaintextSize = 65535;
|
|
function getConversationKey(privkeyA, pubkeyB) {
|
|
const sharedX = secp256k12.getSharedSecret(privkeyA, "02" + pubkeyB).subarray(1, 33);
|
|
return hkdf_extract(sha2563, sharedX, "nip44-v2");
|
|
}
|
|
function getMessageKeys(conversationKey, nonce) {
|
|
const keys = hkdf_expand(sha2563, conversationKey, nonce, 76);
|
|
return {
|
|
chacha_key: keys.subarray(0, 32),
|
|
chacha_nonce: keys.subarray(32, 44),
|
|
hmac_key: keys.subarray(44, 76)
|
|
};
|
|
}
|
|
function calcPaddedLen(len) {
|
|
if (!Number.isSafeInteger(len) || len < 1)
|
|
throw new Error("expected positive integer");
|
|
if (len <= 32)
|
|
return 32;
|
|
const nextPower = 1 << Math.floor(Math.log2(len - 1)) + 1;
|
|
const chunk = nextPower <= 256 ? 32 : nextPower / 8;
|
|
return chunk * (Math.floor((len - 1) / chunk) + 1);
|
|
}
|
|
function writeU16BE(num) {
|
|
if (!Number.isSafeInteger(num) || num < minPlaintextSize || num > maxPlaintextSize)
|
|
throw new Error("invalid plaintext size: must be between 1 and 65535 bytes");
|
|
const arr = new Uint8Array(2);
|
|
new DataView(arr.buffer).setUint16(0, num, false);
|
|
return arr;
|
|
}
|
|
function pad(plaintext) {
|
|
const unpadded = utf8Encoder.encode(plaintext);
|
|
const unpaddedLen = unpadded.length;
|
|
const prefix = writeU16BE(unpaddedLen);
|
|
const suffix = new Uint8Array(calcPaddedLen(unpaddedLen) - unpaddedLen);
|
|
return concatBytes2(prefix, unpadded, suffix);
|
|
}
|
|
function unpad(padded) {
|
|
const unpaddedLen = new DataView(padded.buffer).getUint16(0);
|
|
const unpadded = padded.subarray(2, 2 + unpaddedLen);
|
|
if (unpaddedLen < minPlaintextSize || unpaddedLen > maxPlaintextSize || unpadded.length !== unpaddedLen || padded.length !== 2 + calcPaddedLen(unpaddedLen))
|
|
throw new Error("invalid padding");
|
|
return utf8Decoder.decode(unpadded);
|
|
}
|
|
function hmacAad(key, message, aad) {
|
|
if (aad.length !== 32)
|
|
throw new Error("AAD associated data must be 32 bytes");
|
|
const combined = concatBytes2(aad, message);
|
|
return hmac(sha2563, key, combined);
|
|
}
|
|
function decodePayload(payload) {
|
|
if (typeof payload !== "string")
|
|
throw new Error("payload must be a valid string");
|
|
const plen = payload.length;
|
|
if (plen < 132 || plen > 87472)
|
|
throw new Error("invalid payload length: " + plen);
|
|
if (payload[0] === "#")
|
|
throw new Error("unknown encryption version");
|
|
let data;
|
|
try {
|
|
data = base642.decode(payload);
|
|
} catch (error) {
|
|
throw new Error("invalid base64: " + error.message);
|
|
}
|
|
const dlen = data.length;
|
|
if (dlen < 99 || dlen > 65603)
|
|
throw new Error("invalid data length: " + dlen);
|
|
const vers = data[0];
|
|
if (vers !== 2)
|
|
throw new Error("unknown encryption version " + vers);
|
|
return {
|
|
nonce: data.subarray(1, 33),
|
|
ciphertext: data.subarray(33, -32),
|
|
mac: data.subarray(-32)
|
|
};
|
|
}
|
|
function encrypt2(plaintext, conversationKey, nonce = randomBytes2(32)) {
|
|
const { chacha_key, chacha_nonce, hmac_key } = getMessageKeys(conversationKey, nonce);
|
|
const padded = pad(plaintext);
|
|
const ciphertext = chacha20(chacha_key, chacha_nonce, padded);
|
|
const mac = hmacAad(hmac_key, ciphertext, nonce);
|
|
return base642.encode(concatBytes2(new Uint8Array([2]), nonce, ciphertext, mac));
|
|
}
|
|
function decrypt2(payload, conversationKey) {
|
|
const { nonce, ciphertext, mac } = decodePayload(payload);
|
|
const { chacha_key, chacha_nonce, hmac_key } = getMessageKeys(conversationKey, nonce);
|
|
const calculatedMac = hmacAad(hmac_key, ciphertext, nonce);
|
|
if (!equalBytes(calculatedMac, mac))
|
|
throw new Error("invalid MAC");
|
|
const padded = chacha20(chacha_key, chacha_nonce, ciphertext);
|
|
return unpad(padded);
|
|
}
|
|
var v2 = {
|
|
utils: {
|
|
getConversationKey,
|
|
calcPaddedLen
|
|
},
|
|
encrypt: encrypt2,
|
|
decrypt: decrypt2
|
|
};
|
|
|
|
// nip59.ts
|
|
var TWO_DAYS = 2 * 24 * 60 * 60;
|
|
var now = () => Math.round(Date.now() / 1e3);
|
|
var randomNow = () => Math.round(now() - Math.random() * TWO_DAYS);
|
|
var nip44ConversationKey = (privateKey, publicKey) => getConversationKey(privateKey, publicKey);
|
|
var nip44Encrypt = (data, privateKey, publicKey) => encrypt2(JSON.stringify(data), nip44ConversationKey(privateKey, publicKey));
|
|
var nip44Decrypt = (data, privateKey) => JSON.parse(decrypt2(data.content, nip44ConversationKey(privateKey, data.pubkey)));
|
|
function createRumor(event, privateKey) {
|
|
const rumor = {
|
|
created_at: now(),
|
|
content: "",
|
|
tags: [],
|
|
...event,
|
|
pubkey: getPublicKey(privateKey)
|
|
};
|
|
rumor.id = getEventHash(rumor);
|
|
return rumor;
|
|
}
|
|
function createSeal(rumor, privateKey, recipientPublicKey) {
|
|
return finalizeEvent(
|
|
{
|
|
kind: Seal,
|
|
content: nip44Encrypt(rumor, privateKey, recipientPublicKey),
|
|
created_at: randomNow(),
|
|
tags: []
|
|
},
|
|
privateKey
|
|
);
|
|
}
|
|
function createWrap(seal, recipientPublicKey) {
|
|
const randomKey = generateSecretKey();
|
|
return finalizeEvent(
|
|
{
|
|
kind: GiftWrap,
|
|
content: nip44Encrypt(seal, randomKey, recipientPublicKey),
|
|
created_at: randomNow(),
|
|
tags: [["p", recipientPublicKey]]
|
|
},
|
|
randomKey
|
|
);
|
|
}
|
|
function wrapEvent(event, senderPrivateKey, recipientPublicKey) {
|
|
const rumor = createRumor(event, senderPrivateKey);
|
|
const seal = createSeal(rumor, senderPrivateKey, recipientPublicKey);
|
|
return createWrap(seal, recipientPublicKey);
|
|
}
|
|
function wrapManyEvents(event, senderPrivateKey, recipientsPublicKeys) {
|
|
if (!recipientsPublicKeys || recipientsPublicKeys.length === 0) {
|
|
throw new Error("At least one recipient is required.");
|
|
}
|
|
const senderPublicKey = getPublicKey(senderPrivateKey);
|
|
const wrappeds = [wrapEvent(event, senderPrivateKey, senderPublicKey)];
|
|
recipientsPublicKeys.forEach((recipientPublicKey) => {
|
|
wrappeds.push(wrapEvent(event, senderPrivateKey, recipientPublicKey));
|
|
});
|
|
return wrappeds;
|
|
}
|
|
function unwrapEvent(wrap, recipientPrivateKey) {
|
|
const unwrappedSeal = nip44Decrypt(wrap, recipientPrivateKey);
|
|
return nip44Decrypt(unwrappedSeal, recipientPrivateKey);
|
|
}
|
|
function unwrapManyEvents(wrappedEvents, recipientPrivateKey) {
|
|
let unwrappedEvents = [];
|
|
wrappedEvents.forEach((e) => {
|
|
unwrappedEvents.push(unwrapEvent(e, recipientPrivateKey));
|
|
});
|
|
unwrappedEvents.sort((a, b) => a.created_at - b.created_at);
|
|
return unwrappedEvents;
|
|
}
|
|
|
|
// nip17.ts
|
|
function createEvent(recipients, message, conversationTitle, replyTo) {
|
|
const baseEvent = {
|
|
created_at: Math.ceil(Date.now() / 1e3),
|
|
kind: PrivateDirectMessage,
|
|
tags: [],
|
|
content: message
|
|
};
|
|
const recipientsArray = Array.isArray(recipients) ? recipients : [recipients];
|
|
recipientsArray.forEach(({ publicKey, relayUrl }) => {
|
|
baseEvent.tags.push(relayUrl ? ["p", publicKey, relayUrl] : ["p", publicKey]);
|
|
});
|
|
if (replyTo) {
|
|
baseEvent.tags.push(["e", replyTo.eventId, replyTo.relayUrl || "", "reply"]);
|
|
}
|
|
if (conversationTitle) {
|
|
baseEvent.tags.push(["subject", conversationTitle]);
|
|
}
|
|
return baseEvent;
|
|
}
|
|
function wrapEvent2(senderPrivateKey, recipient, message, conversationTitle, replyTo) {
|
|
const event = createEvent(recipient, message, conversationTitle, replyTo);
|
|
return wrapEvent(event, senderPrivateKey, recipient.publicKey);
|
|
}
|
|
function wrapManyEvents2(senderPrivateKey, recipients, message, conversationTitle, replyTo) {
|
|
if (!recipients || recipients.length === 0) {
|
|
throw new Error("At least one recipient is required.");
|
|
}
|
|
const senderPublicKey = getPublicKey(senderPrivateKey);
|
|
return [{ publicKey: senderPublicKey }, ...recipients].map(
|
|
(recipient) => wrapEvent2(senderPrivateKey, recipient, message, conversationTitle, replyTo)
|
|
);
|
|
}
|
|
var unwrapEvent2 = unwrapEvent;
|
|
var unwrapManyEvents2 = unwrapManyEvents;
|
|
|
|
// nip18.ts
|
|
var nip18_exports = {};
|
|
__export(nip18_exports, {
|
|
finishRepostEvent: () => finishRepostEvent,
|
|
getRepostedEvent: () => getRepostedEvent,
|
|
getRepostedEventPointer: () => getRepostedEventPointer
|
|
});
|
|
function finishRepostEvent(t, reposted, relayUrl, privateKey) {
|
|
let kind;
|
|
const tags = [...t.tags ?? [], ["e", reposted.id, relayUrl], ["p", reposted.pubkey]];
|
|
if (reposted.kind === ShortTextNote) {
|
|
kind = Repost;
|
|
} else {
|
|
kind = GenericRepost;
|
|
tags.push(["k", String(reposted.kind)]);
|
|
}
|
|
return finalizeEvent(
|
|
{
|
|
kind,
|
|
tags,
|
|
content: t.content === "" || reposted.tags?.find((tag) => tag[0] === "-") ? "" : JSON.stringify(reposted),
|
|
created_at: t.created_at
|
|
},
|
|
privateKey
|
|
);
|
|
}
|
|
function getRepostedEventPointer(event) {
|
|
if (![Repost, GenericRepost].includes(event.kind)) {
|
|
return void 0;
|
|
}
|
|
let lastETag;
|
|
let lastPTag;
|
|
for (let i2 = event.tags.length - 1; i2 >= 0 && (lastETag === void 0 || lastPTag === void 0); i2--) {
|
|
const tag = event.tags[i2];
|
|
if (tag.length >= 2) {
|
|
if (tag[0] === "e" && lastETag === void 0) {
|
|
lastETag = tag;
|
|
} else if (tag[0] === "p" && lastPTag === void 0) {
|
|
lastPTag = tag;
|
|
}
|
|
}
|
|
}
|
|
if (lastETag === void 0) {
|
|
return void 0;
|
|
}
|
|
return {
|
|
id: lastETag[1],
|
|
relays: [lastETag[2], lastPTag?.[2]].filter((x) => typeof x === "string"),
|
|
author: lastPTag?.[1]
|
|
};
|
|
}
|
|
function getRepostedEvent(event, { skipVerification } = {}) {
|
|
const pointer = getRepostedEventPointer(event);
|
|
if (pointer === void 0 || event.content === "") {
|
|
return void 0;
|
|
}
|
|
let repostedEvent;
|
|
try {
|
|
repostedEvent = JSON.parse(event.content);
|
|
} catch (error) {
|
|
return void 0;
|
|
}
|
|
if (repostedEvent.id !== pointer.id) {
|
|
return void 0;
|
|
}
|
|
if (!skipVerification && !verifyEvent(repostedEvent)) {
|
|
return void 0;
|
|
}
|
|
return repostedEvent;
|
|
}
|
|
|
|
// nip21.ts
|
|
var nip21_exports = {};
|
|
__export(nip21_exports, {
|
|
NOSTR_URI_REGEX: () => NOSTR_URI_REGEX,
|
|
parse: () => parse2,
|
|
test: () => test
|
|
});
|
|
var NOSTR_URI_REGEX = new RegExp(`nostr:(${BECH32_REGEX.source})`);
|
|
function test(value) {
|
|
return typeof value === "string" && new RegExp(`^${NOSTR_URI_REGEX.source}$`).test(value);
|
|
}
|
|
function parse2(uri) {
|
|
const match = uri.match(new RegExp(`^${NOSTR_URI_REGEX.source}$`));
|
|
if (!match)
|
|
throw new Error(`Invalid Nostr URI: ${uri}`);
|
|
return {
|
|
uri: match[0],
|
|
value: match[1],
|
|
decoded: decode(match[1])
|
|
};
|
|
}
|
|
|
|
// nip25.ts
|
|
var nip25_exports = {};
|
|
__export(nip25_exports, {
|
|
finishReactionEvent: () => finishReactionEvent,
|
|
getReactedEventPointer: () => getReactedEventPointer
|
|
});
|
|
function finishReactionEvent(t, reacted, privateKey) {
|
|
const inheritedTags = reacted.tags.filter((tag) => tag.length >= 2 && (tag[0] === "e" || tag[0] === "p"));
|
|
return finalizeEvent(
|
|
{
|
|
...t,
|
|
kind: Reaction,
|
|
tags: [...t.tags ?? [], ...inheritedTags, ["e", reacted.id], ["p", reacted.pubkey]],
|
|
content: t.content ?? "+"
|
|
},
|
|
privateKey
|
|
);
|
|
}
|
|
function getReactedEventPointer(event) {
|
|
if (event.kind !== Reaction) {
|
|
return void 0;
|
|
}
|
|
let lastETag;
|
|
let lastPTag;
|
|
for (let i2 = event.tags.length - 1; i2 >= 0 && (lastETag === void 0 || lastPTag === void 0); i2--) {
|
|
const tag = event.tags[i2];
|
|
if (tag.length >= 2) {
|
|
if (tag[0] === "e" && lastETag === void 0) {
|
|
lastETag = tag;
|
|
} else if (tag[0] === "p" && lastPTag === void 0) {
|
|
lastPTag = tag;
|
|
}
|
|
}
|
|
}
|
|
if (lastETag === void 0 || lastPTag === void 0) {
|
|
return void 0;
|
|
}
|
|
return {
|
|
id: lastETag[1],
|
|
relays: [lastETag[2], lastPTag[2]].filter((x) => x !== void 0),
|
|
author: lastPTag[1]
|
|
};
|
|
}
|
|
|
|
// nip27.ts
|
|
var nip27_exports = {};
|
|
__export(nip27_exports, {
|
|
parse: () => parse3
|
|
});
|
|
var noCharacter = /\W/m;
|
|
var noURLCharacter = /\W |\W$|$|,| /m;
|
|
function* parse3(content) {
|
|
const max = content.length;
|
|
let prevIndex = 0;
|
|
let index = 0;
|
|
while (index < max) {
|
|
let u = content.indexOf(":", index);
|
|
if (u === -1) {
|
|
break;
|
|
}
|
|
if (content.substring(u - 5, u) === "nostr") {
|
|
const m = content.substring(u + 60).match(noCharacter);
|
|
const end = m ? u + 60 + m.index : max;
|
|
try {
|
|
let pointer;
|
|
let { data, type } = decode(content.substring(u + 1, end));
|
|
switch (type) {
|
|
case "npub":
|
|
pointer = { pubkey: data };
|
|
break;
|
|
case "nsec":
|
|
case "note":
|
|
index = end + 1;
|
|
continue;
|
|
default:
|
|
pointer = data;
|
|
}
|
|
if (prevIndex !== u - 5) {
|
|
yield { type: "text", text: content.substring(prevIndex, u - 5) };
|
|
}
|
|
yield { type: "reference", pointer };
|
|
index = end;
|
|
prevIndex = index;
|
|
continue;
|
|
} catch (_err) {
|
|
index = u + 1;
|
|
continue;
|
|
}
|
|
} else if (content.substring(u - 5, u) === "https" || content.substring(u - 4, u) === "http") {
|
|
const m = content.substring(u + 4).match(noURLCharacter);
|
|
const end = m ? u + 4 + m.index : max;
|
|
const prefixLen = content[u - 1] === "s" ? 5 : 4;
|
|
try {
|
|
let url = new URL(content.substring(u - prefixLen, end));
|
|
if (url.hostname.indexOf(".") === -1) {
|
|
throw new Error("invalid url");
|
|
}
|
|
if (prevIndex !== u - prefixLen) {
|
|
yield { type: "text", text: content.substring(prevIndex, u - prefixLen) };
|
|
}
|
|
if (/\.(png|jpe?g|gif|webp)$/i.test(url.pathname)) {
|
|
yield { type: "image", url: url.toString() };
|
|
index = end;
|
|
prevIndex = index;
|
|
continue;
|
|
}
|
|
if (/\.(mp4|avi|webm|mkv)$/i.test(url.pathname)) {
|
|
yield { type: "video", url: url.toString() };
|
|
index = end;
|
|
prevIndex = index;
|
|
continue;
|
|
}
|
|
if (/\.(mp3|aac|ogg|opus)$/i.test(url.pathname)) {
|
|
yield { type: "audio", url: url.toString() };
|
|
index = end;
|
|
prevIndex = index;
|
|
continue;
|
|
}
|
|
yield { type: "url", url: url.toString() };
|
|
index = end;
|
|
prevIndex = index;
|
|
continue;
|
|
} catch (_err) {
|
|
index = end + 1;
|
|
continue;
|
|
}
|
|
} else if (content.substring(u - 3, u) === "wss" || content.substring(u - 2, u) === "ws") {
|
|
const m = content.substring(u + 4).match(noURLCharacter);
|
|
const end = m ? u + 4 + m.index : max;
|
|
const prefixLen = content[u - 1] === "s" ? 3 : 2;
|
|
try {
|
|
let url = new URL(content.substring(u - prefixLen, end));
|
|
if (url.hostname.indexOf(".") === -1) {
|
|
throw new Error("invalid ws url");
|
|
}
|
|
if (prevIndex !== u - prefixLen) {
|
|
yield { type: "text", text: content.substring(prevIndex, u - prefixLen) };
|
|
}
|
|
yield { type: "relay", url: url.toString() };
|
|
index = end;
|
|
prevIndex = index;
|
|
continue;
|
|
} catch (_err) {
|
|
index = end + 1;
|
|
continue;
|
|
}
|
|
} else {
|
|
index = u + 1;
|
|
continue;
|
|
}
|
|
}
|
|
if (prevIndex !== max) {
|
|
yield { type: "text", text: content.substring(prevIndex) };
|
|
}
|
|
}
|
|
|
|
// nip28.ts
|
|
var nip28_exports = {};
|
|
__export(nip28_exports, {
|
|
channelCreateEvent: () => channelCreateEvent,
|
|
channelHideMessageEvent: () => channelHideMessageEvent,
|
|
channelMessageEvent: () => channelMessageEvent,
|
|
channelMetadataEvent: () => channelMetadataEvent,
|
|
channelMuteUserEvent: () => channelMuteUserEvent
|
|
});
|
|
var channelCreateEvent = (t, privateKey) => {
|
|
let content;
|
|
if (typeof t.content === "object") {
|
|
content = JSON.stringify(t.content);
|
|
} else if (typeof t.content === "string") {
|
|
content = t.content;
|
|
} else {
|
|
return void 0;
|
|
}
|
|
return finalizeEvent(
|
|
{
|
|
kind: ChannelCreation,
|
|
tags: [...t.tags ?? []],
|
|
content,
|
|
created_at: t.created_at
|
|
},
|
|
privateKey
|
|
);
|
|
};
|
|
var channelMetadataEvent = (t, privateKey) => {
|
|
let content;
|
|
if (typeof t.content === "object") {
|
|
content = JSON.stringify(t.content);
|
|
} else if (typeof t.content === "string") {
|
|
content = t.content;
|
|
} else {
|
|
return void 0;
|
|
}
|
|
return finalizeEvent(
|
|
{
|
|
kind: ChannelMetadata,
|
|
tags: [["e", t.channel_create_event_id], ...t.tags ?? []],
|
|
content,
|
|
created_at: t.created_at
|
|
},
|
|
privateKey
|
|
);
|
|
};
|
|
var channelMessageEvent = (t, privateKey) => {
|
|
const tags = [["e", t.channel_create_event_id, t.relay_url, "root"]];
|
|
if (t.reply_to_channel_message_event_id) {
|
|
tags.push(["e", t.reply_to_channel_message_event_id, t.relay_url, "reply"]);
|
|
}
|
|
return finalizeEvent(
|
|
{
|
|
kind: ChannelMessage,
|
|
tags: [...tags, ...t.tags ?? []],
|
|
content: t.content,
|
|
created_at: t.created_at
|
|
},
|
|
privateKey
|
|
);
|
|
};
|
|
var channelHideMessageEvent = (t, privateKey) => {
|
|
let content;
|
|
if (typeof t.content === "object") {
|
|
content = JSON.stringify(t.content);
|
|
} else if (typeof t.content === "string") {
|
|
content = t.content;
|
|
} else {
|
|
return void 0;
|
|
}
|
|
return finalizeEvent(
|
|
{
|
|
kind: ChannelHideMessage,
|
|
tags: [["e", t.channel_message_event_id], ...t.tags ?? []],
|
|
content,
|
|
created_at: t.created_at
|
|
},
|
|
privateKey
|
|
);
|
|
};
|
|
var channelMuteUserEvent = (t, privateKey) => {
|
|
let content;
|
|
if (typeof t.content === "object") {
|
|
content = JSON.stringify(t.content);
|
|
} else if (typeof t.content === "string") {
|
|
content = t.content;
|
|
} else {
|
|
return void 0;
|
|
}
|
|
return finalizeEvent(
|
|
{
|
|
kind: ChannelMuteUser,
|
|
tags: [["p", t.pubkey_to_mute], ...t.tags ?? []],
|
|
content,
|
|
created_at: t.created_at
|
|
},
|
|
privateKey
|
|
);
|
|
};
|
|
|
|
// nip30.ts
|
|
var nip30_exports = {};
|
|
__export(nip30_exports, {
|
|
EMOJI_SHORTCODE_REGEX: () => EMOJI_SHORTCODE_REGEX,
|
|
matchAll: () => matchAll,
|
|
regex: () => regex,
|
|
replaceAll: () => replaceAll
|
|
});
|
|
var EMOJI_SHORTCODE_REGEX = /:(\w+):/;
|
|
var regex = () => new RegExp(`\\B${EMOJI_SHORTCODE_REGEX.source}\\B`, "g");
|
|
function* matchAll(content) {
|
|
const matches = content.matchAll(regex());
|
|
for (const match of matches) {
|
|
try {
|
|
const [shortcode, name] = match;
|
|
yield {
|
|
shortcode,
|
|
name,
|
|
start: match.index,
|
|
end: match.index + shortcode.length
|
|
};
|
|
} catch (_e) {
|
|
}
|
|
}
|
|
}
|
|
function replaceAll(content, replacer) {
|
|
return content.replaceAll(regex(), (shortcode, name) => {
|
|
return replacer({
|
|
shortcode,
|
|
name
|
|
});
|
|
});
|
|
}
|
|
|
|
// nip39.ts
|
|
var nip39_exports = {};
|
|
__export(nip39_exports, {
|
|
useFetchImplementation: () => useFetchImplementation3,
|
|
validateGithub: () => validateGithub
|
|
});
|
|
var _fetch3;
|
|
try {
|
|
_fetch3 = fetch;
|
|
} catch {
|
|
}
|
|
function useFetchImplementation3(fetchImplementation) {
|
|
_fetch3 = fetchImplementation;
|
|
}
|
|
async function validateGithub(pubkey, username, proof) {
|
|
try {
|
|
let res = await (await _fetch3(`https://gist.github.com/${username}/${proof}/raw`)).text();
|
|
return res === `Verifying that I control the following Nostr public key: ${pubkey}`;
|
|
} catch (_) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// nip47.ts
|
|
var nip47_exports = {};
|
|
__export(nip47_exports, {
|
|
makeNwcRequestEvent: () => makeNwcRequestEvent,
|
|
parseConnectionString: () => parseConnectionString
|
|
});
|
|
function parseConnectionString(connectionString) {
|
|
const { host, pathname, searchParams } = new URL(connectionString);
|
|
const pubkey = pathname || host;
|
|
const relay = searchParams.get("relay");
|
|
const secret = searchParams.get("secret");
|
|
if (!pubkey || !relay || !secret) {
|
|
throw new Error("invalid connection string");
|
|
}
|
|
return { pubkey, relay, secret };
|
|
}
|
|
async function makeNwcRequestEvent(pubkey, secretKey, invoice) {
|
|
const content = {
|
|
method: "pay_invoice",
|
|
params: {
|
|
invoice
|
|
}
|
|
};
|
|
const encryptedContent = encrypt(secretKey, pubkey, JSON.stringify(content));
|
|
const eventTemplate = {
|
|
kind: NWCWalletRequest,
|
|
created_at: Math.round(Date.now() / 1e3),
|
|
content: encryptedContent,
|
|
tags: [["p", pubkey]]
|
|
};
|
|
return finalizeEvent(eventTemplate, secretKey);
|
|
}
|
|
|
|
// nip54.ts
|
|
var nip54_exports = {};
|
|
__export(nip54_exports, {
|
|
normalizeIdentifier: () => normalizeIdentifier
|
|
});
|
|
function normalizeIdentifier(name) {
|
|
name = name.trim().toLowerCase();
|
|
name = name.normalize("NFKC");
|
|
return Array.from(name).map((char) => {
|
|
if (/\p{Letter}/u.test(char) || /\p{Number}/u.test(char)) {
|
|
return char;
|
|
}
|
|
return "-";
|
|
}).join("");
|
|
}
|
|
|
|
// nip57.ts
|
|
var nip57_exports = {};
|
|
__export(nip57_exports, {
|
|
getSatoshisAmountFromBolt11: () => getSatoshisAmountFromBolt11,
|
|
getZapEndpoint: () => getZapEndpoint,
|
|
makeZapReceipt: () => makeZapReceipt,
|
|
makeZapRequest: () => makeZapRequest,
|
|
useFetchImplementation: () => useFetchImplementation4,
|
|
validateZapRequest: () => validateZapRequest
|
|
});
|
|
import { bech32 as bech322 } from "@scure/base";
|
|
var _fetch4;
|
|
try {
|
|
_fetch4 = fetch;
|
|
} catch {
|
|
}
|
|
function useFetchImplementation4(fetchImplementation) {
|
|
_fetch4 = fetchImplementation;
|
|
}
|
|
async function getZapEndpoint(metadata) {
|
|
try {
|
|
let lnurl = "";
|
|
let { lud06, lud16 } = JSON.parse(metadata.content);
|
|
if (lud06) {
|
|
let { words } = bech322.decode(lud06, 1e3);
|
|
let data = bech322.fromWords(words);
|
|
lnurl = utf8Decoder.decode(data);
|
|
} else if (lud16) {
|
|
let [name, domain] = lud16.split("@");
|
|
lnurl = new URL(`/.well-known/lnurlp/${name}`, `https://${domain}`).toString();
|
|
} else {
|
|
return null;
|
|
}
|
|
let res = await _fetch4(lnurl);
|
|
let body = await res.json();
|
|
if (body.allowsNostr && body.nostrPubkey) {
|
|
return body.callback;
|
|
}
|
|
} catch (err) {
|
|
}
|
|
return null;
|
|
}
|
|
function makeZapRequest(params) {
|
|
let zr = {
|
|
kind: 9734,
|
|
created_at: Math.round(Date.now() / 1e3),
|
|
content: params.comment || "",
|
|
tags: [
|
|
["p", "pubkey" in params ? params.pubkey : params.event.pubkey],
|
|
["amount", params.amount.toString()],
|
|
["relays", ...params.relays]
|
|
]
|
|
};
|
|
if ("event" in params) {
|
|
zr.tags.push(["e", params.event.id]);
|
|
if (isReplaceableKind(params.event.kind)) {
|
|
const a = ["a", `${params.event.kind}:${params.event.pubkey}:`];
|
|
zr.tags.push(a);
|
|
} else if (isAddressableKind(params.event.kind)) {
|
|
let d = params.event.tags.find(([t, v]) => t === "d" && v);
|
|
if (!d)
|
|
throw new Error("d tag not found or is empty");
|
|
const a = ["a", `${params.event.kind}:${params.event.pubkey}:${d[1]}`];
|
|
zr.tags.push(a);
|
|
}
|
|
zr.tags.push(["k", params.event.kind.toString()]);
|
|
}
|
|
return zr;
|
|
}
|
|
function validateZapRequest(zapRequestString) {
|
|
let zapRequest;
|
|
try {
|
|
zapRequest = JSON.parse(zapRequestString);
|
|
} catch (err) {
|
|
return "Invalid zap request JSON.";
|
|
}
|
|
if (!validateEvent(zapRequest))
|
|
return "Zap request is not a valid Nostr event.";
|
|
if (!verifyEvent(zapRequest))
|
|
return "Invalid signature on zap request.";
|
|
let p = zapRequest.tags.find(([t, v]) => t === "p" && v);
|
|
if (!p)
|
|
return "Zap request doesn't have a 'p' tag.";
|
|
if (!p[1].match(/^[a-f0-9]{64}$/))
|
|
return "Zap request 'p' tag is not valid hex.";
|
|
let e = zapRequest.tags.find(([t, v]) => t === "e" && v);
|
|
if (e && !e[1].match(/^[a-f0-9]{64}$/))
|
|
return "Zap request 'e' tag is not valid hex.";
|
|
let relays = zapRequest.tags.find(([t, v]) => t === "relays" && v);
|
|
if (!relays)
|
|
return "Zap request doesn't have a 'relays' tag.";
|
|
return null;
|
|
}
|
|
function makeZapReceipt({
|
|
zapRequest,
|
|
preimage,
|
|
bolt11,
|
|
paidAt
|
|
}) {
|
|
let zr = JSON.parse(zapRequest);
|
|
let tagsFromZapRequest = zr.tags.filter(([t]) => t === "e" || t === "p" || t === "a");
|
|
let zap = {
|
|
kind: 9735,
|
|
created_at: Math.round(paidAt.getTime() / 1e3),
|
|
content: "",
|
|
tags: [...tagsFromZapRequest, ["P", zr.pubkey], ["bolt11", bolt11], ["description", zapRequest]]
|
|
};
|
|
if (preimage) {
|
|
zap.tags.push(["preimage", preimage]);
|
|
}
|
|
return zap;
|
|
}
|
|
function getSatoshisAmountFromBolt11(bolt11) {
|
|
if (bolt11.length < 50) {
|
|
return 0;
|
|
}
|
|
bolt11 = bolt11.substring(0, 50);
|
|
const idx = bolt11.lastIndexOf("1");
|
|
if (idx === -1) {
|
|
return 0;
|
|
}
|
|
const hrp = bolt11.substring(0, idx);
|
|
if (!hrp.startsWith("lnbc")) {
|
|
return 0;
|
|
}
|
|
const amount = hrp.substring(4);
|
|
if (amount.length < 1) {
|
|
return 0;
|
|
}
|
|
const char = amount[amount.length - 1];
|
|
const digit = char.charCodeAt(0) - "0".charCodeAt(0);
|
|
const isDigit = digit >= 0 && digit <= 9;
|
|
let cutPoint = amount.length - 1;
|
|
if (isDigit) {
|
|
cutPoint++;
|
|
}
|
|
if (cutPoint < 1) {
|
|
return 0;
|
|
}
|
|
const num = parseInt(amount.substring(0, cutPoint));
|
|
switch (char) {
|
|
case "m":
|
|
return num * 1e5;
|
|
case "u":
|
|
return num * 100;
|
|
case "n":
|
|
return num / 10;
|
|
case "p":
|
|
return num / 1e4;
|
|
default:
|
|
return num * 1e8;
|
|
}
|
|
}
|
|
|
|
// nip98.ts
|
|
var nip98_exports = {};
|
|
__export(nip98_exports, {
|
|
getToken: () => getToken,
|
|
hashPayload: () => hashPayload,
|
|
unpackEventFromToken: () => unpackEventFromToken,
|
|
validateEvent: () => validateEvent2,
|
|
validateEventKind: () => validateEventKind,
|
|
validateEventMethodTag: () => validateEventMethodTag,
|
|
validateEventPayloadTag: () => validateEventPayloadTag,
|
|
validateEventTimestamp: () => validateEventTimestamp,
|
|
validateEventUrlTag: () => validateEventUrlTag,
|
|
validateToken: () => validateToken
|
|
});
|
|
import { sha256 as sha2564 } from "@noble/hashes/sha256";
|
|
import { bytesToHex as bytesToHex6 } from "@noble/hashes/utils";
|
|
import { base64 as base643 } from "@scure/base";
|
|
var _authorizationScheme = "Nostr ";
|
|
async function getToken(loginUrl, httpMethod, sign, includeAuthorizationScheme = false, payload) {
|
|
const event = {
|
|
kind: HTTPAuth,
|
|
tags: [
|
|
["u", loginUrl],
|
|
["method", httpMethod]
|
|
],
|
|
created_at: Math.round(new Date().getTime() / 1e3),
|
|
content: ""
|
|
};
|
|
if (payload) {
|
|
event.tags.push(["payload", hashPayload(payload)]);
|
|
}
|
|
const signedEvent = await sign(event);
|
|
const authorizationScheme = includeAuthorizationScheme ? _authorizationScheme : "";
|
|
return authorizationScheme + base643.encode(utf8Encoder.encode(JSON.stringify(signedEvent)));
|
|
}
|
|
async function validateToken(token, url, method) {
|
|
const event = await unpackEventFromToken(token).catch((error) => {
|
|
throw error;
|
|
});
|
|
const valid = await validateEvent2(event, url, method).catch((error) => {
|
|
throw error;
|
|
});
|
|
return valid;
|
|
}
|
|
async function unpackEventFromToken(token) {
|
|
if (!token) {
|
|
throw new Error("Missing token");
|
|
}
|
|
token = token.replace(_authorizationScheme, "");
|
|
const eventB64 = utf8Decoder.decode(base643.decode(token));
|
|
if (!eventB64 || eventB64.length === 0 || !eventB64.startsWith("{")) {
|
|
throw new Error("Invalid token");
|
|
}
|
|
const event = JSON.parse(eventB64);
|
|
return event;
|
|
}
|
|
function validateEventTimestamp(event) {
|
|
if (!event.created_at) {
|
|
return false;
|
|
}
|
|
return Math.round(new Date().getTime() / 1e3) - event.created_at < 60;
|
|
}
|
|
function validateEventKind(event) {
|
|
return event.kind === HTTPAuth;
|
|
}
|
|
function validateEventUrlTag(event, url) {
|
|
const urlTag = event.tags.find((t) => t[0] === "u");
|
|
if (!urlTag) {
|
|
return false;
|
|
}
|
|
return urlTag.length > 0 && urlTag[1] === url;
|
|
}
|
|
function validateEventMethodTag(event, method) {
|
|
const methodTag = event.tags.find((t) => t[0] === "method");
|
|
if (!methodTag) {
|
|
return false;
|
|
}
|
|
return methodTag.length > 0 && methodTag[1].toLowerCase() === method.toLowerCase();
|
|
}
|
|
function hashPayload(payload) {
|
|
const hash = sha2564(utf8Encoder.encode(JSON.stringify(payload)));
|
|
return bytesToHex6(hash);
|
|
}
|
|
function validateEventPayloadTag(event, payload) {
|
|
const payloadTag = event.tags.find((t) => t[0] === "payload");
|
|
if (!payloadTag) {
|
|
return false;
|
|
}
|
|
const payloadHash = hashPayload(payload);
|
|
return payloadTag.length > 0 && payloadTag[1] === payloadHash;
|
|
}
|
|
async function validateEvent2(event, url, method, body) {
|
|
if (!verifyEvent(event)) {
|
|
throw new Error("Invalid nostr event, signature invalid");
|
|
}
|
|
if (!validateEventKind(event)) {
|
|
throw new Error("Invalid nostr event, kind invalid");
|
|
}
|
|
if (!validateEventTimestamp(event)) {
|
|
throw new Error("Invalid nostr event, created_at timestamp invalid");
|
|
}
|
|
if (!validateEventUrlTag(event, url)) {
|
|
throw new Error("Invalid nostr event, url tag invalid");
|
|
}
|
|
if (!validateEventMethodTag(event, method)) {
|
|
throw new Error("Invalid nostr event, method tag invalid");
|
|
}
|
|
if (Boolean(body) && typeof body === "object" && Object.keys(body).length > 0) {
|
|
if (!validateEventPayloadTag(event, body)) {
|
|
throw new Error("Invalid nostr event, payload tag does not match request body hash");
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
export {
|
|
Relay,
|
|
SimplePool,
|
|
finalizeEvent,
|
|
fakejson_exports as fj,
|
|
generateSecretKey,
|
|
getEventHash,
|
|
getFilterLimit,
|
|
getPublicKey,
|
|
kinds_exports as kinds,
|
|
matchFilter,
|
|
matchFilters,
|
|
mergeFilters,
|
|
nip04_exports as nip04,
|
|
nip05_exports as nip05,
|
|
nip10_exports as nip10,
|
|
nip11_exports as nip11,
|
|
nip13_exports as nip13,
|
|
nip17_exports as nip17,
|
|
nip18_exports as nip18,
|
|
nip19_exports as nip19,
|
|
nip21_exports as nip21,
|
|
nip25_exports as nip25,
|
|
nip27_exports as nip27,
|
|
nip28_exports as nip28,
|
|
nip30_exports as nip30,
|
|
nip39_exports as nip39,
|
|
nip42_exports as nip42,
|
|
nip44_exports as nip44,
|
|
nip47_exports as nip47,
|
|
nip54_exports as nip54,
|
|
nip57_exports as nip57,
|
|
nip59_exports as nip59,
|
|
nip98_exports as nip98,
|
|
parseReferences,
|
|
serializeEvent,
|
|
sortEvents,
|
|
utils_exports as utils,
|
|
validateEvent,
|
|
verifiedSymbol,
|
|
verifyEvent
|
|
};
|