Skip to content

feat(platform): Administrator-managed email domains and installation-wide recipient policy - #330

Merged
Brooooooklyn merged 13 commits into
mainfrom
research/void-email
Sep 18, 2026
Merged

Brooooooklyn merged 13 commits into
mainfrom
research/void-email

Conversation

@Brooooooklyn

Copy link
Copy Markdown
Member

Summary

Aligns the platform email feature with the updated design record (platform/meta/design-docs/0078-email-cf-native.md). Target scenario: one Cloudflare administrator deploys Void internally; staff (and their coding agents) deploy apps without a Cloudflare account.

 admin                                                      owner
 ─────                                                      ─────
 void platform email policy-set domains --domains acme.com  sendEmail() reaches any @acme.com
 void platform email settings-set --domains admin           void email domain add → prints the admin command
 void platform email domain-add mail.acme.com --project hr  void email domain list / status → managed_by: admin
 void platform email domain-status|sync|remove              sync / remove on admin rows → 403 naming the command
 void platform email limit hr --monthly 2000 --burst 30
 /admin/email  (policy + management)  ·  project page (add domain, managed-by column)

Design record

  • 0078 rewritten to describe the current design only (history / compat notes dropped); admin-managed domains and the recipient policy are folded in.

Recipient policy (installation-wide)

  • verified (default) | domains [list] | any. Authoritative in new platform_settings table, mirrored to ROUTING KV email-recipient-policy (60s cache). Absent or malformed mirror → verified.
  • Gates shared-sender sends and the inbound forward/reply path. Custom-domain sends untouched. Never widens delivery to the platform's own mail domain.
  • Publish is two advisory-locked steps (clear mirror + commit DB, then republish from DB) so the mirror can never run ahead of the database.

Administrator-managed domains

  • New credential_kind: 'platform' → engine / lease / teardown resolve to CF_API_TOKEN; zone must live in CF_ACCOUNT_ID (checked on setup, re-add, sync, on the locked row). --token-stdin fallback for zones elsewhere.
  • managed_by: 'owner' | 'admin'. Owners get list/status on admin rows; sync/remove/re-add → 403. Admins may sync/remove owner rows; takeover by re-add → 409.
  • email.domains setting: self-serve (default) | admin. In admin mode the owner CLI prints the exact admin command; the server refuses only new owner domains.
  • Domain orchestration extracted from routes/email.ts into email/domain-service.ts so owner routes, operator routes and the admin UI share it.

Limits

  • Burst / monthly caps already applied to every send path; new void platform email limit verb exposes the admin-set KV keys.

Migrations

  • 0041_platform_settings.sql, 0042_email_domains_platform_credential.sql (nullable token_* with CHECK, managed_by).

Rollout note

Pre-upgrade Workers cannot read platform rows — register admin domains after the upgrade completes (documented in the administration guide).

Test plan

  • vp run check clean, vp lint 0 errors
  • vp test --project platform-* — 168 files / 4313 tests
  • CLI unit suites: operator-cli, cli-help, email-cli-domain, platform-setup
  • Two review rounds (adversarial + design↔impl alignment) triaged and closed
  • Live acceptance on a real zone with an account-owned token (Email Sending onboarding endpoint) — open, named in 0078

Generated with Devin

Brooooooklyn and others added 6 commits September 18, 2026 15:26
Keep the email record to current design and architecture: drop dated
probes, fix-round narratives and old-CLI compatibility notes; add the
planned self-hosted flow where an administrator runs the domain engine
with the platform's runtime token and an installation-wide recipient
policy relaxes the shared-sender allowlist.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…ngs.

An administrator can now decide who mail from the shared sender reaches
(verified destinations, listed domains, or anyone), who registers email
domains (owners or administrators), and a project's outbound caps —
from the operator CLI or the admin UI. Settings are authoritative in the
new platform_settings table; the proxy reads the policy from a routing
KV mirror that fails closed to today's verified-only behavior.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…form credential.

Administrators register and maintain projects' email domains from the
operator CLI (`void platform email domain-add|domains|domain-status|
domain-sync|domain-remove`) and the admin UI, running the same setup
engine as owners. Such rows carry the new `platform` credential kind —
no stored token; the engine, lease and teardown resolve them to the
runtime token, and the zone must be in the platform's own account.
Rows record `managed_by`; owners can read an administrator's row but are
pointed at the operator command for sync and remove, and an
administrator-managed platform refuses owner adds up front.

The domain orchestration moves from routes/email.ts into
email/domain-service.ts unchanged so both surfaces share one
implementation of the serialized sections and compensations.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…latforms.

`void email domain add` reads the installation's email settings first and,
where administrators register domains, prints the operator command and
stops instead of opening a Cloudflare consent. Domain output names the
platform credential and administrator-managed rows. The installer's
runtime-token link preselects the Email Routing, Zone Settings and Email
Sending permissions when the runtime enables email.

Docs: administration guide (recipient policy, caps, administrator-managed
domains), self-hosting token permissions, email guide note, and the 0078
design record aligned with what shipped.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…ipient policy.

- Authorize add, sync and remove on the locked email_domains row, so an
  owner and an administrator can no longer interleave into each other's
  rows; an administrator never takes over an owner's row (409), from the
  admin form as well as the operator API.
- Pin platform-credential rows to the platform's account on re-add and
  sync too, never only at first setup — the runtime token cannot be
  pointed at a customer zone through a pasted-token row.
- Publish the recipient policy as delete mirror → write DB → put mirror
  in one transaction under an advisory lock shared with the repair: a KV
  failure leaves the proxy closed, never on an older, looser mirror, and
  a stale repair cannot republish an old policy over a new one.
- Read `any` from the mirror only in the api's own shape; the admin
  settings page repairs a drifted mirror like every other read.
- Surface a kept row after a failed removal as a failed operator
  mutation instead of nesting the failure under a success.
- Drop a generated playground file swept into the previous commit.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…design record.

Findings from the design-vs-implementation review:

- Administrators may sync and remove an owner's row again (the ownership
  table's maintenance powers); only a re-add takeover stays refused.
- An owner keeps renewing, syncing and removing a row registered before
  the switch to administrator-managed domains; only new domains are refused.
- The recipient policy publishes in two locked steps — clear mirror + commit
  DB, then republish from the committed value — so the mirror can never run
  ahead of the database, and it never widens delivery to another tenant's
  address on the platform's own mail domain.
- The proxy accepts only the api's exact mirror shape (`any` with an empty
  list, all-string lists within the cap).
- The admin email pages sit behind the email capability like the API routes.
- Sync on a platform row without a runtime token answers 503, not 500.
- The CLI treats the settings route's own "project not found" as an error
  instead of falling back to self-serve and opening a browser for nothing.
- Tests pin platform-row teardown, runtime-token loss on every surface, the
  reply-sender pin under `any`, and the platform-domain rule; the record and
  guides state the 60-second cache window and the remove path's lock scope.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 18, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-18T13:02:32.885702Z 8594499 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0f9d69d0b9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread platform/packages/api/src/email/settings.ts Outdated
Brooooooklyn and others added 2 commits September 18, 2026 18:40
…ep pre-switch owner rows renewable.

The inbound gate excluded only EMAIL_SENDER_DOMAIN, so an unset binding or a
second routed domain let `any` reach another tenant's inbox; the gate now
carries the domain Cloudflare accepted the message on as well. The admin-mode
refusal of a new owner domain moves into the insert's locked section, the
owner CLI continues the renewal flow for a row it registered before the
switch, and `status` on an administrator's row names the operator command
for each step the owner cannot take. Record and guides aligned, including
the rollback note.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…p the token triple complete.

Workers KV refuses a second write to a key within a second, so the
clear-then-put publish failed in production after committing; a publish is
now a single write ordered by direction so the mirror is never looser than
the database. The token-column CHECK requires all three values for every
stored credential again. The owner CLI pins a renewal to the matched
pre-switch row instead of retargeting on the apex MX, and `status` on a
failed administrator row names `domain-add`.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e32050380f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread platform/packages/api/src/email/settings.ts Outdated
… and probe platform rows by zone walk.

An operator add or sync that ends with the row failed, revoked or expired
now carries a top-level ok:false, so the audit records the failure and the
CLI exits nonzero. The add preview names the stored mode on a re-run instead
of warning about MX that setup itself installed, the policy preview and
admin label disclose the platform-domain exception, and the platform-row
status probe walks parent zones like setup does. Record and reference
aligned, including the mixed-list publish exception.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6cb219db14

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread platform/packages/api/src/admin/routes/email.tsx
Comment thread platform/packages/api/src/operator/email-routes.ts
Brooooooklyn and others added 2 commits September 18, 2026 19:58
…rite as failed mutations.

The admin add form now inspects the returned row and answers 502 with the
failing step when setup ends failed, revoked or expired, so the audit
records the failure instead of a success. The operator limit verb writes
each cap independently and reports which one landed when the other is
refused. Policy descriptions across the operator preview, admin label and
docs now say every address on the platform's own mail domain stays
verified-destination-only, which is what the gate enforces.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Renumbers the email migrations to 0045_platform_settings and
0046_email_domains_platform_credential behind main's authentication
migrations 0041-0044; SQL unchanged, snapshots regenerated from the merged
schema.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 39e4371bc2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/void/src/cli/client.ts Outdated
Comment thread platform/packages/proxy/src/email-policy.ts Outdated
Comment thread platform/packages/api/src/operator/email-routes.ts
…l closed on a rejected policy read.

A list that gains any domain now commits before the mirror is written, so
the mirror never authorizes a domain the database has not committed. The
proxy treats a rejected policy read as verified instead of failing the
send or closing the inbound gate, and the CLI reads the email settings
through the Access-aware request wrapper like its neighbours.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6a0f1a04fd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/void/src/cli/email.ts
…main command.

In admin mode the printed `void platform email domain-add` named the raw
input, so an apex that already receives mail produced a command setup would
refuse; the handoff now makes the owner flow's MX decision first and names
the target setup accepts. Renewals still keep their stored target.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@Brooooooklyn
Brooooooklyn merged commit 9359eed into main Sep 18, 2026
19 checks passed
@Brooooooklyn
Brooooooklyn deleted the research/void-email branch September 18, 2026 13:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant