Editor internals

Reading time 12 min

The optional Node writing server is editor-server.js. It serves sign-in, editor and Admin pages plus internal JSON endpoints. A static reader host does not run it. Keep this server private; it is not an anonymous writing service or a promised versioned integration API.

Request pipeline

Gate Current contract
URL parse Invalid request returns 400
Host Exact lowercased allowed value
Origin When supplied, host must match
POST type application/json required
Session Required for every /api/ route
Forced password Only me/logout/change allowed
CSRF Required for non-GET API requests
Handler Role, path and input checks

The origin gate runs before public and authenticated route dispatch. Allowed hosts include loopback names with configured port plus explicit additional values. Origin comparison checks URL host, not a general cross-origin allowlist; a missing Origin is not itself rejected. CSRF is still required for authenticated non-GET APIs. POST requests with wrong media type return 415, including login. Browser-hidden buttons are never the permission boundary.

JSON bodies have a configured byte limit. Client requests can fail at the gate before handler-specific validation. Environment variables covers parsing limits and proxy settings; Security and privacy covers the deployment boundary.

Public pages and static assets

Route Behavior
/login GET Sign-in page
/api/login POST Login exception to session gate
/, /editor, /editor.html Session or redirect to login
/admin Session and admin role
/admin/users, /admin/links Redirect to Admin fragment
Static/vendor routes Containment-checked files
Other routes 404

Static reader pages are not routed through this server’s content editor. Admin/editor responses use per-response script nonces and CSP, no-store/no-cache controls and security headers. Editor preview permits image sources that include HTTP/HTTPS; a saved remote image can still contact its host. A restrictive page policy is not proof every authored resource is local or every browser safe.

Session and account state

UserStore persists validated account records and access versions. SessionStore holds token hashes, CSRF secrets and idle/absolute lifetimes in process memory. Restart ends sessions. Every resolved session reloads the user’s current state; missing/disabled users or an authVersion mismatch invalidate it.

Successful login returns sanitized user state and CSRF, sets HttpOnly/SameSite=Strict/Path=/ cookie and records the login. Secure mode uses __Host-ms_session, adds Secure and expires the old unprefixed cookie; local HTTP mode uses ms_session. HTTPS itself is an operator responsibility, not created by that switch.

Failed logins use account-address, address-wide and account-wide limiters. Recently successful addresses have a narrow exemption from account-wide blocking; known addresses and limiter state are process memory. Successful login clears the account-address bucket, not every limiter. Do not infer durable intrusion prevention or distributed rate limiting.

Password change verifies current password, rejects the same supplied string, updates the hash/version, ends old sessions and creates a new session. Account assignment, status and session-revocation changes use version invalidation. Accounts explains operations; Storage and formats covers persistence and recovery.

File endpoint contracts

Paths are relative to content, not public URLs. Non-GET calls require the established session/CSRF gates.

Endpoint Request data
GET /api/tree None; filtered tree
GET /api/file Query path
POST /api/file path, string content
POST /api/create-folder parentPath, name
POST /api/create-file parentPath, name
POST /api/rename oldPath, newName, isFolder
DELETE /api/delete Query path

The role module rejects absolute/traversal/NUL paths and resolves the deepest existing ancestor to check symlink containment. An editor only sees assigned real top-level sections, including their descendants. Out-of-assignment content is normally 404; attempts outside content can be 403. Section-root write/rename/delete is admin-only. Cross-section editor rename is refused. Root Markdown belongs to the administrator.

Creation normalizes names, requires an existing parent when supplied and refuses existing targets with 409. Generated templates are scaffolds, not finished documentation. Rename is within the current parent, not a general move endpoint. Top-level section rename updates account assignments, invalidates affected sessions and attempts filesystem rollback if that update fails.

Ordinary Save writes Markdown directly and has no revision token or coauthor conflict merge. It can create missing parent directories after authorization. Delete is recursive and has no built-in recycle bin; there is no account deletion endpoint in this handler. Coordinate writers and retain backups/Git. Metadata revision checks below must not be mistaken for general file conflict protection.

Administrator management

Area Internal routes
Accounts /api/users and user detail
Access /api/users/sections
Status /api/users/disable
Recovery /api/users/reset-password
Sign-out /api/users/sign-out
Sections /api/content-catalog, /api/sections/settings
Site /api/site-settings, /api/links
Operations /api/security, /api/activity
Publication /api/publish, status/history

Account creation is POST; listing/detail is GET. Assignment/status/reset/sign-out are POST. Browser reset supports editors only and returns a one-time temporary password with forced change. Self-disable is refused; server-side recovery handles administrator reset. Never log or paste temporary secrets into public diagnostics.

Section settings POST accepts section name, title, integer order, boolean hidden and string revision. It updates only owned YAML keys while retaining body/other metadata, checks SHA-256 of current text and returns 409 when changed. Complex metadata that cannot be retained safely fails rather than silently discarding it. Catalog is derived from current source and account assignments; it is not the builder’s published-output inventory.

Site settings GET returns effective safe fields, revision and raw-HTML policy; POST validates safe overrides and revision. Links POST replaces the whole list with no revision merge. Icon fetch is capped at five per save; a fetch failure does not block link saving. Unchanged links retain icons, changed URLs drop old icons, explicit refresh may keep the old icon if no replacement is found. Site settings and links covers practical checks.

Activity/history GET accepts event/actor/target/before/limit filters; publish history limits to publish records. Security GET exposes current session policy, cookie mode, raw-HTML flag, audit error and limiter summaries, not credentials. Audit persistence failures report a warning without turning a completed publish into a failed publication.

Response contracts by route

These are the current private browser client’s internal routes, not a stable third-party API or instructions to expose the editor publicly. JSON errors normally have an error string; successful mutations normally include success: true. Tree, login, me, lists/status and settings/catalog reads use their own shapes below. Success is 200, not 201 or 202. No route below automatically commits, pushes or deploys remotely.

The request gates run first, so a handler error table is not a promise that an unauthenticated or wrong-origin caller reaches that handler. Preserve source text and inspect current saved state when a response is lost. Do not copy cookie, CSRF or temporary-password response values into a support report.

Session routes

Route Result
POST /api/login Safe user, csrf, session cookie
GET /api/me Safe user, csrf
POST /api/logout success; expired cookie
POST /api/change-password success, fresh user/CSRF/cookie

Login body is name, password, both strings. Change body is string current and password. Own change ends other sessions and renews this one. Login’s safe user is name/role/sections/forced-change, not the stored hash. /api/me and logout/change are the only API paths available while forced change is pending, subject to their methods and CSRF.

Session failure Status
Missing/wrong login input types 400
Wrong user/password or disabled account 401
Login limiter block 429
Wrong current password 403
Equal supplied password strings 400
Password policy on own change 400
Account sign-in race/storage exception Normally 500

The login race message from UserStore is internal diagnostics, not automatically an exposed 400. Login catches through the generic failure helper; own-change/management catches use the narrower safe-validation helper. Do not interpret every string in source as a browser-visible error.

File routes

Route Result
GET /api/tree Tree array, not wrapper
GET /api/file String content
POST /api/file success
POST /api/create-folder success, relative path
POST /api/create-file success, relative path
POST /api/rename success, relative newPath
DELETE /api/delete success
Operation Handler-specific failures
Read 400 missing/invalid path; 403 containment; 404 absent file/scope
Save 400 path/content; 403 containment; role/path denial; caught storage 500
Create 400 parent/name; 403 containment; 404 parent/scope; 409 exists
Rename 400 request/section name; 403 containment/root; 404 source/scope; 409 target
Delete 400 missing/invalid path; 403 root/containment; 404 absent/scope; caught storage 500

Paths refer to source. The role layer can add 400/401/403/404 denials. Read/delete invalid input says “Missing path parameter” even when a supplied value is unsuitable. Create templates and normalized names are not arbitrary filesystem write endpoints. Rename’s isFolder selects name normalization by truthiness; it is not proof the existing source has that type. Use the real browser action and verify source/output afterward.

Read is a synchronous file read outside a local catch. An authorized existing directory or low-level read exception is not guaranteed to return the generic JSON500 shape. Tree scanning/account/session resolution also have exceptions outside particular handler catches. Validate the intended Markdown file and inspect process state; do not claim every filesystem fault has uniform JSON recovery.

Account routes

For the route table, users means /api/users; suffixes are under /api/users/.

All require admin. Creation body supplies name, role, password, sections; valid roles are admin/editor. Admin sections are normalized to empty. Omitted/falsy creation sections use an empty list. Assignments are real top-level section names, not display titles. Management mutation targets use body name.

Route Result
GET users Safe users, current folder sections
GET :name Safe user, session summaries, target activity
POST users success, safe user
POST sign-out success, ended, signedOut
POST sections success
POST disable success
POST reset-password success, one-time temporaryPassword
Action Boundary
Detail 400 name regex; 404 missing target
Create 400 invalid fields or existing user
Sign-out 400 absent or invalid target
Sections 400 missing user or invalid sections
Disable 400 self-disable, last admin or missing user
Reset 400 missing user or non-editor

Disable coerces body disabled to boolean; send actual booleans through the browser, not string “false”. Reset generates 20 random characters displayed in four groups of five; it sets forced change and ends sessions, but does not automatically enable a disabled editor. Only server recovery handles administrator password reset. A sign-out target equal to current admin returns signedOut: true; honor that outcome rather than retrying with a now-invalid session.

Presentation and catalog routes

In these tables, every path is under ``.

Route Result
GET links links, max, maxTitle
POST links success, links, icon notes
GET icon file Private image bytes/cache policy
GET site-settings Effective settings, revision, raw HTML flag
POST site-settings success, effective settings, new revision
GET content-catalog sections, pages
POST sections/settings success, new revision

Links body is {links: [...]} and replaces the whole list. The icon-file route is /api/links/icon/:filename. Site GET names its raw HTML flag rawHtmlEnabled. Icon filename must match 12 lowercase hexadecimal ID plus png/ico/gif/jpg/webp extension; unknown/missing icon is 404. List validation is 400. Fetch failure is normally a note within a successful save. A list/storage failure and a missing icon are different outcomes.

Site body is settings, revision. Section body is name, title, order, hidden, revision. Both revision conflicts are 409. Section invalid name is 400, absent real section 404. Site settings invalid supplied values normally 400 through the safe helper; unsupported method 405. Content administration’s unsupported method/path returns 404. Complex frontmatter preservation failures begin “Complex” or “Section metadata”, so the safe-validation prefix helper does not expose them as 400: these normally become generic 500, with the actionable source reason in private diagnostics. Preserve intended values and edit the actual _index.md safely instead of pretending a revision bypass fixes it.

Publication and system routes

Route Result
POST publish success, job id
GET publish/status State/stages plus saved-change marker
GET publish/history records, hasMore, nextBefore
GET activity records, hasMore, nextBefore
GET security Policy, cookie, raw HTML, audit and limiter summaries

Publish start does not parse a body, although the POST media-type gate still applies. It returns a job ID before asynchronous completion. Busy start is 409. Polling status is not proof the job is done, correct or remotely deployed. Idle status has no job/page/log fields; job status includes stages, progress, last 80 log lines, current error/failed stage/live/previous state and diagnostics on failure.

Activity/history query filters are exact event, actor, target, opaque before and bounded numeric limit. History also requires a publish summary. Unknown cursor yields no older records, not a history-integrity proof. System reads are admin-only and return 405 for non-GET methods. Security includes no credential/token values. Security and history owns UI pagination and retention details.

Common gate and fallback statuses

Status Current cause
400 URL parse, invalid JSON or explicit input validation
401 API session missing/expired/revoked
403 Host, Origin, forced change, CSRF, role/path
404 Unknown route, wrong method on most routes, scope/absent target
405 Explicit system/site-settings method check
409 Existing target, stale revision or running publish
413 Body exceeds configured byte limit
415 POST media type is not JSON
429 Login limiter pause
500 Caught unclassified/storage failure

Wrong method is not uniformly405. Many unmatched routes fall through to404; system/site handlers explicitly use405. Common JSON helper does not add identical cache headers to every API: session-cookie responses use no-store, tree uses no-cache, icon response uses private five-minute caching. Public editor/Admin pages have their own no-store/CSP protections. Inspect actual route semantics rather than borrowing a general REST convention.

Publisher boundary

Only an admin can start publication or read its status. One in-process job runs at a time; another start returns 409. Stages are snapshot, build, verify and switch. Markdown and selected runtime data are copied; installed static files, templates and file configuration remain builder inputs, not a fully isolated engine snapshot.

The same builder runs with separate content/data/output paths. Verification checks expected output structure, not all authored links, security, accessibility or deployment. Two filesystem renames switch output and keep a previous copy; they are not an indivisible atomic transaction. State-marker or backup-cleanup warnings can occur after new output is live. Inspect actual job state and live output before retrying.

Unpublished detection fingerprints paths, sizes, rounded modification times and directories. It is not a content-hash integrity guarantee. UI status and local Publish do not confirm a remote host release. Publishing covers operator decisions and recovery.

Browser state and source map

Module Owns
editor.html Source editing and preview
editor.html save controller Save and unsaved state
editor.html draft store Browser-local recovery
publish-controller.js Shared job UI
content-controller.js Sections and catalog
site-controller.js Presentation settings
system-controller.js Accounts, history and security

Drafts are local browser storage, not server history or a merge protocol. Preview differs from the final reader renderer. Server role/path checks remain necessary regardless of controller state. Review API tests, synthetic-role behavior, storage failures and actual error-state pixels together before changing these internals.