PageSourceSearch

https://mianfan.fr/wp-content/themes/mianfan-child-2026/js/resa-tr…/2026-09-08-ga4-candidate/bridge.gtm.js

js mianfan.fr collected 2026-10-06 12:45:12 UTC 16,641 bytes, 332 lines download raw bytes

1/* ES5-SYNTAX DERIVATIVE OF bridge.js; NON-PUBLISHED.
2 * Modern browser APIs remain required. No added polyfills or weaker security fallback.
3 */
4/* NON-PUBLISHED PROTOTYPE. No Ads/GA requests, cookies, storage or network interception. */
5(function (root, factory) {
6  if (typeof module === 'object' && module.exports) module.exports = factory();
7  else if (!root.MFResaBridge) root.MFResaBridge = factory();
8})(typeof globalThis !== 'undefined' ? globalThis : this, function () {
9  'use strict';
10  var VERSION = 1;
11  var FRAME_ORIGIN = 'https://app.resa.ninja';
12  var RESTAURANTS = Object.freeze({
13    '982a03d3-e02c-4679-abe5-061dd8ec0ca9': Object.freeze({ name: 'Mogador', parentOrigin: 'https://mianfan.fr' }),
14    'd74eb7ec-1313-4b8e-a14e-d9cfe352bac3': Object.freeze({ name: 'Peletier', parentOrigin: 'https://mianfan.fr' }),
15    'ede23ae3-8582-4ccc-83e5-3622db6798ad': Object.freeze({ name: 'Grands Boulevards', parentOrigin: 'https://mianfan.fr' }),
16    'd45d24fc-13a2-430c-9ddb-4749d85a3930': Object.freeze({ name: 'Dumpling Mogador', parentOrigin: 'https://mianfan.fr' }),
17    'ae2e8000-0c76-49bb-8f19-0aeb2b475c0f': Object.freeze({ name: 'Cocottes', parentOrigin: 'https://mianfan.fr' }),
18    'b214aa56-1a8b-4b60-b83e-ed0262d8a3e0': Object.freeze({ name: 'Montparnasse', parentOrigin: 'https://mianfan.fr' }),
19    'a9a56bd0-4f76-460c-94c9-b1bc08c0bd09': Object.freeze({ name: 'Naï Naï', parentOrigin: 'https://nai-nai-restaurant.fr' })
20  });
21  var NONCE = /^[0-9a-f]{64}$/;
22  var EVENT_ID = /^evt_[0-9a-f]{32}$/;
23  var parentInstances = new WeakMap();
24  var frameInstances = new WeakMap();
25  var normalize = function (text) { return String(text || '').replace(/\s+/g, ' ').trim(); };
26  var sameKeys = function (object, keys) { return object !== null && typeof object === 'object' &&
27    !Array.isArray(object) && Object.keys(object).length === keys.length &&
28    keys.every(function (key) { return Object.prototype.hasOwnProperty.call(object, key); }); };
29  function randomHex(win, byteCount) {
30    if (!win.crypto || typeof win.crypto.getRandomValues !== 'function') {
31      throw new Error('Secure randomness unavailable: bridge disabled.');
32    }
33    var bytes = new Uint8Array(byteCount);
34    win.crypto.getRandomValues(bytes);
35    return Array.from(bytes, function (b) { return b.toString(16).padStart(2, '0'); }).join('');
36  }
37  function restaurantFromUrl(href) {
38    try {
39      var url = new URL(href);
40      var match = /^\/booking-iframe\/web\/([0-9a-f-]{36})$/.exec(url.pathname);
41      if (url.origin !== FRAME_ORIGIN || !match || !RESTAURANTS[match[1]]) return null;
42      // Styling query parameters are allowed; unrelated routes are not.
43      return match[1];
44    } catch (_) { return null; }
45  }
46  function textOnly(node) {
47    return node && Array.from(node.childNodes || []).every(function (child) { return child.nodeType === 3; });
48  }
49  function directParagraphs(pane) {
50    var children = Array.from(pane.children || []);
51    return children.every(function (child) { return child.tagName === 'P'; }) ? children : [];
52  }
53  function classifyPane(pane) {
54    var p = directParagraphs(pane);
55    if (p.length === 4 && textOnly(p[0]) && textOnly(p[1]) && textOnly(p[3]) &&
56        normalize(p[0].textContent) === 'Votre réservation a été soumise au restaurateur.' &&
57        normalize(p[1].textContent) === 'Une fois votre réservation validée un email de confirmation vous sera envoyé.' &&
58        normalize(p[3].textContent) === 'Bonne journée !') {
59      return { outcome: 'request_recorded', partySize: null };
60    }
61    if (p.length !== 3 || !textOnly(p[2]) || normalize(p[2].textContent) !== 'Bonne journée !') return null;
62    // The first paragraph is configurable: never inspect it or infer acceptance from it.
63    var nodes = Array.from(p[1].childNodes || []);
64    if (nodes.length !== 4 || nodes[0].nodeType !== 3 || nodes[2].nodeType !== 3 ||
65        nodes[1].tagName !== 'B' || nodes[3].tagName !== 'B' ||
66        !textOnly(nodes[1]) || !textOnly(nodes[3]) ||
67        normalize(nodes[0].textContent) !== 'Nous vous attendons donc le' ||
68        normalize(nodes[2].textContent) !== 'pour') return null;
69    // Validate, but never retain or transmit the date/selected service text.
70    if (!/^[A-Za-zÀ-ÿ]+ \d{1,2} [A-Za-zÀ-ÿ]+ \d{4} à \d{1,2}:\d{2}$/.test(normalize(nodes[1].textContent))) return null;
71    var covers = /^([1-9][0-9]{0,2}|1000) couvert\(s\)\.$/.exec(normalize(nodes[3].textContent));
72    return covers ? { outcome: 'auto_accepted', partySize: Number(covers[1]) } : null;
73  }
74  function readSnapshot(doc) {
75    var roots = doc.querySelectorAll('.fork-booking');
76    var empty = { valid: false, form: false, final: false, cash: false, error: false, result: null };
77    if (roots.length !== 1) return empty;
78    var root = roots[0];
79    var forms = root.querySelectorAll('form.form-cont');
80    var finals = root.querySelectorAll('.confirmation-cont');
81    var cash = Boolean(root.querySelector('.form-takeResa-cash'));
82    var error = Boolean(doc.querySelector('.el-notification .el-icon-error, .el-message--error'));
83    var result = null;
84    if (finals.length === 1 && forms.length === 0) {
85      var children = Array.from(finals[0].children || []);
86      if (children.length === 1 && children[0].classList.contains('confirmation')) result = classifyPane(children[0]);
87    }
88    return { valid: forms.length <= 1 && finals.length <= 1, form: forms.length === 1,
89      final: finals.length > 0, cash: cash, error: error, result: result };
90  }
91  function createCycleTracker(makeEventId) {
92    var cycle = null;
93    var cashBlocked = false;
94    return {
95      observe: function (snapshot) {
96        if (snapshot.cash) cashBlocked = true;
97        if (!snapshot.valid || cashBlocked) return null;
98        if (snapshot.form && !snapshot.final && (!cycle || cycle.done)) {
99          cycle = { eventId: makeEventId(), done: false, failed: false };
100        }
101        if (!cycle || cycle.done) return null;
102        if (snapshot.error) cycle.failed = true;
103        if (!snapshot.result || snapshot.form || cycle.failed) return null;
104        cycle.done = true;
105        return { eventId: cycle.eventId, outcome: snapshot.result.outcome, partySize: snapshot.result.partySize };
106      }
107    };
108  }
109  var baseKeys = ['type', 'version', 'restaurantId', 'nonce'];
110  function validBase(data, type, id, nonce) {
111    return data && data.type === type && data.version === VERSION && data.restaurantId === id &&
112      typeof data.nonce === 'string' && NONCE.test(data.nonce) && (nonce === undefined || data.nonce === nonce);
113  }
114  function validEvent(data, id, nonce) {
115    if (!sameKeys(data, baseKeys.concat(['eventId', 'outcome', 'partySize'])) ||
116        !validBase(data, 'mf-resa-event', id, nonce) || typeof data.eventId !== 'string' || !EVENT_ID.test(data.eventId)) return false;
117    return data.outcome === 'request_recorded' ? data.partySize === null :
118      data.outcome === 'auto_accepted' && Number.isInteger(data.partySize) && data.partySize >= 1 && data.partySize <= 1000;
119  }
120
121  function createParentBridge(options) {
122    var win = options.window;
123    var iframe = options.iframe;
124    var id = options.restaurantId;
125    var restaurant = RESTAURANTS[id];
126    if (!restaurant || win.location.origin !== restaurant.parentOrigin || typeof options.onConversion !== 'function') {
127      throw new Error('Invalid parent origin, restaurant or integration callback.');
128    }
129    if (parentInstances.has(iframe)) throw new Error('A bridge already exists for this iframe.');
130    var granted = false;
131    var destroyed = false;
132    var nonce = null;
133    var retryTimer = null;
134    var source = null;
135    var seen = new Set();
136    var report = function (error) { if (typeof options.onError === 'function') options.onError(error); };
137    var frameMatches = function () { return restaurantFromUrl(iframe.src) === id; };
138    function stopRetry() {
139      if (retryTimer !== null) win.clearInterval(retryTimer);
140      retryTimer = null;
141    }
142    function sendControl(action) {
143      if (!nonce || !source) return;
144      source.postMessage({ type: 'mf-resa-control', version: VERSION, restaurantId: id, nonce: nonce, action: action }, FRAME_ORIGIN);
145    }
146    function revoke() {
147      granted = false; // Reject already queued events before messaging the iframe.
148      stopRetry();
149      sendControl('revoke');
150      nonce = null;
151      source = null;
152      seen.clear();
153    }
154    function startSession() {
155      stopRetry();
156      nonce = null;
157      source = null;
158      seen.clear();
159      // Preserve the explicit choice while waiting for a lazy iframe to load.
160      if (!granted || !frameMatches() || !iframe.contentWindow) return;
161      source = iframe.contentWindow;
162      nonce = randomHex(win, 32);
163      seen.clear();
164      var attempts = 0;
165      sendControl('grant');
166      retryTimer = win.setInterval(function () {
167        if (!granted || !frameMatches() || iframe.contentWindow !== source || ++attempts >= 10) {
168          stopRetry();
169          return;
170        }
171        sendControl('grant');
172      }, 500);
173    }
174    function receive(event) {
175      if (destroyed || !granted || !frameMatches() || event.origin !== FRAME_ORIGIN ||
176          event.source !== iframe.contentWindow) return;
177      var data = event.data;
178      if (sameKeys(data, ['type', 'version', 'restaurantId']) && data.type === 'mf-resa-hello' &&
179          data.version === VERSION && data.restaurantId === id) {
180        try { startSession(); } catch (error) { revoke(); report(error); }
181        return;
182      }
183      if (!nonce || iframe.contentWindow !== source || event.source !== source) return;
184      if (sameKeys(data, baseKeys) && validBase(data, 'mf-resa-ready', id, nonce)) { stopRetry(); return; }
185      if (!validEvent(data, id, nonce)) return;
186      if (!seen.has(data.eventId)) {
187        if (seen.size >= 1000) { report(new Error('Session event limit reached.')); return; }
188        seen.add(data.eventId); // At-most-once callback even if the callback fails.
189        try {
190          var result = options.onConversion(Object.freeze({ measurement: 'web_confirmation',
191            eventId: data.eventId, restaurantId: id, restaurantName: restaurant.name,
192            outcome: data.outcome, partySize: data.partySize }));
193          if (result && typeof result.then === 'function') report(new Error('Integration callback must be synchronous.'));
194        } catch (_) { report(new Error('Integration callback failed; event is not retried.')); }
195      }
196      // This ACK means handed off to the callback, not received by Google or matched to a booking.
197      if (granted && nonce === data.nonce && event.source === source) {
198        source.postMessage({ type: 'mf-resa-ack', version: VERSION, restaurantId: id, nonce: nonce, eventId: data.eventId }, FRAME_ORIGIN);
199      }
200    }
201    function onLoad() {
202      if (destroyed || !granted) return;
203      // A navigation can preserve WindowProxy identity: invalidate the old nonce regardless.
204      nonce = null;
205      source = null;
206      try { startSession(); } catch (error) { revoke(); report(error); }
207    }
208    win.addEventListener('message', receive);
209    iframe.addEventListener('load', onLoad);
210    var api = {
211      setConsent: function (value) {
212        if (destroyed) throw new Error('Bridge has been destroyed.');
213        if (value !== true) { revoke(); return; }
214        if (granted) return;
215        granted = true;
216        try { startSession(); } catch (error) { revoke(); throw error; }
217      },
218      destroy: function () {
219        if (destroyed) return;
220        revoke();
221        destroyed = true;
222        win.removeEventListener('message', receive);
223        iframe.removeEventListener('load', onLoad);
224        parentInstances.delete(iframe);
225      }
226    };
227    parentInstances.set(iframe, api);
228    return api;
229  }
230
231  function createFrameBridge(options) {
232    var win = options.window;
233    var doc = options.document;
234    var id = restaurantFromUrl(win.location.href);
235    if (!id || win.parent === win) throw new Error('Not an allowed restaurant iframe.');
236    if (frameInstances.has(win)) throw new Error('An iframe bridge is already installed.');
237    var parentOrigin = RESTAURANTS[id].parentOrigin;
238    var report = function () { if (typeof options.onError === 'function') options.onError(new Error('Iframe bridge initialization/observation failed; disabled.')); };
239    var active = false;
240    var destroyed = false;
241    var nonce = null;
242    var tracker = null;
243    var observer = null;
244    var retryTimer = null;
245    var pending = new Map();
246    // Safe exclusion only; this boolean survives refusal, not any booking or user information.
247    var documentCashBlocked = false;
248    function currentRouteAllowed() { return restaurantFromUrl(win.location.href) === id; }
249    function revoke() {
250      active = false;
251      nonce = null;
252      tracker = null;
253      pending.clear();
254      if (observer) observer.disconnect();
255      observer = null;
256      if (retryTimer !== null) win.clearInterval(retryTimer);
257      retryTimer = null;
258    }
259    function send(data) {
260      if (!active || !currentRouteAllowed()) { revoke(); return; }
261      win.parent.postMessage(data, parentOrigin);
262    }
263    function scan() {
264      if (!active || !currentRouteAllowed()) { revoke(); return; }
265      try {
266        var snapshot = readSnapshot(doc);
267        if (snapshot.cash) documentCashBlocked = true;
268        if (documentCashBlocked) return;
269        var result = tracker.observe(snapshot);
270        if (!result) return;
271        var data = { type: 'mf-resa-event', version: VERSION, restaurantId: id, nonce: nonce,
272          eventId: result.eventId, outcome: result.outcome, partySize: result.partySize };
273        pending.set(result.eventId, { data: data, attempts: 1 });
274        send(data);
275      } catch (_) { revoke(); report(); }
276    }
277    function receive(event) {
278      if (destroyed || event.origin !== parentOrigin || event.source !== win.parent) return;
279      var data = event.data;
280      if (sameKeys(data, baseKeys.concat(['eventId'])) && active &&
281          validBase(data, 'mf-resa-ack', id, nonce) && typeof data.eventId === 'string' && EVENT_ID.test(data.eventId)) {
282        pending.delete(data.eventId);
283        return;
284      }
285      if (!sameKeys(data, baseKeys.concat(['action'])) || !validBase(data, 'mf-resa-control', id) ||
286          (data.action !== 'grant' && data.action !== 'revoke')) return;
287      if (data.action === 'revoke') { if (data.nonce === nonce) revoke(); return; }
288      if (!currentRouteAllowed()) { revoke(); return; }
289      if (active && data.nonce === nonce) {
290        send({ type: 'mf-resa-ready', version: VERSION, restaurantId: id, nonce: nonce });
291        return;
292      }
293      revoke();
294      nonce = data.nonce;
295      active = true;
296      try {
297        if (!doc.body || !win.crypto || typeof win.crypto.getRandomValues !== 'function') throw new Error('Missing prerequisites.');
298        tracker = createCycleTracker(function () { return 'evt_' + randomHex(win, 16); });
299        observer = new win.MutationObserver(scan);
300        observer.observe(doc.body, { childList: true, subtree: true, characterData: true, attributes: true, attributeFilter: ['class'] });
301        // An existing final screen has no preceding observed form: it is never backfilled.
302        scan();
303      } catch (_) { revoke(); report(); }
304      if (!active) return;
305      send({ type: 'mf-resa-ready', version: VERSION, restaurantId: id, nonce: nonce });
306      retryTimer = win.setInterval(function () {
307        if (!active || !currentRouteAllowed()) { revoke(); return; }
308        pending.forEach(function (item) {
309          if (item.attempts >= 10) return;
310          item.attempts += 1;
311          send(item.data); // Same event ID on every retry.
312        });
313      }, 2000);
314    }
315    win.addEventListener('message', receive);
316    var api = { destroy: function () {
317      if (destroyed) return;
318      revoke();
319      destroyed = true;
320      win.removeEventListener('message', receive);
321      frameInstances.delete(win);
322    } };
323    frameInstances.set(win, api);
324    // Initialization signal only. No DOM is read and no measurement is buffered before a grant.
325    win.parent.postMessage({ type: 'mf-resa-hello', version: VERSION, restaurantId: id }, parentOrigin);
326    return api;
327  }
328  return Object.freeze({ VERSION: VERSION, RESTAURANTS: RESTAURANTS, FRAME_ORIGIN: FRAME_ORIGIN,
329    restaurantFromUrl: restaurantFromUrl, classifyPane: classifyPane, readSnapshot: readSnapshot,
330    createCycleTracker: createCycleTracker, validEvent: validEvent,
331    createParentBridge: createParentBridge, createFrameBridge: createFrameBridge });
332});

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.