@blamejs/core 0.8.25 → 0.8.27

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
Binary file
package/index.js CHANGED
@@ -102,6 +102,8 @@ var a2a = require("./lib/a2a");
102
102
  var darkPatterns = require("./lib/dark-patterns");
103
103
  var budr = require("./lib/budr");
104
104
  var secCyber = require("./lib/sec-cyber");
105
+ var iabTcf = require("./lib/iab-tcf");
106
+ var fapi2 = require("./lib/fapi2");
105
107
  var safeUrl = require("./lib/safe-url");
106
108
  var safeRedirect = require("./lib/safe-redirect");
107
109
  var pick = require("./lib/pick");
@@ -291,6 +293,8 @@ module.exports = {
291
293
  darkPatterns: darkPatterns,
292
294
  budr: budr,
293
295
  secCyber: secCyber,
296
+ iabTcf: iabTcf,
297
+ fapi2: fapi2,
294
298
  safeUrl: safeUrl,
295
299
  safeRedirect: safeRedirect,
296
300
  pick: pick,
package/lib/audit.js CHANGED
@@ -240,6 +240,8 @@ var FRAMEWORK_NAMESPACES = [
240
240
  "darkpatterns", // b.darkPatterns (darkPatterns.attest / cancel-blocked)
241
241
  "budr", // b.budr (budr.declared)
242
242
  "seccyber", // b.secCyber (seccyber.eight_k_artifact)
243
+ "iabtcf", // b.iabTcf (iabtcf.refused / iabtcf.accepted)
244
+ "fapi2", // b.fapi2 (fapi2.posture_asserted)
243
245
  ];
244
246
  var registeredNamespaces = new Set(FRAMEWORK_NAMESPACES);
245
247
 
package/lib/fapi2.js ADDED
@@ -0,0 +1,165 @@
1
+ "use strict";
2
+ /**
3
+ * b.fapi2 — Financial-grade API 2.0 Final conformance posture.
4
+ *
5
+ * FAPI 2.0 Final (https://openid.net/specs/fapi-2_0-security-profile-FINAL.html)
6
+ * is the OpenID Foundation's security profile for financial / banking
7
+ * APIs. It composes existing IETF + OAuth standards into a single
8
+ * profile that operators MUST satisfy to interoperate with FAPI 2.0
9
+ * client deployments. The composition (per §5):
10
+ *
11
+ * - PAR (Pushed Authorization Requests, RFC 9126) — REQUIRED
12
+ * - PKCE with S256 (RFC 7636) — REQUIRED, PLAIN refused
13
+ * - Sender-constrained tokens via DPoP (RFC 9449) OR mTLS (RFC 8705)
14
+ * — REQUIRED, exactly one
15
+ * - Authorization-server issuer in callback (RFC 9207) — REQUIRED
16
+ * - TLS 1.2+ with FAPI-approved cipher suites (TLS 1.3 default)
17
+ * - JAR (JWT-secured Authorization Request, RFC 9101) when
18
+ * request-object signed
19
+ *
20
+ * The framework already ships every component primitive. FAPI 2.0
21
+ * conformance is therefore a posture-coordination problem: the
22
+ * operator declares the deployment is FAPI-bound, and the framework
23
+ * asserts that every primitive in the chain is configured per the
24
+ * profile.
25
+ *
26
+ * Public API:
27
+ *
28
+ * b.fapi2.assertConformance(opts) -> { conformant, findings }
29
+ * opts:
30
+ * senderConstraint: "dpop" | "mtls" — REQUIRED.
31
+ * parRequired: bool, default true.
32
+ * pkceMethod: must be "S256" (default; refuses "plain").
33
+ * requireIssuerInCallback: bool, default true.
34
+ * requireJarOnSignedRequests: bool, default true.
35
+ *
36
+ * Returns:
37
+ * conformant: bool — every check passed.
38
+ * findings: Array<{ requirement, status, detail? }>
39
+ *
40
+ * b.fapi2.assertOAuthConfig(oauthOpts) -> void
41
+ * Inspects an `b.auth.oauth.create(opts)` configuration object
42
+ * and throws Fapi2Error if any FAPI 2.0 mandate is violated:
43
+ * - PKCE absent / non-S256
44
+ * - state / nonce missing (auto-mint default OK)
45
+ * - Sender-constraint absent
46
+ *
47
+ * b.fapi2.posture() -> "fapi-2.0" | null
48
+ * Returns "fapi-2.0" when b.compliance.set("fapi-2.0") was
49
+ * called, else null. Convenience for code that branches on the
50
+ * posture without calling b.compliance.current() directly.
51
+ *
52
+ * The framework does NOT replace operator OAuth configuration —
53
+ * `b.auth.oauth.create(...)` is still where the operator declares
54
+ * client + scopes + redirect URIs. b.fapi2.assertOAuthConfig is the
55
+ * boot-time gate that refuses to start a FAPI-declared deployment
56
+ * if any mandate is missing.
57
+ */
58
+
59
+ var compliance = require("./compliance");
60
+ var audit = require("./audit");
61
+ var { defineClass } = require("./framework-error");
62
+ var Fapi2Error = defineClass("Fapi2Error", { alwaysPermanent: true });
63
+
64
+ var SENDER_CONSTRAINTS = ["dpop", "mtls"];
65
+
66
+ function assertConformance(opts) {
67
+ if (!opts || typeof opts !== "object") {
68
+ throw Fapi2Error.factory("BAD_OPTS",
69
+ "fapi2.assertConformance: opts required");
70
+ }
71
+ if (SENDER_CONSTRAINTS.indexOf(opts.senderConstraint) === -1) {
72
+ throw Fapi2Error.factory("BAD_SENDER_CONSTRAINT",
73
+ "fapi2.assertConformance: senderConstraint must be 'dpop' or 'mtls'");
74
+ }
75
+ var parRequired = opts.parRequired !== false;
76
+ var pkceMethod = opts.pkceMethod || "S256";
77
+ if (pkceMethod !== "S256") {
78
+ throw Fapi2Error.factory("BAD_PKCE",
79
+ "fapi2.assertConformance: PKCE method must be S256 (FAPI 2.0 §5.3.1.1) — got '" +
80
+ pkceMethod + "'");
81
+ }
82
+ var requireIssuer = opts.requireIssuerInCallback !== false;
83
+ var requireJar = opts.requireJarOnSignedRequests !== false;
84
+
85
+ var findings = [];
86
+ findings.push({ requirement: "pkce-s256", status: "satisfied",
87
+ detail: "PKCE S256 declared (FAPI 2.0 §5.3.1.1)" });
88
+ findings.push({ requirement: "par-required", status: parRequired ? "satisfied" : "WAIVED",
89
+ detail: parRequired
90
+ ? "PAR (RFC 9126) declared required (FAPI 2.0 §5.3.2.2)"
91
+ : "PAR waived by operator — non-conformant unless authorization-server is FAPI-1 fallback" });
92
+ findings.push({ requirement: "sender-constraint", status: "satisfied",
93
+ detail: opts.senderConstraint + " — FAPI 2.0 §5.3.2.5" });
94
+ findings.push({ requirement: "issuer-in-callback", status: requireIssuer ? "satisfied" : "WAIVED",
95
+ detail: requireIssuer
96
+ ? "Issuer in callback (RFC 9207) required"
97
+ : "Issuer-in-callback waived — IdP-mix-up class still open" });
98
+ findings.push({ requirement: "jar-signed-requests", status: requireJar ? "satisfied" : "WAIVED",
99
+ detail: requireJar
100
+ ? "JAR (RFC 9101) required for signed authorization requests"
101
+ : "JAR waived for signed authorization requests" });
102
+
103
+ var conformant = findings.every(function (f) { return f.status === "satisfied"; });
104
+
105
+ audit.safeEmit({
106
+ action: "fapi2.posture_asserted",
107
+ outcome: conformant ? "success" : "warning",
108
+ metadata: {
109
+ senderConstraint: opts.senderConstraint,
110
+ parRequired: parRequired,
111
+ pkceMethod: pkceMethod,
112
+ requireIssuer: requireIssuer,
113
+ requireJar: requireJar,
114
+ conformant: conformant,
115
+ },
116
+ });
117
+
118
+ return { conformant: conformant, findings: findings };
119
+ }
120
+
121
+ function assertOAuthConfig(oauthOpts) {
122
+ if (!oauthOpts || typeof oauthOpts !== "object") {
123
+ throw Fapi2Error.factory("BAD_OAUTH_OPTS",
124
+ "fapi2.assertOAuthConfig: oauth opts required");
125
+ }
126
+ // PKCE — refuse pkce: false (b.auth.oauth.create already does this,
127
+ // but check explicitly for FAPI clarity).
128
+ if (oauthOpts.pkce === false) {
129
+ throw Fapi2Error.factory("PKCE_DISABLED",
130
+ "fapi2.assertOAuthConfig: PKCE is disabled — FAPI 2.0 §5.3.1.1 mandates S256");
131
+ }
132
+ if (oauthOpts.pkceMethod && oauthOpts.pkceMethod !== "S256") {
133
+ throw Fapi2Error.factory("PKCE_NOT_S256",
134
+ "fapi2.assertOAuthConfig: PKCE method '" + oauthOpts.pkceMethod +
135
+ "' is not S256 (FAPI 2.0 §5.3.1.1)");
136
+ }
137
+ // Sender-constraint required
138
+ var hasDpop = oauthOpts.dpop === true || oauthOpts.senderConstraint === "dpop";
139
+ var hasMtls = oauthOpts.mtls === true || oauthOpts.senderConstraint === "mtls";
140
+ if (!hasDpop && !hasMtls) {
141
+ throw Fapi2Error.factory("NO_SENDER_CONSTRAINT",
142
+ "fapi2.assertOAuthConfig: FAPI 2.0 §5.3.2.5 requires sender-constrained tokens via DPoP OR mTLS — neither declared");
143
+ }
144
+ if (hasDpop && hasMtls) {
145
+ throw Fapi2Error.factory("BOTH_SENDER_CONSTRAINTS",
146
+ "fapi2.assertOAuthConfig: declare exactly one of DPoP / mTLS — both creates over-binding ambiguity");
147
+ }
148
+ // PAR
149
+ if (oauthOpts.par === false) {
150
+ throw Fapi2Error.factory("PAR_DISABLED",
151
+ "fapi2.assertOAuthConfig: PAR is disabled — FAPI 2.0 §5.3.2.2 mandates Pushed Authorization Requests");
152
+ }
153
+ }
154
+
155
+ function posture() {
156
+ return compliance.current() === "fapi-2.0" ? "fapi-2.0" : null;
157
+ }
158
+
159
+ module.exports = {
160
+ assertConformance: assertConformance,
161
+ assertOAuthConfig: assertOAuthConfig,
162
+ posture: posture,
163
+ SENDER_CONSTRAINTS: SENDER_CONSTRAINTS.slice(),
164
+ Fapi2Error: Fapi2Error,
165
+ };
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.8.25",
3
+ "version": "0.8.27",
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:a6056d34-71d3-41f1-a99f-80454c5b2780",
5
+ "serialNumber": "urn:uuid:7acc2751-65c7-408f-8206-0688a7f5439e",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-05-07T13:31:44.359Z",
8
+ "timestamp": "2026-05-07T13:52:40.926Z",
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.25",
22
+ "bom-ref": "@blamejs/core@0.8.27",
23
23
  "type": "library",
24
24
  "name": "blamejs",
25
- "version": "0.8.25",
25
+ "version": "0.8.27",
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.25",
29
+ "purl": "pkg:npm/%40blamejs/core@0.8.27",
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.25",
57
+ "ref": "@blamejs/core@0.8.27",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]