Appearance
Pairing
Pairing bonds a local Matrix install (the Device) to a HiveCast principal account. It is the durable approval ceremony — the result is a Host Link record on the platform Host and a per-Device NATS user JWT on the Device.
The uniform model
Per HIVECAST-DEVICE-ENROLLMENT-SPEC.md:
There is one attach model, not separate ad hoc flows. Different surfaces can start the same protocol: local Matrix UI, HiveCast web UI, headless CLI. But they must all terminate in the same backend contract and the same device credential model.
The five-step protocol is:
- Start enrollment.
- Authenticate and approve.
- Redeem on the target Device.
- Issue a device-scoped credential.
- Connect and register.
The two op families
Two backend op families implement the protocol — they share the same outcome but differ in initiation:
| Family | Entry point | Codes | Use |
|---|---|---|---|
auth.device.* | Headless / device-flow | deviceCode + userCode | CLI: hivecast login --device --cloud https://hivecast.ai. User approves on any signed-in browser. |
auth.pair.* | Interactive / browser-pair | pairRequestId + approvalCode | Edge: user clicks "Connect to HiveCast." Browser navigates to platform, approves, returns to Device. |
Source: system-auth/src/index.ts:882-1054 (device-flow), 1055-1196 (pair-flow).
Flow A: headless CLI (device-flow)
This is the canonical headless flow used by hivecast login --device --cloud https://hivecast.ai.
1. auth.device.start
bash
matrix invoke system.auth auth.device.start '{
"hostId": "<install-id>",
"name": "richard-laptop",
"routeKey": "alt.stories.ghost-stories.funny",
"scopes": ["device.runtime.read"]
}'Returns { deviceCode, userCode, expiresIn, interval, device }. The user code is short and human-readable (e.g., WDJB-MJHT); print it. The CLI prompts the user to open https://hivecast.ai/apps/web/#activate and enter the user code.
2. User approves
The user opens HiveCast in a signed-in browser, finds the activation page, enters the user code, and clicks Approve. The browser calls:
bash
matrix invoke system.auth auth.device.approve '{
"userCode": "WDJB-MJHT",
"principalId": "<their-principal-id>"
}'Approve checks:
- The principal owns a Space with a public-namespace claim matching the requested
routeKey(or any active claim, if norouteKeywas supplied). - That Space's
ownerPrincipalId === principalId.
If those check out, a Host Link is created bonded to the Space. If not, the device record is marked namespace_required and auth.device.poll returns that error.
3. CLI polls
The CLI calls auth.device.poll every interval seconds. While status: 'pending', it returns authorization_pending. Once the user approves, status flips to approved.
4. CLI exchanges
bash
matrix invoke system.auth auth.device.exchange '{
"deviceCode": "<from step 1>"
}'This returns the full pairing payload:
ts
{
ok: true,
status: 'exchanged',
hostLink: { id, hostId, deviceSlug, principalId, spaceId, authorityRoot, ... },
credentials: { jwt, seed, ... }, // device-scoped NATS user JWT (24h TTL)
principalId,
routeKey,
publicNamespace,
spaceId,
authorityRoot,
principal: { id, displayName },
displayName,
}The CLI persists hostLink to <host-home>/credentials/hivecast-link.json and credentials to the same file (or a sibling). It rotates the heartbeat token on first exchange (rotateHeartbeatToken).
Flow B: interactive (pair-flow)
This is the canonical browser flow used by Edge "Connect to HiveCast."
1. auth.pair.start
The local Edge calls (via the local gateway) :
bash
matrix invoke system.auth auth.pair.start '{
"hostId": "<install-id>",
"hostName": "richard-laptop",
"localReturnUrl": "http://127.0.0.1:3100/apps/edge/",
"nonce": "<random>",
"routeKey": "alt.stories.ghost-stories.funny",
"scopes": ["device.runtime.read"]
}'Returns { pairRequestId, expiresIn, pairing }. The local UI redirects the user's browser to https://hivecast.ai/auth/pair?pairRequestId=<id>&returnUrl=....
2. User approves on HiveCast
The signed-in user clicks Approve. HiveCast calls:
bash
matrix invoke system.auth auth.pair.approve '{
"pairRequestId": "<id>",
"principalId": "<their-principal-id>"
}'Approval returns an approvalCode. The browser is redirected to <localReturnUrl>?pairRequestId=<id>&approvalCode=<code>.
3. Local Device exchanges
The Device's local gateway, on receiving the redirect, calls:
bash
matrix invoke system.auth auth.pair.exchange '{
"pairRequestId": "<id>",
"approvalCode": "<code>"
}'Same payload returned as device-flow exchange.
4. Device persists and connects
Same as device-flow step 4.
Flow C: HiveCast-initiated
This is the "Connect another device" or "Open on this machine" flow from the HiveCast Devices page. It calls auth.device.start from inside the platform shell (with the principal already known) and renders a QR code or activation URL the target Device opens. The target Device then runs the same redeem flow as Flow A or B.
The current implementation surfaces the user-code on /apps/web/#activate and sends an enrollment URL the target Device can open in any browser. Auto-redeem (the target Device pre-authenticates and fetches via a one-time URL) is target state.
Why three flows but one record
The Host Link record produced is identical regardless of entry point:
ts
IHostLinkRecord {
id, hostId, hostName, deviceSlug,
natsUserPublicKeys[], // recorded so revocation can target this Device
heartbeatTokenHash,
heartbeatTokenIssuedAt,
principalId, spaceId,
routeKey, publicNamespace, // dual-emit
authorityRoot,
scopes[],
status: 'active',
createdAt, updatedAt, lastRefreshedAt
}After exchange, the Device sends authenticated heartbeats; system.devices aggregates the Host Link plus heartbeat presence into a Device row.
Cancel and timeout
Pending requests expire after the configured TTL (DEVICE_LINK_DURATION_MS = 30 * 60 * 1000 for device-flow; PAIRING_DURATION_MS = 10 * 60 * 1000 for pair-flow). Either flow can be canceled:
bash
matrix invoke system.auth auth.device.cancel '{"userCode":"<code>"}'
matrix invoke system.auth auth.pair.cancel '{"pairRequestId":"<id>"}'Cancellation flips status: 'cancelled'; subsequent polls/exchanges fail.
namespace_required failure
If the principal does not own a Space with a public-namespace claim matching the requested routeKey, the device-flow records namespace_required on the device link record. auth.device.poll returns this status until the principal claims a namespace. The pair-flow returns the same error from auth.pair.approve. Operators handling this:
bash
# Inspect what claims the principal has
matrix invoke system.auth auth.namespace.list '{"principalId":"p_xxx"}'
# If empty, claim a path
matrix invoke system.auth auth.namespace.claim '{
"principalId": "p_xxx",
"routeKey": "alt.stories.ghost-stories.funny"
}'
# Resume the original device-flow with the same userCode
matrix invoke system.auth auth.device.approve '{...}'Live proof
Per HIVECAST-DEVICE-ENROLLMENT-SPEC.md and the workstream STATUS, device-scoped credential issuance is proven on hivecast.ai (proof name richard-device-scoped-proof-20260501-060747). Host Link revoke is proven on hivecast.ai (richard-device-revoke-proof-20260501-062524). UI revoke is proven (richard-ui-revoke-proof-20260501-064035). Credential refresh is proven (richard-refresh-proof-20260501-065817).
See also
- Disconnect / unlink — the revoke flow.
- Device health — heartbeat protocol post-exchange.
- Edge / Device Management / Pair — Edge UI side.
- Reference: Actor surfaces — full op list.
Source:
projects/matrix-3/packages/system-auth/src/index.ts:882-1196for the op implementations.WORKSTREAMS/product-launch/HIVECAST-DEVICE-ENROLLMENT-SPEC.mdis the SSOT.