Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
2b094ea
feat(platform): add project Zero Trust protection
Brooooooklyn Sep 30, 2026
2e5dd85
feat(platform): add Zero Trust pages to the React admin
Brooooooklyn Sep 30, 2026
fb30212
fix(platform): harden Zero Trust permissions, retries, and ordering
Brooooooklyn Sep 30, 2026
677fa53
refactor(platform): simplify Zero Trust state and command surface
Brooooooklyn Sep 30, 2026
f807661
refactor(platform): split Zero Trust service into modules
Brooooooklyn Sep 30, 2026
7001bb9
docs(platform): document project Zero Trust
Brooooooklyn Sep 30, 2026
48a8e2d
fix(platform): keep Zero Trust metadata on its own project's routes
Brooooooklyn Sep 30, 2026
00cd87c
fix(platform): retry projects missing Zero Trust state fairly
Brooooooklyn Sep 30, 2026
6605bcc
fix(platform): skip failed projects only for the live Zero Trust oper…
Brooooooklyn Sep 30, 2026
0c456ab
docs(platform): explain how project Zero Trust protection works
Brooooooklyn Sep 30, 2026
f595837
docs(platform): describe skipped projects during Zero Trust disable
Brooooooklyn Sep 30, 2026
3bbc87f
fix(platform): call the global fetch unbound in Zero Trust clients
Brooooooklyn Oct 1, 2026
9579bf2
fix(platform): keep platform health probes outside project Zero Trust
Brooooooklyn Oct 1, 2026
de6b132
fix(platform): route the platform health host like any project host
Brooooooklyn Oct 1, 2026
9859eb3
fix(platform): bypass Access only for the platform health path
Brooooooklyn Oct 1, 2026
bf3be56
fix(platform): refuse uninstall while project Zero Trust owns Access …
Brooooooklyn Oct 1, 2026
be3c7c3
fix(platform): hold off Zero Trust writers until uninstall disables t…
Brooooooklyn Oct 1, 2026
4b46452
fix(platform): point uninstall to rerunning a stopped project deletion
Brooooooklyn Oct 1, 2026
34123c5
docs(platform): record the uninstall lock's remaining window
Brooooooklyn Oct 1, 2026
c999d05
test(platform): give the retained-Queue uninstall tests a Zero Trust …
Brooooooklyn Oct 1, 2026
9bf6740
fix(platform): keep a new application domain pending while a project …
Brooooooklyn Oct 1, 2026
3b8ee88
fix(platform): record a removed custom domain for Zero Trust retry in…
Brooooooklyn Oct 1, 2026
a0cd3fb
fix(platform): refuse Zero Trust writers that waited out the uninstal…
Brooooooklyn Oct 1, 2026
ebc6101
fix(platform): check the uninstall fence before removing a custom domain
Brooooooklyn Oct 1, 2026
02d88f3
fix(platform): record an expired pending domain for Zero Trust retry …
Brooooooklyn Oct 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
1 change: 1 addition & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ const platformSidebar = [
{ text: 'Overview', link: '/guide/platform-administration' },
{ text: 'Sign-in and Access', link: '/guide/platform/administration/access' },
{ text: 'Users and Projects', link: '/guide/platform/administration/projects' },
{ text: 'Project Zero Trust', link: '/guide/platform/administration/zero-trust' },
{ text: 'Email', link: '/guide/platform/administration/email' },
{ text: 'Operations', link: '/guide/platform/administration/operations' },
],
Expand Down
2 changes: 1 addition & 1 deletion docs/guide/edge/revalidation.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ revalidation, Void removes its HTML, Pages JSON, and KV cache entries.

## Cache bypass

Requests with `Cookie` or `Authorization` headers bypass the ISR cache entirely and always dispatch to the worker. This ensures personalized or authenticated pages are never served from a shared cache.
Requests with `Cookie`, `Authorization`, or Cloudflare Access's `Cf-Access-Jwt-Assertion` header bypass the ISR cache entirely and always dispatch to the worker. Authenticated responses are not shared between users, including when Access authentication supplies no cookie.

## Cache keys and rewrites

Expand Down
1 change: 1 addition & 0 deletions docs/guide/edge/static-assets.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ Cached at the edge until the next deploy. Browsers always revalidate on the next
- `/api/*` routes, which always hit the worker
- SSR-rendered pages (paths without file extensions in SSR projects)
- Non-GET requests
- Requests carrying `Cookie`, `Authorization`, or `Cf-Access-Jwt-Assertion` credentials
- Responses other than a complete `200`

### Opting out
Expand Down
1 change: 1 addition & 0 deletions docs/guide/platform-administration.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@ The operator commands for users, projects, deployments, and system status requir

- [Sign-in and Access](/guide/platform/administration/access)
- [Users and Projects](/guide/platform/administration/projects)
- [Project Zero Trust](/guide/platform/administration/zero-trust)
- [Email](/guide/platform/administration/email)
- [Operations](/guide/platform/administration/operations)
318 changes: 318 additions & 0 deletions docs/guide/platform/administration/zero-trust.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/guide/platform/installation/domains.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ void platform domain set example.app

The command selects your installed platform (or offers a picker), finds or creates its zone, sets up DNS and routing, and verifies HTTPS before publishing the new application URLs. Set the management token as described in [installation setup](/guide/platform/installation/setup) when creating DNS or a zone. Grant the existing runtime token **Cache Purge: Purge** on the new zone; Void checks that permission through the running platform without asking you to paste its token again.

If nameservers or certificates are pending, follow the printed guidance and rerun the same command. Your workers.dev app URLs continue working during and after setup. Projects, deployments, secrets, and the platform API URL stay the same, so developers do not reconnect and configured login callbacks do not change. DNS and configuration changes may take time to propagate.
If nameservers, certificates, or project Zero Trust protection are pending, follow the printed guidance and rerun the same command. Void preserves each project's public or protected setting before making the new URLs available. Your workers.dev app URLs continue working during and after setup. Projects, deployments, secrets, and the platform API URL stay the same, so developers do not reconnect and configured login callbacks do not change. DNS and configuration changes may take time to propagate.

Use `--installation <id>` to select an installation explicitly, `--zone example.com` for an app domain such as `apps.example.com`, or `--dedicated-zone` for catch-all routing on a dedicated zone. Nested domains still need the wildcard certificate described below. This command adds the first domain; replacing an existing application domain is not currently supported. It uses the installed runtime and does not require `--runtime` or an app redeploy.

Expand Down
4 changes: 4 additions & 0 deletions docs/guide/platform/installation/first-deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,7 @@ Void shows the proposed access change and asks you to confirm it. Once approved,
To see who can join, run `void platform signup show`. You can also allow an email address or a domain such as `*@example.com`. For an OIDC user without verified email, use `void platform signup allow identity <connection-id> <subject>`; the subject match is exact and the provider's domain or group restrictions still apply. Remove that grant with `void platform signup disallow identity <connection-id> <subject>`. `void platform signup open` permits public signup; `void platform signup restrict` requires an allowlist match again.

Your administrator session lasts for one hour. Use it to inspect users, projects, logs, and platform health. The [Platform Administration guide](/guide/platform-administration) walks through those workflows, previews, and automation. You can also open `<API origin>/admin/login` to use the browser admin UI.

### Protect Project Hostnames with Zero Trust

After installation, an administrator can require Cloudflare Access on new project hostnames while allowing each project owner or project administrator to opt out. This is independent of Access login and protection for the platform API itself, and it needs its own Cloudflare API token. See [Project Zero Trust](/guide/platform/administration/zero-trust) for the requirements, setup command, and recovery.
2 changes: 2 additions & 0 deletions docs/guide/platform/installation/uninstall.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ Cloudflare refuses to delete a resource while a platform Worker still uses it. I

If installation configured Cloudflare Access, the uninstall plan lists its recorded applications and service tokens with their ownership and IDs. Review them in Zero Trust after uninstall. Remove installer-created resources only when nothing else uses them; resources connected from an existing company setup remain under their owner's control.

Uninstall does not remove the applications created for [project Zero Trust](/guide/platform/administration/zero-trust). While Zero Trust is enabled or still has Access applications, uninstall and `--plan` stop before any change and list those applications. Run `void platform zero-trust disable` first. Uninstall reads this from the installation database, so it also stops when the database cannot be reached.

::: details Why some resources require manual cleanup

Resources that may have been shared or repurposed require manual review before deletion. Void verifies ownership, keeps those resources in place, and blocks platform traffic.
Expand Down
64 changes: 63 additions & 1 deletion docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Use this page as a command reference. If you are setting up a project for the fi
| `void project requests` | Show request-level traffic (status, method, timing) |
| `void project rollback` | Roll back to a previous deployment |
| `void project cancel` | Cancel an active deployment |
| `void project zero-trust` | Inspect or override project Zero Trust protection |
| `void project purge-cache` | Purge all cached pages |
| `void build logs` | Stream, tail, or download build logs |
| `void email status` | Show email readiness on your own Cloudflare account (`--platform cloudflare`) |
Expand Down Expand Up @@ -282,6 +283,27 @@ platform, and accepting an invitation leaves directory links intact. To link
the invited application, run `void project link` in an unlinked checkout of
that application.

### `void project zero-trust`

Inspect or override protection for a project on a Void platform:

```sh
void project zero-trust status [--project <slug>]
void project zero-trust protect [--project <slug>]
void project zero-trust public [--project <slug>]
void project zero-trust reconcile [--project <slug>]
```

Project selection follows `--project`, then `VOID_PROJECT`, then the linked
project.

Readers can inspect the state. The owner or a project administrator can protect
the project, make it public, or repair drift. `public` does not change the
platform default for future projects. Deploys and rollbacks wait until a
protection change is finished; if one is refused, the owner or a project
administrator can run `void project zero-trust reconcile` before you retry.
These commands do not apply to direct Cloudflare deployments.

### `void project token <create|list|renew|revoke>`

Manage revocable `aud: deploy` credentials for one Void platform project. The
Expand Down Expand Up @@ -585,6 +607,46 @@ Search matches a project's slug, ID, or owner's login. `show` includes resources

`owner` transfers a project to another registered user. Preview it with `--plan`; apply it interactively or with `--yes`. The preview reports active-work blockers and the account plan that will apply. The former owner becomes a project administrator, existing project-scoped CI deploy credentials are revoked, and usage already incurred remains with the former owner.

#### Project Zero Trust {#operator-zero-trust}

Configure the installation-wide Cloudflare Access policy and project overrides:

```sh
void platform zero-trust status [--check]
printf '%s' "$ACCESS_API_TOKEN" | void platform zero-trust configure \
--identity-providers <id[,id...]> \
--policies <id[,id...]> \
[--existing-projects <protected|public>] \
[--protect-new-projects] \
--token-stdin \
--yes
void platform zero-trust reconcile
void platform zero-trust disable

void platform zero-trust project-status <project-id>
void platform zero-trust project-protect <project-id>
void platform zero-trust project-public <project-id>
void platform zero-trust project-reconcile <project-id>
```

Mutations support `--plan`, `--yes`, and `--timeout`; use `--plan` instead of
`--yes` to inspect this change without applying it. The Access management token
is accepted only on standard input. Zero Trust uses the Cloudflare account that
hosts the platform. Omit `--protect-new-projects` to make new projects public
by default. `--existing-projects` is required when you enable Zero Trust and
rejected once it is enabled; later configuration changes keep each project's
protection. `status` reports saved state; `--check` also compares it with
Cloudflare Access. If status remains `configuring` or `disabling`, run
`void platform zero-trust reconcile` to resume the saved operation. Protected
projects support up to 100 custom domains.

The token needs **Access: Apps and Policies Write** plus **Access:
Organizations, Identity Providers, and Groups Read**, scoped to the platform's
Cloudflare account. Select at least one Allow policy that matches people by
identity; Void rejects Bypass and Service Auth policies and rules that allow
Everyone or any service token. See [Project Zero Trust](../guide/platform/administration/zero-trust.md)
for requirements and recovery.

#### Deployments {#operator-deployments}

Find a deployment, inspect its manifest, or request cancellation:
Expand Down Expand Up @@ -818,7 +880,7 @@ See [Self-host a Void platform](../guide/self-hosted-platform.md) for prerequisi
void platform domain set <domain> [--installation <id>] [--zone <domain>] [--dedicated-zone] [--plan] [--yes]
```

Add an application domain to a workers.dev test platform. Domain-based installations remain the recommended default. The command detects the zone when possible, creates missing DNS and routes after confirmation, and checks HTTPS before making the domain canonical. If DNS or certificates are pending, rerun the same command to resume. `--plan` is read-only; non-interactive mutations require `--yes`.
Add an application domain to a workers.dev test platform. Domain-based installations remain the recommended default. The command detects the zone when possible, creates missing DNS and routes after confirmation, and checks HTTPS and project Zero Trust protection before making the domain canonical. If DNS, certificates, or protection are pending, rerun the same command to resume. `--plan` is read-only; non-interactive mutations require `--yes`.

Existing workers.dev URLs remain available, and the platform API origin, OAuth callback, projects, and deployments stay unchanged. The command verifies the running runtime token's Cache Purge permission for the new zone. A disabled platform stays disabled. Use the database URL from the original installation when administering from another machine. Replacing an already configured application domain is not supported. See [Adding a Domain](../guide/platform/installation/domains.md#adding-a-domain).

Expand Down
4 changes: 4 additions & 0 deletions packages/edge/src/access.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type ProjectZeroTrustRouting = Readonly<{
teamDomain: string;
audiences: ReadonlyArray<Readonly<{ hostname: string; audience: string }>>;
}>;
1 change: 1 addition & 0 deletions packages/edge/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export type { ProjectZeroTrustRouting } from './access.ts';
export {
applyHeaderRules,
parseClearSiteData,
Expand Down
2 changes: 1 addition & 1 deletion packages/isr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ The package has three entry points:
- `@void/isr/core` owns target-neutral codecs, validation, TTL selection, request bypass rules, response construction, header sanitization, Void Pages data extraction, and query-variant normalization.
- `@void/isr/runtime` implements the fail-open, single-tenant Cloudflare KV and Cache API ladder used by direct Void deployments, including bounded variants from `routing.revalidateQueryAllowlist`.

The direct runtime applies ISR only to public page requests. API paths, credentialed requests, Range requests, and EventSource requests render live. A rendered response is shared only when it is a complete `200` response without `Cache-Control: private`, `no-store`, or `no-cache`, and without `Set-Cookie`. When a background revalidation returns one of those opt-outs, the runtime removes the old HTML, Pages JSON, and KV variants so previously public content cannot remain reachable.
The direct runtime applies ISR only to public page requests. API paths, requests carrying `Cookie`, `Authorization`, or `Cf-Access-Jwt-Assertion` credentials, Range requests, and EventSource requests render live. A rendered response is shared only when it is a complete `200` response without `Cache-Control: private`, `no-store`, or `no-cache`, and without `Set-Cookie`. When a background revalidation returns one of those opt-outs, the runtime removes the old HTML, Pages JSON, and KV variants so previously public content cannot remain reachable.

| Symbol | Description |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
Expand Down
3 changes: 2 additions & 1 deletion packages/isr/src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { matchLiteralPattern, matchSegmentPattern } from '@void/edge';
* proven safe to share. Readers reject entries from earlier policies instead
* of replaying opaque bodies whose private headers were stripped at write time.
*/
export const SHARED_CACHE_POLICY_VERSION = 1 as const;
export const SHARED_CACHE_POLICY_VERSION = 2 as const;
export const SHARED_CACHE_POLICY_HEADER = 'x-void-shared-cache-policy';

/** Stamp an internal edge-cache representation with the policy that admitted it. */
Expand Down Expand Up @@ -244,6 +244,7 @@ export function shouldBypassIsr(request: Request): boolean {
return (
request.headers.has('Cookie') ||
request.headers.has('Authorization') ||
request.headers.has('Cf-Access-Jwt-Assertion') ||
acceptsEventStream(request)
);
}
Expand Down
24 changes: 23 additions & 1 deletion packages/platform/src/application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,20 @@ export async function resolvePlatformApplicationEnvironment<
};
}

export function platformProjectIngressPrefix(
workersDev: NonNullable<PlatformApplication['workersDev']>,
): string {
return `void-${workersDev.installationId.slice(-12)}-`;
}

export function platformProjectIngressName(
project: PlatformApplicationProject,
workersDev: NonNullable<PlatformApplication['workersDev']>,
): string {
if (!/^proj_[a-z0-9]{1,32}$/.test(project.id) || !/^[a-z0-9][a-z0-9-]*$/.test(project.slug)) {
throw new Error('Invalid platform project identity.');
}
const prefix = `void-${workersDev.installationId.slice(-12)}-`;
const prefix = platformProjectIngressPrefix(workersDev);
const suffix = `-${project.id.slice(5)}`;
const slug = project.slug.slice(0, 63 - prefix.length - suffix.length).replace(/-+$/, '');
return `${prefix}${slug}${suffix}`;
Expand All @@ -93,6 +99,22 @@ export function platformProjectWorkersDevHostname(
return `${platformProjectIngressName(project, workersDev)}.${workersDev.subdomain}.workers.dev`;
}

/**
* Project slug the platform keeps for its own health probe. Project Zero Trust
* leaves only `/health` on this hostname outside Cloudflare Access so the
* authenticated probe reaches dispatch, which never routes that path to a
* project. New projects cannot claim the slug; a project that already owns it
* keeps every other path, behind Access like any other project hostname.
*/
export const PLATFORM_HEALTH_HOSTNAME_LABEL = 'void-platform-health';

/** The application-domain hostname for dispatch health probes; null without a zone. */
export function platformHealthHostname(application: PlatformApplication): string | null {
return application.zoneName
? `${PLATFORM_HEALTH_HOSTNAME_LABEL}.${application.baseDomain}`
: null;
}

export function platformProjectHostname(
project: PlatformApplicationProject,
application: PlatformApplication,
Expand Down
3 changes: 3 additions & 0 deletions packages/platform/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,12 @@ export {
type PlatformWorkerConfigSet,
} from './worker-config.ts';
export {
PLATFORM_HEALTH_HOSTNAME_LABEL,
platformHealthHostname,
platformProjectHostname,
platformProjectHostnames,
platformProjectIngressName,
platformProjectIngressPrefix,
platformProjectWorkersDevHostname,
platformWorkersDevAliasKey,
readPlatformApplication,
Expand Down
Loading
Loading