Aller au contenu
Krizaka TypeScript
Krizaka TypeScript

@krizaka/privacy

Loi 25 : des consentements par finalité écrits une fois, l’export et l’effacement assemblés depuis chaque module, une conservation réglée par l’admin.

Installer

bashTerminal
npm install @krizaka/privacy

Aussi publié en sous-chemins : @krizaka/privacy/settings, @krizaka/privacy/testkit.

Le README ci-dessous est celui du paquet sur npm, en anglais.

Privacy for every Krizaka product (Loi 25): consents by purpose, the person's data exported from every module that holds it, erased from every module in one transaction, and retention per purpose set by the admin. Six products had a partial copy (Yomizaka's consents, export and delete; Akinaza's recordConsent and eraseClient; the asynchronous export and the admin exports the products carried; the consent columns). This is the one version.

import { transaction } from "@krizaka/db";
import { auditContributor, createPrivacy, privacySchema } from "@krizaka/privacy";
import { privacySettings, retentionFromSettings } from "@krizaka/privacy/settings";

emitMigration(privacySchema, 1, { tables: {} }); // commit the SQL once, in your migrations: privacy.consent and privacy.request

const privacy = createPrivacy({
  clock: { now: () => new Date() },
  purposes: ["marketing", "screening"],                 // the purposes the product declares
  contributors: [auditContributor(audit), ordersContributor, filesContributor], // every module that holds personal data
  transaction: (work) => transaction(connect, work),
  retentionDays: retentionFromSettings((key) => settings.get(key)), // privacy.retention.<purpose>Days
  audit, outbox,                                         // optional: the erasure is recorded; a large export goes through the outbox
});

const eric = { kind: "account", id: account.id };         // a subject is a kind and an id: an account, a client file, an application
await privacy.recordConsent(query, { subject: eric, purpose: "marketing", version: "2026-10-01", granted: true, means: "checkbox" });
await privacy.hasConsent(query, eric, "marketing");       // the consent with the last word; never having said anything is a no
const document = await privacy.exportSubject(query, eric); // { format: 1, subject, generatedAt, modules: { audit, orders, files } }
await privacy.eraseSubject(eric);                         // every contributor, one transaction, consents withdrawn, a record without personal data
await privacy.purge(query);                               // from the product's job: what is past each purpose's retention
  • A subject is a kind and an id, not necessarily an account: Akinaza's data subject is a client file, Yomizaka's a tenant on an application.
  • privacy.consent is written once: a trigger refuses every update; a withdrawal is a new row; the row with the last seq wins. Each consent holds the version of the notice shown, the means and the instant: the proof of what the person agreed to.
  • PrivacyContributor is the port every module implements: exportFor(query, subject), eraseFor(tx, subject) (erase or anonymise through the transaction it is given: a module that keeps an invoice for six years blanks the name and keeps the row), and optionally purgeExpired(tx, { purpose, before }). The product registers one for its own tables. auditContributor(audit) is provided for @krizaka/audit (the export lists the actor's entries; the erasure detaches them and keeps the facts). A side effect outside the database (a file in storage) is not done in eraseFor: append it to the outbox in the same transaction.
  • Export assembles one JSON document with a part per contributor; a contributor that fails fails the export, naming it (a silent omission would be a lie). A large export is asked in the caller's transaction with requestExport(tx, subject) (it appends evt.privacy.export_requested.v1 to the outbox, holding the request id only) and a worker calls fulfilExport(query, requestId, deliver): the asynchronous mode.
  • Erasure runs every contributor's eraseFor in ONE transaction, withdraws the live consents (a new row each, version erasure or the one you pass), records the request and an audit entry privacy.erasure_completed that names the request and the modules and no one. If a contributor fails, nothing is erased, the request is recorded as failed with a code, and the error is thrown (if recording the failure fails too, an AggregateError with the original first).
  • Retention: privacySettings({ screening: { retentionDays: 365 } }) (@krizaka/privacy/settings, needs @krizaka/settings and zod) declares privacy.retention.screeningDays (payment-history → paymentHistoryDays). purge calls every contributor's purgeExpired for each purpose that has a retention.
  • The request table holds an id of the subject and no other personal data, so it can outlive the erasure it proves. privacyStoreContract (from @krizaka/privacy/testkit) is the contract every store runs.

Confidentiality incident register

The register that the Act respecting the protection of personal information in the private sector (CQLR c. P-39.1, ss. 3.5 to 3.8) asks every enterprise to keep, with the content and retention set by the Regulation respecting confidentiality incidents. In short, as we read it (not legal advice): on reason to believe an incident occurred, reduce the risk and prevent a new one; assess the risk of serious injury from the sensitivity of the information, the anticipated consequences and the likelihood of misuse; if the risk is serious, notify the Commission d'accès à l'information and the people concerned promptly; and keep a register of every incident, serious or not, for at least five years after becoming aware of it, handed to the Commission on request.

import { createIncidentRegister, registerToCsv, suggestSeriousInjury } from "@krizaka/privacy";

emitMigration(privacySchema, 2, { tables: {} });            // privacy.incident (after version 1)
const incidents = createIncidentRegister({ clock: { now: () => new Date() }, audit });
const admin = { id: account.id, label: account.name };

const { id } = await incidents.open(query, { scope: tenantId, kind: "access", description: "…", information: ["lease"], personsConcerned: 3 }, admin);
await incidents.recordMeasure(query, tenantId, id, "Session revoked", admin);
await incidents.assess(query, tenantId, id, { sensitivity: "high", consequences: "high", likelihood: "medium", seriousInjury: suggestSeriousInjury(factors), rationale: "…" }, admin);
await incidents.recordNotice(query, tenantId, id, "cai", { channel: "form" }, admin);     // and "persons"
await incidents.close(query, tenantId, id, admin);       // a serious injury closes once both notices are recorded
registerToCsv((await incidents.exportRegister(query, tenantId)).entries);
  • Every call is by scope (the tenant): an entry of another scope is never found, edited or exported. The store is the IncidentStore port (memoryIncidentStore, dbIncidentStore; incidentStoreContract from @krizaka/privacy/testkit); the clock is injected.
  • The register holds no personal data of the people concerned: how many, and the categories of information, never the information.
  • Kept five years: retainUntil is five years after awareAt; the database refuses to delete an entry before it, and refuses to edit a closed one. A drill is marked (drill) and stays in the register.
  • suggestSeriousInjury is a guide for the person in charge; the decision (seriousInjury) is theirs and is recorded with its rationale. An incident without serious injury owes no notice (recordNotice refuses).
  • With audit, each step is traced as privacy.incident_* without the description or any person.

Exports

Les 58 noms que le paquet exporte, lus dans ses types.

NomNatureCe que c’est
AssessmentInputtype
auditContributorfonction@krizaka/audit as a contributor: an export lists the actor's entries; an erasure detaches the person (id and address removed, the label « erased ») and kee…
AuditPorttypeThe structural slice of @krizaka/audit this package writes through, so it adds no dependency.
AuditTrailtypeThe structural slice of @krizaka/audit that contributes: what the trail holds about an actor, and detaching them from it.
checkPurposefonction
checkSubjectfonction
Clocktype
ConsentInputtypeWhat a product records when a person agrees, or withdraws.
ConsentRecordtypeA consent as written.
createIncidentRegisterfonctionThe confidentiality incident register of a product (Loi 25, ss.
createPrivacyfonctionPrivacy for a product (Loi 25).
dbIncidentStorefonctionThe PostgreSQL incident store, over the Query it is given (the caller's transaction).
dbPrivacyStorefonctionThe PostgreSQL store of « privacy », over the Query it is given (the caller's transaction, to join its unit of work).
ExportDocumenttypeThe whole export of a subject: one document, a part per contributor.
IncidenttypeOne register entry: an incident, from the day the enterprise became aware of it.
INCIDENT_CODEconstante
INCIDENT_KINDconstanteWhat happened to the information (the Act speaks of unauthorised access, use, communication and loss).
INCIDENT_STATUSconstante
IncidentActortype
IncidentAssessmenttypeThe three factors of the risk assessment (s.
IncidentCodetype
IncidentDepstype
IncidentErrorclasse
IncidentInputtype
IncidentKindtype
IncidentMeasuretype
IncidentNoticetypeA notice sent, to the Commission or to the people concerned.
IncidentRegistertype
IncidentStatustype
IncidentStoretypeWhere the register lives.
Leveltype
LEVELconstante
memoryIncidentStorefonctionAn in-memory incident store, for unit tests and demos (the contract kit runs on it and on the database's).
memoryPrivacyStorefonctionAn in-memory store, for unit tests and demos (the same behaviour as the database's: the contract kit runs on both).
NOTICE_TARGETconstante
NoticeTargettype
OutboxPorttypeThe structural slice of @krizaka/outbox used for a large export.
Privacytype
PRIVACY_CODEconstante
PrivacyCodetype
PrivacyContributortypeA module that holds personal data.
PrivacyDepstype
PrivacyErrorclasse
PrivacyRequesttypeAn export or an erasure asked for, and where it stands.
privacySchemaconstanteThe tables of « privacy », in their own Postgres schema (ADR-001 §3).
PrivacyStoretypeWhere consents and requests live.
Querytype
REGISTER_CSV_COLUMNSconstante
RegisterDocumenttypeThe register as handed to the Commission on request: one document, the entries oldest first.
registerToCsvfonctionThe register as CSV (RFC 4180), cells that a spreadsheet would read as a formula are quoted with a leading apostrophe.
REQUEST_KINDconstante
REQUEST_STATUSconstante
RequestKindtype
RequestStatustype
retainUntilfonctionFive years after the enterprise became aware of the incident (the Regulation's minimum for keeping the register).
SubjecttypeWhose data it is.
suggestSeriousInjuryfonctionA guide for the person in charge, never the decision: the injury is serious when the information is sensitive and misuse is not unlikely, when the consequenc…
TransactiontypeThe product's unit of work: runs work on one connection, commits when it resolves, rolls back when it throws (transaction of @krizaka/db).

@krizaka/privacy/settings

NomNatureCe que c’est
privacySettingsfonctionThe registry fragment of the retention: privacy.retention.&lt;purpose>Days for each purpose the product declares, an integer with the product's default.
retentionFromSettingsfonctionThe retention of a purpose as createPrivacy takes it, from the product's accessor ((key) => service.get(key, ctx)).
retentionKeyfonctionThe settings key of a purpose's retention: payment-history → privacy.retention.paymentHistoryDays (a key holds letters and digits only).

@krizaka/privacy/testkit

NomNatureCe que c’est
incidentStoreContractfonctionWhat every IncidentStore adapter must do.
privacyStoreContractfonctionWhat every PrivacyStore adapter must do: the same behaviour behind the same port.

Paquet

Version sur npm0.2.2 (npm)
LicenceApache-2.0
Node.js>=22
Dépendances@krizaka/db workspace:^, @krizaka/settings workspace:^, @krizaka/time workspace:^
Dépendances à fournirvitest >=4, zod >=3.23
Code sourceprivacy

Journal des versions

0.2.2

Patch changes

  • The READMEs, comments and published declarations no longer name the products that left the organisation (krizaka/.github#413, rule no-departed-published). The sources were cleaned in #262 with an empty changeset, so the published tarballs still carried the old text; this release replaces them. No behaviour change.
  • Updated dependencies [2a7898c]
  • Updated dependencies [2272b57]

0.2.1

Patch changes

0.2.0

Minor changes

  • The confidentiality incident register (Loi 25, ss. 3.5 to 3.8): createIncidentRegister (open, assess the risk of serious injury, record measures and the notices to the Commission and the people concerned, close, list and export the register by scope), suggestSeriousInjury, registerToCsv, memoryIncidentStore / dbIncidentStore, incidentStoreContract (testkit) and migration 2 of privacySchema (privacy.incident: an entry is kept five years from the day the enterprise became aware, and a closed entry is never edited).

0.1.1

Patch changes

0.1.0

Minor changes

  • New package (#59): privacy (Loi 25). Consents by purpose written once (a trigger refuses updates) with the notice version shown; PrivacyContributor, the port every module implements; exportSubject (one document, a part per contributor) and requestExport / fulfilExport (large exports through the outbox); eraseSubject (every contributor in one transaction, consents withdrawn, a request and an audit entry that name no one); purge with a retention per purpose; auditContributor; privacySchema; @krizaka/privacy/settings (privacy.retention.<purpose>Days) and privacyStoreContract.

Patch changes

Sur cette page