@blamejs/core 0.8.43 → 0.8.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +92 -0
- package/README.md +10 -10
- package/index.js +52 -0
- package/lib/a2a.js +159 -34
- package/lib/acme.js +762 -0
- package/lib/ai-pref.js +166 -43
- package/lib/api-key.js +108 -47
- package/lib/api-snapshot.js +157 -40
- package/lib/app-shutdown.js +113 -77
- package/lib/archive.js +337 -40
- package/lib/arg-parser.js +697 -0
- package/lib/asyncapi.js +99 -55
- package/lib/atomic-file.js +465 -104
- package/lib/audit-chain.js +123 -34
- package/lib/audit-daily-review.js +389 -0
- package/lib/audit-sign.js +302 -56
- package/lib/audit-tools.js +412 -63
- package/lib/audit.js +656 -35
- package/lib/auth/jwt-external.js +17 -0
- package/lib/auth/oauth.js +7 -0
- package/lib/auth-bot-challenge.js +505 -0
- package/lib/auth-header.js +92 -25
- package/lib/backup/bundle.js +26 -0
- package/lib/backup/index.js +512 -89
- package/lib/backup/manifest.js +168 -7
- package/lib/break-glass.js +415 -39
- package/lib/budr.js +103 -30
- package/lib/bundler.js +86 -66
- package/lib/cache.js +192 -72
- package/lib/chain-writer.js +65 -40
- package/lib/circuit-breaker.js +56 -33
- package/lib/cli-helpers.js +106 -75
- package/lib/cli.js +6 -30
- package/lib/cloud-events.js +99 -32
- package/lib/cluster-storage.js +162 -37
- package/lib/cluster.js +340 -49
- package/lib/codepoint-class.js +66 -0
- package/lib/compliance.js +424 -24
- package/lib/config-drift.js +111 -46
- package/lib/config.js +94 -40
- package/lib/consent.js +165 -18
- package/lib/constants.js +1 -0
- package/lib/content-credentials.js +153 -48
- package/lib/cookies.js +154 -62
- package/lib/credential-hash.js +133 -61
- package/lib/crypto-field.js +702 -18
- package/lib/crypto-hpke.js +256 -0
- package/lib/crypto.js +744 -22
- package/lib/csv.js +178 -35
- package/lib/daemon.js +456 -0
- package/lib/dark-patterns.js +186 -55
- package/lib/db-query.js +79 -2
- package/lib/db.js +1431 -60
- package/lib/ddl-change-control.js +523 -0
- package/lib/deprecate.js +195 -40
- package/lib/dev.js +82 -39
- package/lib/dora.js +67 -48
- package/lib/dr-runbook.js +368 -0
- package/lib/dsr.js +142 -11
- package/lib/dual-control.js +91 -56
- package/lib/events.js +120 -41
- package/lib/external-db-migrate.js +192 -2
- package/lib/external-db.js +795 -50
- package/lib/fapi2.js +122 -1
- package/lib/fda-21cfr11.js +395 -0
- package/lib/fdx.js +132 -2
- package/lib/file-type.js +87 -0
- package/lib/file-upload.js +93 -0
- package/lib/flag.js +82 -20
- package/lib/forms.js +132 -29
- package/lib/framework-error.js +169 -0
- package/lib/framework-schema.js +163 -35
- package/lib/gate-contract.js +849 -175
- package/lib/graphql-federation.js +68 -7
- package/lib/guard-all.js +172 -55
- package/lib/guard-archive.js +286 -124
- package/lib/guard-auth.js +194 -21
- package/lib/guard-cidr.js +190 -28
- package/lib/guard-csv.js +397 -51
- package/lib/guard-domain.js +213 -91
- package/lib/guard-email.js +236 -29
- package/lib/guard-filename.js +307 -75
- package/lib/guard-graphql.js +263 -30
- package/lib/guard-html.js +310 -116
- package/lib/guard-image.js +243 -30
- package/lib/guard-json.js +260 -54
- package/lib/guard-jsonpath.js +235 -23
- package/lib/guard-jwt.js +284 -30
- package/lib/guard-markdown.js +204 -22
- package/lib/guard-mime.js +190 -26
- package/lib/guard-oauth.js +277 -28
- package/lib/guard-pdf.js +251 -27
- package/lib/guard-regex.js +226 -18
- package/lib/guard-shell.js +229 -26
- package/lib/guard-svg.js +177 -10
- package/lib/guard-template.js +232 -21
- package/lib/guard-time.js +195 -29
- package/lib/guard-uuid.js +189 -30
- package/lib/guard-xml.js +259 -36
- package/lib/guard-yaml.js +241 -44
- package/lib/honeytoken.js +63 -27
- package/lib/html-balance.js +83 -0
- package/lib/http-client.js +486 -59
- package/lib/http-message-signature.js +582 -0
- package/lib/i18n.js +102 -49
- package/lib/iab-mspa.js +112 -32
- package/lib/iab-tcf.js +107 -2
- package/lib/inbox.js +90 -52
- package/lib/keychain.js +865 -0
- package/lib/legal-hold.js +374 -0
- package/lib/local-db-thin.js +320 -0
- package/lib/log-stream.js +281 -51
- package/lib/log.js +184 -86
- package/lib/mail-bounce.js +107 -62
- package/lib/mail.js +295 -58
- package/lib/mcp.js +108 -27
- package/lib/metrics.js +98 -89
- package/lib/middleware/age-gate.js +36 -0
- package/lib/middleware/ai-act-disclosure.js +37 -0
- package/lib/middleware/api-encrypt.js +45 -0
- package/lib/middleware/assetlinks.js +40 -0
- package/lib/middleware/asyncapi-serve.js +35 -0
- package/lib/middleware/attach-user.js +40 -0
- package/lib/middleware/bearer-auth.js +40 -0
- package/lib/middleware/body-parser.js +230 -0
- package/lib/middleware/bot-disclose.js +34 -0
- package/lib/middleware/bot-guard.js +39 -0
- package/lib/middleware/compression.js +37 -0
- package/lib/middleware/cookies.js +32 -0
- package/lib/middleware/cors.js +40 -0
- package/lib/middleware/csp-nonce.js +40 -0
- package/lib/middleware/csp-report.js +34 -0
- package/lib/middleware/csrf-protect.js +43 -0
- package/lib/middleware/daily-byte-quota.js +53 -85
- package/lib/middleware/db-role-for.js +40 -0
- package/lib/middleware/dpop.js +40 -0
- package/lib/middleware/error-handler.js +37 -14
- package/lib/middleware/fetch-metadata.js +39 -0
- package/lib/middleware/flag-context.js +34 -0
- package/lib/middleware/gpc.js +33 -0
- package/lib/middleware/headers.js +35 -0
- package/lib/middleware/health.js +46 -0
- package/lib/middleware/host-allowlist.js +30 -0
- package/lib/middleware/network-allowlist.js +38 -0
- package/lib/middleware/openapi-serve.js +34 -0
- package/lib/middleware/rate-limit.js +160 -18
- package/lib/middleware/request-id.js +36 -18
- package/lib/middleware/request-log.js +37 -0
- package/lib/middleware/require-aal.js +29 -0
- package/lib/middleware/require-auth.js +32 -0
- package/lib/middleware/require-bound-key.js +41 -0
- package/lib/middleware/require-content-type.js +32 -0
- package/lib/middleware/require-methods.js +27 -0
- package/lib/middleware/require-mtls.js +33 -0
- package/lib/middleware/require-step-up.js +37 -0
- package/lib/middleware/security-headers.js +44 -0
- package/lib/middleware/security-txt.js +38 -0
- package/lib/middleware/span-http-server.js +37 -0
- package/lib/middleware/sse.js +36 -0
- package/lib/middleware/trace-log-correlation.js +33 -0
- package/lib/middleware/trace-propagate.js +32 -0
- package/lib/middleware/tus-upload.js +90 -0
- package/lib/middleware/web-app-manifest.js +53 -0
- package/lib/mtls-ca.js +100 -70
- package/lib/network-byte-quota.js +308 -0
- package/lib/network-heartbeat.js +135 -0
- package/lib/network-tls.js +534 -4
- package/lib/network.js +103 -0
- package/lib/notify.js +114 -43
- package/lib/ntp-check.js +192 -51
- package/lib/observability.js +145 -47
- package/lib/openapi.js +90 -44
- package/lib/outbox.js +99 -1
- package/lib/pagination.js +168 -86
- package/lib/parsers/index.js +16 -5
- package/lib/permissions.js +93 -40
- package/lib/pqc-agent.js +84 -8
- package/lib/pqc-software.js +94 -60
- package/lib/process-spawn.js +95 -21
- package/lib/pubsub.js +96 -66
- package/lib/queue.js +375 -54
- package/lib/redact.js +793 -21
- package/lib/render.js +139 -47
- package/lib/request-helpers.js +485 -121
- package/lib/restore-bundle.js +142 -39
- package/lib/restore-rollback.js +136 -45
- package/lib/retention.js +178 -50
- package/lib/retry.js +116 -33
- package/lib/router.js +475 -23
- package/lib/safe-async.js +543 -94
- package/lib/safe-buffer.js +337 -41
- package/lib/safe-json.js +467 -62
- package/lib/safe-jsonpath.js +285 -0
- package/lib/safe-schema.js +631 -87
- package/lib/safe-sql.js +221 -59
- package/lib/safe-url.js +278 -46
- package/lib/sandbox-worker.js +135 -0
- package/lib/sandbox.js +358 -0
- package/lib/scheduler.js +135 -70
- package/lib/self-update.js +647 -0
- package/lib/session-device-binding.js +431 -0
- package/lib/session.js +259 -49
- package/lib/slug.js +138 -26
- package/lib/ssrf-guard.js +316 -56
- package/lib/storage.js +433 -70
- package/lib/subject.js +405 -23
- package/lib/template.js +148 -8
- package/lib/tenant-quota.js +545 -0
- package/lib/testing.js +440 -53
- package/lib/time.js +291 -23
- package/lib/tls-exporter.js +239 -0
- package/lib/tracing.js +90 -74
- package/lib/uuid.js +97 -22
- package/lib/vault/index.js +284 -22
- package/lib/vault/seal-pem-file.js +66 -0
- package/lib/watcher.js +368 -0
- package/lib/webhook.js +196 -63
- package/lib/websocket.js +393 -68
- package/lib/wiki-concepts.js +338 -0
- package/lib/worker-pool.js +464 -0
- package/package.json +3 -3
- package/sbom.cyclonedx.json +7 -7
package/lib/fapi2.js
CHANGED
|
@@ -1,6 +1,44 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* b.fapi2
|
|
3
|
+
* @module b.fapi2
|
|
4
|
+
* @nav Compliance
|
|
5
|
+
* @title FAPI 2.0
|
|
6
|
+
*
|
|
7
|
+
* @intro
|
|
8
|
+
* FAPI 2.0 financial-API compliance — mTLS-bound tokens, ML-DSA
|
|
9
|
+
* signatures, JAR/JARM, sender-constrained tokens.
|
|
10
|
+
*
|
|
11
|
+
* FAPI 2.0 Final
|
|
12
|
+
* (https://openid.net/specs/fapi-2_0-security-profile-FINAL.html)
|
|
13
|
+
* is the OpenID Foundation's security profile for financial /
|
|
14
|
+
* banking APIs. It composes existing IETF + OAuth standards into
|
|
15
|
+
* a single profile that operators MUST satisfy to interoperate
|
|
16
|
+
* with FAPI 2.0 client deployments. The composition (per §5):
|
|
17
|
+
*
|
|
18
|
+
* - PAR (Pushed Authorization Requests, RFC 9126) — REQUIRED
|
|
19
|
+
* - PKCE with S256 (RFC 7636) — REQUIRED, PLAIN refused
|
|
20
|
+
* - Sender-constrained tokens via DPoP (RFC 9449) OR mTLS
|
|
21
|
+
* (RFC 8705) — REQUIRED, exactly one
|
|
22
|
+
* - Authorization-server issuer in callback (RFC 9207) —
|
|
23
|
+
* REQUIRED
|
|
24
|
+
* - TLS 1.2+ with FAPI-approved cipher suites (TLS 1.3 default)
|
|
25
|
+
* - JAR (JWT-secured Authorization Request, RFC 9101) when the
|
|
26
|
+
* request-object is signed
|
|
27
|
+
*
|
|
28
|
+
* The framework already ships every component primitive. FAPI 2.0
|
|
29
|
+
* conformance is therefore a posture-coordination problem: the
|
|
30
|
+
* operator declares the deployment is FAPI-bound, and the
|
|
31
|
+
* framework asserts every primitive in the chain is configured
|
|
32
|
+
* per the profile. `b.auth.oauth.create(...)` remains the
|
|
33
|
+
* operator's OAuth declaration; `b.fapi2.assertOAuthConfig` is
|
|
34
|
+
* the boot-time gate that refuses to start a FAPI-declared
|
|
35
|
+
* deployment if any mandate is missing.
|
|
36
|
+
*
|
|
37
|
+
* @card
|
|
38
|
+
* FAPI 2.0 financial-API compliance — mTLS-bound tokens, ML-DSA signatures, JAR/JARM, sender-constrained tokens.
|
|
39
|
+
*/
|
|
40
|
+
/*
|
|
41
|
+
* Original prose retained:
|
|
4
42
|
*
|
|
5
43
|
* FAPI 2.0 Final (https://openid.net/specs/fapi-2_0-security-profile-FINAL.html)
|
|
6
44
|
* is the OpenID Foundation's security profile for financial / banking
|
|
@@ -63,6 +101,38 @@ var Fapi2Error = defineClass("Fapi2Error", { alwaysPermanent: true });
|
|
|
63
101
|
|
|
64
102
|
var SENDER_CONSTRAINTS = ["dpop", "mtls"];
|
|
65
103
|
|
|
104
|
+
/**
|
|
105
|
+
* @primitive b.fapi2.assertConformance
|
|
106
|
+
* @signature b.fapi2.assertConformance(opts)
|
|
107
|
+
* @since 0.8.0
|
|
108
|
+
* @status stable
|
|
109
|
+
* @compliance fapi2
|
|
110
|
+
* @related b.fapi2.assertOAuthConfig, b.fapi2.posture
|
|
111
|
+
*
|
|
112
|
+
* Inspect operator-declared FAPI 2.0 wiring and return a structured
|
|
113
|
+
* report. Throws `Fapi2Error` for non-S256 PKCE or absent
|
|
114
|
+
* sender-constraint; non-mandatory mandates report `WAIVED`. Emits
|
|
115
|
+
* a `fapi2.posture_asserted` audit event so regulators see a single
|
|
116
|
+
* conformance assertion per boot.
|
|
117
|
+
*
|
|
118
|
+
* @opts
|
|
119
|
+
* senderConstraint: "dpop" | "mtls", // REQUIRED
|
|
120
|
+
* parRequired: boolean, // default true
|
|
121
|
+
* pkceMethod: "S256", // S256 only; "plain" is refused
|
|
122
|
+
* requireIssuerInCallback: boolean, // default true (RFC 9207)
|
|
123
|
+
* requireJarOnSignedRequests: boolean, // default true (RFC 9101)
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* var report = b.fapi2.assertConformance({
|
|
127
|
+
* senderConstraint: "mtls",
|
|
128
|
+
* parRequired: true,
|
|
129
|
+
* pkceMethod: "S256",
|
|
130
|
+
* });
|
|
131
|
+
* report.conformant;
|
|
132
|
+
* // → true
|
|
133
|
+
* report.findings[0].requirement;
|
|
134
|
+
* // → "pkce-s256"
|
|
135
|
+
*/
|
|
66
136
|
function assertConformance(opts) {
|
|
67
137
|
if (!opts || typeof opts !== "object") {
|
|
68
138
|
throw Fapi2Error.factory("BAD_OPTS",
|
|
@@ -118,6 +188,40 @@ function assertConformance(opts) {
|
|
|
118
188
|
return { conformant: conformant, findings: findings };
|
|
119
189
|
}
|
|
120
190
|
|
|
191
|
+
/**
|
|
192
|
+
* @primitive b.fapi2.assertOAuthConfig
|
|
193
|
+
* @signature b.fapi2.assertOAuthConfig(oauthOpts)
|
|
194
|
+
* @since 0.8.0
|
|
195
|
+
* @status stable
|
|
196
|
+
* @compliance fapi2
|
|
197
|
+
* @related b.fapi2.assertConformance, b.fapi2.posture
|
|
198
|
+
*
|
|
199
|
+
* Boot-time gate over a `b.auth.oauth.create(opts)` configuration.
|
|
200
|
+
* Throws `Fapi2Error` when PKCE is disabled or non-S256, when no
|
|
201
|
+
* sender-constraint is declared, when both DPoP and mTLS are set
|
|
202
|
+
* (over-binding ambiguity), or when PAR is disabled. Operators
|
|
203
|
+
* call this immediately after constructing the OAuth client so a
|
|
204
|
+
* misconfigured deployment refuses to start.
|
|
205
|
+
*
|
|
206
|
+
* @opts
|
|
207
|
+
* pkce: boolean,
|
|
208
|
+
* pkceMethod: "S256",
|
|
209
|
+
* dpop: boolean,
|
|
210
|
+
* mtls: boolean,
|
|
211
|
+
* senderConstraint: "dpop" | "mtls",
|
|
212
|
+
* par: boolean,
|
|
213
|
+
*
|
|
214
|
+
* @example
|
|
215
|
+
* try {
|
|
216
|
+
* b.fapi2.assertOAuthConfig({
|
|
217
|
+
* pkce: true, pkceMethod: "S256",
|
|
218
|
+
* mtls: true, par: true,
|
|
219
|
+
* });
|
|
220
|
+
* } catch (e) {
|
|
221
|
+
* // → never reached for the conformant config above
|
|
222
|
+
* throw e;
|
|
223
|
+
* }
|
|
224
|
+
*/
|
|
121
225
|
function assertOAuthConfig(oauthOpts) {
|
|
122
226
|
if (!oauthOpts || typeof oauthOpts !== "object") {
|
|
123
227
|
throw Fapi2Error.factory("BAD_OAUTH_OPTS",
|
|
@@ -152,6 +256,23 @@ function assertOAuthConfig(oauthOpts) {
|
|
|
152
256
|
}
|
|
153
257
|
}
|
|
154
258
|
|
|
259
|
+
/**
|
|
260
|
+
* @primitive b.fapi2.posture
|
|
261
|
+
* @signature b.fapi2.posture()
|
|
262
|
+
* @since 0.8.0
|
|
263
|
+
* @status stable
|
|
264
|
+
* @compliance fapi2
|
|
265
|
+
* @related b.fapi2.assertConformance, b.compliance.current
|
|
266
|
+
*
|
|
267
|
+
* Returns `"fapi-2.0"` when `b.compliance.set("fapi-2.0")` has
|
|
268
|
+
* been called, else `null`. Convenience for code that branches on
|
|
269
|
+
* the posture without calling `b.compliance.current()` directly.
|
|
270
|
+
*
|
|
271
|
+
* @example
|
|
272
|
+
* b.compliance.set("fapi-2.0");
|
|
273
|
+
* b.fapi2.posture();
|
|
274
|
+
* // → "fapi-2.0"
|
|
275
|
+
*/
|
|
155
276
|
function posture() {
|
|
156
277
|
return compliance.current() === "fapi-2.0" ? "fapi-2.0" : null;
|
|
157
278
|
}
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* b.fda21cfr11 — FDA 21 CFR Part 11 audit-content + electronic-signature
|
|
4
|
+
* shape primitives.
|
|
5
|
+
*
|
|
6
|
+
* Part 11 governs electronic records + electronic signatures for any
|
|
7
|
+
* FDA-regulated activity (drugs, biologics, medical devices, food
|
|
8
|
+
* safety, tobacco). The framework's audit chain already satisfies
|
|
9
|
+
* §11.10(a)/(c)/(d)/(g) (validation, secure protected storage,
|
|
10
|
+
* limited access, signed audit trail). This module closes the
|
|
11
|
+
* remaining shape-of-content gaps:
|
|
12
|
+
*
|
|
13
|
+
* §11.10(e) — Use of secure, computer-generated, time-stamped audit
|
|
14
|
+
* trails to independently record the date and time of
|
|
15
|
+
* operator entries and actions that create, modify, or
|
|
16
|
+
* delete electronic records. RECORD MUST CARRY before /
|
|
17
|
+
* after / actor / reason / timestamp.
|
|
18
|
+
*
|
|
19
|
+
* §11.50(b) — Signed electronic record must carry the printed name
|
|
20
|
+
* of the signer, the date and time when the signature
|
|
21
|
+
* was executed, and the meaning (such as review,
|
|
22
|
+
* approval, responsibility, or authorship) associated
|
|
23
|
+
* with the signature.
|
|
24
|
+
*
|
|
25
|
+
* §11.70 — Electronic signatures and handwritten signatures
|
|
26
|
+
* executed to electronic records shall be linked to
|
|
27
|
+
* their respective electronic records to ensure that
|
|
28
|
+
* the signatures cannot be excised, copied, or otherwise
|
|
29
|
+
* transferred to falsify an electronic record by
|
|
30
|
+
* ordinary means.
|
|
31
|
+
*
|
|
32
|
+
* The primitive doesn't generate signatures itself — it produces the
|
|
33
|
+
* §11.50(b) shape and binds it to an electronic record via SHA3-512
|
|
34
|
+
* hash. Operators with a HSM-backed signer wire signatures through
|
|
35
|
+
* `signWith(payload) → Buffer`. Without `signWith` the primitive
|
|
36
|
+
* still produces the §11.50(b) shape; the operator's own signature
|
|
37
|
+
* apparatus carries the binding.
|
|
38
|
+
*
|
|
39
|
+
* var fda = b.fda21cfr11.posture({ audit: b.audit, signWith: signer });
|
|
40
|
+
* var sig = fda.electronicSignature.create({
|
|
41
|
+
* printedName: "Jane Doe, M.D.",
|
|
42
|
+
* signatureMeaning: "approval",
|
|
43
|
+
* predicateRule: "21 CFR 312.62 — investigator records",
|
|
44
|
+
* boundRecord: recordBytes,
|
|
45
|
+
* });
|
|
46
|
+
* // → { printedName, dateTimeUtc, signatureMeaning, signatureRecord,
|
|
47
|
+
* // predicateRule, recordHash, signature? }
|
|
48
|
+
*
|
|
49
|
+
* // §11.10(e) shape assertion against an audit row (or row-shaped
|
|
50
|
+
* // object with metadata.before / metadata.after).
|
|
51
|
+
* fda.assertGxpAudit(row);
|
|
52
|
+
*
|
|
53
|
+
* Posture interceptor — when wired, intercepts audit.safeEmit on
|
|
54
|
+
* GxP-namespace events (default: namespaces listed in opts.gxpNamespaces
|
|
55
|
+
* or any action under the "subject" / "consent" / "db" namespaces) and
|
|
56
|
+
* refuses any event missing the §11.10(e) shape. Refused events are
|
|
57
|
+
* audited as `fda21cfr11.audit.refused` so the violation is visible.
|
|
58
|
+
*
|
|
59
|
+
* Audit emissions:
|
|
60
|
+
* fda21cfr11.signature.created — every electronicSignature.create
|
|
61
|
+
* fda21cfr11.signature.verified — every electronicSignature.verify
|
|
62
|
+
* fda21cfr11.audit.refused — every interceptor refusal
|
|
63
|
+
* fda21cfr11.posture.installed — when posture interceptor wired
|
|
64
|
+
* fda21cfr11.gxp.assert_failed — every assertGxpAudit failure
|
|
65
|
+
*/
|
|
66
|
+
|
|
67
|
+
var lazyRequire = require("./lazy-require");
|
|
68
|
+
var safeJson = require("./safe-json");
|
|
69
|
+
var validateOpts = require("./validate-opts");
|
|
70
|
+
var { sha3Hash } = require("./crypto");
|
|
71
|
+
var { Fda21Cfr11Error } = require("./framework-error");
|
|
72
|
+
|
|
73
|
+
var audit = lazyRequire(function () { return require("./audit"); });
|
|
74
|
+
|
|
75
|
+
// §11.50(b) signature meanings — operators may extend via opts.meanings
|
|
76
|
+
// at posture creation. The set below covers the verbiage FDA reviewers
|
|
77
|
+
// expect on Part 11-regulated records.
|
|
78
|
+
var DEFAULT_SIGNATURE_MEANINGS = Object.freeze([
|
|
79
|
+
"review",
|
|
80
|
+
"approval",
|
|
81
|
+
"responsibility",
|
|
82
|
+
"authorship",
|
|
83
|
+
"verification",
|
|
84
|
+
"release",
|
|
85
|
+
"rejected",
|
|
86
|
+
"witness",
|
|
87
|
+
]);
|
|
88
|
+
|
|
89
|
+
// Audit namespaces that the framework defaults to treating as "GxP-
|
|
90
|
+
// regulated" — every emit on these namespaces under fda-21cfr11 posture
|
|
91
|
+
// must carry §11.10(e) shape. Operators add more via opts.gxpNamespaces.
|
|
92
|
+
var DEFAULT_GXP_NAMESPACES = Object.freeze(["subject", "consent", "db", "breakglass"]);
|
|
93
|
+
|
|
94
|
+
// §11.10(e) — every modification audit must carry timestamp, actor,
|
|
95
|
+
// before/after pair, and a reason. The framework's audit row shape
|
|
96
|
+
// already carries `recordedAt` (timestamp) + `actorUserId` + `reason`;
|
|
97
|
+
// the gap is `metadata.before` and `metadata.after` — operators were
|
|
98
|
+
// putting them in by convention. Now enforced.
|
|
99
|
+
function _hasRequiredAuditShape(row) {
|
|
100
|
+
if (!row || typeof row !== "object") {
|
|
101
|
+
return { ok: false, reason: "row is not an object" };
|
|
102
|
+
}
|
|
103
|
+
// recordedAt may be raw ms or ISO; presence is what §11.10(e) asks.
|
|
104
|
+
if (row.recordedAt === undefined || row.recordedAt === null) {
|
|
105
|
+
return { ok: false, reason: "row missing recordedAt timestamp (§11.10(e))" };
|
|
106
|
+
}
|
|
107
|
+
// actor-binding — actorUserId or a sub-actor object that carries one
|
|
108
|
+
// (the audit chain row has actorUserId; pre-emit shapes have actor.userId).
|
|
109
|
+
var actorPresent = (row.actorUserId !== undefined && row.actorUserId !== null) ||
|
|
110
|
+
(row.actor && typeof row.actor === "object" && row.actor.userId);
|
|
111
|
+
if (!actorPresent) {
|
|
112
|
+
return { ok: false, reason: "row missing actor identification (§11.10(e))" };
|
|
113
|
+
}
|
|
114
|
+
if (!row.action || typeof row.action !== "string") {
|
|
115
|
+
return { ok: false, reason: "row missing action verb (§11.10(e))" };
|
|
116
|
+
}
|
|
117
|
+
// Modification-shaped events (verbs containing "update" / "modif" /
|
|
118
|
+
// "delete" / "rectif" / "erase" / "set") must carry before/after.
|
|
119
|
+
var verb = row.action.toLowerCase();
|
|
120
|
+
var modShape = /\.(update|updated|modif|modified|delete|deleted|rectif|rectified|erase|erased|set|setrole|put|patched)\b/.test(verb) ||
|
|
121
|
+
/\.(update|delete|modif|set|put|patch|rectif|erase)/.test(verb);
|
|
122
|
+
if (modShape) {
|
|
123
|
+
var meta = row.metadata;
|
|
124
|
+
// Audit chain stores metadata as a JSON string when read back —
|
|
125
|
+
// accept both raw object + JSON-string form.
|
|
126
|
+
if (typeof meta === "string") {
|
|
127
|
+
try { meta = safeJson.parse(meta); } catch (_e) { meta = null; }
|
|
128
|
+
}
|
|
129
|
+
if (!meta || typeof meta !== "object") {
|
|
130
|
+
return { ok: false, reason: "row missing metadata.before/after for modification verb (§11.10(e))" };
|
|
131
|
+
}
|
|
132
|
+
if (meta.before === undefined) {
|
|
133
|
+
return { ok: false, reason: "row missing metadata.before for modification verb (§11.10(e))" };
|
|
134
|
+
}
|
|
135
|
+
if (meta.after === undefined) {
|
|
136
|
+
return { ok: false, reason: "row missing metadata.after for modification verb (§11.10(e))" };
|
|
137
|
+
}
|
|
138
|
+
if (!row.reason && (!meta.reason)) {
|
|
139
|
+
return { ok: false, reason: "row missing reason for modification verb (§11.10(e))" };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return { ok: true };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ---- Signature shape ----
|
|
146
|
+
|
|
147
|
+
function _toRecordHash(record) {
|
|
148
|
+
if (record === undefined || record === null) return null;
|
|
149
|
+
if (Buffer.isBuffer(record)) return sha3Hash(record);
|
|
150
|
+
if (typeof record === "string") return sha3Hash(Buffer.from(record, "utf8"));
|
|
151
|
+
if (typeof record === "object") return sha3Hash(Buffer.from(JSON.stringify(record), "utf8"));
|
|
152
|
+
throw new Fda21Cfr11Error("fda21cfr11/bad-bound-record",
|
|
153
|
+
"electronicSignature.create: boundRecord must be Buffer|string|object");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function _validateSignatureInput(input, meanings) {
|
|
157
|
+
if (!input || typeof input !== "object") {
|
|
158
|
+
throw new Fda21Cfr11Error("fda21cfr11/bad-signature-input",
|
|
159
|
+
"electronicSignature.create: input must be an object");
|
|
160
|
+
}
|
|
161
|
+
if (typeof input.printedName !== "string" || input.printedName.length === 0) {
|
|
162
|
+
throw new Fda21Cfr11Error("fda21cfr11/missing-printed-name",
|
|
163
|
+
"electronicSignature.create: printedName is required (§11.50(b))");
|
|
164
|
+
}
|
|
165
|
+
if (typeof input.signatureMeaning !== "string" || meanings.indexOf(input.signatureMeaning) === -1) {
|
|
166
|
+
throw new Fda21Cfr11Error("fda21cfr11/bad-signature-meaning",
|
|
167
|
+
"electronicSignature.create: signatureMeaning must be one of " +
|
|
168
|
+
meanings.join(", ") + " (§11.50(b))");
|
|
169
|
+
}
|
|
170
|
+
if (typeof input.predicateRule !== "string" || input.predicateRule.length === 0) {
|
|
171
|
+
throw new Fda21Cfr11Error("fda21cfr11/missing-predicate-rule",
|
|
172
|
+
"electronicSignature.create: predicateRule is required (e.g. '21 CFR 312.62')");
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ---- Public surface ----
|
|
177
|
+
|
|
178
|
+
function posture(opts) {
|
|
179
|
+
opts = opts || {};
|
|
180
|
+
validateOpts(opts, [
|
|
181
|
+
"audit", "signWith", "verifyWith", "meanings", "gxpNamespaces",
|
|
182
|
+
"interceptAudit", "now",
|
|
183
|
+
], "fda21cfr11.posture");
|
|
184
|
+
validateOpts.auditShape(opts.audit, "fda21cfr11.posture",
|
|
185
|
+
Fda21Cfr11Error, "fda21cfr11/bad-audit");
|
|
186
|
+
validateOpts.optionalFunction(opts.signWith,
|
|
187
|
+
"fda21cfr11.posture: signWith", Fda21Cfr11Error, "fda21cfr11/bad-signer");
|
|
188
|
+
validateOpts.optionalFunction(opts.verifyWith,
|
|
189
|
+
"fda21cfr11.posture: verifyWith", Fda21Cfr11Error, "fda21cfr11/bad-verifier");
|
|
190
|
+
validateOpts.optionalFunction(opts.now,
|
|
191
|
+
"fda21cfr11.posture: now", Fda21Cfr11Error, "fda21cfr11/bad-now");
|
|
192
|
+
|
|
193
|
+
var auditMod = opts.audit && typeof opts.audit.safeEmit === "function" ? opts.audit : null;
|
|
194
|
+
var signWith = typeof opts.signWith === "function" ? opts.signWith : null;
|
|
195
|
+
var verifyWith = typeof opts.verifyWith === "function" ? opts.verifyWith : null;
|
|
196
|
+
var meanings = Array.isArray(opts.meanings) && opts.meanings.length > 0
|
|
197
|
+
? opts.meanings.slice() : DEFAULT_SIGNATURE_MEANINGS.slice();
|
|
198
|
+
var gxpNamespaces = Array.isArray(opts.gxpNamespaces) && opts.gxpNamespaces.length > 0
|
|
199
|
+
? opts.gxpNamespaces.slice() : DEFAULT_GXP_NAMESPACES.slice();
|
|
200
|
+
var interceptAudit = opts.interceptAudit !== false;
|
|
201
|
+
var now = typeof opts.now === "function" ? opts.now : Date.now;
|
|
202
|
+
|
|
203
|
+
function _emit(action, metadata, outcome) {
|
|
204
|
+
if (!auditMod) return;
|
|
205
|
+
try {
|
|
206
|
+
auditMod.safeEmit({
|
|
207
|
+
action: action,
|
|
208
|
+
outcome: outcome || "success",
|
|
209
|
+
metadata: metadata || {},
|
|
210
|
+
});
|
|
211
|
+
} catch (_e) { /* audit best-effort */ }
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function createSignature(input) {
|
|
215
|
+
_validateSignatureInput(input, meanings);
|
|
216
|
+
var ts = now();
|
|
217
|
+
var dateTimeUtc = new Date(ts).toISOString();
|
|
218
|
+
var recordHash = _toRecordHash(input.boundRecord);
|
|
219
|
+
var payload = {
|
|
220
|
+
printedName: input.printedName,
|
|
221
|
+
dateTimeUtc: dateTimeUtc,
|
|
222
|
+
signatureMeaning: input.signatureMeaning,
|
|
223
|
+
predicateRule: input.predicateRule,
|
|
224
|
+
recordHash: recordHash,
|
|
225
|
+
};
|
|
226
|
+
var signedPayload = JSON.stringify(payload);
|
|
227
|
+
var signatureRecord = sha3Hash(Buffer.from(signedPayload, "utf8"));
|
|
228
|
+
var sig = signWith ? signWith(Buffer.from(signedPayload, "utf8")) : null;
|
|
229
|
+
var sigB64 = sig ? (Buffer.isBuffer(sig) ? sig.toString("base64") : String(sig)) : null;
|
|
230
|
+
var out = {
|
|
231
|
+
printedName: payload.printedName,
|
|
232
|
+
dateTimeUtc: payload.dateTimeUtc,
|
|
233
|
+
signatureMeaning: payload.signatureMeaning,
|
|
234
|
+
predicateRule: payload.predicateRule,
|
|
235
|
+
recordHash: payload.recordHash,
|
|
236
|
+
signatureRecord: signatureRecord,
|
|
237
|
+
signature: sigB64,
|
|
238
|
+
};
|
|
239
|
+
_emit("fda21cfr11.signature.created", {
|
|
240
|
+
printedName: out.printedName,
|
|
241
|
+
signatureMeaning: out.signatureMeaning,
|
|
242
|
+
predicateRule: out.predicateRule,
|
|
243
|
+
recordHash: out.recordHash,
|
|
244
|
+
signatureRecord: out.signatureRecord,
|
|
245
|
+
});
|
|
246
|
+
return out;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function verifySignature(signed, boundRecord) {
|
|
250
|
+
if (!signed || typeof signed !== "object") {
|
|
251
|
+
throw new Fda21Cfr11Error("fda21cfr11/bad-verify-input",
|
|
252
|
+
"electronicSignature.verify: signed must be a signature object");
|
|
253
|
+
}
|
|
254
|
+
var expectedHash = _toRecordHash(boundRecord);
|
|
255
|
+
if (signed.recordHash !== expectedHash) {
|
|
256
|
+
_emit("fda21cfr11.signature.verified", {
|
|
257
|
+
printedName: signed.printedName, ok: false,
|
|
258
|
+
reason: "record-hash-mismatch",
|
|
259
|
+
}, "denied");
|
|
260
|
+
return { ok: false, reason: "record-hash-mismatch" };
|
|
261
|
+
}
|
|
262
|
+
if (verifyWith && signed.signature) {
|
|
263
|
+
var sigBuf = Buffer.from(signed.signature, "base64");
|
|
264
|
+
var payload = JSON.stringify({
|
|
265
|
+
printedName: signed.printedName,
|
|
266
|
+
dateTimeUtc: signed.dateTimeUtc,
|
|
267
|
+
signatureMeaning: signed.signatureMeaning,
|
|
268
|
+
predicateRule: signed.predicateRule,
|
|
269
|
+
recordHash: signed.recordHash,
|
|
270
|
+
});
|
|
271
|
+
var ok;
|
|
272
|
+
try { ok = !!verifyWith(Buffer.from(payload, "utf8"), sigBuf); }
|
|
273
|
+
catch (_e) { ok = false; }
|
|
274
|
+
_emit("fda21cfr11.signature.verified", {
|
|
275
|
+
printedName: signed.printedName, ok: ok,
|
|
276
|
+
}, ok ? "success" : "denied");
|
|
277
|
+
return { ok: ok, reason: ok ? null : "signature-verify-failed" };
|
|
278
|
+
}
|
|
279
|
+
_emit("fda21cfr11.signature.verified", {
|
|
280
|
+
printedName: signed.printedName, ok: true,
|
|
281
|
+
});
|
|
282
|
+
return { ok: true };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function assertGxpAudit(row) {
|
|
286
|
+
var rv = _hasRequiredAuditShape(row);
|
|
287
|
+
if (!rv.ok) {
|
|
288
|
+
_emit("fda21cfr11.gxp.assert_failed", {
|
|
289
|
+
action: row && row.action, reason: rv.reason,
|
|
290
|
+
}, "denied");
|
|
291
|
+
throw new Fda21Cfr11Error("fda21cfr11/gxp-shape-violation",
|
|
292
|
+
"21 CFR 11.10(e) audit shape violation: " + rv.reason);
|
|
293
|
+
}
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function checkGxpAudit(row) {
|
|
298
|
+
return _hasRequiredAuditShape(row);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Posture interceptor — wraps b.audit.safeEmit so events on GxP
|
|
302
|
+
// namespaces refuse-with-audit when their shape is incomplete.
|
|
303
|
+
// Returns an `{ uninstall }` handle so tests / operator teardown
|
|
304
|
+
// can detach.
|
|
305
|
+
var _installed = false;
|
|
306
|
+
var _originalSafeEmit = null;
|
|
307
|
+
|
|
308
|
+
function install() {
|
|
309
|
+
if (_installed) return { uninstall: uninstall };
|
|
310
|
+
if (!interceptAudit) return { uninstall: function () {} };
|
|
311
|
+
var auditMod = audit();
|
|
312
|
+
_originalSafeEmit = auditMod.safeEmit;
|
|
313
|
+
auditMod.safeEmit = function _gxpInterceptedSafeEmit(event) {
|
|
314
|
+
if (!event || typeof event !== "object" || typeof event.action !== "string") {
|
|
315
|
+
return _originalSafeEmit.call(auditMod, event);
|
|
316
|
+
}
|
|
317
|
+
var ns = event.action.split(".")[0];
|
|
318
|
+
if (gxpNamespaces.indexOf(ns) === -1) {
|
|
319
|
+
return _originalSafeEmit.call(auditMod, event);
|
|
320
|
+
}
|
|
321
|
+
var rv = _hasRequiredAuditShape(event);
|
|
322
|
+
if (rv.ok) {
|
|
323
|
+
return _originalSafeEmit.call(auditMod, event);
|
|
324
|
+
}
|
|
325
|
+
// Refusal — audit the refusal so the chain shows the violation,
|
|
326
|
+
// but DON'T propagate the malformed event into the chain.
|
|
327
|
+
try {
|
|
328
|
+
_originalSafeEmit.call(auditMod, {
|
|
329
|
+
action: "fda21cfr11.audit.refused",
|
|
330
|
+
outcome: "denied",
|
|
331
|
+
metadata: {
|
|
332
|
+
attempted: event.action,
|
|
333
|
+
reason: rv.reason,
|
|
334
|
+
},
|
|
335
|
+
});
|
|
336
|
+
} catch (_e) { /* drop-silent */ }
|
|
337
|
+
};
|
|
338
|
+
_installed = true;
|
|
339
|
+
_emit("fda21cfr11.posture.installed", { gxpNamespaces: gxpNamespaces });
|
|
340
|
+
return { uninstall: uninstall };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function uninstall() {
|
|
344
|
+
if (!_installed || !_originalSafeEmit) return;
|
|
345
|
+
var auditMod = audit();
|
|
346
|
+
auditMod.safeEmit = _originalSafeEmit;
|
|
347
|
+
_originalSafeEmit = null;
|
|
348
|
+
_installed = false;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
electronicSignature: {
|
|
353
|
+
create: createSignature,
|
|
354
|
+
verify: verifySignature,
|
|
355
|
+
MEANINGS: meanings.slice(),
|
|
356
|
+
},
|
|
357
|
+
assertGxpAudit: assertGxpAudit,
|
|
358
|
+
checkGxpAudit: checkGxpAudit,
|
|
359
|
+
install: install,
|
|
360
|
+
uninstall: uninstall,
|
|
361
|
+
gxpNamespaces: gxpNamespaces.slice(),
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Module-level convenience for operators who don't need a posture
|
|
366
|
+
// instance — wires audit.safeEmit + Date.now and exposes the same
|
|
367
|
+
// surface via the singleton form.
|
|
368
|
+
var _singleton = null;
|
|
369
|
+
function _getSingleton() {
|
|
370
|
+
if (_singleton) return _singleton;
|
|
371
|
+
_singleton = posture({ audit: audit(), interceptAudit: false });
|
|
372
|
+
return _singleton;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function _resetForTest() {
|
|
376
|
+
if (_singleton) {
|
|
377
|
+
try { _singleton.uninstall(); } catch (_e) { /* best-effort */ }
|
|
378
|
+
}
|
|
379
|
+
_singleton = null;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
module.exports = {
|
|
383
|
+
posture: posture,
|
|
384
|
+
electronicSignature: {
|
|
385
|
+
create: function (input) { return _getSingleton().electronicSignature.create(input); },
|
|
386
|
+
verify: function (signed, record) { return _getSingleton().electronicSignature.verify(signed, record); },
|
|
387
|
+
MEANINGS: DEFAULT_SIGNATURE_MEANINGS.slice(),
|
|
388
|
+
},
|
|
389
|
+
assertGxpAudit: function (row) { return _getSingleton().assertGxpAudit(row); },
|
|
390
|
+
checkGxpAudit: function (row) { return _getSingleton().checkGxpAudit(row); },
|
|
391
|
+
DEFAULT_SIGNATURE_MEANINGS: DEFAULT_SIGNATURE_MEANINGS,
|
|
392
|
+
DEFAULT_GXP_NAMESPACES: DEFAULT_GXP_NAMESPACES,
|
|
393
|
+
Fda21Cfr11Error: Fda21Cfr11Error,
|
|
394
|
+
_resetForTest: _resetForTest,
|
|
395
|
+
};
|