@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/CHANGELOG.md +2006 -0
- package/THIRD-PARTY-NOTICES.md +1 -29
- package/dist/index.d.ts +174 -11
- package/dist/index.js +254 -117
- package/package.json +6 -5
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,
|
|
4
|
-
import { deriveKey as
|
|
5
|
-
import {
|
|
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
|
|
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
|
-
},
|
|
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
|
-
},
|
|
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
|
-
},
|
|
171
|
+
}, V = (e, t, n) => {
|
|
60
172
|
let r = t.dateField ?? "createdAt";
|
|
61
173
|
return e.filter((e) => {
|
|
62
|
-
let i =
|
|
174
|
+
let i = B(e, r, n);
|
|
63
175
|
return i !== null && i > t.ttlMs;
|
|
64
176
|
}).map((e) => String(e.id));
|
|
65
|
-
},
|
|
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"
|
|
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 =
|
|
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
|
-
},
|
|
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
|
-
},
|
|
132
|
-
id:
|
|
133
|
-
subjectId:
|
|
134
|
-
purpose:
|
|
135
|
-
granted:
|
|
136
|
-
at:
|
|
137
|
-
tenantId:
|
|
138
|
-
}).index("byConsentSubjectPurpose", ["subjectId", "purpose"]).index("byConsentSubject", ["subjectId"]),
|
|
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
|
-
}),
|
|
227
|
+
}), X = (e) => {
|
|
145
228
|
let t = (t, n) => e.query({
|
|
146
|
-
table:
|
|
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(
|
|
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(
|
|
168
|
-
return r[0] ?
|
|
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(
|
|
253
|
+
list: async (e) => (await t(w("subjectId", e))).map(Y)
|
|
171
254
|
};
|
|
172
|
-
},
|
|
173
|
-
let
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
},
|
|
178
|
-
|
|
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
|
-
}] : [],
|
|
183
|
-
if (!
|
|
184
|
-
return
|
|
185
|
-
},
|
|
186
|
-
|
|
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 =
|
|
189
|
-
|
|
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 (!
|
|
195
|
-
let e = await
|
|
196
|
-
|
|
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
|
-
},
|
|
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*
|
|
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*
|
|
320
|
+
yield* d(t.request.subject, c);
|
|
211
321
|
let { subjectId: n, mode: r } = e;
|
|
212
|
-
return yield* o.promise(() =>
|
|
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
|
|
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
|
|
344
|
+
return { granted: await I.hasConsent(n.id ?? "anonymous", e.purpose) };
|
|
235
345
|
})
|
|
236
346
|
}
|
|
237
|
-
],
|
|
347
|
+
], V = (e) => ({
|
|
238
348
|
kind: "json",
|
|
239
349
|
data: e
|
|
240
|
-
}),
|
|
350
|
+
}), H = (e, t) => ({
|
|
241
351
|
kind: "json",
|
|
242
352
|
status: e,
|
|
243
353
|
data: { error: t }
|
|
244
|
-
}),
|
|
354
|
+
}), W = (e, t) => new URLSearchParams(new URL(e, "http://x").search).get(t);
|
|
245
355
|
return l({
|
|
246
|
-
name:
|
|
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:
|
|
254
|
-
services: s.succeed(
|
|
255
|
-
routes:
|
|
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(
|
|
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:
|
|
383
|
+
fieldEncryption: N,
|
|
273
384
|
sweepIntervalMs: a.sweepIntervalMs ?? 36e5,
|
|
274
|
-
lastSweepAt:
|
|
275
|
-
lastSweep:
|
|
276
|
-
erasureCount:
|
|
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(
|
|
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 =
|
|
291
|
-
return t ? o.promise(async () =>
|
|
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
|
|
433
|
+
return H(400, "invalid JSON body");
|
|
304
434
|
}
|
|
305
|
-
if (!t.subjectId) return
|
|
435
|
+
if (!t.subjectId) return H(400, "subjectId required");
|
|
306
436
|
try {
|
|
307
|
-
return
|
|
437
|
+
return V({ bundle: await I.exportSubject(t.subjectId) });
|
|
308
438
|
} catch (e) {
|
|
309
|
-
return
|
|
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
|
|
452
|
+
return H(400, "invalid JSON body");
|
|
323
453
|
}
|
|
324
|
-
if (!t.subjectId) return
|
|
454
|
+
if (!t.subjectId) return H(400, "subjectId required");
|
|
325
455
|
try {
|
|
326
|
-
return
|
|
456
|
+
return V({ entry: await I.eraseSubject(t.subjectId, t.mode ? { mode: t.mode } : void 0) });
|
|
327
457
|
} catch (e) {
|
|
328
|
-
return
|
|
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
|
|
337
|
-
lastSweepAt:
|
|
338
|
-
lastSweep:
|
|
466
|
+
handler: () => o.promise(async () => (await I.runRetentionNow(), V({
|
|
467
|
+
lastSweepAt: M,
|
|
468
|
+
lastSweep: j
|
|
339
469
|
})))
|
|
340
470
|
}
|
|
341
471
|
],
|
|
342
|
-
...
|
|
472
|
+
...p ? { extendSchema: { tables: J } } : {},
|
|
343
473
|
bindDataStore: (e, t) => {
|
|
344
|
-
if (
|
|
474
|
+
if (D = A(e), p && (g = X(e)), a.retention && a.retention.length > 0) {
|
|
345
475
|
let e = a.sweepIntervalMs ?? 36e5;
|
|
346
|
-
|
|
476
|
+
O = t?.scheduleCoordinated("governance.retention.sweep", e, () => I.runRetentionNow());
|
|
347
477
|
}
|
|
348
478
|
},
|
|
349
479
|
onActivate: (e) => o.gen(function* () {
|
|
350
|
-
|
|
480
|
+
k = (t, n) => e.logger.warn(t, n);
|
|
351
481
|
let t = !1;
|
|
352
482
|
if (a.fieldEncryption) {
|
|
353
|
-
let 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
|
-
_(
|
|
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
|
-
|
|
493
|
+
e.logger.info("governance active", {
|
|
358
494
|
retention: a.retention?.length ?? 0,
|
|
359
|
-
scopes:
|
|
495
|
+
scopes: n.paths.length,
|
|
496
|
+
scopeSource: b === void 0 ? "declared" : "derived",
|
|
360
497
|
fieldEncryption: t,
|
|
361
|
-
consent:
|
|
498
|
+
consent: p ? "datastore" : a.consent ? "custom" : "memory"
|
|
362
499
|
});
|
|
363
500
|
}),
|
|
364
501
|
onDeactivate: () => o.sync(() => {
|
|
365
|
-
|
|
502
|
+
O?.stop(), O = void 0, a.fieldEncryption && _(void 0);
|
|
366
503
|
})
|
|
367
504
|
});
|
|
368
505
|
};
|
|
369
506
|
//#endregion
|
|
370
|
-
export {
|
|
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.
|
|
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.
|
|
41
|
-
"@voltro/protocol": "0.
|
|
42
|
-
"@voltro/runtime": "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"
|