@kurtbruns/kestrel 1.18.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/.dev.vars.example +16 -4
- package/CHANGELOG.md +76 -1
- package/dist/public/dashboard/app.js +289 -290
- package/dist/public/dashboard/app.js.map +4 -4
- package/dist/public/dashboard/index.html +2 -2
- package/dist/public/dashboard/styles.css +127 -21
- package/dist/worker/index.js +4135 -2973
- package/docs/get-started/02-deploy.md +2 -2
- package/docs/get-started/03-access.md +17 -5
- package/docs/get-started/04-aws-account.md +2 -0
- package/docs/get-started/05-ses.md +49 -28
- package/docs/get-started/06-production-access.md +6 -3
- package/docs/get-started/07-verify.md +9 -7
- package/docs/get-started/08-go-live.md +12 -16
- package/docs/guides/01-allow-claude.md +7 -2
- package/docs/guides/03-notifications.md +10 -10
- package/docs/publishing/04-write-a-post.md +2 -2
- package/docs/publishing/06-troubleshooting.md +70 -21
- package/docs/publishing/08-connect-claude.md +1 -1
- package/docs/publishing/09-share-your-subscribe-link.md +3 -1
- package/docs/publishing/10-check-your-setup.md +13 -9
- package/docs/publishing/11-email-template.md +1 -1
- package/docs/publishing/README.md +8 -3
- package/docs/reference/01-configuration.md +5 -5
- package/migrations/0016_held_receipts.sql +26 -0
- package/migrations/0017_provider_account_rejected.sql +24 -0
- package/migrations/0018_notify_addresses.sql +13 -0
- package/migrations/0019_sending.sql +67 -0
- package/migrations/0020_notifications_not_sending.sql +40 -0
- package/migrations/0021_notifications_outage_receipts.sql +46 -0
- package/package.json +1 -1
- package/packages/kestrel/lib/api.mjs +28 -3
- package/packages/kestrel/lib/oauth.mjs +5 -3
- package/template/claude/CLAUDE.md +4 -2
- package/template/wrangler.jsonc +5 -5
|
@@ -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
|
|
|
@@ -209,7 +209,7 @@ npx wrangler delete --name newsletter-dev
|
|
|
209
209
|
|
|
210
210
|
In a browser, `https://newsletter.example.com/dashboard/` says the editor is locked until you set up Access. That's expected.
|
|
211
211
|
|
|
212
|
-
1. In a browser, `https://newsletter.example.com/` shows the newsletter's public home page. Its subscribe page, `/subscribe`, says signups aren't open yet, until Connect Amazon SES.
|
|
212
|
+
1. In a browser, `https://newsletter.example.com/` shows the newsletter's public home page. Its subscribe page, `/subscribe`, says signups aren't open yet, until Connect Amazon SES has sent its first test email.
|
|
213
213
|
|
|
214
214
|
If the public pages instead say the newsletter is temporarily unavailable, and `/health` answers `500` with a body like `{"error":"invalid_config","variable":"APP_ORIGIN",…}`, the setting it names is missing, malformed, or still on `example.com`. Fix it in `wrangler.jsonc`, commit, and deploy again.
|
|
215
215
|
|
|
@@ -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
|
|
|
@@ -79,13 +79,19 @@ The app verifies each login against your team domain, and checks that it was iss
|
|
|
79
79
|
|
|
80
80
|
A secret takes effect as soon as you store it, with no redeploy.
|
|
81
81
|
|
|
82
|
-
1. **(Optional, recommended)** Limit the editor to named people. The Access policy decides who can sign in. This list keeps that true if the policy is widened later, for another app or by mistake.
|
|
82
|
+
1. **(Optional, recommended)** Limit the editor to named people. The Access policy decides who can sign in. This list keeps that true if the policy is widened later, for another app or by mistake. Store the list of addresses allowed in:
|
|
83
83
|
|
|
84
84
|
```bash
|
|
85
85
|
npx wrangler secret put ACCESS_ALLOWED_EMAILS --env production
|
|
86
86
|
```
|
|
87
87
|
|
|
88
|
-
|
|
88
|
+
At the prompt, enter the addresses separated by commas:
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
you@example.com, publisher@example.com
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Spaces and capital letters don't matter. Without the list, the app lets in anyone your Access policy allows.
|
|
89
95
|
|
|
90
96
|
## Check it
|
|
91
97
|
|
|
@@ -112,7 +118,13 @@ The app verifies each login against your team domain, and checks that it was iss
|
|
|
112
118
|
If a check fails:
|
|
113
119
|
|
|
114
120
|
- **The editor says the configuration needs a fix.** Store the secret it names. While only one of the two is stored, `/health` answers `500` naming the other, and the public pages say the newsletter is temporarily unavailable.
|
|
115
|
-
- **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.
|
|
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.
|
|
116
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.
|
|
117
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
|
+
|
|
118
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).
|
|
@@ -19,7 +19,7 @@ The same form sets a custom MAIL FROM domain, so SPF also passes for your domain
|
|
|
19
19
|
|
|
20
20
|
SES keeps identities, quotas, and the sandbox separately in each region, so do every step on this page in the region you chose. [Configuration](../reference/01-configuration.md#vars) says more about choosing one, under `AWS_REGION`.
|
|
21
21
|
|
|
22
|
-
1. Note the region's code, such as `
|
|
22
|
+
1. Note the region's code, such as `us-west-2`. The menu shows it beside the region's name, and [Amazon SES endpoints](https://docs.aws.amazon.com/general/latest/gr/ses.html) lists each one too. AWS names a region in more than one way, such as United States (Oregon) and US West (Oregon), so go by the code. Later steps use it.
|
|
23
23
|
|
|
24
24
|
1. Under **Configuration**, choose **Identities**, then **Create identity**.
|
|
25
25
|
|
|
@@ -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.
|
|
@@ -73,27 +75,31 @@ A new SES account starts in the sandbox: it sends only to addresses you've verif
|
|
|
73
75
|
|
|
74
76
|
1. Choose two addresses you own, such as a personal address and a work address. Verify it works uses both.
|
|
75
77
|
|
|
76
|
-
1. For each, under **Identities**, choose **Create identity**, select **Email address**, and enter it. Choose **Create identity**.
|
|
78
|
+
1. For each, under **Identities**, choose **Create identity**, select **Email address**, and enter it. Leave **Assign a default configuration set** and **Assign to a tenant** unchecked. The form's notice about DMARC is about sending from the address, and the app only sends to it. Choose **Create identity**.
|
|
77
79
|
|
|
78
80
|
1. Open the email AWS sends to each address, and follow its link. The identity's status changes to **Verified**.
|
|
79
81
|
|
|
82
|
+
AWS's email and confirmation page say you can now send from the address. That's expected: SES has one kind of verified address, and the same verification lets the sandbox send to it.
|
|
83
|
+
|
|
80
84
|
## 4. Send bounces and complaints to the app
|
|
81
85
|
|
|
82
86
|
SES reports bounces and complaints through Amazon Simple Notification Service (SNS), which delivers them to the app. The app then stops mailing an address that bounced or complained. A configuration set says which events SES reports, and an SNS topic carries them.
|
|
83
87
|
|
|
84
|
-
1. In SES, under **Configuration**, choose **Configuration sets**, then **Create set**. Name it `kestrel-mail`, and choose **Create set**.
|
|
88
|
+
1. In SES, under **Configuration**, choose **Configuration sets**, then **Create set**. Name it `kestrel-mail`, keep every other option as it is, and choose **Create set**.
|
|
85
89
|
|
|
86
90
|
1. On the set's **Event destinations** tab, choose **Add destination**. Select **Hard bounces**, **Complaints**, **Deliveries**, and **Rendering failures**, and choose **Next**. The app logs a rendering failure, an email SES couldn't build, as an error.
|
|
87
91
|
|
|
88
|
-
|
|
92
|
+
Leave the other six off, since the app already knows about them or doesn't act on them. **Sends** repeats what the send records, and **Delivery delays** end as a bounce or a delivery. **Rejects** (a virus) and **Subscriptions** (SES's own unsubscribe) don't arise, since the app's emails carry no attachments and their own unsubscribe link. **Opens** and **Clicks** would add tracking the app doesn't record.
|
|
93
|
+
|
|
94
|
+
1. For **Destination type**, select **Amazon SNS**. Name it `kestrel-sns`, and keep **Event publishing** enabled.
|
|
89
95
|
|
|
90
|
-
1.
|
|
96
|
+
1. On the same screen, under **SNS topic**, choose **Create SNS topic**. Name it `kestrel-events`, leave the display name empty, and choose **Create topic**. Then select `kestrel-events` in the topic menu, if it isn't already. Choose **Next**, then **Add destination**.
|
|
91
97
|
|
|
92
|
-
1. Make the set your hostname's default, so SES reports on every email sent from it.
|
|
98
|
+
1. Make the set your hostname's default, so SES reports on every email sent from it. Under **Identities**, open `send.example.com`, and choose its **Configuration set** tab, below the summary. Edit the **Default configuration set**, select **Assign a default configuration set**, select `kestrel-mail`, and save.
|
|
93
99
|
|
|
94
100
|
1. Open the topic in the [SNS console](https://console.aws.amazon.com/sns/), in the same region, and find its ARN. It looks like `arn:aws:sns:us-east-1:111122223333:kestrel-events`, where `us-east-1` is your region's code and `111122223333` is your account ID.
|
|
95
101
|
|
|
96
|
-
1.
|
|
102
|
+
1. Check that SES may publish to the topic, on its **Access policy** tab. A topic created from SES's **Add destination**, as above, already lets SES publish to it from your account. Leave that statement as it is. For a topic you made in the SNS console instead, choose **Edit**, expand **Access policy**, and add this statement to the `Statement` list:
|
|
97
103
|
|
|
98
104
|
```json
|
|
99
105
|
{
|
|
@@ -126,15 +132,13 @@ SES reports bounces and complaints through Amazon Simple Notification Service (S
|
|
|
126
132
|
|
|
127
133
|
## 5. Create the app's AWS key
|
|
128
134
|
|
|
129
|
-
The app sends through an AWS Identity and Access Management (IAM) user whose key can only send email and read the account's sending status. That limits the harm if the key ever leaks. Reading the status lets the app show whether SES is still in the sandbox
|
|
135
|
+
The app sends through an AWS Identity and Access Management (IAM) user whose key can only send email and read the account's sending status. That limits the harm if the key ever leaks. Reading the status lets the app show whether SES is still in the sandbox and take its send rate from the account. It also warns, without sending anything, when the configuration set is missing or paused, the sender isn't verified, or bounces and complaints can't reach the app.
|
|
130
136
|
|
|
131
|
-
If `kestrel-sender` already exists,
|
|
137
|
+
If `kestrel-sender` already exists, from an earlier release, don't create a second user or key. Update its policy instead, as the end of this section shows.
|
|
132
138
|
|
|
133
|
-
1. In the [IAM console](https://console.aws.amazon.com/iam/),
|
|
139
|
+
1. In the [IAM console](https://console.aws.amazon.com/iam/), choose **Policies**, then **Create policy**.
|
|
134
140
|
|
|
135
|
-
1.
|
|
136
|
-
|
|
137
|
-
1. In the new tab, choose the **JSON** editor, and replace its contents with this policy:
|
|
141
|
+
1. Choose the **JSON** editor, and replace its contents with this policy:
|
|
138
142
|
|
|
139
143
|
```json
|
|
140
144
|
{
|
|
@@ -145,8 +149,11 @@ If `kestrel-sender` already exists, as on an instance set up before Kestrel read
|
|
|
145
149
|
"Action": [
|
|
146
150
|
"ses:SendEmail",
|
|
147
151
|
"ses:SendBulkEmail",
|
|
152
|
+
"ses:SendTemplatedEmail",
|
|
153
|
+
"ses:SendBulkTemplatedEmail",
|
|
148
154
|
"ses:GetAccount",
|
|
149
|
-
"ses:GetEmailIdentity"
|
|
155
|
+
"ses:GetEmailIdentity",
|
|
156
|
+
"ses:GetConfigurationSet"
|
|
150
157
|
],
|
|
151
158
|
"Resource": "*"
|
|
152
159
|
}
|
|
@@ -154,13 +161,17 @@ If `kestrel-sender` already exists, as on an instance set up before Kestrel read
|
|
|
154
161
|
}
|
|
155
162
|
```
|
|
156
163
|
|
|
157
|
-
|
|
164
|
+
1. Choose **Next**, name the policy `kestrel-send-email`, and choose **Create policy**. The review lists Pinpoint Email, SES, and SES v2, which is how IAM shows these SES actions.
|
|
165
|
+
|
|
166
|
+
1. Choose **IAM users**, then **Create user**. Name it `kestrel-sender`, leave console access off, and choose **Next**.
|
|
158
167
|
|
|
159
|
-
1.
|
|
168
|
+
1. Choose **Attach policies directly**, search for `kestrel-send-email`, and select it. AWS recommends groups, which help manage many people, and one app's user doesn't need one. Choose **Next**, then **Create user**.
|
|
160
169
|
|
|
161
|
-
1.
|
|
170
|
+
1. Open `kestrel-sender`, and choose **Create access key**. It's an access key, not an API key. For the use case, choose **Application running outside AWS**, and choose **Next**.
|
|
162
171
|
|
|
163
|
-
|
|
172
|
+
AWS suggests IAM Roles Anywhere instead. It needs a certificate authority of your own, so use the access key.
|
|
173
|
+
|
|
174
|
+
1. For the description tag, enter the app's hostname, such as `newsletter.example.com`, so you can tell its key apart later. Choose **Create access key**.
|
|
164
175
|
|
|
165
176
|
1. Keep the page open: AWS shows the secret access key only once. Store the access key ID in the app, and paste it at the prompt:
|
|
166
177
|
|
|
@@ -174,6 +185,8 @@ If `kestrel-sender` already exists, as on an instance set up before Kestrel read
|
|
|
174
185
|
npx wrangler secret put AWS_SECRET_ACCESS_KEY --env production
|
|
175
186
|
```
|
|
176
187
|
|
|
188
|
+
To update an existing key's policy, open **Policies** in the IAM console, then `kestrel-send-email`, and choose **Edit**. In the JSON editor, make its `Action` list match the policy above, and save. Without `ses:SendTemplatedEmail`, SES refuses even a test email. Once the key may call `ses:GetAccount`, the app's next check, within an hour, reads the account. Without `ses:GetConfigurationSet`, the app can't tell whether the configuration set exists, so it says nothing about it rather than guessing.
|
|
189
|
+
|
|
177
190
|
## 6. Connect the app and deploy
|
|
178
191
|
|
|
179
192
|
Until now the app has run with no email provider connected, refusing to send. Setting `PROVIDER` to `ses` connects SES, with the secrets you stored in sections 4 and 5. If a required one is missing, the app refuses to run until you store it, and `/health` names it.
|
|
@@ -222,6 +235,8 @@ Until now the app has run with no email provider connected, refusing to send. Se
|
|
|
222
235
|
|
|
223
236
|
1. In the editor, **Settings → Email sender** shows Amazon SES and your From address, where it said **No email provider connected** before. Within a couple of minutes, a **Sandbox** pill appears beside it, once the app has read your SES account.
|
|
224
237
|
|
|
238
|
+
1. In the editor, open **Template** and select **Send test email**, to an address you verified in [Verify your test addresses](#3-verify-your-test-addresses). It arrives. Signups stay closed until SES takes an email, so the **Not proven yet** pill beside Amazon SES in **Settings → Email sender** is gone once it has. If SES refuses it, the test's line says why and what fixes it.
|
|
239
|
+
|
|
225
240
|
1. In a browser, `https://newsletter.example.com/subscribe` shows the subscribe form, where it said signups aren't open yet.
|
|
226
241
|
|
|
227
242
|
## 7. Subscribe the app to the topic
|
|
@@ -240,7 +255,7 @@ SNS delivers events only to a subscriber that has confirmed. The app confirms on
|
|
|
240
255
|
|
|
241
256
|
Any other answer, such as a redirect to your Cloudflare login, means Access covers the webhook. Remove that path from your Access application.
|
|
242
257
|
|
|
243
|
-
1. In the SNS console, open your topic, and choose **Create subscription**.
|
|
258
|
+
1. In the SNS console, open your topic, and choose **Create subscription**. Check that **Topic ARN** shows the `kestrel-events` topic, and choose it if it's empty.
|
|
244
259
|
|
|
245
260
|
1. Set **Protocol** to **HTTPS**, and enter the endpoint:
|
|
246
261
|
|
|
@@ -248,13 +263,9 @@ SNS delivers events only to a subscriber that has confirmed. The app confirms on
|
|
|
248
263
|
https://newsletter.example.com/webhooks/ses
|
|
249
264
|
```
|
|
250
265
|
|
|
251
|
-
1. Leave raw message delivery off
|
|
266
|
+
1. Leave raw message delivery off.
|
|
252
267
|
|
|
253
|
-
1.
|
|
254
|
-
|
|
255
|
-
If it stays **Pending confirmation**, check that your Access application covers only `/dashboard` and `/api`, that `SNS_TOPIC_ARN` matches the topic's ARN, and that section 6's deploy finished. A mismatched ARN shows in the app's logs as a `webhook.refused` error naming both. Then request confirmation again from the topic's subscriptions. SNS doesn't let you delete a pending subscription, and removes it on its own after 48 hours.
|
|
256
|
-
|
|
257
|
-
1. Give SNS longer to retry. By default it retries a failed delivery 3 times over about a minute, then discards the event, so a bounce that arrives mid-deploy is lost. Open the topic, choose **Edit**, and expand **Delivery retry policy (HTTP/S)**. Turn off the default policy, set these values, and choose **Save changes**:
|
|
268
|
+
1. Give SNS longer to retry. By default it retries a failed delivery 3 times over about a minute, then discards the event, so a bounce that arrives mid-deploy is lost. Expand **Delivery policy (HTTP/S)**, turn off **Use the default delivery policy**, and set these values:
|
|
258
269
|
|
|
259
270
|
| Field | Value |
|
|
260
271
|
| --- | --- |
|
|
@@ -266,13 +277,19 @@ SNS delivers events only to a subscriber that has confirmed. The app confirms on
|
|
|
266
277
|
| Maximum delay retries | `6` |
|
|
267
278
|
| Retry-backoff function | Exponential |
|
|
268
279
|
|
|
269
|
-
SNS then keeps trying for about 40 minutes. It allows an hour at most.
|
|
280
|
+
SNS then keeps trying for about 40 minutes. It allows an hour at most. The topic has a delivery policy of its own, under **Edit**. Setting it there instead also covers any subscription you add later.
|
|
281
|
+
|
|
282
|
+
1. Choose **Create subscription**.
|
|
283
|
+
|
|
284
|
+
1. Reload the subscription. Its status changes to **Confirmed**, with nothing for you to click.
|
|
285
|
+
|
|
286
|
+
If it stays **Pending confirmation**, check that your Access application covers only `/dashboard` and `/api`. Check that `SNS_TOPIC_ARN` matches the topic's ARN, and that section 6's deploy finished. A mismatched ARN shows in the app's logs as a `webhook.refused` error naming both. Then request confirmation again from the topic's subscriptions. SNS doesn't let you delete a pending subscription, and removes it on its own after 48 hours.
|
|
270
287
|
|
|
271
288
|
1. **(Optional)** Keep what SNS still can't deliver. On the subscription, choose **Edit**, and set a **Redrive policy (dead-letter queue)** to an SQS queue of your own. SNS then moves an event there instead of discarding it. [Amazon SNS dead-letter queues](https://docs.aws.amazon.com/sns/latest/dg/sns-dead-letter-queues.html) covers the queue and its permissions.
|
|
272
289
|
|
|
273
290
|
## 8. Receive replies at your From address
|
|
274
291
|
|
|
275
|
-
Replies to a post go to your From address, `newsletter@send.example.com`, which has no inbox yet, so they bounce. This section forwards that address to the publisher's inbox with Cloudflare Email Routing. Forwarding only receives mail, so it doesn't change how SES sends.
|
|
292
|
+
Replies to a post go to your From address, `newsletter@send.example.com`, which has no inbox yet, so they bounce. The reader is told your domain couldn't be found. This section forwards that address to the publisher's inbox with Cloudflare Email Routing. Forwarding only receives mail, so it doesn't change how SES sends.
|
|
276
293
|
|
|
277
294
|
1. Check that your From address has no inbox yet. This prints nothing:
|
|
278
295
|
|
|
@@ -286,7 +303,7 @@ Replies to a post go to your From address, `newsletter@send.example.com`, which
|
|
|
286
303
|
|
|
287
304
|
1. Add `send.example.com` to Email Routing: select `example.com`, then **Settings**, and add `send` under **Subdomains**.
|
|
288
305
|
|
|
289
|
-
1. Cloudflare adds MX, SPF, and DKIM records for `send.example.com`, and locks them. Wait until they show as added.
|
|
306
|
+
1. Cloudflare adds MX, SPF, and DKIM records for `send.example.com`, and locks them. Wait until they show as added. Its SPF record is for forwarding: SES's mail passes SPF at `bounce.send.example.com`, from section 1.
|
|
290
307
|
|
|
291
308
|
1. Under **Destination Addresses**, add the publisher's inbox. Open the email Cloudflare sends to it, and select **Verify email address**. If it isn't your inbox, ask the publisher to do this step.
|
|
292
309
|
|
|
@@ -300,6 +317,10 @@ Replies to a post go to your From address, `newsletter@send.example.com`, which
|
|
|
300
317
|
dig +short MX send.example.com
|
|
301
318
|
```
|
|
302
319
|
|
|
320
|
+
1. Send an email to `newsletter@send.example.com` from an address whose mail doesn't go through the publisher's inbox. Gmail, for one, hides a forwarded copy of mail you sent yourself. In Email Routing's **Activity log**, the email shows as **Forwarded**.
|
|
321
|
+
|
|
322
|
+
If it bounces saying the domain couldn't be found, the sender's provider is still remembering that `send.example.com` had no mail servers. That happens after an email to it before this section, and lasts up to 30 minutes on Cloudflare's DNS. Wait, and send it again.
|
|
323
|
+
|
|
303
324
|
Out-of-office and other automatic replies reach the publisher's inbox too. Bounces don't: they go to SES, which reports them to the app. The publisher answers from their own inbox, not from `newsletter@send.example.com`. To answer from the newsletter's address instead, give it a mailbox at a mail host such as [Fastmail](https://www.fastmail.help/hc/en-us/articles/360060591133) or [Google Workspace](https://support.google.com/a/answer/140034), in place of forwarding.
|
|
304
325
|
|
|
305
326
|
SES is now connected in the sandbox. Until AWS grants production access, it sends only to the addresses you verified in section 3. It also sends to the SES test addresses Verify it works uses. SES charges for each email it sends, test emails included, as [Amazon SES pricing](https://aws.amazon.com/ses/pricing/) lists.
|
|
@@ -34,11 +34,13 @@ AWS reviews each request by hand, for the region the app sends from.
|
|
|
34
34
|
|
|
35
35
|
1. Check the acknowledgement, and select **Submit request**. AWS Support answers by email.
|
|
36
36
|
|
|
37
|
-
1. Within about ten minutes, the editor's **Settings → Email sender** says AWS is reviewing your request for production access
|
|
37
|
+
1. Within about ten minutes, the editor's **Settings → Email sender** says AWS is reviewing your request for production access, and later whether AWS has granted it.
|
|
38
|
+
|
|
39
|
+
Within seconds of every request, AWS sends an automated email asking for more information, in a support case. The SES console then shows **More information needed**, and the editor says AWS hasn't granted production access yet and has replied in AWS Support, with the case number. That's expected, and not a refusal: answer it as the next section describes.
|
|
38
40
|
|
|
39
41
|
## 3. Answer AWS's questions
|
|
40
42
|
|
|
41
|
-
AWS
|
|
43
|
+
AWS asks how you collect addresses and handle bounces before it decides, every time. A newsletter may be asked for more detail again, or turned down the first time, sometimes without a reason. A reply that explains how the list is built usually gets further than a new request. Reply in the support case, whichever it is, or open a new request from the **Account dashboard**. The editor reads the same until AWS grants access, since AWS's account reports a request for more information and a refusal alike.
|
|
42
44
|
|
|
43
45
|
In the sandbox, the subscribe form's confirmation email reaches only addresses verified in SES. If AWS's reviewer tries the form, say that this is why no confirmation arrives. In your reply, cover:
|
|
44
46
|
|
|
@@ -48,6 +50,7 @@ In the sandbox, the subscribe form's confirmation email reaches only addresses v
|
|
|
48
50
|
- That the publication's website shows what the newsletter is, and how readers subscribe.
|
|
49
51
|
- That an imported list was gathered with opt-in on the previous service.
|
|
50
52
|
- How many subscribers the newsletter expects, and how often it sends.
|
|
53
|
+
- A sample email. Send yourself a test of a post, as [Verify it works](07-verify.md#1-send-yourself-a-test-email) shows, and attach it to your reply or paste its text.
|
|
51
54
|
|
|
52
55
|
## 4. Check that AWS granted it
|
|
53
56
|
|
|
@@ -55,7 +58,7 @@ Until AWS grants production access, SES sends only to your verified addresses, s
|
|
|
55
58
|
|
|
56
59
|
1. In the editor, open **Settings → Email sender**. The email provider reads **Production**. From the API, `npx kestrel api GET /api/provider` shows `state` as `production`.
|
|
57
60
|
|
|
58
|
-
1. If it reads **Sandbox**, AWS hasn't granted access yet. Meanwhile, continue with [Verify it works](07-verify.md), which runs in the sandbox. Come back to this section when AWS answers
|
|
61
|
+
1. If it reads **Sandbox**, AWS hasn't granted access yet. If it also says AWS has replied, with a case number, answer in that case, as section 3 describes. Meanwhile, continue with [Verify it works](07-verify.md), which runs in the sandbox. Come back to this section when AWS answers.
|
|
59
62
|
|
|
60
63
|
1. If it says Kestrel's AWS key needs permission to read your account, select **Add the permission** and follow the step it opens. The app checks again within an hour.
|
|
61
64
|
|
|
@@ -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
|
|
|
@@ -41,11 +41,11 @@ If a check fails, compare your DNS records with the ones in [Connect Amazon SES]
|
|
|
41
41
|
|
|
42
42
|
## 3. Check that notifications arrive
|
|
43
43
|
|
|
44
|
-
Kestrel emails
|
|
44
|
+
Kestrel emails each notifications address when a send finishes, and right away when one runs into a problem. Check that they reach you, using one of the addresses you verified in SES. While SES is in the sandbox, every notifications address has to be verified there too, unless notifications go through [Cloudflare's email](../guides/03-notifications.md), where each is verified with Cloudflare instead. If you're setting Kestrel up for someone else, you can add their address beside yours later.
|
|
45
45
|
|
|
46
|
-
1. In the editor, open **Settings → Notifications**,
|
|
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,26 +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
|
-
- [Notifications through Cloudflare's email](../guides/03-notifications.md), so problems are reported even when the email provider has stopped sending.
|
|
24
|
-
- [Rate-limit the subscribe form](../guides/05-rate-limit.md), before the subscribe link is shared widely.
|
|
25
|
-
- [As your list grows](../guides/09-as-your-list-grows.md), from about 1,000 subscribers.
|
|
26
|
-
- [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
|
-
|
|
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.
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
Kestrel emails the publisher when a send finishes, and right away when one runs into a problem. By default, these notifications go through the email provider. The one that matters most, about the provider refusing your account, is the one the provider would refuse too. Cloudflare's email doesn't depend on the provider, so that notification still reaches the publisher.
|
|
4
4
|
|
|
5
|
-
In this guide, you turn on Cloudflare's email for your app's hostname, and verify the notifications
|
|
5
|
+
In this guide, you turn on Cloudflare's email for your app's hostname, and verify the notifications addresses with it. Then you add the binding that sends notifications through it, deploy, and send yourself a test.
|
|
6
6
|
|
|
7
7
|
## Before you begin
|
|
8
8
|
|
|
9
9
|
You need:
|
|
10
10
|
|
|
11
|
-
- The notifications
|
|
11
|
+
- The notifications addresses saved, from [Verify it works](../get-started/07-verify.md#3-check-that-notifications-arrive).
|
|
12
12
|
- Your domain's DNS on Cloudflare, as for the rest of the app.
|
|
13
13
|
|
|
14
14
|
## 1. Turn on Email Routing for your app's hostname
|
|
@@ -23,13 +23,13 @@ Cloudflare's email sends only from a domain set up with Email Routing. Use your
|
|
|
23
23
|
|
|
24
24
|
Avoid turning on Email Routing for a domain that already receives mail elsewhere, such as `example.com` with Google Workspace. Email Routing takes over that domain's MX records. Don't send notifications from `send.example.com` either, since your email provider sends from it. Email Routing there to forward readers' replies, as [Connect Amazon SES](../get-started/05-ses.md#8-receive-replies-at-your-from-address) sets up, is fine, since it only receives.
|
|
25
25
|
|
|
26
|
-
## 2. Verify the notifications
|
|
26
|
+
## 2. Verify the notifications addresses
|
|
27
27
|
|
|
28
|
-
Cloudflare's email delivers only to addresses you've verified with it. Sending to a verified address is free on any plan.
|
|
28
|
+
Cloudflare's email delivers only to addresses you've verified with it. Sending to a verified address is free on any plan. Notifications can go to several addresses, such as the publisher's and yours, and each one needs verifying.
|
|
29
29
|
|
|
30
|
-
1. Under **Email Routing**, go to **Destination Addresses**, and add
|
|
30
|
+
1. Under **Email Routing**, go to **Destination Addresses**, and add each address saved for notifications.
|
|
31
31
|
|
|
32
|
-
1. Open the email Cloudflare sends to
|
|
32
|
+
1. Open the email Cloudflare sends to each address, and select **Verify email address**. If it's the publisher's address, ask them to do this step.
|
|
33
33
|
|
|
34
34
|
A verified address belongs to your Cloudflare account, so every environment in it can use it.
|
|
35
35
|
|
|
@@ -41,11 +41,11 @@ A `send_email` binding named `NOTIFY` moves notifications to Cloudflare's email.
|
|
|
41
41
|
|
|
42
42
|
```jsonc
|
|
43
43
|
"send_email": [
|
|
44
|
-
{ "name": "NOTIFY", "allowed_destination_addresses": ["you@example.com"] } // ← the notifications
|
|
44
|
+
{ "name": "NOTIFY", "allowed_destination_addresses": ["you@example.com"] } // ← the notifications addresses
|
|
45
45
|
],
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
-
`allowed_destination_addresses` is optional. It limits the binding to the addresses you list, so a changed setting in the editor can't send notifications anywhere else. Leave it out to allow any address you've verified. If the publisher changes
|
|
48
|
+
`allowed_destination_addresses` is optional. It limits the binding to the addresses you list, so a changed setting in the editor can't send notifications anywhere else. Leave it out to allow any address you've verified. If the publisher adds or changes a notifications address later, verify the new one and add it here too.
|
|
49
49
|
|
|
50
50
|
1. **(Optional)** Notifications come from `Kestrel <kestrel@newsletter.example.com>`. To use another address on that hostname, set `NOTIFY_FROM` in the block's `vars`:
|
|
51
51
|
|
|
@@ -76,9 +76,9 @@ A `send_email` binding named `NOTIFY` moves notifications to Cloudflare's email.
|
|
|
76
76
|
|
|
77
77
|
1. In the editor, open **Settings → Notifications**. **Sent through** reads "Cloudflare Email, separate from your newsletter's provider", and **From address** shows `Kestrel <kestrel@newsletter.example.com>`.
|
|
78
78
|
|
|
79
|
-
1. Select **Send a test notification**. It arrives at
|
|
79
|
+
1. Select **Send a test notification**. It arrives at each notifications address, and **Last notification** shows it as delivered.
|
|
80
80
|
|
|
81
|
-
If the test isn't delivered, **Last notification** shows Cloudflare's reason, such as an address that isn't verified. Cloudflare's Email Routing summary lists notifications as dropped even when they're delivered, so check the notifications inbox rather than the summary.
|
|
81
|
+
If the test isn't delivered to an address, **Last notification** shows Cloudflare's reason, such as an address that isn't verified, after the address when there are several. The other addresses still get it. Cloudflare's Email Routing summary lists notifications as dropped even when they're delivered, so check the notifications inbox rather than the summary.
|
|
82
82
|
|
|
83
83
|
## What notifications don't cover
|
|
84
84
|
|
|
@@ -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
|
|