@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.
Files changed (222) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/README.md +10 -10
  3. package/index.js +52 -0
  4. package/lib/a2a.js +159 -34
  5. package/lib/acme.js +762 -0
  6. package/lib/ai-pref.js +166 -43
  7. package/lib/api-key.js +108 -47
  8. package/lib/api-snapshot.js +157 -40
  9. package/lib/app-shutdown.js +113 -77
  10. package/lib/archive.js +337 -40
  11. package/lib/arg-parser.js +697 -0
  12. package/lib/asyncapi.js +99 -55
  13. package/lib/atomic-file.js +465 -104
  14. package/lib/audit-chain.js +123 -34
  15. package/lib/audit-daily-review.js +389 -0
  16. package/lib/audit-sign.js +302 -56
  17. package/lib/audit-tools.js +412 -63
  18. package/lib/audit.js +656 -35
  19. package/lib/auth/jwt-external.js +17 -0
  20. package/lib/auth/oauth.js +7 -0
  21. package/lib/auth-bot-challenge.js +505 -0
  22. package/lib/auth-header.js +92 -25
  23. package/lib/backup/bundle.js +26 -0
  24. package/lib/backup/index.js +512 -89
  25. package/lib/backup/manifest.js +168 -7
  26. package/lib/break-glass.js +415 -39
  27. package/lib/budr.js +103 -30
  28. package/lib/bundler.js +86 -66
  29. package/lib/cache.js +192 -72
  30. package/lib/chain-writer.js +65 -40
  31. package/lib/circuit-breaker.js +56 -33
  32. package/lib/cli-helpers.js +106 -75
  33. package/lib/cli.js +6 -30
  34. package/lib/cloud-events.js +99 -32
  35. package/lib/cluster-storage.js +162 -37
  36. package/lib/cluster.js +340 -49
  37. package/lib/codepoint-class.js +66 -0
  38. package/lib/compliance.js +424 -24
  39. package/lib/config-drift.js +111 -46
  40. package/lib/config.js +94 -40
  41. package/lib/consent.js +165 -18
  42. package/lib/constants.js +1 -0
  43. package/lib/content-credentials.js +153 -48
  44. package/lib/cookies.js +154 -62
  45. package/lib/credential-hash.js +133 -61
  46. package/lib/crypto-field.js +702 -18
  47. package/lib/crypto-hpke.js +256 -0
  48. package/lib/crypto.js +744 -22
  49. package/lib/csv.js +178 -35
  50. package/lib/daemon.js +456 -0
  51. package/lib/dark-patterns.js +186 -55
  52. package/lib/db-query.js +79 -2
  53. package/lib/db.js +1431 -60
  54. package/lib/ddl-change-control.js +523 -0
  55. package/lib/deprecate.js +195 -40
  56. package/lib/dev.js +82 -39
  57. package/lib/dora.js +67 -48
  58. package/lib/dr-runbook.js +368 -0
  59. package/lib/dsr.js +142 -11
  60. package/lib/dual-control.js +91 -56
  61. package/lib/events.js +120 -41
  62. package/lib/external-db-migrate.js +192 -2
  63. package/lib/external-db.js +795 -50
  64. package/lib/fapi2.js +122 -1
  65. package/lib/fda-21cfr11.js +395 -0
  66. package/lib/fdx.js +132 -2
  67. package/lib/file-type.js +87 -0
  68. package/lib/file-upload.js +93 -0
  69. package/lib/flag.js +82 -20
  70. package/lib/forms.js +132 -29
  71. package/lib/framework-error.js +169 -0
  72. package/lib/framework-schema.js +163 -35
  73. package/lib/gate-contract.js +849 -175
  74. package/lib/graphql-federation.js +68 -7
  75. package/lib/guard-all.js +172 -55
  76. package/lib/guard-archive.js +286 -124
  77. package/lib/guard-auth.js +194 -21
  78. package/lib/guard-cidr.js +190 -28
  79. package/lib/guard-csv.js +397 -51
  80. package/lib/guard-domain.js +213 -91
  81. package/lib/guard-email.js +236 -29
  82. package/lib/guard-filename.js +307 -75
  83. package/lib/guard-graphql.js +263 -30
  84. package/lib/guard-html.js +310 -116
  85. package/lib/guard-image.js +243 -30
  86. package/lib/guard-json.js +260 -54
  87. package/lib/guard-jsonpath.js +235 -23
  88. package/lib/guard-jwt.js +284 -30
  89. package/lib/guard-markdown.js +204 -22
  90. package/lib/guard-mime.js +190 -26
  91. package/lib/guard-oauth.js +277 -28
  92. package/lib/guard-pdf.js +251 -27
  93. package/lib/guard-regex.js +226 -18
  94. package/lib/guard-shell.js +229 -26
  95. package/lib/guard-svg.js +177 -10
  96. package/lib/guard-template.js +232 -21
  97. package/lib/guard-time.js +195 -29
  98. package/lib/guard-uuid.js +189 -30
  99. package/lib/guard-xml.js +259 -36
  100. package/lib/guard-yaml.js +241 -44
  101. package/lib/honeytoken.js +63 -27
  102. package/lib/html-balance.js +83 -0
  103. package/lib/http-client.js +486 -59
  104. package/lib/http-message-signature.js +582 -0
  105. package/lib/i18n.js +102 -49
  106. package/lib/iab-mspa.js +112 -32
  107. package/lib/iab-tcf.js +107 -2
  108. package/lib/inbox.js +90 -52
  109. package/lib/keychain.js +865 -0
  110. package/lib/legal-hold.js +374 -0
  111. package/lib/local-db-thin.js +320 -0
  112. package/lib/log-stream.js +281 -51
  113. package/lib/log.js +184 -86
  114. package/lib/mail-bounce.js +107 -62
  115. package/lib/mail.js +295 -58
  116. package/lib/mcp.js +108 -27
  117. package/lib/metrics.js +98 -89
  118. package/lib/middleware/age-gate.js +36 -0
  119. package/lib/middleware/ai-act-disclosure.js +37 -0
  120. package/lib/middleware/api-encrypt.js +45 -0
  121. package/lib/middleware/assetlinks.js +40 -0
  122. package/lib/middleware/asyncapi-serve.js +35 -0
  123. package/lib/middleware/attach-user.js +40 -0
  124. package/lib/middleware/bearer-auth.js +40 -0
  125. package/lib/middleware/body-parser.js +230 -0
  126. package/lib/middleware/bot-disclose.js +34 -0
  127. package/lib/middleware/bot-guard.js +39 -0
  128. package/lib/middleware/compression.js +37 -0
  129. package/lib/middleware/cookies.js +32 -0
  130. package/lib/middleware/cors.js +40 -0
  131. package/lib/middleware/csp-nonce.js +40 -0
  132. package/lib/middleware/csp-report.js +34 -0
  133. package/lib/middleware/csrf-protect.js +43 -0
  134. package/lib/middleware/daily-byte-quota.js +53 -85
  135. package/lib/middleware/db-role-for.js +40 -0
  136. package/lib/middleware/dpop.js +40 -0
  137. package/lib/middleware/error-handler.js +37 -14
  138. package/lib/middleware/fetch-metadata.js +39 -0
  139. package/lib/middleware/flag-context.js +34 -0
  140. package/lib/middleware/gpc.js +33 -0
  141. package/lib/middleware/headers.js +35 -0
  142. package/lib/middleware/health.js +46 -0
  143. package/lib/middleware/host-allowlist.js +30 -0
  144. package/lib/middleware/network-allowlist.js +38 -0
  145. package/lib/middleware/openapi-serve.js +34 -0
  146. package/lib/middleware/rate-limit.js +160 -18
  147. package/lib/middleware/request-id.js +36 -18
  148. package/lib/middleware/request-log.js +37 -0
  149. package/lib/middleware/require-aal.js +29 -0
  150. package/lib/middleware/require-auth.js +32 -0
  151. package/lib/middleware/require-bound-key.js +41 -0
  152. package/lib/middleware/require-content-type.js +32 -0
  153. package/lib/middleware/require-methods.js +27 -0
  154. package/lib/middleware/require-mtls.js +33 -0
  155. package/lib/middleware/require-step-up.js +37 -0
  156. package/lib/middleware/security-headers.js +44 -0
  157. package/lib/middleware/security-txt.js +38 -0
  158. package/lib/middleware/span-http-server.js +37 -0
  159. package/lib/middleware/sse.js +36 -0
  160. package/lib/middleware/trace-log-correlation.js +33 -0
  161. package/lib/middleware/trace-propagate.js +32 -0
  162. package/lib/middleware/tus-upload.js +90 -0
  163. package/lib/middleware/web-app-manifest.js +53 -0
  164. package/lib/mtls-ca.js +100 -70
  165. package/lib/network-byte-quota.js +308 -0
  166. package/lib/network-heartbeat.js +135 -0
  167. package/lib/network-tls.js +534 -4
  168. package/lib/network.js +103 -0
  169. package/lib/notify.js +114 -43
  170. package/lib/ntp-check.js +192 -51
  171. package/lib/observability.js +145 -47
  172. package/lib/openapi.js +90 -44
  173. package/lib/outbox.js +99 -1
  174. package/lib/pagination.js +168 -86
  175. package/lib/parsers/index.js +16 -5
  176. package/lib/permissions.js +93 -40
  177. package/lib/pqc-agent.js +84 -8
  178. package/lib/pqc-software.js +94 -60
  179. package/lib/process-spawn.js +95 -21
  180. package/lib/pubsub.js +96 -66
  181. package/lib/queue.js +375 -54
  182. package/lib/redact.js +793 -21
  183. package/lib/render.js +139 -47
  184. package/lib/request-helpers.js +485 -121
  185. package/lib/restore-bundle.js +142 -39
  186. package/lib/restore-rollback.js +136 -45
  187. package/lib/retention.js +178 -50
  188. package/lib/retry.js +116 -33
  189. package/lib/router.js +475 -23
  190. package/lib/safe-async.js +543 -94
  191. package/lib/safe-buffer.js +337 -41
  192. package/lib/safe-json.js +467 -62
  193. package/lib/safe-jsonpath.js +285 -0
  194. package/lib/safe-schema.js +631 -87
  195. package/lib/safe-sql.js +221 -59
  196. package/lib/safe-url.js +278 -46
  197. package/lib/sandbox-worker.js +135 -0
  198. package/lib/sandbox.js +358 -0
  199. package/lib/scheduler.js +135 -70
  200. package/lib/self-update.js +647 -0
  201. package/lib/session-device-binding.js +431 -0
  202. package/lib/session.js +259 -49
  203. package/lib/slug.js +138 -26
  204. package/lib/ssrf-guard.js +316 -56
  205. package/lib/storage.js +433 -70
  206. package/lib/subject.js +405 -23
  207. package/lib/template.js +148 -8
  208. package/lib/tenant-quota.js +545 -0
  209. package/lib/testing.js +440 -53
  210. package/lib/time.js +291 -23
  211. package/lib/tls-exporter.js +239 -0
  212. package/lib/tracing.js +90 -74
  213. package/lib/uuid.js +97 -22
  214. package/lib/vault/index.js +284 -22
  215. package/lib/vault/seal-pem-file.js +66 -0
  216. package/lib/watcher.js +368 -0
  217. package/lib/webhook.js +196 -63
  218. package/lib/websocket.js +393 -68
  219. package/lib/wiki-concepts.js +338 -0
  220. package/lib/worker-pool.js +464 -0
  221. package/package.json +3 -3
  222. package/sbom.cyclonedx.json +7 -7
package/lib/fapi2.js CHANGED
@@ -1,6 +1,44 @@
1
1
  "use strict";
2
2
  /**
3
- * b.fapi2 — Financial-grade API 2.0 Final conformance posture.
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
+ };