npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

blossom-client-sdk

v5.0.0

Published

Client SDK for talking to blossom servers

Readme

🌸 blossom-client-sdk

A client for managing blobs on blossom servers

Documentation

Basic Usage

import { uploadBlob, createUploadAuth, encodeAuthorizationHeader } from "blossom-client-sdk";

async function signer(event) {
  return await window.nostr.signEvent(event);
}

const server = "https://cdn.example.com";

// create an upload auth event
const uploadAuth = await createUploadAuth(signer, file);

// encode it using base64
const encodedAuthHeader = encodeAuthorizationHeader(uploadAuth);

// manually make the request
const res = await fetch(new URL("/upload", server), {
  method: "PUT",
  body: file,
  headers: { authorization: encodedAuthHeader },
});

// or use the action function
const blob = await uploadBlob(server, file, {
  onAuth: async (server, sha256, type) => createUploadAuth(signer, sha256, { type }),
});

Using with NDK

The auth and action functions optionally take a signer method that is used to sign the auth events

If your using NDK in your app you can use this method

const signer = async (draft: EventTemplate) => {
  // add the pubkey to the draft event
  const event: UnsignedEvent = { ...draft, pubkey: user.pubkey };
  // get the signature
  const sig = await ndk.signer!.sign(event);

  // return the event + id + sig
  return { ...event, sig, id: getEventHash(event) };
};

Helper Methods

Getting the hash from a URL

The getHashFromURL method will return the last SHA256 hash it finds in a URL

import { getHashFromURL } from "blossom-client-sdk";

// blossom compatible URLs
console.log(
  getHashFromURL("https://cdn.example.com/b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553.pdf"),
);
// -> b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553

// non-blossom URLs
console.log(
  getHashFromURL(
    "https://cdn.example.com/266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5/media/b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553.pdf",
  ),
);
// -> b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553

// returns null when no hash is found
console.log(getHashFromURL("https://example.com/index.html"));
// -> null

Media fallbacks

The SDK provides several methods for handling broken media elements (<img>, <video>, <audio>) by automatically trying alternative blossom servers. Both regular HTTP blob URLs and blossom: URIs are supported.

All the media fallback methods require a getServers callback to resolve pubkeys to server lists:

import { USER_BLOSSOM_SERVER_LIST_KIND, getServersFromServerListEvent } from "blossom-client-sdk";

async function getServers(pubkey) {
  if (pubkey) {
    const event = await ndk.fetchEvent({ kinds: [USER_BLOSSOM_SERVER_LIST_KIND], authors: [pubkey] });
    if (event) return getServersFromServerListEvent(event);
  }
  return undefined;
}

Automatic fallbacks for a DOM tree

handleBrokenMedia watches a DOM tree for any <img>, <video>, or <audio> elements and automatically handles server fallbacks. It uses a MutationObserver to handle dynamically added elements. Returns a cleanup function to remove all listeners and stop observing.

import { handleBrokenMedia } from "blossom-client-sdk";

// start watching for broken media in the document
const cleanup = handleBrokenMedia(document.body, getServers);

// later, to remove all listeners and stop watching
cleanup();

Blossom URIs in media elements

Media elements can use blossom: URIs directly in the src attribute. The fallback handler will automatically resolve the URI to HTTP URLs using the server hints:

<img
  src="blossom:b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553.png?xs=https://cdn1.com&xs=https://cdn2.com"
/>

When the browser can't load the blossom: protocol, the error handler parses the URI, resolves server URLs from xs and as hints, and sets the first working URL.

Single element fallbacks

handleMediaFallbacks attaches an error listener to a single element. For regular HTTP URLs, it extracts the blob hash and looks up alternative servers using a data-pubkey attribute on the element or its parents:

import { handleMediaFallbacks } from "blossom-client-sdk";

const video = document.createElement("video");
video.src = "https://cdn.example.com/b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553.mp4";
video.dataset.pubkey = event.pubkey;

const removeListener = handleMediaFallbacks(video, getServers);

Get blob URLs without fetching

getBlobUrls returns an ordered list of URLs for a blob from a blossom URI without fetching anything. Useful for building custom UI or populating <source> elements:

import { Actions } from "blossom-client-sdk";

const urls = await Actions.getBlobUrls("blossom:b167...4f553.mp4?xs=https://cdn1.com&xs=https://cdn2.com", {
  getServers,
  fallbackServers: ["https://fallback.cdn.com"],
});
// -> ["https://cdn1.com/b167...4f553.mp4", "https://cdn2.com/b167...4f553.mp4", "https://fallback.cdn.com/b167...4f553.mp4"]

Create <source> elements for video/audio

createSourceElements generates <source> elements from a URL list, giving the browser native fallback for <video> and <audio>:

import { Actions, createSourceElements } from "blossom-client-sdk";

const urls = await Actions.getBlobUrls("blossom:b167...4f553.mp4?xs=https://cdn1.com&xs=https://cdn2.com");
const sources = createSourceElements(urls, "video/mp4");

const video = document.createElement("video");
video.append(...sources);

Resolve to an object URL

resolveToObjectURL fetches a blob with server fallback and returns a blob: object URL that works anywhere:

import { Actions } from "blossom-client-sdk";

const objectUrl = await Actions.resolveToObjectURL("blossom:b167...4f553.png?xs=https://cdn1.com", { getServers });
image.src = objectUrl;

HLS Video Streaming with Multi-Server Fallback

The SDK provides a loader factory for hls.js that automatically retries failed playlist and fragment requests across multiple Blossom servers. This enables resilient HLS playback even when some servers have missing or unavailable segments.

import Hls from "hls.js";
import { createBlossomHlsLoaders } from "blossom-client-sdk/hls";

// Create fallback loaders for hls.js
const { pLoader, fLoader } = createBlossomHlsLoaders({
  // Servers to try when the primary fails
  fallbackServers: ["https://cdn-backup1.example", "https://cdn-backup2.example"],
  // Penalize failed origins briefly to prefer healthy ones (default: false)
  stickyFailover: true,
  // How long to penalize failed servers in ms (default: 30000)
  penalizeMs: 30000,
  // HTTP status codes to retry (default: [404])
  retryStatuses: [404, 502, 503],
  // Callback when a fallback occurs
  onFallback: (info) => {
    console.log(`Falling back from ${info.from} to ${info.to} for ${info.kind}`);
    console.log(`Attempt ${info.attempt} for ${info.url}`);
  },
});

// Initialize hls.js with the fallback loaders
const hls = new Hls({
  pLoader,
  fLoader,
});

// Load a master playlist from any Blossom server
hls.loadSource("https://cdn.example.com/abc123def456.m3u8");
hls.attachMedia(videoElement);

How it works:

  • The loaders keep the original playlist/fragment path and query string but swap the origin
  • If the primary server returns a retryable error (404, 5xx, timeout), it automatically tries the next server
  • With stickyFailover enabled, failed servers are briefly penalized so subsequent requests prefer healthy ones
  • The loader preserves HTTP headers, byte-range requests, and response type

Requirements:

  • hls.js must be installed as a peer dependency
  • Blossom HLS playlists should use relative paths with SHA256 hashes as documented in the HLS formatting guide

Other Examples

List a page of blobs on a server

import { listBlobs, createListAuth } from "blossom-client-sdk";

async function signer(event) {
  return await window.nostr.signEvent(event);
}

const pubkey = "266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5";
const server = "https://cdn.example.com";

const blobs = await listBlobs(server, pubkey, {
  onAuth: async () => createListAuth(signer),
});

Iterate blob pages on a server

import { iterateBlobs, createListAuth } from "blossom-client-sdk/actions";

async function signer(event) {
  return await window.nostr.signEvent(event);
}

const pubkey = "266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5";
const server = "https://cdn.example.com";

for await (const page of iterateBlobs(server, pubkey, {
  limit: 100,
  onAuth: async () => createListAuth(signer),
})) {
  console.log(page);
}

Upload a single blob

import { uploadBlob, createUploadAuth } from "blossom-client-sdk";

async function signer(event) {
  return await window.nostr.signEvent(event);
}

const server = "https://cdn.example.com";

const blob = await uploadBlob(server, new File(["testing"], "test.txt"), {
  onAuth: async (server, sha256, type) => createUploadAuth(signer, sha256, { type }),
});

Upload a single blob to multiple servers

import { uploadBlob, createUploadAuth } from "blossom-client-sdk";

async function signer(event) {
  return await window.nostr.signEvent(event);
}

const servers = ["https://cdn.example.com", "https://cdn.other.com"];
const file = new File(["testing"], "test.txt");

const auth = await createUploadAuth(signer, file, { message: "Upload test.txt" });

for (let server of servers) {
  await uploadBlob(server, file, { auth });
}

Uploading and mirroring to multiple servers

The multiServerUpload method uploads a blob to the first server, then mirrors it to the remaining servers. It runs parallel preflight checks (HEAD /<sha256>) to detect which servers already have the blob, using /mirror to register ownership on those servers instead of re-uploading.

import { multiServerUpload, createUploadAuth } from "blossom-client-sdk";

async function signer(event: any) {
  // @ts-expect-error
  return await window.nostr.signEvent(event);
}

const servers = ["https://cdn.server-a.com", "https://cdn.example.com", "https://cdn.other.com"];
const file = new File(["testing"], "test.txt");

const results = await multiServerUpload(servers, file, {
  onAuth: async (server, sha256, type) => createUploadAuth(signer, sha256, { type }),
  onUpload: (server, sha256, blob) => {},
  onError: (server, sha256, blob, error) => {
    console.log("Failed to upload to", server);
    console.log(error);
  },
  // handle server rejections (413 too large, 415 unsupported type, etc.)
  onRejection: (server, sha256, blob, error) => {
    console.log(`Server ${server} rejected: ${error.code}`);
    return "skip"; // or "cancel" to abort entirely
  },
  // disable preflight checks if not needed (default: true)
  // preflight: false,
});

Uploading media and mirroring

The multiServerMediaUpload method is designed for media files (images, videos) that should be optimized before distribution. It uploads the blob to a single server's BUD-05 /media endpoint for processing, then mirrors the optimized result to all other servers. The original unprocessed blob is never uploaded to other servers since the /media endpoint may transform it (resize, re-encode, etc.).

import { multiServerMediaUpload, createUploadAuth } from "blossom-client-sdk";

async function signer(event: any) {
  // @ts-expect-error
  return await window.nostr.signEvent(event);
}

const servers = ["https://cdn.server-a.com", "https://cdn.example.com", "https://cdn.other.com"];
const media = new File(["image data"], "image.png");

const results = await multiServerMediaUpload(servers, media, {
  // try any server's /media endpoint, not just the first (default: "first")
  mediaUploadBehavior: "any",
  // fall back to regular upload if no /media endpoint is found (default: false)
  mediaUploadFallback: true,
  onAuth: async (server, sha256, type) => createUploadAuth(signer, sha256, { type }),
  onError: (server, sha256, blob, error) => {
    console.log("Failed to upload to", server);
    console.log(error);
  },
});

Upload and Mirror manually

import { uploadBlob, mirrorBlob, createUploadAuth } from "blossom-client-sdk";

async function signer(event) {
  return await window.nostr.signEvent(event);
}

const mainServer = "https://cdn.server-a.com";
const mirrorServers = ["https://cdn.example.com", "https://cdn.other.com"];
const file = new File(["testing"], "test.txt");

const auth = await createUploadAuth(signer, file, { message: "Upload test.txt" });

// first upload blob to main server
const blob = await uploadBlob(mainServer, file, { auth });

// then tell mirror servers to download it
for (let server of mirrorServers) {
  await mirrorBlob(server, blob, { auth });
}

Check if a blob exists

import { hasBlob } from "blossom-client-sdk/actions/has";

const exists = await hasBlob(
  "https://cdn.example.com",
  "b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553",
);

Blossom URIs (BUD-10)

Parse and build blossom: URIs for referencing blobs across servers

import { parseBlossomURI, buildBlossomURI, blossomURIToURL, blossomURIFromURL } from "blossom-client-sdk";

// parse a blossom URI
const parsed = parseBlossomURI(
  "blossom:b1674191a88ec5cdd733e4240a81803105dc412d6c6708d53ab94fc248f4f553.pdf?xs=cdn.example.com&as=266815e0c9210dfa324c6cba3573b14bee49da4209a9456f9484e5106cd408a5&sz=1024",
);
// -> { sha256: "b167...", ext: "pdf", servers: ["cdn.example.com"], authors: ["2668..."], size: 1024 }

// build a blossom URI
const uri = buildBlossomURI({ sha256: "b167...", ext: "pdf", servers: ["cdn.example.com"], authors: [], size: 1024 });
// -> "blossom:b167....pdf?xs=cdn.example.com&sz=1024"

// convert to/from native URL objects
const url = blossomURIToURL(parsed);
const backToParsed = blossomURIFromURL(url);

Resolve and download from a blossom URI

The resolveBlob function tries servers from the URI hints sequentially and returns the first successful response. Author hints are only resolved if server hints fail.

import { Actions } from "blossom-client-sdk";

const response = await Actions.resolveBlob("blossom:b167...4f553.pdf?xs=cdn.example.com&as=2668...08a5", {
  // resolve author pubkeys to server lists (only called if xs servers fail)
  getServers: async (pubkey) => {
    const event = await ndk.fetchEvent({ kinds: [USER_BLOSSOM_SERVER_LIST_KIND], authors: [pubkey] });
    return event ? getServersFromServerListEvent(event) : undefined;
  },
  // additional servers to try as a last resort
  fallbackServers: ["https://fallback.cdn.com"],
});

const blob = await response.blob();