@particle-academy/agent-integrations 0.44.0 → 0.46.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 +42 -3
- package/dist/bridges/artboard.d.cts +3 -3
- package/dist/bridges/artboard.d.ts +3 -3
- package/dist/bridges/catalog.d.cts +3 -3
- package/dist/bridges/catalog.d.ts +3 -3
- package/dist/bridges/charts.d.cts +3 -3
- package/dist/bridges/charts.d.ts +3 -3
- package/dist/bridges/cms.d.cts +3 -3
- package/dist/bridges/cms.d.ts +3 -3
- package/dist/bridges/code.d.cts +3 -3
- package/dist/bridges/code.d.ts +3 -3
- package/dist/bridges/doc.d.cts +3 -3
- package/dist/bridges/doc.d.ts +3 -3
- package/dist/bridges/features.d.cts +3 -3
- package/dist/bridges/features.d.ts +3 -3
- package/dist/bridges/files.d.cts +3 -3
- package/dist/bridges/files.d.ts +3 -3
- package/dist/bridges/flow.d.cts +3 -3
- package/dist/bridges/flow.d.ts +3 -3
- package/dist/bridges/forms.d.cts +3 -3
- package/dist/bridges/forms.d.ts +3 -3
- package/dist/bridges/git.d.cts +3 -3
- package/dist/bridges/git.d.ts +3 -3
- package/dist/bridges/grid.d.cts +3 -3
- package/dist/bridges/grid.d.ts +3 -3
- package/dist/bridges/map.d.cts +3 -3
- package/dist/bridges/map.d.ts +3 -3
- package/dist/bridges/passkeys.d.cts +3 -3
- package/dist/bridges/passkeys.d.ts +3 -3
- package/dist/bridges/scene.d.cts +3 -3
- package/dist/bridges/scene.d.ts +3 -3
- package/dist/bridges/screens.d.cts +3 -3
- package/dist/bridges/screens.d.ts +3 -3
- package/dist/bridges/sheets.d.cts +3 -3
- package/dist/bridges/sheets.d.ts +3 -3
- package/dist/bridges/slides.d.cts +3 -3
- package/dist/bridges/slides.d.ts +3 -3
- package/dist/bridges/terminal.d.cts +3 -3
- package/dist/bridges/terminal.d.ts +3 -3
- package/dist/bridges/tui.d.cts +2 -2
- package/dist/bridges/tui.d.ts +2 -2
- package/dist/bridges/whiteboard.d.cts +3 -3
- package/dist/bridges/whiteboard.d.ts +3 -3
- package/dist/bridges-artboard.cjs.map +1 -1
- package/dist/bridges-artboard.js +2 -2
- package/dist/bridges-catalog.cjs.map +1 -1
- package/dist/bridges-catalog.js +3 -3
- package/dist/bridges-charts.cjs.map +1 -1
- package/dist/bridges-charts.js +3 -3
- package/dist/bridges-cms.cjs.map +1 -1
- package/dist/bridges-cms.js +4 -4
- package/dist/bridges-code.cjs.map +1 -1
- package/dist/bridges-code.js +3 -3
- package/dist/bridges-doc.cjs.map +1 -1
- package/dist/bridges-doc.js +3 -3
- package/dist/bridges-features.cjs.map +1 -1
- package/dist/bridges-features.js +3 -3
- package/dist/bridges-files.cjs.map +1 -1
- package/dist/bridges-files.js +3 -3
- package/dist/bridges-flow.cjs.map +1 -1
- package/dist/bridges-flow.js +2 -2
- package/dist/bridges-forms.cjs.map +1 -1
- package/dist/bridges-forms.js +3 -3
- package/dist/bridges-git.cjs.map +1 -1
- package/dist/bridges-git.js +3 -3
- package/dist/bridges-grid.cjs.map +1 -1
- package/dist/bridges-grid.js +3 -3
- package/dist/bridges-map.cjs.map +1 -1
- package/dist/bridges-map.js +3 -3
- package/dist/bridges-passkeys.cjs.map +1 -1
- package/dist/bridges-passkeys.js +3 -3
- package/dist/bridges-scene.cjs.map +1 -1
- package/dist/bridges-scene.js +3 -3
- package/dist/bridges-screens.cjs.map +1 -1
- package/dist/bridges-screens.js +3 -3
- package/dist/bridges-sheets.cjs.map +1 -1
- package/dist/bridges-sheets.js +3 -3
- package/dist/bridges-slides.cjs.map +1 -1
- package/dist/bridges-slides.js +3 -3
- package/dist/bridges-terminal.cjs.map +1 -1
- package/dist/bridges-terminal.js +3 -3
- package/dist/bridges-tui.cjs.map +1 -1
- package/dist/bridges-tui.js +2 -2
- package/dist/bridges-whiteboard.cjs.map +1 -1
- package/dist/bridges-whiteboard.js +3 -3
- package/dist/{chunk-5W6YOXLT.js → chunk-36MKPTVD.js} +4 -4
- package/dist/{chunk-5W6YOXLT.js.map → chunk-36MKPTVD.js.map} +1 -1
- package/dist/{chunk-SXVRIIXW.js → chunk-3HVYUMQF.js} +4 -4
- package/dist/{chunk-SXVRIIXW.js.map → chunk-3HVYUMQF.js.map} +1 -1
- package/dist/{chunk-MZ74LLDS.js → chunk-3UUF5KKZ.js} +4 -4
- package/dist/{chunk-MZ74LLDS.js.map → chunk-3UUF5KKZ.js.map} +1 -1
- package/dist/{chunk-WEXMZMJH.js → chunk-3WVXAB3D.js} +3 -3
- package/dist/{chunk-WEXMZMJH.js.map → chunk-3WVXAB3D.js.map} +1 -1
- package/dist/{chunk-OZBUPO4K.js → chunk-5W34X4EY.js} +263 -58
- package/dist/chunk-5W34X4EY.js.map +1 -0
- package/dist/{chunk-Z4L2GOAK.js → chunk-5ZWNCHZS.js} +4 -4
- package/dist/{chunk-Z4L2GOAK.js.map → chunk-5ZWNCHZS.js.map} +1 -1
- package/dist/{chunk-TEB5U6Y5.js → chunk-7NVATRLE.js} +4 -4
- package/dist/{chunk-TEB5U6Y5.js.map → chunk-7NVATRLE.js.map} +1 -1
- package/dist/{chunk-PUMQDV67.js → chunk-AXWMWYH5.js} +4 -4
- package/dist/{chunk-PUMQDV67.js.map → chunk-AXWMWYH5.js.map} +1 -1
- package/dist/{chunk-NKAH67JK.js → chunk-CS3CHGYC.js} +4 -4
- package/dist/{chunk-NKAH67JK.js.map → chunk-CS3CHGYC.js.map} +1 -1
- package/dist/{chunk-QJYRWEXD.js → chunk-CZV3WUW5.js} +35 -8
- package/dist/chunk-CZV3WUW5.js.map +1 -0
- package/dist/{chunk-PCFRBVKS.js → chunk-FCXSR7I5.js} +4 -4
- package/dist/{chunk-PCFRBVKS.js.map → chunk-FCXSR7I5.js.map} +1 -1
- package/dist/{chunk-NIFPRC7P.js → chunk-GVTBMJU6.js} +4 -4
- package/dist/{chunk-NIFPRC7P.js.map → chunk-GVTBMJU6.js.map} +1 -1
- package/dist/{chunk-E2I3YVKC.js → chunk-JLHVQWOV.js} +3 -3
- package/dist/{chunk-E2I3YVKC.js.map → chunk-JLHVQWOV.js.map} +1 -1
- package/dist/{chunk-2LSU3NFO.js → chunk-LQB42OSA.js} +4 -4
- package/dist/{chunk-2LSU3NFO.js.map → chunk-LQB42OSA.js.map} +1 -1
- package/dist/{chunk-VJD3Y7E5.js → chunk-MIDZWKIF.js} +4 -4
- package/dist/{chunk-VJD3Y7E5.js.map → chunk-MIDZWKIF.js.map} +1 -1
- package/dist/{chunk-LOD77NCD.js → chunk-PWYEW6NL.js} +4 -4
- package/dist/{chunk-LOD77NCD.js.map → chunk-PWYEW6NL.js.map} +1 -1
- package/dist/{chunk-HVDHT7DM.js → chunk-R4XXOGQH.js} +3 -3
- package/dist/{chunk-HVDHT7DM.js.map → chunk-R4XXOGQH.js.map} +1 -1
- package/dist/{chunk-7LLMILTM.js → chunk-RVMM4BHK.js} +4 -4
- package/dist/{chunk-7LLMILTM.js.map → chunk-RVMM4BHK.js.map} +1 -1
- package/dist/{chunk-NIXOY473.js → chunk-SWWJUOCY.js} +3 -3
- package/dist/{chunk-NIXOY473.js.map → chunk-SWWJUOCY.js.map} +1 -1
- package/dist/{chunk-EIBV76IB.js → chunk-VFWFT3OK.js} +4 -4
- package/dist/{chunk-EIBV76IB.js.map → chunk-VFWFT3OK.js.map} +1 -1
- package/dist/{chunk-2Q4EGZHJ.js → chunk-VMRUW4B4.js} +4 -4
- package/dist/{chunk-2Q4EGZHJ.js.map → chunk-VMRUW4B4.js.map} +1 -1
- package/dist/{chunk-GT63LSPJ.js → chunk-VOCUGTZB.js} +4 -4
- package/dist/{chunk-GT63LSPJ.js.map → chunk-VOCUGTZB.js.map} +1 -1
- package/dist/{chunk-N5VOPV64.js → chunk-Z5NL5D4J.js} +4 -4
- package/dist/{chunk-N5VOPV64.js.map → chunk-Z5NL5D4J.js.map} +1 -1
- package/dist/components-shared-whiteboard.cjs +33 -6
- package/dist/components-shared-whiteboard.cjs.map +1 -1
- package/dist/components-shared-whiteboard.js +3 -3
- package/dist/index.cjs +33 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -7
- package/dist/index.d.ts +7 -7
- package/dist/index.js +27 -27
- package/dist/mcp/index.d.cts +5 -5
- package/dist/mcp/index.d.ts +5 -5
- package/dist/mcp/stdio.d.cts +94 -0
- package/dist/mcp/stdio.d.ts +94 -0
- package/dist/mcp-stdio.cjs +120 -0
- package/dist/mcp-stdio.cjs.map +1 -0
- package/dist/mcp-stdio.js +117 -0
- package/dist/mcp-stdio.js.map +1 -0
- package/dist/mcp.cjs +33 -6
- package/dist/mcp.cjs.map +1 -1
- package/dist/mcp.js +1 -1
- package/dist/presence/index.d.cts +1 -1
- package/dist/presence/index.d.ts +1 -1
- package/dist/relay-server/index.d.cts +110 -26
- package/dist/relay-server/index.d.ts +110 -26
- package/dist/relay-server-cli.cjs +374 -89
- package/dist/relay-server-cli.cjs.map +1 -1
- package/dist/relay-server-cli.js +120 -38
- package/dist/relay-server-cli.js.map +1 -1
- package/dist/relay-server.cjs +255 -51
- package/dist/relay-server.cjs.map +1 -1
- package/dist/relay-server.js +1 -1
- package/dist/{server-BJNF7pQe.d.cts → server-BIcA3ZtK.d.cts} +32 -3
- package/dist/{server-Dt97UxuD.d.ts → server-DnUyeavY.d.ts} +32 -3
- package/dist/sharing/index.d.cts +3 -3
- package/dist/sharing/index.d.ts +3 -3
- package/dist/sheets-adapter.d.cts +3 -3
- package/dist/sheets-adapter.d.ts +3 -3
- package/dist/{tool-host-BpEBqf5u.d.cts → tool-host-LVBgCN5m.d.cts} +1 -1
- package/dist/{tool-host-xY5F-eqG.d.ts → tool-host-Wo0pkY83.d.ts} +1 -1
- package/dist/{types-DSwsNTBG.d.ts → types-BxNRKSVd.d.ts} +1 -1
- package/dist/{types-C58ZXs4K.d.cts → types-C2DtnHIn.d.cts} +25 -1
- package/dist/{types-C58ZXs4K.d.ts → types-C2DtnHIn.d.ts} +25 -1
- package/dist/{types-C4NaIW6B.d.cts → types-EcElKOlk.d.cts} +1 -1
- package/dist/undo/index.d.cts +2 -2
- package/dist/undo/index.d.ts +2 -2
- package/dist/undo.cjs.map +1 -1
- package/dist/undo.js +3 -3
- package/docs/relay-protocol.md +41 -5
- package/docs/relay-server.md +102 -15
- package/package.json +19 -2
- package/dist/chunk-OZBUPO4K.js.map +0 -1
- package/dist/chunk-QJYRWEXD.js.map +0 -1
|
@@ -5,6 +5,136 @@ var http = require('http');
|
|
|
5
5
|
var url = require('url');
|
|
6
6
|
var crypto = require('crypto');
|
|
7
7
|
|
|
8
|
+
// src/relay-server/cors.ts
|
|
9
|
+
function parseCorsOrigins(value) {
|
|
10
|
+
const entries = (typeof value === "string" ? value.split(",") : [...value]).map((entry) => entry.trim()).filter((entry) => entry.length > 0);
|
|
11
|
+
if (entries.length === 0) {
|
|
12
|
+
throw new Error("CORS origin list is empty: pass `*` or one or more origins, e.g. https://example.com");
|
|
13
|
+
}
|
|
14
|
+
if (entries.includes("*")) {
|
|
15
|
+
if (entries.length === 1) return { kind: "any" };
|
|
16
|
+
throw new Error(
|
|
17
|
+
"CORS `*` cannot be combined with named origins \u2014 `*` already allows every origin, so the list would mean nothing. Pass either `*` or the origins."
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
const origins = [];
|
|
21
|
+
for (const entry of entries) {
|
|
22
|
+
const origin = normalizeOrigin(entry);
|
|
23
|
+
if (!origins.includes(origin)) origins.push(origin);
|
|
24
|
+
}
|
|
25
|
+
return { kind: "list", origins };
|
|
26
|
+
}
|
|
27
|
+
function normalizeOrigin(raw) {
|
|
28
|
+
if (raw.toLowerCase() === "null") {
|
|
29
|
+
throw new Error("CORS origin `null` is refused: it is what sandboxed iframes and file:// pages send.");
|
|
30
|
+
}
|
|
31
|
+
let url;
|
|
32
|
+
try {
|
|
33
|
+
url = new URL(raw);
|
|
34
|
+
} catch {
|
|
35
|
+
throw new Error(`CORS origin "${raw}" is not an origin \u2014 expected scheme://host[:port], e.g. https://example.com`);
|
|
36
|
+
}
|
|
37
|
+
if (url.pathname !== "/" && url.pathname !== "" || url.search || url.hash || url.username || url.password) {
|
|
38
|
+
throw new Error(`CORS origin "${raw}" has a path, query or credentials \u2014 an origin is only scheme://host[:port]`);
|
|
39
|
+
}
|
|
40
|
+
if (url.origin !== "null") return url.origin;
|
|
41
|
+
if (url.host) return `${url.protocol}//${url.host}`;
|
|
42
|
+
throw new Error(`CORS origin "${raw}" is not an origin \u2014 expected scheme://host[:port], e.g. https://example.com`);
|
|
43
|
+
}
|
|
44
|
+
function allowOriginFor(policy, requestOrigin) {
|
|
45
|
+
if (policy.kind === "any") return "*";
|
|
46
|
+
return requestOrigin !== void 0 && policy.origins.includes(requestOrigin) ? requestOrigin : null;
|
|
47
|
+
}
|
|
48
|
+
function describeCorsPolicy(policy) {
|
|
49
|
+
return policy.kind === "any" ? "*" : policy.origins.join(",");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// src/relay-server/args.ts
|
|
53
|
+
var SETTINGS = {
|
|
54
|
+
port: { flag: "--port", env: "PORT", fallback: "8787" },
|
|
55
|
+
// Loopback by default: this relay fronts terminal_run (RCE) and register is
|
|
56
|
+
// unauthenticated, so it must not listen on all interfaces unless the
|
|
57
|
+
// operator explicitly opts in with --host 0.0.0.0.
|
|
58
|
+
host: { flag: "--host", env: "HOST", fallback: "127.0.0.1" },
|
|
59
|
+
prefix: { flag: "--prefix", env: "PREFIX", fallback: "" },
|
|
60
|
+
ttlMs: { flag: "--ttl-ms", env: "TTL_MS", fallback: String(4 * 60 * 60 * 1e3) },
|
|
61
|
+
cors: { flag: "--cors", env: "CORS_ALLOW_ORIGIN", fallback: "*" }
|
|
62
|
+
};
|
|
63
|
+
function parseRelayArgs(argv, env) {
|
|
64
|
+
const flags = {};
|
|
65
|
+
let help = false;
|
|
66
|
+
for (let i = 0; i < argv.length; i++) {
|
|
67
|
+
const arg = argv[i];
|
|
68
|
+
if (arg === "-h" || arg === "--help") {
|
|
69
|
+
help = true;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const key = Object.keys(SETTINGS).find((k) => SETTINGS[k].flag === arg);
|
|
73
|
+
if (!key) return { ok: false, error: `unknown flag: ${arg}` };
|
|
74
|
+
const value = argv[i + 1];
|
|
75
|
+
if (value === void 0 || value.startsWith("--")) return { ok: false, error: `${arg} needs a value` };
|
|
76
|
+
i++;
|
|
77
|
+
flags[key] = key === "cors" && flags.cors !== void 0 ? `${flags.cors},${value}` : value;
|
|
78
|
+
}
|
|
79
|
+
const warnings = [];
|
|
80
|
+
const raw = {};
|
|
81
|
+
const sources = {};
|
|
82
|
+
for (const key of Object.keys(SETTINGS)) {
|
|
83
|
+
const { flag, env: envName, fallback } = SETTINGS[key];
|
|
84
|
+
const fromEnv = env[envName] === "" ? void 0 : env[envName];
|
|
85
|
+
const fromFlag = flags[key];
|
|
86
|
+
if (fromFlag !== void 0) {
|
|
87
|
+
raw[key] = fromFlag;
|
|
88
|
+
sources[key] = "flag";
|
|
89
|
+
if (fromEnv !== void 0 && fromEnv !== fromFlag) {
|
|
90
|
+
warnings.push(`${envName}=${fromEnv} is ignored: ${flag} ${fromFlag} was passed, and a flag wins over its env var.`);
|
|
91
|
+
}
|
|
92
|
+
} else if (fromEnv !== void 0) {
|
|
93
|
+
raw[key] = fromEnv;
|
|
94
|
+
sources[key] = "env";
|
|
95
|
+
} else {
|
|
96
|
+
raw[key] = fallback;
|
|
97
|
+
sources[key] = "default";
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const named = (key) => sources[key] === "env" ? SETTINGS[key].env : SETTINGS[key].flag;
|
|
101
|
+
const port = Number(raw.port);
|
|
102
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
|
|
103
|
+
return { ok: false, error: `invalid ${named("port")}: ${raw.port}` };
|
|
104
|
+
}
|
|
105
|
+
const ttlMs = Number(raw.ttlMs);
|
|
106
|
+
if (!Number.isFinite(ttlMs) || ttlMs <= 0) {
|
|
107
|
+
return { ok: false, error: `invalid ${named("ttlMs")}: ${raw.ttlMs}` };
|
|
108
|
+
}
|
|
109
|
+
let cors;
|
|
110
|
+
try {
|
|
111
|
+
cors = parseCorsOrigins(raw.cors);
|
|
112
|
+
} catch (e) {
|
|
113
|
+
return { ok: false, error: `invalid ${named("cors")}: ${e instanceof Error ? e.message : String(e)}` };
|
|
114
|
+
}
|
|
115
|
+
if (sources.cors === "flag" && env.CORS_ALLOW_ORIGIN) {
|
|
116
|
+
try {
|
|
117
|
+
if (describeCorsPolicy(parseCorsOrigins(env.CORS_ALLOW_ORIGIN)) === describeCorsPolicy(cors)) {
|
|
118
|
+
const i = warnings.findIndex((w) => w.startsWith("CORS_ALLOW_ORIGIN="));
|
|
119
|
+
if (i !== -1) warnings.splice(i, 1);
|
|
120
|
+
}
|
|
121
|
+
} catch {
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
ok: true,
|
|
126
|
+
config: {
|
|
127
|
+
port,
|
|
128
|
+
host: raw.host,
|
|
129
|
+
prefix: raw.prefix,
|
|
130
|
+
ttlMs,
|
|
131
|
+
cors,
|
|
132
|
+
sources,
|
|
133
|
+
warnings,
|
|
134
|
+
help
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
8
138
|
var MemoryStore = class {
|
|
9
139
|
constructor() {
|
|
10
140
|
this.sessions = /* @__PURE__ */ new Map();
|
|
@@ -25,6 +155,7 @@ var MemoryStore = class {
|
|
|
25
155
|
}
|
|
26
156
|
};
|
|
27
157
|
var SESSION_ID_PATTERN = /^[A-Za-z0-9_-]{4,64}$/;
|
|
158
|
+
var SUBSCRIBER_ID_PATTERN = /^[a-f0-9]{16}$/;
|
|
28
159
|
var RelayBroker = class {
|
|
29
160
|
constructor(opts = {}) {
|
|
30
161
|
/** Per-session, per-direction subscriber list. */
|
|
@@ -32,6 +163,7 @@ var RelayBroker = class {
|
|
|
32
163
|
/** Per-session map of JSON-RPC request id -> the client that asked. */
|
|
33
164
|
this.callers = /* @__PURE__ */ new Map();
|
|
34
165
|
this.ttlMs = opts.ttlMs ?? 4 * 60 * 60 * 1e3;
|
|
166
|
+
this.pollIdleMs = opts.pollIdleMs ?? 6e4;
|
|
35
167
|
this.store = opts.store ?? new MemoryStore();
|
|
36
168
|
const tick = opts.reapIntervalMs ?? 6e4;
|
|
37
169
|
if (tick > 0) {
|
|
@@ -43,6 +175,7 @@ var RelayBroker = class {
|
|
|
43
175
|
}
|
|
44
176
|
dispose() {
|
|
45
177
|
if (this.reaper) clearInterval(this.reaper);
|
|
178
|
+
for (const id of [...this.subs.keys()]) this.endSubscribers(id);
|
|
46
179
|
this.subs.clear();
|
|
47
180
|
this.callers.clear();
|
|
48
181
|
}
|
|
@@ -67,6 +200,7 @@ var RelayBroker = class {
|
|
|
67
200
|
unregister(id, token) {
|
|
68
201
|
if (!this.validate(id, token)) return false;
|
|
69
202
|
this.store.deleteSession(id);
|
|
203
|
+
this.endSubscribers(id);
|
|
70
204
|
this.subs.delete(id);
|
|
71
205
|
this.callers.delete(id);
|
|
72
206
|
return true;
|
|
@@ -116,6 +250,7 @@ var RelayBroker = class {
|
|
|
116
250
|
/** Drop a session — the page closed, or a test needs it gone. */
|
|
117
251
|
dropSession(id) {
|
|
118
252
|
this.store.deleteSession(id);
|
|
253
|
+
this.endSubscribers(id);
|
|
119
254
|
this.subs.delete(id);
|
|
120
255
|
this.callers.delete(id);
|
|
121
256
|
}
|
|
@@ -146,7 +281,8 @@ var RelayBroker = class {
|
|
|
146
281
|
* the caller (an HTTP handler) pumps as SSE.
|
|
147
282
|
*/
|
|
148
283
|
subscribe(id, token, direction, opts = {}) {
|
|
149
|
-
|
|
284
|
+
const auth = this.check(id, token);
|
|
285
|
+
if (!auth.ok) return auth;
|
|
150
286
|
const subscriberId = crypto.randomBytes(8).toString("hex");
|
|
151
287
|
const subscriber = {
|
|
152
288
|
id: subscriberId,
|
|
@@ -156,31 +292,7 @@ var RelayBroker = class {
|
|
|
156
292
|
client: opts.client
|
|
157
293
|
};
|
|
158
294
|
this.getDirSubs(id, direction).set(subscriberId, subscriber);
|
|
159
|
-
if (direction === "outbound")
|
|
160
|
-
this.fanOut(
|
|
161
|
-
id,
|
|
162
|
-
"inbound",
|
|
163
|
-
JSON.stringify({
|
|
164
|
-
jsonrpc: "2.0",
|
|
165
|
-
method: "notifications/peer_joined",
|
|
166
|
-
params: { subscriberId, ts: Date.now() }
|
|
167
|
-
})
|
|
168
|
-
);
|
|
169
|
-
}
|
|
170
|
-
const unsubscribe = () => {
|
|
171
|
-
this.getDirSubs(id, direction).delete(subscriberId);
|
|
172
|
-
if (direction === "outbound") {
|
|
173
|
-
this.fanOut(
|
|
174
|
-
id,
|
|
175
|
-
"inbound",
|
|
176
|
-
JSON.stringify({
|
|
177
|
-
jsonrpc: "2.0",
|
|
178
|
-
method: "notifications/peer_left",
|
|
179
|
-
params: { subscriberId, ts: Date.now() }
|
|
180
|
-
})
|
|
181
|
-
);
|
|
182
|
-
}
|
|
183
|
-
};
|
|
295
|
+
if (direction === "outbound") this.announcePeer(id, "notifications/peer_joined", subscriberId);
|
|
184
296
|
const frames = async function* () {
|
|
185
297
|
while (true) {
|
|
186
298
|
if (this.queue.length > 0) {
|
|
@@ -188,6 +300,7 @@ var RelayBroker = class {
|
|
|
188
300
|
if (next2 !== void 0) yield next2;
|
|
189
301
|
continue;
|
|
190
302
|
}
|
|
303
|
+
if (this.ended) return;
|
|
191
304
|
const next = await new Promise((resolve) => {
|
|
192
305
|
this.resolveNext = resolve;
|
|
193
306
|
});
|
|
@@ -201,8 +314,88 @@ var RelayBroker = class {
|
|
|
201
314
|
subscriberId,
|
|
202
315
|
frames: frames(),
|
|
203
316
|
unsubscribe: () => {
|
|
204
|
-
|
|
205
|
-
|
|
317
|
+
this.endSubscriber(subscriber);
|
|
318
|
+
this.removeSubscriber(id, subscriber);
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Attach a LONG-POLL subscriber, or re-attach the one a previous poll created.
|
|
324
|
+
*
|
|
325
|
+
* The CDN-safe receive leg: a series of short requests instead of one open
|
|
326
|
+
* stream, because Cloudflare's HTTP/3 edge resets long-lived SSE. Same
|
|
327
|
+
* contract as the PHP relay in px-ui-sandbox and the one
|
|
328
|
+
* `@particle-academy/fancy-cf-relay` and `mcp-relay-client` speak:
|
|
329
|
+
*
|
|
330
|
+
* - The first poll gets a fresh 16-hex subscriber id; the client echoes it back
|
|
331
|
+
* as `subscriber` on every later poll and reads the same queue.
|
|
332
|
+
* - An id in that shape that this session does not know (pruned, say) is
|
|
333
|
+
* adopted as a NEW subscriber under that id, as the PHP relay does, so a
|
|
334
|
+
* client that went quiet and came back keeps its identity.
|
|
335
|
+
* - An id belonging to a STREAMING subscriber is never adopted: one queue read
|
|
336
|
+
* by two consumers splits its frames between them.
|
|
337
|
+
*
|
|
338
|
+
* Frames queue on the subscriber between polls, so nothing is lost in the gap
|
|
339
|
+
* between one request ending and the next arriving. A poller that stops is
|
|
340
|
+
* pruned after `pollIdleMs`.
|
|
341
|
+
*/
|
|
342
|
+
poll(id, token, direction, opts = {}) {
|
|
343
|
+
const auth = this.check(id, token);
|
|
344
|
+
if (!auth.ok) return auth;
|
|
345
|
+
const dir = this.getDirSubs(id, direction);
|
|
346
|
+
const requested = opts.subscriber && SUBSCRIBER_ID_PATTERN.test(opts.subscriber) ? opts.subscriber : void 0;
|
|
347
|
+
const existing = requested ? dir.get(requested) : void 0;
|
|
348
|
+
let subscriber;
|
|
349
|
+
let created;
|
|
350
|
+
if (existing?.poll) {
|
|
351
|
+
subscriber = existing;
|
|
352
|
+
created = false;
|
|
353
|
+
} else {
|
|
354
|
+
const subscriberId = requested && !existing ? requested : crypto.randomBytes(8).toString("hex");
|
|
355
|
+
subscriber = {
|
|
356
|
+
id: subscriberId,
|
|
357
|
+
direction,
|
|
358
|
+
queue: [],
|
|
359
|
+
resolveNext: null,
|
|
360
|
+
client: opts.client,
|
|
361
|
+
poll: { lastSeen: Date.now(), parked: 0, wakers: /* @__PURE__ */ new Set() }
|
|
362
|
+
};
|
|
363
|
+
dir.set(subscriberId, subscriber);
|
|
364
|
+
created = true;
|
|
365
|
+
if (direction === "outbound") this.announcePeer(id, "notifications/peer_joined", subscriberId);
|
|
366
|
+
}
|
|
367
|
+
const state = subscriber.poll;
|
|
368
|
+
state.lastSeen = Date.now();
|
|
369
|
+
return {
|
|
370
|
+
ok: true,
|
|
371
|
+
subscriberId: subscriber.id,
|
|
372
|
+
created,
|
|
373
|
+
wait: async (ms, signal) => {
|
|
374
|
+
state.parked++;
|
|
375
|
+
state.lastSeen = Date.now();
|
|
376
|
+
try {
|
|
377
|
+
if (subscriber.queue.length === 0 && ms > 0 && !subscriber.ended && !signal?.aborted) {
|
|
378
|
+
await new Promise((resolve) => {
|
|
379
|
+
const done = () => {
|
|
380
|
+
clearTimeout(timer);
|
|
381
|
+
signal?.removeEventListener("abort", done);
|
|
382
|
+
state.wakers.delete(done);
|
|
383
|
+
resolve();
|
|
384
|
+
};
|
|
385
|
+
const timer = setTimeout(done, ms);
|
|
386
|
+
state.wakers.add(done);
|
|
387
|
+
signal?.addEventListener("abort", done, { once: true });
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
if (signal?.aborted) return [];
|
|
391
|
+
return subscriber.queue.splice(0);
|
|
392
|
+
} finally {
|
|
393
|
+
state.parked--;
|
|
394
|
+
state.lastSeen = Date.now();
|
|
395
|
+
}
|
|
396
|
+
},
|
|
397
|
+
requeue: (frames) => {
|
|
398
|
+
if (frames.length > 0 && !subscriber.ended) subscriber.queue.unshift(...frames);
|
|
206
399
|
}
|
|
207
400
|
};
|
|
208
401
|
}
|
|
@@ -295,16 +488,78 @@ var RelayBroker = class {
|
|
|
295
488
|
if (!dir) return;
|
|
296
489
|
for (const sub of dir.values()) {
|
|
297
490
|
if (sub.client !== client) continue;
|
|
298
|
-
|
|
299
|
-
sub.resolveNext?.(sub.queue.shift() ?? null);
|
|
491
|
+
this.deliver(sub, payload);
|
|
300
492
|
}
|
|
301
493
|
}
|
|
302
494
|
fanOut(sessionId, direction, payload) {
|
|
303
495
|
const dir = this.subs.get(sessionId)?.get(direction);
|
|
304
496
|
if (!dir) return;
|
|
305
|
-
for (const sub of dir.values())
|
|
306
|
-
|
|
307
|
-
|
|
497
|
+
for (const sub of dir.values()) this.deliver(sub, payload);
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Hand one frame to one subscriber.
|
|
501
|
+
*
|
|
502
|
+
* The resolver is CLEARED before it is called. It used to stay set until the
|
|
503
|
+
* generator resumed on a later microtask, so a second frame in the same tick
|
|
504
|
+
* called an already-settled resolver — a no-op — after shifting its frame out
|
|
505
|
+
* of the queue, and that frame was lost. Cleared first, the second frame just
|
|
506
|
+
* waits in the queue for the generator to come back for it.
|
|
507
|
+
*/
|
|
508
|
+
deliver(sub, payload) {
|
|
509
|
+
if (sub.ended) return;
|
|
510
|
+
sub.queue.push(payload);
|
|
511
|
+
const resolve = sub.resolveNext;
|
|
512
|
+
if (resolve) {
|
|
513
|
+
sub.resolveNext = null;
|
|
514
|
+
resolve(sub.queue.shift() ?? null);
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
if (sub.poll) for (const wake of [...sub.poll.wakers]) wake();
|
|
518
|
+
}
|
|
519
|
+
/** Tell the page (the inbound side) that an agent arrived or left. */
|
|
520
|
+
announcePeer(sessionId, method, subscriberId) {
|
|
521
|
+
this.fanOut(sessionId, "inbound", JSON.stringify({ jsonrpc: "2.0", method, params: { subscriberId, ts: Date.now() } }));
|
|
522
|
+
}
|
|
523
|
+
/** End one subscriber: close its stream, or release its parked polls. Idempotent. */
|
|
524
|
+
endSubscriber(sub) {
|
|
525
|
+
if (sub.ended) return;
|
|
526
|
+
sub.ended = true;
|
|
527
|
+
const resolve = sub.resolveNext;
|
|
528
|
+
sub.resolveNext = null;
|
|
529
|
+
resolve?.(null);
|
|
530
|
+
if (sub.poll) for (const wake of [...sub.poll.wakers]) wake();
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* End every subscriber of a session whose session is going away.
|
|
534
|
+
*
|
|
535
|
+
* Streams used to be dropped from the map WITHOUT being woken, so an SSE leg
|
|
536
|
+
* stayed open, heartbeating, for a session that no longer existed — and its
|
|
537
|
+
* client could not learn the page had closed. Ended, the stream closes, the
|
|
538
|
+
* client reconnects, and `events` answers 410.
|
|
539
|
+
*/
|
|
540
|
+
endSubscribers(sessionId) {
|
|
541
|
+
const dirs = this.subs.get(sessionId);
|
|
542
|
+
if (!dirs) return;
|
|
543
|
+
for (const dir of dirs.values()) for (const sub of dir.values()) this.endSubscriber(sub);
|
|
544
|
+
}
|
|
545
|
+
/** Remove a subscriber from its session, announcing an agent's departure once. */
|
|
546
|
+
removeSubscriber(sessionId, sub) {
|
|
547
|
+
const dir = this.subs.get(sessionId)?.get(sub.direction);
|
|
548
|
+
if (!dir || dir.get(sub.id) !== sub) return;
|
|
549
|
+
dir.delete(sub.id);
|
|
550
|
+
if (sub.direction === "outbound") this.announcePeer(sessionId, "notifications/peer_left", sub.id);
|
|
551
|
+
}
|
|
552
|
+
/** Drop long-poll subscribers that stopped polling. Never one with a poll parked. */
|
|
553
|
+
prunePollers(now) {
|
|
554
|
+
for (const [sessionId, dirs] of this.subs) {
|
|
555
|
+
for (const dir of dirs.values()) {
|
|
556
|
+
for (const sub of [...dir.values()]) {
|
|
557
|
+
if (!sub.poll || sub.poll.parked > 0) continue;
|
|
558
|
+
if (now - sub.poll.lastSeen <= this.pollIdleMs) continue;
|
|
559
|
+
this.endSubscriber(sub);
|
|
560
|
+
this.removeSubscriber(sessionId, sub);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
308
563
|
}
|
|
309
564
|
}
|
|
310
565
|
/**
|
|
@@ -340,18 +595,15 @@ var RelayBroker = class {
|
|
|
340
595
|
return "id" in o && hasResult !== hasError;
|
|
341
596
|
}
|
|
342
597
|
reap() {
|
|
343
|
-
const
|
|
598
|
+
const now = Date.now();
|
|
599
|
+
const cutoff = now - this.ttlMs;
|
|
344
600
|
for (const id of this.store.expiredSessionIds(cutoff)) {
|
|
345
|
-
|
|
346
|
-
if (dirs) {
|
|
347
|
-
for (const dir of dirs.values()) {
|
|
348
|
-
for (const sub of dir.values()) sub.resolveNext?.(null);
|
|
349
|
-
}
|
|
350
|
-
}
|
|
601
|
+
this.endSubscribers(id);
|
|
351
602
|
this.subs.delete(id);
|
|
352
603
|
this.callers.delete(id);
|
|
353
604
|
this.store.deleteSession(id);
|
|
354
605
|
}
|
|
606
|
+
this.prunePollers(now);
|
|
355
607
|
}
|
|
356
608
|
};
|
|
357
609
|
function sha256Hex(input) {
|
|
@@ -363,19 +615,25 @@ function timingSafeEqualHex(a, b) {
|
|
|
363
615
|
}
|
|
364
616
|
|
|
365
617
|
// src/relay-server/node.ts
|
|
618
|
+
var POLL_MAX_WAIT_MS = 25e3;
|
|
619
|
+
var POLL_DEFAULT_WAIT_MS = 2e4;
|
|
620
|
+
function pollWaitMs(raw) {
|
|
621
|
+
if (raw === null || raw === void 0) return POLL_DEFAULT_WAIT_MS;
|
|
622
|
+
const n = Number.parseInt(raw, 10);
|
|
623
|
+
if (!Number.isFinite(n)) return 0;
|
|
624
|
+
return Math.min(Math.max(n, 0), POLL_MAX_WAIT_MS);
|
|
625
|
+
}
|
|
366
626
|
function createNodeRelay(opts = {}) {
|
|
367
627
|
const broker = new RelayBroker(opts);
|
|
368
628
|
const prefix = (opts.pathPrefix ?? "").replace(/\/$/, "");
|
|
369
|
-
const cors = opts.corsAllowOrigin ?? "*";
|
|
370
|
-
const allowedOrigins = opts.allowedOrigins;
|
|
629
|
+
const cors = opts.allowedOrigins && opts.allowedOrigins.length > 0 ? parseCorsOrigins(opts.allowedOrigins) : parseCorsOrigins(opts.corsAllowOrigin ?? "*");
|
|
371
630
|
function setCorsHeaders(res, req) {
|
|
372
|
-
if (
|
|
373
|
-
|
|
374
|
-
res.setHeader("access-control-allow-origin", origin && allowedOrigins.includes(origin) ? origin : "null");
|
|
375
|
-
res.setHeader("vary", "origin");
|
|
376
|
-
} else {
|
|
377
|
-
res.setHeader("access-control-allow-origin", cors);
|
|
631
|
+
if (cors.kind === "list") {
|
|
632
|
+
res.setHeader("vary", "Origin");
|
|
378
633
|
}
|
|
634
|
+
const origin = req?.headers.origin;
|
|
635
|
+
const allow = allowOriginFor(cors, typeof origin === "string" ? origin : void 0);
|
|
636
|
+
if (allow !== null) res.setHeader("access-control-allow-origin", allow);
|
|
379
637
|
res.setHeader("access-control-allow-methods", "GET, POST, OPTIONS");
|
|
380
638
|
res.setHeader("access-control-allow-headers", "content-type, x-csrf-token, accept, authorization");
|
|
381
639
|
res.setHeader("access-control-max-age", "86400");
|
|
@@ -492,7 +750,7 @@ function createNodeRelay(opts = {}) {
|
|
|
492
750
|
client: getQuery(req).get("client") ?? void 0
|
|
493
751
|
});
|
|
494
752
|
if (!sub.ok) {
|
|
495
|
-
res.statusCode = 401;
|
|
753
|
+
res.statusCode = sub.reason === "session_gone" ? 410 : 401;
|
|
496
754
|
res.setHeader("content-type", "text/event-stream");
|
|
497
755
|
res.write(`event: error
|
|
498
756
|
data: ${sub.reason}
|
|
@@ -537,6 +795,38 @@ data: ${frame}
|
|
|
537
795
|
res.end();
|
|
538
796
|
}
|
|
539
797
|
};
|
|
798
|
+
const poll = async (req, res) => {
|
|
799
|
+
setCorsHeaders(res, req);
|
|
800
|
+
if (req.method === "OPTIONS") {
|
|
801
|
+
res.statusCode = 204;
|
|
802
|
+
return res.end();
|
|
803
|
+
}
|
|
804
|
+
if (req.method !== "GET") return json(res, 405, { error: "method_not_allowed" });
|
|
805
|
+
const session = extractSession(req, prefix);
|
|
806
|
+
if (!session) return json(res, 400, { error: "missing_session" });
|
|
807
|
+
const q = getQuery(req);
|
|
808
|
+
const direction = q.get("direction") === "outbound" ? "outbound" : "inbound";
|
|
809
|
+
const attached = broker.poll(session, q.get("token") ?? "", direction, {
|
|
810
|
+
subscriber: q.get("subscriber") ?? void 0,
|
|
811
|
+
client: q.get("client") ?? void 0
|
|
812
|
+
});
|
|
813
|
+
if (!attached.ok) {
|
|
814
|
+
return json(res, attached.reason === "session_gone" ? 410 : 401, { error: attached.reason });
|
|
815
|
+
}
|
|
816
|
+
const gone = new AbortController();
|
|
817
|
+
const onClose = () => {
|
|
818
|
+
if (!res.writableEnded) gone.abort();
|
|
819
|
+
};
|
|
820
|
+
res.on("close", onClose);
|
|
821
|
+
const frames = await attached.wait(pollWaitMs(q.get("wait")), gone.signal);
|
|
822
|
+
res.off("close", onClose);
|
|
823
|
+
if (gone.signal.aborted || res.destroyed) {
|
|
824
|
+
attached.requeue(frames);
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
res.setHeader("cache-control", "no-store");
|
|
828
|
+
return json(res, 200, { subscriber: attached.subscriberId, frames });
|
|
829
|
+
};
|
|
540
830
|
const handler = async (req, res) => {
|
|
541
831
|
const pathname = getPathname(req);
|
|
542
832
|
if (!pathname.startsWith(prefix + "/")) {
|
|
@@ -544,16 +834,17 @@ data: ${frame}
|
|
|
544
834
|
}
|
|
545
835
|
const rest = pathname.slice(prefix.length);
|
|
546
836
|
if (rest === "/register") return register(req, res);
|
|
547
|
-
const m = /^\/([A-Za-z0-9_-]{4,64})\/(inbox|outbox|events|unregister)$/.exec(rest);
|
|
837
|
+
const m = /^\/([A-Za-z0-9_-]{4,64})\/(inbox|outbox|events|poll|unregister)$/.exec(rest);
|
|
548
838
|
if (!m) return json(res, 404, { error: "not_found" });
|
|
549
839
|
const route = m[2];
|
|
550
840
|
if (route === "inbox") return inbox(req, res);
|
|
551
841
|
if (route === "outbox") return outbox(req, res);
|
|
552
842
|
if (route === "events") return events(req, res);
|
|
843
|
+
if (route === "poll") return poll(req, res);
|
|
553
844
|
if (route === "unregister") return unregister(req, res);
|
|
554
845
|
return json(res, 404, { error: "not_found" });
|
|
555
846
|
};
|
|
556
|
-
return { broker, handler, register, inbox, outbox, events, unregister };
|
|
847
|
+
return { broker, handler, register, inbox, outbox, events, poll, unregister };
|
|
557
848
|
}
|
|
558
849
|
function extractSession(req, prefix) {
|
|
559
850
|
const host = req.headers.host || "x";
|
|
@@ -571,46 +862,27 @@ function flush(res) {
|
|
|
571
862
|
|
|
572
863
|
// src/relay-server/cli.ts
|
|
573
864
|
async function main() {
|
|
574
|
-
const
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
let prefix = process.env.PREFIX ?? "";
|
|
578
|
-
let ttlMs = Number(process.env.TTL_MS ?? 4 * 60 * 60 * 1e3);
|
|
579
|
-
let cors = process.env.CORS_ALLOW_ORIGIN ?? "*";
|
|
580
|
-
for (let i = 0; i < argv.length; i++) {
|
|
581
|
-
const a = argv[i];
|
|
582
|
-
switch (a) {
|
|
583
|
-
case "--port":
|
|
584
|
-
port = Number(argv[++i]);
|
|
585
|
-
break;
|
|
586
|
-
case "--host":
|
|
587
|
-
host = argv[++i];
|
|
588
|
-
break;
|
|
589
|
-
case "--prefix":
|
|
590
|
-
prefix = argv[++i];
|
|
591
|
-
break;
|
|
592
|
-
case "--ttl-ms":
|
|
593
|
-
ttlMs = Number(argv[++i]);
|
|
594
|
-
break;
|
|
595
|
-
case "--cors":
|
|
596
|
-
cors = argv[++i];
|
|
597
|
-
break;
|
|
598
|
-
case "-h":
|
|
599
|
-
case "--help":
|
|
600
|
-
process.stdout.write(HELP);
|
|
601
|
-
process.exit(0);
|
|
602
|
-
default:
|
|
603
|
-
process.stderr.write(`[relay] unknown flag: ${a}
|
|
865
|
+
const parsed = parseRelayArgs(process.argv.slice(2), process.env);
|
|
866
|
+
if (!parsed.ok) {
|
|
867
|
+
process.stderr.write(`[relay] ${parsed.error}
|
|
604
868
|
`);
|
|
605
|
-
|
|
606
|
-
}
|
|
869
|
+
process.exit(2);
|
|
607
870
|
}
|
|
608
|
-
|
|
609
|
-
|
|
871
|
+
const { config } = parsed;
|
|
872
|
+
if (config.help) {
|
|
873
|
+
process.stdout.write(HELP);
|
|
874
|
+
process.exit(0);
|
|
875
|
+
}
|
|
876
|
+
for (const warning of config.warnings) {
|
|
877
|
+
process.stderr.write(`[relay] WARNING: ${warning}
|
|
610
878
|
`);
|
|
611
|
-
process.exit(2);
|
|
612
879
|
}
|
|
613
|
-
const
|
|
880
|
+
const { port, host, prefix, ttlMs, cors } = config;
|
|
881
|
+
const relay = createNodeRelay({
|
|
882
|
+
pathPrefix: prefix,
|
|
883
|
+
ttlMs,
|
|
884
|
+
corsAllowOrigin: cors.kind === "any" ? "*" : cors.origins.join(",")
|
|
885
|
+
});
|
|
614
886
|
const server = http.createServer((req, res) => {
|
|
615
887
|
const url = req.url || "/";
|
|
616
888
|
if ((url === "/" || url === "/healthz" || url === prefix + "/") && req.method === "GET") {
|
|
@@ -628,9 +900,10 @@ async function main() {
|
|
|
628
900
|
`
|
|
629
901
|
);
|
|
630
902
|
}
|
|
903
|
+
const corsSource = { flag: "--cors", env: "CORS_ALLOW_ORIGIN", default: "default" }[config.sources.cors];
|
|
631
904
|
server.listen(port, host, () => {
|
|
632
905
|
process.stdout.write(
|
|
633
|
-
`[relay] listening on http://${host}:${port}${prefix || ""} (ttl=${Math.round(ttlMs / 1e3)}s, cors=${cors})
|
|
906
|
+
`[relay] listening on http://${host}:${port}${prefix || ""} (ttl=${Math.round(ttlMs / 1e3)}s, cors=${describeCorsPolicy(cors)} from ${corsSource})
|
|
634
907
|
`
|
|
635
908
|
);
|
|
636
909
|
});
|
|
@@ -653,17 +926,29 @@ Options:
|
|
|
653
926
|
Pass 0.0.0.0 to expose on all interfaces (prints a warning).
|
|
654
927
|
--prefix <path> URL path prefix (env: PREFIX). Default "".
|
|
655
928
|
--ttl-ms <n> Session TTL ms (env: TTL_MS). Default 14_400_000.
|
|
656
|
-
--cors <
|
|
929
|
+
--cors <origins> Allowed browser origins (env: CORS_ALLOW_ORIGIN). Default "*".
|
|
930
|
+
"*", or a comma-separated list such as
|
|
931
|
+
"https://example.com,https://www.example.com". May be
|
|
932
|
+
repeated. A listed Origin is echoed back with Vary: Origin;
|
|
933
|
+
an unlisted one gets no Access-Control-Allow-Origin.
|
|
657
934
|
-h, --help Show this help.
|
|
658
935
|
|
|
936
|
+
Precedence: a flag wins over its env var, which wins over the default. An env
|
|
937
|
+
var that a flag overrides is reported at startup. An empty env var is unset.
|
|
938
|
+
|
|
659
939
|
Endpoints (under --prefix):
|
|
660
940
|
POST /register { session, token } \u2192 { ok: true }
|
|
661
941
|
POST /<session>/inbox?token=... body: JSON-RPC frame
|
|
662
942
|
POST /<session>/outbox?token=... body: JSON-RPC frame
|
|
663
943
|
GET /<session>/events?token=...&direction=inbound|outbound
|
|
664
944
|
Server-sent events stream
|
|
945
|
+
GET /<session>/poll?token=...&direction=inbound|outbound&wait=<ms>&subscriber=<id>
|
|
946
|
+
Long-poll \u2192 { subscriber, frames }
|
|
665
947
|
POST /<session>/unregister?token=... Tear down session
|
|
666
948
|
GET / Healthcheck \u2192 200
|
|
949
|
+
|
|
950
|
+
A session that has ended answers 410 {"error":"session_gone"} on every session
|
|
951
|
+
route; a wrong or missing token answers 401.
|
|
667
952
|
`;
|
|
668
953
|
main().catch((e) => {
|
|
669
954
|
process.stderr.write(`[relay] fatal: ${e instanceof Error ? e.stack ?? e.message : String(e)}
|