Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
ef683b9
fix(desktop): prevent proxy password mask corruption
Sun-GLiang Aug 24, 2026
97e11ed
fix(runtime-host): make proxy updates recoverable
Sun-GLiang Aug 25, 2026
59ba976
chore(test): update Windows skip inventory
Sun-GLiang Aug 25, 2026
e72601e
fix(desktop): close proxy password review gaps
Sun-GLiang Aug 25, 2026
c9bec15
ci: retrigger checks
Sun-GLiang Aug 25, 2026
a35028f
fix(storage): keep proxy secrets inside credential vault
Sun-GLiang Aug 25, 2026
756a283
ci: retrigger checks
Sun-GLiang Aug 25, 2026
0e34e65
chore(test): refresh Windows skip inventory
Sun-GLiang Aug 26, 2026
a431fc4
fix(desktop): address proxy password review findings
Sun-GLiang Aug 26, 2026
8a595e5
chore(test): refresh Windows skip inventory
Sun-GLiang Aug 26, 2026
9921935
Merge upstream/main into fix/3696-proxy-password-editing
Sun-GLiang Aug 29, 2026
24ff828
Merge upstream/main into fix/3696-proxy-password-editing
Sun-GLiang Aug 29, 2026
7e14d89
fix(desktop): preserve credentials-only restores
Sun-GLiang Aug 30, 2026
69b7bda
Merge upstream/main into fix/3696-proxy-password-editing
Sun-GLiang Aug 30, 2026
97dfd64
fix(desktop): isolate proxy password draft feature
Sun-GLiang Aug 30, 2026
262bf0c
fix(desktop): avoid untracked proxy surface
Sun-GLiang Aug 30, 2026
802dde2
fix(desktop): keep proxy test entry minimal
Sun-GLiang Aug 30, 2026
10c18e7
fix(desktop): expand sidebar in proxy e2e
Sun-GLiang Aug 30, 2026
a0ea946
fix(desktop): bind restored credentials to targets
Sun-GLiang Aug 31, 2026
6cf7e7b
Merge upstream/main into fix/3696-proxy-password-editing
Sun-GLiang Aug 31, 2026
6c6644b
fix(config): bind credential transfers to host targets
Sun-GLiang Aug 31, 2026
463eb56
fix(config): reject unbound credential imports
Sun-GLiang Aug 31, 2026
fd08f92
Merge apache/maka main into fix/3696-proxy-password-editing
Sun-GLiang Sep 1, 2026
9aa343f
Merge branch 'main' of https://github.com/apache/maka into codex/pr-3…
Sun-GLiang Sep 1, 2026
a090b83
test(release): make state root qualification portable
Sun-GLiang Sep 1, 2026
9576eee
fix(runtime-host): advance proxy protocol compatibility epoch
Sun-GLiang Sep 1, 2026
5466634
Merge apache/maka main into fix/3696-proxy-password-editing
Sun-GLiang Sep 1, 2026
f0bba78
Merge apache/maka main into fix/3696-proxy-password-editing
Sun-GLiang Sep 2, 2026
b614363
Merge apache/maka main into fix/3696-proxy-password-editing
Sun-GLiang Sep 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 152 additions & 0 deletions apps/desktop/e2e/proxy-password-editing.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import { createServer } from "node:http";
import {
test,
expect,
COMPOSER_INPUT,
ensureSidebarExpanded,
} from "./fixtures";

test("proxy password drafts save once, reload safely, and authenticate offline", async ({
window: page,
}) => {
const username = "proxy-user";
const password = "complete-secret";
const replacementPassword = "replacement-secret";
let acceptAuthorization!: (value: string | undefined) => void;
const authorization = new Promise<string | undefined>((resolve) => {
acceptAuthorization = resolve;
});
const proxy = createServer((request, response) => {
acceptAuthorization(request.headers["proxy-authorization"]);
response.writeHead(200, { "content-length": "0", connection: "close" });
response.end();
});
await new Promise<void>((resolve, reject) => {
proxy.once("error", reject);
proxy.listen(0, "127.0.0.1", () => resolve());
});
const address = proxy.address();
if (!address || typeof address === "string") {
throw new Error("Local proxy did not expose a TCP port");
}

try {
await ensureSidebarExpanded(page);
await page.getByRole("button", { name: "设置" }).click();
await page.getByRole("button", { name: "通用", exact: true }).click();
await page.getByRole("switch", { name: "启用代理服务器" }).click();
await page.getByRole("textbox", { name: "服务器地址" }).fill("127.0.0.1");
await page.getByRole("spinbutton", { name: "端口" }).fill(String(address.port));
await page.getByRole("switch", { name: "启用代理认证" }).click();
await page.getByRole("textbox", { name: "用户名" }).fill(username);

const passwordInput = page.getByRole("textbox", {
name: "密码 凭据值",
exact: true,
});
await passwordInput.pressSequentially(password);
await expect(passwordInput).toHaveValue(password);
await expect
.poll(() =>
page.evaluate(async () =>
(await window.maka.settings.get()).network.proxy.passwordConfigured,
),
)
.toBe(false);

const eye = page.getByRole("button", { name: /显示|隐藏/ });
await eye.click();
await expect(passwordInput).toHaveAttribute("type", "text");
await expect(passwordInput).toHaveValue(password);
await expect
.poll(() =>
page.evaluate(async () =>
(await window.maka.settings.get()).network.proxy.passwordConfigured,
),
)
.toBe(false);

await passwordInput.focus();
await page.keyboard.press("Tab");
await expect(eye).toBeFocused();
await expect
.poll(() =>
page.evaluate(async () =>
(await window.maka.settings.get()).network.proxy.passwordConfigured,
),
)
.toBe(false);

await page.keyboard.press("Tab");
await expect
.poll(() =>
page.evaluate(async () =>
(await window.maka.settings.get()).network.proxy.passwordConfigured,
),
)
.toBe(true);

await page.reload();
await page.waitForSelector(COMPOSER_INPUT);
await ensureSidebarExpanded(page);
await page.getByRole("button", { name: "设置" }).click();
await page.getByRole("button", { name: "通用", exact: true }).click();
const reloadedPassword = page.getByPlaceholder(
"密码已保存;输入新密码以替换",
);
await expect(reloadedPassword).toHaveValue("");
await expect(page.getByRole("button", { name: "复制" })).toHaveCount(0);

await reloadedPassword.pressSequentially("discarded-draft");
await expect(reloadedPassword).toHaveValue("discarded-draft");
await reloadedPassword.press("Escape");
await expect(reloadedPassword).toBeVisible();
await expect(reloadedPassword).toHaveValue("");

await reloadedPassword.pressSequentially(replacementPassword);
await eye.click();
await expect(reloadedPassword).toHaveAttribute("type", "text");
await expect(reloadedPassword).toHaveValue(replacementPassword);
await reloadedPassword.focus();
await reloadedPassword.press("Enter");
await expect(reloadedPassword).toHaveValue("");

await page.reload();
await page.waitForSelector(COMPOSER_INPUT);
await ensureSidebarExpanded(page);
await page.getByRole("button", { name: "设置" }).click();
await page.getByRole("button", { name: "通用", exact: true }).click();
await expect(
page.getByPlaceholder("密码已保存;输入新密码以替换"),
).toHaveValue("");

const tested = await page.evaluate(() =>
window.maka.settings.testNetworkProxy({ url: "http://example.com" }),
);
expect(tested.ok).toBe(true);
expect(await authorization).toBe(
`Basic ${Buffer.from(`${username}:${replacementPassword}`).toString("base64")}`,
);
} finally {
await new Promise<void>((resolve) => proxy.close(() => resolve()));
}
});
2 changes: 1 addition & 1 deletion apps/desktop/renderer-architecture.json
Original file line number Diff line number Diff line change
Expand Up @@ -3066,6 +3066,7 @@
"actionFactories": [],
"dependencyPaths": {
"../features/connection-settings": 1,
"../features/network-proxy/index.js": 1,
"../locales/settings-preferences-copy.js": 1,
"../locales/settings-shared-copy.js": 1,
"../locales/settings-test-result-copy.js": 1,
Expand All @@ -3084,7 +3085,6 @@
"@maka/core/llm-connections": 1,
"@maka/core/model-thinking": 1,
"@maka/core/settings": 3,
"@maka/core/settings/network-settings": 1,
"@maka/ui": 2,
"react": 1
}
Expand Down
177 changes: 173 additions & 4 deletions apps/desktop/src/main/__tests__/config-transfer-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@

import { strict as assert } from 'node:assert';
import { describe, it } from 'node:test';
import type { AppSettings } from '@maka/core/settings';
import type { LlmConnection } from '@maka/core/llm-connections';
import type { CredentialKind } from '@maka/storage/credential-store';
import { applyConfigImport, type ConfigTransferDeps } from '../config-transfer-service.js';

function conn(slug: string): LlmConnection {
function conn(
slug: string,
overrides: Partial<LlmConnection> = {},
): LlmConnection {
return {
slug,
name: slug,
Expand All @@ -33,6 +35,7 @@ function conn(slug: string): LlmConnection {
enabled: true,
createdAt: 1,
updatedAt: 1,
...overrides,
};
}

Expand All @@ -58,12 +61,13 @@ function makeDeps(overrides: Partial<ConfigTransferDeps> = {}): {
settingsStore: {
update: async (patch) => {
updatedSettings.push(patch);
return patch as unknown as AppSettings;
return { skippedCredentials: 0 };
},
},
credentialStore: {
setSecret: async (slug, kind, value) => {
setSecret: async ({ slug, kind, value }) => {
setCreds.push({ slug, kind, value });
return true;
},
},
writeMemory: async (content) => {
Expand Down Expand Up @@ -100,6 +104,28 @@ describe('config-transfer-service', () => {
assert.deepEqual(writtenMemory, ['# imported memory']);
});

it('reports a settings-carried proxy credential skipped by Host target binding', async () => {
const { deps } = makeDeps({
settingsStore: {
update: async () => ({ skippedCredentials: 1 }),
},
} as never);
const bundle = {
schemaVersion: 1,
exportedAt: '',
appVersion: '0.1.0',
includedData: ['settings', 'credentials'] as const,
data: {
settings: { network: { proxy: { credential: { kind: 'replace', secret: 'source' } } } },
credentials: [],
},
};

const result = await applyConfigImport(bundle as any, 'skip', deps);

assert.deepEqual(result.credentials, { applied: 0, skipped: 1 });
});

it('restores the selection a backup states instead of re-enabling its default', async () => {
// A backup can hold a connection whose default model the user had disabled.
// `save()` cannot tell a stated selection from one a sync echoed back, so it
Expand Down Expand Up @@ -160,6 +186,149 @@ describe('config-transfer-service', () => {
assert.deepEqual(setCreds, [{ slug: 'deepseek-main', kind: 'api_key', value: 'sk-new' }]);
assert.deepEqual(result.credentials, { applied: 1, skipped: 0 });
});

it('reports a Host-bound connection credential write that loses its target race', async () => {
const { deps, setCreds } = makeDeps({
credentialStore: {
setSecret: async () => false,
},
} as never);
const bundle = {
schemaVersion: 1,
exportedAt: '',
appVersion: '0.1.0',
includedData: ['connections', 'credentials'] as const,
data: {
connections: [conn('deepseek-main')],
credentials: [{ slug: 'deepseek-main', kind: 'api_key', value: 'source-secret' }],
},
};

const result = await applyConfigImport(bundle as any, 'overwrite', deps);

assert.deepEqual(setCreds, []);
assert.deepEqual(result.credentials, { applied: 0, skipped: 1 });
});

it('writes a credentials-only bundle to an existing connection', async () => {
const { deps, saved, setCreds } = makeDeps();
const bundle = {
schemaVersion: 1,
exportedAt: '',
appVersion: '0.1.0',
includedData: ['credentials'] as const,
data: {
credentials: [
{
slug: 'deepseek-main',
kind: 'api_key',
value: 'sk-restored',
connection: {
providerType: 'deepseek',
effectiveBaseUrl: 'https://api.deepseek.com',
},
},
],
},
};

const result = await applyConfigImport(bundle as any, 'skip', deps);

assert.deepEqual(saved, [], 'credentials-only import does not rewrite the connection');
assert.deepEqual(setCreds, [
{ slug: 'deepseek-main', kind: 'api_key', value: 'sk-restored' },
]);
assert.deepEqual(result.credentials, { applied: 1, skipped: 0 });
});

it('skips a credentials-only entry without a source connection binding', async () => {
const { deps, setCreds } = makeDeps();
const bundle = {
schemaVersion: 1,
exportedAt: '',
appVersion: '0.1.0',
includedData: ['credentials'] as const,
data: {
credentials: [
{
slug: 'deepseek-main',
kind: 'api_key',
value: 'sk-unbound-source',
},
],
},
};

const result = await applyConfigImport(bundle as any, 'skip', deps);

assert.deepEqual(setCreds, []);
assert.deepEqual(result.credentials, { applied: 0, skipped: 1 });
});

it('skips a credentials-only entry when the target slug belongs to another provider', async () => {
const { deps, setCreds } = makeDeps();
const bundle = {
schemaVersion: 1,
exportedAt: '',
appVersion: '0.1.0',
includedData: ['credentials'] as const,
data: {
credentials: [
{
slug: 'deepseek-main',
kind: 'api_key',
value: 'sk-openai-source',
connection: {
providerType: 'openai',
effectiveBaseUrl: 'https://api.openai.com/v1',
},
},
],
},
};

const result = await applyConfigImport(bundle as any, 'skip', deps);

assert.deepEqual(setCreds, []);
assert.deepEqual(result.credentials, { applied: 0, skipped: 1 });
});

it('skips a credentials-only entry when the target endpoint differs', async () => {
const target = conn('deepseek-main', {
baseUrl: 'https://target-relay.example/v1',
});
const { deps, setCreds } = makeDeps({
connectionStore: {
list: async () => [target],
save: async (connection) => connection,
},
});
const bundle = {
schemaVersion: 1,
exportedAt: '',
appVersion: '0.1.0',
includedData: ['credentials'] as const,
data: {
credentials: [
{
slug: 'deepseek-main',
kind: 'api_key',
value: 'sk-source-endpoint',
connection: {
providerType: 'deepseek',
effectiveBaseUrl: 'https://api.deepseek.com',
},
},
],
},
};

const result = await applyConfigImport(bundle as any, 'skip', deps);

assert.deepEqual(setCreds, []);
assert.deepEqual(result.credentials, { applied: 0, skipped: 1 });
});

it('restores the whole bundle when it carries a retained retired connection', async () => {
// A backup taken before the retirement still lists the connection, and the
// catalog refuses to create one. Before this was planned as skipped, the
Expand Down
Loading
Loading