mirror of https://github.com/openclaw/openclaw.git
fix(gateway): prefer bootstrap auth over tailscale (#59232)
* fix(gateway): prefer bootstrap auth over tailscale * fix(gateway): prefer bootstrap auth over tailscale (#59232) (thanks @ngutman)
This commit is contained in:
parent
5cf254a5f7
commit
017bc5261c
|
|
@ -11,6 +11,7 @@ Docs: https://docs.openclaw.ai
|
|||
### Fixes
|
||||
|
||||
- Matrix/multi-account: keep room-level `account` scoping, inherited room overrides, and implicit account selection consistent across top-level default auth, named accounts, and cached-credential env setups. (#58449) thanks @Daanvdplas and @gumadeiras.
|
||||
- Gateway/pairing: prefer explicit QR bootstrap auth over earlier Tailscale auth classification so iOS `/pair qr` silent bootstrap pairing does not fall through to `pairing required`. (#59232) Thanks @ngutman.
|
||||
- Config/Discord: coerce safe integer numeric Discord IDs to strings during config validation, keep unsafe or precision-losing numeric snowflakes rejected, and align `openclaw doctor` repair guidance with the same fail-closed behavior. (#45125) Thanks @moliendocode.
|
||||
- Gateway/sessions: scope bare `sessions.create` aliases like `main` to the requested agent while preserving the canonical `global` and `unknown` sentinel keys. (#58207) thanks @jalehman.
|
||||
- `/context detail` now compares the tracked prompt estimate with cached context usage and surfaces untracked provider/runtime overhead when present. (#28391) thanks @ImLukeF.
|
||||
|
|
|
|||
|
|
@ -169,23 +169,58 @@ describe("resolveConnectAuthDecision", () => {
|
|||
expect(verifyDeviceToken).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns the original decision when device fallback does not apply", async () => {
|
||||
it("prefers a valid bootstrap token over an already successful shared auth path", async () => {
|
||||
const verifyBootstrapToken = vi.fn<VerifyBootstrapTokenFn>(async () => ({ ok: true }));
|
||||
const verifyDeviceToken = vi.fn<VerifyDeviceTokenFn>(async () => ({ ok: true }));
|
||||
const decision = await resolveConnectAuthDecision({
|
||||
state: createBaseState({
|
||||
authResult: { ok: true, method: "token" },
|
||||
authResult: { ok: true, method: "tailscale" },
|
||||
authOk: true,
|
||||
authMethod: "tailscale",
|
||||
bootstrapTokenCandidate: "bootstrap-token",
|
||||
deviceTokenCandidate: undefined,
|
||||
deviceTokenCandidateSource: undefined,
|
||||
}),
|
||||
hasDeviceIdentity: true,
|
||||
deviceId: "dev-1",
|
||||
publicKey: "pub-1",
|
||||
role: "operator",
|
||||
role: "node",
|
||||
scopes: [],
|
||||
verifyBootstrapToken: async () => ({ ok: false, reason: "bootstrap_token_invalid" }),
|
||||
verifyBootstrapToken,
|
||||
verifyDeviceToken,
|
||||
});
|
||||
expect(decision.authOk).toBe(true);
|
||||
expect(decision.authMethod).toBe("token");
|
||||
expect(decision.authMethod).toBe("bootstrap-token");
|
||||
expect(verifyBootstrapToken).toHaveBeenCalledOnce();
|
||||
expect(verifyDeviceToken).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps the original successful auth path when bootstrap validation fails", async () => {
|
||||
const verifyBootstrapToken = vi.fn<VerifyBootstrapTokenFn>(async () => ({
|
||||
ok: false,
|
||||
reason: "bootstrap_token_invalid",
|
||||
}));
|
||||
const verifyDeviceToken = vi.fn<VerifyDeviceTokenFn>(async () => ({ ok: true }));
|
||||
const decision = await resolveConnectAuthDecision({
|
||||
state: createBaseState({
|
||||
authResult: { ok: true, method: "tailscale" },
|
||||
authOk: true,
|
||||
authMethod: "tailscale",
|
||||
bootstrapTokenCandidate: "bootstrap-token",
|
||||
deviceTokenCandidate: undefined,
|
||||
deviceTokenCandidateSource: undefined,
|
||||
}),
|
||||
hasDeviceIdentity: true,
|
||||
deviceId: "dev-1",
|
||||
publicKey: "pub-1",
|
||||
role: "node",
|
||||
scopes: [],
|
||||
verifyBootstrapToken,
|
||||
verifyDeviceToken,
|
||||
});
|
||||
expect(decision.authOk).toBe(true);
|
||||
expect(decision.authMethod).toBe("tailscale");
|
||||
expect(verifyBootstrapToken).toHaveBeenCalledOnce();
|
||||
expect(verifyDeviceToken).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -170,13 +170,7 @@ export async function resolveConnectAuthDecision(params: {
|
|||
let authMethod = params.state.authMethod;
|
||||
|
||||
const bootstrapTokenCandidate = params.state.bootstrapTokenCandidate;
|
||||
if (
|
||||
params.hasDeviceIdentity &&
|
||||
params.deviceId &&
|
||||
params.publicKey &&
|
||||
!authOk &&
|
||||
bootstrapTokenCandidate
|
||||
) {
|
||||
if (params.hasDeviceIdentity && params.deviceId && params.publicKey && bootstrapTokenCandidate) {
|
||||
const tokenCheck = await params.verifyBootstrapToken({
|
||||
deviceId: params.deviceId,
|
||||
publicKey: params.publicKey,
|
||||
|
|
@ -185,9 +179,14 @@ export async function resolveConnectAuthDecision(params: {
|
|||
scopes: params.scopes,
|
||||
});
|
||||
if (tokenCheck.ok) {
|
||||
// Prefer an explicit valid bootstrap token even when another auth path
|
||||
// (for example tailscale serve header auth) already succeeded. QR pairing
|
||||
// relies on the server classifying the handshake as bootstrap-token so the
|
||||
// initial node pairing can be silently auto-approved and the bootstrap
|
||||
// token can be revoked after approval.
|
||||
authOk = true;
|
||||
authMethod = "bootstrap-token";
|
||||
} else {
|
||||
} else if (!authOk) {
|
||||
authResult = { ok: false, reason: tokenCheck.reason ?? "bootstrap_token_invalid" };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue