@blamejs/core 0.13.1 → 0.13.3
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 +4 -0
- package/README.md +2 -1
- package/index.js +1 -0
- package/lib/crypto-xwing.js +213 -0
- package/lib/iab-tcf.js +277 -2
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,10 @@ upgrading across more than a few patches at a time.
|
|
|
8
8
|
|
|
9
9
|
## v0.13.x
|
|
10
10
|
|
|
11
|
+
- v0.13.3 (2026-05-26) — **`b.crypto.xwing` — X-Wing hybrid post-quantum KEM.** b.crypto.xwing adds the X-Wing hybrid key-encapsulation mechanism (draft-connolly-cfrg-xwing-kem): it runs ML-KEM-768 and X25519 side by side and binds their shared secrets with SHA3-256, so an encapsulated key stays secure as long as either ML-KEM-768 or X25519 holds. That is the conservative shape for moving off classical ECDH today — a harvest-now-decrypt-later attacker must break the lattice KEM, and a hypothetical ML-KEM break still leaves X25519 standing. keygen() produces a 32-byte decapsulation seed and a 1216-byte public key; encapsulate(publicKey) returns a 1120-byte ciphertext and a 32-byte shared secret; decapsulate(secretKey, ciphertext) recovers it. The X-Wing combiner is frozen, but its specification is still an IETF Internet-Draft, so this primitive is marked experimental and sits beside the existing pre-RFC post-quantum HPKE drafts; it composes the framework's vendored ML-KEM-768 and X25519 with SHA3 and adds no new cryptographic core. The combiner is known-answer-tested byte-for-byte against the draft's definition. **Added:** *`b.crypto.xwing` — X-Wing hybrid PQ/T KEM (experimental)* — `keygen(seed?)` → `{ publicKey (1216 B), secretKey (32-byte seed) }`; `encapsulate(publicKey, eseed?)` → `{ ciphertext (1120 B), sharedSecret (32 B) }`; `decapsulate(secretKey, ciphertext)` → the 32-byte shared secret. Both `keygen` and `encapsulate` accept an optional seed for deterministic operation. The combiner — `SHA3-256(ssMLKEM ‖ ssX25519 ‖ ctX25519 ‖ pkX25519 ‖ label)` — is exposed as `combiner` for advanced use. Marked `experimental` while draft-connolly-cfrg-xwing-kem remains an Internet-Draft; the algorithm itself is frozen.
|
|
12
|
+
|
|
13
|
+
- 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`.
|
|
14
|
+
|
|
11
15
|
- 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
16
|
|
|
13
17
|
- 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.
|
package/README.md
CHANGED
|
@@ -101,6 +101,7 @@ The framework bundles the surface a typical Node app reaches for. Every primitiv
|
|
|
101
101
|
- **AAD-bound sealed columns** — AEAD tag tied to `(table, rowId, column, schemaVersion)`; copy-paste between rows or schema-version replay surfaces as refused decrypt (`b.vault.aad`)
|
|
102
102
|
- **Signed webhooks + API encryption** — SLH-DSA-SHAKE-256f default; ML-DSA-65 opt-in; ECIES API encryption (`b.webhook`, `b.crypto`)
|
|
103
103
|
- **HPKE / HTTP signatures** — RFC 9180 HPKE with ML-KEM-1024 + HKDF-SHA3-512 + ChaCha20-Poly1305 (`b.crypto.hpke`); RFC 9421 HTTP Message Signatures with derived components and ed25519 / ML-DSA-65 (`b.crypto.httpSig`); RFC 9530 Content-Digest / Repr-Digest body-integrity fields (SHA-256 / SHA-512, legacy algorithms refused — `b.contentDigest`) to sign the digest rather than the whole body
|
|
104
|
+
- **X-Wing hybrid KEM** — `b.crypto.xwing` (draft-connolly-cfrg-xwing-kem, experimental): ML-KEM-768 + X25519 bound by SHA3-256, secure if either component holds — the conservative key-encapsulation shape for migrating off classical ECDH. `keygen` / `encapsulate` / `decapsulate` with a 1216-byte public key, 1120-byte ciphertext, and 32-byte shared secret
|
|
104
105
|
- **Link header** — RFC 8288 Web Linking codec (`b.linkHeader.parse` / `serialize`): parse and build `Link: <uri>; rel="next"` relations, the standard REST pagination mechanism; quote-aware (a comma inside a quoted parameter never splits the list)
|
|
105
106
|
- **URI Templates** — RFC 6570 expansion (`b.uriTemplate.expand` / `compile`): full Level 4 — every operator, the `:N` prefix and `*` explode modifiers — turning `{/path}{?q*}` plus variables into a concrete URI; validated against the official uritemplate-test suite. The `{var}` syntax behind OpenAPI links and HAL `_links`
|
|
106
107
|
- **JSON Type Definition** — RFC 8927 validation (`b.jtd.validate` / `isValid`): portable, cross-implementation schema validation (all eight forms — type / enum / elements / properties / values / discriminator / ref / empty), returning instancePath / schemaPath errors; validated against the official 316-case suite. Interop companion to the fluent `b.safeSchema` builder
|
|
@@ -207,7 +208,7 @@ The framework bundles the surface a typical Node app reaches for. Every primitiv
|
|
|
207
208
|
- **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
209
|
- **Consumer-protection** — FTC click-to-cancel UX-parity attestation (`ftc-2024` / `ca-sb942` / `strict`) (`b.darkPatterns`)
|
|
209
210
|
- **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
|
|
211
|
+
- **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
212
|
- **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
213
|
- **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
214
|
### Observability
|
package/index.js
CHANGED
|
@@ -58,6 +58,7 @@ var crypto = require("./lib/crypto");
|
|
|
58
58
|
// the dedicated lib files; these are thin aliases.
|
|
59
59
|
crypto.hpke = require("./lib/crypto-hpke");
|
|
60
60
|
crypto.oprf = require("./lib/crypto-oprf");
|
|
61
|
+
crypto.xwing = require("./lib/crypto-xwing");
|
|
61
62
|
// Both PQ-HPKE drafts behind one opt-in sub-namespace — see
|
|
62
63
|
// lib/crypto-hpke-pq.js. Operators that need a draft-codepoint
|
|
63
64
|
// shape reach for b.crypto.hpke.pq.connolly / .wg explicitly; the
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @module b.crypto.xwing
|
|
4
|
+
* @nav Crypto
|
|
5
|
+
* @title X-Wing KEM
|
|
6
|
+
*
|
|
7
|
+
* @intro
|
|
8
|
+
* X-Wing is a general-purpose hybrid post-quantum / traditional key
|
|
9
|
+
* encapsulation mechanism: it runs ML-KEM-768 and X25519 side by side and
|
|
10
|
+
* binds their shared secrets with SHA3-256, so the resulting key stays
|
|
11
|
+
* secure as long as <em>either</em> ML-KEM-768 or X25519 holds. That is the
|
|
12
|
+
* conservative shape for migrating off classical ECDH today — a harvest-now-
|
|
13
|
+
* decrypt-later attacker must break the lattice KEM, and a hypothetical
|
|
14
|
+
* ML-KEM break still leaves X25519 standing.
|
|
15
|
+
*
|
|
16
|
+
* The construction follows
|
|
17
|
+
* <code>draft-connolly-cfrg-xwing-kem</code>. The combiner is frozen — it
|
|
18
|
+
* hashes the ML-KEM shared secret, the X25519 shared secret, the X25519
|
|
19
|
+
* ephemeral public key, the recipient's X25519 public key, and a fixed
|
|
20
|
+
* six-byte label — but the document is still an IETF Internet-Draft, so this
|
|
21
|
+
* primitive is marked <code>experimental</code> and sits beside the other
|
|
22
|
+
* pre-RFC post-quantum drafts (<code>b.crypto.hpke.pq</code>). The wire
|
|
23
|
+
* sizes are fixed: a 1216-byte public key (ML-KEM-768 1184 ‖ X25519 32), a
|
|
24
|
+
* 1120-byte ciphertext (ML-KEM-768 1088 ‖ X25519 32), a 32-byte decapsulation
|
|
25
|
+
* seed, and a 32-byte shared secret.
|
|
26
|
+
*
|
|
27
|
+
* X-Wing composes the framework's vendored ML-KEM-768 and X25519 plus
|
|
28
|
+
* SHA3 — it adds no new cryptographic core, only the standard combiner and
|
|
29
|
+
* wire framing.
|
|
30
|
+
*
|
|
31
|
+
* @card
|
|
32
|
+
* X-Wing hybrid PQ/T KEM (`b.crypto.xwing`) — ML-KEM-768 + X25519 bound by
|
|
33
|
+
* SHA3-256 per draft-connolly-cfrg-xwing-kem, secure if either component
|
|
34
|
+
* holds. 1216-byte key, 1120-byte ciphertext, 32-byte shared secret.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
var nodeCrypto = require("node:crypto");
|
|
38
|
+
var pqc = require("./vendor/noble-post-quantum.cjs");
|
|
39
|
+
var { defineClass } = require("./framework-error");
|
|
40
|
+
|
|
41
|
+
var XWingError = defineClass("XWingError", { alwaysPermanent: true });
|
|
42
|
+
|
|
43
|
+
var mlkem = pqc.ml_kem768;
|
|
44
|
+
|
|
45
|
+
// draft-connolly-cfrg-xwing-kem: the combiner label, ASCII "\./" + "/^\".
|
|
46
|
+
var XWING_LABEL = Buffer.from("5c2e2f2f5e5c", "hex");
|
|
47
|
+
|
|
48
|
+
// Component + composite sizes (bytes), fixed by the draft — protocol wire
|
|
49
|
+
// widths, not buffer-capacity tunables.
|
|
50
|
+
var ML_KEM_PK = 1184; // allow:raw-byte-literal — ML-KEM-768 public key
|
|
51
|
+
var ML_KEM_CT = 1088; // allow:raw-byte-literal — ML-KEM-768 ciphertext
|
|
52
|
+
var X25519_LEN = 32; // allow:raw-byte-literal — X25519 key/share length
|
|
53
|
+
var SEED_LEN = 32; // allow:raw-byte-literal — X-Wing seed length
|
|
54
|
+
var SS_LEN = 32; // allow:raw-byte-literal — shared-secret length
|
|
55
|
+
var PK_LEN = ML_KEM_PK + X25519_LEN; // 1216
|
|
56
|
+
var CT_LEN = ML_KEM_CT + X25519_LEN; // 1120
|
|
57
|
+
var MLKEM_SEED = 64; // allow:raw-byte-literal — d ‖ z for ML-KEM KeyGen_internal
|
|
58
|
+
var EXPAND_LEN = 96; // allow:raw-byte-literal — SHAKE256(seed) → d ‖ z ‖ sk_X
|
|
59
|
+
|
|
60
|
+
// X25519 raw-scalar helpers via fixed PKCS8 / SPKI DER prefixes (OID
|
|
61
|
+
// 1.3.101.110). Node clamps the scalar per RFC 7748 on use, matching X-Wing.
|
|
62
|
+
var X25519_PKCS8_PREFIX = Buffer.from("302e020100300506032b656e04220420", "hex");
|
|
63
|
+
var X25519_SPKI_PREFIX = Buffer.from("302a300506032b656e032100", "hex");
|
|
64
|
+
|
|
65
|
+
function _x25519Public(sk) {
|
|
66
|
+
var key = nodeCrypto.createPrivateKey({ key: Buffer.concat([X25519_PKCS8_PREFIX, sk]), format: "der", type: "pkcs8" });
|
|
67
|
+
var spki = nodeCrypto.createPublicKey(key).export({ format: "der", type: "spki" });
|
|
68
|
+
return spki.subarray(spki.length - X25519_LEN);
|
|
69
|
+
}
|
|
70
|
+
function _x25519Shared(sk, pk) {
|
|
71
|
+
return nodeCrypto.diffieHellman({
|
|
72
|
+
privateKey: nodeCrypto.createPrivateKey({ key: Buffer.concat([X25519_PKCS8_PREFIX, sk]), format: "der", type: "pkcs8" }),
|
|
73
|
+
publicKey: nodeCrypto.createPublicKey({ key: Buffer.concat([X25519_SPKI_PREFIX, pk]), format: "der", type: "spki" }),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function _shake256(buf, outLen) { return nodeCrypto.createHash("shake256", { outputLength: outLen }).update(buf).digest(); }
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @primitive b.crypto.xwing.combiner
|
|
81
|
+
* @signature b.crypto.xwing.combiner(ssM, ssX, ctX, pkX)
|
|
82
|
+
* @since 0.13.3
|
|
83
|
+
* @status experimental
|
|
84
|
+
* @compliance soc2
|
|
85
|
+
* @related b.crypto.xwing.encapsulate, b.crypto.xwing.decapsulate
|
|
86
|
+
*
|
|
87
|
+
* The X-Wing combiner: <code>SHA3-256(ssM ‖ ssX ‖ ctX ‖ pkX ‖ label)</code>,
|
|
88
|
+
* where the label is the fixed six bytes the draft defines. Exposed for
|
|
89
|
+
* advanced use and known-answer testing; <code>encapsulate</code> and
|
|
90
|
+
* <code>decapsulate</code> call it internally. Each input must be 32 bytes.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* var ss = b.crypto.xwing.combiner(ssMlkem, ssX25519, ephPub, recipientPub);
|
|
94
|
+
* // → 32-byte shared secret
|
|
95
|
+
*/
|
|
96
|
+
function combiner(ssM, ssX, ctX, pkX) {
|
|
97
|
+
[["ssM", ssM, SS_LEN], ["ssX", ssX, X25519_LEN], ["ctX", ctX, X25519_LEN], ["pkX", pkX, X25519_LEN]].forEach(function (t) {
|
|
98
|
+
// ML-KEM outputs are Uint8Array; X25519 outputs are Buffer — accept both.
|
|
99
|
+
if (!(Buffer.isBuffer(t[1]) || t[1] instanceof Uint8Array) || t[1].length !== t[2]) throw new XWingError("xwing/bad-input", "xwing.combiner: " + t[0] + " must be a " + t[2] + "-byte byte array");
|
|
100
|
+
});
|
|
101
|
+
return nodeCrypto.createHash("sha3-256").update(Buffer.concat([ssM, ssX, ctX, pkX, XWING_LABEL])).digest();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Expand a 32-byte seed into ML-KEM key material + the X25519 scalar.
|
|
105
|
+
function _expand(seed) {
|
|
106
|
+
var e = _shake256(seed, EXPAND_LEN);
|
|
107
|
+
var kp = mlkem.keygen(e.subarray(0, MLKEM_SEED)); // KeyGen_internal(d, z)
|
|
108
|
+
var skX = e.subarray(MLKEM_SEED, EXPAND_LEN);
|
|
109
|
+
return { skM: kp.secretKey, pkM: kp.publicKey, skX: skX, pkX: _x25519Public(skX) };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* @primitive b.crypto.xwing.keygen
|
|
114
|
+
* @signature b.crypto.xwing.keygen(seed?)
|
|
115
|
+
* @since 0.13.3
|
|
116
|
+
* @status experimental
|
|
117
|
+
* @compliance soc2
|
|
118
|
+
* @related b.crypto.xwing.encapsulate, b.crypto.xwing.decapsulate
|
|
119
|
+
*
|
|
120
|
+
* Generate an X-Wing keypair. The decapsulation key is a 32-byte seed (store
|
|
121
|
+
* this); the encapsulation key is the 1216-byte public key to publish. Pass a
|
|
122
|
+
* 32-byte <code>seed</code> for deterministic generation, or omit it for a
|
|
123
|
+
* random key.
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* var kp = b.crypto.xwing.keygen();
|
|
127
|
+
* kp.publicKey.length; // → 1216
|
|
128
|
+
* kp.secretKey.length; // → 32 (the seed — keep it secret)
|
|
129
|
+
*/
|
|
130
|
+
function keygen(seed) {
|
|
131
|
+
if (seed == null) seed = nodeCrypto.randomBytes(SEED_LEN);
|
|
132
|
+
if (!Buffer.isBuffer(seed) || seed.length !== SEED_LEN) throw new XWingError("xwing/bad-seed", "xwing.keygen: seed must be a " + SEED_LEN + "-byte Buffer");
|
|
133
|
+
var k = _expand(seed);
|
|
134
|
+
return { publicKey: Buffer.concat([k.pkM, k.pkX]), secretKey: Buffer.from(seed) };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* @primitive b.crypto.xwing.encapsulate
|
|
139
|
+
* @signature b.crypto.xwing.encapsulate(publicKey, eseed?)
|
|
140
|
+
* @since 0.13.3
|
|
141
|
+
* @status experimental
|
|
142
|
+
* @compliance soc2
|
|
143
|
+
* @related b.crypto.xwing.decapsulate, b.crypto.xwing.keygen
|
|
144
|
+
*
|
|
145
|
+
* Encapsulate to a 1216-byte X-Wing public key. Returns the 1120-byte
|
|
146
|
+
* <code>ciphertext</code> to send and the 32-byte <code>sharedSecret</code> to
|
|
147
|
+
* key a symmetric cipher with. Pass a 64-byte <code>eseed</code>
|
|
148
|
+
* (X25519 ephemeral scalar ‖ ML-KEM coins) for deterministic encapsulation, or
|
|
149
|
+
* omit it for fresh randomness.
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* var enc = b.crypto.xwing.encapsulate(recipientPublicKey);
|
|
153
|
+
* enc.ciphertext.length; // → 1120
|
|
154
|
+
* enc.sharedSecret.length; // → 32
|
|
155
|
+
*/
|
|
156
|
+
function encapsulate(publicKey, eseed) {
|
|
157
|
+
if (!Buffer.isBuffer(publicKey) || publicKey.length !== PK_LEN) throw new XWingError("xwing/bad-public-key", "xwing.encapsulate: publicKey must be a " + PK_LEN + "-byte Buffer");
|
|
158
|
+
var pkM = publicKey.subarray(0, ML_KEM_PK);
|
|
159
|
+
var pkX = publicKey.subarray(ML_KEM_PK, PK_LEN);
|
|
160
|
+
var ekX, mlkemCoins = null;
|
|
161
|
+
if (eseed == null) {
|
|
162
|
+
ekX = nodeCrypto.randomBytes(X25519_LEN);
|
|
163
|
+
} else {
|
|
164
|
+
if (!Buffer.isBuffer(eseed) || eseed.length !== 2 * X25519_LEN) throw new XWingError("xwing/bad-eseed", "xwing.encapsulate: eseed must be a " + (2 * X25519_LEN) + "-byte Buffer");
|
|
165
|
+
// draft EncapsulateDerand: eseed[0:32] = ML-KEM coins, eseed[32:64] = X25519
|
|
166
|
+
// ephemeral scalar. This order matches the draft's test vectors.
|
|
167
|
+
mlkemCoins = eseed.subarray(0, X25519_LEN);
|
|
168
|
+
ekX = eseed.subarray(X25519_LEN, 2 * X25519_LEN);
|
|
169
|
+
}
|
|
170
|
+
var ctX = _x25519Public(ekX);
|
|
171
|
+
var ssX = _x25519Shared(ekX, pkX);
|
|
172
|
+
var kem = mlkemCoins ? mlkem.encapsulate(pkM, mlkemCoins) : mlkem.encapsulate(pkM);
|
|
173
|
+
var ss = combiner(kem.sharedSecret, ssX, ctX, pkX);
|
|
174
|
+
return { ciphertext: Buffer.concat([kem.cipherText, ctX]), sharedSecret: ss };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* @primitive b.crypto.xwing.decapsulate
|
|
179
|
+
* @signature b.crypto.xwing.decapsulate(secretKey, ciphertext)
|
|
180
|
+
* @since 0.13.3
|
|
181
|
+
* @status experimental
|
|
182
|
+
* @compliance soc2
|
|
183
|
+
* @related b.crypto.xwing.encapsulate, b.crypto.xwing.keygen
|
|
184
|
+
*
|
|
185
|
+
* Recover the 32-byte shared secret from a 1120-byte X-Wing ciphertext using
|
|
186
|
+
* the 32-byte decapsulation seed. ML-KEM-768's implicit-rejection means a
|
|
187
|
+
* tampered ciphertext yields a different (still 32-byte) secret rather than an
|
|
188
|
+
* error, so never branch on success — derive keys and let the AEAD tag fail.
|
|
189
|
+
*
|
|
190
|
+
* @example
|
|
191
|
+
* var ss = b.crypto.xwing.decapsulate(kp.secretKey, enc.ciphertext);
|
|
192
|
+
* ss.equals(enc.sharedSecret); // → true
|
|
193
|
+
*/
|
|
194
|
+
function decapsulate(secretKey, ciphertext) {
|
|
195
|
+
if (!Buffer.isBuffer(secretKey) || secretKey.length !== SEED_LEN) throw new XWingError("xwing/bad-seed", "xwing.decapsulate: secretKey must be a " + SEED_LEN + "-byte Buffer");
|
|
196
|
+
if (!Buffer.isBuffer(ciphertext) || ciphertext.length !== CT_LEN) throw new XWingError("xwing/bad-ciphertext", "xwing.decapsulate: ciphertext must be a " + CT_LEN + "-byte Buffer");
|
|
197
|
+
var k = _expand(secretKey);
|
|
198
|
+
var ctM = ciphertext.subarray(0, ML_KEM_CT);
|
|
199
|
+
var ctX = ciphertext.subarray(ML_KEM_CT, CT_LEN);
|
|
200
|
+
var ssM = mlkem.decapsulate(ctM, k.skM);
|
|
201
|
+
var ssX = _x25519Shared(k.skX, ctX);
|
|
202
|
+
return combiner(ssM, ssX, ctX, k.pkX);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
module.exports = {
|
|
206
|
+
NAME: "X-Wing",
|
|
207
|
+
keygen: keygen,
|
|
208
|
+
encapsulate: encapsulate,
|
|
209
|
+
decapsulate: decapsulate,
|
|
210
|
+
combiner: combiner,
|
|
211
|
+
SIZES: { publicKey: PK_LEN, ciphertext: CT_LEN, secretKey: SEED_LEN, sharedSecret: SS_LEN },
|
|
212
|
+
XWingError: XWingError,
|
|
213
|
+
};
|
package/lib/iab-tcf.js
CHANGED
|
@@ -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,
|
package/package.json
CHANGED
package/sbom.cdx.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:4dae8994-eae5-41f2-9afe-f4d18c950644",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-
|
|
8
|
+
"timestamp": "2026-05-27T00:14:33.058Z",
|
|
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.13.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.13.3",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.13.
|
|
25
|
+
"version": "0.13.3",
|
|
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.13.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.13.3",
|
|
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.13.
|
|
57
|
+
"ref": "@blamejs/core@0.13.3",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|