@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.
- package/CHANGELOG.md +33 -1
- package/dist/public/dashboard/app.js +229 -228
- package/dist/public/dashboard/app.js.map +3 -3
- package/dist/public/dashboard/index.html +2 -2
- package/dist/public/dashboard/styles.css +4 -0
- package/dist/worker/index.js +680 -513
- package/docs/get-started/02-deploy.md +1 -1
- package/docs/get-started/03-access.md +8 -4
- package/docs/get-started/04-aws-account.md +2 -0
- package/docs/get-started/05-ses.md +2 -0
- package/docs/get-started/07-verify.md +7 -5
- package/docs/get-started/08-go-live.md +12 -18
- package/docs/guides/01-allow-claude.md +7 -2
- package/docs/publishing/04-write-a-post.md +2 -2
- package/docs/publishing/06-troubleshooting.md +51 -26
- package/docs/publishing/11-email-template.md +1 -1
- package/docs/publishing/README.md +1 -1
- package/migrations/0021_notifications_outage_receipts.sql +46 -0
- package/package.json +1 -1
- package/template/claude/CLAUDE.md +1 -1
|
@@ -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
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
9
|
+
## Hand off to someone else
|
|
10
10
|
|
|
11
|
-
|
|
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
|
-
|
|
13
|
+
## Optional guides
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
Each one adds one thing, and you can come back to any of them later.
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
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
|
|
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
|
|
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](
|
|
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,
|
|
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](
|
|
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,
|
|
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
|
|
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
|
-
|
|
75
|
+
If someone else set up Kestrel for you, pass this along to them.
|
|
58
76
|
|
|
59
|
-
|
|
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,
|
|
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.**
|
|
84
|
-
- Signing in shows `invalid_target`
|
|
85
|
-
- `kestrel login`
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
100
|
-
- when
|
|
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
|
|
103
|
-
- when
|
|
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
|
|
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.
|
|
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
|
|
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, ``, 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.
|