-
Notifications
You must be signed in to change notification settings - Fork 0
Compliance and PII
GDPR compliance is built in by design, not bolted on afterwards. @kengela/pii (core, pure)
covers classification, minimization and redaction; the ports of @kengela/contracts cover field
encryption, the access log and erasure; @kengela/adapter-authn-native provides the concrete
crypto-shredding (see also 03-authentication.md).
The functions of
@kengela/piioperate on the richDirectoryProfileof@kengela/iam-mapping.
classify(field) returns a field's sensitivity, across three levels:
| Sensitivity | Meaning | Examples |
|---|---|---|
none |
non-personal (technical identifier, org attachment) |
externalId, department, title, costCenter, locale
|
pii |
personal data (direct/indirect identifiability) |
email, firstName, lastName, phoneNumber, streetAddress, employeeNumber, manager
|
sensitive |
special category (GDPR art. 9: health, biometrics) | (none in a standard directory; provided for extension) |
import { classify, isPii, PII_FIELDS } from '@kengela/pii';
classify('email'); // 'pii'
classify('department'); // 'none'
isPii('phoneNumber'); // true
PII_FIELDS; // list of fields classified as personalThe registry is the source of truth: an unknown field falls back to none.
minimizeProfile(profile, allowedFields) keeps only the attributes explicitly allowed for the
app's purpose. Raw claims are dropped; unauthorized identity fields are nulled out (null).
import { minimizeProfile } from '@kengela/pii';
const minimal = minimizeProfile(profile, ['email', 'firstName', 'department']);
// firstName kept; lastName/displayName → null; attributes limited to department; claims emptiedIt is the "data" counterpart of the Kengela principle "each app picks its own subset": you don't carry attributes you don't need.
redactProfile(profile) masks personal data without exposing it in clear, for logs or partial
display. The email is masked while keeping the domain; pii fields are reduced to their initial;
non-personal fields stay intact.
import { redactProfile } from '@kengela/pii';
const safe = redactProfile(profile);
// email 'alice@corp.example' → 'a***@corp.example'; firstName 'Alice' → 'A***'; department unchangedretentionExpired(sensitivity, ageMs, policy?) tells whether a datum has exceeded its retention
period. The default policy is conservative:
| Sensitivity | Default period (DEFAULT_RETENTION) |
|---|---|
none |
unlimited (null) |
pii |
2 years (730 days) |
sensitive |
6 months (182 days) |
import { retentionExpired, DEFAULT_RETENTION, type RetentionPolicy } from '@kengela/pii';
const ageMs = Date.now() - createdAt.getTime();
if (retentionExpired('pii', ageMs)) {
// past retention → purge / anonymization
}
// An app can set its own periods:
const myPolicy: RetentionPolicy = {
none: null,
pii: 365 * 24 * 3600 * 1000,
sensitive: 90 * 24 * 3600 * 1000,
};
retentionExpired('pii', ageMs, myPolicy);Stored PII is encrypted at the field level, with cryptographic isolation per tenant. The port:
interface FieldCipherPort {
encryptField(tenantId: TenantId, plaintext: string): Promise<string>; // → storable base64
decryptField(tenantId: TenantId, ciphertext: string): Promise<string>;
}Implementation: AesGcmFieldCipher (AES-256-GCM, per-tenant derived key). See
03-authentication.md.
The recommended erasure is crypto-shredding: each data subject (subjectId) has its own key
(SubjectKeyStore); destroying the key makes all of its encrypted PII permanently unreadable,
without scanning every table.
interface SubjectKeyStore {
getOrCreateKey(tenantId, subjectId): Promise<Uint8Array>;
getKey(tenantId, subjectId): Promise<Uint8Array | null>;
deleteKey(tenantId, subjectId): Promise<void>;
}
interface ErasurePort {
eraseSubject(tenantId: TenantId, subjectId: string): Promise<void>;
}SubjectFieldCipher encrypts/decrypts per subject (returns null if the key has been destroyed),
SubjectCryptoShredder implements ErasurePort:
import { SubjectFieldCipher, SubjectCryptoShredder } from '@kengela/adapter-authn-native';
const cipher = new SubjectFieldCipher(subjectKeyStore);
const shredder = new SubjectCryptoShredder(subjectKeyStore);
await shredder.eraseSubject('t1', 'subject-42');
await cipher.decryptFor('t1', 'subject-42', enc); // null: data "shredded"Proven controls: after erasure, the PII is unreadable; another subject's key does not decrypt it. It is an effective GDPR erasure that does not depend on an exhaustive table scan.
Every read/export of personal data must be traceable: who, which subject, which fields, which purpose. The port:
interface PiiAccessLogSink {
record(entry: {
readonly tenantId: TenantId;
readonly subjectId: string; // data subject
readonly actorId?: UserId; // absent = system
readonly fields: readonly string[];
readonly purpose: string; // processing purpose
readonly at: number;
}): Promise<void> | void;
}const piiLog: PiiAccessLogSink = {
record(entry) {
auditDb.insert('pii_access_log', entry);
},
};
// On every PII access:
piiLog.record({
tenantId: 't1',
subjectId: 'subject-42',
actorId: currentUser.id,
fields: ['email', 'phoneNumber'],
purpose: 'support_ticket_resolution',
at: Date.now(),
});The implementation (the log destination) belongs to the application; the port guarantees that traceability is part of the contract, not an optional add-on.
| GDPR requirement | Tool |
|---|---|
| Minimization (art. 5.1.c) | minimizeProfile |
| Retention (art. 5.1.e) |
retentionExpired, DEFAULT_RETENTION
|
| At-rest encryption |
FieldCipherPort / AesGcmFieldCipher (per tenant), SubjectFieldCipher (per subject) |
| Erasure / right to be forgotten (art. 17) |
ErasurePort / SubjectCryptoShredder (crypto-shredding) |
| Access log (art. 30) | PiiAccessLogSink |
| Log/display masking | redactProfile |
| Classification |
classify, isPii, PII_FIELDS
|
🇬🇧 English
- Home
- Getting started
- Architecture
- Authorization
- Authentication
- Identity federation
- Compliance & PII
- NestJS integration
- Developing an adapter
- Security
Recipes
- NestJS + native + Prisma
- better-auth
- SCIM / Entra
- LDAP / AD
- RBAC / ABAC
- PII / GDPR
- Combo: better-auth + PII
- Combo: SCIM/Entra + authz
- Combo: full stack
🇫🇷 Français
- Accueil
- Prise en main
- Architecture
- Autorisation
- Authentification
- Fédération d'identité
- Conformité & PII
- Intégration NestJS
- Développer un adapter
- Sécurité
Recettes