@streamlayer/feature-advertisement 1.1.1-rc.1345 → 1.1.1-rc.2.pf58ff685

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.
Files changed (60) hide show
  1. package/lib/advertisement.js +24 -9
  2. package/lib/cjs/advertisement.js +24 -9
  3. package/lib/cjs/index.d.ts +2 -0
  4. package/lib/cjs/index.js +2 -0
  5. package/lib/cjs/units/ad-macros.d.ts +7 -0
  6. package/lib/cjs/units/ad-macros.js +91 -0
  7. package/lib/cjs/units/external-ad.d.ts +69 -0
  8. package/lib/cjs/units/external-ad.js +93 -62
  9. package/lib/cjs/units/external-pause-ad.js +7 -1
  10. package/lib/cjs/units/gam.d.ts +11 -1
  11. package/lib/cjs/units/gam.js +65 -17
  12. package/lib/cjs/units/parse-vast-extensions.d.ts +10 -1
  13. package/lib/cjs/units/parse-vast-extensions.js +5 -1
  14. package/lib/cjs/units/sponsor-logo.d.ts +78 -0
  15. package/lib/cjs/units/sponsor-logo.js +194 -0
  16. package/lib/cjs/units/ssai-detector.js +27 -11
  17. package/lib/cjs/units/ssai-log.d.ts +16 -0
  18. package/lib/cjs/units/ssai-log.js +23 -0
  19. package/lib/cjs/units/ssai-providers.js +48 -11
  20. package/lib/classic/advertisement.js +34 -17
  21. package/lib/classic/cjs/advertisement.js +34 -17
  22. package/lib/classic/cjs/index.js +2 -0
  23. package/lib/classic/cjs/units/ad-macros.js +115 -0
  24. package/lib/classic/cjs/units/external-ad.js +111 -64
  25. package/lib/classic/cjs/units/external-pause-ad.js +5 -1
  26. package/lib/classic/cjs/units/gam.js +73 -10
  27. package/lib/classic/cjs/units/parse-vast-extensions.js +5 -1
  28. package/lib/classic/cjs/units/sponsor-logo.js +201 -0
  29. package/lib/classic/cjs/units/ssai-detector.js +33 -8
  30. package/lib/classic/cjs/units/ssai-log.js +23 -0
  31. package/lib/classic/cjs/units/ssai-providers.js +86 -24
  32. package/lib/classic/index.js +1 -0
  33. package/lib/classic/units/ad-macros.js +113 -0
  34. package/lib/classic/units/external-ad.js +109 -65
  35. package/lib/classic/units/external-pause-ad.js +5 -1
  36. package/lib/classic/units/gam.js +73 -10
  37. package/lib/classic/units/parse-vast-extensions.js +5 -1
  38. package/lib/classic/units/sponsor-logo.js +197 -0
  39. package/lib/classic/units/ssai-detector.js +33 -8
  40. package/lib/classic/units/ssai-log.js +21 -0
  41. package/lib/classic/units/ssai-providers.js +86 -24
  42. package/lib/index.d.ts +2 -0
  43. package/lib/index.js +1 -0
  44. package/lib/units/ad-macros.d.ts +7 -0
  45. package/lib/units/ad-macros.js +87 -0
  46. package/lib/units/external-ad.d.ts +69 -0
  47. package/lib/units/external-ad.js +92 -64
  48. package/lib/units/external-pause-ad.js +7 -1
  49. package/lib/units/gam.d.ts +11 -1
  50. package/lib/units/gam.js +65 -17
  51. package/lib/units/parse-vast-extensions.d.ts +10 -1
  52. package/lib/units/parse-vast-extensions.js +5 -1
  53. package/lib/units/sponsor-logo.d.ts +78 -0
  54. package/lib/units/sponsor-logo.js +188 -0
  55. package/lib/units/ssai-detector.js +27 -11
  56. package/lib/units/ssai-log.d.ts +16 -0
  57. package/lib/units/ssai-log.js +18 -0
  58. package/lib/units/ssai-providers.js +48 -11
  59. package/lib/vast-client.d.ts +9 -0
  60. package/package.json +8 -8
@@ -21,6 +21,7 @@ import 'core-js/modules/esnext.set.symmetric-difference.v2.js';
21
21
  import 'core-js/modules/esnext.set.union.v2.js';
22
22
  import 'core-js/modules/web.dom-collections.iterator.js';
23
23
  import { createLogger } from '@streamlayer/sdk-web-logger/classic';
24
+ import { ssaiq } from './ssai-log.js';
24
25
 
25
26
  const log = createLogger('ssai:detector');
26
27
  const RANGE_EXPIRY_MS = 10 * 60 * 1000;
@@ -534,16 +535,37 @@ const createSsaiDetector = options => {
534
535
  // Malformed tags persist in the manifest across refreshes; log each once.
535
536
  const seenDroppedKeys = new Set();
536
537
  const attachMode = player ? 'hls' : videoRef && manifestUrl ? 'manifest-poll' : 'none';
537
- const recordDebugEvent = (type, detail, programMs) => {
538
+ /**
539
+ * The `[label]` a detector event's log line opens with — what KIND of thing
540
+ * happened, for a reader scanning a window: a manifest tag came in (`hls`), a
541
+ * break edge fired (`break`), or playback crossed a tracking marker (`beacon`).
542
+ */
543
+ const eventLabel = type => {
544
+ if (type.indexOf('progress:') === 0) return 'beacon';
545
+ if (type.indexOf('discovered') !== -1 || type === 'range-dropped') return 'hls';
546
+ return 'break';
547
+ };
548
+ /**
549
+ * One call per detector event, feeding both consumers at once: the capped
550
+ * ring buffer behind `getDebugSnapshot()` and the `ssaiq` narrative logger.
551
+ * `data` is whatever the detector already holds for the event — the parsed
552
+ * range, marker or break window — so a log reader sees exactly what the
553
+ * detector acted on, with no second parse of the manifest for its benefit.
554
+ */
555
+ const recordDebugEvent = (type, detail, programMs, data) => {
556
+ const at = programMs !== undefined ? programMs : currentProgramTimeMs();
538
557
  debugEvents.push({
539
558
  wallMs: Date.now(),
540
- programMs: programMs !== undefined ? programMs : currentProgramTimeMs(),
559
+ programMs: at,
541
560
  type,
542
561
  detail
543
562
  });
544
563
  if (debugEvents.length > DEBUG_EVENTS_MAX) {
545
564
  debugEvents.splice(0, debugEvents.length - DEBUG_EVENTS_MAX);
546
565
  }
566
+ ssaiq.info(_objectSpread2(_objectSpread2({}, data), {}, {
567
+ programMs: at
568
+ }), `[${eventLabel(type)}] ${type}${detail ? ` ${detail}` : ''}`);
547
569
  };
548
570
  const getVideo = () => {
549
571
  var _ref, _player$media2;
@@ -596,9 +618,6 @@ const createSsaiDetector = options => {
596
618
  }
597
619
  seenDroppedKeys.add(entry.key);
598
620
  recordDebugEvent('range-dropped', entry.detail);
599
- log.warn({
600
- detail: entry.detail
601
- }, 'ssai DATERANGE dropped');
602
621
  }
603
622
  continue;
604
623
  }
@@ -606,7 +625,9 @@ const createSsaiDetector = options => {
606
625
  const existing = rangesById.get(entry.range.id);
607
626
  if (!existing) {
608
627
  changed = true;
609
- recordDebugEvent('range-discovered', `${entry.range.creativeId} ${debugClock(entry.range.startMs)}+${entry.range.durationSec}s`);
628
+ recordDebugEvent('range-discovered', `${entry.range.creativeId} ${debugClock(entry.range.startMs)}+${entry.range.durationSec}s`, undefined, {
629
+ range: entry.range
630
+ });
610
631
  notifyAdDiscovered(entry.range.creativeId);
611
632
  } else if (existing.startMs !== entry.range.startMs || existing.endMs !== entry.range.endMs) {
612
633
  changed = true;
@@ -615,7 +636,9 @@ const createSsaiDetector = options => {
615
636
  } else if (entry.kind === 'marker') {
616
637
  if (!seenMarkerAdIds.has(entry.marker.adId)) {
617
638
  seenMarkerAdIds.add(entry.marker.adId);
618
- recordDebugEvent('marker-ad-discovered', `${entry.marker.adId} ${entry.marker.event}@${debugClock(entry.marker.timeMs)}`);
639
+ recordDebugEvent('marker-ad-discovered', `${entry.marker.adId} ${entry.marker.event}@${debugClock(entry.marker.timeMs)}`, undefined, {
640
+ marker: entry.marker
641
+ });
619
642
  notifyAdDiscovered(entry.marker.adId);
620
643
  }
621
644
  const existing = markersById.get(entry.marker.id);
@@ -627,7 +650,9 @@ const createSsaiDetector = options => {
627
650
  const existing = adBreaksById.get(entry.window.id);
628
651
  if (!existing) {
629
652
  changed = true;
630
- recordDebugEvent('adbreak-discovered', `${debugClock(entry.window.startMs)}-${debugClock(entry.window.endMs)}`);
653
+ recordDebugEvent('adbreak-discovered', `${debugClock(entry.window.startMs)}-${debugClock(entry.window.endMs)}`, undefined, {
654
+ window: entry.window
655
+ });
631
656
  } else if (existing.startMs !== entry.window.startMs || existing.endMs !== entry.window.endMs) {
632
657
  changed = true;
633
658
  }
@@ -0,0 +1,21 @@
1
+ import { createLogger } from '@streamlayer/sdk-web-logger/classic';
2
+
3
+ /**
4
+ * `ssaiq` — the SSAI ad NARRATIVE log: one line per thing that happens to a
5
+ * server-stitched ad, in the order it happens. The manifest tag the playout
6
+ * published, the metadata round trip it triggers, the break the SDK opens from
7
+ * it, and every beacon fired against it.
8
+ *
9
+ * Named for the Quortex/Synamedia DAI integration it was built for, but it is
10
+ * the path itself that is logged, so a StreamLayer-dialect stream tells the same
11
+ * story. Detector internals — clock sources, poll ticks, fold bookkeeping — stay
12
+ * on `ssai:*`, where `SL_DEBUG` can reach them without filling a demo window.
13
+ *
14
+ * A host publishes it to the page with `SL_LOG_EVENTS=ssaiq` (see
15
+ * `sdk-web-logger`); the demo's log window renders exactly that.
16
+ */
17
+ // Explicit type: the inferred pino type is not nameable across package
18
+ // boundaries (TS2742) when this file is compiled on its own.
19
+ const ssaiq = createLogger('ssaiq');
20
+
21
+ export { ssaiq };
@@ -2,6 +2,8 @@ import { asyncToGenerator as _asyncToGenerator, objectSpread2 as _objectSpread2
2
2
  import 'core-js/modules/es.error.cause.js';
3
3
  import 'core-js/modules/es.array.iterator.js';
4
4
  import 'core-js/modules/es.promise.js';
5
+ import 'core-js/modules/es.regexp.exec.js';
6
+ import 'core-js/modules/es.string.replace.js';
5
7
  import 'core-js/modules/esnext.iterator.constructor.js';
6
8
  import 'core-js/modules/esnext.iterator.filter.js';
7
9
  import 'core-js/modules/esnext.iterator.find.js';
@@ -10,9 +12,82 @@ import 'core-js/modules/web.dom-collections.iterator.js';
10
12
  import 'core-js/modules/web.dom-exception.stack.js';
11
13
  import { createLogger } from '@streamlayer/sdk-web-logger/classic';
12
14
  import { VASTClient } from '@dailymotion/vast-client';
15
+ import { ssaiq } from './ssai-log.js';
13
16
 
14
17
  const log = createLogger('ssai:provider');
15
18
  const DEFAULT_CREATIVE_API_BASE = 'https://hls.next.streamlayer.io/api/creatives/';
19
+ /**
20
+ * One creative-metadata round trip, on the narrative log: the request the SDK
21
+ * made and the body the ad service answered with, verbatim. This is the half of
22
+ * the story the manifest cannot tell — the ad's identity comes from the playout,
23
+ * its content comes from here.
24
+ */
25
+ const fetchCreativeJson = /*#__PURE__*/function () {
26
+ var _ref = _asyncToGenerator(function* (providerId, url, signal) {
27
+ var _raw, _raw2, _raw3;
28
+ // `adApiBase` is customer infrastructure and may carry a key in its query;
29
+ // the request uses the URL as given, the log gets it with such values masked.
30
+ const shown = maskUrlSecrets(url);
31
+ ssaiq.info({
32
+ provider: providerId,
33
+ url: shown
34
+ }, `[GET] → ${shown}`);
35
+ let response;
36
+ try {
37
+ response = yield fetch(url, {
38
+ signal
39
+ });
40
+ } catch (error) {
41
+ // An abort is the SDK cancelling its own request (break ended, creative
42
+ // changed) — a note, not a failure.
43
+ if ((error === null || error === void 0 ? void 0 : error.name) === 'AbortError') {
44
+ ssaiq.debug({
45
+ provider: providerId,
46
+ url: shown
47
+ }, `[GET] ✗ aborted ${shown}`);
48
+ } else {
49
+ ssaiq.warn({
50
+ provider: providerId,
51
+ url: shown,
52
+ error: String(error)
53
+ }, `[GET] ✗ ${shown}`);
54
+ }
55
+ throw error;
56
+ }
57
+ if (!response.ok) {
58
+ ssaiq.warn({
59
+ provider: providerId,
60
+ url: shown,
61
+ status: response.status
62
+ }, `[GET] ✗ ${response.status} ${shown}`);
63
+ throw new Error(`${providerId} creative request failed: ${response.status}`);
64
+ }
65
+ let raw;
66
+ try {
67
+ raw = yield response.json();
68
+ } catch (error) {
69
+ // A 200 with a body that is not JSON would otherwise leave the log looking
70
+ // like a request still in flight.
71
+ ssaiq.warn({
72
+ provider: providerId,
73
+ url: shown,
74
+ error: String(error)
75
+ }, `[GET] ✗ ${response.status} ${shown} — not JSON`);
76
+ throw error;
77
+ }
78
+ ssaiq.info({
79
+ provider: providerId,
80
+ status: response.status,
81
+ body: raw
82
+ }, `[GET] ← ${response.status} ad metadata — ${((_raw = raw) === null || _raw === void 0 ? void 0 : _raw.advertiser) || ((_raw2 = raw) === null || _raw2 === void 0 ? void 0 : _raw2.title) || ((_raw3 = raw) === null || _raw3 === void 0 ? void 0 : _raw3.creativeId) || 'ad'}`);
83
+ return raw;
84
+ });
85
+ return function fetchCreativeJson(_x, _x2, _x3) {
86
+ return _ref.apply(this, arguments);
87
+ };
88
+ }();
89
+ /** `?apiKey=…` → `?apiKey=«redacted»`, for URLs that go on a screen. Regex, not `URL`: this ships to 2016 TV engines. */
90
+ const maskUrlSecrets = url => url.replace(/([?&][^=&]*(?:token|secret|password|key|authorization)[^=&]*=)[^&]*/gi, '$1«redacted»');
16
91
  /**
17
92
  * Shared low-level beacon helper. Prefers `navigator.sendBeacon` and falls back
18
93
  * to a no-cors `fetch`. Used by the default impression path and available to
@@ -105,21 +180,14 @@ const createStreamLayerProvider = (config = {}) => {
105
180
  return {
106
181
  id: 'streamlayer',
107
182
  resolve: function () {
108
- var _ref = _asyncToGenerator(function* (creativeId, signal) {
109
- const url = buildCreativeUrl(creativeApiBase, creativeId);
110
- const response = yield fetch(url, {
111
- signal
112
- });
113
- if (!response.ok) {
114
- throw new Error(`creative request failed: ${response.status}`);
115
- }
116
- const raw = yield response.json();
183
+ var _ref2 = _asyncToGenerator(function* (creativeId, signal) {
184
+ const raw = yield fetchCreativeJson('streamlayer', buildCreativeUrl(creativeApiBase, creativeId), signal);
117
185
  return mapApiResponse(_objectSpread2(_objectSpread2({}, raw), {}, {
118
186
  creativeId: raw.creativeId || creativeId
119
187
  }));
120
188
  });
121
- return function resolve(_x, _x2) {
122
- return _ref.apply(this, arguments);
189
+ return function resolve(_x4, _x5) {
190
+ return _ref2.apply(this, arguments);
123
191
  };
124
192
  }(),
125
193
  fireImpression: fireDefaultImpression
@@ -166,23 +234,17 @@ const createQuortexProvider = (config = {}) => {
166
234
  return {
167
235
  id: 'quortex',
168
236
  resolve: function () {
169
- var _ref2 = _asyncToGenerator(function* (adId, signal) {
237
+ var _ref3 = _asyncToGenerator(function* (adId, signal) {
170
238
  if (!adApiBase) {
171
239
  throw new Error('quortex provider is missing `adApiBase`');
172
240
  }
173
- const response = yield fetch(buildQuortexUrl(adApiBase, tenantName, adId), {
174
- signal
175
- });
176
- if (!response.ok) {
177
- throw new Error(`quortex creative request failed: ${response.status}`);
178
- }
179
- const raw = yield response.json();
241
+ const raw = yield fetchCreativeJson('quortex', buildQuortexUrl(adApiBase, tenantName, adId), signal);
180
242
  return mapApiResponse(_objectSpread2(_objectSpread2({}, raw), {}, {
181
243
  creativeId: raw.creativeId || adId
182
244
  }));
183
245
  });
184
- return function resolve(_x3, _x4) {
185
- return _ref2.apply(this, arguments);
246
+ return function resolve(_x6, _x7) {
247
+ return _ref3.apply(this, arguments);
186
248
  };
187
249
  }(),
188
250
  fireImpression: fireDefaultImpression
@@ -190,7 +252,7 @@ const createQuortexProvider = (config = {}) => {
190
252
  };
191
253
  // --- VAST provider ---------------------------------------------------------
192
254
  const mapVastResponse = /*#__PURE__*/function () {
193
- var _ref3 = _asyncToGenerator(function* (creativeId, signal) {
255
+ var _ref4 = _asyncToGenerator(function* (creativeId, signal) {
194
256
  var _validAd$advertiser$f;
195
257
  const vastClient = new VASTClient();
196
258
  const parsedVAST = yield vastClient.get(creativeId, {
@@ -241,8 +303,8 @@ const mapVastResponse = /*#__PURE__*/function () {
241
303
  companion
242
304
  };
243
305
  });
244
- return function mapVastResponse(_x5, _x6) {
245
- return _ref3.apply(this, arguments);
306
+ return function mapVastResponse(_x8, _x9) {
307
+ return _ref4.apply(this, arguments);
246
308
  };
247
309
  }();
248
310
  /**
package/lib/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ /// <reference path="./vast-client.d.ts" />
1
2
  import { StreamLayerContext } from '@streamlayer/sdk-web-interfaces';
2
3
  declare module '@streamlayer/sdk-web-interfaces' {
3
4
  interface StreamLayerSDK {
@@ -9,6 +10,7 @@ export { type GAMStaticCore, type PauseAdTemplate, isFullbleedVideoTemplate } fr
9
10
  export { INSIGHT_BODY_MAX_LENGTH, type PauseAdCard, type PauseAdKind, type PauseAdMediaMode, } from './units/pause-ad-card.js';
10
11
  export { type PauseAdStore } from './units/pause-ad.js';
11
12
  export { type AdStore, type AdData, type AdCompanion, type AdNonLinear } from './units/external-ad.js';
13
+ export { selectSponsorLogo, type SponsorLogoAsset } from './units/sponsor-logo.js';
12
14
  export { gam, type GamNonceResult, type PalNonceManager } from './units/gam.js';
13
15
  export { createCompanionTracker, createVastTracker, resolveVastTrackerTarget, type VastTrackerTarget, } from './units/vast-tracker.js';
14
16
  export { type SsaiCreative, type SsaiTrackingEvent, type SsaiCompanion, type SsaiProgressEvent, DEFAULT_CREATIVE_API_BASE, } from './units/ssai-creative.js';
package/lib/index.js CHANGED
@@ -6,6 +6,7 @@ export { isFullbleedVideoTemplate } from './units/external-pause-ad.js';
6
6
  // unit spec and any deep importer get the card shapes without the store, and dropping the
7
7
  // `export *` keeps `units/pause-ad` from silently re-exporting `parsePauseAdCards`.
8
8
  export { INSIGHT_BODY_MAX_LENGTH, } from './units/pause-ad-card.js';
9
+ export { selectSponsorLogo } from './units/sponsor-logo.js';
9
10
  export { gam } from './units/gam.js';
10
11
  export { createCompanionTracker, createVastTracker, resolveVastTrackerTarget, } from './units/vast-tracker.js';
11
12
  export { DEFAULT_CREATIVE_API_BASE, } from './units/ssai-creative.js';
@@ -0,0 +1,7 @@
1
+ import type { AdTagParams } from '@streamlayer/sdk-web-core';
2
+ /**
3
+ * Fill the macros this SDK knows in an ad-tag URL. One timestamp per call, so a tag naming the macro
4
+ * twice stays self-consistent. `params` is read as given — the caller passes the host's CURRENT
5
+ * values at request time.
6
+ */
7
+ export declare const substituteAdTagMacros: (url: string, params?: AdTagParams) => string;
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Ad-tag macro substitution, for every ad server — not just GAM.
3
+ *
4
+ * Studio stores a programmatic tag verbatim (`gamRawUrl`, returned untouched by
5
+ * `processGamAdvertisement`), so whatever macros the ad server's own tag builder emitted reach the
6
+ * SDK literal. `gam()` used to fill only the GAM `[placeholder]` token, and only for
7
+ * `pubads.g.doubleclick.net` — every other vendor's tag went out with its macros unexpanded.
8
+ *
9
+ * That is not inert. Measured against the ADITION tag on event 11089
10
+ * (`…/banner?…&ts=[timestamp]&clickurl=[clickmacro]`): ADITION prepends the value of `clickurl` to
11
+ * its own redi URL, so a literal `[clickmacro]` came back inside `<CompanionClickThrough>` as
12
+ * `[clickmacro]https%3A%2F%2Fad1…` — a broken click-through. Sending `clickurl=` empty answers with
13
+ * a valid redi URL.
14
+ *
15
+ * **Two sources.** The cache buster is the SDK's own. The consent family — `${GDPR}` and
16
+ * `${GDPR_CONSENT_<gvlid>}`, IAB TCF, where the id is the vendor's Global Vendor List entry (39 is
17
+ * ADITION / Virtual Minds) — and the click macro come from the host app (`AdTagParams`, set through
18
+ * the `adTagParams` option or `sdk.setAdTagParams`). The SDK never reads a CMP and never guesses
19
+ * consent: a value the host did not pass goes out EMPTY, which ad servers read as "unknown" — never
20
+ * as `gdpr=0`. Empty rather than literal because a literal is not inert either: ADITION copied
21
+ * `${GDPR}` into its own redirect URL (`gdpr%3D%24%7BGDPR%7D`). See
22
+ * `docs/wiki/programmatic-ads-macros.md`.
23
+ *
24
+ * An unknown macro is left exactly as configured, which is what keeps the GAM `[placeholder]`
25
+ * tokens for the PAL path (`gam.ts` fills those per parameter, not per token).
26
+ */
27
+ /**
28
+ * All three spellings in the wild — `[NAME]` (IAB / SpotX), `${NAME}` (Google) and bare `{NAME}` —
29
+ * each with its delimiters either literal or PERCENT-ENCODED.
30
+ *
31
+ * Neither half of that is theoretical. ADITION emitted `[timestamp]` / `[clickmacro]` for the
32
+ * campaign on event 11089 and `{CACHEBUSTER}` / `{GDPR}` for the one on 6203 — same ad server, same
33
+ * advertiser, different tag builder. And Studio stores the 6203 tag as `ts=%7BCACHEBUSTER%7D`, so
34
+ * matching only literal braces made the whole substitution a silent no-op on it: an unexpanded
35
+ * macro raises no error and the ad server still answers 200. `searchParams.get()` decodes on read,
36
+ * which is what hides the encoding from anyone inspecting the parsed URL.
37
+ *
38
+ * The `$` is optional rather than a fourth branch — an unowned `${NAME}` still returns its full
39
+ * matched text, `$` included, so the brace forms cannot corrupt each other.
40
+ */
41
+ const MACRO_PATTERN = /(?:\[|%5B)([a-z0-9_]+)(?:\]|%5D)|(?:\$|%24)?(?:\{|%7B)([a-z0-9_]+)(?:\}|%7D)/gi;
42
+ /**
43
+ * Every host value lands inside a query-string VALUE, so it is encoded exactly once. That matters
44
+ * most for the click URL, which carries its own `?`/`&`: sent raw it re-opens the tag's query, the
45
+ * way the malformed 6203 tag did (`clickurl` truncated, every following parameter duplicated).
46
+ */
47
+ const hostValue = (value) => (value ? encodeURIComponent(value) : '');
48
+ const gdprConsent = ({ params }) => hostValue(params?.tcString);
49
+ /**
50
+ * Macro name (lower-cased) → the value this SDK sends for it.
51
+ *
52
+ * `clickurl` / `clickmacro` take the host's click tracker, which the ad server PREPENDS to its own
53
+ * redirect — so the viewer's click passes host tracker → vendor redirect → landing page. The SDK
54
+ * does not fire that tracker itself; it would count the click twice. Without one it is empty, the
55
+ * documented "no player tracker" value. VAST click beacons are reported separately by
56
+ * `useSidebarVastTracker`.
57
+ */
58
+ const MACRO_VALUES = {
59
+ // One family, one value: every name here means "make this request unique so nothing caches it".
60
+ // Epoch milliseconds satisfies that for all of them, and keeps the value already shipped for
61
+ // `timestamp` unchanged. (IAB defines `[CACHEBUSTING]` as a random 8-digit number and
62
+ // `[TIMESTAMP]` as ISO-8601, but those describe VAST *tracking* URLs; this substitution only
63
+ // ever touches the ad REQUEST url, where every ad server documents the parameter as a number.)
64
+ timestamp: ({ now }) => String(now),
65
+ cachebuster: ({ now }) => String(now),
66
+ cachebusting: ({ now }) => String(now),
67
+ clickmacro: ({ params }) => hostValue(params?.clickUrl),
68
+ clickurl: ({ params }) => hostValue(params?.clickUrl),
69
+ // `1` / `0` only from an explicit boolean — an absent value is unknown, not "does not apply".
70
+ gdpr: ({ params }) => (params?.gdprApplies === undefined ? '' : params.gdprApplies ? '1' : '0'),
71
+ gdpr_consent: gdprConsent,
72
+ };
73
+ /** `GDPR_CONSENT_<gvlid>`: the vendor id names whose tag this is; the value is the same TC string. */
74
+ const GDPR_CONSENT_FOR_VENDOR = /^gdpr_consent_\d+$/;
75
+ const resolveMacro = (name) => MACRO_VALUES[name] ?? (GDPR_CONSENT_FOR_VENDOR.test(name) ? gdprConsent : undefined);
76
+ /**
77
+ * Fill the macros this SDK knows in an ad-tag URL. One timestamp per call, so a tag naming the macro
78
+ * twice stays self-consistent. `params` is read as given — the caller passes the host's CURRENT
79
+ * values at request time.
80
+ */
81
+ export const substituteAdTagMacros = (url, params) => {
82
+ const now = Date.now();
83
+ return url.replace(MACRO_PATTERN, (match, bracketed, braced) => {
84
+ const value = resolveMacro((bracketed ?? braced ?? '').toLowerCase());
85
+ return value ? value({ now, params }) : match;
86
+ });
87
+ };
@@ -1,3 +1,4 @@
1
+ import type { AdTagParams } from '@streamlayer/sdk-web-core';
1
2
  import { StreamLayerContext } from '@streamlayer/sdk-web-interfaces';
2
3
  import { VastAd, VastCompanionAd, VastCreativeCompanion, VastCreativeLinear, VastCreativeNonLinear, VastNonLinearAd, VastResponse } from '@dailymotion/vast-client';
3
4
  import '@streamlayer/sdk-web-api';
@@ -35,6 +36,23 @@ export type AdData = {
35
36
  companion?: AdCompanion;
36
37
  /** The 300x75 `<Companion>` — the sponsor logo. Only set for a companion pair. */
37
38
  logo?: AdCompanion;
39
+ /**
40
+ * The iOS `SLRVASTSlotRole.cardLogo` pick: companion-only, aspect-scored toward 1:1, and minus
41
+ * whatever the hero slot already claimed. Consumed **only** by the hero-card placements
42
+ * (Side-by-Side / Picture in Picture / Sponsor Frame) as a fallback for an otherwise empty
43
+ * header logo. `logo` is the SC-335 display rule and is unchanged; when it fires, this stays
44
+ * `undefined` because the display rule already won the slot.
45
+ */
46
+ cardLogo?: AdCompanion;
47
+ /**
48
+ * The tag's ONLY `<Companion>`, when it is exactly 300x75 and the hero is a Linear or a
49
+ * NonLinear — i.e. the companion is logo-sized and nothing else needs it. The pair rule behind
50
+ * `logo` exists to tell the logo from the media among companions; with the media coming from
51
+ * elsewhere there is nothing to tell apart, and iOS fills its logo from a single companion.
52
+ * Consumed only by the Standard Ad Sidebar (`createActiveAdFromVast`) as a header-logo fallback;
53
+ * a companion-only tag leaves it `undefined`, because there the companion IS the media.
54
+ */
55
+ loneLogo?: AdCompanion;
38
56
  /** Banner-sized `<Companion>` (468x60 class) — the sidebar media for companion-pair tags. */
39
57
  mediaCompanion?: AdCompanion;
40
58
  /** NonLinear (image-only) creative — present when the tag has no `<Linear>` media. */
@@ -55,4 +73,55 @@ export type AdStore = {
55
73
  muted: boolean;
56
74
  nonceManager?: PalNonceManager;
57
75
  };
76
+ /**
77
+ * Split a `<CompanionAds>` creative's variations into the logo and media slots.
78
+ *
79
+ * The pair mapping activates only for a tag that ships an exact 300x75 companion next to at
80
+ * least one other. Deliberately conservative pending broader product sizes: on any other tag
81
+ * no companion is promoted to the logo slot and the first one stays the media/strip candidate.
82
+ *
83
+ * Both halves of that gate are load-bearing and neither is an aspect rule: a 320x80 companion is
84
+ * the same 4:1 shape as 300x75 and is still rejected, and a lone 300x75 companion is rejected too
85
+ * (the Standard Ad Sidebar recovers that one as `AdData.loneLogo` when a Linear / NonLinear is
86
+ * the media, so it is not needed here).
87
+ * This is **narrower than iOS**, which has no size rule at all and fills its logo slot from a
88
+ * single companion — see `docs/wiki/programmatic-ads-display-ios-parity.md`. Exported for
89
+ * `external-ad.spec.ts` only; `src/index.ts` re-exports names explicitly, so this stays out of the
90
+ * package's public API.
91
+ */
92
+ export declare const selectCompanions: (companions: AdCompanion[]) => {
93
+ logo: undefined;
94
+ media: AdCompanion;
95
+ } | {
96
+ logo: AdCompanion;
97
+ media: AdCompanion | undefined;
98
+ };
99
+ /**
100
+ * Map a parsed VAST response into our `AdData` shape. Shared by both acquisition
101
+ * paths — the GAM/VMAP tag flow and the Prebid auction flow — so the two always
102
+ * produce identical downstream data.
103
+ *
104
+ * Returns `undefined` for a no-ad, which is two distinct shapes: a response with no ad carrying
105
+ * creatives, and an ad whose creatives carry no RENDERABLE asset — no linear `<MediaFile>`, no
106
+ * `<NonLinear>` still and no `<Companion>` image. Both callers `filter(Boolean)` the result, so a
107
+ * no-ad leaves `ads: []` and the card keeps whatever the server configured for it.
108
+ *
109
+ * Exported for `vast-mapping.spec.ts`, which drives it over the same fixture corpus the iOS suite
110
+ * uses. `src/index.ts` re-exports names explicitly, so this stays out of the package's public API.
111
+ */
112
+ export declare const buildAdData: (parsedVAST: VastResponse, order: number) => AdData | undefined;
113
+ /**
114
+ * Build the OpenRTB request body for a single video auction. Placement config is
115
+ * carried on the Prebid link's query string so the consumer-facing interface stays a
116
+ * single URL string:
117
+ * - `configId` → `imp.ext.prebid.storedrequest.id` (production stored request).
118
+ * - `storedauctionresponse` → `imp.ext.prebid.storedauctionresponse.id` (staging mock).
119
+ * - `test=1` → top-level `test` flag.
120
+ * - `w` / `h` → video dimensions (default 1920×1080).
121
+ *
122
+ * The host's GDPR consent (`AdTagParams`) rides the OpenRTB 2.5 extension fields Prebid Server
123
+ * reads — `regs.ext.gdpr` and `user.ext.consent` — the same signal the `${GDPR}` /
124
+ * `${GDPR_CONSENT}` macros carry for a VAST tag. Each is sent only when the host passed it.
125
+ */
126
+ export declare const buildAuctionRequest: (url: URL, adTagParams?: AdTagParams) => Record<string, unknown>;
58
127
  export declare const externalAd: (instance: StreamLayerContext) => (gamUrl: string) => import("@nanostores/query").FetcherStore<AdStore | undefined, any>;