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

stable0.8.41hipaapci-dsssox-404dorasoc2
{
  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.