Skip to content

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:

KeyValue
natsUserJwtThe device-scoped user JWT, scoped under the principal's NATS account.
natsUserSeedThe corresponding nkey seed (private).
hostLinkTokenThe 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 fieldPersisted 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

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).