@kurtbruns/kestrel 1.19.0 → 1.20.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.
@@ -195,7 +195,7 @@ npx wrangler delete --name newsletter-dev
195
195
  {"status":"ok","service":"kestrel"}
196
196
  ```
197
197
 
198
- If `curl` reports a certificate error, or says it could not resolve the host, Cloudflare is still setting up the new hostname. Try again in a few minutes.
198
+ If `curl` reports a certificate error or can't resolve the host, Cloudflare is still setting up the hostname. Or your computer remembers it from before it existed. Try again over the next 30 minutes.
199
199
 
200
200
  1. The editor is locked. Until you set up Access in the next step, the app can't tell who is asking, so it lets no one in:
201
201
 
@@ -37,7 +37,7 @@ The editor lives under `/dashboard`, and the API under `/api`. One application c
37
37
 
38
38
  Each path covers everything under it. Leave every other path out. The home page, the archive, subscribing, unsubscribing, images, and your email provider's webhooks must stay public.
39
39
 
40
- 1. Add only these two public hostnames. If the form also shows an empty private hostname or IP row, remove it, or saving fails with an error naming `use_clientless_isolation_app_launcher_url`.
40
+ 1. Add only these two public hostnames. If the form also shows an empty private hostname or IP row, remove it. Otherwise saving fails with an error naming `use_clientless_isolation_app_launcher_url`, saying it needs private destinations.
41
41
 
42
42
  1. Name the application after your app's hostname, `newsletter.example.com`. Cloudflare suggests a name made from the subdomain, which won't tell two newsletters apart.
43
43
 
@@ -55,7 +55,7 @@ Every Access application denies everyone until a policy allows them.
55
55
 
56
56
  1. Select **Create**.
57
57
 
58
- Cloudflare's own login is on by default, and admits members of your Cloudflare account, so the email in the policy you made should be one. To let someone sign in another way, such as a one-time code by email, add it under **Zero Trust → Integrations → Identity providers**.
58
+ Cloudflare's own login is on by default, and admits members of your Cloudflare account, so the email in the policy you made should be one.
59
59
 
60
60
  ## 4. Give the app its two values
61
61
 
@@ -93,8 +93,6 @@ The app verifies each login against your team domain, and checks that it was iss
93
93
 
94
94
  Spaces and capital letters don't matter. Without the list, the app lets in anyone your Access policy allows.
95
95
 
96
- To let someone else into the editor, such as the publisher or a co-editor, add their email address to the policy from [Allow yourself in](#3-allow-yourself-in). If you set `ACCESS_ALLOWED_EMAILS`, run the command again and enter the whole list with their address added, since the new value replaces the old one.
97
-
98
96
  ## Check it
99
97
 
100
98
  1. In a private browser window, open `https://newsletter.example.com/dashboard/`. Cloudflare asks you to sign in.
@@ -123,4 +121,10 @@ If a check fails:
123
121
  - **Cloudflare lets you in, but the editor answers `401`.** Check the two secrets for a typo, and that `ACCESS_ALLOWED_EMAILS`, if you set it, includes your address. The same goes for anyone else you let in.
124
122
  - **The editor says Access let you in but didn't say who you are.** Your login carried no email, which happens through a kind of policy the app doesn't accept, such as a device check. Sign out, and sign in through the policy you made above.
125
123
 
124
+ ## Allow someone else in
125
+
126
+ To let someone else into the editor, such as the publisher or a co-editor, add their email address to the policy from [Allow yourself in](#3-allow-yourself-in). If you set `ACCESS_ALLOWED_EMAILS`, run the command again and enter the whole list with their address added, since the new value replaces the old one.
127
+
128
+ If they don't have a Cloudflare account, add another way to sign in, such as a one-time code by email, under **Zero Trust → Integrations → Identity providers**.
129
+
126
130
  Next, [create an AWS account](04-aws-account.md).
@@ -65,4 +65,6 @@ AWS charges for what you use, with no spending limit, so a mistake or a leaked k
65
65
 
66
66
  1. Under **Budgets**, your budget is listed, and its email recipient shows as **Active**.
67
67
 
68
+ If it shows as **Pending**, select **Resend verification** on its row, and verify the address as section 4 describes.
69
+
68
70
  Next, [connect Amazon SES](05-ses.md).
@@ -47,6 +47,8 @@ The same form sets a custom MAIL FROM domain, so SPF also passes for your domain
47
47
 
48
48
  1. Wait until SES shows **Identity status** as **Verified**, and both **DKIM configuration** and **MAIL FROM configuration** as **Successful**.
49
49
 
50
+ If they stay pending, check that each record's proxy status is **DNS only**.
51
+
50
52
  ## 2. Add a DMARC record
51
53
 
52
54
  DMARC tells inboxes what to do with mail that fails its checks, and sends you reports about it. SES doesn't add this record, so you add it yourself. It starts in monitor mode, which reports without blocking anything.
@@ -17,7 +17,7 @@ A test email goes through the same code as a real send. If the test looks right,
17
17
 
18
18
  1. In the editor at `https://newsletter.example.com/dashboard/`, create a post with a subject, a short body, and an image.
19
19
 
20
- 1. Select **Send test email**, enter your address, and send it.
20
+ 1. Select **Send test email**, enter your address, and send it. If SES refuses it, the test's line says why and what fixes it.
21
21
 
22
22
  1. Open the email. It arrives in your inbox, not in spam, the image loads, and the subject reads as you wrote it. Its links work, but a test email unsubscribes no one; section 5 unsubscribes for real.
23
23
 
@@ -45,7 +45,7 @@ Kestrel emails each notifications address when a send finishes, and right away w
45
45
 
46
46
  1. In the editor, open **Settings → Notifications**, type that address under **Send notifications to**, select **Add inbox**, and then **Save**.
47
47
 
48
- 1. Select **Send a test notification**. It arrives at that address.
48
+ 1. Select **Send a test notification**. It arrives at that address. If it doesn't, **Last notification**, on the same page, says what happened.
49
49
 
50
50
  The publisher sets their own address in [Check your setup](../publishing/10-check-your-setup.md).
51
51
 
@@ -65,11 +65,13 @@ This is a real send, to a list of one. It proves the whole path: subscribing, co
65
65
 
66
66
  1. When the send finishes, the post arrives at your second address. A notification arrives at your first, with the subject `Sent:` and the post's title.
67
67
 
68
+ If the send shows in red under **Sent**, its line names the fix. Once that's fixed, the send resumes on its own.
69
+
68
70
  ## 5. Unsubscribe
69
71
 
70
- Every email carries an unsubscribe link, and a header that lets mail clients show their own **Unsubscribe** button. Both work in one step, with no login.
72
+ Every email carries an unsubscribe link, and a header that lets mail clients show their own **Unsubscribe** button. The button works in one step, and the link asks you to confirm first. Neither needs a login.
71
73
 
72
- 1. Open the post from section 4 in your mail client. It shows an **Unsubscribe** button next to the sender.
74
+ 1. Open the post from section 4 in your mail client. It shows an **Unsubscribe** button next to the sender. If your client shows none, use the **Unsubscribe** link in the email's footer, which asks you to confirm on a page.
73
75
 
74
76
  1. Select it. In the editor, **Subscribers** shows the address as **Unsubscribed**, and later sends leave it out.
75
77
 
@@ -94,7 +96,7 @@ The app writes a log line for each thing it does, and Cloudflare keeps them for
94
96
 
95
97
  1. In the Cloudflare dashboard, go to **Workers & Pages**, select your Worker, the `name` in the `production` block of `wrangler.jsonc`, and select **Observability**.
96
98
 
97
- 1. Filter on `event` equal to `sweep.tick`. A line arrives every minute.
99
+ 1. Filter on `event` equal to `sweep.tick`. A line arrives every minute. If none arrive, the schedule probably isn't running, and deploying again with `--env production` registers it.
98
100
 
99
101
  1. Filter on `event` equal to `send.completed`. Your send from section 4 is there.
100
102
 
@@ -1,28 +1,22 @@
1
1
  # Go live
2
2
 
3
- At this point, your instance should be deployed and verified. Before anyone subscribes, check that AWS granted production access, as [Check that AWS granted it](06-production-access.md#4-check-that-aws-granted-it) describes, so SES can send to anyone. From here, the publisher can set up the publication, and you can continue with the guides below.
3
+ At this point, your instance should be deployed and verified. Before anyone subscribes, [check that AWS granted production access](06-production-access.md#4-check-that-aws-granted-it), so SES can send to anyone. From here, where you go next depends on who's writing the newsletter.
4
4
 
5
5
  ## Start publishing
6
6
 
7
- The rest happens in the editor, at `https://newsletter.example.com/dashboard/`. The Publishing docs walk through it in the order of the dashboard's setup checklist, starting with [Name your publication](../publishing/03-name-your-publication.md).
7
+ Open the editor at `https://newsletter.example.com/dashboard/`. The dashboard's setup steps show what's left, and [Publishing](../publishing/README.md) covers each one.
8
8
 
9
- If someone else is the publisher, let them sign in first:
9
+ ## Hand off to someone else
10
10
 
11
- 1. Add their email address to the policy on the Access application that lets you in, and to `ACCESS_ALLOWED_EMAILS` if you set it, as in [Give the app its two values](03-access.md#4-give-the-app-its-two-values).
11
+ [Let them into the editor](03-access.md#allow-someone-else-in), and add their address beside yours under **Settings → Notifications**, so a problem reaches both of you. Then send them the editor's address and a link to [Publishing](../publishing/README.md). The dashboard's setup steps show them what's left.
12
12
 
13
- 1. If they don't have a Cloudflare account, add another way to sign in, such as a one-time code by email. It's under **Zero Trust → Integrations → Identity providers**.
13
+ ## Optional guides
14
14
 
15
- 1. Add their address under **Settings → Notifications** beside yours, so a problem reaches both of you.
15
+ Each one adds one thing, and you can come back to any of them later.
16
16
 
17
- 1. Send them the editor's address and a link to [Name your publication](../publishing/03-name-your-publication.md).
18
-
19
- ## Optional next steps
20
-
21
- Each of these guides adds one thing, and you can come back to any of them later.
22
-
23
- - [Allow Claude to connect](../guides/01-allow-claude.md), so the publisher can connect Claude Desktop, claude.ai, or Claude Code, and have it draft, proofread, and schedule posts as them, through the same API the editor uses.
24
- - [Automate with a service token](../guides/02-automate.md), so a script can call the API with a service token, when no one is signed in.
25
- - [Notifications through Cloudflare's email](../guides/03-notifications.md), so problems are reported even when the email provider has stopped sending.
26
- - [Rate-limit the subscribe form](../guides/05-rate-limit.md), before the subscribe link is shared widely.
27
- - [As your list grows](../guides/09-as-your-list-grows.md), from about 1,000 subscribers.
28
- - [Upgrade to a new release](../guides/07-upgrade.md), when one comes out.
17
+ - Before you share your subscribe link: [Rate-limit the subscribe form](../guides/05-rate-limit.md)
18
+ - To hear about a problem even when SES has stopped sending: [Notifications through Cloudflare's email](../guides/03-notifications.md)
19
+ - To have Claude draft, proofread, and schedule posts: [Allow Claude to connect](../guides/01-allow-claude.md)
20
+ - To run scripts with no one signed in: [Automate with a service token](../guides/02-automate.md)
21
+ - To see when your list needs Workers Paid or higher SES limits: [As your list grows](../guides/09-as-your-list-grows.md)
22
+ - To move to a new release: [Upgrade to a new release](../guides/07-upgrade.md)
@@ -43,7 +43,7 @@ Managed OAuth lets an app such as Claude act with a person's account, the way th
43
43
  curl -si https://newsletter.example.com/api/whoami
44
44
  ```
45
45
 
46
- You should see `401`, with a `www-authenticate` line that starts with `Bearer`. Access can take a few minutes more to start accepting sign-ins. Until it does, signing in shows `invalid_target`; wait, then try again.
46
+ You should see `401`, with a `www-authenticate` line that starts with `Bearer`. Access can take a few minutes more to start accepting sign-ins. Until it does, signing in shows `invalid_target` (`OAuth not enabled for the targeted resource`); wait, then try again.
47
47
 
48
48
  ## 2. Connect Claude, or send the publisher the address
49
49
 
@@ -65,4 +65,9 @@ Anyone the Access application's Allow policy lets into the dashboard can connect
65
65
 
66
66
  1. Ask Claude what the publication is called. It answers with the name the dashboard shows.
67
67
 
68
- If Claude can't connect, [Claude can't connect](../publishing/06-troubleshooting.md#claude-cant-connect) lists what it reports and the fix.
68
+ If a check fails:
69
+
70
+ - **Claude can't connect, or the sign-in fails.** Check section 1's settings, and that the connector's address is the instance's address followed by `/api/mcp`.
71
+ - `kestrel login` says the instance "doesn't offer a sign-in for programs". Managed OAuth is off. Turn it on, as section 1 describes.
72
+ - `kestrel login` or `kestrel api` reports a `401`. If you set `ACCESS_ALLOWED_EMAILS`, check that it includes the person's address.
73
+ - **The sign-in says Access didn't say who the person is.** The same goes for a `401` naming `access_unidentified`. The app accepts only an Allow policy on people's emails and a Service Auth policy on a service token. Remove or change any other kind.
@@ -6,11 +6,11 @@ Write in Markdown, schedule it, then send yourself a test. Scheduling makes the
6
6
 
7
7
  ## Write
8
8
 
9
- The editor takes Markdown, and Kestrel saves as you type. The same Markdown becomes the email, in your [email template](11-email-template.md), and the post's page in your archive. **Preview** shows the email.
9
+ The editor takes Markdown, and Kestrel saves as you type. The same Markdown becomes the email, in your [email template](11-email-template.md), and the post's page in your archive. **Preview** shows the email, and warns under it about anything it found wrong.
10
10
 
11
11
  - A blank line starts a new paragraph.
12
12
  - The opening lines are the preview a mail client shows beside the subject.
13
- - Paste or drop an image to add it: PNG, JPEG, WebP, or GIF, up to 5 MB.
13
+ - Paste or drop an image to add it: PNG, JPEG, WebP, or GIF, up to 5 MB. Kestrel may change its file name, turning spaces into hyphens and numbering a name the post already has, and inserts the new name for you.
14
14
 
15
15
  ## Schedule and send
16
16
 
@@ -1,18 +1,34 @@
1
1
  # Troubleshooting
2
2
 
3
- When something needs you, Kestrel says so in a line on the send's page, under **Sent**. The top of the dashboard shows it too. Red needs you to act, and amber is worth a look. The line says what happened, what to do, and who does it. This page covers what the screen can't: the fixes made outside Kestrel, and the decisions that are yours.
4
-
5
- **Set a notifications address, so a problem reaches you without your watching.** Set it under **Settings → Notifications**, in **Send notifications to**: type an address and choose **Add inbox**. Add the address of whoever runs your instance too, and both of you are told. With none set, Kestrel emails you nothing. [Notifications](06-troubleshooting.md#notifications), below, lists what you're told.
3
+ When something needs you, Kestrel says so in a line on the send's page, under **Sent**. The top of the dashboard shows it too. Red needs you to act, and amber is worth a look. The line says what happened and what to do. This page covers what the screen can't: the fixes made outside Kestrel, and the decisions that are yours.
4
+
5
+ **Set a notifications address, so a problem reaches you without your watching.** Set it under **Settings → Notifications**, in **Send notifications to**: type an address and choose **Add inbox**. Add the address of whoever runs your instance too, and both of you are told. With none set, Kestrel emails you nothing. [Notifications](#notifications), below, lists what you're told.
6
+
7
+ | Symptom | Where the fix is |
8
+ | --- | --- |
9
+ | [A send is held or waiting](#a-send-is-held-or-waiting) | Usually outside Kestrel |
10
+ | [Someone was mailed twice, or not at all](#someone-was-mailed-twice-or-not-at-all) | In Kestrel |
11
+ | [A reader says they never got a post](#a-reader-says-they-never-got-a-post) | In Kestrel |
12
+ | [A send still reads Settling](#a-send-still-reads-settling) | Nothing to fix, unless no receipts come at all |
13
+ | [A test doesn't arrive](#a-test-doesnt-arrive) | Outside Kestrel |
14
+ | [Signups aren't open yet, or are paused](#signups-arent-open-yet-or-are-paused) | In Kestrel, or outside it |
15
+ | [The dashboard names a problem with your Amazon SES account](#a-problem-with-amazon-ses) | Outside Kestrel |
16
+ | [An address bounced or complained](#an-address-bounced-or-complained) | Nothing to fix, unless you lift a suppression |
17
+ | [A warning says to move to Workers Paid](../guides/09-as-your-list-grows.md) | Outside Kestrel |
18
+ | [An import is refused](#an-import-is-refused) | In your file |
19
+ | [Claude can't connect](#claude-cant-connect) | Usually outside Kestrel |
6
20
 
7
21
  ## A send is held or waiting
8
22
 
9
- Nothing is lost. A problem with your email provider never marks anyone unsent or mails anyone twice. The send waits where it is, and carries on once the problem is fixed. The send's line names the fix. When that fix is outside Kestrel, whoever runs your instance makes it, and it's one of these:
23
+ Nothing is lost. A problem with your email provider never marks anyone unsent or mails anyone twice. The send waits where it is, and carries on once the problem is fixed. The send's line names the fix. When that fix is outside Kestrel, it's one of these:
10
24
 
11
25
  - **Amazon SES refuses your account or the send.** The line carries SES's words and the fix, almost always in the SES console. [Connect Amazon SES](../get-started/05-ses.md) sets up each piece SES checks: the sending domain, the AWS key, and the configuration set. [Request production access](../get-started/06-production-access.md) lifts the sandbox. When SES refuses your account, any other post that comes due meanwhile waits with it. When it refuses only this send's email, other posts go out as usual. While SES takes some readers and refuses the rest, as while a daily quota frees up, those it takes are sent. The line stays until SES takes everyone.
12
26
  - **SES keeps refusing for its sending rate.** Kestrel is sending faster than your SES account allows. **Settings → Email sender** shows the **Send rate** and where it comes from. If `SES_MAX_SEND_RATE` sets it, lower it to the maximum send rate on the SES console's account dashboard, or remove it, then deploy, as [Configuration](../reference/01-configuration.md#vars) describes. If it's Kestrel's default, the app can't read your account: allow its key `ses:GetAccount`, as [Connect Amazon SES](../get-started/05-ses.md#5-create-the-apps-aws-key) shows.
13
27
  - **No email provider is connected.** Connect one, as [Connect Amazon SES](../get-started/05-ses.md) describes, and the send goes out at its next retry.
14
28
  - **A send missed its time, or nothing has gone out for a while.** The part of Kestrel that sends has stopped, or keeps failing on this send. Check the Worker's Cron Trigger, as [Configuration](../reference/01-configuration.md#bindings-and-triggers) describes, and read the app's logs, as [Read the logs](../get-started/07-verify.md#7-read-the-logs) describes. If another send is going out at the same time, this one may only be waiting its turn.
15
29
 
30
+ If someone else set up Kestrel for you, pass this along to them.
31
+
16
32
  ## Someone was mailed twice, or not at all
17
33
 
18
34
  Kestrel never mails a reader twice on its own. When it can't tell whether your provider took some emails, it stops and asks you: the send's page shows **Resolve…**. Choose **Assume not sent** unless you know otherwise, since it's the safe default: those readers may miss this post, but get your next one. Choose **Assume sent** only when you know they got it, such as from a reader who tells you so. Either choice mails no one.
@@ -27,24 +43,26 @@ Under **Subscribers**, search for their address and open their row. Their page l
27
43
 
28
44
  That's normal. **Delivered** counts only the emails your provider confirmed reached the reader's mail server. Confirmations can lag by days, so the counts keep rising after a send finishes. Some are never confirmed at all, so a send can read **Settling** for good, as long as some receipts have come.
29
45
 
30
- When none come, the send's page says so an hour after it went out. Your provider's reports aren't reaching Kestrel, so its bounces suppress no one and its complaints unsubscribe no one. Your next post mails those addresses again. Look for `webhook.refused` errors in the app's logs, as [Read the logs](../get-started/07-verify.md#7-read-the-logs) describes. Each names the SNS topic that reached Kestrel and the one it expects. Then check the subscription, as [Connect Amazon SES](../get-started/05-ses.md#7-subscribe-the-app-to-the-topic) describes.
46
+ When none come, the send's page says so an hour after it went out. Your provider's reports aren't reaching Kestrel, so its bounces suppress no one and its complaints unsubscribe no one. Your next post mails those addresses again. Look for `webhook.refused` errors in the app's logs, as [Read the logs](../get-started/07-verify.md#7-read-the-logs) describes. Each names the SNS topic that reached Kestrel and the one it expects. Then check the subscription, as [Connect Amazon SES](../get-started/05-ses.md#7-subscribe-the-app-to-the-topic) describes. If someone else set up Kestrel for you, pass this along to them.
31
47
 
32
48
  ## A test doesn't arrive
33
49
 
34
- While Amazon SES is in its sandbox, it mails only addresses verified in SES. Verify the address you test with in the SES console, or [request production access](../get-started/06-production-access.md). When the test's line says the provider didn't answer, check the inbox before you send it again, since the first may have arrived. A test SES refuses for your account has the same fix as [a send it refuses](06-troubleshooting.md#a-send-is-held-or-waiting).
50
+ While Amazon SES is in its sandbox, it mails only addresses verified in SES. Verify the address you test with in the SES console, or [request production access](../get-started/06-production-access.md). When the test's line says the provider didn't answer, check the inbox before you send it again, since the first may have arrived. A test SES refuses for your account has the same fix as [a send it refuses](#a-send-is-held-or-waiting). If someone else set up Kestrel for you, pass this along to them.
35
51
 
36
52
  ## Signups aren't open yet, or are paused
37
53
 
38
54
  Your subscribe page takes signups only while email is working, so a reader is never told a link is coming when none can go. Every visitor gets the same page, whatever address they enter.
39
55
 
40
56
  - "Signups aren’t open yet" means SES hasn't sent an email for your publication yet. Send one from **Template → Send test email**, to an address verified in SES while it's in the sandbox. Once SES takes it, signups open. Before an email provider is connected, the page says the same, and [Connect Amazon SES](../get-started/05-ses.md) is the fix.
41
- - "Signups are paused" means SES is refusing your email, every email including confirmations and tests. The dashboard's **Email isn’t going out** line gives SES's words and the fix, which whoever runs your instance makes, as for [a problem with Amazon SES](#a-problem-with-amazon-ses). Then send a test email: once SES takes one, signups reopen.
57
+ - "Signups are paused" means SES is refusing your email, every email including confirmations and tests. The dashboard's **Email isn’t going out** line gives SES's words and the fix, as for [a problem with Amazon SES](#a-problem-with-amazon-ses). Then send a test email: once SES takes one, signups reopen.
42
58
 
43
59
  Kestrel notices a refusal when an email meets it, or within about ten minutes from its check of your SES account. A refusal the check found lifts on its own within about ten minutes of the fix; one an email met lifts when a test email goes through. A reader who signed up just as it started may have been told a link was coming, and can sign up again once signups reopen.
44
60
 
61
+ If someone else set up Kestrel for you, pass this along to them.
62
+
45
63
  ## A problem with Amazon SES
46
64
 
47
- **Settings → Email sender** and the top of the dashboard name a problem with your Amazon SES account within about ten minutes, without sending anything. Red means no email can go out until it's fixed, tests and confirmations included. Amber is worth a look. Each line links the step that fixes it, and whoever runs your instance makes the fix:
65
+ **Settings → Email sender** and the top of the dashboard name a problem with your Amazon SES account within about ten minutes, without sending anything. Red means no email can go out until it's fixed, tests and confirmations included. Amber is worth a look. Each line links the step that fixes it:
48
66
 
49
67
  - **The configuration set is missing, or its sending is off.** The set Kestrel sends with, the one `SES_CONFIGURATION_SET` names or the sending identity's default if that secret isn't set, isn't in your region, or its sending is turned off. Correct the name, or create the set, as [Connect Amazon SES](../get-started/05-ses.md#4-send-bounces-and-complaints-to-the-app) shows. Turn a set's sending back on in the SES console, under its configuration sets.
50
68
  - **SES rejects the AWS key.** The key ID is unknown, the secret doesn't match it, or the AWS account isn't signed up for SES. Store the key's ID and secret again, or create a new key, as [Connect Amazon SES](../get-started/05-ses.md#5-create-the-apps-aws-key) shows.
@@ -54,11 +72,9 @@ Kestrel notices a refusal when an email meets it, or within about ten minutes fr
54
72
 
55
73
  The app needs to read each of these. If its key isn't allowed to, the line says nothing rather than guessing. The policy in [Connect Amazon SES](../get-started/05-ses.md#5-create-the-apps-aws-key) allows all of it.
56
74
 
57
- ## Your list outgrows Workers Free
75
+ If someone else set up Kestrel for you, pass this along to them.
58
76
 
59
- Upgrade the Cloudflare account to Workers Paid, and set `SUBREQUEST_BUDGET` so Kestrel sends faster, as [As your list grows](../guides/09-as-your-list-grows.md) describes. Kestrel warns you before a send that could use up the free plan's daily database writes, which stops the app until midnight UTC. [Bring your existing list](01-your-list.md#limits) explains the limits.
60
-
61
- ## Bounces and complaints
77
+ ## An address bounced or complained
62
78
 
63
79
  Kestrel acts on these for you, so a bad address can't keep hurting your sending reputation:
64
80
 
@@ -78,31 +94,40 @@ Nothing on your list changes. Kestrel refuses a file it can't read with certaint
78
94
 
79
95
  ## Claude can't connect
80
96
 
81
- Most failures are in your instance's Cloudflare Access setup, made by whoever runs your instance. [Connect Claude](08-connect-claude.md) covers connecting it. When that fails:
97
+ [Connect Claude](08-connect-claude.md) covers connecting it. Most failures are in your instance's Cloudflare Access setup, and [Allow Claude to connect](../guides/01-allow-claude.md#check-it) lists their fixes. When connecting fails:
82
98
 
83
- - **Claude says it couldn't connect, or the sign-in failed.** Your instance's Access application needs Managed OAuth set up as [Turn on Managed OAuth](../guides/01-allow-claude.md#1-turn-on-managed-oauth) describes, including Claude's sign-in address for Claude Desktop and claude.ai. Check that the connector's address is your publication's address followed by `/api/mcp`, and that your instance is deployed, not running on your own computer.
84
- - Signing in shows `invalid_target` (`OAuth not enabled for the targeted resource`). Access can take a few minutes to start accepting sign-ins once Managed OAuth is on. Wait, then try again.
85
- - `kestrel login` says your instance "doesn't offer a sign-in for programs". Managed OAuth is off. Turn it on, as the same section describes.
86
- - `kestrel login` or `kestrel api` reports a `401`. Access signed you in, but Kestrel refused the sign-in. If you set `ACCESS_ALLOWED_EMAILS`, check that it includes the address you signed in with. If either names `access_unidentified`, it's the next problem. If `kestrel api` says your sign-in has ended, sign in again as [Connect Claude](08-connect-claude.md#claude-code) describes.
99
+ - **Claude says it couldn't connect, or the sign-in failed.** Check that the connector's address is your publication's address followed by `/api/mcp`, and that your instance is deployed, not running on your own computer. If both are right, your instance's Access setup needs the fix.
100
+ - Signing in shows `invalid_target`, or `kestrel login` says your instance "doesn't offer a sign-in for programs". Your instance's Access setup needs the fix.
101
+ - `kestrel login` or `kestrel api` reports a `401`. Access signed you in, but Kestrel refused the sign-in, and your instance's Access setup needs the fix. If either names `access_unidentified`, it's the "Access let you in" problem below. If `kestrel api` says your sign-in has ended, sign in again as [Connect Claude](08-connect-claude.md#claude-code) describes.
87
102
  - `kestrel api` says it couldn't renew your sign-in because its Access team answered 404 (not found). First check which instance it called: when you don't name one, it prints a line naming it before "the only instance you're signed in to", which may not be the one you meant. Name yours with `--url`. If it is yours, the Access team domain was most likely renamed or removed since you signed in. Sign in again with `npx kestrel login` and your instance's address, which finds the team it uses now.
88
- - **"Access let you in, but didn't say who you are".** Your login carried neither an email nor a service token. Some other kind of Access policy let it through, such as a certificate or device rule, or a login method that gives no email. Kestrel accepts only an Allow policy on people's emails and a Service Auth policy on a service token. Sign out, then sign in through the policy that lets you in by email. Whoever runs your instance removes the other policy from its Access application, or changes it to one of those two kinds. A script that gets this needs a service token under a Service Auth policy, as [Allow the token in](../guides/02-automate.md#2-allow-the-token-in) describes.
103
+ - **"Access let you in, but didn't say who you are".** Your login carried neither an email nor a service token. Sign out, then sign in through the policy that lets you in by email. If you have no such policy, your instance's Access setup needs the fix.
89
104
  - **Claude's changes read "via the API".** Claude called the API some way other than `kestrel api`, which names Claude Code. Your instance's instructions for Claude Code have it use `kestrel api` once they're brought up to date, as [Install the release](../guides/07-upgrade.md#2-install-the-release) describes. Changes reading "a service token" mean Claude still uses one, and [Connect Claude](08-connect-claude.md#claude-code) signs it in as you instead.
90
105
 
91
- ## Cloudflare won't save the Access application
106
+ If someone else set up Kestrel for you, pass this along to them.
107
+
108
+ ## Where to look
109
+
110
+ When a line doesn't settle it, these say the most about what happened:
92
111
 
93
- Whoever sets up your instance can meet this in [Lock the dashboard with Access](../get-started/03-access.md#2-create-the-access-application). Saving the application fails with an error naming `use_clientless_isolation_app_launcher_url`, saying it needs private destinations. The form has an empty private hostname or IP row beside the two public hostnames. Remove that row, and save again.
112
+ - The top of the dashboard: what needs someone now, in Kestrel's own words.
113
+ - The send's page, under **Sent**: what happened on that send, when it next retries, and its record of each reader.
114
+ - **Settings → Email sender**: the state of your Amazon SES account, and your **Send rate**.
115
+ - **Settings → Notifications**: **Last notification** says whether the last one was delivered, or why not.
116
+ - A subscriber's page, under **Subscribers**: every send they were in, and what happened on each.
117
+ - The app's logs, in your instance's Cloudflare account: [Read the logs](../get-started/07-verify.md#7-read-the-logs) describes them. If someone else set up Kestrel for you, pass this along to them.
94
118
 
95
119
  ## Notifications
96
120
 
97
121
  With an address set, Kestrel emails you:
98
122
 
99
- - when a send finishes, with how many emails were accepted, unsent, and skipped
100
- - when your provider refuses your account or a send, or no provider is connected
123
+ - when a send finishes, with how many emails your provider accepted, and how late it went out if it did
124
+ - when no one's email was accepted for a finished send, since it can't be sent again
125
+ - when email isn't going out and signups are paused: once when it starts, and once more if a post is held after an email that named none
126
+ - when your provider refuses a send or hits a limit, or no provider is connected
101
127
  - when the SES sandbox refuses a signup's confirmation, at most once a day
102
- - when email isn't going out and signups are paused, found by a reader's signup or the check of your SES account, at most once a day while it lasts
103
- - when a send stops going out with nothing to explain it, waits for you to resolve it, or misses its time
104
- - when a service token imports a list, since no one saw it happen (an import you make yourself, or through Claude, sends nothing)
128
+ - when a send stops going out with nothing to explain it, waits for you to resolve it, or doesn't go out on time
129
+ - when your provider has sent no receipts for six hours after a send, once until a receipt arrives
105
130
 
106
- Each goes to every address set, links to the page it's about, and is sent once while the problem lasts. If one address can't be reached, the others still get it, and **Last notification** names the one that failed. A bounce spike shows on the dashboard but isn't emailed. **Send a test notification** checks that one arrives.
131
+ Each says whether anyone has to act, and how. If the fix needs whoever set up your instance, you can forward it as it stands. Each goes to every address set, links to the page it's about, and is sent once while the problem lasts. A reader's address in your provider's words shows only as its domain, and in full on the send's page. If one address can't be reached, the others still get it, and **Last notification** names the one that failed. A bounce spike shows on the dashboard but isn't emailed. **Send a test notification** checks that one arrives.
107
132
 
108
133
  By default, notifications go through your newsletter's own email provider. So when that provider refuses your account, the notification about it can't reach you either. You can send them another way, as [Notifications through Cloudflare's email](../guides/03-notifications.md) describes.
@@ -26,7 +26,7 @@ A variable is replaced with its value in each email. Type it exactly as shown, o
26
26
  | `{{ .Publication.LogoURL }}` | Your logo's address, or empty. |
27
27
  | `{{ .Publication.Address }}` | Your mailing address, or empty. |
28
28
  | `{{ .Email.SentTo }}` | The address this email was sent to. |
29
- | `{{ .Email.UnsubscribeURL }}` | This reader's one-click unsubscribe link. Required. |
29
+ | `{{ .Email.UnsubscribeURL }}` | This reader's unsubscribe link. Required. |
30
30
  | `{{ .Email.ViewInBrowserURL }}` | The post's own web link, which opens its page once it's sent, in your archive or not. |
31
31
 
32
32
  ## Test it
@@ -4,7 +4,7 @@ These pages get you publishing in the dashboard, and cover the decisions to make
4
4
 
5
5
  ## Set up your publication
6
6
 
7
- These are the dashboard's setup steps, in its order. Each is checked off from what Kestrel can see, which Claude reads too, except sharing your link, which Kestrel can't see. Connecting Amazon SES and requesting production access are for whoever runs your Kestrel instance, in [Connect Amazon SES](../get-started/05-ses.md) and [Request production access](../get-started/06-production-access.md). If you're starting without a list, skip importing one.
7
+ These are the dashboard's setup steps, in its order. Each is checked off from what Kestrel can see, which Claude reads too, except sharing your link, which Kestrel can't see. Connecting Amazon SES and requesting production access are for whoever runs your Kestrel instance, in [Connect Amazon SES](../get-started/05-ses.md) and [Request production access](../get-started/06-production-access.md). If you're starting without a list, skip importing one. Setup ends when your first post goes out to the archive, and an email-only send doesn't count.
8
8
 
9
9
  1. Connect Amazon SES
10
10
  1. [Send a test email](10-check-your-setup.md)
@@ -0,0 +1,46 @@
1
+ -- 0021_notifications_outage_receipts: one outage, at most two emails, and a notice of silence (SPEC §8).
2
+ --
3
+ -- `not_sending` is now one notice for each stretch email is refusing rather than one a UTC day:
4
+ -- its episode is when the refusal began. A second kind, `not_sending_held`, keyed the same way,
5
+ -- is the one further email when a post is first held after that notice named none, so a unique
6
+ -- index on each kind's episode is what holds an outage to two. `no_receipts` is a send's notice
7
+ -- that the provider's receipts may not be reaching the app. The publisher is no longer told of
8
+ -- an import, so a pending `import` notice is closed as cleared rather than sent; the kind stays
9
+ -- admitted, with the column it reads, since the rows already recorded keep their history and the
10
+ -- settings surface may still show the latest. A STRICT table's CHECK can't be altered, so the
11
+ -- table is rebuilt, every row and column carried over.
12
+ --
13
+ -- What can't be recovered: nothing but the pending import notices, which were only ever going
14
+ -- to tell of an import already on the record.
15
+
16
+ CREATE TABLE notifications_next (
17
+ send_id TEXT REFERENCES sends (id), -- null for a test, an import notice, and an account notice
18
+ import_id TEXT REFERENCES imports (id), -- set only for an import notice
19
+ kind TEXT NOT NULL
20
+ CHECK (kind IN ('finished', 'refused', 'stuck', 'wedged', 'missed', 'no_receipts', 'test', 'import', 'sandbox', 'not_sending', 'not_sending_held')),
21
+ episode INTEGER NOT NULL DEFAULT 0,
22
+ status TEXT NOT NULL DEFAULT 'pending'
23
+ CHECK (status IN ('pending', 'sent', 'failed', 'unaddressed', 'cleared')),
24
+ attempts INTEGER NOT NULL DEFAULT 0,
25
+ error TEXT,
26
+ created_at INTEGER NOT NULL,
27
+ updated_at INTEGER NOT NULL,
28
+ sent_to TEXT NOT NULL DEFAULT '[]' CHECK (json_valid(sent_to)),
29
+ UNIQUE (send_id, kind, episode),
30
+ UNIQUE (import_id),
31
+ CHECK ((send_id IS NULL) = (kind IN ('test', 'import', 'sandbox', 'not_sending', 'not_sending_held'))),
32
+ CHECK ((import_id IS NULL) = (kind <> 'import'))
33
+ ) STRICT;
34
+
35
+ INSERT INTO notifications_next (send_id, import_id, kind, episode, status, attempts, error, created_at, updated_at, sent_to)
36
+ SELECT send_id, import_id, kind, episode,
37
+ CASE WHEN kind = 'import' AND status = 'pending' THEN 'cleared' ELSE status END,
38
+ attempts, error, created_at, updated_at, sent_to
39
+ FROM notifications;
40
+
41
+ DROP TABLE notifications;
42
+ ALTER TABLE notifications_next RENAME TO notifications;
43
+ CREATE INDEX idx_notifications_status ON notifications (status, created_at);
44
+ CREATE UNIQUE INDEX idx_notifications_sandbox_day ON notifications (episode) WHERE kind = 'sandbox';
45
+ CREATE UNIQUE INDEX idx_notifications_not_sending ON notifications (episode) WHERE kind = 'not_sending';
46
+ CREATE UNIQUE INDEX idx_notifications_not_sending_held ON notifications (episode) WHERE kind = 'not_sending_held';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kurtbruns/kestrel",
3
- "version": "1.19.0",
3
+ "version": "1.20.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",
@@ -29,7 +29,7 @@ Deploying, migrating the production database, and anything that sends mail or ch
29
29
  - **Locally,** with `npm run dev` running, add `--local`: it calls this folder's dev server as its stand-in person, via Claude Code, with no sign-in. `GET /api/dev/outbox` shows the email the stand-in caught.
30
30
  - **`npx kestrel api --list` lists the instance's routes** (`--list sends` one resource's), and **`npx kestrel api help <method> <path>` describes one**: its parameters, body fields, answer, and refusals with their next moves. Both read the instance's own OpenAPI document, so they match the release it runs.
31
31
  - **Read `GET /api/reference.md` first:** how to authenticate, what a status and an error body tell you and the refusals any route can give, paging, acting safely, and every route on one line. **Before calling a route, read its resource's reference,** `GET /api/reference/<resource>.md` (the index names it, such as `sends.md`): the object the routes return, and each route's parameters, body, answer, refusals (each with what to do next), and an example. Read a resource's reference once per conversation, not before every call.
32
- - **Posts with images:** upload each image from a file on disk (an image pasted into the chat isn't one; ask the person to save it into this folder), then reference it in the Markdown by its `filename`, with alt text, `![a short description](cover.jpg)`, never by the upload's `url`. The preview warns on a `url` reference, and on a name the post doesn't have.
32
+ - **Posts with images:** upload each image from a file on disk (an image pasted into the chat isn't one; ask the person to save it into this folder), then reference it in the Markdown by the `filename` the upload answers, with alt text, `![a short description](cover.jpg)`, never by the upload's `url`. That name may differ from the one you sent, since spaces and characters that would break a reference become hyphens, and a name the post already has gets a number. An upload never replaces an image: to swap one, upload the new image, reference its `filename`, and delete the old one. The preview warns on a `url` reference, on a name the post doesn't have, on a reference that didn't become an image, and on an uploaded image the post doesn't use.
33
33
  - **Never overwrite someone else's edit.** A save names the revision it was based on (the posts reference says how). On `stale_revision`, someone else changed the post (the refusal's `author` says who): read it again and re-apply your change to the newer revision, or ask.
34
34
  - **Offer only what a post's or a send's `actions` list,** as the reference's Acting safely says, so a post with no `schedule` or `send_now` (no email provider connected) isn't offered for sending. Read the answer to an act, or its refusal, before telling the publisher what happened or deciding again: it carries the resource as it now stands.
35
35
  - **Resolve a wedged send only on the publisher's choice.** Relay the `wedged` condition's message and ask which `resolution` they want, in the words the editor's Resolve dialog shows them: `unsent` is **Assume not sent**, which puts those readers in front of the next post, and `accepted` is **Assume sent**, which records them as sent. Never choose it yourself.