Skip to main content
Version: v0.0.3

@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: "0.0.0" = "0.0.0"

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