Recorded decision (Simon 2026-08-31): personal data may be stored under legitimate interest ONLY as business contacts bound to a company/org entity. Gate enforces it: legitimate_interest requires purpose=business_contact and entity_linked_only=true; recordStored() requires entity_id (orgnr/rgs/LEI, hashed) and refuses person-only rows. +1 test; suite 167 pass | 1 skip; typecheck clean.
121 lines
4.1 KiB
TypeScript
121 lines
4.1 KiB
TypeScript
/**
|
|
* src/types/pii.ts
|
|
* =================
|
|
* 0pt.pii_policy.schema.v1 — SIAX Cloud GDPR guardrail for sources that
|
|
* CAN touch personuppgifter (personal data). Purpose: make processing of
|
|
* personal data "tekniskt OK" per GDPR (art. 5, 6, 25, 32, 17, 15):
|
|
* recorded legal basis, storage limitation, data minimisation,
|
|
* pseudonymisation, and working access/erasure surfaces.
|
|
*
|
|
* The guardrail only ever OPENS when a lawful basis is on record; nothing in
|
|
* 0pt today crawls personal data, so this ships as a fail-closed boundary:
|
|
* no policy → no storage (and, for directory domains like ratsit.se, no
|
|
* query at all).
|
|
*/
|
|
|
|
/** GDPR art. 6(1) legal bases 0pt accepts for personal-data sources. */
|
|
export type LegalBasis = "legitimate_interest" | "consent" | "contract" | "legal_obligation";
|
|
|
|
/**
|
|
* The purpose of processing. SIAX's recorded decision (Simon, 2026-08-31):
|
|
* personal data may be stored when LEGITIMATE INTEREST applies for business
|
|
* contacts — i.e. bound to a company/org entity, for business purposes.
|
|
* Other bases are allowed but every policy records its purpose.
|
|
*/
|
|
export type ProcessingPurpose = "business_contact" | "consent_marketing" | "contract_performance" | "legal_obligation";
|
|
|
|
/** Compliance posture for one source that touches personal data. */
|
|
export interface PiiPolicy {
|
|
source_id: string;
|
|
tenant_id: string;
|
|
project_id: string;
|
|
touches_personal_data: true;
|
|
legal_basis: LegalBasis;
|
|
purpose: ProcessingPurpose;
|
|
/**
|
|
* REQUIRED under legitimate_interest (business_contact): personal data may
|
|
* only be stored in correspondence with a company/org entity (org/rgs-nr,
|
|
* GLEIF/LEI). recordStored() rejects stray person-only rows.
|
|
*/
|
|
entity_linked_only: boolean;
|
|
/**
|
|
* Pointer to the evidence: a recorded Legitimate Interest Assessment
|
|
* (LIA) document id, the consent record id, the contract clause, or the
|
|
* legal provision. Must exist for audit/DPIA purposes — a bare claim is
|
|
* not enough.
|
|
*/
|
|
basis_ref: string;
|
|
/**
|
|
* Max storage in days. 0 = fetch-and-drop only (never retained).
|
|
* The retention worker deletes rows where stored_at + retention_days
|
|
* < now (art. 5(1)(e)).
|
|
*/
|
|
retention_days: number;
|
|
/**
|
|
* The ONLY fields that may be stored for this source (art. 5(1)(c)
|
|
* minimisation). Empty list = nothing may be stored.
|
|
*/
|
|
field_allowlist: string[];
|
|
/**
|
|
* Pseudonymise identity keys (personnummer, email, reg-number) before
|
|
* storage; only sha256 digests are ever written (art. 25).
|
|
*/
|
|
pseudonymize_keys: boolean;
|
|
/**
|
|
* Must be true for web/api sources: robots.txt checked, rate limit in
|
|
* force (SourceRatePolicy), no auth-wall / bot-bypass technique in use.
|
|
*/
|
|
robots_and_rate_confirmed: boolean;
|
|
created_at: string;
|
|
updated_at: string | null;
|
|
created_by: string | null;
|
|
}
|
|
|
|
export interface CreatePiiPolicyInput {
|
|
source_id: string;
|
|
tenant_id: string;
|
|
project_id: string;
|
|
legal_basis: LegalBasis;
|
|
basis_ref: string;
|
|
retention_days: number;
|
|
field_allowlist: string[];
|
|
purpose?: ProcessingPurpose;
|
|
entity_linked_only?: boolean;
|
|
pseudonymize_keys?: boolean;
|
|
robots_and_rate_confirmed?: boolean;
|
|
created_by?: string;
|
|
}
|
|
|
|
/**
|
|
* Registry row for one stored personal-data value. Only hashes are held —
|
|
* the key_hash lets 0pt locate the subject (right of access, art. 15) and
|
|
* the subject_hash lets it erase every artifact for that subject across
|
|
* sources (right to erasure, art. 17) without ever storing the plaintext
|
|
* identity.
|
|
*/
|
|
export interface PiiIdentityKey {
|
|
key_hash: string;
|
|
subject_hash: string;
|
|
/** Company/org entity the value is bound to (orgnr/rgs/LEI), hashed. */
|
|
entity_hash: string | null;
|
|
source_id: string;
|
|
tenant_id: string;
|
|
field: string;
|
|
artifact_ref: string | null;
|
|
stored_at: string;
|
|
expires_at: string;
|
|
status: "active" | "erased" | "purged";
|
|
}
|
|
|
|
export interface PiiErasureRequest {
|
|
subject_hash: string;
|
|
reason: "deleted_by_subject" | "retention_expired" | "consent_withdrawn";
|
|
actor_id?: string;
|
|
correlation_id?: string;
|
|
}
|
|
|
|
export interface PiiAccessStatement {
|
|
subject_hash: string;
|
|
rows: PiiIdentityKey[];
|
|
generated_at: string;
|
|
} |