UNPKG

nostr-tools

Version:
1,068 lines (1,063 loc) 34.9 kB
"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); // nipb0.ts var nipb0_exports = {}; __export(nipb0_exports, { BlossomClient: () => BlossomClient, areServersEqual: () => areServersEqual, blossomURIFromURL: () => blossomURIFromURL, blossomURIToURL: () => blossomURIToURL, buildBlossomURI: () => buildBlossomURI, computeBlobSha256: () => computeBlobSha256, createAuthEvent: () => createAuthEvent, createDeleteAuth: () => createDeleteAuth, createDownloadAuth: () => createDownloadAuth, createListAuth: () => createListAuth, createMirrorAuth: () => createMirrorAuth, createUploadAuth: () => createUploadAuth, deleteBlob: () => deleteBlob, downloadBlob: () => downloadBlob, encodeAuthorizationHeader: () => encodeAuthorizationHeader, getAuthExpiration: () => getAuthExpiration, getAuthTagValues: () => getAuthTagValues, getBlobSize: () => getBlobSize, getBlobType: () => getBlobType, getExtension: () => getExtension, getHashFromURL: () => getHashFromURL, getMIMEType: () => getMIMEType, getServersFromServerListEvent: () => getServersFromServerListEvent, hasBlob: () => hasBlob, isAuthExpired: () => isAuthExpired, isSha256: () => isSha256, iterateBlobs: () => iterateBlobs, listBlobs: () => listBlobs, mirrorBlob: () => mirrorBlob, normalizeServerTag: () => normalizeServerTag, now: () => now, oneHour: () => oneHour, parseBlossomURI: () => parseBlossomURI, reportBlobs: () => reportBlobs, uploadBlob: () => uploadBlob }); module.exports = __toCommonJS(nipb0_exports); var import_sha2 = require("@noble/hashes/sha2.js"); var import_utils = require("@noble/hashes/utils.js"); // 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 i = 0; i < event.tags.length; i++) { let tag = event.tags[i]; if (!Array.isArray(tag)) return false; for (let j = 0; j < tag.length; j++) { if (typeof tag[j] !== "string") return false; } } return true; } // kinds.ts var kinds_exports = {}; __export(kinds_exports, { AIEmbeddings: () => AIEmbeddings, AppCurationSet: () => AppCurationSet, Application: () => Application, AuthoredPodcasts: () => AuthoredPodcasts, BadgeAward: () => BadgeAward, BadgeDefinition: () => BadgeDefinition, Bid: () => Bid, BidConfirmation: () => BidConfirmation, BlobsAuth: () => BlobsAuth, BlockedRelaysList: () => BlockedRelaysList, BlossomServerList: () => BlossomServerList, BookmarkList: () => BookmarkList, Bookmarksets: () => Bookmarksets, Calendar: () => Calendar, CalendarEventRSVP: () => CalendarEventRSVP, CashuMintAnnouncement: () => CashuMintAnnouncement, CashuWalletEvent: () => CashuWalletEvent, CashuWalletHistory: () => CashuWalletHistory, CashuWalletTokens: () => CashuWalletTokens, ChannelCreation: () => ChannelCreation, ChannelHideMessage: () => ChannelHideMessage, ChannelMessage: () => ChannelMessage, ChannelMetadata: () => ChannelMetadata, ChannelMuteUser: () => ChannelMuteUser, ChatMessage: () => ChatMessage, Chess: () => Chess, ClassifiedListing: () => ClassifiedListing, ClientAuth: () => ClientAuth, CodeSnippet: () => CodeSnippet, CoinjoinPool: () => CoinjoinPool, Comment: () => Comment, CommunitiesList: () => CommunitiesList, CommunityDefinition: () => CommunityDefinition, CommunityPostApproval: () => CommunityPostApproval, ConferenceEvent: () => ConferenceEvent, Contacts: () => Contacts, CreateOrUpdateProduct: () => CreateOrUpdateProduct, CreateOrUpdateStall: () => CreateOrUpdateStall, CuratedVideoSets: () => CuratedVideoSets, Curationsets: () => Curationsets, Date: () => Date2, DecoupledEncryptionKeyDistribution: () => DecoupledEncryptionKeyDistribution, DecoupledKeyAnnouncement: () => DecoupledKeyAnnouncement, DecoupledKeyClientAnnouncement: () => DecoupledKeyClientAnnouncement, DirectMessageRelaysList: () => DirectMessageRelaysList, DraftClassifiedListing: () => DraftClassifiedListing, DraftEvent: () => DraftEvent, DraftLong: () => DraftLong, Emojisets: () => Emojisets, EncryptedDirectMessage: () => EncryptedDirectMessage, EventDeletion: () => EventDeletion, FavoritePodcasts: () => FavoritePodcasts, FavoriteRelays: () => FavoriteRelays, FedimintAnnouncement: () => FedimintAnnouncement, Feed: () => Feed, FileMessage: () => FileMessage, FileMetadata: () => FileMetadata, FileServerPreference: () => FileServerPreference, Followsets: () => Followsets, ForumThread: () => ForumThread, GenericRepost: () => GenericRepost, Genericlists: () => Genericlists, GeocacheListing: () => GeocacheListing, GeocacheLog: () => GeocacheLog, GeocacheLogEntry: () => GeocacheLogEntry, GeocacheProofOfFind: () => GeocacheProofOfFind, GiftWrap: () => GiftWrap, GitPullRequest: () => GitPullRequest, GitPullRequestUpdate: () => GitPullRequestUpdate, GoodWikiAuthorList: () => GoodWikiAuthorList, GoodWikiRelayList: () => GoodWikiRelayList, GroupMetadata: () => GroupMetadata, HTTPAuth: () => HTTPAuth, Handlerinformation: () => Handlerinformation, Handlerrecommendation: () => Handlerrecommendation, Highlights: () => Highlights, InteractiveRoom: () => InteractiveRoom, InterestsList: () => InterestsList, Interestsets: () => Interestsets, Issue: () => Issue, JobFeedback: () => JobFeedback, JobRequest: () => JobRequest, JobResult: () => JobResult, Label: () => Label, LegacyNsiteFile: () => LegacyNsiteFile, LightningPubRPC: () => LightningPubRPC, LinkSet: () => LinkSet, LiveChatMessage: () => LiveChatMessage, LiveEvent: () => LiveEvent, LongFormArticle: () => LongFormArticle, MarketplaceUI: () => MarketplaceUI, MediaFollows: () => MediaFollows, MediaStarterPacks: () => MediaStarterPacks, MergeRequests: () => MergeRequests, Metadata: () => Metadata, ModularArticleContent: () => ModularArticleContent, ModularArticleHeader: () => ModularArticleHeader, MuteSets: () => MuteSets, Mutelist: () => Mutelist, NWCWalletInfo: () => NWCWalletInfo, NWCWalletRequest: () => NWCWalletRequest, NWCWalletResponse: () => NWCWalletResponse, NormalVideo: () => NormalVideo, NostrConnect: () => NostrConnect, NsiteNamed: () => NsiteNamed, NsiteRoot: () => NsiteRoot, NutZap: () => NutZap, NutZapInfo: () => NutZapInfo, OpenTimestamps: () => OpenTimestamps, Patch: () => Patch, PeerToPeerOrderEvents: () => PeerToPeerOrderEvents, Photo: () => Photo, Pinlist: () => Pinlist, PodcastEpisode: () => PodcastEpisode, PodcastMetadata: () => PodcastMetadata, Poll: () => Poll, PollResponse: () => PollResponse, PrivateDirectMessage: () => PrivateDirectMessage, PrivateEventRelayList: () => PrivateEventRelayList, ProblemTracker: () => ProblemTracker, ProductSoldAsAuction: () => ProductSoldAsAuction, ProfileBadges: () => ProfileBadges, ProxyAnnouncement: () => ProxyAnnouncement, PublicChatsList: () => PublicChatsList, PublicMessage: () => PublicMessage, Reaction: () => Reaction, ReactionToWebsite: () => ReactionToWebsite, RecommendRelay: () => RecommendRelay, Redirects: () => Redirects, RelayDiscovery: () => RelayDiscovery, RelayList: () => RelayList, RelayMonitorAnnouncement: () => RelayMonitorAnnouncement, RelayReview: () => RelayReview, RelayReviews: () => RelayReviews, Relaysets: () => Relaysets, ReleaseArtifactSets: () => ReleaseArtifactSets, Reply: () => Reply, Report: () => Report, Reporting: () => Reporting, RepositoryAnnouncement: () => RepositoryAnnouncement, RepositoryState: () => RepositoryState, Repost: () => Repost, ReservedCashuWalletTokens: () => ReservedCashuWalletTokens, RoomPresence: () => RoomPresence, Scroll: () => Scroll, Seal: () => Seal, SearchRelaysList: () => SearchRelaysList, ShortTextNote: () => ShortTextNote, ShortVideo: () => ShortVideo, SimpleGroupAdmins: () => SimpleGroupAdmins, SimpleGroupCreateGroup: () => SimpleGroupCreateGroup, SimpleGroupCreateInvite: () => SimpleGroupCreateInvite, SimpleGroupDeleteEvent: () => SimpleGroupDeleteEvent, SimpleGroupDeleteGroup: () => SimpleGroupDeleteGroup, SimpleGroupEditMetadata: () => SimpleGroupEditMetadata, SimpleGroupJoinRequest: () => SimpleGroupJoinRequest, SimpleGroupLeaveRequest: () => SimpleGroupLeaveRequest, SimpleGroupList: () => SimpleGroupList, SimpleGroupLiveKitParticipants: () => SimpleGroupLiveKitParticipants, SimpleGroupMembers: () => SimpleGroupMembers, SimpleGroupPutUser: () => SimpleGroupPutUser, SimpleGroupRemoveUser: () => SimpleGroupRemoveUser, SimpleGroupReply: () => SimpleGroupReply, SimpleGroupRoles: () => SimpleGroupRoles, SimpleGroupThreadedReply: () => SimpleGroupThreadedReply, SlideSet: () => SlideSet, SoftwareApplication: () => SoftwareApplication, StarterPacks: () => StarterPacks, StatusApplied: () => StatusApplied, StatusClosed: () => StatusClosed, StatusDraft: () => StatusDraft, StatusOpen: () => StatusOpen, TidalLogin: () => TidalLogin, Time: () => Time, Torrent: () => Torrent, TorrentComment: () => TorrentComment, TransportMethodAnnouncement: () => TransportMethodAnnouncement, UserEmojiList: () => UserEmojiList, UserGraspList: () => UserGraspList, UserStatuses: () => UserStatuses, VideoViewEvent: () => VideoViewEvent, Voice: () => Voice, VoiceComment: () => VoiceComment, WebBookmarks: () => WebBookmarks, WikiArticle: () => WikiArticle, 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 SimpleGroupThreadedReply = 10; var ForumThread = 11; var SimpleGroupReply = 12; var Seal = 13; var PrivateDirectMessage = 14; var FileMessage = 15; var GenericRepost = 16; var ReactionToWebsite = 17; var Photo = 20; var NormalVideo = 21; var ShortVideo = 22; var PublicMessage = 24; var ChannelCreation = 40; var ChannelMetadata = 41; var ChannelMessage = 42; var ChannelHideMessage = 43; var ChannelMuteUser = 44; var PodcastEpisode = 54; var Chess = 64; var MergeRequests = 818; var PollResponse = 1018; var Bid = 1021; var BidConfirmation = 1022; var OpenTimestamps = 1040; var GiftWrap = 1059; var FileMetadata = 1063; var Poll = 1068; var Comment = 1111; var Voice = 1222; var Scroll = 1227; var VoiceComment = 1244; var LiveChatMessage = 1311; var CodeSnippet = 1337; var Patch = 1617; var GitPullRequest = 1618; var GitPullRequestUpdate = 1619; var Issue = 1621; var Reply = 1622; var StatusOpen = 1630; var StatusApplied = 1631; var StatusClosed = 1632; var StatusDraft = 1633; var ProblemTracker = 1971; var Report = 1984; var Reporting = 1984; var Label = 1985; var RelayReviews = 1986; var AIEmbeddings = 1987; var Torrent = 2003; var TorrentComment = 2004; var CoinjoinPool = 2022; var DecoupledKeyClientAnnouncement = 4454; var DecoupledEncryptionKeyDistribution = 4455; var CommunityPostApproval = 4550; var JobRequest = 5999; var JobResult = 6999; var JobFeedback = 7e3; var ReservedCashuWalletTokens = 7374; var CashuWalletTokens = 7375; var CashuWalletHistory = 7376; var GeocacheLog = 7516; var GeocacheProofOfFind = 7517; var SimpleGroupPutUser = 9e3; var SimpleGroupRemoveUser = 9001; var SimpleGroupEditMetadata = 9002; var SimpleGroupDeleteEvent = 9005; var SimpleGroupCreateGroup = 9007; var SimpleGroupDeleteGroup = 9008; var SimpleGroupCreateInvite = 9009; var SimpleGroupJoinRequest = 9021; var SimpleGroupLeaveRequest = 9022; var ZapGoal = 9041; var NutZap = 9321; var TidalLogin = 9467; var ZapRequest = 9734; var Zap = 9735; var Highlights = 9802; var Mutelist = 1e4; var Pinlist = 10001; var RelayList = 10002; var BookmarkList = 10003; var CommunitiesList = 10004; var PublicChatsList = 10005; var BlockedRelaysList = 10006; var SearchRelaysList = 10007; var SimpleGroupList = 10009; var FavoriteRelays = 10012; var PrivateEventRelayList = 10013; var InterestsList = 10015; var NutZapInfo = 10019; var MediaFollows = 10020; var UserEmojiList = 10030; var DecoupledKeyAnnouncement = 10044; var DirectMessageRelaysList = 10050; var FavoritePodcasts = 10054; var BlossomServerList = 10063; var FileServerPreference = 10096; var GoodWikiAuthorList = 10101; var GoodWikiRelayList = 10102; var PodcastMetadata = 10154; var AuthoredPodcasts = 10164; var RelayMonitorAnnouncement = 10166; var RoomPresence = 10312; var UserGraspList = 10317; var ProxyAnnouncement = 10377; var TransportMethodAnnouncement = 11111; var NWCWalletInfo = 13194; var NsiteRoot = 15128; var CashuWalletEvent = 17375; var LightningPubRPC = 21e3; var ClientAuth = 22242; var NWCWalletRequest = 23194; var NWCWalletResponse = 23195; var NostrConnect = 24133; var BlobsAuth = 24242; var HTTPAuth = 27235; var Followsets = 3e4; var Genericlists = 30001; var Relaysets = 30002; var Bookmarksets = 30003; var Curationsets = 30004; var CuratedVideoSets = 30005; var MuteSets = 30007; var ProfileBadges = 30008; var BadgeDefinition = 30009; var Interestsets = 30015; var CreateOrUpdateStall = 30017; var CreateOrUpdateProduct = 30018; var MarketplaceUI = 30019; var ProductSoldAsAuction = 30020; var LongFormArticle = 30023; var DraftLong = 30024; var Emojisets = 30030; var ModularArticleHeader = 30040; var ModularArticleContent = 30041; var ReleaseArtifactSets = 30063; var Application = 30078; var RelayDiscovery = 30166; var AppCurationSet = 30267; var LiveEvent = 30311; var InteractiveRoom = 30312; var ConferenceEvent = 30313; var UserStatuses = 30315; var SlideSet = 30388; var ClassifiedListing = 30402; var DraftClassifiedListing = 30403; var RepositoryAnnouncement = 30617; var RepositoryState = 30618; var WikiArticle = 30818; var Redirects = 30819; var DraftEvent = 31234; var LinkSet = 31388; var Feed = 31890; var Date2 = 31922; var Time = 31923; var Calendar = 31924; var CalendarEventRSVP = 31925; var RelayReview = 31987; var Handlerrecommendation = 31989; var Handlerinformation = 31990; var SoftwareApplication = 32267; var LegacyNsiteFile = 34128; var VideoViewEvent = 34237; var CommunityDefinition = 34550; var NsiteNamed = 35128; var GeocacheListing = 37515; var GeocacheLogEntry = 37516; var CashuMintAnnouncement = 38172; var FedimintAnnouncement = 38173; var PeerToPeerOrderEvents = 38383; var GroupMetadata = 39e3; var SimpleGroupAdmins = 39001; var SimpleGroupMembers = 39002; var SimpleGroupRoles = 39003; var SimpleGroupLiveKitParticipants = 39004; var StarterPacks = 39089; var MediaStarterPacks = 39092; var WebBookmarks = 39701; // nipb0.ts function isSha256(str) { return /^[0-9a-f]{64}$/i.test(str); } function getBlobSize(blob) { if (typeof File !== "undefined" && blob instanceof File || blob instanceof Blob) { return blob.size; } return blob.length; } function getBlobType(blob) { if (typeof File !== "undefined" && blob instanceof File || blob instanceof Blob) { return blob.type || void 0; } return void 0; } async function computeBlobSha256(blob) { let buffer; if (typeof File !== "undefined" && blob instanceof File || blob instanceof Blob) { buffer = await blob.arrayBuffer(); } else { buffer = blob; } const hash = import_sha2.sha256.create().update(new Uint8Array(buffer)).digest(); return (0, import_utils.bytesToHex)(hash); } function encodeAuthorizationHeader(event) { const json = JSON.stringify(event); const bytes = new TextEncoder().encode(json); let binary = ""; for (const byte of bytes) binary += String.fromCharCode(byte); return "Nostr " + btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, ""); } function now() { return Math.floor(Date.now() / 1e3); } function oneHour() { return now() + 3600; } function getAuthTagValues(auth, tagName) { return auth.tags.filter((tag) => tag[0] === tagName).map((tag) => tag[1]); } function getAuthExpiration(auth) { const expiration = auth.tags.find((tag) => tag[0] === "expiration")?.[1]; if (!expiration) return void 0; const timestamp = Number(expiration); if (!Number.isFinite(timestamp)) return void 0; return timestamp; } function isAuthExpired(auth, timestamp = now()) { const expiration = getAuthExpiration(auth); return expiration !== void 0 && expiration <= timestamp; } function normalizeServerTag(server) { if (server instanceof URL) return server.hostname.toLowerCase(); if (URL.canParse(server)) return new URL(server).hostname.toLowerCase(); return server.toLowerCase(); } function areServersEqual(a, b) { return normalizeServerTag(a) === normalizeServerTag(b); } function normalizeServers(servers) { const values = Array.isArray(servers) ? servers : [servers]; return [...new Set(values.map(normalizeServerTag))]; } async function createAuthEvent(signer, type, options) { const draft = { created_at: now(), kind: kinds_exports.BlobsAuth, content: options?.message ?? "", tags: [ ["t", type], ["expiration", String(options?.expiration ?? oneHour())] ] }; if (options?.blobs) { const blobList = Array.isArray(options.blobs) ? options.blobs : [options.blobs]; const seen = /* @__PURE__ */ new Set(); for (const blob of blobList) { const hash = typeof blob === "string" ? blob : await computeBlobSha256(blob); if (!seen.has(hash)) { draft.tags.push(["x", hash]); seen.add(hash); } } } if (options?.servers) { for (const server of normalizeServers(options.servers)) { draft.tags.push(["server", server]); } } return signer(draft); } async function createUploadAuth(signer, blobs, options) { return createAuthEvent(signer, options?.type ?? "upload", { message: "Upload Blob", ...options, blobs }); } async function createDownloadAuth(signer, hash, options) { return createAuthEvent(signer, "get", { message: "Download Blob", ...options, blobs: [hash] }); } async function createMirrorAuth(signer, hash, options) { return createAuthEvent(signer, "upload", { message: "Mirror Blob", ...options, blobs: [hash] }); } async function createListAuth(signer, options) { return createAuthEvent(signer, "list", { message: "List Blobs", ...options }); } async function createDeleteAuth(signer, hash, options) { return createAuthEvent(signer, "delete", { message: "Delete Blob", ...options, blobs: [hash] }); } function parseBlossomURI(uri) { if (!uri.startsWith("blossom:")) throw new Error("Invalid blossom URI: missing blossom: scheme"); const body = uri.slice("blossom:".length); const queryIndex = body.indexOf("?"); const path = queryIndex === -1 ? body : body.slice(0, queryIndex); const query = queryIndex === -1 ? "" : body.slice(queryIndex + 1); const dotIndex = path.indexOf("."); if (dotIndex === -1) throw new Error("Invalid blossom URI: missing file extension"); const sha2562 = path.slice(0, dotIndex); const ext = path.slice(dotIndex + 1); if (!isSha256(sha2562)) throw new Error("Invalid blossom URI: invalid sha256 hash"); if (!ext) throw new Error("Invalid blossom URI: empty file extension"); const params = new URLSearchParams(query); const servers = params.getAll("xs"); const authors = params.getAll("as"); const szValue = params.get("sz"); let size; if (szValue !== null) { size = Number(szValue); if (!Number.isFinite(size) || size <= 0 || Math.floor(size) !== size) { throw new Error("Invalid blossom URI: sz must be a positive integer"); } } return { sha256: sha2562, ext, servers, authors, size }; } function buildBlossomURI(options) { const params = new URLSearchParams(); for (const server of options.servers) params.append("xs", server); for (const author of options.authors) params.append("as", author); if (options.size !== void 0) params.append("sz", String(options.size)); const query = params.toString(); return `blossom:${options.sha256}.${options.ext}${query ? "?" + query : ""}`; } function blossomURIToURL(uri) { const str = typeof uri === "string" ? uri : buildBlossomURI(uri); return new URL(str); } function blossomURIFromURL(url) { if (url.protocol !== "blossom:") throw new Error("Invalid blossom URL: expected blossom: protocol"); const path = url.pathname; const dotIndex = path.indexOf("."); if (dotIndex === -1) throw new Error("Invalid blossom URL: missing file extension"); const sha2562 = path.slice(0, dotIndex); const ext = path.slice(dotIndex + 1); if (!isSha256(sha2562)) throw new Error("Invalid blossom URL: invalid sha256 hash"); if (!ext) throw new Error("Invalid blossom URL: empty file extension"); const servers = url.searchParams.getAll("xs"); const authors = url.searchParams.getAll("as"); const szValue = url.searchParams.get("sz"); let size; if (szValue !== null) { size = Number(szValue); if (!Number.isFinite(size) || size <= 0 || Math.floor(size) !== size) { throw new Error("Invalid blossom URL: sz must be a positive integer"); } } return { sha256: sha2562, ext, servers, authors, size }; } var commonMimeExtensions = { "application/json": ".json", "application/pdf": ".pdf", "application/vnd.android.package-archive": ".apk", "application/vnd.sqlite3": ".sqlite3", "application/xml": ".xml", "audio/aac": ".aac", "audio/flac": ".flac", "audio/midi": ".midi", "audio/mp3": ".mp3", "audio/mpeg": ".mp3", "audio/mp4": ".m4a", "audio/ogg": ".ogg", "audio/wav": ".wav", "audio/webm": ".weba", "audio/x-aiff": ".aiff", "audio/x-m4a": ".m4a", "image/avif": ".avif", "image/gif": ".gif", "image/jpeg": ".jpg", "image/png": ".png", "image/svg+xml": ".svg", "image/webp": ".webp", "text/css": ".css", "text/csv": ".csv", "text/html": ".html", "text/javascript": ".js", "text/markdown": ".md", "text/plain": ".txt", "text/xml": ".xml", "video/mp2t": ".ts", "video/mp4": ".mp4", "video/ogg": ".ogv", "video/quicktime": ".mov", "video/webm": ".webm", "video/x-matroska": ".mkv" }; var commonExtensionMimes = { ".aac": "audio/aac", ".aiff": "audio/x-aiff", ".apk": "application/vnd.android.package-archive", ".avif": "image/avif", ".css": "text/css; charset=utf-8", ".csv": "text/csv; charset=utf-8", ".flac": "audio/flac", ".gif": "image/gif", ".html": "text/html; charset=utf-8", ".jpeg": "image/jpeg", ".jpg": "image/jpeg", ".js": "text/javascript; charset=utf-8", ".json": "application/json", ".m4a": "audio/mp4", ".md": "text/markdown; charset=utf-8", ".midi": "audio/midi", ".mkv": "video/x-matroska", ".mov": "video/quicktime", ".mp3": "audio/mpeg", ".mp4": "video/mp4", ".oga": "audio/ogg", ".ogg": "audio/ogg", ".ogv": "video/ogg", ".pdf": "application/pdf", ".png": "image/png", ".sqlite3": "application/vnd.sqlite3", ".svg": "image/svg+xml", ".ts": "video/mp2t", ".txt": "text/plain; charset=utf-8", ".wav": "audio/wav", ".weba": "audio/webm", ".webm": "video/webm", ".webp": "image/webp", ".xml": "application/xml" }; function normalizeMIMEType(mimetype) { const idx = mimetype.indexOf(";"); return idx >= 0 ? mimetype.slice(0, idx).trim().toLowerCase() : mimetype.trim().toLowerCase(); } function getExtension(mimetype) { const normalized = normalizeMIMEType(mimetype); if (!normalized) return ""; return commonMimeExtensions[normalized] ?? ""; } function getMIMEType(ext) { if (!ext) return ""; ext = ext.trim().toLowerCase(); if (ext[0] !== ".") ext = "." + ext; return commonExtensionMimes[ext] ?? ""; } function getServersFromServerListEvent(event) { const servers = []; for (const tag of event.tags) { if (tag[0] === "server" && tag[1]) { try { const url = new URL(tag[1]); url.pathname = "/"; servers.push(url); } catch { } } } return servers; } function getHashFromURL(url) { try { if (typeof url === "string") url = new URL(url); const hashes = Array.from(url.pathname.matchAll(/[0-9a-f]{64}/gi)); return hashes.length > 0 ? hashes[hashes.length - 1][0] : null; } catch (_err) { return null; } } async function uploadBlob(server, blob, opts) { const url = new URL("/upload", server).toString(); const sha2562 = await computeBlobSha256(blob); const headers = { "X-SHA-256": sha2562, "Content-Type": getBlobType(blob) || "application/octet-stream" }; if (opts?.auth) { const authEvent = typeof opts.auth === "boolean" ? await opts.onAuth?.(server, sha2562) : opts.auth; if (authEvent) headers["Authorization"] = encodeAuthorizationHeader(authEvent); } const res = await fetch(url, { method: "PUT", body: blob, headers, signal: opts?.signal }); if (res.status >= 300) { const reason = res.headers.get("X-Reason") || res.statusText; throw new Error(`upload returned error (${res.status}): ${reason}`); } return res.json(); } async function downloadBlob(server, hash, opts) { const url = new URL("/" + hash, server).toString(); const headers = {}; if (opts?.auth) { const authEvent = typeof opts.auth === "boolean" ? await opts.onAuth?.(server, hash) : opts.auth; if (authEvent) headers["Authorization"] = encodeAuthorizationHeader(authEvent); } const res = await fetch(url, { headers, signal: opts?.signal }); if (res.status >= 300) { const reason = res.headers.get("X-Reason") || res.statusText; throw new Error(`${hash} download error (${res.status}): ${reason}`); } return res; } async function listBlobs(server, pubkey, opts) { const url = new URL("/list/" + pubkey, server); if (opts?.cursor) url.searchParams.append("cursor", opts.cursor); if (opts?.limit) url.searchParams.append("limit", String(opts.limit)); if (opts?.since) url.searchParams.append("since", String(opts.since)); if (opts?.until) url.searchParams.append("until", String(opts.until)); const headers = {}; if (opts?.auth) { const authEvent = typeof opts.auth === "boolean" ? await opts.onAuth?.(server) : opts.auth; if (authEvent) headers["Authorization"] = encodeAuthorizationHeader(authEvent); } const res = await fetch(url.toString(), { headers, signal: opts?.signal }); if (res.status >= 300) { const reason = res.headers.get("X-Reason") || res.statusText; throw new Error(`list error (${res.status}): ${reason}`); } return res.json(); } async function* iterateBlobs(server, pubkey, opts) { let cursor = opts?.cursor; while (true) { const page = await listBlobs(server, pubkey, { ...opts, cursor }); if (page.length === 0) return; yield page; if (opts?.limit && page.length < opts.limit) return; cursor = page[page.length - 1]?.sha256; if (!cursor) return; } } async function deleteBlob(server, hash, opts) { const url = new URL("/" + hash, server).toString(); const headers = {}; if (opts?.auth) { const authEvent = typeof opts.auth === "boolean" ? await opts.onAuth?.(server, hash) : opts.auth; if (authEvent) headers["Authorization"] = encodeAuthorizationHeader(authEvent); } const res = await fetch(url, { method: "DELETE", headers, signal: opts?.signal }); if (res.status >= 300) { const reason = res.headers.get("X-Reason") || res.statusText; throw new Error(`delete error (${res.status}): ${reason}`); } return res.ok; } async function hasBlob(server, hash, opts) { const url = new URL("/" + hash, server); try { const res = await fetch(url.toString(), { method: "HEAD", signal: opts?.signal }); return res.status !== 404; } catch { return false; } } async function mirrorBlob(server, blob, opts) { const url = new URL("/mirror", server).toString(); const headers = { "Content-Type": "application/json", "X-SHA-256": blob.sha256 }; if (opts?.auth) { const authEvent = typeof opts.auth === "boolean" ? await opts.onAuth?.(server, blob.sha256) : opts.auth; if (authEvent) headers["Authorization"] = encodeAuthorizationHeader(authEvent); } const res = await fetch(url, { method: "PUT", body: JSON.stringify({ url: blob.url }), headers, signal: opts?.signal }); if (res.status >= 300) { const reason = res.headers.get("X-Reason") || res.statusText; throw new Error(`mirror error (${res.status}): ${reason}`); } return res.json(); } async function reportBlobs(servers, report, opts) { if (report.kind !== 1984 || !report.tags.some((tag) => tag[0] === "x" && !!tag[1])) { throw new Error("Invalid blob report event: must be kind 1984 with x tag"); } const results = /* @__PURE__ */ new Map(); const body = JSON.stringify(report); for (const server of servers) { try { const res = await fetch(new URL("/report", server).toString(), { method: "PUT", headers: { "Content-Type": "application/json" }, body, signal: opts?.signal }); if (res.status >= 300) { const reason = res.headers.get("X-Reason") || res.statusText; throw new Error(`report error (${res.status}): ${reason}`); } results.set(server, true); } catch (error) { if (opts?.onError && error instanceof Error) opts.onError(server, error); } } return results; } var BlossomClient = class { constructor(mediaserver, signer) { this.signer = signer; if (!mediaserver.startsWith("http")) { mediaserver = "https://" + mediaserver; } this.mediaserver = mediaserver.replace(/\/$/, "") + "/"; } mediaserver; getMediaServer() { return this.mediaserver; } async authorizationHeader(modify) { const event = { created_at: now(), kind: kinds_exports.BlobsAuth, content: "blossom stuff", tags: [["expiration", String(now() + 60)]] }; modify?.(event); try { const signedEvent = await this.signer.signEvent(event); const json = JSON.stringify(signedEvent); return "Nostr " + btoa(json); } catch { return ""; } } async httpCall(method, url, contentType, addAuthorization, body) { const headers = {}; if (contentType) headers["Content-Type"] = contentType; if (addAuthorization) { const auth = await addAuthorization(); if (auth) headers["Authorization"] = auth; } const res = await fetch(this.mediaserver + url, { method, headers, body }); if (res.status >= 300) { const reason = res.headers.get("X-Reason") || res.statusText; throw new Error(`${url} returned error (${res.status}): ${reason}`); } if (res.headers.get("content-type")?.includes("application/json")) { return res.json(); } return res; } async uploadBlob(file, contentType) { const hash = (0, import_utils.bytesToHex)((0, import_sha2.sha256)(new Uint8Array(await file.arrayBuffer()))); const actualContentType = contentType || getBlobType(file) || "application/octet-stream"; return this.httpCall( "PUT", "upload", actualContentType, () => this.authorizationHeader((evt) => { evt.tags.push(["t", "upload"], ["x", hash]); }), file ); } async download(hash) { const authHeader = await this.authorizationHeader((evt) => { evt.tags.push(["t", "get"], ["x", hash]); }); const res = await fetch(this.mediaserver + hash, { method: "GET", headers: { Authorization: authHeader } }); if (res.status >= 300) { throw new Error(`${hash} not present on ${this.mediaserver}: ${res.status}`); } return res.arrayBuffer(); } async downloadAsBlob(hash) { return new Blob([await this.download(hash)]); } async list() { const pubkey = await this.signer.getPublicKey(); return this.httpCall( "GET", `list/${pubkey}`, void 0, () => this.authorizationHeader((evt) => { evt.tags.push(["t", "list"]); }) ); } async delete(hash) { await this.httpCall( "DELETE", hash, void 0, () => this.authorizationHeader((evt) => { evt.tags.push(["t", "delete"], ["x", hash]); }) ); } async check(hash) { if (!isSha256(hash)) throw new Error(`${hash} is not valid 32-byte hex`); await this.httpCall("HEAD", hash); } async mirror(remoteBlobURL) { const hash = remoteBlobURL.split("/").pop()?.split(".")[0] || ""; return this.httpCall( "PUT", "mirror", "application/json", () => this.authorizationHeader((evt) => { evt.tags.push(["t", "upload"], ["x", hash]); }), JSON.stringify({ url: remoteBlobURL }) ); } };