@nestarc/data-subject 0.1.0 → 0.2.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/README.md +58 -18
- package/dist/artifacts.d.ts +3 -0
- package/dist/artifacts.js +21 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +155 -0
- package/dist/data-subject.service.js +84 -13
- package/dist/erase-runner.d.ts +6 -2
- package/dist/erase-runner.js +66 -5
- package/dist/erasure-evidence.d.ts +9 -0
- package/dist/erasure-evidence.js +18 -0
- package/dist/errors.d.ts +3 -0
- package/dist/errors.js +6 -0
- package/dist/export-runner.js +3 -3
- package/dist/index.d.ts +8 -0
- package/dist/index.js +16 -1
- package/dist/lint/index.d.ts +3 -0
- package/dist/lint/index.js +24 -0
- package/dist/lint/lint.d.ts +4 -0
- package/dist/lint/lint.js +160 -0
- package/dist/lint/prisma-schema.d.ts +11 -0
- package/dist/lint/prisma-schema.js +43 -0
- package/dist/lint/types.d.ts +32 -0
- package/dist/lint/types.js +2 -0
- package/dist/registry-validation.d.ts +9 -0
- package/dist/registry-validation.js +37 -0
- package/dist/stable-json.d.ts +1 -0
- package/dist/stable-json.js +30 -0
- package/dist/storage/prisma-request-storage.d.ts +33 -0
- package/dist/storage/prisma-request-storage.js +156 -0
- package/dist/types.d.ts +84 -0
- package/docs/code-review-src.md +293 -0
- package/docs/compliance.md +178 -0
- package/docs/data-subject-0.2.0-feature-proposal.md +565 -0
- package/docs/data-subject-0.2.0-spec.md +679 -0
- package/docs/prd.md +164 -0
- package/docs/spec.md +282 -0
- package/package.json +24 -1
- package/prisma/schema.example.prisma +31 -0
package/README.md
CHANGED
|
@@ -8,19 +8,22 @@ Today the package ships:
|
|
|
8
8
|
- a `DataSubjectService` for `export`, `erase`, and request lookup
|
|
9
9
|
- a `DataSubjectModule.forRoot(...)` integration for NestJS
|
|
10
10
|
- a lightweight Prisma adapter built on `findMany`, `deleteMany`, and `updateMany`
|
|
11
|
+
- a `PrismaRequestStorage` adapter for persistent request records
|
|
11
12
|
- in-memory request and artifact stores for tests and local development
|
|
13
|
+
- erase evidence artifacts with pre/post scan stats and SHA-256 hashes
|
|
14
|
+
- a `data-subject lint` CLI for Prisma schema/policy checks
|
|
12
15
|
- typed policy validation and typed runtime errors
|
|
13
16
|
|
|
14
17
|
## Current Scope
|
|
15
18
|
|
|
16
|
-
Package version: `0.
|
|
19
|
+
Package version: `0.2.0`
|
|
17
20
|
|
|
18
|
-
This repository
|
|
21
|
+
This repository focuses on the execution core plus the minimum production trust layer. It does **not** currently ship:
|
|
19
22
|
|
|
20
23
|
- decorators or automatic entity discovery
|
|
21
|
-
- a CLI or schema linter
|
|
22
|
-
- persistent request storage adapters
|
|
23
24
|
- persistent artifact storage adapters beyond the in-memory implementation
|
|
25
|
+
- direct Stripe, Intercom, analytics, or support-tool connectors
|
|
26
|
+
- an admin or end-user portal
|
|
24
27
|
- schema-aware Prisma field deletion beyond `null` assignment
|
|
25
28
|
|
|
26
29
|
If you need database-specific behavior, you can plug in your own `EntityExecutor`, `RequestStorage`, or `ArtifactStorage`.
|
|
@@ -47,17 +50,20 @@ import { PrismaClient } from '@prisma/client';
|
|
|
47
50
|
import {
|
|
48
51
|
DataSubjectModule,
|
|
49
52
|
InMemoryArtifactStorage,
|
|
50
|
-
|
|
53
|
+
PrismaRequestStorage,
|
|
51
54
|
fromPrisma,
|
|
52
55
|
} from '@nestarc/data-subject';
|
|
53
56
|
|
|
54
57
|
const prisma = new PrismaClient();
|
|
58
|
+
const artifactStorage = new InMemoryArtifactStorage(); // local/dev only; use private durable storage in production
|
|
55
59
|
|
|
56
60
|
@Module({
|
|
57
61
|
imports: [
|
|
58
62
|
DataSubjectModule.forRoot({
|
|
59
|
-
requestStorage: new
|
|
60
|
-
|
|
63
|
+
requestStorage: new PrismaRequestStorage({
|
|
64
|
+
delegate: prisma.dataSubjectRequest,
|
|
65
|
+
}),
|
|
66
|
+
artifactStorage,
|
|
61
67
|
slaDays: 30,
|
|
62
68
|
strictLegalBasis: true,
|
|
63
69
|
entities: [
|
|
@@ -116,6 +122,8 @@ const prisma = new PrismaClient();
|
|
|
116
122
|
export class AppModule {}
|
|
117
123
|
```
|
|
118
124
|
|
|
125
|
+
For tests and local development, `InMemoryRequestStorage` and `InMemoryArtifactStorage` are still available. Do not use in-memory storage for production request history or evidence retention.
|
|
126
|
+
|
|
119
127
|
Usage:
|
|
120
128
|
|
|
121
129
|
```ts
|
|
@@ -198,7 +206,7 @@ This prevents `retain` fields from being dropped just because some other fields
|
|
|
198
206
|
|
|
199
207
|
Current export artifact shape:
|
|
200
208
|
|
|
201
|
-
- key:
|
|
209
|
+
- key: `data-subject/{tenantId}/{requestId}/export.zip`
|
|
202
210
|
- contents: `<EntityName>.json` files
|
|
203
211
|
|
|
204
212
|
## Erase Behavior
|
|
@@ -206,19 +214,27 @@ Current export artifact shape:
|
|
|
206
214
|
`DataSubjectService.erase(subjectId, tenantId)` does the following:
|
|
207
215
|
|
|
208
216
|
1. creates a request record
|
|
209
|
-
2.
|
|
210
|
-
3.
|
|
211
|
-
4.
|
|
217
|
+
2. transitions the request to `processing`
|
|
218
|
+
3. performs a pre-scan for registered entities
|
|
219
|
+
4. publishes `data_subject.erasure_requested`
|
|
220
|
+
5. executes each registered entity according to its compiled policy
|
|
221
|
+
6. performs a post-scan and residual verification
|
|
222
|
+
7. stores an erase evidence JSON artifact through `ArtifactStorage.put(...)`
|
|
223
|
+
8. records:
|
|
212
224
|
- `stats.entities[]`
|
|
213
225
|
- `stats.retained[]`
|
|
214
226
|
- `stats.verificationResidual[]`
|
|
215
|
-
- `
|
|
227
|
+
- `stats.preScan[]`
|
|
228
|
+
- `stats.postScan[]`
|
|
229
|
+
- `artifactHash` as a SHA-256 digest of the erase evidence artifact
|
|
230
|
+
- `artifactUrl` returned by the artifact storage
|
|
216
231
|
|
|
217
232
|
Important details:
|
|
218
233
|
|
|
219
|
-
- erase
|
|
234
|
+
- erase evidence artifacts intentionally exclude raw rows and field values
|
|
220
235
|
- erase verification currently only fails on residual rows after `delete-row`
|
|
221
236
|
- field-level delete and anonymize operations keep rows in place by design
|
|
237
|
+
- `subjectId` is not written into the default erase evidence artifact; it remains connected through the request record
|
|
222
238
|
|
|
223
239
|
## NestJS Integration
|
|
224
240
|
|
|
@@ -250,9 +266,22 @@ The package currently exports:
|
|
|
250
266
|
- `fromPrisma`
|
|
251
267
|
- `InMemoryRequestStorage`
|
|
252
268
|
- `InMemoryArtifactStorage`
|
|
269
|
+
- `PrismaRequestStorage`
|
|
270
|
+
- `lintPrismaSchema`
|
|
271
|
+
- `validateRegistry`
|
|
253
272
|
- all public types from `src/types.ts`
|
|
254
273
|
- typed errors from `src/errors.ts`
|
|
255
274
|
|
|
275
|
+
## Schema Lint
|
|
276
|
+
|
|
277
|
+
The package includes a small Prisma schema linter:
|
|
278
|
+
|
|
279
|
+
```bash
|
|
280
|
+
npx @nestarc/data-subject lint --schema prisma/schema.prisma --config data-subject.config.json
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
The linter checks for PII-like field names, missing policy fields, missing subject fields, missing tenant metadata, invalid policies, and suppressions without reasons. It is a safety net, not a full data discovery tool.
|
|
284
|
+
|
|
256
285
|
## Events and Hooks
|
|
257
286
|
|
|
258
287
|
### Outbox Hook
|
|
@@ -260,6 +289,7 @@ The package currently exports:
|
|
|
260
289
|
If `publishOutbox` is provided, the built-in service emits:
|
|
261
290
|
|
|
262
291
|
- `data_subject.request_created`
|
|
292
|
+
- `data_subject.request_processing` through the audit hook
|
|
263
293
|
- `data_subject.erasure_requested`
|
|
264
294
|
- `data_subject.request_completed`
|
|
265
295
|
- `data_subject.request_failed`
|
|
@@ -271,8 +301,9 @@ If `publishOutbox` is provided, the built-in service emits:
|
|
|
271
301
|
If `publishAudit` is provided, the built-in service currently emits:
|
|
272
302
|
|
|
273
303
|
- `data_subject.request_created`
|
|
274
|
-
|
|
275
|
-
|
|
304
|
+
- `data_subject.request_processing`
|
|
305
|
+
- `data_subject.request_completed`
|
|
306
|
+
- `data_subject.request_failed`
|
|
276
307
|
|
|
277
308
|
## Typed Errors
|
|
278
309
|
|
|
@@ -286,6 +317,9 @@ Currently used codes include:
|
|
|
286
317
|
- `dsr_entity_already_registered`
|
|
287
318
|
- `dsr_request_conflict`
|
|
288
319
|
- `dsr_request_not_found`
|
|
320
|
+
- `dsr_artifact_write_failed`
|
|
321
|
+
- `dsr_invalid_state_transition`
|
|
322
|
+
- `dsr_evidence_report_invalid`
|
|
289
323
|
|
|
290
324
|
Some additional codes exist in the public enum for future or adapter-specific use.
|
|
291
325
|
|
|
@@ -316,7 +350,7 @@ The current implementation is intentionally small. A few things are important to
|
|
|
316
350
|
- default Prisma field deletion writes `null`; it does not inspect schema nullability
|
|
317
351
|
- request states include `validating`, but the built-in service currently transitions through `created -> processing -> completed|failed`
|
|
318
352
|
- there is no built-in subject existence check before export or erase
|
|
319
|
-
-
|
|
353
|
+
- no production cloud artifact adapter is bundled; use a private `ArtifactStorage` implementation for S3, R2, GCS, or equivalent storage
|
|
320
354
|
|
|
321
355
|
## Development
|
|
322
356
|
|
|
@@ -334,15 +368,21 @@ GitHub Actions is configured with two workflows:
|
|
|
334
368
|
|
|
335
369
|
Release expectations:
|
|
336
370
|
|
|
337
|
-
- configure
|
|
371
|
+
- configure npm Trusted Publisher for GitHub Actions:
|
|
372
|
+
- organization/user: `nestarc`
|
|
373
|
+
- repository: `data-subject`
|
|
374
|
+
- workflow filename: `release.yml`
|
|
375
|
+
- environment: `npm`
|
|
338
376
|
- publish a GitHub Release from a tag that matches `v<package.json version>`
|
|
339
377
|
- prerelease versions publish with npm dist-tag `next`
|
|
340
|
-
- stable versions such as `0.
|
|
378
|
+
- stable versions such as `0.2.0` publish with npm dist-tag `latest`
|
|
341
379
|
|
|
342
380
|
## Related Docs
|
|
343
381
|
|
|
344
382
|
- [docs/prd.md](docs/prd.md)
|
|
345
383
|
- [docs/spec.md](docs/spec.md)
|
|
384
|
+
- [docs/data-subject-0.2.0-feature-proposal.md](docs/data-subject-0.2.0-feature-proposal.md)
|
|
385
|
+
- [docs/data-subject-0.2.0-spec.md](docs/data-subject-0.2.0-spec.md)
|
|
346
386
|
- [docs/compliance.md](docs/compliance.md)
|
|
347
387
|
- [CHANGELOG.md](CHANGELOG.md)
|
|
348
388
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sha256Hex = sha256Hex;
|
|
4
|
+
exports.exportArtifactKey = exportArtifactKey;
|
|
5
|
+
exports.erasureEvidenceArtifactKey = erasureEvidenceArtifactKey;
|
|
6
|
+
const node_crypto_1 = require("node:crypto");
|
|
7
|
+
function sha256Hex(body) {
|
|
8
|
+
return (0, node_crypto_1.createHash)('sha256').update(body).digest('hex');
|
|
9
|
+
}
|
|
10
|
+
function exportArtifactKey(tenantId, requestId) {
|
|
11
|
+
return dataSubjectArtifactKey(tenantId, requestId, 'export.zip');
|
|
12
|
+
}
|
|
13
|
+
function erasureEvidenceArtifactKey(tenantId, requestId) {
|
|
14
|
+
return dataSubjectArtifactKey(tenantId, requestId, 'erase-evidence.json');
|
|
15
|
+
}
|
|
16
|
+
function dataSubjectArtifactKey(tenantId, requestId, fileName) {
|
|
17
|
+
return `data-subject/${keySegment(tenantId)}/${keySegment(requestId)}/${fileName}`;
|
|
18
|
+
}
|
|
19
|
+
function keySegment(value) {
|
|
20
|
+
return encodeURIComponent(value);
|
|
21
|
+
}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
const node_fs_1 = require("node:fs");
|
|
38
|
+
const node_module_1 = require("node:module");
|
|
39
|
+
const node_path_1 = require("node:path");
|
|
40
|
+
const node_url_1 = require("node:url");
|
|
41
|
+
const lint_1 = require("./lint");
|
|
42
|
+
const nodeRequire = (0, node_module_1.createRequire)(__filename);
|
|
43
|
+
void main(process.argv.slice(2)).catch((error) => {
|
|
44
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
45
|
+
process.exitCode = 1;
|
|
46
|
+
});
|
|
47
|
+
async function main(argv) {
|
|
48
|
+
const opts = parseArgs(argv);
|
|
49
|
+
if (opts.command !== 'lint') {
|
|
50
|
+
printHelp();
|
|
51
|
+
process.exitCode = opts.command ? 1 : 0;
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (!opts.schema) {
|
|
55
|
+
throw new Error('data-subject lint requires --schema');
|
|
56
|
+
}
|
|
57
|
+
const schemaSource = (0, node_fs_1.readFileSync)((0, node_path_1.resolve)(opts.schema), 'utf8');
|
|
58
|
+
const config = await loadConfig(opts.config);
|
|
59
|
+
const report = (0, lint_1.lintPrismaSchema)(schemaSource, config);
|
|
60
|
+
if (opts.format === 'json') {
|
|
61
|
+
console.log(JSON.stringify(report, null, 2));
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
console.log((0, lint_1.formatLintReport)(report));
|
|
65
|
+
}
|
|
66
|
+
if ((0, lint_1.shouldFailLint)(report, opts.failOn)) {
|
|
67
|
+
process.exitCode = 1;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function parseArgs(argv) {
|
|
71
|
+
const opts = {
|
|
72
|
+
command: argv[0],
|
|
73
|
+
format: 'text',
|
|
74
|
+
failOn: 'error',
|
|
75
|
+
};
|
|
76
|
+
for (let index = 1; index < argv.length; index += 1) {
|
|
77
|
+
const arg = argv[index];
|
|
78
|
+
const next = argv[index + 1];
|
|
79
|
+
if (arg === '--schema') {
|
|
80
|
+
opts.schema = requireValue(arg, next);
|
|
81
|
+
index += 1;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (arg === '--config') {
|
|
85
|
+
opts.config = requireValue(arg, next);
|
|
86
|
+
index += 1;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (arg === '--format') {
|
|
90
|
+
const value = requireValue(arg, next);
|
|
91
|
+
if (value !== 'text' && value !== 'json') {
|
|
92
|
+
throw new Error('--format must be text or json');
|
|
93
|
+
}
|
|
94
|
+
opts.format = value;
|
|
95
|
+
index += 1;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (arg === '--fail-on') {
|
|
99
|
+
const value = requireValue(arg, next);
|
|
100
|
+
if (value !== 'warning' && value !== 'error') {
|
|
101
|
+
throw new Error('--fail-on must be warning or error');
|
|
102
|
+
}
|
|
103
|
+
opts.failOn = value;
|
|
104
|
+
index += 1;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (arg === '--help' || arg === '-h') {
|
|
108
|
+
opts.command = undefined;
|
|
109
|
+
return opts;
|
|
110
|
+
}
|
|
111
|
+
throw new Error(`unknown option: ${arg}`);
|
|
112
|
+
}
|
|
113
|
+
return opts;
|
|
114
|
+
}
|
|
115
|
+
function requireValue(flag, value) {
|
|
116
|
+
if (!value || value.startsWith('--')) {
|
|
117
|
+
throw new Error(`${flag} requires a value`);
|
|
118
|
+
}
|
|
119
|
+
return value;
|
|
120
|
+
}
|
|
121
|
+
async function loadConfig(configPath) {
|
|
122
|
+
if (!configPath) {
|
|
123
|
+
return {};
|
|
124
|
+
}
|
|
125
|
+
const fullPath = (0, node_path_1.resolve)(configPath);
|
|
126
|
+
const ext = (0, node_path_1.extname)(fullPath);
|
|
127
|
+
if (ext === '.json') {
|
|
128
|
+
return JSON.parse((0, node_fs_1.readFileSync)(fullPath, 'utf8'));
|
|
129
|
+
}
|
|
130
|
+
if (ext === '.mjs') {
|
|
131
|
+
const mod = (await Promise.resolve(`${(0, node_url_1.pathToFileURL)(fullPath).href}`).then(s => __importStar(require(s))));
|
|
132
|
+
return mod.default ?? {};
|
|
133
|
+
}
|
|
134
|
+
if (ext === '.js' || ext === '.cjs') {
|
|
135
|
+
const loaded = nodeRequire(fullPath);
|
|
136
|
+
if (isConfigModule(loaded)) {
|
|
137
|
+
return loaded.default ?? {};
|
|
138
|
+
}
|
|
139
|
+
return loaded;
|
|
140
|
+
}
|
|
141
|
+
throw new Error('data-subject lint supports .json, .js, .cjs, and .mjs config files');
|
|
142
|
+
}
|
|
143
|
+
function isConfigModule(value) {
|
|
144
|
+
return typeof value === 'object' && value !== null && 'default' in value;
|
|
145
|
+
}
|
|
146
|
+
function printHelp() {
|
|
147
|
+
console.log(`Usage:
|
|
148
|
+
data-subject lint --schema prisma/schema.prisma [--config data-subject.config.json]
|
|
149
|
+
|
|
150
|
+
Options:
|
|
151
|
+
--schema <path> Prisma schema path
|
|
152
|
+
--config <path> Optional lint config (.json, .js, .cjs, .mjs)
|
|
153
|
+
--format <text|json> Output format, defaults to text
|
|
154
|
+
--fail-on <level> warning or error, defaults to error`);
|
|
155
|
+
}
|
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.DataSubjectService = void 0;
|
|
4
4
|
const node_crypto_1 = require("node:crypto");
|
|
5
|
+
const artifacts_1 = require("./artifacts");
|
|
5
6
|
const erase_runner_1 = require("./erase-runner");
|
|
7
|
+
const erasure_evidence_1 = require("./erasure-evidence");
|
|
6
8
|
const errors_1 = require("./errors");
|
|
7
9
|
const export_runner_1 = require("./export-runner");
|
|
10
|
+
const stable_json_1 = require("./stable-json");
|
|
8
11
|
class DataSubjectService {
|
|
9
12
|
deps;
|
|
10
13
|
idFactory;
|
|
@@ -24,6 +27,11 @@ class DataSubjectService {
|
|
|
24
27
|
const request = await this.createRequest('export', subjectId, tenantId);
|
|
25
28
|
try {
|
|
26
29
|
await this.setState(request.id, 'processing');
|
|
30
|
+
await this.publishAudit('data_subject.request_processing', {
|
|
31
|
+
requestId: request.id,
|
|
32
|
+
type: request.type,
|
|
33
|
+
tenantId: request.tenantId,
|
|
34
|
+
});
|
|
27
35
|
const runner = new export_runner_1.ExportRunner(this.deps.registry, this.deps.artifactStorage);
|
|
28
36
|
const result = await runner.run(request.id, subjectId, tenantId);
|
|
29
37
|
await this.deps.requestStorage.update(request.id, {
|
|
@@ -35,15 +43,31 @@ class DataSubjectService {
|
|
|
35
43
|
});
|
|
36
44
|
await this.publishOutbox('data_subject.request_completed', {
|
|
37
45
|
requestId: request.id,
|
|
46
|
+
requestType: request.type,
|
|
47
|
+
tenantId: request.tenantId,
|
|
38
48
|
state: 'completed',
|
|
39
49
|
artifactHash: result.artifactHash,
|
|
40
50
|
});
|
|
51
|
+
await this.publishAudit('data_subject.request_completed', {
|
|
52
|
+
requestId: request.id,
|
|
53
|
+
type: request.type,
|
|
54
|
+
tenantId: request.tenantId,
|
|
55
|
+
artifactHash: result.artifactHash,
|
|
56
|
+
});
|
|
41
57
|
}
|
|
42
58
|
catch (error) {
|
|
43
59
|
await this.markFailed(request.id, error);
|
|
44
60
|
await this.publishOutbox('data_subject.request_failed', {
|
|
45
61
|
requestId: request.id,
|
|
46
|
-
|
|
62
|
+
requestType: request.type,
|
|
63
|
+
tenantId: request.tenantId,
|
|
64
|
+
failureReason: sanitizeFailureReason(error),
|
|
65
|
+
});
|
|
66
|
+
await this.publishAudit('data_subject.request_failed', {
|
|
67
|
+
requestId: request.id,
|
|
68
|
+
type: request.type,
|
|
69
|
+
tenantId: request.tenantId,
|
|
70
|
+
failureReason: sanitizeFailureReason(error),
|
|
47
71
|
});
|
|
48
72
|
}
|
|
49
73
|
return this.mustLoad(request.id);
|
|
@@ -52,35 +76,76 @@ class DataSubjectService {
|
|
|
52
76
|
const request = await this.createRequest('erase', subjectId, tenantId);
|
|
53
77
|
try {
|
|
54
78
|
await this.runInTransaction(async () => {
|
|
55
|
-
await this.
|
|
79
|
+
await this.setState(request.id, 'processing');
|
|
80
|
+
await this.publishAudit('data_subject.request_processing', {
|
|
56
81
|
requestId: request.id,
|
|
57
|
-
|
|
58
|
-
tenantId,
|
|
59
|
-
requestedAt: this.clock().toISOString(),
|
|
82
|
+
type: request.type,
|
|
83
|
+
tenantId: request.tenantId,
|
|
60
84
|
});
|
|
61
|
-
await this.setState(request.id, 'processing');
|
|
62
85
|
const runner = new erase_runner_1.EraseRunner(this.deps.registry);
|
|
63
|
-
const result = await runner.run(subjectId, tenantId
|
|
64
|
-
|
|
65
|
-
|
|
86
|
+
const result = await runner.run(subjectId, tenantId, {
|
|
87
|
+
afterPreScan: async () => {
|
|
88
|
+
await this.publishOutbox('data_subject.erasure_requested', {
|
|
89
|
+
requestId: request.id,
|
|
90
|
+
subjectId,
|
|
91
|
+
tenantId,
|
|
92
|
+
requestedAt: this.clock().toISOString(),
|
|
93
|
+
});
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
const artifact = (0, erasure_evidence_1.buildErasureEvidenceArtifact)({
|
|
97
|
+
requestId: request.id,
|
|
98
|
+
tenantId,
|
|
99
|
+
generatedAt: this.clock(),
|
|
100
|
+
stats: result.stats,
|
|
101
|
+
actions: result.actions,
|
|
102
|
+
});
|
|
103
|
+
const body = Buffer.from((0, stable_json_1.stableStringify)(artifact), 'utf8');
|
|
104
|
+
const artifactHash = (0, artifacts_1.sha256Hex)(body);
|
|
105
|
+
const artifactUrl = await this.deps.artifactStorage.put((0, artifacts_1.erasureEvidenceArtifactKey)(tenantId, request.id), body, 'application/json');
|
|
106
|
+
const stats = {
|
|
107
|
+
...result.stats,
|
|
108
|
+
evidence: {
|
|
109
|
+
schemaVersion: 'data-subject.evidence.v1',
|
|
110
|
+
artifactHash,
|
|
111
|
+
artifactUrl,
|
|
112
|
+
},
|
|
113
|
+
};
|
|
66
114
|
await this.deps.requestStorage.update(request.id, {
|
|
67
115
|
state: 'completed',
|
|
68
116
|
completedAt: this.clock(),
|
|
69
|
-
stats
|
|
117
|
+
stats,
|
|
70
118
|
artifactHash,
|
|
119
|
+
artifactUrl,
|
|
71
120
|
});
|
|
72
121
|
await this.publishOutbox('data_subject.request_completed', {
|
|
73
122
|
requestId: request.id,
|
|
123
|
+
requestType: request.type,
|
|
124
|
+
tenantId: request.tenantId,
|
|
74
125
|
state: 'completed',
|
|
75
126
|
artifactHash,
|
|
76
127
|
});
|
|
128
|
+
await this.publishAudit('data_subject.request_completed', {
|
|
129
|
+
requestId: request.id,
|
|
130
|
+
type: request.type,
|
|
131
|
+
tenantId: request.tenantId,
|
|
132
|
+
artifactHash,
|
|
133
|
+
});
|
|
77
134
|
});
|
|
78
135
|
}
|
|
79
136
|
catch (error) {
|
|
80
137
|
await this.markFailed(request.id, error);
|
|
81
138
|
await this.publishOutbox('data_subject.request_failed', {
|
|
82
139
|
requestId: request.id,
|
|
83
|
-
|
|
140
|
+
requestType: request.type,
|
|
141
|
+
tenantId: request.tenantId,
|
|
142
|
+
failureReason: sanitizeFailureReason(error),
|
|
143
|
+
});
|
|
144
|
+
await this.publishAudit('data_subject.request_failed', {
|
|
145
|
+
requestId: request.id,
|
|
146
|
+
type: request.type,
|
|
147
|
+
tenantId: request.tenantId,
|
|
148
|
+
failureReason: sanitizeFailureReason(error),
|
|
84
149
|
});
|
|
85
150
|
}
|
|
86
151
|
return this.mustLoad(request.id);
|
|
@@ -116,7 +181,7 @@ class DataSubjectService {
|
|
|
116
181
|
await this.deps.requestStorage.insert(request);
|
|
117
182
|
await this.publishOutbox('data_subject.request_created', {
|
|
118
183
|
requestId: request.id,
|
|
119
|
-
type,
|
|
184
|
+
requestType: type,
|
|
120
185
|
subjectId,
|
|
121
186
|
tenantId,
|
|
122
187
|
});
|
|
@@ -134,7 +199,7 @@ class DataSubjectService {
|
|
|
134
199
|
await this.deps.requestStorage.update(id, {
|
|
135
200
|
state: 'failed',
|
|
136
201
|
failedAt: this.clock(),
|
|
137
|
-
failureReason:
|
|
202
|
+
failureReason: sanitizeFailureReason(error),
|
|
138
203
|
});
|
|
139
204
|
}
|
|
140
205
|
async mustLoad(id) {
|
|
@@ -146,6 +211,12 @@ class DataSubjectService {
|
|
|
146
211
|
}
|
|
147
212
|
}
|
|
148
213
|
exports.DataSubjectService = DataSubjectService;
|
|
214
|
+
function sanitizeFailureReason(error) {
|
|
215
|
+
const message = messageFromError(error)
|
|
216
|
+
.replace(/\s+/g, ' ')
|
|
217
|
+
.trim();
|
|
218
|
+
return message.length > 500 ? `${message.slice(0, 497)}...` : message;
|
|
219
|
+
}
|
|
149
220
|
function messageFromError(error) {
|
|
150
221
|
if (error instanceof Error && error.message) {
|
|
151
222
|
return error.message;
|
package/dist/erase-runner.d.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import type { Registry } from './registry';
|
|
2
|
-
import type { RequestStats } from './types';
|
|
2
|
+
import type { ErasureEvidenceAction, RequestStats } from './types';
|
|
3
3
|
export interface EraseResult {
|
|
4
4
|
stats: RequestStats;
|
|
5
|
+
actions: ErasureEvidenceAction[];
|
|
6
|
+
}
|
|
7
|
+
export interface EraseRunOptions {
|
|
8
|
+
afterPreScan?: (preScan: NonNullable<RequestStats['preScan']>) => Promise<void>;
|
|
5
9
|
}
|
|
6
10
|
export declare class EraseRunner {
|
|
7
11
|
private readonly registry;
|
|
8
12
|
constructor(registry: Registry);
|
|
9
|
-
run(subjectId: string, tenantId: string): Promise<EraseResult>;
|
|
13
|
+
run(subjectId: string, tenantId: string, opts?: EraseRunOptions): Promise<EraseResult>;
|
|
10
14
|
}
|