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
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
superdocand the proprietary-licensed@superdoc/docx-engineat runtime — installing it means accepting those terms. See License below and THIRD-PARTY-NOTICES.md.
Features
- Browser-native DOCX editing — view and edit
.docxin the sidebar with comments and tracked changes; three open modes selectable in the settings page: editing / suggesting / viewing. - Save back to disk — the
Savebutton 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 manualReload— 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 afterpnpm 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
.docxin 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
suggestingmode; comments and suggestions land as tracked changes. - Quick preview: replaces the built-in code/download viewers — click a
.docxin 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
webprofile; dsh-better-sidebar >= 0.13.0installed 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(perpackage.json; theinforoute 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 fromwebRuntime.trustedHosts;sec-fetch-site: cross-siteand mismatched Origins are refused — a DNS-rebinding / cross-site defense, not authentication. - Workspace fence (
src/paths.ts+ the save route): absolute paths only;isWithincompares path segments (/a/bcis not within/a/b); the parent directory isrealpath-checked to close symlink escapes;.docxonly; bodies abovefileLimitMbget 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 mustrealpathinsidedist-cdnas 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 viawindow.__ModuleLoader__.load({ id, factory }), the official external client-plugin delivery format (same shape asdsh-sidebar-onlyofficeanddsh-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 itsapi.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 andonlyoffice: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
dshSidebarSuperdocnamespace; - 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.writeis 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
workspaceFenceis disabled, files outside the workspace can be opened through the media route, but saving them still requiresallowOutsideWorkspace: truehere. - 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 |
Credits
- SuperDoc by Harbour Enterprises — the editor itself.
- dsh-sidebar-onlyoffice — the plugin shape (runtime script injection, trust fence, host routes) this package follows.