@kurtbruns/kestrel 1.6.0 → 1.8.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/.dev.vars.example +17 -24
- package/CHANGELOG.md +75 -1
- package/README.md +2 -2
- package/cli/dev-api.mjs +7 -1
- package/cli/dev.mjs +46 -1
- package/cli/init.mjs +6 -1
- package/dist/public/dashboard/app.js +234 -236
- package/dist/public/dashboard/app.js.map +4 -4
- package/dist/public/dashboard/index.html +6 -6
- package/dist/public/dashboard/styles.css +187 -177
- package/dist/worker/index.js +2118 -1998
- package/docs/README.md +3 -4
- package/docs/get-started/02-deploy.md +49 -18
- package/docs/get-started/03-access.md +4 -4
- package/docs/{guides/02-ses.md → get-started/04-ses.md} +75 -52
- package/docs/get-started/05-verify.md +8 -10
- package/docs/get-started/06-go-live.md +20 -7
- package/docs/guides/07-upgrade.md +2 -0
- package/docs/guides/08-try-locally.md +108 -0
- package/docs/reference/01-configuration.md +25 -14
- package/docs/reference/02-sending-domain-dns.md +19 -19
- package/migrations/0004_keyed_pending_to_resolve.sql +27 -0
- package/package.json +2 -2
- package/template/claude/CLAUDE.md +3 -1
- package/template/package.json +1 -0
- package/template/wrangler.jsonc +10 -6
- package/docs/get-started/01-overview.md +0 -47
- package/docs/get-started/04-resend.md +0 -134
package/.dev.vars.example
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
# Copy to `.dev.vars` (gitignored) for local development
|
|
2
|
-
#
|
|
1
|
+
# Copy to `.dev.vars` (gitignored) for local development: `npm run dev` reads it, and
|
|
2
|
+
# nothing in it is ever deployed. It holds only what local development uses. A deployed
|
|
3
|
+
# environment's secrets (Access, and the email provider's keys) are stored with
|
|
4
|
+
# `npx wrangler secret put NAME --env production` instead; the Configuration reference
|
|
5
|
+
# lists them: https://getkestrel.dev/docs/configuration/#secrets
|
|
6
|
+
# Restart `npm run dev` after a change.
|
|
3
7
|
|
|
4
8
|
# --- Admin API auth ---
|
|
5
9
|
# Local dev authenticates with a JWT signed by DEV_AUTH_SECRET (the app verifies it
|
|
@@ -10,23 +14,9 @@
|
|
|
10
14
|
# is the only gate.
|
|
11
15
|
DEV_AUTH_SECRET="dev-insecure-do-not-deploy"
|
|
12
16
|
|
|
13
|
-
# --- Cloudflare Access (JWT validation; the deployed gate — optional locally) ---
|
|
14
|
-
ACCESS_TEAM_DOMAIN="" # e.g. your-team.cloudflareaccess.com
|
|
15
|
-
ACCESS_AUD="" # the Access application's AUD tag
|
|
16
|
-
ACCESS_ALLOWED_EMAILS="" # optional CSV allowlist of human admin emails, e.g. you@example.com
|
|
17
|
-
|
|
18
|
-
# --- Amazon SES (SigV4 over HTTPS via aws4fetch) ---
|
|
19
|
-
AWS_ACCESS_KEY_ID=""
|
|
20
|
-
AWS_SECRET_ACCESS_KEY=""
|
|
21
|
-
SES_CONFIGURATION_SET="" # config set that publishes bounce/complaint to SNS
|
|
22
|
-
SNS_TOPIC_ARN="" # the topic the SES webhook accepts; every other topic is refused
|
|
23
|
-
|
|
24
|
-
# --- Resend (alternate transport) ---
|
|
25
|
-
RESEND_API_KEY=""
|
|
26
|
-
RESEND_WEBHOOK_SECRET="" # Svix signing secret for webhook verification
|
|
27
|
-
|
|
28
17
|
# --- Minimum lead ---
|
|
29
|
-
# How long every send stays visible and cancelable before it fires
|
|
18
|
+
# How long every send stays visible and cancelable before it fires
|
|
19
|
+
# (https://getkestrel.dev/docs/configuration/#min_lead_seconds).
|
|
30
20
|
# Deployed environments default to 300 (five minutes); local dev uses the one-minute floor
|
|
31
21
|
# that every environment shares, so a send scheduled a minute out behaves exactly as it
|
|
32
22
|
# would deployed. Not a secret: a deployed environment sets it in its `vars` in
|
|
@@ -39,12 +29,15 @@ MIN_LEAD_SECONDS="60"
|
|
|
39
29
|
# arrive after the send (delivered, bounced, complained), all through the real send loop.
|
|
40
30
|
# Test sends and confirmations go straight to the dev outbox (GET /api/dev/outbox),
|
|
41
31
|
# unsimulated. The profile names the provider modeled:
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
#
|
|
32
|
+
# ses Amazon SES: a second of its send rate a request (up to 50), one request a
|
|
33
|
+
# second, no key (a lost request waits for Resolve), and its daily quota running
|
|
34
|
+
# out once a send; a slice a minute on the default SUBREQUEST_BUDGET, as SES is
|
|
35
|
+
# on Workers Free
|
|
46
36
|
# 1 a generic provider paced to be watched: small, slow batches
|
|
47
37
|
# Add ":none" (e.g. "ses:none") for a clean run with no failures, or set "off" for the
|
|
48
|
-
# plain instant fake.
|
|
38
|
+
# plain instant fake. "not-connected" runs the app as a deployment with no email provider
|
|
39
|
+
# yet: every send refused, signups closed, a due send held. Switched back, the held send
|
|
40
|
+
# goes out at its next retry, a few minutes later.
|
|
41
|
+
# For one run, set it in the shell instead: SIMULATE_SENDS=ses npm run dev
|
|
49
42
|
# Only honored in a dev-shaped env (fake transport, no Access, APP_ORIGIN on localhost).
|
|
50
|
-
SIMULATE_SENDS="
|
|
43
|
+
SIMULATE_SENDS="ses"
|
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,78 @@ 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.8.0] - 2026-10-12
|
|
14
|
+
|
|
15
|
+
**Upgrading from 1.7.0.** Do these in order. First, if your `production` block sets `PROVIDER` to `resend`, switch it to SES on the release you run now, while no send is sending, as [Connect Amazon SES](docs/get-started/04-ses.md#switch-an-instance-that-already-sends) describes, and upgrade only after that: this release refuses `resend`, and until the switch the public pages say the newsletter is temporarily unavailable and nothing is sent. From the switch on, the app no longer records the bounces and complaints Resend reports, so if you can, let a few days pass after your last Resend send before you switch. Then delete the `RESEND_API_KEY` and `RESEND_WEBHOOK_SECRET` secrets and the Resend webhook. Second, if you send through SES, make the `Action` of your sending key's IAM policy (Connect Amazon SES's `kestrel-send-email`) `["ses:SendEmail", "ses:SendBulkEmail"]` before you deploy, where 1.7.0's guide gave only `ses:SendEmail`: this release sends through SES's bulk API, and until the key may call it every list send is held as SES refusing the credentials, while a test email still goes out, so a test won't show it's missing. You may also add **Rendering failures** to your configuration set's event destination, so the app can log one. Third, before you commit the upgrade, as they apply to you: if your `production` block sets `PROVIDER` to `fake` on a real hostname, this release refuses it: set your email provider, or make it `""` until you [connect one](docs/get-started/04-ses.md). A hosted demo that runs `fake` on a real hostname on purpose adds `ALLOW_FAKE_PROVIDER: "true"` before it upgrades, to the env it hands Kestrel: its `vars`, or the object it builds if it builds one, and that object's type. If your `production` block has no `PROVIDER` line, you may add `"PROVIDER": ""` to its `vars` to quiet wrangler's warning that it's missing there; it means the same as leaving it out. If sign-in to a deployed instance has been failing, store `ACCESS_TEAM_DOMAIN` again as the bare hostname (like `yourteam.cloudflareaccess.com`): a value this release still can't read as a hostname, such as one with a path or a port, is now refused, and until it's fixed the public pages say the newsletter is temporarily unavailable and nothing is sent, where before only sign-in failed. A local `.dev.vars` with `SIMULATE_SENDS` set to `resend` (or `resend:none`) changes it to `ses` (or `ses:none`). Fourth, one database migration, which `npm run sync-migrations` brings in before you commit the upgrade: apply it after noting a restore point and before you deploy, as [Upgrade to a new release](docs/guides/07-upgrade.md) does, with `npx wrangler d1 migrations apply DB --remote --env production` (and `--local` for local development). It moves any recipients a send left queued to be re-sent under an earlier provider's key into flight, where **Resolve** settles them, rather than leaving the send open and silent; on a database with none it changes nothing.
|
|
16
|
+
|
|
17
|
+
### Breaking
|
|
18
|
+
|
|
19
|
+
- Resend is no longer supported, so Amazon SES is the one email provider. `PROVIDER=resend` is refused, naming the step that switches to SES; `/webhooks/resend` is gone; and `SIMULATE_SENDS=resend` is refused in local development. Resend sells newsletters as Broadcasts, which would hand it the list and the record of who was mailed (SPEC §10). With no provider left that dedupes a repeated request, a batch whose answer never came always waits for Resolve; before, on Resend, it was sent again under its key (SPEC §12).
|
|
20
|
+
- Sending through SES needs `ses:SendBulkEmail` in the sending key's IAM policy, where `ses:SendEmail` was enough before. On a key without it, SES refuses every list send, and each send is held with nobody consumed, as a credentials refusal, until the policy allows it; the refusal's advice says to give the key both permissions or replace it (SPEC §12). Tests and confirmations still go out, so a test email won't reveal a missing permission.
|
|
21
|
+
- `PROVIDER` set to `fake` is now refused unless `APP_ORIGIN` is on `localhost` or `127.0.0.1`, naming `PROVIDER`, so the development value copied into production, which wrangler's warning about a var missing from `production` invites, shows at Deploy's check instead of opening signups whose confirmations go nowhere and recording sends no one receives. A hosted demo that runs `fake` on purpose opts in with `ALLOW_FAKE_PROVIDER` set to `true`, which no other provider reads (SPEC §9).
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- Local development can run as a deployment with no email provider connected: `SIMULATE_SENDS=not-connected` refuses every send, closes signups, and holds a due send, which goes out at its next retry once the switch is off, with local sign-in still on, so the editor's not-connected state can be seen without deploying. A deployed instance ignores it (SPEC §10).
|
|
26
|
+
- While no email provider is connected, the editor says so where sending would otherwise look possible: a draft's page notes it can be written and previewed but not sent, and a scheduled send's dashboard card and editor banner say it will be held when it fires. `kestrel dev` prints a line naming what `SIMULATE_SENDS` makes of the run, `not-connected` or a simulated provider, since wrangler shows the variable only as "(hidden)".
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- The setup guide walks through Amazon SES. Get started's step 3 is now Connect Amazon SES, which replaces Connect an email provider (Resend) and takes over the Use Amazon SES instead of Resend guide. The editor's links to the setup step, the dashboard's setup checklist, and the notification email name it and open it, and local development simulates SES by default. Connect Amazon SES says where to read your region's code and your account ID, walks through creating the sending policy in its own browser tab, shows the `production` vars as they end up, and gives the IAM user, configuration set, and SNS topic distinct names. It says to name the publication before requesting production access, since AWS may visit the site, and what to tell AWS if it asks for more detail. For an SNS subscription stuck pending, it puts Access covering the webhook first among the reasons, and has you choose **Request confirmation**, since SNS won't let you delete it. Go live first checks that SES production access came through and the send rate is raised from the sandbox's `1`, and says how fast a send goes out and when to raise the send rate or the Workers plan. The Sending-domain DNS reference speaks of SES's three checks, where SPF needs a record only with a custom MAIL FROM domain.
|
|
31
|
+
- SES now takes a batch a request through its bulk API: one second of your account's send rate, up to 50 recipients, one request a second. On Workers Free a minute sends 56 recipients at a send rate of 14 and 200 at 50 or more, where it sent about 18, so SES no longer needs Workers Paid, and the setup guide says to raise the account's send rate to 50 before changing plans. Each reader's email, unsubscribe link, and one-click headers are as before, and a test, a confirmation, or a notification goes through SES's single-send API with the same template, so it is the same email without queuing behind the bulk API's one request a second. A request SES never answers now holds the send for an outage's backoff, so an unreachable SES leaves one batch for Resolve rather than one every minute. Recipients SES holds back for a throttle or a spent quota go back to the queue without spending one of their attempts, and the send says why. A request lost in flight now leaves its whole batch for Resolve rather than one recipient, and an email SES took but couldn't build is logged as `webhook.render_failed` (SPEC §10, §12).
|
|
32
|
+
- A new instance's `production` settings declare `"PROVIDER": ""`, which means no provider connected, so wrangler no longer warns on every production command that `PROVIDER` is missing there. Connect Amazon SES now says to change it rather than add it.
|
|
33
|
+
- Resolve, the wedged-send notification, and Connect Amazon SES say that Kestrel can't tell whether a lost request's emails went out, so unless you know, Assume not sent is the safe default, and Assume sent is for when you know otherwise, such as a reader who says they got it. Before, they said to check the provider's console, which shows sent messages only with SES's Virtual Deliverability Manager turned on. The Resolve dialog also says Assume sent records the recipients as accepted by your provider, with delivery still waiting on a receipt, rather than as delivered (SPEC §12).
|
|
34
|
+
- The operator's Claude context (`.claude/CLAUDE.md` in a new instance) carries a new safety caution: `PROVIDER` stays `""` in production until Connect Amazon SES sets it to `ses`, and is never `fake`, whatever a wrangler warning suggests, so an instance's Claude doesn't "fix" the warning by copying the development value into production. `npm run check-context` reports the change; if you edited your `.claude/CLAUDE.md`, merge the line by hand.
|
|
35
|
+
- The operator's Claude context (`.claude/CLAUDE.md` in a new instance) carries a second new safety caution: until the publisher says AWS granted SES production access, don't help share the subscribe link or send to anyone but the addresses verified in SES, since the sandbox refuses every other recipient. `npm run check-context` reports the change; if you edited your `.claude/CLAUDE.md`, merge the line by hand.
|
|
36
|
+
- Deploy the app's step 3 creates the database and the image bucket with `--env production --binding … --update-config`, so Wrangler writes the new database's id into the `production` block itself, with no id to copy and no prompt to decline. The prompt's default, Yes, added a second binding to the production database in the development settings.
|
|
37
|
+
- A new instance installs `@types/node` as a dev dependency, so `npm run typecheck` no longer ends with Wrangler's "Action required: Install @types/node". The Node types stay out of the type check, so the Worker's own types still apply. An existing instance can silence the notice with `npm i -D @types/node`.
|
|
38
|
+
- The operator's Claude context now names `--env production` on every Wrangler command that creates or binds a resource, not only deploying and migrating, since without it Wrangler offers to bind the new resource in the development settings. It bears on safety: it keeps a binding to a production resource out of the development settings. `npm run check-context` reports the change; if you edited your `.claude/CLAUDE.md`, replace that line by hand.
|
|
39
|
+
- A send held because no email provider is connected reads once on the dashboard and the Sent page, one red message with the not-connected state instead of two, and its advice, there and on the send's page, links to Connect Amazon SES and says "then deploy" rather than "then deploy again". In the local not-connected preview it says to restart `npm run dev` without `SIMULATE_SENDS=not-connected`, which the settings API now reports as `deployment.notConnectedPreview`.
|
|
40
|
+
- The editor offers one Primary action per page: the dashboard's Quick actions New post and Settings' Edit template are now ordinary buttons, beside the page's own New post and the save bar's Save.
|
|
41
|
+
- The "Read-only · set at deploy" tag in Settings, and the note under it, are neutral grey rather than amber, since a value set at deploy is not a warning.
|
|
42
|
+
- The editor's wording uses typographic apostrophes and no em dashes, so the embeddable form's "We’ll email you a link" now matches the hosted subscribe page.
|
|
43
|
+
- When a template or publication change re-makes a scheduled email, the post's scheduled banner says its look changed after your last test while its content and send time stay the same, and the dashboard's scheduled card reads "Look changed since your last test", until you send a test of it. It is the same fact Claude sees, the `remade` condition, whose message now reads the same way, in place of a notice you could dismiss without testing (SPEC §8).
|
|
44
|
+
- A failed editor save stays on the save cue, in red with the reason, until a save succeeds, and the editor tries again on its own after a dropped connection or a server error (a refusal the server would repeat waits for your next edit), so it no longer disappears with a toast while the cue reads like ordinary typing. Errors read in plain words rather than codes like "internal_error" or the browser's "Failed to fetch", a test sent to several addresses names the ones that didn't get it and leaves only those in the dialog to try again, and a failed test notification is said once, in its status row.
|
|
45
|
+
- The Template test's answer names the addresses the provider didn't accept, as `failed`, beside `sent` and `total`, so a client can offer exactly those again and no one gets a second copy.
|
|
46
|
+
- The dashboard's Publication card names only the publication's home page, its address with Copy and View publication ↗, instead of the app and archive addresses side by side.
|
|
47
|
+
- Picking a send time inside the minimum lead says so under the picker, in plain words with Send now as the way to the soonest send, rather than showing the API's message; the Schedule dialog's Send now prompt says the soonest send is one lead out and still cancelable, instead of calling it skipping the review window. When the server refuses a value in a form (Settings, the editor's subject or slug, a send time, a subscriber's address), the field it names is marked and focused, and a refused Settings value names it in the error body's `field`, dotted for a nested one such as `notifications.to`.
|
|
48
|
+
- A spent SES quota's advice says to wait for SES's 24-hour quota to free up or ask AWS to raise it in the Service Quotas console, Settings → Email sender says the sender is set in `wrangler.jsonc` with only the AWS keys stored as secrets, and local development's stand-in reads "Local stand-in: nothing is delivered" rather than "No email provider configured", so it no longer reads like the not-connected state.
|
|
49
|
+
|
|
50
|
+
### Fixed
|
|
51
|
+
|
|
52
|
+
- An `ACCESS_TEAM_DOMAIN` stored with `https://` or a trailing slash no longer fails every sign-in with a `401` that says nothing of why: the app reads it as the bare hostname. A value that still isn't a hostname, such as one with a path or a port, is refused, naming `ACCESS_TEAM_DOMAIN`, like any other setting the app can't run on (SPEC §9).
|
|
53
|
+
- Before Access is set up, the deployed editor says it is locked until you set up Access, with a link to the guide, instead of saying your session expired; the API's 401 carries `reason: "access_not_configured"` in that state.
|
|
54
|
+
- A dialog or row-action menu no longer stays open over the next page after you navigate away, such as Add subscriber after pressing Back, where its confirm could act on something no longer on screen. Leaving a page closes what it opened; a write already started from a dialog still completes, and a template or identity save left waiting on its confirmation says it wasn't saved.
|
|
55
|
+
- `/health` no longer answers ok on a database that hasn't run every migration the release ships, as after a skipped Apply the schema step or a deploy made before its migration. It answers `500` `schema_behind` naming the missing migrations and the command that applies them, the editor says the same in place of the app, the public pages say the newsletter is temporarily unavailable, and nothing is sent until they're applied; your own `local_` migrations don't count. An error nothing handled on a public page now shows a plain error page rather than a raw JSON body (SPEC §5, §9).
|
|
56
|
+
- The Send now dialog counts who the send will reach, confirmed subscribers less suppressed addresses, rather than every confirmed subscriber (SPEC §6).
|
|
57
|
+
- `kestrel init` ends by pointing to the setup guide on getkestrel.dev, and a new instance's `.dev.vars.example` links the Configuration reference there, instead of paths that don't exist in an instance.
|
|
58
|
+
- Dialogs keep keyboard focus inside them, start it on their first control, and return it to whatever opened them when they close; the ⋯ row menus on Drafts and Subscribers and the Template page's Start from example menu open with focus on their first item and move with the arrow keys, Home, and End, and Escape closes them. The session-expired wall moves focus to its heading like the other walls, and stays put when more requests fail behind it. With one dialog over another, only the top one answers Tab and Escape, and a dialog closed by a click outside it hands focus back too.
|
|
59
|
+
- An error in a dialog, such as a test with no recipients, an address that isn't one, or a reschedule the server refused, shows inside the dialog, under the field at fault, instead of in a toast that sat dimmed behind it, and a toast raised while a dialog is open shows above it. Settings shows any failed save in the save bar, a logo that couldn't be saved says why under its tile, and a default test recipient that isn't an address, or is already in the list, says so under its field.
|
|
60
|
+
|
|
61
|
+
## [1.7.0] - 2026-10-12
|
|
62
|
+
|
|
63
|
+
**Upgrading from 1.6.0.** If your `production` block in `wrangler.jsonc` sets `PROVIDER` to `resend` or `ses`, nothing changes. If it still sets `fake` (as Get started's Deploy step set it until you connected a provider), delete that line before you deploy, so the instance runs with no provider connected rather than recording sends it never delivers, and [connect an email provider](docs/get-started/04-ses.md) when you're ready. If it sets no `PROVIDER` at all, the instance used to run on `fake` and now refuses to send; connect a provider first if it has readers. A send already scheduled on such an instance is held, with no one marked sent, until a provider is connected; it then goes out at its first retry after that, which may be long after its fire time, and it can't be canceled meanwhile, since it is already sending. A local development config that leaves `PROVIDER` unset needs `"PROVIDER": "fake"` in its top-level `vars`, as a new instance's has. If only one of the `ACCESS_TEAM_DOMAIN` and `ACCESS_AUD` secrets is stored (`npx wrangler secret list --env production` shows which), store the other before you deploy: this release refuses that configuration, and until both are stored its public pages say the newsletter is temporarily unavailable and nothing is sent. If any of `APP_ORIGIN`, `ARCHIVE_ORIGIN`, `MEDIA_PUBLIC_BASE`, `SENDING_DOMAIN`, or `FROM_ADDRESS` in your `production` block is still on `example.com` (the template's placeholder), replace it with your own before you deploy, whatever `PROVIDER` says: this release refuses it on every provider but `fake`, and until it's fixed the public pages say the newsletter is temporarily unavailable.
|
|
64
|
+
|
|
65
|
+
### Breaking
|
|
66
|
+
|
|
67
|
+
- An unset `PROVIDER` now means no email provider is connected, not the `fake` stand-in. Such a deployment runs, and refuses everything that would send with a 409 `provider_not_connected`: scheduling, send now, a test email, a test notification through the provider, and adding a subscriber; its public subscribe page says signups aren't open yet instead of taking an address it can't confirm. A send scheduled before is held, consuming no one, as a `refused` condition with the new cause `not_connected` and advice to connect a provider. `fake` now runs only where it is named, as local development and a demo do, and a new instance's production settings ship without `PROVIDER` until Get started's Connect an email provider step sets it (SPEC §9).
|
|
68
|
+
- A deployment with no email provider connected is now refused while any origin or sending setting is still on the template's `example.com` placeholder, as one with a real provider already was, so a missed edit shows at Deploy's check instead of at the provider step. Only `fake` may keep the example names (SPEC §9).
|
|
69
|
+
- A deployment with only one of `ACCESS_TEAM_DOMAIN` and `ACCESS_AUD` stored is now refused, naming the one that's missing, like any other configuration the app can't run on: its public pages say the newsletter is temporarily unavailable, and nothing is sent, until the other is stored. It used to keep serving readers and sending while the editor and Claude were locked out without saying why (SPEC §9).
|
|
70
|
+
|
|
71
|
+
### Added
|
|
72
|
+
|
|
73
|
+
- Reader pages, post pages included, show the publication's logo as the browser tab's icon when one is set; with no logo, the browser's default icon stays (SPEC §5).
|
|
74
|
+
- A setup guide, **Try Kestrel locally first**, runs a new instance locally with the demo publication, with no Cloudflare account or Git host, and shows how the folder becomes your instance's repository when you deploy.
|
|
75
|
+
|
|
76
|
+
### Changed
|
|
77
|
+
|
|
78
|
+
- A new instance's `.dev.vars.example` holds only what local development uses (the local sign-in secret, the minimum lead, and the send simulation). The blank Access and email-provider entries are gone: they did nothing locally, and setting an Access value there turned local sign-in off. A deployed instance's secrets are listed in the Configuration reference.
|
|
79
|
+
- The setup guide's Overview page is folded into Deploy the app: what Get started needs is now Deploy's Before you begin, followed by choosing your hostnames. Get started is five steps, and Connect Resend, now step 3, is renamed Connect an email provider: it still walks through Resend, and points to the SES guide.
|
|
80
|
+
- While the configuration is refused, the public pages readers see answer a plain "temporarily unavailable" page (503) that names no setting, instead of the JSON error naming it; the API, `/health`, and the webhooks still answer `invalid_config` with the variable. The editor shows one page saying what's wrong, with a link to that setting in the Configuration reference, instead of failing call by call (SPEC §9).
|
|
81
|
+
- `auth.mode` in `/api/whoami`, and `authMode` in the Settings deployment reflection, now say how the request signed in (through Access, or with the local dev token), rather than whether Access is configured.
|
|
82
|
+
- The dashboard's first-run checklist orders its steps by what each needs first: name the publication, connect a provider (or confirm the sending domain), write the first post, then share the subscribe link. Writing used to come second, and starting a draft ends the checklist, so the steps after it were lost.
|
|
83
|
+
- While no email provider is connected, the editor says so wherever a send would be refused, each time with a "Connect an email provider" link to the setup step: the dashboard's health block and the Sent page carry an amber warning that nothing can be sent yet, the first-run checklist asks you to connect a provider in place of confirming the sending domain and says signups open once one is, Settings' Email provider row reads "No email provider connected" and its Ways to subscribe says signups aren't open yet, and the Send-a-test, Schedule, and Add subscriber dialogs say so in amber before you commit, with their act disabled and nothing to fill in (no recipients, time picker, Send now, or address), until a provider is set. A send held for want of a provider says no provider is connected, on its page, in the dashboard's attention line, and in the notification, rather than that the provider is refusing the account. Local development keeps its "nothing is delivered" wording.
|
|
84
|
+
|
|
13
85
|
## [1.6.0] - 2026-10-11
|
|
14
86
|
|
|
15
87
|
**Upgrading from 1.5.0.** One database migration: run `npm run sync-migrations`, then `npx wrangler d1 migrations apply DB --remote --env production` (and `--local` for local development) before deploying. Existing posts keep their slugs and read as set by hand; tick *Auto-generate from subject* on a draft to have its slug follow the subject again. On npm 12, to end `npm install` without the blocked-install-scripts warning, add this to your `package.json` (a new instance has it already): `"allowScripts": { "esbuild": true, "fsevents": true, "workerd": true }`.
|
|
@@ -287,7 +359,9 @@ The first tagged release: a self-contained newsletter app on a Cloudflare Worker
|
|
|
287
359
|
- A build-version stamp: every instance reports its version, commit, and build time, read-only in the editor and at `GET /api/version`.
|
|
288
360
|
- This changelog and a semantic-versioning release process.
|
|
289
361
|
|
|
290
|
-
[Unreleased]: https://github.com/kurtbruns/kestrel/compare/v1.
|
|
362
|
+
[Unreleased]: https://github.com/kurtbruns/kestrel/compare/v1.8.0...HEAD
|
|
363
|
+
[1.8.0]: https://github.com/kurtbruns/kestrel/releases/tag/v1.8.0
|
|
364
|
+
[1.7.0]: https://github.com/kurtbruns/kestrel/releases/tag/v1.7.0
|
|
291
365
|
[1.6.0]: https://github.com/kurtbruns/kestrel/releases/tag/v1.6.0
|
|
292
366
|
[1.5.0]: https://github.com/kurtbruns/kestrel/releases/tag/v1.5.0
|
|
293
367
|
[1.4.0]: https://github.com/kurtbruns/kestrel/releases/tag/v1.4.0
|
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ Kestrel installs from npm as [`@kurtbruns/kestrel`](https://www.npmjs.com/packag
|
|
|
12
12
|
npx @kurtbruns/kestrel init
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
[Get started](docs/get-started
|
|
15
|
+
To look around before you deploy, [Try Kestrel locally first](docs/guides/08-try-locally.md) runs it with demo content and no Cloudflare account. [Get started](docs/README.md#get-started) takes you from an empty repository to a live newsletter, and [Upgrade to a new release](docs/guides/07-upgrade.md) moves an instance to a newer one. The same guide is in the editor's **Docs** tab.
|
|
16
16
|
|
|
17
17
|
## Work on Kestrel
|
|
18
18
|
|
|
@@ -24,7 +24,7 @@ cp .dev.vars.example .dev.vars
|
|
|
24
24
|
npm run dev
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
Open the editor at http://localhost:8787/dashboard/. There's nothing to sign in with locally, and email goes to a stand-in that never reaches a real inbox. `.dev.vars.example` explains each local setting, including how the stand-in mimics
|
|
27
|
+
Open the editor at http://localhost:8787/dashboard/. There's nothing to sign in with locally, and email goes to a stand-in that never reaches a real inbox. `.dev.vars.example` explains each local setting, including how the stand-in mimics SES on a send.
|
|
28
28
|
|
|
29
29
|
### Demo data
|
|
30
30
|
|
package/cli/dev-api.mjs
CHANGED
|
@@ -137,7 +137,13 @@ async function devToken(base, tag) {
|
|
|
137
137
|
);
|
|
138
138
|
}
|
|
139
139
|
if (!res.ok) {
|
|
140
|
-
|
|
140
|
+
// A refused deployment (a setting, or a database behind the release) says what to do.
|
|
141
|
+
const { message } = await res.json().catch(() => ({}));
|
|
142
|
+
fail(
|
|
143
|
+
tag,
|
|
144
|
+
`could not mint a dev token: ${res.status} ${res.statusText}`,
|
|
145
|
+
...(typeof message === "string" ? [message] : []),
|
|
146
|
+
);
|
|
141
147
|
}
|
|
142
148
|
const { token, runId } = await res.json();
|
|
143
149
|
return { token, runId: typeof runId === "string" && runId ? runId : undefined };
|
package/cli/dev.mjs
CHANGED
|
@@ -25,7 +25,9 @@
|
|
|
25
25
|
* Per-run settings. `SIMULATE_SENDS`, `MIN_LEAD_SECONDS`, and `SUBREQUEST_BUDGET` set in the
|
|
26
26
|
* shell are forwarded over `.dev.vars`, so `SIMULATE_SENDS=ses kestrel dev` picks a
|
|
27
27
|
* simulation profile for one run without editing a file. A `.dev.vars` missing a setting that
|
|
28
|
-
* `.dev.vars.example` gives a value is named, rather than left to differ quietly.
|
|
28
|
+
* `.dev.vars.example` gives a value is named, rather than left to differ quietly. What
|
|
29
|
+
* `SIMULATE_SENDS` makes of this run (a simulated provider, or not connected) is said in one
|
|
30
|
+
* line, since wrangler prints the variable only as "(hidden)".
|
|
29
31
|
*
|
|
30
32
|
* Arguments after the command go to `wrangler dev` (`--remote`, for instance), except `--port`,
|
|
31
33
|
* which is refused in favor of PORT. This repository's `npm run dev` (scripts/dev.mjs) adds
|
|
@@ -119,6 +121,45 @@ function checkDevVars(fromShell) {
|
|
|
119
121
|
}
|
|
120
122
|
}
|
|
121
123
|
|
|
124
|
+
/**
|
|
125
|
+
* The line that says what stands in for a provider this run, from `SIMULATE_SENDS` as the
|
|
126
|
+
* Worker will read it (the shell's value over `.dev.vars`'s), or null for the plain fake.
|
|
127
|
+
* Wrangler prints the variable as "(hidden)", so without this a forgotten `not-connected`
|
|
128
|
+
* in the shell looks like a broken app. The values mirror the Worker's reading of the
|
|
129
|
+
* variable (src/env.ts), which stays the authority: a value it would refuse is named as such
|
|
130
|
+
* here rather than called a simulation.
|
|
131
|
+
*/
|
|
132
|
+
export function simulationLine(value) {
|
|
133
|
+
const v = (value ?? "").trim().toLowerCase();
|
|
134
|
+
if (["", "0", "off", "false", "no"].includes(v)) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
if (v === "not-connected") {
|
|
138
|
+
return "[dev] running not connected: sends, tests and signups are refused (unset SIMULATE_SENDS to send)";
|
|
139
|
+
}
|
|
140
|
+
const [name = "", faults = "realistic", ...rest] = v.split(":");
|
|
141
|
+
const known =
|
|
142
|
+
["generic", "ses", "1", "on", "true", "yes"].includes(name) &&
|
|
143
|
+
["realistic", "none"].includes(faults) &&
|
|
144
|
+
rest.length === 0;
|
|
145
|
+
return known
|
|
146
|
+
? `[dev] simulating a provider for list sends (SIMULATE_SENDS=${v}); nothing is delivered`
|
|
147
|
+
: `[dev] SIMULATE_SENDS=${value.trim()} isn't a value the app accepts, so it will refuse to run: use off, ses, or generic (optionally with :none), or not-connected`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* SIMULATE_SENDS as this run will have it: the shell's value when it sets a non-empty one
|
|
152
|
+
* (`off` included, which overrides `.dev.vars` like any other), else `.dev.vars`'s in `dir`.
|
|
153
|
+
* An empty shell value falls through, as it is not forwarded to wrangler (see `devCommand`).
|
|
154
|
+
*/
|
|
155
|
+
export function effectiveSimulateSends(shell, dir) {
|
|
156
|
+
if (shell?.trim()) {
|
|
157
|
+
return shell;
|
|
158
|
+
}
|
|
159
|
+
const local = join(dir, ".dev.vars");
|
|
160
|
+
return existsSync(local) ? parseEnv(readFileSync(local, "utf8")).SIMULATE_SENDS : undefined;
|
|
161
|
+
}
|
|
162
|
+
|
|
122
163
|
/**
|
|
123
164
|
* `kestrel dev [wrangler dev args]`. `onStop` runs when the server stops, for whatever the
|
|
124
165
|
* caller started beside it.
|
|
@@ -175,6 +216,10 @@ export async function devCommand(argv, { onStop = () => {} } = {}) {
|
|
|
175
216
|
|
|
176
217
|
if (!isRemote) {
|
|
177
218
|
checkDevVars(fromShell);
|
|
219
|
+
const line = simulationLine(effectiveSimulateSends(process.env.SIMULATE_SENDS, process.cwd()));
|
|
220
|
+
if (line) {
|
|
221
|
+
console.log(line);
|
|
222
|
+
}
|
|
178
223
|
}
|
|
179
224
|
|
|
180
225
|
// The Worker reports this ID beside the dev token, and seed and reset with no target given
|
package/cli/init.mjs
CHANGED
|
@@ -88,9 +88,13 @@ export function initCommand(argv) {
|
|
|
88
88
|
copyFileSync(from, join(root, to));
|
|
89
89
|
}
|
|
90
90
|
// The instance pins this exact release, and the tools at the versions it was tested with.
|
|
91
|
+
// @types/node is there only so `wrangler types` finds Node types installed (the Worker has
|
|
92
|
+
// nodejs_compat) and stops ending the typecheck with "Action required"; the scaffold's
|
|
93
|
+
// tsconfig lists no `types`, so they never enter the program and the Worker's types win.
|
|
91
94
|
const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
92
95
|
pkg.name = packageName(root);
|
|
93
96
|
pkg.dependencies[PACKAGE.name] = PACKAGE.version;
|
|
97
|
+
pkg.devDependencies["@types/node"] = PACKAGE.devDependencies["@types/node"];
|
|
94
98
|
pkg.devDependencies.typescript = PACKAGE.devDependencies.typescript;
|
|
95
99
|
pkg.devDependencies.wrangler = PACKAGE.peerDependencies.wrangler;
|
|
96
100
|
writeFileSync(join(root, "package.json"), `${JSON.stringify(pkg, null, 2)}\n`);
|
|
@@ -109,7 +113,8 @@ export function initCommand(argv) {
|
|
|
109
113
|
" npm install",
|
|
110
114
|
" cp .dev.vars.example .dev.vars",
|
|
111
115
|
" npm run dev",
|
|
112
|
-
"Then commit, and follow the setup guide
|
|
116
|
+
"Then commit, and follow the setup guide to deploy: https://getkestrel.dev/docs/",
|
|
117
|
+
"(offline: node_modules/@kurtbruns/kestrel/docs/README.md)",
|
|
113
118
|
].join("\n"),
|
|
114
119
|
);
|
|
115
120
|
}
|