Consent
Consent-record chain — every grant / withdrawal / expiry / supersede for a (subjectId, purpose) pair lands in consent_log as one append-only, hash-chained row. Same tamper-evidence design as audit_log: per-row SHA3-512 hash chain over the sealed payload, verified at boot, refuse-to-boot on a break.
GDPR Art. 7 demands controllers be able to demonstrate the data subject consented; CCPA / CPRA Title 1.81.5 requires evidence the consumer exercised opt-out. consent_log carries subjectId (sealed) + purpose + lawfulBasis + channel + operator-supplied evidenceRef so a regulator request resolves to a specific row, tied to the audit chain via shared chain-writer primitives.
Lawful-basis vocabulary tracks the GDPR Art. 6(1) enumeration: consent, contract, legal_obligation, vital_interests, public_task, legitimate_interests. Any audit event declaring lawfulBasis: 'consent' should reference a current consent_log entry — the framework records the grants and withdrawals here; enforcement at the trust boundary is the app's call (typical shape: if (!b.consent.isGranted({ subjectId, purpose })) return 403).
purpose is free-form, but values matching the recognized-purpose vocabulary (b.consent.recognizedPurpose / listPurposes) carry lawful-basis constraints grant() enforces — e.g. educational-only (FERPA school-official exception / California SOPIPA) refuses a legitimate_interests basis.
Cluster mode keeps _blamejs_consent_tip current with a fenced INSERT … ON CONFLICT DO UPDATE … WHERE fencingToken <= EXCLUDED so a partitioned old leader cannot rewrite the tip even if its application-layer leader gate let the call through. Followers refuse grant / withdraw with NotLeaderError.
b.consent.grant(opts) #
{
subjectId: string, // sealed at rest
purpose: string, // e.g. "marketing"
lawfulBasis: "consent" | "contract" | "legal_obligation"
| "vital_interests" | "public_task"
| "legitimate_interests",
scope: object, // optional, JSON-serialized
channel: string, // "web-banner" / "api" / ...
evidenceRef: string, // optional pointer to UI snapshot
}
Append a "granted" row to consent_log for a (subjectId, purpose) pair. Refuses on a follower (cluster mode). Lawful-basis must come from the GDPR Art. 6(1) enumeration; an unknown value throws synchronously before touching the chain.
await b.consent.grant({
subjectId: "u-42",
purpose: "marketing",
lawfulBasis: "consent",
scope: { channels: ["email", "sms"] },
channel: "web-banner-v3",
evidenceRef: "snapshot-2026-05-09T14:00:00Z",
});
// → { _id, monotonicCounter, rowHash, prevHash, ... }
b.consent.recognizedPurpose(name) #
{
name: string, // a purpose value, e.g. "educational-only"
}
Look up a recognized consent purpose by value. Recognized purposes carry lawful-basis constraints that grant() enforces; the educational-only purpose (FERPA school-official exception / SOPIPA) forbids a legitimate_interests basis and marks the data commercial-use-prohibited. That commercial-use prohibition is an operator trust-boundary obligation — isGranted() does not re-derive it. Returns the frozen entry, or null for a free-form purpose (which remains valid for grant()).
b.consent.recognizedPurpose("educational-only");
// → { purpose: "educational-only", forbidsLawfulBasis: ["legitimate_interests"], ... }
b.consent.recognizedPurpose("marketing"); // → null (free-form)
b.consent.listPurposes() #
Return the recognized-purpose values as a frozen array. Free-form purposes are not listed — they remain valid for grant() but carry no lawful-basis constraint.
b.consent.listPurposes(); // → ["educational-only"]
b.consent.withdraw(opts) #
{
subjectId: string,
purpose: string,
reason: string, // optional, recorded as evidenceRef
channel: string, // optional, defaults to "api"
}
Append a "withdrawn" row to consent_log. After this lands, isGranted returns false for the same (subjectId, purpose). Pair with a downstream sweep over data-classes that depended on the lawful basis (typical pattern: cascade into b.retention or b.subject.erase).
await b.consent.withdraw({
subjectId: "u-42",
purpose: "marketing",
reason: "user-self-service-portal",
});
// → { _id, monotonicCounter, rowHash, ... }
b.consent.isGranted(opts) #
{
subjectId: string,
purpose: string,
}
Returns true when the most recent consent_log row for the (subjectId, purpose) pair has action granted. Lookups go through the derived subjectIdHash so the sealed subjectId column never needs to be unsealed for the query. Safe on followers (read-only).
if (!b.consent.isGranted({ subjectId: "u-42", purpose: "marketing" })) {
res.statusCode = 403;
return res.end("consent required");
}
// → true / false
b.consent.history(subjectId) #
Returns every consent_log row for subjectId, oldest first, decrypted by the framework's row reader. Composes into b.subject.export for GDPR Art. 15 / CCPA §1798.110 right-of-access responses without the caller having to walk the chain manually.
var rows = b.consent.history("u-42");
rows.forEach(function (r) {
console.log(r.recordedAt, r.purpose, r.action, r.lawfulBasis);
});
// → [{ recordedAt, purpose, action, lawfulBasis, channel, ... }]
b.consent.verify(opts) #
{
from: number, // optional monotonicCounter floor
to: number, // optional monotonicCounter ceiling
}
Verify the consent_log hash chain end-to-end. Recomputes each row's rowHash from the sealed-form columns + nonce + prevHash, walking from genesis to tip. Returns { ok, rowsVerified, breakAt? } — a regulator-ready integrity check that auditors can run without holding the vault key.
var report = await b.consent.verify();
if (!report.ok) {
console.error("consent chain break at row", report.breakAt);
}
// → { ok: true, rowsVerified: 1024 }
Last updated 2026-08-08T16:39:49.652Z by seeder.