@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.
- package/dist/server/server/agent/agent-manager.js +4 -1
- package/dist/server/server/agent/agent-storage.d.ts +22 -22
- package/dist/server/server/agent/create-agent/create.d.ts +2 -0
- package/dist/server/server/agent/create-agent/create.js +16 -5
- package/dist/server/server/agent/create-agent-lifecycle-dispatch.d.ts +1 -0
- package/dist/server/server/agent/create-agent-lifecycle-dispatch.js +4 -0
- package/dist/server/server/agent/mcp-server.d.ts +1 -0
- package/dist/server/server/agent/mcp-server.js +137 -63
- package/dist/server/server/agent/mcp-shared.d.ts +1 -0
- package/dist/server/server/agent/providers/pi/agent.js +13 -0
- package/dist/server/server/agent/providers/pi/rpc-types.d.ts +3 -0
- package/dist/server/server/agent/timeline-projection.d.ts +17 -1
- package/dist/server/server/agent/timeline-projection.js +82 -17
- package/dist/server/server/auto-archive-on-merge/archive-if-safe.d.ts +1 -0
- package/dist/server/server/auto-archive-on-merge/archive-if-safe.js +6 -1
- package/dist/server/server/bootstrap.d.ts +7 -2
- package/dist/server/server/bootstrap.js +152 -115
- package/dist/server/server/config.js +41 -0
- package/dist/server/server/loop-service.d.ts +22 -22
- package/dist/server/server/package-version.d.ts +2 -2
- package/dist/server/server/paseo-worktree-archive-service.d.ts +2 -0
- package/dist/server/server/paseo-worktree-archive-service.js +28 -9
- package/dist/server/server/persisted-config.d.ts +89 -33
- package/dist/server/server/persisted-config.js +17 -0
- package/dist/server/server/pid-lock.d.ts +2 -2
- package/dist/server/server/schedule/cron.js +52 -5
- package/dist/server/server/script-health-monitor.d.ts +4 -4
- package/dist/server/server/script-health-monitor.js +6 -6
- package/dist/server/server/script-proxy.d.ts +2 -39
- package/dist/server/server/script-proxy.js +1 -244
- package/dist/server/server/script-route-branch-handler.d.ts +2 -2
- package/dist/server/server/script-route-branch-handler.js +3 -37
- package/dist/server/server/script-status-projection.d.ts +6 -4
- package/dist/server/server/script-status-projection.js +85 -37
- package/dist/server/server/service-proxy.d.ts +237 -0
- package/dist/server/server/service-proxy.js +714 -0
- package/dist/server/server/session.d.ts +11 -4
- package/dist/server/server/session.js +96 -99
- package/dist/server/server/websocket-server.d.ts +7 -4
- package/dist/server/server/websocket-server.js +9 -4
- package/dist/server/server/workspace-directory.js +4 -0
- package/dist/server/server/workspace-git-service.d.ts +3 -0
- package/dist/server/server/workspace-git-service.js +53 -12
- package/dist/server/server/workspace-registry.d.ts +2 -2
- package/dist/server/server/workspace-service-env.d.ts +1 -0
- package/dist/server/server/workspace-service-env.js +23 -18
- package/dist/server/server/worktree/commands.d.ts +2 -0
- package/dist/server/server/worktree/commands.js +4 -1
- package/dist/server/server/worktree-bootstrap.d.ts +4 -3
- package/dist/server/server/worktree-bootstrap.js +14 -13
- package/dist/server/server/worktree-core.d.ts +1 -0
- package/dist/server/server/worktree-core.js +2 -0
- package/dist/server/server/worktree-session.d.ts +6 -2
- package/dist/server/server/worktree-session.js +3 -0
- package/dist/server/services/github-service.d.ts +1 -0
- package/dist/server/services/github-service.js +7 -1
- package/dist/server/terminal/terminal-manager.js +11 -1
- package/dist/server/terminal/terminal-session-controller.d.ts +3 -1
- package/dist/server/terminal/terminal-session-controller.js +22 -12
- package/dist/server/terminal/terminal.d.ts +1 -0
- package/dist/server/terminal/terminal.js +34 -0
- package/dist/server/utils/checkout-git.d.ts +6 -2
- package/dist/server/utils/checkout-git.js +136 -54
- package/dist/server/utils/worktree.d.ts +17 -12
- package/dist/server/utils/worktree.js +39 -22
- package/dist/src/server/persisted-config.js +17 -0
- package/package.json +5 -5
- package/dist/server/utils/script-hostname.d.ts +0 -8
- package/dist/server/utils/script-hostname.js +0 -14
|
@@ -71,9 +71,59 @@ function parseCronExpression(expression) {
|
|
|
71
71
|
function startOfNextMinute(date) {
|
|
72
72
|
return new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), date.getUTCHours(), date.getUTCMinutes() + 1, 0, 0));
|
|
73
73
|
}
|
|
74
|
+
function assertValidTimeZone(timeZone) {
|
|
75
|
+
try {
|
|
76
|
+
new Intl.DateTimeFormat("en-US", { timeZone }).format(new Date(0));
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
throw new Error(`Invalid cron time zone: ${timeZone}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function createCronDatePartsReader(timeZone) {
|
|
83
|
+
if (timeZone === undefined) {
|
|
84
|
+
return (date) => ({
|
|
85
|
+
minute: date.getUTCMinutes(),
|
|
86
|
+
hour: date.getUTCHours(),
|
|
87
|
+
dayOfMonth: date.getUTCDate(),
|
|
88
|
+
month: date.getUTCMonth() + 1,
|
|
89
|
+
dayOfWeek: date.getUTCDay(),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
assertValidTimeZone(timeZone);
|
|
93
|
+
const formatter = new Intl.DateTimeFormat("en-US", {
|
|
94
|
+
timeZone,
|
|
95
|
+
hourCycle: "h23",
|
|
96
|
+
year: "numeric",
|
|
97
|
+
month: "2-digit",
|
|
98
|
+
day: "2-digit",
|
|
99
|
+
hour: "2-digit",
|
|
100
|
+
minute: "2-digit",
|
|
101
|
+
});
|
|
102
|
+
return (date) => {
|
|
103
|
+
const values = {};
|
|
104
|
+
for (const part of formatter.formatToParts(date)) {
|
|
105
|
+
if (part.type !== "literal") {
|
|
106
|
+
values[part.type] = part.value;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const year = Number.parseInt(values.year, 10);
|
|
110
|
+
const month = Number.parseInt(values.month, 10);
|
|
111
|
+
const dayOfMonth = Number.parseInt(values.day, 10);
|
|
112
|
+
return {
|
|
113
|
+
minute: Number.parseInt(values.minute, 10),
|
|
114
|
+
hour: Number.parseInt(values.hour, 10),
|
|
115
|
+
dayOfMonth,
|
|
116
|
+
month,
|
|
117
|
+
dayOfWeek: new Date(Date.UTC(year, month - 1, dayOfMonth)).getUTCDay(),
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
}
|
|
74
121
|
export function validateScheduleCadence(cadence) {
|
|
75
122
|
if (cadence.type === "cron") {
|
|
76
123
|
parseCronExpression(cadence.expression);
|
|
124
|
+
if (cadence.timezone !== undefined) {
|
|
125
|
+
assertValidTimeZone(cadence.timezone);
|
|
126
|
+
}
|
|
77
127
|
}
|
|
78
128
|
}
|
|
79
129
|
export function computeNextRunAt(cadence, after) {
|
|
@@ -81,14 +131,11 @@ export function computeNextRunAt(cadence, after) {
|
|
|
81
131
|
return new Date(after.getTime() + cadence.everyMs);
|
|
82
132
|
}
|
|
83
133
|
const cron = parseCronExpression(cadence.expression);
|
|
134
|
+
const readDateParts = createCronDatePartsReader(cadence.timezone);
|
|
84
135
|
const limit = 366 * 24 * 60;
|
|
85
136
|
let cursor = startOfNextMinute(after);
|
|
86
137
|
for (let index = 0; index < limit; index += 1) {
|
|
87
|
-
const minute = cursor
|
|
88
|
-
const hour = cursor.getUTCHours();
|
|
89
|
-
const dayOfMonth = cursor.getUTCDate();
|
|
90
|
-
const month = cursor.getUTCMonth() + 1;
|
|
91
|
-
const dayOfWeek = cursor.getUTCDay();
|
|
138
|
+
const { minute, hour, dayOfMonth, month, dayOfWeek } = readDateParts(cursor);
|
|
92
139
|
if (cron.minute.matches(minute) &&
|
|
93
140
|
cron.hour.matches(hour) &&
|
|
94
141
|
cron.dayOfMonth.matches(dayOfMonth) &&
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ServiceProxySubsystem } from "./service-proxy.js";
|
|
2
2
|
export type ScriptHealthState = "pending" | "healthy" | "unhealthy";
|
|
3
3
|
export interface ScriptHealthEntry {
|
|
4
4
|
scriptName: string;
|
|
@@ -7,7 +7,7 @@ export interface ScriptHealthEntry {
|
|
|
7
7
|
health: ScriptHealthState;
|
|
8
8
|
}
|
|
9
9
|
export declare class ScriptHealthMonitor {
|
|
10
|
-
private readonly
|
|
10
|
+
private readonly serviceProxy;
|
|
11
11
|
private readonly onChange;
|
|
12
12
|
private readonly pollIntervalMs;
|
|
13
13
|
private readonly probeTimeoutMs;
|
|
@@ -17,8 +17,8 @@ export declare class ScriptHealthMonitor {
|
|
|
17
17
|
private readonly lastEmittedSnapshots;
|
|
18
18
|
private intervalHandle;
|
|
19
19
|
private pollInFlight;
|
|
20
|
-
constructor({
|
|
21
|
-
|
|
20
|
+
constructor({ serviceProxy, onChange, pollIntervalMs, probeTimeoutMs, graceMs, failuresBeforeStopped, }: {
|
|
21
|
+
serviceProxy: ServiceProxySubsystem;
|
|
22
22
|
onChange: (workspaceId: string, scripts: ScriptHealthEntry[]) => void;
|
|
23
23
|
pollIntervalMs?: number;
|
|
24
24
|
probeTimeoutMs?: number;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import net from "node:net";
|
|
2
2
|
export class ScriptHealthMonitor {
|
|
3
|
-
constructor({
|
|
3
|
+
constructor({ serviceProxy, onChange, pollIntervalMs = 3000, probeTimeoutMs = 500, graceMs = 5000, failuresBeforeStopped = 2, }) {
|
|
4
4
|
this.routeStates = new Map();
|
|
5
5
|
this.lastEmittedSnapshots = new Map();
|
|
6
6
|
this.intervalHandle = null;
|
|
7
7
|
this.pollInFlight = false;
|
|
8
|
-
this.
|
|
8
|
+
this.serviceProxy = serviceProxy;
|
|
9
9
|
this.onChange = onChange;
|
|
10
10
|
this.pollIntervalMs = pollIntervalMs;
|
|
11
11
|
this.probeTimeoutMs = probeTimeoutMs;
|
|
@@ -17,7 +17,7 @@ export class ScriptHealthMonitor {
|
|
|
17
17
|
return;
|
|
18
18
|
}
|
|
19
19
|
const now = Date.now();
|
|
20
|
-
for (const route of this.
|
|
20
|
+
for (const route of this.serviceProxy.getHealthCheckTargets()) {
|
|
21
21
|
this.getOrCreateState(route, now);
|
|
22
22
|
}
|
|
23
23
|
this.intervalHandle = setInterval(() => {
|
|
@@ -45,7 +45,7 @@ export class ScriptHealthMonitor {
|
|
|
45
45
|
}
|
|
46
46
|
this.pollInFlight = true;
|
|
47
47
|
try {
|
|
48
|
-
const routes = this.
|
|
48
|
+
const routes = this.serviceProxy.getHealthCheckTargets();
|
|
49
49
|
const activeHostnames = new Set(routes.map((route) => route.hostname));
|
|
50
50
|
const changedWorkspaceIds = new Set();
|
|
51
51
|
const now = Date.now();
|
|
@@ -110,7 +110,7 @@ export class ScriptHealthMonitor {
|
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
buildWorkspaceScriptList(workspaceId) {
|
|
113
|
-
return this.
|
|
113
|
+
return this.serviceProxy.getWorkspaceHealthTargets(workspaceId).flatMap((route) => {
|
|
114
114
|
const state = this.routeStates.get(route.hostname);
|
|
115
115
|
if (!state) {
|
|
116
116
|
return [];
|
|
@@ -123,7 +123,7 @@ export class ScriptHealthMonitor {
|
|
|
123
123
|
if (state) {
|
|
124
124
|
return state.health;
|
|
125
125
|
}
|
|
126
|
-
const route = this.
|
|
126
|
+
const route = this.serviceProxy.getHealthTargetForHostname(hostname);
|
|
127
127
|
if (!route) {
|
|
128
128
|
return null;
|
|
129
129
|
}
|
|
@@ -1,40 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import type { Logger } from "pino";
|
|
4
|
-
import type { RequestHandler } from "express";
|
|
5
|
-
export interface ScriptRoute {
|
|
6
|
-
hostname: string;
|
|
7
|
-
port: number;
|
|
8
|
-
}
|
|
9
|
-
export interface ScriptRouteEntry extends ScriptRoute {
|
|
10
|
-
workspaceId: string;
|
|
11
|
-
projectSlug: string;
|
|
12
|
-
scriptName: string;
|
|
13
|
-
}
|
|
14
|
-
export declare class ScriptRouteStore {
|
|
15
|
-
private routes;
|
|
16
|
-
private workspaceHostnames;
|
|
17
|
-
registerRoute(entry: ScriptRouteEntry): void;
|
|
18
|
-
removeRoute(hostname: string): void;
|
|
19
|
-
removeRouteForWorkspaceScript(params: {
|
|
20
|
-
workspaceId: string;
|
|
21
|
-
scriptName: string;
|
|
22
|
-
}): void;
|
|
23
|
-
removeRoutesForPort(port: number): void;
|
|
24
|
-
findRoute(host: string): ScriptRoute | null;
|
|
25
|
-
getRouteEntry(hostname: string): ScriptRouteEntry | null;
|
|
26
|
-
listRoutes(): ScriptRouteEntry[];
|
|
27
|
-
listRoutesForWorkspace(workspaceId: string): ScriptRouteEntry[];
|
|
28
|
-
private addHostnameToWorkspaceIndex;
|
|
29
|
-
private removeHostnameFromWorkspaceIndex;
|
|
30
|
-
}
|
|
31
|
-
export declare function createScriptProxyMiddleware({ routeStore, logger, }: {
|
|
32
|
-
routeStore: ScriptRouteStore;
|
|
33
|
-
logger: Logger;
|
|
34
|
-
}): RequestHandler;
|
|
35
|
-
export declare function createScriptProxyUpgradeHandler({ routeStore, logger, }: {
|
|
36
|
-
routeStore: ScriptRouteStore;
|
|
37
|
-
logger: Logger;
|
|
38
|
-
}): (req: IncomingMessage, socket: net.Socket, head: Buffer) => void;
|
|
39
|
-
export declare function findFreePort(): Promise<number>;
|
|
1
|
+
export { createScriptProxyMiddleware, createScriptProxyUpgradeHandler, findFreePort, ScriptRouteStore, } from "./service-proxy.js";
|
|
2
|
+
export type { ScriptRoute, ScriptRouteEntry } from "./service-proxy.js";
|
|
40
3
|
//# sourceMappingURL=script-proxy.d.ts.map
|
|
@@ -1,245 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
import net from "node:net";
|
|
3
|
-
// ---------------------------------------------------------------------------
|
|
4
|
-
// Hop-by-hop headers that must not be forwarded
|
|
5
|
-
// ---------------------------------------------------------------------------
|
|
6
|
-
const HOP_BY_HOP_HEADERS = new Set([
|
|
7
|
-
"connection",
|
|
8
|
-
"transfer-encoding",
|
|
9
|
-
"keep-alive",
|
|
10
|
-
"upgrade",
|
|
11
|
-
"proxy-connection",
|
|
12
|
-
"proxy-authenticate",
|
|
13
|
-
"proxy-authorization",
|
|
14
|
-
"te",
|
|
15
|
-
"trailer",
|
|
16
|
-
]);
|
|
17
|
-
export class ScriptRouteStore {
|
|
18
|
-
constructor() {
|
|
19
|
-
this.routes = new Map();
|
|
20
|
-
this.workspaceHostnames = new Map();
|
|
21
|
-
}
|
|
22
|
-
registerRoute(entry) {
|
|
23
|
-
const previous = this.routes.get(entry.hostname);
|
|
24
|
-
if (previous) {
|
|
25
|
-
this.removeHostnameFromWorkspaceIndex(previous.workspaceId, previous.hostname);
|
|
26
|
-
}
|
|
27
|
-
const storedEntry = { ...entry };
|
|
28
|
-
this.routes.set(storedEntry.hostname, storedEntry);
|
|
29
|
-
this.addHostnameToWorkspaceIndex(storedEntry.workspaceId, storedEntry.hostname);
|
|
30
|
-
}
|
|
31
|
-
removeRoute(hostname) {
|
|
32
|
-
const entry = this.routes.get(hostname);
|
|
33
|
-
if (!entry) {
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
36
|
-
this.routes.delete(hostname);
|
|
37
|
-
this.removeHostnameFromWorkspaceIndex(entry.workspaceId, hostname);
|
|
38
|
-
}
|
|
39
|
-
removeRouteForWorkspaceScript(params) {
|
|
40
|
-
const routes = this.listRoutesForWorkspace(params.workspaceId);
|
|
41
|
-
const route = routes.find((entry) => entry.scriptName === params.scriptName);
|
|
42
|
-
if (!route) {
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
45
|
-
this.removeRoute(route.hostname);
|
|
46
|
-
}
|
|
47
|
-
removeRoutesForPort(port) {
|
|
48
|
-
for (const [hostname, entry] of this.routes) {
|
|
49
|
-
if (entry.port === port) {
|
|
50
|
-
this.routes.delete(hostname);
|
|
51
|
-
this.removeHostnameFromWorkspaceIndex(entry.workspaceId, hostname);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
findRoute(host) {
|
|
56
|
-
// Strip port suffix from the Host header value
|
|
57
|
-
const hostname = host.replace(/:\d+$/, "");
|
|
58
|
-
// 1. Exact match
|
|
59
|
-
const exactRoute = this.routes.get(hostname);
|
|
60
|
-
if (exactRoute !== undefined) {
|
|
61
|
-
return { hostname: exactRoute.hostname, port: exactRoute.port };
|
|
62
|
-
}
|
|
63
|
-
// 2. Subdomain match — walk up the labels looking for a registered parent
|
|
64
|
-
const parts = hostname.split(".");
|
|
65
|
-
for (let i = 1; i < parts.length; i++) {
|
|
66
|
-
const candidate = parts.slice(i).join(".");
|
|
67
|
-
const candidateRoute = this.routes.get(candidate);
|
|
68
|
-
if (candidateRoute !== undefined) {
|
|
69
|
-
return { hostname: candidateRoute.hostname, port: candidateRoute.port };
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
getRouteEntry(hostname) {
|
|
75
|
-
const entry = this.routes.get(hostname);
|
|
76
|
-
return entry ? { ...entry } : null;
|
|
77
|
-
}
|
|
78
|
-
listRoutes() {
|
|
79
|
-
return Array.from(this.routes.values()).map((entry) => Object.assign({}, entry));
|
|
80
|
-
}
|
|
81
|
-
listRoutesForWorkspace(workspaceId) {
|
|
82
|
-
const hostnames = this.workspaceHostnames.get(workspaceId);
|
|
83
|
-
if (!hostnames) {
|
|
84
|
-
return [];
|
|
85
|
-
}
|
|
86
|
-
const routes = [];
|
|
87
|
-
for (const hostname of hostnames) {
|
|
88
|
-
const entry = this.routes.get(hostname);
|
|
89
|
-
if (entry) {
|
|
90
|
-
routes.push({ ...entry });
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
return routes;
|
|
94
|
-
}
|
|
95
|
-
addHostnameToWorkspaceIndex(workspaceId, hostname) {
|
|
96
|
-
const hostnames = this.workspaceHostnames.get(workspaceId) ?? new Set();
|
|
97
|
-
hostnames.add(hostname);
|
|
98
|
-
this.workspaceHostnames.set(workspaceId, hostnames);
|
|
99
|
-
}
|
|
100
|
-
removeHostnameFromWorkspaceIndex(workspaceId, hostname) {
|
|
101
|
-
const hostnames = this.workspaceHostnames.get(workspaceId);
|
|
102
|
-
if (!hostnames) {
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
hostnames.delete(hostname);
|
|
106
|
-
if (hostnames.size === 0) {
|
|
107
|
-
this.workspaceHostnames.delete(workspaceId);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
// ---------------------------------------------------------------------------
|
|
112
|
-
// Helpers
|
|
113
|
-
// ---------------------------------------------------------------------------
|
|
114
|
-
function stripHopByHopHeaders(rawHeaders) {
|
|
115
|
-
const out = {};
|
|
116
|
-
for (const [key, value] of Object.entries(rawHeaders)) {
|
|
117
|
-
if (value === undefined)
|
|
118
|
-
continue;
|
|
119
|
-
if (HOP_BY_HOP_HEADERS.has(key.toLowerCase()))
|
|
120
|
-
continue;
|
|
121
|
-
out[key] = value;
|
|
122
|
-
}
|
|
123
|
-
return out;
|
|
124
|
-
}
|
|
125
|
-
// ---------------------------------------------------------------------------
|
|
126
|
-
// createScriptProxyMiddleware
|
|
127
|
-
// ---------------------------------------------------------------------------
|
|
128
|
-
export function createScriptProxyMiddleware({ routeStore, logger, }) {
|
|
129
|
-
return (req, res, next) => {
|
|
130
|
-
const hostHeader = req.headers.host;
|
|
131
|
-
if (!hostHeader) {
|
|
132
|
-
next();
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
const route = routeStore.findRoute(hostHeader);
|
|
136
|
-
if (!route) {
|
|
137
|
-
next();
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
const forwardedHeaders = stripHopByHopHeaders(req.headers);
|
|
141
|
-
forwardedHeaders["x-forwarded-for"] = req.socket.remoteAddress ?? "127.0.0.1";
|
|
142
|
-
forwardedHeaders["x-forwarded-host"] = hostHeader.replace(/:\d+$/, "");
|
|
143
|
-
forwardedHeaders["x-forwarded-proto"] = req.protocol;
|
|
144
|
-
const proxyReq = http.request({
|
|
145
|
-
hostname: "127.0.0.1",
|
|
146
|
-
port: route.port,
|
|
147
|
-
path: req.originalUrl,
|
|
148
|
-
method: req.method,
|
|
149
|
-
headers: forwardedHeaders,
|
|
150
|
-
}, (proxyRes) => {
|
|
151
|
-
const responseHeaders = stripHopByHopHeaders(proxyRes.headers);
|
|
152
|
-
res.writeHead(proxyRes.statusCode ?? 502, responseHeaders);
|
|
153
|
-
proxyRes.pipe(res, { end: true });
|
|
154
|
-
});
|
|
155
|
-
proxyReq.on("error", (err) => {
|
|
156
|
-
logger.warn({ err, hostname: route.hostname, port: route.port }, "Script proxy: upstream unreachable");
|
|
157
|
-
if (!res.headersSent) {
|
|
158
|
-
res.writeHead(502, { "content-type": "text/plain" });
|
|
159
|
-
res.end("502 Bad Gateway");
|
|
160
|
-
}
|
|
161
|
-
});
|
|
162
|
-
req.pipe(proxyReq, { end: true });
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
// ---------------------------------------------------------------------------
|
|
166
|
-
// createScriptProxyUpgradeHandler
|
|
167
|
-
// ---------------------------------------------------------------------------
|
|
168
|
-
export function createScriptProxyUpgradeHandler({ routeStore, logger, }) {
|
|
169
|
-
return (req, socket, head) => {
|
|
170
|
-
const hostHeader = req.headers.host;
|
|
171
|
-
if (!hostHeader) {
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
const route = routeStore.findRoute(hostHeader);
|
|
175
|
-
if (!route) {
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
const targetSocket = net.connect({ host: "127.0.0.1", port: route.port }, () => {
|
|
179
|
-
// Reconstruct the raw HTTP upgrade request to send to the target
|
|
180
|
-
const forwardedHeaders = stripHopByHopHeaders(req.headers);
|
|
181
|
-
forwardedHeaders["x-forwarded-for"] = req.socket.remoteAddress ?? "127.0.0.1";
|
|
182
|
-
forwardedHeaders["x-forwarded-host"] = hostHeader.replace(/:\d+$/, "");
|
|
183
|
-
forwardedHeaders["x-forwarded-proto"] = "http";
|
|
184
|
-
// Re-include upgrade and connection headers — they are required for
|
|
185
|
-
// WebSocket handshake even though they are hop-by-hop.
|
|
186
|
-
forwardedHeaders["connection"] = "Upgrade";
|
|
187
|
-
forwardedHeaders["upgrade"] = req.headers.upgrade ?? "websocket";
|
|
188
|
-
const headerLines = [];
|
|
189
|
-
headerLines.push(`${req.method ?? "GET"} ${req.url ?? "/"} HTTP/${req.httpVersion}`);
|
|
190
|
-
for (const [key, value] of Object.entries(forwardedHeaders)) {
|
|
191
|
-
if (Array.isArray(value)) {
|
|
192
|
-
for (const v of value) {
|
|
193
|
-
headerLines.push(`${key}: ${v}`);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
else {
|
|
197
|
-
headerLines.push(`${key}: ${value}`);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
headerLines.push("\r\n");
|
|
201
|
-
targetSocket.write(headerLines.join("\r\n"));
|
|
202
|
-
if (head.length > 0) {
|
|
203
|
-
targetSocket.write(head);
|
|
204
|
-
}
|
|
205
|
-
// Pipe in both directions
|
|
206
|
-
targetSocket.pipe(socket);
|
|
207
|
-
socket.pipe(targetSocket);
|
|
208
|
-
});
|
|
209
|
-
targetSocket.on("error", (err) => {
|
|
210
|
-
logger.warn({ err, hostname: route.hostname, port: route.port }, "Script proxy: WebSocket upstream unreachable");
|
|
211
|
-
socket.end();
|
|
212
|
-
});
|
|
213
|
-
socket.on("error", () => {
|
|
214
|
-
targetSocket.destroy();
|
|
215
|
-
});
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
// ---------------------------------------------------------------------------
|
|
219
|
-
// findFreePort
|
|
220
|
-
// ---------------------------------------------------------------------------
|
|
221
|
-
export function findFreePort() {
|
|
222
|
-
return new Promise((resolve, reject) => {
|
|
223
|
-
const server = net.createServer();
|
|
224
|
-
server.unref();
|
|
225
|
-
server.listen(0, "127.0.0.1", () => {
|
|
226
|
-
const address = server.address();
|
|
227
|
-
if (!address || typeof address === "string") {
|
|
228
|
-
server.close();
|
|
229
|
-
reject(new Error("Failed to get assigned port"));
|
|
230
|
-
return;
|
|
231
|
-
}
|
|
232
|
-
const { port } = address;
|
|
233
|
-
server.close((err) => {
|
|
234
|
-
if (err) {
|
|
235
|
-
reject(err);
|
|
236
|
-
}
|
|
237
|
-
else {
|
|
238
|
-
resolve(port);
|
|
239
|
-
}
|
|
240
|
-
});
|
|
241
|
-
});
|
|
242
|
-
server.on("error", reject);
|
|
243
|
-
});
|
|
244
|
-
}
|
|
1
|
+
export { createScriptProxyMiddleware, createScriptProxyUpgradeHandler, findFreePort, ScriptRouteStore, } from "./service-proxy.js";
|
|
245
2
|
//# sourceMappingURL=script-proxy.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Logger } from "pino";
|
|
2
|
-
import type {
|
|
2
|
+
import type { ServiceProxySubsystem } from "./service-proxy.js";
|
|
3
3
|
interface BranchChangeRouteHandlerOptions {
|
|
4
|
-
|
|
4
|
+
serviceProxy: ServiceProxySubsystem;
|
|
5
5
|
onRoutesChanged: (workspaceId: string) => void;
|
|
6
6
|
logger?: Logger;
|
|
7
7
|
}
|
|
@@ -1,44 +1,10 @@
|
|
|
1
|
-
import { buildScriptHostname } from "../utils/script-hostname.js";
|
|
2
1
|
export function createBranchChangeRouteHandler(options) {
|
|
3
2
|
return (workspaceId, _oldBranch, newBranch) => {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
if (routes.length === 0) {
|
|
3
|
+
const changed = options.serviceProxy.replaceWorkspaceBranchRoutes({ workspaceId, newBranch });
|
|
4
|
+
if (!changed) {
|
|
7
5
|
return;
|
|
8
6
|
}
|
|
9
|
-
|
|
10
|
-
for (const route of routes) {
|
|
11
|
-
const newHostname = buildScriptHostname({
|
|
12
|
-
projectSlug: route.projectSlug,
|
|
13
|
-
branchName: newBranch,
|
|
14
|
-
scriptName: route.scriptName,
|
|
15
|
-
});
|
|
16
|
-
if (newHostname !== route.hostname) {
|
|
17
|
-
updates.push({
|
|
18
|
-
oldHostname: route.hostname,
|
|
19
|
-
newHostname,
|
|
20
|
-
route,
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
if (updates.length === 0) {
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
for (const { oldHostname, newHostname, route } of updates) {
|
|
28
|
-
options.routeStore.removeRoute(oldHostname);
|
|
29
|
-
options.routeStore.registerRoute({
|
|
30
|
-
hostname: newHostname,
|
|
31
|
-
port: route.port,
|
|
32
|
-
workspaceId: route.workspaceId,
|
|
33
|
-
projectSlug: route.projectSlug,
|
|
34
|
-
scriptName: route.scriptName,
|
|
35
|
-
});
|
|
36
|
-
options.logger?.info({
|
|
37
|
-
oldHostname,
|
|
38
|
-
newHostname,
|
|
39
|
-
scriptName: route.scriptName,
|
|
40
|
-
}, "Updated script route for branch rename");
|
|
41
|
-
}
|
|
7
|
+
options.logger?.info({ workspaceId, newBranch }, "Updated service proxy routes for branch rename");
|
|
42
8
|
options.onRoutesChanged(workspaceId);
|
|
43
9
|
};
|
|
44
10
|
}
|
|
@@ -2,7 +2,7 @@ import type { Logger } from "pino";
|
|
|
2
2
|
import type { SessionOutboundMessage, WorkspaceScriptPayload } from "@getpaseo/protocol/messages";
|
|
3
3
|
import type { PaseoConfig } from "@getpaseo/protocol/paseo-config-schema";
|
|
4
4
|
import type { ScriptHealthEntry, ScriptHealthState } from "./script-health-monitor.js";
|
|
5
|
-
import type {
|
|
5
|
+
import type { ServiceProxySubsystem } from "./service-proxy.js";
|
|
6
6
|
import type { WorkspaceScriptRuntimeStore } from "./workspace-script-runtime-store.js";
|
|
7
7
|
interface SessionEmitter {
|
|
8
8
|
emit(message: SessionOutboundMessage): void;
|
|
@@ -11,9 +11,10 @@ interface BuildWorkspaceScriptPayloadsOptions {
|
|
|
11
11
|
workspaceId: string;
|
|
12
12
|
workspaceDirectory: string;
|
|
13
13
|
paseoConfig: PaseoConfig | null;
|
|
14
|
-
|
|
14
|
+
serviceProxy: ServiceProxySubsystem;
|
|
15
15
|
runtimeStore: WorkspaceScriptRuntimeStore;
|
|
16
16
|
daemonPort: number | null;
|
|
17
|
+
serviceProxyPublicBaseUrl?: string | null;
|
|
17
18
|
gitMetadata?: {
|
|
18
19
|
projectSlug: string;
|
|
19
20
|
currentBranch: string | null;
|
|
@@ -22,11 +23,12 @@ interface BuildWorkspaceScriptPayloadsOptions {
|
|
|
22
23
|
}
|
|
23
24
|
export declare function readPaseoConfigForProjection(workspaceDirectory: string, logger: Logger): PaseoConfig | null;
|
|
24
25
|
export declare function buildWorkspaceScriptPayloads(options: BuildWorkspaceScriptPayloadsOptions): WorkspaceScriptPayload[];
|
|
25
|
-
export declare function createScriptStatusEmitter({ sessions,
|
|
26
|
+
export declare function createScriptStatusEmitter({ sessions, serviceProxy, runtimeStore, daemonPort, serviceProxyPublicBaseUrl, resolveWorkspaceDirectory, logger, }: {
|
|
26
27
|
sessions: () => SessionEmitter[];
|
|
27
|
-
|
|
28
|
+
serviceProxy: ServiceProxySubsystem;
|
|
28
29
|
runtimeStore: WorkspaceScriptRuntimeStore;
|
|
29
30
|
daemonPort: number | null | (() => number | null);
|
|
31
|
+
serviceProxyPublicBaseUrl?: string | null;
|
|
30
32
|
resolveWorkspaceDirectory: (workspaceId: string) => string | null | Promise<string | null>;
|
|
31
33
|
logger: Logger;
|
|
32
34
|
}): (workspaceId: string, scripts: ScriptHealthEntry[]) => void;
|