Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 8 additions & 3 deletions .github/production-repositories.jsonc
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
// Public GitHub repositories served by the production remote cache.
// Each key is a cache namespace. Its endpoint is
// https://voidzero-remote-cache.<subdomain>.workers.dev/projects/<namespace>
// Each production deployment enables every listed namespace and disables every other namespace,
// overriding `pnpm operator policy`. Data in a removed namespace expires with its retention.
// A value is "owner/repo", or { "repository": "owner/repo", "pullRequests": true } to also accept
// uploads from same-repository pull_request runs that target the default branch. This interim
// opt-in lets unreviewed PR code, including Renovate branches, publish entries that default-branch
// builds restore. Read docs/e2e-plan.md#production-deployment before enabling it.
// Each production deployment enables every listed namespace, disables every other namespace, and
// sets each namespace's pull request opt-in from this list, overriding `pnpm operator policy`.
// Data in a removed namespace expires with its retention.
{
"rolldown": "rolldown/rolldown",
"vite-plus": "voidzero-dev/vite-plus",
"void": "voidzero-dev/void",
"void": { "repository": "voidzero-dev/void", "pullRequests": true },
}
17 changes: 13 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Vite+ remote cache for Cloudflare

This repository is Vite+'s Cloudflare implementation of a remote cache server for `vp run`, designed in the [remote cache RFC](rfcs/0001-remote-cache.md) ([PR #716](https://github.com/voidzero-dev/vite-task/pull/716)): a TypeScript Worker, primary D1 metadata, and a private R2 Standard bucket. Anyone can read an enabled namespace. Only a signed GitHub Actions token for the registered public repository's main-branch `push` job can publish.
This repository is Vite+'s Cloudflare implementation of a remote cache server for `vp run`, designed in the [remote cache RFC](rfcs/0001-remote-cache.md) ([PR #716](https://github.com/voidzero-dev/vite-task/pull/716)): a TypeScript Worker, primary D1 metadata, and a private R2 Standard bucket. Anyone can read an enabled namespace. Only a signed GitHub Actions token for the registered public repository's main-branch `push` job can publish, unless an operator opts the namespace in to [pull request uploads](#pull-request-uploads).

The package contains no `vp run` client adapter. Cache keys, values, and blobs remain opaque. A successful lookup does not prove that a result is reusable; a client must validate its inputs and output archive.

Expand Down Expand Up @@ -76,7 +76,15 @@ For a deployment on Workers Paid, explicitly select `--profile paid` when you ne
pnpm operator setup --name my-public-cache --namespace docs --repo owner/repository --origin https://cache.example.com --profile paid --retention-days 30 --byte-limit 30000000000
```

Set GitHub Actions `id-token: write` only on the trusted publishing job. The token audience must equal the printed endpoint. No cache secret is needed in GitHub. A token with a customized `sub` can work because policy uses signed IDs, branch, visibility, and event claims. Tokens for fork repositories, tags, PRs, `pull_request_target`, or `workflow_run` are denied.
Set GitHub Actions `id-token: write` only on the trusted publishing job. The token audience must equal the printed endpoint. No cache secret is needed in GitHub. A token with a customized `sub` can work because policy uses signed IDs, branch, visibility, and event claims. Tokens for fork repositories, tags, `pull_request_target`, or `workflow_run` are denied. PR tokens are denied too, unless the namespace opts in to pull request uploads.

### Pull request uploads

`pull_request` tokens are denied by default. As an interim measure, an operator can opt a namespace in with `pnpm operator policy --namespace docs --pull-requests on`, or pass `--pull-requests on` to the setup that creates the namespace. `--pull-requests off` withdraws the opt-in. An opted-in namespace accepts a `pull_request` token that passes the same audience, repository/owner ID, and visibility checks as a push and also has `ref` `refs/pull/<number>/merge`, `ref_type` `branch`, and a `base_ref` equal to the registered default branch. PRs that target another branch, `pull_request_target`, `workflow_run`, tags, and workflows in fork repositories remain denied.

The opt-in carries a risk: PR entries share the namespace with default-branch entries. Code from any same-repository PR, including Renovate dependency branches, runs before review and can publish an entry that a later default-branch build restores. That is why it is off by default. The long-term fix is to [scope pull request uploads to the pull request](https://github.com/voidzero-dev/vite-plus-remote-cache-cloudflare/issues/16).

Fork PR workflows run in the base repository, so their tokens carry its repository and owner IDs; the server cannot tell them from same-repository PRs. The policy depends on GitHub not granting `id-token: write` to fork PR workflows. GitHub grants write permissions to them only when the repository enables **Send write tokens to workflows from pull requests**, a setting [GitHub offers for private repositories only](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/enabling-features-for-your-repository/managing-github-actions-settings-for-a-repository#enabling-workflows-for-forks-of-private-repositories). GitHub treats Dependabot PRs like fork PRs.

## Limits and authorization

Expand All @@ -99,7 +107,7 @@ Set GitHub Actions `id-token: write` only on the trusted publishing job. The tok

The `LIMITS` Wrangler variable is a JSON object with optional keys `key`, `value`, `metadata`, `fetch`, `blob`, `store`, `headers`, and `deadlineMs`. It can lower the tested ceilings. Envelope limits must accommodate their field limits and framing. Raising ceilings requires code changes and new memory, CPU, and account-limit measurements. An HTTP request without `Content-Length` reserves the full store limit. Actual bytes replace that reservation only at publication; pending, retired, and deleting generations remain charged.

GitHub authorization uses `jose`, `RS256`, the fixed issuer `https://token.actions.githubusercontent.com`, and its fixed HTTPS JWKS endpoint. The server checks exact string types for audience, repository/owner IDs, visibility, branch, ref type, and event. It requires integer `exp`, `nbf`, and `iat`, permits 30 seconds of skew for not-before/issued-at checks, accepts at most a 15-minute token lifetime, and never publishes after expiry. Token headers cannot supply signing-key URLs or embedded keys.
GitHub authorization uses `jose`, `RS256`, the fixed issuer `https://token.actions.githubusercontent.com`, and its fixed HTTPS JWKS endpoint. The server checks exact string types for audience, repository/owner IDs, visibility, branch (`ref`, or `base_ref` for an opted-in pull request), ref type, and event. It requires integer `exp`, `nbf`, and `iat`, permits 30 seconds of skew for not-before/issued-at checks, accepts at most a 15-minute token lifetime, and never publishes after expiry. Token headers cannot supply signing-key URLs or embedded keys.

Tokens are limited to 16 KiB. JWKS bodies are limited to 64 KiB, requests to five seconds, cached keys to ten minutes, and refresh attempts to one per 30 seconds per isolate, including failures. Unknown key IDs cannot cause unlimited refreshes. Missing usable keys fail closed. A maintainer's policy change increments its version; publication checks that version, current scope/deployment state, token expiry, lease, bytes, and identity quotas inside the D1 transaction.

Expand All @@ -112,6 +120,7 @@ pnpm operator status
pnpm operator bind --namespace another-project --repo owner/another-repository
pnpm operator bind --namespace docs --repo owner/repository
pnpm operator policy --namespace docs --writes off
pnpm operator policy --namespace docs --pull-requests on
pnpm operator policy --namespace docs --enabled off
pnpm operator policy --namespace docs --enabled on --writes on
pnpm operator policy --namespace docs --retention-days 30 --byte-limit 30000000000
Expand All @@ -125,7 +134,7 @@ pnpm operator upgrade

`status` reports policy, charged bytes, entry/association counts, generation states, cleanup eligibility, and actual D1 storage. It warns at 400 MB. Set provider alerts at 80% of request, CPU, R2 operation/storage, and D1 read/write/storage allowances. Monitor cleanup delay and pending bytes, and pause writes before a backlog reaches the storage ceiling. Public requests and other services in the same account can exhaust allowances even when this cache's byte budget is respected.

Sampled structured logs include request ID, namespace, operation, exact/fallback/miss/error outcome, HTTP status, request/response sizes, duration, D1 rows/query latency/storage, and R2 operation attempts. Verified writes add signed repository ID, workflow ref, run ID/attempt, and commit SHA. They exclude tokens, keys, values, blobs, and database errors. `LOG_SAMPLE_RATE` defaults to `0.1` and applies to failures too. Provider transfer metrics account for disconnected downloads; logged response size describes the selected response. A server exact hit is distinct from a client-validated cache hit.
Sampled structured logs include request ID, namespace, operation, exact/fallback/miss/error outcome, HTTP status, request/response sizes, duration, D1 rows/query latency/storage, and R2 operation attempts. Verified writes add signed repository ID, workflow ref, run ID/attempt, commit SHA, event name, and a pull request's head branch. They exclude tokens, keys, values, blobs, and database errors. `LOG_SAMPLE_RATE` defaults to `0.1` and applies to failures too. Provider transfer metrics account for disconnected downloads; logged response size describes the selected response. A server exact hit is distinct from a client-validated cache hit.

### Cleanup and withdrawal

Expand Down
6 changes: 4 additions & 2 deletions docs/e2e-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ GitHub can suppress `pull_request` workflows when a PR has merge conflicts. Reso

## Authorization and test levels

The production write policy requires a real GitHub token with `event_name=push`, the registered repository and owner IDs, its configured main branch, and the exact namespace audience. A PR token or a `workflow_dispatch` token cannot pass this policy. GitHub signs the event claim; the test runner cannot change it.
The production write policy requires a real GitHub token with `event_name=push`, the registered repository and owner IDs, its configured main branch, and the exact namespace audience. A `workflow_dispatch` token cannot pass this policy, and neither can a PR token unless its namespace opts in to [pull request uploads](../README.md#pull-request-uploads). Staging keeps that opt-in off, so PR runs keep checking that PR tokens are rejected. GitHub signs the event claim; the test runner cannot change it.

| Level | Trigger | What it proves |
| ---------------------------- | ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
Expand Down Expand Up @@ -177,6 +177,8 @@ The workflow repeats `pnpm check` and `pnpm smoke`, then runs `pnpm ci:deploy pr

To bind another repository, add `"namespace": "owner/repository"` to `.github/production-repositories.jsonc`, merge the change, and run the workflow. Only public repositories can publish. The list decides which namespaces serve reads and accept uploads: each deployment enables every listed namespace and disables every other namespace, so a `pnpm operator policy` change lasts only until the next deployment. To withdraw a repository, remove its entry and run the workflow. For an immediate stop, also run `pnpm operator policy --namespace <namespace> --enabled off`. Data in a disabled namespace expires with its retention; `pnpm operator purge` deletes it sooner. Deployments never change the cache-wide `pnpm operator deployment` switches or move a namespace to another repository. To roll back, re-run an earlier successful production run. A re-run deploys its original commit.

An entry can also be an object. `{ "repository": "owner/repository", "pullRequests": true }` also accepts uploads from `pull_request` runs in that repository whose PR targets its default branch; `"owner/repository"` is short for `"pullRequests": false`. Each deployment also sets every namespace's pull request opt-in from the list, overriding `pnpm operator policy --pull-requests`. This opt-in is interim, and only `void` has it. Code from any same-repository PR, including Renovate dependency branches, runs before review and can publish an entry that a later default-branch build restores. Enable it only for a repository whose maintainers accept that risk. The long-term fix is to [scope pull request uploads to the pull request](https://github.com/voidzero-dev/vite-plus-remote-cache-cloudflare/issues/16). Fork PRs stay out only because GitHub does not grant their workflows `id-token: write`; see [Pull request uploads](../README.md#pull-request-uploads).

Production uses the same `CLOUDFLARE_ACCOUNT_ID` and `CLOUDFLARE_API_TOKEN` secrets as staging. Secrets in the `production` environment take precedence, so a production-only token can replace them without workflow changes. Internal PR staging runs receive the repository secrets; while both environments share one token, anyone who can push a branch here can change production resources. Add required reviewers to the `production` environment to approve each deployment.

Operator commands read `wrangler.operator.json`, which the workflow does not keep. To recreate it, check out the deployed commit and repeat `pnpm operator setup` with the Worker name from `scripts/ci/production.ts` and an entry from `.github/production-repositories.jsonc`. Setup redeploys that checkout.
Expand All @@ -191,7 +193,7 @@ Before a production release, record the commit, workflow URL, Cloudflare plan, r
| Maximum payloads and real Cron | Run two concurrent authorized stores with 4 MiB values and 64 MiB blobs; verify their digests. Expire a fixture and observe scheduled D1/R2 deletion and accounting release |
| Upload interruption | Disconnect a real authorized multipart request after its first R2 part. Verify unchanged mappings, retained reservation, upload abortion, and eventual byte release |
| Lease/token expiry and policy race | Delay a staging upload across expiry or change policy before publication. Neither mapping changes; old data remains readable |
| Real hostile workflow identities | Use controlled fork, tag, PR, `pull_request_target`, and `workflow_run` jobs. All writes fail; public reads still work |
| Real hostile workflow identities | Use controlled fork, tag, PR, `pull_request_target`, and `workflow_run` jobs against a namespace without the pull request opt-in. All writes fail; public reads still work |
| Rate limits | Send a bounded burst to the staging namespace. Check `429`, `Retry-After`, recovery, and a finite identity set for unknown routes |
| Retention and lifecycle | Observe expiry, replacement grace, unfinished multipart abortion, and lifecycle deletion over actual retention windows |
| Migration and rollback | Upgrade a populated staging database, verify old entries, and restore a compatible previous Worker version. Never roll code back across an incompatible schema change |
Expand Down
Loading
Loading