feat(core-dsr): scaffold package + GDPR DSR interfaces and types
- Add pnpm turbo gen core-package dsr generator template and register dsr in CORE_PACKAGE_GENERATORS / choices list - Run generator to produce packages/core-dsr/ shell - Define IDataExport (Art. 15/20), IDataDelete (Art. 17), IDataRectify (Art. 16), IProcessingRestriction (Art. 18) interfaces - Add UserDataBundle and DeletionCertificate types in dsr-types.ts - Ship core-dsr/contexts/user-data.jsonld schema.org JSON-LD @context - Wire @repo/core-dsr into transpilePackages (web-next) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
33
packages/core-dsr/AGENTS.md
Normal file
33
packages/core-dsr/AGENTS.md
Normal file
@@ -0,0 +1,33 @@
|
||||
# @repo/core-dsr
|
||||
|
||||
Optional core package providing GDPR Data Subject Rights (DSR) interfaces and implementations. Scaffold via `pnpm turbo gen core-package dsr`.
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
src/
|
||||
data-export.interface.ts # IDataExport — exportSubjectData
|
||||
data-delete.interface.ts # IDataDelete — deleteSubjectData
|
||||
data-rectify.interface.ts # IDataRectify — updateSubjectField
|
||||
processing-restriction.interface.ts # IProcessingRestriction — setRestriction, isRestricted
|
||||
dsr-types.ts # UserDataBundle, DeletionCertificate, DSR value types
|
||||
contexts/
|
||||
user-data.jsonld # schema.org JSON-LD @context
|
||||
index.ts # Barrel export
|
||||
```
|
||||
|
||||
## Design
|
||||
|
||||
Four interfaces map directly to GDPR Articles 15–18 + 20:
|
||||
|
||||
- `IDataExport` (Art. 15/20) — export a subject's data as `UserDataBundle`
|
||||
- `IDataDelete` (Art. 17) — soft-delete or cascade-hard-delete subject data; returns `DeletionCertificate`
|
||||
- `IDataRectify` (Art. 16) — update a specific field for a subject
|
||||
- `IProcessingRestriction` (Art. 18) — toggle and read the processing restriction flag
|
||||
|
||||
Implementations walk `custom.pii`-tagged fields and `custom.subject`-linked collections. Row semantics:
|
||||
|
||||
- `kind: "self" | "owner"` → directly owned by the subject (export full; delete hard or soft)
|
||||
- `kind: "reference"` → references the subject from another entity (export redacted; redact link on delete)
|
||||
|
||||
See `docs/architecture/agent-first-workflow-and-conformance.md` for the DI conventions.
|
||||
3
packages/core-dsr/eslint.config.js
Normal file
3
packages/core-dsr/eslint.config.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import baseConfig from "@repo/core-eslint/base";
|
||||
|
||||
export default baseConfig;
|
||||
26
packages/core-dsr/package.json
Normal file
26
packages/core-dsr/package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "@repo/core-dsr",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc --noEmit",
|
||||
"lint": "eslint .",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run --passWithNoTests"
|
||||
},
|
||||
"dependencies": {
|
||||
"@repo/core-shared": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@repo/core-eslint": "workspace:*",
|
||||
"@repo/core-testing": "workspace:*",
|
||||
"@repo/core-typescript": "workspace:*",
|
||||
"@vitest/coverage-v8": "^3.0.0",
|
||||
"typescript": "^5.8.0",
|
||||
"vitest": "^3.0.0"
|
||||
}
|
||||
}
|
||||
39
packages/core-dsr/src/contexts/user-data.jsonld
Normal file
39
packages/core-dsr/src/contexts/user-data.jsonld
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"@context": {
|
||||
"@vocab": "https://schema.org/",
|
||||
"dsr": "https://w3.org/ns/dpv#",
|
||||
"prov": "https://www.w3.org/ns/prov#",
|
||||
|
||||
"subjectId": "identifier",
|
||||
"exportedAt": "dateCreated",
|
||||
"format": "encodingFormat",
|
||||
|
||||
"data": {
|
||||
"@id": "prov:hadMember",
|
||||
"@container": "@index"
|
||||
},
|
||||
|
||||
"asSelf": {
|
||||
"@id": "dsr:hasPersonalDataHandling",
|
||||
"@type": "@id"
|
||||
},
|
||||
"asReference": {
|
||||
"@id": "dsr:hasDataSubjectRight",
|
||||
"@type": "@id"
|
||||
},
|
||||
"rowId": "identifier",
|
||||
"linkedField": "name",
|
||||
"linkedThrough": {
|
||||
"@id": "isPartOf",
|
||||
"@type": "@id"
|
||||
},
|
||||
|
||||
"auditLog": {
|
||||
"@id": "prov:wasGeneratedBy",
|
||||
"@type": "@id"
|
||||
},
|
||||
|
||||
"UserDataBundle": "dsr:RightOfAccess",
|
||||
"SubjectReference": "dsr:DataSubjectRight"
|
||||
}
|
||||
}
|
||||
29
packages/core-dsr/src/data-delete.interface.ts
Normal file
29
packages/core-dsr/src/data-delete.interface.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { DeletionCertificate, DeletionMode } from "./dsr-types";
|
||||
|
||||
/**
|
||||
* GDPR Art. 17 (right to erasure / "right to be forgotten").
|
||||
*
|
||||
* Two modes:
|
||||
*
|
||||
* - `"soft"` — sets `processingRestrictedAt`, NULLs all `exportable: true`
|
||||
* PII fields, and redacts `reference`-role linked fields to null. The row
|
||||
* structure is preserved so other subjects' data in shared rows remains
|
||||
* intact. Emits one RESTRICT audit entry per affected collection.
|
||||
*
|
||||
* - `"cascade-hard"` — hard-deletes `self` and `owner` rows immediately, then
|
||||
* redacts `reference` fields. Auth-guarded at the procedure layer; must not
|
||||
* be called from user-facing flows. Emits DELETE audit entries.
|
||||
*/
|
||||
export interface IDataDelete {
|
||||
/**
|
||||
* Delete or erase all personal data held for the given subject.
|
||||
*
|
||||
* @param subjectId - The subject's canonical ID.
|
||||
* @param mode - Deletion strategy (soft redaction vs hard cascade).
|
||||
* @returns A signed `DeletionCertificate` linking to the audit log entry.
|
||||
*/
|
||||
deleteSubjectData(
|
||||
subjectId: string,
|
||||
mode: DeletionMode,
|
||||
): Promise<DeletionCertificate>;
|
||||
}
|
||||
22
packages/core-dsr/src/data-export.interface.ts
Normal file
22
packages/core-dsr/src/data-export.interface.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { DsrFormat, UserDataBundle } from "./dsr-types";
|
||||
|
||||
/**
|
||||
* GDPR Art. 15 (right of access) + Art. 20 (right to data portability).
|
||||
*
|
||||
* Implementations walk all Payload collections with `custom.subject` linkage,
|
||||
* segment rows by role (self/owner vs reference), and filter to fields marked
|
||||
* `exportable: true` in `custom.pii`.
|
||||
*/
|
||||
export interface IDataExport {
|
||||
/**
|
||||
* Export all personal data held for the given subject.
|
||||
*
|
||||
* @param subjectId - The subject's canonical ID (e.g. users.id).
|
||||
* @param format - "json" for a plain JSON bundle; "json-ld" attaches the
|
||||
* @context from `contexts/user-data.jsonld`.
|
||||
*/
|
||||
exportSubjectData(
|
||||
subjectId: string,
|
||||
format: DsrFormat,
|
||||
): Promise<UserDataBundle>;
|
||||
}
|
||||
24
packages/core-dsr/src/data-rectify.interface.ts
Normal file
24
packages/core-dsr/src/data-rectify.interface.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* GDPR Art. 16 (right to rectification).
|
||||
*
|
||||
* Allows a subject to correct inaccurate personal data held about them.
|
||||
* Implementations verify the field is PII-tagged before updating and emit a
|
||||
* RESTRICT audit entry with `reason: "art-16-request"` as the tamper-evident
|
||||
* record of the correction.
|
||||
*/
|
||||
export interface IDataRectify {
|
||||
/**
|
||||
* Update a single PII field for the given subject in the specified collection.
|
||||
*
|
||||
* @param subjectId - The subject's canonical ID.
|
||||
* @param collection - Payload collection slug (e.g. "users").
|
||||
* @param field - Name of the field to update (must be `custom.pii`-tagged).
|
||||
* @param value - New value; must satisfy the field's Payload field type.
|
||||
*/
|
||||
updateSubjectField(
|
||||
subjectId: string,
|
||||
collection: string,
|
||||
field: string,
|
||||
value: unknown,
|
||||
): Promise<void>;
|
||||
}
|
||||
75
packages/core-dsr/src/dsr-types.ts
Normal file
75
packages/core-dsr/src/dsr-types.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import type { AuditEntry } from "@repo/core-shared/audit";
|
||||
|
||||
export type DsrFormat = "json" | "json-ld";
|
||||
|
||||
export type DeletionMode = "soft" | "cascade-hard";
|
||||
|
||||
export type DeletionReason =
|
||||
| "art-17-request"
|
||||
| "admin-expunge"
|
||||
| "retention-policy";
|
||||
|
||||
export type DeletionAction = "deleted" | "redacted" | "pseudonymized";
|
||||
|
||||
/** Row reference from a collection where the subject appears as a non-owner link. */
|
||||
export type SubjectReference = {
|
||||
rowId: string;
|
||||
/** Field name in the collection that links to the subject. */
|
||||
linkedField: string;
|
||||
/** Slug of the collection containing the reference. */
|
||||
linkedThrough: string;
|
||||
};
|
||||
|
||||
/** Per-collection data bucket within a UserDataBundle. */
|
||||
export type CollectionDataBucket = {
|
||||
/** Rows directly owned by the subject (kind: "self" | "owner"). */
|
||||
asSelf?: Array<Record<string, unknown>>;
|
||||
/** Rows referencing the subject without owning the row (kind: "reference"). */
|
||||
asReference?: SubjectReference[];
|
||||
};
|
||||
|
||||
/**
|
||||
* GDPR Art. 15/20 export payload.
|
||||
*
|
||||
* `data` is keyed by Payload collection slug. asSelf contains exportable-PII-
|
||||
* filtered rows the subject owns; asReference lists row IDs + link coordinates
|
||||
* for rows that merely reference the subject.
|
||||
*/
|
||||
export type UserDataBundle = {
|
||||
subjectId: string;
|
||||
/** ISO 8601 timestamp of when the export was produced. */
|
||||
exportedAt: string;
|
||||
format: DsrFormat;
|
||||
data: Record<string, CollectionDataBucket>;
|
||||
/** Audit entries scoped to this subject's activity. */
|
||||
auditLog?: AuditEntry[];
|
||||
/** JSON-LD @context URI or inline object; populated when format === "json-ld". */
|
||||
"@context"?: string | Record<string, unknown>;
|
||||
};
|
||||
|
||||
/** Per-collection summary of what the deletion touched. */
|
||||
export type DeletionAffected = {
|
||||
collection: string;
|
||||
rowsAffected: number;
|
||||
action: DeletionAction;
|
||||
/** PII field names that were NULLed when action === "redacted". */
|
||||
fields?: string[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Immutable proof of a completed GDPR Art. 17 deletion / erasure request.
|
||||
*
|
||||
* The `auditEntryId` links back to the audit log entry created at deletion
|
||||
* time, forming a tamper-evident chain for regulatory inspection.
|
||||
*/
|
||||
export type DeletionCertificate = {
|
||||
/** Subject ID, or "erased-{hash}" if the ID itself was purged. */
|
||||
subjectId: string;
|
||||
mode: DeletionMode;
|
||||
/** ISO 8601 timestamp of the deletion. */
|
||||
timestamp: string;
|
||||
reason: DeletionReason;
|
||||
affected: DeletionAffected[];
|
||||
/** ID of the audit log entry that recorded this operation. */
|
||||
auditEntryId: string;
|
||||
};
|
||||
16
packages/core-dsr/src/index.ts
Normal file
16
packages/core-dsr/src/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
export type {
|
||||
DsrFormat,
|
||||
DeletionMode,
|
||||
DeletionReason,
|
||||
DeletionAction,
|
||||
SubjectReference,
|
||||
CollectionDataBucket,
|
||||
UserDataBundle,
|
||||
DeletionAffected,
|
||||
DeletionCertificate,
|
||||
} from "./dsr-types";
|
||||
|
||||
export type { IDataExport } from "./data-export.interface";
|
||||
export type { IDataDelete } from "./data-delete.interface";
|
||||
export type { IDataRectify } from "./data-rectify.interface";
|
||||
export type { IProcessingRestriction } from "./processing-restriction.interface";
|
||||
25
packages/core-dsr/src/processing-restriction.interface.ts
Normal file
25
packages/core-dsr/src/processing-restriction.interface.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* GDPR Art. 18 (right to restriction of processing).
|
||||
*
|
||||
* Toggles and reads the `processingRestrictedAt` flag on the subject's user
|
||||
* record. When restricted, downstream use cases should call `isRestricted`
|
||||
* before processing personal data and short-circuit if true.
|
||||
*
|
||||
* Emits RESTRICT / UNRESTRICT audit entries on every state change.
|
||||
*/
|
||||
export interface IProcessingRestriction {
|
||||
/**
|
||||
* Grant or revoke processing restriction for the given subject.
|
||||
*
|
||||
* @param subjectId - The subject's canonical ID.
|
||||
* @param granted - true to restrict processing; false to lift restriction.
|
||||
*/
|
||||
setRestriction(subjectId: string, granted: boolean): Promise<void>;
|
||||
|
||||
/**
|
||||
* Return whether processing is currently restricted for the given subject.
|
||||
*
|
||||
* @param subjectId - The subject's canonical ID.
|
||||
*/
|
||||
isRestricted(subjectId: string): Promise<boolean>;
|
||||
}
|
||||
12
packages/core-dsr/tsconfig.json
Normal file
12
packages/core-dsr/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "@repo/core-typescript/base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["**/*.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
4
packages/core-dsr/turbo.json
Normal file
4
packages/core-dsr/turbo.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": ["//"],
|
||||
"tags": ["core"]
|
||||
}
|
||||
9
packages/core-dsr/vitest.config.ts
Normal file
9
packages/core-dsr/vitest.config.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import path from "node:path";
|
||||
import { mergeConfig } from "vitest/config";
|
||||
import { nodeVitestConfig } from "@repo/core-typescript/vitest.base.node";
|
||||
|
||||
export default mergeConfig(nodeVitestConfig, {
|
||||
resolve: {
|
||||
alias: { "@": path.resolve(__dirname, "./src") },
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user