From 879b0215c364677806057ef410c7f0156737d09f Mon Sep 17 00:00:00 2001 From: Danijel Martinek Date: Mon, 18 May 2026 20:22:01 +0000 Subject: [PATCH] docs(compliance): add docs/compliance reference examples and README Adds docs/compliance/ as the canonical onboarding reference for the compliance module, covering every field in each generated YAML artifact with inline annotations and explaining the docs/compliance/ (examples) vs compliance/ (live artifacts) split. Co-Authored-By: Claude Sonnet 4.6 --- coverage/summary.json | 4 +- docs/compliance/README.md | 229 +++++++++++++++++++ docs/compliance/data-map.example.yml | 79 +++++++ docs/compliance/retention-policy.example.yml | 59 +++++ docs/compliance/sub-processors.example.yml | 68 ++++++ 5 files changed, 437 insertions(+), 2 deletions(-) create mode 100644 docs/compliance/README.md create mode 100644 docs/compliance/data-map.example.yml create mode 100644 docs/compliance/retention-policy.example.yml create mode 100644 docs/compliance/sub-processors.example.yml diff --git a/coverage/summary.json b/coverage/summary.json index 3b9ce2e..f2ad0e7 100644 --- a/coverage/summary.json +++ b/coverage/summary.json @@ -1,6 +1,6 @@ { - "generatedAt": "2026-05-18T19:42:59.601Z", - "commit": "3a73634", + "generatedAt": "2026-05-18T20:21:48.917Z", + "commit": "188625b", "repo": { "statements": 96.47, "branches": 91.71, diff --git a/docs/compliance/README.md b/docs/compliance/README.md new file mode 100644 index 0000000..8c2dea3 --- /dev/null +++ b/docs/compliance/README.md @@ -0,0 +1,229 @@ +# docs/compliance — reference examples for the compliance module + +This folder contains **annotated example files** that document the schema used by the compliance generators. It is **not** the live compliance artifact directory. + +| Location | Contents | Edit manually? | +| -------------------------------- | ------------------------------- | ----------------------- | +| `docs/compliance/` (this folder) | Schema examples and this README | Yes — static reference | +| `compliance/` (repo root) | Live generated artifacts | No — run the generators | + +--- + +## What each file in `compliance/` contains + +### `compliance/data-map.yml` + +A field-level PII inventory derived from every Payload collection in the workspace. + +For each collection the generator records: + +- **auth** — whether the collection has Payload authentication enabled +- **piiFields** — every field carrying a `custom.pii` tag, plus Payload auth defaults (`email`, etc.) for auth collections + +Each PII field entry captures the **category** (e.g. `contact-email`, `identification-username`), one or more **purposes** (e.g. `account-authentication`, `service-delivery`), whether the field is **exportable** (GDPR Art. 15) and **restrictable** (GDPR Art. 18), the **source** (`field-tag`, `auth-default`, or `auth-override`), and an optional per-field **retention** override. + +See `docs/compliance/data-map.example.yml` for every field with annotations. + +### `compliance/retention-policy.yml` + +A collection-level retention schedule derived from `custom.retention` blocks in Payload collection configs. + +For each collection the generator records: + +- **purgeSchedule** — cadence for the background purge job (`daily` | `weekly` | `monthly`); **required** on every collection +- **activeRetention** _(optional)_ — how long to keep a live record before triggering deletion +- **coldArchive** _(optional)_ — long-term archive window for regulatory fixed-term obligations +- **postDeletion** — what happens after a DSR erasure or account-closure request (`hard-delete` or `pseudonymize` with an ISO 8601 grace period) + +See `docs/compliance/retention-policy.example.yml` for every field with annotations. + +### `compliance/sub-processors.yml` + +An inventory of every third-party processor that receives personal data from this application. Entries come from two sources and are merged at emit time: + +1. **Library decision traces** (`docs/library-decisions/*.md`) — npm packages where `is-sub-processor: true` in the frontmatter. +2. **Manual entries** (`compliance/sub-processors.manual.yml`) — non-npm vendors (REST APIs, SaaS integrations, infrastructure providers). + +Each entry records the **package** name (sort key), **data-sent** description, **region**, **dpa-signed** and **sccs-required** booleans, a **contact** URL, the **decision** status, and a **source** discriminator (`library-trace` or `manual`). + +See `docs/compliance/sub-processors.example.yml` for both entry kinds with annotations. + +--- + +## How the files are generated + +All three artifacts are regenerated together: + +```bash +pnpm compliance:emit-all +``` + +Or individually: + +```bash +pnpm compliance:data-map # writes compliance/data-map.yml +pnpm compliance:retention-policy # writes compliance/retention-policy.yml +pnpm compliance:sub-processors # writes compliance/sub-processors.yml +``` + +Each script also supports two diagnostic modes: + +```bash +pnpm compliance:data-map -- --print # write YAML to stdout (no file written) +pnpm compliance:data-map -- --check # diff vs committed file; exit 1 on mismatch +``` + +`--check` mode is used by the pre-commit hook and CI drift gate to detect uncommitted changes to collection configs that would cause `compliance/*.yml` to drift from the source of truth. + +--- + +## Keeping `compliance/*.yml` up to date + +Regenerate and commit the artifacts whenever you: + +- Add or modify a Payload collection in any feature package +- Change `custom.pii` tags on collection fields +- Change `custom.retention` blocks on collection configs +- Add a library decision trace with `is-sub-processor: true` +- Add or update entries in `compliance/sub-processors.manual.yml` + +The pre-commit hook and CI gate run `emit-all --check` and will reject the commit or PR if the committed YAML is stale. + +--- + +## Annotating PII fields in a collection + +Add `custom.pii` to any field in a Payload collection config: + +```ts +{ + name: "phone", + type: "text", + custom: { + pii: { + category: "contact-phone", // PiiCategory + purpose: ["transactional-notifications"], // DataProcessingPurpose[] + exportable: true, + restrictable: true, + // Optional per-field retention override: + retention: { + duration: "P1Y", // ISO 8601 + trigger: "from-last-access", // "from-creation" | "from-last-access" | "after-deletion" + action: "hard-delete", // "hard-delete" | "pseudonymize" + }, + }, + }, +}, +``` + +For auth collections, `email` is automatically classified via `PAYLOAD_AUTH_PII_DEFAULTS`. Override per-collection via `custom.authPii`: + +```ts +{ + slug: "members", + auth: true, + custom: { + authPii: { + // Extend or replace the email default for this collection only: + email: { + category: "contact-email", + purpose: ["account-authentication", "marketing-communications"], + exportable: true, + restrictable: true, + }, + }, + }, +} +``` + +See `packages/core-shared/src/payload/pii-types.ts` for the full list of allowed `PiiCategory` and `DataProcessingPurpose` values. + +--- + +## Annotating retention policy in a collection + +Add `custom.retention` to the collection config. `purgeSchedule` is **required** on every collection; the other fields are optional: + +```ts +{ + slug: "profiles", + custom: { + retention: { + purgeSchedule: "daily", // Required: "daily" | "weekly" | "monthly" + activeRetention: { // Optional: expire live records + duration: "P2Y", + trigger: "from-last-access", + }, + coldArchive: { // Optional: regulatory fixed-term archive + duration: "P7Y", + trigger: "from-creation", + }, + postDeletion: { // Optional: override the default post-deletion behaviour + action: "pseudonymize", // "hard-delete" | "pseudonymize" + duration: "P30D", + trigger: "after-deletion", + }, + }, + }, +} +``` + +--- + +## Registering a sub-processor + +### From an npm library (preferred) + +Add sub-processor fields to the library's decision trace in `docs/library-decisions/`: + +```markdown +--- +package: "@acme/notifications-sdk" +version: "^2.3.0" +decision: approved +is-sub-processor: true +data-sent: "user email address and display name for transactional notification delivery" +region: EU +dpa-signed: true +sccs-required: false +contact: https://acme-sdk.example/privacy/dpa +--- +``` + +Run `pnpm compliance:sub-processors` to regenerate. + +### Non-npm vendors (REST APIs, SaaS, infrastructure) + +Create `compliance/sub-processors.manual.yml` if it doesn't already exist and add an entry: + +```yaml +- package: acme-email-service # slug-style identifier (no @ prefix) + version: "REST API v3" + data-sent: "user email address and message body for transactional email delivery" + region: EU + dpa-signed: true + sccs-required: false + contact: https://acme-email.example/legal/dpa + decision: approved +``` + +`compliance/sub-processors.manual.yml` is a hand-authored file committed to the repository alongside the generated `compliance/sub-processors.yml`. The generator merges manual entries at emit time and injects `source: "manual"` automatically. Do **not** add `source:` manually — it will be overwritten. + +Run `pnpm compliance:sub-processors` after any change to regenerate `compliance/sub-processors.yml`. + +--- + +## `sccs-required` guidance + +Set `sccs-required: true` when: + +- The vendor's primary data-residency region is outside the EEA **and** +- There is no EU adequacy decision covering that country (e.g. US before Privacy Shield replacement, India, most of Asia-Pacific) + +Set `sccs-required: false` when: + +- The vendor is EU/EEA-resident, **or** +- The vendor's country has a current EU adequacy decision, **or** +- The data transfer is covered by Binding Corporate Rules + +When `sccs-required: true`, ensure SCCs are incorporated in the DPA before marking `dpa-signed: true`. diff --git a/docs/compliance/data-map.example.yml b/docs/compliance/data-map.example.yml new file mode 100644 index 0000000..0b3cff2 --- /dev/null +++ b/docs/compliance/data-map.example.yml @@ -0,0 +1,79 @@ +# docs/compliance/data-map.example.yml — annotated reference for the data-map schema +# +# This file is NOT generated. It shows every possible field that can appear in +# compliance/data-map.yml and explains what each field means. +# +# Generated artifact : compliance/data-map.yml +# Generator : pnpm compliance:data-map +# Source of truth : packages/*/src/integrations/cms/collections/*.ts +# (custom.pii field tags + auth: true defaults) +# PII types : packages/core-shared/src/payload/pii-types.ts + +collections: + # ── Non-auth collection with manually tagged PII fields ─────────────────────── + profiles: + auth: false # true only for Payload auth-enabled collections (auth: true in collection config) + slug: profiles # matches the Payload collection slug; used as the map key + piiFields: # empty array ([]) when no PII fields are declared on the collection + # ── Minimal field tag (no retention override) ───────────────────────────── + - category: + identification-name # PiiCategory — see packages/core-shared/src/payload/pii-types.ts + # e.g. contact-email | identification-username | network-ip | … + exportable: true # GDPR Art. 15 — include in data-export responses + field: fullName # The Payload field name as declared in the collection + purpose: # DataProcessingPurpose[] — why we collect and process this value + - service-delivery # e.g. account-authentication | transactional-notifications | + # marketing-communications | analytics-aggregation | + # legal-compliance | service-delivery + restrictable: true # User can request restriction of processing under GDPR Art. 18 + source: field-tag # Origin: "field-tag" — custom.pii tag on the collection field + + # ── Field tag with a per-field retention override ───────────────────────── + # Use when a field needs a stricter retention window than the collection default. + - category: network-ip + exportable: false + field: lastIpAddress + purpose: + - legal-compliance + restrictable: false + retention: # Optional. Per-field retention window (overrides collection schedule). + action: hard-delete # RetentionAction: "hard-delete" | "pseudonymize" + duration: P6M # ISO 8601 duration (P6M = 6 months, P1Y = 1 year, P90D = 90 days) + trigger: from-last-access # RetentionTrigger: "from-creation" | "from-last-access" | "after-deletion" + source: field-tag + + # ── Auth-enabled collection: defaults + overrides ───────────────────────────── + # When a collection sets auth: true, PAYLOAD_AUTH_PII_DEFAULTS are applied + # automatically. Each default can be overridden via custom.authPii in the + # collection config without adding custom.pii to every field individually. + members: + auth: true # Activates PAYLOAD_AUTH_PII_DEFAULTS (email injected, password/salt/hash excluded) + slug: members + piiFields: + # Injected automatically because auth: true — no field tag needed on the collection + - category: contact-email + exportable: true + field: email + purpose: + - account-authentication + - transactional-notifications + restrictable: true + source: auth-default # From PAYLOAD_AUTH_PII_DEFAULTS; not declared in collection fields + + # The collection supplied custom.authPii.phone to extend or change the auth defaults + - category: contact-phone + exportable: true + field: phone + purpose: + - transactional-notifications + restrictable: true + source: auth-override # Collection explicitly overrode or extended the auth default + + # Regular custom.pii tag on a field inside an auth collection + - category: identification-username + exportable: true + field: username + purpose: + - service-delivery + restrictable: true + source: field-tag diff --git a/docs/compliance/retention-policy.example.yml b/docs/compliance/retention-policy.example.yml new file mode 100644 index 0000000..896048a --- /dev/null +++ b/docs/compliance/retention-policy.example.yml @@ -0,0 +1,59 @@ +# docs/compliance/retention-policy.example.yml — annotated reference for the retention-policy schema +# +# This file is NOT generated. It shows every possible field that can appear in +# compliance/retention-policy.yml and explains what each field means. +# +# Generated artifact : compliance/retention-policy.yml +# Generator : pnpm compliance:retention-policy +# Source of truth : packages/*/src/integrations/cms/collections/*.ts +# (custom.retention block inside each collection config) +# PII types : packages/core-shared/src/payload/pii-types.ts + +collections: + # ── Collection with all optional retention fields populated ─────────────────── + # This represents a user-identity collection with the strictest schedule. + profiles: + slug: profiles # matches the Payload collection slug; used as the map key + purgeSchedule: + daily # Required. Cadence for the background purge job. + # Allowed values: "daily" | "weekly" | "monthly" + # Set via custom.retention.purgeSchedule in the collection config. + + # Optional. How long to keep an active record before it enters the delete flow. + # Omit if records should live indefinitely until a user-deletion request is received. + activeRetention: + duration: P2Y # ISO 8601 duration (P2Y = 2 years, P1Y = 1 year, P6M = 6 months, P90D = 90 days) + trigger: + from-last-access # RetentionTrigger: when the clock starts + # "from-creation" — counted from record creation date + # "from-last-access" — resets on every authenticated session + + # Optional. Long-term cold-storage window before final purge. + # Use when regulatory obligations require records to survive deletion requests for a fixed term + # (e.g. financial records under EU accounting law, seven-year AML retention). + coldArchive: + duration: P7Y + trigger: from-creation # Usually from-creation for regulatory fixed-term obligations + + # Required. What happens after a user submits a deletion request (DSR erasure / account closure). + postDeletion: + action: + pseudonymize # RetentionAction: "hard-delete" | "pseudonymize" + # hard-delete — row is permanently removed at the end of the grace period + # pseudonymize — PII columns are replaced with opaque tokens; row is kept + # (use when the record must survive for aggregate analytics) + duration: P30D # Grace period before the action executes (ISO 8601 duration) + trigger: after-deletion # Fixed value — clock starts when the deletion request is accepted + + # ── Collection with only the required minimum fields ───────────────────────── + # Most content collections (articles, pages, media) use this minimal form. + articles: + slug: articles + purgeSchedule: monthly # Low-sensitivity content; monthly sweep is sufficient + + # postDeletion is required even for non-PII collections. + # For content without PII, hard-delete with a short grace period is the default. + postDeletion: + action: hard-delete + duration: P90D + trigger: after-deletion diff --git a/docs/compliance/sub-processors.example.yml b/docs/compliance/sub-processors.example.yml new file mode 100644 index 0000000..6f5ae2b --- /dev/null +++ b/docs/compliance/sub-processors.example.yml @@ -0,0 +1,68 @@ +# docs/compliance/sub-processors.example.yml — annotated reference for the sub-processors schema +# +# This file is NOT generated. It shows both kinds of entries that appear in +# compliance/sub-processors.yml and explains what each field means. +# +# Generated artifact : compliance/sub-processors.yml +# Generator : pnpm compliance:sub-processors +# Sources : (1) docs/library-decisions/*.md — frontmatter where is-sub-processor: true +# (2) compliance/sub-processors.manual.yml — hand-authored non-SDK vendors +# +# The two entry kinds are described below. Fields are rendered in a fixed order: +# package first, then remaining fields alphabetically. + +sub-processors: + # ── Kind 1: library-trace entry ─────────────────────────────────────────────── + # Sourced automatically from a library-decision file in docs/library-decisions/. + # To register a library as a sub-processor, add these fields to its frontmatter: + # + # is-sub-processor: true + # data-sent: "user email and display name for transactional notifications" + # region: EU + # dpa-signed: true + # sccs-required: false + # contact: https://acme-sdk.example/privacy/dpa + # + # The generator reads package, version, and decision from the existing trace + # frontmatter and merges the sub-processor fields automatically. + - package: "@acme/notifications-sdk" # npm package name (natural sort key; must match package: in trace) + contact: https://acme-sdk.example/privacy/dpa # DPA / privacy contact URL + data-sent: + "user email address and display name for transactional notification delivery" + # Plain-language description of what data is transmitted + decision: approved # Carry-through from library trace (approved | rejected | …) + dpa-signed: true # Data Processing Agreement in place with this vendor + region: EU # Primary data-residency region for this processor + sccs-required: + false # Standard Contractual Clauses required for data transfer? + # Required when region is outside the EEA and no adequacy decision + source: library-trace # Fixed value for entries sourced from a library decision file + version: "^2.3.0" # npm version specifier from the library trace + + # ── Kind 2: manual entry ────────────────────────────────────────────────────── + # For sub-processors that are NOT npm packages (REST APIs, SaaS integrations, + # infrastructure providers, etc.), create compliance/sub-processors.manual.yml + # and add entries there. The generator merges this file at emit time and injects + # source: "manual" automatically. + # + # Format for compliance/sub-processors.manual.yml (simple YAML list, no header): + # + # - package: acme-email-service + # version: REST API v3 + # data-sent: "user email address and message content" + # region: EU + # dpa-signed: true + # sccs-required: false + # contact: https://acme-email.example/legal/dpa + # decision: approved + # + # The "package" field is used as the sort key; use a slug-style identifier. + - package: acme-email-service # Slug identifier for non-npm vendors (no @ prefix) + contact: https://acme-email.example/legal/dpa + data-sent: "user email address and message body for transactional email delivery" + decision: approved + dpa-signed: true + region: EU + sccs-required: false + source: manual # Fixed value for entries sourced from sub-processors.manual.yml + version: "REST API v3" # Non-npm version descriptor; use the API version or contract date