Skip to content

Commit 9bfb23d

Browse files
committed
Settings live in cloudflare.config.ts; drop compatibility.ts and the nodejs_compat flag; .env holds only the admin password
1 parent 260e2b2 commit 9bfb23d

8 files changed

Lines changed: 60 additions & 70 deletions

File tree

.env.example

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,3 @@
1-
# Sourced into the shell before `pnpm run deploy`: set -a; source .env; set +a
2-
# ADMIN_PASSWORD is written here by `pnpm admin:password`
3-
# LABELERS='[]' # disable the default Bluesky moderation-service labeler
4-
# IMAGES_ENABLED=true
5-
# BLOB_MAX_SIZE=25mb
6-
# MODE=scoped
7-
# SCOPED_COLLECTIONS=app.example.*
8-
# JETSTREAM_URL=wss://jetstream2.us-east.bsky.network
1+
# Written by `pnpm admin:password`; read by `pnpm test:deployed` and handy for curl -u.
2+
# All other settings live in cloudflare.config.ts.
3+
ADMIN_PASSWORD=

AGENTS.md

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,11 @@ is a bug.
5454
10. **Scoped-mode admission is a forward `getRecord` membership check**
5555
through the `Record` entrypoint; never build a reverse index of blob
5656
references. Open and scoped routes never coexist in one deployment.
57-
11. **Config values come from `loadConfig(env)`** in `src/config.ts`;
58-
every setting has a default in `CONFIG_DEFAULTS` and a matching text
59-
binding in `cloudflare.config.ts`. Adding a setting means touching
60-
both plus the README table.
57+
11. **Settings are text bindings in `cloudflare.config.ts`** — that file
58+
is the user-facing configuration. `loadConfig(env)` in `src/config.ts`
59+
parses them, with `CONFIG_DEFAULTS` as the fallback for environments
60+
that omit a binding (tests). Adding a setting means touching both plus
61+
the README table. Nothing reads `process.env` at build time.
6162

6263
## Layout
6364

@@ -74,19 +75,19 @@ src/labels.ts drain.ts labeler config, queryLabels client, subscribeLabels dr
7475
src/jetstream.ts socket.ts Jetstream drain; shared outbound-websocket reader
7576
src/store.ts the only KV schema: cursors, drain status, record-level deny set
7677
src/config.ts CONFIG_DEFAULTS and loadConfig
77-
cloudflare.config.ts bindings, exports (per-entrypoint cache), cron; text bindings read process.env at deploy
78+
cloudflare.config.ts settings (text bindings), exports (per-entrypoint cache), Images, KV, cron
7879
test/ vitest inside workerd; test/integration/ runs on Node against the deployed Worker
7980
```
8081

8182
## Commands
8283

8384
- `pnpm dev` — local dev server (real PLC/PDS, local cache semantics)
8485
- `pnpm test` — workerd suite; `pnpm test:deployed` — HTTP suite against
85-
production (`set -a; source .env; set +a` first; it purges, so run it
86+
production (reads `ADMIN_PASSWORD` from `.env`; it purges, so run it
8687
sparingly — the purge rate limit bites after a few runs)
8788
- `pnpm check` / `pnpm fix` — oxfmt + oxlint + types
8889
- `pnpm run deploy``cf deploy` (plain `pnpm deploy` is a pnpm builtin).
89-
Source `.env` first; it supplies the text-binding values.
90+
- `pnpm admin:password` — generate the admin secret, set it, save it to `.env`.
9091

9192
## Testing notes
9293

@@ -130,8 +131,9 @@ test/ vitest inside workerd; test/integration/ runs on Node
130131
`@cloudflare/config` — different unique symbols break type inference).
131132
`Env` is inferred from it; there is no generated vars list to regenerate.
132133
- The entrypoint import uses `with { type: "cf-worker" }`; keep that form.
133-
- `compatibility.ts` is shared with the vitest runtime so tests match
134-
production.
134+
- The compatibility date appears in `cloudflare.config.ts` and
135+
`vite.config.ts`; change both. `nodejs_compat` is implied by dates
136+
≥ 2026-08-04; do not add it back.
135137
- `vite` must stay aliased to `@voidzero-dev/vite-plus-core` and the
136138
`vitest` pin must match vite-plus (`pnpm exec vp toolchain`).
137139
- Formatting uses tabs; `.claude/docs/**` is excluded from formatting.

README.md

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ against the CID before serving, enforces a MIME allowlist by sniffing, and
77
applies moderation from the ATProto labeler ecosystem by purging the cache.
88

99
```
10-
https://your-worker.example/did:plc:ewvi7nxzyoun6zhxrhs64oiz/bafkreihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku
10+
https://your-worker.example/did:plc:<did>/<cid>
1111
```
1212

1313
Cache hits are served by Cloudflare without running the Worker. A miss
@@ -28,8 +28,6 @@ git clone https://github.com/ascorbic/cumulus
2828
cd cumulus
2929
pnpm install
3030
pnpm cf auth login
31-
cp .env.example .env # optional settings; see Configuration
32-
set -a; source .env; set +a
3331
pnpm run deploy
3432
```
3533

@@ -41,31 +39,32 @@ it with:
4139
pnpm admin:password
4240
```
4341

44-
This generates a random password, saves it to `.env` as `ADMIN_PASSWORD`
45-
(gitignored) and sets it as the Worker's secret. Run it again any time to
46-
rotate. Then `pnpm run deploy`.
42+
This generates a random password, sets it as the Worker's secret, and
43+
saves it to `.env` (gitignored) so you can find it later — that file holds
44+
nothing else. Run it again any time to rotate. Then `pnpm run deploy` again.
4745

4846
`pnpm dev` runs it locally (real PLC directory and PDSes, local cache).
4947

5048
## Configuration
5149

52-
Settings are read from the environment at deploy time and baked into the
53-
Worker as bindings, so `.env` is the configuration file. Quote JSON values.
50+
Settings are plain values in [`cloudflare.config.ts`](cloudflare.config.ts);
51+
edit them there and deploy. Everything except `ADMIN_PASSWORD` is
52+
checked in, so a clone reproduces the deployment.
5453

5554
| Variable | Default | Meaning |
5655
| ------------------------ | ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
57-
| `ADMIN_PASSWORD` | (secret, required) | Basic-auth password for `/admin/*`. |
56+
| `ADMIN_PASSWORD` | secret | Basic-auth password for `/admin/*`; set with `pnpm admin:password`. |
5857
| `BLOB_MAX_SIZE` | `3mb` | Largest blob served. 3 MB keeps a miss under the Free plan's 10 ms CPU cap; set `25mb` on Workers Paid. |
5958
| `BLOB_ALLOWED_MIMETYPES` | `image/jpeg,image/png,image/webp,image/avif,image/gif` | Sniffed types that may be served. SVG is excluded on purpose: it is a script container, and serving it from a shared origin is an XSS risk even with a restrictive CSP. |
6059
| `BLOB_FETCH_TIMEOUT` | `30s` | PDS and directory request timeout. |
6160
| `PLC_URL` | `https://plc.directory` | DID PLC directory. |
6261
| `BROWSER_MAX_AGE` | `3600` | `Cache-Control: max-age` sent to clients. |
6362
| `EDGE_MAX_AGE` | `31536000` | How long Cloudflare keeps an entry. Purge, not expiry, is the takedown mechanism. |
64-
| `LABELERS` | Bluesky moderation service, `!takedown` | JSON array of labelers to enforce: `\'[{"did":"did:plc:…","vals":["!takedown"]}]\'`. Set `\'[]\'` to enforce nothing. |
63+
| `LABELERS` | Bluesky moderation service, `!takedown` | JSON array of labelers to enforce: `[{"did":"did:plc:…","vals":["!takedown"]}]`. `[]` enforces nothing. |
6564
| `LABELER_FAIL_OPEN` | `true` | Serve when a labeler is unreachable (the verdict is not cached). |
6665
| `POLICY_URL` | none | External policy service: `GET {POLICY_URL}/{did}/{cid}` → 200 allow, 403 deny. |
6766
| `POLICY_FAIL_OPEN` | `false` | Serve when the policy service is down. |
68-
| `IMAGES_ENABLED` | unset | `true` adds the Images binding and enables `/img/` presets. |
67+
| `IMAGES` | on | The Images binding, which enables `/img/` presets. Delete the line to disable them. |
6968
| `MODE` | `open` | `scoped` restricts the proxy to blobs referenced by records in allowlisted collections. |
7069
| `SCOPED_COLLECTIONS` | none | Scoped mode: NSIDs or prefixes, e.g. `app.example.post,app.example.*`. |
7170
| `JETSTREAM_URL` | none | Scoped mode: Jetstream endpoint, e.g. `wss://jetstream2.us-east.bsky.network`. |
@@ -91,7 +90,7 @@ Open mode:
9190
GET /{did}/{cid} the blob
9291
HEAD /{did}/{cid} headers only, same cache entry
9392
GET /metadata/{did}/{cid} { mime, ext, size, width, height, animated }
94-
GET /img/{preset}/plain/{did}/{cid} resized variant (IMAGES_ENABLED)
93+
GET /img/{preset}/plain/{did}/{cid} resized variant (IMAGES binding)
9594
GET /img/{preset}/plain/{did}/{cid}@jpeg …with an output format: webp (default), jpeg, png
9695
GET /healthz 200, never cached
9796
```
@@ -221,7 +220,7 @@ the limit; hammering the admin API in a loop is not.
221220

222221
```sh
223222
pnpm test # unit and Worker tests, run inside workerd
224-
pnpm test:deployed # HTTP suite against the deployed Worker (needs ADMIN_PASSWORD)
223+
pnpm test:deployed # HTTP suite against the deployed Worker (reads ADMIN_PASSWORD from .env)
225224
pnpm check # format, lint, types
226225
pnpm build
227226
```

cloudflare.config.ts

Lines changed: 21 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,13 @@ import {
44
exports,
55
triggers,
66
} from "@cloudflare/vite-plugin/experimental-config";
7-
import { compatibilityDate, compatibilityFlags } from "./compatibility.ts";
8-
import { CONFIG_DEFAULTS } from "./src/config.ts";
97
import * as entrypoint from "./src/index.ts" with { type: "cf-worker" };
108

11-
/** Deploy-time overrides come from the environment (`.env` locally). */
12-
function setting(key: keyof typeof CONFIG_DEFAULTS): string {
13-
return process.env[key] ?? CONFIG_DEFAULTS[key];
14-
}
15-
169
export default defineWorker({
1710
name: "cumulus",
1811
entrypoint,
19-
compatibilityDate,
20-
compatibilityFlags,
12+
// Keep in sync with vite.config.ts so tests run on the deployed runtime.
13+
compatibilityDate: "2026-08-22",
2114
cache: {
2215
enabled: true,
2316
crossVersionCache: true,
@@ -29,22 +22,26 @@ export default defineWorker({
2922
},
3023
env: {
3124
VERSION: bindings.versionMetadata(),
32-
ADMIN_PASSWORD: bindings.secret(),
33-
BLOB_MAX_SIZE: bindings.text(setting("BLOB_MAX_SIZE")),
34-
BLOB_ALLOWED_MIMETYPES: bindings.text(setting("BLOB_ALLOWED_MIMETYPES")),
35-
BLOB_FETCH_TIMEOUT: bindings.text(setting("BLOB_FETCH_TIMEOUT")),
36-
PLC_URL: bindings.text(setting("PLC_URL")),
37-
BROWSER_MAX_AGE: bindings.text(setting("BROWSER_MAX_AGE")),
38-
EDGE_MAX_AGE: bindings.text(setting("EDGE_MAX_AGE")),
39-
POLICY_URL: bindings.text(setting("POLICY_URL")),
40-
POLICY_FAIL_OPEN: bindings.text(setting("POLICY_FAIL_OPEN")),
41-
LABELERS: bindings.text(setting("LABELERS")),
42-
LABELER_FAIL_OPEN: bindings.text(setting("LABELER_FAIL_OPEN")),
43-
MODE: bindings.text(setting("MODE")),
44-
SCOPED_COLLECTIONS: bindings.text(setting("SCOPED_COLLECTIONS")),
45-
JETSTREAM_URL: bindings.text(setting("JETSTREAM_URL")),
4625
LABELS_KV: bindings.kv(),
47-
...(process.env.IMAGES_ENABLED === "true" ? { IMAGES: bindings.images() } : {}),
26+
// Set with `pnpm admin:password`.
27+
ADMIN_PASSWORD: bindings.secret(),
28+
// Remove this binding to disable the /img/ presets.
29+
IMAGES: bindings.images(),
30+
31+
// Settings. The README's Configuration section documents each one.
32+
BLOB_MAX_SIZE: bindings.text("3mb"),
33+
BLOB_ALLOWED_MIMETYPES: bindings.text("image/jpeg,image/png,image/webp,image/avif,image/gif"),
34+
BLOB_FETCH_TIMEOUT: bindings.text("30s"),
35+
PLC_URL: bindings.text("https://plc.directory"),
36+
BROWSER_MAX_AGE: bindings.text("3600"),
37+
EDGE_MAX_AGE: bindings.text("31536000"),
38+
LABELERS: bindings.text('[{"did":"did:plc:ar7c4by46qjdydhdevvrndac","vals":["!takedown"]}]'),
39+
LABELER_FAIL_OPEN: bindings.text("true"),
40+
POLICY_URL: bindings.text(""),
41+
POLICY_FAIL_OPEN: bindings.text("false"),
42+
MODE: bindings.text("open"),
43+
SCOPED_COLLECTIONS: bindings.text(""),
44+
JETSTREAM_URL: bindings.text(""),
4845
},
4946
triggers: [triggers.scheduled({ schedule: "*/5 * * * *" })],
5047
observability: {

compatibility.ts

Lines changed: 0 additions & 4 deletions
This file was deleted.

test/integration/deployed.test.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { existsSync, readFileSync } from "node:fs";
12
import { beforeAll, describe, expect, it } from "vitest";
23
import { decodeCborSequence } from "../../src/cbor.ts";
34

@@ -11,7 +12,15 @@ const BASE = (process.env.CUMULUS_URL ?? "https://cumulus.ascorbic.workers.dev")
1112
/\/+$/,
1213
"",
1314
);
14-
const PASSWORD = process.env.ADMIN_PASSWORD;
15+
const PASSWORD = process.env.ADMIN_PASSWORD ?? passwordFromDotenv();
16+
17+
function passwordFromDotenv(): string | undefined {
18+
if (!existsSync(".env")) return undefined;
19+
const line = readFileSync(".env", "utf8")
20+
.split("\n")
21+
.find((entry) => entry.startsWith("ADMIN_PASSWORD="));
22+
return line?.slice("ADMIN_PASSWORD=".length).trim() || undefined;
23+
}
1524
const DID = process.env.TEST_DID ?? "did:plc:uwbl4k3tza7eyjv3morkrld2";
1625
const CID = process.env.TEST_CID ?? "bafkreic4mwsbm2tmuonamj4jq4kcjofk35bwics2f4oorp57f3cdfusjwu";
1726
const MISSING_CID = "bafkreihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku";

tsconfig.json

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,5 @@
2121
"lib": ["es2022"],
2222
"types": ["@cloudflare/vitest-plugin/types"]
2323
},
24-
"include": [
25-
"src",
26-
"test",
27-
"cloudflare.config.ts",
28-
"compatibility.ts",
29-
"vite.config.ts",
30-
"worker-configuration.d.ts"
31-
]
24+
"include": ["src", "test", "cloudflare.config.ts", "vite.config.ts", "worker-configuration.d.ts"]
3225
}

vite.config.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { cloudflare } from "@cloudflare/vite-plugin";
22
import { cloudflareTest } from "@cloudflare/vitest-plugin";
33
import { defineConfig } from "vite-plus";
4-
import { compatibilityDate, compatibilityFlags } from "./compatibility.ts";
54
import { TEST_ADMIN_PASSWORD } from "./test/constants.ts";
65

76
export default defineConfig({
@@ -12,8 +11,8 @@ export default defineConfig({
1211
? cloudflareTest({
1312
main: "./src/index.ts",
1413
miniflare: {
15-
compatibilityDate,
16-
compatibilityFlags,
14+
// Keep in sync with cloudflare.config.ts.
15+
compatibilityDate: "2026-08-22",
1716
versionMetadata: "VERSION",
1817
kvNamespaces: ["LABELS_KV"],
1918
bindings: { ADMIN_PASSWORD: TEST_ADMIN_PASSWORD, LABELERS: "[]" },

0 commit comments

Comments
 (0)