Skip to content

Commit 935bd6d

Browse files
committed
fix(gateway): split credential secret input runtime
1 parent 85fa33d commit 935bd6d

7 files changed

Lines changed: 318 additions & 290 deletions

File tree

src/channels/plugins/setup-wizard-helpers.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { DmPolicy, GroupPolicy } from "../../config/types.js";
1+
import type { DmPolicy, GroupPolicy } from "../../config/types.base.js";
22
import type { OpenClawConfig } from "../../config/types.openclaw.js";
33
import type { SecretInput } from "../../config/types.secrets.js";
44
import { resolveSecretInputModeForEnvSelection } from "../../plugins/provider-auth-mode.js";
@@ -1377,9 +1377,9 @@ export function createNestedChannelParsedAllowFromPrompt(params: {
13771377
getExistingAllowFrom: ({ cfg }: { cfg: OpenClawConfig }) =>
13781378
params.getExistingAllowFrom?.(cfg) ??
13791379
(
1380-
(cfg.channels?.[params.channel] as Record<string, unknown> | undefined)?.[params.section] as
1381-
| { allowFrom?: Array<string | number> }
1382-
| undefined
1380+
(cfg.channels?.[params.channel] as Record<string, unknown> | undefined)?.[
1381+
params.section
1382+
] as { allowFrom?: Array<string | number> } | undefined
13831383
)?.allowFrom ??
13841384
[],
13851385
...(params.mergeEntries ? { mergeEntries: params.mergeEntries } : {}),

src/gateway/auth-config-utils.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import type { GatewayAuthConfig, OpenClawConfig } from "../config/config.js";
1+
import type { GatewayAuthConfig } from "../config/types.gateway.js";
2+
import type { OpenClawConfig } from "../config/types.openclaw.js";
23
import { hasConfiguredSecretInput } from "../config/types.secrets.js";
34
import { resolveRequiredConfiguredSecretRefInputString } from "./resolve-configured-secret-input-string.js";
45
import {

src/gateway/call.ts

Lines changed: 6 additions & 273 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,8 @@ import {
66
resolveStateDir as resolveStateDirFromPaths,
77
} from "../config/paths.js";
88
import type { OpenClawConfig } from "../config/types.openclaw.js";
9-
import { resolveSecretInputRef } from "../config/types.secrets.js";
109
import { loadOrCreateDeviceIdentity } from "../infra/device-identity.js";
1110
import { loadGatewayTlsRuntime } from "../infra/tls/gateway.js";
12-
import { resolveSecretInputString } from "../secrets/resolve-secret-input-string.js";
1311
import { normalizeOptionalString } from "../shared/string-coerce.js";
1412
import {
1513
GATEWAY_CLIENT_MODES,
@@ -23,9 +21,8 @@ import {
2321
buildGatewayConnectionDetailsWithResolvers,
2422
type GatewayConnectionDetails,
2523
} from "./connection-details.js";
24+
import { resolveGatewayCredentialsWithSecretInputs } from "./credentials-secret-inputs.js";
2625
import {
27-
GatewaySecretRefUnavailableError,
28-
resolveGatewayCredentialsFromConfig,
2926
trimToUndefined,
3027
type ExplicitGatewayAuth,
3128
type GatewayCredentialMode,
@@ -40,14 +37,6 @@ import {
4037
type OperatorScope,
4138
} from "./method-scopes.js";
4239
import { PROTOCOL_VERSION } from "./protocol/index.js";
43-
import {
44-
ALL_GATEWAY_SECRET_INPUT_PATHS,
45-
assignResolvedGatewaySecretInput,
46-
isSupportedGatewaySecretInputPath,
47-
isTokenGatewaySecretInputPath,
48-
readGatewaySecretInputValue,
49-
type SupportedGatewaySecretInputPath,
50-
} from "./secret-input-paths.js";
5140
export type { GatewayConnectionDetails };
5241

5342
type CallGatewayBaseOptions = {
@@ -340,27 +329,6 @@ function ensureRemoteModeUrlConfigured(context: ResolvedGatewayCallContext): voi
340329
);
341330
}
342331

343-
async function resolveGatewaySecretInputString(params: {
344-
config: OpenClawConfig;
345-
value: unknown;
346-
path: string;
347-
env: NodeJS.ProcessEnv;
348-
}): Promise<string | undefined> {
349-
const value = await resolveSecretInputString({
350-
config: params.config,
351-
value: params.value,
352-
env: params.env,
353-
normalize: trimToUndefined,
354-
onResolveRefError: () => {
355-
throw new GatewaySecretRefUnavailableError(params.path);
356-
},
357-
});
358-
if (!value) {
359-
throw new Error(`${params.path} resolved to an empty or non-string value.`);
360-
}
361-
return value;
362-
}
363-
364332
async function resolveGatewayCredentials(context: ResolvedGatewayCallContext): Promise<{
365333
token?: string;
366334
password?: string;
@@ -381,258 +349,23 @@ async function resolveGatewayCredentialsWithEnv(
381349
password: context.explicitAuth.password,
382350
};
383351
}
384-
return resolveGatewayCredentialsFromConfigWithSecretInputs({ context, env });
385-
}
386-
387-
function hasConfiguredGatewaySecretRef(
388-
config: OpenClawConfig,
389-
path: SupportedGatewaySecretInputPath,
390-
): boolean {
391-
return Boolean(
392-
resolveSecretInputRef({
393-
value: readGatewaySecretInputValue(config, path),
394-
defaults: config.secrets?.defaults,
395-
}).ref,
396-
);
397-
}
398-
399-
function resolveGatewayCredentialsFromConfigOptions(params: {
400-
context: ResolvedGatewayCallContext;
401-
env: NodeJS.ProcessEnv;
402-
cfg: OpenClawConfig;
403-
}) {
404-
const { context, env, cfg } = params;
405-
return {
406-
cfg,
407-
env,
352+
return resolveGatewayCredentialsWithSecretInputs({
353+
config: context.config,
408354
explicitAuth: context.explicitAuth,
409355
urlOverride: context.urlOverride,
410356
urlOverrideSource: context.urlOverrideSource,
357+
env,
411358
modeOverride: context.modeOverride,
412359
localTokenPrecedence: context.localTokenPrecedence,
413360
localPasswordPrecedence: context.localPasswordPrecedence,
414361
remoteTokenPrecedence: context.remoteTokenPrecedence,
415-
remotePasswordPrecedence: context.remotePasswordPrecedence ?? "env-first", // pragma: allowlist secret
362+
remotePasswordPrecedence: context.remotePasswordPrecedence,
416363
remoteTokenFallback: context.remoteTokenFallback,
417364
remotePasswordFallback: context.remotePasswordFallback,
418-
} as const;
419-
}
420-
421-
function localAuthModeAllowsGatewaySecretInputPath(params: {
422-
authMode: string | undefined;
423-
path: SupportedGatewaySecretInputPath;
424-
}): boolean {
425-
const { authMode, path } = params;
426-
if (authMode === "none" || authMode === "trusted-proxy") {
427-
return false;
428-
}
429-
if (authMode === "token") {
430-
return isTokenGatewaySecretInputPath(path);
431-
}
432-
if (authMode === "password") {
433-
return !isTokenGatewaySecretInputPath(path);
434-
}
435-
return true;
436-
}
437-
438-
function gatewaySecretInputPathCanWin(params: {
439-
context: ResolvedGatewayCallContext;
440-
env: NodeJS.ProcessEnv;
441-
config: OpenClawConfig;
442-
path: SupportedGatewaySecretInputPath;
443-
}): boolean {
444-
if (!hasConfiguredGatewaySecretRef(params.config, params.path)) {
445-
return false;
446-
}
447-
const mode: GatewayCredentialMode =
448-
params.context.modeOverride ?? (params.config.gateway?.mode === "remote" ? "remote" : "local");
449-
if (
450-
mode === "local" &&
451-
!localAuthModeAllowsGatewaySecretInputPath({
452-
authMode: params.config.gateway?.auth?.mode,
453-
path: params.path,
454-
})
455-
) {
456-
return false;
457-
}
458-
const sentinel = `__OPENCLAW_GATEWAY_SECRET_REF_PROBE_${params.path.replaceAll(".", "_")}__`;
459-
const probeConfig = structuredClone(params.config);
460-
for (const candidatePath of ALL_GATEWAY_SECRET_INPUT_PATHS) {
461-
if (!hasConfiguredGatewaySecretRef(probeConfig, candidatePath)) {
462-
continue;
463-
}
464-
assignResolvedGatewaySecretInput({
465-
config: probeConfig,
466-
path: candidatePath,
467-
value: undefined,
468-
});
469-
}
470-
assignResolvedGatewaySecretInput({
471-
config: probeConfig,
472-
path: params.path,
473-
value: sentinel,
474-
});
475-
try {
476-
const resolved = resolveGatewayCredentialsFromConfig(
477-
resolveGatewayCredentialsFromConfigOptions({
478-
context: params.context,
479-
env: params.env,
480-
cfg: probeConfig,
481-
}),
482-
);
483-
const tokenCanWin = resolved.token === sentinel && !resolved.password;
484-
const passwordCanWin = resolved.password === sentinel && !resolved.token;
485-
return tokenCanWin || passwordCanWin;
486-
} catch {
487-
return false;
488-
}
489-
}
490-
491-
async function resolveConfiguredGatewaySecretInput(params: {
492-
config: OpenClawConfig;
493-
path: SupportedGatewaySecretInputPath;
494-
env: NodeJS.ProcessEnv;
495-
}): Promise<string | undefined> {
496-
return resolveGatewaySecretInputString({
497-
config: params.config,
498-
value: readGatewaySecretInputValue(params.config, params.path),
499-
path: params.path,
500-
env: params.env,
501365
});
502366
}
503367

504-
async function resolvePreferredGatewaySecretInputs(params: {
505-
context: ResolvedGatewayCallContext;
506-
env: NodeJS.ProcessEnv;
507-
config: OpenClawConfig;
508-
}): Promise<OpenClawConfig> {
509-
let nextConfig = params.config;
510-
for (const path of ALL_GATEWAY_SECRET_INPUT_PATHS) {
511-
if (
512-
!gatewaySecretInputPathCanWin({
513-
context: params.context,
514-
env: params.env,
515-
config: nextConfig,
516-
path,
517-
})
518-
) {
519-
continue;
520-
}
521-
if (nextConfig === params.config) {
522-
nextConfig = structuredClone(params.config);
523-
}
524-
try {
525-
const resolvedValue = await resolveConfiguredGatewaySecretInput({
526-
config: nextConfig,
527-
path,
528-
env: params.env,
529-
});
530-
assignResolvedGatewaySecretInput({
531-
config: nextConfig,
532-
path,
533-
value: resolvedValue,
534-
});
535-
} catch {
536-
// Keep scanning candidate paths so unresolved higher-priority refs do not
537-
// prevent valid fallback refs from being considered.
538-
continue;
539-
}
540-
}
541-
return nextConfig;
542-
}
543-
544-
async function resolveGatewayCredentialsFromConfigWithSecretInputs(params: {
545-
context: ResolvedGatewayCallContext;
546-
env: NodeJS.ProcessEnv;
547-
}): Promise<{ token?: string; password?: string }> {
548-
let resolvedConfig = await resolvePreferredGatewaySecretInputs({
549-
context: params.context,
550-
env: params.env,
551-
config: params.context.config,
552-
});
553-
const resolvedPaths = new Set<SupportedGatewaySecretInputPath>();
554-
for (;;) {
555-
try {
556-
return resolveGatewayCredentialsFromConfig(
557-
resolveGatewayCredentialsFromConfigOptions({
558-
context: params.context,
559-
env: params.env,
560-
cfg: resolvedConfig,
561-
}),
562-
);
563-
} catch (error) {
564-
if (!(error instanceof GatewaySecretRefUnavailableError)) {
565-
throw error;
566-
}
567-
const path = error.path;
568-
if (!isSupportedGatewaySecretInputPath(path) || resolvedPaths.has(path)) {
569-
throw error;
570-
}
571-
if (resolvedConfig === params.context.config) {
572-
resolvedConfig = structuredClone(params.context.config);
573-
}
574-
const resolvedValue = await resolveConfiguredGatewaySecretInput({
575-
config: resolvedConfig,
576-
path,
577-
env: params.env,
578-
});
579-
assignResolvedGatewaySecretInput({
580-
config: resolvedConfig,
581-
path,
582-
value: resolvedValue,
583-
});
584-
resolvedPaths.add(path);
585-
}
586-
}
587-
}
588-
589-
export async function resolveGatewayCredentialsWithSecretInputs(params: {
590-
config: OpenClawConfig;
591-
explicitAuth?: ExplicitGatewayAuth;
592-
urlOverride?: string;
593-
urlOverrideSource?: "cli" | "env";
594-
env?: NodeJS.ProcessEnv;
595-
modeOverride?: GatewayCredentialMode;
596-
localTokenPrecedence?: GatewayCredentialPrecedence;
597-
localPasswordPrecedence?: GatewayCredentialPrecedence;
598-
remoteTokenPrecedence?: GatewayRemoteCredentialPrecedence;
599-
remotePasswordPrecedence?: GatewayRemoteCredentialPrecedence;
600-
remoteTokenFallback?: GatewayRemoteCredentialFallback;
601-
remotePasswordFallback?: GatewayRemoteCredentialFallback;
602-
}): Promise<{ token?: string; password?: string }> {
603-
const modeOverride = params.modeOverride;
604-
const isRemoteMode = modeOverride
605-
? modeOverride === "remote"
606-
: params.config.gateway?.mode === "remote";
607-
const remoteFromConfig =
608-
params.config.gateway?.mode === "remote"
609-
? (params.config.gateway?.remote as GatewayRemoteSettings | undefined)
610-
: undefined;
611-
const remoteFromOverride =
612-
modeOverride === "remote"
613-
? (params.config.gateway?.remote as GatewayRemoteSettings | undefined)
614-
: undefined;
615-
const context: ResolvedGatewayCallContext = {
616-
config: params.config,
617-
configPath: resolveGatewayConfigPath(process.env),
618-
isRemoteMode,
619-
remote: remoteFromOverride ?? remoteFromConfig,
620-
urlOverride: trimToUndefined(params.urlOverride),
621-
urlOverrideSource: params.urlOverrideSource,
622-
remoteUrl: isRemoteMode
623-
? trimToUndefined((params.config.gateway?.remote as GatewayRemoteSettings | undefined)?.url)
624-
: undefined,
625-
explicitAuth: resolveExplicitGatewayAuth(params.explicitAuth),
626-
modeOverride,
627-
localTokenPrecedence: params.localTokenPrecedence,
628-
localPasswordPrecedence: params.localPasswordPrecedence,
629-
remoteTokenPrecedence: params.remoteTokenPrecedence,
630-
remotePasswordPrecedence: params.remotePasswordPrecedence,
631-
remoteTokenFallback: params.remoteTokenFallback,
632-
remotePasswordFallback: params.remotePasswordFallback,
633-
};
634-
return resolveGatewayCredentialsWithEnv(context, params.env ?? process.env);
635-
}
368+
export { resolveGatewayCredentialsWithSecretInputs };
636369

637370
async function resolveGatewayTlsFingerprint(params: {
638371
opts: CallGatewayBaseOptions;

src/gateway/connection-auth.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { OpenClawConfig } from "../config/types.openclaw.js";
2-
import type { ExplicitGatewayAuth } from "./call.js";
3-
import { resolveGatewayCredentialsWithSecretInputs } from "./call.js";
2+
import { resolveGatewayCredentialsWithSecretInputs } from "./credentials-secret-inputs.js";
43
import type {
4+
ExplicitGatewayAuth,
55
GatewayCredentialMode,
66
GatewayCredentialPrecedence,
77
GatewayRemoteCredentialFallback,

0 commit comments

Comments
 (0)