cb(null, address, family) was returning Invalid IP address: undefined
under undici v6. Returning the full records array (each {address, family})
gives undici what it expects and lets it pick the best family.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Drops into #main POST /api/capture/upload one file at a time, with
space_id pre-filled from localStorage.last_space_id (set whenever the
space view renders).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
POST /api/ingest/karakeep accepts Karakeep webhook payloads. HMAC
signature on the raw body captured by express.json's verify hook.
Mounted on app before mountApi so it bypasses agentOrOwner — the
shared secret IS the auth.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces FTS-only /api/search in place. RRF (k=60) fuses ts_rank and
pgvector cosine distance rankings. Vector branch silently skipped when
Ollama times out / errors, keeping search snappy and resilient.
Messages have no embeddings in Plan 3, so they participate in the FTS
branch only.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces the validate-then-call-fetch pattern (which left a TOCTOU
window where the OS resolver could return a different IP at connect
time) with an undici Agent dispatcher whose lookup() returns the IP we
already validated. Same hardening on every redirect hop.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
create/update on embeddable repos enqueue embed.text with a singleton
key that coalesces rapid edits. No-op when the queue is not running
(server tests construct createApp without booting pg-boss).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Pads nomic-embed-text's 768 dims to 1024 zeros so a later 1024-dim model
swap is a re-embed, not a migration (per master spec).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
safe_fetch.js validates URLs before fetch: rejects non-http(s), literal
or DNS-resolved loopback / RFC1918 / link-local / CGNAT / metadata
addresses; follows redirects manually with the same checks on each hop.
Test fixtures gate the check with VOID_INGEST_ALLOW_PRIVATE for offline
fixtures that hit 127.0.0.1.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Unifies pgboss.job (current, per-queue partitioned) and pgboss.archive
under one SELECT for operator views. retry promotes archived rows back
into the active partition.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Job queue starts only in the CLI gate (not inside createApp), so tests
manage their own queue lifecycle. waitForJob() takes a (name, id) pair
to match pg-boss v10's getJobById signature.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Per-name ensureQueue promise dedup so concurrent enqueue+subscribe
on the same queue do not race createQueue (Postgres deadlock).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Search view: read ?q from hash, call /api/search, group hits by kind
with rank + space_id; sidebar filters for kinds and space_id; updates
on Enter or filter change.
Bumps package.json + server.js VERSION to 2.0.0-alpha.2 and pins the
/health version assertion to match.
CHANGELOG: full Plan 2 entry covering API surface, capability tiering,
audit chain extension (approve/reject events), and the SPA shell.
Security: adds safeHref() to dom.js and applies it everywhere an
API-supplied URL becomes href / src (reference media block + reference
source_url anchor + resource url anchor). javascript: and other
non-http(s)/mailto schemes from agent-suggested content can no longer
execute in the owner's browser.
Plan 2 surface is feature-complete: 22/22 tasks landed, 185 tests
across 43 files, SPA renders end-to-end including the suggest -> approve
agent flow.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Resource: status header (status + runtime_type + host + clickable URL),
three-column row of Dependencies (with add-by-UUID form) / Source docs /
Runbook pages (filters /api/links/to/resource/:id for from_type=page
and relation=runbook, then fetches each page title). Change history
card pulls /api/resources/:id/changes.
Inbox: groups pending changes by agent. Each row shows entity type
badge + action + reason, with a collapsible payload disclosure.
Approve calls /api/pending-changes/:id/approve and, if the response
carries entity_id, navigates to the resulting detail view. Reject just
re-fetches. Both update the shared pending-count emitted to state.js
so the sidebar and topbar badges drop immediately.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Page view: header + split-pane markdown editor (textarea on left,
marked + DOMPurify rendered preview on right) + backlinks card pulling
/api/pages/:id/backlinks. Save calls PATCH /api/pages/:id with body_md
and surfaces the resulting updated_at as a timestamp.
Reference detail: media block (image preview / YouTube embed via
youtube-nocookie / link fallback), summary card, metadata table, tags
card with attach/detach (creates the tag idempotently then attaches),
linked-from card from /api/links/to/ref/:id.
marked + DOMPurify vendored to public/vendor as ESM. The markdown
editor uses the explicit html: opt-in on dom.js's preview element
only — all other text comes from textContent.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Home: recent activity feed from /api/audit/actor?limit=20 with relative
timestamps and entity-typed links into detail views.
Space: header + three-column row of Projects / Open tasks (status=todo) /
Recent pages + refs cards. Status badges on projects and tasks use the
shared .status palette.
Project: header (status + start/complete dates), Tasks card with inline
status badges that cycle todo->doing->blocked->done on click (PATCH
/api/tasks/:id), Pages in space card, Add-task inline form bound to
project_id.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sidebar: Spaces tree with lazy-expand to projects on caret click; bottom
Navigate section with Sacred Valley / Search / Inbox + placeholders for
Agents and Resources greyed out as later. Inbox item carries a
pending-count badge that wires to state.js so the topbar bell and the
sidebar share one poll.
Topbar: brand, + Capture button (modal stub for Plan 3 capture queue),
global search input (Enter -> /search?q=), pending Inbox bell with
matching badge, Owner toggle (stub for agent-switching post-Plan-2).
Rightrail remains the T17 collapsible companion placeholder.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three-column grid (sidebar / main / right rail) with Cradle aesthetic:
blackflame accent on Cinzel display headings + Cormorant Garamond body
in cards, system UI for chrome. Hash-based router covers all entity
routes plus search, inbox, sacred-valley. api.js stores OWNER_TOKEN in
localStorage and prompts via a modal on 401. dom.js provides safe el()
+ mount() builders so no component ever assigns innerHTML from API data
(the only exception is an explicit, scary-named html: opt-in for
sanitizer output, used later by the markdown editor).
state.js is a tiny event bus for shared chrome state (pending count).
Components and views are loaded as ES modules — sidebar / topbar /
rightrail + 9 view stubs that the later Phase E tasks fill in.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Single GET /api/search?q=&space_id=&kinds=&limit=&offset= unions FTS
hits across pages / refs / source_docs / messages with a `kind`
discriminator and ts_rank ordering. Each branch's to_tsvector matches
the GIN index expression on its source table so indexes are used.
Messages have no space_id and are excluded when a space filter is set.
Hybrid vector / RRF lands in Plan 3.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Owner-only routes wired with an applyPendingChange dispatch helper
covering page/project/task/ref/resource/source_doc create/update/delete.
Approve and reject emit their own audit_log entries (actions already in
the CHECK vocab) so the audit trail is self-contained.
Documents a latent bug in security-followups.md: pending_changes.action
CHECK constraint blocks 'upsert' / 'add_dependency' / 'remove_dependency'
divertToPending paths in refs/resources routes when an agent at suggest
tier hits them.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add lib/api/routes/tags.js: list + upsert at /api/tags, and an
entity-scoped router mounted at /api/:entity_type/:entity_id/tags
for attach (idempotent), list, and detach. entity_type is bounded by
a zod enum covering space/project/task/page/ref/resource/source_doc/
conversation.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add conversations CRUD-lite (list, create, get, PATCH status, PATCH
summary which flips status to summarized) and conversation-scoped
messages (append, list ordered by created_at).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add lib/api/routes/agents.js: list/create/get, PATCH capabilities,
mint token (plaintext returned exactly once, then bcrypt-hashed),
revoke token. All endpoints gated by requireOwner so an agent token
can never bootstrap a new agent or grant itself capabilities.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add lib/api/cap.js: requireWrite(entity_type) maps HTTP method to
action, runs canAct, and tags req.capTier as allow|suggest|deny→403.
Mutating routes (pages, projects, tasks, refs, resources, source_docs)
now check req.capTier and either run the repo (allow) or divert to
pending_changes returning 202 (suggest). Owner and worker actors stay
on the allow path. requireOwner helper added for Task 11.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add lib/api/middleware/agent_auth.js: agentOrOwner accepts the owner
token (kind=user actor) or a hashed agent token (kind=agent actor
carrying capabilities + scopes). /api router now mounts this in place
of ownerOnly so agent tokens become first-class.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add lib/api/routes/source_docs.js: scoped POST under a resource,
get/patch/delete by id, and POST /:id/resync as a Plan 3 stub gated
by ENABLE_RESYNC (503 by default, 202 once workers ship). upstream_url
is required by the DB so the zod schema enforces it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add lib/api/routes/resources.js: CRUD scoped to space; dependency
add/list/remove (cross-space attempts mapped to 409 conflict via the
composite FK); source-docs index per resource; change history via
audit.listForEntity.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add lib/api/routes/refs.js: list with space_id/kind filters and
paginated, create, get/patch/delete by id, and /upsert that maps to
repo.upsertByExternal for idempotent capture-source ingest.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add lib/api/routes/pages.js: list by space, create/get/patch/delete,
get-by-slug, list revisions, and backlinks via entity_links.listTo
enriched with the source entity's title (whitelisted entity_type set
to keep the dynamic-table SELECT bounded).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add lib/api/routes/tasks.js: list by space (status filter), list by
project (position then created_at), create scoped to space with
optional project_id, get/patch/delete by id. status=done flips
completed_at via the repo's existing trigger logic.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Catch-all error handlers in lib/api/errors.js and server.js were
echoing raw err.message to clients. Replace with a fixed generic
message; the full error continues to be logged server-side via pino.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add lib/api/routes/projects.js: list by space (with status filter),
create scoped to space, get/patch/delete by id. FK violation from
unknown space_id maps to 400 invalid_space.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add lib/api/routes/spaces.js: list, create, get-by-id, get-by-slug,
patch, delete. Mounted under /api/spaces. Server smoke restored.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add lib/api/{errors,validate,pagination,index}.js: typed ApiError
subclasses, errorMiddleware, zod-backed validate(), parsePagination
with caps, and a mountApi() that owns /api routing + 404 + error tail.
server.js delegates /api to mountApi and drops the inline /api/spaces
smoke (returns in Task 2).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>