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,3299 @@
1
+ import { serve } from "@hono/node-server";
2
+ import { chmod, link, mkdir, open, readFile, realpath, rename, rm, stat } from "node:fs/promises";
3
+ import { createServer, isIP } from "node:net";
4
+ import { dirname, isAbsolute, join, resolve } from "node:path";
5
+ import { createJiti } from "jiti";
6
+ import { ZodError, z } from "zod";
7
+ import { homedir, tmpdir } from "node:os";
8
+ import { once } from "node:events";
9
+ import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
10
+ import { generateCookie } from "hono/cookie";
11
+ import { bodyLimit } from "hono/body-limit";
12
+ import { parse } from "hono/utils/cookie";
13
+ import { appendFileSync, mkdirSync, readFileSync } from "node:fs";
14
+ import { fileURLToPath } from "node:url";
15
+ import { Hono } from "hono";
16
+ import { serveStatic } from "@hono/node-server/serve-static";
17
+ import { proxyFetch, proxyUpgrade } from "httpxy";
18
+ import { WebSocket, WebSocketServer } from "ws";
19
+ import webPush from "web-push";
20
+ import { isDeepStrictEqual } from "node:util";
21
+ //#region ../shared/dist/node/index.js
22
+ const durationPattern$1 = /^([1-9]\d*)(ms|s|m|h|d)$/;
23
+ const millisecondsByUnit$1 = {
24
+ d: 1440 * 60 * 1e3,
25
+ h: 3600 * 1e3,
26
+ m: 60 * 1e3,
27
+ ms: 1,
28
+ s: 1e3
29
+ };
30
+ const durationToMilliseconds$1 = (duration) => {
31
+ const match = durationPattern$1.exec(duration);
32
+ if (!match) return;
33
+ const unit = match[2];
34
+ const milliseconds = Number(match[1]) * millisecondsByUnit$1[unit];
35
+ return Number.isSafeInteger(milliseconds) ? milliseconds : void 0;
36
+ };
37
+ const aiContextSnippets = ["package-source", "tech-stack-recommendations"];
38
+ const zodTypeSchema = z.custom((value) => value instanceof z.ZodType, "Expected a Zod schema");
39
+ const functionSchema = z.custom((value) => typeof value === "function", "Expected a function");
40
+ const contractRuntimeSchema = z.object({
41
+ input: zodTypeSchema,
42
+ output: zodTypeSchema
43
+ });
44
+ const operationRuntimeSchema = z.object({
45
+ handle: functionSchema,
46
+ input: zodTypeSchema,
47
+ output: zodTypeSchema
48
+ });
49
+ const streamHandlerRuntimeSchema = z.object({
50
+ contract: z.object({
51
+ clientMessage: zodTypeSchema,
52
+ input: zodTypeSchema,
53
+ serverMessage: zodTypeSchema
54
+ }),
55
+ open: functionSchema
56
+ });
57
+ const resourceDefinitionRuntimeSchema = z.discriminatedUnion("kind", [
58
+ z.object({
59
+ contract: contractRuntimeSchema,
60
+ kind: z.literal("query"),
61
+ read: functionSchema
62
+ }),
63
+ z.object({
64
+ contract: contractRuntimeSchema,
65
+ kind: z.literal("subscription"),
66
+ read: functionSchema,
67
+ subscribe: functionSchema
68
+ }),
69
+ z.object({
70
+ combine: functionSchema,
71
+ contract: contractRuntimeSchema,
72
+ dependencies: z.record(z.string(), z.string()),
73
+ kind: z.literal("derived")
74
+ })
75
+ ]);
76
+ const authDurationSchema = z.string().regex(/^[1-9]\d*[mhd]$/, "Expected a positive duration such as 30d").refine((value) => durationToMilliseconds$1(value) !== void 0, "Duration is too large");
77
+ const isLoopbackHostname$1 = (hostname) => hostname === "localhost" || hostname === "[::1]" || /^127(?:\.\d{1,3}){3}$/.test(hostname);
78
+ const exactOriginSchema = z.url().refine((value) => new URL(value).origin === value, "Expected an origin").refine((value) => {
79
+ const url = new URL(value);
80
+ return url.protocol === "https:" || url.protocol === "http:" && isLoopbackHostname$1(url.hostname);
81
+ }, "Non-loopback Overmux origins must use HTTPS");
82
+ const loopbackPeerSchema = z.string().refine((value) => isIP(value) !== 0 && (value === "::1" || value.startsWith("127.")), "Expected a loopback IP address");
83
+ const authConfigRuntimeSchema = z.object({
84
+ mode: z.literal("cli-login"),
85
+ origins: z.array(exactOriginSchema).min(1).refine((origins) => new Set(origins).size === origins.length, "Expected unique origins").optional(),
86
+ sessionLifetime: z.union([z.literal("never"), authDurationSchema]).default("never"),
87
+ trustedProxyPeer: loopbackPeerSchema.optional()
88
+ }).strict();
89
+ const serverConfigRuntimeSchema = z.object({
90
+ host: z.string().min(1).optional(),
91
+ logFile: z.string().min(1).optional(),
92
+ port: z.number().int().min(1).max(65535).optional(),
93
+ productionWebAssetsDir: z.string().min(1).optional(),
94
+ watch: z.boolean().optional()
95
+ });
96
+ const serverDefinitionRuntimeSchema = z.object({
97
+ operations: z.record(z.string(), operationRuntimeSchema).optional(),
98
+ resources: z.record(z.string(), resourceDefinitionRuntimeSchema),
99
+ streams: z.record(z.string(), streamHandlerRuntimeSchema).optional()
100
+ });
101
+ const configDefinitionRuntimeSchema = serverConfigRuntimeSchema.extend({
102
+ aiContextSnippets: z.array(z.enum(aiContextSnippets)).optional(),
103
+ auth: authConfigRuntimeSchema,
104
+ debug: z.boolean().optional(),
105
+ server: serverDefinitionRuntimeSchema,
106
+ vite: z.string().min(1).optional()
107
+ });
108
+ const resolveLogicalPath = async (path) => {
109
+ const logicalPath = resolve(path);
110
+ return {
111
+ logicalPath,
112
+ realPath: await realpath(logicalPath)
113
+ };
114
+ };
115
+ const resolveOvermuxConfigPaths = async (configPath) => {
116
+ const { logicalPath, realPath } = await resolveLogicalPath(configPath);
117
+ return {
118
+ logicalApplicationRoot: dirname(logicalPath),
119
+ logicalConfigPath: logicalPath,
120
+ realApplicationRoot: dirname(realPath),
121
+ realConfigPath: realPath
122
+ };
123
+ };
124
+ const unwrapDefaultExport = (module) => {
125
+ if (typeof module === "object" && module !== null && "default" in module) return module.default;
126
+ return module;
127
+ };
128
+ const loadOvermuxConfig = async ({ aliases = {}, configPath }) => {
129
+ const paths = await resolveOvermuxConfigPaths(configPath);
130
+ const module = await createJiti(paths.logicalConfigPath, {
131
+ alias: aliases,
132
+ fsCache: true,
133
+ jsx: {
134
+ importSource: "react",
135
+ runtime: "automatic"
136
+ },
137
+ moduleCache: false,
138
+ sourceMaps: true,
139
+ tsconfigPaths: true
140
+ }).import(paths.logicalConfigPath);
141
+ return {
142
+ config: configDefinitionRuntimeSchema.parse(unwrapDefaultExport(module)),
143
+ paths
144
+ };
145
+ };
146
+ const xdgDirectory = (configured, fallback) => configured && isAbsolute(configured) ? configured : fallback;
147
+ const overmuxDirectory = (root) => join(root, "overmux");
148
+ const getOvermuxPaths = ({ environment = process.env, homeDir = homedir(), xdgCacheHome = environment.XDG_CACHE_HOME, xdgConfigHome = environment.XDG_CONFIG_HOME, xdgDataHome = environment.XDG_DATA_HOME, xdgRuntimeDir = environment.XDG_RUNTIME_DIR, xdgStateHome = environment.XDG_STATE_HOME } = {}) => {
149
+ if (!isAbsolute(homeDir)) throw new TypeError("The home directory must be absolute.");
150
+ const runtimeDir = xdgRuntimeDir && isAbsolute(xdgRuntimeDir) ? overmuxDirectory(xdgRuntimeDir) : void 0;
151
+ return {
152
+ cacheDir: overmuxDirectory(xdgDirectory(xdgCacheHome, join(homeDir, ".cache"))),
153
+ configDir: overmuxDirectory(xdgDirectory(xdgConfigHome, join(homeDir, ".config"))),
154
+ dataDir: overmuxDirectory(xdgDirectory(xdgDataHome, join(homeDir, ".local", "share"))),
155
+ ...runtimeDir ? { runtimeDir } : {},
156
+ stateDir: overmuxDirectory(xdgDirectory(xdgStateHome, join(homeDir, ".local", "state")))
157
+ };
158
+ };
159
+ //#endregion
160
+ //#region ../lib/dist/index.js
161
+ const durationPattern = /^([1-9]\d*)(ms|s|m|h|d)$/;
162
+ const millisecondsByUnit = {
163
+ d: 1440 * 60 * 1e3,
164
+ h: 3600 * 1e3,
165
+ m: 60 * 1e3,
166
+ ms: 1,
167
+ s: 1e3
168
+ };
169
+ const durationToMilliseconds = (duration) => {
170
+ const match = durationPattern.exec(duration);
171
+ if (!match) return;
172
+ const unit = match[2];
173
+ const milliseconds = Number(match[1]) * millisecondsByUnit[unit];
174
+ return Number.isSafeInteger(milliseconds) ? milliseconds : void 0;
175
+ };
176
+ //#endregion
177
+ //#region src/internal/server/notifications/private-json-file.ts
178
+ const missingFile = (cause) => cause.code === "ENOENT";
179
+ const preparePrivateDirectory = async (path) => {
180
+ await mkdir(path, {
181
+ mode: 448,
182
+ recursive: true
183
+ });
184
+ await chmod(path, 448);
185
+ };
186
+ const writeTemporaryPrivateJson = async (path, value) => {
187
+ await preparePrivateDirectory(dirname(path));
188
+ const temporaryPath = `${path}.${process.pid}.${randomUUID()}`;
189
+ try {
190
+ const handle = await open(temporaryPath, "wx", 384);
191
+ try {
192
+ await handle.writeFile(`${JSON.stringify(value)}\n`);
193
+ await handle.sync();
194
+ } finally {
195
+ await handle.close();
196
+ }
197
+ return temporaryPath;
198
+ } catch (cause) {
199
+ await rm(temporaryPath, { force: true });
200
+ throw cause;
201
+ }
202
+ };
203
+ const readPrivateJson = async (path, schema) => {
204
+ try {
205
+ const contents = await readFile(path, "utf8");
206
+ await chmod(path, 384);
207
+ return schema.parse(JSON.parse(contents));
208
+ } catch (cause) {
209
+ if (missingFile(cause)) return;
210
+ throw cause;
211
+ }
212
+ };
213
+ const replacePrivateJsonAtomically = async (path, value) => {
214
+ const temporaryPath = await writeTemporaryPrivateJson(path, value);
215
+ try {
216
+ await rename(temporaryPath, path);
217
+ } catch (cause) {
218
+ await rm(temporaryPath, { force: true });
219
+ throw cause;
220
+ }
221
+ };
222
+ const createPrivateJsonExclusively = async (path, value) => {
223
+ const temporaryPath = await writeTemporaryPrivateJson(path, value);
224
+ try {
225
+ await link(temporaryPath, path);
226
+ } finally {
227
+ await rm(temporaryPath, { force: true });
228
+ }
229
+ };
230
+ //#endregion
231
+ //#region src/internal/server/auth/auth-service.ts
232
+ const loginGrantLifetimeMs = 10 * 6e4;
233
+ const localBearerLifetimeMs = 6e4;
234
+ const maxCodeAttempts = 8;
235
+ const lastSeenWriteIntervalMs = 6e4;
236
+ const codeAlphabet = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
237
+ const loginGrantSchema$1 = z.object({
238
+ codeHash: z.string(),
239
+ codeLookupHash: z.string(),
240
+ consumedAt: z.string().optional(),
241
+ createdAt: z.string(),
242
+ expiresAt: z.string(),
243
+ failedCodeAttempts: z.number().int().nonnegative(),
244
+ id: z.string(),
245
+ linkTokenHash: z.string()
246
+ });
247
+ const authSessionSchema = z.object({
248
+ createdAt: z.string(),
249
+ expiresAt: z.string().optional(),
250
+ id: z.string(),
251
+ kind: z.enum(["browser", "local"]),
252
+ lastSeenAt: z.string(),
253
+ origin: z.string().optional(),
254
+ revokedAt: z.string().optional(),
255
+ tokenHash: z.string()
256
+ });
257
+ const authStoreSchema = z.object({
258
+ grants: z.array(loginGrantSchema$1),
259
+ installationId: z.string(),
260
+ sessions: z.array(authSessionSchema),
261
+ version: z.literal(1)
262
+ });
263
+ const tokenHash = (token) => createHash("sha256").update(token, "utf8").digest("hex");
264
+ const hashesEqual = (left, right) => {
265
+ const leftBytes = Buffer.from(left, "hex");
266
+ const rightBytes = Buffer.from(right, "hex");
267
+ return leftBytes.length === rightBytes.length && timingSafeEqual(leftBytes, rightBytes);
268
+ };
269
+ const randomToken = () => randomBytes(32).toString("base64url");
270
+ const randomCode = () => {
271
+ const bytes = randomBytes(8);
272
+ const characters = Array.from(bytes, (byte) => codeAlphabet[byte % 32]);
273
+ return `${characters.slice(0, 4).join("")}-${characters.slice(4).join("")}`;
274
+ };
275
+ const durationMilliseconds = (duration) => {
276
+ const milliseconds = durationToMilliseconds(authDurationSchema.parse(duration));
277
+ if (milliseconds === void 0) throw new Error("Expected a validated authentication duration");
278
+ return milliseconds;
279
+ };
280
+ const acquireStoreLock = async (path, deadline = Date.now() + 5e3) => {
281
+ await mkdir(dirname(path), {
282
+ mode: 448,
283
+ recursive: true
284
+ });
285
+ await chmod(dirname(path), 448);
286
+ try {
287
+ await (await open(path, "wx", 384)).close();
288
+ return () => rm(path, { force: true });
289
+ } catch (cause) {
290
+ if (cause.code !== "EEXIST") throw cause;
291
+ let lockAge;
292
+ try {
293
+ lockAge = Date.now() - (await stat(path)).mtimeMs;
294
+ } catch (statCause) {
295
+ if (statCause.code === "ENOENT") return acquireStoreLock(path, deadline);
296
+ throw statCause;
297
+ }
298
+ if (lockAge > 3e4) {
299
+ await rm(path, { force: true });
300
+ return acquireStoreLock(path, deadline);
301
+ }
302
+ if (Date.now() >= deadline) throw new Error("Timed out waiting for the authentication store lock");
303
+ await new Promise((resolve) => setTimeout(resolve, 10));
304
+ return acquireStoreLock(path, deadline);
305
+ }
306
+ };
307
+ const dataDirectory = ({ environment, homeDirectory }) => join(getOvermuxPaths({
308
+ environment,
309
+ homeDir: homeDirectory
310
+ }).dataDir, "auth");
311
+ const publicSession = ({ kind: _kind, tokenHash: _tokenHash, ...session }) => session;
312
+ const activeSession = (session, now) => !session.revokedAt && (session.kind === "local" || Boolean(session.origin)) && (!session.expiresAt || Date.parse(session.expiresAt) > now);
313
+ const revokeExpired = (store, currentTime) => {
314
+ const expiredIds = [];
315
+ return {
316
+ expiredIds,
317
+ sessions: store.sessions.map((session) => {
318
+ if (!session.revokedAt && session.expiresAt && Date.parse(session.expiresAt) <= currentTime) {
319
+ expiredIds.push(session.id);
320
+ return {
321
+ ...session,
322
+ revokedAt: new Date(currentTime).toISOString()
323
+ };
324
+ }
325
+ return session;
326
+ })
327
+ };
328
+ };
329
+ const authenticateStoredToken = ({ currentTime, store, origin, token }) => {
330
+ const { expiredIds, sessions } = revokeExpired(store, currentTime);
331
+ const expectedHash = tokenHash(token);
332
+ const session = sessions.find((candidate) => hashesEqual(candidate.tokenHash, expectedHash));
333
+ const authenticated = session && activeSession(session, currentTime) && (session.kind === "local" || session.origin === origin) ? {
334
+ ...session.expiresAt ? { expiresAt: session.expiresAt } : {},
335
+ id: session.id,
336
+ kind: session.kind,
337
+ ...session.origin ? { origin: session.origin } : {},
338
+ token
339
+ } : void 0;
340
+ const updateLastSeen = Boolean(session && authenticated && currentTime - Date.parse(session.lastSeenAt) >= lastSeenWriteIntervalMs);
341
+ return {
342
+ authenticated,
343
+ expiredIds,
344
+ nextSessions: updateLastSeen ? sessions.map((candidate) => candidate.id === session?.id ? {
345
+ ...candidate,
346
+ lastSeenAt: new Date(currentTime).toISOString()
347
+ } : candidate) : sessions,
348
+ updateLastSeen
349
+ };
350
+ };
351
+ const createAuthService = ({ config, environment = process.env, homeDirectory = homedir(), now = Date.now }) => {
352
+ const directory = dataDirectory({
353
+ environment,
354
+ homeDirectory
355
+ });
356
+ const storePath = join(directory, "auth.json");
357
+ const lockPath = join(directory, "auth.lock");
358
+ const listeners = /* @__PURE__ */ new Set();
359
+ let origins = config.origins;
360
+ let pendingWrite = Promise.resolve();
361
+ const readStore = async () => {
362
+ const existing = await readPrivateJson(storePath, authStoreSchema);
363
+ if (existing) return existing;
364
+ const initial = {
365
+ grants: [],
366
+ installationId: randomUUID(),
367
+ sessions: [],
368
+ version: 1
369
+ };
370
+ try {
371
+ await createPrivateJsonExclusively(storePath, initial);
372
+ return initial;
373
+ } catch (cause) {
374
+ if (cause.code !== "EEXIST") throw cause;
375
+ const concurrentlyCreated = await readPrivateJson(storePath, authStoreSchema);
376
+ if (!concurrentlyCreated) throw cause;
377
+ return concurrentlyCreated;
378
+ }
379
+ };
380
+ const updateStore = (update) => {
381
+ const operation = pendingWrite.then(async () => {
382
+ const release = await acquireStoreLock(lockPath);
383
+ try {
384
+ const updated = update(await readStore());
385
+ if (updated.store) await replacePrivateJsonAtomically(storePath, updated.store);
386
+ emitRevoked(updated.revokedIds ?? []);
387
+ return updated.result;
388
+ } finally {
389
+ await release();
390
+ }
391
+ });
392
+ pendingWrite = operation.then(() => void 0, () => void 0);
393
+ return operation;
394
+ };
395
+ const emitRevoked = (ids) => {
396
+ if (ids.length === 0) return;
397
+ const revoked = new Set(ids);
398
+ listeners.forEach((listener) => listener(revoked));
399
+ };
400
+ const createSession = ({ kind, lifetimeMs, origin }) => {
401
+ const token = randomToken();
402
+ const createdAt = new Date(now()).toISOString();
403
+ const configuredLifetime = config.sessionLifetime && config.sessionLifetime !== "never" ? durationMilliseconds(config.sessionLifetime) : void 0;
404
+ const expiresIn = lifetimeMs ?? configuredLifetime;
405
+ return {
406
+ stored: {
407
+ createdAt,
408
+ ...expiresIn ? { expiresAt: new Date(now() + expiresIn).toISOString() } : {},
409
+ id: randomUUID(),
410
+ kind,
411
+ lastSeenAt: createdAt,
412
+ ...origin ? { origin } : {},
413
+ tokenHash: tokenHash(token)
414
+ },
415
+ token
416
+ };
417
+ };
418
+ const authenticateToken = async (token, origin) => {
419
+ await pendingWrite;
420
+ const currentTime = now();
421
+ const inspected = authenticateStoredToken({
422
+ currentTime,
423
+ origin,
424
+ store: await readStore(),
425
+ token
426
+ });
427
+ if (inspected.expiredIds.length === 0 && !inspected.updateLastSeen) return inspected.authenticated;
428
+ return updateStore((store) => {
429
+ const current = authenticateStoredToken({
430
+ currentTime: now(),
431
+ origin,
432
+ store,
433
+ token
434
+ });
435
+ return {
436
+ result: current.authenticated,
437
+ revokedIds: current.expiredIds,
438
+ store: {
439
+ ...store,
440
+ sessions: current.nextSessions
441
+ }
442
+ };
443
+ });
444
+ };
445
+ return {
446
+ authenticateToken,
447
+ consumeLoginGrant: (input) => updateStore((store) => {
448
+ const currentTime = now();
449
+ const presented = input.code?.trim().toUpperCase() ?? input.ticket;
450
+ if (!presented || Boolean(input.code) === Boolean(input.ticket)) return { result: { status: "invalid" } };
451
+ const expectedHash = tokenHash(presented);
452
+ const codeLookupHash = input.code ? tokenHash(presented.split("-", 1)[0] ?? "") : void 0;
453
+ const exactCodeIndex = store.grants.findIndex((grant) => hashesEqual(grant.codeHash, expectedHash));
454
+ const codeIndex = exactCodeIndex >= 0 ? exactCodeIndex : store.grants.findIndex((grant) => hashesEqual(grant.codeLookupHash, codeLookupHash ?? ""));
455
+ const index = input.code ? codeIndex : store.grants.findIndex((grant) => hashesEqual(grant.linkTokenHash, expectedHash));
456
+ if (index < 0) return { result: { status: "invalid" } };
457
+ const grant = store.grants[index];
458
+ if (grant.consumedAt) return { result: { status: "invalid" } };
459
+ if (Date.parse(grant.expiresAt) <= currentTime) return { result: { status: "expired" } };
460
+ if (input.code && grant.failedCodeAttempts >= maxCodeAttempts) return { result: { status: "rate-limited" } };
461
+ if (input.code && !hashesEqual(grant.codeHash, expectedHash)) {
462
+ const grants = store.grants.map((candidate, grantIndex) => grantIndex === index ? {
463
+ ...candidate,
464
+ failedCodeAttempts: candidate.failedCodeAttempts + 1
465
+ } : candidate);
466
+ return {
467
+ result: { status: "invalid" },
468
+ store: {
469
+ ...store,
470
+ grants
471
+ }
472
+ };
473
+ }
474
+ const { stored, token } = createSession({
475
+ kind: "browser",
476
+ origin: input.origin
477
+ });
478
+ const grants = store.grants.map((candidate, grantIndex) => grantIndex === index ? {
479
+ ...candidate,
480
+ consumedAt: new Date(currentTime).toISOString()
481
+ } : candidate);
482
+ return {
483
+ result: {
484
+ session: publicSession(stored),
485
+ status: "authenticated",
486
+ token
487
+ },
488
+ store: {
489
+ ...store,
490
+ grants,
491
+ sessions: [...store.sessions.filter((session) => activeSession(session, currentTime)), stored]
492
+ }
493
+ };
494
+ }),
495
+ createLoginGrant: () => updateStore((store) => {
496
+ if (!origins) throw new Error("The server has not established its browser origins");
497
+ const code = randomCode();
498
+ const ticket = randomToken();
499
+ const createdAt = new Date(now()).toISOString();
500
+ const expiresAt = new Date(now() + loginGrantLifetimeMs).toISOString();
501
+ const id = randomUUID();
502
+ const grant = {
503
+ codeHash: tokenHash(code),
504
+ codeLookupHash: tokenHash(code.split("-", 1)[0]),
505
+ createdAt,
506
+ expiresAt,
507
+ failedCodeAttempts: 0,
508
+ id,
509
+ linkTokenHash: tokenHash(ticket)
510
+ };
511
+ return {
512
+ result: {
513
+ code,
514
+ expiresAt,
515
+ id,
516
+ urls: origins.map((origin) => `${origin}/login#ticket=${encodeURIComponent(ticket)}`)
517
+ },
518
+ store: {
519
+ ...store,
520
+ grants: [...store.grants.filter((candidate) => Date.parse(candidate.expiresAt) > now() && !candidate.consumedAt), grant]
521
+ }
522
+ };
523
+ }),
524
+ issueLocalBearer: () => updateStore((store) => {
525
+ const currentTime = now();
526
+ const { stored, token } = createSession({
527
+ kind: "local",
528
+ lifetimeMs: localBearerLifetimeMs
529
+ });
530
+ return {
531
+ result: {
532
+ expiresAt: stored.expiresAt,
533
+ token
534
+ },
535
+ store: {
536
+ ...store,
537
+ sessions: [...store.sessions.filter((session) => activeSession(session, currentTime)), stored]
538
+ }
539
+ };
540
+ }),
541
+ isSessionActive: (id) => updateStore((store) => {
542
+ const currentTime = now();
543
+ const { expiredIds, sessions } = revokeExpired(store, currentTime);
544
+ return {
545
+ result: Boolean(sessions.find((session) => session.id === id && activeSession(session, currentTime))),
546
+ revokedIds: expiredIds,
547
+ ...expiredIds.length > 0 ? { store: {
548
+ ...store,
549
+ sessions
550
+ } } : {}
551
+ };
552
+ }),
553
+ listSessions: () => updateStore((store) => {
554
+ const currentTime = now();
555
+ const { expiredIds, sessions } = revokeExpired(store, currentTime);
556
+ return {
557
+ result: sessions.filter((session) => session.kind === "browser" && activeSession(session, currentTime)).map(publicSession),
558
+ revokedIds: expiredIds,
559
+ ...expiredIds.length > 0 ? { store: {
560
+ ...store,
561
+ sessions
562
+ } } : {}
563
+ };
564
+ }),
565
+ onSessionsRevoked: (listener) => {
566
+ listeners.add(listener);
567
+ return () => listeners.delete(listener);
568
+ },
569
+ revokeAllSessions: () => updateStore((store) => {
570
+ const currentTime = now();
571
+ const expired = revokeExpired(store, currentTime);
572
+ const ids = expired.sessions.filter((session) => session.kind === "browser" && activeSession(session, currentTime)).map((session) => session.id);
573
+ const revoked = new Set(ids);
574
+ const revokedAt = new Date(currentTime).toISOString();
575
+ const sessions = expired.sessions.map((session) => revoked.has(session.id) ? {
576
+ ...session,
577
+ revokedAt
578
+ } : session);
579
+ const revokedIds = [...expired.expiredIds, ...ids];
580
+ return {
581
+ result: ids.length,
582
+ revokedIds,
583
+ ...revokedIds.length > 0 ? { store: {
584
+ ...store,
585
+ sessions
586
+ } } : {}
587
+ };
588
+ }),
589
+ revokeSession: (id) => updateStore((store) => {
590
+ if (!store.sessions.find((candidate) => candidate.id === id && !candidate.revokedAt)) return { result: false };
591
+ const revokedAt = new Date(now()).toISOString();
592
+ const sessions = store.sessions.map((candidate) => candidate.id === id ? {
593
+ ...candidate,
594
+ revokedAt
595
+ } : candidate);
596
+ return {
597
+ result: true,
598
+ revokedIds: [id],
599
+ store: {
600
+ ...store,
601
+ sessions
602
+ }
603
+ };
604
+ }),
605
+ setOrigins: (nextOrigins) => {
606
+ origins = nextOrigins.map((origin) => new URL(origin).origin);
607
+ }
608
+ };
609
+ };
610
+ //#endregion
611
+ //#region src/internal/server/rate-limiter.ts
612
+ const createRateLimiter = ({ maxAttemptsPerKey, maxGlobalAttempts, maxTrackedKeys, windowMs }) => ({
613
+ attemptsByKey: /* @__PURE__ */ new Map(),
614
+ globalAttempts: [],
615
+ maxAttemptsPerKey,
616
+ maxGlobalAttempts,
617
+ maxTrackedKeys,
618
+ windowMs
619
+ });
620
+ const hasRateLimitCapacity = ({ currentTime = Date.now(), key, limiter }) => {
621
+ const threshold = currentTime - limiter.windowMs;
622
+ const recent = (limiter.attemptsByKey.get(key) ?? []).filter((attempt) => attempt > threshold);
623
+ limiter.globalAttempts = limiter.globalAttempts.filter((attempt) => attempt > threshold);
624
+ if (recent.length >= limiter.maxAttemptsPerKey || limiter.globalAttempts.length >= limiter.maxGlobalAttempts) {
625
+ limiter.attemptsByKey.set(key, recent);
626
+ return false;
627
+ }
628
+ limiter.attemptsByKey.set(key, [...recent, currentTime]);
629
+ limiter.globalAttempts.push(currentTime);
630
+ if (limiter.attemptsByKey.size > limiter.maxTrackedKeys) limiter.attemptsByKey.clear();
631
+ return true;
632
+ };
633
+ //#endregion
634
+ //#region src/internal/server/auth/auth-shell-http.ts
635
+ const authEntry = "index.html";
636
+ const fingerprintedAssetPattern = /^assets\/[A-Za-z0-9_.-]+-[A-Za-z0-9_-]{8}\.(?:css|js)$/;
637
+ const defaultAuthAssetsDirectory = () => fileURLToPath(new URL("../auth/", import.meta.resolve("overmux")));
638
+ const loadAuthShell = (directory = defaultAuthAssetsDirectory()) => {
639
+ const manifest = JSON.parse(readFileSync(`${directory}/.vite/manifest.json`, "utf8"));
640
+ const entry = manifest[authEntry];
641
+ if (!entry?.file || !entry.css?.length) throw new Error("The packaged Overmux authentication assets are incomplete");
642
+ const paths = new Set(Object.values(manifest).flatMap(({ css = [], file }) => file ? [file, ...css] : css));
643
+ const invalidPath = [...paths].find((path) => !fingerprintedAssetPattern.test(path));
644
+ if (invalidPath) throw new Error(`Invalid Overmux authentication asset path: ${invalidPath}`);
645
+ return {
646
+ files: new Map([...paths].map((path) => [path, {
647
+ body: Uint8Array.from(readFileSync(`${directory}/${path}`)).buffer,
648
+ contentType: path.endsWith(".css") ? "text/css; charset=UTF-8" : "text/javascript; charset=UTF-8"
649
+ }])),
650
+ html: readFileSync(`${directory}/index.html`, "utf8")
651
+ };
652
+ };
653
+ const serveAuthShellAsset = (context, shell) => {
654
+ const asset = shell.files.get(context.req.path.slice(21));
655
+ if (!asset) return context.body(null, 404);
656
+ context.header("Cache-Control", "public, max-age=31536000, immutable");
657
+ context.header("Content-Type", asset.contentType);
658
+ context.header("Cross-Origin-Resource-Policy", "same-origin");
659
+ context.header("X-Content-Type-Options", "nosniff");
660
+ return context.body(asset.body);
661
+ };
662
+ const renderAuthShell = (context, shell, status = 200) => {
663
+ context.header("Cache-Control", "no-store");
664
+ context.header("Content-Security-Policy", "default-src 'none'; script-src 'self'; style-src 'self'; connect-src 'self'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'");
665
+ context.header("Cross-Origin-Resource-Policy", "same-origin");
666
+ context.header("Referrer-Policy", "no-referrer");
667
+ context.header("X-Content-Type-Options", "nosniff");
668
+ return context.html(shell.html, status);
669
+ };
670
+ const isBrowserNavigation = (request) => request.headers.get("Sec-Fetch-Dest") === "document" || request.headers.get("Accept")?.includes("text/html");
671
+ //#endregion
672
+ //#region src/internal/server/auth/auth-http.ts
673
+ const maxLoginBodyBytes = 4096;
674
+ const loginWindowMs = 6e4;
675
+ const maxLoginRequestsPerWindow = 12;
676
+ const maxGlobalLoginRequestsPerWindow = 120;
677
+ const maxTrackedLoginAddresses = 1e4;
678
+ const neverCookieLifetimeSeconds = 400 * 24 * 60 * 60;
679
+ const sessionCookieName = "overmux_session";
680
+ const bearerToken = (header) => /^Bearer ([A-Za-z0-9_-]+)$/.exec(header ?? "")?.[1];
681
+ const isLoopbackHostname = (hostname) => hostname === "localhost" || hostname === "::1" || hostname === "[::1]" || /^127(?:\.\d{1,3}){3}$/.test(hostname);
682
+ const validateAuthOrigin = (origin) => {
683
+ const url = new URL(origin);
684
+ if (url.origin !== origin) throw new Error("auth.origins must contain exact origins without paths");
685
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && isLoopbackHostname(url.hostname))) throw new Error("Non-loopback Overmux origins must use HTTPS");
686
+ return origin;
687
+ };
688
+ const exactForwardedValue = (value) => value !== null && value === value.trim() && !value.includes(",") ? value : void 0;
689
+ const resolveRequestOrigin = ({ origins, remoteAddress, request, trustedProxyPeer }) => {
690
+ const requestUrl = new URL(request.url);
691
+ const forwardedHost = request.headers.get("X-Forwarded-Host");
692
+ const forwardedProto = request.headers.get("X-Forwarded-Proto");
693
+ if (trustedProxyPeer !== void 0 && remoteAddress === trustedProxyPeer && (forwardedHost !== null || forwardedProto !== null)) {
694
+ const host = exactForwardedValue(forwardedHost);
695
+ if (exactForwardedValue(forwardedProto) !== "https" || !host || requestUrl.protocol !== "http:" || requestUrl.host !== host) return;
696
+ const forwardedOrigin = new URL(`https://${host}`).origin;
697
+ return origins.includes(forwardedOrigin) ? forwardedOrigin : void 0;
698
+ }
699
+ return origins.includes(requestUrl.origin) ? requestUrl.origin : void 0;
700
+ };
701
+ const cookieMaxAge = (expiresAt) => Math.min(neverCookieLifetimeSeconds, expiresAt ? Math.max(0, Math.floor((Date.parse(expiresAt) - Date.now()) / 1e3)) : neverCookieLifetimeSeconds);
702
+ const generateSessionCookie = ({ origin, token, maxAge }) => generateCookie(sessionCookieName, token, {
703
+ httpOnly: true,
704
+ maxAge,
705
+ path: "/",
706
+ sameSite: "Strict",
707
+ ...new URL(origin).protocol === "https:" ? { prefix: "host" } : {}
708
+ });
709
+ const createRequestAuthentication = ({ service }) => {
710
+ const cookieName = (origin) => new URL(origin).protocol === "https:" ? `__Host-${sessionCookieName}` : sessionCookieName;
711
+ const cookieHeader = (origin, token, expiresAt) => generateSessionCookie({
712
+ maxAge: cookieMaxAge(expiresAt),
713
+ origin,
714
+ token
715
+ });
716
+ return {
717
+ authenticate: async (request, origin) => {
718
+ const authorizationHeader = request.headers.get("Authorization");
719
+ if (authorizationHeader !== null) {
720
+ const token = bearerToken(authorizationHeader);
721
+ const authenticatedSession = token ? await service.authenticateToken(token) : void 0;
722
+ return authenticatedSession?.kind === "local" ? {
723
+ ...authenticatedSession,
724
+ credential: "bearer"
725
+ } : void 0;
726
+ }
727
+ if (!origin) return;
728
+ const name = cookieName(origin);
729
+ const token = parse(request.headers.get("Cookie") ?? "", name)[name];
730
+ const authenticatedSession = token ? await service.authenticateToken(token, origin) : void 0;
731
+ return authenticatedSession?.kind === "browser" ? {
732
+ ...authenticatedSession,
733
+ credential: "cookie"
734
+ } : void 0;
735
+ },
736
+ clearCookieHeader: (origin) => generateSessionCookie({
737
+ maxAge: 0,
738
+ origin,
739
+ token: ""
740
+ }),
741
+ cookieHeader
742
+ };
743
+ };
744
+ const createAuthBoundary = ({ origins, service, trustedProxyPeer }) => ({
745
+ requests: createRequestAuthentication({ service }),
746
+ resolveOrigin: (input) => resolveRequestOrigin({
747
+ ...input,
748
+ origins: origins(),
749
+ trustedProxyPeer
750
+ }),
751
+ service
752
+ });
753
+ const loginInputSchema = z.union([z.object({ code: z.string() }).strict(), z.object({ ticket: z.string() }).strict()]);
754
+ const parseLoginBody = async (request) => {
755
+ if (request.headers.get("Content-Type")?.split(";", 1)[0]?.toLowerCase() !== "application/json") return;
756
+ try {
757
+ return loginInputSchema.safeParse(await request.json()).data;
758
+ } catch {
759
+ return;
760
+ }
761
+ };
762
+ const hasTrustedBrowserHeaders = (request, origin) => request.headers.get("Origin") === origin && request.headers.get("Sec-Fetch-Site") === "same-origin";
763
+ const changesState = (request) => ![
764
+ "GET",
765
+ "HEAD",
766
+ "OPTIONS"
767
+ ].includes(request.method);
768
+ const requestOrigin = (auth, context) => auth.resolveOrigin({
769
+ remoteAddress: context.env?.incoming?.socket.remoteAddress,
770
+ request: context.req.raw
771
+ });
772
+ const createHttpAuthenticationHandlers = ({ auth, authAssetsDirectory }) => {
773
+ const authShell = loadAuthShell(authAssetsDirectory);
774
+ const loginRateLimiter = createRateLimiter({
775
+ maxAttemptsPerKey: maxLoginRequestsPerWindow,
776
+ maxGlobalAttempts: maxGlobalLoginRequestsPerWindow,
777
+ maxTrackedKeys: maxTrackedLoginAddresses,
778
+ windowMs: loginWindowMs
779
+ });
780
+ return {
781
+ asset: (context) => serveAuthShellAsset(context, authShell),
782
+ state: async (context) => {
783
+ context.header("Cache-Control", "no-store");
784
+ const origin = requestOrigin(auth, context);
785
+ const session = await auth.requests.authenticate(context.req.raw, origin);
786
+ if (session?.credential === "cookie" && origin) context.header("Set-Cookie", auth.requests.cookieHeader(origin, session.token, session.expiresAt));
787
+ return context.json({ authenticated: Boolean(session) });
788
+ },
789
+ loginBodyLimitMiddleware: bodyLimit({
790
+ maxSize: maxLoginBodyBytes,
791
+ onError: (context) => context.json({ error: "Login request is too large" }, 413)
792
+ }),
793
+ login: async (context) => {
794
+ context.header("Cache-Control", "no-store");
795
+ const origin = requestOrigin(auth, context);
796
+ if (!origin || !hasTrustedBrowserHeaders(context.req.raw, origin)) return context.json({ error: "Login request origin was rejected" }, 403);
797
+ if (!hasRateLimitCapacity({
798
+ key: context.env?.incoming?.socket.remoteAddress ?? "unknown",
799
+ limiter: loginRateLimiter
800
+ })) return context.json({ status: "rate-limited" }, 429);
801
+ const input = await parseLoginBody(context.req.raw);
802
+ if (!input) return context.json({ error: "Invalid login request" }, 400);
803
+ const result = await auth.service.consumeLoginGrant({
804
+ ...input,
805
+ origin
806
+ });
807
+ if (result.status !== "authenticated") return context.json({ status: result.status }, result.status === "rate-limited" ? 429 : 401);
808
+ context.header("Set-Cookie", auth.requests.cookieHeader(origin, result.token, result.session.expiresAt));
809
+ return context.json({ authenticated: true });
810
+ },
811
+ loginPage: async (context) => {
812
+ const origin = requestOrigin(auth, context);
813
+ if (await auth.requests.authenticate(context.req.raw, origin)) return context.redirect("/");
814
+ return renderAuthShell(context, authShell);
815
+ },
816
+ authenticate: async (context, next) => {
817
+ const origin = requestOrigin(auth, context);
818
+ let session;
819
+ try {
820
+ session = await auth.requests.authenticate(context.req.raw, origin);
821
+ } catch {
822
+ context.header("Cache-Control", "no-store");
823
+ return context.json({ error: "Authentication service unavailable" }, 503);
824
+ }
825
+ if (!session) {
826
+ if (!context.req.path.startsWith("/api/") && isBrowserNavigation(context.req.raw)) return renderAuthShell(context, authShell, 401);
827
+ context.header("Cache-Control", "no-store");
828
+ return context.json({ error: "Authentication required" }, 401);
829
+ }
830
+ context.set("session", session);
831
+ context.header("Cache-Control", "private, no-store");
832
+ if (session.credential === "cookie") {
833
+ if (!origin || changesState(context.req.raw) && !hasTrustedBrowserHeaders(context.req.raw, origin)) return context.json({ error: "Request origin was rejected" }, 403);
834
+ context.header("Set-Cookie", auth.requests.cookieHeader(origin, session.token, session.expiresAt));
835
+ }
836
+ await next();
837
+ },
838
+ logout: async (context) => {
839
+ const session = context.get("session");
840
+ await auth.service.revokeSession(session.id);
841
+ if (session.origin) context.header("Set-Cookie", auth.requests.clearCookieHeader(session.origin));
842
+ return context.json({ authenticated: false });
843
+ }
844
+ };
845
+ };
846
+ //#endregion
847
+ //#region src/internal/server/auth/instance-control.ts
848
+ const maxControlMessageBytes = 16384;
849
+ const controlRequestSchema = z.discriminatedUnion("type", [
850
+ z.object({ type: z.literal("ping") }).strict(),
851
+ z.object({ type: z.literal("create-login") }).strict(),
852
+ z.object({ type: z.literal("issue-bearer") }).strict(),
853
+ z.object({ type: z.literal("list-sessions") }).strict(),
854
+ z.object({
855
+ id: z.string(),
856
+ type: z.literal("revoke-session")
857
+ }).strict(),
858
+ z.object({ type: z.literal("revoke-all") }).strict()
859
+ ]);
860
+ const loginGrantSchema = z.object({
861
+ code: z.string(),
862
+ expiresAt: z.string(),
863
+ id: z.string(),
864
+ urls: z.array(z.string())
865
+ }).strict();
866
+ const sessionSchema = z.object({
867
+ createdAt: z.string(),
868
+ expiresAt: z.string().optional(),
869
+ id: z.string(),
870
+ lastSeenAt: z.string(),
871
+ origin: z.string().optional(),
872
+ revokedAt: z.string().optional()
873
+ }).strict();
874
+ z.object({
875
+ login: loginGrantSchema,
876
+ ok: z.literal(true)
877
+ }).strict(), z.object({
878
+ bearer: z.object({
879
+ expiresAt: z.string(),
880
+ token: z.string()
881
+ }).strict(),
882
+ ok: z.literal(true)
883
+ }).strict(), z.object({
884
+ ok: z.literal(true),
885
+ sessions: z.array(sessionSchema)
886
+ }).strict(), z.object({ ok: z.literal(true) }).strict(), z.object({
887
+ count: z.number().int().nonnegative(),
888
+ ok: z.literal(true)
889
+ }).strict(), z.object({
890
+ ok: z.literal(true),
891
+ revoked: z.boolean()
892
+ }).strict();
893
+ z.object({
894
+ error: z.unknown().optional(),
895
+ ok: z.unknown()
896
+ }).passthrough();
897
+ z.object({
898
+ apiUrl: z.string().url(),
899
+ controlSocket: z.string(),
900
+ id: z.string(),
901
+ pid: z.number().int().positive(),
902
+ port: z.number().int().min(1).max(65535),
903
+ url: z.string().url()
904
+ });
905
+ const runtimeDirectory = () => {
906
+ const paths = getOvermuxPaths();
907
+ const preferred = paths.runtimeDir ?? join(paths.stateDir, "runtime");
908
+ if (Buffer.byteLength(preferred) <= 80) return preferred;
909
+ const suffix = createHash("sha256").update(preferred).digest("hex").slice(0, 12);
910
+ return join(tmpdir(), `overmux-${process.getuid?.() ?? "user"}-${suffix}`);
911
+ };
912
+ const readNewlineFrame = ({ maximumBytes, socket, timeout }) => new Promise((resolve, reject) => {
913
+ let buffer = Buffer.alloc(0);
914
+ let timer;
915
+ const cleanup = () => {
916
+ socket.off("data", onData);
917
+ socket.off("end", onEnd);
918
+ socket.off("error", onError);
919
+ if (timer) clearTimeout(timer);
920
+ };
921
+ const fail = (cause) => {
922
+ cleanup();
923
+ reject(cause);
924
+ };
925
+ const onData = (chunk) => {
926
+ buffer = Buffer.concat([buffer, chunk]);
927
+ if (buffer.length > maximumBytes) {
928
+ fail(/* @__PURE__ */ new Error("Control message is too large"));
929
+ return;
930
+ }
931
+ const newline = buffer.indexOf(10);
932
+ if (newline < 0) return;
933
+ cleanup();
934
+ resolve(buffer.subarray(0, newline));
935
+ };
936
+ const onEnd = () => fail(/* @__PURE__ */ new Error("Control socket ended before a complete frame"));
937
+ const onError = (cause) => fail(cause);
938
+ if (socket.errored) fail(socket.errored);
939
+ else if (socket.readableEnded || socket.destroyed) fail(/* @__PURE__ */ new Error("Control socket is already closed"));
940
+ else {
941
+ socket.on("data", onData);
942
+ socket.once("end", onEnd);
943
+ socket.once("error", onError);
944
+ if (timeout !== void 0) timer = setTimeout(() => {
945
+ socket.destroy();
946
+ fail(/* @__PURE__ */ new Error("Control request timed out"));
947
+ }, timeout);
948
+ }
949
+ });
950
+ const writeResponse = (socket, response) => {
951
+ socket.end(`${JSON.stringify(response)}\n`);
952
+ };
953
+ const handleRequest = async (auth, request) => {
954
+ if (request.type === "ping") return { ok: true };
955
+ if (request.type === "create-login") return {
956
+ login: await auth.createLoginGrant(),
957
+ ok: true
958
+ };
959
+ if (request.type === "issue-bearer") return {
960
+ bearer: await auth.issueLocalBearer(),
961
+ ok: true
962
+ };
963
+ if (request.type === "list-sessions") return {
964
+ ok: true,
965
+ sessions: await auth.listSessions()
966
+ };
967
+ if (request.type === "revoke-session") return {
968
+ ok: true,
969
+ revoked: await auth.revokeSession(request.id)
970
+ };
971
+ return {
972
+ count: await auth.revokeAllSessions(),
973
+ ok: true
974
+ };
975
+ };
976
+ const handleConnection = async ({ auth, socket }) => {
977
+ try {
978
+ const frame = await readNewlineFrame({
979
+ maximumBytes: maxControlMessageBytes,
980
+ socket
981
+ });
982
+ const request = controlRequestSchema.parse(JSON.parse(frame.toString()));
983
+ writeResponse(socket, await handleRequest(auth, request));
984
+ } catch (cause) {
985
+ writeResponse(socket, {
986
+ error: cause instanceof Error ? cause.message : "Invalid control request",
987
+ ok: false
988
+ });
989
+ }
990
+ };
991
+ const startInstanceControl = async ({ auth, apiUrl, port, url }) => {
992
+ const directory = runtimeDirectory();
993
+ await mkdir(directory, {
994
+ mode: 448,
995
+ recursive: true
996
+ });
997
+ await chmod(directory, 448);
998
+ const id = randomBytes(8).toString("hex");
999
+ const socketPath = join(directory, `${id}.sock`);
1000
+ const registrationPath = join(directory, `${id}.json`);
1001
+ const sockets = /* @__PURE__ */ new Set();
1002
+ const server = createServer((socket) => {
1003
+ sockets.add(socket);
1004
+ socket.once("close", () => sockets.delete(socket));
1005
+ handleConnection({
1006
+ auth,
1007
+ socket
1008
+ });
1009
+ });
1010
+ const close = async () => {
1011
+ sockets.forEach((socket) => socket.destroy());
1012
+ try {
1013
+ await new Promise((resolve, reject) => server.close((cause) => cause ? reject(cause) : resolve()));
1014
+ } finally {
1015
+ await Promise.all([rm(socketPath, { force: true }), rm(registrationPath, { force: true })]);
1016
+ }
1017
+ };
1018
+ await new Promise((resolve, reject) => {
1019
+ server.once("error", reject);
1020
+ server.listen(socketPath, () => {
1021
+ server.off("error", reject);
1022
+ resolve();
1023
+ });
1024
+ });
1025
+ try {
1026
+ await chmod(socketPath, 384);
1027
+ await replacePrivateJsonAtomically(registrationPath, {
1028
+ apiUrl,
1029
+ controlSocket: socketPath,
1030
+ id,
1031
+ pid: process.pid,
1032
+ port,
1033
+ url
1034
+ });
1035
+ } catch (cause) {
1036
+ await close().catch(() => void 0);
1037
+ throw cause;
1038
+ }
1039
+ return { close };
1040
+ };
1041
+ //#endregion
1042
+ //#region src/internal/shared/background-notifications.ts
1043
+ const deliveryKeySchema = z.string().min(1).max(1024).regex(/^[\w-]+$/u);
1044
+ const backgroundNotificationSubscriptionSchema = z.object({
1045
+ endpoint: z.url().max(4096).refine((endpoint) => {
1046
+ const url = new URL(endpoint);
1047
+ return url.protocol === "https:" && !url.username && !url.password;
1048
+ }),
1049
+ expirationTime: z.number().int().nonnegative().nullable(),
1050
+ keys: z.object({
1051
+ auth: deliveryKeySchema,
1052
+ p256dh: deliveryKeySchema
1053
+ }).strict()
1054
+ }).strict();
1055
+ z.object({ publicKey: deliveryKeySchema }).strict();
1056
+ const backgroundNotificationDisableRequestSchema = z.object({ endpoint: backgroundNotificationSubscriptionSchema.shape.endpoint }).strict();
1057
+ const overmuxSettingsPath = "/_overmux/settings";
1058
+ const overmuxLogoutPath = "/_overmux/logout";
1059
+ const safeRelativeNotificationLink = (link) => {
1060
+ let decoded;
1061
+ try {
1062
+ decoded = decodeURIComponent(link);
1063
+ } catch {
1064
+ return false;
1065
+ }
1066
+ return !decoded.startsWith("//") && !decoded.includes("\\") && ![...decoded].some((character) => {
1067
+ const codePoint = character.codePointAt(0) ?? 0;
1068
+ return codePoint < 32 || codePoint === 127;
1069
+ });
1070
+ };
1071
+ const notificationLinkSchema = z.union([z.string().max(4096).startsWith("/").refine(safeRelativeNotificationLink), z.string().max(4096).url().refine((link) => {
1072
+ if (!URL.canParse(link)) return false;
1073
+ const url = new URL(link);
1074
+ return ["http:", "https:"].includes(url.protocol) && !url.username && !url.password;
1075
+ })]);
1076
+ const notificationSchema = z.object({
1077
+ body: z.string().max(4096).optional(),
1078
+ open: z.object({ link: notificationLinkSchema }).strict().optional(),
1079
+ title: z.string().min(1).max(4096)
1080
+ }).strict();
1081
+ const webSocketCloseCode = {
1082
+ authenticationRevoked: 4001,
1083
+ authenticationUnavailable: 4002
1084
+ };
1085
+ z.object({
1086
+ debug: z.boolean(),
1087
+ operations: z.array(z.string().min(1)),
1088
+ protocolVersion: z.literal(9),
1089
+ resources: z.array(z.string().min(1)),
1090
+ streams: z.array(z.string().min(1))
1091
+ });
1092
+ const updateAvailableEventSchema = z.object({ type: z.literal("update-available") });
1093
+ const restartingEventSchema = z.object({ type: z.literal("restarting") });
1094
+ z.discriminatedUnion("type", [updateAvailableEventSchema, restartingEventSchema]);
1095
+ const notificationEventSchema = z.object({
1096
+ notification: notificationSchema,
1097
+ type: z.literal("notification")
1098
+ }).strict();
1099
+ const operationIdSchema = z.string().min(1).max(256);
1100
+ const registrationNameSchema = z.string().min(1).max(256);
1101
+ const resourceReadMessageSchema = z.object({
1102
+ input: z.unknown().optional(),
1103
+ operationId: operationIdSchema,
1104
+ resourceName: registrationNameSchema,
1105
+ type: z.literal("resource-read")
1106
+ });
1107
+ const resourceSubscribeMessageSchema = z.object({
1108
+ input: z.unknown().optional(),
1109
+ operationId: operationIdSchema,
1110
+ resourceName: registrationNameSchema,
1111
+ subscriptionId: operationIdSchema,
1112
+ type: z.literal("resource-subscribe")
1113
+ });
1114
+ const resourceUnsubscribeMessageSchema = z.object({
1115
+ subscriptionId: operationIdSchema,
1116
+ type: z.literal("resource-unsubscribe")
1117
+ });
1118
+ const streamOpenMessageSchema = z.object({
1119
+ input: z.unknown().optional(),
1120
+ operationId: operationIdSchema,
1121
+ streamId: operationIdSchema,
1122
+ streamName: registrationNameSchema,
1123
+ type: z.literal("stream-open")
1124
+ });
1125
+ const streamMessageSchema = z.object({
1126
+ message: z.unknown(),
1127
+ streamId: operationIdSchema,
1128
+ type: z.literal("stream-message")
1129
+ });
1130
+ const streamCloseMessageSchema = z.object({
1131
+ streamId: operationIdSchema,
1132
+ type: z.literal("stream-close")
1133
+ });
1134
+ const clientDiagnosticEventSchema = z.object({
1135
+ arguments: z.string().max(16100),
1136
+ level: z.enum([
1137
+ "log",
1138
+ "info",
1139
+ "warn",
1140
+ "error",
1141
+ "debug"
1142
+ ]),
1143
+ message: z.string().max(4100),
1144
+ stack: z.string().max(8100).optional(),
1145
+ timestamp: z.iso.datetime(),
1146
+ type: z.literal("client-diagnostic"),
1147
+ url: z.string().max(2100).optional()
1148
+ });
1149
+ const clientProtocolMessageSchema = z.discriminatedUnion("type", [
1150
+ resourceReadMessageSchema,
1151
+ resourceSubscribeMessageSchema,
1152
+ resourceUnsubscribeMessageSchema,
1153
+ streamOpenMessageSchema,
1154
+ streamMessageSchema,
1155
+ streamCloseMessageSchema,
1156
+ clientDiagnosticEventSchema
1157
+ ]);
1158
+ const resourceResultMessageSchema = z.object({
1159
+ operationId: operationIdSchema,
1160
+ output: z.unknown(),
1161
+ type: z.literal("resource-result")
1162
+ });
1163
+ const resourceInvalidatedMessageSchema = z.object({
1164
+ subscriptionId: operationIdSchema,
1165
+ type: z.literal("resource-invalidated")
1166
+ });
1167
+ const streamOpenedMessageSchema = z.object({
1168
+ operationId: operationIdSchema,
1169
+ streamId: operationIdSchema,
1170
+ type: z.literal("stream-opened")
1171
+ });
1172
+ const streamOutputMessageSchema = z.object({
1173
+ message: z.unknown(),
1174
+ streamId: operationIdSchema,
1175
+ type: z.literal("stream-output")
1176
+ });
1177
+ const streamClosedMessageSchema = z.object({
1178
+ streamId: operationIdSchema,
1179
+ type: z.literal("stream-closed")
1180
+ });
1181
+ const protocolErrorMessageSchema = z.object({
1182
+ code: z.enum([
1183
+ "bad-request",
1184
+ "conflict",
1185
+ "internal",
1186
+ "not-found"
1187
+ ]),
1188
+ message: z.string(),
1189
+ operationId: operationIdSchema.optional(),
1190
+ streamId: operationIdSchema.optional(),
1191
+ subscriptionId: operationIdSchema.optional(),
1192
+ type: z.literal("error")
1193
+ });
1194
+ const serverProtocolMessageSchema = z.discriminatedUnion("type", [
1195
+ resourceResultMessageSchema,
1196
+ resourceInvalidatedMessageSchema,
1197
+ streamOpenedMessageSchema,
1198
+ streamOutputMessageSchema,
1199
+ streamClosedMessageSchema,
1200
+ protocolErrorMessageSchema,
1201
+ updateAvailableEventSchema,
1202
+ restartingEventSchema,
1203
+ notificationEventSchema
1204
+ ]);
1205
+ const byteMarker = "$overmuxBytes";
1206
+ const decodeValue = (value) => {
1207
+ if (Array.isArray(value)) return value.map(decodeValue);
1208
+ if (typeof value !== "object" || value === null) return value;
1209
+ const record = value;
1210
+ if (Object.keys(record).length === 1 && Array.isArray(record[byteMarker]) && record[byteMarker].every((item) => Number.isInteger(item) && item >= 0 && item <= 255)) return Uint8Array.from(record[byteMarker]);
1211
+ return Object.fromEntries(Object.entries(record).map(([key, item]) => [key, decodeValue(item)]));
1212
+ };
1213
+ const binaryMarker = "$overmuxBinary";
1214
+ const binaryMagic = 1330468865;
1215
+ const encodeBinaryValue = (value, payloads, offset) => {
1216
+ if (value instanceof Uint8Array) {
1217
+ const marker = { [binaryMarker]: [offset.value, value.byteLength] };
1218
+ payloads.push(value);
1219
+ offset.value += value.byteLength;
1220
+ return marker;
1221
+ }
1222
+ if (Array.isArray(value)) return value.map((item) => encodeBinaryValue(item, payloads, offset));
1223
+ if (typeof value === "object" && value !== null) return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, encodeBinaryValue(item, payloads, offset)]));
1224
+ return value;
1225
+ };
1226
+ const decodeBinaryValue = (value, payload) => {
1227
+ if (Array.isArray(value)) return value.map((item) => decodeBinaryValue(item, payload));
1228
+ if (typeof value !== "object" || value === null) return value;
1229
+ const record = value;
1230
+ const marker = record[binaryMarker];
1231
+ if (Object.keys(record).length === 1 && Array.isArray(marker) && marker.length === 2 && marker.every((item) => Number.isInteger(item) && item >= 0)) {
1232
+ const [offset, length] = marker;
1233
+ if (offset + length > payload.byteLength) throw new Error("Invalid Overmux binary payload range");
1234
+ return payload.slice(offset, offset + length);
1235
+ }
1236
+ return Object.fromEntries(Object.entries(record).map(([key, item]) => [key, decodeBinaryValue(item, payload)]));
1237
+ };
1238
+ const decodeProtocolMessage = (message) => decodeValue(JSON.parse(message));
1239
+ const encodeProtocolFrame = (message) => {
1240
+ const payloads = [];
1241
+ const encoded = encodeBinaryValue(message, payloads, { value: 0 });
1242
+ if (!payloads.length) return JSON.stringify(encoded);
1243
+ const header = new TextEncoder().encode(JSON.stringify(encoded));
1244
+ const payloadLength = payloads.reduce((total, payload) => total + payload.byteLength, 0);
1245
+ const frame = new Uint8Array(8 + header.byteLength + payloadLength);
1246
+ const view = new DataView(frame.buffer);
1247
+ view.setUint32(0, binaryMagic);
1248
+ view.setUint32(4, header.byteLength);
1249
+ frame.set(header, 8);
1250
+ let offset = 8 + header.byteLength;
1251
+ payloads.forEach((payload) => {
1252
+ frame.set(payload, offset);
1253
+ offset += payload.byteLength;
1254
+ });
1255
+ return frame;
1256
+ };
1257
+ const decodeProtocolFrame = (frame) => {
1258
+ if (typeof frame === "string") return decodeProtocolMessage(frame);
1259
+ const bytes = frame instanceof Uint8Array ? frame : new Uint8Array(frame, 0, frame.byteLength);
1260
+ if (bytes.byteLength < 8) throw new Error("Invalid Overmux binary frame");
1261
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
1262
+ if (view.getUint32(0) !== binaryMagic) throw new Error("Invalid Overmux binary frame magic");
1263
+ const headerLength = view.getUint32(4);
1264
+ if (8 + headerLength > bytes.byteLength) throw new Error("Invalid Overmux binary frame header");
1265
+ const header = JSON.parse(new TextDecoder().decode(bytes.subarray(8, 8 + headerLength)));
1266
+ return decodeBinaryValue(header, bytes.subarray(8 + headerLength));
1267
+ };
1268
+ //#endregion
1269
+ //#region src/internal/server/http/background-notification-handlers.ts
1270
+ const requestLimit = 16384;
1271
+ const parseJson = async (request, schema) => {
1272
+ if (request.headers.get("Content-Type")?.split(";", 1)[0]?.toLowerCase() !== "application/json") return;
1273
+ if (Number(request.headers.get("Content-Length") ?? 0) > requestLimit) return;
1274
+ try {
1275
+ const bytes = await request.arrayBuffer();
1276
+ if (bytes.byteLength > requestLimit) return;
1277
+ const parsed = schema.safeParse(JSON.parse(new TextDecoder().decode(bytes)));
1278
+ return parsed.success ? parsed.data : void 0;
1279
+ } catch {
1280
+ return;
1281
+ }
1282
+ };
1283
+ const createBackgroundNotificationHandlers = ({ backgroundNotifications }) => ({
1284
+ publicKey: async (context) => {
1285
+ context.header("Cache-Control", "no-store");
1286
+ return context.json({ publicKey: await backgroundNotifications.publicKey() });
1287
+ },
1288
+ enable: async (context) => {
1289
+ const subscription = await parseJson(context.req.raw, backgroundNotificationSubscriptionSchema);
1290
+ if (!subscription) return context.json({ error: "Invalid background subscription" }, 400);
1291
+ await backgroundNotifications.enable(context.get("session").id, subscription);
1292
+ return context.json({ ok: true });
1293
+ },
1294
+ disable: async (context) => {
1295
+ const input = await parseJson(context.req.raw, backgroundNotificationDisableRequestSchema);
1296
+ if (!input) return context.json({ error: "Invalid background subscription" }, 400);
1297
+ await backgroundNotifications.disable(input.endpoint);
1298
+ return context.json({ ok: true });
1299
+ }
1300
+ });
1301
+ //#endregion
1302
+ //#region src/internal/server/server-logger.ts
1303
+ const lifecycleEvents = /* @__PURE__ */ new Set([
1304
+ "server-start",
1305
+ "server-stop",
1306
+ "websocket-close",
1307
+ "websocket-close-event",
1308
+ "websocket-connect",
1309
+ "websocket-open"
1310
+ ]);
1311
+ const getDefaultServerLogFile = ({ homeDir, xdgStateHome } = {}) => join(getOvermuxPaths({
1312
+ homeDir,
1313
+ xdgStateHome
1314
+ }).stateDir, "overmux.log");
1315
+ const safeJson = (value) => {
1316
+ try {
1317
+ return JSON.stringify(value);
1318
+ } catch {
1319
+ return JSON.stringify({
1320
+ event: "log-serialization-error",
1321
+ level: "error",
1322
+ source: "server",
1323
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
1324
+ });
1325
+ }
1326
+ };
1327
+ const createServerLogger = ({ enabled, logFile = getDefaultServerLogFile() }) => {
1328
+ let activeLogFile = logFile;
1329
+ let debugEnabled = enabled;
1330
+ return {
1331
+ get enabled() {
1332
+ return debugEnabled;
1333
+ },
1334
+ id: randomUUID,
1335
+ log: (entry) => {
1336
+ if (!debugEnabled && entry.level !== "error" && !lifecycleEvents.has(entry.event)) return;
1337
+ const record = safeJson({
1338
+ timestamp: entry.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
1339
+ level: entry.level ?? "info",
1340
+ source: entry.source ?? "server",
1341
+ ...entry
1342
+ });
1343
+ mkdirSync(dirname(activeLogFile), { recursive: true });
1344
+ appendFileSync(activeLogFile, `${record}\n`, "utf8");
1345
+ process.stderr.write(`[overmux:debug] ${record}\n`);
1346
+ },
1347
+ setEnabled: (nextEnabled) => {
1348
+ debugEnabled = nextEnabled;
1349
+ },
1350
+ setLogFile: (nextLogFile) => {
1351
+ activeLogFile = nextLogFile;
1352
+ }
1353
+ };
1354
+ };
1355
+ const errorMessage = (cause) => cause instanceof Error ? cause.message : String(cause);
1356
+ const errorDetails$1 = (cause) => ({
1357
+ message: errorMessage(cause),
1358
+ ...cause instanceof Error && cause.stack ? { stack: cause.stack } : {}
1359
+ });
1360
+ //#endregion
1361
+ //#region src/internal/server/http/development-web-backend.ts
1362
+ const createForwardingHeaders = ({ origin, remoteAddress, request }) => {
1363
+ const headers = new Headers(request.headers);
1364
+ [...headers.keys()].forEach((name) => {
1365
+ if (name === "authorization" || name === "cookie" || name === "forwarded" || name.startsWith("x-forwarded-")) headers.delete(name);
1366
+ });
1367
+ const publicUrl = new URL(origin);
1368
+ if (remoteAddress) headers.set("x-forwarded-for", remoteAddress);
1369
+ headers.set("x-forwarded-host", publicUrl.host);
1370
+ headers.set("x-forwarded-port", publicUrl.port || (publicUrl.protocol === "https:" ? "443" : "80"));
1371
+ headers.set("x-forwarded-proto", publicUrl.protocol.slice(0, -1));
1372
+ return headers;
1373
+ };
1374
+ const createDevelopmentWebHandler = ({ auth, serverLogger, target }) => async (context) => {
1375
+ const incoming = context.env?.incoming;
1376
+ const origin = auth.resolveOrigin({
1377
+ remoteAddress: incoming?.socket.remoteAddress,
1378
+ request: context.req.raw
1379
+ });
1380
+ if (!origin) return context.text("Request origin was rejected", 403);
1381
+ const headers = createForwardingHeaders({
1382
+ origin,
1383
+ remoteAddress: incoming?.socket.remoteAddress,
1384
+ request: context.req.raw
1385
+ });
1386
+ try {
1387
+ return await proxyFetch(target, new Request(context.req.raw, { headers }), void 0, {
1388
+ changeOrigin: true,
1389
+ xfwd: false
1390
+ });
1391
+ } catch (cause) {
1392
+ serverLogger?.log({
1393
+ details: errorDetails$1(cause),
1394
+ event: "vite-http-proxy-failure",
1395
+ level: "error"
1396
+ });
1397
+ return context.text("Overmux development server is unavailable", 503, { "Cache-Control": "no-store" });
1398
+ }
1399
+ };
1400
+ //#endregion
1401
+ //#region src/internal/server/http/control-handlers.ts
1402
+ const createHealthHandler = ({ shutdown }) => (context) => shutdown.requested ? context.json({ status: "stopping" }, 503) : context.json({ status: "ok" });
1403
+ const createRestartHandler = ({ onRestartRequested, shutdown }) => (context) => {
1404
+ const response = context.json({ status: "restarting" }, 202);
1405
+ if (!onRestartRequested) return response;
1406
+ const beginRestart = () => {
1407
+ shutdown.requested = true;
1408
+ onRestartRequested();
1409
+ };
1410
+ const nodeResponse = context.env?.outgoing;
1411
+ if (nodeResponse) nodeResponse.once("finish", beginRestart);
1412
+ else setImmediate(beginRestart);
1413
+ return response;
1414
+ };
1415
+ //#endregion
1416
+ //#region src/internal/server/runtime/runtime-operations.ts
1417
+ var OperationValidationError = class extends Error {
1418
+ phase;
1419
+ constructor(phase, cause) {
1420
+ super(cause.message, { cause });
1421
+ this.phase = phase;
1422
+ }
1423
+ };
1424
+ const createRuntimeOperations = ({ context, definitions, notifications }) => {
1425
+ const operations = /* @__PURE__ */ new Map();
1426
+ Object.entries(definitions).forEach(([name, definition]) => {
1427
+ if (!name) throw new Error("Operation names must not be empty");
1428
+ operations.set(name, {
1429
+ execute: async (rawInput, signal) => {
1430
+ const operationContext = context(signal);
1431
+ operationContext.signal.throwIfAborted();
1432
+ const input = definition.input.safeParse(rawInput);
1433
+ if (!input.success) throw new OperationValidationError("input", input.error);
1434
+ const rawOutput = await definition.handle(input.data, {
1435
+ ...operationContext,
1436
+ notifications
1437
+ });
1438
+ operationContext.signal.throwIfAborted();
1439
+ const output = definition.output.safeParse(rawOutput);
1440
+ if (!output.success) throw new OperationValidationError("output", output.error);
1441
+ return output.data;
1442
+ },
1443
+ returnsVoid: definition.output instanceof z.ZodVoid
1444
+ });
1445
+ });
1446
+ return {
1447
+ get: (name) => operations.get(name),
1448
+ names: [...operations.keys()]
1449
+ };
1450
+ };
1451
+ //#endregion
1452
+ //#region src/internal/server/http/operation-handler.ts
1453
+ const maxOperationRequestBytes = 1048576;
1454
+ const parseOperationRequest = async (request) => {
1455
+ if ((request.headers.get("Content-Type")?.split(";", 1)[0])?.toLowerCase() !== "application/json") return {
1456
+ error: "Content-Type must be application/json",
1457
+ status: 415
1458
+ };
1459
+ if (Number(request.headers.get("Content-Length") ?? 0) > maxOperationRequestBytes) return {
1460
+ error: "Request body is too large",
1461
+ status: 413
1462
+ };
1463
+ try {
1464
+ const body = await request.arrayBuffer();
1465
+ if (body.byteLength > maxOperationRequestBytes) return {
1466
+ error: "Request body is too large",
1467
+ status: 413
1468
+ };
1469
+ const text = new TextDecoder().decode(body);
1470
+ return { input: text ? JSON.parse(text) : void 0 };
1471
+ } catch {
1472
+ return {
1473
+ error: "Invalid JSON request body",
1474
+ status: 400
1475
+ };
1476
+ }
1477
+ };
1478
+ const createOperationHandler = ({ runtime, serverLogger }) => async (context) => {
1479
+ const operation = runtime.getOperation(context.req.param("name"));
1480
+ if (!operation) return context.json({ error: "Operation not found" }, 404);
1481
+ const parsed = await parseOperationRequest(context.req.raw);
1482
+ if ("error" in parsed) return context.json({ error: parsed.error }, parsed.status);
1483
+ const correlationId = context.res.headers.get("X-Overmux-Correlation-Id") ?? serverLogger?.id();
1484
+ const startedAt = Date.now();
1485
+ serverLogger?.log({
1486
+ correlationId,
1487
+ details: { registeredName: context.req.param("name") },
1488
+ event: "operation-start"
1489
+ });
1490
+ try {
1491
+ const output = await operation.execute(parsed.input, context.req.raw.signal);
1492
+ serverLogger?.log({
1493
+ correlationId,
1494
+ details: { registeredName: context.req.param("name") },
1495
+ durationMs: Date.now() - startedAt,
1496
+ event: "operation-complete"
1497
+ });
1498
+ return operation.returnsVoid ? new Response(void 0, { status: 204 }) : context.json(output);
1499
+ } catch (cause) {
1500
+ const message = cause instanceof Error ? cause.message : "Operation failed";
1501
+ serverLogger?.log({
1502
+ correlationId,
1503
+ details: { registeredName: context.req.param("name") },
1504
+ durationMs: Date.now() - startedAt,
1505
+ event: "operation-error",
1506
+ level: "error",
1507
+ message
1508
+ });
1509
+ return context.json({ error: message }, cause instanceof OperationValidationError && cause.phase === "input" ? 400 : 500);
1510
+ }
1511
+ };
1512
+ //#endregion
1513
+ //#region src/internal/server/http/request-logging.ts
1514
+ const createRequestLoggingMiddleware = ({ serverLogger }) => async (context, next) => {
1515
+ const correlationId = serverLogger?.id();
1516
+ const startedAt = Date.now();
1517
+ if (correlationId) context.header("X-Overmux-Correlation-Id", correlationId);
1518
+ await next();
1519
+ const failed = context.res.status >= 400 || Boolean(context.error);
1520
+ serverLogger?.log({
1521
+ correlationId,
1522
+ details: {
1523
+ method: context.req.method,
1524
+ path: context.req.path,
1525
+ status: context.res.status
1526
+ },
1527
+ durationMs: Date.now() - startedAt,
1528
+ event: failed ? "http-request-failure" : "http-request",
1529
+ level: failed ? "error" : void 0,
1530
+ message: context.error?.message
1531
+ });
1532
+ };
1533
+ //#endregion
1534
+ //#region src/internal/server/http/runtime-manifest-handler.ts
1535
+ const createRuntimeManifestHandler = ({ runtime }) => (context) => {
1536
+ context.header("Cache-Control", "no-store");
1537
+ return context.json(runtime.manifest);
1538
+ };
1539
+ //#endregion
1540
+ //#region src/internal/shared/routes.ts
1541
+ const runtimeWebSocketPath = "/api/socket";
1542
+ const viteInfrastructurePath = "/_overmux/vite/*";
1543
+ //#endregion
1544
+ //#region src/internal/server/http/routes.ts
1545
+ const applyDescriptor = (app, route) => {
1546
+ if (route.method === "USE") {
1547
+ app.use(route.path, route.handler);
1548
+ return;
1549
+ }
1550
+ if (route.middleware) {
1551
+ app.on(route.method, route.path, route.middleware, route.handler);
1552
+ return;
1553
+ }
1554
+ app.on(route.method, route.path, route.handler);
1555
+ };
1556
+ const registerHttpRoutes = ({ app, auth, authAssetsDirectory, backgroundNotifications, developmentWebTarget, onRestartRequested, runtime, serverLogger, shutdown, webAssetsDir }) => {
1557
+ const authentication = createHttpAuthenticationHandlers({
1558
+ auth,
1559
+ authAssetsDirectory
1560
+ });
1561
+ const notificationHandlers = backgroundNotifications ? createBackgroundNotificationHandlers({ backgroundNotifications }) : void 0;
1562
+ const developmentWebHandler = developmentWebTarget ? createDevelopmentWebHandler({
1563
+ auth,
1564
+ serverLogger,
1565
+ target: developmentWebTarget
1566
+ }) : void 0;
1567
+ const productionWebHandler = webAssetsDir ? serveStatic({ path: join(webAssetsDir, "index.html") }) : void 0;
1568
+ const hostedWebHandler = developmentWebHandler ?? productionWebHandler;
1569
+ [
1570
+ {
1571
+ method: "USE",
1572
+ path: "*",
1573
+ handler: createRequestLoggingMiddleware({ serverLogger })
1574
+ },
1575
+ {
1576
+ method: "GET",
1577
+ path: "/_overmux/auth-shell/*",
1578
+ handler: authentication.asset
1579
+ },
1580
+ {
1581
+ method: "GET",
1582
+ path: "/api/auth/state",
1583
+ handler: authentication.state
1584
+ },
1585
+ {
1586
+ method: "POST",
1587
+ path: "/api/auth/login",
1588
+ middleware: authentication.loginBodyLimitMiddleware,
1589
+ handler: authentication.login
1590
+ },
1591
+ {
1592
+ method: "GET",
1593
+ path: "/login",
1594
+ handler: authentication.loginPage
1595
+ },
1596
+ {
1597
+ method: "GET",
1598
+ path: "/api/health",
1599
+ handler: createHealthHandler({ shutdown })
1600
+ },
1601
+ {
1602
+ method: "USE",
1603
+ path: "*",
1604
+ handler: authentication.authenticate
1605
+ },
1606
+ {
1607
+ method: "POST",
1608
+ path: "/api/auth/logout",
1609
+ handler: authentication.logout
1610
+ },
1611
+ {
1612
+ method: "POST",
1613
+ path: "/api/operations/:name",
1614
+ handler: createOperationHandler({
1615
+ runtime,
1616
+ serverLogger
1617
+ })
1618
+ },
1619
+ {
1620
+ method: "POST",
1621
+ path: "/api/restart",
1622
+ handler: createRestartHandler({
1623
+ onRestartRequested,
1624
+ shutdown
1625
+ })
1626
+ },
1627
+ {
1628
+ method: "GET",
1629
+ path: "/api/runtime-manifest",
1630
+ handler: createRuntimeManifestHandler({ runtime })
1631
+ },
1632
+ ...notificationHandlers ? [
1633
+ {
1634
+ method: "GET",
1635
+ path: "/api/background-notifications/public-key",
1636
+ handler: notificationHandlers.publicKey
1637
+ },
1638
+ {
1639
+ method: "PUT",
1640
+ path: "/api/background-notifications/subscription",
1641
+ handler: notificationHandlers.enable
1642
+ },
1643
+ {
1644
+ method: "DELETE",
1645
+ path: "/api/background-notifications/subscription",
1646
+ handler: notificationHandlers.disable
1647
+ }
1648
+ ] : [],
1649
+ {
1650
+ method: "ALL",
1651
+ path: "/api/*",
1652
+ handler: (context) => context.notFound()
1653
+ },
1654
+ ...developmentWebHandler ? [{
1655
+ method: "ALL",
1656
+ path: viteInfrastructurePath,
1657
+ handler: developmentWebHandler
1658
+ }] : [],
1659
+ ...hostedWebHandler ? [{
1660
+ method: "GET",
1661
+ path: overmuxSettingsPath,
1662
+ handler: hostedWebHandler
1663
+ }, {
1664
+ method: "GET",
1665
+ path: overmuxLogoutPath,
1666
+ handler: hostedWebHandler
1667
+ }] : [],
1668
+ {
1669
+ method: "ALL",
1670
+ path: "/_overmux/*",
1671
+ handler: (context) => context.notFound()
1672
+ },
1673
+ ...developmentWebHandler ? [{
1674
+ method: "ALL",
1675
+ path: "*",
1676
+ handler: developmentWebHandler
1677
+ }] : [],
1678
+ ...webAssetsDir ? [{
1679
+ method: "USE",
1680
+ path: "*",
1681
+ handler: serveStatic({ root: webAssetsDir })
1682
+ }, {
1683
+ method: "GET",
1684
+ path: "*",
1685
+ handler: serveStatic({ path: join(webAssetsDir, "index.html") })
1686
+ }] : []
1687
+ ].forEach((route) => applyDescriptor(app, route));
1688
+ };
1689
+ //#endregion
1690
+ //#region src/internal/server/http/create-http-app.ts
1691
+ const createHttpApp = ({ auth, authAssetsDirectory, backgroundNotifications, developmentWebTarget, onRestartRequested, serverLogger, runtime, webAssetsDir }) => {
1692
+ const app = new Hono();
1693
+ const shutdown = { requested: false };
1694
+ registerHttpRoutes({
1695
+ app,
1696
+ auth,
1697
+ authAssetsDirectory,
1698
+ backgroundNotifications,
1699
+ developmentWebTarget,
1700
+ onRestartRequested,
1701
+ runtime,
1702
+ serverLogger,
1703
+ shutdown,
1704
+ webAssetsDir
1705
+ });
1706
+ return Object.assign(app, { beginShutdown: () => {
1707
+ shutdown.requested = true;
1708
+ } });
1709
+ };
1710
+ //#endregion
1711
+ //#region src/internal/server/websocket-connection.ts
1712
+ const maxPendingStreamOutputBytes = 1048576;
1713
+ var OperationError = class extends Error {
1714
+ code;
1715
+ constructor(code, message) {
1716
+ super(message);
1717
+ this.code = code;
1718
+ }
1719
+ };
1720
+ const bytesOf = (data) => {
1721
+ if (Array.isArray(data)) return Buffer.concat(data);
1722
+ if (data instanceof ArrayBuffer) return new Uint8Array(data);
1723
+ return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
1724
+ };
1725
+ const textOf = (data) => Buffer.from(bytesOf(data)).toString("utf8");
1726
+ const frameLength = (message) => {
1727
+ const frame = encodeProtocolFrame(message);
1728
+ return typeof frame === "string" ? Buffer.byteLength(frame, "utf8") : frame.byteLength;
1729
+ };
1730
+ const operationIdOf = (value) => {
1731
+ if (typeof value !== "object" || value === null) return;
1732
+ const operationId = value.operationId;
1733
+ return typeof operationId === "string" && operationId.length <= 256 ? operationId : void 0;
1734
+ };
1735
+ const errorDetails = (cause) => {
1736
+ if (cause instanceof OperationError) return {
1737
+ code: cause.code,
1738
+ message: cause.message
1739
+ };
1740
+ if (cause instanceof ZodError || cause instanceof SyntaxError) return {
1741
+ code: "bad-request",
1742
+ message: cause instanceof ZodError ? cause.issues.map((issue) => `${issue.path.join(".") || "value"}: ${issue.message}`).join("; ") : cause.message
1743
+ };
1744
+ return {
1745
+ code: "internal",
1746
+ message: cause instanceof Error ? cause.message : String(cause)
1747
+ };
1748
+ };
1749
+ const send$1 = (state, message) => {
1750
+ if (state.closed || state.socket.readyState !== WebSocket.OPEN) return false;
1751
+ if (state.socket.bufferedAmount > 1048576) {
1752
+ state.socket.close(1013, "client could not keep up");
1753
+ return false;
1754
+ }
1755
+ state.socket.send(encodeProtocolFrame(serverProtocolMessageSchema.parse(message)));
1756
+ return true;
1757
+ };
1758
+ const sendError = (state, cause, operationId, identity) => {
1759
+ const error = errorDetails(cause);
1760
+ state.serverLogger?.log({
1761
+ correlationId: identity?.operationId ?? operationId ?? state.connectionId,
1762
+ details: {
1763
+ code: error.code,
1764
+ ...identity,
1765
+ ...errorDetails$1(cause)
1766
+ },
1767
+ event: "protocol-handler-error",
1768
+ level: "error"
1769
+ });
1770
+ send$1(state, {
1771
+ ...error,
1772
+ operationId,
1773
+ type: "error"
1774
+ });
1775
+ };
1776
+ const disposeSubscription = async (state, subscriptionId) => {
1777
+ const entry = state.subscriptions.get(subscriptionId);
1778
+ if (!entry) return;
1779
+ state.subscriptions.delete(subscriptionId);
1780
+ entry.controller.abort(/* @__PURE__ */ new Error("Resource subscription closed"));
1781
+ await entry.dispose?.();
1782
+ };
1783
+ const disposeStream = async (state, streamId, notify = true) => {
1784
+ const entry = state.streams.get(streamId);
1785
+ if (!entry) {
1786
+ if (notify) send$1(state, {
1787
+ streamId,
1788
+ type: "stream-closed"
1789
+ });
1790
+ return;
1791
+ }
1792
+ state.streams.delete(streamId);
1793
+ entry.controller.abort(/* @__PURE__ */ new Error("Stream closed"));
1794
+ await entry.session?.dispose();
1795
+ if (notify) send$1(state, {
1796
+ streamId,
1797
+ type: "stream-closed"
1798
+ });
1799
+ };
1800
+ const disposeConnectionOperations = async (state, notify) => {
1801
+ const operationControllers = [...state.operations.values()];
1802
+ state.operations.clear();
1803
+ operationControllers.forEach((controller) => controller.abort(/* @__PURE__ */ new Error("WebSocket connection closed")));
1804
+ const subscriptionIds = [...state.subscriptions.keys()];
1805
+ const streamIds = [...state.streams.keys()];
1806
+ await Promise.allSettled([...subscriptionIds.map((subscriptionId) => disposeSubscription(state, subscriptionId)), ...streamIds.map((streamId) => disposeStream(state, streamId, notify))]);
1807
+ };
1808
+ const runOperation = (state, identity, operation) => {
1809
+ if (state.operations.has(identity.operationId)) {
1810
+ sendError(state, new OperationError("conflict", "Operation ID is already active"), identity.operationId, identity);
1811
+ return;
1812
+ }
1813
+ const controller = new AbortController();
1814
+ const startedAt = Date.now();
1815
+ state.operations.set(identity.operationId, controller);
1816
+ state.serverLogger?.log({
1817
+ correlationId: identity.operationId,
1818
+ details: identity,
1819
+ event: "operation-start"
1820
+ });
1821
+ operation(controller.signal).then(() => {
1822
+ state.serverLogger?.log({
1823
+ correlationId: identity.operationId,
1824
+ details: identity,
1825
+ durationMs: Date.now() - startedAt,
1826
+ event: "operation-complete"
1827
+ });
1828
+ }).catch((cause) => {
1829
+ if (!controller.signal.aborted && !state.closed) {
1830
+ state.serverLogger?.log({
1831
+ correlationId: identity.operationId,
1832
+ details: {
1833
+ ...identity,
1834
+ ...errorDetails$1(cause)
1835
+ },
1836
+ durationMs: Date.now() - startedAt,
1837
+ event: "operation-error",
1838
+ level: "error"
1839
+ });
1840
+ sendError(state, cause, identity.operationId, identity);
1841
+ }
1842
+ }).finally(() => {
1843
+ if (state.operations.get(identity.operationId) === controller) state.operations.delete(identity.operationId);
1844
+ });
1845
+ };
1846
+ const readResource = (state, message) => {
1847
+ const resource = state.runtime.getResource(message.resourceName);
1848
+ const identity = {
1849
+ operationId: message.operationId,
1850
+ operationType: message.type,
1851
+ requestedName: message.resourceName
1852
+ };
1853
+ runOperation(state, identity, async (signal) => {
1854
+ if (!resource) throw new OperationError("not-found", "Resource not found");
1855
+ const output = await resource.read(message.input, signal);
1856
+ send$1(state, {
1857
+ operationId: message.operationId,
1858
+ output,
1859
+ type: "resource-result"
1860
+ });
1861
+ });
1862
+ };
1863
+ const subscribeResource = (state, message) => {
1864
+ const resource = state.runtime.getResource(message.resourceName);
1865
+ const identity = {
1866
+ operationId: message.operationId,
1867
+ operationType: message.type,
1868
+ requestedName: message.resourceName
1869
+ };
1870
+ if (state.subscriptions.has(message.subscriptionId)) {
1871
+ sendError(state, new OperationError("conflict", "Subscription ID is already active"), message.operationId, identity);
1872
+ return;
1873
+ }
1874
+ runOperation(state, identity, async (operationSignal) => {
1875
+ if (!resource) throw new OperationError("not-found", "Resource not found");
1876
+ const controller = new AbortController();
1877
+ const entry = { controller };
1878
+ state.subscriptions.set(message.subscriptionId, entry);
1879
+ const signal = AbortSignal.any([operationSignal, controller.signal]);
1880
+ try {
1881
+ entry.dispose = resource.subscribe(message.input, () => void send$1(state, {
1882
+ subscriptionId: message.subscriptionId,
1883
+ type: "resource-invalidated"
1884
+ }), signal);
1885
+ } catch (cause) {
1886
+ await disposeSubscription(state, message.subscriptionId);
1887
+ throw cause;
1888
+ }
1889
+ });
1890
+ };
1891
+ const openStream = (state, message) => {
1892
+ const stream = state.runtime.getStream(message.streamName);
1893
+ const identity = {
1894
+ operationId: message.operationId,
1895
+ operationType: message.type,
1896
+ requestedName: message.streamName
1897
+ };
1898
+ if (state.streams.has(message.streamId)) {
1899
+ sendError(state, new OperationError("conflict", "Stream ID is already active"), message.operationId, identity);
1900
+ return;
1901
+ }
1902
+ runOperation(state, identity, async (operationSignal) => {
1903
+ if (!stream) throw new OperationError("not-found", "Stream not found");
1904
+ const controller = new AbortController();
1905
+ const entry = {
1906
+ controller,
1907
+ identity
1908
+ };
1909
+ state.streams.set(message.streamId, entry);
1910
+ const pending = [];
1911
+ let pendingBytes = 0;
1912
+ let opened = false;
1913
+ try {
1914
+ entry.session = await stream.open(message.input, (output) => {
1915
+ if (opened) {
1916
+ if (!send$1(state, {
1917
+ message: output,
1918
+ streamId: message.streamId,
1919
+ type: "stream-output"
1920
+ })) throw new Error("Stream output could not be delivered");
1921
+ return;
1922
+ }
1923
+ pendingBytes += frameLength(output);
1924
+ if (pendingBytes > maxPendingStreamOutputBytes) throw new Error("Stream produced too much output while opening");
1925
+ pending.push(output);
1926
+ }, AbortSignal.any([operationSignal, controller.signal]), (cause) => {
1927
+ sendError(state, cause, message.streamId, identity);
1928
+ disposeStream(state, message.streamId).catch((disposeCause) => {
1929
+ sendError(state, disposeCause, message.streamId, identity);
1930
+ });
1931
+ });
1932
+ send$1(state, {
1933
+ operationId: message.operationId,
1934
+ streamId: message.streamId,
1935
+ type: "stream-opened"
1936
+ });
1937
+ opened = true;
1938
+ for (const output of pending) if (!send$1(state, {
1939
+ message: output,
1940
+ streamId: message.streamId,
1941
+ type: "stream-output"
1942
+ })) throw new Error("Stream output could not be delivered");
1943
+ } catch (cause) {
1944
+ await disposeStream(state, message.streamId, false);
1945
+ throw cause;
1946
+ }
1947
+ });
1948
+ };
1949
+ const sendStreamMessage = (state, message) => {
1950
+ const entry = state.streams.get(message.streamId);
1951
+ const identity = {
1952
+ ...entry?.identity,
1953
+ operationId: message.streamId,
1954
+ operationType: message.type
1955
+ };
1956
+ if (!entry?.session) {
1957
+ sendError(state, new OperationError("not-found", "Stream not found"), message.streamId, identity);
1958
+ return;
1959
+ }
1960
+ entry.session.send(message.message).catch((cause) => {
1961
+ sendError(state, cause, message.streamId, identity);
1962
+ disposeStream(state, message.streamId).catch((disposeCause) => {
1963
+ sendError(state, disposeCause, message.streamId, identity);
1964
+ });
1965
+ });
1966
+ };
1967
+ const logClientDiagnostic = (state, message) => {
1968
+ if (!state.serverLogger?.enabled) return;
1969
+ state.serverLogger.log({
1970
+ details: {
1971
+ arguments: message.arguments,
1972
+ ...message.stack ? { stack: message.stack } : {},
1973
+ ...message.url ? { url: message.url } : {}
1974
+ },
1975
+ event: "browser-console",
1976
+ level: message.level === "log" ? "info" : message.level,
1977
+ message: message.message,
1978
+ source: "browser",
1979
+ timestamp: message.timestamp
1980
+ });
1981
+ };
1982
+ const handleMessage = (state, message) => {
1983
+ if (message.type === "client-diagnostic") {
1984
+ logClientDiagnostic(state, message);
1985
+ return;
1986
+ }
1987
+ if (message.type === "resource-read") {
1988
+ readResource(state, message);
1989
+ return;
1990
+ }
1991
+ if (message.type === "resource-subscribe") {
1992
+ subscribeResource(state, message);
1993
+ return;
1994
+ }
1995
+ if (message.type === "resource-unsubscribe") {
1996
+ disposeSubscription(state, message.subscriptionId).catch((cause) => {
1997
+ sendError(state, cause, message.subscriptionId);
1998
+ });
1999
+ return;
2000
+ }
2001
+ if (message.type === "stream-open") {
2002
+ openStream(state, message);
2003
+ return;
2004
+ }
2005
+ if (message.type === "stream-close") {
2006
+ disposeStream(state, message.streamId).catch((cause) => {
2007
+ sendError(state, cause, message.streamId);
2008
+ });
2009
+ return;
2010
+ }
2011
+ sendStreamMessage(state, message);
2012
+ };
2013
+ const handleFrame = (state, data, isBinary) => {
2014
+ let raw;
2015
+ try {
2016
+ raw = decodeProtocolFrame(isBinary ? bytesOf(data) : textOf(data));
2017
+ const result = clientProtocolMessageSchema.safeParse(raw);
2018
+ if (!result.success) {
2019
+ state.serverLogger?.log({
2020
+ correlationId: state.connectionId,
2021
+ details: errorDetails$1(result.error),
2022
+ event: "protocol-parse-error",
2023
+ level: "error"
2024
+ });
2025
+ sendError(state, result.error, operationIdOf(raw));
2026
+ return;
2027
+ }
2028
+ handleMessage(state, result.data);
2029
+ } catch (cause) {
2030
+ state.serverLogger?.log({
2031
+ correlationId: state.connectionId,
2032
+ details: errorDetails$1(cause),
2033
+ event: "protocol-parse-error",
2034
+ level: "error"
2035
+ });
2036
+ sendError(state, cause, operationIdOf(raw));
2037
+ }
2038
+ };
2039
+ const disposeConnection = async (state) => {
2040
+ if (state.closed) return;
2041
+ state.closed = true;
2042
+ state.serverLogger?.log({
2043
+ correlationId: state.connectionId,
2044
+ event: "websocket-close"
2045
+ });
2046
+ await disposeConnectionOperations(state, false);
2047
+ };
2048
+ const createConnectionState = ({ runtime, serverLogger, socket }) => ({
2049
+ closed: false,
2050
+ connectionId: serverLogger?.id(),
2051
+ operations: /* @__PURE__ */ new Map(),
2052
+ runtime,
2053
+ serverLogger,
2054
+ socket,
2055
+ streams: /* @__PURE__ */ new Map(),
2056
+ subscriptions: /* @__PURE__ */ new Map()
2057
+ });
2058
+ const attachWebSocketConnection = ({ runtime, serverLogger, socket }) => {
2059
+ const state = createConnectionState({
2060
+ runtime,
2061
+ serverLogger,
2062
+ socket
2063
+ });
2064
+ serverLogger?.log({
2065
+ correlationId: state.connectionId,
2066
+ event: "websocket-open"
2067
+ });
2068
+ socket.on("message", (data, isBinary) => handleFrame(state, data, isBinary));
2069
+ socket.once("close", (code, reason) => {
2070
+ serverLogger?.log({
2071
+ correlationId: state.connectionId,
2072
+ details: {
2073
+ code,
2074
+ reason: reason?.toString() ?? ""
2075
+ },
2076
+ event: "websocket-close-event"
2077
+ });
2078
+ disposeConnection(state);
2079
+ });
2080
+ socket.once("error", (cause) => {
2081
+ serverLogger?.log({
2082
+ correlationId: state.connectionId,
2083
+ details: errorDetails$1(cause),
2084
+ event: "websocket-error",
2085
+ level: "error"
2086
+ });
2087
+ disposeConnection(state);
2088
+ });
2089
+ return () => disposeConnection(state);
2090
+ };
2091
+ //#endregion
2092
+ //#region src/internal/server/vite-websocket-proxy.ts
2093
+ const setTrustedForwardingHeaders = (request, origin) => {
2094
+ Object.keys(request.headers).forEach((name) => {
2095
+ if (name === "authorization" || name === "cookie" || name === "forwarded" || name.startsWith("x-forwarded-")) Reflect.deleteProperty(request.headers, name);
2096
+ });
2097
+ const publicUrl = new URL(origin);
2098
+ request.headers["x-forwarded-for"] = request.socket.remoteAddress;
2099
+ request.headers["x-forwarded-host"] = publicUrl.host;
2100
+ request.headers["x-forwarded-port"] = publicUrl.port || (publicUrl.protocol === "https:" ? "443" : "80");
2101
+ request.headers["x-forwarded-proto"] = publicUrl.protocol.slice(0, -1);
2102
+ };
2103
+ const closeFrame = (code, reason) => {
2104
+ const payload = Buffer.allocUnsafe(2 + Buffer.byteLength(reason));
2105
+ payload.writeUInt16BE(code);
2106
+ payload.write(reason, 2);
2107
+ return Buffer.concat([Buffer.from([136, payload.length]), payload]);
2108
+ };
2109
+ const closeSocket = (socket, code, reason) => {
2110
+ if (socket.closed) return;
2111
+ socket.closed = true;
2112
+ clearInterval(socket.timer);
2113
+ if (!socket.client.destroyed) socket.client.end(closeFrame(code, reason));
2114
+ socket.upstream?.destroy();
2115
+ };
2116
+ const revalidateSocket = async ({ auth, serverLogger, origin, socket, token }) => {
2117
+ try {
2118
+ if (!await auth.service.authenticateToken(token, origin)) closeSocket(socket, webSocketCloseCode.authenticationRevoked, "authentication revoked");
2119
+ } catch (cause) {
2120
+ serverLogger?.log({
2121
+ details: errorDetails$1(cause),
2122
+ event: "vite-websocket-session-revalidation-failure",
2123
+ level: "error"
2124
+ });
2125
+ closeSocket(socket, webSocketCloseCode.authenticationUnavailable, "authentication unavailable");
2126
+ }
2127
+ };
2128
+ const createViteWebSocketProxy = ({ auth, serverLogger, target }) => {
2129
+ const sockets = /* @__PURE__ */ new Map();
2130
+ const connect = ({ correlationId, head, origin, request, session, socket }) => {
2131
+ let revalidationPending = false;
2132
+ const proxied = {
2133
+ client: socket,
2134
+ closed: false,
2135
+ sessionId: session.id,
2136
+ timer: setInterval(() => {
2137
+ if (revalidationPending) return;
2138
+ revalidationPending = true;
2139
+ revalidateSocket({
2140
+ auth,
2141
+ origin,
2142
+ serverLogger,
2143
+ socket: proxied,
2144
+ token: session.token
2145
+ }).finally(() => {
2146
+ revalidationPending = false;
2147
+ });
2148
+ }, 1e3)
2149
+ };
2150
+ sockets.set(socket, proxied);
2151
+ socket.once("close", () => {
2152
+ clearInterval(proxied.timer);
2153
+ sockets.delete(socket);
2154
+ });
2155
+ setTrustedForwardingHeaders(request, origin);
2156
+ proxyUpgrade(target, request, socket, head, {
2157
+ changeOrigin: true,
2158
+ xfwd: false
2159
+ }).then((upstream) => {
2160
+ if (proxied.closed) {
2161
+ upstream.destroy();
2162
+ return;
2163
+ }
2164
+ proxied.upstream = upstream;
2165
+ }, (cause) => {
2166
+ clearInterval(proxied.timer);
2167
+ sockets.delete(socket);
2168
+ if (proxied.closed) return;
2169
+ proxied.closed = true;
2170
+ serverLogger?.log({
2171
+ correlationId,
2172
+ details: errorDetails$1(cause),
2173
+ event: "vite-websocket-proxy-failure",
2174
+ level: "error"
2175
+ });
2176
+ });
2177
+ };
2178
+ return {
2179
+ close: () => {
2180
+ sockets.forEach((socket) => closeSocket(socket, 1001, "server shutting down"));
2181
+ sockets.clear();
2182
+ },
2183
+ connect,
2184
+ revoke: (ids) => {
2185
+ sockets.forEach((socket) => {
2186
+ if (ids.has(socket.sessionId)) closeSocket(socket, webSocketCloseCode.authenticationRevoked, "authentication revoked");
2187
+ });
2188
+ }
2189
+ };
2190
+ };
2191
+ //#endregion
2192
+ //#region src/internal/server/websocket-server.ts
2193
+ const maxWebSocketPayloadBytes = 1048576;
2194
+ const rejectUpgrade = (socket, status, message) => {
2195
+ socket.end(`HTTP/1.1 ${status} ${message}\r\nConnection: close\r\nContent-Length: 0\r\n\r\n`);
2196
+ };
2197
+ const requestPath = (request) => {
2198
+ try {
2199
+ return new URL(request.url ?? "", "http://localhost").pathname;
2200
+ } catch {
2201
+ return;
2202
+ }
2203
+ };
2204
+ const requestAuthenticationInput = (request) => {
2205
+ const host = request.headers.host;
2206
+ if (!host) return;
2207
+ const headers = new Headers();
2208
+ Object.entries(request.headers).forEach(([name, value]) => {
2209
+ if (Array.isArray(value)) value.forEach((entry) => headers.append(name, entry));
2210
+ else if (value !== void 0) headers.set(name, value);
2211
+ });
2212
+ const encrypted = Boolean(request.socket.encrypted);
2213
+ try {
2214
+ return new Request(`${encrypted ? "https" : "http"}://${host}${request.url ?? "/"}`, { headers });
2215
+ } catch {
2216
+ return;
2217
+ }
2218
+ };
2219
+ const attachSessionLifecycle = ({ auth, serverLogger, session, sessionSockets, socket }) => {
2220
+ sessionSockets.set(socket, session.id);
2221
+ let revalidationPending = false;
2222
+ const revalidationTimer = setInterval(() => {
2223
+ if (revalidationPending) return;
2224
+ revalidationPending = true;
2225
+ auth.service.authenticateToken(session.token, session.origin).then((authenticated) => {
2226
+ if (!authenticated) socket.close(webSocketCloseCode.authenticationRevoked, "authentication revoked");
2227
+ }, (cause) => {
2228
+ serverLogger?.log({
2229
+ details: errorDetails$1(cause),
2230
+ event: "websocket-session-revalidation-failure",
2231
+ level: "error"
2232
+ });
2233
+ socket.close(webSocketCloseCode.authenticationUnavailable, "authentication unavailable");
2234
+ }).finally(() => {
2235
+ revalidationPending = false;
2236
+ });
2237
+ }, 1e3);
2238
+ socket.once("close", () => {
2239
+ clearInterval(revalidationTimer);
2240
+ sessionSockets.delete(socket);
2241
+ });
2242
+ };
2243
+ const createWebSocketServer = ({ auth, developmentWebTarget, runtime, server, serverLogger }) => {
2244
+ const webSocketServer = new WebSocketServer({
2245
+ maxPayload: maxWebSocketPayloadBytes,
2246
+ noServer: true
2247
+ });
2248
+ let updateAvailable = false;
2249
+ const sessionSockets = /* @__PURE__ */ new Map();
2250
+ const viteProxy = developmentWebTarget ? createViteWebSocketProxy({
2251
+ auth,
2252
+ serverLogger,
2253
+ target: developmentWebTarget
2254
+ }) : void 0;
2255
+ const removeRevocationListener = auth.service.onSessionsRevoked((ids) => {
2256
+ sessionSockets.forEach((sessionId, socket) => {
2257
+ if (ids.has(sessionId)) socket.close(webSocketCloseCode.authenticationRevoked, "authentication revoked");
2258
+ });
2259
+ viteProxy?.revoke(ids);
2260
+ });
2261
+ const onUpgrade = async (request, socket, head) => {
2262
+ const correlationId = serverLogger?.id();
2263
+ serverLogger?.log({
2264
+ correlationId,
2265
+ event: "websocket-connect"
2266
+ });
2267
+ const path = requestPath(request);
2268
+ const isRuntimeSocket = path === runtimeWebSocketPath;
2269
+ const isViteSocket = path === "/_overmux/vite/hmr" && developmentWebTarget !== void 0;
2270
+ if (!isRuntimeSocket && !isViteSocket) {
2271
+ serverLogger?.log({
2272
+ correlationId,
2273
+ details: { status: 404 },
2274
+ event: "websocket-connect-rejected",
2275
+ level: "warn"
2276
+ });
2277
+ rejectUpgrade(socket, 404, "Not Found");
2278
+ return;
2279
+ }
2280
+ const authenticationInput = requestAuthenticationInput(request);
2281
+ if (!authenticationInput) {
2282
+ rejectUpgrade(socket, 400, "Bad Request");
2283
+ return;
2284
+ }
2285
+ const origin = auth.resolveOrigin({
2286
+ remoteAddress: request.socket.remoteAddress,
2287
+ request: authenticationInput
2288
+ });
2289
+ let session;
2290
+ try {
2291
+ session = await auth.requests.authenticate(authenticationInput, origin);
2292
+ } catch (cause) {
2293
+ serverLogger?.log({
2294
+ correlationId,
2295
+ details: errorDetails$1(cause),
2296
+ event: "websocket-authentication-failure",
2297
+ level: "error"
2298
+ });
2299
+ rejectUpgrade(socket, 503, "Service Unavailable");
2300
+ return;
2301
+ }
2302
+ if (!session) {
2303
+ rejectUpgrade(socket, 401, "Unauthorized");
2304
+ return;
2305
+ }
2306
+ if (session.kind === "browser" && (!origin || request.headers.origin !== origin)) {
2307
+ rejectUpgrade(socket, 403, "Forbidden");
2308
+ return;
2309
+ }
2310
+ if (isViteSocket) {
2311
+ if (session.kind !== "browser" || session.credential !== "cookie" || !origin) {
2312
+ rejectUpgrade(socket, 401, "Unauthorized");
2313
+ return;
2314
+ }
2315
+ if (!viteProxy) {
2316
+ rejectUpgrade(socket, 503, "Service Unavailable");
2317
+ return;
2318
+ }
2319
+ viteProxy.connect({
2320
+ correlationId,
2321
+ head,
2322
+ origin,
2323
+ request,
2324
+ session,
2325
+ socket
2326
+ });
2327
+ return;
2328
+ }
2329
+ webSocketServer.handleUpgrade(request, socket, head, (webSocket) => {
2330
+ attachSessionLifecycle({
2331
+ auth,
2332
+ serverLogger,
2333
+ session,
2334
+ sessionSockets,
2335
+ socket: webSocket
2336
+ });
2337
+ webSocketServer.emit("connection", webSocket, request);
2338
+ attachWebSocketConnection({
2339
+ runtime,
2340
+ serverLogger,
2341
+ socket: webSocket
2342
+ });
2343
+ if (updateAvailable) webSocket.send(encodeProtocolFrame({ type: "update-available" }));
2344
+ });
2345
+ };
2346
+ server.on("upgrade", onUpgrade);
2347
+ const broadcast = (message) => {
2348
+ const frame = encodeProtocolFrame(serverProtocolMessageSchema.parse(message));
2349
+ let delivered = 0;
2350
+ webSocketServer.clients.forEach((socket) => {
2351
+ if (socket.readyState !== WebSocket.OPEN) return;
2352
+ if (socket.bufferedAmount > 1048576) {
2353
+ socket.close(1013, "client could not keep up");
2354
+ return;
2355
+ }
2356
+ try {
2357
+ socket.send(frame);
2358
+ delivered += 1;
2359
+ } catch (cause) {
2360
+ socket.terminate();
2361
+ serverLogger?.log({
2362
+ details: errorDetails$1(cause),
2363
+ event: "websocket-broadcast-failure",
2364
+ level: "error"
2365
+ });
2366
+ }
2367
+ });
2368
+ return delivered;
2369
+ };
2370
+ return {
2371
+ announceRestarting: () => broadcast({ type: "restarting" }),
2372
+ announceUpdateAvailable: () => {
2373
+ updateAvailable = true;
2374
+ return broadcast({ type: "update-available" });
2375
+ },
2376
+ broadcastNotification: (notification) => broadcast(notificationEventSchema.parse({
2377
+ notification,
2378
+ type: "notification"
2379
+ })),
2380
+ close: async () => {
2381
+ server.off("upgrade", onUpgrade);
2382
+ removeRevocationListener();
2383
+ viteProxy?.close();
2384
+ webSocketServer.clients.forEach((socket) => socket.terminate());
2385
+ await new Promise((resolve) => webSocketServer.close(() => resolve()));
2386
+ }
2387
+ };
2388
+ };
2389
+ //#endregion
2390
+ //#region src/internal/server/notifications/background-notification-payload.ts
2391
+ const maxPayloadBytes = 3800;
2392
+ const payloadBytes = (payload) => new TextEncoder().encode(JSON.stringify(payload)).byteLength;
2393
+ const largestFittingPrefix = (characters, accepts, low = 0, high = characters.length) => {
2394
+ if (low >= high) return characters.slice(0, low).join("");
2395
+ const middle = Math.ceil((low + high) / 2);
2396
+ return accepts(characters.slice(0, middle).join("")) ? largestFittingPrefix(characters, accepts, middle, high) : largestFittingPrefix(characters, accepts, low, middle - 1);
2397
+ };
2398
+ const notificationPayload = (notification) => ({
2399
+ notification,
2400
+ type: "notification"
2401
+ });
2402
+ const createBackgroundNotificationPayload = (notification) => {
2403
+ const original = notificationPayload(notification);
2404
+ if (payloadBytes(original) <= maxPayloadBytes) return JSON.stringify(original);
2405
+ const withoutBody = {
2406
+ ...notification,
2407
+ body: void 0
2408
+ };
2409
+ const body = largestFittingPrefix([...notification.body ?? ""], (prefix) => payloadBytes(notificationPayload({
2410
+ ...withoutBody,
2411
+ ...prefix ? { body: prefix } : {}
2412
+ })) <= maxPayloadBytes);
2413
+ const bodyTruncated = notificationPayload({
2414
+ ...withoutBody,
2415
+ ...body ? { body } : {}
2416
+ });
2417
+ if (payloadBytes(bodyTruncated) <= maxPayloadBytes) return JSON.stringify(bodyTruncated);
2418
+ const title = largestFittingPrefix([...notification.title], (prefix) => Boolean(prefix) && payloadBytes(notificationPayload({
2419
+ ...withoutBody,
2420
+ title: prefix
2421
+ })) <= maxPayloadBytes);
2422
+ if (title) return JSON.stringify(notificationPayload({
2423
+ ...withoutBody,
2424
+ title
2425
+ }));
2426
+ const safeTitle = largestFittingPrefix([...notification.title], (prefix) => Boolean(prefix) && payloadBytes(notificationPayload({ title: prefix })) <= maxPayloadBytes);
2427
+ return JSON.stringify(notificationPayload({ title: safeTitle || "Overmux" }));
2428
+ };
2429
+ //#endregion
2430
+ //#region src/internal/server/notifications/background-notification-service.ts
2431
+ const vapidSubject = "https://github.com/richardgill/overmux";
2432
+ const webPushStatus = (cause) => {
2433
+ if (typeof cause === "object" && cause !== null && "statusCode" in cause && typeof cause.statusCode === "number") return cause.statusCode;
2434
+ };
2435
+ const deliverToSubscription = async ({ keys, logger, payload, sendWebPush, store, subscription }) => {
2436
+ try {
2437
+ await sendWebPush(subscription, payload, {
2438
+ TTL: 60,
2439
+ vapidDetails: {
2440
+ privateKey: keys.privateKey,
2441
+ publicKey: keys.publicKey,
2442
+ subject: vapidSubject
2443
+ }
2444
+ });
2445
+ } catch (cause) {
2446
+ const status = webPushStatus(cause);
2447
+ if (status === 404 || status === 410) await store.removeSubscription(subscription.endpoint);
2448
+ logger?.log({
2449
+ details: {
2450
+ ...errorDetails$1(cause),
2451
+ ...status ? { status } : {}
2452
+ },
2453
+ event: "background-notification-delivery-failure",
2454
+ level: "warn"
2455
+ });
2456
+ }
2457
+ };
2458
+ const createBackgroundNotificationService = ({ isSessionActive = async () => true, logger, sendWebPush = webPush.sendNotification, store }) => {
2459
+ let keysPromise;
2460
+ const getKeys = () => {
2461
+ if (!keysPromise) keysPromise = store.getOrCreateKeys(webPush.generateVAPIDKeys).catch((cause) => {
2462
+ keysPromise = void 0;
2463
+ throw cause;
2464
+ });
2465
+ return keysPromise;
2466
+ };
2467
+ return {
2468
+ disable: (endpoint) => store.removeSubscription(endpoint),
2469
+ disableSessions: (sessionIds) => store.removeSessionSubscriptions(sessionIds),
2470
+ enable: (sessionId, subscription) => store.upsertSubscription(sessionId, subscription),
2471
+ publicKey: async () => (await getKeys()).publicKey,
2472
+ send: async (notification) => {
2473
+ const storedSubscriptions = await store.listSubscriptions();
2474
+ const subscriptions = (await Promise.all(storedSubscriptions.map(async (entry) => ({
2475
+ active: await isSessionActive(entry.sessionId),
2476
+ entry
2477
+ })))).flatMap(({ active: sessionActive, entry }) => sessionActive ? [entry] : []);
2478
+ if (!subscriptions.length) return;
2479
+ const keys = await getKeys();
2480
+ const payload = createBackgroundNotificationPayload(notification);
2481
+ await Promise.all(subscriptions.map((entry) => deliverToSubscription({
2482
+ keys,
2483
+ logger,
2484
+ payload,
2485
+ sendWebPush,
2486
+ store,
2487
+ subscription: entry.subscription
2488
+ })));
2489
+ }
2490
+ };
2491
+ };
2492
+ //#endregion
2493
+ //#region src/internal/server/notifications/background-notification-store.ts
2494
+ const vapidKeysSchema = z.object({
2495
+ privateKey: z.string().min(1),
2496
+ publicKey: z.string().min(1)
2497
+ }).strict();
2498
+ const storedSubscriptionSchema = z.object({
2499
+ sessionId: z.string(),
2500
+ subscription: backgroundNotificationSubscriptionSchema
2501
+ });
2502
+ const subscriptionsSchema = z.array(storedSubscriptionSchema);
2503
+ const backgroundNotificationDataDirectory = ({ environment, homeDirectory }) => join(getOvermuxPaths({
2504
+ environment,
2505
+ homeDir: homeDirectory
2506
+ }).dataDir, "background-notifications");
2507
+ const createFileBackgroundNotificationStore = ({ environment = process.env, homeDirectory = homedir() } = {}) => {
2508
+ const directory = backgroundNotificationDataDirectory({
2509
+ environment,
2510
+ homeDirectory
2511
+ });
2512
+ const keysPath = join(directory, "vapid.json");
2513
+ const subscriptionsPath = join(directory, "subscriptions.json");
2514
+ let pendingSubscriptionWrite = Promise.resolve();
2515
+ const readSubscriptions = async () => await readPrivateJson(subscriptionsPath, subscriptionsSchema) ?? [];
2516
+ const updateSubscriptions = (update) => {
2517
+ const operation = pendingSubscriptionWrite.then(async () => {
2518
+ const updated = update(await readSubscriptions());
2519
+ if (updated) await replacePrivateJsonAtomically(subscriptionsPath, updated);
2520
+ });
2521
+ pendingSubscriptionWrite = operation.catch(() => void 0);
2522
+ return operation;
2523
+ };
2524
+ return {
2525
+ getOrCreateKeys: async (generate) => {
2526
+ const existing = await readPrivateJson(keysPath, vapidKeysSchema);
2527
+ if (existing) return existing;
2528
+ const generated = vapidKeysSchema.parse(generate());
2529
+ try {
2530
+ await createPrivateJsonExclusively(keysPath, generated);
2531
+ return generated;
2532
+ } catch (cause) {
2533
+ if (cause.code !== "EEXIST") throw cause;
2534
+ const concurrentlyCreated = await readPrivateJson(keysPath, vapidKeysSchema);
2535
+ if (!concurrentlyCreated) throw cause;
2536
+ return concurrentlyCreated;
2537
+ }
2538
+ },
2539
+ listSubscriptions: async () => {
2540
+ await pendingSubscriptionWrite;
2541
+ return readSubscriptions();
2542
+ },
2543
+ removeSubscription: (endpoint) => updateSubscriptions((subscriptions) => {
2544
+ if (!subscriptions.some((entry) => entry.subscription.endpoint === endpoint)) return;
2545
+ return subscriptions.filter((entry) => entry.subscription.endpoint !== endpoint);
2546
+ }),
2547
+ removeSessionSubscriptions: (sessionIds) => updateSubscriptions((subscriptions) => {
2548
+ const updated = subscriptions.filter((entry) => !sessionIds.has(entry.sessionId));
2549
+ return updated.length === subscriptions.length ? void 0 : updated;
2550
+ }),
2551
+ upsertSubscription: (sessionId, subscription) => {
2552
+ const parsed = backgroundNotificationSubscriptionSchema.parse(subscription);
2553
+ return updateSubscriptions((subscriptions) => {
2554
+ const entry = {
2555
+ sessionId,
2556
+ subscription: parsed
2557
+ };
2558
+ if (isDeepStrictEqual(subscriptions.find((candidate) => candidate.subscription.endpoint === parsed.endpoint), entry)) return;
2559
+ return [...subscriptions.filter((candidate) => candidate.subscription.endpoint !== parsed.endpoint), entry];
2560
+ });
2561
+ }
2562
+ };
2563
+ };
2564
+ z.void();
2565
+ const jsonValueSchema = z.lazy(() => z.union([
2566
+ z.string(),
2567
+ z.number().finite(),
2568
+ z.boolean(),
2569
+ z.null(),
2570
+ z.array(jsonValueSchema),
2571
+ z.record(z.string(), jsonValueSchema)
2572
+ ]));
2573
+ //#endregion
2574
+ //#region src/internal/server/notifications/notification-service.ts
2575
+ const deliverSafely = async ({ deliver, event, logger }) => {
2576
+ try {
2577
+ await deliver();
2578
+ } catch (cause) {
2579
+ logger?.log({
2580
+ details: errorDetails$1(cause),
2581
+ event,
2582
+ level: "error"
2583
+ });
2584
+ }
2585
+ };
2586
+ const createNotificationService = ({ backgroundNotifications, logger }) => {
2587
+ let liveDeliveryAttached = false;
2588
+ let deliverLive = () => void 0;
2589
+ return {
2590
+ send: async (input) => {
2591
+ const notification = notificationSchema.parse(input);
2592
+ await Promise.all([deliverSafely({
2593
+ deliver: () => deliverLive(notification),
2594
+ event: "live-notification-dispatch-failure",
2595
+ logger
2596
+ }), deliverSafely({
2597
+ deliver: () => backgroundNotifications.send(notification),
2598
+ event: "background-notification-dispatch-failure",
2599
+ logger
2600
+ })]);
2601
+ },
2602
+ attachLiveDelivery: (deliver) => {
2603
+ if (liveDeliveryAttached) throw new Error("Live delivery is already attached");
2604
+ liveDeliveryAttached = true;
2605
+ deliverLive = deliver;
2606
+ }
2607
+ };
2608
+ };
2609
+ //#endregion
2610
+ //#region src/internal/server/runtime/runtime-lifecycle.ts
2611
+ const asError = (cause) => cause instanceof Error ? cause : new Error(String(cause));
2612
+ const disposeAll = async (disposers) => {
2613
+ const errors = (await Promise.allSettled([...disposers].map(async (dispose) => dispose()))).flatMap((result) => result.status === "rejected" ? [asError(result.reason)] : []);
2614
+ if (errors.length) throw new AggregateError(errors, "Runtime disposal failed");
2615
+ };
2616
+ const createRuntimeLifecycle = (preparationSignal) => {
2617
+ const controller = new AbortController();
2618
+ const activeDisposers = /* @__PURE__ */ new Set();
2619
+ let disposal;
2620
+ const abortPreparation = () => controller.abort(preparationSignal?.reason);
2621
+ preparationSignal?.addEventListener("abort", abortPreparation, { once: true });
2622
+ if (preparationSignal?.aborted) abortPreparation();
2623
+ const completePreparation = () => {
2624
+ preparationSignal?.removeEventListener("abort", abortPreparation);
2625
+ };
2626
+ const requestSignal = (signal) => signal ? AbortSignal.any([controller.signal, signal]) : controller.signal;
2627
+ const track = (dispose, signal) => {
2628
+ let pending;
2629
+ const disposeOnce = () => {
2630
+ if (pending) return pending;
2631
+ controller.signal.removeEventListener("abort", onAbort);
2632
+ signal?.removeEventListener("abort", onAbort);
2633
+ activeDisposers.delete(disposeOnce);
2634
+ pending = Promise.resolve().then(dispose);
2635
+ return pending;
2636
+ };
2637
+ const onAbort = () => {
2638
+ disposeOnce().catch(() => void 0);
2639
+ };
2640
+ activeDisposers.add(disposeOnce);
2641
+ controller.signal.addEventListener("abort", onAbort, { once: true });
2642
+ signal?.addEventListener("abort", onAbort, { once: true });
2643
+ if (controller.signal.aborted || signal?.aborted) onAbort();
2644
+ return disposeOnce;
2645
+ };
2646
+ const dispose = (reason = /* @__PURE__ */ new Error("Runtime disposed")) => {
2647
+ if (disposal) return disposal;
2648
+ completePreparation();
2649
+ const pending = [...activeDisposers];
2650
+ activeDisposers.clear();
2651
+ controller.abort(reason);
2652
+ disposal = disposeAll(pending);
2653
+ return disposal;
2654
+ };
2655
+ return {
2656
+ completePreparation,
2657
+ dispose,
2658
+ disposeAll,
2659
+ requestSignal,
2660
+ signal: controller.signal,
2661
+ track
2662
+ };
2663
+ };
2664
+ //#endregion
2665
+ //#region src/internal/server/runtime/resource-dependency-graph.ts
2666
+ const emptyGraphFor = (resourceIds) => new Map(resourceIds.map((resourceId) => [resourceId, /* @__PURE__ */ new Set()]));
2667
+ const visitDependencies = ({ dependencies, path, resourceId, visited, visiting }) => {
2668
+ if (visiting.has(resourceId)) throw new Error(`Resource dependency cycle: ${[...path, resourceId].join(" -> ")}`);
2669
+ if (visited.has(resourceId)) return;
2670
+ visiting.add(resourceId);
2671
+ dependencies.get(resourceId)?.forEach((dependencyId) => {
2672
+ visitDependencies({
2673
+ dependencies,
2674
+ path: [...path, resourceId],
2675
+ resourceId: dependencyId,
2676
+ visited,
2677
+ visiting
2678
+ });
2679
+ });
2680
+ visiting.delete(resourceId);
2681
+ visited.add(resourceId);
2682
+ };
2683
+ const assertAcyclic = (dependencies) => {
2684
+ const visited = /* @__PURE__ */ new Set();
2685
+ const visiting = /* @__PURE__ */ new Set();
2686
+ dependencies.forEach((_, resourceId) => {
2687
+ visitDependencies({
2688
+ dependencies,
2689
+ path: [],
2690
+ resourceId,
2691
+ visited,
2692
+ visiting
2693
+ });
2694
+ });
2695
+ };
2696
+ const createResourceDependencyGraph = (resources) => {
2697
+ const resourceIds = Object.keys(resources);
2698
+ if (resourceIds.some((resourceId) => !resourceId)) throw new Error("Resource IDs must not be empty");
2699
+ const dependencies = emptyGraphFor(resourceIds);
2700
+ const dependants = emptyGraphFor(resourceIds);
2701
+ Object.entries(resources).forEach(([resourceId, resource]) => {
2702
+ if (resource.kind !== "derived") return;
2703
+ Object.entries(resource.dependencies).forEach(([dependencyName, dependencyId]) => {
2704
+ if (!dependencyName) throw new Error(`Resource ${resourceId} has an empty dependency name`);
2705
+ if (!dependencyId) throw new Error(`Resource ${resourceId} has an empty dependency ID`);
2706
+ if (!dependencies.has(dependencyId)) throw new Error(`Unknown resource dependency: ${dependencyId}`);
2707
+ dependencies.get(resourceId)?.add(dependencyId);
2708
+ dependants.get(dependencyId)?.add(resourceId);
2709
+ });
2710
+ });
2711
+ assertAcyclic(dependencies);
2712
+ return Object.freeze({
2713
+ dependencies,
2714
+ dependants
2715
+ });
2716
+ };
2717
+ const collectInvalidatedResourceIds = (graph, resourceId, invalidated) => {
2718
+ if (invalidated.has(resourceId)) return;
2719
+ invalidated.add(resourceId);
2720
+ graph.dependants.get(resourceId)?.forEach((dependantId) => {
2721
+ collectInvalidatedResourceIds(graph, dependantId, invalidated);
2722
+ });
2723
+ };
2724
+ const getInvalidatedResourceIds = (graph, resourceId) => {
2725
+ const invalidated = /* @__PURE__ */ new Set();
2726
+ collectInvalidatedResourceIds(graph, resourceId, invalidated);
2727
+ return invalidated;
2728
+ };
2729
+ //#endregion
2730
+ //#region src/internal/server/runtime/runtime-resources.ts
2731
+ const createRuntimeResources = ({ definitions, lifecycle }) => {
2732
+ const graph = createResourceDependencyGraph(definitions);
2733
+ const invalidationListeners = /* @__PURE__ */ new Set();
2734
+ const contracts = /* @__PURE__ */ new Map();
2735
+ const resources = /* @__PURE__ */ new Map();
2736
+ Object.entries(definitions).forEach(([id, definition]) => {
2737
+ contracts.set(definition.contract, [...contracts.get(definition.contract) ?? [], id]);
2738
+ });
2739
+ const invalidateResource = (id, input, allInputs = false) => {
2740
+ if (lifecycle.signal.aborted) return;
2741
+ const ids = getInvalidatedResourceIds(graph, id);
2742
+ invalidationListeners.forEach((entry) => {
2743
+ if (ids.has(entry.resourceId) && (allInputs || isDeepStrictEqual(entry.input, input))) entry.listener();
2744
+ });
2745
+ };
2746
+ const invalidate = (contract, input) => {
2747
+ const ids = contracts.get(contract) ?? Object.entries(definitions).filter(([, definition]) => definition.contract.input === contract.input && definition.contract.output === contract.output).map(([id]) => id);
2748
+ if (!ids.length) throw new Error("Cannot invalidate a resource outside this runtime");
2749
+ ids.forEach((id) => {
2750
+ const definition = definitions[id];
2751
+ const parsedInput = input === void 0 ? void 0 : definition.contract.input.parse(input);
2752
+ invalidateResource(id, parsedInput, input === void 0);
2753
+ });
2754
+ };
2755
+ const context = (signal) => ({
2756
+ invalidate,
2757
+ signal: lifecycle.requestSignal(signal)
2758
+ });
2759
+ const read = async (id, rawInput, signal) => {
2760
+ const definition = definitions[id];
2761
+ const operationSignal = lifecycle.requestSignal(signal);
2762
+ operationSignal.throwIfAborted();
2763
+ const input = definition.contract.input.parse(rawInput);
2764
+ const rawOutput = definition.kind === "derived" ? definition.combine(Object.fromEntries(await Promise.all(Object.entries(definition.dependencies).map(async ([name, dependencyId]) => [name, await read(dependencyId, rawInput, signal)])))) : await definition.read(input, context(signal));
2765
+ operationSignal.throwIfAborted();
2766
+ return definition.contract.output.parse(rawOutput);
2767
+ };
2768
+ const activateWatchers = (id, rawInput, signal, activated, disposers) => {
2769
+ if (activated.has(id)) return;
2770
+ activated.add(id);
2771
+ const definition = definitions[id];
2772
+ const input = definition.contract.input.parse(rawInput);
2773
+ if (definition.kind === "subscription") {
2774
+ const dispose = definition.subscribe(input, () => invalidateResource(id, input), context(signal));
2775
+ if (typeof dispose !== "function") throw new Error(`Resource ${id} subscribe must return a disposer`);
2776
+ disposers.push(dispose);
2777
+ return;
2778
+ }
2779
+ if (definition.kind === "derived") graph.dependencies.get(id)?.forEach((dependencyId) => {
2780
+ activateWatchers(dependencyId, rawInput, signal, activated, disposers);
2781
+ });
2782
+ };
2783
+ Object.keys(definitions).forEach((name) => {
2784
+ resources.set(name, {
2785
+ read: (input, signal) => read(name, input, signal),
2786
+ subscribe: (rawInput, listener, signal) => {
2787
+ lifecycle.requestSignal(signal).throwIfAborted();
2788
+ const entry = {
2789
+ input: definitions[name].contract.input.parse(rawInput),
2790
+ listener,
2791
+ resourceId: name
2792
+ };
2793
+ const watcherDisposers = [];
2794
+ invalidationListeners.add(entry);
2795
+ try {
2796
+ activateWatchers(name, rawInput, signal, /* @__PURE__ */ new Set(), watcherDisposers);
2797
+ } catch (cause) {
2798
+ invalidationListeners.delete(entry);
2799
+ lifecycle.disposeAll(watcherDisposers).catch(() => void 0);
2800
+ throw cause;
2801
+ }
2802
+ return lifecycle.track(async () => {
2803
+ invalidationListeners.delete(entry);
2804
+ await lifecycle.disposeAll(watcherDisposers);
2805
+ }, signal);
2806
+ }
2807
+ });
2808
+ });
2809
+ return {
2810
+ context,
2811
+ get: (name) => resources.get(name),
2812
+ invalidate,
2813
+ names: [...resources.keys()]
2814
+ };
2815
+ };
2816
+ //#endregion
2817
+ //#region src/internal/server/runtime/runtime-streams.ts
2818
+ const createRuntimeStreams = ({ context, definitions, lifecycle }) => {
2819
+ const streams = /* @__PURE__ */ new Map();
2820
+ Object.entries(definitions).forEach(([name, definition]) => {
2821
+ if (!name) throw new Error("Stream names must not be empty");
2822
+ streams.set(name, { open: async (rawInput, emit, requestSignal, onError) => {
2823
+ const sessionController = new AbortController();
2824
+ const sessionSignal = requestSignal ? AbortSignal.any([requestSignal, sessionController.signal]) : sessionController.signal;
2825
+ const operationSignal = lifecycle.requestSignal(sessionSignal);
2826
+ operationSignal.throwIfAborted();
2827
+ const input = definition.contract.input.parse(rawInput);
2828
+ let acceptingMessages = true;
2829
+ let failed = false;
2830
+ let failure;
2831
+ let opened = false;
2832
+ const fail = (cause) => {
2833
+ if (failed || operationSignal.aborted) return;
2834
+ failed = true;
2835
+ failure = cause;
2836
+ acceptingMessages = false;
2837
+ sessionController.abort(cause);
2838
+ if (opened) onError?.(cause);
2839
+ };
2840
+ const session = await definition.open(input, {
2841
+ ...context(sessionSignal),
2842
+ emit: (message) => {
2843
+ if (operationSignal.aborted) return;
2844
+ try {
2845
+ emit(definition.contract.serverMessage.parse(message));
2846
+ } catch (cause) {
2847
+ fail(cause);
2848
+ }
2849
+ },
2850
+ fail,
2851
+ signal: operationSignal
2852
+ });
2853
+ opened = true;
2854
+ if (failed || operationSignal.aborted) {
2855
+ await session.dispose?.();
2856
+ operationSignal.throwIfAborted();
2857
+ throw failure;
2858
+ }
2859
+ let messages = Promise.resolve();
2860
+ const dispose = lifecycle.track(async () => {
2861
+ acceptingMessages = false;
2862
+ sessionController.abort(/* @__PURE__ */ new Error("Stream session disposed"));
2863
+ await messages.catch(() => void 0);
2864
+ await session.dispose?.();
2865
+ }, requestSignal);
2866
+ return {
2867
+ dispose: async () => dispose(),
2868
+ send: async (rawMessage) => {
2869
+ if (!acceptingMessages) throw new Error("Stream session is closed");
2870
+ const message = definition.contract.clientMessage.parse(rawMessage);
2871
+ messages = messages.then(async () => {
2872
+ operationSignal.throwIfAborted();
2873
+ await session.onMessage?.(message);
2874
+ operationSignal.throwIfAborted();
2875
+ });
2876
+ return messages;
2877
+ }
2878
+ };
2879
+ } });
2880
+ });
2881
+ return {
2882
+ get: (name) => streams.get(name),
2883
+ names: [...streams.keys()]
2884
+ };
2885
+ };
2886
+ //#endregion
2887
+ //#region src/internal/server/runtime/create-runtime.ts
2888
+ const createRuntime = async ({ config, debug = false, notifications = { send: async () => void 0 }, signal }) => {
2889
+ const lifecycle = createRuntimeLifecycle(signal);
2890
+ try {
2891
+ const resources = createRuntimeResources({
2892
+ definitions: config.server.resources ?? {},
2893
+ lifecycle
2894
+ });
2895
+ const context = resources.context;
2896
+ const operations = createRuntimeOperations({
2897
+ context,
2898
+ definitions: config.server.operations ?? {},
2899
+ notifications
2900
+ });
2901
+ const streams = createRuntimeStreams({
2902
+ context,
2903
+ definitions: config.server.streams ?? {},
2904
+ lifecycle
2905
+ });
2906
+ lifecycle.signal.throwIfAborted();
2907
+ lifecycle.completePreparation();
2908
+ return {
2909
+ manifest: {
2910
+ debug,
2911
+ operations: [...operations.names],
2912
+ protocolVersion: 9,
2913
+ resources: [...resources.names],
2914
+ streams: [...streams.names]
2915
+ },
2916
+ dispose: lifecycle.dispose,
2917
+ getOperation: operations.get,
2918
+ getResource: resources.get,
2919
+ getStream: streams.get
2920
+ };
2921
+ } catch (cause) {
2922
+ await lifecycle.dispose(cause).catch(() => void 0);
2923
+ throw cause;
2924
+ }
2925
+ };
2926
+ //#endregion
2927
+ //#region src/internal/server/server-startup-options.ts
2928
+ const resolveServerStartupOptions = ({ config, configPath, overrides = {} }) => {
2929
+ const validatedOverrides = serverConfigRuntimeSchema.omit({ port: true }).extend({ port: z.number().int().min(0).max(65535).optional() }).parse(overrides);
2930
+ const configuredLogFile = config.logFile ? resolve(dirname(configPath), config.logFile) : void 0;
2931
+ const logFile = validatedOverrides.logFile ? resolve(validatedOverrides.logFile) : configuredLogFile;
2932
+ const configuredWebAssetsDir = config.productionWebAssetsDir ? resolve(dirname(configPath), config.productionWebAssetsDir) : void 0;
2933
+ const productionWebAssetsDir = validatedOverrides.productionWebAssetsDir ? resolve(validatedOverrides.productionWebAssetsDir) : configuredWebAssetsDir;
2934
+ return {
2935
+ host: validatedOverrides.host ?? config.host ?? "localhost",
2936
+ ...logFile === void 0 ? {} : { logFile },
2937
+ port: validatedOverrides.port ?? config.port ?? 4200,
2938
+ ...productionWebAssetsDir === void 0 ? {} : { productionWebAssetsDir },
2939
+ watch: validatedOverrides.watch ?? config.watch ?? true
2940
+ };
2941
+ };
2942
+ //#endregion
2943
+ //#region src/internal/server/start-application-server.ts
2944
+ const cleanupResources = async (cleanups) => {
2945
+ const errors = [];
2946
+ for (const cleanup of [...cleanups].reverse()) try {
2947
+ await cleanup();
2948
+ } catch (cause) {
2949
+ errors.push(cause);
2950
+ }
2951
+ if (errors.length > 0) throw new AggregateError(errors, "Failed to clean up server resources");
2952
+ };
2953
+ const createResources = () => {
2954
+ const cleanups = [];
2955
+ let closePromise;
2956
+ return {
2957
+ add: (cleanup) => cleanups.push(cleanup),
2958
+ close: () => {
2959
+ const next = closePromise ?? cleanupResources(cleanups);
2960
+ closePromise = next;
2961
+ return next;
2962
+ }
2963
+ };
2964
+ };
2965
+ const closeNodeServer = (server, cancelActiveWork) => {
2966
+ const closed = new Promise((resolveClose, reject) => {
2967
+ server.close((error) => {
2968
+ if (error && error.code !== "ERR_SERVER_NOT_RUNNING") {
2969
+ reject(error);
2970
+ return;
2971
+ }
2972
+ resolveClose();
2973
+ });
2974
+ });
2975
+ const cancellation = cancelActiveWork();
2976
+ if ("closeAllConnections" in server) server.closeAllConnections();
2977
+ return Promise.all([closed, cancellation]).then(() => void 0);
2978
+ };
2979
+ const getServerAddress = async (server) => {
2980
+ if (!server.address()) await once(server, "listening");
2981
+ const address = server.address();
2982
+ if (!address || typeof address === "string") throw new Error("Overmux server did not bind a TCP address");
2983
+ return address;
2984
+ };
2985
+ const registerProcessErrorLogging = (logger) => {
2986
+ const onUncaughtException = (cause, origin) => logger.log({
2987
+ details: errorDetails$1(cause),
2988
+ event: origin === "unhandledRejection" ? "unhandled-rejection" : "uncaught-exception",
2989
+ level: "error"
2990
+ });
2991
+ process.on("uncaughtExceptionMonitor", onUncaughtException);
2992
+ return () => {
2993
+ process.off("uncaughtExceptionMonitor", onUncaughtException);
2994
+ };
2995
+ };
2996
+ const loadServerSettings = async ({ configPath, options }) => {
2997
+ const { config } = await loadOvermuxConfig({
2998
+ aliases: options.configAliases,
2999
+ configPath
3000
+ });
3001
+ return {
3002
+ config,
3003
+ startup: resolveServerStartupOptions({
3004
+ config,
3005
+ configPath,
3006
+ overrides: options
3007
+ })
3008
+ };
3009
+ };
3010
+ const createAuthOrigins = ({ configuredOrigins, startup }) => {
3011
+ const urlHost = startup.host.includes(":") ? `[${startup.host}]` : startup.host;
3012
+ let values = (configuredOrigins ?? [`http://${urlHost}:${startup.port}`]).map(validateAuthOrigin);
3013
+ return {
3014
+ establish: (localUrl) => {
3015
+ values = (configuredOrigins ?? [localUrl]).map(validateAuthOrigin);
3016
+ return values;
3017
+ },
3018
+ get: () => values
3019
+ };
3020
+ };
3021
+ const createApplicationServices = async ({ config, logger, resources, startup }) => {
3022
+ const authOrigins = createAuthOrigins({
3023
+ configuredOrigins: config.auth.origins,
3024
+ startup
3025
+ });
3026
+ const authService = createAuthService({ config: config.auth });
3027
+ const auth = createAuthBoundary({
3028
+ origins: authOrigins.get,
3029
+ service: authService,
3030
+ trustedProxyPeer: config.auth.trustedProxyPeer
3031
+ });
3032
+ const backgroundNotifications = createBackgroundNotificationService({
3033
+ isSessionActive: authService.isSessionActive,
3034
+ logger,
3035
+ store: createFileBackgroundNotificationStore()
3036
+ });
3037
+ resources.add(authService.onSessionsRevoked((sessionIds) => {
3038
+ backgroundNotifications.disableSessions(sessionIds).catch((cause) => {
3039
+ logger.log({
3040
+ details: errorDetails$1(cause),
3041
+ event: "background-notification-session-cleanup-failed",
3042
+ level: "error"
3043
+ });
3044
+ });
3045
+ }));
3046
+ const notifications = createNotificationService({
3047
+ backgroundNotifications,
3048
+ logger
3049
+ });
3050
+ const runtime = await createRuntime({
3051
+ config,
3052
+ debug: logger.enabled,
3053
+ notifications
3054
+ });
3055
+ resources.add(runtime.dispose);
3056
+ return {
3057
+ auth,
3058
+ authOrigins,
3059
+ authService,
3060
+ backgroundNotifications,
3061
+ notifications,
3062
+ runtime
3063
+ };
3064
+ };
3065
+ const startApplicationTransport = async ({ auth, authOrigins, authService, backgroundNotifications, developmentWebTarget, logger, notifications, onRestartRequested, resources, runtime, startup }) => {
3066
+ let announceRestarting = () => void 0;
3067
+ const app = createHttpApp({
3068
+ auth,
3069
+ backgroundNotifications,
3070
+ developmentWebTarget,
3071
+ onRestartRequested: () => {
3072
+ announceRestarting();
3073
+ onRestartRequested();
3074
+ },
3075
+ runtime,
3076
+ serverLogger: logger,
3077
+ webAssetsDir: developmentWebTarget ? void 0 : startup.productionWebAssetsDir
3078
+ });
3079
+ const server = serve({
3080
+ fetch: app.fetch,
3081
+ hostname: startup.host,
3082
+ port: startup.port
3083
+ });
3084
+ resources.add(() => closeNodeServer(server, runtime.dispose));
3085
+ const webSockets = createWebSocketServer({
3086
+ auth,
3087
+ developmentWebTarget,
3088
+ runtime,
3089
+ server,
3090
+ serverLogger: logger
3091
+ });
3092
+ resources.add(webSockets.close);
3093
+ announceRestarting = webSockets.announceRestarting;
3094
+ notifications.attachLiveDelivery(webSockets.broadcastNotification);
3095
+ const address = await getServerAddress(server);
3096
+ const urlHost = startup.host.includes(":") ? `[${startup.host}]` : startup.host;
3097
+ const localUrl = `http://${urlHost}:${address.port}`;
3098
+ const controlApiHost = startup.host === "0.0.0.0" ? "127.0.0.1" : startup.host === "::" ? "[::1]" : urlHost;
3099
+ const origins = authOrigins.establish(localUrl);
3100
+ authService.setOrigins(origins);
3101
+ const instanceControl = await startInstanceControl({
3102
+ auth: authService,
3103
+ apiUrl: `http://${controlApiHost}:${address.port}`,
3104
+ port: address.port,
3105
+ url: origins[0]
3106
+ });
3107
+ resources.add(instanceControl.close);
3108
+ return {
3109
+ address,
3110
+ app,
3111
+ localUrl,
3112
+ webSockets
3113
+ };
3114
+ };
3115
+ const closeApplicationServer = async ({ app, logger, resources }) => {
3116
+ logger.log({ event: "server-stop" });
3117
+ app.beginShutdown();
3118
+ await resources.close();
3119
+ };
3120
+ const failApplicationServerStartup = async (cause, resources) => {
3121
+ try {
3122
+ await resources.close();
3123
+ } catch (cleanupCause) {
3124
+ throw new AggregateError([cause, cleanupCause], "Server startup and cleanup failed");
3125
+ }
3126
+ throw cause;
3127
+ };
3128
+ const startApplicationServer = async ({ onRestartRequested, options }) => {
3129
+ const logger = createServerLogger({
3130
+ enabled: options.debug ?? false,
3131
+ logFile: options.logFile ? resolve(options.logFile) : void 0
3132
+ });
3133
+ const resources = createResources();
3134
+ resources.add(registerProcessErrorLogging(logger));
3135
+ try {
3136
+ const configPath = resolve(options.configPath);
3137
+ logger.log({
3138
+ details: { configPath },
3139
+ event: "server-start"
3140
+ });
3141
+ const { config, startup } = await loadServerSettings({
3142
+ configPath,
3143
+ options
3144
+ });
3145
+ logger.setEnabled(Boolean(options.debug || config.debug));
3146
+ if (startup.logFile) logger.setLogFile(startup.logFile);
3147
+ const { auth, authOrigins, authService, backgroundNotifications, notifications, runtime } = await createApplicationServices({
3148
+ config,
3149
+ logger,
3150
+ resources,
3151
+ startup
3152
+ });
3153
+ const { address, app, localUrl, webSockets } = await startApplicationTransport({
3154
+ auth,
3155
+ authOrigins,
3156
+ authService,
3157
+ backgroundNotifications,
3158
+ developmentWebTarget: options.developmentWebTarget,
3159
+ logger,
3160
+ notifications,
3161
+ onRestartRequested,
3162
+ resources,
3163
+ runtime,
3164
+ startup
3165
+ });
3166
+ return {
3167
+ announceUpdateAvailable: webSockets.announceUpdateAvailable,
3168
+ close: () => closeApplicationServer({
3169
+ app,
3170
+ logger,
3171
+ resources
3172
+ }),
3173
+ host: startup.host,
3174
+ port: address.port,
3175
+ url: localUrl,
3176
+ watch: startup.watch
3177
+ };
3178
+ } catch (cause) {
3179
+ return failApplicationServerStartup(cause, resources);
3180
+ }
3181
+ };
3182
+ //#endregion
3183
+ //#region src/internal/server/coordinator/ipc-protocol.ts
3184
+ const childOptionsSchema = z.object({
3185
+ configAliases: z.record(z.string(), z.string()).optional(),
3186
+ configPath: z.string(),
3187
+ debug: z.boolean().optional(),
3188
+ developmentWebTarget: z.url().optional(),
3189
+ host: z.string().optional(),
3190
+ logFile: z.string().optional(),
3191
+ port: z.number().int().min(0).max(65535).optional(),
3192
+ productionWebAssetsDir: z.string().optional(),
3193
+ watch: z.boolean().optional()
3194
+ });
3195
+ const parentMessageSchema = z.discriminatedUnion("type", [
3196
+ z.object({
3197
+ options: childOptionsSchema,
3198
+ type: z.literal("start")
3199
+ }),
3200
+ z.object({ type: z.literal("update-available") }),
3201
+ z.object({ type: z.literal("stop") })
3202
+ ]);
3203
+ z.discriminatedUnion("type", [
3204
+ z.object({
3205
+ host: z.string(),
3206
+ port: z.number().int().min(1).max(65535),
3207
+ type: z.literal("ready"),
3208
+ url: z.string(),
3209
+ watch: z.boolean()
3210
+ }),
3211
+ z.object({ type: z.literal("restart-requested") }),
3212
+ z.object({
3213
+ message: z.string(),
3214
+ type: z.literal("startup-failed")
3215
+ })
3216
+ ]);
3217
+ const parseParentMessage = (value) => parentMessageSchema.parse(value);
3218
+ //#endregion
3219
+ //#region src/internal/server/coordinator/server-child.ts
3220
+ const send = (message) => {
3221
+ if (!process.connected) return;
3222
+ try {
3223
+ process.send?.(message, () => void 0);
3224
+ } catch {}
3225
+ };
3226
+ const disconnect = () => {
3227
+ if (!process.connected) return;
3228
+ try {
3229
+ process.disconnect();
3230
+ } catch {}
3231
+ };
3232
+ const runServerChild = () => {
3233
+ let serverPromise;
3234
+ let closing;
3235
+ const close = () => {
3236
+ if (closing) return closing;
3237
+ closing = serverPromise?.then((server) => server.close(), () => void 0) ?? Promise.resolve();
3238
+ return closing;
3239
+ };
3240
+ process.on("message", (raw) => {
3241
+ let message;
3242
+ try {
3243
+ message = parseParentMessage(raw);
3244
+ } catch (cause) {
3245
+ send({
3246
+ message: errorMessage(cause),
3247
+ type: "startup-failed"
3248
+ });
3249
+ return;
3250
+ }
3251
+ if (message.type === "update-available") {
3252
+ if (!closing) serverPromise?.then((server) => server.announceUpdateAvailable(), () => void 0);
3253
+ return;
3254
+ }
3255
+ if (message.type === "stop") {
3256
+ close().catch((cause) => {
3257
+ process.stderr.write(`[overmux] child shutdown failed: ${errorMessage(cause)}\n`);
3258
+ }).finally(disconnect);
3259
+ return;
3260
+ }
3261
+ if (serverPromise || closing) {
3262
+ send({
3263
+ message: "Server child received more than one start request",
3264
+ type: "startup-failed"
3265
+ });
3266
+ return;
3267
+ }
3268
+ serverPromise = startApplicationServer({
3269
+ onRestartRequested: () => send({ type: "restart-requested" }),
3270
+ options: message.options
3271
+ });
3272
+ serverPromise.then((server) => {
3273
+ if (closing) return;
3274
+ send({
3275
+ host: server.host,
3276
+ port: server.port,
3277
+ type: "ready",
3278
+ url: server.url,
3279
+ watch: server.watch
3280
+ });
3281
+ }).catch((cause) => {
3282
+ send({
3283
+ message: errorMessage(cause),
3284
+ type: "startup-failed"
3285
+ });
3286
+ disconnect();
3287
+ });
3288
+ });
3289
+ process.once("disconnect", () => {
3290
+ close().catch((cause) => {
3291
+ process.stderr.write(`[overmux] disconnected child shutdown failed: ${errorMessage(cause)}\n`);
3292
+ });
3293
+ });
3294
+ };
3295
+ runServerChild();
3296
+ //#endregion
3297
+ export {};
3298
+
3299
+ //# sourceMappingURL=server-child.js.map