@blamejs/core 0.8.24 → 0.8.26
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 +0 -0
- package/index.js +4 -0
- package/lib/audit.js +2 -0
- package/lib/iab-tcf.js +356 -0
- package/lib/sec-cyber.js +214 -0
- package/package.json +1 -1
- package/sbom.cyclonedx.json +6 -6
package/CHANGELOG.md
CHANGED
|
Binary file
|
package/index.js
CHANGED
|
@@ -101,6 +101,8 @@ var aiInput = require("./lib/ai-input");
|
|
|
101
101
|
var a2a = require("./lib/a2a");
|
|
102
102
|
var darkPatterns = require("./lib/dark-patterns");
|
|
103
103
|
var budr = require("./lib/budr");
|
|
104
|
+
var secCyber = require("./lib/sec-cyber");
|
|
105
|
+
var iabTcf = require("./lib/iab-tcf");
|
|
104
106
|
var safeUrl = require("./lib/safe-url");
|
|
105
107
|
var safeRedirect = require("./lib/safe-redirect");
|
|
106
108
|
var pick = require("./lib/pick");
|
|
@@ -289,6 +291,8 @@ module.exports = {
|
|
|
289
291
|
a2a: a2a,
|
|
290
292
|
darkPatterns: darkPatterns,
|
|
291
293
|
budr: budr,
|
|
294
|
+
secCyber: secCyber,
|
|
295
|
+
iabTcf: iabTcf,
|
|
292
296
|
safeUrl: safeUrl,
|
|
293
297
|
safeRedirect: safeRedirect,
|
|
294
298
|
pick: pick,
|
package/lib/audit.js
CHANGED
|
@@ -239,6 +239,8 @@ var FRAMEWORK_NAMESPACES = [
|
|
|
239
239
|
"a2a", // b.a2a (a2a.card_signed / verified / rejected)
|
|
240
240
|
"darkpatterns", // b.darkPatterns (darkPatterns.attest / cancel-blocked)
|
|
241
241
|
"budr", // b.budr (budr.declared)
|
|
242
|
+
"seccyber", // b.secCyber (seccyber.eight_k_artifact)
|
|
243
|
+
"iabtcf", // b.iabTcf (iabtcf.refused / iabtcf.accepted)
|
|
242
244
|
];
|
|
243
245
|
var registeredNamespaces = new Set(FRAMEWORK_NAMESPACES);
|
|
244
246
|
|
package/lib/iab-tcf.js
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* b.iabTcf — IAB Europe Transparency & Consent Framework v2.3 consent
|
|
4
|
+
* string parser + disclosedVendors validator.
|
|
5
|
+
*
|
|
6
|
+
* Required by TCF Policy v2.3 §III.B.5 (CMP MUST signal which vendors
|
|
7
|
+
* received disclosure regardless of consent state). Deadline 2026-02-28
|
|
8
|
+
* is past — Google Ads + every major DSP rejects v2.2-shaped strings
|
|
9
|
+
* since that date. EU/UK adtech operators that didn't migrate are
|
|
10
|
+
* losing inventory.
|
|
11
|
+
*
|
|
12
|
+
* Consent-string format (TCF v2.3 spec, §A):
|
|
13
|
+
* Base64URL-no-pad of segments separated by `.`:
|
|
14
|
+
* Core | DisclosedVendors | (AllowedVendors) | PublisherTC
|
|
15
|
+
*
|
|
16
|
+
* Core segment carries: cmpVersion=2, version=4 (TCF v2.3),
|
|
17
|
+
* created/lastUpdated, cmpId, vendorListVersion, policyVersion=4,
|
|
18
|
+
* special-feature-opts-in, purpose-consents, purpose-LIs, vendor
|
|
19
|
+
* consents bitmap, vendor LIs bitmap, publisher restrictions.
|
|
20
|
+
*
|
|
21
|
+
* DisclosedVendors segment (REQUIRED in v2.3): bitmap of every
|
|
22
|
+
* vendor disclosed to the user (regardless of consent). v2.2
|
|
23
|
+
* strings omit this segment entirely.
|
|
24
|
+
*
|
|
25
|
+
* Public API:
|
|
26
|
+
*
|
|
27
|
+
* iabTcf.parseString(tcString) -> {
|
|
28
|
+
* core: { version, cmpId, vendorListVersion, policyVersion,
|
|
29
|
+
* createdAt, lastUpdatedAt, vendorConsents, vendorLIs,
|
|
30
|
+
* ... },
|
|
31
|
+
* disclosedVendors: { present, vendorIds: Set<int> } | null,
|
|
32
|
+
* allowedVendors: { present, vendorIds: Set<int> } | null,
|
|
33
|
+
* publisherTC: { present, ... } | null,
|
|
34
|
+
* errors: Array<string>,
|
|
35
|
+
* }
|
|
36
|
+
*
|
|
37
|
+
* iabTcf.requireV23Disclosed(tcString) -> void
|
|
38
|
+
* Throws iabTcf.IabTcfError on:
|
|
39
|
+
* - missing DisclosedVendors segment (v2.2 string)
|
|
40
|
+
* - core.version !== 4 (TCF v2.3 = version 4 in the spec
|
|
41
|
+
* encoding; v2.2 = version 2 or 3 depending on revision —
|
|
42
|
+
* the framework refuses anything not v=4 under v2.3 posture)
|
|
43
|
+
* - core.policyVersion !== 4 (TCF Policy v2.3 = policyVersion 4)
|
|
44
|
+
*
|
|
45
|
+
* iabTcf.checkVendor(parsed, vendorId) -> {
|
|
46
|
+
* consented: bool, — vendor id in vendorConsents
|
|
47
|
+
* legitimate: bool, — vendor id in vendorLIs
|
|
48
|
+
* disclosed: bool, — vendor id in disclosedVendors
|
|
49
|
+
* }
|
|
50
|
+
*
|
|
51
|
+
* Operator workflow:
|
|
52
|
+
* var parsed = b.iabTcf.parseString(tcString);
|
|
53
|
+
* b.iabTcf.requireV23Disclosed(tcString); // refuses v2.2
|
|
54
|
+
* var verdict = b.iabTcf.checkVendor(parsed, 755); // Google
|
|
55
|
+
* if (!verdict.consented && !verdict.legitimate) refuseAdRequest();
|
|
56
|
+
*
|
|
57
|
+
* The framework does NOT bundle the IAB Global Vendor List (it's a
|
|
58
|
+
* versioned JSON published at https://vendor-list.consensu.org/v3/
|
|
59
|
+
* vendor-list.json that operators fetch and refresh themselves).
|
|
60
|
+
* `parsed.core.vendorListVersion` is the version the consent string
|
|
61
|
+
* was signed against — operators load that version from their cache.
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
var audit = require("./audit");
|
|
65
|
+
var { defineClass } = require("./framework-error");
|
|
66
|
+
var IabTcfError = defineClass("IabTcfError", { alwaysPermanent: true });
|
|
67
|
+
|
|
68
|
+
// TCF v2.3 spec values.
|
|
69
|
+
var TCF_V23_CORE_VERSION = 4; // allow:raw-byte-literal — TCF spec version, not bytes
|
|
70
|
+
var TCF_V23_POLICY_VERSION = 4; // allow:raw-byte-literal — TCF policy version, not bytes
|
|
71
|
+
// SEGMENT_TYPE_CORE = 0 documented but not declared as a const — the
|
|
72
|
+
// core segment is identified positionally (segment[0]) not by the
|
|
73
|
+
// 3-bit type prefix the secondary segments use.
|
|
74
|
+
var SEGMENT_TYPE_DISCLOSED_VENDORS = 1; // allow:raw-byte-literal — TCF segment-type marker, not bytes
|
|
75
|
+
var SEGMENT_TYPE_ALLOWED_VENDORS = 2; // allow:raw-byte-literal — TCF segment-type marker, not bytes
|
|
76
|
+
var SEGMENT_TYPE_PUBLISHER_TC = 3; // allow:raw-byte-literal — TCF segment-type marker, not bytes
|
|
77
|
+
var MAX_TC_STRING_BYTES = 64 * 1024; // allow:raw-byte-literal — request-payload cap
|
|
78
|
+
|
|
79
|
+
// base64url decode (no padding) → Buffer.
|
|
80
|
+
function _b64urlDecode(s) {
|
|
81
|
+
var padded = s.replace(/-/g, "+").replace(/_/g, "/");
|
|
82
|
+
var pad = padded.length % 4;
|
|
83
|
+
if (pad === 2) padded += "==";
|
|
84
|
+
else if (pad === 3) padded += "=";
|
|
85
|
+
else if (pad === 1) throw IabTcfError.factory("BAD_BASE64",
|
|
86
|
+
"iabTcf: base64url segment has invalid length");
|
|
87
|
+
return Buffer.from(padded, "base64");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Bit-level reader over a Buffer.
|
|
91
|
+
function _bitReader(buf) {
|
|
92
|
+
var bitOffset = 0;
|
|
93
|
+
var totalBits = buf.length * 8; // allow:raw-byte-literal — bits per byte
|
|
94
|
+
function read(n) {
|
|
95
|
+
if (bitOffset + n > totalBits) {
|
|
96
|
+
throw IabTcfError.factory("BAD_LENGTH",
|
|
97
|
+
"iabTcf: read past end of segment (offset=" + bitOffset + " want=" + n + " total=" + totalBits + ")");
|
|
98
|
+
}
|
|
99
|
+
var v = 0;
|
|
100
|
+
for (var i = 0; i < n; i += 1) {
|
|
101
|
+
var byteIdx = (bitOffset + i) >> 3;
|
|
102
|
+
var bitIdx = 7 - ((bitOffset + i) & 7); // allow:raw-byte-literal — high-bit-first ordering
|
|
103
|
+
v = (v << 1) | ((buf[byteIdx] >> bitIdx) & 1);
|
|
104
|
+
}
|
|
105
|
+
bitOffset += n;
|
|
106
|
+
return v;
|
|
107
|
+
}
|
|
108
|
+
function readBitField(n) {
|
|
109
|
+
// Returns Set<int> of 1-based positions where the bit is set.
|
|
110
|
+
var ids = new Set();
|
|
111
|
+
for (var i = 0; i < n; i += 1) {
|
|
112
|
+
if (read(1) === 1) ids.add(i + 1);
|
|
113
|
+
}
|
|
114
|
+
return ids;
|
|
115
|
+
}
|
|
116
|
+
function pos() { return bitOffset; }
|
|
117
|
+
function setPos(n) { bitOffset = n; }
|
|
118
|
+
function remaining() { return totalBits - bitOffset; }
|
|
119
|
+
return { read: read, readBitField: readBitField, pos: pos, setPos: setPos, remaining: remaining, totalBits: totalBits };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function _parseCore(buf) {
|
|
123
|
+
var r = _bitReader(buf);
|
|
124
|
+
var version = r.read(6); // allow:raw-byte-literal — TCF spec field width, not bytes
|
|
125
|
+
var createdRaw = r.read(36); // allow:raw-byte-literal — TCF spec field width
|
|
126
|
+
var lastUpdatedRaw = r.read(36); // allow:raw-byte-literal — TCF spec field width
|
|
127
|
+
var cmpId = r.read(12); // allow:raw-byte-literal — TCF spec field width
|
|
128
|
+
var cmpVersion = r.read(12); // allow:raw-byte-literal — TCF spec field width
|
|
129
|
+
var consentScreen = r.read(6); // allow:raw-byte-literal — TCF spec field width
|
|
130
|
+
// ConsentLanguage (12 bits = 2 chars × 6 bits, ASCII A-Z mapped 0-25)
|
|
131
|
+
var lang0 = r.read(6); // allow:raw-byte-literal — TCF spec field width
|
|
132
|
+
var lang1 = r.read(6); // allow:raw-byte-literal — TCF spec field width
|
|
133
|
+
var consentLanguage = String.fromCharCode(0x41 + lang0) + String.fromCharCode(0x41 + lang1); // allow:raw-byte-literal — ASCII 'A' offset
|
|
134
|
+
var vendorListVersion = r.read(12); // allow:raw-byte-literal — TCF spec field width
|
|
135
|
+
var policyVersion = r.read(6); // allow:raw-byte-literal — TCF spec field width
|
|
136
|
+
var isServiceSpecific = r.read(1) === 1;
|
|
137
|
+
var useNonStandardStacks = r.read(1) === 1;
|
|
138
|
+
var specialFeatureOptins = r.readBitField(12); // allow:raw-byte-literal — TCF spec field width
|
|
139
|
+
var purposesConsent = r.readBitField(24); // allow:raw-byte-literal — TCF spec field width
|
|
140
|
+
var purposesLI = r.readBitField(24); // allow:raw-byte-literal — TCF spec field width
|
|
141
|
+
var purposeOneTreatment = r.read(1) === 1;
|
|
142
|
+
var publisherCC = String.fromCharCode(0x41 + r.read(6)) + String.fromCharCode(0x41 + r.read(6)); // allow:raw-byte-literal — TCF spec field width
|
|
143
|
+
// MaxVendorIdConsent + ranged fields skipped for compactness — the
|
|
144
|
+
// framework's defensive parse only extracts the top-level shape +
|
|
145
|
+
// the vendorConsents/LIs bitmaps when present.
|
|
146
|
+
var vendorConsents = _parseVendorSection(r);
|
|
147
|
+
var vendorLIs = _parseVendorSection(r);
|
|
148
|
+
return {
|
|
149
|
+
version: version,
|
|
150
|
+
createdAt: createdRaw * 100, // allow:raw-time-literal — TCF spec deciseconds → ms
|
|
151
|
+
lastUpdatedAt: lastUpdatedRaw * 100, // allow:raw-time-literal — TCF spec deciseconds → ms
|
|
152
|
+
cmpId: cmpId,
|
|
153
|
+
cmpVersion: cmpVersion,
|
|
154
|
+
consentScreen: consentScreen,
|
|
155
|
+
consentLanguage: consentLanguage,
|
|
156
|
+
vendorListVersion: vendorListVersion,
|
|
157
|
+
policyVersion: policyVersion,
|
|
158
|
+
isServiceSpecific: isServiceSpecific,
|
|
159
|
+
useNonStandardStacks: useNonStandardStacks,
|
|
160
|
+
specialFeatureOptins: specialFeatureOptins,
|
|
161
|
+
purposesConsent: purposesConsent,
|
|
162
|
+
purposesLI: purposesLI,
|
|
163
|
+
purposeOneTreatment: purposeOneTreatment,
|
|
164
|
+
publisherCC: publisherCC,
|
|
165
|
+
vendorConsents: vendorConsents,
|
|
166
|
+
vendorLIs: vendorLIs,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Vendor section: MaxVendorId (16 bits) + IsRangeEncoding (1 bit) +
|
|
171
|
+
// either bitmap (MaxVendorId bits) or RangeEntries.
|
|
172
|
+
function _parseVendorSection(r) {
|
|
173
|
+
var maxVendorId = r.read(16); // allow:raw-byte-literal — TCF spec field width
|
|
174
|
+
var isRangeEncoding = r.read(1) === 1;
|
|
175
|
+
var ids = new Set();
|
|
176
|
+
if (isRangeEncoding) {
|
|
177
|
+
var numEntries = r.read(12); // allow:raw-byte-literal — TCF spec field width
|
|
178
|
+
for (var i = 0; i < numEntries; i += 1) {
|
|
179
|
+
var isRange = r.read(1) === 1;
|
|
180
|
+
var startVendorId = r.read(16); // allow:raw-byte-literal — TCF spec field width
|
|
181
|
+
if (isRange) {
|
|
182
|
+
var endVendorId = r.read(16); // allow:raw-byte-literal — TCF spec field width
|
|
183
|
+
for (var v = startVendorId; v <= endVendorId; v += 1) ids.add(v);
|
|
184
|
+
} else {
|
|
185
|
+
ids.add(startVendorId);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
} else {
|
|
189
|
+
for (var b = 0; b < maxVendorId; b += 1) {
|
|
190
|
+
if (r.read(1) === 1) ids.add(b + 1);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return { maxVendorId: maxVendorId, ids: ids };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// DisclosedVendors / AllowedVendors segments share the same shape:
|
|
197
|
+
// SegmentType (3 bits) + MaxVendorId + IsRangeEncoding + section.
|
|
198
|
+
function _parseSecondaryVendorSegment(buf, expectedType) {
|
|
199
|
+
var r = _bitReader(buf);
|
|
200
|
+
var segType = r.read(3); // allow:raw-byte-literal — TCF spec field width
|
|
201
|
+
if (segType !== expectedType) {
|
|
202
|
+
throw IabTcfError.factory("BAD_SEGMENT_TYPE",
|
|
203
|
+
"iabTcf: expected segment type " + expectedType + ", got " + segType);
|
|
204
|
+
}
|
|
205
|
+
return _parseVendorSection(r);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function parseString(tcString) {
|
|
209
|
+
if (typeof tcString !== "string" || tcString.length === 0) {
|
|
210
|
+
throw IabTcfError.factory("BAD_INPUT",
|
|
211
|
+
"iabTcf.parseString: tcString must be a non-empty string");
|
|
212
|
+
}
|
|
213
|
+
if (tcString.length > MAX_TC_STRING_BYTES) {
|
|
214
|
+
throw IabTcfError.factory("INPUT_TOO_LARGE",
|
|
215
|
+
"iabTcf.parseString: tcString exceeds " + MAX_TC_STRING_BYTES + " bytes");
|
|
216
|
+
}
|
|
217
|
+
var segments = tcString.split(".");
|
|
218
|
+
var coreBuf;
|
|
219
|
+
try { coreBuf = _b64urlDecode(segments[0]); }
|
|
220
|
+
catch (e) {
|
|
221
|
+
throw IabTcfError.factory("BAD_CORE",
|
|
222
|
+
"iabTcf.parseString: core segment base64url decode failed: " + e.message);
|
|
223
|
+
}
|
|
224
|
+
var core = _parseCore(coreBuf);
|
|
225
|
+
|
|
226
|
+
var disclosedVendors = null;
|
|
227
|
+
var allowedVendors = null;
|
|
228
|
+
var publisherTC = null;
|
|
229
|
+
var errors = [];
|
|
230
|
+
|
|
231
|
+
for (var i = 1; i < segments.length; i += 1) {
|
|
232
|
+
var segBuf;
|
|
233
|
+
try { segBuf = _b64urlDecode(segments[i]); }
|
|
234
|
+
catch (e) {
|
|
235
|
+
errors.push("segment[" + i + "] base64 decode: " + e.message);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (segBuf.length === 0) continue;
|
|
239
|
+
var segType = (segBuf[0] >> 5) & 0x07; // allow:raw-byte-literal — TCF segment-type lives in top 3 bits
|
|
240
|
+
try {
|
|
241
|
+
if (segType === SEGMENT_TYPE_DISCLOSED_VENDORS) {
|
|
242
|
+
disclosedVendors = { present: true, vendorIds: _parseSecondaryVendorSegment(segBuf, SEGMENT_TYPE_DISCLOSED_VENDORS).ids };
|
|
243
|
+
} else if (segType === SEGMENT_TYPE_ALLOWED_VENDORS) {
|
|
244
|
+
allowedVendors = { present: true, vendorIds: _parseSecondaryVendorSegment(segBuf, SEGMENT_TYPE_ALLOWED_VENDORS).ids };
|
|
245
|
+
} else if (segType === SEGMENT_TYPE_PUBLISHER_TC) {
|
|
246
|
+
publisherTC = { present: true };
|
|
247
|
+
} else {
|
|
248
|
+
errors.push("segment[" + i + "] unknown type: " + segType);
|
|
249
|
+
}
|
|
250
|
+
} catch (e) {
|
|
251
|
+
errors.push("segment[" + i + "] parse: " + e.message);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
core: core,
|
|
257
|
+
disclosedVendors: disclosedVendors,
|
|
258
|
+
allowedVendors: allowedVendors,
|
|
259
|
+
publisherTC: publisherTC,
|
|
260
|
+
errors: errors,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function requireV23Disclosed(tcString, opts) {
|
|
265
|
+
opts = opts || {};
|
|
266
|
+
var auditOn = opts.audit !== false;
|
|
267
|
+
var parsed;
|
|
268
|
+
try { parsed = parseString(tcString); }
|
|
269
|
+
catch (e) {
|
|
270
|
+
if (auditOn) {
|
|
271
|
+
audit.safeEmit({
|
|
272
|
+
action: "iabtcf.refused",
|
|
273
|
+
outcome: "denied",
|
|
274
|
+
reason: "parse_failure",
|
|
275
|
+
metadata: { error: e.message },
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
throw e;
|
|
279
|
+
}
|
|
280
|
+
if (parsed.core.version !== TCF_V23_CORE_VERSION) {
|
|
281
|
+
if (auditOn) {
|
|
282
|
+
audit.safeEmit({
|
|
283
|
+
action: "iabtcf.refused",
|
|
284
|
+
outcome: "denied",
|
|
285
|
+
reason: "wrong_core_version",
|
|
286
|
+
metadata: { coreVersion: parsed.core.version, required: TCF_V23_CORE_VERSION },
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
throw IabTcfError.factory("WRONG_CORE_VERSION",
|
|
290
|
+
"iabTcf: core version " + parsed.core.version + " not v2.3 (required " +
|
|
291
|
+
TCF_V23_CORE_VERSION + ")");
|
|
292
|
+
}
|
|
293
|
+
if (parsed.core.policyVersion !== TCF_V23_POLICY_VERSION) {
|
|
294
|
+
if (auditOn) {
|
|
295
|
+
audit.safeEmit({
|
|
296
|
+
action: "iabtcf.refused",
|
|
297
|
+
outcome: "denied",
|
|
298
|
+
reason: "wrong_policy_version",
|
|
299
|
+
metadata: { policyVersion: parsed.core.policyVersion, required: TCF_V23_POLICY_VERSION },
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
throw IabTcfError.factory("WRONG_POLICY_VERSION",
|
|
303
|
+
"iabTcf: policy version " + parsed.core.policyVersion + " not v2.3 (required " +
|
|
304
|
+
TCF_V23_POLICY_VERSION + ")");
|
|
305
|
+
}
|
|
306
|
+
if (!parsed.disclosedVendors || !parsed.disclosedVendors.present) {
|
|
307
|
+
if (auditOn) {
|
|
308
|
+
audit.safeEmit({
|
|
309
|
+
action: "iabtcf.refused",
|
|
310
|
+
outcome: "denied",
|
|
311
|
+
reason: "missing_disclosed_vendors",
|
|
312
|
+
metadata: {},
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
throw IabTcfError.factory("MISSING_DISCLOSED_VENDORS",
|
|
316
|
+
"iabTcf: TC string lacks DisclosedVendors segment (TCF v2.3 §III.B.5 — REQUIRED since 2026-02-28)");
|
|
317
|
+
}
|
|
318
|
+
if (auditOn) {
|
|
319
|
+
audit.safeEmit({
|
|
320
|
+
action: "iabtcf.accepted",
|
|
321
|
+
outcome: "success",
|
|
322
|
+
metadata: {
|
|
323
|
+
cmpId: parsed.core.cmpId,
|
|
324
|
+
vendorListVersion: parsed.core.vendorListVersion,
|
|
325
|
+
disclosedVendorCount: parsed.disclosedVendors.vendorIds.size,
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
return parsed;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function checkVendor(parsed, vendorId) {
|
|
333
|
+
if (!parsed || !parsed.core) {
|
|
334
|
+
throw IabTcfError.factory("BAD_PARSED",
|
|
335
|
+
"iabTcf.checkVendor: parsed object required (call parseString first)");
|
|
336
|
+
}
|
|
337
|
+
if (typeof vendorId !== "number" || !isFinite(vendorId) || vendorId < 1 ||
|
|
338
|
+
Math.floor(vendorId) !== vendorId) {
|
|
339
|
+
throw IabTcfError.factory("BAD_VENDOR_ID",
|
|
340
|
+
"iabTcf.checkVendor: vendorId must be a positive integer");
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
consented: parsed.core.vendorConsents.ids.has(vendorId),
|
|
344
|
+
legitimate: parsed.core.vendorLIs.ids.has(vendorId),
|
|
345
|
+
disclosed: parsed.disclosedVendors && parsed.disclosedVendors.vendorIds.has(vendorId) || false,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
module.exports = {
|
|
350
|
+
parseString: parseString,
|
|
351
|
+
requireV23Disclosed: requireV23Disclosed,
|
|
352
|
+
checkVendor: checkVendor,
|
|
353
|
+
IabTcfError: IabTcfError,
|
|
354
|
+
TCF_V23_CORE_VERSION: TCF_V23_CORE_VERSION,
|
|
355
|
+
TCF_V23_POLICY_VERSION: TCF_V23_POLICY_VERSION,
|
|
356
|
+
};
|
package/lib/sec-cyber.js
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* b.secCyber — SEC Cybersecurity Disclosure Item 1.05 (Form 8-K)
|
|
4
|
+
* artifact generator.
|
|
5
|
+
*
|
|
6
|
+
* Required by 17 CFR §229.106 / Form 8-K Item 1.05 (final rule
|
|
7
|
+
* effective 2023-12-18). When a registrant determines that a
|
|
8
|
+
* cybersecurity incident is material, it MUST file a Form 8-K within
|
|
9
|
+
* 4 business days of the materiality determination, describing:
|
|
10
|
+
*
|
|
11
|
+
* - The material aspects of the nature, scope, and timing
|
|
12
|
+
* - The material impact or reasonably likely material impact on
|
|
13
|
+
* the registrant (financial condition + results of operations)
|
|
14
|
+
*
|
|
15
|
+
* Materiality determination MUST be made "without unreasonable
|
|
16
|
+
* delay." The Attorney General can authorize a delay (when public
|
|
17
|
+
* disclosure would pose substantial risk to national security or
|
|
18
|
+
* public safety) — registrant requests the delay before the 4-day
|
|
19
|
+
* window elapses.
|
|
20
|
+
*
|
|
21
|
+
* The framework can't decide materiality (that's a fact-and-circum-
|
|
22
|
+
* stances judgment). What it CAN do:
|
|
23
|
+
*
|
|
24
|
+
* - Structure the operator's materiality finding into a
|
|
25
|
+
* tamper-evident audit-chain row (the regulator-facing record).
|
|
26
|
+
* - Generate the 8-K Item 1.05 narrative skeleton with the
|
|
27
|
+
* operator's content slotted in.
|
|
28
|
+
* - Compute the 4-business-day deadline so the operator's
|
|
29
|
+
* filing-system gate refuses to slip past it.
|
|
30
|
+
* - Emit an AG-delay-request artifact when the operator asserts
|
|
31
|
+
* national-security / public-safety risk.
|
|
32
|
+
*
|
|
33
|
+
* Public API:
|
|
34
|
+
*
|
|
35
|
+
* b.secCyber.eightKArtifact(opts) -> { artifact, deadline, audit }
|
|
36
|
+
* opts:
|
|
37
|
+
* incidentId: operator-supplied incident reference (string).
|
|
38
|
+
* registrant: { name, cik, filer }
|
|
39
|
+
* detectedAt: Unix-ms when the incident was detected.
|
|
40
|
+
* materialityDeterminedAt: Unix-ms when materiality was determined.
|
|
41
|
+
* materialityFinding: "material" | "not-material" | "pending".
|
|
42
|
+
* materialityReasoning: operator-provided narrative
|
|
43
|
+
* explaining the materiality call.
|
|
44
|
+
* nature: string describing the incident's nature.
|
|
45
|
+
* scope: string describing the scope.
|
|
46
|
+
* timing: string describing the timing.
|
|
47
|
+
* impact: string describing material/likely-material
|
|
48
|
+
* impact on financial condition + operations.
|
|
49
|
+
* agDelayRequested: bool. When true, the artifact includes the
|
|
50
|
+
* AG-delay-request template and the 4-day
|
|
51
|
+
* deadline is suspended pending DOJ response.
|
|
52
|
+
* agDelayJustification: string explaining the national-security
|
|
53
|
+
* / public-safety risk that justifies delay
|
|
54
|
+
* (REQUIRED when agDelayRequested = true).
|
|
55
|
+
* audit: bool, default true.
|
|
56
|
+
*
|
|
57
|
+
* Returns:
|
|
58
|
+
* artifact: structured 8-K Item 1.05 content (markdown
|
|
59
|
+
* + JSON for downstream EDGAR filing).
|
|
60
|
+
* deadline: Unix-ms 4-business-day deadline (null when
|
|
61
|
+
* AG-delay-requested).
|
|
62
|
+
* deadlineBusinessDays: business-day count (4 by default; spec
|
|
63
|
+
* gives no exception).
|
|
64
|
+
*
|
|
65
|
+
* The framework does NOT submit to EDGAR — operators wire the
|
|
66
|
+
* artifact into their existing filer-attorney workflow.
|
|
67
|
+
*/
|
|
68
|
+
|
|
69
|
+
var audit = require("./audit");
|
|
70
|
+
var C = require("./constants");
|
|
71
|
+
var validateOpts = require("./validate-opts");
|
|
72
|
+
var nb = require("./numeric-bounds");
|
|
73
|
+
var { defineClass } = require("./framework-error");
|
|
74
|
+
var SecCyberError = defineClass("SecCyberError", { alwaysPermanent: true });
|
|
75
|
+
|
|
76
|
+
var FINDINGS = ["material", "not-material", "pending"];
|
|
77
|
+
|
|
78
|
+
function _addBusinessDays(startMs, days) {
|
|
79
|
+
// Walk forward N business days (Mon-Fri). Doesn't honor US federal
|
|
80
|
+
// holidays — operators with a calendar-aware filing system override
|
|
81
|
+
// by reading deadlineBusinessDays and computing themselves.
|
|
82
|
+
var t = new Date(startMs);
|
|
83
|
+
var added = 0;
|
|
84
|
+
while (added < days) {
|
|
85
|
+
t = new Date(t.getTime() + C.TIME.days(1));
|
|
86
|
+
var dow = t.getUTCDay();
|
|
87
|
+
if (dow !== 0 && dow !== 6) added += 1;
|
|
88
|
+
}
|
|
89
|
+
return t.getTime();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function eightKArtifact(opts) {
|
|
93
|
+
if (!opts || typeof opts !== "object") {
|
|
94
|
+
throw SecCyberError.factory("BAD_OPTS",
|
|
95
|
+
"secCyber.eightKArtifact: opts required");
|
|
96
|
+
}
|
|
97
|
+
validateOpts.requireNonEmptyString(opts.incidentId,
|
|
98
|
+
"secCyber.eightKArtifact: incidentId", SecCyberError, "BAD_INCIDENT_ID");
|
|
99
|
+
if (!opts.registrant || typeof opts.registrant !== "object") {
|
|
100
|
+
throw SecCyberError.factory("BAD_REGISTRANT",
|
|
101
|
+
"secCyber.eightKArtifact: registrant object required");
|
|
102
|
+
}
|
|
103
|
+
validateOpts.requireNonEmptyString(opts.registrant.name,
|
|
104
|
+
"secCyber.eightKArtifact: registrant.name", SecCyberError, "BAD_REGISTRANT_NAME");
|
|
105
|
+
validateOpts.requireNonEmptyString(opts.registrant.cik,
|
|
106
|
+
"secCyber.eightKArtifact: registrant.cik", SecCyberError, "BAD_CIK");
|
|
107
|
+
nb.requirePositiveFiniteIntIfPresent(opts.detectedAt,
|
|
108
|
+
"secCyber.eightKArtifact: detectedAt", SecCyberError, "BAD_DETECTED_AT");
|
|
109
|
+
nb.requirePositiveFiniteIntIfPresent(opts.materialityDeterminedAt,
|
|
110
|
+
"secCyber.eightKArtifact: materialityDeterminedAt", SecCyberError, "BAD_MAT_AT");
|
|
111
|
+
|
|
112
|
+
if (FINDINGS.indexOf(opts.materialityFinding) === -1) {
|
|
113
|
+
throw SecCyberError.factory("BAD_FINDING",
|
|
114
|
+
"secCyber.eightKArtifact: materialityFinding must be one of " + FINDINGS.join(", "));
|
|
115
|
+
}
|
|
116
|
+
validateOpts.requireNonEmptyString(opts.materialityReasoning,
|
|
117
|
+
"secCyber.eightKArtifact: materialityReasoning", SecCyberError, "BAD_REASONING");
|
|
118
|
+
|
|
119
|
+
if (opts.materialityFinding === "material") {
|
|
120
|
+
validateOpts.requireNonEmptyString(opts.nature,
|
|
121
|
+
"secCyber.eightKArtifact: nature", SecCyberError, "BAD_NATURE");
|
|
122
|
+
validateOpts.requireNonEmptyString(opts.scope,
|
|
123
|
+
"secCyber.eightKArtifact: scope", SecCyberError, "BAD_SCOPE");
|
|
124
|
+
validateOpts.requireNonEmptyString(opts.timing,
|
|
125
|
+
"secCyber.eightKArtifact: timing", SecCyberError, "BAD_TIMING");
|
|
126
|
+
validateOpts.requireNonEmptyString(opts.impact,
|
|
127
|
+
"secCyber.eightKArtifact: impact", SecCyberError, "BAD_IMPACT");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
var agDelayRequested = opts.agDelayRequested === true;
|
|
131
|
+
if (agDelayRequested) {
|
|
132
|
+
validateOpts.requireNonEmptyString(opts.agDelayJustification,
|
|
133
|
+
"secCyber.eightKArtifact: agDelayJustification (required when agDelayRequested=true)",
|
|
134
|
+
SecCyberError, "BAD_AG_JUSTIFICATION");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
var matAt = opts.materialityDeterminedAt || Date.now();
|
|
138
|
+
var deadline = agDelayRequested ? null : _addBusinessDays(matAt, 4);
|
|
139
|
+
|
|
140
|
+
var markdown = "# Form 8-K — Item 1.05 Material Cybersecurity Incident\n\n" +
|
|
141
|
+
"**Registrant:** " + opts.registrant.name + " (CIK: " + opts.registrant.cik + ")\n\n" +
|
|
142
|
+
"**Incident ID:** " + opts.incidentId + "\n\n" +
|
|
143
|
+
"**Materiality determination date:** " + new Date(matAt).toISOString() + "\n\n" +
|
|
144
|
+
"**Materiality finding:** " + opts.materialityFinding + "\n\n" +
|
|
145
|
+
"**Reasoning:**\n\n" + opts.materialityReasoning + "\n\n";
|
|
146
|
+
|
|
147
|
+
if (opts.materialityFinding === "material") {
|
|
148
|
+
markdown +=
|
|
149
|
+
"## Item 1.05(a) — Material aspects\n\n" +
|
|
150
|
+
"**Nature.** " + opts.nature + "\n\n" +
|
|
151
|
+
"**Scope.** " + opts.scope + "\n\n" +
|
|
152
|
+
"**Timing.** " + opts.timing + "\n\n" +
|
|
153
|
+
"## Item 1.05(b) — Material impact\n\n" + opts.impact + "\n\n";
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (agDelayRequested) {
|
|
157
|
+
markdown += "## AG-delay request (17 CFR §229.106(c)(1)(ii))\n\n" +
|
|
158
|
+
"Registrant asserts that disclosure of this incident would pose a substantial " +
|
|
159
|
+
"risk to national security or public safety. Pursuant to the rule, registrant " +
|
|
160
|
+
"requests that the Attorney General authorize a delay of disclosure.\n\n" +
|
|
161
|
+
"**Justification:** " + opts.agDelayJustification + "\n\n";
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
markdown += "**Filing deadline:** " +
|
|
165
|
+
(deadline ? new Date(deadline).toISOString() + " (4 business days from materiality determination)" :
|
|
166
|
+
"suspended pending DOJ response to AG-delay request") + "\n";
|
|
167
|
+
|
|
168
|
+
var artifactJson = {
|
|
169
|
+
form: "8-K",
|
|
170
|
+
item: "1.05",
|
|
171
|
+
incidentId: opts.incidentId,
|
|
172
|
+
registrant: { name: opts.registrant.name, cik: opts.registrant.cik },
|
|
173
|
+
detectedAt: opts.detectedAt || null,
|
|
174
|
+
materialityDeterminedAt: matAt,
|
|
175
|
+
materialityFinding: opts.materialityFinding,
|
|
176
|
+
materialityReasoning: opts.materialityReasoning,
|
|
177
|
+
items: opts.materialityFinding === "material" ? {
|
|
178
|
+
"1.05(a)": {
|
|
179
|
+
nature: opts.nature, scope: opts.scope, timing: opts.timing,
|
|
180
|
+
},
|
|
181
|
+
"1.05(b)": { impact: opts.impact },
|
|
182
|
+
} : null,
|
|
183
|
+
agDelayRequested: agDelayRequested,
|
|
184
|
+
agDelayJustification: agDelayRequested ? opts.agDelayJustification : null,
|
|
185
|
+
deadlineMs: deadline,
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
if (opts.audit !== false) {
|
|
189
|
+
audit.safeEmit({
|
|
190
|
+
action: "seccyber.eight_k_artifact",
|
|
191
|
+
outcome: "success",
|
|
192
|
+
metadata: {
|
|
193
|
+
incidentId: opts.incidentId,
|
|
194
|
+
registrant: opts.registrant.name,
|
|
195
|
+
cik: opts.registrant.cik,
|
|
196
|
+
materialityFinding: opts.materialityFinding,
|
|
197
|
+
deadlineMs: deadline,
|
|
198
|
+
agDelayRequested: agDelayRequested,
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
artifact: { markdown: markdown, json: artifactJson },
|
|
205
|
+
deadline: deadline,
|
|
206
|
+
deadlineBusinessDays: agDelayRequested ? null : 4, // allow:raw-byte-literal — SEC Item 1.05 4-business-day deadline (17 CFR §229.106(c)(1))
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
module.exports = {
|
|
211
|
+
eightKArtifact: eightKArtifact,
|
|
212
|
+
FINDINGS: FINDINGS.slice(),
|
|
213
|
+
SecCyberError: SecCyberError,
|
|
214
|
+
};
|
package/package.json
CHANGED
package/sbom.cyclonedx.json
CHANGED
|
@@ -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:
|
|
5
|
+
"serialNumber": "urn:uuid:cbce5737-d9f9-4793-b198-266fc75eb98e",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-07T13:
|
|
8
|
+
"timestamp": "2026-05-07T13:46:19.961Z",
|
|
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.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.8.26",
|
|
23
23
|
"type": "library",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.8.
|
|
25
|
+
"version": "0.8.26",
|
|
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.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.8.26",
|
|
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.
|
|
57
|
+
"ref": "@blamejs/core@0.8.26",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|