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:
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>;
|
||||
}
|
||||
Reference in New Issue
Block a user