* feat: add an auto or fixed columns picker to card grids

* feat: add safe mode that only runs layout and theme plugins

* docs: add simple layout to plugin table

* fix: ask for local network access in the macOS app

* feat: allow plugin registries on listed private hosts (#1705)

* chore: add private registry hosts to the changelog

* feat: use the proxy for plugin registry downloads

* fix: allow changing connection origin on shared hosts

* chore: update translations and plugin pins

* chore: lint, format, and bump version to 26.10.5

---------

Co-authored-by: Lukas <80832688+0skater0@users.noreply.github.com>
This commit is contained in:
Luke GustafsonandLukas authored and GitHub committed 2026-10-22 13:30:48 -05:00
1 parent 7b7b17e31f
commit 96b8520fb4
96 files changed
+1885 -474

No files matched your search

@@ -8,10 +8,23 @@ import * as tar from "tar";
const fetchMock = vi.hoisted(() => vi.fn());
const testKey = vi.hoisted(() => ({ value: null as null | object }));
vi.mock("../../utils/safe-outbound-fetch.js", () => ({
vi.mock("../../utils/safe-outbound-fetch.js", async (importOriginal) => ({
...(await importOriginal<
typeof import("../../utils/safe-outbound-fetch.js")
>()),
safeOutboundFetch: fetchMock,
}));
const proxyMock = vi.hoisted(() => ({
on: false,
fetch: vi.fn(),
}));
vi.mock("../../utils/proxy-agent.js", () => ({
getProxyAgent: () => (proxyMock.on ? {} : undefined),
fetchWithProxy: proxyMock.fetch,
}));
vi.mock("../../utils/app-version.js", () => ({
getLocalVersion: () => "26.10.3",
}));
@@ -43,9 +56,11 @@ vi.mock("../../plugins/trust.js", async (importOriginal) => {
import {
communitySource,
downloadRelease,
fetchIndexAt,
fetchRegistryIndex,
fetchRegistryStats,
findRelease,
getRegistryPrivateHosts,
latestOnChannel,
getStatsUrl,
canRunRelease,
@@ -409,6 +424,91 @@ describe("fetchRegistryIndex", () => {
});
});
describe("registry fetches with a proxy", () => {
afterEach(() => {
proxyMock.on = false;
proxyMock.fetch.mockReset();
});
it("goes through the proxy when one is configured", async () => {
proxyMock.on = true;
proxyMock.fetch.mockResolvedValueOnce(
response(JSON.stringify({ plugins: [] })),
);
await fetchIndexAt("https://registry.example/index.json");
expect(proxyMock.fetch).toHaveBeenCalledTimes(1);
expect(fetchMock).not.toHaveBeenCalled();
});
it("still refuses a private address typed in as a literal", async () => {
proxyMock.on = true;
await expect(fetchIndexAt("https://127.0.0.1/index.json")).rejects.toThrow(
"Private destinations are not allowed",
);
expect(proxyMock.fetch).not.toHaveBeenCalled();
});
});
describe("private registry hosts", () => {
afterEach(() => {
delete process.env.TERMIX_PLUGIN_REGISTRY_PRIVATE_HOSTS;
});
it("reads a comma separated list of host names", () => {
process.env.TERMIX_PLUGIN_REGISTRY_PRIVATE_HOSTS =
" GitLab.lan ,, 192.168.1.5 ";
expect(getRegistryPrivateHosts()).toEqual(["gitlab.lan", "192.168.1.5"]);
delete process.env.TERMIX_PLUGIN_REGISTRY_PRIVATE_HOSTS;
expect(getRegistryPrivateHosts()).toEqual([]);
});
it("brings each entry into the form the fetch compares against", () => {
process.env.TERMIX_PLUGIN_REGISTRY_PRIVATE_HOSTS =
"GitLab.LAN,[FD00::0001],fd00::2,bücher.lan";
expect(getRegistryPrivateHosts()).toEqual([
"gitlab.lan",
"fd00::1",
"fd00::2",
"xn--bcher-kva.lan",
]);
});
it("drops entries with more than a host", () => {
process.env.TERMIX_PLUGIN_REGISTRY_PRIVATE_HOSTS =
"gitlab.lan:8443,https://git.lan,[fd00::1]:80,nas.lan";
expect(getRegistryPrivateHosts()).toEqual(["nas.lan"]);
});
it("allows the listed hosts on every hop", async () => {
process.env.TERMIX_PLUGIN_REGISTRY_PRIVATE_HOSTS = "gitlab.lan";
fetchMock
.mockResolvedValueOnce(
response("", 302, { location: "https://gitlab.lan/raw/index.json" }),
)
.mockResolvedValueOnce(response(JSON.stringify({ plugins: [] })));
await fetchIndexAt("https://gitlab.lan/index.json");
expect(fetchMock.mock.calls.map((call) => call[2])).toEqual([
["gitlab.lan"],
["gitlab.lan"],
]);
});
it("still blocks a redirect to a private host that is not listed", async () => {
process.env.TERMIX_PLUGIN_REGISTRY_PRIVATE_HOSTS = "gitlab.lan";
const actual = await vi.importActual<
typeof import("../../utils/safe-outbound-fetch.js")
>("../../utils/safe-outbound-fetch.js");
fetchMock
.mockResolvedValueOnce(
response("", 302, { location: "https://127.0.0.1/index.json" }),
)
.mockImplementationOnce(actual.safeOutboundFetch);
await expect(fetchIndexAt("https://gitlab.lan/index.json")).rejects.toThrow(
"Private destinations are not allowed",
);
});
});
/** A real .tmxplug holding only a manifest, enough for the checks. */
function pack(manifest: Record<string, unknown>): Buffer {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "termix-pack-"));