blossom-upload.ts
Nostr Agent Onboarding · start here · all examples · source:
nostr-dev/sdk/examples/blossom-upload.ts· snapshot 2026-10-10Paths such as
~/Documents/…,~/Production Environment/…,repos/…and services onlocalhostrefer to the author's workstation and are not available to you — read them as worked examples of a setup you can recreate.
Upload a blob to a Blossom server with a signed kind 24242 auth event.
/**
* Upload a file to the local Blossom server (built into the ngit-relay container)
* using a NIP-98-style kind:24242 auth event, then verify it round-trips.
*
* Usage:
* tsx examples/blossom-upload.ts <path-to-file>
*
* Env:
* NOSTR_SECRET_KEY nsec or hex secret key for signing the upload auth event
*
* Server: http://localhost:8081 (override with BLOSSOM_SERVER env var)
*/
import { readFileSync, statSync } from "node:fs";
import { createHash } from "node:crypto";
import { finalizeEvent, getPublicKey, nip19 } from "nostr-tools";
const filePath = process.argv[2];
if (!filePath) {
console.error("usage: tsx examples/blossom-upload.ts <path-to-file>");
process.exit(1);
}
const sec = process.env.NOSTR_SECRET_KEY;
if (!sec) {
console.error("error: set NOSTR_SECRET_KEY (nsec or hex) in the environment");
process.exit(1);
}
const sk: Uint8Array = sec.startsWith("nsec1")
? (nip19.decode(sec).data as Uint8Array)
: Uint8Array.from(Buffer.from(sec, "hex"));
const pk = getPublicKey(sk);
const server = (process.env.BLOSSOM_SERVER ?? "http://localhost:8081").replace(/\/$/, "");
// Read the file and compute its sha256
const data = readFileSync(filePath);
const size = statSync(filePath).size;
const sha256 = createHash("sha256").update(data).digest("hex");
console.log(`file: ${filePath} (${size} bytes)`);
console.log(`sha256: ${sha256}`);
console.log(`pubkey: ${pk}`);
console.log(`server: ${server}`);
// Build the BUD-01 auth event (kind 24242)
const now = Math.floor(Date.now() / 1000);
const authEvent = finalizeEvent(
{
kind: 24242,
created_at: now,
tags: [
["t", "upload"],
["x", sha256],
["expiration", String(now + 600)],
],
content: `upload ${filePath}`,
},
sk
);
const authHeader = "Nostr " + Buffer.from(JSON.stringify(authEvent)).toString("base64");
// Upload
const uploadRes = await fetch(`${server}/upload`, {
method: "PUT",
headers: { Authorization: authHeader, "Content-Type": "application/octet-stream" },
body: data,
});
console.log(`\nupload: HTTP ${uploadRes.status}`);
const uploadBody = await uploadRes.text();
console.log(` ${uploadBody.slice(0, 200)}`);
if (!uploadRes.ok) process.exit(1);
// Verify with HEAD
const head = await fetch(`${server}/${sha256}`, { method: "HEAD" });
console.log(`\nHEAD: HTTP ${head.status} Content-Length=${head.headers.get("content-length")}`);
// Verify with GET
const get = await fetch(`${server}/${sha256}`);
const got = Buffer.from(await get.arrayBuffer());
const gotSha = createHash("sha256").update(got).digest("hex");
console.log(`GET: HTTP ${get.status} ${got.length} bytes sha256=${gotSha}`);
console.log(`match: ${gotSha === sha256 ? "✓" : "✗"}`);
// List
const list = await fetch(`${server}/list/${pk}`).then((r) => r.json() as Promise<any[]>);
console.log(`\nlist/${pk.slice(0, 8)}…: ${list.length} blob(s) for our pubkey`);
process.exit(0);