@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 +11 -0
- package/README.md +16 -0
- package/SECURITY.md +39 -0
- package/package.json +2 -1
- package/site/_/og.png +0 -0
- package/site/pages/index.wd +1 -0
- package/src/cli.js +31 -8
- package/src/compiler.js +18 -7
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
|
+
[](https://www.npmjs.com/package/@zvndev/darkmown)
|
|
4
|
+
[](https://github.com/ZVN-DEV/darkmown/actions/workflows/ci.yml)
|
|
5
|
+
[](https://www.npmjs.com/package/@zvndev/darkmown)
|
|
6
|
+
[](./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: ``. 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.
|
|
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
|
package/site/pages/index.wd
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
28
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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];
|