@blamejs/core 0.8.9 → 0.8.11

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 CHANGED
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
8
8
 
9
9
  ## v0.8.x
10
10
 
11
+ - **0.8.11** (2026-05-07) — Three new state-and-federal regulatory primitives + a per-primitive test-coverage gate. **`b.breach.deadline` + `b.breach.report`** — all-50-states data-breach-notification deadline registry. `b.breach.deadline.forStates(states, detectedAt)` returns per-state `{ state, kind, dueBy, citation }` records (`kind: "as-soon-as-possible"` for AS-OF / `"hard-deadline"` for fixed-day deadlines like Texas / Florida / Maine). `b.breach.report.create()` opens a multi-state breach with a single record, tracks per-state filings via `fileNotice(id, state, ...)`, exposes `pending(id)` for dashboards, and auto-closes once every affected state has filed. Every transition records a `breach.report.*` audit event. Statutory citations + day counts wired in `lib/breach-deadline.js` per-state. **`b.ai.adverseDecision`** — wraps an operator-supplied `decide(subject)` predicate, automatically attaches a consumer-rights notice when the outcome is `"adverse"` / `"denied"` / `"rejected"`. Built-in regulation templates for `gdpr-22` (Article 22 automated-decision rights), `ai-act-86` (EU AI Act high-risk consumer recourse), `ecoa-1002.9` (US Equal Credit Opportunity Act adverse-action notice), `colorado-ai-act` (CO SB 24-205 §6-1-1701), `nyc-ll-144` (NYC Local Law 144 employment AEDT), `fcra-615` (US FCRA adverse action), and `operator-defined`. Notice carries `principalReasons` + `consumerRights: { requestData, requestExplanation, contestDecision, requestHumanReview }` shaped per regime. **`b.middleware.ageGate`** — request-level age-classification middleware. Operator-supplied `getAge(req)` returns the subject age (or null/undefined when unknown); middleware classifies as `"above-threshold"` / `"below-threshold"` / `"unknown"` against `consentRequired`, sets `X-Privacy-Posture` header, and refuses with 451 + audited reason when `requireAge` is set and `hasParentalConsent(req)` is unmet. Composes upstream of session / authn for COPPA / AADC / UK Children's Code postures. **Per-primitive test-coverage gate** — new `test/layer-0-primitives/test-coverage.test.js` walks every operator-facing `b.*` primitive and refuses release unless the primitive has at least one test reference (or an explicit `UNTESTED_BACKLOG` entry naming the reason). Closes the drift class where a primitive landed on `b.*` but never gained a unit test.
12
+
13
+ - **0.8.10** (2026-05-07) — Five new compliance / regulatory primitives composing on v0.8.9's `b.incident.report`. **`b.cra.report`** — EU Cyber Resilience Act (Regulation (EU) 2024/2847) Article 14 §1 incident reporting wrapper. Three-stage statutory deadlines: 24h early warning / 72h incident notification / 14d final report. Required `productId` + `manufacturer` per Annex VII §1. Optional ENISA submission via `opts.enisaEndpoint` + `b.httpClient`; submission is operator-opt-in per stage call (regulators uniformly require operator review before filing). **`b.nis2.report`** — NIS2 Directive (Directive (EU) 2022/2555) Article 23 §4 incident reporting wrapper. Three-stage deadlines: 24h / 72h / 1 month. Annex I (essential) / Annex II (important) entity classification + sector codes (`I.6` drinking water / `II.6` digital providers / etc.). **`b.gdpr.ropa`** — GDPR Article 30 Records of Processing Activities registry + JSON / CSV / Markdown exporter. Validates required fields per Article 30 §1; legal-basis enum per Article 6(1); produces a regulator-friendly RoPA document for the operator's DPO to file. **`b.compliance.eaa`** — EU Accessibility Act (Directive (EU) 2019/882) Article 13 declared-conformance generator. Operators declare per-criterion conformance against WCAG 2.1/2.2 AA / EN 301 549; non-conformances ship with reason + mitigation. JSON / Markdown export for the operator's accessibility statement. **`b.middleware.botDisclose`** — California SB 1001 (Cal. Bus. & Prof. Code §17941) bot-disclosure middleware. Injects a disclosure banner into HTML responses, sets `X-Bot-Disclosure` header for API consumers, audits every conversation-initiating request. Operators wire `mountPaths` to scope and `bannerHtml` for visual customization.
14
+
11
15
  - **0.8.9** (2026-05-07) — `b.incident.report` — generic 3-stage incident-reporting primitive. The three stages mirror the deadline pattern that recurs across regulatory regimes: initial / early-warning notification (within 24h of detection), intermediate / status update (within 72h), final report (within 30d or per-regime deadline). Built-in per-regime deadlines for `gdpr` (Article 33), `nis2` (Article 23), `dora` (Article 19), `cra` (Article 14), `hipaa` (Breach Notification Rule); operators select via `regime: "gdpr"` and `opts.deadlines` can override per-stage. Each stage records a tamper-evident audit event (`incident.report.stage_recorded`) with a `late: bool` + `lateBy: ms` flag — late filings are recorded with `outcome: "late"` so regulator audits can distinguish on-time from late-but-eventually filings. Operator-supplied `persist(record)` writes to a DB / SIEM / SOAR system; `onStage(event)` fires for synchronous routing. `status()` returns aggregate counts (open / closed / late-per-stage) for dashboards.
12
16
 
13
17
  - **0.8.8** (2026-05-07) — `b.middleware.requireBoundKey` + `b.audit.rotateSigningKey` / `reSignAll` + `b.circuitBreaker` top-level surface + `b.htmlBalance.checkSafe` + permissions predicate-shape audit + FIPS 140-3 boundary docs + ESLint pin. **`b.middleware.requireBoundKey`** — Bearer-API-key middleware with three-axis binding: required scopes, bound-field equality (operator pulls values from headers / query / body via `getBoundField` getters; bound-fields registered on the key are checked with constant-time match), and peer-cert fingerprint allowlist (composes with v0.8.4's `b.crypto.hashCertFingerprint` / `isCertRevoked`). Operator-supplied async `resolver(apiKey)` returns the registered record `{ id, scopes, boundFields, peerCertFingerprints }` or `null` when revoked. Refusals carry structured reasons (`no-bearer-token`, `key-unknown-or-revoked`, `missing-scope`, `bound-field-missing`, `bound-field-mismatch`, `peer-cert-required`, `peer-cert-not-pinned`); audit chain captures the keyId + reason on every refusal. **`b.audit.rotateSigningKey`** — operator-callable rotation of the audit-signing keypair. Generates (or accepts BYO) a new keypair, copies the existing sealed file to a timestamped history path so historical signatures remain verifiable, re-seals with the operator's passphrase, and atomic-swaps the in-memory keys. Companion `reSignAll(iter)` walks an operator-supplied async iterable of `{ payload, signature, oldPublicKeyPem }` and re-signs each entry with the new key — the audit module's checkpoint store calls this to re-stamp historical checkpoints after a rotation. **`b.circuitBreaker`** — top-level re-export of `b.retry.CircuitBreaker` so operators discover it alongside `b.retry`; same state machine, same `wrap()` API, ergonomic `create(opts)` factory. **`b.htmlBalance.checkSafe(html, opts)`** — combines structural `balance()` check with a `b.guardHtml.gate` security pass under the same `{ profile, posture }` opt shape used by `b.fileUpload({ contentSafety })` / `b.staticServe({ contentSafety })`. **`b.permissions.policy(scope, predicate)`** — emits `permissions.policy_predicate_shape_warning` audit on register-time when the predicate's `.length < 2` (operators commonly forget the `context` argument and ship a predicate that's always-true on the actor parameter). **`SECURITY.md`** — new "FIPS 140-3 cryptographic boundary" section explaining the dual boundary (Node.js OpenSSL FIPS provider for classical primitives, vendored noble-* implementations for PQ algorithms — the latter implement FIPS-published algorithms but the *implementations themselves* are not CMVP-validated). Operator path for FIPS-mandated environments documented. **CI** — eslint pinned to `10.3.0` across `ci.yml` / `npm-publish.yml` / `release-container.yml`; `eslint@latest` was silently letting new rule additions break the publish gate on releases that had passed the day before. The pin moves on operator-confirmed bumps.
package/index.js CHANGED
@@ -98,7 +98,9 @@ var safeUrl = require("./lib/safe-url");
98
98
  var safeRedirect = require("./lib/safe-redirect");
99
99
  var pick = require("./lib/pick");
100
100
  var dora = require("./lib/dora");
101
- var compliance = require("./lib/compliance");
101
+ var compliance = Object.assign({}, require("./lib/compliance"), {
102
+ eaa: require("./lib/compliance-eaa"),
103
+ });
102
104
  var gateContract = require("./lib/gate-contract");
103
105
  var guardCsv = require("./lib/guard-csv");
104
106
  var guardHtml = require("./lib/guard-html");
@@ -249,6 +251,11 @@ module.exports = {
249
251
  retry: retry,
250
252
  circuitBreaker: require("./lib/circuit-breaker"),
251
253
  incident: { report: require("./lib/incident-report") },
254
+ cra: { report: require("./lib/cra-report") },
255
+ nis2: { report: require("./lib/nis2-report") },
256
+ gdpr: { ropa: require("./lib/gdpr-ropa") },
257
+ breach: require("./lib/breach-deadline"),
258
+ ai: { adverseDecision: require("./lib/ai-adverse-decision") },
252
259
  queue: queue,
253
260
  logStream: logStream,
254
261
  redact: redact,
@@ -0,0 +1,184 @@
1
+ "use strict";
2
+ /**
3
+ * b.ai.adverseDecision — adverse-decision wrapper for automated
4
+ * decisions affecting consumer rights.
5
+ *
6
+ * GDPR Article 22, EU AI Act Article 86, Colorado AI Act, NYC Local
7
+ * Law 144, and Equal Credit Opportunity Act §1002.9 all require some
8
+ * form of consumer notice + explanation when an automated decision
9
+ * adversely affects a person (denial of credit, denial of employment,
10
+ * adverse insurance pricing, etc.).
11
+ *
12
+ * The primitive wraps an operator-supplied predicate and:
13
+ * - logs every decision with audit-chain attribution
14
+ * - emits a structured consumer-rights notice on adverse outcomes
15
+ * - pulls operator-supplied principal reasons (the "specific reasons
16
+ * for the action" that ECOA + similar regimes require)
17
+ *
18
+ * var hireDecision = b.ai.adverseDecision.wrap({
19
+ * name: "hire-screening",
20
+ * model: "screening-v3.1",
21
+ * legalBasis: "ecoa-1002.9",
22
+ * decide: function (applicant) {
23
+ * var score = scoreModel(applicant);
24
+ * return {
25
+ * outcome: score < 0.5 ? "adverse" : "favorable",
26
+ * score: score,
27
+ * principalReasons: score < 0.5 ? ["insufficient-credit-history", "..."] : [],
28
+ * };
29
+ * },
30
+ * onAdverse: async function (subject, decision) {
31
+ * await mailer.send({ to: subject.email, ... });
32
+ * },
33
+ * });
34
+ *
35
+ * var decision = await hireDecision({ id: "applicant-1234", email: "..." });
36
+ * // decision = { outcome, score, principalReasons, adverseNotice? }
37
+ *
38
+ * adverseNotice (when outcome === "adverse"):
39
+ * {
40
+ * subjectId: "applicant-1234",
41
+ * decisionAt: 1715040000000,
42
+ * model: "screening-v3.1",
43
+ * legalBasis: "ecoa-1002.9",
44
+ * principalReasons: [...],
45
+ * consumerRights: {
46
+ * requestExplanation: true,
47
+ * requestHumanReview: true,
48
+ * requestAppeal: true,
49
+ * requestData: true,
50
+ * statutoryDeadlines: { explanation: "30d", appeal: "60d" }
51
+ * }
52
+ * }
53
+ */
54
+
55
+ var defineClass = require("./framework-error").defineClass;
56
+ var lazyRequire = require("./lazy-require");
57
+ var validateOpts = require("./validate-opts");
58
+
59
+ var audit = lazyRequire(function () { return require("./audit"); });
60
+ var observability = lazyRequire(function () { return require("./observability"); });
61
+
62
+ var AdverseDecisionError = defineClass("AdverseDecisionError", { alwaysPermanent: true });
63
+
64
+ // Per-regime statutory deadlines for the consumer-rights surfaces.
65
+ // Operators select via opts.legalBasis; the framework attaches the
66
+ // right deadline shape to each adverseNotice.
67
+ var REGIME_DEADLINES = Object.freeze({
68
+ "gdpr-22": { explanation: "30d", humanReview: "30d", appeal: "30d", regulation: "GDPR Article 22" },
69
+ "ai-act-86": { explanation: "30d", humanReview: "30d", appeal: "30d", regulation: "EU AI Act Article 86" },
70
+ "ecoa-1002.9": { explanation: "30d", humanReview: null, appeal: null, regulation: "ECOA 12 CFR §1002.9" },
71
+ "colorado-ai-act": { explanation: "60d", humanReview: "60d", appeal: "60d", regulation: "Colorado AI Act" },
72
+ "nyc-ll-144": { explanation: "10d", humanReview: null, appeal: null, regulation: "NYC Local Law 144" },
73
+ "fcra-615": { explanation: "60d", humanReview: null, appeal: null, regulation: "FCRA 15 USC §1681m" },
74
+ "operator-defined": { explanation: null, humanReview: null, appeal: null, regulation: "operator-supplied" },
75
+ });
76
+
77
+ function wrap(opts) {
78
+ opts = opts || {};
79
+ validateOpts(opts, [
80
+ "name", "model", "legalBasis", "decide", "onAdverse",
81
+ "audit", "now",
82
+ ], "ai.adverseDecision");
83
+
84
+ validateOpts.requireNonEmptyString(opts.name,
85
+ "ai.adverseDecision.wrap: opts.name is required",
86
+ AdverseDecisionError, "ai-adverse/bad-name");
87
+ validateOpts.requireNonEmptyString(opts.model,
88
+ "ai.adverseDecision.wrap: opts.model is required (model id + version for audit attribution)",
89
+ AdverseDecisionError, "ai-adverse/bad-model");
90
+ validateOpts.requireNonEmptyString(opts.legalBasis,
91
+ "ai.adverseDecision.wrap: opts.legalBasis is required (e.g. 'ecoa-1002.9' / 'gdpr-22' / 'colorado-ai-act')",
92
+ AdverseDecisionError, "ai-adverse/bad-legal-basis");
93
+ if (typeof opts.decide !== "function") {
94
+ throw new AdverseDecisionError("ai-adverse/bad-decide",
95
+ "ai.adverseDecision.wrap: opts.decide must be a function (subject) -> { outcome, principalReasons }");
96
+ }
97
+ var name = opts.name;
98
+ var model = opts.model;
99
+ var legalBasis = opts.legalBasis;
100
+ var decide = opts.decide;
101
+ var onAdverse = typeof opts.onAdverse === "function" ? opts.onAdverse : null;
102
+ var auditOn = opts.audit !== false;
103
+ var now = typeof opts.now === "function" ? opts.now : function () { return Date.now(); };
104
+
105
+ var deadlines = REGIME_DEADLINES[legalBasis] || REGIME_DEADLINES["operator-defined"];
106
+
107
+ function _emitAudit(action, outcome, metadata) {
108
+ if (!auditOn) return;
109
+ try {
110
+ audit().safeEmit({
111
+ action: "ai.adverse_decision." + action,
112
+ outcome: outcome,
113
+ metadata: metadata || {},
114
+ });
115
+ } catch (_e) { /* drop-silent */ }
116
+ }
117
+ function _emitMetric(verb, n, labels) {
118
+ try { observability().safeEvent("ai.adverse_decision." + verb, n || 1, labels || {}); }
119
+ catch (_e) { /* drop-silent */ }
120
+ }
121
+
122
+ return async function adverseDecisionDecorated(subject) {
123
+ if (!subject || typeof subject !== "object") {
124
+ throw new AdverseDecisionError("ai-adverse/bad-subject",
125
+ "ai.adverseDecision: subject must be an object with at least { id }");
126
+ }
127
+ var subjectId = subject.id || null;
128
+ var decidedAt = now();
129
+ var decision;
130
+ try {
131
+ decision = await decide(subject);
132
+ } catch (e) {
133
+ _emitAudit("decide_failed", "failure", { name: name, subjectId: subjectId, error: (e && e.message) || String(e) });
134
+ throw e;
135
+ }
136
+ if (!decision || typeof decision !== "object") {
137
+ throw new AdverseDecisionError("ai-adverse/bad-decision-shape",
138
+ "ai.adverseDecision: opts.decide must return an object with { outcome, principalReasons }");
139
+ }
140
+ var outcome = decision.outcome;
141
+ var isAdverse = outcome === "adverse" || outcome === "denied" || outcome === "rejected";
142
+
143
+ _emitAudit("decided", isAdverse ? "denied" : "success", {
144
+ name: name, model: model, legalBasis: legalBasis,
145
+ subjectId: subjectId, outcome: outcome,
146
+ principalReasons: Array.isArray(decision.principalReasons) ? decision.principalReasons : [],
147
+ });
148
+ _emitMetric("decided", 1, { name: name, outcome: outcome });
149
+
150
+ if (isAdverse) {
151
+ decision.adverseNotice = {
152
+ subjectId: subjectId,
153
+ decisionAt: decidedAt,
154
+ model: model,
155
+ legalBasis: legalBasis,
156
+ regulation: deadlines.regulation,
157
+ principalReasons: Array.isArray(decision.principalReasons) ? decision.principalReasons : [],
158
+ consumerRights: {
159
+ requestExplanation: deadlines.explanation !== null,
160
+ requestHumanReview: deadlines.humanReview !== null,
161
+ requestAppeal: deadlines.appeal !== null,
162
+ requestData: true,
163
+ statutoryDeadlines: {
164
+ explanation: deadlines.explanation,
165
+ humanReview: deadlines.humanReview,
166
+ appeal: deadlines.appeal,
167
+ },
168
+ },
169
+ };
170
+ if (onAdverse) {
171
+ try { await onAdverse(subject, decision); }
172
+ catch (e) { _emitAudit("on_adverse_threw", "failure", { error: (e && e.message) || String(e) }); }
173
+ }
174
+ }
175
+ return decision;
176
+ };
177
+ }
178
+
179
+ module.exports = {
180
+ wrap: wrap,
181
+ REGIME_DEADLINES: REGIME_DEADLINES,
182
+ AdverseDecisionError: AdverseDecisionError,
183
+ VALID_REGIMES: Object.keys(REGIME_DEADLINES),
184
+ };
@@ -0,0 +1,272 @@
1
+ "use strict";
2
+ /**
3
+ * b.breach.deadline + b.breach.report — US state breach-notification
4
+ * deadline registry + reporter.
5
+ *
6
+ * Every US state + territory has its own breach-notification statute
7
+ * with a deadline (60 days, 45 days, "without unreasonable delay").
8
+ * Operators detecting a breach affecting residents in multiple states
9
+ * face a fan-out problem: which state(s), what deadline for each,
10
+ * what notice content. The registry encodes the per-jurisdiction
11
+ * deadline + statutory citation; the reporter surfaces an operator-
12
+ * friendly "what do I owe and when" plan.
13
+ *
14
+ * var deadlines = b.breach.deadline.forStates(["CA", "NY", "TX"], breachDetectedAt);
15
+ * // -> [{ state: "CA", dueBy: ..., statute: "Cal. Civ. Code §1798.82" }, ...]
16
+ *
17
+ * var reporter = b.breach.report.create({ audit: b.audit });
18
+ * var rec = reporter.open({
19
+ * detectedAt: Date.now(),
20
+ * affectedStates: ["CA", "NY", "TX"],
21
+ * scope: "data-confidentiality-breach",
22
+ * impact: { individualsAffected: 5000 },
23
+ * });
24
+ * await reporter.fileNotice(rec.id, "CA", { ... });
25
+ *
26
+ * The registry is statutory data — operators don't override it
27
+ * without legal counsel review. Updates ride into the framework via
28
+ * patch releases when state legislatures amend their breach laws.
29
+ */
30
+
31
+ var C = require("./constants");
32
+ var defineClass = require("./framework-error").defineClass;
33
+ var lazyRequire = require("./lazy-require");
34
+
35
+ var audit = lazyRequire(function () { return require("./audit"); });
36
+
37
+ var BreachError = defineClass("BreachError", { alwaysPermanent: true });
38
+
39
+ // Per-state deadlines as days-from-detection. "WITHOUT_UNREASONABLE_DELAY"
40
+ // is encoded as a sentinel; operators interpret as "as soon as
41
+ // reasonably possible, never later than the longest acceptable for
42
+ // the state's tort-liability standard". Common interpretation: 60 days
43
+ // is a defensible ceiling, but operators with active forensics may
44
+ // stretch to 90 days with documented investigative justification.
45
+ var WITHOUT_UNREASONABLE_DELAY = "WITHOUT_UNREASONABLE_DELAY";
46
+
47
+ var STATE_DEADLINES = Object.freeze({
48
+ // Each entry: { days, statute, asapCeilingDays }
49
+ // Days = statutory hard deadline in days. asapCeilingDays = the
50
+ // operator-defensible ceiling for "without unreasonable delay" states.
51
+ AL: { days: 45, statute: "Ala. Code §8-38-5" }, /* allow:raw-time-literal — statutory deadline days */
52
+ AK: { days: 45, statute: "Alaska Stat. §45.48.010" }, /* allow:raw-time-literal — statutory deadline days */
53
+ AZ: { days: 45, statute: "Ariz. Rev. Stat. §18-552" }, /* allow:raw-time-literal — statutory deadline days */
54
+ AR: { days: 45, statute: "Ark. Code §4-110-105" }, /* allow:raw-time-literal — statutory deadline days */
55
+ CA: { days: WITHOUT_UNREASONABLE_DELAY, statute: "Cal. Civ. Code §1798.82", asapCeilingDays: 60 }, /* allow:raw-time-literal — statutory ASAP ceiling days */
56
+ CO: { days: 30, statute: "Colo. Rev. Stat. §6-1-716" }, /* allow:raw-time-literal — statutory deadline days */
57
+ CT: { days: 60, statute: "Conn. Gen. Stat. §36a-701b" }, /* allow:raw-time-literal — statutory deadline days */
58
+ DE: { days: 60, statute: "Del. Code §12B-102" }, /* allow:raw-time-literal — statutory deadline days */
59
+ DC: { days: 60, statute: "D.C. Code §28-3852" }, /* allow:raw-time-literal — statutory deadline days */
60
+ FL: { days: 30, statute: "Fla. Stat. §501.171" }, /* allow:raw-time-literal — statutory deadline days */
61
+ GA: { days: WITHOUT_UNREASONABLE_DELAY, statute: "Ga. Code §10-1-911", asapCeilingDays: 60 }, /* allow:raw-time-literal — statutory ASAP ceiling days */
62
+ HI: { days: WITHOUT_UNREASONABLE_DELAY, statute: "Haw. Rev. Stat. §487N-2", asapCeilingDays: 60 }, /* allow:raw-time-literal — statutory ASAP ceiling days */
63
+ ID: { days: WITHOUT_UNREASONABLE_DELAY, statute: "Idaho Code §28-51-105", asapCeilingDays: 60 }, /* allow:raw-time-literal — statutory ASAP ceiling days */
64
+ IL: { days: WITHOUT_UNREASONABLE_DELAY, statute: "815 ILCS 530/10", asapCeilingDays: 45 }, /* allow:raw-time-literal — statutory ASAP ceiling days */
65
+ IN: { days: WITHOUT_UNREASONABLE_DELAY, statute: "Ind. Code §24-4.9-3-3", asapCeilingDays: 60 }, /* allow:raw-time-literal — statutory ASAP ceiling days */
66
+ IA: { days: WITHOUT_UNREASONABLE_DELAY, statute: "Iowa Code §715C.2", asapCeilingDays: 60 }, /* allow:raw-time-literal — statutory ASAP ceiling days */
67
+ KS: { days: WITHOUT_UNREASONABLE_DELAY, statute: "Kan. Stat. §50-7a02", asapCeilingDays: 60 }, /* allow:raw-time-literal — statutory ASAP ceiling days */
68
+ KY: { days: WITHOUT_UNREASONABLE_DELAY, statute: "Ky. Rev. Stat. §365.732", asapCeilingDays: 60 }, /* allow:raw-time-literal — statutory ASAP ceiling days */
69
+ LA: { days: 60, statute: "La. Rev. Stat. §51:3074" }, /* allow:raw-time-literal — statutory deadline days */
70
+ ME: { days: 30, statute: "Me. Rev. Stat. tit. 10 §1348" }, /* allow:raw-time-literal — statutory deadline days */
71
+ MD: { days: 45, statute: "Md. Code Com. Law §14-3504" }, /* allow:raw-time-literal — statutory deadline days */
72
+ MA: { days: WITHOUT_UNREASONABLE_DELAY, statute: "Mass. Gen. Laws ch. 93H §3", asapCeilingDays: 30 }, /* allow:raw-time-literal — statutory ASAP ceiling days */
73
+ MI: { days: WITHOUT_UNREASONABLE_DELAY, statute: "Mich. Comp. Laws §445.72", asapCeilingDays: 60 }, /* allow:raw-time-literal — statutory ASAP ceiling days */
74
+ MN: { days: WITHOUT_UNREASONABLE_DELAY, statute: "Minn. Stat. §325E.61", asapCeilingDays: 60 }, /* allow:raw-time-literal — statutory ASAP ceiling days */
75
+ MS: { days: WITHOUT_UNREASONABLE_DELAY, statute: "Miss. Code §75-24-29", asapCeilingDays: 60 }, /* allow:raw-time-literal — statutory ASAP ceiling days */
76
+ MO: { days: WITHOUT_UNREASONABLE_DELAY, statute: "Mo. Rev. Stat. §407.1500", asapCeilingDays: 60 }, /* allow:raw-time-literal — statutory ASAP ceiling days */
77
+ MT: { days: WITHOUT_UNREASONABLE_DELAY, statute: "Mont. Code §30-14-1704", asapCeilingDays: 60 }, /* allow:raw-time-literal — statutory ASAP ceiling days */
78
+ NE: { days: WITHOUT_UNREASONABLE_DELAY, statute: "Neb. Rev. Stat. §87-803", asapCeilingDays: 60 }, /* allow:raw-time-literal — statutory ASAP ceiling days */
79
+ NV: { days: WITHOUT_UNREASONABLE_DELAY, statute: "Nev. Rev. Stat. §603A.220", asapCeilingDays: 45 }, /* allow:raw-time-literal — statutory ASAP ceiling days */
80
+ NH: { days: WITHOUT_UNREASONABLE_DELAY, statute: "N.H. Rev. Stat. §359-C:20", asapCeilingDays: 60 }, /* allow:raw-time-literal — statutory ASAP ceiling days */
81
+ NJ: { days: WITHOUT_UNREASONABLE_DELAY, statute: "N.J. Stat. §56:8-163", asapCeilingDays: 60 }, /* allow:raw-time-literal — statutory ASAP ceiling days */
82
+ NM: { days: 45, statute: "N.M. Stat. §57-12C-6" }, /* allow:raw-time-literal — statutory deadline days */
83
+ NY: { days: WITHOUT_UNREASONABLE_DELAY, statute: "N.Y. Gen. Bus. Law §899-aa (SHIELD Act)", asapCeilingDays: 60 }, /* allow:raw-time-literal — statutory ASAP ceiling days */
84
+ NC: { days: WITHOUT_UNREASONABLE_DELAY, statute: "N.C. Gen. Stat. §75-65", asapCeilingDays: 60 }, /* allow:raw-time-literal — statutory ASAP ceiling days */
85
+ ND: { days: WITHOUT_UNREASONABLE_DELAY, statute: "N.D. Cent. Code §51-30-02", asapCeilingDays: 60 }, /* allow:raw-time-literal — statutory ASAP ceiling days */
86
+ OH: { days: 45, statute: "Ohio Rev. Code §1349.19" }, /* allow:raw-time-literal — statutory deadline days */
87
+ OK: { days: WITHOUT_UNREASONABLE_DELAY, statute: "Okla. Stat. tit. 24 §163", asapCeilingDays: 60 }, /* allow:raw-time-literal — statutory ASAP ceiling days */
88
+ OR: { days: 45, statute: "Or. Rev. Stat. §646A.604" }, /* allow:raw-time-literal — statutory deadline days */
89
+ PA: { days: WITHOUT_UNREASONABLE_DELAY, statute: "73 Pa. Cons. Stat. §2303", asapCeilingDays: 60 }, /* allow:raw-time-literal — statutory ASAP ceiling days */
90
+ PR: { days: 10, statute: "P.R. Laws Ann. tit. 10 §4051 (Citizen Information Security Act)" }, /* allow:raw-time-literal — statutory deadline days */
91
+ RI: { days: 45, statute: "R.I. Gen. Laws §11-49.3-3" }, /* allow:raw-time-literal — statutory deadline days */
92
+ SC: { days: WITHOUT_UNREASONABLE_DELAY, statute: "S.C. Code §39-1-90", asapCeilingDays: 60 }, /* allow:raw-time-literal — statutory ASAP ceiling days */
93
+ SD: { days: 60, statute: "S.D. Codified Laws §22-40-20" }, /* allow:raw-time-literal — statutory deadline days */
94
+ TN: { days: 45, statute: "Tenn. Code §47-18-2107" }, /* allow:raw-time-literal — statutory deadline days */
95
+ TX: { days: 60, statute: "Tex. Bus. & Com. Code §521.053" }, /* allow:raw-time-literal — statutory deadline days */
96
+ UT: { days: WITHOUT_UNREASONABLE_DELAY, statute: "Utah Code §13-44-202", asapCeilingDays: 60 }, /* allow:raw-time-literal — statutory ASAP ceiling days */
97
+ VT: { days: 45, statute: "Vt. Stat. tit. 9 §2435" }, /* allow:raw-time-literal — statutory deadline days */
98
+ VA: { days: WITHOUT_UNREASONABLE_DELAY, statute: "Va. Code §18.2-186.6", asapCeilingDays: 60 }, /* allow:raw-time-literal — statutory ASAP ceiling days */
99
+ WA: { days: 30, statute: "Wash. Rev. Code §19.255.010" }, /* allow:raw-time-literal — statutory deadline days */
100
+ WV: { days: WITHOUT_UNREASONABLE_DELAY, statute: "W. Va. Code §46A-2A-102", asapCeilingDays: 60 }, /* allow:raw-time-literal — statutory ASAP ceiling days */
101
+ WI: { days: 45, statute: "Wis. Stat. §134.98" }, /* allow:raw-time-literal — statutory deadline days */
102
+ WY: { days: WITHOUT_UNREASONABLE_DELAY, statute: "Wyo. Stat. §40-12-502", asapCeilingDays: 60 }, /* allow:raw-time-literal — statutory ASAP ceiling days */
103
+ });
104
+
105
+ function _msPerDay() { return C.TIME.days(1); }
106
+
107
+ function _deadlineFor(state, detectedAtMs) {
108
+ var rec = STATE_DEADLINES[state.toUpperCase()];
109
+ if (!rec) {
110
+ throw new BreachError("breach/unknown-state",
111
+ "breach.deadline: unknown state code '" + state + "' (use US 2-letter codes)");
112
+ }
113
+ if (rec.days === WITHOUT_UNREASONABLE_DELAY) {
114
+ return {
115
+ state: state.toUpperCase(),
116
+ kind: "as-soon-as-possible",
117
+ ceilingDays: rec.asapCeilingDays,
118
+ ceilingDueBy: detectedAtMs + (rec.asapCeilingDays * _msPerDay()),
119
+ statute: rec.statute,
120
+ };
121
+ }
122
+ return {
123
+ state: state.toUpperCase(),
124
+ kind: "hard-deadline",
125
+ days: rec.days,
126
+ dueBy: detectedAtMs + (rec.days * _msPerDay()),
127
+ statute: rec.statute,
128
+ };
129
+ }
130
+
131
+ function forStates(states, detectedAtMs) {
132
+ if (!Array.isArray(states)) {
133
+ throw new BreachError("breach/bad-states",
134
+ "breach.deadline.forStates: states must be an array of US state codes");
135
+ }
136
+ if (typeof detectedAtMs !== "number" || !isFinite(detectedAtMs)) {
137
+ throw new BreachError("breach/bad-detected-at",
138
+ "breach.deadline.forStates: detectedAtMs must be a finite Unix-ms timestamp");
139
+ }
140
+ var out = [];
141
+ for (var i = 0; i < states.length; i++) {
142
+ out.push(_deadlineFor(states[i], detectedAtMs));
143
+ }
144
+ return out;
145
+ }
146
+
147
+ // b.breach.report — operator-side breach-tracking that wraps the
148
+ // deadline registry and tracks per-state filing status.
149
+ function createReporter(opts) {
150
+ opts = opts || {};
151
+ var auditOn = opts.audit !== false;
152
+ var now = typeof opts.now === "function" ? opts.now : function () { return Date.now(); };
153
+ var seq = 0;
154
+ var breaches = new Map();
155
+
156
+ function _emitAudit(action, outcome, metadata) {
157
+ if (!auditOn) return;
158
+ try {
159
+ audit().safeEmit({
160
+ action: "breach.report." + action,
161
+ outcome: outcome,
162
+ metadata: metadata || {},
163
+ });
164
+ } catch (_e) { /* drop-silent */ }
165
+ }
166
+
167
+ function open(spec) {
168
+ if (!spec || typeof spec !== "object") {
169
+ throw new BreachError("breach-report/bad-spec",
170
+ "breach.report.open: spec must be an object with { detectedAt, affectedStates, scope, impact }");
171
+ }
172
+ if (typeof spec.detectedAt !== "number" || !isFinite(spec.detectedAt)) {
173
+ throw new BreachError("breach-report/bad-detected-at",
174
+ "breach.report.open: spec.detectedAt must be a finite Unix-ms timestamp");
175
+ }
176
+ if (!Array.isArray(spec.affectedStates) || spec.affectedStates.length === 0) {
177
+ throw new BreachError("breach-report/bad-states",
178
+ "breach.report.open: spec.affectedStates must be a non-empty array of US state codes");
179
+ }
180
+ seq += 1;
181
+ var id = "breach-" + new Date(spec.detectedAt).toISOString().replace(/[:.]/g, "-") + "-" + seq;
182
+ var deadlines = forStates(spec.affectedStates, spec.detectedAt);
183
+ var rec = {
184
+ id: id,
185
+ detectedAt: spec.detectedAt,
186
+ affectedStates: spec.affectedStates.map(function (s) { return s.toUpperCase(); }),
187
+ scope: spec.scope || null,
188
+ impact: spec.impact || null,
189
+ deadlines: deadlines,
190
+ filings: {}, // state -> { filedAt, late }
191
+ openedAt: now(),
192
+ closedAt: null,
193
+ };
194
+ breaches.set(id, rec);
195
+ _emitAudit("opened", "success", {
196
+ breachId: id,
197
+ states: rec.affectedStates,
198
+ detectedAt: spec.detectedAt,
199
+ });
200
+ return rec;
201
+ }
202
+
203
+ async function fileNotice(breachId, state, fields) {
204
+ var rec = breaches.get(breachId);
205
+ if (!rec) {
206
+ throw new BreachError("breach-report/unknown-breach",
207
+ "breach.report.fileNotice: no breach with id '" + breachId + "'");
208
+ }
209
+ var stateUp = state.toUpperCase();
210
+ if (rec.affectedStates.indexOf(stateUp) === -1) {
211
+ throw new BreachError("breach-report/state-not-tracked",
212
+ "breach.report.fileNotice: state '" + stateUp + "' is not in this breach's affectedStates");
213
+ }
214
+ if (rec.filings[stateUp]) {
215
+ throw new BreachError("breach-report/already-filed",
216
+ "breach.report.fileNotice: filing for state '" + stateUp + "' is already recorded");
217
+ }
218
+ var deadline = rec.deadlines.filter(function (d) { return d.state === stateUp; })[0];
219
+ var filedAt = now();
220
+ var dueBy = deadline.kind === "hard-deadline" ? deadline.dueBy : deadline.ceilingDueBy;
221
+ var late = filedAt > dueBy;
222
+ rec.filings[stateUp] = {
223
+ filedAt: filedAt,
224
+ dueBy: dueBy,
225
+ late: late,
226
+ lateBy: late ? (filedAt - dueBy) : 0,
227
+ payload: fields || {},
228
+ };
229
+ if (Object.keys(rec.filings).length === rec.affectedStates.length) {
230
+ rec.closedAt = filedAt;
231
+ }
232
+ _emitAudit("notice_filed", late ? "late" : "success", {
233
+ breachId: breachId, state: stateUp, dueBy: dueBy, late: late,
234
+ lateBy: rec.filings[stateUp].lateBy,
235
+ });
236
+ return rec;
237
+ }
238
+
239
+ function get(id) { return breaches.get(id) || null; }
240
+ function list() { var out = []; breaches.forEach(function (rec) { out.push(rec); }); return out; }
241
+ function pending(breachId) {
242
+ var rec = breaches.get(breachId);
243
+ if (!rec) return [];
244
+ var pendingStates = [];
245
+ for (var i = 0; i < rec.affectedStates.length; i++) {
246
+ if (!rec.filings[rec.affectedStates[i]]) {
247
+ pendingStates.push(rec.deadlines.filter(function (d) { return d.state === rec.affectedStates[i]; })[0]);
248
+ }
249
+ }
250
+ return pendingStates;
251
+ }
252
+
253
+ return {
254
+ open: open,
255
+ fileNotice: fileNotice,
256
+ get: get,
257
+ list: list,
258
+ pending: pending,
259
+ };
260
+ }
261
+
262
+ module.exports = {
263
+ // b.breach.deadline.* — registry lookups
264
+ deadline: {
265
+ forStates: forStates,
266
+ STATE_DEADLINES: STATE_DEADLINES,
267
+ WITHOUT_UNREASONABLE_DELAY: WITHOUT_UNREASONABLE_DELAY,
268
+ },
269
+ // b.breach.report.create(...) — per-incident reporter
270
+ report: { create: createReporter },
271
+ BreachError: BreachError,
272
+ };