@voltro/plugin-governance 0.86.0 → 0.88.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 +5484 -0
- package/THIRD-PARTY-NOTICES.md +1511 -276
- package/dist/consent.js +1 -2
- package/dist/gdpr.js +15 -14
- package/dist/index.d.ts +9 -0
- package/dist/index.js +149 -75
- package/dist/retention.js +1 -2
- package/dist/rpc.js +1 -2
- package/dist/store.js +1 -2
- package/package.json +5 -5
package/dist/consent.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { and as e, boolean as t, eq as n, id as r, table as i, text as a } from "@voltro/database";
|
|
2
|
-
|
|
2
|
+
// src/consent.ts
|
|
3
3
|
var o = 1e4, s = (e = o) => {
|
|
4
4
|
let t = /* @__PURE__ */ new Map(), n = [], r = (e, t) => `${e}|${t}`;
|
|
5
5
|
return {
|
|
@@ -51,5 +51,4 @@ var o = 1e4, s = (e = o) => {
|
|
|
51
51
|
list: async (e) => (await r(n("subjectId", e))).map(d)
|
|
52
52
|
};
|
|
53
53
|
};
|
|
54
|
-
//#endregion
|
|
55
54
|
export { o as CONSENT_HISTORY_CAP, c as CONSENT_TABLE, l as consentTable, u as consentTables, f as dataStoreConsentStore, s as memoryConsentStore };
|
package/dist/gdpr.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { writeGovernanceRows as e } from "./store.js";
|
|
2
|
-
|
|
2
|
+
// src/gdpr.ts
|
|
3
3
|
var t = 500, n = 5e4, r = (e, t) => {
|
|
4
4
|
let n = [];
|
|
5
5
|
for (let r = 0; r < e.length; r += t) n.push(e.slice(r, r + t));
|
|
@@ -55,22 +55,22 @@ var t = 500, n = 5e4, r = (e, t) => {
|
|
|
55
55
|
}, u = async (t, n) => {
|
|
56
56
|
let { hop: i, children: o } = t, s = c(i.table);
|
|
57
57
|
for (let t of r(n, 500)) {
|
|
58
|
-
let n;
|
|
58
|
+
let n, r = !1;
|
|
59
59
|
for (;;) {
|
|
60
60
|
if (o.size === 0 && s.truncated) return;
|
|
61
|
-
let
|
|
61
|
+
let c = o.size || r ? 500 : Math.min(500, a - s.rows.size + 1), d = e.scan ? await e.scan(i.table, {
|
|
62
62
|
column: i.foreignKey,
|
|
63
63
|
values: t,
|
|
64
|
-
limit:
|
|
64
|
+
limit: c,
|
|
65
65
|
...n === void 0 ? {} : { after: n }
|
|
66
|
-
}) : await e.where(i.table, i.foreignKey, t);
|
|
67
|
-
if (l(i.table,
|
|
68
|
-
let
|
|
69
|
-
for (let e of o.values()) await u(e,
|
|
70
|
-
if (!e.scan ||
|
|
71
|
-
let
|
|
72
|
-
if (
|
|
73
|
-
n =
|
|
66
|
+
}) : await e.where(i.table, i.foreignKey, t), f = s.rows.size;
|
|
67
|
+
if (l(i.table, d), r = d.length > 0 && s.rows.size === f, d.length === 0) break;
|
|
68
|
+
let p = [...new Set(d.map((e) => String(e[i.parentKey] ?? e.id)))];
|
|
69
|
+
for (let e of o.values()) await u(e, p);
|
|
70
|
+
if (!e.scan || d.length < c) break;
|
|
71
|
+
let m = d[d.length - 1].id;
|
|
72
|
+
if (m === void 0 || m === n) throw Error("governance scan must advance a stable id cursor");
|
|
73
|
+
n = m;
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
};
|
|
@@ -81,7 +81,9 @@ var t = 500, n = 5e4, r = (e, t) => {
|
|
|
81
81
|
...t.truncated ? { truncated: !0 } : {}
|
|
82
82
|
}));
|
|
83
83
|
}, o = async (e, t, n) => {
|
|
84
|
-
let r = await a(e, t, n)
|
|
84
|
+
let r = await a(e, t, n);
|
|
85
|
+
if (r.some((e) => e.truncated === !0)) throw Error("governance: subject export exceeds the per-table row limit; no bundle was returned");
|
|
86
|
+
let i = {};
|
|
85
87
|
for (let e of r) e.rows.length > 0 && (i[e.table] = e.rows);
|
|
86
88
|
return i;
|
|
87
89
|
}, s = (e, t) => {
|
|
@@ -112,5 +114,4 @@ var t = 500, n = 5e4, r = (e, t) => {
|
|
|
112
114
|
...p.length > 0 ? { truncated: p } : {}
|
|
113
115
|
};
|
|
114
116
|
};
|
|
115
|
-
//#endregion
|
|
116
117
|
export { t as SUBJECT_KEY_CHUNK, n as SUBJECT_ROW_CAP, s as assertAnonymizable, a as collectSubjectRows, c as eraseSubject, o as exportSubject, i as scopeToPath };
|
package/dist/index.d.ts
CHANGED
|
@@ -5,10 +5,13 @@ import { deriveSubjectGraph } from '@voltro/database';
|
|
|
5
5
|
import { DeriveSubjectGraphInput } from '@voltro/database';
|
|
6
6
|
import { ENCRYPTED_PREFIX } from '@voltro/database';
|
|
7
7
|
import { FieldCipher } from '@voltro/database';
|
|
8
|
+
import { fieldKeyIdOf } from '@voltro/runtime';
|
|
8
9
|
import { formatSubjectGraph } from '@voltro/database';
|
|
9
10
|
import { isEncrypted } from '@voltro/database';
|
|
10
11
|
import { makeAesCipher } from '@voltro/runtime';
|
|
11
12
|
import { makeFieldCipher } from '@voltro/runtime';
|
|
13
|
+
import { makeFieldKeyring } from '@voltro/runtime';
|
|
14
|
+
import { resolveFieldKeyring } from '@voltro/runtime';
|
|
12
15
|
import { SchemaTable } from '@voltro/database';
|
|
13
16
|
import { setFieldCipher } from '@voltro/runtime';
|
|
14
17
|
import { SubjectGraph } from '@voltro/database';
|
|
@@ -155,6 +158,8 @@ export declare const exportSubject: (store: GovStore, paths: ReadonlyArray<Subje
|
|
|
155
158
|
|
|
156
159
|
export { FieldCipher }
|
|
157
160
|
|
|
161
|
+
export { fieldKeyIdOf }
|
|
162
|
+
|
|
158
163
|
export { formatSubjectGraph }
|
|
159
164
|
|
|
160
165
|
export declare const governancePlugin: (options?: GovernancePluginOptions) => VoltroPlugin;
|
|
@@ -334,6 +339,8 @@ export { makeAesCipher }
|
|
|
334
339
|
|
|
335
340
|
export { makeFieldCipher }
|
|
336
341
|
|
|
342
|
+
export { makeFieldKeyring }
|
|
343
|
+
|
|
337
344
|
/**
|
|
338
345
|
* In-memory consent store (default + test). Latest-write-wins per
|
|
339
346
|
* (subject,purpose).
|
|
@@ -354,6 +361,8 @@ export declare const memoryGovStore: (seed?: Record<string, Array<Record<string,
|
|
|
354
361
|
dump: () => Record<string, Array<Record<string, unknown>>>;
|
|
355
362
|
};
|
|
356
363
|
|
|
364
|
+
export { resolveFieldKeyring }
|
|
365
|
+
|
|
357
366
|
export declare interface RetentionPolicy {
|
|
358
367
|
readonly table: string;
|
|
359
368
|
/** Time-to-live in ms (e.g. 90 days = 90*86_400_000). */
|
package/dist/index.js
CHANGED
|
@@ -3,28 +3,28 @@ import { dataStoreGovStore as a, memoryGovStore as o } from "./store.js";
|
|
|
3
3
|
import { SUBJECT_KEY_CHUNK as s, SUBJECT_ROW_CAP as c, assertAnonymizable as l, collectSubjectRows as u, eraseSubject as d, exportSubject as f, scopeToPath as p } from "./gdpr.js";
|
|
4
4
|
import { runRetention as m, selectExpired as h } from "./retention.js";
|
|
5
5
|
import { CONSENT_TABLE as g, consentTable as _, consentTables as v, dataStoreConsentStore as y, memoryConsentStore as b } from "./consent.js";
|
|
6
|
-
import { Context as x, Effect as S, Layer as
|
|
7
|
-
import { ADMIN_SCOPE as
|
|
8
|
-
import { deriveKey as O,
|
|
9
|
-
import { DEFAULT_SUBJECT_GRAPH_DEPTH as
|
|
10
|
-
|
|
11
|
-
var
|
|
12
|
-
let s =
|
|
13
|
-
base:
|
|
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
|
+
// 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
14
|
alias: o.alias,
|
|
15
15
|
instance: o.name
|
|
16
16
|
}), c = o.consent === "datastore", u = o.consent && o.consent !== "datastore" ? o.consent : c ? void 0 : b(), h, g = {
|
|
17
17
|
record: (e) => (u ?? h ?? b()).record(e),
|
|
18
18
|
has: (e, t) => (u ?? h ?? b()).has(e, t),
|
|
19
19
|
list: (e) => (u ?? h ?? b()).list(e)
|
|
20
|
-
}, _ = o.subjectScopes ?? [], x = o.deriveSubjectScopes,
|
|
20
|
+
}, _ = o.subjectScopes ?? [], x = o.deriveSubjectScopes, D = () => {
|
|
21
21
|
let e = x?.subjectTable ?? "actors", t = _.map((t) => p(t, e));
|
|
22
22
|
if (x === void 0) return {
|
|
23
23
|
subjectTable: e,
|
|
24
24
|
paths: t,
|
|
25
25
|
limitations: []
|
|
26
26
|
};
|
|
27
|
-
let n =
|
|
27
|
+
let n = le({
|
|
28
28
|
subjectTable: e,
|
|
29
29
|
...x.maxDepth === void 0 ? {} : { maxDepth: x.maxDepth },
|
|
30
30
|
...x.exclude === void 0 ? {} : { exclude: x.exclude }
|
|
@@ -33,11 +33,11 @@ var V = 1e3, H = class extends x.Service()("@voltro/plugin-governance/Governance
|
|
|
33
33
|
...n,
|
|
34
34
|
paths: [...n.paths, ...i]
|
|
35
35
|
};
|
|
36
|
-
}, k = () =>
|
|
36
|
+
}, k = () => D().paths;
|
|
37
37
|
for (let e of o.retention ?? []) (e.action ?? "delete") === "anonymize" && l(`retention policy '${e.table}'`, e.anonymizeFields);
|
|
38
38
|
o.erasure?.mode === "anonymize" && l("erasure defaults", o.erasure.anonymizeFields);
|
|
39
|
-
let
|
|
40
|
-
name:
|
|
39
|
+
let M = typeof o.fieldEncryption == "object" ? o.fieldEncryption.secretKey ?? "VOLTRO_FIELD_ENCRYPTION_KEY" : "VOLTRO_FIELD_ENCRYPTION_KEY", N = typeof o.fieldEncryption == "object" ? o.fieldEncryption.previousSecretKeys ?? ["VOLTRO_FIELD_ENCRYPTION_KEY_PREVIOUS"] : ["VOLTRO_FIELD_ENCRYPTION_KEY_PREVIOUS"], P = o.fieldEncryption ? [{
|
|
40
|
+
name: M,
|
|
41
41
|
required: !1,
|
|
42
42
|
secret: !0,
|
|
43
43
|
description: "AES-256-GCM key for `.encrypted()` columns (field encryption). Lose the key, lose the data — GCM fails closed."
|
|
@@ -46,41 +46,41 @@ var V = 1e3, H = class extends x.Service()("@voltro/plugin-governance/Governance
|
|
|
46
46
|
required: !1,
|
|
47
47
|
secret: !0,
|
|
48
48
|
description: "A retired field-encryption key, read during a rotation so values written under it stay readable. Remove once they are re-encrypted."
|
|
49
|
-
}))] : [],
|
|
50
|
-
if (!
|
|
51
|
-
return
|
|
52
|
-
},
|
|
53
|
-
subjectGraph:
|
|
54
|
-
exportSubject: (e) => f(
|
|
49
|
+
}))] : [], F = [], R, B, V, H = () => void 0, U = [], W = null, J = !1, Y = () => {
|
|
50
|
+
if (!R) throw Error("governance: data store not bound yet");
|
|
51
|
+
return R;
|
|
52
|
+
}, X = {
|
|
53
|
+
subjectGraph: D,
|
|
54
|
+
exportSubject: (e) => f(Y(), k(), e),
|
|
55
55
|
eraseSubject: async (e, t) => {
|
|
56
56
|
let n = {
|
|
57
57
|
...o.erasure,
|
|
58
58
|
...t
|
|
59
|
-
}, r = await d(
|
|
60
|
-
return
|
|
59
|
+
}, r = await d(Y(), k(), e, n);
|
|
60
|
+
return F.unshift(r), F.length > 1e3 && (F.length = G), r;
|
|
61
61
|
},
|
|
62
62
|
recordConsent: (e) => g.record(e),
|
|
63
63
|
hasConsent: (e, t) => g.has(e, t),
|
|
64
64
|
runRetentionNow: async () => {
|
|
65
|
-
if (!
|
|
66
|
-
let e = await m(
|
|
67
|
-
|
|
65
|
+
if (!R || !o.retention) return;
|
|
66
|
+
let e = await m(R, o.retention);
|
|
67
|
+
U = e, W = (/* @__PURE__ */ new Date()).toISOString(), H("retention sweep", { report: e });
|
|
68
68
|
}
|
|
69
|
-
},
|
|
69
|
+
}, ue = [
|
|
70
70
|
{
|
|
71
71
|
...n,
|
|
72
72
|
description: "GDPR data export for a subject (admin only).",
|
|
73
73
|
execute: (e, t) => S.gen(function* () {
|
|
74
|
-
return yield*
|
|
74
|
+
return yield* w(t.request.subject, C), yield* S.promise(() => X.exportSubject(e.subjectId));
|
|
75
75
|
})
|
|
76
76
|
},
|
|
77
77
|
{
|
|
78
78
|
...t,
|
|
79
79
|
description: "GDPR erasure (right-to-be-forgotten) for a subject (admin only).",
|
|
80
80
|
execute: (e, t) => S.gen(function* () {
|
|
81
|
-
yield*
|
|
81
|
+
yield* w(t.request.subject, C);
|
|
82
82
|
let { subjectId: n, mode: r } = e;
|
|
83
|
-
return yield* S.promise(() =>
|
|
83
|
+
return yield* S.promise(() => X.eraseSubject(n, r ? { mode: r } : void 0));
|
|
84
84
|
})
|
|
85
85
|
},
|
|
86
86
|
{
|
|
@@ -88,7 +88,7 @@ var V = 1e3, H = class extends x.Service()("@voltro/plugin-governance/Governance
|
|
|
88
88
|
description: "Record the calling subject's consent for a purpose.",
|
|
89
89
|
execute: (e, t) => S.promise(async () => {
|
|
90
90
|
let { purpose: n, granted: r } = e, i = t.request.subject;
|
|
91
|
-
return await
|
|
91
|
+
return await X.recordConsent({
|
|
92
92
|
subjectId: i.id ?? "anonymous",
|
|
93
93
|
purpose: n,
|
|
94
94
|
granted: r,
|
|
@@ -102,23 +102,81 @@ var V = 1e3, H = class extends x.Service()("@voltro/plugin-governance/Governance
|
|
|
102
102
|
description: "Whether the calling subject has granted consent for a purpose.",
|
|
103
103
|
execute: (e, t) => S.promise(async () => {
|
|
104
104
|
let n = t.request.subject;
|
|
105
|
-
return { granted: await
|
|
105
|
+
return { granted: await X.hasConsent(n.id ?? "anonymous", e.purpose) };
|
|
106
106
|
})
|
|
107
107
|
}
|
|
108
|
-
],
|
|
108
|
+
], Z = (e) => ({
|
|
109
109
|
kind: "json",
|
|
110
110
|
data: e
|
|
111
|
-
}),
|
|
111
|
+
}), Q = (e, t) => ({
|
|
112
112
|
kind: "json",
|
|
113
113
|
status: e,
|
|
114
114
|
data: { error: t }
|
|
115
|
-
}),
|
|
115
|
+
}), de = (e, t) => new URLSearchParams(new URL(e, "http://x").search).get(t), $ = (e) => e === null ? null : e.toISOString(), fe = async () => {
|
|
116
|
+
let e = A();
|
|
117
|
+
if (!j(e)) return null;
|
|
118
|
+
let t = {
|
|
119
|
+
active: e.activeKeyId,
|
|
120
|
+
requested: e.requestedKeyId,
|
|
121
|
+
held: e.keyIds,
|
|
122
|
+
fingerprints: e.fingerprints,
|
|
123
|
+
decrypts: re()
|
|
124
|
+
};
|
|
125
|
+
if (B === void 0) return {
|
|
126
|
+
...t,
|
|
127
|
+
recorded: null,
|
|
128
|
+
error: "the data store is not bound yet"
|
|
129
|
+
};
|
|
130
|
+
try {
|
|
131
|
+
let [e, n, r] = await Promise.all([
|
|
132
|
+
ce(B),
|
|
133
|
+
oe(B),
|
|
134
|
+
se(B)
|
|
135
|
+
]), i = Date.now();
|
|
136
|
+
return {
|
|
137
|
+
...t,
|
|
138
|
+
recorded: {
|
|
139
|
+
keys: e.map((e) => ({
|
|
140
|
+
keyId: e.keyId,
|
|
141
|
+
state: e.state,
|
|
142
|
+
registeredAt: $(e.registeredAt),
|
|
143
|
+
activatedAt: $(e.activatedAt),
|
|
144
|
+
replacedAt: $(e.replacedAt),
|
|
145
|
+
retiredAt: $(e.retiredAt),
|
|
146
|
+
requestedAt: $(e.requestedAt)
|
|
147
|
+
})),
|
|
148
|
+
holders: n.map((e) => ({
|
|
149
|
+
holderId: e.holderId,
|
|
150
|
+
keyIds: e.keyIds,
|
|
151
|
+
activeKeyId: e.activeKeyId,
|
|
152
|
+
expiresAt: e.expiresAt.toISOString(),
|
|
153
|
+
live: e.expiresAt.getTime() > i
|
|
154
|
+
})),
|
|
155
|
+
lastCount: r.status === void 0 ? null : {
|
|
156
|
+
reportedAt: r.status.reportedAt.toISOString(),
|
|
157
|
+
...r.status.report
|
|
158
|
+
},
|
|
159
|
+
lastRekey: r.rekey === void 0 ? null : {
|
|
160
|
+
reportedAt: r.rekey.reportedAt.toISOString(),
|
|
161
|
+
...r.rekey.report
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
error: null
|
|
165
|
+
};
|
|
166
|
+
} catch (e) {
|
|
167
|
+
return {
|
|
168
|
+
...t,
|
|
169
|
+
recorded: null,
|
|
170
|
+
error: e instanceof Error ? e.message : String(e)
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
}, pe = [
|
|
116
174
|
{
|
|
117
175
|
method: "GET",
|
|
118
176
|
path: "/status",
|
|
119
177
|
scope: "process",
|
|
120
|
-
description: "Retention policies, subject scopes, field-encryption state, last sweep.",
|
|
121
|
-
handler: () => S.
|
|
178
|
+
description: "Retention policies, subject scopes, field-encryption state and keys, last sweep.",
|
|
179
|
+
handler: () => S.promise(async () => Z({
|
|
122
180
|
retention: (o.retention ?? []).map((e) => ({
|
|
123
181
|
table: e.table,
|
|
124
182
|
ttlMs: e.ttlMs,
|
|
@@ -128,11 +186,12 @@ var V = 1e3, H = class extends x.Service()("@voltro/plugin-governance/Governance
|
|
|
128
186
|
table: e.table,
|
|
129
187
|
subjectField: e.subjectField
|
|
130
188
|
})),
|
|
131
|
-
fieldEncryption:
|
|
189
|
+
fieldEncryption: J || A() !== void 0,
|
|
190
|
+
fieldKeys: await fe(),
|
|
132
191
|
sweepIntervalMs: o.sweepIntervalMs ?? 36e5,
|
|
133
|
-
lastSweepAt:
|
|
134
|
-
lastSweep:
|
|
135
|
-
erasureCount:
|
|
192
|
+
lastSweepAt: W,
|
|
193
|
+
lastSweep: U,
|
|
194
|
+
erasureCount: F.length
|
|
136
195
|
}))
|
|
137
196
|
},
|
|
138
197
|
{
|
|
@@ -140,7 +199,7 @@ var V = 1e3, H = class extends x.Service()("@voltro/plugin-governance/Governance
|
|
|
140
199
|
path: "/erasures",
|
|
141
200
|
scope: "process",
|
|
142
201
|
description: "GDPR erasure log (most recent first).",
|
|
143
|
-
handler: () => S.succeed(
|
|
202
|
+
handler: () => S.succeed(Z({ erasures: F.slice(0, 100) }))
|
|
144
203
|
},
|
|
145
204
|
{
|
|
146
205
|
method: "GET",
|
|
@@ -148,8 +207,8 @@ var V = 1e3, H = class extends x.Service()("@voltro/plugin-governance/Governance
|
|
|
148
207
|
scope: "declaration",
|
|
149
208
|
description: "The routes a DSAR export/erasure walks — AND what the derivation cannot see.",
|
|
150
209
|
handler: () => S.sync(() => {
|
|
151
|
-
let e =
|
|
152
|
-
return
|
|
210
|
+
let e = D();
|
|
211
|
+
return Z({
|
|
153
212
|
subjectTable: e.subjectTable,
|
|
154
213
|
derived: x !== void 0,
|
|
155
214
|
paths: e.paths.map((t) => ({
|
|
@@ -168,8 +227,8 @@ var V = 1e3, H = class extends x.Service()("@voltro/plugin-governance/Governance
|
|
|
168
227
|
scope: c ? "shared-store" : "process",
|
|
169
228
|
description: "Consent ledger for a subject (?subjectId=…).",
|
|
170
229
|
handler: (e) => {
|
|
171
|
-
let t =
|
|
172
|
-
return t ? S.promise(async () =>
|
|
230
|
+
let t = de(e.url, "subjectId");
|
|
231
|
+
return t ? S.promise(async () => Z({ records: await g.list(t) })) : S.succeed(Q(400, "subjectId query param required"));
|
|
173
232
|
}
|
|
174
233
|
},
|
|
175
234
|
{
|
|
@@ -181,13 +240,13 @@ var V = 1e3, H = class extends x.Service()("@voltro/plugin-governance/Governance
|
|
|
181
240
|
try {
|
|
182
241
|
t = JSON.parse(e.body || "{}");
|
|
183
242
|
} catch {
|
|
184
|
-
return
|
|
243
|
+
return Q(400, "invalid JSON body");
|
|
185
244
|
}
|
|
186
|
-
if (typeof t != "object" || !t || Array.isArray(t) || !("subjectId" in t) || typeof t.subjectId != "string" || !t.subjectId.trim()) return
|
|
245
|
+
if (typeof t != "object" || !t || Array.isArray(t) || !("subjectId" in t) || typeof t.subjectId != "string" || !t.subjectId.trim()) return Q(400, "subjectId must be a non-empty string");
|
|
187
246
|
try {
|
|
188
|
-
return
|
|
247
|
+
return Z({ bundle: await X.exportSubject(t.subjectId) });
|
|
189
248
|
} catch (e) {
|
|
190
|
-
return
|
|
249
|
+
return Q(500, e.message);
|
|
191
250
|
}
|
|
192
251
|
})
|
|
193
252
|
},
|
|
@@ -200,15 +259,15 @@ var V = 1e3, H = class extends x.Service()("@voltro/plugin-governance/Governance
|
|
|
200
259
|
try {
|
|
201
260
|
t = JSON.parse(e.body || "{}");
|
|
202
261
|
} catch {
|
|
203
|
-
return
|
|
262
|
+
return Q(400, "invalid JSON body");
|
|
204
263
|
}
|
|
205
|
-
if (typeof t != "object" || !t || Array.isArray(t) || !("subjectId" in t) || typeof t.subjectId != "string" || !t.subjectId.trim()) return
|
|
264
|
+
if (typeof t != "object" || !t || Array.isArray(t) || !("subjectId" in t) || typeof t.subjectId != "string" || !t.subjectId.trim()) return Q(400, "subjectId must be a non-empty string");
|
|
206
265
|
let n = "mode" in t ? t.mode : void 0;
|
|
207
|
-
if (n !== void 0 && n !== "delete" && n !== "anonymize") return
|
|
266
|
+
if (n !== void 0 && n !== "delete" && n !== "anonymize") return Q(400, "mode must be delete or anonymize");
|
|
208
267
|
try {
|
|
209
|
-
return
|
|
268
|
+
return Z({ entry: await X.eraseSubject(t.subjectId, n ? { mode: n } : void 0) });
|
|
210
269
|
} catch (e) {
|
|
211
|
-
return
|
|
270
|
+
return Q(500, e.message);
|
|
212
271
|
}
|
|
213
272
|
})
|
|
214
273
|
},
|
|
@@ -216,48 +275,64 @@ var V = 1e3, H = class extends x.Service()("@voltro/plugin-governance/Governance
|
|
|
216
275
|
method: "POST",
|
|
217
276
|
path: "/sweep",
|
|
218
277
|
description: "Run the retention sweep now.",
|
|
219
|
-
handler: () => S.promise(async () => (await
|
|
220
|
-
lastSweepAt:
|
|
221
|
-
lastSweep:
|
|
278
|
+
handler: () => S.promise(async () => (await X.runRetentionNow(), Z({
|
|
279
|
+
lastSweepAt: W,
|
|
280
|
+
lastSweep: U
|
|
222
281
|
})))
|
|
223
282
|
}
|
|
224
283
|
];
|
|
225
|
-
return
|
|
284
|
+
return te({
|
|
226
285
|
name: s,
|
|
227
|
-
baseName:
|
|
286
|
+
baseName: q,
|
|
228
287
|
description: "Data governance — retention TTL sweep, GDPR export/erasure, consent ledger.",
|
|
229
288
|
permissions: [
|
|
230
289
|
"store:write",
|
|
231
290
|
"inspect:read",
|
|
232
291
|
"inspect:write"
|
|
233
292
|
],
|
|
234
|
-
declaredEnv:
|
|
235
|
-
services:
|
|
236
|
-
routes:
|
|
293
|
+
declaredEnv: P,
|
|
294
|
+
services: ee.succeed(K, X),
|
|
295
|
+
routes: ue,
|
|
237
296
|
rpcClientDescriptors: r,
|
|
238
|
-
inspectEndpoints:
|
|
297
|
+
inspectEndpoints: pe,
|
|
239
298
|
...c ? { extendSchema: { tables: v } } : {},
|
|
240
299
|
bindDataStore: (e, t) => {
|
|
241
|
-
if (
|
|
300
|
+
if (R = a(e), B = e, c && (h = y(e)), o.retention && o.retention.length > 0) {
|
|
242
301
|
let e = o.sweepIntervalMs ?? 36e5;
|
|
243
|
-
|
|
302
|
+
V = t?.scheduleCoordinated("governance.retention.sweep", e, () => X.runRetentionNow());
|
|
244
303
|
}
|
|
245
304
|
},
|
|
246
305
|
onActivate: (e) => S.gen(function* () {
|
|
247
|
-
|
|
306
|
+
H = (t, n) => e.logger.warn(t, n);
|
|
248
307
|
let t = !1;
|
|
249
308
|
if (o.fieldEncryption) {
|
|
250
|
-
let e =
|
|
251
|
-
if (!n) return yield* S.die(/* @__PURE__ */ Error(`governance: fieldEncryption is enabled but secret "${e}" did not resolve — set it via the Secrets-Resolver / env`));
|
|
252
|
-
let r = [];
|
|
309
|
+
let e = M, n = yield* S.promise(() => L(e)), r = yield* S.promise(() => L(E)), i = [];
|
|
253
310
|
for (let e of N) {
|
|
254
|
-
let t = yield* S.promise(() =>
|
|
255
|
-
t !== void 0 && t.trim() !== "" &&
|
|
311
|
+
let t = yield* S.promise(() => L(e));
|
|
312
|
+
t !== void 0 && t.trim() !== "" && i.push(t);
|
|
313
|
+
}
|
|
314
|
+
if (r !== void 0 && r.trim() !== "") {
|
|
315
|
+
let e = yield* S.promise(() => L(T)), t = yield* S.try({
|
|
316
|
+
try: () => {
|
|
317
|
+
let t = I({
|
|
318
|
+
[E]: r,
|
|
319
|
+
[T]: e,
|
|
320
|
+
VOLTRO_FIELD_ENCRYPTION_KEY: n
|
|
321
|
+
});
|
|
322
|
+
for (let e of i) if (!t.keys.some((t) => O(t.secret).equals(O(e)))) throw Error(`governance: a key named in fieldEncryption.previousSecretKeys is not one of ${E}`);
|
|
323
|
+
return t;
|
|
324
|
+
},
|
|
325
|
+
catch: (e) => e instanceof Error ? e : Error(String(e))
|
|
326
|
+
}).pipe(S.orDie);
|
|
327
|
+
z(ae(t));
|
|
328
|
+
} else {
|
|
329
|
+
if (!n) return yield* S.die(/* @__PURE__ */ Error(`governance: fieldEncryption is enabled but secret "${e}" did not resolve — set it via the Secrets-Resolver / env, or configure ${E}`));
|
|
330
|
+
z(ie(n, i));
|
|
256
331
|
}
|
|
257
|
-
|
|
332
|
+
t = !0, J = !0;
|
|
258
333
|
}
|
|
259
334
|
!c && !o.consent && e.logger.warn("governance: consent ledger uses the in-memory store — per-process + restart-cleared. Under >1 replica a consent recorded on one replica is invisible to others. Pass consent: 'datastore' (durable, cross-instance) in production.");
|
|
260
|
-
let n =
|
|
335
|
+
let n = D();
|
|
261
336
|
if (x !== void 0) {
|
|
262
337
|
let t = n.limitations.filter((e) => e.kind === "unreachable").length;
|
|
263
338
|
n.paths.length <= 1 && e.logger.warn(`governance: the derived subject scope reached ${n.paths.length} table(s) from '${n.subjectTable}'. A DSAR would export almost nothing. Check that subjectTable names a real table and that the app declares relations() or reference() columns pointing at it.`), t > 0 && e.logger.info(`governance: ${t} registered table(s) are NOT linked to '${n.subjectTable}' and are outside every DSAR. GET /_voltro/inspect/plugins/governance/subject-graph lists them; add subjectScopes for any that hold subject data.`);
|
|
@@ -271,9 +346,8 @@ var V = 1e3, H = class extends x.Service()("@voltro/plugin-governance/Governance
|
|
|
271
346
|
});
|
|
272
347
|
}),
|
|
273
348
|
onDeactivate: () => S.sync(() => {
|
|
274
|
-
|
|
349
|
+
V?.stop(), V = void 0, o.fieldEncryption && z(void 0);
|
|
275
350
|
})
|
|
276
351
|
});
|
|
277
352
|
};
|
|
278
|
-
|
|
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 };
|
|
353
|
+
export { g as CONSENT_TABLE, B as DEFAULT_SUBJECT_GRAPH_DEPTH, V as ENCRYPTED_PREFIX, G as ERASURE_LOG_CAP, K as GovernanceService, s as SUBJECT_KEY_CHUNK, c as SUBJECT_ROW_CAP, u as collectSubjectRows, _ as consentTable, v as consentTables, y as dataStoreConsentStore, a as dataStoreGovStore, D as deriveKey, H as deriveSubjectGraph, d as eraseSubject, f as exportSubject, k as fieldKeyIdOf, U as formatSubjectGraph, J as governancePlugin, W as isEncrypted, M as makeAesCipher, N as makeFieldCipher, P as makeFieldKeyring, b as memoryConsentStore, o as memoryGovStore, F as resolveFieldKeyring, m as runRetention, p as scopeToPath, h as selectExpired, R as setFieldCipher };
|
package/dist/retention.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { writeGovernanceRows as e } from "./store.js";
|
|
2
2
|
import { assertAnonymizable as t } from "./gdpr.js";
|
|
3
|
-
|
|
3
|
+
// src/retention.ts
|
|
4
4
|
var n = (e, t, n) => {
|
|
5
5
|
let r = e[t], i = r instanceof Date ? r.getTime() : typeof r == "string" || typeof r == "number" ? new Date(r).getTime() : NaN;
|
|
6
6
|
return Number.isNaN(i) ? null : n - i;
|
|
@@ -48,5 +48,4 @@ var n = (e, t, n) => {
|
|
|
48
48
|
}
|
|
49
49
|
return c;
|
|
50
50
|
};
|
|
51
|
-
//#endregion
|
|
52
51
|
export { i as RETENTION_BATCH_SIZE, a as runRetention, r as selectExpired };
|
package/dist/rpc.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Schema as e } from "effect";
|
|
2
2
|
import { ADMIN_SCOPE as t, defineAction as n, defineMutation as r, defineQuery as i } from "@voltro/protocol";
|
|
3
|
-
|
|
3
|
+
// src/rpc.ts
|
|
4
4
|
var a = n({
|
|
5
5
|
name: "governance.export",
|
|
6
6
|
input: e.Struct({ subjectId: e.String }),
|
|
@@ -69,5 +69,4 @@ var a = n({
|
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
];
|
|
72
|
-
//#endregion
|
|
73
72
|
export { s as consentDescriptor, o as eraseDescriptor, a as exportDescriptor, l as governanceRpcClientImports, c as hasConsentDescriptor };
|
package/dist/store.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { and as e, getTable as t, gt as n, inSet as r, lt as i } from "@voltro/database";
|
|
2
|
-
|
|
2
|
+
// src/store.ts
|
|
3
3
|
var a = (a) => {
|
|
4
4
|
let o = a, s = (e, t) => ({
|
|
5
5
|
table: void 0,
|
|
@@ -116,5 +116,4 @@ var a = (a) => {
|
|
|
116
116
|
for (let e of s) if (e.status === "rejected") throw e.reason;
|
|
117
117
|
return i;
|
|
118
118
|
};
|
|
119
|
-
//#endregion
|
|
120
119
|
export { a as dataStoreGovStore, o as memoryGovStore, s as writeGovernanceRows };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/plugin-governance",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.88.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,12 +38,12 @@
|
|
|
38
38
|
"node": ">=24.0.0"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@voltro/database": "0.
|
|
42
|
-
"@voltro/protocol": "0.
|
|
43
|
-
"@voltro/runtime": "0.
|
|
41
|
+
"@voltro/database": "0.88.0",
|
|
42
|
+
"@voltro/protocol": "0.88.0",
|
|
43
|
+
"@voltro/runtime": "0.88.0"
|
|
44
44
|
},
|
|
45
45
|
"peerDependencies": {
|
|
46
|
-
"effect": "4.0
|
|
46
|
+
"effect": "4.1.0"
|
|
47
47
|
},
|
|
48
48
|
"publishConfig": {
|
|
49
49
|
"access": "public"
|