Skip to content
Merged
57 changes: 57 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Changelog

All notable changes to git-cms are documented in this file.

## [Unreleased] — git-stunts branch

### Added

- **Content Identity Policy (M1.1):** Canonical slug validation with NFKC normalization, reserved word rejection, and `CmsValidationError` contract (`ContentIdentityPolicy.js`)
- **State Machine (M1.2):** Explicit draft/published/unpublished/reverted states with enforced transition rules (`ContentStatePolicy.js`)
Comment on lines 5 to 10
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Markdownlint MD022: headings need surrounding blank lines.

Lines 7, 22, and 29 (### Added, ### Changed, ### Fixed) each lack a blank line before them. This will trigger MD022 in CI if markdownlint is enforced.

Proposed fix (showing line 5–9; repeat for 21–22 and 28–29)
 ## [Unreleased] — git-stunts branch
 
 ### Added
+
 - **Content Identity Policy (M1.1):** Canonical slug validation with NFKC normalization, reserved word rejection, and `CmsValidationError` contract (`ContentIdentityPolicy.js`)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
## [Unreleased] — git-stunts branch
### Added
- **Content Identity Policy (M1.1):** Canonical slug validation with NFKC normalization, reserved word rejection, and `CmsValidationError` contract (`ContentIdentityPolicy.js`)
- **State Machine (M1.2):** Explicit draft/published/unpublished/reverted states with enforced transition rules (`ContentStatePolicy.js`)
## [Unreleased] — git-stunts branch
### Added
- **Content Identity Policy (M1.1):** Canonical slug validation with NFKC normalization, reserved word rejection, and `CmsValidationError` contract (`ContentIdentityPolicy.js`)
- **State Machine (M1.2):** Explicit draft/published/unpublished/reverted states with enforced transition rules (`ContentStatePolicy.js`)
🧰 Tools
🪛 markdownlint-cli2 (0.20.0)

[warning] 7-7: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

🤖 Prompt for AI Agents
In `@CHANGELOG.md` around lines 5 - 9, Add a blank line before each second-level
heading in CHANGELOG.md that currently lacks surrounding whitespace:
specifically insert an empty line immediately above the "### Added", "###
Changed", and "### Fixed" headings so they comply with markdownlint MD022;
update the three occurrences of the "### Added"/"### Changed"/"### Fixed"
headings in the Unreleased section to be preceded by a blank line.

- **Admin UI overhaul:** Split/edit/preview markdown editor (via `marked`), autosave, toast notifications, skeleton loading, drag-and-drop file uploads, metadata trailer editor, keyboard shortcuts (`Cmd+S`, `Esc`), dark mode token system
- **DI seam in CmsService:** Optional `graph` constructor param enables `InMemoryGraphAdapter` injection for zero-subprocess tests
- **In-memory test adapter:** Unit tests run in ~11ms instead of hundreds of ms (no `git init`/subprocess forks)
- **E2E test separation:** Real-git smoke tests in `test/git-e2e.test.js`, excluded from default `test:local` runs
- **`test:git-e2e` script:** Run real-git integration tests independently
- **`@git-stunts/alfred` dependency:** Resilience policy library (wired but not yet integrated)
- **`@git-stunts/docker-guard` dependency:** Docker isolation helpers
- **ROADMAP.md:** M0–M6 milestone plan with blocking graph
- **Formal LaTeX ADR** (`docs/adr-tex-2/`)
- **Onboarding scripts:** `setup.sh`, `demo.sh`, `quickstart.sh` with interactive menus
- **Dependency integrity check:** `check-dependency-integrity.mjs` prevents `file:` path regressions

### Changed

- CmsService now uses `@git-stunts/git-warp` `GitGraphAdapter` and `@git-stunts/plumbing` `GitRepositoryService` instead of raw plumbing calls
- All `repo.updateRef()` calls routed through `CmsService._updateRef()` for DI/production dual-path
- `listArticles()` supports both plumbing (`for-each-ref`) and in-memory (`graph.listRefs`) paths
- Server endpoints return structured `{ code, field }` errors for validation failures
- Swapped all `file:` dependency paths to versioned npm ranges (PP3)

### Fixed

- Symlink traversal hardening in static file serving
- Slug canonicalization enforced at all API ingress points
- Admin UI API calls aligned with server contract (query params, response shapes)
- Server integration test environment stabilized for CI
- **(P1) Stored XSS via markdown preview:** Sanitize `marked.parse()` output with DOMPurify
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

"markdown" is a proper noun — capitalize it.

markdown previewMarkdown preview. Flagged by LanguageTool; tiny but this is a user-facing changelog.

-- **(P1) Stored XSS via markdown preview:** Sanitize `marked.parse()` output with DOMPurify
+- **(P1) Stored XSS via Markdown preview:** Sanitize `marked.parse()` output with DOMPurify
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- **(P1) Stored XSS via markdown preview:** Sanitize `marked.parse()` output with DOMPurify
- **(P1) Stored XSS via Markdown preview:** Sanitize `marked.parse()` output with DOMPurify
🧰 Tools
🪛 LanguageTool

[uncategorized] ~34-~34: Did you mean the formatting language “Markdown” (= proper noun)?
Context: ...abilized for CI - (P1) Stored XSS via markdown preview: Sanitize marked.parse() ou...

(MARKDOWN_NNP)

🤖 Prompt for AI Agents
In `@CHANGELOG.md` at line 34, Update the changelog entry so the user-facing
phrase uses the proper noun capitalization: change "markdown preview" to
"Markdown preview" in the line that reads "- **(P1) Stored XSS via markdown
preview:** Sanitize `marked.parse()` output with DOMPurify" (referencing the
string containing `marked.parse()` and `DOMPurify` to locate the exact
sentence).

- **(P1) Unpublish atomicity:** Reorder `unpublishArticle` so draft ref updates before published ref deletion
- **(P2) XSS via slug/badge rendering:** Use `textContent` and DOM APIs instead of `innerHTML` interpolation
- **(P2) SRI hashes:** Add `integrity` + `crossorigin` to marked and DOMPurify CDN script tags
- **(P2) Null guards:** `revertArticle` and `unpublishArticle` throw `no_draft` when draft ref is missing; `_resolveArticleState` throws `article_not_found` when both draft and published refs are missing
- **(P2) uploadAsset DI guard:** Throw `unsupported_in_di_mode` when `cas`/`vault` are null
- **(P2) Trailer key casing:** Use camelCase `updatedAt` in `unpublishArticle` and `revertArticle` (was lowercase `updatedat` which broke `renderBadges` lookups); destructure out decoded lowercase key before spreading to avoid `TrailerInvalidError`
- **(P2) XSS in `escAttr`:** Escape single quotes (`'` → `'`) to prevent injection into single-quoted attributes
- **(P2) Supply-chain hardening:** Vendor Open Props CSS files locally (`public/css/`) instead of `@import` from unpkg, eliminating CDN dependency and SRI gap
- **(P2) Monkey-patch safety:** E2E test restores `plumbing.execute` in `finally` block
- Unknown `draftStatus` in `resolveEffectiveState` now throws `unknown_status` instead of silently falling through to draft
- Removed double-canonicalization in `_resolveArticleState`
- Replaced sequential `readRef` loop with `Promise.all` in `listArticles` DI path
- Admin UI: fixed `removeTrailerRow` redundant positional removal, FileReader error handling, autosave-while-saving guard, Escape key scoped to editor panel, drag-and-drop scoped to drop zone
- Test cleanup: extracted `createTestCms()` helper, converted try/catch assertions to `.rejects.toMatchObject()`, added guard-path tests
- `TRANSITIONS` Sets now `Object.freeze`d to prevent mutation via `.add()`/`.delete()`
- DI-mode `_updateRef` now performs manual CAS check against `oldSha`
- Server tests assert setup call status codes to surface silent failures
- Vitest exclude glob `test/git-e2e*` → `test/git-e2e**` to cover future subdirectories

[Unreleased]: https://github.com/flyingrobots/git-cms/compare/main...git-stunts
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ npm run setup
### Try It Out

```bash
# Option 1: See a demo (recommended first time)
# Option 1: Guided walkthrough of key features
npm run demo

# Option 2: Interactive menu
# Option 2: Interactive menu (start server, run tests, open shell)
npm run quickstart

# Option 3: Just start the server
Expand Down
20 changes: 19 additions & 1 deletion bin/git-cms.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,24 @@ async function main() {
console.log(`Published: ${res.sha} (${res.ref})`);
break;
}
case 'unpublish': {
const [rawSlug] = args;
if (!rawSlug) throw new Error('Usage: git cms unpublish <slug>');
const slug = canonicalizeSlug(rawSlug);

const res = await cms.unpublishArticle({ slug });
console.log(`Unpublished: ${res.sha} (${res.ref})`);
break;
}
case 'revert': {
const [rawSlug] = args;
if (!rawSlug) throw new Error('Usage: git cms revert <slug>');
const slug = canonicalizeSlug(rawSlug);

const res = await cms.revertArticle({ slug });
console.log(`Reverted: ${res.sha} (${res.ref})`);
break;
}
case 'list': {
const items = await cms.listArticles();
if (items.length === 0) console.log('No articles found');
Expand All @@ -65,7 +83,7 @@ async function main() {
break;
}
default:
console.log('Usage: git cms <draft|publish|list|show|serve>');
console.log('Usage: git cms <draft|publish|unpublish|revert|list|show|serve>');
process.exit(1);
}
} catch (err) {
Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"test": "./test/run-docker.sh",
"test:setup": "./test/run-setup-tests.sh",
"test:local": "vitest run",
"test:e2e": "playwright test"
"test:e2e": "playwright test",
"test:git-e2e": "vitest run --config vitest.e2e.config.js test/git-e2e.test.js"
},
"author": "James Ross <james@flyingrobots.dev>",
"license": "Apache-2.0",
Expand All @@ -28,7 +29,7 @@
"@git-stunts/alfred": "^0.10.3",
"@git-stunts/docker-guard": "^0.1.0",
"@git-stunts/git-cas": "^3.0.0",
"@git-stunts/git-warp": "^10.4.2",
"@git-stunts/git-warp": "^10.8.0",
"@git-stunts/plumbing": "^2.8.0",
"@git-stunts/trailer-codec": "^2.1.1",
"@git-stunts/vault": "^1.0.0"
Expand Down
1 change: 1 addition & 0 deletions public/css/buttons.min.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading