/** * device-capability-service.ts, the one path a paired device's camera, screen, * location, clipboard, or device command is reached through. * * Every request walks the same seven steps, in this order, with no shortcuts: * 1. the node must be paired and must have announced the capability, * 2. configuration must allow the capability at all, * 3. the request's inputs must satisfy the capability's declared fields, * 4. a live grant is looked up (re-read from disk, never cached in process), * 5. with no grant, the person is asked, ask-every-time is the default for * EVERY capture and effect, and "always allow" is offered on every * capability per the owner ruling of 2026-07-25, * 6. the work is dispatched to the node over the peer transport, * 7. bytes that came back are retained under the capture TTL and disclosed. * * The dispatcher and the confirmation handler are injected, which is what keeps * this node-kind neutral: the same service serves a web PWA node and a native * node with no branch of its own. */ import { type DeviceCapabilityDescriptor, type DeviceCapabilityId, type DeviceNodeProfile } from './device-capability-contract.js'; import type { DeviceGrantStore } from './device-grants.js'; import type { DeviceCaptureArtifact, DeviceCaptureArtifactStore } from './device-capture-artifacts.js'; import { type DevicePolicySource } from './device-policy-source.js'; /** How the feature behaves overall (`device.capabilities.mode`). */ export type DeviceCapabilityMode = 'off' | 'ask-every-time' | 'honor-grants'; /** Which capabilities may be granted durably (`device.capabilities.allowAlwaysOffer`). */ export type DeviceAllowAlwaysOffer = 'every-capability' | 'standard-only' | 'never'; /** Location precision posture (`device.location.precision`). */ export type DeviceLocationPrecision = 'coarse-only' | 'ask-precise' | 'precise-grantable'; /** Clipboard read posture (`device.clipboard.readMode`). */ export type DeviceClipboardReadMode = 'off' | 'ask-only' | 'grantable'; /** The resolved configuration this service enforces. */ export interface DeviceCapabilityPolicy { readonly mode: DeviceCapabilityMode; readonly allowAlwaysOffer: DeviceAllowAlwaysOffer; readonly locationPrecision: DeviceLocationPrecision; readonly clipboardReadMode: DeviceClipboardReadMode; readonly requestTimeoutMs: number; readonly captureRetentionMs: number; } /** * Stock policy, matches the owner rulings exactly: ask every time by default, * "always allow" offered on every capability, 24h capture retention, clipboard * read present and grantable. */ export declare const DEFAULT_DEVICE_CAPABILITY_POLICY: DeviceCapabilityPolicy; /** What the person chose when asked. */ export type DeviceConfirmationDecision = 'once' | 'always' | 'deny'; /** The question put to the person, verbatim fields and all. */ export interface DeviceConfirmationRequest { readonly nodeId: string; readonly nodeKind: string; readonly nodeLabel: string; readonly capabilityId: DeviceCapabilityId; readonly descriptor: DeviceCapabilityDescriptor; /** The caller's stated reason, shown verbatim. */ readonly reason: string; readonly input: Readonly>; /** * Whether the prompt offers a durable "always allow". True for every * capability under stock configuration. */ readonly allowAlwaysOffered: boolean; readonly sessionId?: string | undefined; } export interface DeviceConfirmationResponse { readonly decision: DeviceConfirmationDecision; readonly actor: string; readonly note?: string | undefined; } export type DeviceConfirmationHandler = (request: DeviceConfirmationRequest) => Promise; /** What came back from the node. */ export interface DeviceDispatchResult { readonly ok: boolean; readonly error?: string | undefined; /** Structured payload (a location fix, clipboard text, a command ack). */ readonly data?: unknown | undefined; /** Raw bytes for a capture. Retained under the capture TTL when present. */ readonly bytes?: Uint8Array | undefined; readonly mediaType?: string | undefined; readonly workId?: string | undefined; } export interface DeviceDispatchInput { readonly nodeId: string; readonly capabilityId: DeviceCapabilityId; readonly input: Readonly>; readonly timeoutMs: number; } /** Transport to the node. The peer work queue in the daemon; a stub in tests. */ export interface DeviceCapabilityDispatcher { dispatch(input: DeviceDispatchInput): Promise; } /** Why a request did not run. */ export type DeviceRequestRefusal = 'node-unknown' | 'capability-unknown' | 'capability-unsupported' | 'capability-gated-by-secure-context' | 'disabled-by-config' | 'invalid-input' | 'denied-by-person' | 'dispatch-failed'; export type DeviceCapabilityOutcome = { readonly ok: true; readonly capabilityId: DeviceCapabilityId; readonly nodeId: string; /** How authority was established for this specific request. */ readonly authority: 'existing-grant' | 'confirmed-once' | 'confirmed-always'; readonly grantId?: string | undefined; readonly data?: unknown | undefined; readonly artifact?: DeviceCaptureArtifact | undefined; } | { readonly ok: false; readonly capabilityId: string; readonly nodeId: string; readonly refusal: DeviceRequestRefusal; readonly detail: string; }; export interface DeviceCapabilityServiceOptions { readonly grants: DeviceGrantStore; readonly artifacts: DeviceCaptureArtifactStore; readonly dispatcher: DeviceCapabilityDispatcher; readonly confirm: DeviceConfirmationHandler; /** Paired device nodes, resolved from the peer registry. */ readonly listNodes: () => readonly DeviceNodeProfile[]; /** * A fixed posture, or a resolver called once per request so a settings change * governs the NEXT request rather than waiting for a restart, the same * liveness `device.nodes.maxPaired` already has at the pairing path. See * device-policy-source.ts. */ readonly policy?: DevicePolicySource | undefined; } /** * Whether a durable grant may be offered for this capability under the current * configuration. Under the stock policy this is true for EVERY capability, * the ruling offers "always allow" on front camera, screen capture, precise * location, and clipboard alike. */ export declare function isAllowAlwaysOffered(descriptor: DeviceCapabilityDescriptor, policy: DeviceCapabilityPolicy): boolean; /** * The deadline one request gives the device, from the configured posture and * whatever the caller asked for. * * A caller may ask for a SHORTER deadline than the configured one and get it, * a surface that will stop waiting after ten seconds should not leave a phone * working for sixty. It can never ask for a LONGER one: `device.requestTimeoutMs` * is the posture, and a caller that could extend it would be setting the posture * from the wire. A missing, zero, or nonsense value is simply the configured * deadline. * * This bounds the DISPATCH only. The confirmation prompt keeps the configured * deadline in every case, because the person answering it is not the caller and * their time is not the caller's to shorten. */ export declare function resolveDeviceRequestTimeoutMs(policy: DeviceCapabilityPolicy, requestedMs: number | undefined): number; /** Configuration-level availability, before any node or person is consulted. */ export declare function capabilityDisabledReason(descriptor: DeviceCapabilityDescriptor, policy: DeviceCapabilityPolicy): string | null; export declare class DeviceCapabilityService { private readonly grants; private readonly artifacts; private readonly dispatcher; private readonly confirm; private readonly listNodes; private readonly resolvePolicy; constructor(options: DeviceCapabilityServiceOptions); /** The posture in force right now (re-read when given a resolver). */ getPolicy(): DeviceCapabilityPolicy; /** Paired nodes with the capabilities each can actually serve right now. */ listDeviceNodes(): readonly DeviceNodeProfile[]; /** * Run one capability on one node. * * A grant is consulted but never assumed: `grants.find()` re-reads the store, * so a grant revoked from any surface, or expired, or belonging to a node * that has since been unpaired, falls through to the confirmation prompt * instead of being honoured. */ request(input: { readonly nodeId: string; readonly capabilityId: string; readonly input?: Readonly> | undefined; readonly reason: string; readonly sessionId?: string | undefined; /** * A shorter deadline for the device than the configured one. Clamped by * `resolveDeviceRequestTimeoutMs`, it can only shorten, never extend. */ readonly timeoutMs?: number | undefined; }): Promise; } //# sourceMappingURL=device-capability-service.d.ts.map