@blamejs/core 0.8.9 → 0.8.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
+ };