Skip to content

Error codes

The Devices op surface does not use string error codes. Failures are encoded as { ok: false, ... } returns. This page enumerates each failure and the cause.

system.devices failure shapes

devices.register{ ok: false, deviceId: '', created: false }

Cause: payload missing deviceId / hostId. The op cannot identify which Device to register.

Fix: include either deviceId or hostId in the payload.

devices.register{ ok: false, deviceId: <X>, created: false }

Cause: an existing record for deviceId = X has populated identity scope (principalId, spaceId, authorityRoot, hostLinkId, publicNamespace, or routeKey) and the new payload disagrees. See deviceRecordCanCarryForward() in SystemDevicesActor.ts:510-536.

Fix: either pass the same scope as the existing record (correct case, e.g. heartbeat from the rightful owner) or remove the existing record first by revoking the prior Host Link. This guards against accidental ownership swaps.

devices.heartbeat{ ok: false, known: false, deviceId: '' }

Cause: payload missing deviceId / hostId.

Fix: include either field.

devices.heartbeat{ ok: true, known: false, deviceId: <X> }

Cause: no record exists for deviceId = X. Heartbeat is a no-op.

Fix: register first via devices.register. Heartbeat does not auto-create.

devices.heartbeat{ ok: false, known: true, deviceId: <X> }

Cause: a record exists for deviceId = X, but the payload's scope disagrees with it.

Fix: same as devices.register ownership-mismatch fix.

devices.deregister{ ok: false, known: false, deviceId: '' }

Cause: payload missing deviceId / hostId.

devices.deregister{ ok: true, known: false, deviceId: <X> }

Cause: no record for X. No-op.

devices.deregister{ ok: false, known: true, deviceId: <X> }

Cause: record exists, payload scope disagrees. Refused.

devices.list (no error)

devices.list always returns { ok: true, count, devices }. Passing unknown / mistyped scope fields produces an empty result, not an error.

devices.get{ ok: false, device: null }

Cause: no Device matches the requested deviceId (and, if provided, the scope).

devices.runtimes.register{ ok: false, deviceId, runtimeId, created: false }

Three sub-causes:

  • payload missing deviceId or runtimeIddeviceId and runtimeId may be empty strings in the response
  • no Device record for deviceId
  • payload scope disagrees with the existing Device record

devices.runtimes.list{ ok: false, deviceId, count: 0, runtimes: [] }

Cause: no Device record for deviceId, OR the scope-filter rejects the existing record.

These return null from store methods; the bus op layer wraps them as { ok: false, error?: string }. Common cases:

Throws if principalId, spaceId, or authorityRoot is empty:

Error: Host link requires principalId, spaceId, and authorityRoot

HostLinkStore.create() at host-auth.ts:799-859.

null returns mean "not found" or "scope mismatch". The op layer returns { ok: false }. Specific causes:

  • id not found in hostLinks[]
  • hostId not found among active links
  • id found but hostId filter mismatches the record's hostId
  • id found but principalId filter mismatches

verifyHeartbeatToken returns null for additional reasons:

  • record exists but status !== 'active' (revoked)
  • record has no heartbeatTokenHash (was never rotated, or rotation was lost)
  • bearer token's SHA-256 does not equal the stored hash (safeEqualStrings returns false)

auth.device.start / .approve / .exchange errors

Each store method returns null on a state-machine violation. The op layer surfaces { ok: false }.

State machine refusals:

  • approve when status cancelled or exchanged → returns null
  • exchange when status is anything other than approved → returns null (or returns the record unchanged if already exchanged)
  • get/cancel against a non-existent code → returns null

Cloud HTTP heartbeat error codes

POST /_auth/host-link/heartbeat returns standard HTTP statuses; the gateway is the producer:

StatusCauseFix
200successn/a
400malformed body, missing fieldsclient bug
401no Authorization header, or token has wrong formatinclude the bearer; check <host-home>/credentials/hivecast-host-link-token
403verifyHeartbeatToken rejected (revoked, token mismatch, body identity disagrees with bearer-derived identity)re-pair
404Host Link not foundre-pair
5xxserver-side errorretry

The body→bearer identity-mismatch case (P1.17 adversarial scenario D) is rejected at the verification layer because the cloud uses bearer-derived identity, not body identity.

How errors propagate to the bus client

Bus clients (RequestReply.execute()) receive the JSON return shape directly. There is no separate error envelope. Callers MUST check response.ok === true before treating the data as authoritative. Callers SHOULD log the unhappy path before surfacing to the user.

Logging

host.control emits two failure events on its own mount:

  • device.heartbeat.failed { deviceId?, reason } — most common.
  • device.heartbeat.failed { reason: '<error message>' } — for thrown errors (e.g. cloud HTTP failures).

system.devices does NOT emit devices.error events; failures are returned in the op response.

See also

Source: projects/matrix-3/packages/system/src/SystemDevicesActor.ts (return paths); projects/matrix-3/packages/system-auth/src/host-auth.ts (null return paths in HostLinkStore, HostDeviceLinkStore, HostPairingStore).