UNPKG

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
/** * 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