plans/018-server-side-remote-auth.md
--remote secret on the server, not just the client routerExecutor instructions: This is a larger security design + implementation plan touching the dev-server trust boundary. Read it fully first. Follow the steps, run every verification, and honor the STOP conditions — several ask you to pause and confirm the approach before writing broad changes. When done, update the status row in
plans/README.md.Drift check (run first):
git diff --stat c63cb120..HEAD -- packages/client/setup/routes.ts packages/slidev/node/vite/loaders.ts packages/slidev/node/vite/monacoWrite.ts packages/slidev/node/vite/serverRef.tsOn a mismatch with the excerpts below, treat it as a STOP condition.
c63cb120, 2026-07-10The --remote <password> feature's only access gate is a client-side Vue
Router beforeEnter guard. The dev server's privileged surface has no
server-side check:
GET /__slidev/slides/<n>.json returns full slide content and rendered
speaker notes.POST /__slidev/slides/<n>.json mutates slide content/frontmatter/notes and
persists slides.md to disk (parser.save).So anyone who can reach the server — the LAN when --remote binds 0.0.0.0, the
internet when --tunnel is added, or a malicious page via CSRF in default
localhost mode — can read private notes and overwrite the presenter's deck,
bypassing the password prompt entirely (it only hides SPA routes). This plan adds
a real server-side authorization boundary.
packages/client/setup/routes.ts:8-20:
function passwordGuard(to: RouteLocationNormalized) {
if (!configs.remote || configs.remote === to.query.password) return true
if (configs.remote && to.query.password === undefined) {
const password = prompt('Enter password')
if (configs.remote === password) return true
}
// redirect away
}
beforeEnter on presenter/notes/print/export routes.packages/slidev/node/vite/loaders.ts:79-141
(GET returns withRenderedNote(data.slides[idx]); POST mutates + parser.save).vite/monacoWrite.ts:23-32, vite/serverRef.ts:28-36.configs.remote (the --remote password), already
surfaced to the client via the #slidev/configs virtual module.Use the question flow or report back to the operator on these (each has a
recommended default). Do not write the full change until these are settled:
remote is set.#slidev/configs virtual module, sent on each privileged request
(Authorization: Bearer / a header for HTTP, a field in ws messages). The
--remote password remains the human gate to obtain a session.remote nor editor
features are enabled, behavior is unchanged for read paths.| Purpose | Command | Expected |
|---|---|---|
| Install | pnpm install | exit 0 |
| Build | pnpm build | exit 0 |
| Typecheck | pnpm typecheck | exit 0 |
| Lint | pnpm lint | exit 0 |
| Test | pnpm test | pass (add auth-helper tests) |
In scope (after design confirmation):
packages/slidev/node/vite/loaders.ts — auth check in the /__slidev/* middlewarepackages/slidev/node/vite/monacoWrite.ts, vite/serverRef.ts — auth on privileged ws messagesnode/auth.ts) + the config
virtual module (node/virtual/configs.ts) to expose the token to the clientpackages/client/** editor/presenter/remote code that calls these endpoints,
to send the tokenOut of scope:
feat/server-side-remote-auth.feat(security): authorize privileged dev-server endpoints.Resolve the four decisions above with the operator. Record the chosen answers at the top of the branch's first commit message or in a short note. STOP and ask if any answer is unclear — do not improvise a security protocol.
Add node/auth.ts that, when remote/editor is enabled, generates a random
token at server start (crypto.randomUUID() or crypto.randomBytes). Expose it
to the trusted client through the config virtual module so the app can attach it;
never log the token value.
In loaders.ts, before serving/ mutating in the /__slidev/* handler, verify
the token per the confirmed scope (Step 1 decision 1). Return 401 when missing/
wrong. Keep the bounds check from plan 011 if present.
Require the token field in slidev:monaco-write and in the server-ref persistence
path; reject otherwise. Combine with plan 017's origin check if that landed.
Make the editor save, presenter sync, and remote-control code send the token with each privileged call. Verify HMR and normal navigation are unaffected.
Test the pure token-compare helper (constant-time compare if feasible) for match/mismatch/missing.
Verify: pnpm build && pnpm typecheck && pnpm lint && pnpm test all pass;
manual check with pnpm demo:dev that editing/notes still work in-app and that a
request without the token to POST /__slidev/slides/1.json is rejected (401).
POST /__slidev/slides/<n>.json and an unauthenticated monaco-write are
rejected. If plan 022 introduces a server test harness, add these as automated
cases.pnpm build && pnpm typecheck && pnpm lint && pnpm test passplans/README.md status row updatedStop and report (do not improvise) if:
passwordGuard as cosmetic UX after this lands; the server is
the real boundary.