@zvndev/darkmown 0.12.0 → 0.12.1

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/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
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
+ ## 0.12.1 — 2026-06-18
6
+
7
+ Release-hygiene and DX polish (post-0.12.0 smoke audit).
8
+
9
+ - **Clean compile errors.** `darkmown build` prints a single `✗ <message>` line on a compile error instead of a raw Node.js stack trace (the dev rebuild log inherits the same clean output). The message still carries the file path and corrective hint.
10
+ - **Bare `darkmown` prints help.** Running the command with no arguments shows usage instead of silently running a build.
11
+ - **Social-share images.** A new `image:` frontmatter key sets `og:image` / `twitter:image` and upgrades the Twitter card to `summary_large_image`. The home page now ships an Open Graph card.
12
+ - **`SECURITY.md` ships in the package** so the README's in-package security link resolves for npm consumers.
13
+ - **Static assets documented.** README explains that non-page shelf files (`site/_/`) are copied to `/__wd/media/` (and `.json` to `/__wd/data/`).
14
+ - **README badges** added (npm version, CI, provenance, license).
15
+
5
16
  ## 0.12.0 — 2026-06-18
6
17
 
7
18
  Security and deploy-hardening sprint.
package/README.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Darkmown
2
2
 
3
+ [![npm](https://img.shields.io/npm/v/@zvndev/darkmown)](https://www.npmjs.com/package/@zvndev/darkmown)
4
+ [![CI](https://github.com/ZVN-DEV/darkmown/actions/workflows/ci.yml/badge.svg)](https://github.com/ZVN-DEV/darkmown/actions/workflows/ci.yml)
5
+ [![provenance](https://img.shields.io/badge/npm-provenance-blue)](https://www.npmjs.com/package/@zvndev/darkmown)
6
+ [![license](https://img.shields.io/npm/l/@zvndev/darkmown)](./LICENSE)
7
+
3
8
  **[darkmown.com](https://darkmown.com)** · Markdown that runs.
4
9
 
5
10
  Darkmown is a Markdown-native web framework. Two formats, one rule: `.md` stays plain CommonMark forever, and renaming a file to `.wd` ("whateverdown") is what unlocks directives — includes, loops, state, conditionals, and sections. Static pages ship **zero** framework JavaScript; reactive pages share one runtime around ~4.7 KB gzipped, CI-enforced under 5 KB.
@@ -74,6 +79,17 @@ tags: [sales, revenue, "q1, q2"]
74
79
  - `@loop meta.tags into tag` iterates an array field at build time (stays static, zero-JS).
75
80
  - Arrays are inline flow only (`[a, b]`); quoted items keep internal commas (`"q1, q2"`). A value without a leading `[` stays a plain string.
76
81
 
82
+ Three keys also drive the document `<head>`: `title` sets `<title>`, `description` adds the meta description plus Open Graph / Twitter tags, and `image` (an absolute URL) sets the social-share preview (`og:image` / `twitter:image` and a `summary_large_image` card).
83
+
84
+ ## Static assets
85
+
86
+ Images, fonts, icons, and other non-page files live on the include shelf at `site/_/` and are copied into the build untouched:
87
+
88
+ - Any non-`.md`/`.wd` file in `site/_/` → served at `/__wd/media/<path>` (e.g. `site/_/logo.svg` → `/__wd/media/logo.svg`).
89
+ - `.json` files → served at `/__wd/data/<name>` (this is what `:fetch` reads).
90
+
91
+ Reference them with a normal URL: `![logo](/__wd/media/logo.svg)`. The preview/`serve` layer returns the correct `Content-Type` per extension. (Files dropped in `site/pages/` are treated as routes, not assets — put shareable assets on the shelf.)
92
+
77
93
  ## Loops
78
94
 
79
95
  `@loop <things> into <thing>` is the only loop. The source decides the behavior:
package/SECURITY.md ADDED
@@ -0,0 +1,39 @@
1
+ # Security Policy
2
+
3
+ ## Supported versions
4
+
5
+ Darkmown is pre-1.0. Only the latest published minor version receives security fixes.
6
+
7
+ ## Reporting a vulnerability
8
+
9
+ Please report vulnerabilities privately via [GitHub Security Advisories](https://github.com/ZVN-DEV/darkmown/security/advisories/new). Do not open public issues for security reports.
10
+
11
+ You can expect an initial response within 72 hours. If the report is accepted, a fix ships as the next patch release and the advisory is published after the fix is available.
12
+
13
+ ## Trust boundary / threat model
14
+
15
+ Darkmown is a **trusted-author** static site generator. The threat model assumes you compile content you wrote yourself, the same way you trust your own source code — it does not assume the content is hostile. Within that boundary the guarantees below hold; three assumptions define the boundary itself:
16
+
17
+ 1. **Compile only trusted, author-written content.** Do not compile `.md`/`.wd` files you did not write — user-generated content, third-party docs, or form input — without applying your own sanitization first.
18
+ 2. **Raw HTML passes through by default.** `markdown-it` runs with `html: true`, so raw HTML in content is emitted verbatim. There is no built-in sanitizer, so a raw `<script>` or event-handler attribute in untrusted content would execute in the visitor's browser. A page can opt into strict mode with `html: false` frontmatter, which strips raw HTML.
19
+ 3. **`:fetch` has no URL allowlist, and reactive pages require `unsafe-eval`.** A `:fetch` URL is read directly from the page source, so requests go wherever the author wrote them. Reactive pages also need `script-src 'unsafe-eval'` in your Content-Security-Policy, because the runtime compiles validated `:computed` / `@loop … where` expressions through `new Function`; a strict CSP without `unsafe-eval` breaks reactive pages (static pages are unaffected).
20
+
21
+ This is the same trust posture as every Markdown SSG: the framework is safe for the trusted-author use case it targets, and unsafe content is the author's responsibility to sanitize before it reaches the compiler.
22
+
23
+ ## Security model
24
+
25
+ Things Darkmown deliberately guards at compile time and runtime:
26
+
27
+ - **No arbitrary JavaScript in content files.** Directive actions (`:button -> …`), `:computed` expressions, and `@loop … where` predicates compile through strict whitelisted grammars — only item paths, declared `:state`, numbers, and strings are allowed. Assignment, function calls, and unknown syntax are compile errors. Raw user content is never `eval`'d; what runs at runtime is the *validated* expression, compiled to the whitelisted grammar and executed via `new Function` (see `src/runtime.js`).
28
+ - **Prototype-walk protection.** Path lookups (`{ a.b.c }`, computed expressions) reject `constructor`, `prototype`, and `__proto__` segments in both the compiler and the runtime.
29
+ - **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
+ - **Output escaping.** Interpolated values are HTML-escaped; state scripts escape `<` to prevent script-tag breakout.
31
+ - **Static server path containment.** The dev/preview servers resolve requests strictly inside `dist`.
32
+
33
+ ## The biggest footgun: raw HTML passthrough
34
+
35
+ Darkmown configures markdown-it with `html: true`, so **raw HTML in `.md`/`.wd` files is passed through verbatim** — by design, like every Markdown site generator. This is the single most important thing to understand about Darkmown's security model.
36
+
37
+ - Treat all content files as **trusted input**, the same way you trust your own source code.
38
+ - **Do not compile untrusted or user-submitted Markdown** (comments, form input, third-party docs) without sanitizing it first. Darkmown ships no built-in sanitizer; raw `<script>` and event-handler attributes in untrusted content would execute in the visitor's browser.
39
+ - If you must render untrusted content, sanitize it (for example with a library like DOMPurify) before it reaches the compiler.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zvndev/darkmown",
3
- "version": "0.12.0",
3
+ "version": "0.12.1",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -34,6 +34,7 @@
34
34
  "docs/spec-alignment.md",
35
35
  "AGENTS.md",
36
36
  "README.md",
37
+ "SECURITY.md",
37
38
  "LICENSE",
38
39
  "CHANGELOG.md"
39
40
  ],
package/site/_/og.png ADDED
Binary file
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  title: Darkmown — Markdown that runs, not Markdown that needs React
3
3
  description: A live store widget — search filter, add-to-cart, running total — written as one Markdown file that runs. The same outcome in Markdoc needs a React component, hydration, and a build step. Compare them side by side.
4
+ image: https://darkmown.com/__wd/media/og.png
4
5
  ---
5
6
 
6
7
  @include /nav.wd
package/src/cli.js CHANGED
@@ -10,7 +10,9 @@ import { initProject } from "./scaffold.js";
10
10
  import { contentType, resolvePublicFile, serve } from "./statics.js";
11
11
 
12
12
  const cliPath = fileURLToPath(import.meta.url);
13
- const command = process.argv[2] || "build";
13
+ // Bare `darkmown` prints help (discovery) rather than silently building — a new
14
+ // user typing the command alone expects to see what it can do, not a no-op build.
15
+ const command = process.argv[2] || "help";
14
16
 
15
17
  if (command === "help" || command === "--help" || command === "-h") {
16
18
  printHelp();
@@ -18,14 +20,24 @@ if (command === "help" || command === "--help" || command === "-h") {
18
20
  const pkg = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8"));
19
21
  console.log(pkg.version);
20
22
  } else if (command === "init") {
21
- const target = process.argv[3] || ".";
22
- const result = initProject(path.resolve(process.cwd(), target));
23
- const relativeRoot = path.relative(process.cwd(), result.root) || ".";
24
- console.log(`Created Darkmown project at ${relativeRoot}`);
25
- console.log(`Next: ${nextStep(relativeRoot)}`);
23
+ try {
24
+ const target = process.argv[3] || ".";
25
+ const result = initProject(path.resolve(process.cwd(), target));
26
+ const relativeRoot = path.relative(process.cwd(), result.root) || ".";
27
+ console.log(`Created Darkmown project at ${relativeRoot}`);
28
+ console.log(`Next: ${nextStep(relativeRoot)}`);
29
+ } catch (error) {
30
+ failClean(error);
31
+ }
26
32
  } else if (command === "build") {
27
- const result = buildSite();
28
- console.log(`Built ${result.routes.length} routes into ${path.relative(process.cwd(), result.distRoot)}`);
33
+ try {
34
+ const result = buildSite();
35
+ console.log(`Built ${result.routes.length} routes into ${path.relative(process.cwd(), result.distRoot)}`);
36
+ } catch (error) {
37
+ // A compile error has a clear, file-pathed message — print just that, not a
38
+ // Node stack trace, so a typo reads as "fix your page", not "the tool crashed".
39
+ failClean(error);
40
+ }
29
41
  } else if (command === "dev") {
30
42
  const port = Number(process.env.PORT || 5173);
31
43
  const host = process.env.HOST || "127.0.0.1";
@@ -170,6 +182,17 @@ function broadcast(clients, message) {
170
182
  for (const client of clients) client.write(message);
171
183
  }
172
184
 
185
+ /**
186
+ * Print a compile/CLI error as a single clean line and exit non-zero — no Node
187
+ * stack trace. The thrown Error already carries the file path + corrective hint.
188
+ * @param {unknown} error
189
+ * @returns {never}
190
+ */
191
+ function failClean(error) {
192
+ console.error(`✗ ${error instanceof Error ? error.message : String(error)}`);
193
+ process.exit(1);
194
+ }
195
+
173
196
  function printHelp() {
174
197
  console.log(`Darkmown
175
198
 
package/src/compiler.js CHANGED
@@ -110,13 +110,24 @@ export function compilePage(file, context) {
110
110
  const compiled = compileDocument(file, context);
111
111
  const title = compiled.meta.title || "Darkmown";
112
112
  const description = compiled.meta.description || "";
113
- const descriptionTag = description
114
- ? `\n <meta name="description" content="${escapeHtml(description)}">
115
- <meta property="og:title" content="${escapeHtml(title)}">
116
- <meta property="og:description" content="${escapeHtml(description)}">
117
- <meta property="og:type" content="website">
118
- <meta name="twitter:card" content="summary">`
119
- : "";
113
+ // Optional `image:` frontmatter sets the social-share preview (absolute URL).
114
+ const image = typeof compiled.meta.image === "string" ? compiled.meta.image : "";
115
+ /** @type {string[]} */
116
+ const social = [];
117
+ if (description) social.push(`<meta name="description" content="${escapeHtml(description)}">`);
118
+ if (description || image) {
119
+ social.push(`<meta property="og:title" content="${escapeHtml(title)}">`);
120
+ if (description) social.push(`<meta property="og:description" content="${escapeHtml(description)}">`);
121
+ social.push(`<meta property="og:type" content="website">`);
122
+ }
123
+ if (image) {
124
+ social.push(`<meta property="og:image" content="${escapeHtml(image)}">`);
125
+ social.push(`<meta name="twitter:image" content="${escapeHtml(image)}">`);
126
+ }
127
+ if (description || image) {
128
+ social.push(`<meta name="twitter:card" content="${image ? "summary_large_image" : "summary"}">`);
129
+ }
130
+ const descriptionTag = social.length ? `\n ${social.join("\n ")}` : "";
120
131
  const favicon = "data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2032%2032'%3E%3Crect%20width='32'%20height='32'%20rx='6'%20fill='%2318221d'/%3E%3Ctext%20x='16'%20y='23'%20text-anchor='middle'%20font-family='Georgia,serif'%20font-size='19'%20font-weight='bold'%20fill='%23f7f3ea'%3ED%3C/text%3E%3C/svg%3E";
121
132
  const cssLinks = [...compiled.assets.skins].map((href) => `<link rel="stylesheet" href="${href}">`).join("\n");
122
133
  const scriptSrcs = compiled.assets.runtime ? ["/__wd/runtime.js", ...compiled.assets.scripts] : [...compiled.assets.scripts];