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/platformdotnet add package FabricOrg.Platform.Abstractions
dotnet add package FabricOrg.Platform.PrivacyCore 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"); // nullusing 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"); // nullRecursive 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 payloadvar result = await engine.AnonymizeAssetEventPayloadAsync(@event, context);
// result.Value has "_privacy" metadata injected alongside the redacted payloadEvidence packet
const result = engine.anonymizeEvidencePacket(packet, context);
// result.value.checksum is recomputed over the redacted packet + metadatavar result = await engine.AnonymizeEvidencePacketAsync(packet, context);
// result.Value.Checksum is recomputed over the redacted packet + metadataLifecycle 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:
| Key | Method |
|---|---|
password, token, apiKey, authorization | drop |
ssn, ssnLast4, socialSecurityNumber, dateOfBirth, dob, driverLicense | nullify |
firstName, lastName, fullName, name, email, emailAddress | tokenize |
phone, phoneNumber, mobilePhone, homePhone, workPhone | tokenize |
street, streetAddress, address, mailingAddress | nullify |
partnerLeadId, externalLeadId, loanApplicationId, vin, rawVin | tokenize |
Best practices
- Use
tokenizefor direct identifiers. It produces a deterministic, tenant-scoped surrogate that lets you correlate redacted records without exposing raw PII. - Use
nullifyfor highly sensitive fields (SSN, DOB, addresses) where even a token is too much. - Use
dropfor secrets (passwords, API keys) so the field is removed entirely rather than replaced withnull. - Preserve
_privacykeys. The engine ignores_privacyso you can layer multiple redaction passes without losing audit metadata. - Check
metadata.redactedFields. Every redaction is recorded withfieldPath,method,beforeChecksum, andpolicyVersion. - 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
Vehicleread model), callFieldRedactor.tokenizedirectly before or after the generic pass.