auth configs / projects
Stack: Batch Revoke — Apollo side · PR 3 of 4
Towards DASH-814
We're adding batch-revoke support to the platform: the ability to revoke every connected account in a scope (org / project / auth config / connected account) asynchronously, plus opt-in revoke-on-delete on the public DELETE endpoints. This PR stack is the Apollo side over the Thermos workflow machinery (separate stack).
Apollo-side stack (a DAG — merge bottom-up):
apollo-jobs-table — jobs ownership table + oj_/pj_ ids (base: master)apollo-lib — shared, auth-agnostic revoke-job lib (base: 1)
apollo-endpoints — dashboard + platform job endpoints ← this PR (base: 2)apollo-delete-cascade — opt-in revoke-on-delete on DELETE handlers (base: 2)Base:
apollo-lib— review/merge PRs 1 and 2 first. This PR is a sibling of PR 4 (both build on the shared lib).
There is no way to revoke every connection in a scope — a whole org, a single project, or one auth config — in a single action; callers must revoke connections one at a time. This exposes the async revoke-job primitive so a caller can start a scoped revoke, poll the per-connection outcome once it completes, and retry the failures.
Two thin endpoint surfaces over the shared, auth-agnostic revoke-job lib (src/lib/jobs/revoke),
differing only in auth and scope resolution:
POST/GET/POST /api/internal-dashboard/jobs/revoke[/{job_id}][/retry]
on the dashboard auth router. Scope comes from the x-org-id / x-project-id headers; one
router decides authz in-handler by the job's class, so poll and retry are single routes./api/v3.1/jobs/{org,project}/revoke[/{job_id}][/retry] on the API-key
routers (org key vs project key), split by class. x-internal visibility — live and callable,
but hidden from every public OpenAPI preset.Shared helpers (pages/api/v3.1/jobs/_shared.ts) own the wire schemas + shaping, the
org-owner/admin authority gate, and the ownership-scoped job lookup; the lib never sees auth,
which is what makes the second surface nearly free. A job whose stored job_class differs from
the route's class returns 404 (cross-namespace guard). Platform org create requires the body
org_id to equal the key's org as a whole-org-wipe confirmation. Poll reuses the infinite-scroll
{ items, next_cursor } envelope and forwards the opaque keyset cursor verbatim. v3.1/jobs/* is
added to the proxy.ts passthrough and registered (x-internal) in the v3.1 OpenAPI aggregation.
Automated: apps/apollo/src/pages/api/v3.1/jobs/_shared.unit.test.ts — the shared shaping
helpers: jobClassFromPublicId (prefix → class), toRevokeJobResponse (create/retry view →
snake_case wire body), and toPollResponse (identity-only while in progress; the per-connection
ledger mapped to the infinite-scroll items page once completed; null result + last-page cursor
coerced).
The endpoint handlers themselves are not covered by automated tests (the seam crosses
dashboard/API-key auth + the Thermos enqueue/poll calls + a real jobs table + workerdb).
POST create (org-scope via x-org-id, project-scope via
x-project-id + auth_config_id) → 202 + oj_/pj_ job_id; non-member email → 403;
whole-org/project wipe requires owner/admin role.…/jobs/org/revoke (body org_id must equal the
key's org), project-key create on …/jobs/project/revoke (auth_config_id); a project
job_id on …/jobs/org/… (and vice-versa) → 404 (cross-namespace guard).POST the same scope while a run is in flight → 409 echoing the existing job_id.GET …/{job_id} while running → identity + status: running, no results; once completed
→ status: completed + first page of per-connection results; page through with cursor/limit
(next_cursor opaque, null on the last page); filter=failed → only failed rows.POST …/{job_id}/retry → collects the failed connection ids and enqueues a fresh job
(same dedup key, inherited trigger).v3.1/jobs/* routes are absent from public_docs / sdk_docs /
frontend_docs (the x-internal strip) yet live/callable.auth configs / projects