@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/README.md +131 -0
- package/config.d.ts +7 -0
- package/exchange.d.ts +23 -0
- package/index.d.ts +1 -1
- package/index.js +268 -49
- package/index.js.map +11 -10
- package/main.js +437 -215
- package/main.js.map +13 -12
- package/notopen.d.ts +24 -0
- package/package.json +1 -1
- package/session.d.ts +5 -1
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1790
|
-
<p>You can go back to the AI
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
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=
|
|
3776
|
+
//# debugId=50BF7EDE9EC7441F64756E2164756E21
|
|
3558
3777
|
//# sourceMappingURL=index.js.map
|