@kurtbruns/kestrel 1.15.1 → 1.17.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 CHANGED
@@ -10,6 +10,36 @@ To move a running instance from one version to another, follow [Upgrade to a new
10
10
 
11
11
  <!-- Add entries under Added / Changed / Fixed / Breaking. One operator-facing line each; see .claude/rules/changelog.md. Upgrade steps (a migration, a new setting) go once, in the Upgrading paragraph, never on an entry. -->
12
12
 
13
+ ## [1.17.0] - 2026-10-26
14
+
15
+ ### Changed
16
+
17
+ - The setup guide's Connect Amazon SES page is rewritten for a newcomer. It stores each secret as soon as its value exists, makes the custom MAIL FROM domain a regular step, and checks that the From address has no inbox before forwarding it. The section on switching an instance that already sends is gone, and its caution moves to Configuration's `PROVIDER`. Its sections are renumbered, so a bookmark to one now lands at the page's top. Verify it works, Deploy the app, and Lock the dashboard with Access are tightened to match, and Verify's Send now steps follow the editor's order.
18
+ - The Troubleshooting docs page is organized around what you'd ask (a send is held, a reader never got a post, someone may have been mailed twice) instead of listing each on-screen message, since the screen already says each fix. It keeps the fixes made outside Kestrel, in Amazon SES or Cloudflare, and the decisions that are yours.
19
+
20
+ ## [1.16.0] - 2026-10-26
21
+
22
+ **Upgrading from 1.15.1.** At the top level of your instance's `wrangler.jsonc`, outside `env`, add `"workers_dev": false` and `"preview_urls": false`, and change `triggers` to `{ "crons": [] }`, as a new instance now has, so a deploy without `--env production` answers on no address and never runs the send sweep. Keep the empty list rather than deleting the line, since only a list clears a schedule, and keep the `* * * * *` cron in `production`'s own `triggers`. If you ever deployed without `--env production`, that Worker is still live: remove it with `npx wrangler delete --name kestrel`. A log filter or alert on `auth.service` finds nothing now: filter on `principal` = `service` instead. Run `npm run check-context` in your instance's repository: this release changes the operator's Claude context, and a copy you've edited is merged by hand.
23
+
24
+ ### Added
25
+
26
+ - Two new log lines: `send.scheduled` (info) for each schedule and Send now, with `act` saying which, and `send.resolved` (warn) for each Resolve of a wedged send, with the resolution and how many recipients it settled. A failed `mcp.call` now carries `code`, the refusal code of the route that turned it down, so failed tool calls can be filtered (SPEC §12).
27
+
28
+ ### Changed
29
+
30
+ - The operator's Claude context (`.claude/CLAUDE.md`) says how to put images in a post: upload each from a file on disk, since a pasted image isn't one, and reference it by its filename with alt text, never by the upload's URL, which can leave a sent post with a broken image. Merge it into your instance's copy; `kestrel check-context` shows the change.
31
+ - A new instance's development settings now answer on no address and run no schedule if deployed by mistake, without `--env production`, so a deploy that forgets it can't put the email stand-in and the local sign-in on a public address, or run the send sweep through the stand-in. The setup guide's Deploy step says how to remove such a deploy.
32
+ - A safety caution in the instance's Claude context: never deploy or store a secret without `--env production`, keep the top level's addresses and schedule off, and how to remove a deploy that went out without it, with the person's agreement.
33
+ - The setup guide's logs section now says that a search over a long range may show only a sample of the lines, how to narrow it to follow one send, and that on the Free plan a very large send can leave the rest of the day unlogged once Cloudflare's daily allowance is used up.
34
+
35
+ ### Fixed
36
+
37
+ - An image referenced by its full address on your instance, as an upload answers it, is now checked like one referenced by name: the preview, test, and schedule say to use the file name, and warn when the image is another post's or has been deleted, instead of staying silent while the sent email carried a broken image (SPEC §4).
38
+
39
+ ### Breaking
40
+
41
+ - The app no longer logs `auth.service` on every request a service token makes, since Cloudflare's own request and Access logs already record each one. Instead every log line for a publisher's action, the new `send.scheduled` and `send.resolved` included, says who took it: `principal` (`human` or `service`) and, for a person, `client` (`editor`, `claude-code`, `claude`, or the name a script gives), never the email or the token (SPEC §12).
42
+
13
43
  ## [1.15.1] - 2026-10-23
14
44
 
15
45
  ### Changed
@@ -631,7 +661,9 @@ The first tagged release: a self-contained newsletter app on a Cloudflare Worker
631
661
  - A build-version stamp: every instance reports its version, commit, and build time, read-only in the editor and at `GET /api/version`.
632
662
  - This changelog and a semantic-versioning release process.
633
663
 
634
- [Unreleased]: https://github.com/kurtbruns/kestrel/compare/v1.15.1...HEAD
664
+ [Unreleased]: https://github.com/kurtbruns/kestrel/compare/v1.17.0...HEAD
665
+ [1.17.0]: https://github.com/kurtbruns/kestrel/releases/tag/v1.17.0
666
+ [1.16.0]: https://github.com/kurtbruns/kestrel/releases/tag/v1.16.0
635
667
  [1.15.1]: https://github.com/kurtbruns/kestrel/releases/tag/v1.15.1
636
668
  [1.15.0]: https://github.com/kurtbruns/kestrel/releases/tag/v1.15.0
637
669
  [1.14.0]: https://github.com/kurtbruns/kestrel/releases/tag/v1.14.0
@@ -1,8 +1,8 @@
1
1
  /*
2
2
  * The local send sweep, on production's schedule, and the simulated receipts on theirs.
3
3
  *
4
- * Deployed, a Cron Trigger (`* * * * *` in wrangler.jsonc) runs the send sweep once a
5
- * minute, on the minute: it fires due sends and resumes interrupted and halted ones.
4
+ * Deployed, a Cron Trigger (`* * * * *` in an instance's `production`) runs the send sweep
5
+ * once a minute, on the minute: it fires due sends and resumes interrupted and halted ones.
6
6
  * `wrangler dev` never fires a cron on its own; it only exposes a URL that runs the
7
7
  * scheduled handler once. Without something calling it, a local send scheduled for 10:05 is
8
8
  * never sent and a paused send never resumes.
@@ -23,7 +23,7 @@
23
23
  const SCHEDULED_PATH = "/cdn-cgi/local/scheduled";
24
24
  const MINUTE = 60_000;
25
25
 
26
- /** The deployed sweep's cron (wrangler.jsonc's trigger). */
26
+ /** The deployed sweep's cron (the `production` trigger in template/wrangler.jsonc). */
27
27
  const SWEEP_CRON = "* * * * *";
28
28
 
29
29
  /** The cron value the Worker reads as "settle simulated receipts only": `RECEIPTS_CRON` in
@@ -367,7 +367,7 @@
367
367
  </div>
368
368
  </div>
369
369
  </div>
370
- </section>`}function tl(e,t){let{state:n}=t,s=t.data.settings.confirmation_email_default,i={subject:c("#ceSubject",e),message:c("#ceMessage",e),button:c("#ceButton",e),footer:c("#ceFooter",e)},a={avatar:c("#cePvAvatar",e),from:c("#cePvFrom",e),subject:c("#cePvSubject",e),body:c("#cePvBody",e),button:c("#cePvButton",e),foot:c("#cePvFoot",e),mast:c("#cePvMast",e),mastLogo:c("#cePvMastLogo",e),mastName:c("#cePvMastName",e),mastTag:c("#cePvMastTag",e)},r=()=>{let p=n.confirmation;a.avatar.textContent=Rn(n.name,t.fromName),a.from.textContent=n.name||t.fromName,a.subject.textContent=p.subject.trim()||s.subject||"",a.body.textContent=p.body.trim()||s.body||"",a.button.textContent=p.button_label.trim()||s.button_label||"Confirm",a.foot.textContent=p.reassurance,a.foot.hidden=!p.reassurance.trim();let v=n.name||t.fromDisplay,x=!!n.logoUrl,M=x||!!v;a.mast.hidden=!M,M&&(a.mastLogo.hidden=!x,x&&h(a.mastLogo,o`<img src="${n.logoUrl}" alt="">`),a.mastName.textContent=v,a.mastName.hidden=!v,a.mastTag.textContent=n.tagline,a.mastTag.hidden=!n.tagline)},l=()=>{n.confirmation.subject=i.subject.value,n.confirmation.body=i.message.value,n.confirmation.button_label=i.button.value,n.confirmation.reassurance=i.footer.value,t.changed()};for(let p of Object.values(i))p.addEventListener("input",l);let d=c("#ceEditBody",e),u=c("#cePreviewBody",e),f=c("#ceTabEdit",e),S=c("#ceTabPreview",e),b=p=>{d.hidden=!p,u.hidden=p,f.setAttribute("aria-pressed",String(p)),S.setAttribute("aria-pressed",String(!p)),p&&i.subject.focus()};return S.onclick=()=>b(!1),f.onclick=()=>b(!0),c("#ceToPreview",e).onclick=()=>b(!1),c("#ceReset",e).onclick=()=>{i.subject.value=s.subject||"",i.message.value=s.body||"",i.button.value=s.button_label||"",i.footer.value=s.reassurance||"",l()},r(),{repaint:r,reset(){i.subject.value=n.confirmation.subject,i.message.value=n.confirmation.body,i.button.value=n.confirmation.button_label,i.footer.value=n.confirmation.reassurance,r()}}}var Fu={none:"No email provider connected",fake:"Fake (dev, dead-end)",ses:"Amazon SES"};function Os(e){return Fu[e]||e}function qu(e){return e.provider==="none"?o`${Os(e.provider)}. ${Ae("set-link")} to start sending.`:o`${Os(e.provider)}${e.simulation?o`<span class="set-pill">Simulating ${ju[e.simulation.profile]}${e.simulation.faults==="none"?", no failures":""}</span>`:null}`}var ju={generic:"a generic provider",ses:"Amazon SES"},Wu="#/docs/ses/3-receive-replies-at-your-from-address";function Vu(e){let t=String(e||"").match(/<([^>]+)>/);return t?.[1]?t[1]:String(e||"")}function nl(e){let t=e.data.deployment,{fromName:n}=e;return o`
370
+ </section>`}function tl(e,t){let{state:n}=t,s=t.data.settings.confirmation_email_default,i={subject:c("#ceSubject",e),message:c("#ceMessage",e),button:c("#ceButton",e),footer:c("#ceFooter",e)},a={avatar:c("#cePvAvatar",e),from:c("#cePvFrom",e),subject:c("#cePvSubject",e),body:c("#cePvBody",e),button:c("#cePvButton",e),foot:c("#cePvFoot",e),mast:c("#cePvMast",e),mastLogo:c("#cePvMastLogo",e),mastName:c("#cePvMastName",e),mastTag:c("#cePvMastTag",e)},r=()=>{let p=n.confirmation;a.avatar.textContent=Rn(n.name,t.fromName),a.from.textContent=n.name||t.fromName,a.subject.textContent=p.subject.trim()||s.subject||"",a.body.textContent=p.body.trim()||s.body||"",a.button.textContent=p.button_label.trim()||s.button_label||"Confirm",a.foot.textContent=p.reassurance,a.foot.hidden=!p.reassurance.trim();let v=n.name||t.fromDisplay,x=!!n.logoUrl,M=x||!!v;a.mast.hidden=!M,M&&(a.mastLogo.hidden=!x,x&&h(a.mastLogo,o`<img src="${n.logoUrl}" alt="">`),a.mastName.textContent=v,a.mastName.hidden=!v,a.mastTag.textContent=n.tagline,a.mastTag.hidden=!n.tagline)},l=()=>{n.confirmation.subject=i.subject.value,n.confirmation.body=i.message.value,n.confirmation.button_label=i.button.value,n.confirmation.reassurance=i.footer.value,t.changed()};for(let p of Object.values(i))p.addEventListener("input",l);let d=c("#ceEditBody",e),u=c("#cePreviewBody",e),f=c("#ceTabEdit",e),S=c("#ceTabPreview",e),b=p=>{d.hidden=!p,u.hidden=p,f.setAttribute("aria-pressed",String(p)),S.setAttribute("aria-pressed",String(!p)),p&&i.subject.focus()};return S.onclick=()=>b(!1),f.onclick=()=>b(!0),c("#ceToPreview",e).onclick=()=>b(!1),c("#ceReset",e).onclick=()=>{i.subject.value=s.subject||"",i.message.value=s.body||"",i.button.value=s.button_label||"",i.footer.value=s.reassurance||"",l()},r(),{repaint:r,reset(){i.subject.value=n.confirmation.subject,i.message.value=n.confirmation.body,i.button.value=n.confirmation.button_label,i.footer.value=n.confirmation.reassurance,r()}}}var Fu={none:"No email provider connected",fake:"Fake (dev, dead-end)",ses:"Amazon SES"};function Os(e){return Fu[e]||e}function qu(e){return e.provider==="none"?o`${Os(e.provider)}. ${Ae("set-link")} to start sending.`:o`${Os(e.provider)}${e.simulation?o`<span class="set-pill">Simulating ${ju[e.simulation.profile]}${e.simulation.faults==="none"?", no failures":""}</span>`:null}`}var ju={generic:"a generic provider",ses:"Amazon SES"},Wu="#/docs/ses/9-receive-replies-at-your-from-address";function Vu(e){let t=String(e||"").match(/<([^>]+)>/);return t?.[1]?t[1]:String(e||"")}function nl(e){let t=e.data.deployment,{fromName:n}=e;return o`
371
371
  <section class="set-sec">
372
372
  ${we("Email sender",ye("readonly","Read-only \xB7 set at deploy"))}
373
373
  <div class="set-card sunken">