Skip to content

Commit 2ed01b4

Browse files
authored
Merge pull request #3 from flyingrobots/git-stunts
feat: content model, state machine, admin UI, and test infrastructure
2 parents dfed76f + d649fe0 commit 2ed01b4

18 files changed

Lines changed: 1604 additions & 262 deletions

CHANGELOG.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Changelog
2+
3+
All notable changes to git-cms are documented in this file.
4+
5+
## [Unreleased] — git-stunts branch
6+
7+
### Added
8+
9+
- **Content Identity Policy (M1.1):** Canonical slug validation with NFKC normalization, reserved word rejection, and `CmsValidationError` contract (`ContentIdentityPolicy.js`)
10+
- **State Machine (M1.2):** Explicit draft/published/unpublished/reverted states with enforced transition rules (`ContentStatePolicy.js`)
11+
- **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
12+
- **DI seam in CmsService:** Optional `graph` constructor param enables `InMemoryGraphAdapter` injection for zero-subprocess tests
13+
- **In-memory test adapter:** Unit tests run in ~11ms instead of hundreds of ms (no `git init`/subprocess forks)
14+
- **E2E test separation:** Real-git smoke tests in `test/git-e2e.test.js`, excluded from default `test:local` runs
15+
- **`test:git-e2e` script:** Run real-git integration tests independently
16+
- **`@git-stunts/alfred` dependency:** Resilience policy library (wired but not yet integrated)
17+
- **`@git-stunts/docker-guard` dependency:** Docker isolation helpers
18+
- **ROADMAP.md:** M0–M6 milestone plan with blocking graph
19+
- **Formal LaTeX ADR** (`docs/adr-tex-2/`)
20+
- **Onboarding scripts:** `setup.sh`, `demo.sh`, `quickstart.sh` with interactive menus
21+
- **Dependency integrity check:** `check-dependency-integrity.mjs` prevents `file:` path regressions
22+
23+
### Changed
24+
25+
- CmsService now uses `@git-stunts/git-warp` `GitGraphAdapter` and `@git-stunts/plumbing` `GitRepositoryService` instead of raw plumbing calls
26+
- All `repo.updateRef()` calls routed through `CmsService._updateRef()` for DI/production dual-path
27+
- `listArticles()` supports both plumbing (`for-each-ref`) and in-memory (`graph.listRefs`) paths
28+
- Server endpoints return structured `{ code, field }` errors for validation failures
29+
- Swapped all `file:` dependency paths to versioned npm ranges (PP3)
30+
31+
### Fixed
32+
33+
- Symlink traversal hardening in static file serving
34+
- Slug canonicalization enforced at all API ingress points
35+
- Admin UI API calls aligned with server contract (query params, response shapes)
36+
- Server integration test environment stabilized for CI
37+
- **(P1) Stored XSS via markdown preview:** Sanitize `marked.parse()` output with DOMPurify
38+
- **(P1) Unpublish atomicity:** Reorder `unpublishArticle` so draft ref updates before published ref deletion
39+
- **(P2) XSS via slug/badge rendering:** Use `textContent` and DOM APIs instead of `innerHTML` interpolation
40+
- **(P2) SRI hashes:** Add `integrity` + `crossorigin` to marked and DOMPurify CDN script tags
41+
- **(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
42+
- **(P2) uploadAsset DI guard:** Throw `unsupported_in_di_mode` when `cas`/`vault` are null
43+
- **(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`
44+
- **(P2) XSS in `escAttr`:** Escape single quotes (`'``'`) to prevent injection into single-quoted attributes
45+
- **(P2) Supply-chain hardening:** Vendor Open Props CSS files locally (`public/css/`) instead of `@import` from unpkg, eliminating CDN dependency and SRI gap
46+
- **(P2) Monkey-patch safety:** E2E test restores `plumbing.execute` in `finally` block
47+
- Unknown `draftStatus` in `resolveEffectiveState` now throws `unknown_status` instead of silently falling through to draft
48+
- Removed double-canonicalization in `_resolveArticleState`
49+
- Replaced sequential `readRef` loop with `Promise.all` in `listArticles` DI path
50+
- 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
51+
- Test cleanup: extracted `createTestCms()` helper, converted try/catch assertions to `.rejects.toMatchObject()`, added guard-path tests
52+
- `TRANSITIONS` Sets now `Object.freeze`d to prevent mutation via `.add()`/`.delete()`
53+
- DI-mode `_updateRef` now performs manual CAS check against `oldSha`
54+
- Server tests assert setup call status codes to surface silent failures
55+
- Vitest exclude glob `test/git-e2e*``test/git-e2e**` to cover future subdirectories
56+
57+
[Unreleased]: https://github.com/flyingrobots/git-cms/compare/main...git-stunts

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,10 @@ npm run setup
2424
### Try It Out
2525

2626
```bash
27-
# Option 1: See a demo (recommended first time)
27+
# Option 1: Guided walkthrough of key features
2828
npm run demo
2929

30-
# Option 2: Interactive menu
30+
# Option 2: Interactive menu (start server, run tests, open shell)
3131
npm run quickstart
3232

3333
# Option 3: Just start the server

bin/git-cms.js

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,24 @@ async function main() {
3939
console.log(`Published: ${res.sha} (${res.ref})`);
4040
break;
4141
}
42+
case 'unpublish': {
43+
const [rawSlug] = args;
44+
if (!rawSlug) throw new Error('Usage: git cms unpublish <slug>');
45+
const slug = canonicalizeSlug(rawSlug);
46+
47+
const res = await cms.unpublishArticle({ slug });
48+
console.log(`Unpublished: ${res.sha} (${res.ref})`);
49+
break;
50+
}
51+
case 'revert': {
52+
const [rawSlug] = args;
53+
if (!rawSlug) throw new Error('Usage: git cms revert <slug>');
54+
const slug = canonicalizeSlug(rawSlug);
55+
56+
const res = await cms.revertArticle({ slug });
57+
console.log(`Reverted: ${res.sha} (${res.ref})`);
58+
break;
59+
}
4260
case 'list': {
4361
const items = await cms.listArticles();
4462
if (items.length === 0) console.log('No articles found');
@@ -65,7 +83,7 @@ async function main() {
6583
break;
6684
}
6785
default:
68-
console.log('Usage: git cms <draft|publish|list|show|serve>');
86+
console.log('Usage: git cms <draft|publish|unpublish|revert|list|show|serve>');
6987
process.exit(1);
7088
}
7189
} catch (err) {

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
"test": "./test/run-docker.sh",
1717
"test:setup": "./test/run-setup-tests.sh",
1818
"test:local": "vitest run",
19-
"test:e2e": "playwright test"
19+
"test:e2e": "playwright test",
20+
"test:git-e2e": "vitest run --config vitest.e2e.config.js test/git-e2e.test.js"
2021
},
2122
"author": "James Ross <james@flyingrobots.dev>",
2223
"license": "Apache-2.0",
@@ -28,7 +29,7 @@
2829
"@git-stunts/alfred": "^0.10.3",
2930
"@git-stunts/docker-guard": "^0.1.0",
3031
"@git-stunts/git-cas": "^3.0.0",
31-
"@git-stunts/git-warp": "^10.4.2",
32+
"@git-stunts/git-warp": "^10.8.0",
3233
"@git-stunts/plumbing": "^2.8.0",
3334
"@git-stunts/trailer-codec": "^2.1.1",
3435
"@git-stunts/vault": "^1.0.0"

public/css/buttons.min.css

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)