n8n-nodes-alterlab 0.13.0 → 0.14.1

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
@@ -71,15 +71,43 @@ npm install n8n-nodes-alterlab
71
71
 
72
72
  ### Connect Your Account
73
73
 
74
- **Option A — One-click OAuth (recommended):**
75
- 1. Add an AlterLab node to your workflow
76
- 2. Click the credential dropdown → **Create New → AlterLab OAuth2 API**
77
- 3. Click **Connect** → sign in → done. No API key copy-paste needed.
78
-
79
- **Option B — API key:**
74
+ **Option A — API key (default, works everywhere):**
80
75
  1. [Sign up free](https://app.alterlab.io/signin?redirect=/dashboard/keys&source=n8n&utm_source=n8n&utm_medium=integration&utm_campaign=community_node) and copy your API key
81
76
  2. In n8n: **Credentials → New → AlterLab API** → paste key
82
77
 
78
+ **Option B — OAuth2 (browser sign-in, no key to copy):**
79
+ 1. Add an AlterLab node and choose **Create New → AlterLab OAuth2 API**
80
+ 2. Click **Connect my account**, sign in to AlterLab and approve access
81
+
82
+ That is the whole setup on n8n Cloud and on self-hosted n8n. The credential uses OAuth 2.0 **Authorization Code with PKCE** and **Dynamic Client Registration** (RFC 7591): n8n discovers AlterLab's endpoints from `https://api.alterlab.io/.well-known/oauth-authorization-server`, registers itself for *its own* callback URL and gets a public client ID. No client secret exists, so nothing secret ships in this package and no per-instance allowlist is needed.
83
+
84
+ Requirements:
85
+ - n8n 2.10 or newer (verified on 2.10.2), which has dynamic client registration built into its OAuth2 credential. Older n8n: use Option A or the manual registration below.
86
+ - Your n8n must be reachable over **HTTPS** at its public URL (`WEBHOOK_URL` / `N8N_EDITOR_BASE_URL`); AlterLab only accepts `https://` redirect URIs for n8n-style (web) clients, so a plain `http://` callback is rejected.
87
+ - Access tokens are short lived (1 hour) and n8n refreshes them automatically with the refresh token; reconnect only if you revoke access in AlterLab.
88
+
89
+ #### Manual client registration (if you cannot use dynamic registration)
90
+
91
+ In the credential, switch **Register Client Automatically** off, then register your n8n callback once. Your callback is shown in the credential as **OAuth Redirect URL** (`https://<your-n8n>/rest/oauth2-credential/callback`):
92
+
93
+ ```bash
94
+ curl -sS -X POST https://api.alterlab.io/api/v1/oauth/register \
95
+ -H 'Content-Type: application/json' \
96
+ -d '{
97
+ "client_name": "My n8n",
98
+ "redirect_uris": ["https://n8n.example.com/rest/oauth2-credential/callback"],
99
+ "token_endpoint_auth_method": "none",
100
+ "grant_types": ["authorization_code", "refresh_token"],
101
+ "scope": "scrape"
102
+ }'
103
+ ```
104
+
105
+ Paste the returned `client_id` into **Client ID** and leave **Client Secret** empty (public PKCE client). Registration is limited to 5 requests per hour per IP address.
106
+
107
+ Supported scopes are `scrape` (read and write, enough for every operation of this node), `read`, `write` and `billing`.
108
+
109
+ > Do not put `https://api.alterlab.io/api/v1/oauth/authorize` into an *Authorization URL* field yourself: the AlterLab credential already sets the correct browser route (`https://alterlab.io/oauth/authorize`).
110
+
83
111
  ## Quick Start — Scrape a Page in 30 Seconds
84
112
 
85
113
  1. Add the **AlterLab** node to any workflow
@@ -229,6 +257,7 @@ Content type, custom headers, geo-targeting (`location.country` / `location.lang
229
257
 
230
258
  | Resource | Operations |
231
259
  |---|---|
260
+ | **Account** | Get Balance (credits, tier, auto-recharge; works with an API key or OAuth2) |
232
261
  | **Scrape** | Scrape, Estimate Cost |
233
262
  | **Crawl** | Start (polls to completion), Get Status, Cancel, Resume, Export (JSON / JSONL / CSV, one item per page), List |
234
263
  | **Search** | Web search with engine (DuckDuckGo, Google, Bing), safe search, and optional result scraping |
@@ -241,7 +270,7 @@ Scrape output also includes `jobId`, `finalUrl`, `cachedAt` / `expiresAt` / `sta
241
270
 
242
271
  ### Credential Test
243
272
 
244
- The **Test** button calls `GET /api/v1/auth/identity`, which validates the API key without spending credits.
273
+ The **Test** button calls `GET /api/v1/auth/identity`, which validates the API key or OAuth access token without spending credits.
245
274
 
246
275
  ## Frequently Asked Questions
247
276
 
@@ -263,7 +292,7 @@ Yes. AlterLab handles all major e-commerce anti-bot protection including Cloudfl
263
292
 
264
293
  ### Does AlterLab work with n8n Cloud and self-hosted n8n?
265
294
 
266
- Yes, both. Install via **Settings → Community Nodes** on n8n Cloud, or `npm install n8n-nodes-alterlab` for self-hosted. OAuth2 authentication works on both — click Connect, sign in, done.
295
+ Yes, both. Install via **Settings → Community Nodes** on n8n Cloud, or `npm install n8n-nodes-alterlab` for self-hosted. OAuth2 authentication works on both (see *Connect Your Account* for the HTTPS callback requirement on self-hosted n8n).
267
296
 
268
297
  ### Is there rate limiting?
269
298
 
@@ -1,4 +1,32 @@
1
1
  import type { ICredentialTestRequest, ICredentialType, INodeProperties } from "n8n-workflow";
2
+ /**
3
+ * AlterLab OAuth2 (Authorization Code + PKCE, public client).
4
+ *
5
+ * The credential extends n8n's built-in `oAuth2Api`, so every override below
6
+ * MUST use the inherited property names (`authUrl`, `accessTokenUrl`,
7
+ * `clientId`, ...). An override under any other name leaves the inherited
8
+ * field visible and blank (ALT-80 / #46).
9
+ *
10
+ * No secret is shipped in this package. Self-hosted n8n callback URLs differ
11
+ * per instance, so the default path uses OAuth 2.0 Dynamic Client Registration
12
+ * (RFC 7591): n8n reads AlterLab's discovery document, registers a public
13
+ * client for this instance's own callback URL and runs PKCE (S256). Users who
14
+ * cannot use dynamic registration switch it off and paste a client ID obtained
15
+ * from `POST /api/v1/oauth/register` (see README).
16
+ */
17
+ export declare const ALTERLAB_API_URL = "https://api.alterlab.io";
18
+ /** Browser-facing authorization route (a GET; the API route of the same name is POST-only on older deployments). */
19
+ export declare const ALTERLAB_AUTHORIZE_URL = "https://alterlab.io/oauth/authorize";
20
+ export declare const ALTERLAB_TOKEN_URL = "https://api.alterlab.io/api/v1/oauth/token";
21
+ /** Scope vocabulary of the AlterLab authorization server (`scopes_supported`). */
22
+ export declare const ALTERLAB_OAUTH_SCOPES: string[];
23
+ /**
24
+ * `scrape` grants the read+write API permissions every node operation needs
25
+ * (including Account > Get Balance, which is not scope-gated). Manual
26
+ * registrations should request only this; dynamic registration requests the
27
+ * full `scopes_supported` list because n8n does so itself.
28
+ */
29
+ export declare const ALTERLAB_DEFAULT_SCOPE = "scrape";
2
30
  export declare class AlterLabOAuth2Api implements ICredentialType {
3
31
  name: string;
4
32
  displayName: string;
@@ -1 +1 @@
1
- {"version":3,"file":"AlterLabOAuth2Api.credentials.d.ts","sourceRoot":"","sources":["../../credentials/AlterLabOAuth2Api.credentials.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,sBAAsB,EACtB,eAAe,EACf,eAAe,EAChB,MAAM,cAAc,CAAC;AAEtB,qBAAa,iBAAkB,YAAW,eAAe;IACvD,IAAI,SAAuB;IAC3B,WAAW,SAAyB;IACpC,gBAAgB,SACmF;IAEnG,OAAO,WAAiB;IAExB,UAAU,EAAE,eAAe,EAAE,CAiD3B;IAEF,IAAI,EAAE,sBAAsB,CAM1B;CACH"}
1
+ {"version":3,"file":"AlterLabOAuth2Api.credentials.d.ts","sourceRoot":"","sources":["../../credentials/AlterLabOAuth2Api.credentials.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,sBAAsB,EACtB,eAAe,EACf,eAAe,EAChB,MAAM,cAAc,CAAC;AAEtB;;;;;;;;;;;;;;GAcG;AAEH,eAAO,MAAM,gBAAgB,4BAA4B,CAAC;AAC1D,oHAAoH;AACpH,eAAO,MAAM,sBAAsB,wCAAwC,CAAC;AAC5E,eAAO,MAAM,kBAAkB,+CAA2C,CAAC;AAC3E,kFAAkF;AAClF,eAAO,MAAM,qBAAqB,UAAyC,CAAC;AAC5E;;;;;GAKG;AACH,eAAO,MAAM,sBAAsB,WAAW,CAAC;AAE/C,qBAAa,iBAAkB,YAAW,eAAe;IACvD,IAAI,SAAuB;IAC3B,WAAW,SAAyB;IACpC,gBAAgB,SACmF;IAEnG,OAAO,WAAiB;IAExB,UAAU,EAAE,eAAe,EAAE,CA6E3B;IAIF,IAAI,EAAE,sBAAsB,CAM1B;CACH"}
@@ -1,6 +1,34 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.AlterLabOAuth2Api = void 0;
3
+ exports.AlterLabOAuth2Api = exports.ALTERLAB_DEFAULT_SCOPE = exports.ALTERLAB_OAUTH_SCOPES = exports.ALTERLAB_TOKEN_URL = exports.ALTERLAB_AUTHORIZE_URL = exports.ALTERLAB_API_URL = void 0;
4
+ /**
5
+ * AlterLab OAuth2 (Authorization Code + PKCE, public client).
6
+ *
7
+ * The credential extends n8n's built-in `oAuth2Api`, so every override below
8
+ * MUST use the inherited property names (`authUrl`, `accessTokenUrl`,
9
+ * `clientId`, ...). An override under any other name leaves the inherited
10
+ * field visible and blank (ALT-80 / #46).
11
+ *
12
+ * No secret is shipped in this package. Self-hosted n8n callback URLs differ
13
+ * per instance, so the default path uses OAuth 2.0 Dynamic Client Registration
14
+ * (RFC 7591): n8n reads AlterLab's discovery document, registers a public
15
+ * client for this instance's own callback URL and runs PKCE (S256). Users who
16
+ * cannot use dynamic registration switch it off and paste a client ID obtained
17
+ * from `POST /api/v1/oauth/register` (see README).
18
+ */
19
+ exports.ALTERLAB_API_URL = "https://api.alterlab.io";
20
+ /** Browser-facing authorization route (a GET; the API route of the same name is POST-only on older deployments). */
21
+ exports.ALTERLAB_AUTHORIZE_URL = "https://alterlab.io/oauth/authorize";
22
+ exports.ALTERLAB_TOKEN_URL = `${exports.ALTERLAB_API_URL}/api/v1/oauth/token`;
23
+ /** Scope vocabulary of the AlterLab authorization server (`scopes_supported`). */
24
+ exports.ALTERLAB_OAUTH_SCOPES = ["scrape", "read", "write", "billing"];
25
+ /**
26
+ * `scrape` grants the read+write API permissions every node operation needs
27
+ * (including Account > Get Balance, which is not scope-gated). Manual
28
+ * registrations should request only this; dynamic registration requests the
29
+ * full `scopes_supported` list because n8n does so itself.
30
+ */
31
+ exports.ALTERLAB_DEFAULT_SCOPE = "scrape";
4
32
  class AlterLabOAuth2Api {
5
33
  constructor() {
6
34
  this.name = "alterLabOAuth2Api";
@@ -8,41 +36,64 @@ class AlterLabOAuth2Api {
8
36
  this.documentationUrl = "https://docs.alterlab.io/api?utm_source=n8n&utm_medium=integration&utm_campaign=community_node";
9
37
  this.extends = ["oAuth2Api"];
10
38
  this.properties = [
39
+ {
40
+ displayName: "Register Client Automatically",
41
+ name: "useDynamicClientRegistration",
42
+ type: "boolean",
43
+ default: true,
44
+ description: "Whether n8n registers itself with AlterLab for this instance's callback URL (OAuth Dynamic Client Registration). Turn off only to use a client ID you registered yourself, see the package README.",
45
+ },
46
+ {
47
+ displayName: "Server URL",
48
+ name: "serverUrl",
49
+ type: "hidden",
50
+ default: exports.ALTERLAB_API_URL,
51
+ },
11
52
  {
12
53
  displayName: "Grant Type",
13
54
  name: "grantType",
14
55
  type: "hidden",
15
- default: "authorizationCode",
56
+ default: "pkce",
16
57
  },
17
58
  {
18
59
  displayName: "Authorization URL",
19
- name: "authorizationUrl",
60
+ name: "authUrl",
20
61
  type: "hidden",
21
- default: "https://app.alterlab.io/oauth/authorize",
62
+ default: exports.ALTERLAB_AUTHORIZE_URL,
22
63
  },
23
64
  {
24
65
  displayName: "Access Token URL",
25
66
  name: "accessTokenUrl",
26
67
  type: "hidden",
27
- default: "https://api.alterlab.io/api/v1/oauth/token",
68
+ default: exports.ALTERLAB_TOKEN_URL,
28
69
  },
29
70
  {
30
71
  displayName: "Client ID",
31
72
  name: "clientId",
32
- type: "hidden",
33
- default: "n8n-community-node",
73
+ type: "string",
74
+ default: "",
75
+ required: true,
76
+ displayOptions: {
77
+ show: { useDynamicClientRegistration: [false] },
78
+ },
79
+ description: "The client_id returned by POST /api/v1/oauth/register for this n8n instance's callback URL",
34
80
  },
35
81
  {
36
82
  displayName: "Client Secret",
37
83
  name: "clientSecret",
38
- type: "hidden",
39
- default: "altrlab_n8n_Kx7mP2vQ9wR4jH6nB3cT5zY8",
84
+ type: "string",
85
+ typeOptions: { password: true },
86
+ default: "",
87
+ displayOptions: {
88
+ show: { useDynamicClientRegistration: [false] },
89
+ },
90
+ description: "Leave empty. AlterLab clients registered with token_endpoint_auth_method=none are public and use PKCE instead of a secret",
40
91
  },
41
92
  {
42
93
  displayName: "Scope",
43
94
  name: "scope",
44
95
  type: "hidden",
45
- default: "scrape",
96
+ default: exports.ALTERLAB_DEFAULT_SCOPE,
46
97
  },
47
98
  {
48
99
  displayName: "Auth URI Query Parameters",
@@ -51,15 +102,19 @@ class AlterLabOAuth2Api {
51
102
  default: "",
52
103
  },
53
104
  {
105
+ // Public client: no client secret is sent, so credentials in the
106
+ // Authorization header (the n8n default) are never produced.
54
107
  displayName: "Authentication",
55
108
  name: "authentication",
56
109
  type: "hidden",
57
- default: "body",
110
+ default: "header",
58
111
  },
59
112
  ];
113
+ // GET /api/v1/auth/identity accepts X-API-Key, Bearer sk_ and Bearer alo_at_
114
+ // (OAuth) credentials, spends no credits and does not need a scope.
60
115
  this.test = {
61
116
  request: {
62
- baseURL: "https://api.alterlab.io",
117
+ baseURL: exports.ALTERLAB_API_URL,
63
118
  url: "/api/v1/auth/identity",
64
119
  method: "GET",
65
120
  },
@@ -1 +1 @@
1
- {"version":3,"file":"AlterLabOAuth2Api.credentials.js","sourceRoot":"","sources":["../../credentials/AlterLabOAuth2Api.credentials.ts"],"names":[],"mappings":";;;AAMA,MAAa,iBAAiB;IAA9B;QACE,SAAI,GAAG,mBAAmB,CAAC;QAC3B,gBAAW,GAAG,qBAAqB,CAAC;QACpC,qBAAgB,GACd,gGAAgG,CAAC;QAEnG,YAAO,GAAG,CAAC,WAAW,CAAC,CAAC;QAExB,eAAU,GAAsB;YAC9B;gBACE,WAAW,EAAE,YAAY;gBACzB,IAAI,EAAE,WAAW;gBACjB,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE,mBAAmB;aAC7B;YACD;gBACE,WAAW,EAAE,mBAAmB;gBAChC,IAAI,EAAE,kBAAkB;gBACxB,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE,yCAAyC;aACnD;YACD;gBACE,WAAW,EAAE,kBAAkB;gBAC/B,IAAI,EAAE,gBAAgB;gBACtB,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE,4CAA4C;aACtD;YACD;gBACE,WAAW,EAAE,WAAW;gBACxB,IAAI,EAAE,UAAU;gBAChB,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE,oBAAoB;aAC9B;YACD;gBACE,WAAW,EAAE,eAAe;gBAC5B,IAAI,EAAE,cAAc;gBACpB,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE,sCAAsC;aAChD;YACD;gBACE,WAAW,EAAE,OAAO;gBACpB,IAAI,EAAE,OAAO;gBACb,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE,QAAQ;aAClB;YACD;gBACE,WAAW,EAAE,2BAA2B;gBACxC,IAAI,EAAE,qBAAqB;gBAC3B,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE,EAAE;aACZ;YACD;gBACE,WAAW,EAAE,gBAAgB;gBAC7B,IAAI,EAAE,gBAAgB;gBACtB,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE,MAAM;aAChB;SACF,CAAC;QAEF,SAAI,GAA2B;YAC7B,OAAO,EAAE;gBACP,OAAO,EAAE,yBAAyB;gBAClC,GAAG,EAAE,uBAAuB;gBAC5B,MAAM,EAAE,KAAK;aACd;SACF,CAAC;IACJ,CAAC;CAAA;AAlED,8CAkEC"}
1
+ {"version":3,"file":"AlterLabOAuth2Api.credentials.js","sourceRoot":"","sources":["../../credentials/AlterLabOAuth2Api.credentials.ts"],"names":[],"mappings":";;;AAMA;;;;;;;;;;;;;;GAcG;AAEU,QAAA,gBAAgB,GAAG,yBAAyB,CAAC;AAC1D,oHAAoH;AACvG,QAAA,sBAAsB,GAAG,qCAAqC,CAAC;AAC/D,QAAA,kBAAkB,GAAG,GAAG,wBAAgB,qBAAqB,CAAC;AAC3E,kFAAkF;AACrE,QAAA,qBAAqB,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,CAAC,CAAC;AAC5E;;;;;GAKG;AACU,QAAA,sBAAsB,GAAG,QAAQ,CAAC;AAE/C,MAAa,iBAAiB;IAA9B;QACE,SAAI,GAAG,mBAAmB,CAAC;QAC3B,gBAAW,GAAG,qBAAqB,CAAC;QACpC,qBAAgB,GACd,gGAAgG,CAAC;QAEnG,YAAO,GAAG,CAAC,WAAW,CAAC,CAAC;QAExB,eAAU,GAAsB;YAC9B;gBACE,WAAW,EAAE,+BAA+B;gBAC5C,IAAI,EAAE,8BAA8B;gBACpC,IAAI,EAAE,SAAS;gBACf,OAAO,EAAE,IAAI;gBACb,WAAW,EACT,oMAAoM;aACvM;YACD;gBACE,WAAW,EAAE,YAAY;gBACzB,IAAI,EAAE,WAAW;gBACjB,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE,wBAAgB;aAC1B;YACD;gBACE,WAAW,EAAE,YAAY;gBACzB,IAAI,EAAE,WAAW;gBACjB,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE,MAAM;aAChB;YACD;gBACE,WAAW,EAAE,mBAAmB;gBAChC,IAAI,EAAE,SAAS;gBACf,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE,8BAAsB;aAChC;YACD;gBACE,WAAW,EAAE,kBAAkB;gBAC/B,IAAI,EAAE,gBAAgB;gBACtB,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE,0BAAkB;aAC5B;YACD;gBACE,WAAW,EAAE,WAAW;gBACxB,IAAI,EAAE,UAAU;gBAChB,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE,EAAE;gBACX,QAAQ,EAAE,IAAI;gBACd,cAAc,EAAE;oBACd,IAAI,EAAE,EAAE,4BAA4B,EAAE,CAAC,KAAK,CAAC,EAAE;iBAChD;gBACD,WAAW,EACT,4FAA4F;aAC/F;YACD;gBACE,WAAW,EAAE,eAAe;gBAC5B,IAAI,EAAE,cAAc;gBACpB,IAAI,EAAE,QAAQ;gBACd,WAAW,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE;gBAC/B,OAAO,EAAE,EAAE;gBACX,cAAc,EAAE;oBACd,IAAI,EAAE,EAAE,4BAA4B,EAAE,CAAC,KAAK,CAAC,EAAE;iBAChD;gBACD,WAAW,EACT,2HAA2H;aAC9H;YACD;gBACE,WAAW,EAAE,OAAO;gBACpB,IAAI,EAAE,OAAO;gBACb,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE,8BAAsB;aAChC;YACD;gBACE,WAAW,EAAE,2BAA2B;gBACxC,IAAI,EAAE,qBAAqB;gBAC3B,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE,EAAE;aACZ;YACD;gBACE,iEAAiE;gBACjE,6DAA6D;gBAC7D,WAAW,EAAE,gBAAgB;gBAC7B,IAAI,EAAE,gBAAgB;gBACtB,IAAI,EAAE,QAAQ;gBACd,OAAO,EAAE,QAAQ;aAClB;SACF,CAAC;QAEF,6EAA6E;QAC7E,oEAAoE;QACpE,SAAI,GAA2B;YAC7B,OAAO,EAAE;gBACP,OAAO,EAAE,wBAAgB;gBACzB,GAAG,EAAE,uBAAuB;gBAC5B,MAAM,EAAE,KAAK;aACd;SACF,CAAC;IACJ,CAAC;CAAA;AAhGD,8CAgGC"}
@@ -1 +1 @@
1
- {"version":3,"file":"AlterLab.node.d.ts","sourceRoot":"","sources":["../../../nodes/AlterLab/AlterLab.node.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,iBAAiB,EAEjB,kBAAkB,EAClB,SAAS,EACT,oBAAoB,EAErB,MAAM,cAAc,CAAC;AAkCtB,qBAAa,QAAS,YAAW,SAAS;IACxC,WAAW,EAAE,oBAAoB,CA+hF/B;IAEI,OAAO,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,kBAAkB,EAAE,EAAE,CAAC;CAgqDxE"}
1
+ {"version":3,"file":"AlterLab.node.d.ts","sourceRoot":"","sources":["../../../nodes/AlterLab/AlterLab.node.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,iBAAiB,EAEjB,kBAAkB,EAClB,SAAS,EACT,oBAAoB,EAErB,MAAM,cAAc,CAAC;AAmCtB,qBAAa,QAAS,YAAW,SAAS;IACxC,WAAW,EAAE,oBAAoB,CA6yG/B;IAEI,OAAO,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,kBAAkB,EAAE,EAAE,CAAC;CA4rDxE"}