Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
c616767
feat(integrations): `integrations connect` and `integrations connecti…
thedogwiththedataonit Oct 17, 2026
a46351a
feat(init): provider switch for Supabase and Clerk, brand from --app-…
thedogwiththedataonit Oct 17, 2026
53c9e2e
feat(doctor): `doctor --project` checks an app against its brew.json
thedogwiththedataonit Oct 17, 2026
dde08c4
docs: init providers, integrations connect and doctor --project in RE…
thedogwiththedataonit Oct 17, 2026
b062494
chore: 0.21.0 after #61's 0.20.0 (main published 0.17.0)
thedogwiththedataonit Oct 20, 2026
ccdfa6d
fix(integrations,init): adapt to #2169: idempotent connection, spec p…
thedogwiththedataonit Oct 21, 2026
18b28e3
chore(release): providers are 0.22.0; integrations.connect is SDK 13.7.0
thedogwiththedataonit Oct 25, 2026
a4a53d7
test(integrations): map connect's flags to their body keys; main's re…
thedogwiththedataonit Oct 31, 2026
9182d71
chore(release): providers are 0.23.0 after migrate's 0.22.0
thedogwiththedataonit Oct 31, 2026
0e44ea3
docs(integrations): connect names 409 INTEGRATION_CONNECTED_IN_APP
thedogwiththedataonit Nov 1, 2026
7134371
fix(integrations): check --signing-secret-env before the confirmation…
thedogwiththedataonit Nov 1, 2026
986a525
fix(doctor): warn on a key still in the team-only sandbox
thedogwiththedataonit Nov 1, 2026
7daf6cf
fix(init): change a provider or brand only when nothing breaks
thedogwiththedataonit Nov 1, 2026
e8febab
test(init): imports for #55's tests after the helpers moved to init-api
thedogwiththedataonit Nov 1, 2026
35bc736
fix(init): the provider step follows #55's auth set
thedogwiththedataonit Nov 1, 2026
03a94b9
fix(launchers): --supabase-project takes a value
thedogwiththedataonit Nov 1, 2026
3419e78
fix(init): the domain step waits until the brand is ready, like the kit
thedogwiththedataonit Nov 1, 2026
b9abd8c
fix(init,doctor): keep an app key already on the picked brand; doctor…
thedogwiththedataonit Nov 1, 2026
e6222ac
fix(doctor): a key under a public name in the environment fails env-e…
thedogwiththedataonit Nov 1, 2026
dce5ac3
fix(integrations): connect exits 1 while an auth email failed to set up
thedogwiththedataonit Nov 1, 2026
5d7b568
fix(init): a failed brand lookup holds every brand-specific step
thedogwiththedataonit Nov 1, 2026
4c13154
fix(init): no patch applies while the kit waits for the brand
thedogwiththedataonit Nov 1, 2026
11b9265
docs(changelog): 0.23.0 bundles the #2169 head b884e78157 spec
thedogwiththedataonit Nov 1, 2026
56d36aa
fix(init): a rollback that cannot write brew.json fails its state step
thedogwiththedataonit Nov 1, 2026
ec74349
fix(integrations): connect counts auth emails set up, not ready
thedogwiththedataonit Nov 1, 2026
432bc57
fix(doctor): a brand read that fails is a failed project check
thedogwiththedataonit Nov 1, 2026
72dc77d
fix(init): a refused Supabase project change keeps its backup paired
thedogwiththedataonit Nov 1, 2026
a30ebd5
fix(doctor): skip the SDK check where init installs no SDK
thedogwiththedataonit Nov 1, 2026
8cac402
docs(changelog): 0.23.0 bundles the #2169 head 02a3946b18 spec
thedogwiththedataonit Nov 1, 2026
a41a3da
fix(init): a printed Supabase re-run keeps the project --supabase-pro…
thedogwiththedataonit Nov 1, 2026
41a818c
fix(init): an organization key whose brand list fails holds every bra…
thedogwiththedataonit Nov 1, 2026
a6df37d
fix(init): provider follow-ups name this run's brand and server
thedogwiththedataonit Nov 1, 2026
d35925a
test(init): import SpawnFn for the failed-install case from #55
thedogwiththedataonit Nov 1, 2026
c2b8540
fix(doctor): --project reads every page of domains before calling non…
thedogwiththedataonit Nov 1, 2026
a0121da
fix(doctor): --project sends the .env.local key only to its saved server
thedogwiththedataonit Nov 1, 2026
d26b14b
fix(init): provider follow-ups keep a --stack override
thedogwiththedataonit Nov 1, 2026
ee101e2
fix(doctor): a public Brew setting that holds no key passes the expos…
thedogwiththedataonit Nov 1, 2026
c45651f
fix(init): the .env.local key's brand is asked on its own server
thedogwiththedataonit Nov 1, 2026
39a1206
fix(doctor): --project reads app emails until every brew.json key is …
thedogwiththedataonit Nov 1, 2026
e073a5c
docs(changelog): 0.23.0 bundles 0.20.0's spec, the #2169 head 6e4accea4c
thedogwiththedataonit Nov 1, 2026
6ae5545
fix(init): a refused save keeps the old hook secret; read every app e…
thedogwiththedataonit Nov 1, 2026
c72830b
fix(init): an organization key reads every brand page for the app's b…
thedogwiththedataonit Nov 1, 2026
ba40e0d
fix(init): an app key for another server never clears a brand switch
thedogwiththedataonit Nov 1, 2026
1baeb78
fix(init): check the app key's server and brand when no brand switch …
thedogwiththedataonit Nov 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,103 @@
# Changelog

## 0.23.0

Not released yet. Needs `@brew.new/sdk` 13.7.0: `integrations.connect` and
`integrations.getConnection` (GetBrew/typescript-sdk#74, on top of the
13.6.0 that 0.20.0 needs; see the SDK gates in `RELEASING.md`), and the API
from GetBrew/brew-v2#2169 (`GET|PUT /v1/integrations/{provider}/connection`,
the `supabase_auth` hosted Send Email Hook, and Clerk `email.created`
sending the app's auth emails). The bundled spec is 0.20.0's, the #2169
head `6e4accea4c` (main's spec, brew-v2 `373e900a9a`, plus the DX delta).

### Added

- **`brew-cli integrations connect <provider>`** connects a hosted auth
email hook, `supabase_auth` or `clerk` (`PUT
/v1/integrations/{provider}/connection`, destructive class: a new secret
replaces the old one), and sets up its auth app emails. The signing
secret comes from `--signing-secret-env <VAR>` or
`--signing-secret-stdin`, never a flag value; a value shaped wrong (or
pasted where the variable name goes) is a usage error that never echoes
it. `--project-url` sets Supabase's confirmation link base. Prints
`{ connectionId, webhookUrl, status, authEmails }`. 5-minute default
deadline, as the SDK's. Re-running it is safe: `connectionId` and
`webhookUrl` stay the same for the brand and provider, the new secret
replaces the old one in place, auth emails already set up are kept (one
unpublished in Brew stays off), and keys that failed are finished; while
one has failed it exits 1, with the full response. The human output
counts auth emails set up and points at `app-emails list` for which
send. A Clerk connected on its integration page
in Brew keeps its secret there: another secret exits 1 with `409
INTEGRATION_CONNECTED_IN_APP` (rotate it on that page).
- **`brew-cli integrations connection <provider>`** reads it:
`pending_verification` until the first signed delivery, then `active`
(404 `NOT_FOUND` when not connected).
- **`init --configure-supabase`** (with `SUPABASE_ACCESS_TOKEN` on this
machine, sent to api.supabase.com only) connects Brew as the Send Email
Hook with a freshly generated `v1,whsec_` secret, then points the
project's hook at it through the Supabase Management API (`PATCH
/v1/projects/{ref}/config/auth`: `hook_send_email_enabled`,
`hook_send_email_uri`, `hook_send_email_secrets`). The previous hook
settings are saved to `brew.json#providerConfig` before the change (a
previous secret goes to `.env.local` as
`BREW_SUPABASE_PREVIOUS_HOOK_SECRETS`, never `brew.json`); a re-run that
finds the hook already on Brew changes nothing. The hook is left as it
is while an auth email failed or reaches the team only (sandbox), while
Brew's connection is active for another hook (a new secret would stop
it), or when that backup cannot be saved. `--dry-run --json`
lists the exact requests with the secret replaced. The project ref comes
from `--supabase-project <ref>`, `supabase link`, a `*SUPABASE_URL`, or
Lovable's files.
- **`init --configure-clerk`** turns off "Delivered by Clerk" (Clerk
Backend API `POST /templates/email/{slug}/toggle_delivery`, with
`CLERK_SECRET_KEY` from the shell or `.env.local`) for exactly the
templates whose kit key's app email reaches every user (published, not
the team-only sandbox), and only once Brew's Clerk connection is
`active`; otherwise it refuses and says what is missing. The templates
it turned off are saved in `brew.json`, with a fingerprint of the
`CLERK_SECRET_KEY` used (never the key).
- **`init --rollback-provider`** puts back the Supabase hook settings or
Clerk delivery saved in `brew.json`, and nothing else. It refuses, and
keeps the saved settings, while an enabled hook's old secret is missing
or `CLERK_SECRET_KEY` is another instance's. A `brew.json` it cannot
write (a symbolic link) fails the `state` step, exit 1.
- Without those flags, init prints the manual steps for Supabase (the
hosted hook first; the self-hosted Edge Function it writes is now the
non-blocking alternative) and Clerk. A bare `--yes` never changes a
provider. The commands those steps print (`integrations connect`,
`integrations connection`, `init --configure-…`) carry `--brand` and
`--api-url` when the CLI's own key would resolve another brand or
server than this run.
- **`init --app-url`** with an organization key picks the brand whose
website is the app (a subdomain counts), or creates one (`POST
/v1/brands`) and waits for it to be ready (`--no-wait`: the kit waits for
a re-run). A 403 (`ORG_SCOPE_REQUIRED`, no `brands` scope) becomes a next
step; a key bound to one brand is never moved, a mismatch is reported.
The app's own key in `.env.local` is one: when it is another brand's,
the kit waits for a `replace-app-key` next step. When the brand lookup
or an organization key's brand list fails, the domain, kit and provider
steps wait for a re-run (exit 1).
While the kit waits for the brand, so does every `--apply-patches` patch.
- **`init --domain`** also returns the domain's page in Brew
(`domain.connectUrl`, a `connect-dns-automatically` next step), where
Connect automatically (Entri) adds the records: it runs in the app, not
the API. A `DKIM_SELECTOR_IN_USE` domain gets neither (`connectUrl:
null`): its records would replace another sender's key.
- **`brew-cli doctor --project`** checks an app against its `brew.json`:
managed file hashes, `BREW_API_KEY` set, git-ignored and never under a
browser-exposed name, the SDK declared and installed at `^13.6.0`, and,
with the app key, the brand (the key's brand is `brew.json`'s), every key
set up, published and past the sandbox (a warning names a key whose
purpose has no verified domain), a verified sending domain, and the
hosted hook's status. `--json`; any failed check (a brand read that
fails included) exits 1, no `brew.json` exits 2.
- The init report (schema version 1, additive) gains `providers[]`, the
`provider` step, `brand.domain` / `matchesAppUrl` / `created` and
`domain.connectUrl`.
- The spec parity test's pending-build list is empty again: these
commands bind the two connection routes 0.19.0's spec already carried.

## 0.22.0

Needs the same SDK release as 0.20.0, and the API from GetBrew/brew-v2#2169
Expand Down
75 changes: 73 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ are attached to every [GitHub Release](https://github.com/GetBrew/brew-cli/relea
```bash
brew-cli login # sign in through the browser (or --paste a key)
brew-cli doctor # trust check: auth, reachability, CLI-vs-API drift (exit-code gated)
brew-cli doctor --project # this app vs its brew.json: files, key, SDK, keys, domain, auth hook
brew-cli whoami
brew-cli contacts search --filter email:equals:jane@example.com
brew-cli emails list --limit 10
Expand Down Expand Up @@ -78,8 +79,78 @@ monorepo, editors), signs you in through the browser, and then:
Every file is create-only: a file you changed is reported
`kept-user-version` and left alone, and a re-run with nothing new changes
nothing. Until a sending domain verifies, app emails reach members of
your Brew organization only; `--domain mail.example.com` adds one and
prints its DNS records without waiting.
your Brew organization only; `--domain mail.example.com` adds a
transactional domain and prints its DNS records without waiting, plus the
domain's page in Brew, where **Connect automatically** (Entri) adds the
records at your DNS provider. That button runs in the Brew app: the API
has no endpoint for it, so the CLI cannot do it for you.

`--app-url https://app.example.com` shows the app's URL on the sign-in
approval page. With an organization API key (`BREW_API_KEY` or
`--api-key`), init also picks the brand whose website is the app, or
creates one from the URL (`POST /v1/brands`, organization keys with the
`brands` scope) and waits for it (`--no-wait` leaves that to a re-run). A
key bound to one brand is never moved: a mismatch is reported. The app's
own key in `.env.local` is one: when the app's brand is another, init
stops before the kit and says to replace that key.

### The auth provider's own emails (Supabase, Clerk)

Nothing about your auth provider changes under a bare `--yes`. Without the
flags below, init prints the manual steps.

- **Supabase Auth**: `--configure-supabase` connects Brew as the hosted
Send Email Hook (`integrations connect supabase_auth`) and points
Authentication > Hooks > Send Email at it through the Supabase
Management API, using a Supabase personal access token from
`SUPABASE_ACCESS_TOKEN` on this machine (sent to api.supabase.com only,
never to Brew). The previous hook settings go into `brew.json` first
(a previous hook secret into `.env.local`, never `brew.json`). The hook
changes only once every auth email reaches every user (set up, with a
verified domain, not the team-only sandbox), and never takes over a
connection that is active for another hook.
`--dry-run --json` shows the exact PATCH with the secret replaced. The
self-hosted Edge Function init writes stays the alternative.
- **Clerk**: connect Clerk's `email.created` webhook first
(`integrations connect clerk`). `--configure-clerk` then turns off
"Delivered by Clerk" for exactly the templates whose Brew email reaches
every user (published, not the team-only sandbox), through
Clerk's Backend API with `CLERK_SECRET_KEY` (shell or `.env.local`), and
only once `integrations connection clerk` reports `active`, so no email
is dropped in between.

`init --rollback-provider --yes` puts back what either flag changed, from
`brew.json`.

### Hosted auth hooks: `integrations connect`

```bash
# the secret comes from an environment variable or stdin, never a flag value
brew-cli integrations connect supabase_auth --signing-secret-env SEND_EMAIL_HOOK_SECRET \
--project-url https://<ref>.supabase.co --yes
printenv CLERK_WEBHOOK_SIGNING_SECRET | brew-cli integrations connect clerk --signing-secret-stdin --yes
brew-cli integrations connection clerk # pending_verification until the first signed delivery, then active
```

`connect` prints `{ connectionId, webhookUrl, status }` (and each auth
email it set up): paste `webhookUrl` into Supabase's hook or Clerk's
endpoint. Running it again is safe: `connectionId` and `webhookUrl` stay
the same for the brand and provider, the new secret replaces the old one,
auth emails already set up are kept (one unpublished in Brew stays off;
`app-emails list` shows which send), and keys that failed are finished. A
Clerk connected on its integration page in Brew keeps its secret there:
another secret is `409 INTEGRATION_CONNECTED_IN_APP` (rotate it on that
page).

### Check a project: `doctor --project`

`brew-cli doctor --project [--json]` checks the app against its
`brew.json`: managed files unchanged, `BREW_API_KEY` in `.env.local`,
git-ignored and never under a `NEXT_PUBLIC_`/`VITE_`/`PUBLIC_` name,
`@brew.new/sdk` declared and installed at the version the generated files
need, and with the app key: every key in `brew.json` set up and published
(`GET /v1/app-emails`), a verified sending domain, and the hosted auth
hook's status. Any failed check exits 1.

Agents run it in two steps and read one JSON report
(`schemas/init-report.json`: `status`, `steps[]`, `files[]`, `patches[]`,
Expand Down
15 changes: 15 additions & 0 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,21 @@ what the spec-drift sentinel reads) matching `openapi/public-api-v1.yaml`,
which is the #2169 head `6e4accea4c` (main's spec, brew-v2 `373e900a9a`,
plus the DX delta).

### Hosted auth hook SDK gate

`integrations connect`, `integrations connection`, `init`'s provider step
(`--configure-supabase`, `--configure-clerk`) and `doctor --project` call
`brew.integrations.connect` and `brew.integrations.getConnection`, which
arrive in GetBrew/typescript-sdk#74 (planned 13.7.0, after 13.6.0 above).
Release that SDK first, then `bun add @brew.new/sdk@^13.7.0` (never
hand-write a lockfile entry), run a frozen install, the full checks and
`docs:commands:check`, and release the CLI. The API side,
GetBrew/brew-v2#2169, must be deployed first: re-fetch
`https://brew.new/openapi/public-api-v1.yaml` and compare the two
`/v1/integrations/{provider}/connection` operations with the bundled spec
before tagging. Generated app code still asks for `^13.6.0`
(`SDK_RANGE`): only the CLI itself needs 13.7.0.

```bash
# on a clean main with CI green
npm version <patch|minor|major> --no-git-tag-version
Expand Down
Loading