Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 15 additions & 13 deletions apps/ade-cli/src/services/sync/syncAccountHelloAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,14 @@ import { evaluatePairedHelloDpop, syncDpopFailureMessage, type SyncDpopNonceCach
export const SYNC_REPAIR_REQUIRED_MESSAGE = "This device is not paired with this machine, or its saved"
+ " pairing is no longer valid. Pair it again.";

export const SYNC_ACCOUNT_SESSION_CHANGED_MESSAGE = "The ADE account session on this machine changed"
+ " while connecting. Try again.";
export const SYNC_ACCOUNT_SESSION_CHANGED_MESSAGE = "The ADE account session on the computer you're"
+ " connecting to changed while connecting. Try again.";

export const SYNC_ACCOUNT_VERIFY_UNAVAILABLE_MESSAGE = "This machine cannot verify ADE accounts."
+ " Update ADE on this computer, then try again.";
export const SYNC_ACCOUNT_VERIFY_UNAVAILABLE_MESSAGE = "The computer you're connecting to cannot verify"
+ " ADE accounts. Update ADE there, then try again.";

export const SYNC_ACCOUNT_NOT_SIGNED_IN_MESSAGE = "This machine is not signed in to an ADE account."
+ " Sign in on this computer, then try again.";
export const SYNC_ACCOUNT_NOT_SIGNED_IN_MESSAGE = "The computer you're connecting to is not signed in"
+ " to an ADE account. Sign in on that computer, then try again.";

export const SYNC_ACCOUNT_DEVICE_MISMATCH_MESSAGE = "The account identity in this connection did not"
+ " match the device that sent it.";
Expand All @@ -48,11 +48,12 @@ export const SYNC_ACCOUNT_KEYLESS_RECORD_MESSAGE = "This device's saved pairing
export const SYNC_ACCOUNT_OTHER_OWNER_MESSAGE = "This device is already paired to this machine under"
+ " a different ADE account.";

export const SYNC_ACCOUNT_PAIRING_WRITE_FAILED_MESSAGE = "This machine could not save the new pairing"
+ " for this device. Try again.";
export const SYNC_ACCOUNT_PAIRING_WRITE_FAILED_MESSAGE = "The computer you're connecting to could not"
+ " save the new pairing for this device. Try again.";

export const SYNC_ACCOUNT_VERIFY_FAILED_MESSAGE = "This machine could not verify your ADE account"
+ " session. Sign out and back in on this device, then try again.";
export const SYNC_ACCOUNT_VERIFY_FAILED_MESSAGE = "The computer you're connecting to could not verify"
+ " its ADE account session. Open ADE there and check that it is signed in to the same ADE account,"
+ " then try again.";

export type SyncAccountHelloAuth = Extract<SyncHelloPayload["auth"], { kind: "account" }>;

Expand Down Expand Up @@ -111,8 +112,9 @@ export type SyncAccountHelloAuthOptions = {
/** "PIN" on the project host, "code" on the brain — same instruction, local wording. */
pairingCodeNoun: string;
/**
* Code carried when this machine holds no account session at all. The brain
* answers `relay_account_required` because its only account route is Relay.
* Code carried when this machine holds no account session at all. The project
* host uses `account_not_signed_in`; the brain answers `relay_account_required`
* because its only account route is Relay.
*/
notSignedInCode: SyncHelloErrorPayload["code"];
};
Expand Down Expand Up @@ -298,6 +300,6 @@ export async function authenticateSyncAccountHello(
? (error as { code: string }).code
: "verification_failed",
});
return reject(SYNC_ACCOUNT_VERIFY_FAILED_MESSAGE);
return reject(SYNC_ACCOUNT_VERIFY_FAILED_MESSAGE, "account_verification_failed");
Comment thread
arul28 marked this conversation as resolved.
Outdated
}
}
4 changes: 2 additions & 2 deletions apps/ade-cli/src/services/sync/syncHostService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4156,8 +4156,8 @@ describe("sync host account authentication", () => {
"signed-out account hello_error",
);
expect(signedOutRejected.payload).toMatchObject({
code: "auth_failed",
message: expect.stringMatching(/not signed in.*Sign in on this computer/i),
code: "account_not_signed_in",
message: expect.stringMatching(/computer you're connecting to.*not signed in.*Sign in on that computer/i),
});

const pinClient = await openAccountClient(port);
Expand Down
2 changes: 1 addition & 1 deletion apps/ade-cli/src/services/sync/syncHostService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7290,7 +7290,7 @@ export function createSyncHostService(args: SyncHostServiceArgs) {
arbitrateConnectionAttempt(hello.peer.deviceId, peer, hello.peer),
allowLegacyUpgrade: true,
pairingCodeNoun: "PIN",
notSignedInCode: "auth_failed",
notSignedInCode: "account_not_signed_in",
});
if (accountResult.kind === "stale") return true;
if (accountResult.kind === "rejected") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,16 +173,28 @@ describe("paired runtime endpoint routes", () => {
// account session repaired. Both used to arrive as `auth_failed`.
expect(classifyPairedRuntimeFailure(
new PairedRuntimeHelloRejectedError(
"This machine cannot verify ADE accounts. Update ADE on this computer, then try again.",
"The computer you're connecting to cannot verify ADE accounts. Update ADE there, then try again.",
"host_update_required",
),
)).toBe("protocol");
expect(classifyPairedRuntimeFailure(
new PairedRuntimeHelloRejectedError(
"The ADE account session on this machine changed while connecting. Try again.",
"The ADE account session on the computer you're connecting to changed while connecting. Try again.",
"account_session_changed",
),
)).toBe("authentication");
expect(classifyPairedRuntimeFailure(
new PairedRuntimeHelloRejectedError(
"The computer you're connecting to is not signed in to an ADE account. Sign in on that computer, then try again.",
"account_not_signed_in",
),
)).toBe("authentication");
expect(classifyPairedRuntimeFailure(
new PairedRuntimeHelloRejectedError(
"The computer you're connecting to could not verify its ADE account session.",
"account_verification_failed",
),
)).toBe("authentication");
});

it("diagnoses one dominant cause and says it without routes or ports", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,9 +231,11 @@ const HELLO_CODE_FAILURES: Record<
repair_required: "pairing",
// Desktop→desktop dials with `auth.kind: "paired"`, so the host's generic
// `auth_failed` here is always a pairing-record rejection — including from
// hosts too old to send `repair_required`. Account problems arrive as
// `relay_account_required`, which has its own row.
// hosts too old to send `repair_required`. Account problems use their own
// authentication codes; `relay_account_required` remains the relay-only form.
auth_failed: "pairing",
account_not_signed_in: "authentication",
account_verification_failed: "authentication",
Comment thread
coderabbitai[bot] marked this conversation as resolved.
relay_account_required: "authentication",
// The host is fine and the pairing is fine — it just cannot verify ADE
// accounts yet. "Pair it again" would send the user in circles; the update
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -831,7 +831,9 @@ describe("DesktopPairedMachineStore", () => {
return ws as unknown as WebSocket;
},
},
)).rejects.toThrow(/relay relay\.example:.*relay refused/i);
)).rejects.toThrow(
"Could not reach Legacy Studio. Make sure it's awake and ADE is open, then try again.",
);
} finally {
warn.mockRestore();
}
Expand Down Expand Up @@ -1031,7 +1033,11 @@ describe("DesktopPairedMachineStore", () => {
},
);

await expect(pairing).rejects.toThrow(expectedError);
await expect(pairing).rejects.toThrow(
responseAead
? expectedError
: "Could not connect to Cipher Studio. Update ADE on that computer, then try again.",
);
expect(sentTypes).toEqual(["account_challenge"]);
});

Expand Down Expand Up @@ -1359,8 +1365,10 @@ describe("DesktopPairedMachineStore", () => {
}) as unknown as WebSocket,
},
);
// The host's real reason is surfaced, not the identity-verification error.
await expect(pairing).rejects.toThrow(/Try again in 3 minutes/);
// The host's real retry window is kept, without exposing the route details.
await expect(pairing).rejects.toThrow(
"Could not connect to Expected host. Too many failed authentication attempts. Try again in 3 minutes.",
);
await expect(pairing).rejects.not.toMatchObject({
code: "account_host_identity_verification_failed",
});
Expand Down Expand Up @@ -1424,7 +1432,7 @@ describe("DesktopPairedMachineStore", () => {
expect(sentTypes).toEqual(["account_challenge"]);
});

it("aggregates every failed adoption route with its kind and host", async () => {
it("summarizes failed adoption routes without exposing the route dump in the headline", async () => {
const signing = generateKeyPairSync("ed25519");
const openedEndpoints: string[] = [];
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
Expand All @@ -1448,6 +1456,7 @@ describe("DesktopPairedMachineStore", () => {
};

let failure: Error | null = null;
let warning: unknown = null;
try {
await new DesktopPairedMachineStore().pairWithAccountMachine(
machine,
Expand All @@ -1467,12 +1476,21 @@ describe("DesktopPairedMachineStore", () => {
} catch (error) {
failure = error instanceof Error ? error : new Error(String(error));
} finally {
warning = warn.mock.calls[0]?.[1] ?? null;
warn.mockRestore();
}

expect(failure?.message).toMatch(/relay relay\.example:/);
expect(failure?.message).toMatch(/tailnet 100\.75\.20\.63:/);
expect(failure?.message).toMatch(/lan unavailable-studio\.local:/);
expect(failure?.message).toBe(
"Could not reach Unavailable Studio. Make sure it's awake and ADE is open, then try again.",
);
expect(failure?.message).not.toMatch(/relay|tailnet|100\.75\.20\.63|unavailable-studio/i);
expect(warning).toMatchObject({
attempts: [
{ kind: "lan", host: "unavailable-studio.local", failure: "unreachable" },
{ kind: "tailnet", host: "100.75.20.63", failure: "unreachable" },
{ kind: "relay", host: "relay.example", failure: "unreachable" },
],
});
expect(openedEndpoints.map(endpointWithoutCorrelation)).toEqual([
"ws://unavailable-studio.local:8787/",
"ws://100.75.20.63:8787/",
Expand All @@ -1483,6 +1501,56 @@ describe("DesktopPairedMachineStore", () => {
expect(endpointCorrelationId(openedEndpoints[2]!)).toMatch(/^[0-9a-f-]{36}$/);
});

it("names the account machine when the target is signed out", async () => {
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
const machine: AdeAccountMachine = {
machineKey: "machine-signed-out",
deviceId: "host-signed-out",
name: "Arul's Mac Studio",
platform: "macOS",
deviceType: "desktop",
online: true,
lastSeenAt: Date.now(),
reachableEndpoints: [
{ kind: "relay", url: "wss://relay.example/connect/machine-signed-out" },
],
};

let failure: Error | null = null;
try {
await new DesktopPairedMachineStore().pairWithAccountMachine(
machine,
"laptop-account-token",
"MacBook Pro",
{
accountOwnerUserId: "account-user",
relayBaseUrls: ["https://relay.example"],
createWebSocket: () => new FakeWebSocket((text, ws) => {
const envelope = parseSyncEnvelope(wsDataToText(text));
if (envelope.type !== "hello") return;
ws.receive(encodeSyncEnvelope({
type: "hello_error",
requestId: envelope.requestId,
payload: {
code: "account_not_signed_in",
message: "The computer you're connecting to is not signed in to an ADE account. Sign in on that computer, then try again.",
},
}));
}) as unknown as WebSocket,
},
);
} catch (error) {
failure = error instanceof Error ? error : new Error(String(error));
} finally {
warn.mockRestore();
}

expect(failure?.message).toBe(
"Could not connect to Arul's Mac Studio. Arul's Mac Studio is not signed in to the same ADE account. Open ADE on that computer, sign in, then try again.",
);
expect(failure?.message).not.toMatch(/sign out|this device|relay/i);
});

it.each([
{ name: "sign-out", ownerUserId: null },
{ name: "account switch", ownerUserId: "account-user-2" },
Expand Down
Loading
Loading