@guardian/commercial-core 4.10.0 → 4.11.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.
@@ -31,3 +31,5 @@ export { getSharedTargeting } from './targeting/shared';
31
31
  export type { ViewportTargeting } from './targeting/viewport';
32
32
  export { getViewportTargeting } from './targeting/viewport';
33
33
  export { pickTargetingValues } from './targeting/pick-targeting-values';
34
+ export { init as initMessenger, type RegisterListener, type RegisterPersistentListener, type RespondProxy, } from './messenger';
35
+ export { postMessage } from './messenger/post-message';
package/dist/cjs/index.js CHANGED
@@ -24,7 +24,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
24
24
  return result;
25
25
  };
26
26
  Object.defineProperty(exports, "__esModule", { value: true });
27
- exports.pickTargetingValues = exports.getViewportTargeting = exports.getSharedTargeting = exports.getSessionTargeting = exports.getPersonalisedTargeting = exports.getContentTargeting = exports.constants = exports.concatSizeMappings = exports.createAdSlot = exports.disabledAds = exports.buildAdsConfigWithConsent = exports.initTrackGpcSignal = exports.initTrackLabsContainer = exports.initTrackScrollDepth = exports.getPermutivePFPSegments = exports.getPermutiveSegments = exports.clearPermutiveSegments = exports.isAdBlockInUse = exports.isBreakpoint = exports.createAdSize = exports.slotSizeMappings = exports.getAdSize = exports.adSizes = exports.initCommercialMetrics = exports.bypassCommercialMetricsSampling = exports.EventTimer = exports.remarketing = exports.inizio = exports.twitter = exports.fbPixel = exports.permutive = exports.ias = void 0;
27
+ exports.postMessage = exports.initMessenger = exports.pickTargetingValues = exports.getViewportTargeting = exports.getSharedTargeting = exports.getSessionTargeting = exports.getPersonalisedTargeting = exports.getContentTargeting = exports.constants = exports.concatSizeMappings = exports.createAdSlot = exports.disabledAds = exports.buildAdsConfigWithConsent = exports.initTrackGpcSignal = exports.initTrackLabsContainer = exports.initTrackScrollDepth = exports.getPermutivePFPSegments = exports.getPermutiveSegments = exports.clearPermutiveSegments = exports.isAdBlockInUse = exports.isBreakpoint = exports.createAdSize = exports.slotSizeMappings = exports.getAdSize = exports.adSizes = exports.initCommercialMetrics = exports.bypassCommercialMetricsSampling = exports.EventTimer = exports.remarketing = exports.inizio = exports.twitter = exports.fbPixel = exports.permutive = exports.ias = void 0;
28
28
  var ias_1 = require("./third-party-tags/ias");
29
29
  Object.defineProperty(exports, "ias", { enumerable: true, get: function () { return ias_1.ias; } });
30
30
  var permutive_1 = require("./third-party-tags/permutive");
@@ -80,3 +80,7 @@ var viewport_1 = require("./targeting/viewport");
80
80
  Object.defineProperty(exports, "getViewportTargeting", { enumerable: true, get: function () { return viewport_1.getViewportTargeting; } });
81
81
  var pick_targeting_values_1 = require("./targeting/pick-targeting-values");
82
82
  Object.defineProperty(exports, "pickTargetingValues", { enumerable: true, get: function () { return pick_targeting_values_1.pickTargetingValues; } });
83
+ var messenger_1 = require("./messenger");
84
+ Object.defineProperty(exports, "initMessenger", { enumerable: true, get: function () { return messenger_1.init; } });
85
+ var post_message_1 = require("./messenger/post-message");
86
+ Object.defineProperty(exports, "postMessage", { enumerable: true, get: function () { return post_message_1.postMessage; } });
@@ -0,0 +1 @@
1
+ export declare const postMessage: (message: Record<string, unknown>, target: MessageEventSource, targetOrigin?: string) => void;
@@ -0,0 +1,7 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.postMessage = void 0;
4
+ const postMessage = (message, target, targetOrigin = '*') => {
5
+ target.postMessage(JSON.stringify(message), { targetOrigin });
6
+ };
7
+ exports.postMessage = postMessage;
@@ -0,0 +1,97 @@
1
+ /**
2
+ * The type of iframe messages we accept
3
+ */
4
+ declare type MessageType = 'background' | 'click' | 'disable-refresh' | 'get-page-targeting' | 'get-page-url' | 'get-styles' | 'measure-ad-load' | 'passback' | 'resize' | 'set-ad-height' | 'scroll' | 'type' | 'viewport';
5
+ /**
6
+ * Callbacks that can be registered to fire when receiving messages from an iframe
7
+ */
8
+ declare type ListenerCallback = (
9
+ /**
10
+ * The data payload sent by an iframe, and has type `unknown` because we can't
11
+ * predict what the iframe will send. It is the responsibility of the callback
12
+ * to obtain a value of the desired type (or fail gracefully)
13
+ */
14
+ specs: unknown | undefined,
15
+ /**
16
+ * Non-persistent callbacks can be chained together. This value is the return
17
+ * value of the previously fired callback in the chain. It is the responsibility
18
+ * of the current callback to either ignore it or use it / pass along
19
+ */
20
+ ret: unknown,
21
+ /**
22
+ * Reference to the iframe that is the source of the message
23
+ */
24
+ iframe?: HTMLIFrameElement) => unknown;
25
+ declare type RespondProxy = (error: {
26
+ message: string;
27
+ } | null, result: unknown) => void;
28
+ /**
29
+ * Persistent callbacks that can be registered to fire when receiving messages from an iframe
30
+ *
31
+ * A persistent callback listener will be passed a RespondProxy function
32
+ *
33
+ * This function allows the listener to call respond (i.e. postMessage the originating iFrame) itself whenever it needs to
34
+ *
35
+ * This is useful for listeners such as viewport or scroll where the values change over time
36
+ */
37
+ declare type PersistentListenerCallback = (respondProxy: RespondProxy, specs: unknown,
38
+ /**
39
+ * Reference to the iframe that is the source of the message
40
+ */
41
+ iframe?: HTMLIFrameElement) => void;
42
+ declare type ListenerOptions = {
43
+ window?: WindowProxy;
44
+ };
45
+ declare type MessengerErrorHandler = (err: Error, features: Record<string, string>) => void;
46
+ /**
47
+ * Types of functions to register a listener for a given type of iframe message
48
+ */
49
+ declare type RegisterListener = (type: MessageType, callback: ListenerCallback, options?: ListenerOptions) => void;
50
+ /**
51
+ * Types of functions to register a persistent listener for a given type of iframe message
52
+ */
53
+ declare type RegisterPersistentListener = (type: MessageType, callback: PersistentListenerCallback, options?: ListenerOptions) => void;
54
+ /**
55
+ * Types of functions to unregister a listener for a given type of iframe message
56
+ *
57
+ */
58
+ declare type UnregisterListener = (type: MessageType, callback?: ListenerCallback, options?: ListenerOptions) => void;
59
+ /**
60
+ * Register a listener for a given type of iframe message
61
+ *
62
+ * @param type The `type` of message to register against
63
+ * @param callback The listener callback to register that will receive messages of the given type
64
+ * @param options Options for the target window
65
+ */
66
+ export declare const register: RegisterListener;
67
+ /**
68
+ * Register a persistent listener for a given type of iframe message
69
+ *
70
+ * @param type The `type` of message to register against
71
+ * @param callback The persistent listener callback to register that will receive messages of the given type
72
+ * @param options Options for the target window and whether the callback is persistent
73
+ */
74
+ export declare const registerPersistentListener: RegisterPersistentListener;
75
+ /**
76
+ * Unregister a callback for a given type
77
+ *
78
+ * @param type The type of message to unregister against. An iframe will send
79
+ * messages annotated with the type
80
+ * @param callback Optionally include the original callback. If this is included
81
+ * for a persistent callback this function will be unregistered. If it's
82
+ * included for a non-persistent callback only the matching callback is removed,
83
+ * otherwise all callbacks for that type will be unregistered
84
+ * @param options Option for the target window
85
+ */
86
+ export declare const unregister: UnregisterListener;
87
+ /**
88
+ * Initialize an array of listener callbacks in a batch
89
+ *
90
+ * @param listeners The listener registration functions
91
+ * @param persistentListeners The persistent listener registration functions
92
+ */
93
+ export declare const init: (listeners: ((register: RegisterListener, errorHandler: MessengerErrorHandler) => void)[], persistentListeners: ((register: RegisterPersistentListener, errorHandler: MessengerErrorHandler) => void)[], errorHandler: MessengerErrorHandler) => void;
94
+ export declare const _: {
95
+ onMessage: (event: MessageEvent) => Promise<void>;
96
+ };
97
+ export type { RespondProxy, RegisterListener, RegisterPersistentListener, UnregisterListener, ListenerOptions, MessengerErrorHandler, };
@@ -0,0 +1,281 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports._ = exports.init = exports.unregister = exports.registerPersistentListener = exports.register = void 0;
4
+ const post_message_1 = require("./messenger/post-message");
5
+ const LISTENERS = {};
6
+ let REGISTERED_LISTENERS = 0;
7
+ let reportError = () => {
8
+ // not set yet
9
+ };
10
+ const error405 = {
11
+ code: 405,
12
+ message: 'Service %% not implemented',
13
+ };
14
+ const error500 = {
15
+ code: 500,
16
+ message: 'Internal server error\n\n%%',
17
+ };
18
+ /**
19
+ * Determine if an unknown payload has the shape of a programmatic message
20
+ *
21
+ * @param payload The unknown message payload
22
+ */
23
+ const isProgrammaticMessage = (payload) => {
24
+ const payloadToCheck = payload;
25
+ return (payloadToCheck.type === 'set-ad-height' &&
26
+ ('id' in payloadToCheck.value || 'slotId' in payloadToCheck.value) &&
27
+ 'height' in payloadToCheck.value);
28
+ };
29
+ /**
30
+ * Convert a legacy programmatic message to a standard message
31
+ *
32
+ * Note that this only applies to specific resize programmatic messages
33
+ * (these include specific width and height values)
34
+ */
35
+ const toStandardMessage = (payload) => ({
36
+ id: 'aaaa0000-bb11-cc22-dd33-eeeeee444444',
37
+ type: 'resize',
38
+ iframeId: payload.value.id,
39
+ slotId: payload.value.slotId,
40
+ value: {
41
+ height: payload.value.height,
42
+ width: payload.value.width,
43
+ },
44
+ });
45
+ /**
46
+ * Retrieve a reference to the calling iFrame
47
+ *
48
+ * Attempts the following strategies to find the correct iframe:
49
+ * - using the slotId from the incoming message
50
+ * - using the iframeId from the incoming message
51
+ * - checking message event.source (i.e. window) against all page level iframe contentWindows
52
+ *
53
+ * Listeners can then use the iFrame to determine the slot making the postMessage call
54
+ */
55
+ const getIframe = (message, messageEventSource) => {
56
+ if (message.slotId) {
57
+ const container = document.getElementById(`dfp-ad--${message.slotId}`);
58
+ return container?.querySelector('iframe') ?? undefined;
59
+ }
60
+ else if (message.iframeId) {
61
+ const el = document.getElementById(message.iframeId);
62
+ return el instanceof HTMLIFrameElement ? el : undefined;
63
+ }
64
+ else if (messageEventSource) {
65
+ const iframes = document.querySelectorAll('iframe');
66
+ return Array.from(iframes).find((iframe) => iframe.contentWindow === messageEventSource);
67
+ }
68
+ };
69
+ // Regex for testing validity of message ids
70
+ const validMessageRegex = /^[a-f0-9]{8}-([a-f0-9]{4}-){3}[a-f0-9]{12}$/;
71
+ /**
72
+ * Narrow an `unknown` payload to the standard message format
73
+ *
74
+ * Until DFP provides a way for us to identify with 100% certainty our
75
+ * in-house creatives, we are left with doing some basic tests
76
+ * such as validating the anatomy of the payload and whitelisting
77
+ * event type
78
+ */
79
+ const isValidPayload = (payload) => {
80
+ const payloadToCheck = payload;
81
+ return ('type' in payloadToCheck &&
82
+ 'value' in payloadToCheck &&
83
+ 'id' in payloadToCheck &&
84
+ payloadToCheck.type in LISTENERS &&
85
+ validMessageRegex.test(payloadToCheck.id));
86
+ };
87
+ /**
88
+ * Cheap string formatting function
89
+ *
90
+ * @param error An object `{ code, message }`. `message` is a string where successive
91
+ * occurrences of %% will be replaced by the following arguments
92
+ * @param args Arguments that will replace %%
93
+ *
94
+ * @example
95
+ * formatError({ message: "%%, you are so %%" }, "Regis", "lovely")
96
+ * => { message: "Regis, you are so lovely" }
97
+ */
98
+ const formatError = (error, ...args) => args.reduce((e, arg) => {
99
+ e.message = e.message.replace('%%', arg);
100
+ return e;
101
+ }, error);
102
+ /**
103
+ * Convert a posted message to our StandardMessage format
104
+ *
105
+ * @param event The message event received on the window
106
+ * @returns A message with the `StandardMessage` format, or null if the conversion was unsuccessful
107
+ */
108
+ const eventToStandardMessage = (event) => {
109
+ try {
110
+ // Currently all non-string messages are discarded here since parsing throws an error
111
+ // TODO Review whether this is the desired outcome
112
+ const data = JSON.parse(event.data);
113
+ const message = isProgrammaticMessage(data)
114
+ ? toStandardMessage(data)
115
+ : data;
116
+ if (isValidPayload(message)) {
117
+ return message;
118
+ }
119
+ }
120
+ catch (ex) {
121
+ // Do nothing
122
+ }
123
+ };
124
+ /**
125
+ * Respond to the original iframe with the result of calling the
126
+ * persistent listener / listener chain
127
+ */
128
+ const respond = (id, target, error, result) => {
129
+ (0, post_message_1.postMessage)({
130
+ id,
131
+ error,
132
+ result,
133
+ }, target ?? window);
134
+ };
135
+ /**
136
+ * Callback that is fired when an arbitrary message is received on the window
137
+ *
138
+ * @param event The message event received on the window
139
+ */
140
+ const onMessage = async (event) => {
141
+ const message = eventToStandardMessage(event);
142
+ if (!message) {
143
+ return;
144
+ }
145
+ const listener = LISTENERS[message.type];
146
+ if (Array.isArray(listener) && listener.length) {
147
+ // Because any listener can have side-effects (by unregistering itself),
148
+ // we run the promise chain on a copy of the `LISTENERS` array.
149
+ // Hat tip @piuccio
150
+ const promise =
151
+ // We offer, but don't impose, the possibility that a listener returns
152
+ // a value that must be sent back to the calling frame. To do this,
153
+ // we pass the cumulated returned value as a second argument to each
154
+ // listener. Notice we don't try some clever way to compose the result
155
+ // value ourselves, this would only make the solution more complex.
156
+ // That means a listener can ignore the cumulated return value and
157
+ // return something else entirely—life is unfair.
158
+ // We don't know what each callack will be made of, we don't want to.
159
+ // And so we wrap each call in a promise chain, in case one drops the
160
+ // occasional fastdom bomb in the middle.
161
+ listener.reduce((func, listener) => func.then((ret) => {
162
+ const thisRet = listener(message.value, ret, getIframe(message, event.source));
163
+ return thisRet === undefined ? ret : thisRet;
164
+ }), Promise.resolve());
165
+ return promise
166
+ .then((response) => {
167
+ respond(message.id, event.source, null, response);
168
+ })
169
+ .catch((ex) => {
170
+ reportError(ex, {
171
+ feature: 'native-ads',
172
+ });
173
+ respond(message.id, event.source, formatError(error500, ex.toString()), null);
174
+ });
175
+ }
176
+ else if (typeof listener === 'function') {
177
+ // We found a persistent listener, to which we just delegate
178
+ // responsibility to write something. Anything. Really.
179
+ // The listener writes something by being given the `respond` function as the spec
180
+ listener(
181
+ // TODO change the arguments expected by persistent listeners to avoid this
182
+ (error, result) => respond(message.id, event.source, error, result), message.value, getIframe(message, event.source));
183
+ }
184
+ else {
185
+ // If there is no routine attached to this event type, we just answer
186
+ // with an error code
187
+ respond(message.id, event.source, formatError(error405, message.type), null);
188
+ }
189
+ };
190
+ const on = (window) => {
191
+ window.addEventListener('message', (event) => void onMessage(event));
192
+ };
193
+ const off = (window) => {
194
+ window.removeEventListener('message', (event) => void onMessage(event));
195
+ };
196
+ /**
197
+ * Register a listener for a given type of iframe message
198
+ *
199
+ * @param type The `type` of message to register against
200
+ * @param callback The listener callback to register that will receive messages of the given type
201
+ * @param options Options for the target window
202
+ */
203
+ const register = (type, callback, options) => {
204
+ if (REGISTERED_LISTENERS === 0) {
205
+ on(options?.window ?? window);
206
+ }
207
+ const listeners = LISTENERS[type] ?? [];
208
+ if (Array.isArray(listeners) && !listeners.includes(callback)) {
209
+ LISTENERS[type] = [...listeners, callback];
210
+ REGISTERED_LISTENERS += 1;
211
+ }
212
+ };
213
+ exports.register = register;
214
+ /**
215
+ * Register a persistent listener for a given type of iframe message
216
+ *
217
+ * @param type The `type` of message to register against
218
+ * @param callback The persistent listener callback to register that will receive messages of the given type
219
+ * @param options Options for the target window and whether the callback is persistent
220
+ */
221
+ const registerPersistentListener = (type, callback, options) => {
222
+ if (REGISTERED_LISTENERS === 0) {
223
+ on(options?.window ?? window);
224
+ }
225
+ LISTENERS[type] = callback;
226
+ REGISTERED_LISTENERS += 1;
227
+ };
228
+ exports.registerPersistentListener = registerPersistentListener;
229
+ /**
230
+ * Unregister a callback for a given type
231
+ *
232
+ * @param type The type of message to unregister against. An iframe will send
233
+ * messages annotated with the type
234
+ * @param callback Optionally include the original callback. If this is included
235
+ * for a persistent callback this function will be unregistered. If it's
236
+ * included for a non-persistent callback only the matching callback is removed,
237
+ * otherwise all callbacks for that type will be unregistered
238
+ * @param options Option for the target window
239
+ */
240
+ const unregister = (type, callback, options) => {
241
+ const listeners = LISTENERS[type];
242
+ if (listeners === undefined) {
243
+ throw new Error(formatError(error405, type).message);
244
+ }
245
+ else if (listeners === callback) {
246
+ LISTENERS[type] = undefined;
247
+ REGISTERED_LISTENERS -= 1;
248
+ }
249
+ else if (Array.isArray(listeners)) {
250
+ if (callback === undefined) {
251
+ LISTENERS[type] = [];
252
+ REGISTERED_LISTENERS -= listeners.length;
253
+ }
254
+ else {
255
+ LISTENERS[type] = listeners.filter((cb) => {
256
+ const callbacksEqual = cb === callback;
257
+ if (callbacksEqual) {
258
+ REGISTERED_LISTENERS -= 1;
259
+ }
260
+ return !callbacksEqual;
261
+ });
262
+ }
263
+ }
264
+ if (REGISTERED_LISTENERS === 0) {
265
+ off(options?.window ?? window);
266
+ }
267
+ };
268
+ exports.unregister = unregister;
269
+ /**
270
+ * Initialize an array of listener callbacks in a batch
271
+ *
272
+ * @param listeners The listener registration functions
273
+ * @param persistentListeners The persistent listener registration functions
274
+ */
275
+ const init = (listeners, persistentListeners, errorHandler) => {
276
+ reportError = errorHandler;
277
+ listeners.forEach((moduleInit) => moduleInit(exports.register, errorHandler));
278
+ persistentListeners.forEach((moduleInit) => moduleInit(exports.registerPersistentListener, errorHandler));
279
+ };
280
+ exports.init = init;
281
+ exports._ = { onMessage };
@@ -31,3 +31,5 @@ export { getSharedTargeting } from './targeting/shared';
31
31
  export type { ViewportTargeting } from './targeting/viewport';
32
32
  export { getViewportTargeting } from './targeting/viewport';
33
33
  export { pickTargetingValues } from './targeting/pick-targeting-values';
34
+ export { init as initMessenger, type RegisterListener, type RegisterPersistentListener, type RespondProxy, } from './messenger';
35
+ export { postMessage } from './messenger/post-message';
package/dist/esm/index.js CHANGED
@@ -24,3 +24,5 @@ export { getSessionTargeting } from './targeting/session';
24
24
  export { getSharedTargeting } from './targeting/shared';
25
25
  export { getViewportTargeting } from './targeting/viewport';
26
26
  export { pickTargetingValues } from './targeting/pick-targeting-values';
27
+ export { init as initMessenger, } from './messenger';
28
+ export { postMessage } from './messenger/post-message';
@@ -0,0 +1 @@
1
+ export declare const postMessage: (message: Record<string, unknown>, target: MessageEventSource, targetOrigin?: string) => void;
@@ -0,0 +1,3 @@
1
+ export const postMessage = (message, target, targetOrigin = '*') => {
2
+ target.postMessage(JSON.stringify(message), { targetOrigin });
3
+ };
@@ -0,0 +1,97 @@
1
+ /**
2
+ * The type of iframe messages we accept
3
+ */
4
+ declare type MessageType = 'background' | 'click' | 'disable-refresh' | 'get-page-targeting' | 'get-page-url' | 'get-styles' | 'measure-ad-load' | 'passback' | 'resize' | 'set-ad-height' | 'scroll' | 'type' | 'viewport';
5
+ /**
6
+ * Callbacks that can be registered to fire when receiving messages from an iframe
7
+ */
8
+ declare type ListenerCallback = (
9
+ /**
10
+ * The data payload sent by an iframe, and has type `unknown` because we can't
11
+ * predict what the iframe will send. It is the responsibility of the callback
12
+ * to obtain a value of the desired type (or fail gracefully)
13
+ */
14
+ specs: unknown | undefined,
15
+ /**
16
+ * Non-persistent callbacks can be chained together. This value is the return
17
+ * value of the previously fired callback in the chain. It is the responsibility
18
+ * of the current callback to either ignore it or use it / pass along
19
+ */
20
+ ret: unknown,
21
+ /**
22
+ * Reference to the iframe that is the source of the message
23
+ */
24
+ iframe?: HTMLIFrameElement) => unknown;
25
+ declare type RespondProxy = (error: {
26
+ message: string;
27
+ } | null, result: unknown) => void;
28
+ /**
29
+ * Persistent callbacks that can be registered to fire when receiving messages from an iframe
30
+ *
31
+ * A persistent callback listener will be passed a RespondProxy function
32
+ *
33
+ * This function allows the listener to call respond (i.e. postMessage the originating iFrame) itself whenever it needs to
34
+ *
35
+ * This is useful for listeners such as viewport or scroll where the values change over time
36
+ */
37
+ declare type PersistentListenerCallback = (respondProxy: RespondProxy, specs: unknown,
38
+ /**
39
+ * Reference to the iframe that is the source of the message
40
+ */
41
+ iframe?: HTMLIFrameElement) => void;
42
+ declare type ListenerOptions = {
43
+ window?: WindowProxy;
44
+ };
45
+ declare type MessengerErrorHandler = (err: Error, features: Record<string, string>) => void;
46
+ /**
47
+ * Types of functions to register a listener for a given type of iframe message
48
+ */
49
+ declare type RegisterListener = (type: MessageType, callback: ListenerCallback, options?: ListenerOptions) => void;
50
+ /**
51
+ * Types of functions to register a persistent listener for a given type of iframe message
52
+ */
53
+ declare type RegisterPersistentListener = (type: MessageType, callback: PersistentListenerCallback, options?: ListenerOptions) => void;
54
+ /**
55
+ * Types of functions to unregister a listener for a given type of iframe message
56
+ *
57
+ */
58
+ declare type UnregisterListener = (type: MessageType, callback?: ListenerCallback, options?: ListenerOptions) => void;
59
+ /**
60
+ * Register a listener for a given type of iframe message
61
+ *
62
+ * @param type The `type` of message to register against
63
+ * @param callback The listener callback to register that will receive messages of the given type
64
+ * @param options Options for the target window
65
+ */
66
+ export declare const register: RegisterListener;
67
+ /**
68
+ * Register a persistent listener for a given type of iframe message
69
+ *
70
+ * @param type The `type` of message to register against
71
+ * @param callback The persistent listener callback to register that will receive messages of the given type
72
+ * @param options Options for the target window and whether the callback is persistent
73
+ */
74
+ export declare const registerPersistentListener: RegisterPersistentListener;
75
+ /**
76
+ * Unregister a callback for a given type
77
+ *
78
+ * @param type The type of message to unregister against. An iframe will send
79
+ * messages annotated with the type
80
+ * @param callback Optionally include the original callback. If this is included
81
+ * for a persistent callback this function will be unregistered. If it's
82
+ * included for a non-persistent callback only the matching callback is removed,
83
+ * otherwise all callbacks for that type will be unregistered
84
+ * @param options Option for the target window
85
+ */
86
+ export declare const unregister: UnregisterListener;
87
+ /**
88
+ * Initialize an array of listener callbacks in a batch
89
+ *
90
+ * @param listeners The listener registration functions
91
+ * @param persistentListeners The persistent listener registration functions
92
+ */
93
+ export declare const init: (listeners: ((register: RegisterListener, errorHandler: MessengerErrorHandler) => void)[], persistentListeners: ((register: RegisterPersistentListener, errorHandler: MessengerErrorHandler) => void)[], errorHandler: MessengerErrorHandler) => void;
94
+ export declare const _: {
95
+ onMessage: (event: MessageEvent) => Promise<void>;
96
+ };
97
+ export type { RespondProxy, RegisterListener, RegisterPersistentListener, UnregisterListener, ListenerOptions, MessengerErrorHandler, };
@@ -0,0 +1,274 @@
1
+ import { postMessage } from './messenger/post-message';
2
+ const LISTENERS = {};
3
+ let REGISTERED_LISTENERS = 0;
4
+ let reportError = () => {
5
+ // not set yet
6
+ };
7
+ const error405 = {
8
+ code: 405,
9
+ message: 'Service %% not implemented',
10
+ };
11
+ const error500 = {
12
+ code: 500,
13
+ message: 'Internal server error\n\n%%',
14
+ };
15
+ /**
16
+ * Determine if an unknown payload has the shape of a programmatic message
17
+ *
18
+ * @param payload The unknown message payload
19
+ */
20
+ const isProgrammaticMessage = (payload) => {
21
+ const payloadToCheck = payload;
22
+ return (payloadToCheck.type === 'set-ad-height' &&
23
+ ('id' in payloadToCheck.value || 'slotId' in payloadToCheck.value) &&
24
+ 'height' in payloadToCheck.value);
25
+ };
26
+ /**
27
+ * Convert a legacy programmatic message to a standard message
28
+ *
29
+ * Note that this only applies to specific resize programmatic messages
30
+ * (these include specific width and height values)
31
+ */
32
+ const toStandardMessage = (payload) => ({
33
+ id: 'aaaa0000-bb11-cc22-dd33-eeeeee444444',
34
+ type: 'resize',
35
+ iframeId: payload.value.id,
36
+ slotId: payload.value.slotId,
37
+ value: {
38
+ height: payload.value.height,
39
+ width: payload.value.width,
40
+ },
41
+ });
42
+ /**
43
+ * Retrieve a reference to the calling iFrame
44
+ *
45
+ * Attempts the following strategies to find the correct iframe:
46
+ * - using the slotId from the incoming message
47
+ * - using the iframeId from the incoming message
48
+ * - checking message event.source (i.e. window) against all page level iframe contentWindows
49
+ *
50
+ * Listeners can then use the iFrame to determine the slot making the postMessage call
51
+ */
52
+ const getIframe = (message, messageEventSource) => {
53
+ if (message.slotId) {
54
+ const container = document.getElementById(`dfp-ad--${message.slotId}`);
55
+ return container?.querySelector('iframe') ?? undefined;
56
+ }
57
+ else if (message.iframeId) {
58
+ const el = document.getElementById(message.iframeId);
59
+ return el instanceof HTMLIFrameElement ? el : undefined;
60
+ }
61
+ else if (messageEventSource) {
62
+ const iframes = document.querySelectorAll('iframe');
63
+ return Array.from(iframes).find((iframe) => iframe.contentWindow === messageEventSource);
64
+ }
65
+ };
66
+ // Regex for testing validity of message ids
67
+ const validMessageRegex = /^[a-f0-9]{8}-([a-f0-9]{4}-){3}[a-f0-9]{12}$/;
68
+ /**
69
+ * Narrow an `unknown` payload to the standard message format
70
+ *
71
+ * Until DFP provides a way for us to identify with 100% certainty our
72
+ * in-house creatives, we are left with doing some basic tests
73
+ * such as validating the anatomy of the payload and whitelisting
74
+ * event type
75
+ */
76
+ const isValidPayload = (payload) => {
77
+ const payloadToCheck = payload;
78
+ return ('type' in payloadToCheck &&
79
+ 'value' in payloadToCheck &&
80
+ 'id' in payloadToCheck &&
81
+ payloadToCheck.type in LISTENERS &&
82
+ validMessageRegex.test(payloadToCheck.id));
83
+ };
84
+ /**
85
+ * Cheap string formatting function
86
+ *
87
+ * @param error An object `{ code, message }`. `message` is a string where successive
88
+ * occurrences of %% will be replaced by the following arguments
89
+ * @param args Arguments that will replace %%
90
+ *
91
+ * @example
92
+ * formatError({ message: "%%, you are so %%" }, "Regis", "lovely")
93
+ * => { message: "Regis, you are so lovely" }
94
+ */
95
+ const formatError = (error, ...args) => args.reduce((e, arg) => {
96
+ e.message = e.message.replace('%%', arg);
97
+ return e;
98
+ }, error);
99
+ /**
100
+ * Convert a posted message to our StandardMessage format
101
+ *
102
+ * @param event The message event received on the window
103
+ * @returns A message with the `StandardMessage` format, or null if the conversion was unsuccessful
104
+ */
105
+ const eventToStandardMessage = (event) => {
106
+ try {
107
+ // Currently all non-string messages are discarded here since parsing throws an error
108
+ // TODO Review whether this is the desired outcome
109
+ const data = JSON.parse(event.data);
110
+ const message = isProgrammaticMessage(data)
111
+ ? toStandardMessage(data)
112
+ : data;
113
+ if (isValidPayload(message)) {
114
+ return message;
115
+ }
116
+ }
117
+ catch (ex) {
118
+ // Do nothing
119
+ }
120
+ };
121
+ /**
122
+ * Respond to the original iframe with the result of calling the
123
+ * persistent listener / listener chain
124
+ */
125
+ const respond = (id, target, error, result) => {
126
+ postMessage({
127
+ id,
128
+ error,
129
+ result,
130
+ }, target ?? window);
131
+ };
132
+ /**
133
+ * Callback that is fired when an arbitrary message is received on the window
134
+ *
135
+ * @param event The message event received on the window
136
+ */
137
+ const onMessage = async (event) => {
138
+ const message = eventToStandardMessage(event);
139
+ if (!message) {
140
+ return;
141
+ }
142
+ const listener = LISTENERS[message.type];
143
+ if (Array.isArray(listener) && listener.length) {
144
+ // Because any listener can have side-effects (by unregistering itself),
145
+ // we run the promise chain on a copy of the `LISTENERS` array.
146
+ // Hat tip @piuccio
147
+ const promise =
148
+ // We offer, but don't impose, the possibility that a listener returns
149
+ // a value that must be sent back to the calling frame. To do this,
150
+ // we pass the cumulated returned value as a second argument to each
151
+ // listener. Notice we don't try some clever way to compose the result
152
+ // value ourselves, this would only make the solution more complex.
153
+ // That means a listener can ignore the cumulated return value and
154
+ // return something else entirely—life is unfair.
155
+ // We don't know what each callack will be made of, we don't want to.
156
+ // And so we wrap each call in a promise chain, in case one drops the
157
+ // occasional fastdom bomb in the middle.
158
+ listener.reduce((func, listener) => func.then((ret) => {
159
+ const thisRet = listener(message.value, ret, getIframe(message, event.source));
160
+ return thisRet === undefined ? ret : thisRet;
161
+ }), Promise.resolve());
162
+ return promise
163
+ .then((response) => {
164
+ respond(message.id, event.source, null, response);
165
+ })
166
+ .catch((ex) => {
167
+ reportError(ex, {
168
+ feature: 'native-ads',
169
+ });
170
+ respond(message.id, event.source, formatError(error500, ex.toString()), null);
171
+ });
172
+ }
173
+ else if (typeof listener === 'function') {
174
+ // We found a persistent listener, to which we just delegate
175
+ // responsibility to write something. Anything. Really.
176
+ // The listener writes something by being given the `respond` function as the spec
177
+ listener(
178
+ // TODO change the arguments expected by persistent listeners to avoid this
179
+ (error, result) => respond(message.id, event.source, error, result), message.value, getIframe(message, event.source));
180
+ }
181
+ else {
182
+ // If there is no routine attached to this event type, we just answer
183
+ // with an error code
184
+ respond(message.id, event.source, formatError(error405, message.type), null);
185
+ }
186
+ };
187
+ const on = (window) => {
188
+ window.addEventListener('message', (event) => void onMessage(event));
189
+ };
190
+ const off = (window) => {
191
+ window.removeEventListener('message', (event) => void onMessage(event));
192
+ };
193
+ /**
194
+ * Register a listener for a given type of iframe message
195
+ *
196
+ * @param type The `type` of message to register against
197
+ * @param callback The listener callback to register that will receive messages of the given type
198
+ * @param options Options for the target window
199
+ */
200
+ export const register = (type, callback, options) => {
201
+ if (REGISTERED_LISTENERS === 0) {
202
+ on(options?.window ?? window);
203
+ }
204
+ const listeners = LISTENERS[type] ?? [];
205
+ if (Array.isArray(listeners) && !listeners.includes(callback)) {
206
+ LISTENERS[type] = [...listeners, callback];
207
+ REGISTERED_LISTENERS += 1;
208
+ }
209
+ };
210
+ /**
211
+ * Register a persistent listener for a given type of iframe message
212
+ *
213
+ * @param type The `type` of message to register against
214
+ * @param callback The persistent listener callback to register that will receive messages of the given type
215
+ * @param options Options for the target window and whether the callback is persistent
216
+ */
217
+ export const registerPersistentListener = (type, callback, options) => {
218
+ if (REGISTERED_LISTENERS === 0) {
219
+ on(options?.window ?? window);
220
+ }
221
+ LISTENERS[type] = callback;
222
+ REGISTERED_LISTENERS += 1;
223
+ };
224
+ /**
225
+ * Unregister a callback for a given type
226
+ *
227
+ * @param type The type of message to unregister against. An iframe will send
228
+ * messages annotated with the type
229
+ * @param callback Optionally include the original callback. If this is included
230
+ * for a persistent callback this function will be unregistered. If it's
231
+ * included for a non-persistent callback only the matching callback is removed,
232
+ * otherwise all callbacks for that type will be unregistered
233
+ * @param options Option for the target window
234
+ */
235
+ export const unregister = (type, callback, options) => {
236
+ const listeners = LISTENERS[type];
237
+ if (listeners === undefined) {
238
+ throw new Error(formatError(error405, type).message);
239
+ }
240
+ else if (listeners === callback) {
241
+ LISTENERS[type] = undefined;
242
+ REGISTERED_LISTENERS -= 1;
243
+ }
244
+ else if (Array.isArray(listeners)) {
245
+ if (callback === undefined) {
246
+ LISTENERS[type] = [];
247
+ REGISTERED_LISTENERS -= listeners.length;
248
+ }
249
+ else {
250
+ LISTENERS[type] = listeners.filter((cb) => {
251
+ const callbacksEqual = cb === callback;
252
+ if (callbacksEqual) {
253
+ REGISTERED_LISTENERS -= 1;
254
+ }
255
+ return !callbacksEqual;
256
+ });
257
+ }
258
+ }
259
+ if (REGISTERED_LISTENERS === 0) {
260
+ off(options?.window ?? window);
261
+ }
262
+ };
263
+ /**
264
+ * Initialize an array of listener callbacks in a batch
265
+ *
266
+ * @param listeners The listener registration functions
267
+ * @param persistentListeners The persistent listener registration functions
268
+ */
269
+ export const init = (listeners, persistentListeners, errorHandler) => {
270
+ reportError = errorHandler;
271
+ listeners.forEach((moduleInit) => moduleInit(register, errorHandler));
272
+ persistentListeners.forEach((moduleInit) => moduleInit(registerPersistentListener, errorHandler));
273
+ };
274
+ export const _ = { onMessage };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@guardian/commercial-core",
3
- "version": "4.10.0",
3
+ "version": "4.11.0",
4
4
  "description": "Guardian advertising business logic",
5
5
  "homepage": "https://github.com/guardian/commercial-core#readme",
6
6
  "bugs": {