@mgcrea/mcp-unifi-protect 0.3.0 → 1.0.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 +65 -31
- package/dist/cli.js +19 -4
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +167 -253
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/{server-B1wSHfi7.js → server-DE3vnfF5.js} +652 -887
- package/dist/server-DE3vnfF5.js.map +1 -0
- package/package.json +6 -4
- package/dist/server-B1wSHfi7.js.map +0 -1
package/README.md
CHANGED
|
@@ -21,14 +21,14 @@ at all unless you ask for them.
|
|
|
21
21
|
|
|
22
22
|
## Two ways to connect
|
|
23
23
|
|
|
24
|
-
| | `local` (default)
|
|
25
|
-
| --------------------- |
|
|
26
|
-
| Reaches the console | directly on your LAN
|
|
27
|
-
| Credentials | host + username + password
|
|
28
|
-
| Auth mechanism | UniFi OS login → session cookie + CSRF token
|
|
29
|
-
| TLS | console's self-signed cert —
|
|
30
|
-
| Works off-LAN | no
|
|
31
|
-
| Session state on disk | yes, mode `600`
|
|
24
|
+
| | `local` (default) | `cloud` |
|
|
25
|
+
| --------------------- | ------------------------------------------------ | --------------------------------------- |
|
|
26
|
+
| Reaches the console | directly on your LAN | via `api.ui.com` Site Manager connector |
|
|
27
|
+
| Credentials | host + username + password | API key + console id |
|
|
28
|
+
| Auth mechanism | UniFi OS login → session cookie + CSRF token | `X-API-KEY` header |
|
|
29
|
+
| TLS | console's self-signed cert — pinned on first use | a real certificate, nothing to do |
|
|
30
|
+
| Works off-LAN | no | yes |
|
|
31
|
+
| Session state on disk | yes, mode `600` | none |
|
|
32
32
|
|
|
33
33
|
**Both modes expose exactly the same tools**, because both speak the same private
|
|
34
34
|
Protect API — the connector forwards the whole `/proxy/protect/...` tree, the private
|
|
@@ -51,7 +51,7 @@ UNIFI_PROTECT_CONSOLE_ID=… # curl -H "X-API-KEY: $KEY" https://api.ui.com/
|
|
|
51
51
|
UNIFI_PROTECT_HOST=192.168.1.1
|
|
52
52
|
UNIFI_PROTECT_USERNAME=mcp
|
|
53
53
|
UNIFI_PROTECT_PASSWORD=…
|
|
54
|
-
|
|
54
|
+
# TLS needs nothing: the console's certificate is pinned on the first request.
|
|
55
55
|
```
|
|
56
56
|
|
|
57
57
|
`UNIFI_PROTECT_MODE` is inferred as `cloud` when an API key and a console id are both
|
|
@@ -112,27 +112,40 @@ Integration API — see [Not implemented](#not-implemented).
|
|
|
112
112
|
|
|
113
113
|
## Security
|
|
114
114
|
|
|
115
|
-
**Supply chain.** Three runtime dependencies: the MCP SDK, zod, and
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
115
|
+
**Supply chain.** Three runtime dependencies: the MCP SDK, zod, and
|
|
116
|
+
[`@mgcrea/unifi-protect`](https://github.com/mgcrea/unifi-protect-client) — the console client,
|
|
117
|
+
which is ours and whose own dependencies are `undici`, `ws` and zod. There is no HTTP client
|
|
118
|
+
wrapper, no logger, no crypto library. That client used to be a copy inside this repo; the copy
|
|
119
|
+
and the original had already begun to drift, which is a bad way to hold knowledge about an
|
|
120
|
+
undocumented API. Published from CI with provenance via OIDC trusted publishing; the container
|
|
119
121
|
image is multi-arch, carries an SBOM, and is signed with cosign.
|
|
120
122
|
|
|
121
123
|
**Your credentials.** The username and password come from the environment or a config file, and
|
|
122
124
|
never leave this process except in the login request to your console. The resulting session
|
|
123
125
|
cookie is cached at `~/.config/unifi-protect/session.json` with mode `600`.
|
|
124
126
|
|
|
125
|
-
**Certificate verification is ON by default
|
|
127
|
+
**Certificate verification is ON by default, and now needs no setup.** The console's certificate
|
|
128
|
+
is _pinned_: on the first request the server reads it, records its SHA-256 fingerprint and PEM in
|
|
129
|
+
`~/.config/unifi-protect/trust.json` (mode `600`), and every connection afterwards verifies against
|
|
130
|
+
that one certificate as its own anchor. The host name check is replaced — not skipped — by a
|
|
131
|
+
fingerprint comparison, so addressing the console by IP is fine and a swapped certificate fails
|
|
132
|
+
hard.
|
|
133
|
+
|
|
134
|
+
This is what changed. The certificate is issued to `unifi.local` with **no IP SAN**, so reached by
|
|
135
|
+
an IP address, host name verification could never pass however the certificate was trusted; the
|
|
136
|
+
old advice was to set `NODE_EXTRA_CA_CERTS` _and_ address the console by a resolvable name, or
|
|
137
|
+
give up and disable verification. Pinning removes both requirements.
|
|
138
|
+
|
|
139
|
+
Pinning is trust-on-first-use, and it is only as good as that first moment. The fingerprint is
|
|
140
|
+
logged when it is learned and reported by `unifi_protect_auth_status`; set
|
|
141
|
+
`UNIFI_PROTECT_FINGERPRINT` to make the trust explicit instead, and a console that presents
|
|
142
|
+
anything else is then refused rather than adopted. If the console legitimately reissues its
|
|
143
|
+
certificate, delete the trust file.
|
|
144
|
+
|
|
145
|
+
`UNIFI_PROTECT_VERIFY_TLS=false` still turns checking off entirely, scoped to this server's own
|
|
126
146
|
requests through an undici dispatcher — it is not `NODE_TLS_REJECT_UNAUTHORIZED`, so nothing else
|
|
127
|
-
in the process is affected.
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
Verifying takes two things together, and either alone achieves nothing: the certificate is
|
|
131
|
-
self-signed, so `NODE_EXTRA_CA_CERTS` must point at it; **and** it is issued to `unifi.local` with
|
|
132
|
-
no IP SAN, so `UNIFI_PROTECT_HOST` must be a host name that resolves to the console rather than its
|
|
133
|
-
IP address. Reached by IP, verification fails on the host name however the certificate is trusted.
|
|
134
|
-
See `.env.example` for the two commands. If the console has no name on your network, set
|
|
135
|
-
`UNIFI_PROTECT_VERIFY_TLS=false`; the startup banner then prints `tls=UNVERIFIED` on every run.
|
|
147
|
+
in the process is affected. There should no longer be a reason to use it; the startup banner
|
|
148
|
+
prints `tls=UNVERIFIED` on every run when you do.
|
|
136
149
|
|
|
137
150
|
**Blast radius.** With the defaults, the worst an agent can do is read your cameras and write
|
|
138
151
|
image files into the snapshot directory. With `UNIFI_PROTECT_ALLOW_WRITES=1` it can additionally
|
|
@@ -147,7 +160,9 @@ Local-Access-Only account with View Only rights, and leave writes off unless you
|
|
|
147
160
|
| `UNIFI_PROTECT_USERNAME` | yes | — | Console login |
|
|
148
161
|
| `UNIFI_PROTECT_PASSWORD` | yes | — | Its password |
|
|
149
162
|
| `UNIFI_PROTECT_TOTP` | no | — | 2FA code. Expires in ~30s — prefer `unifi_protect_auth_login` |
|
|
150
|
-
| `UNIFI_PROTECT_VERIFY_TLS` | no | `true` | Verify the console's certificate
|
|
163
|
+
| `UNIFI_PROTECT_VERIFY_TLS` | no | `true` | Verify the console's certificate. Off disables pinning entirely |
|
|
164
|
+
| `UNIFI_PROTECT_FINGERPRINT` | no | — | Pin this SHA-256 up front instead of learning it on first use |
|
|
165
|
+
| `UNIFI_PROTECT_TRUST_FILE` | no | `~/.config/unifi-protect/trust.json` | Where the pinned certificate is remembered, mode 600 |
|
|
151
166
|
| `UNIFI_PROTECT_ALLOW_WRITES` | no | `false` | Register the 12 mutating tools |
|
|
152
167
|
| `UNIFI_PROTECT_SESSION_FILE` | no | `~/.config/unifi-protect/session.json` | Cached session, mode 600 |
|
|
153
168
|
| `UNIFI_PROTECT_SNAPSHOT_DIR` | no | `~/.cache/unifi-protect` | Where images and exports are written |
|
|
@@ -261,7 +276,11 @@ Two prompts carry the procedure, which is the part a tool list cannot express:
|
|
|
261
276
|
- **`check_camera_settings`** — run the audit and interpret it, changing nothing. Several
|
|
262
277
|
findings have two valid opposite fixes, and which is right depends on what the camera is for.
|
|
263
278
|
- **`who_passed`** — find who was present in a window, and **fall back to motion frames on any
|
|
264
|
-
camera whose detector is off** rather than reporting a zero count as an absence.
|
|
279
|
+
camera whose detector is off** rather than reporting a zero count as an absence. It takes the
|
|
280
|
+
question as one free-text argument, so **quote it**: slash-command arguments are split
|
|
281
|
+
shell-style and mapped positionally, so `who_passed in front of the house last night?` arrives
|
|
282
|
+
as just `"in"`, while `who_passed "in front of the house last night?"` arrives whole. A
|
|
283
|
+
single-word question is treated as that truncation and refused rather than answered.
|
|
265
284
|
|
|
266
285
|
## Worked example: what happened at the front door last night
|
|
267
286
|
|
|
@@ -383,7 +402,7 @@ returns the setup steps as data.
|
|
|
383
402
|
`UNIFI_PROTECT_ALLOW_WRITES=1`. That is the design, not a bug — an absent tool cannot be called,
|
|
384
403
|
whereas a refused one invites an agent to keep trying.
|
|
385
404
|
|
|
386
|
-
**`self-signed certificate` errors.**
|
|
405
|
+
**`self-signed certificate` errors.** These should no longer happen: the certificate is pinned on the first request, which works against an IP address. If one appears, the console is presenting a _different_ certificate than the one pinned — delete `~/.config/unifi-protect/trust.json` if it was legitimately reissued, and investigate if it was not
|
|
387
406
|
unless you have installed a trusted certificate on the console.
|
|
388
407
|
|
|
389
408
|
**Cloud mode returns 403 `user cannot access host in the organization`.** The key is
|
|
@@ -428,6 +447,15 @@ writes — each value set to the value it already held — and the device state
|
|
|
428
447
|
afterwards. That run is also what caught three bugs this README's earlier drafts described
|
|
429
448
|
wrongly: the storage layout, the event-thumbnail id, and two camera fields that do not exist.
|
|
430
449
|
|
|
450
|
+
**Certificate pinning** was verified against that same console on 2026-08-31, addressed by IP
|
|
451
|
+
(`192.168.6.3`) with verification ON — the case that was impossible before. Confirmed in one run:
|
|
452
|
+
the certificate is captured and its fingerprint logged on first contact; the trust file is written
|
|
453
|
+
mode `600`; a restart reuses the pin without re-capturing; a correct `UNIFI_PROTECT_FINGERPRINT`
|
|
454
|
+
(colons and all) is accepted; and a wrong one is refused with a message naming both fingerprints,
|
|
455
|
+
surfacing as a failed tool call rather than a server that never starts. `auth_status` reported
|
|
456
|
+
`reachable: true` on Protect 7.2.105 throughout, and a session cached by the previous release was
|
|
457
|
+
restored unchanged — the on-disk format did not move.
|
|
458
|
+
|
|
431
459
|
Two tools remain **unverified for want of hardware**, and are marked here rather than left to look
|
|
432
460
|
tested:
|
|
433
461
|
|
|
@@ -442,11 +470,17 @@ Both follow the same PATCH shape as the tools that were verified, so they are li
|
|
|
442
470
|
|
|
443
471
|
## Not implemented
|
|
444
472
|
|
|
445
|
-
The realtime WebSocket at `/proxy/protect/ws/updates` is not wired up
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
473
|
+
The realtime WebSocket at `/proxy/protect/ws/updates` is not wired up here, though
|
|
474
|
+
`@mgcrea/unifi-protect` now implements it — `connectEventStream` and `ProtectStore` are a
|
|
475
|
+
decoded frame stream and a live device cache, and this server deliberately uses neither.
|
|
476
|
+
|
|
477
|
+
The reason is that they are the wrong shape for an MCP server. Both want a long-lived process
|
|
478
|
+
that bootstraps at start-up and stays subscribed; this server must start with no credentials and
|
|
479
|
+
no connectivity at all, answering `unifi_protect_auth_status` until it is configured. It would
|
|
480
|
+
also gain little: because this wraps the private API, event _history_ is already available over
|
|
481
|
+
REST through `unifi_protect_list_events`, which is what a subscription would have been for.
|
|
482
|
+
Adopting it would mean deferring the bootstrap until credentials appear — worth doing if a tool
|
|
483
|
+
ever needs push rather than poll, and not before.
|
|
450
484
|
|
|
451
485
|
## Develop
|
|
452
486
|
|
package/dist/cli.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { A as isConfigured, H as BUILD_INFO, I as setupInstructions, i as createServer, j as loadConfig } from "./server-DE3vnfF5.js";
|
|
3
3
|
import { ZodError } from "zod";
|
|
4
|
-
import { StdioServerTransport } from "@modelcontextprotocol/
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
|
|
5
5
|
//#region src/cli.ts
|
|
6
6
|
const stderrLogger = {
|
|
7
7
|
debug: (...args) => {
|
|
8
8
|
if (process.env.UNIFI_PROTECT_DEBUG) console.error("[unifi-protect-mcp]", ...args);
|
|
9
9
|
},
|
|
10
|
+
info: (...args) => console.error("[unifi-protect-mcp]", ...args),
|
|
10
11
|
warn: (...args) => console.error("[unifi-protect-mcp]", ...args),
|
|
11
12
|
error: (...args) => console.error("[unifi-protect-mcp]", ...args)
|
|
12
13
|
};
|
|
@@ -18,16 +19,29 @@ const describeFatal = (err) => {
|
|
|
18
19
|
}).join("\n");
|
|
19
20
|
return err instanceof Error ? err.message : String(err);
|
|
20
21
|
};
|
|
22
|
+
/**
|
|
23
|
+
* How TLS will be handled, said once at startup.
|
|
24
|
+
*
|
|
25
|
+
* "pinned" is a promise about the first request rather than a report of one:
|
|
26
|
+
* the certificate is read lazily, because nothing here may open a socket — a
|
|
27
|
+
* console that is asleep has to surface as a failed tool call, not as a server
|
|
28
|
+
* that never starts.
|
|
29
|
+
*/
|
|
30
|
+
const tlsBanner = (config) => {
|
|
31
|
+
if (config.mode === "cloud") return "verified";
|
|
32
|
+
if (!config.verifyTls) return "UNVERIFIED";
|
|
33
|
+
return config.fingerprint ? "pinned (fingerprint configured)" : "pinned on first use";
|
|
34
|
+
};
|
|
21
35
|
const main = async () => {
|
|
22
36
|
stderrLogger.warn(`${BUILD_INFO.name}@${BUILD_INFO.version} (git ${BUILD_INFO.gitCommit} ${BUILD_INFO.gitCommitDate}, node ${process.version})`);
|
|
23
37
|
const config = loadConfig();
|
|
24
|
-
const { server } = createServer({
|
|
38
|
+
const { server, close } = createServer({
|
|
25
39
|
config,
|
|
26
40
|
logger: stderrLogger
|
|
27
41
|
});
|
|
28
42
|
const transport = new StdioServerTransport();
|
|
29
43
|
await server.connect(transport);
|
|
30
|
-
stderrLogger.warn(`unifi-protect-mcp connected (mode=${config.mode}, ` + (config.mode === "cloud" ? `console=${config.consoleId ?? "MISSING"}, auth=${config.apiKey ? "api-key" : "MISSING"}, ` : `host=${config.baseUrl ?? "MISSING"}, user=${config.username ?? "MISSING"}, `) + `writes=${config.allowWrites ? "ENABLED" : "disabled"}, tls=${config
|
|
44
|
+
stderrLogger.warn(`unifi-protect-mcp connected (mode=${config.mode}, ` + (config.mode === "cloud" ? `console=${config.consoleId ?? "MISSING"}, auth=${config.apiKey ? "api-key" : "MISSING"}, ` : `host=${config.baseUrl ?? "MISSING"}, user=${config.username ?? "MISSING"}, `) + `writes=${config.allowWrites ? "ENABLED" : "disabled"}, tls=${tlsBanner(config)})`);
|
|
31
45
|
for (const issue of config.issues) stderrLogger.warn(` ${issue}`);
|
|
32
46
|
if (!isConfigured(config)) {
|
|
33
47
|
stderrLogger.warn(" not configured — only unifi_protect_auth_status is available:");
|
|
@@ -36,6 +50,7 @@ const main = async () => {
|
|
|
36
50
|
}
|
|
37
51
|
const shutdown = (signal) => {
|
|
38
52
|
stderrLogger.warn(`received ${signal}, shutting down`);
|
|
53
|
+
close().catch(() => void 0);
|
|
39
54
|
process.exit(0);
|
|
40
55
|
};
|
|
41
56
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","names":[],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { StdioServerTransport } from \"@modelcontextprotocol/
|
|
1
|
+
{"version":3,"file":"cli.js","names":[],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { StdioServerTransport } from \"@modelcontextprotocol/server/stdio\";\nimport { ZodError } from \"zod\";\n\nimport { BUILD_INFO } from \"#/build-info\";\nimport { isConfigured, loadConfig, setupInstructions, type Config } from \"#/config\";\nimport { createServer } from \"#/server\";\n\n// Everything goes to stderr: stdout is the MCP protocol channel, and a stray\n// log line there corrupts the JSON-RPC stream — usually failing the client's\n// next parse, far from the cause.\n// oxlint-disable no-console -- this is the process entry point; stderr is the log channel.\nconst stderrLogger = {\n debug: (...args: unknown[]) => {\n if (process.env.UNIFI_PROTECT_DEBUG) console.error(\"[unifi-protect-mcp]\", ...args);\n },\n // Not optional despite nothing here calling it: @mgcrea/unifi-protect reports\n // a newly learned certificate fingerprint at info, and that is the one line\n // someone needs in order to pin it explicitly afterwards. Omitting `info`\n // drops it silently, because the client's Logger has every level optional.\n info: (...args: unknown[]) => console.error(\"[unifi-protect-mcp]\", ...args),\n warn: (...args: unknown[]) => console.error(\"[unifi-protect-mcp]\", ...args),\n error: (...args: unknown[]) => console.error(\"[unifi-protect-mcp]\", ...args),\n};\n\n/** Show a config mistake as its field messages, not 40 frames of zod internals. */\nconst describeFatal = (err: unknown): string => {\n if (err instanceof ZodError) {\n return err.issues\n .map((issue) => {\n const path = issue.path.join(\".\");\n return path ? `${path}: ${issue.message}` : issue.message;\n })\n .join(\"\\n\");\n }\n return err instanceof Error ? err.message : String(err);\n};\n\n/**\n * How TLS will be handled, said once at startup.\n *\n * \"pinned\" is a promise about the first request rather than a report of one:\n * the certificate is read lazily, because nothing here may open a socket — a\n * console that is asleep has to surface as a failed tool call, not as a server\n * that never starts.\n */\nconst tlsBanner = (config: Config): string => {\n if (config.mode === \"cloud\") return \"verified\";\n if (!config.verifyTls) return \"UNVERIFIED\";\n return config.fingerprint ? \"pinned (fingerprint configured)\" : \"pinned on first use\";\n};\n\nconst main = async (): Promise<void> => {\n stderrLogger.warn(\n `${BUILD_INFO.name}@${BUILD_INFO.version} (git ${BUILD_INFO.gitCommit} ${BUILD_INFO.gitCommitDate}, node ${process.version})`,\n );\n\n const config = loadConfig();\n\n const { server, close } = createServer({ config, logger: stderrLogger });\n const transport = new StdioServerTransport();\n await server.connect(transport);\n\n stderrLogger.warn(\n `unifi-protect-mcp connected (mode=${config.mode}, ` +\n (config.mode === \"cloud\"\n ? `console=${config.consoleId ?? \"MISSING\"}, auth=${config.apiKey ? \"api-key\" : \"MISSING\"}, `\n : `host=${config.baseUrl ?? \"MISSING\"}, user=${config.username ?? \"MISSING\"}, `) +\n `writes=${config.allowWrites ? \"ENABLED\" : \"disabled\"}, ` +\n `tls=${tlsBanner(config)})`,\n );\n\n // Connecting successfully but exposing one tool is confusing unless we say\n // why. The server no longer refuses to start over this, so the banner and\n // unifi_protect_auth_status are the only channels left.\n for (const issue of config.issues) stderrLogger.warn(` ${issue}`);\n\n if (!isConfigured(config)) {\n stderrLogger.warn(\" not configured — only unifi_protect_auth_status is available:\");\n for (const line of setupInstructions(config)) stderrLogger.warn(` ${line}`);\n stderrLogger.warn(\" Call unifi_protect_auth_status for this same guidance in your client.\");\n }\n\n const shutdown = (signal: string): void => {\n stderrLogger.warn(`received ${signal}, shutting down`);\n // Releases the dispatcher's keep-alive sockets. Best-effort: the exit below\n // is what actually ends the process, and a hung close must not delay it.\n void close().catch(() => undefined);\n process.exit(0);\n };\n process.on(\"SIGINT\", () => shutdown(\"SIGINT\"));\n process.on(\"SIGTERM\", () => shutdown(\"SIGTERM\"));\n};\n\nmain().catch((err: unknown) => {\n console.error(`[unifi-protect-mcp] fatal: ${describeFatal(err)}`);\n if (process.env.UNIFI_PROTECT_DEBUG && err instanceof Error) console.error(err.stack);\n process.exit(1);\n});\n"],"mappings":";;;;;AAYA,MAAM,eAAe;CACnB,QAAQ,GAAG,SAAoB;EAC7B,IAAI,QAAQ,IAAI,qBAAqB,QAAQ,MAAM,uBAAuB,GAAG,IAAI;CACnF;CAKA,OAAO,GAAG,SAAoB,QAAQ,MAAM,uBAAuB,GAAG,IAAI;CAC1E,OAAO,GAAG,SAAoB,QAAQ,MAAM,uBAAuB,GAAG,IAAI;CAC1E,QAAQ,GAAG,SAAoB,QAAQ,MAAM,uBAAuB,GAAG,IAAI;AAC7E;;AAGA,MAAM,iBAAiB,QAAyB;CAC9C,IAAI,eAAe,UACjB,OAAO,IAAI,OACR,KAAK,UAAU;EACd,MAAM,OAAO,MAAM,KAAK,KAAK,GAAG;EAChC,OAAO,OAAO,GAAG,KAAK,IAAI,MAAM,YAAY,MAAM;CACpD,CAAC,CAAC,CACD,KAAK,IAAI;CAEd,OAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AACxD;;;;;;;;;AAUA,MAAM,aAAa,WAA2B;CAC5C,IAAI,OAAO,SAAS,SAAS,OAAO;CACpC,IAAI,CAAC,OAAO,WAAW,OAAO;CAC9B,OAAO,OAAO,cAAc,oCAAoC;AAClE;AAEA,MAAM,OAAO,YAA2B;CACtC,aAAa,KACX,GAAG,WAAW,KAAK,GAAG,WAAW,QAAQ,QAAQ,WAAW,UAAU,GAAG,WAAW,cAAc,SAAS,QAAQ,QAAQ,EAC7H;CAEA,MAAM,SAAS,WAAW;CAE1B,MAAM,EAAE,QAAQ,UAAU,aAAa;EAAE;EAAQ,QAAQ;CAAa,CAAC;CACvE,MAAM,YAAY,IAAI,qBAAqB;CAC3C,MAAM,OAAO,QAAQ,SAAS;CAE9B,aAAa,KACX,qCAAqC,OAAO,KAAK,OAC9C,OAAO,SAAS,UACb,WAAW,OAAO,aAAa,UAAU,SAAS,OAAO,SAAS,YAAY,UAAU,MACxF,QAAQ,OAAO,WAAW,UAAU,SAAS,OAAO,YAAY,UAAU,OAC9E,UAAU,OAAO,cAAc,YAAY,WAAW,QAC/C,UAAU,MAAM,EAAE,EAC7B;CAKA,KAAK,MAAM,SAAS,OAAO,QAAQ,aAAa,KAAK,KAAK,OAAO;CAEjE,IAAI,CAAC,aAAa,MAAM,GAAG;EACzB,aAAa,KAAK,iEAAiE;EACnF,KAAK,MAAM,QAAQ,kBAAkB,MAAM,GAAG,aAAa,KAAK,KAAK,MAAM;EAC3E,aAAa,KAAK,yEAAyE;CAC7F;CAEA,MAAM,YAAY,WAAyB;EACzC,aAAa,KAAK,YAAY,OAAO,gBAAgB;EAGrD,MAAW,CAAC,CAAC,YAAY,KAAA,CAAS;EAClC,QAAQ,KAAK,CAAC;CAChB;CACA,QAAQ,GAAG,gBAAgB,SAAS,QAAQ,CAAC;CAC7C,QAAQ,GAAG,iBAAiB,SAAS,SAAS,CAAC;AACjD;AAEA,KAAK,CAAC,CAAC,OAAO,QAAiB;CAC7B,QAAQ,MAAM,8BAA8B,cAAc,GAAG,GAAG;CAChE,IAAI,QAAQ,IAAI,uBAAuB,eAAe,OAAO,QAAQ,MAAM,IAAI,KAAK;CACpF,QAAQ,KAAK,CAAC;AAChB,CAAC"}
|