Maintenance, disposal and firmware
There is no on-site repair. The rear battery slot, PSU modules and fan modules are swapped without opening the enclosure; anything else means replacing the whole box, which is then factory resealed. Disposal ends with a signed certificate.
| Method · Route | Who | Effect · audit event |
|---|---|---|
POST /api/org/{slug}/roost/maintenance | Owner / Roost Admin | Open a case { serial, kind (Replacement | Rma | Disposal | BatterySwap | PsuSwap | FanSwap), reason }. roost.maintenance.opened |
POST /api/org/{slug}/roost/maintenance/{id}/advance | approve = Owner seal (roost.maintenance, RoostMaintenanceSubject(slug, "approve", caseId)); start, reseal, close = Owner / Roost Admin | Open → Approved → InProgress → FactoryReseal (replacement / RMA) → Closed. A module swap closes only with details JSON and writes the swap on the box timeline. 409 CASE_STEP_OUT_OF_ORDER |
POST /api/org/{slug}/roost/maintenance/{id}/dispose | Owner seal (RoostMaintenanceSubject(slug, "dispose", caseId)) | Issues the disposal certificate, closes the case, the box becomes Destroyed. roost.disposal.certified, roost.device.lifecycle_changed |
GET /api/org/{slug}/roost/maintenance/{id}/certificate | Owner / Roost Admin | The certificate and the public key to verify it |
POST /api/org/{slug}/roost/firmware/rollouts/{id}/rollback | Owner seal (roost.firmware, RoostFirmwareSubject(slug, "rollback", rolloutId)) | Verified / Committed → RollbackRequested; the box reports RolledBack |
Disposal certificate
base64url(payload) "." base64url(signature) — ECDSA P-256 / SHA-256 r‖s with the Roost identity key (not the re-arm key). Payload (UTF-8 JSON): { v: 1, typ: "roost-disposal", serial, model, org, caseId, lastKeysPresent, lastZeroizeState, lastReportAt, signCounter, approvedBy, disposedBy, at, reason }. Verify the signature over the payload bytes as received; certificateHash is the SHA-256 of those bytes. Without a configured identity key the endpoint answers 503 ROOST_IDENTITY_KEY_MISSING.
Firmware rollout (box side)
Both calls use the same signed envelope as /api/roost/report ({ serial, payload, signature }, attestation key).
| Method · Route | Purpose |
|---|---|
POST /api/roost/rollout-poll | payload = { at }. Returns { serial, rollouts[{ rolloutId, state (Pending | RollbackRequested), target, version, imageHash, signatureKid, channel }] } |
POST /api/roost/rollout-status | payload = { rolloutId, state, slot?, failureReason?, at }. state: Downloaded → Staged → Verified → Committed, or RolledBack from any state. Steps must advance one at a time (409 ROLLOUT_STEP_OUT_OF_ORDER). Committed and RolledBack write roost.firmware.committed |
The box downloads the image from the release store, checks the signature with its pinned key, writes the free slot, self-tests and commits; a failed self-test rolls back. Downgrades and images for another board are refused by the box.