mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-14 10:41:23 +00:00
refactor: share gateway auth and approval helpers
This commit is contained in:
@@ -10,16 +10,24 @@ type GatewayClientAuth = {
|
||||
token?: string;
|
||||
password?: string;
|
||||
};
|
||||
type ResolveGatewayConnectionAuth = (params: unknown) => Promise<GatewayClientAuth>;
|
||||
type ResolveGatewayClientBootstrap = (params: unknown) => Promise<{
|
||||
url: string;
|
||||
urlSource: string;
|
||||
auth: GatewayClientAuth;
|
||||
}>;
|
||||
|
||||
const mockState = vi.hoisted(() => ({
|
||||
gateways: [] as MockGatewayClient[],
|
||||
gatewayAuth: [] as GatewayClientAuth[],
|
||||
agentSideConnectionCtor: vi.fn(),
|
||||
agentStart: vi.fn(),
|
||||
resolveGatewayConnectionAuth: vi.fn<ResolveGatewayConnectionAuth>(async (_params) => ({
|
||||
token: undefined,
|
||||
password: undefined,
|
||||
resolveGatewayClientBootstrap: vi.fn<ResolveGatewayClientBootstrap>(async (_params) => ({
|
||||
url: "ws://127.0.0.1:18789",
|
||||
urlSource: "local loopback",
|
||||
auth: {
|
||||
token: undefined,
|
||||
password: undefined,
|
||||
},
|
||||
})),
|
||||
}));
|
||||
|
||||
@@ -82,8 +90,9 @@ vi.mock("../gateway/call.js", () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../gateway/connection-auth.js", () => ({
|
||||
resolveGatewayConnectionAuth: (params: unknown) => mockState.resolveGatewayConnectionAuth(params),
|
||||
vi.mock("../gateway/client-bootstrap.js", () => ({
|
||||
resolveGatewayClientBootstrap: (params: unknown) =>
|
||||
mockState.resolveGatewayClientBootstrap(params),
|
||||
}));
|
||||
|
||||
vi.mock("../gateway/client.js", () => ({
|
||||
@@ -156,10 +165,14 @@ describe("serveAcpGateway startup", () => {
|
||||
mockState.gatewayAuth.length = 0;
|
||||
mockState.agentSideConnectionCtor.mockReset();
|
||||
mockState.agentStart.mockReset();
|
||||
mockState.resolveGatewayConnectionAuth.mockReset();
|
||||
mockState.resolveGatewayConnectionAuth.mockResolvedValue({
|
||||
token: undefined,
|
||||
password: undefined,
|
||||
mockState.resolveGatewayClientBootstrap.mockReset();
|
||||
mockState.resolveGatewayClientBootstrap.mockResolvedValue({
|
||||
url: "ws://127.0.0.1:18789",
|
||||
urlSource: "local loopback",
|
||||
auth: {
|
||||
token: undefined,
|
||||
password: undefined,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -199,9 +212,13 @@ describe("serveAcpGateway startup", () => {
|
||||
});
|
||||
|
||||
it("passes resolved SecretInput gateway credentials to the ACP gateway client", async () => {
|
||||
mockState.resolveGatewayConnectionAuth.mockResolvedValue({
|
||||
token: undefined,
|
||||
password: "resolved-secret-password", // pragma: allowlist secret
|
||||
mockState.resolveGatewayClientBootstrap.mockResolvedValue({
|
||||
url: "ws://127.0.0.1:18789",
|
||||
urlSource: "local loopback",
|
||||
auth: {
|
||||
token: undefined,
|
||||
password: "resolved-secret-password", // pragma: allowlist secret
|
||||
},
|
||||
});
|
||||
const { signalHandlers, onceSpy } = captureProcessSignalHandlers();
|
||||
|
||||
@@ -209,7 +226,7 @@ describe("serveAcpGateway startup", () => {
|
||||
const servePromise = serveAcpGateway({});
|
||||
await Promise.resolve();
|
||||
|
||||
expect(mockState.resolveGatewayConnectionAuth).toHaveBeenCalledWith(
|
||||
expect(mockState.resolveGatewayClientBootstrap).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
env: process.env,
|
||||
}),
|
||||
@@ -235,11 +252,10 @@ describe("serveAcpGateway startup", () => {
|
||||
});
|
||||
await Promise.resolve();
|
||||
|
||||
expect(mockState.resolveGatewayConnectionAuth).toHaveBeenCalledWith(
|
||||
expect(mockState.resolveGatewayClientBootstrap).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
env: process.env,
|
||||
urlOverride: "wss://override.example/ws",
|
||||
urlOverrideSource: "cli",
|
||||
gatewayUrl: "wss://override.example/ws",
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -3,9 +3,8 @@ import { Readable, Writable } from "node:stream";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { AgentSideConnection, ndJsonStream } from "@agentclientprotocol/sdk";
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import { buildGatewayConnectionDetails } from "../gateway/call.js";
|
||||
import { resolveGatewayClientBootstrap } from "../gateway/client-bootstrap.js";
|
||||
import { GatewayClient } from "../gateway/client.js";
|
||||
import { resolveGatewayConnectionAuth } from "../gateway/connection-auth.js";
|
||||
import { isMainModule } from "../infra/is-main.js";
|
||||
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js";
|
||||
import { readSecretFromFile } from "./secret-file.js";
|
||||
@@ -14,25 +13,14 @@ import { normalizeAcpProvenanceMode, type AcpServerOptions } from "./types.js";
|
||||
|
||||
export async function serveAcpGateway(opts: AcpServerOptions = {}): Promise<void> {
|
||||
const cfg = loadConfig();
|
||||
const connection = buildGatewayConnectionDetails({
|
||||
config: cfg,
|
||||
url: opts.gatewayUrl,
|
||||
});
|
||||
const gatewayUrlOverrideSource =
|
||||
connection.urlSource === "cli --url"
|
||||
? "cli"
|
||||
: connection.urlSource === "env OPENCLAW_GATEWAY_URL"
|
||||
? "env"
|
||||
: undefined;
|
||||
const creds = await resolveGatewayConnectionAuth({
|
||||
const bootstrap = await resolveGatewayClientBootstrap({
|
||||
config: cfg,
|
||||
gatewayUrl: opts.gatewayUrl,
|
||||
explicitAuth: {
|
||||
token: opts.gatewayToken,
|
||||
password: opts.gatewayPassword,
|
||||
},
|
||||
env: process.env,
|
||||
urlOverride: gatewayUrlOverrideSource ? connection.url : undefined,
|
||||
urlOverrideSource: gatewayUrlOverrideSource,
|
||||
});
|
||||
|
||||
let agent: AcpGatewayAgent | null = null;
|
||||
@@ -64,9 +52,9 @@ export async function serveAcpGateway(opts: AcpServerOptions = {}): Promise<void
|
||||
};
|
||||
|
||||
const gateway = new GatewayClient({
|
||||
url: connection.url,
|
||||
token: creds.token,
|
||||
password: creds.password,
|
||||
url: bootstrap.url,
|
||||
token: bootstrap.auth.token,
|
||||
password: bootstrap.auth.password,
|
||||
clientName: GATEWAY_CLIENT_NAMES.CLI,
|
||||
clientDisplayName: "ACP",
|
||||
clientVersion: "acp",
|
||||
|
||||
@@ -15,7 +15,9 @@ const serviceRestart = vi.fn().mockResolvedValue({ outcome: "completed" });
|
||||
const serviceIsLoaded = vi.fn().mockResolvedValue(false);
|
||||
const serviceReadCommand = vi.fn().mockResolvedValue(null);
|
||||
const serviceReadRuntime = vi.fn().mockResolvedValue({ status: "running" });
|
||||
const resolveGatewayProbeAuthWithSecretInputs = vi.fn(async (_opts?: unknown) => ({}));
|
||||
const resolveGatewayProbeAuthSafeWithSecretInputs = vi.fn(async (_opts?: unknown) => ({
|
||||
auth: {},
|
||||
}));
|
||||
const findExtraGatewayServices = vi.fn(async (_env: unknown, _opts?: unknown) => []);
|
||||
const inspectPortUsage = vi.fn(async (port: number) => ({
|
||||
port,
|
||||
@@ -62,8 +64,8 @@ vi.mock("./daemon-cli/probe.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../gateway/probe-auth.js", () => ({
|
||||
resolveGatewayProbeAuthWithSecretInputs: (opts: unknown) =>
|
||||
resolveGatewayProbeAuthWithSecretInputs(opts),
|
||||
resolveGatewayProbeAuthSafeWithSecretInputs: (opts: unknown) =>
|
||||
resolveGatewayProbeAuthSafeWithSecretInputs(opts),
|
||||
}));
|
||||
|
||||
vi.mock("../daemon/program-args.js", () => ({
|
||||
@@ -157,7 +159,8 @@ describe("daemon-cli coverage", () => {
|
||||
delete process.env.OPENCLAW_GATEWAY_PORT;
|
||||
delete process.env.OPENCLAW_PROFILE;
|
||||
serviceReadCommand.mockResolvedValue(null);
|
||||
resolveGatewayProbeAuthWithSecretInputs.mockClear();
|
||||
resolveGatewayProbeAuthSafeWithSecretInputs.mockClear();
|
||||
findExtraGatewayServices.mockClear();
|
||||
buildGatewayInstallPlan.mockClear();
|
||||
});
|
||||
|
||||
@@ -175,7 +178,7 @@ describe("daemon-cli coverage", () => {
|
||||
expect(probeGatewayStatus).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ url: "ws://127.0.0.1:18789" }),
|
||||
);
|
||||
expect(findExtraGatewayServices).toHaveBeenCalled();
|
||||
expect(findExtraGatewayServices).not.toHaveBeenCalled();
|
||||
expect(inspectPortUsage).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { resolveSecretInputRef } from "../../config/types.secrets.js";
|
||||
import { createGatewayCredentialPlan, trimToUndefined } from "../../gateway/credential-planner.js";
|
||||
import { resolveGatewayAuthToken } from "../../gateway/auth-token-resolution.js";
|
||||
import { createGatewayCredentialPlan } from "../../gateway/credential-planner.js";
|
||||
import { GatewaySecretRefUnavailableError } from "../../gateway/credentials.js";
|
||||
import { resolveConfiguredSecretInputString } from "../../gateway/resolve-configured-secret-input-string.js";
|
||||
|
||||
function authModeDisablesToken(mode: string | undefined): boolean {
|
||||
return mode === "password" || mode === "none" || mode === "trusted-proxy";
|
||||
@@ -35,24 +34,17 @@ export async function resolveGatewayTokenForDriftCheck(params: {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const tokenInput = params.cfg.gateway?.auth?.token;
|
||||
const tokenRef = resolveSecretInputRef({
|
||||
value: tokenInput,
|
||||
defaults: params.cfg.secrets?.defaults,
|
||||
}).ref;
|
||||
if (!tokenRef) {
|
||||
return trimToUndefined(tokenInput);
|
||||
}
|
||||
|
||||
const resolved = await resolveConfiguredSecretInputString({
|
||||
config: params.cfg,
|
||||
const resolved = await resolveGatewayAuthToken({
|
||||
cfg: params.cfg,
|
||||
env,
|
||||
value: tokenInput,
|
||||
path: "gateway.auth.token",
|
||||
envFallback: "never",
|
||||
unresolvedReasonStyle: "detailed",
|
||||
});
|
||||
if (resolved.value) {
|
||||
return resolved.value;
|
||||
if (resolved.token) {
|
||||
return resolved.token;
|
||||
}
|
||||
if (!resolved.secretRefConfigured) {
|
||||
return undefined;
|
||||
}
|
||||
throw new GatewaySecretRefUnavailableError("gateway.auth.token");
|
||||
}
|
||||
|
||||
@@ -15,11 +15,7 @@ import type { FindExtraGatewayServicesOptions } from "../../daemon/inspect.js";
|
||||
import type { ServiceConfigAudit } from "../../daemon/service-audit.js";
|
||||
import type { GatewayServiceRuntime } from "../../daemon/service-runtime.js";
|
||||
import { resolveGatewayService } from "../../daemon/service.js";
|
||||
import {
|
||||
isGatewaySecretRefUnavailableError,
|
||||
resolveGatewayProbeCredentialsFromConfig,
|
||||
trimToUndefined,
|
||||
} from "../../gateway/credentials.js";
|
||||
import { trimToUndefined } from "../../gateway/credentials.js";
|
||||
import {
|
||||
inspectBestEffortPrimaryTailnetIPv4,
|
||||
resolveBestEffortGatewayBindHostForDisplay,
|
||||
@@ -190,10 +186,6 @@ function shouldReportPortUsage(status: PortUsageStatus | undefined, rpcOk?: bool
|
||||
return true;
|
||||
}
|
||||
|
||||
function parseGatewaySecretRefPathFromError(error: unknown): string | null {
|
||||
return isGatewaySecretRefUnavailableError(error) ? error.path : null;
|
||||
}
|
||||
|
||||
async function loadDaemonConfigContext(
|
||||
serviceEnv?: Record<string, string>,
|
||||
): Promise<DaemonConfigContext> {
|
||||
@@ -408,43 +400,20 @@ export async function gatherDaemonStatus(
|
||||
let rpcAuthWarning: string | undefined;
|
||||
if (opts.probe) {
|
||||
const probeMode = daemonCfg.gateway?.mode === "remote" ? "remote" : "local";
|
||||
try {
|
||||
daemonProbeAuth = resolveGatewayProbeCredentialsFromConfig({
|
||||
cfg: daemonCfg,
|
||||
mode: probeMode,
|
||||
env: mergedDaemonEnv as NodeJS.ProcessEnv,
|
||||
explicitAuth: {
|
||||
token: opts.rpc.token,
|
||||
password: opts.rpc.password,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
const refPath = parseGatewaySecretRefPathFromError(error);
|
||||
if (!refPath) {
|
||||
throw error;
|
||||
}
|
||||
try {
|
||||
daemonProbeAuth = await loadGatewayProbeAuthModule().then(
|
||||
({ resolveGatewayProbeAuthWithSecretInputs }) =>
|
||||
resolveGatewayProbeAuthWithSecretInputs({
|
||||
cfg: daemonCfg,
|
||||
mode: probeMode,
|
||||
env: mergedDaemonEnv as NodeJS.ProcessEnv,
|
||||
explicitAuth: {
|
||||
token: opts.rpc.token,
|
||||
password: opts.rpc.password,
|
||||
},
|
||||
}),
|
||||
);
|
||||
} catch (fallbackError) {
|
||||
const fallbackRefPath = parseGatewaySecretRefPathFromError(fallbackError);
|
||||
if (!fallbackRefPath) {
|
||||
throw fallbackError;
|
||||
}
|
||||
daemonProbeAuth = undefined;
|
||||
rpcAuthWarning = `${fallbackRefPath} SecretRef is unavailable in this command path; probing without configured auth credentials.`;
|
||||
}
|
||||
}
|
||||
const probeAuthResolution = await loadGatewayProbeAuthModule().then(
|
||||
({ resolveGatewayProbeAuthSafeWithSecretInputs }) =>
|
||||
resolveGatewayProbeAuthSafeWithSecretInputs({
|
||||
cfg: daemonCfg,
|
||||
mode: probeMode,
|
||||
env: mergedDaemonEnv as NodeJS.ProcessEnv,
|
||||
explicitAuth: {
|
||||
token: opts.rpc.token,
|
||||
password: opts.rpc.password,
|
||||
},
|
||||
}),
|
||||
);
|
||||
daemonProbeAuth = probeAuthResolution.auth;
|
||||
rpcAuthWarning = probeAuthResolution.warning;
|
||||
}
|
||||
|
||||
const rpc = opts.probe
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { readConfigFileSnapshot, resolveGatewayPort } from "../config/config.js";
|
||||
import type { OpenClawConfig } from "../config/types.js";
|
||||
import { readGatewayTokenEnv } from "../gateway/credentials.js";
|
||||
import { resolveConfiguredSecretInputWithFallback } from "../gateway/resolve-configured-secret-input-string.js";
|
||||
import { resolveGatewayAuthToken } from "../gateway/auth-token-resolution.js";
|
||||
import { copyToClipboard } from "../infra/clipboard.js";
|
||||
import type { RuntimeEnv } from "../runtime.js";
|
||||
import { defaultRuntime } from "../runtime.js";
|
||||
@@ -16,37 +14,6 @@ type DashboardOptions = {
|
||||
noOpen?: boolean;
|
||||
};
|
||||
|
||||
async function resolveDashboardToken(
|
||||
cfg: OpenClawConfig,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
): Promise<{
|
||||
token?: string;
|
||||
source?: "config" | "env" | "secretRef";
|
||||
unresolvedRefReason?: string;
|
||||
tokenSecretRefConfigured: boolean;
|
||||
}> {
|
||||
const resolved = await resolveConfiguredSecretInputWithFallback({
|
||||
config: cfg,
|
||||
env,
|
||||
value: cfg.gateway?.auth?.token,
|
||||
path: "gateway.auth.token",
|
||||
readFallback: () => readGatewayTokenEnv(env),
|
||||
});
|
||||
return {
|
||||
token: resolved.value,
|
||||
source:
|
||||
resolved.source === "config"
|
||||
? "config"
|
||||
: resolved.source === "secretRef"
|
||||
? "secretRef"
|
||||
: resolved.source === "fallback"
|
||||
? "env"
|
||||
: undefined,
|
||||
unresolvedRefReason: resolved.unresolvedRefReason,
|
||||
tokenSecretRefConfigured: resolved.secretRefConfigured,
|
||||
};
|
||||
}
|
||||
|
||||
export async function dashboardCommand(
|
||||
runtime: RuntimeEnv = defaultRuntime,
|
||||
options: DashboardOptions = {},
|
||||
@@ -57,7 +24,11 @@ export async function dashboardCommand(
|
||||
const bind = cfg.gateway?.bind ?? "loopback";
|
||||
const basePath = cfg.gateway?.controlUi?.basePath;
|
||||
const customBindHost = cfg.gateway?.customBindHost;
|
||||
const resolvedToken = await resolveDashboardToken(cfg, process.env);
|
||||
const resolvedToken = await resolveGatewayAuthToken({
|
||||
cfg,
|
||||
env: process.env,
|
||||
envFallback: "always",
|
||||
});
|
||||
const token = resolvedToken.token ?? "";
|
||||
|
||||
// LAN URLs fail secure-context checks in browsers.
|
||||
@@ -69,14 +40,14 @@ export async function dashboardCommand(
|
||||
basePath,
|
||||
});
|
||||
// Avoid embedding externally managed SecretRef tokens in terminal/clipboard/browser args.
|
||||
const includeTokenInUrl = token.length > 0 && !resolvedToken.tokenSecretRefConfigured;
|
||||
const includeTokenInUrl = token.length > 0 && !resolvedToken.secretRefConfigured;
|
||||
// Prefer URL fragment to avoid leaking auth tokens via query params.
|
||||
const dashboardUrl = includeTokenInUrl
|
||||
? `${links.httpUrl}#token=${encodeURIComponent(token)}`
|
||||
: links.httpUrl;
|
||||
|
||||
runtime.log(`Dashboard URL: ${dashboardUrl}`);
|
||||
if (resolvedToken.tokenSecretRefConfigured && token) {
|
||||
if (resolvedToken.secretRefConfigured && token) {
|
||||
runtime.log(
|
||||
"Token auto-auth is disabled for SecretRef-managed gateway.auth.token; use your external token source if prompted.",
|
||||
);
|
||||
|
||||
@@ -1,22 +1,19 @@
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
export { shouldRequireGatewayTokenForInstall } from "../gateway/auth-install-policy.js";
|
||||
import { readGatewayTokenEnv } from "../gateway/credentials.js";
|
||||
import { resolveConfiguredSecretInputWithFallback } from "../gateway/resolve-configured-secret-input-string.js";
|
||||
import { resolveGatewayAuthToken } from "../gateway/auth-token-resolution.js";
|
||||
|
||||
export async function resolveGatewayAuthTokenForService(
|
||||
cfg: OpenClawConfig,
|
||||
env: NodeJS.ProcessEnv,
|
||||
): Promise<{ token?: string; unavailableReason?: string }> {
|
||||
const resolved = await resolveConfiguredSecretInputWithFallback({
|
||||
config: cfg,
|
||||
const resolved = await resolveGatewayAuthToken({
|
||||
cfg,
|
||||
env,
|
||||
value: cfg.gateway?.auth?.token,
|
||||
path: "gateway.auth.token",
|
||||
unresolvedReasonStyle: "detailed",
|
||||
readFallback: () => readGatewayTokenEnv(env),
|
||||
envFallback: "always",
|
||||
});
|
||||
if (resolved.value) {
|
||||
return { token: resolved.value };
|
||||
if (resolved.token) {
|
||||
return { token: resolved.token };
|
||||
}
|
||||
if (!resolved.secretRefConfigured) {
|
||||
return {};
|
||||
|
||||
@@ -7,10 +7,8 @@ import {
|
||||
import { resolveSecretInputRef } from "../config/types.secrets.js";
|
||||
import { shouldRequireGatewayTokenForInstall } from "../gateway/auth-install-policy.js";
|
||||
import { hasAmbiguousGatewayAuthModeConfig } from "../gateway/auth-mode-policy.js";
|
||||
import { resolveGatewayAuthToken } from "../gateway/auth-token-resolution.js";
|
||||
import { resolveGatewayAuth } from "../gateway/auth.js";
|
||||
import { readGatewayTokenEnv } from "../gateway/credentials.js";
|
||||
import { secretRefKey } from "../secrets/ref-contract.js";
|
||||
import { resolveSecretRefValues } from "../secrets/resolve.js";
|
||||
import { randomToken } from "./onboard-helpers.js";
|
||||
|
||||
type GatewayInstallTokenOptions = {
|
||||
@@ -28,41 +26,6 @@ export type GatewayInstallTokenResolution = {
|
||||
warnings: string[];
|
||||
};
|
||||
|
||||
function resolveConfiguredGatewayInstallToken(params: {
|
||||
config: OpenClawConfig;
|
||||
env: NodeJS.ProcessEnv;
|
||||
explicitToken?: string;
|
||||
tokenRef: unknown;
|
||||
}): string | undefined {
|
||||
const configToken =
|
||||
params.tokenRef || typeof params.config.gateway?.auth?.token !== "string"
|
||||
? undefined
|
||||
: params.config.gateway.auth.token.trim() || undefined;
|
||||
const explicitToken = params.explicitToken?.trim() || undefined;
|
||||
const envToken = readGatewayTokenEnv(params.env);
|
||||
return explicitToken || configToken || (params.tokenRef ? undefined : envToken);
|
||||
}
|
||||
|
||||
async function validateGatewayInstallTokenSecretRef(params: {
|
||||
tokenRef: NonNullable<ReturnType<typeof resolveSecretInputRef>["ref"]>;
|
||||
config: OpenClawConfig;
|
||||
env: NodeJS.ProcessEnv;
|
||||
}): Promise<string | undefined> {
|
||||
try {
|
||||
const resolved = await resolveSecretRefValues([params.tokenRef], {
|
||||
config: params.config,
|
||||
env: params.env,
|
||||
});
|
||||
const value = resolved.get(secretRefKey(params.tokenRef));
|
||||
if (typeof value !== "string" || value.trim().length === 0) {
|
||||
throw new Error("gateway.auth.token resolved to an empty or non-string value.");
|
||||
}
|
||||
return undefined;
|
||||
} catch (err) {
|
||||
return `gateway.auth.token SecretRef is configured but unresolved (${String(err)}).`;
|
||||
}
|
||||
}
|
||||
|
||||
async function maybePersistAutoGeneratedGatewayInstallToken(params: {
|
||||
token: string;
|
||||
config: OpenClawConfig;
|
||||
@@ -128,13 +91,14 @@ export async function resolveGatewayInstallToken(
|
||||
): Promise<GatewayInstallTokenResolution> {
|
||||
const cfg = options.config;
|
||||
const warnings: string[] = [];
|
||||
const tokenRef = resolveSecretInputRef({
|
||||
value: cfg.gateway?.auth?.token,
|
||||
defaults: cfg.secrets?.defaults,
|
||||
}).ref;
|
||||
const tokenRefConfigured = Boolean(tokenRef);
|
||||
|
||||
if (hasAmbiguousGatewayAuthModeConfig(cfg)) {
|
||||
const tokenRefConfigured = Boolean(
|
||||
resolveSecretInputRef({
|
||||
value: cfg.gateway?.auth?.token,
|
||||
defaults: cfg.secrets?.defaults,
|
||||
}).ref,
|
||||
);
|
||||
return {
|
||||
token: undefined,
|
||||
tokenRefConfigured,
|
||||
@@ -150,31 +114,43 @@ export async function resolveGatewayInstallToken(
|
||||
});
|
||||
const needsToken =
|
||||
shouldRequireGatewayTokenForInstall(cfg, options.env) && !resolvedAuth.allowTailscale;
|
||||
if (!needsToken) {
|
||||
const tokenRefConfigured = Boolean(
|
||||
resolveSecretInputRef({
|
||||
value: cfg.gateway?.auth?.token,
|
||||
defaults: cfg.secrets?.defaults,
|
||||
}).ref,
|
||||
);
|
||||
return {
|
||||
token: undefined,
|
||||
tokenRefConfigured,
|
||||
unavailableReason: undefined,
|
||||
warnings,
|
||||
};
|
||||
}
|
||||
|
||||
let token = resolveConfiguredGatewayInstallToken({
|
||||
config: cfg,
|
||||
const resolvedToken = await resolveGatewayAuthToken({
|
||||
cfg,
|
||||
env: options.env,
|
||||
explicitToken: options.explicitToken,
|
||||
tokenRef,
|
||||
envFallback: "no-secret-ref",
|
||||
unresolvedReasonStyle: "detailed",
|
||||
});
|
||||
const tokenRefConfigured = resolvedToken.secretRefConfigured;
|
||||
let token = resolvedToken.source === "secretRef" ? undefined : resolvedToken.token;
|
||||
let unavailableReason: string | undefined;
|
||||
|
||||
if (tokenRef && !token && needsToken) {
|
||||
unavailableReason = await validateGatewayInstallTokenSecretRef({
|
||||
tokenRef,
|
||||
config: cfg,
|
||||
env: options.env,
|
||||
});
|
||||
if (!unavailableReason) {
|
||||
warnings.push(
|
||||
"gateway.auth.token is SecretRef-managed; install will not persist a resolved token in service environment. Ensure the SecretRef is resolvable in the daemon runtime context.",
|
||||
);
|
||||
}
|
||||
if (tokenRefConfigured && resolvedToken.source === "secretRef" && needsToken) {
|
||||
warnings.push(
|
||||
"gateway.auth.token is SecretRef-managed; install will not persist a resolved token in service environment. Ensure the SecretRef is resolvable in the daemon runtime context.",
|
||||
);
|
||||
} else if (tokenRefConfigured && !token && needsToken) {
|
||||
unavailableReason = `gateway.auth.token SecretRef is configured but unresolved (${resolvedToken.unresolvedRefReason ?? "unknown reason"}).`;
|
||||
}
|
||||
|
||||
const allowAutoGenerate = options.autoGenerateWhenMissing ?? false;
|
||||
const persistGeneratedToken = options.persistGeneratedToken ?? false;
|
||||
if (!token && needsToken && !tokenRef && allowAutoGenerate) {
|
||||
if (!token && !tokenRefConfigured && allowAutoGenerate) {
|
||||
token = randomToken();
|
||||
warnings.push(
|
||||
persistGeneratedToken
|
||||
|
||||
@@ -2,10 +2,9 @@ import { parseTimeoutMsWithFallback } from "../../cli/parse-timeout.js";
|
||||
import { resolveGatewayPort } from "../../config/config.js";
|
||||
import type { OpenClawConfig, ConfigFileSnapshot } from "../../config/types.js";
|
||||
import { hasConfiguredSecretInput } from "../../config/types.secrets.js";
|
||||
import { readGatewayPasswordEnv, readGatewayTokenEnv } from "../../gateway/credentials.js";
|
||||
import { resolveGatewayProbeSurfaceAuth } from "../../gateway/auth-surface-resolution.js";
|
||||
import { isLoopbackHost } from "../../gateway/net.js";
|
||||
import type { GatewayProbeResult } from "../../gateway/probe.js";
|
||||
import { resolveConfiguredSecretInputString } from "../../gateway/resolve-configured-secret-input-string.js";
|
||||
import { inspectBestEffortPrimaryTailnetIPv4 } from "../../infra/network-discovery-display.js";
|
||||
import { colorize, theme } from "../../terminal/theme.js";
|
||||
import { pickGatewaySelfPresence } from "../gateway-presence.js";
|
||||
@@ -169,92 +168,10 @@ export async function resolveAuthForTarget(
|
||||
return { token: tokenOverride, password: passwordOverride };
|
||||
}
|
||||
|
||||
const diagnostics: string[] = [];
|
||||
const authMode = cfg.gateway?.auth?.mode;
|
||||
const tokenOnly = authMode === "token";
|
||||
const passwordOnly = authMode === "password";
|
||||
|
||||
const resolveToken = async (value: unknown, path: string): Promise<string | undefined> => {
|
||||
const tokenResolution = await resolveConfiguredSecretInputString({
|
||||
config: cfg,
|
||||
env: process.env,
|
||||
value,
|
||||
path,
|
||||
unresolvedReasonStyle: "detailed",
|
||||
});
|
||||
if (tokenResolution.unresolvedRefReason) {
|
||||
diagnostics.push(tokenResolution.unresolvedRefReason);
|
||||
}
|
||||
return tokenResolution.value;
|
||||
};
|
||||
const resolvePassword = async (value: unknown, path: string): Promise<string | undefined> => {
|
||||
const passwordResolution = await resolveConfiguredSecretInputString({
|
||||
config: cfg,
|
||||
env: process.env,
|
||||
value,
|
||||
path,
|
||||
unresolvedReasonStyle: "detailed",
|
||||
});
|
||||
if (passwordResolution.unresolvedRefReason) {
|
||||
diagnostics.push(passwordResolution.unresolvedRefReason);
|
||||
}
|
||||
return passwordResolution.value;
|
||||
};
|
||||
const withDiagnostics = <T extends { token?: string; password?: string }>(result: T) =>
|
||||
diagnostics.length > 0 ? { ...result, diagnostics } : result;
|
||||
|
||||
if (target.kind === "configRemote" || target.kind === "sshTunnel") {
|
||||
const remoteTokenValue = cfg.gateway?.remote?.token;
|
||||
const remotePasswordValue = (cfg.gateway?.remote as { password?: unknown } | undefined)
|
||||
?.password;
|
||||
const token = await resolveToken(remoteTokenValue, "gateway.remote.token");
|
||||
const password = token
|
||||
? undefined
|
||||
: await resolvePassword(remotePasswordValue, "gateway.remote.password");
|
||||
return withDiagnostics({ token, password });
|
||||
}
|
||||
|
||||
const authDisabled = authMode === "none" || authMode === "trusted-proxy";
|
||||
if (authDisabled) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const envToken = readGatewayTokenEnv();
|
||||
const envPassword = readGatewayPasswordEnv();
|
||||
if (tokenOnly) {
|
||||
const token = await resolveToken(cfg.gateway?.auth?.token, "gateway.auth.token");
|
||||
if (token) {
|
||||
return withDiagnostics({ token });
|
||||
}
|
||||
if (envToken) {
|
||||
return { token: envToken };
|
||||
}
|
||||
return withDiagnostics({});
|
||||
}
|
||||
if (passwordOnly) {
|
||||
const password = await resolvePassword(cfg.gateway?.auth?.password, "gateway.auth.password");
|
||||
if (password) {
|
||||
return withDiagnostics({ password });
|
||||
}
|
||||
if (envPassword) {
|
||||
return { password: envPassword };
|
||||
}
|
||||
return withDiagnostics({});
|
||||
}
|
||||
|
||||
const token = await resolveToken(cfg.gateway?.auth?.token, "gateway.auth.token");
|
||||
if (token) {
|
||||
return withDiagnostics({ token });
|
||||
}
|
||||
if (envToken) {
|
||||
return { token: envToken };
|
||||
}
|
||||
if (envPassword) {
|
||||
return withDiagnostics({ password: envPassword });
|
||||
}
|
||||
const password = await resolvePassword(cfg.gateway?.auth?.password, "gateway.auth.password");
|
||||
|
||||
return withDiagnostics({ token, password });
|
||||
return resolveGatewayProbeSurfaceAuth({
|
||||
config: cfg,
|
||||
surface: target.kind === "configRemote" || target.kind === "sshTunnel" ? "remote" : "local",
|
||||
});
|
||||
}
|
||||
|
||||
export { pickGatewaySelfPresence };
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type { loadConfig } from "../config/config.js";
|
||||
import { resolveGatewayProbeAuthSafeWithSecretInputs } from "../gateway/probe-auth.js";
|
||||
import {
|
||||
resolveGatewayProbeAuthSafeWithSecretInputs,
|
||||
resolveGatewayProbeTarget,
|
||||
} from "../gateway/probe-auth.js";
|
||||
export { pickGatewaySelfPresence } from "./gateway-presence.js";
|
||||
|
||||
export async function resolveGatewayProbeAuthResolution(
|
||||
@@ -11,9 +14,10 @@ export async function resolveGatewayProbeAuthResolution(
|
||||
};
|
||||
warning?: string;
|
||||
}> {
|
||||
const target = resolveGatewayProbeTarget(cfg);
|
||||
return resolveGatewayProbeAuthSafeWithSecretInputs({
|
||||
cfg,
|
||||
mode: cfg.gateway?.mode === "remote" ? "remote" : "local",
|
||||
mode: target.mode,
|
||||
env: process.env,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,35 +1,139 @@
|
||||
import type { GatewayAuthConfig, OpenClawConfig } from "../config/config.js";
|
||||
import { resolveSecretInputRef } from "../config/types.secrets.js";
|
||||
import { hasConfiguredSecretInput } from "../config/types.secrets.js";
|
||||
import { resolveRequiredConfiguredSecretRefInputString } from "./resolve-configured-secret-input-string.js";
|
||||
import {
|
||||
assignResolvedGatewaySecretInput,
|
||||
readGatewaySecretInputValue,
|
||||
type SupportedGatewaySecretInputPath,
|
||||
} from "./secret-input-paths.js";
|
||||
|
||||
export function withGatewayAuthPassword(cfg: OpenClawConfig, password: string): OpenClawConfig {
|
||||
return {
|
||||
...cfg,
|
||||
gateway: {
|
||||
...cfg.gateway,
|
||||
auth: {
|
||||
...cfg.gateway?.auth,
|
||||
password,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
export type GatewayAuthSecretInputPath = Extract<
|
||||
SupportedGatewaySecretInputPath,
|
||||
"gateway.auth.token" | "gateway.auth.password"
|
||||
>;
|
||||
|
||||
function shouldResolveGatewayPasswordSecretRef(params: {
|
||||
export type GatewayAuthSecretRefResolutionParams = {
|
||||
cfg: OpenClawConfig;
|
||||
env: NodeJS.ProcessEnv;
|
||||
mode?: GatewayAuthConfig["mode"];
|
||||
hasPasswordCandidate: boolean;
|
||||
hasTokenCandidate: boolean;
|
||||
};
|
||||
|
||||
export function hasConfiguredGatewayAuthSecretInput(
|
||||
cfg: OpenClawConfig,
|
||||
path: GatewayAuthSecretInputPath,
|
||||
): boolean {
|
||||
return hasConfiguredSecretInput(readGatewaySecretInputValue(cfg, path), cfg.secrets?.defaults);
|
||||
}
|
||||
|
||||
export function shouldResolveGatewayAuthSecretRef(params: {
|
||||
mode?: GatewayAuthConfig["mode"];
|
||||
path: GatewayAuthSecretInputPath;
|
||||
hasPasswordCandidate: boolean;
|
||||
hasTokenCandidate: boolean;
|
||||
}): boolean {
|
||||
if (params.hasPasswordCandidate) {
|
||||
const isTokenPath = params.path === "gateway.auth.token";
|
||||
const hasPathCandidate = isTokenPath ? params.hasTokenCandidate : params.hasPasswordCandidate;
|
||||
if (hasPathCandidate) {
|
||||
return false;
|
||||
}
|
||||
if (params.mode === "password") {
|
||||
if (params.mode === (isTokenPath ? "token" : "password")) {
|
||||
return true;
|
||||
}
|
||||
if (params.mode === "token" || params.mode === "none" || params.mode === "trusted-proxy") {
|
||||
return false;
|
||||
}
|
||||
return !params.hasTokenCandidate;
|
||||
if (params.mode === "password") {
|
||||
return !isTokenPath;
|
||||
}
|
||||
return isTokenPath ? !params.hasPasswordCandidate : !params.hasTokenCandidate;
|
||||
}
|
||||
|
||||
export function shouldResolveGatewayTokenSecretRef(
|
||||
params: Omit<GatewayAuthSecretRefResolutionParams, "cfg" | "env">,
|
||||
): boolean {
|
||||
return shouldResolveGatewayAuthSecretRef({
|
||||
mode: params.mode,
|
||||
path: "gateway.auth.token",
|
||||
hasPasswordCandidate: params.hasPasswordCandidate,
|
||||
hasTokenCandidate: params.hasTokenCandidate,
|
||||
});
|
||||
}
|
||||
|
||||
export function shouldResolveGatewayPasswordSecretRef(
|
||||
params: Omit<GatewayAuthSecretRefResolutionParams, "cfg" | "env">,
|
||||
): boolean {
|
||||
return shouldResolveGatewayAuthSecretRef({
|
||||
mode: params.mode,
|
||||
path: "gateway.auth.password",
|
||||
hasPasswordCandidate: params.hasPasswordCandidate,
|
||||
hasTokenCandidate: params.hasTokenCandidate,
|
||||
});
|
||||
}
|
||||
|
||||
export async function resolveGatewayAuthSecretRefValue(params: {
|
||||
cfg: OpenClawConfig;
|
||||
env: NodeJS.ProcessEnv;
|
||||
path: GatewayAuthSecretInputPath;
|
||||
shouldResolve: boolean;
|
||||
}): Promise<string | undefined> {
|
||||
if (!params.shouldResolve) {
|
||||
return undefined;
|
||||
}
|
||||
const value = await resolveRequiredConfiguredSecretRefInputString({
|
||||
config: params.cfg,
|
||||
env: params.env,
|
||||
value: readGatewaySecretInputValue(params.cfg, params.path),
|
||||
path: params.path,
|
||||
});
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export async function resolveGatewayTokenSecretRefValue(
|
||||
params: GatewayAuthSecretRefResolutionParams,
|
||||
): Promise<string | undefined> {
|
||||
return resolveGatewayAuthSecretRefValue({
|
||||
cfg: params.cfg,
|
||||
env: params.env,
|
||||
path: "gateway.auth.token",
|
||||
shouldResolve: shouldResolveGatewayTokenSecretRef(params),
|
||||
});
|
||||
}
|
||||
|
||||
export async function resolveGatewayPasswordSecretRefValue(
|
||||
params: GatewayAuthSecretRefResolutionParams,
|
||||
): Promise<string | undefined> {
|
||||
return resolveGatewayAuthSecretRefValue({
|
||||
cfg: params.cfg,
|
||||
env: params.env,
|
||||
path: "gateway.auth.password",
|
||||
shouldResolve: shouldResolveGatewayPasswordSecretRef(params),
|
||||
});
|
||||
}
|
||||
|
||||
export async function resolveGatewayAuthSecretRef(params: {
|
||||
cfg: OpenClawConfig;
|
||||
env: NodeJS.ProcessEnv;
|
||||
path: GatewayAuthSecretInputPath;
|
||||
shouldResolve: boolean;
|
||||
}): Promise<OpenClawConfig> {
|
||||
const value = await resolveGatewayAuthSecretRefValue(params);
|
||||
if (!value) {
|
||||
return params.cfg;
|
||||
}
|
||||
const nextConfig = structuredClone(params.cfg);
|
||||
nextConfig.gateway ??= {};
|
||||
nextConfig.gateway.auth ??= {};
|
||||
assignResolvedGatewaySecretInput({
|
||||
config: nextConfig,
|
||||
path: params.path,
|
||||
value,
|
||||
});
|
||||
return nextConfig;
|
||||
}
|
||||
|
||||
export async function resolveGatewayPasswordSecretRef(params: {
|
||||
@@ -39,31 +143,30 @@ export async function resolveGatewayPasswordSecretRef(params: {
|
||||
hasPasswordCandidate: boolean;
|
||||
hasTokenCandidate: boolean;
|
||||
}): Promise<OpenClawConfig> {
|
||||
const authPassword = params.cfg.gateway?.auth?.password;
|
||||
const { ref } = resolveSecretInputRef({
|
||||
value: authPassword,
|
||||
defaults: params.cfg.secrets?.defaults,
|
||||
});
|
||||
if (!ref) {
|
||||
return params.cfg;
|
||||
}
|
||||
if (
|
||||
!shouldResolveGatewayPasswordSecretRef({
|
||||
mode: params.mode,
|
||||
hasPasswordCandidate: params.hasPasswordCandidate,
|
||||
hasTokenCandidate: params.hasTokenCandidate,
|
||||
})
|
||||
) {
|
||||
return params.cfg;
|
||||
}
|
||||
const value = await resolveRequiredConfiguredSecretRefInputString({
|
||||
config: params.cfg,
|
||||
return resolveGatewayAuthSecretRef({
|
||||
cfg: params.cfg,
|
||||
env: params.env,
|
||||
value: authPassword,
|
||||
path: "gateway.auth.password",
|
||||
shouldResolve: shouldResolveGatewayPasswordSecretRef(params),
|
||||
});
|
||||
}
|
||||
|
||||
export async function materializeGatewayAuthSecretRefs(
|
||||
params: GatewayAuthSecretRefResolutionParams,
|
||||
): Promise<OpenClawConfig> {
|
||||
const cfgWithToken = await resolveGatewayAuthSecretRef({
|
||||
cfg: params.cfg,
|
||||
env: params.env,
|
||||
path: "gateway.auth.token",
|
||||
shouldResolve: shouldResolveGatewayTokenSecretRef(params),
|
||||
});
|
||||
return await resolveGatewayPasswordSecretRef({
|
||||
cfg: cfgWithToken,
|
||||
env: params.env,
|
||||
mode: params.mode,
|
||||
hasPasswordCandidate: params.hasPasswordCandidate,
|
||||
hasTokenCandidate:
|
||||
params.hasTokenCandidate ||
|
||||
hasConfiguredGatewayAuthSecretInput(cfgWithToken, "gateway.auth.token"),
|
||||
});
|
||||
if (!value) {
|
||||
return params.cfg;
|
||||
}
|
||||
return withGatewayAuthPassword(params.cfg, value);
|
||||
}
|
||||
|
||||
289
src/gateway/auth-surface-resolution.ts
Normal file
289
src/gateway/auth-surface-resolution.ts
Normal file
@@ -0,0 +1,289 @@
|
||||
import type { OpenClawConfig } from "../config/types.js";
|
||||
import { hasConfiguredSecretInput } from "../config/types.secrets.js";
|
||||
import {
|
||||
readGatewayPasswordEnv,
|
||||
readGatewayTokenEnv,
|
||||
trimToUndefined,
|
||||
type ExplicitGatewayAuth,
|
||||
} from "./credentials.js";
|
||||
import { resolveConfiguredSecretInputString } from "./resolve-configured-secret-input-string.js";
|
||||
|
||||
type GatewayCredentialPath =
|
||||
| "gateway.auth.token"
|
||||
| "gateway.auth.password"
|
||||
| "gateway.remote.token"
|
||||
| "gateway.remote.password";
|
||||
|
||||
type ResolvedGatewayCredential = {
|
||||
value?: string;
|
||||
unresolvedRefReason?: string;
|
||||
};
|
||||
|
||||
async function resolveGatewayCredential(params: {
|
||||
config: OpenClawConfig;
|
||||
env: NodeJS.ProcessEnv;
|
||||
diagnostics: string[];
|
||||
path: GatewayCredentialPath;
|
||||
value: unknown;
|
||||
}): Promise<ResolvedGatewayCredential> {
|
||||
const resolved = await resolveConfiguredSecretInputString({
|
||||
config: params.config,
|
||||
env: params.env,
|
||||
value: params.value,
|
||||
path: params.path,
|
||||
unresolvedReasonStyle: "detailed",
|
||||
});
|
||||
if (resolved.unresolvedRefReason) {
|
||||
params.diagnostics.push(resolved.unresolvedRefReason);
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
function withDiagnostics<T extends object>(params: {
|
||||
diagnostics: string[];
|
||||
result: T;
|
||||
}): T & { diagnostics?: string[] } {
|
||||
return params.diagnostics.length > 0
|
||||
? { ...params.result, diagnostics: params.diagnostics }
|
||||
: params.result;
|
||||
}
|
||||
|
||||
export async function resolveGatewayProbeSurfaceAuth(params: {
|
||||
config: OpenClawConfig;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
surface: "local" | "remote";
|
||||
}): Promise<{ token?: string; password?: string; diagnostics?: string[] }> {
|
||||
const env = params.env ?? process.env;
|
||||
const diagnostics: string[] = [];
|
||||
const authMode = params.config.gateway?.auth?.mode;
|
||||
|
||||
if (params.surface === "remote") {
|
||||
const remoteToken = await resolveGatewayCredential({
|
||||
config: params.config,
|
||||
env,
|
||||
diagnostics,
|
||||
path: "gateway.remote.token",
|
||||
value: params.config.gateway?.remote?.token,
|
||||
});
|
||||
const remotePassword = remoteToken.value
|
||||
? { value: undefined }
|
||||
: await resolveGatewayCredential({
|
||||
config: params.config,
|
||||
env,
|
||||
diagnostics,
|
||||
path: "gateway.remote.password",
|
||||
value: params.config.gateway?.remote?.password,
|
||||
});
|
||||
return withDiagnostics({
|
||||
diagnostics,
|
||||
result: { token: remoteToken.value, password: remotePassword.value },
|
||||
});
|
||||
}
|
||||
|
||||
if (authMode === "none" || authMode === "trusted-proxy") {
|
||||
return {};
|
||||
}
|
||||
|
||||
const envToken = readGatewayTokenEnv(env);
|
||||
const envPassword = readGatewayPasswordEnv(env);
|
||||
|
||||
if (authMode === "token") {
|
||||
const token = await resolveGatewayCredential({
|
||||
config: params.config,
|
||||
env,
|
||||
diagnostics,
|
||||
path: "gateway.auth.token",
|
||||
value: params.config.gateway?.auth?.token,
|
||||
});
|
||||
return token.value
|
||||
? withDiagnostics({ diagnostics, result: { token: token.value } })
|
||||
: envToken
|
||||
? { token: envToken }
|
||||
: withDiagnostics({ diagnostics, result: {} });
|
||||
}
|
||||
|
||||
if (authMode === "password") {
|
||||
const password = await resolveGatewayCredential({
|
||||
config: params.config,
|
||||
env,
|
||||
diagnostics,
|
||||
path: "gateway.auth.password",
|
||||
value: params.config.gateway?.auth?.password,
|
||||
});
|
||||
return password.value
|
||||
? withDiagnostics({ diagnostics, result: { password: password.value } })
|
||||
: envPassword
|
||||
? { password: envPassword }
|
||||
: withDiagnostics({ diagnostics, result: {} });
|
||||
}
|
||||
|
||||
const token = await resolveGatewayCredential({
|
||||
config: params.config,
|
||||
env,
|
||||
diagnostics,
|
||||
path: "gateway.auth.token",
|
||||
value: params.config.gateway?.auth?.token,
|
||||
});
|
||||
if (token.value) {
|
||||
return withDiagnostics({ diagnostics, result: { token: token.value } });
|
||||
}
|
||||
if (envToken) {
|
||||
return { token: envToken };
|
||||
}
|
||||
if (envPassword) {
|
||||
return withDiagnostics({ diagnostics, result: { password: envPassword } });
|
||||
}
|
||||
const password = await resolveGatewayCredential({
|
||||
config: params.config,
|
||||
env,
|
||||
diagnostics,
|
||||
path: "gateway.auth.password",
|
||||
value: params.config.gateway?.auth?.password,
|
||||
});
|
||||
return withDiagnostics({
|
||||
diagnostics,
|
||||
result: { token: token.value, password: password.value },
|
||||
});
|
||||
}
|
||||
|
||||
export async function resolveGatewayInteractiveSurfaceAuth(params: {
|
||||
config: OpenClawConfig;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
explicitAuth?: ExplicitGatewayAuth;
|
||||
surface: "local" | "remote";
|
||||
}): Promise<{
|
||||
token?: string;
|
||||
password?: string;
|
||||
failureReason?: string;
|
||||
}> {
|
||||
const env = params.env ?? process.env;
|
||||
const diagnostics: string[] = [];
|
||||
const explicitToken = trimToUndefined(params.explicitAuth?.token);
|
||||
const explicitPassword = trimToUndefined(params.explicitAuth?.password);
|
||||
const envToken = readGatewayTokenEnv(env);
|
||||
const envPassword = readGatewayPasswordEnv(env);
|
||||
|
||||
if (params.surface === "remote") {
|
||||
const remoteToken = explicitToken
|
||||
? { value: explicitToken }
|
||||
: await resolveGatewayCredential({
|
||||
config: params.config,
|
||||
env,
|
||||
diagnostics,
|
||||
path: "gateway.remote.token",
|
||||
value: params.config.gateway?.remote?.token,
|
||||
});
|
||||
const remotePassword =
|
||||
explicitPassword || envPassword
|
||||
? { value: explicitPassword ?? envPassword }
|
||||
: await resolveGatewayCredential({
|
||||
config: params.config,
|
||||
env,
|
||||
diagnostics,
|
||||
path: "gateway.remote.password",
|
||||
value: params.config.gateway?.remote?.password,
|
||||
});
|
||||
const token = explicitToken ?? remoteToken.value;
|
||||
const password = explicitPassword ?? envPassword ?? remotePassword.value;
|
||||
return token || password
|
||||
? { token, password }
|
||||
: {
|
||||
failureReason:
|
||||
remoteToken.unresolvedRefReason ??
|
||||
remotePassword.unresolvedRefReason ??
|
||||
"Missing gateway auth credentials.",
|
||||
};
|
||||
}
|
||||
|
||||
const authMode = params.config.gateway?.auth?.mode;
|
||||
if (authMode === "none" || authMode === "trusted-proxy") {
|
||||
return {
|
||||
token: explicitToken ?? envToken,
|
||||
password: explicitPassword ?? envPassword,
|
||||
};
|
||||
}
|
||||
|
||||
const hasConfiguredToken = hasConfiguredSecretInput(
|
||||
params.config.gateway?.auth?.token,
|
||||
params.config.secrets?.defaults,
|
||||
);
|
||||
const hasConfiguredPassword = hasConfiguredSecretInput(
|
||||
params.config.gateway?.auth?.password,
|
||||
params.config.secrets?.defaults,
|
||||
);
|
||||
|
||||
const resolveToken = async () => {
|
||||
const localToken = explicitToken
|
||||
? { value: explicitToken }
|
||||
: await resolveGatewayCredential({
|
||||
config: params.config,
|
||||
env,
|
||||
diagnostics,
|
||||
path: "gateway.auth.token",
|
||||
value: params.config.gateway?.auth?.token,
|
||||
});
|
||||
const token = explicitToken ?? localToken.value ?? envToken;
|
||||
return {
|
||||
token,
|
||||
failureReason: token
|
||||
? undefined
|
||||
: (localToken.unresolvedRefReason ?? "Missing gateway auth token."),
|
||||
};
|
||||
};
|
||||
|
||||
const resolvePassword = async () => {
|
||||
const localPassword =
|
||||
explicitPassword || envPassword
|
||||
? { value: explicitPassword ?? envPassword }
|
||||
: await resolveGatewayCredential({
|
||||
config: params.config,
|
||||
env,
|
||||
diagnostics,
|
||||
path: "gateway.auth.password",
|
||||
value: params.config.gateway?.auth?.password,
|
||||
});
|
||||
const password = explicitPassword ?? envPassword ?? localPassword.value;
|
||||
return {
|
||||
password,
|
||||
failureReason: password
|
||||
? undefined
|
||||
: (localPassword.unresolvedRefReason ?? "Missing gateway auth password."),
|
||||
};
|
||||
};
|
||||
|
||||
if (authMode === "password") {
|
||||
const password = await resolvePassword();
|
||||
return {
|
||||
token: explicitToken ?? envToken,
|
||||
password: password.password,
|
||||
failureReason: password.failureReason,
|
||||
};
|
||||
}
|
||||
|
||||
if (authMode === "token") {
|
||||
const token = await resolveToken();
|
||||
return {
|
||||
token: token.token,
|
||||
password: explicitPassword ?? envPassword,
|
||||
failureReason: token.failureReason,
|
||||
};
|
||||
}
|
||||
|
||||
const shouldUsePassword =
|
||||
Boolean(explicitPassword ?? envPassword) || (hasConfiguredPassword && !hasConfiguredToken);
|
||||
if (shouldUsePassword) {
|
||||
const password = await resolvePassword();
|
||||
return {
|
||||
token: explicitToken ?? envToken,
|
||||
password: password.password,
|
||||
failureReason: password.failureReason,
|
||||
};
|
||||
}
|
||||
|
||||
const token = await resolveToken();
|
||||
return {
|
||||
token: token.token,
|
||||
password: explicitPassword ?? envPassword,
|
||||
failureReason: token.failureReason,
|
||||
};
|
||||
}
|
||||
85
src/gateway/auth-token-resolution.ts
Normal file
85
src/gateway/auth-token-resolution.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { resolveSecretInputRef } from "../config/types.secrets.js";
|
||||
import { readGatewayTokenEnv, trimToUndefined } from "./credentials.js";
|
||||
import {
|
||||
resolveConfiguredSecretInputString,
|
||||
type SecretInputUnresolvedReasonStyle,
|
||||
} from "./resolve-configured-secret-input-string.js";
|
||||
|
||||
export type GatewayAuthTokenResolutionSource = "explicit" | "config" | "secretRef" | "env";
|
||||
export type GatewayAuthTokenEnvFallback = "never" | "no-secret-ref" | "always";
|
||||
|
||||
export async function resolveGatewayAuthToken(params: {
|
||||
cfg: OpenClawConfig;
|
||||
env: NodeJS.ProcessEnv;
|
||||
explicitToken?: string;
|
||||
envFallback?: GatewayAuthTokenEnvFallback;
|
||||
unresolvedReasonStyle?: SecretInputUnresolvedReasonStyle;
|
||||
}): Promise<{
|
||||
token?: string;
|
||||
source?: GatewayAuthTokenResolutionSource;
|
||||
secretRefConfigured: boolean;
|
||||
unresolvedRefReason?: string;
|
||||
}> {
|
||||
const explicitToken = trimToUndefined(params.explicitToken);
|
||||
if (explicitToken) {
|
||||
return {
|
||||
token: explicitToken,
|
||||
source: "explicit",
|
||||
secretRefConfigured: false,
|
||||
};
|
||||
}
|
||||
|
||||
const tokenInput = params.cfg.gateway?.auth?.token;
|
||||
const tokenRef = resolveSecretInputRef({
|
||||
value: tokenInput,
|
||||
defaults: params.cfg.secrets?.defaults,
|
||||
}).ref;
|
||||
const envFallback = params.envFallback ?? "always";
|
||||
const envToken = readGatewayTokenEnv(params.env);
|
||||
|
||||
if (!tokenRef) {
|
||||
const configToken = trimToUndefined(tokenInput);
|
||||
if (configToken) {
|
||||
return {
|
||||
token: configToken,
|
||||
source: "config",
|
||||
secretRefConfigured: false,
|
||||
};
|
||||
}
|
||||
if (envFallback !== "never" && envToken) {
|
||||
return {
|
||||
token: envToken,
|
||||
source: "env",
|
||||
secretRefConfigured: false,
|
||||
};
|
||||
}
|
||||
return { secretRefConfigured: false };
|
||||
}
|
||||
|
||||
const resolved = await resolveConfiguredSecretInputString({
|
||||
config: params.cfg,
|
||||
env: params.env,
|
||||
value: tokenInput,
|
||||
path: "gateway.auth.token",
|
||||
unresolvedReasonStyle: params.unresolvedReasonStyle,
|
||||
});
|
||||
if (resolved.value) {
|
||||
return {
|
||||
token: resolved.value,
|
||||
source: "secretRef",
|
||||
secretRefConfigured: true,
|
||||
};
|
||||
}
|
||||
if (envFallback === "always" && envToken) {
|
||||
return {
|
||||
token: envToken,
|
||||
source: "env",
|
||||
secretRefConfigured: true,
|
||||
};
|
||||
}
|
||||
return {
|
||||
secretRefConfigured: true,
|
||||
unresolvedRefReason: resolved.unresolvedRefReason,
|
||||
};
|
||||
}
|
||||
@@ -43,6 +43,14 @@ import {
|
||||
type OperatorScope,
|
||||
} from "./method-scopes.js";
|
||||
import { PROTOCOL_VERSION } from "./protocol/index.js";
|
||||
import {
|
||||
ALL_GATEWAY_SECRET_INPUT_PATHS,
|
||||
assignResolvedGatewaySecretInput,
|
||||
isSupportedGatewaySecretInputPath,
|
||||
isTokenGatewaySecretInputPath,
|
||||
readGatewaySecretInputValue,
|
||||
type SupportedGatewaySecretInputPath,
|
||||
} from "./secret-input-paths.js";
|
||||
export type { GatewayConnectionDetails };
|
||||
|
||||
type CallGatewayBaseOptions = {
|
||||
@@ -364,44 +372,6 @@ async function resolveGatewayCredentialsWithEnv(
|
||||
return resolveGatewayCredentialsFromConfigWithSecretInputs({ context, env });
|
||||
}
|
||||
|
||||
type SupportedGatewaySecretInputPath =
|
||||
| "gateway.auth.token"
|
||||
| "gateway.auth.password"
|
||||
| "gateway.remote.token"
|
||||
| "gateway.remote.password";
|
||||
|
||||
const ALL_GATEWAY_SECRET_INPUT_PATHS: SupportedGatewaySecretInputPath[] = [
|
||||
"gateway.auth.token",
|
||||
"gateway.auth.password",
|
||||
"gateway.remote.token",
|
||||
"gateway.remote.password",
|
||||
];
|
||||
|
||||
function isSupportedGatewaySecretInputPath(path: string): path is SupportedGatewaySecretInputPath {
|
||||
return (
|
||||
path === "gateway.auth.token" ||
|
||||
path === "gateway.auth.password" ||
|
||||
path === "gateway.remote.token" ||
|
||||
path === "gateway.remote.password"
|
||||
);
|
||||
}
|
||||
|
||||
function readGatewaySecretInputValue(
|
||||
config: OpenClawConfig,
|
||||
path: SupportedGatewaySecretInputPath,
|
||||
): unknown {
|
||||
if (path === "gateway.auth.token") {
|
||||
return config.gateway?.auth?.token;
|
||||
}
|
||||
if (path === "gateway.auth.password") {
|
||||
return config.gateway?.auth?.password;
|
||||
}
|
||||
if (path === "gateway.remote.token") {
|
||||
return config.gateway?.remote?.token;
|
||||
}
|
||||
return config.gateway?.remote?.password;
|
||||
}
|
||||
|
||||
function hasConfiguredGatewaySecretRef(
|
||||
config: OpenClawConfig,
|
||||
path: SupportedGatewaySecretInputPath,
|
||||
@@ -436,10 +406,6 @@ function resolveGatewayCredentialsFromConfigOptions(params: {
|
||||
} as const;
|
||||
}
|
||||
|
||||
function isTokenGatewaySecretInputPath(path: SupportedGatewaySecretInputPath): boolean {
|
||||
return path === "gateway.auth.token" || path === "gateway.remote.token";
|
||||
}
|
||||
|
||||
function localAuthModeAllowsGatewaySecretInputPath(params: {
|
||||
authMode: string | undefined;
|
||||
path: SupportedGatewaySecretInputPath;
|
||||
@@ -515,68 +481,14 @@ async function resolveConfiguredGatewaySecretInput(params: {
|
||||
path: SupportedGatewaySecretInputPath;
|
||||
env: NodeJS.ProcessEnv;
|
||||
}): Promise<string | undefined> {
|
||||
const { config, path, env } = params;
|
||||
if (path === "gateway.auth.token") {
|
||||
return resolveGatewaySecretInputString({
|
||||
config,
|
||||
value: config.gateway?.auth?.token,
|
||||
path,
|
||||
env,
|
||||
});
|
||||
}
|
||||
if (path === "gateway.auth.password") {
|
||||
return resolveGatewaySecretInputString({
|
||||
config,
|
||||
value: config.gateway?.auth?.password,
|
||||
path,
|
||||
env,
|
||||
});
|
||||
}
|
||||
if (path === "gateway.remote.token") {
|
||||
return resolveGatewaySecretInputString({
|
||||
config,
|
||||
value: config.gateway?.remote?.token,
|
||||
path,
|
||||
env,
|
||||
});
|
||||
}
|
||||
return resolveGatewaySecretInputString({
|
||||
config,
|
||||
value: config.gateway?.remote?.password,
|
||||
path,
|
||||
env,
|
||||
config: params.config,
|
||||
value: readGatewaySecretInputValue(params.config, params.path),
|
||||
path: params.path,
|
||||
env: params.env,
|
||||
});
|
||||
}
|
||||
|
||||
function assignResolvedGatewaySecretInput(params: {
|
||||
config: OpenClawConfig;
|
||||
path: SupportedGatewaySecretInputPath;
|
||||
value: string | undefined;
|
||||
}): void {
|
||||
const { config, path, value } = params;
|
||||
if (path === "gateway.auth.token") {
|
||||
if (config.gateway?.auth) {
|
||||
config.gateway.auth.token = value;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (path === "gateway.auth.password") {
|
||||
if (config.gateway?.auth) {
|
||||
config.gateway.auth.password = value;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (path === "gateway.remote.token") {
|
||||
if (config.gateway?.remote) {
|
||||
config.gateway.remote.token = value;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (config.gateway?.remote) {
|
||||
config.gateway.remote.password = value;
|
||||
}
|
||||
}
|
||||
|
||||
async function resolvePreferredGatewaySecretInputs(params: {
|
||||
context: ResolvedGatewayCallContext;
|
||||
env: NodeJS.ProcessEnv;
|
||||
|
||||
87
src/gateway/client-bootstrap.test.ts
Normal file
87
src/gateway/client-bootstrap.test.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const mockState = vi.hoisted(() => ({
|
||||
buildGatewayConnectionDetails: vi.fn(),
|
||||
resolveGatewayConnectionAuth: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("./call.js", () => ({
|
||||
buildGatewayConnectionDetails: (...args: unknown[]) =>
|
||||
mockState.buildGatewayConnectionDetails(...args),
|
||||
}));
|
||||
|
||||
vi.mock("./connection-auth.js", () => ({
|
||||
resolveGatewayConnectionAuth: (...args: unknown[]) =>
|
||||
mockState.resolveGatewayConnectionAuth(...args),
|
||||
}));
|
||||
|
||||
const { resolveGatewayClientBootstrap, resolveGatewayUrlOverrideSource } =
|
||||
await import("./client-bootstrap.js");
|
||||
|
||||
describe("resolveGatewayUrlOverrideSource", () => {
|
||||
it("maps override url sources only", () => {
|
||||
expect(resolveGatewayUrlOverrideSource("cli --url")).toBe("cli");
|
||||
expect(resolveGatewayUrlOverrideSource("env OPENCLAW_GATEWAY_URL")).toBe("env");
|
||||
expect(resolveGatewayUrlOverrideSource("config gateway.remote.url")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveGatewayClientBootstrap", () => {
|
||||
beforeEach(() => {
|
||||
mockState.buildGatewayConnectionDetails.mockReset();
|
||||
mockState.resolveGatewayConnectionAuth.mockReset();
|
||||
mockState.resolveGatewayConnectionAuth.mockResolvedValue({
|
||||
token: undefined,
|
||||
password: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("passes cli override context into shared auth resolution", async () => {
|
||||
mockState.buildGatewayConnectionDetails.mockReturnValue({
|
||||
url: "wss://override.example/ws",
|
||||
urlSource: "cli --url",
|
||||
});
|
||||
|
||||
const result = await resolveGatewayClientBootstrap({
|
||||
config: {} as never,
|
||||
gatewayUrl: "wss://override.example/ws",
|
||||
env: process.env,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
url: "wss://override.example/ws",
|
||||
urlSource: "cli --url",
|
||||
auth: {
|
||||
token: undefined,
|
||||
password: undefined,
|
||||
},
|
||||
});
|
||||
expect(mockState.resolveGatewayConnectionAuth).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
env: process.env,
|
||||
urlOverride: "wss://override.example/ws",
|
||||
urlOverrideSource: "cli",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not mark config-derived urls as overrides", async () => {
|
||||
mockState.buildGatewayConnectionDetails.mockReturnValue({
|
||||
url: "wss://gateway.example/ws",
|
||||
urlSource: "config gateway.remote.url",
|
||||
});
|
||||
|
||||
await resolveGatewayClientBootstrap({
|
||||
config: {} as never,
|
||||
env: process.env,
|
||||
});
|
||||
|
||||
expect(mockState.resolveGatewayConnectionAuth).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
env: process.env,
|
||||
urlOverride: undefined,
|
||||
urlOverrideSource: undefined,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
46
src/gateway/client-bootstrap.ts
Normal file
46
src/gateway/client-bootstrap.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { buildGatewayConnectionDetails } from "./call.js";
|
||||
import type { ExplicitGatewayAuth } from "./call.js";
|
||||
import { resolveGatewayConnectionAuth } from "./connection-auth.js";
|
||||
|
||||
export function resolveGatewayUrlOverrideSource(urlSource: string): "cli" | "env" | undefined {
|
||||
if (urlSource === "cli --url") {
|
||||
return "cli";
|
||||
}
|
||||
if (urlSource === "env OPENCLAW_GATEWAY_URL") {
|
||||
return "env";
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export async function resolveGatewayClientBootstrap(params: {
|
||||
config: OpenClawConfig;
|
||||
gatewayUrl?: string;
|
||||
explicitAuth?: ExplicitGatewayAuth;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): Promise<{
|
||||
url: string;
|
||||
urlSource: string;
|
||||
auth: {
|
||||
token?: string;
|
||||
password?: string;
|
||||
};
|
||||
}> {
|
||||
const connection = buildGatewayConnectionDetails({
|
||||
config: params.config,
|
||||
url: params.gatewayUrl,
|
||||
});
|
||||
const urlOverrideSource = resolveGatewayUrlOverrideSource(connection.urlSource);
|
||||
const auth = await resolveGatewayConnectionAuth({
|
||||
config: params.config,
|
||||
explicitAuth: params.explicitAuth,
|
||||
env: params.env ?? process.env,
|
||||
urlOverride: urlOverrideSource ? connection.url : undefined,
|
||||
urlOverrideSource,
|
||||
});
|
||||
return {
|
||||
url: connection.url,
|
||||
urlSource: connection.urlSource,
|
||||
auth,
|
||||
};
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js";
|
||||
import { buildGatewayConnectionDetails } from "./call.js";
|
||||
import { resolveGatewayClientBootstrap } from "./client-bootstrap.js";
|
||||
import { GatewayClient, type GatewayClientOptions } from "./client.js";
|
||||
import { resolveGatewayConnectionAuth } from "./connection-auth.js";
|
||||
|
||||
export async function createOperatorApprovalsGatewayClient(
|
||||
params: Pick<
|
||||
@@ -13,27 +12,16 @@ export async function createOperatorApprovalsGatewayClient(
|
||||
gatewayUrl?: string;
|
||||
},
|
||||
): Promise<GatewayClient> {
|
||||
const { url: gatewayUrl, urlSource } = buildGatewayConnectionDetails({
|
||||
config: params.config,
|
||||
url: params.gatewayUrl,
|
||||
});
|
||||
const gatewayUrlOverrideSource =
|
||||
urlSource === "cli --url"
|
||||
? "cli"
|
||||
: urlSource === "env OPENCLAW_GATEWAY_URL"
|
||||
? "env"
|
||||
: undefined;
|
||||
const auth = await resolveGatewayConnectionAuth({
|
||||
const bootstrap = await resolveGatewayClientBootstrap({
|
||||
config: params.config,
|
||||
gatewayUrl: params.gatewayUrl,
|
||||
env: process.env,
|
||||
urlOverride: gatewayUrlOverrideSource ? gatewayUrl : undefined,
|
||||
urlOverrideSource: gatewayUrlOverrideSource,
|
||||
});
|
||||
|
||||
return new GatewayClient({
|
||||
url: gatewayUrl,
|
||||
token: auth.token,
|
||||
password: auth.password,
|
||||
url: bootstrap.url,
|
||||
token: bootstrap.auth.token,
|
||||
password: bootstrap.auth.password,
|
||||
clientName: GATEWAY_CLIENT_NAMES.GATEWAY_CLIENT,
|
||||
clientDisplayName: params.clientDisplayName,
|
||||
mode: GATEWAY_CLIENT_MODES.BACKEND,
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { OpenClawConfig } from "../config/config.js";
|
||||
import {
|
||||
resolveGatewayProbeAuthSafe,
|
||||
resolveGatewayProbeAuthSafeWithSecretInputs,
|
||||
resolveGatewayProbeTarget,
|
||||
resolveGatewayProbeAuthWithSecretInputs,
|
||||
} from "./probe-auth.js";
|
||||
|
||||
@@ -108,6 +109,39 @@ describe("resolveGatewayProbeAuthSafe", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveGatewayProbeTarget", () => {
|
||||
it("falls back to local probe mode when remote mode is configured without remote url", () => {
|
||||
expect(
|
||||
resolveGatewayProbeTarget({
|
||||
gateway: {
|
||||
mode: "remote",
|
||||
},
|
||||
} as OpenClawConfig),
|
||||
).toEqual({
|
||||
gatewayMode: "remote",
|
||||
mode: "local",
|
||||
remoteUrlMissing: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps remote probe mode when remote url is configured", () => {
|
||||
expect(
|
||||
resolveGatewayProbeTarget({
|
||||
gateway: {
|
||||
mode: "remote",
|
||||
remote: {
|
||||
url: "wss://gateway.example",
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig),
|
||||
).toEqual({
|
||||
gatewayMode: "remote",
|
||||
mode: "remote",
|
||||
remoteUrlMissing: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveGatewayProbeAuthSafeWithSecretInputs", () => {
|
||||
it("resolves env SecretRef token via async secret-inputs path", async () => {
|
||||
const result = await resolveGatewayProbeAuthSafeWithSecretInputs({
|
||||
|
||||
@@ -6,6 +6,12 @@ import {
|
||||
resolveGatewayProbeCredentialsFromConfig,
|
||||
} from "./credentials.js";
|
||||
|
||||
export type GatewayProbeTargetResolution = {
|
||||
gatewayMode: "local" | "remote";
|
||||
mode: "local" | "remote";
|
||||
remoteUrlMissing: boolean;
|
||||
};
|
||||
|
||||
function buildGatewayProbeCredentialPolicy(params: {
|
||||
cfg: OpenClawConfig;
|
||||
mode: "local" | "remote";
|
||||
@@ -23,6 +29,42 @@ function buildGatewayProbeCredentialPolicy(params: {
|
||||
};
|
||||
}
|
||||
|
||||
function resolveExplicitProbeAuth(explicitAuth?: ExplicitGatewayAuth): {
|
||||
token?: string;
|
||||
password?: string;
|
||||
} {
|
||||
const token = explicitAuth?.token?.trim() || undefined;
|
||||
const password = explicitAuth?.password?.trim() || undefined;
|
||||
return { token, password };
|
||||
}
|
||||
|
||||
function hasExplicitProbeAuth(auth: { token?: string; password?: string }): boolean {
|
||||
return Boolean(auth.token || auth.password);
|
||||
}
|
||||
|
||||
function buildUnresolvedProbeAuthWarning(path: string): string {
|
||||
return `${path} SecretRef is unresolved in this command path; probing without configured auth credentials.`;
|
||||
}
|
||||
|
||||
function resolveGatewayProbeWarning(error: unknown): string | undefined {
|
||||
if (!isGatewaySecretRefUnavailableError(error)) {
|
||||
throw error;
|
||||
}
|
||||
return buildUnresolvedProbeAuthWarning(error.path);
|
||||
}
|
||||
|
||||
export function resolveGatewayProbeTarget(cfg: OpenClawConfig): GatewayProbeTargetResolution {
|
||||
const gatewayMode = cfg.gateway?.mode === "remote" ? "remote" : "local";
|
||||
const remoteUrlRaw =
|
||||
typeof cfg.gateway?.remote?.url === "string" ? cfg.gateway.remote.url.trim() : "";
|
||||
const remoteUrlMissing = gatewayMode === "remote" && !remoteUrlRaw;
|
||||
return {
|
||||
gatewayMode,
|
||||
mode: gatewayMode === "remote" && !remoteUrlMissing ? "remote" : "local",
|
||||
remoteUrlMissing,
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveGatewayProbeAuth(params: {
|
||||
cfg: OpenClawConfig;
|
||||
mode: "local" | "remote";
|
||||
@@ -57,14 +99,10 @@ export async function resolveGatewayProbeAuthSafeWithSecretInputs(params: {
|
||||
auth: { token?: string; password?: string };
|
||||
warning?: string;
|
||||
}> {
|
||||
const explicitToken = params.explicitAuth?.token?.trim();
|
||||
const explicitPassword = params.explicitAuth?.password?.trim();
|
||||
if (explicitToken || explicitPassword) {
|
||||
const explicitAuth = resolveExplicitProbeAuth(params.explicitAuth);
|
||||
if (hasExplicitProbeAuth(explicitAuth)) {
|
||||
return {
|
||||
auth: {
|
||||
...(explicitToken ? { token: explicitToken } : {}),
|
||||
...(explicitPassword ? { password: explicitPassword } : {}),
|
||||
},
|
||||
auth: explicitAuth,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -72,12 +110,9 @@ export async function resolveGatewayProbeAuthSafeWithSecretInputs(params: {
|
||||
const auth = await resolveGatewayProbeAuthWithSecretInputs(params);
|
||||
return { auth };
|
||||
} catch (error) {
|
||||
if (!isGatewaySecretRefUnavailableError(error)) {
|
||||
throw error;
|
||||
}
|
||||
return {
|
||||
auth: {},
|
||||
warning: `${error.path} SecretRef is unresolved in this command path; probing without configured auth credentials.`,
|
||||
warning: resolveGatewayProbeWarning(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -91,26 +126,19 @@ export function resolveGatewayProbeAuthSafe(params: {
|
||||
auth: { token?: string; password?: string };
|
||||
warning?: string;
|
||||
} {
|
||||
const explicitToken = params.explicitAuth?.token?.trim();
|
||||
const explicitPassword = params.explicitAuth?.password?.trim();
|
||||
if (explicitToken || explicitPassword) {
|
||||
const explicitAuth = resolveExplicitProbeAuth(params.explicitAuth);
|
||||
if (hasExplicitProbeAuth(explicitAuth)) {
|
||||
return {
|
||||
auth: {
|
||||
...(explicitToken ? { token: explicitToken } : {}),
|
||||
...(explicitPassword ? { password: explicitPassword } : {}),
|
||||
},
|
||||
auth: explicitAuth,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
return { auth: resolveGatewayProbeAuth(params) };
|
||||
} catch (error) {
|
||||
if (!isGatewaySecretRefUnavailableError(error)) {
|
||||
throw error;
|
||||
}
|
||||
return {
|
||||
auth: {},
|
||||
warning: `${error.path} SecretRef is unresolved in this command path; probing without configured auth credentials.`,
|
||||
warning: resolveGatewayProbeWarning(error),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
69
src/gateway/secret-input-paths.ts
Normal file
69
src/gateway/secret-input-paths.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
|
||||
export type SupportedGatewaySecretInputPath =
|
||||
| "gateway.auth.token"
|
||||
| "gateway.auth.password"
|
||||
| "gateway.remote.token"
|
||||
| "gateway.remote.password";
|
||||
|
||||
export const ALL_GATEWAY_SECRET_INPUT_PATHS: SupportedGatewaySecretInputPath[] = [
|
||||
"gateway.auth.token",
|
||||
"gateway.auth.password",
|
||||
"gateway.remote.token",
|
||||
"gateway.remote.password",
|
||||
];
|
||||
|
||||
export function isSupportedGatewaySecretInputPath(
|
||||
path: string,
|
||||
): path is SupportedGatewaySecretInputPath {
|
||||
return ALL_GATEWAY_SECRET_INPUT_PATHS.includes(path as SupportedGatewaySecretInputPath);
|
||||
}
|
||||
|
||||
export function readGatewaySecretInputValue(
|
||||
config: OpenClawConfig,
|
||||
path: SupportedGatewaySecretInputPath,
|
||||
): unknown {
|
||||
if (path === "gateway.auth.token") {
|
||||
return config.gateway?.auth?.token;
|
||||
}
|
||||
if (path === "gateway.auth.password") {
|
||||
return config.gateway?.auth?.password;
|
||||
}
|
||||
if (path === "gateway.remote.token") {
|
||||
return config.gateway?.remote?.token;
|
||||
}
|
||||
return config.gateway?.remote?.password;
|
||||
}
|
||||
|
||||
export function assignResolvedGatewaySecretInput(params: {
|
||||
config: OpenClawConfig;
|
||||
path: SupportedGatewaySecretInputPath;
|
||||
value: string | undefined;
|
||||
}): void {
|
||||
const { config, path, value } = params;
|
||||
if (path === "gateway.auth.token") {
|
||||
if (config.gateway?.auth) {
|
||||
config.gateway.auth.token = value;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (path === "gateway.auth.password") {
|
||||
if (config.gateway?.auth) {
|
||||
config.gateway.auth.password = value;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (path === "gateway.remote.token") {
|
||||
if (config.gateway?.remote) {
|
||||
config.gateway.remote.token = value;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (config.gateway?.remote) {
|
||||
config.gateway.remote.password = value;
|
||||
}
|
||||
}
|
||||
|
||||
export function isTokenGatewaySecretInputPath(path: SupportedGatewaySecretInputPath): boolean {
|
||||
return path === "gateway.auth.token" || path === "gateway.remote.token";
|
||||
}
|
||||
317
src/gateway/server-methods/approval-shared.ts
Normal file
317
src/gateway/server-methods/approval-shared.ts
Normal file
@@ -0,0 +1,317 @@
|
||||
import { hasApprovalTurnSourceRoute } from "../../infra/approval-turn-source.js";
|
||||
import type { ExecApprovalDecision } from "../../infra/exec-approvals.js";
|
||||
import type {
|
||||
ExecApprovalIdLookupResult,
|
||||
ExecApprovalManager,
|
||||
ExecApprovalRecord,
|
||||
} from "../exec-approval-manager.js";
|
||||
import { ErrorCodes, errorShape } from "../protocol/index.js";
|
||||
import type { GatewayClient, GatewayRequestContext, RespondFn } from "./types.js";
|
||||
|
||||
export const APPROVAL_NOT_FOUND_DETAILS = {
|
||||
reason: ErrorCodes.APPROVAL_NOT_FOUND,
|
||||
} as const;
|
||||
|
||||
type PendingApprovalLookupError =
|
||||
| "missing"
|
||||
| {
|
||||
code: ErrorCodes.INVALID_REQUEST;
|
||||
message: string;
|
||||
};
|
||||
|
||||
type ApprovalTurnSourceFields = {
|
||||
turnSourceChannel?: string | null;
|
||||
turnSourceAccountId?: string | null;
|
||||
};
|
||||
|
||||
type RequestedApprovalEvent<TPayload extends ApprovalTurnSourceFields> = {
|
||||
id: string;
|
||||
request: TPayload;
|
||||
createdAtMs: number;
|
||||
expiresAtMs: number;
|
||||
};
|
||||
|
||||
function isPromiseLike<T>(value: T | Promise<T>): value is Promise<T> {
|
||||
return typeof value === "object" && value !== null && "then" in value;
|
||||
}
|
||||
|
||||
export function isApprovalDecision(value: string): value is ExecApprovalDecision {
|
||||
return value === "allow-once" || value === "allow-always" || value === "deny";
|
||||
}
|
||||
|
||||
export function respondUnknownOrExpiredApproval(respond: RespondFn): void {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(ErrorCodes.INVALID_REQUEST, "unknown or expired approval id", {
|
||||
details: APPROVAL_NOT_FOUND_DETAILS,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function resolvePendingApprovalLookupError(params: {
|
||||
resolvedId: ExecApprovalIdLookupResult;
|
||||
exposeAmbiguousPrefixError?: boolean;
|
||||
}): PendingApprovalLookupError {
|
||||
if (params.resolvedId.kind === "none") {
|
||||
return "missing";
|
||||
}
|
||||
if (params.resolvedId.kind === "ambiguous" && !params.exposeAmbiguousPrefixError) {
|
||||
return "missing";
|
||||
}
|
||||
return {
|
||||
code: ErrorCodes.INVALID_REQUEST,
|
||||
message: "ambiguous approval id prefix; use the full id",
|
||||
};
|
||||
}
|
||||
|
||||
export function resolvePendingApprovalRecord<TPayload>(params: {
|
||||
manager: ExecApprovalManager<TPayload>;
|
||||
inputId: string;
|
||||
exposeAmbiguousPrefixError?: boolean;
|
||||
}):
|
||||
| {
|
||||
ok: true;
|
||||
approvalId: string;
|
||||
snapshot: ExecApprovalRecord<TPayload>;
|
||||
}
|
||||
| {
|
||||
ok: false;
|
||||
response: PendingApprovalLookupError;
|
||||
} {
|
||||
const resolvedId = params.manager.lookupPendingId(params.inputId);
|
||||
if (resolvedId.kind !== "exact" && resolvedId.kind !== "prefix") {
|
||||
return {
|
||||
ok: false,
|
||||
response: resolvePendingApprovalLookupError({
|
||||
resolvedId,
|
||||
exposeAmbiguousPrefixError: params.exposeAmbiguousPrefixError,
|
||||
}),
|
||||
};
|
||||
}
|
||||
const snapshot = params.manager.getSnapshot(resolvedId.id);
|
||||
if (!snapshot || snapshot.resolvedAtMs !== undefined) {
|
||||
return { ok: false, response: "missing" };
|
||||
}
|
||||
return { ok: true, approvalId: resolvedId.id, snapshot };
|
||||
}
|
||||
|
||||
export function respondPendingApprovalLookupError(params: {
|
||||
respond: RespondFn;
|
||||
response: PendingApprovalLookupError;
|
||||
}): void {
|
||||
if (params.response === "missing") {
|
||||
respondUnknownOrExpiredApproval(params.respond);
|
||||
return;
|
||||
}
|
||||
params.respond(false, undefined, errorShape(params.response.code, params.response.message));
|
||||
}
|
||||
|
||||
export async function handleApprovalWaitDecision<TPayload>(params: {
|
||||
manager: ExecApprovalManager<TPayload>;
|
||||
inputId: unknown;
|
||||
respond: RespondFn;
|
||||
}): Promise<void> {
|
||||
const id = typeof params.inputId === "string" ? params.inputId.trim() : "";
|
||||
if (!id) {
|
||||
params.respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "id is required"));
|
||||
return;
|
||||
}
|
||||
const decisionPromise = params.manager.awaitDecision(id);
|
||||
if (!decisionPromise) {
|
||||
params.respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(ErrorCodes.INVALID_REQUEST, "approval expired or not found"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const snapshot = params.manager.getSnapshot(id);
|
||||
const decision = await decisionPromise;
|
||||
params.respond(
|
||||
true,
|
||||
{
|
||||
id,
|
||||
decision,
|
||||
createdAtMs: snapshot?.createdAtMs,
|
||||
expiresAtMs: snapshot?.expiresAtMs,
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
}
|
||||
|
||||
export async function handlePendingApprovalRequest<
|
||||
TPayload extends ApprovalTurnSourceFields,
|
||||
>(params: {
|
||||
manager: ExecApprovalManager<TPayload>;
|
||||
record: ExecApprovalRecord<TPayload>;
|
||||
decisionPromise: Promise<ExecApprovalDecision | null>;
|
||||
respond: RespondFn;
|
||||
context: GatewayRequestContext;
|
||||
clientConnId?: string;
|
||||
requestEventName: string;
|
||||
requestEvent: RequestedApprovalEvent<TPayload>;
|
||||
twoPhase: boolean;
|
||||
deliverRequest: () => boolean | Promise<boolean>;
|
||||
afterDecision?: (
|
||||
decision: ExecApprovalDecision | null,
|
||||
requestEvent: RequestedApprovalEvent<TPayload>,
|
||||
) => Promise<void> | void;
|
||||
afterDecisionErrorLabel?: string;
|
||||
}): Promise<void> {
|
||||
params.context.broadcast(params.requestEventName, params.requestEvent, { dropIfSlow: true });
|
||||
|
||||
const hasApprovalClients = params.context.hasExecApprovalClients?.(params.clientConnId) ?? false;
|
||||
const hasTurnSourceRoute = hasApprovalTurnSourceRoute({
|
||||
turnSourceChannel: params.record.request.turnSourceChannel,
|
||||
turnSourceAccountId: params.record.request.turnSourceAccountId,
|
||||
});
|
||||
const deliveredResult = params.deliverRequest();
|
||||
const delivered = isPromiseLike(deliveredResult) ? await deliveredResult : deliveredResult;
|
||||
|
||||
if (!hasApprovalClients && !hasTurnSourceRoute && !delivered) {
|
||||
params.manager.expire(params.record.id, "no-approval-route");
|
||||
params.respond(
|
||||
true,
|
||||
{
|
||||
id: params.record.id,
|
||||
decision: null,
|
||||
createdAtMs: params.record.createdAtMs,
|
||||
expiresAtMs: params.record.expiresAtMs,
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (params.twoPhase) {
|
||||
params.respond(
|
||||
true,
|
||||
{
|
||||
status: "accepted",
|
||||
id: params.record.id,
|
||||
createdAtMs: params.record.createdAtMs,
|
||||
expiresAtMs: params.record.expiresAtMs,
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
}
|
||||
|
||||
const decision = await params.decisionPromise;
|
||||
if (params.afterDecision) {
|
||||
try {
|
||||
await params.afterDecision(decision, params.requestEvent);
|
||||
} catch (err) {
|
||||
params.context.logGateway?.error?.(
|
||||
`${params.afterDecisionErrorLabel ?? "approval follow-up failed"}: ${String(err)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
params.respond(
|
||||
true,
|
||||
{
|
||||
id: params.record.id,
|
||||
decision,
|
||||
createdAtMs: params.record.createdAtMs,
|
||||
expiresAtMs: params.record.expiresAtMs,
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
}
|
||||
|
||||
export async function handleApprovalResolve<TPayload, TResolvedEvent extends object>(params: {
|
||||
manager: ExecApprovalManager<TPayload>;
|
||||
inputId: string;
|
||||
decision: ExecApprovalDecision;
|
||||
respond: RespondFn;
|
||||
context: GatewayRequestContext;
|
||||
client: GatewayClient | null;
|
||||
exposeAmbiguousPrefixError?: boolean;
|
||||
validateDecision?: (snapshot: ExecApprovalRecord<TPayload>) =>
|
||||
| {
|
||||
message: string;
|
||||
details?: Record<string, unknown>;
|
||||
}
|
||||
| null
|
||||
| undefined;
|
||||
resolvedEventName: string;
|
||||
buildResolvedEvent: (params: {
|
||||
approvalId: string;
|
||||
decision: ExecApprovalDecision;
|
||||
resolvedBy: string | null;
|
||||
snapshot: ExecApprovalRecord<TPayload>;
|
||||
nowMs: number;
|
||||
}) => TResolvedEvent;
|
||||
forwardResolved?: (event: TResolvedEvent) => Promise<void> | void;
|
||||
forwardResolvedErrorLabel?: string;
|
||||
extraResolvedHandlers?: Array<{
|
||||
run: (event: TResolvedEvent) => Promise<void> | void;
|
||||
errorLabel: string;
|
||||
}>;
|
||||
}): Promise<void> {
|
||||
const resolved = resolvePendingApprovalRecord({
|
||||
manager: params.manager,
|
||||
inputId: params.inputId,
|
||||
exposeAmbiguousPrefixError: params.exposeAmbiguousPrefixError,
|
||||
});
|
||||
if (!resolved.ok) {
|
||||
respondPendingApprovalLookupError({ respond: params.respond, response: resolved.response });
|
||||
return;
|
||||
}
|
||||
|
||||
const validationError = params.validateDecision?.(resolved.snapshot);
|
||||
if (validationError) {
|
||||
params.respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(
|
||||
ErrorCodes.INVALID_REQUEST,
|
||||
validationError.message,
|
||||
validationError.details ? { details: validationError.details } : undefined,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const resolvedBy =
|
||||
params.client?.connect?.client?.displayName ?? params.client?.connect?.client?.id ?? null;
|
||||
const ok = params.manager.resolve(resolved.approvalId, params.decision, resolvedBy);
|
||||
if (!ok) {
|
||||
respondUnknownOrExpiredApproval(params.respond);
|
||||
return;
|
||||
}
|
||||
|
||||
const resolvedEvent = params.buildResolvedEvent({
|
||||
approvalId: resolved.approvalId,
|
||||
decision: params.decision,
|
||||
resolvedBy,
|
||||
snapshot: resolved.snapshot,
|
||||
nowMs: Date.now(),
|
||||
});
|
||||
params.context.broadcast(params.resolvedEventName, resolvedEvent, { dropIfSlow: true });
|
||||
|
||||
const followUps = [
|
||||
params.forwardResolved
|
||||
? {
|
||||
run: params.forwardResolved,
|
||||
errorLabel: params.forwardResolvedErrorLabel ?? "approval resolve follow-up failed",
|
||||
}
|
||||
: null,
|
||||
...(params.extraResolvedHandlers ?? []),
|
||||
].filter(
|
||||
(
|
||||
entry,
|
||||
): entry is { run: (event: TResolvedEvent) => Promise<void> | void; errorLabel: string } =>
|
||||
Boolean(entry),
|
||||
);
|
||||
|
||||
for (const followUp of followUps) {
|
||||
try {
|
||||
await followUp.run(resolvedEvent);
|
||||
} catch (err) {
|
||||
params.context.logGateway?.error?.(`${followUp.errorLabel}: ${String(err)}`);
|
||||
}
|
||||
}
|
||||
|
||||
params.respond(true, { ok: true }, undefined);
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import { hasApprovalTurnSourceRoute } from "../../infra/approval-turn-source.js";
|
||||
import {
|
||||
resolveExecApprovalCommandDisplay,
|
||||
sanitizeExecApprovalDisplayText,
|
||||
@@ -8,7 +7,6 @@ import {
|
||||
DEFAULT_EXEC_APPROVAL_TIMEOUT_MS,
|
||||
resolveExecApprovalAllowedDecisions,
|
||||
resolveExecApprovalRequestAllowedDecisions,
|
||||
type ExecApprovalDecision,
|
||||
type ExecApprovalRequest,
|
||||
type ExecApprovalResolved,
|
||||
} from "../../infra/exec-approvals.js";
|
||||
@@ -26,12 +24,16 @@ import {
|
||||
validateExecApprovalRequestParams,
|
||||
validateExecApprovalResolveParams,
|
||||
} from "../protocol/index.js";
|
||||
import {
|
||||
handleApprovalWaitDecision,
|
||||
handlePendingApprovalRequest,
|
||||
handleApprovalResolve,
|
||||
isApprovalDecision,
|
||||
respondPendingApprovalLookupError,
|
||||
resolvePendingApprovalRecord,
|
||||
} from "./approval-shared.js";
|
||||
import type { GatewayRequestHandlers } from "./types.js";
|
||||
|
||||
const APPROVAL_NOT_FOUND_DETAILS = {
|
||||
reason: ErrorCodes.APPROVAL_NOT_FOUND,
|
||||
} as const;
|
||||
|
||||
const APPROVAL_ALLOW_ALWAYS_UNAVAILABLE_DETAILS = {
|
||||
reason: "APPROVAL_ALLOW_ALWAYS_UNAVAILABLE",
|
||||
} as const;
|
||||
@@ -42,27 +44,6 @@ type ExecApprovalIosPushDelivery = {
|
||||
handleExpired?: (request: ExecApprovalRequest) => Promise<void>;
|
||||
};
|
||||
|
||||
function resolvePendingApprovalRecord(manager: ExecApprovalManager, inputId: string) {
|
||||
const resolvedId = manager.lookupPendingId(inputId);
|
||||
if (resolvedId.kind === "none") {
|
||||
return { ok: false as const, response: "missing" as const };
|
||||
}
|
||||
if (resolvedId.kind === "ambiguous") {
|
||||
return {
|
||||
ok: false as const,
|
||||
response: {
|
||||
code: ErrorCodes.INVALID_REQUEST,
|
||||
message: "ambiguous approval id prefix; use the full id",
|
||||
},
|
||||
};
|
||||
}
|
||||
const snapshot = manager.getSnapshot(resolvedId.id);
|
||||
if (!snapshot || snapshot.resolvedAtMs !== undefined) {
|
||||
return { ok: false as const, response: "missing" as const };
|
||||
}
|
||||
return { ok: true as const, approvalId: resolvedId.id, snapshot };
|
||||
}
|
||||
|
||||
export function createExecApprovalHandlers(
|
||||
manager: ExecApprovalManager,
|
||||
opts?: { forwarder?: ExecApprovalForwarder; iosPushDelivery?: ExecApprovalIosPushDelivery },
|
||||
@@ -83,19 +64,13 @@ export function createExecApprovalHandlers(
|
||||
return;
|
||||
}
|
||||
const p = params as { id: string };
|
||||
const resolved = resolvePendingApprovalRecord(manager, p.id);
|
||||
const resolved = resolvePendingApprovalRecord({
|
||||
manager,
|
||||
inputId: p.id,
|
||||
exposeAmbiguousPrefixError: true,
|
||||
});
|
||||
if (!resolved.ok) {
|
||||
if (resolved.response === "missing") {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(ErrorCodes.INVALID_REQUEST, "unknown or expired approval id", {
|
||||
details: APPROVAL_NOT_FOUND_DETAILS,
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
respond(false, undefined, errorShape(resolved.response.code, resolved.response.message));
|
||||
respondPendingApprovalLookupError({ respond, response: resolved.response });
|
||||
return;
|
||||
}
|
||||
const { commandText, commandPreview } = resolveExecApprovalCommandDisplay(
|
||||
@@ -272,107 +247,63 @@ export function createExecApprovalHandlers(
|
||||
createdAtMs: record.createdAtMs,
|
||||
expiresAtMs: record.expiresAtMs,
|
||||
};
|
||||
context.broadcast("exec.approval.requested", requestEvent, { dropIfSlow: true });
|
||||
const hasExecApprovalClients = context.hasExecApprovalClients?.(client?.connId) ?? false;
|
||||
const hasTurnSourceRoute = hasApprovalTurnSourceRoute({
|
||||
turnSourceChannel: record.request.turnSourceChannel,
|
||||
turnSourceAccountId: record.request.turnSourceAccountId,
|
||||
});
|
||||
let forwarded = false;
|
||||
if (opts?.forwarder) {
|
||||
try {
|
||||
forwarded = await opts.forwarder.handleRequested(requestEvent);
|
||||
} catch (err) {
|
||||
context.logGateway?.error?.(`exec approvals: forward request failed: ${String(err)}`);
|
||||
}
|
||||
}
|
||||
let deliveredToIosPush = false;
|
||||
if (opts?.iosPushDelivery?.handleRequested) {
|
||||
try {
|
||||
deliveredToIosPush = await opts.iosPushDelivery.handleRequested(requestEvent);
|
||||
} catch (err) {
|
||||
context.logGateway?.error?.(`exec approvals: iOS push request failed: ${String(err)}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasExecApprovalClients && !forwarded && !hasTurnSourceRoute && !deliveredToIosPush) {
|
||||
manager.expire(record.id, "no-approval-route");
|
||||
respond(
|
||||
true,
|
||||
{
|
||||
id: record.id,
|
||||
decision: null,
|
||||
createdAtMs: record.createdAtMs,
|
||||
expiresAtMs: record.expiresAtMs,
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Only send immediate "accepted" response when twoPhase is requested.
|
||||
// This preserves single-response semantics for existing callers.
|
||||
if (twoPhase) {
|
||||
respond(
|
||||
true,
|
||||
{
|
||||
status: "accepted",
|
||||
id: record.id,
|
||||
createdAtMs: record.createdAtMs,
|
||||
expiresAtMs: record.expiresAtMs,
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
}
|
||||
|
||||
const decision = await decisionPromise;
|
||||
if (decision === null) {
|
||||
void opts?.iosPushDelivery?.handleExpired?.(requestEvent).catch((err) => {
|
||||
context.logGateway?.error?.(`exec approvals: iOS push expire failed: ${String(err)}`);
|
||||
});
|
||||
}
|
||||
// Send final response with decision for callers using expectFinal:true.
|
||||
respond(
|
||||
true,
|
||||
{
|
||||
id: record.id,
|
||||
decision,
|
||||
createdAtMs: record.createdAtMs,
|
||||
expiresAtMs: record.expiresAtMs,
|
||||
await handlePendingApprovalRequest({
|
||||
manager,
|
||||
record,
|
||||
decisionPromise,
|
||||
respond,
|
||||
context,
|
||||
clientConnId: client?.connId,
|
||||
requestEventName: "exec.approval.requested",
|
||||
requestEvent,
|
||||
twoPhase,
|
||||
deliverRequest: () => {
|
||||
const deliveryTasks: Array<Promise<boolean>> = [];
|
||||
if (opts?.forwarder) {
|
||||
deliveryTasks.push(
|
||||
opts.forwarder.handleRequested(requestEvent).catch((err) => {
|
||||
context.logGateway?.error?.(
|
||||
`exec approvals: forward request failed: ${String(err)}`,
|
||||
);
|
||||
return false;
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (opts?.iosPushDelivery?.handleRequested) {
|
||||
deliveryTasks.push(
|
||||
opts.iosPushDelivery.handleRequested(requestEvent).catch((err) => {
|
||||
context.logGateway?.error?.(
|
||||
`exec approvals: iOS push request failed: ${String(err)}`,
|
||||
);
|
||||
return false;
|
||||
}),
|
||||
);
|
||||
}
|
||||
if (deliveryTasks.length === 0) {
|
||||
return false;
|
||||
}
|
||||
return (async () => {
|
||||
let delivered = false;
|
||||
for (const task of deliveryTasks) {
|
||||
delivered = (await task) || delivered;
|
||||
}
|
||||
return delivered;
|
||||
})();
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
afterDecision: async (decision) => {
|
||||
if (decision === null) {
|
||||
await opts?.iosPushDelivery?.handleExpired?.(requestEvent);
|
||||
}
|
||||
},
|
||||
afterDecisionErrorLabel: "exec approvals: iOS push expire failed",
|
||||
});
|
||||
},
|
||||
"exec.approval.waitDecision": async ({ params, respond }) => {
|
||||
const p = params as { id?: string };
|
||||
const id = typeof p.id === "string" ? p.id.trim() : "";
|
||||
if (!id) {
|
||||
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "id is required"));
|
||||
return;
|
||||
}
|
||||
const decisionPromise = manager.awaitDecision(id);
|
||||
if (!decisionPromise) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(ErrorCodes.INVALID_REQUEST, "approval expired or not found"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Capture snapshot before await (entry may be deleted after grace period)
|
||||
const snapshot = manager.getSnapshot(id);
|
||||
const decision = await decisionPromise;
|
||||
// Return decision (can be null on timeout) - let clients handle via askFallback
|
||||
respond(
|
||||
true,
|
||||
{
|
||||
id,
|
||||
decision,
|
||||
createdAtMs: snapshot?.createdAtMs,
|
||||
expiresAtMs: snapshot?.expiresAtMs,
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
await handleApprovalWaitDecision({
|
||||
manager,
|
||||
inputId: (params as { id?: string }).id,
|
||||
respond,
|
||||
});
|
||||
},
|
||||
"exec.approval.resolve": async ({ params, respond, client, context }) => {
|
||||
if (!validateExecApprovalResolveParams(params)) {
|
||||
@@ -389,70 +320,48 @@ export function createExecApprovalHandlers(
|
||||
return;
|
||||
}
|
||||
const p = params as { id: string; decision: string };
|
||||
const decision = p.decision as ExecApprovalDecision;
|
||||
if (decision !== "allow-once" && decision !== "allow-always" && decision !== "deny") {
|
||||
if (!isApprovalDecision(p.decision)) {
|
||||
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "invalid decision"));
|
||||
return;
|
||||
}
|
||||
const resolved = resolvePendingApprovalRecord(manager, p.id);
|
||||
if (!resolved.ok) {
|
||||
if (resolved.response === "missing") {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(ErrorCodes.INVALID_REQUEST, "unknown or expired approval id", {
|
||||
details: APPROVAL_NOT_FOUND_DETAILS,
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
respond(false, undefined, errorShape(resolved.response.code, resolved.response.message));
|
||||
return;
|
||||
}
|
||||
const approvalId = resolved.approvalId;
|
||||
const snapshot = resolved.snapshot;
|
||||
const allowedDecisions = resolveExecApprovalRequestAllowedDecisions(snapshot?.request);
|
||||
if (snapshot && !allowedDecisions.includes(decision)) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(
|
||||
ErrorCodes.INVALID_REQUEST,
|
||||
"allow-always is unavailable because the effective policy requires approval every time",
|
||||
{
|
||||
details: APPROVAL_ALLOW_ALWAYS_UNAVAILABLE_DETAILS,
|
||||
},
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const resolvedBy = client?.connect?.client?.displayName ?? client?.connect?.client?.id;
|
||||
const ok = manager.resolve(approvalId, decision, resolvedBy ?? null);
|
||||
if (!ok) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(ErrorCodes.INVALID_REQUEST, "unknown or expired approval id", {
|
||||
details: APPROVAL_NOT_FOUND_DETAILS,
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const resolvedEvent: ExecApprovalResolved = {
|
||||
id: approvalId,
|
||||
decision,
|
||||
resolvedBy,
|
||||
ts: Date.now(),
|
||||
request: snapshot?.request,
|
||||
};
|
||||
context.broadcast("exec.approval.resolved", resolvedEvent, { dropIfSlow: true });
|
||||
void opts?.forwarder?.handleResolved(resolvedEvent).catch((err) => {
|
||||
context.logGateway?.error?.(`exec approvals: forward resolve failed: ${String(err)}`);
|
||||
await handleApprovalResolve({
|
||||
manager,
|
||||
inputId: p.id,
|
||||
decision: p.decision,
|
||||
respond,
|
||||
context,
|
||||
client,
|
||||
exposeAmbiguousPrefixError: true,
|
||||
validateDecision: (snapshot) => {
|
||||
const allowedDecisions = resolveExecApprovalRequestAllowedDecisions(snapshot.request);
|
||||
return allowedDecisions.includes(p.decision)
|
||||
? null
|
||||
: {
|
||||
message:
|
||||
"allow-always is unavailable because the effective policy requires approval every time",
|
||||
details: APPROVAL_ALLOW_ALWAYS_UNAVAILABLE_DETAILS,
|
||||
};
|
||||
},
|
||||
resolvedEventName: "exec.approval.resolved",
|
||||
buildResolvedEvent: ({ approvalId, decision, resolvedBy, snapshot, nowMs }) =>
|
||||
({
|
||||
id: approvalId,
|
||||
decision,
|
||||
resolvedBy,
|
||||
ts: nowMs,
|
||||
request: snapshot.request,
|
||||
}) satisfies ExecApprovalResolved,
|
||||
forwardResolved: (resolvedEvent) => opts?.forwarder?.handleResolved(resolvedEvent),
|
||||
forwardResolvedErrorLabel: "exec approvals: forward resolve failed",
|
||||
extraResolvedHandlers: opts?.iosPushDelivery?.handleResolved
|
||||
? [
|
||||
{
|
||||
run: (resolvedEvent) => opts.iosPushDelivery!.handleResolved!(resolvedEvent),
|
||||
errorLabel: "exec approvals: iOS push resolve failed",
|
||||
},
|
||||
]
|
||||
: undefined,
|
||||
});
|
||||
void opts?.iosPushDelivery?.handleResolved?.(resolvedEvent).catch((err) => {
|
||||
context.logGateway?.error?.(`exec approvals: iOS push resolve failed: ${String(err)}`);
|
||||
});
|
||||
respond(true, { ok: true }, undefined);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { hasApprovalTurnSourceRoute } from "../../infra/approval-turn-source.js";
|
||||
import type { ExecApprovalForwarder } from "../../infra/exec-approval-forwarder.js";
|
||||
import type { ExecApprovalDecision } from "../../infra/exec-approvals.js";
|
||||
import type { PluginApprovalRequestPayload } from "../../infra/plugin-approvals.js";
|
||||
@@ -15,12 +14,14 @@ import {
|
||||
validatePluginApprovalRequestParams,
|
||||
validatePluginApprovalResolveParams,
|
||||
} from "../protocol/index.js";
|
||||
import {
|
||||
handleApprovalResolve,
|
||||
handleApprovalWaitDecision,
|
||||
handlePendingApprovalRequest,
|
||||
isApprovalDecision,
|
||||
} from "./approval-shared.js";
|
||||
import type { GatewayRequestHandlers } from "./types.js";
|
||||
|
||||
const APPROVAL_NOT_FOUND_DETAILS = {
|
||||
reason: ErrorCodes.APPROVAL_NOT_FOUND,
|
||||
} as const;
|
||||
|
||||
export function createPluginApprovalHandlers(
|
||||
manager: ExecApprovalManager<PluginApprovalRequestPayload>,
|
||||
opts?: { forwarder?: ExecApprovalForwarder },
|
||||
@@ -96,105 +97,41 @@ export function createPluginApprovalHandlers(
|
||||
return;
|
||||
}
|
||||
|
||||
context.broadcast(
|
||||
"plugin.approval.requested",
|
||||
{
|
||||
id: record.id,
|
||||
request: record.request,
|
||||
createdAtMs: record.createdAtMs,
|
||||
expiresAtMs: record.expiresAtMs,
|
||||
},
|
||||
{ dropIfSlow: true },
|
||||
);
|
||||
const requestEvent = {
|
||||
id: record.id,
|
||||
request: record.request,
|
||||
createdAtMs: record.createdAtMs,
|
||||
expiresAtMs: record.expiresAtMs,
|
||||
};
|
||||
|
||||
let forwarded = false;
|
||||
if (opts?.forwarder?.handlePluginApprovalRequested) {
|
||||
try {
|
||||
forwarded = await opts.forwarder.handlePluginApprovalRequested({
|
||||
id: record.id,
|
||||
request: record.request,
|
||||
createdAtMs: record.createdAtMs,
|
||||
expiresAtMs: record.expiresAtMs,
|
||||
await handlePendingApprovalRequest({
|
||||
manager,
|
||||
record,
|
||||
decisionPromise,
|
||||
respond,
|
||||
context,
|
||||
clientConnId: client?.connId,
|
||||
requestEventName: "plugin.approval.requested",
|
||||
requestEvent,
|
||||
twoPhase,
|
||||
deliverRequest: () => {
|
||||
if (!opts?.forwarder?.handlePluginApprovalRequested) {
|
||||
return false;
|
||||
}
|
||||
return opts.forwarder.handlePluginApprovalRequested(requestEvent).catch((err) => {
|
||||
context.logGateway?.error?.(`plugin approvals: forward request failed: ${String(err)}`);
|
||||
return false;
|
||||
});
|
||||
} catch (err) {
|
||||
context.logGateway?.error?.(`plugin approvals: forward request failed: ${String(err)}`);
|
||||
}
|
||||
}
|
||||
|
||||
const hasApprovalClients = context.hasExecApprovalClients?.(client?.connId) ?? false;
|
||||
const hasTurnSourceRoute = hasApprovalTurnSourceRoute({
|
||||
turnSourceChannel: record.request.turnSourceChannel,
|
||||
turnSourceAccountId: record.request.turnSourceAccountId,
|
||||
});
|
||||
if (!hasApprovalClients && !forwarded && !hasTurnSourceRoute) {
|
||||
manager.expire(record.id, "no-approval-route");
|
||||
respond(
|
||||
true,
|
||||
{
|
||||
id: record.id,
|
||||
decision: null,
|
||||
createdAtMs: record.createdAtMs,
|
||||
expiresAtMs: record.expiresAtMs,
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (twoPhase) {
|
||||
respond(
|
||||
true,
|
||||
{
|
||||
status: "accepted",
|
||||
id: record.id,
|
||||
createdAtMs: record.createdAtMs,
|
||||
expiresAtMs: record.expiresAtMs,
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
}
|
||||
|
||||
const decision = await decisionPromise;
|
||||
respond(
|
||||
true,
|
||||
{
|
||||
id: record.id,
|
||||
decision,
|
||||
createdAtMs: record.createdAtMs,
|
||||
expiresAtMs: record.expiresAtMs,
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
"plugin.approval.waitDecision": async ({ params, respond }) => {
|
||||
const p = params as { id?: string };
|
||||
const id = typeof p.id === "string" ? p.id.trim() : "";
|
||||
if (!id) {
|
||||
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "id is required"));
|
||||
return;
|
||||
}
|
||||
const decisionPromise = manager.awaitDecision(id);
|
||||
if (!decisionPromise) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(ErrorCodes.INVALID_REQUEST, "approval expired or not found"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const snapshot = manager.getSnapshot(id);
|
||||
const decision = await decisionPromise;
|
||||
respond(
|
||||
true,
|
||||
{
|
||||
id,
|
||||
decision,
|
||||
createdAtMs: snapshot?.createdAtMs,
|
||||
expiresAtMs: snapshot?.expiresAtMs,
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
await handleApprovalWaitDecision({
|
||||
manager,
|
||||
inputId: (params as { id?: string }).id,
|
||||
respond,
|
||||
});
|
||||
},
|
||||
|
||||
"plugin.approval.resolve": async ({ params, respond, client, context }) => {
|
||||
@@ -212,63 +149,30 @@ export function createPluginApprovalHandlers(
|
||||
return;
|
||||
}
|
||||
const p = params as { id: string; decision: string };
|
||||
const decision = p.decision as ExecApprovalDecision;
|
||||
if (decision !== "allow-once" && decision !== "allow-always" && decision !== "deny") {
|
||||
if (!isApprovalDecision(p.decision)) {
|
||||
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "invalid decision"));
|
||||
return;
|
||||
}
|
||||
const resolvedId = manager.lookupPendingId(p.id);
|
||||
if (resolvedId.kind === "none" || resolvedId.kind === "ambiguous") {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(ErrorCodes.INVALID_REQUEST, "unknown or expired approval id", {
|
||||
details: APPROVAL_NOT_FOUND_DETAILS,
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const approvalId = resolvedId.id;
|
||||
const snapshot = manager.getSnapshot(approvalId);
|
||||
if (!snapshot || snapshot.resolvedAtMs !== undefined) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(ErrorCodes.INVALID_REQUEST, "unknown or expired approval id", {
|
||||
details: APPROVAL_NOT_FOUND_DETAILS,
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const resolvedBy = client?.connect?.client?.displayName ?? client?.connect?.client?.id;
|
||||
const ok = manager.resolve(approvalId, decision, resolvedBy ?? null);
|
||||
if (!ok) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
errorShape(ErrorCodes.INVALID_REQUEST, "unknown or expired approval id", {
|
||||
details: APPROVAL_NOT_FOUND_DETAILS,
|
||||
}),
|
||||
);
|
||||
return;
|
||||
}
|
||||
context.broadcast(
|
||||
"plugin.approval.resolved",
|
||||
{ id: approvalId, decision, resolvedBy, ts: Date.now(), request: snapshot?.request },
|
||||
{ dropIfSlow: true },
|
||||
);
|
||||
void opts?.forwarder
|
||||
?.handlePluginApprovalResolved?.({
|
||||
await handleApprovalResolve({
|
||||
manager,
|
||||
inputId: p.id,
|
||||
decision: p.decision,
|
||||
respond,
|
||||
context,
|
||||
client,
|
||||
exposeAmbiguousPrefixError: false,
|
||||
resolvedEventName: "plugin.approval.resolved",
|
||||
buildResolvedEvent: ({ approvalId, decision, resolvedBy, snapshot, nowMs }) => ({
|
||||
id: approvalId,
|
||||
decision,
|
||||
resolvedBy,
|
||||
ts: Date.now(),
|
||||
request: snapshot?.request,
|
||||
})
|
||||
.catch((err) => {
|
||||
context.logGateway?.error?.(`plugin approvals: forward resolve failed: ${String(err)}`);
|
||||
});
|
||||
respond(true, { ok: true }, undefined);
|
||||
ts: nowMs,
|
||||
request: snapshot.request,
|
||||
}),
|
||||
forwardResolved: (resolvedEvent) =>
|
||||
opts?.forwarder?.handlePluginApprovalResolved?.(resolvedEvent),
|
||||
forwardResolvedErrorLabel: "plugin approvals: forward resolve failed",
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -414,6 +414,39 @@ describe("gateway send mirroring", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("includes optional poll delivery identifiers in the gateway payload", async () => {
|
||||
mocks.sendPoll.mockResolvedValue({
|
||||
messageId: "poll-rich",
|
||||
channelId: "C123",
|
||||
conversationId: "conv-1",
|
||||
toJid: "jid-1",
|
||||
pollId: "poll-meta-1",
|
||||
});
|
||||
|
||||
const { respond } = await runPoll({
|
||||
to: "channel:C1",
|
||||
question: "Q?",
|
||||
options: ["A", "B"],
|
||||
channel: "slack",
|
||||
idempotencyKey: "idem-poll-rich",
|
||||
});
|
||||
|
||||
expect(respond).toHaveBeenCalledWith(
|
||||
true,
|
||||
expect.objectContaining({
|
||||
runId: "idem-poll-rich",
|
||||
messageId: "poll-rich",
|
||||
channel: "slack",
|
||||
channelId: "C123",
|
||||
conversationId: "conv-1",
|
||||
toJid: "jid-1",
|
||||
pollId: "poll-meta-1",
|
||||
}),
|
||||
undefined,
|
||||
expect.objectContaining({ channel: "slack" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("auto-picks the single configured channel for poll", async () => {
|
||||
const { respond } = await runPoll({
|
||||
to: "x",
|
||||
|
||||
@@ -92,6 +92,88 @@ async function resolveRequestedChannel(params: {
|
||||
return { cfg, channel };
|
||||
}
|
||||
|
||||
function resolveGatewayOutboundTarget(params: {
|
||||
channel: string;
|
||||
to: string;
|
||||
cfg: ReturnType<typeof loadConfig>;
|
||||
accountId?: string;
|
||||
}):
|
||||
| {
|
||||
ok: true;
|
||||
to: string;
|
||||
}
|
||||
| {
|
||||
ok: false;
|
||||
error: ReturnType<typeof errorShape>;
|
||||
} {
|
||||
const resolved = resolveOutboundTarget({
|
||||
channel: params.channel,
|
||||
to: params.to,
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
mode: "explicit",
|
||||
});
|
||||
if (!resolved.ok) {
|
||||
return {
|
||||
ok: false,
|
||||
error: errorShape(ErrorCodes.INVALID_REQUEST, String(resolved.error)),
|
||||
};
|
||||
}
|
||||
return { ok: true, to: resolved.to };
|
||||
}
|
||||
|
||||
function buildGatewayDeliveryPayload(params: {
|
||||
runId: string;
|
||||
channel: string;
|
||||
result: Record<string, unknown>;
|
||||
}): Record<string, unknown> {
|
||||
const payload: Record<string, unknown> = {
|
||||
runId: params.runId,
|
||||
messageId: params.result.messageId,
|
||||
channel: params.channel,
|
||||
};
|
||||
if ("chatId" in params.result) {
|
||||
payload.chatId = params.result.chatId;
|
||||
}
|
||||
if ("channelId" in params.result) {
|
||||
payload.channelId = params.result.channelId;
|
||||
}
|
||||
if ("toJid" in params.result) {
|
||||
payload.toJid = params.result.toJid;
|
||||
}
|
||||
if ("conversationId" in params.result) {
|
||||
payload.conversationId = params.result.conversationId;
|
||||
}
|
||||
if ("pollId" in params.result) {
|
||||
payload.pollId = params.result.pollId;
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
function cacheGatewayDedupeSuccess(params: {
|
||||
context: GatewayRequestContext;
|
||||
dedupeKey: string;
|
||||
payload: Record<string, unknown>;
|
||||
}) {
|
||||
params.context.dedupe.set(params.dedupeKey, {
|
||||
ts: Date.now(),
|
||||
ok: true,
|
||||
payload: params.payload,
|
||||
});
|
||||
}
|
||||
|
||||
function cacheGatewayDedupeFailure(params: {
|
||||
context: GatewayRequestContext;
|
||||
dedupeKey: string;
|
||||
error: ReturnType<typeof errorShape>;
|
||||
}) {
|
||||
params.context.dedupe.set(params.dedupeKey, {
|
||||
ts: Date.now(),
|
||||
ok: false,
|
||||
error: params.error,
|
||||
});
|
||||
}
|
||||
|
||||
export const sendHandlers: GatewayRequestHandlers = {
|
||||
send: async ({ params, respond, context, client }) => {
|
||||
const p = params;
|
||||
@@ -186,27 +268,26 @@ export const sendHandlers: GatewayRequestHandlers = {
|
||||
|
||||
const work = (async (): Promise<InflightResult> => {
|
||||
try {
|
||||
const resolved = resolveOutboundTarget({
|
||||
const resolvedTarget = resolveGatewayOutboundTarget({
|
||||
channel: outboundChannel,
|
||||
to,
|
||||
cfg,
|
||||
accountId,
|
||||
mode: "explicit",
|
||||
});
|
||||
if (!resolved.ok) {
|
||||
if (!resolvedTarget.ok) {
|
||||
return {
|
||||
ok: false,
|
||||
error: errorShape(ErrorCodes.INVALID_REQUEST, String(resolved.error)),
|
||||
error: resolvedTarget.error,
|
||||
meta: { channel },
|
||||
};
|
||||
}
|
||||
const idLikeTarget = await maybeResolveIdLikeTarget({
|
||||
cfg,
|
||||
channel,
|
||||
input: resolved.to,
|
||||
input: resolvedTarget.to,
|
||||
accountId,
|
||||
});
|
||||
const deliveryTarget = idLikeTarget?.to ?? resolved.to;
|
||||
const deliveryTarget = idLikeTarget?.to ?? resolvedTarget.to;
|
||||
const outboundDeps = context.deps ? createOutboundSendDeps(context.deps) : undefined;
|
||||
const mirrorPayloads = normalizeReplyPayloadsForDelivery([
|
||||
{ text: message, mediaUrl, mediaUrls },
|
||||
@@ -290,28 +371,8 @@ export const sendHandlers: GatewayRequestHandlers = {
|
||||
if (!result) {
|
||||
throw new Error("No delivery result");
|
||||
}
|
||||
const payload: Record<string, unknown> = {
|
||||
runId: idem,
|
||||
messageId: result.messageId,
|
||||
channel,
|
||||
};
|
||||
if ("chatId" in result) {
|
||||
payload.chatId = result.chatId;
|
||||
}
|
||||
if ("channelId" in result) {
|
||||
payload.channelId = result.channelId;
|
||||
}
|
||||
if ("toJid" in result) {
|
||||
payload.toJid = result.toJid;
|
||||
}
|
||||
if ("conversationId" in result) {
|
||||
payload.conversationId = result.conversationId;
|
||||
}
|
||||
context.dedupe.set(dedupeKey, {
|
||||
ts: Date.now(),
|
||||
ok: true,
|
||||
payload,
|
||||
});
|
||||
const payload = buildGatewayDeliveryPayload({ runId: idem, channel, result });
|
||||
cacheGatewayDedupeSuccess({ context, dedupeKey, payload });
|
||||
return {
|
||||
ok: true,
|
||||
payload,
|
||||
@@ -319,11 +380,7 @@ export const sendHandlers: GatewayRequestHandlers = {
|
||||
};
|
||||
} catch (err) {
|
||||
const error = errorShape(ErrorCodes.UNAVAILABLE, String(err));
|
||||
context.dedupe.set(dedupeKey, {
|
||||
ts: Date.now(),
|
||||
ok: false,
|
||||
error,
|
||||
});
|
||||
cacheGatewayDedupeFailure({ context, dedupeKey, error });
|
||||
return { ok: false, error, meta: { channel, error: formatForLog(err) } };
|
||||
}
|
||||
})();
|
||||
@@ -429,15 +486,14 @@ export const sendHandlers: GatewayRequestHandlers = {
|
||||
);
|
||||
return;
|
||||
}
|
||||
const resolved = resolveOutboundTarget({
|
||||
const resolvedTarget = resolveGatewayOutboundTarget({
|
||||
channel: channel,
|
||||
to,
|
||||
cfg,
|
||||
accountId,
|
||||
mode: "explicit",
|
||||
});
|
||||
if (!resolved.ok) {
|
||||
respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, String(resolved.error)));
|
||||
if (!resolvedTarget.ok) {
|
||||
respond(false, undefined, resolvedTarget.error);
|
||||
return;
|
||||
}
|
||||
const normalized = outbound.pollMaxOptions
|
||||
@@ -445,7 +501,7 @@ export const sendHandlers: GatewayRequestHandlers = {
|
||||
: normalizePollInput(poll);
|
||||
const result = await outbound.sendPoll({
|
||||
cfg,
|
||||
to: resolved.to,
|
||||
to: resolvedTarget.to,
|
||||
poll: normalized,
|
||||
accountId,
|
||||
threadId,
|
||||
@@ -453,34 +509,18 @@ export const sendHandlers: GatewayRequestHandlers = {
|
||||
isAnonymous: request.isAnonymous,
|
||||
gatewayClientScopes: client?.connect?.scopes ?? [],
|
||||
});
|
||||
const payload: Record<string, unknown> = {
|
||||
runId: idem,
|
||||
messageId: result.messageId,
|
||||
channel,
|
||||
};
|
||||
if (result.toJid) {
|
||||
payload.toJid = result.toJid;
|
||||
}
|
||||
if (result.channelId) {
|
||||
payload.channelId = result.channelId;
|
||||
}
|
||||
if (result.conversationId) {
|
||||
payload.conversationId = result.conversationId;
|
||||
}
|
||||
if (result.pollId) {
|
||||
payload.pollId = result.pollId;
|
||||
}
|
||||
context.dedupe.set(`poll:${idem}`, {
|
||||
ts: Date.now(),
|
||||
ok: true,
|
||||
const payload = buildGatewayDeliveryPayload({ runId: idem, channel, result });
|
||||
cacheGatewayDedupeSuccess({
|
||||
context,
|
||||
dedupeKey: `poll:${idem}`,
|
||||
payload,
|
||||
});
|
||||
respond(true, payload, undefined, { channel });
|
||||
} catch (err) {
|
||||
const error = errorShape(ErrorCodes.UNAVAILABLE, String(err));
|
||||
context.dedupe.set(`poll:${idem}`, {
|
||||
ts: Date.now(),
|
||||
ok: false,
|
||||
cacheGatewayDedupeFailure({
|
||||
context,
|
||||
dedupeKey: `poll:${idem}`,
|
||||
error,
|
||||
});
|
||||
respond(false, undefined, error, {
|
||||
|
||||
@@ -5,7 +5,11 @@ import type {
|
||||
OpenClawConfig,
|
||||
} from "../config/config.js";
|
||||
import { replaceConfigFile } from "../config/config.js";
|
||||
import { hasConfiguredSecretInput } from "../config/types.secrets.js";
|
||||
import {
|
||||
hasConfiguredGatewayAuthSecretInput,
|
||||
resolveGatewayPasswordSecretRefValue,
|
||||
resolveGatewayTokenSecretRefValue,
|
||||
} from "./auth-config-utils.js";
|
||||
import { assertExplicitGatewayAuthModeWhenBothConfigured } from "./auth-mode-policy.js";
|
||||
import { resolveGatewayAuth, type ResolvedGatewayAuth } from "./auth.js";
|
||||
import {
|
||||
@@ -13,7 +17,6 @@ import {
|
||||
hasGatewayTokenEnvCandidate,
|
||||
readGatewayTokenEnv,
|
||||
} from "./credentials.js";
|
||||
import { resolveRequiredConfiguredSecretRefInputString } from "./resolve-configured-secret-input-string.js";
|
||||
|
||||
export function mergeGatewayAuthConfig(
|
||||
base?: GatewayAuthConfig,
|
||||
@@ -111,7 +114,7 @@ function hasGatewayTokenCandidate(params: {
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return hasConfiguredSecretInput(params.cfg.gateway?.auth?.token, params.cfg.secrets?.defaults);
|
||||
return hasConfiguredGatewayAuthSecretInput(params.cfg, "gateway.auth.token");
|
||||
}
|
||||
|
||||
function hasGatewayTokenOverrideCandidate(params: { authOverride?: GatewayAuthConfig }): boolean {
|
||||
@@ -133,88 +136,6 @@ function hasGatewayPasswordOverrideCandidate(params: {
|
||||
);
|
||||
}
|
||||
|
||||
function shouldResolveGatewayTokenSecretRef(params: {
|
||||
cfg: OpenClawConfig;
|
||||
env: NodeJS.ProcessEnv;
|
||||
authOverride?: GatewayAuthConfig;
|
||||
}): boolean {
|
||||
if (hasGatewayTokenOverrideCandidate({ authOverride: params.authOverride })) {
|
||||
return false;
|
||||
}
|
||||
if (hasGatewayTokenEnvCandidate(params.env)) {
|
||||
return false;
|
||||
}
|
||||
const explicitMode = params.authOverride?.mode ?? params.cfg.gateway?.auth?.mode;
|
||||
if (explicitMode === "token") {
|
||||
return true;
|
||||
}
|
||||
if (explicitMode === "password" || explicitMode === "none" || explicitMode === "trusted-proxy") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hasGatewayPasswordOverrideCandidate(params)) {
|
||||
return false;
|
||||
}
|
||||
return !hasConfiguredSecretInput(
|
||||
params.cfg.gateway?.auth?.password,
|
||||
params.cfg.secrets?.defaults,
|
||||
);
|
||||
}
|
||||
|
||||
async function resolveGatewayTokenSecretRef(
|
||||
cfg: OpenClawConfig,
|
||||
env: NodeJS.ProcessEnv,
|
||||
authOverride?: GatewayAuthConfig,
|
||||
): Promise<string | undefined> {
|
||||
if (!shouldResolveGatewayTokenSecretRef({ cfg, env, authOverride })) {
|
||||
return undefined;
|
||||
}
|
||||
return await resolveRequiredConfiguredSecretRefInputString({
|
||||
config: cfg,
|
||||
env,
|
||||
value: cfg.gateway?.auth?.token,
|
||||
path: "gateway.auth.token",
|
||||
});
|
||||
}
|
||||
|
||||
function shouldResolveGatewayPasswordSecretRef(params: {
|
||||
cfg: OpenClawConfig;
|
||||
env: NodeJS.ProcessEnv;
|
||||
authOverride?: GatewayAuthConfig;
|
||||
}): boolean {
|
||||
if (hasGatewayPasswordOverrideCandidate(params)) {
|
||||
return false;
|
||||
}
|
||||
const explicitMode = params.authOverride?.mode ?? params.cfg.gateway?.auth?.mode;
|
||||
if (explicitMode === "password") {
|
||||
return true;
|
||||
}
|
||||
if (explicitMode === "token" || explicitMode === "none" || explicitMode === "trusted-proxy") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hasGatewayTokenCandidate(params)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async function resolveGatewayPasswordSecretRef(
|
||||
cfg: OpenClawConfig,
|
||||
env: NodeJS.ProcessEnv,
|
||||
authOverride?: GatewayAuthConfig,
|
||||
): Promise<string | undefined> {
|
||||
if (!shouldResolveGatewayPasswordSecretRef({ cfg, env, authOverride })) {
|
||||
return undefined;
|
||||
}
|
||||
return await resolveRequiredConfiguredSecretRefInputString({
|
||||
config: cfg,
|
||||
env,
|
||||
value: cfg.gateway?.auth?.password,
|
||||
path: "gateway.auth.password",
|
||||
});
|
||||
}
|
||||
|
||||
export async function ensureGatewayStartupAuth(params: {
|
||||
cfg: OpenClawConfig;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
@@ -231,9 +152,33 @@ export async function ensureGatewayStartupAuth(params: {
|
||||
assertExplicitGatewayAuthModeWhenBothConfigured(params.cfg);
|
||||
const env = params.env ?? process.env;
|
||||
const persistRequested = params.persist === true;
|
||||
const explicitMode = params.authOverride?.mode ?? params.cfg.gateway?.auth?.mode;
|
||||
const [resolvedTokenRefValue, resolvedPasswordRefValue] = await Promise.all([
|
||||
resolveGatewayTokenSecretRef(params.cfg, env, params.authOverride),
|
||||
resolveGatewayPasswordSecretRef(params.cfg, env, params.authOverride),
|
||||
resolveGatewayTokenSecretRefValue({
|
||||
cfg: params.cfg,
|
||||
env,
|
||||
mode: explicitMode,
|
||||
hasTokenCandidate:
|
||||
hasGatewayTokenOverrideCandidate({ authOverride: params.authOverride }) ||
|
||||
hasGatewayTokenEnvCandidate(env),
|
||||
hasPasswordCandidate:
|
||||
hasGatewayPasswordOverrideCandidate({ env, authOverride: params.authOverride }) ||
|
||||
hasConfiguredGatewayAuthSecretInput(params.cfg, "gateway.auth.password"),
|
||||
}),
|
||||
resolveGatewayPasswordSecretRefValue({
|
||||
cfg: params.cfg,
|
||||
env,
|
||||
mode: explicitMode,
|
||||
hasPasswordCandidate: hasGatewayPasswordOverrideCandidate({
|
||||
env,
|
||||
authOverride: params.authOverride,
|
||||
}),
|
||||
hasTokenCandidate: hasGatewayTokenCandidate({
|
||||
cfg: params.cfg,
|
||||
env,
|
||||
authOverride: params.authOverride,
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
const authOverride: GatewayAuthConfig | undefined =
|
||||
params.authOverride || resolvedTokenRefValue || resolvedPasswordRefValue
|
||||
|
||||
@@ -104,6 +104,15 @@ type ApprovalStrategy<
|
||||
) => ReplyPayload;
|
||||
};
|
||||
|
||||
type ApprovalRouteRequestFields = {
|
||||
agentId?: string | null;
|
||||
sessionKey?: string | null;
|
||||
turnSourceChannel?: string | null;
|
||||
turnSourceTo?: string | null;
|
||||
turnSourceAccountId?: string | null;
|
||||
turnSourceThreadId?: string | number | null;
|
||||
};
|
||||
|
||||
export type ExecApprovalForwarder = {
|
||||
handleRequested: (request: ExecApprovalRequest) => Promise<boolean>;
|
||||
handleResolved: (resolved: ExecApprovalResolved) => Promise<void>;
|
||||
@@ -278,6 +287,22 @@ function normalizeTurnSourceChannel(value?: string | null): DeliverableMessageCh
|
||||
return normalized && isDeliverableMessageChannel(normalized) ? normalized : undefined;
|
||||
}
|
||||
|
||||
function extractApprovalRouteRequest(
|
||||
request: ApprovalRouteRequestFields | null | undefined,
|
||||
): ApprovalRouteRequest | null {
|
||||
if (!request) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
agentId: request.agentId ?? null,
|
||||
sessionKey: request.sessionKey ?? null,
|
||||
turnSourceChannel: request.turnSourceChannel ?? null,
|
||||
turnSourceTo: request.turnSourceTo ?? null,
|
||||
turnSourceAccountId: request.turnSourceAccountId ?? null,
|
||||
turnSourceThreadId: request.turnSourceThreadId ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
function defaultResolveSessionTarget(params: {
|
||||
cfg: OpenClawConfig;
|
||||
request: ExecApprovalRequest;
|
||||
@@ -341,33 +366,42 @@ async function deliverToTargets(params: {
|
||||
await Promise.allSettled(deliveries);
|
||||
}
|
||||
|
||||
function buildApprovalRenderPayload<TParams>(params: {
|
||||
target: ForwardTarget;
|
||||
renderParams: TParams;
|
||||
resolveRenderer: (
|
||||
adapter: ReturnType<typeof resolveChannelApprovalAdapter> | undefined,
|
||||
) => ((params: TParams) => ReplyPayload | null) | undefined;
|
||||
buildFallback: () => ReplyPayload;
|
||||
}): ReplyPayload {
|
||||
const channel = normalizeMessageChannel(params.target.channel) ?? params.target.channel;
|
||||
const adapterPayload = channel
|
||||
? params.resolveRenderer(resolveChannelApprovalAdapter(getChannelPlugin(channel)))?.(
|
||||
params.renderParams,
|
||||
)
|
||||
: null;
|
||||
return adapterPayload ?? params.buildFallback();
|
||||
}
|
||||
|
||||
function buildExecPendingPayload(params: {
|
||||
cfg: OpenClawConfig;
|
||||
request: ExecApprovalRequest;
|
||||
target: ForwardTarget;
|
||||
nowMs: number;
|
||||
}): ReplyPayload {
|
||||
const channel = normalizeMessageChannel(params.target.channel) ?? params.target.channel;
|
||||
const pluginPayload = channel
|
||||
? resolveChannelApprovalAdapter(getChannelPlugin(channel))?.render?.exec?.buildPendingPayload?.(
|
||||
{
|
||||
cfg: params.cfg,
|
||||
request: params.request,
|
||||
target: params.target,
|
||||
nowMs: params.nowMs,
|
||||
},
|
||||
)
|
||||
: null;
|
||||
if (pluginPayload) {
|
||||
return pluginPayload;
|
||||
}
|
||||
return buildApprovalPendingReplyPayload({
|
||||
approvalId: params.request.id,
|
||||
approvalSlug: params.request.id.slice(0, 8),
|
||||
text: buildRequestMessage(params.request, params.nowMs),
|
||||
agentId: params.request.request.agentId ?? null,
|
||||
allowedDecisions: resolveExecApprovalRequestAllowedDecisions(params.request.request),
|
||||
sessionKey: params.request.request.sessionKey ?? null,
|
||||
return buildApprovalRenderPayload({
|
||||
target: params.target,
|
||||
renderParams: params,
|
||||
resolveRenderer: (adapter) => adapter?.render?.exec?.buildPendingPayload,
|
||||
buildFallback: () =>
|
||||
buildApprovalPendingReplyPayload({
|
||||
approvalId: params.request.id,
|
||||
approvalSlug: params.request.id.slice(0, 8),
|
||||
text: buildRequestMessage(params.request, params.nowMs),
|
||||
agentId: params.request.request.agentId ?? null,
|
||||
allowedDecisions: resolveExecApprovalRequestAllowedDecisions(params.request.request),
|
||||
sessionKey: params.request.request.sessionKey ?? null,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -376,23 +410,16 @@ function buildExecResolvedPayload(params: {
|
||||
resolved: ExecApprovalResolved;
|
||||
target: ForwardTarget;
|
||||
}): ReplyPayload {
|
||||
const channel = normalizeMessageChannel(params.target.channel) ?? params.target.channel;
|
||||
const pluginPayload = channel
|
||||
? resolveChannelApprovalAdapter(
|
||||
getChannelPlugin(channel),
|
||||
)?.render?.exec?.buildResolvedPayload?.({
|
||||
cfg: params.cfg,
|
||||
resolved: params.resolved,
|
||||
target: params.target,
|
||||
})
|
||||
: null;
|
||||
if (pluginPayload) {
|
||||
return pluginPayload;
|
||||
}
|
||||
return buildApprovalResolvedReplyPayload({
|
||||
approvalId: params.resolved.id,
|
||||
approvalSlug: params.resolved.id.slice(0, 8),
|
||||
text: buildResolvedMessage(params.resolved),
|
||||
return buildApprovalRenderPayload({
|
||||
target: params.target,
|
||||
renderParams: params,
|
||||
resolveRenderer: (adapter) => adapter?.render?.exec?.buildResolvedPayload,
|
||||
buildFallback: () =>
|
||||
buildApprovalResolvedReplyPayload({
|
||||
approvalId: params.resolved.id,
|
||||
approvalSlug: params.resolved.id.slice(0, 8),
|
||||
text: buildResolvedMessage(params.resolved),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -402,24 +429,16 @@ function buildPluginPendingPayload(params: {
|
||||
target: ForwardTarget;
|
||||
nowMs: number;
|
||||
}): ReplyPayload {
|
||||
const channel = normalizeMessageChannel(params.target.channel) ?? params.target.channel;
|
||||
const adapterPayload = channel
|
||||
? resolveChannelApprovalAdapter(
|
||||
getChannelPlugin(channel),
|
||||
)?.render?.plugin?.buildPendingPayload?.({
|
||||
cfg: params.cfg,
|
||||
return buildApprovalRenderPayload({
|
||||
target: params.target,
|
||||
renderParams: params,
|
||||
resolveRenderer: (adapter) => adapter?.render?.plugin?.buildPendingPayload,
|
||||
buildFallback: () =>
|
||||
buildPluginApprovalPendingReplyPayload({
|
||||
request: params.request,
|
||||
target: params.target,
|
||||
nowMs: params.nowMs,
|
||||
})
|
||||
: null;
|
||||
if (adapterPayload) {
|
||||
return adapterPayload;
|
||||
}
|
||||
return buildPluginApprovalPendingReplyPayload({
|
||||
request: params.request,
|
||||
nowMs: params.nowMs,
|
||||
text: buildPluginApprovalRequestMessage(params.request, params.nowMs),
|
||||
text: buildPluginApprovalRequestMessage(params.request, params.nowMs),
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -428,21 +447,14 @@ function buildPluginResolvedPayload(params: {
|
||||
resolved: PluginApprovalResolved;
|
||||
target: ForwardTarget;
|
||||
}): ReplyPayload {
|
||||
const channel = normalizeMessageChannel(params.target.channel) ?? params.target.channel;
|
||||
const adapterPayload = channel
|
||||
? resolveChannelApprovalAdapter(
|
||||
getChannelPlugin(channel),
|
||||
)?.render?.plugin?.buildResolvedPayload?.({
|
||||
cfg: params.cfg,
|
||||
return buildApprovalRenderPayload({
|
||||
target: params.target,
|
||||
renderParams: params,
|
||||
resolveRenderer: (adapter) => adapter?.render?.plugin?.buildResolvedPayload,
|
||||
buildFallback: () =>
|
||||
buildPluginApprovalResolvedReplyPayload({
|
||||
resolved: params.resolved,
|
||||
target: params.target,
|
||||
})
|
||||
: null;
|
||||
if (adapterPayload) {
|
||||
return adapterPayload;
|
||||
}
|
||||
return buildPluginApprovalResolvedReplyPayload({
|
||||
resolved: params.resolved,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -659,31 +671,37 @@ function createApprovalHandlers<
|
||||
return { handleRequested, handleResolved, stop };
|
||||
}
|
||||
|
||||
const execApprovalStrategy: ApprovalStrategy<ExecApprovalRequest, ExecApprovalResolved> = {
|
||||
function createApprovalStrategy<
|
||||
TRequest extends { id: string; request: ApprovalRouteRequestFields; expiresAtMs: number },
|
||||
TResolved extends { id: string; request?: ApprovalRouteRequestFields | null },
|
||||
>(params: {
|
||||
kind: ApprovalKind;
|
||||
config: (cfg: OpenClawConfig) => ExecApprovalForwardingConfig | undefined;
|
||||
buildExpiredText: (request: TRequest) => string;
|
||||
buildPendingPayload: (
|
||||
params: ApprovalPendingRenderContext<TRequest, ApprovalRouteRequest>,
|
||||
) => ReplyPayload;
|
||||
buildResolvedPayload: (
|
||||
params: ApprovalResolvedRenderContext<TResolved, ApprovalRouteRequest>,
|
||||
) => ReplyPayload;
|
||||
}): ApprovalStrategy<TRequest, TResolved> {
|
||||
return {
|
||||
kind: params.kind,
|
||||
config: params.config,
|
||||
getRequestId: (request) => request.id,
|
||||
getResolvedId: (resolved) => resolved.id,
|
||||
getExpiresAtMs: (request) => request.expiresAtMs,
|
||||
getRouteRequestFromRequest: (request) => extractApprovalRouteRequest(request.request) ?? {},
|
||||
getRouteRequestFromResolved: (resolved) => extractApprovalRouteRequest(resolved.request),
|
||||
buildExpiredText: params.buildExpiredText,
|
||||
buildPendingPayload: params.buildPendingPayload,
|
||||
buildResolvedPayload: params.buildResolvedPayload,
|
||||
};
|
||||
}
|
||||
|
||||
const execApprovalStrategy = createApprovalStrategy<ExecApprovalRequest, ExecApprovalResolved>({
|
||||
kind: "exec",
|
||||
config: (cfg) => cfg.approvals?.exec,
|
||||
getRequestId: (request) => request.id,
|
||||
getResolvedId: (resolved) => resolved.id,
|
||||
getExpiresAtMs: (request) => request.expiresAtMs,
|
||||
getRouteRequestFromRequest: (request) => ({
|
||||
agentId: request.request.agentId ?? null,
|
||||
sessionKey: request.request.sessionKey ?? null,
|
||||
turnSourceChannel: request.request.turnSourceChannel ?? null,
|
||||
turnSourceTo: request.request.turnSourceTo ?? null,
|
||||
turnSourceAccountId: request.request.turnSourceAccountId ?? null,
|
||||
turnSourceThreadId: request.request.turnSourceThreadId ?? null,
|
||||
}),
|
||||
getRouteRequestFromResolved: (resolved) =>
|
||||
resolved.request
|
||||
? {
|
||||
agentId: resolved.request.agentId ?? null,
|
||||
sessionKey: resolved.request.sessionKey ?? null,
|
||||
turnSourceChannel: resolved.request.turnSourceChannel ?? null,
|
||||
turnSourceTo: resolved.request.turnSourceTo ?? null,
|
||||
turnSourceAccountId: resolved.request.turnSourceAccountId ?? null,
|
||||
turnSourceThreadId: resolved.request.turnSourceThreadId ?? null,
|
||||
}
|
||||
: null,
|
||||
buildExpiredText: buildExpiredMessage,
|
||||
buildPendingPayload: ({ cfg, request, target, nowMs }) =>
|
||||
buildExecPendingPayload({
|
||||
@@ -698,33 +716,14 @@ const execApprovalStrategy: ApprovalStrategy<ExecApprovalRequest, ExecApprovalRe
|
||||
resolved,
|
||||
target,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const pluginApprovalStrategy: ApprovalStrategy<PluginApprovalRequest, PluginApprovalResolved> = {
|
||||
const pluginApprovalStrategy = createApprovalStrategy<
|
||||
PluginApprovalRequest,
|
||||
PluginApprovalResolved
|
||||
>({
|
||||
kind: "plugin",
|
||||
config: (cfg) => cfg.approvals?.plugin,
|
||||
getRequestId: (request) => request.id,
|
||||
getResolvedId: (resolved) => resolved.id,
|
||||
getExpiresAtMs: (request) => request.expiresAtMs,
|
||||
getRouteRequestFromRequest: (request) => ({
|
||||
agentId: request.request.agentId ?? null,
|
||||
sessionKey: request.request.sessionKey ?? null,
|
||||
turnSourceChannel: request.request.turnSourceChannel ?? null,
|
||||
turnSourceTo: request.request.turnSourceTo ?? null,
|
||||
turnSourceAccountId: request.request.turnSourceAccountId ?? null,
|
||||
turnSourceThreadId: request.request.turnSourceThreadId ?? null,
|
||||
}),
|
||||
getRouteRequestFromResolved: (resolved) =>
|
||||
resolved.request
|
||||
? {
|
||||
agentId: resolved.request.agentId ?? null,
|
||||
sessionKey: resolved.request.sessionKey ?? null,
|
||||
turnSourceChannel: resolved.request.turnSourceChannel ?? null,
|
||||
turnSourceTo: resolved.request.turnSourceTo ?? null,
|
||||
turnSourceAccountId: resolved.request.turnSourceAccountId ?? null,
|
||||
turnSourceThreadId: resolved.request.turnSourceThreadId ?? null,
|
||||
}
|
||||
: null,
|
||||
buildExpiredText: buildPluginApprovalExpiredMessage,
|
||||
buildPendingPayload: ({ cfg, request, target, nowMs }) =>
|
||||
buildPluginPendingPayload({
|
||||
@@ -739,7 +738,7 @@ const pluginApprovalStrategy: ApprovalStrategy<PluginApprovalRequest, PluginAppr
|
||||
resolved,
|
||||
target,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
export function createExecApprovalForwarder(
|
||||
deps: ExecApprovalForwarderDeps = {},
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { buildGatewayConnectionDetails } from "../gateway/call.js";
|
||||
import { resolveGatewayClientBootstrap } from "../gateway/client-bootstrap.js";
|
||||
import { GatewayClient } from "../gateway/client.js";
|
||||
import { resolveGatewayConnectionAuth } from "../gateway/connection-auth.js";
|
||||
import { APPROVALS_SCOPE, READ_SCOPE, WRITE_SCOPE } from "../gateway/method-scopes.js";
|
||||
import type { EventFrame } from "../gateway/protocol/index.js";
|
||||
import { extractFirstTextBlock } from "../shared/chat-message-content.js";
|
||||
@@ -84,25 +83,14 @@ export class OpenClawChannelBridge {
|
||||
return;
|
||||
}
|
||||
this.started = true;
|
||||
const connection = buildGatewayConnectionDetails({
|
||||
config: this.cfg,
|
||||
url: this.params.gatewayUrl,
|
||||
});
|
||||
const gatewayUrlOverrideSource =
|
||||
connection.urlSource === "cli --url"
|
||||
? "cli"
|
||||
: connection.urlSource === "env OPENCLAW_GATEWAY_URL"
|
||||
? "env"
|
||||
: undefined;
|
||||
const creds = await resolveGatewayConnectionAuth({
|
||||
const bootstrap = await resolveGatewayClientBootstrap({
|
||||
config: this.cfg,
|
||||
gatewayUrl: this.params.gatewayUrl,
|
||||
explicitAuth: {
|
||||
token: this.params.gatewayToken,
|
||||
password: this.params.gatewayPassword,
|
||||
},
|
||||
env: process.env,
|
||||
urlOverride: gatewayUrlOverrideSource ? connection.url : undefined,
|
||||
urlOverrideSource: gatewayUrlOverrideSource,
|
||||
});
|
||||
if (this.closed) {
|
||||
this.resolveReadyOnce();
|
||||
@@ -110,9 +98,9 @@ export class OpenClawChannelBridge {
|
||||
}
|
||||
|
||||
this.gateway = new GatewayClient({
|
||||
url: connection.url,
|
||||
token: creds.token,
|
||||
password: creds.password,
|
||||
url: bootstrap.url,
|
||||
token: bootstrap.auth.token,
|
||||
password: bootstrap.auth.password,
|
||||
clientName: GATEWAY_CLIENT_NAMES.CLI,
|
||||
clientDisplayName: "OpenClaw MCP",
|
||||
clientVersion: VERSION,
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import os from "node:os";
|
||||
import { resolveGatewayPort } from "../config/paths.js";
|
||||
import type { OpenClawConfig } from "../config/types.js";
|
||||
import {
|
||||
hasConfiguredSecretInput,
|
||||
normalizeSecretInputString,
|
||||
resolveSecretInputRef,
|
||||
} from "../config/types.secrets.js";
|
||||
import { normalizeSecretInputString, resolveSecretInputRef } from "../config/types.secrets.js";
|
||||
import { materializeGatewayAuthSecretRefs } from "../gateway/auth-config-utils.js";
|
||||
import { assertExplicitGatewayAuthModeWhenBothConfigured } from "../gateway/auth-mode-policy.js";
|
||||
import { isLoopbackHost, isSecureWebSocketUrl } from "../gateway/net.js";
|
||||
import { resolveRequiredConfiguredSecretRefInputString } from "../gateway/resolve-configured-secret-input-string.js";
|
||||
import { issueDeviceBootstrapToken } from "../infra/device-bootstrap.js";
|
||||
import {
|
||||
pickMatchingExternalInterfaceAddress,
|
||||
@@ -268,94 +264,6 @@ function resolvePairingSetupAuthLabel(
|
||||
return { error: "Gateway auth is not configured (no token or password)." };
|
||||
}
|
||||
|
||||
async function resolveGatewayTokenSecretRef(
|
||||
cfg: OpenClawConfig,
|
||||
env: NodeJS.ProcessEnv,
|
||||
): Promise<OpenClawConfig> {
|
||||
const hasTokenEnvCandidate = Boolean(resolveGatewayTokenFromEnv(env));
|
||||
if (hasTokenEnvCandidate) {
|
||||
return cfg;
|
||||
}
|
||||
const mode = cfg.gateway?.auth?.mode;
|
||||
if (mode === "password" || mode === "none" || mode === "trusted-proxy") {
|
||||
return cfg;
|
||||
}
|
||||
if (mode !== "token") {
|
||||
const hasPasswordEnvCandidate = Boolean(env.OPENCLAW_GATEWAY_PASSWORD?.trim());
|
||||
if (hasPasswordEnvCandidate) {
|
||||
return cfg;
|
||||
}
|
||||
}
|
||||
const token = await resolveRequiredConfiguredSecretRefInputString({
|
||||
config: cfg,
|
||||
env,
|
||||
value: cfg.gateway?.auth?.token,
|
||||
path: "gateway.auth.token",
|
||||
});
|
||||
if (!token) {
|
||||
return cfg;
|
||||
}
|
||||
return {
|
||||
...cfg,
|
||||
gateway: {
|
||||
...cfg.gateway,
|
||||
auth: {
|
||||
...cfg.gateway?.auth,
|
||||
token,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function resolveGatewayPasswordSecretRef(
|
||||
cfg: OpenClawConfig,
|
||||
env: NodeJS.ProcessEnv,
|
||||
): Promise<OpenClawConfig> {
|
||||
const hasPasswordEnvCandidate = Boolean(resolveGatewayPasswordFromEnv(env));
|
||||
if (hasPasswordEnvCandidate) {
|
||||
return cfg;
|
||||
}
|
||||
const mode = cfg.gateway?.auth?.mode;
|
||||
if (mode === "token" || mode === "none" || mode === "trusted-proxy") {
|
||||
return cfg;
|
||||
}
|
||||
if (mode !== "password") {
|
||||
const hasTokenCandidate =
|
||||
Boolean(resolveGatewayTokenFromEnv(env)) ||
|
||||
hasConfiguredSecretInput(cfg.gateway?.auth?.token, cfg.secrets?.defaults);
|
||||
if (hasTokenCandidate) {
|
||||
return cfg;
|
||||
}
|
||||
}
|
||||
const password = await resolveRequiredConfiguredSecretRefInputString({
|
||||
config: cfg,
|
||||
env,
|
||||
value: cfg.gateway?.auth?.password,
|
||||
path: "gateway.auth.password",
|
||||
});
|
||||
if (!password) {
|
||||
return cfg;
|
||||
}
|
||||
return {
|
||||
...cfg,
|
||||
gateway: {
|
||||
...cfg.gateway,
|
||||
auth: {
|
||||
...cfg.gateway?.auth,
|
||||
password,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function materializePairingSetupAuthConfig(
|
||||
cfg: OpenClawConfig,
|
||||
env: NodeJS.ProcessEnv,
|
||||
): Promise<OpenClawConfig> {
|
||||
const cfgWithToken = await resolveGatewayTokenSecretRef(cfg, env);
|
||||
return await resolveGatewayPasswordSecretRef(cfgWithToken, env);
|
||||
}
|
||||
|
||||
async function resolveGatewayUrl(
|
||||
cfg: OpenClawConfig,
|
||||
opts: {
|
||||
@@ -430,7 +338,13 @@ export async function resolvePairingSetupFromConfig(
|
||||
): Promise<PairingSetupResolution> {
|
||||
assertExplicitGatewayAuthModeWhenBothConfigured(cfg);
|
||||
const env = options.env ?? process.env;
|
||||
const cfgForAuth = await materializePairingSetupAuthConfig(cfg, env);
|
||||
const cfgForAuth = await materializeGatewayAuthSecretRefs({
|
||||
cfg,
|
||||
env,
|
||||
mode: cfg.gateway?.auth?.mode,
|
||||
hasTokenCandidate: Boolean(resolveGatewayTokenFromEnv(env)),
|
||||
hasPasswordCandidate: Boolean(resolveGatewayPasswordFromEnv(env)),
|
||||
});
|
||||
const authLabel = resolvePairingSetupAuthLabel(cfgForAuth, env);
|
||||
if (authLabel.error) {
|
||||
return { ok: false, error: authLabel.error };
|
||||
|
||||
@@ -128,6 +128,7 @@ let gatewayProbeDepsPromise:
|
||||
| Promise<{
|
||||
buildGatewayConnectionDetails: typeof import("../gateway/call.js").buildGatewayConnectionDetails;
|
||||
resolveGatewayProbeAuthSafe: typeof import("../gateway/probe-auth.js").resolveGatewayProbeAuthSafe;
|
||||
resolveGatewayProbeTarget: typeof import("../gateway/probe-auth.js").resolveGatewayProbeTarget;
|
||||
probeGateway: typeof import("../gateway/probe.js").probeGateway;
|
||||
}>
|
||||
| undefined;
|
||||
@@ -171,6 +172,7 @@ async function loadGatewayProbeDeps() {
|
||||
]).then(([callModule, probeAuthModule, probeModule]) => ({
|
||||
buildGatewayConnectionDetails: callModule.buildGatewayConnectionDetails,
|
||||
resolveGatewayProbeAuthSafe: probeAuthModule.resolveGatewayProbeAuthSafe,
|
||||
resolveGatewayProbeTarget: probeAuthModule.resolveGatewayProbeTarget,
|
||||
probeGateway: probeModule.probeGateway,
|
||||
}));
|
||||
return await gatewayProbeDepsPromise;
|
||||
@@ -1214,29 +1216,18 @@ async function maybeProbeGateway(params: {
|
||||
deep: SecurityAuditReport["deep"];
|
||||
authWarning?: string;
|
||||
}> {
|
||||
const { buildGatewayConnectionDetails, resolveGatewayProbeAuthSafe } =
|
||||
const { buildGatewayConnectionDetails, resolveGatewayProbeAuthSafe, resolveGatewayProbeTarget } =
|
||||
await loadGatewayProbeDeps();
|
||||
const connection = buildGatewayConnectionDetails({ config: params.cfg });
|
||||
const url = connection.url;
|
||||
const isRemoteMode = params.cfg.gateway?.mode === "remote";
|
||||
const remoteUrlRaw =
|
||||
typeof params.cfg.gateway?.remote?.url === "string" ? params.cfg.gateway.remote.url.trim() : "";
|
||||
const remoteUrlMissing = isRemoteMode && !remoteUrlRaw;
|
||||
const probeTarget = resolveGatewayProbeTarget(params.cfg);
|
||||
|
||||
const authResolution =
|
||||
!isRemoteMode || remoteUrlMissing
|
||||
? resolveGatewayProbeAuthSafe({
|
||||
cfg: params.cfg,
|
||||
env: params.env,
|
||||
mode: "local",
|
||||
explicitAuth: params.explicitAuth,
|
||||
})
|
||||
: resolveGatewayProbeAuthSafe({
|
||||
cfg: params.cfg,
|
||||
env: params.env,
|
||||
mode: "remote",
|
||||
explicitAuth: params.explicitAuth,
|
||||
});
|
||||
const authResolution = resolveGatewayProbeAuthSafe({
|
||||
cfg: params.cfg,
|
||||
env: params.env,
|
||||
mode: probeTarget.mode,
|
||||
explicitAuth: params.explicitAuth,
|
||||
});
|
||||
const res = await params
|
||||
.probe({ url, auth: authResolution.auth, timeoutMs: params.timeoutMs })
|
||||
.catch((err) => ({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { loadConfig } from "../config/config.js";
|
||||
import { hasConfiguredSecretInput } from "../config/types.secrets.js";
|
||||
import { assertExplicitGatewayAuthModeWhenBothConfigured } from "../gateway/auth-mode-policy.js";
|
||||
import { resolveGatewayInteractiveSurfaceAuth } from "../gateway/auth-surface-resolution.js";
|
||||
import {
|
||||
buildGatewayConnectionDetails,
|
||||
ensureExplicitGatewayAuth,
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
type SessionsPatchResult,
|
||||
type SessionsPatchParams,
|
||||
} from "../gateway/protocol/index.js";
|
||||
import { resolveConfiguredSecretInputString } from "../gateway/resolve-configured-secret-input-string.js";
|
||||
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js";
|
||||
import { VERSION } from "../version.js";
|
||||
import type { ResponseUsageMode, SessionInfo, SessionScope } from "./tui-types.js";
|
||||
@@ -50,14 +49,6 @@ type ResolvedGatewayConnection = {
|
||||
allowInsecureLocalOperatorUi?: boolean;
|
||||
};
|
||||
|
||||
function trimToUndefined(value: unknown): string | undefined {
|
||||
if (typeof value !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
return trimmed.length > 0 ? trimmed : undefined;
|
||||
}
|
||||
|
||||
function throwGatewayAuthResolutionError(reason: string): never {
|
||||
throw new Error(
|
||||
[
|
||||
@@ -277,9 +268,6 @@ export async function resolveGatewayConnection(
|
||||
const env = process.env;
|
||||
const gatewayAuthMode = config.gateway?.auth?.mode;
|
||||
const isRemoteMode = config.gateway?.mode === "remote";
|
||||
const remote = config.gateway?.remote;
|
||||
const envToken = trimToUndefined(env.OPENCLAW_GATEWAY_TOKEN);
|
||||
const envPassword = trimToUndefined(env.OPENCLAW_GATEWAY_PASSWORD);
|
||||
|
||||
const urlOverride =
|
||||
typeof opts.url === "string" && opts.url.trim().length > 0 ? opts.url.trim() : undefined;
|
||||
@@ -315,41 +303,34 @@ export async function resolveGatewayConnection(
|
||||
}
|
||||
|
||||
if (isRemoteMode) {
|
||||
const remoteToken = explicitAuth.token
|
||||
? { value: explicitAuth.token }
|
||||
: await resolveConfiguredSecretInputString({
|
||||
value: remote?.token,
|
||||
path: "gateway.remote.token",
|
||||
env,
|
||||
config,
|
||||
});
|
||||
const remotePassword =
|
||||
explicitAuth.password || envPassword
|
||||
? { value: explicitAuth.password ?? envPassword }
|
||||
: await resolveConfiguredSecretInputString({
|
||||
value: remote?.password,
|
||||
path: "gateway.remote.password",
|
||||
env,
|
||||
config,
|
||||
});
|
||||
|
||||
const token = explicitAuth.token ?? remoteToken.value;
|
||||
const password = explicitAuth.password ?? envPassword ?? remotePassword.value;
|
||||
if (!token && !password) {
|
||||
throwGatewayAuthResolutionError(
|
||||
remoteToken.unresolvedRefReason ??
|
||||
remotePassword.unresolvedRefReason ??
|
||||
"Missing gateway auth credentials.",
|
||||
);
|
||||
const resolved = await resolveGatewayInteractiveSurfaceAuth({
|
||||
config,
|
||||
env,
|
||||
explicitAuth,
|
||||
surface: "remote",
|
||||
});
|
||||
if (resolved.failureReason) {
|
||||
throwGatewayAuthResolutionError(resolved.failureReason);
|
||||
}
|
||||
return { url, token, password, allowInsecureLocalOperatorUi: false };
|
||||
return {
|
||||
url,
|
||||
token: resolved.token,
|
||||
password: resolved.password,
|
||||
allowInsecureLocalOperatorUi: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (gatewayAuthMode === "none" || gatewayAuthMode === "trusted-proxy") {
|
||||
const resolved = await resolveGatewayInteractiveSurfaceAuth({
|
||||
config,
|
||||
env,
|
||||
explicitAuth,
|
||||
surface: "local",
|
||||
});
|
||||
return {
|
||||
url,
|
||||
token: explicitAuth.token ?? envToken,
|
||||
password: explicitAuth.password ?? envPassword,
|
||||
token: resolved.token,
|
||||
password: resolved.password,
|
||||
allowInsecureLocalOperatorUi,
|
||||
};
|
||||
}
|
||||
@@ -360,93 +341,19 @@ export async function resolveGatewayConnection(
|
||||
throwGatewayAuthResolutionError(err instanceof Error ? err.message : String(err));
|
||||
}
|
||||
|
||||
const defaults = config.secrets?.defaults;
|
||||
const hasConfiguredToken = hasConfiguredSecretInput(config.gateway?.auth?.token, defaults);
|
||||
const hasConfiguredPassword = hasConfiguredSecretInput(config.gateway?.auth?.password, defaults);
|
||||
if (gatewayAuthMode === "password") {
|
||||
const localPassword =
|
||||
explicitAuth.password || envPassword
|
||||
? { value: explicitAuth.password ?? envPassword }
|
||||
: await resolveConfiguredSecretInputString({
|
||||
value: config.gateway?.auth?.password,
|
||||
path: "gateway.auth.password",
|
||||
env,
|
||||
config,
|
||||
});
|
||||
const password = explicitAuth.password ?? envPassword ?? localPassword.value;
|
||||
if (!password) {
|
||||
throwGatewayAuthResolutionError(
|
||||
localPassword.unresolvedRefReason ?? "Missing gateway auth password.",
|
||||
);
|
||||
}
|
||||
return {
|
||||
url,
|
||||
token: explicitAuth.token ?? envToken,
|
||||
password,
|
||||
allowInsecureLocalOperatorUi,
|
||||
};
|
||||
const resolved = await resolveGatewayInteractiveSurfaceAuth({
|
||||
config,
|
||||
env,
|
||||
explicitAuth,
|
||||
surface: "local",
|
||||
});
|
||||
if (resolved.failureReason) {
|
||||
throwGatewayAuthResolutionError(resolved.failureReason);
|
||||
}
|
||||
|
||||
const resolveToken = async () => {
|
||||
const localToken = explicitAuth.token
|
||||
? { value: explicitAuth.token }
|
||||
: await resolveConfiguredSecretInputString({
|
||||
value: config.gateway?.auth?.token,
|
||||
path: "gateway.auth.token",
|
||||
env,
|
||||
config,
|
||||
});
|
||||
const token = explicitAuth.token ?? localToken.value ?? envToken;
|
||||
if (!token) {
|
||||
throwGatewayAuthResolutionError(
|
||||
localToken.unresolvedRefReason ?? "Missing gateway auth token.",
|
||||
);
|
||||
}
|
||||
return token;
|
||||
};
|
||||
|
||||
if (gatewayAuthMode === "token") {
|
||||
const token = await resolveToken();
|
||||
return {
|
||||
url,
|
||||
token,
|
||||
password: explicitAuth.password ?? envPassword,
|
||||
allowInsecureLocalOperatorUi,
|
||||
};
|
||||
}
|
||||
|
||||
const passwordCandidate = explicitAuth.password ?? envPassword;
|
||||
const shouldUsePassword =
|
||||
Boolean(passwordCandidate) || (hasConfiguredPassword && !hasConfiguredToken);
|
||||
|
||||
if (shouldUsePassword) {
|
||||
const localPassword = passwordCandidate
|
||||
? { value: passwordCandidate }
|
||||
: await resolveConfiguredSecretInputString({
|
||||
value: config.gateway?.auth?.password,
|
||||
path: "gateway.auth.password",
|
||||
env,
|
||||
config,
|
||||
});
|
||||
const password = explicitAuth.password ?? localPassword.value ?? envPassword;
|
||||
if (!password) {
|
||||
throwGatewayAuthResolutionError(
|
||||
localPassword.unresolvedRefReason ?? "Missing gateway auth password.",
|
||||
);
|
||||
}
|
||||
return {
|
||||
url,
|
||||
token: explicitAuth.token ?? envToken,
|
||||
password,
|
||||
allowInsecureLocalOperatorUi,
|
||||
};
|
||||
}
|
||||
|
||||
const token = await resolveToken();
|
||||
return {
|
||||
url,
|
||||
token,
|
||||
password: explicitAuth.password ?? envPassword,
|
||||
token: resolved.token,
|
||||
password: resolved.password,
|
||||
allowInsecureLocalOperatorUi,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user