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,414 @@
1
+ import type { HttpBindings } from "@hono/node-server";
2
+ import type { Context, Handler, MiddlewareHandler } from "hono";
3
+ import { generateCookie } from "hono/cookie";
4
+ import { bodyLimit } from "hono/body-limit";
5
+ import { parse as parseCookie } from "hono/utils/cookie";
6
+ import { z } from "zod";
7
+
8
+ import { createRateLimiter, hasRateLimitCapacity } from "../rate-limiter";
9
+ import type { AuthService, AuthenticatedSession } from "./auth-service";
10
+ import {
11
+ isBrowserNavigation,
12
+ loadAuthShell,
13
+ renderAuthShell,
14
+ serveAuthShellAsset,
15
+ } from "./auth-shell-http";
16
+
17
+ const maxLoginBodyBytes = 4_096;
18
+ // Login limits use a rolling window covering the preceding 60 seconds.
19
+ const loginWindowMs = 60_000;
20
+ const maxLoginRequestsPerWindow = 12;
21
+ const maxGlobalLoginRequestsPerWindow = 120;
22
+ const maxTrackedLoginAddresses = 10_000;
23
+ // Browser cookies cap non-expiring and longer sessions at 400 days.
24
+ const neverCookieLifetimeSeconds = 400 * 24 * 60 * 60;
25
+ const sessionCookieName = "overmux_session";
26
+
27
+ // Session kind records the intended client, while credential records how the
28
+ // request presented it. Only local + bearer and browser + cookie are accepted.
29
+ // This prevents a copied browser token from bypassing browser origin policy.
30
+ export type RequestSession = AuthenticatedSession & {
31
+ credential: "bearer" | "cookie";
32
+ };
33
+
34
+ export type AuthEnvironment = {
35
+ Variables: { session: RequestSession };
36
+ };
37
+
38
+ export type RequestAuthentication = {
39
+ authenticate: (
40
+ request: Request,
41
+ origin?: string,
42
+ ) => Promise<RequestSession | undefined>;
43
+ clearCookieHeader: (origin: string) => string;
44
+ cookieHeader: (origin: string, token: string, expiresAt?: string) => string;
45
+ };
46
+
47
+ export type RequestOriginInput = {
48
+ remoteAddress?: string;
49
+ request: Request;
50
+ };
51
+
52
+ // Groups the transport-neutral session service with the HTTP-specific policy
53
+ // needed to authenticate requests against the configured browser origins.
54
+ export type AuthBoundary = {
55
+ // Owns login grants, persisted sessions, token validation, and revocation.
56
+ service: AuthService;
57
+ // Resolves lazily because an ephemeral listening port is unknown until startup.
58
+ resolveOrigin: (input: RequestOriginInput) => string | undefined;
59
+ // Parses request credentials and creates or clears browser session cookies.
60
+ requests: RequestAuthentication;
61
+ };
62
+
63
+ const bearerToken = (header: string | null) =>
64
+ /^Bearer ([A-Za-z0-9_-]+)$/.exec(header ?? "")?.[1];
65
+
66
+ const isLoopbackHostname = (hostname: string) =>
67
+ hostname === "localhost" ||
68
+ hostname === "::1" ||
69
+ hostname === "[::1]" ||
70
+ /^127(?:\.\d{1,3}){3}$/.test(hostname);
71
+
72
+ export const validateAuthOrigin = (origin: string) => {
73
+ const url = new URL(origin);
74
+ if (url.origin !== origin) {
75
+ throw new Error("auth.origins must contain exact origins without paths");
76
+ }
77
+ if (
78
+ url.protocol !== "https:" &&
79
+ !(url.protocol === "http:" && isLoopbackHostname(url.hostname))
80
+ ) {
81
+ throw new Error("Non-loopback Overmux origins must use HTTPS");
82
+ }
83
+ return origin;
84
+ };
85
+
86
+ // Proxy chains commonly represent multiple previous values as a comma-separated list.
87
+ // Authentication accepts only one exact value with no surrounding whitespace.
88
+ const exactForwardedValue = (value: string | null) =>
89
+ value !== null && value === value.trim() && !value.includes(",")
90
+ ? value
91
+ : undefined;
92
+
93
+ // Resolve the browser-facing origin and return it only when explicitly configured.
94
+ export const resolveRequestOrigin = ({
95
+ origins,
96
+ remoteAddress,
97
+ request,
98
+ trustedProxyPeer,
99
+ }: RequestOriginInput & {
100
+ origins: string[];
101
+ trustedProxyPeer?: string;
102
+ }) => {
103
+ const requestUrl = new URL(request.url);
104
+
105
+ // A reverse proxy accepts the browser request, then opens a new backend request to Overmux.
106
+ // These headers report the browser-facing host, optional port, and protocol lost on that hop.
107
+ // They are ordinary spoofable headers until the immediate sender matches trustedProxyPeer.
108
+ const forwardedHost = request.headers.get("X-Forwarded-Host");
109
+ const forwardedProto = request.headers.get("X-Forwarded-Proto");
110
+
111
+ // If the trusted proxy supplies either header, require a complete valid pair.
112
+ // Invalid proxy metadata rejects the request instead of falling back to its direct origin.
113
+ if (
114
+ trustedProxyPeer !== undefined &&
115
+ remoteAddress === trustedProxyPeer &&
116
+ (forwardedHost !== null || forwardedProto !== null)
117
+ ) {
118
+ const host = exactForwardedValue(forwardedHost);
119
+ const proto = exactForwardedValue(forwardedProto);
120
+
121
+ // The supported proxy setup accepts external HTTPS, forwards over HTTP, and preserves Host.
122
+ // Requiring these values to agree rejects partial or contradictory origin descriptions.
123
+ if (
124
+ proto !== "https" ||
125
+ !host ||
126
+ requestUrl.protocol !== "http:" ||
127
+ requestUrl.host !== host
128
+ ) {
129
+ return;
130
+ }
131
+
132
+ // The proxy may describe the request only as one of the explicitly configured origins.
133
+ const forwardedOrigin = new URL(`https://${host}`).origin;
134
+ return origins.includes(forwardedOrigin) ? forwardedOrigin : undefined;
135
+ }
136
+
137
+ // Other requests use their direct URL; any untrusted forwarded headers are ignored.
138
+ return origins.includes(requestUrl.origin) ? requestUrl.origin : undefined;
139
+ };
140
+
141
+ const cookieMaxAge = (expiresAt?: string) =>
142
+ Math.min(
143
+ neverCookieLifetimeSeconds,
144
+ expiresAt
145
+ ? Math.max(0, Math.floor((Date.parse(expiresAt) - Date.now()) / 1_000))
146
+ : neverCookieLifetimeSeconds,
147
+ );
148
+
149
+ const generateSessionCookie = ({
150
+ origin,
151
+ token,
152
+ maxAge,
153
+ }: {
154
+ origin: string;
155
+ token: string;
156
+ maxAge: number;
157
+ }) =>
158
+ generateCookie(sessionCookieName, token, {
159
+ httpOnly: true,
160
+ maxAge,
161
+ path: "/",
162
+ sameSite: "Strict",
163
+ ...(new URL(origin).protocol === "https:"
164
+ ? { prefix: "host" as const }
165
+ : {}),
166
+ });
167
+
168
+ const createRequestAuthentication = ({
169
+ service,
170
+ }: {
171
+ service: AuthService;
172
+ }): RequestAuthentication => {
173
+ // Browser sessions use HttpOnly, SameSite=Strict cookies. On HTTPS the
174
+ // browser-enforced __Host- prefix also requires Secure, Path=/, and no Domain,
175
+ // making the cookie host-only and resistant to subdomain replacement.
176
+ const cookieName = (origin: string) =>
177
+ new URL(origin).protocol === "https:"
178
+ ? `__Host-${sessionCookieName}`
179
+ : sessionCookieName;
180
+ const cookieHeader = (origin: string, token: string, expiresAt?: string) =>
181
+ generateSessionCookie({
182
+ maxAge: cookieMaxAge(expiresAt),
183
+ origin,
184
+ token,
185
+ });
186
+ return {
187
+ authenticate: async (request, origin) => {
188
+ // Authorization is authoritative: an invalid header must not silently
189
+ // fall back to a valid ambient browser cookie.
190
+ const authorizationHeader = request.headers.get("Authorization");
191
+ if (authorizationHeader !== null) {
192
+ const token = bearerToken(authorizationHeader);
193
+ const authenticatedSession = token
194
+ ? await service.authenticateToken(token)
195
+ : undefined;
196
+ return authenticatedSession?.kind === "local"
197
+ ? { ...authenticatedSession, credential: "bearer" }
198
+ : undefined;
199
+ }
200
+ if (!origin) {
201
+ return;
202
+ }
203
+ const name = cookieName(origin);
204
+ const token = parseCookie(request.headers.get("Cookie") ?? "", name)[
205
+ name
206
+ ];
207
+ const authenticatedSession = token
208
+ ? await service.authenticateToken(token, origin)
209
+ : undefined;
210
+ return authenticatedSession?.kind === "browser"
211
+ ? { ...authenticatedSession, credential: "cookie" }
212
+ : undefined;
213
+ },
214
+ clearCookieHeader: (origin) =>
215
+ generateSessionCookie({ maxAge: 0, origin, token: "" }),
216
+ cookieHeader,
217
+ };
218
+ };
219
+
220
+ export const createAuthBoundary = ({
221
+ origins,
222
+ service,
223
+ trustedProxyPeer,
224
+ }: {
225
+ origins: () => string[];
226
+ service: AuthService;
227
+ trustedProxyPeer?: string;
228
+ }): AuthBoundary => ({
229
+ requests: createRequestAuthentication({ service }),
230
+ resolveOrigin: (input) =>
231
+ resolveRequestOrigin({
232
+ ...input,
233
+ origins: origins(),
234
+ trustedProxyPeer,
235
+ }),
236
+ service,
237
+ });
238
+
239
+ const loginInputSchema = z.union([
240
+ z.object({ code: z.string() }).strict(),
241
+ z.object({ ticket: z.string() }).strict(),
242
+ ]);
243
+
244
+ const parseLoginBody = async (request: Request) => {
245
+ if (
246
+ request.headers.get("Content-Type")?.split(";", 1)[0]?.toLowerCase() !==
247
+ "application/json"
248
+ ) {
249
+ return;
250
+ }
251
+ try {
252
+ return loginInputSchema.safeParse(await request.json()).data;
253
+ } catch {
254
+ return;
255
+ }
256
+ };
257
+
258
+ const hasTrustedBrowserHeaders = (request: Request, origin: string) =>
259
+ request.headers.get("Origin") === origin &&
260
+ request.headers.get("Sec-Fetch-Site") === "same-origin";
261
+
262
+ const changesState = (request: Request) =>
263
+ !["GET", "HEAD", "OPTIONS"].includes(request.method);
264
+
265
+ const requestOrigin = (auth: AuthBoundary, context: Context<AuthEnvironment>) =>
266
+ auth.resolveOrigin({
267
+ remoteAddress: (context.env as Partial<HttpBindings> | undefined)?.incoming
268
+ ?.socket.remoteAddress,
269
+ request: context.req.raw,
270
+ });
271
+
272
+ type HttpAuthenticationHandlers = {
273
+ asset: Handler<AuthEnvironment>;
274
+ authenticate: MiddlewareHandler<AuthEnvironment>;
275
+ login: Handler<AuthEnvironment>;
276
+ loginBodyLimitMiddleware: MiddlewareHandler<AuthEnvironment>;
277
+ loginPage: Handler<AuthEnvironment>;
278
+ logout: Handler<AuthEnvironment>;
279
+ state: Handler<AuthEnvironment>;
280
+ };
281
+
282
+ // Browser login and cookie-authenticated mutations require the effective
283
+ // configured Origin plus same-origin Fetch Metadata. Bearer credentials are explicit rather
284
+ // than ambient browser credentials, so they do not use the browser CSRF check.
285
+ export const createHttpAuthenticationHandlers = ({
286
+ auth,
287
+ authAssetsDirectory,
288
+ }: {
289
+ auth: AuthBoundary;
290
+ authAssetsDirectory?: string;
291
+ }): HttpAuthenticationHandlers => {
292
+ const authShell = loadAuthShell(authAssetsDirectory);
293
+ const loginRateLimiter = createRateLimiter({
294
+ maxAttemptsPerKey: maxLoginRequestsPerWindow,
295
+ maxGlobalAttempts: maxGlobalLoginRequestsPerWindow,
296
+ maxTrackedKeys: maxTrackedLoginAddresses,
297
+ windowMs: loginWindowMs,
298
+ });
299
+
300
+ return {
301
+ asset: (context) => serveAuthShellAsset(context, authShell),
302
+ state: async (context) => {
303
+ context.header("Cache-Control", "no-store");
304
+ const origin = requestOrigin(auth, context);
305
+ const session = await auth.requests.authenticate(context.req.raw, origin);
306
+ if (session?.credential === "cookie" && origin) {
307
+ context.header(
308
+ "Set-Cookie",
309
+ auth.requests.cookieHeader(origin, session.token, session.expiresAt),
310
+ );
311
+ }
312
+ return context.json({ authenticated: Boolean(session) });
313
+ },
314
+ loginBodyLimitMiddleware: bodyLimit({
315
+ maxSize: maxLoginBodyBytes,
316
+ onError: (context) =>
317
+ context.json({ error: "Login request is too large" }, 413),
318
+ }),
319
+ login: async (context) => {
320
+ context.header("Cache-Control", "no-store");
321
+ const origin = requestOrigin(auth, context);
322
+ if (!origin || !hasTrustedBrowserHeaders(context.req.raw, origin)) {
323
+ return context.json(
324
+ { error: "Login request origin was rejected" },
325
+ 403,
326
+ );
327
+ }
328
+ const address =
329
+ (context.env as Partial<HttpBindings> | undefined)?.incoming?.socket
330
+ .remoteAddress ?? "unknown";
331
+ if (!hasRateLimitCapacity({ key: address, limiter: loginRateLimiter })) {
332
+ return context.json({ status: "rate-limited" }, 429);
333
+ }
334
+ const input = await parseLoginBody(context.req.raw);
335
+ if (!input) {
336
+ return context.json({ error: "Invalid login request" }, 400);
337
+ }
338
+ const result = await auth.service.consumeLoginGrant({ ...input, origin });
339
+ if (result.status !== "authenticated") {
340
+ return context.json(
341
+ { status: result.status },
342
+ result.status === "rate-limited" ? 429 : 401,
343
+ );
344
+ }
345
+ context.header(
346
+ "Set-Cookie",
347
+ auth.requests.cookieHeader(
348
+ origin,
349
+ result.token,
350
+ result.session.expiresAt,
351
+ ),
352
+ );
353
+ return context.json({ authenticated: true });
354
+ },
355
+ loginPage: async (context) => {
356
+ const origin = requestOrigin(auth, context);
357
+ const session = await auth.requests.authenticate(context.req.raw, origin);
358
+ if (session) {
359
+ return context.redirect("/");
360
+ }
361
+ return renderAuthShell(context, authShell);
362
+ },
363
+ authenticate: async (context, next) => {
364
+ const origin = requestOrigin(auth, context);
365
+ let session: RequestSession | undefined;
366
+ try {
367
+ session = await auth.requests.authenticate(context.req.raw, origin);
368
+ } catch {
369
+ context.header("Cache-Control", "no-store");
370
+ return context.json(
371
+ { error: "Authentication service unavailable" },
372
+ 503,
373
+ );
374
+ }
375
+ if (!session) {
376
+ if (
377
+ !context.req.path.startsWith("/api/") &&
378
+ isBrowserNavigation(context.req.raw)
379
+ ) {
380
+ return renderAuthShell(context, authShell, 401);
381
+ }
382
+ context.header("Cache-Control", "no-store");
383
+ return context.json({ error: "Authentication required" }, 401);
384
+ }
385
+ context.set("session", session);
386
+ context.header("Cache-Control", "private, no-store");
387
+ if (session.credential === "cookie") {
388
+ if (
389
+ !origin ||
390
+ (changesState(context.req.raw) &&
391
+ !hasTrustedBrowserHeaders(context.req.raw, origin))
392
+ ) {
393
+ return context.json({ error: "Request origin was rejected" }, 403);
394
+ }
395
+ context.header(
396
+ "Set-Cookie",
397
+ auth.requests.cookieHeader(origin, session.token, session.expiresAt),
398
+ );
399
+ }
400
+ await next();
401
+ },
402
+ logout: async (context) => {
403
+ const session = context.get("session");
404
+ await auth.service.revokeSession(session.id);
405
+ if (session.origin) {
406
+ context.header(
407
+ "Set-Cookie",
408
+ auth.requests.clearCookieHeader(session.origin),
409
+ );
410
+ }
411
+ return context.json({ authenticated: false });
412
+ },
413
+ };
414
+ };