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.
Files changed (193) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +7 -0
  3. package/dist/auth/.vite/manifest.json +11 -0
  4. package/dist/auth/assets/index-Bx8JhwVi.css +1 -0
  5. package/dist/auth/assets/index-CHIE7qYj.js +72 -0
  6. package/dist/auth/index.html +14 -0
  7. package/dist/bin.js +4504 -7870
  8. package/dist/bin.js.map +1 -0
  9. package/dist/docs/000-index.md +21 -0
  10. package/dist/docs/100-introduction/100-what-is-overmux.md +7 -0
  11. package/dist/docs/100-introduction/200-how-overmux-works.md +7 -0
  12. package/dist/docs/100-introduction/300-why-overmux.md +22 -0
  13. package/dist/docs/200-getting-started/100-install-and-run-overmux.md +51 -0
  14. package/dist/docs/200-getting-started/200-install-overmux-desktop.md +41 -0
  15. package/dist/docs/200-getting-started/300-install-overmux-pwa.mdx +92 -0
  16. package/dist/docs/200-getting-started/400-secure-with-https/100-choose-an-https-setup.md +14 -0
  17. package/dist/docs/200-getting-started/400-secure-with-https/200-tailscale-serve.md +44 -0
  18. package/dist/docs/200-getting-started/400-secure-with-https/300-cloudflare-tunnel.md +59 -0
  19. package/dist/docs/200-getting-started/400-secure-with-https/400-self-hosted-reverse-proxy.md +32 -0
  20. package/dist/docs/300-fundamentals/100-project-structure.md +3 -0
  21. package/dist/docs/300-fundamentals/200-configuration.md +3 -0
  22. package/dist/docs/300-fundamentals/300-theming.md +3 -0
  23. package/dist/docs/300-fundamentals/400-server.md +3 -0
  24. package/dist/docs/300-fundamentals/500-client.md +3 -0
  25. package/dist/docs/300-fundamentals/600-operations.md +3 -0
  26. package/dist/docs/300-fundamentals/700-resources.md +3 -0
  27. package/dist/docs/300-fundamentals/800-streams.md +3 -0
  28. package/dist/docs/300-fundamentals/900-authentication-and-security.md +3 -0
  29. package/dist/docs/400-reference/100-configuration.md +3 -0
  30. package/dist/docs/400-reference/200-server-api.md +3 -0
  31. package/dist/docs/400-reference/300-client-api.md +3 -0
  32. package/dist/docs/400-reference/400-cli/100-serve.md +33 -0
  33. package/dist/docs/400-reference/400-cli/200-auth.md +52 -0
  34. package/dist/docs/400-reference/400-cli/300-call.md +28 -0
  35. package/dist/docs/400-reference/400-cli/400-check.md +20 -0
  36. package/dist/docs/400-reference/400-cli/500-ai-context.md +102 -0
  37. package/dist/docs/400-reference/400-cli/600-docs.md +23 -0
  38. package/dist/docs/400-reference/400-cli/700-desktop.md +35 -0
  39. package/dist/docs/500-hosted-pages.md +31 -0
  40. package/dist/exports/client.d.ts +237 -0
  41. package/dist/exports/client.d.ts.map +1 -0
  42. package/dist/exports/client.js +1643 -0
  43. package/dist/exports/client.js.map +1 -0
  44. package/dist/exports/index-D3_Dpy_T.d.ts +155 -0
  45. package/dist/exports/index-D3_Dpy_T.d.ts.map +1 -0
  46. package/dist/exports/index.d.ts +2 -0
  47. package/dist/exports/index.js +49 -0
  48. package/dist/exports/index.js.map +1 -0
  49. package/dist/exports/notifications-kDp16bU_.js +56 -0
  50. package/dist/exports/notifications-kDp16bU_.js.map +1 -0
  51. package/dist/exports/server.d.ts +63 -0
  52. package/dist/exports/server.d.ts.map +1 -0
  53. package/dist/exports/server.js +1120 -0
  54. package/dist/exports/server.js.map +1 -0
  55. package/dist/exports/style.css +368 -0
  56. package/dist/internal/server/coordinator/server-child.d.ts +1 -0
  57. package/dist/internal/server/coordinator/server-child.js +3299 -0
  58. package/dist/internal/server/coordinator/server-child.js.map +1 -0
  59. package/docs/000-index.md +21 -0
  60. package/docs/100-introduction/100-what-is-overmux.md +7 -0
  61. package/docs/100-introduction/200-how-overmux-works.md +7 -0
  62. package/docs/100-introduction/300-why-overmux.md +22 -0
  63. package/docs/200-getting-started/100-install-and-run-overmux.md +51 -0
  64. package/docs/200-getting-started/200-install-overmux-desktop.md +41 -0
  65. package/docs/200-getting-started/300-install-overmux-pwa.mdx +92 -0
  66. package/docs/200-getting-started/400-secure-with-https/100-choose-an-https-setup.md +14 -0
  67. package/docs/200-getting-started/400-secure-with-https/200-tailscale-serve.md +44 -0
  68. package/docs/200-getting-started/400-secure-with-https/300-cloudflare-tunnel.md +59 -0
  69. package/docs/200-getting-started/400-secure-with-https/400-self-hosted-reverse-proxy.md +32 -0
  70. package/docs/300-fundamentals/100-project-structure.md +3 -0
  71. package/docs/300-fundamentals/200-configuration.md +3 -0
  72. package/docs/300-fundamentals/300-theming.md +3 -0
  73. package/docs/300-fundamentals/400-server.md +3 -0
  74. package/docs/300-fundamentals/500-client.md +3 -0
  75. package/docs/300-fundamentals/600-operations.md +3 -0
  76. package/docs/300-fundamentals/700-resources.md +3 -0
  77. package/docs/300-fundamentals/800-streams.md +3 -0
  78. package/docs/300-fundamentals/900-authentication-and-security.md +3 -0
  79. package/docs/400-reference/100-configuration.md +3 -0
  80. package/docs/400-reference/200-server-api.md +3 -0
  81. package/docs/400-reference/300-client-api.md +3 -0
  82. package/docs/400-reference/400-cli/100-serve.md +33 -0
  83. package/docs/400-reference/400-cli/200-auth.md +52 -0
  84. package/docs/400-reference/400-cli/300-call.md +28 -0
  85. package/docs/400-reference/400-cli/400-check.md +20 -0
  86. package/docs/400-reference/400-cli/500-ai-context.md +102 -0
  87. package/docs/400-reference/400-cli/600-docs.md +23 -0
  88. package/docs/400-reference/400-cli/700-desktop.md +35 -0
  89. package/docs/500-hosted-pages.md +31 -0
  90. package/package.json +59 -18
  91. package/src/internal/cli/app.ts +72 -0
  92. package/src/internal/cli/bin.ts +3 -0
  93. package/src/internal/cli/commands/ai.ts +44 -0
  94. package/src/internal/cli/commands/auth.ts +195 -0
  95. package/src/internal/cli/commands/call.ts +128 -0
  96. package/src/internal/cli/commands/check.ts +48 -0
  97. package/src/internal/cli/commands/desktop-installer.ts +446 -0
  98. package/src/internal/cli/commands/desktop-release.ts +190 -0
  99. package/src/internal/cli/commands/desktop.ts +47 -0
  100. package/src/internal/cli/commands/docs.ts +34 -0
  101. package/src/internal/cli/commands/serve.ts +322 -0
  102. package/src/internal/cli/environment.ts +54 -0
  103. package/src/internal/cli/login.ts +46 -0
  104. package/src/internal/cli/managed-vite.ts +267 -0
  105. package/src/internal/cli/parse-port.ts +8 -0
  106. package/src/internal/client/auth/auth-api.ts +53 -0
  107. package/src/internal/client/auth/auth-shell.css +74 -0
  108. package/src/internal/client/auth/auth-shell.tsx +167 -0
  109. package/src/internal/client/auth/entry.tsx +26 -0
  110. package/src/internal/client/auth/index.html +13 -0
  111. package/src/internal/client/background-notifications.ts +194 -0
  112. package/src/internal/client/browser-api.ts +97 -0
  113. package/src/internal/client/client-definition.ts +184 -0
  114. package/src/internal/client/commands.tsx +260 -0
  115. package/src/internal/client/host/browser-log-forwarding.ts +149 -0
  116. package/src/internal/client/host/hosted-pages/browser-notification-settings.tsx +118 -0
  117. package/src/internal/client/host/hosted-pages/hosted-page.css +87 -0
  118. package/src/internal/client/host/hosted-pages/hosted-page.tsx +9 -0
  119. package/src/internal/client/host/hosted-pages/logout-page.tsx +75 -0
  120. package/src/internal/client/host/hosted-pages/settings-page.tsx +45 -0
  121. package/src/internal/client/host/notification-forwarding.ts +22 -0
  122. package/src/internal/client/host/overmux-host.tsx +200 -0
  123. package/src/internal/client/host/recovery-screen.css +75 -0
  124. package/src/internal/client/host/recovery-screen.tsx +17 -0
  125. package/src/internal/client/host/update-popover.css +82 -0
  126. package/src/internal/client/host/update-popover.tsx +166 -0
  127. package/src/internal/client/index.ts +45 -0
  128. package/src/internal/client/overmux-react.ts +283 -0
  129. package/src/internal/client/shortcuts.tsx +373 -0
  130. package/src/internal/client/theme-scope.css +114 -0
  131. package/src/internal/client/theme-scope.tsx +107 -0
  132. package/src/internal/client/transport.ts +69 -0
  133. package/src/internal/client/websocket.ts +441 -0
  134. package/src/internal/server/auth/auth-http.ts +414 -0
  135. package/src/internal/server/auth/auth-service.ts +627 -0
  136. package/src/internal/server/auth/auth-shell-http.ts +90 -0
  137. package/src/internal/server/auth/instance-control.ts +381 -0
  138. package/src/internal/server/check-overmux.ts +364 -0
  139. package/src/internal/server/coordinator/ipc-protocol.ts +44 -0
  140. package/src/internal/server/coordinator/server-child.ts +108 -0
  141. package/src/internal/server/coordinator/server-coordinator.ts +262 -0
  142. package/src/internal/server/coordinator/update-watcher.ts +54 -0
  143. package/src/internal/server/http/authenticated-http-app-test-helper.ts +45 -0
  144. package/src/internal/server/http/background-notification-handlers.ts +83 -0
  145. package/src/internal/server/http/control-handlers.ts +44 -0
  146. package/src/internal/server/http/create-http-app.ts +53 -0
  147. package/src/internal/server/http/development-web-backend.ts +83 -0
  148. package/src/internal/server/http/operation-handler.ts +93 -0
  149. package/src/internal/server/http/request-logging.ts +34 -0
  150. package/src/internal/server/http/routes.ts +241 -0
  151. package/src/internal/server/http/runtime-manifest-handler.ts +12 -0
  152. package/src/internal/server/index.ts +16 -0
  153. package/src/internal/server/notifications/background-notification-payload.ts +74 -0
  154. package/src/internal/server/notifications/background-notification-service.ts +139 -0
  155. package/src/internal/server/notifications/background-notification-store.ts +162 -0
  156. package/src/internal/server/notifications/notification-service.ts +70 -0
  157. package/src/internal/server/notifications/private-json-file.ts +84 -0
  158. package/src/internal/server/paths.ts +13 -0
  159. package/src/internal/server/rate-limiter.ts +58 -0
  160. package/src/internal/server/runtime/create-runtime.ts +87 -0
  161. package/src/internal/server/runtime/resource-dependency-graph.ts +128 -0
  162. package/src/internal/server/runtime/runtime-lifecycle.ts +95 -0
  163. package/src/internal/server/runtime/runtime-operations.ts +78 -0
  164. package/src/internal/server/runtime/runtime-resources.ts +201 -0
  165. package/src/internal/server/runtime/runtime-streams.ts +130 -0
  166. package/src/internal/server/server-logger.ts +110 -0
  167. package/src/internal/server/server-startup-options.ts +51 -0
  168. package/src/internal/server/start-application-server.ts +412 -0
  169. package/src/internal/server/start-overmux-server.ts +26 -0
  170. package/src/internal/server/test-port.ts +23 -0
  171. package/src/internal/server/vite-websocket-proxy.ts +201 -0
  172. package/src/internal/server/websocket-connection.ts +586 -0
  173. package/src/internal/server/websocket-limits.ts +2 -0
  174. package/src/internal/server/websocket-server.ts +311 -0
  175. package/src/internal/shared/background-notifications.ts +37 -0
  176. package/src/internal/shared/index.ts +2 -0
  177. package/src/internal/shared/protocol.ts +317 -0
  178. package/src/internal/shared/routes.ts +3 -0
  179. package/src/public/ai-context.ts +44 -0
  180. package/src/public/client.ts +53 -0
  181. package/src/public/config.ts +287 -0
  182. package/src/public/contracts.ts +55 -0
  183. package/src/public/index.ts +57 -0
  184. package/src/public/notifications.ts +10 -0
  185. package/src/public/server.ts +16 -0
  186. package/dist/public/core.ts +0 -119
  187. package/dist/public/protocol.ts +0 -51
  188. package/dist/public/terminal.ts +0 -23
  189. package/dist/web/assets/index-DC___IUw.css +0 -2
  190. package/dist/web/assets/index-DF9Cptse.js +0 -74
  191. package/dist/web/assets/terminal-component-BrP-ENHg.css +0 -1
  192. package/dist/web/assets/terminal-component-DyLEbMrx.js +0 -36
  193. package/dist/web/index.html +0 -14
@@ -0,0 +1,262 @@
1
+ // Supervises one disposable application child and the parent-owned update watcher.
2
+ // The coordinator retains only lifecycle state and the stable startup address.
3
+
4
+ import { fork, type ChildProcess } from "node:child_process";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ import { errorMessage } from "../server-logger";
8
+ import type {
9
+ OvermuxServer,
10
+ OvermuxServerOptions,
11
+ } from "../start-overmux-server";
12
+ import {
13
+ parseChildMessage,
14
+ type ChildServerOptions,
15
+ type ParentToChildMessage,
16
+ } from "./ipc-protocol";
17
+ import { startUpdateWatcher, type UpdateWatcher } from "./update-watcher";
18
+
19
+ type ChildInstance = {
20
+ process: ChildProcess;
21
+ host: string;
22
+ port: number;
23
+ url: string;
24
+ watch: boolean;
25
+ };
26
+
27
+ const send = (
28
+ child: ChildProcess,
29
+ message: ParentToChildMessage,
30
+ onError?: (cause: Error) => void,
31
+ ) => {
32
+ if (!child.connected) {
33
+ onError?.(new Error("Server child IPC channel is closed"));
34
+ return;
35
+ }
36
+ try {
37
+ child.send(message, (cause) => {
38
+ if (cause) {
39
+ onError?.(cause);
40
+ }
41
+ });
42
+ } catch (cause) {
43
+ onError?.(cause instanceof Error ? cause : new Error(String(cause)));
44
+ }
45
+ };
46
+
47
+ const stopChild = (child: ChildProcess): Promise<void> =>
48
+ new Promise((resolve) => {
49
+ let timeout: NodeJS.Timeout | undefined;
50
+ const finish = () => {
51
+ child.off("exit", finish);
52
+ clearTimeout(timeout);
53
+ resolve();
54
+ };
55
+ child.once("exit", finish);
56
+ if (child.exitCode !== null || child.signalCode !== null) {
57
+ finish();
58
+ return;
59
+ }
60
+ timeout = setTimeout(() => {
61
+ if (!child.kill("SIGKILL")) {
62
+ finish();
63
+ }
64
+ }, 5_000);
65
+ send(child, { type: "stop" });
66
+ });
67
+
68
+ const serverChildPath = () =>
69
+ fileURLToPath(import.meta.resolve("#server-child"));
70
+
71
+ const startChild = (
72
+ options: ChildServerOptions,
73
+ onRestartRequested: () => void,
74
+ ): Promise<ChildInstance> =>
75
+ new Promise((resolve, reject) => {
76
+ const child = fork(serverChildPath(), [], {
77
+ serialization: "json",
78
+ stdio: ["inherit", "inherit", "inherit", "ipc"],
79
+ });
80
+ let settled = false;
81
+ const fail = (cause: unknown) => {
82
+ if (settled) {
83
+ return;
84
+ }
85
+ settled = true;
86
+ if (child.exitCode === null && child.signalCode === null) {
87
+ child.kill();
88
+ }
89
+ reject(cause instanceof Error ? cause : new Error(String(cause)));
90
+ };
91
+ child.on("error", fail);
92
+ child.on("exit", (code, signal) => {
93
+ fail(
94
+ new Error(
95
+ `Server child exited before readiness (${signal ?? code ?? "unknown"})`,
96
+ ),
97
+ );
98
+ });
99
+ child.on("message", (raw) => {
100
+ let message;
101
+ try {
102
+ message = parseChildMessage(raw);
103
+ } catch (cause) {
104
+ fail(cause);
105
+ return;
106
+ }
107
+ if (message.type === "restart-requested") {
108
+ if (settled) {
109
+ onRestartRequested();
110
+ }
111
+ return;
112
+ }
113
+ if (message.type === "startup-failed") {
114
+ fail(new Error(message.message));
115
+ return;
116
+ }
117
+ if (!settled) {
118
+ settled = true;
119
+ resolve({
120
+ host: message.host,
121
+ port: message.port,
122
+ process: child,
123
+ url: message.url,
124
+ watch: message.watch,
125
+ });
126
+ }
127
+ });
128
+ send(child, { options, type: "start" }, fail);
129
+ });
130
+
131
+ export const startServerCoordinator = async (
132
+ initialOptions: OvermuxServerOptions,
133
+ ): Promise<OvermuxServer> => {
134
+ let child: ChildInstance | undefined;
135
+ let closed = false;
136
+ let closePromise: Promise<void> | undefined;
137
+ let restartPromise: Promise<void> | undefined;
138
+ let updateDuringRestart = false;
139
+ let startupAddress: { host: string; port: number } | undefined;
140
+ let watcher: UpdateWatcher | undefined;
141
+
142
+ const adoptChild = (next: ChildInstance) => {
143
+ child = next;
144
+ next.process.once("exit", () => {
145
+ if (child?.process === next.process) {
146
+ child = undefined;
147
+ }
148
+ });
149
+ };
150
+
151
+ const restart = () => {
152
+ if (closed || restartPromise) {
153
+ return restartPromise ?? Promise.resolve();
154
+ }
155
+ restartPromise = (async () => {
156
+ const previous = child;
157
+ child = undefined;
158
+ if (previous) {
159
+ await stopChild(previous.process);
160
+ }
161
+ if (closed || !startupAddress) {
162
+ return;
163
+ }
164
+ try {
165
+ const replacement = await startChild(
166
+ {
167
+ ...initialOptions,
168
+ host: startupAddress.host,
169
+ port: startupAddress.port,
170
+ },
171
+ () => void restart(),
172
+ );
173
+ if (closed) {
174
+ await stopChild(replacement.process);
175
+ return;
176
+ }
177
+ adoptChild(replacement);
178
+ if (updateDuringRestart) {
179
+ updateDuringRestart = false;
180
+ send(replacement.process, { type: "update-available" });
181
+ }
182
+ } catch (cause) {
183
+ process.stderr.write(
184
+ `[overmux] replacement child failed: ${errorMessage(cause)}\n`,
185
+ );
186
+ }
187
+ })().finally(() => {
188
+ const shouldRetry = updateDuringRestart && !child && !closed;
189
+ updateDuringRestart = false;
190
+ restartPromise = undefined;
191
+ if (shouldRetry) {
192
+ void restart();
193
+ }
194
+ });
195
+ return restartPromise;
196
+ };
197
+
198
+ const onUpdate = () => {
199
+ if (closed) {
200
+ return;
201
+ }
202
+ if (restartPromise) {
203
+ updateDuringRestart = true;
204
+ return;
205
+ }
206
+ if (child) {
207
+ send(child.process, { type: "update-available" });
208
+ return;
209
+ }
210
+ void restart();
211
+ };
212
+
213
+ const initial = await startChild(initialOptions, () => void restart());
214
+ startupAddress = { host: initial.host, port: initial.port };
215
+ adoptChild(initial);
216
+ try {
217
+ if (initial.watch) {
218
+ watcher = startUpdateWatcher({
219
+ configPath: initialOptions.configPath,
220
+ onUpdate,
221
+ });
222
+ }
223
+ } catch (cause) {
224
+ await stopChild(initial.process);
225
+ throw cause;
226
+ }
227
+
228
+ const close = () => {
229
+ if (closePromise) {
230
+ return closePromise;
231
+ }
232
+ closed = true;
233
+ closePromise = (async () => {
234
+ process.off("SIGINT", onSignal);
235
+ process.off("SIGTERM", onSignal);
236
+ watcher?.close();
237
+ const active = child;
238
+ child = undefined;
239
+ if (active) {
240
+ await stopChild(active.process);
241
+ }
242
+ await restartPromise;
243
+ })();
244
+ return closePromise;
245
+ };
246
+ const onSignal = () => {
247
+ void close().catch((cause: unknown) => {
248
+ process.stderr.write(
249
+ `[overmux] coordinator shutdown failed: ${errorMessage(cause)}\n`,
250
+ );
251
+ });
252
+ };
253
+ process.once("SIGINT", onSignal);
254
+ process.once("SIGTERM", onSignal);
255
+
256
+ return {
257
+ announceUpdateAvailable: onUpdate,
258
+ close,
259
+ port: initial.port,
260
+ url: initial.url,
261
+ };
262
+ };
@@ -0,0 +1,54 @@
1
+ // Owns the coordinator's broad recursive watch of the config directory.
2
+ // Filtering and debounce keep update signaling independent of import analysis.
3
+
4
+ import { watch } from "node:fs";
5
+ import { dirname } from "node:path";
6
+
7
+ export type UpdateWatcher = {
8
+ close: () => void;
9
+ };
10
+
11
+ export const isRelevantUpdatePath = (path: string): boolean => {
12
+ const segments = path.split(/[\\/]/);
13
+ if (segments.some((segment) => segment.toLowerCase() === "node_modules")) {
14
+ return false;
15
+ }
16
+ return /\.(?:js|jsx|mjs|cjs|ts|tsx|mts|cts|json|css)$/i.test(path);
17
+ };
18
+
19
+ export const startUpdateWatcher = ({
20
+ configPath,
21
+ onUpdate,
22
+ }: {
23
+ configPath: string;
24
+ onUpdate: () => void;
25
+ }): UpdateWatcher => {
26
+ let closed = false;
27
+ let timeout: NodeJS.Timeout | undefined;
28
+ const watcher = watch(
29
+ dirname(configPath),
30
+ { recursive: true },
31
+ (_event, fileName) => {
32
+ if (closed || !fileName || !isRelevantUpdatePath(fileName.toString())) {
33
+ return;
34
+ }
35
+ if (timeout) {
36
+ clearTimeout(timeout);
37
+ }
38
+ timeout = setTimeout(onUpdate, 100);
39
+ },
40
+ );
41
+
42
+ return {
43
+ close: () => {
44
+ if (closed) {
45
+ return;
46
+ }
47
+ closed = true;
48
+ if (timeout) {
49
+ clearTimeout(timeout);
50
+ }
51
+ watcher.close();
52
+ },
53
+ };
54
+ };
@@ -0,0 +1,45 @@
1
+ import type { AuthService } from "../auth/auth-service";
2
+ import type { RequestAuthentication } from "../auth/auth-http";
3
+ import { createHttpApp, type HttpAppOptions } from "./create-http-app";
4
+
5
+ const origin = "http://localhost";
6
+
7
+ const auth: AuthService = {
8
+ authenticateToken: async () => undefined,
9
+ consumeLoginGrant: async () => ({ status: "invalid" }),
10
+ createLoginGrant: async () => {
11
+ throw new Error("Not implemented in authenticated HTTP tests");
12
+ },
13
+ isSessionActive: async () => true,
14
+ issueLocalBearer: async () => {
15
+ throw new Error("Not implemented in authenticated HTTP tests");
16
+ },
17
+ listSessions: async () => [],
18
+ onSessionsRevoked: () => () => undefined,
19
+ revokeAllSessions: async () => 0,
20
+ revokeSession: async () => false,
21
+ setOrigins: () => undefined,
22
+ };
23
+
24
+ const authentication: RequestAuthentication = {
25
+ authenticate: async () => ({
26
+ credential: "bearer",
27
+ id: "test-session",
28
+ kind: "local",
29
+ token: "test-token",
30
+ }),
31
+ clearCookieHeader: () => "",
32
+ cookieHeader: () => "",
33
+ };
34
+
35
+ type TestHttpAppOptions = Omit<HttpAppOptions, "auth">;
36
+
37
+ export const createAuthenticatedTestHttpApp = (options: TestHttpAppOptions) =>
38
+ createHttpApp({
39
+ ...options,
40
+ auth: {
41
+ requests: authentication,
42
+ resolveOrigin: () => origin,
43
+ service: auth,
44
+ },
45
+ });
@@ -0,0 +1,83 @@
1
+ // Exposes browser controls for enabling and disabling background notifications.
2
+ // Subscription details pass directly to the background-delivery boundary.
3
+ import {
4
+ backgroundNotificationDisableRequestSchema,
5
+ backgroundNotificationSubscriptionSchema,
6
+ } from "../../shared/index";
7
+ import type { Handler } from "hono";
8
+ import type { z } from "zod";
9
+
10
+ import type { AuthEnvironment } from "../auth/auth-http";
11
+ import type { BackgroundNotificationService } from "../notifications/background-notification-service";
12
+
13
+ const requestLimit = 16_384;
14
+
15
+ const parseJson = async <TSchema extends z.ZodType>(
16
+ request: Request,
17
+ schema: TSchema,
18
+ ): Promise<z.output<TSchema> | undefined> => {
19
+ if (
20
+ request.headers.get("Content-Type")?.split(";", 1)[0]?.toLowerCase() !==
21
+ "application/json"
22
+ ) {
23
+ return undefined;
24
+ }
25
+ const contentLength = Number(request.headers.get("Content-Length") ?? 0);
26
+ if (contentLength > requestLimit) {
27
+ return undefined;
28
+ }
29
+ try {
30
+ const bytes = await request.arrayBuffer();
31
+ if (bytes.byteLength > requestLimit) {
32
+ return undefined;
33
+ }
34
+ const parsed = schema.safeParse(
35
+ JSON.parse(new TextDecoder().decode(bytes)),
36
+ );
37
+ return parsed.success ? parsed.data : undefined;
38
+ } catch {
39
+ return undefined;
40
+ }
41
+ };
42
+
43
+ export const createBackgroundNotificationHandlers = ({
44
+ backgroundNotifications,
45
+ }: {
46
+ backgroundNotifications: BackgroundNotificationService;
47
+ }): {
48
+ disable: Handler<AuthEnvironment>;
49
+ enable: Handler<AuthEnvironment>;
50
+ publicKey: Handler<AuthEnvironment>;
51
+ } => ({
52
+ publicKey: async (context) => {
53
+ context.header("Cache-Control", "no-store");
54
+ return context.json({
55
+ publicKey: await backgroundNotifications.publicKey(),
56
+ });
57
+ },
58
+ enable: async (context) => {
59
+ const subscription = await parseJson(
60
+ context.req.raw,
61
+ backgroundNotificationSubscriptionSchema,
62
+ );
63
+ if (!subscription) {
64
+ return context.json({ error: "Invalid background subscription" }, 400);
65
+ }
66
+ await backgroundNotifications.enable(
67
+ context.get("session").id,
68
+ subscription,
69
+ );
70
+ return context.json({ ok: true });
71
+ },
72
+ disable: async (context) => {
73
+ const input = await parseJson(
74
+ context.req.raw,
75
+ backgroundNotificationDisableRequestSchema,
76
+ );
77
+ if (!input) {
78
+ return context.json({ error: "Invalid background subscription" }, 400);
79
+ }
80
+ await backgroundNotifications.disable(input.endpoint);
81
+ return context.json({ ok: true });
82
+ },
83
+ });
@@ -0,0 +1,44 @@
1
+ // Provides server health and acknowledged restart control endpoints.
2
+ // Shutdown state is shared with the HTTP app composition root.
3
+ import type { HttpBindings } from "@hono/node-server";
4
+ import type { Handler } from "hono";
5
+
6
+ import type { AuthEnvironment } from "../auth/auth-http";
7
+
8
+ export type ShutdownState = { requested: boolean };
9
+
10
+ export const createHealthHandler =
11
+ ({ shutdown }: { shutdown: ShutdownState }): Handler<AuthEnvironment> =>
12
+ (context) =>
13
+ shutdown.requested
14
+ ? context.json({ status: "stopping" }, 503)
15
+ : context.json({ status: "ok" });
16
+
17
+ export const createRestartHandler =
18
+ ({
19
+ onRestartRequested,
20
+ shutdown,
21
+ }: {
22
+ onRestartRequested?: () => void;
23
+ shutdown: ShutdownState;
24
+ }): Handler<AuthEnvironment> =>
25
+ (context) => {
26
+ const response = context.json({ status: "restarting" }, 202);
27
+ if (!onRestartRequested) {
28
+ return response;
29
+ }
30
+ const beginRestart = () => {
31
+ shutdown.requested = true;
32
+ onRestartRequested();
33
+ };
34
+ const nodeResponse = (context.env as Partial<HttpBindings> | undefined)
35
+ ?.outgoing;
36
+ if (nodeResponse) {
37
+ // Flush the restart acknowledgement before replacing the server.
38
+ nodeResponse.once("finish", beginRestart);
39
+ } else {
40
+ // In-memory Hono requests have no Node response, so defer past the handler.
41
+ setImmediate(beginRestart);
42
+ }
43
+ return response;
44
+ };
@@ -0,0 +1,53 @@
1
+ import { Hono } from "hono";
2
+
3
+ import type { AuthBoundary, AuthEnvironment } from "../auth/auth-http";
4
+ import type { BackgroundNotificationService } from "../notifications/background-notification-service";
5
+ import type { ServerLogger } from "../server-logger";
6
+ import type { Runtime } from "../runtime/create-runtime";
7
+ import { registerHttpRoutes } from "./routes";
8
+
9
+ export type HttpAppOptions = {
10
+ auth: AuthBoundary;
11
+ authAssetsDirectory?: string;
12
+ backgroundNotifications?: BackgroundNotificationService;
13
+ developmentWebTarget?: string;
14
+ onRestartRequested?: () => void;
15
+ serverLogger?: ServerLogger;
16
+ runtime: Runtime;
17
+ webAssetsDir?: string;
18
+ };
19
+
20
+ export const createHttpApp = ({
21
+ auth,
22
+ authAssetsDirectory,
23
+ backgroundNotifications,
24
+ developmentWebTarget,
25
+ onRestartRequested,
26
+ serverLogger,
27
+ runtime,
28
+ webAssetsDir,
29
+ }: HttpAppOptions) => {
30
+ const app = new Hono<AuthEnvironment>();
31
+ const shutdown = { requested: false };
32
+
33
+ registerHttpRoutes({
34
+ app,
35
+ auth,
36
+ authAssetsDirectory,
37
+ backgroundNotifications,
38
+ developmentWebTarget,
39
+ onRestartRequested,
40
+ runtime,
41
+ serverLogger,
42
+ shutdown,
43
+ webAssetsDir,
44
+ });
45
+
46
+ return Object.assign(app, {
47
+ beginShutdown: () => {
48
+ shutdown.requested = true;
49
+ },
50
+ });
51
+ };
52
+
53
+ export type OvermuxHttpApp = ReturnType<typeof createHttpApp>;
@@ -0,0 +1,83 @@
1
+ import type { HttpBindings } from "@hono/node-server";
2
+ import { proxyFetch } from "httpxy";
3
+ import type { Handler } from "hono";
4
+
5
+ import type { AuthBoundary, AuthEnvironment } from "../auth/auth-http";
6
+ import { errorDetails, type ServerLogger } from "../server-logger";
7
+
8
+ const createForwardingHeaders = ({
9
+ origin,
10
+ remoteAddress,
11
+ request,
12
+ }: {
13
+ origin: string;
14
+ remoteAddress?: string;
15
+ request: Request;
16
+ }) => {
17
+ const headers = new Headers(request.headers);
18
+ [...headers.keys()].forEach((name) => {
19
+ if (
20
+ name === "authorization" ||
21
+ name === "cookie" ||
22
+ name === "forwarded" ||
23
+ name.startsWith("x-forwarded-")
24
+ ) {
25
+ headers.delete(name);
26
+ }
27
+ });
28
+ const publicUrl = new URL(origin);
29
+ if (remoteAddress) {
30
+ headers.set("x-forwarded-for", remoteAddress);
31
+ }
32
+ headers.set("x-forwarded-host", publicUrl.host);
33
+ headers.set(
34
+ "x-forwarded-port",
35
+ publicUrl.port || (publicUrl.protocol === "https:" ? "443" : "80"),
36
+ );
37
+ headers.set("x-forwarded-proto", publicUrl.protocol.slice(0, -1));
38
+ return headers;
39
+ };
40
+
41
+ export const createDevelopmentWebHandler =
42
+ ({
43
+ auth,
44
+ serverLogger,
45
+ target,
46
+ }: {
47
+ auth: AuthBoundary;
48
+ serverLogger?: ServerLogger;
49
+ target: string;
50
+ }): Handler<AuthEnvironment> =>
51
+ async (context) => {
52
+ const incoming = (context.env as Partial<HttpBindings> | undefined)
53
+ ?.incoming;
54
+ const origin = auth.resolveOrigin({
55
+ remoteAddress: incoming?.socket.remoteAddress,
56
+ request: context.req.raw,
57
+ });
58
+ if (!origin) {
59
+ return context.text("Request origin was rejected", 403);
60
+ }
61
+ const headers = createForwardingHeaders({
62
+ origin,
63
+ remoteAddress: incoming?.socket.remoteAddress,
64
+ request: context.req.raw,
65
+ });
66
+ try {
67
+ return await proxyFetch(
68
+ target,
69
+ new Request(context.req.raw, { headers }),
70
+ undefined,
71
+ { changeOrigin: true, xfwd: false },
72
+ );
73
+ } catch (cause) {
74
+ serverLogger?.log({
75
+ details: errorDetails(cause),
76
+ event: "vite-http-proxy-failure",
77
+ level: "error",
78
+ });
79
+ return context.text("Overmux development server is unavailable", 503, {
80
+ "Cache-Control": "no-store",
81
+ });
82
+ }
83
+ };