npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@cosyte/deid

v0.0.9

Published

Healthcare de-identification for Node.js and TypeScript. A HIPAA Safe Harbor policy engine that fails closed and emits a value-free manifest.

Readme

@cosyte/deid

Healthcare de-identification for Node.js and TypeScript: a HIPAA-grounded policy engine that fails closed and emits a value-free manifest.

@cosyte/deid applies a de-identification policy (HIPAA Safe Harbor by default) to a structurally-located model of a healthcare document and returns a transformed model plus a value-free audit of everything it acted on. It is a consumer of the @cosyte/* parsers, not a parser sibling: it borrows the archetype's disciplines (typed diagnostics, immutable output, a policy/profile system) but inverts the reflex: where a parser is liberal on input, a de-identifier is conservative and fails closed. Third-party runtime dependencies: zero (every primitive is node:crypto).

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

Status: pre-alpha (0.0.x), published on npm. This release ships the format-agnostic core plus six format bindings: the HL7 v2 adapter (@cosyte/deid/hl7), the C-CDA adapter (@cosyte/deid/ccda), the FHIR R4 adapter (@cosyte/deid/fhir), the X12 EDI adapter (@cosyte/deid/x12), the NCPDP Telecom adapter (@cosyte/deid/ncpdp), and the DICOM adapter (@cosyte/deid/dicom), plus the longitudinal layer, the corpus registry (createDeidRegistry) for cross-document consistency and the formalized key contract, and the Expert-Determination support report (buildExpertDeterminationSupportReport) that structures the manifest for a statistician without ever rendering a determination. NCPDP SCRIPT is not supported.

Install

npm install @cosyte/deid

De-identify

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

const { document, manifest } = deidentify(
  {
    loci: [
      { path: "PID-5", kind: "identifier", category: SAFE_HARBOR_CATEGORIES.NAMES, value: name },
      { path: "PID-7", kind: "date", category: SAFE_HARBOR_CATEGORIES.DATES, value: dob },
      { path: "OBX-5", kind: "clinical", value: "5.4 mmol/L" },
    ],
  },
  {},
);

// document.loci[0].value === null   (name removed)
// document.loci[1].value === "<year>" (date generalized)
// document.loci[2].value === "5.4 mmol/L" (clinical value retained, the over-scrub guard)
// manifest records each category + locus + disposition, never a value.

Keyed transforms

Pseudonymization and keyed hashing use a keyed HMAC-SHA-256; the key is the consumer's and never leaves the process:

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

const context = createDeidContext({ key: process.env.DEID_KEY!, patientId: "patient-1" });
deidentify(
  {
    loci: [{ path: "PID-3", kind: "identifier", category: SAFE_HARBOR_CATEGORIES.MRN, value: mrn }],
  },
  { context },
);
// The MRN becomes a consistent, non-reversible surrogate; the key never appears in the output or manifest.

De-identify an HL7 v2 message

The @cosyte/deid/hl7 adapter locates PHI structurally in the parsed @cosyte/hl7 model, never by regex over the raw bytes, and returns a transformed Hl7Message plus the value-free manifest. @cosyte/hl7 is an optional peer dependency: install it alongside @cosyte/deid to use this subpath; the core stays dependency-free.

npm install @cosyte/deid @cosyte/hl7
import { parseHL7 } from "@cosyte/hl7";
import { deidentifyHl7 } from "@cosyte/deid/hl7";
import { createDeidContext } from "@cosyte/deid";

const context = createDeidContext({ key: process.env.DEID_KEY! });
const { document, manifest } = deidentifyHl7(parseHL7(rawMessage), { context });

document.toString(); // spec-clean, de-identified HL7 wire
// PID-5 (name), NK1/GT1/IN1/IN2 relatives, SSN, phone → removed; MRN/account → consistent surrogate;
// DOB → year; address → safe 3-digit ZIP. OBX-5/NTE free text and Z-segments fail closed (blocked).
// Structured clinical OBX values, units, codes, and statuses survive untouched.

What it covers. The structured PHI loci of PID (patient) and NK1 / GT1 / IN1 / IN2 (relatives / guarantor / insured), typed by the @cosyte/hl7 model. Fail closed everywhere else: a recognized segment is retained only if it is on an explicit clinical/administrative retain-list, so a known patient-identity segment absent from the map (e.g. MRG prior name + MRN on a merge, FAM, ACC) is blocked, never passed through, and Z-segments / structure unknown to the parser are blocked. OBX-5 is retained only when OBX-2 positively types it as a structured clinical value (numeric / coded / date); narrative (TX/FT), ambiguous String (ST), and any empty/unknown OBX-2 fail closed, as do NTE-3 comments. Structured clinical values, units, codes, and statuses survive untouched.

Known limitations (this release). Free text is block-by-default (no built-in scrub; opt-in BYO redaction: see Free text); within retained clinical / visit segments, patient-related dates (OBR/DG1/PV1 timestamps), visit identifiers (PV1-19), and provider names (PV1-7/8, OBR-16) are not de-identified; the address generalization keeps only the Safe Harbor 3-digit ZIP.

De-identify a C-CDA document

The @cosyte/deid/ccda adapter locates PHI structurally in a parsed @cosyte/ccda document (a <name> under recordTarget/patientRole/patient is the patient's name because the CDA standard says so), and returns a transformed CcdaDocument plus the value-free manifest. @cosyte/ccda is an optional peer dependency; the adapter reaches the CDA DOM only through its hardened parseSecureXml and re-serializes the node it hands back, so the core stays third-party-dependency-free.

npm install @cosyte/deid @cosyte/ccda
import { parseCcda } from "@cosyte/ccda";
import { deidentifyCcda } from "@cosyte/deid/ccda";
import { createDeidContext } from "@cosyte/deid";

const context = createDeidContext({ key: process.env.DEID_KEY! });
const { document, manifest } = deidentifyCcda(parseCcda(xml), { context });

document.toString(); // spec-clean, de-identified C-CDA XML
// recordTarget/guardian/author/informant/custodian names, telecom, ids, addresses, birthTime, and
// participation/encounter dates → transformed; section narrative <text> and unknown elements fail
// closed. Coded clinical entries (codes, values, units, statuses, dosing periods) survive untouched.

What it covers. The structured PHI loci of the CDA header participations: recordTarget (patient) + nested guardian, and author / dataEnterer / informant / authenticator / legalAuthenticator / participant / custodian / documentationOf / componentOf (relatives / providers / contacts). Person <name> / <telecom> removed; person-role <id> pseudonymized (SSN-rooted id removed, assigning root retained); <addr> reduced to the safe 3-digit ZIP; <birthTime> and participation / encounter dates generalized to year. Fail closed everywhere else: section narrative <text> blocks and the unstructured nonXMLBody are blocked; a value-bearing element that is neither mapped PHI nor recognized coded structure is blocked; foreign / sdtc elements are blocked. The clinical structuredBody entries are retained untouched (the over-scrub guard): a <name> there is a drug or material name, not a person.

Known limitations (this release). Narrative is block-by-default (no built-in scrub; opt-in BYO redaction: see Free text); within the retained clinical body, entry service dates, entry ids, in-entry performer names, and family-history relative demographics are not de-identified (mirroring the HL7 adapter's boundary); the document id/code/title envelope is retained (like HL7's MSH).

De-identify a FHIR R4 resource

The @cosyte/deid/fhir adapter locates PHI structurally in a parsed @cosyte/fhir resource (a name under a Patient is the patient's name because FHIR says so), and returns a transformed resource model plus the value-free manifest. @cosyte/fhir is an optional peer dependency; the adapter reaches FHIR data only through its exported model and parseResource / serializeResource codec, so the core stays third-party-dependency-free.

npm install @cosyte/deid @cosyte/fhir
import { parseResource, serializeResource } from "@cosyte/fhir";
import { deidentifyFhir } from "@cosyte/deid/fhir";
import { createDeidContext } from "@cosyte/deid";

const context = createDeidContext({ key: process.env.DEID_KEY! });
const { resource } = parseResource(json);
const { document, manifest } = deidentifyFhir(resource, { context });

serializeResource(document); // spec-clean, de-identified FHIR JSON
// Patient/RelatedPerson/Practitioner/Person names, telecom, photo → removed; address → safe 3-digit ZIP;
// birthDate + every date → year; identifiers pseudonymized by system (a US-SSN identifier removed).
// Narrative text.div, extension values, and Reference.display fail closed; contained resources and
// Bundle entries are walked. Clinical resources (Observation values, codes, units, statuses) survive.

FHIR is a graph of typed resources, so the map splits by role:

| Scope | Loci | Transform | | --------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | Person resources (Patient / RelatedPerson / Practitioner / Person + nested Patient.contact) | name, telecom, photo, address, birthDate, dates | name/telecom/photo removed; address → safe 3-digit ZIP (or 000); dates → year | | Every resource (universal PHI vectors) | identifier, dates, narrative text.div, extension values, Reference.display | identifier → surrogate by system (US-SSN removed); dates → year; narrative / extension values / reference labels blocked | | Clinical resources (Observation, Condition, …) | codes, values, units, statuses, reference wiring | retained untouched (the over-scrub guard) |

A Reference.display (a person label) is blocked; a Coding.display (a coded term like Sodium) is retained: the two are told apart structurally. Contained resources and Bundle entries are walked, with each resource's role re-derived at its own resourceType.

Fail closed governs the person sweep and the frontier: a bare unrecognized string at a person resource's top level is blocked (an allow-list can never satisfy Safe Harbor category (R)); a display that is not on a Coding is treated as a Reference person-label and blocked, including a display-only ({ display }) or type+display reference that names no target; every extension value (a complex valueAddress / valueHumanName, a modifierExtension, a nested extension, or a primitive-level _-sibling extension) is dropped; and free-text loci (note Annotations, contentString, an uncoded valueString) are blocked (the FHIR analogue of the HL7 adapter's OBX-5-ST / NTE fail-closed default).

Known limitations (this release). Extension values are block-only (no profile-aware retention, a us-core-* demographic extension is dropped). Reference wiring (Reference.reference pointers, resource logical ids) is preserved structurally; coordinated pseudonymization of resource ids across a corpus is not performed. Free-text prose loci (note, contentString, uncoded valueString) fail closed by default, or run through an opt-in BYO redactor (see Free text); a built-in semantic (NLP) narrative scrub, contentAttachment binary content, and person names embedded in non-person resources (Organization.contact.name, Location.address) remain out of scope for this release.

De-identify an X12 EDI interchange

The @cosyte/deid/x12 adapter locates PHI structurally in a parsed @cosyte/x12 interchange (HIPAA 005010: 837 claims, 835 remittance, 270/271 eligibility, …). @cosyte/x12 is an optional peer dependency; the adapter reaches EDI data only through its exported model and re-emits with its byte-faithful serializeX12.

import { parseX12 } from "@cosyte/x12";
import { deidentifyX12 } from "@cosyte/deid/x12";
import { createDeidContext } from "@cosyte/deid";

const context = createDeidContext({ key: process.env.DEID_KEY! });
const { x12, manifest } = deidentifyX12(parseX12(raw), { context });
// `x12` is the de-identified interchange; `manifest` is the value-free audit.

| Locus | Handling | | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------ | | NM1 (subscriber / patient / dependent) | name (03–07) removed; id (09) routed by the 08 qualifier: SSN removed, member pseudonymized | | NM1 (recognized provider / organization) | retained (provider identity is not the individual's PHI, mirroring the HL7 adapter) | | NM1 (unknown entity code) | fails closed: name + id blocked | | N1 / SBR | N1 payer/provider org retained (patient-side/unknown party fails closed); SBR-03 group/policy pseudonymized, SBR-04 group name removed | | N3 / N4 | street + city removed, ZIP → safe 3-digit, state retained (unmapped N4-06 location id fails closed) | | DMG-02, DTP-03, DTM-02 | dates → year | | PER | contact name + communication numbers removed | | REF | patient / member / group / SSN identifier removed or pseudonymized; admin/provider reference retained; unknown qualifier fails closed | | CLM-01 / CLP-01 | patient account number pseudonymized | | Clinical / financial (HI, SV*, SVC, AMT, CAS…) | retained untouched: diagnosis / procedure codes, amounts, quantities survive byte-identical |

De-identify an NCPDP Telecom transaction

The @cosyte/deid/ncpdp adapter locates PHI structurally in a parsed @cosyte/ncpdp Telecommunication (vD.0) transaction. @cosyte/ncpdp is an optional peer dependency.

import { parseTelecom } from "@cosyte/ncpdp/telecom";
import { deidentifyTelecom } from "@cosyte/deid/ncpdp";
import { createDeidContext } from "@cosyte/deid";

const context = createDeidContext({ key: process.env.DEID_KEY! });
const { telecom, manifest } = deidentifyTelecom(parseTelecom(raw), { context });

The Patient (01), Insurance (04), and Coordination-of-Benefits (05) segments and the header Date of Service carry the individual's identity: name / phone / street / city removed, ZIP → 3-digit, DOB and dates → year, patient / cardholder / group ids pseudonymized. The Prescriber (03) id is removed (a deliberate asymmetry with the X12 adapter's provider-retention stance). A free-text field (544-FY DUR, 504-F4 message) and any unmapped / unknown segment fail closed; the clinical / financial segments (NDC drug codes, quantities, days-supply, pricing, DUR codes) are retained untouched.

NCPDP SCRIPT (ePrescribing XML) is deferred: its parser's serializeScript emits only modeled fields (a round-trip drops unmodeled XML) and its Patient model has no address / phone / patient-id field, so a faithful structural de-id is not achievable through the current public surface. Shipping a partial pass would be a false-safety hazard, which the fail-closed posture forbids.

De-identify a DICOM study

The @cosyte/deid/dicom adapter delegates rather than reimplements: @cosyte/dicom already ships the PS3.15 Annex E de-identification (the Basic Application Level Confidentiality Profile), so this adapter orchestrates that pass under the unified policy and folds its value-free report into the unified manifest. @cosyte/dicom is an optional peer dependency.

import { parseDicom } from "@cosyte/dicom";
import { deidentifyDicom, deidentifyDicomBuffer } from "@cosyte/deid/dicom";

const { dataset, manifest, burnedInAnnotationHazard } = deidentifyDicom(parseDicom(part10Bytes));
const { bytes } = deidentifyDicomBuffer(part10Bytes); // parse → de-id → re-serialize in one call

The full Basic Profile applies by default (no key needed): Patient Name/ID/Birth Date, institution, referring physician, dates and the enumerated Annex E attributes are removed; Study / Series / SOP Instance UIDs are consistently remapped so image/series/study relationships survive; private tags are removed (fail-closed, kept only via a known-safe retain list, empty by default); clinical/technical values and pixel bytes are retained untouched. The output carries Patient Identity Removed = YES.

Pixel PHI is flagged, never cleaned. This is a metadata-only de-identifier (metadataOnly is always true): it cannot inspect pixels, so recognizable text burned into the image (Safe Harbor category Q) is not removed. When Pixel Data may carry burned-in annotation, the result sets burnedInAnnotationHazard === true and emits DICOM_BURNED_IN_ANNOTATION_NOT_REMOVED: do not release such an image on metadata alone. Pixel cleaning is a future @cosyte/dicom-pixel.

Keep a longitudinal record linkable

For research and analytics, the same patient must stay linkable across a whole corpus after de-id. A registry (createDeidRegistry) holds your key and keeps the same patient's dates shifting by the same offset, intervals preserved, and the same identifier mapping to the same pseudonym, across every document and every run.

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

const registry = createDeidRegistry({ key: process.env.DEID_KEY! });
const research = defineDeidPolicy({
  name: "research", // date-shift may NOT wear the "safe-harbor" label, it is Expert-Determination
  transforms: { [SAFE_HARBOR_CATEGORIES.DATES]: "date-shift" },
});

const ctx = registry.forPatient("patient-1"); // reuse for every document of this patient
deidentify(model, { policy: research, context: ctx }); // dates shift consistently, intervals intact
registry.pseudonym("MRN-1"); // same MRN → same surrogate corpus-wide

The key contract. You supply the key; there is no weak default (an absent key is a fatal DEID_NO_KEY, never a silent fallback). Rotating the key is intentional linkage breakage: a new key un-links a corpus from records made under the old one. The library holds no persistent key store. Date-shift retains dates in shifted form, so it is Expert-Determination-supporting, not Safe Harbor, and the library rejects any date-shifting policy that claims the safe-harbor label (DEID_POLICY_INVALID).

Free text: block-by-default + BYO redaction

Narrative loci (HL7 OBX-5 / NTE, C-CDA section <text>, FHIR note / div, X12 MSG / NTE, NCPDP free text) can carry any of the 18 categories in prose, with no structural handle on where. The default is fail-closed: with no redactor, every free-text locus is blocked (value withheld). The library ships no NLP model and no built-in regex scrub: a naive pass over clinical prose is a false-safety hazard.

To redact free text rather than block it, bring your own redactor: a function wrapping your regex / pattern engine or clinical-NER de-id model. The engine invokes it at each free-text locus and writes its output back in place, recording the locus as consumer-asserted (DEID_FREETEXT_CONSUMER_REDACTED).

import { deidentifyHl7 } from "@cosyte/deid/hl7";
import { type FreeTextRedactor } from "@cosyte/deid";

const redactor: FreeTextRedactor = ({ text }) => ({ text: myNerModel.scrub(text) });
const { document } = deidentifyHl7(parseHL7(raw), { context, redactor });

The fail-closed contract holds regardless of the redactor. No redactor → block; the redactor throws → block; the redactor returns nothing → block; the redactor returns { text } → written back in place. A redactor is never allowed to leak free text through on failure.

The honesty boundary. A returned redaction is trusted as consumer-asserted: the engine does not re-scan it for residual PHI, and "no findings" from a BYO redactor is not an attestation. A BYO redactor's completeness is the consumer's responsibility (Expert-Determination territory). The structural PHI removal the adapters perform, and the clinical over-scrub guard, are unchanged: the redactor handles the free prose only.

Expert-Determination support, never certification

HIPAA has two routes to de-identification: Safe Harbor (mechanical, implemented here) and Expert Determination (§164.514(b)(1), a qualified statistician's risk judgment). @cosyte/deid supports the latter and never renders it. buildExpertDeterminationSupportReport(manifest) structures the value-free manifest into what an expert reasons about: per-locus dispositions, coverage across all 18 categories, and the retained-quasi-identifier inventory (year-only dates, safe 3-digit ZIP prefixes, exact ages ≤ 89), then hands it over.

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

const { manifest } = deidentifyHl7(parseHL7(raw), { context });
const report = buildExpertDeterminationSupportReport(manifest, { policy: "safe-harbor" });
report.determination; // => null: the library never renders one

The hard boundary. The report never says the output "is de-identified", never computes or fabricates a re-identification risk score, and reaches no conclusion: determination is always null and a prominent disclaimer leads. It is value-free (loci / categories / dispositions / counts, never a value). The one quasi-identifier statistic it can surface (the smallest equivalence-class size, a k-anonymity indicator) is computed only over class sizes you supply (the library has no view of quasi-identifier values) and is stamped a descriptive input, never a verdict. formatExpertDeterminationSupportReport(report) renders the same facts as Markdown for a statistician.

Policy profiles: reusable presets, widen-never-narrow

Two named presets ship, and defineDeidProfile() derives a per-site profile that can only ever tighten the base, never loosen it.

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

// The fail-closed default (dates → year, the (R) catch-all blocked).
SAFE_HARBOR_PROFILE.standard; // => "safe-harbor"

// A longitudinal research preset: dates are DATE-SHIFTED, not generalized. Deliberately less
// protective than Safe Harbor → NOT labelled "safe-harbor", requires a keyed per-patient context, and
// is NOT a certified de-identification (nor, on its own, a §164.514(e) Limited Data Set; that needs a
// Data Use Agreement, which is yours).
LIMITED_DATA_SET_PROFILE.requiresContext; // => true

// A per-site profile may only move a category to an equal-or-STRONGER transform; a weakening override
// is a fatal DEID_PROFILE_INVALID.
const strict = defineDeidProfile({
  name: "site-strict",
  transforms: { [SAFE_HARBOR_CATEGORIES.MRN]: "redact" }, // pseudonymize → redact (stronger): OK
});

const ctx = createDeidContext({ key: process.env.DEID_KEY! });
const opts = profileOptions(strict, ctx); // pass straight to any adapter

Known limitations & honesty

@cosyte/deid transforms per a policy and evidences what it did: it never certifies. Read docs-content/limitations.md before relying on it for anything that leaves your control: the fail-closed posture, structured-core-only (free text is block-by-default), DICOM metadata-only (burned-in pixels flagged, not cleaned), NCPDP SCRIPT deferred, the BYO free-text redactor is the consumer's responsibility, and the Expert-Determination report makes no determination.

The design in five pieces

  • Policy engine: safe-harbor built in; defineDeidPolicy() to deviate deliberately.
  • Five transforms: redact, generalize (date→year, ZIP→3-digit/000, age→90+), keyed date-shift, keyed-HMAC pseudonymize, keyed hash.
  • 18 Safe Harbor categories: §164.514(b)(2)(i)(A)–(R), including the open-ended catch-all (R).
  • Fail-closed rule: anything uncertain is blocked, never passed through; clinical values are retained untouched.
  • Value-free manifest: category + transform + locus + count + disposition + code, never a value, never the key, never the date-shift offset. The locus is the one field built out of the document: a per-format adapter names the position with the identifier that sits there, so each identifier is checked against the shape its position promises and a non-conforming one is refused as WITHHELD_LOCUS_TOKEN (<withheld>) rather than echoed.

License

MIT © Cosyte