Skip to content

v0.1.0 — WOPI host, JWT auth, admin panel

Choose a tag to compare

@schnsrw schnsrw released this 24 May 22:53
· 143 commits to main since this release

What's new in v0.1.0 — the first self-host release

v0.0.x was the "build a real editor end-to-end" arc. v0.1.0 is the first release that earns its self-host story: real persistence, JWT-secured access, an admin panel, OCI-labelled multi-arch Docker image, complex pivot-cache passthrough, and full self-hosting + customization docs.

v1.0.0 is not this release — semver-bound API stability comes later. v0.1.0 means "production-grade release with substantial new capability" without committing to the v1 API freeze yet.

Real persistence — WOPI host integration

New host.Integration interface + four concrete backends selected via CASUAL_STORAGE:

  • memory (default) — in-process; preserves the v0.0.x "no DB" shape
  • local — filesystem under CASUAL_LOCAL_PATH
  • s3 — S3-compatible (AWS · MinIO · Cloudflare R2 · Backblaze B2)
  • postgres — single casual_workbooks table with bytea payload; schema auto-created on first connect

Three WOPI endpoints:

  • GET /wopi/files/:id — CheckFileInfo
  • GET /wopi/files/:id/contents — GetFile
  • POST /wopi/files/:id/contents — PutFile (with X-WOPI-ItemVersion honoured as If-Match → 409 on mismatch)

Backwards-compatible: when CASUAL_STORAGE is unset, the in-memory default keeps every v0.0.x deployment working unchanged.

JWT-secured access

When CASUAL_JWT_SECRET is set, every /wopi/files/* request needs a signed JWT. The claim model:

  • sub — username, email, or stable user id
  • file_id — the single file this token authorises (URL :id must match — tokens can't lateral-move)
  • roleadmin · editor · commenter · viewer
  • permissions — per-flag override: read · write · comment · download · share · admin
  • features — feature toggles consumed by the client UI
  • password_required — legacy x-room-password gate still applies on top
  • display_name — labels presence + cursor markers

Plus POST /api/tokens (admin-gated mint), GET /api/me (self-introspection).

Admin panel

/admin gated by CASUAL_ADMIN_USERNAME + CASUAL_ADMIN_PASSWORD. Seven sections backed by a single JSON config on disk:

  • Branding — app name · accent color · logo URL
  • Base path — reverse-proxy sub-path mount with normalisation
  • Storage — backend dropdown + per-backend creds + test-connection
  • Networking — public origin · CORS allowlist · trust proxy · HSTS max-age
  • Room limits — max rooms · max file size · room TTL · max users per room
  • Auth providers — JWT (live) + OIDC + SAML (stubs for v0.2)
  • Webhooks — array of subscriptions with HMAC signing

Login mints a short-lived admin-role JWT for the session. Secrets in the config are redacted on read (***); the panel preserves prior values when the sentinel is sent back unchanged.

Webhook dispatcher

HMAC-SHA256-signed HTTP POSTs to operator-configured URLs. Nine events: room.created · room.dropped · file.uploaded · file.saved · file.deleted · user.joined · user.left · admin.login · admin.login_failed. Signature header X-Casual-Signature: sha256=<hex>. Single retry after 5 s on failure.

Complex pivot cache passthrough (P6.1)

xlsx files authored with pivot tables now round-trip with their cache + table OOXML preserved — Excel re-recognises the file as having pivots after a save through our pipeline. Audit: 46 / 46 → 54 / 54 probes pristine.

Docker labeling + rolling tags

  • OCI org.opencontainers.image.* labels baked into every image
  • Rolling-tag scheme: 0.1.00.10latest. Pin :0.1 for patch updates only
  • Multi-arch amd64 + arm64
  • SBOM + provenance attestations in the OCI manifest

Self-hosting + customization docs

Eleven new doc pages on schnsrw.live/docs/sheets/:

  • Self-hosting: overview · reverse-proxy recipes (nginx/Caddy/Traefik) · TLS · CORS · scaling · backups
  • Customization: overview · auth (JWT claim model + role matrix + token issuance walkthrough) · webhooks (event catalogue + signature verification in Node/Python/Go)
  • ENV.md — canonical env-var reference

Mobile lane

The viewer + light-editor surface that landed on main post-v0.0.6:

  • Touch-pan driver synthesizes wheel events from pointermove (Univer 0.24 has no native touch-pan)
  • Compact chrome at ≤ 720 px / ≤ 480 px
  • Sticky bottom action bar for thumb-reachable formatting
  • Formula bar input pinned to 16 px font (iOS focus-zoom guard)

Test coverage

Unit tests: 8 → 60. New suites:

  • 9 host-integration contract tests (MemoryHost + LocalHost)
  • 7 WOPI-route tests via fastify.inject()
  • 21 auth tests (role permissions matrix · token issuance · route enforcement · CheckFileInfo response shape · back-compat)
  • 15 admin tests (config store · routes · webhook dispatcher with HMAC signature verification)

E2E suite at 357 + the home + mobile + audit specs.

What's not in v0.1.0 (deferred to v0.2 / later)

  • OIDC + SAML backend — UI ships in v0.1; v0.2 ships enforcement without breaking on-disk configs
  • Multiple admin accounts — v0.1 uses a single env-set credential pair
  • Horizontal scale-out — stateless paths work today; collab plane needs sticky-session + cross-replica awareness backplane in v0.2
  • AI / LLM features — Univer's command bus is extensible; left as a v0.3+ slot

Try it

docker run -p 3000:3000 schnsrw/casual-sheets:0.1
# → open http://localhost:3000

With persistence + admin panel:

services:
  app:
    image: schnsrw/casual-sheets:0.1
    ports: ['3000:3000']
    environment:
      REDIS_URL: redis://redis:6379
      CASUAL_STORAGE: local
      CASUAL_LOCAL_PATH: /data/workbooks
      CASUAL_ADMIN_USERNAME: admin
      CASUAL_ADMIN_PASSWORD: ${ADMIN_PASSWORD}
      CASUAL_JWT_SECRET: ${JWT_SECRET}
    volumes:
      - data:/data
    depends_on:
      redis: { condition: service_healthy }
  redis:
    image: redis:7.4-alpine
    command: ['redis-server', '--appendonly', 'yes']
volumes:
  data:

Full docs: schnsrw.live/docs/sheets/self-hosting/