super-dsh 0.1.4 → 0.1.5-a

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.
Files changed (39) hide show
  1. package/README.md +6 -7
  2. package/agent-agy/dist/world-plugin.js +18 -33
  3. package/agent-codex/dist/world-plugin.js +18 -33
  4. package/agent-hermes/dist/world-plugin.js +18 -34
  5. package/agent-omp/dist/world-plugin.js +16 -32
  6. package/agent-pi/dist/world-plugin.js +18 -33
  7. package/client/index.js +2086 -0
  8. package/cordis.patch.yml +21 -7
  9. package/package.json +13 -3
  10. package/superd-hub/dist/agent-roster.d.ts +3 -1
  11. package/superd-hub/dist/agent-roster.js +4 -2
  12. package/superd-hub/dist/carrier.d.ts +41 -28
  13. package/superd-hub/dist/carrier.js +148 -202
  14. package/superd-hub/dist/fusion-transport.d.ts +150 -0
  15. package/superd-hub/dist/fusion-transport.js +369 -0
  16. package/superd-hub/dist/gateway.d.ts +7 -2
  17. package/superd-hub/dist/gateway.js +28 -2
  18. package/superd-hub/dist/hub-route.d.ts +74 -0
  19. package/superd-hub/dist/hub-route.js +188 -0
  20. package/superd-hub/dist/index.d.ts +10 -4
  21. package/superd-hub/dist/index.js +10 -4
  22. package/superd-hub/dist/roster.d.ts +2 -2
  23. package/superd-hub/dist/spawn-world.js +5 -3
  24. package/superd-hub/dist/switch-core.d.ts +630 -0
  25. package/superd-hub/dist/switch-core.js +734 -0
  26. package/superd-hub/dist/web-trust.d.ts +5 -5
  27. package/superd-hub/dist/web-trust.js +5 -5
  28. package/superd-hub/dist/world-entry.d.ts +20 -13
  29. package/superd-hub/dist/world-entry.js +15 -19
  30. package/superd-hub/dist/world-host.d.ts +15 -20
  31. package/superd-hub/dist/world-host.js +25 -4
  32. package/superd-hub/dist/world-join.js +14 -29
  33. package/superd-hub/dist/world-mount.d.ts +48 -26
  34. package/superd-hub/dist/world-mount.js +59 -27
  35. package/superd-hub/dist/world-provision.js +11 -4
  36. package/superd-hub/dist/world-web-server.d.ts +57 -106
  37. package/superd-hub/dist/world-web-server.js +61 -216
  38. package/superd-hub/lib/client/index.js +2086 -0
  39. package/superd-hub/package.json +12 -0
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 host library (selector + roster +
8
- # per-world mount carrier + V5 gateway
9
- # delegation) — ships for the adapters' relative
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,26 @@
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. Composing this row IS the switch — there is no
37
+ # environment variable in the decision (2026-09-27 user ruling: a release
38
+ # gate living in an env var is deployment state no patch layer can see).
39
+ # ROW ORDER IS LOAD ORDER and the row MUST stay after superd-web-trust:
40
+ # the trust script's non-merging `if(!window.__DSH_TRANSPORT__)` verdict
41
+ # must be evaluated before the fusion script could own that global
42
+ # (spec §3.4, hard; precedent world-web-server.ts renderIndex).
43
+ # To turn the fused surface off: restate this row (same id, same name) with
44
+ # `config: {enabled: false}` or `disabled: true`, or drop it from the
45
+ # composition. Off = the component registers nothing: no script, no global.
46
+ - id: superd-fusion-transport
47
+ name: 'super-dsh/fusion-transport'
34
48
  - id: agent-omp
35
49
  name: 'super-dsh/agents/omp'
36
50
  - id: agent-codex
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "super-dsh",
3
- "version": "0.1.4",
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-a",
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, and the path that addresses it. */
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 RealWebServerFace } from './world-web-server.js';
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
- /** Options for mounting one world. */
16
- export interface MountWorldOptions {
17
- /** The ctx0 webServer the translated routes register on. */
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
- * Mount one world on the shared ctx0 webServer.
45
- * @param options - see {@link MountWorldOptions}.
46
- * @returns the live mount handle.
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
- * ctx0 WebCarrier (AW-B DL1/DL5): mounts one world's traffic under `/<label>`.
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
- * Responsibilities (grow per AW-B task):
5
- * - Task 4: unary RPC — `/<label>/api/<endpoint>` decodes the connection
6
- * envelope and dispatches into the ADDRESSED world's gateway instance.
7
- * - Task 6: mux — `/<label>/api/remote.mux` upgrade bridged to the same
8
- * instance (no shared selection anywhere). Wire protocol is dsh 0.1.7's
9
- * stream protocol incl. the client uplink (`item`/`end` frames).
10
- * - 2026-09-17 (H1): non-envelope GET/HEAD on `/<label>/api` forwards into
11
- * the owning world's registered exact Fetch routes — the world connection's
12
- * shared `/api` handler, published from `world-entry` via `world-host` —
13
- * still behind ctx0's fence, so `HEAD /<label>/api/session.export?...`
14
- * reaches the world's session-log-download route.
15
- * - 2026-09-24: `/<label>/open-in-app/*` GETs bridge the same way — the
16
- * world page's open-in-app feature polls its host routes relative to the
17
- * mount base (`/omp/open-in-app/apps`), which live on the WORLD's
18
- * connection, not ctx0's.
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
- * The addressed world is the request's path. Nothing here reads a global.
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
- /** Reject an upgrade on the raw socket (no ws ownership transfer). */
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
- * Mount one world on the shared ctx0 webServer.
83
- * @param options - see {@link MountWorldOptions}.
84
- * @returns the live mount handle.
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 mountWorld(options) {
87
- const labelPath = `/${options.label}`;
88
- const apiPath = `${labelPath}/api`;
89
- const muxPath = `${apiPath}/remote.mux`;
90
- const maxBody = options.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES;
91
- const disposers = [];
92
- const forwardsFetchMethod = (req) => {
93
- const method = req.method;
94
- return method === 'GET' || method === 'HEAD';
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
- * Forward one non-envelope GET/HEAD request into the owning world's shared
98
- * `/api` fetch handler (exact Fetch routes first, then the gateway's RPC
99
- * interceptor, then 404 — the upstream composition). The ctx0 fence has
100
- * already answered by the time we get here; the world's own browser-auth
101
- * gate is never consulted (a different tree, a different secret).
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
- const method = (req.method ?? 'GET');
110
- const resCarrier = res;
111
- // Client-disconnect detection rides the response (same reason as the
112
- // upstream node:http bridge: the request 'close' fires as soon as a
113
- // bodyless GET is consumed).
114
- const abort = new AbortController();
115
- const onClose = () => {
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
- let rpcId = 'invalid-request';
172
- try {
173
- const text = await readBody(req, maxBody);
174
- let envelope;
175
- try {
176
- envelope = parseClientRequest(text);
177
- }
178
- catch (error) {
179
- // Non-envelope GET/HEAD traffic on the shared mount prefix belongs to
180
- // the world's own registered fetch routes; envelopes (and every other
181
- // method) keep the transport-level 400 below.
182
- if (error instanceof EnvelopeError && forwardsFetchMethod(req)) {
183
- await bridgeWorldFetch(req, res);
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
- rpcId = envelope.rpcId;
189
- const controller = new AbortController();
190
- const result = await options.gateway.dispatchRpc(envelope.method, envelope.payload, controller.signal);
191
- // The gateway already returns a Result (`{ok:true,value}` | `{ok:false,error}`),
192
- // which is exactly the server-response's `result` field.
193
- writeJson(res, 200, serverResult(rpcId, result));
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
- const wire = wireErrorOf(error);
197
- // Envelope-level failures are transport-level 400s; dispatch failures are
198
- // business results carried inside a 200 server-response (upstream shape).
199
- if (error instanceof EnvelopeError) {
200
- writeJson(res, 400, serverError(rpcId, wire));
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
- writeJson(res, 200, serverError(rpcId, wire));
176
+ throw error;
204
177
  }
205
- };
206
- disposers.push(options.real.register({
207
- kind: 'prefix',
208
- path: apiPath,
209
- handler: handleUnary,
210
- }));
211
- // The world page's open-in-app feature polls its host routes relative to the
212
- // mount base (`/omp/open-in-app/apps`); those exact routes live on the
213
- // WORLD's connection, so they bridge exactly like the non-envelope `/api`
214
- // GETs — behind ctx0's fence, answered by the owning world.
215
- const openInAppPath = `${labelPath}/open-in-app`;
216
- disposers.push(options.real.register({
217
- kind: 'prefix',
218
- path: openInAppPath,
219
- handler: (async (req, res) => {
220
- const rejection = options.requestRejection?.(req);
221
- if (rejection !== undefined) {
222
- writeText(res, rejection, rejection === 401 ? 'unauthorized' : 'forbidden');
223
- return;
224
- }
225
- await bridgeWorldFetch(req, res);
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
- // The mount HTML pass lives in its own module; re-exported here so the carrier
266
- // remains the single import surface for mount behaviour.
267
- export { rewriteIndexHtml } from './index-pass.js';
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.