@nylorun/runtime 0.13.0-beta → 0.14.0-beta
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/CHANGELOG.md +330 -0
- package/README.md +69 -15
- package/dist/adapters/execution/restate.d.ts +7 -2
- package/dist/adapters/execution/restate.js +38 -8
- package/dist/adapters/replication/pgoutput.d.ts +16 -0
- package/dist/adapters/replication/pgoutput.js +169 -0
- package/dist/adapters/streams/s2.js +37 -20
- package/dist/admin-openapi.json +1071 -0
- package/dist/api/a2a/card.d.ts +15 -0
- package/dist/api/a2a/card.js +24 -0
- package/dist/api/a2a/endpoint.d.ts +8 -0
- package/dist/api/a2a/endpoint.js +81 -0
- package/dist/api/a2a/protocol.d.ts +144 -0
- package/dist/api/a2a/protocol.js +188 -0
- package/dist/api/a2a/routes.d.ts +19 -0
- package/dist/api/a2a/routes.js +289 -0
- package/dist/api/a2a/tasks.d.ts +50 -0
- package/dist/api/a2a/tasks.js +186 -0
- package/dist/api/ag-ui/endpoint.d.ts +8 -0
- package/dist/api/ag-ui/endpoint.js +126 -0
- package/dist/api/ag-ui/history.d.ts +7 -0
- package/dist/api/ag-ui/history.js +60 -0
- package/dist/api/ag-ui/routes.d.ts +32 -0
- package/dist/api/ag-ui/routes.js +305 -0
- package/dist/api/ag-ui/session-id.d.ts +1 -0
- package/dist/api/ag-ui/session-id.js +12 -0
- package/dist/api/ag-ui/sse.d.ts +9 -0
- package/dist/api/ag-ui/sse.js +7 -0
- package/dist/api/ag-ui/translate.d.ts +38 -0
- package/dist/api/ag-ui/translate.js +222 -0
- package/dist/api/components.d.ts +7825 -0
- package/dist/api/components.js +83 -0
- package/dist/api/http/app.d.ts +18 -0
- package/dist/api/http/app.js +58 -0
- package/dist/api/http/body.d.ts +4 -0
- package/dist/api/http/body.js +37 -0
- package/dist/api/http/define.d.ts +39 -0
- package/dist/api/http/define.js +130 -0
- package/dist/api/http/respond.d.ts +9 -0
- package/dist/api/http/respond.js +44 -0
- package/dist/api/http/routes/access.d.ts +24 -0
- package/dist/api/http/routes/access.js +259 -0
- package/dist/api/http/routes/actions.d.ts +8 -0
- package/dist/api/http/routes/actions.js +76 -0
- package/dist/api/http/routes/endpoints.d.ts +7 -0
- package/dist/api/http/routes/endpoints.js +100 -0
- package/dist/api/http/routes/sessions.d.ts +3 -0
- package/dist/api/http/routes/sessions.js +205 -0
- package/dist/api/http/routes/tenant.d.ts +10 -0
- package/dist/api/http/routes/tenant.js +172 -0
- package/dist/api/http/routes/vaults.d.ts +8 -0
- package/dist/api/http/routes/vaults.js +167 -0
- package/dist/api/openapi.d.ts +19 -0
- package/dist/api/openapi.js +138 -0
- package/dist/api/route.d.ts +12 -0
- package/dist/api/route.js +4 -0
- package/dist/core/bearer.d.ts +8 -0
- package/dist/core/bearer.js +11 -0
- package/dist/core/flow-host.d.ts +7 -15
- package/dist/core/flow-host.js +16 -41
- package/dist/core/sandbox-routes.d.ts +12 -7
- package/dist/core/sandbox-routes.js +24 -25
- package/dist/execution/memory.d.ts +8 -5
- package/dist/execution/memory.js +33 -13
- package/dist/execution/types.d.ts +38 -6
- package/dist/execution/types.js +12 -3
- package/dist/host/admin-api.d.ts +22 -0
- package/dist/host/admin-api.js +178 -0
- package/dist/host/app.d.ts +34 -0
- package/dist/host/app.js +185 -0
- package/dist/host/config-for.d.ts +2 -0
- package/dist/host/config-for.js +1 -0
- package/dist/host/config.d.ts +11 -0
- package/dist/host/cors.d.ts +27 -0
- package/dist/host/cors.js +64 -0
- package/dist/host/create-host.d.ts +31 -3
- package/dist/host/create-host.js +168 -327
- package/dist/host/http.d.ts +14 -3
- package/dist/host/http.js +64 -23
- package/dist/host/main.js +55 -4
- package/dist/host/stack-config.d.ts +20 -0
- package/dist/host/stack-config.js +55 -0
- package/dist/mcp/pool.d.ts +19 -1
- package/dist/mcp/pool.js +60 -10
- package/dist/model/catalog.d.ts +2 -10
- package/dist/model/classify.d.ts +13 -0
- package/dist/model/classify.js +61 -0
- package/dist/model/models.d.ts +10 -1
- package/dist/model/models.js +14 -6
- package/dist/model/pi-model.d.ts +17 -2
- package/dist/model/pi-model.js +366 -222
- package/dist/openapi.json +25696 -0
- package/dist/sandbox/manager.d.ts +13 -1
- package/dist/sandbox/manager.js +8 -11
- package/dist/sandbox/resolve.d.ts +33 -0
- package/dist/sandbox/resolve.js +81 -0
- package/dist/sandbox/session-sandbox.d.ts +11 -0
- package/dist/sandbox/session-sandbox.js +51 -0
- package/dist/sandbox/share.d.ts +14 -2
- package/dist/sandbox/share.js +33 -3
- package/dist/sandbox/tenant-config.d.ts +37 -0
- package/dist/sandbox/tenant-config.js +51 -0
- package/dist/sandbox/tools.d.ts +7 -9
- package/dist/sandbox/tools.js +11 -3
- package/dist/sandbox/types.d.ts +8 -2
- package/dist/store/event.d.ts +19 -0
- package/dist/store/event.js +49 -0
- package/dist/store/memory.d.ts +20 -8
- package/dist/store/memory.js +265 -100
- package/dist/store/postgres/connect.d.ts +13 -0
- package/dist/store/postgres/connect.js +5 -0
- package/dist/store/postgres/migrations/003_subject_tokens.d.ts +8 -0
- package/dist/store/postgres/migrations/003_subject_tokens.js +42 -0
- package/dist/store/postgres/migrations/004_publishable_keys.d.ts +6 -0
- package/dist/store/postgres/migrations/004_publishable_keys.js +18 -0
- package/dist/store/postgres/migrations/005_action_endpoints.d.ts +6 -0
- package/dist/store/postgres/migrations/005_action_endpoints.js +32 -0
- package/dist/store/postgres/migrations/006_drop_executors.d.ts +11 -0
- package/dist/store/postgres/migrations/006_drop_executors.js +32 -0
- package/dist/store/postgres/migrations/007_event_time.d.ts +7 -0
- package/dist/store/postgres/migrations/007_event_time.js +14 -0
- package/dist/store/postgres/migrations/008_durable_streams.d.ts +13 -0
- package/dist/store/postgres/migrations/008_durable_streams.js +35 -0
- package/dist/store/postgres/migrations/index.d.ts +5 -1
- package/dist/store/postgres/migrations/index.js +22 -2
- package/dist/store/postgres/migrations/shared/index.d.ts +31 -0
- package/dist/store/postgres/migrations/shared/index.js +92 -0
- package/dist/store/postgres/record.d.ts +11 -0
- package/dist/store/postgres/record.js +64 -0
- package/dist/store/postgres/store.js +337 -95
- package/dist/store/postgres/tenants.js +14 -0
- package/dist/store/types.d.ts +176 -81
- package/dist/streams/basin.d.ts +18 -1
- package/dist/streams/basin.js +34 -2
- package/dist/streams/control.d.ts +18 -0
- package/dist/streams/control.js +29 -0
- package/dist/streams/memory.d.ts +3 -1
- package/dist/streams/memory.js +14 -8
- package/dist/streams/relay/core.d.ts +35 -0
- package/dist/streams/relay/core.js +366 -0
- package/dist/streams/relay/memory.d.ts +69 -0
- package/dist/streams/relay/memory.js +176 -0
- package/dist/streams/relay/types.d.ts +77 -0
- package/dist/streams/relay/types.js +7 -0
- package/dist/streams/types.d.ts +66 -36
- package/dist/streams/types.js +18 -53
- package/dist/tenant/access-policy.d.ts +30 -0
- package/dist/tenant/access-policy.js +52 -0
- package/dist/tenant/actions.d.ts +10 -149
- package/dist/tenant/actions.js +5 -167
- package/dist/tenant/advance.js +32 -0
- package/dist/tenant/auth.d.ts +19 -17
- package/dist/tenant/auth.js +90 -93
- package/dist/tenant/browser.d.ts +15 -0
- package/dist/tenant/browser.js +39 -0
- package/dist/tenant/commands.d.ts +18 -2
- package/dist/tenant/commands.js +107 -113
- package/dist/tenant/context.d.ts +75 -18
- package/dist/tenant/context.js +14 -10
- package/dist/tenant/delivery-token.d.ts +41 -0
- package/dist/tenant/delivery-token.js +105 -0
- package/dist/tenant/delivery.d.ts +62 -0
- package/dist/tenant/delivery.js +476 -0
- package/dist/tenant/effects.d.ts +5 -0
- package/dist/tenant/effects.js +78 -37
- package/dist/tenant/endpoints.d.ts +67 -0
- package/dist/tenant/endpoints.js +90 -0
- package/dist/tenant/ephemeral.d.ts +6 -0
- package/dist/tenant/ephemeral.js +60 -36
- package/dist/tenant/http.d.ts +8 -9
- package/dist/tenant/http.js +13 -27
- package/dist/tenant/jwt.d.ts +16 -0
- package/dist/tenant/jwt.js +50 -0
- package/dist/tenant/module.d.ts +3 -0
- package/dist/tenant/module.js +6 -12
- package/dist/tenant/outbound.d.ts +44 -0
- package/dist/tenant/outbound.js +127 -0
- package/dist/tenant/pool.d.ts +0 -2
- package/dist/tenant/pool.js +0 -18
- package/dist/tenant/reset.d.ts +0 -4
- package/dist/tenant/reset.js +0 -5
- package/dist/tenant/runtime.d.ts +10 -20
- package/dist/tenant/runtime.js +59 -42
- package/dist/tenant/scheduler.d.ts +7 -2
- package/dist/tenant/scheduler.js +7 -3
- package/dist/tenant/session-streams.d.ts +125 -0
- package/dist/tenant/session-streams.js +399 -0
- package/dist/tenant/sessions.d.ts +28 -4
- package/dist/tenant/sessions.js +118 -24
- package/dist/tenant/signing-keys.d.ts +55 -0
- package/dist/tenant/signing-keys.js +172 -0
- package/dist/tenant/slim.d.ts +8 -0
- package/dist/tenant/slim.js +21 -0
- package/dist/tenant/status.d.ts +0 -3
- package/dist/tenant/status.js +19 -12
- package/dist/tenant/streams.d.ts +60 -67
- package/dist/tenant/streams.js +140 -208
- package/dist/tenant/subject-limits.d.ts +16 -0
- package/dist/tenant/subject-limits.js +45 -0
- package/dist/tenant/sweep.d.ts +1 -10
- package/dist/tenant/sweep.js +29 -69
- package/dist/tenant/tokens.d.ts +14 -0
- package/dist/tenant/tokens.js +178 -0
- package/dist/tenant/transcript.d.ts +48 -5
- package/dist/tenant/transcript.js +58 -1
- package/dist/tenant/types.d.ts +29 -7
- package/dist/tenant/worker.d.ts +3 -1
- package/dist/tenant/worker.js +9 -3
- package/dist/vault/service.d.ts +18 -2
- package/dist/vault/service.js +57 -13
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +20 -8
- package/dist/core/executors.d.ts +0 -44
- package/dist/core/executors.js +0 -95
- package/dist/streams/relay.d.ts +0 -37
- package/dist/streams/relay.js +0 -254
- package/dist/tenant/live.d.ts +0 -92
- package/dist/tenant/live.js +0 -290
- package/dist/tenant/routes-tenant.d.ts +0 -12
- package/dist/tenant/routes-tenant.js +0 -137
- package/dist/tenant/routes.d.ts +0 -9
- package/dist/tenant/routes.js +0 -151
package/dist/host/create-host.js
CHANGED
|
@@ -1,42 +1,13 @@
|
|
|
1
1
|
import { createServer, } from "node:http";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { AdminStatusSchema
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
2
|
+
import { getRequestListener, RequestError } from "@hono/node-server";
|
|
3
|
+
import { HOST_PROTOCOL } from "@nylorun/core/compatibility";
|
|
4
|
+
import { AdminStatusSchema } from "@nylorun/core/contracts";
|
|
5
|
+
import { adminDocument } from "../api/openapi.js";
|
|
6
|
+
import { createAdminApi } from "./admin-api.js";
|
|
7
|
+
import { createHostApp } from "./app.js";
|
|
8
|
+
import { EXIT_NON_LOOPBACK, EXIT_PORT_IN_USE, headerValue, HostListenError, isAllowedRequestHost, isLoopbackHost, pathnameIsLogged, redactRoutePath, rejectedResponse, sendRejected, } from "./http.js";
|
|
8
9
|
import { RUNTIME_VERSION } from "../version.js";
|
|
9
|
-
|
|
10
|
-
return createHash("sha256").update(value, "utf8").digest();
|
|
11
|
-
}
|
|
12
|
-
/** Constant-time comparison of SHA-256 digests of the presented and stored admin keys. */
|
|
13
|
-
export function adminKeyMatches(presented, adminKey) {
|
|
14
|
-
if (presented === undefined)
|
|
15
|
-
return false;
|
|
16
|
-
const a = hashUtf8(presented);
|
|
17
|
-
const b = hashUtf8(adminKey);
|
|
18
|
-
return a.length === b.length && timingSafeEqual(a, b);
|
|
19
|
-
}
|
|
20
|
-
function headerValue(headers, name) {
|
|
21
|
-
const raw = headers[name.toLowerCase()];
|
|
22
|
-
if (Array.isArray(raw))
|
|
23
|
-
return raw[0];
|
|
24
|
-
return raw;
|
|
25
|
-
}
|
|
26
|
-
function parseProtocolVersion(raw) {
|
|
27
|
-
if (raw === undefined)
|
|
28
|
-
return undefined;
|
|
29
|
-
const trimmed = raw.trim();
|
|
30
|
-
if (!/^\d+$/.test(trimmed))
|
|
31
|
-
return undefined;
|
|
32
|
-
return Number(trimmed);
|
|
33
|
-
}
|
|
34
|
-
function protocolAccepted(raw) {
|
|
35
|
-
const version = parseProtocolVersion(raw);
|
|
36
|
-
if (version === undefined)
|
|
37
|
-
return false;
|
|
38
|
-
return checkCompatibility({ version, required: [] }, HOST_PROTOCOL).ok;
|
|
39
|
-
}
|
|
10
|
+
export { adminKeyMatches } from "./http.js";
|
|
40
11
|
export function createHost(options) {
|
|
41
12
|
const { hostRoot, module, config, credentials, logger, coreVersion, } = options;
|
|
42
13
|
const pid = options.pid ?? process.pid;
|
|
@@ -46,6 +17,11 @@ export function createHost(options) {
|
|
|
46
17
|
let server;
|
|
47
18
|
let url = "";
|
|
48
19
|
let listenPort = bindPort;
|
|
20
|
+
const operator = options.operator;
|
|
21
|
+
let operatorServer;
|
|
22
|
+
let operatorPort = operator?.port ?? 0;
|
|
23
|
+
let adminUrl = "";
|
|
24
|
+
const mainRole = operator ? "public" : "combined";
|
|
49
25
|
let closing = false;
|
|
50
26
|
let closePromise;
|
|
51
27
|
const adminStatusBody = async () => {
|
|
@@ -68,276 +44,87 @@ export function createHost(options) {
|
|
|
68
44
|
},
|
|
69
45
|
});
|
|
70
46
|
};
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
message: "activeWork must be refuse, drain, or cancel",
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
try {
|
|
136
|
-
await module.delete(id, activeWork);
|
|
137
|
-
response.writeHead(204);
|
|
138
|
-
response.end();
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
catch (error) {
|
|
142
|
-
if (error instanceof TenantNotFoundError)
|
|
143
|
-
return sendOpaqueNotFound(response);
|
|
144
|
-
const code = error.code;
|
|
145
|
-
if (error instanceof TenantBusyError ||
|
|
146
|
-
error.name === "TenantBusyError" ||
|
|
147
|
-
code === "active_work" ||
|
|
148
|
-
code === "conflict") {
|
|
149
|
-
return sendRejected(response, 409, "active_work", error instanceof Error ? error.message : "Active work");
|
|
150
|
-
}
|
|
151
|
-
throw error;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
// D12: /v1/admin/status and /v1/admin/host share one handler.
|
|
157
|
-
if ((segments[2] === "host" || segments[2] === "status") &&
|
|
158
|
-
segments.length === 3 &&
|
|
159
|
-
method === "GET") {
|
|
160
|
-
return sendJson(response, 200, await adminStatusBody());
|
|
161
|
-
}
|
|
162
|
-
if (segments[2] === "host" &&
|
|
163
|
-
segments.length === 4 &&
|
|
164
|
-
segments[3] === "shutdown" &&
|
|
165
|
-
method === "POST") {
|
|
166
|
-
sendJson(response, 200, { status: "shutting_down" });
|
|
167
|
-
void close();
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
return sendRejected(response, 404, "not_found", "Route not found");
|
|
171
|
-
};
|
|
172
|
-
const handle = async (request, response) => {
|
|
173
|
-
const started = Date.now();
|
|
174
|
-
let tenantId;
|
|
175
|
-
let statusCode = 500;
|
|
176
|
-
try {
|
|
177
|
-
const urlObj = new URL(request.url ?? "/", "http://runtime.local");
|
|
178
|
-
const pathname = urlObj.pathname;
|
|
179
|
-
// D§11: Host, Origin, then Content-Type — before any other processing.
|
|
180
|
-
const hostHeader = headerValue(request.headers, "host");
|
|
181
|
-
if (!isAllowedRequestHost(hostHeader, {
|
|
182
|
-
port: listenPort,
|
|
183
|
-
host: config.host,
|
|
184
|
-
allowNonLoopback: config.allowNonLoopback,
|
|
185
|
-
...(containerListen
|
|
186
|
-
? { allowedHosts: containerListen.allowedHosts }
|
|
187
|
-
: {}),
|
|
188
|
-
})) {
|
|
189
|
-
sendRejected(response, 421, "host_rejected", "Host header is not an allowed loopback or configured address");
|
|
190
|
-
statusCode = 421;
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
if (headerValue(request.headers, "origin") !== undefined) {
|
|
194
|
-
sendRejected(response, 403, "origin_rejected", "Browser Origin headers are not accepted");
|
|
195
|
-
statusCode = 403;
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
if (requestHasBody(request) &&
|
|
199
|
-
!isJsonContentType(headerValue(request.headers, "content-type"))) {
|
|
200
|
-
sendRejected(response, 415, "unsupported_media_type", "Request bodies must use application/json");
|
|
201
|
-
statusCode = 415;
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
if (pathname === "/health") {
|
|
205
|
-
sendJson(response, 200, {
|
|
206
|
-
status: "ok",
|
|
207
|
-
service: "nylorun-runtime",
|
|
208
|
-
version: RUNTIME_VERSION,
|
|
209
|
-
protocol: {
|
|
210
|
-
min: HOST_PROTOCOL.min,
|
|
211
|
-
max: HOST_PROTOCOL.max,
|
|
212
|
-
features: [...HOST_PROTOCOL.features],
|
|
213
|
-
},
|
|
214
|
-
coreVersion,
|
|
215
|
-
hostId: config.hostId,
|
|
216
|
-
pid,
|
|
217
|
-
});
|
|
218
|
-
statusCode = 200;
|
|
219
|
-
return;
|
|
220
|
-
}
|
|
221
|
-
if (pathname === "/ready") {
|
|
222
|
-
const listener = Boolean(server?.listening);
|
|
223
|
-
const discovery = module.started;
|
|
224
|
-
const infra = await options.readiness?.();
|
|
225
|
-
const ready = listener && discovery && !closing && (infra?.ok ?? true);
|
|
226
|
-
sendJson(response, ready ? 200 : 503, {
|
|
227
|
-
status: ready ? "ready" : "not_ready",
|
|
228
|
-
service: "nylorun-runtime",
|
|
229
|
-
checks: { listener, discovery, ...infra?.checks },
|
|
230
|
-
});
|
|
231
|
-
statusCode = ready ? 200 : 503;
|
|
232
|
-
return;
|
|
233
|
-
}
|
|
234
|
-
const segments = pathname.split("/").filter(Boolean);
|
|
235
|
-
const isAdmin = segments[0] === "v1" && segments[1] === "admin";
|
|
236
|
-
if (isAdmin) {
|
|
237
|
-
const protocolHeader = headerValue(request.headers, PROTOCOL_HEADER);
|
|
238
|
-
if (!protocolAccepted(protocolHeader)) {
|
|
239
|
-
sendProtocolRejected(response);
|
|
240
|
-
statusCode = 426;
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
|
-
if (!requireAdmin(request, response)) {
|
|
244
|
-
statusCode = 404;
|
|
245
|
-
return;
|
|
246
|
-
}
|
|
247
|
-
await handleAdmin(request, response, urlObj, segments);
|
|
248
|
-
statusCode = response.statusCode || 200;
|
|
249
|
-
return;
|
|
250
|
-
}
|
|
251
|
-
// Tenant-scoped routes: header pattern → protocol → resolve → Tenant handle.
|
|
252
|
-
const tenantHeader = headerValue(request.headers, TENANT_HEADER);
|
|
253
|
-
if (tenantHeader === undefined || tenantHeader.trim() === "") {
|
|
254
|
-
sendJson(response, 400, {
|
|
255
|
-
status: "rejected",
|
|
256
|
-
code: "invalid_request",
|
|
257
|
-
message: `${TENANT_HEADER} header is required`,
|
|
258
|
-
});
|
|
259
|
-
statusCode = 400;
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
if (!isTenantId(tenantHeader)) {
|
|
263
|
-
sendJson(response, 400, {
|
|
264
|
-
status: "rejected",
|
|
265
|
-
code: "invalid_request",
|
|
266
|
-
message: `${TENANT_HEADER} header is malformed`,
|
|
267
|
-
});
|
|
268
|
-
statusCode = 400;
|
|
269
|
-
return;
|
|
270
|
-
}
|
|
271
|
-
tenantId = tenantHeader;
|
|
272
|
-
const protocolHeader = headerValue(request.headers, PROTOCOL_HEADER);
|
|
273
|
-
if (!protocolAccepted(protocolHeader)) {
|
|
274
|
-
sendProtocolRejected(response);
|
|
275
|
-
statusCode = 426;
|
|
276
|
-
return;
|
|
277
|
-
}
|
|
278
|
-
const resolution = await module.resolve(tenantId);
|
|
279
|
-
if (resolution.kind !== "open") {
|
|
280
|
-
if (resolution.kind === "quarantined") {
|
|
281
|
-
logger.warn("tenant_quarantined", {
|
|
282
|
-
tenantId,
|
|
283
|
-
code: resolution.quarantine.code,
|
|
284
|
-
repair: resolution.quarantine.repair,
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
sendOpaqueNotFound(response);
|
|
288
|
-
statusCode = 404;
|
|
289
|
-
return;
|
|
290
|
-
}
|
|
291
|
-
await resolution.handle.handle(request, response, urlObj);
|
|
292
|
-
statusCode = response.statusCode || 200;
|
|
293
|
-
}
|
|
294
|
-
catch (error) {
|
|
295
|
-
const status = error.status;
|
|
296
|
-
if (typeof status === "number" && status >= 400 && status < 600) {
|
|
297
|
-
sendJson(response, status, {
|
|
298
|
-
status: "rejected",
|
|
299
|
-
code: status === 400 ? "invalid_request" : "request_rejected",
|
|
300
|
-
message: error instanceof Error ? error.message : "Request rejected",
|
|
301
|
-
});
|
|
302
|
-
statusCode = status;
|
|
303
|
-
}
|
|
304
|
-
else if (error &&
|
|
305
|
-
typeof error === "object" &&
|
|
306
|
-
"name" in error &&
|
|
307
|
-
error.name === "ZodError") {
|
|
308
|
-
sendJson(response, 400, {
|
|
309
|
-
status: "rejected",
|
|
310
|
-
code: "invalid_request",
|
|
311
|
-
message: "Invalid request body",
|
|
312
|
-
});
|
|
313
|
-
statusCode = 400;
|
|
314
|
-
}
|
|
315
|
-
else {
|
|
316
|
-
logger.error("request_failed", {
|
|
317
|
-
error: error instanceof Error ? error.message : String(error),
|
|
318
|
-
});
|
|
319
|
-
if (!response.headersSent) {
|
|
320
|
-
sendJson(response, 500, {
|
|
321
|
-
status: "rejected",
|
|
322
|
-
code: "internal_error",
|
|
323
|
-
message: "Internal error",
|
|
324
|
-
});
|
|
325
|
-
}
|
|
326
|
-
statusCode = 500;
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
finally {
|
|
330
|
-
if (pathnameIsLogged(request.url)) {
|
|
47
|
+
const adminApi = createAdminApi({
|
|
48
|
+
module,
|
|
49
|
+
status: adminStatusBody,
|
|
50
|
+
shutdown: () => void close(),
|
|
51
|
+
document: adminDocument,
|
|
52
|
+
});
|
|
53
|
+
const app = createHostApp({
|
|
54
|
+
module,
|
|
55
|
+
logger,
|
|
56
|
+
hostId: config.hostId,
|
|
57
|
+
adminKey: credentials.adminKey,
|
|
58
|
+
coreVersion,
|
|
59
|
+
pid,
|
|
60
|
+
browserAccess: options.browserAccess === true,
|
|
61
|
+
...(options.readiness ? { readiness: options.readiness } : {}),
|
|
62
|
+
listening: () => Boolean(server?.listening) && (!operator || Boolean(operatorServer?.listening)),
|
|
63
|
+
closing: () => closing,
|
|
64
|
+
admin: async (request, node) => await adminApi.fetch(request, node),
|
|
65
|
+
});
|
|
66
|
+
/** Where a listener's requests go once their `Host` header checks out. */
|
|
67
|
+
const pipeline = (role) => getRequestListener(async (request, node) => alreadySent(await app.fetch(request, { ...node, role })), {
|
|
68
|
+
// The Runtime runs inside other processes (`startEphemeralRuntime`): leave their
|
|
69
|
+
// `Request` and `Response` alone.
|
|
70
|
+
overrideGlobalObjects: false,
|
|
71
|
+
// A request target `@hono/node-server` cannot make a URL of, such as `*`.
|
|
72
|
+
errorHandler: (error) => {
|
|
73
|
+
if (error instanceof RequestError)
|
|
74
|
+
return rejectedResponse(400, "invalid_request", "Invalid request target");
|
|
75
|
+
logger.error("request_failed", {
|
|
76
|
+
error: error instanceof Error ? error.message : String(error),
|
|
77
|
+
});
|
|
78
|
+
return rejectedResponse(500, "internal_error", "Internal error");
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
/** The `Host` values a listener accepts. */
|
|
82
|
+
const hostAllowed = (role, hostHeader) => role === "operator"
|
|
83
|
+
? isAllowedRequestHost(hostHeader, {
|
|
84
|
+
port: operatorPort,
|
|
85
|
+
host: operator.host,
|
|
86
|
+
allowNonLoopback: false,
|
|
87
|
+
...(operator.allowedHosts ? { allowedHosts: operator.allowedHosts } : {}),
|
|
88
|
+
})
|
|
89
|
+
: isAllowedRequestHost(hostHeader, {
|
|
90
|
+
port: listenPort,
|
|
91
|
+
host: config.host,
|
|
92
|
+
allowNonLoopback: config.allowNonLoopback,
|
|
93
|
+
...(containerListen ? { allowedHosts: containerListen.allowedHosts } : {}),
|
|
94
|
+
});
|
|
95
|
+
/** Responses not yet finished; shutdown ends the streams among them. */
|
|
96
|
+
const inFlight = new Set();
|
|
97
|
+
const serve = (role) => {
|
|
98
|
+
const next = pipeline(role);
|
|
99
|
+
return (req, res) => {
|
|
100
|
+
const started = Date.now();
|
|
101
|
+
inFlight.add(res);
|
|
102
|
+
res.once("close", () => inFlight.delete(res));
|
|
103
|
+
// D§11: `Host` first, before anything reads the request.
|
|
104
|
+
if (hostAllowed(role, headerValue(req, "host")))
|
|
105
|
+
return void next(req, res);
|
|
106
|
+
sendRejected(res, 421, "host_rejected", "Host header is not an allowed loopback or configured address");
|
|
107
|
+
if (pathnameIsLogged(req.url))
|
|
331
108
|
logger.info("request", {
|
|
332
|
-
status:
|
|
333
|
-
tenantId,
|
|
109
|
+
status: 421,
|
|
334
110
|
durationMs: Date.now() - started,
|
|
335
|
-
path: redactRoutePath(new URL(
|
|
336
|
-
method:
|
|
111
|
+
path: redactRoutePath(new URL(req.url ?? "/", "http://runtime.local").pathname),
|
|
112
|
+
method: req.method,
|
|
337
113
|
});
|
|
338
|
-
|
|
339
|
-
}
|
|
114
|
+
};
|
|
340
115
|
};
|
|
116
|
+
/** Binds `listening`; once bound, a listener error is logged rather than lost. */
|
|
117
|
+
const bindListener = (listening, port, host) => new Promise((resolve, reject) => {
|
|
118
|
+
const failed = (error) => reject(error.code === "EADDRINUSE"
|
|
119
|
+
? new HostListenError(`Port ${port} on ${host} is already in use`, EXIT_PORT_IN_USE, error)
|
|
120
|
+
: error);
|
|
121
|
+
listening.once("error", failed);
|
|
122
|
+
listening.listen(port, host, () => {
|
|
123
|
+
listening.off("error", failed);
|
|
124
|
+
listening.on("error", (error) => logger.error("listener_error", { error: error.message }));
|
|
125
|
+
resolve();
|
|
126
|
+
});
|
|
127
|
+
});
|
|
341
128
|
async function listen() {
|
|
342
129
|
if (server)
|
|
343
130
|
throw new Error("Already listening");
|
|
@@ -348,27 +135,35 @@ export function createHost(options) {
|
|
|
348
135
|
!isLoopbackHost(config.host)) {
|
|
349
136
|
throw new HostListenError(`Refusing to bind non-loopback host ${config.host} without allowNonLoopback`, EXIT_NON_LOOPBACK);
|
|
350
137
|
}
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
await
|
|
355
|
-
server.once("error", (error) => {
|
|
356
|
-
if (error.code === "EADDRINUSE") {
|
|
357
|
-
reject(new HostListenError(`Port ${bindPort} on ${bindHost} is already in use`, EXIT_PORT_IN_USE, error));
|
|
358
|
-
return;
|
|
359
|
-
}
|
|
360
|
-
reject(error);
|
|
361
|
-
});
|
|
362
|
-
server.listen(bindPort, bindHost, resolve);
|
|
363
|
-
});
|
|
138
|
+
if (operator && !containerListen && !isLoopbackHost(operator.host))
|
|
139
|
+
throw new HostListenError(`Refusing to bind the operator listener on non-loopback host ${operator.host}`, EXIT_NON_LOOPBACK);
|
|
140
|
+
server = createServer(serve(mainRole));
|
|
141
|
+
await bindListener(server, bindPort, bindHost);
|
|
364
142
|
const address = server.address();
|
|
365
143
|
listenPort =
|
|
366
144
|
typeof address === "object" && address ? address.port : bindPort;
|
|
367
145
|
url = `http://${bindHost}:${listenPort}`;
|
|
146
|
+
adminUrl = url;
|
|
147
|
+
if (operator) {
|
|
148
|
+
operatorServer = createServer(serve("operator"));
|
|
149
|
+
await bindListener(operatorServer, operator.port, operator.host).catch(async (error) => {
|
|
150
|
+
// Leave nothing half-open: the main listener closes too.
|
|
151
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
152
|
+
server = undefined;
|
|
153
|
+
throw error;
|
|
154
|
+
});
|
|
155
|
+
const operatorAddress = operatorServer.address();
|
|
156
|
+
operatorPort =
|
|
157
|
+
typeof operatorAddress === "object" && operatorAddress
|
|
158
|
+
? operatorAddress.port
|
|
159
|
+
: operator.port;
|
|
160
|
+
adminUrl = `http://${operator.host}:${operatorPort}`;
|
|
161
|
+
}
|
|
368
162
|
await module.start();
|
|
369
163
|
logger.info("host_listening", {
|
|
370
164
|
hostId: config.hostId,
|
|
371
165
|
url,
|
|
166
|
+
...(operator ? { adminUrl } : {}),
|
|
372
167
|
pid,
|
|
373
168
|
});
|
|
374
169
|
}
|
|
@@ -393,13 +188,40 @@ export function createHost(options) {
|
|
|
393
188
|
closePromise = (async () => {
|
|
394
189
|
closing = true;
|
|
395
190
|
logger.info("host_shutdown", { hostId: config.hostId });
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
191
|
+
const listeners = [server, operatorServer].filter((listening) => listening !== undefined);
|
|
192
|
+
const stopped = Promise.all(listeners.map((listening) => new Promise((resolve) => {
|
|
193
|
+
listening.close(() => resolve());
|
|
194
|
+
listening.closeIdleConnections();
|
|
195
|
+
})));
|
|
196
|
+
// A stream never finishes by itself, and a listener closes only once every connection
|
|
197
|
+
// has: end the streams, close their connections once idle, let other requests finish,
|
|
198
|
+
// then cut what is left.
|
|
199
|
+
const streams = [...inFlight].filter(isStreaming);
|
|
200
|
+
void Promise.all(streams.map((response) => {
|
|
201
|
+
const done = new Promise((resolve) => response.once("close", resolve));
|
|
202
|
+
response.end();
|
|
203
|
+
return done;
|
|
204
|
+
})).then(() => {
|
|
205
|
+
for (const listening of listeners)
|
|
206
|
+
listening.closeIdleConnections();
|
|
207
|
+
});
|
|
208
|
+
let grace;
|
|
209
|
+
const graceful = await Promise.race([
|
|
210
|
+
stopped.then(() => true),
|
|
211
|
+
new Promise((resolve) => {
|
|
212
|
+
grace = setTimeout(() => resolve(false), SHUTDOWN_GRACE_MS);
|
|
213
|
+
grace.unref();
|
|
214
|
+
}),
|
|
215
|
+
]);
|
|
216
|
+
clearTimeout(grace);
|
|
217
|
+
if (!graceful) {
|
|
218
|
+
logger.warn("host_shutdown_forced", { openResponses: inFlight.size });
|
|
219
|
+
for (const listening of listeners)
|
|
220
|
+
listening.closeAllConnections();
|
|
221
|
+
await stopped;
|
|
402
222
|
}
|
|
223
|
+
server = undefined;
|
|
224
|
+
operatorServer = undefined;
|
|
403
225
|
await step("beforeTenants", options.shutdown?.beforeTenants);
|
|
404
226
|
await step("tenants", () => module.close());
|
|
405
227
|
await step("afterTenants", options.shutdown?.afterTenants);
|
|
@@ -413,11 +235,30 @@ export function createHost(options) {
|
|
|
413
235
|
get url() {
|
|
414
236
|
return url;
|
|
415
237
|
},
|
|
238
|
+
get adminUrl() {
|
|
239
|
+
return adminUrl;
|
|
240
|
+
},
|
|
416
241
|
};
|
|
417
242
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
243
|
+
/**
|
|
244
|
+
* An answer already written to the Node response. `@hono/node-server` skips writing one that
|
|
245
|
+
* says so in its headers, unless it is its own `Response` class, which a process that ran its
|
|
246
|
+
* `serve()` has as the global one: it writes those again. This is never that class.
|
|
247
|
+
*/
|
|
248
|
+
const SENT = {
|
|
249
|
+
status: 200,
|
|
250
|
+
headers: new Headers({ "x-hono-already-sent": "true" }),
|
|
251
|
+
body: null,
|
|
252
|
+
};
|
|
253
|
+
function alreadySent(response) {
|
|
254
|
+
return response.headers.has("x-hono-already-sent") ? SENT : response;
|
|
255
|
+
}
|
|
256
|
+
/** How long shutdown waits for requests in progress before closing their connections. */
|
|
257
|
+
const SHUTDOWN_GRACE_MS = 10_000;
|
|
258
|
+
/**
|
|
259
|
+
* A response that has started and not ended is a stream: JSON answers are written and ended
|
|
260
|
+
* at once. (`getHeader` cannot tell, since streams pass their headers to `writeHead`.)
|
|
261
|
+
*/
|
|
262
|
+
function isStreaming(response) {
|
|
263
|
+
return response.headersSent && !response.writableEnded;
|
|
423
264
|
}
|
package/dist/host/http.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import { jsonResponse } from "../api/http/respond.js";
|
|
2
3
|
import { type ErrorCode, type ProtocolRange } from "@nylorun/core/compatibility";
|
|
3
4
|
export declare const OPAQUE_NOT_FOUND: {
|
|
4
5
|
status: "rejected";
|
|
@@ -13,9 +14,12 @@ export declare class HostListenError extends Error {
|
|
|
13
14
|
constructor(message: string, exitCode: number, cause?: unknown);
|
|
14
15
|
}
|
|
15
16
|
export declare function sendJson(response: ServerResponse, status: number, body: unknown): void;
|
|
17
|
+
/** `sendJson` as a Response, byte for byte (with any extra headers): the Host's answers. */
|
|
18
|
+
export { jsonResponse };
|
|
16
19
|
export declare function sendRejected(response: ServerResponse, status: number, code: ErrorCode, message: string, details?: unknown): void;
|
|
17
|
-
export declare function
|
|
18
|
-
export declare function
|
|
20
|
+
export declare function rejectedResponse(status: number, code: ErrorCode, message: string, details?: unknown): Response;
|
|
21
|
+
export declare function opaqueNotFoundResponse(): Response;
|
|
22
|
+
export declare function protocolRejectedResponse(protocol?: ProtocolRange): Response;
|
|
19
23
|
/**
|
|
20
24
|
* D§11 Host allowlist: loopback forms with the listening port, plus the
|
|
21
25
|
* configured host when `allowNonLoopback` is set.
|
|
@@ -32,8 +36,15 @@ export declare function isAllowedRequestHost(hostHeader: string | undefined, opt
|
|
|
32
36
|
export declare function isJsonContentType(value: string | undefined): boolean;
|
|
33
37
|
/** True when the request carries a body (Content-Length > 0 or chunked). */
|
|
34
38
|
export declare function requestHasBody(request: IncomingMessage): boolean;
|
|
39
|
+
/** `/health` and `/ready` are polled; the request log leaves them out. */
|
|
40
|
+
export declare function pathnameIsLogged(rawUrl: string | undefined): boolean;
|
|
35
41
|
/** Redact path parameters after `/v1/sessions/` for Host logs. */
|
|
36
42
|
export declare function redactRoutePath(pathname: string): string;
|
|
43
|
+
/** Constant-time comparison of SHA-256 digests of the presented and stored admin keys. */
|
|
44
|
+
export declare function adminKeyMatches(presented: string | undefined, adminKey: string): boolean;
|
|
45
|
+
/** A request header's first value. */
|
|
46
|
+
export declare function headerValue(incoming: IncomingMessage, name: string): string | undefined;
|
|
37
47
|
export declare function readBearer(authorization: string | undefined): string | undefined;
|
|
38
|
-
|
|
48
|
+
/** A JSON request body, capped at `limit` bytes (413); `undefined` when there is none. */
|
|
49
|
+
export declare function readJsonBody(request: Request, limit?: number): Promise<unknown>;
|
|
39
50
|
export declare function isLoopbackHost(host: string): boolean;
|