Dual Control
M-of-N approval workflow for destructive operations (eraseHard, key rotation, etc.). b.breakGlass gates a single-actor step-up (TOTP / passkey proof from the SAME actor performing the unseal); dual-control raises the bar to "two distinct named actors must approve before the operation runs" — the standard control for HIPAA admin actions, PCI key rotation, financial close, T+1 settlement, and similar compliance-sensitive flows.
Every state transition emits an audit row: dual.grant.requested / dual.grant.approved / dual.grant.denied / dual.grant.consumed / dual.grant.expired / dual.grant.cancelled. Each event carries the grant ID and the actor 5 W's so a compliance reviewer can reconstruct the chain.
Grants live in a b.cache instance the operator wires in (memory backend → per-process; cluster backend → shared across nodes). The cache TTL bounds grant freshness automatically. Optional cooling-off lock (consumeLockMs) defends against rapid-burst compromise of requester+approver. Optional approverRoles restricts approval to actors carrying a named role.
b.dualControl.create(opts) #
{
namespace: string, // grant key namespace (required, non-empty)
cache: b.cache, // b.cache instance — memory or cluster (required)
audit: b.audit, // optional audit sink (defaults to global b.audit)
ttlMs: number, // grant lifetime (default 15 minutes)
minApprovers: number, // approvals required to reach quorum (default 2, ≥2)
forbidSelfApprove: boolean, // requester cannot approve own grant (default true)
consumeLockMs: number, // cooling-off ms after final approval (default 0)
minReasonLength: number, // minimum chars on reason fields (default 0 → off)
approverRoles: Array, // restrict approve() to actors with one of these roles (default null)
notify: function, // (event) → void; fired on every transition
}
Build a dual-control approval workflow bound to a namespace. Returns a handle exposing async request(args) (open a pending grant), approve(args) (a different actor approves; quorum detection is automatic), cancel(args) (the requester withdraws), revoke(args) (an admin denies), consume(grantId, args) (the destructive code path single-uses the grant), and status(grantId) for inspection. Every transition audits with the actor 5 W's. Grant lifecycle is bounded by ttlMs; cluster-shared grants need a cluster-backed cache.
var approvals = b.dualControl.create({
namespace: "users.eraseHard",
cache: b.cache.create({ namespace: "dc", backend: "memory" }),
audit: b.audit,
ttlMs: b.constants.TIME.minutes(15),
minApprovers: 2,
consumeLockMs: b.constants.TIME.minutes(2),
minReasonLength: 12,
approverRoles: ["security:officer", "compliance:officer"],
});
// Step 1: requester opens the grant.
var opened = await approvals.request({
action: "users.eraseHard",
resource: { kind: "user", id: "user-42" },
requestedBy: { id: "alice", roles: ["engineer"] },
reason: "GDPR Art. 17 erasure request, ticket SUP-4421",
req: req,
});
// → { grantId: "dc-", status: "pending", needs: 2, expiresAt: ... }
// Step 2: a different actor approves.
var approved = await approvals.approve({
grantId: opened.grantId,
approver: { id: "bob", roles: ["security:officer"] },
reason: "verified ticket SUP-4421 against subject identity",
req: req,
});
// → { grantId, status: "approved", approvedBy: ["alice"... wait, no: ["bob"], ... }
// Step 3: the destructive code path consumes the grant once.
var grant = await approvals.consume(opened.grantId, { req: req });
if (!grant.ready) throw new Error("dual-control: " + grant.reason);
await b.db.eraseHard("users", { id: "user-42" });
Last updated 2026-08-08T16:39:49.652Z by seeder.