@dreamdata_io/verify-tracking 0.1.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.
Files changed (108) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +756 -0
  3. package/dist/ai/checker.js +222 -0
  4. package/dist/ai/checker.js.map +1 -0
  5. package/dist/ai/checklist.js +190 -0
  6. package/dist/ai/checklist.js.map +1 -0
  7. package/dist/ai/cli-presets.js +27 -0
  8. package/dist/ai/cli-presets.js.map +1 -0
  9. package/dist/ai/evidence-view.js +143 -0
  10. package/dist/ai/evidence-view.js.map +1 -0
  11. package/dist/ai/local-cli.js +58 -0
  12. package/dist/ai/local-cli.js.map +1 -0
  13. package/dist/ai/openai-compatible.js +78 -0
  14. package/dist/ai/openai-compatible.js.map +1 -0
  15. package/dist/ai/response-parser.js +105 -0
  16. package/dist/ai/response-parser.js.map +1 -0
  17. package/dist/ai/types.js +2 -0
  18. package/dist/ai/types.js.map +1 -0
  19. package/dist/analysis/consent-mode.js +225 -0
  20. package/dist/analysis/consent-mode.js.map +1 -0
  21. package/dist/analysis/deployment.js +2658 -0
  22. package/dist/analysis/deployment.js.map +1 -0
  23. package/dist/analysis/diagnosis/copy.en.js +303 -0
  24. package/dist/analysis/diagnosis/copy.en.js.map +1 -0
  25. package/dist/analysis/diagnosis/diagnose.js +172 -0
  26. package/dist/analysis/diagnosis/diagnose.js.map +1 -0
  27. package/dist/analysis/diagnosis/docs.js +118 -0
  28. package/dist/analysis/diagnosis/docs.js.map +1 -0
  29. package/dist/analysis/diagnosis/index.js +5 -0
  30. package/dist/analysis/diagnosis/index.js.map +1 -0
  31. package/dist/analysis/diagnosis/rules.js +656 -0
  32. package/dist/analysis/diagnosis/rules.js.map +1 -0
  33. package/dist/analysis/diagnosis/types.js +2 -0
  34. package/dist/analysis/diagnosis/types.js.map +1 -0
  35. package/dist/cli.js +344 -0
  36. package/dist/cli.js.map +1 -0
  37. package/dist/config/env.js +100 -0
  38. package/dist/config/env.js.map +1 -0
  39. package/dist/config/preflight.js +167 -0
  40. package/dist/config/preflight.js.map +1 -0
  41. package/dist/config/user-config.js +79 -0
  42. package/dist/config/user-config.js.map +1 -0
  43. package/dist/evidence/collect.js +527 -0
  44. package/dist/evidence/collect.js.map +1 -0
  45. package/dist/evidence/consent-accept.js +377 -0
  46. package/dist/evidence/consent-accept.js.map +1 -0
  47. package/dist/evidence/consent.js +340 -0
  48. package/dist/evidence/consent.js.map +1 -0
  49. package/dist/evidence/constants.js +615 -0
  50. package/dist/evidence/constants.js.map +1 -0
  51. package/dist/evidence/globals.js +132 -0
  52. package/dist/evidence/globals.js.map +1 -0
  53. package/dist/evidence/gtm-container.js +432 -0
  54. package/dist/evidence/gtm-container.js.map +1 -0
  55. package/dist/evidence/heuristic-banner.js +107 -0
  56. package/dist/evidence/heuristic-banner.js.map +1 -0
  57. package/dist/evidence/http-probe.js +139 -0
  58. package/dist/evidence/http-probe.js.map +1 -0
  59. package/dist/evidence/init-script.js +112 -0
  60. package/dist/evidence/init-script.js.map +1 -0
  61. package/dist/evidence/network.js +365 -0
  62. package/dist/evidence/network.js.map +1 -0
  63. package/dist/evidence/preference-center.js +368 -0
  64. package/dist/evidence/preference-center.js.map +1 -0
  65. package/dist/evidence/script.js +425 -0
  66. package/dist/evidence/script.js.map +1 -0
  67. package/dist/evidence/second-page.js +332 -0
  68. package/dist/evidence/second-page.js.map +1 -0
  69. package/dist/evidence/selector-probe.js +53 -0
  70. package/dist/evidence/selector-probe.js.map +1 -0
  71. package/dist/evidence/stealth.js +21 -0
  72. package/dist/evidence/stealth.js.map +1 -0
  73. package/dist/evidence/types.js +14 -0
  74. package/dist/evidence/types.js.map +1 -0
  75. package/dist/interactive/prompt.js +31 -0
  76. package/dist/interactive/prompt.js.map +1 -0
  77. package/dist/mcp/server.js +308 -0
  78. package/dist/mcp/server.js.map +1 -0
  79. package/dist/mcp/summary.js +41 -0
  80. package/dist/mcp/summary.js.map +1 -0
  81. package/dist/report/context.js +184 -0
  82. package/dist/report/context.js.map +1 -0
  83. package/dist/report/projection.js +125 -0
  84. package/dist/report/projection.js.map +1 -0
  85. package/dist/report/quote-check.js +90 -0
  86. package/dist/report/quote-check.js.map +1 -0
  87. package/dist/report/render.js +178 -0
  88. package/dist/report/render.js.map +1 -0
  89. package/dist/report/serializers.js +124 -0
  90. package/dist/report/serializers.js.map +1 -0
  91. package/dist/report/store.js +139 -0
  92. package/dist/report/store.js.map +1 -0
  93. package/dist/run/verify.js +77 -0
  94. package/dist/run/verify.js.map +1 -0
  95. package/dist/utils/duration.js +10 -0
  96. package/dist/utils/duration.js.map +1 -0
  97. package/dist/utils/errors.js +17 -0
  98. package/dist/utils/errors.js.map +1 -0
  99. package/dist/utils/logger.js +59 -0
  100. package/dist/utils/logger.js.map +1 -0
  101. package/dist/utils/runTimingsLog.js +27 -0
  102. package/dist/utils/runTimingsLog.js.map +1 -0
  103. package/dist/utils/slug.js +27 -0
  104. package/dist/utils/slug.js.map +1 -0
  105. package/dist/version.js +10 -0
  106. package/dist/version.js.map +1 -0
  107. package/package.json +57 -0
  108. package/scripts/install.sh +314 -0
@@ -0,0 +1,107 @@
1
+ export function runHeuristicBanner(args) {
2
+ const { mode, direction, acceptPhrases, rejectPhrases, preferenceCounterpartPhrases, contextKeywords } = args;
3
+ const NONE = {
4
+ detected: false,
5
+ acceptLabel: null,
6
+ rejectLabel: null,
7
+ counterpart: null,
8
+ preferenceLabel: null,
9
+ clicked: null,
10
+ };
11
+ const normalize = (s) => (s ?? "").replace(/\s+/g, " ").trim().toLowerCase();
12
+ const isVisible = (el) => {
13
+ const style = window.getComputedStyle(el);
14
+ if (style.display === "none" || style.visibility === "hidden" || Number(style.opacity) === 0)
15
+ return false;
16
+ const rect = el.getBoundingClientRect();
17
+ return rect.width > 0 && rect.height > 0;
18
+ };
19
+ /**
20
+ * Every clickable control on the page, INCLUDING the ones inside open
21
+ * shadow roots.
22
+ *
23
+ * egnyte.com runs TrustArc, which renders its whole banner — "Accept All",
24
+ * "Decline All" and all — inside a shadow root on a
25
+ * `.trustarc_newcm_container` host. `document.querySelectorAll` does not
26
+ * cross that boundary, so the run reported "no consent platform was
27
+ * detected", never answered the banner, and concluded the site tracks
28
+ * nothing in any phase. A click dispatched on an element inside an open
29
+ * shadow root works exactly as it does in the light DOM; the only thing
30
+ * missing was finding it.
31
+ */
32
+ const collectControls = () => {
33
+ const found = [];
34
+ const walk = (root, depth) => {
35
+ for (const el of Array.from(root.querySelectorAll("button, a, [role='button']")))
36
+ found.push(el);
37
+ if (depth >= 5)
38
+ return;
39
+ for (const el of Array.from(root.querySelectorAll("*"))) {
40
+ const shadow = el.shadowRoot;
41
+ if (shadow)
42
+ walk(shadow, depth + 1);
43
+ }
44
+ };
45
+ walk(document, 0);
46
+ return found;
47
+ };
48
+ /** parentElement, but stepping out of a shadow root onto its host. */
49
+ const climb = (node) => node.parentElement ?? (node.getRootNode().host ?? null);
50
+ const candidates = collectControls().filter(isVisible);
51
+ const findMatch = (phrases) => candidates.find((el) => phrases.includes(normalize(el.textContent))) ?? null;
52
+ const acceptEl = findMatch(acceptPhrases);
53
+ const rejectEl = findMatch(rejectPhrases);
54
+ if (!acceptEl && !rejectEl)
55
+ return NONE;
56
+ // Climb from whichever control matched, looking for an ancestor that both
57
+ // reads like a cookie notice and contains a second consent control. Both
58
+ // conditions must be satisfied by the SAME ancestor level's subtree.
59
+ const primary = acceptEl ?? rejectEl;
60
+ const opposite = acceptEl === primary ? rejectEl : acceptEl;
61
+ const preferenceEls = candidates.filter((el) => el !== primary && preferenceCounterpartPhrases.includes(normalize(el.textContent)));
62
+ let container = primary;
63
+ let contextOk = false;
64
+ let counterpart = null;
65
+ let preferenceEl = null;
66
+ for (let depth = 0; depth < 8 && container && !(contextOk && counterpart !== null); depth++) {
67
+ const text = normalize(container.textContent);
68
+ if (!contextOk && contextKeywords.some((k) => text.includes(k)))
69
+ contextOk = true;
70
+ // The opposite control is the stronger proof, so it wins when the same
71
+ // container holds both.
72
+ if (counterpart === null && opposite && container.contains(opposite))
73
+ counterpart = "opposite-control";
74
+ if (counterpart === null) {
75
+ const found = preferenceEls.find((el) => container.contains(el));
76
+ if (found) {
77
+ counterpart = "preference-center";
78
+ preferenceEl = found;
79
+ }
80
+ }
81
+ container = climb(container);
82
+ }
83
+ if (!contextOk || counterpart === null)
84
+ return NONE;
85
+ const acceptLabel = acceptEl ? normalize(acceptEl.textContent) : null;
86
+ const rejectLabel = rejectEl ? normalize(rejectEl.textContent) : null;
87
+ const preferenceLabel = preferenceEl ? normalize(preferenceEl.textContent) : null;
88
+ const found = {
89
+ detected: true,
90
+ acceptLabel,
91
+ rejectLabel,
92
+ counterpart,
93
+ preferenceLabel,
94
+ clicked: null,
95
+ };
96
+ if (mode === "detect")
97
+ return found;
98
+ // Nothing to click in this direction — the banner is real, it just has no
99
+ // one-click answer here. The caller's next fallback is the preference
100
+ // centre, which is the only way such a banner can be answered at all.
101
+ const target = direction === "accept" ? acceptEl : rejectEl;
102
+ if (!target)
103
+ return found;
104
+ target.click();
105
+ return { ...found, clicked: direction === "accept" ? acceptLabel : rejectLabel };
106
+ }
107
+ //# sourceMappingURL=heuristic-banner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"heuristic-banner.js","sourceRoot":"","sources":["../../src/evidence/heuristic-banner.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,139 @@
1
+ /**
2
+ * What the site serves to something that is not a browser.
3
+ *
4
+ * A bot wall does not refuse the request — it answers it with a different
5
+ * document. The browser gets a challenge interstitial (a press-and-hold
6
+ * control, a spinner, a "verify you are human" page), and every field a phase
7
+ * collects from that page is empty: no script tags, no GTM container id, no
8
+ * consent banner, no requests, not even a same-origin link to walk to. That
9
+ * emptiness is indistinguishable from a site with nothing installed, and it
10
+ * is the shape a report is most dangerous in — zoominfo.com was reported as
11
+ * "nothing-installed" with "install the script" as the advice, while its GTM
12
+ * container shipped both bundles all along.
13
+ *
14
+ * A plain HTTP GET carries none of the fingerprints those walls key on, so it
15
+ * is usually served the real markup. It cannot answer a single behavioural
16
+ * question — nothing executes, no request is sent, no consent is answered — but
17
+ * it answers two the run otherwise loses entirely: what is installed, and
18
+ * whether the browser was shown the same page at all.
19
+ *
20
+ * Never fails the run. Every failure mode is recorded and handed on as
21
+ * "we could not corroborate", which is what it is.
22
+ */
23
+ import { logger } from "../utils/logger.js";
24
+ import { CMP_MARKUP_MARKERS, CSR_FRAMEWORK_MARKERS } from "./constants.js";
25
+ import { gtmContainerIdsFromText, readScriptPresenceFromHtml } from "./script.js";
26
+ /**
27
+ * An honest client identity, deliberately not a browser's.
28
+ *
29
+ * Claiming to be Chrome was tried and is worse on both counts. It is a
30
+ * disguise, which is the line this tool does not cross — and on zoominfo.com
31
+ * it is also what gets the request refused: a Chrome UA is answered HTTP 403
32
+ * with a 5 KB challenge body, while this one is served the full 428 KB page,
33
+ * because what the wall is defending against is browsers it cannot fingerprint.
34
+ * Chasing the header combination that slips past a given WAF is an arms race
35
+ * with no end and a silent failure mode, so the probe does not play it: it says
36
+ * what it is, and a refusal is reported as a refusal (see
37
+ * assessPageReadability, which treats a non-2xx here as a finding in its own
38
+ * right rather than as an absence of evidence).
39
+ */
40
+ const PROBE_USER_AGENT = "dreamdata-tracking-verification/0.1 (+https://dreamdata.io)";
41
+ const PROBE_ACCEPT = "text/html,*/*;q=0.8";
42
+ /** Enough markup to read the head and the nav; whole pages can be megabytes. */
43
+ const MAX_BYTES = 3_000_000;
44
+ /**
45
+ * Same-origin links, counted the way the second-page probe looks for one.
46
+ *
47
+ * Deliberately crude — this is a corroboration signal, not a navigation
48
+ * target. What matters is the difference between "the document has a site's
49
+ * worth of links in it" and "the document has none", because the second is
50
+ * what an interstitial looks like and the first is what the same URL serves
51
+ * over plain HTTP when a wall turned the browser away.
52
+ */
53
+ function countSameOriginLinks(html, baseUrl) {
54
+ let origin;
55
+ try {
56
+ origin = new URL(baseUrl).origin;
57
+ }
58
+ catch {
59
+ return 0;
60
+ }
61
+ const hrefs = [...html.matchAll(/<a[^>]*\shref=["']([^"']+)["']/gi)].map((m) => m[1]);
62
+ const sameOrigin = new Set();
63
+ for (const href of hrefs) {
64
+ if (href.startsWith("#") || /^(?:mailto|tel|javascript):/i.test(href))
65
+ continue;
66
+ try {
67
+ const resolved = new URL(href, baseUrl);
68
+ if (resolved.origin === origin)
69
+ sameOrigin.add(resolved.href.replace(/#.*$/, ""));
70
+ }
71
+ catch {
72
+ // A malformed href is not a link.
73
+ }
74
+ }
75
+ return sameOrigin.size;
76
+ }
77
+ function cmpMarkersIn(html) {
78
+ return [...new Set(CMP_MARKUP_MARKERS.filter((m) => m.pattern.test(html)).map((m) => m.vendor))];
79
+ }
80
+ /**
81
+ * Corroboration only, and the weakest kind. A framework name cannot say how
82
+ * the site navigates — see CSR_FRAMEWORK_MARKERS — but it is the only routing
83
+ * hint a run has left when a wall turned the browser away.
84
+ */
85
+ function frameworkMarkersIn(html) {
86
+ return [...new Set(CSR_FRAMEWORK_MARKERS.filter((m) => m.pattern.test(html)).map((m) => m.framework))];
87
+ }
88
+ export async function probeServedMarkup(url, timeoutMs, extraHosts = []) {
89
+ const controller = new AbortController();
90
+ const deadline = setTimeout(() => controller.abort(), Math.min(20_000, Math.max(5_000, timeoutMs)));
91
+ try {
92
+ const response = await fetch(url, {
93
+ redirect: "follow",
94
+ signal: controller.signal,
95
+ headers: {
96
+ "user-agent": PROBE_USER_AGENT,
97
+ accept: PROBE_ACCEPT,
98
+ "accept-language": "en-US,en;q=0.9",
99
+ },
100
+ });
101
+ const body = await response.text();
102
+ const html = body.length > MAX_BYTES ? body.slice(0, MAX_BYTES) : body;
103
+ const finalUrl = response.url || url;
104
+ return {
105
+ attempted: true,
106
+ status: response.status,
107
+ finalUrl,
108
+ bytes: body.length,
109
+ truncated: body.length > MAX_BYTES,
110
+ failedReason: null,
111
+ sameOriginLinkCount: countSameOriginLinks(html, finalUrl),
112
+ tagManagerContainerIds: gtmContainerIdsFromText(html),
113
+ cmpMarkers: cmpMarkersIn(html),
114
+ frameworkMarkers: frameworkMarkersIn(html),
115
+ script: readScriptPresenceFromHtml(html, finalUrl, extraHosts),
116
+ };
117
+ }
118
+ catch (err) {
119
+ const reason = err.name === "AbortError" ? `timed out after ${timeoutMs}ms` : err.message;
120
+ logger.debug(`Plain-HTTP probe of ${url} failed`, err);
121
+ return {
122
+ attempted: true,
123
+ status: null,
124
+ finalUrl: null,
125
+ bytes: null,
126
+ truncated: false,
127
+ failedReason: reason,
128
+ sameOriginLinkCount: 0,
129
+ tagManagerContainerIds: [],
130
+ cmpMarkers: [],
131
+ frameworkMarkers: [],
132
+ script: null,
133
+ };
134
+ }
135
+ finally {
136
+ clearTimeout(deadline);
137
+ }
138
+ }
139
+ //# sourceMappingURL=http-probe.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http-probe.js","sourceRoot":"","sources":["../../src/evidence/http-probe.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Injected via page.addInitScript before any page script runs, so it can
3
+ * observe dataLayer.push(['consent', ...]) calls and window.gtag(...) calls
4
+ * even if the site never leaves those values inspectable afterwards.
5
+ *
6
+ * A site that reassigns window.dataLayer to a brand new array (some CMP
7
+ * snippets do) is covered: the accessor below re-wraps whatever is assigned.
8
+ * Still best-effort rather than a hard guarantee — a snippet that captures a
9
+ * direct reference to the array before this script runs can push through it
10
+ * without going past the wrapper.
11
+ */
12
+ export function installEvidenceHooks() {
13
+ const w = window;
14
+ w.__ddEvidence = {
15
+ consentPushes: [],
16
+ gtagCalls: [],
17
+ historyEvents: [],
18
+ // This script re-runs on every document, so a changed id after following
19
+ // a link means a new document was loaded and an unchanged one means the
20
+ // site's own router handled it without one — the difference between "the
21
+ // whole install re-ran" and "nothing re-ran", which is invisible from the
22
+ // URL alone.
23
+ documentId: `${Date.now()}-${Math.random().toString(36).slice(2)}`,
24
+ };
25
+ // A soft navigation leaves no trace in the network log or the DOM; the
26
+ // History API call is the only evidence that the site navigated at all.
27
+ const recordHistory = (kind, url) => {
28
+ w.__ddEvidence.historyEvents.push({
29
+ kind,
30
+ url: typeof url === "string" ? url : null,
31
+ timestamp: Date.now(),
32
+ });
33
+ };
34
+ for (const method of ["pushState", "replaceState"]) {
35
+ const original = history[method].bind(history);
36
+ history[method] = function patched(...args) {
37
+ recordHistory(method, args[2]);
38
+ return original(...args);
39
+ };
40
+ }
41
+ window.addEventListener("popstate", () => recordHistory("popstate", location.href));
42
+ /**
43
+ * Marks an array whose push is already wrapped. It has to live on the array
44
+ * itself, not in a flag in this closure: the accessor below re-wraps on
45
+ * every assignment to window.dataLayer, and a site that genuinely swaps in a
46
+ * new array still needs that one wrapped.
47
+ */
48
+ const WRAPPED_MARKER = "__ddPushWrapped";
49
+ const wrapDataLayer = (arr) => {
50
+ // The near-universal `window.dataLayer = window.dataLayer || []` snippet
51
+ // assigns the SAME array back, so without this guard every such
52
+ // assignment stacked another wrapper on it and one real push was recorded
53
+ // once per layer — 13 identical entries, same millisecond, for a single
54
+ // gtag('consent', ...) call.
55
+ if (arr[WRAPPED_MARKER])
56
+ return;
57
+ Object.defineProperty(arr, WRAPPED_MARKER, { value: true, enumerable: false, configurable: true });
58
+ const originalPush = arr.push.bind(arr);
59
+ arr.push = ((...items) => {
60
+ for (const item of items) {
61
+ // gtag.js's own `gtag(){dataLayer.push(arguments)}` pushes an
62
+ // arguments object, not a real Array — Array.isArray on it is
63
+ // always false, so array-likes must be normalized before the
64
+ // consent-tuple check or every gtag('consent', ...) call is missed.
65
+ const normalized = item !== null &&
66
+ typeof item === "object" &&
67
+ !Array.isArray(item) &&
68
+ typeof item.length === "number"
69
+ ? Array.from(item)
70
+ : item;
71
+ const isConsentTuple = Array.isArray(normalized) && normalized[0] === "consent";
72
+ const isConsentEvent = normalized !== null &&
73
+ typeof normalized === "object" &&
74
+ !Array.isArray(normalized) &&
75
+ normalized.event === "consent";
76
+ if (isConsentTuple || isConsentEvent) {
77
+ w.__ddEvidence.consentPushes.push({ raw: normalized, timestamp: Date.now() });
78
+ }
79
+ }
80
+ return originalPush(...items);
81
+ });
82
+ };
83
+ if (Array.isArray(w.dataLayer)) {
84
+ wrapDataLayer(w.dataLayer);
85
+ }
86
+ let currentDataLayer = w.dataLayer;
87
+ Object.defineProperty(w, "dataLayer", {
88
+ configurable: true,
89
+ get() {
90
+ return currentDataLayer;
91
+ },
92
+ set(value) {
93
+ currentDataLayer = value;
94
+ if (Array.isArray(value))
95
+ wrapDataLayer(value);
96
+ },
97
+ });
98
+ let currentGtag = w.gtag;
99
+ Object.defineProperty(w, "gtag", {
100
+ configurable: true,
101
+ get() {
102
+ return currentGtag;
103
+ },
104
+ set(fn) {
105
+ currentGtag = (...args) => {
106
+ w.__ddEvidence.gtagCalls.push({ args, timestamp: Date.now() });
107
+ return fn(...args);
108
+ };
109
+ },
110
+ });
111
+ }
112
+ //# sourceMappingURL=init-script.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init-script.js","sourceRoot":"","sources":["../../src/evidence/init-script.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,MAAM,UAAU,oBAAoB;IAClC,MAAM,CAAC,GAAG,MAST,CAAC;IAEF,CAAC,CAAC,YAAY,GAAG;QACf,aAAa,EAAE,EAAE;QACjB,SAAS,EAAE,EAAE;QACb,aAAa,EAAE,EAAE;QACjB,yEAAyE;QACzE,wEAAwE;QACxE,yEAAyE;QACzE,0EAA0E;QAC1E,aAAa;QACb,UAAU,EAAE,GAAG,IAAI,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE;KACnE,CAAC;IAEF,uEAAuE;IACvE,wEAAwE;IACxE,MAAM,aAAa,GAAG,CAAC,IAAY,EAAE,GAAY,EAAQ,EAAE;QACzD,CAAC,CAAC,YAAY,CAAC,aAAa,CAAC,IAAI,CAAC;YAChC,IAAI;YACJ,GAAG,EAAE,OAAO,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI;YACzC,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;SACtB,CAAC,CAAC;IACL,CAAC,CAAC;IAEF,KAAK,MAAM,MAAM,IAAI,CAAC,WAAW,EAAE,cAAc,CAAU,EAAE,CAAC;QAC5D,MAAM,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC/C,OAAO,CAAC,MAAM,CAAC,GAAG,SAAS,OAAO,CAAgB,GAAG,IAAsC;YACzF,aAAa,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;YAC/B,OAAO,QAAQ,CAAC,GAAG,IAAI,CAAC,CAAC;QAC3B,CAAC,CAAC;IACJ,CAAC;IACD,MAAM,CAAC,gBAAgB,CAAC,UAAU,EAAE,GAAG,EAAE,CAAC,aAAa,CAAC,UAAU,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC;IAEpF;;;;;OAKG;IACH,MAAM,cAAc,GAAG,iBAAiB,CAAC;IAEzC,MAAM,aAAa,GAAG,CAAC,GAAc,EAAQ,EAAE;QAC7C,yEAAyE;QACzE,gEAAgE;QAChE,0EAA0E;QAC1E,wEAAwE;QACxE,6BAA6B;QAC7B,IAAK,GAA0C,CAAC,cAAc,CAAC;YAAE,OAAO;QACxE,MAAM,CAAC,cAAc,CAAC,GAAG,EAAE,cAAc,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC;QAEnG,MAAM,YAAY,GAAG,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACxC,GAAG,CAAC,IAAI,GAAG,CAAC,CAAC,GAAG,KAAgB,EAAE,EAAE;YAClC,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;gBACzB,8DAA8D;gBAC9D,8DAA8D;gBAC9D,6DAA6D;gBAC7D,oEAAoE;gBACpE,MAAM,UAAU,GACd,IAAI,KAAK,IAAI;oBACb,OAAO,IAAI,KAAK,QAAQ;oBACxB,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;oBACpB,OAAQ,IAA6B,CAAC,MAAM,KAAK,QAAQ;oBACvD,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,IAA0B,CAAC;oBACxC,CAAC,CAAC,IAAI,CAAC;gBACX,MAAM,cAAc,GAAG,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,IAAI,UAAU,CAAC,CAAC,CAAC,KAAK,SAAS,CAAC;gBAChF,MAAM,cAAc,GAClB,UAAU,KAAK,IAAI;oBACnB,OAAO,UAAU,KAAK,QAAQ;oBAC9B,CAAC,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC;oBACzB,UAAiC,CAAC,KAAK,KAAK,SAAS,CAAC;gBACzD,IAAI,cAAc,IAAI,cAAc,EAAE,CAAC;oBACrC,CAAC,CAAC,YAAY,CAAC,aAAa,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,UAAU,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;gBAChF,CAAC;YACH,CAAC;YACD,OAAO,YAAY,CAAC,GAAI,KAAiB,CAAC,CAAC;QAC7C,CAAC,CAAoB,CAAC;IACxB,CAAC,CAAC;IAEF,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,EAAE,CAAC;QAC/B,aAAa,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IAC7B,CAAC;IAED,IAAI,gBAAgB,GAAG,CAAC,CAAC,SAAS,CAAC;IACnC,MAAM,CAAC,cAAc,CAAC,CAAC,EAAE,WAAW,EAAE;QACpC,YAAY,EAAE,IAAI;QAClB,GAAG;YACD,OAAO,gBAAgB,CAAC;QAC1B,CAAC;QACD,GAAG,CAAC,KAAgB;YAClB,gBAAgB,GAAG,KAAK,CAAC;YACzB,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;gBAAE,aAAa,CAAC,KAAK,CAAC,CAAC;QACjD,CAAC;KACF,CAAC,CAAC;IAEH,IAAI,WAAW,GAAG,CAAC,CAAC,IAAI,CAAC;IACzB,MAAM,CAAC,cAAc,CAAC,CAAC,EAAE,MAAM,EAAE;QAC/B,YAAY,EAAE,IAAI;QAClB,GAAG;YACD,OAAO,WAAW,CAAC;QACrB,CAAC;QACD,GAAG,CAAC,EAAgC;YAClC,WAAW,GAAG,CAAC,GAAG,IAAe,EAAE,EAAE;gBACnC,CAAC,CAAC,YAAY,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;gBAC/D,OAAO,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC;YACrB,CAAC,CAAC;QACJ,CAAC;KACF,CAAC,CAAC;AACL,CAAC"}