trellis
Version:
Agentic State Engine — event-sourced causal graph with branching, decision traces, and realtime sync for AI-native applications
203 lines • 8.19 kB
TypeScript
/**
* Device pairing (ADR 0020 Phase 0)
*
* Delegated device keys under an Ed25519 identity. Never copies identity.json
* private key. OOB string payloads: start → join → approve → accept.
*/
import type { IdentityResolver } from './signing-middleware.js';
export declare const ROOT_DEVICE_ID = "root";
export declare const PAIR_TTL_SECONDS: number;
export declare const PAIR_PREFIX = "trellis:pair:v1:";
export declare const JOIN_PREFIX = "trellis:join:v1:";
export declare const AUTH_PREFIX = "trellis:auth:v1:";
export interface PairChallenge {
v: 1;
challengeId: string;
did: string;
identityEntityId: string;
/** Root public key (base64 SPKI) so B can verify SignedDeviceAuthorization. */
rootPublicKey: string;
exp: number;
nonce: string;
}
export interface JoinResponse {
v: 1;
challengeId: string;
devicePublicKey: string;
deviceLabel?: string;
/** Slice C metadata — stamped by the joining device, honored by the registry. */
kind?: DeviceKind;
transport?: DeviceTransport;
/** Signature over canonical challenge bytes. */
signature: string;
}
/** Slice C — device identity metadata (docs/planning/device-registry-and-sprite-pairing.md). */
export type DeviceKind = 'desktop' | 'cli' | 'cloud-sprite';
export type DeviceTransport = 'ws' | 'http' | 'iroh';
export type DeviceSyncState = 'idle' | 'syncing' | 'behind' | 'diverged' | 'offline';
export interface DeviceAuthorization {
v: 1;
deviceId: string;
identityEntityId: string;
did: string;
devicePublicKey: string;
deviceLabel?: string;
/** Slice C metadata — passed through from the joining device. */
kind?: DeviceKind;
transport?: DeviceTransport;
issuedAt: string;
expiresAt?: string;
issuerDeviceId: string;
challengeId: string;
}
export interface SignedDeviceAuthorization {
authorization: DeviceAuthorization;
signature: string;
}
export interface DeviceRecord {
deviceId: string;
devicePublicKey: string;
deviceLabel?: string;
authorizedAt: string;
revokedAt?: string;
issuerDeviceId: string;
challengeId: string;
/** Slice C — metadata + state (fed by pairing stamps, sync daemon, presence). */
kind?: DeviceKind;
transport?: DeviceTransport;
lastSeenAt?: string;
lastSyncOpHash?: string;
syncState?: DeviceSyncState;
}
export interface DeviceRegistry {
identityEntityId: string;
did: string;
rootPublicKey: string;
devices: DeviceRecord[];
}
export interface LocalDeviceKey {
deviceId: string;
identityEntityId: string;
did: string;
publicKey: string;
privateKey: string;
deviceLabel?: string;
createdAt: string;
/** Slice C — this device's own metadata + state (self touchpoint). */
kind?: DeviceKind;
transport?: DeviceTransport;
lastSeenAt?: string;
lastSyncOpHash?: string;
syncState?: DeviceSyncState;
}
export declare function encodePayload(prefix: string, obj: unknown): string;
export declare function decodePayload<T>(prefix: string, payload: string): T;
/** Crockford base32 short code from challengeId (first 8 chars of hash). */
export declare function challengeShortCode(challengeId: string): string;
export declare function deviceFingerprint(publicKeyBase64: string): string;
/**
* Person-scoped device store (Slice B, docs/planning/device-registry-and-
* sprite-pairing.md): a paired device follows the person, so every clone of
* the identity recognizes it. `~/.trellis/devices` mirrors the person
* identity dir (ADR 0032 §3). The legacy repo-scoped store
* (`.trellis/devices`) is still read as a fallback and migrated up once.
*/
export declare function personDevicesDir(): string;
export declare function loadRegistry(trellisDir: string): DeviceRegistry | null;
export declare function saveRegistry(trellisDir: string, registry: DeviceRegistry): void;
export declare function loadLocalDevice(trellisDir: string): LocalDeviceKey | null;
export declare function saveLocalDevice(trellisDir: string, local: LocalDeviceKey): void;
export declare function listDevices(trellisDir: string): DeviceRecord[];
export declare function revokeDevice(trellisDir: string, deviceId: string): boolean;
/**
* Register a device record without the QR handshake (Slice D — sprite
* provisioning). The device key is minted out-of-band (never the root
* identity key) and installed on the sprite; this records it in the
* person-scoped registry so it appears in `pair list` and resolves through
* the resolver like any paired device.
*/
export declare function registerDevice(trellisDir: string, record: Omit<DeviceRecord, 'authorizedAt' | 'issuerDeviceId' | 'challengeId' | 'deviceId'> & {
deviceId: string;
}): DeviceRecord;
export declare function resolveDevicePublicKey(trellisDir: string, identityEntityId: string, deviceId: string): string | null;
export declare function resolvePublicKeys(trellisDir: string, identityEntityId: string): string[];
export declare function assertChallengeValid(challenge: PairChallenge): void;
export declare function pairStart(trellisDir: string, opts?: {
ttlSeconds?: number;
}): {
challenge: PairChallenge;
payload: string;
shortCode: string;
};
export declare function pairJoin(trellisDir: string, challengePayloadOrCode: string, opts?: {
deviceLabel?: string;
kind?: DeviceKind;
transport?: DeviceTransport;
}): {
join: JoinResponse;
payload: string;
local: LocalDeviceKey;
challenge: PairChallenge;
};
export declare function pairApprove(trellisDir: string, joinPayload: string, opts?: {
yes?: boolean;
}): {
signed: SignedDeviceAuthorization;
payload: string;
fingerprint: string;
};
export declare function pairAccept(trellisDir: string, authPayload: string): {
local: LocalDeviceKey;
authorization: DeviceAuthorization;
};
/**
* Signing material for this install: prefer paired device key, else root identity.
*/
export declare function getSigningMaterial(trellisDir: string): {
privateKey: string;
identityEntityId: string;
signedWith: string;
} | null;
/**
* Update this machine's device state (Slice C): stamps `lastSeenAt` plus
* optional sync metadata on the local device key, and mirrors the same onto
* this device's record in the local registry when present (e.g. the approving
* machine's own paired devices). Returns false when this machine has no
* local device key (not paired).
*/
export declare function markDeviceSeen(trellisDir: string, patch?: {
syncState?: DeviceSyncState;
lastSyncOpHash?: string;
}): boolean;
/**
* Patch state on an arbitrary device record in the registry (Slice C) —
* used by the sync daemon and future sprite heartbeats. No-op for unknown or
* revoked devices.
*/
export declare function updateDeviceState(trellisDir: string, deviceId: string, patch: {
syncState?: DeviceSyncState;
lastSyncOpHash?: string;
lastSeenAt?: string;
}): boolean;
/**
* Build an `IdentityResolver` bound to a trellis directory (ADR 0022 Phase 3).
*
* The `IdentityResolver` interface takes only an `entityId` (it is injected
* into the engine, which has no filesystem concept), whereas the registry
* lookups in this module need the `trellisDir`. This adapter closes that gap:
* it captures the dir and forwards to the registry/local-device resolution
* that backs ADR 0020 device keys.
*
* Returns `null` when the directory has no identity at all, so callers can
* keep the resolver opt-in (an identity-less repo gets no PKI enforcement).
*/
/**
* Build an `IdentityResolver` bound to a trellis directory (ADR 0022 Phase 3,
* ADR 0036). Resolves keys from the device registries (person scope first,
* repo scope fallback) plus the local identity root — regardless of whether a
* local identity exists: a machine with no identity still verifies remote ops
* signed by identities it knows through pairing. An identity whose key is in
* neither the registries nor the local store resolves to nothing (fail-closed).
*/
export declare function pairingResolver(trellisDir: string): IdentityResolver;
//# sourceMappingURL=pairing.d.ts.map