@kurtbruns/kestrel 1.17.0 → 1.18.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 (35) hide show
  1. package/.dev.vars.example +10 -0
  2. package/CHANGELOG.md +30 -1
  3. package/bin/kestrel.mjs +1 -1
  4. package/cli/dev.mjs +3 -3
  5. package/cli/init.mjs +98 -4
  6. package/dist/public/dashboard/app.js +288 -286
  7. package/dist/public/dashboard/app.js.map +4 -4
  8. package/dist/public/dashboard/index.html +6 -3
  9. package/dist/public/dashboard/styles.css +10 -0
  10. package/dist/worker/index.js +5606 -5095
  11. package/docs/README.md +5 -3
  12. package/docs/get-started/02-deploy.md +24 -20
  13. package/docs/get-started/03-access.md +9 -5
  14. package/docs/get-started/04-aws-account.md +68 -0
  15. package/docs/get-started/{04-ses.md → 05-ses.md} +89 -92
  16. package/docs/get-started/06-production-access.md +64 -0
  17. package/docs/get-started/{05-verify.md → 07-verify.md} +16 -26
  18. package/docs/get-started/{06-go-live.md → 08-go-live.md} +2 -2
  19. package/docs/guides/01-allow-claude.md +1 -1
  20. package/docs/guides/02-automate.md +1 -1
  21. package/docs/guides/03-notifications.md +3 -3
  22. package/docs/guides/07-upgrade.md +4 -4
  23. package/docs/guides/09-as-your-list-grows.md +8 -18
  24. package/docs/publishing/02-sending-rules.md +2 -2
  25. package/docs/publishing/03-name-your-publication.md +2 -0
  26. package/docs/publishing/04-write-a-post.md +1 -1
  27. package/docs/publishing/06-troubleshooting.md +11 -7
  28. package/docs/publishing/09-share-your-subscribe-link.md +1 -1
  29. package/docs/publishing/10-check-your-setup.md +4 -2
  30. package/docs/reference/01-configuration.md +14 -12
  31. package/docs/reference/02-sending-domain-dns.md +4 -4
  32. package/migrations/0015_provider_account.sql +18 -0
  33. package/package.json +1 -1
  34. package/template/claude/CLAUDE.md +3 -3
  35. package/template/wrangler.jsonc +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kurtbruns/kestrel",
3
- "version": "1.17.0",
3
+ "version": "1.18.0",
4
4
  "type": "module",
5
5
  "description": "An open-source, self-hosted newsletter app where you own the list and the archive. Work in the editor, or just ask Claude. Runs on Cloudflare Workers.",
6
6
  "license": "MIT",
@@ -14,9 +14,9 @@ Deploying, migrating the production database, and anything that sends mail or ch
14
14
 
15
15
  ## Running the instance
16
16
 
17
- - **Follow the guide page, step by step.** Each page is a complete procedure. Do what a step says, ask the person for what only they can do (a dashboard click, a DNS record at their registrar), and end with the page's **Check it** list, reporting each check's result.
17
+ - **Follow the guide page, step by step.** Each page is a complete procedure. Do what a step says, ask the person for what only they can do (a dashboard click, a DNS record at their registrar), and run each check the page gives, reporting each one's result: the step that ends a section, and the page's **Check it** list where it has one.
18
18
  - **Deploying, migrating, and creating or binding a resource are plain wrangler,** always naming the environment, since the top level is development: `npx wrangler deploy --env production`, `npx wrangler d1 migrations apply DB --remote --env production`, and `npx wrangler d1 create NAME --env production --binding DB --update-config`, which writes the binding into `production` instead of offering to add it to the top level.
19
- - **Never deploy or store a secret without `--env production`.** Without it, wrangler acts on the top level: a deploy creates a second Worker, named `kestrel`, running the stand-in transport and the local sign-in, and a secret lands on that Worker instead of production. The top level turns off every address that Worker could answer on and its send schedule; keep `workers_dev` and `preview_urls` there `false` and its `crons` present and empty (deleting the line leaves an earlier schedule running), and never add a route there. If a deploy went out without `--env`, tell the person, and remove it with `npx wrangler delete --name kestrel` once they agree.
19
+ - **Never deploy or store a secret without `--env production`.** Without it, wrangler acts on the top level: a deploy creates a second Worker, named by the top-level `name` in `wrangler.jsonc`, running the stand-in transport and the local sign-in, and a secret lands on that Worker instead of production. The top level turns off every address that Worker could answer on and its send schedule; keep `workers_dev` and `preview_urls` there `false` and its `crons` present and empty (deleting the line leaves an earlier schedule running), and never add a route there. If a deploy went out without `--env`, tell the person, and remove it with `npx wrangler delete --name` and that top-level name once they agree.
20
20
  - **Upgrade with the `/upgrade` skill.** It follows the guide's upgrade page.
21
21
  - **Bring a list from another service with the `/migrate-list` skill.** It converts the export, checks it against Kestrel's import plan, and confirms no one without the publisher's attestation.
22
22
  - **Secrets never pass through you.** The person enters each one themselves with `npx wrangler secret put NAME --env production`, which prompts for the value; never ask for a secret in chat. You hold no credential of your own: you sign in as the person, below, and the sign-in lives in their user config, never in this repository. Never write a secret into `wrangler.jsonc`, `.claude/settings.json`, or a commit.
@@ -38,7 +38,7 @@ Deploying, migrating the production database, and anything that sends mail or ch
38
38
  - **Re-make a scheduled send only on the publisher's word.** Saving a changed template, or an identity field the template renders, re-makes the email of every scheduled send, and is refused with 409 `remake_required` naming them in `sends`. Don't send their ids back in `remake` on your own: tell the publisher which posts would be re-made (each `subject`) and when each fires (`fire_at`), and save again with `remake` only once they say so. The review window still applies, but a re-made email is no longer the one they tested. Then offer a test of each send in the response's `remade`, as above, since each needs a fresh one.
39
39
  - **Say where a post goes before it's scheduled or sent.** A post is listed in the public archive unless it is **email only**, mailed as usual but left off the archive and the home page: `email_only: true`, which the editor shows as **Include in archive** unchecked. Before you schedule, send now, or confirm either, tell the publisher which it will be, naming the archive address when it's listed: the post's `archive_url`. Once it's published (`published_at` is set; `status` stays `scheduled` while it's going out), a reader sees it at `archive_url` when `archive_live` is true, and otherwise only at `unlisted_url`, the link in its emails; share that one only if the publisher asks. Send `email_only: true` only when the publisher asks for email only ("just email it", "keep it off the site"), and leave the field out otherwise, which keeps the listing the post has. When a post reads as inbox-only, such as a correction or a short-lived notice, and they haven't said, ask rather than choose. A test email puts nothing in the archive.
40
40
  - **Change a post's listing or address only when the publisher asks.** Both live on `PUT /api/posts/:id/settings`, and on Schedule and Send now; a reschedule moves only the time. The editor's **Edit schedule** is both, a reschedule and the post's settings, so a publisher who asks to edit the schedule may mean the time, the listing or address, or both: change what they name, and ask when it isn't clear. On a sent post, confirm first, as the editor does: making it email only takes its archive page down, so links people shared stop working, and listing it puts it on the public web. A sent post's address is fixed (`slug_permanent`). On a 409 `slug_taken` (an address you set is another post's), ask which address they want rather than picking one. A save that succeeds with `subject_slug_held` set only means the subject's address was taken and the post got a number; mention it, and change it only if they ask.
41
- - **Until AWS has granted SES production access, never help share the subscribe link or send to anyone but the addresses verified in SES.** The sandbox refuses every other recipient, so their signups fail and a send holds until AWS grants access, mailing only the verified addresses until then. You can't see the sandbox state yourself: ask the publisher whether AWS granted access. Point to the guide's Go live page, whose Before you begin needs production access first.
41
+ - **Until AWS has granted SES production access, never help share the subscribe link or send to anyone but the addresses verified in SES.** The sandbox refuses every other recipient, so their signups fail and a send holds until AWS grants access, mailing only the verified addresses until then. Read `state` from `GET /api/provider`: `sandbox` means not yet, and `review` says whether AWS is reviewing a request (`pending`) or denied it; `production` means granted. Only when it's `unknown`, ask the publisher, and if `unknown_reason` is `permission`, suggest allowing the AWS key `ses:GetAccount` and `ses:GetEmailIdentity`, as Connect Amazon SES's Create the app's AWS key section shows. Its `warnings` are worth telling the publisher, in their words. To ask for it, or to check the grant, point to the guide's Request production access page: its Check that AWS granted it section covers the grant.
42
42
  - **Never add a subscriber the publisher didn't name,** with one exception: an import the publisher asked for, run through the `/migrate-list` skill and the import plan, and confirming anyone only with their attestation. Treat an unsubscribe as final: only the reader undoes it, by subscribing again. Someone whose `unsubscribe_cause` is `complaint` or `erased` can't be added at all: `POST /api/subscribers` refuses them with `signup_required`, and you relay that they have to subscribe through the subscribe page themselves.
43
43
  - **Pick the act that records whose decision it was.** Unsubscribe someone only when they asked to leave; when the publisher decides themselves to stop mailing an address, block it (`POST /api/suppressions`) instead. The reference says what each act and each suppression `reason` means, in the words the editor shows the publisher. Unblock or clear only an address the publisher names, through `POST /api/suppressions/lift` with the `reason` the suppression shows; on `suppression_changed`, tell the publisher what stands now and ask again. Never clear a bounce unless the publisher says the problem is gone, and confirm first: it lets mail go to an address that failed. Only a block or a bounce is lifted; a complaint or an erasure is how someone left, not a suppression, and nothing lifts it.
44
44
  - **Erase a subscriber only when the publisher names them and asks for it, and confirm first:** an erasure can't be undone. It unsubscribes them and strips their record to the address, and only their own sign-up brings them back.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "./node_modules/wrangler/config-schema.json",
3
- "name": "kestrel",
3
+ "name": "kestrel-dev",
4
4
  // The Worker is Kestrel's: src/index.ts re-exports it from the release this repository pins.
5
5
  "main": "src/index.ts",
6
6
  // Matches the release's own config. An upgrade's changelog says when to change it.