@kurtbruns/kestrel 1.19.0 → 1.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/CHANGELOG.md +69 -1
  2. package/bin/kestrel.mjs +1 -1
  3. package/cli/seed.mjs +2 -8
  4. package/dist/public/dashboard/app.js +307 -306
  5. package/dist/public/dashboard/app.js.map +4 -4
  6. package/dist/public/dashboard/index.html +2 -2
  7. package/dist/public/dashboard/styles.css +90 -51
  8. package/dist/worker/index.js +890 -559
  9. package/docs/get-started/02-deploy.md +3 -3
  10. package/docs/get-started/03-access.md +8 -4
  11. package/docs/get-started/04-aws-account.md +2 -0
  12. package/docs/get-started/05-ses.md +4 -2
  13. package/docs/get-started/07-verify.md +8 -6
  14. package/docs/get-started/08-go-live.md +12 -18
  15. package/docs/guides/01-allow-claude.md +7 -2
  16. package/docs/guides/03-notifications.md +17 -13
  17. package/docs/guides/07-upgrade.md +16 -2
  18. package/docs/publishing/03-name-your-publication.md +8 -0
  19. package/docs/publishing/04-write-a-post.md +2 -2
  20. package/docs/publishing/06-troubleshooting.md +52 -26
  21. package/docs/publishing/11-email-template.md +1 -1
  22. package/docs/publishing/README.md +1 -1
  23. package/docs/reference/02-sending-domain-dns.md +3 -3
  24. package/migrations/0021_notifications_outage_receipts.sql +46 -0
  25. package/package.json +10 -10
  26. package/packages/kestrel/lib/api.mjs +19 -12
  27. package/packages/kestrel/lib/login.mjs +2 -2
  28. package/packages/kestrel/lib/media-types.mjs +34 -0
  29. package/packages/kestrel/lib/oauth.mjs +19 -2
  30. package/packages/kestrel/lib/openapi.mjs +1 -1
  31. package/template/README.md +1 -1
  32. package/template/claude/CLAUDE.md +1 -1
  33. package/template/claude/skills/upgrade/SKILL.md +1 -0
package/CHANGELOG.md CHANGED
@@ -10,6 +10,72 @@ To move a running instance from one version to another, follow [Upgrade to a new
10
10
 
11
11
  <!-- Add entries under Added / Changed / Fixed / Breaking. One operator-facing line each; see .claude/rules/changelog.md. Upgrade steps (a migration, a new setting) go once, in the Upgrading paragraph, never on an entry. -->
12
12
 
13
+ ## [1.21.0] - 2026-11-06
14
+
15
+ **Upgrading from 1.20.0.** No database migrations, so `npm run sync-migrations` brings in nothing and there's nothing to apply. In your instance's repository, start with nothing uncommitted, as [Upgrade to a new release](https://github.com/kurtbruns/kestrel/blob/v1.21.0/docs/guides/07-upgrade.md) now says, and install the release with `npm install @kurtbruns/kestrel@1.21.0`. If your `package.json` holds wrangler below 4.156.0, with an exact version or a `~` range, npm refuses that with `ERESOLVE`, so run `npm install @kurtbruns/kestrel@1.21.0 "wrangler@^4.156.0"` instead. Run `npm run check-context`: this release changes two files of the operator's Claude context, `CLAUDE.md` and the upgrade skill, the skill's change bearing on safety, and a copy you've edited is merged by hand. If a script of yours uploads images or a logo, make it send each file's real type and a name that matches it before you deploy (see Breaking). Then run `npm run typecheck`, commit the upgrade, note a restore point, and deploy with `npx wrangler deploy --env production`, as the guide lays out. If you alert on `error` in your logs, alert on `d1.unavailable` repeating as well, since a database outage can now show only as that warning. If you use Claude Code, update it to v2.1.172 or later, or its changes read "via the Kestrel CLI" and the dashboard can stop saying Claude is connected.
16
+
17
+ ### Added
18
+
19
+ - Each section of Settings has its own address, such as `#/settings/notifications`, and the dashboard's failed-notification line, the Get notified step, the test notification's Open Settings link, and the sections the Docs tab's pages name now open the section they mean.
20
+ - A new log line, `mcp.initialize` (info), records the name and version a connector says it is each time it opens. Nothing a call records changes, so a change made through the connector is still recorded as `claude` (SPEC §12).
21
+ - You can drop an image on the logo under Settings → Publication identity to upload it, the same as choosing it in the file picker, and Name your publication now says what format, size, and shape a logo works best in. One logo change now runs at a time: a pick, a drop, or Remove while another is saving is refused, with a message to try again once it's done.
22
+
23
+ ### Changed
24
+
25
+ - Kestrel now needs wrangler 4.156.0 or later, which carries fixes for security advisories in the local runtime it bundles. A new instance starts on it, and installing this release moves an existing instance's wrangler up on its own, with no change to its `package.json`, unless that `package.json` holds it below 4.156.0 with an exact version or a `~` range.
26
+ - Upgrade to a new release now starts from a clean repository, with anything `git status` lists committed on its own or moved out first, and the operator's Claude context (`template/claude/skills/upgrade/SKILL.md`) has Claude ask before committing it and otherwise wait. This bears on safety: unrelated work swept into the upgrade's commit would be undone by going back. The same page and Deploy the app now say that the migration command, run by Claude or in any other shell that isn't interactive, applies migrations without asking.
27
+ - The operator's Claude context (`template/claude/CLAUDE.md`) gives a corrected reason for asking for an image's original file: in the Claude desktop app a pasted image can be uploaded, but it's a converted copy under a generated name, not the file the person had. The advice to ask for the original is unchanged.
28
+ - A brief D1 stall the next sweep tick makes up for now logs one `d1.unavailable` line at `warn`, not `error` lines and a failed invocation, and a failure the sweep catches now carries a `stack` field (SPEC §12).
29
+ - The setup guide's Connect Amazon SES and Sending-domain DNS pages now say that `p=none` only monitors, so it doesn't stop anyone sending as your domain, and that DMARC reports (`rua`) are optional, and the optional step that forwards reports now has you check that they arrive.
30
+ - The setup guide's Notifications through Cloudflare's email page now shows the `send_email` binding in place in `env.production`, with only its name, so no notifications address is committed to your repository. It also says how the dashboard shows the DNS records once they're added, no longer promises a DKIM record for the hostname, points to Email Routing's **Activity log** for a notification's status, and has you check the spam folder for the test. An instance set up from the old page, with addresses in its binding, keeps working.
31
+ - An update to the Markdown parser can change a new render in a few edge cases: a link with an unbalanced parenthesis, such as `[a](https://example.com/foo(bar)`, now shows as typed instead of as a link that lost its last parenthesis; a numeric character reference such as `&#8212;` shows as its character in the plain-text part too; `www.` inside a word no longer links; and lazy lines after a blockquote, a `#` with no space in a list item, and HTML right after a paragraph can land in a different block. A post already sent or scheduled keeps its email, unless a template or identity change re-makes a scheduled one (SPEC §6).
32
+ - On a phone, the text and images in a post's email sit closer to the edges, in mail apps that support it, such as Mail on iPhone; other apps and wide displays show what they did. This applies to emails made from now on and their post pages: a post already sent or scheduled keeps its email and page, unless a template or identity change re-makes a scheduled one (SPEC §6).
33
+ - The confirmation email now looks like a post's email, with the same white page, column, and margins on every display, in place of a card on a gray page. It's made when it's sent, so this applies as soon as you deploy. Its preview in Settings shows it the same way.
34
+
35
+ ### Fixed
36
+
37
+ - `kestrel api` now names Claude Code only when Claude Code runs it, where it named Claude Code whoever ran it. Run any other way, by you in a terminal or by a script of yours that doesn't pass `--client`, its changes read "via the Kestrel CLI" in the editor and its client is `kestrel-cli`. So do Claude Code's own changes on a version older than v2.1.172, and the dashboard can stop saying Claude is connected. Changes already recorded keep the name they have.
38
+ - The API reference now says what each client name read back as `client` or `author_client` means (`editor`, `claude-code`, `kestrel-cli`, and `claude`, the name a change made through the connector is recorded under), so Claude can tell a publisher who scheduled a send; it also says no client of Kestrel's but the web editor claims `editor`.
39
+ - `npx kestrel api ... --form file=@cover.png` uploads a post image or a logo, where every file was refused as not a PNG, JPEG, WebP, or GIF: it now states the file's type, read from its extension.
40
+ - A `HEAD` request now answers like the `GET` for the same path, without the body, where every path answered 404: an uptime monitor pointed at `/health` no longer reports the instance down, and link checkers and `curl -I` see pages and images as they are.
41
+ - A send to one person now reads "1 recipient", not "1 recipients", on its scheduled card on the dashboard and the Sent page, and on its page while sending or held and once sent.
42
+ - A reader's confirm or unsubscribe token, and a post's unlisted link, no longer reach Cloudflare's invocation log in the `Referer` header their browser sent from those pages. Reader pages, post pages included, now tell the browser to send no referrer, so a site a post page links to also stops seeing where the visit came from. Lines already stored age out with log retention (SPEC §5, §12).
43
+
44
+ ### Breaking
45
+
46
+ - Image and logo uploads read a file's format from its bytes (SPEC §4). A file that isn't a PNG, JPEG, WebP, or GIF is refused even under an image's name, where it used to upload and show as a broken image, and so is one named or declared as a different one of those formats than it is, like a JPEG called `photo.png`, with a message saying what it really is. So a script that always sends `--type image/png`, or always names its uploads `cover.png`, is now refused on any file of another format.
47
+
48
+ ## [1.20.0] - 2026-11-04
49
+
50
+ **Upgrading from 1.19.0.** In your instance's repository, run `npm run sync-migrations`, which brings in one database migration, `0021_notifications_outage_receipts`: it rebuilds the notifications table for the new notices, carrying every row over, and closes any import notice not yet sent, since this release sends none. Run `npm run check-context`: this release changes the operator's Claude context, and a copy you've edited is merged by hand. Apply the migration before you deploy, with `npx wrangler d1 migrations apply DB --remote --env production` (and `--local` for local development). Then deploy with `npx wrangler deploy --env production`. A script of yours that replaced an image by uploading its name again needs changing (see Breaking); nothing else does.
51
+
52
+ ### Added
53
+
54
+ - Kestrel emails you when your provider has sent no receipts for six hours after a send, once until a receipt arrives, since a broken connection stops bounces and complaints from suppressing anyone (SPEC §8).
55
+
56
+ ### Changed
57
+
58
+ - Notification emails are rewritten to say whether anyone has to act, and how, with the details left to the dashboard: Sent is a line or two and a link, every problem reads in one order, links are named for what you'd do there, and each email says why you got it (SPEC §8).
59
+ - One outage now sends one email instead of a per-day notice beside a per-post one, naming the posts it holds, plus one more if a post is first held after an email that named none (SPEC §8). The notification status's `kind` gains `no_receipts` and `not_sending_held`.
60
+ - A send that goes out late says so in its Sent email instead of a separate one, and a finished send no one accepted is now told as a problem, since it can't be sent again (SPEC §8).
61
+ - A reader's address in your provider's words shows only as its domain in an email, and in full on the send's page (SPEC §12).
62
+ - The fix for a provider refusal, in the editor, the API's `advice` and `message`, and email, now says what to do and adds "If someone else set up Kestrel for you, pass this along to them." instead of "Whoever runs the deployment" (SPEC §12).
63
+ - Kestrel no longer emails you when a service token imports a list. The import stays on the record with who committed it (SPEC §7).
64
+ - The setup guide's checks now say what to do about the failures a real setup ran into, in a sentence beside the check, and Troubleshooting is a publisher's page again: it opens with a table of symptoms and where each fix is, and a section on where to look. Its section on Claude's Access setup moved to Allow Claude to connect, the one on Cloudflare refusing to save the Access application to Lock the dashboard with Access, and the one on outgrowing Workers Free now points to As your list grows.
65
+ - The dashboard's **Finish setting up** card now says under its title that it stays until your first post goes out to the archive and that email-only sends don't count, and **Share your subscribe link** says to share it before then, so readers get that post.
66
+ - A template test email now says what it is, under the subject "Template test: how your posts will look", instead of arriving as a made-up post about starlings under your own sign-off. Settings' previews of the reader pages show the same sample; the Template page's preview keeps its placeholder text.
67
+ - The setup guide's Go live page is now a checkpoint on where to go next: one path if you're writing the newsletter, one if someone else is, and the optional guides, each named by when you'd want it. How to let someone else into the editor moves to its own section of Lock the dashboard with Access.
68
+ - The operator's Claude context (`template/claude/CLAUDE.md`) now says to reference an image by the `filename` the upload answers, since it may differ from the name sent, says how to swap an image, and names the preview's new warnings.
69
+
70
+ ### Fixed
71
+
72
+ - An image uploaded with spaces in its name, as every macOS screenshot has, or with other characters that break a Markdown reference, now shows in the email instead of its `![…](…)` text: the upload stores it with hyphens in their place and answers that `filename` to reference, and images already stored keep their names. The preview now warns when text that reads as an image reference didn't become an image, and when an image uploaded to the post isn't used by it (SPEC §4), and the editor's Preview tab now shows a render's warnings, which it used to show only after a test.
73
+ - Two images pasted into one post no longer become one image: a browser names every pasted image the same, so the second replaced the first, both references showed it, and the preview could show a stale copy.
74
+
75
+ ### Breaking
76
+
77
+ - An upload no longer replaces an image of the same name. A name the post already has gets a number, so a second `cover.jpg` is stored as `cover-2.jpg`, and the response's `filename` says which (SPEC §4). A client that replaced an image by uploading its name again now uploads the new one, references its `filename`, and deletes the old one.
78
+
13
79
  ## [1.19.0] - 2026-11-02
14
80
 
15
81
  **Upgrading from 1.18.0.** If your instance sends through Amazon SES, let the app's AWS key send templated email now, whether or not you upgrade: in the IAM console, open **Policies**, then `kestrel-send-email`, choose **Edit**, add `"ses:SendTemplatedEmail"` and `"ses:SendBulkTemplatedEmail"` to its `Action` list in the JSON editor, and save. Keep the actions already there. A key with the guide's earlier policy can't send at all: until you add them, SES refuses every email the app sends as not authorized, test emails included. While you're there, add `"ses:GetConfigurationSet"` too, so the app can tell whether the configuration set exists and can send. Without it the app says nothing about the set, and is never wrong about it. In your instance's repository, run `npm run sync-migrations`, which brings in five database migrations: `0016_held_receipts`, a new table for receipts SES reports before the app has recorded the message they name, which rewrites nothing; `0017_provider_account_rejected`, which rebuilds the one-row table where the app keeps what it reads of your SES account, so it can record a key SES rejects, carrying its row over; `0018_notify_addresses`, which records, for each notification, the addresses it has reached, and rewrites nothing; `0019_sending`, a one-row table recording whether email is getting out, which starts as working when your instance's records show email went out (a post SES took a recipient of, or a subscriber who confirmed through a link it mailed), so a live instance keeps its signups open; and `0020_notifications_not_sending`, which rebuilds the notifications table for the new notice that email isn't going out, carrying every row over. Your saved notifications address carries over as a list of that one address. Run `npm run check-context`: this release changes the operator's Claude context, and a copy you've edited is merged by hand. Apply the migrations before you deploy, with `npx wrangler d1 migrations apply DB --remote --env production` (and `--local` for local development). Then deploy with `npx wrangler deploy --env production`. Then open **Settings → Email sender**. If it shows **Not proven yet**, as it does when your records show no email went out beyond tests, signups stay closed until SES takes an email: send one test email from **Template → Send test email** to open them. Within about ten minutes the app also checks your SES account, and a red warning there now pauses signups until the check after the fix. A script of yours that read a subscriber's `confirm_token` or `unsub_token` from the API, or read or wrote `notifications.to` in the settings, needs changing (see Breaking); nothing else does.
@@ -731,7 +797,9 @@ The first tagged release: a self-contained newsletter app on a Cloudflare Worker
731
797
  - A build-version stamp: every instance reports its version, commit, and build time, read-only in the editor and at `GET /api/version`.
732
798
  - This changelog and a semantic-versioning release process.
733
799
 
734
- [Unreleased]: https://github.com/kurtbruns/kestrel/compare/v1.19.0...HEAD
800
+ [Unreleased]: https://github.com/kurtbruns/kestrel/compare/v1.21.0...HEAD
801
+ [1.21.0]: https://github.com/kurtbruns/kestrel/releases/tag/v1.21.0
802
+ [1.20.0]: https://github.com/kurtbruns/kestrel/releases/tag/v1.20.0
735
803
  [1.19.0]: https://github.com/kurtbruns/kestrel/releases/tag/v1.19.0
736
804
  [1.18.0]: https://github.com/kurtbruns/kestrel/releases/tag/v1.18.0
737
805
  [1.17.0]: https://github.com/kurtbruns/kestrel/releases/tag/v1.17.0
package/bin/kestrel.mjs CHANGED
@@ -36,7 +36,7 @@ Usage: kestrel <command> [options]
36
36
 
37
37
  login <url> [--no-browser] sign in to a deployed instance as you, in your browser
38
38
  logout [url] sign out of it on this computer
39
- api <method> <path> [opts] call the API as you, via Claude Code (kestrel api for options)
39
+ api <method> <path> [opts] call the API as you (kestrel api for options)
40
40
  api --list | help <m> <path> list the instance's routes, or describe one
41
41
 
42
42
  help, --help show this
package/cli/seed.mjs CHANGED
@@ -22,6 +22,7 @@ import { readdir, readFile } from "node:fs/promises";
22
22
  import { dirname, join } from "node:path";
23
23
  import { fileURLToPath } from "node:url";
24
24
  import { devTarget, fail, parseArgs } from "../packages/kestrel/lib/dev-api.mjs";
25
+ import { fileMediaType } from "../packages/kestrel/lib/media-types.mjs";
25
26
 
26
27
  // The demo ships beside the CLI, in this repository and in the package.
27
28
  const root = join(dirname(fileURLToPath(import.meta.url)), "..");
@@ -43,13 +44,6 @@ export async function seedDemo(base, token, { size, seed } = {}, tag = "seed") {
43
44
  const url = `${base}/api/dev/seed${qs ? `?${qs}` : ""}`;
44
45
 
45
46
  const form = new FormData();
46
- const CONTENT_TYPE = {
47
- ".webp": "image/webp",
48
- ".jpg": "image/jpeg",
49
- ".jpeg": "image/jpeg",
50
- ".png": "image/png",
51
- ".gif": "image/gif",
52
- };
53
47
  // The images are the files in `demo/` that the demo refers to, so those files are the only
54
48
  // place to change them. The worker parses the posts and front matter for everything else.
55
49
  const demoDir = join(root, "demo");
@@ -65,7 +59,7 @@ export async function seedDemo(base, token, { size, seed } = {}, tag = "seed") {
65
59
  console.warn(`[${tag}] no demo/${rel} — seeding without the ${what}.`);
66
60
  return;
67
61
  }
68
- const type = CONTENT_TYPE[rel.slice(rel.lastIndexOf("."))] || "application/octet-stream";
62
+ const type = fileMediaType(rel);
69
63
  form.append(field, new Blob([await readFile(path)], { type }), name);
70
64
  };
71
65