| Name | Modified | Size | Downloads / 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:parentdelegation, 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 D1roomsindex query plus anonymous probes of gate-exemptGET /_/<parent>/accesscounted 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#mirrorIndextreated 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-childrenand/_do/set-parentexist only on the internal Durable Object dispatch surface (packages/worker/src/room.ts:371,374) with no Worker route proxying them;ETHERCALC_MIGRATE_TOKENis unset in production so operator-gated paths 404 (packages/worker/src/lib/migrate-auth.ts:40); andRoomDO.#postBackfillChildrenenforceslocal.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 aroundd2afa90, 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
501on/_do/import-append-tocto a retryable503with clear copy (Worker/DO skew during deploy), instead of surfacing bare "Not implemented". - Closed stored DOM-XSS and cross-origin
postMessageseams 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_sesssessions, 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 fromETHERCALC_ORIGIN, not a spoofableHost. - 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
sharp0.35.3, resolving GHSA-f88m-g3jw-g9cj in its bundled libvips. - Made staging deploy validation pass
--config wrangler.tomlexplicitly, 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.txtreturns a real robots file (Allow:/only) instead of falling through the/:roomcatch-all as app HTML. The robots file deliberately carries noDisallowdirective —Disallowforbids fetching, so a crawler blocked from a URL can never observe itsnoindexheader 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:parentso 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 usevp run, local binaries usevp 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-vpaction 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 buildandvp devproduction-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.shmutation score parsing injqandnodebranches by computing the score from mutant status counts rather than reading a non-existentmutationScoreproperty, 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/whoamire-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 thatauthtime). 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/whoaminow reportsexpiresInMs; 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 /_/:roomreturning202on Durable Object command rejections, which silently discarded writes. The route now propagates the DO status and response body (such as413 command exceeds sheet limitswhen 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-completerun the ceremonies against a new singletonAuthDO(credentials keyed by credential ID, usernameless discoverable login, replay-safe signature counters, HMAC-signed ~30-day sessions). The session travels only in an HttpOnlyec_sesscookie;GET /_auth/whoamiandPOST /_auth/logoutround out the surface. Zero third-party auth dependencies beyond@simplewebauthn/server. - Private sheets:
POST /_/privatecreates an owner-only room andPOST /_from/:template/privatecopies a readable sheet into one, both backed by a new atomicPOST /_do/init-privateprimitive 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 themeta:access/meta:aclplane (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_AUTHplus theETHERCALC_RP_ID/ETHERCALC_RP_NAME/ETHERCALC_ORIGINtrust anchors are configured (see docs/SELFHOST_HARDENING.md §7). - Client: a dependency-free
static/passkey.jsadds 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 (seedocs/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.