@chrischall/mcp-utils 0.7.0 → 0.8.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
@@ -28,7 +28,7 @@ import light:
28
28
  | Import | Contents |
29
29
  | --- | --- |
30
30
  | `@chrischall/mcp-utils` | core barrel: `server` + `response` + `errors` + `config` + `fs` + `http` + `zod` + `auth` |
31
- | `@chrischall/mcp-utils/session` | session registry, session store, token manager |
31
+ | `@chrischall/mcp-utils/session` | session registry, session store, token manager, cookie-session manager |
32
32
  | `@chrischall/mcp-utils/fetchproxy` | fetchproxy transport adapter, bot-wall / retry / concurrency helpers |
33
33
  | `@chrischall/mcp-utils/html` | opt-in HTML scraping helpers (needs `node-html-parser`) |
34
34
  | `@chrischall/mcp-utils/test` | in-memory test harness for tool registration |
@@ -109,19 +109,46 @@ tool surface can show the user.
109
109
 
110
110
  ### `config` — hardened env/config
111
111
 
112
- `readEnvVar`, `requireEnvVar`, `parseBoolEnv`, `expandPath`, `loadDotenvSafely`.
112
+ `readEnvVar`, `requireEnvVar`, `parseBoolEnv`, `readPortEnv`, `expandPath`,
113
+ `loadDotenvSafely`, `createCachedJsonArrayLoader`.
113
114
 
114
115
  ```ts
115
- import { requireEnvVar, parseBoolEnv, expandPath } from '@chrischall/mcp-utils';
116
+ import { requireEnvVar, parseBoolEnv, readPortEnv, expandPath } from '@chrischall/mcp-utils';
116
117
 
117
118
  const apiKey = requireEnvVar('MY_API_KEY');
118
119
  const debug = parseBoolEnv('MY_DEBUG', { default: false });
120
+ const port = readPortEnv('MY_WS_PORT', 37149); // placeholder/NaN/out-of-range → fallback
119
121
  const home = expandPath('~/.config/my-mcp');
120
122
  ```
121
123
 
124
+ `readPortEnv` parses a TCP port with the same placeholder hardening as
125
+ `readEnvVar`, plus integer + `1..65535` range validation — so an unexpanded
126
+ `${MY_WS_PORT}` or junk falls back to the default instead of handing `NaN` to
127
+ the server.
128
+
122
129
  `loadDotenvSafely` is a no-throw `.env` loader (returns `false` instead of
123
130
  failing when the file is absent).
124
131
 
132
+ `createCachedJsonArrayLoader` builds a cached, negative-cached loader for an
133
+ env-named JSON string-array file — the `loadCommunities`/`DEFAULT_COMMUNITIES`
134
+ pattern shared across the realty servers:
135
+
136
+ ```ts
137
+ import { createCachedJsonArrayLoader } from '@chrischall/mcp-utils';
138
+
139
+ const loadCommunities = createCachedJsonArrayLoader({
140
+ envVar: 'REDFIN_COMMUNITIES_FILE', // path to a JSON string-array file
141
+ defaults: DEFAULT_COMMUNITIES, // returned when unset/missing/invalid
142
+ label: 'redfin-mcp',
143
+ });
144
+
145
+ const communities = loadCommunities(); // parses + caches; re-reads only on path change
146
+ ```
147
+
148
+ A successful parse is cached; a missing/unreadable file, invalid JSON, or a
149
+ non-string-array logs one stderr warning and negative-caches (returns defaults
150
+ without re-reading). Pass `readFile` to inject a reader in tests.
151
+
125
152
  ### `fs` — streaming file helpers (uploads)
126
153
 
127
154
  `fileBlob`, `readFileHead`.
@@ -145,10 +172,15 @@ constant memory instead of a 20 MB Buffer.
145
172
  ### `http` — bearer API-client kit
146
173
 
147
174
  `createApiClient` plus building blocks: `buildQueryString`, `buildOptionalBody`,
148
- `formatApiError`, `parseLinkHeader`, `parseCookieJar`, JWT helpers
149
- (`decodeJwtExp`, `decodeJwtSessionId`, `validateJwtExpiry`), and the
175
+ `formatApiError`, `parseLinkHeader`, `parseCookieJar`, `parseCookieHeader`,
176
+ `runBoundedBatch`, JWT helpers (`decodeJwtExp`, `decodeJwtSessionId`,
177
+ `decodeJwtClaim`, `validateJwtExpiry`), and the `ApiError` / `UpstreamHttpError` /
150
178
  `UnauthorizedError` / `RateLimitedError` / `RequestTimeoutError` classes.
151
179
 
180
+ `decodeJwtClaim(token, claim)` is the generic single-claim reader — returns the
181
+ raw claim value (`unknown`) or `undefined` for an undecodable token / absent
182
+ claim, so a repo doesn't hand-roll its own `extractXFromJwt`.
183
+
152
184
  ```ts
153
185
  import { createApiClient } from '@chrischall/mcp-utils';
154
186
 
@@ -167,6 +199,37 @@ const data = await api.get('/v1/things', { query: { page: 2 } });
167
199
  `RequestTimeoutError` instead of hanging the tool call. A 429 retry gets a fresh
168
200
  timeout. Omit it to keep the previous unbounded behavior.
169
201
 
202
+ `parseCookieHeader(header)` parses an inbound *request* `Cookie:` header
203
+ (`name=value; name2=value2`) into a `Record<string, string>` (first `=` splits,
204
+ so values may contain `=`; last value wins on a duplicate name). It's the
205
+ counterpart to `parseCookieJar`, which parses *response* `Set-Cookie` headers
206
+ with their attributes and deletion semantics.
207
+
208
+ `UpstreamHttpError(status, message)` is a directly-`throw new`-able,
209
+ status-carrying HTTP error — the manual-throw parallel to `ApiError` (which
210
+ `createApiClient` throws internally). It `extends ApiError`, so both the
211
+ `err instanceof ApiError && err.status === 404` branch and a narrower
212
+ `instanceof UpstreamHttpError` check work. Use it from a transport/bridge code
213
+ path that doesn't route through `createApiClient` but still needs to branch on a
214
+ 404.
215
+
216
+ ```ts
217
+ import { runBoundedBatch } from '@chrischall/mcp-utils';
218
+
219
+ const rows = await runBoundedBatch(ids, (id, signal) => fetchRow(id, signal), {
220
+ deadlineMs: 45_000, // overall hard deadline for the whole batch
221
+ concurrency: 4, // optional fan-out cap
222
+ onTimeout: (id, i) => ({ id, pending: true }), // backfill any row the deadline cut off
223
+ });
224
+ ```
225
+
226
+ `runBoundedBatch(items, worker, opts)` races the whole batch against one overall
227
+ `deadlineMs`; any item still unsettled when it fires is filled by
228
+ `onTimeout(item, index)` (and its worker abandoned + `AbortSignal`-signalled) so
229
+ a single hung row can't wedge the call. It always returns a full-length,
230
+ input-ordered array. `setTimer`/`clearTimer` are injectable for tests. This
231
+ hoists zillow's bulk-tool deadline + `pending`-backfill primitive.
232
+
170
233
  ### `dates` — date-format converters
171
234
 
172
235
  `isoToDmy`, `dmyToIso`, `isoToCompactTimestamp`. For upstreams that don't speak
@@ -184,7 +247,8 @@ deepMapStringField(payload, 'eventDate', dmyToIso); // '28-08-2025' → '202
184
247
  ### `zod` — schema atoms
185
248
 
186
249
  Reusable schemas (`PositiveInt`, `NonNegInt`, `NonEmptyString`, `IsoDate`,
187
- `IsoTime`, `schemaOrigin`, `schemaConfirm`), pagination helpers
250
+ `IsoTime`, `NumericIdString`, `SafePathSegment`, `schemaOrigin`,
251
+ `schemaConfirm`), pagination helpers
188
252
  (`paginationSchema`, `pageSchema`, `calculateOffset`), tool-annotation builders
189
253
  (`toolAnnotations`), and time normalizers (`extractTime`, `normalizeTime`).
190
254
 
@@ -196,6 +260,11 @@ const offset = calculateOffset(page, size);
196
260
  const annotations = toolAnnotations({ readOnly: true });
197
261
  ```
198
262
 
263
+ `NumericIdString` (`/^\d+$/`) and `SafePathSegment` (rejects `/`, `..`, `?`,
264
+ `#`, and whitespace) harden caller-supplied ids that get interpolated into
265
+ request paths — defense-in-depth against path traversal and query/fragment
266
+ injection.
267
+
199
268
  ### `auth` — auth resolver skeletons
200
269
 
201
270
  `createAuthResolver`, `resolveAuthPattern`, `sessionLoginFlow`,
@@ -209,13 +278,14 @@ const resolver = createAuthResolver({ /* ... */ });
209
278
  const refresh = createOAuth2Refresher({ /* ... */ });
210
279
  ```
211
280
 
212
- ### `session` — session registry & token manager *(subpath)*
281
+ ### `session` — session registry, token manager & cookie-session manager *(subpath)*
213
282
 
214
283
  ```ts
215
284
  import {
216
285
  createSessionRegistry,
217
286
  registerSessionTools,
218
287
  TokenManager,
288
+ CookieSessionManager,
219
289
  } from '@chrischall/mcp-utils/session';
220
290
 
221
291
  const registry = createSessionRegistry();
@@ -225,12 +295,40 @@ registerSessionTools(server, { registry /* ... */ });
225
295
  Includes `SessionStore`, `normalizeOrigin`, `AuthMode`, and `TokenManager`
226
296
  (with `TOKEN_REFRESH_SKEW_MS` for proactive refresh).
227
297
 
298
+ `CookieSessionManager<S>` is the cookie-session analog of `TokenManager` for
299
+ sites authenticated by a browser-style cookie session rather than a bearer
300
+ token. It owns *when* to log in (single-flight, so concurrent callers coalesce
301
+ into ONE login), clears the in-flight promise on settle (a rejected login never
302
+ sticks — the next `ensure()` retries), and `withSession()` re-logs-in and
303
+ replays a request **exactly once** on a detected expiry (no infinite loop). The
304
+ injected `isExpired(res)` predicate is the hook for body/URL heuristics — so a
305
+ `200` serving an HTML login page or a redirect away from the target is treated
306
+ as expired, not just `401`/`403`. An optional `isPermanentError` caches genuine
307
+ missing-config errors while leaving transient login failures retryable.
308
+
309
+ ```ts
310
+ const sessions = new CookieSessionManager<{ cookieHeader: string; csrfToken?: string }>({
311
+ login: () => loginWithPassword(), // mints a fresh cookie session
312
+ isExpired: async (res) =>
313
+ res.status === 401 || /<form[^>]*id="login"/i.test(await res.clone().text()),
314
+ });
315
+
316
+ const res = await sessions.withSession((s) =>
317
+ fetch(url, { headers: { cookie: s.cookieHeader } }),
318
+ );
319
+ ```
320
+
321
+ Replaces the hand-rolled re-login / single-flight / 401-replay code in
322
+ `artsonia-mcp`, `canvas-parent-mcp`, `evite-mcp`, `signupgenius-mcp`, and
323
+ `skylight-mcp`.
324
+
228
325
  ### `fetchproxy` — transport adapter *(subpath, optional peer)*
229
326
 
230
327
  ```ts
231
328
  import {
232
329
  createFetchproxyTransport,
233
330
  createBootstrapOpts,
331
+ registerBridgeHealthcheckTool,
234
332
  mapWithConcurrency,
235
333
  TokenBucket,
236
334
  classifyBotWall,
@@ -241,6 +339,43 @@ Wraps `@fetchproxy/server` with the fleet's transport, bot-wall classification,
241
339
  deadline/retry, token-bucket rate limiting, and bounded-concurrency helpers, and
242
340
  re-exports the fetchproxy typed-error hierarchy.
243
341
 
342
+ **Transport verb adapters.** Beyond the `start` / `close` / `status` lifecycle,
343
+ `createFetchproxyTransport` exposes the verb passthroughs redfin / homes /
344
+ compass / musescore had each hand-rolled over the server:
345
+
346
+ - `fetch(init)` → `{ status, body, url }` via `server.request(...)`;
347
+ - `requestJson(method, path, init?)` → `{ data, result }` via
348
+ `server.requestJson(...)` (serialization + header defaults + 204→null +
349
+ `JSON.parse`; the caller keeps its per-site `throwIfNotOk` over `result`);
350
+ - `runProbe(fetchFn, probePath)` → the healthcheck probe loop.
351
+
352
+ The one per-site bit is the subdomain: pass `defaultSubdomain: 'www'` for sites
353
+ served from `www` (redfin/homes/compass); omit it for apex-served sites
354
+ (musescore). A per-call `subdomain` always overrides the default, and absolute
355
+ `http(s)://` paths self-describe their host. Other per-site verbs (e.g.
356
+ musescore's `download` capability) stay caller-supplied — the factory covers the
357
+ common subset, not the long tail.
358
+
359
+ **Bridge-healthcheck tool factory.** `registerBridgeHealthcheckTool({ server,
360
+ prefix, probePath, hostLabel, transport, probeFn })` registers a
361
+ `<prefix>_healthcheck` tool that round-trips `probePath` through the bridge and
362
+ reports bridge role / port / timing plus an actionable hint ladder
363
+ (`bridge_down` → wake the SW, `role === null` → check startup, `timeout` →
364
+ extension not connected, …). The failure hint cites the **actual configured
365
+ bridge port** from `bridgeHealth()`, not a hardcoded `37149` — fixing the bug
366
+ the per-site compass + musescore copies shared.
367
+
368
+ ```ts
369
+ registerBridgeHealthcheckTool({
370
+ server,
371
+ prefix: 'compass',
372
+ probePath: '/robots.txt',
373
+ hostLabel: 'compass.com',
374
+ transport,
375
+ probeFn: (path) => client.fetchHtml(path),
376
+ });
377
+ ```
378
+
244
379
  ### `html` — scraping helpers *(subpath, optional peer)*
245
380
 
246
381
  ```ts
@@ -62,6 +62,72 @@ export declare function parseBoolEnv(key: string, opts?: ParseBoolEnvOptions): b
62
62
  * NOT expanded — only the current user's home is ever substituted.
63
63
  */
64
64
  export declare function expandPath(p: string): string;
65
+ /** Options for {@link readPortEnv}. */
66
+ export interface ReadPortEnvOptions {
67
+ /** The source to read from. Defaults to {@link process.env}. */
68
+ env?: EnvSource;
69
+ }
70
+ /**
71
+ * Read a TCP port from an environment variable, hardened the way
72
+ * {@link readEnvVar} hardens any var (trim, treat blank / `'undefined'` /
73
+ * `'null'` / unsubstituted `${...}` placeholder as unset) PLUS numeric
74
+ * validation: the value must parse to an integer in the valid port range
75
+ * `1..65535`.
76
+ *
77
+ * Returns `fallback` when the variable is unset, a placeholder, non-numeric, or
78
+ * out of range. Consolidates the bare `Number(process.env.X_WS_PORT)` pattern
79
+ * across compass/redfin/homes/musescore, which yields `NaN` on an unexpanded
80
+ * `${...}` placeholder or junk and then hands `NaN` to the server.
81
+ *
82
+ * @example readPortEnv('REDFIN_WS_PORT', 37149)
83
+ */
84
+ export declare function readPortEnv(key: string, fallback: number, opts?: ReadPortEnvOptions): number;
85
+ /** A minimal injectable file reader: returns the file's UTF-8 contents. */
86
+ export type ReadFileSyncFn = (path: string) => string;
87
+ /** Options for {@link createCachedJsonArrayLoader}. */
88
+ export interface CachedJsonArrayLoaderOptions {
89
+ /** Name of the env var holding the path to a JSON string-array file. */
90
+ envVar: string;
91
+ /** Returned when the var is unset, or the file is missing/unreadable/invalid. */
92
+ defaults: string[];
93
+ /** The source to read env from. Defaults to {@link process.env}. */
94
+ env?: EnvSource;
95
+ /**
96
+ * Injectable file reader (for tests). Defaults to a `node:fs` reader that
97
+ * throws when the file is missing — a missing file is caught and
98
+ * negative-cached like any other read failure.
99
+ */
100
+ readFile?: ReadFileSyncFn;
101
+ /**
102
+ * Label woven into the stderr warning on a missing/invalid file (e.g.
103
+ * `'redfin-mcp'`). Defaults to the env-var name.
104
+ */
105
+ label?: string;
106
+ }
107
+ /**
108
+ * Build a cached, negative-cached loader for an env-named JSON string-array
109
+ * file — the `loadCommunities` + `DEFAULT_COMMUNITIES` pattern quadruplicated
110
+ * across redfin/zillow/homes/onehome (only the env var differs:
111
+ * `REDFIN_/ZILLOW_/HOMES_/ONEHOME_COMMUNITIES_FILE`).
112
+ *
113
+ * The returned function:
114
+ * - reads the file path from `envVar` via {@link readEnvVar} (placeholder
115
+ * hardening), returning `defaults` when unset (and clearing any cache),
116
+ * - parses the file as a JSON array of strings; on success caches and returns
117
+ * it (keyed by the env-var value, so a path change re-reads),
118
+ * - on a missing / unreadable file, invalid JSON, or non-string-array,
119
+ * logs a single stderr warning and **negative-caches** — it returns
120
+ * `defaults` without re-reading on subsequent calls for the same path,
121
+ * - never re-reads a successfully-cached file.
122
+ *
123
+ * @example
124
+ * const loadCommunities = createCachedJsonArrayLoader({
125
+ * envVar: 'REDFIN_COMMUNITIES_FILE',
126
+ * defaults: DEFAULT_COMMUNITIES,
127
+ * label: 'redfin-mcp',
128
+ * });
129
+ */
130
+ export declare function createCachedJsonArrayLoader(opts: CachedJsonArrayLoaderOptions): () => string[];
65
131
  /** Options for {@link loadDotenvSafely}. */
66
132
  export interface LoadDotenvOptions {
67
133
  /** Path to the `.env` file. Defaults to dotenv's own resolution (CWD). */
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/config/index.ts"],"names":[],"mappings":"AAGA,+DAA+D;AAC/D,MAAM,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;AAE3D,iDAAiD;AACjD,MAAM,WAAW,cAAc;IAC7B,gEAAgE;IAChE,GAAG,CAAC,EAAE,SAAS,CAAC;IAChB,kDAAkD;IAClD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAaD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,GAAE,cAAmB,GAAG,MAAM,GAAG,SAAS,CAerF;AAED,yCAAyC;AACzC,MAAM,WAAW,iBAAiB;IAChC,gEAAgE;IAChE,GAAG,CAAC,EAAE,SAAS,CAAC;IAChB,8EAA8E;IAC9E,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,GAAE,iBAAsB,GAAG,MAAM,CAO/E;AAED,wCAAwC;AACxC,MAAM,WAAW,mBAAmB;IAClC,gEAAgE;IAChE,GAAG,CAAC,EAAE,SAAS,CAAC;IAChB,8EAA8E;IAC9E,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAKD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,GAAE,mBAAwB,GAAG,OAAO,CAQjF;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAQ5C;AAED,4CAA4C;AAC5C,MAAM,WAAW,iBAAiB;IAChC,0EAA0E;IAC1E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;GASG;AACH,wBAAsB,gBAAgB,CAAC,IAAI,GAAE,iBAAsB,GAAG,OAAO,CAAC,OAAO,CAAC,CAwBrF"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/config/index.ts"],"names":[],"mappings":"AAIA,+DAA+D;AAC/D,MAAM,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;AAE3D,iDAAiD;AACjD,MAAM,WAAW,cAAc;IAC7B,gEAAgE;IAChE,GAAG,CAAC,EAAE,SAAS,CAAC;IAChB,kDAAkD;IAClD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAaD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,GAAE,cAAmB,GAAG,MAAM,GAAG,SAAS,CAerF;AAED,yCAAyC;AACzC,MAAM,WAAW,iBAAiB;IAChC,gEAAgE;IAChE,GAAG,CAAC,EAAE,SAAS,CAAC;IAChB,8EAA8E;IAC9E,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,GAAE,iBAAsB,GAAG,MAAM,CAO/E;AAED,wCAAwC;AACxC,MAAM,WAAW,mBAAmB;IAClC,gEAAgE;IAChE,GAAG,CAAC,EAAE,SAAS,CAAC;IAChB,8EAA8E;IAC9E,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAKD;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,GAAE,mBAAwB,GAAG,OAAO,CAQjF;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAQ5C;AAED,uCAAuC;AACvC,MAAM,WAAW,kBAAkB;IACjC,gEAAgE;IAChE,GAAG,CAAC,EAAE,SAAS,CAAC;CACjB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,GAAE,kBAAuB,GAAG,MAAM,CAShG;AAED,2EAA2E;AAC3E,MAAM,MAAM,cAAc,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;AAEtD,uDAAuD;AACvD,MAAM,WAAW,4BAA4B;IAC3C,wEAAwE;IACxE,MAAM,EAAE,MAAM,CAAC;IACf,iFAAiF;IACjF,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,oEAAoE;IACpE,GAAG,CAAC,EAAE,SAAS,CAAC;IAChB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B;;;OAGG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,2BAA2B,CAAC,IAAI,EAAE,4BAA4B,GAAG,MAAM,MAAM,EAAE,CAoD9F;AAED,4CAA4C;AAC5C,MAAM,WAAW,iBAAiB;IAChC,0EAA0E;IAC1E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;GASG;AACH,wBAAsB,gBAAgB,CAAC,IAAI,GAAE,iBAAsB,GAAG,OAAO,CAAC,OAAO,CAAC,CAwBrF"}
@@ -1,3 +1,4 @@
1
+ import { readFileSync } from 'node:fs';
1
2
  import { homedir } from 'node:os';
2
3
  import { isAbsolute, join, resolve } from 'node:path';
3
4
  /**
@@ -93,6 +94,102 @@ export function expandPath(p) {
93
94
  }
94
95
  return isAbsolute(expanded) ? expanded : resolve(expanded);
95
96
  }
97
+ /**
98
+ * Read a TCP port from an environment variable, hardened the way
99
+ * {@link readEnvVar} hardens any var (trim, treat blank / `'undefined'` /
100
+ * `'null'` / unsubstituted `${...}` placeholder as unset) PLUS numeric
101
+ * validation: the value must parse to an integer in the valid port range
102
+ * `1..65535`.
103
+ *
104
+ * Returns `fallback` when the variable is unset, a placeholder, non-numeric, or
105
+ * out of range. Consolidates the bare `Number(process.env.X_WS_PORT)` pattern
106
+ * across compass/redfin/homes/musescore, which yields `NaN` on an unexpanded
107
+ * `${...}` placeholder or junk and then hands `NaN` to the server.
108
+ *
109
+ * @example readPortEnv('REDFIN_WS_PORT', 37149)
110
+ */
111
+ export function readPortEnv(key, fallback, opts = {}) {
112
+ const raw = readEnvVar(key, { env: opts.env });
113
+ if (raw === undefined)
114
+ return fallback;
115
+ // Strict integer parse: reject `12abc`, `1.5`, `0x10`, etc. that `Number`
116
+ // would otherwise coerce or accept.
117
+ if (!/^\d+$/.test(raw))
118
+ return fallback;
119
+ const port = Number(raw);
120
+ if (!Number.isInteger(port) || port < 1 || port > 65535)
121
+ return fallback;
122
+ return port;
123
+ }
124
+ /**
125
+ * Build a cached, negative-cached loader for an env-named JSON string-array
126
+ * file — the `loadCommunities` + `DEFAULT_COMMUNITIES` pattern quadruplicated
127
+ * across redfin/zillow/homes/onehome (only the env var differs:
128
+ * `REDFIN_/ZILLOW_/HOMES_/ONEHOME_COMMUNITIES_FILE`).
129
+ *
130
+ * The returned function:
131
+ * - reads the file path from `envVar` via {@link readEnvVar} (placeholder
132
+ * hardening), returning `defaults` when unset (and clearing any cache),
133
+ * - parses the file as a JSON array of strings; on success caches and returns
134
+ * it (keyed by the env-var value, so a path change re-reads),
135
+ * - on a missing / unreadable file, invalid JSON, or non-string-array,
136
+ * logs a single stderr warning and **negative-caches** — it returns
137
+ * `defaults` without re-reading on subsequent calls for the same path,
138
+ * - never re-reads a successfully-cached file.
139
+ *
140
+ * @example
141
+ * const loadCommunities = createCachedJsonArrayLoader({
142
+ * envVar: 'REDFIN_COMMUNITIES_FILE',
143
+ * defaults: DEFAULT_COMMUNITIES,
144
+ * label: 'redfin-mcp',
145
+ * });
146
+ */
147
+ export function createCachedJsonArrayLoader(opts) {
148
+ const env = opts.env ?? process.env;
149
+ const label = opts.label ?? opts.envVar;
150
+ // Default reader: `node:fs#readFileSync` (utf8). A missing file throws, which
151
+ // the catch below turns into a negative-cached fallback — mirroring redfin's
152
+ // original `existsSync` guard without the extra stat.
153
+ const readFile = opts.readFile ?? ((path) => readFileSync(path, 'utf8'));
154
+ let cached = null;
155
+ // Path the cache (positive OR negative) is keyed to. A negative cache is
156
+ // `cached === null && cachedPath === <path>`: we resolved that path to
157
+ // defaults and won't re-read it.
158
+ let cachedPath = null;
159
+ return function load() {
160
+ const path = readEnvVar(opts.envVar, { env });
161
+ if (!path) {
162
+ // Unset: never cache (the var may be set later) and return defaults.
163
+ cached = null;
164
+ cachedPath = null;
165
+ return opts.defaults;
166
+ }
167
+ if (cachedPath === path) {
168
+ // Same path as last time — positive cache (return it) or negative cache
169
+ // (cached === null → return defaults without re-reading).
170
+ return cached ?? opts.defaults;
171
+ }
172
+ try {
173
+ const raw = readFile(path);
174
+ const parsed = JSON.parse(raw);
175
+ if (!Array.isArray(parsed) || !parsed.every((s) => typeof s === 'string')) {
176
+ console.error(`[${label}] ${opts.envVar}="${path}" must be a JSON string array — falling back to defaults.`);
177
+ cached = null;
178
+ cachedPath = path; // negative-cache this path
179
+ return opts.defaults;
180
+ }
181
+ cached = parsed;
182
+ cachedPath = path;
183
+ return cached;
184
+ }
185
+ catch (err) {
186
+ console.error(`[${label}] failed to load ${opts.envVar}="${path}": ${err instanceof Error ? err.message : String(err)} — falling back to defaults.`);
187
+ cached = null;
188
+ cachedPath = path; // negative-cache this path
189
+ return opts.defaults;
190
+ }
191
+ };
192
+ }
96
193
  /**
97
194
  * Load a `.env` file for local development, swallowing any failure.
98
195
  *
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/config/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/config/index.ts"],"names":[],"mappings":"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"}
@@ -9,7 +9,13 @@
9
9
  *
10
10
  * - {@link createFetchproxyTransport} wraps a `FetchproxyServer` in the
11
11
  * `start` / `close` / `status` lifecycle every MCP's transport interface
12
- * expects, with optional debug-gated role logging.
12
+ * expects, with optional debug-gated role logging, PLUS the opt-in verb
13
+ * passthroughs (`fetch` / `requestJson` / `runProbe`) that redfin / homes /
14
+ * compass / musescore had each hand-rolled over the server.
15
+ * - {@link registerBridgeHealthcheckTool} registers a `<prefix>_healthcheck`
16
+ * tool that round-trips a probe path through the bridge and surfaces the
17
+ * actionable hint ladder compass + musescore had each copied (with drifted
18
+ * internals + a hardcoded-port bug) into `src/tools/healthcheck.ts`.
13
19
  * - {@link createBootstrapOpts} assembles a multi-domain / capture-header /
14
20
  * storage-pointer declaration fragment of `FetchproxyServerOpts`, deriving
15
21
  * the required `capabilities` from the declared bootstrap so callers can't
@@ -18,10 +24,52 @@
18
24
  * The adapter shape is identical across 12+ fetchproxy MCPs; collapsing it here
19
25
  * keeps the per-row / concurrency / deadline helpers as re-exports rather than
20
26
  * re-rolled code.
27
+ *
28
+ * Lazy-import note: this whole subpath is gated behind the OPTIONAL
29
+ * `@fetchproxy/server` peer dep — a consumer only resolves it by importing
30
+ * `@chrischall/mcp-utils/fetchproxy`, never via the core barrel. The eager
31
+ * top-level import below therefore never reaches a `.mcpb` bundle that
32
+ * externalizes `@fetchproxy/server` unless that consumer actually opted into
33
+ * the bridge. Everything added here routes through that same already-imported
34
+ * `FetchproxyServer` instance (no NEW top-level `@fetchproxy/server` import),
35
+ * so the bundle-load smoke posture is unchanged.
21
36
  */
22
- import { FetchproxyServer, type FetchproxyServerOpts } from '@fetchproxy/server';
37
+ import { FetchproxyServer, type FetchproxyServerOpts, type HttpResponse, type BridgeProbeResult } from '@fetchproxy/server';
38
+ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
23
39
  export { FetchproxyServer, mapWithConcurrency, withDeadline, TokenBucket, classifyBotWall, retryOnceOnTimeout, FetchproxyProtocolError, FetchproxyHttpError, FetchproxyBridgeDownError, FetchproxyTimeoutError, classifyBridgeError, classifyRowError, classifyFetchError, backoffDelayMs, BRIDGE_CONCURRENCY, chunk, sleep, extractGlobalAssign, extractBalancedObject, extractImgTags, lastPathSegment, } from '@fetchproxy/server';
24
40
  export type { FetchproxyServerOpts, FetchResult, FetchResultError, HttpResponse, RequestOpts, BodylessRequestOpts, BridgeHealth, BridgeProbeResult, BridgeError, FetchErrorKind, BotWallResult, BotWallVendor, TokenBucketOptions, BackoffOptions, DeadlineOutcome, } from '@fetchproxy/server';
41
+ /**
42
+ * One request to round-trip through the bridge. The path is resolved relative
43
+ * to the transport's declared domain (a `defaultSubdomain` — e.g. `'www'` — is
44
+ * applied unless the caller overrides it per-call). This is the common
45
+ * `FetchInit` redfin / homes / compass / musescore each declared verbatim in
46
+ * their `src/transport.ts`.
47
+ */
48
+ export interface FetchproxyFetchInit {
49
+ /** Path-and-query relative to the declared domain, e.g. `/robots.txt`. */
50
+ path: string;
51
+ method: 'GET' | 'POST' | 'PUT' | 'DELETE';
52
+ headers?: Record<string, string>;
53
+ /** Serialized request body. JSON callers stringify before calling. */
54
+ body?: string;
55
+ /**
56
+ * Per-call subdomain override. Defaults to the transport's `defaultSubdomain`
57
+ * (and, absent that, the apex). Absolute `http(s)://` paths self-describe
58
+ * their host, so this is ignored for them.
59
+ */
60
+ subdomain?: string;
61
+ /** Per-call base-domain selector (required only for multi-domain MCPs). */
62
+ domain?: string;
63
+ }
64
+ /** The success-arm `{status, body, url}` triple every consumer returns. */
65
+ export type FetchproxyFetchResult = HttpResponse;
66
+ /** Options for {@link FetchproxyTransport.requestJson}. */
67
+ export interface FetchproxyRequestJsonInit {
68
+ headers?: Record<string, string>;
69
+ body?: unknown;
70
+ subdomain?: string;
71
+ domain?: string;
72
+ }
25
73
  /**
26
74
  * The lifecycle surface every per-MCP fetchproxy transport interface exposes.
27
75
  * `createFetchproxyTransport` returns this, typed as the caller's `T` so it can
@@ -48,6 +96,31 @@ export interface FetchproxyTransport {
48
96
  * verb.
49
97
  */
50
98
  readonly server: FetchproxyServer;
99
+ /**
100
+ * Verb passthrough: round-trip one request through `server.request(...)`,
101
+ * applying the transport's `defaultSubdomain`, and return the success-arm
102
+ * `{status, body, url}` triple. Bridge failures throw the typed errors
103
+ * (`FetchproxyBridgeDownError` / `FetchproxyTimeoutError` / …), exactly like
104
+ * `server.request`. This is the `fetch(init)` redfin / homes / compass /
105
+ * musescore each wrote by hand.
106
+ */
107
+ fetch(init: FetchproxyFetchInit): Promise<FetchproxyFetchResult>;
108
+ /**
109
+ * Verb passthrough over `server.requestJson(...)` (serialization + header
110
+ * defaults + 204→null + JSON.parse). Returns BOTH the parsed `data` and the
111
+ * raw success-arm `result`, so the caller keeps its per-site `throwIfNotOk` /
112
+ * sign-in guards over `result`. `defaultSubdomain` is applied.
113
+ */
114
+ requestJson<T = unknown>(method: 'GET' | 'POST' | 'PUT' | 'DELETE', path: string, init?: FetchproxyRequestJsonInit): Promise<{
115
+ data: T | null;
116
+ result: FetchproxyFetchResult;
117
+ }>;
118
+ /**
119
+ * Verb passthrough over `server.runProbe(...)` — run one healthcheck probe,
120
+ * measure elapsed ms, classify any thrown error, and project the post-probe
121
+ * `bridgeHealth()`. Powers {@link registerBridgeHealthcheckTool}.
122
+ */
123
+ runProbe(fetchFn: (path: string) => Promise<unknown>, probePath: string): Promise<BridgeProbeResult>;
51
124
  }
52
125
  /** Options for {@link createFetchproxyTransport}. */
53
126
  export type CreateFetchproxyTransportOptions = FetchproxyServerOpts & {
@@ -59,6 +132,14 @@ export type CreateFetchproxyTransportOptions = FetchproxyServerOpts & {
59
132
  debugEnvVar?: string;
60
133
  /** Env source for {@link debugEnvVar}. Defaults to `process.env`. */
61
134
  env?: NodeJS.ProcessEnv;
135
+ /**
136
+ * Subdomain the verb adapters (`fetch` / `requestJson`) apply per call unless
137
+ * the caller overrides it. This is the ONE per-site bit of the verb surface:
138
+ * redfin / homes / compass pin `'www'`; apex-served sites (musescore) omit it
139
+ * to hit the bare domain. Absolute `http(s)://` paths self-describe their host
140
+ * and ignore this entirely.
141
+ */
142
+ defaultSubdomain?: string;
62
143
  };
63
144
  /**
64
145
  * Wrap a `FetchproxyServer` in the `start`/`close`/`status` lifecycle the
@@ -155,4 +236,70 @@ export interface BridgeErrorInfo {
155
236
  * string kind (drop-in compatible with `@fetchproxy/server`).
156
237
  */
157
238
  export declare function bridgeErrorInfo(err: unknown): BridgeErrorInfo;
239
+ /** The diagnostic result a `<prefix>_healthcheck` tool returns. */
240
+ export interface BridgeHealthcheckResult {
241
+ ok: boolean;
242
+ bridge: BridgeProbeResult['bridge'] & {
243
+ last_extension_message_at: number | null;
244
+ };
245
+ probe: {
246
+ url: string;
247
+ elapsed_ms: number;
248
+ status?: number;
249
+ body_length?: number;
250
+ };
251
+ error?: {
252
+ kind: BridgeErrorInfo['type'];
253
+ message: string;
254
+ /** Server-authored next-step hint (`FetchproxyBridgeDownError.hint`), when present. */
255
+ bridge_hint?: string;
256
+ };
257
+ /** Plain-English next-step suggestion derived from the result. */
258
+ hint: string;
259
+ }
260
+ /** Options for {@link registerBridgeHealthcheckTool}. */
261
+ export interface RegisterBridgeHealthcheckToolArgs {
262
+ /** The `McpServer` to register the tool on. */
263
+ server: McpServer;
264
+ /** Tool-name + host-banner prefix, e.g. `'compass'` → `compass_healthcheck`. */
265
+ prefix: string;
266
+ /** Public probe path to round-trip, e.g. `'/robots.txt'`. */
267
+ probePath: string;
268
+ /**
269
+ * Display host for the probe URL + hint copy, e.g. `'compass.com'` or
270
+ * `'www.redfin.com'`. The probe URL is `https://<hostLabel><probePath>`.
271
+ */
272
+ hostLabel: string;
273
+ /**
274
+ * The bridge transport — supplies `runProbe` (the probe loop + classification
275
+ * + post-probe bridge projection) and `status()` (for the liveness counter the
276
+ * projection omits). Any {@link FetchproxyTransport}-shaped object works.
277
+ */
278
+ transport: Pick<FetchproxyTransport, 'runProbe' | 'status'>;
279
+ /**
280
+ * Performs the actual probe fetch for `probePath`. Required — most consumers
281
+ * pass `(path) => client.fetchHtml(path)` so the probe exercises the same
282
+ * client path real tools use (sign-in guards and all).
283
+ */
284
+ probeFn: (path: string) => Promise<string>;
285
+ }
286
+ /**
287
+ * Register a `<prefix>_healthcheck` MCP tool that round-trips `probePath`
288
+ * through the bridge and reports bridge status + role + timing, plus an
289
+ * actionable hint ladder on failure.
290
+ *
291
+ * The probe loop / error classification / post-probe bridge projection all live
292
+ * in `transport.runProbe` (the `@fetchproxy/server` primitive); this factory
293
+ * owns only the tool registration, the result shape, and the hint ladder. The
294
+ * per-site bits (`prefix`, `probePath`, `hostLabel`, the probe `fetchFn`) are
295
+ * options.
296
+ *
297
+ * @example
298
+ * registerBridgeHealthcheckTool({
299
+ * server, prefix: 'compass', probePath: '/robots.txt',
300
+ * hostLabel: 'compass.com', transport,
301
+ * probeFn: (p) => client.fetchHtml(p),
302
+ * });
303
+ */
304
+ export declare function registerBridgeHealthcheckTool(args: RegisterBridgeHealthcheckToolArgs): void;
158
305
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/fetchproxy/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/fetchproxy/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,OAAO,EACL,gBAAgB,EAGhB,KAAK,oBAAoB,EAGzB,KAAK,YAAY,EACjB,KAAK,iBAAiB,EACvB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAQzE,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,YAAY,EACZ,WAAW,EACX,eAAe,EACf,kBAAkB,EAClB,uBAAuB,EACvB,mBAAmB,EACnB,yBAAyB,EACzB,sBAAsB,EACtB,mBAAmB,EACnB,gBAAgB,EAChB,kBAAkB,EAClB,cAAc,EACd,kBAAkB,EAIlB,KAAK,EACL,KAAK,EACL,mBAAmB,EACnB,qBAAqB,EACrB,cAAc,EACd,eAAe,GAChB,MAAM,oBAAoB,CAAC;AAO5B,YAAY,EACV,oBAAoB,EACpB,WAAW,EACX,gBAAgB,EAChB,YAAY,EACZ,WAAW,EACX,mBAAmB,EACnB,YAAY,EACZ,iBAAiB,EACjB,WAAW,EACX,cAAc,EACd,aAAa,EACb,aAAa,EACb,kBAAkB,EAClB,cAAc,EACd,eAAe,GAChB,MAAM,oBAAoB,CAAC;AA2B5B;;;;;;GAMG;AACH,MAAM,WAAW,mBAAmB;IAClC,0EAA0E;IAC1E,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,CAAC;IAC1C,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,sEAAsE;IACtE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2EAA2E;IAC3E,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,2EAA2E;AAC3E,MAAM,MAAM,qBAAqB,GAAG,YAAY,CAAC;AAEjD,2DAA2D;AAC3D,MAAM,WAAW,yBAAyB;IACxC,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IAClC;;;;OAIG;IACH,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,0EAA0E;IAC1E,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,sEAAsE;IACtE,MAAM,IAAI,UAAU,CAAC,gBAAgB,CAAC,cAAc,CAAC,CAAC,CAAC;IACvD,wEAAwE;IACxE,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC,MAAM,CAAC,CAAC;IACxC;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAClC;;;;;;;OAOG;IACH,KAAK,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAAC;IACjE;;;;;OAKG;IACH,WAAW,CAAC,CAAC,GAAG,OAAO,EACrB,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,EACzC,IAAI,EAAE,MAAM,EACZ,IAAI,CAAC,EAAE,yBAAyB,GAC/B,OAAO,CAAC;QAAE,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC;QAAC,MAAM,EAAE,qBAAqB,CAAA;KAAE,CAAC,CAAC;IAC9D;;;;OAIG;IACH,QAAQ,CACN,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,EAC3C,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,iBAAiB,CAAC,CAAC;CAC/B;AAED,qDAAqD;AACrD,MAAM,MAAM,gCAAgC,GAAG,oBAAoB,GAAG;IACpE;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,qEAAqE;IACrE,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,yBAAyB,CAAC,CAAC,GAAG,mBAAmB,EAC/D,IAAI,EAAE,gCAAgC,GACrC,CAAC,CAuEH;AAMD,iFAAiF;AACjF,YAAY,EACV,UAAU,EACV,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,KAAK,EACV,iBAAiB,EACjB,kBAAkB,EAClB,kBAAkB,EACnB,MAAM,sBAAsB,CAAC;AAE9B;;;;;GAKG;AACH,MAAM,WAAW,cAAc;IAC7B,kFAAkF;IAClF,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,wDAAwD;IACxD,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC5B,4DAA4D;IAC5D,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC9B,wFAAwF;IACxF,oBAAoB,CAAC,EAAE,kBAAkB,EAAE,CAAC;IAC5C,4FAA4F;IAC5F,sBAAsB,CAAC,EAAE,kBAAkB,EAAE,CAAC;IAC9C,6EAA6E;IAC7E,cAAc,CAAC,EAAE,iBAAiB,EAAE,CAAC;IACrC,oDAAoD;IACpD,eAAe,CAAC,EAAE,kBAAkB,EAAE,CAAC;CACxC;AAED,+CAA+C;AAC/C,MAAM,WAAW,uBAAuB;IACtC;;;;OAIG;IACH,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC3B;;;;;OAKG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,2EAA2E;IAC3E,SAAS,CAAC,EAAE,cAAc,CAAC;CAC5B;AAMD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,uBAAuB,GAC5B,IAAI,CACL,oBAAoB,EAClB,SAAS,GACT,cAAc,GACd,YAAY,GACZ,kBAAkB,GAClB,oBAAoB,GACpB,sBAAsB,GACtB,wBAAwB,GACxB,gBAAgB,GAChB,iBAAiB,CACpB,CAwCA;AAOD,6DAA6D;AAC7D,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,aAAa,GAAG,SAAS,GAAG,MAAM,GAAG,UAAU,GAAG,SAAS,CAAC;IAClE,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,OAAO,GAAG,eAAe,CA+B7D;AAeD,mEAAmE;AACnE,MAAM,WAAW,uBAAuB;IACtC,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,iBAAiB,CAAC,QAAQ,CAAC,GAAG;QACpC,yBAAyB,EAAE,MAAM,GAAG,IAAI,CAAC;KAC1C,CAAC;IACF,KAAK,EAAE;QACL,GAAG,EAAE,MAAM,CAAC;QACZ,UAAU,EAAE,MAAM,CAAC;QACnB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,WAAW,CAAC,EAAE,MAAM,CAAC;KACtB,CAAC;IACF,KAAK,CAAC,EAAE;QACN,IAAI,EAAE,eAAe,CAAC,MAAM,CAAC,CAAC;QAC9B,OAAO,EAAE,MAAM,CAAC;QAChB,uFAAuF;QACvF,WAAW,CAAC,EAAE,MAAM,CAAC;KACtB,CAAC;IACF,kEAAkE;IAClE,IAAI,EAAE,MAAM,CAAC;CACd;AAED,yDAAyD;AACzD,MAAM,WAAW,iCAAiC;IAChD,+CAA+C;IAC/C,MAAM,EAAE,SAAS,CAAC;IAClB,gFAAgF;IAChF,MAAM,EAAE,MAAM,CAAC;IACf,6DAA6D;IAC7D,SAAS,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,SAAS,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,SAAS,EAAE,IAAI,CAAC,mBAAmB,EAAE,UAAU,GAAG,QAAQ,CAAC,CAAC;IAC5D;;;;OAIG;IACH,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;CAC5C;AA0CD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,6BAA6B,CAAC,IAAI,EAAE,iCAAiC,GAAG,IAAI,CA2F3F"}