@lacneu/wix-openclaw 0.2.2 → 0.4.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 CHANGED
@@ -15,10 +15,10 @@
15
15
  ## Overview
16
16
 
17
17
  `wix-openclaw` is an OpenClaw plugin that lets your agent operate a Wix site
18
- through the official Wix REST API. It registers ~50 tools split across a dozen
18
+ through the official Wix REST API. It registers ~60 tools split across a dozen
19
19
  Wix products (Blog, CMS Data, Forms, Bookings, Contacts, Events, Reviews, FAQ,
20
- Multilingual, Sites, Site Media), plus a `wix_design_brief` helper that turns
21
- loose textual briefs into structured markdown artefacts.
20
+ Multilingual, Sites, Site Media, SEO), plus a `wix_design_brief` helper that
21
+ turns loose textual briefs into structured markdown artefacts.
22
22
 
23
23
  Every tool funnels through a single authenticated HTTP client that:
24
24
 
@@ -175,6 +175,21 @@ should stay unticked.
175
175
  Tick these only if the corresponding Wix product is actually in use on
176
176
  the target site:
177
177
 
178
+ - [ ] **Manage SEO** (`SCOPE.PROMOTE.MANAGE-SEO`) — **only if you set
179
+ `enableSeoTools: true`**, which registers **nineteen** tools of which
180
+ **eleven write irreversibly** (four on redirects, seven on tags and
181
+ patterns; a pattern write re-renders every page of its type). In the default
182
+ configuration the SEO tools are not registered at all, so granting this scope would hand the key the power to
183
+ delete redirects for tools that cannot run. Wix exposes a **single** SEO
184
+ scope, not a read/write pair: ticking it grants redirect creation and deletion
185
+ along with the read-only audit, so there is no such thing as an audit-only SEO
186
+ key. In the Wix UI the checkbox sits under **All site permissions**, between
187
+ *Social Posts* and *Import orders*; its label is currently untranslated and
188
+ renders literally as `site.scope.SCOPE.PROMOTE.MANAGE-SEO.name`. Since Wix
189
+ cannot narrow it, the read/write boundary is enforced by this plugin instead —
190
+ each of the eleven writing tools refuses until what it would destroy, be it a
191
+ redirect, a tag or a template, has been named and acknowledged.
192
+
178
193
  - [ ] **Pricing Plans** — if you sell subscription packages
179
194
  - [ ] **Wix Stores** + **Wix eCommerce** — if you sell physical/digital
180
195
  products. Without these, the `wix_*` tools that touch e-commerce
@@ -225,6 +240,7 @@ columns and remove the matching tools from your agent's allow-list.
225
240
  | `wix_reviews_*` | Wix Reviews | Reviews moderation |
226
241
  | `wix_faq_*` | Manage FAQ | FAQ tooling |
227
242
  | `wix_multilingual_*` | Multilingual Translation Schema Read | Translation reads |
243
+ | `wix_seo_*` (all nineteen) | Manage SEO (`SCOPE.PROMOTE.MANAGE-SEO`) | SEO audit **and** redirect changes — Wix has no read-only SEO scope, so this one permission covers both |
228
244
  | `wix_design_brief` | _(none — no API call)_ | Always |
229
245
 
230
246
  ### Wix app installation requirements
@@ -237,7 +253,7 @@ working as soon as the app is installed (no plugin upgrade needed).
237
253
  | Tool group | Wix app required | Error returned when missing |
238
254
  |---|---|---|
239
255
  | `wix_blog_*` | Wix Blog | `404` |
240
- | `wix_data_*` | Velo / Wix Code (enable in Editor) | `400 WDE0110: Wix Code not enabled` |
256
+ | `wix_data_*` | Wix CMS (Velo / Wix Code enable in Editor) | `400 WDE0110: Wix CMS app is not installed for site` |
241
257
  | `wix_forms_*` | Wix Forms | `404` |
242
258
  | `wix_bookings_*` | Wix Bookings | `404` |
243
259
  | `wix_events_*` | Wix Events | `428 WIX_EVENTS_APP_NOT_INSTALLED` |
@@ -338,7 +354,9 @@ site; `app-not-installed` and `velo-not-enabled` mean the tool's
338
354
  endpoint exists but its backing Wix product is not active on the
339
355
  smoke-test site (the request still reaches Wix and the path/headers
340
356
  are correct); `docs` means write/destructive endpoints that we do not
341
- exercise live.
357
+ exercise live. The SEO reads are skipped by the smoke test unless
358
+ `WIX_SMOKE_SEO=1` is set, so a key without `SCOPE.PROMOTE.MANAGE-SEO` is not
359
+ failed for lacking a scope the default configuration never uses.
342
360
 
343
361
  ### Blog
344
362
 
@@ -455,15 +473,98 @@ Requires the Wix FAQ app (returns `400 App is not installed` otherwise).
455
473
 
456
474
  ### Multilingual (read-only in V1)
457
475
 
458
- Returns `403` on the Ataraxis smoke-test site likely missing the
459
- `Multilingual Translation Schema Read` permission on the API key
460
- rather than an app-not-installed condition.
476
+ Returns `403` on the Ataraxis smoke-test site, with an EMPTY body. The earlier
477
+ guess — a missing `Multilingual Translation Schema Read` permission has since
478
+ been ruled out: that permission is granted on the key and the `403` persists.
479
+ What remains undetermined is whether the Wix Multilingual app is simply not
480
+ installed, or whether these two endpoints also require the separate
481
+ `SCOPE.MULTILINGUAL.MANAGE_TRANSLATIONS` scope. The empty body does not say, and
482
+ neither answer has been tested.
461
483
 
462
484
  | Tool | What it does | Approval | Validated |
463
485
  |---|---|---|---|
464
486
  | `wix_multilingual_list_languages` | List configured languages; standard query envelope | — | 403-permission |
465
487
  | `wix_multilingual_list_schemas` | List translation schemas | — | 403-permission |
466
488
 
489
+ ### SEO
490
+
491
+ > **Developer Preview, and OFF by default.** Every Wix endpoint behind these
492
+ > nineteen tools is listed in Wix's
493
+ > [Developer Preview](https://dev.wix.com/docs/api-reference/articles/work-with-wix-apis/platform/about-developer-preview)
494
+ > — Item SEO Tags, Site SEO Tags, SEO Patterns and all six Redirects methods.
495
+ > Wix may change
496
+ > them at any time, and states plainly that you **shouldn't use them on live
497
+ > sites**: they are for testing and feedback. An API stays in preview for at
498
+ > most six months.
499
+ >
500
+ > Nothing here registers unless you set `enableSeoTools: true` in the plugin
501
+ > config. Eleven of the tools write irreversibly to the live site the moment
502
+ > they succeed — and a pattern write re-renders every page of its type — so
503
+ > opting in is the operator saying they accept both risks.
504
+ > _Preview status verified against the Wix reference on 2026-08-28._
505
+
506
+ Auditing a site's SEO, correcting its tags, and managing its URL redirects and
507
+ tag patterns.
508
+
509
+ | Tool | What it does | Approval | Validated |
510
+ |---|---|---|---|
511
+ | `wix_seo_list_item_tags` | List the items of one type with their SEO tags. `STATIC_PAGE` enumerates the site's pages. Reports `hasOverride`, the item's own `tags`, and `resolvedTags` marked with each tag's source. Cursor paging. | — | live |
512
+ | `wix_seo_get_item_tags` | One item's tags, own and resolved | — | schema |
513
+ | `wix_seo_get_site_tags` | Site-wide tags, plus dashboard verification embeds (read-only) | — | live |
514
+ | `wix_seo_list_redirects` | Every redirect on the site. No filter, sort or paging — the endpoint takes none | — | live |
515
+ | `wix_seo_get_redirect` | One redirect by GUID | — | schema |
516
+ | `wix_seo_create_redirect` | Create a 301 redirect | **required** | schema |
517
+ | `wix_seo_delete_redirect` | Delete one redirect (re-reads it and refuses until its token is acknowledged) | **required** | schema |
518
+ | `wix_seo_bulk_create_redirects` | Create 1–100 redirects | **required** | schema |
519
+ | `wix_seo_bulk_delete_redirects` | Delete 1–500 redirects | **required** | schema |
520
+ | `wix_seo_set_item_tags` | Set one item's tags — REPLACES its own set in full; refuses until the tags it would drop are acknowledged | **required** | schema |
521
+ | `wix_seo_bulk_set_item_tags` | Set tags for many items of one type (100 max, 20 for `BLOG_POST`); per-entry results | **required** | schema |
522
+ | `wix_seo_reset_item_tags` | Give an item back the tags it inherits, discarding its own (all or nothing) | **required** | schema |
523
+ | `wix_seo_set_site_tags` | Set the site-wide tags — `meta` only; Wix rejects `title`/`script`/`link` here | **required** | schema |
524
+ | `wix_seo_list_patterns` | The site's tag templates, per page type | — | schema |
525
+ | `wix_seo_get_pattern` | One page type's pattern; read `source` to know whose it is — an owned pattern may hold zero templates, so empty ≠ absent | — | schema |
526
+ | `wix_seo_list_pattern_variables` | The variables a page type's pattern may reference | — | schema |
527
+ | `wix_seo_set_pattern` | Change an existing pattern — re-renders EVERY page of the type | **required** | schema |
528
+ | `wix_seo_create_pattern` | Give a page type its first pattern (same path as the set, `POST` not `PATCH`) | **required** | schema |
529
+ | `wix_seo_reset_pattern` | Return a page type to the Wix default pattern | **required** | schema |
530
+
531
+ **Three things to know before using these.**
532
+
533
+ *A read of a static page is the SAVED revision, not the live one.* For
534
+ `STATIC_PAGE`, `tags`, `focusKeywords` and `resolvedTags` always reflect the
535
+ saved revision, and `publishStatus` is always `PUBLISH_STATUS_UNSPECIFIED` — the
536
+ API has no path to read the published revision back. An audit report should say
537
+ it is about the saved page. `resolvedTags` also omits anything site code or apps
538
+ add while the page renders, so it is not a copy of the rendered `<head>`.
539
+
540
+ *Creating a redirect can delete another one.* Redirects do not chain: if an
541
+ existing redirect **starts at** the path a new one points to, that existing
542
+ redirect is deleted and the create proceeds — no flag involved, no way back.
543
+ The tool's own top-level `forceReplace` parameter additionally deletes a
544
+ redirect holding the same `from` path. Wix documents that flag nested as
545
+ `options.forceReplace`; do **not** send it there — the tool rebuilds the
546
+ redirect from the fields it supports, so a nested flag is dropped and the create
547
+ fails with `FROM_URL_EXISTS` as if it had never been set. This is why
548
+ `wix_seo_create_redirect` is approval-gated by default
549
+ alongside the deletes. A redirect also takes precedence over a real page at the
550
+ same path, which makes that page unreachable.
551
+
552
+ *The create refuses before it destroys.* `wix_seo_create_redirect` and
553
+ `wix_seo_bulk_create_redirects` list the site's redirects themselves and refuse,
554
+ naming each redirect they would delete — with its paths, language and whether it
555
+ is a group rule — until the caller repeats the `token` from that refusal back in
556
+ `acknowledgeDeletions`. The token is derived from the redirect's current state,
557
+ so it cannot be guessed without listing and goes stale if the redirect changes.
558
+ The check is in the execution path, not in the tool description, so a direct call
559
+ gets it too, and it refuses outright rather than assuming an empty list when the
560
+ redirects cannot be read. Destructive writes are serialised per site; a change
561
+ made in the Wix dashboard between the read and the write remains possible.
562
+
563
+ *There is no update method and no query method.* To change a redirect: get it,
564
+ delete it, then create it carrying **every** field over — an omitted field is
565
+ filled with its default, not its previous value. To find one, list them all and
566
+ filter client-side.
567
+
467
568
  ### Design
468
569
 
469
570
  | Tool | What it does | Approval | Validated |
package/dist/config.d.ts CHANGED
@@ -6,6 +6,7 @@ import type { ResolvedWixConfig, WixPluginConfig } from "./types.js";
6
6
  * become empty strings to avoid leaking `undefined` into downstream code.
7
7
  */
8
8
  export declare function resolveEnv<T>(value: T): T;
9
+ export declare const DEFAULT_APPROVAL_REQUIRED: string[];
9
10
  /**
10
11
  * Apply defaults and env substitution to the raw plugin config.
11
12
  *
package/dist/config.js CHANGED
@@ -15,7 +15,7 @@ export function resolveEnv(value) {
15
15
  return process.env[name] ?? "";
16
16
  });
17
17
  }
18
- const DEFAULT_APPROVAL_REQUIRED = [
18
+ export const DEFAULT_APPROVAL_REQUIRED = [
19
19
  "wix_blog_publish_draft",
20
20
  "wix_blog_unpublish",
21
21
  "wix_blog_delete_draft",
@@ -25,6 +25,24 @@ const DEFAULT_APPROVAL_REQUIRED = [
25
25
  "wix_bookings_cancel",
26
26
  "wix_reviews_moderate",
27
27
  "wix_faq_delete_question",
28
+ // SEO redirects. `create` is gated like the deletes on purpose: it is
29
+ // destructive by construction. Redirects do not chain — an existing redirect
30
+ // that STARTS AT the path a new one points to is deleted so the create can
31
+ // proceed, with no flag asked for and no way back. Classifying these by verb
32
+ // would have left the most damaging call in the set ungated.
33
+ "wix_seo_create_redirect",
34
+ "wix_seo_delete_redirect",
35
+ "wix_seo_bulk_create_redirects",
36
+ "wix_seo_bulk_delete_redirects",
37
+ // Tranche 2 — writing tags. Each REPLACES what it writes: the previous
38
+ // value is gone, and a pattern write re-renders every page of its type.
39
+ "wix_seo_set_item_tags",
40
+ "wix_seo_bulk_set_item_tags",
41
+ "wix_seo_reset_item_tags",
42
+ "wix_seo_set_site_tags",
43
+ "wix_seo_set_pattern",
44
+ "wix_seo_create_pattern",
45
+ "wix_seo_reset_pattern",
28
46
  ];
29
47
  const VALID_LOG_LEVELS = ["debug", "info", "warn", "error"];
30
48
  /**
@@ -49,6 +67,12 @@ export function resolveConfig(cfg = {}) {
49
67
  : "info";
50
68
  return {
51
69
  enabled: cfg.enabled !== false,
70
+ // OPT-IN, unlike every other tool group. The Wix SEO endpoints are in
71
+ // Developer Preview: Wix may change them at any time and says they
72
+ // "shouldn't be used on live sites". Four of these tools also write
73
+ // irreversibly to a live site the moment they succeed. Defaulting them on
74
+ // would enable that on every existing install without anyone deciding to.
75
+ enableSeoTools: cfg.enableSeoTools === true,
52
76
  apiKey,
53
77
  accountId,
54
78
  allowedSiteIds,
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,EAAE;AACF,2EAA2E;AAC3E,6DAA6D;AAQ7D;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CAAI,KAAQ;IACpC,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC5C,OAAO,KAAK,CAAC,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC,EAAE,IAAY,EAAE,EAAE;QACvD,OAAO,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;IACjC,CAAC,CAAiB,CAAC;AACrB,CAAC;AAED,MAAM,yBAAyB,GAAG;IAChC,wBAAwB;IACxB,oBAAoB;IACpB,uBAAuB;IACvB,sBAAsB;IACtB,qBAAqB;IACrB,yBAAyB;IACzB,qBAAqB;IACrB,sBAAsB;IACtB,yBAAyB;CAC1B,CAAC;AAEF,MAAM,gBAAgB,GAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;AAE3E;;;;;;;;;GASG;AACH,MAAM,UAAU,aAAa,CAC3B,MAAuB,EAAE;IAEzB,MAAM,MAAM,GAAG,UAAU,CAAC,GAAG,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC;IAC5C,MAAM,SAAS,GAAG,UAAU,CAAC,GAAG,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC;IAClD,MAAM,cAAc,GAAG,CAAC,GAAG,CAAC,cAAc,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC;IAC9E,MAAM,eAAe,GAAG,UAAU,CAAC,GAAG,CAAC,aAAa,IAAI,EAAE,CAAC,CAAC;IAC5D,MAAM,aAAa,GACjB,eAAe,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IAE/C,MAAM,gBAAgB,GAAG,GAAG,CAAC,gBAAgB,IAAI,yBAAyB,CAAC;IAE3E,MAAM,QAAQ,GAAgB,gBAAgB,CAAC,QAAQ,CACrD,GAAG,CAAC,QAAuB,CAC5B;QACC,CAAC,CAAE,GAAG,CAAC,QAAwB;QAC/B,CAAC,CAAC,MAAM,CAAC;IAEX,OAAO;QACL,OAAO,EAAE,GAAG,CAAC,OAAO,KAAK,KAAK;QAC9B,MAAM;QACN,SAAS;QACT,cAAc;QACd,aAAa;QACb,gBAAgB,EAAE,IAAI,GAAG,CAAC,gBAAgB,CAAC;QAC3C,QAAQ;KACT,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,EAAE;AACF,2EAA2E;AAC3E,6DAA6D;AAQ7D;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CAAI,KAAQ;IACpC,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IAC5C,OAAO,KAAK,CAAC,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC,EAAE,IAAY,EAAE,EAAE;QACvD,OAAO,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;IACjC,CAAC,CAAiB,CAAC;AACrB,CAAC;AAED,MAAM,CAAC,MAAM,yBAAyB,GAAG;IACvC,wBAAwB;IACxB,oBAAoB;IACpB,uBAAuB;IACvB,sBAAsB;IACtB,qBAAqB;IACrB,yBAAyB;IACzB,qBAAqB;IACrB,sBAAsB;IACtB,yBAAyB;IACzB,sEAAsE;IACtE,6EAA6E;IAC7E,2EAA2E;IAC3E,6EAA6E;IAC7E,6DAA6D;IAC7D,yBAAyB;IACzB,yBAAyB;IACzB,+BAA+B;IAC/B,+BAA+B;IAC/B,uEAAuE;IACvE,wEAAwE;IACxE,uBAAuB;IACvB,4BAA4B;IAC5B,yBAAyB;IACzB,uBAAuB;IACvB,qBAAqB;IACrB,wBAAwB;IACxB,uBAAuB;CACxB,CAAC;AAEF,MAAM,gBAAgB,GAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;AAE3E;;;;;;;;;GASG;AACH,MAAM,UAAU,aAAa,CAC3B,MAAuB,EAAE;IAEzB,MAAM,MAAM,GAAG,UAAU,CAAC,GAAG,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC;IAC5C,MAAM,SAAS,GAAG,UAAU,CAAC,GAAG,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC;IAClD,MAAM,cAAc,GAAG,CAAC,GAAG,CAAC,cAAc,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC;IAC9E,MAAM,eAAe,GAAG,UAAU,CAAC,GAAG,CAAC,aAAa,IAAI,EAAE,CAAC,CAAC;IAC5D,MAAM,aAAa,GACjB,eAAe,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IAE/C,MAAM,gBAAgB,GAAG,GAAG,CAAC,gBAAgB,IAAI,yBAAyB,CAAC;IAE3E,MAAM,QAAQ,GAAgB,gBAAgB,CAAC,QAAQ,CACrD,GAAG,CAAC,QAAuB,CAC5B;QACC,CAAC,CAAE,GAAG,CAAC,QAAwB;QAC/B,CAAC,CAAC,MAAM,CAAC;IAEX,OAAO;QACL,OAAO,EAAE,GAAG,CAAC,OAAO,KAAK,KAAK;QAC9B,sEAAsE;QACtE,mEAAmE;QACnE,oEAAoE;QACpE,0EAA0E;QAC1E,0EAA0E;QAC1E,cAAc,EAAE,GAAG,CAAC,cAAc,KAAK,IAAI;QAC3C,MAAM;QACN,SAAS;QACT,cAAc;QACd,aAAa;QACb,gBAAgB,EAAE,IAAI,GAAG,CAAC,gBAAgB,CAAC;QAC3C,QAAQ;KACT,CAAC;AACJ,CAAC"}
@@ -21,6 +21,16 @@ export interface BeforeToolCallResult {
21
21
  timeoutBehavior?: "allow" | "deny";
22
22
  };
23
23
  }
24
+ /**
25
+ * The approval description, guaranteed to satisfy the protocol.
26
+ *
27
+ * Ordered by what decides a refusal: the irreversible effect, the site the call
28
+ * actually lands on (the preview used to strip `siteId`, so on a multi-site key
29
+ * the operator never saw which tenant), then each highlighted parameter.
30
+ */
31
+ export declare function buildApprovalDescription(toolName: string, params: Record<string, unknown>, defaultSiteId: string): string;
32
+ /** The approval title, likewise bounded (protocol: 1–80). */
33
+ export declare function buildApprovalTitle(toolName: string): string;
24
34
  /**
25
35
  * Build a `before_tool_call` handler bound to a resolved Wix config.
26
36
  * Extracted as a factory so tests can exercise it without a full plugin
@@ -11,25 +11,313 @@
11
11
  // - `timeoutBehavior` accepts "allow" | "deny".
12
12
  // - `pluginId` is set automatically by the hook runner — do not set it
13
13
  // yourself.
14
+ import { createHash } from "node:crypto";
14
15
  const DEFAULT_TIMEOUT_MS = 600_000; // 10 minutes
15
- const PARAM_PREVIEW_CHARS = 600;
16
16
  /**
17
- * Truncate the parameter snapshot so we never surface a wall of JSON to
18
- * the operator. We strip `siteId` since it's covered by the config and
19
- * adds noise to the prompt.
17
+ * THE PROTOCOL'S OWN LIMITS, not ours.
18
+ *
19
+ * `PluginApprovalRequestParamsSchema` (openclaw gateway, plugin-approvals)
20
+ * declares `title` as 1–80 characters and `description` as 1–256. A longer one
21
+ * is not truncated by the gateway — the request fails validation, so the
22
+ * operator is never asked and the tool is blocked. The previous prompt embedded
23
+ * a 600-character JSON preview under a paragraph of boilerplate and could not
24
+ * fit: every gated tool was affected, not only the SEO ones.
25
+ *
26
+ * So the description is BUILT to the budget rather than written and hoped for.
20
27
  */
21
- function previewParams(params) {
22
- const { siteId: _siteId, ...rest } = params; // eslint-disable-line @typescript-eslint/no-unused-vars
28
+ const APPROVAL_TITLE_MAX = 80;
29
+ const APPROVAL_DESCRIPTION_MAX = 256;
30
+ /**
31
+ * Parameters that must reach the operator, in priority order.
32
+ *
33
+ * With 256 characters there is no room for the full parameters, so what is
34
+ * shown has to be what DECIDES: a redirect carries two free-form paths, and
35
+ * `forceReplace` — the flag that deletes an existing redirect for good — must
36
+ * never be the part that falls off the end.
37
+ *
38
+ * TABLE-DRIVEN so a destructive parameter cannot be added to a tool without
39
+ * saying where it ranks in the prompt.
40
+ */
41
+ const APPROVAL_HIGHLIGHTS = {
42
+ wix_seo_create_redirect: ["forceReplace", "redirect"],
43
+ wix_seo_delete_redirect: ["redirectId"],
44
+ wix_seo_bulk_create_redirects: ["redirects"],
45
+ wix_seo_bulk_delete_redirects: ["redirectIds"],
46
+ };
47
+ /** One-line effect warnings, shown FIRST because they are the reason to refuse. */
48
+ const APPROVAL_EFFECTS = {
49
+ // A create destroys in TWO ways, and the second needs no existing redirect:
50
+ // a redirect takes precedence over a real page at the same path, so creating
51
+ // one from a path that still serves a page makes that page unreachable. The
52
+ // pre-flight cannot see it — it reads redirects, not pages — so the prompt is
53
+ // the only place the operator can be told.
54
+ wix_seo_create_redirect: "may DELETE a redirect; HIDES any real page at `from` (permanent)",
55
+ wix_seo_delete_redirect: "permanent",
56
+ wix_seo_bulk_create_redirects: "not atomic; may DELETE redirects; HIDES real pages at a `from`",
57
+ wix_seo_bulk_delete_redirects: "permanent",
58
+ };
59
+ /** Longest a single value may occupy in the prompt, so one long path cannot
60
+ * crowd out everything after it. */
61
+ const VALUE_MAX = 60;
62
+ /** Make control characters VISIBLE instead of letting them act.
63
+ *
64
+ * The description is rendered as a line-structured message, so a `from` path,
65
+ * a language or a parameter name carrying a newline can paint fields that were
66
+ * never sent — on the one surface whose whole purpose is an operator deciding
67
+ * whether to allow an irreversible write. The generic branch was safe by
68
+ * accident, `JSON.stringify` escaping these; the redirect rendering, which
69
+ * exists precisely to be more readable than JSON, was not. */
70
+ function sanitize(value) {
71
+ return value.replace(
72
+ // eslint-disable-next-line no-control-regex
73
+ /[\u0000-\u001f\u007f\u0085\u2028\u2029]/g, (c) => {
74
+ if (c === "\n")
75
+ return "\\n";
76
+ if (c === "\r")
77
+ return "\\r";
78
+ if (c === "\t")
79
+ return "\\t";
80
+ return `\\u${c.codePointAt(0).toString(16).padStart(4, "0")}`;
81
+ });
82
+ }
83
+ /** Shorten in the MIDDLE: a path's beginning and its end are what identify it,
84
+ * and two long paths sharing a prefix are told apart only by their tails.
85
+ * Neutralises controls FIRST, so truncation can never be what lets one
86
+ * through. */
87
+ function shorten(raw, max = VALUE_MAX) {
88
+ const value = sanitize(raw);
89
+ if (value.length <= max)
90
+ return value;
91
+ const head = Math.ceil((max - 1) / 2);
92
+ const tail = Math.floor((max - 1) / 2);
93
+ return `${value.slice(0, head)}…${value.slice(value.length - tail)}`;
94
+ }
95
+ /** A short, stable fingerprint of a whole set, so two approvals for DIFFERENT
96
+ * sets never read identically even when only a few members are shown.
97
+ *
98
+ * SHA-256, truncated to 128 bits. A 32-bit FNV was small enough to collide on
99
+ * purpose: two batches whose first three members matched could be given the
100
+ * same digest, and with only three members sampled the two prompts became
101
+ * indistinguishable — for two different permanent deletions. */
102
+ function digest(input) {
103
+ return createHash("sha256").update(input).digest("hex").slice(0, 32);
104
+ }
105
+ /** A stable serialisation of the WHOLE value, before anything is shortened for
106
+ * display. What the fingerprint must identify is the batch, not its preview. */
107
+ function canonical(value) {
108
+ const walk = (v) => {
109
+ if (Array.isArray(v))
110
+ return v.map(walk);
111
+ if (v !== null && typeof v === "object") {
112
+ // KEYS SORTED. `JSON.stringify` preserves insertion order, so the same
113
+ // batch written with its fields in a different order fingerprinted
114
+ // differently — an identity that changes for no reason is no identity.
115
+ const obj = v;
116
+ const out = {};
117
+ for (const k of Object.keys(obj).sort())
118
+ out[k] = walk(obj[k]);
119
+ return out;
120
+ }
121
+ return v;
122
+ };
123
+ try {
124
+ return JSON.stringify(walk(value)) ?? String(value);
125
+ }
126
+ catch {
127
+ return String(value);
128
+ }
129
+ }
130
+ /** How many members of a list to name before the fingerprint stands in for the
131
+ * rest. */
132
+ const LIST_SAMPLE = 3;
133
+ /** Compact rendering of a highlighted value.
134
+ *
135
+ * A list reports its LENGTH, then a few members, then a fingerprint of the
136
+ * whole set. Length alone made two bulk deletes of completely different GUIDs
137
+ * produce the same prompt — the operator was approving a permanent deletion
138
+ * with nothing on screen that distinguished it from another one.
139
+ */
140
+ /** `from → to [exact|group, language]`, or null when the value is not a
141
+ * redirect. Shared by the single and the bulk rendering: a bulk create's
142
+ * members went through the generic JSON path, so up to 500 GROUP rules over
143
+ * whole branches could read the same as 500 exact ones. */
144
+ function renderRedirect(value, max) {
145
+ if (value === null || typeof value !== "object")
146
+ return null;
147
+ const obj = value;
148
+ if (typeof obj.from !== "string" || typeof obj.to !== "string")
149
+ return null;
150
+ const options = obj.options;
151
+ const scope = options?.groupRedirect === true ? "group" : "exact";
152
+ // Bounded and neutralised like the paths: `language` went in raw, so it was
153
+ // both an injection point and a way to crowd out the rest of the line.
154
+ const language = typeof obj.language === "string" && obj.language.length > 0
155
+ ? shorten(obj.language, 16)
156
+ : "all";
157
+ return `${shorten(obj.from, max)} → ${shorten(obj.to, max)} [${scope}, ${language}]`;
158
+ }
159
+ /** Widest blast radius first: a group rule carries every path under its own,
160
+ * and a rule with no language applies to every language. Sampling in order
161
+ * showed three exact rules while the global group ones sat behind a `+N`. */
162
+ function blastRadius(value) {
163
+ if (value === null || typeof value !== "object")
164
+ return 0;
165
+ const obj = value;
166
+ const options = obj.options;
167
+ const group = options?.groupRedirect === true ? 2 : 0;
168
+ const global = typeof obj.language === "string" && obj.language.length > 0 ? 0 : 1;
169
+ return group + global;
170
+ }
171
+ /** `2 group/all, 1 exact/fr` — what the batch DOES, over every member, in the
172
+ * space a sample cannot cover. */
173
+ function scopeTally(value) {
174
+ const counts = new Map();
175
+ for (const v of value) {
176
+ const rendered = renderRedirect(v, 1);
177
+ if (rendered === null)
178
+ return null;
179
+ const scope = rendered.slice(rendered.lastIndexOf("[") + 1, -1);
180
+ counts.set(scope, (counts.get(scope) ?? 0) + 1);
181
+ }
182
+ return [...counts.entries()].map(([k, n]) => `${n} ${k}`).join(", ");
183
+ }
184
+ function renderHighlight(rawKey, value) {
185
+ // A PARAMETER NAME IS CALLER-SUPPLIED TOO. The tail loop renders every key the
186
+ // call carries, declared or not.
187
+ const key = shorten(rawKey, 40);
188
+ if (Array.isArray(value)) {
189
+ const members = value.map((v) => {
190
+ const asRedirect = renderRedirect(v, 12);
191
+ if (asRedirect !== null)
192
+ return asRedirect;
193
+ return typeof v === "string" ? v : (JSON.stringify(v) ?? "");
194
+ });
195
+ const sample = value
196
+ .map((v, i) => ({ i, radius: blastRadius(v) }))
197
+ .sort((a, b) => b.radius - a.radius || a.i - b.i)
198
+ .slice(0, LIST_SAMPLE)
199
+ .map(({ i }) => shorten(members[i], 40))
200
+ .join(", ");
201
+ const rest = members.length - Math.min(LIST_SAMPLE, members.length);
202
+ const tally = scopeTally(value);
203
+ // THREE PARTS, NOT ONE. Joined into a single string, a long tally — eight
204
+ // distinct languages is enough — pushed the whole thing over the budget and
205
+ // `fitParts` skipped it entire: no count, no member, no fingerprint, and an
206
+ // operator approving an irreversible batch with nothing identifying it.
207
+ // Split, the core survives and only the detail is dropped.
208
+ return [
209
+ // Fingerprint over the ORIGINAL values, not the shortened renderings:
210
+ // `/sameX-A-different-Ytail` and `/sameX-B-different-Ytail` both render as
211
+ // `/sameX…Ytail`, so a digest taken after truncation gave two different
212
+ // destructive batches the same identity.
213
+ `${key}: ${members.length}× #${digest(canonical(value))}`,
214
+ ...(tally !== null ? [`(${tally})`] : []),
215
+ `[${sample}${rest > 0 ? `, +${rest}` : ""}]`,
216
+ ];
217
+ }
218
+ // A redirect reads as `from → to` WITH ITS SCOPE. Showing only the paths made
219
+ // an exact French-only rule and a global group rule over a whole branch
220
+ // produce the same prompt — and the second can make far more pages
221
+ // unreachable than the first.
222
+ const asRedirect = renderRedirect(value, 40);
223
+ if (asRedirect !== null)
224
+ return asRedirect;
23
225
  let json;
24
226
  try {
25
- json = JSON.stringify(rest, null, 2);
227
+ json = JSON.stringify(value);
26
228
  }
27
229
  catch {
28
- json = "<unserializable params>";
230
+ json = "<unserializable>";
231
+ }
232
+ return `${key}: ${shorten(json)}`;
233
+ }
234
+ /** Join what fits, in order, marking that something was left out.
235
+ *
236
+ * SKIPS an oversized part rather than stopping at it: stopping meant one long
237
+ * value hid every fact after it, which is how `from` and `to` could vanish
238
+ * from a prompt entirely. Every part is already length-bounded, so skipping is
239
+ * the rare case, not the normal one. */
240
+ const OMISSION_MARKER = " \u00b7 \u2026";
241
+ function fitParts(parts, budget) {
242
+ const out = [];
243
+ let used = 0;
244
+ let dropped = false;
245
+ for (const part of parts) {
246
+ const cost = out.length === 0 ? part.length : part.length + 3;
247
+ // Room for the marker is reserved once something HAS been dropped — that is
248
+ // when it will certainly be appended. The reservation used to be the other
249
+ // way round, so after the first drop the remaining parts filled the budget
250
+ // to the brim and the final `slice` ate the marker: an approval that had
251
+ // silently omitted parameters read as complete, on destructive calls.
252
+ if (used + cost > budget - (dropped ? OMISSION_MARKER.length : 0)) {
253
+ dropped = true;
254
+ continue;
255
+ }
256
+ out.push(part);
257
+ used += cost;
258
+ }
259
+ if (!dropped)
260
+ return out.join(" · ").slice(0, budget);
261
+ // Reserving as we go is not enough on its own: the drop can be the LAST
262
+ // part, after earlier ones already filled the budget. Give the marker its
263
+ // room by surrendering trailing parts — parts are ordered by what decides a
264
+ // refusal, so the least decisive go first, and a truthful "something is
265
+ // missing" outranks one more shown value.
266
+ let joined = out.join(" · ");
267
+ while (out.length > 0 && joined.length + OMISSION_MARKER.length > budget) {
268
+ out.pop();
269
+ joined = out.join(" · ");
29
270
  }
30
- if (json.length <= PARAM_PREVIEW_CHARS)
31
- return json;
32
- return `${json.slice(0, PARAM_PREVIEW_CHARS)}…`;
271
+ if (out.length === 0)
272
+ return "";
273
+ return `${joined}${OMISSION_MARKER}`.slice(0, budget);
274
+ }
275
+ /**
276
+ * The approval description, guaranteed to satisfy the protocol.
277
+ *
278
+ * Ordered by what decides a refusal: the irreversible effect, the site the call
279
+ * actually lands on (the preview used to strip `siteId`, so on a multi-site key
280
+ * the operator never saw which tenant), then each highlighted parameter.
281
+ */
282
+ export function buildApprovalDescription(toolName, params, defaultSiteId) {
283
+ const site = typeof params.siteId === "string" && params.siteId.length > 0
284
+ ? params.siteId
285
+ : defaultSiteId || "<no site configured>";
286
+ const parts = [];
287
+ const effect = APPROVAL_EFFECTS[toolName];
288
+ if (effect !== undefined)
289
+ parts.push(effect.toUpperCase());
290
+ parts.push(`site ${site}`);
291
+ // Highlighted parameters first, in the table's order, then EVERYTHING ELSE
292
+ // the call carries. Without the tail, a gated tool with no table entry —
293
+ // every pre-existing one — would show the operator nothing but a site.
294
+ const highlighted = APPROVAL_HIGHLIGHTS[toolName] ?? [];
295
+ const seen = new Set(["siteId"]);
296
+ const push = (rendered) => {
297
+ if (Array.isArray(rendered))
298
+ parts.push(...rendered);
299
+ else
300
+ parts.push(rendered);
301
+ };
302
+ for (const key of highlighted) {
303
+ if (!(key in params))
304
+ continue;
305
+ seen.add(key);
306
+ push(renderHighlight(key, params[key]));
307
+ }
308
+ for (const [key, value] of Object.entries(params)) {
309
+ if (seen.has(key))
310
+ continue;
311
+ push(renderHighlight(key, value));
312
+ }
313
+ const body = fitParts(parts, APPROVAL_DESCRIPTION_MAX);
314
+ // fitParts already respects the budget; the guard is belt-and-braces so a
315
+ // future edit cannot reintroduce the failure this function exists to prevent.
316
+ return body.slice(0, APPROVAL_DESCRIPTION_MAX) || toolName;
317
+ }
318
+ /** The approval title, likewise bounded (protocol: 1–80). */
319
+ export function buildApprovalTitle(toolName) {
320
+ return `Wix: confirm ${toolName}`.slice(0, APPROVAL_TITLE_MAX);
33
321
  }
34
322
  /**
35
323
  * Build a `before_tool_call` handler bound to a resolved Wix config.
@@ -42,18 +330,13 @@ export function createApprovalHook(config, logger) {
42
330
  return undefined;
43
331
  if (!config.approvalRequired.has(event.toolName))
44
332
  return undefined;
45
- const description = `Tool \`${event.toolName}\` is about to run with the following parameters:\n\n` +
46
- "```json\n" +
47
- previewParams(event.params) +
48
- "\n```\n\n" +
49
- "Approve to execute, deny to cancel. The call will deny automatically " +
50
- "if no decision is made within 10 minutes.";
333
+ const description = buildApprovalDescription(event.toolName, event.params, config.defaultSiteId);
51
334
  if (config.logLevel === "debug") {
52
335
  logger.debug?.(`wix: requesting approval for ${event.toolName} (runId=${event.runId ?? "?"})`);
53
336
  }
54
337
  return {
55
338
  requireApproval: {
56
- title: `Wix: confirm ${event.toolName}`,
339
+ title: buildApprovalTitle(event.toolName),
57
340
  description,
58
341
  severity: "critical",
59
342
  timeoutMs: DEFAULT_TIMEOUT_MS,
@@ -1 +1 @@
1
- {"version":3,"file":"approval.js","sourceRoot":"","sources":["../../src/hooks/approval.ts"],"names":[],"mappings":"AAAA,0DAA0D;AAC1D,EAAE;AACF,yEAAyE;AACzE,uEAAuE;AACvE,0EAA0E;AAC1E,iCAAiC;AACjC,EAAE;AACF,yEAAyE;AACzE,4BAA4B;AAC5B,uEAAuE;AACvE,kDAAkD;AAClD,yEAAyE;AACzE,gBAAgB;AA4BhB,MAAM,kBAAkB,GAAG,OAAO,CAAC,CAAC,aAAa;AACjD,MAAM,mBAAmB,GAAG,GAAG,CAAC;AAEhC;;;;GAIG;AACH,SAAS,aAAa,CAAC,MAA+B;IACpD,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,IAAI,EAAE,GAAG,MAAM,CAAC,CAAC,wDAAwD;IACrG,IAAI,IAAY,CAAC;IACjB,IAAI,CAAC;QACH,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IACvC,CAAC;IAAC,MAAM,CAAC;QACP,IAAI,GAAG,yBAAyB,CAAC;IACnC,CAAC;IACD,IAAI,IAAI,CAAC,MAAM,IAAI,mBAAmB;QAAE,OAAO,IAAI,CAAC;IACpD,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,mBAAmB,CAAC,GAAG,CAAC;AAClD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAChC,MAAyB,EACzB,MAAiB;IAEjB,OAAO,SAAS,cAAc,CAC5B,KAA0B;QAE1B,IAAI,CAAC,MAAM,CAAC,OAAO;YAAE,OAAO,SAAS,CAAC;QACtC,IAAI,CAAC,MAAM,CAAC,gBAAgB,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC;YAAE,OAAO,SAAS,CAAC;QAEnE,MAAM,WAAW,GACf,UAAU,KAAK,CAAC,QAAQ,uDAAuD;YAC/E,WAAW;YACX,aAAa,CAAC,KAAK,CAAC,MAAM,CAAC;YAC3B,WAAW;YACX,uEAAuE;YACvE,2CAA2C,CAAC;QAE9C,IAAI,MAAM,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;YAChC,MAAM,CAAC,KAAK,EAAE,CACZ,gCAAgC,KAAK,CAAC,QAAQ,WAAW,KAAK,CAAC,KAAK,IAAI,GAAG,GAAG,CAC/E,CAAC;QACJ,CAAC;QAED,OAAO;YACL,eAAe,EAAE;gBACf,KAAK,EAAE,gBAAgB,KAAK,CAAC,QAAQ,EAAE;gBACvC,WAAW;gBACX,QAAQ,EAAE,UAAU;gBACpB,SAAS,EAAE,kBAAkB;gBAC7B,eAAe,EAAE,MAAM;aACxB;SACF,CAAC;IACJ,CAAC,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"approval.js","sourceRoot":"","sources":["../../src/hooks/approval.ts"],"names":[],"mappings":"AAAA,0DAA0D;AAC1D,EAAE;AACF,yEAAyE;AACzE,uEAAuE;AACvE,0EAA0E;AAC1E,iCAAiC;AACjC,EAAE;AACF,yEAAyE;AACzE,4BAA4B;AAC5B,uEAAuE;AACvE,kDAAkD;AAClD,yEAAyE;AACzE,gBAAgB;AAEhB,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AA4BzC,MAAM,kBAAkB,GAAG,OAAO,CAAC,CAAC,aAAa;AAEjD;;;;;;;;;;;GAWG;AACH,MAAM,kBAAkB,GAAG,EAAE,CAAC;AAC9B,MAAM,wBAAwB,GAAG,GAAG,CAAC;AAErC;;;;;;;;;;GAUG;AACH,MAAM,mBAAmB,GAA0C;IACjE,uBAAuB,EAAE,CAAC,cAAc,EAAE,UAAU,CAAC;IACrD,uBAAuB,EAAE,CAAC,YAAY,CAAC;IACvC,6BAA6B,EAAE,CAAC,WAAW,CAAC;IAC5C,6BAA6B,EAAE,CAAC,aAAa,CAAC;CAC/C,CAAC;AAEF,mFAAmF;AACnF,MAAM,gBAAgB,GAA2B;IAC/C,4EAA4E;IAC5E,6EAA6E;IAC7E,4EAA4E;IAC5E,8EAA8E;IAC9E,2CAA2C;IAC3C,uBAAuB,EACrB,kEAAkE;IACpE,uBAAuB,EAAE,WAAW;IACpC,6BAA6B,EAC3B,gEAAgE;IAClE,6BAA6B,EAAE,WAAW;CAC3C,CAAC;AAEF;qCACqC;AACrC,MAAM,SAAS,GAAG,EAAE,CAAC;AAErB;;;;;;;+DAO+D;AAC/D,SAAS,QAAQ,CAAC,KAAa;IAC7B,OAAO,KAAK,CAAC,OAAO;IAClB,4CAA4C;IAC5C,0CAA0C,EAC1C,CAAC,CAAC,EAAE,EAAE;QACJ,IAAI,CAAC,KAAK,IAAI;YAAE,OAAO,KAAK,CAAC;QAC7B,IAAI,CAAC,KAAK,IAAI;YAAE,OAAO,KAAK,CAAC;QAC7B,IAAI,CAAC,KAAK,IAAI;YAAE,OAAO,KAAK,CAAC;QAC7B,OAAO,MAAM,CAAC,CAAC,WAAW,CAAC,CAAC,CAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,CAAC;IACjE,CAAC,CACF,CAAC;AACJ,CAAC;AAED;;;eAGe;AACf,SAAS,OAAO,CAAC,GAAW,EAAE,GAAG,GAAG,SAAS;IAC3C,MAAM,KAAK,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC;IAC5B,IAAI,KAAK,CAAC,MAAM,IAAI,GAAG;QAAE,OAAO,KAAK,CAAC;IACtC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IACtC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IACvC,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,CAAC;AACvE,CAAC;AAED;;;;;;iEAMiE;AACjE,SAAS,MAAM,CAAC,KAAa;IAC3B,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;AACvE,CAAC;AAED;iFACiF;AACjF,SAAS,SAAS,CAAC,KAAc;IAC/B,MAAM,IAAI,GAAG,CAAC,CAAU,EAAW,EAAE;QACnC,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;YAAE,OAAO,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACzC,IAAI,CAAC,KAAK,IAAI,IAAI,OAAO,CAAC,KAAK,QAAQ,EAAE,CAAC;YACxC,uEAAuE;YACvE,mEAAmE;YACnE,uEAAuE;YACvE,MAAM,GAAG,GAAG,CAA4B,CAAC;YACzC,MAAM,GAAG,GAA4B,EAAE,CAAC;YACxC,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE;gBAAE,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;YAC/D,OAAO,GAAG,CAAC;QACb,CAAC;QACD,OAAO,CAAC,CAAC;IACX,CAAC,CAAC;IACF,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC;IACtD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;IACvB,CAAC;AACH,CAAC;AAED;YACY;AACZ,MAAM,WAAW,GAAG,CAAC,CAAC;AAEtB;;;;;;GAMG;AACH;;;4DAG4D;AAC5D,SAAS,cAAc,CAAC,KAAc,EAAE,GAAW;IACjD,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC7D,MAAM,GAAG,GAAG,KAAgC,CAAC;IAC7C,IAAI,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ,IAAI,OAAO,GAAG,CAAC,EAAE,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC5E,MAAM,OAAO,GAAG,GAAG,CAAC,OAAkD,CAAC;IACvE,MAAM,KAAK,GAAG,OAAO,EAAE,aAAa,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC;IAClE,4EAA4E;IAC5E,uEAAuE;IACvE,MAAM,QAAQ,GACZ,OAAO,GAAG,CAAC,QAAQ,KAAK,QAAQ,IAAI,GAAG,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC;QACzD,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,EAAE,CAAC;QAC3B,CAAC,CAAC,KAAK,CAAC;IACZ,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,MAAM,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,KAAK,KAAK,KAAK,QAAQ,GAAG,CAAC;AACvF,CAAC;AAED;;8EAE8E;AAC9E,SAAS,WAAW,CAAC,KAAc;IACjC,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,CAAC,CAAC;IAC1D,MAAM,GAAG,GAAG,KAAgC,CAAC;IAC7C,MAAM,OAAO,GAAG,GAAG,CAAC,OAAkD,CAAC;IACvE,MAAM,KAAK,GAAG,OAAO,EAAE,aAAa,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACtD,MAAM,MAAM,GACV,OAAO,GAAG,CAAC,QAAQ,KAAK,QAAQ,IAAI,GAAG,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACtE,OAAO,KAAK,GAAG,MAAM,CAAC;AACxB,CAAC;AAED;mCACmC;AACnC,SAAS,UAAU,CAAC,KAAgB;IAClC,MAAM,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAC;IACzC,KAAK,MAAM,CAAC,IAAI,KAAK,EAAE,CAAC;QACtB,MAAM,QAAQ,GAAG,cAAc,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QACtC,IAAI,QAAQ,KAAK,IAAI;YAAE,OAAO,IAAI,CAAC;QACnC,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,QAAQ,CAAC,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QAChE,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IAClD,CAAC;IACD,OAAO,CAAC,GAAG,MAAM,CAAC,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACvE,CAAC;AAED,SAAS,eAAe,CAAC,MAAc,EAAE,KAAc;IACrD,+EAA+E;IAC/E,iCAAiC;IACjC,MAAM,GAAG,GAAG,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAChC,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,MAAM,OAAO,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;YAC9B,MAAM,UAAU,GAAG,cAAc,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACzC,IAAI,UAAU,KAAK,IAAI;gBAAE,OAAO,UAAU,CAAC;YAC3C,OAAO,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QAC/D,CAAC,CAAC,CAAC;QACH,MAAM,MAAM,GAAG,KAAK;aACjB,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;aAC9C,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;aAChD,KAAK,CAAC,CAAC,EAAE,WAAW,CAAC;aACrB,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAE,EAAE,EAAE,CAAC,CAAC;aACxC,IAAI,CAAC,IAAI,CAAC,CAAC;QACd,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;QACpE,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,CAAC;QAChC,0EAA0E;QAC1E,4EAA4E;QAC5E,4EAA4E;QAC5E,wEAAwE;QACxE,2DAA2D;QAC3D,OAAO;YACL,sEAAsE;YACtE,2EAA2E;YAC3E,wEAAwE;YACxE,yCAAyC;YACzC,GAAG,GAAG,KAAK,OAAO,CAAC,MAAM,MAAM,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE;YACzD,GAAG,CAAC,KAAK,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YACzC,IAAI,MAAM,GAAG,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG;SAC7C,CAAC;IACJ,CAAC;IACD,8EAA8E;IAC9E,wEAAwE;IACxE,mEAAmE;IACnE,8BAA8B;IAC9B,MAAM,UAAU,GAAG,cAAc,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IAC7C,IAAI,UAAU,KAAK,IAAI;QAAE,OAAO,UAAU,CAAC;IAC3C,IAAI,IAAY,CAAC;IACjB,IAAI,CAAC;QACH,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IAC/B,CAAC;IAAC,MAAM,CAAC;QACP,IAAI,GAAG,kBAAkB,CAAC;IAC5B,CAAC;IACD,OAAO,GAAG,GAAG,KAAK,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;AACpC,CAAC;AAED;;;;;yCAKyC;AACzC,MAAM,eAAe,GAAG,gBAAgB,CAAC;AAEzC,SAAS,QAAQ,CAAC,KAAe,EAAE,MAAc;IAC/C,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,IAAI,OAAO,GAAG,KAAK,CAAC;IACpB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,IAAI,GAAG,GAAG,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;QAC9D,4EAA4E;QAC5E,2EAA2E;QAC3E,2EAA2E;QAC3E,yEAAyE;QACzE,sEAAsE;QACtE,IAAI,IAAI,GAAG,IAAI,GAAG,MAAM,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YAClE,OAAO,GAAG,IAAI,CAAC;YACf,SAAS;QACX,CAAC;QACD,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACf,IAAI,IAAI,IAAI,CAAC;IACf,CAAC;IACD,IAAI,CAAC,OAAO;QAAE,OAAO,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;IACtD,wEAAwE;IACxE,0EAA0E;IAC1E,4EAA4E;IAC5E,wEAAwE;IACxE,0CAA0C;IAC1C,IAAI,MAAM,GAAG,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC7B,OAAO,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,MAAM,CAAC,MAAM,GAAG,eAAe,CAAC,MAAM,GAAG,MAAM,EAAE,CAAC;QACzE,GAAG,CAAC,GAAG,EAAE,CAAC;QACV,MAAM,GAAG,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAC3B,CAAC;IACD,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAChC,OAAO,GAAG,MAAM,GAAG,eAAe,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;AACxD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,wBAAwB,CACtC,QAAgB,EAChB,MAA+B,EAC/B,aAAqB;IAErB,MAAM,IAAI,GACR,OAAO,MAAM,CAAC,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC;QAC3D,CAAC,CAAC,MAAM,CAAC,MAAM;QACf,CAAC,CAAC,aAAa,IAAI,sBAAsB,CAAC;IAC9C,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,MAAM,MAAM,GAAG,gBAAgB,CAAC,QAAQ,CAAC,CAAC;IAC1C,IAAI,MAAM,KAAK,SAAS;QAAE,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC;IAC3D,KAAK,CAAC,IAAI,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC;IAC3B,2EAA2E;IAC3E,yEAAyE;IACzE,uEAAuE;IACvE,MAAM,WAAW,GAAG,mBAAmB,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;IACxD,MAAM,IAAI,GAAG,IAAI,GAAG,CAAS,CAAC,QAAQ,CAAC,CAAC,CAAC;IACzC,MAAM,IAAI,GAAG,CAAC,QAA2B,EAAE,EAAE;QAC3C,IAAI,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC;YAAE,KAAK,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAC,CAAC;;YAChD,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC5B,CAAC,CAAC;IACF,KAAK,MAAM,GAAG,IAAI,WAAW,EAAE,CAAC;QAC9B,IAAI,CAAC,CAAC,GAAG,IAAI,MAAM,CAAC;YAAE,SAAS;QAC/B,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACd,IAAI,CAAC,eAAe,CAAC,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;IAC1C,CAAC;IACD,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAClD,IAAI,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,SAAS;QAC5B,IAAI,CAAC,eAAe,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC;IACpC,CAAC;IACD,MAAM,IAAI,GAAG,QAAQ,CAAC,KAAK,EAAE,wBAAwB,CAAC,CAAC;IACvD,0EAA0E;IAC1E,8EAA8E;IAC9E,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,wBAAwB,CAAC,IAAI,QAAQ,CAAC;AAC7D,CAAC;AAED,6DAA6D;AAC7D,MAAM,UAAU,kBAAkB,CAAC,QAAgB;IACjD,OAAO,gBAAgB,QAAQ,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,kBAAkB,CAAC,CAAC;AACjE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAChC,MAAyB,EACzB,MAAiB;IAEjB,OAAO,SAAS,cAAc,CAC5B,KAA0B;QAE1B,IAAI,CAAC,MAAM,CAAC,OAAO;YAAE,OAAO,SAAS,CAAC;QACtC,IAAI,CAAC,MAAM,CAAC,gBAAgB,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC;YAAE,OAAO,SAAS,CAAC;QAEnE,MAAM,WAAW,GAAG,wBAAwB,CAC1C,KAAK,CAAC,QAAQ,EACd,KAAK,CAAC,MAAM,EACZ,MAAM,CAAC,aAAa,CACrB,CAAC;QAEF,IAAI,MAAM,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;YAChC,MAAM,CAAC,KAAK,EAAE,CACZ,gCAAgC,KAAK,CAAC,QAAQ,WAAW,KAAK,CAAC,KAAK,IAAI,GAAG,GAAG,CAC/E,CAAC;QACJ,CAAC;QAED,OAAO;YACL,eAAe,EAAE;gBACf,KAAK,EAAE,kBAAkB,CAAC,KAAK,CAAC,QAAQ,CAAC;gBACzC,WAAW;gBACX,QAAQ,EAAE,UAAU;gBACpB,SAAS,EAAE,kBAAkB;gBAC7B,eAAe,EAAE,MAAM;aACxB;SACF,CAAC;IACJ,CAAC,CAAC;AACJ,CAAC"}