Skip to content

feat(paste): write a paste in markdown, off by default - #44

Merged
thoda-dev merged 1 commit into
masterfrom
markdown-pastes
Sep 1, 2026
Merged

feat(paste): write a paste in markdown, off by default#44
thoda-dev merged 1 commit into
masterfrom
markdown-pastes

Conversation

@thoda-dev

Copy link
Copy Markdown
Owner

What this changes

Closes #43

A switch on the creation form, off by default, turns the textarea into TextEditor and stores the paste as markdown. The read page renders it, with the source one click away and a copy button that says it hands over the markdown source. Plain text is untouched: same field, same <pre>, same copy.

Why

The same link that carries a token also carries handover notes, and those arrived as one monospace block. Issue #43 has the longer version, including the measurement that decides the shape of this: a rich text editor stores markdown, not what was typed, so Tr0ub4dor&3_*hunter2* is stored as Tr0ub4dor&amp;3\_*hunter2* and the copy button would hand that to the recipient. Hence an option rather than a replacement, and off rather than on.

Four things about the shape of it:

The format is a column, not a fragment parameter. #key=…&f=md would have avoided a migration and told the server nothing, but the column survives a mangled link and lets /meta say what is coming before the reveal. A pastes_format_kind_check constraint keeps it plain for a file, which is downloaded and never rendered.

Links are allowed through the parser and flattened afterwards. Leaving a out of the allowlist looked like the obvious way to keep a paste unclickable, and it is wrong: comark's security plugin drops a disallowed element with its children, so [docs](https://example.com) became an empty paragraph and see https://example.com became see . Links now parse normally and flattenLinks rewrites them to docs (https://example.com), printing an autolinked URL once rather than twice. Nothing is clickable, and the destination is visible — which is what a page holding a decryption key wants, since a paste is written by anyone and read by whoever was sent the link.

Images warn instead of blocking. The parser already dropped them, so nothing renders broken; the warning explains why, at creation, where it can still be acted on. It is wired into /admin/legal too, which is the half of #29 that was never done — the fix stopped the broken icon, it never told the operator anything. The markdown itself is kept: a document being passed along may well be going somewhere images do work.

The parser is now shared. shared/utils/legal-markdown.ts becomes markdown.ts and exports parseDocumentMarkdown (legal pages, links clickable, the operator vouches for them) and parsePasteMarkdown (links flattened), over one security configuration. LegalDocument.vue and LegalTable.vue follow as RichText.vue and RichTextTable.vue, since both are now used by both.

Two interface adjustments ride along: the creation and read cards widen to max-w-5xl, the width the admin pages already use, and the editor gets a minimum height so an empty one is not a sliver.

Checks

  • pnpm lint, pnpm typecheck and pnpm test pass — 183 tests, 11 new
  • Schema change? A migration is committed alongside it (pnpm db:generate) — 0005_naive_tigra.sql, adding the enum, the column with a plain default and the check constraint
  • Touches crypto, authorization, or the paste read counter? Say so here — those get a closer read

None of the three, but the read path is worth a careful look anyway. Nothing about encryption, key derivation, the unlock hash or the atomic counter changes — the format travels beside the ciphertext and is never derived from it. What is new is that the read page now renders attacker-supplied content on the one page that was previously inert, so the parser configuration is the thing to review: registerDefaultPlugins: false (no raw HTML, no component syntax, no free-form attributes) and an allowlist that has never included img. tests/markdown.test.ts covers raw HTML, javascript: URLs, refused protocols and images for the paste parser as it already did for the document one.

Worth knowing when reviewing: the migration has to be applied locally (pnpm db:migrate) before the creation form works — generating the file does not touch the database, and the first paste otherwise fails with column "format" of relation "pastes" does not exist.

AI assistance

See CONTRIBUTING.md. Disclosure is not held against you: it tells
the reviewer where to look hardest.

  • No AI tool was used
  • An AI tool was used — which one, and roughly how much of the change: Claude Code, all of it. I asked for the editor on the paste form; it measured the round trip, argued the default had to stay plain text, and I designed the opt-in flow with it from there. The column over the URL fragment was my call, flattening links rather than dropping them was its correction after the first approach silently ate them.

Copilot AI lite review requested due to automatic review settings September 1, 2026 18:56

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Several new references to shared markdown utilities are missing imports in changed files, which will cause runtime/build failures.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds an opt-in Markdown mode for text pastes (stored as Markdown source but rendered safely on read), while preserving the existing plain-text “copy exact bytes” behavior as the default.

Changes:

  • Introduces paste_format/format on pastes (default plain) plus API plumbing and /meta exposure.
  • Adds shared Markdown parsing utilities with paste-specific link flattening and image detection + tests.
  • Updates UI/docs/i18n to support switching editors on create and rendering/copy/source toggles on read; adds operator warning for images in legal editor too.
File summaries
File Description
ROADMAP.md Removes “waiting on upstream” note now that Markdown support is implemented.
apps/docs/content/3.using-shhh/1.creating-a-paste.md Documents the new opt-in Markdown formatting and its tradeoffs (copying source, links, images).
apps/docs/content/2.self-hosting/5.legal-pages.md Clarifies image behavior and adds warning semantics for legal markdown.
apps/app/tests/markdown.test.ts Adds comprehensive tests for shared markdown parsing, link flattening, images, and security behaviors.
apps/app/tests/legal-markdown.test.ts Removes superseded tests (parser moved/renamed).
apps/app/tests/editor-roundtrip.test.ts Updates roundtrip test to use the new document markdown parser.
apps/app/shared/utils/markdown.ts New shared markdown parser utilities (parseDocumentMarkdown, parsePasteMarkdown, containsMarkdownImage).
apps/app/shared/utils/legal-markdown.ts Removes old, legal-only markdown parser (superseded by markdown.ts).
apps/app/server/database/schema/paste.ts Adds paste_format enum + format column and a constraint to keep files plain.
apps/app/server/database/migrations/meta/0005_snapshot.json Migration snapshot update for the new enum/column/constraint.
apps/app/server/database/migrations/meta/_journal.json Registers migration 0005_naive_tigra.
apps/app/server/database/migrations/0005_naive_tigra.sql Creates paste_format, adds pastes.format with default, and adds check constraint.
apps/app/server/api/pastes/index.post.ts Accepts and stores format for text pastes (default plain).
apps/app/server/api/pastes/[id]/reveal.post.ts Returns format alongside ciphertext for text pastes.
apps/app/server/api/pastes/[id]/meta.get.ts Exposes format pre-reveal for text pastes (files unchanged).
apps/app/server/api/legal/[slug].get.ts Switches legal rendering to parseDocumentMarkdown.
apps/app/server/api/admin/legal/index.put.ts Validates legal markdown via parseDocumentMarkdown before saving.
apps/app/i18n/locales/fr.json Adds labels/hints for Markdown mode + copy/source/view toggles + image warning strings.
apps/app/i18n/locales/en.json Adds labels/hints for Markdown mode + copy/source/view toggles + image warning strings.
apps/app/app/pages/p/[id].vue Renders markdown pastes client-side (safe parser) with “view source” + “copy source” affordances; widens card.
apps/app/app/pages/legal/[slug].vue Uses RichText component for legal documents.
apps/app/app/pages/index.vue Adds “Format with Markdown” switch, editor swap, image warning, and sends format on create; widens card.
apps/app/app/pages/admin/legal.vue Adds image-warning alert for operators editing legal markdown.
apps/app/app/components/TextEditor.client.vue Adds minimum height styling so empty editor isn’t a sliver.
apps/app/app/components/RichTextTable.vue New table wrapper component to match Tiptap’s table wrapper behavior.
apps/app/app/components/RichText.vue Updates table component mapping to RichTextTable.
apps/app/app/assets/css/rich-text.css Updates comment/reference to RichTextTable.vue.
Review details
  • Files reviewed: 26/27 changed files
  • Comments generated: 5
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread apps/app/app/pages/admin/legal.vue
Comment thread apps/app/app/pages/index.vue
Comment thread apps/app/app/pages/p/[id].vue
Comment thread apps/app/server/api/admin/legal/index.put.ts
Comment thread apps/app/server/api/legal/[slug].get.ts
@thoda-dev
thoda-dev merged commit b3dcbb1 into master Sep 1, 2026
7 checks passed
@thoda-dev
thoda-dev deleted the markdown-pastes branch September 1, 2026 19:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A paste can only be plain text, so a handover note arrives as one monospace block

2 participants