cuonztech-privacy-chaos-gate 0.0.0-stage → 0.2.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.
- package/README.md +142 -2
- package/action.yml +109 -0
- package/dist/chunk-FN3QUFUZ.js +36 -0
- package/dist/chunk-FN3QUFUZ.js.map +1 -0
- package/dist/chunk-T5D6BUAC.js +4514 -0
- package/dist/chunk-T5D6BUAC.js.map +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +122 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +1545 -0
- package/dist/index.js +77 -0
- package/dist/index.js.map +1 -0
- package/dist/resolveGrant-6U6VG3DM.js +230 -0
- package/dist/resolveGrant-6U6VG3DM.js.map +1 -0
- package/docs/BRAND-PILOT-EVIDENCE.md +122 -0
- package/docs/LIZENZ-KAUFFLUSS.md +105 -0
- package/docs/PREISMODELL.md +63 -0
- package/package.json +68 -4
package/README.md
CHANGED
|
@@ -1,3 +1,143 @@
|
|
|
1
|
-
#
|
|
1
|
+
# CuonzTech Privacy Chaos Gate (CPCG)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
CI-Gate und Auto-Fix für Consent-Tracker: findet Requests, die **vor** der Einwilligung
|
|
4
|
+
firen — und liefert den Fix als Pull Request. Prüft auch die Gegenrichtung:
|
|
5
|
+
läuft erlaubtes Tracking nach dem Akzeptieren korrekt an?
|
|
6
|
+
|
|
7
|
+
> **Hinweis:** Technisches Hilfsmittel, **keine Rechtsberatung**, keine Garantie für
|
|
8
|
+
> revDSG-/DSGVO-/TTDSG-Konformität. / Technical aid, **not legal advice**.
|
|
9
|
+
|
|
10
|
+
## Installieren
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pnpm add -D @cuonztech/privacy-chaos-gate
|
|
14
|
+
# oder direkt
|
|
15
|
+
npx @cuonztech/privacy-chaos-gate scan --url https://staging.example.com --license CPCG-…
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Lizenz
|
|
19
|
+
|
|
20
|
+
Online-Kauf → Code per Mail.
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
export CPCG_LICENSE="CPCG-…"
|
|
24
|
+
cpcg scan --url https://staging.example.com --jurisdiction CH
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Ohne Lizenz: localhost/Test-Fixtures; Produktiv-URLs mit Warnung (Trial-Modus).
|
|
28
|
+
|
|
29
|
+
## Was das Tool prüft
|
|
30
|
+
|
|
31
|
+
15 Chaos-Szenarien in einem Lauf — jedes mit ehrlichem Status
|
|
32
|
+
(**bestanden / auffällig / nicht prüfbar**). Ein Button, der nicht gefunden wird,
|
|
33
|
+
wird nie zu einem grünen Häkchen.
|
|
34
|
+
|
|
35
|
+
| # | Szenario | Frage |
|
|
36
|
+
|---|----------|-------|
|
|
37
|
+
| 1 | happy-path-denied | Bleibt Tracking nach «Ablehnen» aus? |
|
|
38
|
+
| 2 | happy-path-accepted | Ist vor dem Klick Ruhe — und läuft erlaubtes Tracking danach an? |
|
|
39
|
+
| 3 | fast-click | Navigation vor dem Banner: feuert dann schon etwas? |
|
|
40
|
+
| 4 | state-manipulation | Tracking nach Storage-Wipe? |
|
|
41
|
+
| 5 | network-drop | Tracker trotz blockiertem CMP? |
|
|
42
|
+
| 6 | consent-revoked | Stoppt Tracking nach Widerruf? Welche IDs bleiben liegen? |
|
|
43
|
+
| 7 | category-selective | Nur Statistik erlaubt — bleiben Marketing-Tracker aus? |
|
|
44
|
+
| 8 | consent-persistence | Überlebt die Entscheidung Reload, Unterseite und Neustart? |
|
|
45
|
+
| 9 | banner-ignored | Starten Tracker per Timer/Scroll ohne Klick? |
|
|
46
|
+
| 10 | subpage-spa | Greift der Schutz auf Unterseiten und bei SPA-Navigation? |
|
|
47
|
+
| 11 | cmp-failure | Tracking-Zeitfenster bei verzögerter/defekter CMP? |
|
|
48
|
+
| 12 | embed-leaks | Kontaktieren YouTube/Maps/Social-Embeds vor Freigabe? |
|
|
49
|
+
| 13 | consent-mode-v2 | Decken sich Consent-Mode-Zustände mit Cookies/Requests? |
|
|
50
|
+
| 14 | duplicate-events | Doppelte page_view-Events (Plugin + GTM parallel)? |
|
|
51
|
+
| 15 | mobile-usability | Sind Ablehnen/Einstellungen auf dem Handy bedienbar? |
|
|
52
|
+
|
|
53
|
+
**Tracker (Auswahl):** Google Tag Manager, Google Analytics, Meta, TikTok, LinkedIn,
|
|
54
|
+
Hotjar, Criteo, Marketo, HubSpot, Matomo.
|
|
55
|
+
|
|
56
|
+
**CMPs (Auswahl):** OneTrust, Cookiebot, Usercentrics, Tealium, iubenda, etracker,
|
|
57
|
+
JustRelate, Didomi, Axeptio, Sourcepoint, Consentmanager, Borlabs, Klaro.
|
|
58
|
+
|
|
59
|
+
## Output: der Prüfbericht
|
|
60
|
+
|
|
61
|
+
Jeder Scan schreibt ein Evidence-Paket:
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
report.json LeakReport (maschinenlesbar)
|
|
65
|
+
report-evidence.md Kunden-Report (Markdown)
|
|
66
|
+
report-evidence.html Kunden-Report (HTML, CuonzTech-Brand)
|
|
67
|
+
evidence/ Screenshots (portabel, relative Pfade)
|
|
68
|
+
report-deluxe.pdf Deluxe-PDF mit Deckblatt (mit --report-pdf)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Der Report führt **Befunde** (Regelverstösse) und **beobachtete Requests**
|
|
72
|
+
(Timelines) getrennt — und ordnet jedes Ergebnis mehreren Rechtsräumen zu
|
|
73
|
+
(CH, EU, UK, US-CA, BR, ROW). Keine globale Compliance-Aussage.
|
|
74
|
+
|
|
75
|
+
## Flags
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
cpcg scan \
|
|
79
|
+
--url https://staging.example.com \
|
|
80
|
+
--license CPCG-… \
|
|
81
|
+
--report-pdf # Deluxe-PDF (Deckblatt + Evidence) erzeugen
|
|
82
|
+
--client "Firma AG" # Kundenname fürs Deckblatt
|
|
83
|
+
--demo # Report als synthetische Testdaten kennzeichnen
|
|
84
|
+
--jurisdiction CH # CH | EU | UK | US-CA | BR | ROW
|
|
85
|
+
--auto-fix true # Auto-Fix generieren
|
|
86
|
+
--fail-on-leak true # Exit 1 bei Leaks (CI-Gate)
|
|
87
|
+
--report-output reports/scan.json
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Demo-Portal
|
|
91
|
+
|
|
92
|
+
Reproduzierbare Testseite mit fünf absichtlichen Consent-Leaks
|
|
93
|
+
(Pre-Consent-Bootstrap, Timer, Widerruf, Embeds, doppelte Events):
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
node demo-portal/server.mjs # http://127.0.0.1:8787
|
|
97
|
+
pnpm exec tsx scripts/scan-demo-portal.mts # voller Scan + Report
|
|
98
|
+
pnpm exec tsx scripts/make-deluxe-report.mts # Deluxe-PDF daraus
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Erwartet: Gate **BLOCKED**, Szenarien «auffällig» — exakt dieselben Zahlen bei jedem Lauf.
|
|
102
|
+
|
|
103
|
+
## GitHub Action
|
|
104
|
+
|
|
105
|
+
```yaml
|
|
106
|
+
- uses: cuonztech/privacy-chaos-gate@v1
|
|
107
|
+
with:
|
|
108
|
+
staging-url: ${{ vars.STAGING_URL }}
|
|
109
|
+
license: ${{ secrets.CPCG_LICENSE }}
|
|
110
|
+
report-pdf: true
|
|
111
|
+
client-name: "Firma AG"
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Die Action nutzt `dist/cli.js` aus dem Action-Path; erst bei fehlendem Build greift `npx`.
|
|
115
|
+
|
|
116
|
+
## Jurisdiktion
|
|
117
|
+
|
|
118
|
+
`--jurisdiction CH|EU|UK|US-CA|BR|ROW` — gleicher Befund, unterschiedliche Einordnung:
|
|
119
|
+
|
|
120
|
+
- **EU:** Opt-In-Pflicht für nicht-essenzielle Tracker. Ein beobachteter Request ist ein
|
|
121
|
+
schwerwiegender Hinweis — die Einstufung als Verstoß erfordert die Prüfung von Zweck,
|
|
122
|
+
Technologie und Consent-Zustand. Consent Mode ist eine technische Implementierung,
|
|
123
|
+
keine Rechtsgrundlage.
|
|
124
|
+
- **CH:** Risiko nach revDSG-Praxis; der EDÖB verlangt insbesondere bei personalisierter
|
|
125
|
+
Werbung eine Einwilligung.
|
|
126
|
+
- **US-CA:** Opt-out-Modell. Ohne Prüfung von Sale/Sharing-Opt-out und GPC ist die
|
|
127
|
+
Bewertung unvollständig.
|
|
128
|
+
- **ROW:** Nicht bewertet — länderspezifische Prüfung erforderlich.
|
|
129
|
+
|
|
130
|
+
## Entwicklung
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
pnpm install
|
|
134
|
+
pnpm test # 136 Tests
|
|
135
|
+
pnpm build
|
|
136
|
+
pnpm exec tsx scripts/e2e-cli-deluxe.mts # E2E: echte CLI gegen Demo-Portal
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Secrets (Stripe, Lizenz-Secret) nur über Env/KeePass — nie im Repo.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
© CuonzTech · Carlo Cuonz · [cuonztech.ch](https://cuonztech.ch)
|
package/action.yml
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
name: "Cuonztech Privacy Chaos Gate"
|
|
2
|
+
description: "Agentic CI/CD Privacy Chaos Engineering & Auto-Remediation Gate for Cookie/Consent Leaks"
|
|
3
|
+
author: "Cuonztech"
|
|
4
|
+
branding:
|
|
5
|
+
icon: "shield"
|
|
6
|
+
color: "purple"
|
|
7
|
+
|
|
8
|
+
inputs:
|
|
9
|
+
staging-url:
|
|
10
|
+
description: "The URL of the staging or localhost web environment to stress-test"
|
|
11
|
+
required: true
|
|
12
|
+
api-key:
|
|
13
|
+
description: "API key for the auto-remediation LLM (e.g. OpenAI, Anthropic, Gemini)"
|
|
14
|
+
required: false
|
|
15
|
+
default: ""
|
|
16
|
+
llm-provider:
|
|
17
|
+
description: "LLM Provider to use for auto-remediation ('openai', 'anthropic', 'gemini')"
|
|
18
|
+
required: false
|
|
19
|
+
default: "openai"
|
|
20
|
+
llm-model:
|
|
21
|
+
description: "Model to use for auto-remediation"
|
|
22
|
+
required: false
|
|
23
|
+
default: "gpt-4o"
|
|
24
|
+
auto-fix:
|
|
25
|
+
description: "Automatically generate and apply code patches when tracker leaks are detected"
|
|
26
|
+
required: false
|
|
27
|
+
default: "true"
|
|
28
|
+
fail-on-leak:
|
|
29
|
+
description: "Block the build (exit code 1) if unpatched critical leaks remain"
|
|
30
|
+
required: false
|
|
31
|
+
default: "true"
|
|
32
|
+
report-output:
|
|
33
|
+
description: "File path where the JSON LeakReport will be written"
|
|
34
|
+
required: false
|
|
35
|
+
default: "reports/privacy-chaos-report.json"
|
|
36
|
+
target-source-dir:
|
|
37
|
+
description: "Root directory of the application source code to inspect and patch"
|
|
38
|
+
required: false
|
|
39
|
+
default: "."
|
|
40
|
+
git-commit:
|
|
41
|
+
description: "Automatically commit generated privacy fixes into the branch"
|
|
42
|
+
required: false
|
|
43
|
+
default: "false"
|
|
44
|
+
license:
|
|
45
|
+
description: "CuonzTech CPCG Lizenzcode (CPCG-…) — aus Secrets, nach Online-Kauf"
|
|
46
|
+
required: false
|
|
47
|
+
default: ""
|
|
48
|
+
report-pdf:
|
|
49
|
+
description: "Deluxe-PDF (Deckblatt + Evidence) nach dem Scan erzeugen"
|
|
50
|
+
required: false
|
|
51
|
+
default: "false"
|
|
52
|
+
client-name:
|
|
53
|
+
description: "Kundenname für das PDF-Deckblatt"
|
|
54
|
+
required: false
|
|
55
|
+
default: ""
|
|
56
|
+
jurisdiction:
|
|
57
|
+
description: "Rechtsraum für Bewertung: CH | EU | UK | US-CA | BR | ROW"
|
|
58
|
+
required: false
|
|
59
|
+
default: "CH"
|
|
60
|
+
|
|
61
|
+
outputs:
|
|
62
|
+
leaks-found:
|
|
63
|
+
description: "'true' if leaks were detected during chaos tests"
|
|
64
|
+
value: ${{ steps.cpcg-run.outputs.leaks_found }}
|
|
65
|
+
leaks-count:
|
|
66
|
+
description: "Number of tracker leaks detected"
|
|
67
|
+
value: ${{ steps.cpcg-run.outputs.leaks_count }}
|
|
68
|
+
patch-applied:
|
|
69
|
+
description: "'true' if an auto-remediation patch was successfully applied"
|
|
70
|
+
value: ${{ steps.cpcg-run.outputs.patch_applied }}
|
|
71
|
+
report-path:
|
|
72
|
+
description: "Absolute or relative path to the generated JSON report"
|
|
73
|
+
value: ${{ steps.cpcg-run.outputs.report_path }}
|
|
74
|
+
|
|
75
|
+
runs:
|
|
76
|
+
using: "composite"
|
|
77
|
+
steps:
|
|
78
|
+
- name: Setup Node.js
|
|
79
|
+
uses: actions/setup-node@v4
|
|
80
|
+
with:
|
|
81
|
+
node-version: "20"
|
|
82
|
+
|
|
83
|
+
- name: Run Privacy Chaos Gate
|
|
84
|
+
id: cpcg-run
|
|
85
|
+
shell: bash
|
|
86
|
+
env:
|
|
87
|
+
CPCG_LLM_API_KEY: ${{ inputs.api-key }}
|
|
88
|
+
CPCG_LLM_PROVIDER: ${{ inputs.llm-provider }}
|
|
89
|
+
CPCG_LLM_MODEL: ${{ inputs.llm-model }}
|
|
90
|
+
CPCG_LICENSE: ${{ inputs.license }}
|
|
91
|
+
run: |
|
|
92
|
+
CLI_PATH="${{ github.action_path }}/dist/cli.js"
|
|
93
|
+
if [ -f "$CLI_PATH" ]; then
|
|
94
|
+
RUNNER="node $CLI_PATH"
|
|
95
|
+
else
|
|
96
|
+
RUNNER="npx @cuonztech/privacy-chaos-gate"
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
$RUNNER \
|
|
100
|
+
--url "${{ inputs.staging-url }}" \
|
|
101
|
+
--auto-fix "${{ inputs.auto-fix }}" \
|
|
102
|
+
--fail-on-leak "${{ inputs.fail-on-leak }}" \
|
|
103
|
+
--report-output "${{ inputs.report-output }}" \
|
|
104
|
+
--source-dir "${{ inputs.target-source-dir }}" \
|
|
105
|
+
--git-commit "${{ inputs.git-commit }}" \
|
|
106
|
+
--license "${{ inputs.license }}" \
|
|
107
|
+
--report-pdf "${{ inputs.report-pdf }}" \
|
|
108
|
+
--client "${{ inputs.client-name }}" \
|
|
109
|
+
--jurisdiction "${{ inputs.jurisdiction }}"
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// src/core/license/ed25519.ts
|
|
2
|
+
import crypto from "crypto";
|
|
3
|
+
|
|
4
|
+
// src/core/license/publicKey.ts
|
|
5
|
+
var LICENSE_PUBLIC_KEY = "q20XyLRZGURVvMLR6q0SCIc6poLhW73KE5Ds94Yq1FQ=";
|
|
6
|
+
|
|
7
|
+
// src/core/license/ed25519.ts
|
|
8
|
+
var PKCS8_PREFIX = Buffer.from("302e020100300506032b657004220420", "hex");
|
|
9
|
+
var SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
|
|
10
|
+
function rawKey(b64, what) {
|
|
11
|
+
const raw = Buffer.from(b64, "base64");
|
|
12
|
+
if (raw.length !== 32) throw new Error(`${what}: erwartet 32 Byte (base64), erhalten ${raw.length}`);
|
|
13
|
+
return raw;
|
|
14
|
+
}
|
|
15
|
+
function defaultPublicKey() {
|
|
16
|
+
return process.env.CPCG_LICENSE_PUBLIC_KEY || LICENSE_PUBLIC_KEY;
|
|
17
|
+
}
|
|
18
|
+
function verifyEd25519(domain, payload, sigB64url, publicKeyB64 = defaultPublicKey()) {
|
|
19
|
+
try {
|
|
20
|
+
const key = crypto.createPublicKey({
|
|
21
|
+
key: Buffer.concat([SPKI_PREFIX, rawKey(publicKeyB64, "Public Key")]),
|
|
22
|
+
format: "der",
|
|
23
|
+
type: "spki"
|
|
24
|
+
});
|
|
25
|
+
const sig = Buffer.from(sigB64url, "base64url");
|
|
26
|
+
return sig.length === 64 && crypto.verify(null, Buffer.from(`${domain}.${payload}`), key, sig);
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export {
|
|
33
|
+
defaultPublicKey,
|
|
34
|
+
verifyEd25519
|
|
35
|
+
};
|
|
36
|
+
//# sourceMappingURL=chunk-FN3QUFUZ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/license/ed25519.ts","../src/core/license/publicKey.ts"],"sourcesContent":["import crypto from \"node:crypto\";\nimport { LICENSE_PUBLIC_KEY } from \"./publicKey.js\";\n\n/**\n * Ed25519-Signaturen für Lizenzen und Grants.\n *\n * Der Server signiert mit dem privaten Seed (nur in config.php / KeePass),\n * der Client prüft ausschliesslich mit dem öffentlichen Schlüssel. Wer das CLI\n * besitzt, kann damit nichts ausstellen. Schlüssel sind rohe 32 Byte, base64.\n *\n * Jeder Token-Typ signiert mit eigenem Domain-Präfix, damit eine Lizenz-\n * Signatur nie als Grant (oder umgekehrt) durchgeht.\n */\nexport type SignDomain = \"cpcg-license-v1\" | \"cpcg-grant-v1\";\n\nconst PKCS8_PREFIX = Buffer.from(\"302e020100300506032b657004220420\", \"hex\");\nconst SPKI_PREFIX = Buffer.from(\"302a300506032b6570032100\", \"hex\");\n\nfunction rawKey(b64: string, what: string): Buffer {\n const raw = Buffer.from(b64, \"base64\");\n if (raw.length !== 32) throw new Error(`${what}: erwartet 32 Byte (base64), erhalten ${raw.length}`);\n return raw;\n}\n\nexport function generateLicenseKeyPair(): { seed: string; publicKey: string } {\n const { privateKey, publicKey } = crypto.generateKeyPairSync(\"ed25519\");\n return {\n seed: privateKey.export({ format: \"der\", type: \"pkcs8\" }).subarray(-32).toString(\"base64\"),\n publicKey: publicKey.export({ format: \"der\", type: \"spki\" }).subarray(-32).toString(\"base64\"),\n };\n}\n\nexport function signEd25519(domain: SignDomain, payload: string, seedB64: string): string {\n const key = crypto.createPrivateKey({\n key: Buffer.concat([PKCS8_PREFIX, rawKey(seedB64, \"CPCG_LICENSE_SIGNING_SEED\")]),\n format: \"der\",\n type: \"pkcs8\",\n });\n return crypto.sign(null, Buffer.from(`${domain}.${payload}`), key).toString(\"base64url\");\n}\n\n/** Öffentlicher Schlüssel: CPCG_LICENSE_PUBLIC_KEY (Tests, Self-Hosting) vor dem eingebetteten. */\nexport function defaultPublicKey(): string {\n return process.env.CPCG_LICENSE_PUBLIC_KEY || LICENSE_PUBLIC_KEY;\n}\n\nexport function verifyEd25519(\n domain: SignDomain,\n payload: string,\n sigB64url: string,\n publicKeyB64: string = defaultPublicKey()\n): boolean {\n try {\n const key = crypto.createPublicKey({\n key: Buffer.concat([SPKI_PREFIX, rawKey(publicKeyB64, \"Public Key\")]),\n format: \"der\",\n type: \"spki\",\n });\n const sig = Buffer.from(sigB64url, \"base64url\");\n return sig.length === 64 && crypto.verify(null, Buffer.from(`${domain}.${payload}`), key, sig);\n } catch {\n return false;\n }\n}\n","/**\n * Öffentlicher Ed25519-Schlüssel des Cuonztech-Lizenzdienstes (32 Byte, base64).\n * Der zugehörige private Seed liegt nur serverseitig (config.php) und im KeePass.\n */\nexport const LICENSE_PUBLIC_KEY = \"q20XyLRZGURVvMLR6q0SCIc6poLhW73KE5Ds94Yq1FQ=\";\n"],"mappings":";AAAA,OAAO,YAAY;;;ACIZ,IAAM,qBAAqB;;;ADWlC,IAAM,eAAe,OAAO,KAAK,oCAAoC,KAAK;AAC1E,IAAM,cAAc,OAAO,KAAK,4BAA4B,KAAK;AAEjE,SAAS,OAAO,KAAa,MAAsB;AACjD,QAAM,MAAM,OAAO,KAAK,KAAK,QAAQ;AACrC,MAAI,IAAI,WAAW,GAAI,OAAM,IAAI,MAAM,GAAG,IAAI,yCAAyC,IAAI,MAAM,EAAE;AACnG,SAAO;AACT;AAoBO,SAAS,mBAA2B;AACzC,SAAO,QAAQ,IAAI,2BAA2B;AAChD;AAEO,SAAS,cACd,QACA,SACA,WACA,eAAuB,iBAAiB,GAC/B;AACT,MAAI;AACF,UAAM,MAAM,OAAO,gBAAgB;AAAA,MACjC,KAAK,OAAO,OAAO,CAAC,aAAa,OAAO,cAAc,YAAY,CAAC,CAAC;AAAA,MACpE,QAAQ;AAAA,MACR,MAAM;AAAA,IACR,CAAC;AACD,UAAM,MAAM,OAAO,KAAK,WAAW,WAAW;AAC9C,WAAO,IAAI,WAAW,MAAM,OAAO,OAAO,MAAM,OAAO,KAAK,GAAG,MAAM,IAAI,OAAO,EAAE,GAAG,KAAK,GAAG;AAAA,EAC/F,QAAQ;AACN,WAAO;AAAA,EACT;AACF;","names":[]}
|