@volter/world-core 3.0.162 → 3.0.164
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/src/browser-session.d.ts +7 -1
- package/dist/src/browser-session.js +21 -10
- package/dist/src/derived-core.d.ts +36 -3
- package/dist/src/derived-core.js +121 -13
- package/dist/src/derived-real.js +15 -3
- package/dist/src/events.d.ts +6 -3
- package/dist/src/events.js +52 -7
- package/dist/src/file-response.d.ts +6 -2
- package/dist/src/file-response.js +29 -5
- package/dist/src/index.d.ts +2 -1
- package/dist/src/index.js +2 -1
- package/dist/src/managed-database.d.ts +4 -2
- package/dist/src/managed-database.js +1 -0
- package/dist/src/media-header.d.ts +34 -0
- package/dist/src/media-header.js +129 -0
- package/dist/src/pack-fetch.d.ts +24 -3
- package/dist/src/pack-fetch.js +114 -94
- package/dist/src/packRegistry.d.ts +12 -0
- package/dist/src/request-body.d.ts +2 -3
- package/dist/src/request-body.js +2 -3
- package/dist/src/runtime.d.ts +2 -1
- package/dist/src/runtime.js +2 -1
- package/dist/src/serve-http.d.ts +1 -1
- package/dist/src/serve-http.js +93 -17
- package/package.json +1 -1
- package/src/browser-session.ts +17 -5
- package/src/derived-core.ts +135 -17
- package/src/derived-real.ts +10 -2
- package/src/events.ts +54 -9
- package/src/file-response.ts +28 -6
- package/src/index.ts +2 -1
- package/src/managed-database.ts +4 -3
- package/src/media-header.ts +108 -0
- package/src/pack-fetch.ts +107 -85
- package/src/packRegistry.ts +7 -1
- package/src/request-body.ts +2 -3
- package/src/runtime.ts +2 -1
- package/src/serve-http.ts +70 -16
package/src/runtime.ts
CHANGED
|
@@ -16,11 +16,12 @@ export { ORIGINAL_PATH_HEADER } from './sigv4.ts';
|
|
|
16
16
|
export { createTwinFetchFromHandler, placedCookies, TWIN_PREFIX_HEADER, TWIN_REQUEST_SCOPES, TWIN_SITES_HEADER, TWIN_SITE_HEADER, siteLabel, siteUrlOf, twinPublicBase, twinSiteUrl, twinVendorUrl, withRequestScopes } from './twin-fetch.ts';
|
|
17
17
|
export { isReadOnlyRequest, READ_ONLY_REQUEST_HEADER, ReadOnlyRequestError, runAsReadOnlyRequest, runAsVendorMove, writesRefused } from './request-scope.ts';
|
|
18
18
|
export { compileSurface, createDerivedFetch, matchOperation, observeDerivedDispatch } from './derived.ts';
|
|
19
|
+
export { hostOf, laneSelector, slashesMerged, unitRequestRouter, worldDoorMatcher } from './pack-fetch.ts';
|
|
19
20
|
export { currentTraceparent, deliveryTraceHeaders, newTraceparent, parseTraceparent, runWithRequestTrace, runWithTraceparent, TRACEPARENT_HEADER, traceparentForDelivery, validTraceparent } from './trace-context.ts';
|
|
20
21
|
export type { Traceparent } from './trace-context.ts';
|
|
21
22
|
export type { DerivedCall, DerivedCoreOutcome, DerivedDispatch, DerivedFetch, DerivedFetchOptions, DerivedHandler, DerivedOperation, DerivedOwner, DerivedSurface, BoardFrame } from './derived.ts';
|
|
22
23
|
export { authRefusal, bindSemantics, coreFor, crossCutting, flagOf, derivedRequestScopes, observeTransitions, resourcesOfType as twinResourcesOfType, semanticsContext, stateOf, transitionFor, parseBracketForm, readParams, render as renderDerived, serveCore, sse, vendorError } from './derived-core.ts';
|
|
23
|
-
export { deliverEvents, fillEnvelope, setDeliveryObserver, signEvent, signEventAll, verifyEvent } from './events.ts';
|
|
24
|
+
export { deliverEvents, fillEnvelope, setDeliveryObserver, settleEventDeliveries, signEvent, signEventAll, verifyEvent } from './events.ts';
|
|
24
25
|
export { equalSecrets, hmac, jwks, jwtDecode, jwtSign, jwtSignClaims, jwtVerify, jwtVerifySignature, keyId, lettersFrom, md5, sha256, SIGNING_ALGORITHMS, uuidFrom } from './signing.ts';
|
|
25
26
|
export type { HandlerCrypto, Jwk, Jwks, JwtDecodeOptions, JwtSignatureOptions, JwtVerdict, JwtVerificationKey, SigningKey } from './signing.ts';
|
|
26
27
|
export type { EventDelivery, EventRender, EventScheme, EventsDecl, EventTransport, EventWrite } from './events.ts';
|
package/src/serve-http.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// The HTTP server seam of the kernel (docs/contributing/architecture.md#the-serve-seam): a fetch handler on
|
|
2
2
|
// a port, in the shape every `Bun.serve` call already has, so the serve path meets the runtime in
|
|
3
|
-
// ONE place.
|
|
4
|
-
// as a standard Request and the handler's Response written back. Async, because binding a port is
|
|
3
|
+
// ONE place. Ordinary HTTP uses `node:http`; requested WebSockets retain Bun's native backend.
|
|
4
|
+
// The request is rebuilt as a standard Request and the handler's Response written back. Async, because binding a port is
|
|
5
5
|
// asynchronous on Node — Bun's synchronous bind is the special case.
|
|
6
6
|
//
|
|
7
7
|
// Nothing here runs at module scope (the served-handler registry is made on first use), and `node:http` is reached through `process.getBuiltinModule`
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import type { TwinStream } from './twin-fetch.ts';
|
|
12
12
|
import { worldBootAnswer } from './world-boot.ts';
|
|
13
|
+
import { DEFAULT_MAX_DECODED_REQUEST_BODY_SIZE, RequestBodyTooLargeError } from './request-body.ts';
|
|
13
14
|
|
|
14
15
|
/** One socket library at the host seam, shared by inbound upgrades and authenticated outbound relays. */
|
|
15
16
|
const socketLibrary = () => import('ws');
|
|
@@ -41,7 +42,7 @@ export type ServeHttpOptions = {
|
|
|
41
42
|
fetch: HttpHandler;
|
|
42
43
|
/** seconds a connection may idle (Bun honors it; Node's keep-alive timeout is set from it) */
|
|
43
44
|
idleTimeout?: number;
|
|
44
|
-
/** Explicit transport limit in bytes; absent,
|
|
45
|
+
/** Explicit transport limit in bytes; absent, 128 MiB on both runtimes. */
|
|
45
46
|
maxRequestBodySize?: number;
|
|
46
47
|
/** what a thrown handler answers with; absent, a 500 carrying the error (as Bun.serve) */
|
|
47
48
|
error?: (error: Error) => Response | Promise<Response>;
|
|
@@ -101,7 +102,7 @@ export async function serveHttp(options: ServeHttpOptions): Promise<HttpServer>
|
|
|
101
102
|
const hostname = options.hostname ?? '127.0.0.1';
|
|
102
103
|
const urlHost = options.hostname ?? '127.0.0.1';
|
|
103
104
|
const bun = bunRuntime();
|
|
104
|
-
if (bun) {
|
|
105
|
+
if (bun && (options.upgrade !== undefined || 'websocket' in options)) {
|
|
105
106
|
// the global `Bun.serve` — the request journal's wrap of it (serve.ts) applies to packs and to
|
|
106
107
|
// this seam alike, so the seam never decorates twice on Bun
|
|
107
108
|
const upgrade = options.upgrade;
|
|
@@ -221,7 +222,8 @@ function builtin<T>(name: string): T {
|
|
|
221
222
|
|
|
222
223
|
async function serveOnNode(options: ServeHttpOptions & { hostname: string; urlHost: string }): Promise<HttpServer> {
|
|
223
224
|
const http = builtin<NodeHttp>('node:http');
|
|
224
|
-
const { Readable } = builtin<NodeStream>('node:stream');
|
|
225
|
+
const { Readable, Transform } = builtin<NodeStream>('node:stream');
|
|
226
|
+
const requestLimit = options.maxRequestBodySize ?? DEFAULT_MAX_DECODED_REQUEST_BODY_SIZE;
|
|
225
227
|
if ('websocket' in options) throw new Error('serveHttp: use the portable upgrade callbacks for WebSocket support on Node');
|
|
226
228
|
const protocol = options.tls ? 'https' : 'http';
|
|
227
229
|
const factory = (handler: import('node:http').RequestListener) => options.tls
|
|
@@ -245,23 +247,35 @@ async function serveOnNode(options: ServeHttpOptions & { hostname: string; urlHo
|
|
|
245
247
|
} catch { /* hostile thrown values are still reported by stage */ }
|
|
246
248
|
try { process.stderr.write(`serveHttp ${stage}: ${detail.slice(0, 512)}\n`); } catch { /* stderr is best-effort */ }
|
|
247
249
|
};
|
|
248
|
-
const active = new Set<
|
|
250
|
+
const active = new Set<() => void>();
|
|
249
251
|
// Node's Request signal is dependent on the source signal. Neither a pending
|
|
250
252
|
// handler nor a streaming response necessarily keeps the Request alive, so
|
|
251
253
|
// retain it until the whole HTTP response path (including body cleanup) ends.
|
|
252
254
|
const activeRequests = new Set<Request>();
|
|
253
255
|
const server = factory((req: import('node:http').IncomingMessage, res: import('node:http').ServerResponse) => {
|
|
254
256
|
const controller = new AbortController();
|
|
255
|
-
|
|
256
|
-
const
|
|
257
|
+
const socket = req.socket;
|
|
258
|
+
const hasInputBody = req.headers['transfer-encoding'] !== undefined || Number(req.headers['content-length']) > 0;
|
|
259
|
+
let incoming: import('node:stream').Transform | undefined;
|
|
260
|
+
let flushingBodyRefusal = false;
|
|
261
|
+
const abort = () => {
|
|
262
|
+
if (!controller.signal.aborted) controller.abort();
|
|
263
|
+
incoming?.destroy(controller.signal.reason);
|
|
264
|
+
if (!flushingBodyRefusal) {
|
|
265
|
+
req.destroy();
|
|
266
|
+
if (!res.writableFinished) res.destroy();
|
|
267
|
+
socket.destroy();
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
active.add(abort);
|
|
257
271
|
const onRequestClose = () => { if (!req.complete) abort(); };
|
|
258
|
-
const onResponseClose = () => { if (!res.writableFinished) abort(); };
|
|
272
|
+
const onResponseClose = () => { if (!res.writableFinished) { flushingBodyRefusal = false; abort(); } };
|
|
259
273
|
const onSocketClose = () => { if (!res.writableFinished) abort(); };
|
|
260
274
|
req.on('aborted', abort);
|
|
261
275
|
req.on('close', onRequestClose);
|
|
262
276
|
req.on('error', abort);
|
|
263
|
-
|
|
264
|
-
|
|
277
|
+
socket.on('close', onSocketClose);
|
|
278
|
+
socket.on('error', abort);
|
|
265
279
|
res.on('close', onResponseClose);
|
|
266
280
|
res.on('error', abort);
|
|
267
281
|
const disconnected = () => controller.signal.aborted || res.destroyed;
|
|
@@ -288,17 +302,50 @@ async function serveOnNode(options: ServeHttpOptions & { hostname: string; urlHo
|
|
|
288
302
|
if (disconnected()) closed();
|
|
289
303
|
});
|
|
290
304
|
let request: Request | undefined;
|
|
305
|
+
let bodyTooLarge = false;
|
|
306
|
+
const rejectBody = () => {
|
|
307
|
+
if (bodyTooLarge) return;
|
|
308
|
+
bodyTooLarge = true;
|
|
309
|
+
const error = new RequestBodyTooLargeError();
|
|
310
|
+
if (incoming) req.unpipe(incoming);
|
|
311
|
+
req.pause();
|
|
312
|
+
if (!res.headersSent && !res.destroyed) {
|
|
313
|
+
for (const name of res.getHeaderNames()) res.removeHeader(name);
|
|
314
|
+
flushingBodyRefusal = true;
|
|
315
|
+
res.writeHead(413, { 'connection': 'close', 'content-type': 'text/plain', 'content-length': Buffer.byteLength(error.message) });
|
|
316
|
+
res.once('finish', () => { flushingBodyRefusal = false; req.destroy(); });
|
|
317
|
+
res.end(error.message);
|
|
318
|
+
} else if (!res.destroyed) res.destroy(error);
|
|
319
|
+
abort();
|
|
320
|
+
};
|
|
321
|
+
const inputSettled = new Promise<void>((resolve) => {
|
|
322
|
+
const settled = () => { req.off('end', settled); req.off('close', settled); resolve(); };
|
|
323
|
+
req.once('end', settled);
|
|
324
|
+
req.once('close', settled);
|
|
325
|
+
});
|
|
291
326
|
const handle = async () => {
|
|
292
327
|
const method = req.method ?? 'GET';
|
|
328
|
+
if (Number(req.headers['content-length']) > requestLimit) { rejectBody(); return; }
|
|
293
329
|
let response: Response;
|
|
294
330
|
try {
|
|
295
331
|
const headers = new Headers();
|
|
296
332
|
for (const [k, v] of Object.entries(req.headers)) { if (v === undefined) continue; if (Array.isArray(v)) for (const one of v) headers.append(k, one); else headers.set(k, v); }
|
|
297
333
|
const url = `${protocol}://${req.headers.host ?? `${options.hostname}:${(server.address() as { port: number } | null)?.port ?? 0}`}${req.url ?? '/'}`;
|
|
298
334
|
const withBody = method !== 'GET' && method !== 'HEAD';
|
|
299
|
-
|
|
335
|
+
let received = 0;
|
|
336
|
+
incoming = new Transform({ transform(chunk: Buffer, _encoding, callback) {
|
|
337
|
+
received += chunk.byteLength;
|
|
338
|
+
if (received > requestLimit) { rejectBody(); callback(new RequestBodyTooLargeError()); }
|
|
339
|
+
else callback(null, chunk);
|
|
340
|
+
} });
|
|
341
|
+
const inputClosed = () => { if (!req.readableEnded) abort(); };
|
|
342
|
+
incoming.on('error', inputClosed);
|
|
343
|
+
incoming.once('close', inputClosed);
|
|
344
|
+
const init: RequestInit & { duplex?: 'half' } = { method, headers, signal: controller.signal, ...(withBody ? { body: Readable.toWeb(incoming) as unknown as ReadableStream, duplex: 'half' } : {}) };
|
|
300
345
|
request = new Request(url, init);
|
|
301
346
|
activeRequests.add(request);
|
|
347
|
+
req.pipe(incoming);
|
|
348
|
+
if (!withBody) incoming.resume();
|
|
302
349
|
response = await options.fetch(request);
|
|
303
350
|
} catch (error) {
|
|
304
351
|
if (disconnected()) return;
|
|
@@ -340,6 +387,7 @@ async function serveOnNode(options: ServeHttpOptions & { hostname: string; urlHo
|
|
|
340
387
|
if (disconnected()) { await cancelOwnedReader(); return; }
|
|
341
388
|
res.statusCode = response.status;
|
|
342
389
|
for (const [k, v] of response.headers) { if (k !== 'set-cookie') res.setHeader(k, v); }
|
|
390
|
+
res.strictContentLength = response.headers.has('content-length');
|
|
343
391
|
const cookies = response.headers.getSetCookie?.() ?? [];
|
|
344
392
|
if (cookies.length) res.setHeader('set-cookie', cookies);
|
|
345
393
|
if (reader) {
|
|
@@ -382,12 +430,18 @@ async function serveOnNode(options: ServeHttpOptions & { hostname: string; urlHo
|
|
|
382
430
|
if (!res.destroyed) res.destroy(error instanceof Error ? error : new Error(String(error)));
|
|
383
431
|
}).then(async () => {
|
|
384
432
|
await responseSettled;
|
|
385
|
-
|
|
433
|
+
// A runtime may retire its native body forwarding when the response ends.
|
|
434
|
+
// Close unfinished input after the response flushes; a consumed upload keeps its connection.
|
|
435
|
+
if (hasInputBody && !incoming?.readableEnded) abort();
|
|
436
|
+
else incoming?.resume();
|
|
437
|
+
await inputSettled;
|
|
438
|
+
if (incoming) { req.unpipe(incoming); incoming.destroy(); }
|
|
439
|
+
active.delete(abort);
|
|
386
440
|
req.off('aborted', abort);
|
|
387
441
|
req.off('close', onRequestClose);
|
|
388
442
|
req.off('error', abort);
|
|
389
|
-
|
|
390
|
-
|
|
443
|
+
socket.off('close', onSocketClose);
|
|
444
|
+
socket.off('error', abort);
|
|
391
445
|
res.off('close', onResponseClose);
|
|
392
446
|
res.off('error', abort);
|
|
393
447
|
});
|
|
@@ -415,7 +469,7 @@ async function serveOnNode(options: ServeHttpOptions & { hostname: string; urlHo
|
|
|
415
469
|
const port = (server.address() as { port: number }).port;
|
|
416
470
|
return {
|
|
417
471
|
hostname: options.hostname, port, url: new URL(`${protocol}://${options.urlHost}:${port}/`),
|
|
418
|
-
stop: (closeActive = true) => new Promise<void>((resolve) => { if (closeActive) { for (const
|
|
472
|
+
stop: (closeActive = true) => new Promise<void>((resolve) => { if (closeActive) { for (const abort of active) abort(); for (const peer of socketServer?.clients ?? []) peer.terminate(); } socketServer?.close(); server.close(() => { if (closeActive && active.size) reportCleanup('forced stop left pending handler cleanup', `${active.size} request(s)`); resolve(); }); if (closeActive) server.closeAllConnections(); }),
|
|
419
473
|
};
|
|
420
474
|
}
|
|
421
475
|
|