Add the bool.email battery - #23
Closed
max-bader wants to merge 1 commit into
Closed
Conversation
Collaborator
|
shelving for now |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds the second SDK battery: a deployed app can send email with no SMTP config, no
provider account, and no API key in the bundle.
Calls route through the gateway's email plane (
/_bool/v1/email/send), whichrenders the message, meters the owner's app-credit pool, and queues it in a
durable outbox — so once
sendresolves, the platform keeps retrying deliveryserver-side. Platform side: codehs/bool#574.
Recipients are restricted, and that's the feature
Only two kinds of recipient work, enforced server-side:
to: "owner";An address a visitor merely typed into a form is refused with
recipient_not_allowed. An app that could email anyone would make Bool's sendingdomain a spam relay, and the cost of that lands on every other app's
deliverability — including platform password-reset mail.
The
"owner"sentinel exists because the client has no way to know the owner'saddress, and the alternative — a builder pasting their own email into app source —
publishes it in every visitor's browser bundle. It resolves server-side.
There's also no HTML body, no attachments and no custom
from: app-authoredmarkup sent from a Bool-signed domain would be a ready-made phishing kit, so every
message renders into one fixed template carrying the app's name.
Notes on the implementation
client.tsnext tobool.ai, sharing its header helper (renamedto cover both batteries) — same transport, same
credentials: "include"+viewer/eu-session identity replay as the db, users and ai planes.
recipient rule, body limits and https-only button URLs. A second copy of those
rules here would drift from it and, worse, would reject things a newer gateway
allows. What the client owes the caller is a clear error instead.
BoolEmailErrorcarriesdetail— the gateway's human-readableexplanation — on top of
status/code.recipient_not_allowedis the one 4xxa builder will hit constantly while building, and swallowing the explanation
would turn it into a support ticket.
{ duplicate: true }(anidempotencyKeymatched an existing send) and{ suppressed: true }(the address hard-bounced or reported spam, so theplatform's do-not-mail list dropped it) resolve normally rather than throwing —
neither is the app's bug, and throwing would crash an app over one dead address.
Drive-by fix
BoolAiError's docs still promisedout_of_ai_credits, which the platform'srename of the battery pool to app credits ("juice") broke — its own comment noted
the SDK needed a matching release. Now documented as
out_of_app_credits, with anote to branch on
status === 402if you need to cover older gateways too.Testing
bun test161 pass,tsc --noEmitclean,bun run buildclean. 9 new casescovering the transport, verbatim field pass-through, the owner sentinel never
being resolved client-side, typed errors (including a non-JSON error body), the
duplicate/suppressed results, and viewer-token replay.
Ships as
0.2.0-next.26on the canary channel. Not publishable until theplatform PR merges — the plane 404s without it, and the
bool-emailworkspaceflag is off by default.
🤖 Generated with Claude Code