# blossom-upload.ts

> **Nostr Agent Onboarding** · [start here](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/start.md) · [all examples](https://npub1d70emggs6jzun5lhvqnfqsd9reqmaarn2qjf6q3r02gryyl4v8sqjn44xe.nsite.lol/examples/README.md) · source: `nostr-dev/sdk/examples/blossom-upload.ts` · snapshot 2026-10-10
>
> Paths such as `~/Documents/…`, `~/Production Environment/…`, `repos/…` and services on `localhost` refer 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.

```ts
/**
 * 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);
```
