pleach

@pleach/compliance

Regulated-environment bundle — four shipping scrubbers (SSN-US, Luhn, US-DL, KeyedRegex) plus tenant-scoping and event log audit gates that hosts inherit by adopting the package.

Pruning shears at the garden wall — what crosses the boundary, in what shape, with what scrub. @pleach/compliance is the regulated-environment add-on to @pleach/core. The first release ships four Scrubber implementations that gate event log writes, plus two CI audit gates that hosts inherit at adoption time. The package is aimed at deployments where the audit ledger is evidence — healthcare, finance, govtech — and where every harness_event_log row needs both a tenant scope and a redaction guarantee before it lands on disk.

Beta. While @pleach/core ships stable, @pleach/compliance ships beta for the first release. The scrubbers, tenant-scoping, and CI audit gates below are production-solid and battery-proven; the attestation / audit-report surface is the beta part — envelopes are unsigned by default unless a real key-store is wired, and the version maps are Phase-A-empty (see Attestation). A stable @pleach/compliance attestation claim lands at the 1.0 cut.

The canonical use case is alongside @pleach/core's EventLogWriter: scrubbers run on the persistence boundary, the CI gates enforce that no code path writes an untenanted row, and the schema check guarantees tenant_id NOT NULL after migration.

Install

npm install @pleach/compliance
pnpm add @pleach/compliance
yarn add @pleach/compliance
bun add @pleach/compliance
import {
  SsnUsScrubber,
  CreditCardScrubber,
  UsDriverLicenseScrubber,
  KeyedRegexScrubber,
} from "@pleach/compliance";

The package exports the four scrubber classes (instantiate with new), the complianceProfilePlugin(profile) factory that bundles them by named profile ("hipaa" / "gdpr" / "pci-dss" / "soc2"), the createComplianceRuntime wrapper for transparent interception, and the lower-level scrubbersForProfile(profile) helper for hand-rolled plugins. The published README is the source of truth for export names and constructor options — check it before pinning a version.

The four bundled scrubbers

Each implements the Scrubber contract from @pleach/core/scrubbers. The contract — scrub(input, ctx) => ScrubResult returning redacted and matched spans for audit reporting — is documented in Scrubbers; this section covers what ships in the bundle.

ScrubberPatternRedaction key
SsnUsScrubberUS Social Security numbers (NNN-NN-NNNN; opt-in unseparated NNNNNNNNN) with SSA invalid-range guards[REDACTED:SSN]
CreditCardScrubberCredit-card-shaped digit runs validated by Luhn check digit AND IIN range gate[REDACTED:CARD]
UsDriverLicenseScrubberUS driver's license numbers — per-state format table, anchored or per-state mode[REDACTED:DL]
KeyedRegexScrubberGeneric pattern + key adapter — the building block hosts extendcaller-supplied

SsnUsScrubber matches the canonical hyphenated form and (when opted in) the unhyphenated nine-digit form, with SSA invalid-range guards (area ≠ 000/666/900–999, group ≠ 00, serial ≠ 0000). The redacted form replaces the matched span with [REDACTED:SSN]; the original digits never reach storage.

CreditCardScrubber validates against the Luhn check digit AND the IIN range before redacting. The double gate cuts the false-positive rate that a naive 16-digit regex would produce against arbitrary numeric content — order numbers, session ids, and similar digit runs pass through untouched. Opt out of the IIN gate via new CreditCardScrubber({ validateIin: false }) if you process exotic card-network ranges.

UsDriverLicenseScrubber composes a state-keyed pattern table because the driver's license format varies per state. The bundle ships the table; consumers that need a state the upstream table doesn't cover yet should open an issue on the package repo rather than fork the scrubber.

KeyedRegexScrubber is the parameterized scrubber — the other three are specializations that bind a fixed pattern and redaction key. The next section covers it as the extension hook.

KeyedRegexScrubber — the extension hook

KeyedRegexScrubber is the one scrubber consumers commonly instantiate directly. It's not a custom scrubber — it's a parameterized one — so it stays inside the contract and inherits the matched-spans audit reporting that ships with the bundle.

// lib/compliance/tenantMrnPlugin.ts
import { KeyedRegexScrubber } from "@pleach/compliance";
import type { HarnessPlugin } from "@pleach/core";

const mrnScrubber = new KeyedRegexScrubber({
  id: "tenant-mrn",
  entries: [
    {
      name:        "mrn",
      pattern:     /\b\d{7}\b/g,
      replacement: "[REDACTED:MRN]",
    },
  ],
});

export const tenantCompliancePlugin: HarnessPlugin = {
  name: "tenant-mrn-compliance",
  contributeScrubbers: () => [mrnScrubber],
};

The name (here "mrn") is what shows up in the matched-spans report and in any downstream audit consumer — pick a name that reads in a compliance review, not the regex itself. The replacement may be a string or a function (match, name) => string if the redacted form needs to be derived from match groups (e.g. preserving the last 4 chars).

For a host with multiple regulated identifier families — MRN, employee ID, internal account number — register them as separate entries on a single KeyedRegexScrubber, or use one scrubber per family if you want the audit report grouped per-scrubber-id. The per-entry name field is the attribution unit either way.

Inherited audit gates

Adopting @pleach/compliance adds two CI audit gates to the consumer build. The gates are the load-bearing piece for regulated deployments: they fail the build before a code path that violates tenant scoping can reach production.

audit:tenant-scoping

Enforces that every event log write threads tenant_id. The gate walks the consumer's source for call sites against EventLogWriter (and its registered wrappers) and fails if any call site can reach append without a tenant_id resolved from the runtime.

The check is structural — it flags the absence of the threading, not the value. A code path that resolves tenantId from a request and then drops it before calling the writer fails the gate at the drop site, with the drop site named in the failure output.

audit:harness-event-log-tenant-id-required

Schema-level check that harness_event_log.tenant_id is NOT NULL after migration. The gate reads the consumer's applied schema (via the standard migration introspection helper) and fails if the column is nullable.

Run it as part of the post-migration smoke step. A nullable tenant_id column is the silent-isolation failure mode: writes succeed, rollups miss rows, and the regulator's "show me tenant X's events" query under-counts.

Deployment pattern

Adopt the package once at the top of your SessionRuntime construction via the standard plugin registration. The scrubbers wire into EventLogWriter automatically; consumer code doesn't call them directly.

import { SessionRuntime } from "@pleach/core";
import { complianceProfilePlugin } from "@pleach/compliance";

const runtime = new SessionRuntime({
  storage,
  checkpointer,
  userId:   req.user.id,
  tenantId: req.user.orgId,
  plugins:  [complianceProfilePlugin("hipaa") /* , gatewayPlugin, ... */],
});

complianceProfilePlugin(profile) accepts "hipaa" | "gdpr" | "pci-dss" | "soc2" and returns a HarnessPlugin whose contributeScrubbers resolves to the scrubber bundle for that profile. Use scrubbersForProfile(profile) directly if you need to hand-roll the plugin shape (e.g. composing your own name + id fields). Custom scrubbers (additional KeyedRegexScrubber instances for tenant-specific identifiers) register through their own plugin alongside this one — composition is additive.

The CI gates are picked up by the consumer's existing audit harness; no per-build wiring beyond installing the package and listing it in the audit config.

What's NOT in this package today

Honest about the scope of the 1.0 cut:

  • No gateway-level auth or rate limiting. That lives in @pleach/gateway@0.1.0. Today's compliance package does not see the outbound provider call.
  • No SBOM or attestation bundle. @pleach/trust-pack remains reserved at 0.0.1 · UNLICENSED. Supply-chain attestation is out of scope for this cut. (Cryptographic attestation of individual audit rows ships in @pleach/core/attestation — see Attestation.)
  • Deterministic replay over the event log ships in @pleach/replay@0.1.0 (replayTurn, fromSnapshot, fork, and aggregateMultiTenant bodies all land at the 0.1.0 cut) and @pleach/eval@0.1.0 (scoring + diff). The compliance package writes the rows; the replay/eval packages read them.
  • Tamper-evidence hash chain stamping ships in @pleach/core/eventLog (chainStep, computeRowHash) behind the c9PhaseBEnabled flag; verification ships in the same @pleach/core/eventLog barrel (verifyChainForChat, generateProof). The TamperEvidence plug-point in @pleach/core/audit keeps its no-op default for hosts that route attestation elsewhere.
  • No broader scrubber set. Four scrubbers ship in 1.0. Additional patterns (international tax IDs, EU formats, sector-specific identifiers) are planned but not present. Use KeyedRegexScrubber for anything outside the bundled four.

Check the package README for the current roadmap before assuming a feature.

Adopting in stages

A host migrating an existing deployment toward mandatory tenant scoping may want the scrubbers immediately but defer the CI gates until the codebase is ready. The gates fail fast — a multi-week migration with the gates on means a broken build for the duration.

Register only the scrubbers via contributeScrubbers from a custom plugin, and skip the package's full plugin export:

// lib/compliance/scrubbersOnlyPlugin.ts
import {
  SsnUsScrubber,
  CreditCardScrubber,
  UsDriverLicenseScrubber,
} from "@pleach/compliance";
import type { HarnessPlugin } from "@pleach/core";

export const scrubbersOnlyPlugin: HarnessPlugin = {
  name: "compliance-scrubbers-only",
  contributeScrubbers: () => [
    new SsnUsScrubber(),
    new CreditCardScrubber(),
    new UsDriverLicenseScrubber(),
  ],
};

The scrubbers run; the CI gates stay off. Track the migration as a real ticket — running the scrubbers without the tenant-scoping guarantee means redacted rows can still land untenanted, which is a partial-isolation posture, not a finished one. Flip to the full complianceProfilePlugin("hipaa") (or the profile matching your posture) once the codebase passes the gates locally.

  • @pleach/core — the substrate this package wraps. Scrubbers run at EventLogWriter's persistence boundary; the CI gates check call sites against @pleach/core/eventLog.
  • @pleach/gateway — emits domain.gateway.cost.recorded events with the full RoutingDecision shape. Compliance's attestation runtime reads those events without round-tripping the audit ledger.
  • @pleach/replayverifyChainIntegrity walks the hash-chained event log. Compliance writes the rows; replay attests them.
  • @pleach/eval — scoring + diff over replayed, scrubbed event logs.

For the full SKU map see Which SKU do I need?.

Where to go next

On this page