You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by as...@apache.org on 2026/10/12 01:08:16 UTC

(maka) 02/04: fix(settings): preserve bot credentials through settings updates

This is an automated email from the ASF dual-hosted git repository.

Astro-Han pushed a commit to branch fix/channels-settings-roundtrip
in repository https://gitbox.apache.org/repos/asf/maka.git

commit 15451892c12ada2b76cb60c66c241a79df548c77
Author: AstroHan <le...@outlook.com>
AuthorDate: Sun Oct 11 22:28:43 2026 +1000

    fix(settings): preserve bot credentials through settings updates
    
    Generated-by: Codex
---
 .../__tests__/client-settings-ipc-main.test.ts     |  6 ++-
 .../main/__tests__/settings-ipc-helpers.test.ts    | 46 ++++++++++++++--------
 apps/desktop/src/main/bot-onboarding-main.ts       |  2 +
 apps/desktop/src/main/settings-ipc-helpers.ts      | 27 ++-----------
 .../core/src/__tests__/bot-chat-settings.test.ts   | 34 ++++++++++++++++
 packages/core/src/bot-chat-settings.ts             | 24 +++++------
 .../core/src/settings/__tests__/sensitive.test.ts  |  1 +
 packages/core/src/settings/network-settings.ts     |  5 ++-
 8 files changed, 86 insertions(+), 59 deletions(-)

diff --git a/apps/desktop/src/main/__tests__/client-settings-ipc-main.test.ts b/apps/desktop/src/main/__tests__/client-settings-ipc-main.test.ts
index 0bf6e0bc2d..e0f6d0d21f 100644
--- a/apps/desktop/src/main/__tests__/client-settings-ipc-main.test.ts
+++ b/apps/desktop/src/main/__tests__/client-settings-ipc-main.test.ts
@@ -19,6 +19,7 @@
 
 import assert from "node:assert/strict";
 import test from "node:test";
+import { SENSITIVE_PLACEHOLDER } from "@maka/core/settings/network-settings";
 import {
   createDefaultSettings,
   mergeSettings,
@@ -26,7 +27,7 @@ import {
 } from "@maka/core/settings";
 import { registerClientSettingsIpc } from "../client-settings-ipc-main.js";
 
-test("client settings updates filter Host policy and return newly submitted secrets", async () => {
+test("client settings updates filter Host policy and mask newly submitted secrets", async () => {
   const handlers = new Map<string, (...args: unknown[]) => unknown>();
   let settings = createDefaultSettings();
   let storedPatch: UpdateAppSettingsInput | undefined;
@@ -63,8 +64,9 @@ test("client settings updates filter Host policy and return newly submitted secr
   assert.equal((result as { settings: typeof settings }).settings.appearance.theme, "dark");
   assert.equal(
     (result as { settings: typeof settings }).settings.botChat.channels.telegram.token,
-    "fresh-bot-token",
+    SENSITIVE_PLACEHOLDER,
   );
+  assert.equal(settings.botChat.channels.telegram.token, "fresh-bot-token");
   assert.equal(settings.chatDefaults.permissionMode, "bypass");
   assert.equal(applied, 1);
 });
diff --git a/apps/desktop/src/main/__tests__/settings-ipc-helpers.test.ts b/apps/desktop/src/main/__tests__/settings-ipc-helpers.test.ts
index 3cbd2eb556..55bd8620d1 100644
--- a/apps/desktop/src/main/__tests__/settings-ipc-helpers.test.ts
+++ b/apps/desktop/src/main/__tests__/settings-ipc-helpers.test.ts
@@ -19,7 +19,11 @@
 
 import assert from "node:assert/strict";
 import { describe, test } from "node:test";
-import { createDefaultSettings, mergeSettings } from "@maka/core/settings";
+import { mkdtemp, rm } from "node:fs/promises";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { createSettingsStore } from "@maka/storage/settings-store";
+import { BOT_PROVIDERS, createDefaultSettings, mergeSettings } from "@maka/core/settings";
 import { SENSITIVE_PLACEHOLDER } from "@maka/core/settings/network-settings";
 import {
   buildSettingsUpdateResult,
@@ -65,20 +69,28 @@ describe("settings IPC helpers", () => {
     }
   });
 
-  test("reveals sensitive fields only when the current patch explicitly changes them", () => {
-    const settings = createDefaultSettings();
-    settings.botChat.channels.telegram.token = "new-bot-token";
-    settings.botChat.channels.feishu.appSecret = "stored-feishu-secret";
-
-    const masked = maskAppSettings(settings, {
-      botChat: { channels: { telegram: { token: "new-bot-token" } } },
-    });
-
-    assert.equal(masked.botChat.channels.telegram.token, "new-bot-token");
-    assert.equal(
-      masked.botChat.channels.feishu.appSecret,
-      SENSITIVE_PLACEHOLDER,
-    );
+  test("masks bot credentials in save responses and preserves them across masked saves and reloads", async () => {
+    const workspaceRoot = await mkdtemp(join(tmpdir(), "maka-bot-settings-"));
+    try {
+      const store = createSettingsStore(workspaceRoot);
+      for (const provider of BOT_PROVIDERS) {
+        const patch = { botChat: { channels: { [provider]: { token: "stored-token", appSecret: "stored-secret" } } } };
+        const configured = await store.update(patch);
+        const response = buildSettingsUpdateResult(configured, patch);
+        assert.equal(response.settings.botChat.channels[provider].token, SENSITIVE_PLACEHOLDER, provider);
+        assert.equal(response.settings.botChat.channels[provider].appSecret, SENSITIVE_PLACEHOLDER, provider);
+        await store.update({ botChat: response.settings.botChat });
+        const saved = await createSettingsStore(workspaceRoot).get();
+        assert.equal(saved.botChat.channels[provider].token, "stored-token", provider);
+        assert.equal(saved.botChat.channels[provider].appSecret, "stored-secret", provider);
+        await store.update({ botChat: { channels: { [provider]: { token: "", appSecret: "" } } } });
+        const cleared = await createSettingsStore(workspaceRoot).get();
+        assert.equal(cleared.botChat.channels[provider].token, "", provider);
+        assert.ok(!cleared.botChat.channels[provider].appSecret, provider);
+      }
+    } finally {
+      await rm(workspaceRoot, { recursive: true, force: true });
+    }
   });
 
   test("never reveals Tavily API key back to renderer, even on the save response", () => {
@@ -86,9 +98,9 @@ describe("settings IPC helpers", () => {
       webSearch: { providers: { tavily: { apiKey: "tvly-new-secret" } } },
     });
 
-    const masked = maskAppSettings(settings, {
+    const masked = buildSettingsUpdateResult(settings, {
       webSearch: { providers: { tavily: { apiKey: "tvly-new-secret" } } },
-    });
+    }).settings;
 
     assert.equal(
       masked.webSearch.providers.tavily.apiKey,
diff --git a/apps/desktop/src/main/bot-onboarding-main.ts b/apps/desktop/src/main/bot-onboarding-main.ts
index 3f3135ca00..9df7ece3a2 100644
--- a/apps/desktop/src/main/bot-onboarding-main.ts
+++ b/apps/desktop/src/main/bot-onboarding-main.ts
@@ -376,6 +376,8 @@ export class BotOnboardingService {
     ownedFields: ReadonlyArray<keyof BotChannelSettings>,
   ): Promise<void> {
     const previousValues = pickChannelFields(previousChannel, ownedFields);
+    // An absent pre-onboarding secret needs an explicit clear on rollback.
+    if ('appSecret' in previousValues) previousValues.appSecret ??= '';
     const rollbackPatch: UpdateAppSettingsInput = {
       botChat: { channels: { [provider]: previousValues } },
     };
diff --git a/apps/desktop/src/main/settings-ipc-helpers.ts b/apps/desktop/src/main/settings-ipc-helpers.ts
index c633809d0f..73f81fb94b 100644
--- a/apps/desktop/src/main/settings-ipc-helpers.ts
+++ b/apps/desktop/src/main/settings-ipc-helpers.ts
@@ -29,7 +29,6 @@ import type { BotProvider } from '@maka/core/bot-chat-settings';
 import { botDisplayLabel } from '@maka/core/bot-events';
 import { generalizedErrorMessage, redactSecrets } from '@maka/core/redaction';
 import {
-  SENSITIVE_PLACEHOLDER,
   maskSensitive,
   type TestProxyResult,
 } from "@maka/core/settings/network-settings";
@@ -72,15 +71,12 @@ export function proxyTestFailure(result: TestProxyResult): {
 
 export function maskAppSettings(
   settings: RuntimeHostAppSettings,
-  revealPatch?: UpdateAppSettingsInput,
 ): RuntimeHostAppSettings;
 export function maskAppSettings(
   settings: AppSettings,
-  revealPatch?: UpdateAppSettingsInput,
 ): AppSettings;
 export function maskAppSettings(
   settings: AppSettings,
-  revealPatch: UpdateAppSettingsInput = {},
 ): AppSettings {
   return {
     ...settings,
@@ -92,17 +88,8 @@ export function maskAppSettings(
           provider,
           {
             ...channel,
-            token: shouldReveal(
-              revealPatch.botChat?.channels?.[provider as BotProvider]?.token,
-            )
-              ? channel.token
-              : (maskSensitive(channel.token) ?? ""),
-            appSecret: shouldReveal(
-              revealPatch.botChat?.channels?.[provider as BotProvider]
-                ?.appSecret,
-            )
-              ? channel.appSecret
-              : (maskSensitive(channel.appSecret) ?? ""),
+            token: maskSensitive(channel.token) ?? "",
+            appSecret: maskSensitive(channel.appSecret) ?? "",
           },
         ]),
       ) as AppSettings["botChat"]["channels"],
@@ -182,19 +169,11 @@ export function buildSettingsUpdateResult(
 ): UpdateAppSettingsResult {
   const personalization = collectPersonalizationWarnings(patch.personalization);
   return {
-    settings: maskAppSettings(settings, patch),
+    settings: maskAppSettings(settings),
     ...(personalization.length ? { warnings: { personalization } } : {}),
   };
 }
 
-function shouldReveal(value: string | undefined): boolean {
-  return (
-    typeof value === "string" &&
-    value.length > 0 &&
-    value !== SENSITIVE_PLACEHOLDER
-  );
-}
-
 export function toSettingsTestResult(
   provider: BotProvider,
   result: BotTestResult,
diff --git a/packages/core/src/__tests__/bot-chat-settings.test.ts b/packages/core/src/__tests__/bot-chat-settings.test.ts
index aced836c8c..75adfeb501 100644
--- a/packages/core/src/__tests__/bot-chat-settings.test.ts
+++ b/packages/core/src/__tests__/bot-chat-settings.test.ts
@@ -21,12 +21,46 @@ import assert from 'node:assert/strict';
 import { describe, test } from 'node:test';
 import {
   createDefaultBotChatSettings,
+  BOT_PROVIDERS,
   mergeBotChatSettings,
   normalizeBotChatSettings,
   parseAllowedUserIdsFromText,
 } from '../bot-chat-settings.js';
+import { SENSITIVE_PLACEHOLDER } from '../settings/network-settings.js';
+import { MASKED_TOKEN_SENTINEL } from '../web-search.js';
 
 describe('bot chat settings owner', () => {
+  test('preserves masked credentials and supports replacement, omission and explicit clearing', () => {
+    for (const provider of BOT_PROVIDERS) {
+      const configured = mergeBotChatSettings(createDefaultBotChatSettings(), {
+        channels: { [provider]: { token: 'stored-token', appSecret: 'stored-secret' } },
+      });
+      for (const mask of [SENSITIVE_PLACEHOLDER, MASKED_TOKEN_SENTINEL]) {
+        const saved = mergeBotChatSettings(configured, {
+          channels: { [provider]: { enabled: true, token: mask, appSecret: mask } },
+        });
+        assert.equal(saved.channels[provider].token, 'stored-token', provider);
+        assert.equal(saved.channels[provider].appSecret, 'stored-secret', provider);
+        assert.equal(saved.channels[provider].enabled, true, provider);
+      }
+      const omitted = mergeBotChatSettings(configured, {
+        channels: { [provider]: { token: undefined, appSecret: undefined } },
+      });
+      assert.equal(omitted.channels[provider].token, 'stored-token', provider);
+      assert.equal(omitted.channels[provider].appSecret, 'stored-secret', provider);
+      const replaced = mergeBotChatSettings(configured, {
+        channels: { [provider]: { token: 'new-token', appSecret: 'new-secret' } },
+      });
+      assert.equal(replaced.channels[provider].token, 'new-token', provider);
+      assert.equal(replaced.channels[provider].appSecret, 'new-secret', provider);
+      const cleared = mergeBotChatSettings(configured, {
+        channels: { [provider]: { token: '', appSecret: '' } },
+      });
+      assert.equal(cleared.channels[provider].token, '', provider);
+      assert.ok(!cleared.channels[provider].appSecret, provider);
+    }
+  });
+
   test('normalizes an explicitly patched allowlist without touching it on unrelated patches', () => {
     const defaults = createDefaultBotChatSettings();
     const withAllowlist = mergeBotChatSettings(defaults, {
diff --git a/packages/core/src/bot-chat-settings.ts b/packages/core/src/bot-chat-settings.ts
index 83376311c4..2e80c8444d 100644
--- a/packages/core/src/bot-chat-settings.ts
+++ b/packages/core/src/bot-chat-settings.ts
@@ -17,6 +17,8 @@
  * under the License.
  */
 
+import { applySensitivePatch } from './settings/network-settings.js';
+
 export type BotProvider =
   | 'telegram'
   | 'feishu'
@@ -59,15 +61,9 @@ export interface BotChannelSettings {
   lastTestAt?: number;
   lastError?: string;
   /**
-   * PR-BOT-USER-ALLOWLIST-0 (external bot research): platform-native user IDs
-   * permitted to message this bot. `undefined` or empty means no
-   * restriction (preserves the V0.1 behavior for existing installs).
-   * When non-empty, the bot bridge silently drops inbound messages from
-   * any other user — no acknowledgement is sent back, so unauthorized
-   * scanners cannot use bounce behavior to enumerate the bot's policy.
-   *
-   * Stored as a string array since Telegram IDs are 64-bit and JS
-   * `Number` loses precision past 2^53.
+   * Platform-native sender IDs. An absent or empty list allows everyone.
+   * A configured list is enforced by the shared adapter before routing.
+   * IDs stay strings to preserve Telegram's 64-bit values.
    */
   allowedUserIds?: ReadonlyArray<string>;
 }
@@ -149,15 +145,13 @@ export function mergeBotChatSettings(
       ...current.channels,
       ...Object.fromEntries(
         Object.entries(patch?.channels ?? {}).map(([provider, channelPatch]) => {
+          const previous = current.channels[provider as BotProvider];
           const merged = {
-            ...current.channels[provider as BotProvider],
+            ...previous,
             ...channelPatch,
+            token: applySensitivePatch(previous?.token, channelPatch?.token) ?? '',
+            appSecret: applySensitivePatch(previous?.appSecret, channelPatch?.appSecret),
           };
-          // PR-BOT-USER-ALLOWLIST-0: keep the persisted allowlist
-          // shape consistent on every save, not only on initial load.
-          // The renderer textarea sends an array; the normalize step
-          // trims/dedups/caps and downgrades the empty case to
-          // `undefined` (the V0.1 "no restriction" sentinel).
           if ('allowedUserIds' in (channelPatch ?? {})) {
             const normalized = normalizeAllowedUserIds(merged.allowedUserIds);
             if (normalized) merged.allowedUserIds = normalized;
diff --git a/packages/core/src/settings/__tests__/sensitive.test.ts b/packages/core/src/settings/__tests__/sensitive.test.ts
index be28cb7f94..222ea57d4f 100644
--- a/packages/core/src/settings/__tests__/sensitive.test.ts
+++ b/packages/core/src/settings/__tests__/sensitive.test.ts
@@ -25,6 +25,7 @@ describe('applySensitivePatch', () => {
   test('handles plaintext, placeholder, empty string, and undefined', () => {
     assert.strictEqual(applySensitivePatch('old', 'new'), 'new');
     assert.strictEqual(applySensitivePatch('old', '••••••••'), 'old');
+    assert.strictEqual(applySensitivePatch('old', '••••••'), 'old');
     assert.strictEqual(applySensitivePatch('old', ''), undefined);
     assert.strictEqual(applySensitivePatch('old', undefined), 'old');
   });
diff --git a/packages/core/src/settings/network-settings.ts b/packages/core/src/settings/network-settings.ts
index 25f8ee4aa8..7fdf8002db 100644
--- a/packages/core/src/settings/network-settings.ts
+++ b/packages/core/src/settings/network-settings.ts
@@ -17,6 +17,8 @@
  * under the License.
  */
 
+import { MASKED_TOKEN_SENTINEL } from '../web-search.js';
+
 export type ProxyType = 'http' | 'https' | 'socks5';
 
 export const SENSITIVE_PLACEHOLDER = '••••••••' as const;
@@ -26,7 +28,8 @@ export function applySensitivePatch(
   prev: string | undefined,
   next: Sensitive | undefined,
 ): string | undefined {
-  if (next === undefined || next === SENSITIVE_PLACEHOLDER) return prev;
+  if (next === undefined || next === SENSITIVE_PLACEHOLDER || next === MASKED_TOKEN_SENTINEL)
+    return prev;
   if (next === '') return undefined;
   return next;
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org