@mcpmake/core 0.3.4 → 0.3.5
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/goal-crawler.d.ts.map +1 -1
- package/dist/analyzer/goal-crawler.js +2 -1
- package/dist/analyzer/goal-crawler.js.map +1 -1
- package/dist/analyzer/site-crawler.d.ts.map +1 -1
- package/dist/analyzer/site-crawler.js +9 -3
- package/dist/analyzer/site-crawler.js.map +1 -1
- package/dist/emitter/index.d.ts.map +1 -1
- package/dist/emitter/index.js +128 -18
- package/dist/emitter/index.js.map +1 -1
- package/dist/emitter/mcpb-bundler.d.ts +14 -0
- package/dist/emitter/mcpb-bundler.d.ts.map +1 -1
- package/dist/emitter/mcpb-bundler.js +52 -4
- package/dist/emitter/mcpb-bundler.js.map +1 -1
- package/dist/emitter/python-annotations.d.ts +77 -0
- package/dist/emitter/python-annotations.d.ts.map +1 -0
- package/dist/emitter/python-annotations.js +330 -0
- package/dist/emitter/python-annotations.js.map +1 -0
- package/dist/emitter/python-templates/server.py.hbs +29 -8
- package/dist/emitter/site-templates/browser-manager.ts.hbs +140 -30
- package/dist/emitter/site-templates/config.ts.hbs +13 -1
- package/dist/emitter/site-templates/env.example.hbs +8 -0
- package/dist/emitter/site-templates/telemetry.ts.hbs +13 -2
- package/dist/emitter/templates/discovery.ts.hbs +43 -4
- package/dist/emitter/templates/server-main-http.ts.hbs +79 -1
- package/dist/emitter/worker-templates/wrangler.toml.hbs +5 -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/parser/har-normalizer.d.ts.map +1 -1
- package/dist/parser/har-normalizer.js +27 -2
- package/dist/parser/har-normalizer.js.map +1 -1
- package/dist/parser/postman-loader.d.ts.map +1 -1
- package/dist/parser/postman-loader.js +24 -2
- package/dist/parser/postman-loader.js.map +1 -1
- package/dist/parser/schema-converter.d.ts +8 -1
- package/dist/parser/schema-converter.d.ts.map +1 -1
- package/dist/parser/schema-converter.js +20 -2
- package/dist/parser/schema-converter.js.map +1 -1
- package/dist/recorder/browser-recorder.d.ts.map +1 -1
- package/dist/recorder/browser-recorder.js +2 -1
- package/dist/recorder/browser-recorder.js.map +1 -1
- package/dist/rescan/rescan-scheduler.d.ts +53 -2
- package/dist/rescan/rescan-scheduler.d.ts.map +1 -1
- package/dist/rescan/rescan-scheduler.js +163 -2
- package/dist/rescan/rescan-scheduler.js.map +1 -1
- package/dist/transformer/catalog-builder.d.ts +7 -1
- package/dist/transformer/catalog-builder.d.ts.map +1 -1
- package/dist/transformer/catalog-builder.js +1 -0
- package/dist/transformer/catalog-builder.js.map +1 -1
- package/dist/transformer/tool-builder.d.ts.map +1 -1
- package/dist/transformer/tool-builder.js +4 -1
- package/dist/transformer/tool-builder.js.map +1 -1
- package/dist/types/index.d.ts +39 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/logger.d.ts +38 -1
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +75 -1
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/playwright-loader.d.ts +21 -0
- package/dist/utils/playwright-loader.d.ts.map +1 -0
- package/dist/utils/playwright-loader.js +21 -0
- package/dist/utils/playwright-loader.js.map +1 -0
- package/dist/utils/ssrf-guard.d.ts.map +1 -1
- package/dist/utils/ssrf-guard.js +8 -0
- package/dist/utils/ssrf-guard.js.map +1 -1
- package/package.json +4 -3
|
@@ -28,6 +28,10 @@ const sessionCreations = new Map<string, Promise<{ page: Page; sessionId: string
|
|
|
28
28
|
// Single-flight the browser launch so concurrent cold starts can't spawn
|
|
29
29
|
// multiple Chromium processes.
|
|
30
30
|
let browserLaunch: Promise<Browser> | undefined;
|
|
31
|
+
// Incremented whenever closeBrowser() begins. A launch captures the current
|
|
32
|
+
// generation and must close itself instead of becoming active if teardown
|
|
33
|
+
// invalidated it while chromium.launch() was still pending.
|
|
34
|
+
let browserGeneration = 0;
|
|
31
35
|
|
|
32
36
|
const DEFAULT_SESSION = '__default__';
|
|
33
37
|
|
|
@@ -99,13 +103,35 @@ async function getBrowser(): Promise<Browser> {
|
|
|
99
103
|
|
|
100
104
|
// Only disable sandbox when running as root in a container.
|
|
101
105
|
const isRoot = process.getuid?.() === 0;
|
|
106
|
+
const generation = browserGeneration;
|
|
102
107
|
browserLaunch = chromium
|
|
103
108
|
.launch({
|
|
104
109
|
headless: config.headless,
|
|
105
110
|
args: isRoot ? ['--no-sandbox', '--disable-setuid-sandbox'] : [],
|
|
106
111
|
})
|
|
107
|
-
.then((b) => {
|
|
112
|
+
.then(async (b) => {
|
|
113
|
+
// closeBrowser() may have run while Chromium was launching. Never publish
|
|
114
|
+
// that stale browser into manager state: close it and fail the waiting
|
|
115
|
+
// creation so no resident process escapes the pool/cap.
|
|
116
|
+
if (generation !== browserGeneration) {
|
|
117
|
+
await b.close().catch(() => {});
|
|
118
|
+
throw new Error('Browser launch aborted: browser manager shut down');
|
|
119
|
+
}
|
|
108
120
|
browser = b;
|
|
121
|
+
// If the whole browser process dies, every session's context is dead.
|
|
122
|
+
// Reclaim them all: emit one session_end each (skipping reserved slots
|
|
123
|
+
// that never started) and clear the pool + idle timer so occupancy frees
|
|
124
|
+
// promptly. No-ops for our own browser.close() (the pool is already empty
|
|
125
|
+
// and `browser` already nulled by then).
|
|
126
|
+
b.on('disconnected', () => {
|
|
127
|
+
if (browser !== b) return;
|
|
128
|
+
browser = undefined;
|
|
129
|
+
clearIdleTimer();
|
|
130
|
+
for (const [id, session] of [...sessions]) {
|
|
131
|
+
sessions.delete(id);
|
|
132
|
+
if (session.context) telemetry.sessionEnded(id, 'crash');
|
|
133
|
+
}
|
|
134
|
+
});
|
|
109
135
|
return b;
|
|
110
136
|
})
|
|
111
137
|
.finally(() => {
|
|
@@ -125,20 +151,24 @@ async function getBrowser(): Promise<Browser> {
|
|
|
125
151
|
export async function getOrCreateSession(sessionId?: string): Promise<{ page: Page; sessionId: string }> {
|
|
126
152
|
const id = sessionId ?? DEFAULT_SESSION;
|
|
127
153
|
|
|
128
|
-
//
|
|
154
|
+
// Join an in-flight creation for this id FIRST. createSession reserves its slot
|
|
155
|
+
// synchronously (before its first await), so a concurrent second call that
|
|
156
|
+
// checked `sessions` first would observe that placeholder and return its
|
|
157
|
+
// not-yet-assigned (undefined) page. The in-flight promise resolves to the
|
|
158
|
+
// real page, so coalescing here gives every concurrent caller a usable page
|
|
159
|
+
// and a single shared context.
|
|
160
|
+
const inFlight = sessionCreations.get(id);
|
|
161
|
+
if (inFlight) return inFlight;
|
|
162
|
+
|
|
163
|
+
// Reuse a fully-created session (a placeholder reservation has no context yet
|
|
164
|
+
// and is always covered by the in-flight check above).
|
|
129
165
|
const existing = sessions.get(id);
|
|
130
|
-
if (existing) {
|
|
166
|
+
if (existing && existing.context) {
|
|
131
167
|
existing.lastAccessedAt = Date.now();
|
|
132
|
-
|
|
168
|
+
ensureIdleTimer();
|
|
133
169
|
return { page: existing.page, sessionId: id };
|
|
134
170
|
}
|
|
135
171
|
|
|
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
172
|
const creation = createSession(id).finally(() => {
|
|
143
173
|
sessionCreations.delete(id);
|
|
144
174
|
});
|
|
@@ -153,8 +183,8 @@ async function createSession(id: string): Promise<{ page: Page; sessionId: strin
|
|
|
153
183
|
const maxSessions = config.maxSessions ?? 10;
|
|
154
184
|
if (sessions.size >= maxSessions) {
|
|
155
185
|
throw new Error(
|
|
156
|
-
`
|
|
157
|
-
'Close an existing session
|
|
186
|
+
`Browser session limit reached (${maxSessions}). ` +
|
|
187
|
+
'Close an existing session and retry.'
|
|
158
188
|
);
|
|
159
189
|
}
|
|
160
190
|
const reserved: BrowserSession = {
|
|
@@ -164,28 +194,61 @@ async function createSession(id: string): Promise<{ page: Page; sessionId: strin
|
|
|
164
194
|
};
|
|
165
195
|
sessions.set(id, reserved);
|
|
166
196
|
|
|
197
|
+
let context: BrowserContext | undefined;
|
|
167
198
|
try {
|
|
168
199
|
const activeBrowser = await getBrowser();
|
|
169
|
-
|
|
200
|
+
context = await activeBrowser.newContext({
|
|
170
201
|
viewport: config.viewport,
|
|
171
202
|
...(config.userAgent ? { userAgent: config.userAgent } : {}),
|
|
172
203
|
});
|
|
173
204
|
const page = await context.newPage();
|
|
174
205
|
|
|
206
|
+
// If the manager was torn down (shutdown/disconnect) while we awaited the
|
|
207
|
+
// browser/context/page, our reservation was cleared. Do NOT resurrect it:
|
|
208
|
+
// bail so the catch below closes this fresh context. Otherwise we would
|
|
209
|
+
// orphan a live Chromium context outside the pool — escaping the cap, never
|
|
210
|
+
// reaped, and emitting a session_start AFTER its session_end. There is no
|
|
211
|
+
// await between here and `return`, so once this check passes the rest of
|
|
212
|
+
// creation is atomic with respect to teardown.
|
|
213
|
+
if (sessions.get(id) !== reserved) {
|
|
214
|
+
throw new Error('Session creation aborted: browser manager shut down');
|
|
215
|
+
}
|
|
216
|
+
|
|
175
217
|
reserved.context = context;
|
|
176
218
|
reserved.page = page;
|
|
177
219
|
reserved.lastAccessedAt = Date.now();
|
|
178
220
|
|
|
179
|
-
|
|
221
|
+
ensureIdleTimer();
|
|
180
222
|
|
|
181
223
|
// The browser context is now resident — start billing this session (no-op
|
|
182
224
|
// unless the host injected telemetry env).
|
|
183
225
|
telemetry.sessionStarted(id);
|
|
184
226
|
|
|
227
|
+
// Reclaim the slot exactly once if this session dies on its own — a Chromium
|
|
228
|
+
// context close, a renderer (page) crash, or an external close. Every
|
|
229
|
+
// intentional teardown path deletes the map entry BEFORE closing, so the
|
|
230
|
+
// `=== reserved` guard makes this a no-op for those and lets it fire only on
|
|
231
|
+
// an unexpected death, freeing host occupancy promptly instead of waiting
|
|
232
|
+
// out the idle grace. The guard also dedupes a page crash that then closes
|
|
233
|
+
// the context (only the first reclaim wins).
|
|
234
|
+
const reclaim = (reason: string): void => {
|
|
235
|
+
if (sessions.get(id) !== reserved) return;
|
|
236
|
+
sessions.delete(id);
|
|
237
|
+
context?.close().catch(() => {});
|
|
238
|
+
telemetry.sessionEnded(id, reason);
|
|
239
|
+
if (sessions.size === 0) void closeBrowser();
|
|
240
|
+
};
|
|
241
|
+
context.on('close', () => reclaim('crash'));
|
|
242
|
+
page.on('crash', () => reclaim('crash'));
|
|
243
|
+
|
|
185
244
|
return { page, sessionId: id };
|
|
186
245
|
} catch (err) {
|
|
187
|
-
// Release the
|
|
188
|
-
|
|
246
|
+
// Release the slot AND close any context we created, so a failed or aborted
|
|
247
|
+
// creation never leaks a live Chromium context or pins the cap slot. Only
|
|
248
|
+
// delete the entry if it is still ours (a concurrent shutdown may have
|
|
249
|
+
// already cleared it).
|
|
250
|
+
if (sessions.get(id) === reserved) sessions.delete(id);
|
|
251
|
+
await context?.close().catch(() => {});
|
|
189
252
|
throw err;
|
|
190
253
|
}
|
|
191
254
|
}
|
|
@@ -198,10 +261,18 @@ export async function closeSession(sessionId?: string): Promise<void> {
|
|
|
198
261
|
const session = sessions.get(id);
|
|
199
262
|
if (!session) return;
|
|
200
263
|
|
|
201
|
-
|
|
264
|
+
// Claim the teardown AND emit session_end SYNCHRONOUSLY, before the first
|
|
265
|
+
// await. Deleting the map entry and removing the telemetry token together,
|
|
266
|
+
// up front, means: (a) a concurrent idle sweep or second close sees the slot
|
|
267
|
+
// gone and skips it; and (b) a same-id session re-created during the
|
|
268
|
+
// context-close await below gets its OWN fresh telemetry token instead of
|
|
269
|
+
// sharing this one (which would suppress its later session_end). Exactly one
|
|
270
|
+
// session_end per teardown.
|
|
202
271
|
sessions.delete(id);
|
|
203
272
|
telemetry.sessionEnded(id, 'explicit');
|
|
204
273
|
|
|
274
|
+
await session.context?.close().catch(() => {});
|
|
275
|
+
|
|
205
276
|
// If no sessions remain, close the browser
|
|
206
277
|
if (sessions.size === 0) {
|
|
207
278
|
await closeBrowser();
|
|
@@ -213,17 +284,42 @@ export async function closeSession(sessionId?: string): Promise<void> {
|
|
|
213
284
|
*/
|
|
214
285
|
export async function closeBrowser(): Promise<void> {
|
|
215
286
|
clearIdleTimer();
|
|
216
|
-
|
|
217
|
-
|
|
287
|
+
// Invalidate a chromium.launch() already in progress. Its completion handler
|
|
288
|
+
// will close the resulting process rather than assigning it to `browser`.
|
|
289
|
+
browserGeneration++;
|
|
290
|
+
const launching = browserLaunch;
|
|
291
|
+
|
|
292
|
+
// Null `browser` synchronously BEFORE awaiting its close. While close() is in
|
|
293
|
+
// flight the instance still reports isConnected() === true, so without this a
|
|
294
|
+
// concurrent getOrCreateSession would be handed the dying browser and build a
|
|
295
|
+
// context on it that dies immediately. Nulling up front forces a relaunch
|
|
296
|
+
// instead, and single-flights closeBrowser (a second concurrent call sees no
|
|
297
|
+
// browser and skips the redundant close()).
|
|
298
|
+
const dying = browser;
|
|
299
|
+
browser = undefined;
|
|
300
|
+
|
|
301
|
+
// Snapshot and clear the pool, then emit every session_end SYNCHRONOUSLY
|
|
302
|
+
// (before any await) so a same-id session re-created during the context-close
|
|
303
|
+
// awaits gets its own telemetry token rather than sharing a snapshotted one.
|
|
304
|
+
// Reserved-but-unstarted slots are skipped: no context to close, and no
|
|
305
|
+
// session_start to balance (the in-flight createSession aborts itself once it
|
|
306
|
+
// sees its reservation cleared).
|
|
307
|
+
const entries = [...sessions];
|
|
308
|
+
sessions.clear();
|
|
309
|
+
for (const [id, session] of entries) {
|
|
310
|
+
if (session.context) telemetry.sessionEnded(id, 'shutdown');
|
|
311
|
+
}
|
|
312
|
+
for (const [, session] of entries) {
|
|
218
313
|
await session.context?.close().catch(() => {});
|
|
219
|
-
sessions.delete(id);
|
|
220
|
-
telemetry.sessionEnded(id, 'shutdown');
|
|
221
314
|
}
|
|
222
315
|
|
|
223
|
-
if (
|
|
224
|
-
await
|
|
225
|
-
browser = undefined;
|
|
316
|
+
if (dying) {
|
|
317
|
+
await dying.close().catch(() => {});
|
|
226
318
|
}
|
|
319
|
+
// Wait for an invalidated launch to finish its self-cleanup. This makes
|
|
320
|
+
// closeBrowser() a real lifecycle barrier: after it resolves, no Chromium
|
|
321
|
+
// process from work that began before teardown can still become resident.
|
|
322
|
+
await launching?.catch(() => {});
|
|
227
323
|
}
|
|
228
324
|
|
|
229
325
|
/**
|
|
@@ -235,7 +331,9 @@ export async function takeScreenshot(
|
|
|
235
331
|
): Promise<Buffer> {
|
|
236
332
|
const id = sessionId ?? DEFAULT_SESSION;
|
|
237
333
|
const session = sessions.get(id);
|
|
238
|
-
|
|
334
|
+
// A reserved, mid-creation slot is in the map but has no page yet — treat it
|
|
335
|
+
// as not-yet-active so callers get the clear error, not a TypeError.
|
|
336
|
+
if (!session || !session.context) {
|
|
239
337
|
throw new Error(`No active session: ${id}`);
|
|
240
338
|
}
|
|
241
339
|
session.lastAccessedAt = Date.now();
|
|
@@ -243,10 +341,13 @@ export async function takeScreenshot(
|
|
|
243
341
|
}
|
|
244
342
|
|
|
245
343
|
/**
|
|
246
|
-
* List active session IDs.
|
|
344
|
+
* List active session IDs. Excludes reserved, mid-creation slots (no page yet)
|
|
345
|
+
* so a returned id is always usable with takeScreenshot/tool calls.
|
|
247
346
|
*/
|
|
248
347
|
export function listSessions(): string[] {
|
|
249
|
-
return Array.from(sessions.
|
|
348
|
+
return Array.from(sessions.entries())
|
|
349
|
+
.filter(([, session]) => session.context !== undefined)
|
|
350
|
+
.map(([id]) => id);
|
|
250
351
|
}
|
|
251
352
|
|
|
252
353
|
/**
|
|
@@ -258,16 +359,25 @@ export function isBrowserRunning(): boolean {
|
|
|
258
359
|
|
|
259
360
|
// ─── Idle Timer ─────────────────────────────────────────────────────
|
|
260
361
|
|
|
261
|
-
|
|
262
|
-
|
|
362
|
+
// Start the idle reaper once and let it tick at a FIXED cadence. It must NOT be
|
|
363
|
+
// reset on session access: resetting on every access would let a busy container
|
|
364
|
+
// (one session touched within each interval) starve the reaper so it never
|
|
365
|
+
// fires, leaving genuinely-idle sibling sessions resident forever. Eviction
|
|
366
|
+
// keys off each session's own lastAccessedAt, so a steady tick is sufficient —
|
|
367
|
+
// touching a session keeps only that session alive.
|
|
368
|
+
function ensureIdleTimer(): void {
|
|
369
|
+
if (idleTimer) return;
|
|
263
370
|
idleTimer = setInterval(() => {
|
|
264
371
|
const now = Date.now();
|
|
265
372
|
// Close idle sessions. Skip slots still being created (no context yet).
|
|
266
373
|
for (const [id, session] of sessions) {
|
|
267
374
|
if (!session.context) continue;
|
|
268
375
|
if (now - session.lastAccessedAt > config.idleTimeoutMs) {
|
|
269
|
-
|
|
376
|
+
// Delete before closing so the context's own 'close' handler (and any
|
|
377
|
+
// concurrent teardown) sees the slot already claimed — session_end is
|
|
378
|
+
// emitted once, here, with reason 'idle'.
|
|
270
379
|
sessions.delete(id);
|
|
380
|
+
session.context.close().catch(() => {});
|
|
271
381
|
telemetry.sessionEnded(id, 'idle');
|
|
272
382
|
}
|
|
273
383
|
}
|
|
@@ -16,6 +16,15 @@ const posInt = (v: string | undefined, d: number): number => {
|
|
|
16
16
|
return Number.isInteger(n) && n > 0 ? n : d;
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
+
// Hard ceiling on concurrently-resident browser sessions for THIS container.
|
|
20
|
+
// The cloud host injects MCP_MAX_BROWSER_SESSIONS at container start to pin the
|
|
21
|
+
// owner's per-plan concurrent-browser limit, making the container's hard cap the
|
|
22
|
+
// plan cap. MAX_SESSIONS is the legacy/self-host alias. Either env var, when a
|
|
23
|
+
// valid positive integer, overrides the baked-in default; an unset or invalid
|
|
24
|
+
// value falls through to the next source. DEFAULT_MAX_SESSIONS is the safe
|
|
25
|
+
// fallback. The browser manager refuses to launch a session past this cap.
|
|
26
|
+
const DEFAULT_MAX_SESSIONS = {{#if browserConfig.maxSessions}}{{browserConfig.maxSessions}}{{else}}10{{/if}};
|
|
27
|
+
|
|
19
28
|
export function loadConfig(): AppConfig {
|
|
20
29
|
return {
|
|
21
30
|
baseUrl: process.env.BASE_URL ?? '{{{baseUrl}}}',
|
|
@@ -27,7 +36,10 @@ export function loadConfig(): AppConfig {
|
|
|
27
36
|
height: posInt(process.env.VIEWPORT_HEIGHT, {{browserConfig.viewport.height}}),
|
|
28
37
|
},
|
|
29
38
|
userAgent: process.env.USER_AGENT || undefined,
|
|
30
|
-
maxSessions: posInt(
|
|
39
|
+
maxSessions: posInt(
|
|
40
|
+
process.env.MCP_MAX_BROWSER_SESSIONS,
|
|
41
|
+
posInt(process.env.MAX_SESSIONS, DEFAULT_MAX_SESSIONS),
|
|
42
|
+
),
|
|
31
43
|
},
|
|
32
44
|
};
|
|
33
45
|
}
|
|
@@ -10,6 +10,14 @@ VIEWPORT_WIDTH={{browserConfig.viewport.width}}
|
|
|
10
10
|
VIEWPORT_HEIGHT={{browserConfig.viewport.height}}
|
|
11
11
|
# USER_AGENT=
|
|
12
12
|
|
|
13
|
+
# Hard cap on concurrently-resident browser sessions (Chromium contexts) this
|
|
14
|
+
# server will hold. A tool call that would open one past the cap is refused with
|
|
15
|
+
# a clear error rather than launching another browser. Managed hosting injects
|
|
16
|
+
# this to enforce the plan's concurrent-browser limit. MAX_SESSIONS is a legacy
|
|
17
|
+
# alias; MCP_MAX_BROWSER_SESSIONS wins when both are set.
|
|
18
|
+
MCP_MAX_BROWSER_SESSIONS={{#if browserConfig.maxSessions}}{{browserConfig.maxSessions}}{{else}}10{{/if}}
|
|
19
|
+
# MAX_SESSIONS={{#if browserConfig.maxSessions}}{{browserConfig.maxSessions}}{{else}}10{{/if}}
|
|
20
|
+
|
|
13
21
|
# Browser-session telemetry (managed hosting only — leave UNSET to self-host).
|
|
14
22
|
# When the host sets these, the server reports exact browser-session lifetime
|
|
15
23
|
# for metering. It NEVER phones home unless MCPMAKE_TELEMETRY_URL is set.
|
|
@@ -66,10 +66,16 @@ export function sessionStarted(sessionId: string): void {
|
|
|
66
66
|
ensureTimer();
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
/** Record that a browser session was torn down (idle | explicit | shutdown
|
|
69
|
+
/** Record that a browser session was torn down (idle | explicit | shutdown |
|
|
70
|
+
* crash). Idempotent: emits at most one session_end per session. A second end
|
|
71
|
+
* for the same id (e.g. an explicit close that resumes after flushOnShutdown
|
|
72
|
+
* already ended it on SIGTERM, or a crash racing a teardown) is a no-op, and an
|
|
73
|
+
* end for a session that never started (a reserved-but-unstarted slot) emits
|
|
74
|
+
* nothing — so the host's occupancy ledger can never be double-decremented. */
|
|
70
75
|
export function sessionEnded(sessionId: string, reason: string): void {
|
|
71
76
|
if (!ENABLED) return;
|
|
72
|
-
openSessions.
|
|
77
|
+
// openSessions is the source of truth: only the call that removes the id emits.
|
|
78
|
+
if (!openSessions.delete(sessionId)) return;
|
|
73
79
|
pushEvent({ type: 'session_end', sessionId, ts: Date.now(), reason });
|
|
74
80
|
// Flush promptly so the host can finalize this session's billing.
|
|
75
81
|
void flush();
|
|
@@ -108,6 +114,11 @@ async function flush(): Promise<void> {
|
|
|
108
114
|
/** Best-effort final flush on shutdown: end every open session and send. */
|
|
109
115
|
export async function flushOnShutdown(reason = 'shutdown'): Promise<void> {
|
|
110
116
|
if (!ENABLED) return;
|
|
117
|
+
// Stop the heartbeat interval so no flush can fire after this final boundary.
|
|
118
|
+
if (flushTimer) {
|
|
119
|
+
clearInterval(flushTimer);
|
|
120
|
+
flushTimer = undefined;
|
|
121
|
+
}
|
|
111
122
|
const now = Date.now();
|
|
112
123
|
for (const id of [...openSessions]) {
|
|
113
124
|
openSessions.delete(id);
|
|
@@ -10,6 +10,7 @@ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
10
10
|
import { executeRequest } from './http.js';
|
|
11
11
|
import { applyJqFilter } from './response-filter.js';
|
|
12
12
|
import type { AppConfig } from './config.js';
|
|
13
|
+
import type { AuthRequirement } from './auth.js';
|
|
13
14
|
import catalogData from './tool-catalog.json' with { type: 'json' };
|
|
14
15
|
|
|
15
16
|
interface CatalogParamMapping {
|
|
@@ -35,6 +36,10 @@ interface CatalogEntry {
|
|
|
35
36
|
/** The MCP input key for the request body (`body`, `requestBody`, etc.).
|
|
36
37
|
* Only present when `hasRequestBody` is true. */
|
|
37
38
|
bodyInputKey?: string;
|
|
39
|
+
/** Per-operation outbound-auth requirement (D-H2). Forwarded to executeRequest
|
|
40
|
+
* so a public operation (`security: []`) sends no auth and a scheme-scoped
|
|
41
|
+
* operation sends only its scheme(s). Absent → every configured scheme. */
|
|
42
|
+
authRequirement?: AuthRequirement;
|
|
38
43
|
}
|
|
39
44
|
|
|
40
45
|
const catalog: CatalogEntry[] = catalogData as CatalogEntry[];
|
|
@@ -169,15 +174,46 @@ export function registerDiscoveryTools(server: McpServer, config: AppConfig): vo
|
|
|
169
174
|
}
|
|
170
175
|
|
|
171
176
|
try {
|
|
172
|
-
// Build
|
|
173
|
-
// into the
|
|
174
|
-
|
|
177
|
+
// Build the path suffix from the template — read args by inputKey,
|
|
178
|
+
// substitute wireName into the path. Handles parameter name collisions
|
|
179
|
+
// (R14-A). The leftover-placeholder check (R5-F) runs on the PATH ONLY,
|
|
180
|
+
// not on config.baseUrl: a base URL may legitimately contain an
|
|
181
|
+
// unresolved OpenAPI server variable (e.g. `https://api.{region}.x.com`)
|
|
182
|
+
// that is not a tool path parameter, and scanning it would falsely fail
|
|
183
|
+
// every call.
|
|
184
|
+
let pathSuffix = tool.path;
|
|
175
185
|
for (const p of tool.pathParams) {
|
|
176
186
|
if (args[p.inputKey] !== undefined) {
|
|
177
|
-
|
|
187
|
+
pathSuffix = pathSuffix.replace(
|
|
188
|
+
`{${p.wireName}}`,
|
|
189
|
+
encodeURIComponent(String(args[p.inputKey])),
|
|
190
|
+
);
|
|
178
191
|
}
|
|
179
192
|
}
|
|
180
193
|
|
|
194
|
+
// R5-F: refuse to fire a request with unsubstituted path placeholders.
|
|
195
|
+
// A missing required path parameter leaves a literal `{name}` in the
|
|
196
|
+
// path, which would otherwise be sent verbatim upstream. Return an MCP
|
|
197
|
+
// error listing the unfilled parameter(s) instead.
|
|
198
|
+
const missing = pathSuffix.match(/\{[^}]+\}/g);
|
|
199
|
+
if (missing && missing.length > 0) {
|
|
200
|
+
return {
|
|
201
|
+
content: [
|
|
202
|
+
{
|
|
203
|
+
type: 'text' as const,
|
|
204
|
+
text: `Missing required path parameter(s): ${missing
|
|
205
|
+
.map((s) => s.slice(1, -1))
|
|
206
|
+
.join(', ')}. Use get_tool_schema to see required inputs.`,
|
|
207
|
+
},
|
|
208
|
+
],
|
|
209
|
+
isError: true,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Path is fully substituted — prepend the (possibly server-variable-
|
|
214
|
+
// bearing) base URL to form the request URL.
|
|
215
|
+
let url = config.baseUrl + pathSuffix;
|
|
216
|
+
|
|
181
217
|
// Add query params — read args by inputKey, send upstream under wireName (R14-A).
|
|
182
218
|
const queryParams = new URLSearchParams();
|
|
183
219
|
for (const p of tool.queryParams) {
|
|
@@ -198,6 +234,9 @@ export function registerDiscoveryTools(server: McpServer, config: AppConfig): vo
|
|
|
198
234
|
headers: {},
|
|
199
235
|
config,
|
|
200
236
|
idempotencyKey,
|
|
237
|
+
// R5-E: scope outbound auth per operation exactly like the static
|
|
238
|
+
// per-tool handlers. Absent → executeRequest applies every scheme.
|
|
239
|
+
authRequirement: tool.authRequirement,
|
|
201
240
|
});
|
|
202
241
|
|
|
203
242
|
const result =
|
|
@@ -174,6 +174,39 @@ if (transportMode === 'http') {
|
|
|
174
174
|
}
|
|
175
175
|
const allowedOrigin = process.env.ALLOWED_ORIGIN;
|
|
176
176
|
const authToken = process.env.MCP_AUTH_TOKEN;
|
|
177
|
+
{{#if hasOAuth}}
|
|
178
|
+
/* ── OAuth issuer resolution inputs ──────────────────────────────────────
|
|
179
|
+
* The unauthenticated /.well-known/oauth-authorization-server endpoint must
|
|
180
|
+
* not reflect an arbitrary client-controlled Host header into the issuer
|
|
181
|
+
* (host-header spoofing). Operators pin the issuer with one of:
|
|
182
|
+
* - ALLOWED_ORIGIN (existing; takes precedence)
|
|
183
|
+
* - MCP_PUBLIC_URL (absolute http(s) URL; its origin is used)
|
|
184
|
+
* - MCP_ALLOWED_HOSTS (comma/space-separated Host allowlist)
|
|
185
|
+
* With none set we fall back to the request Host for backward-compat (dev),
|
|
186
|
+
* after warning once that the issuer is Host-derived. */
|
|
187
|
+
const mcpPublicOrigin = (() => {
|
|
188
|
+
const raw = process.env.MCP_PUBLIC_URL;
|
|
189
|
+
if (!raw) return undefined;
|
|
190
|
+
try {
|
|
191
|
+
const u = new URL(raw);
|
|
192
|
+
if (u.protocol !== 'http:' && u.protocol !== 'https:') {
|
|
193
|
+
log('error', 'MCP_PUBLIC_URL must be an http(s) URL — ignoring', { MCP_PUBLIC_URL: raw });
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
return u.origin;
|
|
197
|
+
} catch {
|
|
198
|
+
log('error', 'MCP_PUBLIC_URL is not a valid absolute URL — ignoring', { MCP_PUBLIC_URL: raw });
|
|
199
|
+
return undefined;
|
|
200
|
+
}
|
|
201
|
+
})();
|
|
202
|
+
const allowedHosts = new Set(
|
|
203
|
+
(process.env.MCP_ALLOWED_HOSTS ?? '')
|
|
204
|
+
.split(/[\s,]+/)
|
|
205
|
+
.map((h) => h.trim().toLowerCase())
|
|
206
|
+
.filter((h) => h.length > 0),
|
|
207
|
+
);
|
|
208
|
+
let warnedHostDerivedIssuer = false;
|
|
209
|
+
{{/if}}
|
|
177
210
|
/* Fail closed: with no MCP_AUTH_TOKEN the server denies every authenticated
|
|
178
211
|
* route (401) unless the operator opted in via MCP_ALLOW_UNAUTHENTICATED=true.
|
|
179
212
|
* Warn loudly, once, when running wide open so it is never a silent default. */
|
|
@@ -369,7 +402,52 @@ if (transportMode === 'http') {
|
|
|
369
402
|
return;
|
|
370
403
|
}
|
|
371
404
|
const proto = req.headers['x-forwarded-proto'] === 'https' ? 'https' : 'http';
|
|
372
|
-
|
|
405
|
+
/* Resolve the OAuth issuer without trusting an arbitrary Host header.
|
|
406
|
+
* Precedence: ALLOWED_ORIGIN → MCP_PUBLIC_URL → MCP_ALLOWED_HOSTS →
|
|
407
|
+
* (dev fallback) the request Host, but only when no allowlist/public
|
|
408
|
+
* URL pins it. When MCP_ALLOWED_HOSTS is configured, a Host outside it
|
|
409
|
+
* is rejected rather than reflected. */
|
|
410
|
+
const rawHost = req.headers.host;
|
|
411
|
+
const host = typeof rawHost === 'string' ? rawHost.toLowerCase() : undefined;
|
|
412
|
+
// Parse the Host once. `hostname` (no port, no userinfo) drives the
|
|
413
|
+
// allowlist so a bare-hostname allowlist (MCP_ALLOWED_HOSTS=example.com)
|
|
414
|
+
// matches `Host: example.com:3000`; `hostPort` (hostname:port, userinfo
|
|
415
|
+
// STRIPPED) builds the issuer so a `Host: evil@example.com` cannot inject
|
|
416
|
+
// a userinfo segment into the issued metadata. URL parsing also handles
|
|
417
|
+
// IPv6 brackets. Falls back to undefined when the Host is malformed.
|
|
418
|
+
const parsedHost = (() => {
|
|
419
|
+
if (typeof rawHost !== 'string') return undefined;
|
|
420
|
+
try {
|
|
421
|
+
const u = new URL(`http://${rawHost}`);
|
|
422
|
+
return { hostname: u.hostname.toLowerCase(), hostPort: u.host };
|
|
423
|
+
} catch {
|
|
424
|
+
return undefined;
|
|
425
|
+
}
|
|
426
|
+
})();
|
|
427
|
+
const hostNoPort = parsedHost?.hostname ?? host;
|
|
428
|
+
let issuer: string;
|
|
429
|
+
if (allowedOrigin) {
|
|
430
|
+
issuer = allowedOrigin;
|
|
431
|
+
} else if (mcpPublicOrigin) {
|
|
432
|
+
issuer = mcpPublicOrigin;
|
|
433
|
+
} else if (allowedHosts.size > 0) {
|
|
434
|
+
if (!host || (!allowedHosts.has(host) && !(hostNoPort && allowedHosts.has(hostNoPort)))) {
|
|
435
|
+
log('error', 'Invalid Host header for OAuth metadata', { host: rawHost });
|
|
436
|
+
sendJson(res, 400, { error: 'Invalid Host header for OAuth metadata' });
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
// Build from the parsed host:port (userinfo stripped), never raw.
|
|
440
|
+
issuer = `${proto}://${parsedHost?.hostPort ?? rawHost}`;
|
|
441
|
+
} else {
|
|
442
|
+
/* No issuer pinned: reflect the request Host for backward-compat with
|
|
443
|
+
* proxy deployments that set neither var. Warn once so operators know
|
|
444
|
+
* to lock it down via MCP_PUBLIC_URL or MCP_ALLOWED_HOSTS. */
|
|
445
|
+
if (!warnedHostDerivedIssuer) {
|
|
446
|
+
warnedHostDerivedIssuer = true;
|
|
447
|
+
log('info', 'OAuth issuer derived from the Host header — set MCP_PUBLIC_URL or MCP_ALLOWED_HOSTS to pin it', { host: rawHost });
|
|
448
|
+
}
|
|
449
|
+
issuer = `${proto}://${rawHost ?? 'localhost'}`;
|
|
450
|
+
}
|
|
373
451
|
sendJson(
|
|
374
452
|
res,
|
|
375
453
|
200,
|
|
@@ -7,7 +7,11 @@ compatibility_flags = ["nodejs_compat"]
|
|
|
7
7
|
|
|
8
8
|
# Non-secret configuration. Override per-environment as needed.
|
|
9
9
|
[vars]
|
|
10
|
-
|
|
10
|
+
# {{{json}}} emits a JSON string literal, which is a valid TOML basic string with
|
|
11
|
+
# identical escape semantics. Unlike a raw "{{baseUrl}}" interpolation, this fully
|
|
12
|
+
# escapes backslashes/quotes/newlines so an attacker-controlled URL cannot inject a
|
|
13
|
+
# spurious key or [section] header into the generated wrangler.toml (A4-15).
|
|
14
|
+
BASE_URL = {{{json baseUrl}}}
|
|
11
15
|
MAX_RETRIES = "3"
|
|
12
16
|
REQUEST_INTERVAL_MS = "100"
|
|
13
17
|
# ALLOWED_ORIGIN = "https://your-client.example.com"
|
package/dist/index.d.ts
CHANGED
|
@@ -74,6 +74,7 @@ export { fail } from './utils/fail.js';
|
|
|
74
74
|
export { pathExists } from './utils/fs.js';
|
|
75
75
|
export { confirmOperations } from './utils/interactive.js';
|
|
76
76
|
export { watchFile } from './utils/watcher.js';
|
|
77
|
+
export { loadChromium } from './utils/playwright-loader.js';
|
|
77
78
|
export { FAMILY_A_PRICING, DEFAULT_PRICING_SERVER, fetchPricing, formatPrice } from './pricing.js';
|
|
78
79
|
export type { BillingPeriod, PricePoint } from './pricing.js';
|
|
79
80
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,OAAO,EACL,WAAW,EACX,iBAAiB,EACjB,iBAAiB,EACjB,eAAe,GAChB,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAC9D,OAAO,EAAE,qBAAqB,EAAE,MAAM,iCAAiC,CAAC;AAKxE,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAC7D,YAAY,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AAC7D,OAAO,EAAE,eAAe,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AACpG,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC1D,OAAO,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AACpE,YAAY,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AACxE,OAAO,EAAE,mBAAmB,EAAE,yBAAyB,EAAE,MAAM,8BAA8B,CAAC;AAC9F,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,YAAY,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAC5D,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAC5D,YAAY,EAAE,eAAe,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AACzF,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AAKnE,OAAO,EACL,UAAU,EACV,WAAW,EACX,UAAU,EACV,cAAc,EACd,kBAAkB,GACnB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AACpE,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AACnF,OAAO,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AACrE,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAC;AACjF,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACnE,YAAY,EAAE,UAAU,EAAE,MAAM,gCAAgC,CAAC;AACjE,OAAO,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAC9D,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,YAAY,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAChE,YAAY,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,EAAE,oBAAoB,EAAE,MAAM,oCAAoC,CAAC;AAC1E,YAAY,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,oCAAoC,CAAC;AAC5F,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,EACL,eAAe,EACf,mBAAmB,EACnB,sBAAsB,GACvB,MAAM,sCAAsC,CAAC;AAC9C,OAAO,EAAE,oBAAoB,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAC1F,OAAO,EAAE,kBAAkB,EAAE,MAAM,uCAAuC,CAAC;AAK3E,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AACvD,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAC5E,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AACrD,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AACpF,OAAO,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AACnE,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAC9D,YAAY,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AAC1E,OAAO,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAC/D,YAAY,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAKnE,OAAO,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAC;AACzE,OAAO,EAAE,0BAA0B,EAAE,MAAM,qCAAqC,CAAC;AACjF,OAAO,EAAE,kBAAkB,EAAE,MAAM,uCAAuC,CAAC;AAK3E,OAAO,EAAE,2BAA2B,EAAE,MAAM,+BAA+B,CAAC;AAK5E,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AACpF,YAAY,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAK3D,OAAO,EAAE,oBAAoB,EAAE,MAAM,gCAAgC,CAAC;AAKtE,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAC9D,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAC/E,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAClF,OAAO,EAAE,6BAA6B,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAC3F,YAAY,EAAE,qBAAqB,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAKpG,OAAO,EAAE,yBAAyB,EAAE,MAAM,kCAAkC,CAAC;AAK7E,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAChF,YAAY,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAK3D,cAAc,kBAAkB,CAAC;AACjC,cAAc,iBAAiB,CAAC;AAKhC,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAC3C,OAAO,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AACvC,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,OAAO,EACL,WAAW,EACX,iBAAiB,EACjB,iBAAiB,EACjB,eAAe,GAChB,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAC9D,OAAO,EAAE,qBAAqB,EAAE,MAAM,iCAAiC,CAAC;AAKxE,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAC7D,YAAY,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AAC7D,OAAO,EAAE,eAAe,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AACpG,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC1D,OAAO,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AACpE,YAAY,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AACxE,OAAO,EAAE,mBAAmB,EAAE,yBAAyB,EAAE,MAAM,8BAA8B,CAAC;AAC9F,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AACrD,OAAO,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAC1D,YAAY,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAC5D,OAAO,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAC5D,YAAY,EAAE,eAAe,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AACzF,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AAKnE,OAAO,EACL,UAAU,EACV,WAAW,EACX,UAAU,EACV,cAAc,EACd,kBAAkB,GACnB,MAAM,yBAAyB,CAAC;AACjC,OAAO,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AACpE,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AACnF,OAAO,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AACrE,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAC;AACjF,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACnE,YAAY,EAAE,UAAU,EAAE,MAAM,gCAAgC,CAAC;AACjE,OAAO,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAC9D,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,YAAY,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAChE,YAAY,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,EAAE,oBAAoB,EAAE,MAAM,oCAAoC,CAAC;AAC1E,YAAY,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,oCAAoC,CAAC;AAC5F,OAAO,EAAE,kBAAkB,EAAE,MAAM,4BAA4B,CAAC;AAChE,OAAO,EACL,eAAe,EACf,mBAAmB,EACnB,sBAAsB,GACvB,MAAM,sCAAsC,CAAC;AAC9C,OAAO,EAAE,oBAAoB,EAAE,eAAe,EAAE,MAAM,mCAAmC,CAAC;AAC1F,OAAO,EAAE,kBAAkB,EAAE,MAAM,uCAAuC,CAAC;AAK3E,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AACvD,YAAY,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAC5E,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAC;AACrD,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AACpF,OAAO,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AACnE,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,EAAE,aAAa,EAAE,MAAM,+BAA+B,CAAC;AAC9D,YAAY,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AAC1E,OAAO,EAAE,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAC/D,YAAY,EAAE,gBAAgB,EAAE,MAAM,4BAA4B,CAAC;AAKnE,OAAO,EAAE,iBAAiB,EAAE,MAAM,sCAAsC,CAAC;AACzE,OAAO,EAAE,0BAA0B,EAAE,MAAM,qCAAqC,CAAC;AACjF,OAAO,EAAE,kBAAkB,EAAE,MAAM,uCAAuC,CAAC;AAK3E,OAAO,EAAE,2BAA2B,EAAE,MAAM,+BAA+B,CAAC;AAK5E,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,sBAAsB,CAAC;AACpF,YAAY,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAK3D,OAAO,EAAE,oBAAoB,EAAE,MAAM,gCAAgC,CAAC;AAKtE,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAC9D,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAC/E,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAClF,OAAO,EAAE,6BAA6B,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAC3F,YAAY,EAAE,qBAAqB,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAKpG,OAAO,EAAE,yBAAyB,EAAE,MAAM,kCAAkC,CAAC;AAK7E,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAChF,YAAY,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAK3D,cAAc,kBAAkB,CAAC;AACjC,cAAc,iBAAiB,CAAC;AAKhC,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAC3C,OAAO,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AACvC,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,8BAA8B,CAAC;AAK5D,OAAO,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AACnG,YAAY,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -97,6 +97,7 @@ export { fail } from './utils/fail.js';
|
|
|
97
97
|
export { pathExists } from './utils/fs.js';
|
|
98
98
|
export { confirmOperations } from './utils/interactive.js';
|
|
99
99
|
export { watchFile } from './utils/watcher.js';
|
|
100
|
+
export { loadChromium } from './utils/playwright-loader.js';
|
|
100
101
|
// ---------------------------------------------------------------------------
|
|
101
102
|
// Pricing
|
|
102
103
|
// ---------------------------------------------------------------------------
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"har-normalizer.d.ts","sourceRoot":"","sources":["../../src/parser/har-normalizer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAOxC,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,KAAK,CAAC;IACb,cAAc,EAAE,MAAM,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,SAAS,EAAE,CAAC;IACxB,WAAW,EAAE,UAAU,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,GAAG,SAAS,GAAG,QAAQ,CAAC;CAC7C;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,SAAS,GAAG,SAAS,GAAG,QAAQ,CAAC;CAChD;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,KAAK,GAAG,eAAe,
|
|
1
|
+
{"version":3,"file":"har-normalizer.d.ts","sourceRoot":"","sources":["../../src/parser/har-normalizer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAOxC,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,KAAK,CAAC;IACb,cAAc,EAAE,MAAM,CAAC;IACvB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,SAAS,EAAE,CAAC;IACxB,WAAW,EAAE,UAAU,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,GAAG,SAAS,GAAG,QAAQ,CAAC;CAC7C;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,SAAS,GAAG,SAAS,GAAG,QAAQ,CAAC;CAChD;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,KAAK,GAAG,eAAe,CAwD5D"}
|
|
@@ -39,6 +39,12 @@ export function normalizeEntry(entry) {
|
|
|
39
39
|
const normalizedPath = '/' + normalizedSegments.join('/');
|
|
40
40
|
const queryParams = [];
|
|
41
41
|
for (const qs of entry.request.queryString ?? []) {
|
|
42
|
+
if (qs === null ||
|
|
43
|
+
typeof qs !== 'object' ||
|
|
44
|
+
typeof qs.name !== 'string' ||
|
|
45
|
+
typeof qs.value !== 'string') {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
42
48
|
queryParams.push({
|
|
43
49
|
name: qs.name,
|
|
44
50
|
exampleValue: qs.value,
|
|
@@ -56,9 +62,28 @@ function deriveParamName(segments, currentIndex, counter) {
|
|
|
56
62
|
}
|
|
57
63
|
return counter === 0 ? 'id' : `id${counter + 1}`;
|
|
58
64
|
}
|
|
65
|
+
// Version / routing prefixes that look plural-ish or otherwise sneak past the
|
|
66
|
+
// pattern but are never REST collections. `vN` is matched structurally below.
|
|
67
|
+
const NON_COLLECTION_SEGMENTS = new Set(['api', 'rest', 'graphql', 'rpc', 'status']);
|
|
59
68
|
function isCollectionName(segment) {
|
|
60
|
-
//
|
|
61
|
-
|
|
69
|
+
// Discriminating REST collection heuristic. The previous version returned true
|
|
70
|
+
// for almost any segment (its second clause was `!['api','v1',...].includes(x)`,
|
|
71
|
+
// i.e. true for everything except those four), which over-parameterized paths.
|
|
72
|
+
//
|
|
73
|
+
// A collection segment is a *plural resource name*: lowercase, alphanumeric
|
|
74
|
+
// (allowing `_`/`-`), and ending in `s` but not `ss` (e.g. `users`, `posts`,
|
|
75
|
+
// `items`, `api_keys` — but not `class`, `address`). We additionally exclude
|
|
76
|
+
// version prefixes (`v1`, `v2`, …) and a small set of routing/non-resource
|
|
77
|
+
// tokens, so a generic singular segment like `user`, `profile`, or a version
|
|
78
|
+
// like `v1` is no longer treated as a collection.
|
|
79
|
+
const s = segment.toLowerCase();
|
|
80
|
+
if (NON_COLLECTION_SEGMENTS.has(s))
|
|
81
|
+
return false;
|
|
82
|
+
if (/^v\d+$/.test(s))
|
|
83
|
+
return false; // version prefixes: v1, v2, v10, ...
|
|
84
|
+
// Plural resource: starts with a letter, ends in `s`, but the char before the
|
|
85
|
+
// trailing `s` is not itself `s` (excludes `class`, `address`, `status`).
|
|
86
|
+
return /^[a-z][a-z0-9_-]*[^s]s$/.test(s);
|
|
62
87
|
}
|
|
63
88
|
function singularize(word) {
|
|
64
89
|
if (word.endsWith('ies'))
|