You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
3462 lines
101 KiB
3462 lines
101 KiB
"use strict"; |
|
var __defProp = Object.defineProperty; |
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor; |
|
var __getOwnPropNames = Object.getOwnPropertyNames; |
|
var __hasOwnProp = Object.prototype.hasOwnProperty; |
|
var __export = (target, all) => { |
|
for (var name in all) |
|
__defProp(target, name, { get: all[name], enumerable: true }); |
|
}; |
|
var __copyProps = (to, from, except, desc) => { |
|
if (from && typeof from === "object" || typeof from === "function") { |
|
for (let key of __getOwnPropNames(from)) |
|
if (!__hasOwnProp.call(to, key) && key !== except) |
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); |
|
} |
|
return to; |
|
}; |
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); |
|
|
|
// index.ts |
|
var nostr_tools_exports = {}; |
|
__export(nostr_tools_exports, { |
|
Relay: () => Relay, |
|
SimplePool: () => SimplePool, |
|
finalizeEvent: () => finalizeEvent, |
|
fj: () => fakejson_exports, |
|
generateSecretKey: () => generateSecretKey, |
|
getEventHash: () => getEventHash, |
|
getFilterLimit: () => getFilterLimit, |
|
getPublicKey: () => getPublicKey, |
|
kinds: () => kinds_exports, |
|
matchFilter: () => matchFilter, |
|
matchFilters: () => matchFilters, |
|
mergeFilters: () => mergeFilters, |
|
nip04: () => nip04_exports, |
|
nip05: () => nip05_exports, |
|
nip10: () => nip10_exports, |
|
nip11: () => nip11_exports, |
|
nip13: () => nip13_exports, |
|
nip17: () => nip17_exports, |
|
nip18: () => nip18_exports, |
|
nip19: () => nip19_exports, |
|
nip21: () => nip21_exports, |
|
nip25: () => nip25_exports, |
|
nip27: () => nip27_exports, |
|
nip28: () => nip28_exports, |
|
nip30: () => nip30_exports, |
|
nip39: () => nip39_exports, |
|
nip42: () => nip42_exports, |
|
nip44: () => nip44_exports, |
|
nip47: () => nip47_exports, |
|
nip54: () => nip54_exports, |
|
nip57: () => nip57_exports, |
|
nip59: () => nip59_exports, |
|
nip77: () => nip77_exports, |
|
nip98: () => nip98_exports, |
|
parseReferences: () => parseReferences, |
|
serializeEvent: () => serializeEvent, |
|
sortEvents: () => sortEvents, |
|
utils: () => utils_exports, |
|
validateEvent: () => validateEvent, |
|
verifiedSymbol: () => verifiedSymbol, |
|
verifyEvent: () => verifyEvent |
|
}); |
|
module.exports = __toCommonJS(nostr_tools_exports); |
|
|
|
// pure.ts |
|
var import_secp256k1 = require("@noble/curves/secp256k1"); |
|
var import_utils2 = require("@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 |
|
var import_sha256 = require("@noble/hashes/sha256"); |
|
|
|
// utils.ts |
|
var utils_exports = {}; |
|
__export(utils_exports, { |
|
Queue: () => Queue, |
|
QueueNode: () => QueueNode, |
|
binarySearch: () => binarySearch, |
|
bytesToHex: () => import_utils.bytesToHex, |
|
hexToBytes: () => import_utils.hexToBytes, |
|
insertEventIntoAscendingList: () => insertEventIntoAscendingList, |
|
insertEventIntoDescendingList: () => insertEventIntoDescendingList, |
|
normalizeURL: () => normalizeURL, |
|
utf8Decoder: () => utf8Decoder, |
|
utf8Encoder: () => utf8Encoder |
|
}); |
|
var import_utils = require("@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); |
|
if (p.protocol === "http:") |
|
p.protocol = "ws:"; |
|
else if (p.protocol === "https:") |
|
p.protocol = "wss:"; |
|
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 import_secp256k1.schnorr.utils.randomPrivateKey(); |
|
} |
|
getPublicKey(secretKey) { |
|
return (0, import_utils2.bytesToHex)(import_secp256k1.schnorr.getPublicKey(secretKey)); |
|
} |
|
finalizeEvent(t, secretKey) { |
|
const event = t; |
|
event.pubkey = (0, import_utils2.bytesToHex)(import_secp256k1.schnorr.getPublicKey(secretKey)); |
|
event.id = getEventHash(event); |
|
event.sig = (0, import_utils2.bytesToHex)(import_secp256k1.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 = import_secp256k1.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 = (0, import_sha256.sha256)(utf8Encoder.encode(serializeEvent(event))); |
|
return (0, import_utils2.bytesToHex)(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, |
|
BlossomServerList: () => BlossomServerList, |
|
BookmarkList: () => BookmarkList, |
|
Bookmarksets: () => Bookmarksets, |
|
Calendar: () => Calendar, |
|
CalendarEventRSVP: () => CalendarEventRSVP, |
|
ChannelCreation: () => ChannelCreation, |
|
ChannelHideMessage: () => ChannelHideMessage, |
|
ChannelMessage: () => ChannelMessage, |
|
ChannelMetadata: () => ChannelMetadata, |
|
ChannelMuteUser: () => ChannelMuteUser, |
|
ChatMessage: () => ChatMessage, |
|
ClassifiedListing: () => ClassifiedListing, |
|
ClientAuth: () => ClientAuth, |
|
Comment: () => Comment, |
|
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, |
|
FavoriteRelays: () => FavoriteRelays, |
|
FileMessage: () => FileMessage, |
|
FileMetadata: () => FileMetadata, |
|
FileServerPreference: () => FileServerPreference, |
|
Followsets: () => Followsets, |
|
ForumThread: () => ForumThread, |
|
GenericRepost: () => GenericRepost, |
|
Genericlists: () => Genericlists, |
|
GiftWrap: () => GiftWrap, |
|
GroupMetadata: () => GroupMetadata, |
|
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, |
|
NormalVideo: () => NormalVideo, |
|
NostrConnect: () => NostrConnect, |
|
OpenTimestamps: () => OpenTimestamps, |
|
Photo: () => Photo, |
|
Pinlist: () => Pinlist, |
|
Poll: () => Poll, |
|
PollResponse: () => PollResponse, |
|
PrivateDirectMessage: () => PrivateDirectMessage, |
|
ProblemTracker: () => ProblemTracker, |
|
ProfileBadges: () => ProfileBadges, |
|
PublicChatsList: () => PublicChatsList, |
|
Reaction: () => Reaction, |
|
RecommendRelay: () => RecommendRelay, |
|
RelayList: () => RelayList, |
|
RelayReview: () => RelayReview, |
|
Relaysets: () => Relaysets, |
|
Report: () => Report, |
|
Reporting: () => Reporting, |
|
Repost: () => Repost, |
|
Seal: () => Seal, |
|
SearchRelaysList: () => SearchRelaysList, |
|
ShortTextNote: () => ShortTextNote, |
|
ShortVideo: () => ShortVideo, |
|
Time: () => Time, |
|
UserEmojiList: () => UserEmojiList, |
|
UserStatuses: () => UserStatuses, |
|
Voice: () => Voice, |
|
VoiceComment: () => VoiceComment, |
|
Zap: () => Zap, |
|
ZapGoal: () => ZapGoal, |
|
ZapRequest: () => ZapRequest, |
|
classifyKind: () => classifyKind, |
|
isAddressableKind: () => isAddressableKind, |
|
isEphemeralKind: () => isEphemeralKind, |
|
isKind: () => isKind, |
|
isRegularKind: () => isRegularKind, |
|
isReplaceableKind: () => isReplaceableKind |
|
}); |
|
function isRegularKind(kind) { |
|
return kind < 1e4 && kind !== 0 && kind !== 3; |
|
} |
|
function isReplaceableKind(kind) { |
|
return kind === 0 || kind === 3 || 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 ChatMessage = 9; |
|
var ForumThread = 11; |
|
var Seal = 13; |
|
var PrivateDirectMessage = 14; |
|
var FileMessage = 15; |
|
var GenericRepost = 16; |
|
var Photo = 20; |
|
var NormalVideo = 21; |
|
var ShortVideo = 22; |
|
var ChannelCreation = 40; |
|
var ChannelMetadata = 41; |
|
var ChannelMessage = 42; |
|
var ChannelHideMessage = 43; |
|
var ChannelMuteUser = 44; |
|
var OpenTimestamps = 1040; |
|
var GiftWrap = 1059; |
|
var Poll = 1068; |
|
var FileMetadata = 1063; |
|
var Comment = 1111; |
|
var LiveChatMessage = 1311; |
|
var Voice = 1222; |
|
var VoiceComment = 1244; |
|
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 PollResponse = 1018; |
|
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 FavoriteRelays = 10012; |
|
var InterestsList = 10015; |
|
var UserEmojiList = 10030; |
|
var DirectMessageRelaysList = 10050; |
|
var FileServerPreference = 10096; |
|
var BlossomServerList = 10063; |
|
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 RelayReview = 31987; |
|
var Handlerrecommendation = 31989; |
|
var Handlerinformation = 31990; |
|
var CommunityDefinition = 34550; |
|
var GroupMetadata = 39e3; |
|
|
|
// 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, reject) => { |
|
try { |
|
if (typeof MessageChannel !== "undefined") { |
|
const ch = new MessageChannel(); |
|
const handler = () => { |
|
ch.port1.removeEventListener("message", handler); |
|
resolve(); |
|
}; |
|
ch.port1.addEventListener("message", handler); |
|
ch.port2.postMessage(0); |
|
ch.port1.start(); |
|
} else { |
|
if (typeof setImmediate !== "undefined") { |
|
setImmediate(resolve); |
|
} else if (typeof setTimeout !== "undefined") { |
|
setTimeout(resolve, 0); |
|
} else { |
|
resolve(); |
|
} |
|
} |
|
} catch (e) { |
|
console.error("during yield: ", e); |
|
reject(e); |
|
} |
|
}); |
|
} |
|
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}`); |
|
onauth; |
|
baseEoseTimeout = 4400; |
|
connectionTimeout = 4400; |
|
publishTimeout = 4400; |
|
pingFrequency = 29e3; |
|
pingTimeout = 2e4; |
|
resubscribeBackoff = [1e4, 1e4, 1e4, 2e4, 2e4, 3e4, 6e4]; |
|
openSubs = /* @__PURE__ */ new Map(); |
|
enablePing; |
|
enableReconnect; |
|
connectionTimeoutHandle; |
|
reconnectTimeoutHandle; |
|
pingIntervalHandle; |
|
reconnectAttempts = 0; |
|
closedIntentionally = false; |
|
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; |
|
this.enableReconnect = opts.enableReconnect || false; |
|
} |
|
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 reconnect() { |
|
const backoff = this.resubscribeBackoff[Math.min(this.reconnectAttempts, this.resubscribeBackoff.length - 1)]; |
|
this.reconnectAttempts++; |
|
this.reconnectTimeoutHandle = setTimeout(async () => { |
|
try { |
|
await this.connect(); |
|
} catch (err) { |
|
} |
|
}, backoff); |
|
} |
|
handleHardClose(reason) { |
|
if (this.pingIntervalHandle) { |
|
clearInterval(this.pingIntervalHandle); |
|
this.pingIntervalHandle = void 0; |
|
} |
|
this._connected = false; |
|
this.connectionPromise = void 0; |
|
const wasIntentional = this.closedIntentionally; |
|
this.closedIntentionally = false; |
|
this.onclose?.(); |
|
if (this.enableReconnect && !wasIntentional) { |
|
this.reconnect(); |
|
} else { |
|
this.closeAllSubscriptions(reason); |
|
} |
|
} |
|
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 = () => { |
|
if (this.reconnectTimeoutHandle) { |
|
clearTimeout(this.reconnectTimeoutHandle); |
|
this.reconnectTimeoutHandle = void 0; |
|
} |
|
clearTimeout(this.connectionTimeoutHandle); |
|
this._connected = true; |
|
const isReconnection = this.reconnectAttempts > 0; |
|
this.reconnectAttempts = 0; |
|
for (const sub of this.openSubs.values()) { |
|
sub.eosed = false; |
|
if (isReconnection) { |
|
for (let f = 0; f < sub.filters.length; f++) { |
|
if (sub.lastEmitted) { |
|
sub.filters[f].since = sub.lastEmitted + 1; |
|
} |
|
} |
|
} |
|
sub.fire(); |
|
} |
|
if (this.enablePing) { |
|
this.pingIntervalHandle = setInterval(() => this.pingpong(), this.pingFrequency); |
|
} |
|
resolve(); |
|
}; |
|
this.ws.onerror = (ev) => { |
|
clearTimeout(this.connectionTimeoutHandle); |
|
reject(ev.message || "websocket error"); |
|
this.handleHardClose("relay connection errored"); |
|
}; |
|
this.ws.onclose = (ev) => { |
|
clearTimeout(this.connectionTimeoutHandle); |
|
reject(ev.message || "websocket closed"); |
|
this.handleHardClose("relay connection closed"); |
|
}; |
|
this.ws.onmessage = this._onmessage.bind(this); |
|
}); |
|
return this.connectionPromise; |
|
} |
|
waitForPingPong() { |
|
return new Promise((resolve) => { |
|
; |
|
this.ws.once("pong", () => resolve(true)); |
|
this.ws.ping(); |
|
}); |
|
} |
|
waitForDummyReq() { |
|
return new Promise((resolve, reject) => { |
|
if (!this.connectionPromise) |
|
return reject(new Error(`no connection to ${this.url}, can't ping`)); |
|
try { |
|
const sub = this.subscribe( |
|
[{ ids: ["aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"], limit: 0 }], |
|
{ |
|
label: "forced-ping", |
|
oneose: () => { |
|
resolve(true); |
|
sub.close(); |
|
}, |
|
onclose() { |
|
resolve(true); |
|
}, |
|
eoseTimeout: this.pingTimeout + 1e3 |
|
} |
|
); |
|
} catch (err) { |
|
reject(err); |
|
} |
|
}); |
|
} |
|
async pingpong() { |
|
if (this.ws?.readyState === 1) { |
|
const result = await Promise.any([ |
|
this.ws && this.ws.ping && this.ws.once ? this.waitForPingPong() : this.waitForDummyReq(), |
|
new Promise((res) => setTimeout(() => res(false), this.pingTimeout)) |
|
]); |
|
if (!result) { |
|
if (this.ws?.readyState === this._WebSocket.OPEN) { |
|
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); |
|
} |
|
if (!so.lastEmitted || so.lastEmitted < event.created_at) |
|
so.lastEmitted = event.created_at; |
|
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]; |
|
if (this.onauth) { |
|
this.auth(this.onauth); |
|
} |
|
return; |
|
} |
|
default: { |
|
const so = this.openSubs.get(data[1]); |
|
so?.oncustom?.(data); |
|
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 sub = this.prepareSubscription(filters, params); |
|
sub.fire(); |
|
return sub; |
|
} |
|
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.closedIntentionally = true; |
|
if (this.reconnectTimeoutHandle) { |
|
clearTimeout(this.reconnectTimeoutHandle); |
|
this.reconnectTimeoutHandle = void 0; |
|
} |
|
if (this.pingIntervalHandle) { |
|
clearInterval(this.pingIntervalHandle); |
|
this.pingIntervalHandle = void 0; |
|
} |
|
this.closeAllSubscriptions("relay connection closed by us"); |
|
this._connected = false; |
|
this.onclose?.(); |
|
if (this.ws?.readyState === this._WebSocket.OPEN) { |
|
this.ws?.close(); |
|
} |
|
} |
|
_onmessage(ev) { |
|
this.incomingMessageQueue.enqueue(ev.data); |
|
if (!this.queueRunning) { |
|
this.runQueue(); |
|
} |
|
} |
|
}; |
|
var Subscription = class { |
|
relay; |
|
id; |
|
lastEmitted; |
|
closed = false; |
|
eosed = false; |
|
filters; |
|
alreadyHaveEvent; |
|
receivedEvent; |
|
onevent; |
|
oneose; |
|
onclose; |
|
oncustom; |
|
eoseTimeout; |
|
eoseTimeoutHandle; |
|
constructor(relay, id, filters, params) { |
|
if (filters.length === 0) |
|
throw new Error("subscription can't be created with zero filters"); |
|
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, options) { |
|
super(url, { verifyEvent, websocketImplementation: _WebSocket, ...options }); |
|
} |
|
static async connect(url, options) { |
|
const relay = new Relay(url, options); |
|
await relay.connect(); |
|
return relay; |
|
} |
|
}; |
|
|
|
// abstract-pool.ts |
|
var AbstractSimplePool = class { |
|
relays = /* @__PURE__ */ new Map(); |
|
seenOn = /* @__PURE__ */ new Map(); |
|
trackRelays = false; |
|
verifyEvent; |
|
enablePing; |
|
enableReconnect; |
|
automaticallyAuth; |
|
trustedRelayURLs = /* @__PURE__ */ new Set(); |
|
_WebSocket; |
|
constructor(opts) { |
|
this.verifyEvent = opts.verifyEvent; |
|
this._WebSocket = opts.websocketImplementation; |
|
this.enablePing = opts.enablePing; |
|
this.enableReconnect = opts.enableReconnect || false; |
|
this.automaticallyAuth = opts.automaticallyAuth; |
|
} |
|
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, |
|
enableReconnect: this.enableReconnect |
|
}); |
|
relay.onclose = () => { |
|
if (relay && !relay.enableReconnect) { |
|
this.relays.delete(url); |
|
} |
|
}; |
|
if (params?.connectionTimeout) |
|
relay.connectionTimeout = params.connectionTimeout; |
|
this.relays.set(url, relay); |
|
} |
|
if (this.automaticallyAuth) { |
|
const authSignerFn = this.automaticallyAuth(url); |
|
if (authSignerFn) { |
|
relay.onauth = authSignerFn; |
|
} |
|
} |
|
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) { |
|
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) { |
|
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) { |
|
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 === groupedRequests.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 === groupedRequests.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) { |
|
const subcloser = this.subscribe(relays, filter, { |
|
...params, |
|
oneose() { |
|
subcloser.close("closed automatically on eose"); |
|
} |
|
}); |
|
return subcloser; |
|
} |
|
subscribeManyEose(relays, filter, params) { |
|
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 |
|
}); |
|
var import_utils6 = require("@noble/hashes/utils"); |
|
var import_base = require("@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 } = import_base.bech32.decode(code, Bech32MaxSize); |
|
let data = new Uint8Array(import_base.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: (0, import_utils6.bytesToHex)(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: (0, import_utils6.bytesToHex)(tlv[0][0]), |
|
relays: tlv[1] ? tlv[1].map((d) => utf8Decoder.decode(d)) : [], |
|
author: tlv[2]?.[0] ? (0, import_utils6.bytesToHex)(tlv[2][0]) : void 0, |
|
kind: tlv[3]?.[0] ? parseInt((0, import_utils6.bytesToHex)(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: (0, import_utils6.bytesToHex)(tlv[2][0]), |
|
kind: parseInt((0, import_utils6.bytesToHex)(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: (0, import_utils6.bytesToHex)(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", (0, import_utils6.hexToBytes)(hex)); |
|
} |
|
function noteEncode(hex) { |
|
return encodeBytes("note", (0, import_utils6.hexToBytes)(hex)); |
|
} |
|
function encodeBech32(prefix, data) { |
|
let words = import_base.bech32.toWords(data); |
|
return import_base.bech32.encode(prefix, words, Bech32MaxSize); |
|
} |
|
function encodeBytes(prefix, bytes) { |
|
return encodeBech32(prefix, bytes); |
|
} |
|
function nprofileEncode(profile) { |
|
let data = encodeTLV({ |
|
0: [(0, import_utils6.hexToBytes)(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: [(0, import_utils6.hexToBytes)(event.id)], |
|
1: (event.relays || []).map((url) => utf8Encoder.encode(url)), |
|
2: event.author ? [(0, import_utils6.hexToBytes)(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: [(0, import_utils6.hexToBytes)(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 (0, import_utils6.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 |
|
}); |
|
var import_utils8 = require("@noble/hashes/utils"); |
|
var import_secp256k12 = require("@noble/curves/secp256k1"); |
|
var import_aes = require("@noble/ciphers/aes"); |
|
var import_base2 = require("@scure/base"); |
|
function encrypt(secretKey, pubkey, text) { |
|
const privkey = secretKey instanceof Uint8Array ? (0, import_utils8.bytesToHex)(secretKey) : secretKey; |
|
const key = import_secp256k12.secp256k1.getSharedSecret(privkey, "02" + pubkey); |
|
const normalizedKey = getNormalizedX(key); |
|
let iv = Uint8Array.from((0, import_utils8.randomBytes)(16)); |
|
let plaintext = utf8Encoder.encode(text); |
|
let ciphertext = (0, import_aes.cbc)(normalizedKey, iv).encrypt(plaintext); |
|
let ctb64 = import_base2.base64.encode(new Uint8Array(ciphertext)); |
|
let ivb64 = import_base2.base64.encode(new Uint8Array(iv.buffer)); |
|
return `${ctb64}?iv=${ivb64}`; |
|
} |
|
function decrypt(secretKey, pubkey, data) { |
|
const privkey = secretKey instanceof Uint8Array ? (0, import_utils8.bytesToHex)(secretKey) : secretKey; |
|
let [ctb64, ivb64] = data.split("?iv="); |
|
let key = import_secp256k12.secp256k1.getSharedSecret(privkey, "02" + pubkey); |
|
let normalizedKey = getNormalizedX(key); |
|
let iv = import_base2.base64.decode(ivb64); |
|
let ciphertext = import_base2.base64.decode(ctb64); |
|
let plaintext = (0, import_aes.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 |
|
}); |
|
var import_utils10 = require("@noble/hashes/utils"); |
|
var import_sha2562 = require("@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 (0, import_utils10.bytesToHex)( |
|
(0, import_sha2562.sha256)(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 |
|
}); |
|
var import_chacha = require("@noble/ciphers/chacha"); |
|
var import_utils12 = require("@noble/ciphers/utils"); |
|
var import_secp256k13 = require("@noble/curves/secp256k1"); |
|
var import_hkdf = require("@noble/hashes/hkdf"); |
|
var import_hmac = require("@noble/hashes/hmac"); |
|
var import_sha2563 = require("@noble/hashes/sha256"); |
|
var import_utils13 = require("@noble/hashes/utils"); |
|
var import_base3 = require("@scure/base"); |
|
var minPlaintextSize = 1; |
|
var maxPlaintextSize = 65535; |
|
function getConversationKey(privkeyA, pubkeyB) { |
|
const sharedX = import_secp256k13.secp256k1.getSharedSecret(privkeyA, "02" + pubkeyB).subarray(1, 33); |
|
return (0, import_hkdf.extract)(import_sha2563.sha256, sharedX, "nip44-v2"); |
|
} |
|
function getMessageKeys(conversationKey, nonce) { |
|
const keys = (0, import_hkdf.expand)(import_sha2563.sha256, 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 (0, import_utils13.concatBytes)(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 = (0, import_utils13.concatBytes)(aad, message); |
|
return (0, import_hmac.hmac)(import_sha2563.sha256, 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 = import_base3.base64.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 = (0, import_utils13.randomBytes)(32)) { |
|
const { chacha_key, chacha_nonce, hmac_key } = getMessageKeys(conversationKey, nonce); |
|
const padded = pad(plaintext); |
|
const ciphertext = (0, import_chacha.chacha20)(chacha_key, chacha_nonce, padded); |
|
const mac = hmacAad(hmac_key, ciphertext, nonce); |
|
return import_base3.base64.encode((0, import_utils13.concatBytes)(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 (!(0, import_utils12.equalBytes)(calculatedMac, mac)) |
|
throw new Error("invalid MAC"); |
|
const padded = (0, import_chacha.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; |
|
var MAX_HASHTAG_LENGTH = 42; |
|
function* parse3(content) { |
|
let emojis = []; |
|
if (typeof content !== "string") { |
|
for (let i2 = 0; i2 < content.tags.length; i2++) { |
|
const tag = content.tags[i2]; |
|
if (tag[0] === "emoji" && tag.length >= 3) { |
|
emojis.push({ type: "emoji", shortcode: tag[1], url: tag[2] }); |
|
} |
|
} |
|
content = content.content; |
|
} |
|
const max = content.length; |
|
let prevIndex = 0; |
|
let index = 0; |
|
mainloop: |
|
while (index < max) { |
|
const u = content.indexOf(":", index); |
|
const h = content.indexOf("#", index); |
|
if (u === -1 && h === -1) { |
|
break mainloop; |
|
} |
|
if (u === -1 || h >= 0 && h < u) { |
|
if (h === 0 || content[h - 1] === " ") { |
|
const m = content.slice(h + 1, h + MAX_HASHTAG_LENGTH).match(noCharacter); |
|
const end = m ? h + 1 + m.index : max; |
|
yield { type: "text", text: content.slice(prevIndex, h) }; |
|
yield { type: "hashtag", value: content.slice(h + 1, end) }; |
|
index = end; |
|
prevIndex = index; |
|
continue mainloop; |
|
} |
|
index = h + 1; |
|
continue mainloop; |
|
} |
|
if (content.slice(u - 5, u) === "nostr") { |
|
const m = content.slice(u + 60).match(noCharacter); |
|
const end = m ? u + 60 + m.index : max; |
|
try { |
|
let pointer; |
|
let { data, type } = decode(content.slice(u + 1, end)); |
|
switch (type) { |
|
case "npub": |
|
pointer = { pubkey: data }; |
|
break; |
|
case "note": |
|
pointer = { id: data }; |
|
break; |
|
case "nsec": |
|
index = end + 1; |
|
continue; |
|
default: |
|
pointer = data; |
|
} |
|
if (prevIndex !== u - 5) { |
|
yield { type: "text", text: content.slice(prevIndex, u - 5) }; |
|
} |
|
yield { type: "reference", pointer }; |
|
index = end; |
|
prevIndex = index; |
|
continue mainloop; |
|
} catch (_err) { |
|
index = u + 1; |
|
continue mainloop; |
|
} |
|
} else if (content.slice(u - 5, u) === "https" || content.slice(u - 4, u) === "http") { |
|
const m = content.slice(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.slice(u - prefixLen, end)); |
|
if (url.hostname.indexOf(".") === -1) { |
|
throw new Error("invalid url"); |
|
} |
|
if (prevIndex !== u - prefixLen) { |
|
yield { type: "text", text: content.slice(prevIndex, u - prefixLen) }; |
|
} |
|
if (/\.(png|jpe?g|gif|webp|heic|svg)$/i.test(url.pathname)) { |
|
yield { type: "image", url: url.toString() }; |
|
index = end; |
|
prevIndex = index; |
|
continue mainloop; |
|
} |
|
if (/\.(mp4|avi|webm|mkv|mov)$/i.test(url.pathname)) { |
|
yield { type: "video", url: url.toString() }; |
|
index = end; |
|
prevIndex = index; |
|
continue mainloop; |
|
} |
|
if (/\.(mp3|aac|ogg|opus|wav|flac)$/i.test(url.pathname)) { |
|
yield { type: "audio", url: url.toString() }; |
|
index = end; |
|
prevIndex = index; |
|
continue mainloop; |
|
} |
|
yield { type: "url", url: url.toString() }; |
|
index = end; |
|
prevIndex = index; |
|
continue mainloop; |
|
} catch (_err) { |
|
index = end + 1; |
|
continue mainloop; |
|
} |
|
} else if (content.slice(u - 3, u) === "wss" || content.slice(u - 2, u) === "ws") { |
|
const m = content.slice(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.slice(u - prefixLen, end)); |
|
if (url.hostname.indexOf(".") === -1) { |
|
throw new Error("invalid ws url"); |
|
} |
|
if (prevIndex !== u - prefixLen) { |
|
yield { type: "text", text: content.slice(prevIndex, u - prefixLen) }; |
|
} |
|
yield { type: "relay", url: url.toString() }; |
|
index = end; |
|
prevIndex = index; |
|
continue mainloop; |
|
} catch (_err) { |
|
index = end + 1; |
|
continue mainloop; |
|
} |
|
} else { |
|
for (let e = 0; e < emojis.length; e++) { |
|
const emoji = emojis[e]; |
|
if (content[u + emoji.shortcode.length + 1] === ":" && content.slice(u + 1, u + emoji.shortcode.length + 1) === emoji.shortcode) { |
|
if (prevIndex !== u) { |
|
yield { type: "text", text: content.slice(prevIndex, u) }; |
|
} |
|
yield emoji; |
|
index = u + emoji.shortcode.length + 2; |
|
prevIndex = index; |
|
continue mainloop; |
|
} |
|
} |
|
index = u + 1; |
|
continue mainloop; |
|
} |
|
} |
|
if (prevIndex !== max) { |
|
yield { type: "text", text: content.slice(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 |
|
}); |
|
var import_base4 = require("@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 (lud16) { |
|
let [name, domain] = lud16.split("@"); |
|
lnurl = new URL(`/.well-known/lnurlp/${name}`, `https://${domain}`).toString(); |
|
} else if (lud06) { |
|
let { words } = import_base4.bech32.decode(lud06, 1e3); |
|
let data = import_base4.bech32.fromWords(words); |
|
lnurl = utf8Decoder.decode(data); |
|
} 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; |
|
} |
|
} |
|
|
|
// nip77.ts |
|
var nip77_exports = {}; |
|
__export(nip77_exports, { |
|
Negentropy: () => Negentropy, |
|
NegentropyStorageVector: () => NegentropyStorageVector, |
|
NegentropySync: () => NegentropySync |
|
}); |
|
var import_utils16 = require("@noble/ciphers/utils"); |
|
var import_sha2564 = require("@noble/hashes/sha256"); |
|
var PROTOCOL_VERSION = 97; |
|
var ID_SIZE = 32; |
|
var FINGERPRINT_SIZE = 16; |
|
var Mode = { |
|
Skip: 0, |
|
Fingerprint: 1, |
|
IdList: 2 |
|
}; |
|
var WrappedBuffer = class { |
|
_raw; |
|
length; |
|
constructor(buffer) { |
|
if (typeof buffer === "number") { |
|
this._raw = new Uint8Array(buffer); |
|
this.length = 0; |
|
} else if (buffer instanceof Uint8Array) { |
|
this._raw = new Uint8Array(buffer); |
|
this.length = buffer.length; |
|
} else { |
|
this._raw = new Uint8Array(512); |
|
this.length = 0; |
|
} |
|
} |
|
unwrap() { |
|
return this._raw.subarray(0, this.length); |
|
} |
|
get capacity() { |
|
return this._raw.byteLength; |
|
} |
|
extend(buf) { |
|
if (buf instanceof WrappedBuffer) |
|
buf = buf.unwrap(); |
|
if (typeof buf.length !== "number") |
|
throw Error("bad length"); |
|
const targetSize = buf.length + this.length; |
|
if (this.capacity < targetSize) { |
|
const oldRaw = this._raw; |
|
const newCapacity = Math.max(this.capacity * 2, targetSize); |
|
this._raw = new Uint8Array(newCapacity); |
|
this._raw.set(oldRaw); |
|
} |
|
this._raw.set(buf, this.length); |
|
this.length += buf.length; |
|
} |
|
shift() { |
|
const first = this._raw[0]; |
|
this._raw = this._raw.subarray(1); |
|
this.length--; |
|
return first; |
|
} |
|
shiftN(n = 1) { |
|
const firstSubarray = this._raw.subarray(0, n); |
|
this._raw = this._raw.subarray(n); |
|
this.length -= n; |
|
return firstSubarray; |
|
} |
|
}; |
|
function decodeVarInt(buf) { |
|
let res = 0; |
|
while (1) { |
|
if (buf.length === 0) |
|
throw Error("parse ends prematurely"); |
|
let byte = buf.shift(); |
|
res = res << 7 | byte & 127; |
|
if ((byte & 128) === 0) |
|
break; |
|
} |
|
return res; |
|
} |
|
function encodeVarInt(n) { |
|
if (n === 0) |
|
return new WrappedBuffer(new Uint8Array([0])); |
|
let o = []; |
|
while (n !== 0) { |
|
o.push(n & 127); |
|
n >>>= 7; |
|
} |
|
o.reverse(); |
|
for (let i2 = 0; i2 < o.length - 1; i2++) |
|
o[i2] |= 128; |
|
return new WrappedBuffer(new Uint8Array(o)); |
|
} |
|
function getByte(buf) { |
|
return getBytes(buf, 1)[0]; |
|
} |
|
function getBytes(buf, n) { |
|
if (buf.length < n) |
|
throw Error("parse ends prematurely"); |
|
return buf.shiftN(n); |
|
} |
|
var Accumulator = class { |
|
buf; |
|
constructor() { |
|
this.setToZero(); |
|
} |
|
setToZero() { |
|
this.buf = new Uint8Array(ID_SIZE); |
|
} |
|
add(otherBuf) { |
|
let currCarry = 0, nextCarry = 0; |
|
let p = new DataView(this.buf.buffer); |
|
let po = new DataView(otherBuf.buffer); |
|
for (let i2 = 0; i2 < 8; i2++) { |
|
let offset = i2 * 4; |
|
let orig = p.getUint32(offset, true); |
|
let otherV = po.getUint32(offset, true); |
|
let next = orig; |
|
next += currCarry; |
|
next += otherV; |
|
if (next > 4294967295) |
|
nextCarry = 1; |
|
p.setUint32(offset, next & 4294967295, true); |
|
currCarry = nextCarry; |
|
nextCarry = 0; |
|
} |
|
} |
|
negate() { |
|
let p = new DataView(this.buf.buffer); |
|
for (let i2 = 0; i2 < 8; i2++) { |
|
let offset = i2 * 4; |
|
p.setUint32(offset, ~p.getUint32(offset, true)); |
|
} |
|
let one = new Uint8Array(ID_SIZE); |
|
one[0] = 1; |
|
this.add(one); |
|
} |
|
getFingerprint(n) { |
|
let input = new WrappedBuffer(); |
|
input.extend(this.buf); |
|
input.extend(encodeVarInt(n)); |
|
let hash = (0, import_sha2564.sha256)(input.unwrap()); |
|
return hash.subarray(0, FINGERPRINT_SIZE); |
|
} |
|
}; |
|
var NegentropyStorageVector = class { |
|
items; |
|
sealed; |
|
constructor() { |
|
this.items = []; |
|
this.sealed = false; |
|
} |
|
insert(timestamp, id) { |
|
if (this.sealed) |
|
throw Error("already sealed"); |
|
const idb = (0, import_utils16.hexToBytes)(id); |
|
if (idb.byteLength !== ID_SIZE) |
|
throw Error("bad id size for added item"); |
|
this.items.push({ timestamp, id: idb }); |
|
} |
|
seal() { |
|
if (this.sealed) |
|
throw Error("already sealed"); |
|
this.sealed = true; |
|
this.items.sort(itemCompare); |
|
for (let i2 = 1; i2 < this.items.length; i2++) { |
|
if (itemCompare(this.items[i2 - 1], this.items[i2]) === 0) |
|
throw Error("duplicate item inserted"); |
|
} |
|
} |
|
unseal() { |
|
this.sealed = false; |
|
} |
|
size() { |
|
this._checkSealed(); |
|
return this.items.length; |
|
} |
|
getItem(i2) { |
|
this._checkSealed(); |
|
if (i2 >= this.items.length) |
|
throw Error("out of range"); |
|
return this.items[i2]; |
|
} |
|
iterate(begin, end, cb) { |
|
this._checkSealed(); |
|
this._checkBounds(begin, end); |
|
for (let i2 = begin; i2 < end; ++i2) { |
|
if (!cb(this.items[i2], i2)) |
|
break; |
|
} |
|
} |
|
findLowerBound(begin, end, bound) { |
|
this._checkSealed(); |
|
this._checkBounds(begin, end); |
|
return this._binarySearch(this.items, begin, end, (a) => itemCompare(a, bound) < 0); |
|
} |
|
fingerprint(begin, end) { |
|
let out = new Accumulator(); |
|
out.setToZero(); |
|
this.iterate(begin, end, (item) => { |
|
out.add(item.id); |
|
return true; |
|
}); |
|
return out.getFingerprint(end - begin); |
|
} |
|
_checkSealed() { |
|
if (!this.sealed) |
|
throw Error("not sealed"); |
|
} |
|
_checkBounds(begin, end) { |
|
if (begin > end || end > this.items.length) |
|
throw Error("bad range"); |
|
} |
|
_binarySearch(arr, first, last, cmp) { |
|
let count = last - first; |
|
while (count > 0) { |
|
let it = first; |
|
let step = Math.floor(count / 2); |
|
it += step; |
|
if (cmp(arr[it])) { |
|
first = ++it; |
|
count -= step + 1; |
|
} else { |
|
count = step; |
|
} |
|
} |
|
return first; |
|
} |
|
}; |
|
var Negentropy = class { |
|
storage; |
|
frameSizeLimit; |
|
lastTimestampIn; |
|
lastTimestampOut; |
|
constructor(storage, frameSizeLimit = 6e4) { |
|
if (frameSizeLimit < 4096) |
|
throw Error("frameSizeLimit too small"); |
|
this.storage = storage; |
|
this.frameSizeLimit = frameSizeLimit; |
|
this.lastTimestampIn = 0; |
|
this.lastTimestampOut = 0; |
|
} |
|
_bound(timestamp, id) { |
|
return { timestamp, id: id || new Uint8Array(0) }; |
|
} |
|
initiate() { |
|
let output = new WrappedBuffer(); |
|
output.extend(new Uint8Array([PROTOCOL_VERSION])); |
|
this.splitRange(0, this.storage.size(), this._bound(Number.MAX_VALUE), output); |
|
return (0, import_utils16.bytesToHex)(output.unwrap()); |
|
} |
|
reconcile(queryMsg, onhave, onneed) { |
|
const query = new WrappedBuffer((0, import_utils16.hexToBytes)(queryMsg)); |
|
this.lastTimestampIn = this.lastTimestampOut = 0; |
|
let fullOutput = new WrappedBuffer(); |
|
fullOutput.extend(new Uint8Array([PROTOCOL_VERSION])); |
|
let protocolVersion = getByte(query); |
|
if (protocolVersion < 96 || protocolVersion > 111) |
|
throw Error("invalid negentropy protocol version byte"); |
|
if (protocolVersion !== PROTOCOL_VERSION) { |
|
throw Error("unsupported negentropy protocol version requested: " + (protocolVersion - 96)); |
|
} |
|
let storageSize = this.storage.size(); |
|
let prevBound = this._bound(0); |
|
let prevIndex = 0; |
|
let skip = false; |
|
while (query.length !== 0) { |
|
let o = new WrappedBuffer(); |
|
let doSkip = () => { |
|
if (skip) { |
|
skip = false; |
|
o.extend(this.encodeBound(prevBound)); |
|
o.extend(encodeVarInt(Mode.Skip)); |
|
} |
|
}; |
|
let currBound = this.decodeBound(query); |
|
let mode = decodeVarInt(query); |
|
let lower = prevIndex; |
|
let upper = this.storage.findLowerBound(prevIndex, storageSize, currBound); |
|
if (mode === Mode.Skip) { |
|
skip = true; |
|
} else if (mode === Mode.Fingerprint) { |
|
let theirFingerprint = getBytes(query, FINGERPRINT_SIZE); |
|
let ourFingerprint = this.storage.fingerprint(lower, upper); |
|
if (compareUint8Array(theirFingerprint, ourFingerprint) !== 0) { |
|
doSkip(); |
|
this.splitRange(lower, upper, currBound, o); |
|
} else { |
|
skip = true; |
|
} |
|
} else if (mode === Mode.IdList) { |
|
let numIds = decodeVarInt(query); |
|
let theirElems = {}; |
|
for (let i2 = 0; i2 < numIds; i2++) { |
|
let e = getBytes(query, ID_SIZE); |
|
theirElems[(0, import_utils16.bytesToHex)(e)] = e; |
|
} |
|
skip = true; |
|
this.storage.iterate(lower, upper, (item) => { |
|
let k = item.id; |
|
const id = (0, import_utils16.bytesToHex)(k); |
|
if (!theirElems[id]) { |
|
onhave?.(id); |
|
} else { |
|
delete theirElems[(0, import_utils16.bytesToHex)(k)]; |
|
} |
|
return true; |
|
}); |
|
if (onneed) { |
|
for (let v of Object.values(theirElems)) { |
|
onneed((0, import_utils16.bytesToHex)(v)); |
|
} |
|
} |
|
} else { |
|
throw Error("unexpected mode"); |
|
} |
|
if (this.exceededFrameSizeLimit(fullOutput.length + o.length)) { |
|
let remainingFingerprint = this.storage.fingerprint(upper, storageSize); |
|
fullOutput.extend(this.encodeBound(this._bound(Number.MAX_VALUE))); |
|
fullOutput.extend(encodeVarInt(Mode.Fingerprint)); |
|
fullOutput.extend(remainingFingerprint); |
|
break; |
|
} else { |
|
fullOutput.extend(o); |
|
} |
|
prevIndex = upper; |
|
prevBound = currBound; |
|
} |
|
return fullOutput.length === 1 ? null : (0, import_utils16.bytesToHex)(fullOutput.unwrap()); |
|
} |
|
splitRange(lower, upper, upperBound, o) { |
|
let numElems = upper - lower; |
|
let buckets = 16; |
|
if (numElems < buckets * 2) { |
|
o.extend(this.encodeBound(upperBound)); |
|
o.extend(encodeVarInt(Mode.IdList)); |
|
o.extend(encodeVarInt(numElems)); |
|
this.storage.iterate(lower, upper, (item) => { |
|
o.extend(item.id); |
|
return true; |
|
}); |
|
} else { |
|
let itemsPerBucket = Math.floor(numElems / buckets); |
|
let bucketsWithExtra = numElems % buckets; |
|
let curr = lower; |
|
for (let i2 = 0; i2 < buckets; i2++) { |
|
let bucketSize = itemsPerBucket + (i2 < bucketsWithExtra ? 1 : 0); |
|
let ourFingerprint = this.storage.fingerprint(curr, curr + bucketSize); |
|
curr += bucketSize; |
|
let nextBound; |
|
if (curr === upper) { |
|
nextBound = upperBound; |
|
} else { |
|
let prevItem; |
|
let currItem; |
|
this.storage.iterate(curr - 1, curr + 1, (item, index) => { |
|
if (index === curr - 1) |
|
prevItem = item; |
|
else |
|
currItem = item; |
|
return true; |
|
}); |
|
nextBound = this.getMinimalBound(prevItem, currItem); |
|
} |
|
o.extend(this.encodeBound(nextBound)); |
|
o.extend(encodeVarInt(Mode.Fingerprint)); |
|
o.extend(ourFingerprint); |
|
} |
|
} |
|
} |
|
exceededFrameSizeLimit(n) { |
|
return n > this.frameSizeLimit - 200; |
|
} |
|
decodeTimestampIn(encoded) { |
|
let timestamp = decodeVarInt(encoded); |
|
timestamp = timestamp === 0 ? Number.MAX_VALUE : timestamp - 1; |
|
if (this.lastTimestampIn === Number.MAX_VALUE || timestamp === Number.MAX_VALUE) { |
|
this.lastTimestampIn = Number.MAX_VALUE; |
|
return Number.MAX_VALUE; |
|
} |
|
timestamp += this.lastTimestampIn; |
|
this.lastTimestampIn = timestamp; |
|
return timestamp; |
|
} |
|
decodeBound(encoded) { |
|
let timestamp = this.decodeTimestampIn(encoded); |
|
let len = decodeVarInt(encoded); |
|
if (len > ID_SIZE) |
|
throw Error("bound key too long"); |
|
let id = getBytes(encoded, len); |
|
return { timestamp, id }; |
|
} |
|
encodeTimestampOut(timestamp) { |
|
if (timestamp === Number.MAX_VALUE) { |
|
this.lastTimestampOut = Number.MAX_VALUE; |
|
return encodeVarInt(0); |
|
} |
|
let temp = timestamp; |
|
timestamp -= this.lastTimestampOut; |
|
this.lastTimestampOut = temp; |
|
return encodeVarInt(timestamp + 1); |
|
} |
|
encodeBound(key) { |
|
let output = new WrappedBuffer(); |
|
output.extend(this.encodeTimestampOut(key.timestamp)); |
|
output.extend(encodeVarInt(key.id.length)); |
|
output.extend(key.id); |
|
return output; |
|
} |
|
getMinimalBound(prev, curr) { |
|
if (curr.timestamp !== prev.timestamp) { |
|
return this._bound(curr.timestamp); |
|
} else { |
|
let sharedPrefixBytes = 0; |
|
let currKey = curr.id; |
|
let prevKey = prev.id; |
|
for (let i2 = 0; i2 < ID_SIZE; i2++) { |
|
if (currKey[i2] !== prevKey[i2]) |
|
break; |
|
sharedPrefixBytes++; |
|
} |
|
return this._bound(curr.timestamp, curr.id.subarray(0, sharedPrefixBytes + 1)); |
|
} |
|
} |
|
}; |
|
function compareUint8Array(a, b) { |
|
for (let i2 = 0; i2 < a.byteLength; i2++) { |
|
if (a[i2] < b[i2]) |
|
return -1; |
|
if (a[i2] > b[i2]) |
|
return 1; |
|
} |
|
if (a.byteLength > b.byteLength) |
|
return 1; |
|
if (a.byteLength < b.byteLength) |
|
return -1; |
|
return 0; |
|
} |
|
function itemCompare(a, b) { |
|
if (a.timestamp === b.timestamp) { |
|
return compareUint8Array(a.id, b.id); |
|
} |
|
return a.timestamp - b.timestamp; |
|
} |
|
var NegentropySync = class { |
|
relay; |
|
storage; |
|
neg; |
|
filter; |
|
subscription; |
|
onhave; |
|
onneed; |
|
constructor(relay, storage, filter, params = {}) { |
|
this.relay = relay; |
|
this.storage = storage; |
|
this.neg = new Negentropy(storage); |
|
this.onhave = params.onhave; |
|
this.onneed = params.onneed; |
|
this.filter = filter; |
|
this.subscription = this.relay.prepareSubscription([{}], { label: params.label || "negentropy" }); |
|
this.subscription.oncustom = (data) => { |
|
switch (data[0]) { |
|
case "NEG-MSG": { |
|
if (data.length < 3) { |
|
console.warn(`got invalid NEG-MSG from ${this.relay.url}: ${data}`); |
|
} |
|
try { |
|
const response = this.neg.reconcile(data[2], this.onhave, this.onneed); |
|
if (response) { |
|
this.relay.send(`["NEG-MSG", "${this.subscription.id}", "${response}"]`); |
|
} else { |
|
this.close(); |
|
params.onclose?.(); |
|
} |
|
} catch (error) { |
|
console.error("negentropy reconcile error:", error); |
|
params?.onclose?.(`reconcile error: ${error}`); |
|
} |
|
break; |
|
} |
|
case "NEG-CLOSE": { |
|
const reason = data[2]; |
|
console.warn("negentropy error:", reason); |
|
params.onclose?.(reason); |
|
break; |
|
} |
|
case "NEG-ERR": { |
|
params.onclose?.(); |
|
} |
|
} |
|
}; |
|
} |
|
async start() { |
|
const initMsg = this.neg.initiate(); |
|
this.relay.send(`["NEG-OPEN","${this.subscription.id}",${JSON.stringify(this.filter)},"${initMsg}"]`); |
|
} |
|
close() { |
|
this.relay.send(`["NEG-CLOSE","${this.subscription.id}"]`); |
|
this.subscription.close(); |
|
} |
|
}; |
|
|
|
// 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 |
|
}); |
|
var import_sha2565 = require("@noble/hashes/sha256"); |
|
var import_utils17 = require("@noble/hashes/utils"); |
|
var import_base5 = require("@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 + import_base5.base64.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(import_base5.base64.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 = (0, import_sha2565.sha256)(utf8Encoder.encode(JSON.stringify(payload))); |
|
return (0, import_utils17.bytesToHex)(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; |
|
}
|
|
|