@blotoutio/edgetag-sdk-js 1.58.1 → 1.59.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.cjs.js CHANGED
@@ -507,77 +507,6 @@ const usStates = new Map([
507
507
  ['US-VI', 'Virgin Islands, U.S.'],
508
508
  ]);
509
509
  new Set([...isoCountries.keys(), ...usStates.keys()]);
510
- /**
511
- * ISO 3166-1 alpha-2 codes for EU member states (as of 2024)
512
- */
513
- const EU_COUNTRY_CODES = new Set([
514
- 'AT', // Austria
515
- 'BE', // Belgium
516
- 'BG', // Bulgaria
517
- 'CY', // Cyprus
518
- 'CZ', // Czech Republic
519
- 'DE', // Germany
520
- 'DK', // Denmark
521
- 'EE', // Estonia
522
- 'ES', // Spain
523
- 'FI', // Finland
524
- 'FR', // France
525
- 'GR', // Greece
526
- 'HR', // Croatia
527
- 'HU', // Hungary
528
- 'IE', // Ireland
529
- 'IT', // Italy
530
- 'LT', // Lithuania
531
- 'LU', // Luxembourg
532
- 'LV', // Latvia
533
- 'MT', // Malta
534
- 'NL', // Netherlands
535
- 'PL', // Poland
536
- 'PT', // Portugal
537
- 'RO', // Romania
538
- 'SE', // Sweden
539
- 'SI', // Slovenia
540
- 'SK', // Slovakia
541
- ]);
542
- /**
543
- * Returns true if the request originates from the EU or the UK (GB).
544
- * Cloudflare's isEUCountry flag excludes the UK post-Brexit, so GB is
545
- * checked explicitly alongside the EU member set.
546
- *
547
- * Fails closed: when the country cannot be resolved, the request is treated
548
- * as EU/UK. This is the safe default for the compliance-sensitive Grey Switch
549
- * (an unknown visitor may be in the EU/UK, so it must not slip through when
550
- * the EU/UK toggle is off).
551
- */
552
- const isEuUkRegion = (country, isEURequest) => {
553
- if (isEURequest) {
554
- return true;
555
- }
556
- if (!country) {
557
- return true;
558
- }
559
- const upper = country.toUpperCase();
560
- return upper === 'GB' || EU_COUNTRY_CODES.has(upper);
561
- };
562
- /**
563
- * Grey Switch fire rule, shared by the SDK and the CDN worker so both stay in
564
- * sync. When Grey Switch is on, EdgeTag fires server-side events for
565
- * non-consented users. The EU/UK toggle is off by default, which keeps EU/UK
566
- * visitors on normal consent unless the merchant explicitly opts those regions
567
- * in. Returns whether a server-side event should fire for a non-consented user.
568
- */
569
- const greySwitchAllowsServerEvent = ({ greySwitch, greySwitchEuUk, country, isEURequest, isGPC, }) => {
570
- if (isGPC) {
571
- return false;
572
- }
573
- if (!greySwitch) {
574
- return false;
575
- }
576
- if (greySwitchEuUk) {
577
- return true;
578
- }
579
- return !isEuUkRegion(country, isEURequest);
580
- };
581
510
  const parseCache = new Map();
582
511
  const parseRegions = (regionString) => {
583
512
  const include = new Set();
@@ -1541,7 +1470,7 @@ const getStandardPayload = (destination, payload) => {
1541
1470
  referrer: getReferrer(destination),
1542
1471
  search: getSearch(destination),
1543
1472
  locale: getLocale(),
1544
- sdkVersion: "1.58.1" ,
1473
+ sdkVersion: "1.59.0" ,
1545
1474
  ...(payload || {}),
1546
1475
  };
1547
1476
  let storage = {};
@@ -1729,6 +1658,72 @@ const sendTag = (destination, { eventName, eventId, data, providerData, provider
1729
1658
  }
1730
1659
  postRequest(getTagURL(destination, eventName, options), payload, options).catch(logger.error);
1731
1660
  };
1661
+ // Scope (scripts/providers allow-lists) is delivered per plugin via /init, keyed
1662
+ // by the plugin's identity `${providerId}||${name}` (which is carried in the
1663
+ // plugin object's `name`). A missing scope means the plugin is global.
1664
+ //
1665
+ // Mirrors the edge getAllowedPlugins per-item logic: at instance level the
1666
+ // scripts allow-list wins, otherwise fall back to the providers allow-list.
1667
+ const isPluginAllowed = (scope, providerId, instanceName) => {
1668
+ if (!scope) {
1669
+ return true;
1670
+ }
1671
+ if (instanceName && scope.scripts.length > 0) {
1672
+ return scope.scripts.includes(`${providerId}||${instanceName}`);
1673
+ }
1674
+ return scope.providers.length === 0 || scope.providers.includes(providerId);
1675
+ };
1676
+ // Mirrors the edge processPluginsRoot skip scope: a global plugin (no scope)
1677
+ // skips every browser instance (null), a scoped plugin skips only the instances
1678
+ // its scripts/providers allow-lists resolve to.
1679
+ const getScopedPluginSkipKeys = (scope, configuredTags) => {
1680
+ if (!scope || (scope.scripts.length === 0 && scope.providers.length === 0)) {
1681
+ return null;
1682
+ }
1683
+ const result = new Set();
1684
+ for (const script of scope.scripts) {
1685
+ result.add(script);
1686
+ }
1687
+ for (const providerId of scope.providers) {
1688
+ const instances = configuredTags.get(providerId);
1689
+ if (!instances)
1690
+ continue;
1691
+ for (const instanceName of instances) {
1692
+ result.add(`${providerId}||${instanceName}`);
1693
+ }
1694
+ }
1695
+ return result;
1696
+ };
1697
+ // Mirrors the edge getProviderLimits: an additional event emitted by a scoped
1698
+ // plugin is restricted to that plugin's channels/instances; a global plugin
1699
+ // imposes no restriction (undefined).
1700
+ const getPluginProviderLimits = (scope) => {
1701
+ if (!scope || (scope.scripts.length === 0 && scope.providers.length === 0)) {
1702
+ return undefined;
1703
+ }
1704
+ const limited = {};
1705
+ for (const script of scope.scripts) {
1706
+ const separator = script.indexOf('||');
1707
+ if (separator === -1) {
1708
+ continue;
1709
+ }
1710
+ const provider = script.slice(0, separator);
1711
+ const instance = script.slice(separator + 2);
1712
+ const existing = limited[provider];
1713
+ if (existing && existing !== true) {
1714
+ existing[instance] = true;
1715
+ }
1716
+ else {
1717
+ limited[provider] = { [instance]: true };
1718
+ }
1719
+ }
1720
+ for (const provider of scope.providers) {
1721
+ if (!(provider in limited)) {
1722
+ limited[provider] = true;
1723
+ }
1724
+ }
1725
+ return limited;
1726
+ };
1732
1727
  const getPlugins = (destination) => {
1733
1728
  var _a, _b, _c;
1734
1729
  try {
@@ -1739,7 +1734,19 @@ const getPlugins = (destination) => {
1739
1734
  return [];
1740
1735
  }
1741
1736
  };
1742
- const runPluginHook = async (plugins, hookName, baseParams) => {
1737
+ // Dispatches a plugin's additional events. Mirrors the edge: each event is
1738
+ // stamped with the emitting plugin (so it cannot reprocess itself) and limited
1739
+ // to that plugin's scope.
1740
+ const dispatchAdditionalEvents = (plugin, pluginScopes, events) => {
1741
+ const providerLimits = getPluginProviderLimits(pluginScopes === null || pluginScopes === void 0 ? void 0 : pluginScopes[plugin.name]);
1742
+ for (const evt of events) {
1743
+ handleTag(evt.eventName, evt.data, providerLimits, undefined, plugin.name);
1744
+ }
1745
+ };
1746
+ // Runs a channel/instance hook chain. The emitting plugin is excluded by the
1747
+ // caller's allow-list filter; additional events fire before any skip is applied
1748
+ // (matching the edge), and a skip is reported without mutating the final tag.
1749
+ const runPluginHook = async (plugins, hookName, pluginScopes, baseParams) => {
1743
1750
  var _a, _b;
1744
1751
  const payload = baseParams['payload'];
1745
1752
  let currentEventName = payload['eventName'];
@@ -1760,6 +1767,9 @@ const runPluginHook = async (plugins, hookName, baseParams) => {
1760
1767
  },
1761
1768
  variables: plugin.variables || {},
1762
1769
  });
1770
+ if (result === null || result === void 0 ? void 0 : result.additionalEvents) {
1771
+ dispatchAdditionalEvents(plugin, pluginScopes, result.additionalEvents);
1772
+ }
1763
1773
  if (result === null || result === void 0 ? void 0 : result.skipEvent) {
1764
1774
  skip = true;
1765
1775
  break;
@@ -1773,24 +1783,87 @@ const runPluginHook = async (plugins, hookName, baseParams) => {
1773
1783
  if ((result === null || result === void 0 ? void 0 : result.providers) !== undefined) {
1774
1784
  currentProviders = result.providers;
1775
1785
  }
1786
+ }
1787
+ catch (e) {
1788
+ logger.error(`Plugin ${plugin.name} ${hookName} error: ${e}`);
1789
+ }
1790
+ }
1791
+ return {
1792
+ eventName: currentEventName,
1793
+ data: currentData,
1794
+ providers: currentProviders,
1795
+ skip,
1796
+ };
1797
+ };
1798
+ // Root hooks mirror the edge processPluginsRoot: every plugin runs (except the
1799
+ // one that emitted this event), additional events are dispatched before any
1800
+ // skip is applied, and skips are scoped — a global plugin marks every browser
1801
+ // instance, a scoped one only its resolved instances. The skip is a marker:
1802
+ // channel/instance hooks still run and only the final tag is suppressed.
1803
+ const runRootPluginHooks = async (plugins, baseParams, pluginScopes, configuredTags, pluginSource) => {
1804
+ var _a, _b;
1805
+ const payload = baseParams['payload'];
1806
+ let currentEventName = payload['eventName'];
1807
+ let currentData = payload['data'];
1808
+ let currentProviders = baseParams['providers'];
1809
+ const skippedInstances = new Set();
1810
+ for (const plugin of plugins) {
1811
+ if (plugin.name === pluginSource)
1812
+ continue;
1813
+ const hook = plugin.rules.tagRoot;
1814
+ if (!hook)
1815
+ continue;
1816
+ try {
1817
+ const result = await hook({
1818
+ ...baseParams,
1819
+ payload: {
1820
+ ...payload,
1821
+ eventName: currentEventName,
1822
+ data: jsonClone(currentData),
1823
+ },
1824
+ variables: plugin.variables || {},
1825
+ });
1776
1826
  if (result === null || result === void 0 ? void 0 : result.additionalEvents) {
1777
- for (const evt of result.additionalEvents) {
1778
- handleTag(evt.eventName, evt.data, undefined, undefined);
1827
+ dispatchAdditionalEvents(plugin, pluginScopes, result.additionalEvents);
1828
+ }
1829
+ if (result === null || result === void 0 ? void 0 : result.skipEvent) {
1830
+ const scopeSkipKeys = getScopedPluginSkipKeys(pluginScopes === null || pluginScopes === void 0 ? void 0 : pluginScopes[plugin.name], configuredTags);
1831
+ if (scopeSkipKeys === null) {
1832
+ // Global plugin: skip every configured browser instance.
1833
+ for (const [provider, instances] of configuredTags) {
1834
+ for (const instanceName of instances) {
1835
+ skippedInstances.add(`${provider}||${instanceName}`);
1836
+ }
1837
+ }
1779
1838
  }
1839
+ else {
1840
+ for (const key of scopeSkipKeys) {
1841
+ skippedInstances.add(key);
1842
+ }
1843
+ }
1844
+ }
1845
+ if ((_a = result === null || result === void 0 ? void 0 : result.payload) === null || _a === void 0 ? void 0 : _a.eventName) {
1846
+ currentEventName = result.payload.eventName;
1847
+ }
1848
+ if ((_b = result === null || result === void 0 ? void 0 : result.payload) === null || _b === void 0 ? void 0 : _b.data) {
1849
+ currentData = result.payload.data;
1850
+ }
1851
+ if ((result === null || result === void 0 ? void 0 : result.providers) !== undefined) {
1852
+ currentProviders = result.providers;
1780
1853
  }
1781
1854
  }
1782
1855
  catch (e) {
1783
- logger.error(`Plugin ${plugin.name} ${hookName} error: ${e}`);
1856
+ logger.error(`Plugin ${plugin.name} tagRoot error: ${e}`);
1784
1857
  }
1785
1858
  }
1786
1859
  return {
1787
1860
  eventName: currentEventName,
1788
1861
  data: currentData,
1789
1862
  providers: currentProviders,
1790
- skip,
1863
+ skippedInstances,
1791
1864
  };
1792
1865
  };
1793
- const processTag = async (destination, eventName, data = {}, providers, options) => {
1866
+ const processTag = async (destination, eventName, data = {}, providers, options, pluginSource) => {
1794
1867
  var _a, _b;
1795
1868
  let currentEventName = eventName;
1796
1869
  if (!getSetting(destination, 'initialized')) {
@@ -1817,15 +1890,6 @@ const processTag = async (destination, eventName, data = {}, providers, options)
1817
1890
  const consentCategory = getConsentCategories(destination);
1818
1891
  const consentSettings = getSetting(destination, 'consentSetting');
1819
1892
  const userConsent = { consentChannel, consentCategory, consentSettings };
1820
- // Grey Switch: when enabled, still send the event to the server for
1821
- // non-consented users. Browser pixels remain gated by hasUserConsent below,
1822
- // so nothing fires client-side.
1823
- const greySwitchAllows = greySwitchAllowsServerEvent({
1824
- greySwitch: getSetting(destination, 'greySwitch') || false,
1825
- greySwitchEuUk: getSetting(destination, 'greySwitchEuUk') || false,
1826
- country: requestCountry,
1827
- isEURequest,
1828
- });
1829
1893
  const ip = getSetting(destination, 'ip') || null;
1830
1894
  const userProperties = getSetting(destination, 'userProperties');
1831
1895
  if (skipZeroPurchaseEvent && isZeroPurchaseEvent({ eventName, data })) {
@@ -1846,6 +1910,7 @@ const processTag = async (destination, eventName, data = {}, providers, options)
1846
1910
  providers = rulesResult.updatedProviders;
1847
1911
  }
1848
1912
  const plugins = getPlugins(destination);
1913
+ const pluginScopes = getSetting(destination, 'pluginScopes');
1849
1914
  const pluginSettings = {
1850
1915
  userId,
1851
1916
  sessionId,
@@ -1861,26 +1926,15 @@ const processTag = async (destination, eventName, data = {}, providers, options)
1861
1926
  const pluginUtilities = {
1862
1927
  getIsNewCustomerFlag: () => getIsNewCustomerFlag(processGetData.bind(null, destination)),
1863
1928
  };
1929
+ let rootSkippedInstances = new Set();
1864
1930
  if (plugins.length > 0) {
1865
- const rootResult = await runPluginHook(plugins, 'tagRoot', {
1931
+ const rootResult = await runRootPluginHooks(plugins, {
1866
1932
  payload: { eventName: currentEventName, data, eventId },
1867
1933
  context: pluginContext,
1868
1934
  providers,
1869
1935
  settings: pluginSettings,
1870
1936
  utilities: pluginUtilities,
1871
- });
1872
- if (rootResult.skip) {
1873
- sendTag(destination, {
1874
- configuratorProcessed: true,
1875
- eventName: currentEventName,
1876
- eventId,
1877
- data,
1878
- providerData: {},
1879
- providers,
1880
- options,
1881
- });
1882
- return;
1883
- }
1937
+ }, pluginScopes, configuredTags, pluginSource);
1884
1938
  if (rootResult.providers !== undefined) {
1885
1939
  // eslint-disable-next-line no-param-reassign
1886
1940
  providers = rootResult.providers;
@@ -1888,6 +1942,7 @@ const processTag = async (destination, eventName, data = {}, providers, options)
1888
1942
  currentEventName = rootResult.eventName;
1889
1943
  // eslint-disable-next-line no-param-reassign
1890
1944
  data = rootResult.data;
1945
+ rootSkippedInstances = rootResult.skippedInstances;
1891
1946
  }
1892
1947
  if (!rulesResult.skipBrowserEvent) {
1893
1948
  const currencySettings = getSetting(destination, 'currency');
@@ -1899,69 +1954,96 @@ const processTag = async (destination, eventName, data = {}, providers, options)
1899
1954
  logger.log(`Provider ${pkg.name} is not in allow list`);
1900
1955
  continue;
1901
1956
  }
1902
- let providerEventName = currentEventName;
1903
- let channelData = data;
1904
- if (plugins.length > 0) {
1905
- const channelResult = await runPluginHook(plugins, 'tagChannel', {
1906
- payload: {
1907
- eventName: providerEventName,
1908
- data: channelData,
1909
- eventId,
1910
- },
1911
- context: pluginContext,
1912
- settings: pluginSettings,
1913
- providerId: pkg.name,
1914
- utilities: pluginUtilities,
1957
+ const providerId = pkg.name;
1958
+ const variables = getProviderVariables(destination, pkg.name);
1959
+ // Per-instance state, mirroring the edge PluginData array: each instance
1960
+ // carries its own payload and skip marker (seeded from the root skip
1961
+ // scope). A skip never short-circuits the hooks — only the final tag.
1962
+ const instanceStates = new Map();
1963
+ for (const variable of variables) {
1964
+ instanceStates.set(variable.tagName, {
1965
+ eventName: currentEventName,
1966
+ data,
1967
+ skipped: rootSkippedInstances.has(`${providerId}||${variable.tagName}`),
1915
1968
  });
1916
- if (channelResult.skip)
1917
- continue;
1918
- providerEventName = channelResult.eventName;
1919
- channelData = channelResult.data;
1920
1969
  }
1921
- const variables = getProviderVariables(destination, pkg.name);
1970
+ // Channel phase (processPluginsChannel): tagChannel runs once per
1971
+ // instance, regardless of skip/consent/geo, so its side effects and
1972
+ // additional events still fire on skipped instances.
1973
+ const channelPlugins = plugins.filter((plugin) => plugin.name !== pluginSource &&
1974
+ isPluginAllowed(pluginScopes === null || pluginScopes === void 0 ? void 0 : pluginScopes[plugin.name], providerId, null));
1975
+ if (channelPlugins.length > 0) {
1976
+ for (const variable of variables) {
1977
+ const state = instanceStates.get(variable.tagName);
1978
+ const channelResult = await runPluginHook(channelPlugins, 'tagChannel', pluginScopes, {
1979
+ payload: {
1980
+ eventName: state.eventName,
1981
+ data: state.data,
1982
+ eventId,
1983
+ },
1984
+ context: pluginContext,
1985
+ settings: pluginSettings,
1986
+ providerId,
1987
+ utilities: pluginUtilities,
1988
+ });
1989
+ if (channelResult.skip) {
1990
+ state.skipped = true;
1991
+ }
1992
+ state.eventName = channelResult.eventName;
1993
+ state.data = channelResult.data;
1994
+ }
1995
+ }
1996
+ // Instance phase (processPluginsInstance + tag send): consent/geo gate,
1997
+ // run tagInstance, then suppress only the final tag when skipped.
1922
1998
  const result = {};
1923
1999
  const executionContext = new Map();
1924
2000
  for (const variable of variables) {
1925
- if (!isProviderInstanceAllowed(providers, pkg.name, variable.tagName)) {
1926
- logger.log(`Provider instance is not allowed (${pkg.name}: ${variable.tagName})`);
2001
+ const state = instanceStates.get(variable.tagName);
2002
+ if (!isProviderInstanceAllowed(providers, providerId, variable.tagName)) {
2003
+ logger.log(`Provider instance is not allowed (${providerId}: ${variable.tagName})`);
1927
2004
  continue;
1928
2005
  }
1929
- if (!hasUserConsent(userConsent, pkg.name, variable.tagName)) {
1930
- logger.log(`Consent is missing (${pkg.name}: ${variable.tagName})`);
2006
+ if (!hasUserConsent(userConsent, providerId, variable.tagName)) {
2007
+ logger.log(`Consent is missing (${providerId}: ${variable.tagName})`);
1931
2008
  continue;
1932
2009
  }
1933
2010
  if (!doesGeoRequestMatchList(requestCountry, requestRegion, isEURequest, variable.geoRegions)) {
1934
2011
  logger.log('GEO request region does not match the filter, skipping');
1935
2012
  continue;
1936
2013
  }
1937
- let instanceEventName = providerEventName;
1938
- let instanceData = channelData;
1939
- if (plugins.length > 0) {
1940
- const instanceResult = await runPluginHook(plugins, 'tagInstance', {
2014
+ const instancePlugins = plugins.filter((plugin) => plugin.name !== pluginSource &&
2015
+ isPluginAllowed(pluginScopes === null || pluginScopes === void 0 ? void 0 : pluginScopes[plugin.name], providerId, variable.tagName));
2016
+ if (instancePlugins.length > 0) {
2017
+ const instanceResult = await runPluginHook(instancePlugins, 'tagInstance', pluginScopes, {
1941
2018
  payload: {
1942
- eventName: instanceEventName,
1943
- data: instanceData,
2019
+ eventName: state.eventName,
2020
+ data: state.data,
1944
2021
  eventId,
1945
2022
  },
1946
2023
  context: pluginContext,
1947
2024
  settings: pluginSettings,
1948
- providerId: pkg.name,
2025
+ providerId,
1949
2026
  utilities: pluginUtilities,
1950
2027
  });
1951
- if (instanceResult.skip)
1952
- continue;
1953
- instanceEventName = instanceResult.eventName;
1954
- instanceData = instanceResult.data;
2028
+ if (instanceResult.skip) {
2029
+ state.skipped = true;
2030
+ }
2031
+ state.eventName = instanceResult.eventName;
2032
+ state.data = instanceResult.data;
2033
+ }
2034
+ if (state.skipped) {
2035
+ logger.log(`Skipping event due to plugin condition (${providerId}: ${variable.tagName})`);
2036
+ continue;
1955
2037
  }
1956
- const conversion = preparePayloadWithConversion(jsonClone(instanceData), currencySettings);
2038
+ const conversion = preparePayloadWithConversion(jsonClone(state.data), currencySettings);
1957
2039
  const payload = ((_a = conversion === null || conversion === void 0 ? void 0 : conversion.providers) === null || _a === void 0 ? void 0 : _a.length) === 0 ||
1958
- ((_b = conversion === null || conversion === void 0 ? void 0 : conversion.providers) === null || _b === void 0 ? void 0 : _b.includes(pkg.name))
2040
+ ((_b = conversion === null || conversion === void 0 ? void 0 : conversion.providers) === null || _b === void 0 ? void 0 : _b.includes(providerId))
1959
2041
  ? conversion.payload
1960
- : instanceData;
2042
+ : state.data;
1961
2043
  result[variable.tagName] = pkg.tag({
1962
2044
  userId,
1963
2045
  sessionId,
1964
- eventName: instanceEventName,
2046
+ eventName: state.eventName,
1965
2047
  eventId,
1966
2048
  data: jsonClone(payload),
1967
2049
  sendTag: sendTag.bind(null, destination),
@@ -1976,16 +2058,17 @@ const processTag = async (destination, eventName, data = {}, providers, options)
1976
2058
  pageUrl: getPageUrl(destination),
1977
2059
  });
1978
2060
  }
1979
- providerData[pkg.name] = result;
2061
+ if (Object.keys(result).length > 0) {
2062
+ providerData[providerId] = result;
2063
+ }
1980
2064
  }
1981
2065
  }
1982
2066
  else {
1983
2067
  logger.log('Browser events skipped by event rules, sending tag to server only');
1984
2068
  }
1985
- if (!hasAllowedManifestTags(configuredTags, userConsent, providers) &&
1986
- !greySwitchAllows) {
1987
- return;
1988
- }
2069
+ // The server event is always forwarded to EdgeTag regardless of consent or
2070
+ // provider config; the CDN re-runs this logic server-side. Browser tags are
2071
+ // still individually gated by consent/geo/provider above.
1989
2072
  sendTag(destination, {
1990
2073
  configuratorProcessed: true,
1991
2074
  eventName: currentEventName,
@@ -2004,26 +2087,15 @@ const processTag = async (destination, eventName, data = {}, providers, options)
2004
2087
  handleTag(additionalEventName, jsonClone(originalData || {}), originalProviders, options);
2005
2088
  }
2006
2089
  };
2007
- const handleTag = (eventName, data = {}, providers, options) => {
2090
+ const handleTag = (eventName, data = {}, providers, options, pluginSource) => {
2008
2091
  if (options === null || options === void 0 ? void 0 : options.destination) {
2009
- processTag(options.destination, eventName, data, providers, options).catch(logger.error);
2092
+ processTag(options.destination, eventName, data, providers, options, pluginSource).catch(logger.error);
2010
2093
  return;
2011
2094
  }
2012
2095
  getInstances().forEach((instance) => {
2013
- processTag(instance, eventName, data, providers, options).catch(logger.error);
2096
+ processTag(instance, eventName, data, providers, options, pluginSource).catch(logger.error);
2014
2097
  });
2015
2098
  };
2016
- const hasAllowedManifestTags = (tags, consent, providersConfig) => {
2017
- for (const [pkg, tagNames] of tags) {
2018
- for (const tagName of tagNames) {
2019
- if (hasUserConsent(consent, pkg, tagName) &&
2020
- isProviderInstanceAllowed(providersConfig, pkg, tagName)) {
2021
- return true;
2022
- }
2023
- }
2024
- }
2025
- return false;
2026
- };
2027
2099
 
2028
2100
  const processData = (destination, data, providers, options) => {
2029
2101
  saveKV(destination, data);
@@ -2563,6 +2635,7 @@ const handleInit = (preferences) => {
2563
2635
  configuratorSetting: result.configuratorSetting,
2564
2636
  userProperties: result.userProperties,
2565
2637
  ip: result.ip,
2638
+ pluginScopes: result.pluginScopes,
2566
2639
  });
2567
2640
  if (result.storageId != null) {
2568
2641
  savePerKey(preferences.edgeURL, 'local', tagStorage, result.storageId, storageIdKey);
package/index.mjs CHANGED
@@ -505,77 +505,6 @@ const usStates = new Map([
505
505
  ['US-VI', 'Virgin Islands, U.S.'],
506
506
  ]);
507
507
  new Set([...isoCountries.keys(), ...usStates.keys()]);
508
- /**
509
- * ISO 3166-1 alpha-2 codes for EU member states (as of 2024)
510
- */
511
- const EU_COUNTRY_CODES = new Set([
512
- 'AT', // Austria
513
- 'BE', // Belgium
514
- 'BG', // Bulgaria
515
- 'CY', // Cyprus
516
- 'CZ', // Czech Republic
517
- 'DE', // Germany
518
- 'DK', // Denmark
519
- 'EE', // Estonia
520
- 'ES', // Spain
521
- 'FI', // Finland
522
- 'FR', // France
523
- 'GR', // Greece
524
- 'HR', // Croatia
525
- 'HU', // Hungary
526
- 'IE', // Ireland
527
- 'IT', // Italy
528
- 'LT', // Lithuania
529
- 'LU', // Luxembourg
530
- 'LV', // Latvia
531
- 'MT', // Malta
532
- 'NL', // Netherlands
533
- 'PL', // Poland
534
- 'PT', // Portugal
535
- 'RO', // Romania
536
- 'SE', // Sweden
537
- 'SI', // Slovenia
538
- 'SK', // Slovakia
539
- ]);
540
- /**
541
- * Returns true if the request originates from the EU or the UK (GB).
542
- * Cloudflare's isEUCountry flag excludes the UK post-Brexit, so GB is
543
- * checked explicitly alongside the EU member set.
544
- *
545
- * Fails closed: when the country cannot be resolved, the request is treated
546
- * as EU/UK. This is the safe default for the compliance-sensitive Grey Switch
547
- * (an unknown visitor may be in the EU/UK, so it must not slip through when
548
- * the EU/UK toggle is off).
549
- */
550
- const isEuUkRegion = (country, isEURequest) => {
551
- if (isEURequest) {
552
- return true;
553
- }
554
- if (!country) {
555
- return true;
556
- }
557
- const upper = country.toUpperCase();
558
- return upper === 'GB' || EU_COUNTRY_CODES.has(upper);
559
- };
560
- /**
561
- * Grey Switch fire rule, shared by the SDK and the CDN worker so both stay in
562
- * sync. When Grey Switch is on, EdgeTag fires server-side events for
563
- * non-consented users. The EU/UK toggle is off by default, which keeps EU/UK
564
- * visitors on normal consent unless the merchant explicitly opts those regions
565
- * in. Returns whether a server-side event should fire for a non-consented user.
566
- */
567
- const greySwitchAllowsServerEvent = ({ greySwitch, greySwitchEuUk, country, isEURequest, isGPC, }) => {
568
- if (isGPC) {
569
- return false;
570
- }
571
- if (!greySwitch) {
572
- return false;
573
- }
574
- if (greySwitchEuUk) {
575
- return true;
576
- }
577
- return !isEuUkRegion(country, isEURequest);
578
- };
579
508
  const parseCache = new Map();
580
509
  const parseRegions = (regionString) => {
581
510
  const include = new Set();
@@ -1539,7 +1468,7 @@ const getStandardPayload = (destination, payload) => {
1539
1468
  referrer: getReferrer(destination),
1540
1469
  search: getSearch(destination),
1541
1470
  locale: getLocale(),
1542
- sdkVersion: "1.58.1" ,
1471
+ sdkVersion: "1.59.0" ,
1543
1472
  ...(payload || {}),
1544
1473
  };
1545
1474
  let storage = {};
@@ -1727,6 +1656,72 @@ const sendTag = (destination, { eventName, eventId, data, providerData, provider
1727
1656
  }
1728
1657
  postRequest(getTagURL(destination, eventName, options), payload, options).catch(logger.error);
1729
1658
  };
1659
+ // Scope (scripts/providers allow-lists) is delivered per plugin via /init, keyed
1660
+ // by the plugin's identity `${providerId}||${name}` (which is carried in the
1661
+ // plugin object's `name`). A missing scope means the plugin is global.
1662
+ //
1663
+ // Mirrors the edge getAllowedPlugins per-item logic: at instance level the
1664
+ // scripts allow-list wins, otherwise fall back to the providers allow-list.
1665
+ const isPluginAllowed = (scope, providerId, instanceName) => {
1666
+ if (!scope) {
1667
+ return true;
1668
+ }
1669
+ if (instanceName && scope.scripts.length > 0) {
1670
+ return scope.scripts.includes(`${providerId}||${instanceName}`);
1671
+ }
1672
+ return scope.providers.length === 0 || scope.providers.includes(providerId);
1673
+ };
1674
+ // Mirrors the edge processPluginsRoot skip scope: a global plugin (no scope)
1675
+ // skips every browser instance (null), a scoped plugin skips only the instances
1676
+ // its scripts/providers allow-lists resolve to.
1677
+ const getScopedPluginSkipKeys = (scope, configuredTags) => {
1678
+ if (!scope || (scope.scripts.length === 0 && scope.providers.length === 0)) {
1679
+ return null;
1680
+ }
1681
+ const result = new Set();
1682
+ for (const script of scope.scripts) {
1683
+ result.add(script);
1684
+ }
1685
+ for (const providerId of scope.providers) {
1686
+ const instances = configuredTags.get(providerId);
1687
+ if (!instances)
1688
+ continue;
1689
+ for (const instanceName of instances) {
1690
+ result.add(`${providerId}||${instanceName}`);
1691
+ }
1692
+ }
1693
+ return result;
1694
+ };
1695
+ // Mirrors the edge getProviderLimits: an additional event emitted by a scoped
1696
+ // plugin is restricted to that plugin's channels/instances; a global plugin
1697
+ // imposes no restriction (undefined).
1698
+ const getPluginProviderLimits = (scope) => {
1699
+ if (!scope || (scope.scripts.length === 0 && scope.providers.length === 0)) {
1700
+ return undefined;
1701
+ }
1702
+ const limited = {};
1703
+ for (const script of scope.scripts) {
1704
+ const separator = script.indexOf('||');
1705
+ if (separator === -1) {
1706
+ continue;
1707
+ }
1708
+ const provider = script.slice(0, separator);
1709
+ const instance = script.slice(separator + 2);
1710
+ const existing = limited[provider];
1711
+ if (existing && existing !== true) {
1712
+ existing[instance] = true;
1713
+ }
1714
+ else {
1715
+ limited[provider] = { [instance]: true };
1716
+ }
1717
+ }
1718
+ for (const provider of scope.providers) {
1719
+ if (!(provider in limited)) {
1720
+ limited[provider] = true;
1721
+ }
1722
+ }
1723
+ return limited;
1724
+ };
1730
1725
  const getPlugins = (destination) => {
1731
1726
  var _a, _b, _c;
1732
1727
  try {
@@ -1737,7 +1732,19 @@ const getPlugins = (destination) => {
1737
1732
  return [];
1738
1733
  }
1739
1734
  };
1740
- const runPluginHook = async (plugins, hookName, baseParams) => {
1735
+ // Dispatches a plugin's additional events. Mirrors the edge: each event is
1736
+ // stamped with the emitting plugin (so it cannot reprocess itself) and limited
1737
+ // to that plugin's scope.
1738
+ const dispatchAdditionalEvents = (plugin, pluginScopes, events) => {
1739
+ const providerLimits = getPluginProviderLimits(pluginScopes === null || pluginScopes === void 0 ? void 0 : pluginScopes[plugin.name]);
1740
+ for (const evt of events) {
1741
+ handleTag(evt.eventName, evt.data, providerLimits, undefined, plugin.name);
1742
+ }
1743
+ };
1744
+ // Runs a channel/instance hook chain. The emitting plugin is excluded by the
1745
+ // caller's allow-list filter; additional events fire before any skip is applied
1746
+ // (matching the edge), and a skip is reported without mutating the final tag.
1747
+ const runPluginHook = async (plugins, hookName, pluginScopes, baseParams) => {
1741
1748
  var _a, _b;
1742
1749
  const payload = baseParams['payload'];
1743
1750
  let currentEventName = payload['eventName'];
@@ -1758,6 +1765,9 @@ const runPluginHook = async (plugins, hookName, baseParams) => {
1758
1765
  },
1759
1766
  variables: plugin.variables || {},
1760
1767
  });
1768
+ if (result === null || result === void 0 ? void 0 : result.additionalEvents) {
1769
+ dispatchAdditionalEvents(plugin, pluginScopes, result.additionalEvents);
1770
+ }
1761
1771
  if (result === null || result === void 0 ? void 0 : result.skipEvent) {
1762
1772
  skip = true;
1763
1773
  break;
@@ -1771,24 +1781,87 @@ const runPluginHook = async (plugins, hookName, baseParams) => {
1771
1781
  if ((result === null || result === void 0 ? void 0 : result.providers) !== undefined) {
1772
1782
  currentProviders = result.providers;
1773
1783
  }
1784
+ }
1785
+ catch (e) {
1786
+ logger.error(`Plugin ${plugin.name} ${hookName} error: ${e}`);
1787
+ }
1788
+ }
1789
+ return {
1790
+ eventName: currentEventName,
1791
+ data: currentData,
1792
+ providers: currentProviders,
1793
+ skip,
1794
+ };
1795
+ };
1796
+ // Root hooks mirror the edge processPluginsRoot: every plugin runs (except the
1797
+ // one that emitted this event), additional events are dispatched before any
1798
+ // skip is applied, and skips are scoped — a global plugin marks every browser
1799
+ // instance, a scoped one only its resolved instances. The skip is a marker:
1800
+ // channel/instance hooks still run and only the final tag is suppressed.
1801
+ const runRootPluginHooks = async (plugins, baseParams, pluginScopes, configuredTags, pluginSource) => {
1802
+ var _a, _b;
1803
+ const payload = baseParams['payload'];
1804
+ let currentEventName = payload['eventName'];
1805
+ let currentData = payload['data'];
1806
+ let currentProviders = baseParams['providers'];
1807
+ const skippedInstances = new Set();
1808
+ for (const plugin of plugins) {
1809
+ if (plugin.name === pluginSource)
1810
+ continue;
1811
+ const hook = plugin.rules.tagRoot;
1812
+ if (!hook)
1813
+ continue;
1814
+ try {
1815
+ const result = await hook({
1816
+ ...baseParams,
1817
+ payload: {
1818
+ ...payload,
1819
+ eventName: currentEventName,
1820
+ data: jsonClone(currentData),
1821
+ },
1822
+ variables: plugin.variables || {},
1823
+ });
1774
1824
  if (result === null || result === void 0 ? void 0 : result.additionalEvents) {
1775
- for (const evt of result.additionalEvents) {
1776
- handleTag(evt.eventName, evt.data, undefined, undefined);
1825
+ dispatchAdditionalEvents(plugin, pluginScopes, result.additionalEvents);
1826
+ }
1827
+ if (result === null || result === void 0 ? void 0 : result.skipEvent) {
1828
+ const scopeSkipKeys = getScopedPluginSkipKeys(pluginScopes === null || pluginScopes === void 0 ? void 0 : pluginScopes[plugin.name], configuredTags);
1829
+ if (scopeSkipKeys === null) {
1830
+ // Global plugin: skip every configured browser instance.
1831
+ for (const [provider, instances] of configuredTags) {
1832
+ for (const instanceName of instances) {
1833
+ skippedInstances.add(`${provider}||${instanceName}`);
1834
+ }
1835
+ }
1777
1836
  }
1837
+ else {
1838
+ for (const key of scopeSkipKeys) {
1839
+ skippedInstances.add(key);
1840
+ }
1841
+ }
1842
+ }
1843
+ if ((_a = result === null || result === void 0 ? void 0 : result.payload) === null || _a === void 0 ? void 0 : _a.eventName) {
1844
+ currentEventName = result.payload.eventName;
1845
+ }
1846
+ if ((_b = result === null || result === void 0 ? void 0 : result.payload) === null || _b === void 0 ? void 0 : _b.data) {
1847
+ currentData = result.payload.data;
1848
+ }
1849
+ if ((result === null || result === void 0 ? void 0 : result.providers) !== undefined) {
1850
+ currentProviders = result.providers;
1778
1851
  }
1779
1852
  }
1780
1853
  catch (e) {
1781
- logger.error(`Plugin ${plugin.name} ${hookName} error: ${e}`);
1854
+ logger.error(`Plugin ${plugin.name} tagRoot error: ${e}`);
1782
1855
  }
1783
1856
  }
1784
1857
  return {
1785
1858
  eventName: currentEventName,
1786
1859
  data: currentData,
1787
1860
  providers: currentProviders,
1788
- skip,
1861
+ skippedInstances,
1789
1862
  };
1790
1863
  };
1791
- const processTag = async (destination, eventName, data = {}, providers, options) => {
1864
+ const processTag = async (destination, eventName, data = {}, providers, options, pluginSource) => {
1792
1865
  var _a, _b;
1793
1866
  let currentEventName = eventName;
1794
1867
  if (!getSetting(destination, 'initialized')) {
@@ -1815,15 +1888,6 @@ const processTag = async (destination, eventName, data = {}, providers, options)
1815
1888
  const consentCategory = getConsentCategories(destination);
1816
1889
  const consentSettings = getSetting(destination, 'consentSetting');
1817
1890
  const userConsent = { consentChannel, consentCategory, consentSettings };
1818
- // Grey Switch: when enabled, still send the event to the server for
1819
- // non-consented users. Browser pixels remain gated by hasUserConsent below,
1820
- // so nothing fires client-side.
1821
- const greySwitchAllows = greySwitchAllowsServerEvent({
1822
- greySwitch: getSetting(destination, 'greySwitch') || false,
1823
- greySwitchEuUk: getSetting(destination, 'greySwitchEuUk') || false,
1824
- country: requestCountry,
1825
- isEURequest,
1826
- });
1827
1891
  const ip = getSetting(destination, 'ip') || null;
1828
1892
  const userProperties = getSetting(destination, 'userProperties');
1829
1893
  if (skipZeroPurchaseEvent && isZeroPurchaseEvent({ eventName, data })) {
@@ -1844,6 +1908,7 @@ const processTag = async (destination, eventName, data = {}, providers, options)
1844
1908
  providers = rulesResult.updatedProviders;
1845
1909
  }
1846
1910
  const plugins = getPlugins(destination);
1911
+ const pluginScopes = getSetting(destination, 'pluginScopes');
1847
1912
  const pluginSettings = {
1848
1913
  userId,
1849
1914
  sessionId,
@@ -1859,26 +1924,15 @@ const processTag = async (destination, eventName, data = {}, providers, options)
1859
1924
  const pluginUtilities = {
1860
1925
  getIsNewCustomerFlag: () => getIsNewCustomerFlag(processGetData.bind(null, destination)),
1861
1926
  };
1927
+ let rootSkippedInstances = new Set();
1862
1928
  if (plugins.length > 0) {
1863
- const rootResult = await runPluginHook(plugins, 'tagRoot', {
1929
+ const rootResult = await runRootPluginHooks(plugins, {
1864
1930
  payload: { eventName: currentEventName, data, eventId },
1865
1931
  context: pluginContext,
1866
1932
  providers,
1867
1933
  settings: pluginSettings,
1868
1934
  utilities: pluginUtilities,
1869
- });
1870
- if (rootResult.skip) {
1871
- sendTag(destination, {
1872
- configuratorProcessed: true,
1873
- eventName: currentEventName,
1874
- eventId,
1875
- data,
1876
- providerData: {},
1877
- providers,
1878
- options,
1879
- });
1880
- return;
1881
- }
1935
+ }, pluginScopes, configuredTags, pluginSource);
1882
1936
  if (rootResult.providers !== undefined) {
1883
1937
  // eslint-disable-next-line no-param-reassign
1884
1938
  providers = rootResult.providers;
@@ -1886,6 +1940,7 @@ const processTag = async (destination, eventName, data = {}, providers, options)
1886
1940
  currentEventName = rootResult.eventName;
1887
1941
  // eslint-disable-next-line no-param-reassign
1888
1942
  data = rootResult.data;
1943
+ rootSkippedInstances = rootResult.skippedInstances;
1889
1944
  }
1890
1945
  if (!rulesResult.skipBrowserEvent) {
1891
1946
  const currencySettings = getSetting(destination, 'currency');
@@ -1897,69 +1952,96 @@ const processTag = async (destination, eventName, data = {}, providers, options)
1897
1952
  logger.log(`Provider ${pkg.name} is not in allow list`);
1898
1953
  continue;
1899
1954
  }
1900
- let providerEventName = currentEventName;
1901
- let channelData = data;
1902
- if (plugins.length > 0) {
1903
- const channelResult = await runPluginHook(plugins, 'tagChannel', {
1904
- payload: {
1905
- eventName: providerEventName,
1906
- data: channelData,
1907
- eventId,
1908
- },
1909
- context: pluginContext,
1910
- settings: pluginSettings,
1911
- providerId: pkg.name,
1912
- utilities: pluginUtilities,
1955
+ const providerId = pkg.name;
1956
+ const variables = getProviderVariables(destination, pkg.name);
1957
+ // Per-instance state, mirroring the edge PluginData array: each instance
1958
+ // carries its own payload and skip marker (seeded from the root skip
1959
+ // scope). A skip never short-circuits the hooks — only the final tag.
1960
+ const instanceStates = new Map();
1961
+ for (const variable of variables) {
1962
+ instanceStates.set(variable.tagName, {
1963
+ eventName: currentEventName,
1964
+ data,
1965
+ skipped: rootSkippedInstances.has(`${providerId}||${variable.tagName}`),
1913
1966
  });
1914
- if (channelResult.skip)
1915
- continue;
1916
- providerEventName = channelResult.eventName;
1917
- channelData = channelResult.data;
1918
1967
  }
1919
- const variables = getProviderVariables(destination, pkg.name);
1968
+ // Channel phase (processPluginsChannel): tagChannel runs once per
1969
+ // instance, regardless of skip/consent/geo, so its side effects and
1970
+ // additional events still fire on skipped instances.
1971
+ const channelPlugins = plugins.filter((plugin) => plugin.name !== pluginSource &&
1972
+ isPluginAllowed(pluginScopes === null || pluginScopes === void 0 ? void 0 : pluginScopes[plugin.name], providerId, null));
1973
+ if (channelPlugins.length > 0) {
1974
+ for (const variable of variables) {
1975
+ const state = instanceStates.get(variable.tagName);
1976
+ const channelResult = await runPluginHook(channelPlugins, 'tagChannel', pluginScopes, {
1977
+ payload: {
1978
+ eventName: state.eventName,
1979
+ data: state.data,
1980
+ eventId,
1981
+ },
1982
+ context: pluginContext,
1983
+ settings: pluginSettings,
1984
+ providerId,
1985
+ utilities: pluginUtilities,
1986
+ });
1987
+ if (channelResult.skip) {
1988
+ state.skipped = true;
1989
+ }
1990
+ state.eventName = channelResult.eventName;
1991
+ state.data = channelResult.data;
1992
+ }
1993
+ }
1994
+ // Instance phase (processPluginsInstance + tag send): consent/geo gate,
1995
+ // run tagInstance, then suppress only the final tag when skipped.
1920
1996
  const result = {};
1921
1997
  const executionContext = new Map();
1922
1998
  for (const variable of variables) {
1923
- if (!isProviderInstanceAllowed(providers, pkg.name, variable.tagName)) {
1924
- logger.log(`Provider instance is not allowed (${pkg.name}: ${variable.tagName})`);
1999
+ const state = instanceStates.get(variable.tagName);
2000
+ if (!isProviderInstanceAllowed(providers, providerId, variable.tagName)) {
2001
+ logger.log(`Provider instance is not allowed (${providerId}: ${variable.tagName})`);
1925
2002
  continue;
1926
2003
  }
1927
- if (!hasUserConsent(userConsent, pkg.name, variable.tagName)) {
1928
- logger.log(`Consent is missing (${pkg.name}: ${variable.tagName})`);
2004
+ if (!hasUserConsent(userConsent, providerId, variable.tagName)) {
2005
+ logger.log(`Consent is missing (${providerId}: ${variable.tagName})`);
1929
2006
  continue;
1930
2007
  }
1931
2008
  if (!doesGeoRequestMatchList(requestCountry, requestRegion, isEURequest, variable.geoRegions)) {
1932
2009
  logger.log('GEO request region does not match the filter, skipping');
1933
2010
  continue;
1934
2011
  }
1935
- let instanceEventName = providerEventName;
1936
- let instanceData = channelData;
1937
- if (plugins.length > 0) {
1938
- const instanceResult = await runPluginHook(plugins, 'tagInstance', {
2012
+ const instancePlugins = plugins.filter((plugin) => plugin.name !== pluginSource &&
2013
+ isPluginAllowed(pluginScopes === null || pluginScopes === void 0 ? void 0 : pluginScopes[plugin.name], providerId, variable.tagName));
2014
+ if (instancePlugins.length > 0) {
2015
+ const instanceResult = await runPluginHook(instancePlugins, 'tagInstance', pluginScopes, {
1939
2016
  payload: {
1940
- eventName: instanceEventName,
1941
- data: instanceData,
2017
+ eventName: state.eventName,
2018
+ data: state.data,
1942
2019
  eventId,
1943
2020
  },
1944
2021
  context: pluginContext,
1945
2022
  settings: pluginSettings,
1946
- providerId: pkg.name,
2023
+ providerId,
1947
2024
  utilities: pluginUtilities,
1948
2025
  });
1949
- if (instanceResult.skip)
1950
- continue;
1951
- instanceEventName = instanceResult.eventName;
1952
- instanceData = instanceResult.data;
2026
+ if (instanceResult.skip) {
2027
+ state.skipped = true;
2028
+ }
2029
+ state.eventName = instanceResult.eventName;
2030
+ state.data = instanceResult.data;
2031
+ }
2032
+ if (state.skipped) {
2033
+ logger.log(`Skipping event due to plugin condition (${providerId}: ${variable.tagName})`);
2034
+ continue;
1953
2035
  }
1954
- const conversion = preparePayloadWithConversion(jsonClone(instanceData), currencySettings);
2036
+ const conversion = preparePayloadWithConversion(jsonClone(state.data), currencySettings);
1955
2037
  const payload = ((_a = conversion === null || conversion === void 0 ? void 0 : conversion.providers) === null || _a === void 0 ? void 0 : _a.length) === 0 ||
1956
- ((_b = conversion === null || conversion === void 0 ? void 0 : conversion.providers) === null || _b === void 0 ? void 0 : _b.includes(pkg.name))
2038
+ ((_b = conversion === null || conversion === void 0 ? void 0 : conversion.providers) === null || _b === void 0 ? void 0 : _b.includes(providerId))
1957
2039
  ? conversion.payload
1958
- : instanceData;
2040
+ : state.data;
1959
2041
  result[variable.tagName] = pkg.tag({
1960
2042
  userId,
1961
2043
  sessionId,
1962
- eventName: instanceEventName,
2044
+ eventName: state.eventName,
1963
2045
  eventId,
1964
2046
  data: jsonClone(payload),
1965
2047
  sendTag: sendTag.bind(null, destination),
@@ -1974,16 +2056,17 @@ const processTag = async (destination, eventName, data = {}, providers, options)
1974
2056
  pageUrl: getPageUrl(destination),
1975
2057
  });
1976
2058
  }
1977
- providerData[pkg.name] = result;
2059
+ if (Object.keys(result).length > 0) {
2060
+ providerData[providerId] = result;
2061
+ }
1978
2062
  }
1979
2063
  }
1980
2064
  else {
1981
2065
  logger.log('Browser events skipped by event rules, sending tag to server only');
1982
2066
  }
1983
- if (!hasAllowedManifestTags(configuredTags, userConsent, providers) &&
1984
- !greySwitchAllows) {
1985
- return;
1986
- }
2067
+ // The server event is always forwarded to EdgeTag regardless of consent or
2068
+ // provider config; the CDN re-runs this logic server-side. Browser tags are
2069
+ // still individually gated by consent/geo/provider above.
1987
2070
  sendTag(destination, {
1988
2071
  configuratorProcessed: true,
1989
2072
  eventName: currentEventName,
@@ -2002,26 +2085,15 @@ const processTag = async (destination, eventName, data = {}, providers, options)
2002
2085
  handleTag(additionalEventName, jsonClone(originalData || {}), originalProviders, options);
2003
2086
  }
2004
2087
  };
2005
- const handleTag = (eventName, data = {}, providers, options) => {
2088
+ const handleTag = (eventName, data = {}, providers, options, pluginSource) => {
2006
2089
  if (options === null || options === void 0 ? void 0 : options.destination) {
2007
- processTag(options.destination, eventName, data, providers, options).catch(logger.error);
2090
+ processTag(options.destination, eventName, data, providers, options, pluginSource).catch(logger.error);
2008
2091
  return;
2009
2092
  }
2010
2093
  getInstances().forEach((instance) => {
2011
- processTag(instance, eventName, data, providers, options).catch(logger.error);
2094
+ processTag(instance, eventName, data, providers, options, pluginSource).catch(logger.error);
2012
2095
  });
2013
2096
  };
2014
- const hasAllowedManifestTags = (tags, consent, providersConfig) => {
2015
- for (const [pkg, tagNames] of tags) {
2016
- for (const tagName of tagNames) {
2017
- if (hasUserConsent(consent, pkg, tagName) &&
2018
- isProviderInstanceAllowed(providersConfig, pkg, tagName)) {
2019
- return true;
2020
- }
2021
- }
2022
- }
2023
- return false;
2024
- };
2025
2097
 
2026
2098
  const processData = (destination, data, providers, options) => {
2027
2099
  saveKV(destination, data);
@@ -2561,6 +2633,7 @@ const handleInit = (preferences) => {
2561
2633
  configuratorSetting: result.configuratorSetting,
2562
2634
  userProperties: result.userProperties,
2563
2635
  ip: result.ip,
2636
+ pluginScopes: result.pluginScopes,
2564
2637
  });
2565
2638
  if (result.storageId != null) {
2566
2639
  savePerKey(preferences.edgeURL, 'local', tagStorage, result.storageId, storageIdKey);
package/internal.d.ts CHANGED
@@ -79,4 +79,5 @@ type InitResponse = {
79
79
  isNewCustomer: boolean | undefined
80
80
  }
81
81
  ip: string | null
82
+ pluginScopes?: Record<string, { scripts: string[]; providers: string[] }>
82
83
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blotoutio/edgetag-sdk-js",
3
- "version": "1.58.1",
3
+ "version": "1.59.0",
4
4
  "description": "JS SDK for EdgeTag",
5
5
  "author": "Blotout",
6
6
  "license": "MIT",