@svta/cml-cmcd 2.1.2 → 2.2.0

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/dist/index.js CHANGED
@@ -62,11 +62,11 @@ const CMCD_V2 = 2;
62
62
  //#endregion
63
63
  //#region src/CmcdEventType.ts
64
64
  /**
65
- * CMCD event type for the 'br' key (bitrate change).
65
+ * CMCD event type for the 'bc' key (bitrate change).
66
66
  *
67
67
  * @public
68
68
  */
69
- const CMCD_EVENT_BITRATE_CHANGE = "br";
69
+ const CMCD_EVENT_BITRATE_CHANGE = "bc";
70
70
  /**
71
71
  * CMCD event type for the 'ps' key (play state change).
72
72
  *
@@ -297,6 +297,7 @@ function isCmcdCustomKey(key) {
297
297
 
298
298
  //#endregion
299
299
  //#region src/isCmcdRequestKey.ts
300
+ const CMCD_REQUEST_KEY_SET = new Set(CMCD_REQUEST_KEYS);
300
301
  /**
301
302
  * Check if a key is a valid CMCD request key.
302
303
  *
@@ -310,7 +311,7 @@ function isCmcdCustomKey(key) {
310
311
  * {@includeCode ../test/isCmcdRequestKey.test.ts#example}
311
312
  */
312
313
  function isCmcdRequestKey(key) {
313
- return CMCD_REQUEST_KEYS.includes(key) || isCmcdCustomKey(key);
314
+ return CMCD_REQUEST_KEY_SET.has(key) || isCmcdCustomKey(key);
314
315
  }
315
316
 
316
317
  //#endregion
@@ -333,6 +334,7 @@ const CMCD_RESPONSE_KEYS = [
333
334
 
334
335
  //#endregion
335
336
  //#region src/isCmcdResponseReceivedKey.ts
337
+ const CMCD_RESPONSE_KEY_SET = new Set(CMCD_RESPONSE_KEYS);
336
338
  /**
337
339
  * Check if a key is a valid CMCD response key.
338
340
  *
@@ -346,11 +348,12 @@ const CMCD_RESPONSE_KEYS = [
346
348
  * {@includeCode ../test/isCmcdResponseReceivedKey.test.ts#example}
347
349
  */
348
350
  function isCmcdResponseReceivedKey(key) {
349
- return CMCD_RESPONSE_KEYS.includes(key);
351
+ return CMCD_RESPONSE_KEY_SET.has(key);
350
352
  }
351
353
 
352
354
  //#endregion
353
355
  //#region src/isCmcdEventKey.ts
356
+ const CMCD_EVENT_KEY_SET = new Set(CMCD_EVENT_KEYS);
354
357
  /**
355
358
  * Check if a key is a valid CMCD event key.
356
359
  *
@@ -364,9 +367,35 @@ function isCmcdResponseReceivedKey(key) {
364
367
  * {@includeCode ../test/isCmcdEventKey.test.ts#example}
365
368
  */
366
369
  function isCmcdEventKey(key) {
367
- return isCmcdRequestKey(key) || isCmcdResponseReceivedKey(key) || CMCD_EVENT_KEYS.includes(key);
370
+ return isCmcdRequestKey(key) || isCmcdResponseReceivedKey(key) || CMCD_EVENT_KEY_SET.has(key);
368
371
  }
369
372
 
373
+ //#endregion
374
+ //#region src/CMCD_INNER_LIST_KEYS.ts
375
+ /**
376
+ * Keys that are inner lists in V2 but plain scalars in V1.
377
+ *
378
+ * Used by both encoding (down-conversion) and decoding (up-conversion).
379
+ *
380
+ * @internal
381
+ */
382
+ const CMCD_INNER_LIST_KEYS = new Set([
383
+ "ab",
384
+ "bl",
385
+ "br",
386
+ "bsa",
387
+ "bsd",
388
+ "bsda",
389
+ "lab",
390
+ "lb",
391
+ "mtp",
392
+ "pb",
393
+ "tab",
394
+ "tb",
395
+ "tbl",
396
+ "tpb"
397
+ ]);
398
+
370
399
  //#endregion
371
400
  //#region src/CMCD_V1_KEYS.ts
372
401
  /**
@@ -397,6 +426,7 @@ const CMCD_V1_KEYS = [
397
426
 
398
427
  //#endregion
399
428
  //#region src/isCmcdV1Key.ts
429
+ const CMCD_V1_KEY_SET$1 = new Set(CMCD_V1_KEYS);
400
430
  /**
401
431
  * Filter function for CMCD v1 keys.
402
432
  *
@@ -410,11 +440,18 @@ const CMCD_V1_KEYS = [
410
440
  * {@includeCode ../test/isCmcdV1Key.test.ts#example}
411
441
  */
412
442
  function isCmcdV1Key(key) {
413
- return CMCD_V1_KEYS.includes(key) || isCmcdCustomKey(key);
443
+ return CMCD_V1_KEY_SET$1.has(key) || isCmcdCustomKey(key);
414
444
  }
415
445
 
416
446
  //#endregion
417
447
  //#region src/isTokenField.ts
448
+ const TOKEN_FIELDS = new Set([
449
+ "ot",
450
+ "sf",
451
+ "st",
452
+ "e",
453
+ "sta"
454
+ ]);
418
455
  /**
419
456
  * Checks if the given key is a token field.
420
457
  *
@@ -425,13 +462,7 @@ function isCmcdV1Key(key) {
425
462
  * @internal
426
463
  */
427
464
  function isTokenField(key) {
428
- return [
429
- "ot",
430
- "sf",
431
- "st",
432
- "e",
433
- "sta"
434
- ].includes(key);
465
+ return TOKEN_FIELDS.has(key);
435
466
  }
436
467
 
437
468
  //#endregion
@@ -457,25 +488,6 @@ const filterMap = {
457
488
  [CMCD_REQUEST_MODE]: isCmcdRequestKey
458
489
  };
459
490
  /**
460
- * V1 keys that use inner lists in V2 but plain numbers in V1.
461
- */
462
- const INNER_LIST_V1_KEYS = new Set([
463
- "ab",
464
- "bl",
465
- "br",
466
- "bsa",
467
- "bsd",
468
- "bsda",
469
- "lab",
470
- "lb",
471
- "mtp",
472
- "pb",
473
- "tab",
474
- "tb",
475
- "tbl",
476
- "tpb"
477
- ]);
478
- /**
479
491
  * Unwrap an inner list or SfItem value to a plain scalar.
480
492
  */
481
493
  function unwrapValue(value, ot) {
@@ -507,7 +519,7 @@ function downConvertToV1(obj) {
507
519
  result["nor"] = first.value;
508
520
  if (first.params?.r) result["nrr"] = first.params.r;
509
521
  } else result["nor"] = first;
510
- } else if (INNER_LIST_V1_KEYS.has(key)) result[key] = unwrapValue(value, obj["ot"]);
522
+ } else if (CMCD_INNER_LIST_KEYS.has(key)) result[key] = unwrapValue(value, obj["ot"]);
511
523
  else result[key] = value;
512
524
  }
513
525
  return result;
@@ -527,37 +539,37 @@ function prepareCmcdData(obj, options = {}) {
527
539
  const reportingMode = options.reportingMode || CMCD_REQUEST_MODE;
528
540
  const data = version === 1 ? downConvertToV1(obj) : obj;
529
541
  const keyFilter = version === 1 ? isCmcdV1Key : filterMap[reportingMode];
530
- let keys$1 = Object.keys(data).filter(keyFilter);
531
- if (data["e"] && data["e"] !== CMCD_EVENT_RESPONSE_RECEIVED) keys$1 = keys$1.filter((key) => !isCmcdResponseReceivedKey(key));
542
+ let keys = Object.keys(data).filter(keyFilter);
543
+ if (data["e"] && data["e"] !== CMCD_EVENT_RESPONSE_RECEIVED) keys = keys.filter((key) => !isCmcdResponseReceivedKey(key));
532
544
  const filter = options.filter;
533
- if (typeof filter === "function") keys$1 = keys$1.filter(filter);
545
+ if (typeof filter === "function") keys = keys.filter(filter);
534
546
  const isEventMode = reportingMode === CMCD_EVENT_MODE;
535
547
  if (isEventMode) {
536
548
  const eventType = data["e"];
537
- if (!keys$1.includes("e") && eventType != null) keys$1.push("e");
538
- if (!keys$1.includes("ts")) keys$1.push("ts");
539
- if (!keys$1.includes("cen") && data["cen"] != null && eventType === CMCD_EVENT_CUSTOM_EVENT) keys$1.push("cen");
549
+ if (!keys.includes("e") && eventType != null) keys.push("e");
550
+ if (!keys.includes("ts")) keys.push("ts");
551
+ if (!keys.includes("cen") && data["cen"] != null && eventType === CMCD_EVENT_CUSTOM_EVENT) keys.push("cen");
540
552
  }
541
- if (keys$1.length === 0) return results;
542
- if (version > 1 && !keys$1.includes("v")) keys$1.push("v");
543
- const formatters = Object.assign({}, CMCD_FORMATTER_MAP, options.formatters);
553
+ if (keys.length === 0) return results;
554
+ if (version > 1 && !keys.includes("v")) keys.push("v");
544
555
  const formatterOptions = {
545
556
  version,
546
557
  reportingMode,
547
558
  baseUrl: options.baseUrl
548
559
  };
549
- keys$1.sort().forEach((key) => {
560
+ keys.sort();
561
+ for (const key of keys) {
550
562
  let value = data[key];
551
- const formatter = formatters[key];
563
+ const formatter = options.formatters?.[key] ?? CMCD_FORMATTER_MAP[key];
552
564
  if (typeof formatter === "function") value = formatter(value, formatterOptions);
553
- if (key === "v") if (version === 1) return;
565
+ if (key === "v") if (version === 1) continue;
554
566
  else value = version;
555
- if (key == "pr" && value === 1) return;
567
+ if (key === "pr" && value === 1) continue;
556
568
  if (isEventMode && key === "ts" && !Number.isFinite(value)) value = Date.now();
557
- if (!isValid(value)) return;
569
+ if (!isValid(value)) continue;
558
570
  if (isTokenField(key) && typeof value === "string") value = new SfToken(value);
559
571
  results[key] = value;
560
- });
572
+ }
561
573
  return results;
562
574
  }
563
575
 
@@ -601,12 +613,27 @@ const CmcdHeaderField = {
601
613
  SESSION: CMCD_SESSION,
602
614
  STATUS: CMCD_STATUS
603
615
  };
616
+ /**
617
+ * All CMCD header fields as an array.
618
+ *
619
+ * @public
620
+ */
621
+ const CMCD_HEADER_FIELDS = [
622
+ CMCD_OBJECT,
623
+ CMCD_REQUEST,
624
+ CMCD_SESSION,
625
+ CMCD_STATUS
626
+ ];
604
627
 
605
628
  //#endregion
606
629
  //#region src/CMCD_HEADER_MAP.ts
607
630
  /**
608
631
  * The map of CMCD keys to their appropriate header shard.
609
632
  *
633
+ * Note: Event-only keys (e, ts, cen, h) and response-received keys
634
+ * (rc, ttfb, ttlb, url, etc.) are intentionally absent. They are
635
+ * transmitted via the event-mode POST body, not HTTP headers.
636
+ *
610
637
  * @public
611
638
  */
612
639
  const CMCD_HEADER_MAP = {
@@ -672,14 +699,14 @@ function createHeaderMap(headerMap) {
672
699
  function groupCmcdHeaders(cmcd, customHeaderMap) {
673
700
  const result = {};
674
701
  if (!cmcd) return result;
675
- const keys$1 = Object.keys(cmcd);
702
+ const keys = Object.keys(cmcd);
676
703
  const custom = customHeaderMap ? createHeaderMap(customHeaderMap) : {};
677
- return keys$1.reduce((acc, key) => {
704
+ for (const key of keys) {
678
705
  const field = CMCD_HEADER_MAP[key] || custom[key] || CmcdHeaderField.REQUEST;
679
- const data = acc[field] ??= {};
706
+ const data = result[field] ??= {};
680
707
  data[key] = cmcd[key];
681
- return acc;
682
- }, result);
708
+ }
709
+ return result;
683
710
  }
684
711
 
685
712
  //#endregion
@@ -696,11 +723,11 @@ function groupCmcdHeaders(cmcd, customHeaderMap) {
696
723
  function toPreparedCmcdHeaders(data, customHeaderMap) {
697
724
  const result = {};
698
725
  const shards = groupCmcdHeaders(data, customHeaderMap);
699
- return Object.entries(shards).reduce((acc, [field, value]) => {
726
+ for (const [field, value] of Object.entries(shards)) {
700
727
  const shard = encodeSfDict(value, { whitespace: false });
701
- if (shard) acc[field] = shard;
702
- return acc;
703
- }, result);
728
+ if (shard) result[field] = shard;
729
+ }
730
+ return result;
704
731
  }
705
732
 
706
733
  //#endregion
@@ -859,18 +886,17 @@ const CMCD_DEFAULT_TIME_INTERVAL = 30;
859
886
 
860
887
  //#endregion
861
888
  //#region src/CMCD_KEYS.ts
862
- const keySet = new Set([
863
- ...CMCD_V1_KEYS,
864
- ...CMCD_REQUEST_KEYS,
865
- ...CMCD_RESPONSE_KEYS,
866
- ...CMCD_EVENT_KEYS
867
- ]);
868
889
  /**
869
890
  * A list of all CMCD keys.
870
891
  *
871
892
  * @public
872
893
  */
873
- const CMCD_KEYS = Array.from(keySet);
894
+ const CMCD_KEYS = [
895
+ ...CMCD_V1_KEYS,
896
+ ...CMCD_REQUEST_KEYS,
897
+ ...CMCD_RESPONSE_KEYS,
898
+ ...CMCD_EVENT_KEYS
899
+ ].filter((key, index, arr) => arr.indexOf(key) === index);
874
900
 
875
901
  //#endregion
876
902
  //#region src/CMCD_V1.ts
@@ -963,11 +989,11 @@ const CmcdTransmissionMode = {
963
989
  //#endregion
964
990
  //#region src/CmcdReporter.ts
965
991
  function createEncodingOptions(reportingMode, config, baseUrl) {
966
- const { enabledKeys = [] } = config;
992
+ const enabledKeySet = new Set(config.enabledKeys ?? []);
967
993
  return {
968
994
  version: config.version || CMCD_V2,
969
995
  reportingMode,
970
- filter: (key) => enabledKeys.includes(key),
996
+ filter: (key) => enabledKeySet.has(key),
971
997
  baseUrl
972
998
  };
973
999
  }
@@ -1034,6 +1060,10 @@ var CmcdReporter = class {
1034
1060
  }
1035
1061
  /**
1036
1062
  * Starts the CMCD reporter. Called by the player when the reporter is enabled.
1063
+ *
1064
+ * Note: This fires an initial time-interval event immediately (synchronously)
1065
+ * before the first interval elapses. Ensure CMCD data (sid, cid, etc.) is
1066
+ * populated before calling start().
1037
1067
  */
1038
1068
  start() {
1039
1069
  this.eventTargets.forEach((target, config) => {
@@ -1071,10 +1101,7 @@ var CmcdReporter = class {
1071
1101
  * @param data - The data to update.
1072
1102
  */
1073
1103
  update(data) {
1074
- if (data.sid && data.sid !== this.data.sid) {
1075
- this.resetSession();
1076
- return;
1077
- }
1104
+ if (data.sid && data.sid !== this.data.sid) this.resetSession();
1078
1105
  if (data.msd && !isNaN(data.msd)) this.msd = data.msd;
1079
1106
  this.data = {
1080
1107
  ...this.data,
@@ -1189,7 +1216,7 @@ var CmcdReporter = class {
1189
1216
  * Creates a new request with the CMCD request report data applied. Called by the player
1190
1217
  * before sending the request.
1191
1218
  *
1192
- * @param req - The request to apply the CMCD request report to.
1219
+ * @param request - The request to apply the CMCD request report to.
1193
1220
  * @param data - The data to apply to the request. This data only
1194
1221
  * applies to this request report. Persistent data
1195
1222
  * should be updated using `update()`.
@@ -1245,7 +1272,9 @@ var CmcdReporter = class {
1245
1272
  if (queue.length < config.batchSize && !flush) return;
1246
1273
  const deleteCount = flush ? queue.length : config.batchSize;
1247
1274
  const events = queue.splice(0, deleteCount);
1248
- this.sendEventReport(config, events);
1275
+ this.sendEventReport(config, events).catch(() => {
1276
+ target.queue.unshift(...events);
1277
+ });
1249
1278
  reprocess ||= queue.length > 0;
1250
1279
  });
1251
1280
  if (reprocess) this.processEventTargets();
@@ -1262,17 +1291,10 @@ var CmcdReporter = class {
1262
1291
  url: target.url,
1263
1292
  method: "POST",
1264
1293
  headers: { "Content-Type": "text/cmcd" },
1265
- body: data.reduce((acc, item) => acc += `${encodeCmcd(item, options)}\n`, "")
1294
+ body: data.map((item) => encodeCmcd(item, options)).join("\n") + "\n"
1266
1295
  });
1267
- switch (status) {
1268
- case 410:
1269
- this.eventTargets.delete(target);
1270
- break;
1271
- case 429: break;
1272
- default:
1273
- if (status > 499 && status < 600) {}
1274
- break;
1275
- }
1296
+ if (status === 410) this.eventTargets.delete(target);
1297
+ else if (status === 429 || status > 499 && status < 600) throw new Error(`Event report failed with status ${status}`);
1276
1298
  }
1277
1299
  /**
1278
1300
  * Resets the session related data. Called when the session ID changes.
@@ -1315,116 +1337,909 @@ const CmcdStreamType = {
1315
1337
  LOW_LATENCY: "ll"
1316
1338
  };
1317
1339
 
1340
+ //#endregion
1341
+ //#region src/upConvertToV2.ts
1342
+ /**
1343
+ * Up-convert V1 CMCD data to V2 format.
1344
+ *
1345
+ * - Wraps plain scalar values in arrays for inner-list keys.
1346
+ * - Wraps `nor` string in an array.
1347
+ *
1348
+ * If the data is already V2 (has `v: 2`), it is returned unchanged.
1349
+ *
1350
+ * @internal
1351
+ */
1352
+ function upConvertToV2(obj) {
1353
+ if (obj["v"] === CMCD_V2) return obj;
1354
+ const result = {};
1355
+ for (const [key, value] of Object.entries(obj)) {
1356
+ if (value == null) {
1357
+ result[key] = value;
1358
+ continue;
1359
+ }
1360
+ if (CMCD_INNER_LIST_KEYS.has(key) && !Array.isArray(value)) result[key] = [value];
1361
+ else if (key === "nor" && typeof value === "string") result[key] = [value];
1362
+ else result[key] = value;
1363
+ }
1364
+ return result;
1365
+ }
1366
+
1318
1367
  //#endregion
1319
1368
  //#region src/decodeCmcd.ts
1320
1369
  function reduceValue(value) {
1321
1370
  if (Array.isArray(value)) return value.map(reduceValue);
1322
1371
  if (typeof value === "symbol") return symbolToStr(value);
1323
1372
  if (value instanceof SfItem && !value.params) return reduceValue(value.value);
1324
- if (typeof value === "string") return decodeURIComponent(value);
1373
+ if (typeof value === "string") return value;
1325
1374
  return value;
1326
1375
  }
1376
+ function decodeCmcd(cmcd, options) {
1377
+ if (!cmcd) return {};
1378
+ const sfDict = decodeSfDict(cmcd);
1379
+ const result = {};
1380
+ for (const [key, item] of Object.entries(sfDict)) result[key] = reduceValue(item.value);
1381
+ if (options?.convertToLatest) return upConvertToV2(result);
1382
+ return result;
1383
+ }
1384
+
1385
+ //#endregion
1386
+ //#region src/ensureHeaders.ts
1387
+ /**
1388
+ * Converts a record of header fields to a `Headers` instance if necessary.
1389
+ *
1390
+ * @param headers - A `Headers` instance or a plain record of header fields.
1391
+ * @returns A `Headers` instance.
1392
+ *
1393
+ * @internal
1394
+ */
1395
+ function ensureHeaders(headers) {
1396
+ return headers instanceof Headers ? headers : new Headers(headers);
1397
+ }
1398
+
1399
+ //#endregion
1400
+ //#region src/fromCmcdHeaders.ts
1401
+ function fromCmcdHeaders(headers, options) {
1402
+ const h = ensureHeaders(headers);
1403
+ const result = {};
1404
+ for (const field of CMCD_HEADER_FIELDS) {
1405
+ const value = h.get(field);
1406
+ if (value) Object.assign(result, decodeCmcd(value));
1407
+ }
1408
+ if (options?.convertToLatest) return upConvertToV2(result);
1409
+ return result;
1410
+ }
1411
+
1412
+ //#endregion
1413
+ //#region src/fromCmcdQuery.ts
1414
+ function fromCmcdQuery(query, options) {
1415
+ if (typeof query === "string") query = new URLSearchParams(query);
1416
+ return decodeCmcd(query.get(CMCD_PARAM) ?? "", options);
1417
+ }
1418
+
1419
+ //#endregion
1420
+ //#region src/fromCmcdUrl.ts
1421
+ function fromCmcdUrl(url, options) {
1422
+ return decodeCmcd(decodeURIComponent(url.replace(/^CMCD=/, "")), options);
1423
+ }
1424
+
1425
+ //#endregion
1426
+ //#region src/isCmcdV1Data.ts
1327
1427
  /**
1328
- * Decode a CMCD string to an object.
1428
+ * Check if a CMCD data object is version 1.
1329
1429
  *
1330
- * @param cmcd - The CMCD string to decode.
1430
+ * @param data - The CMCD data object to check.
1331
1431
  *
1332
- * @returns The decoded CMCD object.
1432
+ * @returns `true` if the data is version 1, `false` otherwise.
1333
1433
  *
1334
1434
  * @public
1335
1435
  *
1336
1436
  * @example
1337
- * {@includeCode ../test/decodeCmcd.test.ts#example}
1437
+ * {@includeCode ../test/isCmcdV1Data.test.ts#example}
1338
1438
  */
1339
- function decodeCmcd(cmcd) {
1340
- if (!cmcd) return {};
1341
- const sfDict = decodeSfDict(cmcd);
1342
- return Object.entries(sfDict).reduce((acc, [key, item]) => {
1343
- acc[key] = reduceValue(item.value);
1344
- return acc;
1345
- }, {});
1439
+ function isCmcdV1Data(data) {
1440
+ return data.v !== CMCD_V2;
1346
1441
  }
1347
1442
 
1348
1443
  //#endregion
1349
- //#region src/fromCmcdHeaders.ts
1350
- const keys = [
1351
- CMCD_OBJECT,
1352
- CMCD_REQUEST,
1353
- CMCD_SESSION,
1354
- CMCD_STATUS
1355
- ];
1444
+ //#region src/isCmcdV2Data.ts
1356
1445
  /**
1357
- * Decode CMCD data from request headers.
1446
+ * Check if a CMCD data object is version 2.
1358
1447
  *
1359
- * @param headers - The request headers to decode.
1448
+ * @param data - The CMCD data object to check.
1360
1449
  *
1361
- * @returns The decoded CMCD data.
1450
+ * @returns `true` if the data is version 2, `false` otherwise.
1362
1451
  *
1363
1452
  * @public
1364
1453
  *
1365
1454
  * @example
1366
- * {@includeCode ../test/fromCmcdHeaders.test.ts#example}
1455
+ * {@includeCode ../test/isCmcdV2Data.test.ts#example}
1367
1456
  */
1368
- function fromCmcdHeaders(headers) {
1369
- if (!(headers instanceof Headers)) headers = new Headers(headers);
1370
- return keys.reduce((acc, key) => {
1371
- const value = headers.get(key);
1372
- return Object.assign(acc, decodeCmcd(value));
1373
- }, {});
1457
+ function isCmcdV2Data(data) {
1458
+ return data.v === CMCD_V2;
1374
1459
  }
1375
1460
 
1376
1461
  //#endregion
1377
- //#region src/fromCmcdQuery.ts
1462
+ //#region src/toCmcdValue.ts
1378
1463
  /**
1379
- * Decode CMCD data from a query string.
1464
+ * Convert a value to a CMCD value.
1380
1465
  *
1381
- * @param query - The query string to decode.
1466
+ * @param value - The value to convert to a CMCD value.
1467
+ * @param params - The parameters to convert to a CMCD value.
1468
+ * @returns The CMCD value.
1469
+ *
1470
+ * @public
1471
+ */
1472
+ function toCmcdValue(value, params) {
1473
+ return new SfItem(value, params);
1474
+ }
1475
+
1476
+ //#endregion
1477
+ //#region src/CmcdValidationSeverity.ts
1478
+ /**
1479
+ * CMCD validation severity level: error.
1480
+ *
1481
+ * @public
1482
+ */
1483
+ const CMCD_VALIDATION_SEVERITY_ERROR = "error";
1484
+ /**
1485
+ * CMCD validation severity level: warning.
1382
1486
  *
1383
- * @returns The decoded CMCD data.
1487
+ * @public
1488
+ */
1489
+ const CMCD_VALIDATION_SEVERITY_WARNING = "warning";
1490
+ /**
1491
+ * CMCD validation severity level.
1384
1492
  *
1385
1493
  * @public
1494
+ */
1495
+ const CmcdValidationSeverity = {
1496
+ ERROR: CMCD_VALIDATION_SEVERITY_ERROR,
1497
+ WARNING: CMCD_VALIDATION_SEVERITY_WARNING
1498
+ };
1499
+
1500
+ //#endregion
1501
+ //#region src/mergeValidationResults.ts
1502
+ /**
1503
+ * Merges multiple validation results into a single result.
1504
+ *
1505
+ * @internal
1506
+ */
1507
+ function mergeValidationResults(...results) {
1508
+ const issues = results.flatMap((r) => r.issues);
1509
+ return {
1510
+ valid: issues.every((i) => i.severity !== CMCD_VALIDATION_SEVERITY_ERROR),
1511
+ issues
1512
+ };
1513
+ }
1514
+
1515
+ //#endregion
1516
+ //#region src/resolveVersion.ts
1517
+ /**
1518
+ * Resolves the CMCD version from explicit options, the payload's `v` key, or the default (v1).
1519
+ *
1520
+ * @internal
1521
+ */
1522
+ function resolveVersion(data, options) {
1523
+ if (options?.version === 1 || options?.version === 2) return options.version;
1524
+ const payloadVersion = data["v"];
1525
+ if (payloadVersion === 1 || payloadVersion === 2) return payloadVersion;
1526
+ return CMCD_V1;
1527
+ }
1528
+
1529
+ //#endregion
1530
+ //#region src/validateCmcdKeys.ts
1531
+ const CMCD_V1_KEY_SET = new Set(CMCD_V1_KEYS);
1532
+ const CMCD_KEY_SET = new Set(CMCD_KEYS);
1533
+ /**
1534
+ * Validates that all keys in a CMCD payload are recognized spec keys or valid custom keys.
1386
1535
  *
1387
1536
  * @example
1388
- * {@includeCode ../test/fromCmcdQuery.test.ts#example}
1537
+ * {@includeCode ../test/validateCmcdKeys.test.ts#example}
1538
+ *
1539
+ * @param data - The CMCD payload to validate.
1540
+ * @param options - Validation options.
1541
+ * @returns The validation result.
1542
+ *
1543
+ * @public
1389
1544
  */
1390
- function fromCmcdQuery(query) {
1391
- if (typeof query === "string") query = new URLSearchParams(query);
1392
- return decodeCmcd(query.get(CMCD_PARAM));
1545
+ function validateCmcdKeys(data, options) {
1546
+ const version = resolveVersion(data, options);
1547
+ const validKeySet = version === CMCD_V1 ? CMCD_V1_KEY_SET : CMCD_KEY_SET;
1548
+ const issues = [];
1549
+ for (const key of Object.keys(data)) {
1550
+ if (isCmcdCustomKey(key)) continue;
1551
+ if (!validKeySet.has(key)) issues.push({
1552
+ key,
1553
+ message: `Unknown CMCD key "${key}" for version ${version}.`,
1554
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
1555
+ });
1556
+ }
1557
+ return {
1558
+ valid: issues.length === 0,
1559
+ issues
1560
+ };
1393
1561
  }
1394
1562
 
1395
1563
  //#endregion
1396
- //#region src/fromCmcdUrl.ts
1564
+ //#region src/validateCmcdStructure.ts
1397
1565
  /**
1398
- * Decode CMCD data from a url encoded string.
1566
+ * Validates the structural rules of a CMCD payload.
1399
1567
  *
1400
- * @param url - The url encoded string to decode.
1568
+ * @example
1569
+ * {@includeCode ../test/validateCmcdStructure.test.ts#example}
1570
+ *
1571
+ * @param data - The CMCD payload to validate.
1572
+ * @param options - Validation options.
1573
+ * @returns The validation result.
1574
+ *
1575
+ * @public
1576
+ */
1577
+ function validateCmcdStructure(data, options) {
1578
+ const version = resolveVersion(data, options);
1579
+ const issues = [];
1580
+ if (options?.reportingMode === CMCD_REQUEST_MODE) {
1581
+ for (const key of CMCD_EVENT_KEYS) if (key in data) issues.push({
1582
+ key,
1583
+ message: `Event key "${key}" must not be present in request mode.`,
1584
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
1585
+ });
1586
+ for (const key of CMCD_RESPONSE_KEYS) if (key in data) issues.push({
1587
+ key,
1588
+ message: `Response key "${key}" must not be present in request mode.`,
1589
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
1590
+ });
1591
+ }
1592
+ if (options?.reportingMode === CMCD_EVENT_MODE) {
1593
+ if (!("e" in data)) issues.push({
1594
+ key: "e",
1595
+ message: "Event mode requires the \"e\" key to be present.",
1596
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
1597
+ });
1598
+ if (!("ts" in data)) issues.push({
1599
+ key: "ts",
1600
+ message: "Event mode requires the \"ts\" key to be present.",
1601
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
1602
+ });
1603
+ }
1604
+ if ("e" in data) {
1605
+ if (data["e"] === CMCD_EVENT_CUSTOM_EVENT) {
1606
+ if (!("cen" in data)) issues.push({
1607
+ key: "cen",
1608
+ message: "Custom event (e=\"ce\") requires the \"cen\" key to be present.",
1609
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
1610
+ });
1611
+ } else if ("cen" in data) issues.push({
1612
+ key: "cen",
1613
+ message: "The \"cen\" key must only be present when e=\"ce\".",
1614
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
1615
+ });
1616
+ if (data["e"] === CMCD_EVENT_RESPONSE_RECEIVED) {
1617
+ if (!("url" in data)) issues.push({
1618
+ key: "url",
1619
+ message: "Response received event (e=\"rr\") requires the \"url\" key to be present.",
1620
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
1621
+ });
1622
+ } else for (const key of CMCD_RESPONSE_KEYS) if (key in data) issues.push({
1623
+ key,
1624
+ message: `Response key "${key}" must only be present when e="rr".`,
1625
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
1626
+ });
1627
+ if (data["e"] === CMCD_EVENT_PLAY_STATE && !("sta" in data)) issues.push({
1628
+ key: "sta",
1629
+ message: "Play state event (e=\"ps\") requires the \"sta\" key to be present.",
1630
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
1631
+ });
1632
+ if (data["e"] === CMCD_EVENT_ERROR && !("ec" in data)) issues.push({
1633
+ key: "ec",
1634
+ message: "Error event (e=\"e\") requires the \"ec\" key to be present.",
1635
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
1636
+ });
1637
+ }
1638
+ if ("v" in data && data["v"] !== 1 && data["v"] !== 2) issues.push({
1639
+ key: "v",
1640
+ message: `Unsupported CMCD version "${String(data["v"])}". Expected 1 or 2.`,
1641
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
1642
+ });
1643
+ else if (version > 1 && !("v" in data)) issues.push({
1644
+ key: "v",
1645
+ message: "Version 2 payloads require the \"v\" key to be present.",
1646
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
1647
+ });
1648
+ else if (version === CMCD_V1 && "v" in data) issues.push({
1649
+ key: "v",
1650
+ message: "Version 1 payloads should omit the \"v\" key (v1 is the default).",
1651
+ severity: CMCD_VALIDATION_SEVERITY_WARNING
1652
+ });
1653
+ return {
1654
+ valid: issues.every((i) => i.severity !== CMCD_VALIDATION_SEVERITY_ERROR),
1655
+ issues
1656
+ };
1657
+ }
1658
+
1659
+ //#endregion
1660
+ //#region src/CMCD_KEY_TYPES.ts
1661
+ /**
1662
+ * CMCD key value type: inner list of numbers with token identifiers.
1663
+ *
1664
+ * @internal
1665
+ */
1666
+ const CMCD_KEY_TYPE_NUMBER_LIST = "number[]";
1667
+ /**
1668
+ * CMCD key value type: inner list of strings.
1669
+ *
1670
+ * @internal
1671
+ */
1672
+ const CMCD_KEY_TYPE_STRING_LIST = "string[]";
1673
+ /**
1674
+ * CMCD key value type: integer.
1675
+ *
1676
+ * @internal
1677
+ */
1678
+ const CMCD_KEY_TYPE_INTEGER = "integer";
1679
+ /**
1680
+ * CMCD key value type: number (decimal).
1681
+ *
1682
+ * @internal
1683
+ */
1684
+ const CMCD_KEY_TYPE_NUMBER = "number";
1685
+ /**
1686
+ * CMCD key value type: boolean.
1687
+ *
1688
+ * @internal
1689
+ */
1690
+ const CMCD_KEY_TYPE_BOOLEAN = "boolean";
1691
+ /**
1692
+ * CMCD key value type: string.
1693
+ *
1694
+ * @internal
1695
+ */
1696
+ const CMCD_KEY_TYPE_STRING = "string";
1697
+ /**
1698
+ * CMCD key value type: token.
1699
+ *
1700
+ * @internal
1701
+ */
1702
+ const CMCD_KEY_TYPE_TOKEN = "token";
1703
+ /**
1704
+ * Maps each CMCD spec key to its expected value type for v2.
1705
+ * Keys that differ between v1 and v2 are handled by CMCD_V1_KEY_TYPE_OVERRIDES.
1706
+ *
1707
+ * @internal
1708
+ */
1709
+ const CMCD_KEY_TYPES = {
1710
+ ab: CMCD_KEY_TYPE_NUMBER_LIST,
1711
+ bl: CMCD_KEY_TYPE_NUMBER_LIST,
1712
+ br: CMCD_KEY_TYPE_NUMBER_LIST,
1713
+ bsa: CMCD_KEY_TYPE_NUMBER_LIST,
1714
+ bsd: CMCD_KEY_TYPE_NUMBER_LIST,
1715
+ bsda: CMCD_KEY_TYPE_NUMBER_LIST,
1716
+ lab: CMCD_KEY_TYPE_NUMBER_LIST,
1717
+ lb: CMCD_KEY_TYPE_NUMBER_LIST,
1718
+ mtp: CMCD_KEY_TYPE_NUMBER_LIST,
1719
+ pb: CMCD_KEY_TYPE_NUMBER_LIST,
1720
+ tab: CMCD_KEY_TYPE_NUMBER_LIST,
1721
+ tb: CMCD_KEY_TYPE_NUMBER_LIST,
1722
+ tbl: CMCD_KEY_TYPE_NUMBER_LIST,
1723
+ tpb: CMCD_KEY_TYPE_NUMBER_LIST,
1724
+ ec: CMCD_KEY_TYPE_STRING_LIST,
1725
+ nor: CMCD_KEY_TYPE_STRING_LIST,
1726
+ d: CMCD_KEY_TYPE_INTEGER,
1727
+ dfa: CMCD_KEY_TYPE_INTEGER,
1728
+ dl: CMCD_KEY_TYPE_INTEGER,
1729
+ ltc: CMCD_KEY_TYPE_INTEGER,
1730
+ msd: CMCD_KEY_TYPE_INTEGER,
1731
+ pt: CMCD_KEY_TYPE_INTEGER,
1732
+ rc: CMCD_KEY_TYPE_INTEGER,
1733
+ rtp: CMCD_KEY_TYPE_INTEGER,
1734
+ sn: CMCD_KEY_TYPE_INTEGER,
1735
+ ts: CMCD_KEY_TYPE_INTEGER,
1736
+ ttfb: CMCD_KEY_TYPE_INTEGER,
1737
+ ttfbb: CMCD_KEY_TYPE_INTEGER,
1738
+ ttlb: CMCD_KEY_TYPE_INTEGER,
1739
+ v: CMCD_KEY_TYPE_INTEGER,
1740
+ pr: CMCD_KEY_TYPE_NUMBER,
1741
+ bg: CMCD_KEY_TYPE_BOOLEAN,
1742
+ bs: CMCD_KEY_TYPE_BOOLEAN,
1743
+ nr: CMCD_KEY_TYPE_BOOLEAN,
1744
+ su: CMCD_KEY_TYPE_BOOLEAN,
1745
+ cdn: CMCD_KEY_TYPE_STRING,
1746
+ cen: CMCD_KEY_TYPE_STRING,
1747
+ cid: CMCD_KEY_TYPE_STRING,
1748
+ cmsdd: CMCD_KEY_TYPE_STRING,
1749
+ cmsds: CMCD_KEY_TYPE_STRING,
1750
+ cs: CMCD_KEY_TYPE_STRING,
1751
+ h: CMCD_KEY_TYPE_STRING,
1752
+ nrr: CMCD_KEY_TYPE_STRING,
1753
+ sid: CMCD_KEY_TYPE_STRING,
1754
+ smrt: CMCD_KEY_TYPE_STRING,
1755
+ url: CMCD_KEY_TYPE_STRING,
1756
+ e: CMCD_KEY_TYPE_TOKEN,
1757
+ ot: CMCD_KEY_TYPE_TOKEN,
1758
+ sf: CMCD_KEY_TYPE_TOKEN,
1759
+ st: CMCD_KEY_TYPE_TOKEN,
1760
+ sta: CMCD_KEY_TYPE_TOKEN
1761
+ };
1762
+ /**
1763
+ * Maps keys to their v1-specific types when they differ from v2.
1764
+ *
1765
+ * @internal
1766
+ */
1767
+ const CMCD_V1_KEY_TYPE_OVERRIDES = {
1768
+ bl: CMCD_KEY_TYPE_INTEGER,
1769
+ br: CMCD_KEY_TYPE_NUMBER,
1770
+ mtp: CMCD_KEY_TYPE_INTEGER,
1771
+ tb: CMCD_KEY_TYPE_NUMBER,
1772
+ nor: CMCD_KEY_TYPE_STRING
1773
+ };
1774
+
1775
+ //#endregion
1776
+ //#region src/CMCD_STRING_LENGTH_LIMITS.ts
1777
+ /**
1778
+ * Maps CMCD keys to their maximum string length.
1779
+ *
1780
+ * @internal
1781
+ */
1782
+ const CMCD_STRING_LENGTH_LIMITS = {
1783
+ sid: 64,
1784
+ cid: 128,
1785
+ cdn: 128,
1786
+ h: 128,
1787
+ cen: 64
1788
+ };
1789
+ /**
1790
+ * Maximum length for custom key values.
1791
+ *
1792
+ * @internal
1793
+ */
1794
+ const CMCD_CUSTOM_KEY_VALUE_MAX_LENGTH = 64;
1795
+
1796
+ //#endregion
1797
+ //#region src/CMCD_TOKEN_VALUES.ts
1798
+ /**
1799
+ * Maps token keys to their valid values.
1800
+ *
1801
+ * @internal
1802
+ */
1803
+ const CMCD_TOKEN_VALUES = {
1804
+ e: [
1805
+ "bc",
1806
+ "ps",
1807
+ "e",
1808
+ "t",
1809
+ "c",
1810
+ "b",
1811
+ "m",
1812
+ "um",
1813
+ "pe",
1814
+ "pc",
1815
+ "rr",
1816
+ "as",
1817
+ "ae",
1818
+ "abs",
1819
+ "abe",
1820
+ "sk",
1821
+ "ce"
1822
+ ],
1823
+ ot: [
1824
+ "m",
1825
+ "a",
1826
+ "v",
1827
+ "av",
1828
+ "i",
1829
+ "c",
1830
+ "tt",
1831
+ "k",
1832
+ "o"
1833
+ ],
1834
+ sf: [
1835
+ "d",
1836
+ "h",
1837
+ "s",
1838
+ "o"
1839
+ ],
1840
+ st: [
1841
+ "v",
1842
+ "l",
1843
+ "ll"
1844
+ ],
1845
+ sta: [
1846
+ "s",
1847
+ "p",
1848
+ "k",
1849
+ "r",
1850
+ "a",
1851
+ "w",
1852
+ "e",
1853
+ "f",
1854
+ "q",
1855
+ "d"
1856
+ ]
1857
+ };
1858
+
1859
+ //#endregion
1860
+ //#region src/validateCmcdValues.ts
1861
+ const HUNDRED_ROUNDING_KEYS = new Set([
1862
+ "bl",
1863
+ "dl",
1864
+ "mtp",
1865
+ "rtp",
1866
+ "tbl"
1867
+ ]);
1868
+ const INTEGER_ROUNDING_KEYS = new Set([
1869
+ "br",
1870
+ "d",
1871
+ "tb"
1872
+ ]);
1873
+ function isFiniteNumber(value) {
1874
+ return typeof value === "number" && Number.isFinite(value);
1875
+ }
1876
+ function validateListValue(key, value, issues) {
1877
+ if (!Array.isArray(value)) {
1878
+ issues.push({
1879
+ key,
1880
+ message: `Key "${key}" must be an array.`,
1881
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
1882
+ });
1883
+ return;
1884
+ }
1885
+ for (let i = 0; i < value.length; i++) {
1886
+ const element = value[i];
1887
+ if (element instanceof SfItem) {
1888
+ if (!isFiniteNumber(element.value)) issues.push({
1889
+ key,
1890
+ message: `Key "${key}" array element [${i}] must be a finite number.`,
1891
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
1892
+ });
1893
+ } else if (!isFiniteNumber(element)) issues.push({
1894
+ key,
1895
+ message: `Key "${key}" array element [${i}] must be a finite number.`,
1896
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
1897
+ });
1898
+ }
1899
+ }
1900
+ function validateStringArrayValue(key, value, issues) {
1901
+ if (!Array.isArray(value)) {
1902
+ issues.push({
1903
+ key,
1904
+ message: `Key "${key}" must be an array.`,
1905
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
1906
+ });
1907
+ return;
1908
+ }
1909
+ for (let i = 0; i < value.length; i++) {
1910
+ const element = value[i];
1911
+ if (element instanceof SfItem) {
1912
+ if (typeof element.value !== "string") issues.push({
1913
+ key,
1914
+ message: `Key "${key}" array element [${i}] must be a string.`,
1915
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
1916
+ });
1917
+ } else if (typeof element !== "string") issues.push({
1918
+ key,
1919
+ message: `Key "${key}" array element [${i}] must be a string.`,
1920
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
1921
+ });
1922
+ }
1923
+ }
1924
+ /**
1925
+ * Validates that all values in a CMCD payload conform to the expected types and constraints.
1926
+ *
1927
+ * @example
1928
+ * {@includeCode ../test/validateCmcdValues.test.ts#example}
1401
1929
  *
1402
- * @returns The decoded CMCD data.
1930
+ * @param data - The CMCD payload to validate.
1931
+ * @param options - Validation options.
1932
+ * @returns The validation result.
1403
1933
  *
1404
1934
  * @public
1935
+ */
1936
+ function validateCmcdValues(data, options) {
1937
+ const version = resolveVersion(data, options);
1938
+ const issues = [];
1939
+ for (const [key, value] of Object.entries(data)) {
1940
+ if (isCmcdCustomKey(key)) {
1941
+ if (typeof value !== "string") issues.push({
1942
+ key,
1943
+ message: `Custom key "${key}" value must be a string or token.`,
1944
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
1945
+ });
1946
+ else if (value.length > CMCD_CUSTOM_KEY_VALUE_MAX_LENGTH) issues.push({
1947
+ key,
1948
+ message: `Custom key "${key}" value exceeds maximum length of ${CMCD_CUSTOM_KEY_VALUE_MAX_LENGTH}.`,
1949
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
1950
+ });
1951
+ continue;
1952
+ }
1953
+ if (key === "v") {
1954
+ if (value !== 1 && value !== 2) issues.push({
1955
+ key,
1956
+ message: `Key "v" must be 1 or 2.`,
1957
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
1958
+ });
1959
+ continue;
1960
+ }
1961
+ let expectedType = CMCD_KEY_TYPES[key];
1962
+ if (!expectedType) continue;
1963
+ if (version === CMCD_V1 && key in CMCD_V1_KEY_TYPE_OVERRIDES) expectedType = CMCD_V1_KEY_TYPE_OVERRIDES[key];
1964
+ switch (expectedType) {
1965
+ case CMCD_KEY_TYPE_INTEGER:
1966
+ if (!isFiniteNumber(value) || !Number.isInteger(value)) issues.push({
1967
+ key,
1968
+ message: `Key "${key}" must be a finite integer.`,
1969
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
1970
+ });
1971
+ else if (HUNDRED_ROUNDING_KEYS.has(key) && value % 100 !== 0) issues.push({
1972
+ key,
1973
+ message: `Key "${key}" should be rounded to the nearest 100.`,
1974
+ severity: CMCD_VALIDATION_SEVERITY_WARNING
1975
+ });
1976
+ break;
1977
+ case CMCD_KEY_TYPE_NUMBER:
1978
+ if (!isFiniteNumber(value)) issues.push({
1979
+ key,
1980
+ message: `Key "${key}" must be a finite number.`,
1981
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
1982
+ });
1983
+ else if (HUNDRED_ROUNDING_KEYS.has(key) && value % 100 !== 0) issues.push({
1984
+ key,
1985
+ message: `Key "${key}" should be rounded to the nearest 100.`,
1986
+ severity: CMCD_VALIDATION_SEVERITY_WARNING
1987
+ });
1988
+ else if (INTEGER_ROUNDING_KEYS.has(key) && !Number.isInteger(value)) issues.push({
1989
+ key,
1990
+ message: `Key "${key}" should be rounded to an integer.`,
1991
+ severity: CMCD_VALIDATION_SEVERITY_WARNING
1992
+ });
1993
+ break;
1994
+ case CMCD_KEY_TYPE_BOOLEAN:
1995
+ if (typeof value !== "boolean") issues.push({
1996
+ key,
1997
+ message: `Key "${key}" must be a boolean.`,
1998
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
1999
+ });
2000
+ break;
2001
+ case CMCD_KEY_TYPE_STRING:
2002
+ if (typeof value !== "string") issues.push({
2003
+ key,
2004
+ message: `Key "${key}" must be a string.`,
2005
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
2006
+ });
2007
+ else if (key in CMCD_STRING_LENGTH_LIMITS && value.length > CMCD_STRING_LENGTH_LIMITS[key]) issues.push({
2008
+ key,
2009
+ message: `Key "${key}" exceeds maximum length of ${CMCD_STRING_LENGTH_LIMITS[key]}.`,
2010
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
2011
+ });
2012
+ break;
2013
+ case CMCD_KEY_TYPE_TOKEN: {
2014
+ const validValues = CMCD_TOKEN_VALUES[key];
2015
+ if (validValues && !validValues.includes(value)) issues.push({
2016
+ key,
2017
+ message: `Key "${key}" has invalid token value "${String(value)}". Expected one of: ${validValues.join(", ")}.`,
2018
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
2019
+ });
2020
+ break;
2021
+ }
2022
+ case CMCD_KEY_TYPE_NUMBER_LIST:
2023
+ validateListValue(key, value, issues);
2024
+ break;
2025
+ case CMCD_KEY_TYPE_STRING_LIST:
2026
+ validateStringArrayValue(key, value, issues);
2027
+ break;
2028
+ }
2029
+ }
2030
+ return {
2031
+ valid: issues.every((i) => i.severity !== CMCD_VALIDATION_SEVERITY_ERROR),
2032
+ issues
2033
+ };
2034
+ }
2035
+
2036
+ //#endregion
2037
+ //#region src/validateCmcd.ts
2038
+ /**
2039
+ * Validates a CMCD payload by checking keys, values, and structure.
1405
2040
  *
1406
2041
  * @example
1407
- * {@includeCode ../test/fromCmcdUrl.test.ts#example}
2042
+ * {@includeCode ../test/validateCmcd.test.ts#example}
2043
+ *
2044
+ * @param data - The CMCD payload to validate.
2045
+ * @param options - Validation options.
2046
+ * @returns The validation result.
2047
+ *
2048
+ * @public
1408
2049
  */
1409
- function fromCmcdUrl(url) {
1410
- return decodeCmcd(decodeURIComponent(url.replace(/^CMCD=/, "")));
2050
+ function validateCmcd(data, options) {
2051
+ return mergeValidationResults(validateCmcdKeys(data, options), validateCmcdValues(data, options), validateCmcdStructure(data, options));
1411
2052
  }
1412
2053
 
1413
2054
  //#endregion
1414
- //#region src/toCmcdValue.ts
2055
+ //#region src/validateCmcdEvents.ts
1415
2056
  /**
1416
- * Convert a value to a CMCD value.
2057
+ * Validates a raw CMCD string as an event-mode payload.
1417
2058
  *
1418
- * @param value - The value to convert to a CMCD value.
1419
- * @param params - The parameters to convert to a CMCD value.
1420
- * @returns The CMCD value.
2059
+ * This function decodes the string internally and validates it with
2060
+ * `reportingMode` set to `'event'`. The input may contain multiple
2061
+ * newline-separated events (e.g. a `text/cmcd` POST body), in which
2062
+ * case each line is validated independently and the results are merged.
2063
+ *
2064
+ * @example
2065
+ * {@includeCode ../test/validateCmcdEvents.test.ts#example}
2066
+ *
2067
+ * @param cmcd - The raw CMCD-encoded string to validate. May contain
2068
+ * multiple newline-separated event lines.
2069
+ * @param options - Validation options (excluding `reportingMode`).
2070
+ * @returns The validation result including decoded data per event line.
1421
2071
  *
1422
2072
  * @public
1423
2073
  */
1424
- function toCmcdValue(value, params) {
1425
- return new SfItem(value, params);
2074
+ function validateCmcdEvents(cmcd, options) {
2075
+ const opts = {
2076
+ ...options,
2077
+ reportingMode: CMCD_EVENT_MODE
2078
+ };
2079
+ const lines = cmcd.split(/\r?\n/).filter((line) => line.length > 0);
2080
+ if (lines.length === 0) return {
2081
+ valid: false,
2082
+ issues: [{
2083
+ message: "Empty event mode payload. At least one event line is required.",
2084
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
2085
+ }],
2086
+ data: []
2087
+ };
2088
+ const decodedLines = [];
2089
+ const lineResults = [];
2090
+ for (let i = 0; i < lines.length; i++) try {
2091
+ const data = decodeCmcd(lines[i]);
2092
+ decodedLines.push(data);
2093
+ lineResults.push(validateCmcd(data, opts));
2094
+ } catch {
2095
+ decodedLines.push({});
2096
+ lineResults.push({
2097
+ valid: false,
2098
+ issues: [{
2099
+ message: `Failed to decode event line ${i + 1}: invalid structured field syntax.`,
2100
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
2101
+ }]
2102
+ });
2103
+ }
2104
+ return {
2105
+ ...mergeValidationResults(...lineResults),
2106
+ data: decodedLines
2107
+ };
2108
+ }
2109
+
2110
+ //#endregion
2111
+ //#region src/validateCmcdHeaders.ts
2112
+ /**
2113
+ * Validates CMCD HTTP headers by checking shard placement and payload validity.
2114
+ *
2115
+ * This function accepts raw CMCD header strings, decodes each shard
2116
+ * internally, verifies that each key is placed in its correct header shard,
2117
+ * then merges all shards and runs full payload validation (keys, values, and
2118
+ * structure) on the merged data.
2119
+ *
2120
+ * @example
2121
+ * {@includeCode ../test/validateCmcdHeaders.test.ts#example}
2122
+ *
2123
+ * @param headers - A `Headers` instance or a record of CMCD header fields to their raw encoded string values.
2124
+ * @param options - Validation options (excluding `reportingMode`).
2125
+ * @returns The validation result including decoded data.
2126
+ *
2127
+ * @public
2128
+ */
2129
+ function validateCmcdHeaders(headers, options) {
2130
+ const h = ensureHeaders(headers);
2131
+ const issues = [];
2132
+ const decoded = {};
2133
+ for (const headerField of CMCD_HEADER_FIELDS) {
2134
+ const raw = h.get(headerField);
2135
+ if (!raw) continue;
2136
+ let shard;
2137
+ try {
2138
+ shard = decodeCmcd(raw);
2139
+ } catch {
2140
+ issues.push({
2141
+ key: headerField,
2142
+ message: `Failed to decode "${headerField}" header: invalid structured field syntax.`,
2143
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
2144
+ });
2145
+ continue;
2146
+ }
2147
+ decoded[headerField] = shard;
2148
+ for (const key of Object.keys(shard)) {
2149
+ if (isCmcdCustomKey(key)) continue;
2150
+ const expectedHeader = CMCD_HEADER_MAP[key];
2151
+ if (expectedHeader && expectedHeader !== headerField) issues.push({
2152
+ key,
2153
+ message: `Key "${key}" is in "${headerField}" but should be in "${expectedHeader}".`,
2154
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
2155
+ });
2156
+ }
2157
+ }
2158
+ const shardResult = {
2159
+ valid: issues.length === 0,
2160
+ issues
2161
+ };
2162
+ const merged = Object.assign({}, ...CMCD_HEADER_FIELDS.map((f) => decoded[f]).filter(Boolean));
2163
+ return {
2164
+ ...mergeValidationResults(shardResult, validateCmcd(merged, {
2165
+ ...options,
2166
+ reportingMode: CMCD_REQUEST_MODE
2167
+ })),
2168
+ data: merged
2169
+ };
2170
+ }
2171
+
2172
+ //#endregion
2173
+ //#region src/validateCmcdRequest.ts
2174
+ /**
2175
+ * Validates CMCD data from a request as a request-mode payload.
2176
+ *
2177
+ * Accepts a
2178
+ * {@link https://developer.mozilla.org/en-US/docs/Web/API/Request | Request}
2179
+ * object or an {@link @svta/cml-utils#HttpRequest | HttpRequest} object.
2180
+ *
2181
+ * The function checks for CMCD data in the HTTP headers first. If CMCD
2182
+ * headers are found, validation includes shard-placement checks via
2183
+ * {@link validateCmcdHeaders}. Otherwise, the CMCD query parameter is
2184
+ * extracted from the URL and validated.
2185
+ *
2186
+ * @example
2187
+ * {@includeCode ../test/validateCmcdRequest.test.ts#example}
2188
+ *
2189
+ * @param request - A `Request` or `HttpRequest` to validate.
2190
+ * @param options - Validation options (excluding `reportingMode`).
2191
+ * @returns The validation result including decoded data.
2192
+ *
2193
+ * @public
2194
+ */
2195
+ function validateCmcdRequest(request, options) {
2196
+ const headers = extractHeaderRecord(request.headers);
2197
+ if (headers) return validateCmcdHeaders(headers, options);
2198
+ const param = new URL(request.url).searchParams.get(CMCD_PARAM);
2199
+ if (!param) return {
2200
+ valid: false,
2201
+ issues: [{
2202
+ message: "No CMCD data found in request headers or query parameters.",
2203
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
2204
+ }],
2205
+ data: {}
2206
+ };
2207
+ let data;
2208
+ try {
2209
+ data = decodeCmcd(param);
2210
+ } catch {
2211
+ return {
2212
+ valid: false,
2213
+ issues: [{
2214
+ message: "Failed to decode CMCD query parameter: invalid structured field syntax.",
2215
+ severity: CMCD_VALIDATION_SEVERITY_ERROR
2216
+ }],
2217
+ data: {}
2218
+ };
2219
+ }
2220
+ return {
2221
+ ...validateCmcd(data, {
2222
+ ...options,
2223
+ reportingMode: CMCD_REQUEST_MODE
2224
+ }),
2225
+ data
2226
+ };
2227
+ }
2228
+ function extractHeaderRecord(headers) {
2229
+ if (!headers) return;
2230
+ const h = ensureHeaders(headers);
2231
+ const result = {};
2232
+ let found = false;
2233
+ for (const field of CMCD_HEADER_FIELDS) {
2234
+ const value = h.get(field);
2235
+ if (value) {
2236
+ result[field] = value;
2237
+ found = true;
2238
+ }
2239
+ }
2240
+ return found ? result : void 0;
1426
2241
  }
1427
2242
 
1428
2243
  //#endregion
1429
- export { CMCD_DEFAULT_TIME_INTERVAL, CMCD_EVENT_AD_BREAK_END, CMCD_EVENT_AD_BREAK_START, CMCD_EVENT_AD_END, CMCD_EVENT_AD_START, CMCD_EVENT_BACKGROUNDED_MODE, CMCD_EVENT_BITRATE_CHANGE, CMCD_EVENT_CONTENT_ID, CMCD_EVENT_CUSTOM_EVENT, CMCD_EVENT_ERROR, CMCD_EVENT_KEYS, CMCD_EVENT_MODE, CMCD_EVENT_MUTE, CMCD_EVENT_PLAYER_COLLAPSE, CMCD_EVENT_PLAYER_EXPAND, CMCD_EVENT_PLAY_STATE, CMCD_EVENT_RESPONSE_RECEIVED, CMCD_EVENT_SKIP, CMCD_EVENT_TIME_INTERVAL, CMCD_EVENT_UNMUTE, CMCD_FORMATTER_MAP, CMCD_HEADERS, CMCD_HEADER_MAP, CMCD_JSON, CMCD_KEYS, CMCD_OBJECT, CMCD_PARAM, CMCD_QUERY, CMCD_REQUEST, CMCD_REQUEST_KEYS, CMCD_REQUEST_MODE, CMCD_RESPONSE_KEYS, CMCD_SESSION, CMCD_STATUS, CMCD_V1, CMCD_V1_KEYS, CMCD_V2, CmcdEventType, CmcdHeaderField, CmcdObjectType, CmcdPlayerState, CmcdReporter, CmcdReportingMode, CmcdStreamType, CmcdStreamingFormat, CmcdTransmissionMode, appendCmcdHeaders, appendCmcdQuery, decodeCmcd, encodeCmcd, fromCmcdHeaders, fromCmcdQuery, fromCmcdUrl, groupCmcdHeaders, isCmcdCustomKey, isCmcdEventKey, isCmcdRequestKey, isCmcdResponseReceivedKey, isCmcdV1Key, prepareCmcdData, toCmcdHeaders, toCmcdQuery, toCmcdUrl, toCmcdValue };
2244
+ export { CMCD_DEFAULT_TIME_INTERVAL, CMCD_EVENT_AD_BREAK_END, CMCD_EVENT_AD_BREAK_START, CMCD_EVENT_AD_END, CMCD_EVENT_AD_START, CMCD_EVENT_BACKGROUNDED_MODE, CMCD_EVENT_BITRATE_CHANGE, CMCD_EVENT_CONTENT_ID, CMCD_EVENT_CUSTOM_EVENT, CMCD_EVENT_ERROR, CMCD_EVENT_KEYS, CMCD_EVENT_MODE, CMCD_EVENT_MUTE, CMCD_EVENT_PLAYER_COLLAPSE, CMCD_EVENT_PLAYER_EXPAND, CMCD_EVENT_PLAY_STATE, CMCD_EVENT_RESPONSE_RECEIVED, CMCD_EVENT_SKIP, CMCD_EVENT_TIME_INTERVAL, CMCD_EVENT_UNMUTE, CMCD_FORMATTER_MAP, CMCD_HEADERS, CMCD_HEADER_FIELDS, CMCD_HEADER_MAP, CMCD_JSON, CMCD_KEYS, CMCD_OBJECT, CMCD_PARAM, CMCD_QUERY, CMCD_REQUEST, CMCD_REQUEST_KEYS, CMCD_REQUEST_MODE, CMCD_RESPONSE_KEYS, CMCD_SESSION, CMCD_STATUS, CMCD_V1, CMCD_V1_KEYS, CMCD_V2, CMCD_VALIDATION_SEVERITY_ERROR, CMCD_VALIDATION_SEVERITY_WARNING, CmcdEventType, CmcdHeaderField, CmcdObjectType, CmcdPlayerState, CmcdReporter, CmcdReportingMode, CmcdStreamType, CmcdStreamingFormat, CmcdTransmissionMode, CmcdValidationSeverity, appendCmcdHeaders, appendCmcdQuery, decodeCmcd, encodeCmcd, fromCmcdHeaders, fromCmcdQuery, fromCmcdUrl, groupCmcdHeaders, isCmcdCustomKey, isCmcdEventKey, isCmcdRequestKey, isCmcdResponseReceivedKey, isCmcdV1Data, isCmcdV1Key, isCmcdV2Data, prepareCmcdData, toCmcdHeaders, toCmcdQuery, toCmcdUrl, toCmcdValue, validateCmcd, validateCmcdEvents, validateCmcdHeaders, validateCmcdKeys, validateCmcdRequest, validateCmcdStructure, validateCmcdValues };
1430
2245
  //# sourceMappingURL=index.js.map