@nestarc/data-subject 0.1.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/LICENSE +21 -0
- package/README.md +351 -0
- package/dist/data-subject.module.d.ts +19 -0
- package/dist/data-subject.module.js +55 -0
- package/dist/data-subject.service.d.ts +35 -0
- package/dist/data-subject.service.js +162 -0
- package/dist/erase-runner.d.ts +10 -0
- package/dist/erase-runner.js +114 -0
- package/dist/errors.d.ts +16 -0
- package/dist/errors.js +34 -0
- package/dist/export-runner.d.ts +14 -0
- package/dist/export-runner.js +34 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +36 -0
- package/dist/legal-basis.d.ts +4 -0
- package/dist/legal-basis.js +22 -0
- package/dist/policy-compiler.d.ts +11 -0
- package/dist/policy-compiler.js +50 -0
- package/dist/prisma/from-prisma.d.ts +23 -0
- package/dist/prisma/from-prisma.js +42 -0
- package/dist/registry.d.ts +14 -0
- package/dist/registry.js +27 -0
- package/dist/storage/artifact-storage.interface.d.ts +7 -0
- package/dist/storage/artifact-storage.interface.js +2 -0
- package/dist/storage/in-memory-artifact-storage.d.ts +9 -0
- package/dist/storage/in-memory-artifact-storage.js +14 -0
- package/dist/storage/in-memory-request-storage.d.ts +12 -0
- package/dist/storage/in-memory-request-storage.js +38 -0
- package/dist/storage/request-storage.interface.d.ts +10 -0
- package/dist/storage/request-storage.interface.js +2 -0
- package/dist/types.d.ts +68 -0
- package/dist/types.js +2 -0
- package/package.json +59 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EraseRunner = void 0;
|
|
4
|
+
const errors_1 = require("./errors");
|
|
5
|
+
class EraseRunner {
|
|
6
|
+
registry;
|
|
7
|
+
constructor(registry) {
|
|
8
|
+
this.registry = registry;
|
|
9
|
+
}
|
|
10
|
+
async run(subjectId, tenantId) {
|
|
11
|
+
const entities = [];
|
|
12
|
+
const retained = [];
|
|
13
|
+
const verificationResidual = [];
|
|
14
|
+
for (const entry of this.registry.list()) {
|
|
15
|
+
const outcome = classify(entry.policy);
|
|
16
|
+
let affected = 0;
|
|
17
|
+
if (outcome.rowStrategy === 'delete') {
|
|
18
|
+
affected = await entry.executor.erase(subjectId, tenantId, {
|
|
19
|
+
rowLevel: entry.policy.rowLevel,
|
|
20
|
+
deleteFields: outcome.deleteFields,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
else if (outcome.rowStrategy === 'anonymize') {
|
|
24
|
+
affected = await entry.executor.anonymize(subjectId, tenantId, outcome.updateMap);
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
const rows = await entry.executor.select(subjectId, tenantId);
|
|
28
|
+
affected = rows.length;
|
|
29
|
+
}
|
|
30
|
+
const rowsAfter = await entry.executor.select(subjectId, tenantId);
|
|
31
|
+
for (const item of outcome.retained) {
|
|
32
|
+
retained.push({
|
|
33
|
+
entityName: entry.policy.entityName,
|
|
34
|
+
field: item.field,
|
|
35
|
+
legalBasis: item.legalBasis,
|
|
36
|
+
count: rowsAfter.length,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
if (outcome.rowStrategy === 'delete' &&
|
|
40
|
+
entry.policy.rowLevel === 'delete-row' &&
|
|
41
|
+
rowsAfter.length > 0) {
|
|
42
|
+
verificationResidual.push({
|
|
43
|
+
entityName: entry.policy.entityName,
|
|
44
|
+
count: rowsAfter.length,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
entities.push({
|
|
48
|
+
entityName: entry.policy.entityName,
|
|
49
|
+
affected,
|
|
50
|
+
strategy: outcome.summaryStrategy,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
if (verificationResidual.length > 0) {
|
|
54
|
+
throw new errors_1.DataSubjectError(errors_1.DataSubjectErrorCode.VerificationFailed, `residual rows: ${verificationResidual
|
|
55
|
+
.map((entry) => `${entry.entityName}(${entry.count})`)
|
|
56
|
+
.join(', ')}`);
|
|
57
|
+
}
|
|
58
|
+
return { stats: { entities, retained, verificationResidual } };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
exports.EraseRunner = EraseRunner;
|
|
62
|
+
function classify(policy) {
|
|
63
|
+
const updateMap = {};
|
|
64
|
+
const deleteFields = [];
|
|
65
|
+
const retained = [];
|
|
66
|
+
const strategies = new Set();
|
|
67
|
+
for (const [field, entry] of Object.entries(policy.fields)) {
|
|
68
|
+
const normalized = normalize(entry);
|
|
69
|
+
strategies.add(normalized.strategy);
|
|
70
|
+
if (normalized.strategy === 'anonymize') {
|
|
71
|
+
updateMap[field] = normalized.replacement;
|
|
72
|
+
}
|
|
73
|
+
if (normalized.strategy === 'delete') {
|
|
74
|
+
deleteFields.push(field);
|
|
75
|
+
updateMap[field] = null;
|
|
76
|
+
}
|
|
77
|
+
if (normalized.strategy === 'retain') {
|
|
78
|
+
retained.push({
|
|
79
|
+
field,
|
|
80
|
+
legalBasis: normalized.legalBasis,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const rowStrategy = chooseRowStrategy(strategies);
|
|
85
|
+
const summaryStrategy = strategies.size > 1 ? 'mixed' : rowStrategy;
|
|
86
|
+
return { rowStrategy, summaryStrategy, deleteFields, updateMap, retained };
|
|
87
|
+
}
|
|
88
|
+
function chooseRowStrategy(strategies) {
|
|
89
|
+
if (strategies.size === 1 && strategies.has('delete')) {
|
|
90
|
+
return 'delete';
|
|
91
|
+
}
|
|
92
|
+
if (strategies.size === 1 && strategies.has('retain')) {
|
|
93
|
+
return 'retain';
|
|
94
|
+
}
|
|
95
|
+
return 'anonymize';
|
|
96
|
+
}
|
|
97
|
+
function normalize(entry) {
|
|
98
|
+
if (entry === 'delete') {
|
|
99
|
+
return { strategy: 'delete' };
|
|
100
|
+
}
|
|
101
|
+
if (entry.strategy === 'anonymize') {
|
|
102
|
+
return {
|
|
103
|
+
strategy: 'anonymize',
|
|
104
|
+
replacement: entry.replacement,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
if (entry.strategy === 'retain') {
|
|
108
|
+
return {
|
|
109
|
+
strategy: 'retain',
|
|
110
|
+
legalBasis: entry.legalBasis,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
return { strategy: 'delete' };
|
|
114
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare const DataSubjectErrorCode: {
|
|
2
|
+
readonly SubjectNotFound: "dsr_subject_not_found";
|
|
3
|
+
readonly UnregisteredEntity: "dsr_unregistered_entity";
|
|
4
|
+
readonly InvalidPolicy: "dsr_invalid_policy";
|
|
5
|
+
readonly VerificationFailed: "dsr_verification_failed";
|
|
6
|
+
readonly AnonymizeDynamicReplacement: "dsr_anonymize_dynamic_replacement";
|
|
7
|
+
readonly EntityAlreadyRegistered: "dsr_entity_already_registered";
|
|
8
|
+
readonly RequestConflict: "dsr_request_conflict";
|
|
9
|
+
readonly RequestNotFound: "dsr_request_not_found";
|
|
10
|
+
};
|
|
11
|
+
export type DataSubjectErrorCode = (typeof DataSubjectErrorCode)[keyof typeof DataSubjectErrorCode];
|
|
12
|
+
export declare class DataSubjectError extends Error {
|
|
13
|
+
readonly code: DataSubjectErrorCode;
|
|
14
|
+
readonly httpStatus: number;
|
|
15
|
+
constructor(code: DataSubjectErrorCode, reason?: string);
|
|
16
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DataSubjectError = exports.DataSubjectErrorCode = void 0;
|
|
4
|
+
exports.DataSubjectErrorCode = {
|
|
5
|
+
SubjectNotFound: 'dsr_subject_not_found',
|
|
6
|
+
UnregisteredEntity: 'dsr_unregistered_entity',
|
|
7
|
+
InvalidPolicy: 'dsr_invalid_policy',
|
|
8
|
+
VerificationFailed: 'dsr_verification_failed',
|
|
9
|
+
AnonymizeDynamicReplacement: 'dsr_anonymize_dynamic_replacement',
|
|
10
|
+
EntityAlreadyRegistered: 'dsr_entity_already_registered',
|
|
11
|
+
RequestConflict: 'dsr_request_conflict',
|
|
12
|
+
RequestNotFound: 'dsr_request_not_found',
|
|
13
|
+
};
|
|
14
|
+
const HTTP_STATUS = {
|
|
15
|
+
dsr_subject_not_found: 404,
|
|
16
|
+
dsr_unregistered_entity: 500,
|
|
17
|
+
dsr_invalid_policy: 500,
|
|
18
|
+
dsr_verification_failed: 500,
|
|
19
|
+
dsr_anonymize_dynamic_replacement: 500,
|
|
20
|
+
dsr_entity_already_registered: 500,
|
|
21
|
+
dsr_request_conflict: 409,
|
|
22
|
+
dsr_request_not_found: 404,
|
|
23
|
+
};
|
|
24
|
+
class DataSubjectError extends Error {
|
|
25
|
+
code;
|
|
26
|
+
httpStatus;
|
|
27
|
+
constructor(code, reason) {
|
|
28
|
+
super(reason ? `${code}: ${reason}` : code);
|
|
29
|
+
this.name = 'DataSubjectError';
|
|
30
|
+
this.code = code;
|
|
31
|
+
this.httpStatus = HTTP_STATUS[code];
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
exports.DataSubjectError = DataSubjectError;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Registry } from './registry';
|
|
2
|
+
import type { ArtifactStorage } from './storage/artifact-storage.interface';
|
|
3
|
+
import type { RequestStats } from './types';
|
|
4
|
+
export interface ExportResult {
|
|
5
|
+
artifactHash: string;
|
|
6
|
+
artifactUrl: string;
|
|
7
|
+
stats: RequestStats;
|
|
8
|
+
}
|
|
9
|
+
export declare class ExportRunner {
|
|
10
|
+
private readonly registry;
|
|
11
|
+
private readonly artifacts;
|
|
12
|
+
constructor(registry: Registry, artifacts: ArtifactStorage);
|
|
13
|
+
run(requestId: string, subjectId: string, tenantId: string): Promise<ExportResult>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.ExportRunner = void 0;
|
|
7
|
+
const node_crypto_1 = require("node:crypto");
|
|
8
|
+
const jszip_1 = __importDefault(require("jszip"));
|
|
9
|
+
class ExportRunner {
|
|
10
|
+
registry;
|
|
11
|
+
artifacts;
|
|
12
|
+
constructor(registry, artifacts) {
|
|
13
|
+
this.registry = registry;
|
|
14
|
+
this.artifacts = artifacts;
|
|
15
|
+
}
|
|
16
|
+
async run(requestId, subjectId, tenantId) {
|
|
17
|
+
const zip = new jszip_1.default();
|
|
18
|
+
const entities = [];
|
|
19
|
+
for (const entry of this.registry.list()) {
|
|
20
|
+
const rows = await entry.executor.select(subjectId, tenantId);
|
|
21
|
+
zip.file(`${entry.policy.entityName}.json`, JSON.stringify(rows, null, 2));
|
|
22
|
+
entities.push({
|
|
23
|
+
entityName: entry.policy.entityName,
|
|
24
|
+
affected: rows.length,
|
|
25
|
+
strategy: 'export',
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
const body = await zip.generateAsync({ type: 'nodebuffer' });
|
|
29
|
+
const artifactHash = (0, node_crypto_1.createHash)('sha256').update(body).digest('hex');
|
|
30
|
+
const artifactUrl = await this.artifacts.put(`${requestId}.zip`, body, 'application/zip');
|
|
31
|
+
return { artifactHash, artifactUrl, stats: { entities } };
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
exports.ExportRunner = ExportRunner;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export * from './types';
|
|
2
|
+
export * from './errors';
|
|
3
|
+
export { validateLegalBasis } from './legal-basis';
|
|
4
|
+
export type { LegalBasisOptions } from './legal-basis';
|
|
5
|
+
export { compilePolicy } from './policy-compiler';
|
|
6
|
+
export type { CompileOptions, PolicySpec } from './policy-compiler';
|
|
7
|
+
export { Registry } from './registry';
|
|
8
|
+
export type { RegisterInput } from './registry';
|
|
9
|
+
export { DataSubjectService } from './data-subject.service';
|
|
10
|
+
export type { DataSubjectServiceDeps } from './data-subject.service';
|
|
11
|
+
export { DATA_SUBJECT_REGISTRY, DataSubjectModule, } from './data-subject.module';
|
|
12
|
+
export type { DataSubjectModuleOptions } from './data-subject.module';
|
|
13
|
+
export type { RequestStorage } from './storage/request-storage.interface';
|
|
14
|
+
export { InMemoryRequestStorage } from './storage/in-memory-request-storage';
|
|
15
|
+
export type { ArtifactStorage } from './storage/artifact-storage.interface';
|
|
16
|
+
export { InMemoryArtifactStorage } from './storage/in-memory-artifact-storage';
|
|
17
|
+
export { fromPrisma } from './prisma/from-prisma';
|
|
18
|
+
export type { FromPrismaOptions, PrismaDelegate } from './prisma/from-prisma';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.fromPrisma = exports.InMemoryArtifactStorage = exports.InMemoryRequestStorage = exports.DataSubjectModule = exports.DATA_SUBJECT_REGISTRY = exports.DataSubjectService = exports.Registry = exports.compilePolicy = exports.validateLegalBasis = void 0;
|
|
18
|
+
__exportStar(require("./types"), exports);
|
|
19
|
+
__exportStar(require("./errors"), exports);
|
|
20
|
+
var legal_basis_1 = require("./legal-basis");
|
|
21
|
+
Object.defineProperty(exports, "validateLegalBasis", { enumerable: true, get: function () { return legal_basis_1.validateLegalBasis; } });
|
|
22
|
+
var policy_compiler_1 = require("./policy-compiler");
|
|
23
|
+
Object.defineProperty(exports, "compilePolicy", { enumerable: true, get: function () { return policy_compiler_1.compilePolicy; } });
|
|
24
|
+
var registry_1 = require("./registry");
|
|
25
|
+
Object.defineProperty(exports, "Registry", { enumerable: true, get: function () { return registry_1.Registry; } });
|
|
26
|
+
var data_subject_service_1 = require("./data-subject.service");
|
|
27
|
+
Object.defineProperty(exports, "DataSubjectService", { enumerable: true, get: function () { return data_subject_service_1.DataSubjectService; } });
|
|
28
|
+
var data_subject_module_1 = require("./data-subject.module");
|
|
29
|
+
Object.defineProperty(exports, "DATA_SUBJECT_REGISTRY", { enumerable: true, get: function () { return data_subject_module_1.DATA_SUBJECT_REGISTRY; } });
|
|
30
|
+
Object.defineProperty(exports, "DataSubjectModule", { enumerable: true, get: function () { return data_subject_module_1.DataSubjectModule; } });
|
|
31
|
+
var in_memory_request_storage_1 = require("./storage/in-memory-request-storage");
|
|
32
|
+
Object.defineProperty(exports, "InMemoryRequestStorage", { enumerable: true, get: function () { return in_memory_request_storage_1.InMemoryRequestStorage; } });
|
|
33
|
+
var in_memory_artifact_storage_1 = require("./storage/in-memory-artifact-storage");
|
|
34
|
+
Object.defineProperty(exports, "InMemoryArtifactStorage", { enumerable: true, get: function () { return in_memory_artifact_storage_1.InMemoryArtifactStorage; } });
|
|
35
|
+
var from_prisma_1 = require("./prisma/from-prisma");
|
|
36
|
+
Object.defineProperty(exports, "fromPrisma", { enumerable: true, get: function () { return from_prisma_1.fromPrisma; } });
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateLegalBasis = validateLegalBasis;
|
|
4
|
+
const STRICT_FORMAT = /^[a-z][a-z0-9-]*:[^\s].*$/i;
|
|
5
|
+
function validateLegalBasis(value, opts = {}) {
|
|
6
|
+
if (!value || value.trim().length === 0) {
|
|
7
|
+
return 'legalBasis is empty';
|
|
8
|
+
}
|
|
9
|
+
if (opts.strict) {
|
|
10
|
+
if (!value.includes(':')) {
|
|
11
|
+
return 'legalBasis missing scheme (expected "scheme:reference")';
|
|
12
|
+
}
|
|
13
|
+
const [, ref] = value.split(':', 2);
|
|
14
|
+
if (!ref || ref.trim().length === 0) {
|
|
15
|
+
return 'legalBasis missing reference after scheme';
|
|
16
|
+
}
|
|
17
|
+
if (!STRICT_FORMAT.test(value)) {
|
|
18
|
+
return 'legalBasis does not match "scheme:reference" format';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { EntityPolicy, PolicyEntry } from './types';
|
|
2
|
+
export interface CompileOptions {
|
|
3
|
+
strictLegalBasis?: boolean;
|
|
4
|
+
}
|
|
5
|
+
export interface PolicySpec {
|
|
6
|
+
entityName: string;
|
|
7
|
+
subjectField: string;
|
|
8
|
+
rowLevel?: 'delete-row' | 'delete-fields';
|
|
9
|
+
fields: Record<string, PolicyEntry>;
|
|
10
|
+
}
|
|
11
|
+
export declare function compilePolicy(spec: PolicySpec, opts?: CompileOptions): EntityPolicy;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.compilePolicy = compilePolicy;
|
|
4
|
+
const errors_1 = require("./errors");
|
|
5
|
+
const legal_basis_1 = require("./legal-basis");
|
|
6
|
+
function compilePolicy(spec, opts = {}) {
|
|
7
|
+
const fields = {};
|
|
8
|
+
for (const [name, raw] of Object.entries(spec.fields)) {
|
|
9
|
+
fields[name] = normalizeEntry(spec.entityName, name, raw, opts);
|
|
10
|
+
}
|
|
11
|
+
return {
|
|
12
|
+
entityName: spec.entityName,
|
|
13
|
+
subjectField: spec.subjectField,
|
|
14
|
+
rowLevel: spec.rowLevel ?? 'delete-fields',
|
|
15
|
+
fields,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function normalizeEntry(entityName, fieldName, entry, opts) {
|
|
19
|
+
if (entry === 'delete') {
|
|
20
|
+
return { strategy: 'delete' };
|
|
21
|
+
}
|
|
22
|
+
if (entry.strategy === 'delete') {
|
|
23
|
+
return { strategy: 'delete' };
|
|
24
|
+
}
|
|
25
|
+
if (entry.strategy === 'anonymize') {
|
|
26
|
+
if (typeof entry.replacement === 'function') {
|
|
27
|
+
throw new errors_1.DataSubjectError(errors_1.DataSubjectErrorCode.AnonymizeDynamicReplacement, `${entityName}.${fieldName}: replacement must be static`);
|
|
28
|
+
}
|
|
29
|
+
return { strategy: 'anonymize', replacement: entry.replacement };
|
|
30
|
+
}
|
|
31
|
+
if (entry.strategy === 'retain') {
|
|
32
|
+
const basis = entry.legalBasis;
|
|
33
|
+
if (!basis) {
|
|
34
|
+
throw new errors_1.DataSubjectError(errors_1.DataSubjectErrorCode.InvalidPolicy, `${entityName}.${fieldName}: retain requires legalBasis`);
|
|
35
|
+
}
|
|
36
|
+
const validationError = (0, legal_basis_1.validateLegalBasis)(basis, {
|
|
37
|
+
strict: opts.strictLegalBasis,
|
|
38
|
+
});
|
|
39
|
+
if (validationError) {
|
|
40
|
+
throw new errors_1.DataSubjectError(errors_1.DataSubjectErrorCode.InvalidPolicy, `${entityName}.${fieldName}: ${validationError}`);
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
strategy: 'retain',
|
|
44
|
+
legalBasis: basis,
|
|
45
|
+
until: entry.until,
|
|
46
|
+
pseudonymize: entry.pseudonymize ?? 'none',
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
throw new errors_1.DataSubjectError(errors_1.DataSubjectErrorCode.InvalidPolicy, `${entityName}.${fieldName}: unknown strategy`);
|
|
50
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { EntityExecutor } from '../types';
|
|
2
|
+
export interface PrismaDelegate {
|
|
3
|
+
findMany(args: {
|
|
4
|
+
where: Record<string, unknown>;
|
|
5
|
+
}): Promise<Record<string, unknown>[]>;
|
|
6
|
+
deleteMany(args: {
|
|
7
|
+
where: Record<string, unknown>;
|
|
8
|
+
}): Promise<{
|
|
9
|
+
count: number;
|
|
10
|
+
}>;
|
|
11
|
+
updateMany(args: {
|
|
12
|
+
where: Record<string, unknown>;
|
|
13
|
+
data: Record<string, unknown>;
|
|
14
|
+
}): Promise<{
|
|
15
|
+
count: number;
|
|
16
|
+
}>;
|
|
17
|
+
}
|
|
18
|
+
export interface FromPrismaOptions {
|
|
19
|
+
delegate: PrismaDelegate;
|
|
20
|
+
subjectField: string;
|
|
21
|
+
tenantField?: string;
|
|
22
|
+
}
|
|
23
|
+
export declare function fromPrisma(opts: FromPrismaOptions): EntityExecutor;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.fromPrisma = fromPrisma;
|
|
4
|
+
function fromPrisma(opts) {
|
|
5
|
+
const { delegate, subjectField, tenantField } = opts;
|
|
6
|
+
const whereFor = (subjectId, tenantId) => {
|
|
7
|
+
const where = { [subjectField]: subjectId };
|
|
8
|
+
if (tenantField) {
|
|
9
|
+
where[tenantField] = tenantId;
|
|
10
|
+
}
|
|
11
|
+
return where;
|
|
12
|
+
};
|
|
13
|
+
return {
|
|
14
|
+
async select(subjectId, tenantId) {
|
|
15
|
+
return delegate.findMany({ where: whereFor(subjectId, tenantId) });
|
|
16
|
+
},
|
|
17
|
+
async erase(subjectId, tenantId, plan) {
|
|
18
|
+
if (plan.rowLevel === 'delete-row') {
|
|
19
|
+
const result = await delegate.deleteMany({
|
|
20
|
+
where: whereFor(subjectId, tenantId),
|
|
21
|
+
});
|
|
22
|
+
return result.count;
|
|
23
|
+
}
|
|
24
|
+
const data = Object.fromEntries(plan.deleteFields.map((field) => [field, null]));
|
|
25
|
+
if (Object.keys(data).length === 0) {
|
|
26
|
+
return 0;
|
|
27
|
+
}
|
|
28
|
+
const result = await delegate.updateMany({
|
|
29
|
+
where: whereFor(subjectId, tenantId),
|
|
30
|
+
data,
|
|
31
|
+
});
|
|
32
|
+
return result.count;
|
|
33
|
+
},
|
|
34
|
+
async anonymize(subjectId, tenantId, replacements) {
|
|
35
|
+
const result = await delegate.updateMany({
|
|
36
|
+
where: whereFor(subjectId, tenantId),
|
|
37
|
+
data: replacements,
|
|
38
|
+
});
|
|
39
|
+
return result.count;
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type CompileOptions, type PolicySpec } from './policy-compiler';
|
|
2
|
+
import type { EntityExecutor, RegisteredEntity } from './types';
|
|
3
|
+
export interface RegisterInput {
|
|
4
|
+
policy: PolicySpec;
|
|
5
|
+
executor: EntityExecutor;
|
|
6
|
+
}
|
|
7
|
+
export declare class Registry {
|
|
8
|
+
private readonly opts;
|
|
9
|
+
private readonly entries;
|
|
10
|
+
constructor(opts?: CompileOptions);
|
|
11
|
+
register(input: RegisterInput): void;
|
|
12
|
+
get(name: string): RegisteredEntity | undefined;
|
|
13
|
+
list(): RegisteredEntity[];
|
|
14
|
+
}
|
package/dist/registry.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Registry = void 0;
|
|
4
|
+
const errors_1 = require("./errors");
|
|
5
|
+
const policy_compiler_1 = require("./policy-compiler");
|
|
6
|
+
class Registry {
|
|
7
|
+
opts;
|
|
8
|
+
entries = new Map();
|
|
9
|
+
constructor(opts = {}) {
|
|
10
|
+
this.opts = opts;
|
|
11
|
+
}
|
|
12
|
+
register(input) {
|
|
13
|
+
const name = input.policy.entityName;
|
|
14
|
+
if (this.entries.has(name)) {
|
|
15
|
+
throw new errors_1.DataSubjectError(errors_1.DataSubjectErrorCode.EntityAlreadyRegistered, `entity ${name} already registered`);
|
|
16
|
+
}
|
|
17
|
+
const compiled = (0, policy_compiler_1.compilePolicy)(input.policy, this.opts);
|
|
18
|
+
this.entries.set(name, { policy: compiled, executor: input.executor });
|
|
19
|
+
}
|
|
20
|
+
get(name) {
|
|
21
|
+
return this.entries.get(name);
|
|
22
|
+
}
|
|
23
|
+
list() {
|
|
24
|
+
return [...this.entries.values()];
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
exports.Registry = Registry;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ArtifactStorage } from './artifact-storage.interface';
|
|
2
|
+
export declare class InMemoryArtifactStorage implements ArtifactStorage {
|
|
3
|
+
private readonly store;
|
|
4
|
+
put(key: string, body: Buffer, contentType: string): Promise<string>;
|
|
5
|
+
get(key: string): Promise<{
|
|
6
|
+
body: Buffer;
|
|
7
|
+
contentType: string;
|
|
8
|
+
} | null>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.InMemoryArtifactStorage = void 0;
|
|
4
|
+
class InMemoryArtifactStorage {
|
|
5
|
+
store = new Map();
|
|
6
|
+
async put(key, body, contentType) {
|
|
7
|
+
this.store.set(key, { body, contentType });
|
|
8
|
+
return `memory://${key}`;
|
|
9
|
+
}
|
|
10
|
+
async get(key) {
|
|
11
|
+
return this.store.get(key) ?? null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
exports.InMemoryArtifactStorage = InMemoryArtifactStorage;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { DataSubjectRequest, RequestState } from '../types';
|
|
2
|
+
import type { RequestStorage } from './request-storage.interface';
|
|
3
|
+
export declare class InMemoryRequestStorage implements RequestStorage {
|
|
4
|
+
private readonly store;
|
|
5
|
+
insert(req: DataSubjectRequest): Promise<void>;
|
|
6
|
+
update(id: string, patch: Partial<DataSubjectRequest>): Promise<void>;
|
|
7
|
+
findById(id: string): Promise<DataSubjectRequest | null>;
|
|
8
|
+
listByTenant(tenantId: string, opts?: {
|
|
9
|
+
state?: RequestState;
|
|
10
|
+
}): Promise<DataSubjectRequest[]>;
|
|
11
|
+
listOverdue(now: Date): Promise<DataSubjectRequest[]>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.InMemoryRequestStorage = void 0;
|
|
4
|
+
const errors_1 = require("../errors");
|
|
5
|
+
const PENDING = ['created', 'validating', 'processing'];
|
|
6
|
+
class InMemoryRequestStorage {
|
|
7
|
+
store = new Map();
|
|
8
|
+
async insert(req) {
|
|
9
|
+
if (this.store.has(req.id)) {
|
|
10
|
+
throw new errors_1.DataSubjectError(errors_1.DataSubjectErrorCode.RequestConflict, `duplicate id: ${req.id}`);
|
|
11
|
+
}
|
|
12
|
+
this.store.set(req.id, { ...req });
|
|
13
|
+
}
|
|
14
|
+
async update(id, patch) {
|
|
15
|
+
const request = this.store.get(id);
|
|
16
|
+
if (!request) {
|
|
17
|
+
throw new errors_1.DataSubjectError(errors_1.DataSubjectErrorCode.RequestNotFound, `request ${id} not found`);
|
|
18
|
+
}
|
|
19
|
+
Object.assign(request, patch);
|
|
20
|
+
}
|
|
21
|
+
async findById(id) {
|
|
22
|
+
const request = this.store.get(id);
|
|
23
|
+
return request ? { ...request } : null;
|
|
24
|
+
}
|
|
25
|
+
async listByTenant(tenantId, opts = {}) {
|
|
26
|
+
return [...this.store.values()]
|
|
27
|
+
.filter((request) => request.tenantId === tenantId)
|
|
28
|
+
.filter((request) => (opts.state ? request.state === opts.state : true))
|
|
29
|
+
.map((request) => ({ ...request }));
|
|
30
|
+
}
|
|
31
|
+
async listOverdue(now) {
|
|
32
|
+
return [...this.store.values()]
|
|
33
|
+
.filter((request) => PENDING.includes(request.state) &&
|
|
34
|
+
request.dueAt.getTime() < now.getTime())
|
|
35
|
+
.map((request) => ({ ...request }));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
exports.InMemoryRequestStorage = InMemoryRequestStorage;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { DataSubjectRequest, RequestState } from '../types';
|
|
2
|
+
export interface RequestStorage {
|
|
3
|
+
insert(req: DataSubjectRequest): Promise<void>;
|
|
4
|
+
update(id: string, patch: Partial<DataSubjectRequest>): Promise<void>;
|
|
5
|
+
findById(id: string): Promise<DataSubjectRequest | null>;
|
|
6
|
+
listByTenant(tenantId: string, opts?: {
|
|
7
|
+
state?: RequestState;
|
|
8
|
+
}): Promise<DataSubjectRequest[]>;
|
|
9
|
+
listOverdue(now: Date): Promise<DataSubjectRequest[]>;
|
|
10
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export type Strategy = 'delete' | 'anonymize' | 'retain';
|
|
2
|
+
export type RequestEntityStrategy = Strategy | 'mixed' | 'export';
|
|
3
|
+
export type RowLevel = 'delete-row' | 'delete-fields';
|
|
4
|
+
export type PolicyEntry = 'delete' | {
|
|
5
|
+
strategy: 'delete';
|
|
6
|
+
} | {
|
|
7
|
+
strategy: 'anonymize';
|
|
8
|
+
replacement: string | number | null;
|
|
9
|
+
} | {
|
|
10
|
+
strategy: 'retain';
|
|
11
|
+
legalBasis: string;
|
|
12
|
+
until?: string;
|
|
13
|
+
pseudonymize?: 'hmac' | 'none';
|
|
14
|
+
};
|
|
15
|
+
export interface EntityPolicy {
|
|
16
|
+
entityName: string;
|
|
17
|
+
subjectField: string;
|
|
18
|
+
rowLevel: RowLevel;
|
|
19
|
+
fields: Record<string, PolicyEntry>;
|
|
20
|
+
}
|
|
21
|
+
export interface ErasePlan {
|
|
22
|
+
rowLevel: RowLevel;
|
|
23
|
+
deleteFields: string[];
|
|
24
|
+
}
|
|
25
|
+
export interface EntityExecutor {
|
|
26
|
+
select(subjectId: string, tenantId: string): Promise<Record<string, unknown>[]>;
|
|
27
|
+
erase(subjectId: string, tenantId: string, plan: ErasePlan): Promise<number>;
|
|
28
|
+
anonymize(subjectId: string, tenantId: string, replacements: Record<string, unknown>): Promise<number>;
|
|
29
|
+
}
|
|
30
|
+
export interface RegisteredEntity {
|
|
31
|
+
policy: EntityPolicy;
|
|
32
|
+
executor: EntityExecutor;
|
|
33
|
+
}
|
|
34
|
+
export type RequestType = 'export' | 'erase';
|
|
35
|
+
export type RequestState = 'created' | 'validating' | 'processing' | 'completed' | 'failed';
|
|
36
|
+
export interface DataSubjectRequest {
|
|
37
|
+
id: string;
|
|
38
|
+
tenantId: string;
|
|
39
|
+
subjectId: string;
|
|
40
|
+
type: RequestType;
|
|
41
|
+
state: RequestState;
|
|
42
|
+
createdAt: Date;
|
|
43
|
+
dueAt: Date;
|
|
44
|
+
completedAt: Date | null;
|
|
45
|
+
failedAt: Date | null;
|
|
46
|
+
failureReason: string | null;
|
|
47
|
+
artifactHash: string | null;
|
|
48
|
+
artifactUrl: string | null;
|
|
49
|
+
stats: RequestStats | null;
|
|
50
|
+
requestedBy: string | null;
|
|
51
|
+
}
|
|
52
|
+
export interface RequestStats {
|
|
53
|
+
entities: Array<{
|
|
54
|
+
entityName: string;
|
|
55
|
+
affected: number;
|
|
56
|
+
strategy: RequestEntityStrategy;
|
|
57
|
+
}>;
|
|
58
|
+
retained?: Array<{
|
|
59
|
+
entityName: string;
|
|
60
|
+
field: string;
|
|
61
|
+
legalBasis: string;
|
|
62
|
+
count: number;
|
|
63
|
+
}>;
|
|
64
|
+
verificationResidual?: Array<{
|
|
65
|
+
entityName: string;
|
|
66
|
+
count: number;
|
|
67
|
+
}>;
|
|
68
|
+
}
|
package/dist/types.js
ADDED