@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.
@@ -7,11 +7,23 @@ class EraseRunner {
7
7
  constructor(registry) {
8
8
  this.registry = registry;
9
9
  }
10
- async run(subjectId, tenantId) {
10
+ async run(subjectId, tenantId, opts = {}) {
11
+ const entries = this.registry.list();
12
+ const preScan = [];
11
13
  const entities = [];
12
14
  const retained = [];
13
15
  const verificationResidual = [];
14
- for (const entry of this.registry.list()) {
16
+ const postScan = [];
17
+ const actions = [];
18
+ for (const entry of entries) {
19
+ const rowsBefore = await entry.executor.select(subjectId, tenantId);
20
+ preScan.push({
21
+ entityName: entry.policy.entityName,
22
+ count: rowsBefore.length,
23
+ });
24
+ }
25
+ await opts.afterPreScan?.(preScan);
26
+ for (const entry of entries) {
15
27
  const outcome = classify(entry.policy);
16
28
  let affected = 0;
17
29
  if (outcome.rowStrategy === 'delete') {
@@ -28,11 +40,24 @@ class EraseRunner {
28
40
  affected = rows.length;
29
41
  }
30
42
  const rowsAfter = await entry.executor.select(subjectId, tenantId);
43
+ postScan.push({
44
+ entityName: entry.policy.entityName,
45
+ count: rowsAfter.length,
46
+ });
47
+ const retainedFields = [];
31
48
  for (const item of outcome.retained) {
32
- retained.push({
49
+ const retainedItem = {
33
50
  entityName: entry.policy.entityName,
34
51
  field: item.field,
35
52
  legalBasis: item.legalBasis,
53
+ ...(item.until ? { until: item.until } : {}),
54
+ count: rowsAfter.length,
55
+ };
56
+ retained.push(retainedItem);
57
+ retainedFields.push({
58
+ field: item.field,
59
+ legalBasis: item.legalBasis,
60
+ ...(item.until ? { until: item.until } : {}),
36
61
  count: rowsAfter.length,
37
62
  });
38
63
  }
@@ -44,6 +69,22 @@ class EraseRunner {
44
69
  count: rowsAfter.length,
45
70
  });
46
71
  }
72
+ const action = {
73
+ entityName: entry.policy.entityName,
74
+ affected,
75
+ strategy: outcome.summaryStrategy,
76
+ rowLevel: entry.policy.rowLevel,
77
+ };
78
+ if (outcome.deleteFields.length > 0) {
79
+ action.deleteFields = [...outcome.deleteFields];
80
+ }
81
+ if (outcome.anonymizedFields.length > 0) {
82
+ action.anonymizedFields = [...outcome.anonymizedFields];
83
+ }
84
+ if (retainedFields.length > 0) {
85
+ action.retainedFields = retainedFields;
86
+ }
87
+ actions.push(action);
47
88
  entities.push({
48
89
  entityName: entry.policy.entityName,
49
90
  affected,
@@ -55,13 +96,23 @@ class EraseRunner {
55
96
  .map((entry) => `${entry.entityName}(${entry.count})`)
56
97
  .join(', ')}`);
57
98
  }
58
- return { stats: { entities, retained, verificationResidual } };
99
+ return {
100
+ stats: {
101
+ entities,
102
+ retained,
103
+ verificationResidual,
104
+ preScan,
105
+ postScan,
106
+ },
107
+ actions,
108
+ };
59
109
  }
60
110
  }
61
111
  exports.EraseRunner = EraseRunner;
62
112
  function classify(policy) {
63
113
  const updateMap = {};
64
114
  const deleteFields = [];
115
+ const anonymizedFields = [];
65
116
  const retained = [];
66
117
  const strategies = new Set();
67
118
  for (const [field, entry] of Object.entries(policy.fields)) {
@@ -69,6 +120,7 @@ function classify(policy) {
69
120
  strategies.add(normalized.strategy);
70
121
  if (normalized.strategy === 'anonymize') {
71
122
  updateMap[field] = normalized.replacement;
123
+ anonymizedFields.push(field);
72
124
  }
73
125
  if (normalized.strategy === 'delete') {
74
126
  deleteFields.push(field);
@@ -78,12 +130,20 @@ function classify(policy) {
78
130
  retained.push({
79
131
  field,
80
132
  legalBasis: normalized.legalBasis,
133
+ ...(normalized.until ? { until: normalized.until } : {}),
81
134
  });
82
135
  }
83
136
  }
84
137
  const rowStrategy = chooseRowStrategy(strategies);
85
138
  const summaryStrategy = strategies.size > 1 ? 'mixed' : rowStrategy;
86
- return { rowStrategy, summaryStrategy, deleteFields, updateMap, retained };
139
+ return {
140
+ rowStrategy,
141
+ summaryStrategy,
142
+ deleteFields,
143
+ updateMap,
144
+ anonymizedFields,
145
+ retained,
146
+ };
87
147
  }
88
148
  function chooseRowStrategy(strategies) {
89
149
  if (strategies.size === 1 && strategies.has('delete')) {
@@ -108,6 +168,7 @@ function normalize(entry) {
108
168
  return {
109
169
  strategy: 'retain',
110
170
  legalBasis: entry.legalBasis,
171
+ ...(entry.until ? { until: entry.until } : {}),
111
172
  };
112
173
  }
113
174
  return { strategy: 'delete' };
@@ -0,0 +1,9 @@
1
+ import type { ErasureEvidenceAction, ErasureEvidenceArtifact, RequestStats } from './types';
2
+ export interface BuildErasureEvidenceInput {
3
+ requestId: string;
4
+ tenantId: string;
5
+ generatedAt: Date;
6
+ stats: RequestStats;
7
+ actions: ErasureEvidenceAction[];
8
+ }
9
+ export declare function buildErasureEvidenceArtifact(input: BuildErasureEvidenceInput): ErasureEvidenceArtifact;
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildErasureEvidenceArtifact = buildErasureEvidenceArtifact;
4
+ function buildErasureEvidenceArtifact(input) {
5
+ return {
6
+ schemaVersion: 'data-subject.erasure-evidence.v1',
7
+ requestId: input.requestId,
8
+ tenantId: input.tenantId,
9
+ requestType: 'erase',
10
+ generatedAt: input.generatedAt.toISOString(),
11
+ state: 'completed',
12
+ preScan: input.stats.preScan ?? [],
13
+ actions: input.actions,
14
+ postScan: input.stats.postScan ?? [],
15
+ verificationResidual: input.stats.verificationResidual ?? [],
16
+ artifactHashAlgorithm: 'sha256',
17
+ };
18
+ }
package/dist/errors.d.ts CHANGED
@@ -7,6 +7,9 @@ export declare const DataSubjectErrorCode: {
7
7
  readonly EntityAlreadyRegistered: "dsr_entity_already_registered";
8
8
  readonly RequestConflict: "dsr_request_conflict";
9
9
  readonly RequestNotFound: "dsr_request_not_found";
10
+ readonly ArtifactWriteFailed: "dsr_artifact_write_failed";
11
+ readonly InvalidStateTransition: "dsr_invalid_state_transition";
12
+ readonly EvidenceReportInvalid: "dsr_evidence_report_invalid";
10
13
  };
11
14
  export type DataSubjectErrorCode = (typeof DataSubjectErrorCode)[keyof typeof DataSubjectErrorCode];
12
15
  export declare class DataSubjectError extends Error {
package/dist/errors.js CHANGED
@@ -10,6 +10,9 @@ exports.DataSubjectErrorCode = {
10
10
  EntityAlreadyRegistered: 'dsr_entity_already_registered',
11
11
  RequestConflict: 'dsr_request_conflict',
12
12
  RequestNotFound: 'dsr_request_not_found',
13
+ ArtifactWriteFailed: 'dsr_artifact_write_failed',
14
+ InvalidStateTransition: 'dsr_invalid_state_transition',
15
+ EvidenceReportInvalid: 'dsr_evidence_report_invalid',
13
16
  };
14
17
  const HTTP_STATUS = {
15
18
  dsr_subject_not_found: 404,
@@ -20,6 +23,9 @@ const HTTP_STATUS = {
20
23
  dsr_entity_already_registered: 500,
21
24
  dsr_request_conflict: 409,
22
25
  dsr_request_not_found: 404,
26
+ dsr_artifact_write_failed: 500,
27
+ dsr_invalid_state_transition: 500,
28
+ dsr_evidence_report_invalid: 500,
23
29
  };
24
30
  class DataSubjectError extends Error {
25
31
  code;
@@ -4,8 +4,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.ExportRunner = void 0;
7
- const node_crypto_1 = require("node:crypto");
8
7
  const jszip_1 = __importDefault(require("jszip"));
8
+ const artifacts_1 = require("./artifacts");
9
9
  class ExportRunner {
10
10
  registry;
11
11
  artifacts;
@@ -26,8 +26,8 @@ class ExportRunner {
26
26
  });
27
27
  }
28
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');
29
+ const artifactHash = (0, artifacts_1.sha256Hex)(body);
30
+ const artifactUrl = await this.artifacts.put((0, artifacts_1.exportArtifactKey)(tenantId, requestId), body, 'application/zip');
31
31
  return { artifactHash, artifactUrl, stats: { entities } };
32
32
  }
33
33
  }
package/dist/index.d.ts CHANGED
@@ -12,7 +12,15 @@ export { DATA_SUBJECT_REGISTRY, DataSubjectModule, } from './data-subject.module
12
12
  export type { DataSubjectModuleOptions } from './data-subject.module';
13
13
  export type { RequestStorage } from './storage/request-storage.interface';
14
14
  export { InMemoryRequestStorage } from './storage/in-memory-request-storage';
15
+ export { PrismaRequestStorage } from './storage/prisma-request-storage';
16
+ export type { PrismaDataSubjectRequestDelegate, PrismaRequestStorageOptions, } from './storage/prisma-request-storage';
15
17
  export type { ArtifactStorage } from './storage/artifact-storage.interface';
16
18
  export { InMemoryArtifactStorage } from './storage/in-memory-artifact-storage';
17
19
  export { fromPrisma } from './prisma/from-prisma';
18
20
  export type { FromPrismaOptions, PrismaDelegate } from './prisma/from-prisma';
21
+ export { exportArtifactKey, erasureEvidenceArtifactKey, sha256Hex, } from './artifacts';
22
+ export { buildErasureEvidenceArtifact } from './erasure-evidence';
23
+ export { validateRegistry } from './registry-validation';
24
+ export type { RegistryValidationReport } from './registry-validation';
25
+ export { formatLintReport, lintPrismaSchema, parsePrismaSchema, shouldFailLint, } from './lint';
26
+ export type { DataSubjectLintCode, DataSubjectLintConfig, DataSubjectLintFinding, DataSubjectLintRegistryEntry, DataSubjectLintReport, DataSubjectLintSeverity, DataSubjectLintSuppression, } from './lint';
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
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;
17
+ exports.shouldFailLint = exports.parsePrismaSchema = exports.lintPrismaSchema = exports.formatLintReport = exports.validateRegistry = exports.buildErasureEvidenceArtifact = exports.sha256Hex = exports.erasureEvidenceArtifactKey = exports.exportArtifactKey = exports.fromPrisma = exports.InMemoryArtifactStorage = exports.PrismaRequestStorage = exports.InMemoryRequestStorage = exports.DataSubjectModule = exports.DATA_SUBJECT_REGISTRY = exports.DataSubjectService = exports.Registry = exports.compilePolicy = exports.validateLegalBasis = void 0;
18
18
  __exportStar(require("./types"), exports);
19
19
  __exportStar(require("./errors"), exports);
20
20
  var legal_basis_1 = require("./legal-basis");
@@ -30,7 +30,22 @@ Object.defineProperty(exports, "DATA_SUBJECT_REGISTRY", { enumerable: true, get:
30
30
  Object.defineProperty(exports, "DataSubjectModule", { enumerable: true, get: function () { return data_subject_module_1.DataSubjectModule; } });
31
31
  var in_memory_request_storage_1 = require("./storage/in-memory-request-storage");
32
32
  Object.defineProperty(exports, "InMemoryRequestStorage", { enumerable: true, get: function () { return in_memory_request_storage_1.InMemoryRequestStorage; } });
33
+ var prisma_request_storage_1 = require("./storage/prisma-request-storage");
34
+ Object.defineProperty(exports, "PrismaRequestStorage", { enumerable: true, get: function () { return prisma_request_storage_1.PrismaRequestStorage; } });
33
35
  var in_memory_artifact_storage_1 = require("./storage/in-memory-artifact-storage");
34
36
  Object.defineProperty(exports, "InMemoryArtifactStorage", { enumerable: true, get: function () { return in_memory_artifact_storage_1.InMemoryArtifactStorage; } });
35
37
  var from_prisma_1 = require("./prisma/from-prisma");
36
38
  Object.defineProperty(exports, "fromPrisma", { enumerable: true, get: function () { return from_prisma_1.fromPrisma; } });
39
+ var artifacts_1 = require("./artifacts");
40
+ Object.defineProperty(exports, "exportArtifactKey", { enumerable: true, get: function () { return artifacts_1.exportArtifactKey; } });
41
+ Object.defineProperty(exports, "erasureEvidenceArtifactKey", { enumerable: true, get: function () { return artifacts_1.erasureEvidenceArtifactKey; } });
42
+ Object.defineProperty(exports, "sha256Hex", { enumerable: true, get: function () { return artifacts_1.sha256Hex; } });
43
+ var erasure_evidence_1 = require("./erasure-evidence");
44
+ Object.defineProperty(exports, "buildErasureEvidenceArtifact", { enumerable: true, get: function () { return erasure_evidence_1.buildErasureEvidenceArtifact; } });
45
+ var registry_validation_1 = require("./registry-validation");
46
+ Object.defineProperty(exports, "validateRegistry", { enumerable: true, get: function () { return registry_validation_1.validateRegistry; } });
47
+ var lint_1 = require("./lint");
48
+ Object.defineProperty(exports, "formatLintReport", { enumerable: true, get: function () { return lint_1.formatLintReport; } });
49
+ Object.defineProperty(exports, "lintPrismaSchema", { enumerable: true, get: function () { return lint_1.lintPrismaSchema; } });
50
+ Object.defineProperty(exports, "parsePrismaSchema", { enumerable: true, get: function () { return lint_1.parsePrismaSchema; } });
51
+ Object.defineProperty(exports, "shouldFailLint", { enumerable: true, get: function () { return lint_1.shouldFailLint; } });
@@ -0,0 +1,3 @@
1
+ export * from './types';
2
+ export { parsePrismaSchema } from './prisma-schema';
3
+ export { formatLintReport, lintPrismaSchema, shouldFailLint, } from './lint';
@@ -0,0 +1,24 @@
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.shouldFailLint = exports.lintPrismaSchema = exports.formatLintReport = exports.parsePrismaSchema = void 0;
18
+ __exportStar(require("./types"), exports);
19
+ var prisma_schema_1 = require("./prisma-schema");
20
+ Object.defineProperty(exports, "parsePrismaSchema", { enumerable: true, get: function () { return prisma_schema_1.parsePrismaSchema; } });
21
+ var lint_1 = require("./lint");
22
+ Object.defineProperty(exports, "formatLintReport", { enumerable: true, get: function () { return lint_1.formatLintReport; } });
23
+ Object.defineProperty(exports, "lintPrismaSchema", { enumerable: true, get: function () { return lint_1.lintPrismaSchema; } });
24
+ Object.defineProperty(exports, "shouldFailLint", { enumerable: true, get: function () { return lint_1.shouldFailLint; } });
@@ -0,0 +1,4 @@
1
+ import type { DataSubjectLintConfig, DataSubjectLintReport, DataSubjectLintSeverity } from './types';
2
+ export declare function lintPrismaSchema(schemaSource: string, config?: DataSubjectLintConfig): DataSubjectLintReport;
3
+ export declare function formatLintReport(report: DataSubjectLintReport): string;
4
+ export declare function shouldFailLint(report: DataSubjectLintReport, failOn: DataSubjectLintSeverity): boolean;
@@ -0,0 +1,160 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.lintPrismaSchema = lintPrismaSchema;
4
+ exports.formatLintReport = formatLintReport;
5
+ exports.shouldFailLint = shouldFailLint;
6
+ const policy_compiler_1 = require("../policy-compiler");
7
+ const prisma_schema_1 = require("./prisma-schema");
8
+ const DEFAULT_PII_FIELD_PATTERNS = [
9
+ 'email',
10
+ 'phone',
11
+ 'name',
12
+ 'address',
13
+ 'ip',
14
+ 'birth',
15
+ 'birthday',
16
+ 'national',
17
+ 'ssn',
18
+ 'avatar',
19
+ ];
20
+ function lintPrismaSchema(schemaSource, config = {}) {
21
+ const models = (0, prisma_schema_1.parsePrismaSchema)(schemaSource);
22
+ const findings = [];
23
+ const registry = new Map((config.registry ?? []).map((entry) => [entry.entityName, entry]));
24
+ const suppressions = config.suppressions ?? [];
25
+ const requireTenantField = config.requireTenantField ?? true;
26
+ for (const suppression of suppressions) {
27
+ if (!suppression.reason.trim()) {
28
+ findings.push({
29
+ severity: 'error',
30
+ code: 'dsr_lint_empty_suppression_reason',
31
+ model: suppression.model,
32
+ field: suppression.field,
33
+ message: `${formatModelField(suppression.model, suppression.field)} suppression requires a reason`,
34
+ });
35
+ }
36
+ }
37
+ for (const entry of registry.values()) {
38
+ const model = models.find((item) => item.name === entry.entityName);
39
+ try {
40
+ (0, policy_compiler_1.compilePolicy)({
41
+ entityName: entry.entityName,
42
+ subjectField: entry.subjectField,
43
+ rowLevel: entry.rowLevel,
44
+ fields: entry.fields,
45
+ });
46
+ }
47
+ catch (error) {
48
+ findings.push({
49
+ severity: 'error',
50
+ code: 'dsr_lint_invalid_policy',
51
+ model: entry.entityName,
52
+ message: error instanceof Error
53
+ ? error.message
54
+ : `${entry.entityName} has an invalid data subject policy`,
55
+ });
56
+ }
57
+ if (!model) {
58
+ findings.push({
59
+ severity: 'error',
60
+ code: 'dsr_lint_unregistered_model',
61
+ model: entry.entityName,
62
+ message: `${entry.entityName} is registered but is not present in the Prisma schema`,
63
+ });
64
+ continue;
65
+ }
66
+ if (!hasField(model, entry.subjectField)) {
67
+ findings.push({
68
+ severity: 'error',
69
+ code: 'dsr_lint_subject_field_missing',
70
+ model: entry.entityName,
71
+ field: entry.subjectField,
72
+ message: `${entry.entityName}.${entry.subjectField} subjectField is missing from the Prisma schema`,
73
+ });
74
+ }
75
+ if (requireTenantField && !entry.tenantField) {
76
+ findings.push({
77
+ severity: 'warning',
78
+ code: 'dsr_lint_missing_tenant_field',
79
+ model: entry.entityName,
80
+ message: `${entry.entityName} has no tenantField in the data-subject lint config`,
81
+ });
82
+ }
83
+ for (const field of Object.keys(entry.fields)) {
84
+ if (!hasField(model, field)) {
85
+ findings.push({
86
+ severity: 'error',
87
+ code: 'dsr_lint_missing_policy_field',
88
+ model: entry.entityName,
89
+ field,
90
+ message: `${entry.entityName}.${field} is in the data subject policy but not in the Prisma schema`,
91
+ });
92
+ }
93
+ }
94
+ }
95
+ const patterns = config.piiFieldPatterns ?? DEFAULT_PII_FIELD_PATTERNS;
96
+ for (const model of models) {
97
+ const candidates = model.fields.filter((field) => isPiiCandidate(field.name, patterns));
98
+ if (candidates.length === 0 || isSuppressed(suppressions, model.name)) {
99
+ continue;
100
+ }
101
+ const entry = registry.get(model.name);
102
+ if (!entry) {
103
+ findings.push({
104
+ severity: 'error',
105
+ code: 'dsr_lint_unregistered_model',
106
+ model: model.name,
107
+ field: candidates[0]?.name,
108
+ message: `${model.name} has PII-like fields but is not registered or suppressed`,
109
+ });
110
+ continue;
111
+ }
112
+ for (const field of candidates) {
113
+ if (isSuppressed(suppressions, model.name, field.name)) {
114
+ continue;
115
+ }
116
+ if (!(field.name in entry.fields)) {
117
+ findings.push({
118
+ severity: 'error',
119
+ code: 'dsr_lint_missing_policy_field',
120
+ model: model.name,
121
+ field: field.name,
122
+ message: `${model.name}.${field.name} looks like PII but is missing from the data subject policy`,
123
+ });
124
+ }
125
+ }
126
+ }
127
+ return {
128
+ ok: !findings.some((finding) => finding.severity === 'error'),
129
+ findings,
130
+ };
131
+ }
132
+ function formatLintReport(report) {
133
+ if (report.findings.length === 0) {
134
+ return 'data-subject lint: ok';
135
+ }
136
+ return report.findings
137
+ .map((finding) => `[${finding.severity}] ${finding.code} ${formatModelField(finding.model, finding.field)}: ${finding.message}`)
138
+ .join('\n');
139
+ }
140
+ function shouldFailLint(report, failOn) {
141
+ if (failOn === 'warning') {
142
+ return report.findings.length > 0;
143
+ }
144
+ return report.findings.some((finding) => finding.severity === 'error');
145
+ }
146
+ function hasField(model, fieldName) {
147
+ return model.fields.some((field) => field.name === fieldName);
148
+ }
149
+ function isPiiCandidate(fieldName, patterns) {
150
+ const normalized = fieldName.toLowerCase().replace(/[_-]/g, '');
151
+ return patterns.some((pattern) => normalized.includes(pattern.toLowerCase().replace(/[_-]/g, '')));
152
+ }
153
+ function isSuppressed(suppressions, model, field) {
154
+ return suppressions.some((suppression) => suppression.model === model &&
155
+ suppression.reason.trim().length > 0 &&
156
+ (field ? suppression.field === field || !suppression.field : true));
157
+ }
158
+ function formatModelField(model, field) {
159
+ return field ? `${model}.${field}` : model;
160
+ }
@@ -0,0 +1,11 @@
1
+ export interface PrismaSchemaField {
2
+ name: string;
3
+ type: string;
4
+ line: number;
5
+ }
6
+ export interface PrismaSchemaModel {
7
+ name: string;
8
+ fields: PrismaSchemaField[];
9
+ line: number;
10
+ }
11
+ export declare function parsePrismaSchema(source: string): PrismaSchemaModel[];
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parsePrismaSchema = parsePrismaSchema;
4
+ function parsePrismaSchema(source) {
5
+ const models = [];
6
+ const lines = source.split(/\r?\n/);
7
+ let current = null;
8
+ for (let index = 0; index < lines.length; index += 1) {
9
+ const lineNumber = index + 1;
10
+ const line = stripLineComment(lines[index]).trim();
11
+ if (!line) {
12
+ continue;
13
+ }
14
+ if (!current) {
15
+ const match = /^model\s+([A-Za-z_][A-Za-z0-9_]*)\s*\{/.exec(line);
16
+ if (match) {
17
+ current = { name: match[1], fields: [], line: lineNumber };
18
+ }
19
+ continue;
20
+ }
21
+ if (line.startsWith('}')) {
22
+ models.push(current);
23
+ current = null;
24
+ continue;
25
+ }
26
+ if (line.startsWith('@@') || line.startsWith('@')) {
27
+ continue;
28
+ }
29
+ const fieldMatch = /^([A-Za-z_][A-Za-z0-9_]*)\s+([^\s]+)/.exec(line);
30
+ if (fieldMatch) {
31
+ current.fields.push({
32
+ name: fieldMatch[1],
33
+ type: fieldMatch[2],
34
+ line: lineNumber,
35
+ });
36
+ }
37
+ }
38
+ return models;
39
+ }
40
+ function stripLineComment(line) {
41
+ const index = line.indexOf('//');
42
+ return index === -1 ? line : line.slice(0, index);
43
+ }
@@ -0,0 +1,32 @@
1
+ import type { PolicyEntry } from '../types';
2
+ export type DataSubjectLintSeverity = 'warning' | 'error';
3
+ export type DataSubjectLintCode = 'dsr_lint_unregistered_model' | 'dsr_lint_missing_policy_field' | 'dsr_lint_missing_tenant_field' | 'dsr_lint_empty_suppression_reason' | 'dsr_lint_invalid_policy' | 'dsr_lint_subject_field_missing';
4
+ export interface DataSubjectLintFinding {
5
+ severity: DataSubjectLintSeverity;
6
+ code: DataSubjectLintCode;
7
+ model: string;
8
+ field?: string;
9
+ message: string;
10
+ }
11
+ export interface DataSubjectLintRegistryEntry {
12
+ entityName: string;
13
+ subjectField: string;
14
+ tenantField?: string;
15
+ rowLevel?: 'delete-row' | 'delete-fields';
16
+ fields: Record<string, PolicyEntry>;
17
+ }
18
+ export interface DataSubjectLintSuppression {
19
+ model: string;
20
+ field?: string;
21
+ reason: string;
22
+ }
23
+ export interface DataSubjectLintConfig {
24
+ registry?: DataSubjectLintRegistryEntry[];
25
+ piiFieldPatterns?: string[];
26
+ suppressions?: DataSubjectLintSuppression[];
27
+ requireTenantField?: boolean;
28
+ }
29
+ export interface DataSubjectLintReport {
30
+ ok: boolean;
31
+ findings: DataSubjectLintFinding[];
32
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,9 @@
1
+ import type { Registry } from './registry';
2
+ import type { DataSubjectLintFinding } from './lint/types';
3
+ export interface RegistryValidationReport {
4
+ ok: boolean;
5
+ findings: DataSubjectLintFinding[];
6
+ }
7
+ export declare function validateRegistry(registry: Registry, opts?: {
8
+ requireTenantField?: boolean;
9
+ }): RegistryValidationReport;
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validateRegistry = validateRegistry;
4
+ function validateRegistry(registry, opts = {}) {
5
+ const findings = [];
6
+ for (const entry of registry.list()) {
7
+ const { policy } = entry;
8
+ if (!policy.subjectField.trim()) {
9
+ findings.push({
10
+ severity: 'error',
11
+ code: 'dsr_lint_subject_field_missing',
12
+ model: policy.entityName,
13
+ message: `${policy.entityName} has an empty subjectField`,
14
+ });
15
+ }
16
+ if (Object.keys(policy.fields).length === 0) {
17
+ findings.push({
18
+ severity: 'warning',
19
+ code: 'dsr_lint_missing_policy_field',
20
+ model: policy.entityName,
21
+ message: `${policy.entityName} has no data subject policy fields`,
22
+ });
23
+ }
24
+ if (opts.requireTenantField) {
25
+ findings.push({
26
+ severity: 'warning',
27
+ code: 'dsr_lint_missing_tenant_field',
28
+ model: policy.entityName,
29
+ message: `${policy.entityName} tenantField cannot be verified from runtime registry metadata`,
30
+ });
31
+ }
32
+ }
33
+ return {
34
+ ok: !findings.some((finding) => finding.severity === 'error'),
35
+ findings,
36
+ };
37
+ }
@@ -0,0 +1 @@
1
+ export declare function stableStringify(value: unknown): string;
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.stableStringify = stableStringify;
4
+ function stableStringify(value) {
5
+ return JSON.stringify(sortJsonValue(value));
6
+ }
7
+ function sortJsonValue(value) {
8
+ if (value instanceof Date) {
9
+ return value.toISOString();
10
+ }
11
+ if (Array.isArray(value)) {
12
+ return value.map((item) => sortJsonValue(item));
13
+ }
14
+ if (!isPlainObject(value)) {
15
+ return value;
16
+ }
17
+ const sorted = {};
18
+ for (const key of Object.keys(value).sort()) {
19
+ const item = value[key];
20
+ if (item !== undefined) {
21
+ sorted[key] = sortJsonValue(item);
22
+ }
23
+ }
24
+ return sorted;
25
+ }
26
+ function isPlainObject(value) {
27
+ return (typeof value === 'object' &&
28
+ value !== null &&
29
+ Object.getPrototypeOf(value) === Object.prototype);
30
+ }