diff --git a/CHANGELOG.md b/CHANGELOG.md index b3118d2..23998d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ All notable changes to git-cms are documented in this file. ### Added +- **Version History Browser (CE3):** Browse prior versions of an article, preview old content, and restore a selected version as a new draft commit + - `CmsService.getArticleHistory()` — walk parent chain to list version summaries (SHA, title, status, author, date) + - `CmsService.readVersion()` — read full content of a specific commit by SHA + - `CmsService.restoreVersion()` — restore historical content as a new draft with ancestry validation and provenance trailers (`restoredFromSha`, `restoredAt`) + - `GET /api/cms/history`, `GET /api/cms/show-version`, `POST /api/cms/restore` server endpoints + - Admin UI: collapsible history panel with lazy-fetch, version preview, and restore button + - **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`) - **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 @@ -40,6 +47,9 @@ All notable changes to git-cms are documented in this file. - **(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 +- **(P1) Path traversal in upload handler:** Sanitize user-controlled `filename` to `path.basename()` preventing writes outside tmpDir +- **(P1) readVersion lineage scoping:** `readVersion` now validates SHA ancestry (prevents cross-article content leakage) +- **(P1) readVersion published fallback:** `readVersion` checks both draft and published refs (consistent with `getArticleHistory`) - **(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 @@ -53,5 +63,14 @@ All notable changes to git-cms are documented in this file. - 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 +- Admin UI: reset history panel state (versions list, preview, selection) when creating a new article to prevent stale data +- Defensive `|| {}` guard on `decoded.trailers` destructuring in `unpublishArticle` and `revertArticle` (prevents TypeError if trailers is undefined) +- `readVersion` now returns `trailers: decoded.trailers || {}` ensuring callers always receive an object +- Upload handler: moved tmpDir cleanup to `finally` block preventing temp directory leaks on failure +- **(P1) sendError info leak:** 500 responses now return generic 'Internal server error' instead of raw `err.message` (prevents leaking file paths, git subprocess details, or internal state) +- **(P2) readBody O(n²):** `readBody` now accumulates chunks in an array and uses `Buffer.concat` instead of repeated string concatenation +- Admin UI: `loadArticle` unconditionally resets `historyVersions` and `selectedVersion` to prevent stale history state when switching articles with the panel closed +- Admin UI: `selectVersion` guards against out-of-order async responses (prevents stale preview flash from rapid clicks) +- **(P2) walkLimit divergence:** Extracted `HISTORY_WALK_LIMIT` as a shared exported constant used by both `_validateAncestry` and the server's history limit clamp [Unreleased]: https://github.com/flyingrobots/git-cms/compare/main...git-stunts diff --git a/package.json b/package.json index e3d02f5..c65371d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "git-cms", - "version": "1.0.0", + "version": "1.0.2", "description": "A serverless, database-free CMS built on Git plumbing.", "type": "module", "bin": { diff --git a/public/index.html b/public/index.html index 6cd5884..b4371da 100644 --- a/public/index.html +++ b/public/index.html @@ -359,6 +359,50 @@ margin-top: var(--size-2); } + /* ── History Section ── */ + #historySection .history-list { + max-height: 240px; + overflow-y: auto; + margin-top: var(--size-2); + display: flex; + flex-direction: column; + gap: 2px; + } + .history-item { + display: flex; + align-items: center; + gap: var(--size-3); + padding: var(--size-2) var(--size-3); + border-radius: var(--radius-2); + cursor: pointer; + transition: background 0.15s; + font-size: var(--font-size-0); + } + .history-item:hover { background: var(--surface-3); } + .history-item.active { background: var(--brand); color: white; } + .history-item .hist-sha { font-family: var(--font-mono); font-size: var(--font-size-00); opacity: 0.7; } + .history-item .hist-title { flex: 1; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .history-item .hist-date { font-size: var(--font-size-00); opacity: 0.7; white-space: nowrap; } + .history-item .hist-status { font-size: var(--font-size-00); opacity: 0.7; } + + #historyPreview { + margin-top: var(--size-3); + border: 1px solid var(--surface-3); + border-radius: var(--radius-2); + padding: var(--size-3); + display: none; + } + #historyPreview .preview-content { + max-height: 300px; + overflow-y: auto; + line-height: 1.7; + } + #historyPreview .preview-actions { + margin-top: var(--size-3); + display: flex; + gap: var(--size-2); + } + /* ── Status Bar ── */ .status-bar { font-size: var(--font-size-0); @@ -457,6 +501,17 @@

Git CMS

+
+ Version History +
+
+
+
+ +
+
+
+
Drop files here or