@mcpmake/core 0.2.4 → 0.3.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/dist/analyzer/dom-parser.d.ts +35 -0
- package/dist/analyzer/dom-parser.d.ts.map +1 -1
- package/dist/analyzer/dom-parser.js +107 -6
- package/dist/analyzer/dom-parser.js.map +1 -1
- package/dist/analyzer/goal-crawler.d.ts +2 -1
- package/dist/analyzer/goal-crawler.d.ts.map +1 -1
- package/dist/analyzer/goal-crawler.js +66 -17
- package/dist/analyzer/goal-crawler.js.map +1 -1
- package/dist/analyzer/semantic-analyzer.d.ts +1 -1
- package/dist/analyzer/semantic-analyzer.d.ts.map +1 -1
- package/dist/analyzer/semantic-analyzer.js +15 -16
- package/dist/analyzer/semantic-analyzer.js.map +1 -1
- package/dist/analyzer/site-crawler.d.ts.map +1 -1
- package/dist/analyzer/site-crawler.js +122 -16
- package/dist/analyzer/site-crawler.js.map +1 -1
- package/dist/config/mcpmake-config.d.ts.map +1 -1
- package/dist/config/mcpmake-config.js +5 -0
- package/dist/config/mcpmake-config.js.map +1 -1
- package/dist/emitter/code-writer.d.ts +1 -0
- package/dist/emitter/code-writer.d.ts.map +1 -1
- package/dist/emitter/code-writer.js +79 -12
- package/dist/emitter/code-writer.js.map +1 -1
- package/dist/emitter/index.d.ts +8 -0
- package/dist/emitter/index.d.ts.map +1 -1
- package/dist/emitter/index.js +113 -6
- package/dist/emitter/index.js.map +1 -1
- package/dist/emitter/project-scaffolder.d.ts +13 -0
- package/dist/emitter/project-scaffolder.d.ts.map +1 -1
- package/dist/emitter/project-scaffolder.js +29 -0
- package/dist/emitter/project-scaffolder.js.map +1 -1
- package/dist/emitter/python-template-loader.d.ts.map +1 -1
- package/dist/emitter/python-template-loader.js +10 -0
- package/dist/emitter/python-template-loader.js.map +1 -1
- package/dist/emitter/python-templates/dockerfile.hbs +4 -1
- package/dist/emitter/python-templates/server.py.hbs +82 -8
- package/dist/emitter/site-scaffolder.d.ts.map +1 -1
- package/dist/emitter/site-scaffolder.js +28 -1
- package/dist/emitter/site-scaffolder.js.map +1 -1
- package/dist/emitter/site-templates/browser-manager.ts.hbs +104 -34
- package/dist/emitter/site-templates/dockerfile.hbs +13 -4
- package/dist/emitter/site-templates/env.example.hbs +8 -0
- package/dist/emitter/site-templates/server-main-http.ts.hbs +77 -6
- package/dist/emitter/site-templates/telemetry.ts.hbs +117 -0
- package/dist/emitter/site-templates/tool-handler-form.ts.hbs +10 -8
- package/dist/emitter/template-loader.d.ts.map +1 -1
- package/dist/emitter/template-loader.js +9 -0
- package/dist/emitter/template-loader.js.map +1 -1
- package/dist/emitter/templates/auth-provider.ts.hbs +86 -21
- package/dist/emitter/templates/config.ts.hbs +29 -0
- package/dist/emitter/templates/dockerfile.hbs +12 -4
- package/dist/emitter/templates/env.example.hbs +6 -0
- package/dist/emitter/templates/http-executor.ts.hbs +150 -7
- package/dist/emitter/templates/oauth.ts.hbs +19 -65
- package/dist/emitter/templates/server-main-http.ts.hbs +93 -12
- package/dist/emitter/templates/task-handlers.ts.hbs +40 -1
- package/dist/emitter/templates/tool-handler.ts.hbs +37 -2
- package/dist/emitter/templates/tool-test.ts.hbs +9 -1
- package/dist/emitter/worker-templates/config.ts.hbs +7 -0
- package/dist/emitter/worker-templates/tool-handler.ts.hbs +37 -2
- package/dist/emitter/worker-templates/worker.ts.hbs +119 -25
- package/dist/generator/spec-generator.d.ts.map +1 -1
- package/dist/generator/spec-generator.js +9 -54
- package/dist/generator/spec-generator.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/llm/anthropic-provider.d.ts +14 -0
- package/dist/llm/anthropic-provider.d.ts.map +1 -0
- package/dist/llm/anthropic-provider.js +50 -0
- package/dist/llm/anthropic-provider.js.map +1 -0
- package/dist/llm/index.d.ts +19 -0
- package/dist/llm/index.d.ts.map +1 -0
- package/dist/llm/index.js +73 -0
- package/dist/llm/index.js.map +1 -0
- package/dist/llm/openai-provider.d.ts +46 -0
- package/dist/llm/openai-provider.d.ts.map +1 -0
- package/dist/llm/openai-provider.js +221 -0
- package/dist/llm/openai-provider.js.map +1 -0
- package/dist/llm/types.d.ts +61 -0
- package/dist/llm/types.d.ts.map +1 -0
- package/dist/llm/types.js +16 -0
- package/dist/llm/types.js.map +1 -0
- package/dist/parser/har-filter.d.ts +7 -0
- package/dist/parser/har-filter.d.ts.map +1 -1
- package/dist/parser/har-filter.js +107 -1
- package/dist/parser/har-filter.js.map +1 -1
- package/dist/parser/index.d.ts +1 -1
- package/dist/parser/index.d.ts.map +1 -1
- package/dist/parser/index.js +1 -1
- package/dist/parser/index.js.map +1 -1
- package/dist/parser/openapi-loader.d.ts.map +1 -1
- package/dist/parser/openapi-loader.js +36 -2
- package/dist/parser/openapi-loader.js.map +1 -1
- package/dist/parser/operation-extractor.d.ts.map +1 -1
- package/dist/parser/operation-extractor.js +12 -1
- package/dist/parser/operation-extractor.js.map +1 -1
- package/dist/parser/overlay-loader.d.ts.map +1 -1
- package/dist/parser/overlay-loader.js +11 -2
- package/dist/parser/overlay-loader.js.map +1 -1
- package/dist/parser/postman-loader.d.ts.map +1 -1
- package/dist/parser/postman-loader.js +6 -1
- package/dist/parser/postman-loader.js.map +1 -1
- package/dist/parser/schema-converter.d.ts +13 -2
- package/dist/parser/schema-converter.d.ts.map +1 -1
- package/dist/parser/schema-converter.js +64 -42
- package/dist/parser/schema-converter.js.map +1 -1
- package/dist/plugins/loader.d.ts +5 -1
- package/dist/plugins/loader.d.ts.map +1 -1
- package/dist/plugins/loader.js +39 -10
- package/dist/plugins/loader.js.map +1 -1
- package/dist/pricing.d.ts +5 -1
- package/dist/pricing.d.ts.map +1 -1
- package/dist/pricing.js +7 -3
- package/dist/pricing.js.map +1 -1
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +6 -21
- package/dist/providers/index.js.map +1 -1
- package/dist/recorder/browser-recorder.d.ts.map +1 -1
- package/dist/recorder/browser-recorder.js +89 -12
- package/dist/recorder/browser-recorder.js.map +1 -1
- package/dist/rescan/rescan-scheduler.d.ts.map +1 -1
- package/dist/rescan/rescan-scheduler.js +22 -7
- package/dist/rescan/rescan-scheduler.js.map +1 -1
- package/dist/site-transformer/selector-healer.d.ts +1 -1
- package/dist/site-transformer/selector-healer.d.ts.map +1 -1
- package/dist/site-transformer/selector-healer.js +17 -18
- package/dist/site-transformer/selector-healer.js.map +1 -1
- package/dist/site-transformer/tool-generator.d.ts.map +1 -1
- package/dist/site-transformer/tool-generator.js +90 -22
- package/dist/site-transformer/tool-generator.js.map +1 -1
- package/dist/transformer/auth-detector.d.ts.map +1 -1
- package/dist/transformer/auth-detector.js +24 -5
- package/dist/transformer/auth-detector.js.map +1 -1
- package/dist/transformer/har-schema-inferrer.d.ts.map +1 -1
- package/dist/transformer/har-schema-inferrer.js +2 -0
- package/dist/transformer/har-schema-inferrer.js.map +1 -1
- package/dist/transformer/har-to-operations.d.ts +1 -0
- package/dist/transformer/har-to-operations.d.ts.map +1 -1
- package/dist/transformer/har-to-operations.js +42 -10
- package/dist/transformer/har-to-operations.js.map +1 -1
- package/dist/transformer/llm-namer.d.ts.map +1 -1
- package/dist/transformer/llm-namer.js +10 -13
- package/dist/transformer/llm-namer.js.map +1 -1
- package/dist/transformer/naming.d.ts.map +1 -1
- package/dist/transformer/naming.js +16 -8
- package/dist/transformer/naming.js.map +1 -1
- package/dist/transformer/resource-builder.d.ts.map +1 -1
- package/dist/transformer/resource-builder.js +13 -6
- package/dist/transformer/resource-builder.js.map +1 -1
- package/dist/transformer/stainless-translator.d.ts +6 -0
- package/dist/transformer/stainless-translator.d.ts.map +1 -1
- package/dist/transformer/stainless-translator.js +18 -1
- package/dist/transformer/stainless-translator.js.map +1 -1
- package/dist/transformer/tool-builder.d.ts.map +1 -1
- package/dist/transformer/tool-builder.js +141 -14
- package/dist/transformer/tool-builder.js.map +1 -1
- package/dist/types/index.d.ts +79 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/site.d.ts +19 -1
- package/dist/types/site.d.ts.map +1 -1
- package/dist/utils/fail.d.ts.map +1 -1
- package/dist/utils/fail.js +22 -4
- package/dist/utils/fail.js.map +1 -1
- package/dist/utils/json-extract.d.ts +21 -0
- package/dist/utils/json-extract.d.ts.map +1 -0
- package/dist/utils/json-extract.js +67 -0
- package/dist/utils/json-extract.js.map +1 -0
- package/dist/utils/model-resolver.d.ts.map +1 -1
- package/dist/utils/model-resolver.js +6 -0
- package/dist/utils/model-resolver.js.map +1 -1
- package/dist/utils/sanitize.d.ts +63 -1
- package/dist/utils/sanitize.d.ts.map +1 -1
- package/dist/utils/sanitize.js +147 -2
- package/dist/utils/sanitize.js.map +1 -1
- package/dist/utils/ssrf-guard.d.ts +10 -0
- package/dist/utils/ssrf-guard.d.ts.map +1 -0
- package/dist/utils/ssrf-guard.js +130 -0
- package/dist/utils/ssrf-guard.js.map +1 -0
- package/dist/utils/watcher.d.ts +17 -1
- package/dist/utils/watcher.d.ts.map +1 -1
- package/dist/utils/watcher.js +53 -21
- package/dist/utils/watcher.js.map +1 -1
- package/package.json +2 -1
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { chromium } from 'playwright';
|
|
7
7
|
import type { Browser, BrowserContext, Page } from 'playwright';
|
|
8
8
|
import type { BrowserConfig } from './config.js';
|
|
9
|
+
import * as telemetry from './telemetry.js';
|
|
9
10
|
|
|
10
11
|
interface BrowserSession {
|
|
11
12
|
context: BrowserContext;
|
|
@@ -18,6 +19,16 @@ const sessions = new Map<string, BrowserSession>();
|
|
|
18
19
|
let idleTimer: ReturnType<typeof setInterval> | undefined;
|
|
19
20
|
let config: BrowserConfig;
|
|
20
21
|
|
|
22
|
+
// In-flight session creations, keyed by session id. Single-flighting per id
|
|
23
|
+
// means concurrent no-ID calls (which all resolve to DEFAULT_SESSION) and
|
|
24
|
+
// repeated calls with the same explicit id share one Chromium context instead
|
|
25
|
+
// of racing to create several.
|
|
26
|
+
const sessionCreations = new Map<string, Promise<{ page: Page; sessionId: string }>>();
|
|
27
|
+
|
|
28
|
+
// Single-flight the browser launch so concurrent cold starts can't spawn
|
|
29
|
+
// multiple Chromium processes.
|
|
30
|
+
let browserLaunch: Promise<Browser> | undefined;
|
|
31
|
+
|
|
21
32
|
const DEFAULT_SESSION = '__default__';
|
|
22
33
|
|
|
23
34
|
/**
|
|
@@ -78,10 +89,37 @@ export async function resolveSelector(
|
|
|
78
89
|
}
|
|
79
90
|
}
|
|
80
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Launch the shared browser, single-flighting concurrent cold starts so two
|
|
94
|
+
* simultaneous requests can never spawn two Chromium processes.
|
|
95
|
+
*/
|
|
96
|
+
async function getBrowser(): Promise<Browser> {
|
|
97
|
+
if (browser && browser.isConnected()) return browser;
|
|
98
|
+
if (browserLaunch) return browserLaunch;
|
|
99
|
+
|
|
100
|
+
// Only disable sandbox when running as root in a container.
|
|
101
|
+
const isRoot = process.getuid?.() === 0;
|
|
102
|
+
browserLaunch = chromium
|
|
103
|
+
.launch({
|
|
104
|
+
headless: config.headless,
|
|
105
|
+
args: isRoot ? ['--no-sandbox', '--disable-setuid-sandbox'] : [],
|
|
106
|
+
})
|
|
107
|
+
.then((b) => {
|
|
108
|
+
browser = b;
|
|
109
|
+
return b;
|
|
110
|
+
})
|
|
111
|
+
.finally(() => {
|
|
112
|
+
browserLaunch = undefined;
|
|
113
|
+
});
|
|
114
|
+
return browserLaunch;
|
|
115
|
+
}
|
|
116
|
+
|
|
81
117
|
/**
|
|
82
118
|
* Get or create a browser session.
|
|
83
119
|
* If sessionId is provided, reuses an existing session or creates one with that ID.
|
|
84
|
-
* If not, creates a
|
|
120
|
+
* If not, reuses (or creates) a single shared default session under a fixed ID,
|
|
121
|
+
* so repeat no-ID calls and `take_screenshot` without an ID all hit the same
|
|
122
|
+
* context instead of leaking a new Chromium context per call.
|
|
85
123
|
* Throws if the session pool is at capacity (maxSessions).
|
|
86
124
|
*/
|
|
87
125
|
export async function getOrCreateSession(sessionId?: string): Promise<{ page: Page; sessionId: string }> {
|
|
@@ -95,7 +133,23 @@ export async function getOrCreateSession(sessionId?: string): Promise<{ page: Pa
|
|
|
95
133
|
return { page: existing.page, sessionId: id };
|
|
96
134
|
}
|
|
97
135
|
|
|
98
|
-
//
|
|
136
|
+
// Coalesce concurrent creations for the same id (notably the default
|
|
137
|
+
// session) onto one promise so we don't create duplicate contexts or
|
|
138
|
+
// double-count capacity.
|
|
139
|
+
const inFlight = sessionCreations.get(id);
|
|
140
|
+
if (inFlight) return inFlight;
|
|
141
|
+
|
|
142
|
+
const creation = createSession(id).finally(() => {
|
|
143
|
+
sessionCreations.delete(id);
|
|
144
|
+
});
|
|
145
|
+
sessionCreations.set(id, creation);
|
|
146
|
+
return creation;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function createSession(id: string): Promise<{ page: Page; sessionId: string }> {
|
|
150
|
+
// Reserve the capacity slot BEFORE any await so concurrent creations of
|
|
151
|
+
// distinct ids can't all pass the gate and then blow past maxSessions. We
|
|
152
|
+
// insert a placeholder entry now and fill in the real handles below.
|
|
99
153
|
const maxSessions = config.maxSessions ?? 10;
|
|
100
154
|
if (sessions.size >= maxSessions) {
|
|
101
155
|
throw new Error(
|
|
@@ -103,38 +157,37 @@ export async function getOrCreateSession(sessionId?: string): Promise<{ page: Pa
|
|
|
103
157
|
'Close an existing session before creating a new one.'
|
|
104
158
|
);
|
|
105
159
|
}
|
|
160
|
+
const reserved: BrowserSession = {
|
|
161
|
+
context: undefined as unknown as BrowserContext,
|
|
162
|
+
page: undefined as unknown as Page,
|
|
163
|
+
lastAccessedAt: Date.now(),
|
|
164
|
+
};
|
|
165
|
+
sessions.set(id, reserved);
|
|
106
166
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
headless: config.headless,
|
|
113
|
-
args: isRoot ? ['--no-sandbox', '--disable-setuid-sandbox'] : [],
|
|
167
|
+
try {
|
|
168
|
+
const activeBrowser = await getBrowser();
|
|
169
|
+
const context = await activeBrowser.newContext({
|
|
170
|
+
viewport: config.viewport,
|
|
171
|
+
...(config.userAgent ? { userAgent: config.userAgent } : {}),
|
|
114
172
|
});
|
|
115
|
-
|
|
173
|
+
const page = await context.newPage();
|
|
116
174
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
...(config.userAgent ? { userAgent: config.userAgent } : {}),
|
|
121
|
-
});
|
|
122
|
-
const page = await context.newPage();
|
|
123
|
-
|
|
124
|
-
// Generate final session ID upfront to avoid race conditions
|
|
125
|
-
const finalId = id === DEFAULT_SESSION
|
|
126
|
-
? `session_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`
|
|
127
|
-
: id;
|
|
175
|
+
reserved.context = context;
|
|
176
|
+
reserved.page = page;
|
|
177
|
+
reserved.lastAccessedAt = Date.now();
|
|
128
178
|
|
|
129
|
-
|
|
130
|
-
context,
|
|
131
|
-
page,
|
|
132
|
-
lastAccessedAt: Date.now(),
|
|
133
|
-
});
|
|
179
|
+
resetIdleTimer();
|
|
134
180
|
|
|
135
|
-
|
|
181
|
+
// The browser context is now resident — start billing this session (no-op
|
|
182
|
+
// unless the host injected telemetry env).
|
|
183
|
+
telemetry.sessionStarted(id);
|
|
136
184
|
|
|
137
|
-
|
|
185
|
+
return { page, sessionId: id };
|
|
186
|
+
} catch (err) {
|
|
187
|
+
// Release the reserved slot if context creation failed.
|
|
188
|
+
sessions.delete(id);
|
|
189
|
+
throw err;
|
|
190
|
+
}
|
|
138
191
|
}
|
|
139
192
|
|
|
140
193
|
/**
|
|
@@ -145,8 +198,9 @@ export async function closeSession(sessionId?: string): Promise<void> {
|
|
|
145
198
|
const session = sessions.get(id);
|
|
146
199
|
if (!session) return;
|
|
147
200
|
|
|
148
|
-
await session.context
|
|
201
|
+
await session.context?.close().catch(() => {});
|
|
149
202
|
sessions.delete(id);
|
|
203
|
+
telemetry.sessionEnded(id, 'explicit');
|
|
150
204
|
|
|
151
205
|
// If no sessions remain, close the browser
|
|
152
206
|
if (sessions.size === 0) {
|
|
@@ -161,8 +215,9 @@ export async function closeBrowser(): Promise<void> {
|
|
|
161
215
|
clearIdleTimer();
|
|
162
216
|
|
|
163
217
|
for (const [id, session] of sessions) {
|
|
164
|
-
await session.context
|
|
218
|
+
await session.context?.close().catch(() => {});
|
|
165
219
|
sessions.delete(id);
|
|
220
|
+
telemetry.sessionEnded(id, 'shutdown');
|
|
166
221
|
}
|
|
167
222
|
|
|
168
223
|
if (browser) {
|
|
@@ -207,11 +262,13 @@ function resetIdleTimer(): void {
|
|
|
207
262
|
clearIdleTimer();
|
|
208
263
|
idleTimer = setInterval(() => {
|
|
209
264
|
const now = Date.now();
|
|
210
|
-
// Close idle sessions
|
|
265
|
+
// Close idle sessions. Skip slots still being created (no context yet).
|
|
211
266
|
for (const [id, session] of sessions) {
|
|
267
|
+
if (!session.context) continue;
|
|
212
268
|
if (now - session.lastAccessedAt > config.idleTimeoutMs) {
|
|
213
269
|
session.context.close().catch(() => {});
|
|
214
270
|
sessions.delete(id);
|
|
271
|
+
telemetry.sessionEnded(id, 'idle');
|
|
215
272
|
}
|
|
216
273
|
}
|
|
217
274
|
// Close browser if no sessions
|
|
@@ -228,6 +285,19 @@ function clearIdleTimer(): void {
|
|
|
228
285
|
}
|
|
229
286
|
}
|
|
230
287
|
|
|
231
|
-
// Clean up on process exit
|
|
232
|
-
|
|
233
|
-
|
|
288
|
+
// Clean up on process exit. Close the browser (which emits a session_end for
|
|
289
|
+
// every still-resident session), then flush telemetry best-effort so the host
|
|
290
|
+
// sees the final boundary instead of waiting out the heartbeat grace.
|
|
291
|
+
async function shutdown(): Promise<void> {
|
|
292
|
+
try {
|
|
293
|
+
await closeBrowser();
|
|
294
|
+
} finally {
|
|
295
|
+
await telemetry.flushOnShutdown();
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
process.on('SIGTERM', () => {
|
|
299
|
+
void shutdown();
|
|
300
|
+
});
|
|
301
|
+
process.on('SIGINT', () => {
|
|
302
|
+
void shutdown();
|
|
303
|
+
});
|
|
@@ -5,8 +5,14 @@
|
|
|
5
5
|
FROM mcr.microsoft.com/playwright:v1.59.1-noble AS builder
|
|
6
6
|
|
|
7
7
|
WORKDIR /app
|
|
8
|
-
COPY package.json
|
|
9
|
-
|
|
8
|
+
COPY package.json ./
|
|
9
|
+
# The generator does not emit a package-lock.json, so use `npm install` (not
|
|
10
|
+
# `npm ci`, which fails without a lockfile). --ignore-scripts: a malicious
|
|
11
|
+
# transitive dependency's install lifecycle hook (preinstall/install/postinstall)
|
|
12
|
+
# would otherwise run as root at build time — install-time RCE in the build
|
|
13
|
+
# container. The server's own compile is an explicit `npm run build` step below,
|
|
14
|
+
# so it does not depend on install hooks.
|
|
15
|
+
RUN npm install --ignore-scripts
|
|
10
16
|
COPY tsconfig.json ./
|
|
11
17
|
COPY src/ src/
|
|
12
18
|
RUN npm run build
|
|
@@ -20,8 +26,11 @@ ENV PORT=3000
|
|
|
20
26
|
ENV HEADLESS=true
|
|
21
27
|
|
|
22
28
|
WORKDIR /app
|
|
23
|
-
COPY package.json
|
|
24
|
-
|
|
29
|
+
COPY package.json ./
|
|
30
|
+
# Runtime deps only (no lockfile emitted → `npm install`); --ignore-scripts
|
|
31
|
+
# blocks dependency install hooks from running as root. The Playwright browser
|
|
32
|
+
# binaries ship in the base image, so no install-time download lifecycle is needed.
|
|
33
|
+
RUN npm install --omit=dev --ignore-scripts
|
|
25
34
|
|
|
26
35
|
COPY --from=builder /app/dist dist/
|
|
27
36
|
|
|
@@ -9,6 +9,14 @@ IDLE_TIMEOUT_MS={{browserConfig.idleTimeoutMs}}
|
|
|
9
9
|
VIEWPORT_WIDTH={{browserConfig.viewport.width}}
|
|
10
10
|
VIEWPORT_HEIGHT={{browserConfig.viewport.height}}
|
|
11
11
|
# USER_AGENT=
|
|
12
|
+
|
|
13
|
+
# Browser-session telemetry (managed hosting only — leave UNSET to self-host).
|
|
14
|
+
# When the host sets these, the server reports exact browser-session lifetime
|
|
15
|
+
# for metering. It NEVER phones home unless MCPMAKE_TELEMETRY_URL is set.
|
|
16
|
+
# MCPMAKE_TELEMETRY_URL=
|
|
17
|
+
# MCPMAKE_TELEMETRY_TOKEN=
|
|
18
|
+
# MCPMAKE_SERVER_SLUG=
|
|
19
|
+
# MCPMAKE_TELEMETRY_HEARTBEAT_MS=30000
|
|
12
20
|
{{#if (eq transport "http")}}
|
|
13
21
|
|
|
14
22
|
# Transport: "stdio" or "http"
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import http from 'node:http';
|
|
2
|
+
import crypto from 'node:crypto';
|
|
2
3
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
4
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
5
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
@@ -14,12 +15,60 @@ function log(level: string, message: string, data?: Record<string, unknown>): vo
|
|
|
14
15
|
const config = loadConfig();
|
|
15
16
|
initBrowserManager(config.browser);
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
/* Build a fully-registered McpServer. A single McpServer connected to multiple
|
|
19
|
+
* transports concurrently races on its internal transport reference (the API
|
|
20
|
+
* HTTP template documents the same hazard), so HTTP requests get a fresh,
|
|
21
|
+
* stateless server + transport per request instead of sharing one global
|
|
22
|
+
* instance. */
|
|
23
|
+
function createMcpServer(): McpServer {
|
|
24
|
+
const server = new McpServer({
|
|
25
|
+
name: '{{serverName}}',
|
|
26
|
+
version: '{{serverVersion}}',
|
|
27
|
+
});
|
|
28
|
+
registerAllTools(server, config);
|
|
29
|
+
return server;
|
|
30
|
+
}
|
|
21
31
|
|
|
22
|
-
|
|
32
|
+
/* Bearer authentication — mirrors the API/node server. Constant-time compare of
|
|
33
|
+
* the `Authorization: Bearer <token>` header against MCP_AUTH_TOKEN.
|
|
34
|
+
*
|
|
35
|
+
* Fail CLOSED: when MCP_AUTH_TOKEN is unset, authenticated routes are denied
|
|
36
|
+
* unless the operator opts in with MCP_ALLOW_UNAUTHENTICATED === 'true' (a
|
|
37
|
+
* local/dev escape hatch). We warn once so the open posture is loud. The token
|
|
38
|
+
* is never read from the query string — URLs leak through logs and referrers. */
|
|
39
|
+
let unauthWarned = false;
|
|
40
|
+
function isAuthorized(req: http.IncomingMessage): boolean {
|
|
41
|
+
const expected = process.env.MCP_AUTH_TOKEN;
|
|
42
|
+
if (!expected) {
|
|
43
|
+
if (process.env.MCP_ALLOW_UNAUTHENTICATED === 'true') {
|
|
44
|
+
if (!unauthWarned) {
|
|
45
|
+
unauthWarned = true;
|
|
46
|
+
console.error(
|
|
47
|
+
'SECURITY: MCP_AUTH_TOKEN is unset and MCP_ALLOW_UNAUTHENTICATED=true — ' +
|
|
48
|
+
'serving authenticated routes WITHOUT a bearer token. ' +
|
|
49
|
+
'Set MCP_AUTH_TOKEN before exposing this endpoint.',
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
if (!unauthWarned) {
|
|
55
|
+
unauthWarned = true;
|
|
56
|
+
console.error(
|
|
57
|
+
'SECURITY: MCP_AUTH_TOKEN is unset — denying all authenticated routes. ' +
|
|
58
|
+
'Set MCP_AUTH_TOKEN, or set MCP_ALLOW_UNAUTHENTICATED=true for local/dev only.',
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
const header = req.headers.authorization;
|
|
64
|
+
if (typeof header !== 'string' || !header.startsWith('Bearer ')) return false;
|
|
65
|
+
const presented = header.slice(7).trim();
|
|
66
|
+
if (!presented) return false;
|
|
67
|
+
const a = Buffer.from(presented);
|
|
68
|
+
const b = Buffer.from(expected);
|
|
69
|
+
if (a.length !== b.length) return false;
|
|
70
|
+
return crypto.timingSafeEqual(a, b);
|
|
71
|
+
}
|
|
23
72
|
|
|
24
73
|
let isReady = false;
|
|
25
74
|
|
|
@@ -43,6 +92,17 @@ if (process.env.TRANSPORT === 'http') {
|
|
|
43
92
|
}
|
|
44
93
|
|
|
45
94
|
if (url.pathname === '/mcp') {
|
|
95
|
+
// Bearer auth (in addition to the origin check below). /health and /ready
|
|
96
|
+
// above stay open for liveness/readiness probes.
|
|
97
|
+
if (!isAuthorized(req)) {
|
|
98
|
+
res.writeHead(401, {
|
|
99
|
+
'Content-Type': 'application/json',
|
|
100
|
+
'WWW-Authenticate': 'Bearer',
|
|
101
|
+
});
|
|
102
|
+
res.end(JSON.stringify({ error: 'Unauthorized: missing or invalid bearer token' }));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
46
106
|
if (allowedOrigin) {
|
|
47
107
|
// Compare host-to-host exactly (DNS-rebinding protection). A substring
|
|
48
108
|
// match would let "evil-{ALLOWED_ORIGIN}.attacker.com" through.
|
|
@@ -72,8 +132,17 @@ if (process.env.TRANSPORT === 'http') {
|
|
|
72
132
|
}
|
|
73
133
|
}
|
|
74
134
|
|
|
135
|
+
// Fresh stateless server + transport per request. Both are closed when
|
|
136
|
+
// the response finishes or the connection aborts, so transports and
|
|
137
|
+
// their listeners are never leaked and concurrent requests can't
|
|
138
|
+
// misroute through a shared transport reference.
|
|
139
|
+
const reqServer = createMcpServer();
|
|
75
140
|
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
76
|
-
|
|
141
|
+
res.on('close', () => {
|
|
142
|
+
void transport.close();
|
|
143
|
+
void reqServer.close();
|
|
144
|
+
});
|
|
145
|
+
await reqServer.connect(transport);
|
|
77
146
|
await transport.handleRequest(req, res);
|
|
78
147
|
return;
|
|
79
148
|
}
|
|
@@ -97,6 +166,8 @@ if (process.env.TRANSPORT === 'http') {
|
|
|
97
166
|
process.on('SIGTERM', shutdown);
|
|
98
167
|
process.on('SIGINT', shutdown);
|
|
99
168
|
} else {
|
|
169
|
+
// Stdio is inherently single-connection, so one long-lived server is correct.
|
|
170
|
+
const server = createMcpServer();
|
|
100
171
|
const transport = new StdioServerTransport();
|
|
101
172
|
await server.connect(transport);
|
|
102
173
|
isReady = true;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-session telemetry for exact metering on managed hosting.
|
|
3
|
+
*
|
|
4
|
+
* NO-OP unless MCPMAKE_TELEMETRY_URL is set — a self-hosted or local server
|
|
5
|
+
* never phones home. When the cloud injects the ingest URL + token + slug, this
|
|
6
|
+
* batches session_start / heartbeat / session_end events and POSTs them so the
|
|
7
|
+
* host can bill the REAL resident browser lifetime instead of approximating it
|
|
8
|
+
* from request latency.
|
|
9
|
+
*
|
|
10
|
+
* Strictly best-effort and isolated: every network/serialization error is
|
|
11
|
+
* swallowed. Telemetry must NEVER affect the MCP server's behavior. Dropped
|
|
12
|
+
* events can only ever UNDER-bill (the host applies a heartbeat grace + an
|
|
13
|
+
* independent proxy-activity clamp), never break a tool call.
|
|
14
|
+
*
|
|
15
|
+
* An OPEN session emits a heartbeat on every flush: an open session means a
|
|
16
|
+
* resident Chromium context, which is the billable cost — independent of how
|
|
17
|
+
* often tools are called. The host treats "last heartbeat + grace" as the close
|
|
18
|
+
* time if a session_end is ever lost (e.g. the server is killed).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
type TelemetryEventType = 'session_start' | 'heartbeat' | 'session_end';
|
|
22
|
+
|
|
23
|
+
interface TelemetryEvent {
|
|
24
|
+
type: TelemetryEventType;
|
|
25
|
+
sessionId: string;
|
|
26
|
+
ts: number;
|
|
27
|
+
reason?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const TELEMETRY_URL = process.env.MCPMAKE_TELEMETRY_URL;
|
|
31
|
+
const TELEMETRY_TOKEN = process.env.MCPMAKE_TELEMETRY_TOKEN ?? '';
|
|
32
|
+
const SERVER_SLUG = process.env.MCPMAKE_SERVER_SLUG ?? '';
|
|
33
|
+
const HEARTBEAT_MS = (() => {
|
|
34
|
+
const n = parseInt(process.env.MCPMAKE_TELEMETRY_HEARTBEAT_MS ?? '30000', 10);
|
|
35
|
+
return Number.isFinite(n) && n >= 1000 ? n : 30000;
|
|
36
|
+
})();
|
|
37
|
+
const POST_TIMEOUT_MS = 5000;
|
|
38
|
+
const MAX_BUFFER = 1000;
|
|
39
|
+
|
|
40
|
+
const ENABLED = Boolean(TELEMETRY_URL) && Boolean(TELEMETRY_TOKEN) && Boolean(SERVER_SLUG);
|
|
41
|
+
|
|
42
|
+
const buffer: TelemetryEvent[] = [];
|
|
43
|
+
const openSessions = new Set<string>();
|
|
44
|
+
let flushTimer: ReturnType<typeof setInterval> | undefined;
|
|
45
|
+
|
|
46
|
+
function pushEvent(ev: TelemetryEvent): void {
|
|
47
|
+
buffer.push(ev);
|
|
48
|
+
// Bound memory if the ingest is unreachable for a long time: keep the newest.
|
|
49
|
+
if (buffer.length > MAX_BUFFER) buffer.splice(0, buffer.length - MAX_BUFFER);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function ensureTimer(): void {
|
|
53
|
+
if (!ENABLED || flushTimer) return;
|
|
54
|
+
flushTimer = setInterval(() => {
|
|
55
|
+
void flush();
|
|
56
|
+
}, HEARTBEAT_MS);
|
|
57
|
+
// Never keep the process alive just for telemetry.
|
|
58
|
+
flushTimer.unref?.();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Record that a browser session became resident. */
|
|
62
|
+
export function sessionStarted(sessionId: string): void {
|
|
63
|
+
if (!ENABLED) return;
|
|
64
|
+
openSessions.add(sessionId);
|
|
65
|
+
pushEvent({ type: 'session_start', sessionId, ts: Date.now() });
|
|
66
|
+
ensureTimer();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Record that a browser session was torn down (idle | explicit | shutdown). */
|
|
70
|
+
export function sessionEnded(sessionId: string, reason: string): void {
|
|
71
|
+
if (!ENABLED) return;
|
|
72
|
+
openSessions.delete(sessionId);
|
|
73
|
+
pushEvent({ type: 'session_end', sessionId, ts: Date.now(), reason });
|
|
74
|
+
// Flush promptly so the host can finalize this session's billing.
|
|
75
|
+
void flush();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function flush(): Promise<void> {
|
|
79
|
+
if (!ENABLED) return;
|
|
80
|
+
const now = Date.now();
|
|
81
|
+
// Heartbeat every still-open session — proof the browser is still resident.
|
|
82
|
+
for (const id of openSessions) {
|
|
83
|
+
pushEvent({ type: 'heartbeat', sessionId: id, ts: now });
|
|
84
|
+
}
|
|
85
|
+
if (buffer.length === 0) return;
|
|
86
|
+
const events = buffer.splice(0, buffer.length);
|
|
87
|
+
try {
|
|
88
|
+
const controller = new AbortController();
|
|
89
|
+
const timer = setTimeout(() => controller.abort(), POST_TIMEOUT_MS);
|
|
90
|
+
try {
|
|
91
|
+
await fetch(TELEMETRY_URL as string, {
|
|
92
|
+
method: 'POST',
|
|
93
|
+
headers: {
|
|
94
|
+
'Content-Type': 'application/json',
|
|
95
|
+
Authorization: `Bearer ${TELEMETRY_TOKEN}`,
|
|
96
|
+
},
|
|
97
|
+
body: JSON.stringify({ v: 1, slug: SERVER_SLUG, events }),
|
|
98
|
+
signal: controller.signal,
|
|
99
|
+
});
|
|
100
|
+
} finally {
|
|
101
|
+
clearTimeout(timer);
|
|
102
|
+
}
|
|
103
|
+
} catch {
|
|
104
|
+
// Swallow: telemetry is best-effort and must never affect the server.
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Best-effort final flush on shutdown: end every open session and send. */
|
|
109
|
+
export async function flushOnShutdown(reason = 'shutdown'): Promise<void> {
|
|
110
|
+
if (!ENABLED) return;
|
|
111
|
+
const now = Date.now();
|
|
112
|
+
for (const id of [...openSessions]) {
|
|
113
|
+
openSessions.delete(id);
|
|
114
|
+
pushEvent({ type: 'session_end', sessionId: id, ts: now, reason });
|
|
115
|
+
}
|
|
116
|
+
await flush();
|
|
117
|
+
}
|
|
@@ -45,8 +45,8 @@ export function register(server: McpServer, config: AppConfig): void {
|
|
|
45
45
|
{{#each form.fields}}
|
|
46
46
|
{{#unless (eq fieldType "hidden")}}
|
|
47
47
|
{{#if (eq fieldType "checkbox")}}
|
|
48
|
-
if ((input as Record<string, unknown>)['{{jsString
|
|
49
|
-
const checked = Boolean((input as Record<string, unknown>)['{{jsString
|
|
48
|
+
if ((input as Record<string, unknown>)['{{jsString inputKey}}'] !== undefined) {
|
|
49
|
+
const checked = Boolean((input as Record<string, unknown>)['{{jsString inputKey}}']);
|
|
50
50
|
const fieldSelector = await resolveSelector(page, ['{{jsString selector.primary}}'{{#each selector.fallbacks}}, '{{jsString this}}'{{/each}}]);
|
|
51
51
|
if (checked) {
|
|
52
52
|
await page.check(fieldSelector);
|
|
@@ -55,21 +55,23 @@ export function register(server: McpServer, config: AppConfig): void {
|
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
{{else if (eq fieldType "select")}}
|
|
58
|
-
if ((input as Record<string, unknown>)['{{jsString
|
|
58
|
+
if ((input as Record<string, unknown>)['{{jsString inputKey}}'] !== undefined) {
|
|
59
59
|
const fieldSelector = await resolveSelector(page, ['{{jsString selector.primary}}'{{#each selector.fallbacks}}, '{{jsString this}}'{{/each}}]);
|
|
60
|
-
|
|
60
|
+
// The schema enum is built from option VALUES, and selectOption
|
|
61
|
+
// matches by value, so pass the value through directly.
|
|
62
|
+
await page.selectOption(fieldSelector, String((input as Record<string, unknown>)['{{jsString inputKey}}']));
|
|
61
63
|
}
|
|
62
64
|
{{else if (eq fieldType "radio")}}
|
|
63
|
-
if ((input as Record<string, unknown>)['{{jsString
|
|
65
|
+
if ((input as Record<string, unknown>)['{{jsString inputKey}}'] !== undefined) {
|
|
64
66
|
const radioName = '{{jsString name}}';
|
|
65
|
-
const radioValue = String((input as Record<string, unknown>)['{{jsString
|
|
67
|
+
const radioValue = String((input as Record<string, unknown>)['{{jsString inputKey}}']).replace(/["\\[\]()]/g, '\\$&');
|
|
66
68
|
const radioNameSel = radioName.replace(/["\\]/g, '\\$&');
|
|
67
69
|
await page.check(`input[name="${radioNameSel}"][value="${radioValue}"]`);
|
|
68
70
|
}
|
|
69
71
|
{{else}}
|
|
70
|
-
if ((input as Record<string, unknown>)['{{jsString
|
|
72
|
+
if ((input as Record<string, unknown>)['{{jsString inputKey}}'] !== undefined) {
|
|
71
73
|
const fieldSelector = await resolveSelector(page, ['{{jsString selector.primary}}'{{#each selector.fallbacks}}, '{{jsString this}}'{{/each}}]);
|
|
72
|
-
await page.fill(fieldSelector, String((input as Record<string, unknown>)['{{jsString
|
|
74
|
+
await page.fill(fieldSelector, String((input as Record<string, unknown>)['{{jsString inputKey}}']));
|
|
73
75
|
}
|
|
74
76
|
{{/if}}
|
|
75
77
|
{{/unless}}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"template-loader.d.ts","sourceRoot":"","sources":["../../src/emitter/template-loader.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"template-loader.d.ts","sourceRoot":"","sources":["../../src/emitter/template-loader.ts"],"names":[],"mappings":"AA8BA,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,GAAG,MAAM,CAU9D"}
|
|
@@ -13,6 +13,15 @@ Handlebars.registerHelper('capitalize', function (str) {
|
|
|
13
13
|
return str;
|
|
14
14
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
15
15
|
});
|
|
16
|
+
// Emit a value as a valid JS/TS literal via JSON.stringify. Safe for embedding
|
|
17
|
+
// untrusted spec-derived strings (e.g. OpenAPI security-scheme names) into
|
|
18
|
+
// generated code: JSON.stringify produces a double-quoted literal with all
|
|
19
|
+
// special characters escaped, so the value can never break out of the literal.
|
|
20
|
+
// Registered on the shared Handlebars singleton, so worker templates (which
|
|
21
|
+
// import the same instance) can use it too.
|
|
22
|
+
Handlebars.registerHelper('json', function (value) {
|
|
23
|
+
return JSON.stringify(value === undefined ? null : value);
|
|
24
|
+
});
|
|
16
25
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
17
26
|
export function renderTemplate(name, data) {
|
|
18
27
|
if (!templateCache.has(name)) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"template-loader.js","sourceRoot":"","sources":["../../src/emitter/template-loader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,UAAU,MAAM,YAAY,CAAC;AAEpC,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAC1D,MAAM,YAAY,GAAG,OAAO,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC;AAErD,MAAM,aAAa,GAAG,IAAI,GAAG,EAAuC,CAAC;AAErE,UAAU,CAAC,cAAc,CAAC,IAAI,EAAE,UAAU,CAAU,EAAE,CAAU;IAC9D,OAAO,CAAC,KAAK,CAAC,CAAC;AACjB,CAAC,CAAC,CAAC;AAEH,UAAU,CAAC,cAAc,CAAC,YAAY,EAAE,UAAU,GAAW;IAC3D,IAAI,CAAC,GAAG;QAAE,OAAO,GAAG,CAAC;IACrB,OAAO,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACpD,CAAC,CAAC,CAAC;AAEH,8DAA8D;AAC9D,MAAM,UAAU,cAAc,CAAC,IAAY,EAAE,IAAS;IACpD,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;QAC7B,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,EAAE,GAAG,IAAI,MAAM,CAAC,CAAC;QAC1D,IAAI,CAAC,YAAY,CAAC,UAAU,CAAC,YAAY,GAAG,GAAG,CAAC,EAAE,CAAC;YACjD,MAAM,IAAI,KAAK,CAAC,0BAA0B,IAAI,EAAE,CAAC,CAAC;QACpD,CAAC;QACD,MAAM,GAAG,GAAG,YAAY,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;QAChD,aAAa,CAAC,GAAG,CAAC,IAAI,EAAE,UAAU,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IACvE,CAAC;IACD,OAAO,aAAa,CAAC,GAAG,CAAC,IAAI,CAAE,CAAC,IAAI,CAAC,CAAC;AACxC,CAAC"}
|
|
1
|
+
{"version":3,"file":"template-loader.js","sourceRoot":"","sources":["../../src/emitter/template-loader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,UAAU,MAAM,YAAY,CAAC;AAEpC,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAC1D,MAAM,YAAY,GAAG,OAAO,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC;AAErD,MAAM,aAAa,GAAG,IAAI,GAAG,EAAuC,CAAC;AAErE,UAAU,CAAC,cAAc,CAAC,IAAI,EAAE,UAAU,CAAU,EAAE,CAAU;IAC9D,OAAO,CAAC,KAAK,CAAC,CAAC;AACjB,CAAC,CAAC,CAAC;AAEH,UAAU,CAAC,cAAc,CAAC,YAAY,EAAE,UAAU,GAAW;IAC3D,IAAI,CAAC,GAAG;QAAE,OAAO,GAAG,CAAC;IACrB,OAAO,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACpD,CAAC,CAAC,CAAC;AAEH,+EAA+E;AAC/E,2EAA2E;AAC3E,2EAA2E;AAC3E,+EAA+E;AAC/E,4EAA4E;AAC5E,4CAA4C;AAC5C,UAAU,CAAC,cAAc,CAAC,MAAM,EAAE,UAAU,KAAc;IACxD,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;AAC5D,CAAC,CAAC,CAAC;AAEH,8DAA8D;AAC9D,MAAM,UAAU,cAAc,CAAC,IAAY,EAAE,IAAS;IACpD,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;QAC7B,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,EAAE,GAAG,IAAI,MAAM,CAAC,CAAC;QAC1D,IAAI,CAAC,YAAY,CAAC,UAAU,CAAC,YAAY,GAAG,GAAG,CAAC,EAAE,CAAC;YACjD,MAAM,IAAI,KAAK,CAAC,0BAA0B,IAAI,EAAE,CAAC,CAAC;QACpD,CAAC;QACD,MAAM,GAAG,GAAG,YAAY,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;QAChD,aAAa,CAAC,GAAG,CAAC,IAAI,EAAE,UAAU,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IACvE,CAAC;IACD,OAAO,aAAa,CAAC,GAAG,CAAC,IAAI,CAAE,CAAC,IAAI,CAAC,CAAC;AACxC,CAAC"}
|