@voltro/plugin-governance 0.86.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/gdpr.js CHANGED
@@ -55,22 +55,22 @@ var t = 500, n = 5e4, r = (e, t) => {
55
55
  }, u = async (t, n) => {
56
56
  let { hop: i, children: o } = t, s = c(i.table);
57
57
  for (let t of r(n, 500)) {
58
- let n;
58
+ let n, r = !1;
59
59
  for (;;) {
60
60
  if (o.size === 0 && s.truncated) return;
61
- let r = o.size ? 500 : Math.min(500, a - s.rows.size + 1), c = e.scan ? await e.scan(i.table, {
61
+ let c = o.size || r ? 500 : Math.min(500, a - s.rows.size + 1), d = e.scan ? await e.scan(i.table, {
62
62
  column: i.foreignKey,
63
63
  values: t,
64
- limit: r,
64
+ limit: c,
65
65
  ...n === void 0 ? {} : { after: n }
66
- }) : await e.where(i.table, i.foreignKey, t);
67
- if (l(i.table, c), c.length === 0) break;
68
- let d = [...new Set(c.map((e) => String(e[i.parentKey] ?? e.id)))];
69
- for (let e of o.values()) await u(e, d);
70
- if (!e.scan || c.length < r) break;
71
- let f = c[c.length - 1].id;
72
- if (f === void 0 || f === n) throw Error("governance scan must advance a stable id cursor");
73
- n = f;
66
+ }) : await e.where(i.table, i.foreignKey, t), f = s.rows.size;
67
+ if (l(i.table, d), r = d.length > 0 && s.rows.size === f, d.length === 0) break;
68
+ let p = [...new Set(d.map((e) => String(e[i.parentKey] ?? e.id)))];
69
+ for (let e of o.values()) await u(e, p);
70
+ if (!e.scan || d.length < c) break;
71
+ let m = d[d.length - 1].id;
72
+ if (m === void 0 || m === n) throw Error("governance scan must advance a stable id cursor");
73
+ n = m;
74
74
  }
75
75
  }
76
76
  };
@@ -81,7 +81,9 @@ var t = 500, n = 5e4, r = (e, t) => {
81
81
  ...t.truncated ? { truncated: !0 } : {}
82
82
  }));
83
83
  }, o = async (e, t, n) => {
84
- let r = await a(e, t, n), i = {};
84
+ let r = await a(e, t, n);
85
+ if (r.some((e) => e.truncated === !0)) throw Error("governance: subject export exceeds the per-table row limit; no bundle was returned");
86
+ let i = {};
85
87
  for (let e of r) e.rows.length > 0 && (i[e.table] = e.rows);
86
88
  return i;
87
89
  }, s = (e, t) => {
package/dist/index.d.ts CHANGED
@@ -5,10 +5,13 @@ import { deriveSubjectGraph } from '@voltro/database';
5
5
  import { DeriveSubjectGraphInput } from '@voltro/database';
6
6
  import { ENCRYPTED_PREFIX } from '@voltro/database';
7
7
  import { FieldCipher } from '@voltro/database';
8
+ import { fieldKeyIdOf } from '@voltro/runtime';
8
9
  import { formatSubjectGraph } from '@voltro/database';
9
10
  import { isEncrypted } from '@voltro/database';
10
11
  import { makeAesCipher } from '@voltro/runtime';
11
12
  import { makeFieldCipher } from '@voltro/runtime';
13
+ import { makeFieldKeyring } from '@voltro/runtime';
14
+ import { resolveFieldKeyring } from '@voltro/runtime';
12
15
  import { SchemaTable } from '@voltro/database';
13
16
  import { setFieldCipher } from '@voltro/runtime';
14
17
  import { SubjectGraph } from '@voltro/database';
@@ -155,6 +158,8 @@ export declare const exportSubject: (store: GovStore, paths: ReadonlyArray<Subje
155
158
 
156
159
  export { FieldCipher }
157
160
 
161
+ export { fieldKeyIdOf }
162
+
158
163
  export { formatSubjectGraph }
159
164
 
160
165
  export declare const governancePlugin: (options?: GovernancePluginOptions) => VoltroPlugin;
@@ -334,6 +339,8 @@ export { makeAesCipher }
334
339
 
335
340
  export { makeFieldCipher }
336
341
 
342
+ export { makeFieldKeyring }
343
+
337
344
  /**
338
345
  * In-memory consent store (default + test). Latest-write-wins per
339
346
  * (subject,purpose).
@@ -354,6 +361,8 @@ export declare const memoryGovStore: (seed?: Record<string, Array<Record<string,
354
361
  dump: () => Record<string, Array<Record<string, unknown>>>;
355
362
  };
356
363
 
364
+ export { resolveFieldKeyring }
365
+
357
366
  export declare interface RetentionPolicy {
358
367
  readonly table: string;
359
368
  /** Time-to-live in ms (e.g. 90 days = 90*86_400_000). */
package/dist/index.js CHANGED
@@ -3,28 +3,28 @@ import { dataStoreGovStore as a, memoryGovStore as o } from "./store.js";
3
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
4
  import { runRetention as m, selectExpired as h } from "./retention.js";
5
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 C } from "effect";
7
- import { ADMIN_SCOPE as w, definePlugin as T, pluginInstanceName as E, requireScope as D } from "@voltro/protocol";
8
- import { deriveKey as O, makeAesCipher as k, makeFieldCipher as A, makeFieldCipher as j, resolveSecret as M, setFieldCipher as N, setFieldCipher as P } from "@voltro/runtime";
9
- import { DEFAULT_SUBJECT_GRAPH_DEPTH as F, ENCRYPTED_PREFIX as I, deriveSubjectGraph as L, deriveSubjectGraph as R, formatSubjectGraph as z, isEncrypted as B } from "@voltro/database";
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
10
  //#region src/index.ts
11
- var V = 1e3, H = class extends x.Service()("@voltro/plugin-governance/GovernanceService") {}, U = "@voltro/plugin-governance", W = (o = {}) => {
12
- let s = E({
13
- base: U,
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
14
  alias: o.alias,
15
15
  instance: o.name
16
16
  }), c = o.consent === "datastore", u = o.consent && o.consent !== "datastore" ? o.consent : c ? void 0 : b(), h, g = {
17
17
  record: (e) => (u ?? h ?? b()).record(e),
18
18
  has: (e, t) => (u ?? h ?? b()).has(e, t),
19
19
  list: (e) => (u ?? h ?? b()).list(e)
20
- }, _ = o.subjectScopes ?? [], x = o.deriveSubjectScopes, O = () => {
20
+ }, _ = o.subjectScopes ?? [], x = o.deriveSubjectScopes, D = () => {
21
21
  let e = x?.subjectTable ?? "actors", t = _.map((t) => p(t, e));
22
22
  if (x === void 0) return {
23
23
  subjectTable: e,
24
24
  paths: t,
25
25
  limitations: []
26
26
  };
27
- let n = R({
27
+ let n = le({
28
28
  subjectTable: e,
29
29
  ...x.maxDepth === void 0 ? {} : { maxDepth: x.maxDepth },
30
30
  ...x.exclude === void 0 ? {} : { exclude: x.exclude }
@@ -33,11 +33,11 @@ var V = 1e3, H = class extends x.Service()("@voltro/plugin-governance/Governance
33
33
  ...n,
34
34
  paths: [...n.paths, ...i]
35
35
  };
36
- }, k = () => O().paths;
36
+ }, k = () => D().paths;
37
37
  for (let e of o.retention ?? []) (e.action ?? "delete") === "anonymize" && l(`retention policy '${e.table}'`, e.anonymizeFields);
38
38
  o.erasure?.mode === "anonymize" && l("erasure defaults", o.erasure.anonymizeFields);
39
- let A = 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"], F = o.fieldEncryption ? [{
40
- name: A,
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,
41
41
  required: !1,
42
42
  secret: !0,
43
43
  description: "AES-256-GCM key for `.encrypted()` columns (field encryption). Lose the key, lose the data — GCM fails closed."
@@ -46,41 +46,41 @@ var V = 1e3, H = class extends x.Service()("@voltro/plugin-governance/Governance
46
46
  required: !1,
47
47
  secret: !0,
48
48
  description: "A retired field-encryption key, read during a rotation so values written under it stay readable. Remove once they are re-encrypted."
49
- }))] : [], I = [], L, z, B = () => void 0, W = [], G = null, K = !1, q = () => {
50
- if (!L) throw Error("governance: data store not bound yet");
51
- return L;
52
- }, J = {
53
- subjectGraph: O,
54
- exportSubject: (e) => f(q(), k(), 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),
55
55
  eraseSubject: async (e, t) => {
56
56
  let n = {
57
57
  ...o.erasure,
58
58
  ...t
59
- }, r = await d(q(), k(), e, n);
60
- return I.unshift(r), I.length > 1e3 && (I.length = V), r;
59
+ }, r = await d(Y(), k(), e, n);
60
+ return F.unshift(r), F.length > 1e3 && (F.length = G), r;
61
61
  },
62
62
  recordConsent: (e) => g.record(e),
63
63
  hasConsent: (e, t) => g.has(e, t),
64
64
  runRetentionNow: async () => {
65
- if (!L || !o.retention) return;
66
- let e = await m(L, o.retention);
67
- W = e, G = (/* @__PURE__ */ new Date()).toISOString(), B("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 });
68
68
  }
69
- }, Y = [
69
+ }, ue = [
70
70
  {
71
71
  ...n,
72
72
  description: "GDPR data export for a subject (admin only).",
73
73
  execute: (e, t) => S.gen(function* () {
74
- return yield* D(t.request.subject, w), yield* S.promise(() => J.exportSubject(e.subjectId));
74
+ return yield* w(t.request.subject, C), yield* S.promise(() => X.exportSubject(e.subjectId));
75
75
  })
76
76
  },
77
77
  {
78
78
  ...t,
79
79
  description: "GDPR erasure (right-to-be-forgotten) for a subject (admin only).",
80
80
  execute: (e, t) => S.gen(function* () {
81
- yield* D(t.request.subject, w);
81
+ yield* w(t.request.subject, C);
82
82
  let { subjectId: n, mode: r } = e;
83
- return yield* S.promise(() => J.eraseSubject(n, r ? { mode: r } : void 0));
83
+ return yield* S.promise(() => X.eraseSubject(n, r ? { mode: r } : void 0));
84
84
  })
85
85
  },
86
86
  {
@@ -88,7 +88,7 @@ var V = 1e3, H = class extends x.Service()("@voltro/plugin-governance/Governance
88
88
  description: "Record the calling subject's consent for a purpose.",
89
89
  execute: (e, t) => S.promise(async () => {
90
90
  let { purpose: n, granted: r } = e, i = t.request.subject;
91
- return await J.recordConsent({
91
+ return await X.recordConsent({
92
92
  subjectId: i.id ?? "anonymous",
93
93
  purpose: n,
94
94
  granted: r,
@@ -102,23 +102,81 @@ var V = 1e3, H = class extends x.Service()("@voltro/plugin-governance/Governance
102
102
  description: "Whether the calling subject has granted consent for a purpose.",
103
103
  execute: (e, t) => S.promise(async () => {
104
104
  let n = t.request.subject;
105
- return { granted: await J.hasConsent(n.id ?? "anonymous", e.purpose) };
105
+ return { granted: await X.hasConsent(n.id ?? "anonymous", e.purpose) };
106
106
  })
107
107
  }
108
- ], X = (e) => ({
108
+ ], Z = (e) => ({
109
109
  kind: "json",
110
110
  data: e
111
- }), Z = (e, t) => ({
111
+ }), Q = (e, t) => ({
112
112
  kind: "json",
113
113
  status: e,
114
114
  data: { error: t }
115
- }), Q = (e, t) => new URLSearchParams(new URL(e, "http://x").search).get(t), $ = [
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 = [
116
174
  {
117
175
  method: "GET",
118
176
  path: "/status",
119
177
  scope: "process",
120
- description: "Retention policies, subject scopes, field-encryption state, last sweep.",
121
- handler: () => S.succeed(X({
178
+ description: "Retention policies, subject scopes, field-encryption state and keys, last sweep.",
179
+ handler: () => S.promise(async () => Z({
122
180
  retention: (o.retention ?? []).map((e) => ({
123
181
  table: e.table,
124
182
  ttlMs: e.ttlMs,
@@ -128,11 +186,12 @@ var V = 1e3, H = class extends x.Service()("@voltro/plugin-governance/Governance
128
186
  table: e.table,
129
187
  subjectField: e.subjectField
130
188
  })),
131
- fieldEncryption: K,
189
+ fieldEncryption: J || A() !== void 0,
190
+ fieldKeys: await fe(),
132
191
  sweepIntervalMs: o.sweepIntervalMs ?? 36e5,
133
- lastSweepAt: G,
134
- lastSweep: W,
135
- erasureCount: I.length
192
+ lastSweepAt: W,
193
+ lastSweep: U,
194
+ erasureCount: F.length
136
195
  }))
137
196
  },
138
197
  {
@@ -140,7 +199,7 @@ var V = 1e3, H = class extends x.Service()("@voltro/plugin-governance/Governance
140
199
  path: "/erasures",
141
200
  scope: "process",
142
201
  description: "GDPR erasure log (most recent first).",
143
- handler: () => S.succeed(X({ erasures: I.slice(0, 100) }))
202
+ handler: () => S.succeed(Z({ erasures: F.slice(0, 100) }))
144
203
  },
145
204
  {
146
205
  method: "GET",
@@ -148,8 +207,8 @@ var V = 1e3, H = class extends x.Service()("@voltro/plugin-governance/Governance
148
207
  scope: "declaration",
149
208
  description: "The routes a DSAR export/erasure walks — AND what the derivation cannot see.",
150
209
  handler: () => S.sync(() => {
151
- let e = O();
152
- return X({
210
+ let e = D();
211
+ return Z({
153
212
  subjectTable: e.subjectTable,
154
213
  derived: x !== void 0,
155
214
  paths: e.paths.map((t) => ({
@@ -168,8 +227,8 @@ var V = 1e3, H = class extends x.Service()("@voltro/plugin-governance/Governance
168
227
  scope: c ? "shared-store" : "process",
169
228
  description: "Consent ledger for a subject (?subjectId=…).",
170
229
  handler: (e) => {
171
- let t = Q(e.url, "subjectId");
172
- return t ? S.promise(async () => X({ records: await g.list(t) })) : S.succeed(Z(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"));
173
232
  }
174
233
  },
175
234
  {
@@ -181,13 +240,13 @@ var V = 1e3, H = class extends x.Service()("@voltro/plugin-governance/Governance
181
240
  try {
182
241
  t = JSON.parse(e.body || "{}");
183
242
  } catch {
184
- return Z(400, "invalid JSON body");
243
+ return Q(400, "invalid JSON body");
185
244
  }
186
- if (typeof t != "object" || !t || Array.isArray(t) || !("subjectId" in t) || typeof t.subjectId != "string" || !t.subjectId.trim()) return Z(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");
187
246
  try {
188
- return X({ bundle: await J.exportSubject(t.subjectId) });
247
+ return Z({ bundle: await X.exportSubject(t.subjectId) });
189
248
  } catch (e) {
190
- return Z(500, e.message);
249
+ return Q(500, e.message);
191
250
  }
192
251
  })
193
252
  },
@@ -200,15 +259,15 @@ var V = 1e3, H = class extends x.Service()("@voltro/plugin-governance/Governance
200
259
  try {
201
260
  t = JSON.parse(e.body || "{}");
202
261
  } catch {
203
- return Z(400, "invalid JSON body");
262
+ return Q(400, "invalid JSON body");
204
263
  }
205
- if (typeof t != "object" || !t || Array.isArray(t) || !("subjectId" in t) || typeof t.subjectId != "string" || !t.subjectId.trim()) return Z(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");
206
265
  let n = "mode" in t ? t.mode : void 0;
207
- if (n !== void 0 && n !== "delete" && n !== "anonymize") return Z(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");
208
267
  try {
209
- return X({ entry: await J.eraseSubject(t.subjectId, n ? { mode: n } : void 0) });
268
+ return Z({ entry: await X.eraseSubject(t.subjectId, n ? { mode: n } : void 0) });
210
269
  } catch (e) {
211
- return Z(500, e.message);
270
+ return Q(500, e.message);
212
271
  }
213
272
  })
214
273
  },
@@ -216,48 +275,64 @@ var V = 1e3, H = class extends x.Service()("@voltro/plugin-governance/Governance
216
275
  method: "POST",
217
276
  path: "/sweep",
218
277
  description: "Run the retention sweep now.",
219
- handler: () => S.promise(async () => (await J.runRetentionNow(), X({
220
- lastSweepAt: G,
221
- lastSweep: W
278
+ handler: () => S.promise(async () => (await X.runRetentionNow(), Z({
279
+ lastSweepAt: W,
280
+ lastSweep: U
222
281
  })))
223
282
  }
224
283
  ];
225
- return T({
284
+ return te({
226
285
  name: s,
227
- baseName: U,
286
+ baseName: q,
228
287
  description: "Data governance — retention TTL sweep, GDPR export/erasure, consent ledger.",
229
288
  permissions: [
230
289
  "store:write",
231
290
  "inspect:read",
232
291
  "inspect:write"
233
292
  ],
234
- declaredEnv: F,
235
- services: C.succeed(H, J),
236
- routes: Y,
293
+ declaredEnv: P,
294
+ services: ee.succeed(K, X),
295
+ routes: ue,
237
296
  rpcClientDescriptors: r,
238
- inspectEndpoints: $,
297
+ inspectEndpoints: pe,
239
298
  ...c ? { extendSchema: { tables: v } } : {},
240
299
  bindDataStore: (e, t) => {
241
- if (L = a(e), c && (h = y(e)), o.retention && o.retention.length > 0) {
300
+ if (R = a(e), B = e, c && (h = y(e)), o.retention && o.retention.length > 0) {
242
301
  let e = o.sweepIntervalMs ?? 36e5;
243
- z = t?.scheduleCoordinated("governance.retention.sweep", e, () => J.runRetentionNow());
302
+ V = t?.scheduleCoordinated("governance.retention.sweep", e, () => X.runRetentionNow());
244
303
  }
245
304
  },
246
305
  onActivate: (e) => S.gen(function* () {
247
- B = (t, n) => e.logger.warn(t, n);
306
+ H = (t, n) => e.logger.warn(t, n);
248
307
  let t = !1;
249
308
  if (o.fieldEncryption) {
250
- let e = A, n = yield* S.promise(() => M(e));
251
- 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`));
252
- let r = [];
309
+ let e = M, n = yield* S.promise(() => L(e)), r = yield* S.promise(() => L(E)), i = [];
253
310
  for (let e of N) {
254
- let t = yield* S.promise(() => M(e));
255
- t !== void 0 && t.trim() !== "" && r.push(t);
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));
256
331
  }
257
- P(j(n, r)), t = !0, K = !0;
332
+ t = !0, J = !0;
258
333
  }
259
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.");
260
- let n = O();
335
+ let n = D();
261
336
  if (x !== void 0) {
262
337
  let t = n.limitations.filter((e) => e.kind === "unreachable").length;
263
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.`);
@@ -271,9 +346,9 @@ var V = 1e3, H = class extends x.Service()("@voltro/plugin-governance/Governance
271
346
  });
272
347
  }),
273
348
  onDeactivate: () => S.sync(() => {
274
- z?.stop(), z = void 0, o.fieldEncryption && P(void 0);
349
+ V?.stop(), V = void 0, o.fieldEncryption && z(void 0);
275
350
  })
276
351
  });
277
352
  };
278
353
  //#endregion
279
- export { g as CONSENT_TABLE, F as DEFAULT_SUBJECT_GRAPH_DEPTH, I as ENCRYPTED_PREFIX, V as ERASURE_LOG_CAP, H 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, O as deriveKey, L as deriveSubjectGraph, d as eraseSubject, f as exportSubject, z as formatSubjectGraph, W as governancePlugin, B as isEncrypted, k as makeAesCipher, A as makeFieldCipher, b as memoryConsentStore, o as memoryGovStore, m as runRetention, p as scopeToPath, h as selectExpired, N 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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltro/plugin-governance",
3
- "version": "0.86.0",
3
+ "version": "0.87.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",
@@ -38,9 +38,9 @@
38
38
  "node": ">=24.0.0"
39
39
  },
40
40
  "dependencies": {
41
- "@voltro/database": "0.86.0",
42
- "@voltro/protocol": "0.86.0",
43
- "@voltro/runtime": "0.86.0"
41
+ "@voltro/database": "0.87.0",
42
+ "@voltro/protocol": "0.87.0",
43
+ "@voltro/runtime": "0.87.0"
44
44
  },
45
45
  "peerDependencies": {
46
46
  "effect": "4.0.1"