Appearance
Grant schema
A "grant" is what pairing produces. Cloud-side it's the IHostLinkRecord; on the wire to the Device it's the DeviceSetupExchangeResponse. Both shapes appear in the docs-devices Reference; this page restates them from the security perspective and adds the scope-normalization rule.
IHostLinkRecord (cloud authority)
Source: projects/matrix-3/packages/system-auth/src/host-auth.ts:94-113.
ts
interface IHostLinkRecord {
readonly id: string; // hostlink_<24 hex>
readonly hostId: string; // host_<20 hex>
readonly hostName?: string; // mutable label
readonly deviceSlug: string; // address-safe projection key
readonly natsUserPublicKeys?: readonly string[];
heartbeatTokenHash?: string; // sha256 of the bearer
heartbeatTokenIssuedAt?: string;
readonly principalId: string;
readonly spaceId: string;
readonly routeKey?: string;
readonly publicNamespace?: string;
readonly authorityRoot: string;
readonly scopes: readonly string[];
status: 'active' | 'revoked';
readonly createdAt: string;
updatedAt: string;
lastRefreshedAt?: string;
revokedAt?: string;
}DeviceSetupExchangeResponse (wire)
ts
interface DeviceSetupExchangeResponse {
ok: true;
authorityRoot: string;
deviceRegistryRoot: string;
hostLinkId?: string;
hostId?: string;
deviceId?: string; // == hostId
hostName?: string;
deviceSlug?: string;
principalId?: string;
routeKey?: string;
publicNamespace?: string;
spaceId?: string;
credentials: Record<string, string>;
nats: {
url: string;
wsUrl?: string;
};
}credentials keys present today:
| Key | Value |
|---|---|
natsUserJwt | device-scoped NATS user JWT |
natsUserSeed | nkey seed |
hostLinkToken | plaintext heartbeat bearer (returned exactly once) |
registryToken | (optional) device registry write token |
Scope normalization
scopes[] on the Host Link record is sorted, de-duplicated, with empty entries dropped. From host-auth.ts:1775-1784:
ts
function normalizeScopes(value: readonly string[] | undefined): readonly string[] {
const clean = new Set<string>();
for (const entry of value ?? []) {
const scope = readNonEmptyString(entry);
if (scope) clean.add(scope);
}
return [...clean].sort();
}So:
input: ['invoke', 'invoke', '', 'install', 'invoke']
output: ['install', 'invoke']This canonicalization is applied at HostLinkStore.create() and during pending device-link / pairing record construction.
Note: the scopes field is recorded for audit but not used as a per-op gate today. See Capability tokens.
Pending grant records
Pairing records that have not yet been exchanged carry their own shapes:
IHostDeviceLinkRecord
ts
interface IHostDeviceLinkRecord {
readonly deviceCode: string;
readonly userCode: string;
readonly hostId?: string;
readonly hostName?: string;
readonly routeKey?: string;
readonly scopes: readonly string[];
readonly intervalSeconds: number;
readonly createdAt: string;
readonly expiresAt: string;
status: 'pending' | 'approved' | 'namespace_required' | 'exchanged' | 'cancelled';
updatedAt: string;
error?: string;
approvedAt?: string;
approvedByPrincipalId?: string;
hostLinkId?: string;
exchangedAt?: string;
cancelledAt?: string;
}IHostPairingRecord
ts
interface IHostPairingRecord {
readonly pairRequestId: string;
readonly hostId?: string;
readonly hostName?: string;
readonly localReturnUrl?: string;
readonly nonce?: string;
readonly routeKey?: string;
readonly scopes: readonly string[];
readonly createdAt: string;
readonly expiresAt: string;
status: 'pending' | 'approved' | 'exchanged' | 'cancelled';
updatedAt: string;
approvalCode?: string;
approvedAt?: string;
approvedByPrincipalId?: string;
hostLinkId?: string;
exchangedAt?: string;
cancelledAt?: string;
}The IHostDeviceLinkRecord is for headless device-code flow; IHostPairingRecord is for interactive browser-pair. Both terminate in a Host Link record.
Identity-fields summary
| Field | Format | Generator |
|---|---|---|
principalId | p_<20 hex> | stablePrincipalId(issuer, subject) |
spaceId | space_<20 hex> | generateSpaceId() |
hostId | host_<20 hex> | generateHostId() |
hostLinkId | hostlink_<24 hex> | generateHostLinkId() |
deviceCode | base64url, 32 random bytes | generateDeviceCode() |
userCode | XXXX-XXXX (32-char alphabet) | generateDeviceUserCode() |
pairRequestId | pair_<24 hex> | generatePairRequestId() |
approvalCode | base64url, 24 bytes | generatePairApprovalCode() |
hostLinkToken | hlink_<base64url 32 bytes> | generateHostLinkHeartbeatToken() |
deviceSlug | kebab-lowercase, ≤64 chars | deviceSlugFromLabel() + allocateDeviceSlug() |
See also
- Token schema — JWT and bearer formats.
- Claim schema — namespace claim shapes.
- docs-devices Reference / Host link schema — Device-side mirror.
Source:
projects/matrix-3/packages/system-auth/src/host-auth.ts(IHostLinkRecord,IHostDeviceLinkRecord,IHostPairingRecord,normalizeScopes, identity generators);WORKSTREAMS/product-launch/HIVECAST-DEVICE-ENROLLMENT-SPEC.md§ "Setup-exchange / device redeem response".