@voltro/plugin-governance 0.32.0 → 0.34.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,10 +1,10 @@
1
1
  import { consentDescriptor as e, eraseDescriptor as t, exportDescriptor as n, governanceRpcClientImports as r, hasConsentDescriptor as i } from "./rpc.js";
2
2
  import { Context as a, Effect as o, Layer as s } from "effect";
3
- import { ADMIN_SCOPE as c, definePlugin as l, requireScope as u } from "@voltro/protocol";
4
- import { deriveKey as d, makeAesCipher as f, makeFieldCipher as p, makeFieldCipher as m, resolveSecret as h, setFieldCipher as g, setFieldCipher as _ } from "@voltro/runtime";
5
- import { ENCRYPTED_PREFIX as v, and as y, boolean as b, eq as x, id as S, isEncrypted as C, table as w, text as T } from "@voltro/database";
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 C, eq as w, formatSubjectGraph as T, id as E, isEncrypted as D, table as O, text as k } from "@voltro/database";
6
6
  //#region src/store.ts
7
- var E = (e) => {
7
+ var A = (e) => {
8
8
  let t = e, n = (e, t) => ({
9
9
  table: void 0,
10
10
  predicate: void 0,
@@ -21,6 +21,15 @@ var E = (e) => {
21
21
  ...n(void 0, void 0),
22
22
  table: e
23
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
+ }),
24
33
  page: (e, r, i) => t.query({
25
34
  ...n(r, i),
26
35
  table: e
@@ -32,13 +41,18 @@ var E = (e) => {
32
41
  await t.update(e, n, r);
33
42
  }
34
43
  };
35
- }, D = (e = {}) => {
44
+ }, j = (e = {}) => {
36
45
  let t = new Map(Object.entries(e).map(([e, t]) => [e, t.map((e) => ({ ...e }))])), n = (e) => {
37
46
  let n = t.get(e);
38
47
  return n || (n = [], t.set(e, n)), n;
39
48
  };
40
49
  return {
41
50
  all: async (e) => [...n(e)],
51
+ where: async (e, t, r) => {
52
+ if (r.length === 0) return [];
53
+ let i = new Set(r.map((e) => String(e)));
54
+ return n(e).filter((e) => i.has(String(e[t]))).map((e) => ({ ...e }));
55
+ },
42
56
  page: async (e, t, r) => n(e).slice(r, r + t).map((e) => ({ ...e })),
43
57
  delete: async (e, t) => {
44
58
  let r = n(e), i = r.findIndex((e) => String(e.id) === t);
@@ -53,21 +67,121 @@ var E = (e) => {
53
67
  },
54
68
  dump: () => Object.fromEntries([...t.entries()].map(([e, t]) => [e, t.map((e) => ({ ...e }))]))
55
69
  };
56
- }, O = (e, t, n) => {
70
+ }, M = 500, N = 5e4, P = (e, t) => {
71
+ let n = [];
72
+ for (let r = 0; r < e.length; r += t) n.push(e.slice(r, r + t));
73
+ return n;
74
+ }, F = (e, t) => e.table === t && e.subjectField === "id" ? {
75
+ table: e.table,
76
+ hops: [],
77
+ depth: 0,
78
+ via: "declared"
79
+ } : {
80
+ table: e.table,
81
+ hops: [{
82
+ table: e.table,
83
+ foreignKey: e.subjectField,
84
+ parentKey: "id"
85
+ }],
86
+ depth: 1,
87
+ via: "declared"
88
+ }, I = async (e, t, n, r = N) => {
89
+ let i = /* @__PURE__ */ new Map(), a = /* @__PURE__ */ new Map(), o = async (t, n, r) => {
90
+ if (r.length === 0) return [];
91
+ let i = `${t}\u0000${n}\u0000${r.join(",")}`, o = a.get(i);
92
+ if (o !== void 0) return o;
93
+ let s = [];
94
+ for (let i of P(r, 500)) s.push(...await e.where(t, n, i));
95
+ return a.set(i, s), s;
96
+ }, s = (e, t) => {
97
+ let n = i.get(e);
98
+ n === void 0 && (n = {
99
+ rows: /* @__PURE__ */ new Map(),
100
+ truncated: !1
101
+ }, i.set(e, n));
102
+ for (let e of t) {
103
+ if (n.rows.size >= r) {
104
+ n.truncated = !0;
105
+ break;
106
+ }
107
+ n.rows.set(String(e.id ?? JSON.stringify(e)), e);
108
+ }
109
+ };
110
+ for (let e of t) {
111
+ if (e.hops.length === 0) {
112
+ s(e.table, await o(e.table, "id", [n]));
113
+ continue;
114
+ }
115
+ let t = [n];
116
+ for (let n of e.hops) {
117
+ let e = await o(n.table, n.foreignKey, t);
118
+ if (e.length === 0) {
119
+ t = [];
120
+ break;
121
+ }
122
+ s(n.table, e), t = [...new Set(e.map((e) => String(e[n.parentKey] ?? e.id)))];
123
+ }
124
+ }
125
+ return [...i.entries()].map(([e, t]) => ({
126
+ table: e,
127
+ rows: [...t.rows.values()],
128
+ ...t.truncated ? { truncated: !0 } : {}
129
+ }));
130
+ }, L = async (e, t, n) => {
131
+ let r = await I(e, t, n), i = {};
132
+ for (let e of r) e.rows.length > 0 && (i[e.table] = e.rows);
133
+ return i;
134
+ }, R = (e, t) => {
135
+ 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`);
136
+ }, z = async (e, t, n, r = {}, i = () => (/* @__PURE__ */ new Date()).toISOString()) => {
137
+ let a = r.mode ?? "delete";
138
+ a === "anonymize" && R("governance erasure", r.anonymizeFields);
139
+ let o = await I(e, t, n), s = /* @__PURE__ */ new Map();
140
+ for (let e of t) {
141
+ let t = s.get(e.table);
142
+ (t === void 0 || e.depth > t) && s.set(e.table, e.depth);
143
+ }
144
+ let c = [...o].sort((e, t) => (s.get(t.table) ?? 0) - (s.get(e.table) ?? 0)), l = [], u = [];
145
+ for (let t of c) if (t.rows.length !== 0) {
146
+ t.truncated === !0 && u.push(t.table);
147
+ for (let n of t.rows) {
148
+ let i = String(n.id);
149
+ if (a === "delete") await e.delete(t.table, i);
150
+ else {
151
+ let n = {};
152
+ for (let e of r.anonymizeFields ?? []) n[e] = null;
153
+ await e.update(t.table, i, n);
154
+ }
155
+ }
156
+ l.push({
157
+ table: t.table,
158
+ count: t.rows.length
159
+ });
160
+ }
161
+ return {
162
+ subjectId: n,
163
+ at: i(),
164
+ mode: a,
165
+ affected: l,
166
+ ...u.length > 0 ? { truncated: u } : {}
167
+ };
168
+ }, B = (e, t, n) => {
57
169
  let r = e[t], i = r instanceof Date ? r.getTime() : typeof r == "string" || typeof r == "number" ? new Date(r).getTime() : NaN;
58
170
  return Number.isNaN(i) ? null : n - i;
59
- }, k = (e, t, n) => {
171
+ }, V = (e, t, n) => {
60
172
  let r = t.dateField ?? "createdAt";
61
173
  return e.filter((e) => {
62
- let i = O(e, r, n);
174
+ let i = B(e, r, n);
63
175
  return i !== null && i > t.ttlMs;
64
176
  }).map((e) => String(e.id));
65
- }, A = 1e3, j = async (e, t, n = Date.now(), r = A) => {
177
+ }, H = 1e3, U = async (e, t, n = Date.now(), r = H) => {
66
178
  let i = [];
67
179
  for (let a of t) {
68
- let t = a.action ?? "delete", o = 0, s = 0, c = !1;
180
+ let t = a.action ?? "delete";
181
+ t === "anonymize" && R(`retention policy '${a.table}'`, a.anonymizeFields);
182
+ let o = 0, s = 0, c = !1;
69
183
  for (; !c;) {
70
- let i = await e.page(a.table, r, s), l = k(i, a, n);
184
+ let i = await e.page(a.table, r, s), l = V(i, a, n);
71
185
  if (t === "delete") {
72
186
  for (let t of l) await e.delete(a.table, t);
73
187
  s += i.length - l.length;
@@ -88,38 +202,7 @@ var E = (e) => {
88
202
  });
89
203
  }
90
204
  return i;
91
- }, M = async (e, t, n) => {
92
- let r = {};
93
- for (let i of t) {
94
- let t = (await e.all(i.table)).filter((e) => String(e[i.subjectField]) === n);
95
- t.length > 0 && (r[i.table] = (r[i.table] ?? []).concat(t));
96
- }
97
- return r;
98
- }, N = async (e, t, n, r = {}, i = () => (/* @__PURE__ */ new Date()).toISOString()) => {
99
- let a = r.mode ?? "delete", o = [];
100
- for (let i of t) {
101
- let t = (await e.all(i.table)).filter((e) => String(e[i.subjectField]) === n);
102
- for (let n of t) {
103
- let t = String(n.id);
104
- if (a === "delete") await e.delete(i.table, t);
105
- else {
106
- let n = {};
107
- for (let e of r.anonymizeFields ?? []) n[e] = null;
108
- await e.update(i.table, t, n);
109
- }
110
- }
111
- t.length > 0 && o.push({
112
- table: i.table,
113
- count: t.length
114
- });
115
- }
116
- return {
117
- subjectId: n,
118
- at: i(),
119
- mode: a,
120
- affected: o
121
- };
122
- }, P = 1e4, F = (e = P) => {
205
+ }, W = 1e4, G = (e = W) => {
123
206
  let t = /* @__PURE__ */ new Map(), n = [], r = (e, t) => `${e}|${t}`;
124
207
  return {
125
208
  record: async (i) => {
@@ -128,22 +211,22 @@ var E = (e) => {
128
211
  has: async (e, n) => t.get(r(e, n))?.granted ?? !1,
129
212
  list: async (e) => n.filter((t) => t.subjectId === e)
130
213
  };
131
- }, I = "_voltro_consent", L = w(I, {
132
- id: S({ prefix: "consent" }),
133
- subjectId: T(),
134
- purpose: T(),
135
- granted: b(),
136
- at: T(),
137
- tenantId: T().nullable()
138
- }).index("byConsentSubjectPurpose", ["subjectId", "purpose"]).index("byConsentSubject", ["subjectId"]), R = [L], z = (e) => ({
214
+ }, K = "_voltro_consent", q = O(K, {
215
+ id: E({ prefix: "consent" }),
216
+ subjectId: k(),
217
+ purpose: k(),
218
+ granted: x(),
219
+ at: k(),
220
+ tenantId: k().nullable()
221
+ }).index("byConsentSubjectPurpose", ["subjectId", "purpose"]).index("byConsentSubject", ["subjectId"]), J = [q], Y = (e) => ({
139
222
  subjectId: String(e.subjectId),
140
223
  purpose: String(e.purpose),
141
224
  granted: e.granted === !0 || e.granted === 1 || e.granted === "true",
142
225
  at: String(e.at),
143
226
  tenantId: e.tenantId ?? null
144
- }), B = (e) => {
227
+ }), X = (e) => {
145
228
  let t = (t, n) => e.query({
146
- table: I,
229
+ table: K,
147
230
  predicate: t,
148
231
  order: [{
149
232
  column: "at",
@@ -155,7 +238,7 @@ var E = (e) => {
155
238
  });
156
239
  return {
157
240
  record: async (t) => {
158
- await e.insert(I, {
241
+ await e.insert(K, {
159
242
  subjectId: t.subjectId,
160
243
  purpose: t.purpose,
161
244
  granted: t.granted,
@@ -164,52 +247,79 @@ var E = (e) => {
164
247
  });
165
248
  },
166
249
  has: async (e, n) => {
167
- let r = await t(y(x("subjectId", e), x("purpose", n)), 1);
168
- return r[0] ? z(r[0]).granted : !1;
250
+ let r = await t(b(w("subjectId", e), w("purpose", n)), 1);
251
+ return r[0] ? Y(r[0]).granted : !1;
169
252
  },
170
- list: async (e) => (await t(x("subjectId", e))).map(z)
253
+ list: async (e) => (await t(w("subjectId", e))).map(Y)
171
254
  };
172
- }, V = 1e3, H = class extends a.Tag("@voltro/plugin-governance/GovernanceService")() {}, U = (a = {}) => {
173
- let d = a.name ? `@voltro/plugin-governance#${a.name}` : "@voltro/plugin-governance", f = a.consent === "datastore", p = a.consent && a.consent !== "datastore" ? a.consent : f ? void 0 : F(), g, v = {
174
- record: (e) => (p ?? g ?? F()).record(e),
175
- has: (e, t) => (p ?? g ?? F()).has(e, t),
176
- list: (e) => (p ?? g ?? F()).list(e)
177
- }, y = a.subjectScopes ?? [], b = typeof a.fieldEncryption == "object" ? a.fieldEncryption.secretKey ?? "VOLTRO_FIELD_ENCRYPTION_KEY" : "VOLTRO_FIELD_ENCRYPTION_KEY", x = a.fieldEncryption ? [{
178
- name: b,
255
+ }, Z = 1e3, Q = class extends a.Tag("@voltro/plugin-governance/GovernanceService")() {}, $ = "@voltro/plugin-governance", te = (a = {}) => {
256
+ let f = u({
257
+ base: $,
258
+ alias: a.alias,
259
+ instance: a.name
260
+ }), p = a.consent === "datastore", m = a.consent && a.consent !== "datastore" ? a.consent : p ? void 0 : G(), g, v = {
261
+ record: (e) => (m ?? g ?? G()).record(e),
262
+ has: (e, t) => (m ?? g ?? G()).has(e, t),
263
+ list: (e) => (m ?? g ?? G()).list(e)
264
+ }, y = a.subjectScopes ?? [], b = a.deriveSubjectScopes, x = () => {
265
+ let e = b?.subjectTable ?? "actors", t = y.map((t) => F(t, e));
266
+ if (b === void 0) return {
267
+ subjectTable: e,
268
+ paths: t,
269
+ limitations: []
270
+ };
271
+ let n = C({
272
+ subjectTable: e,
273
+ ...b.maxDepth === void 0 ? {} : { maxDepth: b.maxDepth },
274
+ ...b.exclude === void 0 ? {} : { exclude: b.exclude }
275
+ }), 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(">")}`));
276
+ return {
277
+ ...n,
278
+ paths: [...n.paths, ...i]
279
+ };
280
+ }, S = () => x().paths;
281
+ for (let e of a.retention ?? []) (e.action ?? "delete") === "anonymize" && R(`retention policy '${e.table}'`, e.anonymizeFields);
282
+ a.erasure?.mode === "anonymize" && R("erasure defaults", a.erasure.anonymizeFields);
283
+ let w = typeof a.fieldEncryption == "object" ? a.fieldEncryption.secretKey ?? "VOLTRO_FIELD_ENCRYPTION_KEY" : "VOLTRO_FIELD_ENCRYPTION_KEY", T = a.fieldEncryption ? [{
284
+ name: w,
179
285
  required: !1,
180
286
  secret: !0,
181
287
  description: "AES-256-GCM key for `.encrypted()` columns (field encryption). Lose the key, lose the data — GCM fails closed."
182
- }] : [], S = [], C, w, T = () => void 0, D = [], O = null, k = !1, A = () => {
183
- if (!C) throw Error("governance: data store not bound yet");
184
- return C;
185
- }, P = {
186
- exportSubject: (e) => M(A(), y, e),
288
+ }] : [], E = [], D, O, k = () => void 0, j = [], M = null, N = !1, P = () => {
289
+ if (!D) throw Error("governance: data store not bound yet");
290
+ return D;
291
+ }, I = {
292
+ subjectGraph: x,
293
+ exportSubject: (e) => L(P(), S(), e),
187
294
  eraseSubject: async (e, t) => {
188
- let n = await N(A(), y, e, t ?? a.erasure ?? {});
189
- return S.unshift(n), S.length > 1e3 && (S.length = V), n;
295
+ let n = {
296
+ ...a.erasure,
297
+ ...t
298
+ }, r = await z(P(), S(), e, n);
299
+ return E.unshift(r), E.length > 1e3 && (E.length = Z), r;
190
300
  },
191
301
  recordConsent: (e) => v.record(e),
192
302
  hasConsent: (e, t) => v.has(e, t),
193
303
  runRetentionNow: async () => {
194
- if (!C || !a.retention) return;
195
- let e = await j(C, a.retention);
196
- D = e, O = (/* @__PURE__ */ new Date()).toISOString(), T("retention sweep", { report: e });
304
+ if (!D || !a.retention) return;
305
+ let e = await U(D, a.retention);
306
+ j = e, M = (/* @__PURE__ */ new Date()).toISOString(), k("retention sweep", { report: e });
197
307
  }
198
- }, I = [
308
+ }, B = [
199
309
  {
200
310
  ...n,
201
311
  description: "GDPR data export for a subject (admin only).",
202
312
  execute: (e, t) => o.gen(function* () {
203
- return yield* u(t.request.subject, c), yield* o.promise(() => P.exportSubject(e.subjectId));
313
+ return yield* d(t.request.subject, c), yield* o.promise(() => I.exportSubject(e.subjectId));
204
314
  })
205
315
  },
206
316
  {
207
317
  ...t,
208
318
  description: "GDPR erasure (right-to-be-forgotten) for a subject (admin only).",
209
319
  execute: (e, t) => o.gen(function* () {
210
- yield* u(t.request.subject, c);
320
+ yield* d(t.request.subject, c);
211
321
  let { subjectId: n, mode: r } = e;
212
- return yield* o.promise(() => P.eraseSubject(n, r ? { mode: r } : void 0));
322
+ return yield* o.promise(() => I.eraseSubject(n, r ? { mode: r } : void 0));
213
323
  })
214
324
  },
215
325
  {
@@ -217,7 +327,7 @@ var E = (e) => {
217
327
  description: "Record the calling subject's consent for a purpose.",
218
328
  execute: (e, t) => o.promise(async () => {
219
329
  let { purpose: n, granted: r } = e, i = t.request.subject;
220
- return await P.recordConsent({
330
+ return await I.recordConsent({
221
331
  subjectId: i.id ?? "anonymous",
222
332
  purpose: n,
223
333
  granted: r,
@@ -231,35 +341,36 @@ var E = (e) => {
231
341
  description: "Whether the calling subject has granted consent for a purpose.",
232
342
  execute: (e, t) => o.promise(async () => {
233
343
  let n = t.request.subject;
234
- return { granted: await P.hasConsent(n.id ?? "anonymous", e.purpose) };
344
+ return { granted: await I.hasConsent(n.id ?? "anonymous", e.purpose) };
235
345
  })
236
346
  }
237
- ], L = (e) => ({
347
+ ], V = (e) => ({
238
348
  kind: "json",
239
349
  data: e
240
- }), z = (e, t) => ({
350
+ }), H = (e, t) => ({
241
351
  kind: "json",
242
352
  status: e,
243
353
  data: { error: t }
244
- }), U = (e, t) => new URLSearchParams(new URL(e, "http://x").search).get(t);
354
+ }), W = (e, t) => new URLSearchParams(new URL(e, "http://x").search).get(t);
245
355
  return l({
246
- name: d,
356
+ name: f,
357
+ baseName: $,
247
358
  description: "Data governance — retention TTL sweep, GDPR export/erasure, consent ledger.",
248
359
  permissions: [
249
360
  "store:write",
250
361
  "inspect:read",
251
362
  "inspect:write"
252
363
  ],
253
- declaredEnv: x,
254
- services: s.succeed(H, P),
255
- routes: I,
364
+ declaredEnv: T,
365
+ services: s.succeed(Q, I),
366
+ routes: B,
256
367
  rpcClientDescriptors: r,
257
368
  inspectEndpoints: [
258
369
  {
259
370
  method: "GET",
260
371
  path: "/status",
261
372
  description: "Retention policies, subject scopes, field-encryption state, last sweep.",
262
- handler: () => o.succeed(L({
373
+ handler: () => o.succeed(V({
263
374
  retention: (a.retention ?? []).map((e) => ({
264
375
  table: e.table,
265
376
  ttlMs: e.ttlMs,
@@ -269,26 +380,45 @@ var E = (e) => {
269
380
  table: e.table,
270
381
  subjectField: e.subjectField
271
382
  })),
272
- fieldEncryption: k,
383
+ fieldEncryption: N,
273
384
  sweepIntervalMs: a.sweepIntervalMs ?? 36e5,
274
- lastSweepAt: O,
275
- lastSweep: D,
276
- erasureCount: S.length
385
+ lastSweepAt: M,
386
+ lastSweep: j,
387
+ erasureCount: E.length
277
388
  }))
278
389
  },
279
390
  {
280
391
  method: "GET",
281
392
  path: "/erasures",
282
393
  description: "GDPR erasure log (most recent first).",
283
- handler: () => o.succeed(L({ erasures: S.slice(0, 100) }))
394
+ handler: () => o.succeed(V({ erasures: E.slice(0, 100) }))
395
+ },
396
+ {
397
+ method: "GET",
398
+ path: "/subject-graph",
399
+ description: "The routes a DSAR export/erasure walks — AND what the derivation cannot see.",
400
+ handler: () => o.sync(() => {
401
+ let e = x();
402
+ return V({
403
+ subjectTable: e.subjectTable,
404
+ derived: b !== void 0,
405
+ paths: e.paths.map((t) => ({
406
+ table: t.table,
407
+ depth: t.depth,
408
+ via: t.via,
409
+ route: [e.subjectTable, ...t.hops.map((e) => `${e.table}.${e.foreignKey}`)]
410
+ })),
411
+ limitations: e.limitations
412
+ });
413
+ })
284
414
  },
285
415
  {
286
416
  method: "GET",
287
417
  path: "/consent",
288
418
  description: "Consent ledger for a subject (?subjectId=…).",
289
419
  handler: (e) => {
290
- let t = U(e.url, "subjectId");
291
- return t ? o.promise(async () => L({ records: await v.list(t) })) : o.succeed(z(400, "subjectId query param required"));
420
+ let t = W(e.url, "subjectId");
421
+ return t ? o.promise(async () => V({ records: await v.list(t) })) : o.succeed(H(400, "subjectId query param required"));
292
422
  }
293
423
  },
294
424
  {
@@ -300,13 +430,13 @@ var E = (e) => {
300
430
  try {
301
431
  t = JSON.parse(e.body || "{}");
302
432
  } catch {
303
- return z(400, "invalid JSON body");
433
+ return H(400, "invalid JSON body");
304
434
  }
305
- if (!t.subjectId) return z(400, "subjectId required");
435
+ if (!t.subjectId) return H(400, "subjectId required");
306
436
  try {
307
- return L({ bundle: await P.exportSubject(t.subjectId) });
437
+ return V({ bundle: await I.exportSubject(t.subjectId) });
308
438
  } catch (e) {
309
- return z(500, e.message);
439
+ return H(500, e.message);
310
440
  }
311
441
  })
312
442
  },
@@ -319,13 +449,13 @@ var E = (e) => {
319
449
  try {
320
450
  t = JSON.parse(e.body || "{}");
321
451
  } catch {
322
- return z(400, "invalid JSON body");
452
+ return H(400, "invalid JSON body");
323
453
  }
324
- if (!t.subjectId) return z(400, "subjectId required");
454
+ if (!t.subjectId) return H(400, "subjectId required");
325
455
  try {
326
- return L({ entry: await P.eraseSubject(t.subjectId, t.mode ? { mode: t.mode } : void 0) });
456
+ return V({ entry: await I.eraseSubject(t.subjectId, t.mode ? { mode: t.mode } : void 0) });
327
457
  } catch (e) {
328
- return z(500, e.message);
458
+ return H(500, e.message);
329
459
  }
330
460
  })
331
461
  },
@@ -333,38 +463,45 @@ var E = (e) => {
333
463
  method: "POST",
334
464
  path: "/sweep",
335
465
  description: "Run the retention sweep now.",
336
- handler: () => o.promise(async () => (await P.runRetentionNow(), L({
337
- lastSweepAt: O,
338
- lastSweep: D
466
+ handler: () => o.promise(async () => (await I.runRetentionNow(), V({
467
+ lastSweepAt: M,
468
+ lastSweep: j
339
469
  })))
340
470
  }
341
471
  ],
342
- ...f ? { extendSchema: { tables: R } } : {},
472
+ ...p ? { extendSchema: { tables: J } } : {},
343
473
  bindDataStore: (e, t) => {
344
- if (C = E(e), f && (g = B(e)), a.retention && a.retention.length > 0) {
474
+ if (D = A(e), p && (g = X(e)), a.retention && a.retention.length > 0) {
345
475
  let e = a.sweepIntervalMs ?? 36e5;
346
- w = t?.scheduleCoordinated("governance.retention.sweep", e, () => P.runRetentionNow());
476
+ O = t?.scheduleCoordinated("governance.retention.sweep", e, () => I.runRetentionNow());
347
477
  }
348
478
  },
349
479
  onActivate: (e) => o.gen(function* () {
350
- T = (t, n) => e.logger.warn(t, n);
480
+ k = (t, n) => e.logger.warn(t, n);
351
481
  let t = !1;
352
482
  if (a.fieldEncryption) {
353
- let e = b, n = yield* o.promise(() => h(e));
483
+ let e = w, n = yield* o.promise(() => h(e));
354
484
  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`));
355
- _(m(n)), t = !0, k = !0;
485
+ _(ee(n)), t = !0, N = !0;
486
+ }
487
+ !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.");
488
+ let n = x();
489
+ if (b !== void 0) {
490
+ let t = n.limitations.filter((e) => e.kind === "unreachable").length;
491
+ 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.`);
356
492
  }
357
- !f && !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."), e.logger.info("governance active", {
493
+ e.logger.info("governance active", {
358
494
  retention: a.retention?.length ?? 0,
359
- scopes: y.length,
495
+ scopes: n.paths.length,
496
+ scopeSource: b === void 0 ? "declared" : "derived",
360
497
  fieldEncryption: t,
361
- consent: f ? "datastore" : a.consent ? "custom" : "memory"
498
+ consent: p ? "datastore" : a.consent ? "custom" : "memory"
362
499
  });
363
500
  }),
364
501
  onDeactivate: () => o.sync(() => {
365
- w?.stop(), w = void 0, a.fieldEncryption && _(void 0);
502
+ O?.stop(), O = void 0, a.fieldEncryption && _(void 0);
366
503
  })
367
504
  });
368
505
  };
369
506
  //#endregion
370
- export { I as CONSENT_TABLE, v as ENCRYPTED_PREFIX, V as ERASURE_LOG_CAP, H as GovernanceService, L as consentTable, R as consentTables, B as dataStoreConsentStore, E as dataStoreGovStore, d as deriveKey, N as eraseSubject, M as exportSubject, U as governancePlugin, C as isEncrypted, f as makeAesCipher, p as makeFieldCipher, F as memoryConsentStore, D as memoryGovStore, j as runRetention, k as selectExpired, g as setFieldCipher };
507
+ export { K as CONSENT_TABLE, v as DEFAULT_SUBJECT_GRAPH_DEPTH, y as ENCRYPTED_PREFIX, Z as ERASURE_LOG_CAP, Q as GovernanceService, M as SUBJECT_KEY_CHUNK, N as SUBJECT_ROW_CAP, I as collectSubjectRows, q as consentTable, J as consentTables, X as dataStoreConsentStore, A as dataStoreGovStore, f as deriveKey, S as deriveSubjectGraph, z as eraseSubject, L as exportSubject, T as formatSubjectGraph, te as governancePlugin, D as isEncrypted, p as makeAesCipher, m as makeFieldCipher, G as memoryConsentStore, j as memoryGovStore, U as runRetention, F as scopeToPath, V as selectExpired, g as setFieldCipher };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltro/plugin-governance",
3
- "version": "0.32.0",
3
+ "version": "0.34.0",
4
4
  "description": "Data governance — retention policies (TTL sweep), GDPR subject export + erasure (right-to-be-forgotten), and consent recording. Builds on audit + soft-delete.",
5
5
  "keywords": [
6
6
  "voltro",
@@ -27,7 +27,8 @@
27
27
  "types": "./dist/rpc.d.ts",
28
28
  "import": "./dist/rpc.js",
29
29
  "default": "./dist/rpc.js"
30
- }
30
+ },
31
+ "./package.json": "./package.json"
31
32
  },
32
33
  "main": "./dist/index.js",
33
34
  "module": "./dist/index.js",
@@ -37,9 +38,9 @@
37
38
  "node": ">=24.0.0"
38
39
  },
39
40
  "dependencies": {
40
- "@voltro/database": "0.32.0",
41
- "@voltro/protocol": "0.32.0",
42
- "@voltro/runtime": "0.32.0"
41
+ "@voltro/database": "0.34.0",
42
+ "@voltro/protocol": "0.34.0",
43
+ "@voltro/runtime": "0.34.0"
43
44
  },
44
45
  "peerDependencies": {
45
46
  "effect": "^3.22.0"