@kurtbruns/kestrel 1.20.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.
package/CHANGELOG.md CHANGED
@@ -10,6 +10,41 @@ 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
+
13
48
  ## [1.20.0] - 2026-11-04
14
49
 
15
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.
@@ -762,7 +797,8 @@ The first tagged release: a self-contained newsletter app on a Cloudflare Worker
762
797
  - A build-version stamp: every instance reports its version, commit, and build time, read-only in the editor and at `GET /api/version`.
763
798
  - This changelog and a semantic-versioning release process.
764
799
 
765
- [Unreleased]: https://github.com/kurtbruns/kestrel/compare/v1.20.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
766
802
  [1.20.0]: https://github.com/kurtbruns/kestrel/releases/tag/v1.20.0
767
803
  [1.19.0]: https://github.com/kurtbruns/kestrel/releases/tag/v1.19.0
768
804
  [1.18.0]: https://github.com/kurtbruns/kestrel/releases/tag/v1.18.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