overmux 0.0.1 → 0.0.3

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 (201) hide show
  1. package/CHANGELOG.md +27 -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 +4863 -7897
  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 +50 -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 +23 -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 +20 -0
  32. package/dist/docs/400-reference/400-cli/050-init.md +17 -0
  33. package/dist/docs/400-reference/400-cli/100-serve.md +33 -0
  34. package/dist/docs/400-reference/400-cli/200-auth.md +52 -0
  35. package/dist/docs/400-reference/400-cli/300-call.md +28 -0
  36. package/dist/docs/400-reference/400-cli/400-check.md +20 -0
  37. package/dist/docs/400-reference/400-cli/500-ai-context.md +102 -0
  38. package/dist/docs/400-reference/400-cli/600-docs.md +23 -0
  39. package/dist/docs/400-reference/400-cli/700-desktop.md +35 -0
  40. package/dist/docs/500-hosted-pages.md +31 -0
  41. package/dist/exports/client.d.ts +254 -0
  42. package/dist/exports/client.d.ts.map +1 -0
  43. package/dist/exports/client.js +1665 -0
  44. package/dist/exports/client.js.map +1 -0
  45. package/dist/exports/index-D3_Dpy_T.d.ts +155 -0
  46. package/dist/exports/index-D3_Dpy_T.d.ts.map +1 -0
  47. package/dist/exports/index.d.ts +2 -0
  48. package/dist/exports/index.js +49 -0
  49. package/dist/exports/index.js.map +1 -0
  50. package/dist/exports/notifications-kDp16bU_.js +56 -0
  51. package/dist/exports/notifications-kDp16bU_.js.map +1 -0
  52. package/dist/exports/server.d.ts +63 -0
  53. package/dist/exports/server.d.ts.map +1 -0
  54. package/dist/exports/server.js +1120 -0
  55. package/dist/exports/server.js.map +1 -0
  56. package/dist/exports/style.css +368 -0
  57. package/dist/internal/server/coordinator/server-child.d.ts +1 -0
  58. package/dist/internal/server/coordinator/server-child.js +3299 -0
  59. package/dist/internal/server/coordinator/server-child.js.map +1 -0
  60. package/docs/000-index.md +21 -0
  61. package/docs/100-introduction/100-what-is-overmux.md +7 -0
  62. package/docs/100-introduction/200-how-overmux-works.md +7 -0
  63. package/docs/100-introduction/300-why-overmux.md +22 -0
  64. package/docs/200-getting-started/100-install-and-run-overmux.md +50 -0
  65. package/docs/200-getting-started/200-install-overmux-desktop.md +41 -0
  66. package/docs/200-getting-started/300-install-overmux-pwa.mdx +92 -0
  67. package/docs/200-getting-started/400-secure-with-https/100-choose-an-https-setup.md +14 -0
  68. package/docs/200-getting-started/400-secure-with-https/200-tailscale-serve.md +44 -0
  69. package/docs/200-getting-started/400-secure-with-https/300-cloudflare-tunnel.md +59 -0
  70. package/docs/200-getting-started/400-secure-with-https/400-self-hosted-reverse-proxy.md +32 -0
  71. package/docs/300-fundamentals/100-project-structure.md +23 -0
  72. package/docs/300-fundamentals/200-configuration.md +3 -0
  73. package/docs/300-fundamentals/300-theming.md +3 -0
  74. package/docs/300-fundamentals/400-server.md +3 -0
  75. package/docs/300-fundamentals/500-client.md +3 -0
  76. package/docs/300-fundamentals/600-operations.md +3 -0
  77. package/docs/300-fundamentals/700-resources.md +3 -0
  78. package/docs/300-fundamentals/800-streams.md +3 -0
  79. package/docs/300-fundamentals/900-authentication-and-security.md +3 -0
  80. package/docs/400-reference/100-configuration.md +3 -0
  81. package/docs/400-reference/200-server-api.md +3 -0
  82. package/docs/400-reference/300-client-api.md +20 -0
  83. package/docs/400-reference/400-cli/050-init.md +17 -0
  84. package/docs/400-reference/400-cli/100-serve.md +33 -0
  85. package/docs/400-reference/400-cli/200-auth.md +52 -0
  86. package/docs/400-reference/400-cli/300-call.md +28 -0
  87. package/docs/400-reference/400-cli/400-check.md +20 -0
  88. package/docs/400-reference/400-cli/500-ai-context.md +102 -0
  89. package/docs/400-reference/400-cli/600-docs.md +23 -0
  90. package/docs/400-reference/400-cli/700-desktop.md +35 -0
  91. package/docs/500-hosted-pages.md +31 -0
  92. package/package.json +59 -18
  93. package/src/internal/cli/app.ts +76 -0
  94. package/src/internal/cli/bin.ts +3 -0
  95. package/src/internal/cli/commands/ai.ts +44 -0
  96. package/src/internal/cli/commands/auth.ts +195 -0
  97. package/src/internal/cli/commands/call.ts +128 -0
  98. package/src/internal/cli/commands/check.ts +48 -0
  99. package/src/internal/cli/commands/desktop-installer.ts +446 -0
  100. package/src/internal/cli/commands/desktop-release.ts +190 -0
  101. package/src/internal/cli/commands/desktop.ts +47 -0
  102. package/src/internal/cli/commands/docs.ts +34 -0
  103. package/src/internal/cli/commands/init-template.ts +146 -0
  104. package/src/internal/cli/commands/init.ts +267 -0
  105. package/src/internal/cli/commands/integration.ts +15 -0
  106. package/src/internal/cli/commands/serve.ts +322 -0
  107. package/src/internal/cli/commands/zellij-install.ts +96 -0
  108. package/src/internal/cli/environment.ts +54 -0
  109. package/src/internal/cli/login.ts +46 -0
  110. package/src/internal/cli/managed-vite.ts +267 -0
  111. package/src/internal/cli/parse-port.ts +8 -0
  112. package/src/internal/client/auth/auth-api.ts +53 -0
  113. package/src/internal/client/auth/auth-shell.css +74 -0
  114. package/src/internal/client/auth/auth-shell.tsx +167 -0
  115. package/src/internal/client/auth/entry.tsx +26 -0
  116. package/src/internal/client/auth/index.html +13 -0
  117. package/src/internal/client/background-notifications.ts +194 -0
  118. package/src/internal/client/browser-api.ts +97 -0
  119. package/src/internal/client/client-definition.ts +184 -0
  120. package/src/internal/client/clipboard.ts +22 -0
  121. package/src/internal/client/commands.tsx +260 -0
  122. package/src/internal/client/host/browser-log-forwarding.ts +149 -0
  123. package/src/internal/client/host/desktop-host.ts +12 -0
  124. package/src/internal/client/host/hosted-pages/browser-notification-settings.tsx +118 -0
  125. package/src/internal/client/host/hosted-pages/hosted-page.css +87 -0
  126. package/src/internal/client/host/hosted-pages/hosted-page.tsx +9 -0
  127. package/src/internal/client/host/hosted-pages/logout-page.tsx +75 -0
  128. package/src/internal/client/host/hosted-pages/settings-page.tsx +45 -0
  129. package/src/internal/client/host/notification-forwarding.ts +13 -0
  130. package/src/internal/client/host/overmux-host.tsx +200 -0
  131. package/src/internal/client/host/recovery-screen.css +75 -0
  132. package/src/internal/client/host/recovery-screen.tsx +17 -0
  133. package/src/internal/client/host/update-popover.css +82 -0
  134. package/src/internal/client/host/update-popover.tsx +166 -0
  135. package/src/internal/client/index.ts +45 -0
  136. package/src/internal/client/overmux-react.ts +283 -0
  137. package/src/internal/client/shortcuts.tsx +373 -0
  138. package/src/internal/client/theme-scope.css +114 -0
  139. package/src/internal/client/theme-scope.tsx +107 -0
  140. package/src/internal/client/transport.ts +69 -0
  141. package/src/internal/client/websocket.ts +449 -0
  142. package/src/internal/server/auth/auth-http.ts +414 -0
  143. package/src/internal/server/auth/auth-service.ts +627 -0
  144. package/src/internal/server/auth/auth-shell-http.ts +90 -0
  145. package/src/internal/server/auth/instance-control.ts +381 -0
  146. package/src/internal/server/check-overmux.ts +364 -0
  147. package/src/internal/server/coordinator/ipc-protocol.ts +44 -0
  148. package/src/internal/server/coordinator/server-child.ts +108 -0
  149. package/src/internal/server/coordinator/server-coordinator.ts +262 -0
  150. package/src/internal/server/coordinator/update-watcher.ts +54 -0
  151. package/src/internal/server/http/authenticated-http-app-test-helper.ts +45 -0
  152. package/src/internal/server/http/background-notification-handlers.ts +83 -0
  153. package/src/internal/server/http/control-handlers.ts +44 -0
  154. package/src/internal/server/http/create-http-app.ts +53 -0
  155. package/src/internal/server/http/development-web-backend.ts +83 -0
  156. package/src/internal/server/http/operation-handler.ts +93 -0
  157. package/src/internal/server/http/request-logging.ts +34 -0
  158. package/src/internal/server/http/routes.ts +241 -0
  159. package/src/internal/server/http/runtime-manifest-handler.ts +12 -0
  160. package/src/internal/server/index.ts +16 -0
  161. package/src/internal/server/notifications/background-notification-payload.ts +74 -0
  162. package/src/internal/server/notifications/background-notification-service.ts +139 -0
  163. package/src/internal/server/notifications/background-notification-store.ts +162 -0
  164. package/src/internal/server/notifications/notification-service.ts +70 -0
  165. package/src/internal/server/notifications/private-json-file.ts +84 -0
  166. package/src/internal/server/paths.ts +13 -0
  167. package/src/internal/server/rate-limiter.ts +58 -0
  168. package/src/internal/server/runtime/create-runtime.ts +87 -0
  169. package/src/internal/server/runtime/resource-dependency-graph.ts +128 -0
  170. package/src/internal/server/runtime/runtime-lifecycle.ts +95 -0
  171. package/src/internal/server/runtime/runtime-operations.ts +78 -0
  172. package/src/internal/server/runtime/runtime-resources.ts +201 -0
  173. package/src/internal/server/runtime/runtime-streams.ts +130 -0
  174. package/src/internal/server/server-logger.ts +110 -0
  175. package/src/internal/server/server-startup-options.ts +51 -0
  176. package/src/internal/server/start-application-server.ts +412 -0
  177. package/src/internal/server/start-overmux-server.ts +26 -0
  178. package/src/internal/server/test-port.ts +23 -0
  179. package/src/internal/server/vite-websocket-proxy.ts +201 -0
  180. package/src/internal/server/websocket-connection.ts +586 -0
  181. package/src/internal/server/websocket-limits.ts +2 -0
  182. package/src/internal/server/websocket-server.ts +311 -0
  183. package/src/internal/shared/background-notifications.ts +37 -0
  184. package/src/internal/shared/index.ts +2 -0
  185. package/src/internal/shared/protocol.ts +317 -0
  186. package/src/internal/shared/routes.ts +3 -0
  187. package/src/public/ai-context.ts +44 -0
  188. package/src/public/client.ts +57 -0
  189. package/src/public/config.ts +287 -0
  190. package/src/public/contracts.ts +55 -0
  191. package/src/public/index.ts +57 -0
  192. package/src/public/notifications.ts +10 -0
  193. package/src/public/server.ts +16 -0
  194. package/dist/public/core.ts +0 -119
  195. package/dist/public/protocol.ts +0 -51
  196. package/dist/public/terminal.ts +0 -23
  197. package/dist/web/assets/index-DC___IUw.css +0 -2
  198. package/dist/web/assets/index-DF9Cptse.js +0 -74
  199. package/dist/web/assets/terminal-component-BrP-ENHg.css +0 -1
  200. package/dist/web/assets/terminal-component-DyLEbMrx.js +0 -36
  201. package/dist/web/index.html +0 -14
@@ -0,0 +1,93 @@
1
+ // Handles operation request validation, execution, cancellation, and lifecycle logs.
2
+ // Operation payloads are limited to 1 MiB before JSON parsing.
3
+ import type { Handler } from "hono";
4
+
5
+ import type { AuthEnvironment } from "../auth/auth-http";
6
+ import type { ServerLogger } from "../server-logger";
7
+ import type { Runtime } from "../runtime/create-runtime";
8
+ import { OperationValidationError } from "../runtime/runtime-operations";
9
+
10
+ const maxOperationRequestBytes = 1_048_576;
11
+
12
+ const parseOperationRequest = async (request: Request) => {
13
+ const contentType = request.headers.get("Content-Type")?.split(";", 1)[0];
14
+ if (contentType?.toLowerCase() !== "application/json") {
15
+ return {
16
+ error: "Content-Type must be application/json",
17
+ status: 415,
18
+ } as const;
19
+ }
20
+ const contentLength = Number(request.headers.get("Content-Length") ?? 0);
21
+ if (contentLength > maxOperationRequestBytes) {
22
+ return { error: "Request body is too large", status: 413 } as const;
23
+ }
24
+ try {
25
+ const body = await request.arrayBuffer();
26
+ if (body.byteLength > maxOperationRequestBytes) {
27
+ return { error: "Request body is too large", status: 413 } as const;
28
+ }
29
+ const text = new TextDecoder().decode(body);
30
+ return { input: text ? (JSON.parse(text) as unknown) : undefined };
31
+ } catch {
32
+ return { error: "Invalid JSON request body", status: 400 } as const;
33
+ }
34
+ };
35
+
36
+ export const createOperationHandler =
37
+ ({
38
+ runtime,
39
+ serverLogger,
40
+ }: {
41
+ runtime: Runtime;
42
+ serverLogger?: ServerLogger;
43
+ }): Handler<AuthEnvironment, "/api/operations/:name"> =>
44
+ async (context) => {
45
+ const operation = runtime.getOperation(context.req.param("name"));
46
+ if (!operation) {
47
+ return context.json({ error: "Operation not found" }, 404);
48
+ }
49
+ const parsed = await parseOperationRequest(context.req.raw);
50
+ if ("error" in parsed) {
51
+ return context.json({ error: parsed.error }, parsed.status);
52
+ }
53
+ const correlationId =
54
+ context.res.headers.get("X-Overmux-Correlation-Id") ?? serverLogger?.id();
55
+ const startedAt = Date.now();
56
+ serverLogger?.log({
57
+ correlationId,
58
+ details: { registeredName: context.req.param("name") },
59
+ event: "operation-start",
60
+ });
61
+ try {
62
+ const output = await operation.execute(
63
+ parsed.input,
64
+ context.req.raw.signal,
65
+ );
66
+ serverLogger?.log({
67
+ correlationId,
68
+ details: { registeredName: context.req.param("name") },
69
+ durationMs: Date.now() - startedAt,
70
+ event: "operation-complete",
71
+ });
72
+ return operation.returnsVoid
73
+ ? new Response(undefined, { status: 204 })
74
+ : context.json(output);
75
+ } catch (cause) {
76
+ const message =
77
+ cause instanceof Error ? cause.message : "Operation failed";
78
+ serverLogger?.log({
79
+ correlationId,
80
+ details: { registeredName: context.req.param("name") },
81
+ durationMs: Date.now() - startedAt,
82
+ event: "operation-error",
83
+ level: "error",
84
+ message,
85
+ });
86
+ return context.json(
87
+ { error: message },
88
+ cause instanceof OperationValidationError && cause.phase === "input"
89
+ ? 400
90
+ : 500,
91
+ );
92
+ }
93
+ };
@@ -0,0 +1,34 @@
1
+ // Adds request correlation IDs and emits the final HTTP request log entry.
2
+ // Logging runs after the matched middleware and route handler complete.
3
+ import type { MiddlewareHandler } from "hono";
4
+
5
+ import type { AuthEnvironment } from "../auth/auth-http";
6
+ import type { ServerLogger } from "../server-logger";
7
+
8
+ export const createRequestLoggingMiddleware =
9
+ ({
10
+ serverLogger,
11
+ }: {
12
+ serverLogger?: ServerLogger;
13
+ }): MiddlewareHandler<AuthEnvironment> =>
14
+ async (context, next) => {
15
+ const correlationId = serverLogger?.id();
16
+ const startedAt = Date.now();
17
+ if (correlationId) {
18
+ context.header("X-Overmux-Correlation-Id", correlationId);
19
+ }
20
+ await next();
21
+ const failed = context.res.status >= 400 || Boolean(context.error);
22
+ serverLogger?.log({
23
+ correlationId,
24
+ details: {
25
+ method: context.req.method,
26
+ path: context.req.path,
27
+ status: context.res.status,
28
+ },
29
+ durationMs: Date.now() - startedAt,
30
+ event: failed ? "http-request-failure" : "http-request",
31
+ level: failed ? "error" : undefined,
32
+ message: context.error?.message,
33
+ });
34
+ };
@@ -0,0 +1,241 @@
1
+ import { serveStatic } from "@hono/node-server/serve-static";
2
+ import type { Handler, Hono, MiddlewareHandler } from "hono";
3
+ import { join } from "node:path";
4
+
5
+ import {
6
+ createHttpAuthenticationHandlers,
7
+ type AuthBoundary,
8
+ type AuthEnvironment,
9
+ } from "../auth/auth-http";
10
+ import type { BackgroundNotificationService } from "../notifications/background-notification-service";
11
+ import type { Runtime } from "../runtime/create-runtime";
12
+ import type { ServerLogger } from "../server-logger";
13
+ import { createBackgroundNotificationHandlers } from "./background-notification-handlers";
14
+ import { createDevelopmentWebHandler } from "./development-web-backend";
15
+ import {
16
+ createHealthHandler,
17
+ createRestartHandler,
18
+ type ShutdownState,
19
+ } from "./control-handlers";
20
+ import { createOperationHandler } from "./operation-handler";
21
+ import { createRequestLoggingMiddleware } from "./request-logging";
22
+ import { createRuntimeManifestHandler } from "./runtime-manifest-handler";
23
+ import { overmuxLogoutPath, overmuxSettingsPath } from "@overmux/shared";
24
+ import { viteInfrastructurePath } from "../../shared/routes";
25
+
26
+ // Registration order is the public/protected boundary. Authentication state,
27
+ // login, and health are registered before the session gate; runtime APIs and
28
+ // static assets are registered after it. Keep every Hono route visible in this
29
+ // file for review.
30
+ type RouteDescriptor =
31
+ | {
32
+ method: "USE";
33
+ path: string;
34
+ handler: MiddlewareHandler<AuthEnvironment>;
35
+ }
36
+ | {
37
+ method: "GET" | "POST" | "PUT" | "DELETE" | "ALL";
38
+ path: string;
39
+ handler: Handler<AuthEnvironment> | MiddlewareHandler<AuthEnvironment>;
40
+ middleware?: MiddlewareHandler<AuthEnvironment>;
41
+ };
42
+
43
+ type RegisterHttpRoutesOptions = {
44
+ app: Hono<AuthEnvironment>;
45
+ auth: AuthBoundary;
46
+ authAssetsDirectory?: string;
47
+ backgroundNotifications?: BackgroundNotificationService;
48
+ developmentWebTarget?: string;
49
+ onRestartRequested?: () => void;
50
+ runtime: Runtime;
51
+ serverLogger?: ServerLogger;
52
+ shutdown: ShutdownState;
53
+ webAssetsDir?: string;
54
+ };
55
+
56
+ const applyDescriptor = (
57
+ app: Hono<AuthEnvironment>,
58
+ route: RouteDescriptor,
59
+ ) => {
60
+ if (route.method === "USE") {
61
+ app.use(route.path, route.handler);
62
+ return;
63
+ }
64
+ if (route.middleware) {
65
+ app.on(route.method, route.path, route.middleware, route.handler);
66
+ return;
67
+ }
68
+ app.on(route.method, route.path, route.handler);
69
+ };
70
+
71
+ export const registerHttpRoutes = ({
72
+ app,
73
+ auth,
74
+ authAssetsDirectory,
75
+ backgroundNotifications,
76
+ developmentWebTarget,
77
+ onRestartRequested,
78
+ runtime,
79
+ serverLogger,
80
+ shutdown,
81
+ webAssetsDir,
82
+ }: RegisterHttpRoutesOptions) => {
83
+ const authentication = createHttpAuthenticationHandlers({
84
+ auth,
85
+ authAssetsDirectory,
86
+ });
87
+ const notificationHandlers = backgroundNotifications
88
+ ? createBackgroundNotificationHandlers({ backgroundNotifications })
89
+ : undefined;
90
+ const developmentWebHandler = developmentWebTarget
91
+ ? createDevelopmentWebHandler({
92
+ auth,
93
+ serverLogger,
94
+ target: developmentWebTarget,
95
+ })
96
+ : undefined;
97
+ const productionWebHandler = webAssetsDir
98
+ ? serveStatic({ path: join(webAssetsDir, "index.html") })
99
+ : undefined;
100
+ const hostedWebHandler = developmentWebHandler ?? productionWebHandler;
101
+ const routes: RouteDescriptor[] = [
102
+ {
103
+ method: "USE",
104
+ path: "*",
105
+ handler: createRequestLoggingMiddleware({ serverLogger }),
106
+ },
107
+ // BEGIN: UNAUTHENTICATED
108
+ {
109
+ method: "GET",
110
+ path: "/_overmux/auth-shell/*",
111
+ handler: authentication.asset,
112
+ },
113
+ {
114
+ method: "GET",
115
+ path: "/api/auth/state",
116
+ handler: authentication.state,
117
+ },
118
+ {
119
+ method: "POST",
120
+ path: "/api/auth/login",
121
+ middleware: authentication.loginBodyLimitMiddleware,
122
+ handler: authentication.login,
123
+ },
124
+ {
125
+ method: "GET",
126
+ path: "/login",
127
+ handler: authentication.loginPage,
128
+ },
129
+ {
130
+ method: "GET",
131
+ path: "/api/health",
132
+ handler: createHealthHandler({ shutdown }),
133
+ },
134
+ // END: UNAUTHENTICATED
135
+ {
136
+ method: "USE",
137
+ path: "*",
138
+ handler: authentication.authenticate,
139
+ },
140
+ // BEGIN: AUTHENTICATED
141
+ {
142
+ method: "POST",
143
+ path: "/api/auth/logout",
144
+ handler: authentication.logout,
145
+ },
146
+ {
147
+ method: "POST",
148
+ path: "/api/operations/:name",
149
+ handler: createOperationHandler({ runtime, serverLogger }),
150
+ },
151
+ {
152
+ method: "POST",
153
+ path: "/api/restart",
154
+ handler: createRestartHandler({ onRestartRequested, shutdown }),
155
+ },
156
+ {
157
+ method: "GET",
158
+ path: "/api/runtime-manifest",
159
+ handler: createRuntimeManifestHandler({ runtime }),
160
+ },
161
+ ...(notificationHandlers
162
+ ? [
163
+ {
164
+ method: "GET" as const,
165
+ path: "/api/background-notifications/public-key",
166
+ handler: notificationHandlers.publicKey,
167
+ },
168
+ {
169
+ method: "PUT" as const,
170
+ path: "/api/background-notifications/subscription",
171
+ handler: notificationHandlers.enable,
172
+ },
173
+ {
174
+ method: "DELETE" as const,
175
+ path: "/api/background-notifications/subscription",
176
+ handler: notificationHandlers.disable,
177
+ },
178
+ ]
179
+ : []),
180
+ {
181
+ method: "ALL",
182
+ path: "/api/*",
183
+ handler: (context) => context.notFound(),
184
+ },
185
+ ...(developmentWebHandler
186
+ ? [
187
+ {
188
+ method: "ALL" as const,
189
+ path: viteInfrastructurePath,
190
+ handler: developmentWebHandler,
191
+ },
192
+ ]
193
+ : []),
194
+ ...(hostedWebHandler
195
+ ? [
196
+ {
197
+ method: "GET" as const,
198
+ path: overmuxSettingsPath,
199
+ handler: hostedWebHandler,
200
+ },
201
+ {
202
+ method: "GET" as const,
203
+ path: overmuxLogoutPath,
204
+ handler: hostedWebHandler,
205
+ },
206
+ ]
207
+ : []),
208
+ {
209
+ method: "ALL",
210
+ path: "/_overmux/*",
211
+ handler: (context) => context.notFound(),
212
+ },
213
+ ...(developmentWebHandler
214
+ ? [
215
+ {
216
+ method: "ALL" as const,
217
+ path: "*",
218
+ handler: developmentWebHandler,
219
+ },
220
+ ]
221
+ : []),
222
+ ...(webAssetsDir
223
+ ? [
224
+ {
225
+ method: "USE" as const,
226
+ path: "*",
227
+ handler: serveStatic({ root: webAssetsDir }),
228
+ },
229
+ // Serve the SPA shell for client-side routes that do not match a static asset.
230
+ {
231
+ method: "GET" as const,
232
+ path: "*",
233
+ handler: serveStatic({ path: join(webAssetsDir, "index.html") }),
234
+ },
235
+ ]
236
+ : []),
237
+ // END: AUTHENTICATED
238
+ ];
239
+
240
+ routes.forEach((route) => applyDescriptor(app, route));
241
+ };
@@ -0,0 +1,12 @@
1
+ // Runtime manifest responses must not be stored by client caches.
2
+ import type { Handler } from "hono";
3
+
4
+ import type { AuthEnvironment } from "../auth/auth-http";
5
+ import type { Runtime } from "../runtime/create-runtime";
6
+
7
+ export const createRuntimeManifestHandler =
8
+ ({ runtime }: { runtime: Runtime }): Handler<AuthEnvironment> =>
9
+ (context) => {
10
+ context.header("Cache-Control", "no-store");
11
+ return context.json(runtime.manifest);
12
+ };
@@ -0,0 +1,16 @@
1
+ export {
2
+ checkOvermux,
3
+ type CheckDiagnostic,
4
+ type CheckOvermuxOptions,
5
+ type CheckResult,
6
+ } from "./check-overmux";
7
+ export {
8
+ getOvermuxPaths,
9
+ type GetOvermuxPathsOptions,
10
+ type OvermuxPaths,
11
+ } from "./paths";
12
+ export {
13
+ startOvermuxServer,
14
+ type OvermuxServer,
15
+ type OvermuxServerOptions,
16
+ } from "./start-overmux-server";
@@ -0,0 +1,74 @@
1
+ // Fits background notification payloads within the Web Push provider limit.
2
+ // Input is validated by the notification service before this internal boundary.
3
+ import type { Notification } from "../../../public/index";
4
+
5
+ const maxPayloadBytes = 3_800;
6
+
7
+ const payloadBytes = (payload: unknown) =>
8
+ new TextEncoder().encode(JSON.stringify(payload)).byteLength;
9
+
10
+ const largestFittingPrefix = (
11
+ characters: string[],
12
+ accepts: (prefix: string) => boolean,
13
+ low = 0,
14
+ high = characters.length,
15
+ ): string => {
16
+ if (low >= high) {
17
+ return characters.slice(0, low).join("");
18
+ }
19
+ const middle = Math.ceil((low + high) / 2);
20
+ return accepts(characters.slice(0, middle).join(""))
21
+ ? largestFittingPrefix(characters, accepts, middle, high)
22
+ : largestFittingPrefix(characters, accepts, low, middle - 1);
23
+ };
24
+
25
+ const notificationPayload = (notification: Notification) => ({
26
+ notification,
27
+ type: "notification" as const,
28
+ });
29
+
30
+ export const createBackgroundNotificationPayload = (
31
+ notification: Notification,
32
+ ) => {
33
+ const original = notificationPayload(notification);
34
+ if (payloadBytes(original) <= maxPayloadBytes) {
35
+ return JSON.stringify(original);
36
+ }
37
+
38
+ const withoutBody = { ...notification, body: undefined };
39
+ const body = largestFittingPrefix(
40
+ [...(notification.body ?? "")],
41
+ (prefix) =>
42
+ payloadBytes(
43
+ notificationPayload({
44
+ ...withoutBody,
45
+ ...(prefix ? { body: prefix } : {}),
46
+ }),
47
+ ) <= maxPayloadBytes,
48
+ );
49
+ const bodyTruncated = notificationPayload({
50
+ ...withoutBody,
51
+ ...(body ? { body } : {}),
52
+ });
53
+ if (payloadBytes(bodyTruncated) <= maxPayloadBytes) {
54
+ return JSON.stringify(bodyTruncated);
55
+ }
56
+
57
+ const title = largestFittingPrefix(
58
+ [...notification.title],
59
+ (prefix) =>
60
+ Boolean(prefix) &&
61
+ payloadBytes(notificationPayload({ ...withoutBody, title: prefix })) <=
62
+ maxPayloadBytes,
63
+ );
64
+ if (title) {
65
+ return JSON.stringify(notificationPayload({ ...withoutBody, title }));
66
+ }
67
+ const safeTitle = largestFittingPrefix(
68
+ [...notification.title],
69
+ (prefix) =>
70
+ Boolean(prefix) &&
71
+ payloadBytes(notificationPayload({ title: prefix })) <= maxPayloadBytes,
72
+ );
73
+ return JSON.stringify(notificationPayload({ title: safeTitle || "Overmux" }));
74
+ };
@@ -0,0 +1,139 @@
1
+ // Delivers background notifications through the browser Web Push standard.
2
+ // Provider payload, VAPID signing, and expired endpoints remain inside this boundary.
3
+ import type { BackgroundNotificationSubscription } from "../../shared/index";
4
+ import type { Notification } from "../../../public/index";
5
+ import webPush from "web-push";
6
+
7
+ import { errorDetails, type ServerLogger } from "../server-logger";
8
+ import type { BackgroundNotificationStore } from "./background-notification-store";
9
+ import { createBackgroundNotificationPayload } from "./background-notification-payload";
10
+
11
+ const vapidSubject = "https://github.com/richardgill/overmux";
12
+
13
+ const webPushStatus = (cause: unknown) => {
14
+ if (
15
+ typeof cause === "object" &&
16
+ cause !== null &&
17
+ "statusCode" in cause &&
18
+ typeof cause.statusCode === "number"
19
+ ) {
20
+ return cause.statusCode;
21
+ }
22
+ return undefined;
23
+ };
24
+
25
+ const deliverToSubscription = async ({
26
+ keys,
27
+ logger,
28
+ payload,
29
+ sendWebPush,
30
+ store,
31
+ subscription,
32
+ }: {
33
+ keys: Awaited<ReturnType<BackgroundNotificationStore["getOrCreateKeys"]>>;
34
+ logger?: ServerLogger;
35
+ payload: string;
36
+ sendWebPush: typeof webPush.sendNotification;
37
+ store: BackgroundNotificationStore;
38
+ subscription: BackgroundNotificationSubscription;
39
+ }) => {
40
+ try {
41
+ await sendWebPush(subscription, payload, {
42
+ TTL: 60,
43
+ vapidDetails: {
44
+ privateKey: keys.privateKey,
45
+ publicKey: keys.publicKey,
46
+ subject: vapidSubject,
47
+ },
48
+ });
49
+ } catch (cause) {
50
+ const status = webPushStatus(cause);
51
+ if (status === 404 || status === 410) {
52
+ await store.removeSubscription(subscription.endpoint);
53
+ }
54
+ logger?.log({
55
+ details: {
56
+ ...errorDetails(cause),
57
+ ...(status ? { status } : {}),
58
+ },
59
+ event: "background-notification-delivery-failure",
60
+ level: "warn",
61
+ });
62
+ }
63
+ };
64
+
65
+ export type BackgroundNotificationService = {
66
+ disable: (endpoint: string) => Promise<void>;
67
+ disableSessions: (sessionIds: ReadonlySet<string>) => Promise<void>;
68
+ enable: (
69
+ sessionId: string,
70
+ subscription: BackgroundNotificationSubscription,
71
+ ) => Promise<void>;
72
+ publicKey: () => Promise<string>;
73
+ send: (notification: Notification) => Promise<void>;
74
+ };
75
+
76
+ export const createBackgroundNotificationService = ({
77
+ isSessionActive = async () => true,
78
+ logger,
79
+ sendWebPush = webPush.sendNotification,
80
+ store,
81
+ }: {
82
+ isSessionActive?: (sessionId: string) => Promise<boolean>;
83
+ logger?: ServerLogger;
84
+ sendWebPush?: typeof webPush.sendNotification;
85
+ store: BackgroundNotificationStore;
86
+ }): BackgroundNotificationService => {
87
+ let keysPromise:
88
+ | ReturnType<BackgroundNotificationStore["getOrCreateKeys"]>
89
+ | undefined;
90
+ const getKeys = () => {
91
+ if (!keysPromise) {
92
+ keysPromise = store
93
+ .getOrCreateKeys(webPush.generateVAPIDKeys)
94
+ .catch((cause: unknown) => {
95
+ keysPromise = undefined;
96
+ throw cause;
97
+ });
98
+ }
99
+ return keysPromise;
100
+ };
101
+
102
+ return {
103
+ disable: (endpoint) => store.removeSubscription(endpoint),
104
+ disableSessions: (sessionIds) =>
105
+ store.removeSessionSubscriptions(sessionIds),
106
+ enable: (sessionId, subscription) =>
107
+ store.upsertSubscription(sessionId, subscription),
108
+ publicKey: async () => (await getKeys()).publicKey,
109
+ send: async (notification) => {
110
+ const storedSubscriptions = await store.listSubscriptions();
111
+ const active = await Promise.all(
112
+ storedSubscriptions.map(async (entry) => ({
113
+ active: await isSessionActive(entry.sessionId),
114
+ entry,
115
+ })),
116
+ );
117
+ const subscriptions = active.flatMap(
118
+ ({ active: sessionActive, entry }) => (sessionActive ? [entry] : []),
119
+ );
120
+ if (!subscriptions.length) {
121
+ return;
122
+ }
123
+ const keys = await getKeys();
124
+ const payload = createBackgroundNotificationPayload(notification);
125
+ await Promise.all(
126
+ subscriptions.map((entry) =>
127
+ deliverToSubscription({
128
+ keys,
129
+ logger,
130
+ payload,
131
+ sendWebPush,
132
+ store,
133
+ subscription: entry.subscription,
134
+ }),
135
+ ),
136
+ );
137
+ },
138
+ };
139
+ };