Decouple core from Cloudflare; add a self-hosted Node/Docker deployment path
Cinderbox was fully hard-locked to Cloudflare (D1Database and ForwardableEmailMessage types baked directly into core/db.ts, core/ratelimit.ts, and core/email.ts) and the README read like a Discord bot's docs rather than what this actually is: a small mail-routing engine with a pluggable delivery adapter, of which Discord is the first. - Add SqlExecutor (core/storage.ts): the minimal run/first/all interface core runs SQL against. D1 and SQLite both speak SQLite dialect, so every query in core/db.ts and core/ratelimit.ts is now shared verbatim between both storage backends -- no duplicated business logic, only two thin driver implementations (storage/d1.ts, storage/sqlite.ts via better-sqlite3, chosen over node:sqlite because the latter is still flagged experimental). - core/email.ts no longer takes Cloudflare's ForwardableEmailMessage -- postal-mime's own RawEmail type already accepts everything either deployment target hands it (a Worker's ReadableStream, a Buffer from a self-hosted SMTP server), so that's the only shape core needs. - Add a self-hosted entrypoint (src/node/): an SMTP server for inbound mail (smtp-server), a plain HTTP server for Discord interactions, and a cleanup schedule matching the Workers cron timing. Runs as a plain Node process or via the new Dockerfile, storing state in a local SQLite file with the schema applied automatically on first run. - Rate limits and address quota/TTL are now env-configurable (MAX_ACTIVE_ADDRESSES, ADDRESS_TTL_SECONDS, RATE_LIMIT_<CMD>_*) with defaults matching prior hardcoded behavior. Self-hosters own the abuse-vs-friction tradeoff for their own deployment; setting any RATE_LIMIT_*_MAX to 0 disables that command's limit entirely. - Every relative import gained an explicit .ts extension. Node's native ESM loader (used by the new self-hosted path) requires this for runtime imports -- it has no bundler resolving extensionless or directory specifiers the way Wrangler's esbuild step does for the Workers path. Harmless for the Workers build, required for the Node one. - Rewrite the README: leads with the core service and the two pluggable boundaries (delivery adapters, storage/receiver backends) instead of treating Discord as the product. Documents both deployment paths side by side, including the DNS setup for self-hosting (MX -> your own IP, genuinely different from the Cloudflare path) and the port-25/HTTPS- webhook caveats that path involves. Verified: full core logic (create/get/list/extend/revoke/rate-limit) produces identical results against SqliteExecutor as against D1 in a direct smoke test; the self-hosted SMTP+HTTP host boots, correctly rejects mail for the wrong domain (550) and silently drops mail for an unregistered address on the right domain, and delivers a real Discord DM end-to-end through the new code path (confirmed receipt). The Cloudflare Worker still bundles and deploys cleanly after the import-path changes, and its live interactions endpoint was reverified post-deploy.
22 files changed+869−185
Rendering diff…