rivetkit 2.0.1 → 2.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (253) hide show
  1. package/dist/schemas/actor-persist/v1.ts +228 -0
  2. package/dist/schemas/client-protocol/v1.ts +429 -0
  3. package/dist/schemas/file-system-driver/v1.ts +102 -0
  4. package/dist/tsup/actor/errors.cjs +69 -0
  5. package/dist/tsup/actor/errors.cjs.map +1 -0
  6. package/dist/tsup/actor/errors.d.cts +143 -0
  7. package/dist/tsup/actor/errors.d.ts +143 -0
  8. package/dist/tsup/actor/errors.js +69 -0
  9. package/dist/tsup/actor/errors.js.map +1 -0
  10. package/dist/tsup/chunk-2CRLFV6Z.cjs +202 -0
  11. package/dist/tsup/chunk-2CRLFV6Z.cjs.map +1 -0
  12. package/dist/tsup/chunk-3H7O2A7I.js +525 -0
  13. package/dist/tsup/chunk-3H7O2A7I.js.map +1 -0
  14. package/dist/tsup/chunk-42I3OZ3Q.js +15 -0
  15. package/dist/tsup/chunk-42I3OZ3Q.js.map +1 -0
  16. package/dist/tsup/chunk-4NSUQZ2H.js +1790 -0
  17. package/dist/tsup/chunk-4NSUQZ2H.js.map +1 -0
  18. package/dist/tsup/chunk-6PDXBYI5.js +132 -0
  19. package/dist/tsup/chunk-6PDXBYI5.js.map +1 -0
  20. package/dist/tsup/chunk-6WKQDDUD.cjs +1790 -0
  21. package/dist/tsup/chunk-6WKQDDUD.cjs.map +1 -0
  22. package/dist/tsup/chunk-CTBOSFUH.cjs +116 -0
  23. package/dist/tsup/chunk-CTBOSFUH.cjs.map +1 -0
  24. package/dist/tsup/chunk-EGVZZFE2.js +2857 -0
  25. package/dist/tsup/chunk-EGVZZFE2.js.map +1 -0
  26. package/dist/tsup/chunk-FCCPJNMA.cjs +132 -0
  27. package/dist/tsup/chunk-FCCPJNMA.cjs.map +1 -0
  28. package/dist/tsup/chunk-FLMTTN27.js +244 -0
  29. package/dist/tsup/chunk-FLMTTN27.js.map +1 -0
  30. package/dist/tsup/chunk-GIR3AFFI.cjs +315 -0
  31. package/dist/tsup/chunk-GIR3AFFI.cjs.map +1 -0
  32. package/dist/tsup/chunk-INGJP237.js +315 -0
  33. package/dist/tsup/chunk-INGJP237.js.map +1 -0
  34. package/dist/tsup/chunk-KJCJLKRM.js +116 -0
  35. package/dist/tsup/chunk-KJCJLKRM.js.map +1 -0
  36. package/dist/tsup/chunk-KUPQZYUQ.cjs +15 -0
  37. package/dist/tsup/chunk-KUPQZYUQ.cjs.map +1 -0
  38. package/dist/tsup/chunk-O2MBYIXO.cjs +2857 -0
  39. package/dist/tsup/chunk-O2MBYIXO.cjs.map +1 -0
  40. package/dist/tsup/chunk-OGAPU3UG.cjs +525 -0
  41. package/dist/tsup/chunk-OGAPU3UG.cjs.map +1 -0
  42. package/dist/tsup/chunk-OV6AYD4S.js +4406 -0
  43. package/dist/tsup/chunk-OV6AYD4S.js.map +1 -0
  44. package/dist/tsup/chunk-PO4VLDWA.js +47 -0
  45. package/dist/tsup/chunk-PO4VLDWA.js.map +1 -0
  46. package/dist/tsup/chunk-R2OPSKIV.cjs +244 -0
  47. package/dist/tsup/chunk-R2OPSKIV.cjs.map +1 -0
  48. package/dist/tsup/chunk-TZJKSBUQ.cjs +47 -0
  49. package/dist/tsup/chunk-TZJKSBUQ.cjs.map +1 -0
  50. package/dist/tsup/chunk-UBUC5C3G.cjs +189 -0
  51. package/dist/tsup/chunk-UBUC5C3G.cjs.map +1 -0
  52. package/dist/tsup/chunk-UIM22YJL.cjs +4406 -0
  53. package/dist/tsup/chunk-UIM22YJL.cjs.map +1 -0
  54. package/dist/tsup/chunk-URVFQMYI.cjs +230 -0
  55. package/dist/tsup/chunk-URVFQMYI.cjs.map +1 -0
  56. package/dist/tsup/chunk-UVUPOS46.js +230 -0
  57. package/dist/tsup/chunk-UVUPOS46.js.map +1 -0
  58. package/dist/tsup/chunk-VRRHBNJC.js +189 -0
  59. package/dist/tsup/chunk-VRRHBNJC.js.map +1 -0
  60. package/dist/tsup/chunk-XFSS33EQ.js +202 -0
  61. package/dist/tsup/chunk-XFSS33EQ.js.map +1 -0
  62. package/dist/tsup/client/mod.cjs +32 -0
  63. package/dist/tsup/client/mod.cjs.map +1 -0
  64. package/dist/tsup/client/mod.d.cts +26 -0
  65. package/dist/tsup/client/mod.d.ts +26 -0
  66. package/dist/tsup/client/mod.js +32 -0
  67. package/dist/tsup/client/mod.js.map +1 -0
  68. package/dist/tsup/common/log.cjs +13 -0
  69. package/dist/tsup/common/log.cjs.map +1 -0
  70. package/dist/tsup/common/log.d.cts +20 -0
  71. package/dist/tsup/common/log.d.ts +20 -0
  72. package/dist/tsup/common/log.js +13 -0
  73. package/dist/tsup/common/log.js.map +1 -0
  74. package/dist/tsup/common/websocket.cjs +10 -0
  75. package/dist/tsup/common/websocket.cjs.map +1 -0
  76. package/dist/tsup/common/websocket.d.cts +3 -0
  77. package/dist/tsup/common/websocket.d.ts +3 -0
  78. package/dist/tsup/common/websocket.js +10 -0
  79. package/dist/tsup/common/websocket.js.map +1 -0
  80. package/dist/tsup/common-CpqORuCq.d.cts +218 -0
  81. package/dist/tsup/common-CpqORuCq.d.ts +218 -0
  82. package/dist/tsup/connection-BR_Ve4ku.d.cts +2117 -0
  83. package/dist/tsup/connection-BwUMoe6n.d.ts +2117 -0
  84. package/dist/tsup/driver-helpers/mod.cjs +33 -0
  85. package/dist/tsup/driver-helpers/mod.cjs.map +1 -0
  86. package/dist/tsup/driver-helpers/mod.d.cts +18 -0
  87. package/dist/tsup/driver-helpers/mod.d.ts +18 -0
  88. package/dist/tsup/driver-helpers/mod.js +33 -0
  89. package/dist/tsup/driver-helpers/mod.js.map +1 -0
  90. package/dist/tsup/driver-test-suite/mod.cjs +4619 -0
  91. package/dist/tsup/driver-test-suite/mod.cjs.map +1 -0
  92. package/dist/tsup/driver-test-suite/mod.d.cts +57 -0
  93. package/dist/tsup/driver-test-suite/mod.d.ts +57 -0
  94. package/dist/tsup/driver-test-suite/mod.js +4619 -0
  95. package/dist/tsup/driver-test-suite/mod.js.map +1 -0
  96. package/dist/tsup/inspector/mod.cjs +53 -0
  97. package/dist/tsup/inspector/mod.cjs.map +1 -0
  98. package/dist/tsup/inspector/mod.d.cts +408 -0
  99. package/dist/tsup/inspector/mod.d.ts +408 -0
  100. package/dist/tsup/inspector/mod.js +53 -0
  101. package/dist/tsup/inspector/mod.js.map +1 -0
  102. package/dist/tsup/mod.cjs +73 -0
  103. package/dist/tsup/mod.cjs.map +1 -0
  104. package/dist/tsup/mod.d.cts +100 -0
  105. package/dist/tsup/mod.d.ts +100 -0
  106. package/dist/tsup/mod.js +73 -0
  107. package/dist/tsup/mod.js.map +1 -0
  108. package/dist/tsup/router-endpoints-AYkXG8Tl.d.cts +66 -0
  109. package/dist/tsup/router-endpoints-DAbqVFx2.d.ts +66 -0
  110. package/dist/tsup/test/mod.cjs +21 -0
  111. package/dist/tsup/test/mod.cjs.map +1 -0
  112. package/dist/tsup/test/mod.d.cts +27 -0
  113. package/dist/tsup/test/mod.d.ts +27 -0
  114. package/dist/tsup/test/mod.js +21 -0
  115. package/dist/tsup/test/mod.js.map +1 -0
  116. package/dist/tsup/utils-CT0cv4jd.d.cts +17 -0
  117. package/dist/tsup/utils-CT0cv4jd.d.ts +17 -0
  118. package/dist/tsup/utils.cjs +26 -0
  119. package/dist/tsup/utils.cjs.map +1 -0
  120. package/dist/tsup/utils.d.cts +36 -0
  121. package/dist/tsup/utils.d.ts +36 -0
  122. package/dist/tsup/utils.js +26 -0
  123. package/dist/tsup/utils.js.map +1 -0
  124. package/package.json +208 -5
  125. package/src/actor/action.ts +182 -0
  126. package/src/actor/config.ts +765 -0
  127. package/src/actor/connection.ts +260 -0
  128. package/src/actor/context.ts +171 -0
  129. package/src/actor/database.ts +23 -0
  130. package/src/actor/definition.ts +86 -0
  131. package/src/actor/driver.ts +84 -0
  132. package/src/actor/errors.ts +360 -0
  133. package/src/actor/generic-conn-driver.ts +234 -0
  134. package/src/actor/instance.ts +1800 -0
  135. package/src/actor/log.ts +15 -0
  136. package/src/actor/mod.ts +113 -0
  137. package/src/actor/persisted.ts +42 -0
  138. package/src/actor/protocol/old.ts +281 -0
  139. package/src/actor/protocol/serde.ts +131 -0
  140. package/src/actor/router-endpoints.ts +685 -0
  141. package/src/actor/router.ts +263 -0
  142. package/src/actor/schedule.ts +17 -0
  143. package/src/actor/unstable-react.ts +110 -0
  144. package/src/actor/utils.ts +98 -0
  145. package/src/client/actor-common.ts +30 -0
  146. package/src/client/actor-conn.ts +804 -0
  147. package/src/client/actor-handle.ts +208 -0
  148. package/src/client/client.ts +623 -0
  149. package/src/client/errors.ts +41 -0
  150. package/src/client/http-client-driver.ts +326 -0
  151. package/src/client/log.ts +7 -0
  152. package/src/client/mod.ts +56 -0
  153. package/src/client/raw-utils.ts +92 -0
  154. package/src/client/test.ts +44 -0
  155. package/src/client/utils.ts +150 -0
  156. package/src/common/eventsource-interface.ts +47 -0
  157. package/src/common/eventsource.ts +80 -0
  158. package/src/common/fake-event-source.ts +266 -0
  159. package/src/common/inline-websocket-adapter2.ts +445 -0
  160. package/src/common/log-levels.ts +27 -0
  161. package/src/common/log.ts +139 -0
  162. package/src/common/logfmt.ts +228 -0
  163. package/src/common/network.ts +2 -0
  164. package/src/common/router.ts +87 -0
  165. package/src/common/utils.ts +322 -0
  166. package/src/common/versioned-data.ts +95 -0
  167. package/src/common/websocket-interface.ts +49 -0
  168. package/src/common/websocket.ts +43 -0
  169. package/src/driver-helpers/mod.ts +22 -0
  170. package/src/driver-helpers/utils.ts +17 -0
  171. package/src/driver-test-suite/log.ts +7 -0
  172. package/src/driver-test-suite/mod.ts +213 -0
  173. package/src/driver-test-suite/test-inline-client-driver.ts +402 -0
  174. package/src/driver-test-suite/tests/action-features.ts +136 -0
  175. package/src/driver-test-suite/tests/actor-auth.ts +591 -0
  176. package/src/driver-test-suite/tests/actor-conn-state.ts +249 -0
  177. package/src/driver-test-suite/tests/actor-conn.ts +349 -0
  178. package/src/driver-test-suite/tests/actor-driver.ts +25 -0
  179. package/src/driver-test-suite/tests/actor-error-handling.ts +158 -0
  180. package/src/driver-test-suite/tests/actor-handle.ts +259 -0
  181. package/src/driver-test-suite/tests/actor-inline-client.ts +152 -0
  182. package/src/driver-test-suite/tests/actor-inspector.ts +570 -0
  183. package/src/driver-test-suite/tests/actor-metadata.ts +116 -0
  184. package/src/driver-test-suite/tests/actor-onstatechange.ts +95 -0
  185. package/src/driver-test-suite/tests/actor-schedule.ts +108 -0
  186. package/src/driver-test-suite/tests/actor-sleep.ts +413 -0
  187. package/src/driver-test-suite/tests/actor-state.ts +54 -0
  188. package/src/driver-test-suite/tests/actor-vars.ts +93 -0
  189. package/src/driver-test-suite/tests/manager-driver.ts +365 -0
  190. package/src/driver-test-suite/tests/raw-http-direct-registry.ts +226 -0
  191. package/src/driver-test-suite/tests/raw-http-request-properties.ts +414 -0
  192. package/src/driver-test-suite/tests/raw-http.ts +347 -0
  193. package/src/driver-test-suite/tests/raw-websocket-direct-registry.ts +392 -0
  194. package/src/driver-test-suite/tests/raw-websocket.ts +484 -0
  195. package/src/driver-test-suite/tests/request-access.ts +244 -0
  196. package/src/driver-test-suite/utils.ts +68 -0
  197. package/src/drivers/default.ts +31 -0
  198. package/src/drivers/engine/actor-driver.ts +360 -0
  199. package/src/drivers/engine/api-endpoints.ts +128 -0
  200. package/src/drivers/engine/api-utils.ts +70 -0
  201. package/src/drivers/engine/config.ts +39 -0
  202. package/src/drivers/engine/keys.test.ts +266 -0
  203. package/src/drivers/engine/keys.ts +89 -0
  204. package/src/drivers/engine/kv.ts +3 -0
  205. package/src/drivers/engine/log.ts +7 -0
  206. package/src/drivers/engine/manager-driver.ts +391 -0
  207. package/src/drivers/engine/mod.ts +36 -0
  208. package/src/drivers/engine/ws-proxy.ts +170 -0
  209. package/src/drivers/file-system/actor.ts +91 -0
  210. package/src/drivers/file-system/global-state.ts +673 -0
  211. package/src/drivers/file-system/log.ts +7 -0
  212. package/src/drivers/file-system/manager.ts +306 -0
  213. package/src/drivers/file-system/mod.ts +48 -0
  214. package/src/drivers/file-system/utils.ts +109 -0
  215. package/src/globals.d.ts +6 -0
  216. package/src/inline-client-driver/log.ts +7 -0
  217. package/src/inline-client-driver/mod.ts +385 -0
  218. package/src/inspector/actor.ts +298 -0
  219. package/src/inspector/config.ts +83 -0
  220. package/src/inspector/log.ts +5 -0
  221. package/src/inspector/manager.ts +86 -0
  222. package/src/inspector/mod.ts +2 -0
  223. package/src/inspector/protocol/actor.ts +10 -0
  224. package/src/inspector/protocol/common.ts +196 -0
  225. package/src/inspector/protocol/manager.ts +10 -0
  226. package/src/inspector/protocol/mod.ts +2 -0
  227. package/src/inspector/utils.ts +76 -0
  228. package/src/manager/auth.ts +121 -0
  229. package/src/manager/driver.ts +80 -0
  230. package/src/manager/hono-websocket-adapter.ts +333 -0
  231. package/src/manager/log.ts +7 -0
  232. package/src/manager/mod.ts +2 -0
  233. package/src/manager/protocol/mod.ts +24 -0
  234. package/src/manager/protocol/query.ts +89 -0
  235. package/src/manager/router.ts +1792 -0
  236. package/src/mod.ts +20 -0
  237. package/src/registry/config.ts +32 -0
  238. package/src/registry/log.ts +7 -0
  239. package/src/registry/mod.ts +124 -0
  240. package/src/registry/run-config.ts +54 -0
  241. package/src/registry/serve.ts +53 -0
  242. package/src/schemas/actor-persist/mod.ts +1 -0
  243. package/src/schemas/actor-persist/versioned.ts +25 -0
  244. package/src/schemas/client-protocol/mod.ts +1 -0
  245. package/src/schemas/client-protocol/versioned.ts +63 -0
  246. package/src/schemas/file-system-driver/mod.ts +1 -0
  247. package/src/schemas/file-system-driver/versioned.ts +28 -0
  248. package/src/serde.ts +84 -0
  249. package/src/test/config.ts +16 -0
  250. package/src/test/log.ts +7 -0
  251. package/src/test/mod.ts +153 -0
  252. package/src/utils.ts +172 -0
  253. package/README.md +0 -13
@@ -0,0 +1,1792 @@
1
+ import { createRoute, OpenAPIHono } from "@hono/zod-openapi";
2
+ import * as cbor from "cbor-x";
3
+ import {
4
+ Hono,
5
+ type Context as HonoContext,
6
+ type MiddlewareHandler,
7
+ } from "hono";
8
+ import { cors } from "hono/cors";
9
+ import { streamSSE } from "hono/streaming";
10
+ import type { WSContext } from "hono/ws";
11
+ import invariant from "invariant";
12
+ import type { CloseEvent, MessageEvent, WebSocket } from "ws";
13
+ import { z } from "zod";
14
+ import * as errors from "@/actor/errors";
15
+ import type { Transport } from "@/actor/protocol/old";
16
+ import type { Encoding } from "@/actor/protocol/serde";
17
+ import {
18
+ PATH_CONNECT_WEBSOCKET,
19
+ PATH_RAW_WEBSOCKET_PREFIX,
20
+ } from "@/actor/router";
21
+ import {
22
+ ALLOWED_PUBLIC_HEADERS,
23
+ getRequestEncoding,
24
+ getRequestQuery,
25
+ HEADER_ACTOR_ID,
26
+ HEADER_ACTOR_QUERY,
27
+ HEADER_AUTH_DATA,
28
+ HEADER_CONN_ID,
29
+ HEADER_CONN_PARAMS,
30
+ HEADER_CONN_TOKEN,
31
+ HEADER_ENCODING,
32
+ } from "@/actor/router-endpoints";
33
+ import type { ClientDriver } from "@/client/client";
34
+ import {
35
+ handleRouteError,
36
+ handleRouteNotFound,
37
+ loggerMiddleware,
38
+ } from "@/common/router";
39
+ import {
40
+ type DeconstructedError,
41
+ deconstructError,
42
+ noopNext,
43
+ stringifyError,
44
+ } from "@/common/utils";
45
+ import { createManagerInspectorRouter } from "@/inspector/manager";
46
+ import { secureInspector } from "@/inspector/utils";
47
+ import type { UpgradeWebSocketArgs } from "@/mod";
48
+ import type { RegistryConfig } from "@/registry/config";
49
+ import type { RunConfig } from "@/registry/run-config";
50
+ import type * as protocol from "@/schemas/client-protocol/mod";
51
+ import {
52
+ HTTP_RESOLVE_RESPONSE_VERSIONED,
53
+ TO_CLIENT_VERSIONED,
54
+ } from "@/schemas/client-protocol/versioned";
55
+ import { serializeWithEncoding } from "@/serde";
56
+ import { bufferToArrayBuffer } from "@/utils";
57
+ import { authenticateEndpoint } from "./auth";
58
+ import type { ManagerDriver } from "./driver";
59
+ import { logger } from "./log";
60
+ import type { ActorQuery } from "./protocol/query";
61
+ import {
62
+ ActorQuerySchema,
63
+ ConnectRequestSchema,
64
+ ConnectWebSocketRequestSchema,
65
+ ConnMessageRequestSchema,
66
+ ResolveRequestSchema,
67
+ } from "./protocol/query";
68
+
69
+ /**
70
+ * Parse WebSocket protocol headers for query and connection parameters
71
+ */
72
+ function parseWebSocketProtocols(protocols: string | undefined): {
73
+ queryRaw: string | undefined;
74
+ encodingRaw: string | undefined;
75
+ connParamsRaw: string | undefined;
76
+ } {
77
+ let queryRaw: string | undefined;
78
+ let encodingRaw: string | undefined;
79
+ let connParamsRaw: string | undefined;
80
+
81
+ if (protocols) {
82
+ const protocolList = protocols.split(",").map((p) => p.trim());
83
+ for (const protocol of protocolList) {
84
+ if (protocol.startsWith("query.")) {
85
+ queryRaw = decodeURIComponent(protocol.substring("query.".length));
86
+ } else if (protocol.startsWith("encoding.")) {
87
+ encodingRaw = protocol.substring("encoding.".length);
88
+ } else if (protocol.startsWith("conn_params.")) {
89
+ connParamsRaw = decodeURIComponent(
90
+ protocol.substring("conn_params.".length),
91
+ );
92
+ }
93
+ }
94
+ }
95
+
96
+ return { queryRaw, encodingRaw, connParamsRaw };
97
+ }
98
+
99
+ const OPENAPI_ENCODING = z.string().openapi({
100
+ description: "The encoding format to use for the response (json, cbor)",
101
+ example: "json",
102
+ });
103
+
104
+ const OPENAPI_ACTOR_QUERY = z.string().openapi({
105
+ description: "Actor query information",
106
+ });
107
+
108
+ const OPENAPI_CONN_PARAMS = z.string().openapi({
109
+ description: "Connection parameters",
110
+ });
111
+
112
+ const OPENAPI_ACTOR_ID = z.string().openapi({
113
+ description: "Actor ID (used in some endpoints)",
114
+ example: "actor-123456",
115
+ });
116
+
117
+ const OPENAPI_CONN_ID = z.string().openapi({
118
+ description: "Connection ID",
119
+ example: "conn-123456",
120
+ });
121
+
122
+ const OPENAPI_CONN_TOKEN = z.string().openapi({
123
+ description: "Connection token",
124
+ });
125
+
126
+ function buildOpenApiResponses<T>(schema: T, validateBody: boolean) {
127
+ return {
128
+ 200: {
129
+ description: "Success",
130
+ content: validateBody
131
+ ? {
132
+ "application/json": {
133
+ schema,
134
+ },
135
+ }
136
+ : {},
137
+ },
138
+ 400: {
139
+ description: "User error",
140
+ },
141
+ 500: {
142
+ description: "Internal error",
143
+ },
144
+ };
145
+ }
146
+
147
+ /**
148
+ * Only use `validateBody` to `true` if you need to export OpenAPI JSON.
149
+ *
150
+ * If left enabled for production, this will cause errors. We disable JSON validation since:
151
+ * - It prevents us from proxying requests, since validating the body requires consuming the body so we can't forward the body
152
+ * - We validate all types at the actor router layer since most requests are proxied
153
+ */
154
+ export function createManagerRouter(
155
+ registryConfig: RegistryConfig,
156
+ runConfig: RunConfig,
157
+ inlineClientDriver: ClientDriver,
158
+ managerDriver: ManagerDriver,
159
+ validateBody: boolean,
160
+ ): { router: Hono; openapi: OpenAPIHono } {
161
+ const router = new OpenAPIHono({ strict: false }).basePath(
162
+ runConfig.basePath,
163
+ );
164
+
165
+ router.use("*", loggerMiddleware(logger()));
166
+
167
+ if (runConfig.cors || runConfig.inspector?.cors) {
168
+ router.use("*", async (c, next) => {
169
+ // Don't apply to WebSocket routes
170
+ // HACK: This could be insecure if we had a varargs path. We have to check the path suffix for WS since we don't know the path that this router was mounted.
171
+ // HACK: Checking "/websocket/" is not safe, but there is no other way to handle this if we don't know the base path this is
172
+ // mounted on
173
+ const path = c.req.path;
174
+ if (
175
+ path.endsWith("/actors/connect/websocket") ||
176
+ path.includes("/actors/raw/websocket/") ||
177
+ // inspectors implement their own CORS handling
178
+ path.endsWith("/inspect") ||
179
+ path.endsWith("/actors/inspect")
180
+ ) {
181
+ return next();
182
+ }
183
+
184
+ return cors({
185
+ ...(runConfig.cors ?? {}),
186
+ ...(runConfig.inspector?.cors ?? {}),
187
+ origin: (origin, c) => {
188
+ const inspectorOrigin = runConfig.inspector?.cors?.origin;
189
+
190
+ if (inspectorOrigin !== undefined) {
191
+ if (typeof inspectorOrigin === "function") {
192
+ const allowed = inspectorOrigin(origin, c);
193
+ if (allowed) return allowed;
194
+ // Proceed to next CORS config if none provided
195
+ } else if (Array.isArray(inspectorOrigin)) {
196
+ return inspectorOrigin.includes(origin) ? origin : undefined;
197
+ } else {
198
+ return inspectorOrigin;
199
+ }
200
+ }
201
+
202
+ if (runConfig.cors?.origin !== undefined) {
203
+ if (typeof runConfig.cors.origin === "function") {
204
+ const allowed = runConfig.cors.origin(origin, c);
205
+ if (allowed) return allowed;
206
+ } else {
207
+ return runConfig.cors.origin as string;
208
+ }
209
+ }
210
+
211
+ return null;
212
+ },
213
+ allowMethods: (origin, c) => {
214
+ const inspectorMethods = runConfig.inspector?.cors?.allowMethods;
215
+ if (inspectorMethods) {
216
+ if (typeof inspectorMethods === "function") {
217
+ return inspectorMethods(origin, c);
218
+ }
219
+ return inspectorMethods;
220
+ }
221
+
222
+ if (runConfig.cors?.allowMethods) {
223
+ if (typeof runConfig.cors.allowMethods === "function") {
224
+ return runConfig.cors.allowMethods(origin, c);
225
+ }
226
+ return runConfig.cors.allowMethods;
227
+ }
228
+
229
+ return [];
230
+ },
231
+ allowHeaders: [
232
+ ...(runConfig.cors?.allowHeaders ?? []),
233
+ ...(runConfig.inspector?.cors?.allowHeaders ?? []),
234
+ ...ALLOWED_PUBLIC_HEADERS,
235
+ "Content-Type",
236
+ "User-Agent",
237
+ ],
238
+ credentials:
239
+ runConfig.cors?.credentials ??
240
+ runConfig.inspector?.cors?.credentials ??
241
+ true,
242
+ })(c, next);
243
+ });
244
+ }
245
+
246
+ // GET /
247
+ router.get("/", (c: HonoContext) => {
248
+ return c.text(
249
+ "This is an RivetKit registry.\n\nLearn more at https://rivetkit.org",
250
+ );
251
+ });
252
+
253
+ // POST /actors/resolve
254
+ {
255
+ const ResolveQuerySchema = z
256
+ .object({
257
+ query: z.any().openapi({
258
+ example: { getForId: { actorId: "actor-123" } },
259
+ }),
260
+ })
261
+ .openapi("ResolveQuery");
262
+
263
+ const ResolveResponseSchema = z
264
+ .object({
265
+ i: z.string().openapi({
266
+ example: "actor-123",
267
+ }),
268
+ })
269
+ .openapi("ResolveResponse");
270
+
271
+ const resolveRoute = createRoute({
272
+ method: "post",
273
+ path: "/actors/resolve",
274
+ request: {
275
+ body: {
276
+ content: validateBody
277
+ ? {
278
+ "application/json": {
279
+ schema: ResolveQuerySchema,
280
+ },
281
+ }
282
+ : {},
283
+ },
284
+ headers: z.object({
285
+ [HEADER_ACTOR_QUERY]: OPENAPI_ACTOR_QUERY,
286
+ }),
287
+ },
288
+ responses: buildOpenApiResponses(ResolveResponseSchema, validateBody),
289
+ });
290
+
291
+ router.openapi(resolveRoute, (c) =>
292
+ handleResolveRequest(c, registryConfig, managerDriver),
293
+ );
294
+ }
295
+
296
+ // GET /actors/connect/websocket
297
+ {
298
+ // HACK: WebSockets don't work with mounts, so we need to dynamically match the trailing path
299
+ router.use("*", (c, next) => {
300
+ if (c.req.path.endsWith("/actors/connect/websocket")) {
301
+ return handleWebSocketConnectRequest(
302
+ c,
303
+ registryConfig,
304
+ runConfig,
305
+ managerDriver,
306
+ );
307
+ }
308
+
309
+ return next();
310
+ });
311
+
312
+ // This route is a noop, just used to generate docs
313
+ const wsRoute = createRoute({
314
+ method: "get",
315
+ path: "/actors/connect/websocket",
316
+ responses: {
317
+ 101: {
318
+ description: "WebSocket upgrade",
319
+ },
320
+ },
321
+ });
322
+
323
+ router.openapi(wsRoute, () => {
324
+ throw new Error("Should be unreachable");
325
+ });
326
+ }
327
+
328
+ // GET /actors/connect/sse
329
+ {
330
+ const sseRoute = createRoute({
331
+ method: "get",
332
+ path: "/actors/connect/sse",
333
+ request: {
334
+ headers: z.object({
335
+ [HEADER_ENCODING]: OPENAPI_ENCODING,
336
+ [HEADER_ACTOR_QUERY]: OPENAPI_ACTOR_QUERY,
337
+ [HEADER_CONN_PARAMS]: OPENAPI_CONN_PARAMS.optional(),
338
+ }),
339
+ },
340
+ responses: {
341
+ 200: {
342
+ description: "SSE stream",
343
+ content: {
344
+ "text/event-stream": {
345
+ schema: z.unknown(),
346
+ },
347
+ },
348
+ },
349
+ },
350
+ });
351
+
352
+ router.openapi(sseRoute, (c) =>
353
+ handleSseConnectRequest(c, registryConfig, runConfig, managerDriver),
354
+ );
355
+ }
356
+
357
+ // POST /actors/action/:action
358
+ {
359
+ const ActionParamsSchema = z
360
+ .object({
361
+ action: z.string().openapi({
362
+ param: {
363
+ name: "action",
364
+ in: "path",
365
+ },
366
+ example: "myAction",
367
+ }),
368
+ })
369
+ .openapi("ActionParams");
370
+
371
+ const ActionRequestSchema = z
372
+ .object({
373
+ query: z.any().openapi({
374
+ example: { getForId: { actorId: "actor-123" } },
375
+ }),
376
+ body: z
377
+ .any()
378
+ .optional()
379
+ .openapi({
380
+ example: { param1: "value1", param2: 123 },
381
+ }),
382
+ })
383
+ .openapi("ActionRequest");
384
+
385
+ const ActionResponseSchema = z.any().openapi("ActionResponse");
386
+
387
+ const actionRoute = createRoute({
388
+ method: "post",
389
+ path: "/actors/actions/{action}",
390
+ request: {
391
+ params: ActionParamsSchema,
392
+ body: {
393
+ content: validateBody
394
+ ? {
395
+ "application/json": {
396
+ schema: ActionRequestSchema,
397
+ },
398
+ }
399
+ : {},
400
+ },
401
+ headers: z.object({
402
+ [HEADER_ENCODING]: OPENAPI_ENCODING,
403
+ [HEADER_CONN_PARAMS]: OPENAPI_CONN_PARAMS.optional(),
404
+ }),
405
+ },
406
+ responses: buildOpenApiResponses(ActionResponseSchema, validateBody),
407
+ });
408
+
409
+ router.openapi(actionRoute, (c) =>
410
+ handleActionRequest(c, registryConfig, runConfig, managerDriver),
411
+ );
412
+ }
413
+
414
+ // POST /actors/message
415
+ {
416
+ const ConnectionMessageRequestSchema = z
417
+ .object({
418
+ message: z.any().openapi({
419
+ example: { type: "message", content: "Hello, actor!" },
420
+ }),
421
+ })
422
+ .openapi("ConnectionMessageRequest");
423
+
424
+ const ConnectionMessageResponseSchema = z
425
+ .any()
426
+ .openapi("ConnectionMessageResponse");
427
+
428
+ const messageRoute = createRoute({
429
+ method: "post",
430
+ path: "/actors/message",
431
+ request: {
432
+ body: {
433
+ content: validateBody
434
+ ? {
435
+ "application/json": {
436
+ schema: ConnectionMessageRequestSchema,
437
+ },
438
+ }
439
+ : {},
440
+ },
441
+ headers: z.object({
442
+ [HEADER_ACTOR_ID]: OPENAPI_ACTOR_ID,
443
+ [HEADER_CONN_ID]: OPENAPI_CONN_ID,
444
+ [HEADER_ENCODING]: OPENAPI_ENCODING,
445
+ [HEADER_CONN_TOKEN]: OPENAPI_CONN_TOKEN,
446
+ }),
447
+ },
448
+ responses: buildOpenApiResponses(
449
+ ConnectionMessageResponseSchema,
450
+ validateBody,
451
+ ),
452
+ });
453
+
454
+ router.openapi(messageRoute, (c) =>
455
+ handleMessageRequest(c, registryConfig, runConfig, managerDriver),
456
+ );
457
+ }
458
+
459
+ // Raw HTTP endpoints - /actors/raw/http/*
460
+ {
461
+ const RawHttpRequestBodySchema = z.any().optional().openapi({
462
+ description: "Raw request body (can be any content type)",
463
+ });
464
+
465
+ const RawHttpResponseSchema = z.any().openapi({
466
+ description: "Raw response from actor's onFetch handler",
467
+ });
468
+
469
+ // Define common route config
470
+ const rawHttpRouteConfig = {
471
+ path: "/actors/raw/http/*",
472
+ request: {
473
+ headers: z.object({
474
+ [HEADER_ACTOR_QUERY]: OPENAPI_ACTOR_QUERY.optional(),
475
+ [HEADER_CONN_PARAMS]: OPENAPI_CONN_PARAMS.optional(),
476
+ }),
477
+ body: {
478
+ content: {
479
+ "*/*": {
480
+ schema: RawHttpRequestBodySchema,
481
+ },
482
+ },
483
+ },
484
+ },
485
+ responses: {
486
+ 200: {
487
+ description: "Success - response from actor's onFetch handler",
488
+ content: {
489
+ "*/*": {
490
+ schema: RawHttpResponseSchema,
491
+ },
492
+ },
493
+ },
494
+ 404: {
495
+ description: "Actor does not have an onFetch handler",
496
+ },
497
+ 500: {
498
+ description: "Internal server error or invalid response from actor",
499
+ },
500
+ },
501
+ };
502
+
503
+ // Create routes for each HTTP method
504
+ const httpMethods = [
505
+ "get",
506
+ "post",
507
+ "put",
508
+ "delete",
509
+ "patch",
510
+ "head",
511
+ "options",
512
+ ] as const;
513
+ for (const method of httpMethods) {
514
+ const route = createRoute({
515
+ method,
516
+ ...rawHttpRouteConfig,
517
+ });
518
+
519
+ router.openapi(route, async (c) => {
520
+ return handleRawHttpRequest(
521
+ c,
522
+ registryConfig,
523
+ runConfig,
524
+ managerDriver,
525
+ );
526
+ });
527
+ }
528
+ }
529
+
530
+ // Raw WebSocket endpoint - /actors/raw/websocket/*
531
+ {
532
+ // HACK: WebSockets don't work with mounts, so we need to dynamically match the trailing path
533
+ router.use("*", async (c, next) => {
534
+ if (c.req.path.includes("/raw/websocket/")) {
535
+ return handleRawWebSocketRequest(
536
+ c,
537
+ registryConfig,
538
+ runConfig,
539
+ managerDriver,
540
+ );
541
+ }
542
+
543
+ return next();
544
+ });
545
+
546
+ // This route is a noop, just used to generate docs
547
+ const rawWebSocketRoute = createRoute({
548
+ method: "get",
549
+ path: "/actors/raw/websocket/*",
550
+ request: {},
551
+ responses: {
552
+ 101: {
553
+ description: "WebSocket upgrade successful",
554
+ },
555
+ 400: {
556
+ description: "WebSockets not enabled or invalid request",
557
+ },
558
+ 404: {
559
+ description: "Actor does not have an onWebSocket handler",
560
+ },
561
+ },
562
+ });
563
+
564
+ router.openapi(rawWebSocketRoute, () => {
565
+ throw new Error("Should be unreachable");
566
+ });
567
+ }
568
+
569
+ if (runConfig.inspector?.enabled) {
570
+ router.route(
571
+ "/actors/inspect",
572
+ new Hono()
573
+ .use(
574
+ cors(runConfig.inspector.cors),
575
+ secureInspector(runConfig),
576
+ universalActorProxy({
577
+ registryConfig,
578
+ runConfig,
579
+ driver: managerDriver,
580
+ }),
581
+ )
582
+ .all("/", (c) =>
583
+ // this should be handled by the actor proxy, but just in case
584
+ c.text("Unreachable.", 404),
585
+ ),
586
+ );
587
+ router.route(
588
+ "/inspect",
589
+ new Hono()
590
+ .use(
591
+ cors(runConfig.inspector.cors),
592
+ secureInspector(runConfig),
593
+ async (c, next) => {
594
+ const inspector = managerDriver.inspector;
595
+ invariant(inspector, "inspector not supported on this platform");
596
+
597
+ c.set("inspector", inspector);
598
+ await next();
599
+ },
600
+ )
601
+ .route("/", createManagerInspectorRouter()),
602
+ );
603
+ }
604
+
605
+ if (registryConfig.test.enabled) {
606
+ // Add HTTP endpoint to test the inline client
607
+ //
608
+ // We have to do this in a router since this needs to run in the same server as the RivetKit registry. Some test contexts to not run in the same server.
609
+ router.post(".test/inline-driver/call", async (c) => {
610
+ // TODO: use openapi instead
611
+ const buffer = await c.req.arrayBuffer();
612
+ const { encoding, transport, method, args }: TestInlineDriverCallRequest =
613
+ cbor.decode(new Uint8Array(buffer));
614
+
615
+ logger().debug("received inline request", {
616
+ encoding,
617
+ transport,
618
+ method,
619
+ args,
620
+ });
621
+
622
+ // Forward inline driver request
623
+ let response: TestInlineDriverCallResponse<unknown>;
624
+ try {
625
+ const output = await ((inlineClientDriver as any)[method] as any)(
626
+ ...args,
627
+ );
628
+ response = { ok: output };
629
+ } catch (rawErr) {
630
+ const err = deconstructError(rawErr, logger(), {}, true);
631
+ response = { err };
632
+ }
633
+
634
+ return c.body(cbor.encode(response));
635
+ });
636
+
637
+ router.get(".test/inline-driver/connect-websocket", async (c) => {
638
+ const upgradeWebSocket = runConfig.getUpgradeWebSocket?.();
639
+ invariant(upgradeWebSocket, "websockets not supported on this platform");
640
+
641
+ return upgradeWebSocket(async (c: any) => {
642
+ const {
643
+ actorQuery: actorQueryRaw,
644
+ params: paramsRaw,
645
+ encodingKind,
646
+ } = c.req.query() as {
647
+ actorQuery: string;
648
+ params?: string;
649
+ encodingKind: Encoding;
650
+ };
651
+ const actorQuery = JSON.parse(actorQueryRaw);
652
+ const params =
653
+ paramsRaw !== undefined ? JSON.parse(paramsRaw) : undefined;
654
+
655
+ logger().debug("received test inline driver websocket", {
656
+ actorQuery,
657
+ params,
658
+ encodingKind,
659
+ });
660
+
661
+ // Connect to the actor using the inline client driver - this returns a Promise<WebSocket>
662
+ const clientWsPromise = inlineClientDriver.connectWebSocket(
663
+ undefined,
664
+ actorQuery,
665
+ encodingKind,
666
+ params,
667
+ undefined,
668
+ );
669
+
670
+ return await createTestWebSocketProxy(clientWsPromise, "standard");
671
+ })(c, noopNext());
672
+ });
673
+
674
+ router.get(".test/inline-driver/raw-websocket", async (c) => {
675
+ const upgradeWebSocket = runConfig.getUpgradeWebSocket?.();
676
+ invariant(upgradeWebSocket, "websockets not supported on this platform");
677
+
678
+ return upgradeWebSocket(async (c: any) => {
679
+ const {
680
+ actorQuery: actorQueryRaw,
681
+ params: paramsRaw,
682
+ encodingKind,
683
+ path,
684
+ protocols: protocolsRaw,
685
+ } = c.req.query() as {
686
+ actorQuery: string;
687
+ params?: string;
688
+ encodingKind: Encoding;
689
+ path: string;
690
+ protocols?: string;
691
+ };
692
+ const actorQuery = JSON.parse(actorQueryRaw);
693
+ const params =
694
+ paramsRaw !== undefined ? JSON.parse(paramsRaw) : undefined;
695
+ const protocols =
696
+ protocolsRaw !== undefined ? JSON.parse(protocolsRaw) : undefined;
697
+
698
+ logger().debug("received test inline driver raw websocket", {
699
+ actorQuery,
700
+ params,
701
+ encodingKind,
702
+ path,
703
+ protocols,
704
+ });
705
+
706
+ // Connect to the actor using the inline client driver - this returns a Promise<WebSocket>
707
+ logger().debug("calling inlineClientDriver.rawWebSocket");
708
+ const clientWsPromise = inlineClientDriver.rawWebSocket(
709
+ undefined,
710
+ actorQuery,
711
+ encodingKind,
712
+ params,
713
+ path,
714
+ protocols,
715
+ undefined,
716
+ );
717
+
718
+ logger().debug("calling createTestWebSocketProxy");
719
+ return await createTestWebSocketProxy(clientWsPromise, "raw");
720
+ })(c, noopNext());
721
+ });
722
+
723
+ // Raw HTTP endpoint for test inline driver
724
+ router.all(".test/inline-driver/raw-http/*", async (c) => {
725
+ // Extract parameters from headers
726
+ const actorQueryHeader = c.req.header(HEADER_ACTOR_QUERY);
727
+ const paramsHeader = c.req.header(HEADER_CONN_PARAMS);
728
+ const encodingHeader = c.req.header(HEADER_ENCODING);
729
+
730
+ if (!actorQueryHeader || !encodingHeader) {
731
+ return c.text("Missing required headers", 400);
732
+ }
733
+
734
+ const actorQuery = JSON.parse(actorQueryHeader);
735
+ const params = paramsHeader ? JSON.parse(paramsHeader) : undefined;
736
+ const encoding = encodingHeader as Encoding;
737
+
738
+ // Extract the path after /raw-http/
739
+ const fullPath = c.req.path;
740
+ const pathOnly =
741
+ fullPath.split("/.test/inline-driver/raw-http/")[1] || "";
742
+
743
+ // Include query string
744
+ const url = new URL(c.req.url);
745
+ const pathWithQuery = pathOnly + url.search;
746
+
747
+ logger().debug("received test inline driver raw http", {
748
+ actorQuery,
749
+ params,
750
+ encoding,
751
+ path: pathWithQuery,
752
+ method: c.req.method,
753
+ });
754
+
755
+ try {
756
+ // Forward the request using the inline client driver
757
+ const response = await inlineClientDriver.rawHttpRequest(
758
+ undefined,
759
+ actorQuery,
760
+ encoding,
761
+ params,
762
+ pathWithQuery,
763
+ {
764
+ method: c.req.method,
765
+ headers: c.req.raw.headers,
766
+ body: c.req.raw.body,
767
+ },
768
+ undefined,
769
+ );
770
+
771
+ // Return the response directly
772
+ return response;
773
+ } catch (error) {
774
+ logger().error("error in test inline raw http", {
775
+ error: stringifyError(error),
776
+ });
777
+
778
+ // Return error response
779
+ const err = deconstructError(error, logger(), {}, true);
780
+ return c.json(
781
+ {
782
+ error: {
783
+ code: err.code,
784
+ message: err.message,
785
+ metadata: err.metadata,
786
+ },
787
+ },
788
+ err.statusCode,
789
+ );
790
+ }
791
+ });
792
+ }
793
+
794
+ managerDriver.modifyManagerRouter?.(
795
+ registryConfig,
796
+ router as unknown as Hono,
797
+ );
798
+
799
+ // Mount on both / and /registry
800
+ //
801
+ // We do this because the default requests are to `/registry/*`.
802
+ //
803
+ // If using `app.fetch` directly in a non-hono router, paths
804
+ // might not be truncated so they'll come to this router as
805
+ // `/registry/*`. If mounted correctly in Hono, requests will
806
+ // come in at the root as `/*`.
807
+ const mountedRouter = new Hono();
808
+ mountedRouter.route("/", router);
809
+ mountedRouter.route("/registry", router);
810
+
811
+ // IMPORTANT: These must be on `mountedRouter` instead of `router` or else they will not be called.
812
+ mountedRouter.notFound(handleRouteNotFound);
813
+ mountedRouter.onError(handleRouteError.bind(undefined, {}));
814
+
815
+ return { router: mountedRouter, openapi: router };
816
+ }
817
+
818
+ export interface TestInlineDriverCallRequest {
819
+ encoding: Encoding;
820
+ transport: Transport;
821
+ method: string;
822
+ args: unknown[];
823
+ }
824
+
825
+ export type TestInlineDriverCallResponse<T> =
826
+ | {
827
+ ok: T;
828
+ }
829
+ | {
830
+ err: DeconstructedError;
831
+ };
832
+
833
+ /**
834
+ * Query the manager driver to get or create a actor based on the provided query
835
+ */
836
+ export async function queryActor(
837
+ c: HonoContext,
838
+ query: ActorQuery,
839
+ driver: ManagerDriver,
840
+ ): Promise<{ actorId: string }> {
841
+ logger().debug("querying actor", { query });
842
+ let actorOutput: { actorId: string };
843
+ if ("getForId" in query) {
844
+ const output = await driver.getForId({
845
+ c,
846
+ name: query.getForId.name,
847
+ actorId: query.getForId.actorId,
848
+ });
849
+ if (!output) throw new errors.ActorNotFound(query.getForId.actorId);
850
+ actorOutput = output;
851
+ } else if ("getForKey" in query) {
852
+ const existingActor = await driver.getWithKey({
853
+ c,
854
+ name: query.getForKey.name,
855
+ key: query.getForKey.key,
856
+ });
857
+ if (!existingActor) {
858
+ throw new errors.ActorNotFound(
859
+ `${query.getForKey.name}:${JSON.stringify(query.getForKey.key)}`,
860
+ );
861
+ }
862
+ actorOutput = existingActor;
863
+ } else if ("getOrCreateForKey" in query) {
864
+ const getOrCreateOutput = await driver.getOrCreateWithKey({
865
+ c,
866
+ name: query.getOrCreateForKey.name,
867
+ key: query.getOrCreateForKey.key,
868
+ input: query.getOrCreateForKey.input,
869
+ region: query.getOrCreateForKey.region,
870
+ });
871
+ actorOutput = {
872
+ actorId: getOrCreateOutput.actorId,
873
+ };
874
+ } else if ("create" in query) {
875
+ const createOutput = await driver.createActor({
876
+ c,
877
+ name: query.create.name,
878
+ key: query.create.key,
879
+ input: query.create.input,
880
+ region: query.create.region,
881
+ });
882
+ actorOutput = {
883
+ actorId: createOutput.actorId,
884
+ };
885
+ } else {
886
+ throw new errors.InvalidRequest("Invalid query format");
887
+ }
888
+
889
+ logger().debug("actor query result", {
890
+ actorId: actorOutput.actorId,
891
+ });
892
+ return { actorId: actorOutput.actorId };
893
+ }
894
+
895
+ /**
896
+ * Creates a WebSocket proxy for test endpoints that forwards messages between server and client WebSockets
897
+ */
898
+ async function createTestWebSocketProxy(
899
+ clientWsPromise: Promise<WebSocket>,
900
+ connectionType: string,
901
+ ): Promise<UpgradeWebSocketArgs> {
902
+ // Store a reference to the resolved WebSocket
903
+ let clientWs: WebSocket | null = null;
904
+ try {
905
+ // Resolve the client WebSocket promise
906
+ logger().debug("awaiting client websocket promise");
907
+ const ws = await clientWsPromise;
908
+ clientWs = ws;
909
+ logger().debug("client websocket promise resolved", {
910
+ constructor: ws?.constructor.name,
911
+ });
912
+
913
+ // Wait for ws to open
914
+ await new Promise<void>((resolve, reject) => {
915
+ const onOpen = () => {
916
+ logger().debug("test websocket connection opened");
917
+ resolve();
918
+ };
919
+ const onError = (error: any) => {
920
+ logger().error("test websocket connection failed", { error });
921
+ reject(
922
+ new Error(`Failed to open WebSocket: ${error.message || error}`),
923
+ );
924
+ };
925
+ ws.addEventListener("open", onOpen);
926
+ ws.addEventListener("error", onError);
927
+ });
928
+ } catch (error) {
929
+ logger().error(
930
+ `failed to establish client ${connectionType} websocket connection`,
931
+ { error },
932
+ );
933
+ return {
934
+ onOpen: (_evt, serverWs) => {
935
+ serverWs.close(1011, "Failed to establish connection");
936
+ },
937
+ onMessage: () => {},
938
+ onError: () => {},
939
+ onClose: () => {},
940
+ };
941
+ }
942
+
943
+ // Create WebSocket proxy handlers to relay messages between client and server
944
+ return {
945
+ onOpen: (_evt: any, serverWs: WSContext) => {
946
+ logger().debug(`test ${connectionType} websocket connection opened`);
947
+
948
+ // Check WebSocket type
949
+ logger().debug("clientWs info", {
950
+ constructor: clientWs.constructor.name,
951
+ hasAddEventListener: typeof clientWs.addEventListener === "function",
952
+ readyState: clientWs.readyState,
953
+ });
954
+
955
+ // Add message handler to forward messages from client to server
956
+ clientWs.addEventListener("message", (clientEvt: MessageEvent) => {
957
+ logger().debug(
958
+ `test ${connectionType} websocket connection message from client`,
959
+ {
960
+ dataType: typeof clientEvt.data,
961
+ isBlob: clientEvt.data instanceof Blob,
962
+ isArrayBuffer: clientEvt.data instanceof ArrayBuffer,
963
+ dataConstructor: clientEvt.data?.constructor?.name,
964
+ dataStr:
965
+ typeof clientEvt.data === "string"
966
+ ? clientEvt.data.substring(0, 100)
967
+ : undefined,
968
+ },
969
+ );
970
+
971
+ if (serverWs.readyState === 1) {
972
+ // OPEN
973
+ // Handle Blob data
974
+ if (clientEvt.data instanceof Blob) {
975
+ clientEvt.data
976
+ .arrayBuffer()
977
+ .then((buffer) => {
978
+ logger().debug(
979
+ "converted client blob to arraybuffer, sending to server",
980
+ {
981
+ bufferSize: buffer.byteLength,
982
+ },
983
+ );
984
+ serverWs.send(buffer as any);
985
+ })
986
+ .catch((error) => {
987
+ logger().error("failed to convert blob to arraybuffer", {
988
+ error,
989
+ });
990
+ });
991
+ } else {
992
+ logger().debug("sending client data directly to server", {
993
+ dataType: typeof clientEvt.data,
994
+ dataLength:
995
+ typeof clientEvt.data === "string"
996
+ ? clientEvt.data.length
997
+ : undefined,
998
+ });
999
+ serverWs.send(clientEvt.data as any);
1000
+ }
1001
+ }
1002
+ });
1003
+
1004
+ // Add close handler to close server when client closes
1005
+ clientWs.addEventListener("close", (clientEvt: CloseEvent) => {
1006
+ logger().debug(`test ${connectionType} websocket connection closed`);
1007
+
1008
+ if (serverWs.readyState !== 3) {
1009
+ // Not CLOSED
1010
+ serverWs.close(clientEvt.code, clientEvt.reason);
1011
+ }
1012
+ });
1013
+
1014
+ // Add error handler
1015
+ clientWs.addEventListener("error", () => {
1016
+ logger().debug(`test ${connectionType} websocket connection error`);
1017
+
1018
+ if (serverWs.readyState !== 3) {
1019
+ // Not CLOSED
1020
+ serverWs.close(1011, "Error in client websocket");
1021
+ }
1022
+ });
1023
+ },
1024
+ onMessage: (evt: { data: any }) => {
1025
+ logger().debug("received message from server", {
1026
+ dataType: typeof evt.data,
1027
+ isBlob: evt.data instanceof Blob,
1028
+ isArrayBuffer: evt.data instanceof ArrayBuffer,
1029
+ dataConstructor: evt.data?.constructor?.name,
1030
+ dataStr:
1031
+ typeof evt.data === "string" ? evt.data.substring(0, 100) : undefined,
1032
+ });
1033
+
1034
+ // Forward messages from server websocket to client websocket
1035
+ if (clientWs.readyState === 1) {
1036
+ // OPEN
1037
+ // Handle Blob data
1038
+ if (evt.data instanceof Blob) {
1039
+ evt.data
1040
+ .arrayBuffer()
1041
+ .then((buffer) => {
1042
+ logger().debug("converted blob to arraybuffer, sending", {
1043
+ bufferSize: buffer.byteLength,
1044
+ });
1045
+ clientWs.send(buffer);
1046
+ })
1047
+ .catch((error) => {
1048
+ logger().error("failed to convert blob to arraybuffer", {
1049
+ error,
1050
+ });
1051
+ });
1052
+ } else {
1053
+ logger().debug("sending data directly", {
1054
+ dataType: typeof evt.data,
1055
+ dataLength:
1056
+ typeof evt.data === "string" ? evt.data.length : undefined,
1057
+ });
1058
+ clientWs.send(evt.data);
1059
+ }
1060
+ }
1061
+ },
1062
+ onClose: (
1063
+ event: {
1064
+ wasClean: boolean;
1065
+ code: number;
1066
+ reason: string;
1067
+ },
1068
+ serverWs: WSContext,
1069
+ ) => {
1070
+ logger().debug(`server ${connectionType} websocket closed`, {
1071
+ wasClean: event.wasClean,
1072
+ code: event.code,
1073
+ reason: event.reason,
1074
+ });
1075
+
1076
+ // HACK: Close socket in order to fix bug with Cloudflare leaving WS in closing state
1077
+ // https://github.com/cloudflare/workerd/issues/2569
1078
+ serverWs.close(1000, "hack_force_close");
1079
+
1080
+ // Close the client websocket when the server websocket closes
1081
+ if (
1082
+ clientWs &&
1083
+ clientWs.readyState !== clientWs.CLOSED &&
1084
+ clientWs.readyState !== clientWs.CLOSING
1085
+ ) {
1086
+ // Don't pass code/message since this may affect how close events are triggered
1087
+ clientWs.close(1000, event.reason);
1088
+ }
1089
+ },
1090
+ onError: (error: unknown) => {
1091
+ logger().error(`error in server ${connectionType} websocket`, { error });
1092
+
1093
+ // Close the client websocket on error
1094
+ if (
1095
+ clientWs &&
1096
+ clientWs.readyState !== clientWs.CLOSED &&
1097
+ clientWs.readyState !== clientWs.CLOSING
1098
+ ) {
1099
+ clientWs.close(1011, "Error in server websocket");
1100
+ }
1101
+ },
1102
+ };
1103
+ }
1104
+
1105
+ /**
1106
+ * Handle SSE connection request
1107
+ */
1108
+ async function handleSseConnectRequest(
1109
+ c: HonoContext,
1110
+ registryConfig: RegistryConfig,
1111
+ _runConfig: RunConfig,
1112
+ driver: ManagerDriver,
1113
+ ): Promise<Response> {
1114
+ let encoding: Encoding | undefined;
1115
+ try {
1116
+ encoding = getRequestEncoding(c.req);
1117
+ logger().debug("sse connection request received", { encoding });
1118
+
1119
+ const params = ConnectRequestSchema.safeParse({
1120
+ query: getRequestQuery(c),
1121
+ encoding: c.req.header(HEADER_ENCODING),
1122
+ connParams: c.req.header(HEADER_CONN_PARAMS),
1123
+ });
1124
+
1125
+ if (!params.success) {
1126
+ logger().error("invalid connection parameters", {
1127
+ error: params.error,
1128
+ });
1129
+ throw new errors.InvalidRequest(params.error);
1130
+ }
1131
+
1132
+ const query = params.data.query;
1133
+
1134
+ // Parse connection parameters for authentication
1135
+ const connParams = params.data.connParams
1136
+ ? JSON.parse(params.data.connParams)
1137
+ : undefined;
1138
+
1139
+ // Authenticate the request
1140
+ const authData = await authenticateEndpoint(
1141
+ c,
1142
+ driver,
1143
+ registryConfig,
1144
+ query,
1145
+ ["connect"],
1146
+ connParams,
1147
+ );
1148
+
1149
+ // Get the actor ID
1150
+ const { actorId } = await queryActor(c, query, driver);
1151
+ invariant(actorId, "Missing actor ID");
1152
+ logger().debug("sse connection to actor", { actorId });
1153
+
1154
+ // Handle based on mode
1155
+ logger().debug("using custom proxy mode for sse connection");
1156
+ const url = new URL("http://actor/connect/sse");
1157
+
1158
+ // Always build fresh request to prevent forwarding unwanted headers
1159
+ const proxyRequestHeaderes = new Headers();
1160
+ proxyRequestHeaderes.set(HEADER_ENCODING, params.data.encoding);
1161
+ if (params.data.connParams) {
1162
+ proxyRequestHeaderes.set(HEADER_CONN_PARAMS, params.data.connParams);
1163
+ }
1164
+ if (authData) {
1165
+ proxyRequestHeaderes.set(HEADER_AUTH_DATA, JSON.stringify(authData));
1166
+ }
1167
+
1168
+ const proxyRequest = new Request(url, { headers: proxyRequestHeaderes });
1169
+
1170
+ return await driver.proxyRequest(c, proxyRequest, actorId);
1171
+ } catch (error) {
1172
+ // If we receive an error during setup, we send the error and close the socket immediately
1173
+ //
1174
+ // We have to return the error over SSE since SSE clients cannot read vanilla HTTP responses
1175
+
1176
+ const { code, message, metadata } = deconstructError(error, logger(), {
1177
+ sseEvent: "setup",
1178
+ });
1179
+
1180
+ return streamSSE(c, async (stream) => {
1181
+ try {
1182
+ if (encoding) {
1183
+ // Serialize and send the connection error
1184
+ const errorMsg: protocol.ToClient = {
1185
+ body: {
1186
+ tag: "Error",
1187
+ val: {
1188
+ code,
1189
+ message,
1190
+ metadata: bufferToArrayBuffer(cbor.encode(metadata)),
1191
+ actionId: null,
1192
+ },
1193
+ },
1194
+ };
1195
+
1196
+ // Send the error message to the client
1197
+ const serialized = serializeWithEncoding(
1198
+ encoding,
1199
+ errorMsg,
1200
+ TO_CLIENT_VERSIONED,
1201
+ );
1202
+ await stream.writeSSE({
1203
+ data:
1204
+ typeof serialized === "string"
1205
+ ? serialized
1206
+ : Buffer.from(serialized).toString("base64"),
1207
+ });
1208
+ } else {
1209
+ // We don't know the encoding, send an error and close
1210
+ await stream.writeSSE({
1211
+ data: code,
1212
+ event: "error",
1213
+ });
1214
+ }
1215
+ } catch (serializeError) {
1216
+ logger().error("failed to send error to sse client", {
1217
+ error: serializeError,
1218
+ });
1219
+ await stream.writeSSE({
1220
+ data: "internal error during error handling",
1221
+ event: "error",
1222
+ });
1223
+ }
1224
+
1225
+ // Stream will exit completely once function exits
1226
+ });
1227
+ }
1228
+ }
1229
+
1230
+ /**
1231
+ * Handle WebSocket connection request
1232
+ */
1233
+ async function handleWebSocketConnectRequest(
1234
+ c: HonoContext,
1235
+ registryConfig: RegistryConfig,
1236
+ runConfig: RunConfig,
1237
+ driver: ManagerDriver,
1238
+ ): Promise<Response> {
1239
+ const upgradeWebSocket = runConfig.getUpgradeWebSocket?.();
1240
+ if (!upgradeWebSocket) {
1241
+ return c.text(
1242
+ "WebSockets are not enabled for this driver. Use SSE instead.",
1243
+ 400,
1244
+ );
1245
+ }
1246
+
1247
+ let encoding: Encoding | undefined;
1248
+ try {
1249
+ logger().debug("websocket connection request received");
1250
+
1251
+ // Parse configuration from Sec-WebSocket-Protocol header
1252
+ //
1253
+ // We use this instead of query parameters since this is more secure than
1254
+ // query parameters. Query parameters often get logged.
1255
+ //
1256
+ // Browsers don't support using headers, so this is the only way to
1257
+ // pass data securely.
1258
+ const protocols = c.req.header("sec-websocket-protocol");
1259
+ const { queryRaw, encodingRaw, connParamsRaw } =
1260
+ parseWebSocketProtocols(protocols);
1261
+
1262
+ // Parse query
1263
+ let queryUnvalidated: unknown;
1264
+ try {
1265
+ queryUnvalidated = JSON.parse(queryRaw!);
1266
+ } catch (error) {
1267
+ logger().error("invalid query json", { error });
1268
+ throw new errors.InvalidQueryJSON(error);
1269
+ }
1270
+
1271
+ // Parse conn params
1272
+ let connParamsUnvalidated: unknown = null;
1273
+ try {
1274
+ if (connParamsRaw) {
1275
+ connParamsUnvalidated = JSON.parse(connParamsRaw!);
1276
+ }
1277
+ } catch (error) {
1278
+ logger().error("invalid conn params", { error });
1279
+ throw new errors.InvalidParams(
1280
+ `Invalid params JSON: ${stringifyError(error)}`,
1281
+ );
1282
+ }
1283
+
1284
+ // We can't use the standard headers with WebSockets
1285
+ //
1286
+ // All other information will be sent over the socket itself, since that data needs to be E2EE
1287
+ const params = ConnectWebSocketRequestSchema.safeParse({
1288
+ query: queryUnvalidated,
1289
+ encoding: encodingRaw,
1290
+ connParams: connParamsUnvalidated,
1291
+ });
1292
+ if (!params.success) {
1293
+ logger().error("invalid connection parameters", {
1294
+ error: params.error,
1295
+ });
1296
+ throw new errors.InvalidRequest(params.error);
1297
+ }
1298
+ encoding = params.data.encoding;
1299
+
1300
+ // Authenticate endpoint
1301
+ const authData = await authenticateEndpoint(
1302
+ c,
1303
+ driver,
1304
+ registryConfig,
1305
+ params.data.query,
1306
+ ["connect"],
1307
+ connParamsRaw,
1308
+ );
1309
+
1310
+ // Get the actor ID
1311
+ const { actorId } = await queryActor(c, params.data.query, driver);
1312
+ logger().debug("found actor for websocket connection", {
1313
+ actorId,
1314
+ });
1315
+ invariant(actorId, "missing actor id");
1316
+
1317
+ // Proxy the WebSocket connection to the actor
1318
+ //
1319
+ // The proxyWebSocket handler will:
1320
+ // 1. Validate the WebSocket upgrade request
1321
+ // 2. Forward the request to the actor with the appropriate path
1322
+ // 3. Handle the WebSocket pair and proxy messages between client and actor
1323
+ return await driver.proxyWebSocket(
1324
+ c,
1325
+ PATH_CONNECT_WEBSOCKET,
1326
+ actorId,
1327
+ params.data.encoding,
1328
+ params.data.connParams,
1329
+ authData,
1330
+ );
1331
+ } catch (error) {
1332
+ // If we receive an error during setup, we send the error and close the socket immediately
1333
+ //
1334
+ // We have to return the error over WS since WebSocket clients cannot read vanilla HTTP responses
1335
+
1336
+ const { code, message, metadata } = deconstructError(error, logger(), {
1337
+ wsEvent: "setup",
1338
+ });
1339
+
1340
+ return await upgradeWebSocket(() => ({
1341
+ onOpen: (_evt: unknown, ws: WSContext) => {
1342
+ if (encoding) {
1343
+ try {
1344
+ // Serialize and send the connection error
1345
+ const errorMsg: protocol.ToClient = {
1346
+ body: {
1347
+ tag: "Error",
1348
+ val: {
1349
+ code,
1350
+ message,
1351
+ metadata: bufferToArrayBuffer(cbor.encode(metadata)),
1352
+ actionId: null,
1353
+ },
1354
+ },
1355
+ };
1356
+
1357
+ // Send the error message to the client
1358
+ const serialized = serializeWithEncoding(
1359
+ encoding,
1360
+ errorMsg,
1361
+ TO_CLIENT_VERSIONED,
1362
+ );
1363
+ ws.send(serialized);
1364
+
1365
+ // Close the connection with an error code
1366
+ ws.close(1011, code);
1367
+ } catch (serializeError) {
1368
+ logger().error("failed to send error to websocket client", {
1369
+ error: serializeError,
1370
+ });
1371
+ ws.close(1011, "internal error during error handling");
1372
+ }
1373
+ } else {
1374
+ // We don't know the encoding so we send what we can
1375
+ ws.close(1011, code);
1376
+ }
1377
+ },
1378
+ }))(c, noopNext());
1379
+ }
1380
+ }
1381
+
1382
+ /**
1383
+ * Handle a connection message request to a actor
1384
+ *
1385
+ * There is no authentication handler on this request since the connection
1386
+ * token is used to authenticate the message.
1387
+ */
1388
+ async function handleMessageRequest(
1389
+ c: HonoContext,
1390
+ _registryConfig: RegistryConfig,
1391
+ _runConfig: RunConfig,
1392
+ driver: ManagerDriver,
1393
+ ): Promise<Response> {
1394
+ logger().debug("connection message request received");
1395
+ try {
1396
+ const params = ConnMessageRequestSchema.safeParse({
1397
+ actorId: c.req.header(HEADER_ACTOR_ID),
1398
+ connId: c.req.header(HEADER_CONN_ID),
1399
+ encoding: c.req.header(HEADER_ENCODING),
1400
+ connToken: c.req.header(HEADER_CONN_TOKEN),
1401
+ });
1402
+ if (!params.success) {
1403
+ logger().error("invalid connection parameters", {
1404
+ error: params.error,
1405
+ });
1406
+ throw new errors.InvalidRequest(params.error);
1407
+ }
1408
+ const { actorId, connId, encoding, connToken } = params.data;
1409
+
1410
+ // TODO: This endpoint can be used to exhause resources (DoS attack) on an actor if you know the actor ID:
1411
+ // 1. Get the actor ID (usually this is reasonably secure, but we don't assume actor ID is sensitive)
1412
+ // 2. Spam messages to the actor (the conn token can be invalid)
1413
+ // 3. The actor will be exhausted processing messages — even if the token is invalid
1414
+ //
1415
+ // The solution is we need to move the authorization of the connection token to this request handler
1416
+ // AND include the actor ID in the connection token so we can verify that it has permission to send
1417
+ // a message to that actor. This would require changing the token to a JWT so we can include a secure
1418
+ // payload, but this requires managing a private key & managing key rotations.
1419
+ //
1420
+ // All other solutions (e.g. include the actor name as a header or include the actor name in the actor ID)
1421
+ // have exploits that allow the caller to send messages to arbitrary actors.
1422
+ //
1423
+ // Currently, we assume this is not a critical problem because requests will likely get rate
1424
+ // limited before enough messages are passed to the actor to exhaust resources.
1425
+
1426
+ const url = new URL("http://actor/connections/message");
1427
+
1428
+ // Always build fresh request to prevent forwarding unwanted headers
1429
+ const proxyRequestHeaders = new Headers();
1430
+ proxyRequestHeaders.set(HEADER_ENCODING, encoding);
1431
+ proxyRequestHeaders.set(HEADER_CONN_ID, connId);
1432
+ proxyRequestHeaders.set(HEADER_CONN_TOKEN, connToken);
1433
+
1434
+ const proxyRequest = new Request(url, {
1435
+ method: "POST",
1436
+ body: c.req.raw.body,
1437
+ duplex: "half",
1438
+ headers: proxyRequestHeaders,
1439
+ });
1440
+
1441
+ return await driver.proxyRequest(c, proxyRequest, actorId);
1442
+ } catch (error) {
1443
+ logger().error("error proxying connection message", { error });
1444
+
1445
+ // Use ProxyError if it's not already an ActorError
1446
+ if (!errors.ActorError.isActorError(error)) {
1447
+ throw new errors.ProxyError("connection message", error);
1448
+ } else {
1449
+ throw error;
1450
+ }
1451
+ }
1452
+ }
1453
+
1454
+ /**
1455
+ * Handle an action request to a actor
1456
+ */
1457
+ async function handleActionRequest(
1458
+ c: HonoContext,
1459
+ registryConfig: RegistryConfig,
1460
+ _runConfig: RunConfig,
1461
+ driver: ManagerDriver,
1462
+ ): Promise<Response> {
1463
+ try {
1464
+ const actionName = c.req.param("action");
1465
+ logger().debug("action call received", { actionName });
1466
+
1467
+ const params = ConnectRequestSchema.safeParse({
1468
+ query: getRequestQuery(c),
1469
+ encoding: c.req.header(HEADER_ENCODING),
1470
+ connParams: c.req.header(HEADER_CONN_PARAMS),
1471
+ });
1472
+
1473
+ if (!params.success) {
1474
+ logger().error("invalid connection parameters", {
1475
+ error: params.error,
1476
+ });
1477
+ throw new errors.InvalidRequest(params.error);
1478
+ }
1479
+
1480
+ // Parse connection parameters for authentication
1481
+ const connParams = params.data.connParams
1482
+ ? JSON.parse(params.data.connParams)
1483
+ : undefined;
1484
+
1485
+ // Authenticate the request
1486
+ const authData = await authenticateEndpoint(
1487
+ c,
1488
+ driver,
1489
+ registryConfig,
1490
+ params.data.query,
1491
+ ["action"],
1492
+ connParams,
1493
+ );
1494
+
1495
+ // Get the actor ID
1496
+ const { actorId } = await queryActor(c, params.data.query, driver);
1497
+ logger().debug("found actor for action", { actorId });
1498
+ invariant(actorId, "Missing actor ID");
1499
+
1500
+ const url = new URL(
1501
+ `http://actor/action/${encodeURIComponent(actionName)}`,
1502
+ );
1503
+
1504
+ // Always build fresh request to prevent forwarding unwanted headers
1505
+ const proxyRequestHeaders = new Headers();
1506
+ proxyRequestHeaders.set(HEADER_ENCODING, params.data.encoding);
1507
+ if (params.data.connParams) {
1508
+ proxyRequestHeaders.set(HEADER_CONN_PARAMS, params.data.connParams);
1509
+ }
1510
+ if (authData) {
1511
+ proxyRequestHeaders.set(HEADER_AUTH_DATA, JSON.stringify(authData));
1512
+ }
1513
+
1514
+ const proxyRequest = new Request(url, {
1515
+ method: "POST",
1516
+ body: c.req.raw.body,
1517
+ headers: proxyRequestHeaders,
1518
+ });
1519
+
1520
+ return await driver.proxyRequest(c, proxyRequest, actorId);
1521
+ } catch (error) {
1522
+ logger().error("error in action handler", { error: stringifyError(error) });
1523
+
1524
+ // Use ProxyError if it's not already an ActorError
1525
+ if (!errors.ActorError.isActorError(error)) {
1526
+ throw new errors.ProxyError("Action call", error);
1527
+ } else {
1528
+ throw error;
1529
+ }
1530
+ }
1531
+ }
1532
+
1533
+ /**
1534
+ * Handle the resolve request to get a actor ID from a query
1535
+ */
1536
+ async function handleResolveRequest(
1537
+ c: HonoContext,
1538
+ registryConfig: RegistryConfig,
1539
+ driver: ManagerDriver,
1540
+ ): Promise<Response> {
1541
+ const encoding = getRequestEncoding(c.req);
1542
+ logger().debug("resolve request encoding", { encoding });
1543
+
1544
+ const params = ResolveRequestSchema.safeParse({
1545
+ query: getRequestQuery(c),
1546
+ connParams: c.req.header(HEADER_CONN_PARAMS),
1547
+ });
1548
+ if (!params.success) {
1549
+ logger().error("invalid connection parameters", {
1550
+ error: params.error,
1551
+ });
1552
+ throw new errors.InvalidRequest(params.error);
1553
+ }
1554
+
1555
+ // Parse connection parameters for authentication
1556
+ const connParams = params.data.connParams
1557
+ ? JSON.parse(params.data.connParams)
1558
+ : undefined;
1559
+
1560
+ const query = params.data.query;
1561
+
1562
+ // Authenticate the request
1563
+ await authenticateEndpoint(c, driver, registryConfig, query, [], connParams);
1564
+
1565
+ // Get the actor ID
1566
+ const { actorId } = await queryActor(c, query, driver);
1567
+ logger().debug("resolved actor", { actorId });
1568
+ invariant(actorId, "Missing actor ID");
1569
+
1570
+ // Format response according to protocol
1571
+ const response: protocol.HttpResolveResponse = {
1572
+ actorId,
1573
+ };
1574
+ const serialized = serializeWithEncoding(
1575
+ encoding,
1576
+ response,
1577
+ HTTP_RESOLVE_RESPONSE_VERSIONED,
1578
+ );
1579
+ return c.body(serialized);
1580
+ }
1581
+
1582
+ /**
1583
+ * Handle raw HTTP requests to an actor
1584
+ */
1585
+ async function handleRawHttpRequest(
1586
+ c: HonoContext,
1587
+ registryConfig: RegistryConfig,
1588
+ _runConfig: RunConfig,
1589
+ driver: ManagerDriver,
1590
+ ): Promise<Response> {
1591
+ try {
1592
+ const subpath = c.req.path.split("/raw/http/")[1] || "";
1593
+ logger().debug("raw http request received", { subpath });
1594
+
1595
+ // Get actor query from header (consistent with other endpoints)
1596
+ const queryHeader = c.req.header(HEADER_ACTOR_QUERY);
1597
+ if (!queryHeader) {
1598
+ throw new errors.InvalidRequest("Missing actor query header");
1599
+ }
1600
+ const query: ActorQuery = JSON.parse(queryHeader);
1601
+
1602
+ // Parse connection parameters for authentication
1603
+ const connParamsHeader = c.req.header(HEADER_CONN_PARAMS);
1604
+ const connParams = connParamsHeader
1605
+ ? JSON.parse(connParamsHeader)
1606
+ : undefined;
1607
+
1608
+ // Authenticate the request
1609
+ const authData = await authenticateEndpoint(
1610
+ c,
1611
+ driver,
1612
+ registryConfig,
1613
+ query,
1614
+ ["action"],
1615
+ connParams,
1616
+ );
1617
+
1618
+ // Get the actor ID
1619
+ const { actorId } = await queryActor(c, query, driver);
1620
+ logger().debug("found actor for raw http", { actorId });
1621
+ invariant(actorId, "Missing actor ID");
1622
+
1623
+ // Preserve the original URL's query parameters
1624
+ const originalUrl = new URL(c.req.url);
1625
+ const url = new URL(
1626
+ `http://actor/raw/http/${subpath}${originalUrl.search}`,
1627
+ );
1628
+
1629
+ // Forward the request to the actor
1630
+
1631
+ logger().debug("rewriting http url", {
1632
+ from: c.req.url,
1633
+ to: url,
1634
+ });
1635
+
1636
+ const proxyRequestHeaders = new Headers(c.req.raw.headers);
1637
+ if (connParams) {
1638
+ proxyRequestHeaders.set(HEADER_CONN_PARAMS, JSON.stringify(connParams));
1639
+ }
1640
+ if (authData) {
1641
+ proxyRequestHeaders.set(HEADER_AUTH_DATA, JSON.stringify(authData));
1642
+ }
1643
+
1644
+ const proxyRequest = new Request(url, {
1645
+ method: c.req.method,
1646
+ headers: proxyRequestHeaders,
1647
+ body: c.req.raw.body,
1648
+ });
1649
+
1650
+ return await driver.proxyRequest(c, proxyRequest, actorId);
1651
+ } catch (error) {
1652
+ logger().error("error in raw http handler", {
1653
+ error: stringifyError(error),
1654
+ });
1655
+
1656
+ // Use ProxyError if it's not already an ActorError
1657
+ if (!errors.ActorError.isActorError(error)) {
1658
+ throw new errors.ProxyError("Raw HTTP request", error);
1659
+ } else {
1660
+ throw error;
1661
+ }
1662
+ }
1663
+ }
1664
+
1665
+ /**
1666
+ * Handle raw WebSocket requests to an actor
1667
+ */
1668
+ async function handleRawWebSocketRequest(
1669
+ c: HonoContext,
1670
+ registryConfig: RegistryConfig,
1671
+ runConfig: RunConfig,
1672
+ driver: ManagerDriver,
1673
+ ): Promise<Response> {
1674
+ const upgradeWebSocket = runConfig.getUpgradeWebSocket?.();
1675
+ if (!upgradeWebSocket) {
1676
+ return c.text("WebSockets are not enabled for this driver.", 400);
1677
+ }
1678
+
1679
+ try {
1680
+ const subpath = c.req.path.split("/raw/websocket/")[1] || "";
1681
+ logger().debug("raw websocket request received", { subpath });
1682
+
1683
+ // Parse protocols from Sec-WebSocket-Protocol header
1684
+ const protocols = c.req.header("sec-websocket-protocol");
1685
+ const {
1686
+ queryRaw: queryFromProtocol,
1687
+ connParamsRaw: connParamsFromProtocol,
1688
+ } = parseWebSocketProtocols(protocols);
1689
+
1690
+ if (!queryFromProtocol) {
1691
+ throw new errors.InvalidRequest("Missing query in WebSocket protocol");
1692
+ }
1693
+ const query = JSON.parse(queryFromProtocol);
1694
+
1695
+ // Parse connection parameters from protocol
1696
+ let connParams: unknown;
1697
+ if (connParamsFromProtocol) {
1698
+ connParams = JSON.parse(connParamsFromProtocol);
1699
+ }
1700
+
1701
+ // Authenticate the request
1702
+ const authData = await authenticateEndpoint(
1703
+ c,
1704
+ driver,
1705
+ registryConfig,
1706
+ query,
1707
+ ["action"],
1708
+ connParams,
1709
+ );
1710
+
1711
+ // Get the actor ID
1712
+ const { actorId } = await queryActor(c, query, driver);
1713
+ logger().debug("found actor for raw websocket", { actorId });
1714
+ invariant(actorId, "Missing actor ID");
1715
+
1716
+ logger().debug("using custom proxy mode for raw websocket");
1717
+
1718
+ // Preserve the original URL's query parameters
1719
+ const originalUrl = new URL(c.req.url);
1720
+ const proxyPath = `${PATH_RAW_WEBSOCKET_PREFIX}${subpath}${originalUrl.search}`;
1721
+
1722
+ logger().debug("manager router proxyWebSocket", {
1723
+ originalUrl: c.req.url,
1724
+ subpath,
1725
+ search: originalUrl.search,
1726
+ proxyPath,
1727
+ });
1728
+
1729
+ // For raw WebSocket, we need to use proxyWebSocket instead of proxyRequest
1730
+ return await driver.proxyWebSocket(
1731
+ c,
1732
+ proxyPath,
1733
+ actorId,
1734
+ "json", // Default encoding for raw WebSocket
1735
+ connParams,
1736
+ authData,
1737
+ );
1738
+ } catch (error) {
1739
+ // If we receive an error during setup, we send the error and close the socket immediately
1740
+ //
1741
+ // We have to return the error over WS since WebSocket clients cannot read vanilla HTTP responses
1742
+
1743
+ const { code } = deconstructError(error, logger(), {
1744
+ wsEvent: "setup",
1745
+ });
1746
+
1747
+ return await upgradeWebSocket(() => ({
1748
+ onOpen: (_evt: unknown, ws: WSContext) => {
1749
+ // Close with message so we can see the error on the client
1750
+ ws.close(1011, code);
1751
+ },
1752
+ }))(c, noopNext());
1753
+ }
1754
+ }
1755
+
1756
+ function universalActorProxy({
1757
+ registryConfig,
1758
+ runConfig,
1759
+ driver,
1760
+ }: {
1761
+ registryConfig: RegistryConfig;
1762
+ runConfig: RunConfig;
1763
+ driver: ManagerDriver;
1764
+ }): MiddlewareHandler {
1765
+ return async (c, _next) => {
1766
+ if (c.req.header("upgrade") === "websocket") {
1767
+ return handleRawWebSocketRequest(c, registryConfig, runConfig, driver);
1768
+ } else {
1769
+ const queryHeader = c.req.header(HEADER_ACTOR_QUERY);
1770
+ if (!queryHeader) {
1771
+ throw new errors.InvalidRequest("Missing actor query header");
1772
+ }
1773
+ const query = ActorQuerySchema.parse(JSON.parse(queryHeader));
1774
+
1775
+ const { actorId } = await queryActor(c, query, driver);
1776
+
1777
+ const url = new URL(c.req.url);
1778
+ url.hostname = "actor";
1779
+ url.pathname = url.pathname
1780
+ .replace(new RegExp(`^${runConfig.basePath}`, ""), "")
1781
+ .replace(/^\/?registry\/actors/, "")
1782
+ .replace(/^\/?actors/, ""); // Remove /registry prefix if present
1783
+
1784
+ const proxyRequest = new Request(url, {
1785
+ method: c.req.method,
1786
+ headers: c.req.raw.headers,
1787
+ body: c.req.raw.body,
1788
+ });
1789
+ return await driver.proxyRequest(c, proxyRequest, actorId);
1790
+ }
1791
+ };
1792
+ }