@aglyn/aglyn 1.0.0-beta.233 → 1.0.0-beta.235

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/package.json +11 -11
  2. package/src/lib/app-utils/activity-labels.d.ts +117 -0
  3. package/src/lib/app-utils/activity-labels.js +343 -0
  4. package/src/lib/app-utils/activity-labels.js.map +1 -0
  5. package/src/lib/app-utils/admin-audit-index.d.ts +11 -0
  6. package/src/lib/app-utils/admin-audit-index.js +12 -1
  7. package/src/lib/app-utils/admin-audit-index.js.map +1 -1
  8. package/src/lib/app-utils/advertising-consent.d.ts +111 -0
  9. package/src/lib/app-utils/advertising-consent.js +216 -0
  10. package/src/lib/app-utils/advertising-consent.js.map +1 -0
  11. package/src/lib/app-utils/advertising-events.d.ts +98 -0
  12. package/src/lib/app-utils/advertising-events.js +340 -0
  13. package/src/lib/app-utils/advertising-events.js.map +1 -0
  14. package/src/lib/app-utils/advertising-tag-mounts.js +15 -8
  15. package/src/lib/app-utils/advertising-tag-mounts.js.map +1 -1
  16. package/src/lib/app-utils/advertising-tags.d.ts +54 -8
  17. package/src/lib/app-utils/advertising-tags.js +126 -28
  18. package/src/lib/app-utils/advertising-tags.js.map +1 -1
  19. package/src/lib/app-utils/analytics-events.d.ts +15 -86
  20. package/src/lib/app-utils/analytics-events.js +21 -6
  21. package/src/lib/app-utils/analytics-events.js.map +1 -1
  22. package/src/lib/app-utils/consent-banner-ui.d.ts +14 -0
  23. package/src/lib/app-utils/consent-banner-ui.js +23 -2
  24. package/src/lib/app-utils/consent-banner-ui.js.map +1 -1
  25. package/src/lib/app-utils/docs-help-section-excerpt-text.d.ts +14 -0
  26. package/src/lib/app-utils/docs-help-section-excerpt-text.js +31 -0
  27. package/src/lib/app-utils/docs-help-section-excerpt-text.js.map +1 -0
  28. package/src/lib/app-utils/docs-help-section-excerpt.d.ts +13 -0
  29. package/src/lib/app-utils/docs-help-section-excerpt.js +37 -0
  30. package/src/lib/app-utils/docs-help-section-excerpt.js.map +1 -0
  31. package/src/lib/app-utils/docs-help-sections.generated.d.ts +31 -0
  32. package/src/lib/app-utils/docs-help-sections.generated.js +660 -0
  33. package/src/lib/app-utils/docs-help-sections.generated.js.map +1 -0
  34. package/src/lib/app-utils/docs-help.d.ts +16 -4
  35. package/src/lib/app-utils/docs-help.generated.d.ts +171 -34
  36. package/src/lib/app-utils/docs-help.generated.js +1104 -3
  37. package/src/lib/app-utils/docs-help.generated.js.map +1 -1
  38. package/src/lib/app-utils/docs-help.js +25 -6
  39. package/src/lib/app-utils/docs-help.js.map +1 -1
  40. package/src/lib/app-utils/docs-index.generated.js +1008 -91
  41. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  42. package/src/lib/app-utils/health-report.js +12 -0
  43. package/src/lib/app-utils/health-report.js.map +1 -1
  44. package/src/lib/app-utils/plugin-release-flags.generated.d.ts +1 -1
  45. package/src/lib/app-utils/plugin-release-flags.generated.js +7 -1
  46. package/src/lib/app-utils/plugin-release-flags.generated.js.map +1 -1
  47. package/src/lib/app-utils/realm-host-surface.generated.js +1 -0
  48. package/src/lib/app-utils/realm-host-surface.generated.js.map +1 -1
  49. package/src/lib/app-utils/variables.d.ts +2 -0
  50. package/src/lib/app-utils/variables.js +3 -0
  51. package/src/lib/app-utils/variables.js.map +1 -1
  52. package/src/lib/app-utils/visitor-consent.d.ts +34 -0
  53. package/src/lib/app-utils/visitor-consent.js +49 -2
  54. package/src/lib/app-utils/visitor-consent.js.map +1 -1
  55. package/src/lib/app-utils/where-used-summary.d.ts +41 -0
  56. package/src/lib/app-utils/where-used-summary.js +33 -0
  57. package/src/lib/app-utils/where-used-summary.js.map +1 -0
  58. package/src/lib/app-utils/where-used.d.ts +2 -20
  59. package/src/lib/app-utils/where-used.js +13 -12
  60. package/src/lib/app-utils/where-used.js.map +1 -1
  61. package/src/lib/foundation/definitions/platform.types.d.ts +33 -0
  62. package/src/lib/foundation/definitions/platform.types.js.map +1 -1
  63. package/src/lib/plugin-manager/first-party-plugins.generated.d.ts +7 -0
  64. package/src/lib/plugin-manager/first-party-plugins.generated.js +166 -2
  65. package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
  66. package/src/lib/plugin-manager/plugin-activity-actions.d.ts +51 -0
  67. package/src/lib/plugin-manager/plugin-activity-actions.js +43 -0
  68. package/src/lib/plugin-manager/plugin-activity-actions.js.map +1 -1
  69. package/src/lib/plugin-manager/plugin-advertising-conversions.d.ts +84 -0
  70. package/src/lib/plugin-manager/plugin-advertising-conversions.js +86 -0
  71. package/src/lib/plugin-manager/plugin-advertising-conversions.js.map +1 -0
  72. package/src/lib/plugin-manager/plugin-config.d.ts +23 -0
  73. package/src/lib/plugin-manager/plugin-config.js.map +1 -1
  74. package/src/lib/plugin-manager/plugin-local-deliveries.d.ts +161 -0
  75. package/src/lib/plugin-manager/plugin-local-deliveries.js +46 -0
  76. package/src/lib/plugin-manager/plugin-local-deliveries.js.map +1 -0
  77. package/src/lib/plugin-manager/plugin-media-ingest.d.ts +87 -0
  78. package/src/lib/plugin-manager/plugin-media-ingest.js +34 -0
  79. package/src/lib/plugin-manager/plugin-media-ingest.js.map +1 -0
  80. package/src/lib/plugin-manager/plugin-order-email-copies.d.ts +93 -0
  81. package/src/lib/plugin-manager/plugin-order-email-copies.js +104 -0
  82. package/src/lib/plugin-manager/plugin-order-email-copies.js.map +1 -0
  83. package/src/lib/plugin-manager/plugin-shipment-records.d.ts +7 -0
  84. package/src/lib/plugin-manager/plugin-shipment-records.js.map +1 -1
  85. package/src/lib/plugin-manager/plugin-site-csp.d.ts +58 -0
  86. package/src/lib/plugin-manager/plugin-site-csp.js +103 -0
  87. package/src/lib/plugin-manager/plugin-site-csp.js.map +1 -0
  88. package/src/lib/plugin-manager/realm-host-aglyn.generated.js +1 -0
  89. package/src/lib/plugin-manager/realm-host-aglyn.generated.js.map +1 -1
  90. package/src/lib/plugin-manager/site-page-hooks.d.ts +13 -0
  91. package/src/lib/plugin-manager/site-page-hooks.js +31 -0
  92. package/src/lib/plugin-manager/site-page-hooks.js.map +1 -1
  93. package/src/lib/plugin-manager/stock-photo-provider.d.ts +141 -0
  94. package/src/lib/plugin-manager/stock-photo-provider.js +59 -0
  95. package/src/lib/plugin-manager/stock-photo-provider.js.map +1 -0
@@ -57,12 +57,15 @@
57
57
  *
58
58
  * ## The six conditions, all independent, all required
59
59
  *
60
- * 1. **{@link isPlatformMarketingHost}** — Aglyn's own marketing site, never a
61
- * customer's. This is the DPA §3.2 boundary: Aglyn promises customers it
62
- * does not "sell"/"share" Customer Personal Data, and an ad pixel on a
63
- * customer's published site — or in the console — would breach that. Note
64
- * the console needs no condition of its own: it does not render the tenant
65
- * runtime's analytics component at all, so there is no mount point to gate.
60
+ * 1. **Whose tags.** On Aglyn's own marketing site
61
+ * ({@link isPlatformMarketingHost}), ours; on a customer's site, only the
62
+ * tags that site's owner configured with their own account ids
63
+ * (AGL-3694) — see "A site owner's OWN tags" below. Never OUR tag on a
64
+ * customer's site: that is the DPA §3.2 boundary, Aglyn not "selling" or
65
+ * "sharing" Customer Personal Data for its own advertising, and the
66
+ * platform's ids are on no host but ours. Note the console needs no
67
+ * condition of its own: it does not render the tenant runtime's analytics
68
+ * component at all, so there is no mount point to gate.
66
69
  * 2. **{@link analyticsMayEmit}** — a real production deployment. AGL-2067's
67
70
  * finding was that `next dev` and Vercel PREVIEW builds resolve
68
71
  * `aglyn.com`'s host document exactly as production does; without this a
@@ -105,22 +108,28 @@
105
108
  * this the hatch reopens precisely the hole condition 2 closes, which is why
106
109
  * `INTERNAL_TRAFFIC_FORCED_SNIPPET` exists on the GA side.
107
110
  *
108
- * ## What is NOT wired, and why that is the point
111
+ * ## A site owner's OWN tags (AGL-3694)
109
112
  *
110
- * No host document carries an `analytics.adTags` entry. The Meta descriptor
111
- * below is a description of how that vendor would be loaded and — more
112
- * importantly — how it would be torn down; it is inert until someone writes a
113
- * pixel id onto the `aglyn-marketing` host. That makes deployment a DATA
114
- * change reviewed on its own merits, exactly as configuring GA is, rather than
115
- * something that rides along with this mechanism.
116
- *
117
- * ## Why customer sites are excluded outright rather than offered this
118
- *
119
- * A customer-facing "run your own ad pixel" feature is a different product
113
+ * Condition 1 above is about OUR tags. A customer's site may mount the tags its
114
+ * owner configured — their own Meta pixel, TikTok pixel, Pinterest tag, Google
115
+ * Ads id or LinkedIn partner id, written on Site → Setup → Tracking — and
116
+ * nothing else: the account ids come from that site's own host document, so
117
+ * every hit lands in the owner's own ad account, under the owner's
118
+ * instruction, exactly as the site's own GA4 measurement id does. That is the
119
+ * product decision the comment here used to leave open ("a different product
120
120
  * decision with its own consent copy, its own cookie-policy rows and its own
121
- * DPA implications. Nothing here forecloses it. What this module must not do
122
- * is arrive as that feature by accident.
123
- */ import { GOOGLE_ADS_ID_PATTERN, LINKEDIN_PARTNER_ID_PATTERN, META_PIXEL_ID_PATTERN } from "./visitor-consent.js";
121
+ * DPA implications"), taken with those rows: the cookie inventory names every
122
+ * vendor below, the docs say what loads and when, and the consent conditions
123
+ * 2–6 apply to a customer's site unchanged. A customer's tag is marked
124
+ * {@link ADVERTISING_EVENTS_ATTRIBUTE} as well, which is what lets the site's
125
+ * conversion events reach it (`advertising-events.ts`) and keeps them away
126
+ * from ours.
127
+ *
128
+ * What still never happens is OUR tag on a customer's site: the platform's
129
+ * account ids live only on the `aglyn-marketing` host, which only
130
+ * `isPlatformMarketingHost` matches.
131
+ */ import { GOOGLE_ADS_ID_PATTERN, LINKEDIN_PARTNER_ID_PATTERN, META_PIXEL_ID_PATTERN, PINTEREST_TAG_ID_PATTERN, TIKTOK_PIXEL_ID_PATTERN } from "./visitor-consent.js";
132
+ import { ADVERTISING_EVENTS_ATTRIBUTE, ADVERTISING_TAG_ATTRIBUTE } from "./advertising-events.js";
124
133
  import { analyticsEnvironmentForcesInternal, analyticsMayEmit, readAnalyticsEnvironment } from "./analytics-environment.js";
125
134
  import { readInternalTrafficOverride } from "./internal-traffic.js";
126
135
  import { isPlatformMarketingHost } from "./platform-marketing-host.js";
@@ -133,7 +142,10 @@ import { ADVERTISING_COOKIE_PREFIXES, advertisingGrantedByRecord, clearCookiesWi
133
142
  * their own site's Custom HTML is never touched by our withdrawal path — we
134
143
  * did not load it, we do not know what basis it runs on, and silently killing
135
144
  * it would be us configuring a customer's site. Its value is the vendor id.
136
- */ export const ADVERTISING_TAG_ATTRIBUTE = 'data-aglyn-ad-tag';
145
+ *
146
+ * Declared in `advertising-events.ts` (AGL-3694), which finds a mounted tag by
147
+ * it without importing this module.
148
+ */ export { ADVERTISING_EVENTS_ATTRIBUTE, ADVERTISING_TAG_ATTRIBUTE };
137
149
  /**
138
150
  * Meta (Facebook/Instagram) Pixel.
139
151
  *
@@ -341,13 +353,91 @@ import { ADVERTISING_COOKIE_PREFIXES, advertisingGrantedByRecord, clearCookiesWi
341
353
  }
342
354
  }
343
355
  };
356
+ /**
357
+ * TikTok Pixel (AGL-3694) — a site owner's own, from their TikTok Ads Manager.
358
+ *
359
+ * TikTok's published base code is a loader that inserts `events.js` itself.
360
+ * Here the QUEUE half of it runs as the boot snippet and the library is the
361
+ * marked `<script>` the mount renders, so the teardown can find it — the same
362
+ * split every vendor in this registry has. `ttq.load` is reduced accordingly
363
+ * to the registration it does (`_i`, `_t`, `_o` for the pixel code), without
364
+ * the insertion; `events.js` reads that registration when it arrives.
365
+ *
366
+ * `grantConsent`/`revokeConsent` are TikTok's documented consent controls,
367
+ * and are the `fbq('consent', …)` analogue for the AGL-1608 order: told
368
+ * first, then removed, then swept.
369
+ *
370
+ * Cookies: `_ttp` (the browser id), `_tt_enable_cookie` (the cookie probe) and
371
+ * `ttcsid` / `ttcsid_<pixel>` (the session), all first-party at the
372
+ * registrable domain, which the ladder sweep reaches.
373
+ */ export const TIKTOK_PIXEL_VENDOR = {
374
+ id: 'tiktok',
375
+ label: 'TikTok Pixel',
376
+ accountIdPattern: TIKTOK_PIXEL_ID_PATTERN,
377
+ scriptSrc: 'https://analytics.tiktok.com/i18n/pixel/events.js',
378
+ scriptSrcFor: (accountId)=>`https://analytics.tiktok.com/i18n/pixel/events.js?sdkid=${accountId}&lib=ttq`,
379
+ scriptMatch: 'analytics.tiktok.com',
380
+ cookiePrefixes: [
381
+ '_ttp',
382
+ '_tt_enable_cookie',
383
+ 'ttcsid'
384
+ ],
385
+ bootSnippet: (accountId)=>"!function(w,t){w.TiktokAnalyticsObject=t;var ttq=w[t]=w[t]||[];" + 'ttq.methods=["page","track","identify","instances","debug","on","off",' + '"once","ready","alias","group","enableCookie","disableCookie",' + '"holdConsent","revokeConsent","grantConsent"];' + 'ttq.setAndDefer=function(t,e){t[e]=function(){' + 't.push([e].concat(Array.prototype.slice.call(arguments,0)))}};' + 'for(var i=0;i<ttq.methods.length;i++)ttq.setAndDefer(ttq,ttq.methods[i]);' + 'ttq.instance=function(t){for(var e=ttq._i[t]||[],n=0;n<ttq.methods.length;n++)' + 'ttq.setAndDefer(e,ttq.methods[n]);return e};' + 'ttq.load=function(e,n){var r="https://analytics.tiktok.com/i18n/pixel/events.js";' + 'ttq._i=ttq._i||{};ttq._i[e]=[];ttq._i[e]._u=r;ttq._t=ttq._t||{};' + 'ttq._t[e]=+new Date;ttq._o=ttq._o||{};ttq._o[e]=n||{}}}' + "(window,'ttq');" + `ttq.load('${accountId}');` + 'ttq.grantConsent();' + 'ttq.page();',
386
+ setConsent: (scope, granted)=>{
387
+ try {
388
+ const ttq = scope.ttq;
389
+ const call = ttq == null ? void 0 : ttq[granted ? 'grantConsent' : 'revokeConsent'];
390
+ if (typeof call === 'function') call.call(ttq);
391
+ } catch (unused) {
392
+ // The element removal and the cookie sweep still stand.
393
+ }
394
+ }
395
+ };
396
+ /**
397
+ * Pinterest Tag (AGL-3694) — a site owner's own, from Pinterest Ads.
398
+ *
399
+ * The published base code is a queue shim plus an inserted `core.js`; as with
400
+ * TikTok the shim is the boot and the library is our marked element.
401
+ *
402
+ * Pinterest publishes no consent call for the tag. On withdrawal the queue
403
+ * function is replaced with one that drops every call, so a late or cached
404
+ * `core.js` finds nothing to report; the element removal and the sweep are
405
+ * the substance, as they are for LinkedIn.
406
+ *
407
+ * Cookies: `_pin_unauth` (the browser id), `_pinterest_ct_ua` /
408
+ * `_pinterest_ct_rt` (conversion tracking) and `_epik` / `_derived_epik` (the
409
+ * click id) — first-party; `_pinterest_sess` lives on pinterest.com and only
410
+ * Pinterest can clear it.
411
+ */ export const PINTEREST_TAG_VENDOR = {
412
+ id: 'pinterest',
413
+ label: 'Pinterest Tag',
414
+ accountIdPattern: PINTEREST_TAG_ID_PATTERN,
415
+ scriptSrc: 'https://s.pinimg.com/ct/core.js',
416
+ scriptMatch: 's.pinimg.com',
417
+ cookiePrefixes: [
418
+ '_pin_unauth',
419
+ '_pinterest_ct',
420
+ '_epik',
421
+ '_derived_epik'
422
+ ],
423
+ bootSnippet: (accountId)=>'!function(){if(!window.pintrk){window.pintrk=function(){' + 'window.pintrk.queue.push(Array.prototype.slice.call(arguments))};' + 'var n=window.pintrk;n.queue=[];n.version="3.0"}}();' + `pintrk('load','${accountId}');` + "pintrk('page');",
424
+ setConsent: (scope, granted)=>{
425
+ try {
426
+ if (!granted) scope.pintrk = function pintrkWithdrawn() {};
427
+ } catch (unused) {
428
+ // Same standing as the others: the teardown does not depend on this.
429
+ }
430
+ }
431
+ };
344
432
  /**
345
433
  * Every vendor this gate knows how to tear down — which is a SUPERSET of the
346
434
  * vendors it knows how to load, now that a sweep-only member exists.
347
435
  */ export const ADVERTISING_VENDORS = [
348
436
  META_PIXEL_VENDOR,
349
437
  GOOGLE_ADS_VENDOR,
350
- LINKEDIN_INSIGHT_VENDOR
438
+ LINKEDIN_INSIGHT_VENDOR,
439
+ TIKTOK_PIXEL_VENDOR,
440
+ PINTEREST_TAG_VENDOR
351
441
  ];
352
442
  /**
353
443
  * The whole verdict: which advertising tags may exist in this document, for
@@ -367,7 +457,10 @@ import { ADVERTISING_COOKIE_PREFIXES, advertisingGrantedByRecord, clearCookiesWi
367
457
  * exercised one way is the shape that ships broken (AGL-2067).
368
458
  */ export function resolveAdvertisingTags(host, stored, env = readAnalyticsEnvironment(), internal = readInternalTrafficOverride()) {
369
459
  var _host_analytics;
370
- if (isPlatformMarketingHost(host) === false) return [];
460
+ // Condition 1, as of AGL-3694: OUR surface, or a customer's site mounting
461
+ // the tags ITS OWNER configured. Either way the account ids come from this
462
+ // host's own document, so nothing here can put one site's tag on another.
463
+ const siteOwned = isPlatformMarketingHost(host) === false;
371
464
  if (analyticsMayEmit(env) === false) return [];
372
465
  if (hostConsentRequired(host) === false) return [];
373
466
  if (advertisingGrantedByRecord(host, stored) === false) return [];
@@ -394,7 +487,11 @@ import { ADVERTISING_COOKIE_PREFIXES, advertisingGrantedByRecord, clearCookiesWi
394
487
  if (vendor.sweepOnly || !vendor.accountIdPattern) continue;
395
488
  const accountId = String((_configured_vendor_id = configured[vendor.id]) != null ? _configured_vendor_id : '');
396
489
  if (vendor.accountIdPattern.test(accountId)) {
397
- tags.push({
490
+ tags.push(siteOwned ? {
491
+ vendor,
492
+ accountId,
493
+ siteOwned: true
494
+ } : {
398
495
  vendor,
399
496
  accountId
400
497
  });
@@ -453,10 +550,11 @@ import { ADVERTISING_COOKIE_PREFIXES, advertisingGrantedByRecord, clearCookiesWi
453
550
  * 3. **Sweep the cookies.** Last, because steps 1 and 2 are what stop them
454
551
  * coming straight back (AGL-1608).
455
552
  *
456
- * Acts only on tags carrying {@link ADVERTISING_TAG_ATTRIBUTE}. On a customer
457
- * site — where this module never loaded anything — it is a no-op that touches
458
- * no cookie, which is the only correct behaviour: their pixel, if any, runs on
459
- * a basis that is not ours to withdraw.
553
+ * Acts only on tags carrying {@link ADVERTISING_TAG_ATTRIBUTE}: the tags this
554
+ * module mounted, which on a customer's site are the ones its owner configured
555
+ * on Setup → Tracking (AGL-3694). A pixel pasted into Custom HTML carries no
556
+ * mark and is never touched — it runs on a basis that is not ours to
557
+ * withdraw.
460
558
  */ export function revokeAdvertisingTags(hostname) {
461
559
  if (typeof window === 'undefined') return [];
462
560
  const acted = [];
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/advertising-tags.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The VENDOR-AGNOSTIC advertising-tag gate.\n *\n * ## The hole this closes\n *\n * Until now the consent machinery could gate exactly one thing: Google. The\n * `advertising` category has existed since AGL-1649 and it is honest — a\n * refusal always wins, and a host that never asked gets nothing — but the\n * only ENFORCEMENT it ever reached was {@link consentModeSignals}, four\n * strings handed to `gtag`. A non-Google tag could not be consent-gated at\n * all, because nothing in the repo knew how to gate one. Deciding to deploy a\n * Meta Pixel with that as the state of the art would have meant either\n * shipping it ungated or inventing a second consent implementation beside the\n * first — the outcome AGL-1579 explicitly ruled out.\n *\n * This module is the missing enforcement channel, built for vendors in\n * general and populated with one descriptor. **Deploying the Meta Pixel is a\n * separate decision and this module does not take it** — see \"What is NOT\n * wired\" below.\n *\n * ## Why the gate is STRUCTURAL\n *\n * The lesson AGL-1498 paid for and AGL-1608 paid for again: a boolean checked\n * before each call is not a gate. A resident tag fires on its own — GA4\n * enhanced measurement re-created `_ga_YW5PG16YTM` on aglyn.com after a\n * hand-run sweep to zero cookies, from one scroll to the footer — so the only\n * enforcement that holds is the tag never being in the document.\n *\n * So the shape here mirrors the GA gate exactly:\n *\n * - {@link resolveAdvertisingTags} is a PURE verdict over the host document\n * and the visitor's stored record. It returns the tags that may exist. An\n * ungranted visitor gets an empty array, the component renders no\n * `<Script>`, and no request goes to the vendor. Not loaded-then-suppressed:\n * not loaded.\n * - {@link revokeAdvertisingTags} exists because the gate CANNOT unload a tag.\n * Dropping the element on a mid-pageview withdrawal leaves the vendor's\n * script executing, so withdrawal additionally revokes consent on the\n * resident tag, removes its script elements, and sweeps its cookies — in\n * that order, for the AGL-1608 reason: sweeping first deletes the cookies\n * and immediately gets them back.\n *\n * ## The six conditions, all independent, all required\n *\n * 1. **{@link isPlatformMarketingHost}** — Aglyn's own marketing site, never a\n * customer's. This is the DPA §3.2 boundary: Aglyn promises customers it\n * does not \"sell\"/\"share\" Customer Personal Data, and an ad pixel on a\n * customer's published site — or in the console — would breach that. Note\n * the console needs no condition of its own: it does not render the tenant\n * runtime's analytics component at all, so there is no mount point to gate.\n * 2. **{@link analyticsMayEmit}** — a real production deployment. AGL-2067's\n * finding was that `next dev` and Vercel PREVIEW builds resolve\n * `aglyn.com`'s host document exactly as production does; without this a\n * preview deploy would build real retargeting audiences out of our own\n * engineers.\n * 3. **{@link hostConsentRequired}** — the consent machinery is actually live\n * for this site. A host that switched the tool off (`consent.disabled`)\n * runs their own CMP and ours has no answer on file to act on.\n * 4. **{@link advertisingGrantedByRecord}** — the host asks about advertising\n * AND the visitor explicitly said yes to that specific category. This is\n * where the region behaviour is INHERITED rather than reimplemented: an\n * EEA/UK visitor pre-choice has no record, an unknown-region visitor has no\n * record, a US `implied` visitor's status cannot carry an advertising\n * grant, and `gpc-opt-out` cannot either. There is deliberately no country\n * logic in this file.\n * 5. **A configured, well-formed account id for a KNOWN vendor.** The gate\n * describes what may load; the host document decides what is configured.\n * 6. **{@link readInternalTrafficOverride} is false** — this browser has not\n * been declared one of ours. A GA4 data filter is PROPERTY-scoped: it drops\n * `traffic_type: internal` hits from the GA4 property and has no reach into\n * an `AW-`/Meta/LinkedIn destination at all, which are separate products\n * reached by separate requests. Measured on `aglyn.com`: a flagged browser's\n * pageview is correctly absent from GA4 while the same pageview still sends\n * `ccm/collect`, `pagead/1p-user-list` (`is_vtc=1`) and\n * `viewthroughconversion` to Google Ads, joining our own staff to the\n * remarketing audiences those requests build. Excluding ourselves from the\n * reports while still training the bidding on ourselves is the worse half of\n * the problem, because it is the half nobody can see in a report.\n *\n * This is the same browser-scoped opt-in the GA4 stamp uses, so one visit to\n * `?aglyn_internal=1` covers both, and the two cannot drift apart into a\n * browser that is internal for one product and external for the other. Note\n * it is the BROWSER, not the account: staff ID-token claims are the console\n * mechanism (AGL-1582) and there is no account to consult here.\n *\n * {@link analyticsEnvironmentForcesInternal} is the other half of the same\n * condition. Condition 2 passes under the non-production escape hatch, and a\n * build that emits because someone asked it to is ours by definition — so\n * the hatch must not hand a dev or preview build the real `AW-` id. Without\n * this the hatch reopens precisely the hole condition 2 closes, which is why\n * `INTERNAL_TRAFFIC_FORCED_SNIPPET` exists on the GA side.\n *\n * ## What is NOT wired, and why that is the point\n *\n * No host document carries an `analytics.adTags` entry. The Meta descriptor\n * below is a description of how that vendor would be loaded and — more\n * importantly — how it would be torn down; it is inert until someone writes a\n * pixel id onto the `aglyn-marketing` host. That makes deployment a DATA\n * change reviewed on its own merits, exactly as configuring GA is, rather than\n * something that rides along with this mechanism.\n *\n * ## Why customer sites are excluded outright rather than offered this\n *\n * A customer-facing \"run your own ad pixel\" feature is a different product\n * decision with its own consent copy, its own cookie-policy rows and its own\n * DPA implications. Nothing here forecloses it. What this module must not do\n * is arrive as that feature by accident.\n */\n\nimport {\n GOOGLE_ADS_ID_PATTERN,\n LINKEDIN_PARTNER_ID_PATTERN,\n META_PIXEL_ID_PATTERN,\n} from './visitor-consent'\nimport {\n analyticsEnvironmentForcesInternal,\n analyticsMayEmit,\n type AnalyticsEnvironment,\n readAnalyticsEnvironment,\n} from './analytics-environment'\nimport { readInternalTrafficOverride } from './internal-traffic'\nimport { isPlatformMarketingHost } from './platform-marketing-host'\nimport {\n ADVERTISING_COOKIE_PREFIXES,\n advertisingGrantedByRecord,\n clearCookiesWithPrefixes,\n hostConsentRequired,\n type StoredVisitorConsent,\n type VisitorConsentHost,\n} from './visitor-consent'\n\n/**\n * The attribute every script element this module renders carries.\n *\n * Load-bearing for the teardown, not decoration. {@link revokeAdvertisingTags}\n * acts ONLY on elements carrying it, so a vendor tag a customer pasted into\n * their own site's Custom HTML is never touched by our withdrawal path — we\n * did not load it, we do not know what basis it runs on, and silently killing\n * it would be us configuring a customer's site. Its value is the vendor id.\n */\nexport const ADVERTISING_TAG_ATTRIBUTE = 'data-aglyn-ad-tag'\n\n/**\n * One advertising vendor, described completely enough to LOAD it and — the\n * half that is easy to forget and impossible to retrofit — to STOP it.\n *\n * Every field exists because withdrawal needs it. A vendor that cannot be\n * revoked, whose script cannot be found in the document, and whose cookies are\n * not named, cannot be consent-gated at all; requiring the descriptor to\n * answer all three is what stops a future vendor being added load-only.\n *\n * TWO SHAPES, since AGL-2486. A vendor we MOUNT answers all three and every\n * field below is required of it. A {@link AdvertisingVendor.sweepOnly} vendor\n * answers only the third, because there is no script of ours to find or stop —\n * its cookies are a side effect of a tag another module owns. The mount fields\n * are therefore optional at the type level and mandatory in practice for\n * anything without that flag, which `advertising-tag-gate.spec.tsx` asserts.\n * `cookiePrefixes` stays required of BOTH: naming the cookies is the one thing\n * no vendor is excused from, and it is what the Cookie Policy is written from.\n */\nexport interface AdvertisingVendor {\n /** Stable key: the `analytics.adTags` map key and the attribute value. */\n readonly id: string\n /** Human name, for the cookie policy and the consent copy. */\n readonly label: string\n /**\n * A vendor this module never LOADS — its cookies are a side effect of a tag\n * some other module owns (today: the GA4 gtag that `site-analytics.tsx`\n * mounts, which writes `_gcl_*` once `ad_storage` is granted). AGL-2486.\n *\n * It is a member of this registry because the registry's job is every\n * advertising artifact that touches the browser, and because the disclosure\n * guard in `cookie-inventory.spec.ts` keys on `cookiePrefixes` — a vendor\n * missing from here is a vendor missing from the Cookie Policy. Meta being\n * present while Google was absent is the asymmetry that produced the gap.\n *\n * Such a vendor declares `id`, `label` and `cookiePrefixes` and NOTHING\n * else: there is no script to mount, none to remove, and no vendor-specific\n * consent call — GA's own consent-mode signals carry the state. The mount\n * fields below are therefore optional, and `advertising-tag-gate.spec.tsx`\n * asserts every vendor WITHOUT this flag still declares all of them, so the\n * original invariant — a vendor that cannot be revoked cannot be gated —\n * survives for everything that does load.\n */\n readonly sweepOnly?: true\n /**\n * Sweep this vendor's cookies even when no marked element is present.\n *\n * The element check is an OWNERSHIP test, not a liveness one: a pixel we did\n * not load is a pixel running on a basis that is not ours to withdraw, so\n * its cookies are not ours to clear either. `alwaysSweep` is for a vendor\n * whose cookies cannot belong to anybody else's tag on our surfaces —\n * Google's `_gcl_*`, which a GTM container or a bare gtag writes without any\n * marker of ours ever existing. That is the AGL-2486 case, and it is a\n * property of the COOKIE rather than of the loader.\n */\n readonly alwaysSweep?: true\n /**\n * Strict format check on the configured account id. The id lands inside an\n * inline script, so this is load-bearing exactly as\n * `GA_MEASUREMENT_ID_PATTERN` is (the AGL-138 concern).\n */\n readonly accountIdPattern?: RegExp\n /** The vendor library URL. Constant — no interpolation reaches it. */\n readonly scriptSrc?: string\n /**\n * The library URL for a vendor whose LOADER carries the account id.\n *\n * Most vendors take their id in the boot snippet and fetch a constant URL,\n * which is what {@link scriptSrc} is. `gtag.js` is the exception: Google's\n * documented install is `gtag/js?id=<account>`, and the id in the query is\n * what tells the loader which container's configuration to fetch. Without\n * it the library still returns 200 and still defines `gtag()`, so nothing\n * anywhere reports an error — it simply registers no container, and every\n * `config` for the account queues against a runtime that will never serve\n * it. Measured on `app.aglyn.com` (AGL-2559): the bare loader left\n * `google_tag_data.tidr.container` holding the GA4 id and an EMPTY string,\n * with no request to `googleadservices` at all.\n *\n * Present, it wins over {@link scriptSrc}. `scriptSrc` stays the vendor's\n * base URL, because it is what {@link sharesLibrary}, {@link scriptMatch}\n * and the CSP origin list are all matched against, and none of those may\n * vary per account.\n *\n * The id reaching a URL is the same load-bearing check as the id reaching an\n * inline script: it is interpolated only after `accountIdPattern` passed.\n */\n readonly scriptSrcFor?: (accountId: string) => string\n /**\n * A substring of a library URL this vendor SHARES with another loader.\n *\n * Set it and the tag mounts its boot snippet but skips its own `<script>`\n * when a matching one is already in the document. Google Ads is the case:\n * `gtag.js` is the same library the GA4 measurement id loads, so a site with\n * both configured would fetch it twice, define `gtag()` twice, and — the\n * part that actually corrupts data — push a second `consent default` that\n * re-denies what the first one granted, mid-pageview.\n *\n * The boot snippet still runs, because a `config` for a SECOND product is\n * exactly how gtag is meant to carry two: one library, two configs, no\n * double count.\n */\n readonly sharesLibrary?: string\n /**\n * Substring that identifies a RESIDENT script element for this vendor,\n * used together with {@link ADVERTISING_TAG_ATTRIBUTE} at teardown.\n */\n readonly scriptMatch?: string\n /** Cookie-name prefixes this vendor sets, swept on withdrawal. */\n readonly cookiePrefixes: readonly string[]\n /** The inline boot snippet, built from a FORMAT-CHECKED account id. */\n readonly bootSnippet?: (accountId: string) => string\n /**\n * Tell a RESIDENT tag what the visitor decided. The vendor's own documented\n * opt-out mechanism — the `ga-disable-<id>` analogue — reached through\n * whatever global it left behind. Called in both directions, so a visitor\n * who withdraws and changes their mind in one pageview is not silently\n * unmeasured until they navigate.\n */\n readonly setConsent?: (\n scope: Record<string, unknown>,\n granted: boolean,\n ) => void\n}\n\n\n/**\n * Meta (Facebook/Instagram) Pixel.\n *\n * ## What happens to `_fbp` / `_fbc` on withdrawal: they are DELETED\n *\n * Not merely stopped. The decision is inherited, not invented: AGL-1606\n * settled that a withdrawal \"cleans up, it does not merely stop adding\" for\n * the GA equivalents, and there is no honest reading under which Meta's\n * first-party identifiers get a softer rule than Google's. Both are\n * browser-lifetime identifiers written by an advertising tag under a consent\n * the visitor has just withdrawn.\n *\n * They are swept with the SAME ladder as `_ga` — the exact hostname and every\n * domain up to the registrable one — because Meta writes them the same way GA\n * does: path `/`, first-party, at the registrable domain. A deletion aimed at\n * the exact hostname would silently no-op.\n *\n * Two limits, stated rather than glossed. Any copy Meta already ingested is\n * gone from our reach — deletion stops future joins, it does not retract past\n * ones; the CPRA opt-out title on the control is about the ongoing \"share\",\n * which this does stop. And a cookie written at a path other than `/` is not\n * reachable through `document.cookie`; Meta does not write one.\n *\n * ## Why `consent revoke` is called BEFORE the sweep\n *\n * `fbq('consent', 'revoke')` is Meta's documented CMP control and the exact\n * counterpart of the `ga-disable-<id>` flag. Skipping it and going straight to\n * the cookies reproduces AGL-1608: the resident pixel's next automatic event\n * re-writes `_fbp` and the sweep un-does itself inside the same pageview.\n */\nexport const META_PIXEL_VENDOR: AdvertisingVendor = {\n id: 'meta',\n label: 'Meta Pixel',\n // Meta pixel ids are numeric; the length band is generous on both sides\n // rather than pinned to today's 15-16 digits.\n accountIdPattern: META_PIXEL_ID_PATTERN,\n scriptSrc: 'https://connect.facebook.net/en_US/fbevents.js',\n scriptMatch: 'connect.facebook.net',\n cookiePrefixes: ['_fbp', '_fbc'],\n bootSnippet: (accountId: string) =>\n // Meta's own shim, then an EXPLICIT grant, then init. The explicit grant\n // is the AGL-1622 move applied to this vendor: the tag's first hit should\n // carry a state someone actually chose rather than the vendor's built-in\n // default, and declaring it before `init` means the automatic PageView is\n // already covered by it. Reached only where the gate said yes.\n \"!function(f,b,e,v,n,t,s){if(f.fbq)return;n=f.fbq=function(){n.callMethod?\" +\n \"n.callMethod.apply(n,arguments):n.queue.push(arguments)};\" +\n \"if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';n.queue=[]}\" +\n '(window,document);' +\n \"fbq('consent', 'grant');\" +\n `fbq('init', '${accountId}');` +\n \"fbq('track', 'PageView');\",\n setConsent: (scope: Record<string, unknown>, granted: boolean) => {\n try {\n const fbq = scope.fbq\n if (typeof fbq === 'function') {\n ;(fbq as (...args: unknown[]) => void)(\n 'consent',\n granted ? 'grant' : 'revoke',\n )\n }\n } catch {\n // A tag that throws on its own consent call is one we cannot silence\n // that way; the element removal and the cookie sweep still stand.\n }\n },\n}\n\n/**\n * Google advertising storage — a SWEEP-ONLY vendor (AGL-2486).\n *\n * There is no Google ad script for this module to load. `_gcl_*` and `_gac_*`\n * are written by the GA4 gtag that `site-analytics.tsx` already mounts, once\n * the visitor's `ad_storage` is granted. So the tag is somebody else's to\n * mount and somebody else's to signal; what belongs here is the half nobody\n * owned, which is the cookies.\n *\n * `_gcl_au` is the specific reason this exists. It does not begin with `_ga`,\n * so {@link ANALYTICS_COOKIE_PREFIXES} never reached it, and `revokeAdvertisingTags`\n * could not either — that function acts on marked script ELEMENTS, and there\n * has never been one for Google. The cookie therefore survived every\n * withdrawal on every surface. See {@link ADVERTISING_COOKIE_PREFIXES} for why\n * the prefix is `_gcl` ALONE and cannot reach `_ga`/`_gid`.\n *\n * ⚠️ `_gac` is NOT in that list and must not be added back. It is written\n * under the analytics loader, sits in the `Google Analytics` row of\n * `apps/console/constants/cookie-inventory.ts`, and is already swept by the\n * `_ga` prefix; `advertising-cookie-sweep.spec.ts` pins it as analytics-owned.\n * An earlier version of this comment named it here, and a published Cookie\n * Policy line describing `_gac` as advertising-gated traces to exactly that.\n *\n * The prefixes are IMPORTED rather than restated. They are consumed in two\n * places — the universal sweep in `storeVisitorConsent` and the element-scoped\n * one below — and a second copy would be a second thing to forget.\n */\nexport const GOOGLE_ADS_VENDOR: AdvertisingVendor = {\n id: 'google-ads',\n label: 'Google advertising',\n /*\n * A LOADER now, not sweep-only (AGL-1152).\n *\n * It was sweep-only because Google's ad tags arrived through something else\n * — a GA4 id or a GTM container — so there was nothing of ours to mount and\n * only cookies to clear. That made advertising reachable ONLY through an\n * analytics product: a site that wanted Google Ads and no analytics had no\n * route at all, and the CSP gate that reads `adTags` never opened for it.\n *\n * `gtag.js` with an `AW-` id is Google's own documented install for Ads\n * without Analytics. The same library serves both products; what differs is\n * the id it is configured with, which is why the two patterns are separate\n * and neither field accepts the other's.\n */\n accountIdPattern: GOOGLE_ADS_ID_PATTERN,\n // Keeps the AGL-2486 sweep it had as a sweep-only vendor: `_gcl_*` is\n // written by any Google tag, including ones we never marked.\n alwaysSweep: true,\n scriptSrc: 'https://www.googletagmanager.com/gtag/js',\n // The loader carries the account, because gtag resolves the container from\n // the query rather than from the `config` that follows. See `scriptSrcFor`;\n // reached only when `sharesLibrary` did NOT find a loader to ride.\n scriptSrcFor: (accountId: string) =>\n `https://www.googletagmanager.com/gtag/js?id=${accountId}`,\n // The GA4 measurement id loads this exact library. See `sharesLibrary`.\n sharesLibrary: 'googletagmanager.com/gtag/js',\n scriptMatch: 'googletagmanager.com/gtag/js',\n cookiePrefixes: ADVERTISING_COOKIE_PREFIXES,\n bootSnippet: (accountId: string) =>\n /*\n * DENIED FIRST, then granted — the AGL-1622 order.\n *\n * `gtag('consent', 'default', …)` has to run before the config, or the\n * library's own default applies to the first hit. This snippet is reached\n * only where the gate already said yes, so the grant follows immediately;\n * declaring the default anyway is what makes the first hit carry a state\n * somebody chose rather than Google's.\n *\n * `ad_user_data` and `ad_personalization` are named alongside\n * `ad_storage`: Consent Mode v2 treats them as separate signals, and a\n * grant that sets only storage leaves the other two at the library's\n * default on every EEA request.\n */\n /*\n * NO `consent default` here, deliberately.\n *\n * A default is a page-level declaration and something else may already\n * have made it — the GA4 loader above declares one, and re-declaring it\n * mid-pageview re-DENIES what that grant allowed until the update lands.\n * This snippet is reached only where the gate already said yes, so it\n * states the update and leaves the default to whoever mounts first.\n *\n * `ad_user_data` and `ad_personalization` travel with `ad_storage`:\n * Consent Mode v2 treats them as separate signals, and an update setting\n * only storage leaves the other two at the library's default on every\n * EEA request.\n */\n 'window.dataLayer=window.dataLayer||[];' +\n 'function gtag(){dataLayer.push(arguments);}' +\n \"gtag('consent','update',{ad_storage:'granted',\" +\n \"ad_user_data:'granted',ad_personalization:'granted'});\" +\n \"gtag('js', new Date());\" +\n `gtag('config', '${accountId}');`,\n setConsent: (scope: Record<string, unknown>, granted: boolean) => {\n try {\n const gtag = scope.gtag\n if (typeof gtag === 'function') {\n ;(gtag as (...args: unknown[]) => void)('consent', 'update', {\n ad_storage: granted ? 'granted' : 'denied',\n ad_user_data: granted ? 'granted' : 'denied',\n ad_personalization: granted ? 'granted' : 'denied',\n })\n }\n } catch {\n // A tag that throws on its own consent call is one we cannot silence\n // that way; the element removal and the cookie sweep still stand.\n }\n },\n}\n\n/**\n * LinkedIn Insight Tag.\n *\n * ## Cookies, and why the prefix list is long\n *\n * LinkedIn writes more names than the other two and they do not share a stem:\n * `li_sugr` and `UserMatchHistory` are the retargeting identifiers, `bcookie`\n * and `lidc` are set on the `.linkedin.com` domain by the loader, and\n * `AnalyticsSyncHistory` records the last sync. A prefix list that named only\n * `li_` would leave three of them behind on withdrawal — the AGL-2486 shape,\n * where a sweep looks thorough because it cleared the ones that happen to\n * share a prefix.\n *\n * ⚠️ `bcookie` and `lidc` are written at `.linkedin.com`, a domain a page on\n * our origin cannot delete through `document.cookie`. The sweep removes what\n * it can reach and the element removal stops the tag writing more; the rest is\n * LinkedIn's to hold, and saying so is better than a sweep that quietly\n * half-works.\n */\nexport const LINKEDIN_INSIGHT_VENDOR: AdvertisingVendor = {\n id: 'linkedin',\n label: 'LinkedIn Insight Tag',\n accountIdPattern: LINKEDIN_PARTNER_ID_PATTERN,\n scriptSrc: 'https://snap.licdn.com/li.lms-analytics/insight.min.js',\n scriptMatch: 'snap.licdn.com',\n cookiePrefixes: [\n 'li_sugr',\n 'UserMatchHistory',\n 'AnalyticsSyncHistory',\n 'bcookie',\n 'lidc',\n 'li_gc',\n ],\n bootSnippet: (accountId: string) =>\n // The Insight Tag reads its partner id off a global array the library\n // drains on load, so the id is pushed before the script is appended\n // rather than passed to it.\n `window._linkedin_partner_id='${accountId}';` +\n 'window._linkedin_data_partner_ids=window._linkedin_data_partner_ids||[];' +\n 'window._linkedin_data_partner_ids.push(window._linkedin_partner_id);',\n setConsent: (scope: Record<string, unknown>, granted: boolean) => {\n try {\n // The tag has no consent API of its own. What it does have is the\n // partner-id array it drains on load: emptying it stops a late or\n // re-inserted library finding an id to report against. The element\n // removal and the cookie sweep remain the substance.\n if (!granted) scope._linkedin_data_partner_ids = []\n } catch {\n // Same standing as the others: the teardown does not depend on this.\n }\n },\n}\n\n/**\n * Every vendor this gate knows how to tear down — which is a SUPERSET of the\n * vendors it knows how to load, now that a sweep-only member exists.\n */\nexport const ADVERTISING_VENDORS: readonly AdvertisingVendor[] = [\n META_PIXEL_VENDOR,\n GOOGLE_ADS_VENDOR,\n LINKEDIN_INSIGHT_VENDOR,\n]\n\n/**\n * The host fields this module reads, on top of the consent ones.\n *\n * `adTags` maps a vendor id to that vendor's account id. NO host document\n * carries it today; see the module comment on why deployment is a separate,\n * reviewable data change rather than a consequence of merging this.\n */\nexport interface AdvertisingTagHost extends VisitorConsentHost {\n analytics?: {\n gaMeasurementId?: string\n /** Vendor id → account id. Absent on every site that exists. */\n adTags?: Record<string, string> | null\n } | null\n}\n\n/** A vendor that may load, paired with the id it was configured with. */\nexport interface ResolvedAdvertisingTag {\n readonly vendor: AdvertisingVendor\n readonly accountId: string\n}\n\n/**\n * The whole verdict: which advertising tags may exist in this document, for\n * this visitor, on this site, right now?\n *\n * Pure, and empty is the answer to every question it cannot answer — an absent\n * host, an unreadable record, an unknown vendor id, a malformed account id.\n * See the module comment for the six conditions and why each one is separate.\n *\n * `stored` is the CLIENT-resolved record. Like the GA gate, this is evaluated\n * after hydration only: tenant pages are ISR-cached, so the server HTML must\n * be identical for every visitor and cannot carry a tag one of them granted.\n *\n * `internal` is read the same way and for the same reason. Taking it as a\n * defaulted parameter rather than calling into the browser mid-verdict is what\n * keeps this function testable in both directions — a gate that can only be\n * exercised one way is the shape that ships broken (AGL-2067).\n */\nexport function resolveAdvertisingTags(\n host: AdvertisingTagHost | null | undefined,\n stored: StoredVisitorConsent | null | undefined,\n env: AnalyticsEnvironment = readAnalyticsEnvironment(),\n internal: boolean = readInternalTrafficOverride(),\n): ResolvedAdvertisingTag[] {\n if (isPlatformMarketingHost(host) === false) return []\n if (analyticsMayEmit(env) === false) return []\n if (hostConsentRequired(host) === false) return []\n if (advertisingGrantedByRecord(host, stored) === false) return []\n // Condition 6. Structural, like every other clause here: the tag is not\n // mounted rather than mounted-and-suppressed, because a resident tag fires\n // on its own and a `_gcl_*` cookie is written by the first automatic event.\n //\n // The environment half is not redundant with `analyticsMayEmit` above. That\n // clause passes under the non-production escape hatch, and a build emitting\n // because someone asked it to is ours by definition — so the hatch must not\n // hand a dev or preview build the real `AW-` id and let it build remarketing\n // audiences out of our own engineers, which is the hole condition 2 exists\n // to close. Same reasoning as `INTERNAL_TRAFFIC_FORCED_SNIPPET`.\n if (internal === true) return []\n if (analyticsEnvironmentForcesInternal(env) === true) return []\n const configured = host?.analytics?.adTags\n if (!configured) return []\n const tags: ResolvedAdvertisingTag[] = []\n for (const vendor of ADVERTISING_VENDORS) {\n // Sweep-only: nothing to mount, and no `accountIdPattern` to test with.\n // Skipped explicitly rather than left to fail a pattern check, so a stray\n // `adTags['google-ads']` cannot conjure a script (AGL-2486).\n if (vendor.sweepOnly || !vendor.accountIdPattern) continue\n const accountId = String(configured[vendor.id] ?? '')\n if (vendor.accountIdPattern.test(accountId)) {\n tags.push({ vendor, accountId })\n }\n }\n return tags\n}\n\n/**\n * The script elements THIS MODULE put in the document for a given vendor —\n * both the inline boot and the library, since a vendor mounts as a pair.\n *\n * Scoped by {@link ADVERTISING_TAG_ATTRIBUTE}, which is the clause that keeps\n * a pixel a customer pasted into their own Custom HTML out of our teardown.\n * An element that carries the attribute but whose `src` points somewhere other\n * than this vendor is excluded as well: the attribute is ours to write, so a\n * mismatch means something rewrote it and the safe reading is \"not ours\".\n * An element with NO `src` is the inline boot and passes — it has no URL to\n * disagree with.\n */\nfunction markedVendorElements(vendor: AdvertisingVendor): Element[] {\n if (typeof document === 'undefined') return []\n // A sweep-only vendor has no script of ours and therefore no element that\n // could be \"ours\" (AGL-2486). Returning early is also what stops an absent\n // `scriptMatch` reaching `src.includes(...)` below — an empty string there\n // would have matched EVERY marked script, handing one vendor's teardown\n // another vendor's elements.\n if (vendor.sweepOnly || !vendor.scriptMatch) return []\n try {\n const selector = `script[${ADVERTISING_TAG_ATTRIBUTE}=\"${vendor.id}\"]`\n return Array.from(document.querySelectorAll(selector)).filter((element) => {\n const src = String((element as HTMLScriptElement).src ?? '')\n return src === '' || src.includes(vendor.scriptMatch)\n })\n } catch {\n // A hostile or absent DOM: nothing we can prove is ours.\n return []\n }\n}\n\n/** Every vendor whose tag this module loaded and that is still resident. */\nexport function residentAdvertisingVendors(): AdvertisingVendor[] {\n return ADVERTISING_VENDORS.filter(\n (vendor) => markedVendorElements(vendor).length > 0,\n )\n}\n\n/**\n * Stop every advertising tag THIS MODULE loaded, and return the vendor ids it\n * acted on — which is what makes the withdrawal assertable rather than\n * assumed.\n *\n * Three steps, in this order and for these reasons:\n *\n * 1. **Revoke on the resident tag.** The gate cannot unload a script that has\n * already executed. This is the vendor's own kill switch and it is what\n * actually stops the next automatic event.\n * 2. **Remove the script elements.** So the structural property the gate\n * asserts — the vendor's script is not in this document — is restored and\n * not merely claimed. React unmounting its `<Script>` does not reliably do\n * this: `next/script` injects into the head and leaves it there.\n * 3. **Sweep the cookies.** Last, because steps 1 and 2 are what stop them\n * coming straight back (AGL-1608).\n *\n * Acts only on tags carrying {@link ADVERTISING_TAG_ATTRIBUTE}. On a customer\n * site — where this module never loaded anything — it is a no-op that touches\n * no cookie, which is the only correct behaviour: their pixel, if any, runs on\n * a basis that is not ours to withdraw.\n */\nexport function revokeAdvertisingTags(hostname?: string | null): string[] {\n if (typeof window === 'undefined') return []\n const acted: string[] = []\n const scope = window as unknown as Record<string, unknown>\n for (const vendor of ADVERTISING_VENDORS) {\n /*\n * The element check is an OWNERSHIP test (AGL-1498 case (e)).\n *\n * A pixel we did not load is one running on a basis that is not ours to\n * withdraw — a customer's own Custom HTML, on their own site, under their\n * own notice. Killing it, or clearing its cookies, would be us\n * reconfiguring their site against a consent record their tag never ran\n * on. So no marker of ours means hands off, cookies included.\n *\n * `alwaysSweep` is the narrow exception and it is about the COOKIE, not\n * the loader: `_gcl_*` is written by any Google tag — a GTM container, a\n * bare gtag — with no marker of ours ever existing, which is why it\n * survived every withdrawal until AGL-2486. Giving Google a loader must\n * not quietly take that sweep away again.\n *\n * ORDER, which is the whole of AGL-1608: revoke on the tag FIRST, then\n * remove it, then sweep. Sweeping first deletes the cookies and the\n * resident pixel's next automatic event writes them straight back inside\n * the same pageview.\n */\n const elements = vendor.sweepOnly ? [] : markedVendorElements(vendor)\n const ours = elements.length > 0\n if (ours) {\n vendor.setConsent?.(scope, false)\n for (const element of elements) {\n try {\n element.remove()\n } catch {\n // A detached or frozen node: the revoke above still stands.\n }\n }\n }\n const maySweep = ours || vendor.sweepOnly || vendor.alwaysSweep\n const swept = maySweep\n ? clearCookiesWithPrefixes(vendor.cookiePrefixes, hostname)\n : []\n if (ours || swept.length > 0) acted.push(vendor.id)\n }\n return acted\n}\n\n/**\n * Re-grant on a tag that is still resident — the symmetric half.\n *\n * A visitor who withdraws and changes their mind inside one pageview would\n * otherwise stay silently un-tracked until they navigated, because a re-\n * rendered `<Script>` cannot re-execute a library the browser already ran.\n * `setResidentAnalyticsTags` is symmetric for exactly this reason and this is\n * the same move for a second channel.\n *\n * Returns the vendor ids it reached; empty when nothing is resident, which is\n * the normal case — a re-grant after the elements were removed goes through\n * the gate and loads a fresh tag instead.\n */\nexport function restoreAdvertisingTags(): string[] {\n if (typeof window === 'undefined') return []\n const scope = window as unknown as Record<string, unknown>\n const acted: string[] = []\n for (const vendor of residentAdvertisingVendors()) {\n vendor.setConsent?.(scope, true)\n acted.push(vendor.id)\n }\n return acted\n}\n"],"names":["GOOGLE_ADS_ID_PATTERN","LINKEDIN_PARTNER_ID_PATTERN","META_PIXEL_ID_PATTERN","analyticsEnvironmentForcesInternal","analyticsMayEmit","readAnalyticsEnvironment","readInternalTrafficOverride","isPlatformMarketingHost","ADVERTISING_COOKIE_PREFIXES","advertisingGrantedByRecord","clearCookiesWithPrefixes","hostConsentRequired","ADVERTISING_TAG_ATTRIBUTE","META_PIXEL_VENDOR","id","label","accountIdPattern","scriptSrc","scriptMatch","cookiePrefixes","bootSnippet","accountId","setConsent","scope","granted","fbq","GOOGLE_ADS_VENDOR","alwaysSweep","scriptSrcFor","sharesLibrary","gtag","ad_storage","ad_user_data","ad_personalization","LINKEDIN_INSIGHT_VENDOR","_linkedin_data_partner_ids","ADVERTISING_VENDORS","resolveAdvertisingTags","host","stored","env","internal","configured","analytics","adTags","tags","vendor","sweepOnly","String","test","push","markedVendorElements","document","selector","Array","from","querySelectorAll","filter","element","src","includes","residentAdvertisingVendors","length","revokeAdvertisingTags","hostname","window","acted","elements","ours","remove","maySweep","swept","restoreAdvertisingTags"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/advertising-tags.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * The VENDOR-AGNOSTIC advertising-tag gate.\n *\n * ## The hole this closes\n *\n * Until now the consent machinery could gate exactly one thing: Google. The\n * `advertising` category has existed since AGL-1649 and it is honest — a\n * refusal always wins, and a host that never asked gets nothing — but the\n * only ENFORCEMENT it ever reached was {@link consentModeSignals}, four\n * strings handed to `gtag`. A non-Google tag could not be consent-gated at\n * all, because nothing in the repo knew how to gate one. Deciding to deploy a\n * Meta Pixel with that as the state of the art would have meant either\n * shipping it ungated or inventing a second consent implementation beside the\n * first — the outcome AGL-1579 explicitly ruled out.\n *\n * This module is the missing enforcement channel, built for vendors in\n * general and populated with one descriptor. **Deploying the Meta Pixel is a\n * separate decision and this module does not take it** — see \"What is NOT\n * wired\" below.\n *\n * ## Why the gate is STRUCTURAL\n *\n * The lesson AGL-1498 paid for and AGL-1608 paid for again: a boolean checked\n * before each call is not a gate. A resident tag fires on its own — GA4\n * enhanced measurement re-created `_ga_YW5PG16YTM` on aglyn.com after a\n * hand-run sweep to zero cookies, from one scroll to the footer — so the only\n * enforcement that holds is the tag never being in the document.\n *\n * So the shape here mirrors the GA gate exactly:\n *\n * - {@link resolveAdvertisingTags} is a PURE verdict over the host document\n * and the visitor's stored record. It returns the tags that may exist. An\n * ungranted visitor gets an empty array, the component renders no\n * `<Script>`, and no request goes to the vendor. Not loaded-then-suppressed:\n * not loaded.\n * - {@link revokeAdvertisingTags} exists because the gate CANNOT unload a tag.\n * Dropping the element on a mid-pageview withdrawal leaves the vendor's\n * script executing, so withdrawal additionally revokes consent on the\n * resident tag, removes its script elements, and sweeps its cookies — in\n * that order, for the AGL-1608 reason: sweeping first deletes the cookies\n * and immediately gets them back.\n *\n * ## The six conditions, all independent, all required\n *\n * 1. **Whose tags.** On Aglyn's own marketing site\n * ({@link isPlatformMarketingHost}), ours; on a customer's site, only the\n * tags that site's owner configured with their own account ids\n * (AGL-3694) — see \"A site owner's OWN tags\" below. Never OUR tag on a\n * customer's site: that is the DPA §3.2 boundary, Aglyn not \"selling\" or\n * \"sharing\" Customer Personal Data for its own advertising, and the\n * platform's ids are on no host but ours. Note the console needs no\n * condition of its own: it does not render the tenant runtime's analytics\n * component at all, so there is no mount point to gate.\n * 2. **{@link analyticsMayEmit}** — a real production deployment. AGL-2067's\n * finding was that `next dev` and Vercel PREVIEW builds resolve\n * `aglyn.com`'s host document exactly as production does; without this a\n * preview deploy would build real retargeting audiences out of our own\n * engineers.\n * 3. **{@link hostConsentRequired}** — the consent machinery is actually live\n * for this site. A host that switched the tool off (`consent.disabled`)\n * runs their own CMP and ours has no answer on file to act on.\n * 4. **{@link advertisingGrantedByRecord}** — the host asks about advertising\n * AND the visitor explicitly said yes to that specific category. This is\n * where the region behaviour is INHERITED rather than reimplemented: an\n * EEA/UK visitor pre-choice has no record, an unknown-region visitor has no\n * record, a US `implied` visitor's status cannot carry an advertising\n * grant, and `gpc-opt-out` cannot either. There is deliberately no country\n * logic in this file.\n * 5. **A configured, well-formed account id for a KNOWN vendor.** The gate\n * describes what may load; the host document decides what is configured.\n * 6. **{@link readInternalTrafficOverride} is false** — this browser has not\n * been declared one of ours. A GA4 data filter is PROPERTY-scoped: it drops\n * `traffic_type: internal` hits from the GA4 property and has no reach into\n * an `AW-`/Meta/LinkedIn destination at all, which are separate products\n * reached by separate requests. Measured on `aglyn.com`: a flagged browser's\n * pageview is correctly absent from GA4 while the same pageview still sends\n * `ccm/collect`, `pagead/1p-user-list` (`is_vtc=1`) and\n * `viewthroughconversion` to Google Ads, joining our own staff to the\n * remarketing audiences those requests build. Excluding ourselves from the\n * reports while still training the bidding on ourselves is the worse half of\n * the problem, because it is the half nobody can see in a report.\n *\n * This is the same browser-scoped opt-in the GA4 stamp uses, so one visit to\n * `?aglyn_internal=1` covers both, and the two cannot drift apart into a\n * browser that is internal for one product and external for the other. Note\n * it is the BROWSER, not the account: staff ID-token claims are the console\n * mechanism (AGL-1582) and there is no account to consult here.\n *\n * {@link analyticsEnvironmentForcesInternal} is the other half of the same\n * condition. Condition 2 passes under the non-production escape hatch, and a\n * build that emits because someone asked it to is ours by definition — so\n * the hatch must not hand a dev or preview build the real `AW-` id. Without\n * this the hatch reopens precisely the hole condition 2 closes, which is why\n * `INTERNAL_TRAFFIC_FORCED_SNIPPET` exists on the GA side.\n *\n * ## A site owner's OWN tags (AGL-3694)\n *\n * Condition 1 above is about OUR tags. A customer's site may mount the tags its\n * owner configured — their own Meta pixel, TikTok pixel, Pinterest tag, Google\n * Ads id or LinkedIn partner id, written on Site → Setup → Tracking — and\n * nothing else: the account ids come from that site's own host document, so\n * every hit lands in the owner's own ad account, under the owner's\n * instruction, exactly as the site's own GA4 measurement id does. That is the\n * product decision the comment here used to leave open (\"a different product\n * decision with its own consent copy, its own cookie-policy rows and its own\n * DPA implications\"), taken with those rows: the cookie inventory names every\n * vendor below, the docs say what loads and when, and the consent conditions\n * 2–6 apply to a customer's site unchanged. A customer's tag is marked\n * {@link ADVERTISING_EVENTS_ATTRIBUTE} as well, which is what lets the site's\n * conversion events reach it (`advertising-events.ts`) and keeps them away\n * from ours.\n *\n * What still never happens is OUR tag on a customer's site: the platform's\n * account ids live only on the `aglyn-marketing` host, which only\n * `isPlatformMarketingHost` matches.\n */\n\nimport {\n GOOGLE_ADS_ID_PATTERN,\n LINKEDIN_PARTNER_ID_PATTERN,\n META_PIXEL_ID_PATTERN,\n PINTEREST_TAG_ID_PATTERN,\n TIKTOK_PIXEL_ID_PATTERN,\n} from './visitor-consent'\nimport {\n ADVERTISING_EVENTS_ATTRIBUTE,\n ADVERTISING_TAG_ATTRIBUTE,\n} from './advertising-events'\nimport {\n analyticsEnvironmentForcesInternal,\n analyticsMayEmit,\n type AnalyticsEnvironment,\n readAnalyticsEnvironment,\n} from './analytics-environment'\nimport { readInternalTrafficOverride } from './internal-traffic'\nimport { isPlatformMarketingHost } from './platform-marketing-host'\nimport {\n ADVERTISING_COOKIE_PREFIXES,\n advertisingGrantedByRecord,\n clearCookiesWithPrefixes,\n hostConsentRequired,\n type StoredVisitorConsent,\n type VisitorConsentHost,\n} from './visitor-consent'\n\n/**\n * The attribute every script element this module renders carries.\n *\n * Load-bearing for the teardown, not decoration. {@link revokeAdvertisingTags}\n * acts ONLY on elements carrying it, so a vendor tag a customer pasted into\n * their own site's Custom HTML is never touched by our withdrawal path — we\n * did not load it, we do not know what basis it runs on, and silently killing\n * it would be us configuring a customer's site. Its value is the vendor id.\n *\n * Declared in `advertising-events.ts` (AGL-3694), which finds a mounted tag by\n * it without importing this module.\n */\nexport { ADVERTISING_EVENTS_ATTRIBUTE, ADVERTISING_TAG_ATTRIBUTE }\n\n/**\n * One advertising vendor, described completely enough to LOAD it and — the\n * half that is easy to forget and impossible to retrofit — to STOP it.\n *\n * Every field exists because withdrawal needs it. A vendor that cannot be\n * revoked, whose script cannot be found in the document, and whose cookies are\n * not named, cannot be consent-gated at all; requiring the descriptor to\n * answer all three is what stops a future vendor being added load-only.\n *\n * TWO SHAPES, since AGL-2486. A vendor we MOUNT answers all three and every\n * field below is required of it. A {@link AdvertisingVendor.sweepOnly} vendor\n * answers only the third, because there is no script of ours to find or stop —\n * its cookies are a side effect of a tag another module owns. The mount fields\n * are therefore optional at the type level and mandatory in practice for\n * anything without that flag, which `advertising-tag-gate.spec.tsx` asserts.\n * `cookiePrefixes` stays required of BOTH: naming the cookies is the one thing\n * no vendor is excused from, and it is what the Cookie Policy is written from.\n */\nexport interface AdvertisingVendor {\n /** Stable key: the `analytics.adTags` map key and the attribute value. */\n readonly id: string\n /** Human name, for the cookie policy and the consent copy. */\n readonly label: string\n /**\n * A vendor this module never LOADS — its cookies are a side effect of a tag\n * some other module owns (today: the GA4 gtag that `site-analytics.tsx`\n * mounts, which writes `_gcl_*` once `ad_storage` is granted). AGL-2486.\n *\n * It is a member of this registry because the registry's job is every\n * advertising artifact that touches the browser, and because the disclosure\n * guard in `cookie-inventory.spec.ts` keys on `cookiePrefixes` — a vendor\n * missing from here is a vendor missing from the Cookie Policy. Meta being\n * present while Google was absent is the asymmetry that produced the gap.\n *\n * Such a vendor declares `id`, `label` and `cookiePrefixes` and NOTHING\n * else: there is no script to mount, none to remove, and no vendor-specific\n * consent call — GA's own consent-mode signals carry the state. The mount\n * fields below are therefore optional, and `advertising-tag-gate.spec.tsx`\n * asserts every vendor WITHOUT this flag still declares all of them, so the\n * original invariant — a vendor that cannot be revoked cannot be gated —\n * survives for everything that does load.\n */\n readonly sweepOnly?: true\n /**\n * Sweep this vendor's cookies even when no marked element is present.\n *\n * The element check is an OWNERSHIP test, not a liveness one: a pixel we did\n * not load is a pixel running on a basis that is not ours to withdraw, so\n * its cookies are not ours to clear either. `alwaysSweep` is for a vendor\n * whose cookies cannot belong to anybody else's tag on our surfaces —\n * Google's `_gcl_*`, which a GTM container or a bare gtag writes without any\n * marker of ours ever existing. That is the AGL-2486 case, and it is a\n * property of the COOKIE rather than of the loader.\n */\n readonly alwaysSweep?: true\n /**\n * Strict format check on the configured account id. The id lands inside an\n * inline script, so this is load-bearing exactly as\n * `GA_MEASUREMENT_ID_PATTERN` is (the AGL-138 concern).\n */\n readonly accountIdPattern?: RegExp\n /** The vendor library URL. Constant — no interpolation reaches it. */\n readonly scriptSrc?: string\n /**\n * The library URL for a vendor whose LOADER carries the account id.\n *\n * Most vendors take their id in the boot snippet and fetch a constant URL,\n * which is what {@link scriptSrc} is. `gtag.js` is the exception: Google's\n * documented install is `gtag/js?id=<account>`, and the id in the query is\n * what tells the loader which container's configuration to fetch. Without\n * it the library still returns 200 and still defines `gtag()`, so nothing\n * anywhere reports an error — it simply registers no container, and every\n * `config` for the account queues against a runtime that will never serve\n * it. Measured on `app.aglyn.com` (AGL-2559): the bare loader left\n * `google_tag_data.tidr.container` holding the GA4 id and an EMPTY string,\n * with no request to `googleadservices` at all.\n *\n * Present, it wins over {@link scriptSrc}. `scriptSrc` stays the vendor's\n * base URL, because it is what {@link sharesLibrary}, {@link scriptMatch}\n * and the CSP origin list are all matched against, and none of those may\n * vary per account.\n *\n * The id reaching a URL is the same load-bearing check as the id reaching an\n * inline script: it is interpolated only after `accountIdPattern` passed.\n */\n readonly scriptSrcFor?: (accountId: string) => string\n /**\n * A substring of a library URL this vendor SHARES with another loader.\n *\n * Set it and the tag mounts its boot snippet but skips its own `<script>`\n * when a matching one is already in the document. Google Ads is the case:\n * `gtag.js` is the same library the GA4 measurement id loads, so a site with\n * both configured would fetch it twice, define `gtag()` twice, and — the\n * part that actually corrupts data — push a second `consent default` that\n * re-denies what the first one granted, mid-pageview.\n *\n * The boot snippet still runs, because a `config` for a SECOND product is\n * exactly how gtag is meant to carry two: one library, two configs, no\n * double count.\n */\n readonly sharesLibrary?: string\n /**\n * Substring that identifies a RESIDENT script element for this vendor,\n * used together with {@link ADVERTISING_TAG_ATTRIBUTE} at teardown.\n */\n readonly scriptMatch?: string\n /** Cookie-name prefixes this vendor sets, swept on withdrawal. */\n readonly cookiePrefixes: readonly string[]\n /** The inline boot snippet, built from a FORMAT-CHECKED account id. */\n readonly bootSnippet?: (accountId: string) => string\n /**\n * Tell a RESIDENT tag what the visitor decided. The vendor's own documented\n * opt-out mechanism — the `ga-disable-<id>` analogue — reached through\n * whatever global it left behind. Called in both directions, so a visitor\n * who withdraws and changes their mind in one pageview is not silently\n * unmeasured until they navigate.\n */\n readonly setConsent?: (\n scope: Record<string, unknown>,\n granted: boolean,\n ) => void\n}\n\n\n/**\n * Meta (Facebook/Instagram) Pixel.\n *\n * ## What happens to `_fbp` / `_fbc` on withdrawal: they are DELETED\n *\n * Not merely stopped. The decision is inherited, not invented: AGL-1606\n * settled that a withdrawal \"cleans up, it does not merely stop adding\" for\n * the GA equivalents, and there is no honest reading under which Meta's\n * first-party identifiers get a softer rule than Google's. Both are\n * browser-lifetime identifiers written by an advertising tag under a consent\n * the visitor has just withdrawn.\n *\n * They are swept with the SAME ladder as `_ga` — the exact hostname and every\n * domain up to the registrable one — because Meta writes them the same way GA\n * does: path `/`, first-party, at the registrable domain. A deletion aimed at\n * the exact hostname would silently no-op.\n *\n * Two limits, stated rather than glossed. Any copy Meta already ingested is\n * gone from our reach — deletion stops future joins, it does not retract past\n * ones; the CPRA opt-out title on the control is about the ongoing \"share\",\n * which this does stop. And a cookie written at a path other than `/` is not\n * reachable through `document.cookie`; Meta does not write one.\n *\n * ## Why `consent revoke` is called BEFORE the sweep\n *\n * `fbq('consent', 'revoke')` is Meta's documented CMP control and the exact\n * counterpart of the `ga-disable-<id>` flag. Skipping it and going straight to\n * the cookies reproduces AGL-1608: the resident pixel's next automatic event\n * re-writes `_fbp` and the sweep un-does itself inside the same pageview.\n */\nexport const META_PIXEL_VENDOR: AdvertisingVendor = {\n id: 'meta',\n label: 'Meta Pixel',\n // Meta pixel ids are numeric; the length band is generous on both sides\n // rather than pinned to today's 15-16 digits.\n accountIdPattern: META_PIXEL_ID_PATTERN,\n scriptSrc: 'https://connect.facebook.net/en_US/fbevents.js',\n scriptMatch: 'connect.facebook.net',\n cookiePrefixes: ['_fbp', '_fbc'],\n bootSnippet: (accountId: string) =>\n // Meta's own shim, then an EXPLICIT grant, then init. The explicit grant\n // is the AGL-1622 move applied to this vendor: the tag's first hit should\n // carry a state someone actually chose rather than the vendor's built-in\n // default, and declaring it before `init` means the automatic PageView is\n // already covered by it. Reached only where the gate said yes.\n \"!function(f,b,e,v,n,t,s){if(f.fbq)return;n=f.fbq=function(){n.callMethod?\" +\n \"n.callMethod.apply(n,arguments):n.queue.push(arguments)};\" +\n \"if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';n.queue=[]}\" +\n '(window,document);' +\n \"fbq('consent', 'grant');\" +\n `fbq('init', '${accountId}');` +\n \"fbq('track', 'PageView');\",\n setConsent: (scope: Record<string, unknown>, granted: boolean) => {\n try {\n const fbq = scope.fbq\n if (typeof fbq === 'function') {\n ;(fbq as (...args: unknown[]) => void)(\n 'consent',\n granted ? 'grant' : 'revoke',\n )\n }\n } catch {\n // A tag that throws on its own consent call is one we cannot silence\n // that way; the element removal and the cookie sweep still stand.\n }\n },\n}\n\n/**\n * Google advertising storage — a SWEEP-ONLY vendor (AGL-2486).\n *\n * There is no Google ad script for this module to load. `_gcl_*` and `_gac_*`\n * are written by the GA4 gtag that `site-analytics.tsx` already mounts, once\n * the visitor's `ad_storage` is granted. So the tag is somebody else's to\n * mount and somebody else's to signal; what belongs here is the half nobody\n * owned, which is the cookies.\n *\n * `_gcl_au` is the specific reason this exists. It does not begin with `_ga`,\n * so {@link ANALYTICS_COOKIE_PREFIXES} never reached it, and `revokeAdvertisingTags`\n * could not either — that function acts on marked script ELEMENTS, and there\n * has never been one for Google. The cookie therefore survived every\n * withdrawal on every surface. See {@link ADVERTISING_COOKIE_PREFIXES} for why\n * the prefix is `_gcl` ALONE and cannot reach `_ga`/`_gid`.\n *\n * ⚠️ `_gac` is NOT in that list and must not be added back. It is written\n * under the analytics loader, sits in the `Google Analytics` row of\n * `apps/console/constants/cookie-inventory.ts`, and is already swept by the\n * `_ga` prefix; `advertising-cookie-sweep.spec.ts` pins it as analytics-owned.\n * An earlier version of this comment named it here, and a published Cookie\n * Policy line describing `_gac` as advertising-gated traces to exactly that.\n *\n * The prefixes are IMPORTED rather than restated. They are consumed in two\n * places — the universal sweep in `storeVisitorConsent` and the element-scoped\n * one below — and a second copy would be a second thing to forget.\n */\nexport const GOOGLE_ADS_VENDOR: AdvertisingVendor = {\n id: 'google-ads',\n label: 'Google advertising',\n /*\n * A LOADER now, not sweep-only (AGL-1152).\n *\n * It was sweep-only because Google's ad tags arrived through something else\n * — a GA4 id or a GTM container — so there was nothing of ours to mount and\n * only cookies to clear. That made advertising reachable ONLY through an\n * analytics product: a site that wanted Google Ads and no analytics had no\n * route at all, and the CSP gate that reads `adTags` never opened for it.\n *\n * `gtag.js` with an `AW-` id is Google's own documented install for Ads\n * without Analytics. The same library serves both products; what differs is\n * the id it is configured with, which is why the two patterns are separate\n * and neither field accepts the other's.\n */\n accountIdPattern: GOOGLE_ADS_ID_PATTERN,\n // Keeps the AGL-2486 sweep it had as a sweep-only vendor: `_gcl_*` is\n // written by any Google tag, including ones we never marked.\n alwaysSweep: true,\n scriptSrc: 'https://www.googletagmanager.com/gtag/js',\n // The loader carries the account, because gtag resolves the container from\n // the query rather than from the `config` that follows. See `scriptSrcFor`;\n // reached only when `sharesLibrary` did NOT find a loader to ride.\n scriptSrcFor: (accountId: string) =>\n `https://www.googletagmanager.com/gtag/js?id=${accountId}`,\n // The GA4 measurement id loads this exact library. See `sharesLibrary`.\n sharesLibrary: 'googletagmanager.com/gtag/js',\n scriptMatch: 'googletagmanager.com/gtag/js',\n cookiePrefixes: ADVERTISING_COOKIE_PREFIXES,\n bootSnippet: (accountId: string) =>\n /*\n * DENIED FIRST, then granted — the AGL-1622 order.\n *\n * `gtag('consent', 'default', …)` has to run before the config, or the\n * library's own default applies to the first hit. This snippet is reached\n * only where the gate already said yes, so the grant follows immediately;\n * declaring the default anyway is what makes the first hit carry a state\n * somebody chose rather than Google's.\n *\n * `ad_user_data` and `ad_personalization` are named alongside\n * `ad_storage`: Consent Mode v2 treats them as separate signals, and a\n * grant that sets only storage leaves the other two at the library's\n * default on every EEA request.\n */\n /*\n * NO `consent default` here, deliberately.\n *\n * A default is a page-level declaration and something else may already\n * have made it — the GA4 loader above declares one, and re-declaring it\n * mid-pageview re-DENIES what that grant allowed until the update lands.\n * This snippet is reached only where the gate already said yes, so it\n * states the update and leaves the default to whoever mounts first.\n *\n * `ad_user_data` and `ad_personalization` travel with `ad_storage`:\n * Consent Mode v2 treats them as separate signals, and an update setting\n * only storage leaves the other two at the library's default on every\n * EEA request.\n */\n 'window.dataLayer=window.dataLayer||[];' +\n 'function gtag(){dataLayer.push(arguments);}' +\n \"gtag('consent','update',{ad_storage:'granted',\" +\n \"ad_user_data:'granted',ad_personalization:'granted'});\" +\n \"gtag('js', new Date());\" +\n `gtag('config', '${accountId}');`,\n setConsent: (scope: Record<string, unknown>, granted: boolean) => {\n try {\n const gtag = scope.gtag\n if (typeof gtag === 'function') {\n ;(gtag as (...args: unknown[]) => void)('consent', 'update', {\n ad_storage: granted ? 'granted' : 'denied',\n ad_user_data: granted ? 'granted' : 'denied',\n ad_personalization: granted ? 'granted' : 'denied',\n })\n }\n } catch {\n // A tag that throws on its own consent call is one we cannot silence\n // that way; the element removal and the cookie sweep still stand.\n }\n },\n}\n\n/**\n * LinkedIn Insight Tag.\n *\n * ## Cookies, and why the prefix list is long\n *\n * LinkedIn writes more names than the other two and they do not share a stem:\n * `li_sugr` and `UserMatchHistory` are the retargeting identifiers, `bcookie`\n * and `lidc` are set on the `.linkedin.com` domain by the loader, and\n * `AnalyticsSyncHistory` records the last sync. A prefix list that named only\n * `li_` would leave three of them behind on withdrawal — the AGL-2486 shape,\n * where a sweep looks thorough because it cleared the ones that happen to\n * share a prefix.\n *\n * ⚠️ `bcookie` and `lidc` are written at `.linkedin.com`, a domain a page on\n * our origin cannot delete through `document.cookie`. The sweep removes what\n * it can reach and the element removal stops the tag writing more; the rest is\n * LinkedIn's to hold, and saying so is better than a sweep that quietly\n * half-works.\n */\nexport const LINKEDIN_INSIGHT_VENDOR: AdvertisingVendor = {\n id: 'linkedin',\n label: 'LinkedIn Insight Tag',\n accountIdPattern: LINKEDIN_PARTNER_ID_PATTERN,\n scriptSrc: 'https://snap.licdn.com/li.lms-analytics/insight.min.js',\n scriptMatch: 'snap.licdn.com',\n cookiePrefixes: [\n 'li_sugr',\n 'UserMatchHistory',\n 'AnalyticsSyncHistory',\n 'bcookie',\n 'lidc',\n 'li_gc',\n ],\n bootSnippet: (accountId: string) =>\n // The Insight Tag reads its partner id off a global array the library\n // drains on load, so the id is pushed before the script is appended\n // rather than passed to it.\n `window._linkedin_partner_id='${accountId}';` +\n 'window._linkedin_data_partner_ids=window._linkedin_data_partner_ids||[];' +\n 'window._linkedin_data_partner_ids.push(window._linkedin_partner_id);',\n setConsent: (scope: Record<string, unknown>, granted: boolean) => {\n try {\n // The tag has no consent API of its own. What it does have is the\n // partner-id array it drains on load: emptying it stops a late or\n // re-inserted library finding an id to report against. The element\n // removal and the cookie sweep remain the substance.\n if (!granted) scope._linkedin_data_partner_ids = []\n } catch {\n // Same standing as the others: the teardown does not depend on this.\n }\n },\n}\n\n/**\n * TikTok Pixel (AGL-3694) — a site owner's own, from their TikTok Ads Manager.\n *\n * TikTok's published base code is a loader that inserts `events.js` itself.\n * Here the QUEUE half of it runs as the boot snippet and the library is the\n * marked `<script>` the mount renders, so the teardown can find it — the same\n * split every vendor in this registry has. `ttq.load` is reduced accordingly\n * to the registration it does (`_i`, `_t`, `_o` for the pixel code), without\n * the insertion; `events.js` reads that registration when it arrives.\n *\n * `grantConsent`/`revokeConsent` are TikTok's documented consent controls,\n * and are the `fbq('consent', …)` analogue for the AGL-1608 order: told\n * first, then removed, then swept.\n *\n * Cookies: `_ttp` (the browser id), `_tt_enable_cookie` (the cookie probe) and\n * `ttcsid` / `ttcsid_<pixel>` (the session), all first-party at the\n * registrable domain, which the ladder sweep reaches.\n */\nexport const TIKTOK_PIXEL_VENDOR: AdvertisingVendor = {\n id: 'tiktok',\n label: 'TikTok Pixel',\n accountIdPattern: TIKTOK_PIXEL_ID_PATTERN,\n scriptSrc: 'https://analytics.tiktok.com/i18n/pixel/events.js',\n scriptSrcFor: (accountId: string) =>\n `https://analytics.tiktok.com/i18n/pixel/events.js?sdkid=${accountId}&lib=ttq`,\n scriptMatch: 'analytics.tiktok.com',\n cookiePrefixes: ['_ttp', '_tt_enable_cookie', 'ttcsid'],\n bootSnippet: (accountId: string) =>\n \"!function(w,t){w.TiktokAnalyticsObject=t;var ttq=w[t]=w[t]||[];\" +\n 'ttq.methods=[\"page\",\"track\",\"identify\",\"instances\",\"debug\",\"on\",\"off\",' +\n '\"once\",\"ready\",\"alias\",\"group\",\"enableCookie\",\"disableCookie\",' +\n '\"holdConsent\",\"revokeConsent\",\"grantConsent\"];' +\n 'ttq.setAndDefer=function(t,e){t[e]=function(){' +\n 't.push([e].concat(Array.prototype.slice.call(arguments,0)))}};' +\n 'for(var i=0;i<ttq.methods.length;i++)ttq.setAndDefer(ttq,ttq.methods[i]);' +\n 'ttq.instance=function(t){for(var e=ttq._i[t]||[],n=0;n<ttq.methods.length;n++)' +\n 'ttq.setAndDefer(e,ttq.methods[n]);return e};' +\n 'ttq.load=function(e,n){var r=\"https://analytics.tiktok.com/i18n/pixel/events.js\";' +\n 'ttq._i=ttq._i||{};ttq._i[e]=[];ttq._i[e]._u=r;ttq._t=ttq._t||{};' +\n 'ttq._t[e]=+new Date;ttq._o=ttq._o||{};ttq._o[e]=n||{}}}' +\n \"(window,'ttq');\" +\n `ttq.load('${accountId}');` +\n 'ttq.grantConsent();' +\n 'ttq.page();',\n setConsent: (scope: Record<string, unknown>, granted: boolean) => {\n try {\n const ttq = scope.ttq as Record<string, unknown> | undefined\n const call = ttq?.[granted ? 'grantConsent' : 'revokeConsent']\n if (typeof call === 'function') (call as () => void).call(ttq)\n } catch {\n // The element removal and the cookie sweep still stand.\n }\n },\n}\n\n/**\n * Pinterest Tag (AGL-3694) — a site owner's own, from Pinterest Ads.\n *\n * The published base code is a queue shim plus an inserted `core.js`; as with\n * TikTok the shim is the boot and the library is our marked element.\n *\n * Pinterest publishes no consent call for the tag. On withdrawal the queue\n * function is replaced with one that drops every call, so a late or cached\n * `core.js` finds nothing to report; the element removal and the sweep are\n * the substance, as they are for LinkedIn.\n *\n * Cookies: `_pin_unauth` (the browser id), `_pinterest_ct_ua` /\n * `_pinterest_ct_rt` (conversion tracking) and `_epik` / `_derived_epik` (the\n * click id) — first-party; `_pinterest_sess` lives on pinterest.com and only\n * Pinterest can clear it.\n */\nexport const PINTEREST_TAG_VENDOR: AdvertisingVendor = {\n id: 'pinterest',\n label: 'Pinterest Tag',\n accountIdPattern: PINTEREST_TAG_ID_PATTERN,\n scriptSrc: 'https://s.pinimg.com/ct/core.js',\n scriptMatch: 's.pinimg.com',\n cookiePrefixes: ['_pin_unauth', '_pinterest_ct', '_epik', '_derived_epik'],\n bootSnippet: (accountId: string) =>\n '!function(){if(!window.pintrk){window.pintrk=function(){' +\n 'window.pintrk.queue.push(Array.prototype.slice.call(arguments))};' +\n 'var n=window.pintrk;n.queue=[];n.version=\"3.0\"}}();' +\n `pintrk('load','${accountId}');` +\n \"pintrk('page');\",\n setConsent: (scope: Record<string, unknown>, granted: boolean) => {\n try {\n if (!granted) scope.pintrk = function pintrkWithdrawn() {}\n } catch {\n // Same standing as the others: the teardown does not depend on this.\n }\n },\n}\n\n/**\n * Every vendor this gate knows how to tear down — which is a SUPERSET of the\n * vendors it knows how to load, now that a sweep-only member exists.\n */\nexport const ADVERTISING_VENDORS: readonly AdvertisingVendor[] = [\n META_PIXEL_VENDOR,\n GOOGLE_ADS_VENDOR,\n LINKEDIN_INSIGHT_VENDOR,\n TIKTOK_PIXEL_VENDOR,\n PINTEREST_TAG_VENDOR,\n]\n\n/**\n * The host fields this module reads, on top of the consent ones.\n *\n * `adTags` maps a vendor id to that vendor's account id: on our marketing\n * host, our own; on a customer's site, the owner's own (AGL-3694).\n */\nexport interface AdvertisingTagHost extends VisitorConsentHost {\n analytics?: {\n gaMeasurementId?: string\n /** Vendor id → account id. Absent on every site that exists. */\n adTags?: Record<string, string> | null\n } | null\n}\n\n/** A vendor that may load, paired with the id it was configured with. */\nexport interface ResolvedAdvertisingTag {\n readonly vendor: AdvertisingVendor\n readonly accountId: string\n /**\n * The tag is a site owner's own, on their own site (AGL-3694): it is marked\n * {@link ADVERTISING_EVENTS_ATTRIBUTE} and takes the site's conversion\n * events. Absent on our own surfaces' tags.\n */\n readonly siteOwned?: true\n}\n\n/**\n * The whole verdict: which advertising tags may exist in this document, for\n * this visitor, on this site, right now?\n *\n * Pure, and empty is the answer to every question it cannot answer — an absent\n * host, an unreadable record, an unknown vendor id, a malformed account id.\n * See the module comment for the six conditions and why each one is separate.\n *\n * `stored` is the CLIENT-resolved record. Like the GA gate, this is evaluated\n * after hydration only: tenant pages are ISR-cached, so the server HTML must\n * be identical for every visitor and cannot carry a tag one of them granted.\n *\n * `internal` is read the same way and for the same reason. Taking it as a\n * defaulted parameter rather than calling into the browser mid-verdict is what\n * keeps this function testable in both directions — a gate that can only be\n * exercised one way is the shape that ships broken (AGL-2067).\n */\nexport function resolveAdvertisingTags(\n host: AdvertisingTagHost | null | undefined,\n stored: StoredVisitorConsent | null | undefined,\n env: AnalyticsEnvironment = readAnalyticsEnvironment(),\n internal: boolean = readInternalTrafficOverride(),\n): ResolvedAdvertisingTag[] {\n // Condition 1, as of AGL-3694: OUR surface, or a customer's site mounting\n // the tags ITS OWNER configured. Either way the account ids come from this\n // host's own document, so nothing here can put one site's tag on another.\n const siteOwned = isPlatformMarketingHost(host) === false\n if (analyticsMayEmit(env) === false) return []\n if (hostConsentRequired(host) === false) return []\n if (advertisingGrantedByRecord(host, stored) === false) return []\n // Condition 6. Structural, like every other clause here: the tag is not\n // mounted rather than mounted-and-suppressed, because a resident tag fires\n // on its own and a `_gcl_*` cookie is written by the first automatic event.\n //\n // The environment half is not redundant with `analyticsMayEmit` above. That\n // clause passes under the non-production escape hatch, and a build emitting\n // because someone asked it to is ours by definition — so the hatch must not\n // hand a dev or preview build the real `AW-` id and let it build remarketing\n // audiences out of our own engineers, which is the hole condition 2 exists\n // to close. Same reasoning as `INTERNAL_TRAFFIC_FORCED_SNIPPET`.\n if (internal === true) return []\n if (analyticsEnvironmentForcesInternal(env) === true) return []\n const configured = host?.analytics?.adTags\n if (!configured) return []\n const tags: ResolvedAdvertisingTag[] = []\n for (const vendor of ADVERTISING_VENDORS) {\n // Sweep-only: nothing to mount, and no `accountIdPattern` to test with.\n // Skipped explicitly rather than left to fail a pattern check, so a stray\n // `adTags['google-ads']` cannot conjure a script (AGL-2486).\n if (vendor.sweepOnly || !vendor.accountIdPattern) continue\n const accountId = String(configured[vendor.id] ?? '')\n if (vendor.accountIdPattern.test(accountId)) {\n tags.push(siteOwned ? { vendor, accountId, siteOwned: true } : { vendor, accountId })\n }\n }\n return tags\n}\n\n/**\n * The script elements THIS MODULE put in the document for a given vendor —\n * both the inline boot and the library, since a vendor mounts as a pair.\n *\n * Scoped by {@link ADVERTISING_TAG_ATTRIBUTE}, which is the clause that keeps\n * a pixel a customer pasted into their own Custom HTML out of our teardown.\n * An element that carries the attribute but whose `src` points somewhere other\n * than this vendor is excluded as well: the attribute is ours to write, so a\n * mismatch means something rewrote it and the safe reading is \"not ours\".\n * An element with NO `src` is the inline boot and passes — it has no URL to\n * disagree with.\n */\nfunction markedVendorElements(vendor: AdvertisingVendor): Element[] {\n if (typeof document === 'undefined') return []\n // A sweep-only vendor has no script of ours and therefore no element that\n // could be \"ours\" (AGL-2486). Returning early is also what stops an absent\n // `scriptMatch` reaching `src.includes(...)` below — an empty string there\n // would have matched EVERY marked script, handing one vendor's teardown\n // another vendor's elements.\n if (vendor.sweepOnly || !vendor.scriptMatch) return []\n try {\n const selector = `script[${ADVERTISING_TAG_ATTRIBUTE}=\"${vendor.id}\"]`\n return Array.from(document.querySelectorAll(selector)).filter((element) => {\n const src = String((element as HTMLScriptElement).src ?? '')\n return src === '' || src.includes(vendor.scriptMatch)\n })\n } catch {\n // A hostile or absent DOM: nothing we can prove is ours.\n return []\n }\n}\n\n/** Every vendor whose tag this module loaded and that is still resident. */\nexport function residentAdvertisingVendors(): AdvertisingVendor[] {\n return ADVERTISING_VENDORS.filter(\n (vendor) => markedVendorElements(vendor).length > 0,\n )\n}\n\n/**\n * Stop every advertising tag THIS MODULE loaded, and return the vendor ids it\n * acted on — which is what makes the withdrawal assertable rather than\n * assumed.\n *\n * Three steps, in this order and for these reasons:\n *\n * 1. **Revoke on the resident tag.** The gate cannot unload a script that has\n * already executed. This is the vendor's own kill switch and it is what\n * actually stops the next automatic event.\n * 2. **Remove the script elements.** So the structural property the gate\n * asserts — the vendor's script is not in this document — is restored and\n * not merely claimed. React unmounting its `<Script>` does not reliably do\n * this: `next/script` injects into the head and leaves it there.\n * 3. **Sweep the cookies.** Last, because steps 1 and 2 are what stop them\n * coming straight back (AGL-1608).\n *\n * Acts only on tags carrying {@link ADVERTISING_TAG_ATTRIBUTE}: the tags this\n * module mounted, which on a customer's site are the ones its owner configured\n * on Setup → Tracking (AGL-3694). A pixel pasted into Custom HTML carries no\n * mark and is never touched — it runs on a basis that is not ours to\n * withdraw.\n */\nexport function revokeAdvertisingTags(hostname?: string | null): string[] {\n if (typeof window === 'undefined') return []\n const acted: string[] = []\n const scope = window as unknown as Record<string, unknown>\n for (const vendor of ADVERTISING_VENDORS) {\n /*\n * The element check is an OWNERSHIP test (AGL-1498 case (e)).\n *\n * A pixel we did not load is one running on a basis that is not ours to\n * withdraw — a customer's own Custom HTML, on their own site, under their\n * own notice. Killing it, or clearing its cookies, would be us\n * reconfiguring their site against a consent record their tag never ran\n * on. So no marker of ours means hands off, cookies included.\n *\n * `alwaysSweep` is the narrow exception and it is about the COOKIE, not\n * the loader: `_gcl_*` is written by any Google tag — a GTM container, a\n * bare gtag — with no marker of ours ever existing, which is why it\n * survived every withdrawal until AGL-2486. Giving Google a loader must\n * not quietly take that sweep away again.\n *\n * ORDER, which is the whole of AGL-1608: revoke on the tag FIRST, then\n * remove it, then sweep. Sweeping first deletes the cookies and the\n * resident pixel's next automatic event writes them straight back inside\n * the same pageview.\n */\n const elements = vendor.sweepOnly ? [] : markedVendorElements(vendor)\n const ours = elements.length > 0\n if (ours) {\n vendor.setConsent?.(scope, false)\n for (const element of elements) {\n try {\n element.remove()\n } catch {\n // A detached or frozen node: the revoke above still stands.\n }\n }\n }\n const maySweep = ours || vendor.sweepOnly || vendor.alwaysSweep\n const swept = maySweep\n ? clearCookiesWithPrefixes(vendor.cookiePrefixes, hostname)\n : []\n if (ours || swept.length > 0) acted.push(vendor.id)\n }\n return acted\n}\n\n/**\n * Re-grant on a tag that is still resident — the symmetric half.\n *\n * A visitor who withdraws and changes their mind inside one pageview would\n * otherwise stay silently un-tracked until they navigated, because a re-\n * rendered `<Script>` cannot re-execute a library the browser already ran.\n * `setResidentAnalyticsTags` is symmetric for exactly this reason and this is\n * the same move for a second channel.\n *\n * Returns the vendor ids it reached; empty when nothing is resident, which is\n * the normal case — a re-grant after the elements were removed goes through\n * the gate and loads a fresh tag instead.\n */\nexport function restoreAdvertisingTags(): string[] {\n if (typeof window === 'undefined') return []\n const scope = window as unknown as Record<string, unknown>\n const acted: string[] = []\n for (const vendor of residentAdvertisingVendors()) {\n vendor.setConsent?.(scope, true)\n acted.push(vendor.id)\n }\n return acted\n}\n"],"names":["GOOGLE_ADS_ID_PATTERN","LINKEDIN_PARTNER_ID_PATTERN","META_PIXEL_ID_PATTERN","PINTEREST_TAG_ID_PATTERN","TIKTOK_PIXEL_ID_PATTERN","ADVERTISING_EVENTS_ATTRIBUTE","ADVERTISING_TAG_ATTRIBUTE","analyticsEnvironmentForcesInternal","analyticsMayEmit","readAnalyticsEnvironment","readInternalTrafficOverride","isPlatformMarketingHost","ADVERTISING_COOKIE_PREFIXES","advertisingGrantedByRecord","clearCookiesWithPrefixes","hostConsentRequired","META_PIXEL_VENDOR","id","label","accountIdPattern","scriptSrc","scriptMatch","cookiePrefixes","bootSnippet","accountId","setConsent","scope","granted","fbq","GOOGLE_ADS_VENDOR","alwaysSweep","scriptSrcFor","sharesLibrary","gtag","ad_storage","ad_user_data","ad_personalization","LINKEDIN_INSIGHT_VENDOR","_linkedin_data_partner_ids","TIKTOK_PIXEL_VENDOR","ttq","call","PINTEREST_TAG_VENDOR","pintrk","pintrkWithdrawn","ADVERTISING_VENDORS","resolveAdvertisingTags","host","stored","env","internal","siteOwned","configured","analytics","adTags","tags","vendor","sweepOnly","String","test","push","markedVendorElements","document","selector","Array","from","querySelectorAll","filter","element","src","includes","residentAdvertisingVendors","length","revokeAdvertisingTags","hostname","window","acted","elements","ours","remove","maySweep","swept","restoreAdvertisingTags"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmHC,GAED,SACEA,qBAAqB,EACrBC,2BAA2B,EAC3BC,qBAAqB,EACrBC,wBAAwB,EACxBC,uBAAuB,QAClB,uBAAmB;AAC1B,SACEC,4BAA4B,EAC5BC,yBAAyB,QACpB,0BAAsB;AAC7B,SACEC,kCAAkC,EAClCC,gBAAgB,EAEhBC,wBAAwB,QACnB,6BAAyB;AAChC,SAASC,2BAA2B,QAAQ,wBAAoB;AAChE,SAASC,uBAAuB,QAAQ,+BAA2B;AACnE,SACEC,2BAA2B,EAC3BC,0BAA0B,EAC1BC,wBAAwB,EACxBC,mBAAmB,QAGd,uBAAmB;AAE1B;;;;;;;;;;;CAWC,GACD,SAASV,4BAA4B,EAAEC,yBAAyB,GAAE;AA8HlE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6BC,GACD,OAAO,MAAMU,oBAAuC;IAClDC,IAAI;IACJC,OAAO;IACP,wEAAwE;IACxE,8CAA8C;IAC9CC,kBAAkBjB;IAClBkB,WAAW;IACXC,aAAa;IACbC,gBAAgB;QAAC;QAAQ;KAAO;IAChCC,aAAa,CAACC,YACZ,yEAAyE;QACzE,0EAA0E;QAC1E,yEAAyE;QACzE,0EAA0E;QAC1E,+DAA+D;QAC/D,8EACA,8DACA,yEACA,uBACA,6BACA,CAAC,aAAa,EAAEA,UAAU,GAAG,CAAC,GAC9B;IACFC,YAAY,CAACC,OAAgCC;QAC3C,IAAI;YACF,MAAMC,MAAMF,MAAME,GAAG;YACrB,IAAI,OAAOA,QAAQ,YAAY;;gBAC3BA,IACA,WACAD,UAAU,UAAU;YAExB;QACF,EAAE,eAAM;QACN,qEAAqE;QACrE,kEAAkE;QACpE;IACF;AACF,EAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;CA0BC,GACD,OAAO,MAAME,oBAAuC;IAClDZ,IAAI;IACJC,OAAO;IACP;;;;;;;;;;;;;GAaC,GACDC,kBAAkBnB;IAClB,sEAAsE;IACtE,6DAA6D;IAC7D8B,aAAa;IACbV,WAAW;IACX,2EAA2E;IAC3E,4EAA4E;IAC5E,mEAAmE;IACnEW,cAAc,CAACP,YACb,CAAC,4CAA4C,EAAEA,WAAW;IAC5D,wEAAwE;IACxEQ,eAAe;IACfX,aAAa;IACbC,gBAAgBV;IAChBW,aAAa,CAACC,YACZ;;;;;;;;;;;;;KAaC,GACD;;;;;;;;;;;;;KAaC,GACD,2CACA,gDACA,mDACA,2DACA,4BACA,CAAC,gBAAgB,EAAEA,UAAU,GAAG,CAAC;IACnCC,YAAY,CAACC,OAAgCC;QAC3C,IAAI;YACF,MAAMM,OAAOP,MAAMO,IAAI;YACvB,IAAI,OAAOA,SAAS,YAAY;;gBAC5BA,KAAsC,WAAW,UAAU;oBAC3DC,YAAYP,UAAU,YAAY;oBAClCQ,cAAcR,UAAU,YAAY;oBACpCS,oBAAoBT,UAAU,YAAY;gBAC5C;YACF;QACF,EAAE,eAAM;QACN,qEAAqE;QACrE,kEAAkE;QACpE;IACF;AACF,EAAC;AAED;;;;;;;;;;;;;;;;;;CAkBC,GACD,OAAO,MAAMU,0BAA6C;IACxDpB,IAAI;IACJC,OAAO;IACPC,kBAAkBlB;IAClBmB,WAAW;IACXC,aAAa;IACbC,gBAAgB;QACd;QACA;QACA;QACA;QACA;QACA;KACD;IACDC,aAAa,CAACC,YACZ,sEAAsE;QACtE,oEAAoE;QACpE,4BAA4B;QAC5B,CAAC,6BAA6B,EAAEA,UAAU,EAAE,CAAC,GAC7C,6EACA;IACFC,YAAY,CAACC,OAAgCC;QAC3C,IAAI;YACF,kEAAkE;YAClE,kEAAkE;YAClE,mEAAmE;YACnE,qDAAqD;YACrD,IAAI,CAACA,SAASD,MAAMY,0BAA0B,GAAG,EAAE;QACrD,EAAE,eAAM;QACN,qEAAqE;QACvE;IACF;AACF,EAAC;AAED;;;;;;;;;;;;;;;;;CAiBC,GACD,OAAO,MAAMC,sBAAyC;IACpDtB,IAAI;IACJC,OAAO;IACPC,kBAAkBf;IAClBgB,WAAW;IACXW,cAAc,CAACP,YACb,CAAC,wDAAwD,EAAEA,UAAU,QAAQ,CAAC;IAChFH,aAAa;IACbC,gBAAgB;QAAC;QAAQ;QAAqB;KAAS;IACvDC,aAAa,CAACC,YACZ,oEACA,2EACA,mEACA,mDACA,mDACA,mEACA,8EACA,mFACA,iDACA,sFACA,qEACA,4DACA,oBACA,CAAC,UAAU,EAAEA,UAAU,GAAG,CAAC,GAC3B,wBACA;IACFC,YAAY,CAACC,OAAgCC;QAC3C,IAAI;YACF,MAAMa,MAAMd,MAAMc,GAAG;YACrB,MAAMC,OAAOD,uBAAAA,GAAK,CAACb,UAAU,iBAAiB,gBAAgB;YAC9D,IAAI,OAAOc,SAAS,YAAY,AAACA,KAAoBA,IAAI,CAACD;QAC5D,EAAE,eAAM;QACN,wDAAwD;QAC1D;IACF;AACF,EAAC;AAED;;;;;;;;;;;;;;;CAeC,GACD,OAAO,MAAME,uBAA0C;IACrDzB,IAAI;IACJC,OAAO;IACPC,kBAAkBhB;IAClBiB,WAAW;IACXC,aAAa;IACbC,gBAAgB;QAAC;QAAe;QAAiB;QAAS;KAAgB;IAC1EC,aAAa,CAACC,YACZ,6DACA,sEACA,wDACA,CAAC,eAAe,EAAEA,UAAU,GAAG,CAAC,GAChC;IACFC,YAAY,CAACC,OAAgCC;QAC3C,IAAI;YACF,IAAI,CAACA,SAASD,MAAMiB,MAAM,GAAG,SAASC,mBAAmB;QAC3D,EAAE,eAAM;QACN,qEAAqE;QACvE;IACF;AACF,EAAC;AAED;;;CAGC,GACD,OAAO,MAAMC,sBAAoD;IAC/D7B;IACAa;IACAQ;IACAE;IACAG;CACD,CAAA;AA4BD;;;;;;;;;;;;;;;;CAgBC,GACD,OAAO,SAASI,uBACdC,IAA2C,EAC3CC,MAA+C,EAC/CC,MAA4BxC,0BAA0B,EACtDyC,WAAoBxC,6BAA6B;QAqB9BqC;IAnBnB,0EAA0E;IAC1E,2EAA2E;IAC3E,0EAA0E;IAC1E,MAAMI,YAAYxC,wBAAwBoC,UAAU;IACpD,IAAIvC,iBAAiByC,SAAS,OAAO,OAAO,EAAE;IAC9C,IAAIlC,oBAAoBgC,UAAU,OAAO,OAAO,EAAE;IAClD,IAAIlC,2BAA2BkC,MAAMC,YAAY,OAAO,OAAO,EAAE;IACjE,wEAAwE;IACxE,2EAA2E;IAC3E,4EAA4E;IAC5E,EAAE;IACF,4EAA4E;IAC5E,4EAA4E;IAC5E,4EAA4E;IAC5E,6EAA6E;IAC7E,2EAA2E;IAC3E,iEAAiE;IACjE,IAAIE,aAAa,MAAM,OAAO,EAAE;IAChC,IAAI3C,mCAAmC0C,SAAS,MAAM,OAAO,EAAE;IAC/D,MAAMG,aAAaL,yBAAAA,kBAAAA,KAAMM,SAAS,qBAAfN,gBAAiBO,MAAM;IAC1C,IAAI,CAACF,YAAY,OAAO,EAAE;IAC1B,MAAMG,OAAiC,EAAE;IACzC,KAAK,MAAMC,UAAUX,oBAAqB;YAKfO;QAJzB,wEAAwE;QACxE,0EAA0E;QAC1E,6DAA6D;QAC7D,IAAII,OAAOC,SAAS,IAAI,CAACD,OAAOrC,gBAAgB,EAAE;QAClD,MAAMK,YAAYkC,QAAON,wBAAAA,UAAU,CAACI,OAAOvC,EAAE,CAAC,YAArBmC,wBAAyB;QAClD,IAAII,OAAOrC,gBAAgB,CAACwC,IAAI,CAACnC,YAAY;YAC3C+B,KAAKK,IAAI,CAACT,YAAY;gBAAEK;gBAAQhC;gBAAW2B,WAAW;YAAK,IAAI;gBAAEK;gBAAQhC;YAAU;QACrF;IACF;IACA,OAAO+B;AACT;AAEA;;;;;;;;;;;CAWC,GACD,SAASM,qBAAqBL,MAAyB;IACrD,IAAI,OAAOM,aAAa,aAAa,OAAO,EAAE;IAC9C,0EAA0E;IAC1E,2EAA2E;IAC3E,2EAA2E;IAC3E,wEAAwE;IACxE,6BAA6B;IAC7B,IAAIN,OAAOC,SAAS,IAAI,CAACD,OAAOnC,WAAW,EAAE,OAAO,EAAE;IACtD,IAAI;QACF,MAAM0C,WAAW,CAAC,OAAO,EAAEzD,0BAA0B,EAAE,EAAEkD,OAAOvC,EAAE,CAAC,EAAE,CAAC;QACtE,OAAO+C,MAAMC,IAAI,CAACH,SAASI,gBAAgB,CAACH,WAAWI,MAAM,CAAC,CAACC;gBAC1C;YAAnB,MAAMC,MAAMX,QAAO,eAAA,AAACU,QAA8BC,GAAG,YAAlC,eAAsC;YACzD,OAAOA,QAAQ,MAAMA,IAAIC,QAAQ,CAACd,OAAOnC,WAAW;QACtD;IACF,EAAE,eAAM;QACN,yDAAyD;QACzD,OAAO,EAAE;IACX;AACF;AAEA,0EAA0E,GAC1E,OAAO,SAASkD;IACd,OAAO1B,oBAAoBsB,MAAM,CAC/B,CAACX,SAAWK,qBAAqBL,QAAQgB,MAAM,GAAG;AAEtD;AAEA;;;;;;;;;;;;;;;;;;;;;;CAsBC,GACD,OAAO,SAASC,sBAAsBC,QAAwB;IAC5D,IAAI,OAAOC,WAAW,aAAa,OAAO,EAAE;IAC5C,MAAMC,QAAkB,EAAE;IAC1B,MAAMlD,QAAQiD;IACd,KAAK,MAAMnB,UAAUX,oBAAqB;QACxC;;;;;;;;;;;;;;;;;;;KAmBC,GACD,MAAMgC,WAAWrB,OAAOC,SAAS,GAAG,EAAE,GAAGI,qBAAqBL;QAC9D,MAAMsB,OAAOD,SAASL,MAAM,GAAG;QAC/B,IAAIM,MAAM;YACRtB,OAAO/B,UAAU,oBAAjB+B,OAAO/B,UAAU,MAAjB+B,QAAoB9B,OAAO;YAC3B,KAAK,MAAM0C,WAAWS,SAAU;gBAC9B,IAAI;oBACFT,QAAQW,MAAM;gBAChB,EAAE,eAAM;gBACN,4DAA4D;gBAC9D;YACF;QACF;QACA,MAAMC,WAAWF,QAAQtB,OAAOC,SAAS,IAAID,OAAO1B,WAAW;QAC/D,MAAMmD,QAAQD,WACVlE,yBAAyB0C,OAAOlC,cAAc,EAAEoD,YAChD,EAAE;QACN,IAAII,QAAQG,MAAMT,MAAM,GAAG,GAAGI,MAAMhB,IAAI,CAACJ,OAAOvC,EAAE;IACpD;IACA,OAAO2D;AACT;AAEA;;;;;;;;;;;;CAYC,GACD,OAAO,SAASM;IACd,IAAI,OAAOP,WAAW,aAAa,OAAO,EAAE;IAC5C,MAAMjD,QAAQiD;IACd,MAAMC,QAAkB,EAAE;IAC1B,KAAK,MAAMpB,UAAUe,6BAA8B;QACjDf,OAAO/B,UAAU,oBAAjB+B,OAAO/B,UAAU,MAAjB+B,QAAoB9B,OAAO;QAC3BkD,MAAMhB,IAAI,CAACJ,OAAOvC,EAAE;IACtB;IACA,OAAO2D;AACT"}
@@ -14,90 +14,6 @@
14
14
  * See the License for the specific language governing permissions and
15
15
  * limitations under the License.
16
16
  */
17
- /**
18
- * The one GA4 event taxonomy, shared by the marketing site (tenant runtime)
19
- * and the console (AGL-1561). See `docs/ANALYTICS.md` for the event map and
20
- * which GTM-plan §6 metric each event serves.
21
- *
22
- * ## Why this module exists
23
- *
24
- * Before it, every GA event in the repo was an ad-hoc
25
- * `;(window as any).gtag?.('event', 'name', {...})` — five of them across the
26
- * marketing and commerce plugins, plus bare string literals passed to Firebase
27
- * `logEvent` in the console. Nothing checked the names, nothing checked the
28
- * params, and a typo produced a silently-missing metric rather than an error.
29
- * That is the failure mode analytics is worst at surfacing: the number simply
30
- * reads zero, and zero is indistinguishable from "nobody did it".
31
- *
32
- * So the names and their params are a TYPE here ({@link AnalyticsEventParams}),
33
- * and `trackEvent` is generic over it: a misspelled event name or a missing
34
- * required param is a compile error.
35
- *
36
- * That sweep missed five (AGL-1591, closed): the commerce plugin's
37
- * `view_item` / `add_to_cart` / `begin_checkout` and the marketing runtime's
38
- * `aglyn_overlay` / `aglyn_experiment`. `window.gtag` is now called in exactly
39
- * two places in the repo — {@link deliver} below, and `readGaClientId` above,
40
- * which reads rather than sends.
41
- *
42
- * ## Reserved names
43
- *
44
- * Where GA4 has a recommended event we use its exact name and its exact param
45
- * spelling — `sign_up`, `login`, `generate_lead`, `begin_checkout`,
46
- * `purchase`, `select_content` — so the built-in reports, the funnel
47
- * explorations and the "key events" toggles work without custom definitions.
48
- * Custom snake_case names appear only where GA4 has no standard: the four
49
- * activation events, which are Aglyn-specific product milestones.
50
- *
51
- * ## Consent (AGL-1498) — the gate is that gtag never loads
52
- *
53
- * On tenant sites, including aglyn.com itself, `site-analytics.tsx` renders the
54
- * gtag `<Script>` pair ONLY when the recorded consent state grants analytics.
55
- * There is therefore no `window.gtag` at all for a visitor who has not granted,
56
- * and {@link trackEvent} drops the event on the floor.
57
- *
58
- * It drops it — it does not QUEUE it. That distinction is the whole point and
59
- * `analytics-events.spec.ts` asserts it: an event fired before consent is gone
60
- * for good, and does not reappear when a later grant loads gtag. A queue would
61
- * quietly convert "we did not track you" into "we tracked you and waited", and
62
- * a replayed hit carries the pre-consent timestamp and page into GA, which is
63
- * exactly the thing the consent gate exists to prevent. Deliberately no retry,
64
- * no buffer, no flush-on-grant.
65
- *
66
- * ## No PII, enforced rather than promised
67
- *
68
- * Every payload passes through {@link sanitizeEventParams} before it reaches a
69
- * transport: an exact-key denylist drops the identity-bearing params someone
70
- * will eventually add by reflex (`email`, `org_name`, `first_name`, ...), any
71
- * value that looks like an email address drops its key entirely, URLs are
72
- * reduced to origin + pathname so query strings can never smuggle a token or
73
- * an address, and strings are length-capped. The console separately sets a
74
- * `user_id` — that is an opaque Firebase uid and is the one identifier GA is
75
- * allowed to hold.
76
- *
77
- * Sanitizing here rather than at each call site is the point: a new call site
78
- * cannot forget.
79
- *
80
- * ## Authored events (AGL-1587)
81
- *
82
- * One call site cannot use the taxonomy at all: the `trackGaEvent` action step,
83
- * whose event name and params are typed by a SITE AUTHOR in the interaction
84
- * builder. A closed union cannot contain a name nobody has written yet, so
85
- * {@link trackAuthoredEvent} is the escape hatch — and it is deliberately the
86
- * only one, so that untrusted input still passes {@link sanitizeEventParams}
87
- * rather than reaching `window.gtag` raw.
88
- *
89
- * The test for which door an event uses is WHO NAMED IT, not whether GA4
90
- * recommends the name. `aglyn_overlay` and `aglyn_experiment` are outside GA4's
91
- * recommended set and were candidates for the hatch (AGL-1591); they are in the
92
- * union instead, because a developer wrote their names and their keys, so they
93
- * can have compile-time checking — and because the hatch guarantees the
94
- * opposite of what they need. {@link resolveAuthoredEventName} refuses any name
95
- * we send — the union AND the server-only names beside it — precisely so that
96
- * "not one of ours" means "authored"; put our own events through the hatch and
97
- * that stops being true, authored hits stop being separable from ours in
98
- * reports, and an authored `aglyn_experiment` step starts voting in the
99
- * experiment that decides which variant ships.
100
- */
101
17
  /**
102
18
  * Read the browser's GA `client_id` — the identifier that ties a hit to a GA
103
19
  * user and session.
@@ -560,7 +476,20 @@ export declare function sanitizeEventParams(params: Record<string, unknown> | un
560
476
  * analytics consent — the event is DROPPED, permanently. See the module
561
477
  * comment for why a queue would be the wrong answer.
562
478
  */
563
- export declare function trackEvent<K extends AnalyticsEventName>(name: K, params: AnalyticsEventParams[K]): void;
479
+ export declare function trackEvent<K extends AnalyticsEventName>(name: K, params: AnalyticsEventParams[K], options?: AnalyticsEventOptions): void;
480
+ /**
481
+ * What travels beside an event without being one of its parameters
482
+ * (AGL-3694). Never sent to the Google tag.
483
+ */
484
+ export interface AnalyticsEventOptions {
485
+ /**
486
+ * The id a site's own advertising tags send this conversion under, when the
487
+ * server reports the same conversion through a Conversions API and the
488
+ * vendor must pair the two (`advertising-events.ts`). A purchase derives its
489
+ * own from `transaction_id` and needs none.
490
+ */
491
+ advertisingEventId?: string | null;
492
+ }
564
493
  /**
565
494
  * Fire a GA4 event and resolve once it has been HANDED TO gtag — for the call
566
495
  * sites that navigate away in the same tick (AGL-1580).
@@ -599,7 +528,7 @@ export declare function trackEvent<K extends AnalyticsEventName>(name: K, params
599
528
  * bundles, which go straight to `window.gtag` — delivery is synchronous, so
600
529
  * this resolves immediately and costs the storefront checkout nothing at all.
601
530
  */
602
- export declare function trackEventBeforeNavigation<K extends AnalyticsEventName>(name: K, params: AnalyticsEventParams[K]): Promise<void>;
531
+ export declare function trackEventBeforeNavigation<K extends AnalyticsEventName>(name: K, params: AnalyticsEventParams[K], options?: AnalyticsEventOptions): Promise<void>;
603
532
  /**
604
533
  * Build the ONE `begin_checkout` payload, for every surface that starts a
605
534
  * checkout (AGL-1591).
@@ -97,7 +97,8 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
97
97
  * that stops being true, authored hits stop being separable from ours in
98
98
  * reports, and an authored `aglyn_experiment` step starts voting in the
99
99
  * experiment that decides which variant ships.
100
- */ /**
100
+ */ import { sendAdvertisingEvent } from "./advertising-events.js";
101
+ /**
101
102
  * Read the browser's GA `client_id` — the identifier that ties a hit to a GA
102
103
  * user and session.
103
104
  *
@@ -252,8 +253,8 @@ function scrubValue(value) {
252
253
  * `window.gtag` — which on a tenant site means the visitor has not granted
253
254
  * analytics consent — the event is DROPPED, permanently. See the module
254
255
  * comment for why a queue would be the wrong answer.
255
- */ export function trackEvent(name, params) {
256
- deliver(name, sanitizeEventParams(params));
256
+ */ export function trackEvent(name, params, options) {
257
+ deliver(name, sanitizeEventParams(params), options);
257
258
  }
258
259
  /**
259
260
  * How long a navigation may be held waiting for a hit to reach gtag. Short
@@ -297,8 +298,8 @@ function scrubValue(value) {
297
298
  * On a surface with no registered transport — the tenant runtime and the plugin
298
299
  * bundles, which go straight to `window.gtag` — delivery is synchronous, so
299
300
  * this resolves immediately and costs the storefront checkout nothing at all.
300
- */ export async function trackEventBeforeNavigation(name, params) {
301
- const delivered = deliver(name, sanitizeEventParams(params));
301
+ */ export async function trackEventBeforeNavigation(name, params, options) {
302
+ const delivered = deliver(name, sanitizeEventParams(params), options);
302
303
  // Synchronous transport (or none): already handed over, nothing to wait for.
303
304
  if (!delivered || typeof delivered.then !== 'function') return;
304
305
  await Promise.race([
@@ -439,7 +440,21 @@ function scrubValue(value) {
439
440
  * {@link trackAuthoredEvent}. Takes an ALREADY-sanitized payload — every
440
441
  * caller sanitizes first, which is what keeps "a new call site cannot forget"
441
442
  * true of the authored path too.
442
- */ function deliver(name, safe) {
443
+ */ function deliver(name, safe, options) {
444
+ // A site owner's own advertising tags (AGL-3694), BEFORE the Google path
445
+ // and independent of it: a site may run a pixel and no Google tag at all.
446
+ // Structural like the rest — only a tag the consent gate mounted on the
447
+ // merchant's own site carries the marks this looks for, so for a visitor
448
+ // who did not grant advertising this reaches nothing. Synchronous, so it
449
+ // holds no navigation.
450
+ try {
451
+ var _ref;
452
+ sendAdvertisingEvent(name, safe, {
453
+ eventId: (_ref = options == null ? void 0 : options.advertisingEventId) != null ? _ref : null
454
+ });
455
+ } catch (unused) {
456
+ // Never breaks the page, like every other delivery here.
457
+ }
443
458
  try {
444
459
  if (configuredTransport) {
445
460
  // The transport's name parameter is the taxonomy union, which an