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) #

stable0.8.0gdprccpa
{
  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() #

stable0.8.0

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) #

stable0.8.0gdprccpa
{
  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) #

stable0.14.26gdprccpa
{
  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) #

0.8.77

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() #

0.8.77

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.