atproto user agency toolkit for individuals and groups
1/**
2 * Type definitions for the declarative policy engine.
3 *
4 * Policies are plain data (JSON-serializable), deterministic, and
5 * transport-agnostic — they operate on atproto accounts (DIDs), not raw blocks.
6 */
7
8// ============================================
9// Target: which accounts a policy applies to
10// ============================================
11
12/** Match all serviced accounts. */
13export interface TargetAll {
14 type: "all";
15}
16
17/** Match an explicit list of DIDs. */
18export interface TargetList {
19 type: "list";
20 dids: string[];
21}
22
23/** Match DIDs whose string representation matches a pattern (substring or prefix). */
24export interface TargetPattern {
25 type: "pattern";
26 /** A prefix string to match against DIDs, e.g. "did:plc:" or "did:web:example.com". */
27 prefix: string;
28}
29
30export type PolicyTarget = TargetAll | TargetList | TargetPattern;
31
32// ============================================
33// Replication goals
34// ============================================
35
36export interface ReplicationGoals {
37 /** Minimum number of copies across peers (including this node). Default: 1. */
38 minCopies: number;
39 /** Preferred peer DIDs to replicate with. Optional. */
40 preferredPeers?: string[];
41}
42
43// ============================================
44// Sync configuration
45// ============================================
46
47export interface SyncConfig {
48 /** How often to sync, in seconds. Default: 300 (5 minutes). */
49 intervalSec: number;
50}
51
52// ============================================
53// Retention configuration
54// ============================================
55
56export interface RetentionConfig {
57 /** How long to keep data, in seconds. 0 = forever. Default: 0. */
58 maxAgeSec: number;
59 /** Whether to keep historical revisions. Default: false. */
60 keepHistory: boolean;
61}
62
63// ============================================
64// Policy definition
65// ============================================
66
67export interface Policy {
68 /** Unique identifier for this policy. */
69 id: string;
70 /** Human-readable name. */
71 name: string;
72 /** Which accounts this policy applies to. */
73 target: PolicyTarget;
74 /** Replication goals. */
75 replication: ReplicationGoals;
76 /** Sync frequency. */
77 sync: SyncConfig;
78 /** Data retention. */
79 retention: RetentionConfig;
80 /** Priority (higher = more important). Range: 0-100. Default: 50. */
81 priority: number;
82 /** Whether this policy is currently active. */
83 enabled: boolean;
84}
85
86// ============================================
87// Effective policy: the resolved result for a single DID
88// ============================================
89
90/**
91 * The effective (merged) policy for a single DID after evaluating
92 * all matching policies.
93 */
94export interface EffectivePolicy {
95 /** The DID this effective policy is for. */
96 did: string;
97 /** IDs of all policies that contributed to this result. */
98 sourcePolicyIds: string[];
99 /** Merged replication goals. */
100 replication: ReplicationGoals;
101 /** Merged sync config. */
102 sync: SyncConfig;
103 /** Merged retention config. */
104 retention: RetentionConfig;
105 /** Highest priority among matching policies. */
106 priority: number;
107 /** Whether replication is enabled (at least one enabled policy matches). */
108 shouldReplicate: boolean;
109}
110
111// ============================================
112// Policy set: what gets loaded from config / stored on disk
113// ============================================
114
115export interface PolicySet {
116 /** Schema version for forward compatibility. */
117 version: 1;
118 /** All defined policies. */
119 policies: Policy[];
120}
121
122// ============================================
123// Defaults
124// ============================================
125
126export const DEFAULT_REPLICATION: ReplicationGoals = {
127 minCopies: 1,
128};
129
130export const DEFAULT_SYNC: SyncConfig = {
131 intervalSec: 300,
132};
133
134export const DEFAULT_RETENTION: RetentionConfig = {
135 maxAgeSec: 0,
136 keepHistory: false,
137};
138
139export const DEFAULT_PRIORITY = 50;
140
141// ============================================
142// Lifecycle types for persistent policies
143// ============================================
144
145/** Policy lifecycle state. */
146export type PolicyState = "proposed" | "active" | "suspended" | "terminated" | "purged";
147
148/** What kind of policy this is. */
149export type PolicyType = "reciprocal" | "archive" | "config" | "saas" | "group";
150
151/** How this policy was created. */
152export type PolicySource = "config" | "file" | "offer" | "manual" | "api";
153
154/** Consent status for the replication relationship. */
155export type ConsentStatus = "reciprocal" | "consented" | "unconsented" | "revoked";
156
157/**
158 * A policy with lifecycle metadata for persistence.
159 * Extends the base Policy with type, state, source, consent, and timestamps.
160 */
161export interface StoredPolicy extends Policy {
162 /** What kind of policy this is. */
163 type: PolicyType;
164 /** Current lifecycle state. */
165 state: PolicyState;
166 /** How this policy was created. */
167 source: PolicySource;
168 /** Consent status. */
169 consent: ConsentStatus;
170 /** Who/what created this policy. */
171 createdBy: string;
172 /** The counterparty DID (for reciprocal/P2P policies). */
173 counterpartyDid?: string;
174 /** URI of the local offer record (for P2P policies). */
175 localOfferUri?: string;
176 /** URI of the remote offer record (for P2P policies). */
177 remoteOfferUri?: string;
178 /** When this policy was created. */
179 createdAt: string;
180 /** When this policy was activated. */
181 activatedAt: string | null;
182 /** When this policy was suspended. */
183 suspendedAt: string | null;
184 /** When this policy was terminated. */
185 terminatedAt: string | null;
186 /** When this policy expires (null = never). */
187 expiresAt: string | null;
188}
189
190/**
191 * Create a StoredPolicy from a base Policy with lifecycle metadata.
192 * `type` is required (no inference from ID prefix).
193 */
194export function toStoredPolicy(
195 policy: Policy,
196 type: PolicyType,
197 overrides?: Partial<Omit<StoredPolicy, keyof Policy | "type">>,
198): StoredPolicy {
199 const now = new Date().toISOString();
200 return {
201 ...policy,
202 type,
203 state: overrides?.state ?? "active",
204 source: overrides?.source ?? "manual",
205 consent: overrides?.consent ?? "unconsented",
206 createdBy: overrides?.createdBy ?? "system",
207 counterpartyDid: overrides?.counterpartyDid,
208 localOfferUri: overrides?.localOfferUri,
209 remoteOfferUri: overrides?.remoteOfferUri,
210 createdAt: overrides?.createdAt ?? now,
211 activatedAt: overrides?.activatedAt ?? (overrides?.state === "active" || !overrides?.state ? now : null),
212 suspendedAt: overrides?.suspendedAt ?? null,
213 terminatedAt: overrides?.terminatedAt ?? null,
214 expiresAt: overrides?.expiresAt ?? null,
215 };
216}