@voltro/plugin-governance 0.81.0 → 0.82.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.
@@ -4551,7 +4551,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
4551
4551
  SOFTWARE.
4552
4552
  ```
4553
4553
 
4554
- ## undici@8.10.0
4554
+ ## undici@8.11.2
4555
4555
 
4556
4556
  License: MIT
4557
4557
 
package/dist/index.d.ts CHANGED
@@ -221,6 +221,9 @@ export declare interface GovernancePluginOptions {
221
221
  */
222
222
  readonly fieldEncryption?: boolean | {
223
223
  readonly secretKey?: string;
224
+ /** Names of the secrets holding the keys a rotation retires — read, never
225
+ * written with. Default `['VOLTRO_FIELD_ENCRYPTION_KEY_PREVIOUS']`. */
226
+ readonly previousSecretKeys?: ReadonlyArray<string>;
224
227
  };
225
228
  /**
226
229
  * Namespace for this plugin's rpc tags + inspect endpoints. Default `governance`.
package/dist/index.js CHANGED
@@ -280,37 +280,42 @@ var A = (e) => {
280
280
  }, S = () => x().paths;
281
281
  for (let e of a.retention ?? []) (e.action ?? "delete") === "anonymize" && R(`retention policy '${e.table}'`, e.anonymizeFields);
282
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 ? [{
283
+ let w = typeof a.fieldEncryption == "object" ? a.fieldEncryption.secretKey ?? "VOLTRO_FIELD_ENCRYPTION_KEY" : "VOLTRO_FIELD_ENCRYPTION_KEY", T = typeof a.fieldEncryption == "object" ? a.fieldEncryption.previousSecretKeys ?? ["VOLTRO_FIELD_ENCRYPTION_KEY_PREVIOUS"] : ["VOLTRO_FIELD_ENCRYPTION_KEY_PREVIOUS"], E = a.fieldEncryption ? [{
284
284
  name: w,
285
285
  required: !1,
286
286
  secret: !0,
287
287
  description: "AES-256-GCM key for `.encrypted()` columns (field encryption). Lose the key, lose the data — GCM fails closed."
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 = {
288
+ }, ...T.map((e) => ({
289
+ name: e,
290
+ required: !1,
291
+ secret: !0,
292
+ description: "A retired field-encryption key, read during a rotation so values written under it stay readable. Remove once they are re-encrypted."
293
+ }))] : [], D = [], O, k, j = () => void 0, M = [], N = null, P = !1, I = () => {
294
+ if (!O) throw Error("governance: data store not bound yet");
295
+ return O;
296
+ }, B = {
292
297
  subjectGraph: x,
293
- exportSubject: (e) => L(P(), S(), e),
298
+ exportSubject: (e) => L(I(), S(), e),
294
299
  eraseSubject: async (e, t) => {
295
300
  let n = {
296
301
  ...a.erasure,
297
302
  ...t
298
- }, r = await z(P(), S(), e, n);
299
- return E.unshift(r), E.length > 1e3 && (E.length = Z), r;
303
+ }, r = await z(I(), S(), e, n);
304
+ return D.unshift(r), D.length > 1e3 && (D.length = Z), r;
300
305
  },
301
306
  recordConsent: (e) => v.record(e),
302
307
  hasConsent: (e, t) => v.has(e, t),
303
308
  runRetentionNow: async () => {
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 });
309
+ if (!O || !a.retention) return;
310
+ let e = await U(O, a.retention);
311
+ M = e, N = (/* @__PURE__ */ new Date()).toISOString(), j("retention sweep", { report: e });
307
312
  }
308
- }, B = [
313
+ }, V = [
309
314
  {
310
315
  ...n,
311
316
  description: "GDPR data export for a subject (admin only).",
312
317
  execute: (e, t) => o.gen(function* () {
313
- return yield* d(t.request.subject, c), yield* o.promise(() => I.exportSubject(e.subjectId));
318
+ return yield* d(t.request.subject, c), yield* o.promise(() => B.exportSubject(e.subjectId));
314
319
  })
315
320
  },
316
321
  {
@@ -319,7 +324,7 @@ var A = (e) => {
319
324
  execute: (e, t) => o.gen(function* () {
320
325
  yield* d(t.request.subject, c);
321
326
  let { subjectId: n, mode: r } = e;
322
- return yield* o.promise(() => I.eraseSubject(n, r ? { mode: r } : void 0));
327
+ return yield* o.promise(() => B.eraseSubject(n, r ? { mode: r } : void 0));
323
328
  })
324
329
  },
325
330
  {
@@ -327,7 +332,7 @@ var A = (e) => {
327
332
  description: "Record the calling subject's consent for a purpose.",
328
333
  execute: (e, t) => o.promise(async () => {
329
334
  let { purpose: n, granted: r } = e, i = t.request.subject;
330
- return await I.recordConsent({
335
+ return await B.recordConsent({
331
336
  subjectId: i.id ?? "anonymous",
332
337
  purpose: n,
333
338
  granted: r,
@@ -341,23 +346,23 @@ var A = (e) => {
341
346
  description: "Whether the calling subject has granted consent for a purpose.",
342
347
  execute: (e, t) => o.promise(async () => {
343
348
  let n = t.request.subject;
344
- return { granted: await I.hasConsent(n.id ?? "anonymous", e.purpose) };
349
+ return { granted: await B.hasConsent(n.id ?? "anonymous", e.purpose) };
345
350
  })
346
351
  }
347
- ], V = (e) => ({
352
+ ], H = (e) => ({
348
353
  kind: "json",
349
354
  data: e
350
- }), H = (e, t) => ({
355
+ }), W = (e, t) => ({
351
356
  kind: "json",
352
357
  status: e,
353
358
  data: { error: t }
354
- }), W = (e, t) => new URLSearchParams(new URL(e, "http://x").search).get(t), K = [
359
+ }), K = (e, t) => new URLSearchParams(new URL(e, "http://x").search).get(t), q = [
355
360
  {
356
361
  method: "GET",
357
362
  path: "/status",
358
363
  scope: "process",
359
364
  description: "Retention policies, subject scopes, field-encryption state, last sweep.",
360
- handler: () => o.succeed(V({
365
+ handler: () => o.succeed(H({
361
366
  retention: (a.retention ?? []).map((e) => ({
362
367
  table: e.table,
363
368
  ttlMs: e.ttlMs,
@@ -367,11 +372,11 @@ var A = (e) => {
367
372
  table: e.table,
368
373
  subjectField: e.subjectField
369
374
  })),
370
- fieldEncryption: N,
375
+ fieldEncryption: P,
371
376
  sweepIntervalMs: a.sweepIntervalMs ?? 36e5,
372
- lastSweepAt: M,
373
- lastSweep: j,
374
- erasureCount: E.length
377
+ lastSweepAt: N,
378
+ lastSweep: M,
379
+ erasureCount: D.length
375
380
  }))
376
381
  },
377
382
  {
@@ -379,7 +384,7 @@ var A = (e) => {
379
384
  path: "/erasures",
380
385
  scope: "process",
381
386
  description: "GDPR erasure log (most recent first).",
382
- handler: () => o.succeed(V({ erasures: E.slice(0, 100) }))
387
+ handler: () => o.succeed(H({ erasures: D.slice(0, 100) }))
383
388
  },
384
389
  {
385
390
  method: "GET",
@@ -388,7 +393,7 @@ var A = (e) => {
388
393
  description: "The routes a DSAR export/erasure walks — AND what the derivation cannot see.",
389
394
  handler: () => o.sync(() => {
390
395
  let e = x();
391
- return V({
396
+ return H({
392
397
  subjectTable: e.subjectTable,
393
398
  derived: b !== void 0,
394
399
  paths: e.paths.map((t) => ({
@@ -407,8 +412,8 @@ var A = (e) => {
407
412
  scope: p ? "shared-store" : "process",
408
413
  description: "Consent ledger for a subject (?subjectId=…).",
409
414
  handler: (e) => {
410
- let t = W(e.url, "subjectId");
411
- return t ? o.promise(async () => V({ records: await v.list(t) })) : o.succeed(H(400, "subjectId query param required"));
415
+ let t = K(e.url, "subjectId");
416
+ return t ? o.promise(async () => H({ records: await v.list(t) })) : o.succeed(W(400, "subjectId query param required"));
412
417
  }
413
418
  },
414
419
  {
@@ -420,13 +425,13 @@ var A = (e) => {
420
425
  try {
421
426
  t = JSON.parse(e.body || "{}");
422
427
  } catch {
423
- return H(400, "invalid JSON body");
428
+ return W(400, "invalid JSON body");
424
429
  }
425
- if (typeof t != "object" || !t || Array.isArray(t) || !("subjectId" in t) || typeof t.subjectId != "string" || !t.subjectId.trim()) return H(400, "subjectId must be a non-empty string");
430
+ if (typeof t != "object" || !t || Array.isArray(t) || !("subjectId" in t) || typeof t.subjectId != "string" || !t.subjectId.trim()) return W(400, "subjectId must be a non-empty string");
426
431
  try {
427
- return V({ bundle: await I.exportSubject(t.subjectId) });
432
+ return H({ bundle: await B.exportSubject(t.subjectId) });
428
433
  } catch (e) {
429
- return H(500, e.message);
434
+ return W(500, e.message);
430
435
  }
431
436
  })
432
437
  },
@@ -439,15 +444,15 @@ var A = (e) => {
439
444
  try {
440
445
  t = JSON.parse(e.body || "{}");
441
446
  } catch {
442
- return H(400, "invalid JSON body");
447
+ return W(400, "invalid JSON body");
443
448
  }
444
- if (typeof t != "object" || !t || Array.isArray(t) || !("subjectId" in t) || typeof t.subjectId != "string" || !t.subjectId.trim()) return H(400, "subjectId must be a non-empty string");
449
+ if (typeof t != "object" || !t || Array.isArray(t) || !("subjectId" in t) || typeof t.subjectId != "string" || !t.subjectId.trim()) return W(400, "subjectId must be a non-empty string");
445
450
  let n = "mode" in t ? t.mode : void 0;
446
- if (n !== void 0 && n !== "delete" && n !== "anonymize") return H(400, "mode must be delete or anonymize");
451
+ if (n !== void 0 && n !== "delete" && n !== "anonymize") return W(400, "mode must be delete or anonymize");
447
452
  try {
448
- return V({ entry: await I.eraseSubject(t.subjectId, n ? { mode: n } : void 0) });
453
+ return H({ entry: await B.eraseSubject(t.subjectId, n ? { mode: n } : void 0) });
449
454
  } catch (e) {
450
- return H(500, e.message);
455
+ return W(500, e.message);
451
456
  }
452
457
  })
453
458
  },
@@ -455,9 +460,9 @@ var A = (e) => {
455
460
  method: "POST",
456
461
  path: "/sweep",
457
462
  description: "Run the retention sweep now.",
458
- handler: () => o.promise(async () => (await I.runRetentionNow(), V({
459
- lastSweepAt: M,
460
- lastSweep: j
463
+ handler: () => o.promise(async () => (await B.runRetentionNow(), H({
464
+ lastSweepAt: N,
465
+ lastSweep: M
461
466
  })))
462
467
  }
463
468
  ];
@@ -470,25 +475,30 @@ var A = (e) => {
470
475
  "inspect:read",
471
476
  "inspect:write"
472
477
  ],
473
- declaredEnv: T,
474
- services: s.succeed(Q, I),
475
- routes: B,
478
+ declaredEnv: E,
479
+ services: s.succeed(Q, B),
480
+ routes: V,
476
481
  rpcClientDescriptors: r,
477
- inspectEndpoints: K,
482
+ inspectEndpoints: q,
478
483
  ...p ? { extendSchema: { tables: J } } : {},
479
484
  bindDataStore: (e, t) => {
480
- if (D = A(e), p && (g = X(e)), a.retention && a.retention.length > 0) {
485
+ if (O = A(e), p && (g = X(e)), a.retention && a.retention.length > 0) {
481
486
  let e = a.sweepIntervalMs ?? 36e5;
482
- O = t?.scheduleCoordinated("governance.retention.sweep", e, () => I.runRetentionNow());
487
+ k = t?.scheduleCoordinated("governance.retention.sweep", e, () => B.runRetentionNow());
483
488
  }
484
489
  },
485
490
  onActivate: (e) => o.gen(function* () {
486
- k = (t, n) => e.logger.warn(t, n);
491
+ j = (t, n) => e.logger.warn(t, n);
487
492
  let t = !1;
488
493
  if (a.fieldEncryption) {
489
494
  let e = w, n = yield* o.promise(() => h(e));
490
495
  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`));
491
- _(ee(n)), t = !0, N = !0;
496
+ let r = [];
497
+ for (let e of T) {
498
+ let t = yield* o.promise(() => h(e));
499
+ t !== void 0 && t.trim() !== "" && r.push(t);
500
+ }
501
+ _(ee(n, r)), t = !0, P = !0;
492
502
  }
493
503
  !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.");
494
504
  let n = x();
@@ -505,7 +515,7 @@ var A = (e) => {
505
515
  });
506
516
  }),
507
517
  onDeactivate: () => o.sync(() => {
508
- O?.stop(), O = void 0, a.fieldEncryption && _(void 0);
518
+ k?.stop(), k = void 0, a.fieldEncryption && _(void 0);
509
519
  })
510
520
  });
511
521
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@voltro/plugin-governance",
3
- "version": "0.81.0",
3
+ "version": "0.82.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.81.0",
42
- "@voltro/protocol": "0.81.0",
43
- "@voltro/runtime": "0.81.0"
41
+ "@voltro/database": "0.82.0",
42
+ "@voltro/protocol": "0.82.0",
43
+ "@voltro/runtime": "0.82.0"
44
44
  },
45
45
  "peerDependencies": {
46
46
  "effect": "^3.22.2"