FabricFabricPlatform
Platform referenceReference

Privacy engine

Field-level redaction and recursive JSON anonymization for asset events, evidence packets, and read models.

The @fabricorg/platform/privacy module provides a generic, vertical-agnostic engine for redacting sensitive fields from JSON-like values. It recognizes fields by key name (e.g. ssn, email, firstName) and applies a configurable redaction method.

Install

pnpm add @fabricorg/platform
dotnet add package FabricOrg.Platform.Abstractions
dotnet add package FabricOrg.Platform.Privacy

Core types

export type RedactionMethod = "nullify" | "mask" | "hash" | "tokenize" | "generalize" | "drop";

export type PlatformDataClass =
  | "asset_event"
  | "read_model"
  | "raw_payload"
  | "evidence_packet"
  | "action_evidence"
  | "access_event";

export interface AnonymizationContext {
  tenantId: string;
  spaceId?: string | null;
  jobId: string;
  reason: string;
  redactedAt?: Date;
  policyVersion?: string;
}

export interface AnonymizationResult<TValue> {
  value: TValue;
  metadata: AnonymizationMetadata;
}
public enum RedactionMethod { Nullify, Mask, Hash, Tokenize, Generalize, Drop }

public enum PlatformDataClass
{
    AssetEvent, ReadModel, RawPayload,
    EvidencePacket, ActionEvidence, AccessEvent
}

public sealed class AnonymizationContext
{
    public string TenantId { get; }
    public string? SpaceId { get; }
    public string JobId { get; }
    public string Reason { get; }
    public DateTimeOffset? RedactedAt { get; }
    public string? PolicyVersion { get; }
}

public sealed class AnonymizationResult<T>
{
    public T Value { get; }
    public AnonymizationMetadata Metadata { get; }
}

Field redaction

FieldRedactor applies a single redaction method to a single value. AnonymizationEngine uses it internally, but you can also call it directly for vertical-specific fields.

import { FieldRedactor } from "@fabricorg/platform/privacy";

const redactor = new FieldRedactor();

redactor.mask("1234567890");        // "******7890"
redactor.hash("secret");             // "sha256_2bb80d5..."
redactor.tokenize("jane@example.com", context, "$.email");
// "tok_a3f7d2e8b1c9..."  (deterministic per tenant+space+path)
redactor.generalize(92500);          // 93000
redactor.generalize("90210");        // "902**"
redactor.redactField("x", "drop", context, "$.password");     // undefined
redactor.redactField("x", "nullify", context, "$.ssn");      // null
using FabricOrg.Platform.Privacy;

var redactor = new FieldRedactor();

redactor.Mask("1234567890");         // "******7890"
redactor.Hash("secret");             // "sha256_2bb80d5..."
redactor.Tokenize("jane@example.com", context, "$.email");
// "tok_a3f7d2e8b1c9..."  (deterministic per tenant+space+path)
redactor.Generalize(92500);          // 93000.0
redactor.Generalize("90210");        // "902**"
redactor.RedactField("x", RedactionMethod.Drop, context, "$.password");    // null
redactor.RedactField("x", RedactionMethod.Nullify, context, "$.ssn");      // null

Recursive JSON anonymization

The engine walks nested objects and arrays, matching field keys against built-in policies. Matched fields are redacted; everything else is preserved. The _privacy key is never touched.

import { AnonymizationEngine } from "@fabricorg/platform/privacy";

const engine = new AnonymizationEngine();

const result = engine.anonymizeJsonValue(
  {
    firstName: "Jane",
    lastName: "Doe",
    email: "jane@example.com",
    ssnLast4: "1234",
    creditTier: "Prime",
  },
  context,
  "asset_event",
);

// result.value:
// {
//   firstName: "tok_a3f7...",
//   lastName:  "tok_8e2c...",
//   email:     "tok_9d1b...",
//   ssnLast4:  null,
//   creditTier: "Prime",
// }
using FabricOrg.Platform.Privacy;

var engine = new AnonymizationEngine(new FieldRedactor());

var result = await engine.AnonymizeJsonValueAsync(
    new Dictionary<string, object?>
    {
        ["firstName"] = "Jane",
        ["lastName"] = "Doe",
        ["email"] = "jane@example.com",
        ["ssnLast4"] = "1234",
        ["creditTier"] = "Prime",
    },
    context,
    PlatformDataClass.AssetEvent);

// result.Value:
// {
//   firstName: "tok_a3f7...",
//   lastName:  "tok_8e2c...",
//   email:     "tok_9d1b...",
//   ssnLast4:  null,
//   creditTier: "Prime",
// }

Anonymizing at a specific path

When you already know where PII lives inside a larger record, use the path-scoped variant:

// Anonymize only the nested `properties` object
const propsResult = engine.anonymizeJsonValueAtPath(
  record.properties,
  context,
  "read_model",
  "$.properties",
);
record.properties = propsResult.value;
// Anonymize only the nested Properties object
var propsResult = await engine.AnonymizeJsonValueAtPathAsync(
    record.Properties,
    context,
    PlatformDataClass.ReadModel,
    "$.properties");
record.Properties = propsResult.Value;

Asset event payload

const result = engine.anonymizeAssetEventPayload(event, context);
// result.value has `_privacy` metadata injected alongside the redacted payload
var result = await engine.AnonymizeAssetEventPayloadAsync(@event, context);
// result.Value has "_privacy" metadata injected alongside the redacted payload

Evidence packet

const result = engine.anonymizeEvidencePacket(packet, context);
// result.value.checksum is recomputed over the redacted packet + metadata
var result = await engine.AnonymizeEvidencePacketAsync(packet, context);
// result.Value.Checksum is recomputed over the redacted packet + metadata

Lifecycle evidence

Aggregate metadata from multiple anonymized records into a single audit evidence document:

import { buildAnonymizationLifecycleEvidence } from "@fabricorg/platform/privacy";

const evidence = buildAnonymizationLifecycleEvidence({
  context,
  assetEvents: [{ event, metadata: assetResult.metadata }],
  readModels: [
    { modelKind: "Account", record: { id: "rm-1", ... }, metadata: readModelMeta },
  ],
  evidencePackets: [{ packet, metadata: packetMeta }],
});
var evidence = await engine.BuildAnonymizationLifecycleEvidenceAsync(
    new BuildAnonymizationLifecycleEvidenceInput(
        context,
        assetEvents: new[]
        {
            new AssetEventAnonymizationInput(@event, assetMeta)
        },
        readModels: new[]
        {
            new ReadModelAnonymizationInput(
                "Account",
                new Dictionary<string, object?> { ["id"] = "rm-1" },
                readModelMeta)
        },
        evidencePackets: new[]
        {
            new EvidencePacketAnonymizationInput(packet, packetMeta)
        }));

Built-in field policies

The engine ships with 20+ built-in policies keyed by normalized field name:

KeyMethod
password, token, apiKey, authorizationdrop
ssn, ssnLast4, socialSecurityNumber, dateOfBirth, dob, driverLicensenullify
firstName, lastName, fullName, name, email, emailAddresstokenize
phone, phoneNumber, mobilePhone, homePhone, workPhonetokenize
street, streetAddress, address, mailingAddressnullify
partnerLeadId, externalLeadId, loanApplicationId, vin, rawVintokenize

Best practices

  • Use tokenize for direct identifiers. It produces a deterministic, tenant-scoped surrogate that lets you correlate redacted records without exposing raw PII.
  • Use nullify for highly sensitive fields (SSN, DOB, addresses) where even a token is too much.
  • Use drop for secrets (passwords, API keys) so the field is removed entirely rather than replaced with null.
  • Preserve _privacy keys. The engine ignores _privacy so you can layer multiple redaction passes without losing audit metadata.
  • Check metadata.redactedFields. Every redaction is recorded with fieldPath, method, beforeChecksum, and policyVersion.
  • Vertical-specific fields belong in the vertical. The engine handles generic JSON. If your vertical has model-specific fields (e.g. a vehicle VIN on a Vehicle read model), call FieldRedactor.tokenize directly before or after the generic pass.

On this page