name = "ethercalc"
main = "src/index.ts"
compatibility_date = "2024-11-12"
compatibility_flags = ["nodejs_compat"]

# Disable the `*.workers.dev` hostname for production (L-14). It serves the
# identical Worker but lives OUTSIDE the `ethercalc.net` zone, so any zone-
# scoped WAF / Rate Limiting rules (the whole §13 Q7 abuse-prevention plan)
# do NOT apply to it — an attacker could hit the workers.dev URL and bypass
# every rate-limit. With this false, only the zone-protected custom domains
# below serve traffic. Migration tooling must target the custom domain
# (https://ethercalc.net) rather than the old workers.dev URL.
workers_dev = false

# Custom domains. Requires the `ethercalc.net` zone to live under this
# Cloudflare account (one-time nameserver swap at the registrar). On
# deploy, wrangler auto-creates proxied DNS records and provisions TLS.
# `override_existing_dns_record = true` replaces the legacy A/CNAME
# records (2026-04-22 cutover from Zappa/Node to this worker).
[[routes]]
pattern = "ethercalc.net"
zone_name = "ethercalc.net"
custom_domain = true
[[routes]]
pattern = "www.ethercalc.net"
zone_name = "ethercalc.net"
custom_domain = true
# Durable Object: one instance per spreadsheet room.
# Holds live SocialCalc state, WebSocket clients, and the snapshot/log/audit
# storage. See AGENTS.md §3.2 and §3.3.
[[durable_objects.bindings]]
name = "ROOM"
class_name = "RoomDO"

[[migrations]]
tag = "v1"
new_sqlite_classes = ["RoomDO"]

# Default vars — overridden per-deploy by `wrangler secret put` or
# wrangler.toml overlays. `ETHERCALC_KEY` is unset by default (anonymous
# mode / identity HMAC); production deploys should run
# `wrangler secret put ETHERCALC_KEY` instead of committing a value.
# `BASEPATH` is empty by default (legacy `--basepath` CLI flag).
#
# §13 Q6: the CLI (`bin/ethercalc --key …`, Phase 11) will also set
# ETHERCALC_KEY in the Miniflare env for self-host parity.
[vars]
BASEPATH = ""
ETHERCALC_CORS = "1"

# Staging overlay — isolated from production routes and cron triggers. It uses
# its own D1 database and a temporary workers.dev hostname for release checks.
[env.staging]
name = "ethercalc-staging"
workers_dev = true
preview_urls = true
routes = []

[env.staging.vars]
BASEPATH = ""
ETHERCALC_CORS = "1"

[[env.staging.durable_objects.bindings]]
name = "ROOM"
class_name = "RoomDO"

[[env.staging.d1_databases]]
binding = "DB"
database_name = "ethercalc_rooms_staging"
database_id = "273b1db3-17bc-44dd-bbc2-62ce1727abde"
migrations_dir = "./migrations"

[env.staging.assets]
directory = "../../assets"
binding = "ASSETS"

[env.staging.triggers]
crons = []

# Workers Assets binding (Phase 4.1/11). `directory` is resolved relative
# to this wrangler.toml, so it points up two levels to the repo-root
# `assets/` produced by `scripts/build-assets.ts`. That builder copies the
# curated set of static files (index.html, start.html, panels.html,
# icons, manifest.appcache, manifest.json, l10n/*.json, the built
# single-sheet client at static/player.js, the SocialCalc runtime at
# static/socialcalc.js, and the multi-sheet React app under multi/).
#
# The legacy repo root contains `node_modules/` and other >25 MiB files
# that trip Workers' per-asset size limit, so we point at the curated
# directory instead of `./`. Also `assets/` is .gitignored — CI rebuilds
# it before `wrangler deploy --dry-run`.
#
# Runtime fallback: when `ASSETS` is unbound (test env), the route layer
# at `src/routes/assets.ts` returns 404. Integration tests stub the
# binding with a mock Fetcher to exercise the proxied path.
[assets]
directory = "../../assets"
binding = "ASSETS"

# Phase 9 — Cron Trigger. Replaces the legacy external-cron `GET
# /_timetrigger` ping; Cloudflare invokes the Worker's `scheduled()`
# handler every minute on this schedule. The backwards-compat HTTP
# endpoint `/_timetrigger` is kept for self-host users whose external
# cron still points at it (see `src/routes/timetrigger.ts`).
[triggers]
crons = ["*/1 * * * *"]

# Phase 9 — Cloudflare `send_email` binding (§13 Q3). The actual
# destination address must be verified in the Cloudflare dashboard
# before `wrangler deploy`; see FINDINGS.md for the setup steps. Under
# vitest-pool-workers / Miniflare this binding is UNBOUND — the
# `EmailSender` constructor in `src/handlers/cron.ts` falls back to
# `StubEmailSender` when `env.EMAIL` is undefined.
#
# The `name = "EMAIL"` key must match `Env.EMAIL` in src/env.ts.
#
# Commented out by default: miniflare + vitest-pool-workers currently
# don't ship a `send_email` shim, and uncommenting this during tests
# fails binding validation. Production deploys should uncomment this
# section AND verify the destination address via the Cloudflare
# dashboard. See FINDINGS.md "Phase 9 — send_email binding setup".
# [[send_email]]
# name = "EMAIL"

# KV, R2 bindings will be added in subsequent phases as they are used.
# Scaffolding them eagerly would force empty bindings into the local
# Miniflare state and muddy the test environment.

# D1 — cross-room index mirror (Phase 5.1). DO storage is the
# authoritative source of truth for a single room; this table provides
# the cross-room query surface needed by `/_rooms`, `/_roomlinks`,
# `/_roomtimes`. Mirror writes land in `src/room.ts` after every
# snapshot mutation; reads in `src/routes/rooms.ts` fall back to empty
# when the binding is absent (Node unit tests without Miniflare).
#
# Binding name is `DB`. Schema at `migrations/0001_rooms.sql`. The
# `database_id` placeholder is fine for local Miniflare and `wrangler
# deploy --dry-run` — a real id is required only for an actual
# `wrangler deploy`. See §10.2 for the Redis → D1 mapping table.
[[d1_databases]]
binding = "DB"
database_name = "ethercalc_rooms"
database_id = "bd9247bd-5b50-4c47-8ce6-de3196511684"
migrations_dir = "./migrations"

# Load SocialCalc.js as a Text module so `new Function(rawSource)` inside
# `@ethercalc/socialcalc-headless` can eval it. The `?raw` Vite suffix
# handles this under vitest-pool-workers (Vite resolves it directly);
# wrangler's esbuild bundler needs an explicit rule, otherwise it tries
# to parse the 27k-line UMD (sloppy-mode `delete varname;` that esbuild
# rejects in strict ESM).
#
# Important: this rule is only consumed by `wrangler deploy`. The
# `vitest.config.ts` sets its miniflare options directly (no `wrangler:
# { configPath }`) so this rule doesn't leak into the vitest runner and
# mangle `?raw` imports with `?mf_vitest_force=Text`.
[[rules]]
type = "Text"
globs = ["**/SocialCalc.js?raw", "**/SocialCalc.js"]
fallthrough = true

# `unsafe_eval` binding is no longer needed: `socialcalc.bundled.ts` is
# transpiled at build time into the worker bundle (see
# `packages/socialcalc-headless/scripts/build.js`). No runtime `eval` /
# `new Function` calls remain, so CF validation no longer needs the
# capability object. If we ever revert to `?raw`-import + eval, re-add:
#
#   [[unsafe.bindings]]
#   name = "UNSAFE_EVAL"
#   type = "unsafe_eval"