import { PersistentStore } from '../state/persistent-store.js'; import { type DeviceCapabilityId, type DeviceNodeKind } from './device-capability-contract.js'; import { type DevicePolicySource } from './device-policy-source.js'; /** Scope of an approval a person gave. */ export type DeviceGrantScope = 'always' | 'session'; /** A durable per-capability, per-node approval. */ export interface DeviceCapabilityGrant { readonly id: string; readonly nodeId: string; readonly nodeKind: DeviceNodeKind; readonly capabilityId: DeviceCapabilityId; readonly scope: DeviceGrantScope; /** Present only for session-scoped grants; reaped when the session is gone. */ readonly sessionId?: string | undefined; readonly grantedAt: number; /** Age TTL. Every grant has one, nothing is granted forever. */ readonly expiresAt: number; readonly lastUsedAt?: number | undefined; readonly useCount: number; /** Who approved it (operator id / surface actor). */ readonly grantedBy: string; } /** Why a grant left the store. */ export type DeviceGrantRemovalReason = 'revoked' | 'expired' | 'node-gone' | 'session-gone' | 'malformed' | 'per-node-cap' | 'total-cap'; /** One removal, itemised for disclosure. */ export interface DeviceGrantRemoval { readonly grantId: string; readonly nodeId: string; readonly capabilityId: string; readonly scope: string; readonly reason: DeviceGrantRemovalReason; readonly removedAt: number; readonly note?: string | undefined; } /** Bounded ledger entry so a surface can show what happened and why. */ export interface DeviceGrantAuditRecord { readonly id: string; readonly action: 'granted' | 'used' | 'removed'; readonly grantId: string; readonly nodeId: string; readonly capabilityId: string; readonly at: number; readonly actor: string; readonly reason?: string | undefined; } interface DeviceGrantSnapshot extends Record { readonly version: 1; readonly grants: readonly DeviceCapabilityGrant[]; readonly audit: readonly DeviceGrantAuditRecord[]; } /** Result of one housekeeping pass over the grant store. */ export interface DeviceGrantSweepReport { readonly sweptAt: number; readonly removed: readonly DeviceGrantRemoval[]; readonly retained: number; readonly auditTrimmed: number; } export interface DeviceGrantPolicy { /** Age TTL applied when a grant is recorded. */ readonly grantTtlMs: number; /** Count cap per node; oldest grants past the cap are reaped. */ readonly maxGrantsPerNode: number; /** Absolute count cap across all nodes. */ readonly maxGrantsTotal: number; /** Age TTL for the audit ledger. */ readonly auditRetentionMs: number; /** Count cap for the audit ledger. */ readonly maxAuditRecords: number; } export declare const DEFAULT_DEVICE_GRANT_POLICY: DeviceGrantPolicy; /** * Liveness probes the sweep uses to decide whether a grant's owner still * exists. Both default to "still there" so a caller that cannot answer never * causes silent data loss. */ export interface DeviceGrantOwnership { readonly isKnownNode?: ((nodeId: string) => boolean) | undefined; readonly isActiveSession?: ((sessionId: string) => boolean) | undefined; } export interface DeviceGrantStoreOptions { /** * Fixed bounds, or a resolver read at every use so a live settings change * applies without a restart (see device-policy-source.ts). */ readonly policy?: DevicePolicySource | undefined; readonly now?: (() => number) | undefined; readonly ownership?: DeviceGrantOwnership | undefined; } /** * Durable grant store. Every read re-loads from disk so a revocation written by * another process (the webui grants surface, say) is honoured immediately. */ export declare class DeviceGrantStore { private readonly store; private readonly resolvePolicy; private readonly now; private readonly ownership; private writeChain; constructor(storeOrPath: PersistentStore | string, options?: DeviceGrantStoreOptions); /** * The bounds in force right now. With a resolver this re-reads the live * configuration, so a caller rendering the policy shows what the next sweep * will actually apply. */ getPolicy(): DeviceGrantPolicy; private readWithDrops; private read; /** Serialise writes within this process; across processes the write is atomic. */ private mutate; /** * Every grant currently on disk, after dropping malformed and expired ones. * Expired records are filtered here as well as reaped by `sweep()`, so an * expired grant can never be honoured even between sweeps. */ list(): Promise; /** The audit ledger, newest last. */ listAudit(limit?: number): Promise; /** * Find a live grant authorising this capability on this node, or null. * * Returns null for anything revoked (the record is gone), expired, or scoped * to a session that is no longer active, a revoked or expired grant is never * silently honoured. */ find(input: { readonly nodeId: string; readonly capabilityId: DeviceCapabilityId; readonly sessionId?: string | undefined; }): Promise; /** Record an approval. Re-granting an existing pair refreshes it rather than duplicating. */ record(input: { readonly nodeId: string; readonly nodeKind: DeviceNodeKind; readonly capabilityId: DeviceCapabilityId; readonly scope: DeviceGrantScope; readonly sessionId?: string | undefined; readonly grantedBy: string; readonly ttlMs?: number | undefined; }): Promise; /** Note a use of a grant (drives "last used" in the grants surface). */ markUsed(grantId: string): Promise; /** * Revoke grants. The matching records are DELETED, not flagged, there is no * "revoked but present" state a later read could mistake for authority. * Returns the itemised removals for disclosure. */ revoke(input: { readonly grantId?: string | undefined; readonly nodeId?: string | undefined; readonly capabilityId?: DeviceCapabilityId | undefined; readonly actor: string; readonly note?: string | undefined; }): Promise; /** * One housekeeping pass. Safe at recovery, safe on a timer, safe concurrently *, it recomputes every removal from the file it just read and writes the * result atomically, so running it twice removes nothing extra. */ sweep(): Promise; } export {}; //# sourceMappingURL=device-grants.d.ts.map