overmux 0.0.3 → 0.0.5
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 +44 -0
- package/README.md +2 -2
- package/dist/auth/.vite/manifest.json +2 -2
- package/dist/auth/assets/{index-Bx8JhwVi.css → index-BM9gETvx.css} +1 -1
- package/dist/auth/assets/{index-CHIE7qYj.js → index-DabAfcs1.js} +1 -1
- package/dist/auth/index.html +2 -2
- package/dist/bin.js +219 -126
- package/dist/bin.js.map +1 -1
- package/dist/docs/000-index.md +2 -4
- package/dist/docs/100-introduction/200-how-overmux-works.md +126 -2
- package/{docs/100-introduction/300-why-overmux.md → dist/docs/100-introduction/300-why-i-built-overmux.md} +1 -1
- package/dist/docs/200-getting-started/100-install-and-run-overmux.md +2 -2
- package/dist/docs/200-getting-started/400-secure-with-https/200-tailscale-serve.md +3 -3
- package/dist/docs/200-getting-started/400-secure-with-https/300-cloudflare-tunnel.md +2 -2
- package/dist/docs/200-getting-started/400-secure-with-https/400-self-hosted-reverse-proxy.md +2 -2
- package/dist/docs/400-reference/100-project-structure.md +83 -0
- package/dist/docs/400-reference/200-configuration.md +257 -0
- package/dist/docs/400-reference/300-storage-locations.md +46 -0
- package/dist/docs/400-reference/400-authentication-and-security.md +37 -0
- package/dist/docs/400-reference/500-server/000-index.md +13 -0
- package/dist/docs/400-reference/500-server/100-resources.md +191 -0
- package/dist/docs/400-reference/500-server/200-operations.md +111 -0
- package/dist/docs/400-reference/500-server/300-streams.md +140 -0
- package/dist/docs/400-reference/500-server/400-notifications.md +32 -0
- package/dist/docs/400-reference/500-server/500-api.md +118 -0
- package/dist/docs/400-reference/600-client/000-index.md +9 -0
- package/dist/docs/400-reference/600-client/100-api.md +370 -0
- package/dist/docs/400-reference/600-client/200-theming.md +94 -0
- package/dist/docs/400-reference/600-client/300-tech-stack-recommendations.md +12 -0
- package/{docs/400-reference/400-cli → dist/docs/400-reference/700-cli}/050-init.md +2 -4
- package/dist/docs/400-reference/{400-cli → 700-cli}/100-serve.md +9 -4
- package/{docs/400-reference/400-cli → dist/docs/400-reference/700-cli}/200-auth.md +1 -4
- package/{docs/400-reference/400-cli → dist/docs/400-reference/700-cli}/300-call.md +1 -4
- package/dist/docs/400-reference/700-cli/350-instance.md +21 -0
- package/dist/docs/400-reference/700-cli/600-docs.md +128 -0
- package/{docs/400-reference/400-cli → dist/docs/400-reference/700-cli}/700-desktop.md +1 -3
- package/dist/docs/500-hosted-pages.md +0 -1
- package/dist/exports/client.d.ts +19 -15
- package/dist/exports/client.d.ts.map +1 -1
- package/dist/exports/client.js +240 -58
- package/dist/exports/client.js.map +1 -1
- package/dist/exports/{index-D3_Dpy_T.d.ts → index-Cz3xkCa4.d.ts} +62 -36
- package/dist/exports/index-Cz3xkCa4.d.ts.map +1 -0
- package/dist/exports/index.d.ts +2 -2
- package/dist/exports/index.js +19 -5
- package/dist/exports/index.js.map +1 -1
- package/dist/exports/{notifications-kDp16bU_.js → notifications-BFAD3QQl.js} +34 -4
- package/dist/exports/notifications-BFAD3QQl.js.map +1 -0
- package/dist/exports/server.d.ts +1 -44
- package/dist/exports/server.d.ts.map +1 -1
- package/dist/exports/server.js +8 -1018
- package/dist/exports/server.js.map +1 -1
- package/dist/internal/server/coordinator/server-child.js +153 -85
- package/dist/internal/server/coordinator/server-child.js.map +1 -1
- package/docs/000-index.md +2 -4
- package/docs/100-introduction/200-how-overmux-works.md +126 -2
- package/{dist/docs/100-introduction/300-why-overmux.md → docs/100-introduction/300-why-i-built-overmux.md} +1 -1
- package/docs/200-getting-started/100-install-and-run-overmux.md +2 -2
- package/docs/200-getting-started/400-secure-with-https/200-tailscale-serve.md +3 -3
- package/docs/200-getting-started/400-secure-with-https/300-cloudflare-tunnel.md +2 -2
- package/docs/200-getting-started/400-secure-with-https/400-self-hosted-reverse-proxy.md +2 -2
- package/docs/400-reference/100-project-structure.md +83 -0
- package/docs/400-reference/200-configuration.md +257 -0
- package/docs/400-reference/300-storage-locations.md +46 -0
- package/docs/400-reference/400-authentication-and-security.md +37 -0
- package/docs/400-reference/500-server/000-index.md +13 -0
- package/docs/400-reference/500-server/100-resources.md +191 -0
- package/docs/400-reference/500-server/200-operations.md +111 -0
- package/docs/400-reference/500-server/300-streams.md +140 -0
- package/docs/400-reference/500-server/400-notifications.md +32 -0
- package/docs/400-reference/500-server/500-api.md +118 -0
- package/docs/400-reference/600-client/000-index.md +9 -0
- package/docs/400-reference/600-client/100-api.md +370 -0
- package/docs/400-reference/600-client/200-theming.md +94 -0
- package/docs/400-reference/600-client/300-tech-stack-recommendations.md +12 -0
- package/{dist/docs/400-reference/400-cli → docs/400-reference/700-cli}/050-init.md +2 -4
- package/docs/400-reference/{400-cli → 700-cli}/100-serve.md +9 -4
- package/{dist/docs/400-reference/400-cli → docs/400-reference/700-cli}/200-auth.md +1 -4
- package/{dist/docs/400-reference/400-cli → docs/400-reference/700-cli}/300-call.md +1 -4
- package/docs/400-reference/700-cli/350-instance.md +21 -0
- package/docs/400-reference/700-cli/600-docs.md +128 -0
- package/{dist/docs/400-reference/400-cli → docs/400-reference/700-cli}/700-desktop.md +1 -3
- package/docs/500-hosted-pages.md +0 -1
- package/package.json +3 -2
- package/src/internal/cli/app.ts +5 -5
- package/src/internal/cli/commands/docs-ai-context.ts +33 -0
- package/src/internal/cli/commands/docs.ts +19 -2
- package/src/internal/cli/commands/init-template.ts +1 -1
- package/src/internal/cli/commands/instance.ts +58 -0
- package/src/internal/cli/commands/serve.ts +3 -0
- package/src/internal/cli/login.ts +9 -9
- package/src/internal/cli/parse-port.ts +2 -1
- package/src/internal/client/auth/auth-shell.css +17 -1
- package/src/internal/client/auth/auth-shell.tsx +2 -11
- package/src/internal/client/auth/login-code-input.tsx +140 -0
- package/src/internal/client/browser-api.ts +17 -3
- package/src/internal/client/client-definition.ts +6 -8
- package/src/internal/client/host/deep-link-navigation.ts +75 -0
- package/src/internal/client/host/desktop-host.ts +3 -0
- package/src/internal/client/host/environment.ts +67 -0
- package/src/internal/client/host/instance-forwarding.ts +21 -0
- package/src/internal/client/host/overmux-host.tsx +17 -0
- package/src/internal/client/index.ts +0 -6
- package/src/internal/client/overmux-react.ts +95 -42
- package/src/internal/client/websocket.ts +32 -12
- package/src/internal/server/auth/auth-service.ts +2 -2
- package/src/internal/server/auth/instance-control.ts +89 -17
- package/src/internal/server/coordinator/ipc-protocol.ts +0 -1
- package/src/internal/server/runtime/create-runtime.ts +15 -2
- package/src/internal/server/runtime/runtime-instance.ts +47 -0
- package/src/internal/server/runtime/runtime-operations.ts +9 -3
- package/src/internal/server/runtime/runtime-resources.ts +15 -32
- package/src/internal/server/runtime/runtime-streams.ts +5 -6
- package/src/internal/server/server-logger.ts +5 -10
- package/src/internal/server/server-startup-options.ts +14 -14
- package/src/internal/server/start-application-server.ts +24 -19
- package/src/internal/server/websocket-server.ts +7 -0
- package/src/internal/shared/index.ts +2 -0
- package/src/internal/shared/protocol.ts +10 -1
- package/src/public/ai-context.ts +7 -29
- package/src/public/client.ts +1 -6
- package/src/public/config.ts +163 -31
- package/src/public/index.ts +2 -0
- package/src/public/server.ts +1 -16
- package/dist/docs/100-introduction/100-what-is-overmux.md +0 -7
- package/dist/docs/300-fundamentals/100-project-structure.md +0 -23
- package/dist/docs/300-fundamentals/200-configuration.md +0 -3
- package/dist/docs/300-fundamentals/300-theming.md +0 -3
- package/dist/docs/300-fundamentals/400-server.md +0 -3
- package/dist/docs/300-fundamentals/500-client.md +0 -3
- package/dist/docs/300-fundamentals/600-operations.md +0 -3
- package/dist/docs/300-fundamentals/700-resources.md +0 -3
- package/dist/docs/300-fundamentals/800-streams.md +0 -3
- package/dist/docs/300-fundamentals/900-authentication-and-security.md +0 -3
- package/dist/docs/400-reference/100-configuration.md +0 -3
- package/dist/docs/400-reference/200-server-api.md +0 -3
- package/dist/docs/400-reference/300-client-api.md +0 -20
- package/dist/docs/400-reference/400-cli/400-check.md +0 -20
- package/dist/docs/400-reference/400-cli/500-ai-context.md +0 -102
- package/dist/docs/400-reference/400-cli/600-docs.md +0 -23
- package/dist/exports/index-D3_Dpy_T.d.ts.map +0 -1
- package/dist/exports/notifications-kDp16bU_.js.map +0 -1
- package/docs/100-introduction/100-what-is-overmux.md +0 -7
- package/docs/300-fundamentals/100-project-structure.md +0 -23
- package/docs/300-fundamentals/200-configuration.md +0 -3
- package/docs/300-fundamentals/300-theming.md +0 -3
- package/docs/300-fundamentals/400-server.md +0 -3
- package/docs/300-fundamentals/500-client.md +0 -3
- package/docs/300-fundamentals/600-operations.md +0 -3
- package/docs/300-fundamentals/700-resources.md +0 -3
- package/docs/300-fundamentals/800-streams.md +0 -3
- package/docs/300-fundamentals/900-authentication-and-security.md +0 -3
- package/docs/400-reference/100-configuration.md +0 -3
- package/docs/400-reference/200-server-api.md +0 -3
- package/docs/400-reference/300-client-api.md +0 -20
- package/docs/400-reference/400-cli/400-check.md +0 -20
- package/docs/400-reference/400-cli/500-ai-context.md +0 -102
- package/docs/400-reference/400-cli/600-docs.md +0 -23
- package/src/internal/cli/commands/ai.ts +0 -44
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { chmod, mkdir, open, readdir, rm } from "node:fs/promises";
|
|
3
4
|
import { createConnection, createServer, type Socket } from "node:net";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
|
|
8
|
+
import { instanceIdSchema } from "@overmux/shared";
|
|
9
|
+
|
|
8
10
|
import { getOvermuxPaths } from "../paths";
|
|
9
|
-
import {
|
|
10
|
-
readPrivateJson,
|
|
11
|
-
replacePrivateJsonAtomically,
|
|
12
|
-
} from "../notifications/private-json-file";
|
|
11
|
+
import { replacePrivateJsonAtomically } from "../notifications/private-json-file";
|
|
13
12
|
import type { AuthService } from "./auth-service";
|
|
14
13
|
|
|
15
14
|
const maxControlMessageBytes = 16_384;
|
|
16
15
|
|
|
17
16
|
const controlRequestSchema = z.discriminatedUnion("type", [
|
|
17
|
+
z.object({ type: z.literal("get-instance") }).strict(),
|
|
18
18
|
z.object({ type: z.literal("ping") }).strict(),
|
|
19
19
|
z.object({ type: z.literal("create-login") }).strict(),
|
|
20
20
|
z.object({ type: z.literal("issue-bearer") }).strict(),
|
|
@@ -44,6 +44,9 @@ const sessionSchema = z
|
|
|
44
44
|
.strict();
|
|
45
45
|
|
|
46
46
|
const controlResponseSchemas = {
|
|
47
|
+
"get-instance": z
|
|
48
|
+
.object({ instanceId: instanceIdSchema, ok: z.literal(true) })
|
|
49
|
+
.strict(),
|
|
47
50
|
"create-login": z
|
|
48
51
|
.object({ login: loginGrantSchema, ok: z.literal(true) })
|
|
49
52
|
.strict(),
|
|
@@ -88,9 +91,20 @@ type ControlResponseByType = {
|
|
|
88
91
|
>;
|
|
89
92
|
};
|
|
90
93
|
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
+
const currentUid = () => {
|
|
95
|
+
if (!process.getuid) {
|
|
96
|
+
throw new Error("Overmux instance control requires a Unix user ID");
|
|
97
|
+
}
|
|
98
|
+
return process.getuid();
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export const runtimeDirectory = () => {
|
|
102
|
+
const fallback = `/tmp/overmux-${currentUid()}`;
|
|
103
|
+
const preferred = getOvermuxPaths().runtimeDir;
|
|
104
|
+
if (!preferred) {
|
|
105
|
+
return fallback;
|
|
106
|
+
}
|
|
107
|
+
// Leave room for the slash, 16-character ID and .sock on macOS and Linux.
|
|
94
108
|
if (Buffer.byteLength(preferred) <= 80) {
|
|
95
109
|
return preferred;
|
|
96
110
|
}
|
|
@@ -98,7 +112,51 @@ const runtimeDirectory = () => {
|
|
|
98
112
|
.update(preferred)
|
|
99
113
|
.digest("hex")
|
|
100
114
|
.slice(0, 12);
|
|
101
|
-
return
|
|
115
|
+
return `${fallback}-${suffix}`;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const secureRuntimeDirectory = async (directory: string) => {
|
|
119
|
+
// Only the final component must not be a symlink: macOS /tmp is a symlink.
|
|
120
|
+
// Validate and chmod the same inode, never a path that could be swapped
|
|
121
|
+
// between checking ownership and changing permissions. The sticky /tmp
|
|
122
|
+
// parent (or user-controlled XDG parent) then protects our owned directory
|
|
123
|
+
// from replacement by other users. Same-user processes are already trusted.
|
|
124
|
+
const handle = await open(
|
|
125
|
+
directory,
|
|
126
|
+
constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW,
|
|
127
|
+
);
|
|
128
|
+
try {
|
|
129
|
+
const metadata = await handle.stat();
|
|
130
|
+
if (!metadata.isDirectory() || metadata.uid !== currentUid()) {
|
|
131
|
+
throw new Error(`Unsafe Overmux runtime directory: ${directory}`);
|
|
132
|
+
}
|
|
133
|
+
await handle.chmod(0o700);
|
|
134
|
+
} finally {
|
|
135
|
+
await handle.close();
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const readRegistration = async (path: string) => {
|
|
140
|
+
// An existing directory may have been writable before we secured it. Do not
|
|
141
|
+
// trust planted symlinks, foreign-owned files or publicly writable records.
|
|
142
|
+
// O_NONBLOCK also prevents a planted FIFO from hanging discovery on open.
|
|
143
|
+
const handle = await open(
|
|
144
|
+
path,
|
|
145
|
+
constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK,
|
|
146
|
+
);
|
|
147
|
+
try {
|
|
148
|
+
const metadata = await handle.stat();
|
|
149
|
+
if (
|
|
150
|
+
!metadata.isFile() ||
|
|
151
|
+
metadata.uid !== currentUid() ||
|
|
152
|
+
(metadata.mode & 0o077) !== 0
|
|
153
|
+
) {
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
return registrationSchema.parse(JSON.parse(await handle.readFile("utf8")));
|
|
157
|
+
} finally {
|
|
158
|
+
await handle.close();
|
|
159
|
+
}
|
|
102
160
|
};
|
|
103
161
|
|
|
104
162
|
const readNewlineFrame = ({
|
|
@@ -164,7 +222,18 @@ const writeResponse = (socket: Socket, response: unknown) => {
|
|
|
164
222
|
socket.end(`${JSON.stringify(response)}\n`);
|
|
165
223
|
};
|
|
166
224
|
|
|
167
|
-
const handleRequest = async (
|
|
225
|
+
const handleRequest = async ({
|
|
226
|
+
auth,
|
|
227
|
+
instanceId,
|
|
228
|
+
request,
|
|
229
|
+
}: {
|
|
230
|
+
auth: AuthService;
|
|
231
|
+
instanceId: string;
|
|
232
|
+
request: ControlRequest;
|
|
233
|
+
}) => {
|
|
234
|
+
if (request.type === "get-instance") {
|
|
235
|
+
return { instanceId, ok: true };
|
|
236
|
+
}
|
|
168
237
|
if (request.type === "ping") {
|
|
169
238
|
return { ok: true };
|
|
170
239
|
}
|
|
@@ -185,9 +254,11 @@ const handleRequest = async (auth: AuthService, request: ControlRequest) => {
|
|
|
185
254
|
|
|
186
255
|
const handleConnection = async ({
|
|
187
256
|
auth,
|
|
257
|
+
instanceId,
|
|
188
258
|
socket,
|
|
189
259
|
}: {
|
|
190
260
|
auth: AuthService;
|
|
261
|
+
instanceId: string;
|
|
191
262
|
socket: Socket;
|
|
192
263
|
}) => {
|
|
193
264
|
try {
|
|
@@ -196,7 +267,7 @@ const handleConnection = async ({
|
|
|
196
267
|
socket,
|
|
197
268
|
});
|
|
198
269
|
const request = controlRequestSchema.parse(JSON.parse(frame.toString()));
|
|
199
|
-
writeResponse(socket, await handleRequest(auth, request));
|
|
270
|
+
writeResponse(socket, await handleRequest({ auth, instanceId, request }));
|
|
200
271
|
} catch (cause) {
|
|
201
272
|
writeResponse(socket, {
|
|
202
273
|
error: cause instanceof Error ? cause.message : "Invalid control request",
|
|
@@ -214,17 +285,19 @@ const handleConnection = async ({
|
|
|
214
285
|
export const startInstanceControl = async ({
|
|
215
286
|
auth,
|
|
216
287
|
apiUrl,
|
|
288
|
+
instanceId,
|
|
217
289
|
port,
|
|
218
290
|
url,
|
|
219
291
|
}: {
|
|
220
292
|
auth: AuthService;
|
|
221
293
|
apiUrl: string;
|
|
294
|
+
instanceId: string;
|
|
222
295
|
port: number;
|
|
223
296
|
url: string;
|
|
224
297
|
}): Promise<InstanceControl> => {
|
|
225
298
|
const directory = runtimeDirectory();
|
|
226
299
|
await mkdir(directory, { mode: 0o700, recursive: true });
|
|
227
|
-
await
|
|
300
|
+
await secureRuntimeDirectory(directory);
|
|
228
301
|
const id = randomBytes(8).toString("hex");
|
|
229
302
|
const socketPath = join(directory, `${id}.sock`);
|
|
230
303
|
const registrationPath = join(directory, `${id}.json`);
|
|
@@ -232,7 +305,7 @@ export const startInstanceControl = async ({
|
|
|
232
305
|
const server = createServer((socket) => {
|
|
233
306
|
sockets.add(socket);
|
|
234
307
|
socket.once("close", () => sockets.delete(socket));
|
|
235
|
-
void handleConnection({ auth, socket });
|
|
308
|
+
void handleConnection({ auth, instanceId, socket });
|
|
236
309
|
});
|
|
237
310
|
const close = async () => {
|
|
238
311
|
sockets.forEach((socket) => socket.destroy());
|
|
@@ -289,6 +362,7 @@ export const discoverInstance = async (
|
|
|
289
362
|
const directory = runtimeDirectory();
|
|
290
363
|
let names: string[];
|
|
291
364
|
try {
|
|
365
|
+
await secureRuntimeDirectory(directory);
|
|
292
366
|
names = await readdir(directory);
|
|
293
367
|
} catch (cause) {
|
|
294
368
|
if ((cause as NodeJS.ErrnoException).code === "ENOENT") {
|
|
@@ -301,9 +375,7 @@ export const discoverInstance = async (
|
|
|
301
375
|
names
|
|
302
376
|
.filter((name) => name.endsWith(".json"))
|
|
303
377
|
.map((name) =>
|
|
304
|
-
|
|
305
|
-
() => undefined,
|
|
306
|
-
),
|
|
378
|
+
readRegistration(join(directory, name)).catch(() => undefined),
|
|
307
379
|
),
|
|
308
380
|
)
|
|
309
381
|
).filter((registration): registration is InstanceRegistration =>
|
|
@@ -9,7 +9,6 @@ const childOptionsSchema = z.object({
|
|
|
9
9
|
debug: z.boolean().optional(),
|
|
10
10
|
developmentWebTarget: z.url().optional(),
|
|
11
11
|
host: z.string().optional(),
|
|
12
|
-
logFile: z.string().optional(),
|
|
13
12
|
port: z.number().int().min(0).max(65_535).optional(),
|
|
14
13
|
productionWebAssetsDir: z.string().optional(),
|
|
15
14
|
watch: z.boolean().optional(),
|
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
import type { RuntimeConfigDefinition } from "@overmux/shared/node";
|
|
4
4
|
|
|
5
|
-
import type {
|
|
5
|
+
import type {
|
|
6
|
+
InstanceContext,
|
|
7
|
+
InstanceIdentity,
|
|
8
|
+
Notifications,
|
|
9
|
+
} from "../../../public/index";
|
|
10
|
+
import { createRuntimeInstance, defaultInstanceId } from "./runtime-instance";
|
|
6
11
|
import { protocolVersion, type RuntimeManifest } from "../../shared/index";
|
|
7
12
|
|
|
8
13
|
import { createRuntimeLifecycle } from "./runtime-lifecycle";
|
|
@@ -23,6 +28,8 @@ import {
|
|
|
23
28
|
} from "./runtime-streams";
|
|
24
29
|
|
|
25
30
|
export type Runtime = {
|
|
31
|
+
instance: InstanceContext;
|
|
32
|
+
establishInstance: (port: number) => InstanceIdentity;
|
|
26
33
|
manifest: RuntimeManifest;
|
|
27
34
|
dispose: () => Promise<void>;
|
|
28
35
|
getOperation: (name: string) => RuntimeOperation | undefined;
|
|
@@ -39,15 +46,19 @@ type CreateRuntimeOptions = {
|
|
|
39
46
|
|
|
40
47
|
export const createRuntime = async ({
|
|
41
48
|
config,
|
|
42
|
-
debug =
|
|
49
|
+
debug = true,
|
|
43
50
|
notifications = { send: async () => undefined },
|
|
44
51
|
signal,
|
|
45
52
|
}: CreateRuntimeOptions): Promise<Runtime> => {
|
|
46
53
|
const lifecycle = createRuntimeLifecycle(signal);
|
|
54
|
+
const instance = createRuntimeInstance(
|
|
55
|
+
config.instanceId ?? defaultInstanceId,
|
|
56
|
+
);
|
|
47
57
|
try {
|
|
48
58
|
const resources = createRuntimeResources({
|
|
49
59
|
definitions: (config.server.resources ??
|
|
50
60
|
{}) as RuntimeResourceDefinitions,
|
|
61
|
+
instance: instance.context,
|
|
51
62
|
lifecycle,
|
|
52
63
|
});
|
|
53
64
|
const context = resources.context;
|
|
@@ -74,6 +85,8 @@ export const createRuntime = async ({
|
|
|
74
85
|
streams: [...streams.names],
|
|
75
86
|
};
|
|
76
87
|
return {
|
|
88
|
+
instance: instance.context,
|
|
89
|
+
establishInstance: instance.establish,
|
|
77
90
|
manifest,
|
|
78
91
|
dispose: lifecycle.dispose,
|
|
79
92
|
getOperation: operations.get,
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { hostname } from "node:os";
|
|
2
|
+
import type { RuntimeConfigDefinition } from "@overmux/shared/node";
|
|
3
|
+
import {
|
|
4
|
+
createInstanceIdentity,
|
|
5
|
+
type InstanceIdentity,
|
|
6
|
+
} from "../../shared/index";
|
|
7
|
+
import type { InstanceContext } from "../../../public/config";
|
|
8
|
+
|
|
9
|
+
export const defaultInstanceId = ({ port }: { port: number }) =>
|
|
10
|
+
`${hostname()}-${port}`;
|
|
11
|
+
|
|
12
|
+
// The scope exists during preparation, but identity is immutable once the listener
|
|
13
|
+
// binds. No handler or connection may observe it before establishment.
|
|
14
|
+
export const createRuntimeInstance = (
|
|
15
|
+
configured: NonNullable<RuntimeConfigDefinition["instanceId"]>,
|
|
16
|
+
) => {
|
|
17
|
+
let identity: InstanceIdentity | undefined;
|
|
18
|
+
const get = () => {
|
|
19
|
+
if (!identity) {
|
|
20
|
+
throw new Error("Overmux instance identity is not ready");
|
|
21
|
+
}
|
|
22
|
+
return identity;
|
|
23
|
+
};
|
|
24
|
+
const context: InstanceContext = {
|
|
25
|
+
getInstanceId: () => get().instanceId,
|
|
26
|
+
getDeepLinkPrefix: () => get().deepLinkPrefix,
|
|
27
|
+
};
|
|
28
|
+
return {
|
|
29
|
+
context,
|
|
30
|
+
establish: (port: number) => {
|
|
31
|
+
if (identity) {
|
|
32
|
+
throw new Error("Overmux instance identity is already established");
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
identity = createInstanceIdentity(
|
|
36
|
+
typeof configured === "function" ? configured({ port }) : configured,
|
|
37
|
+
);
|
|
38
|
+
} catch (cause) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
"Invalid Overmux instance ID; configure instanceId with a canonical lowercase ID",
|
|
41
|
+
{ cause },
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
return identity;
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
};
|
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
// Lookup metadata stays aligned with the names published in the runtime manifest.
|
|
3
3
|
|
|
4
4
|
import type {
|
|
5
|
-
|
|
5
|
+
OperationContext,
|
|
6
6
|
OperationDefinition,
|
|
7
7
|
} from "../../../public/index";
|
|
8
8
|
import type { Notifications } from "../../../public/index";
|
|
9
9
|
import { z } from "zod";
|
|
10
10
|
|
|
11
|
+
import type { RuntimeHandlerContext } from "./runtime-resources";
|
|
12
|
+
|
|
11
13
|
export class OperationValidationError extends Error {
|
|
12
14
|
constructor(
|
|
13
15
|
readonly phase: "input" | "output",
|
|
@@ -27,14 +29,18 @@ export type RuntimeOperations = {
|
|
|
27
29
|
names: readonly string[];
|
|
28
30
|
};
|
|
29
31
|
|
|
30
|
-
type AnyOperation = OperationDefinition<
|
|
32
|
+
type AnyOperation = OperationDefinition<
|
|
33
|
+
z.ZodTypeAny,
|
|
34
|
+
z.ZodTypeAny,
|
|
35
|
+
OperationContext<Record<string, unknown>>
|
|
36
|
+
>;
|
|
31
37
|
|
|
32
38
|
export type RuntimeOperationDefinitions = Readonly<
|
|
33
39
|
Record<string, AnyOperation>
|
|
34
40
|
>;
|
|
35
41
|
|
|
36
42
|
type CreateRuntimeOperationsOptions = {
|
|
37
|
-
context: (signal?: AbortSignal) =>
|
|
43
|
+
context: (signal?: AbortSignal) => RuntimeHandlerContext;
|
|
38
44
|
definitions: RuntimeOperationDefinitions;
|
|
39
45
|
notifications: Notifications;
|
|
40
46
|
};
|
|
@@ -5,7 +5,6 @@ import {
|
|
|
5
5
|
type ResourceDefinition,
|
|
6
6
|
type RuntimeDisposer,
|
|
7
7
|
} from "../../../public/index";
|
|
8
|
-
import type { ResourceContract } from "../../../public/index";
|
|
9
8
|
import { isDeepStrictEqual } from "node:util";
|
|
10
9
|
|
|
11
10
|
import {
|
|
@@ -27,10 +26,12 @@ export type RuntimeResourceDefinitions = Readonly<
|
|
|
27
26
|
Record<string, ResourceDefinition>
|
|
28
27
|
>;
|
|
29
28
|
|
|
29
|
+
export type RuntimeHandlerContext = HandlerContext<Record<string, unknown>>;
|
|
30
|
+
|
|
30
31
|
export type RuntimeResources = {
|
|
31
|
-
context: (signal?: AbortSignal) =>
|
|
32
|
+
context: (signal?: AbortSignal) => RuntimeHandlerContext;
|
|
32
33
|
get: (name: string) => RuntimeResource | undefined;
|
|
33
|
-
invalidate:
|
|
34
|
+
invalidate: RuntimeHandlerContext["invalidate"];
|
|
34
35
|
names: readonly string[];
|
|
35
36
|
};
|
|
36
37
|
|
|
@@ -42,25 +43,19 @@ type InvalidationListener = {
|
|
|
42
43
|
|
|
43
44
|
type CreateRuntimeResourcesOptions = {
|
|
44
45
|
definitions: RuntimeResourceDefinitions;
|
|
46
|
+
instance: HandlerContext["instance"];
|
|
45
47
|
lifecycle: RuntimeLifecycle;
|
|
46
48
|
};
|
|
47
49
|
|
|
48
50
|
export const createRuntimeResources = ({
|
|
49
51
|
definitions,
|
|
52
|
+
instance,
|
|
50
53
|
lifecycle,
|
|
51
54
|
}: CreateRuntimeResourcesOptions): RuntimeResources => {
|
|
52
55
|
const graph = createResourceDependencyGraph(definitions);
|
|
53
56
|
const invalidationListeners = new Set<InvalidationListener>();
|
|
54
|
-
const contracts = new Map<ResourceContract, string[]>();
|
|
55
57
|
const resources = new Map<string, RuntimeResource>();
|
|
56
58
|
|
|
57
|
-
Object.entries(definitions).forEach(([id, definition]) => {
|
|
58
|
-
contracts.set(definition.contract, [
|
|
59
|
-
...(contracts.get(definition.contract) ?? []),
|
|
60
|
-
id,
|
|
61
|
-
]);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
59
|
const invalidateResource = (
|
|
65
60
|
id: string,
|
|
66
61
|
input: unknown,
|
|
@@ -80,30 +75,18 @@ export const createRuntimeResources = ({
|
|
|
80
75
|
});
|
|
81
76
|
};
|
|
82
77
|
|
|
83
|
-
const invalidate = (
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
Object.entries(definitions)
|
|
87
|
-
.filter(
|
|
88
|
-
([, definition]) =>
|
|
89
|
-
definition.contract.input === contract.input &&
|
|
90
|
-
definition.contract.output === contract.output,
|
|
91
|
-
)
|
|
92
|
-
.map(([id]) => id);
|
|
93
|
-
if (!ids.length) {
|
|
94
|
-
throw new Error("Cannot invalidate a resource outside this runtime");
|
|
78
|
+
const invalidate = (resourceId: string, input?: unknown) => {
|
|
79
|
+
if (!resources.has(resourceId)) {
|
|
80
|
+
throw new Error(`Unknown resource ID: ${resourceId}`);
|
|
95
81
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
? undefined
|
|
101
|
-
: definition.contract.input.parse(input);
|
|
102
|
-
invalidateResource(id, parsedInput, input === undefined);
|
|
103
|
-
});
|
|
82
|
+
const definition = definitions[resourceId]!;
|
|
83
|
+
const parsedInput =
|
|
84
|
+
input === undefined ? undefined : definition.contract.input.parse(input);
|
|
85
|
+
invalidateResource(resourceId, parsedInput, input === undefined);
|
|
104
86
|
};
|
|
105
87
|
|
|
106
|
-
const context = (signal?: AbortSignal):
|
|
88
|
+
const context = (signal?: AbortSignal): RuntimeHandlerContext => ({
|
|
89
|
+
instance,
|
|
107
90
|
invalidate,
|
|
108
91
|
signal: lifecycle.requestSignal(signal),
|
|
109
92
|
});
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
// Owns named stream registration and ordered, validated session messaging.
|
|
2
2
|
// Session abort, failure, and disposal remain exactly-once across every close path.
|
|
3
3
|
|
|
4
|
-
import type {
|
|
5
|
-
HandlerContext,
|
|
6
|
-
StreamHandlerDefinition,
|
|
7
|
-
} from "../../../public/index";
|
|
4
|
+
import type { StreamHandlerDefinition } from "../../../public/index";
|
|
8
5
|
import { z } from "zod";
|
|
9
6
|
|
|
10
7
|
import type { RuntimeLifecycle } from "./runtime-lifecycle";
|
|
8
|
+
import type { RuntimeHandlerContext } from "./runtime-resources";
|
|
11
9
|
|
|
12
10
|
export type RuntimeStreamSession = {
|
|
13
11
|
dispose: () => Promise<void>;
|
|
@@ -31,13 +29,14 @@ export type RuntimeStreams = {
|
|
|
31
29
|
type AnyStream = StreamHandlerDefinition<
|
|
32
30
|
z.ZodTypeAny,
|
|
33
31
|
z.ZodTypeAny,
|
|
34
|
-
z.ZodTypeAny
|
|
32
|
+
z.ZodTypeAny,
|
|
33
|
+
RuntimeHandlerContext
|
|
35
34
|
>;
|
|
36
35
|
|
|
37
36
|
export type RuntimeStreamDefinitions = Readonly<Record<string, AnyStream>>;
|
|
38
37
|
|
|
39
38
|
type CreateRuntimeStreamsOptions = {
|
|
40
|
-
context: (signal?: AbortSignal) =>
|
|
39
|
+
context: (signal?: AbortSignal) => RuntimeHandlerContext;
|
|
41
40
|
definitions: RuntimeStreamDefinitions;
|
|
42
41
|
lifecycle: RuntimeLifecycle;
|
|
43
42
|
};
|
|
@@ -26,7 +26,6 @@ export type ServerLogger = {
|
|
|
26
26
|
|
|
27
27
|
type ConfigurableServerLogger = ServerLogger & {
|
|
28
28
|
setEnabled: (enabled: boolean) => void;
|
|
29
|
-
setLogFile: (logFile: string) => void;
|
|
30
29
|
};
|
|
31
30
|
|
|
32
31
|
const lifecycleEvents = new Set([
|
|
@@ -38,7 +37,7 @@ const lifecycleEvents = new Set([
|
|
|
38
37
|
"websocket-open",
|
|
39
38
|
]);
|
|
40
39
|
|
|
41
|
-
export const
|
|
40
|
+
export const getServerLogFile = ({
|
|
42
41
|
homeDir,
|
|
43
42
|
xdgStateHome,
|
|
44
43
|
}: {
|
|
@@ -62,12 +61,11 @@ const safeJson = (value: unknown) => {
|
|
|
62
61
|
|
|
63
62
|
export const createServerLogger = ({
|
|
64
63
|
enabled,
|
|
65
|
-
logFile
|
|
64
|
+
logFile,
|
|
66
65
|
}: {
|
|
67
66
|
enabled: boolean;
|
|
68
|
-
logFile
|
|
67
|
+
logFile: string;
|
|
69
68
|
}): ConfigurableServerLogger => {
|
|
70
|
-
let activeLogFile = logFile;
|
|
71
69
|
let debugEnabled = enabled;
|
|
72
70
|
return {
|
|
73
71
|
get enabled() {
|
|
@@ -88,16 +86,13 @@ export const createServerLogger = ({
|
|
|
88
86
|
source: entry.source ?? "server",
|
|
89
87
|
...entry,
|
|
90
88
|
});
|
|
91
|
-
mkdirSync(dirname(
|
|
92
|
-
appendFileSync(
|
|
89
|
+
mkdirSync(dirname(logFile), { recursive: true });
|
|
90
|
+
appendFileSync(logFile, `${record}\n`, "utf8");
|
|
93
91
|
process.stderr.write(`[overmux:debug] ${record}\n`);
|
|
94
92
|
},
|
|
95
93
|
setEnabled: (nextEnabled) => {
|
|
96
94
|
debugEnabled = nextEnabled;
|
|
97
95
|
},
|
|
98
|
-
setLogFile: (nextLogFile) => {
|
|
99
|
-
activeLogFile = nextLogFile;
|
|
100
|
-
},
|
|
101
96
|
};
|
|
102
97
|
};
|
|
103
98
|
|
|
@@ -7,9 +7,17 @@ import { z } from "zod";
|
|
|
7
7
|
|
|
8
8
|
import type { ServerConfigDefinition } from "../../public/index";
|
|
9
9
|
|
|
10
|
+
const serverStartupOverridesRuntimeSchema = serverConfigRuntimeSchema
|
|
11
|
+
.omit({ port: true })
|
|
12
|
+
.extend({
|
|
13
|
+
debug: z.boolean().optional(),
|
|
14
|
+
// Port 0 asks the OS for a free port for internal listeners and tests; it must not leak into user config.
|
|
15
|
+
port: z.number().int().min(0).max(65_535).optional(),
|
|
16
|
+
});
|
|
17
|
+
|
|
10
18
|
export type ResolvedServerStartupOptions = {
|
|
19
|
+
debug: boolean;
|
|
11
20
|
host: string;
|
|
12
|
-
logFile?: string;
|
|
13
21
|
port: number;
|
|
14
22
|
productionWebAssetsDir?: string;
|
|
15
23
|
watch: boolean;
|
|
@@ -22,18 +30,10 @@ export const resolveServerStartupOptions = ({
|
|
|
22
30
|
}: {
|
|
23
31
|
config: RuntimeConfigDefinition;
|
|
24
32
|
configPath: string;
|
|
25
|
-
overrides?: ServerConfigDefinition;
|
|
33
|
+
overrides?: ServerConfigDefinition & { debug?: boolean };
|
|
26
34
|
}): ResolvedServerStartupOptions => {
|
|
27
|
-
const validatedOverrides =
|
|
28
|
-
.
|
|
29
|
-
.extend({ port: z.number().int().min(0).max(65_535).optional() })
|
|
30
|
-
.parse(overrides);
|
|
31
|
-
const configuredLogFile = config.logFile
|
|
32
|
-
? resolve(dirname(configPath), config.logFile)
|
|
33
|
-
: undefined;
|
|
34
|
-
const logFile = validatedOverrides.logFile
|
|
35
|
-
? resolve(validatedOverrides.logFile)
|
|
36
|
-
: configuredLogFile;
|
|
35
|
+
const validatedOverrides =
|
|
36
|
+
serverStartupOverridesRuntimeSchema.parse(overrides);
|
|
37
37
|
const configuredWebAssetsDir = config.productionWebAssetsDir
|
|
38
38
|
? resolve(dirname(configPath), config.productionWebAssetsDir)
|
|
39
39
|
: undefined;
|
|
@@ -42,9 +42,9 @@ export const resolveServerStartupOptions = ({
|
|
|
42
42
|
: configuredWebAssetsDir;
|
|
43
43
|
|
|
44
44
|
return {
|
|
45
|
+
debug: validatedOverrides.debug ?? config.debug ?? true,
|
|
45
46
|
host: validatedOverrides.host ?? config.host ?? "localhost",
|
|
46
|
-
|
|
47
|
-
port: validatedOverrides.port ?? config.port ?? 4200,
|
|
47
|
+
port: validatedOverrides.port ?? config.port ?? 4242,
|
|
48
48
|
...(productionWebAssetsDir === undefined ? {} : { productionWebAssetsDir }),
|
|
49
49
|
watch: validatedOverrides.watch ?? config.watch ?? true,
|
|
50
50
|
};
|
|
@@ -29,6 +29,7 @@ import { createRuntime, type Runtime } from "./runtime/create-runtime";
|
|
|
29
29
|
import {
|
|
30
30
|
createServerLogger,
|
|
31
31
|
errorDetails,
|
|
32
|
+
getServerLogFile,
|
|
32
33
|
type ServerLogger,
|
|
33
34
|
} from "./server-logger";
|
|
34
35
|
import {
|
|
@@ -40,7 +41,6 @@ import type { OvermuxServerOptions } from "./start-overmux-server";
|
|
|
40
41
|
type ResourceCleanup = () => void | Promise<void>;
|
|
41
42
|
type ServerResources = ReturnType<typeof createResources>;
|
|
42
43
|
type NotificationService = ReturnType<typeof createNotificationService>;
|
|
43
|
-
type ApplicationLogger = ReturnType<typeof createServerLogger>;
|
|
44
44
|
type StartApplicationServerOptions = {
|
|
45
45
|
onRestartRequested: () => void;
|
|
46
46
|
options: OvermuxServerOptions;
|
|
@@ -279,24 +279,20 @@ const startApplicationTransport = async ({
|
|
|
279
279
|
? undefined
|
|
280
280
|
: startup.productionWebAssetsDir,
|
|
281
281
|
});
|
|
282
|
+
// Binding allocates the actual port. Reject early HTTP traffic until identity
|
|
283
|
+
// and trusted origins are established; WebSocket upgrades attach only after that.
|
|
284
|
+
let ready = false;
|
|
282
285
|
const server = serve({
|
|
283
|
-
fetch:
|
|
286
|
+
fetch: (request, env) =>
|
|
287
|
+
ready
|
|
288
|
+
? app.fetch(request, env)
|
|
289
|
+
: new Response("Starting", { status: 503 }),
|
|
284
290
|
hostname: startup.host,
|
|
285
291
|
port: startup.port,
|
|
286
292
|
});
|
|
287
293
|
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
294
|
const address = await getServerAddress(server);
|
|
295
|
+
runtime.establishInstance(address.port);
|
|
300
296
|
const urlHost = startup.host.includes(":")
|
|
301
297
|
? `[${startup.host}]`
|
|
302
298
|
: startup.host;
|
|
@@ -309,9 +305,21 @@ const startApplicationTransport = async ({
|
|
|
309
305
|
: urlHost;
|
|
310
306
|
const origins = authOrigins.establish(localUrl);
|
|
311
307
|
authService.setOrigins(origins);
|
|
308
|
+
const webSockets = createWebSocketServer({
|
|
309
|
+
auth,
|
|
310
|
+
developmentWebTarget,
|
|
311
|
+
runtime,
|
|
312
|
+
server,
|
|
313
|
+
serverLogger: logger,
|
|
314
|
+
});
|
|
315
|
+
resources.add(webSockets.close);
|
|
316
|
+
announceRestarting = webSockets.announceRestarting;
|
|
317
|
+
notifications.attachLiveDelivery(webSockets.broadcastNotification);
|
|
318
|
+
ready = true;
|
|
312
319
|
const instanceControl = await startInstanceControl({
|
|
313
320
|
auth: authService,
|
|
314
321
|
apiUrl: `http://${controlApiHost}:${address.port}`,
|
|
322
|
+
instanceId: runtime.instance.getInstanceId(),
|
|
315
323
|
port: address.port,
|
|
316
324
|
url: origins[0]!,
|
|
317
325
|
});
|
|
@@ -353,8 +361,8 @@ export const startApplicationServer = async ({
|
|
|
353
361
|
options,
|
|
354
362
|
}: StartApplicationServerOptions): Promise<ApplicationServer> => {
|
|
355
363
|
const logger = createServerLogger({
|
|
356
|
-
enabled: options.debug ??
|
|
357
|
-
logFile:
|
|
364
|
+
enabled: options.debug ?? true,
|
|
365
|
+
logFile: getServerLogFile(),
|
|
358
366
|
});
|
|
359
367
|
const resources = createResources();
|
|
360
368
|
resources.add(registerProcessErrorLogging(logger));
|
|
@@ -366,10 +374,7 @@ export const startApplicationServer = async ({
|
|
|
366
374
|
configPath,
|
|
367
375
|
options,
|
|
368
376
|
});
|
|
369
|
-
logger.setEnabled(
|
|
370
|
-
if (startup.logFile) {
|
|
371
|
-
logger.setLogFile(startup.logFile);
|
|
372
|
-
}
|
|
377
|
+
logger.setEnabled(startup.debug);
|
|
373
378
|
const {
|
|
374
379
|
auth,
|
|
375
380
|
authOrigins,
|
|
@@ -247,6 +247,13 @@ export const createWebSocketServer = ({
|
|
|
247
247
|
sessionSockets,
|
|
248
248
|
socket: webSocket,
|
|
249
249
|
});
|
|
250
|
+
// Discovery precedes all connection traffic, including sticky announcements.
|
|
251
|
+
webSocket.send(
|
|
252
|
+
encodeProtocolFrame({
|
|
253
|
+
type: "server-info",
|
|
254
|
+
instanceId: runtime.instance.getInstanceId(),
|
|
255
|
+
}),
|
|
256
|
+
);
|
|
250
257
|
webSocketServer.emit("connection", webSocket, request);
|
|
251
258
|
attachWebSocketConnection({ runtime, serverLogger, socket: webSocket });
|
|
252
259
|
if (updateAvailable) {
|