FDA 21 CFR Part 11
What 21 CFR Part 11 asks of a system keeping electronic records and accepting electronic signatures, enforced rather than described.
An electronic signature under 11.50 is not a signature unless it records who signed, when, and what the signing meant, so one is built with a meaning from the permitted set and is refused without it. The signature is bound to a digest of the record, which makes it a signature of that record rather than a note attached to it.
The 11.10(e) audit requirement is checked on the events themselves. Every event in a GxP namespace has to carry recordedAt, an actor as either actorUserId or actor.userId, and a string action.
An action whose last segment is not a reading verb is a modification, and a modification has to say what changed: metadata.before, metadata.after and a reason, on the row or in the metadata. That is the difference between an audit trail and a log. The posture can intercept the audit sink, and an event missing any of it is then dropped instead of written, with a denied event recorded in its place. The drop is silent to the code that emitted it, so a deployment that wants the failure to reach its own code calls assertGxpAudit on the row first. Reads are recorded too, because under Part 11 who looked is part of the record.
b.fda21cfr11.posture(opts?) #
{
signWith: object, // function (payloadBuffer) returning a signature
verifyWith: object, // function (payloadBuffer, signature) → boolean
meanings: array, // permitted signing meanings
gxpNamespaces: array, // audit namespaces the shape is enforced on
interceptAudit: boolean, // false leaves the audit sink alone
audit: object, // audit instance to record to
now: object, // function returning epoch ms; for tests
A signature's input needs `printedName`, a `signatureMeaning` from the
permitted set, and `predicateRule`, the regulation the signing is made
under. Each has its own refusal code, because 11.50(b) requires the printed
name and the meaning to be part of the signed record rather than metadata
beside it. `boundRecord` is what the signature is a signature of: it is
digested into the signed payload, and `verify` recomputes that digest from
the record it is given, so a signature presented against different content
answers `record-hash-mismatch`.
Without `signWith` and `verifyWith` the signature field is `null` and
`verify` only recomputes the record digest, which is unkeyed: anyone who can
change the record can recompute it and have the result verify. That mode is
for checking the shape of a record, not for signing one. A Part 11
signature needs both hooks wired to the deployment's signing key.
}
Build the Part 11 posture. The returned object carries electronicSignature with create and verify, the audit checks assertGxpAudit and checkGxpAudit, and install / uninstall for the audit interception.
meanings is the set of signing meanings a signature may declare, and defaults to review, approval, responsibility, authorship, verification, release, rejected and witness. A signature declaring anything else is refused, since 11.50 requires the meaning to be recorded and an unconstrained string is not a recorded meaning.
gxpNamespaces is which audit events the 11.10(e) shape is enforced on, and defaults to subject, consent, db and breakglass. With interceptAudit left on, installing wraps the audit sink so an event in one of those namespaces missing the required fields is dropped rather than written, and a denied fda21cfr11.audit.refused naming the attempted action and the missing part is written instead. safeEmit does not raise, so the emitting code carries on; assertGxpAudit is the call that reports the failure to the caller.
signWith and verifyWith supply the signing and verification, so the deployment's own key custody is used rather than one invented here. Both receive the canonical signed payload as a Buffer, the JSON carrying the printed name, timestamp, meaning, predicate rule and record digest, and not a digest of it: signWith(payload) returns the signature, and verifyWith(payload, signature) answers whether it holds. The payload is passed whole so a signer can apply its own digest, which is what an HSM or a KMS expects.
var b = require("@blamejs/core");
var crypto = require("node:crypto");
var key = crypto.randomBytes(32);
var part11 = b.fda21cfr11.posture({
meanings: ["approval", "review"],
signWith: function (payload) {
return crypto.createHmac("sha3-512", key).update(payload).digest();
},
verifyWith: function (payload, signature) {
var want = crypto.createHmac("sha3-512", key).update(payload).digest();
return want.length === signature.length &&
crypto.timingSafeEqual(want, signature);
},
});
var batch = { batchId: "B-204", released: true };
var signed = part11.electronicSignature.create({
printedName: "Dr. A. Chen",
signatureMeaning: "approval",
predicateRule: "21 CFR 312.62",
boundRecord: batch,
});
part11.electronicSignature.verify(signed, batch).ok; // → true
part11.electronicSignature.verify(signed, { batchId: "B-205" }).reason;
// → "record-hash-mismatch"
b.fda21cfr11.assertGxpAudit(row) #
Require an audit row to carry the 11.10(e) shape, and raise fda21cfr11/gxp-shape-violation naming the missing part otherwise.
Every row needs recordedAt, an actor as actorUserId or actor.userId, and a string action. When the action's last segment is not a reading verb the row is a modification and also needs metadata.before, metadata.after and a reason, as row.reason or metadata.reason.
Call this where a failure should reach the caller, which is while the row is being built. The refusal is itself recorded as a denied fda21cfr11.gxp.assert_failed event, so a rejected write leaves a trace rather than disappearing.
The installed interception does not call this. It applies the same shape check inside b.audit.safeEmit, and a row that fails the check is dropped and recorded as a denied fda21cfr11.audit.refused without raising: safeEmit runs on the request path, and a throw there would fail the request that was only writing an audit row. Interception keeps a malformed GxP row out of the chain; it does not report back to the code that wrote it.
var b = require("@blamejs/core");
b.fda21cfr11.assertGxpAudit({
action: "subject.consent.granted", // a modification verb
actorUserId: "dr.chen",
recordedAt: Date.now(),
reason: "signed consent form received",
metadata: { before: { consent: false }, after: { consent: true } },
}); // → true
b.fda21cfr11.checkGxpAudit(row) #
Answer { ok, reason } for an audit row's 11.10(e) shape without raising.
This is the form to use while building a row or while surveying rows already written, where a throw would stop the survey at the first finding. assertGxpAudit is the enforcing form.
var b = require("@blamejs/core");
b.fda21cfr11.checkGxpAudit({ action: "subject.read" });
// → { ok: false, reason: "…the field it is missing…" }