@voltro/plugin-governance 0.85.0 → 0.86.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 +666 -0
- package/THIRD-PARTY-NOTICES.md +5 -33
- package/dist/consent.js +55 -0
- package/dist/gdpr.js +116 -0
- package/dist/index.js +115 -439
- package/dist/retention.js +52 -0
- package/dist/store.js +120 -0
- package/package.json +4 -4
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 {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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 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";
|
|
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,
|
|
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, O = () => {
|
|
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 =
|
|
27
|
+
let n = R({
|
|
352
28
|
subjectTable: e,
|
|
353
|
-
...
|
|
354
|
-
...
|
|
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
|
-
},
|
|
361
|
-
for (let e of
|
|
362
|
-
|
|
363
|
-
let
|
|
364
|
-
name:
|
|
36
|
+
}, k = () => O().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 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,
|
|
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
|
-
}, ...
|
|
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
|
-
}))] : [],
|
|
374
|
-
if (!
|
|
375
|
-
return
|
|
376
|
-
},
|
|
377
|
-
subjectGraph:
|
|
378
|
-
exportSubject: (e) =>
|
|
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),
|
|
379
55
|
eraseSubject: async (e, t) => {
|
|
380
56
|
let n = {
|
|
381
|
-
...
|
|
57
|
+
...o.erasure,
|
|
382
58
|
...t
|
|
383
|
-
}, r = await
|
|
384
|
-
return
|
|
59
|
+
}, r = await d(q(), k(), e, n);
|
|
60
|
+
return I.unshift(r), I.length > 1e3 && (I.length = V), r;
|
|
385
61
|
},
|
|
386
|
-
recordConsent: (e) =>
|
|
387
|
-
hasConsent: (e, t) =>
|
|
62
|
+
recordConsent: (e) => g.record(e),
|
|
63
|
+
hasConsent: (e, t) => g.has(e, t),
|
|
388
64
|
runRetentionNow: async () => {
|
|
389
|
-
if (!
|
|
390
|
-
let e = await
|
|
391
|
-
|
|
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 });
|
|
392
68
|
}
|
|
393
|
-
},
|
|
69
|
+
}, Y = [
|
|
394
70
|
{
|
|
395
71
|
...n,
|
|
396
72
|
description: "GDPR data export for a subject (admin only).",
|
|
397
|
-
execute: (e, t) =>
|
|
398
|
-
return yield*
|
|
73
|
+
execute: (e, t) => S.gen(function* () {
|
|
74
|
+
return yield* D(t.request.subject, w), yield* S.promise(() => J.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) =>
|
|
405
|
-
yield*
|
|
80
|
+
execute: (e, t) => S.gen(function* () {
|
|
81
|
+
yield* D(t.request.subject, w);
|
|
406
82
|
let { subjectId: n, mode: r } = e;
|
|
407
|
-
return yield*
|
|
83
|
+
return yield* S.promise(() => J.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) =>
|
|
89
|
+
execute: (e, t) => S.promise(async () => {
|
|
414
90
|
let { purpose: n, granted: r } = e, i = t.request.subject;
|
|
415
|
-
return await
|
|
91
|
+
return await J.recordConsent({
|
|
416
92
|
subjectId: i.id ?? "anonymous",
|
|
417
93
|
purpose: n,
|
|
418
94
|
granted: r,
|
|
@@ -424,39 +100,39 @@ 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) =>
|
|
103
|
+
execute: (e, t) => S.promise(async () => {
|
|
428
104
|
let n = t.request.subject;
|
|
429
|
-
return { granted: await
|
|
105
|
+
return { granted: await J.hasConsent(n.id ?? "anonymous", e.purpose) };
|
|
430
106
|
})
|
|
431
107
|
}
|
|
432
|
-
],
|
|
108
|
+
], X = (e) => ({
|
|
433
109
|
kind: "json",
|
|
434
110
|
data: e
|
|
435
|
-
}),
|
|
111
|
+
}), Z = (e, t) => ({
|
|
436
112
|
kind: "json",
|
|
437
113
|
status: e,
|
|
438
114
|
data: { error: t }
|
|
439
|
-
}),
|
|
115
|
+
}), Q = (e, t) => new URLSearchParams(new URL(e, "http://x").search).get(t), $ = [
|
|
440
116
|
{
|
|
441
117
|
method: "GET",
|
|
442
118
|
path: "/status",
|
|
443
119
|
scope: "process",
|
|
444
120
|
description: "Retention policies, subject scopes, field-encryption state, last sweep.",
|
|
445
|
-
handler: () =>
|
|
446
|
-
retention: (
|
|
121
|
+
handler: () => S.succeed(X({
|
|
122
|
+
retention: (o.retention ?? []).map((e) => ({
|
|
447
123
|
table: e.table,
|
|
448
124
|
ttlMs: e.ttlMs,
|
|
449
125
|
action: e.action ?? "delete"
|
|
450
126
|
})),
|
|
451
|
-
scopes:
|
|
127
|
+
scopes: _.map((e) => ({
|
|
452
128
|
table: e.table,
|
|
453
129
|
subjectField: e.subjectField
|
|
454
130
|
})),
|
|
455
|
-
fieldEncryption:
|
|
456
|
-
sweepIntervalMs:
|
|
457
|
-
lastSweepAt:
|
|
458
|
-
lastSweep:
|
|
459
|
-
erasureCount:
|
|
131
|
+
fieldEncryption: K,
|
|
132
|
+
sweepIntervalMs: o.sweepIntervalMs ?? 36e5,
|
|
133
|
+
lastSweepAt: G,
|
|
134
|
+
lastSweep: W,
|
|
135
|
+
erasureCount: I.length
|
|
460
136
|
}))
|
|
461
137
|
},
|
|
462
138
|
{
|
|
@@ -464,18 +140,18 @@ var N = (e) => {
|
|
|
464
140
|
path: "/erasures",
|
|
465
141
|
scope: "process",
|
|
466
142
|
description: "GDPR erasure log (most recent first).",
|
|
467
|
-
handler: () =>
|
|
143
|
+
handler: () => S.succeed(X({ erasures: I.slice(0, 100) }))
|
|
468
144
|
},
|
|
469
145
|
{
|
|
470
146
|
method: "GET",
|
|
471
147
|
path: "/subject-graph",
|
|
472
148
|
scope: "declaration",
|
|
473
149
|
description: "The routes a DSAR export/erasure walks — AND what the derivation cannot see.",
|
|
474
|
-
handler: () =>
|
|
475
|
-
let e =
|
|
476
|
-
return
|
|
150
|
+
handler: () => S.sync(() => {
|
|
151
|
+
let e = O();
|
|
152
|
+
return X({
|
|
477
153
|
subjectTable: e.subjectTable,
|
|
478
|
-
derived:
|
|
154
|
+
derived: x !== void 0,
|
|
479
155
|
paths: e.paths.map((t) => ({
|
|
480
156
|
table: t.table,
|
|
481
157
|
depth: t.depth,
|
|
@@ -489,29 +165,29 @@ var N = (e) => {
|
|
|
489
165
|
{
|
|
490
166
|
method: "GET",
|
|
491
167
|
path: "/consent",
|
|
492
|
-
scope:
|
|
168
|
+
scope: c ? "shared-store" : "process",
|
|
493
169
|
description: "Consent ledger for a subject (?subjectId=…).",
|
|
494
170
|
handler: (e) => {
|
|
495
|
-
let t =
|
|
496
|
-
return t ?
|
|
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"));
|
|
497
173
|
}
|
|
498
174
|
},
|
|
499
175
|
{
|
|
500
176
|
method: "POST",
|
|
501
177
|
path: "/export",
|
|
502
178
|
description: "Run a GDPR subject export.",
|
|
503
|
-
handler: (e) =>
|
|
179
|
+
handler: (e) => S.promise(async () => {
|
|
504
180
|
let t;
|
|
505
181
|
try {
|
|
506
182
|
t = JSON.parse(e.body || "{}");
|
|
507
183
|
} catch {
|
|
508
|
-
return
|
|
184
|
+
return Z(400, "invalid JSON body");
|
|
509
185
|
}
|
|
510
|
-
if (typeof t != "object" || !t || Array.isArray(t) || !("subjectId" in t) || typeof t.subjectId != "string" || !t.subjectId.trim()) return
|
|
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");
|
|
511
187
|
try {
|
|
512
|
-
return
|
|
188
|
+
return X({ bundle: await J.exportSubject(t.subjectId) });
|
|
513
189
|
} catch (e) {
|
|
514
|
-
return
|
|
190
|
+
return Z(500, e.message);
|
|
515
191
|
}
|
|
516
192
|
})
|
|
517
193
|
},
|
|
@@ -519,20 +195,20 @@ var N = (e) => {
|
|
|
519
195
|
method: "POST",
|
|
520
196
|
path: "/erase",
|
|
521
197
|
description: "Run a GDPR erasure (right-to-be-forgotten).",
|
|
522
|
-
handler: (e) =>
|
|
198
|
+
handler: (e) => S.promise(async () => {
|
|
523
199
|
let t;
|
|
524
200
|
try {
|
|
525
201
|
t = JSON.parse(e.body || "{}");
|
|
526
202
|
} catch {
|
|
527
|
-
return
|
|
203
|
+
return Z(400, "invalid JSON body");
|
|
528
204
|
}
|
|
529
|
-
if (typeof t != "object" || !t || Array.isArray(t) || !("subjectId" in t) || typeof t.subjectId != "string" || !t.subjectId.trim()) return
|
|
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");
|
|
530
206
|
let n = "mode" in t ? t.mode : void 0;
|
|
531
|
-
if (n !== void 0 && n !== "delete" && n !== "anonymize") return
|
|
207
|
+
if (n !== void 0 && n !== "delete" && n !== "anonymize") return Z(400, "mode must be delete or anonymize");
|
|
532
208
|
try {
|
|
533
|
-
return
|
|
209
|
+
return X({ entry: await J.eraseSubject(t.subjectId, n ? { mode: n } : void 0) });
|
|
534
210
|
} catch (e) {
|
|
535
|
-
return
|
|
211
|
+
return Z(500, e.message);
|
|
536
212
|
}
|
|
537
213
|
})
|
|
538
214
|
},
|
|
@@ -540,64 +216,64 @@ var N = (e) => {
|
|
|
540
216
|
method: "POST",
|
|
541
217
|
path: "/sweep",
|
|
542
218
|
description: "Run the retention sweep now.",
|
|
543
|
-
handler: () =>
|
|
544
|
-
lastSweepAt:
|
|
545
|
-
lastSweep:
|
|
219
|
+
handler: () => S.promise(async () => (await J.runRetentionNow(), X({
|
|
220
|
+
lastSweepAt: G,
|
|
221
|
+
lastSweep: W
|
|
546
222
|
})))
|
|
547
223
|
}
|
|
548
224
|
];
|
|
549
|
-
return
|
|
550
|
-
name:
|
|
551
|
-
baseName:
|
|
225
|
+
return T({
|
|
226
|
+
name: s,
|
|
227
|
+
baseName: U,
|
|
552
228
|
description: "Data governance — retention TTL sweep, GDPR export/erasure, consent ledger.",
|
|
553
229
|
permissions: [
|
|
554
230
|
"store:write",
|
|
555
231
|
"inspect:read",
|
|
556
232
|
"inspect:write"
|
|
557
233
|
],
|
|
558
|
-
declaredEnv:
|
|
559
|
-
services:
|
|
560
|
-
routes:
|
|
234
|
+
declaredEnv: F,
|
|
235
|
+
services: C.succeed(H, J),
|
|
236
|
+
routes: Y,
|
|
561
237
|
rpcClientDescriptors: r,
|
|
562
|
-
inspectEndpoints:
|
|
563
|
-
...
|
|
238
|
+
inspectEndpoints: $,
|
|
239
|
+
...c ? { extendSchema: { tables: v } } : {},
|
|
564
240
|
bindDataStore: (e, t) => {
|
|
565
|
-
if (
|
|
566
|
-
let e =
|
|
567
|
-
|
|
241
|
+
if (L = a(e), c && (h = y(e)), o.retention && o.retention.length > 0) {
|
|
242
|
+
let e = o.sweepIntervalMs ?? 36e5;
|
|
243
|
+
z = t?.scheduleCoordinated("governance.retention.sweep", e, () => J.runRetentionNow());
|
|
568
244
|
}
|
|
569
245
|
},
|
|
570
|
-
onActivate: (e) =>
|
|
571
|
-
|
|
246
|
+
onActivate: (e) => S.gen(function* () {
|
|
247
|
+
B = (t, n) => e.logger.warn(t, n);
|
|
572
248
|
let t = !1;
|
|
573
|
-
if (
|
|
574
|
-
let e =
|
|
575
|
-
if (!n) return yield*
|
|
249
|
+
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`));
|
|
576
252
|
let r = [];
|
|
577
|
-
for (let e of
|
|
578
|
-
let t = yield*
|
|
253
|
+
for (let e of N) {
|
|
254
|
+
let t = yield* S.promise(() => M(e));
|
|
579
255
|
t !== void 0 && t.trim() !== "" && r.push(t);
|
|
580
256
|
}
|
|
581
|
-
|
|
257
|
+
P(j(n, r)), t = !0, K = !0;
|
|
582
258
|
}
|
|
583
|
-
!
|
|
584
|
-
let n =
|
|
585
|
-
if (
|
|
259
|
+
!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();
|
|
261
|
+
if (x !== void 0) {
|
|
586
262
|
let t = n.limitations.filter((e) => e.kind === "unreachable").length;
|
|
587
263
|
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
264
|
}
|
|
589
265
|
e.logger.info("governance active", {
|
|
590
|
-
retention:
|
|
266
|
+
retention: o.retention?.length ?? 0,
|
|
591
267
|
scopes: n.paths.length,
|
|
592
|
-
scopeSource:
|
|
268
|
+
scopeSource: x === void 0 ? "declared" : "derived",
|
|
593
269
|
fieldEncryption: t,
|
|
594
|
-
consent:
|
|
270
|
+
consent: c ? "datastore" : o.consent ? "custom" : "memory"
|
|
595
271
|
});
|
|
596
272
|
}),
|
|
597
|
-
onDeactivate: () =>
|
|
598
|
-
|
|
273
|
+
onDeactivate: () => S.sync(() => {
|
|
274
|
+
z?.stop(), z = void 0, o.fieldEncryption && P(void 0);
|
|
599
275
|
})
|
|
600
276
|
});
|
|
601
277
|
};
|
|
602
278
|
//#endregion
|
|
603
|
-
export {
|
|
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 };
|