@blamejs/blamejs-shop 0.1.33 → 0.1.34
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 +2 -0
- package/lib/asset-manifest.json +1 -1
- package/lib/vendor/MANIFEST.json +2 -2
- package/lib/vendor/blamejs/CHANGELOG.md +2 -0
- package/lib/vendor/blamejs/README.md +1 -1
- package/lib/vendor/blamejs/api-snapshot.json +10 -2
- package/lib/vendor/blamejs/lib/iab-tcf.js +277 -2
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.13.2.json +27 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/iab-tcf.test.js +61 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.1.x
|
|
10
10
|
|
|
11
|
+
- v0.1.34 (2026-05-26) — **Vendored blamejs refreshed to v0.13.0.** Refreshes the vendored blamejs stack from v0.12.11 to v0.13.0 — the single source of every security, crypto, and primitive the shop composes. No application API changes; the refresh keeps the shop current with upstream fixes and broadens the primitive surface the shop builds on. The vendored tree is verified against the release tag by the vendor-integrity gate, and the in-image smoke gate runs against the refreshed tree before any image ships. **Changed:** *Vendored blamejs v0.12.11 → v0.13.0* — The committed copy under `lib/vendor/blamejs/` is updated to the v0.13.0 release tag via the vendor-refresh entry point. Composed primitives are unchanged at the call sites the shop already uses; the refresh carries upstream maintenance and expands the available primitive surface for future shop features.
|
|
12
|
+
|
|
11
13
|
- v0.1.33 (2026-05-26) — **Search filters and synonyms — narrow results by facet, and match what shoppers mean, not just what they typed.** Search gains filters and smarter matching. A search result page now shows facet groups — collection, price range, and in-stock — as filter controls; choosing one narrows the results, the counts beside each option reflect the current result set, and choices combine across groups. Active filters show with a one-click clear and carry through pagination rather than dropping on page two. All filtering is server-side from query parameters, so it works with no client JavaScript. Separately, the query is expanded through an operator-curated synonym and typo vocabulary before the catalog is searched, so a search for "tee" matches "t-shirt" and a near-miss spelling still finds the product, with a "Showing results for" note when the query was corrected. Filters and synonym matching work identically whether the page is served from the edge or the container. This release also moves the shipped theme's stylesheets and island scripts to content-fingerprinted filenames so an asset and the page that references it can never fall out of sync. **Added:** *Faceted search filters* — A search result page shows facet groups — collection, price range, and in-stock availability — as server-rendered filter controls. Selecting a facet narrows the results and is reflected in the URL (for example `/search?q=shirt&collection=summer&in_stock=1`), the count beside each option reflects how many results it would yield against the current query, and facets combine across groups. Active filters render as removable chips with a clear-all path, and the active filters carry through result pagination so page two keeps the filter set. All filtering is server-side from the query string — no client JavaScript — and unknown facet keys, out-of-range price values, and garbage filter values are ignored rather than erroring. With filters applied and no matches, the page shows an empty state with a clear-filters link. · *Synonym and typo-tolerant query matching* — Before the catalog is searched, the query is expanded through an operator-curated vocabulary: synonym groups (so "tee" matches "t-shirt"), common typo corrections, and stopword removal. When the query is corrected or expanded, the page shows a "Showing results for" note. A query that still matches nothing falls back to the raw terms so search never silently empties on an unknown word. **Changed:** *Search filters and synonyms render identically at the edge and the container* — The faceting and synonym logic runs in both the edge worker and the container search path, producing the same results and the same filter chrome whichever serves the page. The edge path reads the catalog and the facet/synonym vocabulary directly from the database, missing-table-resilient, so a shop without the optional facet configuration still searches normally. · *Theme stylesheets and scripts use content-fingerprinted filenames* — The shipped default theme's stylesheets and island scripts are now referenced by a content-hashed filename (`main.<hash>.css`) rather than a `?v=` query, emitted identically by the edge and container renderers and served from object storage under the fingerprinted key. Each URL maps to exactly one byte-content, so an asset and the page that references it can never disagree on a Subresource Integrity digest, and previously-served pages keep working across an asset change. The edge stylesheet link now also carries an integrity attribute, matching the container.
|
|
12
14
|
|
|
13
15
|
- v0.1.32 (2026-05-26) — **Bundles and quantity discounts — sell products together at a set price, and reward buying more.** Two merchandising surfaces go live on the product page and in the cart, both priced authoritatively on the server. A product that belongs to a bundle shows a "Bundle & save" offer with its member products and the bundle price against the sum of the parts; one action adds the whole bundle to the cart at that price, all-or-nothing, and the discount is split across the member lines so the cart subtotal equals the quoted bundle price. Separately, a product with quantity breaks shows its price tiers ("1–2 / 3–5 / 6+") on the page, and the cart applies the right unit price for each line's quantity — recomputed on every cart render and again at checkout, so the line, cart, and order totals all reflect the break. The client never sends a price: it sends a bundle code or a quantity, and the server prices both from the live catalog. A bundle whose member is archived or out of stock is shown unavailable rather than adding a broken line, and a missing offer simply renders nothing. **Added:** *Bundles on the product page* — A product that is part of one or more bundles shows a "Bundle & save" offer listing the bundle's member products, the bundle price, and the saving against buying the parts separately. `POST /cart/bundle` adds the whole bundle to the cart in one action: the bundle price is recomputed from the live catalog and allocated across the member lines (proportional to each member's list price, with the rounding remainder on the last line) so the cart subtotal equals the quoted price. The add is atomic — if any member is archived or out of stock the bundle is shown unavailable and the action adds nothing rather than a partial set. A product in no bundle shows no offer; an unpriceable bundle (a missing member price or a currency mismatch) is shown unavailable, never a server error. The offer renders identically from the edge worker and the container. · *Quantity discounts on the product page and in the cart* — A product with quantity breaks shows its price tiers on the page (for example 1–2, 3–5, 6+ at descending unit prices). In the cart, each line is priced at the unit price for its quantity — applied server-side on every cart render so changing the quantity re-prices the line, and applied again at checkout so the per-line price and the order total written onto the order reflect the break, not the list price. A quantity below the first tier falls back to the base price; a quantity above the top tier takes the top tier. Stacks with the rest of the cart math. The tier table renders identically from the edge worker and the container. **Fixed:** *Themed product template carries the compare control* — The file-backed default product template now includes the Add-to-compare slot, matching the built-in render path so the comparison control appears on the product page whichever template is in use.
|
package/lib/asset-manifest.json
CHANGED
package/lib/vendor/MANIFEST.json
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
"_about": "blamejs.shop vendors a single framework — blamejs — which itself bundles every server-side crypto/identity dependency. The transitive packages blamejs ships are surfaced in its own MANIFEST.json at lib/vendor/blamejs/lib/vendor/MANIFEST.json — Trivy / Grype rely on that nested data for CVE attribution.",
|
|
4
4
|
"packages": {
|
|
5
5
|
"blamejs": {
|
|
6
|
-
"version": "0.13.
|
|
7
|
-
"tag": "v0.13.
|
|
6
|
+
"version": "0.13.2",
|
|
7
|
+
"tag": "v0.13.2",
|
|
8
8
|
"license": "Apache-2.0",
|
|
9
9
|
"author": "blamejs contributors",
|
|
10
10
|
"source": "https://github.com/blamejs/blamejs",
|
|
@@ -8,6 +8,8 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.13.x
|
|
10
10
|
|
|
11
|
+
- v0.13.2 (2026-05-26) — **`b.iabTcf.encode` — write TCF consent strings, and a TC-string timestamp fix.** b.iabTcf gains the encode half of its consent-string codec: b.iabTcf.encode(obj) serialises a parsed object back into an IAB TCF v2 TC string, and b.iabTcf.isValid(tcString) is a total never-throwing validity check. Vendor and purpose collections may be Sets, id arrays, or the parsed sections parseString returns; vendor sections are written with whichever of the bit-field and range forms is smaller, matching the reference CMP encoders, so a parsed string round-trips to an equivalent signal. parseString now fully decodes the Core publisher-restrictions list and the PublisherTC segment's publisher and custom purposes, where it previously reported only the segment's presence. The encoder is verified against the worked-example string in the IAB Tech Lab consent-string specification: it re-encodes that string's Core segment byte-for-byte. This release also fixes a TC-string parsing bug — the bit reader accumulated values with a 32-bit shift, so the 36-bit Created and LastUpdated timestamp fields were silently truncated for any real date; they now decode and round-trip exactly. **Added:** *`b.iabTcf.encode` / `b.iabTcf.isValid`* — `encode(obj)` serialises a TCF object (the shape `parseString` returns) into a TC string — Core plus optional DisclosedVendors, AllowedVendors, and PublisherTC segments — choosing the smaller of the bit-field and range vendor encodings. `isValid(tcString)` returns whether a string parses as a well-formed Core segment without throwing. `parseString` now fully decodes Core publisher restrictions and the PublisherTC purposes that were previously reported only as present. **Fixed:** *TC-string 36-bit timestamps were truncated on parse* — `b.iabTcf.parseString` read multi-bit fields with a 32-bit left-shift accumulation. The 36-bit Created and LastUpdated fields hold deciseconds-since-epoch, which exceeds 2^31 for any date after 1976, so those timestamps were silently corrupted. The reader now accumulates without the 32-bit truncation; timestamps decode correctly and round-trip through `encode`.
|
|
12
|
+
|
|
11
13
|
- v0.13.1 (2026-05-26) — **`b.worm` — write-once-read-many retention.** Store records that cannot be altered or deleted before a retention period elapses — the immutable-storage discipline regulators require (SEC 17a-4(f), CFTC 1.31, FINRA 4511). b.worm.create(opts) returns a WORM store that enforces, on every mutating call, that a record is not overwritten or deleted while it is within its retainUntil window or under a legal hold. Two modes mirror cloud Object-Lock: compliance (the default — no one, including the operator, can delete before expiry) and governance (a privileged caller may override with an audited reason). Retention can only be extended, never shortened; every record carries a SHA3-512 digest that get verifies, so tampering with the underlying bytes is detected on read; every allow/refuse decision is audited. Storage is pluggable via a synchronous store adapter, so the policy layer sits over a sealed DB table, a filesystem, or any non-S3 backend — the store-agnostic, application-level companion to b.objectStore's S3 Object Lock, with content-integrity verification that native Object Lock does not provide. **Added:** *`b.worm.create` — write-once-read-many retention* — Returns a store with `put` / `get` / `delete` / `extendRetention` / `placeLegalHold` / `releaseLegalHold` / `list`. `put` is write-once (an overwrite of a retained or held record is refused); `delete` is gated by the retention window, legal holds, and the mode (`compliance` refuses any early delete; `governance` allows a privileged override with a required, audited reason); `extendRetention` is extend-only; `get` verifies the stored SHA3-512 digest and throws `worm/tampered` on a mismatch. Storage is a pluggable synchronous adapter (`get` / `set` / `delete` / `has` / `keys`), defaulting to in-memory for tests. Use it for SEC 17a-4 / CFTC / FINRA immutable records on backends without native Object Lock; `b.objectStore` remains the path for S3 Object Lock.
|
|
12
14
|
|
|
13
15
|
- v0.13.0 (2026-05-26) — **`b.crypto.oprf` — RFC 9497 Oblivious PRFs.** Compute F(serverKey, input) without the server learning the input and without the client learning the key — the Oblivious PRF primitive behind password hardening (the server peppers a password it never sees), private set intersection, and Privacy Pass. b.crypto.oprf.suite(name) returns an RFC 9497 ciphersuite — ristretto255-sha512, p256-sha256, p384-sha384, or p521-sha512 — each exposing the base oprf mode and the verifiable voprf mode (a DLEQ proof lets the client confirm the server used the key committed in its public key). The client blinds its input, the server blind-evaluates with its secret key, and the client finalizes by un-blinding and hashing; because un-blinding cancels the blind, the output depends only on key and input. Validated byte-for-byte against the RFC 9497 Appendix-A test vectors. Group and hash-to-curve operations come from the newly vendored @noble/curves (Paul Miller, MIT) — the same maintainer as the framework's existing vendored @noble/post-quantum and @noble/ciphers, with no added npm runtime dependency. **Added:** *`b.crypto.oprf` — RFC 9497 OPRF / VOPRF* — `suite(name)` returns `{ name, oprf, voprf }` for one of the four RFC 9497 ciphersuites (ristretto255-SHA512 / P-256-SHA256 / P-384-SHA384 / P-521-SHA512). The `oprf` (base) mode provides `deriveKeyPair` / `generateKeyPair` / `blind` / `blindEvaluate` / `finalize` / `evaluate`; `voprf` (verifiable) adds a DLEQ proof so the client can prove the server used the committed key. Use it for password hardening, private set intersection, and OPRF-based tokens. Verified against the RFC 9497 Appendix-A vectors. The partially-oblivious `poprf` mode is not yet exposed (the vendored `@noble/curves` does not implement it) and will follow upstream. · *Vendored `@noble/curves`* — `@noble/curves` 2.2.0 (Paul Miller, MIT) is vendored under `lib/vendor/` (no npm runtime dependency), supplying the ristretto255 / NIST-curve group and hash-to-curve operations behind `b.crypto.oprf`. It joins the existing vendored `@noble/post-quantum` and `@noble/ciphers` from the same maintainer; tracked in the SBOM and the vendor-currency gate.
|
|
@@ -207,7 +207,7 @@ The framework bundles the surface a typical Node app reaches for. Every primitiv
|
|
|
207
207
|
- **Change control + WORM** — m-of-n approver DDL change-control with maintenance-window + ML-DSA-87 signed proposals (`b.ddlChangeControl`); row-level WORM triggers boot-asserted under `sec-17a-4` / `finra-4511` / `fda-21cfr11` (`b.db.declareWorm`); dual-control physical delete + crypto-erase + REINDEX in one transaction (`b.db.declareRequireDualControl`, `b.db.eraseHard`)
|
|
208
208
|
- **Consumer-protection** — FTC click-to-cancel UX-parity attestation (`ftc-2024` / `ca-sb942` / `strict`) (`b.darkPatterns`)
|
|
209
209
|
- **Differential privacy** — float-safe DP for aggregate releases: snapping-mechanism Laplace (Mironov 2012) + discrete Gaussian (Canonne–Kamath–Steinke 2020), CSPRNG noise, per-scope ε/δ budgets with basic + Rényi-DP accounting; defends the floating-point distinguishing attack that breaks naive Laplace samplers (NIST SP 800-226) (`b.ai.dp`)
|
|
210
|
-
- **Privacy / DSR** — GDPR Articles 15–22 / CCPA / CPRA / LGPD / PIPEDA data-subject-rights workflow (`b.dsr`); IAB TCF v2
|
|
210
|
+
- **Privacy / DSR** — GDPR Articles 15–22 / CCPA / CPRA / LGPD / PIPEDA data-subject-rights workflow (`b.dsr`); IAB TCF v2 consent-string parse + encode + `disclosedVendors` validator (`b.iabTcf`); IAB MSPA / GPP universal-opt-out (USNAT / USCA / USVA / USCO / USCT / USUT) + GPC mirror (`b.iabMspa`); generic consent capture + withdrawal (`b.consent`)
|
|
211
211
|
- **Incident reporters** — EU DORA Article 17 ICT-incident workflow per Commission Delegated Regulation 2024/1772 (`b.dora`); EU NIS2 (`b.nis2`); EU Cyber Resilience Act SBOM + secure-software-attestation (`b.cra`); SEC Form 8-K Item 1.05 cybersecurity-incident materiality-disclosure (`b.secCyber`); incident lifecycle coordinator (`b.incident`)
|
|
212
212
|
- **Outbound DLP** — interceptor-installed on httpClient + mail + webhook with built-in detectors for PAN (Luhn), SSN, EIN, IBAN (mod-97), api-key shapes, PEM, SSH private keys, JWTs, AWS access keys, PHI composite; refuse / redact / audit-only verdicts under pci-dss / hipaa / fapi2 / soc2 / gdpr presets (`b.redact.installOutboundDlp`)
|
|
213
213
|
### Observability
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 1,
|
|
3
|
-
"frameworkVersion": "0.13.
|
|
4
|
-
"createdAt": "2026-05-
|
|
3
|
+
"frameworkVersion": "0.13.2",
|
|
4
|
+
"createdAt": "2026-05-26T22:29:54.847Z",
|
|
5
5
|
"exports": {
|
|
6
6
|
"a2a": {
|
|
7
7
|
"type": "object",
|
|
@@ -36828,6 +36828,14 @@
|
|
|
36828
36828
|
"type": "function",
|
|
36829
36829
|
"arity": 2
|
|
36830
36830
|
},
|
|
36831
|
+
"encode": {
|
|
36832
|
+
"type": "function",
|
|
36833
|
+
"arity": 1
|
|
36834
|
+
},
|
|
36835
|
+
"isValid": {
|
|
36836
|
+
"type": "function",
|
|
36837
|
+
"arity": 1
|
|
36838
|
+
},
|
|
36831
36839
|
"parseString": {
|
|
36832
36840
|
"type": "function",
|
|
36833
36841
|
"arity": 1
|
|
@@ -95,6 +95,7 @@
|
|
|
95
95
|
*/
|
|
96
96
|
|
|
97
97
|
var audit = require("./audit");
|
|
98
|
+
var bCrypto = require("./crypto");
|
|
98
99
|
var { defineClass } = require("./framework-error");
|
|
99
100
|
var IabTcfError = defineClass("IabTcfError", { alwaysPermanent: true });
|
|
100
101
|
|
|
@@ -129,11 +130,15 @@ function _bitReader(buf) {
|
|
|
129
130
|
throw IabTcfError.factory("BAD_LENGTH",
|
|
130
131
|
"iabTcf: read past end of segment (offset=" + bitOffset + " want=" + n + " total=" + totalBits + ")");
|
|
131
132
|
}
|
|
133
|
+
// Accumulate with `* 2`, not `<< 1`: the Created / LastUpdated fields are
|
|
134
|
+
// 36 bits and their deciseconds values exceed 2^31 for any real date, so a
|
|
135
|
+
// 32-bit shift would silently truncate them. `* 2 + bit` stays exact up to
|
|
136
|
+
// 2^53, well above the widest TCF field.
|
|
132
137
|
var v = 0;
|
|
133
138
|
for (var i = 0; i < n; i += 1) {
|
|
134
139
|
var byteIdx = (bitOffset + i) >> 3;
|
|
135
140
|
var bitIdx = 7 - ((bitOffset + i) & 7); // allow:raw-byte-literal — high-bit-first ordering
|
|
136
|
-
v = (v
|
|
141
|
+
v = (v * 2) + ((buf[byteIdx] >> bitIdx) & 1);
|
|
137
142
|
}
|
|
138
143
|
bitOffset += n;
|
|
139
144
|
return v;
|
|
@@ -178,6 +183,7 @@ function _parseCore(buf) {
|
|
|
178
183
|
// the vendorConsents/LIs bitmaps when present.
|
|
179
184
|
var vendorConsents = _parseVendorSection(r);
|
|
180
185
|
var vendorLIs = _parseVendorSection(r);
|
|
186
|
+
var publisherRestrictions = _parsePublisherRestrictions(r);
|
|
181
187
|
return {
|
|
182
188
|
version: version,
|
|
183
189
|
createdAt: createdRaw * 100, // allow:raw-time-literal — TCF spec deciseconds → ms
|
|
@@ -197,6 +203,56 @@ function _parseCore(buf) {
|
|
|
197
203
|
publisherCC: publisherCC,
|
|
198
204
|
vendorConsents: vendorConsents,
|
|
199
205
|
vendorLIs: vendorLIs,
|
|
206
|
+
publisherRestrictions: publisherRestrictions,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Publisher restrictions follow the two core vendor sections:
|
|
211
|
+
// NumPubRestrictions (12 bits) then, per restriction, PurposeId (6) +
|
|
212
|
+
// RestrictionType (2) + a range list of vendor ids. NumPubRestrictions is
|
|
213
|
+
// mandatory even when zero, so a core that ends before it is truncated — the
|
|
214
|
+
// reader's bounds check throws rather than treating the gap as "no
|
|
215
|
+
// restrictions", which would let a malformed string validate.
|
|
216
|
+
function _parsePublisherRestrictions(r) {
|
|
217
|
+
var out = [];
|
|
218
|
+
var num = r.read(12); // allow:raw-byte-literal — TCF spec field width
|
|
219
|
+
for (var i = 0; i < num; i += 1) {
|
|
220
|
+
var purposeId = r.read(6); // allow:raw-byte-literal — TCF spec field width
|
|
221
|
+
var restrictionType = r.read(2);
|
|
222
|
+
var numEntries = r.read(12); // allow:raw-byte-literal — TCF spec field width
|
|
223
|
+
var vendorIds = [];
|
|
224
|
+
for (var e = 0; e < numEntries; e += 1) {
|
|
225
|
+
var isRange = r.read(1) === 1;
|
|
226
|
+
var startVendorId = r.read(16); // allow:raw-byte-literal — TCF spec field width
|
|
227
|
+
if (isRange) {
|
|
228
|
+
var endVendorId = r.read(16); // allow:raw-byte-literal — TCF spec field width
|
|
229
|
+
for (var v = startVendorId; v <= endVendorId; v += 1) vendorIds.push(v);
|
|
230
|
+
} else {
|
|
231
|
+
vendorIds.push(startVendorId);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
out.push({ purposeId: purposeId, restrictionType: restrictionType, vendorIds: vendorIds });
|
|
235
|
+
}
|
|
236
|
+
return out;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// PublisherTC segment (type 3): publisher purpose consent + LI bit-fields
|
|
240
|
+
// then a custom-purpose count and its two bit-fields.
|
|
241
|
+
function _parsePublisherTC(buf) {
|
|
242
|
+
var r = _bitReader(buf);
|
|
243
|
+
r.read(3); // allow:raw-byte-literal — segment-type prefix
|
|
244
|
+
var pubPurposesConsent = r.readBitField(24); // allow:raw-byte-literal — TCF spec field width
|
|
245
|
+
var pubPurposesLI = r.readBitField(24); // allow:raw-byte-literal — TCF spec field width
|
|
246
|
+
var numCustomPurposes = r.read(6); // allow:raw-byte-literal — TCF spec field width
|
|
247
|
+
var customConsent = r.readBitField(numCustomPurposes);
|
|
248
|
+
var customLI = r.readBitField(numCustomPurposes);
|
|
249
|
+
return {
|
|
250
|
+
present: true,
|
|
251
|
+
pubPurposesConsent: pubPurposesConsent,
|
|
252
|
+
pubPurposesLITransparency: pubPurposesLI,
|
|
253
|
+
numCustomPurposes: numCustomPurposes,
|
|
254
|
+
customPurposesConsent: customConsent,
|
|
255
|
+
customPurposesLITransparency: customLI,
|
|
200
256
|
};
|
|
201
257
|
}
|
|
202
258
|
|
|
@@ -298,7 +354,7 @@ function parseString(tcString) {
|
|
|
298
354
|
} else if (segType === SEGMENT_TYPE_ALLOWED_VENDORS) {
|
|
299
355
|
allowedVendors = { present: true, vendorIds: _parseSecondaryVendorSegment(segBuf, SEGMENT_TYPE_ALLOWED_VENDORS).ids };
|
|
300
356
|
} else if (segType === SEGMENT_TYPE_PUBLISHER_TC) {
|
|
301
|
-
publisherTC =
|
|
357
|
+
publisherTC = _parsePublisherTC(segBuf);
|
|
302
358
|
} else {
|
|
303
359
|
errors.push("segment[" + i + "] unknown type: " + segType);
|
|
304
360
|
}
|
|
@@ -451,8 +507,227 @@ function checkVendor(parsed, vendorId) {
|
|
|
451
507
|
};
|
|
452
508
|
}
|
|
453
509
|
|
|
510
|
+
// ---- encode ----------------------------------------------------------------
|
|
511
|
+
|
|
512
|
+
// Accept a Set, an array, or a parsed `{ ids: Set }` / `{ vendorIds: Set }`
|
|
513
|
+
// section as an id collection, and return a sorted unique array of positive
|
|
514
|
+
// integers.
|
|
515
|
+
function _idArray(x) {
|
|
516
|
+
var src = x;
|
|
517
|
+
if (x && typeof x === "object" && !Array.isArray(x) && !(x instanceof Set)) {
|
|
518
|
+
src = x.ids != null ? x.ids : x.vendorIds;
|
|
519
|
+
}
|
|
520
|
+
var list = src instanceof Set ? Array.from(src) : (Array.isArray(src) ? src : []);
|
|
521
|
+
var seen = Object.create(null);
|
|
522
|
+
var out = [];
|
|
523
|
+
list.forEach(function (id) {
|
|
524
|
+
if (typeof id !== "number" || !isFinite(id) || id < 1 || Math.floor(id) !== id) {
|
|
525
|
+
throw IabTcfError.factory("BAD_VALUE", "iabTcf.encode: vendor/purpose ids must be positive integers, got " + id);
|
|
526
|
+
}
|
|
527
|
+
if (!seen[id]) { seen[id] = 1; out.push(id); }
|
|
528
|
+
});
|
|
529
|
+
out.sort(function (a, b) { return a - b; });
|
|
530
|
+
return out;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Collapse a sorted unique id array into [start, end] runs for range encoding.
|
|
534
|
+
function _idRuns(ids) {
|
|
535
|
+
var runs = [];
|
|
536
|
+
for (var i = 0; i < ids.length; i += 1) {
|
|
537
|
+
var start = ids[i];
|
|
538
|
+
var end = start;
|
|
539
|
+
while (i + 1 < ids.length && ids[i + 1] === end + 1) { end = ids[i + 1]; i += 1; }
|
|
540
|
+
runs.push([start, end]);
|
|
541
|
+
}
|
|
542
|
+
return runs;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function _decisec(t) {
|
|
546
|
+
var ms = t instanceof Date ? t.getTime() : (t == null ? Date.now() : Number(t));
|
|
547
|
+
if (!isFinite(ms) || ms < 0) throw IabTcfError.factory("BAD_VALUE", "iabTcf.encode: timestamp must be a Date or non-negative epoch-ms");
|
|
548
|
+
return Math.round(ms / 100); // allow:raw-time-literal — ms → TCF deciseconds
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Bit writer mirroring _bitReader: bits are packed high-bit-first, then the
|
|
552
|
+
// stream is right-padded to a whole number of bytes (byte-oriented base64url,
|
|
553
|
+
// matching the reference CMP encoders and this module's own reader).
|
|
554
|
+
function _bitWriter() {
|
|
555
|
+
var bits = "";
|
|
556
|
+
function writeInt(v, n) {
|
|
557
|
+
if (typeof v !== "number" || !isFinite(v) || v < 0 || Math.floor(v) !== v) throw IabTcfError.factory("BAD_VALUE", "iabTcf.encode: expected a non-negative integer, got " + v);
|
|
558
|
+
if (v >= Math.pow(2, n)) throw IabTcfError.factory("VALUE_OVERFLOW", "iabTcf.encode: " + v + " does not fit in " + n + " bits");
|
|
559
|
+
bits += v.toString(2).padStart(n, "0");
|
|
560
|
+
}
|
|
561
|
+
function writeBool(flag) { bits += flag ? "1" : "0"; }
|
|
562
|
+
function writeBitField(ids, n) {
|
|
563
|
+
var set = Object.create(null);
|
|
564
|
+
_idArray(ids).forEach(function (id) { set[id] = 1; });
|
|
565
|
+
for (var i = 1; i <= n; i += 1) writeBool(set[i] === 1);
|
|
566
|
+
}
|
|
567
|
+
function writeVendorSection(ids) {
|
|
568
|
+
var clean = _idArray(ids);
|
|
569
|
+
var maxVendorId = clean.length ? clean[clean.length - 1] : 0;
|
|
570
|
+
writeInt(maxVendorId, 16); // allow:raw-byte-literal — TCF spec field width
|
|
571
|
+
if (maxVendorId === 0) { writeBool(false); return; }
|
|
572
|
+
var runs = _idRuns(clean);
|
|
573
|
+
var rangeBits = 1 + 12; // allow:raw-byte-literal — TCF spec field width
|
|
574
|
+
runs.forEach(function (run) { rangeBits += 1 + 16 + (run[0] === run[1] ? 0 : 16); }); // allow:raw-byte-literal — TCF spec field width
|
|
575
|
+
var bitfieldBits = 1 + maxVendorId;
|
|
576
|
+
if (rangeBits < bitfieldBits) {
|
|
577
|
+
writeBool(true);
|
|
578
|
+
writeInt(runs.length, 12); // allow:raw-byte-literal — TCF spec field width
|
|
579
|
+
runs.forEach(function (run) {
|
|
580
|
+
if (run[0] === run[1]) { writeBool(false); writeInt(run[0], 16); } // allow:raw-byte-literal — TCF spec field width
|
|
581
|
+
else { writeBool(true); writeInt(run[0], 16); writeInt(run[1], 16); } // allow:raw-byte-literal — TCF spec field width
|
|
582
|
+
});
|
|
583
|
+
} else {
|
|
584
|
+
writeBool(false);
|
|
585
|
+
writeBitField(clean, maxVendorId);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
function toBuffer() {
|
|
589
|
+
var padded = bits + "0".repeat((8 - (bits.length % 8)) % 8); // allow:raw-byte-literal — pad to whole bytes
|
|
590
|
+
var byteLen = padded.length / 8; // allow:raw-byte-literal — bits per byte
|
|
591
|
+
var out = Buffer.alloc(byteLen);
|
|
592
|
+
for (var i = 0; i < byteLen; i += 1) out[i] = parseInt(padded.slice(i * 8, i * 8 + 8), 2); // allow:raw-byte-literal — bits per byte
|
|
593
|
+
return out;
|
|
594
|
+
}
|
|
595
|
+
return { writeInt: writeInt, writeBool: writeBool, writeBitField: writeBitField, writeVendorSection: writeVendorSection, toBuffer: toBuffer };
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function _b64urlEncode(buf) {
|
|
599
|
+
return bCrypto.toBase64Url(buf);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function _writeLetters(w, s, label) {
|
|
603
|
+
var str = String(s).toUpperCase();
|
|
604
|
+
if (str.length !== 2) throw IabTcfError.factory("BAD_VALUE", "iabTcf.encode: " + label + " must be a 2-letter code, got '" + s + "'");
|
|
605
|
+
for (var i = 0; i < 2; i += 1) {
|
|
606
|
+
var v = str.charCodeAt(i) - 0x41; // allow:raw-byte-literal — ASCII 'A' offset
|
|
607
|
+
if (v < 0 || v > 25) throw IabTcfError.factory("BAD_VALUE", "iabTcf.encode: '" + str.charAt(i) + "' is not an A-Z letter");
|
|
608
|
+
w.writeInt(v, 6); // allow:raw-byte-literal — TCF spec field width
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function _encodePublisherTC(pub) {
|
|
613
|
+
var w = _bitWriter();
|
|
614
|
+
w.writeInt(SEGMENT_TYPE_PUBLISHER_TC, 3); // allow:raw-byte-literal — segment-type prefix
|
|
615
|
+
w.writeBitField(pub.pubPurposesConsent || [], 24); // allow:raw-byte-literal — TCF spec field width
|
|
616
|
+
w.writeBitField(pub.pubPurposesLITransparency || [], 24); // allow:raw-byte-literal — TCF spec field width
|
|
617
|
+
var custom = _idArray(pub.customPurposesConsent || []);
|
|
618
|
+
var customLI = _idArray(pub.customPurposesLITransparency || []);
|
|
619
|
+
var n = pub.numCustomPurposes != null
|
|
620
|
+
? pub.numCustomPurposes
|
|
621
|
+
: Math.max(custom.length ? custom[custom.length - 1] : 0, customLI.length ? customLI[customLI.length - 1] : 0);
|
|
622
|
+
w.writeInt(n, 6); // allow:raw-byte-literal — TCF spec field width
|
|
623
|
+
w.writeBitField(custom, n);
|
|
624
|
+
w.writeBitField(customLI, n);
|
|
625
|
+
return _b64urlEncode(w.toBuffer());
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* @primitive b.iabTcf.encode
|
|
630
|
+
* @signature b.iabTcf.encode(obj)
|
|
631
|
+
* @since 0.13.1
|
|
632
|
+
* @status stable
|
|
633
|
+
* @compliance iab-tcf
|
|
634
|
+
* @related b.iabTcf.parseString, b.iabTcf.isValid
|
|
635
|
+
*
|
|
636
|
+
* Serialise a TCF object — in the shape `parseString` returns — back into a
|
|
637
|
+
* TC string. Vendor and purpose collections may be `Set`s, arrays of ids, or
|
|
638
|
+
* the parsed `{ ids }` / `{ vendorIds }` sections. Vendor sections are written
|
|
639
|
+
* with whichever of the bit-field and range forms is smaller, matching the
|
|
640
|
+
* reference CMP encoders, so a parsed string round-trips to an equivalent
|
|
641
|
+
* signal. Pass `disclosedVendors` / `allowedVendors` / `publisherTC` to append
|
|
642
|
+
* those segments. Throws `IabTcfError` on a value that does not fit its field.
|
|
643
|
+
*
|
|
644
|
+
* @example
|
|
645
|
+
* var s = b.iabTcf.encode({
|
|
646
|
+
* core: { version: 2, cmpId: 5, vendorListVersion: 100, consentLanguage: "EN",
|
|
647
|
+
* purposesConsent: [1, 2, 3], vendorConsents: [1, 28, 100], publisherCC: "DE" },
|
|
648
|
+
* disclosedVendors: [1, 28, 100],
|
|
649
|
+
* });
|
|
650
|
+
*/
|
|
651
|
+
function encode(obj) {
|
|
652
|
+
if (!obj || typeof obj !== "object" || !obj.core || typeof obj.core !== "object") {
|
|
653
|
+
throw IabTcfError.factory("BAD_INPUT", "iabTcf.encode: obj must have a 'core' object");
|
|
654
|
+
}
|
|
655
|
+
var c = obj.core;
|
|
656
|
+
var w = _bitWriter();
|
|
657
|
+
w.writeInt(c.version != null ? c.version : TCF_V23_CORE_VERSION, 6); // allow:raw-byte-literal — TCF spec field width
|
|
658
|
+
w.writeInt(_decisec(c.createdAt), 36); // allow:raw-byte-literal — TCF spec field width
|
|
659
|
+
w.writeInt(_decisec(c.lastUpdatedAt != null ? c.lastUpdatedAt : c.createdAt), 36); // allow:raw-byte-literal — TCF spec field width
|
|
660
|
+
w.writeInt(c.cmpId || 0, 12); // allow:raw-byte-literal — TCF spec field width
|
|
661
|
+
w.writeInt(c.cmpVersion || 0, 12); // allow:raw-byte-literal — TCF spec field width
|
|
662
|
+
w.writeInt(c.consentScreen || 0, 6); // allow:raw-byte-literal — TCF spec field width
|
|
663
|
+
_writeLetters(w, c.consentLanguage || "EN", "consentLanguage");
|
|
664
|
+
w.writeInt(c.vendorListVersion || 0, 12); // allow:raw-byte-literal — TCF spec field width
|
|
665
|
+
w.writeInt(c.policyVersion != null ? c.policyVersion : TCF_V23_POLICY_VERSION, 6); // allow:raw-byte-literal — TCF spec field width
|
|
666
|
+
w.writeBool(c.isServiceSpecific !== false);
|
|
667
|
+
w.writeBool(c.useNonStandardStacks === true);
|
|
668
|
+
w.writeBitField(c.specialFeatureOptins || [], 12); // allow:raw-byte-literal — TCF spec field width
|
|
669
|
+
w.writeBitField(c.purposesConsent || [], 24); // allow:raw-byte-literal — TCF spec field width
|
|
670
|
+
w.writeBitField(c.purposesLI || [], 24); // allow:raw-byte-literal — TCF spec field width
|
|
671
|
+
w.writeBool(c.purposeOneTreatment === true);
|
|
672
|
+
_writeLetters(w, c.publisherCC || "AA", "publisherCC");
|
|
673
|
+
w.writeVendorSection(c.vendorConsents || []);
|
|
674
|
+
w.writeVendorSection(c.vendorLIs || []);
|
|
675
|
+
var restrictions = c.publisherRestrictions || [];
|
|
676
|
+
w.writeInt(restrictions.length, 12); // allow:raw-byte-literal — TCF spec field width
|
|
677
|
+
restrictions.forEach(function (pr) {
|
|
678
|
+
w.writeInt(pr.purposeId, 6); // allow:raw-byte-literal — TCF spec field width
|
|
679
|
+
w.writeInt(typeof pr.restrictionType === "number" ? pr.restrictionType : 0, 2);
|
|
680
|
+
var runs = _idRuns(_idArray(pr.vendorIds || []));
|
|
681
|
+
w.writeInt(runs.length, 12); // allow:raw-byte-literal — TCF spec field width
|
|
682
|
+
runs.forEach(function (run) {
|
|
683
|
+
if (run[0] === run[1]) { w.writeBool(false); w.writeInt(run[0], 16); } // allow:raw-byte-literal — TCF spec field width
|
|
684
|
+
else { w.writeBool(true); w.writeInt(run[0], 16); w.writeInt(run[1], 16); } // allow:raw-byte-literal — TCF spec field width
|
|
685
|
+
});
|
|
686
|
+
});
|
|
687
|
+
var segs = [_b64urlEncode(w.toBuffer())];
|
|
688
|
+
|
|
689
|
+
if (obj.disclosedVendors != null) {
|
|
690
|
+
var dw = _bitWriter();
|
|
691
|
+
dw.writeInt(SEGMENT_TYPE_DISCLOSED_VENDORS, 3); // allow:raw-byte-literal — segment-type prefix
|
|
692
|
+
dw.writeVendorSection(obj.disclosedVendors);
|
|
693
|
+
segs.push(_b64urlEncode(dw.toBuffer()));
|
|
694
|
+
}
|
|
695
|
+
if (obj.allowedVendors != null) {
|
|
696
|
+
var aw = _bitWriter();
|
|
697
|
+
aw.writeInt(SEGMENT_TYPE_ALLOWED_VENDORS, 3); // allow:raw-byte-literal — segment-type prefix
|
|
698
|
+
aw.writeVendorSection(obj.allowedVendors);
|
|
699
|
+
segs.push(_b64urlEncode(aw.toBuffer()));
|
|
700
|
+
}
|
|
701
|
+
if (obj.publisherTC != null && obj.publisherTC.present !== false) {
|
|
702
|
+
segs.push(_encodePublisherTC(obj.publisherTC));
|
|
703
|
+
}
|
|
704
|
+
return segs.join(".");
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* @primitive b.iabTcf.isValid
|
|
709
|
+
* @signature b.iabTcf.isValid(tcString)
|
|
710
|
+
* @since 0.13.1
|
|
711
|
+
* @status stable
|
|
712
|
+
* @compliance iab-tcf
|
|
713
|
+
* @related b.iabTcf.parseString
|
|
714
|
+
*
|
|
715
|
+
* Return `true` if the string parses as a well-formed TCF Core segment,
|
|
716
|
+
* `false` otherwise. A total predicate — never throws. Note this checks
|
|
717
|
+
* structural validity only; use `requireV23Disclosed` for the v2.3 policy gate.
|
|
718
|
+
*
|
|
719
|
+
* @example
|
|
720
|
+
* b.iabTcf.isValid("CQSbk4AQSbk4ANwAAAENAwCgAAAAAAAAAAYgACPAAAAA"); // → true
|
|
721
|
+
* b.iabTcf.isValid("nonsense"); // → false
|
|
722
|
+
*/
|
|
723
|
+
function isValid(tcString) {
|
|
724
|
+
try { parseString(tcString); return true; } catch (_e) { return false; }
|
|
725
|
+
}
|
|
726
|
+
|
|
454
727
|
module.exports = {
|
|
455
728
|
parseString: parseString,
|
|
729
|
+
encode: encode,
|
|
730
|
+
isValid: isValid,
|
|
456
731
|
requireV23Disclosed: requireV23Disclosed,
|
|
457
732
|
checkVendor: checkVendor,
|
|
458
733
|
IabTcfError: IabTcfError,
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../scripts/release-notes-schema.json",
|
|
3
|
+
"version": "0.13.2",
|
|
4
|
+
"date": "2026-05-26",
|
|
5
|
+
"headline": "`b.iabTcf.encode` — write TCF consent strings, and a TC-string timestamp fix",
|
|
6
|
+
"summary": "b.iabTcf gains the encode half of its consent-string codec: b.iabTcf.encode(obj) serialises a parsed object back into an IAB TCF v2 TC string, and b.iabTcf.isValid(tcString) is a total never-throwing validity check. Vendor and purpose collections may be Sets, id arrays, or the parsed sections parseString returns; vendor sections are written with whichever of the bit-field and range forms is smaller, matching the reference CMP encoders, so a parsed string round-trips to an equivalent signal. parseString now fully decodes the Core publisher-restrictions list and the PublisherTC segment's publisher and custom purposes, where it previously reported only the segment's presence. The encoder is verified against the worked-example string in the IAB Tech Lab consent-string specification: it re-encodes that string's Core segment byte-for-byte. This release also fixes a TC-string parsing bug — the bit reader accumulated values with a 32-bit shift, so the 36-bit Created and LastUpdated timestamp fields were silently truncated for any real date; they now decode and round-trip exactly.",
|
|
7
|
+
"sections": [
|
|
8
|
+
{
|
|
9
|
+
"heading": "Added",
|
|
10
|
+
"items": [
|
|
11
|
+
{
|
|
12
|
+
"title": "`b.iabTcf.encode` / `b.iabTcf.isValid`",
|
|
13
|
+
"body": "`encode(obj)` serialises a TCF object (the shape `parseString` returns) into a TC string — Core plus optional DisclosedVendors, AllowedVendors, and PublisherTC segments — choosing the smaller of the bit-field and range vendor encodings. `isValid(tcString)` returns whether a string parses as a well-formed Core segment without throwing. `parseString` now fully decodes Core publisher restrictions and the PublisherTC purposes that were previously reported only as present."
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"heading": "Fixed",
|
|
19
|
+
"items": [
|
|
20
|
+
{
|
|
21
|
+
"title": "TC-string 36-bit timestamps were truncated on parse",
|
|
22
|
+
"body": "`b.iabTcf.parseString` read multi-bit fields with a 32-bit left-shift accumulation. The 36-bit Created and LastUpdated fields hold deciseconds-since-epoch, which exceeds 2^31 for any date after 1976, so those timestamps were silently corrupted. The reader now accumulates without the 32-bit truncation; timestamps decode correctly and round-trip through `encode`."
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
}
|
|
@@ -41,7 +41,8 @@ async function run() {
|
|
|
41
41
|
// bytes with a zero pad.
|
|
42
42
|
var totalBits = 6 + 36 + 36 + 12 + 12 + 6 + 6 + 6 + 12 + 6 + 1 + 1 +
|
|
43
43
|
12 + 24 + 24 + 1 + 6 + 6 +
|
|
44
|
-
16 + 1 + 16 + 1
|
|
44
|
+
16 + 1 + 16 + 1 + // two empty vendor sections
|
|
45
|
+
12; // NumPubRestrictions (mandatory, =0)
|
|
45
46
|
var byteLen = Math.ceil(totalBits / 8); // allow:raw-byte-literal — bits-per-byte
|
|
46
47
|
var buf = Buffer.alloc(byteLen);
|
|
47
48
|
// Set version field (top 6 bits of byte 0).
|
|
@@ -68,6 +69,65 @@ async function run() {
|
|
|
68
69
|
check("core version=4", parsed.core.version === 4);
|
|
69
70
|
check("checkVendor handles bare core",
|
|
70
71
|
b.iabTcf.checkVendor(parsed, 755).consented === false);
|
|
72
|
+
|
|
73
|
+
// ---- encode + isValid + completed parse --------------------------------
|
|
74
|
+
check("iabTcf.encode is fn", typeof b.iabTcf.encode === "function");
|
|
75
|
+
check("iabTcf.isValid is fn", typeof b.iabTcf.isValid === "function");
|
|
76
|
+
|
|
77
|
+
// Independent oracle: the worked-example string from the IAB Tech Lab
|
|
78
|
+
// "Consent string and vendor list formats v2" specification.
|
|
79
|
+
var SPEC = "CQSbk4AQSbk4ANwAAAENAwCgAAAAAAAAAAYgACPAAAAA.IDKQA4AAgAKAGQAygAAA.YAAAAAAAAAAA";
|
|
80
|
+
var SPEC_CORE = "CQSbk4AQSbk4ANwAAAENAwCgAAAAAAAAAAYgACPAAAAA";
|
|
81
|
+
var sp = b.iabTcf.parseString(SPEC);
|
|
82
|
+
check("spec core version 2", sp.core.version === 2);
|
|
83
|
+
check("spec cmpId 880", sp.core.cmpId === 880);
|
|
84
|
+
check("spec vendorListVersion 48", sp.core.vendorListVersion === 48);
|
|
85
|
+
check("spec created 2025-06-03", new Date(sp.core.createdAt).toISOString() === "2025-06-03T00:00:00.000Z");
|
|
86
|
+
check("spec vendorConsents [1,2,3,4]", JSON.stringify(Array.from(sp.core.vendorConsents.ids)) === "[1,2,3,4]");
|
|
87
|
+
check("spec disclosedVendors decoded", JSON.stringify(Array.from(sp.disclosedVendors.vendorIds)) === "[1,2,3,4,5,100,404]");
|
|
88
|
+
check("spec publisherTC fully parsed", sp.publisherTC && sp.publisherTC.present === true && typeof sp.publisherTC.numCustomPurposes === "number");
|
|
89
|
+
check("core.publisherRestrictions array", Array.isArray(sp.core.publisherRestrictions));
|
|
90
|
+
// Strong oracle: re-encoding the parsed core reproduces the spec Core
|
|
91
|
+
// segment byte-for-byte.
|
|
92
|
+
check("re-encoded core is byte-identical to the spec", b.iabTcf.encode({ core: sp.core }) === SPEC_CORE);
|
|
93
|
+
// Disclosed content round-trips (the encoder writes the minimal valid form).
|
|
94
|
+
var rt = b.iabTcf.parseString(b.iabTcf.encode({ core: sp.core, disclosedVendors: sp.disclosedVendors }));
|
|
95
|
+
check("disclosed content round-trips", JSON.stringify(Array.from(rt.disclosedVendors.vendorIds)) === "[1,2,3,4,5,100,404]");
|
|
96
|
+
|
|
97
|
+
// 36-bit timestamp regression: a real date (deciseconds > 2^31) must survive
|
|
98
|
+
// parse → encode unchanged. Guards the reader's `* 2` (not `<< 1`)
|
|
99
|
+
// accumulation, without which Created / LastUpdated truncate at 32 bits.
|
|
100
|
+
var bigCreated = Date.UTC(2026, 4, 26);
|
|
101
|
+
var encTs = b.iabTcf.encode({ core: { version: 2, createdAt: bigCreated, lastUpdatedAt: bigCreated, cmpId: 300, vendorListVersion: 100, consentLanguage: "EN", publisherCC: "DE", vendorConsents: [], vendorLIs: [] } });
|
|
102
|
+
var decTs = b.iabTcf.parseString(encTs);
|
|
103
|
+
check("36-bit timestamp survives round-trip", decTs.core.createdAt === bigCreated);
|
|
104
|
+
check("cmpId survives round-trip", decTs.core.cmpId === 300);
|
|
105
|
+
check("consentLanguage survives round-trip", decTs.core.consentLanguage === "EN");
|
|
106
|
+
|
|
107
|
+
// Vendor-section round-trip across range-favouring and sparse sets.
|
|
108
|
+
[[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], [12, 37, 199, 755], [1]].forEach(function (vc, idx) {
|
|
109
|
+
var s = b.iabTcf.encode({ core: { version: 2, createdAt: bigCreated, lastUpdatedAt: bigCreated, cmpId: 5, vendorListVersion: 10, consentLanguage: "EN", publisherCC: "FR", purposesConsent: [1, 2, 3], vendorConsents: vc, vendorLIs: [] } });
|
|
110
|
+
var d = b.iabTcf.parseString(s);
|
|
111
|
+
check("vendorConsents round-trip " + idx, JSON.stringify(Array.from(d.core.vendorConsents.ids)) === JSON.stringify(vc));
|
|
112
|
+
check("purposesConsent round-trip " + idx, JSON.stringify(Array.from(d.core.purposesConsent)) === "[1,2,3]");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Publisher restrictions round-trip.
|
|
116
|
+
var prDec = b.iabTcf.parseString(b.iabTcf.encode({ core: { version: 2, createdAt: bigCreated, lastUpdatedAt: bigCreated, cmpId: 9, vendorListVersion: 1, consentLanguage: "IT", publisherCC: "IT", vendorConsents: [], vendorLIs: [], publisherRestrictions: [{ purposeId: 2, restrictionType: 1, vendorIds: [5, 6, 7, 50] }] } }));
|
|
117
|
+
check("publisher restriction round-trips",
|
|
118
|
+
prDec.core.publisherRestrictions.length === 1 &&
|
|
119
|
+
prDec.core.publisherRestrictions[0].purposeId === 2 &&
|
|
120
|
+
JSON.stringify(prDec.core.publisherRestrictions[0].vendorIds) === "[5,6,7,50]");
|
|
121
|
+
|
|
122
|
+
check("isValid true for the spec vector", b.iabTcf.isValid(SPEC) === true);
|
|
123
|
+
check("isValid false for garbage", b.iabTcf.isValid("nonsense!!") === false);
|
|
124
|
+
// A truncated core (a dropped trailing character cuts into the mandatory
|
|
125
|
+
// NumPubRestrictions field) must NOT validate — the reader's bounds check
|
|
126
|
+
// rejects it rather than treating the gap as "no restrictions".
|
|
127
|
+
check("isValid false for a truncated core", b.iabTcf.isValid(SPEC_CORE.slice(0, -1)) === false);
|
|
128
|
+
rejects("encode without core throws", function () { b.iabTcf.encode({}); }, "BAD_INPUT");
|
|
129
|
+
rejects("encode rejects a non-positive id",
|
|
130
|
+
function () { b.iabTcf.encode({ core: { consentLanguage: "EN", publisherCC: "DE", vendorConsents: [0], vendorLIs: [] } }); }, "BAD_VALUE");
|
|
71
131
|
}
|
|
72
132
|
|
|
73
133
|
module.exports = { run: run };
|
package/package.json
CHANGED