From 314164fb8a31fed1c2fed9d8b14f531b8fc53eac Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 2 Jan 2026 16:56:27 +0100 Subject: [PATCH] chore: fix lint and add gateway auth tests --- .../Clawdis/GatewayEndpointStore.swift | 77 ++++++++++++------- .../Clawdis/GatewayLaunchAgentManager.swift | 4 + .../GatewayEndpointStoreTests.swift | 38 +++++++++ .../LowCoverageHelperTests.swift | 3 + src/auto-reply/reply.ts | 4 +- src/cli/program.ts | 4 +- src/commands/doctor.ts | 10 ++- src/config/config.test.ts | 4 +- src/config/config.ts | 4 +- src/gateway/server.ts | 3 +- 10 files changed, 115 insertions(+), 36 deletions(-) diff --git a/apps/macos/Sources/Clawdis/GatewayEndpointStore.swift b/apps/macos/Sources/Clawdis/GatewayEndpointStore.swift index 7be78c407..1c2aa841d 100644 --- a/apps/macos/Sources/Clawdis/GatewayEndpointStore.swift +++ b/apps/macos/Sources/Clawdis/GatewayEndpointStore.swift @@ -26,40 +26,51 @@ actor GatewayEndpointStore { mode: { await MainActor.run { AppStateStore.shared.connectionMode } }, token: { ProcessInfo.processInfo.environment["CLAWDIS_GATEWAY_TOKEN"] }, password: { - let raw = ProcessInfo.processInfo.environment["CLAWDIS_GATEWAY_PASSWORD"] ?? "" - let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines) - if !trimmed.isEmpty { - return trimmed - } let root = ClawdisConfigFile.loadDict() - if CommandResolver.connectionModeIsRemote() { - if let gateway = root["gateway"] as? [String: Any], - let remote = gateway["remote"] as? [String: Any], - let password = remote["password"] as? String - { - let pw = password.trimmingCharacters(in: .whitespacesAndNewlines) - if !pw.isEmpty { - return pw - } - } - return nil - } - if let gateway = root["gateway"] as? [String: Any], - let auth = gateway["auth"] as? [String: Any], - let password = auth["password"] as? String - { - let pw = password.trimmingCharacters(in: .whitespacesAndNewlines) - if !pw.isEmpty { - return pw - } - } - return nil + return GatewayEndpointStore.resolveGatewayPassword( + isRemote: CommandResolver.connectionModeIsRemote(), + root: root, + env: ProcessInfo.processInfo.environment) }, localPort: { GatewayEnvironment.gatewayPort() }, remotePortIfRunning: { await RemoteTunnelManager.shared.controlTunnelPortIfRunning() }, ensureRemoteTunnel: { try await RemoteTunnelManager.shared.ensureControlTunnel() }) } + private static func resolveGatewayPassword( + isRemote: Bool, + root: [String: Any], + env: [String: String] + ) -> String? { + let raw = env["CLAWDIS_GATEWAY_PASSWORD"] ?? "" + let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines) + if !trimmed.isEmpty { + return trimmed + } + if isRemote { + if let gateway = root["gateway"] as? [String: Any], + let remote = gateway["remote"] as? [String: Any], + let password = remote["password"] as? String + { + let pw = password.trimmingCharacters(in: .whitespacesAndNewlines) + if !pw.isEmpty { + return pw + } + } + return nil + } + if let gateway = root["gateway"] as? [String: Any], + let auth = gateway["auth"] as? [String: Any], + let password = auth["password"] as? String + { + let pw = password.trimmingCharacters(in: .whitespacesAndNewlines) + if !pw.isEmpty { + return pw + } + } + return nil + } + private let deps: Deps private let logger = Logger(subsystem: "com.steipete.clawdis", category: "gateway-endpoint") @@ -193,3 +204,15 @@ actor GatewayEndpointStore { } } } + +#if DEBUG +extension GatewayEndpointStore { + static func _testResolveGatewayPassword( + isRemote: Bool, + root: [String: Any], + env: [String: String] + ) -> String? { + self.resolveGatewayPassword(isRemote: isRemote, root: root, env: env) + } +} +#endif diff --git a/apps/macos/Sources/Clawdis/GatewayLaunchAgentManager.swift b/apps/macos/Sources/Clawdis/GatewayLaunchAgentManager.swift index 7d0f04995..eeee7f344 100644 --- a/apps/macos/Sources/Clawdis/GatewayLaunchAgentManager.swift +++ b/apps/macos/Sources/Clawdis/GatewayLaunchAgentManager.swift @@ -226,5 +226,9 @@ extension GatewayLaunchAgentManager { static func _testPreferredGatewayToken() -> String? { self.preferredGatewayToken() } + + static func _testEscapePlistValue(_ raw: String) -> String { + self.escapePlistValue(raw) + } } #endif diff --git a/apps/macos/Tests/ClawdisIPCTests/GatewayEndpointStoreTests.swift b/apps/macos/Tests/ClawdisIPCTests/GatewayEndpointStoreTests.swift index 6061fc8f9..891fb35cc 100644 --- a/apps/macos/Tests/ClawdisIPCTests/GatewayEndpointStoreTests.swift +++ b/apps/macos/Tests/ClawdisIPCTests/GatewayEndpointStoreTests.swift @@ -82,6 +82,44 @@ import Testing #expect(password == "pw") } + @Test func resolvesGatewayPasswordByMode() { + let root: [String: Any] = [ + "gateway": [ + "auth": ["password": " local "], + "remote": ["password": " remote "], + ], + ] + let env: [String: String] = [:] + + #expect(GatewayEndpointStore._testResolveGatewayPassword( + isRemote: false, + root: root, + env: env) == "local") + #expect(GatewayEndpointStore._testResolveGatewayPassword( + isRemote: true, + root: root, + env: env) == "remote") + } + + @Test func gatewayPasswordEnvOverridesConfig() { + let root: [String: Any] = [ + "gateway": [ + "auth": ["password": "local"], + "remote": ["password": "remote"], + ], + ] + let env = ["CLAWDIS_GATEWAY_PASSWORD": " env "] + + #expect(GatewayEndpointStore._testResolveGatewayPassword( + isRemote: false, + root: root, + env: env) == "env") + #expect(GatewayEndpointStore._testResolveGatewayPassword( + isRemote: true, + root: root, + env: env) == "env") + } + @Test func unconfiguredModeRejectsConfig() async { let mode = ModeBox(.unconfigured) let store = GatewayEndpointStore(deps: .init( diff --git a/apps/macos/Tests/ClawdisIPCTests/LowCoverageHelperTests.swift b/apps/macos/Tests/ClawdisIPCTests/LowCoverageHelperTests.swift index 92a7b891c..f9ae977da 100644 --- a/apps/macos/Tests/ClawdisIPCTests/LowCoverageHelperTests.swift +++ b/apps/macos/Tests/ClawdisIPCTests/LowCoverageHelperTests.swift @@ -114,6 +114,9 @@ struct LowCoverageHelperTests { setenv(keyToken, " secret ", 1) #expect(GatewayLaunchAgentManager._testPreferredGatewayBind() == "lan") #expect(GatewayLaunchAgentManager._testPreferredGatewayToken() == "secret") + #expect( + GatewayLaunchAgentManager._testEscapePlistValue("a&b\"'") == + "a&b<c>"'") #expect(GatewayLaunchAgentManager._testGatewayExecutablePath(bundlePath: "/App") == "/App/Contents/Resources/Relay/clawdis") #expect(GatewayLaunchAgentManager._testRelayDir(bundlePath: "/App") == "/App/Contents/Resources/Relay") diff --git a/src/auto-reply/reply.ts b/src/auto-reply/reply.ts index b3c380a38..47ec99056 100644 --- a/src/auto-reply/reply.ts +++ b/src/auto-reply/reply.ts @@ -854,7 +854,9 @@ export async function getReplyFromConfig( const from = (ctx.From ?? "").replace(/^whatsapp:/, ""); const to = (ctx.To ?? "").replace(/^whatsapp:/, ""); const defaultAllowFrom = - isWhatsAppSurface && (!configuredAllowFrom || configuredAllowFrom.length === 0) && to + isWhatsAppSurface && + (!configuredAllowFrom || configuredAllowFrom.length === 0) && + to ? [to] : undefined; const allowFrom = diff --git a/src/cli/program.ts b/src/cli/program.ts index 926094987..a6961d339 100644 --- a/src/cli/program.ts +++ b/src/cli/program.ts @@ -10,11 +10,11 @@ import { sessionsCommand } from "../commands/sessions.js"; import { setupCommand } from "../commands/setup.js"; import { statusCommand } from "../commands/status.js"; import { updateCommand } from "../commands/update.js"; +import { readConfigFileSnapshot } from "../config/config.js"; import { danger, setVerbose } from "../globals.js"; import { loginWeb, logoutWeb } from "../provider-web.js"; import { defaultRuntime } from "../runtime.js"; import { VERSION } from "../version.js"; -import { readConfigFileSnapshot } from "../config/config.js"; import { registerBrowserCli } from "./browser-cli.js"; import { registerCanvasCli } from "./canvas-cli.js"; import { registerCronCli } from "./cron-cli.js"; @@ -79,7 +79,7 @@ export function buildProgram() { .join("\n"); defaultRuntime.error( danger( - `Legacy config entries detected. Run \"clawdis doctor\" (or ask your agent) to migrate.\n${issues}`, + `Legacy config entries detected. Run "clawdis doctor" (or ask your agent) to migrate.\n${issues}`, ), ); process.exit(1); diff --git a/src/commands/doctor.ts b/src/commands/doctor.ts index 22696e9e8..635bd28a0 100644 --- a/src/commands/doctor.ts +++ b/src/commands/doctor.ts @@ -30,7 +30,11 @@ export async function doctorCommand(runtime: RuntimeEnv = defaultRuntime) { const snapshot = await readConfigFileSnapshot(); let cfg: ClawdisConfig = snapshot.valid ? snapshot.config : {}; - if (snapshot.exists && !snapshot.valid && snapshot.legacyIssues.length === 0) { + if ( + snapshot.exists && + !snapshot.valid && + snapshot.legacyIssues.length === 0 + ) { note("Config invalid; doctor will run with defaults.", "Config"); } @@ -50,7 +54,9 @@ export async function doctorCommand(runtime: RuntimeEnv = defaultRuntime) { ); if (migrate) { // Legacy migration (2026-01-02, commit: 16420e5b) — normalize per-provider allowlists; move WhatsApp gating into whatsapp.allowFrom. - const { config: migrated, changes } = migrateLegacyConfig(snapshot.parsed); + const { config: migrated, changes } = migrateLegacyConfig( + snapshot.parsed, + ); if (changes.length > 0) { note(changes.join("\n"), "Doctor changes"); } diff --git a/src/config/config.test.ts b/src/config/config.test.ts index 4d46782fe..6ababcbe4 100644 --- a/src/config/config.test.ts +++ b/src/config/config.test.ts @@ -508,7 +508,9 @@ describe("legacy config detection", () => { const res = migrateLegacyConfig({ routing: { allowFrom: ["+15555550123"] }, }); - expect(res.changes).toContain("Moved routing.allowFrom → whatsapp.allowFrom."); + expect(res.changes).toContain( + "Moved routing.allowFrom → whatsapp.allowFrom.", + ); expect(res.config?.whatsapp?.allowFrom).toEqual(["+15555550123"]); expect(res.config?.routing?.allowFrom).toBeUndefined(); }); diff --git a/src/config/config.ts b/src/config/config.ts index cd979570b..d58142fcd 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -1217,7 +1217,9 @@ const LEGACY_CONFIG_MIGRATIONS: LegacyConfigMigration[] = [ whatsapp.allowFrom = allowFrom; changes.push("Moved routing.allowFrom → whatsapp.allowFrom."); } else { - changes.push("Removed routing.allowFrom (whatsapp.allowFrom already set)."); + changes.push( + "Removed routing.allowFrom (whatsapp.allowFrom already set).", + ); } delete (routing as Record).allowFrom; diff --git a/src/gateway/server.ts b/src/gateway/server.ts index e3b89bf2e..b27b29f25 100644 --- a/src/gateway/server.ts +++ b/src/gateway/server.ts @@ -660,7 +660,6 @@ type DedupeEntry = { error?: ErrorShape; }; - function formatForLog(value: unknown): string { try { if (value instanceof Error) { @@ -1334,7 +1333,7 @@ export async function startGatewayServer( ); if (!migrated) { throw new Error( - "Legacy config entries detected but auto-migration failed. Run \"clawdis doctor\" to migrate.", + 'Legacy config entries detected but auto-migration failed. Run "clawdis doctor" to migrate.', ); } await writeConfigFile(migrated);