agent-comms 8.2.1 → 8.3.0

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 (47) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +15 -1
  4. package/dist/bridges/user/web/dist/bundle.js +1 -1
  5. package/dist/bridges/user/web/dist/sw.js +1 -1
  6. package/dist/bridges/user/web/frontend/boot-logic.d.ts +9 -8
  7. package/dist/bridges/user/web/frontend/boot-logic.d.ts.map +1 -1
  8. package/dist/bridges/user/web/frontend/boot-logic.js +9 -12
  9. package/dist/bridges/user/web/frontend/boot-logic.js.map +1 -1
  10. package/dist/bridges/user/web/frontend/main.js +3 -6
  11. package/dist/bridges/user/web/frontend/main.js.map +1 -1
  12. package/dist/bridges/user/web/frontend/mesh-client.d.ts.map +1 -1
  13. package/dist/bridges/user/web/frontend/mesh-client.js +3 -4
  14. package/dist/bridges/user/web/frontend/mesh-client.js.map +1 -1
  15. package/dist/bridges/user/web/frontend/served-by-bridge.d.ts +10 -0
  16. package/dist/bridges/user/web/frontend/served-by-bridge.d.ts.map +1 -0
  17. package/dist/bridges/user/web/frontend/served-by-bridge.js +10 -0
  18. package/dist/bridges/user/web/frontend/served-by-bridge.js.map +1 -0
  19. package/dist/bridges/user/web/server.d.ts +10 -0
  20. package/dist/bridges/user/web/server.d.ts.map +1 -1
  21. package/dist/bridges/user/web/server.js +84 -7
  22. package/dist/bridges/user/web/server.js.map +1 -1
  23. package/dist/bridges/user/web/test/lan-web-server.integration.test.d.ts +7 -0
  24. package/dist/bridges/user/web/test/lan-web-server.integration.test.d.ts.map +1 -0
  25. package/dist/bridges/user/web/test/lan-web-server.integration.test.js +245 -0
  26. package/dist/bridges/user/web/test/lan-web-server.integration.test.js.map +1 -0
  27. package/dist/bridges/user/web/test/web-access.unit.test.d.ts +5 -0
  28. package/dist/bridges/user/web/test/web-access.unit.test.d.ts.map +1 -0
  29. package/dist/bridges/user/web/test/web-access.unit.test.js +271 -0
  30. package/dist/bridges/user/web/test/web-access.unit.test.js.map +1 -0
  31. package/dist/bridges/user/web/test/web-beacon.unit.test.d.ts +5 -0
  32. package/dist/bridges/user/web/test/web-beacon.unit.test.d.ts.map +1 -0
  33. package/dist/bridges/user/web/test/web-beacon.unit.test.js +40 -0
  34. package/dist/bridges/user/web/test/web-beacon.unit.test.js.map +1 -0
  35. package/dist/bridges/user/web/web-access.d.ts +79 -0
  36. package/dist/bridges/user/web/web-access.d.ts.map +1 -0
  37. package/dist/bridges/user/web/web-access.js +262 -0
  38. package/dist/bridges/user/web/web-access.js.map +1 -0
  39. package/dist/bridges/user/web/web-beacon.d.ts +27 -0
  40. package/dist/bridges/user/web/web-beacon.d.ts.map +1 -0
  41. package/dist/bridges/user/web/web-beacon.js +45 -0
  42. package/dist/bridges/user/web/web-beacon.js.map +1 -0
  43. package/dist/core/first-contact.d.ts +2 -0
  44. package/dist/core/first-contact.d.ts.map +1 -1
  45. package/dist/core/first-contact.js +1 -1
  46. package/dist/core/first-contact.js.map +1 -1
  47. package/package.json +1 -1
@@ -0,0 +1,245 @@
1
+ /**
2
+ * The web server's LAN exposure end to end (agent-comms#346): loopback behaviour is unchanged by default, and a bind beyond loopback admits a non-loopback client only with the per-process token.
3
+ *
4
+ * A request to this machine's own LAN address arrives from that address, not from loopback, so it exercises the non-loopback path over a real socket. A machine with no external IPv4 address skips those cases; web-access.unit.test.ts covers the same gate without a network.
5
+ */
6
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
7
+ import * as http from "node:http";
8
+ import * as os from "node:os";
9
+ import WS from "ws";
10
+ import { freeLocalPort, TeardownStack, unreachableHubUrl, } from "../../../../test/hub-helpers.js";
11
+ import { createWebServer, getWebUrlStatus, } from "../server.js";
12
+ import { accessCookieName, ownInterfaceAddresses, WEB_HOST_ENV, } from "../web-access.js";
13
+ const HTTP_OK = 200;
14
+ const HTTP_FOUND = 302;
15
+ const HTTP_UNAUTHORIZED = 401;
16
+ const HTTP_FORBIDDEN = 403;
17
+ const cleanups = new TeardownStack();
18
+ beforeEach(() => {
19
+ vi.stubEnv(WEB_HOST_ENV, "");
20
+ });
21
+ afterEach(async () => {
22
+ vi.unstubAllEnvs();
23
+ vi.restoreAllMocks();
24
+ await cleanups.run();
25
+ });
26
+ function lanAddress() {
27
+ for (const addresses of Object.values(os.networkInterfaces())) {
28
+ for (const entry of addresses ?? []) {
29
+ if (!entry.internal && entry.family === "IPv4")
30
+ return entry.address;
31
+ }
32
+ }
33
+ return undefined;
34
+ }
35
+ const LAN = lanAddress();
36
+ /** By default the server is told it has no interface addresses of its own, so a request to this machine's LAN address is treated as coming from a remote device: the real thing needs a second machine. */
37
+ async function start(host, localClientAddresses = () => new Set()) {
38
+ const handle = await createWebServer({
39
+ host,
40
+ localClientAddresses,
41
+ coordinatorPort: await freeLocalPort(),
42
+ hubUrl: await unreachableHubUrl(),
43
+ beaconPort: await freeLocalPort(),
44
+ });
45
+ cleanups.push(async () => {
46
+ await new Promise((resolve) => {
47
+ handle.wss.close(() => {
48
+ resolve();
49
+ });
50
+ });
51
+ await new Promise((resolve) => {
52
+ handle.server.close(() => {
53
+ resolve();
54
+ });
55
+ });
56
+ await handle.controller.shutdown();
57
+ });
58
+ if (!handle.server.listening) {
59
+ await new Promise((resolve) => {
60
+ handle.server.once("listening", () => {
61
+ resolve();
62
+ });
63
+ });
64
+ }
65
+ const address = handle.server.address();
66
+ if (address === null || typeof address === "string") {
67
+ throw new Error("expected a TCP listen address");
68
+ }
69
+ return { handle, port: address.port };
70
+ }
71
+ async function call(target, port, options) {
72
+ return new Promise((resolve, reject) => {
73
+ const req = http.request({
74
+ host: target,
75
+ port,
76
+ method: options.method ?? "GET",
77
+ path: options.path ?? "/api/agents",
78
+ headers: options.headers ?? {},
79
+ }, (res) => {
80
+ const chunks = [];
81
+ res.on("data", (chunk) => {
82
+ chunks.push(chunk);
83
+ });
84
+ res.on("end", () => {
85
+ resolve({
86
+ status: res.statusCode ?? 0,
87
+ headers: res.headers,
88
+ body: Buffer.concat(chunks).toString(),
89
+ });
90
+ });
91
+ });
92
+ req.on("error", reject);
93
+ req.end(options.body);
94
+ });
95
+ }
96
+ async function upgrade(target, port, headers) {
97
+ return new Promise((resolve) => {
98
+ const ws = new WS(`ws://${target}:${String(port)}/ws/mesh`, { headers });
99
+ ws.on("open", () => {
100
+ ws.close();
101
+ resolve("open");
102
+ });
103
+ ws.on("error", () => {
104
+ resolve("rejected");
105
+ });
106
+ });
107
+ }
108
+ describe("default bind", () => {
109
+ it("listens on loopback only, with no token, and serves without credentials", async () => {
110
+ const { handle, port } = await start(undefined);
111
+ expect(handle.host).toBe("127.0.0.1");
112
+ expect(handle.accessToken).toBeUndefined();
113
+ const reply = await call("127.0.0.1", port, {});
114
+ expect(reply.status).toBe(HTTP_OK);
115
+ });
116
+ it("reports its own loopback URL as the web_url", async () => {
117
+ const { handle, port } = await start(undefined);
118
+ expect(getWebUrlStatus(handle)).toEqual({
119
+ kind: "ready",
120
+ url: `http://127.0.0.1:${String(port)}`,
121
+ });
122
+ });
123
+ });
124
+ describe("Host check on loopback", () => {
125
+ it("rejects a loopback request whose Host names another domain, so a rebound name cannot reach the default bind", async () => {
126
+ const { port } = await start(undefined);
127
+ const reply = await call("127.0.0.1", port, {
128
+ headers: { host: `rebind.example:${String(port)}` },
129
+ });
130
+ expect(reply.status).toBe(HTTP_FORBIDDEN);
131
+ });
132
+ it("rejects a loopback request to a wildcard bind whose Host is the wildcard address", async () => {
133
+ const { port } = await start("0.0.0.0");
134
+ const reply = await call("127.0.0.1", port, {
135
+ headers: { host: `0.0.0.0:${String(port)}` },
136
+ });
137
+ expect(reply.status).toBe(HTTP_FORBIDDEN);
138
+ });
139
+ });
140
+ describe("bind beyond loopback", () => {
141
+ it("prints the token URL on stderr and never on stdout", async () => {
142
+ const stderr = vi.spyOn(console, "error").mockImplementation(() => { });
143
+ const stdout = vi.spyOn(console, "log").mockImplementation(() => { });
144
+ const { handle } = await start("0.0.0.0");
145
+ const token = handle.accessToken;
146
+ if (token === undefined)
147
+ throw new Error("expected an access token");
148
+ const printed = (spy) => spy.mock.calls.map((args) => args.join(" ")).join("\n");
149
+ expect(printed(stderr)).toContain(`token=${token}`);
150
+ expect(printed(stdout)).not.toContain(token);
151
+ });
152
+ it("leaves loopback clients unchanged: no token needed, and web_url stays loopback", async () => {
153
+ const { handle, port } = await start("0.0.0.0");
154
+ const reply = await call("127.0.0.1", port, {});
155
+ expect(reply.status).toBe(HTTP_OK);
156
+ expect(getWebUrlStatus(handle)).toEqual({
157
+ kind: "ready",
158
+ url: `http://127.0.0.1:${String(port)}`,
159
+ });
160
+ });
161
+ it.skipIf(LAN === undefined)("rejects a LAN client without the token, for reads, POST /api/action and the websocket", async () => {
162
+ const { port } = await start("0.0.0.0");
163
+ if (LAN === undefined)
164
+ throw new Error("unreachable");
165
+ expect((await call(LAN, port, {})).status).toBe(HTTP_UNAUTHORIZED);
166
+ const action = await call(LAN, port, {
167
+ method: "POST",
168
+ path: "/api/action",
169
+ body: JSON.stringify({ action: "list_rooms" }),
170
+ });
171
+ expect(action.status).toBe(HTTP_UNAUTHORIZED);
172
+ expect(await upgrade(LAN, port, {})).toBe("rejected");
173
+ const wrong = await call(LAN, port, {
174
+ headers: { authorization: "Bearer wrong" },
175
+ });
176
+ expect(wrong.status).toBe(HTTP_UNAUTHORIZED);
177
+ });
178
+ it.skipIf(LAN === undefined)("admits a LAN client with the bearer token, including a mutating action and the websocket", async () => {
179
+ const { handle, port } = await start("0.0.0.0");
180
+ if (LAN === undefined || handle.accessToken === undefined) {
181
+ throw new Error("unreachable");
182
+ }
183
+ const authorization = `Bearer ${handle.accessToken}`;
184
+ expect((await call(LAN, port, { headers: { authorization } })).status).toBe(HTTP_OK);
185
+ const action = await call(LAN, port, {
186
+ method: "POST",
187
+ path: "/api/action",
188
+ headers: { authorization },
189
+ body: JSON.stringify({ action: "list_rooms" }),
190
+ });
191
+ expect(action.status).toBe(HTTP_OK);
192
+ expect(await upgrade(LAN, port, { authorization })).toBe("open");
193
+ });
194
+ it.skipIf(LAN === undefined)("exchanges the one-time URL token for a cookie that then admits the websocket", async () => {
195
+ const { handle, port } = await start("0.0.0.0");
196
+ if (LAN === undefined || handle.accessToken === undefined) {
197
+ throw new Error("unreachable");
198
+ }
199
+ const exchange = await call(LAN, port, {
200
+ path: `/?token=${handle.accessToken}`,
201
+ });
202
+ expect(exchange.status).toBe(HTTP_FOUND);
203
+ expect(exchange.headers.location).toBe("/");
204
+ const cookie = `${accessCookieName(port)}=${handle.accessToken}`;
205
+ expect(String(exchange.headers["set-cookie"])).toContain(cookie);
206
+ expect(await upgrade(LAN, port, { cookie })).toBe("open");
207
+ });
208
+ it.skipIf(LAN === undefined)("rejects a LAN request whose Host names another domain even with the token", async () => {
209
+ const { handle, port } = await start("0.0.0.0");
210
+ if (LAN === undefined || handle.accessToken === undefined) {
211
+ throw new Error("unreachable");
212
+ }
213
+ const reply = await call(LAN, port, {
214
+ headers: {
215
+ host: `rebind.example:${String(port)}`,
216
+ authorization: `Bearer ${handle.accessToken}`,
217
+ },
218
+ });
219
+ expect(reply.status).toBe(HTTP_FORBIDDEN);
220
+ });
221
+ it.skipIf(LAN === undefined)("gives a specific-address bind a web_url that answers from this machine without the token", async () => {
222
+ if (LAN === undefined)
223
+ throw new Error("unreachable");
224
+ const { handle, port } = await start(LAN, ownInterfaceAddresses);
225
+ const status = getWebUrlStatus(handle);
226
+ expect(status).toEqual({
227
+ kind: "ready",
228
+ url: `http://${LAN}:${String(port)}`,
229
+ });
230
+ const reply = await call(LAN, port, {});
231
+ expect(reply.status).toBe(HTTP_OK);
232
+ });
233
+ it.skipIf(LAN === undefined)("treats a non-canonical wildcard spelling as a wildcard: a LAN client with the token is admitted", async () => {
234
+ const { handle, port } = await start("::0");
235
+ if (LAN === undefined || handle.accessToken === undefined) {
236
+ throw new Error("unreachable");
237
+ }
238
+ expect(handle.host).toBe("::");
239
+ const reply = await call(LAN, port, {
240
+ headers: { authorization: `Bearer ${handle.accessToken}` },
241
+ });
242
+ expect(reply.status).toBe(HTTP_OK);
243
+ });
244
+ });
245
+ //# sourceMappingURL=lan-web-server.integration.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lan-web-server.integration.test.js","sourceRoot":"","sources":["../../../../../src/bridges/user/web/test/lan-web-server.integration.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Unit tests for the web server's network exposure gate (agent-comms#346): bind resolution, the constant-time token comparison, and the per-request decision for loopback and non-loopback clients.
3
+ */
4
+ export {};
5
+ //# sourceMappingURL=web-access.unit.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web-access.unit.test.d.ts","sourceRoot":"","sources":["../../../../../src/bridges/user/web/test/web-access.unit.test.ts"],"names":[],"mappings":"AAAA;;GAEG"}
@@ -0,0 +1,271 @@
1
+ /**
2
+ * Unit tests for the web server's network exposure gate (agent-comms#346): bind resolution, the constant-time token comparison, and the per-request decision for loopback and non-loopback clients.
3
+ */
4
+ import { describe, expect, it } from "vitest";
5
+ import * as os from "node:os";
6
+ import { accessCookieName, authorise, canonicalAddress, generateAccessToken, isLoopbackAddress, isWildcardHost, localWebUrl, LOOPBACK_HOST, requiresAccessToken, resolveWebBindHost, tokensMatch, WEB_HOST_ENV, } from "../web-access.js";
7
+ const HTTP_FOUND = 302;
8
+ const HTTP_UNAUTHORIZED = 401;
9
+ const HTTP_FORBIDDEN = 403;
10
+ const PORT = 41234;
11
+ const LAN_CLIENT = "192.168.1.50";
12
+ const BIND = "192.168.1.10";
13
+ const HOST_HEADER = `${BIND}:${String(PORT)}`;
14
+ const COOKIE_NAME = accessCookieName(PORT);
15
+ /** A policy for a LAN bind whose machine has no interface addresses, so every non-loopback client is a remote device. */
16
+ function policy(overrides = {}) {
17
+ return {
18
+ token: generateAccessToken(),
19
+ bindHost: BIND,
20
+ port: PORT,
21
+ localClientAddresses: () => new Set(),
22
+ ...overrides,
23
+ };
24
+ }
25
+ function request(overrides) {
26
+ return {
27
+ method: "GET",
28
+ url: "/",
29
+ remoteAddress: LAN_CLIENT,
30
+ headers: { host: HOST_HEADER },
31
+ ...overrides,
32
+ };
33
+ }
34
+ describe("resolveWebBindHost", () => {
35
+ it("defaults to loopback with no option and no environment variable", () => {
36
+ expect(resolveWebBindHost(undefined, {})).toBe(LOOPBACK_HOST);
37
+ });
38
+ it("takes the environment variable as the opt-in", () => {
39
+ expect(resolveWebBindHost(undefined, { [WEB_HOST_ENV]: "0.0.0.0" })).toBe("0.0.0.0");
40
+ });
41
+ it("prefers an explicit option over the environment", () => {
42
+ expect(resolveWebBindHost("::", { [WEB_HOST_ENV]: "0.0.0.0" })).toBe("::");
43
+ });
44
+ it.each(["::0", "0:0:0:0:0:0:0:0", "::ffff:0.0.0.0", "::ffff:0:0"])("canonicalises the wildcard spelling %s so it is classified as a wildcard", (spelling) => {
45
+ const host = resolveWebBindHost(spelling, {});
46
+ expect(isWildcardHost(host)).toBe(true);
47
+ expect(requiresAccessToken(host)).toBe(true);
48
+ });
49
+ it("rejects a value that is not an IP address instead of falling back", () => {
50
+ expect(() => resolveWebBindHost(undefined, { [WEB_HOST_ENV]: "lan" })).toThrow(WEB_HOST_ENV);
51
+ });
52
+ });
53
+ describe("canonicalAddress", () => {
54
+ it.each([
55
+ ["::0", "::"],
56
+ ["0:0:0:0:0:0:0:1", "::1"],
57
+ ["::ffff:127.0.0.1", "127.0.0.1"],
58
+ ["::ffff:7f00:1", "127.0.0.1"],
59
+ ["192.168.1.10", "192.168.1.10"],
60
+ ["fe80::1%en0", "fe80::1%en0"],
61
+ ])("spells %s as %s", (input, expected) => {
62
+ expect(canonicalAddress(input)).toBe(expected);
63
+ });
64
+ });
65
+ describe("isWildcardHost", () => {
66
+ it.each(["0.0.0.0", "::", "::0", "0:0:0:0:0:0:0:0", "::ffff:0.0.0.0"])("%s binds every interface", (host) => {
67
+ expect(isWildcardHost(host)).toBe(true);
68
+ });
69
+ it.each(["192.168.1.10", "127.0.0.1", "::1", "fe80::1%en0"])("%s binds one interface", (host) => {
70
+ expect(isWildcardHost(host)).toBe(false);
71
+ });
72
+ });
73
+ describe("localWebUrl", () => {
74
+ it("maps a wildcard bind to loopback and keeps a specific bind", () => {
75
+ expect(localWebUrl("0.0.0.0", PORT)).toBe(`http://127.0.0.1:${PORT}`);
76
+ expect(localWebUrl("::", PORT)).toBe(`http://127.0.0.1:${PORT}`);
77
+ expect(localWebUrl(BIND, PORT)).toBe(`http://${BIND}:${PORT}`);
78
+ });
79
+ it("brackets an IPv6 bind", () => {
80
+ expect(localWebUrl("::1", PORT)).toBe(`http://[::1]:${PORT}`);
81
+ });
82
+ });
83
+ describe("requiresAccessToken", () => {
84
+ it.each(["127.0.0.1", "127.0.0.5", "::1"])("%s needs no token", (host) => {
85
+ expect(requiresAccessToken(host)).toBe(false);
86
+ });
87
+ it.each(["0.0.0.0", "::", "192.168.1.10"])("%s needs a token", (host) => {
88
+ expect(requiresAccessToken(host)).toBe(true);
89
+ });
90
+ });
91
+ describe("isLoopbackAddress", () => {
92
+ it.each(["127.0.0.1", "::1", "::ffff:127.0.0.1"])("%s is loopback", (a) => {
93
+ expect(isLoopbackAddress(a)).toBe(true);
94
+ });
95
+ it.each(["192.168.1.50", "::ffff:192.168.1.50", "fe80::1"])("%s is not loopback", (a) => {
96
+ expect(isLoopbackAddress(a)).toBe(false);
97
+ });
98
+ });
99
+ describe("tokensMatch", () => {
100
+ it("accepts an identical token", () => {
101
+ const token = generateAccessToken();
102
+ expect(tokensMatch(token, token)).toBe(true);
103
+ });
104
+ it("rejects a prefix, an extension, a different token and the empty string without throwing on length", () => {
105
+ const token = generateAccessToken();
106
+ expect(tokensMatch(token.slice(0, -1), token)).toBe(false);
107
+ expect(tokensMatch(`${token}0`, token)).toBe(false);
108
+ expect(tokensMatch(generateAccessToken(), token)).toBe(false);
109
+ expect(tokensMatch("", token)).toBe(false);
110
+ });
111
+ it("generates distinct tokens", () => {
112
+ expect(generateAccessToken()).not.toBe(generateAccessToken());
113
+ });
114
+ });
115
+ describe("authorise", () => {
116
+ it("allows a loopback client with a loopback Host and no token, under a loopback bind", () => {
117
+ const loopbackBind = policy({ token: undefined, bindHost: LOOPBACK_HOST });
118
+ for (const host of ["localhost", LOOPBACK_HOST, "[::1]"]) {
119
+ expect(authorise(loopbackBind, request({
120
+ remoteAddress: "127.0.0.1",
121
+ headers: { host: `${host}:${String(PORT)}` },
122
+ }))).toEqual({ kind: "allow" });
123
+ }
124
+ });
125
+ it("rejects a rebound Host from a loopback client, under a loopback bind and under a LAN bind", () => {
126
+ for (const p of [
127
+ policy({ token: undefined, bindHost: LOOPBACK_HOST }),
128
+ policy(),
129
+ ]) {
130
+ expect(authorise(p, request({
131
+ remoteAddress: "127.0.0.1",
132
+ headers: { host: `rebind.example:${String(PORT)}` },
133
+ }))).toMatchObject({ kind: "deny", status: HTTP_FORBIDDEN });
134
+ }
135
+ });
136
+ it("rejects the wildcard address as a Host even from loopback, under a wildcard bind", () => {
137
+ expect(authorise(policy({ bindHost: "0.0.0.0" }), request({
138
+ remoteAddress: "127.0.0.1",
139
+ headers: { host: `0.0.0.0:${String(PORT)}` },
140
+ }))).toMatchObject({ kind: "deny", status: HTTP_FORBIDDEN });
141
+ });
142
+ it("allows a loopback client without a token under a LAN bind", () => {
143
+ const decision = authorise(policy(), request({
144
+ remoteAddress: "127.0.0.1",
145
+ headers: { host: `localhost:${String(PORT)}` },
146
+ }));
147
+ expect(decision).toEqual({ kind: "allow" });
148
+ });
149
+ it("allows a client connecting from one of this machine's own interface addresses without a token", () => {
150
+ const decision = authorise(policy({ localClientAddresses: () => new Set([BIND]) }), request({ remoteAddress: BIND }));
151
+ expect(decision).toEqual({ kind: "allow" });
152
+ });
153
+ it("answers 401 to a remote client under a loopback bind, which has no token to present", () => {
154
+ expect(authorise(policy({ token: undefined, bindHost: LOOPBACK_HOST }), request({ headers: { host: `localhost:${String(PORT)}` } }))).toMatchObject({ kind: "deny", status: HTTP_UNAUTHORIZED });
155
+ });
156
+ it("scopes the cookie to the server's port so two servers on one host keep separate cookies", () => {
157
+ const portA = 41001;
158
+ const portB = 41002;
159
+ const a = policy({ port: portA });
160
+ const b = policy({ port: portB });
161
+ const both = `${accessCookieName(portA)}=${a.token}; ${accessCookieName(portB)}=${b.token}`;
162
+ const via = (p, port, cookie) => authorise(p, request({ headers: { host: `${BIND}:${String(port)}`, cookie } }));
163
+ expect(via(a, portA, both)).toEqual({ kind: "allow" });
164
+ expect(via(b, portB, both)).toEqual({ kind: "allow" });
165
+ expect(accessCookieName(portA)).not.toBe(accessCookieName(portB));
166
+ expect(via(a, portA, `${accessCookieName(portB)}=${b.token}`)).toMatchObject({ kind: "deny", status: HTTP_UNAUTHORIZED });
167
+ expect(via(b, portB, `${accessCookieName(portA)}=${a.token}`)).toMatchObject({ kind: "deny", status: HTTP_UNAUTHORIZED });
168
+ });
169
+ it("answers 401 to a non-loopback client with no token", () => {
170
+ expect(authorise(policy(), request({}))).toMatchObject({
171
+ kind: "deny",
172
+ status: HTTP_UNAUTHORIZED,
173
+ });
174
+ });
175
+ it("answers 401 to a wrong bearer token", () => {
176
+ const decision = authorise(policy(), request({
177
+ headers: { host: HOST_HEADER, authorization: "Bearer wrong" },
178
+ }));
179
+ expect(decision).toMatchObject({ kind: "deny", status: HTTP_UNAUTHORIZED });
180
+ });
181
+ it("allows a correct bearer token", () => {
182
+ const p = policy();
183
+ const decision = authorise(p, request({
184
+ headers: { host: HOST_HEADER, authorization: `Bearer ${p.token}` },
185
+ }));
186
+ expect(decision).toEqual({ kind: "allow" });
187
+ });
188
+ it("allows a correct cookie and rejects a wrong one", () => {
189
+ const p = policy();
190
+ expect(authorise(p, request({
191
+ headers: {
192
+ host: HOST_HEADER,
193
+ cookie: `a=b; ${COOKIE_NAME}=${p.token}`,
194
+ },
195
+ }))).toEqual({ kind: "allow" });
196
+ expect(authorise(p, request({
197
+ headers: {
198
+ host: HOST_HEADER,
199
+ cookie: `${COOKIE_NAME}=wrong`,
200
+ },
201
+ }))).toMatchObject({ kind: "deny", status: HTTP_UNAUTHORIZED });
202
+ });
203
+ it("exchanges a correct ?token= on a GET for a cookie and a redirect without the token", () => {
204
+ const p = policy();
205
+ const decision = authorise(p, request({ url: `/room?x=1&token=${p.token}` }));
206
+ expect(decision).toEqual({
207
+ kind: "exchange",
208
+ status: HTTP_FOUND,
209
+ cookie: `${COOKIE_NAME}=${p.token}; HttpOnly; SameSite=Strict; Path=/`,
210
+ location: "/room?x=1",
211
+ });
212
+ });
213
+ it("does not exchange a wrong ?token=, nor any ?token= on a POST", () => {
214
+ const p = policy();
215
+ expect(authorise(p, request({ url: "/?token=wrong" }))).toMatchObject({
216
+ kind: "deny",
217
+ status: HTTP_UNAUTHORIZED,
218
+ });
219
+ expect(authorise(p, request({ method: "POST", url: `/api/action?token=${p.token}` }))).toMatchObject({ kind: "deny", status: HTTP_UNAUTHORIZED });
220
+ });
221
+ it("rejects a Host naming another domain with 403 even when the token is right (DNS rebinding)", () => {
222
+ const p = policy();
223
+ const decision = authorise(p, request({
224
+ headers: {
225
+ host: `rebind.example:${String(PORT)}`,
226
+ authorization: `Bearer ${p.token}`,
227
+ },
228
+ }));
229
+ expect(decision).toMatchObject({ kind: "deny", status: HTTP_FORBIDDEN });
230
+ });
231
+ it("rejects a Host on the right address but the wrong port, and a missing Host", () => {
232
+ const p = policy();
233
+ const authorization = `Bearer ${p.token}`;
234
+ expect(authorise(p, request({ headers: { host: `${BIND}:1`, authorization } }))).toMatchObject({ kind: "deny", status: HTTP_FORBIDDEN });
235
+ expect(authorise(p, request({ headers: { authorization } }))).toMatchObject({ kind: "deny", status: HTTP_FORBIDDEN });
236
+ });
237
+ it("accepts this machine's own hostname as the Host", () => {
238
+ const p = policy();
239
+ const machine = os.hostname().replace(/\.local$/i, "");
240
+ const decision = authorise(p, request({
241
+ headers: {
242
+ host: `${machine}.local:${String(PORT)}`,
243
+ authorization: `Bearer ${p.token}`,
244
+ },
245
+ }));
246
+ expect(decision).toEqual({ kind: "allow" });
247
+ });
248
+ it("rejects a cross-origin request carrying a valid cookie", () => {
249
+ const p = policy();
250
+ const decision = authorise(p, request({
251
+ headers: {
252
+ host: HOST_HEADER,
253
+ cookie: `${COOKIE_NAME}=${p.token}`,
254
+ origin: `http://${BIND}:9999`,
255
+ },
256
+ }));
257
+ expect(decision).toMatchObject({ kind: "deny", status: HTTP_FORBIDDEN });
258
+ });
259
+ it("allows a same-origin request carrying a valid cookie", () => {
260
+ const p = policy();
261
+ const decision = authorise(p, request({
262
+ headers: {
263
+ host: HOST_HEADER,
264
+ cookie: `${COOKIE_NAME}=${p.token}`,
265
+ origin: `http://${HOST_HEADER}`,
266
+ },
267
+ }));
268
+ expect(decision).toEqual({ kind: "allow" });
269
+ });
270
+ });
271
+ //# sourceMappingURL=web-access.unit.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web-access.unit.test.js","sourceRoot":"","sources":["../../../../../src/bridges/user/web/test/web-access.unit.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ /**
2
+ * The LAN beacon advertises the web UI's host (by source address) and port, and never the access token (agent-comms#346).
3
+ */
4
+ export {};
5
+ //# sourceMappingURL=web-beacon.unit.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web-beacon.unit.test.d.ts","sourceRoot":"","sources":["../../../../../src/bridges/user/web/test/web-beacon.unit.test.ts"],"names":[],"mappings":"AAAA;;GAEG"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * The LAN beacon advertises the web UI's host (by source address) and port, and never the access token (agent-comms#346).
3
+ */
4
+ import { afterEach, expect, it } from "vitest";
5
+ import * as dgram from "node:dgram";
6
+ import { freeLocalPort, TeardownStack } from "../../../../test/hub-helpers.js";
7
+ import { startWebBeacon, WEB_BEACON_TYPE } from "../web-beacon.js";
8
+ const cleanups = new TeardownStack();
9
+ afterEach(async () => {
10
+ await cleanups.run();
11
+ });
12
+ it("sends one datagram naming the peer and the web port, with nothing else", async () => {
13
+ const port = await freeLocalPort();
14
+ const received = new Promise((resolve) => {
15
+ const listener = dgram.createSocket("udp4");
16
+ cleanups.push(async () => {
17
+ listener.close();
18
+ });
19
+ listener.on("message", (message) => {
20
+ resolve(message.toString());
21
+ });
22
+ listener.bind(port, "127.0.0.1");
23
+ });
24
+ const beacon = startWebBeacon({
25
+ peerId: "peer-1",
26
+ webPort: 4321,
27
+ port,
28
+ address: "127.0.0.1",
29
+ });
30
+ cleanups.push(async () => {
31
+ beacon.stop();
32
+ });
33
+ const payload = JSON.parse(await received);
34
+ expect(payload).toEqual({
35
+ type: WEB_BEACON_TYPE,
36
+ peerId: "peer-1",
37
+ webPort: 4321,
38
+ });
39
+ });
40
+ //# sourceMappingURL=web-beacon.unit.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web-beacon.unit.test.js","sourceRoot":"","sources":["../../../../../src/bridges/user/web/test/web-beacon.unit.test.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC/C,OAAO,KAAK,KAAK,MAAM,YAAY,CAAC;AACpC,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,iCAAiC,CAAC;AAC/E,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAEnE,MAAM,QAAQ,GAAG,IAAI,aAAa,EAAE,CAAC;AAErC,SAAS,CAAC,KAAK,IAAI,EAAE;IACnB,MAAM,QAAQ,CAAC,GAAG,EAAE,CAAC;AACvB,CAAC,CAAC,CAAC;AAEH,EAAE,CAAC,wEAAwE,EAAE,KAAK,IAAI,EAAE;IACtF,MAAM,IAAI,GAAG,MAAM,aAAa,EAAE,CAAC;IACnC,MAAM,QAAQ,GAAG,IAAI,OAAO,CAAS,CAAC,OAAO,EAAE,EAAE;QAC/C,MAAM,QAAQ,GAAG,KAAK,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC;QAC5C,QAAQ,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE;YACvB,QAAQ,CAAC,KAAK,EAAE,CAAC;QACnB,CAAC,CAAC,CAAC;QACH,QAAQ,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,OAAO,EAAE,EAAE;YACjC,OAAO,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;QAC9B,CAAC,CAAC,CAAC;QACH,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;IACnC,CAAC,CAAC,CAAC;IACH,MAAM,MAAM,GAAG,cAAc,CAAC;QAC5B,MAAM,EAAE,QAAQ;QAChB,OAAO,EAAE,IAAI;QACb,IAAI;QACJ,OAAO,EAAE,WAAW;KACrB,CAAC,CAAC;IACH,QAAQ,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE;QACvB,MAAM,CAAC,IAAI,EAAE,CAAC;IAChB,CAAC,CAAC,CAAC;IAEH,MAAM,OAAO,GAAY,IAAI,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,CAAC;IACpD,MAAM,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC;QACtB,IAAI,EAAE,eAAe;QACrB,MAAM,EAAE,QAAQ;QAChB,OAAO,EAAE,IAAI;KACd,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}