Download Latest Version EtherCalc 0.20261101.2 source code.zip (3.8 MB) Google Add to Preferred Sources
Home / 0.20261026.0
Name Modified Size InfoDownloads / Week
Parent folder
EtherCalc 0.20261026.0 source code.tar.gz 2026-10-26 3.4 MB
EtherCalc 0.20261026.0 source code.zip 2026-10-26 3.8 MB
README.md 2026-10-26 11.3 kB
Totals: 3 Items   7.2 MB 3

Security

  • Private multi-sheet workbooks now initialize children with immutable meta:parent delegation, closing a confidentiality exposure where sub-sheets of a private room were anonymously readable. RoomDO derives each child authorization verdict from the parent, rejects local-ACL conflicts and occupied-room capture, and keeps workbook-internal children out of the public rooms index. On 2026-08-11 a read-only D1 rooms index query plus anonymous probes of gate-exempt GET /_/<parent>/access counted 1,883,069 rooms, 68,582 child-shaped names (<base>.<positive decimal>), and 13,875 distinct candidate parents — 13,873 public, 2 not-found, 0 private. Pre-fix children had no access metadata, so #mirrorIndex treated them as public and wrote them into the index; post-fix parented children are never indexed. No remediation is required in hosted production: the fix is prospective and closed the hole before anyone was exposed by it. The invokability facts remain true and explain why a sweep was never needed (and would still block anyone who later wants one): /_do/backfill-children and /_do/set-parent exist only on the internal Durable Object dispatch surface (packages/worker/src/room.ts:371,374) with no Worker route proxying them; ETHERCALC_MIGRATE_TOKEN is unset in production so operator-gated paths 404 (packages/worker/src/lib/migrate-auth.ts:40); and RoomDO.#postBackfillChildren enforces local.acl.owner === uid (packages/worker/src/room.ts:916-928), preventing operator sweeps of user rooms. A hosted sweep would still require new code and a deploy (a public Worker route, a provisioned token, and a policy choice between owner-only vs. operator sweeps). The D1 count is a lower bound — a child whose mirror write failed, or any self-hosted deployment, is not represented — but zero is plausible rather than lucky: exposure required a private room (passkeys reached production only around d2afa90, 2026-07-18) and multi-sheet use and a child that received content. New children are safe on creation, and pristine pre-fix children are marked on the next TOC write (packages/worker/src/routes/multi-import.ts:415).
  • Multi-sheet append import maps a mixed-version RoomDO 501 on /_do/import-append-toc to a retryable 503 with clear copy (Worker/DO skew during deploy), instead of surfacing bare "Not implemented".
  • Closed stored DOM-XSS and cross-origin postMessage seams in graph and multi-sheet clients; HTML exports now use the same element, attribute, URL, and CSS-fetch allowlist as the live editor.
  • Bound native and legacy WebSocket mutations to the authenticated attachment room, validate complete message schemas and resource caps, re-check auth on every state-changing frame, and reap bounded Socket.IO handshake sessions.
  • Added bounded auth/import bodies, ZIP/sheet/cell/command limits, AuthDO challenge capacity and per-IP ceremony limits, revocable __Host-ec_sess sessions, live-socket expiry, and fail-closed private-room authorization.
  • Gated /_timetrigger, fixed partial scheduler retries, hardened trusted proxy attribution and room-ID entropy, and added private-response no-store plus CSP/HSTS/nosniff/referrer/permissions/COOP headers. CSP WebSocket authority now comes from ETHERCALC_ORIGIN, not a spoofable Host.
  • Hardened Docker/Helm defaults (read-only/no-new-privileges containers, disabled service-account token mounting, fail-closed passkey anchors) and upgraded docs image processing to sharp 0.35.3, resolving GHSA-f88m-g3jw-g9cj in its bundled libvips.
  • Made staging deploy validation pass --config wrangler.toml explicitly, so Vite's generated production-config redirect cannot silently ignore --env staging.
  • Room URLs stay unlisted by design: every non-landing response now carries X-Robots-Tag: noindex, nofollow, noarchive, and only / remains indexable. GET /robots.txt returns a real robots file (Allow: / only) instead of falling through the /:room catch-all as app HTML. The robots file deliberately carries no Disallow directive — Disallow forbids fetching, so a crawler blocked from a URL can never observe its noindex header and already-indexed rooms would be stranded permanently.
  • Multi-sheet workbook import (replace and append — xlsx/ods/fods and csv/tsv/txt/socialcalc) is re-enabled for private rooms: children are created with immutable meta:parent so they inherit the parent's access instead of publishing as public. All formats share one guarded server append door; the multi-sheet client no longer PUTs sub-rooms directly.

Tooling

  • Migrated the repository interface to Vite+ 0.2.4: package operations use vp install/add/remove/update, workspace tasks use vp run, local binaries use vp exec, and package Vite/Vitest imports and scripts use Vite+.
  • Replaced the Biome gate with Vite+ Oxlint while preserving the existing three-rule lint contract and the repository's no-formatter-gate policy. CI, deploy, nightly, LemmaScript, and npm validation jobs now use the pinned setup-vp action and Vite+ commands.
  • Pinned the underlying package manager as bun@1.3.14. Bun remains the native runtime for Bun-specific tests/scripts and for Docker or packed-package bootstrap/runtime paths where Vite+ is intentionally not shipped.
  • Made root vp build and vp dev production-faithful full-stack workflows: Vite+ prepares the curated browser assets once, then the Cloudflare Vite plugin builds or serves the Worker, Durable Objects, bindings, and assets.
  • Kept integrated multi-sheet development same-origin, made standalone port 8080 strict, and excluded generated Worker Vite output from npm tarballs.
  • Hardened self-host Docker builds against local-state leakage: build context excludes dependencies, Vite/Wrangler output, secrets, migration dumps, and persistent room data; the smoke gate asserts the resulting image is clean.
  • Fixed scripts/ratchet-verify.sh mutation score parsing in jq and node branches by computing the score from mutant status counts rather than reading a non-existent mutationScore property, resolving an unconditional parse error (exit 2) during mutation ratchet audits.

Integration

  • Prepared the headless adapter for the next SocialCalc release: bundle transforms now fail loudly on upstream wrapper drift, and the DOM shim supports safely serialized attributes, class lists, and text content used by accessible sheet rendering.
  • Added multi-sheet bridge regressions for exact TOC routing, child-room isolation/mutation, and cross-sheet recalculation through sibling RoomDO hydration. EtherCalc keeps canonical per-sheet persistence; the embedded SocialCalc workbook remains the in-memory calculation and interchange model.
  • Moved the account/access cluster into its in-flow fallback at or below the measured 920px cutoff, avoiding overlap with SocialCalc's expanded menu while preserving fixed top-right chrome on wider sheets.

Bug fixes

  • Passkey sessions now renew while in use. /_auth/whoami re-issues the session cookie at most once a day with a fresh 30-day lifetime, capped at 90 days after the passkey ceremony that started the chain (renewed tokens carry that auth time). Logout revocation now compares the chain's authentication time, so a renewal cannot outlive a later logout. An AuthDO without the refresh route falls back to plain verification. The sheet client reconnects its WebSocket daily so sockets pick up the renewed session.
  • Fixed silent loss of sheet edits when a passkey session ends while a sheet is open. RoomDO closes such sockets (1008 Session expired/invalid), but the client kept retrying, queued every later edit in memory, and showed nothing, so closing or reloading the tab discarded them. /_auth/whoami now reports expiresInMs; the single-sheet client holds edits shortly before expiry, shows a banner offering an in-place passkey sign-in that reconnects and flushes them, surfaces lost connections and reconnect give-ups, and warns before unloading a tab with unsent edits.
  • Fixed POST /_/:room returning 202 on Durable Object command rejections, which silently discarded writes. The route now propagates the DO status and response body (such as 413 command exceeds sheet limits when a batch exceeds 200,000 cells).

Passkey (WebAuthn) accounts and private sheets — Phase A.

  • The Worker is now its own WebAuthn relying party: POST /_auth/ register-init|register-complete|login-init|login-complete run the ceremonies against a new singleton AuthDO (credentials keyed by credential ID, usernameless discoverable login, replay-safe signature counters, HMAC-signed ~30-day sessions). The session travels only in an HttpOnly ec_sess cookie; GET /_auth/whoami and POST /_auth/logout round out the surface. Zero third-party auth dependencies beyond @simplewebauthn/server.
  • Private sheets: POST /_/private creates an owner-only room and POST /_from/:template/private copies a readable sheet into one, both backed by a new atomic POST /_do/init-private primitive that claims only truly-empty rooms (409 otherwise) and requires the caller-owned ACL. RoomDO is the sole authorization boundary: every /_do/* read/write, export, and WebSocket path enforces the meta:access/meta:acl plane (deny-overrides; legacy ?auth= tokens are demoted on private rooms), the rooms index and D1 mirror exclude private rooms, and full-storage replacement paths (snapshot PUT, seed, install, delete) preserve the access plane — deletion leaves a tombstone so private room names cannot be squatted, while TTL expiry still reclaims them. Private rename/clone are refused rather than declassified.
  • Anonymous collaboration on public rooms is byte-identical; passkey auth ships dark on self-host until ETHERCALC_AUTH plus the ETHERCALC_RP_ID/ETHERCALC_RP_NAME/ETHERCALC_ORIGIN trust anchors are configured (see docs/SELFHOST_HARDENING.md §7).
  • Client: a dependency-free static/passkey.js adds Sign in / New Private Sheet / Copy to Private / sign-out affordances plus a private-sheet notice on both the landing and sheet pages.

Test tooling

  • Added release-gated Playwright coverage for two-context native WebSocket collaboration and reconnect; real CDP WebAuthn registration/login with private RoomDO HTTP and WebSocket authorization; production Worker-served multi-sheet assets; and a representative room CRUD/export lifecycle. Viewer ACL coverage is not implied: no HTTP sharing route currently creates a genuine read-only viewer state.
  • Worker mutation testing (packages/worker/stryker.conf.json, break: 90): Phase A's post-rebase-onto-main hardening pass (see docs/MUTATION_REPORT.md's 2026-07-17 addendum) closed test-gap clusters in auth-do.ts, auth-session.ts/authorize.ts/ws-upgrade.ts, and rate-limit.ts/room-create-limit.ts, restoring the combined score to 90.20% — at or above main's own pre-feature baseline (90.16%). No source/config behavior changed; test-only.

npm: ethercalc@0.20261026.0 · Docker: audreyt/ethercalc:0.20261026.0 · Already running on ethercalc.net.

Source: README.md, updated 2026-10-26