@tapflowio/relay 0.20.1 → 0.22.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/dist/RelayServer.d.ts +33 -0
- package/dist/RelayServer.d.ts.map +1 -1
- package/dist/RelayServer.js +173 -26
- package/dist/RelayServer.js.map +1 -1
- package/dist/api/team.d.ts +2 -1
- package/dist/api/team.d.ts.map +1 -1
- package/dist/api/team.js +7 -5
- package/dist/api/team.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/buildTickets.d.ts +50 -0
- package/dist/lib/buildTickets.d.ts.map +1 -0
- package/dist/lib/buildTickets.js +47 -0
- package/dist/lib/buildTickets.js.map +1 -0
- package/dist/lib/lanAddress.d.ts +10 -0
- package/dist/lib/lanAddress.d.ts.map +1 -1
- package/dist/lib/lanAddress.js +13 -0
- package/dist/lib/lanAddress.js.map +1 -1
- package/dist/lib/proxyConfig.d.ts +4 -2
- package/dist/lib/proxyConfig.d.ts.map +1 -1
- package/dist/lib/proxyConfig.js +22 -13
- package/dist/lib/proxyConfig.js.map +1 -1
- package/dist/lib/publicUrl.d.ts +31 -1
- package/dist/lib/publicUrl.d.ts.map +1 -1
- package/dist/lib/publicUrl.js +51 -9
- package/dist/lib/publicUrl.js.map +1 -1
- package/dist/server.js +7 -1
- package/dist/server.js.map +1 -1
- package/package.json +4 -4
- package/public/assets/{AppCenter-DtF_g4F7.js → AppCenter-gbsJscjW.js} +5 -5
- package/public/assets/AppCenter-gbsJscjW.js.br +0 -0
- package/public/assets/AppCenter-gbsJscjW.js.gz +0 -0
- package/public/assets/{Default-C_p-ZWS6.js → Default-2rFTkbo9.js} +1 -1
- package/public/assets/Default-2rFTkbo9.js.br +0 -0
- package/public/assets/Default-2rFTkbo9.js.gz +0 -0
- package/public/assets/{Invite-DKT4qT6M.js → Invite-aN4DXJyM.js} +1 -1
- package/public/assets/Invite-aN4DXJyM.js.br +0 -0
- package/public/assets/Invite-aN4DXJyM.js.gz +0 -0
- package/public/assets/MacResources-D3IlesXU.js +3 -0
- package/public/assets/MacResources-D3IlesXU.js.br +0 -0
- package/public/assets/MacResources-D3IlesXU.js.gz +0 -0
- package/public/assets/NotFound-BoDDivK5.js.gz +0 -0
- package/public/assets/QASession-U29t9j0E.js +140 -0
- package/public/assets/QASession-U29t9j0E.js.br +0 -0
- package/public/assets/QASession-U29t9j0E.js.gz +0 -0
- package/public/assets/{ResetPassword-BnmE9MfY.js → ResetPassword-DPc5kwQU.js} +1 -1
- package/public/assets/ResetPassword-DPc5kwQU.js.br +0 -0
- package/public/assets/ResetPassword-DPc5kwQU.js.gz +0 -0
- package/public/assets/{Setup-I6nFcwNZ.js → Setup-CwPx_e_O.js} +1 -1
- package/public/assets/Setup-CwPx_e_O.js.br +0 -0
- package/public/assets/Setup-CwPx_e_O.js.gz +0 -0
- package/public/assets/Team-DD9li-7M.js +6 -0
- package/public/assets/Team-DD9li-7M.js.br +0 -0
- package/public/assets/Team-DD9li-7M.js.gz +0 -0
- package/public/assets/Tokens-DdMMke73.js +1 -0
- package/public/assets/Tokens-DdMMke73.js.br +0 -0
- package/public/assets/Tokens-DdMMke73.js.gz +0 -0
- package/public/assets/{alert-dialog-CLhPOSdB.js → alert-dialog-DADd1jHN.js} +1 -1
- package/public/assets/alert-dialog-DADd1jHN.js.br +0 -0
- package/public/assets/alert-dialog-DADd1jHN.js.gz +0 -0
- package/public/assets/{badge-CP-xbjo3.js → badge-CMf8d5RJ.js} +1 -1
- package/public/assets/badge-CMf8d5RJ.js.br +0 -0
- package/public/assets/badge-CMf8d5RJ.js.gz +0 -0
- package/public/assets/{build-format-DO1zN4vW.js → build-format-lp_8kSvq.js} +3 -3
- package/public/assets/build-format-lp_8kSvq.js.br +0 -0
- package/public/assets/build-format-lp_8kSvq.js.gz +0 -0
- package/public/assets/{dialog-BGH9MPcH.js → dialog-pS8vHXXv.js} +3 -3
- package/public/assets/dialog-pS8vHXXv.js.br +0 -0
- package/public/assets/dialog-pS8vHXXv.js.gz +0 -0
- package/public/assets/index-Bxs7CdEi.css +1 -0
- package/public/assets/index-Bxs7CdEi.css.br +0 -0
- package/public/assets/index-Bxs7CdEi.css.gz +0 -0
- package/public/assets/{index-O-Temo_p.js → index-Cajf0T-5.js} +23 -23
- package/public/assets/index-Cajf0T-5.js.br +0 -0
- package/public/assets/index-Cajf0T-5.js.gz +0 -0
- package/public/assets/{pencil-0UYCdq7e.js → pencil-LD1ga-Kk.js} +2 -2
- package/public/assets/pencil-LD1ga-Kk.js.br +0 -0
- package/public/assets/pencil-LD1ga-Kk.js.gz +0 -0
- package/public/assets/publicLink-D6zKS_wu.js +1 -0
- package/public/assets/publicLink-D6zKS_wu.js.br +0 -0
- package/public/assets/publicLink-D6zKS_wu.js.gz +0 -0
- package/public/assets/resource-health-BScurILF.js +1 -0
- package/public/assets/resource-health-BScurILF.js.br +0 -0
- package/public/assets/resource-health-BScurILF.js.gz +0 -0
- package/public/assets/{table-4DnBixRm.js → table-CL9AvoTp.js} +1 -1
- package/public/assets/table-CL9AvoTp.js.br +0 -0
- package/public/assets/table-CL9AvoTp.js.gz +0 -0
- package/public/assets/tinyh264.worker-DiJ50Hai.js.gz +0 -0
- package/public/assets/{trash-2-CAhMuWgg.js → trash-mvVp4nwq.js} +4 -4
- package/public/assets/trash-mvVp4nwq.js.br +0 -0
- package/public/assets/trash-mvVp4nwq.js.gz +0 -0
- package/public/assets/vendor-charts-8kVeHLwY.js +5 -0
- package/public/assets/vendor-charts-8kVeHLwY.js.br +0 -0
- package/public/assets/vendor-charts-8kVeHLwY.js.gz +0 -0
- package/public/assets/vendor-react-AJpf4YZ0.js.gz +0 -0
- package/public/assets/vendor-router-BFibuab1.js.gz +0 -0
- package/public/favicon.svg.gz +0 -0
- package/public/index.html +2 -2
- package/public/index.html.br +0 -0
- package/public/index.html.gz +0 -0
- package/public/logo-dark.svg.gz +0 -0
- package/public/logo.svg.gz +0 -0
- package/public/assets/AppCenter-DtF_g4F7.js.br +0 -0
- package/public/assets/Default-C_p-ZWS6.js.br +0 -0
- package/public/assets/Invite-DKT4qT6M.js.br +0 -0
- package/public/assets/MacResources-D8T_KpPV.js +0 -1
- package/public/assets/MacResources-D8T_KpPV.js.br +0 -0
- package/public/assets/QASession-Daup7RGP.js +0 -140
- package/public/assets/QASession-Daup7RGP.js.br +0 -0
- package/public/assets/ResetPassword-BnmE9MfY.js.br +0 -0
- package/public/assets/Setup-I6nFcwNZ.js.br +0 -0
- package/public/assets/Team-JhXhGt1H.js +0 -6
- package/public/assets/Team-JhXhGt1H.js.br +0 -0
- package/public/assets/Tokens-KAKIDZ0V.js +0 -1
- package/public/assets/Tokens-KAKIDZ0V.js.br +0 -0
- package/public/assets/alert-dialog-CLhPOSdB.js.br +0 -0
- package/public/assets/badge-CP-xbjo3.js.br +0 -0
- package/public/assets/build-format-DO1zN4vW.js.br +0 -0
- package/public/assets/dialog-BGH9MPcH.js.br +0 -0
- package/public/assets/index-ClHUQm-F.css +0 -1
- package/public/assets/index-ClHUQm-F.css.br +0 -0
- package/public/assets/index-O-Temo_p.js.br +0 -0
- package/public/assets/pencil-0UYCdq7e.js.br +0 -0
- package/public/assets/table-4DnBixRm.js.br +0 -0
- package/public/assets/tabs-DwiCx3dV.js +0 -1
- package/public/assets/tabs-DwiCx3dV.js.br +0 -0
- package/public/assets/trash-2-CAhMuWgg.js.br +0 -0
- package/public/assets/vendor-charts-DyX5CBcf.js +0 -5
- package/public/assets/vendor-charts-DyX5CBcf.js.br +0 -0
package/dist/RelayServer.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type TunnelRuntime } from './lib/publicUrl.js';
|
|
1
2
|
export declare function isExternalAddress(addr: string): boolean;
|
|
2
3
|
/** Exported for the re-join test, which pins both edges of this window rather than hardcoding 500 —
|
|
3
4
|
* a test carrying its own copy of the number passes when the constant moves, which is the drift the
|
|
@@ -91,6 +92,9 @@ export declare class RelayServer {
|
|
|
91
92
|
private readonly agentGraceMs;
|
|
92
93
|
private readonly corsAllowed;
|
|
93
94
|
private warnedProxyMisconfig;
|
|
95
|
+
/** Agent identities already warned about a missing `build-download`. See `warnLegacyInstaller`. */
|
|
96
|
+
private readonly warnedLegacyInstaller;
|
|
97
|
+
private readonly buildTickets;
|
|
94
98
|
private pendingScreenshots;
|
|
95
99
|
private readonly uiTreeTimeoutMs;
|
|
96
100
|
private pendingUITrees;
|
|
@@ -109,6 +113,7 @@ export declare class RelayServer {
|
|
|
109
113
|
key: string;
|
|
110
114
|
};
|
|
111
115
|
agentGraceMs?: number;
|
|
116
|
+
tunnel?: TunnelRuntime;
|
|
112
117
|
});
|
|
113
118
|
private registerRoutes;
|
|
114
119
|
pushLog(msg: string): void;
|
|
@@ -410,6 +415,20 @@ export declare class RelayServer {
|
|
|
410
415
|
* from silence and it waits out the deadline (#445). `Session not found` is app-specific for the
|
|
411
416
|
* same reason: a generic `error` cannot be correlated by construction. */
|
|
412
417
|
private handleBrowserAppInstall;
|
|
418
|
+
/**
|
|
419
|
+
* Says once, per agent, that this agent cannot fetch builds over HTTP.
|
|
420
|
+
*
|
|
421
|
+
* **The wording is conditional on purpose.** Such an agent installs perfectly well when it shares
|
|
422
|
+
* the relay's filesystem, which is every `tapflow start`, so "installs will fail" would be false
|
|
423
|
+
* for most of the people who see it. What it cannot do is install from a relay that is somewhere
|
|
424
|
+
* else — and the relay cannot tell which case this is: the dashboard hands agents a **LAN**
|
|
425
|
+
* address even for the same machine (`pickLanAddress`), so the socket's own address answers a
|
|
426
|
+
* different question than the one that matters.
|
|
427
|
+
*
|
|
428
|
+
* Once per identity, because `agent:register` runs again on every reconnect — a Wi-Fi blip or a
|
|
429
|
+
* laptop waking up would otherwise repeat this indefinitely. Same shape as `warnedProxyMisconfig`.
|
|
430
|
+
*/
|
|
431
|
+
private warnLegacyInstaller;
|
|
413
432
|
/** Relay looks up bundle_id from DB. Same correlation rules as `handleBrowserAppInstall`. */
|
|
414
433
|
private handleBrowserAppLaunch;
|
|
415
434
|
private handleGetScreenshot;
|
|
@@ -420,6 +439,20 @@ export declare class RelayServer {
|
|
|
420
439
|
private handleScreenshotError;
|
|
421
440
|
private flushResourceBuffers;
|
|
422
441
|
private handleRequest;
|
|
442
|
+
/**
|
|
443
|
+
* Serves one build to the agent that was just told to install it.
|
|
444
|
+
*
|
|
445
|
+
* **The ticket is the whole credential, and it travels in a header.** No PAT is asked for —
|
|
446
|
+
* requiring one would break `tapflow start`, whose agent connects on loopback with no token at
|
|
447
|
+
* all. In a URL the ticket would reach the request log the first time this handler threw
|
|
448
|
+
* (`router.ts` logs `${method} ${url}`, and its redaction only knows PAT and JWT shapes) and every
|
|
449
|
+
* reverse proxy's access log besides.
|
|
450
|
+
*
|
|
451
|
+
* The three failures are answered separately because a user does something different about each:
|
|
452
|
+
* a ticket nobody minted, a ticket that sat too long, and a file that went away between the mint
|
|
453
|
+
* and the fetch. Collapsing them is the defect this change exists to stop repeating.
|
|
454
|
+
*/
|
|
455
|
+
private handleBuildDownload;
|
|
423
456
|
private serveUpload;
|
|
424
457
|
private serveStatic;
|
|
425
458
|
}
|
|
@@ -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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"}
|
package/dist/RelayServer.js
CHANGED
|
@@ -11,9 +11,12 @@ import { Router, json } from './router.js';
|
|
|
11
11
|
import { requireViewAuth, requireAuth, getAuth, verifyPat } from './middleware/auth.js';
|
|
12
12
|
import { classifyConnection } from './lib/connectionAuth.js';
|
|
13
13
|
import { resolveClientAddress } from './lib/clientAddress.js';
|
|
14
|
+
import { BuildTicketStore } from './lib/buildTickets.js';
|
|
14
15
|
import { resolveCorsHeaders } from './lib/cors.js';
|
|
15
16
|
import { isCsrfBlocked } from './lib/csrf.js';
|
|
16
|
-
import { pickLanAddress } from './lib/lanAddress.js';
|
|
17
|
+
import { pickLanAddress, runningInContainer } from './lib/lanAddress.js';
|
|
18
|
+
import { config } from './lib/config.js';
|
|
19
|
+
import { forTeammates, resolveAgentRelayUrl, resolvePublicBaseUrl } from './lib/publicUrl.js';
|
|
17
20
|
import { createTrailingRequester, systemTimerScheduler } from './lib/trailingRequester.js';
|
|
18
21
|
import { getDb } from './db.js';
|
|
19
22
|
import { handleLogin, handleLogout, handleMe, handleChangePassword, handleInit, handleAuthStatus } from './api/auth.js';
|
|
@@ -229,6 +232,9 @@ export class RelayServer {
|
|
|
229
232
|
corsAllowed;
|
|
230
233
|
// One-shot warning when XFF arrives on a loopback socket but TAPFLOW_TRUSTED_PROXIES is unset.
|
|
231
234
|
warnedProxyMisconfig = false;
|
|
235
|
+
/** Agent identities already warned about a missing `build-download`. See `warnLegacyInstaller`. */
|
|
236
|
+
warnedLegacyInstaller = new Set();
|
|
237
|
+
buildTickets = new BuildTicketStore();
|
|
232
238
|
pendingScreenshots = new Map();
|
|
233
239
|
uiTreeTimeoutMs;
|
|
234
240
|
pendingUITrees = new Map();
|
|
@@ -301,6 +307,12 @@ export class RelayServer {
|
|
|
301
307
|
this.router.post('/api/v1/builds/:id/schedule-deletion', handleScheduleBuildDeletion);
|
|
302
308
|
this.router.delete('/api/v1/builds/:id/schedule-deletion', handleCancelBuildDeletion);
|
|
303
309
|
this.router.post('/api/v1/builds', (req, res) => handleUploadBuild(req, res, u));
|
|
310
|
+
// **Deliberately not under `/api/v1/builds/`.** `:id` compiles to `([^/]+)` and the router
|
|
311
|
+
// returns on the first registered match, so `/api/v1/builds/download` is captured by
|
|
312
|
+
// `/api/v1/builds/:id` as `id="download"` — `requireBuildAuth` then answers 401 to a ticketed
|
|
313
|
+
// agent carrying no PAT. Registration order could avoid it, which is exactly the objection:
|
|
314
|
+
// that makes the order load-bearing with nothing to say so.
|
|
315
|
+
this.router.get('/api/v1/build-download', (req, res) => this.handleBuildDownload(req, res));
|
|
304
316
|
// webhooks (outbound build-status notifications)
|
|
305
317
|
this.router.get('/api/v1/webhooks', handleListWebhooks);
|
|
306
318
|
this.router.post('/api/v1/webhooks', handleCreateWebhook);
|
|
@@ -312,7 +324,7 @@ export class RelayServer {
|
|
|
312
324
|
this.router.delete('/api/v1/comments/:id', handleDeleteComment);
|
|
313
325
|
// team
|
|
314
326
|
this.router.get('/api/v1/team/members', handleListMembers);
|
|
315
|
-
this.router.post('/api/v1/team/invite', handleInvite);
|
|
327
|
+
this.router.post('/api/v1/team/invite', (req, res) => handleInvite(req, res, this.options.tunnel));
|
|
316
328
|
this.router.patch('/api/v1/team/members/:id', handleUpdateMember);
|
|
317
329
|
this.router.delete('/api/v1/team/members/:id', handleDeleteMember);
|
|
318
330
|
this.router.post('/api/v1/team/members/:id/send-reset', handleSendMemberReset);
|
|
@@ -336,14 +348,22 @@ export class RelayServer {
|
|
|
336
348
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
337
349
|
res.end(JSON.stringify(this.logBuffer.slice(-lines)));
|
|
338
350
|
});
|
|
339
|
-
//
|
|
351
|
+
// What the dashboard needs to build an address for someone else (#271, #788): a link base for a
|
|
352
|
+
// teammate's browser and an agent address, reported separately because they come from different
|
|
353
|
+
// settings (see lib/publicUrl.ts). Config comes from the singleton, as in the team handlers; whether a
|
|
354
|
+
// tunnel actually came up is known only to the entry point that started it, hence the `tunnel` option.
|
|
340
355
|
this.router.get('/api/v1/relay/host', (req, res) => {
|
|
341
356
|
if (!requireAuth(req, res))
|
|
342
357
|
return;
|
|
343
358
|
const addr = this.httpServer.address();
|
|
344
359
|
const port = typeof addr === 'object' && addr !== null ? addr.port : this.options.port;
|
|
345
360
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
346
|
-
res.end(JSON.stringify({
|
|
361
|
+
res.end(JSON.stringify({
|
|
362
|
+
lanHost: runningInContainer() ? null : pickLanAddress(os.networkInterfaces()),
|
|
363
|
+
port,
|
|
364
|
+
publicBaseUrl: forTeammates(resolvePublicBaseUrl(config, this.options.tunnel)),
|
|
365
|
+
agentRelayUrl: forTeammates(resolveAgentRelayUrl(config)),
|
|
366
|
+
}));
|
|
347
367
|
});
|
|
348
368
|
// agent resources
|
|
349
369
|
this.router.get('/api/v1/agents', handleListAgents);
|
|
@@ -1418,6 +1438,7 @@ export class RelayServer {
|
|
|
1418
1438
|
// its socket before creating the new ones. Identity is agentId (unique per Mac) when present,
|
|
1419
1439
|
// else agentName. (Heartbeat backstop for never-reconnecting agents: #313.)
|
|
1420
1440
|
const identity = msg.agentId ?? msg.agentName;
|
|
1441
|
+
this.warnLegacyInstaller(identity, msg.capabilities ?? []);
|
|
1421
1442
|
// Deduplicate first. Everything below is keyed by device id, so a payload naming one device
|
|
1422
1443
|
// twice would collapse to a single entry in `registeredSessions` while `create()` had already
|
|
1423
1444
|
// made two sessions — leaving one the agent is never told about. That is the same orphan the
|
|
@@ -1956,13 +1977,56 @@ export class RelayServer {
|
|
|
1956
1977
|
// agent's reply is forwarded back generically without the relay looking at it. So if the id does not
|
|
1957
1978
|
// reach the agent, nothing downstream can attribute the reply. Nothing type-checks that the value is
|
|
1958
1979
|
// the *request's*: a brand cannot express provenance, so a test carries it.
|
|
1980
|
+
// **`statSync`, and this handler stays synchronous.** Its caller does not await it
|
|
1981
|
+
// (`case 'app:install'` above), and the message loop's `catch` is synchronous — so making this
|
|
1982
|
+
// `async` would turn the `getDb()` throw named in the comment at the top of this function
|
|
1983
|
+
// (SQLITE_BUSY, a closed db, I/O) from "logged, one request hangs" into an unhandled rejection,
|
|
1984
|
+
// and the CLI exits the process on those. The relay would die with every session on it.
|
|
1985
|
+
//
|
|
1986
|
+
// A missing file is answered rather than thrown for the same reason the lookup above is: the
|
|
1987
|
+
// build row outlives its file whenever a bind mount goes away or a purge half-ran, and an
|
|
1988
|
+
// unanswered install is a spinner that never stops and a caller that times out with no cause.
|
|
1989
|
+
let bytes;
|
|
1990
|
+
try {
|
|
1991
|
+
bytes = fs.statSync(build.file_path).size;
|
|
1992
|
+
}
|
|
1993
|
+
catch {
|
|
1994
|
+
return fail('The relay cannot read this build file. It may have been deleted — re-upload the build.');
|
|
1995
|
+
}
|
|
1959
1996
|
this.sendTo(session.agentSocket, {
|
|
1960
1997
|
type: 'app:install',
|
|
1961
1998
|
sessionId,
|
|
1962
1999
|
requestId,
|
|
1963
|
-
payload: {
|
|
2000
|
+
payload: {
|
|
2001
|
+
// Still sent, and sent first: an agent that predates `build-download` reads only this.
|
|
2002
|
+
filePath: build.file_path,
|
|
2003
|
+
bundleId: build.bundle_id,
|
|
2004
|
+
buildTicket: this.buildTickets.mint(msg.buildId, build.file_path),
|
|
2005
|
+
buildName: path.basename(build.file_path),
|
|
2006
|
+
buildBytes: bytes,
|
|
2007
|
+
},
|
|
1964
2008
|
});
|
|
1965
2009
|
}
|
|
2010
|
+
/**
|
|
2011
|
+
* Says once, per agent, that this agent cannot fetch builds over HTTP.
|
|
2012
|
+
*
|
|
2013
|
+
* **The wording is conditional on purpose.** Such an agent installs perfectly well when it shares
|
|
2014
|
+
* the relay's filesystem, which is every `tapflow start`, so "installs will fail" would be false
|
|
2015
|
+
* for most of the people who see it. What it cannot do is install from a relay that is somewhere
|
|
2016
|
+
* else — and the relay cannot tell which case this is: the dashboard hands agents a **LAN**
|
|
2017
|
+
* address even for the same machine (`pickLanAddress`), so the socket's own address answers a
|
|
2018
|
+
* different question than the one that matters.
|
|
2019
|
+
*
|
|
2020
|
+
* Once per identity, because `agent:register` runs again on every reconnect — a Wi-Fi blip or a
|
|
2021
|
+
* laptop waking up would otherwise repeat this indefinitely. Same shape as `warnedProxyMisconfig`.
|
|
2022
|
+
*/
|
|
2023
|
+
warnLegacyInstaller(identity, capabilities) {
|
|
2024
|
+
if (capabilities.includes('build-download') || this.warnedLegacyInstaller.has(identity))
|
|
2025
|
+
return;
|
|
2026
|
+
this.warnedLegacyInstaller.add(identity);
|
|
2027
|
+
logger.warn(`[relay] agent ${identity} predates build downloading. If it is not on the same machine as ` +
|
|
2028
|
+
'this relay, installing a build will fail — update the agent to match the relay.');
|
|
2029
|
+
}
|
|
1966
2030
|
/** Relay looks up bundle_id from DB. Same correlation rules as `handleBrowserAppInstall`. */
|
|
1967
2031
|
handleBrowserAppLaunch(ws, msg) {
|
|
1968
2032
|
const sessionId = msg.sessionId;
|
|
@@ -2180,6 +2244,52 @@ export class RelayServer {
|
|
|
2180
2244
|
// SPA static fallback
|
|
2181
2245
|
this.serveStatic(req, res);
|
|
2182
2246
|
}
|
|
2247
|
+
/**
|
|
2248
|
+
* Serves one build to the agent that was just told to install it.
|
|
2249
|
+
*
|
|
2250
|
+
* **The ticket is the whole credential, and it travels in a header.** No PAT is asked for —
|
|
2251
|
+
* requiring one would break `tapflow start`, whose agent connects on loopback with no token at
|
|
2252
|
+
* all. In a URL the ticket would reach the request log the first time this handler threw
|
|
2253
|
+
* (`router.ts` logs `${method} ${url}`, and its redaction only knows PAT and JWT shapes) and every
|
|
2254
|
+
* reverse proxy's access log besides.
|
|
2255
|
+
*
|
|
2256
|
+
* The three failures are answered separately because a user does something different about each:
|
|
2257
|
+
* a ticket nobody minted, a ticket that sat too long, and a file that went away between the mint
|
|
2258
|
+
* and the fetch. Collapsing them is the defect this change exists to stop repeating.
|
|
2259
|
+
*/
|
|
2260
|
+
handleBuildDownload(req, res) {
|
|
2261
|
+
const ticket = req.headers['x-tapflow-build-ticket'];
|
|
2262
|
+
if (typeof ticket !== 'string' || ticket === '') {
|
|
2263
|
+
return json(res, 401, { error: 'Missing X-Tapflow-Build-Ticket' });
|
|
2264
|
+
}
|
|
2265
|
+
const found = this.buildTickets.redeem(ticket);
|
|
2266
|
+
if (found === 'unknown') {
|
|
2267
|
+
return json(res, 404, { error: 'This build ticket is not valid. It may already have been used.' });
|
|
2268
|
+
}
|
|
2269
|
+
if (found === 'expired') {
|
|
2270
|
+
return json(res, 410, { error: 'This build ticket expired before it was used.' });
|
|
2271
|
+
}
|
|
2272
|
+
let size;
|
|
2273
|
+
try {
|
|
2274
|
+
size = fs.statSync(found.filePath).size;
|
|
2275
|
+
}
|
|
2276
|
+
catch {
|
|
2277
|
+
return json(res, 404, { error: 'The build file is no longer on the relay.' });
|
|
2278
|
+
}
|
|
2279
|
+
// Set even though the agent compares against `buildBytes` instead: a correct header costs
|
|
2280
|
+
// nothing and helps anything else that looks. It is not the check — a proxy may drop it.
|
|
2281
|
+
res.writeHead(200, { 'Content-Type': 'application/octet-stream', 'Content-Length': size });
|
|
2282
|
+
// **`pipe` does not destroy its source when the destination goes away** — it only unpipes — so an
|
|
2283
|
+
// aborted download leaves the read stream and its file descriptor open for the life of the
|
|
2284
|
+
// process. Measured: after a client abort, `rs.destroyed` is still false and the fd still stats.
|
|
2285
|
+
// On this route an abort is the *ordinary* failure, not an edge case: the agent's own idle
|
|
2286
|
+
// timeout destroys the request, and so does an agent that exits or a link that drops. Enough of
|
|
2287
|
+
// them and the relay hits its descriptor limit and stops accepting connections at all.
|
|
2288
|
+
const rs = fs.createReadStream(found.filePath);
|
|
2289
|
+
res.on('close', () => { rs.destroy(); });
|
|
2290
|
+
rs.on('error', () => { res.destroy(); });
|
|
2291
|
+
rs.pipe(res);
|
|
2292
|
+
}
|
|
2183
2293
|
serveUpload(req, res) {
|
|
2184
2294
|
const urlPath = (req.url ?? '/').split('?')[0];
|
|
2185
2295
|
const filePath = path.join(this.uploadsDir, urlPath.replace('/uploads/', ''));
|
|
@@ -2196,9 +2306,11 @@ export class RelayServer {
|
|
|
2196
2306
|
}
|
|
2197
2307
|
const contentType = MIME_TYPES[path.extname(filePath)] ?? 'application/octet-stream';
|
|
2198
2308
|
res.writeHead(200, { 'Content-Type': contentType });
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2309
|
+
// Same leak as `handleBuildDownload` above, and the same one line closes it.
|
|
2310
|
+
const rs = fs.createReadStream(filePath);
|
|
2311
|
+
res.on('close', () => { rs.destroy(); });
|
|
2312
|
+
rs.on('error', () => { res.destroy(); });
|
|
2313
|
+
rs.pipe(res);
|
|
2202
2314
|
}
|
|
2203
2315
|
serveStatic(req, res) {
|
|
2204
2316
|
const urlPath = (req.url ?? '/').split('?')[0];
|
|
@@ -2222,34 +2334,69 @@ export class RelayServer {
|
|
|
2222
2334
|
const contentType = MIME_TYPES[path.extname(filePath)] ?? 'text/html';
|
|
2223
2335
|
const headers = { 'Content-Type': contentType };
|
|
2224
2336
|
// Content-hashed build assets never change → cache them forever.
|
|
2337
|
+
// HTML entry points must revalidate on every load so updates are picked up immediately.
|
|
2225
2338
|
if (urlPath.startsWith('/assets/')) {
|
|
2226
2339
|
headers['Cache-Control'] = 'public, max-age=31536000, immutable';
|
|
2227
2340
|
}
|
|
2228
|
-
|
|
2341
|
+
else if (filePath.endsWith('index.html')) {
|
|
2342
|
+
headers['Cache-Control'] = 'no-cache';
|
|
2343
|
+
}
|
|
2344
|
+
// Serve precompressed siblings (.br or .gz) when accepted (precompressed → no runtime CPU on the stream path).
|
|
2229
2345
|
const acceptHeader = req.headers['accept-encoding'];
|
|
2230
2346
|
const accept = Array.isArray(acceptHeader) ? acceptHeader.join(',') : acceptHeader ?? '';
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2347
|
+
// **An explicit coding outranks `*`, rather than being maxed with it.** RFC 9110 §12.5.3 gives
|
|
2348
|
+
// the more specific match precedence, and taking the maximum across both meant
|
|
2349
|
+
// `Accept-Encoding: br;q=0, *` answered 1 for `br` — serving brotli to a client that named it
|
|
2350
|
+
// and refused it. The two are tracked apart and the wildcard is consulted only when the coding
|
|
2351
|
+
// was never named.
|
|
2352
|
+
const parseQuality = (codingName) => {
|
|
2353
|
+
let named = -1;
|
|
2354
|
+
let wildcard = -1;
|
|
2355
|
+
for (const token of accept.split(',')) {
|
|
2356
|
+
const [name, ...params] = token.trim().split(';');
|
|
2357
|
+
const coding = name.trim().toLowerCase();
|
|
2358
|
+
if (coding !== codingName && coding !== '*')
|
|
2359
|
+
continue;
|
|
2360
|
+
// Lowercased because RFC 9110 §5.6.6 makes parameter names case-insensitive: `br;Q=0` was
|
|
2361
|
+
// missed, fell through to the default weight of 1, and served brotli to a client that had
|
|
2362
|
+
// refused it. The qvalue itself is numeric, so folding the whole token is safe.
|
|
2363
|
+
const qParam = params.map((p) => p.trim().toLowerCase()).find((p) => p.startsWith('q='));
|
|
2364
|
+
const q = qParam ? Number(qParam.slice(2)) : 1;
|
|
2365
|
+
if (Number.isNaN(q))
|
|
2366
|
+
continue;
|
|
2367
|
+
if (coding === '*')
|
|
2368
|
+
wildcard = Math.max(wildcard, q);
|
|
2369
|
+
else
|
|
2370
|
+
named = Math.max(named, q);
|
|
2371
|
+
}
|
|
2372
|
+
return named >= 0 ? named : wildcard;
|
|
2373
|
+
};
|
|
2374
|
+
const brQ = parseQuality('br');
|
|
2375
|
+
const gzQ = Math.max(parseQuality('gzip'), parseQuality('x-gzip'));
|
|
2376
|
+
// **`Vary` unconditionally, and that is what lets the probes below be skipped.** It declares a
|
|
2377
|
+
// cache key rather than describing this response: a shared cache that stored the uncompressed
|
|
2378
|
+
// body without it would hand that body to the next client, which may accept brotli. Deriving it
|
|
2379
|
+
// from "does a sibling exist" is what forced a `stat` on every static request even when the
|
|
2380
|
+
// client asked for no encoding at all — a syscall per asset per request, on the path this exists
|
|
2381
|
+
// to make cheaper. Set it always, and ask the filesystem only about codings that could be served.
|
|
2382
|
+
headers['Vary'] = 'Accept-Encoding';
|
|
2383
|
+
const hasBr = brQ > 0 && fs.existsSync(filePath + '.br');
|
|
2384
|
+
const hasGz = gzQ > 0 && fs.existsSync(filePath + '.gz');
|
|
2240
2385
|
let servePath = filePath;
|
|
2241
|
-
|
|
2242
|
-
// Vary whenever a compressed variant exists, even if raw is served, so caches don't cross-serve.
|
|
2243
|
-
if (hasBr)
|
|
2244
|
-
headers['Vary'] = 'Accept-Encoding';
|
|
2245
|
-
if (brAccepted && hasBr) {
|
|
2386
|
+
if (brQ > 0 && hasBr && (gzQ <= 0 || !hasGz || brQ >= gzQ)) {
|
|
2246
2387
|
servePath = filePath + '.br';
|
|
2247
2388
|
headers['Content-Encoding'] = 'br';
|
|
2248
2389
|
}
|
|
2390
|
+
else if (gzQ > 0 && hasGz) {
|
|
2391
|
+
servePath = filePath + '.gz';
|
|
2392
|
+
headers['Content-Encoding'] = 'gzip';
|
|
2393
|
+
}
|
|
2249
2394
|
res.writeHead(200, headers);
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2395
|
+
// Same leak as `handleBuildDownload` above, and the same one line closes it.
|
|
2396
|
+
const rs = fs.createReadStream(servePath);
|
|
2397
|
+
res.on('close', () => { rs.destroy(); });
|
|
2398
|
+
rs.on('error', () => { res.destroy(); });
|
|
2399
|
+
rs.pipe(res);
|
|
2253
2400
|
}
|
|
2254
2401
|
}
|
|
2255
2402
|
//# sourceMappingURL=RelayServer.js.map
|