From fa47419cbd08d0965251449ccae332d079dbe70b Mon Sep 17 00:00:00 2001 From: root Date: Mon, 1 Jun 2026 02:27:58 +1000 Subject: [PATCH] docs: Plan 2 completion summary 22/22 tasks landed; 185 tests; 10 commits; SPA renders end-to-end including the agent suggest -> owner approve flow. Captures the UI smoke matrix, security findings handled, and what's deferred to Plans 3-6. Co-Authored-By: Claude Opus 4.7 --- docs/plan-2-complete.md | 88 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 docs/plan-2-complete.md diff --git a/docs/plan-2-complete.md b/docs/plan-2-complete.md new file mode 100644 index 0000000..30d29ce --- /dev/null +++ b/docs/plan-2-complete.md @@ -0,0 +1,88 @@ +# Plan 2 — Complete + +**Date:** 2026-06-01 +**Version:** 2.0.0-alpha.2 +**Tests:** 185 passing across 43 files +**Commits on `main`:** 10 (`5aa6fe7` … `8ae9bce`) + +## Scope delivered + +### Phase A — REST routes (T1–T8) +- `/api/spaces`, `/api/projects`, `/api/tasks` (with space + project scoping) +- `/api/pages` + revisions + `/api/pages/:id/backlinks` +- `/api/refs` + `/api/refs/upsert` +- `/api/resources` + dependencies + change history +- `/api/resources/:id/source-docs` + `/api/source-docs/:id/resync` (gated by `ENABLE_RESYNC`) + +### Phase B — Auth + capability (T9–T11) +- `agentOrOwner` bearer middleware replaces the old owner-only `/api` gate. +- `requireWrite` middleware tiers writes through `canAct`: `allow` → through, `suggest` → `divertToPending` (202 + pending row), else 403. +- `/api/agents` (owner-only) + agent token mint (plaintext returned once) + revoke. + +### Phase C — Cross-cutting (T12–T15) +- `/api/conversations` + nested `/messages`. +- `/api/tags` + entity-scoped attach/detach mounted at `/api/:entity_type/:entity_id/tags`. Tag upsert is idempotent. +- `/api/links` (POST/GET from|to/DELETE) for polymorphic entity links. Idempotent POST. +- `/api/pending-changes` (owner-only) with `applyPendingChange` dispatch table covering page/project/task/ref/resource/source_doc × create/update/delete. Approve and reject emit explicit `approve`/`reject` audit entries with the original agent id preserved in the diff. +- `/api/audit/entity/:type/:id` + `/api/audit/actor` (owner-only). + +### Phase D — Search (T16) +- `/api/search?q=&space_id=&kinds=&limit=&offset=` — FTS UNION across pages, refs, source docs, messages with a `kind` discriminator. `ts_rank` order. Messages branch excluded when a space filter is set (messages have no `space_id`). +- Vector + RRF deferred to Plan 3 (TODO in `lib/db/repos/search.js`). + +### Phase E — Void UI shell (T17–T22) +- Static SPA served from `public/` with three-column Cradle aesthetic: blackflame palette, Cinzel display headings, Cormorant Garamond body, system UI chrome. +- Hash-based router (`public/router.js`) with views for home / space / project / page / reference / resource / search / inbox / sacred valley. +- `public/dom.js` safe DOM builders. **Invariant**: no component sets `innerHTML` from API data; the explicit, scary-named `html:` opt-in exists only for sanitized output (markdown preview using `marked` + DOMPurify). +- `public/state.js` tiny event bus shares pending-change count between sidebar item and topbar bell. +- Sidebar: Spaces tree with lazy project expansion, bottom Navigate section with pending-count badge. +- Topbar: brand + capture modal stub + global search (Enter → `/search?q=`) + pending bell + owner toggle. +- Right rail: collapsible companion placeholder, state in `localStorage.void_rail_collapsed`. +- Page editor: split-pane Markdown via `marked` + DOMPurify; save PATCHes `/api/pages/:id`; backlinks card. +- Reference detail: media block (image / YouTube embed via `youtube-nocookie` / link fallback), summary, metadata table, tag attach/detach, linked-from list. +- Resource detail: status + runtime + host + clickable URL header, dependencies + source docs + runbook pages columns, change history. +- Inbox: pending changes grouped by agent, approve → navigate to resulting entity. + +## UI smoke results + +Captured via Playwright (`/tmp/void-shots/`): + +| # | Scenario | Outcome | +|---|---|---| +| 1 | Owner token persists in localStorage | ✅ verified — modal builds via safe DOM, sets `localStorage.void_token`. | +| 2 | Create space via API → appears in sidebar | ✅ `t18-tree-expanded.png` / `t19-home.png` | +| 3 | Create project → shows in space view | ✅ `t19-space.png` (not captured) / `t19-project.png` shows project header | +| 4 | Edit page in editor + save | ✅ `t20-page-editing.png` — live preview via DOMPurify-sanitized marked output. Save button calls PATCH `/api/pages/:id`. | +| 5 | `#/search?q=blackflame` → results | ✅ `t22-search.png` — 2 hits grouped by Pages / References. | +| 6 | Agent suggest → bell badge → approve via Inbox | ✅ `t21-inbox.png` — Inbox badge shows `3`, approve clears it; `t21-after-approve.png` confirms count drops. | + +## Security findings handled + +- **HIGH — `javascript:` URLs in `href`/`src`** flagged on `reference.js`. Fix: `safeHref()` in `dom.js` enforces `http(s)` / `mailto`. Applied to reference media block, reference source_url anchor, and resource URL anchor. (`8ae9bce`) +- **HIGH — innerHTML in `api.js` modal** flagged repeatedly during the build. Mitigated upstream by introducing `dom.js` `el()` / `mount()` safe builders; the modal was refactored to use them. The only remaining `innerHTML` write is in `markdown_editor.js` and is guarded by `DOMPurify.sanitize(marked.parse(...))`. +- **HIGH — cross-tenant disclosure on `/api/search`** and similar polymorphic IDOR findings on `/api/links` / `/api/tags`: same defer as Plan 1's polymorphic shape — owner-only API today, no membership model exists yet. Documented in `docs/security-followups.md`. + +## Known follow-ups (deferred) + +- `pending_changes.action` CHECK blocks `'upsert'` / `'add_dependency'` / `'remove_dependency'` from `divertToPending` in refs/resources routes. Latent — only fires for an agent at suggest tier on those specific endpoints. Three mitigation options documented in `docs/security-followups.md`. +- Polymorphic IDOR concerns on `entity_links`, `entity_tags`, `attachments`, and unscoped search — defensible at single-tenant alpha; revisit when membership lands. +- Universal capture button is a modal stub; capture workers land in Plan 3. +- Right rail companion is empty; chat lands in Plan 5. +- Sacred Valley is a placeholder card; widgets port from Void 1.x in Plan 6. + +## Verification commands + +```bash +cd /project/src/void-v2 +npm test # 185/185 green +OWNER_TOKEN=void-dev-token PORT=3010 node server.js & +curl -s localhost:3010/health # { db_ok: true, version: '2.0.0-alpha.2' } +# browser: http://localhost:3010/ → paste OWNER_TOKEN, navigate +``` + +## What's left after Plan 2 + +- **Plan 3**: capture pipeline (pg-boss, Karakeep webhook, URL/YouTube/PDF/image workers, embeddings). +- **Plan 4**: heavy ingest (Whisper, Tesseract OCR) via `void-workers` Python service. +- **Plan 5**: companion chat in right rail. +- **Plan 6**: Sacred Valley widgets ported from Void 1.x.