overmux 0.0.1 → 0.0.2
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/CHANGELOG.md +16 -0
- package/README.md +7 -0
- package/dist/auth/.vite/manifest.json +11 -0
- package/dist/auth/assets/index-Bx8JhwVi.css +1 -0
- package/dist/auth/assets/index-CHIE7qYj.js +72 -0
- package/dist/auth/index.html +14 -0
- package/dist/bin.js +4504 -7870
- package/dist/bin.js.map +1 -0
- package/dist/docs/000-index.md +21 -0
- package/dist/docs/100-introduction/100-what-is-overmux.md +7 -0
- package/dist/docs/100-introduction/200-how-overmux-works.md +7 -0
- package/dist/docs/100-introduction/300-why-overmux.md +22 -0
- package/dist/docs/200-getting-started/100-install-and-run-overmux.md +51 -0
- package/dist/docs/200-getting-started/200-install-overmux-desktop.md +41 -0
- package/dist/docs/200-getting-started/300-install-overmux-pwa.mdx +92 -0
- package/dist/docs/200-getting-started/400-secure-with-https/100-choose-an-https-setup.md +14 -0
- package/dist/docs/200-getting-started/400-secure-with-https/200-tailscale-serve.md +44 -0
- package/dist/docs/200-getting-started/400-secure-with-https/300-cloudflare-tunnel.md +59 -0
- package/dist/docs/200-getting-started/400-secure-with-https/400-self-hosted-reverse-proxy.md +32 -0
- package/dist/docs/300-fundamentals/100-project-structure.md +3 -0
- package/dist/docs/300-fundamentals/200-configuration.md +3 -0
- package/dist/docs/300-fundamentals/300-theming.md +3 -0
- package/dist/docs/300-fundamentals/400-server.md +3 -0
- package/dist/docs/300-fundamentals/500-client.md +3 -0
- package/dist/docs/300-fundamentals/600-operations.md +3 -0
- package/dist/docs/300-fundamentals/700-resources.md +3 -0
- package/dist/docs/300-fundamentals/800-streams.md +3 -0
- package/dist/docs/300-fundamentals/900-authentication-and-security.md +3 -0
- package/dist/docs/400-reference/100-configuration.md +3 -0
- package/dist/docs/400-reference/200-server-api.md +3 -0
- package/dist/docs/400-reference/300-client-api.md +3 -0
- package/dist/docs/400-reference/400-cli/100-serve.md +33 -0
- package/dist/docs/400-reference/400-cli/200-auth.md +52 -0
- package/dist/docs/400-reference/400-cli/300-call.md +28 -0
- package/dist/docs/400-reference/400-cli/400-check.md +20 -0
- package/dist/docs/400-reference/400-cli/500-ai-context.md +102 -0
- package/dist/docs/400-reference/400-cli/600-docs.md +23 -0
- package/dist/docs/400-reference/400-cli/700-desktop.md +35 -0
- package/dist/docs/500-hosted-pages.md +31 -0
- package/dist/exports/client.d.ts +237 -0
- package/dist/exports/client.d.ts.map +1 -0
- package/dist/exports/client.js +1643 -0
- package/dist/exports/client.js.map +1 -0
- package/dist/exports/index-D3_Dpy_T.d.ts +155 -0
- package/dist/exports/index-D3_Dpy_T.d.ts.map +1 -0
- package/dist/exports/index.d.ts +2 -0
- package/dist/exports/index.js +49 -0
- package/dist/exports/index.js.map +1 -0
- package/dist/exports/notifications-kDp16bU_.js +56 -0
- package/dist/exports/notifications-kDp16bU_.js.map +1 -0
- package/dist/exports/server.d.ts +63 -0
- package/dist/exports/server.d.ts.map +1 -0
- package/dist/exports/server.js +1120 -0
- package/dist/exports/server.js.map +1 -0
- package/dist/exports/style.css +368 -0
- package/dist/internal/server/coordinator/server-child.d.ts +1 -0
- package/dist/internal/server/coordinator/server-child.js +3299 -0
- package/dist/internal/server/coordinator/server-child.js.map +1 -0
- package/docs/000-index.md +21 -0
- package/docs/100-introduction/100-what-is-overmux.md +7 -0
- package/docs/100-introduction/200-how-overmux-works.md +7 -0
- package/docs/100-introduction/300-why-overmux.md +22 -0
- package/docs/200-getting-started/100-install-and-run-overmux.md +51 -0
- package/docs/200-getting-started/200-install-overmux-desktop.md +41 -0
- package/docs/200-getting-started/300-install-overmux-pwa.mdx +92 -0
- package/docs/200-getting-started/400-secure-with-https/100-choose-an-https-setup.md +14 -0
- package/docs/200-getting-started/400-secure-with-https/200-tailscale-serve.md +44 -0
- package/docs/200-getting-started/400-secure-with-https/300-cloudflare-tunnel.md +59 -0
- package/docs/200-getting-started/400-secure-with-https/400-self-hosted-reverse-proxy.md +32 -0
- package/docs/300-fundamentals/100-project-structure.md +3 -0
- package/docs/300-fundamentals/200-configuration.md +3 -0
- package/docs/300-fundamentals/300-theming.md +3 -0
- package/docs/300-fundamentals/400-server.md +3 -0
- package/docs/300-fundamentals/500-client.md +3 -0
- package/docs/300-fundamentals/600-operations.md +3 -0
- package/docs/300-fundamentals/700-resources.md +3 -0
- package/docs/300-fundamentals/800-streams.md +3 -0
- package/docs/300-fundamentals/900-authentication-and-security.md +3 -0
- package/docs/400-reference/100-configuration.md +3 -0
- package/docs/400-reference/200-server-api.md +3 -0
- package/docs/400-reference/300-client-api.md +3 -0
- package/docs/400-reference/400-cli/100-serve.md +33 -0
- package/docs/400-reference/400-cli/200-auth.md +52 -0
- package/docs/400-reference/400-cli/300-call.md +28 -0
- package/docs/400-reference/400-cli/400-check.md +20 -0
- package/docs/400-reference/400-cli/500-ai-context.md +102 -0
- package/docs/400-reference/400-cli/600-docs.md +23 -0
- package/docs/400-reference/400-cli/700-desktop.md +35 -0
- package/docs/500-hosted-pages.md +31 -0
- package/package.json +59 -18
- package/src/internal/cli/app.ts +72 -0
- package/src/internal/cli/bin.ts +3 -0
- package/src/internal/cli/commands/ai.ts +44 -0
- package/src/internal/cli/commands/auth.ts +195 -0
- package/src/internal/cli/commands/call.ts +128 -0
- package/src/internal/cli/commands/check.ts +48 -0
- package/src/internal/cli/commands/desktop-installer.ts +446 -0
- package/src/internal/cli/commands/desktop-release.ts +190 -0
- package/src/internal/cli/commands/desktop.ts +47 -0
- package/src/internal/cli/commands/docs.ts +34 -0
- package/src/internal/cli/commands/serve.ts +322 -0
- package/src/internal/cli/environment.ts +54 -0
- package/src/internal/cli/login.ts +46 -0
- package/src/internal/cli/managed-vite.ts +267 -0
- package/src/internal/cli/parse-port.ts +8 -0
- package/src/internal/client/auth/auth-api.ts +53 -0
- package/src/internal/client/auth/auth-shell.css +74 -0
- package/src/internal/client/auth/auth-shell.tsx +167 -0
- package/src/internal/client/auth/entry.tsx +26 -0
- package/src/internal/client/auth/index.html +13 -0
- package/src/internal/client/background-notifications.ts +194 -0
- package/src/internal/client/browser-api.ts +97 -0
- package/src/internal/client/client-definition.ts +184 -0
- package/src/internal/client/commands.tsx +260 -0
- package/src/internal/client/host/browser-log-forwarding.ts +149 -0
- package/src/internal/client/host/hosted-pages/browser-notification-settings.tsx +118 -0
- package/src/internal/client/host/hosted-pages/hosted-page.css +87 -0
- package/src/internal/client/host/hosted-pages/hosted-page.tsx +9 -0
- package/src/internal/client/host/hosted-pages/logout-page.tsx +75 -0
- package/src/internal/client/host/hosted-pages/settings-page.tsx +45 -0
- package/src/internal/client/host/notification-forwarding.ts +22 -0
- package/src/internal/client/host/overmux-host.tsx +200 -0
- package/src/internal/client/host/recovery-screen.css +75 -0
- package/src/internal/client/host/recovery-screen.tsx +17 -0
- package/src/internal/client/host/update-popover.css +82 -0
- package/src/internal/client/host/update-popover.tsx +166 -0
- package/src/internal/client/index.ts +45 -0
- package/src/internal/client/overmux-react.ts +283 -0
- package/src/internal/client/shortcuts.tsx +373 -0
- package/src/internal/client/theme-scope.css +114 -0
- package/src/internal/client/theme-scope.tsx +107 -0
- package/src/internal/client/transport.ts +69 -0
- package/src/internal/client/websocket.ts +441 -0
- package/src/internal/server/auth/auth-http.ts +414 -0
- package/src/internal/server/auth/auth-service.ts +627 -0
- package/src/internal/server/auth/auth-shell-http.ts +90 -0
- package/src/internal/server/auth/instance-control.ts +381 -0
- package/src/internal/server/check-overmux.ts +364 -0
- package/src/internal/server/coordinator/ipc-protocol.ts +44 -0
- package/src/internal/server/coordinator/server-child.ts +108 -0
- package/src/internal/server/coordinator/server-coordinator.ts +262 -0
- package/src/internal/server/coordinator/update-watcher.ts +54 -0
- package/src/internal/server/http/authenticated-http-app-test-helper.ts +45 -0
- package/src/internal/server/http/background-notification-handlers.ts +83 -0
- package/src/internal/server/http/control-handlers.ts +44 -0
- package/src/internal/server/http/create-http-app.ts +53 -0
- package/src/internal/server/http/development-web-backend.ts +83 -0
- package/src/internal/server/http/operation-handler.ts +93 -0
- package/src/internal/server/http/request-logging.ts +34 -0
- package/src/internal/server/http/routes.ts +241 -0
- package/src/internal/server/http/runtime-manifest-handler.ts +12 -0
- package/src/internal/server/index.ts +16 -0
- package/src/internal/server/notifications/background-notification-payload.ts +74 -0
- package/src/internal/server/notifications/background-notification-service.ts +139 -0
- package/src/internal/server/notifications/background-notification-store.ts +162 -0
- package/src/internal/server/notifications/notification-service.ts +70 -0
- package/src/internal/server/notifications/private-json-file.ts +84 -0
- package/src/internal/server/paths.ts +13 -0
- package/src/internal/server/rate-limiter.ts +58 -0
- package/src/internal/server/runtime/create-runtime.ts +87 -0
- package/src/internal/server/runtime/resource-dependency-graph.ts +128 -0
- package/src/internal/server/runtime/runtime-lifecycle.ts +95 -0
- package/src/internal/server/runtime/runtime-operations.ts +78 -0
- package/src/internal/server/runtime/runtime-resources.ts +201 -0
- package/src/internal/server/runtime/runtime-streams.ts +130 -0
- package/src/internal/server/server-logger.ts +110 -0
- package/src/internal/server/server-startup-options.ts +51 -0
- package/src/internal/server/start-application-server.ts +412 -0
- package/src/internal/server/start-overmux-server.ts +26 -0
- package/src/internal/server/test-port.ts +23 -0
- package/src/internal/server/vite-websocket-proxy.ts +201 -0
- package/src/internal/server/websocket-connection.ts +586 -0
- package/src/internal/server/websocket-limits.ts +2 -0
- package/src/internal/server/websocket-server.ts +311 -0
- package/src/internal/shared/background-notifications.ts +37 -0
- package/src/internal/shared/index.ts +2 -0
- package/src/internal/shared/protocol.ts +317 -0
- package/src/internal/shared/routes.ts +3 -0
- package/src/public/ai-context.ts +44 -0
- package/src/public/client.ts +53 -0
- package/src/public/config.ts +287 -0
- package/src/public/contracts.ts +55 -0
- package/src/public/index.ts +57 -0
- package/src/public/notifications.ts +10 -0
- package/src/public/server.ts +16 -0
- package/dist/public/core.ts +0 -119
- package/dist/public/protocol.ts +0 -51
- package/dist/public/terminal.ts +0 -23
- package/dist/web/assets/index-DC___IUw.css +0 -2
- package/dist/web/assets/index-DF9Cptse.js +0 -74
- package/dist/web/assets/terminal-component-BrP-ENHg.css +0 -1
- package/dist/web/assets/terminal-component-DyLEbMrx.js +0 -36
- package/dist/web/index.html +0 -14
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
// Starts one immutable application runtime inside the supervised child process.
|
|
2
|
+
// It owns config loading, transport, and the listening socket.
|
|
3
|
+
|
|
4
|
+
import { serve, type ServerType as NodeServer } from "@hono/node-server";
|
|
5
|
+
import {
|
|
6
|
+
loadOvermuxConfig,
|
|
7
|
+
type RuntimeConfigDefinition,
|
|
8
|
+
} from "@overmux/shared/node";
|
|
9
|
+
import { once } from "node:events";
|
|
10
|
+
import type { AddressInfo } from "node:net";
|
|
11
|
+
import { resolve } from "node:path";
|
|
12
|
+
|
|
13
|
+
import { createAuthService, type AuthService } from "./auth/auth-service";
|
|
14
|
+
import {
|
|
15
|
+
createAuthBoundary,
|
|
16
|
+
validateAuthOrigin,
|
|
17
|
+
type AuthBoundary,
|
|
18
|
+
} from "./auth/auth-http";
|
|
19
|
+
import { startInstanceControl } from "./auth/instance-control";
|
|
20
|
+
import { createHttpApp } from "./http/create-http-app";
|
|
21
|
+
import { createWebSocketServer } from "./websocket-server";
|
|
22
|
+
import {
|
|
23
|
+
createBackgroundNotificationService,
|
|
24
|
+
type BackgroundNotificationService,
|
|
25
|
+
} from "./notifications/background-notification-service";
|
|
26
|
+
import { createFileBackgroundNotificationStore } from "./notifications/background-notification-store";
|
|
27
|
+
import { createNotificationService } from "./notifications/notification-service";
|
|
28
|
+
import { createRuntime, type Runtime } from "./runtime/create-runtime";
|
|
29
|
+
import {
|
|
30
|
+
createServerLogger,
|
|
31
|
+
errorDetails,
|
|
32
|
+
type ServerLogger,
|
|
33
|
+
} from "./server-logger";
|
|
34
|
+
import {
|
|
35
|
+
resolveServerStartupOptions,
|
|
36
|
+
type ResolvedServerStartupOptions,
|
|
37
|
+
} from "./server-startup-options";
|
|
38
|
+
import type { OvermuxServerOptions } from "./start-overmux-server";
|
|
39
|
+
|
|
40
|
+
type ResourceCleanup = () => void | Promise<void>;
|
|
41
|
+
type ServerResources = ReturnType<typeof createResources>;
|
|
42
|
+
type NotificationService = ReturnType<typeof createNotificationService>;
|
|
43
|
+
type ApplicationLogger = ReturnType<typeof createServerLogger>;
|
|
44
|
+
type StartApplicationServerOptions = {
|
|
45
|
+
onRestartRequested: () => void;
|
|
46
|
+
options: OvermuxServerOptions;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export type ApplicationServer = {
|
|
50
|
+
announceUpdateAvailable: () => void;
|
|
51
|
+
close: () => Promise<void>;
|
|
52
|
+
host: string;
|
|
53
|
+
port: number;
|
|
54
|
+
url: string;
|
|
55
|
+
watch: boolean;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const cleanupResources = async (cleanups: ResourceCleanup[]) => {
|
|
59
|
+
const errors: unknown[] = [];
|
|
60
|
+
for (const cleanup of [...cleanups].reverse()) {
|
|
61
|
+
try {
|
|
62
|
+
await cleanup();
|
|
63
|
+
} catch (cause) {
|
|
64
|
+
errors.push(cause);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (errors.length > 0) {
|
|
68
|
+
throw new AggregateError(errors, "Failed to clean up server resources");
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const createResources = () => {
|
|
73
|
+
const cleanups: ResourceCleanup[] = [];
|
|
74
|
+
let closePromise: Promise<void> | undefined;
|
|
75
|
+
return {
|
|
76
|
+
add: (cleanup: ResourceCleanup) => cleanups.push(cleanup),
|
|
77
|
+
close: () => {
|
|
78
|
+
const next = closePromise ?? cleanupResources(cleanups);
|
|
79
|
+
closePromise = next;
|
|
80
|
+
return next;
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const closeNodeServer = (
|
|
86
|
+
server: NodeServer,
|
|
87
|
+
cancelActiveWork: () => Promise<void>,
|
|
88
|
+
): Promise<void> => {
|
|
89
|
+
const closed = new Promise<void>((resolveClose, reject) => {
|
|
90
|
+
server.close((error) => {
|
|
91
|
+
if (
|
|
92
|
+
error &&
|
|
93
|
+
(error as NodeJS.ErrnoException).code !== "ERR_SERVER_NOT_RUNNING"
|
|
94
|
+
) {
|
|
95
|
+
reject(error);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
resolveClose();
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
const cancellation = cancelActiveWork();
|
|
102
|
+
if ("closeAllConnections" in server) {
|
|
103
|
+
server.closeAllConnections();
|
|
104
|
+
}
|
|
105
|
+
return Promise.all([closed, cancellation]).then(() => undefined);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const getServerAddress = async (server: NodeServer): Promise<AddressInfo> => {
|
|
109
|
+
if (!server.address()) {
|
|
110
|
+
await once(server, "listening");
|
|
111
|
+
}
|
|
112
|
+
const address = server.address();
|
|
113
|
+
if (!address || typeof address === "string") {
|
|
114
|
+
throw new Error("Overmux server did not bind a TCP address");
|
|
115
|
+
}
|
|
116
|
+
return address;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const registerProcessErrorLogging = (logger: ServerLogger) => {
|
|
120
|
+
const onUncaughtException = (
|
|
121
|
+
cause: Error,
|
|
122
|
+
origin: "uncaughtException" | "unhandledRejection",
|
|
123
|
+
) =>
|
|
124
|
+
logger.log({
|
|
125
|
+
details: errorDetails(cause),
|
|
126
|
+
event:
|
|
127
|
+
origin === "unhandledRejection"
|
|
128
|
+
? "unhandled-rejection"
|
|
129
|
+
: "uncaught-exception",
|
|
130
|
+
level: "error",
|
|
131
|
+
});
|
|
132
|
+
process.on("uncaughtExceptionMonitor", onUncaughtException);
|
|
133
|
+
return () => {
|
|
134
|
+
process.off("uncaughtExceptionMonitor", onUncaughtException);
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const loadServerSettings = async ({
|
|
139
|
+
configPath,
|
|
140
|
+
options,
|
|
141
|
+
}: {
|
|
142
|
+
configPath: string;
|
|
143
|
+
options: OvermuxServerOptions;
|
|
144
|
+
}) => {
|
|
145
|
+
const { config } = await loadOvermuxConfig({
|
|
146
|
+
aliases: options.configAliases,
|
|
147
|
+
configPath,
|
|
148
|
+
});
|
|
149
|
+
const startup = resolveServerStartupOptions({
|
|
150
|
+
config,
|
|
151
|
+
configPath,
|
|
152
|
+
overrides: options,
|
|
153
|
+
});
|
|
154
|
+
return { config, startup };
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const createAuthOrigins = ({
|
|
158
|
+
configuredOrigins,
|
|
159
|
+
startup,
|
|
160
|
+
}: {
|
|
161
|
+
configuredOrigins?: string[];
|
|
162
|
+
startup: ResolvedServerStartupOptions;
|
|
163
|
+
}) => {
|
|
164
|
+
const urlHost = startup.host.includes(":")
|
|
165
|
+
? `[${startup.host}]`
|
|
166
|
+
: startup.host;
|
|
167
|
+
let values = (configuredOrigins ?? [`http://${urlHost}:${startup.port}`]).map(
|
|
168
|
+
validateAuthOrigin,
|
|
169
|
+
);
|
|
170
|
+
return {
|
|
171
|
+
establish: (localUrl: string) => {
|
|
172
|
+
values = (configuredOrigins ?? [localUrl]).map(validateAuthOrigin);
|
|
173
|
+
return values;
|
|
174
|
+
},
|
|
175
|
+
get: () => values,
|
|
176
|
+
};
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const createApplicationServices = async ({
|
|
180
|
+
config,
|
|
181
|
+
logger,
|
|
182
|
+
resources,
|
|
183
|
+
startup,
|
|
184
|
+
}: {
|
|
185
|
+
config: RuntimeConfigDefinition;
|
|
186
|
+
logger: ServerLogger;
|
|
187
|
+
resources: ServerResources;
|
|
188
|
+
startup: ResolvedServerStartupOptions;
|
|
189
|
+
}) => {
|
|
190
|
+
// Browser origins are fixed from trusted startup data, never discovered from
|
|
191
|
+
// requests. Ephemeral listener ports are finalized after binding.
|
|
192
|
+
const authOrigins = createAuthOrigins({
|
|
193
|
+
configuredOrigins: config.auth.origins,
|
|
194
|
+
startup,
|
|
195
|
+
});
|
|
196
|
+
const authService = createAuthService({ config: config.auth });
|
|
197
|
+
const auth = createAuthBoundary({
|
|
198
|
+
origins: authOrigins.get,
|
|
199
|
+
service: authService,
|
|
200
|
+
trustedProxyPeer: config.auth.trustedProxyPeer,
|
|
201
|
+
});
|
|
202
|
+
const backgroundNotifications = createBackgroundNotificationService({
|
|
203
|
+
isSessionActive: authService.isSessionActive,
|
|
204
|
+
logger,
|
|
205
|
+
store: createFileBackgroundNotificationStore(),
|
|
206
|
+
});
|
|
207
|
+
// Background subscriptions belong to browser sessions, so revocation also
|
|
208
|
+
// removes delivery credentials owned by those sessions.
|
|
209
|
+
resources.add(
|
|
210
|
+
authService.onSessionsRevoked((sessionIds) => {
|
|
211
|
+
void backgroundNotifications
|
|
212
|
+
.disableSessions(sessionIds)
|
|
213
|
+
.catch((cause: unknown) => {
|
|
214
|
+
logger.log({
|
|
215
|
+
details: errorDetails(cause),
|
|
216
|
+
event: "background-notification-session-cleanup-failed",
|
|
217
|
+
level: "error",
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
}),
|
|
221
|
+
);
|
|
222
|
+
const notifications = createNotificationService({
|
|
223
|
+
backgroundNotifications,
|
|
224
|
+
logger,
|
|
225
|
+
});
|
|
226
|
+
const runtime = await createRuntime({
|
|
227
|
+
config,
|
|
228
|
+
debug: logger.enabled,
|
|
229
|
+
notifications,
|
|
230
|
+
});
|
|
231
|
+
resources.add(runtime.dispose);
|
|
232
|
+
return {
|
|
233
|
+
auth,
|
|
234
|
+
authOrigins,
|
|
235
|
+
authService,
|
|
236
|
+
backgroundNotifications,
|
|
237
|
+
notifications,
|
|
238
|
+
runtime,
|
|
239
|
+
};
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
const startApplicationTransport = async ({
|
|
243
|
+
auth,
|
|
244
|
+
authOrigins,
|
|
245
|
+
authService,
|
|
246
|
+
backgroundNotifications,
|
|
247
|
+
developmentWebTarget,
|
|
248
|
+
logger,
|
|
249
|
+
notifications,
|
|
250
|
+
onRestartRequested,
|
|
251
|
+
resources,
|
|
252
|
+
runtime,
|
|
253
|
+
startup,
|
|
254
|
+
}: {
|
|
255
|
+
auth: AuthBoundary;
|
|
256
|
+
authOrigins: ReturnType<typeof createAuthOrigins>;
|
|
257
|
+
authService: AuthService;
|
|
258
|
+
backgroundNotifications: BackgroundNotificationService;
|
|
259
|
+
developmentWebTarget?: string;
|
|
260
|
+
logger: ServerLogger;
|
|
261
|
+
notifications: NotificationService;
|
|
262
|
+
onRestartRequested: () => void;
|
|
263
|
+
resources: ServerResources;
|
|
264
|
+
runtime: Runtime;
|
|
265
|
+
startup: ResolvedServerStartupOptions;
|
|
266
|
+
}) => {
|
|
267
|
+
let announceRestarting: () => void = () => undefined;
|
|
268
|
+
const app = createHttpApp({
|
|
269
|
+
auth,
|
|
270
|
+
backgroundNotifications,
|
|
271
|
+
developmentWebTarget,
|
|
272
|
+
onRestartRequested: () => {
|
|
273
|
+
announceRestarting();
|
|
274
|
+
onRestartRequested();
|
|
275
|
+
},
|
|
276
|
+
runtime,
|
|
277
|
+
serverLogger: logger,
|
|
278
|
+
webAssetsDir: developmentWebTarget
|
|
279
|
+
? undefined
|
|
280
|
+
: startup.productionWebAssetsDir,
|
|
281
|
+
});
|
|
282
|
+
const server = serve({
|
|
283
|
+
fetch: app.fetch,
|
|
284
|
+
hostname: startup.host,
|
|
285
|
+
port: startup.port,
|
|
286
|
+
});
|
|
287
|
+
resources.add(() => closeNodeServer(server, runtime.dispose));
|
|
288
|
+
const webSockets = createWebSocketServer({
|
|
289
|
+
auth,
|
|
290
|
+
developmentWebTarget,
|
|
291
|
+
runtime,
|
|
292
|
+
server,
|
|
293
|
+
serverLogger: logger,
|
|
294
|
+
});
|
|
295
|
+
resources.add(webSockets.close);
|
|
296
|
+
announceRestarting = webSockets.announceRestarting;
|
|
297
|
+
notifications.attachLiveDelivery(webSockets.broadcastNotification);
|
|
298
|
+
|
|
299
|
+
const address = await getServerAddress(server);
|
|
300
|
+
const urlHost = startup.host.includes(":")
|
|
301
|
+
? `[${startup.host}]`
|
|
302
|
+
: startup.host;
|
|
303
|
+
const localUrl = `http://${urlHost}:${address.port}`;
|
|
304
|
+
const controlApiHost =
|
|
305
|
+
startup.host === "0.0.0.0"
|
|
306
|
+
? "127.0.0.1"
|
|
307
|
+
: startup.host === "::"
|
|
308
|
+
? "[::1]"
|
|
309
|
+
: urlHost;
|
|
310
|
+
const origins = authOrigins.establish(localUrl);
|
|
311
|
+
authService.setOrigins(origins);
|
|
312
|
+
const instanceControl = await startInstanceControl({
|
|
313
|
+
auth: authService,
|
|
314
|
+
apiUrl: `http://${controlApiHost}:${address.port}`,
|
|
315
|
+
port: address.port,
|
|
316
|
+
url: origins[0]!,
|
|
317
|
+
});
|
|
318
|
+
resources.add(instanceControl.close);
|
|
319
|
+
return { address, app, localUrl, webSockets };
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const closeApplicationServer = async ({
|
|
323
|
+
app,
|
|
324
|
+
logger,
|
|
325
|
+
resources,
|
|
326
|
+
}: {
|
|
327
|
+
app: ReturnType<typeof createHttpApp>;
|
|
328
|
+
logger: ServerLogger;
|
|
329
|
+
resources: ServerResources;
|
|
330
|
+
}) => {
|
|
331
|
+
logger.log({ event: "server-stop" });
|
|
332
|
+
app.beginShutdown();
|
|
333
|
+
await resources.close();
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
const failApplicationServerStartup = async (
|
|
337
|
+
cause: unknown,
|
|
338
|
+
resources: ServerResources,
|
|
339
|
+
): Promise<never> => {
|
|
340
|
+
try {
|
|
341
|
+
await resources.close();
|
|
342
|
+
} catch (cleanupCause) {
|
|
343
|
+
throw new AggregateError(
|
|
344
|
+
[cause, cleanupCause],
|
|
345
|
+
"Server startup and cleanup failed",
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
throw cause;
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
export const startApplicationServer = async ({
|
|
352
|
+
onRestartRequested,
|
|
353
|
+
options,
|
|
354
|
+
}: StartApplicationServerOptions): Promise<ApplicationServer> => {
|
|
355
|
+
const logger = createServerLogger({
|
|
356
|
+
enabled: options.debug ?? false,
|
|
357
|
+
logFile: options.logFile ? resolve(options.logFile) : undefined,
|
|
358
|
+
});
|
|
359
|
+
const resources = createResources();
|
|
360
|
+
resources.add(registerProcessErrorLogging(logger));
|
|
361
|
+
|
|
362
|
+
try {
|
|
363
|
+
const configPath = resolve(options.configPath);
|
|
364
|
+
logger.log({ details: { configPath }, event: "server-start" });
|
|
365
|
+
const { config, startup } = await loadServerSettings({
|
|
366
|
+
configPath,
|
|
367
|
+
options,
|
|
368
|
+
});
|
|
369
|
+
logger.setEnabled(Boolean(options.debug || config.debug));
|
|
370
|
+
if (startup.logFile) {
|
|
371
|
+
logger.setLogFile(startup.logFile);
|
|
372
|
+
}
|
|
373
|
+
const {
|
|
374
|
+
auth,
|
|
375
|
+
authOrigins,
|
|
376
|
+
authService,
|
|
377
|
+
backgroundNotifications,
|
|
378
|
+
notifications,
|
|
379
|
+
runtime,
|
|
380
|
+
} = await createApplicationServices({
|
|
381
|
+
config,
|
|
382
|
+
logger,
|
|
383
|
+
resources,
|
|
384
|
+
startup,
|
|
385
|
+
});
|
|
386
|
+
const { address, app, localUrl, webSockets } =
|
|
387
|
+
await startApplicationTransport({
|
|
388
|
+
auth,
|
|
389
|
+
authOrigins,
|
|
390
|
+
authService,
|
|
391
|
+
backgroundNotifications,
|
|
392
|
+
developmentWebTarget: options.developmentWebTarget,
|
|
393
|
+
logger,
|
|
394
|
+
notifications,
|
|
395
|
+
onRestartRequested,
|
|
396
|
+
resources,
|
|
397
|
+
runtime,
|
|
398
|
+
startup,
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
return {
|
|
402
|
+
announceUpdateAvailable: webSockets.announceUpdateAvailable,
|
|
403
|
+
close: () => closeApplicationServer({ app, logger, resources }),
|
|
404
|
+
host: startup.host,
|
|
405
|
+
port: address.port,
|
|
406
|
+
url: localUrl,
|
|
407
|
+
watch: startup.watch,
|
|
408
|
+
};
|
|
409
|
+
} catch (cause) {
|
|
410
|
+
return failApplicationServerStartup(cause, resources);
|
|
411
|
+
}
|
|
412
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ServerConfigDefinition } from "../../public/index";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { startServerCoordinator } from "./coordinator/server-coordinator";
|
|
5
|
+
|
|
6
|
+
export type OvermuxServerOptions = ServerConfigDefinition & {
|
|
7
|
+
configAliases?: Record<string, string>;
|
|
8
|
+
debug?: boolean;
|
|
9
|
+
developmentWebTarget?: string;
|
|
10
|
+
configPath: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type OvermuxServer = {
|
|
14
|
+
announceUpdateAvailable: () => void;
|
|
15
|
+
close: () => Promise<void>;
|
|
16
|
+
port: number;
|
|
17
|
+
url: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const startOvermuxServer = async (
|
|
21
|
+
options: OvermuxServerOptions,
|
|
22
|
+
): Promise<OvermuxServer> =>
|
|
23
|
+
startServerCoordinator({
|
|
24
|
+
...options,
|
|
25
|
+
configPath: resolve(options.configPath),
|
|
26
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Shared utility for tests that need an ephemeral local port.
|
|
2
|
+
import { createServer } from "node:net";
|
|
3
|
+
|
|
4
|
+
export const findAvailablePort = async (): Promise<number> =>
|
|
5
|
+
new Promise((resolve, reject) => {
|
|
6
|
+
const server = createServer();
|
|
7
|
+
server.once("error", reject);
|
|
8
|
+
server.listen(0, "127.0.0.1", () => {
|
|
9
|
+
const address = server.address();
|
|
10
|
+
if (!address || typeof address === "string") {
|
|
11
|
+
server.close();
|
|
12
|
+
reject(new Error("Failed to allocate a test port"));
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
server.close((cause) => {
|
|
16
|
+
if (cause) {
|
|
17
|
+
reject(cause);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
resolve(address.port);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
});
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import type { IncomingMessage } from "node:http";
|
|
2
|
+
import type { Socket } from "node:net";
|
|
3
|
+
import type { Duplex } from "node:stream";
|
|
4
|
+
import { proxyUpgrade } from "httpxy";
|
|
5
|
+
|
|
6
|
+
import { webSocketCloseCode } from "../shared/index";
|
|
7
|
+
import type { AuthBoundary, RequestSession } from "./auth/auth-http";
|
|
8
|
+
import {
|
|
9
|
+
errorDetails as diagnosticErrorDetails,
|
|
10
|
+
type ServerLogger,
|
|
11
|
+
} from "./server-logger";
|
|
12
|
+
|
|
13
|
+
type ProxiedSocket = {
|
|
14
|
+
client: Duplex;
|
|
15
|
+
closed: boolean;
|
|
16
|
+
sessionId: string;
|
|
17
|
+
timer: NodeJS.Timeout;
|
|
18
|
+
upstream?: Socket;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const setTrustedForwardingHeaders = (
|
|
22
|
+
request: IncomingMessage,
|
|
23
|
+
origin: string,
|
|
24
|
+
) => {
|
|
25
|
+
Object.keys(request.headers).forEach((name) => {
|
|
26
|
+
if (
|
|
27
|
+
name === "authorization" ||
|
|
28
|
+
name === "cookie" ||
|
|
29
|
+
name === "forwarded" ||
|
|
30
|
+
name.startsWith("x-forwarded-")
|
|
31
|
+
) {
|
|
32
|
+
Reflect.deleteProperty(request.headers, name);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
const publicUrl = new URL(origin);
|
|
36
|
+
request.headers["x-forwarded-for"] = request.socket.remoteAddress;
|
|
37
|
+
request.headers["x-forwarded-host"] = publicUrl.host;
|
|
38
|
+
request.headers["x-forwarded-port"] =
|
|
39
|
+
publicUrl.port || (publicUrl.protocol === "https:" ? "443" : "80");
|
|
40
|
+
request.headers["x-forwarded-proto"] = publicUrl.protocol.slice(0, -1);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// httpxy preserves an established WebSocket as a raw tunnel, so gateway-owned
|
|
44
|
+
// revocation writes the server close frame before ending both sides.
|
|
45
|
+
const closeFrame = (code: number, reason: string) => {
|
|
46
|
+
const payload = Buffer.allocUnsafe(2 + Buffer.byteLength(reason));
|
|
47
|
+
payload.writeUInt16BE(code);
|
|
48
|
+
payload.write(reason, 2);
|
|
49
|
+
return Buffer.concat([Buffer.from([0x88, payload.length]), payload]);
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const closeSocket = (socket: ProxiedSocket, code: number, reason: string) => {
|
|
53
|
+
if (socket.closed) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
socket.closed = true;
|
|
57
|
+
clearInterval(socket.timer);
|
|
58
|
+
if (!socket.client.destroyed) {
|
|
59
|
+
socket.client.end(closeFrame(code, reason));
|
|
60
|
+
}
|
|
61
|
+
socket.upstream?.destroy();
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const revalidateSocket = async ({
|
|
65
|
+
auth,
|
|
66
|
+
serverLogger,
|
|
67
|
+
origin,
|
|
68
|
+
socket,
|
|
69
|
+
token,
|
|
70
|
+
}: {
|
|
71
|
+
auth: AuthBoundary;
|
|
72
|
+
origin: string;
|
|
73
|
+
serverLogger?: ServerLogger;
|
|
74
|
+
socket: ProxiedSocket;
|
|
75
|
+
token: string;
|
|
76
|
+
}) => {
|
|
77
|
+
try {
|
|
78
|
+
if (!(await auth.service.authenticateToken(token, origin))) {
|
|
79
|
+
closeSocket(
|
|
80
|
+
socket,
|
|
81
|
+
webSocketCloseCode.authenticationRevoked,
|
|
82
|
+
"authentication revoked",
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
} catch (cause) {
|
|
86
|
+
serverLogger?.log({
|
|
87
|
+
details: diagnosticErrorDetails(cause),
|
|
88
|
+
event: "vite-websocket-session-revalidation-failure",
|
|
89
|
+
level: "error",
|
|
90
|
+
});
|
|
91
|
+
closeSocket(
|
|
92
|
+
socket,
|
|
93
|
+
webSocketCloseCode.authenticationUnavailable,
|
|
94
|
+
"authentication unavailable",
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export const createViteWebSocketProxy = ({
|
|
100
|
+
auth,
|
|
101
|
+
serverLogger,
|
|
102
|
+
target,
|
|
103
|
+
}: {
|
|
104
|
+
auth: AuthBoundary;
|
|
105
|
+
serverLogger?: ServerLogger;
|
|
106
|
+
target: string;
|
|
107
|
+
}) => {
|
|
108
|
+
const sockets = new Map<Duplex, ProxiedSocket>();
|
|
109
|
+
|
|
110
|
+
const connect = ({
|
|
111
|
+
correlationId,
|
|
112
|
+
head,
|
|
113
|
+
origin,
|
|
114
|
+
request,
|
|
115
|
+
session,
|
|
116
|
+
socket,
|
|
117
|
+
}: {
|
|
118
|
+
correlationId?: string;
|
|
119
|
+
head: Buffer;
|
|
120
|
+
origin: string;
|
|
121
|
+
request: IncomingMessage;
|
|
122
|
+
session: RequestSession;
|
|
123
|
+
socket: Duplex;
|
|
124
|
+
}) => {
|
|
125
|
+
let revalidationPending = false;
|
|
126
|
+
const proxied: ProxiedSocket = {
|
|
127
|
+
client: socket,
|
|
128
|
+
closed: false,
|
|
129
|
+
sessionId: session.id,
|
|
130
|
+
timer: setInterval(() => {
|
|
131
|
+
if (revalidationPending) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
revalidationPending = true;
|
|
135
|
+
void revalidateSocket({
|
|
136
|
+
auth,
|
|
137
|
+
origin,
|
|
138
|
+
serverLogger,
|
|
139
|
+
socket: proxied,
|
|
140
|
+
token: session.token,
|
|
141
|
+
}).finally(() => {
|
|
142
|
+
revalidationPending = false;
|
|
143
|
+
});
|
|
144
|
+
}, 1_000),
|
|
145
|
+
};
|
|
146
|
+
sockets.set(socket, proxied);
|
|
147
|
+
socket.once("close", () => {
|
|
148
|
+
clearInterval(proxied.timer);
|
|
149
|
+
sockets.delete(socket);
|
|
150
|
+
});
|
|
151
|
+
setTrustedForwardingHeaders(request, origin);
|
|
152
|
+
void proxyUpgrade(target, request, socket, head, {
|
|
153
|
+
changeOrigin: true,
|
|
154
|
+
xfwd: false,
|
|
155
|
+
}).then(
|
|
156
|
+
(upstream) => {
|
|
157
|
+
// Revocation can win while httpxy is opening the private side.
|
|
158
|
+
if (proxied.closed) {
|
|
159
|
+
upstream.destroy();
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
proxied.upstream = upstream;
|
|
163
|
+
},
|
|
164
|
+
(cause: unknown) => {
|
|
165
|
+
clearInterval(proxied.timer);
|
|
166
|
+
sockets.delete(socket);
|
|
167
|
+
if (proxied.closed) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
proxied.closed = true;
|
|
171
|
+
serverLogger?.log({
|
|
172
|
+
correlationId,
|
|
173
|
+
details: diagnosticErrorDetails(cause),
|
|
174
|
+
event: "vite-websocket-proxy-failure",
|
|
175
|
+
level: "error",
|
|
176
|
+
});
|
|
177
|
+
},
|
|
178
|
+
);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
close: () => {
|
|
183
|
+
sockets.forEach((socket) =>
|
|
184
|
+
closeSocket(socket, 1001, "server shutting down"),
|
|
185
|
+
);
|
|
186
|
+
sockets.clear();
|
|
187
|
+
},
|
|
188
|
+
connect,
|
|
189
|
+
revoke: (ids: ReadonlySet<string>) => {
|
|
190
|
+
sockets.forEach((socket) => {
|
|
191
|
+
if (ids.has(socket.sessionId)) {
|
|
192
|
+
closeSocket(
|
|
193
|
+
socket,
|
|
194
|
+
webSocketCloseCode.authenticationRevoked,
|
|
195
|
+
"authentication revoked",
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
};
|