@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 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.1.0`
19
+ Package version: `0.2.0`
17
20
 
18
- This repository currently focuses on the execution core. It does **not** currently ship:
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
- InMemoryRequestStorage,
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 InMemoryRequestStorage(),
60
- artifactStorage: new InMemoryArtifactStorage(),
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: `<requestId>.zip`
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. publishes `data_subject.erasure_requested`
210
- 3. executes each registered entity according to its compiled policy
211
- 4. records:
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
- - `artifactHash` as a SHA-256 digest of the erase report JSON
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 uses `ArtifactStorage` for exports, but **not** for erase reports
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
- No additional audit lifecycle events are emitted by the current implementation.
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
- - only in-memory request and artifact adapters are included in this repository
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 repository secret `NPM_TOKEN`
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.1.0` publish with npm dist-tag `latest`
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,3 @@
1
+ export declare function sha256Hex(body: Buffer | string): string;
2
+ export declare function exportArtifactKey(tenantId: string, requestId: string): string;
3
+ export declare function erasureEvidenceArtifactKey(tenantId: string, requestId: string): string;
@@ -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
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
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
- failureReason: messageFromError(error),
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.publishOutbox('data_subject.erasure_requested', {
79
+ await this.setState(request.id, 'processing');
80
+ await this.publishAudit('data_subject.request_processing', {
56
81
  requestId: request.id,
57
- subjectId,
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
- const report = JSON.stringify({ requestId: request.id, stats: result.stats });
65
- const artifactHash = (0, node_crypto_1.createHash)('sha256').update(report).digest('hex');
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: result.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
- failureReason: messageFromError(error),
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: messageFromError(error),
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;
@@ -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
  }