Skip to content

Commit d77a095

Browse files
authored
fix(session): disable generic broker API by default (#332)
1 parent 0dd4abe commit d77a095

11 files changed

Lines changed: 120 additions & 34 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ All notable user-visible changes to Hunk are documented in this file.
1111
### Fixed
1212

1313
- Hardened the local session daemon against browser-originated requests by validating Host and Origin headers and requiring JSON content types for API posts.
14+
- Disabled the generic broker HTTP API by default so Hunk's supported session API is the only app-daemon command surface.
1415

1516
## [0.13.0] - 2026-05-18
1617

packages/session-broker-bun/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Use this package when you want to serve a runtime-neutral `SessionBrokerDaemon`
1010
- upgrades websocket requests on the daemon socket path
1111
- forwards websocket messages and close events into the daemon
1212
- exposes a `stopped` promise compatible with Hunk's daemon lifecycle
13-
- lets callers override or add custom HTTP routes before the generic broker routes
13+
- lets callers override or add custom HTTP routes before the daemon's built-in routes
1414

1515
## Usage
1616

@@ -55,7 +55,7 @@ const server = serveSessionBrokerDaemon({
5555
});
5656
```
5757

58-
Return `undefined` to fall through to the generic broker routes.
58+
Return `undefined` to fall through to the daemon's built-in routes. The raw `/broker` HTTP API is available only when the daemon was created with `exposeHttpApi: true`.
5959

6060
## License
6161

packages/session-broker-bun/src/serve.test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,11 @@ describe("session broker bun adapter", () => {
139139
parseRegistration: (value) => parseSessionRegistrationEnvelope(value, parseInfo),
140140
parseSnapshot: (value) => parseSessionSnapshotEnvelope(value, parseState),
141141
});
142-
const daemon = createSessionBrokerDaemon({ broker, capabilities: { version: 1 } });
142+
const daemon = createSessionBrokerDaemon({
143+
broker,
144+
capabilities: { version: 1 },
145+
exposeHttpApi: true,
146+
});
143147
const port = await reserveLoopbackPort();
144148
const server = serveSessionBrokerDaemon({
145149
daemon,

packages/session-broker-node/src/serve.test.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,11 @@ describe("session broker node adapter", () => {
106106
parseRegistration: (value) => parseSessionRegistrationEnvelope(value, parseInfo),
107107
parseSnapshot: (value) => parseSessionSnapshotEnvelope(value, parseState),
108108
});
109-
const daemon = createSessionBrokerDaemon({ broker, capabilities: { version: 1 } });
109+
const daemon = createSessionBrokerDaemon({
110+
broker,
111+
capabilities: { version: 1 },
112+
exposeHttpApi: true,
113+
});
110114
const port = await reserveLoopbackPort();
111115
const server = await serveSessionBrokerDaemon({
112116
daemon,

packages/session-broker/README.md

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ Use this package when you want to:
99
- track live sessions
1010
- register and update session snapshots
1111
- route commands to one live session
12-
- expose broker health and raw list/get/dispatch APIs
12+
- expose broker health and optional raw list/get/dispatch APIs
1313
- manage session-side websocket connection state
1414

1515
## Package roles
@@ -29,7 +29,7 @@ If you are choosing one package to build against, start here.
2929
- `SessionBrokerDaemon` runtime-neutral daemon engine
3030
- `SessionBrokerConnection` runtime-neutral session-side websocket helper
3131
- raw broker HTTP request types
32-
- health and capabilities handling
32+
- health handling and optional capabilities API handling
3333
- stale-session pruning and idle shutdown
3434

3535
## What this package does not own
@@ -103,11 +103,22 @@ const daemon = createSessionBrokerDaemon({
103103
At this point the daemon can:
104104

105105
- handle health requests
106-
- handle capabilities requests
107-
- handle raw `list` / `get` / `dispatch` broker API requests
108106
- process websocket register/snapshot/heartbeat/result messages
109107
- prune stale sessions and request idle shutdown
110108

109+
The raw HTTP broker API is opt-in. Enable it only when your host application wants to expose the generic `list` / `get` / `dispatch` command surface:
110+
111+
```ts
112+
const daemon = createSessionBrokerDaemon({
113+
broker,
114+
capabilities: {
115+
version: 1,
116+
name: "example-broker",
117+
},
118+
exposeHttpApi: true,
119+
});
120+
```
121+
111122
### 3. Serve it through a runtime adapter
112123

113124
#### Bun
@@ -167,7 +178,7 @@ The helper owns:
167178

168179
## Raw broker API
169180

170-
The daemon's runtime-neutral HTTP API is intentionally small:
181+
The daemon's runtime-neutral HTTP API is intentionally small and disabled by default. When `exposeHttpApi: true` is set, it serves:
171182

172183
- `GET /health`
173184
- `GET /broker/capabilities`

packages/session-broker/src/daemon.test.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,10 +98,11 @@ function createConnection() {
9898
}
9999

100100
describe("session broker daemon", () => {
101-
test("serves health and raw list/get requests", async () => {
101+
test("serves health and raw list/get requests when the HTTP API is enabled", async () => {
102102
const daemon = createSessionBrokerDaemon({
103103
broker: createBroker(),
104104
capabilities: { version: 1, name: "test-broker" },
105+
exposeHttpApi: true,
105106
});
106107
const { connection } = createConnection();
107108
daemon.handleConnectionMessage(
@@ -149,10 +150,38 @@ describe("session broker daemon", () => {
149150
daemon.shutdown();
150151
});
151152

153+
test("does not expose the raw broker HTTP API by default", async () => {
154+
const daemon = createSessionBrokerDaemon({
155+
broker: createBroker(),
156+
capabilities: { version: 1 },
157+
});
158+
159+
await expect(
160+
daemon.handleRequest(new Request("http://broker.test/broker/capabilities")),
161+
).resolves.toBeNull();
162+
163+
await expect(
164+
daemon.handleRequest(
165+
new Request("http://broker.test/broker", {
166+
method: "POST",
167+
headers: { "content-type": "application/json" },
168+
body: JSON.stringify({ action: "list" }),
169+
}),
170+
),
171+
).resolves.toBeNull();
172+
173+
await expect(
174+
daemon.handleRequest(new Request("http://broker.test/health")),
175+
).resolves.toBeInstanceOf(Response);
176+
expect(daemon.paths).toEqual({ health: "/health", socket: "/session" });
177+
daemon.shutdown();
178+
});
179+
152180
test("requires JSON content type for raw broker API posts", async () => {
153181
const daemon = createSessionBrokerDaemon({
154182
broker: createBroker(),
155183
capabilities: { version: 1 },
184+
exposeHttpApi: true,
156185
});
157186

158187
const response = await daemon.handleRequest(
@@ -174,6 +203,7 @@ describe("session broker daemon", () => {
174203
const daemon = createSessionBrokerDaemon({
175204
broker: createBroker(),
176205
capabilities: { version: 1 },
206+
exposeHttpApi: true,
177207
});
178208
const session = createConnection();
179209
const { connection, sent } = session;

packages/session-broker/src/daemon.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export interface SessionBrokerDaemonOptions<
2525
broker: SessionBrokerController<SessionView, ServerMessage, CommandResult>;
2626
capabilities?: SessionBrokerCapabilities;
2727
paths?: Partial<SessionBrokerHttpPaths>;
28+
exposeHttpApi?: boolean;
2829
idleTimeoutMs?: number;
2930
staleSessionTtlMs?: number;
3031
staleSessionSweepIntervalMs?: number;
@@ -105,11 +106,14 @@ export class SessionBrokerDaemon<
105106
"broker"
106107
> = {},
107108
) {
109+
const exposeHttpApi = options.exposeHttpApi ?? false;
108110
this.paths = {
109111
health: options.paths?.health ?? DEFAULT_SESSION_BROKER_HEALTH_PATH,
110-
api: options.paths?.api ?? DEFAULT_SESSION_BROKER_API_PATH,
111-
capabilities: options.paths?.capabilities ?? DEFAULT_SESSION_BROKER_CAPABILITIES_PATH,
112112
socket: options.paths?.socket ?? DEFAULT_SESSION_BROKER_SOCKET_PATH,
113+
api: exposeHttpApi ? (options.paths?.api ?? DEFAULT_SESSION_BROKER_API_PATH) : undefined,
114+
capabilities: exposeHttpApi
115+
? (options.paths?.capabilities ?? DEFAULT_SESSION_BROKER_CAPABILITIES_PATH)
116+
: undefined,
113117
};
114118
this.capabilities = options.capabilities ?? { version: 1 };
115119
this.idleTimeoutMs = options.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;
@@ -162,12 +166,12 @@ export class SessionBrokerDaemon<
162166
return Response.json(this.getHealth());
163167
}
164168

165-
if (url.pathname === this.paths.capabilities) {
169+
if (this.paths.capabilities && url.pathname === this.paths.capabilities) {
166170
this.noteActivity();
167171
return Response.json(this.capabilities);
168172
}
169173

170-
if (url.pathname === this.paths.api) {
174+
if (this.paths.api && url.pathname === this.paths.api) {
171175
this.noteActivity();
172176
return this.handleApiRequest(request);
173177
}

packages/session-broker/src/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ export interface SessionBrokerCapabilities {
1515

1616
export interface SessionBrokerHttpPaths {
1717
health: string;
18-
api: string;
19-
capabilities: string;
2018
socket: string;
19+
api?: string;
20+
capabilities?: string;
2121
}
2222

2323
export type SessionBrokerDaemonRequest<

src/hunk-session/cli.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,7 @@ describe("Hunk session CLI formatters", () => {
388388
"Selected hunk: -",
389389
"Agent notes visible: no",
390390
"Live comments: 1",
391+
"Review notes: 0",
391392
"Files:",
392393
" - src/first.ts (+2 -1, hunks: 2)",
393394
" hunk 1: @@ -1,1 +1,2 @@",

src/session-broker/brokerServer.test.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ interface HealthResponse {
1818
pid: number;
1919
sessions: number;
2020
pendingCommands: number;
21+
paths?: Record<string, string>;
22+
sessionApi?: string;
23+
sessionCapabilities?: string;
24+
sessionSocket?: string;
2125
}
2226

2327
async function reserveLoopbackPort() {
@@ -250,14 +254,39 @@ describe("Hunk session daemon server", () => {
250254
}
251255
});
252256

253-
test("exposes session capabilities and rejects the old MCP tool endpoint", async () => {
257+
test("exposes only Hunk session endpoints and rejects the old MCP tool endpoint", async () => {
254258
const port = await reserveLoopbackPort();
255259
process.env.HUNK_MCP_HOST = "127.0.0.1";
256260
process.env.HUNK_MCP_PORT = String(port);
257261

258262
const server = serveSessionBrokerDaemon();
259263

260264
try {
265+
const health = await fetch(`http://127.0.0.1:${port}/health`);
266+
expect(health.status).toBe(200);
267+
const healthPayload = (await health.json()) as HealthResponse;
268+
expect(healthPayload.paths).toEqual({
269+
health: "/health",
270+
socket: "/session",
271+
});
272+
expect(healthPayload).toMatchObject({
273+
sessionApi: `http://127.0.0.1:${port}/session-api`,
274+
sessionCapabilities: `http://127.0.0.1:${port}/session-api/capabilities`,
275+
sessionSocket: `ws://127.0.0.1:${port}/session`,
276+
});
277+
278+
const genericCapabilities = await fetch(`http://127.0.0.1:${port}/broker/capabilities`);
279+
expect(genericCapabilities.status).toBe(404);
280+
281+
const genericBroker = await fetch(`http://127.0.0.1:${port}/broker`, {
282+
method: "POST",
283+
headers: {
284+
"content-type": "application/json",
285+
},
286+
body: JSON.stringify({ action: "list" }),
287+
});
288+
expect(genericBroker.status).toBe(404);
289+
261290
const capabilities = await fetch(`http://127.0.0.1:${port}/session-api/capabilities`);
262291
expect(capabilities.status).toBe(200);
263292
await expect(capabilities.json()).resolves.toMatchObject({

0 commit comments

Comments
 (0)