Dsr
Data Subject Rights workflow (GDPR Art 15-22, CCPA opt-out / right-to-know / right-to-delete) — ticket lifecycle, deadline tracking, source-by-source export.
Coordinates the operator's response to GDPR Articles 15-22 / CCPA / CPRA / LGPD / PIPEDA / UK-GDPR data-subject requests. The framework owns the ticket state machine, deadline computation, audit emission, and source orchestration. The operator owns the storage backend (declares a ticketStore that satisfies the { insert, get, list, update } shape) and the per-source query / erase callbacks.
Ticket states: pending -> in_progress -> (completed | partially_completed | cancelled | rejected | expired).
Posture-aware deadlines: gdpr/uk-gdpr/pipeda-ca = 30 days, ccpa = 45 days, lgpd-br/pipl-cn = 15 days. Operators override per-ticket via submit({ deadlineMs }).
Verification ladder (GDPR Art 12(6) / CCPA §1798.140(y)): minimal / secondary / strong. Erasure + portability + rectification default to secondary; the framework refuses process() when the actual level is below the per-type floor.
b.dsr.create(opts) #
{
ticketStore: { insert, get, list, update },
posture: string, // "gdpr" | "ccpa" | "lgpd-br" | ...
identityResolver: async function (input) -> resolvedSubject,
sources: [{ name, query?, erase?, eraseExclusions? }],
audit: boolean, // default true
retentionFloorMs: number, // export TTL; default 30 days
deadlineMs: number, // overrides posture default
verificationLevel: "minimal" | "secondary" | "strong",
minVerificationByType: { erasure: "secondary", ... },
receiptSigner: async function (receipt) -> { issuer, algorithm, signature },
}
Build a Data Subject Rights workflow handle. Wires the ticket store, identity resolver, and per-source query/erase callbacks into one coordinator that exposes submit, process, cancel, reject, expireOverdue, buildReceipt, and buildPortabilityBundle. Posture (gdpr, ccpa, lgpd-br, uk-gdpr, pipeda-ca, etc.) sets the default deadline; the framework refuses process() when the actual verification level is below the per-type floor.
var dsr = b.dsr.create({
ticketStore: b.dsr.memoryTicketStore(),
posture: "gdpr",
identityResolver: async function (input) {
return { subjectId: "u-42", email: input.email, phone: null };
},
sources: [{
name: "users",
query: async function (subj) { return [{ email: subj.email }]; },
erase: async function (subj) { return { deletedIds: [subj.subjectId] }; },
}],
});
var ticket = await dsr.submit({
type: "access",
subject: { email: "alice@example.com" },
reason: "user-initiated",
});
var processed = await dsr.process(ticket.id, {
actor: "compliance@example.com",
verificationLevel: "secondary",
});
processed.status;
// → "completed"
b.dsr.memoryTicketStore() #
In-memory ticket store — operator dev / test scaffold. Production operators wire b.dsr.dbTicketStore (or their own b.externalDb- backed store). The shape is the contract: { insert(ticket), get(id), list(filter), update(id, ticket) }. The returned store also exposes _size() for tests.
var store = b.dsr.memoryTicketStore();
await store.insert({ id: "DSR-1", status: "pending", subject: {} });
var t = await store.get("DSR-1");
t.status;
// → "pending"
var pending = await store.list({ status: "pending" });
pending.length;
// → 1
b.dsr.dbTicketStore(opts) #
{
db: b.db-shaped handle (`{ runSql, prepare }`),
table: string, // SQL identifier; defaults to "dsr_tickets"
}
Production-grade ticket store backed by b.db. Auto-provisions the table on first use, indexes on subject_email and status, persists the full ticket as a JSON payload column, and exposes purgeExpired(asOfMs?) for retention-floor enforcement.
var store = b.dsr.dbTicketStore({ db: b.db.handle(), table: "dsr_tickets" });
await store.insert({
id: "DSR-1234567-DEADBEEF",
type: "erasure",
status: "pending",
subject: { subjectId: "u-42", email: "alice@example.com" },
submittedAt: Date.now(),
deadlineAt: Date.now() + 30 * 86400 * 1000,
retentionUntil: Date.now() + 30 * 86400 * 1000,
});
var purged = await store.purgeExpired();
typeof purged;
// → "number"
b.dsr.reseal(args) #
{
store: { listAll(): rows[], putResealed(row) }, // sync or async
oldRootJson: string, // b.vault.getKeysJson() of the retired keypair
newRootJson: string, // b.vault.getKeysJson() of the new keypair
}
Re-seals every AAD-bound DSR-ticket cell on an operator-supplied store from the OLD vault keypair to the NEW one, out of band. dbTicketStore seals the subject PII + payload as {aad:true} cells; the in-tree vault-key rotation pipeline only walks tables inside db.enc, so a DSR store that lives on the operator's own database is unreachable to it — after a keypair rotation its cells would otherwise be orphaned under the retired root (CWE-320). Composes the same AAD re-seal the rotation pipeline uses (b.vaultAad.resealRoot), rebuilding each cell's AAD from the registered schema (one source of truth). Only AAD-sealed cells are touched; vault-less / plaintext rows pass through.
await b.dsr.reseal({ store: dsrStore, oldRootJson: oldKeys, newRootJson: newKeys });
// → { table: "dsr_tickets", resealed: 7 }
b.dsr.stateRules(state) #
Returns per-state DSR rules: response window, extension period, cure period (statutory grace before enforcement attaches), profiling-opt-out availability, and minor-consent age threshold. state accepts either the posture name ("vcdpa") or the 2-letter state abbreviation ("VA"). Returns null when unknown.
var rules = b.dsr.stateRules("vcdpa");
// rules.responseDays → 45
// rules.cureDays → 30
// rules.profilingOptOut → true
b.dsr.listStateRules() #
Returns every state-rule entry as an array (useful for admin UI cure-period dashboards / operator-facing matrices).
var all = b.dsr.listStateRules();
// → [{ posture: "vcdpa", state: "VA", responseDays: 45, ... }, ...]
Last updated 2026-08-08T16:39:49.652Z by seeder.