@lunora/privacy 0.0.0-stage → 1.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,368 @@
1
+ import { b as RetainDirective, R as RetentionRule, P as PrivacyPreset } from "./rules.d-DJa3SIb3.mjs";
2
+ /** One problem the coverage check found. */
3
+ interface CoverageProblem {
4
+ /** `unclassified`: no dataset and no exclusion. `excluded_without_reason`: excluded with a blank reason. */
5
+ readonly code: "excluded_without_reason" | "unclassified";
6
+ readonly table: string;
7
+ }
8
+ /** The better-auth tables `authDataset()` covers when no table is renamed. */
9
+ declare const AUTH_DEFAULT_TABLES: readonly ["account", "apikey", "member", "passkey", "session", "teamMember", "twoFactor", "user"];
10
+ /**
11
+ * Which GDPR articles a dataset's export falls under. One export serves both
12
+ * Art. 15 (access, every origin) and Art. 20 (portability, data the subject
13
+ * provided, on consent or contract, by automated means).
14
+ */
15
+ /** Where a dataset's data came from. */
16
+ type DataOrigin = "provided" | "observed" | "derived" | "inferred";
17
+ /** The Art. 6(1) basis the data is processed on. */
18
+ type LegalBasis = "consent" | "contract" | "legal_obligation" | "legitimate_interest";
19
+ /** When a dataset is handled. Erasure runs `quiesce` first, so nothing writes behind it. */
20
+ type DatasetPhase = "quiesce" | "erase" | "tombstone";
21
+ /** Which shard holds the subject's rows. */
22
+ type DatasetPlacement = "subject" | "root" | "global";
23
+ /** What erasure does to one table. */
24
+ type TableErasure = "delete" | "anonymise" | RetainDirective;
25
+ /** How one table in a dataset is treated. */
26
+ interface TableSpec {
27
+ /** Defaults to `delete`. */
28
+ readonly erase?: TableErasure;
29
+ /** Columns left out of the export, such as internal bookkeeping. */
30
+ readonly omit?: ReadonlyArray<string>;
31
+ /** Overrides the dataset's origin for this table. */
32
+ readonly origin?: DataOrigin;
33
+ }
34
+ /** The description a module writes for its dataset. */
35
+ interface DatasetSpec {
36
+ /**
37
+ * Object-storage files the subject owns. `prefix` returns the key prefix of one subject's
38
+ * files; it must be specific to that subject, because erasure deletes everything under it.
39
+ */
40
+ readonly files?: {
41
+ readonly prefix: (subjectId: string) => string;
42
+ };
43
+ readonly legalBasis: LegalBasis;
44
+ readonly origin: DataOrigin;
45
+ readonly phase?: DatasetPhase;
46
+ readonly placement?: DatasetPlacement;
47
+ readonly purpose: string;
48
+ readonly recipients?: ReadonlyArray<string>;
49
+ readonly tables: Readonly<Record<string, TableSpec>>;
50
+ }
51
+ /** A validated dataset, ready for the coverage check and the workflows. */
52
+ interface Dataset extends DatasetSpec {
53
+ readonly id: string;
54
+ readonly phase: DatasetPhase;
55
+ readonly placement: DatasetPlacement;
56
+ readonly recipients: ReadonlyArray<string>;
57
+ /** The tables this dataset covers, by bare name. */
58
+ readonly tableNames: ReadonlyArray<string>;
59
+ }
60
+ /**
61
+ * Validate and freeze a dataset. Refuses an empty purpose, a dataset with no
62
+ * tables, and an id that is not a plain identifier, so the Art. 30 draft and the
63
+ * workflow step names stay well formed.
64
+ */
65
+ declare const defineDataset: (id: string, spec: DatasetSpec) => Dataset;
66
+ /** Rows per table per call. A drain step never erases more, so one step stays inside the platform's limits. */
67
+ declare const ERASE_BATCH = 100;
68
+ /** What happens to a row when its table is erased. */
69
+ type Disposition = {
70
+ readonly kind: "delete";
71
+ } | {
72
+ readonly kind: "anonymise";
73
+ } | {
74
+ readonly kind: "keep";
75
+ readonly until: Date;
76
+ };
77
+ /** The package's verdict for one row, which the app's `eraseBatch` applies. */
78
+ type RowDisposition = (dataset: Dataset, table: string, row: {
79
+ readonly [column: string]: unknown;
80
+ }, now: Date) => Disposition;
81
+ /**
82
+ * Bind the presets' retention rules into the verdict `eraseBatch` receives. A table with no
83
+ * `erase` is deleted, `delete` and `anonymise` apply as written, and a `retain` directive keeps
84
+ * the row while its rule's period is running.
85
+ */
86
+ declare const dispositionFor: (rules: ReadonlyMap<string, RetentionRule>) => RowDisposition;
87
+ /**
88
+ * The statuses a privacy request moves through, and which of them are terminal.
89
+ *
90
+ * `@lunora/privacy` stores them and the Studio's Privacy requests page renders them.
91
+ * The Studio must not depend on the runtime package, so both read the list from here
92
+ * and the bundler inlines it into each. A status added here reaches both, and the
93
+ * Studio's exhaustive badge map fails to compile until it has a variant.
94
+ */
95
+ /** Every status, in lifecycle order. */
96
+ declare const REQUEST_STATUSES: readonly ["received", "grace", "processing", "ready", "completed", "failed", "cancelled", "expired"];
97
+ type RequestStatus = (typeof REQUEST_STATUSES)[number];
98
+ type RequestType = "erasure" | "export";
99
+ declare const isTerminal: (status: RequestStatus) => boolean;
100
+ /** A request as the service sees it. */
101
+ interface RequestRecord {
102
+ readonly dueAt: Date;
103
+ readonly erasureGraceUntil?: Date;
104
+ /** When the archive stops being downloadable. */
105
+ readonly expiresAt?: Date;
106
+ readonly extendedDueAt?: Date;
107
+ readonly id: string;
108
+ readonly reason?: string;
109
+ readonly receivedAt: Date;
110
+ readonly status: RequestStatus;
111
+ /** Where the finished archive is kept, once the request is ready. */
112
+ readonly storageKey?: string;
113
+ readonly subjectId: string;
114
+ readonly type: RequestType;
115
+ }
116
+ /** The storage the service needs. The app implements it over `ctx.db`. */
117
+ interface RequestStore {
118
+ addEvent: (requestId: string, at: Date, kind: string, detail?: string) => Promise<void>;
119
+ /**
120
+ * Apply the patch only if the request still holds `from`, and report whether it did. The host must
121
+ * make the check and the write one step, so two callers that both read `from` cannot both win.
122
+ */
123
+ claim: (id: string, from: RequestStatus, patch: Partial<Omit<RequestRecord, "id">>) => Promise<boolean>;
124
+ /** The subject's request of this type that is not yet terminal, if any. */
125
+ findOpen: (subjectId: string, type: RequestType) => Promise<RequestRecord | undefined>;
126
+ get: (id: string) => Promise<RequestRecord | undefined>;
127
+ insert: (record: RequestRecord) => Promise<void>;
128
+ /** Every request of the subject, in every status. Erasure deletes the files each one wrote. */
129
+ listForSubject: (subjectId: string) => Promise<ReadonlyArray<RequestRecord>>;
130
+ /** Every request that is not yet terminal, for the deadline sweep. */
131
+ listOpen: () => Promise<ReadonlyArray<RequestRecord>>;
132
+ update: (id: string, patch: Partial<Omit<RequestRecord, "id">>) => Promise<void>;
133
+ }
134
+ /** Inputs that come from the caller and the app's settings. */
135
+ interface ErasureSettings {
136
+ readonly drainBudgetDays: number;
137
+ /** The sign-in window an erasure needs, in milliseconds. Account deletion uses 24 hours. */
138
+ readonly freshAgeMs: number;
139
+ readonly graceDays: number;
140
+ }
141
+ /**
142
+ * The storage the tombstones need. Keep it outside the database a restore rolls back: a tombstone
143
+ * restored away with the data it guards would let the restore bring the erased subject back.
144
+ */
145
+ interface TombstonePort {
146
+ /**
147
+ * Record that the subject was erased at this time. Must be an idempotent upsert keyed by `key`:
148
+ * a workflow step that fails after the write is retried and writes the same tombstone again, and a
149
+ * second row for one key must not appear.
150
+ */
151
+ readonly add: (key: string, erasedAt: Date) => Promise<void>;
152
+ /** Every tombstone key on record. */
153
+ readonly keys: () => Promise<ReadonlySet<string>>;
154
+ }
155
+ /** The app's data, one dataset at a time. The app implements it over its tables. */
156
+ interface DataPort {
157
+ /** The subject's rows for each table of the dataset, keyed by table name. */
158
+ readonly collect: (dataset: Dataset, subjectId: string) => Promise<Readonly<Record<string, ReadonlyArray<Readonly<Record<string, unknown>>>>>>;
159
+ /**
160
+ * Erase one batch of the dataset's rows for the subject. The package decides and the app
161
+ * writes: call `disposition` for each row, then delete a `delete` row, anonymise an
162
+ * `anonymise` row, and move a `keep` row into the archive table. A kept row must not be
163
+ * selected again, or the drain never finishes.
164
+ */
165
+ readonly eraseBatch: (dataset: Dataset, subjectId: string, now: Date, disposition: RowDisposition) => Promise<{
166
+ readonly hasMore: boolean;
167
+ }>;
168
+ /**
169
+ * Rows still present per table after erasure, for the verify step. Two kinds of row may be
170
+ * counted and are excluded from the verdict by the package: a row a retention rule still keeps
171
+ * (verify judges each one again through `collect`), and every row of an `anonymise` table, which
172
+ * stays in place once anonymised. Prefer not to count anonymised rows at all; the package cannot
173
+ * tell an anonymised row from one `eraseBatch` missed, so it does not hold them against the erasure.
174
+ */
175
+ readonly remaining: (dataset: Dataset, subjectId: string) => Promise<Readonly<Record<string, number>>>;
176
+ }
177
+ /**
178
+ * Where a request's files are kept between steps and in the end. `put` must key a request's
179
+ * files under `${requestId}/`, so listing that prefix finds every file the request wrote.
180
+ */
181
+ interface StorePort {
182
+ /** Remove one stored object. Removing a key that is not there is not an error. */
183
+ readonly delete: (key: string) => Promise<void>;
184
+ /** The keys of every stored object whose key starts with the prefix. */
185
+ readonly list: (prefix: string) => Promise<ReadonlyArray<string>>;
186
+ readonly put: (requestId: string, file: {
187
+ readonly data: Uint8Array | string;
188
+ readonly name: string;
189
+ }) => Promise<string>;
190
+ /** The text of a file written earlier in the same request, or `undefined` if it is not there. */
191
+ readonly read: (requestId: string, name: string) => Promise<string | undefined>;
192
+ /** A short-lived link to a stored file. The caller decides the lifetime, and the link is checked by the host. */
193
+ readonly signedUrl: (key: string, ttlSeconds: number) => Promise<string>;
194
+ }
195
+ /**
196
+ * The archive of rows erasure kept under a retention rule (`privacy_archive`, or the app's own
197
+ * table). The app writes a `keep` row into it with the `until` its disposition carries as
198
+ * `retainUntil`; the sweep purges it once that date has passed.
199
+ */
200
+ interface ArchivePort {
201
+ /**
202
+ * Delete up to `ERASE_BATCH` archived rows whose `retainUntil` is before `before`, and say how
203
+ * many it deleted and whether more may remain.
204
+ */
205
+ readonly purgeBatch: (before: Date) => Promise<{
206
+ readonly hasMore: boolean;
207
+ readonly purged: number;
208
+ }>;
209
+ }
210
+ /** The ports one invocation binds: the app's data, storage, requests and clock. */
211
+ interface PrivacyPorts {
212
+ /** Where kept rows wait out their retention period. Required for `admin.sweep` once a dataset uses `retain`. */
213
+ readonly archive?: ArchivePort;
214
+ readonly data: DataPort;
215
+ readonly files: StorePort;
216
+ readonly newId: () => string;
217
+ readonly now: () => Date;
218
+ readonly store: RequestStore;
219
+ /**
220
+ * Where erased subjects are tombstoned, so a restored backup can be erased again. An erasure and
221
+ * `restore` refuse to run without it; a call that only exports or reads may leave it out.
222
+ */
223
+ readonly tombstones?: TombstonePort;
224
+ }
225
+ /**
226
+ * How a run executes its named steps. A durable host memoises each `do` by name and retries a step
227
+ * that throws, so every step result is JSON-safe. `sleepUntil` is the host's durable sleep; the
228
+ * one-call path has none, so it refuses an erasure that is still inside its grace window.
229
+ */
230
+ interface StepRunner {
231
+ readonly do: <T>(name: string, run: () => Promise<T>) => Promise<T>;
232
+ readonly sleepUntil?: (name: string, when: Date) => Promise<void>;
233
+ }
234
+ /** The outcome of an export run. */
235
+ interface ExportOutcome {
236
+ readonly archiveKey?: string;
237
+ readonly status: RequestStatus;
238
+ }
239
+ /** The outcome of an erasure run. */
240
+ interface ErasureOutcome {
241
+ readonly leftover: ReadonlyArray<string>;
242
+ readonly status: RequestStatus;
243
+ }
244
+ /** The app's erasure settings. An unset field takes the decided default (D3 and the sign-in window). */
245
+ type ErasureOptions = Partial<ErasureSettings>;
246
+ /**
247
+ * The authenticated subject of a call. The app resolves it from its session, never from a request
248
+ * argument, and passes it as the second argument of `privacyFor(ctx, identity)`, so a caller cannot
249
+ * act on another subject's data by naming them. An operator call, a cron or a workflow passes none.
250
+ */
251
+ interface PrivacyIdentity {
252
+ readonly sessionAgeMs: number;
253
+ readonly subjectId: string;
254
+ }
255
+ /**
256
+ * Everything `definePrivacy` needs from the app. The ports depend on the invocation: a
257
+ * mutation's `ctx.db` exists only inside that call, so `ports` runs once per call with
258
+ * the caller's context.
259
+ */
260
+ interface PrivacyConfig<C> {
261
+ readonly datasets: ReadonlyArray<Dataset>;
262
+ readonly erasure?: ErasureOptions;
263
+ /** Tables left out of the export and erasure, each with the reason the reviewer reads. */
264
+ readonly exclude?: Readonly<Record<string, string>>;
265
+ readonly ports: (context: C) => PrivacyPorts;
266
+ readonly presets?: ReadonlyArray<PrivacyPreset>;
267
+ /** The identifier the archive shows for the subject. Defaults to the subject id itself. */
268
+ readonly subjectKey?: (subjectId: string) => string;
269
+ /**
270
+ * The HMAC secret tombstone keys are derived with. Every erasure and `restore` refuse to run
271
+ * without it and without a `tombstones` port. Keep it in a secret binding, and keep it stable,
272
+ * because a new secret no longer matches the tombstones already written.
273
+ */
274
+ readonly tombstoneSecret?: string;
275
+ }
276
+ /**
277
+ * Binds an invocation's context, and the caller's identity when the caller is a data subject, into
278
+ * the API: `privacyFor(ctx, identity).requestExport()`. The subject methods refuse a call bound
279
+ * without an identity; the operator methods and the workflows never read it.
280
+ */
281
+ type PrivacyFor<C> = (context: C, identity?: PrivacyIdentity) => Privacy;
282
+ /** The step surface a Workflow handler receives. Matches the native `WorkflowStep` methods the package uses. */
283
+ interface PrivacyWorkflowContext {
284
+ readonly params: {
285
+ readonly requestId: string;
286
+ };
287
+ readonly step: Required<StepRunner>;
288
+ }
289
+ /** The bound API. */
290
+ interface Privacy {
291
+ /** Operator views and actions. Each one is for the operator's own console, not the subject. */
292
+ readonly admin: {
293
+ /**
294
+ * Extend a request's deadline by the policy's further months (Art. 12(3) sentence 2). Extension
295
+ * is the controller's decision, so it is an operator action: once per request, within the first
296
+ * period, with the reasons the controller must give the subject.
297
+ */
298
+ readonly extend: (requestId: string, reason: string) => Promise<RequestRecord>;
299
+ /** Every request that is not yet terminal. */
300
+ readonly open: () => Promise<ReadonlyArray<RequestRecord>>;
301
+ /**
302
+ * Retry a failed export or erasure; any other status is refused. The Art. 12(3) clock keeps
303
+ * running throughout. An export runs again in this call and returns `ran` with its outcome. An
304
+ * erasure is moved to `processing` in this call and returns `restart`: the app then starts its
305
+ * erasure workflow for the request, in the same mutation, and the workflow resumes it. Because
306
+ * the request leaves `failed` here, a second retry is refused rather than starting a second run.
307
+ */
308
+ readonly retry: (requestId: string) => Promise<RetryResult>;
309
+ /**
310
+ * The housekeeping sweep. Run it from a cron. It expires every ready export past its download
311
+ * window (its files go first, then it is marked `expired`), and it purges every archived row
312
+ * whose retention period has ended through the `archive` port. Returns the expired requests
313
+ * and how many archived rows it purged.
314
+ */
315
+ readonly sweep: () => Promise<SweepResult>;
316
+ };
317
+ readonly cancelRequest: (input: {
318
+ requestId: string;
319
+ }) => Promise<RequestRecord>;
320
+ readonly coverage: (tables: ReadonlyArray<string>, exclusions?: Readonly<Record<string, string>>) => ReadonlyArray<CoverageProblem>;
321
+ /** The subject's download link for a finished export. Refused for anyone but the subject, and once expired. */
322
+ readonly downloadUrl: (input: {
323
+ requestId: string;
324
+ }) => Promise<string>;
325
+ /** Every request of the subject, in every status, newest first. A finished erasure's requests no longer carry the subject's id. */
326
+ readonly myRequests: () => Promise<ReadonlyArray<RequestRecord>>;
327
+ readonly requestErasure: () => Promise<RequestRecord>;
328
+ readonly requestExport: () => Promise<RequestRecord>;
329
+ /** Erase again any restored subject that was erased after the backup. Returns the subject ids it re-erased. */
330
+ readonly restore: (input: {
331
+ restoredSubjectIds: ReadonlyArray<string>;
332
+ }) => Promise<ReadonlyArray<string>>;
333
+ readonly runErasure: (requestId: string) => Promise<ErasureOutcome>;
334
+ readonly runExport: (requestId: string) => Promise<ExportOutcome>;
335
+ /** The deadline sweep: requests due within seven days, and those already overdue. Run it from a cron. */
336
+ readonly tick: () => Promise<{
337
+ readonly approaching: ReadonlyArray<RequestRecord>;
338
+ readonly overdue: ReadonlyArray<RequestRecord>;
339
+ }>;
340
+ /** The durable handlers the app registers with `defineWorkflow`. Each step is memoised by name. */
341
+ readonly workflows: {
342
+ readonly erasureHandler: (context: PrivacyWorkflowContext) => Promise<void>;
343
+ readonly exportHandler: (context: PrivacyWorkflowContext) => Promise<void>;
344
+ };
345
+ }
346
+ /**
347
+ * What `admin.retry` did: an export ran again (`ran`), or an erasure was moved out of `failed` and the
348
+ * app must now start its erasure workflow for `requestId` (`restart`).
349
+ */
350
+ type RetryResult = {
351
+ readonly kind: "ran";
352
+ readonly outcome: ExportOutcome;
353
+ } | {
354
+ readonly kind: "restart";
355
+ readonly requestId: string;
356
+ };
357
+ /** What `admin.sweep` did. */
358
+ interface SweepResult {
359
+ readonly expired: ReadonlyArray<RequestRecord>;
360
+ /** Archived rows deleted because their retention period has ended. */
361
+ readonly purged: number;
362
+ }
363
+ /**
364
+ * Bind the settings into a factory. Pure construction: nothing runs until a method is
365
+ * called, and each call of the factory binds the ports for that invocation.
366
+ */
367
+ declare const definePrivacy: <C>(config: PrivacyConfig<C>) => PrivacyFor<C>;
368
+ export { AUTH_DEFAULT_TABLES as A, isTerminal as B, CoverageProblem as C, Dataset as D, ERASE_BATCH as E, LegalBasis as L, PrivacyConfig as P, RequestStatus as R, StorePort as S, TableErasure as T, PrivacyPorts as a, RequestType as b, RequestRecord as c, ArchivePort as d, DataOrigin as e, DataPort as f, DatasetPhase as g, DatasetPlacement as h, DatasetSpec as i, Disposition as j, ErasureOptions as k, ErasureOutcome as l, ExportOutcome as m, Privacy as n, PrivacyFor as o, PrivacyIdentity as p, PrivacyWorkflowContext as q, RequestStore as r, RetryResult as s, RowDisposition as t, SweepResult as u, TableSpec as v, TombstonePort as w, defineDataset as x, definePrivacy as y, dispositionFor as z };