rivetkit 2.0.2 → 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.
- package/dist/schemas/actor-persist/v1.ts +228 -0
- package/dist/schemas/client-protocol/v1.ts +429 -0
- package/dist/schemas/file-system-driver/v1.ts +102 -0
- package/dist/tsup/actor/errors.cjs +69 -0
- package/dist/tsup/actor/errors.cjs.map +1 -0
- package/dist/tsup/actor/errors.d.cts +143 -0
- package/dist/tsup/actor/errors.d.ts +143 -0
- package/dist/tsup/actor/errors.js +69 -0
- package/dist/tsup/actor/errors.js.map +1 -0
- package/dist/tsup/chunk-2CRLFV6Z.cjs +202 -0
- package/dist/tsup/chunk-2CRLFV6Z.cjs.map +1 -0
- package/dist/tsup/chunk-3H7O2A7I.js +525 -0
- package/dist/tsup/chunk-3H7O2A7I.js.map +1 -0
- package/dist/tsup/chunk-42I3OZ3Q.js +15 -0
- package/dist/tsup/chunk-42I3OZ3Q.js.map +1 -0
- package/dist/tsup/chunk-4NSUQZ2H.js +1790 -0
- package/dist/tsup/chunk-4NSUQZ2H.js.map +1 -0
- package/dist/tsup/chunk-6PDXBYI5.js +132 -0
- package/dist/tsup/chunk-6PDXBYI5.js.map +1 -0
- package/dist/tsup/chunk-6WKQDDUD.cjs +1790 -0
- package/dist/tsup/chunk-6WKQDDUD.cjs.map +1 -0
- package/dist/tsup/chunk-CTBOSFUH.cjs +116 -0
- package/dist/tsup/chunk-CTBOSFUH.cjs.map +1 -0
- package/dist/tsup/chunk-EGVZZFE2.js +2857 -0
- package/dist/tsup/chunk-EGVZZFE2.js.map +1 -0
- package/dist/tsup/chunk-FCCPJNMA.cjs +132 -0
- package/dist/tsup/chunk-FCCPJNMA.cjs.map +1 -0
- package/dist/tsup/chunk-FLMTTN27.js +244 -0
- package/dist/tsup/chunk-FLMTTN27.js.map +1 -0
- package/dist/tsup/chunk-GIR3AFFI.cjs +315 -0
- package/dist/tsup/chunk-GIR3AFFI.cjs.map +1 -0
- package/dist/tsup/chunk-INGJP237.js +315 -0
- package/dist/tsup/chunk-INGJP237.js.map +1 -0
- package/dist/tsup/chunk-KJCJLKRM.js +116 -0
- package/dist/tsup/chunk-KJCJLKRM.js.map +1 -0
- package/dist/tsup/chunk-KUPQZYUQ.cjs +15 -0
- package/dist/tsup/chunk-KUPQZYUQ.cjs.map +1 -0
- package/dist/tsup/chunk-O2MBYIXO.cjs +2857 -0
- package/dist/tsup/chunk-O2MBYIXO.cjs.map +1 -0
- package/dist/tsup/chunk-OGAPU3UG.cjs +525 -0
- package/dist/tsup/chunk-OGAPU3UG.cjs.map +1 -0
- package/dist/tsup/chunk-OV6AYD4S.js +4406 -0
- package/dist/tsup/chunk-OV6AYD4S.js.map +1 -0
- package/dist/tsup/chunk-PO4VLDWA.js +47 -0
- package/dist/tsup/chunk-PO4VLDWA.js.map +1 -0
- package/dist/tsup/chunk-R2OPSKIV.cjs +244 -0
- package/dist/tsup/chunk-R2OPSKIV.cjs.map +1 -0
- package/dist/tsup/chunk-TZJKSBUQ.cjs +47 -0
- package/dist/tsup/chunk-TZJKSBUQ.cjs.map +1 -0
- package/dist/tsup/chunk-UBUC5C3G.cjs +189 -0
- package/dist/tsup/chunk-UBUC5C3G.cjs.map +1 -0
- package/dist/tsup/chunk-UIM22YJL.cjs +4406 -0
- package/dist/tsup/chunk-UIM22YJL.cjs.map +1 -0
- package/dist/tsup/chunk-URVFQMYI.cjs +230 -0
- package/dist/tsup/chunk-URVFQMYI.cjs.map +1 -0
- package/dist/tsup/chunk-UVUPOS46.js +230 -0
- package/dist/tsup/chunk-UVUPOS46.js.map +1 -0
- package/dist/tsup/chunk-VRRHBNJC.js +189 -0
- package/dist/tsup/chunk-VRRHBNJC.js.map +1 -0
- package/dist/tsup/chunk-XFSS33EQ.js +202 -0
- package/dist/tsup/chunk-XFSS33EQ.js.map +1 -0
- package/dist/tsup/client/mod.cjs +32 -0
- package/dist/tsup/client/mod.cjs.map +1 -0
- package/dist/tsup/client/mod.d.cts +26 -0
- package/dist/tsup/client/mod.d.ts +26 -0
- package/dist/tsup/client/mod.js +32 -0
- package/dist/tsup/client/mod.js.map +1 -0
- package/dist/tsup/common/log.cjs +13 -0
- package/dist/tsup/common/log.cjs.map +1 -0
- package/dist/tsup/common/log.d.cts +20 -0
- package/dist/tsup/common/log.d.ts +20 -0
- package/dist/tsup/common/log.js +13 -0
- package/dist/tsup/common/log.js.map +1 -0
- package/dist/tsup/common/websocket.cjs +10 -0
- package/dist/tsup/common/websocket.cjs.map +1 -0
- package/dist/tsup/common/websocket.d.cts +3 -0
- package/dist/tsup/common/websocket.d.ts +3 -0
- package/dist/tsup/common/websocket.js +10 -0
- package/dist/tsup/common/websocket.js.map +1 -0
- package/dist/tsup/common-CpqORuCq.d.cts +218 -0
- package/dist/tsup/common-CpqORuCq.d.ts +218 -0
- package/dist/tsup/connection-BR_Ve4ku.d.cts +2117 -0
- package/dist/tsup/connection-BwUMoe6n.d.ts +2117 -0
- package/dist/tsup/driver-helpers/mod.cjs +33 -0
- package/dist/tsup/driver-helpers/mod.cjs.map +1 -0
- package/dist/tsup/driver-helpers/mod.d.cts +18 -0
- package/dist/tsup/driver-helpers/mod.d.ts +18 -0
- package/dist/tsup/driver-helpers/mod.js +33 -0
- package/dist/tsup/driver-helpers/mod.js.map +1 -0
- package/dist/tsup/driver-test-suite/mod.cjs +4619 -0
- package/dist/tsup/driver-test-suite/mod.cjs.map +1 -0
- package/dist/tsup/driver-test-suite/mod.d.cts +57 -0
- package/dist/tsup/driver-test-suite/mod.d.ts +57 -0
- package/dist/tsup/driver-test-suite/mod.js +4619 -0
- package/dist/tsup/driver-test-suite/mod.js.map +1 -0
- package/dist/tsup/inspector/mod.cjs +53 -0
- package/dist/tsup/inspector/mod.cjs.map +1 -0
- package/dist/tsup/inspector/mod.d.cts +408 -0
- package/dist/tsup/inspector/mod.d.ts +408 -0
- package/dist/tsup/inspector/mod.js +53 -0
- package/dist/tsup/inspector/mod.js.map +1 -0
- package/dist/tsup/mod.cjs +73 -0
- package/dist/tsup/mod.cjs.map +1 -0
- package/dist/tsup/mod.d.cts +100 -0
- package/dist/tsup/mod.d.ts +100 -0
- package/dist/tsup/mod.js +73 -0
- package/dist/tsup/mod.js.map +1 -0
- package/dist/tsup/router-endpoints-AYkXG8Tl.d.cts +66 -0
- package/dist/tsup/router-endpoints-DAbqVFx2.d.ts +66 -0
- package/dist/tsup/test/mod.cjs +21 -0
- package/dist/tsup/test/mod.cjs.map +1 -0
- package/dist/tsup/test/mod.d.cts +27 -0
- package/dist/tsup/test/mod.d.ts +27 -0
- package/dist/tsup/test/mod.js +21 -0
- package/dist/tsup/test/mod.js.map +1 -0
- package/dist/tsup/utils-CT0cv4jd.d.cts +17 -0
- package/dist/tsup/utils-CT0cv4jd.d.ts +17 -0
- package/dist/tsup/utils.cjs +26 -0
- package/dist/tsup/utils.cjs.map +1 -0
- package/dist/tsup/utils.d.cts +36 -0
- package/dist/tsup/utils.d.ts +36 -0
- package/dist/tsup/utils.js +26 -0
- package/dist/tsup/utils.js.map +1 -0
- package/package.json +208 -5
- package/src/actor/action.ts +182 -0
- package/src/actor/config.ts +765 -0
- package/src/actor/connection.ts +260 -0
- package/src/actor/context.ts +171 -0
- package/src/actor/database.ts +23 -0
- package/src/actor/definition.ts +86 -0
- package/src/actor/driver.ts +84 -0
- package/src/actor/errors.ts +360 -0
- package/src/actor/generic-conn-driver.ts +234 -0
- package/src/actor/instance.ts +1800 -0
- package/src/actor/log.ts +15 -0
- package/src/actor/mod.ts +113 -0
- package/src/actor/persisted.ts +42 -0
- package/src/actor/protocol/old.ts +281 -0
- package/src/actor/protocol/serde.ts +131 -0
- package/src/actor/router-endpoints.ts +685 -0
- package/src/actor/router.ts +263 -0
- package/src/actor/schedule.ts +17 -0
- package/src/actor/unstable-react.ts +110 -0
- package/src/actor/utils.ts +98 -0
- package/src/client/actor-common.ts +30 -0
- package/src/client/actor-conn.ts +804 -0
- package/src/client/actor-handle.ts +208 -0
- package/src/client/client.ts +623 -0
- package/src/client/errors.ts +41 -0
- package/src/client/http-client-driver.ts +326 -0
- package/src/client/log.ts +7 -0
- package/src/client/mod.ts +56 -0
- package/src/client/raw-utils.ts +92 -0
- package/src/client/test.ts +44 -0
- package/src/client/utils.ts +150 -0
- package/src/common/eventsource-interface.ts +47 -0
- package/src/common/eventsource.ts +80 -0
- package/src/common/fake-event-source.ts +266 -0
- package/src/common/inline-websocket-adapter2.ts +445 -0
- package/src/common/log-levels.ts +27 -0
- package/src/common/log.ts +139 -0
- package/src/common/logfmt.ts +228 -0
- package/src/common/network.ts +2 -0
- package/src/common/router.ts +87 -0
- package/src/common/utils.ts +322 -0
- package/src/common/versioned-data.ts +95 -0
- package/src/common/websocket-interface.ts +49 -0
- package/src/common/websocket.ts +43 -0
- package/src/driver-helpers/mod.ts +22 -0
- package/src/driver-helpers/utils.ts +17 -0
- package/src/driver-test-suite/log.ts +7 -0
- package/src/driver-test-suite/mod.ts +213 -0
- package/src/driver-test-suite/test-inline-client-driver.ts +402 -0
- package/src/driver-test-suite/tests/action-features.ts +136 -0
- package/src/driver-test-suite/tests/actor-auth.ts +591 -0
- package/src/driver-test-suite/tests/actor-conn-state.ts +249 -0
- package/src/driver-test-suite/tests/actor-conn.ts +349 -0
- package/src/driver-test-suite/tests/actor-driver.ts +25 -0
- package/src/driver-test-suite/tests/actor-error-handling.ts +158 -0
- package/src/driver-test-suite/tests/actor-handle.ts +259 -0
- package/src/driver-test-suite/tests/actor-inline-client.ts +152 -0
- package/src/driver-test-suite/tests/actor-inspector.ts +570 -0
- package/src/driver-test-suite/tests/actor-metadata.ts +116 -0
- package/src/driver-test-suite/tests/actor-onstatechange.ts +95 -0
- package/src/driver-test-suite/tests/actor-schedule.ts +108 -0
- package/src/driver-test-suite/tests/actor-sleep.ts +413 -0
- package/src/driver-test-suite/tests/actor-state.ts +54 -0
- package/src/driver-test-suite/tests/actor-vars.ts +93 -0
- package/src/driver-test-suite/tests/manager-driver.ts +365 -0
- package/src/driver-test-suite/tests/raw-http-direct-registry.ts +226 -0
- package/src/driver-test-suite/tests/raw-http-request-properties.ts +414 -0
- package/src/driver-test-suite/tests/raw-http.ts +347 -0
- package/src/driver-test-suite/tests/raw-websocket-direct-registry.ts +392 -0
- package/src/driver-test-suite/tests/raw-websocket.ts +484 -0
- package/src/driver-test-suite/tests/request-access.ts +244 -0
- package/src/driver-test-suite/utils.ts +68 -0
- package/src/drivers/default.ts +31 -0
- package/src/drivers/engine/actor-driver.ts +360 -0
- package/src/drivers/engine/api-endpoints.ts +128 -0
- package/src/drivers/engine/api-utils.ts +70 -0
- package/src/drivers/engine/config.ts +39 -0
- package/src/drivers/engine/keys.test.ts +266 -0
- package/src/drivers/engine/keys.ts +89 -0
- package/src/drivers/engine/kv.ts +3 -0
- package/src/drivers/engine/log.ts +7 -0
- package/src/drivers/engine/manager-driver.ts +391 -0
- package/src/drivers/engine/mod.ts +36 -0
- package/src/drivers/engine/ws-proxy.ts +170 -0
- package/src/drivers/file-system/actor.ts +91 -0
- package/src/drivers/file-system/global-state.ts +673 -0
- package/src/drivers/file-system/log.ts +7 -0
- package/src/drivers/file-system/manager.ts +306 -0
- package/src/drivers/file-system/mod.ts +48 -0
- package/src/drivers/file-system/utils.ts +109 -0
- package/src/globals.d.ts +6 -0
- package/src/inline-client-driver/log.ts +7 -0
- package/src/inline-client-driver/mod.ts +385 -0
- package/src/inspector/actor.ts +298 -0
- package/src/inspector/config.ts +83 -0
- package/src/inspector/log.ts +5 -0
- package/src/inspector/manager.ts +86 -0
- package/src/inspector/mod.ts +2 -0
- package/src/inspector/protocol/actor.ts +10 -0
- package/src/inspector/protocol/common.ts +196 -0
- package/src/inspector/protocol/manager.ts +10 -0
- package/src/inspector/protocol/mod.ts +2 -0
- package/src/inspector/utils.ts +76 -0
- package/src/manager/auth.ts +121 -0
- package/src/manager/driver.ts +80 -0
- package/src/manager/hono-websocket-adapter.ts +333 -0
- package/src/manager/log.ts +7 -0
- package/src/manager/mod.ts +2 -0
- package/src/manager/protocol/mod.ts +24 -0
- package/src/manager/protocol/query.ts +89 -0
- package/src/manager/router.ts +1792 -0
- package/src/mod.ts +20 -0
- package/src/registry/config.ts +32 -0
- package/src/registry/log.ts +7 -0
- package/src/registry/mod.ts +124 -0
- package/src/registry/run-config.ts +54 -0
- package/src/registry/serve.ts +53 -0
- package/src/schemas/actor-persist/mod.ts +1 -0
- package/src/schemas/actor-persist/versioned.ts +25 -0
- package/src/schemas/client-protocol/mod.ts +1 -0
- package/src/schemas/client-protocol/versioned.ts +63 -0
- package/src/schemas/file-system-driver/mod.ts +1 -0
- package/src/schemas/file-system-driver/versioned.ts +28 -0
- package/src/serde.ts +84 -0
- package/src/test/config.ts +16 -0
- package/src/test/log.ts +7 -0
- package/src/test/mod.ts +153 -0
- package/src/utils.ts +172 -0
- 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
|
+
}
|