@wix/web5-core 1.63.15 → 1.63.17
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/dist/cjs/component/componentDefinitions/TextBlockSectionDefinition.js +36 -32
- package/dist/cjs/component/componentDefinitions/TextBlockSectionDefinition.js.map +1 -1
- package/dist/cjs/component/componentDefinitions/index.js +5 -4
- package/dist/cjs/component/componentDefinitions/index.js.map +1 -1
- package/dist/cjs/component/componentDefinitions/parse-utils.js +43 -6
- package/dist/cjs/component/componentDefinitions/parse-utils.js.map +1 -1
- package/dist/cjs/index.js +32 -3
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/match/astToParts.js +14 -0
- package/dist/cjs/match/astToParts.js.map +1 -1
- package/dist/cjs/privacy/consentGate.js +275 -0
- package/dist/cjs/privacy/consentGate.js.map +1 -0
- package/dist/cjs/privacy/consentOverride.js +94 -0
- package/dist/cjs/privacy/consentOverride.js.map +1 -0
- package/dist/cjs/privacy/detectProvider.js +71 -0
- package/dist/cjs/privacy/detectProvider.js.map +1 -0
- package/dist/cjs/privacy/index.js +40 -0
- package/dist/cjs/privacy/index.js.map +1 -0
- package/dist/cjs/privacy/providers/hostSuppliedProvider.js +89 -0
- package/dist/cjs/privacy/providers/hostSuppliedProvider.js.map +1 -0
- package/dist/cjs/privacy/providers/oneTrustProvider.js +72 -0
- package/dist/cjs/privacy/providers/oneTrustProvider.js.map +1 -0
- package/dist/cjs/privacy/providers/shopifyProvider.js +177 -0
- package/dist/cjs/privacy/providers/shopifyProvider.js.map +1 -0
- package/dist/cjs/privacy/types.js +34 -0
- package/dist/cjs/privacy/types.js.map +1 -0
- package/dist/cjs/utils/analyticsEvents.js +30 -25
- package/dist/cjs/utils/analyticsEvents.js.map +1 -1
- package/dist/esm/component/componentDefinitions/TextBlockSectionDefinition.js +37 -33
- package/dist/esm/component/componentDefinitions/TextBlockSectionDefinition.js.map +1 -1
- package/dist/esm/component/componentDefinitions/index.js +5 -4
- package/dist/esm/component/componentDefinitions/index.js.map +1 -1
- package/dist/esm/component/componentDefinitions/parse-utils.js +42 -6
- package/dist/esm/component/componentDefinitions/parse-utils.js.map +1 -1
- package/dist/esm/index.js +3 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/match/astToParts.js +14 -0
- package/dist/esm/match/astToParts.js.map +1 -1
- package/dist/esm/privacy/consentGate.js +265 -0
- package/dist/esm/privacy/consentGate.js.map +1 -0
- package/dist/esm/privacy/consentOverride.js +87 -0
- package/dist/esm/privacy/consentOverride.js.map +1 -0
- package/dist/esm/privacy/detectProvider.js +65 -0
- package/dist/esm/privacy/detectProvider.js.map +1 -0
- package/dist/esm/privacy/index.js +8 -0
- package/dist/esm/privacy/index.js.map +1 -0
- package/dist/esm/privacy/providers/hostSuppliedProvider.js +81 -0
- package/dist/esm/privacy/providers/hostSuppliedProvider.js.map +1 -0
- package/dist/esm/privacy/providers/oneTrustProvider.js +66 -0
- package/dist/esm/privacy/providers/oneTrustProvider.js.map +1 -0
- package/dist/esm/privacy/providers/shopifyProvider.js +174 -0
- package/dist/esm/privacy/providers/shopifyProvider.js.map +1 -0
- package/dist/esm/privacy/types.js +30 -0
- package/dist/esm/privacy/types.js.map +1 -0
- package/dist/esm/utils/analyticsEvents.js +30 -25
- package/dist/esm/utils/analyticsEvents.js.map +1 -1
- package/dist/types/component/componentDefinitions/TextBlockSectionDefinition.d.ts.map +1 -1
- package/dist/types/component/componentDefinitions/index.d.ts.map +1 -1
- package/dist/types/component/componentDefinitions/parse-utils.d.ts +21 -0
- package/dist/types/component/componentDefinitions/parse-utils.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/match/astToParts.d.ts.map +1 -1
- package/dist/types/privacy/consentGate.d.ts +85 -0
- package/dist/types/privacy/consentGate.d.ts.map +1 -0
- package/dist/types/privacy/consentOverride.d.ts +33 -0
- package/dist/types/privacy/consentOverride.d.ts.map +1 -0
- package/dist/types/privacy/detectProvider.d.ts +25 -0
- package/dist/types/privacy/detectProvider.d.ts.map +1 -0
- package/dist/types/privacy/index.d.ts +8 -0
- package/dist/types/privacy/index.d.ts.map +1 -0
- package/dist/types/privacy/providers/hostSuppliedProvider.d.ts +29 -0
- package/dist/types/privacy/providers/hostSuppliedProvider.d.ts.map +1 -0
- package/dist/types/privacy/providers/oneTrustProvider.d.ts +15 -0
- package/dist/types/privacy/providers/oneTrustProvider.d.ts.map +1 -0
- package/dist/types/privacy/providers/shopifyProvider.d.ts +26 -0
- package/dist/types/privacy/providers/shopifyProvider.d.ts.map +1 -0
- package/dist/types/privacy/types.d.ts +32 -0
- package/dist/types/privacy/types.d.ts.map +1 -0
- package/dist/types/utils/analyticsEvents.d.ts +9 -0
- package/dist/types/utils/analyticsEvents.d.ts.map +1 -1
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["convertToBlockElements","extractContent","extractContentMarkdown","deriveRole","parseCalloutTag","parseWeb5Url","Web5UrlType","blockElementToPartType","element","startsWith","findNode","node","type","Array","isArray","children","child","found","extractTableCellLabel","parsed","url","ICON","alt","value","map","join","tableCellMeta","cell","label","trim","link","href","undefined","image","imageUrl","iconParsed","ENTITY","entityType","entityId","iconHint","path","resolveSource","block","astNode","cursors","n","content","md","markdown","cursor","get","astNodeIndex","target","i","length","set","text","buildListItems","listNode","item","itemParts","nodesToParts","isComplex","part","role","meta","parts","buildMeta","sourceNode","level","depth","lm","linkMetadata","title","isEntity","img","src","iconImg","iconUrl","web5Match","match","items","ordered","rows","headerRow","headerCells","columnLabels","featureColumnLabel","dataRows","slice","row","cells","firstCell","values","feature","columns","raw","kind","htmlCommentMetadata","nodes","enrichedBlocks","typeCounts","childCursors","Map","count","source","allComplex","every","it","push"],"sources":["../../../src/match/astToParts.ts"],"sourcesContent":["import type { RootContent } from 'mdast';\nimport { convertToBlockElements } from '../markdownBlocks';\nimport type {\n EnrichedBlockElement,\n BlockElement,\n} from '../patternValidator/types';\nimport {\n type Part,\n type PartType,\n extractContent,\n extractContentMarkdown,\n deriveRole,\n} from '../parts/parts';\nimport { parseCalloutTag } from '../component/componentDefinitions/parse-utils';\nimport { parseWeb5Url } from '../utils/entityLinkParser';\nimport { Web5UrlType } from '../types/link-types';\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\nfunction blockElementToPartType(element: BlockElement | string): PartType {\n if (typeof element === 'string' && element.startsWith('list{')) {\n return 'list';\n }\n switch (element) {\n case 'h1':\n case 'h2':\n case 'h3':\n case 'h4':\n case 'h5':\n case 'h6':\n return 'heading';\n case 't':\n return 'text';\n case 'link':\n return 'link';\n case 'image':\n return 'image';\n case 'icon':\n return 'icon';\n case 'list':\n return 'list';\n case 'table':\n return 'table';\n case 'blockquote':\n return 'blockquote';\n case 'callout':\n return 'callout';\n case 'html_comment':\n return 'htmlComment';\n case 'thematicBreak':\n return 'thematicBreak';\n default:\n return 'text';\n }\n}\n\n/** Find a descendant node of a given type */\nfunction findNode(node: any, type: string): any {\n if (!node) {\n return null;\n }\n if (node.type === type) {\n return node;\n }\n if (Array.isArray(node.children)) {\n for (const child of node.children) {\n const found = findNode(child, type);\n if (found) {\n return found;\n }\n }\n }\n return null;\n}\n\nfunction extractTableCellLabel(node: any): string {\n if (!node) {\n return '';\n }\n if (node.type === 'image') {\n const parsed =\n typeof node.url === 'string' ? parseWeb5Url(node.url) : null;\n return parsed?.type === Web5UrlType.ICON ? '' : node.alt || '';\n }\n if (typeof node.value === 'string') {\n return node.value;\n }\n if (Array.isArray(node.children)) {\n return node.children.map(extractTableCellLabel).join('');\n }\n return '';\n}\n\nfunction tableCellMeta(cell: any): {\n label: string;\n href?: string;\n entityType?: string;\n entityId?: string;\n iconHint?: string;\n} {\n const label = extractTableCellLabel(cell).trim();\n const link = findNode(cell, 'link');\n const href = typeof link?.url === 'string' ? link.url : undefined;\n const parsed = href ? parseWeb5Url(href) : null;\n const image = findNode(cell, 'image');\n const imageUrl = typeof image?.url === 'string' ? image.url : '';\n const iconParsed = imageUrl ? parseWeb5Url(imageUrl) : null;\n\n return {\n label,\n ...(href ? { href } : {}),\n ...(parsed?.type === Web5UrlType.ENTITY\n ? {\n entityType: parsed.entityType,\n entityId: parsed.entityId,\n }\n : {}),\n ...(iconParsed?.type === Web5UrlType.ICON\n ? { iconHint: iconParsed.path }\n : {}),\n };\n}\n\n// ---------------------------------------------------------------------------\n// Source resolution — maps each enriched block back to its specific AST child\n// ---------------------------------------------------------------------------\n\ninterface ResolvedSource {\n content: string;\n markdown?: string;\n node: any;\n}\n\n/**\n * For non-paragraph nodes the whole AST node is the source.\n * For paragraphs, walk children with a cursor so that each block\n * (link, image, text-run) resolves to its own specific child(ren).\n */\nfunction resolveSource(\n block: EnrichedBlockElement,\n astNode: RootContent,\n cursors: Map<number, number>,\n): ResolvedSource {\n const n = astNode as any;\n\n if (n.type !== 'paragraph') {\n const content = extractContent(astNode);\n const md = extractContentMarkdown(astNode);\n return { content, markdown: md !== content ? md : undefined, node: n };\n }\n\n const children: any[] = n.children || [];\n const cursor = cursors.get(block.astNodeIndex) || 0;\n\n if (\n block.element === 'link' ||\n block.element === 'image' ||\n block.element === 'icon'\n ) {\n const target = block.element === 'icon' ? 'image' : block.element;\n for (let i = cursor; i < children.length; i++) {\n const found =\n children[i].type === target\n ? children[i]\n : findNode(children[i], target);\n if (found) {\n cursors.set(block.astNodeIndex, i + 1);\n return { content: extractContent(found), node: found };\n }\n }\n return { content: '', node: n };\n }\n\n if (block.element === 't') {\n let text = '';\n let md = '';\n let i = cursor;\n for (; i < children.length; i++) {\n const child = children[i];\n if (child.type === 'link' || child.type === 'image') {\n break;\n }\n // Also stop at formatting wrappers that contain links/images\n if (findNode(child, 'link') || findNode(child, 'image')) {\n break;\n }\n text += extractContent(child);\n md += extractContentMarkdown(child);\n }\n cursors.set(block.astNodeIndex, i);\n // Preserve markdown when it differs from plain text (has inline formatting)\n return { content: text, markdown: md !== text ? md : undefined, node: n };\n }\n\n return { content: extractContent(astNode), node: n };\n}\n\n// ---------------------------------------------------------------------------\n// Meta extraction — uses the resolved source node, not the whole parent\n// ---------------------------------------------------------------------------\n\n/**\n * Decompose list items into ListItemParts, each carrying nested Part[] in meta.\n * Reuses nodesToParts to resolve each item's inline content (links, images, text, etc.).\n */\nfunction buildListItems(listNode: any): Part[] {\n return (listNode.children || []).map((item: any) => {\n const itemParts = nodesToParts(item.children || []);\n\n // A \"complex\" item has more than plain text (e.g. links, images, mixed\n // inline elements). In that case content is meaningless — the real data\n // lives inside the nested parts.\n const isComplex =\n itemParts.length > 1 ||\n (itemParts.length === 1 && itemParts[0].type !== 'text');\n\n const part: Part = {\n type: 'listItem',\n role: 'list-item',\n content: isComplex ? '' : extractContent(item).trim(),\n };\n if (itemParts.length > 0) {\n const markdown = extractContentMarkdown(item).trim();\n part.meta = {\n parts: itemParts,\n // Always store markdown so consumers can access the original formatting\n // (e.g. **bold** titles). For simple items, content already has plain\n // text; markdown preserves inline formatting.\n ...(markdown ? { markdown } : {}),\n };\n }\n return part;\n });\n}\n\nfunction buildMeta(\n type: PartType,\n sourceNode: any,\n block: EnrichedBlockElement,\n): Record<string, unknown> | undefined {\n if (!sourceNode) {\n return undefined;\n }\n switch (type) {\n case 'heading':\n return { level: sourceNode.depth };\n case 'link': {\n const link =\n sourceNode.type === 'link' ? sourceNode : findNode(sourceNode, 'link');\n const lm = block.linkMetadata;\n return {\n href: lm?.url || link?.url || '',\n ...(link?.title ? { title: link.title } : {}),\n ...(lm?.entityType\n ? { isEntity: true, entityType: lm.entityType }\n : {}),\n };\n }\n case 'image': {\n const img =\n sourceNode.type === 'image'\n ? sourceNode\n : findNode(sourceNode, 'image');\n return img\n ? { src: img.url || '', ...(img.alt ? { alt: img.alt } : {}) }\n : undefined;\n }\n case 'icon': {\n const iconImg =\n sourceNode.type === 'image'\n ? sourceNode\n : findNode(sourceNode, 'image');\n const iconUrl: string = iconImg?.url || '';\n const web5Match = iconUrl.match(/^web5:\\/\\/icon\\/(.+)/);\n const iconHint = web5Match?.[1] || '';\n return iconHint ? { iconHint } : undefined;\n }\n case 'list': {\n const items = buildListItems(sourceNode);\n return { ordered: !!sourceNode.ordered, items };\n }\n case 'table': {\n const rows: any[] = sourceNode.children || [];\n if (rows.length === 0) {\n return undefined;\n }\n const headerRow = rows[0];\n const headerCells: ReturnType<typeof tableCellMeta>[] = (\n headerRow?.children || []\n ).map((cell: any) => tableCellMeta(cell));\n const columnLabels = headerCells.map((cell) => cell.label);\n const featureColumnLabel = columnLabels[0] || 'Feature';\n const dataRows = rows.slice(1).map((row: any) => {\n const cells = row.children || [];\n const firstCell = tableCellMeta(cells[0]);\n const values = cells\n .slice(1)\n .map((cell: any) => extractContent(cell).trim());\n return {\n feature: firstCell.label || '',\n ...(firstCell.iconHint ? { iconHint: firstCell.iconHint } : {}),\n values,\n };\n });\n return {\n columnLabels: columnLabels.slice(1),\n columns: headerCells.slice(1),\n featureColumnLabel,\n rows: dataRows,\n };\n }\n case 'callout': {\n const raw = extractContent(sourceNode).trim();\n const { kind } = parseCalloutTag(raw);\n return kind ? { kind } : undefined;\n }\n case 'htmlComment':\n return block.htmlCommentMetadata\n ? { ...block.htmlCommentMetadata }\n : undefined;\n default:\n return undefined;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Convert markdown AST nodes into a flat array of `Part` objects.\n *\n * Each enriched block element becomes one `Part`. When a single AST node\n * (e.g. a paragraph) produces multiple blocks (link + image + text), each\n * block becomes a separate Part with content resolved from its specific\n * child within the paragraph.\n *\n * @param nodes - Markdown AST nodes (typically a slice of `root.children`)\n * @returns Ordered array of Part objects\n */\nexport function nodesToParts(nodes: RootContent[]): Part[] {\n if (!nodes || nodes.length === 0) {\n return [];\n }\n\n const enrichedBlocks = convertToBlockElements(nodes);\n const parts: Part[] = [];\n const typeCounts: Record<string, number> = {};\n const childCursors = new Map<number, number>();\n\n for (const block of enrichedBlocks) {\n const astNode = nodes[block.astNodeIndex];\n const type = blockElementToPartType(block.element);\n const count = (typeCounts[type] || 0) + 1;\n typeCounts[type] = count;\n\n const source = resolveSource(block, astNode, childCursors);\n\n const part: Part = {\n type,\n role: deriveRole(type, count),\n content: source.content.trim(),\n };\n const meta = buildMeta(type, source.node, block);\n if (meta) {\n part.meta = meta;\n }\n\n // Preserve original markdown when inline formatting exists (bold, italic, etc.)\n if (source.markdown) {\n part.meta = { ...part.meta, markdown: source.markdown.trim() };\n }\n\n // For lists whose items are all complex, the list-level content is\n // meaningless (just a concatenation of child text). Clear it so that\n // consumers rely on the structured items in meta instead.\n if (type === 'list' && meta) {\n const items = (meta.items as Part[]) || [];\n const allComplex =\n items.length > 0 && items.every((it) => it.content === '');\n if (allComplex) {\n part.content = '';\n }\n }\n\n parts.push(part);\n }\n\n return parts;\n}\n"],"mappings":"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","ignoreList":[]}
|
|
1
|
+
{"version":3,"names":["convertToBlockElements","extractContent","extractContentMarkdown","deriveRole","parseCalloutTag","parseWeb5Url","Web5UrlType","blockElementToPartType","element","startsWith","findNode","node","type","Array","isArray","children","child","found","extractTableCellLabel","parsed","url","ICON","alt","value","map","join","tableCellMeta","cell","label","trim","link","href","undefined","image","imageUrl","iconParsed","ENTITY","entityType","entityId","iconHint","path","resolveSource","block","astNode","cursors","n","content","md","markdown","cursor","get","astNodeIndex","target","i","length","set","text","buildListItems","listNode","item","itemParts","nodesToParts","isComplex","part","role","meta","parts","buildMeta","sourceNode","level","depth","lm","linkMetadata","title","isEntity","img","src","iconImg","iconUrl","web5Match","match","items","ordered","rows","headerRow","headerCells","columnLabels","featureColumnLabel","dataRows","slice","row","cells","firstCell","values","feature","columns","raw","kind","htmlCommentMetadata","nodes","enrichedBlocks","typeCounts","childCursors","Map","seenNodes","Set","count","source","has","continuesNode","add","allComplex","every","it","push"],"sources":["../../../src/match/astToParts.ts"],"sourcesContent":["import type { RootContent } from 'mdast';\nimport { convertToBlockElements } from '../markdownBlocks';\nimport type {\n EnrichedBlockElement,\n BlockElement,\n} from '../patternValidator/types';\nimport {\n type Part,\n type PartType,\n extractContent,\n extractContentMarkdown,\n deriveRole,\n} from '../parts/parts';\nimport { parseCalloutTag } from '../component/componentDefinitions/parse-utils';\nimport { parseWeb5Url } from '../utils/entityLinkParser';\nimport { Web5UrlType } from '../types/link-types';\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\nfunction blockElementToPartType(element: BlockElement | string): PartType {\n if (typeof element === 'string' && element.startsWith('list{')) {\n return 'list';\n }\n switch (element) {\n case 'h1':\n case 'h2':\n case 'h3':\n case 'h4':\n case 'h5':\n case 'h6':\n return 'heading';\n case 't':\n return 'text';\n case 'link':\n return 'link';\n case 'image':\n return 'image';\n case 'icon':\n return 'icon';\n case 'list':\n return 'list';\n case 'table':\n return 'table';\n case 'blockquote':\n return 'blockquote';\n case 'callout':\n return 'callout';\n case 'html_comment':\n return 'htmlComment';\n case 'thematicBreak':\n return 'thematicBreak';\n default:\n return 'text';\n }\n}\n\n/** Find a descendant node of a given type */\nfunction findNode(node: any, type: string): any {\n if (!node) {\n return null;\n }\n if (node.type === type) {\n return node;\n }\n if (Array.isArray(node.children)) {\n for (const child of node.children) {\n const found = findNode(child, type);\n if (found) {\n return found;\n }\n }\n }\n return null;\n}\n\nfunction extractTableCellLabel(node: any): string {\n if (!node) {\n return '';\n }\n if (node.type === 'image') {\n const parsed =\n typeof node.url === 'string' ? parseWeb5Url(node.url) : null;\n return parsed?.type === Web5UrlType.ICON ? '' : node.alt || '';\n }\n if (typeof node.value === 'string') {\n return node.value;\n }\n if (Array.isArray(node.children)) {\n return node.children.map(extractTableCellLabel).join('');\n }\n return '';\n}\n\nfunction tableCellMeta(cell: any): {\n label: string;\n href?: string;\n entityType?: string;\n entityId?: string;\n iconHint?: string;\n} {\n const label = extractTableCellLabel(cell).trim();\n const link = findNode(cell, 'link');\n const href = typeof link?.url === 'string' ? link.url : undefined;\n const parsed = href ? parseWeb5Url(href) : null;\n const image = findNode(cell, 'image');\n const imageUrl = typeof image?.url === 'string' ? image.url : '';\n const iconParsed = imageUrl ? parseWeb5Url(imageUrl) : null;\n\n return {\n label,\n ...(href ? { href } : {}),\n ...(parsed?.type === Web5UrlType.ENTITY\n ? {\n entityType: parsed.entityType,\n entityId: parsed.entityId,\n }\n : {}),\n ...(iconParsed?.type === Web5UrlType.ICON\n ? { iconHint: iconParsed.path }\n : {}),\n };\n}\n\n// ---------------------------------------------------------------------------\n// Source resolution — maps each enriched block back to its specific AST child\n// ---------------------------------------------------------------------------\n\ninterface ResolvedSource {\n content: string;\n markdown?: string;\n node: any;\n}\n\n/**\n * For non-paragraph nodes the whole AST node is the source.\n * For paragraphs, walk children with a cursor so that each block\n * (link, image, text-run) resolves to its own specific child(ren).\n */\nfunction resolveSource(\n block: EnrichedBlockElement,\n astNode: RootContent,\n cursors: Map<number, number>,\n): ResolvedSource {\n const n = astNode as any;\n\n if (n.type !== 'paragraph') {\n const content = extractContent(astNode);\n const md = extractContentMarkdown(astNode);\n return { content, markdown: md !== content ? md : undefined, node: n };\n }\n\n const children: any[] = n.children || [];\n const cursor = cursors.get(block.astNodeIndex) || 0;\n\n if (\n block.element === 'link' ||\n block.element === 'image' ||\n block.element === 'icon'\n ) {\n const target = block.element === 'icon' ? 'image' : block.element;\n for (let i = cursor; i < children.length; i++) {\n const found =\n children[i].type === target\n ? children[i]\n : findNode(children[i], target);\n if (found) {\n cursors.set(block.astNodeIndex, i + 1);\n return { content: extractContent(found), node: found };\n }\n }\n return { content: '', node: n };\n }\n\n if (block.element === 't') {\n let text = '';\n let md = '';\n let i = cursor;\n for (; i < children.length; i++) {\n const child = children[i];\n if (child.type === 'link' || child.type === 'image') {\n break;\n }\n // Also stop at formatting wrappers that contain links/images\n if (findNode(child, 'link') || findNode(child, 'image')) {\n break;\n }\n text += extractContent(child);\n md += extractContentMarkdown(child);\n }\n cursors.set(block.astNodeIndex, i);\n // Preserve markdown when it differs from plain text (has inline formatting)\n return { content: text, markdown: md !== text ? md : undefined, node: n };\n }\n\n return { content: extractContent(astNode), node: n };\n}\n\n// ---------------------------------------------------------------------------\n// Meta extraction — uses the resolved source node, not the whole parent\n// ---------------------------------------------------------------------------\n\n/**\n * Decompose list items into ListItemParts, each carrying nested Part[] in meta.\n * Reuses nodesToParts to resolve each item's inline content (links, images, text, etc.).\n */\nfunction buildListItems(listNode: any): Part[] {\n return (listNode.children || []).map((item: any) => {\n const itemParts = nodesToParts(item.children || []);\n\n // A \"complex\" item has more than plain text (e.g. links, images, mixed\n // inline elements). In that case content is meaningless — the real data\n // lives inside the nested parts.\n const isComplex =\n itemParts.length > 1 ||\n (itemParts.length === 1 && itemParts[0].type !== 'text');\n\n const part: Part = {\n type: 'listItem',\n role: 'list-item',\n content: isComplex ? '' : extractContent(item).trim(),\n };\n if (itemParts.length > 0) {\n const markdown = extractContentMarkdown(item).trim();\n part.meta = {\n parts: itemParts,\n // Always store markdown so consumers can access the original formatting\n // (e.g. **bold** titles). For simple items, content already has plain\n // text; markdown preserves inline formatting.\n ...(markdown ? { markdown } : {}),\n };\n }\n return part;\n });\n}\n\nfunction buildMeta(\n type: PartType,\n sourceNode: any,\n block: EnrichedBlockElement,\n): Record<string, unknown> | undefined {\n if (!sourceNode) {\n return undefined;\n }\n switch (type) {\n case 'heading':\n return { level: sourceNode.depth };\n case 'link': {\n const link =\n sourceNode.type === 'link' ? sourceNode : findNode(sourceNode, 'link');\n const lm = block.linkMetadata;\n return {\n href: lm?.url || link?.url || '',\n ...(link?.title ? { title: link.title } : {}),\n ...(lm?.entityType\n ? { isEntity: true, entityType: lm.entityType }\n : {}),\n };\n }\n case 'image': {\n const img =\n sourceNode.type === 'image'\n ? sourceNode\n : findNode(sourceNode, 'image');\n return img\n ? { src: img.url || '', ...(img.alt ? { alt: img.alt } : {}) }\n : undefined;\n }\n case 'icon': {\n const iconImg =\n sourceNode.type === 'image'\n ? sourceNode\n : findNode(sourceNode, 'image');\n const iconUrl: string = iconImg?.url || '';\n const web5Match = iconUrl.match(/^web5:\\/\\/icon\\/(.+)/);\n const iconHint = web5Match?.[1] || '';\n return iconHint ? { iconHint } : undefined;\n }\n case 'list': {\n const items = buildListItems(sourceNode);\n return { ordered: !!sourceNode.ordered, items };\n }\n case 'table': {\n const rows: any[] = sourceNode.children || [];\n if (rows.length === 0) {\n return undefined;\n }\n const headerRow = rows[0];\n const headerCells: ReturnType<typeof tableCellMeta>[] = (\n headerRow?.children || []\n ).map((cell: any) => tableCellMeta(cell));\n const columnLabels = headerCells.map((cell) => cell.label);\n const featureColumnLabel = columnLabels[0] || 'Feature';\n const dataRows = rows.slice(1).map((row: any) => {\n const cells = row.children || [];\n const firstCell = tableCellMeta(cells[0]);\n const values = cells\n .slice(1)\n .map((cell: any) => extractContent(cell).trim());\n return {\n feature: firstCell.label || '',\n ...(firstCell.iconHint ? { iconHint: firstCell.iconHint } : {}),\n values,\n };\n });\n return {\n columnLabels: columnLabels.slice(1),\n columns: headerCells.slice(1),\n featureColumnLabel,\n rows: dataRows,\n };\n }\n case 'callout': {\n const raw = extractContent(sourceNode).trim();\n const { kind } = parseCalloutTag(raw);\n return kind ? { kind } : undefined;\n }\n case 'htmlComment':\n return block.htmlCommentMetadata\n ? { ...block.htmlCommentMetadata }\n : undefined;\n default:\n return undefined;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\n/**\n * Convert markdown AST nodes into a flat array of `Part` objects.\n *\n * Each enriched block element becomes one `Part`. When a single AST node\n * (e.g. a paragraph) produces multiple blocks (link + image + text), each\n * block becomes a separate Part with content resolved from its specific\n * child within the paragraph.\n *\n * @param nodes - Markdown AST nodes (typically a slice of `root.children`)\n * @returns Ordered array of Part objects\n */\nexport function nodesToParts(nodes: RootContent[]): Part[] {\n if (!nodes || nodes.length === 0) {\n return [];\n }\n\n const enrichedBlocks = convertToBlockElements(nodes);\n const parts: Part[] = [];\n const typeCounts: Record<string, number> = {};\n const childCursors = new Map<number, number>();\n // A paragraph carrying an inline link is unwrapped into several block\n // elements (`t`, `link`, `t`), so its parts are siblings with nothing to say\n // they came from one paragraph. Mark every part after the first of a given\n // AST node, so a consumer rebuilding prose can tell \"same paragraph, keep\n // going\" from \"next paragraph, break\". Only split nodes carry the flag —\n // plain paragraphs, headings and images stay meta-free.\n const seenNodes = new Set<number>();\n\n for (const block of enrichedBlocks) {\n const astNode = nodes[block.astNodeIndex];\n const type = blockElementToPartType(block.element);\n const count = (typeCounts[type] || 0) + 1;\n typeCounts[type] = count;\n\n const source = resolveSource(block, astNode, childCursors);\n\n const part: Part = {\n type,\n role: deriveRole(type, count),\n content: source.content.trim(),\n };\n const meta = buildMeta(type, source.node, block);\n if (meta) {\n part.meta = meta;\n }\n\n // Preserve original markdown when inline formatting exists (bold, italic, etc.)\n if (source.markdown) {\n part.meta = { ...part.meta, markdown: source.markdown.trim() };\n }\n\n if (seenNodes.has(block.astNodeIndex)) {\n part.meta = { ...part.meta, continuesNode: true };\n }\n seenNodes.add(block.astNodeIndex);\n\n // For lists whose items are all complex, the list-level content is\n // meaningless (just a concatenation of child text). Clear it so that\n // consumers rely on the structured items in meta instead.\n if (type === 'list' && meta) {\n const items = (meta.items as Part[]) || [];\n const allComplex =\n items.length > 0 && items.every((it) => it.content === '');\n if (allComplex) {\n part.content = '';\n }\n }\n\n parts.push(part);\n }\n\n return parts;\n}\n"],"mappings":"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","ignoreList":[]}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The consent gate — DL #218.
|
|
3
|
+
*
|
|
4
|
+
* Every BI, telemetry and analytics emitter in web5 goes through `transmit()`
|
|
5
|
+
* instead of calling its transport directly. The gate exists because the
|
|
6
|
+
* consent signal resolves *after* the events that need it: Shopify's consent
|
|
7
|
+
* API is not on the page by default and has to be requested, and the fedops
|
|
8
|
+
* app-load pair fires inside that window. A boolean checked at each call site
|
|
9
|
+
* can only ever drop an event that is already in flight; the gate can hold it.
|
|
10
|
+
*
|
|
11
|
+
* Two independent unlocks, and they do not compose as a plain "either/or":
|
|
12
|
+
*
|
|
13
|
+
* | consent | engaged | result |
|
|
14
|
+
* |-----------|---------|---------------------------------|
|
|
15
|
+
* | granted | either | transmit |
|
|
16
|
+
* | unknown | no | hold in the buffer |
|
|
17
|
+
* | unknown | yes | transmit, and flush what's held |
|
|
18
|
+
* | denied | yes | still nothing |
|
|
19
|
+
*
|
|
20
|
+
* Denied outranks engaged. A visitor who refused analytics in the banner does
|
|
21
|
+
* not re-enable tracking by typing a question.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { UNKNOWN_CONSENT } from './types.js';
|
|
25
|
+
|
|
26
|
+
/** Held events are dropped past this many — a runaway emitter must not become a leak. */
|
|
27
|
+
const DEFAULT_BUFFER_LIMIT = 50;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* One prefix for the whole consent trail, so `[w5-consent]` in the console
|
|
31
|
+
* filter shows the full story: which CMP was found, what it answered, and for
|
|
32
|
+
* every event whether it was sent, held, dropped or replayed.
|
|
33
|
+
*/
|
|
34
|
+
const LOG_PREFIX = '[w5-consent]';
|
|
35
|
+
const log = function () {
|
|
36
|
+
try {
|
|
37
|
+
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
|
|
38
|
+
args[_key] = arguments[_key];
|
|
39
|
+
}
|
|
40
|
+
console.log(LOG_PREFIX, ...args);
|
|
41
|
+
} catch {
|
|
42
|
+
// A console that throws must never break a send.
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
const describe = s => `analytics=${s.analytics} performance=${s.performance} marketing=${s.marketing}`;
|
|
46
|
+
let snapshot = UNKNOWN_CONSENT;
|
|
47
|
+
let engaged = false;
|
|
48
|
+
let provider = null;
|
|
49
|
+
let unsubscribeProvider = null;
|
|
50
|
+
let buffer = [];
|
|
51
|
+
let bufferLimit = DEFAULT_BUFFER_LIMIT;
|
|
52
|
+
let droppedFromOverflow = 0;
|
|
53
|
+
const listeners = new Set();
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Cached so `getGateView` is referentially stable between changes —
|
|
57
|
+
* `useSyncExternalStore` re-renders forever if the snapshot is a fresh object
|
|
58
|
+
* on every read.
|
|
59
|
+
*/
|
|
60
|
+
let view = Object.freeze({
|
|
61
|
+
consent: UNKNOWN_CONSENT,
|
|
62
|
+
engaged: false
|
|
63
|
+
});
|
|
64
|
+
const rebuildView = () => {
|
|
65
|
+
view = Object.freeze({
|
|
66
|
+
consent: snapshot,
|
|
67
|
+
engaged
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
const notify = () => {
|
|
71
|
+
for (const listener of listeners) {
|
|
72
|
+
try {
|
|
73
|
+
listener();
|
|
74
|
+
} catch {
|
|
75
|
+
// A subscriber that throws must not stop the others, or stop a flush.
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Whether an event for `purpose` may go on the wire right now.
|
|
82
|
+
*
|
|
83
|
+
* `necessary` is always allowed. For everything else: explicit consent wins in
|
|
84
|
+
* both directions, and engagement resolves only the `unknown` case.
|
|
85
|
+
*/
|
|
86
|
+
export const mayTransmit = purpose => {
|
|
87
|
+
if (purpose === 'necessary') {
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
const state = snapshot[purpose];
|
|
91
|
+
if (state === 'denied') {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
if (state === 'granted') {
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
return engaged;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Release everything the gate is holding that is now allowed, and drop
|
|
102
|
+
* everything that is now refused. Anything still `unknown` stays held.
|
|
103
|
+
*
|
|
104
|
+
* Order is preserved among released events, but a flush necessarily emits them
|
|
105
|
+
* later than they were raised — a held app-load event reaches the wire after
|
|
106
|
+
* the prompt that unlocked it. Anything reading these as a timeline has to
|
|
107
|
+
* know that.
|
|
108
|
+
*/
|
|
109
|
+
const settleBuffer = () => {
|
|
110
|
+
if (buffer.length === 0) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const stillHeld = [];
|
|
114
|
+
const releasing = [];
|
|
115
|
+
for (const held of buffer) {
|
|
116
|
+
if (mayTransmit(held.purpose)) {
|
|
117
|
+
releasing.push(held);
|
|
118
|
+
} else if (snapshot[held.purpose] === 'denied') {
|
|
119
|
+
// dropped on the floor, deliberately and permanently
|
|
120
|
+
} else {
|
|
121
|
+
stillHeld.push(held);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const dropped = buffer.length - stillHeld.length - releasing.length;
|
|
125
|
+
buffer = stillHeld;
|
|
126
|
+
if (releasing.length || dropped) {
|
|
127
|
+
log(`settle — releasing ${releasing.length}` + (dropped ? `, dropping ${dropped} (refused)` : '') + (stillHeld.length ? `, still holding ${stillHeld.length}` : ''));
|
|
128
|
+
}
|
|
129
|
+
for (const held of releasing) {
|
|
130
|
+
try {
|
|
131
|
+
log(` ↳ REPLAY ${held.label} (${held.purpose})`);
|
|
132
|
+
held.send();
|
|
133
|
+
} catch {
|
|
134
|
+
// A failed send must not strand the rest of the flush.
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* The single entry point for anything that would put a request on the wire.
|
|
141
|
+
*
|
|
142
|
+
* Allowed → sent now. Refused → dropped. Not yet known → held until an unlock
|
|
143
|
+
* settles it, or discarded with the page.
|
|
144
|
+
*/
|
|
145
|
+
export const transmit = function (purpose, send, label) {
|
|
146
|
+
if (label === void 0) {
|
|
147
|
+
label = 'event';
|
|
148
|
+
}
|
|
149
|
+
if (mayTransmit(purpose)) {
|
|
150
|
+
log(`SEND ${label} (${purpose}) — allowed`);
|
|
151
|
+
send();
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (purpose !== 'necessary' && snapshot[purpose] === 'denied') {
|
|
155
|
+
log(`DROP ${label} (${purpose}) — visitor refused`);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (buffer.length >= bufferLimit) {
|
|
159
|
+
droppedFromOverflow += 1;
|
|
160
|
+
log(`DROP ${label} (${purpose}) — buffer full at ${bufferLimit}, ${droppedFromOverflow} lost`);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
buffer.push({
|
|
164
|
+
purpose: purpose,
|
|
165
|
+
send,
|
|
166
|
+
label
|
|
167
|
+
});
|
|
168
|
+
log(`HOLD ${label} (${purpose}) — no answer yet, ${buffer.length} held`);
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* The visitor did something deliberate — submitted a prompt — so the session
|
|
173
|
+
* may be measured even though no CMP has answered.
|
|
174
|
+
*
|
|
175
|
+
* This is the unlock that keeps web5 measurable at all: on a Shopify store
|
|
176
|
+
* with no privacy configuration the consent API never loads, consent stays
|
|
177
|
+
* `unknown` forever, and without this every store would report nothing.
|
|
178
|
+
*
|
|
179
|
+
* It unlocks **transmission only**. Persistent identity stays gated on real
|
|
180
|
+
* consent (see `mayPersistIdentity`) — typing a question is a reason to
|
|
181
|
+
* measure the session, not a reason to persist an identifier across visits.
|
|
182
|
+
* It also cannot override a `denied`.
|
|
183
|
+
*/
|
|
184
|
+
export const unlockOnUserAction = () => {
|
|
185
|
+
if (engaged) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
engaged = true;
|
|
189
|
+
log(`UNLOCK — visitor submitted a prompt; ${buffer.length} held event(s) to settle`);
|
|
190
|
+
rebuildView();
|
|
191
|
+
settleBuffer();
|
|
192
|
+
notify();
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
/** Whether the visitor has taken the deliberate action that unlocks the session. */
|
|
196
|
+
export const isUserEngaged = () => engaged;
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Persistent, cross-visit storage of a visitor identifier requires real
|
|
200
|
+
* consent — engagement is deliberately not enough.
|
|
201
|
+
*/
|
|
202
|
+
export const mayPersistIdentity = () => snapshot.analytics === 'granted';
|
|
203
|
+
export const getConsentSnapshot = () => snapshot;
|
|
204
|
+
|
|
205
|
+
/** Referentially stable between changes, for `useSyncExternalStore`. */
|
|
206
|
+
export const getGateView = () => view;
|
|
207
|
+
export const subscribeToConsent = listener => {
|
|
208
|
+
listeners.add(listener);
|
|
209
|
+
return () => {
|
|
210
|
+
listeners.delete(listener);
|
|
211
|
+
};
|
|
212
|
+
};
|
|
213
|
+
const applySnapshot = next => {
|
|
214
|
+
if (next.analytics === snapshot.analytics && next.marketing === snapshot.marketing && next.performance === snapshot.performance) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
log(`ANSWER — ${describe(snapshot)} → ${describe(next)}`);
|
|
218
|
+
snapshot = Object.freeze({
|
|
219
|
+
...next
|
|
220
|
+
});
|
|
221
|
+
rebuildView();
|
|
222
|
+
settleBuffer();
|
|
223
|
+
notify();
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Install the host's provider. Reads its current state immediately, *then*
|
|
228
|
+
* subscribes — DL #217: a listener registered into an already-rendered page is
|
|
229
|
+
* bound to an event that may have fired long before the bundle executed, so
|
|
230
|
+
* subscribing alone would leave the gate permanently at `unknown`.
|
|
231
|
+
*/
|
|
232
|
+
export const installConsentProvider = next => {
|
|
233
|
+
unsubscribeProvider == null || unsubscribeProvider();
|
|
234
|
+
provider = next;
|
|
235
|
+
const initial = next.read();
|
|
236
|
+
log(`provider installed: ${next.name} — reads ${describe(initial)}`);
|
|
237
|
+
applySnapshot(initial);
|
|
238
|
+
unsubscribeProvider = next.subscribe(applySnapshot);
|
|
239
|
+
};
|
|
240
|
+
export const getInstalledProviderName = () => {
|
|
241
|
+
var _provider;
|
|
242
|
+
return ((_provider = provider) == null ? void 0 : _provider.name) ?? null;
|
|
243
|
+
};
|
|
244
|
+
export const setConsentBufferLimit = limit => {
|
|
245
|
+
bufferLimit = Math.max(0, limit);
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
/** Diagnostics only — how much the gate is holding, and what it had to drop. */
|
|
249
|
+
export const getConsentGateStats = () => ({
|
|
250
|
+
held: buffer.length,
|
|
251
|
+
droppedFromOverflow
|
|
252
|
+
});
|
|
253
|
+
export const resetConsentGateForTests = () => {
|
|
254
|
+
unsubscribeProvider == null || unsubscribeProvider();
|
|
255
|
+
unsubscribeProvider = null;
|
|
256
|
+
provider = null;
|
|
257
|
+
snapshot = UNKNOWN_CONSENT;
|
|
258
|
+
engaged = false;
|
|
259
|
+
buffer = [];
|
|
260
|
+
bufferLimit = DEFAULT_BUFFER_LIMIT;
|
|
261
|
+
droppedFromOverflow = 0;
|
|
262
|
+
listeners.clear();
|
|
263
|
+
rebuildView();
|
|
264
|
+
};
|
|
265
|
+
//# sourceMappingURL=consentGate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["UNKNOWN_CONSENT","DEFAULT_BUFFER_LIMIT","LOG_PREFIX","log","_len","arguments","length","args","Array","_key","console","describe","s","analytics","performance","marketing","snapshot","engaged","provider","unsubscribeProvider","buffer","bufferLimit","droppedFromOverflow","listeners","Set","view","Object","freeze","consent","rebuildView","notify","listener","mayTransmit","purpose","state","settleBuffer","stillHeld","releasing","held","push","dropped","label","send","transmit","unlockOnUserAction","isUserEngaged","mayPersistIdentity","getConsentSnapshot","getGateView","subscribeToConsent","add","delete","applySnapshot","next","installConsentProvider","initial","read","name","subscribe","getInstalledProviderName","_provider","setConsentBufferLimit","limit","Math","max","getConsentGateStats","resetConsentGateForTests","clear"],"sources":["../../../src/privacy/consentGate.ts"],"sourcesContent":["/**\n * The consent gate — DL #218.\n *\n * Every BI, telemetry and analytics emitter in web5 goes through `transmit()`\n * instead of calling its transport directly. The gate exists because the\n * consent signal resolves *after* the events that need it: Shopify's consent\n * API is not on the page by default and has to be requested, and the fedops\n * app-load pair fires inside that window. A boolean checked at each call site\n * can only ever drop an event that is already in flight; the gate can hold it.\n *\n * Two independent unlocks, and they do not compose as a plain \"either/or\":\n *\n * | consent | engaged | result |\n * |-----------|---------|---------------------------------|\n * | granted | either | transmit |\n * | unknown | no | hold in the buffer |\n * | unknown | yes | transmit, and flush what's held |\n * | denied | yes | still nothing |\n *\n * Denied outranks engaged. A visitor who refused analytics in the banner does\n * not re-enable tracking by typing a question.\n */\n\nimport {\n UNKNOWN_CONSENT,\n type ConsentProvider,\n type ConsentPurpose,\n type ConsentSnapshot,\n type GatedPurpose,\n} from './types';\n\n/** Held events are dropped past this many — a runaway emitter must not become a leak. */\nconst DEFAULT_BUFFER_LIMIT = 50;\n\n/**\n * One prefix for the whole consent trail, so `[w5-consent]` in the console\n * filter shows the full story: which CMP was found, what it answered, and for\n * every event whether it was sent, held, dropped or replayed.\n */\nconst LOG_PREFIX = '[w5-consent]';\n\nconst log = (...args: unknown[]): void => {\n try {\n console.log(LOG_PREFIX, ...args);\n } catch {\n // A console that throws must never break a send.\n }\n};\n\nconst describe = (s: ConsentSnapshot): string =>\n `analytics=${s.analytics} performance=${s.performance} marketing=${s.marketing}`;\n\ninterface HeldEvent {\n purpose: GatedPurpose;\n send: () => void;\n label: string;\n}\n\nexport interface GateView {\n consent: ConsentSnapshot;\n engaged: boolean;\n}\n\ntype Listener = () => void;\n\nlet snapshot: ConsentSnapshot = UNKNOWN_CONSENT;\nlet engaged = false;\nlet provider: ConsentProvider | null = null;\nlet unsubscribeProvider: (() => void) | null = null;\nlet buffer: HeldEvent[] = [];\nlet bufferLimit = DEFAULT_BUFFER_LIMIT;\nlet droppedFromOverflow = 0;\n\nconst listeners = new Set<Listener>();\n\n/**\n * Cached so `getGateView` is referentially stable between changes —\n * `useSyncExternalStore` re-renders forever if the snapshot is a fresh object\n * on every read.\n */\nlet view: GateView = Object.freeze({ consent: UNKNOWN_CONSENT, engaged: false });\n\nconst rebuildView = (): void => {\n view = Object.freeze({ consent: snapshot, engaged });\n};\n\nconst notify = (): void => {\n for (const listener of listeners) {\n try {\n listener();\n } catch {\n // A subscriber that throws must not stop the others, or stop a flush.\n }\n }\n};\n\n/**\n * Whether an event for `purpose` may go on the wire right now.\n *\n * `necessary` is always allowed. For everything else: explicit consent wins in\n * both directions, and engagement resolves only the `unknown` case.\n */\nexport const mayTransmit = (purpose: ConsentPurpose): boolean => {\n if (purpose === 'necessary') {\n return true;\n }\n const state = snapshot[purpose];\n if (state === 'denied') {\n return false;\n }\n if (state === 'granted') {\n return true;\n }\n return engaged;\n};\n\n/**\n * Release everything the gate is holding that is now allowed, and drop\n * everything that is now refused. Anything still `unknown` stays held.\n *\n * Order is preserved among released events, but a flush necessarily emits them\n * later than they were raised — a held app-load event reaches the wire after\n * the prompt that unlocked it. Anything reading these as a timeline has to\n * know that.\n */\nconst settleBuffer = (): void => {\n if (buffer.length === 0) {\n return;\n }\n const stillHeld: HeldEvent[] = [];\n const releasing: HeldEvent[] = [];\n\n for (const held of buffer) {\n if (mayTransmit(held.purpose)) {\n releasing.push(held);\n } else if (snapshot[held.purpose] === 'denied') {\n // dropped on the floor, deliberately and permanently\n } else {\n stillHeld.push(held);\n }\n }\n\n const dropped = buffer.length - stillHeld.length - releasing.length;\n buffer = stillHeld;\n\n if (releasing.length || dropped) {\n log(\n `settle — releasing ${releasing.length}` +\n (dropped ? `, dropping ${dropped} (refused)` : '') +\n (stillHeld.length ? `, still holding ${stillHeld.length}` : ''),\n );\n }\n\n for (const held of releasing) {\n try {\n log(` ↳ REPLAY ${held.label} (${held.purpose})`);\n held.send();\n } catch {\n // A failed send must not strand the rest of the flush.\n }\n }\n};\n\n/**\n * The single entry point for anything that would put a request on the wire.\n *\n * Allowed → sent now. Refused → dropped. Not yet known → held until an unlock\n * settles it, or discarded with the page.\n */\nexport const transmit = (\n purpose: ConsentPurpose,\n send: () => void,\n label = 'event',\n): void => {\n if (mayTransmit(purpose)) {\n log(`SEND ${label} (${purpose}) — allowed`);\n send();\n return;\n }\n if (purpose !== 'necessary' && snapshot[purpose] === 'denied') {\n log(`DROP ${label} (${purpose}) — visitor refused`);\n return;\n }\n if (buffer.length >= bufferLimit) {\n droppedFromOverflow += 1;\n log(\n `DROP ${label} (${purpose}) — buffer full at ${bufferLimit}, ${droppedFromOverflow} lost`,\n );\n return;\n }\n buffer.push({ purpose: purpose as GatedPurpose, send, label });\n log(`HOLD ${label} (${purpose}) — no answer yet, ${buffer.length} held`);\n};\n\n/**\n * The visitor did something deliberate — submitted a prompt — so the session\n * may be measured even though no CMP has answered.\n *\n * This is the unlock that keeps web5 measurable at all: on a Shopify store\n * with no privacy configuration the consent API never loads, consent stays\n * `unknown` forever, and without this every store would report nothing.\n *\n * It unlocks **transmission only**. Persistent identity stays gated on real\n * consent (see `mayPersistIdentity`) — typing a question is a reason to\n * measure the session, not a reason to persist an identifier across visits.\n * It also cannot override a `denied`.\n */\nexport const unlockOnUserAction = (): void => {\n if (engaged) {\n return;\n }\n engaged = true;\n log(\n `UNLOCK — visitor submitted a prompt; ${buffer.length} held event(s) to settle`,\n );\n rebuildView();\n settleBuffer();\n notify();\n};\n\n/** Whether the visitor has taken the deliberate action that unlocks the session. */\nexport const isUserEngaged = (): boolean => engaged;\n\n/**\n * Persistent, cross-visit storage of a visitor identifier requires real\n * consent — engagement is deliberately not enough.\n */\nexport const mayPersistIdentity = (): boolean => snapshot.analytics === 'granted';\n\nexport const getConsentSnapshot = (): ConsentSnapshot => snapshot;\n\n/** Referentially stable between changes, for `useSyncExternalStore`. */\nexport const getGateView = (): GateView => view;\n\nexport const subscribeToConsent = (listener: Listener): (() => void) => {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n};\n\nconst applySnapshot = (next: ConsentSnapshot): void => {\n if (\n next.analytics === snapshot.analytics &&\n next.marketing === snapshot.marketing &&\n next.performance === snapshot.performance\n ) {\n return;\n }\n log(`ANSWER — ${describe(snapshot)} → ${describe(next)}`);\n snapshot = Object.freeze({ ...next });\n rebuildView();\n settleBuffer();\n notify();\n};\n\n/**\n * Install the host's provider. Reads its current state immediately, *then*\n * subscribes — DL #217: a listener registered into an already-rendered page is\n * bound to an event that may have fired long before the bundle executed, so\n * subscribing alone would leave the gate permanently at `unknown`.\n */\nexport const installConsentProvider = (next: ConsentProvider): void => {\n unsubscribeProvider?.();\n provider = next;\n const initial = next.read();\n log(`provider installed: ${next.name} — reads ${describe(initial)}`);\n applySnapshot(initial);\n unsubscribeProvider = next.subscribe(applySnapshot);\n};\n\nexport const getInstalledProviderName = (): string | null => provider?.name ?? null;\n\nexport const setConsentBufferLimit = (limit: number): void => {\n bufferLimit = Math.max(0, limit);\n};\n\n/** Diagnostics only — how much the gate is holding, and what it had to drop. */\nexport const getConsentGateStats = (): {\n held: number;\n droppedFromOverflow: number;\n} => ({ held: buffer.length, droppedFromOverflow });\n\nexport const resetConsentGateForTests = (): void => {\n unsubscribeProvider?.();\n unsubscribeProvider = null;\n provider = null;\n snapshot = UNKNOWN_CONSENT;\n engaged = false;\n buffer = [];\n bufferLimit = DEFAULT_BUFFER_LIMIT;\n droppedFromOverflow = 0;\n listeners.clear();\n rebuildView();\n};\n"],"mappings":"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","ignoreList":[]}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consent override (debug-only) — DL #218.
|
|
3
|
+
*
|
|
4
|
+
* Shopify only serves its cookie banner where consent is legally required, so
|
|
5
|
+
* from most of the world `shouldShowBanner()` is `false` and there is no way to
|
|
6
|
+
* reach the `unknown` or `denied` branches by hand. Development stores often
|
|
7
|
+
* have no privacy configuration at all, which pins the gate at `unknown`
|
|
8
|
+
* forever. Neither is a bug, and both make the interesting states unreachable
|
|
9
|
+
* from a browser.
|
|
10
|
+
*
|
|
11
|
+
* This forces the gate's answer so all four rows of the truth table can be
|
|
12
|
+
* walked on any store, in any region, published or not.
|
|
13
|
+
*
|
|
14
|
+
* ?w5consent=granted | denied | unknown (one-shot)
|
|
15
|
+
* localStorage["w5_consent_override"] (sticky)
|
|
16
|
+
*
|
|
17
|
+
* **The two are not equally trusted, on purpose.** A link is something one
|
|
18
|
+
* person can send another, so a query param may only ever *reduce* what is
|
|
19
|
+
* collected: `denied` is honoured anywhere, while `granted` and `unknown` are
|
|
20
|
+
* honoured only on a local origin. Turning tracking *on* for someone else by
|
|
21
|
+
* sending them a URL is exactly the thing this whole feature exists to
|
|
22
|
+
* prevent. On a real storefront, set the localStorage key in devtools — which
|
|
23
|
+
* is a deliberate act by the person whose browser it is.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
export const CONSENT_OVERRIDE_KEY = 'w5_consent_override';
|
|
27
|
+
export const CONSENT_OVERRIDE_QUERY_PARAM = 'w5consent';
|
|
28
|
+
const VALID = ['granted', 'denied', 'unknown'];
|
|
29
|
+
const isConsentState = value => typeof value === 'string' && VALID.includes(value);
|
|
30
|
+
|
|
31
|
+
/** Only a local origin may force a *more* permissive state from a URL. */
|
|
32
|
+
const isLocalOrigin = () => {
|
|
33
|
+
try {
|
|
34
|
+
const {
|
|
35
|
+
hostname
|
|
36
|
+
} = window.location;
|
|
37
|
+
return hostname === 'localhost' || hostname === '127.0.0.1' || hostname.endsWith('.local');
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
const readQueryParam = () => {
|
|
43
|
+
try {
|
|
44
|
+
const raw = new URLSearchParams(window.location.search).get(CONSENT_OVERRIDE_QUERY_PARAM);
|
|
45
|
+
if (!isConsentState(raw)) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
// `denied` can only ever collect less, so a link may set it anywhere.
|
|
49
|
+
if (raw === 'denied' || isLocalOrigin()) {
|
|
50
|
+
return raw;
|
|
51
|
+
}
|
|
52
|
+
console.warn(`[w5-consent] ignoring ?${CONSENT_OVERRIDE_QUERY_PARAM}=${raw} — only "denied" is accepted from a URL off a local origin. ` + `Set localStorage["${CONSENT_OVERRIDE_KEY}"] = "${raw}" instead.`);
|
|
53
|
+
return null;
|
|
54
|
+
} catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
const readStorage = () => {
|
|
59
|
+
try {
|
|
60
|
+
const raw = localStorage.getItem(CONSENT_OVERRIDE_KEY);
|
|
61
|
+
return isConsentState(raw) ? raw : null;
|
|
62
|
+
} catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/** The forced consent state, or `null` when no override is active. */
|
|
68
|
+
export const getConsentOverride = () => readQueryParam() ?? readStorage();
|
|
69
|
+
export const overrideSnapshot = state => ({
|
|
70
|
+
analytics: state,
|
|
71
|
+
marketing: state,
|
|
72
|
+
performance: state
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
/** Persist the override and apply it on the next load. `null` clears it. */
|
|
76
|
+
export const setConsentOverride = state => {
|
|
77
|
+
try {
|
|
78
|
+
if (state) {
|
|
79
|
+
localStorage.setItem(CONSENT_OVERRIDE_KEY, state);
|
|
80
|
+
} else {
|
|
81
|
+
localStorage.removeItem(CONSENT_OVERRIDE_KEY);
|
|
82
|
+
}
|
|
83
|
+
} catch {
|
|
84
|
+
// Ignore — the override simply won't persist.
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
//# sourceMappingURL=consentOverride.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["CONSENT_OVERRIDE_KEY","CONSENT_OVERRIDE_QUERY_PARAM","VALID","isConsentState","value","includes","isLocalOrigin","hostname","window","location","endsWith","readQueryParam","raw","URLSearchParams","search","get","console","warn","readStorage","localStorage","getItem","getConsentOverride","overrideSnapshot","state","analytics","marketing","performance","setConsentOverride","setItem","removeItem"],"sources":["../../../src/privacy/consentOverride.ts"],"sourcesContent":["/**\n * Consent override (debug-only) — DL #218.\n *\n * Shopify only serves its cookie banner where consent is legally required, so\n * from most of the world `shouldShowBanner()` is `false` and there is no way to\n * reach the `unknown` or `denied` branches by hand. Development stores often\n * have no privacy configuration at all, which pins the gate at `unknown`\n * forever. Neither is a bug, and both make the interesting states unreachable\n * from a browser.\n *\n * This forces the gate's answer so all four rows of the truth table can be\n * walked on any store, in any region, published or not.\n *\n * ?w5consent=granted | denied | unknown (one-shot)\n * localStorage[\"w5_consent_override\"] (sticky)\n *\n * **The two are not equally trusted, on purpose.** A link is something one\n * person can send another, so a query param may only ever *reduce* what is\n * collected: `denied` is honoured anywhere, while `granted` and `unknown` are\n * honoured only on a local origin. Turning tracking *on* for someone else by\n * sending them a URL is exactly the thing this whole feature exists to\n * prevent. On a real storefront, set the localStorage key in devtools — which\n * is a deliberate act by the person whose browser it is.\n */\n\nimport type { ConsentSnapshot, ConsentState } from './types';\n\nexport const CONSENT_OVERRIDE_KEY = 'w5_consent_override';\nexport const CONSENT_OVERRIDE_QUERY_PARAM = 'w5consent';\n\nconst VALID: readonly ConsentState[] = ['granted', 'denied', 'unknown'];\n\nconst isConsentState = (value: unknown): value is ConsentState =>\n typeof value === 'string' && (VALID as readonly string[]).includes(value);\n\n/** Only a local origin may force a *more* permissive state from a URL. */\nconst isLocalOrigin = (): boolean => {\n try {\n const { hostname } = window.location;\n return (\n hostname === 'localhost' ||\n hostname === '127.0.0.1' ||\n hostname.endsWith('.local')\n );\n } catch {\n return false;\n }\n};\n\nconst readQueryParam = (): ConsentState | null => {\n try {\n const raw = new URLSearchParams(window.location.search).get(\n CONSENT_OVERRIDE_QUERY_PARAM,\n );\n if (!isConsentState(raw)) {\n return null;\n }\n // `denied` can only ever collect less, so a link may set it anywhere.\n if (raw === 'denied' || isLocalOrigin()) {\n return raw;\n }\n console.warn(\n `[w5-consent] ignoring ?${CONSENT_OVERRIDE_QUERY_PARAM}=${raw} — only \"denied\" is accepted from a URL off a local origin. ` +\n `Set localStorage[\"${CONSENT_OVERRIDE_KEY}\"] = \"${raw}\" instead.`,\n );\n return null;\n } catch {\n return null;\n }\n};\n\nconst readStorage = (): ConsentState | null => {\n try {\n const raw = localStorage.getItem(CONSENT_OVERRIDE_KEY);\n return isConsentState(raw) ? raw : null;\n } catch {\n return null;\n }\n};\n\n/** The forced consent state, or `null` when no override is active. */\nexport const getConsentOverride = (): ConsentState | null =>\n readQueryParam() ?? readStorage();\n\nexport const overrideSnapshot = (state: ConsentState): ConsentSnapshot => ({\n analytics: state,\n marketing: state,\n performance: state,\n});\n\n/** Persist the override and apply it on the next load. `null` clears it. */\nexport const setConsentOverride = (state: ConsentState | null): void => {\n try {\n if (state) {\n localStorage.setItem(CONSENT_OVERRIDE_KEY, state);\n } else {\n localStorage.removeItem(CONSENT_OVERRIDE_KEY);\n }\n } catch {\n // Ignore — the override simply won't persist.\n }\n};\n"],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAIA,OAAO,MAAMA,oBAAoB,GAAG,qBAAqB;AACzD,OAAO,MAAMC,4BAA4B,GAAG,WAAW;AAEvD,MAAMC,KAA8B,GAAG,CAAC,SAAS,EAAE,QAAQ,EAAE,SAAS,CAAC;AAEvE,MAAMC,cAAc,GAAIC,KAAc,IACpC,OAAOA,KAAK,KAAK,QAAQ,IAAKF,KAAK,CAAuBG,QAAQ,CAACD,KAAK,CAAC;;AAE3E;AACA,MAAME,aAAa,GAAGA,CAAA,KAAe;EACnC,IAAI;IACF,MAAM;MAAEC;IAAS,CAAC,GAAGC,MAAM,CAACC,QAAQ;IACpC,OACEF,QAAQ,KAAK,WAAW,IACxBA,QAAQ,KAAK,WAAW,IACxBA,QAAQ,CAACG,QAAQ,CAAC,QAAQ,CAAC;EAE/B,CAAC,CAAC,MAAM;IACN,OAAO,KAAK;EACd;AACF,CAAC;AAED,MAAMC,cAAc,GAAGA,CAAA,KAA2B;EAChD,IAAI;IACF,MAAMC,GAAG,GAAG,IAAIC,eAAe,CAACL,MAAM,CAACC,QAAQ,CAACK,MAAM,CAAC,CAACC,GAAG,CACzDd,4BACF,CAAC;IACD,IAAI,CAACE,cAAc,CAACS,GAAG,CAAC,EAAE;MACxB,OAAO,IAAI;IACb;IACA;IACA,IAAIA,GAAG,KAAK,QAAQ,IAAIN,aAAa,CAAC,CAAC,EAAE;MACvC,OAAOM,GAAG;IACZ;IACAI,OAAO,CAACC,IAAI,CACV,0BAA0BhB,4BAA4B,IAAIW,GAAG,8DAA8D,GACzH,qBAAqBZ,oBAAoB,SAASY,GAAG,YACzD,CAAC;IACD,OAAO,IAAI;EACb,CAAC,CAAC,MAAM;IACN,OAAO,IAAI;EACb;AACF,CAAC;AAED,MAAMM,WAAW,GAAGA,CAAA,KAA2B;EAC7C,IAAI;IACF,MAAMN,GAAG,GAAGO,YAAY,CAACC,OAAO,CAACpB,oBAAoB,CAAC;IACtD,OAAOG,cAAc,CAACS,GAAG,CAAC,GAAGA,GAAG,GAAG,IAAI;EACzC,CAAC,CAAC,MAAM;IACN,OAAO,IAAI;EACb;AACF,CAAC;;AAED;AACA,OAAO,MAAMS,kBAAkB,GAAGA,CAAA,KAChCV,cAAc,CAAC,CAAC,IAAIO,WAAW,CAAC,CAAC;AAEnC,OAAO,MAAMI,gBAAgB,GAAIC,KAAmB,KAAuB;EACzEC,SAAS,EAAED,KAAK;EAChBE,SAAS,EAAEF,KAAK;EAChBG,WAAW,EAAEH;AACf,CAAC,CAAC;;AAEF;AACA,OAAO,MAAMI,kBAAkB,GAAIJ,KAA0B,IAAW;EACtE,IAAI;IACF,IAAIA,KAAK,EAAE;MACTJ,YAAY,CAACS,OAAO,CAAC5B,oBAAoB,EAAEuB,KAAK,CAAC;IACnD,CAAC,MAAM;MACLJ,YAAY,CAACU,UAAU,CAAC7B,oBAAoB,CAAC;IAC/C;EACF,CAAC,CAAC,MAAM;IACN;EAAA;AAEJ,CAAC","ignoreList":[]}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider selection — DL #218.
|
|
3
|
+
*
|
|
4
|
+
* Detected at boot, not configured per client, because a merchant can install
|
|
5
|
+
* a CMP long after we deploy and the bundle is not the source of truth for a
|
|
6
|
+
* store's behaviour (DL #214).
|
|
7
|
+
*
|
|
8
|
+
* Order is host-supplied → Shopify → OneTrust → none. A host that publishes an
|
|
9
|
+
* answer outranks anything we could sniff, because it knows things we cannot:
|
|
10
|
+
* a server-side consent record, a CMP behind its own abstraction, or a legal
|
|
11
|
+
* position we have no business guessing at.
|
|
12
|
+
*
|
|
13
|
+
* When nothing is detected the gate keeps its default — everything `unknown`,
|
|
14
|
+
* held until the visitor acts. That is the case on every Shopify store with no
|
|
15
|
+
* privacy configuration, which today is most of them.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { installConsentProvider } from './consentGate.js';
|
|
19
|
+
import { getConsentOverride, overrideSnapshot } from './consentOverride.js';
|
|
20
|
+
import { createHostSuppliedConsentProvider, hasHostSuppliedConsent } from './providers/hostSuppliedProvider.js';
|
|
21
|
+
import { createShopifyConsentProvider, isShopifyHost } from './providers/shopifyProvider.js';
|
|
22
|
+
import { createOneTrustConsentProvider, isOneTrustHost } from './providers/oneTrustProvider.js';
|
|
23
|
+
export const detectConsentProvider = () => {
|
|
24
|
+
// Debug override outranks every real signal, so the states a region or an
|
|
25
|
+
// unconfigured store makes unreachable can still be walked by hand.
|
|
26
|
+
const forced = getConsentOverride();
|
|
27
|
+
if (forced) {
|
|
28
|
+
console.warn(`[w5-consent] OVERRIDE ACTIVE — consent forced to "${forced}". This is a debug switch, not a real answer.`);
|
|
29
|
+
return {
|
|
30
|
+
name: `override:${forced}`,
|
|
31
|
+
read: () => overrideSnapshot(forced),
|
|
32
|
+
subscribe: () => () => {}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
if (hasHostSuppliedConsent()) {
|
|
36
|
+
return createHostSuppliedConsentProvider();
|
|
37
|
+
}
|
|
38
|
+
if (isShopifyHost()) {
|
|
39
|
+
return createShopifyConsentProvider();
|
|
40
|
+
}
|
|
41
|
+
if (isOneTrustHost()) {
|
|
42
|
+
return createOneTrustConsentProvider();
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Install the detected provider, if any. Call once at boot, before the first
|
|
49
|
+
* emitter runs — anything raised earlier is held rather than lost, but the
|
|
50
|
+
* sooner this runs the less the buffer has to carry.
|
|
51
|
+
*/
|
|
52
|
+
export const initConsentGate = () => {
|
|
53
|
+
const provider = detectConsentProvider();
|
|
54
|
+
if (provider) {
|
|
55
|
+
installConsentProvider(provider);
|
|
56
|
+
} else {
|
|
57
|
+
try {
|
|
58
|
+
console.log('[w5-consent] no CMP detected (no host-supplied consent, no Shopify customerPrivacy, no OneTrust) — everything held until the visitor submits a prompt');
|
|
59
|
+
} catch {
|
|
60
|
+
/* never break on a console */
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return provider;
|
|
64
|
+
};
|
|
65
|
+
//# sourceMappingURL=detectProvider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["installConsentProvider","getConsentOverride","overrideSnapshot","createHostSuppliedConsentProvider","hasHostSuppliedConsent","createShopifyConsentProvider","isShopifyHost","createOneTrustConsentProvider","isOneTrustHost","detectConsentProvider","forced","console","warn","name","read","subscribe","initConsentGate","provider","log"],"sources":["../../../src/privacy/detectProvider.ts"],"sourcesContent":["/**\n * Provider selection — DL #218.\n *\n * Detected at boot, not configured per client, because a merchant can install\n * a CMP long after we deploy and the bundle is not the source of truth for a\n * store's behaviour (DL #214).\n *\n * Order is host-supplied → Shopify → OneTrust → none. A host that publishes an\n * answer outranks anything we could sniff, because it knows things we cannot:\n * a server-side consent record, a CMP behind its own abstraction, or a legal\n * position we have no business guessing at.\n *\n * When nothing is detected the gate keeps its default — everything `unknown`,\n * held until the visitor acts. That is the case on every Shopify store with no\n * privacy configuration, which today is most of them.\n */\n\nimport { installConsentProvider } from './consentGate';\nimport { getConsentOverride, overrideSnapshot } from './consentOverride';\nimport type { ConsentProvider } from './types';\nimport {\n createHostSuppliedConsentProvider,\n hasHostSuppliedConsent,\n} from './providers/hostSuppliedProvider';\nimport {\n createShopifyConsentProvider,\n isShopifyHost,\n} from './providers/shopifyProvider';\nimport {\n createOneTrustConsentProvider,\n isOneTrustHost,\n} from './providers/oneTrustProvider';\n\nexport const detectConsentProvider = (): ConsentProvider | null => {\n // Debug override outranks every real signal, so the states a region or an\n // unconfigured store makes unreachable can still be walked by hand.\n const forced = getConsentOverride();\n if (forced) {\n console.warn(\n `[w5-consent] OVERRIDE ACTIVE — consent forced to \"${forced}\". This is a debug switch, not a real answer.`,\n );\n return {\n name: `override:${forced}`,\n read: () => overrideSnapshot(forced),\n subscribe: () => () => {},\n };\n }\n if (hasHostSuppliedConsent()) {\n return createHostSuppliedConsentProvider();\n }\n if (isShopifyHost()) {\n return createShopifyConsentProvider();\n }\n if (isOneTrustHost()) {\n return createOneTrustConsentProvider();\n }\n return null;\n};\n\n/**\n * Install the detected provider, if any. Call once at boot, before the first\n * emitter runs — anything raised earlier is held rather than lost, but the\n * sooner this runs the less the buffer has to carry.\n */\nexport const initConsentGate = (): ConsentProvider | null => {\n const provider = detectConsentProvider();\n if (provider) {\n installConsentProvider(provider);\n } else {\n try {\n console.log(\n '[w5-consent] no CMP detected (no host-supplied consent, no Shopify customerPrivacy, no OneTrust) — everything held until the visitor submits a prompt',\n );\n } catch {\n /* never break on a console */\n }\n }\n return provider;\n};\n"],"mappings":"AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA,SAASA,sBAAsB,QAAQ,eAAe;AACtD,SAASC,kBAAkB,EAAEC,gBAAgB,QAAQ,mBAAmB;AAExE,SACEC,iCAAiC,EACjCC,sBAAsB,QACjB,kCAAkC;AACzC,SACEC,4BAA4B,EAC5BC,aAAa,QACR,6BAA6B;AACpC,SACEC,6BAA6B,EAC7BC,cAAc,QACT,8BAA8B;AAErC,OAAO,MAAMC,qBAAqB,GAAGA,CAAA,KAA8B;EACjE;EACA;EACA,MAAMC,MAAM,GAAGT,kBAAkB,CAAC,CAAC;EACnC,IAAIS,MAAM,EAAE;IACVC,OAAO,CAACC,IAAI,CACV,qDAAqDF,MAAM,+CAC7D,CAAC;IACD,OAAO;MACLG,IAAI,EAAE,YAAYH,MAAM,EAAE;MAC1BI,IAAI,EAAEA,CAAA,KAAMZ,gBAAgB,CAACQ,MAAM,CAAC;MACpCK,SAAS,EAAEA,CAAA,KAAM,MAAM,CAAC;IAC1B,CAAC;EACH;EACA,IAAIX,sBAAsB,CAAC,CAAC,EAAE;IAC5B,OAAOD,iCAAiC,CAAC,CAAC;EAC5C;EACA,IAAIG,aAAa,CAAC,CAAC,EAAE;IACnB,OAAOD,4BAA4B,CAAC,CAAC;EACvC;EACA,IAAIG,cAAc,CAAC,CAAC,EAAE;IACpB,OAAOD,6BAA6B,CAAC,CAAC;EACxC;EACA,OAAO,IAAI;AACb,CAAC;;AAED;AACA;AACA;AACA;AACA;AACA,OAAO,MAAMS,eAAe,GAAGA,CAAA,KAA8B;EAC3D,MAAMC,QAAQ,GAAGR,qBAAqB,CAAC,CAAC;EACxC,IAAIQ,QAAQ,EAAE;IACZjB,sBAAsB,CAACiB,QAAQ,CAAC;EAClC,CAAC,MAAM;IACL,IAAI;MACFN,OAAO,CAACO,GAAG,CACT,uJACF,CAAC;IACH,CAAC,CAAC,MAAM;MACN;IAAA;EAEJ;EACA,OAAOD,QAAQ;AACjB,CAAC","ignoreList":[]}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { UNKNOWN_CONSENT } from './types.js';
|
|
2
|
+
export { transmit, mayTransmit, unlockOnUserAction, isUserEngaged, mayPersistIdentity, getConsentSnapshot, getGateView, subscribeToConsent, installConsentProvider, getInstalledProviderName, setConsentBufferLimit, getConsentGateStats, resetConsentGateForTests } from './consentGate.js';
|
|
3
|
+
export { detectConsentProvider, initConsentGate } from './detectProvider.js';
|
|
4
|
+
export { CONSENT_OVERRIDE_KEY, CONSENT_OVERRIDE_QUERY_PARAM, getConsentOverride, setConsentOverride } from './consentOverride.js';
|
|
5
|
+
export { createShopifyConsentProvider, isShopifyHost } from './providers/shopifyProvider.js';
|
|
6
|
+
export { createOneTrustConsentProvider, isOneTrustHost } from './providers/oneTrustProvider.js';
|
|
7
|
+
export { HOST_CONSENT_GLOBAL, createHostSuppliedConsentProvider, hasHostSuppliedConsent, publishHostConsent } from './providers/hostSuppliedProvider.js';
|
|
8
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["UNKNOWN_CONSENT","transmit","mayTransmit","unlockOnUserAction","isUserEngaged","mayPersistIdentity","getConsentSnapshot","getGateView","subscribeToConsent","installConsentProvider","getInstalledProviderName","setConsentBufferLimit","getConsentGateStats","resetConsentGateForTests","detectConsentProvider","initConsentGate","CONSENT_OVERRIDE_KEY","CONSENT_OVERRIDE_QUERY_PARAM","getConsentOverride","setConsentOverride","createShopifyConsentProvider","isShopifyHost","createOneTrustConsentProvider","isOneTrustHost","HOST_CONSENT_GLOBAL","createHostSuppliedConsentProvider","hasHostSuppliedConsent","publishHostConsent"],"sources":["../../../src/privacy/index.ts"],"sourcesContent":["export {\n type ConsentState,\n type ConsentPurpose,\n type GatedPurpose,\n type ConsentSnapshot,\n type ConsentProvider,\n UNKNOWN_CONSENT,\n} from './types';\n\nexport {\n type GateView,\n transmit,\n mayTransmit,\n unlockOnUserAction,\n isUserEngaged,\n mayPersistIdentity,\n getConsentSnapshot,\n getGateView,\n subscribeToConsent,\n installConsentProvider,\n getInstalledProviderName,\n setConsentBufferLimit,\n getConsentGateStats,\n resetConsentGateForTests,\n} from './consentGate';\n\nexport { detectConsentProvider, initConsentGate } from './detectProvider';\n\nexport {\n CONSENT_OVERRIDE_KEY,\n CONSENT_OVERRIDE_QUERY_PARAM,\n getConsentOverride,\n setConsentOverride,\n} from './consentOverride';\n\nexport {\n createShopifyConsentProvider,\n isShopifyHost,\n} from './providers/shopifyProvider';\nexport {\n createOneTrustConsentProvider,\n isOneTrustHost,\n} from './providers/oneTrustProvider';\nexport {\n type HostConsentInput,\n HOST_CONSENT_GLOBAL,\n createHostSuppliedConsentProvider,\n hasHostSuppliedConsent,\n publishHostConsent,\n} from './providers/hostSuppliedProvider';\n"],"mappings":"AAAA,SAMEA,eAAe,QACV,SAAS;AAEhB,SAEEC,QAAQ,EACRC,WAAW,EACXC,kBAAkB,EAClBC,aAAa,EACbC,kBAAkB,EAClBC,kBAAkB,EAClBC,WAAW,EACXC,kBAAkB,EAClBC,sBAAsB,EACtBC,wBAAwB,EACxBC,qBAAqB,EACrBC,mBAAmB,EACnBC,wBAAwB,QACnB,eAAe;AAEtB,SAASC,qBAAqB,EAAEC,eAAe,QAAQ,kBAAkB;AAEzE,SACEC,oBAAoB,EACpBC,4BAA4B,EAC5BC,kBAAkB,EAClBC,kBAAkB,QACb,mBAAmB;AAE1B,SACEC,4BAA4B,EAC5BC,aAAa,QACR,6BAA6B;AACpC,SACEC,6BAA6B,EAC7BC,cAAc,QACT,8BAA8B;AACrC,SAEEC,mBAAmB,EACnBC,iCAAiC,EACjCC,sBAAsB,EACtBC,kBAAkB,QACb,kCAAkC","ignoreList":[]}
|