Files
podsteadr/server/src/services/blossom.ts
T

71 lines
2.2 KiB
TypeScript
Raw Normal View History

import { createHash } from 'node:crypto';
import { readFile } from 'node:fs/promises';
import { finalizeEvent } from 'nostr-tools/pure';
export interface BlobCheck {
exists: boolean;
size: number | null;
}
/** HEAD a blob on a blossom server to confirm it exists and matches the claimed size. */
export async function checkBlob(blossomUrl: string, sha256: string): Promise<BlobCheck> {
const res = await fetch(`${blossomUrl.replace(/\/+$/, '')}/${sha256}`, { method: 'HEAD' });
if (!res.ok) return { exists: false, size: null };
const len = res.headers.get('content-length');
return { exists: true, size: len ? Number(len) : null };
}
/** Build a BUD-02 upload authorization event (kind 24242) signed with the given secret key. */
export function buildUploadAuth(secretKey: Uint8Array, sha256: string, description: string) {
const now = Math.floor(Date.now() / 1000);
return finalizeEvent(
{
kind: 24242,
created_at: now,
content: description,
tags: [
['t', 'upload'],
['x', sha256],
['expiration', String(now + 600)],
],
},
secretKey,
);
}
export interface UploadResult {
sha256: string;
size: number;
url: string;
}
/**
* Server-side blossom upload (used for publishing stream recordings, which live on
* the server and are signed with the server's own nostr key).
*/
export async function uploadFile(
blossomUrl: string,
filePath: string,
mime: string,
secretKey: Uint8Array,
): Promise<UploadResult> {
const data = await readFile(filePath);
const sha256 = createHash('sha256').update(data).digest('hex');
const auth = buildUploadAuth(secretKey, sha256, `Upload ${sha256}`);
const base = blossomUrl.replace(/\/+$/, '');
const res = await fetch(`${base}/upload`, {
method: 'PUT',
headers: {
authorization: `Nostr ${Buffer.from(JSON.stringify(auth)).toString('base64')}`,
'content-type': mime,
'content-length': String(data.length),
},
body: data,
});
if (!res.ok) {
const text = await res.text().catch(() => '');
throw new Error(`blossom upload failed: ${res.status} ${text.slice(0, 200)}`);
}
return { sha256, size: data.length, url: `${base}/${sha256}` };
}