@tapflowio/relay 0.17.0 → 0.19.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 +1 -1
- package/dist/RelayServer.d.ts +313 -2
- package/dist/RelayServer.d.ts.map +1 -1
- package/dist/RelayServer.js +1245 -211
- package/dist/RelayServer.js.map +1 -1
- package/dist/SessionManager.d.ts +124 -21
- package/dist/SessionManager.d.ts.map +1 -1
- package/dist/SessionManager.js +201 -22
- package/dist/SessionManager.js.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/types.d.ts +3 -44
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +13 -0
- package/dist/types.js.map +1 -1
- package/package.json +10 -8
- package/public/assets/{AppCenter-BRmG2FEm.js → AppCenter-DyP0aqfF.js} +2 -2
- package/public/assets/AppCenter-DyP0aqfF.js.br +0 -0
- package/public/assets/Default-D7ScPE30.js +1 -0
- package/public/assets/Default-D7ScPE30.js.br +0 -0
- package/public/assets/Invite-UmhV5rAf.js +1 -0
- package/public/assets/Invite-UmhV5rAf.js.br +0 -0
- package/public/assets/MacResources-MS4E00w3.js +1 -0
- package/public/assets/MacResources-MS4E00w3.js.br +0 -0
- package/public/assets/NotFound-BoDDivK5.js +1 -0
- package/public/assets/NotFound-BoDDivK5.js.br +0 -0
- package/public/assets/QASession-CjgJHDZ3.js +125 -0
- package/public/assets/QASession-CjgJHDZ3.js.br +0 -0
- package/public/assets/ResetPassword-CAlB6CfP.js +1 -0
- package/public/assets/ResetPassword-CAlB6CfP.js.br +0 -0
- package/public/assets/Setup-CPm2-hPJ.js +1 -0
- package/public/assets/Setup-CPm2-hPJ.js.br +0 -0
- package/public/assets/{Team-D1iwg2vL.js → Team-GEIzrM1D.js} +2 -2
- package/public/assets/Team-GEIzrM1D.js.br +0 -0
- package/public/assets/Tokens-DSeBz_CJ.js +1 -0
- package/public/assets/Tokens-DSeBz_CJ.js.br +0 -0
- package/public/assets/alert-dialog-BsXI9SVV.js +1 -0
- package/public/assets/alert-dialog-BsXI9SVV.js.br +0 -0
- package/public/assets/badge-BkdYQFsh.js +1 -0
- package/public/assets/badge-BkdYQFsh.js.br +0 -0
- package/public/assets/{build-format-BZV6gt41.js → build-format-CXuQt1X2.js} +3 -3
- package/public/assets/build-format-CXuQt1X2.js.br +0 -0
- package/public/assets/dialog-DuXZpbiF.js +11 -0
- package/public/assets/dialog-DuXZpbiF.js.br +0 -0
- package/public/assets/index-Bk2hqaHZ.css +1 -0
- package/public/assets/index-Bk2hqaHZ.css.br +0 -0
- package/public/assets/index-bitr6DIC.js +215 -0
- package/public/assets/index-bitr6DIC.js.br +0 -0
- package/public/assets/{pencil-BGrOEXS6.js → pencil-iRR275s8.js} +1 -1
- package/public/assets/pencil-iRR275s8.js.br +0 -0
- package/public/assets/table-CHmhGpHG.js +1 -0
- package/public/assets/table-CHmhGpHG.js.br +0 -0
- package/public/assets/tabs-BnbeuAOe.js +1 -0
- package/public/assets/tabs-BnbeuAOe.js.br +0 -0
- package/public/assets/{trash-2-CwlFrbEF.js → trash-2-QI162w2K.js} +1 -1
- package/public/assets/trash-2-QI162w2K.js.br +0 -0
- package/public/assets/vendor-charts-DyX5CBcf.js +5 -0
- package/public/assets/vendor-charts-DyX5CBcf.js.br +0 -0
- package/public/assets/vendor-react-AJpf4YZ0.js +49 -0
- package/public/assets/vendor-react-AJpf4YZ0.js.br +0 -0
- package/public/assets/vendor-router-ChFy8CZ1.js +12 -0
- package/public/assets/vendor-router-ChFy8CZ1.js.br +0 -0
- package/public/index.html +4 -2
- package/public/index.html.br +0 -0
- package/public/assets/AppCenter-BRmG2FEm.js.br +0 -0
- package/public/assets/Default-DTr0wqds.js +0 -1
- package/public/assets/Default-DTr0wqds.js.br +0 -0
- package/public/assets/Invite-DKsNh2GB.js +0 -1
- package/public/assets/Invite-DKsNh2GB.js.br +0 -0
- package/public/assets/MacResources-CLDdcibL.js +0 -5
- package/public/assets/MacResources-CLDdcibL.js.br +0 -0
- package/public/assets/NotFound-BtKq2uv6.js +0 -1
- package/public/assets/NotFound-BtKq2uv6.js.br +0 -0
- package/public/assets/QASession-B_5aJ67l.js +0 -130
- package/public/assets/QASession-B_5aJ67l.js.br +0 -0
- package/public/assets/ResetPassword-gp6hVQrf.js +0 -1
- package/public/assets/ResetPassword-gp6hVQrf.js.br +0 -0
- package/public/assets/Setup-DiG4Nbs7.js +0 -1
- package/public/assets/Setup-DiG4Nbs7.js.br +0 -0
- package/public/assets/Team-D1iwg2vL.js.br +0 -0
- package/public/assets/Tokens-DGtE4Xcy.js +0 -1
- package/public/assets/Tokens-DGtE4Xcy.js.br +0 -0
- package/public/assets/alert-dialog-CXwkFlF3.js +0 -1
- package/public/assets/alert-dialog-CXwkFlF3.js.br +0 -0
- package/public/assets/badge-CavXTq2x.js +0 -1
- package/public/assets/badge-CavXTq2x.js.br +0 -0
- package/public/assets/build-format-BZV6gt41.js.br +0 -0
- package/public/assets/dialog-JbKwqqS0.js +0 -11
- package/public/assets/dialog-JbKwqqS0.js.br +0 -0
- package/public/assets/index-CBnxiD7-.js +0 -274
- package/public/assets/index-CBnxiD7-.js.br +0 -0
- package/public/assets/index-Ctx3_cYz.css +0 -1
- package/public/assets/index-Ctx3_cYz.css.br +0 -0
- package/public/assets/pencil-BGrOEXS6.js.br +0 -0
- package/public/assets/table-CqOA0Bb1.js +0 -1
- package/public/assets/table-CqOA0Bb1.js.br +0 -0
- package/public/assets/tabs-BVBbhCFG.js +0 -1
- package/public/assets/tabs-BVBbhCFG.js.br +0 -0
- package/public/assets/trash-2-CwlFrbEF.js.br +0 -0
package/README.md
CHANGED
package/dist/RelayServer.d.ts
CHANGED
|
@@ -1,4 +1,27 @@
|
|
|
1
1
|
export declare function isExternalAddress(addr: string): boolean;
|
|
2
|
+
/** Exported for the re-join test, which pins both edges of this window rather than hardcoding 500 —
|
|
3
|
+
* a test carrying its own copy of the number passes when the constant moves, which is the drift the
|
|
4
|
+
* wire-contract work keeps finding. Not re-exported from the package index. */
|
|
5
|
+
export declare const IDR_REQUEST_THROTTLE_MS = 500;
|
|
6
|
+
/**
|
|
7
|
+
* The owner key a connection speaks for, from the authenticated user and the client id it claimed.
|
|
8
|
+
*
|
|
9
|
+
* **Both halves, and a pure function so both can be tested.** The user id is what keeps a leaked client id
|
|
10
|
+
* useless to anyone else — without it this would be the relay's first ownership claim a caller can forge,
|
|
11
|
+
* where socket identity could not be. It comes from the cookie *or* the PAT: reading only the cookie would
|
|
12
|
+
* have made every agent and every non-browser client `anon:`, which is the population most likely to have
|
|
13
|
+
* its handshake URL end up in a reverse proxy's access log. The mint is what removes the fallback branch: a connection that
|
|
14
|
+
* claims nothing gets an identity of its own, which is per-socket, which is the behaviour ownership had
|
|
15
|
+
* before it moved off the socket.
|
|
16
|
+
*
|
|
17
|
+
* Exported for the test that holds the pairing. Inline in the handler it was unreachable without standing
|
|
18
|
+
* up an authenticated handshake, and a mutation dropping the user id survived the whole suite.
|
|
19
|
+
*/
|
|
20
|
+
export declare function ownerKeyFor(userId: number | string | undefined, claimed: string | null): {
|
|
21
|
+
key: string;
|
|
22
|
+
user: string;
|
|
23
|
+
minted: boolean;
|
|
24
|
+
};
|
|
2
25
|
export declare class RelayServer {
|
|
3
26
|
private readonly options;
|
|
4
27
|
private httpServer;
|
|
@@ -15,15 +38,50 @@ export declare class RelayServer {
|
|
|
15
38
|
private purgeBuildsTimer;
|
|
16
39
|
private flushResourcesTimer;
|
|
17
40
|
private heartbeatTimer;
|
|
18
|
-
|
|
41
|
+
/**
|
|
42
|
+
* When each socket last answered a ping. WeakMap → no manual cleanup on close (GC handles it).
|
|
43
|
+
*
|
|
44
|
+
* **A timestamp, not the swept boolean it replaced.** That flag was set `false` for *every* socket at the
|
|
45
|
+
* start of each sweep and back to `true` only when the pong returned, so a perfectly healthy socket read
|
|
46
|
+
* as dead for the whole round trip — a normal state on a 30s cycle rather than an edge case. And the
|
|
47
|
+
* window is longest for exactly the socket that matters: video goes out on `browserSocket`, and the ping
|
|
48
|
+
* is written behind whatever is already queued there, so an external viewer's pong can take a second or
|
|
49
|
+
* two.
|
|
50
|
+
*
|
|
51
|
+
* That was invisible while the flag only decided termination, because a sweep that finds `false` has by
|
|
52
|
+
* definition already waited a full interval. Reading it to answer *"is this holder alive right now"* is
|
|
53
|
+
* what exposes it: occupancy would hand a live tester's session away, and `busy` would flash a device as
|
|
54
|
+
* free every 30 seconds — the dashboard polls every 5s and `disabled={isBusy}`, so the UI would open the
|
|
55
|
+
* button rather than a race being needed.
|
|
56
|
+
*/
|
|
57
|
+
private lastPongAt;
|
|
58
|
+
/**
|
|
59
|
+
* Who each socket speaks for — `<userId>:<clientId>`, the key a session's ownership is compared against.
|
|
60
|
+
*
|
|
61
|
+
* **Two parts, and both are load-bearing.** The client id is supplied by the caller (`?client=`), so on
|
|
62
|
+
* its own it is the relay's first *forgeable* ownership claim: today's ownership is socket identity,
|
|
63
|
+
* which nothing can spoof. Binding it to the authenticated user — which `getAuth` already resolves at
|
|
64
|
+
* the handshake — keeps a leaked id useless to anyone else's account.
|
|
65
|
+
*
|
|
66
|
+
* **A connection that supplies no client id is given one**, rather than falling back to socket identity.
|
|
67
|
+
* There is deliberately no fallback branch: one model, and a client that does not identify itself gets an
|
|
68
|
+
* identity that happens to be per-socket, which is exactly today's behaviour.
|
|
69
|
+
*/
|
|
70
|
+
private ownerKey;
|
|
19
71
|
private dropHandlers;
|
|
20
72
|
private audioDropHandlers;
|
|
21
73
|
private droppers;
|
|
22
74
|
private idrRequesters;
|
|
23
75
|
private wsRoles;
|
|
76
|
+
/** Per-socket throttle state for `logInboundRejection`. A `WeakMap` so a closed socket's entry goes
|
|
77
|
+
* with the socket — there is no cleanup to forget, unlike the maps keyed by session id nearby. */
|
|
78
|
+
private readonly rejectionLog;
|
|
79
|
+
private agentHolds;
|
|
80
|
+
private stopping;
|
|
24
81
|
private wsExternal;
|
|
25
82
|
private readonly backpressureBytes;
|
|
26
83
|
private readonly screenshotTimeoutMs;
|
|
84
|
+
private readonly agentGraceMs;
|
|
27
85
|
private readonly corsAllowed;
|
|
28
86
|
private warnedProxyMisconfig;
|
|
29
87
|
private pendingScreenshots;
|
|
@@ -43,6 +101,7 @@ export declare class RelayServer {
|
|
|
43
101
|
cert: string;
|
|
44
102
|
key: string;
|
|
45
103
|
};
|
|
104
|
+
agentGraceMs?: number;
|
|
46
105
|
});
|
|
47
106
|
private registerRoutes;
|
|
48
107
|
pushLog(msg: string): void;
|
|
@@ -50,20 +109,272 @@ export declare class RelayServer {
|
|
|
50
109
|
stop(): Promise<void>;
|
|
51
110
|
address(): string | import("net").AddressInfo | null;
|
|
52
111
|
private runHeartbeat;
|
|
112
|
+
/**
|
|
113
|
+
* Has this socket answered recently enough to be treated as present?
|
|
114
|
+
*
|
|
115
|
+
* `1.5` intervals rather than one: a pong that arrives *during* a sweep must not be read as late, and a
|
|
116
|
+
* socket pinged at the end of one interval has until halfway through the next. The cost is that a
|
|
117
|
+
* genuinely dead socket is recognised in up to 45s instead of 30 — **and 45 seconds with no false
|
|
118
|
+
* negative beats 30 with one every cycle**, because a false negative here does not merely delay: it
|
|
119
|
+
* hands a live tester's session to whoever asked next.
|
|
120
|
+
*/
|
|
121
|
+
private isAlive;
|
|
53
122
|
updateTlsContext(material: {
|
|
54
123
|
cert: string;
|
|
55
124
|
key: string;
|
|
56
125
|
}): void;
|
|
57
|
-
|
|
126
|
+
/**
|
|
127
|
+
* Drop every per-session stream record keyed by this id.
|
|
128
|
+
*
|
|
129
|
+
* **Four maps that have to move together, called from four places that did not.** They were deleted
|
|
130
|
+
* inline in `session:end` and `session:leave` and nowhere else, so a session ending any other way —
|
|
131
|
+
* the agent going away, the idle timer, a device disappearing from a re-register — left all four
|
|
132
|
+
* behind for the life of the process.
|
|
133
|
+
*
|
|
134
|
+
* Pre-existing, and this slice is what makes it worth fixing here rather than filing: `idrRequesters`
|
|
135
|
+
* used to be populated only by the binary drop path, i.e. only under backpressure, and a re-join now
|
|
136
|
+
* creates an entry for any streaming session (#515). The leak went from rare to ordinary.
|
|
137
|
+
*/
|
|
138
|
+
private forgetSessionStreamState;
|
|
139
|
+
/**
|
|
140
|
+
* Throttled callback asking the session's agent for an on-demand IDR (fast drop recovery); ignored by
|
|
141
|
+
* agents that don't support it.
|
|
142
|
+
*
|
|
143
|
+
* **One per session, and the construction is folded in here on purpose.** The throttle is `lastAt` in
|
|
144
|
+
* the closure, so a second requester for the same session is a second budget — which is what the
|
|
145
|
+
* `idrRequesters` map exists to prevent, and it prevented it only as long as every caller remembered
|
|
146
|
+
* to look the session up first. There are two callers now (the drop path and the re-join in
|
|
147
|
+
* `handleSessionStart`), and a separate `makeIdrRequester` was a factory either of them could reach
|
|
148
|
+
* past the map. Measured: calling it directly from one caller left every test green.
|
|
149
|
+
*/
|
|
150
|
+
private idrRequester;
|
|
58
151
|
private remoteAddressOf;
|
|
59
152
|
private warnProxyMisconfigOnce;
|
|
60
153
|
private handleConnection;
|
|
154
|
+
/**
|
|
155
|
+
* @param msg what the door proved — see `parseInbound`. An Envelope-tier member arrives carrying
|
|
156
|
+
* `type` and its correlators and nothing else, so a payload the parser did not check
|
|
157
|
+
* cannot be read off it.
|
|
158
|
+
* @param raw the frame as it arrived. Agent-origin messages are **forwarded as this**, so a field
|
|
159
|
+
* a newer agent added survives a relay that does not know it — `z.object` strips, and
|
|
160
|
+
* stripping is wrong in the one direction where the sender is the more recently
|
|
161
|
+
* updated side. Browser-origin messages are forwarded as `msg` instead, so a key an attacker
|
|
162
|
+
* appended does not survive. It is also where the two stored Envelope payloads come
|
|
163
|
+
* from, which is the point: a value read off `raw` is one the parser did not vouch for.
|
|
164
|
+
*/
|
|
165
|
+
/**
|
|
166
|
+
* Assign this socket's role if it does not have one, then refuse a message its role may not send.
|
|
167
|
+
*
|
|
168
|
+
* @returns `false` when the caller must stop — the socket was closed, or the frame is a failed
|
|
169
|
+
* handshake that must not confer a role.
|
|
170
|
+
*/
|
|
171
|
+
private settleRole;
|
|
172
|
+
/**
|
|
173
|
+
* What the door refused, said once and in a form an operator can act on.
|
|
174
|
+
*
|
|
175
|
+
* This is where `isAddressed` and `isCorrelated` ended up. Both were predicates whose whole
|
|
176
|
+
* observable output was a `console.warn` — an id-less request resolved no session and was dropped by
|
|
177
|
+
* the miss anyway — and the schemas now reject the same frames earlier, including the empty-string
|
|
178
|
+
* case a bare `z.string()` would have let through. What is new is that the log names the *field*:
|
|
179
|
+
* "requestId: Too small" instead of "dropped, cannot correlate a reply".
|
|
180
|
+
*
|
|
181
|
+
* Worth logging at all for the reason `isCorrelated` gave: the three places a bad frame can be
|
|
182
|
+
* dropped are otherwise silent, and an operator who upgrades the relay but not an independently
|
|
183
|
+
* installed `mcp-server` would watch commands do nothing with no trace.
|
|
184
|
+
*
|
|
185
|
+
* **Throttled per socket, and the "per socket" is the whole design.** A first draft wrote one line
|
|
186
|
+
* per rejected frame, on the direction a viewer with devtools controls and at the rate a gesture
|
|
187
|
+
* produces — the unbounded, attacker-driven log volume this file already refuses at
|
|
188
|
+
* `forwardUnacked`, with the reason written beside it. A single module-level timestamp would fix the
|
|
189
|
+
* volume and break the diagnostic: one noisy socket would silence the *other* socket's first bad
|
|
190
|
+
* frame, which is the skewed-client case the log exists for. So the state is keyed by socket, in a
|
|
191
|
+
* `WeakMap` that needs no cleanup, and **the first rejection from any socket is always written** —
|
|
192
|
+
* the one that names the skew is never the hundredth. What is dropped is only repetition, and the
|
|
193
|
+
* next line says how much.
|
|
194
|
+
*
|
|
195
|
+
* `unknown-type` stays at debug: eleven relay-produced literals land here whenever a client echoes
|
|
196
|
+
* one back, and none of them is a defect. It shares the throttle so turning debug on cannot
|
|
197
|
+
* reintroduce the volume.
|
|
198
|
+
*/
|
|
199
|
+
private logInboundRejection;
|
|
61
200
|
private route;
|
|
62
201
|
private handleAgentResources;
|
|
202
|
+
/**
|
|
203
|
+
* Every message the relay *originates* goes through here. The union checks the shape where the
|
|
204
|
+
* literal is written — a mistyped `type`, a missing field or one that does not belong are all
|
|
205
|
+
* compile errors — and the readyState guard replaces the check that was repeated (and sometimes
|
|
206
|
+
* omitted) at each call site.
|
|
207
|
+
*
|
|
208
|
+
* Messages the relay merely *forwards* do not use this: they are re-serialised unchanged, so
|
|
209
|
+
* they keep their inbound type and there is nothing new to check.
|
|
210
|
+
*/
|
|
211
|
+
private sendTo;
|
|
212
|
+
/**
|
|
213
|
+
* Settle every in-flight screenshot / UI-tree request belonging to these sessions. Both callers
|
|
214
|
+
* are moments where the agent that was going to answer them no longer exists — it disconnected,
|
|
215
|
+
* or it restarted and its session moved to the new process. Left alone these reject on their own
|
|
216
|
+
* timeout, minutes later, having told the caller nothing in between.
|
|
217
|
+
*/
|
|
218
|
+
private rejectPending;
|
|
219
|
+
/**
|
|
220
|
+
* The agent's socket went away. Keep its sessions and wait for that agent to come back, instead
|
|
221
|
+
* of ending them here — a restarting agent registers about a second later, and until #426 stage 3
|
|
222
|
+
* the close always won that race, so a restart cost the tester their place.
|
|
223
|
+
*
|
|
224
|
+
* The sessions stay exactly where they are. They are NOT moved to a holding structure of their
|
|
225
|
+
* own: `getAgentSocketsByIdentity` finds the returning agent's previous socket by walking
|
|
226
|
+
* `sessions` and reading `agentSocket`, so a session parked anywhere else is a session that can
|
|
227
|
+
* never be reclaimed.
|
|
228
|
+
*
|
|
229
|
+
* @returns whether this was an agent socket, matching `evictAgentSocket`'s contract — the close
|
|
230
|
+
* handler uses it to decide whether to keep looking.
|
|
231
|
+
*/
|
|
232
|
+
private holdAgentSocket;
|
|
233
|
+
/** Stop waiting on a socket, whether or not anything was reclaimed from it. */
|
|
234
|
+
private releaseHold;
|
|
235
|
+
/**
|
|
236
|
+
* @param cause `'disconnect'` — the socket closed. `'replaced'` — the same agent re-registered
|
|
237
|
+
* and this is its previous socket. Only affects the log line: a restart otherwise reads as a
|
|
238
|
+
* crash followed by a recovery, which is not what happened.
|
|
239
|
+
*/
|
|
63
240
|
private evictAgentSocket;
|
|
64
241
|
private handleAgentRegister;
|
|
242
|
+
/** The only producer of `error` — all five exits below, and nothing else in the repo sends that message.
|
|
243
|
+
*
|
|
244
|
+
* That is what makes the address possible rather than aspirational: `msg.sessionId` is narrowed to a
|
|
245
|
+
* non-empty `string` by the door — the inbound schema, `isAddressed` before it — so every refusal can
|
|
246
|
+
* name the join it refuses. Before
|
|
247
|
+
* L5d they carried none, and the clients' join waiters matched `sessionId === undefined || sessionId ===
|
|
248
|
+
* mine` — with no such key the left half was always true, so any refusal resolved any pending join. */
|
|
65
249
|
private handleSessionStart;
|
|
250
|
+
/** Shut the device down because nobody is watching it any more. The idle timer's payload, hoisted out
|
|
251
|
+
* of the close handler so the release loop there reads as one line per session. */
|
|
252
|
+
private idleShutdown;
|
|
253
|
+
/** Where a browser-role command can be dispatched, or what its sender needs to be told instead.
|
|
254
|
+
*
|
|
255
|
+
* Three refusals used to be spread across seven `case` bodies as two conditions each, and the ownership
|
|
256
|
+
* one was in none of them. Collapsed here so the set is decided once: a command reaches an agent only if
|
|
257
|
+
* the session exists, **this socket holds it**, and the agent is connected. Anything else has prose the
|
|
258
|
+
* caller can act on, and each case wraps that prose in the reply type its own waiter reads.
|
|
259
|
+
*
|
|
260
|
+
* The two ownership strings are one reason with different prose, the treatment `#492` settled: telling a
|
|
261
|
+
* caller the session is in use when it is idle steers it off a device it could have had.
|
|
262
|
+
*/
|
|
263
|
+
private dispatchTarget;
|
|
264
|
+
/** `dispatchTarget` without the ownership clause: is there a session here, and is its agent listening?
|
|
265
|
+
*
|
|
266
|
+
* **The name carries the omission because a doc block does not reach an autocomplete.** This is the
|
|
267
|
+
* resolver a future handler must not pick by accident — it is the general-looking one and the unsafe
|
|
268
|
+
* one at the same time, which is the pairing worth spelling out.
|
|
269
|
+
*
|
|
270
|
+
* **Still here after #527, and this paragraph used to say it would not be.** It predicted that
|
|
271
|
+
* answering "who may shut a device down" would move `device:shutdown` up to `dispatchTarget`. The
|
|
272
|
+
* answer turned out to be a *different* gate rather than that one: `mayShutDown` refuses a session
|
|
273
|
+
* another client holds and permits an unheld one, because the dashboard's unmount teardown races its
|
|
274
|
+
* own viewer socket's close and an owns-it gate refuses the teardown whenever the close lands first —
|
|
275
|
+
* leaving the device booted until the idle timer, which is the cost #527 was filed to avoid.
|
|
276
|
+
*
|
|
277
|
+
* So the two are ordered rather than merged: `mayShutDown` decides *who*, this decides *whether it can
|
|
278
|
+
* be delivered at all*, and without the second a shutdown addressed to a missing session or a closed
|
|
279
|
+
* agent socket is dropped in silence and `mcp-server`'s caller burns a 30s deadline (#542).
|
|
280
|
+
*
|
|
281
|
+
* The disclosure it was written to record stands: a caller that is not the holder still learns whether
|
|
282
|
+
* a session id exists and whether its agent is up.
|
|
283
|
+
*/
|
|
284
|
+
private reachableTargetWithoutOwnership;
|
|
285
|
+
/** Is `ws` the socket this session is bound to?
|
|
286
|
+
*
|
|
287
|
+
* Until L5c the input path resolved the session and forwarded without asking. `clipboard:data` asks the
|
|
288
|
+
* mirror-image question one branch up (`session?.agentSocket !== ws`) with the reason written beside it —
|
|
289
|
+
* a second agent on the same relay must not address someone else's session — and the browser direction
|
|
290
|
+
* had no equivalent. So any authenticated client that knew a session id could drive a device another
|
|
291
|
+
* tester was looking at, and the agent's ack went to the session holder rather than to whoever asked.
|
|
292
|
+
*
|
|
293
|
+
* An **unheld** session (no `browserSocket`, or one that has gone) is also not owned by the sender. That
|
|
294
|
+
* is deliberate rather than incidental: it makes an input arriving inside the reconnect grace fail fast
|
|
295
|
+
* instead of being applied to a device with nobody watching the result. */
|
|
296
|
+
private ownsSession;
|
|
297
|
+
/** Which owner this socket speaks for. Every connection has one — supplied or minted — so this never
|
|
298
|
+
* answers `undefined` and two anonymous sockets can never match by both lacking an identity. */
|
|
299
|
+
private ownerOf;
|
|
300
|
+
/** The principal behind a socket — a user id, or `anon` for a connection carrying no cookie and no PAT. */
|
|
301
|
+
private userOf;
|
|
302
|
+
/** Everything the bind needs to record about who is joining.
|
|
303
|
+
*
|
|
304
|
+
* **A socket with no record gets one of its own rather than a shared sentinel.** `handleConnection`
|
|
305
|
+
* seeds every accepted connection before a message can arrive, so the fallback is unreachable today —
|
|
306
|
+
* but a literal would make any two unseeded sockets *equal*, and equal keys pass `ownsSession` while
|
|
307
|
+
* equal users pass `mayShutDown`'s same-principal exemption. That rests the whole ownership invariant
|
|
308
|
+
* on one call site staying total, which is the shape of dependency the sentinel is supposed to remove.
|
|
309
|
+
* Minting keeps the answer "this socket and no other"; storing it keeps repeat calls consistent, since
|
|
310
|
+
* a fresh value each time would fail to recognise the very socket that bound the session. `minted` is
|
|
311
|
+
* `false` deliberately — the legacy exemption exists for a client that identified itself with nothing,
|
|
312
|
+
* not for one the relay failed to record. */
|
|
313
|
+
private ownerRecord;
|
|
314
|
+
/**
|
|
315
|
+
* May this socket shut the session's device down?
|
|
316
|
+
*
|
|
317
|
+
* **"Not someone else's" rather than "mine", and the difference is the reason #527 stayed open.** The
|
|
318
|
+
* dashboard's unmount teardown sends `device:shutdown` from one socket while another socket's `close`
|
|
319
|
+
* races it to the relay — two connections, no ordering — and if the close lands first the session is
|
|
320
|
+
* unheld. An owns-it gate refuses the teardown there, and the device stays booted until the five-minute
|
|
321
|
+
* idle timer: the path #527 named as the reason a gate could not be added at all.
|
|
322
|
+
*
|
|
323
|
+
* An unheld session therefore stays shutdownable by anyone authenticated, which is unchanged from today
|
|
324
|
+
* — this command has never had a gate — and is written down rather than implied. What changes is the
|
|
325
|
+
* case the issue is about: a device another tester is *holding* can no longer be powered off.
|
|
326
|
+
*/
|
|
327
|
+
private mayShutDown;
|
|
328
|
+
/** Input that no ack answers: opening and move frames, rotation, the keyboard-forwarding toggle.
|
|
329
|
+
*
|
|
330
|
+
* Dropped rather than refused when unowned, and that asymmetry with `handleAckedInput` is the whole
|
|
331
|
+
* reason these have their own clause: refusing means answering, and there is no waiter here to answer.
|
|
332
|
+
* `clipboard:data`'s silent `break` is the precedent that fits — a frame nobody is waiting on. */
|
|
333
|
+
private forwardUnacked;
|
|
334
|
+
/** The five inputs an ack answers, so the five that carry a required correlator.
|
|
335
|
+
*
|
|
336
|
+
* Every exit answers the sender. That is the rule this method exists to hold: a request with a waiter on
|
|
337
|
+
* a 2s deadline must not be dropped silently, because the caller's fallback reports silence from a
|
|
338
|
+
* session that has never acked as **success** (#457) — so a silent drop here would report an input that
|
|
339
|
+
* never left the relay as landed, which is worse than the misrouting it replaced. */
|
|
340
|
+
private handleAckedInput;
|
|
341
|
+
/**
|
|
342
|
+
* Tells the sender its payload was refused, in the shape that request's own waiter reads.
|
|
343
|
+
*
|
|
344
|
+
* **This is what keeps the door from turning an answered failure into silence.** Before it, a
|
|
345
|
+
* malformed `open-url` reached the agent and the agent's own guard answered `open-url:error`;
|
|
346
|
+
* `IOSAgent.ts` says so beside that guard, and names this validation as what would take it over.
|
|
347
|
+
* Taking the responsibility without taking the answer would have been a regression — worst on the
|
|
348
|
+
* inputs, and not obviously: `awaitInputAck` reports silence from a session that has never acked as
|
|
349
|
+
* **success** (#457), so a dropped `input:key` reads to an MCP caller as an input that landed.
|
|
350
|
+
*
|
|
351
|
+
* The address and the correlator come from the envelope, which `parseInbound` judges separately from
|
|
352
|
+
* the payload for exactly this reason — a frame with a good envelope carries everything a reply
|
|
353
|
+
* needs. `reason: 'malformed'` is not a new member; the input vocabulary already had it, and until
|
|
354
|
+
* now only agents produced it.
|
|
355
|
+
*
|
|
356
|
+
* No ownership check, deliberately: the reply goes to the socket that sent the frame and says only
|
|
357
|
+
* that its own message was malformed, so it discloses nothing about the session. Every other refusal
|
|
358
|
+
* in this file answers a question about the session's state and is gated for that reason.
|
|
359
|
+
*/
|
|
360
|
+
private refuseMalformed;
|
|
361
|
+
/** Answers the sender in the shape its waiter is keyed on.
|
|
362
|
+
*
|
|
363
|
+
* `input:type` needs `input:type-error`, not `input:error` — its waiters in `mcp-server` and
|
|
364
|
+
* `flow-runner` match on the `input:type-*` pair and ignore an `input:error` entirely. That is why
|
|
365
|
+
* adding `input:type` to the terminal set was never the fix for it, and answering it properly here is
|
|
366
|
+
* closes the gap `relay/AGENTS.md` recorded: before L5c an `input:type` on an offline agent got nothing
|
|
367
|
+
* and burned its caller's full deadline. */
|
|
368
|
+
private refuseInput;
|
|
369
|
+
/** Relay looks up file_path from DB and enriches it for the agent.
|
|
370
|
+
*
|
|
371
|
+
* Every exit carries the request's `sessionId`, including the failures. A dashboard viewer holds
|
|
372
|
+
* one session per socket, so an uncorrelated error still lands somewhere sensible — but an MCP
|
|
373
|
+
* caller waits for the reply matching its own sessionId, so anything else is indistinguishable
|
|
374
|
+
* from silence and it waits out the deadline (#445). `Session not found` is app-specific for the
|
|
375
|
+
* same reason: a generic `error` cannot be correlated by construction. */
|
|
66
376
|
private handleBrowserAppInstall;
|
|
377
|
+
/** Relay looks up bundle_id from DB. Same correlation rules as `handleBrowserAppInstall`. */
|
|
67
378
|
private handleBrowserAppLaunch;
|
|
68
379
|
private handleGetScreenshot;
|
|
69
380
|
private handleGetUITree;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RelayServer.d.ts","sourceRoot":"","sources":["../src/RelayServer.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"RelayServer.d.ts","sourceRoot":"","sources":["../src/RelayServer.ts"],"names":[],"mappings":"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"}
|