Skip to main content
Version: v0.0.9

@cosyte/deid

Public entry point for @cosyte/deid: a healthcare de-identification engine.

@cosyte/deid is not a parser. It is a consumer-tier library: it applies a HIPAA-grounded de-identification policy (Safe Harbor by default) to a structurally-located model of a healthcare document and returns a transformed model plus a value-free manifest of what it acted on. It borrows the parser archetype's disciplines (typed diagnostics, immutable output, a policy/profile system) but inverts the parser's reflex: it fails closed, an unrecognized structure or an un-locatable identifier is blocked, never passed through as safe.

Honesty line (governs the whole library). Results are "Safe-Harbor-transformed per the configured policy", never "de-identified" and never "HIPAA-compliant". Safe Harbor is implemented mechanically; the §164.514(b)(2)(ii) actual-knowledge condition is the consumer's; Expert Determination (§164.514(b)(1)) is supported, never rendered or certified here.

The root entry ships the format-agnostic core: the policy engine, the five transforms, the 18-category Safe Harbor model, the fail-closed rule, and the value-free manifest: tested against a generic locus model. The per-format locus maps (HL7 v2, C-CDA, FHIR, X12, NCPDP, DICOM) live behind the matching subpath exports.

Classes​

DeidContext​

An opaque handle to the consumer's key material. Construct it with createDeidContext; it exposes no secret field and redacts itself through every stringify channel.

Example​

import { createDeidContext } from "@cosyte/deid";

const ctx = createDeidContext({ key: "secret" });
JSON.stringify({ ctx }); // => '{"ctx":"[DeidContext:redacted]"}'

Constructors​

Constructor​

new DeidContext(): DeidContext

Internal

Use createDeidContext.

Returns​

DeidContext

Methods​

forPatient()​

forPatient(patientId): DeidContext

Derive a sibling context scoped to a different patient, reusing the same key and seed. Used to de-identify a multi-patient corpus with one key while keeping each patient's date-shift offset distinct.

Parameters​
patientId​

string

The patient scope for the derived context.

Returns​

DeidContext

A new DeidContext sharing this one's key/seed, scoped to patientId.

Example​
import { createDeidContext } from "@cosyte/deid";

const base = createDeidContext({ key: "secret" });
const forPatient = base.forPatient("patient-2");
toJSON()​

toJSON(): string

Redacts through JSON.stringify.

Returns​

string

toString()​

toString(): string

Redacts through String(...) and template interpolation.

Returns​

string


DeidError​

The error thrown on a FATAL_CODES condition. Carries a stable code; its message is safe to log: it never contains PHI (no value, no key, no offset).

Example​

import { deidentify, DeidError, FATAL_CODES } from "@cosyte/deid";

try {
deidentify(null as never, {});
} catch (err) {
if (err instanceof DeidError && err.code === FATAL_CODES.EMPTY_INPUT) {
// handle empty input
}
}

Extends​

  • Error

Constructors​

Constructor​

new DeidError(code, message): DeidError

Parameters​
code​

FatalCode

The FatalCode classifying this fatal.

message​

string

A PHI-free explanation safe to log.

Returns​

DeidError

Overrides​

Error.constructor

Properties​

code​

readonly code: FatalCode

The stable fatal code.


DeidRegistry​

The corpus-level consistency handle. Construct it with createDeidRegistry. It holds the consumer's key in a module-private registry: the handle exposes no secret field and redacts itself through every stringify channel.

Example​

import { createDeidRegistry } from "@cosyte/deid";

const registry = createDeidRegistry({ key: "secret" });
// Same patient across documents shifts by the same offset:
const a = registry.forPatient("patient-1");
const b = registry.forPatient("patient-1");
a === b; // => true (memoized, same handle, same offset)

Constructors​

Constructor​

new DeidRegistry(): DeidRegistry

Internal

Use createDeidRegistry.

Returns​

DeidRegistry

Methods​

forPatient()​

forPatient(patientKey): DeidContext

Return the DeidContext scoped to patientKey, minting and memoizing it on first use. The same key always yields the same context, hence the same deterministic date-shift offset, so a patient's dates shift consistently across every document in the corpus. Pass this context as DeidOptions.context to deidentify (or a per-format adapter) for that patient's documents.

Parameters​
patientKey​

string

A stable per-patient key (an MRN, an enterprise patient id, the consumer's choice, provided it is the same across that patient's documents).

Returns​

DeidContext

The patient-scoped, self-redacting context.

Example​
import { createDeidRegistry } from "@cosyte/deid";

const registry = createDeidRegistry({ key: "secret" });
const ctx = registry.forPatient("patient-1");
pseudonym()​

pseudonym(id): string

Map an identifier to its corpus-wide consistent pseudonym: a keyed-HMAC surrogate that is the same for the same input everywhere and not reversible without the key. Patient-independent: the same MRN maps to the same token regardless of which patient scope processes it, so records link.

Parameters​
id​

string

The identifier to pseudonymize (MRN, beneficiary number, account number).

Returns​

string

The consistent lowercase-hex surrogate.

Example​
import { createDeidRegistry } from "@cosyte/deid";

const registry = createDeidRegistry({ key: "secret" });
registry.pseudonym("MRN-1") === registry.pseudonym("MRN-1"); // => true (consistent)
remapUid()​

remapUid(uid): string

Map an opaque unique identifier (a DICOM study/series/instance UID, a GUID) to its corpus-wide consistent surrogate, so cross-document UID linkage survives de-identification. Domain-separated from pseudonym, so a UID and an identifier with the same text never share a surrogate. This is the format-agnostic linkage primitive; a format that owns UID validity (DICOM UIDs must be a valid 0.… OID) handles that in its own adapter.

Parameters​
uid​

string

The unique identifier to remap.

Returns​

string

The consistent lowercase-hex surrogate.

Example​
import { createDeidRegistry } from "@cosyte/deid";

const registry = createDeidRegistry({ key: "secret" });
registry.remapUid("1.2.840.113619.2.55") === registry.remapUid("1.2.840.113619.2.55"); // => true
toJSON()​

toJSON(): string

Redacts through JSON.stringify.

Returns​

string

toString()​

toString(): string

Redacts through String(...) and template interpolation.

Returns​

string

Interfaces​

CategoryCoverage​

Per-category coverage, for one of the 18 Safe Harbor categories (45 CFR §164.514(b)(2)(i)(A)–(R)), whether the pass acted on it and how. Present for all 18 categories in the report (in regulatory order A→R), so a reader sees the categories not acted on as plainly as those that were.

Example​

import { buildExpertDeterminationSupportReport, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid";

const report = buildExpertDeterminationSupportReport([
{ category: SAFE_HARBOR_CATEGORIES.SSN, transform: "redact", locus: "PID-19", count: 1,
disposition: "removed", code: "DEID_CATEGORY_REMOVED" },
]);
const ssn = report.categoryCoverage.find((c) => c.category === SAFE_HARBOR_CATEGORIES.SSN);
ssn?.actedOn; // => true

Properties​

actedOn​

readonly actedOn: boolean

true when at least one manifest entry acted on this category.

category​

readonly category: SafeHarborCategory

The Safe Harbor category.

codes​

readonly codes: readonly DeidDispositionCode[]

The distinct disposition codes recorded for this category, sorted.

dispositions​

readonly dispositions: Readonly<Record<ReportDisposition, number>>

Count of acted-on values by disposition.

letter​

readonly letter: string

The §164.514(b)(2)(i) sub-paragraph letter (A–R).

number​

readonly number: number

The category ordinal (1–18).

residualRetained​

readonly residualRetained: boolean

true when a coarse identifying residual was retained for this category.

title​

readonly title: string

A short human title (no PHI).

totalCount​

readonly totalCount: number

Total count of values acted on across every locus of this category.

transforms​

readonly transforms: readonly TransformName[]

The distinct transforms applied to this category, sorted.


DeidContextSpec​

Specification for createDeidContext.

Example​

import { createDeidContext } from "@cosyte/deid";

const ctx = createDeidContext({ key: "consumer-held-secret", patientId: "patient-1" });

Properties​

dateShiftSeed?​

readonly optional dateShiftSeed?: string | Uint8Array<ArrayBufferLike>

A separate seed for deriving per-patient date-shift offsets. Defaults to the key when omitted, so date-shifting works out of the box; supply a distinct seed to decouple the two secrets.

key​

readonly key: string | Uint8Array<ArrayBufferLike>

The HMAC key for keyed transforms. Consumer-held; never emitted. Must be non-empty.

maxShiftDays?​

readonly optional maxShiftDays?: number

Absolute bound (days) on the per-patient date-shift offset. Defaults to 365.

patientId?​

readonly optional patientId?: string

The patient scope this context de-identifies. Required for the deterministic date-shift offset.


DeidDocument​

The transformed document: the same loci with de-identified values. Format-specific documents replace this unknown-shaped placeholder; the core returns this generic shape.

Example​

import { type DeidDocument } from "@cosyte/deid";

const doc: DeidDocument = { loci: [] };

Properties​

loci​

readonly loci: readonly TransformedLocus[]

The transformed loci.


DeidManifestEntry​

A single value-free manifest entry.

Example​

import { type DeidManifestEntry, SAFE_HARBOR_CATEGORIES, DEID_DISPOSITION_CODES } from "@cosyte/deid";

const entry: DeidManifestEntry = {
category: SAFE_HARBOR_CATEGORIES.SSN,
transform: "redact",
locus: "PID-19",
count: 1,
disposition: "removed",
code: DEID_DISPOSITION_CODES.DEID_CATEGORY_REMOVED,
};

Properties​

category​

readonly category: SafeHarborCategory

The Safe Harbor category acted on.

code​

readonly code: DeidDispositionCode

The stable disposition code.

count​

readonly count: number

How many values at this locus/category/disposition were acted on.

disposition​

readonly disposition: "transformed" | "removed" | "blocked"

The disposition.

locus​

readonly locus: string

The format-neutral locus (segment/field index · path · tag), never a value.

transform​

readonly transform: TransformName

The transform applied.


DeidOptions​

Options for deidentify.

Example​

import { type DeidOptions, createDeidContext } from "@cosyte/deid";

const opts: DeidOptions = { policy: "safe-harbor", context: createDeidContext({ key: "secret" }) };

Properties​

context?​

readonly optional context?: DeidContext

The context carrying the consumer's key, required only when the policy uses a keyed transform.

policy?​

readonly optional policy?: DeidPolicy | "safe-harbor"

The policy to apply. Defaults to the built-in Safe Harbor policy.

redactor?​

readonly optional redactor?: FreeTextRedactor

A consumer-supplied free-text redactor. When present, the engine invokes it at each free-text locus instead of blocking, and records its output as consumer-asserted (DEID_FREETEXT_CONSUMER_REDACTED). The library bundles no redactor. Fail-closed contract: when this is omitted, or when the redactor throws or returns nothing, the free-text locus is blocked (the safe default), never emitted un-redacted. A returned redaction is trusted as the consumer's; the engine does not re-verify it and does not touch the structural PHI the adapters remove. See FreeTextRedactor.


DeidPolicy​

A de-identification policy: a name plus a per-category transform assignment covering all 18 categories.

Example​

import { SAFE_HARBOR_POLICY } from "@cosyte/deid";

SAFE_HARBOR_POLICY.name; // => "safe-harbor"

Properties​

name​

readonly name: string

The policy name: surfaced in output labelling ("Safe-Harbor-transformed per the configured policy").

transforms​

readonly transforms: Readonly<Record<SafeHarborCategory, TransformName>>

The transform applied to each Safe Harbor category.


DeidPolicySpec​

The spec accepted by defineDeidPolicy: a name and a partial transform map that overrides the Safe Harbor defaults for the categories it names.

Example​

import { type DeidPolicySpec, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid";

const spec: DeidPolicySpec = {
name: "research",
transforms: { [SAFE_HARBOR_CATEGORIES.DATES]: "date-shift" },
};

Properties​

name​

readonly name: string

The policy name.

transforms?​

readonly optional transforms?: Partial<Readonly<Record<SafeHarborCategory, TransformName>>>

Per-category transform overrides; unlisted categories keep their Safe Harbor default.


DeidProfile​

A reusable, named de-identification preset: a policy plus its honest usage posture and an optional default free-text redactor.

Example​

import { SAFE_HARBOR_PROFILE } from "@cosyte/deid";

SAFE_HARBOR_PROFILE.name; // => "safe-harbor"

Properties​

description​

readonly description: string

A one-line honest description of what the profile does and does not guarantee: surfaced in docs and tooling so a preset is never adopted without its caveats.

name​

readonly name: string

The profile name (also the policy name).

policy​

readonly policy: DeidPolicy

The concrete policy the engine applies.

redactor?​

readonly optional redactor?: FreeTextRedactor

An optional default free-text redactor the profile carries into profileOptions.

requiresContext​

readonly requiresContext: boolean

Whether the profile requires a keyed per-patient DeidContext to run at all (true when any category uses a keyed transform such as date-shift on a category that is always present).

standard​

readonly standard: DeidStandard

The standard this profile targets: governs how its output may honestly be described.


DeidProfileSpec​

The spec accepted by defineDeidProfile: a name, an optional base profile (default SAFE_HARBOR_PROFILE), per-category transform overrides, and an optional default redactor.

Example​

import { type DeidProfileSpec, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid";

const spec: DeidProfileSpec = {
name: "site-a",
transforms: { [SAFE_HARBOR_CATEGORIES.MRN]: "redact" }, // tighten MRN from pseudonymize to redact
};

Properties​

base?​

readonly optional base?: DeidProfile

The base profile to derive from. Defaults to SAFE_HARBOR_PROFILE.

description?​

readonly optional description?: string

An optional human description; a default is synthesized from the base when omitted.

name​

readonly name: string

The profile name. Must not be a reserved standard label unless it genuinely matches it.

redactor?​

readonly optional redactor?: FreeTextRedactor

An optional default free-text redactor the derived profile carries.

transforms?​

readonly optional transforms?: Partial<Readonly<Record<SafeHarborCategory, TransformName>>>

Per-category transform overrides. Each may only move a category to an equal-or-stronger transform than the base (widen-never-narrow); a weakening override is rejected.


DeidRegistrySpec​

Specification for createDeidRegistry. Mirrors DeidContextSpec minus the per-patient scope: the registry mints patient scopes itself via DeidRegistry.forPatient.

Example​

import { createDeidRegistry } from "@cosyte/deid";

const registry = createDeidRegistry({ key: process.env.DEID_KEY! });

Properties​

dateShiftSeed?​

readonly optional dateShiftSeed?: string | Uint8Array<ArrayBufferLike>

A separate seed for deriving per-patient date-shift offsets. Defaults to the key when omitted; supply a distinct seed to decouple pseudonymization from date-shifting.

key​

readonly key: string | Uint8Array<ArrayBufferLike>

The HMAC key for keyed transforms. Consumer-held; never emitted. Must be non-empty.

maxShiftDays?​

readonly optional maxShiftDays?: number

Absolute bound (days) on each derived per-patient date-shift offset. Defaults to 365.


DeidResult​

The immutable result of a de-id pass: the transformed document plus the value-free manifest.

Example​

import { deidentify } from "@cosyte/deid";

const result = deidentify({ loci: [] }, {});
result.manifest; // => [] (nothing acted on)

Properties​

document​

readonly document: DeidDocument

The transformed document (the generic locus document from the core).

manifest​

readonly manifest: readonly DeidManifestEntry[]

The value-free audit of every action, in locus order.


DispositionSummary​

A roll-up of how many values landed in each disposition across the whole report: the one-glance posture of the pass. Every field is a count; none is a value.

Example​

import { buildExpertDeterminationSupportReport, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid";

const report = buildExpertDeterminationSupportReport([
{ category: SAFE_HARBOR_CATEGORIES.SSN, transform: "redact", locus: "PID-19", count: 2,
disposition: "removed", code: "DEID_CATEGORY_REMOVED" },
]);
report.dispositionSummary.removed; // => 2

Properties​

blocked​

readonly blocked: number

Loci failed closed (blocked; value withheld).

freeTextBlocked​

readonly freeTextBlocked: number

Free-text loci blocked by default (DEID_FREETEXT_BLOCKED).

freeTextConsumerRedacted​

readonly freeTextConsumerRedacted: number

Free-text loci redacted by a consumer-supplied BYO redactor (DEID_FREETEXT_CONSUMER_REDACTED).

removed​

readonly removed: number

Values removed outright.

residualRetained​

readonly residualRetained: number

Of the transformed values, how many retained a coarse residual (DEID_RESIDUAL_RETAINED).

transformed​

readonly transformed: number

Values replaced with a surrogate / generalized / shifted / hashed / BYO-redacted.


ExpertDeterminationReportOptions​

Options for buildExpertDeterminationSupportReport.

Properties​

policy?​

readonly optional policy?: string | DeidPolicy

The policy applied (or its name): surfaced as the report's policy label.

quasiIdentifiers?​

readonly optional quasiIdentifiers?: QuasiIdentifierClassInput

Consumer-supplied quasi-identifier equivalence-class sizes for the descriptive k-indicator.


ExpertDeterminationSupportReport​

The structured, value-free Expert-Determination support report. Machine-readable (this object) and human-readable (via formatExpertDeterminationSupportReport). It supports a determination and is never one: determination is always null and disclaimer leads.

Example​

import { buildExpertDeterminationSupportReport, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid";

const report = buildExpertDeterminationSupportReport([
{ category: SAFE_HARBOR_CATEGORIES.NAMES, transform: "redact", locus: "PID-5", count: 1,
disposition: "removed", code: "DEID_CATEGORY_REMOVED" },
]);
report.determination; // => null (the library never renders one)
report.totals.categoriesActedOn; // => 1

Properties​

categoryCoverage​

readonly categoryCoverage: readonly CategoryCoverage[]

Coverage for all 18 Safe Harbor categories, in regulatory order (A→R).

determination​

readonly determination: null

Always null: the library renders no determination.

disclaimer​

readonly disclaimer: string

The prominent non-certification statement (EXPERT_DETERMINATION_DISCLAIMER).

dispositionSummary​

readonly dispositionSummary: DispositionSummary

The disposition roll-up.

documentCount​

readonly documentCount: number

How many documents' manifests this report summarizes (1 for a single manifest).

kind​

readonly kind: "expert-determination-support"

A stable discriminant for the report shape.

outputLabel​

readonly outputLabel: string

The output label the pass applied: "Safe-Harbor-transformed per the configured policy".

perLocus​

readonly perLocus: readonly DeidManifestEntry[]

Every acted-on locus, aggregated and in a deterministic order: the value-free manifest, structured.

policy​

readonly policy: string | null

The policy name applied, or null if not supplied.

quasiIdentifierStatistics​

readonly quasiIdentifierStatistics: QuasiIdentifierStatistics | null

Descriptive quasi-identifier statistics, only when the consumer supplied class sizes; else null.

retainedQuasiIdentifiers​

readonly retainedQuasiIdentifiers: readonly RetainedQuasiIdentifier[]

The retained-quasi-identifier residual inventory (coarse residuals the pass recorded as retained).

totals​

readonly totals: object

Headline totals.

categoriesActedOn​

readonly categoriesActedOn: number

How many of the 18 Safe Harbor categories were acted on.

loci​

readonly loci: number

Count of distinct acted-on loci (distinct locus paths across the whole report).

rows​

readonly rows: number

Count of aggregated perLocus rows (distinct category·transform·locus·disposition·code tuples).


FreeTextRedactionRequest​

The value-bearing request the engine hands a FreeTextRedactor at each free-text locus. This is the one place a value crosses into consumer code by design: it never reaches the manifest.

Example​

import { type FreeTextRedactionRequest } from "@cosyte/deid";

const request: FreeTextRedactionRequest = { text: "patient prose…", locus: "OBX-5" };

Properties​

category?​

readonly optional category?: SafeHarborCategory

The Safe Harbor category associated with the locus, when the adapter could classify it.

locus​

readonly locus: string

The format-neutral locus path (e.g. "OBX-5", "section/text"): value-free, for routing/logging.

text​

readonly text: string

The free-text prose at the locus, for the consumer's redactor to de-identify.


FreeTextRedactionResult​

The successful result of a FreeTextRedactor: the redacted prose to write back in place. To decline a locus (so the engine fails closed and blocks it), a redactor returns null / undefined or throws: it never returns un-redacted text expecting the engine to catch a miss.

Example​

import { type FreeTextRedactionResult } from "@cosyte/deid";

const result: FreeTextRedactionResult = { text: "patient [REDACTED]…" };

Properties​

text​

readonly text: string

The redacted prose. May be an empty string (all prose removed); that is a valid redaction.


GeneralizeOutcome​

The outcome of a generalization: the reduced value plus whether it retains a coarse residual (a kept year, a retained safe 3-digit ZIP prefix) that the manifest must surface for the actual-knowledge test, versus a fully non-identifying result (000, 90+).

Properties​

residual​

readonly residual: boolean

true when a coarse identifying residual was retained; false when fully suppressed.

value​

readonly value: string

The generalized value.


GenericLocus​

A single structurally-located candidate value. category is omitted when the caller cannot classify the locus: an unclassified PHI-bearing locus is treated as catch-all (R) and fails closed.

Example​

import { type GenericLocus, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid";

const locus: GenericLocus = {
path: "PID-5",
kind: "identifier",
category: SAFE_HARBOR_CATEGORIES.NAMES,
value: "SENTINEL_NAME_01",
};

Properties​

category?​

readonly optional category?: SafeHarborCategory

The Safe Harbor category, when known. Omit to force fail-closed handling as category (R).

kind​

readonly kind: LocusKind

The kind of value at this locus.

path​

readonly path: string

The format-neutral path to the value. Recorded in the manifest; never a value.

value​

readonly value: string

The value at the locus. Consumed by the engine, never copied into the manifest.


LocusModel​

The format-agnostic input model: a flat list of located candidate values. A per-format adapter produces this from a parsed HL7 / C-CDA / FHIR / X12 / NCPDP / DICOM model.

Example​

import { type LocusModel } from "@cosyte/deid";

const model: LocusModel = { loci: [{ path: "PID-3", kind: "identifier", value: "X" }] };

Properties​

loci​

readonly loci: readonly GenericLocus[]

The located candidate values to de-identify.


QuasiIdentifierClassInput​

Consumer-supplied quasi-identifier equivalence-class data. The library never derives this: it has no view of the quasi-identifier values. The consumer, who holds the values, groups their records by the chosen quasi-identifier set (e.g. 3-digit ZIP × birth year × sex) and supplies the size of each distinct group; the report echoes descriptive counts over those sizes (see QuasiIdentifierStatistics).

Example​

import { type QuasiIdentifierClassInput } from "@cosyte/deid";

// Consumer grouped 100 records into classes of these sizes over their chosen quasi-identifier set:
const qi: QuasiIdentifierClassInput = {
quasiIdentifierSet: "3-digit ZIP × birth year × sex",
equivalenceClassSizes: [40, 33, 20, 5, 1, 1],
};

Properties​

equivalenceClassSizes​

readonly equivalenceClassSizes: readonly number[]

One size per distinct quasi-identifier combination: the record count in that equivalence class.

quasiIdentifierSet?​

readonly optional quasiIdentifierSet?: string

A human label for the quasi-identifier set the sizes were computed over. No PHI.


QuasiIdentifierStatistics​

Descriptive statistics over consumer-supplied equivalence-class sizes, including the smallest class size, the widely-used k-anonymity indicator.

This is not a risk score and not a determination. It is arithmetic over sizes the consumer supplied: the library counts what it is given, applies no threshold, draws no k ≥ n ⇒ safe conclusion, and emits no verdict. A statistician documents an indicator like this as one input to a §164.514(b)(1) determination: the determination remains theirs. See note.

Example​

import { buildExpertDeterminationSupportReport } from "@cosyte/deid";

const report = buildExpertDeterminationSupportReport([], {
quasiIdentifiers: { equivalenceClassSizes: [40, 20, 5, 1, 1] },
});
report.quasiIdentifierStatistics?.minimumEquivalenceClassSize; // => 1
report.quasiIdentifierStatistics?.uniqueRecords; // => 2

Properties​

distinctCombinations​

readonly distinctCombinations: number

Number of distinct quasi-identifier combinations = the number of equivalence classes supplied.

minimumEquivalenceClassSize​

readonly minimumEquivalenceClassSize: number

The smallest equivalence-class size: the k-anonymity indicator. Descriptive only.

note​

readonly note: string

The honesty note: this is a descriptive input, never a risk score or determination.

quasiIdentifierSet​

readonly quasiIdentifierSet: string | null

The label the consumer gave the quasi-identifier set, or null if unlabelled.

totalRecords​

readonly totalRecords: number

Total records across all classes = the sum of the supplied sizes.

uniqueRecords​

readonly uniqueRecords: number

How many records fall in a class of size 1 (sample-uniques on the chosen set). Descriptive only.


RetainedQuasiIdentifier​

One entry in the retained-quasi-identifier residual inventory: a coarse identifying element the pass deliberately kept for analytic utility and recorded as DEID_RESIDUAL_RETAINED: a year-only date, a retained safe 3-digit ZIP prefix, an exact age ≤ 89. These are exactly the residuals an expert reasons about under the §164.514(b)(2)(ii) actual-knowledge test.

Example​

import { buildExpertDeterminationSupportReport, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid";

const report = buildExpertDeterminationSupportReport([
{ category: SAFE_HARBOR_CATEGORIES.DATES, transform: "generalize", locus: "PID-7", count: 1,
disposition: "transformed", code: "DEID_RESIDUAL_RETAINED" },
]);
report.retainedQuasiIdentifiers[0]?.locus; // => "PID-7"

Properties​

category​

readonly category: SafeHarborCategory

The Safe Harbor category of the retained residual.

count​

readonly count: number

How many values at this locus retained a residual.

locus​

readonly locus: string

The format-neutral locus (segment/field index · path · tag), never a value.


TransformedLocus​

The transformed value of a locus after a de-id pass: the reduced/surrogate value, or null when the value was removed or blocked (fail-closed). Carries no secret and no original value.

Example​

import { type TransformedLocus } from "@cosyte/deid";

const t: TransformedLocus = { path: "PID-3", kind: "identifier", value: null, disposition: "removed" };

Properties​

disposition​

readonly disposition: "transformed" | "removed" | "blocked" | "retained"

What happened to the value.

kind​

readonly kind: LocusKind

The kind of value at this locus (unchanged from input).

path​

readonly path: string

The format-neutral path (unchanged from input).

value​

readonly value: string | null

The transformed value, or null when removed / blocked.

Type Aliases​

DeidDispositionCode​

DeidDispositionCode = typeof DEID_DISPOSITION_CODES[keyof typeof DEID_DISPOSITION_CODES]

A value from DEID_DISPOSITION_CODES: the code every manifest entry carries.

Example​

import { DEID_DISPOSITION_CODES, type DeidDispositionCode } from "@cosyte/deid";

const code: DeidDispositionCode = DEID_DISPOSITION_CODES.DEID_CATEGORY_REMOVED;

DeidStandard​

DeidStandard = "safe-harbor" | "limited-data-set" | "custom"

The named standard a profile targets, surfaced so output labelling can never overclaim.


FatalCode​

FatalCode = typeof FATAL_CODES[keyof typeof FATAL_CODES]

A value from FATAL_CODES: the code carried by a thrown DeidError.

Example​

import { FATAL_CODES, type FatalCode } from "@cosyte/deid";

const code: FatalCode = FATAL_CODES.EMPTY_INPUT;

FreeTextRedactor​

FreeTextRedactor = (request) => FreeTextRedactionResult | null | undefined

A consumer-supplied free-text redactor. The engine invokes it at each free-text locus and treats a returned FreeTextRedactionResult as consumer-asserted (recorded as DEID_FREETEXT_CONSUMER_REDACTED). Returning null / undefined, or throwing, makes the engine fail closed and block the locus. The library never inspects the returned text for residual PHI; completeness is the consumer's responsibility.

Parameters​

request​

FreeTextRedactionRequest

The free-text value, its value-free locus path, and its category when known.

Returns​

FreeTextRedactionResult | null | undefined

The redacted prose as { text }, or null / undefined to decline (engine blocks).

Example​

import { deidentify, type FreeTextRedactor } from "@cosyte/deid";

// A consumer plugs in their own detector; the library bundles none.
const redactor: FreeTextRedactor = ({ text }) => ({ text: myNerModel.scrub(text) });
const result = deidentify(model, { redactor });

LocusKind​

LocusKind = "identifier" | "date" | "age" | "zip" | "freetext" | "clinical" | "unknown"

The kind of value at a locus: drives which generalization applies and whether the engine must fail closed. clinical is the over-scrub guard: a clinical value (a lab result, a dose, a code, a status) is not an identifier and must survive untouched.

Example​

import { type LocusKind } from "@cosyte/deid";

const kind: LocusKind = "identifier";

ReportDisposition​

ReportDisposition = "transformed" | "removed" | "blocked"

A manifest disposition: the three outcomes a locus can have.


SafeHarborCategory​

SafeHarborCategory = typeof SAFE_HARBOR_CATEGORIES[keyof typeof SAFE_HARBOR_CATEGORIES]

A value from SAFE_HARBOR_CATEGORIES: the type a policy and a manifest entry carry.

Example​

import { SAFE_HARBOR_CATEGORIES, type SafeHarborCategory } from "@cosyte/deid";

const c: SafeHarborCategory = SAFE_HARBOR_CATEGORIES.SSN;

TransformName​

TransformName = "redact" | "generalize" | "date-shift" | "pseudonymize" | "hash" | "block" | "byo-redact"

The name of a transform a policy can assign to a category. block is the fail-closed action (withhold the value); generalize selects the correct generalization from the locus kind (date / ZIP / age).

byo-redact is not a policy-assignable Safe Harbor transform and not something the library performs: it is the manifest marker the engine records when a consumer-supplied free-text redactor redacts a free-text locus. The library bundles no redactor; the consumer brings the detector. Assigning byo-redact to a category in a policy has no effect beyond the fail-closed default (the engine blocks it), because free-text redaction is driven by the redactor option, not by the per-category policy map.

Example​

import { type TransformName } from "@cosyte/deid";

const t: TransformName = "pseudonymize";

Variables​

DEID_DISPOSITION_CODES​

const DEID_DISPOSITION_CODES: object

Disposition codes: the value-free record of what the engine did at a locus. Every manifest entry carries exactly one. They describe the action and its residual, never the value acted on.

Type Declaration​

DEID_CATEGORY_DATE_SHIFTED​

readonly DEID_CATEGORY_DATE_SHIFTED: "DEID_CATEGORY_DATE_SHIFTED" = "DEID_CATEGORY_DATE_SHIFTED"

A date was shifted by a deterministic per-patient offset (interval-preserving).

DEID_CATEGORY_GENERALIZED​

readonly DEID_CATEGORY_GENERALIZED: "DEID_CATEGORY_GENERALIZED" = "DEID_CATEGORY_GENERALIZED"

A category was generalized to a fully non-identifying form (ZIP → 000, age → 90+).

DEID_CATEGORY_HASHED​

readonly DEID_CATEGORY_HASHED: "DEID_CATEGORY_HASHED" = "DEID_CATEGORY_HASHED"

A value was replaced by a keyed one-way digest (keyed hash).

DEID_CATEGORY_PSEUDONYMIZED​

readonly DEID_CATEGORY_PSEUDONYMIZED: "DEID_CATEGORY_PSEUDONYMIZED" = "DEID_CATEGORY_PSEUDONYMIZED"

A category was replaced by a consistent keyed-HMAC surrogate (pseudonymization).

DEID_CATEGORY_REMOVED​

readonly DEID_CATEGORY_REMOVED: "DEID_CATEGORY_REMOVED" = "DEID_CATEGORY_REMOVED"

A category was removed outright (redaction).

DEID_FREETEXT_BLOCKED​

readonly DEID_FREETEXT_BLOCKED: "DEID_FREETEXT_BLOCKED" = "DEID_FREETEXT_BLOCKED"

Fail-closed: a free-text locus was blocked by default (no naive regex scrub).

DEID_FREETEXT_CONSUMER_REDACTED​

readonly DEID_FREETEXT_CONSUMER_REDACTED: "DEID_FREETEXT_CONSUMER_REDACTED" = "DEID_FREETEXT_CONSUMER_REDACTED"

A free-text locus was redacted by a consumer-supplied BYO redactor, not by the library. The library ships no NLP/PHI-detection engine; it orchestrates the consumer's redactor at free-text loci and records the outcome here. This code is consumer-asserted, never a library guarantee: "no findings" from a BYO redactor is not an attestation, and a redactor's completeness is the consumer's responsibility (Expert-Determination territory). The structural PHI removal the format adapters perform is unaffected: this covers only the free prose.

DEID_LOCUS_BLOCKED​

readonly DEID_LOCUS_BLOCKED: "DEID_LOCUS_BLOCKED" = "DEID_LOCUS_BLOCKED"

Fail-closed: an unrecognized / un-locatable / uncertain locus was blocked (value withheld).

DEID_RESIDUAL_RETAINED​

readonly DEID_RESIDUAL_RETAINED: "DEID_RESIDUAL_RETAINED" = "DEID_RESIDUAL_RETAINED"

A generalization retained a coarse residual (a kept year, a retained safe 3-digit ZIP prefix). Surfaced so a human can apply the §164.514(b)(2)(ii) actual-knowledge test with the facts present.

Example​

import { DEID_DISPOSITION_CODES } from "@cosyte/deid";

DEID_DISPOSITION_CODES.DEID_LOCUS_BLOCKED; // => "DEID_LOCUS_BLOCKED"

EXPERT_DETERMINATION_DISCLAIMER​

const EXPERT_DETERMINATION_DISCLAIMER: string

The prominent non-certification statement. It is the first field a reader sees and is repeated at the head of the human-readable rendering. Its job is to make over-claiming impossible to do by accident: the report supports a determination, it is never the determination.

Example​

import { EXPERT_DETERMINATION_DISCLAIMER } from "@cosyte/deid";

EXPERT_DETERMINATION_DISCLAIMER.includes("NOT a determination"); // => true

FATAL_CODES​

const FATAL_CODES: object

Fatal codes: conditions that abort a de-identification pass by throwing a DeidError. The engine fails closed: it never silently degrades a fatal into a pass-through of PHI.

Type Declaration​

DEID_CONTEXT_INVALID​

readonly DEID_CONTEXT_INVALID: "DEID_CONTEXT_INVALID" = "DEID_CONTEXT_INVALID"

A DeidContext was configured with an invalid parameter that would silently weaken de-identification: most importantly a maxShiftDays that floors to 0, which pins every per-patient date-shift offset to zero, so a date-shift policy would emit the original real dates under a research label. A no-op shift is a leak, so the engine rejects the degenerate configuration at construction rather than silently shipping unshifted dates. The fatal set is additions-only.

DEID_NO_KEY​

readonly DEID_NO_KEY: "DEID_NO_KEY" = "DEID_NO_KEY"

A keyed transform (pseudonymize / keyed-hash / date-shift) was required for a category present in the model, but no key (or, for date-shift, no per-patient scope) was supplied. The engine never falls back to an unkeyed transform: an unkeyed hash of an identifier is re-identifiable.

DEID_POLICY_INVALID​

readonly DEID_POLICY_INVALID: "DEID_POLICY_INVALID" = "DEID_POLICY_INVALID"

A policy violates the key/label contract: most importantly, it applies the interval-preserving date-shift transform while carrying the reserved safe-harbor label. A shifted-but-real date is still "an element of a date" under §164.514(b)(2)(i)(C), so date-shift is an Expert-Determination technique, not Safe Harbor; labelling it safe-harbor would misrepresent the residual risk. The engine rejects it at point of use rather than silently emit shifted real dates under a Safe Harbor claim. The fatal set is additions-only.

DEID_PROFILE_INVALID​

readonly DEID_PROFILE_INVALID: "DEID_PROFILE_INVALID" = "DEID_PROFILE_INVALID"

A DeidProfile spec violates the widen-never-narrow contract: a per-site profile derived from a base profile may only move a category to an equal-or-stronger transform (more removal, never less), and may never re-weaken a category the base scrubs. A profile that would reduce the de-identification strength of any category is rejected, so a site preset can only ever tighten, not quietly loosen, the base standard's protection. The fatal set is additions-only.

EMPTY_INPUT​

readonly EMPTY_INPUT: "EMPTY_INPUT" = "EMPTY_INPUT"

The input model was null/undefined or carried no locus list, nothing to de-identify.

Example​

import { FATAL_CODES } from "@cosyte/deid";

FATAL_CODES.DEID_NO_KEY; // => "DEID_NO_KEY"

KEYED_TRANSFORMS​

const KEYED_TRANSFORMS: ReadonlySet<TransformName>

The transforms that require the consumer's key (and, for date-shift, a per-patient scope).


LIMITED_DATA_SET_PROFILE​

const LIMITED_DATA_SET_PROFILE: DeidProfile

The Limited Data Set / longitudinal research profile. Identical to Safe Harbor except dates are date-shifted (a single consistent per-patient offset, intervals preserved) rather than generalized to year, so time-series analysis survives.

This is deliberately less protective than Safe Harbor and is NOT Safe Harbor. A shifted-but-real date is still "an element of a date" (§164.514(b)(2)(i)(C)), so this profile:

  • is not labelled safe-harbor (the reserved-label guard would reject it);
  • requires a keyed per-patient DeidContext (an absent key is a fatal DEID_NO_KEY);
  • produces an Expert-Determination-supporting dataset, not a certified de-identification, and not, on its own, a HIPAA §164.514(e) Limited Data Set: disclosing an actual Limited Data Set additionally requires a Data Use Agreement, which is the consumer's responsibility.

Example​

import { LIMITED_DATA_SET_PROFILE } from "@cosyte/deid";

LIMITED_DATA_SET_PROFILE.requiresContext; // => true (date-shift needs a per-patient key)

OUTPUT_LABEL​

const OUTPUT_LABEL: "Safe-Harbor-transformed per the configured policy" = "Safe-Harbor-transformed per the configured policy"

The label the library applies to its output. Deliberately not "de-identified" / "HIPAA-compliant": the certification is always the consumer's.

Example​

import { OUTPUT_LABEL } from "@cosyte/deid";

OUTPUT_LABEL; // => "Safe-Harbor-transformed per the configured policy"

RESTRICTED_ZIP3​

const RESTRICTED_ZIP3: ReadonlySet<string>

The 17 restricted three-digit ZIP prefixes (population ≤ 20,000 per 2000 Census). A ZIP whose first three digits are in this set is generalized to 000; any other prefix retains its three digits.

Example​

import { RESTRICTED_ZIP3 } from "@cosyte/deid";

RESTRICTED_ZIP3.has("036"); // => true
RESTRICTED_ZIP3.has("902"); // => false

RESTRICTED_ZIP3_SOURCE​

const RESTRICTED_ZIP3_SOURCE: Readonly<{ census: "2000"; citation: "HHS OCR, Guidance Regarding Methods for De-identification of PHI (2012); 45 CFR §164.514(b)(2)(i)(B)"; }>

The Census vintage this list is grounded in, surfaced so a consumer knows exactly which published artifact the 000 rule is applied from.

Example​

import { RESTRICTED_ZIP3_SOURCE } from "@cosyte/deid";

RESTRICTED_ZIP3_SOURCE.census; // => "2000"

SAFE_HARBOR_CATEGORIES​

const SAFE_HARBOR_CATEGORIES: object

The stable registry of the 18 Safe Harbor identifier categories. key === value so the set survives an Object.values(...) snapshot into a stability tripwire. Renaming a category is a breaking change: consumers branch on these in policies and manifests.

Type Declaration​

ACCOUNT​

readonly ACCOUNT: "ACCOUNT" = "ACCOUNT"

(J) Account numbers.

BIOMETRIC​

readonly BIOMETRIC: "BIOMETRIC" = "BIOMETRIC"

(P) Biometric identifiers, including finger and voice prints.

CERTIFICATE_LICENSE​

readonly CERTIFICATE_LICENSE: "CERTIFICATE_LICENSE" = "CERTIFICATE_LICENSE"

(K) Certificate / license numbers.

DATES​

readonly DATES: "DATES" = "DATES"

(C) All elements of dates (except year) directly related to the individual, and all ages > 89.

DEVICE​

readonly DEVICE: "DEVICE" = "DEVICE"

(M) Device identifiers and serial numbers.

EMAIL​

readonly EMAIL: "EMAIL" = "EMAIL"

(F) Email addresses.

FAX​

readonly FAX: "FAX" = "FAX"

(E) Fax numbers.

FULL_FACE_PHOTO​

readonly FULL_FACE_PHOTO: "FULL_FACE_PHOTO" = "FULL_FACE_PHOTO"

(Q) Full-face photographs and any comparable images.

GEOGRAPHIC​

readonly GEOGRAPHIC: "GEOGRAPHIC" = "GEOGRAPHIC"

(B) All geographic subdivisions smaller than a state: street, city, county, precinct, ZIP, geocodes.

HEALTH_PLAN_BENEFICIARY​

readonly HEALTH_PLAN_BENEFICIARY: "HEALTH_PLAN_BENEFICIARY" = "HEALTH_PLAN_BENEFICIARY"

(I) Health plan beneficiary numbers.

IP_ADDRESS​

readonly IP_ADDRESS: "IP_ADDRESS" = "IP_ADDRESS"

(O) IP addresses.

MRN​

readonly MRN: "MRN" = "MRN"

(H) Medical record numbers.

NAMES​

readonly NAMES: "NAMES" = "NAMES"

(A) Names: patient, relatives, employers, household members.

OTHER_UNIQUE_ID​

readonly OTHER_UNIQUE_ID: "OTHER_UNIQUE_ID" = "OTHER_UNIQUE_ID"

(R) Any other unique identifying number, characteristic, or code (the open-ended catch-all).

PHONE​

readonly PHONE: "PHONE" = "PHONE"

(D) Telephone numbers.

SSN​

readonly SSN: "SSN" = "SSN"

(G) Social Security numbers.

URL​

readonly URL: "URL" = "URL"

(N) Web URLs.

VEHICLE​

readonly VEHICLE: "VEHICLE" = "VEHICLE"

(L) Vehicle identifiers and serial numbers, including license plates.

Example​

import { SAFE_HARBOR_CATEGORIES } from "@cosyte/deid";

SAFE_HARBOR_CATEGORIES.MRN; // => "MRN"

SAFE_HARBOR_CATEGORY_META​

const SAFE_HARBOR_CATEGORY_META: Readonly<Record<SafeHarborCategory, { letter: string; number: number; title: string; }>>

Per-category regulatory metadata: the §164.514(b)(2)(i) sub-paragraph letter (A–R), the ordinal number (1–18), and a short human title. Grounded firsthand in the regulation text; contains no PHI.

Example​

import { SAFE_HARBOR_CATEGORY_META, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid";

SAFE_HARBOR_CATEGORY_META[SAFE_HARBOR_CATEGORIES.GEOGRAPHIC].letter; // => "B"

SAFE_HARBOR_POLICY​

const SAFE_HARBOR_POLICY: DeidPolicy

The built-in Safe Harbor policy. Direct identifiers with no analytic value are redacted; MRN / beneficiary / account numbers are pseudonymized (consistent surrogates); geography and dates are generalized; the open-ended catch-all (R) is blocked (fail-closed). Dates generalize to year: date-shift is an Expert-Determination mode, not Safe Harbor.

Example​

import { SAFE_HARBOR_POLICY, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid";

SAFE_HARBOR_POLICY.transforms[SAFE_HARBOR_CATEGORIES.MRN]; // => "pseudonymize"

SAFE_HARBOR_PROFILE​

const SAFE_HARBOR_PROFILE: DeidProfile

The Safe Harbor profile: the fail-closed default. Wraps SAFE_HARBOR_POLICY: direct identifiers removed, MRN/beneficiary/account pseudonymized, geography and dates generalized, the catch-all (R) blocked. Output is "Safe-Harbor-transformed per the configured policy", never "de-identified".

Example​

import { SAFE_HARBOR_PROFILE } from "@cosyte/deid";

SAFE_HARBOR_PROFILE.standard; // => "safe-harbor"

VERSION​

const VERSION: string = "0.0.9"

Library version string, synced with package.json#version at release time.

Example​

import { VERSION } from "@cosyte/deid";

typeof VERSION; // => "string"

WITHHELD_LOCUS_TOKEN​

const WITHHELD_LOCUS_TOKEN: "<withheld>" = "<withheld>"

What a manifest locus prints in place of an identifier it may not echo, i.e. an identifier the adapter read out of the document that does not match the shape its position promises.

Deliberately carries no length and no prefix of the refused token: the length of a refused identifier is itself derived from the content that was refused.

Example​

import { WITHHELD_LOCUS_TOKEN } from "@cosyte/deid";

// A manifest entry whose position could not be trusted reads, e.g., "<withheld>-1".
WITHHELD_LOCUS_TOKEN; // => "<withheld>"

Functions​

buildExpertDeterminationSupportReport()​

buildExpertDeterminationSupportReport(manifests, options?): ExpertDeterminationSupportReport

Build the ExpertDeterminationSupportReport from the value-free manifest(s) of one or more de-identification passes. Deterministic; the input is never mutated; the result is deeply frozen.

Accepts either a single manifest (readonly DeidManifestEntry[]) or a corpus (an array of manifests). Counts for identical loci are summed across the corpus. The report is value-free: it carries categories, dispositions, loci, and counts, never a PHI value, and it renders no determination: determination is null and the disclaimer leads.

Parameters​

manifests​

readonly DeidManifestEntry[] | readonly readonly DeidManifestEntry[][]

A single manifest, or an array of manifests (a corpus).

options?​

ExpertDeterminationReportOptions = {}

The policy label and optional consumer-supplied quasi-identifier class sizes.

Returns​

ExpertDeterminationSupportReport

The frozen, value-free support report.

Example​

import { deidentify, buildExpertDeterminationSupportReport, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid";

const { manifest } = deidentify(
{ loci: [{ path: "PID-5", kind: "name", category: SAFE_HARBOR_CATEGORIES.NAMES, value: "X" }] },
{},
);
const report = buildExpertDeterminationSupportReport(manifest, { policy: "safe-harbor" });
report.determination; // => null
report.policy; // => "safe-harbor"

createDeidContext()​

createDeidContext(spec): DeidContext

Create a DeidContext from consumer-held key material. The key is coerced to bytes and stored only in the module-private registry, never on the returned handle.

Parameters​

spec​

DeidContextSpec

The key, optional date-shift seed, optional patient scope, and offset bound.

Returns​

DeidContext

An opaque, self-redacting context.

Throws​

DeidError with code DEID_NO_KEY if the key (or an explicit seed) is empty.

Example​

import { createDeidContext } from "@cosyte/deid";

const ctx = createDeidContext({ key: "consumer-secret", patientId: "patient-1" });

createDeidRegistry()​

createDeidRegistry(spec): DeidRegistry

Create a DeidRegistry from consumer-held key material. Fails closed on an absent/empty key: there is no default or weak key (the underlying createDeidContext throws DEID_NO_KEY). The key is stored only in the module-private state, never on the returned handle.

Parameters​

spec​

DeidRegistrySpec

The key, optional date-shift seed, and optional offset bound.

Returns​

DeidRegistry

An opaque, self-redacting corpus consistency handle.

Throws​

DeidError with code DEID_NO_KEY if the key (or an explicit seed) is empty.

Example​

import { createDeidRegistry } from "@cosyte/deid";

const registry = createDeidRegistry({ key: "consumer-secret" });

dateShift()​

dateShift(value, ctx): string | null

Shift a date by the context's deterministic per-patient offset, preserving intervals. Supports ISO YYYY-MM-DD, HL7 YYYYMMDD, and an ISO datetime (YYYY-MM-DDThh:mm…). Fails closed (null) on an unparseable or invalid date.

Timezone-independent. Only the calendar-date portion is shifted (via UTC calendar math); a datetime's time-of-day and zone designator are preserved verbatim, so the same input yields the same output on every host regardless of the machine's TZ. Because the offset is a whole number of days and the clock/zone are untouched, intervals are preserved exactly.

The offset is not returned: only the shifted value is. Two dates for the same patient move by the same amount, so the number of days between them is unchanged.

Parameters​

value​

string

The date value to shift.

ctx​

DeidContext

The de-identification context (must carry a patientId scope for the offset).

Returns​

string | null

The shifted date at the original precision, or null if it could not be parsed.

Throws​

DeidError with code DEID_NO_KEY if the context has no per-patient scope.

Example​

import { dateShift, createDeidContext } from "@cosyte/deid";

const ctx = createDeidContext({ key: "secret", patientId: "patient-1" });
const a = dateShift("2020-01-01", ctx);
const b = dateShift("2020-01-11", ctx);
// a and b are shifted, but remain exactly 10 days apart.

defineDeidPolicy()​

defineDeidPolicy(spec): DeidPolicy

Derive a custom policy from the Safe Harbor defaults. Unlisted categories keep the safe default, so a custom policy can only ever be built by deviating from Safe Harbor deliberately, never by forgetting a category. The result is frozen.

Parameters​

spec​

DeidPolicySpec

The policy name and per-category overrides.

Returns​

DeidPolicy

A frozen DeidPolicy covering all 18 categories.

Example​

import { defineDeidPolicy, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid";

const research = defineDeidPolicy({
name: "research",
transforms: { [SAFE_HARBOR_CATEGORIES.DATES]: "date-shift" },
});
research.transforms[SAFE_HARBOR_CATEGORIES.NAMES]; // => "redact" (kept from Safe Harbor)

defineDeidProfile()​

defineDeidProfile(spec): DeidProfile

Derive a per-site DeidProfile from a base profile (Safe Harbor by default), enforcing the widen-never-narrow contract: every override must move its category to an equal-or-stronger transform than the base's. A weakening override, or reclaiming a reserved standard label, is rejected (FATAL_CODES.DEID_PROFILE_INVALID), so a site preset can only tighten, never loosen, the base standard's protection.

Parameters​

spec​

DeidProfileSpec

The profile name, base, per-category overrides, and optional redactor.

Returns​

DeidProfile

A frozen DeidProfile.

Throws​

DeidError DEID_PROFILE_INVALID if an override weakens a category or the name reclaims a reserved standard label; DEID_POLICY_INVALID if the derived policy violates the key/label contract (e.g. a safe-harbor-labelled policy that date-shifts).

Example​

import { defineDeidProfile, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid";

const strict = defineDeidProfile({
name: "site-strict",
transforms: { [SAFE_HARBOR_CATEGORIES.MRN]: "redact" }, // pseudonymize -> redact (stronger): OK
});
strict.policy.transforms[SAFE_HARBOR_CATEGORIES.MRN]; // => "redact"

deidentify()​

deidentify(model, options): DeidResult

De-identify a format-agnostic LocusModel under a policy. Returns the transformed document and a value-free manifest. The input model is never mutated; the result is deeply frozen.

The output is "Safe-Harbor-transformed per the configured policy": it is not certified de-identified, and Expert Determination is not rendered.

Parameters​

model​

The located candidate values to de-identify.

loci?​

readonly GenericLocus[]

options​

DeidOptions

The policy and (for keyed transforms) the key context.

Returns​

DeidResult

The frozen DeidResult: transformed document + value-free manifest.

Throws​

DeidError EMPTY_INPUT if the model is null or carries no locus list; DEID_NO_KEY if a keyed transform is required but no key context was supplied.

Example​

import { deidentify, SAFE_HARBOR_CATEGORIES } from "@cosyte/deid";

const result = deidentify(
{ loci: [{ path: "PID-19", kind: "identifier", category: SAFE_HARBOR_CATEGORIES.SSN, value: "SENTINEL" }] },
{},
);
result.document.loci[0]?.value; // => null (removed)
result.manifest[0]?.disposition; // => "removed"

formatExpertDeterminationSupportReport()​

formatExpertDeterminationSupportReport(report): string

Render an ExpertDeterminationSupportReport as a human-readable Markdown document: the same value-free facts as the structured object, led by the non-certification disclaimer. Suitable to hand to a statistician alongside the machine-readable report.

Parameters​

report​

ExpertDeterminationSupportReport

A report from buildExpertDeterminationSupportReport.

Returns​

string

A Markdown string (value-free: categories, dispositions, loci, counts, never a value).

Example​

import { buildExpertDeterminationSupportReport, formatExpertDeterminationSupportReport } from "@cosyte/deid";

const md = formatExpertDeterminationSupportReport(buildExpertDeterminationSupportReport([]));
md.startsWith("# Expert-Determination support report"); // => true

generalizeAge()​

generalizeAge(age): GeneralizeOutcome | null

Generalize an age (§164.514(b)(2)(i)(C)): any age over 89 aggregates to "90+"; ages 0–89 are retained (a residual). Fails closed (null) for a non-finite or negative age.

Parameters​

age​

number

The age in years.

Returns​

GeneralizeOutcome | null

The { value, residual } outcome (residual: false for "90+", true for a kept age), or null if the age is not a finite non-negative number.

Example​

import { generalizeAge } from "@cosyte/deid";

generalizeAge(92)?.value; // => "90+"
generalizeAge(89)?.value; // => "89"

generalizeDate()​

generalizeDate(value): GeneralizeOutcome | null

Generalize a date to its year (§164.514(b)(2)(i)(C)). Accepts common encodings: ISO YYYY-MM-DD / YYYY-MM-DDThh:mm:ss, HL7 YYYYMMDD, anything beginning with a plausible four-digit year. Fails closed (null) when no year can be extracted; the retained year is a residual.

Parameters​

value​

string

The date value to generalize.

Returns​

GeneralizeOutcome | null

The { value: year, residual: true } outcome, or null if no year is present.

Example​

import { generalizeDate } from "@cosyte/deid";

generalizeDate("2019-03-14")?.value; // => "2019"
generalizeDate("not-a-date"); // => null

generalizeZip()​

generalizeZip(zip): GeneralizeOutcome | null

Generalize a ZIP code to its safe form (§164.514(b)(2)(i)(B)): the initial three digits, or 000 when those three digits name an area with ≤ 20,000 people (the cited restricted list). Fails closed (null) when three leading digits cannot be read.

Parameters​

zip​

string

The ZIP code (5-digit, ZIP+4, or any form beginning with digits).

Returns​

GeneralizeOutcome | null

The { value, residual } outcome (residual: false for 000, true for a kept prefix), or null if fewer than three leading digits are present.

Example​

import { generalizeZip } from "@cosyte/deid";

generalizeZip("90210")?.value; // => "902"
generalizeZip("03601")?.value; // => "000" (036 is a restricted prefix)

keyedHash()​

keyedHash(value, ctx): string

Replace a value with a keyed one-way digest (HMAC-SHA-256). Like pseudonymize it is consistent and non-reversible without the key, but domain-separated so it is a distinct surrogate space from pseudonyms.

Parameters​

value​

string

The value to hash.

ctx​

DeidContext

The de-identification context holding the consumer's key.

Returns​

string

A lowercase-hex keyed digest, consistent per (key, value).

Example​

import { keyedHash, createDeidContext } from "@cosyte/deid";

const ctx = createDeidContext({ key: "secret" });
typeof keyedHash("value", ctx); // => "string"

profileOptions()​

profileOptions(profile, context?, overrides?): DeidOptions

Build the DeidOptions to pass to any adapter (deidentifyHl7, deidentifyFhir, …) from a profile: its policy, the supplied key context, and the profile's default redactor (unless overridden).

Parameters​

profile​

DeidProfile

The profile to apply.

context?​

DeidContext

The keyed per-patient context (required by profiles whose requiresContext is true).

overrides?​

Optional context/redactor overrides merged over the profile's defaults.

context?​

DeidContext

redactor?​

FreeTextRedactor

Returns​

DeidOptions

The DeidOptions for an adapter call.

Example​

import { SAFE_HARBOR_PROFILE, profileOptions, createDeidContext } from "@cosyte/deid";

const opts = profileOptions(SAFE_HARBOR_PROFILE, createDeidContext({ key: "k" }));
opts.policy === SAFE_HARBOR_PROFILE.policy; // => true

pseudonymize()​

pseudonymize(id, ctx): string

Replace an identifier with a consistent keyed-HMAC surrogate. The same input under the same key always yields the same surrogate (linkage preserved); the surrogate is not reversible without the key (collision-resistant, key-dependent). Domain-separated from keyedHash.

Parameters​

id​

string

The identifier to pseudonymize (MRN, beneficiary number, account number).

ctx​

DeidContext

The de-identification context holding the consumer's key.

Returns​

string

A lowercase-hex surrogate, consistent per (key, id).

Example​

import { pseudonymize, createDeidContext } from "@cosyte/deid";

const ctx = createDeidContext({ key: "secret" });
pseudonymize("MRN-123", ctx) === pseudonymize("MRN-123", ctx); // => true (consistent)

redact()​

redact(): null

Redact a value. Returns null: the removed-value sentinel used throughout the transformed document. Pure and keyless.

Returns​

null

null, signalling the value has been removed.

Example​

import { redact } from "@cosyte/deid";

redact(); // => null

resolvePolicy()​

resolvePolicy(policy): DeidPolicy

Resolve the policy argument accepted by the engine: the string "safe-harbor" (or undefined) yields the built-in policy; a DeidPolicy object is returned as-is.

Parameters​

policy​

DeidPolicy | "safe-harbor" | undefined

"safe-harbor", a DeidPolicy, or undefined.

Returns​

DeidPolicy

The concrete policy to apply.

Example​

import { resolvePolicy, SAFE_HARBOR_POLICY } from "@cosyte/deid";

resolvePolicy("safe-harbor") === SAFE_HARBOR_POLICY; // => true

unkeyedHash()​

unkeyedHash(value): string

A plain, unsalted SHA-256 digest: NON-CONFORMING to §164.514(c) and never used by the engine. It is re-identifiable for a small, enumerable identifier space (an attacker hashes every candidate and matches). Exported solely so the test suite can prove the reversibility hazard the keyed path avoids. Do not use this to de-identify anything.

Parameters​

value​

string

The value to hash.

Returns​

string

The lowercase-hex SHA-256 of value: reversible for a small input space.

Example​

import { unkeyedHash } from "@cosyte/deid";

// Deterministic and unsalted: this is the footgun, shown so tests can assert against it.
unkeyedHash("a") === unkeyedHash("a"); // => true