# Contributing 感谢关注 dsh-tu4-inline-images。本项目安全优先:任何改动都必须保持"安全保障是前提,功能实现才是目的"。 ## Setup Requires Node.js ≥ 22 (tests rely on native TS type-stripping — `.mjs` tests import `.ts` sources directly) and [pnpm](https://pnpm.io) ≥ 10 (any package manager that can run the scripts works for local dev; the published install is a self-contained tgz with **zero runtime dependencies**). ```bash pnpm install pnpm run check # build + security gate + 74 tests ``` ## Scripts | Script | What it does | |---|---| | `pnpm run build` | esbuild dual bundle → `lib/index.js` (host, node ESM) + `lib/client.js` (browser, `__ModuleLoader__.load` contract) | | `pnpm run gate` | `test/gate.mjs` — build-time security invariants (fail = no ship) | | `pnpm run check` | build + gate + `node --test "test/*.test.mjs"` (full chain) | | `pnpm pack` | → `dsh-tu4-inline-images-.tgz` (contents = `files` field: lib + manifests + README + LICENSE) | ## Security invariants (do not break) 1. **The image route handler is the sole file-read enforcement point.** Client code never reads the filesystem. Stream rewriting is display-only and every candidate path re-passes the guard. 2. **Zero URL literals** in shipped code — the route URL prefix is assembled at runtime from `webServer.host`/`port` (forced loopback when `0.0.0.0`). `gate.mjs` enforces this. 3. **Multi-root realpath containment before any read**: sanitize → `fs.resolve` (realpath) → `fs.contains(root, target)` against every allowed root → regular-file `stat` → byte limit. `..` traversal and symlink/junction escape must stay rejected (see `test/path-guard.test.mjs` T6). 4. **Token** = `crypto.randomBytes(32)`; no `Math.random` anywhere (gate-enforced). 5. **Error responses are fixed short strings** — never echo paths or tokens. 6. **No install-type lifecycle scripts** (`preinstall`/`install`/`postinstall`/`prepare`/`postpublish`) — gate-enforced. 7. Client half: no `innerHTML` with user data, no global listeners, same-origin network only. 8. `llm/stream` must never break the stream: any internal error → passthrough unchanged. When adding tests, keep the convention: pure helpers are unit-tested directly; `apply()` wiring is integration-tested with a mock `ctx` (see `test/apply.test.mjs`, `test/client.test.mjs`). ## TypeScript conventions - src relative imports **must use explicit `.ts` extensions** (native TS type-stripping in tests; esbuild accepts both). - No TS features that require emit transforms (no enums/namespace/parameter properties) — Node type-stripping must handle them. - `esbuild` does not type-check; type correctness is review responsibility. ## Client bundle contract `lib/client.js` must start with `window.__ModuleLoader__.load({ id: "dsh-tu4-inline-images", factory: (require) => {` and end with `return module.exports; } });` (banner/footer injected by `build.mjs`, same shape as DSH's own `packages/client/tsdown.client.ts`). React is obtained through the factory's `require` (module table) with a `window.React` fallback. The structure test at the end of `test/client.test.mjs` asserts this — if you change `build.mjs` banners, update that test in the same commit. ## Release process 1. `pnpm run check` green 2. bump `version` in `package.json` + `dsh.plugin.json` (keep in sync) 3. `pnpm pack` 4. install the tgz into a scratch profile, run the acceptance checklist (see `docs/SELF-AUDIT.md` §阶段 2 for the surface list) 5. update `CHANGELOG.md`, tag, publish the tgz as a GitHub Release asset