@ractive-ch/tab-mcp 0.1.0-alpha.1 → 0.2.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/index.js CHANGED
@@ -38,6 +38,7 @@ function shortAddress(id) {
38
38
  }
39
39
  // src/config.ts
40
40
  var MIN_MESSAGE_BYTES = 16384;
41
+ var MAX_PAGE_WAIT_MS = 60000;
41
42
  var MAX_APP_NAME = 64;
42
43
  var DEFAULT_CONFIG = {
43
44
  port: 8080,
@@ -59,6 +60,7 @@ var DEFAULT_CONFIG = {
59
60
  heartbeatMs: 25000,
60
61
  helloTimeoutMs: 1e4,
61
62
  bodyTimeoutMs: 1e4,
63
+ pageWaitMs: 1e4,
62
64
  maxMessageBytes: 1048576,
63
65
  accessTokenTtlMs: 3600000,
64
66
  refreshTokenTtlMs: 2592000000,
@@ -142,6 +144,9 @@ function resolveConfig(input = {}) {
142
144
  throw new ConfigError(`${key} must be a positive number`);
143
145
  }
144
146
  }
147
+ if (typeof config.pageWaitMs !== "number" || !Number.isFinite(config.pageWaitMs) || config.pageWaitMs < 0 || config.pageWaitMs > MAX_PAGE_WAIT_MS) {
148
+ throw new ConfigError(`pageWaitMs must be 0-${MAX_PAGE_WAIT_MS} (0-${MAX_PAGE_WAIT_MS / 1000} seconds)`);
149
+ }
145
150
  if (config.maxMessageBytes < MIN_MESSAGE_BYTES) {
146
151
  throw new ConfigError(`maxMessageBytes must be at least ${MIN_MESSAGE_BYTES} (16 KiB)`);
147
152
  }
@@ -265,6 +270,16 @@ function seconds(env, name) {
265
270
  }
266
271
  return Math.round(n * 1000);
267
272
  }
273
+ function secondsOrZero(env, name) {
274
+ const raw = env[name];
275
+ if (raw === undefined || raw === "")
276
+ return;
277
+ const n = Number(raw);
278
+ if (!Number.isFinite(n) || n < 0) {
279
+ throw new ConfigError(`${name} must be 0 or a positive number of seconds`);
280
+ }
281
+ return Math.round(n * 1000);
282
+ }
268
283
  function integer(env, name) {
269
284
  const raw = env[name];
270
285
  if (raw === undefined || raw === "")
@@ -341,6 +356,7 @@ function configFromEnv(env) {
341
356
  set("sessionLifetimeMs", seconds(env, "TAB_MCP_SESSION_LIFETIME_S"));
342
357
  set("heartbeatMs", seconds(env, "TAB_MCP_HEARTBEAT_S"));
343
358
  set("bodyTimeoutMs", seconds(env, "TAB_MCP_BODY_TIMEOUT_S"));
359
+ set("pageWaitMs", secondsOrZero(env, "TAB_MCP_PAGE_WAIT_S"));
344
360
  set("maxMessageBytes", integer(env, "TAB_MCP_MAX_MESSAGE_BYTES"));
345
361
  set("accessTokenTtlMs", seconds(env, "TAB_MCP_ACCESS_TOKEN_TTL_S"));
346
362
  set("refreshTokenTtlMs", seconds(env, "TAB_MCP_REFRESH_TOKEN_TTL_S"));
@@ -708,7 +724,7 @@ function parseTabMessage(text) {
708
724
  }
709
725
  }
710
726
  // package.json
711
- var version = "0.1.0-alpha.1";
727
+ var version = "0.2.0";
712
728
 
713
729
  // src/exchange.ts
714
730
  var RelayErrorCode = {
@@ -866,6 +882,31 @@ class Exchange {
866
882
  this.#options.cancel("client_closed");
867
883
  this.#finish("client_closed", false);
868
884
  }
885
+ tabGone() {
886
+ if (this.#state === "done")
887
+ return;
888
+ const waiting = this.#state === "waiting";
889
+ const reply = this.#options.pageGone?.(waiting);
890
+ if (reply?.retry === true && waiting) {
891
+ clearTimeout(this.#startTimer);
892
+ this.#finish("page_gone_retry");
893
+ return;
894
+ }
895
+ const body = reply?.retry === false ? reply.body : undefined;
896
+ if (body === undefined) {
897
+ this.fail(503, RelayErrorCode.tabGone, "The page closed its connection to the relay.", false);
898
+ return;
899
+ }
900
+ if (waiting) {
901
+ this.#respond(new Response(body, {
902
+ status: 200,
903
+ headers: this.#headers({ "content-type": "application/json" })
904
+ }), 200);
905
+ } else if (this.#atBoundary) {
906
+ this.#write(sseEvent(body));
907
+ }
908
+ this.#finish("page_gone");
909
+ }
869
910
  fail(status, code, message, cancelTab) {
870
911
  if (this.#state === "done")
871
912
  return;
@@ -1004,6 +1045,67 @@ class Exchange {
1004
1045
  }
1005
1046
  }
1006
1047
 
1048
+ // src/notopen.ts
1049
+ var RETRYABLE = new Set(["tools/list"]);
1050
+ function isRetryable(method) {
1051
+ return typeof method === "string" && RETRYABLE.has(method);
1052
+ }
1053
+ function pageName(appName) {
1054
+ return appName ?? "the web page";
1055
+ }
1056
+ function notOpenText(appName) {
1057
+ return `The page is not open, or not in front. Ask the user to open ${pageName(appName)} and keep it in front (on a phone, in split screen), then try again.`;
1058
+ }
1059
+ function goneDuringCallText(appName) {
1060
+ return `The page closed while the tool was running, so it may or may not have finished. Ask the user to open ${pageName(appName)} and keep it in front (on a phone, in split screen), then check the result before trying again.`;
1061
+ }
1062
+ function notOpenAnswer(body, rpcId, appName, phase, relay) {
1063
+ if (rpcId === undefined)
1064
+ return;
1065
+ const id = rpcId;
1066
+ const method = body["method"];
1067
+ if (method === "tools/call") {
1068
+ const text = phase === "gone" ? goneDuringCallText(appName) : notOpenText(appName);
1069
+ return JSON.stringify({
1070
+ jsonrpc: "2.0",
1071
+ id,
1072
+ result: {
1073
+ resultType: "complete",
1074
+ content: [{ type: "text", text }],
1075
+ isError: true
1076
+ }
1077
+ });
1078
+ }
1079
+ if (method === "server/discover") {
1080
+ return JSON.stringify({
1081
+ jsonrpc: "2.0",
1082
+ id,
1083
+ result: {
1084
+ resultType: "complete",
1085
+ supportedVersions: [MCP_PROTOCOL_VERSION],
1086
+ capabilities: { tools: { listChanged: true } },
1087
+ instructions: notOpenText(appName),
1088
+ ttlMs: 0,
1089
+ cacheScope: "private",
1090
+ _meta: {
1091
+ "io.modelcontextprotocol/serverInfo": {
1092
+ name: relay.name,
1093
+ version: relay.version
1094
+ }
1095
+ }
1096
+ }
1097
+ });
1098
+ }
1099
+ return JSON.stringify({
1100
+ jsonrpc: "2.0",
1101
+ id,
1102
+ error: {
1103
+ code: phase === "gone" ? RelayErrorCode.tabGone : RelayErrorCode.pageNotOpen,
1104
+ message: notOpenText(appName)
1105
+ }
1106
+ });
1107
+ }
1108
+
1007
1109
  // src/oauth/server.ts
1008
1110
  import { timingSafeEqual as timingSafeEqual2 } from "crypto";
1009
1111
 
@@ -1461,7 +1563,7 @@ function renderPage(title, body) {
1461
1563
  <body>
1462
1564
  <main>
1463
1565
  ${body}
1464
- <footer>tab-mcp relay. It passes requests between AI tools and web pages and does not store their contents.</footer>
1566
+ <footer>tab-mcp relay. It passes requests between AI apps and web pages and does not store their contents.</footer>
1465
1567
  </main>
1466
1568
  </body>
1467
1569
  </html>`;
@@ -1562,7 +1664,7 @@ var MESSAGES = {
1562
1664
  wrong: "That code is not right. Check the code shown in the page and try again.",
1563
1665
  expired: "That code has expired. The page now shows a new code: enter that one.",
1564
1666
  locked: "Too many wrong codes. The page now shows a new code: enter that one.",
1565
- inactive: "The page is not showing a code right now. In the page, open \u201CConnect your AI tool\u201D to see one, then enter it here.",
1667
+ inactive: "The page is not showing a code right now. In the page, open \u201CConnect your AI app\u201D to see one, then enter it here.",
1566
1668
  malformed: "Enter the 6 digits shown in the page.",
1567
1669
  limited: "Too many attempts for this page. Wait a few minutes and try again. If it keeps happening, someone else may be guessing: use \u201CNew address\u201D in the page."
1568
1670
  };
@@ -1571,7 +1673,7 @@ var codePairing = {
1571
1673
  render() {
1572
1674
  return `<p><label for="code">Code shown in the page</label>
1573
1675
  <input id="code" name="code" type="text" inputmode="numeric" autocomplete="one-time-code" pattern="[0-9 ]{6,7}" maxlength="7" required autofocus aria-describedby="code-help"></p>
1574
- <p id="code-help" class="help">In the page, \u201CConnect your AI tool\u201D shows a 6-digit code. It changes every 2 minutes.</p>`;
1676
+ <p id="code-help" class="help">In the page, \u201CConnect your AI app\u201D shows a 6-digit code. It changes every 2 minutes.</p>`;
1575
1677
  },
1576
1678
  check(target, form, admit) {
1577
1679
  const result = target.codes.check(form.get("code") ?? "", Date.now(), admit);
@@ -1768,7 +1870,7 @@ class AuthorizationServer {
1768
1870
  #errorPage(status, message) {
1769
1871
  return this.#page(status, "This connection cannot be made", `<h1>This connection cannot be made</h1>
1770
1872
  <p role="alert">${escapeHtml(message)}</p>
1771
- <p>Go back to your AI tool and try again. If it keeps happening, the AI tool may not work with this relay.</p>`);
1873
+ <p>Go back to your AI app and try again. If it keeps happening, the AI app may not work with this relay.</p>`);
1772
1874
  }
1773
1875
  #errorUrl(redirectUri, state, error, description) {
1774
1876
  const url = new URL(redirectUri);
@@ -1786,8 +1888,8 @@ class AuthorizationServer {
1786
1888
  const url = this.#errorUrl(redirectUri, state, error, description);
1787
1889
  const host = hostOf(redirectUri);
1788
1890
  return this.#page(400, "This connection cannot be made", `<h1>This connection cannot be made</h1>
1789
- <p role="alert">The AI tool sent a request this relay cannot accept: ${escapeHtml(description)} (${escapeHtml(error)}).</p>
1790
- <p>You can go back to the AI tool at ${escapeHtml(host)} to tell it, or close this page.</p>
1891
+ <p role="alert">The AI app sent a request this relay cannot accept: ${escapeHtml(description)} (${escapeHtml(error)}).</p>
1892
+ <p>You can go back to the AI app at ${escapeHtml(host)} to tell it, or close this page.</p>
1791
1893
  <div class="actions"><a class="button" href="${escapeHtml(url)}">Go back to ${escapeHtml(host)}</a></div>`);
1792
1894
  }
1793
1895
  #redirectError(redirectUri, state, error, description) {
@@ -1937,7 +2039,7 @@ class AuthorizationServer {
1937
2039
  const body = `<h1>Open the page first</h1>
1938
2040
  ${errorHtml}
1939
2041
  <p><strong>${escapeHtml(client.name)}</strong> (${escapeHtml(client.host)}) wants to use the tools of a web page, but that page is not open right now.</p>
1940
- <p>Open the page, click \u201CConnect your AI tool\u201D, then try again here.</p>
2042
+ <p>Open the page, click \u201CConnect your AI app\u201D, then try again here.</p>
1941
2043
  <div class="actions">
1942
2044
  <a class="button primary" href="${escapeHtml(retryUrl)}">Try again</a>
1943
2045
  <a class="button" href="${escapeHtml(cancelUrl)}">Cancel</a>
@@ -1951,7 +2053,7 @@ ${errorHtml}
1951
2053
  return this.#errorPage(403, "This form can only be sent from this page.");
1952
2054
  }
1953
2055
  if (!this.#attemptsPerIp.take(ip)) {
1954
- return this.#errorPage(429, "Too many attempts from your network. Wait a few minutes, then start again from your AI tool.");
2056
+ return this.#errorPage(429, "Too many attempts from your network. Wait a few minutes, then start again from your AI app.");
1955
2057
  }
1956
2058
  const type = (req.headers.get("content-type") ?? "").split(";")[0]?.trim();
1957
2059
  if (type !== "application/x-www-form-urlencoded") {
@@ -2090,7 +2192,7 @@ ${errorHtml}
2090
2192
  const body = `<h1>Open the page first</h1>
2091
2193
  ${errorHtml}
2092
2194
  <p>${clientLabel} wants to use the tools of a web page, but that page is not open right now.</p>
2093
- <p>Open the page, click \u201CConnect your AI tool\u201D, then try again here.</p>
2195
+ <p>Open the page, click \u201CConnect your AI app\u201D, then try again here.</p>
2094
2196
  <form method="post" action="/authorize">
2095
2197
  ${hidden}
2096
2198
  <div class="actions">
@@ -2119,7 +2221,7 @@ ${method.render(session)}
2119
2221
  ${cancel}
2120
2222
  </div>
2121
2223
  </form>
2122
- <p class="help">After you connect, your browser goes back to ${escapeHtml(hostOf(pending.redirectUri))}. Only connect AI tools you started yourself.</p>`;
2224
+ <p class="help">After you connect, your browser goes back to ${escapeHtml(hostOf(pending.redirectUri))}. Only connect AI apps you started yourself.</p>`;
2123
2225
  return this.#page(status, heading, body, targets, extra);
2124
2226
  }
2125
2227
  async token(req, ip) {
@@ -2669,6 +2771,7 @@ class TabSession {
2669
2771
  library: `${library.name}@${library.version}`,
2670
2772
  replaced: replaced !== undefined
2671
2773
  });
2774
+ this.#host.ready?.(this);
2672
2775
  }
2673
2776
  #tick() {
2674
2777
  if (this.#state !== "ready")
@@ -2707,7 +2810,8 @@ class TabSession {
2707
2810
  this.pending.delete(id);
2708
2811
  this.#lastRequestAt = Date.now();
2709
2812
  input.onDone(e);
2710
- }
2813
+ },
2814
+ ...input.pageGone === undefined ? {} : { pageGone: input.pageGone }
2711
2815
  });
2712
2816
  this.pending.set(id, exchange);
2713
2817
  const sent = this.send({
@@ -2746,10 +2850,9 @@ class TabSession {
2746
2850
  clearTimeout(this.#helloTimer);
2747
2851
  clearInterval(this.#heartbeatTimer);
2748
2852
  clearTimeout(this.#lifetimeTimer);
2749
- for (const exchange of [...this.pending.values()]) {
2750
- exchange.fail(503, RelayErrorCode.tabGone, "The page closed its connection to the relay.", false);
2751
- }
2752
2853
  this.#host.released(this);
2854
+ for (const exchange of [...this.pending.values()])
2855
+ exchange.tabGone();
2753
2856
  if (wasReady) {
2754
2857
  this.#host.logger.info("tab.closed", {
2755
2858
  session: this.sessionId,
@@ -3001,6 +3104,8 @@ class SqliteTokenStore {
3001
3104
 
3002
3105
  // src/relay.ts
3003
3106
  var VERSION = version;
3107
+ var RELAY_INFO = { name: "tab-mcp", version };
3108
+ var RETRY = Symbol("retry");
3004
3109
  var CLAUDE_MISMATCHED_HEADER = "2025-11-25";
3005
3110
  var FORWARDED_HEADERS = new Set([
3006
3111
  "content-type",
@@ -3061,6 +3166,9 @@ function startRelay(options, hooks) {
3061
3166
  const sessions = new Map;
3062
3167
  const allSessions = new Set;
3063
3168
  const perIp = new Map;
3169
+ const waiters = new Map;
3170
+ let waitingCount = 0;
3171
+ const appNames = new Map;
3064
3172
  let claudeWorkarounds = 0;
3065
3173
  let publicUrl = config.publicUrl ?? "";
3066
3174
  const corsHeaders = config.cors ? {
@@ -3124,8 +3232,68 @@ function startRelay(options, hooks) {
3124
3232
  sessions.delete(session.addressId);
3125
3233
  }
3126
3234
  releaseIp(session.ip);
3235
+ },
3236
+ ready(session) {
3237
+ const id = session.addressId;
3238
+ if (id === undefined)
3239
+ return;
3240
+ if (session.appName !== undefined) {
3241
+ appNames.delete(id);
3242
+ appNames.set(id, session.appName);
3243
+ if (appNames.size > config.maxSessions * 2) {
3244
+ const oldest = appNames.keys().next().value;
3245
+ if (oldest !== undefined)
3246
+ appNames.delete(oldest);
3247
+ }
3248
+ }
3249
+ const waiting = waiters.get(id);
3250
+ if (waiting !== undefined)
3251
+ for (const wake of [...waiting])
3252
+ wake(session);
3127
3253
  }
3128
3254
  };
3255
+ function readySession(addressId) {
3256
+ const s = sessions.get(addressId);
3257
+ return s?.ready === true ? s : undefined;
3258
+ }
3259
+ function waitForPage(addressId, signal, waitMs) {
3260
+ const now = readySession(addressId);
3261
+ if (now !== undefined)
3262
+ return Promise.resolve(now);
3263
+ if (waitMs === 0 || stopping !== undefined) {
3264
+ return Promise.resolve(undefined);
3265
+ }
3266
+ const set = waiters.get(addressId) ?? new Set;
3267
+ if (set.size >= config.maxConcurrentRequests || waitingCount >= config.maxSessions) {
3268
+ return Promise.resolve(undefined);
3269
+ }
3270
+ return new Promise((resolve) => {
3271
+ let timer;
3272
+ let settled = false;
3273
+ const done = (result) => {
3274
+ if (settled)
3275
+ return;
3276
+ settled = true;
3277
+ clearTimeout(timer);
3278
+ signal.removeEventListener("abort", onAbort);
3279
+ set.delete(wake);
3280
+ waitingCount--;
3281
+ if (set.size === 0 && waiters.get(addressId) === set) {
3282
+ waiters.delete(addressId);
3283
+ }
3284
+ resolve(result);
3285
+ };
3286
+ const wake = (session) => done(session);
3287
+ const onAbort = () => done("aborted");
3288
+ set.add(wake);
3289
+ waiters.set(addressId, set);
3290
+ waitingCount++;
3291
+ timer = setTimeout(() => done(undefined), waitMs);
3292
+ signal.addEventListener("abort", onAbort, { once: true });
3293
+ if (signal.aborted)
3294
+ onAbort();
3295
+ });
3296
+ }
3129
3297
  function releaseIp(ip) {
3130
3298
  const n = (perIp.get(ip) ?? 1) - 1;
3131
3299
  if (n <= 0)
@@ -3329,15 +3497,6 @@ function startRelay(options, hooks) {
3329
3497
  "www-authenticate": wwwAuthenticate(addressId, auth === "invalid")
3330
3498
  });
3331
3499
  }
3332
- const session = sessions.get(addressId);
3333
- if (session === undefined || !session.ready) {
3334
- logDone(503, method, { reason: "page_not_open" });
3335
- return rpcError(503, rpcId, RelayErrorCode.pageNotOpen, 'The page is not open. Open the web page that offers these tools and click "Connect your AI tool".', corsHeaders);
3336
- }
3337
- if (session.pending.size >= session.limits.maxConcurrentRequests) {
3338
- logDone(429, method);
3339
- return rpcError(429, rpcId, RelayErrorCode.overloaded, "Too many requests at once for this page.", corsHeaders);
3340
- }
3341
3500
  const headers = {};
3342
3501
  for (const [k, v] of req.headers) {
3343
3502
  const key = k.toLowerCase();
@@ -3352,33 +3511,89 @@ function startRelay(options, hooks) {
3352
3511
  return "";
3353
3512
  }
3354
3513
  })();
3355
- if (req.signal.aborted) {
3514
+ const isListen = body.value["method"] === "subscriptions/listen";
3515
+ const waitMs = body.value["method"] === "server/discover" || Buffer.byteLength(body.text) > MIN_MESSAGE_BYTES ? 0 : config.pageWaitMs;
3516
+ const closed = () => {
3356
3517
  logDone(499, method, { outcome: "client_closed" });
3357
3518
  return new Response(null, { status: 499 });
3519
+ };
3520
+ if (readySession(addressId) === undefined && waitMs > 0) {
3521
+ server.timeout(req, 0);
3358
3522
  }
3359
- server.timeout(req, 0);
3360
- const exchange = session.forward({
3361
- method: "POST",
3362
- headers,
3363
- body: body.text,
3364
- rpcId,
3365
- isListen: body.value["method"] === "subscriptions/listen",
3366
- grant: {
3367
- grantId: auth.grantId,
3368
- clientId: auth.clientId,
3369
- scopes: auth.scopes
3370
- },
3371
- extraHeaders: corsHeaders,
3372
- onDone: (e) => logDone(e.status, method, {
3373
- session: session.sessionId,
3374
- client: clientHostName,
3375
- outcome: e.outcome
3376
- })
3377
- });
3378
- req.signal.addEventListener("abort", () => exchange.clientClosed(), {
3379
- once: true
3523
+ let retried = false;
3524
+ for (;; ) {
3525
+ const found = await waitForPage(addressId, req.signal, waitMs);
3526
+ if (found === "aborted" || req.signal.aborted)
3527
+ return closed();
3528
+ if (found === undefined) {
3529
+ return pageNotOpen(body.value, rpcId, addressId, logDone, method);
3530
+ }
3531
+ const session = found;
3532
+ if (Buffer.byteLength(body.text) > session.limits.maxMessageBytes) {
3533
+ logDone(413, method);
3534
+ return rpcError(413, rpcId, -32600, "Request body too large.", corsHeaders);
3535
+ }
3536
+ if (session.pending.size >= session.limits.maxConcurrentRequests) {
3537
+ logDone(429, method);
3538
+ return rpcError(429, rpcId, RelayErrorCode.overloaded, "Too many requests at once for this page.", corsHeaders);
3539
+ }
3540
+ server.timeout(req, 0);
3541
+ let retry = () => {};
3542
+ const retrying = new Promise((resolve) => {
3543
+ retry = () => resolve(RETRY);
3544
+ });
3545
+ const mayRetry = !retried && waitMs > 0 && isRetryable(body.value["method"]);
3546
+ const exchange = session.forward({
3547
+ method: "POST",
3548
+ headers,
3549
+ body: body.text,
3550
+ rpcId,
3551
+ isListen,
3552
+ grant: {
3553
+ grantId: auth.grantId,
3554
+ clientId: auth.clientId,
3555
+ scopes: auth.scopes
3556
+ },
3557
+ extraHeaders: corsHeaders,
3558
+ onDone: (e) => {
3559
+ if (e.outcome === "page_gone_retry")
3560
+ return;
3561
+ logDone(e.status, method, {
3562
+ session: session.sessionId,
3563
+ client: clientHostName,
3564
+ outcome: e.outcome,
3565
+ ...e.outcome === "page_gone" ? { reason: "page_not_open" } : {}
3566
+ });
3567
+ },
3568
+ pageGone: (waiting) => {
3569
+ if (waiting && mayRetry && !req.signal.aborted) {
3570
+ retry();
3571
+ return { retry: true };
3572
+ }
3573
+ const answer = notOpenAnswer(body.value, rpcId, session.appName, "gone", RELAY_INFO);
3574
+ return answer === undefined ? { retry: false } : { retry: false, body: answer };
3575
+ }
3576
+ });
3577
+ const onAbort = () => exchange.clientClosed();
3578
+ req.signal.addEventListener("abort", onAbort, { once: true });
3579
+ const result = await Promise.race([exchange.response, retrying]);
3580
+ if (result !== RETRY)
3581
+ return result;
3582
+ req.signal.removeEventListener("abort", onAbort);
3583
+ retried = true;
3584
+ }
3585
+ }
3586
+ function pageNotOpen(value, rpcId, addressId, logDone, method) {
3587
+ const answer = notOpenAnswer(value, rpcId, appNames.get(addressId), "not_open", RELAY_INFO);
3588
+ if (answer === undefined) {
3589
+ logDone(503, method, { reason: "page_not_open" });
3590
+ return rpcError(503, rpcId, RelayErrorCode.pageNotOpen, "The page is not open.", corsHeaders);
3591
+ }
3592
+ logDone(200, method, { reason: "page_not_open" });
3593
+ return new Response(answer, {
3594
+ status: 200,
3595
+ headers: { ...corsHeaders, "content-type": "application/json" }
3380
3596
  });
3381
- return exchange.response;
3382
3597
  }
3383
3598
  function preflight(req) {
3384
3599
  if (!config.cors)
@@ -3484,6 +3699,7 @@ function startRelay(options, hooks) {
3484
3699
  }
3485
3700
  }
3486
3701
  });
3702
+ let stopping;
3487
3703
  const port = server.port ?? config.port;
3488
3704
  if (publicUrl === "")
3489
3705
  publicUrl = `http://localhost:${port}`;
@@ -3514,7 +3730,6 @@ function startRelay(options, hooks) {
3514
3730
  store: ownsStore ? config.store : "custom",
3515
3731
  origins: siteCount
3516
3732
  });
3517
- let stopping;
3518
3733
  return {
3519
3734
  url: publicUrl,
3520
3735
  port,
@@ -3526,6 +3741,10 @@ function startRelay(options, hooks) {
3526
3741
  stop() {
3527
3742
  stopping ??= (async () => {
3528
3743
  clearInterval(sweep);
3744
+ for (const set of [...waiters.values()]) {
3745
+ for (const wake of [...set])
3746
+ wake(undefined);
3747
+ }
3529
3748
  for (const session of [...allSessions]) {
3530
3749
  session.end(CloseCode.shuttingDown, "relay shutting down", "shutdown");
3531
3750
  }
@@ -3554,5 +3773,5 @@ export {
3554
3773
  resolveConfig
3555
3774
  };
3556
3775
 
3557
- //# debugId=5AAD754DD6F4F7B264756E2164756E21
3776
+ //# debugId=50BF7EDE9EC7441F64756E2164756E21
3558
3777
  //# sourceMappingURL=index.js.map