super-dsh 0.1.4 → 0.1.5
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/README.md +6 -7
- package/agent-agy/dist/world-plugin.js +18 -33
- package/agent-codex/dist/world-plugin.js +18 -33
- package/agent-hermes/dist/world-plugin.js +18 -34
- package/agent-omp/dist/world-plugin.js +16 -32
- package/agent-pi/dist/world-plugin.js +18 -33
- package/client/index.js +2086 -0
- package/cordis.patch.yml +20 -7
- package/package.json +13 -3
- package/superd-hub/dist/agent-roster.d.ts +3 -1
- package/superd-hub/dist/agent-roster.js +4 -2
- package/superd-hub/dist/carrier.d.ts +41 -28
- package/superd-hub/dist/carrier.js +148 -202
- package/superd-hub/dist/fusion-transport.d.ts +148 -0
- package/superd-hub/dist/fusion-transport.js +397 -0
- package/superd-hub/dist/gateway.d.ts +7 -2
- package/superd-hub/dist/gateway.js +28 -2
- package/superd-hub/dist/hub-route.d.ts +74 -0
- package/superd-hub/dist/hub-route.js +188 -0
- package/superd-hub/dist/index.d.ts +10 -4
- package/superd-hub/dist/index.js +10 -4
- package/superd-hub/dist/roster.d.ts +2 -2
- package/superd-hub/dist/spawn-world.js +5 -3
- package/superd-hub/dist/switch-core.d.ts +630 -0
- package/superd-hub/dist/switch-core.js +734 -0
- package/superd-hub/dist/web-trust.d.ts +5 -5
- package/superd-hub/dist/web-trust.js +5 -5
- package/superd-hub/dist/world-entry.d.ts +20 -13
- package/superd-hub/dist/world-entry.js +15 -19
- package/superd-hub/dist/world-host.d.ts +15 -20
- package/superd-hub/dist/world-host.js +25 -4
- package/superd-hub/dist/world-join.js +14 -29
- package/superd-hub/dist/world-mount.d.ts +48 -26
- package/superd-hub/dist/world-mount.js +59 -27
- package/superd-hub/dist/world-provision.js +11 -4
- package/superd-hub/dist/world-web-server.d.ts +57 -106
- package/superd-hub/dist/world-web-server.js +61 -216
- package/superd-hub/lib/client/index.js +2086 -0
- package/superd-hub/package.json +12 -0
- package/agent-agy/dist/agent-preset-projection.d.ts +0 -31
- package/agent-agy/dist/agent-preset-projection.js +0 -33
- package/agent-claude/dist/agent-preset-projection.js +0 -30
- package/agent-codex/dist/agent-preset-projection.js +0 -37
- package/agent-codex/dist/app-home.js +0 -65
- package/agent-codex/dist/spike.js +0 -46
- package/agent-hermes/dist/agent-preset-projection.js +0 -34
- package/agent-omp/dist/agent-preset-projection.js +0 -36
- package/agent-omp/dist/app-home.js +0 -86
- package/agent-pi/dist/agent-preset-projection.js +0 -29
- package/superd-hub/dist/client-shim.d.ts +0 -28
- package/superd-hub/dist/client-shim.js +0 -251
- package/superd-hub/dist/index-pass.d.ts +0 -22
- package/superd-hub/dist/index-pass.js +0 -47
- package/superd-hub/dist/labels.d.ts +0 -45
- package/superd-hub/dist/labels.js +0 -83
package/cordis.patch.yml
CHANGED
|
@@ -4,13 +4,9 @@
|
|
|
4
4
|
# exports, so every row below is individually toggleable on the plugin page
|
|
5
5
|
# (cordis hmr; docs/06 §二.5 — insert rows = the "Components" lines).
|
|
6
6
|
#
|
|
7
|
-
# - super-dsh/hub the hub
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
# imports (world spawn/mount/carrier/web-trust);
|
|
11
|
-
# the ctx0 control-plane row and ui client half
|
|
12
|
-
# were stripped in 0.1.4 (archival final state
|
|
13
|
-
# of the multi-webui path)
|
|
7
|
+
# - super-dsh/hub the hub: selector + roster + per-world mount
|
|
8
|
+
# carrier + V5 gateway delegation (wraps the live
|
|
9
|
+
# typertGateway; upstream dispatch untouched)
|
|
14
10
|
# - super-dsh/agents/<label> one world plugin per adapter (activation IS
|
|
15
11
|
# roster membership, S2) — each spawns its own
|
|
16
12
|
# zero-port sibling root, mounted under /<label>
|
|
@@ -29,8 +25,25 @@
|
|
|
29
25
|
# Profile-layer values (webserver port/host, settings path) stay in the USER's
|
|
30
26
|
# profile patch layer, never here.
|
|
31
27
|
- insert:
|
|
28
|
+
# BARE package row: subpath rows (…/agents/omp) are permanently not client
|
|
29
|
+
# rows in client-modules' scan (exactPackageSpecifier), so THIS row is
|
|
30
|
+
# what registers the package's client half — the sidebar selector.
|
|
31
|
+
- id: superd-hub
|
|
32
|
+
name: 'super-dsh'
|
|
32
33
|
- id: superd-web-trust
|
|
33
34
|
name: 'super-dsh/web-trust'
|
|
35
|
+
# Native UI fusion transport (plan Task 3): the ctx0 page-level state atom
|
|
36
|
+
# + five call-time patches, flag-gated (SUPERD_UI_FUSION, default off).
|
|
37
|
+
# ROW ORDER IS LOAD ORDER and the row MUST stay after superd-web-trust:
|
|
38
|
+
# the trust script's non-merging `if(!window.__DSH_TRANSPORT__)` verdict
|
|
39
|
+
# must be evaluated before the fusion script could own that global
|
|
40
|
+
# (spec §3.4, hard; precedent world-web-server.ts renderIndex).
|
|
41
|
+
# On/off (Task 5 formalization): env `SUPERD_UI_FUSION` derives the
|
|
42
|
+
# schema default (off when unset); a profile layer may restate this row
|
|
43
|
+
# (same id, same name) with `config: {enabled: true|false}` to override.
|
|
44
|
+
# Off = the component registers nothing: no script, no flag global.
|
|
45
|
+
- id: superd-fusion-transport
|
|
46
|
+
name: 'super-dsh/fusion-transport'
|
|
34
47
|
- id: agent-omp
|
|
35
48
|
name: 'super-dsh/agents/omp'
|
|
36
49
|
- id: agent-codex
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "super-dsh",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Super D for the DeepSeek Harness (DSH) — one dsh plugin that mounts the superd hub (roster, per-runtime agents) and joins local foreign-agent runtimes (OMP, Codex, Claude, Pi, Hermes, Antigravity) into the DSH Web UI. Each runtime keeps its native home and session store; the hub only bridges.",
|
|
3
|
+
"version": "0.1.5",
|
|
4
|
+
"description": "Super D for the DeepSeek Harness (DSH) — one dsh plugin that mounts the superd hub (runtime selector, roster, per-runtime agents) and joins local foreign-agent runtimes (OMP, Codex, Claude, Pi, Hermes, Antigravity) into the DSH Web UI. Each runtime keeps its native home and session store; the hub only bridges.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -18,14 +18,18 @@
|
|
|
18
18
|
"./agents/pi": "./agent-pi/dist/world-plugin.js",
|
|
19
19
|
"./agents/hermes": "./agent-hermes/dist/world-plugin.js",
|
|
20
20
|
"./agents/agy": "./agent-agy/dist/world-plugin.js",
|
|
21
|
+
"./client": "./client/index.js",
|
|
21
22
|
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
22
23
|
"./package.json": "./package.json",
|
|
23
|
-
"./web-trust": "./superd-hub/dist/web-trust.js"
|
|
24
|
+
"./web-trust": "./superd-hub/dist/web-trust.js",
|
|
25
|
+
"./fusion-transport": "./superd-hub/dist/fusion-transport.js"
|
|
24
26
|
},
|
|
25
27
|
"files": [
|
|
26
28
|
"cordis.patch.yml",
|
|
29
|
+
"client",
|
|
27
30
|
"README.md",
|
|
28
31
|
"superd-hub/dist",
|
|
32
|
+
"superd-hub/lib",
|
|
29
33
|
"superd-hub/package.json",
|
|
30
34
|
"superd-hub/cordis.patch.yml",
|
|
31
35
|
"agent-omp/dist",
|
|
@@ -52,6 +56,12 @@
|
|
|
52
56
|
"dsh": {
|
|
53
57
|
"bundle": {
|
|
54
58
|
"patch": "./cordis.patch.yml"
|
|
59
|
+
},
|
|
60
|
+
"client": {
|
|
61
|
+
"inject": [
|
|
62
|
+
"@deepseek-ai/dsh-client-ui-slots"
|
|
63
|
+
],
|
|
64
|
+
"platform": "web"
|
|
55
65
|
}
|
|
56
66
|
},
|
|
57
67
|
"dependencies": {
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* /codex/ → the Codex world's mount
|
|
13
13
|
*/
|
|
14
14
|
import type { IndexInjection } from '@deepseek-ai/dsh-host-webserver';
|
|
15
|
-
/** One selectable runtime: its key, its label,
|
|
15
|
+
/** One selectable runtime: its key, its label, the path that addresses it, its machine. */
|
|
16
16
|
export interface AgentLink {
|
|
17
17
|
/** `native` for ctx0, otherwise the adapter's runtime key. */
|
|
18
18
|
key: string;
|
|
@@ -20,6 +20,8 @@ export interface AgentLink {
|
|
|
20
20
|
label: string;
|
|
21
21
|
/** Mount root, always trailing-slashed except the native `/`. */
|
|
22
22
|
path: string;
|
|
23
|
+
/** Machine the runtime runs on — the Hub UI's grouping key. */
|
|
24
|
+
machine: string;
|
|
23
25
|
}
|
|
24
26
|
/** The global this module publishes the roster under. */
|
|
25
27
|
export declare const AGENT_ROSTER_GLOBAL = "__DSH_AGENT_ROSTER__";
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { listAgents } from './roster.js';
|
|
2
2
|
import { takeHostMount } from './world-host.js';
|
|
3
|
+
/** Machine of every roster row today — the local constant; the extension point a multi-machine registry replaces. */
|
|
4
|
+
const LOCAL_MACHINE = 'local';
|
|
3
5
|
/** The native runtime's synthetic entry (no adapter plugin backs it). */
|
|
4
|
-
const NATIVE = { key: 'native', label: 'DSH', path: '/' };
|
|
6
|
+
const NATIVE = { key: 'native', label: 'DSH', path: '/', machine: LOCAL_MACHINE };
|
|
5
7
|
/** The global this module publishes the roster under. */
|
|
6
8
|
export const AGENT_ROSTER_GLOBAL = '__DSH_AGENT_ROSTER__';
|
|
7
9
|
/**
|
|
@@ -17,7 +19,7 @@ export function agentLinks() {
|
|
|
17
19
|
if (mount === undefined)
|
|
18
20
|
continue;
|
|
19
21
|
const path = mount.labelPath.endsWith('/') ? mount.labelPath : `${mount.labelPath}/`;
|
|
20
|
-
links.push({ key: agent.key, label: agent.label, path });
|
|
22
|
+
links.push({ key: agent.key, label: agent.label, path, machine: LOCAL_MACHINE });
|
|
21
23
|
}
|
|
22
24
|
return links;
|
|
23
25
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type
|
|
2
|
-
import { type MuxFailure } from './world-mux.js';
|
|
1
|
+
import { type WorldRequest, type WorldResponse } from './world-web-server.js';
|
|
2
|
+
import { WorldMuxServer, type MuxFailure } from './world-mux.js';
|
|
3
3
|
/** The three-method face of a world's typertGateway service instance. */
|
|
4
4
|
export interface CarrierGatewayFace {
|
|
5
5
|
dispatchRpc(endpoint: string, payload: unknown, signal: AbortSignal): Promise<unknown>;
|
|
@@ -12,38 +12,51 @@ export interface CarrierGatewayFace {
|
|
|
12
12
|
*/
|
|
13
13
|
openWireStream(endpoint: string, payload: unknown, uplink: AsyncIterable<unknown>, peer: undefined, signal: AbortSignal, control: AbortController): Promise<AsyncIterable<unknown>> | AsyncIterable<unknown>;
|
|
14
14
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
real: RealWebServerFace;
|
|
19
|
-
/** Runtime key: the mount label (`omp` → `/omp`). */
|
|
20
|
-
label: string;
|
|
15
|
+
export declare function writeJson(res: WorldResponse, status: number, body: string): void;
|
|
16
|
+
/** Deps the forwarding kernel needs to answer for one world's traffic. */
|
|
17
|
+
export interface ForwardOptions {
|
|
21
18
|
/** The addressed world's gateway instance. */
|
|
22
19
|
gateway: CarrierGatewayFace;
|
|
20
|
+
/** World key addressing the world's published `/api` fetch handler. */
|
|
21
|
+
label: string;
|
|
22
|
+
/** URL prefix this carrier strips before the world sees the request (`/omp`, `/hub`). */
|
|
23
|
+
mountPath: string;
|
|
23
24
|
/** ctx0's browser-trust fence: returns an HTTP status to reject with. */
|
|
24
25
|
requestRejection?(req: unknown): number | undefined;
|
|
25
26
|
/** Max request body bytes (mirrors the connection default). */
|
|
26
27
|
maxBodyBytes?: number;
|
|
27
|
-
/** Mux ping interval (upstream default 30s). */
|
|
28
|
-
heartbeatIntervalMs?: number;
|
|
29
|
-
/** Error → wire failure mapper for the world's streams. */
|
|
30
|
-
failure?(error: unknown): MuxFailure;
|
|
31
|
-
}
|
|
32
|
-
/** The live mount handle. */
|
|
33
|
-
export interface MountedWorld {
|
|
34
|
-
/** Mount root (`/omp`). */
|
|
35
|
-
readonly labelPath: string;
|
|
36
|
-
/** Unary channel root (`/omp/api`). */
|
|
37
|
-
readonly apiPath: string;
|
|
38
|
-
/** Mux websocket path (`/omp/api/remote.mux`). */
|
|
39
|
-
readonly muxPath: string;
|
|
40
|
-
/** Unregister everything this mount installed. */
|
|
41
|
-
dispose(): void;
|
|
42
28
|
}
|
|
43
29
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
30
|
+
* Forward one non-envelope GET/HEAD request into the owning world's shared
|
|
31
|
+
* `/api` fetch handler (exact Fetch routes first, then the gateway's RPC
|
|
32
|
+
* interceptor, then 404 — the upstream composition). The caller has already
|
|
33
|
+
* answered the ctx0 fence by the time we get here; the world's own
|
|
34
|
+
* browser-auth gate is never consulted (a different tree, a different secret).
|
|
35
|
+
*/
|
|
36
|
+
export declare function forwardWorldFetch(options: {
|
|
37
|
+
label: string;
|
|
38
|
+
mountPath: string;
|
|
39
|
+
}, req: WorldRequest, res: WorldResponse): Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* The reusable forwarding kernel (envelope dispatch + world fetch bridge):
|
|
42
|
+
* decode the connection envelope and dispatch into the ADDRESSED world's
|
|
43
|
+
* gateway instance; non-envelope GET/HEAD traffic forwards into the world's
|
|
44
|
+
* own registered fetch routes. Parameterized by (gateway, label, mountPath)
|
|
45
|
+
* so the `/hub` route family shares this exact behavior — the demux key is
|
|
46
|
+
* the caller's business, the wire semantics live here.
|
|
47
|
+
*/
|
|
48
|
+
export declare function forwardApiCall(options: ForwardOptions, req: WorldRequest, res: WorldResponse): Promise<void>;
|
|
49
|
+
/** One WorldMuxServer bound to `gateway` — the canonical openWireStream wiring in one place. */
|
|
50
|
+
export declare function worldMuxOf(gateway: CarrierGatewayFace, options?: {
|
|
51
|
+
heartbeatIntervalMs?: number;
|
|
52
|
+
failure?: (error: unknown) => MuxFailure;
|
|
53
|
+
}): WorldMuxServer;
|
|
54
|
+
/**
|
|
55
|
+
* RETIRED with T-D (2026-09-26): the T-C `mountWorld` no-op — which itself
|
|
56
|
+
* used to register `/<label>/api`, `/<label>/open-in-app` and
|
|
57
|
+
* `/<label>/api/remote.mux` on the real webServer — is GONE from the module.
|
|
58
|
+
* The adapter world-plugins' spawn-time call sites were updated in the same
|
|
59
|
+
* round (the registerHostMount record needs no webServer and no fence; the
|
|
60
|
+
* `/hub` family reaches each world's gateway through the foreign-target
|
|
61
|
+
* table). Git history keeps the function.
|
|
47
62
|
*/
|
|
48
|
-
export declare function mountWorld(options: MountWorldOptions): MountedWorld;
|
|
49
|
-
export { rewriteIndexHtml } from './index-pass.js';
|
|
@@ -1,23 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The carrier forwarding kernel (AW-B DL1/DL5 → native UI fusion Task 2 →
|
|
3
|
+
* path-separation T-C): the wire semantics every foreign-agent entry shares.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
* -
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
5
|
+
* History and the T-C/T-D cuts:
|
|
6
|
+
* - The kernel — envelope dispatch ({@link forwardApiCall}), the world fetch
|
|
7
|
+
* bridge ({@link forwardWorldFetch}) and the mux construction
|
|
8
|
+
* ({@link worldMuxOf}) — is parameterized by (gateway, label, mountPath).
|
|
9
|
+
* - It used to be mounted twice: the per-label page family
|
|
10
|
+
* (`/<label>/api`, `/<label>/api/remote.mux`, `/<label>/open-in-app` —
|
|
11
|
+
* registered by the now-deleted `mountWorld`) and, since 2026-09-26, the
|
|
12
|
+
* hub-owned `/hub` family (`hub-route.ts`, demuxed by `?agent=`).
|
|
13
|
+
* - T-C (2026-09-26, user ruling "strip off the per agent url path … no need
|
|
14
|
+
* the url path fallback (we have git history)"): the per-label family is
|
|
15
|
+
* RETIRED — `/hub` is the sole foreign entry and the only remaining
|
|
16
|
+
* consumer of this kernel. The world DATA planes are untouched: the same
|
|
17
|
+
* gateway faces (dispatchRpc/openWireStream), the same WorldMuxServer, the
|
|
18
|
+
* same envelope, the same worldFetchHandlerOf lane, all reached through
|
|
19
|
+
* `/hub`.
|
|
20
|
+
* - T-D (2026-09-26, "disable their webui build"): the retired `mountWorld`
|
|
21
|
+
* no-op and the mount HTML pass re-export left the module; the carrier is
|
|
22
|
+
* the forwarding kernel alone.
|
|
19
23
|
*
|
|
20
|
-
*
|
|
24
|
+
* Nothing here reads a global: the addressed world travels with the request.
|
|
21
25
|
*/
|
|
22
26
|
import { EnvelopeError, parseClientRequest, serverError, serverResult, wireErrorOf, } from './envelope.js';
|
|
23
27
|
import { stripLabelFromUrl } from './world-web-server.js';
|
|
@@ -55,19 +59,7 @@ async function readBody(req, limit) {
|
|
|
55
59
|
carrier.on?.('error', (error) => reject(error instanceof Error ? error : new Error(String(error))));
|
|
56
60
|
});
|
|
57
61
|
}
|
|
58
|
-
|
|
59
|
-
function rejectUpgrade(socket, status) {
|
|
60
|
-
const reason = status === 401 ? 'Unauthorized' : 'Forbidden';
|
|
61
|
-
const body = reason.toLowerCase();
|
|
62
|
-
const writable = socket;
|
|
63
|
-
writable.write?.(`HTTP/1.1 ${status} ${reason}\r\n`
|
|
64
|
-
+ 'Connection: close\r\n'
|
|
65
|
-
+ 'Content-Type: text/plain; charset=utf-8\r\n'
|
|
66
|
-
+ `Content-Length: ${String(Buffer.byteLength(body, 'utf8'))}\r\n\r\n`
|
|
67
|
-
+ body);
|
|
68
|
-
writable.destroy?.();
|
|
69
|
-
}
|
|
70
|
-
function writeJson(res, status, body) {
|
|
62
|
+
export function writeJson(res, status, body) {
|
|
71
63
|
const carrier = res;
|
|
72
64
|
carrier.writeHead?.(status, { 'content-type': 'application/json' });
|
|
73
65
|
res.end(body);
|
|
@@ -78,190 +70,144 @@ function writeText(res, status, body) {
|
|
|
78
70
|
carrier.writeHead?.(status, { 'content-type': 'text/plain; charset=utf-8' });
|
|
79
71
|
res.end(body);
|
|
80
72
|
}
|
|
73
|
+
const forwardsFetchMethod = (req) => {
|
|
74
|
+
const method = req.method;
|
|
75
|
+
return method === 'GET' || method === 'HEAD';
|
|
76
|
+
};
|
|
81
77
|
/**
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
78
|
+
* Forward one non-envelope GET/HEAD request into the owning world's shared
|
|
79
|
+
* `/api` fetch handler (exact Fetch routes first, then the gateway's RPC
|
|
80
|
+
* interceptor, then 404 — the upstream composition). The caller has already
|
|
81
|
+
* answered the ctx0 fence by the time we get here; the world's own
|
|
82
|
+
* browser-auth gate is never consulted (a different tree, a different secret).
|
|
85
83
|
*/
|
|
86
|
-
export function
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
84
|
+
export async function forwardWorldFetch(options, req, res) {
|
|
85
|
+
const worldFetch = worldFetchHandlerOf(options.label);
|
|
86
|
+
if (worldFetch === undefined) {
|
|
87
|
+
writeText(res, 404, 'not found');
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const method = (req.method ?? 'GET');
|
|
91
|
+
const resCarrier = res;
|
|
92
|
+
// Client-disconnect detection rides the response (same reason as the
|
|
93
|
+
// upstream node:http bridge: the request 'close' fires as soon as a
|
|
94
|
+
// bodyless GET is consumed).
|
|
95
|
+
const abort = new AbortController();
|
|
96
|
+
const onClose = () => {
|
|
97
|
+
if (resCarrier.writableEnded !== true)
|
|
98
|
+
abort.abort();
|
|
95
99
|
};
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const bridgeWorldFetch = async (req, res) => {
|
|
104
|
-
const worldFetch = worldFetchHandlerOf(options.label);
|
|
105
|
-
if (worldFetch === undefined) {
|
|
106
|
-
writeText(res, 404, 'not found');
|
|
107
|
-
return;
|
|
100
|
+
resCarrier.once?.('close', onClose);
|
|
101
|
+
let headersSent = false;
|
|
102
|
+
try {
|
|
103
|
+
const headers = {};
|
|
104
|
+
for (const [name, value] of Object.entries(req.headers ?? {})) {
|
|
105
|
+
if (typeof value === 'string')
|
|
106
|
+
headers[name.toLowerCase()] = value;
|
|
108
107
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (resCarrier.writableEnded !== true)
|
|
117
|
-
abort.abort();
|
|
118
|
-
};
|
|
119
|
-
resCarrier.once?.('close', onClose);
|
|
120
|
-
let headersSent = false;
|
|
121
|
-
try {
|
|
122
|
-
const headers = {};
|
|
123
|
-
for (const [name, value] of Object.entries(req.headers ?? {})) {
|
|
124
|
-
if (typeof value === 'string')
|
|
125
|
-
headers[name.toLowerCase()] = value;
|
|
126
|
-
}
|
|
127
|
-
// The world route owns the world-relative URL: strip the mount label,
|
|
128
|
-
// keep the query (the session-export route reads it from searchParams).
|
|
129
|
-
const request = new Request(new URL(stripLabelFromUrl(req.url, labelPath), 'http://dsh.internal'), { method, headers, signal: abort.signal });
|
|
130
|
-
const response = await worldFetch.fetch(request);
|
|
131
|
-
res.writeHead(response.status, Object.fromEntries(response.headers.entries()));
|
|
132
|
-
headersSent = true;
|
|
133
|
-
if (response.body === null) {
|
|
134
|
-
res.end();
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
for await (const chunk of response.body) {
|
|
138
|
-
// Backpressure: wait for drain instead of buffering unboundedly; a
|
|
139
|
-
// socket close resolves the wait and the aborted signal ends the stream.
|
|
140
|
-
if (resCarrier.write?.(chunk) === false && typeof resCarrier.once === 'function') {
|
|
141
|
-
await new Promise((resolve) => {
|
|
142
|
-
const done = () => {
|
|
143
|
-
resCarrier.off?.('drain', done);
|
|
144
|
-
resCarrier.off?.('close', done);
|
|
145
|
-
resolve();
|
|
146
|
-
};
|
|
147
|
-
resCarrier.once?.('drain', done);
|
|
148
|
-
resCarrier.once?.('close', done);
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
}
|
|
108
|
+
// The world route owns the world-relative URL: strip the mount prefix,
|
|
109
|
+
// keep the query (the session-export route reads it from searchParams).
|
|
110
|
+
const request = new Request(new URL(stripLabelFromUrl(req.url, options.mountPath), 'http://dsh.internal'), { method, headers, signal: abort.signal });
|
|
111
|
+
const response = await worldFetch.fetch(request);
|
|
112
|
+
res.writeHead(response.status, Object.fromEntries(response.headers.entries()));
|
|
113
|
+
headersSent = true;
|
|
114
|
+
if (response.body === null) {
|
|
152
115
|
res.end();
|
|
153
|
-
}
|
|
154
|
-
catch {
|
|
155
|
-
// The world route owns its statuses; only carrier-level faults land here.
|
|
156
|
-
if (!headersSent)
|
|
157
|
-
writeText(res, 500, 'carrier fetch bridge failure');
|
|
158
|
-
else
|
|
159
|
-
resCarrier.destroy?.();
|
|
160
|
-
}
|
|
161
|
-
finally {
|
|
162
|
-
resCarrier.off?.('close', onClose);
|
|
163
|
-
}
|
|
164
|
-
};
|
|
165
|
-
const handleUnary = async (req, res) => {
|
|
166
|
-
const rejection = options.requestRejection?.(req);
|
|
167
|
-
if (rejection !== undefined) {
|
|
168
|
-
writeJson(res, rejection, JSON.stringify({ error: rejection === 401 ? 'unauthorized' : 'forbidden' }));
|
|
169
116
|
return;
|
|
170
117
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
throw error;
|
|
118
|
+
for await (const chunk of response.body) {
|
|
119
|
+
// Backpressure: wait for drain instead of buffering unboundedly; a
|
|
120
|
+
// socket close resolves the wait and the aborted signal ends the stream.
|
|
121
|
+
if (resCarrier.write?.(chunk) === false && typeof resCarrier.once === 'function') {
|
|
122
|
+
await new Promise((resolve) => {
|
|
123
|
+
const done = () => {
|
|
124
|
+
resCarrier.off?.('drain', done);
|
|
125
|
+
resCarrier.off?.('close', done);
|
|
126
|
+
resolve();
|
|
127
|
+
};
|
|
128
|
+
resCarrier.once?.('drain', done);
|
|
129
|
+
resCarrier.once?.('close', done);
|
|
130
|
+
});
|
|
187
131
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
132
|
+
}
|
|
133
|
+
res.end();
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
// The world route owns its statuses; only carrier-level faults land here.
|
|
137
|
+
if (!headersSent)
|
|
138
|
+
writeText(res, 500, 'carrier fetch bridge failure');
|
|
139
|
+
else
|
|
140
|
+
resCarrier.destroy?.();
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
resCarrier.off?.('close', onClose);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* The reusable forwarding kernel (envelope dispatch + world fetch bridge):
|
|
148
|
+
* decode the connection envelope and dispatch into the ADDRESSED world's
|
|
149
|
+
* gateway instance; non-envelope GET/HEAD traffic forwards into the world's
|
|
150
|
+
* own registered fetch routes. Parameterized by (gateway, label, mountPath)
|
|
151
|
+
* so the `/hub` route family shares this exact behavior — the demux key is
|
|
152
|
+
* the caller's business, the wire semantics live here.
|
|
153
|
+
*/
|
|
154
|
+
export async function forwardApiCall(options, req, res) {
|
|
155
|
+
const maxBody = options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
|
|
156
|
+
const rejection = options.requestRejection?.(req);
|
|
157
|
+
if (rejection !== undefined) {
|
|
158
|
+
writeJson(res, rejection, JSON.stringify({ error: rejection === 401 ? 'unauthorized' : 'forbidden' }));
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
let rpcId = 'invalid-request';
|
|
162
|
+
try {
|
|
163
|
+
const text = await readBody(req, maxBody);
|
|
164
|
+
let envelope;
|
|
165
|
+
try {
|
|
166
|
+
envelope = parseClientRequest(text);
|
|
194
167
|
}
|
|
195
168
|
catch (error) {
|
|
196
|
-
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
if (error instanceof EnvelopeError) {
|
|
200
|
-
|
|
169
|
+
// Non-envelope GET/HEAD traffic on the shared mount prefix belongs to
|
|
170
|
+
// the world's own registered fetch routes; envelopes (and every other
|
|
171
|
+
// method) keep the transport-level 400 below.
|
|
172
|
+
if (error instanceof EnvelopeError && forwardsFetchMethod(req)) {
|
|
173
|
+
await forwardWorldFetch(options, req, res);
|
|
201
174
|
return;
|
|
202
175
|
}
|
|
203
|
-
|
|
176
|
+
throw error;
|
|
204
177
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
}),
|
|
227
|
-
|
|
228
|
-
// Mux: the world's own stream face behind the same mount prefix. The wire
|
|
229
|
-
// protocol is dsh 0.1.7's stream protocol — including the client uplink —
|
|
230
|
-
// and the world call uses the gateway's canonical argument order
|
|
231
|
-
// (endpoint, payload, uplink, peer, signal, control): the mux's controller
|
|
232
|
-
// supplies both the request signal and the logical-stream control.
|
|
233
|
-
const mux = new WorldMuxServer((endpoint, payload, uplink, control) => Promise.resolve(options.gateway.openWireStream(endpoint, payload, uplink, undefined, control.signal, control)), {
|
|
234
|
-
...(options.heartbeatIntervalMs === undefined ? {} : { heartbeatIntervalMs: options.heartbeatIntervalMs }),
|
|
235
|
-
...(options.failure === undefined ? {} : { failure: options.failure }),
|
|
178
|
+
rpcId = envelope.rpcId;
|
|
179
|
+
const controller = new AbortController();
|
|
180
|
+
const result = await options.gateway.dispatchRpc(envelope.method, envelope.payload, controller.signal);
|
|
181
|
+
// The gateway already returns a Result (`{ok:true,value}` | `{ok:false,error}`),
|
|
182
|
+
// which is exactly the server-response's `result` field.
|
|
183
|
+
writeJson(res, 200, serverResult(rpcId, result));
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
const wire = wireErrorOf(error);
|
|
187
|
+
// Envelope-level failures are transport-level 400s; dispatch failures are
|
|
188
|
+
// business results carried inside a 200 server-response (upstream shape).
|
|
189
|
+
if (error instanceof EnvelopeError) {
|
|
190
|
+
writeJson(res, 400, serverError(rpcId, wire));
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
writeJson(res, 200, serverError(rpcId, wire));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/** One WorldMuxServer bound to `gateway` — the canonical openWireStream wiring in one place. */
|
|
197
|
+
export function worldMuxOf(gateway, options) {
|
|
198
|
+
return new WorldMuxServer((endpoint, payload, uplink, control) => Promise.resolve(gateway.openWireStream(endpoint, payload, uplink, undefined, control.signal, control)), {
|
|
199
|
+
...(options?.heartbeatIntervalMs === undefined ? {} : { heartbeatIntervalMs: options.heartbeatIntervalMs }),
|
|
200
|
+
...(options?.failure === undefined ? {} : { failure: options.failure }),
|
|
236
201
|
});
|
|
237
|
-
disposers.push(options.real.registerUpgrade({
|
|
238
|
-
path: muxPath,
|
|
239
|
-
handler: ((req, socket, head) => {
|
|
240
|
-
const rejection = options.requestRejection?.(req);
|
|
241
|
-
if (rejection !== undefined) {
|
|
242
|
-
rejectUpgrade(socket, rejection === 401 ? 401 : 403);
|
|
243
|
-
return;
|
|
244
|
-
}
|
|
245
|
-
return mux.handleUpgrade(req, socket, head);
|
|
246
|
-
}),
|
|
247
|
-
}));
|
|
248
|
-
disposers.push(() => { void mux.close().catch(() => undefined); });
|
|
249
|
-
return {
|
|
250
|
-
labelPath,
|
|
251
|
-
apiPath,
|
|
252
|
-
muxPath,
|
|
253
|
-
dispose() {
|
|
254
|
-
for (const dispose of disposers.splice(0).reverse()) {
|
|
255
|
-
try {
|
|
256
|
-
dispose();
|
|
257
|
-
}
|
|
258
|
-
catch {
|
|
259
|
-
// teardown must not throw
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
},
|
|
263
|
-
};
|
|
264
202
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
203
|
+
/**
|
|
204
|
+
* RETIRED with T-D (2026-09-26): the T-C `mountWorld` no-op — which itself
|
|
205
|
+
* used to register `/<label>/api`, `/<label>/open-in-app` and
|
|
206
|
+
* `/<label>/api/remote.mux` on the real webServer — is GONE from the module.
|
|
207
|
+
* The adapter world-plugins' spawn-time call sites were updated in the same
|
|
208
|
+
* round (the registerHostMount record needs no webServer and no fence; the
|
|
209
|
+
* `/hub` family reaches each world's gateway through the foreign-target
|
|
210
|
+
* table). Git history keeps the function.
|
|
211
|
+
*/
|
|
212
|
+
// The carrier is now purely the forwarding kernel: no mount-time HTML pass,
|
|
213
|
+
// no per-label registration — `/hub` (hub-route.ts) is the sole consumer.
|