@teamscala/service-boot 0.23.350 → 0.23.352
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/package.json +479 -567
- package/src/agreement-gate.ts +1 -1
- package/src/auth-gate.ts +0 -11
- package/src/fn-routers/index.ts +1 -12
- package/src/fn-routers/public-agreement-router.ts +37 -49
- package/src/mount-orpc-router.test.ts +46 -0
- package/src/mount-orpc-router.ts +16 -3
- package/src/mutable-app-ref.ts +0 -20
- package/src/page-server-orpc/puck-pages-router.ts +0 -102
- package/src/register-page-server-client.ts +0 -4
- package/src/register-ws-routes.ts +10 -3
- package/src/two-phase-boot.ts +2 -1
- package/src/ui-service.ts +11 -18
- package/src/fn-routers/ai-health-router.ts +0 -64
- package/src/fn-routers/panel-data-router.ts +0 -93
- package/src/fn-routers/plans-router.ts +0 -141
- package/src/fn-routers/task-sync-assign.test.ts +0 -483
- package/src/fn-routers/task-sync-assign.ts +0 -485
- package/src/fn-routers/task-sync-bind.test.ts +0 -110
- package/src/fn-routers/task-sync-brief.test.ts +0 -120
- package/src/fn-routers/task-sync-brief.ts +0 -151
- package/src/fn-routers/task-sync-classify.test.ts +0 -109
- package/src/fn-routers/task-sync-close.test.ts +0 -44
- package/src/fn-routers/task-sync-close.ts +0 -82
- package/src/fn-routers/task-sync-created-status.test.ts +0 -58
- package/src/fn-routers/task-sync-dispatch.test.ts +0 -62
- package/src/fn-routers/task-sync-hook-lifecycle.test.ts +0 -204
- package/src/fn-routers/task-sync-lane.test.ts +0 -36
- package/src/fn-routers/task-sync-projection.test.ts +0 -172
- package/src/fn-routers/task-sync-projection.ts +0 -273
- package/src/fn-routers/task-sync-rename.test.ts +0 -53
- package/src/fn-routers/task-sync-rename.ts +0 -111
- package/src/fn-routers/tasks-router.ts +0 -77
- package/src/page-server-orpc/portal-diagnostics-router.ts +0 -170
- package/src/page-server-orpc/templates-router.ts +0 -116
- package/src/serve/serve-ws.test.ts +0 -135
- package/src/serve/serve.ts +0 -148
- package/src/serve/stamp-transport-identity.test.ts +0 -24
- package/src/serve/stamp-transport-identity.ts +0 -109
package/src/serve/serve.ts
DELETED
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @system runtime-serve
|
|
3
|
-
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* Listener primitives. Provides the Bun.serve production listener (serveStart)
|
|
7
|
-
* and the WebSocket upgrade helper (upgradeWebSocket) that work identically
|
|
8
|
-
* dev and prod.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import type { Hono } from "hono";
|
|
12
|
-
import { createBunWebSocket } from "hono/bun";
|
|
13
|
-
import { stampTransportIdentity } from "./stamp-transport-identity.ts";
|
|
14
|
-
|
|
15
|
-
const _bunWs = createBunWebSocket();
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Hono `upgradeWebSocket` helper bound to the same `createBunWebSocket()`
|
|
19
|
-
* pair that drives Bun.serve.websocket. Use exclusively from this package so
|
|
20
|
-
* the upgrade path and the message dispatch share state.
|
|
21
|
-
*/
|
|
22
|
-
export const upgradeWebSocket = _bunWs.upgradeWebSocket;
|
|
23
|
-
|
|
24
|
-
export interface ServeOptions {
|
|
25
|
-
/** TCP port to bind. */
|
|
26
|
-
port: number;
|
|
27
|
-
/** For logging only. */
|
|
28
|
-
serviceSlug: string;
|
|
29
|
-
/**
|
|
30
|
-
* Optional logger; defaults to console.
|
|
31
|
-
*/
|
|
32
|
-
logger?: { info(msg: string): void; warn(msg: string): void };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export interface ServeHandle {
|
|
36
|
-
server: ReturnType<typeof Bun.serve>;
|
|
37
|
-
stop(): void;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Create the fetch handler with transport-identity stamping. Used in direct
|
|
42
|
-
* mode (mutable-ref mode uses fetchWithMutableApp, which also stamps via the
|
|
43
|
-
* same stampTransportIdentity helper — one stamp point for every request).
|
|
44
|
-
*/
|
|
45
|
-
function createFetchHandler(app: Hono) {
|
|
46
|
-
return async (
|
|
47
|
-
req: Request,
|
|
48
|
-
server: ReturnType<typeof Bun.serve>,
|
|
49
|
-
) => {
|
|
50
|
-
// app.fetch's second arg becomes c.env. Hono's bun WebSocket upgrade
|
|
51
|
-
// adapter checks `"server" in c.env` for the Bun server reference (WS
|
|
52
|
-
// upgrade throws TypeError without it). stampTransportIdentity handles
|
|
53
|
-
// the WS-upgrade no-clone rule + best-effort identity stamping internally.
|
|
54
|
-
return app.fetch(stampTransportIdentity(req, server), { server });
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Internal serve setup that's shared between both modes.
|
|
60
|
-
*/
|
|
61
|
-
function serveStartInternal(
|
|
62
|
-
fetchHandler: (req: Request, server: ReturnType<typeof Bun.serve>) => Promise<Response>,
|
|
63
|
-
options: ServeOptions,
|
|
64
|
-
logger: { info(msg: string): void; warn(msg: string): void },
|
|
65
|
-
mode: "direct" | "mutable-ref",
|
|
66
|
-
): ServeHandle {
|
|
67
|
-
const server = Bun.serve({
|
|
68
|
-
port: options.port,
|
|
69
|
-
// reusePort is deliberately NOT set. SO_REUSEPORT lets a second process
|
|
70
|
-
// bind a port this service already owns; the kernel then load-balances
|
|
71
|
-
// between them and BOTH look healthy while each receives a fraction of
|
|
72
|
-
// the traffic. That is the worst available failure mode — it presents as
|
|
73
|
-
// intermittent, unattributable errors in whichever service is unlucky,
|
|
74
|
-
// far from the process that actually took the port.
|
|
75
|
-
//
|
|
76
|
-
// It has now cost two incidents. mcp-ai-chat-system bound session-picker's
|
|
77
|
-
// 3021 and broke tool curation; scala-quiz did the same on 2026-07-31 and
|
|
78
|
-
// answered ~half of every session-daemon RPC with a 404. Neither failed at
|
|
79
|
-
// bind, because reusePort made a collision legal.
|
|
80
|
-
//
|
|
81
|
-
// Nothing here needs it: PM2 fork mode stops before it starts, dev runs one
|
|
82
|
-
// process per service (`readiness-gated-boot` — the boot window is handled
|
|
83
|
-
// by the in-process readiness gate, explicitly not blue/green), and each
|
|
84
|
-
// Railway container is alone. So the only thing reusePort can express on
|
|
85
|
-
// this platform is a mistake, and without it that mistake is an immediate,
|
|
86
|
-
// legible EADDRINUSE naming the port.
|
|
87
|
-
// Bun's default idleTimeout (~10s, swept on a coarse timer wheel)
|
|
88
|
-
// FIN-closes every pending connection in one ~2ms sweep once their
|
|
89
|
-
// handlers exceed it — packet-captured 2026-06-06: concurrent SSR
|
|
90
|
-
// requests all died at the same instant at 8.2-10.5s with clean
|
|
91
|
-
// server-side FINs while the renders completed and logged 200. THIS
|
|
92
|
-
// file is the serve path for kind:ui services (service-runtime's
|
|
93
|
-
// twin got the option first and the deploy changed nothing — the
|
|
94
|
-
// wrong twin). 120s bounds wedged connections without guillotining
|
|
95
|
-
// slow SSR.
|
|
96
|
-
idleTimeout: 120,
|
|
97
|
-
fetch: fetchHandler as never,
|
|
98
|
-
websocket: _bunWs.websocket,
|
|
99
|
-
});
|
|
100
|
-
// Export the actual listening port to process.env so loopback code reads the
|
|
101
|
-
// REAL port without a separate PORT injection. The central-tool-forwarder
|
|
102
|
-
// (chat tool dispatch) falls back to http://localhost:${PORT} for the
|
|
103
|
-
// co-located /mcp; without this export every chat tool call fails with
|
|
104
|
-
// "cannot resolve the /mcp base URL" (the highlight-ring end-to-end blocker,
|
|
105
|
-
// 2026-07-02). Respect Railway's $PORT when present.
|
|
106
|
-
if (!process.env.PORT) process.env.PORT = String(server.port);
|
|
107
|
-
logger.info(
|
|
108
|
-
`[runtime-serve:${options.serviceSlug}] Bun.serve listening on :${server.port} (${mode})`,
|
|
109
|
-
);
|
|
110
|
-
return {
|
|
111
|
-
server,
|
|
112
|
-
stop() {
|
|
113
|
-
server.stop(true);
|
|
114
|
-
},
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Start Bun.serve listening on the configured port. Used by:
|
|
120
|
-
* - bootUiService when running the production bundle (`bun dist/server/index.js`)
|
|
121
|
-
* - bootBackendService for non-UI services
|
|
122
|
-
*
|
|
123
|
-
* UI services in dev mode also call this — they run `bun src/index.ts`
|
|
124
|
-
* directly under PM2 with the same Bun.serve listener.
|
|
125
|
-
*
|
|
126
|
-
* Two-phase boot variant: when useMutableRef is true, `app` is used as the
|
|
127
|
-
* initial app for the mutable reference (minimal tool-only surface), and the
|
|
128
|
-
* caller can swap to the full app later via swapApp().
|
|
129
|
-
*/
|
|
130
|
-
export function serveStart(
|
|
131
|
-
app: Hono,
|
|
132
|
-
options: ServeOptions,
|
|
133
|
-
useMutableRef = false,
|
|
134
|
-
): ServeHandle {
|
|
135
|
-
const log = options.logger ?? console;
|
|
136
|
-
|
|
137
|
-
// For two-phase boot: initialize the mutable app reference with the
|
|
138
|
-
// minimal tool-only app. The caller will swap to the full app later.
|
|
139
|
-
if (useMutableRef) {
|
|
140
|
-
const { initMutableAppRef, fetchWithMutableApp } = require("../mutable-app-ref");
|
|
141
|
-
initMutableAppRef(app, "minimal");
|
|
142
|
-
return serveStartInternal(fetchWithMutableApp, options, log, "mutable-ref");
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Direct mode: use the app directly
|
|
146
|
-
const fetchHandler = createFetchHandler(app);
|
|
147
|
-
return serveStartInternal(fetchHandler, options, log, "direct");
|
|
148
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { summariseLocalCommand } from "./stamp-transport-identity.ts";
|
|
3
|
-
|
|
4
|
-
describe("local command attribution", () => {
|
|
5
|
-
test("never copies command arguments into telemetry", () => {
|
|
6
|
-
const token = "operator-token-must-not-appear";
|
|
7
|
-
const summary = summariseLocalCommand([
|
|
8
|
-
"curl",
|
|
9
|
-
"-H",
|
|
10
|
-
`x-operator-spawn-token: ${token}`,
|
|
11
|
-
"http://127.0.0.1:3021/rpc/spawn",
|
|
12
|
-
]);
|
|
13
|
-
expect(summary).toBe("curl [arguments redacted]");
|
|
14
|
-
expect(summary).not.toContain(token);
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
test("keeps a lone executable name", () => {
|
|
18
|
-
expect(summariseLocalCommand(["scala-picker"])).toBe("scala-picker");
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
test("omits an empty command line", () => {
|
|
22
|
-
expect(summariseLocalCommand([])).toBeNull();
|
|
23
|
-
});
|
|
24
|
-
});
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @system service-boot
|
|
3
|
-
* @status handwritten
|
|
4
|
-
* @edit edit directly
|
|
5
|
-
*
|
|
6
|
-
* Stamp transport-derived identity onto a request — the single shared stamp
|
|
7
|
-
* point used by BOTH serve paths (direct `createFetchHandler` + mutable-ref
|
|
8
|
-
* `fetchWithMutableApp`), so every request on every UI service carries the
|
|
9
|
-
* same identity regardless of boot mode.
|
|
10
|
-
*
|
|
11
|
-
* Two identity layers, both transport-derived (no-client-controlled-trust-
|
|
12
|
-
* signals — never from a client-controllable header):
|
|
13
|
-
* 1. `x-caller-remote-address` / `-port` — the unforgeable socket peer, from
|
|
14
|
-
* `Bun.serve.requestIP`. ALWAYS sanitised (any client-supplied spoof
|
|
15
|
-
* deleted) then set to the verified peer. The /mcp + fleet loopback gates
|
|
16
|
-
* read this.
|
|
17
|
-
* 2. `x-caller-local-comm` / `-cmd` — for a LOOPBACK peer with no forwarding
|
|
18
|
-
* header (a local `fetch` carries no CF-Connecting-IP), the owning local
|
|
19
|
-
* PROCESS resolved via /proc (@teamscala/proc-walker). Names the daemon
|
|
20
|
-
* behind every local request on the access log — including an unidentified
|
|
21
|
-
* sweeper. Attribution only (trust stays transport-derived).
|
|
22
|
-
*
|
|
23
|
-
* Best-effort + never throws: a resolution miss returns the original request
|
|
24
|
-
* unchanged (the request is always served; identity degrades to address-only).
|
|
25
|
-
* WebSocket upgrades are returned untouched (Bun tracks upgrades by Request
|
|
26
|
-
* identity — a clone can never upgrade).
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
|
-
import { CALLER_HEADERS } from "@teamscala/os/contracts/mcp";
|
|
30
|
-
import { resolveSocketPeer } from "@teamscala/proc-walker/socket-peer";
|
|
31
|
-
|
|
32
|
-
const LOOPBACK = new Set(["127.0.0.1", "::1", "::ffff:127.0.0.1"]);
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Preserve useful process attribution without copying arbitrary argv into logs.
|
|
36
|
-
* Arguments routinely contain bearer headers, inline scripts, URLs and secret
|
|
37
|
-
* values. There is no complete flag-name redaction list, so the safe boundary
|
|
38
|
-
* is structural: retain argv[0] and state that arguments were removed.
|
|
39
|
-
*/
|
|
40
|
-
export function summariseLocalCommand(cmdline: readonly string[]): string | null {
|
|
41
|
-
const executable = cmdline[0]?.trim();
|
|
42
|
-
if (!executable) return null;
|
|
43
|
-
return cmdline.length > 1 ? `${executable} [arguments redacted]` : executable;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Return a Request with transport-identity headers stamped, or the original
|
|
48
|
-
* `req` when no stamping is needed/possible. Never throws.
|
|
49
|
-
*/
|
|
50
|
-
export function stampTransportIdentity(
|
|
51
|
-
req: Request,
|
|
52
|
-
server: ReturnType<typeof Bun.serve>,
|
|
53
|
-
): Request {
|
|
54
|
-
// WebSocket upgrade requests MUST NOT be cloned — Bun's server.upgrade()
|
|
55
|
-
// tracks pending upgrades by Request identity, so a cloned Request can
|
|
56
|
-
// never upgrade.
|
|
57
|
-
if (req.headers.get("upgrade")?.toLowerCase() === "websocket") {
|
|
58
|
-
return req;
|
|
59
|
-
}
|
|
60
|
-
try {
|
|
61
|
-
const peer = (
|
|
62
|
-
server as unknown as {
|
|
63
|
-
requestIP(r: Request): { port?: number; address?: string } | null;
|
|
64
|
-
}
|
|
65
|
-
).requestIP?.(req);
|
|
66
|
-
const port = peer?.port;
|
|
67
|
-
const address = peer?.address ?? null;
|
|
68
|
-
const needsPort =
|
|
69
|
-
typeof port === "number" && port > 0 && !req.headers.has(CALLER_HEADERS.REMOTE_PORT);
|
|
70
|
-
// Nothing to stamp when there is no peer address and no port to add.
|
|
71
|
-
if (!needsPort && address === null) return req;
|
|
72
|
-
|
|
73
|
-
const headers = new Headers(req.headers);
|
|
74
|
-
if (needsPort) headers.set(CALLER_HEADERS.REMOTE_PORT, String(port));
|
|
75
|
-
if (address) {
|
|
76
|
-
headers.delete(CALLER_HEADERS.REMOTE_ADDRESS);
|
|
77
|
-
headers.set(CALLER_HEADERS.REMOTE_ADDRESS, String(address));
|
|
78
|
-
}
|
|
79
|
-
// Local-caller attribution: loopback peer + no forwarding header => a
|
|
80
|
-
// direct local fetch whose owning process we can name via /proc.
|
|
81
|
-
if (
|
|
82
|
-
address &&
|
|
83
|
-
LOOPBACK.has(address) &&
|
|
84
|
-
!headers.has("cf-connecting-ip") &&
|
|
85
|
-
!headers.get("x-forwarded-for") &&
|
|
86
|
-
typeof port === "number" &&
|
|
87
|
-
port > 0
|
|
88
|
-
) {
|
|
89
|
-
const owner = resolveSocketPeer(port);
|
|
90
|
-
if (owner) {
|
|
91
|
-
headers.set(CALLER_HEADERS.LOCAL_COMM, owner.comm);
|
|
92
|
-
const cmd = summariseLocalCommand(owner.cmdline);
|
|
93
|
-
if (cmd) headers.set(CALLER_HEADERS.LOCAL_CMD, cmd);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
return new Request(req.url, {
|
|
97
|
-
method: req.method,
|
|
98
|
-
headers,
|
|
99
|
-
body: req.body,
|
|
100
|
-
signal: req.signal,
|
|
101
|
-
// @ts-expect-error duplex is required by spec when body is a stream
|
|
102
|
-
duplex: "half",
|
|
103
|
-
});
|
|
104
|
-
} catch {
|
|
105
|
-
// A broken peer resolution must never block serving — degrade to the
|
|
106
|
-
// original request (identity is best-effort, the request always served).
|
|
107
|
-
return req;
|
|
108
|
-
}
|
|
109
|
-
}
|