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:
2026-05-19 19:32:03 +00:00
parent 86d949294f
commit e378c950a9
24 changed files with 490 additions and 9 deletions

View 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"
}
}

View 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>;
}

View 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>;
}

View 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>;
}

View 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;
};

View 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";

View 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>;
}