@volter/world-core 3.0.160 → 3.0.162
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/generated/pack-facts.json +190 -6
- package/dist/inject.cjs +2 -2
- package/dist/src/derived-core.d.ts +9 -1
- package/dist/src/derived-core.js +120 -57
- package/dist/src/index.d.ts +4 -2
- package/dist/src/index.js +3 -1
- package/dist/src/live/rtmp.js +3 -0
- package/dist/src/machines.d.ts +4 -16
- package/dist/src/machines.js +10 -44
- package/dist/src/pack-fetch.js +29 -5
- package/dist/src/packRegistry.d.ts +7 -0
- package/dist/src/protobuf.d.ts +4 -0
- package/dist/src/protobuf.js +43 -2
- package/dist/src/request-scope.d.ts +2 -0
- package/dist/src/request-scope.js +9 -0
- package/dist/src/runtime.d.ts +6 -3
- package/dist/src/runtime.js +4 -1
- package/dist/src/serve-http.d.ts +11 -0
- package/dist/src/serve-http.js +132 -0
- package/dist/src/signing.d.ts +30 -7
- package/dist/src/signing.js +32 -16
- package/dist/src/sockets.d.ts +1 -1
- package/dist/src/sockets.js +11 -7
- package/dist/src/world-account-name.d.ts +2 -0
- package/dist/src/world-account-name.js +4 -0
- package/generated/pack-facts.json +190 -6
- package/inject.cjs +2 -2
- package/package.json +9 -1
- package/src/derived-core.ts +125 -63
- package/src/index.ts +5 -2
- package/src/live/rtmp.ts +3 -0
- package/src/machines.ts +10 -38
- package/src/pack-fetch.ts +27 -5
- package/src/packRegistry.ts +2 -2
- package/src/protobuf.ts +39 -2
- package/src/request-scope.ts +9 -0
- package/src/runtime.ts +8 -3
- package/src/serve-http.ts +111 -0
- package/src/signing.ts +38 -18
- package/src/sockets.ts +12 -9
- package/src/world-account-name.ts +4 -0
package/dist/src/pack-fetch.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { runWithStorageTenant } from "./request-scope.js";
|
|
1
2
|
import { parseExactJson } from "./exact-json.js";
|
|
2
3
|
import { redisServiceTimes } from "./redis/service-times.js";
|
|
3
4
|
import { readHostPort } from "./host-port.js";
|
|
@@ -472,7 +473,7 @@ export function createPackFetch(parts, options = {}) {
|
|
|
472
473
|
redisServiceTimes.lap('2 content and lane routes, decoding, routing');
|
|
473
474
|
if (request.method === 'GET' && path === '/twin' && m.discovery) {
|
|
474
475
|
const d = m.discovery;
|
|
475
|
-
const discoveryDoors = [...(m.doors ?? []), ...(m.machines ? [{ id: 'machine-pool', method: 'POST', path: '/_twin/machine-pool', note: 'Enroll the World execution pool: { kind:
|
|
476
|
+
const discoveryDoors = [...(m.doors ?? []), ...(m.machines ? [{ id: 'machine-pool', method: 'POST', path: '/_twin/machine-pool', note: 'Enroll the World execution pool: { kind: docker | none | fake }.' }] : [])];
|
|
476
477
|
// the workspaces a person can be shown (Viewing a World): each built workspace screen's id and path
|
|
477
478
|
const laneDiscovery = await laneDoor(request, '/twin');
|
|
478
479
|
const resources = {};
|
|
@@ -525,7 +526,7 @@ export function createPackFetch(parts, options = {}) {
|
|
|
525
526
|
const kind = enrollment.kind;
|
|
526
527
|
if (!POOL_KINDS.includes(kind))
|
|
527
528
|
return Response.json({ message: `kind must be one of ${POOL_KINDS.join(', ')}` }, { status: 400 });
|
|
528
|
-
await applyTwinWrite(m.service, { operation: 'machine_pool.enroll', subjectType: MACHINE_POOL, subjectId: 'pool', fields: { kind, ...(kind === 'fake' && Array.isArray(enrollment.httpReplies) ? { httpReplies: enrollment.httpReplies } : {})
|
|
529
|
+
await applyTwinWrite(m.service, { operation: 'machine_pool.enroll', subjectType: MACHINE_POOL, subjectId: 'pool', fields: { kind, ...(kind === 'fake' && Array.isArray(enrollment.httpReplies) ? { httpReplies: enrollment.httpReplies } : {}) }, occurredAt: (await open(request, 'machine-pool', 'POST', '/_twin/machine-pool', {})).occurredAt, actor: { kind: 'system' } }, options.root);
|
|
529
530
|
if (kind === 'fake')
|
|
530
531
|
resetFakePool(m.service, options.root);
|
|
531
532
|
return Response.json({ kind }, { status: 201 });
|
|
@@ -657,7 +658,18 @@ export function createPackFetch(parts, options = {}) {
|
|
|
657
658
|
};
|
|
658
659
|
const cors = m.cors ? withCors(m.cors, labelled) : labelled;
|
|
659
660
|
// at a World's place every cookie the twin sets is kept to the twin's base path (twin-fetch.ts, placedCookies)
|
|
660
|
-
const
|
|
661
|
+
const selectedAccount = async (request) => {
|
|
662
|
+
if (m.tenant?.storage !== 'namespaced' || !parts.tenant)
|
|
663
|
+
return undefined;
|
|
664
|
+
const authority = routed(request.clone()), path = new URL(authority.url).pathname;
|
|
665
|
+
return runWithStorageTenant(m.service, undefined, async () => (await parts.tenant(await open(authority, 'account-authority', request.method, path, {}))) ?? '__unselected__');
|
|
666
|
+
};
|
|
667
|
+
const served = async (request) => {
|
|
668
|
+
const path = new URL(routed(request.clone()).url).pathname;
|
|
669
|
+
if (m.tenant?.storage !== 'namespaced' || !parts.tenant || path === '/twin' || path.startsWith('/twin/'))
|
|
670
|
+
return placedCookies(request, await cors(request));
|
|
671
|
+
return runWithStorageTenant(m.service, await selectedAccount(request), async () => placedCookies(request, await cors(request)));
|
|
672
|
+
};
|
|
661
673
|
// which paths this pack's API serves, for a vendor's lane router that asks (unlessOnlyRoot)
|
|
662
674
|
const serves = (method, path) => Boolean(matchOperation(routes, method, path));
|
|
663
675
|
// the vendor's sockets: a request to a declared socket's path opens a session of its engine, over a context of the
|
|
@@ -668,7 +680,19 @@ export function createPackFetch(parts, options = {}) {
|
|
|
668
680
|
const ownUpgrade = m.sockets?.length ? socketUpgrade(m.service, options.root, m.sockets, parts.sockets, (request, d, params) => open(request, `socket:${d.id}`, 'GET', d.path, params), hostOf, (request) => new URL(routed(request).url).pathname,
|
|
669
681
|
// a turn a session speaks (a voice agent's reply) is the World's scenario's, as a turn over HTTP is
|
|
670
682
|
async (asked) => engineOf()?.serve(asked)) : undefined;
|
|
671
|
-
const
|
|
683
|
+
const rawUpgrade = laneUpgrades(parts.lanes ?? {}, ownUpgrade);
|
|
684
|
+
const opening = new WeakMap();
|
|
685
|
+
const upgrade = rawUpgrade && m.tenant?.storage === 'namespaced' && parts.tenant ? {
|
|
686
|
+
accepts: request => rawUpgrade.accepts(request),
|
|
687
|
+
protocol: request => rawUpgrade.protocol?.(request),
|
|
688
|
+
open(peer, request) {
|
|
689
|
+
const ready = selectedAccount(request).then(account => runWithStorageTenant(m.service, account, () => rawUpgrade.open(peer, request)));
|
|
690
|
+
opening.set(peer, ready);
|
|
691
|
+
void ready.catch(error => peer.close(1011, String(error instanceof Error ? error.message : error).slice(0, 120)));
|
|
692
|
+
},
|
|
693
|
+
async message(peer, data) { await opening.get(peer); return rawUpgrade.message(peer, data); },
|
|
694
|
+
close(peer) { const ready = opening.get(peer); opening.delete(peer); void ready?.then(() => rawUpgrade.close(peer), () => undefined); },
|
|
695
|
+
} : rawUpgrade;
|
|
672
696
|
// the broadcasts the vendor takes over RTMP: a publisher's connection, its publish ruled by the pack's dialect over a
|
|
673
697
|
// context of its own (operation `live:<id>`), its recording kept as the World's bookkeeping and blobs (live/ingest.ts)
|
|
674
698
|
const ownIngest = m.live?.length ? liveIngest({
|
|
@@ -694,7 +718,7 @@ export function createPackFetch(parts, options = {}) {
|
|
|
694
718
|
...lanesOwners(parts.lanes ?? {}),
|
|
695
719
|
});
|
|
696
720
|
const owners = () => flatOwners(ownersByUnit());
|
|
697
|
-
return derivedRequestScopes(m, Object.assign(served, { owners, ownersByUnit, serves, advanceClock, workspaces, boards: Boolean(parts.board), maxRequestBodySize: m.descriptor?.maxRequestBodySize, draws: drawsUrl, contentHosts, ...(upgrade ? { upgrade } : {}), ...(ingest ? { ingest } : {}) }), options.readOnly);
|
|
721
|
+
return derivedRequestScopes(m, Object.assign(served, { owners, ownersByUnit, serves, advanceClock: async (request) => runWithStorageTenant(m.service, await selectedAccount(request), () => advanceClock(request)), workspaces, boards: Boolean(parts.board), maxRequestBodySize: m.descriptor?.maxRequestBodySize, draws: drawsUrl, contentHosts, ...(upgrade ? { upgrade } : {}), ...(ingest ? { ingest } : {}) }), options.readOnly);
|
|
698
722
|
}
|
|
699
723
|
/** The vendor's gateway over its lanes: the lane a request goes to (a door to the lane that declares it, then the first
|
|
700
724
|
* route whose conditions all hold, then the default), behind the gateway's CORS; undefined when none takes it. */
|
|
@@ -202,6 +202,9 @@ export type TwinPack = {
|
|
|
202
202
|
path: string;
|
|
203
203
|
body: Record<string, unknown>;
|
|
204
204
|
fill: Record<string, string>;
|
|
205
|
+
account?: {
|
|
206
|
+
field: string;
|
|
207
|
+
};
|
|
205
208
|
};
|
|
206
209
|
/** Another pack of this vendor issues the API's credentials; architecture, descriptor World wiring. */
|
|
207
210
|
credentialIssuer?: string;
|
|
@@ -226,6 +229,10 @@ export type TwinPack = {
|
|
|
226
229
|
managedDatabase?: {
|
|
227
230
|
kind: 'postgres';
|
|
228
231
|
arg: string;
|
|
232
|
+
user: string;
|
|
233
|
+
database: string;
|
|
234
|
+
clock: 'wall';
|
|
235
|
+
credentialField: string;
|
|
229
236
|
note: string;
|
|
230
237
|
};
|
|
231
238
|
sidePorts?: Array<{
|
package/dist/src/protobuf.d.ts
CHANGED
|
@@ -26,3 +26,7 @@ export type ProtoSchema = {
|
|
|
26
26
|
export declare function encodeMessage(schema: ProtoSchema, type: string, value: unknown): Uint8Array;
|
|
27
27
|
/** A message's binary encoding as its JSON form (proto3 JSON; a field at its default is left out). */
|
|
28
28
|
export declare function decodeMessage(schema: ProtoSchema, type: string, bytes: Uint8Array): unknown;
|
|
29
|
+
/** Rewrite selected length-delimited fields without decoding/re-encoding the rest of a message.
|
|
30
|
+
* Undefined keeps the original field bytes; null removes that occurrence. Nested rewrites use this same visitor.
|
|
31
|
+
* This is for transparent vendor fronts: unknown fields, ordering and untouched encodings survive unchanged. */
|
|
32
|
+
export declare function rewriteProtobufFields(bytes: Uint8Array, fields: Readonly<Record<number, (value: Uint8Array) => Uint8Array | null | undefined>>): Uint8Array;
|
package/dist/src/protobuf.js
CHANGED
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
const VARINT = new Set(['int32', 'int64', 'uint32', 'uint64', 'sint32', 'sint64', 'bool']);
|
|
9
9
|
const FIXED64 = new Set(['fixed64', 'sfixed64', 'double']);
|
|
10
10
|
const FIXED32 = new Set(['fixed32', 'sfixed32', 'float']);
|
|
11
|
-
const LONG = new Set(['int64', 'uint64', 'sint64', 'fixed64', 'sfixed64']);
|
|
12
11
|
const WRAPPERS = {
|
|
13
12
|
'google.protobuf.DoubleValue': 'double', 'google.protobuf.FloatValue': 'float', 'google.protobuf.Int64Value': 'int64',
|
|
14
13
|
'google.protobuf.UInt64Value': 'uint64', 'google.protobuf.Int32Value': 'int32', 'google.protobuf.UInt32Value': 'uint32',
|
|
@@ -208,7 +207,7 @@ class Reader {
|
|
|
208
207
|
return out;
|
|
209
208
|
}
|
|
210
209
|
bytes() { return this.take(Number(this.varint())); }
|
|
211
|
-
skip(wire) {
|
|
210
|
+
skip(wire, number) {
|
|
212
211
|
if (wire === 0)
|
|
213
212
|
this.varint();
|
|
214
213
|
else if (wire === 1)
|
|
@@ -217,6 +216,21 @@ class Reader {
|
|
|
217
216
|
this.take(4);
|
|
218
217
|
else if (wire === 2)
|
|
219
218
|
this.bytes();
|
|
219
|
+
else if (wire === 3) {
|
|
220
|
+
while (!this.done) {
|
|
221
|
+
const tag = this.varint();
|
|
222
|
+
if (tag > 0xffffffffn || (tag >> 3n) === 0n)
|
|
223
|
+
throw new Error('protobuf: invalid group field tag');
|
|
224
|
+
const field = Number(tag >> 3n), kind = Number(tag & 7n);
|
|
225
|
+
if (kind === 4) {
|
|
226
|
+
if (number !== undefined && field !== number)
|
|
227
|
+
throw new Error('protobuf: mismatched group end');
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
this.skip(kind, field);
|
|
231
|
+
}
|
|
232
|
+
throw new Error('protobuf: unterminated group');
|
|
233
|
+
}
|
|
220
234
|
else
|
|
221
235
|
throw new Error(`protobuf: wire type ${wire} is not supported`);
|
|
222
236
|
}
|
|
@@ -336,3 +350,30 @@ export function decodeMessage(schema, type, bytes) {
|
|
|
336
350
|
}
|
|
337
351
|
return wellKnownOut(type, out);
|
|
338
352
|
}
|
|
353
|
+
/** Rewrite selected length-delimited fields without decoding/re-encoding the rest of a message.
|
|
354
|
+
* Undefined keeps the original field bytes; null removes that occurrence. Nested rewrites use this same visitor.
|
|
355
|
+
* This is for transparent vendor fronts: unknown fields, ordering and untouched encodings survive unchanged. */
|
|
356
|
+
export function rewriteProtobufFields(bytes, fields) {
|
|
357
|
+
const reader = new Reader(bytes), writer = new Writer();
|
|
358
|
+
while (!reader.done) {
|
|
359
|
+
const start = reader.at, tag = reader.varint();
|
|
360
|
+
if (tag > 0xffffffffn || (tag >> 3n) === 0n)
|
|
361
|
+
throw new Error('protobuf: invalid field tag');
|
|
362
|
+
const number = Number(tag >> 3n), wire = Number(tag & 7n);
|
|
363
|
+
const rewrite = Object.hasOwn(fields, number) ? fields[number] : undefined;
|
|
364
|
+
if (wire === 2 && rewrite) {
|
|
365
|
+
const changed = rewrite(reader.bytes());
|
|
366
|
+
if (changed === undefined)
|
|
367
|
+
writer.raw(bytes.subarray(start, reader.at));
|
|
368
|
+
else if (changed !== null) {
|
|
369
|
+
writer.tag(number, wire);
|
|
370
|
+
writer.bytes(changed);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
reader.skip(wire, number);
|
|
375
|
+
writer.raw(bytes.subarray(start, reader.at));
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return writer.done();
|
|
379
|
+
}
|
|
@@ -28,3 +28,5 @@ export declare function writesRefused(): boolean;
|
|
|
28
28
|
/** The write seam's check: throws (and records) the refusal when a write attempted here is a
|
|
29
29
|
* read-only request's. Called by every appender before it writes anything. */
|
|
30
30
|
export declare function refuseReadOnlyWrite(service: string): void;
|
|
31
|
+
export declare function storageTenant(service: string): string | undefined;
|
|
32
|
+
export declare function runWithStorageTenant<T>(service: string, tenant: string | undefined, fn: () => T): T;
|
|
@@ -77,3 +77,12 @@ export function refuseReadOnlyWrite(service) {
|
|
|
77
77
|
writeScopeStore.getStore().refusals.push(error);
|
|
78
78
|
throw error;
|
|
79
79
|
}
|
|
80
|
+
/** Request-local service tenancy. Created lazily so imports remain inert. */
|
|
81
|
+
let tenantStore;
|
|
82
|
+
export function storageTenant(service) { return tenantStore?.getStore()?.get(service); }
|
|
83
|
+
export function runWithStorageTenant(service, tenant, fn) {
|
|
84
|
+
tenantStore ??= new AsyncLocalStorage();
|
|
85
|
+
const scope = new Map(tenantStore.getStore());
|
|
86
|
+
scope.set(service, tenant);
|
|
87
|
+
return tenantStore.run(scope, fn);
|
|
88
|
+
}
|
package/dist/src/runtime.d.ts
CHANGED
|
@@ -12,8 +12,8 @@ export type { Traceparent } from './trace-context.js';
|
|
|
12
12
|
export type { DerivedCall, DerivedCoreOutcome, DerivedDispatch, DerivedFetch, DerivedFetchOptions, DerivedHandler, DerivedOperation, DerivedOwner, DerivedSurface, BoardFrame } from './derived.js';
|
|
13
13
|
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.js';
|
|
14
14
|
export { deliverEvents, fillEnvelope, setDeliveryObserver, signEvent, signEventAll, verifyEvent } from './events.js';
|
|
15
|
-
export { equalSecrets, hmac, jwks, jwtDecode, jwtSign, jwtVerify, keyId, lettersFrom, md5, sha256, SIGNING_ALGORITHMS, uuidFrom } from './signing.js';
|
|
16
|
-
export type { HandlerCrypto, Jwk, Jwks, JwtVerdict, SigningKey } from './signing.js';
|
|
15
|
+
export { equalSecrets, hmac, jwks, jwtDecode, jwtSign, jwtSignClaims, jwtVerify, jwtVerifySignature, keyId, lettersFrom, md5, sha256, SIGNING_ALGORITHMS, uuidFrom } from './signing.js';
|
|
16
|
+
export type { HandlerCrypto, Jwk, Jwks, JwtDecodeOptions, JwtSignatureOptions, JwtVerdict, JwtVerificationKey, SigningKey } from './signing.js';
|
|
17
17
|
export type { EventDelivery, EventRender, EventScheme, EventsDecl, EventTransport, EventWrite } from './events.js';
|
|
18
18
|
export type { Actor, CoreScope, DerivedManifest, ErrorSpec, FieldRule, Handler, HandlerContext, ResourceDecl, WriteHookContext, ScreenDecl, Semantics, SemanticsContext, StateField, Transition, TransitionObserver, SeedCall } from './derived-core.js';
|
|
19
19
|
export type { RemoteExecute, RemoteExecuteRequest, RemoteExecuteResponse } from './remote-execute.js';
|
|
@@ -86,6 +86,9 @@ export { captureHistory, captureParentHistory, historyChanges, historyAtInstant,
|
|
|
86
86
|
export type { HistoryView, HistoryLayout, HistoryReference, HistoryOrigin } from './history.js';
|
|
87
87
|
export { foldHistory } from './log.js';
|
|
88
88
|
export { volterHome } from './volter-home.js';
|
|
89
|
-
export type { MachinePool, MachineSpec, MachineProcess, MachineStatus, MachineFiles, MachineFileStat
|
|
89
|
+
export type { MachinePool, MachineSpec, MachineProcess, MachineStatus, MachineFiles, MachineFileStat } from './machines.js';
|
|
90
90
|
export { boardFrameKey } from './board-keys.js';
|
|
91
91
|
export * as managedOriginalInputs from './managed-original-inputs.js';
|
|
92
|
+
export { worldSecret, worldAccount } from './derived-core.js';
|
|
93
|
+
export { rewriteProtobufFields } from './protobuf.js';
|
|
94
|
+
export { attachWebSocketRelay } from './serve-http.js';
|
package/dist/src/runtime.js
CHANGED
|
@@ -18,7 +18,7 @@ export { compileSurface, createDerivedFetch, matchOperation, observeDerivedDispa
|
|
|
18
18
|
export { currentTraceparent, deliveryTraceHeaders, newTraceparent, parseTraceparent, runWithRequestTrace, runWithTraceparent, TRACEPARENT_HEADER, traceparentForDelivery, validTraceparent } from "./trace-context.js";
|
|
19
19
|
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.js";
|
|
20
20
|
export { deliverEvents, fillEnvelope, setDeliveryObserver, signEvent, signEventAll, verifyEvent } from "./events.js";
|
|
21
|
-
export { equalSecrets, hmac, jwks, jwtDecode, jwtSign, jwtVerify, keyId, lettersFrom, md5, sha256, SIGNING_ALGORITHMS, uuidFrom } from "./signing.js";
|
|
21
|
+
export { equalSecrets, hmac, jwks, jwtDecode, jwtSign, jwtSignClaims, jwtVerify, jwtVerifySignature, keyId, lettersFrom, md5, sha256, SIGNING_ALGORITHMS, uuidFrom } from "./signing.js";
|
|
22
22
|
export { emitTwinEvent, eventSubscriptionMatches, listEmittable, runEmitCli } from "./emit.js";
|
|
23
23
|
// The vendor-agnostic CLIENT-SIDE RATE BUDGET — the fail-closed backstop a pack's guarded client
|
|
24
24
|
// routes every live vendor call through. The MECHANISM is here; the per-vendor ceiling/window/
|
|
@@ -109,3 +109,6 @@ export { foldHistory } from "./log.js";
|
|
|
109
109
|
export { volterHome } from "./volter-home.js";
|
|
110
110
|
export { boardFrameKey } from "./board-keys.js";
|
|
111
111
|
export * as managedOriginalInputs from "./managed-original-inputs.js";
|
|
112
|
+
export { worldSecret, worldAccount } from "./derived-core.js";
|
|
113
|
+
export { rewriteProtobufFields } from "./protobuf.js";
|
|
114
|
+
export { attachWebSocketRelay } from "./serve-http.js";
|
package/dist/src/serve-http.d.ts
CHANGED
|
@@ -69,3 +69,14 @@ export declare function serveStream(options: {
|
|
|
69
69
|
port: number;
|
|
70
70
|
stop: () => void;
|
|
71
71
|
}>;
|
|
72
|
+
export type WebSocketRelay = {
|
|
73
|
+
url: string;
|
|
74
|
+
headers?: Record<string, string | string[] | undefined>;
|
|
75
|
+
outgoing?: (frame: string | Uint8Array) => string | Uint8Array | Promise<string | Uint8Array>;
|
|
76
|
+
};
|
|
77
|
+
/** Attach an authenticated native WebSocket relay to an existing Node front.
|
|
78
|
+
* The shared socket library owns handshake, fragmentation, masking, compression and control frames.
|
|
79
|
+
* Payload transforms are ordered; unknown vendor messages remain opaque bytes. */
|
|
80
|
+
export declare function attachWebSocketRelay(server: import('node:http').Server, route: (request: import('node:http').IncomingMessage) => WebSocketRelay | undefined): Promise<{
|
|
81
|
+
stop: () => void;
|
|
82
|
+
}>;
|
package/dist/src/serve-http.js
CHANGED
|
@@ -471,3 +471,135 @@ async function serveOnNode(options) {
|
|
|
471
471
|
server.closeAllConnections(); }),
|
|
472
472
|
};
|
|
473
473
|
}
|
|
474
|
+
/** Attach an authenticated native WebSocket relay to an existing Node front.
|
|
475
|
+
* The shared socket library owns handshake, fragmentation, masking, compression and control frames.
|
|
476
|
+
* Payload transforms are ordered; unknown vendor messages remain opaque bytes. */
|
|
477
|
+
export async function attachWebSocketRelay(server, route) {
|
|
478
|
+
const { WebSocket: Client, WebSocketServer } = await socketLibrary();
|
|
479
|
+
const routes = new WeakMap();
|
|
480
|
+
const pairs = new Set();
|
|
481
|
+
const accepted = new WebSocketServer({ noServer: true, handleProtocols: (_offered, request) => routes.get(request)?.protocol || false });
|
|
482
|
+
let stopped = false;
|
|
483
|
+
const reject = (socket, status, message) => {
|
|
484
|
+
const body = new TextEncoder().encode(message);
|
|
485
|
+
socket.end('HTTP/1.1 ' + status + ' Rejected\r\nConnection: close\r\nContent-Type: text/plain\r\nContent-Length: ' + body.length + '\r\n\r\n' + message);
|
|
486
|
+
};
|
|
487
|
+
const upgrade = (request, socket, head) => {
|
|
488
|
+
let selected;
|
|
489
|
+
try {
|
|
490
|
+
if (!stopped)
|
|
491
|
+
selected = route(request);
|
|
492
|
+
}
|
|
493
|
+
catch {
|
|
494
|
+
reject(socket, 401, 'invalid authorization token');
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
if (!selected) {
|
|
498
|
+
reject(socket, 404, 'WebSocket route is not served');
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
const headers = { ...selected.headers };
|
|
502
|
+
// Each hop has its own RFC6455 handshake; the library regenerates these headers.
|
|
503
|
+
for (const name of Object.keys(headers))
|
|
504
|
+
if (/^(host|connection|upgrade|sec-websocket-(key|version|extensions|protocol))$/i.test(name))
|
|
505
|
+
delete headers[name];
|
|
506
|
+
const protocols = String(request.headers['sec-websocket-protocol'] ?? '').split(',').map(value => value.trim()).filter(Boolean);
|
|
507
|
+
const upstream = new Client(selected.url, protocols, { headers });
|
|
508
|
+
const pair = { upstream };
|
|
509
|
+
pairs.add(pair);
|
|
510
|
+
const pending = [];
|
|
511
|
+
let queue = Promise.resolve(), closed = false;
|
|
512
|
+
const dispose = () => {
|
|
513
|
+
if (closed)
|
|
514
|
+
return;
|
|
515
|
+
closed = true;
|
|
516
|
+
pairs.delete(pair);
|
|
517
|
+
upstream.terminate();
|
|
518
|
+
pair.downstream?.terminate();
|
|
519
|
+
socket.destroy();
|
|
520
|
+
};
|
|
521
|
+
const closeCode = (code) => code === 1005 ? 1000 : code === 1006 ? 1011 : code;
|
|
522
|
+
const frameOf = (data, binary) => {
|
|
523
|
+
const raw = Array.isArray(data) ? Buffer.concat(data) : data instanceof ArrayBuffer ? new Uint8Array(data) : data;
|
|
524
|
+
return binary ? new Uint8Array(raw) : Buffer.from(raw).toString('utf8');
|
|
525
|
+
};
|
|
526
|
+
const send = (frame) => {
|
|
527
|
+
queue = queue.then(async () => {
|
|
528
|
+
const output = await (selected.outgoing?.(frame) ?? frame);
|
|
529
|
+
if (!closed && pair.downstream?.readyState === Client.OPEN)
|
|
530
|
+
pair.downstream.send(output, { binary: typeof output !== 'string' });
|
|
531
|
+
}).catch(dispose);
|
|
532
|
+
};
|
|
533
|
+
upstream.on('message', (data, binary) => {
|
|
534
|
+
const frame = frameOf(data, binary);
|
|
535
|
+
if (!pair.downstream)
|
|
536
|
+
pending.push(frame);
|
|
537
|
+
else
|
|
538
|
+
send(frame);
|
|
539
|
+
});
|
|
540
|
+
upstream.once('open', () => {
|
|
541
|
+
if (closed || stopped || socket.destroyed) {
|
|
542
|
+
dispose();
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
routes.set(request, upstream);
|
|
546
|
+
accepted.handleUpgrade(request, socket, head, downstream => {
|
|
547
|
+
pair.downstream = downstream;
|
|
548
|
+
downstream.on('message', (data, binary) => {
|
|
549
|
+
const frame = frameOf(data, binary);
|
|
550
|
+
if (!closed && upstream.readyState === Client.OPEN)
|
|
551
|
+
upstream.send(frame, { binary: typeof frame !== 'string' });
|
|
552
|
+
});
|
|
553
|
+
downstream.on('error', dispose);
|
|
554
|
+
downstream.once('close', (code, reason) => { if (!closed && upstream.readyState === Client.OPEN)
|
|
555
|
+
upstream.close(closeCode(code), reason); });
|
|
556
|
+
for (const frame of pending.splice(0))
|
|
557
|
+
send(frame);
|
|
558
|
+
});
|
|
559
|
+
});
|
|
560
|
+
upstream.once('unexpected-response', (_request, response) => {
|
|
561
|
+
if (pair.downstream) {
|
|
562
|
+
dispose();
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
const chunks = [];
|
|
566
|
+
response.on('data', (chunk) => chunks.push(chunk));
|
|
567
|
+
response.once('end', () => {
|
|
568
|
+
const body = Buffer.concat(chunks);
|
|
569
|
+
const carried = Object.entries(response.headers).filter(([name]) => !/^(connection|content-length|transfer-encoding)$/i.test(name))
|
|
570
|
+
.flatMap(([name, values]) => (Array.isArray(values) ? values : [values]).filter(value => value !== undefined).map(value => name + ': ' + value));
|
|
571
|
+
const header = Buffer.from(['HTTP/1.1 ' + (response.statusCode ?? 502) + ' ' + (response.statusMessage ?? 'Rejected'),
|
|
572
|
+
'Connection: close', 'Content-Length: ' + body.length, ...carried, '', ''].join('\r\n'));
|
|
573
|
+
socket.end(Buffer.concat([header, body]));
|
|
574
|
+
pairs.delete(pair);
|
|
575
|
+
closed = true;
|
|
576
|
+
upstream.terminate();
|
|
577
|
+
});
|
|
578
|
+
response.once('error', dispose);
|
|
579
|
+
});
|
|
580
|
+
upstream.on('error', dispose);
|
|
581
|
+
upstream.once('close', (code, reason) => {
|
|
582
|
+
if (pair.downstream && pair.downstream.readyState === Client.OPEN)
|
|
583
|
+
pair.downstream.close(closeCode(code), reason);
|
|
584
|
+
else if (!pair.downstream)
|
|
585
|
+
socket.destroy();
|
|
586
|
+
pairs.delete(pair);
|
|
587
|
+
});
|
|
588
|
+
socket.once('error', dispose);
|
|
589
|
+
socket.once('close', () => { if (!pair.downstream)
|
|
590
|
+
dispose(); });
|
|
591
|
+
};
|
|
592
|
+
server.on('upgrade', upgrade);
|
|
593
|
+
return { stop: () => {
|
|
594
|
+
stopped = true;
|
|
595
|
+
server.off('upgrade', upgrade);
|
|
596
|
+
for (const pair of pairs) {
|
|
597
|
+
pair.upstream.terminate();
|
|
598
|
+
pair.downstream?.terminate();
|
|
599
|
+
}
|
|
600
|
+
pairs.clear();
|
|
601
|
+
for (const peer of accepted.clients)
|
|
602
|
+
peer.terminate();
|
|
603
|
+
accepted.close();
|
|
604
|
+
} };
|
|
605
|
+
}
|
package/dist/src/signing.d.ts
CHANGED
|
@@ -31,12 +31,23 @@ export type SigningKey = {
|
|
|
31
31
|
};
|
|
32
32
|
/** The algorithms `jwtSign` signs with. */
|
|
33
33
|
export declare const SIGNING_ALGORITHMS: ReadonlyArray<SigningKey['alg']>;
|
|
34
|
-
/**
|
|
34
|
+
/** A token check's result, with its payload on success. `jwtVerify` checks time as well as the signature. */
|
|
35
35
|
export type JwtVerdict = {
|
|
36
36
|
valid: boolean;
|
|
37
37
|
payload?: Row;
|
|
38
38
|
reason?: 'malformed' | 'unexpected_alg' | 'no_matching_key' | 'bad_signature' | 'expired' | 'not_yet_valid';
|
|
39
39
|
};
|
|
40
|
+
export type JwtVerificationKey = Jwks | {
|
|
41
|
+
alg: 'HS256' | 'HS384' | 'HS512';
|
|
42
|
+
secret: string;
|
|
43
|
+
};
|
|
44
|
+
export type JwtDecodeOptions = {
|
|
45
|
+
strict?: boolean;
|
|
46
|
+
integers?: 'exact' | 'number';
|
|
47
|
+
};
|
|
48
|
+
export type JwtSignatureOptions = JwtDecodeOptions & {
|
|
49
|
+
hmacSignature?: 'canonical' | 'bytes';
|
|
50
|
+
};
|
|
40
51
|
/** A public key's id: a prefix and the first 16 hex of SHA-256 over its DER (what its JWKS and every token name). */
|
|
41
52
|
export declare function keyId(publicPem: string, prefix?: string): string;
|
|
42
53
|
/** A JWT: `iat` and `nbf` are `now` (seconds, the World's), `exp` `now` plus the lifetime (60 s when none is given),
|
|
@@ -47,19 +58,29 @@ export declare function jwtSign(claims: Row, key: SigningKey, opts: {
|
|
|
47
58
|
kidPrefix?: string;
|
|
48
59
|
typ?: string;
|
|
49
60
|
}): string;
|
|
50
|
-
/** A JWT
|
|
51
|
-
export declare function
|
|
61
|
+
/** A JWT over exactly the supplied claims, without adding temporal claims. The caller owns their policy. */
|
|
62
|
+
export declare function jwtSignClaims(claims: Row, key: SigningKey, opts?: {
|
|
63
|
+
kidPrefix?: string;
|
|
64
|
+
typ?: string;
|
|
65
|
+
}): string;
|
|
66
|
+
/** A JWT's header and payload, unverified. Strict compact framing and ordinary JSON numbers are explicit
|
|
67
|
+
* protocol policies; existing kernel callers retain permissive decoding and exact integer values. */
|
|
68
|
+
export declare function jwtDecode(token: string, opts?: JwtDecodeOptions): {
|
|
52
69
|
header: Row;
|
|
53
70
|
payload: Row;
|
|
54
71
|
};
|
|
55
72
|
/** A token checked: an RS256 or EdDSA one against a JWKS (its `kid`'s key), an HS256 one against its secret (constant time),
|
|
56
73
|
* then its `exp`/`nbf` at `now` (seconds). */
|
|
57
|
-
export declare function jwtVerify(token: string, key:
|
|
58
|
-
alg: 'HS256' | 'HS384' | 'HS512';
|
|
59
|
-
secret: string;
|
|
60
|
-
}, opts: {
|
|
74
|
+
export declare function jwtVerify(token: string, key: JwtVerificationKey, opts: {
|
|
61
75
|
now: number;
|
|
62
76
|
}): JwtVerdict;
|
|
77
|
+
/** A token's signature checked without an `exp`/`nbf` policy. An issuer selector sees unverified claims and may
|
|
78
|
+
* enforce its vendor's claim shape and key ownership before returning a key. HMAC signatures use canonical
|
|
79
|
+
* text by default; a protocol that compares decoded bytes selects that existing wire policy explicitly. */
|
|
80
|
+
export declare function jwtVerifySignature(token: string, key: JwtVerificationKey | ((decoded: {
|
|
81
|
+
header: Row;
|
|
82
|
+
payload: Row;
|
|
83
|
+
}) => JwtVerificationKey), opts?: JwtSignatureOptions): JwtVerdict;
|
|
63
84
|
/** The JWKS that verifies every token a public key's pair signs. */
|
|
64
85
|
export declare function jwks(publicPem: string, kidPrefix?: string): Jwks;
|
|
65
86
|
/** An HMAC of a value, hex or base64 (a webhook's signature, a signed id): SHA-256 unless the vendor names another
|
|
@@ -125,7 +146,9 @@ export declare function certificateOf(cert: string | Uint8Array): {
|
|
|
125
146
|
/** What a handler signs and hashes with: `ctx.crypto`. */
|
|
126
147
|
export declare const handlerCrypto: {
|
|
127
148
|
readonly jwtSign: typeof jwtSign;
|
|
149
|
+
readonly jwtSignClaims: typeof jwtSignClaims;
|
|
128
150
|
readonly jwtVerify: typeof jwtVerify;
|
|
151
|
+
readonly jwtVerifySignature: typeof jwtVerifySignature;
|
|
129
152
|
readonly jwtDecode: typeof jwtDecode;
|
|
130
153
|
readonly jwks: typeof jwks;
|
|
131
154
|
readonly keyId: typeof keyId;
|
package/dist/src/signing.js
CHANGED
|
@@ -22,6 +22,10 @@ export function keyId(publicPem, prefix = 'ins_') {
|
|
|
22
22
|
/** A JWT: `iat` and `nbf` are `now` (seconds, the World's), `exp` `now` plus the lifetime (60 s when none is given),
|
|
23
23
|
* unless the claims name their own. A token of a key pair carries the key's `kid`. */
|
|
24
24
|
export function jwtSign(claims, key, opts) {
|
|
25
|
+
return jwtSignClaims({ iat: opts.now, exp: opts.now + (opts.expiresInSeconds ?? 60), nbf: opts.now, ...claims }, key, opts);
|
|
26
|
+
}
|
|
27
|
+
/** A JWT over exactly the supplied claims, without adding temporal claims. The caller owns their policy. */
|
|
28
|
+
export function jwtSignClaims(claims, key, opts = {}) {
|
|
25
29
|
// a key of another kind than its algorithm's is refused by name (an RSA key under EdDSA would fail inside the signer)
|
|
26
30
|
if ('privatePem' in key) {
|
|
27
31
|
const kind = nodeCrypto.createPrivateKey(key.privatePem).asymmetricKeyType;
|
|
@@ -32,8 +36,7 @@ export function jwtSign(claims, key, opts) {
|
|
|
32
36
|
const header = 'privatePem' in key
|
|
33
37
|
? { alg: key.alg, typ: opts.typ ?? 'JWT', kid: keyId(key.publicPem ?? nodeCrypto.createPublicKey(key.privatePem).export({ type: 'spki', format: 'pem' }).toString(), opts.kidPrefix) }
|
|
34
38
|
: { alg: key.alg, typ: opts.typ ?? 'JWT' };
|
|
35
|
-
const
|
|
36
|
-
const input = `${jsonPart(header)}.${jsonPart(payload)}`;
|
|
39
|
+
const input = `${jsonPart(header)}.${jsonPart(claims)}`;
|
|
37
40
|
if ('secret' in key)
|
|
38
41
|
return `${input}.${base64url(nodeCrypto.createHmac(digestOf(key.alg), key.secret).update(input).digest())}`;
|
|
39
42
|
if (key.alg === 'EdDSA')
|
|
@@ -43,12 +46,16 @@ export function jwtSign(claims, key, opts) {
|
|
|
43
46
|
signer.end();
|
|
44
47
|
return `${input}.${base64url(signer.sign(key.privatePem))}`;
|
|
45
48
|
}
|
|
46
|
-
/** A JWT's header and payload, unverified.
|
|
47
|
-
|
|
49
|
+
/** A JWT's header and payload, unverified. Strict compact framing and ordinary JSON numbers are explicit
|
|
50
|
+
* protocol policies; existing kernel callers retain permissive decoding and exact integer values. */
|
|
51
|
+
export function jwtDecode(token, opts = {}) {
|
|
52
|
+
if (opts.strict && typeof token !== 'string')
|
|
53
|
+
throw new Error('malformed jwt');
|
|
48
54
|
const parts = token.split('.');
|
|
49
|
-
if (parts.length !== 3)
|
|
55
|
+
if (parts.length !== 3 || opts.strict && parts.some(part => !/^[A-Za-z0-9_-]+$/.test(part)))
|
|
50
56
|
throw new Error('malformed jwt');
|
|
51
|
-
const
|
|
57
|
+
const parse = opts.integers === 'number' ? JSON.parse : parseExactJson;
|
|
58
|
+
const part = (s) => parse(fromBase64url(s).toString('utf8'));
|
|
52
59
|
return { header: part(parts[0]), payload: part(parts[1]) };
|
|
53
60
|
}
|
|
54
61
|
function timely(payload, now) {
|
|
@@ -61,27 +68,36 @@ function timely(payload, now) {
|
|
|
61
68
|
/** A token checked: an RS256 or EdDSA one against a JWKS (its `kid`'s key), an HS256 one against its secret (constant time),
|
|
62
69
|
* then its `exp`/`nbf` at `now` (seconds). */
|
|
63
70
|
export function jwtVerify(token, key, opts) {
|
|
71
|
+
const verdict = jwtVerifySignature(token, key);
|
|
72
|
+
return verdict.valid ? timely(verdict.payload, opts.now) : verdict;
|
|
73
|
+
}
|
|
74
|
+
/** A token's signature checked without an `exp`/`nbf` policy. An issuer selector sees unverified claims and may
|
|
75
|
+
* enforce its vendor's claim shape and key ownership before returning a key. HMAC signatures use canonical
|
|
76
|
+
* text by default; a protocol that compares decoded bytes selects that existing wire policy explicitly. */
|
|
77
|
+
export function jwtVerifySignature(token, key, opts = {}) {
|
|
64
78
|
let decoded;
|
|
65
79
|
try {
|
|
66
|
-
decoded = jwtDecode(token);
|
|
80
|
+
decoded = jwtDecode(token, opts);
|
|
67
81
|
}
|
|
68
82
|
catch {
|
|
69
83
|
return { valid: false, reason: 'malformed' };
|
|
70
84
|
}
|
|
85
|
+
const selected = typeof key === 'function' ? key(decoded) : key;
|
|
71
86
|
const [h, p, s] = token.split('.');
|
|
72
|
-
if ('secret' in
|
|
73
|
-
if (decoded.header.alg !==
|
|
87
|
+
if ('secret' in selected) {
|
|
88
|
+
if (decoded.header.alg !== selected.alg)
|
|
74
89
|
return { valid: false, reason: 'unexpected_alg' };
|
|
75
|
-
const
|
|
76
|
-
const
|
|
90
|
+
const bytes = nodeCrypto.createHmac(digestOf(selected.alg), selected.secret).update(`${h}.${p}`).digest();
|
|
91
|
+
const expected = opts.hmacSignature === 'bytes' ? bytes : Buffer.from(base64url(bytes), 'utf8');
|
|
92
|
+
const given = opts.hmacSignature === 'bytes' ? fromBase64url(s) : Buffer.from(s, 'utf8');
|
|
77
93
|
if (given.length !== expected.length || !nodeCrypto.timingSafeEqual(given, expected))
|
|
78
94
|
return { valid: false, reason: 'bad_signature' };
|
|
79
|
-
return
|
|
95
|
+
return { valid: true, payload: decoded.payload };
|
|
80
96
|
}
|
|
81
97
|
if (!['RS256', 'RS384', 'RS512', 'EdDSA'].includes(String(decoded.header.alg)))
|
|
82
98
|
return { valid: false, reason: 'unexpected_alg' };
|
|
83
99
|
// a token naming a key the set does not hold matches none; one naming no key is tried against the set's only key
|
|
84
|
-
const jwk = decoded.header.kid !== undefined ?
|
|
100
|
+
const jwk = decoded.header.kid !== undefined ? selected.keys.find((k) => k.kid === decoded.header.kid) : selected.keys.length === 1 ? selected.keys[0] : undefined;
|
|
85
101
|
if (!jwk)
|
|
86
102
|
return { valid: false, reason: 'no_matching_key' };
|
|
87
103
|
if ((jwk.kty === 'OKP') !== (decoded.header.alg === 'EdDSA'))
|
|
@@ -90,7 +106,7 @@ export function jwtVerify(token, key, opts) {
|
|
|
90
106
|
const edKey = nodeCrypto.createPublicKey({ key: { kty: 'OKP', crv: 'Ed25519', x: jwk.x }, format: 'jwk' });
|
|
91
107
|
if (!nodeCrypto.verify(null, Buffer.from(`${h}.${p}`), edKey, fromBase64url(s)))
|
|
92
108
|
return { valid: false, reason: 'bad_signature' };
|
|
93
|
-
return
|
|
109
|
+
return { valid: true, payload: decoded.payload };
|
|
94
110
|
}
|
|
95
111
|
const verifier = nodeCrypto.createVerify(`RSA-SHA${String(decoded.header.alg).slice(2)}`);
|
|
96
112
|
verifier.update(`${h}.${p}`);
|
|
@@ -98,7 +114,7 @@ export function jwtVerify(token, key, opts) {
|
|
|
98
114
|
const publicKey = nodeCrypto.createPublicKey({ key: { kty: 'RSA', n: jwk.n, e: jwk.e }, format: 'jwk' });
|
|
99
115
|
if (!verifier.verify(publicKey, fromBase64url(s)))
|
|
100
116
|
return { valid: false, reason: 'bad_signature' };
|
|
101
|
-
return
|
|
117
|
+
return { valid: true, payload: decoded.payload };
|
|
102
118
|
}
|
|
103
119
|
/** The JWKS that verifies every token a public key's pair signs. */
|
|
104
120
|
export function jwks(publicPem, kidPrefix) {
|
|
@@ -256,7 +272,7 @@ export function certificateOf(cert) {
|
|
|
256
272
|
}
|
|
257
273
|
}
|
|
258
274
|
/** What a handler signs and hashes with: `ctx.crypto`. */
|
|
259
|
-
export const handlerCrypto = { jwtSign, jwtVerify, jwtDecode, jwks, keyId, hmac, sha256, md5, digest, uuidFrom, lettersFrom, base62From, equalSecrets, sealedBoxKeyPair, sealedBoxOpen, signWith, verifyWith, publicKeyOf, certificateOf };
|
|
275
|
+
export const handlerCrypto = { jwtSign, jwtSignClaims, jwtVerify, jwtVerifySignature, jwtDecode, jwks, keyId, hmac, sha256, md5, digest, uuidFrom, lettersFrom, base62From, equalSecrets, sealedBoxKeyPair, sealedBoxOpen, signWith, verifyWith, publicKeyOf, certificateOf };
|
|
260
276
|
/** A [KSUID](https://github.com/segmentio/ksuid) as 27 base62 characters (0-9A-Za-z, most significant first): `seconds`
|
|
261
277
|
* past KSUID's epoch (1400000000) in four bytes, then `n` in four, then twelve bytes derived from `seed`, so KSUIDs
|
|
262
278
|
* made in one second sort by `n`, at `width` characters (27 unless a vendor's id is longer or shorter). Deterministic:
|
package/dist/src/sockets.d.ts
CHANGED
|
@@ -51,4 +51,4 @@ export declare function expireSocketWaits(service: string, root: string | undefi
|
|
|
51
51
|
* awaited: the write may be one a session's own frame made, which that session finishes before it is told. */
|
|
52
52
|
export declare function socketWrite(service: string, root: string | undefined, write: EventWrite): void;
|
|
53
53
|
/** The upgrade the serve seam takes: a request to a declared socket's path (and host) opens a session of its engine. */
|
|
54
|
-
export declare function socketUpgrade<C>(service: string, root: string | undefined, decls: ReadonlyArray<SocketDecl>, engines: Record<string, SocketEngine<C>>, context: (request: Request, decl: SocketDecl, params: Record<string, string>) => Promise<C>, hostOf: (request: Request) => string, pathOf
|
|
54
|
+
export declare function socketUpgrade<C>(service: string, root: string | undefined, decls: ReadonlyArray<SocketDecl>, engines: Record<string, SocketEngine<C>>, context: (request: Request, decl: SocketDecl, params: Record<string, string>) => Promise<C>, hostOf: (request: Request) => string, pathOf: (request: Request) => string, decide: (asked: unknown) => Promise<ScenarioServed | undefined>): WebSocketUpgrade;
|