pairing: keep setup codes bootstrap-token only (#51259)

This commit is contained in:
Vincent Koc 2026-03-20 13:27:39 -07:00 committed by GitHub
parent 5a5e84ca1d
commit 11d71ca352
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 17 additions and 86 deletions

View File

@ -135,24 +135,16 @@ describe("registerQrCli", () => {
}; };
} }
function expectLoggedSetupCode( function expectLoggedSetupCode(url: string) {
url: string,
auth?: {
token?: string;
password?: string;
},
) {
const expected = encodePairingSetupCode({ const expected = encodePairingSetupCode({
url, url,
bootstrapToken: "bootstrap-123", bootstrapToken: "bootstrap-123",
...(auth?.token ? { token: auth.token } : {}),
...(auth?.password ? { password: auth.password } : {}),
}); });
expect(runtime.log).toHaveBeenCalledWith(expected); expect(runtime.log).toHaveBeenCalledWith(expected);
} }
function expectLoggedLocalSetupCode(auth?: { token?: string; password?: string }) { function expectLoggedLocalSetupCode() {
expectLoggedSetupCode("ws://gateway.local:18789", auth); expectLoggedSetupCode("ws://gateway.local:18789");
} }
function mockTailscaleStatusLookup() { function mockTailscaleStatusLookup() {
@ -189,7 +181,6 @@ describe("registerQrCli", () => {
const expected = encodePairingSetupCode({ const expected = encodePairingSetupCode({
url: "ws://gateway.local:18789", url: "ws://gateway.local:18789",
bootstrapToken: "bootstrap-123", bootstrapToken: "bootstrap-123",
token: "tok",
}); });
expect(runtime.log).toHaveBeenCalledWith(expected); expect(runtime.log).toHaveBeenCalledWith(expected);
expect(qrGenerate).not.toHaveBeenCalled(); expect(qrGenerate).not.toHaveBeenCalled();
@ -225,7 +216,7 @@ describe("registerQrCli", () => {
await runQr(["--setup-code-only", "--token", "override-token"]); await runQr(["--setup-code-only", "--token", "override-token"]);
expectLoggedLocalSetupCode({ token: "override-token" }); expectLoggedLocalSetupCode();
}); });
it("skips local password SecretRef resolution when --token override is provided", async () => { it("skips local password SecretRef resolution when --token override is provided", async () => {
@ -237,7 +228,7 @@ describe("registerQrCli", () => {
await runQr(["--setup-code-only", "--token", "override-token"]); await runQr(["--setup-code-only", "--token", "override-token"]);
expectLoggedLocalSetupCode({ token: "override-token" }); expectLoggedLocalSetupCode();
}); });
it("resolves local gateway auth password SecretRefs before setup code generation", async () => { it("resolves local gateway auth password SecretRefs before setup code generation", async () => {
@ -250,7 +241,7 @@ describe("registerQrCli", () => {
await runQr(["--setup-code-only"]); await runQr(["--setup-code-only"]);
expectLoggedLocalSetupCode({ password: "local-password-secret" }); expectLoggedLocalSetupCode();
expect(resolveCommandSecretRefsViaGateway).not.toHaveBeenCalled(); expect(resolveCommandSecretRefsViaGateway).not.toHaveBeenCalled();
}); });
@ -264,7 +255,7 @@ describe("registerQrCli", () => {
await runQr(["--setup-code-only"]); await runQr(["--setup-code-only"]);
expectLoggedLocalSetupCode({ password: "password-from-env" }); expectLoggedLocalSetupCode();
expect(resolveCommandSecretRefsViaGateway).not.toHaveBeenCalled(); expect(resolveCommandSecretRefsViaGateway).not.toHaveBeenCalled();
}); });
@ -279,7 +270,7 @@ describe("registerQrCli", () => {
await runQr(["--setup-code-only"]); await runQr(["--setup-code-only"]);
expectLoggedLocalSetupCode({ token: "token-123" }); expectLoggedLocalSetupCode();
expect(resolveCommandSecretRefsViaGateway).not.toHaveBeenCalled(); expect(resolveCommandSecretRefsViaGateway).not.toHaveBeenCalled();
}); });
@ -293,7 +284,7 @@ describe("registerQrCli", () => {
await runQr(["--setup-code-only"]); await runQr(["--setup-code-only"]);
expectLoggedLocalSetupCode({ password: "inferred-password" }); expectLoggedLocalSetupCode();
expect(resolveCommandSecretRefsViaGateway).not.toHaveBeenCalled(); expect(resolveCommandSecretRefsViaGateway).not.toHaveBeenCalled();
}); });
@ -342,7 +333,6 @@ describe("registerQrCli", () => {
const expected = encodePairingSetupCode({ const expected = encodePairingSetupCode({
url: "wss://remote.example.com:444", url: "wss://remote.example.com:444",
bootstrapToken: "bootstrap-123", bootstrapToken: "bootstrap-123",
token: "remote-tok",
}); });
expect(runtime.log).toHaveBeenCalledWith(expected); expect(runtime.log).toHaveBeenCalledWith(expected);
expect(resolveCommandSecretRefsViaGateway).toHaveBeenCalledWith( expect(resolveCommandSecretRefsViaGateway).toHaveBeenCalledWith(
@ -386,7 +376,6 @@ describe("registerQrCli", () => {
const expected = encodePairingSetupCode({ const expected = encodePairingSetupCode({
url: "wss://remote.example.com:444", url: "wss://remote.example.com:444",
bootstrapToken: "bootstrap-123", bootstrapToken: "bootstrap-123",
token: "remote-tok",
}); });
expect(runtime.log).toHaveBeenCalledWith(expected); expect(runtime.log).toHaveBeenCalledWith(expected);
}); });

View File

@ -69,8 +69,6 @@ function createGatewayTokenRefFixture() {
function decodeSetupCode(setupCode: string): { function decodeSetupCode(setupCode: string): {
url?: string; url?: string;
bootstrapToken?: string; bootstrapToken?: string;
token?: string;
password?: string;
} { } {
const padded = setupCode.replace(/-/g, "+").replace(/_/g, "/"); const padded = setupCode.replace(/-/g, "+").replace(/_/g, "/");
const padLength = (4 - (padded.length % 4)) % 4; const padLength = (4 - (padded.length % 4)) % 4;
@ -79,8 +77,6 @@ function decodeSetupCode(setupCode: string): {
return JSON.parse(json) as { return JSON.parse(json) as {
url?: string; url?: string;
bootstrapToken?: string; bootstrapToken?: string;
token?: string;
password?: string;
}; };
} }
@ -119,7 +115,7 @@ describe("cli integration: qr + dashboard token SecretRef", () => {
delete process.env.SHARED_GATEWAY_TOKEN; delete process.env.SHARED_GATEWAY_TOKEN;
}); });
it("uses the same resolved token SecretRef for both qr and dashboard commands", async () => { it("uses the same resolved token SecretRef for qr auth validation and dashboard commands", async () => {
const fixture = createGatewayTokenRefFixture(); const fixture = createGatewayTokenRefFixture();
process.env.SHARED_GATEWAY_TOKEN = "shared-token-123"; process.env.SHARED_GATEWAY_TOKEN = "shared-token-123";
loadConfigMock.mockReturnValue(fixture); loadConfigMock.mockReturnValue(fixture);
@ -137,7 +133,6 @@ describe("cli integration: qr + dashboard token SecretRef", () => {
const payload = decodeSetupCode(setupCode ?? ""); const payload = decodeSetupCode(setupCode ?? "");
expect(payload.url).toBe("ws://gateway.local:18789"); expect(payload.url).toBe("ws://gateway.local:18789");
expect(payload.bootstrapToken).toBeTruthy(); expect(payload.bootstrapToken).toBeTruthy();
expect(payload.token).toBe("shared-token-123");
expect(runtimeErrors).toEqual([]); expect(runtimeErrors).toEqual([]);
runtimeLogs.length = 0; runtimeLogs.length = 0;

View File

@ -45,8 +45,6 @@ describe("pairing setup code", () => {
authLabel: string; authLabel: string;
url?: string; url?: string;
urlSource?: string; urlSource?: string;
token?: string;
password?: string;
}, },
) { ) {
expect(resolved.ok).toBe(true); expect(resolved.ok).toBe(true);
@ -55,8 +53,6 @@ describe("pairing setup code", () => {
} }
expect(resolved.authLabel).toBe(params.authLabel); expect(resolved.authLabel).toBe(params.authLabel);
expect(resolved.payload.bootstrapToken).toBe("bootstrap-123"); expect(resolved.payload.bootstrapToken).toBe("bootstrap-123");
expect(resolved.payload.token).toBe(params.token);
expect(resolved.payload.password).toBe(params.password);
if (params.url) { if (params.url) {
expect(resolved.payload.url).toBe(params.url); expect(resolved.payload.url).toBe(params.url);
} }
@ -117,7 +113,6 @@ describe("pairing setup code", () => {
payload: { payload: {
url: "ws://gateway.local:19001", url: "ws://gateway.local:19001",
bootstrapToken: "bootstrap-123", bootstrapToken: "bootstrap-123",
token: "tok_123",
}, },
authLabel: "token", authLabel: "token",
urlSource: "gateway.bind=custom", urlSource: "gateway.bind=custom",
@ -144,7 +139,7 @@ describe("pairing setup code", () => {
}, },
); );
expectResolvedSetupOk(resolved, { authLabel: "password", password: "resolved-password" }); expectResolvedSetupOk(resolved, { authLabel: "password" });
}); });
it("uses OPENCLAW_GATEWAY_PASSWORD without resolving configured password SecretRef", async () => { it("uses OPENCLAW_GATEWAY_PASSWORD without resolving configured password SecretRef", async () => {
@ -167,7 +162,7 @@ describe("pairing setup code", () => {
}, },
); );
expectResolvedSetupOk(resolved, { authLabel: "password", password: "password-from-env" }); expectResolvedSetupOk(resolved, { authLabel: "password" });
}); });
it("does not resolve gateway.auth.password SecretRef in token mode", async () => { it("does not resolve gateway.auth.password SecretRef in token mode", async () => {
@ -189,7 +184,7 @@ describe("pairing setup code", () => {
}, },
); );
expectResolvedSetupOk(resolved, { authLabel: "token", token: "tok_123" }); expectResolvedSetupOk(resolved, { authLabel: "token" });
}); });
it("resolves gateway.auth.token SecretRef for pairing payload", async () => { it("resolves gateway.auth.token SecretRef for pairing payload", async () => {
@ -212,7 +207,7 @@ describe("pairing setup code", () => {
}, },
); );
expectResolvedSetupOk(resolved, { authLabel: "token", token: "resolved-token" }); expectResolvedSetupOk(resolved, { authLabel: "token" });
}); });
it("errors when gateway.auth.token SecretRef is unresolved in token mode", async () => { it("errors when gateway.auth.token SecretRef is unresolved in token mode", async () => {
@ -261,13 +256,13 @@ describe("pairing setup code", () => {
id: "MISSING_GW_TOKEN", id: "MISSING_GW_TOKEN",
}); });
expectResolvedSetupOk(resolved, { authLabel: "password", password: "password-from-env" }); expectResolvedSetupOk(resolved, { authLabel: "password" });
}); });
it("does not treat env-template token as plaintext in inferred mode", async () => { it("does not treat env-template token as plaintext in inferred mode", async () => {
const resolved = await resolveInferredModeWithPasswordEnv("${MISSING_GW_TOKEN}"); const resolved = await resolveInferredModeWithPasswordEnv("${MISSING_GW_TOKEN}");
expectResolvedSetupOk(resolved, { authLabel: "password", password: "password-from-env" }); expectResolvedSetupOk(resolved, { authLabel: "password" });
}); });
it("requires explicit auth mode when token and password are both configured", async () => { it("requires explicit auth mode when token and password are both configured", async () => {
@ -333,7 +328,7 @@ describe("pairing setup code", () => {
}, },
); );
expectResolvedSetupOk(resolved, { authLabel: "token", token: "new-token" }); expectResolvedSetupOk(resolved, { authLabel: "token" });
}); });
it("errors when gateway is loopback only", async () => { it("errors when gateway is loopback only", async () => {
@ -367,7 +362,6 @@ describe("pairing setup code", () => {
payload: { payload: {
url: "wss://mb-server.tailnet.ts.net", url: "wss://mb-server.tailnet.ts.net",
bootstrapToken: "bootstrap-123", bootstrapToken: "bootstrap-123",
password: "secret",
}, },
authLabel: "password", authLabel: "password",
urlSource: "gateway.tailscale.mode=serve", urlSource: "gateway.tailscale.mode=serve",
@ -396,7 +390,6 @@ describe("pairing setup code", () => {
payload: { payload: {
url: "wss://remote.example.com:444", url: "wss://remote.example.com:444",
bootstrapToken: "bootstrap-123", bootstrapToken: "bootstrap-123",
token: "tok_123",
}, },
authLabel: "token", authLabel: "token",
urlSource: "gateway.remote.url", urlSource: "gateway.remote.url",

View File

@ -16,8 +16,6 @@ import { resolveTailnetHostWithRunner } from "../shared/tailscale-status.js";
export type PairingSetupPayload = { export type PairingSetupPayload = {
url: string; url: string;
bootstrapToken: string; bootstrapToken: string;
token?: string;
password?: string;
}; };
export type PairingSetupCommandResult = { export type PairingSetupCommandResult = {
@ -64,11 +62,6 @@ type ResolveAuthLabelResult = {
error?: string; error?: string;
}; };
type ResolveSharedAuthResult = {
token?: string;
password?: string;
};
function normalizeUrl(raw: string, schemeFallback: "ws" | "wss"): string | null { function normalizeUrl(raw: string, schemeFallback: "ws" | "wss"): string | null {
const trimmed = raw.trim(); const trimmed = raw.trim();
if (!trimmed) { if (!trimmed) {
@ -213,41 +206,6 @@ function resolvePairingSetupAuthLabel(
return { error: "Gateway auth is not configured (no token or password)." }; return { error: "Gateway auth is not configured (no token or password)." };
} }
function resolvePairingSetupSharedAuth(
cfg: OpenClawConfig,
env: NodeJS.ProcessEnv,
): ResolveSharedAuthResult {
const defaults = cfg.secrets?.defaults;
const tokenRef = resolveSecretInputRef({
value: cfg.gateway?.auth?.token,
defaults,
}).ref;
const passwordRef = resolveSecretInputRef({
value: cfg.gateway?.auth?.password,
defaults,
}).ref;
const token =
resolveGatewayTokenFromEnv(env) ||
(tokenRef ? undefined : normalizeSecretInputString(cfg.gateway?.auth?.token));
const password =
resolveGatewayPasswordFromEnv(env) ||
(passwordRef ? undefined : normalizeSecretInputString(cfg.gateway?.auth?.password));
const mode = cfg.gateway?.auth?.mode;
if (mode === "token") {
return { token };
}
if (mode === "password") {
return { password };
}
if (token) {
return { token };
}
if (password) {
return { password };
}
return {};
}
async function resolveGatewayTokenSecretRef( async function resolveGatewayTokenSecretRef(
cfg: OpenClawConfig, cfg: OpenClawConfig,
env: NodeJS.ProcessEnv, env: NodeJS.ProcessEnv,
@ -417,8 +375,6 @@ export async function resolvePairingSetupFromConfig(
if (authLabel.error) { if (authLabel.error) {
return { ok: false, error: authLabel.error }; return { ok: false, error: authLabel.error };
} }
const sharedAuth = resolvePairingSetupSharedAuth(cfgForAuth, env);
const urlResult = await resolveGatewayUrl(cfgForAuth, { const urlResult = await resolveGatewayUrl(cfgForAuth, {
env, env,
publicUrl: options.publicUrl, publicUrl: options.publicUrl,
@ -445,8 +401,6 @@ export async function resolvePairingSetupFromConfig(
baseDir: options.pairingBaseDir, baseDir: options.pairingBaseDir,
}) })
).token, ).token,
...(sharedAuth.token ? { token: sharedAuth.token } : {}),
...(sharedAuth.password ? { password: sharedAuth.password } : {}),
}, },
authLabel: authLabel.label, authLabel: authLabel.label,
urlSource: urlResult.source ?? "unknown", urlSource: urlResult.source ?? "unknown",