diff --git a/README.md b/README.md index 96749a0..2e1a97d 100644 --- a/README.md +++ b/README.md @@ -204,6 +204,8 @@ Blossom is a protocol for storing blobs (files) on media servers using your Nost 2. Click **Publish to Blossom (with header)** (or run **Nostr: Publish to Blossom (with header)**). 3. The front-matter block is stripped, the markdown body is uploaded as a blob, and a kind 1063 event is built from the descriptor + front-matter (title, author, language, year, cover, tags) and published to your relays. +> **Markdown MIME handling:** markdown blobs are uploaded as `text/plain` so browsers render them inline (no mainstream browser displays `text/markdown`, and servers send `X-Content-Type-Options: nosniff` which forbids guessing). The kind 1063 event's `m` tag still advertises `text/markdown`, so Nostr clients that render markdown can detect the file's true type. + **Blossom servers**: - The **Blossom Servers** section shows your servers with checkboxes — exactly like the Relays section. Every checked server receives the upload. diff --git a/package.json b/package.json index 7be933c..a8d1276 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "nostr-publish", "displayName": "Nostr Publish", "description": "Publish files to Nostr from Codium/VSCode. Phase 0: raw unsigned event JSON. Phase 1: long-form notes (.md). Phase 2: signer (Rust n_signer port) signing backend.", - "version": "0.1.1", + "version": "0.1.2", "publisher": "laantungir", "license": "MIT", "repository": { diff --git a/src/extension.ts b/src/extension.ts index 08a79f7..cb9e7d7 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1869,6 +1869,12 @@ function resolveBlossomServers(): string[] { * 3. Uploads to all checked Blossom servers concurrently. * 4. Builds a kind 1063 NIP-94 event from the first successful descriptor. * 5. Signs and publishes the 1063 event to the enabled relays. + * + * Markdown bodies are uploaded as `text/plain` (browsers render text/plain + * inline; no mainstream browser displays text/markdown, and servers send + * `X-Content-Type-Options: nosniff` which forbids guessing). The kind 1063 + * event's `m` tag still advertises `text/markdown` so Nostr clients that + * render markdown can detect the file's true semantic type. */ async function uploadAndPublishMetadata( state: StateService, @@ -1877,6 +1883,12 @@ async function uploadAndPublishMetadata( body: Buffer, mime: string, ): Promise { + // Hybrid MIME handling: markdown uploads go out as text/plain for + // browser-renderability; the semantic type is preserved for the m tag. + const isMarkdown = mime === "text/markdown"; + const uploadMime = isMarkdown ? "text/plain" : mime; + const semanticMime = isMarkdown ? "text/markdown" : undefined; + // Require sign-in. let signer = getSigner(state, config); if (!signer) { @@ -1938,7 +1950,7 @@ async function uploadAndPublishMetadata( results = await uploadBlobToMany( { body, - contentType: mime, + contentType: uploadMime, sha256, signer, authTokenTtlSec: config.blossomAuthTokenTtlSec, @@ -1966,7 +1978,12 @@ async function uploadAndPublishMetadata( // Build the kind 1063 event from the first successful descriptor. const descriptor: BlobDescriptor = ok[0].descriptor!; - const template = buildFileMetadataEvent({ descriptor, meta, publishedAt }); + const template = buildFileMetadataEvent({ + descriptor, + meta, + publishedAt, + mimeType: semanticMime, + }); // Validate. const validation = validateEventTemplate(template); diff --git a/src/nostr/nip94.ts b/src/nostr/nip94.ts index cf70859..fc13bec 100644 --- a/src/nostr/nip94.ts +++ b/src/nostr/nip94.ts @@ -13,6 +13,17 @@ export interface FileMetadataInput { meta: BlossomFrontMatter; /** First-publish timestamp (unix seconds). Reused on updates. */ publishedAt: number; + /** + * Override for the `m` tag (the file's semantic MIME type). + * + * Markdown blobs are uploaded as `text/plain` so browsers render them + * inline (no mainstream browser displays `text/markdown`, and + * `X-Content-Type-Options: nosniff` forbids guessing). The server then + * reports `type: text/plain` in the descriptor. This override keeps the + * Nostr `m` tag advertising the true semantic type (`text/markdown`) so + * clients that render markdown can still detect it. + */ + mimeType?: string; } /** @@ -47,10 +58,21 @@ export function buildFileMetadataEvent(input: FileMetadataInput): EventTemplate // Ensure the core tags exist even if nip94 omitted them. ensureTag(tags, "url", descriptor.url); - ensureTag(tags, "m", descriptor.type); + ensureTag(tags, "m", input.mimeType ?? descriptor.type); ensureTag(tags, "x", descriptor.sha256); ensureTag(tags, "size", String(descriptor.size)); + // When an explicit mimeType override is given, replace any server-provided + // `m` tag (BUD-08 nip94 tags carry the upload Content-Type, which for + // markdown is deliberately text/plain — see FileMetadataInput.mimeType). + if (input.mimeType) { + for (const t of tags) { + if (t[0] === "m") { + t[1] = input.mimeType; + } + } + } + // Descriptive metadata from front-matter. tags.push(["title", meta.title]); if (meta.summary) {