@streamlayer/feature-advertisement 1.1.1-rc.1350 → 1.1.1-rc.3.pda7fd93b

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
@@ -7,6 +7,7 @@ import { SsaiCreativeCache } from './units/ssai-creative.js';
7
7
  import { resolveProvider } from './units/ssai-providers.js';
8
8
  import { createSsaiDetector, } from './units/ssai-detector.js';
9
9
  import { createSsaiAdBreakStore } from './units/ssai-ad-break.js';
10
+ import { ssaiq } from './units/ssai-log.js';
10
11
  import { preloadCreativeAssets } from './units/ssai-preload.js';
11
12
  export class Advertisement extends AbstractFeatureUnit {
12
13
  getExternalAd;
@@ -121,10 +122,25 @@ export class Advertisement extends AbstractFeatureUnit {
121
122
  const provider = resolveProvider(options.provider);
122
123
  const cache = new SsaiCreativeCache(provider);
123
124
  this.ssaiCache = cache;
125
+ /**
126
+ * Every beacon the SDK fires gets a line, with the URL it was fired at.
127
+ * The narrative log is read as a live trace of one ad, and "we called the
128
+ * ad server's own tracking URL" is as much of the story as the request
129
+ * that fetched the creative in the first place.
130
+ */
131
+ const logBeacon = (creativeId, event) => {
132
+ const url = cache.peek(creativeId)?.trackingEvents?.find((entry) => entry.event === event)?.url;
133
+ ssaiq.info({ creativeId, event, url }, `[beacon] ${event} fired — ${creativeId}`);
134
+ };
124
135
  this.fireSsaiImpression = (creativeId) => {
136
+ // The UI only calls this once the bar is actually rendered, so this is
137
+ // the moment the viewer saw the ad — not the moment it resolved.
138
+ ssaiq.info({ creativeId }, `[ad] ad on screen — ${cache.peek(creativeId)?.advertiser || creativeId}`);
139
+ logBeacon(creativeId, 'impression');
125
140
  cache.fireImpressionById(creativeId);
126
141
  };
127
142
  this.fireSsaiTracking = (creativeId, event) => {
143
+ logBeacon(creativeId, event);
128
144
  cache.fireTrackingById(creativeId, event);
129
145
  };
130
146
  this.fireSsaiCompanionClick = (creativeId) => {
@@ -144,12 +160,14 @@ export class Advertisement extends AbstractFeatureUnit {
144
160
  const prefetchCreative = (creativeId) => {
145
161
  cache
146
162
  .resolve(creativeId)
147
- .then((creative) => preloadCreativeAssets(creative))
148
- .then(() => {
163
+ .then((creative) => preloadCreativeAssets(creative).then(() => creative))
164
+ .then((creative) => {
149
165
  preloadedIds.add(creativeId);
166
+ ssaiq.info({ creativeId }, `[ad] creative ready — ${creative.advertiser} (artwork preloaded)`);
150
167
  })
151
- .catch((error) => {
152
- this.log.debug(error, 'ssai creative prefetch failed');
168
+ .catch(() => {
169
+ // Already on `ssaiq` from the provider; the break may still be
170
+ // minutes away, and `loadCreative` retries when it actually starts.
153
171
  });
154
172
  };
155
173
  const publishReady = (range, creative) => {
@@ -214,11 +232,9 @@ export class Advertisement extends AbstractFeatureUnit {
214
232
  videoRef: options.videoRef,
215
233
  manifestUrl: options.manifestUrl,
216
234
  onAdDiscovered: (creativeId) => {
217
- this.log.debug({ creativeId }, 'ssai ad discovered, prefetching creative');
218
235
  prefetchCreative(creativeId);
219
236
  },
220
237
  onBreakStart: (range) => {
221
- this.log.debug({ creativeId: range.creativeId }, 'ssai break start');
222
238
  eventBus.emit('ssaiAdBreak', {
223
239
  action: 'breakStart',
224
240
  payload: { creativeId: range.creativeId },
@@ -226,7 +242,6 @@ export class Advertisement extends AbstractFeatureUnit {
226
242
  loadCreative(range);
227
243
  },
228
244
  onCreativeChange: (range) => {
229
- this.log.debug({ creativeId: range.creativeId }, 'ssai creative change');
230
245
  loadCreative(range);
231
246
  },
232
247
  onAdEnding: (range) => {
@@ -239,11 +254,11 @@ export class Advertisement extends AbstractFeatureUnit {
239
254
  this.ssaiAdBreak.set({ ...current, status: 'ending' });
240
255
  },
241
256
  onProgress: (range, event) => {
242
- this.log.debug({ creativeId: range.creativeId, event }, 'ssai progress');
257
+ // Logged by the detector as `progress:<event>` at the moment playback
258
+ // crosses the marker; nothing to add here.
243
259
  cache.fireTrackingById(range.creativeId, event);
244
260
  },
245
261
  onBreakEnd: () => {
246
- this.log.debug('ssai break end');
247
262
  const current = this.ssaiAdBreak.get();
248
263
  eventBus.emit('ssaiAdBreak', {
249
264
  action: 'breakEnd',
@@ -9,6 +9,7 @@ var ssaiCreative = require('./units/ssai-creative.js');
9
9
  var ssaiProviders = require('./units/ssai-providers.js');
10
10
  var ssaiDetector = require('./units/ssai-detector.js');
11
11
  var ssaiAdBreak = require('./units/ssai-ad-break.js');
12
+ var ssaiLog = require('./units/ssai-log.js');
12
13
  var ssaiPreload = require('./units/ssai-preload.js');
13
14
 
14
15
  class Advertisement extends sdkWebInterfaces.AbstractFeatureUnit {
@@ -124,10 +125,25 @@ class Advertisement extends sdkWebInterfaces.AbstractFeatureUnit {
124
125
  const provider = ssaiProviders.resolveProvider(options.provider);
125
126
  const cache = new ssaiCreative.SsaiCreativeCache(provider);
126
127
  this.ssaiCache = cache;
128
+ /**
129
+ * Every beacon the SDK fires gets a line, with the URL it was fired at.
130
+ * The narrative log is read as a live trace of one ad, and "we called the
131
+ * ad server's own tracking URL" is as much of the story as the request
132
+ * that fetched the creative in the first place.
133
+ */
134
+ const logBeacon = (creativeId, event) => {
135
+ const url = cache.peek(creativeId)?.trackingEvents?.find((entry) => entry.event === event)?.url;
136
+ ssaiLog.ssaiq.info({ creativeId, event, url }, `[beacon] ${event} fired — ${creativeId}`);
137
+ };
127
138
  this.fireSsaiImpression = (creativeId) => {
139
+ // The UI only calls this once the bar is actually rendered, so this is
140
+ // the moment the viewer saw the ad — not the moment it resolved.
141
+ ssaiLog.ssaiq.info({ creativeId }, `[ad] ad on screen — ${cache.peek(creativeId)?.advertiser || creativeId}`);
142
+ logBeacon(creativeId, 'impression');
128
143
  cache.fireImpressionById(creativeId);
129
144
  };
130
145
  this.fireSsaiTracking = (creativeId, event) => {
146
+ logBeacon(creativeId, event);
131
147
  cache.fireTrackingById(creativeId, event);
132
148
  };
133
149
  this.fireSsaiCompanionClick = (creativeId) => {
@@ -147,12 +163,14 @@ class Advertisement extends sdkWebInterfaces.AbstractFeatureUnit {
147
163
  const prefetchCreative = (creativeId) => {
148
164
  cache
149
165
  .resolve(creativeId)
150
- .then((creative) => ssaiPreload.preloadCreativeAssets(creative))
151
- .then(() => {
166
+ .then((creative) => ssaiPreload.preloadCreativeAssets(creative).then(() => creative))
167
+ .then((creative) => {
152
168
  preloadedIds.add(creativeId);
169
+ ssaiLog.ssaiq.info({ creativeId }, `[ad] creative ready — ${creative.advertiser} (artwork preloaded)`);
153
170
  })
154
- .catch((error) => {
155
- this.log.debug(error, 'ssai creative prefetch failed');
171
+ .catch(() => {
172
+ // Already on `ssaiq` from the provider; the break may still be
173
+ // minutes away, and `loadCreative` retries when it actually starts.
156
174
  });
157
175
  };
158
176
  const publishReady = (range, creative) => {
@@ -217,11 +235,9 @@ class Advertisement extends sdkWebInterfaces.AbstractFeatureUnit {
217
235
  videoRef: options.videoRef,
218
236
  manifestUrl: options.manifestUrl,
219
237
  onAdDiscovered: (creativeId) => {
220
- this.log.debug({ creativeId }, 'ssai ad discovered, prefetching creative');
221
238
  prefetchCreative(creativeId);
222
239
  },
223
240
  onBreakStart: (range) => {
224
- this.log.debug({ creativeId: range.creativeId }, 'ssai break start');
225
241
  sdkWebInterfaces.eventBus.emit('ssaiAdBreak', {
226
242
  action: 'breakStart',
227
243
  payload: { creativeId: range.creativeId },
@@ -229,7 +245,6 @@ class Advertisement extends sdkWebInterfaces.AbstractFeatureUnit {
229
245
  loadCreative(range);
230
246
  },
231
247
  onCreativeChange: (range) => {
232
- this.log.debug({ creativeId: range.creativeId }, 'ssai creative change');
233
248
  loadCreative(range);
234
249
  },
235
250
  onAdEnding: (range) => {
@@ -242,11 +257,11 @@ class Advertisement extends sdkWebInterfaces.AbstractFeatureUnit {
242
257
  this.ssaiAdBreak.set({ ...current, status: 'ending' });
243
258
  },
244
259
  onProgress: (range, event) => {
245
- this.log.debug({ creativeId: range.creativeId, event }, 'ssai progress');
260
+ // Logged by the detector as `progress:<event>` at the moment playback
261
+ // crosses the marker; nothing to add here.
246
262
  cache.fireTrackingById(range.creativeId, event);
247
263
  },
248
264
  onBreakEnd: () => {
249
- this.log.debug('ssai break end');
250
265
  const current = this.ssaiAdBreak.get();
251
266
  sdkWebInterfaces.eventBus.emit('ssaiAdBreak', {
252
267
  action: 'breakEnd',
@@ -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/cjs/index.js CHANGED
@@ -3,6 +3,7 @@
3
3
  var advertisement$1 = require('./advertisement.js');
4
4
  var externalPauseAd = require('./units/external-pause-ad.js');
5
5
  var pauseAdCard = require('./units/pause-ad-card.js');
6
+ var sponsorLogo = require('./units/sponsor-logo.js');
6
7
  var gam = require('./units/gam.js');
7
8
  var vastTracker = require('./units/vast-tracker.js');
8
9
  var ssaiProviders = require('./units/ssai-providers.js');
@@ -21,6 +22,7 @@ const advertisement = (instance, _opts, done) => {
21
22
 
22
23
  exports.isFullbleedVideoTemplate = externalPauseAd.isFullbleedVideoTemplate;
23
24
  exports.INSIGHT_BODY_MAX_LENGTH = pauseAdCard.INSIGHT_BODY_MAX_LENGTH;
25
+ exports.selectSponsorLogo = sponsorLogo.selectSponsorLogo;
24
26
  exports.gam = gam.gam;
25
27
  exports.createCompanionTracker = vastTracker.createCompanionTracker;
26
28
  exports.createVastTracker = vastTracker.createVastTracker;
@@ -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,91 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Ad-tag macro substitution, for every ad server — not just GAM.
5
+ *
6
+ * Studio stores a programmatic tag verbatim (`gamRawUrl`, returned untouched by
7
+ * `processGamAdvertisement`), so whatever macros the ad server's own tag builder emitted reach the
8
+ * SDK literal. `gam()` used to fill only the GAM `[placeholder]` token, and only for
9
+ * `pubads.g.doubleclick.net` — every other vendor's tag went out with its macros unexpanded.
10
+ *
11
+ * That is not inert. Measured against the ADITION tag on event 11089
12
+ * (`…/banner?…&ts=[timestamp]&clickurl=[clickmacro]`): ADITION prepends the value of `clickurl` to
13
+ * its own redi URL, so a literal `[clickmacro]` came back inside `<CompanionClickThrough>` as
14
+ * `[clickmacro]https%3A%2F%2Fad1…` — a broken click-through. Sending `clickurl=` empty answers with
15
+ * a valid redi URL.
16
+ *
17
+ * **Two sources.** The cache buster is the SDK's own. The consent family — `${GDPR}` and
18
+ * `${GDPR_CONSENT_<gvlid>}`, IAB TCF, where the id is the vendor's Global Vendor List entry (39 is
19
+ * ADITION / Virtual Minds) — and the click macro come from the host app (`AdTagParams`, set through
20
+ * the `adTagParams` option or `sdk.setAdTagParams`). The SDK never reads a CMP and never guesses
21
+ * consent: a value the host did not pass goes out EMPTY, which ad servers read as "unknown" — never
22
+ * as `gdpr=0`. Empty rather than literal because a literal is not inert either: ADITION copied
23
+ * `${GDPR}` into its own redirect URL (`gdpr%3D%24%7BGDPR%7D`). See
24
+ * `docs/wiki/programmatic-ads-macros.md`.
25
+ *
26
+ * An unknown macro is left exactly as configured, which is what keeps the GAM `[placeholder]`
27
+ * tokens for the PAL path (`gam.ts` fills those per parameter, not per token).
28
+ */
29
+ /**
30
+ * All three spellings in the wild — `[NAME]` (IAB / SpotX), `${NAME}` (Google) and bare `{NAME}` —
31
+ * each with its delimiters either literal or PERCENT-ENCODED.
32
+ *
33
+ * Neither half of that is theoretical. ADITION emitted `[timestamp]` / `[clickmacro]` for the
34
+ * campaign on event 11089 and `{CACHEBUSTER}` / `{GDPR}` for the one on 6203 — same ad server, same
35
+ * advertiser, different tag builder. And Studio stores the 6203 tag as `ts=%7BCACHEBUSTER%7D`, so
36
+ * matching only literal braces made the whole substitution a silent no-op on it: an unexpanded
37
+ * macro raises no error and the ad server still answers 200. `searchParams.get()` decodes on read,
38
+ * which is what hides the encoding from anyone inspecting the parsed URL.
39
+ *
40
+ * The `$` is optional rather than a fourth branch — an unowned `${NAME}` still returns its full
41
+ * matched text, `$` included, so the brace forms cannot corrupt each other.
42
+ */
43
+ const MACRO_PATTERN = /(?:\[|%5B)([a-z0-9_]+)(?:\]|%5D)|(?:\$|%24)?(?:\{|%7B)([a-z0-9_]+)(?:\}|%7D)/gi;
44
+ /**
45
+ * Every host value lands inside a query-string VALUE, so it is encoded exactly once. That matters
46
+ * most for the click URL, which carries its own `?`/`&`: sent raw it re-opens the tag's query, the
47
+ * way the malformed 6203 tag did (`clickurl` truncated, every following parameter duplicated).
48
+ */
49
+ const hostValue = (value) => (value ? encodeURIComponent(value) : '');
50
+ const gdprConsent = ({ params }) => hostValue(params?.tcString);
51
+ /**
52
+ * Macro name (lower-cased) → the value this SDK sends for it.
53
+ *
54
+ * `clickurl` / `clickmacro` take the host's click tracker, which the ad server PREPENDS to its own
55
+ * redirect — so the viewer's click passes host tracker → vendor redirect → landing page. The SDK
56
+ * does not fire that tracker itself; it would count the click twice. Without one it is empty, the
57
+ * documented "no player tracker" value. VAST click beacons are reported separately by
58
+ * `useSidebarVastTracker`.
59
+ */
60
+ const MACRO_VALUES = {
61
+ // One family, one value: every name here means "make this request unique so nothing caches it".
62
+ // Epoch milliseconds satisfies that for all of them, and keeps the value already shipped for
63
+ // `timestamp` unchanged. (IAB defines `[CACHEBUSTING]` as a random 8-digit number and
64
+ // `[TIMESTAMP]` as ISO-8601, but those describe VAST *tracking* URLs; this substitution only
65
+ // ever touches the ad REQUEST url, where every ad server documents the parameter as a number.)
66
+ timestamp: ({ now }) => String(now),
67
+ cachebuster: ({ now }) => String(now),
68
+ cachebusting: ({ now }) => String(now),
69
+ clickmacro: ({ params }) => hostValue(params?.clickUrl),
70
+ clickurl: ({ params }) => hostValue(params?.clickUrl),
71
+ // `1` / `0` only from an explicit boolean — an absent value is unknown, not "does not apply".
72
+ gdpr: ({ params }) => (params?.gdprApplies === undefined ? '' : params.gdprApplies ? '1' : '0'),
73
+ gdpr_consent: gdprConsent,
74
+ };
75
+ /** `GDPR_CONSENT_<gvlid>`: the vendor id names whose tag this is; the value is the same TC string. */
76
+ const GDPR_CONSENT_FOR_VENDOR = /^gdpr_consent_\d+$/;
77
+ const resolveMacro = (name) => MACRO_VALUES[name] ?? (GDPR_CONSENT_FOR_VENDOR.test(name) ? gdprConsent : undefined);
78
+ /**
79
+ * Fill the macros this SDK knows in an ad-tag URL. One timestamp per call, so a tag naming the macro
80
+ * twice stays self-consistent. `params` is read as given — the caller passes the host's CURRENT
81
+ * values at request time.
82
+ */
83
+ const substituteAdTagMacros = (url, params) => {
84
+ const now = Date.now();
85
+ return url.replace(MACRO_PATTERN, (match, bracketed, braced) => {
86
+ const value = resolveMacro((bracketed ?? braced ?? '').toLowerCase());
87
+ return value ? value({ now, params }) : match;
88
+ });
89
+ };
90
+
91
+ exports.substituteAdTagMacros = substituteAdTagMacros;
@@ -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", { with: { "resolution-mode": "import" } }).FetcherStore<AdStore | undefined, any>;
@@ -6,6 +6,7 @@ require('@streamlayer/sdk-web-api');
6
6
  var VMAP = require('vmap');
7
7
  var gam = require('./gam.js');
8
8
  var parseVastExtensions = require('./parse-vast-extensions.js');
9
+ var sponsorLogo = require('./sponsor-logo.js');
9
10
 
10
11
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
11
12
 
@@ -45,7 +46,7 @@ const withTimeout = (promise, ms) => {
45
46
  * Get the first valid creative from an ad
46
47
  */
47
48
  const getCreative = (ad) => {
48
- return ad.creatives.find((creative) => creative.mediaFiles);
49
+ return ad.creatives.find((creative) => getMediaFileUrl(creative));
49
50
  };
50
51
  /**
51
52
  * Get the URL of the first media file in the creative
@@ -66,31 +67,10 @@ const getClickThrough = (creative) => {
66
67
  // @ts-ignore — vast-client types `videoClickThroughURLTemplate` loosely
67
68
  return creative?.videoClickThroughURLTemplate?.url || undefined;
68
69
  };
69
- /**
70
- * Extract every `<Companion>` variation of the first `<CompanionAds>` creative that carries a
71
- * `<StaticResource>`, in document order. `width`/`height` are XML attributes (strings at
72
- * runtime), normalised to numbers so the size mapping can compare them.
73
- */
74
- const getCompanions = (ad) => {
75
- const creative = ad.creatives.find((item) => item.type === 'companion');
76
- return (creative?.variations || []).reduce((acc, variation) => {
77
- const imageUrl = variation?.staticResources?.[0]?.url;
78
- if (!imageUrl) {
79
- return acc;
80
- }
81
- return acc.concat({
82
- imageUrl,
83
- clickUrl: variation.companionClickThroughURLTemplate ?? undefined,
84
- width: Number(variation.width) || undefined,
85
- height: Number(variation.height) || undefined,
86
- creative,
87
- variation,
88
- });
89
- }, []);
90
- };
91
70
  /** Product companion mapping: the 300x75 companion is the logo, the banner-sized one the media. */
92
71
  const LOGO_COMPANION_WIDTH = 300;
93
72
  const LOGO_COMPANION_HEIGHT = 75;
73
+ const isLogoSized = (companion) => companion?.width === LOGO_COMPANION_WIDTH && companion?.height === LOGO_COMPANION_HEIGHT;
94
74
  const companionArea = (companion) => (companion.width || 0) * (companion.height || 0);
95
75
  /**
96
76
  * Split a `<CompanionAds>` creative's variations into the logo and media slots.
@@ -98,11 +78,18 @@ const companionArea = (companion) => (companion.width || 0) * (companion.height
98
78
  * The pair mapping activates only for a tag that ships an exact 300x75 companion next to at
99
79
  * least one other. Deliberately conservative pending broader product sizes: on any other tag
100
80
  * no companion is promoted to the logo slot and the first one stays the media/strip candidate.
81
+ *
82
+ * Both halves of that gate are load-bearing and neither is an aspect rule: a 320x80 companion is
83
+ * the same 4:1 shape as 300x75 and is still rejected, and a lone 300x75 companion is rejected too
84
+ * (the Standard Ad Sidebar recovers that one as `AdData.loneLogo` when a Linear / NonLinear is
85
+ * the media, so it is not needed here).
86
+ * This is **narrower than iOS**, which has no size rule at all and fills its logo slot from a
87
+ * single companion — see `docs/wiki/programmatic-ads-display-ios-parity.md`. Exported for
88
+ * `external-ad.spec.ts` only; `src/index.ts` re-exports names explicitly, so this stays out of the
89
+ * package's public API.
101
90
  */
102
91
  const selectCompanions = (companions) => {
103
- const logo = companions.length > 1
104
- ? companions.find((companion) => companion.width === LOGO_COMPANION_WIDTH && companion.height === LOGO_COMPANION_HEIGHT)
105
- : undefined;
92
+ const logo = companions.length > 1 ? companions.find((companion) => isLogoSized(companion)) : undefined;
106
93
  if (!logo) {
107
94
  return { logo: undefined, media: companions[0] };
108
95
  }
@@ -112,40 +99,26 @@ const selectCompanions = (companions) => {
112
99
  .pop();
113
100
  return { logo, media };
114
101
  };
115
- /**
116
- * Extract the first `<NonLinearAds>` creative's static-image variation. Both the creative (which
117
- * owns the `<TrackingEvents>`) and the variation (which owns the click-through) are kept —
118
- * `VASTTracker` needs both.
119
- *
120
- * `@dailymotion/vast-client` 6.4.2 emits `nonlinearClickThroughURLTemplate` (lower-case `l`),
121
- * which the `@types/vast-client` shim declares as `nonLinearClickThroughURLTemplate` — the
122
- * runtime spelling is authoritative.
123
- */
124
- const getNonLinear = (ad) => {
125
- const creative = ad.creatives.find((item) => item.type === 'nonlinear');
126
- const variation = creative?.variations?.[0];
127
- const imageUrl = variation?.staticResource;
128
- if (!creative || !variation || !imageUrl) {
129
- return undefined;
130
- }
131
- return {
132
- imageUrl,
133
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
134
- // @ts-ignore — see the runtime-vs-shim spelling note above
135
- clickUrl: variation.nonlinearClickThroughURLTemplate ?? undefined,
136
- width: Number(variation.width) || undefined,
137
- height: Number(variation.height) || undefined,
138
- creative,
139
- variation,
140
- };
141
- };
142
102
  /**
143
103
  * Map a parsed VAST response into our `AdData` shape. Shared by both acquisition
144
104
  * paths — the GAM/VMAP tag flow and the Prebid auction flow — so the two always
145
- * produce identical downstream data. Returns `undefined` when the response has no
146
- * ad carrying creatives.
105
+ * produce identical downstream data.
106
+ *
107
+ * Returns `undefined` for a no-ad, which is two distinct shapes: a response with no ad carrying
108
+ * creatives, and an ad whose creatives carry no RENDERABLE asset — no linear `<MediaFile>`, no
109
+ * `<NonLinear>` still and no `<Companion>` image. Both callers `filter(Boolean)` the result, so a
110
+ * no-ad leaves `ads: []` and the card keeps whatever the server configured for it.
111
+ *
112
+ * Exported for `vast-mapping.spec.ts`, which drives it over the same fixture corpus the iOS suite
113
+ * uses. `src/index.ts` re-exports names explicitly, so this stays out of the package's public API.
147
114
  */
148
115
  const buildAdData = (parsedVAST, order) => {
116
+ // Defensive, and unreachable through either production path: `@dailymotion/vast-client` discards
117
+ // an ad whose creatives all fail to parse (empty `<Linear>`, empty/missing `<Creatives>`, unknown
118
+ // creative element) before `parseVAST` resolves, so it never yields an Ad with zero creatives.
119
+ // Kept because it costs nothing and `buildAdData` is also reachable from tests with a hand-built
120
+ // response — see the `invalid_ad` case in `vast-mapping.spec.ts`, which pins the parser contract
121
+ // this relies on rather than pretending to exercise the guard.
149
122
  const validAd = parsedVAST.ads.find((ads) => ads.creatives.length > 0);
150
123
  if (!validAd) {
151
124
  log.warn({ order, ads: parsedVAST.ads.length }, 'vast response carries no ad with creatives — treated as no-ad');
@@ -157,10 +130,47 @@ const buildAdData = (parsedVAST, order) => {
157
130
  const parsedExtension = parseVastExtensions.parseVastExtension(extension);
158
131
  const creative = getCreative(validAd);
159
132
  const mediaFileUrl = getMediaFileUrl(creative);
160
- const companions = getCompanions(validAd);
133
+ const images = sponsorLogo.collectAssets(validAd);
134
+ const companions = images.filter((asset) => asset.origin === 'companion');
135
+ const nonLinear = images.find((asset) => asset.origin === 'nonlinear');
136
+ // An ad the parser KEPT whose creatives carry nothing any shell can draw is a no-ad, exactly
137
+ // like a response with no ad at all. vast-client only discards a creative it cannot parse; a
138
+ // `<NonLinear>` or `<Companion>` whose `<StaticResource>` body is empty parses fine and just
139
+ // yields no url, so the guard above cannot catch it. Without this the fabricated promotion still
140
+ // took the slot and drew the ad's TEXT — title and description over an empty media box — which
141
+ // is a billable impression for a creative that does not exist.
142
+ //
143
+ // "Renderable" is the union of the three slots the shells can fill: a linear `<MediaFile>`, a
144
+ // `<NonLinear>` still, or a `<Companion>` image. `collectAssets` already drops the imageless
145
+ // ones, so a non-empty `companions` means at least one drawable image. A tag carrying only a
146
+ // `<Mezzanine>` never reaches here — the parser drops that Linear itself — but it lands in the
147
+ // same place either way.
148
+ if (!mediaFileUrl && !nonLinear && companions.length === 0) {
149
+ log.warn({ order, adId: validAd.id, creatives: validAd.creatives.length }, 'vast ad carries no renderable asset (no media file, no nonlinear, no companion) — treated as no-ad');
150
+ return undefined;
151
+ }
161
152
  const { logo, media: mediaCompanion } = selectCompanions(companions);
162
- const nonLinear = getNonLinear(validAd);
163
153
  const clickThrough = getClickThrough(creative) || nonLinear?.clickUrl || mediaCompanion?.clickUrl;
154
+ // Mirror iOS's greedy `pool.select([hero, .cardLogo])`: the hero claims first and a claimed
155
+ // asset is gone for later roles. Web's hero precedence (`createActiveAdFromVast`) is linear
156
+ // video → pair media companion (unreachable here: a `logo` short-circuits the whole selection)
157
+ // → NonLinear still → lone companion, so a companion is only unavailable to the logo slot when
158
+ // it IS the hero, i.e. when the tag ships neither. `selectCardLogo` already filters to companion
159
+ // origins, so the NonLinear never needs excluding.
160
+ //
161
+ // Excluding `companions[0]` (which is what `mediaCompanion` is when there is no `logo`) rather
162
+ // than the largest-area asset is a deliberate difference from iOS's `areaDominant` hero: web's
163
+ // content slot draws the FIRST companion in document order, so document order is the correct
164
+ // criterion for "what the hero already claimed" on this platform.
165
+ const companionIsHero = !mediaFileUrl && !nonLinear;
166
+ const cardLogoAsset = logo ? undefined : sponsorLogo.selectCardLogo(validAd, companionIsHero ? [mediaCompanion] : []);
167
+ // Share the selected resource wrapper so drawn-companion identity remains stable.
168
+ const cardLogo = cardLogoAsset
169
+ ? companions.find((item) => item.variation === cardLogoAsset.variation && item.imageUrl === cardLogoAsset.imageUrl)
170
+ : undefined;
171
+ // Same collected instance as `companion` (identity matters to `drawnCompanions`). A lone
172
+ // companion never satisfies the pair rule, so this can only fill a slot `logo` left empty.
173
+ const loneLogo = companions.length === 1 && !companionIsHero && isLogoSized(companions[0]) ? companions[0] : undefined;
164
174
  log.info({
165
175
  order,
166
176
  adId: validAd.id,
@@ -176,6 +186,8 @@ const buildAdData = (parsedVAST, order) => {
176
186
  media: mediaCompanion
177
187
  ? { size: `${mediaCompanion.width}x${mediaCompanion.height}`, url: mediaCompanion.imageUrl }
178
188
  : undefined,
189
+ cardLogo: cardLogo ? { size: `${cardLogo.width}x${cardLogo.height}`, url: cardLogo.imageUrl } : undefined,
190
+ loneLogo: loneLogo ? { size: `${loneLogo.width}x${loneLogo.height}`, url: loneLogo.imageUrl } : undefined,
179
191
  clickThrough: !!clickThrough,
180
192
  },
181
193
  }, 'vast ad parsed');
@@ -185,6 +197,8 @@ const buildAdData = (parsedVAST, order) => {
185
197
  mediaFileUrl,
186
198
  companion: companions[0],
187
199
  logo,
200
+ cardLogo,
201
+ loneLogo,
188
202
  mediaCompanion,
189
203
  nonLinear,
190
204
  clickThrough,
@@ -213,8 +227,12 @@ const createRequestId = () => `sl-web-${Date.now().toString(36)}-${Math.random()
213
227
  * - `storedauctionresponse` → `imp.ext.prebid.storedauctionresponse.id` (staging mock).
214
228
  * - `test=1` → top-level `test` flag.
215
229
  * - `w` / `h` → video dimensions (default 1920×1080).
230
+ *
231
+ * The host's GDPR consent (`AdTagParams`) rides the OpenRTB 2.5 extension fields Prebid Server
232
+ * reads — `regs.ext.gdpr` and `user.ext.consent` — the same signal the `${GDPR}` /
233
+ * `${GDPR_CONSENT}` macros carry for a VAST tag. Each is sent only when the host passed it.
216
234
  */
217
- const buildAuctionRequest = (url) => {
235
+ const buildAuctionRequest = (url, adTagParams) => {
218
236
  const params = url.searchParams;
219
237
  const width = Number(params.get('w')) || 1920;
220
238
  const height = Number(params.get('h')) || 1080;
@@ -252,6 +270,12 @@ const buildAuctionRequest = (url) => {
252
270
  if (params.get('test') === '1') {
253
271
  request.test = 1;
254
272
  }
273
+ if (adTagParams?.gdprApplies !== undefined) {
274
+ request.regs = { ext: { gdpr: adTagParams.gdprApplies ? 1 : 0 } };
275
+ }
276
+ if (adTagParams?.tcString) {
277
+ request.user = { ext: { consent: adTagParams.tcString } };
278
+ }
255
279
  return request;
256
280
  };
257
281
  /**
@@ -259,11 +283,11 @@ const buildAuctionRequest = (url) => {
259
283
  * Every bid's `adm` is inline VAST XML (no URL to fetch); bids are ordered by price
260
284
  * so the winner plays first (RFC §2). A single bad bid must not drop the batch.
261
285
  */
262
- const fetchPrebidAds = async (prebidUrl) => {
286
+ const fetchPrebidAds = async (prebidUrl, adTagParams) => {
263
287
  const response = await fetch(prebidUrl.toString(), {
264
288
  method: 'POST',
265
289
  headers: { 'Content-Type': 'application/json' },
266
- body: JSON.stringify(buildAuctionRequest(prebidUrl)),
290
+ body: JSON.stringify(buildAuctionRequest(prebidUrl, adTagParams)),
267
291
  });
268
292
  if (!response.ok) {
269
293
  throw new Error(`prebid auction failed with status ${response.status}`);
@@ -316,13 +340,17 @@ const externalAd = (instance) => {
316
340
  return undefined;
317
341
  }
318
342
  })();
343
+ // The host's CURRENT macro values, read per request rather than baked into the store: the
344
+ // store is cached by url for the session, and consent routinely arrives after the SDK
345
+ // starts (a CMP answers asynchronously) or changes mid-session.
346
+ const adTagParams = instance.sdk.options.get().adTagParams;
319
347
  // Prebid links hit our Prebid Server auction endpoint instead of a VAST tag.
320
348
  // Run the OpenRTB auction and parse the winning bid's inline VAST (`adm`) into
321
349
  // the same `AdStore` shape the GAM/VMAP path returns. See rfcs/prebid.
322
350
  if (parsedAdUrl && isPrebidUrl(parsedAdUrl)) {
323
- return fetchPrebidAds(parsedAdUrl);
351
+ return fetchPrebidAds(parsedAdUrl, adTagParams);
324
352
  }
325
- const generateUrl = gam.gam(adUrl);
353
+ const generateUrl = gam.gam(adUrl, undefined, adTagParams);
326
354
  const generatedUrl = await generateUrl();
327
355
  const noncedLink = generatedUrl?.url;
328
356
  if (!noncedLink) {
@@ -387,4 +415,7 @@ const externalAd = (instance) => {
387
415
  });
388
416
  };
389
417
 
418
+ exports.buildAdData = buildAdData;
419
+ exports.buildAuctionRequest = buildAuctionRequest;
390
420
  exports.externalAd = externalAd;
421
+ exports.selectCompanions = selectCompanions;