Files
0pt/src/types/pii.ts
T
admin 17bd1b40a0 feat(0pt): record legitimate-interest stance — B2B contacts, company-linked only
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.
2026-09-15 02:40:52 +02:00

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