github audreyt/ethercalc 0.20261026.0
EtherCalc 0.20261026.0

4 hours ago

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.

Don't miss a new ethercalc release

NewReleases is sending notifications on new releases.