chore: fix lint and add gateway auth tests
This commit is contained in:
@@ -26,13 +26,28 @@ actor GatewayEndpointStore {
|
|||||||
mode: { await MainActor.run { AppStateStore.shared.connectionMode } },
|
mode: { await MainActor.run { AppStateStore.shared.connectionMode } },
|
||||||
token: { ProcessInfo.processInfo.environment["CLAWDIS_GATEWAY_TOKEN"] },
|
token: { ProcessInfo.processInfo.environment["CLAWDIS_GATEWAY_TOKEN"] },
|
||||||
password: {
|
password: {
|
||||||
let raw = ProcessInfo.processInfo.environment["CLAWDIS_GATEWAY_PASSWORD"] ?? ""
|
let root = ClawdisConfigFile.loadDict()
|
||||||
|
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)
|
let trimmed = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
if !trimmed.isEmpty {
|
if !trimmed.isEmpty {
|
||||||
return trimmed
|
return trimmed
|
||||||
}
|
}
|
||||||
let root = ClawdisConfigFile.loadDict()
|
if isRemote {
|
||||||
if CommandResolver.connectionModeIsRemote() {
|
|
||||||
if let gateway = root["gateway"] as? [String: Any],
|
if let gateway = root["gateway"] as? [String: Any],
|
||||||
let remote = gateway["remote"] as? [String: Any],
|
let remote = gateway["remote"] as? [String: Any],
|
||||||
let password = remote["password"] as? String
|
let password = remote["password"] as? String
|
||||||
@@ -54,10 +69,6 @@ actor GatewayEndpointStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
},
|
|
||||||
localPort: { GatewayEnvironment.gatewayPort() },
|
|
||||||
remotePortIfRunning: { await RemoteTunnelManager.shared.controlTunnelPortIfRunning() },
|
|
||||||
ensureRemoteTunnel: { try await RemoteTunnelManager.shared.ensureControlTunnel() })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private let deps: Deps
|
private let deps: Deps
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -226,5 +226,9 @@ extension GatewayLaunchAgentManager {
|
|||||||
static func _testPreferredGatewayToken() -> String? {
|
static func _testPreferredGatewayToken() -> String? {
|
||||||
self.preferredGatewayToken()
|
self.preferredGatewayToken()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func _testEscapePlistValue(_ raw: String) -> String {
|
||||||
|
self.escapePlistValue(raw)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -82,6 +82,44 @@ import Testing
|
|||||||
#expect(password == "pw")
|
#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 {
|
@Test func unconfiguredModeRejectsConfig() async {
|
||||||
let mode = ModeBox(.unconfigured)
|
let mode = ModeBox(.unconfigured)
|
||||||
let store = GatewayEndpointStore(deps: .init(
|
let store = GatewayEndpointStore(deps: .init(
|
||||||
|
|||||||
@@ -114,6 +114,9 @@ struct LowCoverageHelperTests {
|
|||||||
setenv(keyToken, " secret ", 1)
|
setenv(keyToken, " secret ", 1)
|
||||||
#expect(GatewayLaunchAgentManager._testPreferredGatewayBind() == "lan")
|
#expect(GatewayLaunchAgentManager._testPreferredGatewayBind() == "lan")
|
||||||
#expect(GatewayLaunchAgentManager._testPreferredGatewayToken() == "secret")
|
#expect(GatewayLaunchAgentManager._testPreferredGatewayToken() == "secret")
|
||||||
|
#expect(
|
||||||
|
GatewayLaunchAgentManager._testEscapePlistValue("a&b<c>\"'") ==
|
||||||
|
"a&b<c>"'")
|
||||||
|
|
||||||
#expect(GatewayLaunchAgentManager._testGatewayExecutablePath(bundlePath: "/App") == "/App/Contents/Resources/Relay/clawdis")
|
#expect(GatewayLaunchAgentManager._testGatewayExecutablePath(bundlePath: "/App") == "/App/Contents/Resources/Relay/clawdis")
|
||||||
#expect(GatewayLaunchAgentManager._testRelayDir(bundlePath: "/App") == "/App/Contents/Resources/Relay")
|
#expect(GatewayLaunchAgentManager._testRelayDir(bundlePath: "/App") == "/App/Contents/Resources/Relay")
|
||||||
|
|||||||
@@ -854,7 +854,9 @@ export async function getReplyFromConfig(
|
|||||||
const from = (ctx.From ?? "").replace(/^whatsapp:/, "");
|
const from = (ctx.From ?? "").replace(/^whatsapp:/, "");
|
||||||
const to = (ctx.To ?? "").replace(/^whatsapp:/, "");
|
const to = (ctx.To ?? "").replace(/^whatsapp:/, "");
|
||||||
const defaultAllowFrom =
|
const defaultAllowFrom =
|
||||||
isWhatsAppSurface && (!configuredAllowFrom || configuredAllowFrom.length === 0) && to
|
isWhatsAppSurface &&
|
||||||
|
(!configuredAllowFrom || configuredAllowFrom.length === 0) &&
|
||||||
|
to
|
||||||
? [to]
|
? [to]
|
||||||
: undefined;
|
: undefined;
|
||||||
const allowFrom =
|
const allowFrom =
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ import { sessionsCommand } from "../commands/sessions.js";
|
|||||||
import { setupCommand } from "../commands/setup.js";
|
import { setupCommand } from "../commands/setup.js";
|
||||||
import { statusCommand } from "../commands/status.js";
|
import { statusCommand } from "../commands/status.js";
|
||||||
import { updateCommand } from "../commands/update.js";
|
import { updateCommand } from "../commands/update.js";
|
||||||
|
import { readConfigFileSnapshot } from "../config/config.js";
|
||||||
import { danger, setVerbose } from "../globals.js";
|
import { danger, setVerbose } from "../globals.js";
|
||||||
import { loginWeb, logoutWeb } from "../provider-web.js";
|
import { loginWeb, logoutWeb } from "../provider-web.js";
|
||||||
import { defaultRuntime } from "../runtime.js";
|
import { defaultRuntime } from "../runtime.js";
|
||||||
import { VERSION } from "../version.js";
|
import { VERSION } from "../version.js";
|
||||||
import { readConfigFileSnapshot } from "../config/config.js";
|
|
||||||
import { registerBrowserCli } from "./browser-cli.js";
|
import { registerBrowserCli } from "./browser-cli.js";
|
||||||
import { registerCanvasCli } from "./canvas-cli.js";
|
import { registerCanvasCli } from "./canvas-cli.js";
|
||||||
import { registerCronCli } from "./cron-cli.js";
|
import { registerCronCli } from "./cron-cli.js";
|
||||||
@@ -79,7 +79,7 @@ export function buildProgram() {
|
|||||||
.join("\n");
|
.join("\n");
|
||||||
defaultRuntime.error(
|
defaultRuntime.error(
|
||||||
danger(
|
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);
|
process.exit(1);
|
||||||
|
|||||||
@@ -30,7 +30,11 @@ export async function doctorCommand(runtime: RuntimeEnv = defaultRuntime) {
|
|||||||
|
|
||||||
const snapshot = await readConfigFileSnapshot();
|
const snapshot = await readConfigFileSnapshot();
|
||||||
let cfg: ClawdisConfig = snapshot.valid ? snapshot.config : {};
|
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");
|
note("Config invalid; doctor will run with defaults.", "Config");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,7 +54,9 @@ export async function doctorCommand(runtime: RuntimeEnv = defaultRuntime) {
|
|||||||
);
|
);
|
||||||
if (migrate) {
|
if (migrate) {
|
||||||
// Legacy migration (2026-01-02, commit: 16420e5b) — normalize per-provider allowlists; move WhatsApp gating into whatsapp.allowFrom.
|
// 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) {
|
if (changes.length > 0) {
|
||||||
note(changes.join("\n"), "Doctor changes");
|
note(changes.join("\n"), "Doctor changes");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -508,7 +508,9 @@ describe("legacy config detection", () => {
|
|||||||
const res = migrateLegacyConfig({
|
const res = migrateLegacyConfig({
|
||||||
routing: { allowFrom: ["+15555550123"] },
|
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?.whatsapp?.allowFrom).toEqual(["+15555550123"]);
|
||||||
expect(res.config?.routing?.allowFrom).toBeUndefined();
|
expect(res.config?.routing?.allowFrom).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1217,7 +1217,9 @@ const LEGACY_CONFIG_MIGRATIONS: LegacyConfigMigration[] = [
|
|||||||
whatsapp.allowFrom = allowFrom;
|
whatsapp.allowFrom = allowFrom;
|
||||||
changes.push("Moved routing.allowFrom → whatsapp.allowFrom.");
|
changes.push("Moved routing.allowFrom → whatsapp.allowFrom.");
|
||||||
} else {
|
} else {
|
||||||
changes.push("Removed routing.allowFrom (whatsapp.allowFrom already set).");
|
changes.push(
|
||||||
|
"Removed routing.allowFrom (whatsapp.allowFrom already set).",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
delete (routing as Record<string, unknown>).allowFrom;
|
delete (routing as Record<string, unknown>).allowFrom;
|
||||||
|
|||||||
@@ -660,7 +660,6 @@ type DedupeEntry = {
|
|||||||
error?: ErrorShape;
|
error?: ErrorShape;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
function formatForLog(value: unknown): string {
|
function formatForLog(value: unknown): string {
|
||||||
try {
|
try {
|
||||||
if (value instanceof Error) {
|
if (value instanceof Error) {
|
||||||
@@ -1334,7 +1333,7 @@ export async function startGatewayServer(
|
|||||||
);
|
);
|
||||||
if (!migrated) {
|
if (!migrated) {
|
||||||
throw new Error(
|
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);
|
await writeConfigFile(migrated);
|
||||||
|
|||||||
Reference in New Issue
Block a user