@blamejs/core 0.8.10 → 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,8 @@ 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
+
11
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.
12
14
 
13
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.
package/index.js CHANGED
@@ -254,6 +254,8 @@ module.exports = {
254
254
  cra: { report: require("./lib/cra-report") },
255
255
  nis2: { report: require("./lib/nis2-report") },
256
256
  gdpr: { ropa: require("./lib/gdpr-ropa") },
257
+ breach: require("./lib/breach-deadline"),
258
+ ai: { adverseDecision: require("./lib/ai-adverse-decision") },
257
259
  queue: queue,
258
260
  logStream: logStream,
259
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
+ };
@@ -0,0 +1,141 @@
1
+ "use strict";
2
+ /**
3
+ * ageGate middleware — request-level age classification + high-privacy
4
+ * default headers for routes that need stricter handling for users
5
+ * below an operator-configured age threshold.
6
+ *
7
+ * COPPA (US, 13 and under), UK Children's Code (16 and under),
8
+ * California AADC (18 and under), and similar regimes require
9
+ * operators to apply heightened privacy protections when serving
10
+ * users below a regulatory age. The middleware:
11
+ *
12
+ * 1. Reads the operator's `getAge(req)` predicate to classify the
13
+ * request as "above-threshold" / "below-threshold" / "unknown"
14
+ * 2. Sets high-privacy defaults on below-threshold + unknown
15
+ * responses:
16
+ * - `Cache-Control: private, no-store`
17
+ * - `Referrer-Policy: no-referrer`
18
+ * - `X-Privacy-Posture: below-threshold`
19
+ * 3. Refuses with 451 (Unavailable For Legal Reasons) when the
20
+ * operator-supplied requireAge: 18 is set and the request is
21
+ * below threshold without a parental-consent record
22
+ * 4. Audits the classification decision
23
+ *
24
+ * var gate = b.middleware.ageGate({
25
+ * getAge: function (req) {
26
+ * if (req.user && typeof req.user.age === "number") return req.user.age;
27
+ * return null; // unknown
28
+ * },
29
+ * requireAge: null, // null = don't gate, just headers
30
+ * consentRequired: 18, // require parental consent below this
31
+ * hasParentalConsent: function (req) {
32
+ * return req.user && req.user.parentalConsent === true;
33
+ * },
34
+ * });
35
+ * router.use(gate);
36
+ */
37
+
38
+ var defineClass = require("../framework-error").defineClass;
39
+ var lazyRequire = require("../lazy-require");
40
+ var validateOpts = require("../validate-opts");
41
+
42
+ var audit = lazyRequire(function () { return require("../audit"); });
43
+
44
+ var AgeGateError = defineClass("AgeGateError", { alwaysPermanent: true });
45
+
46
+ function create(opts) {
47
+ opts = opts || {};
48
+ validateOpts(opts, [
49
+ "audit", "getAge", "requireAge", "consentRequired",
50
+ "hasParentalConsent", "skipPaths", "errorMessage",
51
+ ], "middleware.ageGate");
52
+
53
+ if (typeof opts.getAge !== "function") {
54
+ throw new AgeGateError("age-gate/bad-get-age",
55
+ "middleware.ageGate: opts.getAge must be a function (req) -> number | null");
56
+ }
57
+ var getAge = opts.getAge;
58
+ var requireAge = (typeof opts.requireAge === "number" && opts.requireAge > 0) // allow:numeric-opt-Infinity — age is operator domain, not a bytes/time-shaped opt
59
+ ? opts.requireAge : null;
60
+ var consentRequired = (typeof opts.consentRequired === "number" && opts.consentRequired > 0) // allow:numeric-opt-Infinity — age threshold, not a bytes/time-shaped opt
61
+ ? opts.consentRequired : null;
62
+ var hasParentalConsent = typeof opts.hasParentalConsent === "function" ? opts.hasParentalConsent : null;
63
+ var skipPaths = Array.isArray(opts.skipPaths) ? opts.skipPaths.slice() : [];
64
+ var auditOn = opts.audit !== false;
65
+ var errorMessage = typeof opts.errorMessage === "string" && opts.errorMessage.length > 0
66
+ ? opts.errorMessage : "service unavailable without parental consent";
67
+
68
+ function _shouldSkip(req) {
69
+ if (skipPaths.length === 0) return false;
70
+ var p = req.url || "";
71
+ var qpos = p.indexOf("?");
72
+ if (qpos !== -1) p = p.slice(0, qpos);
73
+ for (var i = 0; i < skipPaths.length; i++) {
74
+ var s = skipPaths[i];
75
+ if (typeof s === "string" && (p === s || p.indexOf(s + "/") === 0)) return true;
76
+ if (s instanceof RegExp && s.test(p)) return true;
77
+ }
78
+ return false;
79
+ }
80
+
81
+ function _emitAudit(action, outcome, metadata) {
82
+ if (!auditOn) return;
83
+ try {
84
+ audit().safeEmit({
85
+ action: "middleware.age_gate." + action,
86
+ outcome: outcome,
87
+ metadata: metadata || {},
88
+ });
89
+ } catch (_e) { /* drop-silent */ }
90
+ }
91
+
92
+ return function ageGateMiddleware(req, res, next) {
93
+ if (_shouldSkip(req)) return next();
94
+
95
+ var age;
96
+ try { age = getAge(req); }
97
+ catch (e) {
98
+ _emitAudit("get_age_failed", "failure", { error: (e && e.message) || String(e) });
99
+ age = null;
100
+ }
101
+
102
+ var classification;
103
+ if (age === null || typeof age !== "number") classification = "unknown";
104
+ else if (consentRequired !== null && age < consentRequired) classification = "below-threshold";
105
+ else classification = "above-threshold";
106
+
107
+ if (classification !== "above-threshold") {
108
+ if (typeof res.setHeader === "function") {
109
+ res.setHeader("Cache-Control", "private, no-store");
110
+ res.setHeader("Referrer-Policy", "no-referrer");
111
+ res.setHeader("X-Privacy-Posture", classification);
112
+ }
113
+ }
114
+
115
+ if (requireAge !== null && classification === "below-threshold" && (age === null || age < requireAge)) {
116
+ var hasConsent = hasParentalConsent ? !!hasParentalConsent(req) : false;
117
+ if (!hasConsent) {
118
+ _emitAudit("refused", "denied", { age: age, classification: classification, requireAge: requireAge });
119
+ if (!res.writableEnded && typeof res.writeHead === "function") {
120
+ res.writeHead(451, { // allow:raw-byte-literal — HTTP 451 Unavailable For Legal Reasons
121
+ "Content-Type": "application/json; charset=utf-8",
122
+ "Cache-Control": "no-store, private",
123
+ });
124
+ res.end(JSON.stringify({ error: errorMessage, requireAge: requireAge, parentalConsent: false }));
125
+ }
126
+ return;
127
+ }
128
+ }
129
+
130
+ if (req.locals && typeof req.locals === "object") {
131
+ req.locals.ageGateClassification = classification;
132
+ }
133
+ _emitAudit("classified", "success", { classification: classification, age: age });
134
+ return next();
135
+ };
136
+ }
137
+
138
+ module.exports = {
139
+ create: create,
140
+ AgeGateError: AgeGateError,
141
+ };
@@ -48,6 +48,7 @@ var requestLog = require("./request-log");
48
48
  var requireAal = require("./require-aal");
49
49
  var requireAuth = require("./require-auth");
50
50
  var requireContentType = require("./require-content-type");
51
+ var ageGate = require("./age-gate");
51
52
  var requireMethods = require("./require-methods");
52
53
  var requireMtls = require("./require-mtls");
53
54
  var requireStepUp = require("./require-step-up");
@@ -74,6 +75,7 @@ module.exports = {
74
75
  requireAal: requireAal.create,
75
76
  requireAuth: requireAuth.create,
76
77
  requireContentType: requireContentType.create,
78
+ ageGate: ageGate.create,
77
79
  requireMethods: requireMethods.create,
78
80
  requireMtls: requireMtls.create,
79
81
  requireStepUp: requireStepUp.create,
@@ -120,6 +122,7 @@ module.exports = {
120
122
  requireAal: requireAal,
121
123
  requireAuth: requireAuth,
122
124
  requireContentType: requireContentType,
125
+ ageGate: ageGate,
123
126
  requireMethods: requireMethods,
124
127
  requireMtls: requireMtls,
125
128
  requireStepUp: requireStepUp,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.8.10",
3
+ "version": "0.8.11",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
@@ -2,10 +2,10 @@
2
2
  "$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
3
3
  "bomFormat": "CycloneDX",
4
4
  "specVersion": "1.5",
5
- "serialNumber": "urn:uuid:4c9e2f2f-795d-4920-a093-90770087d324",
5
+ "serialNumber": "urn:uuid:ae68f706-2c92-4dfa-b852-f29896f91c62",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-07T02:38:14.544Z",
8
+ "timestamp": "2026-05-07T03:30:10.287Z",
9
9
  "lifecycles": [
10
10
  {
11
11
  "phase": "build"
@@ -19,14 +19,14 @@
19
19
  }
20
20
  ],
21
21
  "component": {
22
- "bom-ref": "@blamejs/core@0.8.10",
22
+ "bom-ref": "@blamejs/core@0.8.11",
23
23
  "type": "library",
24
24
  "name": "blamejs",
25
- "version": "0.8.10",
25
+ "version": "0.8.11",
26
26
  "scope": "required",
27
27
  "author": "blamejs contributors",
28
28
  "description": "The Node framework that owns its stack.",
29
- "purl": "pkg:npm/%40blamejs/core@0.8.10",
29
+ "purl": "pkg:npm/%40blamejs/core@0.8.11",
30
30
  "properties": [],
31
31
  "externalReferences": [
32
32
  {
@@ -54,7 +54,7 @@
54
54
  "components": [],
55
55
  "dependencies": [
56
56
  {
57
- "ref": "@blamejs/core@0.8.10",
57
+ "ref": "@blamejs/core@0.8.11",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]