Skip to content

Send to Kindle: name the reader in From, Reply-To them; doc@vapor.fyi as the reference sender - #107

Merged
alcor merged 2 commits into
mainfrom
feat/100-kindle-sender
Sep 13, 2026
Merged

alcor merged 2 commits into
mainfrom
feat/100-kindle-sender

Conversation

@alcor

@alcor alcor commented Sep 13, 2026

Copy link
Copy Markdown
Member

Follows #100 and #101.

Sender

Resend only mails from a domain verified in the operator's account, so a reader's own address cannot be the From. Instead each message goes out as "<Reader's name> via vapor" <SEND_FROM_EMAIL> with reply_to set to the reader's email (from the session), so the approved-sender address readers add at Amazon never changes while replies still reach the person who pressed Send. Nameless or anonymous senders fall back to the bare address. Quotes, angle brackets, and line breaks are stripped from the name before it enters the header.

handleDeviceRoutes gains a displayName(principal) dep, wired to the Registry profile in workers/app.ts, and passes sender through to sendKindle.

Reference instance

deploy/vapor.fyi.jsonc sets SEND_FROM_EMAIL to doc@vapor.fyi. Send to Kindle by email goes live once RESEND_API_KEY is set on the Worker (npx wrangler secret put RESEND_API_KEY -c deploy/vapor.fyi.jsonc) and vapor.fyi is verified as a sending domain in Resend; until then the Kindle row keeps offering the EPUB download.

Also

Drops the unused READ import in agents/mcp.ts left by #106, which failed npm run lint on main.

Verified

Typecheck and lint clean; 878 tests pass. New: From carries the name and Reply-To the email; bare address and no Reply-To when neither is known; header-unsafe characters stripped; the route passes the session's name and email as the sender.

🤖 Generated with Claude Code

alcor and others added 2 commits September 12, 2026 21:51
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… as the reference sender

Resend only mails from a domain verified in the operator's account, so the
reader's own address cannot be the From. Each message now goes out as
"<Reader's name> via vapor" <SEND_FROM_EMAIL> with Reply-To set to the
reader's email, so the approved-sender address stays constant while replies
still reach the person who pressed Send. Anonymous or nameless senders fall
back to the bare address.

The reference instance sets SEND_FROM_EMAIL to doc@vapor.fyi; RESEND_API_KEY
is a Workers secret and is set out of band.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@alcor
alcor merged commit 6557424 into main Sep 13, 2026
3 checks passed
@alcor
alcor deleted the feat/100-kindle-sender branch September 13, 2026 05:27
alcor added a commit that referenced this pull request Sep 13, 2026
…erialise from the editor (#108)

## Links open in a new tab

A browser never follows a link inside editable content, so clicking a
link in a document did nothing. The `AppLinks` plugin now handles every
link in the text: `vapor:` links act as before (Agents panel, New
document, Sign in), anything else opens in a new tab with
`noopener,noreferrer`, and the document stays where it is. Taps on touch
do the same.

Links imported from markdown also rendered without `target`: the
`richSchema` link mark stores `target: null`, and that null won over
TipTap's `_blank` default at render time. The `Link` mark in
`app/lib/app-links.ts` keeps `target` and `rel` on the mark for schema
parity with the server but never renders them from it, so every link
element carries `target="_blank" rel="noopener noreferrer nofollow"`.
`@tiptap/extension-link` becomes a direct dependency at the version
StarterKit already installs.

## Table text at 80%

`.tiptap table { font-size: 0.8em }`. Tables carry more per line than
prose; the smaller type keeps columns readable. Cells measure 12.88px
against 16.1px prose in the dev server.

## Table cells serialise from the editor (found while verifying)

`serializeCellInline` wrapped a cell's content in a `richSchema`
paragraph. The editor serialises TipTap documents through the same path,
and a paragraph from one schema refuses content from another, so on any
document with a table every Yjs update threw inside the client's update
handler (`Caught error while handling a Yjs update: Invalid content for
node paragraph`). The client's markdown state then stayed empty: the tab
title fell back to "vapor", the URL lost its slug, and anything else
derived from that markdown went stale. The wrapper nodes now come from
the cell's own schema. Dates from #27; separate commit.

## Verified

Typecheck clean; 880 tests pass. Lint reports only the unused `READ`
import on main that #107 removes. New tests: a click or tap on an
ordinary link calls `window.open` with `_blank` and is prevented; plain
text is left alone; a link stored with `target: null` renders `_blank`
and its rel; a table from a second schema instance serialises unchanged.
In the browser: the seeded document's link carries `target="_blank"`, a
click attempts to open example.com in a new tab while the document tab
stays put, cells render at 80%, and the tab title and slug now survive
load on a document with a table.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
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.

1 participant