@zvndev/darkmown 2.12.2 → 2.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -5,6 +5,7 @@ Darkmown is a Markdown-native web framework. You build sites by writing `.md` an
5
5
  ## The one rule
6
6
 
7
7
  - `.md` files are plain CommonMark. Directives stay inert text.
8
+ - `.wd` adds read-only task lists: `- [ ] item` and `- [x] item` render as checkboxes, with no runtime. For a checkbox the reader can tick, use `:checkbox`.
8
9
  - `.wd` files are the same Markdown **plus directives** (state, loops, conditionals, includes, forms).
9
10
  - To make a `.md` page interactive, **rename it to `.wd`**. ⚠️ Renaming means the old `.md` is **gone** — never leave both `index.md` and `index.wd`, that is a fatal `Duplicate route` build error. Upgrade = delete the `.md`, create the `.wd`.
10
11
  - Static pages ship **zero** JavaScript; a page loads the ~6.6 KB gzipped runtime (CI-enforced under 8 KB) only if it declares reactive behavior. What ships at `/__wd/runtime.js` is a minified build of the readable source, with a sourcemap beside it at `/__wd/runtime.js.map`.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,49 @@
2
2
 
3
3
  All notable changes to Darkmown are documented here. Versions follow [semver](https://semver.org); pre-1.0 minor versions may contain breaking changes.
4
4
 
5
+ ## 2.13.0 (2026-11-01)
6
+
7
+ ### Added
8
+
9
+ - `.wd` task lists: `- [ ] item` and `- [x] item` render as read-only checkboxes, with no runtime. `.md` stays strict CommonMark.
10
+
11
+ ## 2.12.4 (2026-11-01)
12
+
13
+ Closes what the 2026-10-23 audit got past the 2.12.3 security pass, and stops two kinds of broken page from passing `validate` as clean.
14
+
15
+ ### Security
16
+
17
+ - Raw-HTML interpolation now reads a fragment the way the browser's tokenizer does. A `>` inside an earlier quoted value (`<button title=">" onclick="{ x }">`) no longer hides the attribute from the guard.
18
+ - A value in a `<script>` or `<style>` body is dropped with a warning. JSON scripts such as `application/ld+json` still take values.
19
+ - `to`, `from`, `by` and each entry of `values` get the URL scheme check, closing SVG `<animate>` and `<set>`.
20
+ - `<{ x }>` can no longer choose the tag name.
21
+ - The dev and preview servers apply the cross-site guard to every URL the `api/` runner serves, including `/api` itself.
22
+ - Host-only files are refused in any letter case, for macOS and Windows filesystems.
23
+ - A `:fetch` placeholder holding `[".."]` is treated as missing, like `..`.
24
+ - The generated Cloudflare worker answers malformed percent-encoding with a 400 instead of throwing.
25
+
26
+ ### Warnings
27
+
28
+ - `{ name }` that matches nothing now warns with its line and the nearest declared name. It still ships as literal text. Write `\{ name }` to show braces on purpose.
29
+ - A `:fetch` URL reading a name the page never declares now warns, since the request will not fire until something sets it.
30
+
31
+ ### Fixed
32
+
33
+ - The MCP stdio test accepts a prerelease version, which had been failing every canary since 2026-10-15.
34
+
35
+ ## 2.12.3 (2026-10-22)
36
+
37
+ A security pass over the runtime, the compiler's output escaping, and the local servers. Nothing found executed script on a default (`html: false`) page; the fixes close denial-of-service, redirect, and leak paths, plus script injection on `html: true` pages fed by data.
38
+
39
+ - **Local dev/preview servers refuse other websites.** `darkmown dev` and `darkmown serve` now answer `403` to an unrecognized `Host` (DNS rebinding) and to cross-site calls into `/api/*`. Before, any page you visited could rebind its domain to `127.0.0.1` and read drafts, the dependency map, and `api/*` responses, whose handlers run with the full `process.env`. New `DARKMOWN_ALLOWED_HOSTS` env var for tunnels (`*` disables). **BEHAVIOR CHANGE** if you open the dev server through a custom domain or tunnel: add it to the variable.
40
+ - **Runtime: hostile JSON can no longer wedge a page.** `?n={"toString":1}` on a `from-url` key made every render throw, and a `persist` key then kept the page broken after reloading the clean URL. All untrusted JSON (fetch bodies, URL params, localStorage, storage events) now drops own `toString`/`valueOf` keys. A `:fetch` placeholder valued `.` or `..` is treated as missing, so `?id=..` can no longer retarget `/api/users/{ id }/profile` at `/api/profile`. Runtime 6,603 → 6,621 B gzipped.
41
+ - **Raw HTML interpolation is attribute-context aware** on `html: true` pages: values in `on*=`, `srcdoc=`, or bare tag position are emitted empty with a warning; unquoted values are entity-encoded; `data`, `poster`, `cite`, `background` and `ping` join the URL scheme guard.
42
+ - **Collection-author vectors closed.** The excerpt/RSS description regex was quadratic (a 200k-character body took 17 s or more per call); one control character in a post title made `rss.xml` invalid XML; a data-driven image src could probe for `@width` variants outside `site/`; a symlinked `.md` page compiled any file on the machine into `dist`. All fixed. A `__proto__` frontmatter key is ignored.
43
+ - **Preview parity.** `serve` no longer serves `_worker.js/` (your `api/` handler source), `_headers`, `_redirects`, `_routes.json`, or the dev dependency map, and sends `nosniff` and the other base headers on every file, not only HTML.
44
+ - **HSTS.** Every surface now sends `Strict-Transport-Security: max-age=31536000` (no `includeSubDomains`/`preload`).
45
+ - **Scaffold.** `init` writes with exclusive opens, so a dangling symlink in the target can no longer redirect a write, and `--template` must name a shipped template.
46
+ - **Dependencies and CI.** `markdown-it` 14.3.3 fixes two quadratic-time advisories (GHSA-vfgh-jcmw-g37q, GHSA-gfr2-rv44-r23c; the second affects default options). The CI workflow token is now `contents: read`.
47
+
5
48
  ## 2.12.1 (2026-10-16)
6
49
 
7
50
  A one-item patch revising the navigation animation again, this time from frame-level measurement instead of reading the stylesheet.
package/README.md CHANGED
@@ -68,6 +68,15 @@ We rebuilt a live production marketing site (Next.js 16, React 19, Tailwind, on
68
68
 
69
69
  The honest losses, because a benchmark that only wins is marketing: the rebuild shipped 1.07 MB of raw TTF fonts against `next/font`'s 158 KB of subset woff2, and 1,045 KB of unoptimized photos against 147 KB of CDN width variants. Fonts now have a documented woff2 + subsetting recipe (2.8), and responsive image variants are generated at build time (2.9).
70
70
 
71
+ ## Example rebuilds
72
+
73
+ Private example repos under ZVN-DEV. Each one rebuilds a live Next.js marketing site in Darkmown, pulls content from the same production CMS at build time, and leaves the live site untouched. Each repo's commit history includes Darkmown friction notes for the framework.
74
+
75
+ - `darkmown-toothdocs`: ToothDocs rebuild, 67 routes. The benchmark subject above.
76
+ - `darkmown-7thst`: 7th Street Orthodontics rebuild, 36 routes.
77
+ - `darkmown-eetf`: East End Tooth Ferry rebuild, 31 routes.
78
+ - `darkmown-dafko`: DAFKO rebuild, 40 routes.
79
+
71
80
  ## Working from this repo
72
81
 
73
82
  ```sh
@@ -94,6 +103,7 @@ npm run dev # live demo site — the same site that runs darkmown.com
94
103
 
95
104
  - `site/pages` is the route tree. `.md` and `.wd` files become pages.
96
105
  - `.md` is strict CommonMark (real parser: ordered lists, tables, blockquotes, images, the lot). Directives stay plain text, and the build hints when it spots `.wd` syntax in a `.md` file.
106
+ - `.wd` adds read-only task lists: `- [ ] item` and `- [x] item` render as checkboxes, with no runtime. For a checkbox the reader can tick, use `:checkbox`.
97
107
  - Files or folders starting with `.`, `-`, or `_` are hidden from routing.
98
108
  - `site/_` is the include shelf for `@include /name.wd`.
99
109
  - Matching `page.skin` and `page.js` colocate styling and behavior by basename.
package/SECURITY.md CHANGED
@@ -18,6 +18,9 @@ Darkmown is a **trusted-author** static site generator. The threat model assumes
18
18
  2. **Raw HTML is escaped by default; `html: true` re-opens the boundary per page.** `markdown-it` runs with `html: false`, so raw HTML in content renders as inert escaped text — a `<script>` or `onerror=` attribute in a contributed markdown file cannot execute. A page opts back into verbatim raw HTML with `html: true` frontmatter; on such a page there is no built-in sanitizer and untrusted content would execute in the visitor's browser, so reserve the opt-in for pages whose HTML you wrote yourself.
19
19
  3. **`:fetch` and `:form action=` have no host allowlist.** A `:fetch`, `refresh=`, or `:form action=` URL is read directly from the page source; the compiler rejects non-http(s) schemes (`file:`, `data:`, `javascript:`, protocol-relative `//host`) but does not restrict which hosts you call, so SSRF/exfiltration protection is the author's responsibility. *(Since 2.1, reactive pages no longer need `'unsafe-eval'` — the runtime interprets a validated expression AST instead of building a `new Function`, so reactive and static pages share the same strict, eval-free CSP.)*
20
20
 
21
+ 4. **Collections that take contributions need a `_schema.wd`.** `html:` is per-file frontmatter, so without a schema a contributor can put `html: true` on their own entry and opt it back into raw HTML. A collection with a `_schema.wd` rejects any undeclared frontmatter key at build time (`WD122`), so `html: true` fails the build unless the maintainer declares `html` in the schema.
22
+ 5. **Bound link destinations are not restricted to your origin.** `[Next]({ next })` paints whatever `next` holds. `javascript:`, `data:` and `vbscript:` are always blanked, but an `https://other.example` or `//other.example` value is a working link. If the value comes from the URL (`from-url`) or a `:fetch`, that is an open-redirect shape; build the destination from a literal prefix (`/posts/{ slug }/`) instead.
23
+
21
24
  The default is deliberately stricter than most Markdown SSGs: with content collections making multi-author markdown a first-class input, escaped-by-default is the only default that is safe when one of those authors isn't you. Unsafe content on an `html: true` page remains the author's responsibility to sanitize before it reaches the compiler.
22
25
 
23
26
  ## Security model
@@ -28,8 +31,11 @@ Things Darkmown deliberately guards at compile time and runtime:
28
31
  - **Prototype-walk protection.** Path lookups (`{ a.b.c }`, computed expressions) reject `constructor`, `prototype`, and `__proto__` segments in both the compiler and the runtime.
29
32
  - **Include sandboxing.** `@include` and `@loop` data sources must resolve inside `site/pages` or `site/_`; traversal outside the site tree is a compile error, and include cycles are detected.
30
33
  - **Private page assets stay private.** Page-colocated assets under `site/pages` are copied only when their relative path has no hidden segment (`.`, `-`, or `_`) and is not a symlink. This keeps `.env`, `_private/*`, `-draft/*`, and symlink-to-outside files out of `dist`.
31
- - **Output escaping.** Interpolated values are HTML-escaped; state scripts escape `<` to prevent script-tag breakout.
32
- - **Static server path containment.** The dev/preview servers resolve requests strictly inside `dist`.
34
+ - **Output escaping.** Interpolated values are HTML-escaped; state scripts escape `<` to prevent script-tag breakout. In raw HTML on an `html: true` page, escaping is context-aware: a value that lands in an `on*=` handler, a `srcdoc=`, or bare tag position (`<div { x }>`) is emitted empty with a warning, an unquoted attribute value is entity-encoded so it stays one value, and every URL-bearing attribute (`href`, `src`, `action`, `formaction`, `xlink:href`, `data`, `poster`, `cite`, `background`, `ping`) refuses executable schemes.
35
+ - **Untrusted runtime JSON.** A `:fetch` body, a `from-url` parameter, a `persist` value from localStorage and a cross-tab storage event all parse through one helper that drops own `toString`/`valueOf` keys, so a crafted value cannot make every render throw. A `:fetch` URL placeholder whose value is `.` or `..` is treated as missing rather than resolved as a path segment.
36
+ - **Symlinked pages are not routes.** A `.md`/`.wd` symlink under `site/pages` is skipped, like every other symlinked asset, so a contributed symlink cannot compile a file from elsewhere on the build machine into `dist`.
37
+ - **Static server path containment.** The dev/preview servers resolve requests strictly inside `dist`, and never serve host-only build files (`_worker.js/`, `_headers`, `_redirects`, `_routes.json`, the dev dependency map).
38
+ - **Local servers refuse other websites.** `darkmown dev` and `darkmown serve` bind to `127.0.0.1` and also answer `403` to a `Host` that is not `localhost`, `*.localhost`, an IP literal, or the bind address. That blocks DNS rebinding, which would otherwise let any page you visit read drafts and `api/*` responses (handlers run with your full environment). Requests to `/api/*` additionally refuse `Sec-Fetch-Site: cross-site` and non-local `Origin`s, so a website cannot trigger a handler's side effects with a blind POST. Set `DARKMOWN_ALLOWED_HOSTS=abc.ngrok.app` (comma-separated) to preview through a tunnel; `*` turns the guard off.
33
39
  - **Build caches are trusted input.** The `node_modules/.cache/darkmown-*` directories (image variants, font subsets) are copied into `dist` verbatim when their content-hash keys match, so a poisoned shared CI cache ships attacker bytes. Pin and verify cache artifacts in CI, or delete the directories to rebuild from source.
34
40
 
35
41
  ## The #1 footgun: the raw HTML opt-in (`html: true`)
@@ -63,6 +69,7 @@ Builds emit security response headers so a deployed site is hardened by default
63
69
 
64
70
  - **`Content-Security-Policy`** — **no `'unsafe-inline'` and no `'unsafe-eval'` on `script-src`, for any page.** The inline state seed is a `<script type="application/json">` data block, which is non-executable and therefore not gated by `script-src` at all; the one inline script CSP does gate — the fixed `<script type="speculationrules">` block a `transitions: true` page emits — is authorized by a build-time `'sha256-…'` hash source (plus the `'inline-speculation-rules'` keyword for browsers that check that instead). Since 2.1, **static and reactive pages share the same strict, eval-free policy**: the reactive runtime interprets a validated `:computed` / `@loop … where` / `.class when` expression AST instead of building a `new Function`, so it needs no `'unsafe-eval'`. The only `script-src` a reactive page adds over a static one is `'self'` for the same-origin `/__wd/runtime.js` — already granted.
65
71
  - **`X-Content-Type-Options: nosniff`** — stops MIME-type sniffing.
72
+ - **`Strict-Transport-Security: max-age=31536000`**: one year of HTTPS-only for the host. It deliberately omits `includeSubDomains` and `preload`, which pin every sibling subdomain; add them in your deploy config if you own the whole domain.
66
73
  - **`Referrer-Policy`** — limits referrer leakage.
67
74
  - **`frame-ancestors`** — clickjacking protection (controls who may frame the page).
68
75
 
package/docs/cli.md CHANGED
@@ -73,6 +73,16 @@ Rebuilds are **incremental** for `site/` content changes. Every dev build writes
73
73
 
74
74
  Correctness beats speed: **any uncertainty runs a full rebuild** — a new, deleted, or renamed file; a file no dependency graph accounts for; a change to the site-wide feed link; a missing or stale map. A change under `src/` (the framework's own code) always runs a full rebuild in a child process so fresh modules load. Production `darkmown build` writes no map and is unaffected.
75
75
 
76
+ Both `dev` and `serve` read three environment variables:
77
+
78
+ | Variable | Default | Meaning |
79
+ |---|---|---|
80
+ | `PORT` | `5173` (dev), `4173` (serve) | Port to listen on. |
81
+ | `HOST` | `127.0.0.1` | Address to bind. `0.0.0.0` exposes the server to your network. |
82
+ | `DARKMOWN_ALLOWED_HOSTS` | empty | Extra `Host` names to accept, comma-separated, such as a tunnel domain. `*` disables the check. |
83
+
84
+ Requests whose `Host` is not `localhost`, `*.localhost`, an IP literal, the bind address, or listed in `DARKMOWN_ALLOWED_HOSTS` get a `403`. This stops DNS rebinding, where a website you visit points its own domain at `127.0.0.1` to read your dev server. Requests to `/api/*` also get a `403` when they are cross-site (`Sec-Fetch-Site: cross-site`, or an `Origin` that is not local or allow-listed).
85
+
76
86
  ### `darkmown build [--target cloudflare] [--drafts]`
77
87
 
78
88
  Compiles `site/pages` into `dist`. The output is always 100% static HTML.
@@ -115,6 +115,7 @@ The 1.3.0 cycle shipped the crawler *files* (sitemap, rss, robots). This closes
115
115
  ## Fundamentally aligned
116
116
 
117
117
  - **Markdown-first authoring:** `.md` is strict CommonMark rendered by a real parser (markdown-it); `.wd` is the same Markdown plus first-party directives. Renaming the file is the upgrade path — extensions genuinely gate functionality.
118
+ - **Task lists in `.wd`:** `- [ ]` and `- [x]` compile to read-only checkboxes in `.wd` only, as static output. `.md` stays strict CommonMark. Covered by `tests/task-lists.test.js`.
118
119
  - **Content tree over components:** routing, includes, and repeated fragments are file/document based instead of React-style components.
119
120
  - **Tiered output:** static routes emit HTML/CSS and no Darkmown runtime; reactive routes opt into `/__wd/runtime.js`, a minified build of the readable source with a sourcemap beside it (~6.6 KB gzipped, CI-enforced below the 8 KB target).
120
121
  - **No virtual DOM:** state changes patch direct DOM bindings, conditional regions, and keyed loop regions.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zvndev/darkmown",
3
- "version": "2.12.2",
3
+ "version": "2.13.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -80,7 +80,7 @@
80
80
  "dependencies": {
81
81
  "@types/markdown-it": "^14.1.2",
82
82
  "highlight.js": "^11.11.1",
83
- "markdown-it": "^14.3.0"
83
+ "markdown-it": "^14.3.3"
84
84
  },
85
85
  "devDependencies": {
86
86
  "@biomejs/biome": "^2.5.7",
@@ -91,6 +91,7 @@ For editing, there is a VS Code extension for `.wd` and `.skin` files: see [Edit
91
91
  ## The two formats
92
92
 
93
93
  - `.md` is strict CommonMark. Directives, includes, and `{ bindings }` stay plain text, and the build prints a hint if it spots `.wd` syntax in a `.md` file.
94
+ - `.wd` adds read-only task lists: `- [ ] item` and `- [x] item` render as checkboxes, with no runtime. For a checkbox the reader can tick, use `:checkbox`.
94
95
  - `.wd` is Markdown plus directives. Renaming a file from `.md` to `.wd` is the upgrade path — nothing else changes.
95
96
 
96
97
  ## Routing rules