ethercalc
Version:
Multi-User Spreadsheet Server — TypeScript rewrite (Cloudflare fullstack)
170 lines (154 loc) • 7.14 kB
text/typescript
/**
* 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`).
*/
export interface Env {
/** One Durable Object per spreadsheet room. */
ROOM: DurableObjectNamespace;
/**
* 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 room. When set, `GET /` 302-redirects into
* `/<ETHERCALC_DEFAULT_ROOM>` instead of serving the "create new
* sheet" landing page. Used by Sandstorm grains — where each grain
* IS a single sheet — to take the user straight to the spreadsheet
* on `/`. Unset on ethercalc.net (shared pool of rooms, where `/`
* legitimately is a landing page).
*/
readonly ETHERCALC_DEFAULT_ROOM?: string;
/**
* Cloudflare `send_email` binding (Phase 9, §13 Q3). When present,
* the cron/email layer wraps it via `BindingEmailSender`. When
* unbound (Node unit tests, or deployments that omit the
* `[[send_email]]` entry in wrangler.toml), callers fall back to
* `StubEmailSender`. Shape is structural — we only invoke `.send()`.
*/
readonly EMAIL?: {
send(message: {
from: string;
to: string;
raw: string | ReadableStream<Uint8Array>;
}): Promise<unknown>;
};
/**
* 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 the legacy migration write routes
* and hosted `POST /_/:room/pitr-restore`. When unset, those routes
* return `404`; migration and recovery are operator-only actions, not
* public surfaces. See `src/lib/migrate-auth.ts` for the verifier and
* `src/routes/{migrate,rooms}.ts` for the Hono glue.
*
* Typical local flow:
* echo 'ETHERCALC_MIGRATE_TOKEN="local-only"' > packages/worker/.dev.vars
* ./bin/ethercalc # Miniflare
* bun run migrate -- --input dump.rdb \
* --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-<room>`. 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` / first `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;
}