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>
Apply same composite-FK pattern as 001/002 for migration 003:
- resources: add UNIQUE (id, space_id) as FK target.
- resource_dependencies: denormalize space_id, composite FKs on both endpoints
(enforces both ends of a dep live in the same space at the DB layer).
- resource_credentials: denormalize space_id, composite FK to resources.
- source_docs.resource_id: NOT NULL (tenancy anchor); resource_id was already
absent from the update FIELDS list so docs cannot move resources.
Repos derive space_id from the resource on addDependency/addCredential so callers
can't fake cross-tenant assignment. 3 regression tests added.
Three security-review findings on migration 002:
- pages.space_id and refs.space_id changed to NOT NULL + ON DELETE CASCADE
(was nullable + SET NULL, which allowed orphan rows surviving space deletion).
- pages.parent_id wrapped in composite FK (parent_id, space_id) -> pages(id, space_id)
to prevent cross-space parent linkage (same pattern as tasks.project_id in 001).
- idx_refs_external promoted to UNIQUE on (space_id, source_kind, external_id);
upsertByExternal now requires space_id and dedups per-space, not globally.
Added 3 regression tests covering composite FK rejection, CASCADE-on-space-delete,
and per-space dedup independence.
Security review flagged that tasks.project_id could reference a project in
a different space. Added composite FK (project_id, space_id) -> projects(id, space_id)
with ON DELETE SET NULL (project_id) so a deleted project leaves the task in
its space with project_id NULL rather than orphaning into a NULL space.
Added two regression tests: cross-space FK rejection + cascade behavior.