[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
171
electron/plugins/networkBroker.test.cjs
Normal file
171
electron/plugins/networkBroker.test.cjs
Normal file
@@ -0,0 +1,171 @@
|
||||
"use strict";
|
||||
|
||||
const assert = require("node:assert/strict");
|
||||
const test = require("node:test");
|
||||
|
||||
const {
|
||||
PluginNetworkBroker,
|
||||
MAX_NETWORK_BODY_BYTES,
|
||||
MAX_NETWORK_HEADER_BYTES,
|
||||
} = require("./networkBroker.cjs");
|
||||
const { RPC_ERRORS } = require("./rpcRouter.cjs");
|
||||
|
||||
function runtimeContext() {
|
||||
return {
|
||||
pluginId: "com.example.network",
|
||||
pluginVersion: "1.0.0",
|
||||
runtimeId: "runtime-1",
|
||||
runtimeKind: "browser",
|
||||
manifest: { permissions: { optional: ["network"] } },
|
||||
signal: new AbortController().signal,
|
||||
assertActive: async () => {},
|
||||
};
|
||||
}
|
||||
|
||||
test("network broker strips cookies, bounds responses, and charges decoded bytes", async () => {
|
||||
const calls = [];
|
||||
const charged = [];
|
||||
const broker = new PluginNetworkBroker({
|
||||
fetch: async (url, options) => {
|
||||
calls.push({ url: String(url), options });
|
||||
return new Response("hello", {
|
||||
status: 200,
|
||||
headers: { "content-type": "text/plain", "set-cookie": "secret=1" },
|
||||
});
|
||||
},
|
||||
permissionEngine: { authorize: async () => {} },
|
||||
quotaManager: { chargeBytes: (...args) => charged.push(args) },
|
||||
});
|
||||
const response = await broker.request({
|
||||
url: "https://example.com/data",
|
||||
headers: { accept: "text/plain" },
|
||||
}, runtimeContext());
|
||||
assert.equal(calls[0].options.credentials, "omit");
|
||||
assert.equal(calls[0].options.redirect, "manual");
|
||||
assert.equal(response.headers["set-cookie"], undefined);
|
||||
assert.equal(Buffer.from(response.body.data, "base64").toString(), "hello");
|
||||
assert.deepEqual(charged, [["runtime-1", "network", 5]]);
|
||||
});
|
||||
|
||||
test("every redirect origin is authorized and cross-origin credentials are stripped", async () => {
|
||||
const calls = [];
|
||||
const authorized = [];
|
||||
const broker = new PluginNetworkBroker({
|
||||
fetch: async (url, options) => {
|
||||
calls.push({ url: String(url), options });
|
||||
if (calls.length === 1) {
|
||||
return new Response(null, { status: 302, headers: { location: "https://other.example/final" } });
|
||||
}
|
||||
return new Response("ok", { status: 200 });
|
||||
},
|
||||
permissionEngine: { authorize: async (_context, descriptor) => authorized.push(descriptor) },
|
||||
});
|
||||
await broker.request({
|
||||
url: "https://first.example/start",
|
||||
headers: { authorization: "Bearer host-secret", "x-request-id": "one" },
|
||||
}, runtimeContext());
|
||||
assert.equal(authorized[0].resources[0], "https://other.example");
|
||||
assert.equal(calls[1].options.headers.authorization, undefined);
|
||||
assert.equal(calls[1].options.headers["x-request-id"], "one");
|
||||
});
|
||||
|
||||
test("HTTP redirect method semantics do not replay POST bodies on 302", async () => {
|
||||
const calls = [];
|
||||
const charges = [];
|
||||
const broker = new PluginNetworkBroker({
|
||||
fetch: async (_url, options) => {
|
||||
calls.push(options);
|
||||
if (calls.length === 1) return new Response(null, { status: 302, headers: { location: "/done" } });
|
||||
return new Response("ok");
|
||||
},
|
||||
permissionEngine: { authorize: async () => {} },
|
||||
quotaManager: { chargeBytes: (...args) => charges.push(args) },
|
||||
});
|
||||
await broker.request({
|
||||
url: "https://example.com/start",
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: { encoding: "utf8", data: "{}" },
|
||||
}, runtimeContext());
|
||||
assert.equal(calls[1].method, "GET");
|
||||
assert.equal(calls[1].body, undefined);
|
||||
assert.equal(calls[1].headers["content-type"], undefined);
|
||||
assert.deepEqual(charges, [
|
||||
["runtime-1", "network", 2],
|
||||
["runtime-1", "network", 2],
|
||||
], "the request body and final response are both charged");
|
||||
});
|
||||
|
||||
test("network redirects reject embedded credentials before authorization or fetch", async () => {
|
||||
let calls = 0;
|
||||
let authorizations = 0;
|
||||
const broker = new PluginNetworkBroker({
|
||||
fetch: async () => {
|
||||
calls += 1;
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
headers: { location: "https://user:password@other.example/private" },
|
||||
});
|
||||
},
|
||||
permissionEngine: { authorize: async () => { authorizations += 1; } },
|
||||
});
|
||||
await assert.rejects(
|
||||
broker.request({ url: "https://first.example/start" }, runtimeContext()),
|
||||
/cannot contain credentials/,
|
||||
);
|
||||
assert.equal(calls, 1);
|
||||
assert.equal(authorizations, 0);
|
||||
});
|
||||
|
||||
test("network request validation rejects transport headers, credentials, and non-canonical bodies", () => {
|
||||
const broker = new PluginNetworkBroker({
|
||||
fetch: async () => new Response(),
|
||||
permissionEngine: { authorize: async () => {} },
|
||||
});
|
||||
assert.throws(() => broker.validate({
|
||||
url: "https://example.com",
|
||||
headers: { cookie: "secret=1" },
|
||||
}), /forbidden/);
|
||||
assert.throws(() => broker.validate({ url: "https://user:pass@example.com" }), /credentials/);
|
||||
assert.throws(() => broker.validate({
|
||||
url: "https://example.com",
|
||||
method: "POST",
|
||||
body: { encoding: "base64", data: "YQ" },
|
||||
}), /not canonical/);
|
||||
assert.throws(() => broker.validate({
|
||||
url: "https://example.com",
|
||||
headers: Object.fromEntries(Array.from({ length: 9 }, (_, index) => (
|
||||
[`x-budget-${index}`, "x".repeat(MAX_NETWORK_HEADER_BYTES / 8)]
|
||||
))),
|
||||
}), /headers are too large/);
|
||||
assert.throws(() => broker.validate({
|
||||
url: "https://example.com",
|
||||
headers: { "x-control": "before\0after" },
|
||||
}), /header value is invalid/);
|
||||
});
|
||||
|
||||
test("network response streaming stops above the byte cap", async () => {
|
||||
const broker = new PluginNetworkBroker({
|
||||
fetch: async () => new Response("small", {
|
||||
headers: { "content-length": String(MAX_NETWORK_BODY_BYTES + 1) },
|
||||
}),
|
||||
permissionEngine: { authorize: async () => {} },
|
||||
});
|
||||
await assert.rejects(
|
||||
broker.request({ url: "https://example.com" }, runtimeContext()),
|
||||
(error) => error.code === RPC_ERRORS.resourceExhausted,
|
||||
);
|
||||
});
|
||||
|
||||
test("network broker reports its own timeout as a stable deadline error", async () => {
|
||||
const broker = new PluginNetworkBroker({
|
||||
fetch: async (_url, options) => new Promise((_resolve, reject) => {
|
||||
options.signal.addEventListener("abort", () => reject(options.signal.reason), { once: true });
|
||||
}),
|
||||
permissionEngine: { authorize: async () => {} },
|
||||
});
|
||||
await assert.rejects(
|
||||
broker.request({ url: "https://example.com", timeoutMs: 5 }, runtimeContext()),
|
||||
(error) => error.code === RPC_ERRORS.deadlineExceeded,
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user