Skip to content

feat(api): /api/v1 lifecycle endpoints — suspend / resume / delete (p1b)#28

Merged
nechodom merged 1 commit into
mainfrom
feat/api-v1-lifecycle-endpoints
Jul 2, 2026
Merged

feat(api): /api/v1 lifecycle endpoints — suspend / resume / delete (p1b)#28
nechodom merged 1 commit into
mainfrom
feat/api-v1-lifecycle-endpoints

Conversation

@nechodom

@nechodom nechodom commented Jul 2, 2026

Copy link
Copy Markdown
Owner

Continues the remote management API (p1b). The API could only read; this adds the first write slice so a key can manage existing hostings.

Endpoints

Method Path Cap Shape
POST /api/v1/hostings/:id/suspend HostingSuspend sync → {id, state:"suspended"}
POST /api/v1/hostings/:id/resume HostingSuspend sync → {id, state:"active"}
DELETE /api/v1/hostings/:id HostingDelete 202{job_id}

:id accepts a hosting id or a domain (same disambiguation as the read detail route).

Authz — safe by construction

A shared resolve_manage() resolves :id → detail + owning node via find_hosting_anywhere, then reuses the browser UI's own require_hosting_access gate (capability held → scope_all reaches every hosting → a non-scope_all key fails closed until per-key grants land), swapping its HTML 403 for the JSON envelope. This is deliberately not a mint-time-only fence — the exact gap the earlier audit flagged — so when per-tenant keys ship in the next slice these endpoints are already scoped.

Delete = background job

Delete reuses the same job the UI spawns (nginx reload, acme cleanup, DROP DATABASE, rm -rf, userdel are slow), audited as actor apikey:<label>. Clients poll GET /api/v1/jobs/:id (already gated to scope_all in #26). Suspend/resume are synchronous and reuse the existing HostingSuspend/HostingResume RPCs.

Not in scope (still TODO(api-p1b))

  • POST /api/v1/hostings (create) — large request body + node placement, its own slice.
  • Tenant read-scoping so non-scope_all keys can finally be minted.

Test

New e2e test: all three endpoints return 401 with no key and with an unknown Bearer key — guards routing + method wiring (a 404/405 would mean a mistake). clippy -D warnings clean; full hyperion-web suite green.

🤖 Generated with Claude Code

The remote API could only READ. Add the first write/lifecycle slice so a key
can actually manage existing hostings:

  * POST   /api/v1/hostings/:id/suspend   cap HostingSuspend  (sync)
  * POST   /api/v1/hostings/:id/resume    cap HostingSuspend  (sync)
  * DELETE /api/v1/hostings/:id           cap HostingDelete   → 202 {job_id}

Authz is safe-by-construction, not a mint-time fence: a shared resolve_manage()
resolves :id (id OR domain) to its detail + owning node via find_hosting_anywhere,
then reuses the browser UI's own require_hosting_access gate (capability held →
scope_all reaches every hosting → non-scope_all key fails closed until per-key
grants land), swapping its HTML 403 for the JSON envelope. So when per-tenant
keys ship these endpoints are already scoped — no follow-up fence needed.

Delete runs as the same background job as the UI (nginx reload / acme cleanup /
DROP DATABASE / rm -rf / userdel are slow), audited as actor apikey:<label>;
poll GET /api/v1/jobs/:id for completion. Suspend/resume are synchronous and
return the new state. All reuse the existing HostingSuspend/Resume/Delete RPCs.

Still TODO(api-p1b): POST /api/v1/hostings (create — large body + placement) and
tenant read-scoping so non-scope_all keys can be minted.

e2e test: all three endpoints 401 without / with an unknown Bearer key (guards
routing + method wiring). clippy -D warnings clean; hyperion-web suite green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@nechodom nechodom merged commit 6d8fe0c into main Jul 2, 2026
1 check passed
@nechodom nechodom deleted the feat/api-v1-lifecycle-endpoints branch July 2, 2026 17:05
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