Appearance
Pairing grant
The "pairing grant" is the cloud's redeem response — the single payload that flows back to the Device when auth.device.exchange (or auth.pair.exchange) succeeds. It carries everything the Device needs to post heartbeats and to connect to NATS, and intentionally nothing else.
Canonical wire shape
The contract is defined in HIVECAST-DEVICE-ENROLLMENT-SPEC.md § "Setup-exchange / device redeem response":
ts
interface DeviceSetupExchangeResponse {
ok: true;
authorityRoot: string;
deviceRegistryRoot: string;
hostLinkId?: string;
hostId?: string;
deviceId?: string;
hostName?: string;
deviceSlug?: string;
principalId?: string;
routeKey?: string;
publicNamespace?: string;
spaceId?: string;
credentials: Record<string, string>;
nats: {
url: string;
wsUrl?: string;
};
}deviceId is the same value as hostId. The product-facing field name is deviceId; the internal name is hostId. Both are returned for the duration of the v1 transition; new consumers should prefer deviceId.
What credentials contains
credentials is a string→string map. Today the keys present are:
| Key | Value |
|---|---|
natsUserJwt | The device-scoped user JWT, scoped under the principal's NATS account. |
natsUserSeed | The corresponding nkey seed (private). |
hostLinkToken | The plaintext heartbeat bearer token (hlink_<base64url>). Returned exactly once. |
registryToken | (optional) Token for the Device's writes against the cloud-side device registry, if deviceRegistryRoot is set. |
Record<string, string> is intentional rather than a typed record so the spec can grow new credential families without breaking existing Device clients. New consumers SHOULD ignore unrecognized keys.
How the Device persists this
saveHiveCastHostLink() in mx-cli/src/utils/hivecast-link-store.ts:454-512 accepts the response fields and writes them to per-purpose files (NOT a single dump):
| Source field | Persisted to |
|---|---|
credentials.natsUserJwt + credentials.natsUserSeed (via the typed IHiveCastNatsCredentials) | <host-home>/credentials/hivecast-nats.json (chmod 0o600) and mirrored to <host-home>/credentials.json |
credentials.hostLinkToken | <host-home>/credentials/hivecast-host-link-token (single line, chmod 0o600) |
hostLinkId, hostId, principalId, hostName, deviceSlug, spaceId, routeKey, publicNamespace, authorityRoot, cloudUrl, linkedAt | <host-home>/credentials/hivecast-link.json (mirror; chmod 0o600) |
nats.url / nats.wsUrl | <host-home>/host.json transport.nats.url / .wsUrl; transport mode set to external with credentialsRef pointing at the host credential file |
The Device never writes the redeem response to one combined file. The split keeps the heartbeat token off the same disk inode as the Host config (making leak surface analysis easier) and keeps NATS creds self-contained.
Forbidden fields
Per HIVECAST-DEVICE-ENROLLMENT-SPEC.md § "Setup-exchange / device redeem response":
Forbidden in setup-exchange and device redeem records:
displayName as device label deviceName as slug or label
displayName is principal/user identity only. deviceName is forbidden because it's ambiguous between label and slug. New code MUST use hostName (label) and deviceSlug (slug). Old payloads that still carry displayName are migrated on read on the Device side via readLegacyHiveCastLinkDisplayNameAsHostName(); they are NOT migrated by new writes.
What is NOT in the grant
The redeem response intentionally excludes:
- the principal's HiveCast session cookie or session JWT (no SSO leakage)
- the principal's NATS account seed (account seed stays cloud-side)
- raw OAuth tokens for any provider (Google, GitHub, Anthropic, Codex)
- the heartbeat-token hash (the cloud holds that; the Device holds the plaintext)
- the principal's federated identity claims (
email,iss,sub)
The argument for each exclusion is in Why hosts do not receive raw OAuth tokens.
Lifetime and rotation
The Device-scoped NATS user JWT is valid for DEVICE_NATS_CREDENTIAL_TTL_SECONDS = 86_400 (24 hours). After that, the Host's NATS connection will start failing with auth errors. Rotation is performed via auth.hostLink.credentials.refresh, which mints fresh credentials and a fresh heartbeat token in one step.
The heartbeat token does not have a separate TTL today; it is only revoked by being superseded (rotate) or by Host Link revocation. A future hardening task may add explicit token expiry. Today: rotate before expiring NATS creds, and the heartbeat token rotates with them.
See also
- Login — the wrapper command that triggers exchange and persists the response.
- Host link — the durable cloud-side record this response refers to.
- Why hosts do not receive raw OAuth tokens — why these specific exclusions exist.
Source:
WORKSTREAMS/product-launch/HIVECAST-DEVICE-ENROLLMENT-SPEC.md§ "Setup-exchange / device redeem response" (canonical wire shape);projects/matrix-3/packages/mx-cli/src/utils/hivecast-link-store.ts(Device-side persistence).