@voltro/plugin-governance 0.85.0 → 0.87.0

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.
package/dist/index.js CHANGED
@@ -1,418 +1,94 @@
1
1
  import { consentDescriptor as e, eraseDescriptor as t, exportDescriptor as n, governanceRpcClientImports as r, hasConsentDescriptor as i } from "./rpc.js";
2
- import { Context as a, Effect as o, Layer as s } from "effect";
3
- import { ADMIN_SCOPE as c, definePlugin as l, pluginInstanceName as u, requireScope as d } from "@voltro/protocol";
4
- import { deriveKey as f, makeAesCipher as p, makeFieldCipher as m, makeFieldCipher as ee, resolveSecret as h, setFieldCipher as g, setFieldCipher as _ } from "@voltro/runtime";
5
- import { DEFAULT_SUBJECT_GRAPH_DEPTH as v, ENCRYPTED_PREFIX as y, and as b, boolean as x, deriveSubjectGraph as S, deriveSubjectGraph as te, eq as C, formatSubjectGraph as w, getTable as T, gt as E, id as D, inSet as O, isEncrypted as k, lt as A, table as j, text as M } from "@voltro/database";
6
- //#region src/store.ts
7
- var N = (e) => {
8
- let t = e, n = (e, t) => ({
9
- table: void 0,
10
- predicate: void 0,
11
- order: [{
12
- column: "id",
13
- direction: "asc"
14
- }],
15
- take: e,
16
- skip: t,
17
- projection: void 0
18
- });
19
- return {
20
- all: (e) => t.query({
21
- ...n(void 0, void 0),
22
- table: e
23
- }),
24
- where: async (e, r, i) => i.length === 0 ? [] : t.query({
25
- ...n(void 0, void 0),
26
- table: e,
27
- predicate: {
28
- column: r,
29
- op: "in",
30
- value: [...i]
31
- }
32
- }),
33
- page: (e, r, i) => t.query({
34
- ...n(r, i),
35
- table: e
36
- }),
37
- scan: (e, r) => {
38
- if (r.column !== void 0 && r.values?.length === 0) return Promise.resolve([]);
39
- let i = [];
40
- if (r.after !== void 0 && i.push(E("id", r.after)), r.column !== void 0) {
41
- if (r.values === void 0) throw Error("governance scan needs values for its subject column");
42
- i.push(O(r.column, r.values));
43
- }
44
- return r.expired && T(e)?.fields[r.expired.column]?.type === "timestamp" && i.push(A(r.expired.column, r.expired.before)), t.query({
45
- ...n(r.limit, void 0),
46
- table: e,
47
- predicate: i.length ? b(...i) : void 0,
48
- projection: r.projection
49
- });
50
- },
51
- ...t.deleteMany && t.updateMany ? { writeMany: async (e, n, r) => {
52
- if (n.length === 0) return 0;
53
- let i = O("id", n);
54
- return r === void 0 ? t.deleteMany(e, { where: i }) : t.updateMany(e, r, { where: i });
55
- } } : {},
56
- delete: async (e, n) => {
57
- await (t.hardDelete ?? t.delete).call(t, e, n);
58
- },
59
- update: async (e, n, r) => {
60
- await t.update(e, n, r);
61
- }
62
- };
63
- }, P = (e = {}) => {
64
- let t = new Map(Object.entries(e).map(([e, t]) => [e, t.map((e) => ({ ...e }))])), n = (e) => {
65
- let n = t.get(e);
66
- return n || (n = [], t.set(e, n)), n;
67
- };
68
- return {
69
- all: async (e) => [...n(e)],
70
- where: async (e, t, r) => {
71
- if (r.length === 0) return [];
72
- let i = new Set(r.map((e) => String(e)));
73
- return n(e).filter((e) => i.has(String(e[t]))).map((e) => ({ ...e }));
74
- },
75
- page: async (e, t, r) => n(e).slice(r, r + t).map((e) => ({ ...e })),
76
- scan: async (e, t) => {
77
- let r = t.values === void 0 ? void 0 : new Set(t.values.map(String));
78
- return n(e).filter((e) => (t.after === void 0 || String(e.id) > String(t.after)) && (t.column === void 0 || r?.has(String(e[t.column])))).sort((e, t) => String(e.id) < String(t.id) ? -1 : +(String(e.id) > String(t.id))).slice(0, t.limit).map((e) => t.projection ? Object.fromEntries(t.projection.map((t) => [t, e[t]])) : { ...e });
79
- },
80
- writeMany: async (e, r, i) => {
81
- let a = new Set(r), o = n(e), s = 0;
82
- if (i === void 0) {
83
- let n = o.filter((e) => !a.has(String(e.id)) || (s++, !1));
84
- t.set(e, n);
85
- } else for (let e = 0; e < o.length; e++) a.has(String(o[e].id)) && (o[e] = {
86
- ...o[e],
87
- ...i
88
- }, s++);
89
- return s;
90
- },
91
- delete: async (e, t) => {
92
- let r = n(e), i = r.findIndex((e) => String(e.id) === t);
93
- i !== -1 && r.splice(i, 1);
94
- },
95
- update: async (e, t, r) => {
96
- let i = n(e), a = i.findIndex((e) => String(e.id) === t);
97
- a !== -1 && (i[a] = {
98
- ...i[a],
99
- ...r
100
- });
101
- },
102
- dump: () => Object.fromEntries([...t.entries()].map(([e, t]) => [e, t.map((e) => ({ ...e }))]))
103
- };
104
- }, F = async (e, t, n, r) => {
105
- let i = 0;
106
- if (e.writeMany) {
107
- for (let a = 0; a < n.length; a += 500) i += await e.writeMany(t, n.slice(a, a + 500), r);
108
- return i;
109
- }
110
- let a = 0, o = !1, s = await Promise.allSettled(Array.from({ length: Math.min(8, n.length) }, async () => {
111
- for (; !o && a < n.length;) {
112
- let s = n[a++];
113
- try {
114
- r === void 0 ? await e.delete(t, s) : await e.update(t, s, r), i++;
115
- } catch (e) {
116
- throw o = !0, e;
117
- }
118
- }
119
- }));
120
- for (let e of s) if (e.status === "rejected") throw e.reason;
121
- return i;
122
- }, I = 500, L = 5e4, R = (e, t) => {
123
- let n = [];
124
- for (let r = 0; r < e.length; r += t) n.push(e.slice(r, r + t));
125
- return n;
126
- }, z = (e, t) => e.table === t && e.subjectField === "id" ? {
127
- table: e.table,
128
- hops: [],
129
- depth: 0,
130
- via: "declared"
131
- } : {
132
- table: e.table,
133
- hops: [{
134
- table: e.table,
135
- foreignKey: e.subjectField,
136
- parentKey: "id"
137
- }],
138
- depth: 1,
139
- via: "declared"
140
- }, B = async (e, t, n, r = L) => {
141
- let i = /* @__PURE__ */ new Map();
142
- if (!Number.isSafeInteger(r) || r < 1) throw Error("subject rowCap must be a positive safe integer");
143
- let a = /* @__PURE__ */ new Map();
144
- for (let e of t) {
145
- let t = e.hops.length ? e.hops : [{
146
- table: e.table,
147
- foreignKey: "id",
148
- parentKey: "id"
149
- }], n = a;
150
- for (let e of t) {
151
- let t = JSON.stringify([
152
- e.table,
153
- e.foreignKey,
154
- e.parentKey
155
- ]), r = n.get(t);
156
- r || (r = {
157
- hop: e,
158
- children: /* @__PURE__ */ new Map()
159
- }, n.set(t, r)), n = r.children;
160
- }
161
- }
162
- let o = (e) => {
163
- let t = i.get(e);
164
- return t || (t = {
165
- rows: /* @__PURE__ */ new Map(),
166
- truncated: !1
167
- }, i.set(e, t)), t;
168
- }, s = (e, t) => {
169
- let n = o(e);
170
- for (let e of t) {
171
- let t = String(e.id ?? JSON.stringify(e));
172
- if (!n.rows.has(t)) {
173
- if (n.rows.size >= r) {
174
- n.truncated = !0;
175
- continue;
176
- }
177
- n.rows.set(t, e);
178
- }
179
- }
180
- }, c = async (t, n) => {
181
- let { hop: i, children: a } = t, l = o(i.table);
182
- for (let t of R(n, 500)) {
183
- let n;
184
- for (;;) {
185
- if (a.size === 0 && l.truncated) return;
186
- let o = a.size ? 500 : Math.min(500, r - l.rows.size + 1), u = e.scan ? await e.scan(i.table, {
187
- column: i.foreignKey,
188
- values: t,
189
- limit: o,
190
- ...n === void 0 ? {} : { after: n }
191
- }) : await e.where(i.table, i.foreignKey, t);
192
- if (s(i.table, u), u.length === 0) break;
193
- let d = [...new Set(u.map((e) => String(e[i.parentKey] ?? e.id)))];
194
- for (let e of a.values()) await c(e, d);
195
- if (!e.scan || u.length < o) break;
196
- let f = u[u.length - 1].id;
197
- if (f === void 0 || f === n) throw Error("governance scan must advance a stable id cursor");
198
- n = f;
199
- }
200
- }
201
- };
202
- for (let e of a.values()) await c(e, [n]);
203
- return [...i.entries()].map(([e, t]) => ({
204
- table: e,
205
- rows: [...t.rows.values()],
206
- ...t.truncated ? { truncated: !0 } : {}
207
- }));
208
- }, V = async (e, t, n) => {
209
- let r = await B(e, t, n), i = {};
210
- for (let e of r) e.rows.length > 0 && (i[e.table] = e.rows);
211
- return i;
212
- }, H = (e, t) => {
213
- if (t === void 0 || t.length === 0) throw Error(`${e}: mode 'anonymize' needs a non-empty 'anonymizeFields' — with none, nothing is written and the result would report rows as affected that were not changed`);
214
- }, U = async (e, t, n, r = {}, i = () => (/* @__PURE__ */ new Date()).toISOString()) => {
215
- let a = r.mode ?? "delete";
216
- a === "anonymize" && H("governance erasure", r.anonymizeFields);
217
- let o = await B(e, t, n), s = /* @__PURE__ */ new Map();
218
- for (let e of t) {
219
- let t = s.get(e.table);
220
- (t === void 0 || e.depth > t) && s.set(e.table, e.depth);
221
- }
222
- let c = [...o].sort((e, t) => (s.get(t.table) ?? 0) - (s.get(e.table) ?? 0)), l = [], u = [];
223
- for (let t of c) {
224
- if (t.rows.length === 0) continue;
225
- t.truncated === !0 && u.push(t.table);
226
- let n = a === "delete" ? void 0 : Object.fromEntries((r.anonymizeFields ?? []).map((e) => [e, null])), i = await F(e, t.table, t.rows.map((e) => String(e.id)), n);
227
- l.push({
228
- table: t.table,
229
- count: i
230
- });
231
- }
232
- return {
233
- subjectId: n,
234
- at: i(),
235
- mode: a,
236
- affected: l,
237
- ...u.length > 0 ? { truncated: u } : {}
238
- };
239
- }, W = (e, t, n) => {
240
- let r = e[t], i = r instanceof Date ? r.getTime() : typeof r == "string" || typeof r == "number" ? new Date(r).getTime() : NaN;
241
- return Number.isNaN(i) ? null : n - i;
242
- }, G = (e, t, n) => {
243
- let r = t.dateField ?? "createdAt";
244
- return e.filter((e) => {
245
- let i = W(e, r, n);
246
- return i !== null && i > t.ttlMs;
247
- }).map((e) => String(e.id));
248
- }, ne = 1e3, K = async (e, t, n = Date.now(), r = ne) => {
249
- if (!Number.isSafeInteger(r) || r < 1) throw Error("retention batchSize must be a positive safe integer");
250
- let i = [];
251
- for (let a of t) {
252
- let t = a.action ?? "delete";
253
- t === "anonymize" && H(`retention policy '${a.table}'`, a.anonymizeFields);
254
- let o = 0, s = 0, c, l = !1;
255
- for (; !l;) {
256
- let i = a.dateField ?? "createdAt", u = new Date(Math.ceil(n - a.ttlMs)), d = e.scan ? await e.scan(a.table, {
257
- limit: r,
258
- ...c === void 0 ? {} : { after: c },
259
- projection: ["id", i],
260
- ...Number.isNaN(u.getTime()) ? {} : { expired: {
261
- column: i,
262
- before: u
263
- } }
264
- }) : await e.page(a.table, r, s), f = G(d, a, n);
265
- if (t === "delete") o += await F(e, a.table, f), s += d.length - f.length;
266
- else {
267
- let t = {};
268
- for (let e of a.anonymizeFields ?? []) t[e] = null;
269
- o += await F(e, a.table, f, t), s += d.length;
270
- }
271
- if (d.length && e.scan) {
272
- let e = d[d.length - 1].id;
273
- if (e === void 0 || e === c) throw Error("retention scan must advance a stable id cursor");
274
- c = e;
275
- }
276
- l = d.length < r;
277
- }
278
- i.push({
279
- table: a.table,
280
- action: t,
281
- affected: o
282
- });
283
- }
284
- return i;
285
- }, re = 1e4, q = (e = re) => {
286
- let t = /* @__PURE__ */ new Map(), n = [], r = (e, t) => `${e}|${t}`;
287
- return {
288
- record: async (i) => {
289
- t.set(r(i.subjectId, i.purpose), i), n.unshift(i), n.length > e && (n.length = e);
290
- },
291
- has: async (e, n) => t.get(r(e, n))?.granted ?? !1,
292
- list: async (e) => n.filter((t) => t.subjectId === e)
293
- };
294
- }, J = "_voltro_consent", Y = j(J, {
295
- id: D({ prefix: "consent" }),
296
- subjectId: M(),
297
- purpose: M(),
298
- granted: x(),
299
- at: M(),
300
- tenantId: M().nullable()
301
- }).index("byConsentSubjectPurpose", ["subjectId", "purpose"]).index("byConsentSubject", ["subjectId"]), X = [Y], Z = (e) => ({
302
- subjectId: String(e.subjectId),
303
- purpose: String(e.purpose),
304
- granted: e.granted === !0 || e.granted === 1 || e.granted === "true",
305
- at: String(e.at),
306
- tenantId: e.tenantId ?? null
307
- }), Q = (e) => {
308
- let t = (t, n) => e.query({
309
- table: J,
310
- predicate: t,
311
- order: [{
312
- column: "at",
313
- direction: "desc"
314
- }],
315
- take: n,
316
- skip: void 0,
317
- projection: void 0
318
- });
319
- return {
320
- record: async (t) => {
321
- await e.insert(J, {
322
- subjectId: t.subjectId,
323
- purpose: t.purpose,
324
- granted: t.granted,
325
- at: t.at,
326
- tenantId: t.tenantId ?? null
327
- });
328
- },
329
- has: async (e, n) => {
330
- let r = await t(b(C("subjectId", e), C("purpose", n)), 1);
331
- return r[0] ? Z(r[0]).granted : !1;
332
- },
333
- list: async (e) => (await t(C("subjectId", e))).map(Z)
334
- };
335
- }, $ = 1e3, ie = class extends a.Service()("@voltro/plugin-governance/GovernanceService") {}, ae = "@voltro/plugin-governance", oe = (a = {}) => {
336
- let f = u({
337
- base: ae,
338
- alias: a.alias,
339
- instance: a.name
340
- }), p = a.consent === "datastore", m = a.consent && a.consent !== "datastore" ? a.consent : p ? void 0 : q(), g, v = {
341
- record: (e) => (m ?? g ?? q()).record(e),
342
- has: (e, t) => (m ?? g ?? q()).has(e, t),
343
- list: (e) => (m ?? g ?? q()).list(e)
344
- }, y = a.subjectScopes ?? [], b = a.deriveSubjectScopes, x = () => {
345
- let e = b?.subjectTable ?? "actors", t = y.map((t) => z(t, e));
346
- if (b === void 0) return {
2
+ import { dataStoreGovStore as a, memoryGovStore as o } from "./store.js";
3
+ import { SUBJECT_KEY_CHUNK as s, SUBJECT_ROW_CAP as c, assertAnonymizable as l, collectSubjectRows as u, eraseSubject as d, exportSubject as f, scopeToPath as p } from "./gdpr.js";
4
+ import { runRetention as m, selectExpired as h } from "./retention.js";
5
+ import { CONSENT_TABLE as g, consentTable as _, consentTables as v, dataStoreConsentStore as y, memoryConsentStore as b } from "./consent.js";
6
+ import { Context as x, Effect as S, Layer as ee } from "effect";
7
+ import { ADMIN_SCOPE as C, definePlugin as te, pluginInstanceName as ne, requireScope as w } from "@voltro/protocol";
8
+ import { FIELD_ENCRYPTION_ACTIVE_ENV as T, FIELD_ENCRYPTION_KEYS_ENV as E, deriveKey as D, deriveKey as O, fieldDecryptSnapshot as re, fieldKeyIdOf as k, getFieldCipher as A, isFieldKeyring as j, makeAesCipher as M, makeFieldCipher as N, makeFieldCipher as ie, makeFieldKeyring as P, makeFieldKeyring as ae, readFieldKeyHolders as oe, readFieldKeyReports as se, readFieldKeys as ce, resolveFieldKeyring as F, resolveFieldKeyring as I, resolveSecret as L, setFieldCipher as R, setFieldCipher as z } from "@voltro/runtime";
9
+ import { DEFAULT_SUBJECT_GRAPH_DEPTH as B, ENCRYPTED_PREFIX as V, deriveSubjectGraph as H, deriveSubjectGraph as le, formatSubjectGraph as U, isEncrypted as W } from "@voltro/database";
10
+ //#region src/index.ts
11
+ var G = 1e3, K = class extends x.Service()("@voltro/plugin-governance/GovernanceService") {}, q = "@voltro/plugin-governance", J = (o = {}) => {
12
+ let s = ne({
13
+ base: q,
14
+ alias: o.alias,
15
+ instance: o.name
16
+ }), c = o.consent === "datastore", u = o.consent && o.consent !== "datastore" ? o.consent : c ? void 0 : b(), h, g = {
17
+ record: (e) => (u ?? h ?? b()).record(e),
18
+ has: (e, t) => (u ?? h ?? b()).has(e, t),
19
+ list: (e) => (u ?? h ?? b()).list(e)
20
+ }, _ = o.subjectScopes ?? [], x = o.deriveSubjectScopes, D = () => {
21
+ let e = x?.subjectTable ?? "actors", t = _.map((t) => p(t, e));
22
+ if (x === void 0) return {
347
23
  subjectTable: e,
348
24
  paths: t,
349
25
  limitations: []
350
26
  };
351
- let n = te({
27
+ let n = le({
352
28
  subjectTable: e,
353
- ...b.maxDepth === void 0 ? {} : { maxDepth: b.maxDepth },
354
- ...b.exclude === void 0 ? {} : { exclude: b.exclude }
29
+ ...x.maxDepth === void 0 ? {} : { maxDepth: x.maxDepth },
30
+ ...x.exclude === void 0 ? {} : { exclude: x.exclude }
355
31
  }), r = new Set(n.paths.map((e) => `${e.table}|${e.hops.map((e) => e.foreignKey).join(">")}`)), i = t.filter((e) => !r.has(`${e.table}|${e.hops.map((e) => e.foreignKey).join(">")}`));
356
32
  return {
357
33
  ...n,
358
34
  paths: [...n.paths, ...i]
359
35
  };
360
- }, S = () => x().paths;
361
- for (let e of a.retention ?? []) (e.action ?? "delete") === "anonymize" && H(`retention policy '${e.table}'`, e.anonymizeFields);
362
- a.erasure?.mode === "anonymize" && H("erasure defaults", a.erasure.anonymizeFields);
363
- let C = typeof a.fieldEncryption == "object" ? a.fieldEncryption.secretKey ?? "VOLTRO_FIELD_ENCRYPTION_KEY" : "VOLTRO_FIELD_ENCRYPTION_KEY", w = typeof a.fieldEncryption == "object" ? a.fieldEncryption.previousSecretKeys ?? ["VOLTRO_FIELD_ENCRYPTION_KEY_PREVIOUS"] : ["VOLTRO_FIELD_ENCRYPTION_KEY_PREVIOUS"], T = a.fieldEncryption ? [{
364
- name: C,
36
+ }, k = () => D().paths;
37
+ for (let e of o.retention ?? []) (e.action ?? "delete") === "anonymize" && l(`retention policy '${e.table}'`, e.anonymizeFields);
38
+ o.erasure?.mode === "anonymize" && l("erasure defaults", o.erasure.anonymizeFields);
39
+ let M = typeof o.fieldEncryption == "object" ? o.fieldEncryption.secretKey ?? "VOLTRO_FIELD_ENCRYPTION_KEY" : "VOLTRO_FIELD_ENCRYPTION_KEY", N = typeof o.fieldEncryption == "object" ? o.fieldEncryption.previousSecretKeys ?? ["VOLTRO_FIELD_ENCRYPTION_KEY_PREVIOUS"] : ["VOLTRO_FIELD_ENCRYPTION_KEY_PREVIOUS"], P = o.fieldEncryption ? [{
40
+ name: M,
365
41
  required: !1,
366
42
  secret: !0,
367
43
  description: "AES-256-GCM key for `.encrypted()` columns (field encryption). Lose the key, lose the data — GCM fails closed."
368
- }, ...w.map((e) => ({
44
+ }, ...N.map((e) => ({
369
45
  name: e,
370
46
  required: !1,
371
47
  secret: !0,
372
48
  description: "A retired field-encryption key, read during a rotation so values written under it stay readable. Remove once they are re-encrypted."
373
- }))] : [], E = [], D, O, k = () => void 0, A = [], j = null, M = !1, P = () => {
374
- if (!D) throw Error("governance: data store not bound yet");
375
- return D;
376
- }, F = {
377
- subjectGraph: x,
378
- exportSubject: (e) => V(P(), S(), e),
49
+ }))] : [], F = [], R, B, V, H = () => void 0, U = [], W = null, J = !1, Y = () => {
50
+ if (!R) throw Error("governance: data store not bound yet");
51
+ return R;
52
+ }, X = {
53
+ subjectGraph: D,
54
+ exportSubject: (e) => f(Y(), k(), e),
379
55
  eraseSubject: async (e, t) => {
380
56
  let n = {
381
- ...a.erasure,
57
+ ...o.erasure,
382
58
  ...t
383
- }, r = await U(P(), S(), e, n);
384
- return E.unshift(r), E.length > 1e3 && (E.length = $), r;
59
+ }, r = await d(Y(), k(), e, n);
60
+ return F.unshift(r), F.length > 1e3 && (F.length = G), r;
385
61
  },
386
- recordConsent: (e) => v.record(e),
387
- hasConsent: (e, t) => v.has(e, t),
62
+ recordConsent: (e) => g.record(e),
63
+ hasConsent: (e, t) => g.has(e, t),
388
64
  runRetentionNow: async () => {
389
- if (!D || !a.retention) return;
390
- let e = await K(D, a.retention);
391
- A = e, j = (/* @__PURE__ */ new Date()).toISOString(), k("retention sweep", { report: e });
65
+ if (!R || !o.retention) return;
66
+ let e = await m(R, o.retention);
67
+ U = e, W = (/* @__PURE__ */ new Date()).toISOString(), H("retention sweep", { report: e });
392
68
  }
393
- }, I = [
69
+ }, ue = [
394
70
  {
395
71
  ...n,
396
72
  description: "GDPR data export for a subject (admin only).",
397
- execute: (e, t) => o.gen(function* () {
398
- return yield* d(t.request.subject, c), yield* o.promise(() => F.exportSubject(e.subjectId));
73
+ execute: (e, t) => S.gen(function* () {
74
+ return yield* w(t.request.subject, C), yield* S.promise(() => X.exportSubject(e.subjectId));
399
75
  })
400
76
  },
401
77
  {
402
78
  ...t,
403
79
  description: "GDPR erasure (right-to-be-forgotten) for a subject (admin only).",
404
- execute: (e, t) => o.gen(function* () {
405
- yield* d(t.request.subject, c);
80
+ execute: (e, t) => S.gen(function* () {
81
+ yield* w(t.request.subject, C);
406
82
  let { subjectId: n, mode: r } = e;
407
- return yield* o.promise(() => F.eraseSubject(n, r ? { mode: r } : void 0));
83
+ return yield* S.promise(() => X.eraseSubject(n, r ? { mode: r } : void 0));
408
84
  })
409
85
  },
410
86
  {
411
87
  ...e,
412
88
  description: "Record the calling subject's consent for a purpose.",
413
- execute: (e, t) => o.promise(async () => {
89
+ execute: (e, t) => S.promise(async () => {
414
90
  let { purpose: n, granted: r } = e, i = t.request.subject;
415
- return await F.recordConsent({
91
+ return await X.recordConsent({
416
92
  subjectId: i.id ?? "anonymous",
417
93
  purpose: n,
418
94
  granted: r,
@@ -424,39 +100,98 @@ var N = (e) => {
424
100
  {
425
101
  ...i,
426
102
  description: "Whether the calling subject has granted consent for a purpose.",
427
- execute: (e, t) => o.promise(async () => {
103
+ execute: (e, t) => S.promise(async () => {
428
104
  let n = t.request.subject;
429
- return { granted: await F.hasConsent(n.id ?? "anonymous", e.purpose) };
105
+ return { granted: await X.hasConsent(n.id ?? "anonymous", e.purpose) };
430
106
  })
431
107
  }
432
- ], L = (e) => ({
108
+ ], Z = (e) => ({
433
109
  kind: "json",
434
110
  data: e
435
- }), R = (e, t) => ({
111
+ }), Q = (e, t) => ({
436
112
  kind: "json",
437
113
  status: e,
438
114
  data: { error: t }
439
- }), B = (e, t) => new URLSearchParams(new URL(e, "http://x").search).get(t), W = [
115
+ }), de = (e, t) => new URLSearchParams(new URL(e, "http://x").search).get(t), $ = (e) => e === null ? null : e.toISOString(), fe = async () => {
116
+ let e = A();
117
+ if (!j(e)) return null;
118
+ let t = {
119
+ active: e.activeKeyId,
120
+ requested: e.requestedKeyId,
121
+ held: e.keyIds,
122
+ fingerprints: e.fingerprints,
123
+ decrypts: re()
124
+ };
125
+ if (B === void 0) return {
126
+ ...t,
127
+ recorded: null,
128
+ error: "the data store is not bound yet"
129
+ };
130
+ try {
131
+ let [e, n, r] = await Promise.all([
132
+ ce(B),
133
+ oe(B),
134
+ se(B)
135
+ ]), i = Date.now();
136
+ return {
137
+ ...t,
138
+ recorded: {
139
+ keys: e.map((e) => ({
140
+ keyId: e.keyId,
141
+ state: e.state,
142
+ registeredAt: $(e.registeredAt),
143
+ activatedAt: $(e.activatedAt),
144
+ replacedAt: $(e.replacedAt),
145
+ retiredAt: $(e.retiredAt),
146
+ requestedAt: $(e.requestedAt)
147
+ })),
148
+ holders: n.map((e) => ({
149
+ holderId: e.holderId,
150
+ keyIds: e.keyIds,
151
+ activeKeyId: e.activeKeyId,
152
+ expiresAt: e.expiresAt.toISOString(),
153
+ live: e.expiresAt.getTime() > i
154
+ })),
155
+ lastCount: r.status === void 0 ? null : {
156
+ reportedAt: r.status.reportedAt.toISOString(),
157
+ ...r.status.report
158
+ },
159
+ lastRekey: r.rekey === void 0 ? null : {
160
+ reportedAt: r.rekey.reportedAt.toISOString(),
161
+ ...r.rekey.report
162
+ }
163
+ },
164
+ error: null
165
+ };
166
+ } catch (e) {
167
+ return {
168
+ ...t,
169
+ recorded: null,
170
+ error: e instanceof Error ? e.message : String(e)
171
+ };
172
+ }
173
+ }, pe = [
440
174
  {
441
175
  method: "GET",
442
176
  path: "/status",
443
177
  scope: "process",
444
- description: "Retention policies, subject scopes, field-encryption state, last sweep.",
445
- handler: () => o.succeed(L({
446
- retention: (a.retention ?? []).map((e) => ({
178
+ description: "Retention policies, subject scopes, field-encryption state and keys, last sweep.",
179
+ handler: () => S.promise(async () => Z({
180
+ retention: (o.retention ?? []).map((e) => ({
447
181
  table: e.table,
448
182
  ttlMs: e.ttlMs,
449
183
  action: e.action ?? "delete"
450
184
  })),
451
- scopes: y.map((e) => ({
185
+ scopes: _.map((e) => ({
452
186
  table: e.table,
453
187
  subjectField: e.subjectField
454
188
  })),
455
- fieldEncryption: M,
456
- sweepIntervalMs: a.sweepIntervalMs ?? 36e5,
457
- lastSweepAt: j,
458
- lastSweep: A,
459
- erasureCount: E.length
189
+ fieldEncryption: J || A() !== void 0,
190
+ fieldKeys: await fe(),
191
+ sweepIntervalMs: o.sweepIntervalMs ?? 36e5,
192
+ lastSweepAt: W,
193
+ lastSweep: U,
194
+ erasureCount: F.length
460
195
  }))
461
196
  },
462
197
  {
@@ -464,18 +199,18 @@ var N = (e) => {
464
199
  path: "/erasures",
465
200
  scope: "process",
466
201
  description: "GDPR erasure log (most recent first).",
467
- handler: () => o.succeed(L({ erasures: E.slice(0, 100) }))
202
+ handler: () => S.succeed(Z({ erasures: F.slice(0, 100) }))
468
203
  },
469
204
  {
470
205
  method: "GET",
471
206
  path: "/subject-graph",
472
207
  scope: "declaration",
473
208
  description: "The routes a DSAR export/erasure walks — AND what the derivation cannot see.",
474
- handler: () => o.sync(() => {
475
- let e = x();
476
- return L({
209
+ handler: () => S.sync(() => {
210
+ let e = D();
211
+ return Z({
477
212
  subjectTable: e.subjectTable,
478
- derived: b !== void 0,
213
+ derived: x !== void 0,
479
214
  paths: e.paths.map((t) => ({
480
215
  table: t.table,
481
216
  depth: t.depth,
@@ -489,29 +224,29 @@ var N = (e) => {
489
224
  {
490
225
  method: "GET",
491
226
  path: "/consent",
492
- scope: p ? "shared-store" : "process",
227
+ scope: c ? "shared-store" : "process",
493
228
  description: "Consent ledger for a subject (?subjectId=…).",
494
229
  handler: (e) => {
495
- let t = B(e.url, "subjectId");
496
- return t ? o.promise(async () => L({ records: await v.list(t) })) : o.succeed(R(400, "subjectId query param required"));
230
+ let t = de(e.url, "subjectId");
231
+ return t ? S.promise(async () => Z({ records: await g.list(t) })) : S.succeed(Q(400, "subjectId query param required"));
497
232
  }
498
233
  },
499
234
  {
500
235
  method: "POST",
501
236
  path: "/export",
502
237
  description: "Run a GDPR subject export.",
503
- handler: (e) => o.promise(async () => {
238
+ handler: (e) => S.promise(async () => {
504
239
  let t;
505
240
  try {
506
241
  t = JSON.parse(e.body || "{}");
507
242
  } catch {
508
- return R(400, "invalid JSON body");
243
+ return Q(400, "invalid JSON body");
509
244
  }
510
- if (typeof t != "object" || !t || Array.isArray(t) || !("subjectId" in t) || typeof t.subjectId != "string" || !t.subjectId.trim()) return R(400, "subjectId must be a non-empty string");
245
+ if (typeof t != "object" || !t || Array.isArray(t) || !("subjectId" in t) || typeof t.subjectId != "string" || !t.subjectId.trim()) return Q(400, "subjectId must be a non-empty string");
511
246
  try {
512
- return L({ bundle: await F.exportSubject(t.subjectId) });
247
+ return Z({ bundle: await X.exportSubject(t.subjectId) });
513
248
  } catch (e) {
514
- return R(500, e.message);
249
+ return Q(500, e.message);
515
250
  }
516
251
  })
517
252
  },
@@ -519,20 +254,20 @@ var N = (e) => {
519
254
  method: "POST",
520
255
  path: "/erase",
521
256
  description: "Run a GDPR erasure (right-to-be-forgotten).",
522
- handler: (e) => o.promise(async () => {
257
+ handler: (e) => S.promise(async () => {
523
258
  let t;
524
259
  try {
525
260
  t = JSON.parse(e.body || "{}");
526
261
  } catch {
527
- return R(400, "invalid JSON body");
262
+ return Q(400, "invalid JSON body");
528
263
  }
529
- if (typeof t != "object" || !t || Array.isArray(t) || !("subjectId" in t) || typeof t.subjectId != "string" || !t.subjectId.trim()) return R(400, "subjectId must be a non-empty string");
264
+ if (typeof t != "object" || !t || Array.isArray(t) || !("subjectId" in t) || typeof t.subjectId != "string" || !t.subjectId.trim()) return Q(400, "subjectId must be a non-empty string");
530
265
  let n = "mode" in t ? t.mode : void 0;
531
- if (n !== void 0 && n !== "delete" && n !== "anonymize") return R(400, "mode must be delete or anonymize");
266
+ if (n !== void 0 && n !== "delete" && n !== "anonymize") return Q(400, "mode must be delete or anonymize");
532
267
  try {
533
- return L({ entry: await F.eraseSubject(t.subjectId, n ? { mode: n } : void 0) });
268
+ return Z({ entry: await X.eraseSubject(t.subjectId, n ? { mode: n } : void 0) });
534
269
  } catch (e) {
535
- return R(500, e.message);
270
+ return Q(500, e.message);
536
271
  }
537
272
  })
538
273
  },
@@ -540,64 +275,80 @@ var N = (e) => {
540
275
  method: "POST",
541
276
  path: "/sweep",
542
277
  description: "Run the retention sweep now.",
543
- handler: () => o.promise(async () => (await F.runRetentionNow(), L({
544
- lastSweepAt: j,
545
- lastSweep: A
278
+ handler: () => S.promise(async () => (await X.runRetentionNow(), Z({
279
+ lastSweepAt: W,
280
+ lastSweep: U
546
281
  })))
547
282
  }
548
283
  ];
549
- return l({
550
- name: f,
551
- baseName: ae,
284
+ return te({
285
+ name: s,
286
+ baseName: q,
552
287
  description: "Data governance — retention TTL sweep, GDPR export/erasure, consent ledger.",
553
288
  permissions: [
554
289
  "store:write",
555
290
  "inspect:read",
556
291
  "inspect:write"
557
292
  ],
558
- declaredEnv: T,
559
- services: s.succeed(ie, F),
560
- routes: I,
293
+ declaredEnv: P,
294
+ services: ee.succeed(K, X),
295
+ routes: ue,
561
296
  rpcClientDescriptors: r,
562
- inspectEndpoints: W,
563
- ...p ? { extendSchema: { tables: X } } : {},
297
+ inspectEndpoints: pe,
298
+ ...c ? { extendSchema: { tables: v } } : {},
564
299
  bindDataStore: (e, t) => {
565
- if (D = N(e), p && (g = Q(e)), a.retention && a.retention.length > 0) {
566
- let e = a.sweepIntervalMs ?? 36e5;
567
- O = t?.scheduleCoordinated("governance.retention.sweep", e, () => F.runRetentionNow());
300
+ if (R = a(e), B = e, c && (h = y(e)), o.retention && o.retention.length > 0) {
301
+ let e = o.sweepIntervalMs ?? 36e5;
302
+ V = t?.scheduleCoordinated("governance.retention.sweep", e, () => X.runRetentionNow());
568
303
  }
569
304
  },
570
- onActivate: (e) => o.gen(function* () {
571
- k = (t, n) => e.logger.warn(t, n);
305
+ onActivate: (e) => S.gen(function* () {
306
+ H = (t, n) => e.logger.warn(t, n);
572
307
  let t = !1;
573
- if (a.fieldEncryption) {
574
- let e = C, n = yield* o.promise(() => h(e));
575
- if (!n) return yield* o.die(/* @__PURE__ */ Error(`governance: fieldEncryption is enabled but secret "${e}" did not resolve — set it via the Secrets-Resolver / env`));
576
- let r = [];
577
- for (let e of w) {
578
- let t = yield* o.promise(() => h(e));
579
- t !== void 0 && t.trim() !== "" && r.push(t);
308
+ if (o.fieldEncryption) {
309
+ let e = M, n = yield* S.promise(() => L(e)), r = yield* S.promise(() => L(E)), i = [];
310
+ for (let e of N) {
311
+ let t = yield* S.promise(() => L(e));
312
+ t !== void 0 && t.trim() !== "" && i.push(t);
313
+ }
314
+ if (r !== void 0 && r.trim() !== "") {
315
+ let e = yield* S.promise(() => L(T)), t = yield* S.try({
316
+ try: () => {
317
+ let t = I({
318
+ [E]: r,
319
+ [T]: e,
320
+ VOLTRO_FIELD_ENCRYPTION_KEY: n
321
+ });
322
+ for (let e of i) if (!t.keys.some((t) => O(t.secret).equals(O(e)))) throw Error(`governance: a key named in fieldEncryption.previousSecretKeys is not one of ${E}`);
323
+ return t;
324
+ },
325
+ catch: (e) => e instanceof Error ? e : Error(String(e))
326
+ }).pipe(S.orDie);
327
+ z(ae(t));
328
+ } else {
329
+ if (!n) return yield* S.die(/* @__PURE__ */ Error(`governance: fieldEncryption is enabled but secret "${e}" did not resolve — set it via the Secrets-Resolver / env, or configure ${E}`));
330
+ z(ie(n, i));
580
331
  }
581
- _(ee(n, r)), t = !0, M = !0;
332
+ t = !0, J = !0;
582
333
  }
583
- !p && !a.consent && e.logger.warn("governance: consent ledger uses the in-memory store — per-process + restart-cleared. Under >1 replica a consent recorded on one replica is invisible to others. Pass consent: 'datastore' (durable, cross-instance) in production.");
584
- let n = x();
585
- if (b !== void 0) {
334
+ !c && !o.consent && e.logger.warn("governance: consent ledger uses the in-memory store — per-process + restart-cleared. Under >1 replica a consent recorded on one replica is invisible to others. Pass consent: 'datastore' (durable, cross-instance) in production.");
335
+ let n = D();
336
+ if (x !== void 0) {
586
337
  let t = n.limitations.filter((e) => e.kind === "unreachable").length;
587
338
  n.paths.length <= 1 && e.logger.warn(`governance: the derived subject scope reached ${n.paths.length} table(s) from '${n.subjectTable}'. A DSAR would export almost nothing. Check that subjectTable names a real table and that the app declares relations() or reference() columns pointing at it.`), t > 0 && e.logger.info(`governance: ${t} registered table(s) are NOT linked to '${n.subjectTable}' and are outside every DSAR. GET /_voltro/inspect/plugins/governance/subject-graph lists them; add subjectScopes for any that hold subject data.`);
588
339
  }
589
340
  e.logger.info("governance active", {
590
- retention: a.retention?.length ?? 0,
341
+ retention: o.retention?.length ?? 0,
591
342
  scopes: n.paths.length,
592
- scopeSource: b === void 0 ? "declared" : "derived",
343
+ scopeSource: x === void 0 ? "declared" : "derived",
593
344
  fieldEncryption: t,
594
- consent: p ? "datastore" : a.consent ? "custom" : "memory"
345
+ consent: c ? "datastore" : o.consent ? "custom" : "memory"
595
346
  });
596
347
  }),
597
- onDeactivate: () => o.sync(() => {
598
- O?.stop(), O = void 0, a.fieldEncryption && _(void 0);
348
+ onDeactivate: () => S.sync(() => {
349
+ V?.stop(), V = void 0, o.fieldEncryption && z(void 0);
599
350
  })
600
351
  });
601
352
  };
602
353
  //#endregion
603
- export { J as CONSENT_TABLE, v as DEFAULT_SUBJECT_GRAPH_DEPTH, y as ENCRYPTED_PREFIX, $ as ERASURE_LOG_CAP, ie as GovernanceService, I as SUBJECT_KEY_CHUNK, L as SUBJECT_ROW_CAP, B as collectSubjectRows, Y as consentTable, X as consentTables, Q as dataStoreConsentStore, N as dataStoreGovStore, f as deriveKey, S as deriveSubjectGraph, U as eraseSubject, V as exportSubject, w as formatSubjectGraph, oe as governancePlugin, k as isEncrypted, p as makeAesCipher, m as makeFieldCipher, q as memoryConsentStore, P as memoryGovStore, K as runRetention, z as scopeToPath, G as selectExpired, g as setFieldCipher };
354
+ export { g as CONSENT_TABLE, B as DEFAULT_SUBJECT_GRAPH_DEPTH, V as ENCRYPTED_PREFIX, G as ERASURE_LOG_CAP, K as GovernanceService, s as SUBJECT_KEY_CHUNK, c as SUBJECT_ROW_CAP, u as collectSubjectRows, _ as consentTable, v as consentTables, y as dataStoreConsentStore, a as dataStoreGovStore, D as deriveKey, H as deriveSubjectGraph, d as eraseSubject, f as exportSubject, k as fieldKeyIdOf, U as formatSubjectGraph, J as governancePlugin, W as isEncrypted, M as makeAesCipher, N as makeFieldCipher, P as makeFieldKeyring, b as memoryConsentStore, o as memoryGovStore, F as resolveFieldKeyring, m as runRetention, p as scopeToPath, h as selectExpired, R as setFieldCipher };