mirror of https://github.com/openclaw/openclaw.git
474 lines
13 KiB
TypeScript
474 lines
13 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import type { OpenClawConfig } from "../config/config.js";
|
|
import {
|
|
resolveGatewayConnectionAuth,
|
|
resolveGatewayConnectionAuthFromConfig,
|
|
type GatewayConnectionAuthOptions,
|
|
} from "./connection-auth.js";
|
|
|
|
type ResolvedAuth = { token?: string; password?: string };
|
|
|
|
type ConnectionAuthCase = {
|
|
name: string;
|
|
cfg: OpenClawConfig;
|
|
env: NodeJS.ProcessEnv;
|
|
options?: Partial<Omit<GatewayConnectionAuthOptions, "config" | "env">>;
|
|
expected: ResolvedAuth;
|
|
};
|
|
|
|
function cfg(input: Partial<OpenClawConfig>): OpenClawConfig {
|
|
return input as OpenClawConfig;
|
|
}
|
|
|
|
function createRemoteModeConfig() {
|
|
return {
|
|
gateway: {
|
|
mode: "remote" as const,
|
|
auth: {
|
|
token: "local-token",
|
|
password: "local-password", // pragma: allowlist secret
|
|
},
|
|
remote: {
|
|
url: "wss://remote.example",
|
|
token: "remote-token",
|
|
password: "remote-password", // pragma: allowlist secret
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
function createUnresolvedLocalAuthConfig(params: {
|
|
mode: "token" | "password";
|
|
id: string;
|
|
remoteFallback: string;
|
|
}) {
|
|
return cfg({
|
|
gateway: {
|
|
mode: "local",
|
|
auth: {
|
|
mode: params.mode,
|
|
[params.mode]: { source: "env", provider: "default", id: params.id },
|
|
},
|
|
remote: {
|
|
[params.mode]: params.remoteFallback,
|
|
},
|
|
},
|
|
secrets: {
|
|
providers: {
|
|
default: { source: "env" },
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
async function expectFailClosedOnUnresolvedLocalAuth(config: OpenClawConfig, path: string) {
|
|
await expect(
|
|
resolveGatewayConnectionAuth({
|
|
config,
|
|
env: {} as NodeJS.ProcessEnv,
|
|
includeLegacyEnv: false,
|
|
}),
|
|
).rejects.toThrow(path);
|
|
expect(() =>
|
|
resolveGatewayConnectionAuthFromConfig({
|
|
cfg: config,
|
|
env: {} as NodeJS.ProcessEnv,
|
|
includeLegacyEnv: false,
|
|
}),
|
|
).toThrow(path);
|
|
}
|
|
|
|
const DEFAULT_ENV = {
|
|
OPENCLAW_GATEWAY_TOKEN: "env-token",
|
|
OPENCLAW_GATEWAY_PASSWORD: "env-password", // pragma: allowlist secret
|
|
} as NodeJS.ProcessEnv;
|
|
|
|
describe("resolveGatewayConnectionAuth", () => {
|
|
const cases: ConnectionAuthCase[] = [
|
|
{
|
|
name: "local mode defaults to env-first token/password",
|
|
cfg: cfg({
|
|
gateway: {
|
|
mode: "local",
|
|
auth: {
|
|
token: "config-token",
|
|
password: "config-password", // pragma: allowlist secret
|
|
},
|
|
remote: {
|
|
token: "remote-token",
|
|
password: "remote-password", // pragma: allowlist secret
|
|
},
|
|
},
|
|
}),
|
|
env: DEFAULT_ENV,
|
|
expected: {
|
|
token: "env-token",
|
|
password: "env-password", // pragma: allowlist secret
|
|
},
|
|
},
|
|
{
|
|
name: "local mode supports config-first token/password",
|
|
cfg: cfg({
|
|
gateway: {
|
|
mode: "local",
|
|
auth: {
|
|
token: "config-token",
|
|
password: "config-password", // pragma: allowlist secret
|
|
},
|
|
},
|
|
}),
|
|
env: DEFAULT_ENV,
|
|
options: {
|
|
localTokenPrecedence: "config-first",
|
|
localPasswordPrecedence: "config-first", // pragma: allowlist secret
|
|
},
|
|
expected: {
|
|
token: "config-token",
|
|
password: "config-password", // pragma: allowlist secret
|
|
},
|
|
},
|
|
{
|
|
name: "local mode precedence can mix env-first token with config-first password",
|
|
cfg: cfg({
|
|
gateway: {
|
|
mode: "local",
|
|
auth: {},
|
|
remote: {
|
|
token: "remote-token",
|
|
password: "remote-password", // pragma: allowlist secret
|
|
},
|
|
},
|
|
}),
|
|
env: DEFAULT_ENV,
|
|
options: {
|
|
localTokenPrecedence: "env-first",
|
|
localPasswordPrecedence: "config-first", // pragma: allowlist secret
|
|
},
|
|
expected: {
|
|
token: "env-token",
|
|
password: "remote-password", // pragma: allowlist secret
|
|
},
|
|
},
|
|
{
|
|
name: "remote mode defaults to remote-first token and env-first password",
|
|
cfg: cfg(createRemoteModeConfig()),
|
|
env: DEFAULT_ENV,
|
|
expected: {
|
|
token: "remote-token",
|
|
password: "env-password", // pragma: allowlist secret
|
|
},
|
|
},
|
|
{
|
|
name: "remote mode supports env-first token with remote-first password",
|
|
cfg: cfg(createRemoteModeConfig()),
|
|
env: DEFAULT_ENV,
|
|
options: {
|
|
remoteTokenPrecedence: "env-first",
|
|
remotePasswordPrecedence: "remote-first", // pragma: allowlist secret
|
|
},
|
|
expected: {
|
|
token: "env-token",
|
|
password: "remote-password", // pragma: allowlist secret
|
|
},
|
|
},
|
|
{
|
|
name: "remote-only fallback can suppress env/local password fallback",
|
|
cfg: cfg({
|
|
gateway: {
|
|
mode: "remote",
|
|
auth: {
|
|
token: "local-token",
|
|
password: "local-password", // pragma: allowlist secret
|
|
},
|
|
remote: {
|
|
url: "wss://remote.example",
|
|
token: "remote-token",
|
|
},
|
|
},
|
|
}),
|
|
env: DEFAULT_ENV,
|
|
options: {
|
|
remoteTokenFallback: "remote-only",
|
|
remotePasswordFallback: "remote-only", // pragma: allowlist secret
|
|
},
|
|
expected: {
|
|
token: "remote-token",
|
|
password: undefined,
|
|
},
|
|
},
|
|
{
|
|
name: "modeOverride can force remote precedence while config gateway.mode is local",
|
|
cfg: cfg({
|
|
gateway: {
|
|
mode: "local",
|
|
auth: {
|
|
token: "local-token",
|
|
password: "local-password", // pragma: allowlist secret
|
|
},
|
|
remote: {
|
|
url: "wss://remote.example",
|
|
token: "remote-token",
|
|
password: "remote-password", // pragma: allowlist secret
|
|
},
|
|
},
|
|
}),
|
|
env: DEFAULT_ENV,
|
|
options: {
|
|
modeOverride: "remote",
|
|
remoteTokenPrecedence: "remote-first",
|
|
remotePasswordPrecedence: "remote-first", // pragma: allowlist secret
|
|
},
|
|
expected: {
|
|
token: "remote-token",
|
|
password: "remote-password", // pragma: allowlist secret
|
|
},
|
|
},
|
|
{
|
|
name: "includeLegacyEnv controls CLAWDBOT fallback",
|
|
cfg: cfg({
|
|
gateway: {
|
|
mode: "local",
|
|
auth: {},
|
|
},
|
|
}),
|
|
env: {
|
|
CLAWDBOT_GATEWAY_TOKEN: "legacy-token",
|
|
CLAWDBOT_GATEWAY_PASSWORD: "legacy-password", // pragma: allowlist secret
|
|
} as NodeJS.ProcessEnv,
|
|
options: {
|
|
includeLegacyEnv: true,
|
|
},
|
|
expected: {
|
|
token: "legacy-token",
|
|
password: "legacy-password", // pragma: allowlist secret
|
|
},
|
|
},
|
|
];
|
|
|
|
it.each(cases)("$name", async ({ cfg, env, options, expected }) => {
|
|
const asyncResolved = await resolveGatewayConnectionAuth({
|
|
config: cfg,
|
|
env,
|
|
...options,
|
|
});
|
|
const syncResolved = resolveGatewayConnectionAuthFromConfig({
|
|
cfg,
|
|
env,
|
|
...options,
|
|
});
|
|
expect(asyncResolved).toEqual(expected);
|
|
expect(syncResolved).toEqual(expected);
|
|
});
|
|
|
|
it("can disable legacy env fallback", async () => {
|
|
const config = cfg({
|
|
gateway: {
|
|
mode: "local",
|
|
auth: {},
|
|
},
|
|
});
|
|
const env = {
|
|
CLAWDBOT_GATEWAY_TOKEN: "legacy-token",
|
|
CLAWDBOT_GATEWAY_PASSWORD: "legacy-password", // pragma: allowlist secret
|
|
} as NodeJS.ProcessEnv;
|
|
|
|
const resolved = await resolveGatewayConnectionAuth({
|
|
config,
|
|
env,
|
|
includeLegacyEnv: false,
|
|
});
|
|
expect(resolved).toEqual({
|
|
token: undefined,
|
|
password: undefined,
|
|
});
|
|
});
|
|
|
|
it("resolves local SecretRef token when legacy env is disabled", async () => {
|
|
const config = cfg({
|
|
gateway: {
|
|
mode: "local",
|
|
auth: {
|
|
token: { source: "env", provider: "default", id: "LOCAL_SECRET_TOKEN" },
|
|
},
|
|
},
|
|
secrets: {
|
|
providers: {
|
|
default: { source: "env" },
|
|
},
|
|
},
|
|
});
|
|
const env = {
|
|
CLAWDBOT_GATEWAY_TOKEN: "legacy-token",
|
|
LOCAL_SECRET_TOKEN: "resolved-from-secretref", // pragma: allowlist secret
|
|
} as NodeJS.ProcessEnv;
|
|
|
|
const resolved = await resolveGatewayConnectionAuth({
|
|
config,
|
|
env,
|
|
includeLegacyEnv: false,
|
|
});
|
|
expect(resolved).toEqual({
|
|
token: "resolved-from-secretref",
|
|
password: undefined,
|
|
});
|
|
});
|
|
|
|
it("resolves config-first token SecretRef even when OPENCLAW env token exists", async () => {
|
|
const config = cfg({
|
|
gateway: {
|
|
mode: "local",
|
|
auth: {
|
|
token: { source: "env", provider: "default", id: "CONFIG_FIRST_TOKEN" },
|
|
},
|
|
},
|
|
secrets: {
|
|
providers: {
|
|
default: { source: "env" },
|
|
},
|
|
},
|
|
});
|
|
const env = {
|
|
OPENCLAW_GATEWAY_TOKEN: "env-token",
|
|
CONFIG_FIRST_TOKEN: "config-first-token",
|
|
} as NodeJS.ProcessEnv;
|
|
|
|
const resolved = await resolveGatewayConnectionAuth({
|
|
config,
|
|
env,
|
|
includeLegacyEnv: false,
|
|
localTokenPrecedence: "config-first",
|
|
});
|
|
expect(resolved).toEqual({
|
|
token: "config-first-token",
|
|
password: undefined,
|
|
});
|
|
});
|
|
|
|
it("resolves config-first password SecretRef even when OPENCLAW env password exists", async () => {
|
|
const config = cfg({
|
|
gateway: {
|
|
mode: "local",
|
|
auth: {
|
|
mode: "password",
|
|
password: { source: "env", provider: "default", id: "CONFIG_FIRST_PASSWORD" },
|
|
},
|
|
},
|
|
secrets: {
|
|
providers: {
|
|
default: { source: "env" },
|
|
},
|
|
},
|
|
});
|
|
const env = {
|
|
OPENCLAW_GATEWAY_PASSWORD: "env-password", // pragma: allowlist secret
|
|
CONFIG_FIRST_PASSWORD: "config-first-password", // pragma: allowlist secret
|
|
} as NodeJS.ProcessEnv;
|
|
|
|
const resolved = await resolveGatewayConnectionAuth({
|
|
config,
|
|
env,
|
|
includeLegacyEnv: false,
|
|
localPasswordPrecedence: "config-first", // pragma: allowlist secret
|
|
});
|
|
expect(resolved).toEqual({
|
|
token: undefined,
|
|
password: "config-first-password", // pragma: allowlist secret
|
|
});
|
|
});
|
|
|
|
it("throws when config-first token SecretRef cannot resolve even if env token exists", async () => {
|
|
const config = cfg({
|
|
gateway: {
|
|
mode: "local",
|
|
auth: {
|
|
token: { source: "env", provider: "default", id: "MISSING_CONFIG_FIRST_TOKEN" },
|
|
},
|
|
},
|
|
secrets: {
|
|
providers: {
|
|
default: { source: "env" },
|
|
},
|
|
},
|
|
});
|
|
const env = {
|
|
OPENCLAW_GATEWAY_TOKEN: "env-token",
|
|
} as NodeJS.ProcessEnv;
|
|
|
|
await expect(
|
|
resolveGatewayConnectionAuth({
|
|
config,
|
|
env,
|
|
includeLegacyEnv: false,
|
|
localTokenPrecedence: "config-first",
|
|
}),
|
|
).rejects.toThrow("gateway.auth.token");
|
|
expect(() =>
|
|
resolveGatewayConnectionAuthFromConfig({
|
|
cfg: config,
|
|
env,
|
|
includeLegacyEnv: false,
|
|
localTokenPrecedence: "config-first",
|
|
}),
|
|
).toThrow("gateway.auth.token");
|
|
});
|
|
|
|
it("throws when config-first password SecretRef cannot resolve even if env password exists", async () => {
|
|
const config = cfg({
|
|
gateway: {
|
|
mode: "local",
|
|
auth: {
|
|
mode: "password",
|
|
password: { source: "env", provider: "default", id: "MISSING_CONFIG_FIRST_PASSWORD" },
|
|
},
|
|
},
|
|
secrets: {
|
|
providers: {
|
|
default: { source: "env" },
|
|
},
|
|
},
|
|
});
|
|
const env = {
|
|
OPENCLAW_GATEWAY_PASSWORD: "env-password", // pragma: allowlist secret
|
|
} as NodeJS.ProcessEnv;
|
|
|
|
await expect(
|
|
resolveGatewayConnectionAuth({
|
|
config,
|
|
env,
|
|
includeLegacyEnv: false,
|
|
localPasswordPrecedence: "config-first", // pragma: allowlist secret
|
|
}),
|
|
).rejects.toThrow("gateway.auth.password");
|
|
expect(() =>
|
|
resolveGatewayConnectionAuthFromConfig({
|
|
cfg: config,
|
|
env,
|
|
includeLegacyEnv: false,
|
|
localPasswordPrecedence: "config-first", // pragma: allowlist secret
|
|
}),
|
|
).toThrow("gateway.auth.password");
|
|
});
|
|
|
|
it("fails closed when local token SecretRef is unresolved and remote token fallback exists", async () => {
|
|
await expectFailClosedOnUnresolvedLocalAuth(
|
|
createUnresolvedLocalAuthConfig({
|
|
mode: "token",
|
|
id: "MISSING_LOCAL_TOKEN",
|
|
remoteFallback: "remote-token",
|
|
}),
|
|
"gateway.auth.token",
|
|
);
|
|
});
|
|
|
|
it("fails closed when local password SecretRef is unresolved and remote password fallback exists", async () => {
|
|
await expectFailClosedOnUnresolvedLocalAuth(
|
|
createUnresolvedLocalAuthConfig({
|
|
mode: "password",
|
|
id: "MISSING_LOCAL_PASSWORD",
|
|
remoteFallback: "remote-password", // pragma: allowlist secret
|
|
}),
|
|
"gateway.auth.password",
|
|
);
|
|
});
|
|
});
|