@voltro/plugin-governance 0.85.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/CHANGELOG.md +5987 -0
- package/THIRD-PARTY-NOTICES.md +864 -253
- package/dist/consent.js +55 -0
- package/dist/gdpr.js +118 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +193 -442
- 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 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
|
+
//#region src/index.ts
|
|
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
|
+
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, D = () => {
|
|
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 = le({
|
|
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 = () => D().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 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,
|
|
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
|
+
}))] : [], 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),
|
|
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(Y(), k(), e, n);
|
|
60
|
+
return F.unshift(r), F.length > 1e3 && (F.length = G), 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 (!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 });
|
|
392
68
|
}
|
|
393
|
-
},
|
|
69
|
+
}, ue = [
|
|
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* w(t.request.subject, C), yield* S.promise(() => X.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* w(t.request.subject, C);
|
|
406
82
|
let { subjectId: n, mode: r } = e;
|
|
407
|
-
return yield*
|
|
83
|
+
return yield* S.promise(() => X.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 X.recordConsent({
|
|
416
92
|
subjectId: i.id ?? "anonymous",
|
|
417
93
|
purpose: n,
|
|
418
94
|
granted: r,
|
|
@@ -424,39 +100,98 @@ 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 X.hasConsent(n.id ?? "anonymous", e.purpose) };
|
|
430
106
|
})
|
|
431
107
|
}
|
|
432
|
-
],
|
|
108
|
+
], Z = (e) => ({
|
|
433
109
|
kind: "json",
|
|
434
110
|
data: e
|
|
435
|
-
}),
|
|
111
|
+
}), Q = (e, t) => ({
|
|
436
112
|
kind: "json",
|
|
437
113
|
status: e,
|
|
438
114
|
data: { error: t }
|
|
439
|
-
}),
|
|
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 = [
|
|
440
174
|
{
|
|
441
175
|
method: "GET",
|
|
442
176
|
path: "/status",
|
|
443
177
|
scope: "process",
|
|
444
|
-
description: "Retention policies, subject scopes, field-encryption state, last sweep.",
|
|
445
|
-
handler: () =>
|
|
446
|
-
retention: (
|
|
178
|
+
description: "Retention policies, subject scopes, field-encryption state and keys, last sweep.",
|
|
179
|
+
handler: () => S.promise(async () => Z({
|
|
180
|
+
retention: (o.retention ?? []).map((e) => ({
|
|
447
181
|
table: e.table,
|
|
448
182
|
ttlMs: e.ttlMs,
|
|
449
183
|
action: e.action ?? "delete"
|
|
450
184
|
})),
|
|
451
|
-
scopes:
|
|
185
|
+
scopes: _.map((e) => ({
|
|
452
186
|
table: e.table,
|
|
453
187
|
subjectField: e.subjectField
|
|
454
188
|
})),
|
|
455
|
-
fieldEncryption:
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
189
|
+
fieldEncryption: J || A() !== void 0,
|
|
190
|
+
fieldKeys: await fe(),
|
|
191
|
+
sweepIntervalMs: o.sweepIntervalMs ?? 36e5,
|
|
192
|
+
lastSweepAt: W,
|
|
193
|
+
lastSweep: U,
|
|
194
|
+
erasureCount: F.length
|
|
460
195
|
}))
|
|
461
196
|
},
|
|
462
197
|
{
|
|
@@ -464,18 +199,18 @@ var N = (e) => {
|
|
|
464
199
|
path: "/erasures",
|
|
465
200
|
scope: "process",
|
|
466
201
|
description: "GDPR erasure log (most recent first).",
|
|
467
|
-
handler: () =>
|
|
202
|
+
handler: () => S.succeed(Z({ erasures: F.slice(0, 100) }))
|
|
468
203
|
},
|
|
469
204
|
{
|
|
470
205
|
method: "GET",
|
|
471
206
|
path: "/subject-graph",
|
|
472
207
|
scope: "declaration",
|
|
473
208
|
description: "The routes a DSAR export/erasure walks — AND what the derivation cannot see.",
|
|
474
|
-
handler: () =>
|
|
475
|
-
let e =
|
|
476
|
-
return
|
|
209
|
+
handler: () => S.sync(() => {
|
|
210
|
+
let e = D();
|
|
211
|
+
return Z({
|
|
477
212
|
subjectTable: e.subjectTable,
|
|
478
|
-
derived:
|
|
213
|
+
derived: x !== void 0,
|
|
479
214
|
paths: e.paths.map((t) => ({
|
|
480
215
|
table: t.table,
|
|
481
216
|
depth: t.depth,
|
|
@@ -489,29 +224,29 @@ var N = (e) => {
|
|
|
489
224
|
{
|
|
490
225
|
method: "GET",
|
|
491
226
|
path: "/consent",
|
|
492
|
-
scope:
|
|
227
|
+
scope: c ? "shared-store" : "process",
|
|
493
228
|
description: "Consent ledger for a subject (?subjectId=…).",
|
|
494
229
|
handler: (e) => {
|
|
495
|
-
let t =
|
|
496
|
-
return t ?
|
|
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"));
|
|
497
232
|
}
|
|
498
233
|
},
|
|
499
234
|
{
|
|
500
235
|
method: "POST",
|
|
501
236
|
path: "/export",
|
|
502
237
|
description: "Run a GDPR subject export.",
|
|
503
|
-
handler: (e) =>
|
|
238
|
+
handler: (e) => S.promise(async () => {
|
|
504
239
|
let t;
|
|
505
240
|
try {
|
|
506
241
|
t = JSON.parse(e.body || "{}");
|
|
507
242
|
} catch {
|
|
508
|
-
return
|
|
243
|
+
return Q(400, "invalid JSON body");
|
|
509
244
|
}
|
|
510
|
-
if (typeof t != "object" || !t || Array.isArray(t) || !("subjectId" in t) || typeof t.subjectId != "string" || !t.subjectId.trim()) return
|
|
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");
|
|
511
246
|
try {
|
|
512
|
-
return
|
|
247
|
+
return Z({ bundle: await X.exportSubject(t.subjectId) });
|
|
513
248
|
} catch (e) {
|
|
514
|
-
return
|
|
249
|
+
return Q(500, e.message);
|
|
515
250
|
}
|
|
516
251
|
})
|
|
517
252
|
},
|
|
@@ -519,20 +254,20 @@ var N = (e) => {
|
|
|
519
254
|
method: "POST",
|
|
520
255
|
path: "/erase",
|
|
521
256
|
description: "Run a GDPR erasure (right-to-be-forgotten).",
|
|
522
|
-
handler: (e) =>
|
|
257
|
+
handler: (e) => S.promise(async () => {
|
|
523
258
|
let t;
|
|
524
259
|
try {
|
|
525
260
|
t = JSON.parse(e.body || "{}");
|
|
526
261
|
} catch {
|
|
527
|
-
return
|
|
262
|
+
return Q(400, "invalid JSON body");
|
|
528
263
|
}
|
|
529
|
-
if (typeof t != "object" || !t || Array.isArray(t) || !("subjectId" in t) || typeof t.subjectId != "string" || !t.subjectId.trim()) return
|
|
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");
|
|
530
265
|
let n = "mode" in t ? t.mode : void 0;
|
|
531
|
-
if (n !== void 0 && n !== "delete" && n !== "anonymize") return
|
|
266
|
+
if (n !== void 0 && n !== "delete" && n !== "anonymize") return Q(400, "mode must be delete or anonymize");
|
|
532
267
|
try {
|
|
533
|
-
return
|
|
268
|
+
return Z({ entry: await X.eraseSubject(t.subjectId, n ? { mode: n } : void 0) });
|
|
534
269
|
} catch (e) {
|
|
535
|
-
return
|
|
270
|
+
return Q(500, e.message);
|
|
536
271
|
}
|
|
537
272
|
})
|
|
538
273
|
},
|
|
@@ -540,64 +275,80 @@ var N = (e) => {
|
|
|
540
275
|
method: "POST",
|
|
541
276
|
path: "/sweep",
|
|
542
277
|
description: "Run the retention sweep now.",
|
|
543
|
-
handler: () =>
|
|
544
|
-
lastSweepAt:
|
|
545
|
-
lastSweep:
|
|
278
|
+
handler: () => S.promise(async () => (await X.runRetentionNow(), Z({
|
|
279
|
+
lastSweepAt: W,
|
|
280
|
+
lastSweep: U
|
|
546
281
|
})))
|
|
547
282
|
}
|
|
548
283
|
];
|
|
549
|
-
return
|
|
550
|
-
name:
|
|
551
|
-
baseName:
|
|
284
|
+
return te({
|
|
285
|
+
name: s,
|
|
286
|
+
baseName: q,
|
|
552
287
|
description: "Data governance — retention TTL sweep, GDPR export/erasure, consent ledger.",
|
|
553
288
|
permissions: [
|
|
554
289
|
"store:write",
|
|
555
290
|
"inspect:read",
|
|
556
291
|
"inspect:write"
|
|
557
292
|
],
|
|
558
|
-
declaredEnv:
|
|
559
|
-
services:
|
|
560
|
-
routes:
|
|
293
|
+
declaredEnv: P,
|
|
294
|
+
services: ee.succeed(K, X),
|
|
295
|
+
routes: ue,
|
|
561
296
|
rpcClientDescriptors: r,
|
|
562
|
-
inspectEndpoints:
|
|
563
|
-
...
|
|
297
|
+
inspectEndpoints: pe,
|
|
298
|
+
...c ? { extendSchema: { tables: v } } : {},
|
|
564
299
|
bindDataStore: (e, t) => {
|
|
565
|
-
if (
|
|
566
|
-
let e =
|
|
567
|
-
|
|
300
|
+
if (R = a(e), B = e, c && (h = y(e)), o.retention && o.retention.length > 0) {
|
|
301
|
+
let e = o.sweepIntervalMs ?? 36e5;
|
|
302
|
+
V = t?.scheduleCoordinated("governance.retention.sweep", e, () => X.runRetentionNow());
|
|
568
303
|
}
|
|
569
304
|
},
|
|
570
|
-
onActivate: (e) =>
|
|
571
|
-
|
|
305
|
+
onActivate: (e) => S.gen(function* () {
|
|
306
|
+
H = (t, n) => e.logger.warn(t, n);
|
|
572
307
|
let t = !1;
|
|
573
|
-
if (
|
|
574
|
-
let e =
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
308
|
+
if (o.fieldEncryption) {
|
|
309
|
+
let e = M, n = yield* S.promise(() => L(e)), r = yield* S.promise(() => L(E)), i = [];
|
|
310
|
+
for (let e of N) {
|
|
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));
|
|
580
331
|
}
|
|
581
|
-
|
|
332
|
+
t = !0, J = !0;
|
|
582
333
|
}
|
|
583
|
-
!
|
|
584
|
-
let n =
|
|
585
|
-
if (
|
|
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.");
|
|
335
|
+
let n = D();
|
|
336
|
+
if (x !== void 0) {
|
|
586
337
|
let t = n.limitations.filter((e) => e.kind === "unreachable").length;
|
|
587
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.`);
|
|
588
339
|
}
|
|
589
340
|
e.logger.info("governance active", {
|
|
590
|
-
retention:
|
|
341
|
+
retention: o.retention?.length ?? 0,
|
|
591
342
|
scopes: n.paths.length,
|
|
592
|
-
scopeSource:
|
|
343
|
+
scopeSource: x === void 0 ? "declared" : "derived",
|
|
593
344
|
fieldEncryption: t,
|
|
594
|
-
consent:
|
|
345
|
+
consent: c ? "datastore" : o.consent ? "custom" : "memory"
|
|
595
346
|
});
|
|
596
347
|
}),
|
|
597
|
-
onDeactivate: () =>
|
|
598
|
-
|
|
348
|
+
onDeactivate: () => S.sync(() => {
|
|
349
|
+
V?.stop(), V = void 0, o.fieldEncryption && z(void 0);
|
|
599
350
|
})
|
|
600
351
|
});
|
|
601
352
|
};
|
|
602
353
|
//#endregion
|
|
603
|
-
export {
|
|
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 };
|