Appearance
Device grants
A "Device grant" is the bundle of artefacts a successful pairing leaves on the Device. Each artefact has a different scope, lifetime, and revocation path. Treating them as one thing is the source of most pairing-security confusion, so this page enumerates them.
The four artefacts
| Artefact | Carries | Audience | Scope | Lifetime | Revocation |
|---|---|---|---|---|---|
| Host Link record | hostLinkId, hostId, principalId, spaceId, authorityRoot, deviceSlug, scopes[] | cloud-side system.auth, mirrored to local hivecast-link.json | One Device on one principal/Space | Until auth.hostLink.revoke or --all logout | auth.hostLink.revoke |
| Heartbeat token | Opaque bearer token, only its SHA-256 stored cloud-side | Used by host.control against /_auth/host-link/heartbeat | One Host Link | Rotates on every auth.hostLink.credentials.refresh | superseded by next rotate, or by revoke |
| Device-scoped NATS credentials | NATS user JWT + nkey seed, scoped under the principal's NATS account | Used by the Host's NATS client to connect to HiveCast NATS | One Device, NATS user permissions only | DEVICE_NATS_CREDENTIAL_TTL_SECONDS = 86_400 (24h); refresh via auth.hostLink.credentials.refresh | superseded on rotate; revoked by deactivating the user JWT cloud-side |
| Scope set | scopes[] on the Host Link record | Recorded for audit; consumers of bus token may inspect | Names the capabilities granted to this Device | Lifetime of the Host Link | (no per-scope revoke; revoke the link) |
Host Link record
This is the durable cloud-side row in system.auth. Its TypeScript shape:
ts
// projects/matrix-3/packages/system-auth/src/host-auth.ts:94-113
export interface IHostLinkRecord {
readonly id: string; // hostlink_<24 hex>
readonly hostId: string; // host_<20 hex>
readonly hostName?: string; // mutable label
readonly deviceSlug: string; // stable management projection key
readonly natsUserPublicKeys?: readonly string[];
heartbeatTokenHash?: string;
heartbeatTokenIssuedAt?: string;
readonly principalId: string;
readonly spaceId: string;
readonly routeKey?: string; // v1 alias for spacePath
readonly publicNamespace?: string; // typed namespace claim, e.g. space.alt.stories
readonly authorityRoot: string; // bus/security root
readonly scopes: readonly string[];
status: HostLinkStatus; // 'active' | 'revoked'
readonly createdAt: string;
updatedAt: string;
lastRefreshedAt?: string;
revokedAt?: string;
}The id is hostlink_<24 hex> (generateHostLinkId() in host-auth.ts:1687). The hostId is host_<20 hex> (generateHostId() at host-auth.ts:1683).
The Device side mirrors the link as IHiveCastHostLinkRecord in mx-cli/src/utils/hivecast-link-store.ts:19-38.
Heartbeat token
The cloud never stores the bearer token in plaintext. It stores only the SHA-256 hash. From host-auth.ts:1691-1697:
ts
function generateHostLinkHeartbeatToken(): string {
return `hlink_${randomBytes(32).toString('base64url')}`;
}
function hashHostLinkHeartbeatToken(token: string): string {
return createHash('sha256').update(token).digest('base64url');
}HostLinkStore.rotateHeartbeatToken() mints a fresh token, hashes it, records the hash on the Host Link row, and returns the plaintext token to the caller exactly once. The Device writes that plaintext to <host-home>/credentials/hivecast-host-link-token (one line, file mode 0o600).
Verification on heartbeat:
ts
// host-auth.ts:926-949
verifyHeartbeatToken(input: { token; id?; hostId?; principalId? }): IHostLinkRecord | null {
// ...
const presentedHash = hashHostLinkHeartbeatToken(token);
if (!safeEqualStrings(presentedHash, record.heartbeatTokenHash)) return null;
// ...mark refreshed
}safeEqualStrings uses timingSafeEqual to avoid timing attacks. The op exposed on the bus is auth.hostLink.verifyHeartbeat; the HTTP route that actually receives heartbeats is /_auth/host-link/heartbeat in the gateway.
Device-scoped NATS credentials
Credentials are issued under the principal's NATS account. The Device gets a user JWT — not the account seed. The account seed stays server-side; the device user JWT is short-lived and revocable independently.
SystemAuthActor.onAuthHostLinkCredentialsRefresh in system-auth/src/index.ts calls into @open-matrix/nats-auth helpers (scopedJwtCredentialsFromAccountSeed, rootScopedNatsUserPermissions) to mint a per-device user with subject permissions scoped to the link's authorityRoot. The TTL constant is in system-auth/src/index.ts:
ts
const DEVICE_NATS_CREDENTIAL_TTL_SECONDS = 86_400;Files written on the Device:
| File | Contents | Mode |
|---|---|---|
<host-home>/credentials/hivecast-nats.json | { jwt, seed, accountSeed?, … } | 0o600 |
<host-home>/credentials.json | mirror, used by the Host's own NATS client | 0o600 |
Note: the field names in
hivecast-nats.jsonmirror theIHiveCastNatsCredentialsinterface inmx-cli/src/utils/hivecast-link-store.ts:13-17.
Scopes
scopes[] on the Host Link is a sorted, de-duplicated list of capability names attached at pairing time. The default scope set is established by auth.device.start / auth.pair.start callers; today it is small and mostly aspirational. The scopes field is recorded for audit and may be consulted by future authorization checks. It is not itself a per-op permission gate today — that gate is the Host Link status (active vs revoked) and the bearer-token check.
Revoke surfaces
Three different revocation paths exist:
| Revoke action | Effect | Op |
|---|---|---|
| Revoke Host Link | status: 'revoked', revokedAt set, all subsequent heartbeat-token checks fail | auth.hostLink.revoke |
| Rotate heartbeat token | Old token no longer matches heartbeatTokenHash (new hash recorded) | auth.hostLink.credentials.refresh (also rotates NATS) |
Local hivecast logout (default) | Removes local credential files; cloud row stays active until heartbeats time out | none — local-only |
hivecast logout --revoke-cloud-link | Calls cloud revoke endpoint | auth.hostLink.revoke |
See also
- Pairing / Pairing grant — the wire-level shape of the response that delivers these artefacts.
- Operations / Revoke grant — runbook.
- Operations / Rotate credentials — rotation runbook.
- Reference / Host link schema — full shape.
Source:
projects/matrix-3/packages/system-auth/src/host-auth.ts(IHostLinkRecord,HostLinkStore,generateHostLink*,verifyHeartbeatToken);projects/matrix-3/packages/mx-cli/src/utils/hivecast-link-store.ts(Device-side mirror).