/** * Environment bindings available to the Worker at runtime. Kept in one place * so Hono's generic type argument stays in sync with `wrangler.toml`. * * Runtime caveat: under standalone workerd (`config.capnp` * `fromEnvironment` bindings), an env var that is UNSET in the process * environment arrives here as `null`, not `undefined` and not `''`. * Guard optional string fields with truthiness or `!= null`, never * `!== undefined` (see `lib/room-index-access.ts`). */ import type { SessionPrincipal } from './lib/session.ts'; export interface Env { /** One Durable Object per spreadsheet room. */ ROOM: DurableObjectNamespace; /** Singleton Durable Object for WebAuthn credentials and sessions. */ readonly AUTH?: DurableObjectNamespace; /** Enable passkey authentication and private-room routes. */ readonly ETHERCALC_AUTH?: string | null; /** WebAuthn relying-party identifier, for example `ethercalc.net`. */ readonly ETHERCALC_RP_ID?: string | null; /** Human-readable WebAuthn relying-party name. */ readonly ETHERCALC_RP_NAME?: string | null; /** Trusted WebAuthn origin, including scheme and optional port. */ readonly ETHERCALC_ORIGIN?: string | null; /** * D1 binding — cross-room index mirror (Phase 5.1). Authoritative room * state lives in the DO's own storage; this table provides the * cross-room query surface used by `/_rooms`, `/_roomlinks`, * `/_roomtimes`. Every snapshot mutation in `src/room.ts` upserts * here via `mirrorRoomToD1`; `DELETE /_do/all` removes the row via * `deleteRoomFromD1`. See AGENTS.md §3.3 and §10.2. * * Optional because Node unit tests construct `RoomDO` and the Hono * app without a Miniflare-bound D1; the mirror helpers (and the * `/_rooms*` handlers) no-op / return empty when the binding is * missing. */ readonly DB?: D1Database; /** * Static assets bound via Workers Assets. Used by the stateless route * layer to serve `index.html`, `start.html`, icons, `manifest.appcache`, * and so on. In local tests we may leave this undefined — routes fall * back to a 404 when ASSETS is absent (see `src/routes/assets.ts`). * * The asset pipeline itself (collecting the legacy repo-root files into * an asset dir) lands in Phase 11 (§8). Phase 4 only scaffolds the * binding so the rest of the wiring is ready. */ readonly ASSETS?: Fetcher; /** * HMAC secret used to gate `edit`/`view`/`app` routes and WS `execute` * commands. When unset, the server falls back to identity HMAC and * anonymous operation (matches legacy `--key` absent behavior; see * AGENTS.md §6.4). */ readonly ETHERCALC_KEY?: string; /** * Optional URL basepath prefix (legacy `--basepath`). Threaded through * the redirect builders so deployments behind a sub-path router stay * consistent. Defaults to empty string. */ readonly BASEPATH?: string; /** * DevMode flag. Legacy `src/main.ls` checked `fs.existsSync('.git')` to * decide whether to serve the dynamic `manifest.appcache` stub. Under * Workers we can't hit the filesystem, so we expose an explicit env * var: any truthy string (`'1'` or `'true'`) enables the DevMode path * which returns a fresh-timestamp CACHE MANIFEST body. Defaults to * off; production asset serving goes through `env.ASSETS` for the * static manifest file. */ readonly DEVMODE?: string; /** * Single-grain default workbook. When set, `GET /` 302-redirects into * this room. Fresh rooms and multi-sheet TOCs open the React workbook * interface; existing ordinary sheets keep the classic single-sheet * interface. Used by Sandstorm grains, where `/` must open the grain's * workbook rather than the shared "create new spreadsheet" landing page. * Unset on ethercalc.net. */ readonly ETHERCALC_DEFAULT_ROOM?: string; /** * Cloudflare Email Service `send_email` binding (Phase 9, §13 Q3). * Unbound deployments use `DisabledEmailSender`. The narrow structural * shape matches the recommended Workers structured-message API. */ readonly EMAIL?: { send(message: { readonly from: { readonly email: string; readonly name?: string }; readonly to: string; readonly subject: string; readonly text: string; }): Promise; }; /** * Default "from" address for outgoing mail (Phase 9). Must be a * verified sender in the Cloudflare dashboard for the bound zone. * Defaults to `noreply@ethercalc.invalid` when unset — the stub * sender ignores it entirely, so this only matters once `EMAIL` is * bound. */ readonly EMAIL_FROM?: string; /** * Operator bearer token that unlocks migration writes, hosted PITR restore, * and the side-effecting legacy `GET /_timetrigger` compatibility route. * When unset those surfaces return `404`. See `src/lib/migrate-auth.ts`. * * Typical local flow: * echo 'ETHERCALC_MIGRATE_TOKEN="local-only"' > packages/worker/.dev.vars * ./bin/ethercalc # Miniflare * vp run @ethercalc/migrate#migrate --source file:///path/to/dump-dir \ * --target http://127.0.0.1:8000 --token local-only */ readonly ETHERCALC_MIGRATE_TOKEN?: string; /** * Legacy room-index gate, kept for hosted/back-compat (`wrangler.toml` * pins `'1'` for production Cloudflare deploys; the CLI `--cors` flag * sets it too). When enabled it 403s the cross-room discovery * endpoints: `/_rooms`, `/_roomlinks`, `/_roomtimes`, and * `/_exists/:room`. CORS *headers* are no longer affected — they are * emitted unconditionally for embed compatibility. Sensible-fix note: * parsed via `flagEnabled` (boolean-string), so `'0'`/`'false'`/`'no'`/ * `'off'` now read as gate-OFF, where the pre-2026-06 code treated any * non-empty string as gate-ON. Prefer `ETHERCALC_DISABLE_ROOM_INDEX` * below for new configs. */ readonly ETHERCALC_CORS?: string; /** * Clearly named self-host switch for hiding cross-room discovery endpoints: * `/_rooms`, `/_roomlinks`, `/_roomtimes`, and `/_exists/:room`. When unset, * the Worker falls back to the legacy `ETHERCALC_CORS` gate above so hosted * deployments keep their existing default. */ readonly ETHERCALC_DISABLE_ROOM_INDEX?: string; /** * Room TTL in SECONDS (legacy `--expire` / Redis `EXPIRE` semantics, * §13 Q10). When set, the RoomDO's housekeeping `alarm()` wipes any * room whose `meta:updated_at` is older than this TTL — the DO-storage * equivalent of the legacy `EXPIRE snapshot-`. Unset (the * default) means rooms live forever. Wired by `bin/ethercalc` into the * Miniflare env; on Cloudflare it's an optional `[vars]` entry. */ readonly ETHERCALC_EXPIRE?: string; /** * Optional per-IP HTTP rate limit for self-host (§13 Q7). Unset or * false-like → off (hosted deploys rely on the Cloudflare edge). * Bare `1`/`true`/`on` → 10 r/s with burst 30 (nginx recipe default). * Plain number `N` → `N` requests/s; `window:max` → `max` per `window` * seconds. Keyed on `CF-Connecting-IP` / rightmost `X-Forwarded-For` hop. */ readonly ETHERCALC_RATELIMIT?: string; /** * Optional per-IP cap on room-creation routes (SH-3). Unset → off. * Bare `1`/`true` → 6 new rooms per minute per source. */ readonly ETHERCALC_ROOM_CREATE_LIMIT?: string; /** * When truthy, honour Sandstorm `X-Sandstorm-Permissions` (SH-6). * Set by `run_grain.sh`; inert on Cloudflare/Docker self-host. */ readonly ETHERCALC_SANDSTORM?: string; } /** Shared Hono binding and request-local identity types for every route. */ export interface EtherCalcHonoEnv { Bindings: Env; Variables: { principal: SessionPrincipal | null | undefined; }; }