Skip to content

docs(api): OpenAPI, guide, upgrade steps and security for the Task API - #76

Merged
HecFranco merged 30 commits into
mainfrom
docs/213-s8-docs
Nov 8, 2026
Merged

HecFranco merged 30 commits into
mainfrom
docs/213-s8-docs

Conversation

@HecFranco

Copy link
Copy Markdown
Contributor

Summary

Ninth and last slice of spec 213: docs (FR-018). Stacked on #75; the base is main because CI runs only on PRs to it. Audit status re-verification pending.

  • docs/openapi.yaml: OpenAPI 3.1 for all 42 routes (paths, methods, parameters, bodies, responses, the error shape and every ECODE). ApiOpenApiTest fails if a route is missing from it or it documents one that does not exist, and if a $ref does not resolve.
  • docs/API.md rewritten: turning it on, the hooks, the firewall, access, read, write and management endpoints, idempotency, every ECODE, differences from ClickUp (the contract test's table, plus board deletes in slices, time without a task, expiring tokens, column paths), differences from the UI, and material for a host's manual (levels, where, examples, refusals, coming from ClickUp) for Beacon's English and Spanish manual.
  • docs/UPGRADING.md To 2.2.0: nothing changes for a host that changes nothing; the deprecated $syncLimits; ten steps to turn the API on, including the task_board_api_idempotency schema, hooks, firewall, listeners, access, tenancy, purge and forget(), idempotent delete listeners, time.
  • docs/SECURITY.md: threats and mitigations of the Task API; known and documented limits.
  • docs/CHANGELOG.md [Unreleased] complete for 2.2.0.

Gates run locally

composer test-coverage-100 (100%), PHPStan, cs-check.

Not for merge until the coordinator's audit step.

…pec 213 S0)

Pins what the UI answers before the rules behind it move in the next slice:
status, Location, flashes, log lines, events and history for the comment and
restore routes, and the constraints of the six write forms. No src change.

With TASK_BOARD_CHARACTERIZE_DIR set the tests also write the pages with ids and
dates masked, so two checkouts on one vendor can be compared byte by byte.
… 213 S1)

A write path other than the UI's forms must apply the same rules, so they leave
the controller and the form types: TaskCommentPolicy, TaskManager::restore() and
Validation\TaskConstraints. tests/Functional/Characterization from the previous
commit is unchanged and passes, and the rendered pages are byte-identical.

Adds what the UI does not have and the API will need, with no caller yet:
TaskBoardEraser::eraseTask() (leaf-first, so PreRemove keeps every minute; refuses
a task that follows a provider), TaskBoardEditor, TaskManager::archive(),
TaskDependencyManager::findBetween(), TaskFormData::fromTask().
Off by default: no route, service, listener, parameter or table for a host that
changes nothing. On: stateless routes under api.prefix, GET /v2/user and /v2/team,
and what every later endpoint goes through: the request pipeline, TaskApiGuard
(level, the person's access and where, per request), the capability table and one
{err, ECODE} shape.

The bundle stores no tokens. The hooks live in Api\Hook, the authenticator is
required and every other default denies. It fails closed when the security user is
not the token's user, as access checkers read the security token, and the build
refuses a configuration that would open every board to every token.

Audit status: re-verification pending.
The HTML coverage report ran out of 512M with the API tests: main already peaks
near 440M before it. The PHPUnit ceiling goes to 1G, and the API tests compile
fewer kernels: one host configuration for the pipeline and the guard, and the
"off equals unset" check moves to the container builder.
Spaces, folders (none), lists, tasks with ClickUp's filters and 100 per page,
a task, comments, members, tags, imported custom fields and time entries.
Every endpoint goes through the guard; lists hold and count only what the token
may read. Each list costs the same queries at 25 and 100 tasks (9 for a list's
tasks). Read access matrix: four levels by own task, another's, another
project, a board outside where, a removed member, a revoked token and the plan.
…d-moves

# Conflicts:
#	docs/CHANGELOG.md
#	docs/UPGRADING.md
…audit IA)

IA-M1: eraseTask() handed the host no stored-file refs. After an unlink the
sync's attachment rows outlive the link (GWd-M1), so the files were never named
again; on SQLite the rows stayed orphaned. The refs of the subtree are read
first, passed per task in TaskErasedEvent, and the rows removed. Proof test
ported from the auditor; run on SQLite and PostgreSQL 16.

IA-I1: TaskBoardManageController keeps $syncLimits in its place, deprecated
and ignored, so positional arguments do not move in a minor release.

Also takes main (2.1.1) and the 1G PHPUnit ceiling the coverage report needs.
…t IA)

IA-L1: a valid token whose user the access checker refuses keeps its 401 and
now counts as a failed token, so it cannot call the authenticator unthrottled.

IA-L2: with tenancy the guard asks the bundle's tenant resolver for the board's
tenant, as the UI does through owns(), so a permissive workspace hook opens no
other tenant; a board without a tenant is not served under tenancy.

IA-I4: where's board ids grant on their own, documented as intended.
IA-I2, I3, I5: notes in SECURITY and API docs, no code change.
Takes main (2.1.1), IA-M1, IA-I1, IA-L1, IA-L2 and IA-I4. The tenant check of
IA-L2 also applies to spaces and to the boards a space lists.
IA-M2: a task's subtasks are those the token may open; for My tasks the checklist
items of its own tasks stay, whoever created them (owner's decision).
IA-I7: dependencies and links leave out tasks the token may not open.
IA-L3: the tag filter runs in SQL (NOWO_TB_JSON_TEXT, with api.enabled only),
so pages and last_page cost what any filter costs; budget cases added.
IA-I8: the boards' columns load with the tasks, so a team's tasks cost the same
queries with 1 board or 10 (9); the per-board access cost is documented.
IA-I9: documented that time entries without task_id rely on the host hook.

Tag filter tested on SQLite, PostgreSQL 16 and MySQL 8.4.
…13 S4)

Tasks (create, update, delete with tracked time kept), comments, tags,
dependencies, links and checklists as tagged subtasks, through the services the
UI uses. Read writes nothing; My tasks works only on its own tasks' status, dates,
description, comments and checklists; custom fields answer 409 FIELD_001; a task
that follows a provider answers 409 TASK_002. Writes need an API_WRITE listener
(503 AUDIT_001) and creates a QUOTA_CHECK listener (403 PLAN_002).

A status change moves the task through moveToColumn(), so completion, the
reopen limit and their events follow; TaskManager::update() never moves it.

Idempotency-Key on every write: per token, 24 hours, same request replayed,
another one 409, refusals not kept, a replay checked again. The bundle's store is
a DBAL table mapped only with api.enabled; a host may replace it.

Write access matrix: 18 write endpoints by four levels, own and others' tasks,
another project, a board outside where, a removed member, a reader, the plan,
a revoked token. API tests also run on PostgreSQL 16 and MySQL 8.4.
An API comment passed no mention ids, so TASK_MENTION and the host's mention
notifications never ran, while a UI comment that names someone fires them
(FR-011). Each candidate written @Label in the text is now mentioned; the
comment manager checks the text again, as for the UI.

docs/API.md: comments' mentions, partial writes not being one transaction, and
the column-less task limitation.
POST /v2/task/{id}/attachment goes to the host's TaskApiAttachmentsInterface,
which holds every upload check (FR-014); an upload's bytes are part of its
idempotency fingerprint. POST /v2/team/{id}/time_entries logs the token's own
person's time through TaskApiTimeEntriesInterface; DELETE of a time entry is
always 403 TIME_002 (owner). ManualTaskApiTimeEntries, selectable as the time
store, uses the bundle's manual entries through TaskTimeLogger.
…(audit IA)

IA-L5: the fingerprint includes the sorted query string, so one key on
?depends_on=B and ?dependency_of=C is 409 IDEMP_002, not a false replay.
IA-L6: an update asks the host's active-task limit once, for a restore and a
reopening together, before anything is saved (TaskManager::activeTaskRefusal()).
Tags: PUT warns about tags instead of ignoring them; create and the tag endpoint
apply the task form's rules (1 to 64 characters, at most 20). Endpoints that read
no body warn about one. docs/API.md: what a replay re-checks, checklist item
rights, forget()'s reach.
An upload and a time log name their board for a replay's reach check; a time
log without a task touches no board, so its replay needs only the token. Form
fields beside an upload's file are warned about. docs/API.md and CHANGELOG for
files and time.
Full management through the UI's services: create a board in a space the token
reaches as a whole (create check and board plan limit), rename it, delete it
(tasks erased with tracked time kept, in slices answered 202; a provider-linked
board is 409 LIST_002), and columns as statuses on four paths of this API's own
(owner, Q-O2). Every write needs the board's manage decision. Statuses are
answered by position, also right after a reorder.
…13 S7, wip)

Shapes (field names and types, never values) derived from the response examples in
ClickUp's API v2 reference pages; not live recordings. Small conformity fixes:
link endpoints answer {task}, comment and attachment dates are numbers, checklist
orderindex a number, people have initials, spaces carry ClickUp's fields.
…audit IA-L7)

The same bytes resent under another file name or MIME type with one key are now
409 IDEMP_002, instead of being replayed under the first name.
…(audit IA)

Also takes S5's upload fingerprint fix.
…ec 213 S7)

ApiClickUpContractTest sends each request a ClickUp reference page describes and
compares the answer's shape with the shape of that page's published example
(tests/Fixtures/clickup/shapes.json: names and types, no values, not live
recordings). 40 documented differences, each with its reason, also listed in
docs/API.md; an undocumented or stale one fails. List items are merged into one
shape so the order of tasks cannot change the result.
…k API (spec 213 S8)

docs/openapi.yaml (OpenAPI 3.1) for every route, kept in step by ApiOpenApiTest.
docs/API.md: turning it on, hooks, firewall, access, endpoints, idempotency, every
ECODE, the differences from ClickUp (contract-test table and others, including
board deletes in slices and time without a task), and material for a host's
manual. UPGRADING 'To 2.2.0' with the schema and every host step. SECURITY:
threats and mitigations; audit status re-verification pending.
…audit IA-L8)

prepend() mapped sync, task_numbers and api tables when any config file set
enabled: true, even if a later override set false. It now takes the last value
set, as the merge does, and so does the idempotency_store check. Docs warn
hosts never to map an empty token scope to TaskApiWhere::everything().
Audit IA (2026-11-08): Pass-Low.
…umbers

Last-value-wins now applies only to the API table, NOWO_TB_JSON_TEXT and the
idempotency_store check. sync and task_numbers keep mapping their tables when
any config file enables them, so a layered config ending at false never makes
migrations drop tables holding data; deprecated, changes in 3.0.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant