@cookieyes/core 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2 @@
1
+ "use strict";function t(t,e){"undefined"!=typeof console&&(void 0!==e?console.error(`[cookieyes] ${t}`,e):console.warn(`[cookieyes] ${t}`))}function e(t,e){const o=Array.isArray(t.category)?t.category:[t.category];return 0!==o.length&&("any"===t.match?o.some(t=>e.granted(t)):o.every(t=>e.granted(t)))}exports.INTEGRATION_FORMAT_VERSION=1,exports.runIntegrations=function(o,n){const i=[],r=new Set;let s=!1;for(const e of o){const o=e;"string"!=typeof o.vendor||"function"==typeof o.setup?1===e.version?r.has(e.id)?t(`integration "${e.id}" is registered more than once; skipping the duplicate.`):(r.add(e.id),i.push({integration:e,status:"idle",everLoaded:!1,loading:!1,control:void 0,subs:new Set,wasGranted:!1})):t(`integration "${e.id}" uses format version ${e.version}, but this build understands 1. Skipping it.`):t(`an entry in "integrations" looks like the old built-in format ({ vendor: "${o.vendor}" }). That moved to "builtInIntegrations" — move it there, or use a preset from "@cookieyes/scripts" in "integrations". Skipping it.`)}function a(t){for(const e of t.subs)try{e()}catch{}t.subs.clear()}function c(e){a(e),"active"===e.status&&(!function(e){const{integration:o,control:n}=e;try{"remove"===o.onRevoke?n?.():"silence"===o.onRevoke&&n?.silence()}catch(e){t(`integration "${o.id}" threw while being torn down.`,e)}e.control=void 0}(e),"remove"===e.integration.onRevoke?e.status="removed":"silence"===e.integration.onRevoke&&(e.status="silenced"))}function d(o){o.loading=!0,o.status="loading",Promise.resolve().then(()=>o.integration.setup(function(t){return{granted:()=>e(t.integration,n),onConsentChange:e=>{const o=n.subscribe(e);return t.subs.add(o),()=>{t.subs.delete(o)&&o()}},region:n.region}}(o))).then(t=>{o.loading=!1,o.control=t??void 0,o.everLoaded=!0,o.status="active",s?c(o):u(o)}).catch(e=>{o.loading=!1,o.status="error",a(o),t(`integration "${o.integration.id}" failed to load; will retry on the next change.`,e)})}function u(o){const{integration:i}=o,r=e(i,n);if(r&&(o.wasGranted=!0),s||o.loading)return;if("idle"===o.status||"removed"===o.status||"error"===o.status){return void((o.everLoaded?r:"immediately"===i.load||r)&&d(o))}if("keep"===i.onRevoke)return;if("remove"===i.onRevoke){if(!r&&"active"===o.status&&o.wasGranted){a(o);try{o.control?.()}catch(e){t(`integration "${i.id}" cleanup threw on revoke.`,e)}o.control=void 0,o.status="removed"}return}const c=o.control;if(r||"active"!==o.status){if(r&&"silenced"===o.status){try{c?.resume()}catch(e){t(`integration "${i.id}" resume() threw.`,e)}o.status="active"}}else{try{c?.silence()}catch(e){t(`integration "${i.id}" silence() threw.`,e)}o.status="silenced"}}function g(){for(const t of i)u(t)}const l=n.subscribe(g);return g(),{status:()=>{const t={};for(const e of i)t[e.integration.id]=e.status;return t},list:()=>i.map(t=>({id:t.integration.id,category:t.integration.category,load:t.integration.load,onRevoke:t.integration.onRevoke,status:t.status})),stop:()=>{if(!s){s=!0,l();for(const t of i)c(t)}}}},exports.warnOverlappingVendors=function(e,o){const n=new Set(e);for(const e of o)n.has(e)&&t(`"${e}" is configured as both a script integration and a built-in integration — remove one to avoid loading it twice (e.g. double-counted events).`)},exports.warnUnknownCategories=function(e,o){const n=new Set(o);for(const o of e){if("afterConsent"!==o.load)continue;const e=o.category,i=Array.isArray(e)?e:[e];if(0!==i.length)for(const e of i)n.has(e)||t(`integration "${o.id}" is gated on category "${e}", which isn't in your configured categories — it will never load. Pass a category that exists (e.g. segment({ category: "…" })), or add it to your taxonomy.`);else t(`integration "${o.id}" has an empty category list — it will never load. Give it at least one category that exists in your taxonomy.`)}};
2
+ //# sourceMappingURL=integrations.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"integrations.cjs","sources":["../src/integrations.ts"],"sourcesContent":["import type { ConsentCategory, RegionDecision } from \"./types.js\";\n\n/**\n * The integration format — the shared contract between the consent engine and\n * every vendor preset (Google, Segment, Meta, …). It carries no vendor\n * knowledge; a preset in `@cookieyes/scripts` fills it in.\n *\n * Two plain axes describe every vendor:\n * - `load`: when the vendor starts — right away, or only after consent.\n * - `onRevoke`: what happens when consent is withdrawn — one of three modes.\n *\n * `onRevoke` is declarative (readable without running `setup`, so a debug view\n * or a version check can inspect it), and it's a discriminated union so the\n * compiler forces the matching handler — you can't declare `\"silence\"` with no\n * way to actually silence.\n */\nexport type Cleanup = () => void;\nexport type SilenceControl = { silence: () => void; resume: () => void };\n\n/** What a vendor's `setup` receives from the engine. */\nexport type SetupCtx = {\n /** Is this integration's category currently committed-granted? */\n granted: () => boolean;\n /**\n * Subscribe to committed-consent changes; returns an unsubscribe function.\n * `keep` vendors (e.g. Google Consent Mode) use this to emit their own\n * updates on every change. The engine also releases these automatically when\n * the integration is torn down, so a vendor listener never outlives it.\n */\n onConsentChange: (fn: () => void) => () => void;\n /** Resolved region/regulation — for a vendor's US-state privacy switch. */\n region: RegionDecision;\n};\n\ntype Base = {\n id: string;\n /**\n * The consent category (or categories) that gate this integration. Pass an\n * array to require more than one — combined with {@link Base.match}.\n */\n category: ConsentCategory | ConsentCategory[];\n /**\n * How to combine multiple categories: `\"all\"` (default) needs every one\n * granted; `\"any\"` needs at least one. Ignored for a single category.\n */\n match?: \"all\" | \"any\";\n /** Format version; an unknown version is refused (see {@link runIntegrations}). */\n version: number;\n /**\n * When to start. Note: `setup` always runs on a microtask, so `immediately`\n * is *very early* but not synchronous / same-tick. The truly-first-thing case\n * (Consent Mode's deny-default before any tag fires) is the server `<head>`\n * snippet's job, not the engine's.\n */\n load: \"immediately\" | \"afterConsent\";\n};\n\nexport type Integration = Base &\n (\n | { onRevoke: \"keep\"; setup: (ctx: SetupCtx) => void | Promise<void> }\n | {\n onRevoke: \"remove\";\n setup: (ctx: SetupCtx) => Cleanup | Promise<Cleanup>;\n }\n | {\n onRevoke: \"silence\";\n setup: (ctx: SetupCtx) => SilenceControl | Promise<SilenceControl>;\n }\n );\n\n/** The format version this engine understands. Bump on a breaking format change. */\nexport const INTEGRATION_FORMAT_VERSION = 1;\n\n/**\n * Warn when a vendor is configured on both sides — a new `integrations` preset\n * whose id matches a deprecated `builtInIntegrations` vendor. Both would run,\n * which for a tracker means double-counted events (the DEVP-38 double-pixel).\n * Preset ids are the vendor's canonical name (`\"segment\"`, `\"meta\"`, …), which\n * is exactly the built-in vendor name, so the match is a plain id === vendor.\n */\nexport function warnOverlappingVendors(integrationIds: string[], builtInVendors: string[]): void {\n const ids = new Set(integrationIds);\n for (const vendor of builtInVendors) {\n if (ids.has(vendor)) {\n warn(\n `\"${vendor}\" is configured as both a script integration and a built-in integration — ` +\n \"remove one to avoid loading it twice (e.g. double-counted events).\",\n );\n }\n }\n}\n\n/**\n * Warn when an `afterConsent` integration is gated on a category that isn't in\n * the configured taxonomy — otherwise it waits for consent that can never be\n * granted and silently never loads. A common trap with a custom `categories`\n * list plus a preset left on its default category (e.g. `segment()` → \"analytics\").\n * `immediately` integrations aren't gated by category, so they're not checked.\n */\nexport function warnUnknownCategories(\n integrations: Integration[],\n knownCategoryIds: string[],\n): void {\n const known = new Set(knownCategoryIds);\n for (const integration of integrations) {\n if (integration.load !== \"afterConsent\") continue;\n const raw = integration.category as ConsentCategory | ConsentCategory[];\n const cats = Array.isArray(raw) ? raw : [raw];\n if (cats.length === 0) {\n warn(\n `integration \"${integration.id}\" has an empty category list — it will never load. ` +\n \"Give it at least one category that exists in your taxonomy.\",\n );\n continue;\n }\n for (const category of cats) {\n if (!known.has(category)) {\n warn(\n `integration \"${integration.id}\" is gated on category \"${category}\", which isn't in ` +\n \"your configured categories — it will never load. Pass a category that exists \" +\n '(e.g. segment({ category: \"…\" })), or add it to your taxonomy.',\n );\n }\n }\n }\n}\n\n/** Live status of one integration — read by the debug/self-check view. */\nexport type IntegrationStatus =\n | \"idle\" // afterConsent, waiting for consent\n | \"loading\"\n | \"active\" // loaded and running\n | \"silenced\" // loaded, told to go quiet\n | \"removed\" // was loaded, removed on revoke\n | \"error\"; // setup failed; retried on the next trigger\n\n/** What the engine needs from the consent runtime — framework-agnostic. */\nexport type IntegrationHost = {\n /** Committed-granted state for a category (never the unsaved toggle). */\n granted: (category: ConsentCategory) => boolean;\n /** Subscribe to committed-consent changes; returns unsubscribe. */\n subscribe: (fn: () => void) => () => void;\n region: RegionDecision;\n};\n\ntype Entry = {\n integration: Integration;\n status: IntegrationStatus;\n everLoaded: boolean;\n loading: boolean;\n control: undefined | Cleanup | SilenceControl;\n /** Has this category ever been granted? Gates `remove` to real withdrawals. */\n wasGranted: boolean;\n /** Consent-change listeners the vendor opened via `ctx.onConsentChange`. */\n subs: Set<() => void>;\n};\n\n/** One row for a debug view: an integration's config plus its live status. */\nexport type IntegrationDebugInfo = {\n id: string;\n category: ConsentCategory | ConsentCategory[];\n load: \"immediately\" | \"afterConsent\";\n onRevoke: \"keep\" | \"remove\" | \"silence\";\n status: IntegrationStatus;\n};\n\nexport type IntegrationRunner = {\n /** Current status per integration id — the debug/self-check view reads this. */\n status: () => Record<string, IntegrationStatus>;\n /**\n * Config + live status for every registered integration, in order — the data\n * for a debug view (e.g. `console.table(runner.list())`).\n */\n list: () => IntegrationDebugInfo[];\n /**\n * Tear the runner down: stop reconciling, release every vendor's\n * consent-change listener, and undo each loaded vendor — `remove` runs its\n * cleanup, `silence` is silenced. `keep` vendors' scripts stay (that's the\n * mode), but their listeners are still released. Safe to call more than once.\n */\n stop: () => void;\n};\n\nfunction warn(message: string, err?: unknown): void {\n if (typeof console !== \"undefined\") {\n // eslint-disable-next-line no-console\n if (err !== undefined) console.error(`[cookieyes] ${message}`, err);\n else console.warn(`[cookieyes] ${message}`);\n }\n}\n\n/**\n * Is this integration's category requirement met? One category → just that one.\n * Several → `match: \"all\"` (default) needs every one granted, `\"any\"` needs one.\n */\nfunction isGranted(integration: Integration, host: IntegrationHost): boolean {\n const cats = Array.isArray(integration.category) ? integration.category : [integration.category];\n if (cats.length === 0) return false;\n return integration.match === \"any\"\n ? cats.some((c) => host.granted(c))\n : cats.every((c) => host.granted(c));\n}\n\n/**\n * Run a set of integrations against the consent runtime. Reconciles each one on\n * load and on every committed-consent change:\n * - not loaded → load it (immediately, or once its category is granted)\n * - `keep` → nothing on revoke (the vendor manages its own update)\n * - `remove` → run cleanup on revoke; re-load on re-grant\n * - `silence` → call `silence()` on revoke; `resume()` on re-grant\n *\n * `setup` may be async and may fail; a failure marks the integration `error`\n * and is retried on the next trigger (never loops on its own). Nothing here\n * throws into the host.\n */\nexport function runIntegrations(\n integrations: Integration[],\n host: IntegrationHost,\n): IntegrationRunner {\n const entries: Entry[] = [];\n const seenIds = new Set<string>();\n let stopped = false;\n\n for (const integration of integrations) {\n // A leftover from the old API — `integrations: [{ vendor: \"meta\" }]`. That\n // field was renamed to `builtInIntegrations`, so guide the developer here\n // instead of skipping it with a confusing \"unknown version\" warning.\n const legacy = integration as unknown as { vendor?: unknown; setup?: unknown };\n if (typeof legacy.vendor === \"string\" && typeof legacy.setup !== \"function\") {\n warn(\n `an entry in \"integrations\" looks like the old built-in format ` +\n `({ vendor: \"${legacy.vendor}\" }). That moved to \"builtInIntegrations\" — move it ` +\n `there, or use a preset from \"@cookieyes/scripts\" in \"integrations\". Skipping it.`,\n );\n continue;\n }\n if (integration.version !== INTEGRATION_FORMAT_VERSION) {\n // Unknown format version → refuse to run it, don't guess (Story 5).\n warn(\n `integration \"${integration.id}\" uses format version ${integration.version}, ` +\n `but this build understands ${INTEGRATION_FORMAT_VERSION}. Skipping it.`,\n );\n continue;\n }\n if (seenIds.has(integration.id)) {\n // A duplicate id would collide in the status view and run twice — skip it\n // rather than show a wrong picture (Story 7's \"flag common mistakes\").\n warn(`integration \"${integration.id}\" is registered more than once; skipping the duplicate.`);\n continue;\n }\n seenIds.add(integration.id);\n entries.push({\n integration,\n status: \"idle\",\n everLoaded: false,\n loading: false,\n control: undefined,\n subs: new Set(),\n wasGranted: false,\n });\n }\n\n function ctxFor(entry: Entry): SetupCtx {\n return {\n granted: () => isGranted(entry.integration, host),\n onConsentChange: (fn) => {\n // Track the vendor's listener so it's released on teardown — otherwise a\n // removed or stopped vendor keeps listening forever.\n const unsub = host.subscribe(fn);\n entry.subs.add(unsub);\n return () => {\n if (entry.subs.delete(unsub)) unsub();\n };\n },\n region: host.region,\n };\n }\n\n function releaseSubs(entry: Entry): void {\n for (const unsub of entry.subs) {\n try {\n unsub();\n } catch {\n // A vendor's own unsubscribe throwing is non-fatal.\n }\n }\n entry.subs.clear();\n }\n\n /** Undo a loaded vendor: remove → cleanup, silence → silence. keep: nothing. */\n function undo(entry: Entry): void {\n const { integration, control } = entry;\n try {\n if (integration.onRevoke === \"remove\") (control as Cleanup | undefined)?.();\n else if (integration.onRevoke === \"silence\")\n (control as SilenceControl | undefined)?.silence();\n } catch (err) {\n warn(`integration \"${integration.id}\" threw while being torn down.`, err);\n }\n entry.control = undefined;\n }\n\n /** Release listeners and undo the vendor if it's currently loaded/active. */\n function teardownEntry(entry: Entry): void {\n releaseSubs(entry);\n if (entry.status !== \"active\") return; // idle/silenced/removed/error: nothing to undo\n undo(entry);\n // `keep` leaves the script on the page (only the listener was released).\n if (entry.integration.onRevoke === \"remove\") entry.status = \"removed\";\n else if (entry.integration.onRevoke === \"silence\") entry.status = \"silenced\";\n }\n\n function load(entry: Entry): void {\n entry.loading = true;\n entry.status = \"loading\";\n // Normalise sync/async setup into a promise; a sync throw becomes a reject.\n Promise.resolve()\n .then(() => entry.integration.setup(ctxFor(entry)))\n .then((control) => {\n entry.loading = false;\n entry.control = control ?? undefined;\n entry.everLoaded = true;\n entry.status = \"active\";\n if (stopped) {\n // Torn down while loading → undo this load right away so the vendor\n // doesn't linger past teardown.\n teardownEntry(entry);\n return;\n }\n // Consent may have flipped while we were loading — reconcile once more.\n reconcile(entry);\n })\n .catch((err) => {\n entry.loading = false;\n entry.status = \"error\";\n releaseSubs(entry); // a failed setup may have subscribed before throwing\n warn(\n `integration \"${entry.integration.id}\" failed to load; will retry on the next change.`,\n err,\n );\n });\n }\n\n function reconcile(entry: Entry): void {\n const { integration } = entry;\n const granted = isGranted(integration, host);\n // Record a grant even while the script is still loading — otherwise a\n // grant-then-withdraw during the load window is lost, and a `remove` vendor\n // would never be removed afterward.\n if (granted) entry.wasGranted = true;\n if (stopped || entry.loading) return;\n\n // Not currently loaded (idle / removed / error) → decide whether to load.\n if (entry.status === \"idle\" || entry.status === \"removed\" || entry.status === \"error\") {\n const shouldLoad = entry.everLoaded ? granted : integration.load === \"immediately\" || granted;\n if (shouldLoad) load(entry);\n return;\n }\n\n // Loaded (active / silenced) → apply the revoke mode.\n if (integration.onRevoke === \"keep\") return;\n\n if (integration.onRevoke === \"remove\") {\n // Only a real withdrawal (granted → denied) removes. A never-granted\n // `immediately` load must not load-then-remove (a wasted request);\n // `silence`, by contrast, stays ungated so it can quiet a vendor before\n // the first consent.\n if (!granted && entry.status === \"active\" && entry.wasGranted) {\n releaseSubs(entry); // the removed script's listeners go with it\n try {\n (entry.control as Cleanup | undefined)?.();\n } catch (err) {\n warn(`integration \"${integration.id}\" cleanup threw on revoke.`, err);\n }\n entry.control = undefined;\n entry.status = \"removed\";\n }\n return;\n }\n\n // silence\n const sc = entry.control as SilenceControl | undefined;\n if (!granted && entry.status === \"active\") {\n try {\n sc?.silence();\n } catch (err) {\n warn(`integration \"${integration.id}\" silence() threw.`, err);\n }\n entry.status = \"silenced\";\n } else if (granted && entry.status === \"silenced\") {\n try {\n sc?.resume();\n } catch (err) {\n warn(`integration \"${integration.id}\" resume() threw.`, err);\n }\n entry.status = \"active\";\n }\n }\n\n function reconcileAll(): void {\n for (const entry of entries) reconcile(entry);\n }\n\n const unsubscribe = host.subscribe(reconcileAll);\n reconcileAll(); // initial pass\n\n return {\n status: () => {\n const out: Record<string, IntegrationStatus> = {};\n for (const entry of entries) out[entry.integration.id] = entry.status;\n return out;\n },\n list: () =>\n entries.map((entry) => ({\n id: entry.integration.id,\n category: entry.integration.category,\n load: entry.integration.load,\n onRevoke: entry.integration.onRevoke,\n status: entry.status,\n })),\n stop: () => {\n if (stopped) return;\n stopped = true;\n unsubscribe();\n for (const entry of entries) teardownEntry(entry);\n },\n };\n}\n"],"names":["warn","message","err","console","error","isGranted","integration","host","cats","Array","isArray","category","length","match","some","c","granted","every","integrations","entries","seenIds","Set","stopped","legacy","vendor","setup","version","has","id","add","push","status","everLoaded","loading","control","subs","wasGranted","releaseSubs","entry","unsub","clear","teardownEntry","onRevoke","silence","undo","load","Promise","resolve","then","onConsentChange","fn","subscribe","delete","region","ctxFor","reconcile","catch","sc","resume","reconcileAll","unsubscribe","out","list","map","stop","integrationIds","builtInVendors","ids","knownCategoryIds","known","raw"],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export { x as Cleanup, O as INTEGRATION_FORMAT_VERSION, d as Integration, Q as IntegrationDebugInfo, n as IntegrationHost, o as IntegrationRunner, U as IntegrationStatus, $ as SetupCtx, a0 as SilenceControl, a7 as runIntegrations, a9 as warnOverlappingVendors, aa as warnUnknownCategories } from './categories-DMBKo4Eb.js';
@@ -0,0 +1,2 @@
1
+ const t=1;function e(t,e){const o=new Set(t);for(const t of e)o.has(t)&&n(`"${t}" is configured as both a script integration and a built-in integration — remove one to avoid loading it twice (e.g. double-counted events).`)}function o(t,e){const o=new Set(e);for(const e of t){if("afterConsent"!==e.load)continue;const t=e.category,i=Array.isArray(t)?t:[t];if(0!==i.length)for(const t of i)o.has(t)||n(`integration "${e.id}" is gated on category "${t}", which isn't in your configured categories — it will never load. Pass a category that exists (e.g. segment({ category: "…" })), or add it to your taxonomy.`);else n(`integration "${e.id}" has an empty category list — it will never load. Give it at least one category that exists in your taxonomy.`)}}function n(t,e){"undefined"!=typeof console&&(void 0!==e?console.error(`[cookieyes] ${t}`,e):console.warn(`[cookieyes] ${t}`))}function i(t,e){const o=Array.isArray(t.category)?t.category:[t.category];return 0!==o.length&&("any"===t.match?o.some(t=>e.granted(t)):o.every(t=>e.granted(t)))}function r(t,e){const o=[],r=new Set;let s=!1;for(const e of t){const t=e;"string"!=typeof t.vendor||"function"==typeof t.setup?1===e.version?r.has(e.id)?n(`integration "${e.id}" is registered more than once; skipping the duplicate.`):(r.add(e.id),o.push({integration:e,status:"idle",everLoaded:!1,loading:!1,control:void 0,subs:new Set,wasGranted:!1})):n(`integration "${e.id}" uses format version ${e.version}, but this build understands 1. Skipping it.`):n(`an entry in "integrations" looks like the old built-in format ({ vendor: "${t.vendor}" }). That moved to "builtInIntegrations" — move it there, or use a preset from "@cookieyes/scripts" in "integrations". Skipping it.`)}function a(t){for(const e of t.subs)try{e()}catch{}t.subs.clear()}function c(t){a(t),"active"===t.status&&(!function(t){const{integration:e,control:o}=t;try{"remove"===e.onRevoke?o?.():"silence"===e.onRevoke&&o?.silence()}catch(t){n(`integration "${e.id}" threw while being torn down.`,t)}t.control=void 0}(t),"remove"===t.integration.onRevoke?t.status="removed":"silence"===t.integration.onRevoke&&(t.status="silenced"))}function d(t){t.loading=!0,t.status="loading",Promise.resolve().then(()=>t.integration.setup(function(t){return{granted:()=>i(t.integration,e),onConsentChange:o=>{const n=e.subscribe(o);return t.subs.add(n),()=>{t.subs.delete(n)&&n()}},region:e.region}}(t))).then(e=>{t.loading=!1,t.control=e??void 0,t.everLoaded=!0,t.status="active",s?c(t):u(t)}).catch(e=>{t.loading=!1,t.status="error",a(t),n(`integration "${t.integration.id}" failed to load; will retry on the next change.`,e)})}function u(t){const{integration:o}=t,r=i(o,e);if(r&&(t.wasGranted=!0),s||t.loading)return;if("idle"===t.status||"removed"===t.status||"error"===t.status){return void((t.everLoaded?r:"immediately"===o.load||r)&&d(t))}if("keep"===o.onRevoke)return;if("remove"===o.onRevoke){if(!r&&"active"===t.status&&t.wasGranted){a(t);try{t.control?.()}catch(t){n(`integration "${o.id}" cleanup threw on revoke.`,t)}t.control=void 0,t.status="removed"}return}const c=t.control;if(r||"active"!==t.status){if(r&&"silenced"===t.status){try{c?.resume()}catch(t){n(`integration "${o.id}" resume() threw.`,t)}t.status="active"}}else{try{c?.silence()}catch(t){n(`integration "${o.id}" silence() threw.`,t)}t.status="silenced"}}function g(){for(const t of o)u(t)}const l=e.subscribe(g);return g(),{status:()=>{const t={};for(const e of o)t[e.integration.id]=e.status;return t},list:()=>o.map(t=>({id:t.integration.id,category:t.integration.category,load:t.integration.load,onRevoke:t.integration.onRevoke,status:t.status})),stop:()=>{if(!s){s=!0,l();for(const t of o)c(t)}}}}export{t as INTEGRATION_FORMAT_VERSION,r as runIntegrations,e as warnOverlappingVendors,o as warnUnknownCategories};
2
+ //# sourceMappingURL=integrations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"integrations.js","sources":["../src/integrations.ts"],"sourcesContent":["import type { ConsentCategory, RegionDecision } from \"./types.js\";\n\n/**\n * The integration format — the shared contract between the consent engine and\n * every vendor preset (Google, Segment, Meta, …). It carries no vendor\n * knowledge; a preset in `@cookieyes/scripts` fills it in.\n *\n * Two plain axes describe every vendor:\n * - `load`: when the vendor starts — right away, or only after consent.\n * - `onRevoke`: what happens when consent is withdrawn — one of three modes.\n *\n * `onRevoke` is declarative (readable without running `setup`, so a debug view\n * or a version check can inspect it), and it's a discriminated union so the\n * compiler forces the matching handler — you can't declare `\"silence\"` with no\n * way to actually silence.\n */\nexport type Cleanup = () => void;\nexport type SilenceControl = { silence: () => void; resume: () => void };\n\n/** What a vendor's `setup` receives from the engine. */\nexport type SetupCtx = {\n /** Is this integration's category currently committed-granted? */\n granted: () => boolean;\n /**\n * Subscribe to committed-consent changes; returns an unsubscribe function.\n * `keep` vendors (e.g. Google Consent Mode) use this to emit their own\n * updates on every change. The engine also releases these automatically when\n * the integration is torn down, so a vendor listener never outlives it.\n */\n onConsentChange: (fn: () => void) => () => void;\n /** Resolved region/regulation — for a vendor's US-state privacy switch. */\n region: RegionDecision;\n};\n\ntype Base = {\n id: string;\n /**\n * The consent category (or categories) that gate this integration. Pass an\n * array to require more than one — combined with {@link Base.match}.\n */\n category: ConsentCategory | ConsentCategory[];\n /**\n * How to combine multiple categories: `\"all\"` (default) needs every one\n * granted; `\"any\"` needs at least one. Ignored for a single category.\n */\n match?: \"all\" | \"any\";\n /** Format version; an unknown version is refused (see {@link runIntegrations}). */\n version: number;\n /**\n * When to start. Note: `setup` always runs on a microtask, so `immediately`\n * is *very early* but not synchronous / same-tick. The truly-first-thing case\n * (Consent Mode's deny-default before any tag fires) is the server `<head>`\n * snippet's job, not the engine's.\n */\n load: \"immediately\" | \"afterConsent\";\n};\n\nexport type Integration = Base &\n (\n | { onRevoke: \"keep\"; setup: (ctx: SetupCtx) => void | Promise<void> }\n | {\n onRevoke: \"remove\";\n setup: (ctx: SetupCtx) => Cleanup | Promise<Cleanup>;\n }\n | {\n onRevoke: \"silence\";\n setup: (ctx: SetupCtx) => SilenceControl | Promise<SilenceControl>;\n }\n );\n\n/** The format version this engine understands. Bump on a breaking format change. */\nexport const INTEGRATION_FORMAT_VERSION = 1;\n\n/**\n * Warn when a vendor is configured on both sides — a new `integrations` preset\n * whose id matches a deprecated `builtInIntegrations` vendor. Both would run,\n * which for a tracker means double-counted events (the DEVP-38 double-pixel).\n * Preset ids are the vendor's canonical name (`\"segment\"`, `\"meta\"`, …), which\n * is exactly the built-in vendor name, so the match is a plain id === vendor.\n */\nexport function warnOverlappingVendors(integrationIds: string[], builtInVendors: string[]): void {\n const ids = new Set(integrationIds);\n for (const vendor of builtInVendors) {\n if (ids.has(vendor)) {\n warn(\n `\"${vendor}\" is configured as both a script integration and a built-in integration — ` +\n \"remove one to avoid loading it twice (e.g. double-counted events).\",\n );\n }\n }\n}\n\n/**\n * Warn when an `afterConsent` integration is gated on a category that isn't in\n * the configured taxonomy — otherwise it waits for consent that can never be\n * granted and silently never loads. A common trap with a custom `categories`\n * list plus a preset left on its default category (e.g. `segment()` → \"analytics\").\n * `immediately` integrations aren't gated by category, so they're not checked.\n */\nexport function warnUnknownCategories(\n integrations: Integration[],\n knownCategoryIds: string[],\n): void {\n const known = new Set(knownCategoryIds);\n for (const integration of integrations) {\n if (integration.load !== \"afterConsent\") continue;\n const raw = integration.category as ConsentCategory | ConsentCategory[];\n const cats = Array.isArray(raw) ? raw : [raw];\n if (cats.length === 0) {\n warn(\n `integration \"${integration.id}\" has an empty category list — it will never load. ` +\n \"Give it at least one category that exists in your taxonomy.\",\n );\n continue;\n }\n for (const category of cats) {\n if (!known.has(category)) {\n warn(\n `integration \"${integration.id}\" is gated on category \"${category}\", which isn't in ` +\n \"your configured categories — it will never load. Pass a category that exists \" +\n '(e.g. segment({ category: \"…\" })), or add it to your taxonomy.',\n );\n }\n }\n }\n}\n\n/** Live status of one integration — read by the debug/self-check view. */\nexport type IntegrationStatus =\n | \"idle\" // afterConsent, waiting for consent\n | \"loading\"\n | \"active\" // loaded and running\n | \"silenced\" // loaded, told to go quiet\n | \"removed\" // was loaded, removed on revoke\n | \"error\"; // setup failed; retried on the next trigger\n\n/** What the engine needs from the consent runtime — framework-agnostic. */\nexport type IntegrationHost = {\n /** Committed-granted state for a category (never the unsaved toggle). */\n granted: (category: ConsentCategory) => boolean;\n /** Subscribe to committed-consent changes; returns unsubscribe. */\n subscribe: (fn: () => void) => () => void;\n region: RegionDecision;\n};\n\ntype Entry = {\n integration: Integration;\n status: IntegrationStatus;\n everLoaded: boolean;\n loading: boolean;\n control: undefined | Cleanup | SilenceControl;\n /** Has this category ever been granted? Gates `remove` to real withdrawals. */\n wasGranted: boolean;\n /** Consent-change listeners the vendor opened via `ctx.onConsentChange`. */\n subs: Set<() => void>;\n};\n\n/** One row for a debug view: an integration's config plus its live status. */\nexport type IntegrationDebugInfo = {\n id: string;\n category: ConsentCategory | ConsentCategory[];\n load: \"immediately\" | \"afterConsent\";\n onRevoke: \"keep\" | \"remove\" | \"silence\";\n status: IntegrationStatus;\n};\n\nexport type IntegrationRunner = {\n /** Current status per integration id — the debug/self-check view reads this. */\n status: () => Record<string, IntegrationStatus>;\n /**\n * Config + live status for every registered integration, in order — the data\n * for a debug view (e.g. `console.table(runner.list())`).\n */\n list: () => IntegrationDebugInfo[];\n /**\n * Tear the runner down: stop reconciling, release every vendor's\n * consent-change listener, and undo each loaded vendor — `remove` runs its\n * cleanup, `silence` is silenced. `keep` vendors' scripts stay (that's the\n * mode), but their listeners are still released. Safe to call more than once.\n */\n stop: () => void;\n};\n\nfunction warn(message: string, err?: unknown): void {\n if (typeof console !== \"undefined\") {\n // eslint-disable-next-line no-console\n if (err !== undefined) console.error(`[cookieyes] ${message}`, err);\n else console.warn(`[cookieyes] ${message}`);\n }\n}\n\n/**\n * Is this integration's category requirement met? One category → just that one.\n * Several → `match: \"all\"` (default) needs every one granted, `\"any\"` needs one.\n */\nfunction isGranted(integration: Integration, host: IntegrationHost): boolean {\n const cats = Array.isArray(integration.category) ? integration.category : [integration.category];\n if (cats.length === 0) return false;\n return integration.match === \"any\"\n ? cats.some((c) => host.granted(c))\n : cats.every((c) => host.granted(c));\n}\n\n/**\n * Run a set of integrations against the consent runtime. Reconciles each one on\n * load and on every committed-consent change:\n * - not loaded → load it (immediately, or once its category is granted)\n * - `keep` → nothing on revoke (the vendor manages its own update)\n * - `remove` → run cleanup on revoke; re-load on re-grant\n * - `silence` → call `silence()` on revoke; `resume()` on re-grant\n *\n * `setup` may be async and may fail; a failure marks the integration `error`\n * and is retried on the next trigger (never loops on its own). Nothing here\n * throws into the host.\n */\nexport function runIntegrations(\n integrations: Integration[],\n host: IntegrationHost,\n): IntegrationRunner {\n const entries: Entry[] = [];\n const seenIds = new Set<string>();\n let stopped = false;\n\n for (const integration of integrations) {\n // A leftover from the old API — `integrations: [{ vendor: \"meta\" }]`. That\n // field was renamed to `builtInIntegrations`, so guide the developer here\n // instead of skipping it with a confusing \"unknown version\" warning.\n const legacy = integration as unknown as { vendor?: unknown; setup?: unknown };\n if (typeof legacy.vendor === \"string\" && typeof legacy.setup !== \"function\") {\n warn(\n `an entry in \"integrations\" looks like the old built-in format ` +\n `({ vendor: \"${legacy.vendor}\" }). That moved to \"builtInIntegrations\" — move it ` +\n `there, or use a preset from \"@cookieyes/scripts\" in \"integrations\". Skipping it.`,\n );\n continue;\n }\n if (integration.version !== INTEGRATION_FORMAT_VERSION) {\n // Unknown format version → refuse to run it, don't guess (Story 5).\n warn(\n `integration \"${integration.id}\" uses format version ${integration.version}, ` +\n `but this build understands ${INTEGRATION_FORMAT_VERSION}. Skipping it.`,\n );\n continue;\n }\n if (seenIds.has(integration.id)) {\n // A duplicate id would collide in the status view and run twice — skip it\n // rather than show a wrong picture (Story 7's \"flag common mistakes\").\n warn(`integration \"${integration.id}\" is registered more than once; skipping the duplicate.`);\n continue;\n }\n seenIds.add(integration.id);\n entries.push({\n integration,\n status: \"idle\",\n everLoaded: false,\n loading: false,\n control: undefined,\n subs: new Set(),\n wasGranted: false,\n });\n }\n\n function ctxFor(entry: Entry): SetupCtx {\n return {\n granted: () => isGranted(entry.integration, host),\n onConsentChange: (fn) => {\n // Track the vendor's listener so it's released on teardown — otherwise a\n // removed or stopped vendor keeps listening forever.\n const unsub = host.subscribe(fn);\n entry.subs.add(unsub);\n return () => {\n if (entry.subs.delete(unsub)) unsub();\n };\n },\n region: host.region,\n };\n }\n\n function releaseSubs(entry: Entry): void {\n for (const unsub of entry.subs) {\n try {\n unsub();\n } catch {\n // A vendor's own unsubscribe throwing is non-fatal.\n }\n }\n entry.subs.clear();\n }\n\n /** Undo a loaded vendor: remove → cleanup, silence → silence. keep: nothing. */\n function undo(entry: Entry): void {\n const { integration, control } = entry;\n try {\n if (integration.onRevoke === \"remove\") (control as Cleanup | undefined)?.();\n else if (integration.onRevoke === \"silence\")\n (control as SilenceControl | undefined)?.silence();\n } catch (err) {\n warn(`integration \"${integration.id}\" threw while being torn down.`, err);\n }\n entry.control = undefined;\n }\n\n /** Release listeners and undo the vendor if it's currently loaded/active. */\n function teardownEntry(entry: Entry): void {\n releaseSubs(entry);\n if (entry.status !== \"active\") return; // idle/silenced/removed/error: nothing to undo\n undo(entry);\n // `keep` leaves the script on the page (only the listener was released).\n if (entry.integration.onRevoke === \"remove\") entry.status = \"removed\";\n else if (entry.integration.onRevoke === \"silence\") entry.status = \"silenced\";\n }\n\n function load(entry: Entry): void {\n entry.loading = true;\n entry.status = \"loading\";\n // Normalise sync/async setup into a promise; a sync throw becomes a reject.\n Promise.resolve()\n .then(() => entry.integration.setup(ctxFor(entry)))\n .then((control) => {\n entry.loading = false;\n entry.control = control ?? undefined;\n entry.everLoaded = true;\n entry.status = \"active\";\n if (stopped) {\n // Torn down while loading → undo this load right away so the vendor\n // doesn't linger past teardown.\n teardownEntry(entry);\n return;\n }\n // Consent may have flipped while we were loading — reconcile once more.\n reconcile(entry);\n })\n .catch((err) => {\n entry.loading = false;\n entry.status = \"error\";\n releaseSubs(entry); // a failed setup may have subscribed before throwing\n warn(\n `integration \"${entry.integration.id}\" failed to load; will retry on the next change.`,\n err,\n );\n });\n }\n\n function reconcile(entry: Entry): void {\n const { integration } = entry;\n const granted = isGranted(integration, host);\n // Record a grant even while the script is still loading — otherwise a\n // grant-then-withdraw during the load window is lost, and a `remove` vendor\n // would never be removed afterward.\n if (granted) entry.wasGranted = true;\n if (stopped || entry.loading) return;\n\n // Not currently loaded (idle / removed / error) → decide whether to load.\n if (entry.status === \"idle\" || entry.status === \"removed\" || entry.status === \"error\") {\n const shouldLoad = entry.everLoaded ? granted : integration.load === \"immediately\" || granted;\n if (shouldLoad) load(entry);\n return;\n }\n\n // Loaded (active / silenced) → apply the revoke mode.\n if (integration.onRevoke === \"keep\") return;\n\n if (integration.onRevoke === \"remove\") {\n // Only a real withdrawal (granted → denied) removes. A never-granted\n // `immediately` load must not load-then-remove (a wasted request);\n // `silence`, by contrast, stays ungated so it can quiet a vendor before\n // the first consent.\n if (!granted && entry.status === \"active\" && entry.wasGranted) {\n releaseSubs(entry); // the removed script's listeners go with it\n try {\n (entry.control as Cleanup | undefined)?.();\n } catch (err) {\n warn(`integration \"${integration.id}\" cleanup threw on revoke.`, err);\n }\n entry.control = undefined;\n entry.status = \"removed\";\n }\n return;\n }\n\n // silence\n const sc = entry.control as SilenceControl | undefined;\n if (!granted && entry.status === \"active\") {\n try {\n sc?.silence();\n } catch (err) {\n warn(`integration \"${integration.id}\" silence() threw.`, err);\n }\n entry.status = \"silenced\";\n } else if (granted && entry.status === \"silenced\") {\n try {\n sc?.resume();\n } catch (err) {\n warn(`integration \"${integration.id}\" resume() threw.`, err);\n }\n entry.status = \"active\";\n }\n }\n\n function reconcileAll(): void {\n for (const entry of entries) reconcile(entry);\n }\n\n const unsubscribe = host.subscribe(reconcileAll);\n reconcileAll(); // initial pass\n\n return {\n status: () => {\n const out: Record<string, IntegrationStatus> = {};\n for (const entry of entries) out[entry.integration.id] = entry.status;\n return out;\n },\n list: () =>\n entries.map((entry) => ({\n id: entry.integration.id,\n category: entry.integration.category,\n load: entry.integration.load,\n onRevoke: entry.integration.onRevoke,\n status: entry.status,\n })),\n stop: () => {\n if (stopped) return;\n stopped = true;\n unsubscribe();\n for (const entry of entries) teardownEntry(entry);\n },\n };\n}\n"],"names":["INTEGRATION_FORMAT_VERSION","warnOverlappingVendors","integrationIds","builtInVendors","ids","Set","vendor","has","warn","warnUnknownCategories","integrations","knownCategoryIds","known","integration","load","raw","category","cats","Array","isArray","length","id","message","err","console","error","isGranted","host","match","some","c","granted","every","runIntegrations","entries","seenIds","stopped","legacy","setup","version","add","push","status","everLoaded","loading","control","subs","wasGranted","releaseSubs","entry","unsub","clear","teardownEntry","onRevoke","silence","undo","Promise","resolve","then","onConsentChange","fn","subscribe","delete","region","ctxFor","reconcile","catch","sc","resume","reconcileAll","unsubscribe","out","list","map","stop"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ "use strict";let e=null,o=null;function t(e){o=e}function n(e,o,t,n){let r;try{const e="undefined"!=typeof window?window.location.href:"http://localhost";r=new URL(o,e)}catch{return null}const i=r.hostname.toLowerCase(),l=r.pathname+r.search,c=t.toUpperCase();for(const o of e){const e=o.domain.toLowerCase();if((i===e||i.endsWith("."+e))&&((!o.pathIncludes||l.includes(o.pathIncludes))&&(!o.methods||o.methods.map(e=>e.toUpperCase()).includes(c))&&!n(o.category)))return o}return null}let r=null;function i(e,o){if("undefined"==typeof window)return()=>{};if(r)return()=>{};if(!e.rules.length)return()=>{};const i={originalFetch:window.fetch,originalXhrOpen:XMLHttpRequest.prototype.open,originalXhrSend:XMLHttpRequest.prototype.send,originalSendBeacon:"undefined"!=typeof navigator&&"function"==typeof navigator.sendBeacon?navigator.sendBeacon:void 0};r=i;const c=!1!==e.logBlockedRequests;function s(o){c&&console.warn(`[cookieyes] blocked ${o.method} ${o.url} (rule "${o.rule.id}", category: ${o.rule.category})`),e.onRequestBlocked?.(o)}if(window.fetch=function(t,r){let l="",c=r?.method??"GET";"string"==typeof t?l=t:t instanceof URL?l=t.toString():(l=t.url,c=r?.method??t.method);const u=n(e.rules,l,c,o);return u?(s({rule:u,url:l,method:c}),Promise.reject(new TypeError(`Blocked by consent (rule: ${u.id}, category: ${u.category})`))):i.originalFetch.call(window,t,r)},XMLHttpRequest.prototype.open=function(e,o,...t){return this._cyUrl=o.toString(),this._cyMethod=e,i.originalXhrOpen.apply(this,[e,o,...t])},XMLHttpRequest.prototype.send=function(t){const r=this._cyUrl??"",l=this._cyMethod??"GET",c=n(e.rules,r,l,o);return c?(s({rule:c,url:r,method:l}),void this.abort()):i.originalXhrSend.call(this,t)},i.originalSendBeacon){const t=i.originalSendBeacon;navigator.sendBeacon=function(r,i){const l=n(e.rules,r.toString(),"POST",o);return l?(s({rule:l,url:r.toString(),method:"POST"}),!0):t.call(navigator,r,i)}}return t(l),l}function l(){r&&("undefined"!=typeof window&&(window.fetch=r.originalFetch,XMLHttpRequest.prototype.open=r.originalXhrOpen,XMLHttpRequest.prototype.send=r.originalXhrSend,r.originalSendBeacon&&(navigator.sendBeacon=r.originalSendBeacon)),r=null,t(null))}exports._clearNetworkBlockerInstaller=function(){o?.(),o=null,e=null},exports._hasNetworkBlockerInstaller=function(){return null!==e},exports._installRegisteredNetworkBlocker=function(t,n){e?o=e(t,n):"undefined"!=typeof console&&console.error('[cookieyes] `networkBlocker` is configured but nothing is being blocked. Add:\n import { registerNetworkBlocker } from "@cookieyes/core/network-blocker";\n registerNetworkBlocker(); // before your setup call\nIt is a separate entry point so it is not downloaded by those who do not use it.')},exports._uninstallRegisteredNetworkBlocker=function(){const e=o;o=null,e?.()},exports.installNetworkBlocker=i,exports.registerNetworkBlocker=function(){e=i},exports.uninstallNetworkBlocker=l;
2
+ //# sourceMappingURL=network-blocker--Z2J__Xr.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"network-blocker--Z2J__Xr.cjs","sources":["../src/network-blocker-slot.ts","../src/network-blocker.ts"],"sourcesContent":["import type { NetworkBlockerConfig } from \"./network-blocker.js\";\nimport type { ConsentCategory } from \"./types.js\";\n\n/**\n * The seam that lets the network blocker ship only to customers who use it.\n *\n * ## Why this exists\n *\n * The blocker replaces `fetch`, `XMLHttpRequest.prototype.open`/`send` and\n * `navigator.sendBeacon` so that tracking requests are stopped before a visitor\n * has agreed to them. It is a real feature, and until now every customer\n * downloaded it whether they had configured it or not — because `runtime.ts`\n * imported it at the top of the file unconditionally and only *called* it if\n * `networkBlocker` was configured. A bundler can drop code nobody uses, but an\n * unconditional import counts as using it.\n *\n * ## Why not a dynamic import\n *\n * Deferring the blocker behind `import()` would save the same bytes and put a\n * hole in the feature. Its whole purpose is to have the browser's networking\n * already replaced when the page starts; between the page starting and the\n * chunk arriving, nothing is patched and an early-firing tracker gets through.\n * On a compliance product that is not a trade worth a kilobyte.\n *\n * ## What happens instead\n *\n * `@cookieyes/core/network-blocker` is its own entry point. A customer who\n * wants the feature imports it and calls `registerNetworkBlocker()`, which\n * fills the slot below with the real installer. Because that is an ordinary\n * static import, the blocker is present and installs **eagerly** during setup —\n * exactly as it does today, with no timing gap. A customer who does not import\n * it never downloads it, and this file is all that remains in the main chunk.\n *\n * The `networkBlocker` config field is unchanged. What changed is that\n * configuring it is no longer sufficient on its own, so a setup that configures\n * it without registering fails loudly rather than silently not blocking —\n * see `_installRegisteredNetworkBlocker`.\n */\n\ntype Installer = (\n config: NetworkBlockerConfig,\n hasConsent: (category: ConsentCategory) => boolean,\n) => () => void;\n\nlet installer: Installer | null = null;\nlet activeUninstall: (() => void) | null = null;\n\n/**\n * Fill the slot. Called by `registerNetworkBlocker()` in the\n * `@cookieyes/core/network-blocker` entry point — not something to call\n * directly.\n *\n * @internal\n */\nexport function _setNetworkBlockerInstaller(fn: Installer): void {\n installer = fn;\n}\n\n/**\n * Record the teardown for a blocker that is currently installed.\n *\n * `installNetworkBlocker` calls this itself, so `resetConsentRuntime()` un-patches\n * the transports whether the blocker was installed through the runtime's config\n * or by a direct call. That matters beyond tidiness: the blocker is a\n * module-level singleton and a second install is a silent no-op while one is\n * active, so a reset that left it installed would leave the *next* setup running\n * on the old rules and the old consent closure while appearing to accept new\n * ones — wrong behaviour with nothing in the console.\n *\n * @internal\n */\nexport function _setActiveNetworkBlockerUninstall(fn: (() => void) | null): void {\n activeUninstall = fn;\n}\n\n/** @internal Test-only — empties the slot so one test cannot leak into the next. */\nexport function _clearNetworkBlockerInstaller(): void {\n activeUninstall?.();\n activeUninstall = null;\n installer = null;\n}\n\n/** True when a customer has registered the blocker. @internal */\nexport function _hasNetworkBlockerInstaller(): boolean {\n return installer !== null;\n}\n\n/**\n * Install the registered blocker, or complain loudly that there isn't one.\n *\n * The failure mode this guards against is the dangerous one: a customer who\n * configured `networkBlocker` before this change and upgrades would otherwise\n * find their requests silently no longer blocked, which on a consent product is\n * worse than a crash. `console.error` rather than `throw` because taking the\n * page down is not proportionate, and the message names the exact two lines\n * needed to fix it.\n *\n * @internal — consumed by core's runtime and by framework adapters.\n */\nexport function _installRegisteredNetworkBlocker(\n config: NetworkBlockerConfig,\n hasConsent: (category: ConsentCategory) => boolean,\n): void {\n if (!installer) {\n if (typeof console !== \"undefined\") {\n // Kept terse on purpose: this string sits in every consumer's bundle,\n // including those that never configure the blocker. It has to be\n // unmissable and name the fix, and nothing more.\n console.error(\n \"[cookieyes] `networkBlocker` is configured but nothing is being blocked. Add:\\n\" +\n ' import { registerNetworkBlocker } from \"@cookieyes/core/network-blocker\";\\n' +\n \" registerNetworkBlocker(); // before your setup call\\n\" +\n \"It is a separate entry point so it is not downloaded by those who do not use it.\",\n );\n }\n return;\n }\n activeUninstall = installer(config, hasConsent);\n}\n\n/**\n * Restore the browser's networking functions, if the blocker was installed.\n *\n * Idempotent, and a no-op when nothing was installed — which is why the runtime\n * can call it unconditionally on reset without dragging the blocker back into\n * the bundle.\n *\n * @internal\n */\nexport function _uninstallRegisteredNetworkBlocker(): void {\n // Cleared before the call, not after: the teardown itself clears this slot\n // (see `_setActiveNetworkBlockerUninstall`), and reading a stale reference\n // afterwards would undo that.\n const teardown = activeUninstall;\n activeUninstall = null;\n teardown?.();\n}\n","import {\n _setActiveNetworkBlockerUninstall,\n _setNetworkBlockerInstaller,\n} from \"./network-blocker-slot.js\";\nimport type { ConsentCategory } from \"./types.js\";\n\nexport type NetworkBlockerRule = {\n id: string;\n domain: string;\n pathIncludes?: string | undefined;\n methods?: string[] | undefined;\n category: ConsentCategory;\n};\n\nexport type BlockedRequestInfo = {\n rule: NetworkBlockerRule;\n url: string;\n method: string;\n};\n\nexport type NetworkBlockerConfig = {\n rules: NetworkBlockerRule[];\n onRequestBlocked?: ((info: BlockedRequestInfo) => void) | undefined;\n logBlockedRequests?: boolean | undefined;\n};\n\ntype ConsentChecker = (category: ConsentCategory) => boolean;\n\nfunction findBlockingRule(\n rules: NetworkBlockerRule[],\n url: string,\n method: string,\n hasConsent: ConsentChecker,\n): NetworkBlockerRule | null {\n let parsed: URL;\n try {\n const base = typeof window !== \"undefined\" ? window.location.href : \"http://localhost\";\n parsed = new URL(url, base);\n } catch {\n return null;\n }\n const host = parsed.hostname.toLowerCase();\n const path = parsed.pathname + parsed.search;\n const upperMethod = method.toUpperCase();\n\n for (const rule of rules) {\n const ruleHost = rule.domain.toLowerCase();\n if (host !== ruleHost && !host.endsWith(\".\" + ruleHost)) continue;\n if (rule.pathIncludes && !path.includes(rule.pathIncludes)) continue;\n if (rule.methods && !rule.methods.map((m) => m.toUpperCase()).includes(upperMethod)) {\n continue;\n }\n if (hasConsent(rule.category)) continue;\n return rule;\n }\n return null;\n}\n\ntype InstallationState = {\n originalFetch: typeof fetch;\n originalXhrOpen: typeof XMLHttpRequest.prototype.open;\n originalXhrSend: typeof XMLHttpRequest.prototype.send;\n originalSendBeacon: typeof navigator.sendBeacon | undefined;\n};\n\nlet active: InstallationState | null = null;\n\nexport function installNetworkBlocker(\n config: NetworkBlockerConfig,\n hasConsent: ConsentChecker,\n): () => void {\n if (typeof window === \"undefined\") return () => undefined;\n if (active) return () => undefined;\n if (!config.rules.length) return () => undefined;\n\n const state: InstallationState = {\n originalFetch: window.fetch,\n originalXhrOpen: XMLHttpRequest.prototype.open,\n originalXhrSend: XMLHttpRequest.prototype.send,\n // `navigator.sendBeacon` is how GA4, Meta, and others fire exit/unload\n // tracking — fetch/XHR interception alone misses it, so patch it too.\n // Store the raw reference (not bound) so uninstall restores it exactly.\n originalSendBeacon:\n typeof navigator !== \"undefined\" && typeof navigator.sendBeacon === \"function\"\n ? navigator.sendBeacon\n : undefined,\n };\n active = state;\n\n const logBlocked = config.logBlockedRequests !== false;\n function notify(info: BlockedRequestInfo): void {\n if (logBlocked) {\n // eslint-disable-next-line no-console\n console.warn(\n `[cookieyes] blocked ${info.method} ${info.url} (rule \"${info.rule.id}\", category: ${info.rule.category})`,\n );\n }\n config.onRequestBlocked?.(info);\n }\n\n window.fetch = function patchedFetch(\n input: RequestInfo | URL,\n init?: RequestInit,\n ): Promise<Response> {\n let url = \"\";\n let method = init?.method ?? \"GET\";\n if (typeof input === \"string\") {\n url = input;\n } else if (input instanceof URL) {\n url = input.toString();\n } else {\n url = (input as Request).url;\n method = init?.method ?? (input as Request).method;\n }\n const blockingRule = findBlockingRule(config.rules, url, method, hasConsent);\n if (blockingRule) {\n notify({ rule: blockingRule, url, method });\n return Promise.reject(\n new TypeError(\n `Blocked by consent (rule: ${blockingRule.id}, category: ${blockingRule.category})`,\n ),\n );\n }\n return state.originalFetch.call(window, input, init);\n };\n\n type XHRMeta = { _cyUrl?: string; _cyMethod?: string };\n XMLHttpRequest.prototype.open = function patchedOpen(\n this: XMLHttpRequest & XHRMeta,\n method: string,\n url: string | URL,\n ...rest: unknown[]\n ) {\n this._cyUrl = url.toString();\n this._cyMethod = method;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return state.originalXhrOpen.apply(this, [method, url, ...rest] as any);\n };\n\n XMLHttpRequest.prototype.send = function patchedSend(\n this: XMLHttpRequest & XHRMeta,\n body?: Document | XMLHttpRequestBodyInit | null,\n ) {\n const url = this._cyUrl ?? \"\";\n const method = this._cyMethod ?? \"GET\";\n const blockingRule = findBlockingRule(config.rules, url, method, hasConsent);\n if (blockingRule) {\n notify({ rule: blockingRule, url, method });\n this.abort();\n return;\n }\n return state.originalXhrSend.call(this, body);\n };\n\n if (state.originalSendBeacon) {\n const originalSendBeacon = state.originalSendBeacon;\n navigator.sendBeacon = function patchedSendBeacon(\n url: string | URL,\n data?: BodyInit | null,\n ): boolean {\n // sendBeacon is always a POST.\n const blockingRule = findBlockingRule(config.rules, url.toString(), \"POST\", hasConsent);\n if (blockingRule) {\n notify({ rule: blockingRule, url: url.toString(), method: \"POST\" });\n // Returning true tells the caller the beacon was \"queued\" — nothing is\n // actually sent. Returning false makes some libraries retry via other\n // transports, which we'd then also block; true is the quieter stop.\n return true;\n }\n return originalSendBeacon.call(navigator, url, data);\n };\n }\n\n // Recorded so `resetConsentRuntime()` un-patches the transports even when the\n // blocker was installed by a direct call rather than through config.\n _setActiveNetworkBlockerUninstall(uninstallNetworkBlocker);\n return uninstallNetworkBlocker;\n}\n\nexport function uninstallNetworkBlocker(): void {\n if (!active) return;\n if (typeof window !== \"undefined\") {\n window.fetch = active.originalFetch;\n XMLHttpRequest.prototype.open = active.originalXhrOpen;\n XMLHttpRequest.prototype.send = active.originalXhrSend;\n if (active.originalSendBeacon) {\n navigator.sendBeacon = active.originalSendBeacon;\n }\n }\n active = null;\n _setActiveNetworkBlockerUninstall(null);\n}\n\n/**\n * Register the network blocker so that a configured `networkBlocker` actually\n * installs. Call it once, before your setup call:\n *\n * ```ts\n * import { initCookieYes } from \"@cookieyes/core\";\n * import { registerNetworkBlocker } from \"@cookieyes/core/network-blocker\";\n *\n * registerNetworkBlocker();\n * initCookieYes({ mode: \"cookie-only\", networkBlocker: { rules: [...] } });\n * ```\n *\n * This indirection is what keeps the blocker out of the download for everyone\n * who does not use it. Because it is reached by an ordinary static import, the\n * blocker is already loaded when setup runs and patches the browser's\n * networking immediately — there is no window in which requests slip through,\n * which is the flaw a dynamic import would have introduced. See\n * `network-blocker-slot.ts` for the full reasoning.\n *\n * Idempotent: calling it more than once replaces the registration with the same\n * installer and changes nothing.\n */\nexport function registerNetworkBlocker(): void {\n _setNetworkBlockerInstaller(installNetworkBlocker);\n}\n"],"names":["installer","activeUninstall","_setActiveNetworkBlockerUninstall","fn","findBlockingRule","rules","url","method","hasConsent","parsed","base","window","location","href","URL","host","hostname","toLowerCase","path","pathname","search","upperMethod","toUpperCase","rule","ruleHost","domain","endsWith","pathIncludes","includes","methods","map","m","category","active","installNetworkBlocker","config","length","state","originalFetch","fetch","originalXhrOpen","XMLHttpRequest","prototype","open","originalXhrSend","send","originalSendBeacon","navigator","sendBeacon","logBlocked","logBlockedRequests","notify","info","console","warn","id","onRequestBlocked","input","init","toString","blockingRule","Promise","reject","TypeError","call","rest","this","_cyUrl","_cyMethod","apply","body","abort","data","uninstallNetworkBlocker","error","teardown"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ let e=null,n=null;function o(e){n=e}function t(){n?.(),n=null,e=null}function r(){return null!==e}function i(o,t){e?n=e(o,t):"undefined"!=typeof console&&console.error('[cookieyes] `networkBlocker` is configured but nothing is being blocked. Add:\n import { registerNetworkBlocker } from "@cookieyes/core/network-blocker";\n registerNetworkBlocker(); // before your setup call\nIt is a separate entry point so it is not downloaded by those who do not use it.')}function l(){const e=n;n=null,e?.()}function c(e,n,o,t){let r;try{const e="undefined"!=typeof window?window.location.href:"http://localhost";r=new URL(n,e)}catch{return null}const i=r.hostname.toLowerCase(),l=r.pathname+r.search,c=o.toUpperCase();for(const n of e){const e=n.domain.toLowerCase();if((i===e||i.endsWith("."+e))&&((!n.pathIncludes||l.includes(n.pathIncludes))&&(!n.methods||n.methods.map(e=>e.toUpperCase()).includes(c))&&!t(n.category)))return n}return null}let s=null;function u(e,n){if("undefined"==typeof window)return()=>{};if(s)return()=>{};if(!e.rules.length)return()=>{};const t={originalFetch:window.fetch,originalXhrOpen:XMLHttpRequest.prototype.open,originalXhrSend:XMLHttpRequest.prototype.send,originalSendBeacon:"undefined"!=typeof navigator&&"function"==typeof navigator.sendBeacon?navigator.sendBeacon:void 0};s=t;const r=!1!==e.logBlockedRequests;function i(n){r&&console.warn(`[cookieyes] blocked ${n.method} ${n.url} (rule "${n.rule.id}", category: ${n.rule.category})`),e.onRequestBlocked?.(n)}if(window.fetch=function(o,r){let l="",s=r?.method??"GET";"string"==typeof o?l=o:o instanceof URL?l=o.toString():(l=o.url,s=r?.method??o.method);const u=c(e.rules,l,s,n);return u?(i({rule:u,url:l,method:s}),Promise.reject(new TypeError(`Blocked by consent (rule: ${u.id}, category: ${u.category})`))):t.originalFetch.call(window,o,r)},XMLHttpRequest.prototype.open=function(e,n,...o){return this._cyUrl=n.toString(),this._cyMethod=e,t.originalXhrOpen.apply(this,[e,n,...o])},XMLHttpRequest.prototype.send=function(o){const r=this._cyUrl??"",l=this._cyMethod??"GET",s=c(e.rules,r,l,n);return s?(i({rule:s,url:r,method:l}),void this.abort()):t.originalXhrSend.call(this,o)},t.originalSendBeacon){const o=t.originalSendBeacon;navigator.sendBeacon=function(t,r){const l=c(e.rules,t.toString(),"POST",n);return l?(i({rule:l,url:t.toString(),method:"POST"}),!0):o.call(navigator,t,r)}}return o(a),a}function a(){s&&("undefined"!=typeof window&&(window.fetch=s.originalFetch,XMLHttpRequest.prototype.open=s.originalXhrOpen,XMLHttpRequest.prototype.send=s.originalXhrSend,s.originalSendBeacon&&(navigator.sendBeacon=s.originalSendBeacon)),s=null,o(null))}function d(){e=u}export{i as _,l as a,t as b,r as c,u as i,d as r,a as u};
2
+ //# sourceMappingURL=network-blocker-zdjGiuQN.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"network-blocker-zdjGiuQN.js","sources":["../src/network-blocker-slot.ts","../src/network-blocker.ts"],"sourcesContent":["import type { NetworkBlockerConfig } from \"./network-blocker.js\";\nimport type { ConsentCategory } from \"./types.js\";\n\n/**\n * The seam that lets the network blocker ship only to customers who use it.\n *\n * ## Why this exists\n *\n * The blocker replaces `fetch`, `XMLHttpRequest.prototype.open`/`send` and\n * `navigator.sendBeacon` so that tracking requests are stopped before a visitor\n * has agreed to them. It is a real feature, and until now every customer\n * downloaded it whether they had configured it or not — because `runtime.ts`\n * imported it at the top of the file unconditionally and only *called* it if\n * `networkBlocker` was configured. A bundler can drop code nobody uses, but an\n * unconditional import counts as using it.\n *\n * ## Why not a dynamic import\n *\n * Deferring the blocker behind `import()` would save the same bytes and put a\n * hole in the feature. Its whole purpose is to have the browser's networking\n * already replaced when the page starts; between the page starting and the\n * chunk arriving, nothing is patched and an early-firing tracker gets through.\n * On a compliance product that is not a trade worth a kilobyte.\n *\n * ## What happens instead\n *\n * `@cookieyes/core/network-blocker` is its own entry point. A customer who\n * wants the feature imports it and calls `registerNetworkBlocker()`, which\n * fills the slot below with the real installer. Because that is an ordinary\n * static import, the blocker is present and installs **eagerly** during setup —\n * exactly as it does today, with no timing gap. A customer who does not import\n * it never downloads it, and this file is all that remains in the main chunk.\n *\n * The `networkBlocker` config field is unchanged. What changed is that\n * configuring it is no longer sufficient on its own, so a setup that configures\n * it without registering fails loudly rather than silently not blocking —\n * see `_installRegisteredNetworkBlocker`.\n */\n\ntype Installer = (\n config: NetworkBlockerConfig,\n hasConsent: (category: ConsentCategory) => boolean,\n) => () => void;\n\nlet installer: Installer | null = null;\nlet activeUninstall: (() => void) | null = null;\n\n/**\n * Fill the slot. Called by `registerNetworkBlocker()` in the\n * `@cookieyes/core/network-blocker` entry point — not something to call\n * directly.\n *\n * @internal\n */\nexport function _setNetworkBlockerInstaller(fn: Installer): void {\n installer = fn;\n}\n\n/**\n * Record the teardown for a blocker that is currently installed.\n *\n * `installNetworkBlocker` calls this itself, so `resetConsentRuntime()` un-patches\n * the transports whether the blocker was installed through the runtime's config\n * or by a direct call. That matters beyond tidiness: the blocker is a\n * module-level singleton and a second install is a silent no-op while one is\n * active, so a reset that left it installed would leave the *next* setup running\n * on the old rules and the old consent closure while appearing to accept new\n * ones — wrong behaviour with nothing in the console.\n *\n * @internal\n */\nexport function _setActiveNetworkBlockerUninstall(fn: (() => void) | null): void {\n activeUninstall = fn;\n}\n\n/** @internal Test-only — empties the slot so one test cannot leak into the next. */\nexport function _clearNetworkBlockerInstaller(): void {\n activeUninstall?.();\n activeUninstall = null;\n installer = null;\n}\n\n/** True when a customer has registered the blocker. @internal */\nexport function _hasNetworkBlockerInstaller(): boolean {\n return installer !== null;\n}\n\n/**\n * Install the registered blocker, or complain loudly that there isn't one.\n *\n * The failure mode this guards against is the dangerous one: a customer who\n * configured `networkBlocker` before this change and upgrades would otherwise\n * find their requests silently no longer blocked, which on a consent product is\n * worse than a crash. `console.error` rather than `throw` because taking the\n * page down is not proportionate, and the message names the exact two lines\n * needed to fix it.\n *\n * @internal — consumed by core's runtime and by framework adapters.\n */\nexport function _installRegisteredNetworkBlocker(\n config: NetworkBlockerConfig,\n hasConsent: (category: ConsentCategory) => boolean,\n): void {\n if (!installer) {\n if (typeof console !== \"undefined\") {\n // Kept terse on purpose: this string sits in every consumer's bundle,\n // including those that never configure the blocker. It has to be\n // unmissable and name the fix, and nothing more.\n console.error(\n \"[cookieyes] `networkBlocker` is configured but nothing is being blocked. Add:\\n\" +\n ' import { registerNetworkBlocker } from \"@cookieyes/core/network-blocker\";\\n' +\n \" registerNetworkBlocker(); // before your setup call\\n\" +\n \"It is a separate entry point so it is not downloaded by those who do not use it.\",\n );\n }\n return;\n }\n activeUninstall = installer(config, hasConsent);\n}\n\n/**\n * Restore the browser's networking functions, if the blocker was installed.\n *\n * Idempotent, and a no-op when nothing was installed — which is why the runtime\n * can call it unconditionally on reset without dragging the blocker back into\n * the bundle.\n *\n * @internal\n */\nexport function _uninstallRegisteredNetworkBlocker(): void {\n // Cleared before the call, not after: the teardown itself clears this slot\n // (see `_setActiveNetworkBlockerUninstall`), and reading a stale reference\n // afterwards would undo that.\n const teardown = activeUninstall;\n activeUninstall = null;\n teardown?.();\n}\n","import {\n _setActiveNetworkBlockerUninstall,\n _setNetworkBlockerInstaller,\n} from \"./network-blocker-slot.js\";\nimport type { ConsentCategory } from \"./types.js\";\n\nexport type NetworkBlockerRule = {\n id: string;\n domain: string;\n pathIncludes?: string | undefined;\n methods?: string[] | undefined;\n category: ConsentCategory;\n};\n\nexport type BlockedRequestInfo = {\n rule: NetworkBlockerRule;\n url: string;\n method: string;\n};\n\nexport type NetworkBlockerConfig = {\n rules: NetworkBlockerRule[];\n onRequestBlocked?: ((info: BlockedRequestInfo) => void) | undefined;\n logBlockedRequests?: boolean | undefined;\n};\n\ntype ConsentChecker = (category: ConsentCategory) => boolean;\n\nfunction findBlockingRule(\n rules: NetworkBlockerRule[],\n url: string,\n method: string,\n hasConsent: ConsentChecker,\n): NetworkBlockerRule | null {\n let parsed: URL;\n try {\n const base = typeof window !== \"undefined\" ? window.location.href : \"http://localhost\";\n parsed = new URL(url, base);\n } catch {\n return null;\n }\n const host = parsed.hostname.toLowerCase();\n const path = parsed.pathname + parsed.search;\n const upperMethod = method.toUpperCase();\n\n for (const rule of rules) {\n const ruleHost = rule.domain.toLowerCase();\n if (host !== ruleHost && !host.endsWith(\".\" + ruleHost)) continue;\n if (rule.pathIncludes && !path.includes(rule.pathIncludes)) continue;\n if (rule.methods && !rule.methods.map((m) => m.toUpperCase()).includes(upperMethod)) {\n continue;\n }\n if (hasConsent(rule.category)) continue;\n return rule;\n }\n return null;\n}\n\ntype InstallationState = {\n originalFetch: typeof fetch;\n originalXhrOpen: typeof XMLHttpRequest.prototype.open;\n originalXhrSend: typeof XMLHttpRequest.prototype.send;\n originalSendBeacon: typeof navigator.sendBeacon | undefined;\n};\n\nlet active: InstallationState | null = null;\n\nexport function installNetworkBlocker(\n config: NetworkBlockerConfig,\n hasConsent: ConsentChecker,\n): () => void {\n if (typeof window === \"undefined\") return () => undefined;\n if (active) return () => undefined;\n if (!config.rules.length) return () => undefined;\n\n const state: InstallationState = {\n originalFetch: window.fetch,\n originalXhrOpen: XMLHttpRequest.prototype.open,\n originalXhrSend: XMLHttpRequest.prototype.send,\n // `navigator.sendBeacon` is how GA4, Meta, and others fire exit/unload\n // tracking — fetch/XHR interception alone misses it, so patch it too.\n // Store the raw reference (not bound) so uninstall restores it exactly.\n originalSendBeacon:\n typeof navigator !== \"undefined\" && typeof navigator.sendBeacon === \"function\"\n ? navigator.sendBeacon\n : undefined,\n };\n active = state;\n\n const logBlocked = config.logBlockedRequests !== false;\n function notify(info: BlockedRequestInfo): void {\n if (logBlocked) {\n // eslint-disable-next-line no-console\n console.warn(\n `[cookieyes] blocked ${info.method} ${info.url} (rule \"${info.rule.id}\", category: ${info.rule.category})`,\n );\n }\n config.onRequestBlocked?.(info);\n }\n\n window.fetch = function patchedFetch(\n input: RequestInfo | URL,\n init?: RequestInit,\n ): Promise<Response> {\n let url = \"\";\n let method = init?.method ?? \"GET\";\n if (typeof input === \"string\") {\n url = input;\n } else if (input instanceof URL) {\n url = input.toString();\n } else {\n url = (input as Request).url;\n method = init?.method ?? (input as Request).method;\n }\n const blockingRule = findBlockingRule(config.rules, url, method, hasConsent);\n if (blockingRule) {\n notify({ rule: blockingRule, url, method });\n return Promise.reject(\n new TypeError(\n `Blocked by consent (rule: ${blockingRule.id}, category: ${blockingRule.category})`,\n ),\n );\n }\n return state.originalFetch.call(window, input, init);\n };\n\n type XHRMeta = { _cyUrl?: string; _cyMethod?: string };\n XMLHttpRequest.prototype.open = function patchedOpen(\n this: XMLHttpRequest & XHRMeta,\n method: string,\n url: string | URL,\n ...rest: unknown[]\n ) {\n this._cyUrl = url.toString();\n this._cyMethod = method;\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n return state.originalXhrOpen.apply(this, [method, url, ...rest] as any);\n };\n\n XMLHttpRequest.prototype.send = function patchedSend(\n this: XMLHttpRequest & XHRMeta,\n body?: Document | XMLHttpRequestBodyInit | null,\n ) {\n const url = this._cyUrl ?? \"\";\n const method = this._cyMethod ?? \"GET\";\n const blockingRule = findBlockingRule(config.rules, url, method, hasConsent);\n if (blockingRule) {\n notify({ rule: blockingRule, url, method });\n this.abort();\n return;\n }\n return state.originalXhrSend.call(this, body);\n };\n\n if (state.originalSendBeacon) {\n const originalSendBeacon = state.originalSendBeacon;\n navigator.sendBeacon = function patchedSendBeacon(\n url: string | URL,\n data?: BodyInit | null,\n ): boolean {\n // sendBeacon is always a POST.\n const blockingRule = findBlockingRule(config.rules, url.toString(), \"POST\", hasConsent);\n if (blockingRule) {\n notify({ rule: blockingRule, url: url.toString(), method: \"POST\" });\n // Returning true tells the caller the beacon was \"queued\" — nothing is\n // actually sent. Returning false makes some libraries retry via other\n // transports, which we'd then also block; true is the quieter stop.\n return true;\n }\n return originalSendBeacon.call(navigator, url, data);\n };\n }\n\n // Recorded so `resetConsentRuntime()` un-patches the transports even when the\n // blocker was installed by a direct call rather than through config.\n _setActiveNetworkBlockerUninstall(uninstallNetworkBlocker);\n return uninstallNetworkBlocker;\n}\n\nexport function uninstallNetworkBlocker(): void {\n if (!active) return;\n if (typeof window !== \"undefined\") {\n window.fetch = active.originalFetch;\n XMLHttpRequest.prototype.open = active.originalXhrOpen;\n XMLHttpRequest.prototype.send = active.originalXhrSend;\n if (active.originalSendBeacon) {\n navigator.sendBeacon = active.originalSendBeacon;\n }\n }\n active = null;\n _setActiveNetworkBlockerUninstall(null);\n}\n\n/**\n * Register the network blocker so that a configured `networkBlocker` actually\n * installs. Call it once, before your setup call:\n *\n * ```ts\n * import { initCookieYes } from \"@cookieyes/core\";\n * import { registerNetworkBlocker } from \"@cookieyes/core/network-blocker\";\n *\n * registerNetworkBlocker();\n * initCookieYes({ mode: \"cookie-only\", networkBlocker: { rules: [...] } });\n * ```\n *\n * This indirection is what keeps the blocker out of the download for everyone\n * who does not use it. Because it is reached by an ordinary static import, the\n * blocker is already loaded when setup runs and patches the browser's\n * networking immediately — there is no window in which requests slip through,\n * which is the flaw a dynamic import would have introduced. See\n * `network-blocker-slot.ts` for the full reasoning.\n *\n * Idempotent: calling it more than once replaces the registration with the same\n * installer and changes nothing.\n */\nexport function registerNetworkBlocker(): void {\n _setNetworkBlockerInstaller(installNetworkBlocker);\n}\n"],"names":["installer","activeUninstall","_setActiveNetworkBlockerUninstall","fn","_clearNetworkBlockerInstaller","_hasNetworkBlockerInstaller","_installRegisteredNetworkBlocker","config","hasConsent","console","error","_uninstallRegisteredNetworkBlocker","teardown","findBlockingRule","rules","url","method","parsed","base","window","location","href","URL","host","hostname","toLowerCase","path","pathname","search","upperMethod","toUpperCase","rule","ruleHost","domain","endsWith","pathIncludes","includes","methods","map","m","category","active","installNetworkBlocker","length","state","originalFetch","fetch","originalXhrOpen","XMLHttpRequest","prototype","open","originalXhrSend","send","originalSendBeacon","navigator","sendBeacon","logBlocked","logBlockedRequests","notify","info","warn","id","onRequestBlocked","input","init","toString","blockingRule","Promise","reject","TypeError","call","rest","this","_cyUrl","_cyMethod","apply","body","abort","data","uninstallNetworkBlocker","registerNetworkBlocker"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ "use strict";var r=require("./network-blocker--Z2J__Xr.cjs");exports.installNetworkBlocker=r.installNetworkBlocker,exports.registerNetworkBlocker=r.registerNetworkBlocker,exports.uninstallNetworkBlocker=r.uninstallNetworkBlocker;
2
+ //# sourceMappingURL=network-blocker.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"network-blocker.cjs","sources":[],"sourcesContent":[],"names":[],"mappings":""}
@@ -0,0 +1 @@
1
+ export { w as BlockedRequestInfo, N as NetworkBlockerConfig, V as NetworkBlockerRule, a2 as installNetworkBlocker, a3 as registerNetworkBlocker, a8 as uninstallNetworkBlocker } from './categories-DMBKo4Eb.js';
@@ -0,0 +1,2 @@
1
+ export{i as installNetworkBlocker,r as registerNetworkBlocker,u as uninstallNetworkBlocker}from"./network-blocker-zdjGiuQN.js";
2
+ //# sourceMappingURL=network-blocker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"network-blocker.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cookieyes/core",
3
- "version": "0.5.0",
3
+ "version": "0.7.0",
4
4
  "description": "Headless consent engine — the single source of truth for all CookieYes consent logic",
5
5
  "keywords": [
6
6
  "cookie consent",
@@ -33,6 +33,11 @@
33
33
  "types": "./dist/index.d.ts",
34
34
  "import": "./dist/index.js",
35
35
  "require": "./dist/index.cjs"
36
+ },
37
+ "./network-blocker": {
38
+ "types": "./dist/network-blocker.d.ts",
39
+ "import": "./dist/network-blocker.js",
40
+ "require": "./dist/network-blocker.cjs"
36
41
  }
37
42
  },
38
43
  "main": "./dist/index.cjs",