@getpaseo/server 0.1.87 → 0.1.89

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.
Files changed (69) hide show
  1. package/dist/server/server/agent/agent-manager.js +4 -1
  2. package/dist/server/server/agent/agent-storage.d.ts +22 -22
  3. package/dist/server/server/agent/create-agent/create.d.ts +2 -0
  4. package/dist/server/server/agent/create-agent/create.js +16 -5
  5. package/dist/server/server/agent/create-agent-lifecycle-dispatch.d.ts +1 -0
  6. package/dist/server/server/agent/create-agent-lifecycle-dispatch.js +4 -0
  7. package/dist/server/server/agent/mcp-server.d.ts +1 -0
  8. package/dist/server/server/agent/mcp-server.js +137 -63
  9. package/dist/server/server/agent/mcp-shared.d.ts +1 -0
  10. package/dist/server/server/agent/providers/pi/agent.js +13 -0
  11. package/dist/server/server/agent/providers/pi/rpc-types.d.ts +3 -0
  12. package/dist/server/server/agent/timeline-projection.d.ts +17 -1
  13. package/dist/server/server/agent/timeline-projection.js +82 -17
  14. package/dist/server/server/auto-archive-on-merge/archive-if-safe.d.ts +1 -0
  15. package/dist/server/server/auto-archive-on-merge/archive-if-safe.js +6 -1
  16. package/dist/server/server/bootstrap.d.ts +7 -2
  17. package/dist/server/server/bootstrap.js +152 -115
  18. package/dist/server/server/config.js +41 -0
  19. package/dist/server/server/loop-service.d.ts +22 -22
  20. package/dist/server/server/package-version.d.ts +2 -2
  21. package/dist/server/server/paseo-worktree-archive-service.d.ts +2 -0
  22. package/dist/server/server/paseo-worktree-archive-service.js +28 -9
  23. package/dist/server/server/persisted-config.d.ts +89 -33
  24. package/dist/server/server/persisted-config.js +17 -0
  25. package/dist/server/server/pid-lock.d.ts +2 -2
  26. package/dist/server/server/schedule/cron.js +52 -5
  27. package/dist/server/server/script-health-monitor.d.ts +4 -4
  28. package/dist/server/server/script-health-monitor.js +6 -6
  29. package/dist/server/server/script-proxy.d.ts +2 -39
  30. package/dist/server/server/script-proxy.js +1 -244
  31. package/dist/server/server/script-route-branch-handler.d.ts +2 -2
  32. package/dist/server/server/script-route-branch-handler.js +3 -37
  33. package/dist/server/server/script-status-projection.d.ts +6 -4
  34. package/dist/server/server/script-status-projection.js +85 -37
  35. package/dist/server/server/service-proxy.d.ts +237 -0
  36. package/dist/server/server/service-proxy.js +714 -0
  37. package/dist/server/server/session.d.ts +11 -4
  38. package/dist/server/server/session.js +96 -99
  39. package/dist/server/server/websocket-server.d.ts +7 -4
  40. package/dist/server/server/websocket-server.js +9 -4
  41. package/dist/server/server/workspace-directory.js +4 -0
  42. package/dist/server/server/workspace-git-service.d.ts +3 -0
  43. package/dist/server/server/workspace-git-service.js +53 -12
  44. package/dist/server/server/workspace-registry.d.ts +2 -2
  45. package/dist/server/server/workspace-service-env.d.ts +1 -0
  46. package/dist/server/server/workspace-service-env.js +23 -18
  47. package/dist/server/server/worktree/commands.d.ts +2 -0
  48. package/dist/server/server/worktree/commands.js +4 -1
  49. package/dist/server/server/worktree-bootstrap.d.ts +4 -3
  50. package/dist/server/server/worktree-bootstrap.js +14 -13
  51. package/dist/server/server/worktree-core.d.ts +1 -0
  52. package/dist/server/server/worktree-core.js +2 -0
  53. package/dist/server/server/worktree-session.d.ts +6 -2
  54. package/dist/server/server/worktree-session.js +3 -0
  55. package/dist/server/services/github-service.d.ts +1 -0
  56. package/dist/server/services/github-service.js +7 -1
  57. package/dist/server/terminal/terminal-manager.js +11 -1
  58. package/dist/server/terminal/terminal-session-controller.d.ts +3 -1
  59. package/dist/server/terminal/terminal-session-controller.js +22 -12
  60. package/dist/server/terminal/terminal.d.ts +1 -0
  61. package/dist/server/terminal/terminal.js +34 -0
  62. package/dist/server/utils/checkout-git.d.ts +6 -2
  63. package/dist/server/utils/checkout-git.js +136 -54
  64. package/dist/server/utils/worktree.d.ts +17 -12
  65. package/dist/server/utils/worktree.js +39 -22
  66. package/dist/src/server/persisted-config.js +17 -0
  67. package/package.json +5 -5
  68. package/dist/server/utils/script-hostname.d.ts +0 -8
  69. package/dist/server/utils/script-hostname.js +0 -14
@@ -0,0 +1,714 @@
1
+ import http, { createServer as createHTTPServer } from "node:http";
2
+ import net from "node:net";
3
+ import { createHash } from "node:crypto";
4
+ import { existsSync, unlinkSync } from "node:fs";
5
+ import express from "express";
6
+ const MAX_DNS_LABEL_LENGTH = 63;
7
+ const HASH_SUFFIX_LENGTH = 8;
8
+ const HOP_BY_HOP_HEADERS = new Set([
9
+ "connection",
10
+ "transfer-encoding",
11
+ "keep-alive",
12
+ "upgrade",
13
+ "proxy-connection",
14
+ "proxy-authenticate",
15
+ "proxy-authorization",
16
+ "te",
17
+ "trailer",
18
+ ]);
19
+ function normalizeHostHeader(host) {
20
+ return host.trim().toLowerCase().replace(/:\d+$/, "");
21
+ }
22
+ function toHostnameLabel(value) {
23
+ return (value
24
+ .toLowerCase()
25
+ .normalize("NFKD")
26
+ .replace(/[\u0300-\u036f]/g, "")
27
+ .replace(/[^a-z0-9]+/g, "-")
28
+ .replace(/^-+|-+$/g, "") || "untitled");
29
+ }
30
+ function hashLabel(label) {
31
+ return createHash("sha256").update(label).digest("hex").slice(0, HASH_SUFFIX_LENGTH);
32
+ }
33
+ function capDnsLabel(label) {
34
+ if (label.length <= MAX_DNS_LABEL_LENGTH) {
35
+ return label;
36
+ }
37
+ const suffix = hashLabel(label);
38
+ const maxPrefixLength = MAX_DNS_LABEL_LENGTH - suffix.length - 2;
39
+ const prefix = label.slice(0, maxPrefixLength).replace(/-+$/g, "") || "svc";
40
+ return `${prefix}--${suffix}`;
41
+ }
42
+ export function buildServiceProxyLabel({ projectSlug, branchName, scriptName, }) {
43
+ const labels = [toHostnameLabel(scriptName)];
44
+ const isDefaultBranch = branchName === null || branchName === "main" || branchName === "master";
45
+ if (!isDefaultBranch) {
46
+ labels.push(toHostnameLabel(branchName));
47
+ }
48
+ labels.push(toHostnameLabel(projectSlug));
49
+ return capDnsLabel(labels.join("--"));
50
+ }
51
+ export function buildLocalServiceHostname(input) {
52
+ return `${buildServiceProxyLabel(input)}.localhost`;
53
+ }
54
+ export function buildPublicServiceHostname({ publicBaseUrl, ...service }) {
55
+ const base = new URL(publicBaseUrl);
56
+ return `${buildServiceProxyLabel(service)}.${base.hostname}`;
57
+ }
58
+ export function buildPublicServiceProxyUrl(options) {
59
+ const base = new URL(options.publicBaseUrl);
60
+ const hostname = buildPublicServiceHostname(options);
61
+ const port = base.port ? `:${base.port}` : "";
62
+ return `${base.protocol}//${hostname}${port}`;
63
+ }
64
+ export function projectServiceProxyUrls(options) {
65
+ const localHostname = buildLocalServiceHostname(options);
66
+ const localProxyUrl = options.daemonPort === null || options.daemonPort === undefined
67
+ ? null
68
+ : `http://${localHostname}:${options.daemonPort}`;
69
+ const publicProxyUrl = options.publicBaseUrl
70
+ ? buildPublicServiceProxyUrl({
71
+ projectSlug: options.projectSlug,
72
+ branchName: options.branchName,
73
+ scriptName: options.scriptName,
74
+ publicBaseUrl: options.publicBaseUrl,
75
+ })
76
+ : null;
77
+ return {
78
+ localProxyUrl,
79
+ publicProxyUrl,
80
+ proxyUrl: publicProxyUrl ?? localProxyUrl,
81
+ };
82
+ }
83
+ export function projectWorkspaceService(input) {
84
+ return {
85
+ hostname: buildLocalServiceHostname(input),
86
+ ...projectServiceProxyUrls(input),
87
+ };
88
+ }
89
+ export function projectRegisteredServiceProxyUrls(options) {
90
+ const localProxyUrl = options.daemonPort === null || options.daemonPort === undefined
91
+ ? null
92
+ : `http://${options.route.hostname}:${options.daemonPort}`;
93
+ let publicProxyUrl = null;
94
+ if (options.route.publicHostname && options.route.publicBaseUrl) {
95
+ const base = new URL(options.route.publicBaseUrl);
96
+ const port = base.port ? `:${base.port}` : "";
97
+ publicProxyUrl = `${base.protocol}//${options.route.publicHostname}${port}`;
98
+ }
99
+ return {
100
+ localProxyUrl,
101
+ publicProxyUrl,
102
+ proxyUrl: publicProxyUrl ?? localProxyUrl,
103
+ };
104
+ }
105
+ function toHealthTarget(route) {
106
+ return {
107
+ workspaceId: route.workspaceId,
108
+ scriptName: route.scriptName,
109
+ hostname: route.hostname,
110
+ port: route.port,
111
+ };
112
+ }
113
+ function stripHopByHopHeaders(rawHeaders) {
114
+ const out = {};
115
+ for (const [key, value] of Object.entries(rawHeaders)) {
116
+ if (value === undefined)
117
+ continue;
118
+ if (HOP_BY_HOP_HEADERS.has(key.toLowerCase()))
119
+ continue;
120
+ out[key] = value;
121
+ }
122
+ return out;
123
+ }
124
+ function proxyHttpRequest({ req, res, route, logger, }) {
125
+ const hostHeader = req.headers.host ?? route.hostname;
126
+ const forwardedHeaders = stripHopByHopHeaders(req.headers);
127
+ forwardedHeaders["x-forwarded-for"] = req.socket.remoteAddress ?? "127.0.0.1";
128
+ forwardedHeaders["x-forwarded-host"] = String(hostHeader).replace(/:\d+$/, "");
129
+ forwardedHeaders["x-forwarded-proto"] = req.protocol;
130
+ const proxyReq = http.request({
131
+ hostname: "127.0.0.1",
132
+ port: route.port,
133
+ path: req.originalUrl,
134
+ method: req.method,
135
+ headers: forwardedHeaders,
136
+ }, (proxyRes) => {
137
+ const responseHeaders = stripHopByHopHeaders(proxyRes.headers);
138
+ res.writeHead(proxyRes.statusCode ?? 502, responseHeaders);
139
+ proxyRes.pipe(res, { end: true });
140
+ });
141
+ proxyReq.on("error", (err) => {
142
+ logger.warn({ err, hostname: route.hostname, port: route.port }, "Service proxy: upstream unreachable");
143
+ if (!res.headersSent) {
144
+ res.writeHead(502, { "content-type": "text/plain" });
145
+ res.end("502 Bad Gateway");
146
+ }
147
+ });
148
+ req.pipe(proxyReq, { end: true });
149
+ }
150
+ function proxyUpgradeRequest({ req, socket, head, route, logger, }) {
151
+ const hostHeader = req.headers.host ?? route.hostname;
152
+ const targetSocket = net.connect({ host: "127.0.0.1", port: route.port }, () => {
153
+ const forwardedHeaders = stripHopByHopHeaders(req.headers);
154
+ forwardedHeaders["x-forwarded-for"] = req.socket.remoteAddress ?? "127.0.0.1";
155
+ forwardedHeaders["x-forwarded-host"] = String(hostHeader).replace(/:\d+$/, "");
156
+ forwardedHeaders["x-forwarded-proto"] = "http";
157
+ forwardedHeaders.connection = "Upgrade";
158
+ forwardedHeaders.upgrade = req.headers.upgrade ?? "websocket";
159
+ const headerLines = [];
160
+ headerLines.push(`${req.method ?? "GET"} ${req.url ?? "/"} HTTP/${req.httpVersion}`);
161
+ for (const [key, value] of Object.entries(forwardedHeaders)) {
162
+ if (Array.isArray(value)) {
163
+ for (const v of value)
164
+ headerLines.push(`${key}: ${v}`);
165
+ }
166
+ else {
167
+ headerLines.push(`${key}: ${value}`);
168
+ }
169
+ }
170
+ headerLines.push("\r\n");
171
+ targetSocket.write(headerLines.join("\r\n"));
172
+ if (head.length > 0)
173
+ targetSocket.write(head);
174
+ targetSocket.pipe(socket);
175
+ socket.pipe(targetSocket);
176
+ });
177
+ targetSocket.on("error", (err) => {
178
+ logger.warn({ err, hostname: route.hostname, port: route.port }, "Service proxy: WebSocket upstream unreachable");
179
+ socket.end();
180
+ });
181
+ socket.on("error", () => {
182
+ targetSocket.destroy();
183
+ });
184
+ }
185
+ function sameRouteOwner(left, right) {
186
+ return left.workspaceId === right.workspaceId && left.scriptName === right.scriptName;
187
+ }
188
+ export class ServiceProxyRouteCollisionError extends Error {
189
+ constructor(hostname, existing, incoming) {
190
+ super(`Service proxy hostname collision for ${hostname}: ${existing.workspaceId}/${existing.scriptName} already owns it`);
191
+ this.hostname = hostname;
192
+ this.existing = existing;
193
+ this.incoming = incoming;
194
+ this.name = "ServiceProxyRouteCollisionError";
195
+ }
196
+ }
197
+ export class ServiceProxyRouteRegistry {
198
+ constructor(publicBaseUrl) {
199
+ this.routes = new Map();
200
+ this.hostnameAliases = new Map();
201
+ this.workspaceHostnames = new Map();
202
+ this.configuredPublicBaseHostnames = new Set();
203
+ this.publicBaseHostnames = new Set();
204
+ if (publicBaseUrl) {
205
+ const hostname = new URL(publicBaseUrl).hostname.toLowerCase();
206
+ this.configuredPublicBaseHostnames.add(hostname);
207
+ this.publicBaseHostnames.add(hostname);
208
+ }
209
+ }
210
+ registerWorkspaceService(input) {
211
+ const localHostname = buildLocalServiceHostname(input);
212
+ const publicHostname = input.publicBaseUrl
213
+ ? buildPublicServiceHostname({ ...input, publicBaseUrl: input.publicBaseUrl })
214
+ : null;
215
+ const entry = {
216
+ hostname: localHostname,
217
+ ...(publicHostname ? { publicHostname } : {}),
218
+ ...(input.publicBaseUrl ? { publicBaseUrl: input.publicBaseUrl } : {}),
219
+ port: input.port,
220
+ workspaceId: input.workspaceId,
221
+ projectSlug: input.projectSlug,
222
+ scriptName: input.scriptName,
223
+ };
224
+ this.registerRoute(entry);
225
+ return { ...entry };
226
+ }
227
+ registerRoute(entry) {
228
+ this.assertCanRegister(entry);
229
+ const previous = this.routes.get(entry.hostname);
230
+ if (previous) {
231
+ this.removeRoute(previous.hostname);
232
+ }
233
+ const storedEntry = this.toStoredEntry(entry);
234
+ this.routes.set(storedEntry.hostname, storedEntry);
235
+ for (const alias of this.getRouteHostnames(storedEntry)) {
236
+ this.hostnameAliases.set(alias, storedEntry.hostname);
237
+ }
238
+ if (storedEntry.publicBaseUrl) {
239
+ this.publicBaseHostnames.add(new URL(storedEntry.publicBaseUrl).hostname.toLowerCase());
240
+ }
241
+ this.addHostnameToWorkspaceIndex(storedEntry.workspaceId, storedEntry.hostname);
242
+ }
243
+ replaceWorkspaceBranchRoutes(params) {
244
+ const routes = this.listRoutesForWorkspace(params.workspaceId);
245
+ if (routes.length === 0) {
246
+ return false;
247
+ }
248
+ const updates = routes.map((route) => ({
249
+ oldHostname: route.hostname,
250
+ entry: this.toStoredEntry({
251
+ ...route,
252
+ hostname: buildLocalServiceHostname({
253
+ projectSlug: route.projectSlug,
254
+ branchName: params.newBranch,
255
+ scriptName: route.scriptName,
256
+ }),
257
+ publicHostname: route.publicBaseUrl
258
+ ? buildPublicServiceHostname({
259
+ projectSlug: route.projectSlug,
260
+ branchName: params.newBranch,
261
+ scriptName: route.scriptName,
262
+ publicBaseUrl: route.publicBaseUrl,
263
+ })
264
+ : null,
265
+ }),
266
+ }));
267
+ if (updates.every(({ oldHostname, entry }) => oldHostname === entry.hostname &&
268
+ (this.routes.get(oldHostname)?.publicHostname ?? null) === (entry.publicHostname ?? null))) {
269
+ return false;
270
+ }
271
+ const replacingHostnames = new Set(routes.map((route) => route.hostname));
272
+ this.assertNoInternalCollisions(updates.map(({ entry }) => entry));
273
+ for (const { entry } of updates) {
274
+ this.assertCanRegister(entry, replacingHostnames);
275
+ }
276
+ for (const { oldHostname } of updates) {
277
+ this.removeRoute(oldHostname);
278
+ }
279
+ for (const { entry } of updates) {
280
+ this.registerRoute(entry);
281
+ }
282
+ return true;
283
+ }
284
+ removeRoute(hostname) {
285
+ const canonicalHostname = this.hostnameAliases.get(normalizeHostHeader(hostname)) ?? hostname;
286
+ const entry = this.routes.get(canonicalHostname);
287
+ if (!entry) {
288
+ return;
289
+ }
290
+ this.routes.delete(canonicalHostname);
291
+ for (const alias of this.getRouteHostnames(entry)) {
292
+ this.hostnameAliases.delete(alias);
293
+ }
294
+ this.removeHostnameFromWorkspaceIndex(entry.workspaceId, canonicalHostname);
295
+ this.rebuildPublicBaseHostnames();
296
+ }
297
+ removeRouteForWorkspaceScript(params) {
298
+ const route = this.listRoutesForWorkspace(params.workspaceId).find((entry) => entry.scriptName === params.scriptName);
299
+ if (route) {
300
+ this.removeRoute(route.hostname);
301
+ }
302
+ }
303
+ removeWorkspaceService(params) {
304
+ this.removeRouteForWorkspaceScript(params);
305
+ }
306
+ projectUrls(input) {
307
+ return projectServiceProxyUrls(input);
308
+ }
309
+ projectWorkspaceService(input) {
310
+ return projectWorkspaceService(input);
311
+ }
312
+ projectWorkspaceServiceState(input) {
313
+ const route = this.listRoutesForWorkspace(input.workspaceId).find((entry) => entry.scriptName === input.scriptName);
314
+ if (route) {
315
+ return {
316
+ hostname: route.hostname,
317
+ port: route.port,
318
+ ...projectRegisteredServiceProxyUrls({ route, daemonPort: input.daemonPort }),
319
+ };
320
+ }
321
+ return {
322
+ port: null,
323
+ ...projectWorkspaceService(input),
324
+ };
325
+ }
326
+ getHealthCheckTargets() {
327
+ return this.listRoutes().map(toHealthTarget);
328
+ }
329
+ getWorkspaceHealthTargets(workspaceId) {
330
+ return this.listRoutesForWorkspace(workspaceId).map(toHealthTarget);
331
+ }
332
+ getHealthTargetForHostname(hostname) {
333
+ const entry = this.getRouteEntry(hostname);
334
+ return entry ? toHealthTarget(entry) : null;
335
+ }
336
+ removeServiceRoutesByHostnames(hostnames) {
337
+ for (const hostname of hostnames) {
338
+ this.removeRoute(hostname);
339
+ }
340
+ }
341
+ removeRoutesForPort(port) {
342
+ for (const [hostname, entry] of Array.from(this.routes)) {
343
+ if (entry.port === port) {
344
+ this.routes.delete(hostname);
345
+ for (const alias of this.getRouteHostnames(entry)) {
346
+ this.hostnameAliases.delete(alias);
347
+ }
348
+ this.removeHostnameFromWorkspaceIndex(entry.workspaceId, hostname);
349
+ }
350
+ }
351
+ this.rebuildPublicBaseHostnames();
352
+ }
353
+ classifyHost(host) {
354
+ if (!host) {
355
+ return { type: "daemon" };
356
+ }
357
+ const hostname = normalizeHostHeader(host);
358
+ const exactRoute = this.getRouteByHostname(hostname);
359
+ if (exactRoute) {
360
+ return {
361
+ type: "registered-service",
362
+ route: { hostname: exactRoute.hostname, port: exactRoute.port },
363
+ };
364
+ }
365
+ if (hostname.endsWith(".localhost") && hostname.split(".")[0]?.includes("--")) {
366
+ return { type: "known-service-miss" };
367
+ }
368
+ for (const baseHostname of this.publicBaseHostnames) {
369
+ if (hostname === baseHostname || hostname.endsWith(`.${baseHostname}`)) {
370
+ return { type: "known-service-miss" };
371
+ }
372
+ }
373
+ return { type: "daemon" };
374
+ }
375
+ findRoute(host) {
376
+ const classification = this.classifyHost(host);
377
+ return classification.type === "registered-service" ? classification.route : null;
378
+ }
379
+ getRouteEntry(hostname) {
380
+ const entry = this.getRouteByHostname(normalizeHostHeader(hostname));
381
+ return entry ? { ...entry } : null;
382
+ }
383
+ listRoutes() {
384
+ return Array.from(this.routes.values()).map((entry) => Object.assign({}, entry));
385
+ }
386
+ listRoutesForWorkspace(workspaceId) {
387
+ const hostnames = this.workspaceHostnames.get(workspaceId);
388
+ if (!hostnames) {
389
+ return [];
390
+ }
391
+ const routes = [];
392
+ for (const hostname of hostnames) {
393
+ const entry = this.routes.get(hostname);
394
+ if (entry) {
395
+ routes.push({ ...entry });
396
+ }
397
+ }
398
+ return routes;
399
+ }
400
+ assertCanRegister(entry, replacingHostnames = new Set()) {
401
+ const incomingHostnames = this.getRouteHostnames(entry);
402
+ for (const hostname of incomingHostnames) {
403
+ const canonical = this.hostnameAliases.get(hostname) ?? hostname;
404
+ if (replacingHostnames.has(canonical)) {
405
+ continue;
406
+ }
407
+ const existing = this.routes.get(canonical);
408
+ if (existing && !sameRouteOwner(existing, entry)) {
409
+ throw new ServiceProxyRouteCollisionError(hostname, existing, entry);
410
+ }
411
+ }
412
+ }
413
+ assertNoInternalCollisions(entries) {
414
+ const ownersByHostname = new Map();
415
+ for (const entry of entries) {
416
+ for (const hostname of this.getRouteHostnames(entry)) {
417
+ const existing = ownersByHostname.get(hostname);
418
+ if (existing) {
419
+ throw new ServiceProxyRouteCollisionError(hostname, existing, entry);
420
+ }
421
+ ownersByHostname.set(hostname, entry);
422
+ }
423
+ }
424
+ }
425
+ toStoredEntry(entry) {
426
+ const { publicHostname, publicBaseUrl, ...requiredEntry } = entry;
427
+ return {
428
+ ...requiredEntry,
429
+ ...(publicHostname ? { publicHostname } : {}),
430
+ ...(publicBaseUrl ? { publicBaseUrl } : {}),
431
+ };
432
+ }
433
+ getRouteByHostname(hostname) {
434
+ const canonicalHostname = this.hostnameAliases.get(hostname) ?? hostname;
435
+ return this.routes.get(canonicalHostname);
436
+ }
437
+ getRouteHostnames(entry) {
438
+ return [entry.hostname, ...(entry.publicHostname ? [entry.publicHostname] : [])].map((host) => host.toLowerCase());
439
+ }
440
+ addHostnameToWorkspaceIndex(workspaceId, hostname) {
441
+ const hostnames = this.workspaceHostnames.get(workspaceId) ?? new Set();
442
+ hostnames.add(hostname);
443
+ this.workspaceHostnames.set(workspaceId, hostnames);
444
+ }
445
+ removeHostnameFromWorkspaceIndex(workspaceId, hostname) {
446
+ const hostnames = this.workspaceHostnames.get(workspaceId);
447
+ if (!hostnames) {
448
+ return;
449
+ }
450
+ hostnames.delete(hostname);
451
+ if (hostnames.size === 0) {
452
+ this.workspaceHostnames.delete(workspaceId);
453
+ }
454
+ }
455
+ rebuildPublicBaseHostnames() {
456
+ this.publicBaseHostnames = new Set(this.configuredPublicBaseHostnames);
457
+ for (const entry of this.routes.values()) {
458
+ if (entry.publicBaseUrl) {
459
+ this.publicBaseHostnames.add(new URL(entry.publicBaseUrl).hostname.toLowerCase());
460
+ }
461
+ }
462
+ }
463
+ }
464
+ export { ServiceProxyRouteRegistry as ScriptRouteStore };
465
+ export function createScriptProxyMiddleware({ routeStore, logger, }) {
466
+ return (req, res, next) => {
467
+ const classification = routeStore.classifyHost(req.headers.host);
468
+ if (classification.type === "daemon") {
469
+ next();
470
+ return;
471
+ }
472
+ if (classification.type === "known-service-miss") {
473
+ res.status(404).send("404 Not Found");
474
+ return;
475
+ }
476
+ proxyHttpRequest({ req, res, route: classification.route, logger });
477
+ };
478
+ }
479
+ export function createScriptProxyUpgradeHandler({ routeStore, logger, passthroughUnknown = true, }) {
480
+ return (req, socket, head) => {
481
+ const classification = routeStore.classifyHost(req.headers.host);
482
+ if (classification.type !== "registered-service") {
483
+ if (!passthroughUnknown) {
484
+ socket.destroy();
485
+ }
486
+ return;
487
+ }
488
+ proxyUpgradeRequest({ req, socket, head, route: classification.route, logger });
489
+ };
490
+ }
491
+ export function createServiceProxySubsystem({ logger, publicBaseUrl, }) {
492
+ return new NodeServiceProxySubsystem(logger, publicBaseUrl ?? null);
493
+ }
494
+ class NodeServiceProxySubsystem {
495
+ constructor(logger, publicBaseUrl) {
496
+ this.logger = logger;
497
+ this.standaloneServer = null;
498
+ this.standaloneListenTarget = null;
499
+ this.routes = new ServiceProxyRouteRegistry(publicBaseUrl);
500
+ }
501
+ registerWorkspaceService(input) {
502
+ return this.routes.registerWorkspaceService(input);
503
+ }
504
+ removeWorkspaceService(params) {
505
+ this.routes.removeRouteForWorkspaceScript(params);
506
+ }
507
+ removeServiceRoutesByHostnames(hostnames) {
508
+ this.routes.removeServiceRoutesByHostnames(hostnames);
509
+ }
510
+ replaceWorkspaceBranchRoutes(params) {
511
+ return this.routes.replaceWorkspaceBranchRoutes(params);
512
+ }
513
+ getHealthCheckTargets() {
514
+ return this.routes.getHealthCheckTargets();
515
+ }
516
+ getWorkspaceHealthTargets(workspaceId) {
517
+ return this.routes.getWorkspaceHealthTargets(workspaceId);
518
+ }
519
+ getHealthTargetForHostname(hostname) {
520
+ return this.routes.getHealthTargetForHostname(hostname);
521
+ }
522
+ projectUrls(input) {
523
+ return projectServiceProxyUrls(input);
524
+ }
525
+ projectWorkspaceService(input) {
526
+ return projectWorkspaceService(input);
527
+ }
528
+ projectWorkspaceServiceState(input) {
529
+ return this.routes.projectWorkspaceServiceState(input);
530
+ }
531
+ middleware() {
532
+ return (req, res, next) => {
533
+ const classification = this.routes.classifyHost(req.headers.host);
534
+ if (classification.type === "daemon") {
535
+ next();
536
+ return;
537
+ }
538
+ if (classification.type === "known-service-miss") {
539
+ res.status(404).send("404 Not Found");
540
+ return;
541
+ }
542
+ this.proxyHttpRequest(req, res, classification.route);
543
+ };
544
+ }
545
+ upgradeHandler(options) {
546
+ return (req, socket, head) => {
547
+ const classification = this.routes.classifyHost(req.headers.host);
548
+ if (classification.type !== "registered-service") {
549
+ if (!options.passthroughUnknown) {
550
+ socket.destroy();
551
+ }
552
+ return;
553
+ }
554
+ this.proxyUpgradeRequest(req, socket, head, classification.route);
555
+ };
556
+ }
557
+ async startStandalone(options) {
558
+ if (this.standaloneServer) {
559
+ return this.standaloneListenTarget ?? options.listenTarget;
560
+ }
561
+ const app = express();
562
+ app.set("trust proxy", true);
563
+ app.use(this.middleware());
564
+ app.use((_req, res) => {
565
+ res.status(404).send("404 Not Found");
566
+ });
567
+ const server = createHTTPServer(app);
568
+ server.on("upgrade", this.upgradeHandler({ passthroughUnknown: false }));
569
+ this.standaloneServer = server;
570
+ try {
571
+ await listen(server, options.listenTarget);
572
+ this.standaloneListenTarget = resolveBoundListenTarget(options.listenTarget, server);
573
+ return this.standaloneListenTarget;
574
+ }
575
+ catch (error) {
576
+ this.standaloneServer = null;
577
+ this.standaloneListenTarget = null;
578
+ throw error;
579
+ }
580
+ }
581
+ async stopStandalone() {
582
+ const server = this.standaloneServer;
583
+ const listenTarget = this.standaloneListenTarget;
584
+ this.standaloneServer = null;
585
+ this.standaloneListenTarget = null;
586
+ if (!server) {
587
+ return;
588
+ }
589
+ server.closeAllConnections();
590
+ await new Promise((resolve) => {
591
+ server.close(() => resolve());
592
+ });
593
+ if (listenTarget?.type === "socket" && existsSync(listenTarget.path)) {
594
+ unlinkSync(listenTarget.path);
595
+ }
596
+ }
597
+ proxyHttpRequest(req, res, route) {
598
+ const hostHeader = req.headers.host ?? route.hostname;
599
+ const forwardedHeaders = stripHopByHopHeaders(req.headers);
600
+ forwardedHeaders["x-forwarded-for"] = req.socket.remoteAddress ?? "127.0.0.1";
601
+ forwardedHeaders["x-forwarded-host"] = String(hostHeader).replace(/:\d+$/, "");
602
+ forwardedHeaders["x-forwarded-proto"] = req.protocol;
603
+ const proxyReq = http.request({
604
+ hostname: "127.0.0.1",
605
+ port: route.port,
606
+ path: req.originalUrl,
607
+ method: req.method,
608
+ headers: forwardedHeaders,
609
+ }, (proxyRes) => {
610
+ const responseHeaders = stripHopByHopHeaders(proxyRes.headers);
611
+ res.writeHead(proxyRes.statusCode ?? 502, responseHeaders);
612
+ proxyRes.pipe(res, { end: true });
613
+ });
614
+ proxyReq.on("error", (err) => {
615
+ this.logger.warn({ err, hostname: route.hostname, port: route.port }, "Service proxy: upstream unreachable");
616
+ if (!res.headersSent) {
617
+ res.writeHead(502, { "content-type": "text/plain" });
618
+ res.end("502 Bad Gateway");
619
+ }
620
+ });
621
+ req.pipe(proxyReq, { end: true });
622
+ }
623
+ proxyUpgradeRequest(req, socket, head, route) {
624
+ const hostHeader = req.headers.host ?? route.hostname;
625
+ const targetSocket = net.connect({ host: "127.0.0.1", port: route.port }, () => {
626
+ const forwardedHeaders = stripHopByHopHeaders(req.headers);
627
+ forwardedHeaders["x-forwarded-for"] = req.socket.remoteAddress ?? "127.0.0.1";
628
+ forwardedHeaders["x-forwarded-host"] = String(hostHeader).replace(/:\d+$/, "");
629
+ forwardedHeaders["x-forwarded-proto"] = "http";
630
+ forwardedHeaders.connection = "Upgrade";
631
+ forwardedHeaders.upgrade = req.headers.upgrade ?? "websocket";
632
+ const headerLines = [];
633
+ headerLines.push(`${req.method ?? "GET"} ${req.url ?? "/"} HTTP/${req.httpVersion}`);
634
+ for (const [key, value] of Object.entries(forwardedHeaders)) {
635
+ if (Array.isArray(value)) {
636
+ for (const v of value)
637
+ headerLines.push(`${key}: ${v}`);
638
+ }
639
+ else {
640
+ headerLines.push(`${key}: ${value}`);
641
+ }
642
+ }
643
+ headerLines.push("\r\n");
644
+ targetSocket.write(headerLines.join("\r\n"));
645
+ if (head.length > 0)
646
+ targetSocket.write(head);
647
+ targetSocket.pipe(socket);
648
+ socket.pipe(targetSocket);
649
+ });
650
+ targetSocket.on("error", (err) => {
651
+ this.logger.warn({ err, hostname: route.hostname, port: route.port }, "Service proxy: WebSocket upstream unreachable");
652
+ socket.end();
653
+ });
654
+ socket.on("error", () => {
655
+ targetSocket.destroy();
656
+ });
657
+ }
658
+ }
659
+ function listen(server, listenTarget) {
660
+ return new Promise((resolve, reject) => {
661
+ const onError = (err) => {
662
+ server.off("listening", onListening);
663
+ reject(err);
664
+ };
665
+ const onListening = () => {
666
+ server.off("error", onError);
667
+ resolve();
668
+ };
669
+ server.once("error", onError);
670
+ server.once("listening", onListening);
671
+ if (listenTarget.type === "tcp") {
672
+ server.listen(listenTarget.port, listenTarget.host);
673
+ }
674
+ else {
675
+ if (listenTarget.type === "socket" && existsSync(listenTarget.path)) {
676
+ unlinkSync(listenTarget.path);
677
+ }
678
+ server.listen(listenTarget.path);
679
+ }
680
+ });
681
+ }
682
+ function resolveBoundListenTarget(listenTarget, httpServer) {
683
+ if (listenTarget.type !== "tcp") {
684
+ return listenTarget;
685
+ }
686
+ const address = httpServer.address();
687
+ if (!address || typeof address === "string") {
688
+ throw new Error("HTTP server did not expose a TCP address after listening");
689
+ }
690
+ return { type: "tcp", host: listenTarget.host, port: address.port };
691
+ }
692
+ export function findFreePort() {
693
+ return new Promise((resolve, reject) => {
694
+ const server = net.createServer();
695
+ server.unref();
696
+ server.listen(0, "127.0.0.1", () => {
697
+ const address = server.address();
698
+ if (!address || typeof address === "string") {
699
+ server.close();
700
+ reject(new Error("Failed to get assigned port"));
701
+ return;
702
+ }
703
+ const { port } = address;
704
+ server.close((err) => {
705
+ if (err)
706
+ reject(err);
707
+ else
708
+ resolve(port);
709
+ });
710
+ });
711
+ server.on("error", reject);
712
+ });
713
+ }
714
+ //# sourceMappingURL=service-proxy.js.map