@blamejs/blamejs-shop 0.1.33 → 0.1.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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
+ };
@@ -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 << 1) | ((buf[byteIdx] >> bitIdx) & 1);
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 = { present: true };
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,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/core",
3
- "version": "0.13.1",
3
+ "version": "0.13.4",
4
4
  "description": "The Node framework that owns its stack.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
@@ -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
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "$schema": "../scripts/release-notes-schema.json",
3
+ "version": "0.13.3",
4
+ "date": "2026-05-26",
5
+ "headline": "`b.crypto.xwing` — X-Wing hybrid post-quantum KEM",
6
+ "summary": "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.",
7
+ "sections": [
8
+ {
9
+ "heading": "Added",
10
+ "items": [
11
+ {
12
+ "title": "`b.crypto.xwing` — X-Wing hybrid PQ/T KEM (experimental)",
13
+ "body": "`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."
14
+ }
15
+ ]
16
+ }
17
+ ]
18
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "$schema": "../scripts/release-notes-schema.json",
3
+ "version": "0.13.4",
4
+ "date": "2026-05-26",
5
+ "headline": "`b.crdt` — conflict-free replicated data types",
6
+ "summary": "b.crdt adds state-based Conflict-free Replicated Data Types: data structures that independent replicas update without coordination and still converge to the same value once they have exchanged state. Each type's merge is a join over a semilattice — commutative, associative, and idempotent — so replicas can merge in any order, any number of times, and agree, which makes these the substrate for active/active cluster state, offline-first clients that reconcile on reconnect, and eventually-consistent counters, sets, and maps. The release ships the full state-based family: grow-only and positive-negative counters (gCounter / pnCounter), grow-only, two-phase, and observed-remove sets (gSet / twoPSet / orSet), a last-write-wins register (lwwRegister), and an observed-remove map (orMap). Every type exposes the same contract — local mutators, merge(other) that returns a converged instance without mutating either operand, value() for the materialized value, and state() / fromState() for a JSON-serializable form to snapshot via b.archive or b.backup or ship to a peer — and carries a replicaId so per-replica contributions stay distinct.",
7
+ "sections": [
8
+ {
9
+ "heading": "Added",
10
+ "items": [
11
+ {
12
+ "title": "`b.crdt` — state-based CvRDT counters, sets, register, and map",
13
+ "body": "`b.crdt.gCounter` / `pnCounter` (grow-only and increment/decrement counters), `b.crdt.gSet` / `twoPSet` / `orSet` (grow-only, two-phase, and observed-remove sets — `orSet` supports re-add and resolves a concurrent add-vs-remove as add-wins), `b.crdt.lwwRegister` (last-write-wins with a deterministic replicaId tie-break), and `b.crdt.orMap` (observed-remove keys with last-write-wins values). Each exposes `merge` / `value` / `state` / `fromState` and converges by the CvRDT laws. `orSet` and `orMap` accept `tombstoneRetention` to bound tombstone memory against a remove flood."
14
+ }
15
+ ]
16
+ }
17
+ ],
18
+ "outOfScope": [
19
+ "Operation-based sequence CRDTs (RGA) — a different formal class (CmRDT) requiring a causal-delivery channel; ships when collaborative-text / ordered-list editing is needed.",
20
+ "Delta-state mutators — a bandwidth optimization over full-state merge; ships under large-state replication pressure.",
21
+ "The event-bus replicator for live multi-node auto-sync — the state-based types merge manually standalone; ships when live multi-node synchronization is wired, together with the causal-delivery option and its detector."
22
+ ]
23
+ }