/** * Type definitions for the declarative policy engine. * * Policies are plain data (JSON-serializable), deterministic, and * transport-agnostic — they operate on atproto accounts (DIDs), not raw blocks. */ // ============================================ // Target: which accounts a policy applies to // ============================================ /** Match all serviced accounts. */ export interface TargetAll { type: "all"; } /** Match an explicit list of DIDs. */ export interface TargetList { type: "list"; dids: string[]; } /** Match DIDs whose string representation matches a pattern (substring or prefix). */ export interface TargetPattern { type: "pattern"; /** A prefix string to match against DIDs, e.g. "did:plc:" or "did:web:example.com". */ prefix: string; } export type PolicyTarget = TargetAll | TargetList | TargetPattern; // ============================================ // Replication goals // ============================================ export interface ReplicationGoals { /** Minimum number of copies across peers (including this node). Default: 1. */ minCopies: number; /** Preferred peer DIDs to replicate with. Optional. */ preferredPeers?: string[]; } // ============================================ // Sync configuration // ============================================ export interface SyncConfig { /** How often to sync, in seconds. Default: 300 (5 minutes). */ intervalSec: number; } // ============================================ // Retention configuration // ============================================ export interface RetentionConfig { /** How long to keep data, in seconds. 0 = forever. Default: 0. */ maxAgeSec: number; /** Whether to keep historical revisions. Default: false. */ keepHistory: boolean; } // ============================================ // Policy definition // ============================================ export interface Policy { /** Unique identifier for this policy. */ id: string; /** Human-readable name. */ name: string; /** Which accounts this policy applies to. */ target: PolicyTarget; /** Replication goals. */ replication: ReplicationGoals; /** Sync frequency. */ sync: SyncConfig; /** Data retention. */ retention: RetentionConfig; /** Priority (higher = more important). Range: 0-100. Default: 50. */ priority: number; /** Whether this policy is currently active. */ enabled: boolean; } // ============================================ // Effective policy: the resolved result for a single DID // ============================================ /** * The effective (merged) policy for a single DID after evaluating * all matching policies. */ export interface EffectivePolicy { /** The DID this effective policy is for. */ did: string; /** IDs of all policies that contributed to this result. */ sourcePolicyIds: string[]; /** Merged replication goals. */ replication: ReplicationGoals; /** Merged sync config. */ sync: SyncConfig; /** Merged retention config. */ retention: RetentionConfig; /** Highest priority among matching policies. */ priority: number; /** Whether replication is enabled (at least one enabled policy matches). */ shouldReplicate: boolean; } // ============================================ // Policy set: what gets loaded from config / stored on disk // ============================================ export interface PolicySet { /** Schema version for forward compatibility. */ version: 1; /** All defined policies. */ policies: Policy[]; } // ============================================ // Defaults // ============================================ export const DEFAULT_REPLICATION: ReplicationGoals = { minCopies: 1, }; export const DEFAULT_SYNC: SyncConfig = { intervalSec: 300, }; export const DEFAULT_RETENTION: RetentionConfig = { maxAgeSec: 0, keepHistory: false, }; export const DEFAULT_PRIORITY = 50; // ============================================ // Lifecycle types for persistent policies // ============================================ /** Policy lifecycle state. */ export type PolicyState = "proposed" | "active" | "suspended" | "terminated" | "purged"; /** What kind of policy this is. */ export type PolicyType = "reciprocal" | "archive" | "config" | "saas" | "group"; /** How this policy was created. */ export type PolicySource = "config" | "file" | "offer" | "manual" | "api"; /** Consent status for the replication relationship. */ export type ConsentStatus = "reciprocal" | "consented" | "unconsented" | "revoked"; /** * A policy with lifecycle metadata for persistence. * Extends the base Policy with type, state, source, consent, and timestamps. */ export interface StoredPolicy extends Policy { /** What kind of policy this is. */ type: PolicyType; /** Current lifecycle state. */ state: PolicyState; /** How this policy was created. */ source: PolicySource; /** Consent status. */ consent: ConsentStatus; /** Who/what created this policy. */ createdBy: string; /** The counterparty DID (for reciprocal/P2P policies). */ counterpartyDid?: string; /** URI of the local offer record (for P2P policies). */ localOfferUri?: string; /** URI of the remote offer record (for P2P policies). */ remoteOfferUri?: string; /** When this policy was created. */ createdAt: string; /** When this policy was activated. */ activatedAt: string | null; /** When this policy was suspended. */ suspendedAt: string | null; /** When this policy was terminated. */ terminatedAt: string | null; /** When this policy expires (null = never). */ expiresAt: string | null; } /** * Create a StoredPolicy from a base Policy with lifecycle metadata. * `type` is required (no inference from ID prefix). */ export function toStoredPolicy( policy: Policy, type: PolicyType, overrides?: Partial>, ): StoredPolicy { const now = new Date().toISOString(); return { ...policy, type, state: overrides?.state ?? "active", source: overrides?.source ?? "manual", consent: overrides?.consent ?? "unconsented", createdBy: overrides?.createdBy ?? "system", counterpartyDid: overrides?.counterpartyDid, localOfferUri: overrides?.localOfferUri, remoteOfferUri: overrides?.remoteOfferUri, createdAt: overrides?.createdAt ?? now, activatedAt: overrides?.activatedAt ?? (overrides?.state === "active" || !overrides?.state ? now : null), suspendedAt: overrides?.suspendedAt ?? null, terminatedAt: overrides?.terminatedAt ?? null, expiresAt: overrides?.expiresAt ?? null, }; }