@blamejs/core 0.8.9 → 0.8.10

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.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
+
11
13
  - **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
14
 
13
15
  - **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,9 @@ 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") },
252
257
  queue: queue,
253
258
  logStream: logStream,
254
259
  redact: redact,
@@ -0,0 +1,204 @@
1
+ "use strict";
2
+ /**
3
+ * b.compliance.eaa — EU Accessibility Act declared-conformance.
4
+ *
5
+ * Directive (EU) 2019/882 (the European Accessibility Act) requires
6
+ * digital products + services placed on the EU market to meet WCAG
7
+ * 2.1 AA accessibility requirements (extended to 2.2 by national
8
+ * implementing law in many member states). Operators producing a
9
+ * compliant deployment ship a "conformance statement" — a document
10
+ * declaring the product, the assessed standards (WCAG 2.1 / 2.2 /
11
+ * EN 301 549), the scope of testing, and any non-conforming
12
+ * features with operator-supplied justification.
13
+ *
14
+ * var eaa = b.compliance.eaa.create({
15
+ * audit: b.audit,
16
+ * productName: "Acme Customer Portal",
17
+ * productScope: "https://portal.acme.example",
18
+ * standards: ["WCAG 2.2 AA", "EN 301 549 v3.2.1"],
19
+ * });
20
+ * eaa.declareCriterion("1.1.1", { conformance: "supports", note: "..." });
21
+ * eaa.declareCriterion("1.4.3", { conformance: "supports", note: "ratio >= 4.5:1" });
22
+ * eaa.declareNonConformance({
23
+ * criterion: "2.5.5",
24
+ * reason: "legacy desktop-only interaction, replacement Q3 2026",
25
+ * mitigation: "alternative keyboard path documented",
26
+ * });
27
+ * var doc = eaa.export({ format: "markdown" });
28
+ *
29
+ * The exported document goes alongside the operator's product
30
+ * documentation and serves as the "Accessibility Statement" required
31
+ * by Article 13 §3.
32
+ */
33
+
34
+ var defineClass = require("./framework-error").defineClass;
35
+ var lazyRequire = require("./lazy-require");
36
+ var validateOpts = require("./validate-opts");
37
+
38
+ var audit = lazyRequire(function () { return require("./audit"); });
39
+
40
+ var ComplianceEaaError = defineClass("ComplianceEaaError", { alwaysPermanent: true });
41
+
42
+ var VALID_CONFORMANCE = Object.freeze({
43
+ "supports": 1, // criterion fully met
44
+ "partially-supports": 1, // some content meets, gaps documented
45
+ "does-not-support": 1, // criterion not met (declared non-conformance)
46
+ "not-applicable": 1, // criterion does not apply to product
47
+ "not-evaluated": 1, // outside the assessed scope
48
+ });
49
+
50
+ function create(opts) {
51
+ opts = opts || {};
52
+ validateOpts(opts, [
53
+ "audit", "productName", "productScope", "standards",
54
+ "contact", "supervisoryAuthority", "now",
55
+ ], "compliance.eaa");
56
+
57
+ validateOpts.requireNonEmptyString(opts.productName,
58
+ "compliance.eaa.create: opts.productName is required (Article 13 §3 requires product identification)",
59
+ ComplianceEaaError, "compliance-eaa/bad-product");
60
+ if (!Array.isArray(opts.standards) || opts.standards.length === 0) {
61
+ throw new ComplianceEaaError("compliance-eaa/bad-standards",
62
+ "compliance.eaa.create: opts.standards is required (e.g. ['WCAG 2.2 AA', 'EN 301 549 v3.2.1'])");
63
+ }
64
+ var productName = opts.productName;
65
+ var productScope = opts.productScope || null;
66
+ var standards = opts.standards.slice();
67
+ var contact = opts.contact || null;
68
+ var supervisoryAuthority = opts.supervisoryAuthority || null;
69
+ var auditOn = opts.audit !== false;
70
+ var now = typeof opts.now === "function" ? opts.now : function () { return Date.now(); };
71
+
72
+ var criteria = new Map();
73
+ var nonConformances = [];
74
+
75
+ function _emitAudit(action, outcome, metadata) {
76
+ if (!auditOn) return;
77
+ try {
78
+ audit().safeEmit({
79
+ action: "compliance.eaa." + action,
80
+ outcome: outcome,
81
+ metadata: metadata || {},
82
+ });
83
+ } catch (_e) { /* drop-silent */ }
84
+ }
85
+
86
+ function declareCriterion(id, decl) {
87
+ if (typeof id !== "string" || id.length === 0) {
88
+ throw new ComplianceEaaError("compliance-eaa/bad-criterion-id",
89
+ "compliance.eaa.declareCriterion: id must be a non-empty string (e.g. '1.1.1')");
90
+ }
91
+ if (!decl || typeof decl !== "object") {
92
+ throw new ComplianceEaaError("compliance-eaa/bad-decl",
93
+ "compliance.eaa.declareCriterion: decl must be an object with { conformance, note? }");
94
+ }
95
+ if (!VALID_CONFORMANCE[decl.conformance]) {
96
+ throw new ComplianceEaaError("compliance-eaa/bad-conformance",
97
+ "compliance.eaa.declareCriterion: conformance must be one of " + Object.keys(VALID_CONFORMANCE).join(", "));
98
+ }
99
+ criteria.set(id, {
100
+ criterion: id,
101
+ conformance: decl.conformance,
102
+ note: decl.note || "",
103
+ declaredAt: now(),
104
+ });
105
+ _emitAudit("criterion_declared", "success", { criterion: id, conformance: decl.conformance });
106
+ }
107
+
108
+ function declareNonConformance(decl) {
109
+ if (!decl || typeof decl !== "object" || !decl.criterion || !decl.reason) {
110
+ throw new ComplianceEaaError("compliance-eaa/bad-non-conformance",
111
+ "compliance.eaa.declareNonConformance: decl must include { criterion, reason, mitigation? }");
112
+ }
113
+ nonConformances.push({
114
+ criterion: decl.criterion,
115
+ reason: decl.reason,
116
+ mitigation: decl.mitigation || null,
117
+ declaredAt: now(),
118
+ });
119
+ criteria.set(decl.criterion, {
120
+ criterion: decl.criterion,
121
+ conformance: "does-not-support",
122
+ note: decl.reason + (decl.mitigation ? " (mitigation: " + decl.mitigation + ")" : ""),
123
+ declaredAt: now(),
124
+ });
125
+ _emitAudit("non_conformance_declared", "warning", { criterion: decl.criterion });
126
+ }
127
+
128
+ function _stats() {
129
+ var counts = { supports: 0, "partially-supports": 0, "does-not-support": 0, "not-applicable": 0, "not-evaluated": 0 };
130
+ criteria.forEach(function (c) { counts[c.conformance] += 1; });
131
+ return counts;
132
+ }
133
+
134
+ function _exportJson() {
135
+ var c = []; criteria.forEach(function (rec) { c.push(rec); });
136
+ return {
137
+ directive: "(EU) 2019/882",
138
+ article: "13",
139
+ generatedAt: new Date(now()).toISOString(),
140
+ product: { name: productName, scope: productScope },
141
+ standards: standards,
142
+ contact: contact,
143
+ supervisoryAuthority: supervisoryAuthority,
144
+ criteria: c,
145
+ nonConformances: nonConformances,
146
+ stats: _stats(),
147
+ };
148
+ }
149
+ function _exportMarkdown() {
150
+ var stats = _stats();
151
+ var c = []; criteria.forEach(function (rec) { c.push(rec); });
152
+ var md = "# Accessibility Statement — " + productName + "\n\n";
153
+ md += "**Standards:** " + standards.join(", ") + "\n\n";
154
+ if (productScope) md += "**Scope:** " + productScope + "\n\n";
155
+ md += "Generated: " + new Date(now()).toISOString() + "\n\n";
156
+ md += "## Conformance summary\n\n";
157
+ md += "- Supports: " + stats.supports + "\n";
158
+ md += "- Partially supports: " + stats["partially-supports"] + "\n";
159
+ md += "- Does not support: " + stats["does-not-support"] + "\n";
160
+ md += "- Not applicable: " + stats["not-applicable"] + "\n\n";
161
+ if (nonConformances.length > 0) {
162
+ md += "## Non-conformances\n\n";
163
+ for (var i = 0; i < nonConformances.length; i++) {
164
+ var nc = nonConformances[i];
165
+ md += "### " + nc.criterion + "\n\n";
166
+ md += "- Reason: " + nc.reason + "\n";
167
+ if (nc.mitigation) md += "- Mitigation: " + nc.mitigation + "\n";
168
+ md += "\n";
169
+ }
170
+ }
171
+ md += "## Per-criterion declarations\n\n";
172
+ for (var ci = 0; ci < c.length; ci++) {
173
+ md += "- **" + c[ci].criterion + "** — " + c[ci].conformance;
174
+ if (c[ci].note) md += " — " + c[ci].note;
175
+ md += "\n";
176
+ }
177
+ if (contact) md += "\n## Contact\n\n" + (contact.name || "") + " (" + (contact.email || "") + ")\n";
178
+ return md;
179
+ }
180
+
181
+ function exportEaa(eopts) {
182
+ eopts = eopts || {};
183
+ var format = (eopts.format || "json").toLowerCase();
184
+ _emitAudit("exported", "success", { format: format, criteriaCount: criteria.size });
185
+ if (format === "json") return _exportJson();
186
+ if (format === "markdown") return _exportMarkdown();
187
+ throw new ComplianceEaaError("compliance-eaa/bad-format",
188
+ "compliance.eaa.export: format must be 'json' or 'markdown'");
189
+ }
190
+
191
+ return {
192
+ declareCriterion: declareCriterion,
193
+ declareNonConformance: declareNonConformance,
194
+ "export": exportEaa,
195
+ stats: _stats,
196
+ VALID_CONFORMANCE: Object.keys(VALID_CONFORMANCE),
197
+ };
198
+ }
199
+
200
+ module.exports = {
201
+ create: create,
202
+ ComplianceEaaError: ComplianceEaaError,
203
+ VALID_CONFORMANCE: Object.keys(VALID_CONFORMANCE),
204
+ };
@@ -0,0 +1,195 @@
1
+ "use strict";
2
+ /**
3
+ * b.cra.report — EU Cyber Resilience Act incident-reporting wrapper.
4
+ *
5
+ * The Cyber Resilience Act (Regulation (EU) 2024/2847) Article 14 §1
6
+ * mandates that manufacturers of digital products report actively-
7
+ * exploited vulnerabilities and severe incidents to ENISA + national
8
+ * authorities. The framework's b.incident.report primitive provides
9
+ * the generic 3-stage shape; this wrapper specializes it with the
10
+ * CRA-specific reporting fields, deadlines (24h early warning / 72h
11
+ * incident notification / 14d final report), and the ENISA single-
12
+ * reporting-point destination.
13
+ *
14
+ * var cra = b.cra.report.create({
15
+ * enisaEndpoint: "https://enisa-spr.europa.eu/api/incidents",
16
+ * httpClient: b.httpClient,
17
+ * audit: b.audit,
18
+ * productId: "blamejs-1.x",
19
+ * manufacturer: { name: "Acme Co", contact: "security@acme.example" },
20
+ * });
21
+ * var inc = await cra.open({
22
+ * detectedAt: Date.now(),
23
+ * vulnerability: { cveId: "CVE-2026-99999", actively_exploited: true },
24
+ * impact: { ... },
25
+ * });
26
+ * await cra.earlyWarning(inc.id, { ... }); // 24h
27
+ * await cra.notification(inc.id, { ... }); // 72h
28
+ * await cra.finalReport(inc.id, { ... }); // 14d
29
+ *
30
+ * The wrapper composes b.incident.report so the audit chain shape,
31
+ * persistence hook, and status surface stay consistent across every
32
+ * regulatory regime. Per-regime CRA semantics:
33
+ * - early warning may be terse ("incident detected, scope unknown")
34
+ * - notification carries impact + mitigation + scope
35
+ * - final report adds root cause + lessons learned
36
+ *
37
+ * Submission to ENISA is opt-in per call (operators may want to
38
+ * batch / approve before submission); pass { submit: true } on each
39
+ * stage call to push through the operator's b.httpClient. The
40
+ * primitive does NOT auto-submit on stage transitions — regulators
41
+ * uniformly require operator review before filing.
42
+ */
43
+
44
+ var C = require("./constants");
45
+ var defineClass = require("./framework-error").defineClass;
46
+ var lazyRequire = require("./lazy-require");
47
+ var validateOpts = require("./validate-opts");
48
+
49
+ var incidentReport = lazyRequire(function () { return require("./incident-report"); });
50
+ var audit = lazyRequire(function () { return require("./audit"); });
51
+
52
+ var CraReportError = defineClass("CraReportError", { alwaysPermanent: true });
53
+
54
+ function create(opts) {
55
+ opts = opts || {};
56
+ validateOpts(opts, [
57
+ "audit", "persist", "httpClient", "enisaEndpoint",
58
+ "productId", "manufacturer", "now",
59
+ ], "cra.report");
60
+
61
+ validateOpts.requireNonEmptyString(opts.productId,
62
+ "cra.report.create: opts.productId is required (CRA Annex VII §1 requires a stable product identifier)",
63
+ CraReportError, "cra-report/bad-product-id");
64
+ if (!opts.manufacturer || typeof opts.manufacturer !== "object") {
65
+ throw new CraReportError("cra-report/bad-manufacturer",
66
+ "cra.report.create: opts.manufacturer is required (CRA Annex VII §1 requires manufacturer name + contact)");
67
+ }
68
+ var productId = opts.productId;
69
+ var manufacturer = opts.manufacturer;
70
+ var enisaEndpoint = typeof opts.enisaEndpoint === "string" && opts.enisaEndpoint.length > 0
71
+ ? opts.enisaEndpoint : null;
72
+ var httpClient = opts.httpClient || null;
73
+ var auditOn = opts.audit !== false;
74
+
75
+ // CRA Article 14 deadlines — operators don't override these without
76
+ // documented regulatory justification (the deadlines are statutory,
77
+ // not operator preference).
78
+ var ir = incidentReport().create({
79
+ audit: opts.audit,
80
+ persist: opts.persist,
81
+ now: opts.now,
82
+ deadlines: {
83
+ // initial = "early warning" per CRA Article 14 §1(a) — 24h
84
+ initial: C.TIME.hours(24),
85
+ // intermediate = "incident notification" per CRA Article 14 §1(b) — 72h
86
+ intermediate: C.TIME.hours(72),
87
+ // final = "final report" per CRA Article 14 §1(c) — 14 days
88
+ final: C.TIME.days(14),
89
+ },
90
+ });
91
+
92
+ function _emitAudit(action, outcome, metadata) {
93
+ if (!auditOn) return;
94
+ try {
95
+ audit().safeEmit({
96
+ action: "cra.report." + action,
97
+ outcome: outcome,
98
+ metadata: metadata || {},
99
+ });
100
+ } catch (_e) { /* drop-silent */ }
101
+ }
102
+
103
+ async function _submitToEnisa(payload) {
104
+ if (!enisaEndpoint || !httpClient) {
105
+ _emitAudit("submit_skipped", "warning", { reason: "no-endpoint-or-client" });
106
+ return { submitted: false, reason: "no-endpoint-or-client" };
107
+ }
108
+ try {
109
+ var res = await httpClient.request({
110
+ url: enisaEndpoint,
111
+ method: "POST",
112
+ headers: { "Content-Type": "application/json" },
113
+ body: Buffer.from(JSON.stringify(payload), "utf8"),
114
+ responseMode: "always-resolve",
115
+ });
116
+ var ok = res.statusCode >= 200 && res.statusCode < 300; // allow:raw-byte-literal — HTTP status range
117
+ _emitAudit("submitted", ok ? "success" : "failure", {
118
+ statusCode: res.statusCode, productId: productId,
119
+ });
120
+ return { submitted: ok, statusCode: res.statusCode };
121
+ } catch (e) {
122
+ _emitAudit("submit_failed", "failure", { error: (e && e.message) || String(e) });
123
+ return { submitted: false, error: (e && e.message) || String(e) };
124
+ }
125
+ }
126
+
127
+ function _craEnvelope(stage, incident, fields) {
128
+ return {
129
+ cra_version: "2024/2847",
130
+ stage: stage, // "early-warning" / "notification" / "final"
131
+ product: { id: productId },
132
+ manufacturer: manufacturer,
133
+ incident: {
134
+ id: incident.id,
135
+ detected_at: new Date(incident.detectedAt).toISOString(),
136
+ scope: incident.scope,
137
+ summary: incident.summary,
138
+ impact: incident.impact,
139
+ },
140
+ fields: fields || {},
141
+ };
142
+ }
143
+
144
+ async function open(spec) {
145
+ spec = Object.assign({}, spec || {}, { regime: "cra" });
146
+ var rec = await ir.open(spec);
147
+ _emitAudit("opened", "success", { incidentId: rec.id, productId: productId });
148
+ return rec;
149
+ }
150
+
151
+ async function earlyWarning(incidentId, fields) {
152
+ var rec = await ir.recordInitial(incidentId, fields || {});
153
+ var result = { record: rec, submitted: null };
154
+ if (fields && fields.submit === true) {
155
+ result.submitted = await _submitToEnisa(_craEnvelope("early-warning", rec, fields));
156
+ }
157
+ return result;
158
+ }
159
+
160
+ async function notification(incidentId, fields) {
161
+ var rec = await ir.recordIntermediate(incidentId, fields || {});
162
+ var result = { record: rec, submitted: null };
163
+ if (fields && fields.submit === true) {
164
+ result.submitted = await _submitToEnisa(_craEnvelope("notification", rec, fields));
165
+ }
166
+ return result;
167
+ }
168
+
169
+ async function finalReport(incidentId, fields) {
170
+ var rec = await ir.recordFinal(incidentId, fields || {});
171
+ var result = { record: rec, submitted: null };
172
+ if (fields && fields.submit === true) {
173
+ result.submitted = await _submitToEnisa(_craEnvelope("final", rec, fields));
174
+ }
175
+ return result;
176
+ }
177
+
178
+ return {
179
+ open: open,
180
+ earlyWarning: earlyWarning,
181
+ notification: notification,
182
+ finalReport: finalReport,
183
+ // Forward incident.report observability surface
184
+ get: function (id) { return ir.get(id); },
185
+ list: function () { return ir.list(); },
186
+ status: function () { return ir.status(); },
187
+ productId: productId,
188
+ manufacturer: manufacturer,
189
+ };
190
+ }
191
+
192
+ module.exports = {
193
+ create: create,
194
+ CraReportError: CraReportError,
195
+ };
@@ -0,0 +1,261 @@
1
+ "use strict";
2
+ /**
3
+ * b.gdpr.ropa — GDPR Article 30 Records of Processing Activities.
4
+ *
5
+ * Article 30 §1 (controller) + §2 (processor) require a written
6
+ * record of processing activities. The framework's existing audit
7
+ * chain captures *what happened* on each request; the RoPA captures
8
+ * *what processing the operator does in general* — purposes, data
9
+ * categories, retention periods, recipients, transfers outside the
10
+ * EEA, security measures.
11
+ *
12
+ * The primitive is a registry + exporter:
13
+ * - register(activity) — add a processing-activity record
14
+ * - update(id, patch) — modify an existing record
15
+ * - remove(id) — soft-delete (operator audit trail)
16
+ * - export({ format }) — emit RoPA as JSON / CSV / Markdown
17
+ *
18
+ * var ropa = b.gdpr.ropa.create({
19
+ * audit: b.audit,
20
+ * controller: { name: "Acme Co", contact: "dpo@acme.example" },
21
+ * });
22
+ * ropa.register({
23
+ * id: "sales-funnel-tracking",
24
+ * name: "Sales-funnel CRM tracking",
25
+ * purposes: ["lead-tracking", "sales-attribution"],
26
+ * legalBasis: "legitimate-interests",
27
+ * dataCategories: ["contact-info", "engagement-history"],
28
+ * dataSubjectCategories: ["prospects", "customers"],
29
+ * recipients: ["analytics-vendor", "sales-team"],
30
+ * thirdCountryTransfers: [{ country: "US", safeguard: "scc-2021" }],
31
+ * retentionPeriod: "5 years post-relationship",
32
+ * securityMeasures: ["encrypted-at-rest", "tls-13", "access-control"],
33
+ * });
34
+ * var json = ropa.export({ format: "json" });
35
+ */
36
+
37
+ var defineClass = require("./framework-error").defineClass;
38
+ var lazyRequire = require("./lazy-require");
39
+ var validateOpts = require("./validate-opts");
40
+
41
+ var audit = lazyRequire(function () { return require("./audit"); });
42
+
43
+ var GdprRopaError = defineClass("GdprRopaError", { alwaysPermanent: true });
44
+
45
+ var REQUIRED_ACTIVITY_FIELDS = Object.freeze([
46
+ "id", "name", "purposes", "legalBasis", "dataCategories",
47
+ ]);
48
+
49
+ var VALID_LEGAL_BASES = Object.freeze({
50
+ "consent": 1, // Art 6(1)(a)
51
+ "contract": 1, // Art 6(1)(b)
52
+ "legal-obligation": 1, // Art 6(1)(c)
53
+ "vital-interests": 1, // Art 6(1)(d)
54
+ "public-task": 1, // Art 6(1)(e)
55
+ "legitimate-interests": 1, // Art 6(1)(f)
56
+ });
57
+
58
+ function create(opts) {
59
+ opts = opts || {};
60
+ validateOpts(opts, [
61
+ "audit", "controller", "dpo", "supervisoryAuthority", "now",
62
+ ], "gdpr.ropa");
63
+
64
+ if (!opts.controller || typeof opts.controller !== "object") {
65
+ throw new GdprRopaError("gdpr-ropa/bad-controller",
66
+ "gdpr.ropa.create: opts.controller is required (Article 30 §1(a) requires controller name + contact)");
67
+ }
68
+ var controller = opts.controller;
69
+ var dpo = opts.dpo || null;
70
+ var supervisoryAuthority = opts.supervisoryAuthority || null;
71
+ var auditOn = opts.audit !== false;
72
+ var now = typeof opts.now === "function" ? opts.now : function () { return Date.now(); };
73
+
74
+ var activities = new Map();
75
+
76
+ function _emitAudit(action, outcome, metadata) {
77
+ if (!auditOn) return;
78
+ try {
79
+ audit().safeEmit({
80
+ action: "gdpr.ropa." + action,
81
+ outcome: outcome,
82
+ metadata: metadata || {},
83
+ });
84
+ } catch (_e) { /* drop-silent */ }
85
+ }
86
+
87
+ function _validateActivity(activity, op) {
88
+ if (!activity || typeof activity !== "object") {
89
+ throw new GdprRopaError("gdpr-ropa/bad-activity",
90
+ "gdpr.ropa." + op + ": activity must be an object");
91
+ }
92
+ for (var i = 0; i < REQUIRED_ACTIVITY_FIELDS.length; i++) {
93
+ var f = REQUIRED_ACTIVITY_FIELDS[i];
94
+ if (activity[f] === undefined) {
95
+ throw new GdprRopaError("gdpr-ropa/missing-field",
96
+ "gdpr.ropa." + op + ": activity is missing required field '" + f + "' (per Article 30 §1)");
97
+ }
98
+ }
99
+ if (typeof activity.id !== "string" || activity.id.length === 0) {
100
+ throw new GdprRopaError("gdpr-ropa/bad-id",
101
+ "gdpr.ropa." + op + ": activity.id must be a non-empty string");
102
+ }
103
+ if (!VALID_LEGAL_BASES[activity.legalBasis]) {
104
+ throw new GdprRopaError("gdpr-ropa/bad-legal-basis",
105
+ "gdpr.ropa." + op + ": activity.legalBasis must be one of " + Object.keys(VALID_LEGAL_BASES).join(", "));
106
+ }
107
+ }
108
+
109
+ function register(activity) {
110
+ _validateActivity(activity, "register");
111
+ if (activities.has(activity.id)) {
112
+ throw new GdprRopaError("gdpr-ropa/duplicate-id",
113
+ "gdpr.ropa.register: activity '" + activity.id + "' already registered");
114
+ }
115
+ var rec = Object.assign({}, activity, {
116
+ registeredAt: now(),
117
+ lastUpdatedAt: now(),
118
+ });
119
+ activities.set(activity.id, rec);
120
+ _emitAudit("registered", "success", { id: activity.id, purposes: activity.purposes });
121
+ return rec;
122
+ }
123
+
124
+ function update(id, patch) {
125
+ var existing = activities.get(id);
126
+ if (!existing) {
127
+ throw new GdprRopaError("gdpr-ropa/not-found",
128
+ "gdpr.ropa.update: no activity with id '" + id + "'");
129
+ }
130
+ if (!patch || typeof patch !== "object") {
131
+ throw new GdprRopaError("gdpr-ropa/bad-patch",
132
+ "gdpr.ropa.update: patch must be an object");
133
+ }
134
+ var merged = Object.assign({}, existing, patch, {
135
+ id: id, // id is immutable on update
136
+ registeredAt: existing.registeredAt,
137
+ lastUpdatedAt: now(),
138
+ });
139
+ if (patch.legalBasis && !VALID_LEGAL_BASES[merged.legalBasis]) {
140
+ throw new GdprRopaError("gdpr-ropa/bad-legal-basis",
141
+ "gdpr.ropa.update: legalBasis must be one of " + Object.keys(VALID_LEGAL_BASES).join(", "));
142
+ }
143
+ activities.set(id, merged);
144
+ _emitAudit("updated", "success", { id: id, fields: Object.keys(patch) });
145
+ return merged;
146
+ }
147
+
148
+ function remove(id, info) {
149
+ var existing = activities.get(id);
150
+ if (!existing) {
151
+ throw new GdprRopaError("gdpr-ropa/not-found",
152
+ "gdpr.ropa.remove: no activity with id '" + id + "'");
153
+ }
154
+ activities.delete(id);
155
+ _emitAudit("removed", "success", {
156
+ id: id,
157
+ reason: (info && info.reason) || null,
158
+ actor: (info && info.actor) || null,
159
+ });
160
+ return { removed: true, id: id };
161
+ }
162
+
163
+ function get(id) { return activities.get(id) || null; }
164
+ function list() {
165
+ var out = [];
166
+ activities.forEach(function (rec) { out.push(rec); });
167
+ return out;
168
+ }
169
+
170
+ // Operator-facing exporter — JSON for API integrations / SCC, CSV
171
+ // for spreadsheet handoff to legal, Markdown for human-readable
172
+ // operator documentation. Each shape carries the same fields per
173
+ // Article 30 §1(a-h) / §2(a-d).
174
+ function _exportJson() {
175
+ return {
176
+ controller: controller,
177
+ dpo: dpo,
178
+ supervisoryAuthority: supervisoryAuthority,
179
+ generatedAt: new Date(now()).toISOString(),
180
+ article: "30",
181
+ regulation: "(EU) 2016/679 (GDPR)",
182
+ activities: list(),
183
+ };
184
+ }
185
+ function _exportCsv() {
186
+ var headers = [
187
+ "id", "name", "purposes", "legalBasis", "dataCategories",
188
+ "dataSubjectCategories", "recipients", "thirdCountryTransfers",
189
+ "retentionPeriod", "securityMeasures",
190
+ ];
191
+ var rows = [headers.join(",")];
192
+ var entries = list();
193
+ for (var i = 0; i < entries.length; i++) {
194
+ var e = entries[i];
195
+ var row = headers.map(function (h) {
196
+ var v = e[h];
197
+ if (v === undefined || v === null) return "";
198
+ if (Array.isArray(v)) return JSON.stringify(v).replace(/"/g, "\"\"");
199
+ return String(v).replace(/"/g, "\"\"");
200
+ });
201
+ rows.push(row.map(function (c) { return '"' + c + '"'; }).join(","));
202
+ }
203
+ return rows.join("\n");
204
+ }
205
+ function _exportMarkdown() {
206
+ var entries = list();
207
+ var md = "# GDPR Article 30 Records of Processing Activities\n\n";
208
+ md += "Generated: " + new Date(now()).toISOString() + "\n\n";
209
+ md += "Controller: " + (controller.name || "(unspecified)") + "\n";
210
+ md += "Contact: " + (controller.contact || "(unspecified)") + "\n\n";
211
+ if (dpo) md += "DPO: " + (dpo.name || "(unspecified)") + " (" + (dpo.contact || "") + ")\n\n";
212
+ md += "## Activities (" + entries.length + ")\n\n";
213
+ for (var i = 0; i < entries.length; i++) {
214
+ var e = entries[i];
215
+ md += "### " + (e.name || e.id) + " (`" + e.id + "`)\n\n";
216
+ md += "- Purposes: " + (e.purposes || []).join(", ") + "\n";
217
+ md += "- Legal basis: " + e.legalBasis + "\n";
218
+ md += "- Data categories: " + (e.dataCategories || []).join(", ") + "\n";
219
+ if (e.dataSubjectCategories) md += "- Data subjects: " + e.dataSubjectCategories.join(", ") + "\n";
220
+ if (e.recipients) md += "- Recipients: " + e.recipients.join(", ") + "\n";
221
+ if (e.retentionPeriod) md += "- Retention: " + e.retentionPeriod + "\n";
222
+ if (e.securityMeasures) md += "- Security: " + e.securityMeasures.join(", ") + "\n";
223
+ if (e.thirdCountryTransfers && e.thirdCountryTransfers.length > 0) {
224
+ md += "- Third-country transfers:\n";
225
+ for (var ti = 0; ti < e.thirdCountryTransfers.length; ti++) {
226
+ var t = e.thirdCountryTransfers[ti];
227
+ md += " - " + t.country + " (safeguard: " + (t.safeguard || "n/a") + ")\n";
228
+ }
229
+ }
230
+ md += "\n";
231
+ }
232
+ return md;
233
+ }
234
+ function exportRopa(eopts) {
235
+ eopts = eopts || {};
236
+ var format = (eopts.format || "json").toLowerCase();
237
+ _emitAudit("exported", "success", { format: format, count: activities.size });
238
+ if (format === "csv") return _exportCsv();
239
+ if (format === "markdown") return _exportMarkdown();
240
+ if (format === "json") return _exportJson();
241
+ throw new GdprRopaError("gdpr-ropa/bad-format",
242
+ "gdpr.ropa.export: format must be 'json' / 'csv' / 'markdown'");
243
+ }
244
+
245
+ return {
246
+ register: register,
247
+ update: update,
248
+ remove: remove,
249
+ get: get,
250
+ list: list,
251
+ "export": exportRopa, // explicit string-key — `export` is reserved
252
+ VALID_LEGAL_BASES: Object.keys(VALID_LEGAL_BASES),
253
+ };
254
+ }
255
+
256
+ module.exports = {
257
+ create: create,
258
+ GdprRopaError: GdprRopaError,
259
+ VALID_LEGAL_BASES: Object.keys(VALID_LEGAL_BASES),
260
+ REQUIRED_ACTIVITY_FIELDS: REQUIRED_ACTIVITY_FIELDS,
261
+ };
@@ -0,0 +1,149 @@
1
+ "use strict";
2
+ /**
3
+ * botDisclose middleware — California SB 1001 bot-disclosure.
4
+ *
5
+ * Cal. Bus. & Prof. Code §17941 (SB 1001, effective 2019) requires
6
+ * that any "bot" used to communicate with persons in California for
7
+ * the purpose of incentivizing a sale or influencing an election
8
+ * disclose its non-human nature. Operators serving an automated
9
+ * conversation surface (LLM-backed chat / IVR / SMS) wire this
10
+ * middleware to:
11
+ *
12
+ * 1. Inject a disclosure banner into HTML responses (server-rendered)
13
+ * 2. Set an X-Bot-Disclosure header for API consumers
14
+ * 3. Emit an audit event for every conversation-initiating request
15
+ *
16
+ * var bot = b.middleware.botDisclose({
17
+ * audit: b.audit,
18
+ * mountPaths: ["/chat", "/api/chat"],
19
+ * bannerHtml: '<div role="status">You are interacting with an automated assistant.</div>',
20
+ * bannerJson: { _bot: true, disclosure: "automated-assistant" },
21
+ * });
22
+ * router.use(bot);
23
+ *
24
+ * Per SB 1001 §17941(a), the disclosure must be "clear, conspicuous,
25
+ * and reasonably designed to inform"; operators with custom UI wire
26
+ * `bannerHtml` to match their visual design.
27
+ */
28
+
29
+ var defineClass = require("../framework-error").defineClass;
30
+ var lazyRequire = require("../lazy-require");
31
+ var validateOpts = require("../validate-opts");
32
+
33
+ var audit = lazyRequire(function () { return require("../audit"); });
34
+
35
+ var BotDiscloseError = defineClass("BotDiscloseError", { alwaysPermanent: true });
36
+
37
+ var DEFAULT_BANNER_HTML = '<div role="status" data-bot-disclosure="true" ' +
38
+ 'style="border:1px solid #888;padding:8px;margin:8px 0;background:#fff8e1;font-size:14px;">' +
39
+ '<strong>Automated assistant.</strong> ' +
40
+ 'You are interacting with an automated agent. ' +
41
+ 'For California users: this disclosure is provided per Cal. Bus. &amp; Prof. Code §17941.' +
42
+ '</div>';
43
+
44
+ function create(opts) {
45
+ opts = opts || {};
46
+ validateOpts(opts, [
47
+ "audit", "mountPaths", "bannerHtml", "bannerJson",
48
+ "headerName", "auditAction",
49
+ ], "middleware.botDisclose");
50
+
51
+ var mountPaths = Array.isArray(opts.mountPaths) ? opts.mountPaths.slice() : null;
52
+ var bannerHtml = typeof opts.bannerHtml === "string" ? opts.bannerHtml : DEFAULT_BANNER_HTML;
53
+ var bannerJson = (opts.bannerJson && typeof opts.bannerJson === "object")
54
+ ? opts.bannerJson : { _bot: true, disclosure: "automated-assistant" };
55
+ var headerName = typeof opts.headerName === "string" && opts.headerName.length > 0
56
+ ? opts.headerName : "X-Bot-Disclosure";
57
+ var auditOn = opts.audit !== false;
58
+ var actionBase = typeof opts.auditAction === "string" && opts.auditAction.length > 0
59
+ ? opts.auditAction : "middleware.bot_disclose";
60
+
61
+ function _matches(req) {
62
+ if (!mountPaths) return true; // null = match every path
63
+ var p = req.url || "";
64
+ var qpos = p.indexOf("?");
65
+ if (qpos !== -1) p = p.slice(0, qpos);
66
+ for (var i = 0; i < mountPaths.length; i++) {
67
+ var m = mountPaths[i];
68
+ if (typeof m === "string" && (p === m || p.indexOf(m + "/") === 0)) return true;
69
+ if (m instanceof RegExp && m.test(p)) return true;
70
+ }
71
+ return false;
72
+ }
73
+
74
+ function _emitAudit(action, outcome, metadata) {
75
+ if (!auditOn) return;
76
+ try {
77
+ audit().safeEmit({
78
+ action: actionBase + "." + action,
79
+ outcome: outcome,
80
+ metadata: metadata || {},
81
+ });
82
+ } catch (_e) { /* drop-silent */ }
83
+ }
84
+
85
+ return function botDiscloseMiddleware(req, res, next) {
86
+ if (!_matches(req)) return next();
87
+
88
+ // Always set the header (cheap; API consumers see the disclosure
89
+ // even on JSON endpoints).
90
+ if (typeof res.setHeader === "function") {
91
+ res.setHeader(headerName, "automated-assistant");
92
+ }
93
+ _emitAudit("disclosed", "success", { method: req.method, path: req.url });
94
+
95
+ // Patch res.write / res.end so the first text/html response gets
96
+ // the banner injected before <body>'s first child. Operators
97
+ // wanting deeper integration override bannerHtml or set
98
+ // mountPaths to scope where injection happens.
99
+ var origWrite = res.write && res.write.bind(res);
100
+ var origEnd = res.end && res.end.bind(res);
101
+ var injected = false;
102
+
103
+ function _maybeInject(chunk, encoding) {
104
+ if (injected) return chunk;
105
+ var ct = typeof res.getHeader === "function" ? res.getHeader("content-type") : "";
106
+ if (typeof ct !== "string" || ct.indexOf("text/html") === -1) return chunk;
107
+ var body = Buffer.isBuffer(chunk) ? chunk.toString("utf8") :
108
+ (typeof chunk === "string" ? chunk : "");
109
+ // Inject after <body> opening tag if present, else after <html>
110
+ // opening tag, else prepend.
111
+ var bodyMatch = body.match(/<body[^>]*>/i);
112
+ if (bodyMatch) {
113
+ var idx = bodyMatch.index + bodyMatch[0].length;
114
+ body = body.slice(0, idx) + "\n" + bannerHtml + "\n" + body.slice(idx);
115
+ } else {
116
+ body = bannerHtml + "\n" + body;
117
+ }
118
+ injected = true;
119
+ return Buffer.from(body, "utf8");
120
+ }
121
+
122
+ if (origWrite) {
123
+ res.write = function (chunk, encoding, cb) {
124
+ return origWrite(_maybeInject(chunk, encoding), encoding, cb);
125
+ };
126
+ }
127
+ if (origEnd) {
128
+ res.end = function (chunk, encoding, cb) {
129
+ if (chunk) chunk = _maybeInject(chunk, encoding);
130
+ return origEnd(chunk, encoding, cb);
131
+ };
132
+ }
133
+
134
+ // For JSON responses, attach the disclosure to res.locals so
135
+ // operator handlers building JSON via res.json(...) can include
136
+ // it explicitly. We don't auto-merge into the JSON body — the
137
+ // header + audit are the load-bearing disclosure surfaces.
138
+ if (res.locals && typeof res.locals === "object") {
139
+ res.locals.botDisclosure = bannerJson;
140
+ }
141
+ return next();
142
+ };
143
+ }
144
+
145
+ module.exports = {
146
+ create: create,
147
+ BotDiscloseError: BotDiscloseError,
148
+ DEFAULT_BANNER_HTML: DEFAULT_BANNER_HTML,
149
+ };
@@ -25,6 +25,7 @@ var assetlinks = require("./assetlinks");
25
25
  var attachUser = require("./attach-user");
26
26
  var bearerAuth = require("./bearer-auth");
27
27
  var bodyParser = require("./body-parser");
28
+ var botDisclose = require("./bot-disclose");
28
29
  var botGuard = require("./bot-guard");
29
30
  var compression = require("./compression");
30
31
  var cookies = require("./cookies");
@@ -63,6 +64,7 @@ module.exports = {
63
64
  requestId: requestId.create,
64
65
  securityHeaders: securityHeaders.create,
65
66
  errorHandler: errorHandler.create,
67
+ botDisclose: botDisclose.create,
66
68
  botGuard: botGuard.create,
67
69
  cors: cors.create,
68
70
  dailyByteQuota: dailyByteQuota.create,
@@ -108,6 +110,7 @@ module.exports = {
108
110
  requestId: requestId,
109
111
  securityHeaders: securityHeaders,
110
112
  errorHandler: errorHandler,
113
+ botDisclose: botDisclose,
111
114
  botGuard: botGuard,
112
115
  cors: cors,
113
116
  dailyByteQuota: dailyByteQuota,
@@ -0,0 +1,181 @@
1
+ "use strict";
2
+ /**
3
+ * b.nis2.report — NIS2 Directive incident-reporting wrapper.
4
+ *
5
+ * Directive (EU) 2022/2555 (NIS2) Article 23 mandates that essential
6
+ * + important entities report significant incidents to the national
7
+ * CSIRT or competent authority. Three-stage pattern with statutory
8
+ * deadlines:
9
+ * - early warning within 24h of becoming aware (Art. 23 §4(a))
10
+ * - incident notification within 72h, including initial assessment
11
+ * of severity + impact + indicators of compromise (Art. 23 §4(b))
12
+ * - final report within 1 month, with detailed root cause +
13
+ * remediation + cross-border implications (Art. 23 §4(d))
14
+ *
15
+ * var nis2 = b.nis2.report.create({
16
+ * audit: b.audit,
17
+ * entityId: "acme-cloud-1",
18
+ * entityType: "essential", // "essential" | "important"
19
+ * sectorAnnex: "I.6", // NIS2 Annex I/II row id
20
+ * csirtEndpoint: "https://csirt.example/api",
21
+ * httpClient: b.httpClient,
22
+ * });
23
+ *
24
+ * Sector annex codes follow NIS2's two annexes:
25
+ * - Annex I (essential): I.1 energy / I.2 transport / I.3 banking /
26
+ * I.4 financial-market-infrastructures / I.5 health /
27
+ * I.6 drinking-water / I.7 wastewater / I.8 digital-infrastructure /
28
+ * I.9 ICT-service-management / I.10 public-administration /
29
+ * I.11 space
30
+ * - Annex II (important): II.1 postal / II.2 waste-management /
31
+ * II.3 chemicals / II.4 food / II.5 manufacturing /
32
+ * II.6 digital-providers / II.7 research
33
+ */
34
+
35
+ var C = require("./constants");
36
+ var defineClass = require("./framework-error").defineClass;
37
+ var lazyRequire = require("./lazy-require");
38
+ var validateOpts = require("./validate-opts");
39
+
40
+ var incidentReport = lazyRequire(function () { return require("./incident-report"); });
41
+ var audit = lazyRequire(function () { return require("./audit"); });
42
+
43
+ var Nis2ReportError = defineClass("Nis2ReportError", { alwaysPermanent: true });
44
+
45
+ var VALID_ENTITY_TYPES = Object.freeze({ essential: 1, important: 1 });
46
+
47
+ function create(opts) {
48
+ opts = opts || {};
49
+ validateOpts(opts, [
50
+ "audit", "persist", "httpClient", "csirtEndpoint",
51
+ "entityId", "entityType", "sectorAnnex", "now",
52
+ ], "nis2.report");
53
+
54
+ validateOpts.requireNonEmptyString(opts.entityId,
55
+ "nis2.report.create: opts.entityId is required (NIS2 registration ID)",
56
+ Nis2ReportError, "nis2-report/bad-entity-id");
57
+ if (!VALID_ENTITY_TYPES[opts.entityType]) {
58
+ throw new Nis2ReportError("nis2-report/bad-entity-type",
59
+ "nis2.report.create: opts.entityType must be 'essential' or 'important' (NIS2 Article 3 classification)");
60
+ }
61
+ validateOpts.requireNonEmptyString(opts.sectorAnnex,
62
+ "nis2.report.create: opts.sectorAnnex is required (e.g. 'I.6' for drinking water, 'II.6' for digital-providers)",
63
+ Nis2ReportError, "nis2-report/bad-sector");
64
+ var entityId = opts.entityId;
65
+ var entityType = opts.entityType;
66
+ var sectorAnnex = opts.sectorAnnex;
67
+ var csirtEndpoint = opts.csirtEndpoint || null;
68
+ var httpClient = opts.httpClient || null;
69
+ var auditOn = opts.audit !== false;
70
+
71
+ var ir = incidentReport().create({
72
+ audit: opts.audit,
73
+ persist: opts.persist,
74
+ now: opts.now,
75
+ deadlines: {
76
+ initial: C.TIME.hours(24), // NIS2 Art. 23 §4(a) — early warning
77
+ intermediate: C.TIME.hours(72), // NIS2 Art. 23 §4(b) — incident notification
78
+ final: C.TIME.days(30), // NIS2 Art. 23 §4(d) — final report (1 month)
79
+ },
80
+ });
81
+
82
+ function _emitAudit(action, outcome, metadata) {
83
+ if (!auditOn) return;
84
+ try {
85
+ audit().safeEmit({
86
+ action: "nis2.report." + action,
87
+ outcome: outcome,
88
+ metadata: metadata || {},
89
+ });
90
+ } catch (_e) { /* drop-silent */ }
91
+ }
92
+
93
+ async function _submitToCsirt(payload) {
94
+ if (!csirtEndpoint || !httpClient) {
95
+ _emitAudit("submit_skipped", "warning", { reason: "no-endpoint-or-client" });
96
+ return { submitted: false, reason: "no-endpoint-or-client" };
97
+ }
98
+ try {
99
+ var res = await httpClient.request({
100
+ url: csirtEndpoint, method: "POST",
101
+ headers: { "Content-Type": "application/json" },
102
+ body: Buffer.from(JSON.stringify(payload), "utf8"),
103
+ responseMode: "always-resolve",
104
+ });
105
+ var ok = res.statusCode >= 200 && res.statusCode < 300; // allow:raw-byte-literal — HTTP status range
106
+ _emitAudit("submitted", ok ? "success" : "failure", { statusCode: res.statusCode });
107
+ return { submitted: ok, statusCode: res.statusCode };
108
+ } catch (e) {
109
+ _emitAudit("submit_failed", "failure", { error: (e && e.message) || String(e) });
110
+ return { submitted: false, error: (e && e.message) || String(e) };
111
+ }
112
+ }
113
+
114
+ function _envelope(stage, incident, fields) {
115
+ return {
116
+ directive: "(EU) 2022/2555",
117
+ article: "23",
118
+ stage: stage, // "early-warning" / "notification" / "final"
119
+ entity: { id: entityId, type: entityType, sector: sectorAnnex },
120
+ incident: {
121
+ id: incident.id,
122
+ detected_at: new Date(incident.detectedAt).toISOString(),
123
+ scope: incident.scope,
124
+ summary: incident.summary,
125
+ impact: incident.impact,
126
+ },
127
+ fields: fields || {},
128
+ };
129
+ }
130
+
131
+ async function open(spec) {
132
+ spec = Object.assign({}, spec || {}, { regime: "nis2" });
133
+ var rec = await ir.open(spec);
134
+ _emitAudit("opened", "success", { incidentId: rec.id, entityId: entityId, entityType: entityType });
135
+ return rec;
136
+ }
137
+
138
+ async function earlyWarning(incidentId, fields) {
139
+ var rec = await ir.recordInitial(incidentId, fields || {});
140
+ var result = { record: rec, submitted: null };
141
+ if (fields && fields.submit === true) {
142
+ result.submitted = await _submitToCsirt(_envelope("early-warning", rec, fields));
143
+ }
144
+ return result;
145
+ }
146
+ async function notification(incidentId, fields) {
147
+ var rec = await ir.recordIntermediate(incidentId, fields || {});
148
+ var result = { record: rec, submitted: null };
149
+ if (fields && fields.submit === true) {
150
+ result.submitted = await _submitToCsirt(_envelope("notification", rec, fields));
151
+ }
152
+ return result;
153
+ }
154
+ async function finalReport(incidentId, fields) {
155
+ var rec = await ir.recordFinal(incidentId, fields || {});
156
+ var result = { record: rec, submitted: null };
157
+ if (fields && fields.submit === true) {
158
+ result.submitted = await _submitToCsirt(_envelope("final", rec, fields));
159
+ }
160
+ return result;
161
+ }
162
+
163
+ return {
164
+ open: open,
165
+ earlyWarning: earlyWarning,
166
+ notification: notification,
167
+ finalReport: finalReport,
168
+ get: function (id) { return ir.get(id); },
169
+ list: function () { return ir.list(); },
170
+ status: function () { return ir.status(); },
171
+ entityId: entityId,
172
+ entityType: entityType,
173
+ sectorAnnex: sectorAnnex,
174
+ };
175
+ }
176
+
177
+ module.exports = {
178
+ create: create,
179
+ Nis2ReportError: Nis2ReportError,
180
+ VALID_ENTITY_TYPES: Object.keys(VALID_ENTITY_TYPES),
181
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.8.9",
3
+ "version": "0.8.10",
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:b2addb57-b40a-4a44-9c34-7f7bd7d741c3",
5
+ "serialNumber": "urn:uuid:4c9e2f2f-795d-4920-a093-90770087d324",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-07T02:18:24.599Z",
8
+ "timestamp": "2026-05-07T02:38:14.544Z",
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.9",
22
+ "bom-ref": "@blamejs/core@0.8.10",
23
23
  "type": "library",
24
24
  "name": "blamejs",
25
- "version": "0.8.9",
25
+ "version": "0.8.10",
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.9",
29
+ "purl": "pkg:npm/%40blamejs/core@0.8.10",
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.9",
57
+ "ref": "@blamejs/core@0.8.10",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]