Back to home@chendefine

dsh-sidebar-superdoc-docx

DSH web plugin: open and edit .docx in the better-sidebar via SuperDoc — self-hosted, offline-capable, atomic saves

Stars
0
Language
TypeScript
Created
Sep 1, 2026
Updated
Sep 1, 2026
GitHub repo

Introduction

dsh-sidebar-superdoc-docx

English | 中文

Open and edit .docx files directly in the DSH web GUI's better-sidebar editor, powered by SuperDoc — a browser-native DOCX editor that reads and writes the real OOXML (no server-side document service needed).

Dependency: this plugin registers its file viewer with dsh-better-sidebar (>= 0.13.0) — a mandatory peer dependency. Without it, the viewer never appears. Install it first (or together with this plugin).

⚠️ License notice: this plugin's own code is MIT, but it integrates AGPL-3.0 superdoc and the proprietary-licensed @superdoc/docx-engine at runtime — installing it means accepting those terms. See License below and THIRD-PARTY-NOTICES.md.

Features

  • Browser-native DOCX editing — view and edit .docx in the sidebar with comments and tracked changes; three open modes selectable in the settings page: editing / suggesting / viewing.
  • Save back to disk — the Save button exports the edited document and atomically overwrites the opened file through a dedicated route (tmp + rename, never a half-written state); unsaved edits show a dot.
  • Follows outside edits — a 3s disk poll: a clean editor auto-swaps to the new version in place (replaceFile); a dirty editor only shows a banner with a manual Reload — your edits are never silently dropped.
  • Fully self-hosted & offline-capable — the SuperDoc editor build and the DOCX engine (web workers included) are served same-origin from this package's node_modules: no CDN traffic, no third-party document service, telemetry off by default. Fully offline after pnpm install.
  • Download fallback — every surface (including all error states) keeps a plain download link.
  • Sidebar-adaptive — the toolbar folds into an overflow menu as the pane narrows; pages fit-to-pane zoom; readable in light and dark themes.

Use cases

  • Human–AI collaboration on one Word file: the AI agent edits a .docx in the session; you see the new version in the sidebar within seconds, polish it by hand, save — and the agent's next turn works from the latest version. A two-way loop without interruptions.
  • Intranet / offline / compliance environments: deployments that must not reach CDNs such as jsdelivr or SaaS document services; every asset is self-hosted same-origin.
  • No document server wanted: unlike OnlyOffice / Collabora, which need a separate Document Server, this plugin has zero service dependencies — install and go.
  • Document review workflows: open in suggesting mode; comments and suggestions land as tracked changes.
  • Quick preview: replaces the built-in code/download viewers — click a .docx in the file tree and see the paginated document, with a download link one click away.

Installation

Prerequisites

  • Node.js >= 20;
  • the DSH web GUI and its web profile;
  • dsh-better-sidebar >= 0.13.0 installed in the same profile (see the dependency note above).

From npm (recommended)

The package is published on npmjs as dsh-sidebar-superdoc-docx:

dsh plugin --profile web add dsh-better-sidebar dsh-sidebar-superdoc-docx

Or by hand: edit the profile's package.json (e.g. ~/.dsh/profiles/web/package.json), add the two npm dependencies and the bundle entries, then run pnpm install in the profile directory:

{
  "dependencies": {
    "dsh-better-sidebar": ">=0.13.0",
    "dsh-sidebar-superdoc-docx": "^0.1.0"
  },
  "dsh": { "profile": { "bundles": ["…", "dsh-better-sidebar", "dsh-sidebar-superdoc-docx"] } }
}

Finally restart dsh web (the host half must reload) and hard-refresh the browser (Ctrl/Cmd+Shift+R).

From GitHub (source / development)

dsh plugin --profile web add github:chendefine/dsh-sidebar-superdoc-docx

For local development, clone, build, and link:

git clone https://github.com/chendefine/dsh-sidebar-superdoc-docx
cd dsh-sidebar-superdoc-docx
pnpm install
pnpm build        # → lib/index.js + lib/client.js + lib/types

then, in the profile's package.json, point the dependency at the clone and run pnpm install in the profile directory:

{
  "dependencies": {
    "dsh-sidebar-superdoc-docx": "link:/absolute/path/to/dsh-sidebar-superdoc-docx"
  }
}

Plugin config (cordis.patch.yml of the profile)

- id: dsh-sidebar-superdoc-docx
  config:
    fileLimitMb: 100            # save-route size cap in MB, default 100
    allowOutsideWorkspace: false # allow saving files resolved outside the session cwd, default false

Usage

Opening a document

Click any .docx in the sidebar file tree — it opens in the DOCX (SuperDoc) viewer instead of the built-in code/download viewers.

Switching the open mode

Under Settings → Side cards → File preview → DOCX (SuperDoc), the gear exposes the "open mode" select: editing / suggesting / viewing. The choice persists in pluginSettings['superdoc:docx'].mode and takes effect immediately (the editor remounts in the new mode); viewing also hides the save button.

Editing & saving

  • The top toolbar is SuperDoc's native one (bold, lists, comments, …) and folds automatically as the pane narrows;
  • after edits, the header shows ● Unsaved changes; click Save to export and atomically write back to the opened path;
  • state machine: Saving… → Saved / Save failed (failures carry a reason and can be retried); edits made while a save is in flight keep the unsaved hint so you can re-save.

Following outside edits (e.g. an AI agent edited the file)

  • clean editor: the 3s poll detects the disk change → re-fetches and swaps in place via replaceFile, re-fitting the zoom;
  • dirty editor: only a "File changed on disk" banner appears — you decide whether to Reload (reloading discards the current unsaved edits).

Coexistence with other viewers

Vieweridpriority
built-in code viewercode-100
built-in download viewerbinary-download-50
office preview plugindocx0
OnlyOffice pluginonlyoffice:docx10
this pluginsuperdoc:docx10

Ties (e.g. with OnlyOffice) break by registration order. Every viewer can be toggled individually under Settings → Side cards → File preview without affecting the others.

Architecture

Two halves

browser (client half, tiny CJS bundle via window.__ModuleLoader__)
  └─ ctx.betterSidebar.registerFileViewer('superdoc:docx', exts:['docx'], priority:10, fetchStrategy:'mediaUrl')
      └─ SuperDocView: <script src="/sidebar/superdoc/assets/superdoc.min.js">  (global `SuperDoc`)
          reads:  fetch(/sidebar/file?sessionId=&path=)        → Blob → new SuperDoc({ document: blob, contained: true })
          saves:  superdoc.export({triggerDownload:false}) → Blob → PUT /sidebar/superdoc/save?sessionId=&path=

node (host half, 4 fenced routes)
  ├─ GET  /sidebar/superdoc/info                    versions / health / cache-bust seed
  ├─ GET  /sidebar/superdoc/assets/<file>           superdoc/dist-cdn (closed whitelist)
  ├─ GET  /sidebar/superdoc/engine/dist-cdn/<path>  @superdoc/docx-engine/dist-cdn mirror (engine + workers)
  └─ PUT  /sidebar/superdoc/save                    raw DOCX bytes → atomic write inside the session cwd
  • the client half does exactly three things: register the viewer, fetch the file bytes through better-sidebar's fenced media route, and mount the SuperDoc instance into the sidebar pane;
  • the host half runs no document logic — it only serves assets same-origin and fenced saves;
  • vendored versions: superdoc@2.10.0 + @superdoc/docx-engine@0.9.0 (per package.json; the info route reports the installed versions and doubles as the cache-bust seed).

Why the engine mirror exists

Before the script loads, the client sets globalThis.SUPERDOC_ENGINE_CDN_BASE_URL = '/sidebar/superdoc/engine', pointing SuperDoc's engine resolver at this plugin's route. The engine then dynamic-imports …/dist-cdn/docx-engine.es.js and resolves its web workers relative to that same-origin URL — browsers refuse cross-origin worker creation, and otherwise jsdelivr would become a runtime dependency. That is the entire reason the host half mirrors the dist-cdn tree.

Security boundaries

  • Trust fence (src/trust-fence.ts, behaviorally identical to better-sidebar's): the Host header must be loopback or a trusted authority from webRuntime.trustedHosts; sec-fetch-site: cross-site and mismatched Origins are refused — a DNS-rebinding / cross-site defense, not authentication.
  • Workspace fence (src/paths.ts + the save route): absolute paths only; isWithin compares path segments (/a/bc is not within /a/b); the parent directory is realpath-checked to close symlink escapes; .docx only; bodies above fileLimitMb get a 413; writes go through tmp + rename atomic replacement.
  • Asset whitelisting (src/assets.ts): the superdoc build exposes a closed whitelist of 3 files; engine subpaths pass a shape check, reject ./.. segments, and must realpath inside dist-cdn as regular files.
  • No leakage: telemetry off by default (telemetry: { enabled: false }); the plugin keeps no state on disk.

Development details & conventions

Directory layout

src/
  index.ts            host half: build & register the 4 routes (buildRoutes is pure, test-friendly)
  assets.ts           node_modules asset discovery / whitelists / realpath containment / content types
  config.ts           config resolution (fileLimitMb, allowOutsideWorkspace; plain TS, zero deps)
  paths.ts            absolute-path requirement + segment-aware containment + symlink-safe parent realpath
  trust-fence.ts      browser trust fence (copied, not imported — plugins must not depend on upstream internals)
  wire.ts             {ok,...} / {ok:false,error:{code,message}} JSON shape + size-capped raw-body reader
  client/
    index.ts          client half: register the superdoc:docx viewer + attach dictionaries
    SuperDocView.tsx  the editor component (mount / save state machine / disk poll / fit-to-pane zoom)
    loader.ts         runtime loader (script/stylesheet singletons, engine base URL, contained layout CSS)
    settings.ts       open-mode read (validated, falls back to editing)
    urls.ts           URL builders for /sidebar/file and the save route (better-sidebar request contract)
    i18n.ts / locales.ts / icons.tsx   zh/en dictionaries, registration, icons
tests/                vitest: routes / save-flow / viewers / trust-fence / locales

Build artifacts

  • host: lib/index.js, ESM (es2023), zero runtime dependencies;
  • client: lib/client.js — a CJS bundle registered via window.__ModuleLoader__.load({ id, factory }), the official external client-plugin delivery format (same shape as dsh-sidebar-onlyoffice and dsh-web-search-aggregation);
  • the SuperDoc editor itself is not bundled: the host routes inject it at runtime as a classic <script> (structurally identical to how onlyoffice loads its api.js).

Client purity gate

tsdown.config.ts ships a rolldown plugin that fails the build when the client bundle imports any Node builtin or value-imports @deepseek-ai/*; React / react-dom / cordis stay external and come from the host's module table. The browser half must be self-contained.

Code conventions

  • No imports of monorepo-internal types: both halves declare structural context faces (RouteContext, ClientContextFace) — external plugins must not reach into the monorepo's Context augmentation graph;
  • browser JSON is always {ok:...} / {ok:false,error:{code,message}} (aligned with better-sidebar's wire format); error codes: forbidden / method-error / bad-request / not-found / fs-error / internal;
  • the viewer id is namespaced (superdoc:docx) to avoid clashes with the builtins and onlyoffice:docx; priority 10 beats the builtins; fetchStrategy: 'mediaUrl';
  • the zh/en dictionaries must carry exactly the same key set (enforced by the locales test) under the plugin's single dshSidebarSuperdoc namespace;
  • every page-level injection is idempotent (stylesheet, layout CSS, editor script are singletons — remount-safe);
  • the save route is the only fs write surface; better-sidebar's own fs.write is UTF-8-text-only, so binary exports must go through it.

Tests

pnpm test (vitest run) covers:

FileCoverage
routes.test.tsthe 4 routes: whitelist hits / traversal & symlink refusals / workspace fence on & off / 413 / 405 / 403
save-flow.test.tssave state machine: hint clears after a successful save, in-flight edits stay unsaved, header buttons position-stable
viewers.test.tsviewer contract: id / exts / priority / fetchStrategy / settings row; no id clash with existing viewers
trust-fence.test.tsloopback & trusted authorities pass; unknown hosts / cross-site markers / mismatched origins refused
locales.test.tszh/en key parity, non-empty values, unique namespace

Common commands

pnpm typecheck   # tsc --noEmit
pnpm test        # vitest run
pnpm build       # host ESM + client ModuleLoader bundle (purity gate enforced)

Known limitations

  • Only .docx (SuperDoc does not open legacy .doc).
  • Closing the tab with unsaved edits cannot be intercepted — watch the unsaved dot.
  • If better-sidebar's workspaceFence is disabled, files outside the workspace can be opened through the media route, but saving them still requires allowOutsideWorkspace: true here.
  • Fonts: SuperDoc core ships no fonts; documents render with system fonts unless you additionally integrate @superdoc-dev/fonts (not wired up here).

License

This plugin's code is MIT. It integrates (unmodified, installed by pnpm install and served verbatim by its routes) two SuperDoc components:

PackageLicenseNote
superdocAGPL-3.0unmodified npm artifact; network-service use triggers AGPL source-offer duties
@superdoc/docx-engineProprietary (DOCX Engine Proprietary License)without a commercial agreement, use is limited to SuperDoc-dependent, AGPL-permitted purposes (evaluation/development/testing); commercial use needs a SuperDoc license

See THIRD-PARTY-NOTICES.md.

Credits

  • SuperDoc by Harbour Enterprises — the editor itself.
  • dsh-sidebar-onlyoffice — the plugin shape (runtime script injection, trust fence, host routes) this package follows.