@blamejs/core 0.8.8 → 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,10 @@ 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
+
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.
14
+
11
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.
12
16
 
13
17
  - **0.8.7** (2026-05-06) — `b.auth.accessLock` — three-mode access-lock primitive for stop-the-world / read-only / role-restricted operator interventions. `"open"` is normal operation; `"read-only"` refuses non-idempotent methods (POST/PUT/PATCH/DELETE) with 503 + Retry-After while letting GET/HEAD/OPTIONS pass; `"locked"` refuses everything except an operator-supplied `passthroughPaths` allowlist (status / health / unlock endpoint). Operators flip modes during incident response, schema-migration windows, or break-glass review via `lock.set("locked", { actor, reason })`; the transition emits `auth.access_lock.mode_changed` audit + metric. `unlockRoles: ["sre", ...]` lets a privileged role bypass all three modes via `getRole(req)` so a break-glass operator can always reach the unlock endpoint to flip back. The boot-time mode emits `auth.access_lock.boot` so the audit chain captures the deploy posture.
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");
@@ -248,6 +250,10 @@ module.exports = {
248
250
  objectStore: objectStore,
249
251
  retry: retry,
250
252
  circuitBreaker: require("./lib/circuit-breaker"),
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") },
251
257
  queue: queue,
252
258
  logStream: logStream,
253
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
+ };