@kurtbruns/kestrel 1.12.0 → 1.14.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 +137 -1
- package/README.md +1 -1
- package/bin/kestrel.mjs +17 -2
- package/cli/api.mjs +462 -0
- package/cli/credentials.mjs +192 -0
- package/cli/dev-api.mjs +17 -11
- package/cli/login.mjs +155 -0
- package/cli/oauth.mjs +368 -0
- package/cli/openapi.mjs +569 -0
- package/cli/seed.mjs +1 -1
- package/dist/public/dashboard/app.js +350 -295
- package/dist/public/dashboard/app.js.map +4 -4
- package/dist/public/dashboard/index.html +2 -2
- package/dist/public/dashboard/styles.css +466 -153
- package/dist/worker/index.js +10634 -5773
- package/docs/README.md +3 -2
- package/docs/get-started/02-deploy.md +18 -21
- package/docs/get-started/03-access.md +10 -8
- package/docs/get-started/04-ses.md +2 -2
- package/docs/get-started/05-verify.md +4 -4
- package/docs/get-started/06-go-live.md +3 -2
- package/docs/guides/01-allow-claude.md +68 -0
- package/docs/guides/02-automate.md +88 -0
- package/docs/guides/09-as-your-list-grows.md +8 -4
- package/docs/publishing/01-your-list.md +38 -33
- package/docs/publishing/02-sending-rules.md +3 -3
- package/docs/publishing/03-name-your-publication.md +1 -1
- package/docs/publishing/04-write-a-post.md +15 -52
- package/docs/publishing/06-troubleshooting.md +30 -14
- package/docs/publishing/08-connect-claude.md +60 -0
- package/docs/publishing/09-share-your-subscribe-link.md +3 -3
- package/docs/publishing/10-check-your-setup.md +9 -16
- package/docs/publishing/11-email-template.md +3 -1
- package/docs/publishing/README.md +2 -2
- package/docs/reference/01-configuration.md +132 -85
- package/docs/reference/02-sending-domain-dns.md +2 -2
- package/migrations/0012_unsubscribe_cause.sql +139 -0
- package/migrations/0013_who_acted_client.sql +20 -0
- package/migrations/0014_send_acts.sql +25 -0
- package/package.json +1 -1
- package/template/README.md +2 -0
- package/template/claude/CLAUDE.md +18 -14
- package/template/claude/skills/migrate-list/SKILL.md +5 -4
- package/docs/guides/01-connect-claude.md +0 -98
- package/docs/publishing/07-working-with-claude.md +0 -30
package/docs/README.md
CHANGED
|
@@ -4,7 +4,7 @@ These pages cover deploying and running your own Kestrel, from an empty reposito
|
|
|
4
4
|
|
|
5
5
|
## Get started
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
These are the steps to deploy a Kestrel instance and get it ready to send. Each one ends with a check, so you know it worked before you move on.
|
|
8
8
|
|
|
9
9
|
1. [Deploy the app](get-started/02-deploy.md)
|
|
10
10
|
1. [Lock the dashboard with Access](get-started/03-access.md)
|
|
@@ -16,7 +16,8 @@ Follow these steps, from an empty repository to a live newsletter. Each one ends
|
|
|
16
16
|
|
|
17
17
|
Each one stands on its own. Pick the ones you need.
|
|
18
18
|
|
|
19
|
-
- [
|
|
19
|
+
- [Allow Claude to connect](guides/01-allow-claude.md)
|
|
20
|
+
- [Automate with a service token](guides/02-automate.md)
|
|
20
21
|
- [Notifications through Cloudflare's email](guides/03-notifications.md)
|
|
21
22
|
- [Rate-limit the subscribe form](guides/05-rate-limit.md)
|
|
22
23
|
- [As your list grows](guides/09-as-your-list-grows.md)
|
|
@@ -63,7 +63,7 @@ Your instance's repository holds its configuration, its database migrations, and
|
|
|
63
63
|
npm run dev
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
-
Open http://localhost:8787/dashboard/, and run `npm run seed` in a second terminal to load a demo publication. Email goes to a stand-in, so nothing reaches a real inbox.
|
|
66
|
+
Open http://localhost:8787/dashboard/, and run `npm run seed` in a second terminal to load a demo publication. Email goes to a stand-in, so nothing reaches a real inbox. Press Ctrl+C to stop the server when you're done.
|
|
67
67
|
|
|
68
68
|
## 2. Sign in to Cloudflare
|
|
69
69
|
|
|
@@ -85,7 +85,7 @@ Every command from here on acts on your Cloudflare account through the Wrangler
|
|
|
85
85
|
|
|
86
86
|
## 3. Create the database and the image bucket
|
|
87
87
|
|
|
88
|
-
Each command creates the resource and writes
|
|
88
|
+
Each command creates the resource and writes it into the `production` block of `wrangler.jsonc`, under the binding the app already uses. Name `--env production` on each one, or Wrangler writes to the top level, which is local development.
|
|
89
89
|
|
|
90
90
|
1. Create the database:
|
|
91
91
|
|
|
@@ -93,29 +93,28 @@ Each command creates the resource and writes its id or name into the `production
|
|
|
93
93
|
npx wrangler d1 create kestrel-production --env production --binding DB --update-config
|
|
94
94
|
```
|
|
95
95
|
|
|
96
|
-
It prints the new database's settings, then a snippet it says to add to your configuration. Wrangler has already added it: it fills in the `database_id` of the production `DB` binding with the database's real id, in place of `REPLACE_WITH_PRODUCTION_D1_ID`, so there's nothing to paste.
|
|
97
|
-
|
|
98
96
|
1. Create the bucket for images:
|
|
99
97
|
|
|
100
98
|
```bash
|
|
101
99
|
npx wrangler r2 bucket create kestrel-media-production --env production --binding MEDIA --update-config
|
|
102
100
|
```
|
|
103
101
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
Wrangler rewrites the whole of `wrangler.jsonc` in its own layout when it saves: tabs, and one value per line. Your comments and settings stay; only the spacing changes.
|
|
107
|
-
|
|
108
|
-
Name `--env production` on every Wrangler command that creates or binds a resource. Without it, Wrangler writes to the top level, which is local development.
|
|
102
|
+
Each prints a snippet to add to your configuration. There's nothing to paste: `--update-config` has filled in the database's id, and the `MEDIA` binding already names the bucket. Wrangler also rewrites `wrangler.jsonc` in its own layout, with tabs and one value per line, and your settings stay.
|
|
109
103
|
|
|
110
104
|
## 4. Configure production
|
|
111
105
|
|
|
112
106
|
`wrangler.jsonc` holds two sets of settings. The top level is for local development, so leave it alone. The `production` block under `env` is yours to fill in.
|
|
113
107
|
|
|
114
|
-
1. Open `wrangler.jsonc` and change the values marked here in the `production` block.
|
|
108
|
+
1. Open `wrangler.jsonc` and change the values marked here in the `production` block. Only the settings you change are shown; leave the others as they are:
|
|
115
109
|
|
|
116
110
|
```jsonc
|
|
117
111
|
"production": {
|
|
118
|
-
"routes": [
|
|
112
|
+
"routes": [
|
|
113
|
+
{
|
|
114
|
+
"pattern": "newsletter.example.com", // ← your app's hostname
|
|
115
|
+
"custom_domain": true
|
|
116
|
+
}
|
|
117
|
+
],
|
|
119
118
|
"d1_databases": [
|
|
120
119
|
{
|
|
121
120
|
"binding": "DB",
|
|
@@ -136,24 +135,22 @@ Name `--env production` on every Wrangler command that creates or binds a resour
|
|
|
136
135
|
|
|
137
136
|
What each one does:
|
|
138
137
|
|
|
139
|
-
-
|
|
140
|
-
-
|
|
141
|
-
-
|
|
142
|
-
-
|
|
138
|
+
- `routes` puts the app on your hostname when you deploy. Cloudflare creates the DNS record and the certificate. Your domain must be in the same Cloudflare account, and the hostname can't already have a CNAME record; delete one if it does. The block also turns off the `workers.dev` addresses, so the app answers only on your hostname.
|
|
139
|
+
- `PROVIDER` stays empty until [Connect Amazon SES](04-ses.md) sets it. Until then the app runs without an email provider: posts can be written and previewed, but scheduling, test emails, and signups are refused. Never set it to `fake`, the top level's value, even when a wrangler warning suggests copying it: `fake` delivers nothing, and the app refuses it on your hostname.
|
|
140
|
+
- `ARCHIVE_BASE_PATH` is permanent once the first post goes out. Every post sent carries its `/archive/…` link, and changing the prefix later breaks the links already mailed. Keep `/archive` unless you have a reason not to.
|
|
141
|
+
- `FROM_ADDRESS` is the address every email comes from. Keep it on your sending hostname, and set `SENDING_DOMAIN` to the part after the `@`.
|
|
143
142
|
|
|
144
|
-
Change every `example.com` value, the route included
|
|
143
|
+
Change every `example.com` value, the route included. The app refuses to run while any of its settings still names `example.com`.
|
|
145
144
|
|
|
146
145
|
Every other setting has a working default. [Configuration](../reference/01-configuration.md) lists them all, and what the app refuses.
|
|
147
146
|
|
|
148
|
-
1. Check the file.
|
|
147
|
+
1. Check the file. It fails if the configuration doesn't parse:
|
|
149
148
|
|
|
150
149
|
```bash
|
|
151
150
|
npm run typecheck
|
|
152
151
|
```
|
|
153
152
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
1. Commit your settings and push them to your repository. An upgrade changes only the release your instance runs, so your settings stay as they are:
|
|
153
|
+
1. Commit your settings and push them to your repository:
|
|
157
154
|
|
|
158
155
|
```bash
|
|
159
156
|
git commit -am "Configure production"
|
|
@@ -202,7 +199,7 @@ This deploys the release your instance runs, with your settings. The output ends
|
|
|
202
199
|
|
|
203
200
|
In a browser, `https://newsletter.example.com/dashboard/` says the editor is locked until you set up Access. That's expected.
|
|
204
201
|
|
|
205
|
-
1. In a browser, `https://newsletter.example.com/` shows the newsletter's public
|
|
202
|
+
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.
|
|
206
203
|
|
|
207
204
|
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.
|
|
208
205
|
|
|
@@ -33,9 +33,9 @@ The editor lives under `/dashboard`, and the API under `/api`. One application c
|
|
|
33
33
|
| Path | What it covers |
|
|
34
34
|
| --- | --- |
|
|
35
35
|
| `dashboard` | the editor |
|
|
36
|
-
| `api` | everything the editor and Claude read and change: posts, sends, subscribers, settings,
|
|
36
|
+
| `api` | everything the editor and Claude read and change: posts, sends, subscribers, settings, the in-app docs, and the MCP server Claude Desktop and claude.ai connect to (`/api/mcp`) |
|
|
37
37
|
|
|
38
|
-
Each path covers everything under it. Leave every other path out. The
|
|
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
40
|
1. **(Optional)** Choose how long a sign-in lasts. The application's session duration defaults to 24 hours, so everyone signs in again each day. You can set anything from immediately to one month, as Cloudflare's [session management](https://developers.cloudflare.com/cloudflare-one/access-controls/access-settings/session-management/) describes. A longer session suits a publisher on their own computer, and a shorter one is safer on a shared one.
|
|
41
41
|
|
|
@@ -49,10 +49,12 @@ Every Access application denies everyone until a policy allows them.
|
|
|
49
49
|
- **Action:** Allow
|
|
50
50
|
- **Include:** the **Emails** selector, with your email address
|
|
51
51
|
|
|
52
|
-
1. Keep the login method a new Zero Trust organization starts with: you sign in with your Cloudflare account. To sign in another way, such as a one-time code by email, add it under **Zero Trust → Integrations → Identity providers** first.
|
|
53
|
-
|
|
54
52
|
1. Select **Create**.
|
|
55
53
|
|
|
54
|
+
Cloudflare's own login is on by default, and admits members of your Cloudflare account, so the email in your `Publishers` policy 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**.
|
|
55
|
+
|
|
56
|
+
The app accepts only two kinds of policy on this application. One is an Allow policy on people's emails, like this one. The other is a Service Auth policy on a service token, from [Automate with a service token](../guides/02-automate.md). A login through any other kind, such as a certificate, a device check, or an identity provider that gives no email, is refused with a `401`, and the editor says so.
|
|
57
|
+
|
|
56
58
|
## 4. Give the app its two values
|
|
57
59
|
|
|
58
60
|
The app verifies each login against your team domain, and checks that it was issued for this application.
|
|
@@ -61,7 +63,7 @@ The app verifies each login against your team domain, and checks that it was iss
|
|
|
61
63
|
|
|
62
64
|
1. Find the application's audience tag. Under **Access controls → Applications**, select **Configure** on your application. On the **Additional settings** tab, copy the **Application Audience (AUD) Tag**.
|
|
63
65
|
|
|
64
|
-
1. Store the team domain as a secret. The command prompts you for the value. Enter the hostname alone, without `https
|
|
66
|
+
1. Store the team domain as a secret. The command prompts you for the value. Enter the hostname alone, without `https://`:
|
|
65
67
|
|
|
66
68
|
```bash
|
|
67
69
|
npx wrangler secret put ACCESS_TEAM_DOMAIN --env production
|
|
@@ -73,7 +75,7 @@ The app verifies each login against your team domain, and checks that it was iss
|
|
|
73
75
|
npx wrangler secret put ACCESS_AUD --env production
|
|
74
76
|
```
|
|
75
77
|
|
|
76
|
-
A secret takes effect as soon as you store it, with no redeploy.
|
|
78
|
+
A secret takes effect as soon as you store it, with no redeploy.
|
|
77
79
|
|
|
78
80
|
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. Enter a comma-separated list of email addresses at the prompt:
|
|
79
81
|
|
|
@@ -92,7 +94,7 @@ The app verifies each login against your team domain, and checks that it was iss
|
|
|
92
94
|
1. In the same window, open `https://newsletter.example.com/api/whoami`. It shows you as a signed-in person, verified through Access:
|
|
93
95
|
|
|
94
96
|
```
|
|
95
|
-
{"principal":{"kind":"human","email":"you@example.com"},"auth":{"mode":"access"}}
|
|
97
|
+
{"principal":{"kind":"human","email":"you@example.com","client":null},"auth":{"mode":"access"}}
|
|
96
98
|
```
|
|
97
99
|
|
|
98
100
|
1. The public pages still need no login:
|
|
@@ -105,6 +107,6 @@ The app verifies each login against your team domain, and checks that it was iss
|
|
|
105
107
|
{"status":"ok","service":"kestrel"}
|
|
106
108
|
```
|
|
107
109
|
|
|
108
|
-
If the editor says the configuration needs a fix, store the secret it names. If Cloudflare lets you in but the editor answers `401`, check the two secrets for a typo, or `ACCESS_ALLOWED_EMAILS` may leave out your address.
|
|
110
|
+
If the editor says the configuration needs a fix, store the secret it names. The app needs both values, so while only one is stored, `/health` answers `500` naming the other, and the public pages say the newsletter is temporarily unavailable. If Cloudflare lets you in but the editor answers `401`, check the two secrets for a typo, or `ACCESS_ALLOWED_EMAILS` may leave out your address. If the editor says Access let you in but didn't say who you are, your login carried no email: sign out, and sign in through the `Publishers` policy.
|
|
109
111
|
|
|
110
112
|
Next, [connect Amazon SES](04-ses.md).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Connect Amazon SES
|
|
2
2
|
|
|
3
|
-
Kestrel sends the newsletter's posts through Amazon SES, Amazon's email service. SES only delivers the mail: Kestrel keeps the list, each subscriber's consent, and the record of every send. AWS reviews your account before it lets you send to anyone but yourself. This page asks for that early, and you test in the sandbox while you wait.
|
|
3
|
+
Kestrel sends the newsletter's posts through Amazon SES, Amazon's email service. SES only delivers the mail: Kestrel keeps the list, each subscriber's consent, and the record of every send. SES prices every email alike, with no split between transactional and marketing mail, and costs the least at every list size above a tiny one. AWS reviews your account before it lets you send to anyone but yourself. This page asks for that early, and you test in the sandbox while you wait.
|
|
4
4
|
|
|
5
5
|
In this guide, you verify your sending hostname with SES and add a DMARC record. You forward readers' replies to the publisher's inbox, and ask AWS for production access. You also route bounces and complaints to the app through SNS. Then you give the app its AWS credentials, set the provider, and deploy.
|
|
6
6
|
|
|
@@ -227,7 +227,7 @@ Until now the app has run with no email provider connected, refusing to send. Se
|
|
|
227
227
|
}
|
|
228
228
|
```
|
|
229
229
|
|
|
230
|
-
On Workers Paid, also add `"SUBREQUEST_BUDGET": "10000"`, the plan's limit, so each minute sends more. Leave it out on Workers Free. [As your list grows](../guides/09-as-your-list-grows.md) says
|
|
230
|
+
On Workers Paid, also add `"SUBREQUEST_BUDGET": "10000"`, the plan's limit, so each minute sends more. Leave it out on Workers Free, which suits a list of up to about 1,000 subscribers. [As your list grows](../guides/09-as-your-list-grows.md) says why.
|
|
231
231
|
|
|
232
232
|
When AWS grants production access, set `SES_MAX_SEND_RATE` to the maximum send rate on the **Account dashboard**, and deploy again. The app keeps to that rate, so SES doesn't throttle your send.
|
|
233
233
|
|
|
@@ -77,7 +77,7 @@ Every email carries an unsubscribe link, and a header that lets mail clients sho
|
|
|
77
77
|
|
|
78
78
|
## 6. Check that bounces and complaints come back
|
|
79
79
|
|
|
80
|
-
When an address bounces or marks your mail as spam, SES tells the app through SNS. The app then stops mailing that address. SES has test addresses that act out each case without reaching a real person, and the sandbox lets you mail them.
|
|
80
|
+
When an address bounces or marks your mail as spam, SES tells the app through SNS. The app then stops mailing that address: a bounce suppresses it, and a complaint unsubscribes it. SES has test addresses that act out each case without reaching a real person, and the sandbox lets you mail them.
|
|
81
81
|
|
|
82
82
|
1. Open `https://newsletter.example.com/subscribe`, and subscribe each test address:
|
|
83
83
|
|
|
@@ -86,9 +86,9 @@ When an address bounces or marks your mail as spam, SES tells the app through SN
|
|
|
86
86
|
|
|
87
87
|
Each one gets a confirmation email, which bounces or is marked as spam.
|
|
88
88
|
|
|
89
|
-
1. In the editor, **Subscribers** lists
|
|
89
|
+
1. In the editor, **Subscribers** lists the bounce address as **Pending** with a **bounced** flag, and the complaint address as **Unsubscribed**. Open its row: its page says it left by a complaint. The app mails neither again.
|
|
90
90
|
|
|
91
|
-
If
|
|
91
|
+
If the bounce address has no flag, or the complaint address is still pending, SES's events aren't reaching the app. Check the configuration set and the SNS subscription in [Connect Amazon SES](04-ses.md#6-send-bounces-and-complaints-to-sns).
|
|
92
92
|
|
|
93
93
|
## 7. Read the logs
|
|
94
94
|
|
|
@@ -122,7 +122,7 @@ The checks leave test addresses on the list. Erase them, so the publisher starts
|
|
|
122
122
|
|
|
123
123
|
1. Erase your second address too, if it won't be a reader.
|
|
124
124
|
|
|
125
|
-
Erasing keeps
|
|
125
|
+
Erasing keeps only the address, so it's never mailed again, but the person can subscribe again through the subscribe page. Erasing your own address doesn't lock you out.
|
|
126
126
|
|
|
127
127
|
When every section gives the result it describes, your instance is ready for subscribers.
|
|
128
128
|
|
|
@@ -18,8 +18,9 @@ If someone else is the publisher, let them sign in first:
|
|
|
18
18
|
|
|
19
19
|
Each of these guides adds one thing, and you can come back to any of them later.
|
|
20
20
|
|
|
21
|
-
- [
|
|
21
|
+
- [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.
|
|
22
|
+
- [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.
|
|
22
23
|
- [Notifications through Cloudflare's email](../guides/03-notifications.md), so problems are reported even when the email provider has stopped sending.
|
|
23
24
|
- [Rate-limit the subscribe form](../guides/05-rate-limit.md), before the subscribe link is shared widely.
|
|
24
|
-
- [As your list grows](../guides/09-as-your-list-grows.md), from about
|
|
25
|
+
- [As your list grows](../guides/09-as-your-list-grows.md), from about 1,000 subscribers.
|
|
25
26
|
- [Upgrade to a new release](../guides/07-upgrade.md), when one comes out.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Allow Claude to connect
|
|
2
|
+
|
|
3
|
+
Connecting Claude to the instance lets the publisher draft, test, and schedule posts by asking for them in Claude. Claude works through the same API as the dashboard, and its changes show as the publisher's, via Claude.
|
|
4
|
+
|
|
5
|
+
In this guide, you turn on Cloudflare's Managed OAuth for the Access application that locks the dashboard, then connect Claude, or send the publisher the address to connect to.
|
|
6
|
+
|
|
7
|
+
To run a script or scheduled job with no one present, use a service token instead, as [Automate with a service token](02-automate.md) describes.
|
|
8
|
+
|
|
9
|
+
## Before you begin
|
|
10
|
+
|
|
11
|
+
You need:
|
|
12
|
+
|
|
13
|
+
- The dashboard locked with Access, from [Lock the dashboard with Access](../get-started/03-access.md).
|
|
14
|
+
- A deployed instance. Claude Desktop and claude.ai reach it from Anthropic's servers, so they can't reach one running on your own computer.
|
|
15
|
+
|
|
16
|
+
## 1. Turn on Managed OAuth
|
|
17
|
+
|
|
18
|
+
Managed OAuth lets an app such as Claude act with a person's account, the way the editor does in their browser. Cloudflare marks it as beta, and its [Managed OAuth](https://developers.cloudflare.com/cloudflare-one/access-controls/applications/http-apps/managed-oauth/) page describes it.
|
|
19
|
+
|
|
20
|
+
1. In **Zero Trust**, go to **Access controls → Applications**, and select **Edit** on your application.
|
|
21
|
+
|
|
22
|
+
1. On the **Additional settings** tab, select the **OAuth** filter, and find **Managed OAuth (Beta)**.
|
|
23
|
+
|
|
24
|
+
1. Turn on **Managed OAuth**.
|
|
25
|
+
|
|
26
|
+
1. Turn on **Allow localhost clients** and **Allow loopback clients**. Claude Code receives the person's approval on their own computer, at a `localhost` or `127.0.0.1` address.
|
|
27
|
+
|
|
28
|
+
1. Under **Allowed redirect URIs**, add Claude's address, so Claude Desktop and claude.ai can connect:
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
https://claude.ai/api/mcp/auth_callback
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Access accepts Claude only with an address listed here. Leave it out if no one will connect Claude Desktop or claude.ai.
|
|
35
|
+
|
|
36
|
+
1. Set the **Grant session duration** to 1 week. A connection renews itself while it's in use until this ends, and then Claude asks the person to approve it again. Cloudflare recommends one to two weeks for a command-line tool.
|
|
37
|
+
|
|
38
|
+
1. Save the application.
|
|
39
|
+
|
|
40
|
+
1. Check that a request with no sign-in now says where an app signs in:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
curl -si https://newsletter.example.com/api/whoami
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
It answers `401`, and its `www-authenticate` line starts with `Bearer` and names `resource_metadata`. Without Managed OAuth, Cloudflare answers with a redirect to its login page instead. A browser still gets the login page either way.
|
|
47
|
+
|
|
48
|
+
## 2. Connect Claude, or send the publisher the address
|
|
49
|
+
|
|
50
|
+
The publisher connects at the instance's address followed by `/api/mcp`:
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
https://newsletter.example.com/api/mcp
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
If you're the publisher, connect now, as [Connect Claude](../publishing/08-connect-claude.md) describes. If someone else is, send them that address and a link to the same page.
|
|
57
|
+
|
|
58
|
+
## Who can connect
|
|
59
|
+
|
|
60
|
+
Anyone the `Publishers` policy lets into the dashboard can connect Claude, since they sign in the same way. To stop someone connecting, remove them from that policy. If you set `ACCESS_ALLOWED_EMAILS`, store the list again without them: the app checks it on every request, so it takes effect at once.
|
|
61
|
+
|
|
62
|
+
## Check it
|
|
63
|
+
|
|
64
|
+
1. Connect Claude Desktop or claude.ai, as [Connect Claude](../publishing/08-connect-claude.md) describes. The connector lists Kestrel's tools, such as **Get the publication** and **Save a draft**.
|
|
65
|
+
|
|
66
|
+
1. Ask Claude what the publication is called. It answers with the name the dashboard shows.
|
|
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.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Automate with a service token
|
|
2
|
+
|
|
3
|
+
A service token lets a script or scheduled job call the API, such as a job that drafts a weekly digest. Kestrel labels these changes **a service token**, rather than with a person's name.
|
|
4
|
+
|
|
5
|
+
In this guide, you create a service token, allow it in through the Access application, give it to your script, and check that it works. To connect Claude as yourself instead, follow [Allow Claude to connect](01-allow-claude.md).
|
|
6
|
+
|
|
7
|
+
## Before you begin
|
|
8
|
+
|
|
9
|
+
You need:
|
|
10
|
+
|
|
11
|
+
- The dashboard locked with Access, from [Lock the dashboard with Access](../get-started/03-access.md).
|
|
12
|
+
- The script or job that will call the API, and somewhere it keeps secrets.
|
|
13
|
+
|
|
14
|
+
## 1. Create a service token
|
|
15
|
+
|
|
16
|
+
A service token is a Client ID and a Client Secret that a program sends with each request. It carries no email, so the app tells it apart from a person's sign-in.
|
|
17
|
+
|
|
18
|
+
1. In **Zero Trust**, go to **Access controls → Service credentials → Service Tokens**, and select **Create Service Token**.
|
|
19
|
+
|
|
20
|
+
1. Name it for what will use it, such as `kestrel-automation`, and choose a **Service Token Duration**. A year is a good default.
|
|
21
|
+
|
|
22
|
+
1. Select **Generate token**.
|
|
23
|
+
|
|
24
|
+
1. Copy the **Client ID** and the **Client Secret**. Cloudflare shows the secret only once.
|
|
25
|
+
|
|
26
|
+
## 2. Allow the token in
|
|
27
|
+
|
|
28
|
+
The application lets in only the people its `Publishers` policy names, so the token needs a policy of its own on the same application.
|
|
29
|
+
|
|
30
|
+
1. Under **Access controls → Applications**, select **Configure** on the application from [Lock the dashboard with Access](../get-started/03-access.md#2-create-the-access-application).
|
|
31
|
+
|
|
32
|
+
1. Add a policy:
|
|
33
|
+
|
|
34
|
+
- **Policy name:** `Automation`
|
|
35
|
+
- **Action:** Service Auth
|
|
36
|
+
- **Include:** the **Service Token** selector, with your token
|
|
37
|
+
|
|
38
|
+
1. Save the application.
|
|
39
|
+
|
|
40
|
+
This policy is the only thing that lets the token in. `ACCESS_ALLOWED_EMAILS` doesn't apply to it, since it has no email.
|
|
41
|
+
|
|
42
|
+
## 3. Give the token to your script
|
|
43
|
+
|
|
44
|
+
The script sends the two values as headers on every request: `CF-Access-Client-Id` with the Client ID, and `CF-Access-Client-Secret` with the Client Secret.
|
|
45
|
+
|
|
46
|
+
1. Store the Client ID and the Client Secret where your script reads secrets, such as its environment, your CI's secrets, or a password manager. Never commit the secret to a repository.
|
|
47
|
+
|
|
48
|
+
1. Have the script send both headers with every request to `https://newsletter.example.com/api/…`. For example, this request saves a new draft:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
curl -s https://newsletter.example.com/api/posts \
|
|
52
|
+
-H "CF-Access-Client-Id: $CF_ACCESS_CLIENT_ID" \
|
|
53
|
+
-H "CF-Access-Client-Secret: $CF_ACCESS_CLIENT_SECRET" \
|
|
54
|
+
-H "Content-Type: application/json" \
|
|
55
|
+
-d '{"subject":"Weekly digest","markdown":"# This week\n\nThree things worth reading."}'
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Check it
|
|
59
|
+
|
|
60
|
+
1. Call `GET /api/whoami` with the two headers. The app answers with a program's identity, not a person's:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
curl -s https://newsletter.example.com/api/whoami \
|
|
64
|
+
-H "CF-Access-Client-Id: $CF_ACCESS_CLIENT_ID" \
|
|
65
|
+
-H "CF-Access-Client-Secret: $CF_ACCESS_CLIENT_SECRET"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
{"principal":{"kind":"service","token_id":"e367c0ffee5eb4.access","client":null},"auth":{"mode":"access"}}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The `token_id` is the Client ID Zero Trust shows beside your token. If it's a different one, the script is using another token.
|
|
73
|
+
|
|
74
|
+
1. Call `GET /api/posts`. It answers with the posts the editor shows.
|
|
75
|
+
|
|
76
|
+
1. Open a draft in the editor, and have the script save a change to it. The editor shows that the draft was changed elsewhere, last edited by **a service token**.
|
|
77
|
+
|
|
78
|
+
If the script gets a `401`, a `403`, or a login page, check the policy from section 2, and the token's two values. The policy's action must be Service Auth, since the app refuses a login through any other kind of policy with a `401` whose `reason` is `access_unidentified`. Check that the token hasn't expired, too.
|
|
79
|
+
|
|
80
|
+
## Renew or revoke a token
|
|
81
|
+
|
|
82
|
+
A service token stops working when its duration ends. Before then, extend it on the token under **Access controls → Service credentials → Service Tokens**, or create a new token, add it to the `Automation` policy, give it to the script, and then delete the old one.
|
|
83
|
+
|
|
84
|
+
To stop a script at once, delete its token there. Its next request is refused.
|
|
85
|
+
|
|
86
|
+
## More information
|
|
87
|
+
|
|
88
|
+
The API reference describes every route: its method and path, the body it takes, its refusals, and an example. The editor's **API** tab shows it, and a program reads it as Markdown: `GET /api/reference.md` is the index, and `GET /api/reference/<resource>.md` holds one resource's routes in full. `GET /api/openapi.json` is the same reference as an OpenAPI 3.1 document, for a tool that imports one.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# As your list grows
|
|
2
2
|
|
|
3
|
-
A new instance runs on Cloudflare's free Workers plan with your starting Amazon SES limits.
|
|
3
|
+
A new instance runs on Cloudflare's free Workers plan with your starting Amazon SES limits. Workers Free suits a list of up to about 1,000 subscribers. Past about 5,000, one send can use up the day's database writes and stop the newsletter until midnight UTC. Two limits matter as the list grows: how fast SES lets you send, and how many database writes the free plan allows a day.
|
|
4
4
|
|
|
5
5
|
In this guide, you raise your SES limits, which helps on either plan, and upgrade to Workers Paid, which a larger list needs. Then you see how long a send takes after each.
|
|
6
6
|
|
|
@@ -10,7 +10,7 @@ SES caps how many emails your account sends a day, and how many a second. A send
|
|
|
10
10
|
|
|
11
11
|
1. Ask AWS for a higher daily quota and a maximum send rate of 50, as AWS's [Increasing your Amazon SES sending quotas](https://docs.aws.amazon.com/ses/latest/dg/manage-sending-quotas-request-increase.html) describes.
|
|
12
12
|
|
|
13
|
-
1. When AWS grants it, the SES **Account dashboard** shows your new rate. In `wrangler.jsonc`, set the `production` block's
|
|
13
|
+
1. When AWS grants it, the SES **Account dashboard** shows your new rate. In `wrangler.jsonc`, set the `production` block's `SES_MAX_SEND_RATE` to match, as [Configuration](../reference/01-configuration.md#vars) describes.
|
|
14
14
|
|
|
15
15
|
1. Commit the change, and push it:
|
|
16
16
|
|
|
@@ -27,11 +27,13 @@ SES caps how many emails your account sends a day, and how many a second. A send
|
|
|
27
27
|
|
|
28
28
|
## 2. Upgrade to Workers Paid
|
|
29
29
|
|
|
30
|
-
On the free plan, [D1](https://developers.cloudflare.com/d1/platform/pricing/) allows
|
|
30
|
+
On the free plan, [D1](https://developers.cloudflare.com/d1/platform/pricing/) allows 100,000 written rows a day, reset at midnight UTC. A send writes about 14 rows a recipient, receipts included, and an import about 7 for each new subscriber. So at 1,000 subscribers, an import of the whole list and two sends on one day use about a third of the day. Once the day's rows are spent, the whole app stops until midnight UTC, the SES webhook with it, so that day's bounces and complaints are lost, and suppress or unsubscribe no one.
|
|
31
|
+
|
|
32
|
+
It's recommended to upgrade from about 1,000 subscribers. Past that, the Dashboard and the Subscribers screen say so, and so does an import that would take the list past it. Past about 5,000, a send that could use up the day on its own also warns before it's scheduled. The app doesn't count the day's writes, so between the two, two large sends or several imports and a send on one UTC day can still use it up without a warning at that moment.
|
|
31
33
|
|
|
32
34
|
1. Upgrade your Cloudflare account to the $5 [Workers Paid plan](https://developers.cloudflare.com/workers/platform/pricing/), which has no daily limit on writes.
|
|
33
35
|
|
|
34
|
-
1. In `wrangler.jsonc`, add `"SUBREQUEST_BUDGET": "10000"` to the `production` block's `vars`. It tells the app it's on Workers Paid, so each minute sends far more
|
|
36
|
+
1. In `wrangler.jsonc`, add `"SUBREQUEST_BUDGET": "10000"` to the `production` block's `vars`. It tells the app it's on Workers Paid, so each minute sends far more. [Configuration](../reference/01-configuration.md#vars) describes `SUBREQUEST_BUDGET`.
|
|
35
37
|
|
|
36
38
|
1. Commit the change, and push it:
|
|
37
39
|
|
|
@@ -46,6 +48,8 @@ On the free plan, [D1](https://developers.cloudflare.com/d1/platform/pricing/) a
|
|
|
46
48
|
npx wrangler deploy --env production
|
|
47
49
|
```
|
|
48
50
|
|
|
51
|
+
Over years, one more limit can matter: on the free plan, one [D1 database](https://developers.cloudflare.com/d1/platform/limits/) holds at most 500 MB, and every send keeps a row for each recipient in its record. Workers Paid raises it to 10 GB.
|
|
52
|
+
|
|
49
53
|
## How long a send takes
|
|
50
54
|
|
|
51
55
|
| Setup | 1,000 subscribers | 5,000 subscribers | 10,000 subscribers |
|