Editor internals
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.