@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.
- package/package.json +11 -11
- package/src/lib/app-utils/activity-labels.d.ts +117 -0
- package/src/lib/app-utils/activity-labels.js +343 -0
- package/src/lib/app-utils/activity-labels.js.map +1 -0
- package/src/lib/app-utils/admin-audit-index.d.ts +11 -0
- package/src/lib/app-utils/admin-audit-index.js +12 -1
- package/src/lib/app-utils/admin-audit-index.js.map +1 -1
- package/src/lib/app-utils/advertising-consent.d.ts +111 -0
- package/src/lib/app-utils/advertising-consent.js +216 -0
- package/src/lib/app-utils/advertising-consent.js.map +1 -0
- package/src/lib/app-utils/advertising-events.d.ts +98 -0
- package/src/lib/app-utils/advertising-events.js +340 -0
- package/src/lib/app-utils/advertising-events.js.map +1 -0
- package/src/lib/app-utils/advertising-tag-mounts.js +15 -8
- package/src/lib/app-utils/advertising-tag-mounts.js.map +1 -1
- package/src/lib/app-utils/advertising-tags.d.ts +54 -8
- package/src/lib/app-utils/advertising-tags.js +126 -28
- package/src/lib/app-utils/advertising-tags.js.map +1 -1
- package/src/lib/app-utils/analytics-events.d.ts +15 -86
- package/src/lib/app-utils/analytics-events.js +21 -6
- package/src/lib/app-utils/analytics-events.js.map +1 -1
- package/src/lib/app-utils/consent-banner-ui.d.ts +14 -0
- package/src/lib/app-utils/consent-banner-ui.js +23 -2
- package/src/lib/app-utils/consent-banner-ui.js.map +1 -1
- package/src/lib/app-utils/docs-help-section-excerpt-text.d.ts +14 -0
- package/src/lib/app-utils/docs-help-section-excerpt-text.js +31 -0
- package/src/lib/app-utils/docs-help-section-excerpt-text.js.map +1 -0
- package/src/lib/app-utils/docs-help-section-excerpt.d.ts +13 -0
- package/src/lib/app-utils/docs-help-section-excerpt.js +37 -0
- package/src/lib/app-utils/docs-help-section-excerpt.js.map +1 -0
- package/src/lib/app-utils/docs-help-sections.generated.d.ts +31 -0
- package/src/lib/app-utils/docs-help-sections.generated.js +660 -0
- package/src/lib/app-utils/docs-help-sections.generated.js.map +1 -0
- package/src/lib/app-utils/docs-help.d.ts +16 -4
- package/src/lib/app-utils/docs-help.generated.d.ts +171 -34
- package/src/lib/app-utils/docs-help.generated.js +1104 -3
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-help.js +25 -6
- package/src/lib/app-utils/docs-help.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +1008 -91
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/health-report.js +12 -0
- package/src/lib/app-utils/health-report.js.map +1 -1
- package/src/lib/app-utils/plugin-release-flags.generated.d.ts +1 -1
- package/src/lib/app-utils/plugin-release-flags.generated.js +7 -1
- package/src/lib/app-utils/plugin-release-flags.generated.js.map +1 -1
- package/src/lib/app-utils/realm-host-surface.generated.js +1 -0
- package/src/lib/app-utils/realm-host-surface.generated.js.map +1 -1
- package/src/lib/app-utils/variables.d.ts +2 -0
- package/src/lib/app-utils/variables.js +3 -0
- package/src/lib/app-utils/variables.js.map +1 -1
- package/src/lib/app-utils/visitor-consent.d.ts +34 -0
- package/src/lib/app-utils/visitor-consent.js +49 -2
- package/src/lib/app-utils/visitor-consent.js.map +1 -1
- package/src/lib/app-utils/where-used-summary.d.ts +41 -0
- package/src/lib/app-utils/where-used-summary.js +33 -0
- package/src/lib/app-utils/where-used-summary.js.map +1 -0
- package/src/lib/app-utils/where-used.d.ts +2 -20
- package/src/lib/app-utils/where-used.js +13 -12
- package/src/lib/app-utils/where-used.js.map +1 -1
- package/src/lib/foundation/definitions/platform.types.d.ts +33 -0
- package/src/lib/foundation/definitions/platform.types.js.map +1 -1
- package/src/lib/plugin-manager/first-party-plugins.generated.d.ts +7 -0
- package/src/lib/plugin-manager/first-party-plugins.generated.js +166 -2
- package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
- package/src/lib/plugin-manager/plugin-activity-actions.d.ts +51 -0
- package/src/lib/plugin-manager/plugin-activity-actions.js +43 -0
- package/src/lib/plugin-manager/plugin-activity-actions.js.map +1 -1
- package/src/lib/plugin-manager/plugin-advertising-conversions.d.ts +84 -0
- package/src/lib/plugin-manager/plugin-advertising-conversions.js +86 -0
- package/src/lib/plugin-manager/plugin-advertising-conversions.js.map +1 -0
- package/src/lib/plugin-manager/plugin-config.d.ts +23 -0
- package/src/lib/plugin-manager/plugin-config.js.map +1 -1
- package/src/lib/plugin-manager/plugin-local-deliveries.d.ts +161 -0
- package/src/lib/plugin-manager/plugin-local-deliveries.js +46 -0
- package/src/lib/plugin-manager/plugin-local-deliveries.js.map +1 -0
- package/src/lib/plugin-manager/plugin-media-ingest.d.ts +87 -0
- package/src/lib/plugin-manager/plugin-media-ingest.js +34 -0
- package/src/lib/plugin-manager/plugin-media-ingest.js.map +1 -0
- package/src/lib/plugin-manager/plugin-order-email-copies.d.ts +93 -0
- package/src/lib/plugin-manager/plugin-order-email-copies.js +104 -0
- package/src/lib/plugin-manager/plugin-order-email-copies.js.map +1 -0
- package/src/lib/plugin-manager/plugin-shipment-records.d.ts +7 -0
- package/src/lib/plugin-manager/plugin-shipment-records.js.map +1 -1
- package/src/lib/plugin-manager/plugin-site-csp.d.ts +58 -0
- package/src/lib/plugin-manager/plugin-site-csp.js +103 -0
- package/src/lib/plugin-manager/plugin-site-csp.js.map +1 -0
- package/src/lib/plugin-manager/realm-host-aglyn.generated.js +1 -0
- package/src/lib/plugin-manager/realm-host-aglyn.generated.js.map +1 -1
- package/src/lib/plugin-manager/site-page-hooks.d.ts +13 -0
- package/src/lib/plugin-manager/site-page-hooks.js +31 -0
- package/src/lib/plugin-manager/site-page-hooks.js.map +1 -1
- package/src/lib/plugin-manager/stock-photo-provider.d.ts +141 -0
- package/src/lib/plugin-manager/stock-photo-provider.js +59 -0
- package/src/lib/plugin-manager/stock-photo-provider.js.map +1 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/visitor-consent.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\nimport { ANALYTICS_PROVIDERS_DECLARED } from '../plugin-manager/first-party-plugins.generated'\nimport {\n applyAnalyticsConsent,\n type AnalyticsProviderDeclaration,\n} from './analytics-provider'\n\n/**\n * Visitor tracking consent (AGL-1498): the single answer to \"may this\n * tracking feature run for this visitor?\", shared by every surface that has\n * to agree about it — the tenant's GA injection, the consent banner, the\n * marketing runtime's visitor id, the console card that configures it, and\n * the console preview's region simulator.\n *\n * The model, and the honesty bar it was built against:\n *\n * - **Strictly necessary — no consent asked, none required.** The cart cookie\n * (`aglyn_cart_{hostId}`, commerce checkout), the member session\n * (`aglyn_member_{hostId}`, memberships sign-in), the `aglyn-tenant-host`\n * preview-host override, popup/announcement dismissal stamps (local-only,\n * never transmitted, and blocking them would show MORE popups), the\n * first-party pageview beacon (cookieless, stores no visitor identifier —\n * AGL-82), and the stored consent choice itself.\n * - **Consent-gated: `advertising`** (AGL-1649) — GA4's `ad_storage`,\n * `ad_user_data` and `ad_personalization`. OFF for every site unless the\n * host turns the question on, and granted only by an explicit visitor yes\n * to that specific category: never by the implied default, never by a\n * record written before the category existed, never by any refusal status.\n * A host who needs Ads storage previously had one workaround — switch the\n * whole tool off and run their own CMP — which is why this exists.\n * - **Consent-gated: `analytics`.** Today that is the customer-configured\n * Google Analytics tag — a third-party identifier-setting script — plus\n * the CROSS-VISIT persistence of the `aglyn:visitor` experiment id (the id\n * never leaves the browser, but an indefinite-lifetime identifier is\n * disproportionate without a basis; without one it degrades to\n * sessionStorage, so variants stay stable within the visit).\n *\n * **Two postures, resolved per visitor** (the Squarespace shape, host-chosen):\n *\n * - **`opt-in`** — prior consent: nothing gated loads until an explicit\n * accept. Applied in regions whose law requires prior consent (EU/EEA/UK),\n * whenever the visitor's region is UNKNOWN, and everywhere when the host\n * picks `strict` mode. Unknown-geo falls to opt-in deliberately: the\n * asymmetry is a few lost analytics events on rare headerless visits\n * versus pre-consent tracking of an EU visitor.\n * - **`opt-out`** — implied consent: gated features are live from first\n * paint, no banner and no notice; the visitor's recorded state is\n * `implied`, and the always-present \"Your Privacy Choices\" control is the\n * opt-out surface. Applied outside prior-consent regions when the host\n * mode is `geo`.\n *\n * **GPC (Global Privacy Control)** is honored as an automatic opt-out in\n * both postures: a visitor whose browser sends it is recorded `gpc-opt-out`\n * and never tracked, unless they later explicitly accept (the signal is a\n * default; a specific, informed choice overrides it).\n *\n * Enforcement is AT THE SOURCE and MODE-INDEPENDENT: whenever the verdict is\n * \"no\", the GA script never loads — not load-then-suppress — and the gate\n * does not care WHY consent is absent. Because tenant pages are ISR-cached,\n * both the posture and the verdict are evaluated client-side only — the\n * cached HTML never varies by region or consent state.\n */\n\n/** The host fields this module reads; keeps callers free of the full doc. */\nexport interface VisitorConsentHost {\n analytics?: {\n gaMeasurementId?: string\n /** GTM container (AGL-2486) — gated exactly as the GA id is. */\n gtmContainerId?: string\n } | null\n consent?: {\n /**\n * Host opt-out of the whole tool (AGL-1498). PERSISTED NAME. `true`\n * means the host runs their own consent solution (or accepts the\n * exposure): no banner, no privacy-choices control, and gated features\n * load for every visitor. Absent means the tool is active.\n */\n disabled?: boolean\n /**\n * The host's consent posture (AGL-1498). PERSISTED NAME.\n * - `'geo'` — geo-conditional: implied consent (tracking on, opt-out\n * offered) outside prior-consent regions; a prior-consent banner in\n * EU/EEA/UK and wherever the region is unknown.\n * - `'strict'` — opt-in everywhere: every visitor gets the banner.\n * Absent behaves as `'geo'` — the field practice (Squarespace) and the\n * posture the console preselects; recorded here so nobody discovers it\n * by surprise.\n */\n mode?: 'geo' | 'strict'\n /**\n * Host opt-in to ASKING about advertising storage (AGL-1649). PERSISTED\n * NAME. Absent or `false` — the default for every site that exists — means\n * the visitor is asked about analytics only and the three advertising\n * signals stay denied, which is where AGL-1622 put them.\n *\n * `true` adds a SECOND question to the banner and the privacy-choices\n * panel. It does not grant anything by itself: it creates the opportunity\n * for a visitor to say yes, which is the thing a host previously could not\n * obtain without switching the whole tool off and running their own CMP.\n *\n * Strictly `=== true`, like {@link isConsentToolDisabled}: a truthy string\n * out of a hand-edited host document must not turn a consent category on.\n */\n advertising?: boolean\n } | null\n}\n\n/** The host's configured posture; absent means `'geo'`. */\nexport type HostConsentMode = 'geo' | 'strict'\n\n/** The per-visitor resolved posture. */\nexport type VisitorConsentPosture = 'opt-in' | 'opt-out'\n\n/**\n * Consent-gated feature categories.\n *\n * `advertising` (AGL-1649) is gated ONLY where the host opted in — see\n * {@link hostAsksAboutAdvertising}. The stored shape is keyed by category, so\n * this extended rather than migrated, and a record written before it existed\n * reads as \"never asked\", never as a grant.\n */\nexport type VisitorConsentCategory = 'analytics' | 'advertising'\n\n/**\n * A visitor's recorded consent state. The five are DISTINCT on purpose:\n * \"how many visitors are tracked\" (and any future regulator question) needs\n * to distinguish a visitor who clicked Accept from one who was defaulted in.\n *\n * - `implied` — opt-out posture, no action taken: tracking is on.\n * - `accepted` — explicit yes (either posture): tracking is on.\n * - `declined` — explicit no to a prior-consent banner: tracking is off.\n * - `opted-out` — explicit opt-out from the implied default: tracking off.\n * - `gpc-opt-out` — the browser's GPC signal, honored: tracking off.\n */\nexport type VisitorConsentStatus =\n | 'implied'\n | 'accepted'\n | 'declined'\n | 'opted-out'\n | 'gpc-opt-out'\n\nconst CONSENT_STATUSES: ReadonlySet<string> = new Set([\n 'implied',\n 'accepted',\n 'declined',\n 'opted-out',\n 'gpc-opt-out',\n])\n\n/** The statuses that are a visitor's own click, not a default or a signal. */\nexport function isExplicitConsentStatus(\n status: VisitorConsentStatus | null | undefined,\n): boolean {\n return (\n status === 'accepted' || status === 'declined' || status === 'opted-out'\n )\n}\n\n/**\n * Which refusal a \"no\" is, for a visitor in this state.\n *\n * `opted-out` when they were defaulted in, `declined` when they were asked\n * first. The GATE does not care — both refuse — but the RECORD does: \"how many\n * visitors were tracked before they said no\" is a question only these two\n * answers separate, and it is a question a regulator can ask.\n *\n * Lives here rather than in either surface that needs it. Both the consent\n * overlay and the console's own module were deriving it, and a rule with two\n * derivations is a rule that eventually means two different things.\n */\nexport function refusalStatusFor(\n stored: { status?: VisitorConsentStatus } | null | undefined,\n posture: VisitorConsentPosture | null | undefined,\n): VisitorConsentStatus {\n return stored?.status === 'implied' || posture === 'opt-out'\n ? 'opted-out'\n : 'declined'\n}\n\n/** Whether a status grants the analytics category. */\nexport function analyticsGrantedByStatus(\n status: VisitorConsentStatus,\n): boolean {\n return status === 'accepted' || status === 'implied'\n}\n\n/**\n * Whether a status can carry an advertising grant at all (AGL-1649; widened by\n * AGL-2402, narrowed back on 2026-08-24, widened again on 2026-08-25).\n *\n * The SAME status set as {@link analyticsGrantedByStatus}, and the agreement is\n * deliberate: `implied` grants both. Being defaulted in where the law permits a\n * default is one posture, applied to both categories, and the visitor's opt-out\n * surface is the same one.\n *\n * ## Why it moved three times, because the next person will move it again\n *\n * AGL-2402 (`a410d8785`) first made `implied` qualify, on the reasoning that\n * the rule is safe by GEOGRAPHY: an `implied` record can only ever be written\n * in the OPT-OUT posture, because {@link resolveConsentPosture} puts the EU 27,\n * the EEA/EFTA three, the UK, Gibraltar, the EU outermost regions AND any\n * visitor whose region cannot be determined into opt-in, where\n * `decideVisitorConsent` records no default at all. That half was always sound\n * and is still asserted in `visitor-consent-advertising.spec.ts`.\n *\n * The half that failed was DISCLOSURE. That commit stated the published Cookie\n * Policy had been updated first; read against the live page on 2026-08-24 it\n * said two different things — its \"Marketing / advertising\" paragraph described\n * the opt-out posture, while the per-cookie table said `_gac`, `_gcl_au`,\n * `_fbp` and `_fbc` were \"set only where you have allowed advertising cookies\"\n * and \"Your choices\" repeated it. A policy that says both cannot authorise the\n * wider behaviour, so the code was narrowed to the strictest thing it said.\n *\n * On 2026-08-25 the master moved for real, in the right order: those five\n * opt-in-only sentences were rewritten in the Cookie Policy gdoc, and only then\n * did this function follow. The Privacy Policy needed nothing — it already said\n * *\"With your consent, we do 'share' … for cross-context behavioral\n * advertising\"*, named Google and Meta, and described the EU/UK-ask-first vs.\n * elsewhere-from-first-visit split. The flat \"we do not sell or share\" denial\n * that the narrowing reasoned from was a paraphrase in `docs/ANALYTICS.md`, not\n * a sentence in the document.\n *\n * ⛔ Re-narrowing is a POLICY act, not a code change. Move the published\n * masters first and let this follow — never the other way round. Nine surfaces\n * describe this rule to a human and `consent-advertising-copy-drift.spec.ts`\n * fails on all of them the moment the two disagree.\n *\n * A `true` here is necessary, never sufficient: the host must ALSO have opted\n * into asking (see {@link hostAsksAboutAdvertising}), and a refusal — declined,\n * opted out, or a GPC signal — outranks the default everywhere.\n */\nexport function advertisingGrantedByStatus(\n status: VisitorConsentStatus,\n): boolean {\n // Fails CLOSED: an exact match against the granting statuses, so any\n // unknown, absent or future status answers `false`. `strictNullChecks` is\n // off repo-wide, which means `status` can be `null`/`undefined` at runtime\n // despite the type — an equality test denies those, an exclusion list\n // (`status !== 'declined' && …`) would have granted them.\n //\n // `implied` GRANTS from 2026-08-25, and the policy went first (the call,\n // executed in that order). `decideVisitorConsent` only ever writes `implied`\n // in the OPT-OUT posture — `resolveConsentPosture` keeps the EU 27, the\n // EEA/EFTA three, the UK, Gibraltar, the EU outermost regions AND any visitor\n // whose region cannot be determined in opt-in, where no default is recorded at\n // all — so this widens advertising to the rest of the world only, and the\n // geographic guarantee `visitor-consent-advertising.spec.ts` asserts is\n // untouched. `gpc-opt-out` and `declined` still answer `false`.\n //\n // The disclosure this rests on is now consistent in BOTH masters, which is\n // what the 2026-08-24 narrowing was waiting for: the Privacy Policy already\n // said *\"With your consent, we do 'share' … Where the law requires us to ask\n // first — the EU, the UK, and anywhere we cannot determine your region — it\n // does not load until you accept. Elsewhere it runs from your first visit\"*,\n // and the Cookie Policy's two remaining opt-in-only sentences (the ×4\n // per-cookie note and the \"Your choices\" bullet) were rewritten to match it\n // the same day. Re-narrow this ONLY by moving the policies back first.\n return status === 'accepted' || status === 'implied'\n}\n\n/**\n * Strict GA measurement-id check (AGL-138) — the id lands inside an inline\n * script, so the format gate is load-bearing, not cosmetic.\n */\nexport const GA_MEASUREMENT_ID_PATTERN = /^G-[A-Z0-9]{4,16}$/\n\n/**\n * The shape a Meta pixel id has, shared by the LOADER and the console field.\n *\n * Declared HERE and not in `advertising-tags.ts`, which the console may not\n * import: `advertising-tag-gate.spec` case (f) fails any console file that\n * reaches for that module, because it is what mounts a vendor's script. A\n * format string is not a vendor, so it lives with the other id patterns and\n * the loader imports it from here.\n *\n * One pattern, because two would be a form that accepts an id the loader then\n * refuses — the tag never mounts and the operator is left looking at a saved\n * field and no pixel.\n */\nexport const META_PIXEL_ID_PATTERN = /^[0-9]{8,20}$/\n\n/**\n * A Google Ads conversion id — `AW-` and digits.\n *\n * Distinct from `GA_MEASUREMENT_ID_PATTERN` (`G-…`) on purpose: they are\n * different products with different ids, and a form that accepted either in\n * either field would load an analytics id into an ads tag and report nothing.\n */\nexport const GOOGLE_ADS_ID_PATTERN = /^AW-[0-9]{6,16}$/\n\n/**\n * A LinkedIn Insight partner id — numeric, and short.\n *\n * Bounded well under the Meta band rather than sharing it: a partner id is six\n * or seven digits today, and a pattern loose enough to accept a Meta pixel id\n * would let a mis-paste into the wrong field validate and then load a tag that\n * matches nothing.\n */\nexport const LINKEDIN_PARTNER_ID_PATTERN = /^[0-9]{4,10}$/\n\n/** The configured GA id when it is well-formed, else null. */\nexport function resolveGaMeasurementId(\n host: VisitorConsentHost | null | undefined,\n): string | null {\n const candidate = String(host?.analytics?.gaMeasurementId ?? '')\n return GA_MEASUREMENT_ID_PATTERN.test(candidate) ? candidate : null\n}\n\n/**\n * A Google Tag Manager container id — `GTM-` and 5–10 upper-case\n * alphanumerics.\n *\n * Format-checked for the same reason the GA id is: it lands inside an INLINE\n * SCRIPT on a published page, so anything that is not this shape must never\n * reach it (AGL-138).\n */\nexport const GTM_CONTAINER_ID_PATTERN = /^GTM-[A-Z0-9]{5,10}$/\n\n/** The configured GTM container when it is well-formed, else null. */\nexport function resolveGtmContainerId(\n host: VisitorConsentHost | null | undefined,\n): string | null {\n const candidate = String(host?.analytics?.gtmContainerId ?? '')\n return GTM_CONTAINER_ID_PATTERN.test(candidate) ? candidate : null\n}\n\n/**\n * The site's analytics settings that can configure a tag, and the shape each\n * must have to reach a page — the same patterns the console's Tracking tab\n * validates with. A provider declares which of these it mounts a tag for\n * (`analyticsProvider.settings` in `plugins.config.json`); a declared setting\n * with no shape here configures nothing.\n */\nexport const ANALYTICS_SETTING_PATTERNS: Readonly<Record<string, RegExp>> =\n Object.freeze({\n gaMeasurementId: GA_MEASUREMENT_ID_PATTERN,\n gtmContainerId: GTM_CONTAINER_ID_PATTERN,\n })\n\n/**\n * Whether the site runs ANY analytics tag a provider mounts — a measurement\n * id, a tag container, or both (AGL-2486).\n *\n * The one predicate both consent questions are asked through, because the\n * alternative is two places that each remember only one of the settings. A\n * site with a container and no measurement id is a site that tracks: it was\n * the shape that would have had `consentGatedCategories` return `[]`, no\n * banner rendered, and the container loading ungated — which for a container\n * is worse than for a single tag, since a container is exactly the thing that\n * can carry advertising tags.\n *\n * Read from the COMPILED provider declarations, never from which adapters\n * have loaded: this is asked during render on the published page and in the\n * console, before any adapter could have arrived, and \"no tag\" would mean no\n * banner in front of one. With no provider declared, nothing can mount, and\n * the answer is `false`.\n */\nexport function hostConfiguresAnalyticsTag(\n host: VisitorConsentHost | null | undefined,\n declared: readonly AnalyticsProviderDeclaration[] = ANALYTICS_PROVIDERS_DECLARED,\n): boolean {\n const settings = (host?.analytics ?? null) as Record<string, unknown> | null\n if (!settings) return false\n for (const provider of declared) {\n for (const field of provider.settings) {\n const pattern = ANALYTICS_SETTING_PATTERNS[field]\n if (pattern && pattern.test(String(settings[field] ?? ''))) return true\n }\n }\n return false\n}\n\n/** Host opt-out of the tool. Absent means ACTIVE — the safe default. */\nexport function isConsentToolDisabled(\n host: VisitorConsentHost | null | undefined,\n): boolean {\n return host?.consent?.disabled === true\n}\n\n/** The host's posture choice; anything but `'strict'` reads as `'geo'`. */\nexport function resolveHostConsentMode(\n host: VisitorConsentHost | null | undefined,\n): HostConsentMode {\n return host?.consent?.mode === 'strict' ? 'strict' : 'geo'\n}\n\n/**\n * The gated categories this site actually uses. Empty means there is\n * nothing to consent to and no consent UI renders — a banner with no\n * question behind it is decoration that trains visitors to click banners.\n */\nexport function consentGatedCategories(\n host: VisitorConsentHost | null | undefined,\n): VisitorConsentCategory[] {\n // A CONTAINER counts (AGL-2486). Read as the measurement id alone, a site\n // running only a container had no gated category, so no banner rendered\n // and the container loaded ungated — see `hostConfiguresAnalyticsTag`.\n if (!hostConfiguresAnalyticsTag(host)) return []\n return hostAsksAboutAdvertising(host)\n ? ['analytics', 'advertising']\n : ['analytics']\n}\n\n/**\n * Whether this site asks its visitors about advertising storage (AGL-1649).\n *\n * Two conditions, both required. The host has to have opted in — the flag is\n * off for every site that exists and stays off unless someone turns it on —\n * and the site has to actually have an analytics tag, because advertising\n * storage here is GA4's `ad_storage` and there is nothing to ask about\n * without one. A banner with no question behind it is decoration (AGL-1498).\n */\nexport function hostAsksAboutAdvertising(\n host: VisitorConsentHost | null | undefined,\n): boolean {\n // Either tag qualifies (AGL-2486): advertising storage is what a\n // measurement tag reads and what a container's advertising tags read, and\n // a container is the likelier of the two to carry them.\n return host?.consent?.advertising === true && hostConfiguresAnalyticsTag(host)\n}\n\n/**\n * Whether the published site runs the consent machinery at all: the tool is\n * active AND the site uses at least one gated feature.\n */\nexport function hostConsentRequired(\n host: VisitorConsentHost | null | undefined,\n): boolean {\n return !isConsentToolDisabled(host) && consentGatedCategories(host).length > 0\n}\n\n/**\n * Regions whose law requires PRIOR consent for non-essential\n * cookies/identifiers (ePrivacy and its UK retention): the EU 27, the three\n * EEA/EFTA states, the UK, Gibraltar (UK GDPR extends there), and the EU\n * outermost regions that carry their own ISO codes. ISO 3166-1 alpha-2,\n * matching `x-vercel-ip-country`.\n *\n * Deliberately NOT a \"privacy-law countries\" list: plenty of laws (CCPA,\n * LGPD, PIPEDA) regulate tracking on an opt-out basis, which is exactly what\n * the opt-out posture plus GPC provides. This set is only the prior-consent\n * boundary.\n */\nexport const PRIOR_CONSENT_COUNTRY_CODES: ReadonlySet<string> = new Set([\n // EU 27\n 'AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE', 'FI', 'FR', 'DE', 'GR',\n 'HU', 'IE', 'IT', 'LV', 'LT', 'LU', 'MT', 'NL', 'PL', 'PT', 'RO', 'SK',\n 'SI', 'ES', 'SE',\n // EEA / EFTA\n 'IS', 'LI', 'NO',\n // UK and Gibraltar\n 'GB', 'GI',\n // EU outermost regions with their own ISO codes (legally EU territory)\n 'AX', 'GF', 'GP', 'MQ', 'RE', 'YT',\n])\n\n/**\n * The per-visitor posture. Client-evaluated (ISR forbids varying the HTML),\n * from the country the region endpoint reports.\n *\n * Unknown region (`null`) falls to `opt-in` — the ONE place this feature\n * does not maximize tracking, because the asymmetry is lopsided: a few lost\n * analytics events on rare headerless visits, versus pre-consent tracking\n * of an EU visitor, which \"track as soon as we legally can\" excludes by its\n * own terms.\n */\nexport function resolveConsentPosture(\n host: VisitorConsentHost | null | undefined,\n country: string | null | undefined,\n): VisitorConsentPosture {\n if (resolveHostConsentMode(host) === 'strict') return 'opt-in'\n if (!country) return 'opt-in'\n return PRIOR_CONSENT_COUNTRY_CODES.has(country.toUpperCase())\n ? 'opt-in'\n : 'opt-out'\n}\n\n/**\n * Whether this browser sends Global Privacy Control. Read live at decision\n * time — the signal is the user agent's, not ours to cache.\n */\nexport function hasGlobalPrivacyControl(): boolean {\n return (\n typeof navigator !== 'undefined' &&\n (navigator as { globalPrivacyControl?: unknown }).globalPrivacyControl ===\n true\n )\n}\n\n/**\n * A visitor's stored consent record. Versioned so a future category\n * addition can distinguish \"declined analytics\" from \"was never asked about\n * marketing\".\n */\nexport interface StoredVisitorConsent {\n v: 1\n /** Epoch ms of the decision (or of the implied default's recording). */\n at: number\n status: VisitorConsentStatus\n /** The category grant, derived from `status` at store time. */\n analytics: boolean\n /**\n * The advertising grant (AGL-1649). ABSENT on every record written before\n * the category existed, and absent reads as `false` — \"was never asked\"\n * is not \"said yes\". Reading it the other way would hand Google a basis\n * for every visitor who ever clicked Allow on an analytics-only banner.\n *\n * Carried by an explicit accept, and — since 2026-08-25 — by the opt-out\n * posture's implied default, on a site whose host turned the question on.\n * Never by a refusal: declined, opted out and GPC all deny it everywhere.\n * See {@link advertisingGrantedByStatus} for the order the policy and the\n * code move in.\n */\n advertising?: boolean\n /** ISO country at decision time, when known — the `implied,us` shape. */\n country?: string | null\n}\n\n/**\n * Keyed by hostId, not a bare name: preview deployments and localhost serve\n * many hosts from one origin, and one site's \"yes\" must never leak to\n * another's.\n */\nexport const VISITOR_CONSENT_STORAGE_PREFIX = 'aglyn:consent:'\n\n/** The marketing runtime's cross-visit experiment id (`site-runtime.tsx`). */\nexport const VISITOR_ID_STORAGE_KEY = 'aglyn:visitor'\n\n/** Fired on `window` whenever {@link storeVisitorConsent} records a state. */\nexport const VISITOR_CONSENT_CHANGED_EVENT = 'aglyn:consent:changed'\n\n/**\n * Fired on `window` to open the privacy-choices panel — the change-your-mind\n * path, in BOTH directions. Sites can also link any element to\n * `#aglyn-consent`; the banner listens for those clicks.\n */\nexport const VISITOR_CONSENT_OPEN_EVENT = 'aglyn:consent:open'\n\nexport function visitorConsentStorageKey(hostId: string): string {\n return `${VISITOR_CONSENT_STORAGE_PREFIX}${hostId}`\n}\n\n/**\n * Cookie-name PREFIXES the analytics grant is answerable for (AGL-1606).\n *\n * A prefix sweep rather than a name list, because the names are derived and\n * configuration-dependent: `_ga` is joined by `_ga_<measurement id without the\n * `G-`>` per property, by `_gac_<id>` when Google Ads linking is on, and by\n * `_gid` where the legacy daily id is still issued. Hardcoding the two names a\n * given site happens to carry today is precisely how a withdrawal leaves an\n * identifier behind on the next site. `_ga` subsumes `_ga`, `_ga_*` and\n * `_gac_*`; `_gid` is the only Google analytics cookie that does not start\n * with it.\n */\nexport const ANALYTICS_COOKIE_PREFIXES: readonly string[] = ['_ga', '_gid']\n\n/**\n * Cookie-name PREFIXES the ADVERTISING grant is answerable for (AGL-2486).\n *\n * These are Google's, and they are separate from the analytics list above for\n * a reason that is not tidiness: an advertising-only withdrawal must sweep\n * them while analytics keeps running. Folding them into\n * {@link ANALYTICS_COOKIE_PREFIXES} would have swept them on ANALYTICS\n * withdrawal and still not on the case that motivates this — leaving the same\n * hole, narrower — besides filing a conversion-linker cookie under a category\n * it does not belong to.\n *\n * `_gcl_*` is the conversion linker, written by gtag once `ad_storage` is\n * granted, and it is the one that started this: it does not begin with `_ga`,\n * so nothing swept it, on any path.\n *\n * ONE PREFIX, deliberately. `_gac_*` (Google Ads linking) was in this list\n * briefly and is not, because it is written under the ANALYTICS grant and is\n * already swept by the `_ga` prefix. Listing it here made every\n * analytics-only GRANT delete it — on a site that never asks the advertising\n * question `stored.advertising` is always false, so the sweep ran on the way\n * IN — which broke \"cleanup is the withdrawal path only\" (AGL-1606). Sweeping\n * wider is not free when a category nobody was asked about owns the trigger.\n *\n * PRECISION MATTERS, because {@link clearCookiesWithPrefixes} is `startsWith`.\n * `_gcl` cannot reach `_ga`, `_ga_<id>`, `_gac_<id>` or `_gid` — those are\n * analytics and MUST survive an advertising-only withdrawal.\n */\nexport const ADVERTISING_COOKIE_PREFIXES: readonly string[] = ['_gcl']\n\n/**\n * Expire the advertising identifiers, on the same domain ladder as the\n * analytics sweep (AGL-2486).\n *\n * Deleting a cookie that was never set is a no-op; failing to delete one that\n * was is the defect — so this runs on every non-granting store rather than\n * only on a transition observed to be a withdrawal, which the record cannot\n * reliably tell us anyway.\n */\nexport function clearAdvertisingCookies(hostname?: string | null): string[] {\n return clearCookiesWithPrefixes(ADVERTISING_COOKIE_PREFIXES, hostname)\n}\n\n/**\n * Multi-label public suffixes: names under which registrations happen, so the\n * label to their left is the registrable one. A short list rather than the\n * Public Suffix List — the same trade `console-domains.ts` makes, and the\n * failure mode is mild in both directions: an unlisted suffix costs one\n * over-broad `domain=` write that the browser silently refuses (cookies cannot\n * be set on a public suffix), never a deletion on someone else's domain.\n */\nconst MULTI_LABEL_PUBLIC_SUFFIXES: ReadonlySet<string> = new Set([\n 'co.uk', 'org.uk', 'me.uk', 'ac.uk', 'gov.uk',\n 'com.au', 'net.au', 'org.au',\n 'co.nz', 'co.za',\n 'co.jp', 'ne.jp', 'or.jp',\n 'com.br', 'com.mx', 'com.cn', 'com.tr',\n 'co.in', 'co.kr', 'com.sg', 'com.hk',\n])\n\nfunction normalizeCookieHostname(hostname: string | null | undefined): string {\n return String(hostname ?? '')\n .trim()\n .toLowerCase()\n .replace(/\\.+$/, '')\n}\n\n/**\n * The registrable domain of a hostname — the highest name a cookie may be\n * written at — or `null` when there is none.\n *\n * This is the fiddly half of the cleanup. GA's `cookie_domain: 'auto'` walks\n * UP from the hostname and writes at the highest domain the browser accepts,\n * which is the registrable domain with a leading dot: a visit to\n * `www.shop.acme.co.uk` leaves `_ga` on `.acme.co.uk`, not on the exact host.\n * A deletion aimed at the exact hostname therefore silently no-ops — it writes\n * a second, host-scoped, already-expired cookie and leaves the real one alone.\n *\n * `null` for a single-label host (`localhost`) and for IP literals: those take\n * host-only cookies, which the undecorated deletion already covers.\n */\nexport function registrableCookieDomain(\n hostname: string | null | undefined,\n): string | null {\n // Named `name`, not `host`: `host.foo` inside app-utils is what the\n // AGL-1361 write-deny sweep reads as a host-document field, and a local\n // string's `.split`/`.includes` would land in it as phantom fields.\n const name = normalizeCookieHostname(hostname)\n if (!name) return null\n // IPv6 literal (bracketed or bare) and IPv4 literal: host-only cookies.\n if (name.startsWith('[') || name.includes(':')) return null\n if (/^\\d+(\\.\\d+){3}$/.test(name)) return null\n const labels = name.split('.')\n if (labels.length < 2 || labels.some((label) => !label)) return null\n const lastTwo = labels.slice(-2).join('.')\n if (MULTI_LABEL_PUBLIC_SUFFIXES.has(lastTwo)) {\n // `acme.co.uk` is registrable; `co.uk` on its own is not a domain at all.\n return labels.length > 2 ? labels.slice(-3).join('.') : null\n }\n return lastTwo\n}\n\n/**\n * Every `domain=` value a cookie deletion has to try, most specific first and\n * stopping at the registrable domain.\n *\n * The whole ladder rather than the registrable domain alone because\n * `cookie_domain` is configurable: a host that pins it to `shop.acme.com`\n * leaves the cookie one rung below where `auto` would. Walking the rungs costs\n * a handful of no-op writes and removes the guess.\n *\n * Empty when there is no registrable domain — the host-only deletion that\n * {@link clearAnalyticsCookies} always performs is the complete answer there.\n */\nexport function analyticsCookieDomains(\n hostname: string | null | undefined,\n): string[] {\n const name = normalizeCookieHostname(hostname)\n const registrable = registrableCookieDomain(name)\n if (!registrable) return []\n const domains: string[] = []\n let current = name\n for (;;) {\n domains.push(`.${current}`)\n if (current === registrable) break\n const cut = current.indexOf('.')\n if (cut < 0) break\n current = current.slice(cut + 1)\n }\n return domains\n}\n\n/**\n * Expire every analytics cookie this browser is carrying, at the host itself\n * and at every domain up to the registrable one. Returns the names it acted\n * on, which is what makes the cleanup assertable.\n *\n * Best-effort by construction: a cookie written at a path other than `/` is\n * not reachable this way, and `document.cookie` cannot report which domain a\n * cookie came from — hence the ladder. Neither limit applies to the cookies GA\n * actually writes (path `/`, registrable domain).\n */\nexport function clearAnalyticsCookies(\n hostname?: string | null,\n): string[] {\n return clearCookiesWithPrefixes(ANALYTICS_COOKIE_PREFIXES, hostname)\n}\n\n/**\n * The same sweep for an ARBITRARY prefix set — the generalisation\n * {@link clearAnalyticsCookies} now delegates to.\n *\n * Extracted rather than copied because the hard part is not the prefix match,\n * it is {@link analyticsCookieDomains}: the ladder from the exact hostname up\n * to the registrable domain, which exists because a tag written with an `auto`\n * cookie domain leaves its identifier one or more rungs ABOVE the host. A\n * second vendor's sweep re-derived by hand would get that wrong the first time\n * and pass its own test, because a deletion aimed at the wrong domain silently\n * no-ops — it writes an already-expired host-scoped cookie and reports success.\n * One implementation, one ladder, one place to fix.\n *\n * Meta's `_fbp`/`_fbc` are written exactly like `_ga`: path `/`, registrable\n * domain, first-party. Nothing here is Google-specific and nothing here is\n * Meta-specific.\n */\nexport function clearCookiesWithPrefixes(\n prefixes: readonly string[],\n hostname?: string | null,\n): string[] {\n if (typeof document === 'undefined') return []\n // An empty prefix set would match EVERY cookie via `''.startsWith`, which\n // would delete the cart, the member session and the consent record itself.\n // A caller with nothing to sweep gets nothing swept.\n if (!prefixes || prefixes.length === 0) return []\n try {\n const names = new Set<string>()\n for (const pair of String(document.cookie ?? '').split(';')) {\n const name = pair.split('=')[0].trim()\n if (name && prefixes.some((p) => !!p && name.startsWith(p))) {\n names.add(name)\n }\n }\n if (names.size === 0) return []\n const domains = analyticsCookieDomains(\n hostname ??\n (typeof window !== 'undefined' ? window.location?.hostname : ''),\n )\n for (const name of names) {\n const expired = `${name}=; Max-Age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`\n document.cookie = expired\n for (const domain of domains) {\n document.cookie = `${expired}; domain=${domain}`\n }\n }\n return [...names]\n } catch {\n // Cookies disabled or a hostile `document.cookie`: the gate still keeps\n // the script out, which is the load-bearing half.\n return []\n }\n}\n\n/** A single consent-mode signal value. */\nexport type AnalyticsConsentSignal = 'granted' | 'denied'\n\n/**\n * The consent-mode signal set this tool is answerable for.\n *\n * `ad_storage` and friends are typed as the literal `'denied'`, not as\n * {@link AnalyticsConsentSignal}: the tool asks a visitor about analytics and\n * nothing else, so there is no advertising basis on file to grant, in either\n * direction. Widening them is a change the type has to be edited to allow.\n */\nexport interface AnalyticsConsentSignals {\n analytics_storage: AnalyticsConsentSignal\n ad_storage: 'denied'\n ad_user_data: 'denied'\n ad_personalization: 'denied'\n}\n\n/**\n * The consent-mode payload for a given analytics grant — ONE source, so the\n * `default` declared before the tag loads (AGL-1622) and the `update` sent on a\n * mid-pageview withdrawal (AGL-1608) can never drift apart. Two hand-written\n * literals that disagree is how a tag ends up believing a state the visitor\n * never chose.\n */\nexport function analyticsConsentSignals(\n granted: boolean,\n): AnalyticsConsentSignals {\n return {\n analytics_storage: granted ? 'granted' : 'denied',\n ad_storage: 'denied',\n ad_user_data: 'denied',\n ad_personalization: 'denied',\n }\n}\n\n/** The four signals when advertising is in play (AGL-1649). */\nexport interface VisitorConsentSignals {\n analytics_storage: AnalyticsConsentSignal\n ad_storage: AnalyticsConsentSignal\n ad_user_data: AnalyticsConsentSignal\n ad_personalization: AnalyticsConsentSignal\n}\n\n/**\n * The consent-mode payload for a pair of category grants (AGL-1649).\n *\n * A SEPARATE function from {@link analyticsConsentSignals} rather than a\n * widening of it, deliberately. AGL-1649 says not to widen those three\n * literals, and the reason survives this change: a caller that only knows\n * about analytics should be unable to express a grant it has no answer for.\n * Reaching an advertising grant now requires calling something else and\n * passing it, which is a thing a reviewer can see in a diff.\n *\n * Advertising is clamped to analytics. `ad_storage: 'granted'` with\n * `analytics_storage: 'denied'` is not a state this tool can reach honestly —\n * every refusal path (declined, opted out, GPC) withdraws both together — so\n * the clamp makes the impossible state unrepresentable rather than merely\n * unreached.\n */\nexport function consentModeSignals(grants: {\n analytics: boolean\n advertising: boolean\n}): VisitorConsentSignals {\n const analytics = grants.analytics === true\n const advertising = analytics && grants.advertising === true\n const ads: AnalyticsConsentSignal = advertising ? 'granted' : 'denied'\n return {\n analytics_storage: analytics ? 'granted' : 'denied',\n ad_storage: ads,\n ad_user_data: ads,\n ad_personalization: ads,\n }\n}\n\n/**\n * Whether advertising storage may run for this visitor on this site.\n *\n * THREE independent conditions, and all of them are load-bearing:\n *\n * 1. The host asks about advertising at all. A record that says yes is\n * ignored on a site that never turned the category on — which covers a\n * host switching it back off, and covers a stale record generally.\n * 2. The record's status can carry a grant — an explicit accept, and nothing\n * else (see {@link advertisingGrantedByStatus}). An `implied` default\n * cannot, and every refusal status refuses.\n * 3. The record actually says yes to this category.\n *\n * Absent, unreadable or legacy records all answer `false`, which is the same\n * answer they gave before the category existed.\n */\nexport function advertisingGrantedByRecord(\n host: VisitorConsentHost | null | undefined,\n stored: StoredVisitorConsent | null | undefined,\n): boolean {\n if (!hostAsksAboutAdvertising(host)) return false\n if (!stored) return false\n if (!advertisingGrantedByStatus(stored.status)) return false\n return stored.advertising === true\n}\n\n/**\n * Make every resident analytics tag agree with the consent state, and return\n * the ids it acted on (AGL-1608).\n *\n * The AGL-1498 gate stops a tag from LOADING, which is the right enforcement\n * for a fresh pageview and the wrong tool for a visitor who withdraws\n * mid-pageview: the vendor's library has already executed, and React\n * unmounting its `<script>` element does not unload it. So the tag that is\n * already there is told, through the adapter of whichever vendor it is\n * (`analytics-provider.ts`). A tag only exists after a grant, so this never\n * touches the gate.\n *\n * Symmetric on purpose: a visitor who opts out and changes their mind in the\n * same pageview would otherwise stay silently unmeasured until they\n * navigated. The re-grant restores whatever the CALLER passes — analytics\n * alone on a site that never asks the advertising question (`advertising`\n * defaults to false), and both categories where the visitor's record grants\n * both (AGL-1649). Advertising is clamped to analytics, so a withdrawal can\n * never restore advertising alone.\n */\nexport function setResidentAnalyticsTags(\n granted: boolean,\n advertising = false,\n): string[] {\n if (typeof window === 'undefined') return []\n return applyAnalyticsConsent({ analytics: granted, advertising })\n}\n\n/**\n * The stored record, or null when there is none (never resolved, storage\n * unavailable, or an unrecognized shape — treated alike: not yet decided).\n */\nexport function readStoredVisitorConsent(\n hostId: string | null | undefined,\n): StoredVisitorConsent | null {\n if (!hostId || typeof window === 'undefined') return null\n try {\n return parseStoredVisitorConsent(\n window.localStorage.getItem(visitorConsentStorageKey(hostId)),\n )\n } catch {\n // No storage (privacy mode) — the visitor is simply undecided, which is\n // the safe reading.\n return null\n }\n}\n\n/**\n * A stored record's text as the record it may claim to be, or null.\n *\n * The one validation every copy of a record passes through — the per-origin\n * store above, and the `aglyn_consent` mirror a server reads off a request\n * (AGL-3289) — so a copy read anywhere grants exactly what its status\n * supports and nothing a hand edit added.\n */\nexport function parseStoredVisitorConsent(\n raw: string | null | undefined,\n): StoredVisitorConsent | null {\n if (!raw) return null\n try {\n const parsed = JSON.parse(raw)\n if (\n parsed?.v !== 1 ||\n typeof parsed.status !== 'string' ||\n !CONSENT_STATUSES.has(parsed.status)\n ) {\n return null\n }\n const status = parsed.status as VisitorConsentStatus\n return {\n v: 1,\n at: typeof parsed.at === 'number' ? parsed.at : 0,\n status,\n // Derived, never trusted from storage: a hand-edited record must not\n // grant what its status does not.\n analytics: analyticsGrantedByStatus(status),\n // Same discipline for advertising (AGL-1649), plus one more clause:\n // the key has to be PRESENT and true. Absent is a record written\n // before the category existed — never asked, which is not a yes.\n advertising:\n advertisingGrantedByStatus(status) && parsed.advertising === true,\n country: typeof parsed.country === 'string' ? parsed.country : null,\n }\n } catch {\n // Corrupt JSON — the visitor is simply undecided, which is the safe\n // reading.\n return null\n }\n}\n\n/**\n * Record a consent state and make the page agree with it immediately.\n *\n * Storing the record is itself strictly-necessary storage (it is how a \"no\"\n * is remembered), so it is exempt from the consent it records. Any\n * non-granting state also REMOVES the identifiers the grant allowed —\n * the persistent `aglyn:visitor` id AND the GA cookies\n * ({@link clearAnalyticsCookies}): opting out cleans up, it does not merely\n * stop adding (AGL-1606) — and SILENCES the tag that is already running\n * ({@link setResidentAnalyticsTags}), without which the cleanup un-does itself\n * within the same pageview (AGL-1608). Listeners hear about it via\n * {@link VISITOR_CONSENT_CHANGED_EVENT} so the GA gate and the runtime react\n * without a reload.\n *\n * The two cleanups are deliberately in SEPARATE try blocks: private mode\n * throws on `localStorage` while cookies still work, and a withdrawal must\n * not lose the cookie sweep to that.\n */\nexport function storeVisitorConsent(\n hostId: string,\n state: {\n status: VisitorConsentStatus\n country?: string | null\n /**\n * The visitor's answer to the advertising question (AGL-1649). Omitted\n * by every caller that does not ask one, and omitted means NO.\n */\n advertising?: boolean\n },\n): StoredVisitorConsent {\n const stored: StoredVisitorConsent = {\n v: 1,\n at: Date.now(),\n status: state.status,\n analytics: analyticsGrantedByStatus(state.status),\n // Derived at store time exactly as `analytics` is, so a caller cannot\n // persist a grant the status does not support — a refusal always writes\n // `false` here even if the panel's checkbox was left ticked.\n advertising:\n advertisingGrantedByStatus(state.status) && state.advertising === true,\n country: state.country ?? null,\n }\n if (typeof window !== 'undefined') {\n try {\n window.localStorage.setItem(\n visitorConsentStorageKey(hostId),\n JSON.stringify(stored),\n )\n if (!stored.analytics) {\n window.localStorage.removeItem(VISITOR_ID_STORAGE_KEY)\n }\n } catch {\n // Private mode: the state holds for this page via the event below,\n // and is re-derived next visit — the safe failure.\n }\n // Silence the RESIDENT tag before sweeping, never after (AGL-1608): the\n // gate only keeps the script from LOADING, so a mid-pageview withdrawal\n // leaves gtag.js live and it re-writes `_ga_<id>` on its next enhanced\n // measurement event. Sweeping first would delete three cookies and\n // immediately get one back.\n setResidentAnalyticsTags(stored.analytics, stored.advertising === true)\n if (!stored.analytics) {\n clearAnalyticsCookies()\n }\n // The advertising half, and it belongs HERE rather than only in\n // `revokeAdvertisingTags` (AGL-2486). That function sweeps a vendor whose\n // marked script elements are in the document, and it is installed only on\n // our own marketing host — so on a CUSTOMER site, where the advertising\n // question is equally available, withdrawing advertising ran no sweep at\n // all. `_gcl_au` is written by the GA tag `site-analytics.tsx` mounts, not\n // by a script this module marked, so no element-scoped teardown was ever\n // going to reach it on any surface.\n //\n // This path is the one every site shares: it is where a decision becomes a\n // record. Unconditional on the grant being absent, for the reason in\n // `clearAdvertisingCookies` — a delete of something never set costs\n // nothing, a missed delete is the defect.\n if (!stored.advertising) {\n clearAdvertisingCookies()\n }\n try {\n window.dispatchEvent(new CustomEvent(VISITOR_CONSENT_CHANGED_EVENT))\n } catch {\n // A dispatch failure only delays consumers until the next read.\n }\n }\n return stored\n}\n\n/**\n * The GA gate's whole verdict: may the analytics category run for this\n * visitor, on this site, right now?\n *\n * - Consent machinery not required (no gated features, or host opted out of\n * the tool) → yes.\n * - Required and the recorded state grants analytics (`accepted` or\n * `implied`) → yes.\n * - Required and anything else — declined, opted out, GPC, or NOT YET\n * RESOLVED → no. Undecided is not a maybe: the script stays out until a\n * state that grants it exists. The gate never asks WHY consent is absent,\n * which is what keeps it posture-independent.\n */\nexport function isAnalyticsAllowed(\n host: VisitorConsentHost | null | undefined,\n stored: StoredVisitorConsent | null,\n): boolean {\n if (!hostConsentRequired(host)) return true\n return stored?.analytics === true\n}\n\n/**\n * Where the marketing runtime may keep its experiment visitor id.\n *\n * `local` — the id persists across visits. `session` — the id lives in\n * sessionStorage: variants stay stable within the visit and the identifier\n * dies with the tab. The downgrade applies while the consent machinery is\n * active for this site and analytics is not granted; the id itself never\n * leaves the browser either way (the experiment beacons are aggregate — no\n * visitor id on the wire).\n */\nexport function resolveVisitorIdPersistence(\n host: VisitorConsentHost | null | undefined,\n stored: StoredVisitorConsent | null,\n): 'local' | 'session' {\n return isAnalyticsAllowed(host, stored) ? 'local' : 'session'\n}\n"],"names":["ANALYTICS_PROVIDERS_DECLARED","applyAnalyticsConsent","CONSENT_STATUSES","Set","isExplicitConsentStatus","status","refusalStatusFor","stored","posture","analyticsGrantedByStatus","advertisingGrantedByStatus","GA_MEASUREMENT_ID_PATTERN","META_PIXEL_ID_PATTERN","GOOGLE_ADS_ID_PATTERN","LINKEDIN_PARTNER_ID_PATTERN","resolveGaMeasurementId","host","candidate","String","analytics","gaMeasurementId","test","GTM_CONTAINER_ID_PATTERN","resolveGtmContainerId","gtmContainerId","ANALYTICS_SETTING_PATTERNS","Object","freeze","hostConfiguresAnalyticsTag","declared","settings","provider","field","pattern","isConsentToolDisabled","consent","disabled","resolveHostConsentMode","mode","consentGatedCategories","hostAsksAboutAdvertising","advertising","hostConsentRequired","length","PRIOR_CONSENT_COUNTRY_CODES","resolveConsentPosture","country","has","toUpperCase","hasGlobalPrivacyControl","navigator","globalPrivacyControl","VISITOR_CONSENT_STORAGE_PREFIX","VISITOR_ID_STORAGE_KEY","VISITOR_CONSENT_CHANGED_EVENT","VISITOR_CONSENT_OPEN_EVENT","visitorConsentStorageKey","hostId","ANALYTICS_COOKIE_PREFIXES","ADVERTISING_COOKIE_PREFIXES","clearAdvertisingCookies","hostname","clearCookiesWithPrefixes","MULTI_LABEL_PUBLIC_SUFFIXES","normalizeCookieHostname","trim","toLowerCase","replace","registrableCookieDomain","name","startsWith","includes","labels","split","some","label","lastTwo","slice","join","analyticsCookieDomains","registrable","domains","current","push","cut","indexOf","clearAnalyticsCookies","prefixes","document","window","names","pair","cookie","p","add","size","location","expired","domain","analyticsConsentSignals","granted","analytics_storage","ad_storage","ad_user_data","ad_personalization","consentModeSignals","grants","ads","advertisingGrantedByRecord","setResidentAnalyticsTags","readStoredVisitorConsent","parseStoredVisitorConsent","localStorage","getItem","raw","parsed","JSON","parse","v","at","storeVisitorConsent","state","Date","now","setItem","stringify","removeItem","dispatchEvent","CustomEvent","isAnalyticsAllowed","resolveVisitorIdPersistence"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/visitor-consent.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\nimport { ANALYTICS_PROVIDERS_DECLARED } from '../plugin-manager/first-party-plugins.generated'\nimport {\n applyAnalyticsConsent,\n type AnalyticsProviderDeclaration,\n} from './analytics-provider'\n\n/**\n * Visitor tracking consent (AGL-1498): the single answer to \"may this\n * tracking feature run for this visitor?\", shared by every surface that has\n * to agree about it — the tenant's GA injection, the consent banner, the\n * marketing runtime's visitor id, the console card that configures it, and\n * the console preview's region simulator.\n *\n * The model, and the honesty bar it was built against:\n *\n * - **Strictly necessary — no consent asked, none required.** The cart cookie\n * (`aglyn_cart_{hostId}`, commerce checkout), the member session\n * (`aglyn_member_{hostId}`, memberships sign-in), the `aglyn-tenant-host`\n * preview-host override, popup/announcement dismissal stamps (local-only,\n * never transmitted, and blocking them would show MORE popups), the\n * first-party pageview beacon (cookieless, stores no visitor identifier —\n * AGL-82), and the stored consent choice itself.\n * - **Consent-gated: `advertising`** (AGL-1649) — GA4's `ad_storage`,\n * `ad_user_data` and `ad_personalization`. OFF for every site unless the\n * host turns the question on, and granted only by an explicit visitor yes\n * to that specific category: never by the implied default, never by a\n * record written before the category existed, never by any refusal status.\n * A host who needs Ads storage previously had one workaround — switch the\n * whole tool off and run their own CMP — which is why this exists.\n * - **Consent-gated: `analytics`.** Today that is the customer-configured\n * Google Analytics tag — a third-party identifier-setting script — plus\n * the CROSS-VISIT persistence of the `aglyn:visitor` experiment id (the id\n * never leaves the browser, but an indefinite-lifetime identifier is\n * disproportionate without a basis; without one it degrades to\n * sessionStorage, so variants stay stable within the visit).\n *\n * **Two postures, resolved per visitor** (the Squarespace shape, host-chosen):\n *\n * - **`opt-in`** — prior consent: nothing gated loads until an explicit\n * accept. Applied in regions whose law requires prior consent (EU/EEA/UK),\n * whenever the visitor's region is UNKNOWN, and everywhere when the host\n * picks `strict` mode. Unknown-geo falls to opt-in deliberately: the\n * asymmetry is a few lost analytics events on rare headerless visits\n * versus pre-consent tracking of an EU visitor.\n * - **`opt-out`** — implied consent: gated features are live from first\n * paint, no banner and no notice; the visitor's recorded state is\n * `implied`, and the always-present \"Your Privacy Choices\" control is the\n * opt-out surface. Applied outside prior-consent regions when the host\n * mode is `geo`.\n *\n * **GPC (Global Privacy Control)** is honored as an automatic opt-out in\n * both postures: a visitor whose browser sends it is recorded `gpc-opt-out`\n * and never tracked, unless they later explicitly accept (the signal is a\n * default; a specific, informed choice overrides it).\n *\n * Enforcement is AT THE SOURCE and MODE-INDEPENDENT: whenever the verdict is\n * \"no\", the GA script never loads — not load-then-suppress — and the gate\n * does not care WHY consent is absent. Because tenant pages are ISR-cached,\n * both the posture and the verdict are evaluated client-side only — the\n * cached HTML never varies by region or consent state.\n */\n\n/** The host fields this module reads; keeps callers free of the full doc. */\nexport interface VisitorConsentHost {\n analytics?: {\n gaMeasurementId?: string\n /** GTM container (AGL-2486) — gated exactly as the GA id is. */\n gtmContainerId?: string\n /**\n * Advertising vendor id → the site's account id with that vendor (AGL-1152,\n * AGL-3694): the merchant's own Meta pixel, TikTok pixel, Pinterest tag,\n * Google Ads id or LinkedIn partner id, each shaped by\n * {@link ADVERTISING_TAG_ID_PATTERNS}.\n */\n adTags?: Record<string, string> | null\n } | null\n consent?: {\n /**\n * Host opt-out of the whole tool (AGL-1498). PERSISTED NAME. `true`\n * means the host runs their own consent solution (or accepts the\n * exposure): no banner, no privacy-choices control, and gated features\n * load for every visitor. Absent means the tool is active.\n */\n disabled?: boolean\n /**\n * The host's consent posture (AGL-1498). PERSISTED NAME.\n * - `'geo'` — geo-conditional: implied consent (tracking on, opt-out\n * offered) outside prior-consent regions; a prior-consent banner in\n * EU/EEA/UK and wherever the region is unknown.\n * - `'strict'` — opt-in everywhere: every visitor gets the banner.\n * Absent behaves as `'geo'` — the field practice (Squarespace) and the\n * posture the console preselects; recorded here so nobody discovers it\n * by surprise.\n */\n mode?: 'geo' | 'strict'\n /**\n * Host opt-in to ASKING about advertising storage (AGL-1649). PERSISTED\n * NAME. Absent or `false` — the default for every site that exists — means\n * the visitor is asked about analytics only and the three advertising\n * signals stay denied, which is where AGL-1622 put them.\n *\n * `true` adds a SECOND question to the banner and the privacy-choices\n * panel. It does not grant anything by itself: it creates the opportunity\n * for a visitor to say yes, which is the thing a host previously could not\n * obtain without switching the whole tool off and running their own CMP.\n *\n * Strictly `=== true`, like {@link isConsentToolDisabled}: a truthy string\n * out of a hand-edited host document must not turn a consent category on.\n */\n advertising?: boolean\n } | null\n}\n\n/** The host's configured posture; absent means `'geo'`. */\nexport type HostConsentMode = 'geo' | 'strict'\n\n/** The per-visitor resolved posture. */\nexport type VisitorConsentPosture = 'opt-in' | 'opt-out'\n\n/**\n * Consent-gated feature categories.\n *\n * `advertising` (AGL-1649) is gated ONLY where the host opted in — see\n * {@link hostAsksAboutAdvertising}. The stored shape is keyed by category, so\n * this extended rather than migrated, and a record written before it existed\n * reads as \"never asked\", never as a grant.\n */\nexport type VisitorConsentCategory = 'analytics' | 'advertising'\n\n/**\n * A visitor's recorded consent state. The five are DISTINCT on purpose:\n * \"how many visitors are tracked\" (and any future regulator question) needs\n * to distinguish a visitor who clicked Accept from one who was defaulted in.\n *\n * - `implied` — opt-out posture, no action taken: tracking is on.\n * - `accepted` — explicit yes (either posture): tracking is on.\n * - `declined` — explicit no to a prior-consent banner: tracking is off.\n * - `opted-out` — explicit opt-out from the implied default: tracking off.\n * - `gpc-opt-out` — the browser's GPC signal, honored: tracking off.\n */\nexport type VisitorConsentStatus =\n | 'implied'\n | 'accepted'\n | 'declined'\n | 'opted-out'\n | 'gpc-opt-out'\n\nconst CONSENT_STATUSES: ReadonlySet<string> = new Set([\n 'implied',\n 'accepted',\n 'declined',\n 'opted-out',\n 'gpc-opt-out',\n])\n\n/** The statuses that are a visitor's own click, not a default or a signal. */\nexport function isExplicitConsentStatus(\n status: VisitorConsentStatus | null | undefined,\n): boolean {\n return (\n status === 'accepted' || status === 'declined' || status === 'opted-out'\n )\n}\n\n/**\n * Which refusal a \"no\" is, for a visitor in this state.\n *\n * `opted-out` when they were defaulted in, `declined` when they were asked\n * first. The GATE does not care — both refuse — but the RECORD does: \"how many\n * visitors were tracked before they said no\" is a question only these two\n * answers separate, and it is a question a regulator can ask.\n *\n * Lives here rather than in either surface that needs it. Both the consent\n * overlay and the console's own module were deriving it, and a rule with two\n * derivations is a rule that eventually means two different things.\n */\nexport function refusalStatusFor(\n stored: { status?: VisitorConsentStatus } | null | undefined,\n posture: VisitorConsentPosture | null | undefined,\n): VisitorConsentStatus {\n return stored?.status === 'implied' || posture === 'opt-out'\n ? 'opted-out'\n : 'declined'\n}\n\n/** Whether a status grants the analytics category. */\nexport function analyticsGrantedByStatus(\n status: VisitorConsentStatus,\n): boolean {\n return status === 'accepted' || status === 'implied'\n}\n\n/**\n * Whether a status can carry an advertising grant at all (AGL-1649; widened by\n * AGL-2402, narrowed back on 2026-08-24, widened again on 2026-08-25).\n *\n * The SAME status set as {@link analyticsGrantedByStatus}, and the agreement is\n * deliberate: `implied` grants both. Being defaulted in where the law permits a\n * default is one posture, applied to both categories, and the visitor's opt-out\n * surface is the same one.\n *\n * ## Why it moved three times, because the next person will move it again\n *\n * AGL-2402 (`a410d8785`) first made `implied` qualify, on the reasoning that\n * the rule is safe by GEOGRAPHY: an `implied` record can only ever be written\n * in the OPT-OUT posture, because {@link resolveConsentPosture} puts the EU 27,\n * the EEA/EFTA three, the UK, Gibraltar, the EU outermost regions AND any\n * visitor whose region cannot be determined into opt-in, where\n * `decideVisitorConsent` records no default at all. That half was always sound\n * and is still asserted in `visitor-consent-advertising.spec.ts`.\n *\n * The half that failed was DISCLOSURE. That commit stated the published Cookie\n * Policy had been updated first; read against the live page on 2026-08-24 it\n * said two different things — its \"Marketing / advertising\" paragraph described\n * the opt-out posture, while the per-cookie table said `_gac`, `_gcl_au`,\n * `_fbp` and `_fbc` were \"set only where you have allowed advertising cookies\"\n * and \"Your choices\" repeated it. A policy that says both cannot authorise the\n * wider behaviour, so the code was narrowed to the strictest thing it said.\n *\n * On 2026-08-25 the master moved for real, in the right order: those five\n * opt-in-only sentences were rewritten in the Cookie Policy gdoc, and only then\n * did this function follow. The Privacy Policy needed nothing — it already said\n * *\"With your consent, we do 'share' … for cross-context behavioral\n * advertising\"*, named Google and Meta, and described the EU/UK-ask-first vs.\n * elsewhere-from-first-visit split. The flat \"we do not sell or share\" denial\n * that the narrowing reasoned from was a paraphrase in `docs/ANALYTICS.md`, not\n * a sentence in the document.\n *\n * ⛔ Re-narrowing is a POLICY act, not a code change. Move the published\n * masters first and let this follow — never the other way round. Nine surfaces\n * describe this rule to a human and `consent-advertising-copy-drift.spec.ts`\n * fails on all of them the moment the two disagree.\n *\n * A `true` here is necessary, never sufficient: the host must ALSO have opted\n * into asking (see {@link hostAsksAboutAdvertising}), and a refusal — declined,\n * opted out, or a GPC signal — outranks the default everywhere.\n */\nexport function advertisingGrantedByStatus(\n status: VisitorConsentStatus,\n): boolean {\n // Fails CLOSED: an exact match against the granting statuses, so any\n // unknown, absent or future status answers `false`. `strictNullChecks` is\n // off repo-wide, which means `status` can be `null`/`undefined` at runtime\n // despite the type — an equality test denies those, an exclusion list\n // (`status !== 'declined' && …`) would have granted them.\n //\n // `implied` GRANTS from 2026-08-25, and the policy went first (the call,\n // executed in that order). `decideVisitorConsent` only ever writes `implied`\n // in the OPT-OUT posture — `resolveConsentPosture` keeps the EU 27, the\n // EEA/EFTA three, the UK, Gibraltar, the EU outermost regions AND any visitor\n // whose region cannot be determined in opt-in, where no default is recorded at\n // all — so this widens advertising to the rest of the world only, and the\n // geographic guarantee `visitor-consent-advertising.spec.ts` asserts is\n // untouched. `gpc-opt-out` and `declined` still answer `false`.\n //\n // The disclosure this rests on is now consistent in BOTH masters, which is\n // what the 2026-08-24 narrowing was waiting for: the Privacy Policy already\n // said *\"With your consent, we do 'share' … Where the law requires us to ask\n // first — the EU, the UK, and anywhere we cannot determine your region — it\n // does not load until you accept. Elsewhere it runs from your first visit\"*,\n // and the Cookie Policy's two remaining opt-in-only sentences (the ×4\n // per-cookie note and the \"Your choices\" bullet) were rewritten to match it\n // the same day. Re-narrow this ONLY by moving the policies back first.\n return status === 'accepted' || status === 'implied'\n}\n\n/**\n * Strict GA measurement-id check (AGL-138) — the id lands inside an inline\n * script, so the format gate is load-bearing, not cosmetic.\n */\nexport const GA_MEASUREMENT_ID_PATTERN = /^G-[A-Z0-9]{4,16}$/\n\n/**\n * The shape a Meta pixel id has, shared by the LOADER and the console field.\n *\n * Declared HERE and not in `advertising-tags.ts`, which the console may not\n * import: `advertising-tag-gate.spec` case (f) fails any console file that\n * reaches for that module, because it is what mounts a vendor's script. A\n * format string is not a vendor, so it lives with the other id patterns and\n * the loader imports it from here.\n *\n * One pattern, because two would be a form that accepts an id the loader then\n * refuses — the tag never mounts and the operator is left looking at a saved\n * field and no pixel.\n */\nexport const META_PIXEL_ID_PATTERN = /^[0-9]{8,20}$/\n\n/**\n * A Google Ads conversion id — `AW-` and digits.\n *\n * Distinct from `GA_MEASUREMENT_ID_PATTERN` (`G-…`) on purpose: they are\n * different products with different ids, and a form that accepted either in\n * either field would load an analytics id into an ads tag and report nothing.\n */\nexport const GOOGLE_ADS_ID_PATTERN = /^AW-[0-9]{6,16}$/\n\n/**\n * A LinkedIn Insight partner id — numeric, and short.\n *\n * Bounded well under the Meta band rather than sharing it: a partner id is six\n * or seven digits today, and a pattern loose enough to accept a Meta pixel id\n * would let a mis-paste into the wrong field validate and then load a tag that\n * matches nothing.\n */\nexport const LINKEDIN_PARTNER_ID_PATTERN = /^[0-9]{4,10}$/\n\n/**\n * A TikTok pixel code (AGL-3694) — upper-case letters and digits, 20 today.\n * The band is generous on both sides, like the Meta one; what it must refuse\n * is anything that could escape the inline boot it is written into.\n */\nexport const TIKTOK_PIXEL_ID_PATTERN = /^[A-Z0-9]{16,24}$/\n\n/**\n * A Pinterest tag id (AGL-3694) — numeric, 13 digits today. Its own field, so\n * the overlap with the Meta band never lets one vendor's id load the other.\n */\nexport const PINTEREST_TAG_ID_PATTERN = /^[0-9]{10,16}$/\n\n/**\n * Every advertising vendor a site can configure, by its `analytics.adTags`\n * key, and the shape its account id must have — the patterns the loader and\n * the console's Tracking tab share (AGL-3694). Declared here, not beside the\n * vendor descriptors, because the console may not import the module that\n * mounts them (`advertising-tag-gate.spec` case (f)).\n */\nexport const ADVERTISING_TAG_ID_PATTERNS: Readonly<Record<string, RegExp>> =\n Object.freeze({\n meta: META_PIXEL_ID_PATTERN,\n 'google-ads': GOOGLE_ADS_ID_PATTERN,\n linkedin: LINKEDIN_PARTNER_ID_PATTERN,\n tiktok: TIKTOK_PIXEL_ID_PATTERN,\n pinterest: PINTEREST_TAG_ID_PATTERN,\n })\n\n/** The site's well-formed account id with one advertising vendor, else null. */\nexport function resolveAdvertisingTagId(\n host: VisitorConsentHost | null | undefined,\n vendorId: string,\n): string | null {\n const pattern = ADVERTISING_TAG_ID_PATTERNS[vendorId]\n if (!pattern) return null\n const candidate = String(host?.analytics?.adTags?.[vendorId] ?? '')\n return pattern.test(candidate) ? candidate : null\n}\n\n/**\n * Whether the site configures ANY advertising tag with a well-formed id\n * (AGL-3694). A pixel is a tracker in its own right, so a site running one and\n * no analytics tag still needs the consent machinery in front of it.\n */\nexport function hostConfiguresAdvertisingTag(\n host: VisitorConsentHost | null | undefined,\n): boolean {\n return Object.keys(ADVERTISING_TAG_ID_PATTERNS).some(\n (vendorId) => resolveAdvertisingTagId(host, vendorId) !== null,\n )\n}\n\n/** The configured GA id when it is well-formed, else null. */\nexport function resolveGaMeasurementId(\n host: VisitorConsentHost | null | undefined,\n): string | null {\n const candidate = String(host?.analytics?.gaMeasurementId ?? '')\n return GA_MEASUREMENT_ID_PATTERN.test(candidate) ? candidate : null\n}\n\n/**\n * A Google Tag Manager container id — `GTM-` and 5–10 upper-case\n * alphanumerics.\n *\n * Format-checked for the same reason the GA id is: it lands inside an INLINE\n * SCRIPT on a published page, so anything that is not this shape must never\n * reach it (AGL-138).\n */\nexport const GTM_CONTAINER_ID_PATTERN = /^GTM-[A-Z0-9]{5,10}$/\n\n/** The configured GTM container when it is well-formed, else null. */\nexport function resolveGtmContainerId(\n host: VisitorConsentHost | null | undefined,\n): string | null {\n const candidate = String(host?.analytics?.gtmContainerId ?? '')\n return GTM_CONTAINER_ID_PATTERN.test(candidate) ? candidate : null\n}\n\n/**\n * The site's analytics settings that can configure a tag, and the shape each\n * must have to reach a page — the same patterns the console's Tracking tab\n * validates with. A provider declares which of these it mounts a tag for\n * (`analyticsProvider.settings` in `plugins.config.json`); a declared setting\n * with no shape here configures nothing.\n */\nexport const ANALYTICS_SETTING_PATTERNS: Readonly<Record<string, RegExp>> =\n Object.freeze({\n gaMeasurementId: GA_MEASUREMENT_ID_PATTERN,\n gtmContainerId: GTM_CONTAINER_ID_PATTERN,\n })\n\n/**\n * Whether the site runs ANY analytics tag a provider mounts — a measurement\n * id, a tag container, or both (AGL-2486).\n *\n * The one predicate both consent questions are asked through, because the\n * alternative is two places that each remember only one of the settings. A\n * site with a container and no measurement id is a site that tracks: it was\n * the shape that would have had `consentGatedCategories` return `[]`, no\n * banner rendered, and the container loading ungated — which for a container\n * is worse than for a single tag, since a container is exactly the thing that\n * can carry advertising tags.\n *\n * Read from the COMPILED provider declarations, never from which adapters\n * have loaded: this is asked during render on the published page and in the\n * console, before any adapter could have arrived, and \"no tag\" would mean no\n * banner in front of one. With no provider declared, nothing can mount, and\n * the answer is `false`.\n */\nexport function hostConfiguresAnalyticsTag(\n host: VisitorConsentHost | null | undefined,\n declared: readonly AnalyticsProviderDeclaration[] = ANALYTICS_PROVIDERS_DECLARED,\n): boolean {\n const settings = (host?.analytics ?? null) as Record<string, unknown> | null\n if (!settings) return false\n for (const provider of declared) {\n for (const field of provider.settings) {\n const pattern = ANALYTICS_SETTING_PATTERNS[field]\n if (pattern && pattern.test(String(settings[field] ?? ''))) return true\n }\n }\n return false\n}\n\n/** Host opt-out of the tool. Absent means ACTIVE — the safe default. */\nexport function isConsentToolDisabled(\n host: VisitorConsentHost | null | undefined,\n): boolean {\n return host?.consent?.disabled === true\n}\n\n/** The host's posture choice; anything but `'strict'` reads as `'geo'`. */\nexport function resolveHostConsentMode(\n host: VisitorConsentHost | null | undefined,\n): HostConsentMode {\n return host?.consent?.mode === 'strict' ? 'strict' : 'geo'\n}\n\n/**\n * The gated categories this site actually uses. Empty means there is\n * nothing to consent to and no consent UI renders — a banner with no\n * question behind it is decoration that trains visitors to click banners.\n */\nexport function consentGatedCategories(\n host: VisitorConsentHost | null | undefined,\n): VisitorConsentCategory[] {\n // A CONTAINER counts (AGL-2486). Read as the measurement id alone, a site\n // running only a container had no gated category, so no banner rendered\n // and the container loaded ungated — see `hostConfiguresAnalyticsTag`.\n // An advertising tag counts too (AGL-3694): a site running only a pixel\n // has a tracker to ask about, and with no category it would get no banner\n // and its pixel could never be granted.\n if (!hostConfiguresAnalyticsTag(host) && !hostConfiguresAdvertisingTag(host)) {\n return []\n }\n return hostAsksAboutAdvertising(host)\n ? ['analytics', 'advertising']\n : ['analytics']\n}\n\n/**\n * Whether this site asks its visitors about advertising storage (AGL-1649).\n *\n * Two conditions, both required. The host has to have opted in — the flag is\n * off for every site that exists and stays off unless someone turns it on —\n * and the site has to actually have an analytics tag, because advertising\n * storage here is GA4's `ad_storage` and there is nothing to ask about\n * without one. A banner with no question behind it is decoration (AGL-1498).\n */\nexport function hostAsksAboutAdvertising(\n host: VisitorConsentHost | null | undefined,\n): boolean {\n // Either tag qualifies (AGL-2486): advertising storage is what a\n // measurement tag reads and what a container's advertising tags read, and\n // a container is the likelier of the two to carry them.\n //\n // An advertising tag of the site's own qualifies as well (AGL-3694): it is\n // the most direct thing the question is about, and without this a site\n // running a pixel and no analytics id could never be asked, so its pixel\n // could never load.\n return (\n host?.consent?.advertising === true &&\n (hostConfiguresAnalyticsTag(host) || hostConfiguresAdvertisingTag(host))\n )\n}\n\n/**\n * Whether the published site runs the consent machinery at all: the tool is\n * active AND the site uses at least one gated feature.\n */\nexport function hostConsentRequired(\n host: VisitorConsentHost | null | undefined,\n): boolean {\n return !isConsentToolDisabled(host) && consentGatedCategories(host).length > 0\n}\n\n/**\n * Regions whose law requires PRIOR consent for non-essential\n * cookies/identifiers (ePrivacy and its UK retention): the EU 27, the three\n * EEA/EFTA states, the UK, Gibraltar (UK GDPR extends there), and the EU\n * outermost regions that carry their own ISO codes. ISO 3166-1 alpha-2,\n * matching `x-vercel-ip-country`.\n *\n * Deliberately NOT a \"privacy-law countries\" list: plenty of laws (CCPA,\n * LGPD, PIPEDA) regulate tracking on an opt-out basis, which is exactly what\n * the opt-out posture plus GPC provides. This set is only the prior-consent\n * boundary.\n */\nexport const PRIOR_CONSENT_COUNTRY_CODES: ReadonlySet<string> = new Set([\n // EU 27\n 'AT', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE', 'FI', 'FR', 'DE', 'GR',\n 'HU', 'IE', 'IT', 'LV', 'LT', 'LU', 'MT', 'NL', 'PL', 'PT', 'RO', 'SK',\n 'SI', 'ES', 'SE',\n // EEA / EFTA\n 'IS', 'LI', 'NO',\n // UK and Gibraltar\n 'GB', 'GI',\n // EU outermost regions with their own ISO codes (legally EU territory)\n 'AX', 'GF', 'GP', 'MQ', 'RE', 'YT',\n])\n\n/**\n * The per-visitor posture. Client-evaluated (ISR forbids varying the HTML),\n * from the country the region endpoint reports.\n *\n * Unknown region (`null`) falls to `opt-in` — the ONE place this feature\n * does not maximize tracking, because the asymmetry is lopsided: a few lost\n * analytics events on rare headerless visits, versus pre-consent tracking\n * of an EU visitor, which \"track as soon as we legally can\" excludes by its\n * own terms.\n */\nexport function resolveConsentPosture(\n host: VisitorConsentHost | null | undefined,\n country: string | null | undefined,\n): VisitorConsentPosture {\n if (resolveHostConsentMode(host) === 'strict') return 'opt-in'\n if (!country) return 'opt-in'\n return PRIOR_CONSENT_COUNTRY_CODES.has(country.toUpperCase())\n ? 'opt-in'\n : 'opt-out'\n}\n\n/**\n * Whether this browser sends Global Privacy Control. Read live at decision\n * time — the signal is the user agent's, not ours to cache.\n */\nexport function hasGlobalPrivacyControl(): boolean {\n return (\n typeof navigator !== 'undefined' &&\n (navigator as { globalPrivacyControl?: unknown }).globalPrivacyControl ===\n true\n )\n}\n\n/**\n * A visitor's stored consent record. Versioned so a future category\n * addition can distinguish \"declined analytics\" from \"was never asked about\n * marketing\".\n */\nexport interface StoredVisitorConsent {\n v: 1\n /** Epoch ms of the decision (or of the implied default's recording). */\n at: number\n status: VisitorConsentStatus\n /** The category grant, derived from `status` at store time. */\n analytics: boolean\n /**\n * The advertising grant (AGL-1649). ABSENT on every record written before\n * the category existed, and absent reads as `false` — \"was never asked\"\n * is not \"said yes\". Reading it the other way would hand Google a basis\n * for every visitor who ever clicked Allow on an analytics-only banner.\n *\n * Carried by an explicit accept, and — since 2026-08-25 — by the opt-out\n * posture's implied default, on a site whose host turned the question on.\n * Never by a refusal: declined, opted out and GPC all deny it everywhere.\n * See {@link advertisingGrantedByStatus} for the order the policy and the\n * code move in.\n */\n advertising?: boolean\n /** ISO country at decision time, when known — the `implied,us` shape. */\n country?: string | null\n}\n\n/**\n * Keyed by hostId, not a bare name: preview deployments and localhost serve\n * many hosts from one origin, and one site's \"yes\" must never leak to\n * another's.\n */\nexport const VISITOR_CONSENT_STORAGE_PREFIX = 'aglyn:consent:'\n\n/** The marketing runtime's cross-visit experiment id (`site-runtime.tsx`). */\nexport const VISITOR_ID_STORAGE_KEY = 'aglyn:visitor'\n\n/** Fired on `window` whenever {@link storeVisitorConsent} records a state. */\nexport const VISITOR_CONSENT_CHANGED_EVENT = 'aglyn:consent:changed'\n\n/**\n * Fired on `window` to open the privacy-choices panel — the change-your-mind\n * path, in BOTH directions. Sites can also link any element to\n * `#aglyn-consent`; the banner listens for those clicks.\n */\nexport const VISITOR_CONSENT_OPEN_EVENT = 'aglyn:consent:open'\n\nexport function visitorConsentStorageKey(hostId: string): string {\n return `${VISITOR_CONSENT_STORAGE_PREFIX}${hostId}`\n}\n\n/**\n * Cookie-name PREFIXES the analytics grant is answerable for (AGL-1606).\n *\n * A prefix sweep rather than a name list, because the names are derived and\n * configuration-dependent: `_ga` is joined by `_ga_<measurement id without the\n * `G-`>` per property, by `_gac_<id>` when Google Ads linking is on, and by\n * `_gid` where the legacy daily id is still issued. Hardcoding the two names a\n * given site happens to carry today is precisely how a withdrawal leaves an\n * identifier behind on the next site. `_ga` subsumes `_ga`, `_ga_*` and\n * `_gac_*`; `_gid` is the only Google analytics cookie that does not start\n * with it.\n */\nexport const ANALYTICS_COOKIE_PREFIXES: readonly string[] = ['_ga', '_gid']\n\n/**\n * Cookie-name PREFIXES the ADVERTISING grant is answerable for (AGL-2486).\n *\n * These are Google's, and they are separate from the analytics list above for\n * a reason that is not tidiness: an advertising-only withdrawal must sweep\n * them while analytics keeps running. Folding them into\n * {@link ANALYTICS_COOKIE_PREFIXES} would have swept them on ANALYTICS\n * withdrawal and still not on the case that motivates this — leaving the same\n * hole, narrower — besides filing a conversion-linker cookie under a category\n * it does not belong to.\n *\n * `_gcl_*` is the conversion linker, written by gtag once `ad_storage` is\n * granted, and it is the one that started this: it does not begin with `_ga`,\n * so nothing swept it, on any path.\n *\n * ONE PREFIX, deliberately. `_gac_*` (Google Ads linking) was in this list\n * briefly and is not, because it is written under the ANALYTICS grant and is\n * already swept by the `_ga` prefix. Listing it here made every\n * analytics-only GRANT delete it — on a site that never asks the advertising\n * question `stored.advertising` is always false, so the sweep ran on the way\n * IN — which broke \"cleanup is the withdrawal path only\" (AGL-1606). Sweeping\n * wider is not free when a category nobody was asked about owns the trigger.\n *\n * PRECISION MATTERS, because {@link clearCookiesWithPrefixes} is `startsWith`.\n * `_gcl` cannot reach `_ga`, `_ga_<id>`, `_gac_<id>` or `_gid` — those are\n * analytics and MUST survive an advertising-only withdrawal.\n */\nexport const ADVERTISING_COOKIE_PREFIXES: readonly string[] = ['_gcl']\n\n/**\n * Expire the advertising identifiers, on the same domain ladder as the\n * analytics sweep (AGL-2486).\n *\n * Deleting a cookie that was never set is a no-op; failing to delete one that\n * was is the defect — so this runs on every non-granting store rather than\n * only on a transition observed to be a withdrawal, which the record cannot\n * reliably tell us anyway.\n */\nexport function clearAdvertisingCookies(hostname?: string | null): string[] {\n return clearCookiesWithPrefixes(ADVERTISING_COOKIE_PREFIXES, hostname)\n}\n\n/**\n * Multi-label public suffixes: names under which registrations happen, so the\n * label to their left is the registrable one. A short list rather than the\n * Public Suffix List — the same trade `console-domains.ts` makes, and the\n * failure mode is mild in both directions: an unlisted suffix costs one\n * over-broad `domain=` write that the browser silently refuses (cookies cannot\n * be set on a public suffix), never a deletion on someone else's domain.\n */\nconst MULTI_LABEL_PUBLIC_SUFFIXES: ReadonlySet<string> = new Set([\n 'co.uk', 'org.uk', 'me.uk', 'ac.uk', 'gov.uk',\n 'com.au', 'net.au', 'org.au',\n 'co.nz', 'co.za',\n 'co.jp', 'ne.jp', 'or.jp',\n 'com.br', 'com.mx', 'com.cn', 'com.tr',\n 'co.in', 'co.kr', 'com.sg', 'com.hk',\n])\n\nfunction normalizeCookieHostname(hostname: string | null | undefined): string {\n return String(hostname ?? '')\n .trim()\n .toLowerCase()\n .replace(/\\.+$/, '')\n}\n\n/**\n * The registrable domain of a hostname — the highest name a cookie may be\n * written at — or `null` when there is none.\n *\n * This is the fiddly half of the cleanup. GA's `cookie_domain: 'auto'` walks\n * UP from the hostname and writes at the highest domain the browser accepts,\n * which is the registrable domain with a leading dot: a visit to\n * `www.shop.acme.co.uk` leaves `_ga` on `.acme.co.uk`, not on the exact host.\n * A deletion aimed at the exact hostname therefore silently no-ops — it writes\n * a second, host-scoped, already-expired cookie and leaves the real one alone.\n *\n * `null` for a single-label host (`localhost`) and for IP literals: those take\n * host-only cookies, which the undecorated deletion already covers.\n */\nexport function registrableCookieDomain(\n hostname: string | null | undefined,\n): string | null {\n // Named `name`, not `host`: `host.foo` inside app-utils is what the\n // AGL-1361 write-deny sweep reads as a host-document field, and a local\n // string's `.split`/`.includes` would land in it as phantom fields.\n const name = normalizeCookieHostname(hostname)\n if (!name) return null\n // IPv6 literal (bracketed or bare) and IPv4 literal: host-only cookies.\n if (name.startsWith('[') || name.includes(':')) return null\n if (/^\\d+(\\.\\d+){3}$/.test(name)) return null\n const labels = name.split('.')\n if (labels.length < 2 || labels.some((label) => !label)) return null\n const lastTwo = labels.slice(-2).join('.')\n if (MULTI_LABEL_PUBLIC_SUFFIXES.has(lastTwo)) {\n // `acme.co.uk` is registrable; `co.uk` on its own is not a domain at all.\n return labels.length > 2 ? labels.slice(-3).join('.') : null\n }\n return lastTwo\n}\n\n/**\n * Every `domain=` value a cookie deletion has to try, most specific first and\n * stopping at the registrable domain.\n *\n * The whole ladder rather than the registrable domain alone because\n * `cookie_domain` is configurable: a host that pins it to `shop.acme.com`\n * leaves the cookie one rung below where `auto` would. Walking the rungs costs\n * a handful of no-op writes and removes the guess.\n *\n * Empty when there is no registrable domain — the host-only deletion that\n * {@link clearAnalyticsCookies} always performs is the complete answer there.\n */\nexport function analyticsCookieDomains(\n hostname: string | null | undefined,\n): string[] {\n const name = normalizeCookieHostname(hostname)\n const registrable = registrableCookieDomain(name)\n if (!registrable) return []\n const domains: string[] = []\n let current = name\n for (;;) {\n domains.push(`.${current}`)\n if (current === registrable) break\n const cut = current.indexOf('.')\n if (cut < 0) break\n current = current.slice(cut + 1)\n }\n return domains\n}\n\n/**\n * Expire every analytics cookie this browser is carrying, at the host itself\n * and at every domain up to the registrable one. Returns the names it acted\n * on, which is what makes the cleanup assertable.\n *\n * Best-effort by construction: a cookie written at a path other than `/` is\n * not reachable this way, and `document.cookie` cannot report which domain a\n * cookie came from — hence the ladder. Neither limit applies to the cookies GA\n * actually writes (path `/`, registrable domain).\n */\nexport function clearAnalyticsCookies(\n hostname?: string | null,\n): string[] {\n return clearCookiesWithPrefixes(ANALYTICS_COOKIE_PREFIXES, hostname)\n}\n\n/**\n * The same sweep for an ARBITRARY prefix set — the generalisation\n * {@link clearAnalyticsCookies} now delegates to.\n *\n * Extracted rather than copied because the hard part is not the prefix match,\n * it is {@link analyticsCookieDomains}: the ladder from the exact hostname up\n * to the registrable domain, which exists because a tag written with an `auto`\n * cookie domain leaves its identifier one or more rungs ABOVE the host. A\n * second vendor's sweep re-derived by hand would get that wrong the first time\n * and pass its own test, because a deletion aimed at the wrong domain silently\n * no-ops — it writes an already-expired host-scoped cookie and reports success.\n * One implementation, one ladder, one place to fix.\n *\n * Meta's `_fbp`/`_fbc` are written exactly like `_ga`: path `/`, registrable\n * domain, first-party. Nothing here is Google-specific and nothing here is\n * Meta-specific.\n */\nexport function clearCookiesWithPrefixes(\n prefixes: readonly string[],\n hostname?: string | null,\n): string[] {\n if (typeof document === 'undefined') return []\n // An empty prefix set would match EVERY cookie via `''.startsWith`, which\n // would delete the cart, the member session and the consent record itself.\n // A caller with nothing to sweep gets nothing swept.\n if (!prefixes || prefixes.length === 0) return []\n try {\n const names = new Set<string>()\n for (const pair of String(document.cookie ?? '').split(';')) {\n const name = pair.split('=')[0].trim()\n if (name && prefixes.some((p) => !!p && name.startsWith(p))) {\n names.add(name)\n }\n }\n if (names.size === 0) return []\n const domains = analyticsCookieDomains(\n hostname ??\n (typeof window !== 'undefined' ? window.location?.hostname : ''),\n )\n for (const name of names) {\n const expired = `${name}=; Max-Age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`\n document.cookie = expired\n for (const domain of domains) {\n document.cookie = `${expired}; domain=${domain}`\n }\n }\n return [...names]\n } catch {\n // Cookies disabled or a hostile `document.cookie`: the gate still keeps\n // the script out, which is the load-bearing half.\n return []\n }\n}\n\n/** A single consent-mode signal value. */\nexport type AnalyticsConsentSignal = 'granted' | 'denied'\n\n/**\n * The consent-mode signal set this tool is answerable for.\n *\n * `ad_storage` and friends are typed as the literal `'denied'`, not as\n * {@link AnalyticsConsentSignal}: the tool asks a visitor about analytics and\n * nothing else, so there is no advertising basis on file to grant, in either\n * direction. Widening them is a change the type has to be edited to allow.\n */\nexport interface AnalyticsConsentSignals {\n analytics_storage: AnalyticsConsentSignal\n ad_storage: 'denied'\n ad_user_data: 'denied'\n ad_personalization: 'denied'\n}\n\n/**\n * The consent-mode payload for a given analytics grant — ONE source, so the\n * `default` declared before the tag loads (AGL-1622) and the `update` sent on a\n * mid-pageview withdrawal (AGL-1608) can never drift apart. Two hand-written\n * literals that disagree is how a tag ends up believing a state the visitor\n * never chose.\n */\nexport function analyticsConsentSignals(\n granted: boolean,\n): AnalyticsConsentSignals {\n return {\n analytics_storage: granted ? 'granted' : 'denied',\n ad_storage: 'denied',\n ad_user_data: 'denied',\n ad_personalization: 'denied',\n }\n}\n\n/** The four signals when advertising is in play (AGL-1649). */\nexport interface VisitorConsentSignals {\n analytics_storage: AnalyticsConsentSignal\n ad_storage: AnalyticsConsentSignal\n ad_user_data: AnalyticsConsentSignal\n ad_personalization: AnalyticsConsentSignal\n}\n\n/**\n * The consent-mode payload for a pair of category grants (AGL-1649).\n *\n * A SEPARATE function from {@link analyticsConsentSignals} rather than a\n * widening of it, deliberately. AGL-1649 says not to widen those three\n * literals, and the reason survives this change: a caller that only knows\n * about analytics should be unable to express a grant it has no answer for.\n * Reaching an advertising grant now requires calling something else and\n * passing it, which is a thing a reviewer can see in a diff.\n *\n * Advertising is clamped to analytics. `ad_storage: 'granted'` with\n * `analytics_storage: 'denied'` is not a state this tool can reach honestly —\n * every refusal path (declined, opted out, GPC) withdraws both together — so\n * the clamp makes the impossible state unrepresentable rather than merely\n * unreached.\n */\nexport function consentModeSignals(grants: {\n analytics: boolean\n advertising: boolean\n}): VisitorConsentSignals {\n const analytics = grants.analytics === true\n const advertising = analytics && grants.advertising === true\n const ads: AnalyticsConsentSignal = advertising ? 'granted' : 'denied'\n return {\n analytics_storage: analytics ? 'granted' : 'denied',\n ad_storage: ads,\n ad_user_data: ads,\n ad_personalization: ads,\n }\n}\n\n/**\n * Whether advertising storage may run for this visitor on this site.\n *\n * THREE independent conditions, and all of them are load-bearing:\n *\n * 1. The host asks about advertising at all. A record that says yes is\n * ignored on a site that never turned the category on — which covers a\n * host switching it back off, and covers a stale record generally.\n * 2. The record's status can carry a grant — an explicit accept, and nothing\n * else (see {@link advertisingGrantedByStatus}). An `implied` default\n * cannot, and every refusal status refuses.\n * 3. The record actually says yes to this category.\n *\n * Absent, unreadable or legacy records all answer `false`, which is the same\n * answer they gave before the category existed.\n */\nexport function advertisingGrantedByRecord(\n host: VisitorConsentHost | null | undefined,\n stored: StoredVisitorConsent | null | undefined,\n): boolean {\n if (!hostAsksAboutAdvertising(host)) return false\n if (!stored) return false\n if (!advertisingGrantedByStatus(stored.status)) return false\n return stored.advertising === true\n}\n\n/**\n * Make every resident analytics tag agree with the consent state, and return\n * the ids it acted on (AGL-1608).\n *\n * The AGL-1498 gate stops a tag from LOADING, which is the right enforcement\n * for a fresh pageview and the wrong tool for a visitor who withdraws\n * mid-pageview: the vendor's library has already executed, and React\n * unmounting its `<script>` element does not unload it. So the tag that is\n * already there is told, through the adapter of whichever vendor it is\n * (`analytics-provider.ts`). A tag only exists after a grant, so this never\n * touches the gate.\n *\n * Symmetric on purpose: a visitor who opts out and changes their mind in the\n * same pageview would otherwise stay silently unmeasured until they\n * navigated. The re-grant restores whatever the CALLER passes — analytics\n * alone on a site that never asks the advertising question (`advertising`\n * defaults to false), and both categories where the visitor's record grants\n * both (AGL-1649). Advertising is clamped to analytics, so a withdrawal can\n * never restore advertising alone.\n */\nexport function setResidentAnalyticsTags(\n granted: boolean,\n advertising = false,\n): string[] {\n if (typeof window === 'undefined') return []\n return applyAnalyticsConsent({ analytics: granted, advertising })\n}\n\n/**\n * The stored record, or null when there is none (never resolved, storage\n * unavailable, or an unrecognized shape — treated alike: not yet decided).\n */\nexport function readStoredVisitorConsent(\n hostId: string | null | undefined,\n): StoredVisitorConsent | null {\n if (!hostId || typeof window === 'undefined') return null\n try {\n return parseStoredVisitorConsent(\n window.localStorage.getItem(visitorConsentStorageKey(hostId)),\n )\n } catch {\n // No storage (privacy mode) — the visitor is simply undecided, which is\n // the safe reading.\n return null\n }\n}\n\n/**\n * A stored record's text as the record it may claim to be, or null.\n *\n * The one validation every copy of a record passes through — the per-origin\n * store above, and the `aglyn_consent` mirror a server reads off a request\n * (AGL-3289) — so a copy read anywhere grants exactly what its status\n * supports and nothing a hand edit added.\n */\nexport function parseStoredVisitorConsent(\n raw: string | null | undefined,\n): StoredVisitorConsent | null {\n if (!raw) return null\n try {\n const parsed = JSON.parse(raw)\n if (\n parsed?.v !== 1 ||\n typeof parsed.status !== 'string' ||\n !CONSENT_STATUSES.has(parsed.status)\n ) {\n return null\n }\n const status = parsed.status as VisitorConsentStatus\n return {\n v: 1,\n at: typeof parsed.at === 'number' ? parsed.at : 0,\n status,\n // Derived, never trusted from storage: a hand-edited record must not\n // grant what its status does not.\n analytics: analyticsGrantedByStatus(status),\n // Same discipline for advertising (AGL-1649), plus one more clause:\n // the key has to be PRESENT and true. Absent is a record written\n // before the category existed — never asked, which is not a yes.\n advertising:\n advertisingGrantedByStatus(status) && parsed.advertising === true,\n country: typeof parsed.country === 'string' ? parsed.country : null,\n }\n } catch {\n // Corrupt JSON — the visitor is simply undecided, which is the safe\n // reading.\n return null\n }\n}\n\n/**\n * Record a consent state and make the page agree with it immediately.\n *\n * Storing the record is itself strictly-necessary storage (it is how a \"no\"\n * is remembered), so it is exempt from the consent it records. Any\n * non-granting state also REMOVES the identifiers the grant allowed —\n * the persistent `aglyn:visitor` id AND the GA cookies\n * ({@link clearAnalyticsCookies}): opting out cleans up, it does not merely\n * stop adding (AGL-1606) — and SILENCES the tag that is already running\n * ({@link setResidentAnalyticsTags}), without which the cleanup un-does itself\n * within the same pageview (AGL-1608). Listeners hear about it via\n * {@link VISITOR_CONSENT_CHANGED_EVENT} so the GA gate and the runtime react\n * without a reload.\n *\n * The two cleanups are deliberately in SEPARATE try blocks: private mode\n * throws on `localStorage` while cookies still work, and a withdrawal must\n * not lose the cookie sweep to that.\n */\nexport function storeVisitorConsent(\n hostId: string,\n state: {\n status: VisitorConsentStatus\n country?: string | null\n /**\n * The visitor's answer to the advertising question (AGL-1649). Omitted\n * by every caller that does not ask one, and omitted means NO.\n */\n advertising?: boolean\n },\n): StoredVisitorConsent {\n const stored: StoredVisitorConsent = {\n v: 1,\n at: Date.now(),\n status: state.status,\n analytics: analyticsGrantedByStatus(state.status),\n // Derived at store time exactly as `analytics` is, so a caller cannot\n // persist a grant the status does not support — a refusal always writes\n // `false` here even if the panel's checkbox was left ticked.\n advertising:\n advertisingGrantedByStatus(state.status) && state.advertising === true,\n country: state.country ?? null,\n }\n if (typeof window !== 'undefined') {\n try {\n window.localStorage.setItem(\n visitorConsentStorageKey(hostId),\n JSON.stringify(stored),\n )\n if (!stored.analytics) {\n window.localStorage.removeItem(VISITOR_ID_STORAGE_KEY)\n }\n } catch {\n // Private mode: the state holds for this page via the event below,\n // and is re-derived next visit — the safe failure.\n }\n // Silence the RESIDENT tag before sweeping, never after (AGL-1608): the\n // gate only keeps the script from LOADING, so a mid-pageview withdrawal\n // leaves gtag.js live and it re-writes `_ga_<id>` on its next enhanced\n // measurement event. Sweeping first would delete three cookies and\n // immediately get one back.\n setResidentAnalyticsTags(stored.analytics, stored.advertising === true)\n if (!stored.analytics) {\n clearAnalyticsCookies()\n }\n // The advertising half, and it belongs HERE rather than only in\n // `revokeAdvertisingTags` (AGL-2486). That function sweeps a vendor whose\n // marked script elements are in the document, and it is installed only on\n // our own marketing host — so on a CUSTOMER site, where the advertising\n // question is equally available, withdrawing advertising ran no sweep at\n // all. `_gcl_au` is written by the GA tag `site-analytics.tsx` mounts, not\n // by a script this module marked, so no element-scoped teardown was ever\n // going to reach it on any surface.\n //\n // This path is the one every site shares: it is where a decision becomes a\n // record. Unconditional on the grant being absent, for the reason in\n // `clearAdvertisingCookies` — a delete of something never set costs\n // nothing, a missed delete is the defect.\n if (!stored.advertising) {\n clearAdvertisingCookies()\n }\n try {\n window.dispatchEvent(new CustomEvent(VISITOR_CONSENT_CHANGED_EVENT))\n } catch {\n // A dispatch failure only delays consumers until the next read.\n }\n }\n return stored\n}\n\n/**\n * The GA gate's whole verdict: may the analytics category run for this\n * visitor, on this site, right now?\n *\n * - Consent machinery not required (no gated features, or host opted out of\n * the tool) → yes.\n * - Required and the recorded state grants analytics (`accepted` or\n * `implied`) → yes.\n * - Required and anything else — declined, opted out, GPC, or NOT YET\n * RESOLVED → no. Undecided is not a maybe: the script stays out until a\n * state that grants it exists. The gate never asks WHY consent is absent,\n * which is what keeps it posture-independent.\n */\nexport function isAnalyticsAllowed(\n host: VisitorConsentHost | null | undefined,\n stored: StoredVisitorConsent | null,\n): boolean {\n if (!hostConsentRequired(host)) return true\n return stored?.analytics === true\n}\n\n/**\n * Where the marketing runtime may keep its experiment visitor id.\n *\n * `local` — the id persists across visits. `session` — the id lives in\n * sessionStorage: variants stay stable within the visit and the identifier\n * dies with the tab. The downgrade applies while the consent machinery is\n * active for this site and analytics is not granted; the id itself never\n * leaves the browser either way (the experiment beacons are aggregate — no\n * visitor id on the wire).\n */\nexport function resolveVisitorIdPersistence(\n host: VisitorConsentHost | null | undefined,\n stored: StoredVisitorConsent | null,\n): 'local' | 'session' {\n return isAnalyticsAllowed(host, stored) ? 'local' : 'session'\n}\n"],"names":["ANALYTICS_PROVIDERS_DECLARED","applyAnalyticsConsent","CONSENT_STATUSES","Set","isExplicitConsentStatus","status","refusalStatusFor","stored","posture","analyticsGrantedByStatus","advertisingGrantedByStatus","GA_MEASUREMENT_ID_PATTERN","META_PIXEL_ID_PATTERN","GOOGLE_ADS_ID_PATTERN","LINKEDIN_PARTNER_ID_PATTERN","TIKTOK_PIXEL_ID_PATTERN","PINTEREST_TAG_ID_PATTERN","ADVERTISING_TAG_ID_PATTERNS","Object","freeze","meta","linkedin","tiktok","pinterest","resolveAdvertisingTagId","host","vendorId","pattern","candidate","String","analytics","adTags","test","hostConfiguresAdvertisingTag","keys","some","resolveGaMeasurementId","gaMeasurementId","GTM_CONTAINER_ID_PATTERN","resolveGtmContainerId","gtmContainerId","ANALYTICS_SETTING_PATTERNS","hostConfiguresAnalyticsTag","declared","settings","provider","field","isConsentToolDisabled","consent","disabled","resolveHostConsentMode","mode","consentGatedCategories","hostAsksAboutAdvertising","advertising","hostConsentRequired","length","PRIOR_CONSENT_COUNTRY_CODES","resolveConsentPosture","country","has","toUpperCase","hasGlobalPrivacyControl","navigator","globalPrivacyControl","VISITOR_CONSENT_STORAGE_PREFIX","VISITOR_ID_STORAGE_KEY","VISITOR_CONSENT_CHANGED_EVENT","VISITOR_CONSENT_OPEN_EVENT","visitorConsentStorageKey","hostId","ANALYTICS_COOKIE_PREFIXES","ADVERTISING_COOKIE_PREFIXES","clearAdvertisingCookies","hostname","clearCookiesWithPrefixes","MULTI_LABEL_PUBLIC_SUFFIXES","normalizeCookieHostname","trim","toLowerCase","replace","registrableCookieDomain","name","startsWith","includes","labels","split","label","lastTwo","slice","join","analyticsCookieDomains","registrable","domains","current","push","cut","indexOf","clearAnalyticsCookies","prefixes","document","window","names","pair","cookie","p","add","size","location","expired","domain","analyticsConsentSignals","granted","analytics_storage","ad_storage","ad_user_data","ad_personalization","consentModeSignals","grants","ads","advertisingGrantedByRecord","setResidentAnalyticsTags","readStoredVisitorConsent","parseStoredVisitorConsent","localStorage","getItem","raw","parsed","JSON","parse","v","at","storeVisitorConsent","state","Date","now","setItem","stringify","removeItem","dispatchEvent","CustomEvent","isAnalyticsAllowed","resolveVisitorIdPersistence"],"mappings":"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"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* The where-used scan's answer and its one-line summary, without the fetch
|
|
19
|
+
* (AGL-3668): pure, so the native apps word a delete's warning as the
|
|
20
|
+
* console does. `where-used.ts` re-exports it beside its client.
|
|
21
|
+
*/
|
|
22
|
+
export interface WhereUsedDependent {
|
|
23
|
+
/**
|
|
24
|
+
* `screen` or `layout` for a published page; otherwise the kind of record
|
|
25
|
+
* the plugin that answered names (`variable`, `workflow`).
|
|
26
|
+
*/
|
|
27
|
+
type: string;
|
|
28
|
+
id: string;
|
|
29
|
+
name: string;
|
|
30
|
+
via: Array<'id' | 'name'>;
|
|
31
|
+
/** Published version scanned (screens/layouts) — deep-link target. */
|
|
32
|
+
versionId?: string;
|
|
33
|
+
}
|
|
34
|
+
export interface WhereUsedResult {
|
|
35
|
+
dependents: WhereUsedDependent[];
|
|
36
|
+
total: number;
|
|
37
|
+
/** Dependents holding legacy name tokens — a rename breaks these. */
|
|
38
|
+
legacyCount: number;
|
|
39
|
+
}
|
|
40
|
+
/** One-line summary for confirm dialogs: `2 pages, 1 workflow`. */
|
|
41
|
+
export declare function summarizeDependents(result: WhereUsedResult): string;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ /**
|
|
17
|
+
* The where-used scan's answer and its one-line summary, without the fetch
|
|
18
|
+
* (AGL-3668): pure, so the native apps word a delete's warning as the
|
|
19
|
+
* console does. `where-used.ts` re-exports it beside its client.
|
|
20
|
+
*/ /** One-line summary for confirm dialogs: `2 pages, 1 workflow`. */ export function summarizeDependents(result) {
|
|
21
|
+
const counts = new Map();
|
|
22
|
+
for (const dependent of result.dependents){
|
|
23
|
+
var _counts_get;
|
|
24
|
+
// A `screen` is called a page wherever a person reads it.
|
|
25
|
+
const label = dependent.type === 'screen' ? 'page' : dependent.type;
|
|
26
|
+
counts.set(label, ((_counts_get = counts.get(label)) != null ? _counts_get : 0) + 1);
|
|
27
|
+
}
|
|
28
|
+
return [
|
|
29
|
+
...counts.entries()
|
|
30
|
+
].map(([label, count])=>`${count} ${label}${count === 1 ? '' : 's'}`).join(', ');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
//# sourceMappingURL=where-used-summary.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/where-used-summary.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 where-used scan's answer and its one-line summary, without the fetch\n * (AGL-3668): pure, so the native apps word a delete's warning as the\n * console does. `where-used.ts` re-exports it beside its client.\n */\n\nexport interface WhereUsedDependent {\n /**\n * `screen` or `layout` for a published page; otherwise the kind of record\n * the plugin that answered names (`variable`, `workflow`).\n */\n type: string\n id: string\n name: string\n via: Array<'id' | 'name'>\n /** Published version scanned (screens/layouts) — deep-link target. */\n versionId?: string\n}\n\nexport interface WhereUsedResult {\n dependents: WhereUsedDependent[]\n total: number\n /** Dependents holding legacy name tokens — a rename breaks these. */\n legacyCount: number\n}\n\n/** One-line summary for confirm dialogs: `2 pages, 1 workflow`. */\nexport function summarizeDependents(result: WhereUsedResult): string {\n const counts = new Map<string, number>()\n for (const dependent of result.dependents) {\n // A `screen` is called a page wherever a person reads it.\n const label = dependent.type === 'screen' ? 'page' : dependent.type\n counts.set(label, (counts.get(label) ?? 0) + 1)\n }\n return [...counts.entries()]\n .map(([label, count]) => `${count} ${label}${count === 1 ? '' : 's'}`)\n .join(', ')\n}\n"],"names":["summarizeDependents","result","counts","Map","dependent","dependents","label","type","set","get","entries","map","count","join"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;CAIC,GAsBD,iEAAiE,GACjE,OAAO,SAASA,oBAAoBC,MAAuB;IACzD,MAAMC,SAAS,IAAIC;IACnB,KAAK,MAAMC,aAAaH,OAAOI,UAAU,CAAE;YAGtBH;QAFnB,0DAA0D;QAC1D,MAAMI,QAAQF,UAAUG,IAAI,KAAK,WAAW,SAASH,UAAUG,IAAI;QACnEL,OAAOM,GAAG,CAACF,OAAO,EAACJ,cAAAA,OAAOO,GAAG,CAACH,kBAAXJ,cAAqB,KAAK;IAC/C;IACA,OAAO;WAAIA,OAAOQ,OAAO;KAAG,CACzBC,GAAG,CAAC,CAAC,CAACL,OAAOM,MAAM,GAAK,GAAGA,MAAM,CAAC,EAAEN,QAAQM,UAAU,IAAI,KAAK,KAAK,EACpEC,IAAI,CAAC;AACV"}
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/
|
|
17
|
+
import type { WhereUsedResult } from './where-used-summary';
|
|
17
18
|
/**
|
|
18
19
|
* The client of the console's where-used scan (`/api/hosts/where-used`).
|
|
19
20
|
*
|
|
@@ -27,26 +28,7 @@
|
|
|
27
28
|
* Imported by its own subpath; it is not in the barrel, so a page that never
|
|
28
29
|
* asks does not carry it.
|
|
29
30
|
*/
|
|
30
|
-
export
|
|
31
|
-
/**
|
|
32
|
-
* `screen` or `layout` for a published page; otherwise the kind of record
|
|
33
|
-
* the plugin that answered names (`variable`, `workflow`).
|
|
34
|
-
*/
|
|
35
|
-
type: string;
|
|
36
|
-
id: string;
|
|
37
|
-
name: string;
|
|
38
|
-
via: Array<'id' | 'name'>;
|
|
39
|
-
/** Published version scanned (screens/layouts) — deep-link target. */
|
|
40
|
-
versionId?: string;
|
|
41
|
-
}
|
|
42
|
-
export interface WhereUsedResult {
|
|
43
|
-
dependents: WhereUsedDependent[];
|
|
44
|
-
total: number;
|
|
45
|
-
/** Dependents holding legacy name tokens — a rename breaks these. */
|
|
46
|
-
legacyCount: number;
|
|
47
|
-
}
|
|
48
|
-
/** One-line summary for confirm dialogs: `2 pages, 1 workflow`. */
|
|
49
|
-
export declare function summarizeDependents(result: WhereUsedResult): string;
|
|
31
|
+
export { summarizeDependents, type WhereUsedDependent, type WhereUsedResult, } from './where-used-summary';
|
|
50
32
|
/**
|
|
51
33
|
* Calls the AGL-187 where-used scan for a variable/function/workflow.
|
|
52
34
|
* Fail-open: scan errors return an empty result so delete/rename flows
|
|
@@ -14,18 +14,19 @@
|
|
|
14
14
|
* See the License for the specific language governing permissions and
|
|
15
15
|
* limitations under the License.
|
|
16
16
|
*/ import { authorizedFetch } from "@aglyn/shared-util-http/authorized-token";
|
|
17
|
-
/**
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
17
|
+
/**
|
|
18
|
+
* The client of the console's where-used scan (`/api/hosts/where-used`).
|
|
19
|
+
*
|
|
20
|
+
* The scan is the platform's: one route answers what references a variable, a
|
|
21
|
+
* function or a workflow across a site's published screens and layouts, and
|
|
22
|
+
* asks the plugins whose records refer to it (`plugin-dependents`) for the
|
|
23
|
+
* rest. Its client sat in the logic plugin, so the workflows plugin imported
|
|
24
|
+
* logic to ask about a workflow. It is here beside the route it calls, and
|
|
25
|
+
* both ask the platform.
|
|
26
|
+
*
|
|
27
|
+
* Imported by its own subpath; it is not in the barrel, so a page that never
|
|
28
|
+
* asks does not carry it.
|
|
29
|
+
*/ export { summarizeDependents } from "./where-used-summary.js";
|
|
29
30
|
/**
|
|
30
31
|
* Calls the AGL-187 where-used scan for a variable/function/workflow.
|
|
31
32
|
* Fail-open: scan errors return an empty result so delete/rename flows
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/where-used.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\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\n\n/**\n * The client of the console's where-used scan (`/api/hosts/where-used`).\n *\n * The scan is the platform's: one route answers what references a variable, a\n * function or a workflow across a site's published screens and layouts, and\n * asks the plugins whose records refer to it (`plugin-dependents`) for the\n * rest. Its client sat in the logic plugin, so the workflows plugin imported\n * logic to ask about a workflow. It is here beside the route it calls, and\n * both ask the platform.\n *\n * Imported by its own subpath; it is not in the barrel, so a page that never\n * asks does not carry it.\n */\n\nexport
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/where-used.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\nimport { authorizedFetch } from '@aglyn/shared-util-http/authorized-token'\nimport type { WhereUsedResult } from './where-used-summary'\n\n/**\n * The client of the console's where-used scan (`/api/hosts/where-used`).\n *\n * The scan is the platform's: one route answers what references a variable, a\n * function or a workflow across a site's published screens and layouts, and\n * asks the plugins whose records refer to it (`plugin-dependents`) for the\n * rest. Its client sat in the logic plugin, so the workflows plugin imported\n * logic to ask about a workflow. It is here beside the route it calls, and\n * both ask the platform.\n *\n * Imported by its own subpath; it is not in the barrel, so a page that never\n * asks does not carry it.\n */\n\nexport {\n summarizeDependents,\n type WhereUsedDependent,\n type WhereUsedResult,\n} from './where-used-summary'\n\n/**\n * Calls the AGL-187 where-used scan for a variable/function/workflow.\n * Fail-open: scan errors return an empty result so delete/rename flows\n * degrade to their old (unwarned) behavior instead of blocking.\n */\nexport async function fetchWhereUsed(\n user: { getIdToken?: () => Promise<string> } | null | undefined,\n body: {\n hostId: string\n kind: 'variable' | 'function' | 'workflow'\n id: string\n name?: string\n },\n): Promise<WhereUsedResult> {\n try {\n const response = await authorizedFetch(user, '/api/hosts/where-used', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(body),\n })\n if (!response.ok) throw new Error(`Scan failed (${response.status})`)\n return (await response.json()) as WhereUsedResult\n } catch (error) {\n console.error(error)\n return { dependents: [], total: 0, legacyCount: 0 }\n }\n}\n"],"names":["authorizedFetch","summarizeDependents","fetchWhereUsed","user","body","response","method","headers","JSON","stringify","ok","Error","status","json","error","console","dependents","total","legacyCount"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,eAAe,QAAQ,2CAA0C;AAG1E;;;;;;;;;;;;CAYC,GAED,SACEC,mBAAmB,QAGd,0BAAsB;AAE7B;;;;CAIC,GACD,OAAO,eAAeC,eACpBC,IAA+D,EAC/DC,IAKC;IAED,IAAI;QACF,MAAMC,WAAW,MAAML,gBAAgBG,MAAM,yBAAyB;YACpEG,QAAQ;YACRC,SAAS;gBAAE,gBAAgB;YAAmB;YAC9CH,MAAMI,KAAKC,SAAS,CAACL;QACvB;QACA,IAAI,CAACC,SAASK,EAAE,EAAE,MAAM,IAAIC,MAAM,CAAC,aAAa,EAAEN,SAASO,MAAM,CAAC,CAAC,CAAC;QACpE,OAAQ,MAAMP,SAASQ,IAAI;IAC7B,EAAE,OAAOC,OAAO;QACdC,QAAQD,KAAK,CAACA;QACd,OAAO;YAAEE,YAAY,EAAE;YAAEC,OAAO;YAAGC,aAAa;QAAE;IACpD;AACF"}
|
|
@@ -842,10 +842,43 @@ export interface AglynHostMedia {
|
|
|
842
842
|
* site", which is what `visibleTo` is for.
|
|
843
843
|
*/
|
|
844
844
|
private?: boolean;
|
|
845
|
+
/**
|
|
846
|
+
* Where a photo copied from a stock library came from (AGL-3660), written
|
|
847
|
+
* by the server ingest that copied it (`core.media-ingest`). `key` is what
|
|
848
|
+
* a site's library is searched by before a photo is copied again, so one
|
|
849
|
+
* photo is stored once per site.
|
|
850
|
+
*/
|
|
851
|
+
stockPhoto?: AglynHostMediaStockSource;
|
|
845
852
|
createdAt?: ITimestamp;
|
|
846
853
|
updatedAt?: ITimestamp;
|
|
847
854
|
deletedAt?: ITimestamp;
|
|
848
855
|
}
|
|
856
|
+
/**
|
|
857
|
+
* The credit an asset copied from a stock photo library keeps (AGL-3660):
|
|
858
|
+
* the library, the photo's page there, its contributor and its license.
|
|
859
|
+
* Recorded whether or not the license requires a credit shown.
|
|
860
|
+
*/
|
|
861
|
+
export interface AglynHostMediaStockSource {
|
|
862
|
+
/** `{provider}:{id}`: the key a site's library reuses the asset by. */
|
|
863
|
+
key: string;
|
|
864
|
+
/** The provider's id, `pixabay`. */
|
|
865
|
+
provider: string;
|
|
866
|
+
/** The library's name, `Pixabay`. */
|
|
867
|
+
providerLabel: string;
|
|
868
|
+
/** The library's own id for the photo. */
|
|
869
|
+
id: string;
|
|
870
|
+
/** The photo's page at the library. */
|
|
871
|
+
pageUrl: string;
|
|
872
|
+
photographer: string;
|
|
873
|
+
photographerUrl?: string;
|
|
874
|
+
license: string;
|
|
875
|
+
licenseUrl: string;
|
|
876
|
+
/** Whether the license requires the credit shown wherever the photo is. */
|
|
877
|
+
attributionRequired: boolean;
|
|
878
|
+
/** The search words that found it. */
|
|
879
|
+
query?: string;
|
|
880
|
+
importedAt?: ITimestamp;
|
|
881
|
+
}
|
|
849
882
|
/**
|
|
850
883
|
* Media folder doc (AGL-171): `hosts/{hostId}/mediaFolders/{folderId}`.
|
|
851
884
|
* Hierarchy/validation helpers live in `app-utils/media-folders`.
|