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
| Viewer | id | priority |
|---|
| built-in code viewer | code | -100 |
| built-in download viewer | binary-download | -50 |
| office preview plugin | docx | 0 |
| OnlyOffice plugin | onlyoffice:docx | 10 |
| this plugin | superdoc:docx | 10 |
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:
| File | Coverage |
|---|
routes.test.ts | the 4 routes: whitelist hits / traversal & symlink refusals / workspace fence on & off / 413 / 405 / 403 |
save-flow.test.ts | save state machine: hint clears after a successful save, in-flight edits stay unsaved, header buttons position-stable |
viewers.test.ts | viewer contract: id / exts / priority / fetchStrategy / settings row; no id clash with existing viewers |
trust-fence.test.ts | loopback & trusted authorities pass; unknown hosts / cross-site markers / mismatched origins refused |
locales.test.ts | zh/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:
| Package | License | Note |
|---|
superdoc | AGPL-3.0 | unmodified npm artifact; network-service use triggers AGPL source-offer duties |
@superdoc/docx-engine | Proprietary (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.