fix(signal): clear stale account identity

This commit is contained in:
jesse-merhi
2026-08-11 17:19:13 +10:00
parent 7ccc9cb62d
commit e2c7edf856
4 changed files with 101 additions and 2 deletions
+41
View File
@@ -145,6 +145,47 @@ describe("signalSetupAdapter", () => {
},
);
it("clears the UUID when setup replaces the normalized Signal account", () => {
const next = signalSetupAdapter.applyAccountConfig?.({
cfg: {
channels: {
signal: {
accounts: {
work: {
account: "+15555550123",
accountUuid: "123e4567-e89b-12d3-a456-426614174000",
},
},
},
},
},
accountId: "work",
input: { signalNumber: "+15555550124" },
});
expect(next?.channels?.signal?.accounts?.work?.account).toBe("+15555550124");
expect(next?.channels?.signal?.accounts?.work?.accountUuid).toBeUndefined();
});
it("preserves the UUID when setup keeps the same normalized Signal account", () => {
const accountUuid = "123e4567-e89b-12d3-a456-426614174000";
const next = signalSetupAdapter.applyAccountConfig?.({
cfg: {
channels: {
signal: {
account: "+15555550123",
accountUuid,
},
},
},
accountId: "default",
input: { signalNumber: "signal: +1 (555) 555-0123" },
});
expect(next?.channels?.signal?.account).toBe("+15555550123");
expect(next?.channels?.signal?.accountUuid).toBe(accountUuid);
});
it("restores a generically promoted default account before writing a named account", () => {
const next = signalSetupAdapter.applyAccountConfig?.({
cfg: {
+17 -1
View File
@@ -10,6 +10,7 @@ import {
createPatchedAccountSetupAdapter,
createSetupInputPresenceValidator,
DEFAULT_ACCOUNT_ID,
patchChannelConfigForAccount,
promptParsedAllowFromForAccount,
setAccountAllowFromForChannel,
setSetupChannelEnabled,
@@ -169,6 +170,10 @@ export function normalizeSignalAccountInput(value: string | null | undefined): s
return `+${digits}`;
}
export function isSameSignalAccount(left: string | null | undefined, right: string): boolean {
return normalizeSignalAccountInput(left) === normalizeSignalAccountInput(right);
}
function isUuidLike(value: string): boolean {
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
}
@@ -504,7 +509,18 @@ export const signalSetupAdapter: ChannelSetupAdapter<SignalSetupInput> = {
// transport. Rejoin that pair here so Signal keeps one canonical default-account shape.
const cfg = restorePromotedSignalDefaultAccount(params.cfg);
const previousTransport = resolveConfiguredSignalTransport(cfg, accountId);
const next = signalSetupAdapterBase.applyAccountConfig?.({ ...params, cfg, accountId }) ?? cfg;
let next = signalSetupAdapterBase.applyAccountConfig?.({ ...params, cfg, accountId }) ?? cfg;
if (
suppliedAccount &&
!isSameSignalAccount(resolveSignalAccount({ cfg, accountId }).config.account, suppliedAccount)
) {
next = patchChannelConfigForAccount({
cfg: next,
channel,
accountId,
patch: { accountUuid: undefined },
});
}
const configuredTransport = resolveConfiguredSignalTransport(next, accountId);
if (configuredTransport && configuredTransport.kind !== "managed-native") {
const transport =
@@ -308,6 +308,39 @@ describe("Signal existing-server setup", () => {
expect(finalized?.cfg?.channels?.signal?.account).toBe("+15555550123");
});
it("preserves the UUID when recovery keeps the same normalized account", async () => {
mocks.probeSignalTransport.mockResolvedValueOnce({
ok: false,
error: "selected account is unavailable",
});
const accountUuid = "123e4567-e89b-12d3-a456-426614174000";
const queued = createQueuedWizardPrompter({
selectValues: ["account"],
textValues: ["signal: +1 (555) 555-0123"],
});
const finalized = await runSetupWizardFinalize({
finalize: signalSetupWizard.finalize,
cfg: {
channels: {
signal: {
account: "+15555550123",
accountUuid,
},
},
},
credentialValues: {
signalTransportKind: "external-native",
signalServerUrl: "http://signal-helper:8080",
},
prompter: queued.prompter,
runtime: createRuntimeEnv({ throwOnExit: false }),
});
expect(finalized?.cfg?.channels?.signal?.account).toBe("+15555550123");
expect(finalized?.cfg?.channels?.signal?.accountUuid).toBe(accountUuid);
});
it("rejects an existing-server recovery account owned by a sibling", async () => {
mocks.probeSignalTransport.mockResolvedValueOnce({
ok: false,
+10 -1
View File
@@ -15,6 +15,7 @@ import {
} from "./accounts.js";
import {
assertSignalAccountNotAssignedToSibling,
isSameSignalAccount,
normalizeSignalAccountInput,
signalSetupStateKeys,
} from "./setup-core.js";
@@ -67,7 +68,15 @@ export async function finalizeSignalExistingServerSetup(params: SignalFinalizePa
cfg,
channel: "signal",
accountId: params.accountId,
patch: { account, accountUuid: undefined },
patch: {
account,
...(isSameSignalAccount(
resolveSignalAccount({ cfg, accountId: params.accountId }).config.account,
account,
)
? {}
: { accountUuid: undefined }),
},
});
shouldPromptAccount = false;
}