fix(pairing): support legacy telegram allowFrom migration

This commit is contained in:
Peter Steinberger
2026-02-16 03:25:59 +00:00
parent 18c6f40d32
commit 6754a926ee
5 changed files with 175 additions and 8 deletions

View File

@@ -178,6 +178,42 @@ describe("doctor legacy state migrations", () => {
expect(fs.existsSync(path.join(oauthDir, "creds.json"))).toBe(false);
});
it("migrates legacy Telegram pairing allowFrom store to account-scoped default file", async () => {
const root = await makeTempRoot();
const cfg: OpenClawConfig = {};
const oauthDir = path.join(root, "credentials");
fs.mkdirSync(oauthDir, { recursive: true });
fs.writeFileSync(
path.join(oauthDir, "telegram-allowFrom.json"),
JSON.stringify(
{
version: 1,
allowFrom: ["123456"],
},
null,
2,
) + "\n",
"utf-8",
);
const detected = await detectLegacyStateMigrations({
cfg,
env: { OPENCLAW_STATE_DIR: root } as NodeJS.ProcessEnv,
});
expect(detected.pairingAllowFrom.hasLegacyTelegram).toBe(true);
const result = await runLegacyStateMigrations({ detected, now: () => 123 });
expect(result.warnings).toEqual([]);
const target = path.join(oauthDir, "telegram-default-allowFrom.json");
expect(fs.existsSync(target)).toBe(true);
expect(JSON.parse(fs.readFileSync(target, "utf-8"))).toEqual({
version: 1,
allowFrom: ["123456"],
});
});
it("no-ops when nothing detected", async () => {
const root = await makeTempRoot();
const cfg: OpenClawConfig = {};

View File

@@ -116,6 +116,11 @@ export const detectLegacyStateMigrations = vi.fn().mockResolvedValue({
targetDir: "/tmp/oauth/whatsapp/default",
hasLegacy: false,
},
pairingAllowFrom: {
legacyTelegramPath: "/tmp/oauth/telegram-allowFrom.json",
targetTelegramPath: "/tmp/oauth/telegram-default-allowFrom.json",
hasLegacyTelegram: false,
},
preview: [],
}) as unknown as MockFn;
@@ -306,6 +311,11 @@ export async function arrangeLegacyStateMigrationTest(): Promise<{
targetDir: "/tmp/oauth/whatsapp/default",
hasLegacy: false,
},
pairingAllowFrom: {
legacyTelegramPath: "/tmp/oauth/telegram-allowFrom.json",
targetTelegramPath: "/tmp/oauth/telegram-default-allowFrom.json",
hasLegacyTelegram: false,
},
preview: ["- Legacy sessions detected"],
});
runLegacyStateMigrations.mockResolvedValueOnce({

View File

@@ -55,6 +55,11 @@ export type LegacyStateDetection = {
targetDir: string;
hasLegacy: boolean;
};
pairingAllowFrom: {
legacyTelegramPath: string;
targetTelegramPath: string;
hasLegacyTelegram: boolean;
};
preview: string[];
};
@@ -612,6 +617,13 @@ export async function detectLegacyStateMigrations(params: {
const hasLegacyWhatsAppAuth =
fileExists(path.join(oauthDir, "creds.json")) &&
!fileExists(path.join(targetWhatsAppAuthDir, "creds.json"));
const legacyTelegramAllowFromPath = path.join(oauthDir, "telegram-allowFrom.json");
const targetTelegramAllowFromPath = path.join(
oauthDir,
`telegram-${DEFAULT_ACCOUNT_ID}-allowFrom.json`,
);
const hasLegacyTelegramAllowFrom =
fileExists(legacyTelegramAllowFromPath) && !fileExists(targetTelegramAllowFromPath);
const preview: string[] = [];
if (hasLegacySessions) {
@@ -626,6 +638,11 @@ export async function detectLegacyStateMigrations(params: {
if (hasLegacyWhatsAppAuth) {
preview.push(`- WhatsApp auth: ${oauthDir}${targetWhatsAppAuthDir} (keep oauth.json)`);
}
if (hasLegacyTelegramAllowFrom) {
preview.push(
`- Telegram pairing allowFrom: ${legacyTelegramAllowFromPath}${targetTelegramAllowFromPath}`,
);
}
return {
targetAgentId,
@@ -651,6 +668,11 @@ export async function detectLegacyStateMigrations(params: {
targetDir: targetWhatsAppAuthDir,
hasLegacy: hasLegacyWhatsAppAuth,
},
pairingAllowFrom: {
legacyTelegramPath: legacyTelegramAllowFromPath,
targetTelegramPath: targetTelegramAllowFromPath,
hasLegacyTelegram: hasLegacyTelegramAllowFrom,
},
preview,
};
}
@@ -867,6 +889,28 @@ async function migrateLegacyWhatsAppAuth(
return { changes, warnings };
}
async function migrateLegacyTelegramPairingAllowFrom(
detected: LegacyStateDetection,
): Promise<{ changes: string[]; warnings: string[] }> {
const changes: string[] = [];
const warnings: string[] = [];
if (!detected.pairingAllowFrom.hasLegacyTelegram) {
return { changes, warnings };
}
const legacyPath = detected.pairingAllowFrom.legacyTelegramPath;
const targetPath = detected.pairingAllowFrom.targetTelegramPath;
try {
ensureDir(path.dirname(targetPath));
fs.copyFileSync(legacyPath, targetPath);
changes.push(`Copied Telegram pairing allowFrom → ${targetPath}`);
} catch (err) {
warnings.push(`Failed migrating Telegram pairing allowFrom (${legacyPath}): ${String(err)}`);
}
return { changes, warnings };
}
export async function runLegacyStateMigrations(params: {
detected: LegacyStateDetection;
now?: () => number;
@@ -876,9 +920,20 @@ export async function runLegacyStateMigrations(params: {
const sessions = await migrateLegacySessions(detected, now);
const agentDir = await migrateLegacyAgentDir(detected, now);
const whatsappAuth = await migrateLegacyWhatsAppAuth(detected);
const telegramPairingAllowFrom = await migrateLegacyTelegramPairingAllowFrom(detected);
return {
changes: [...sessions.changes, ...agentDir.changes, ...whatsappAuth.changes],
warnings: [...sessions.warnings, ...agentDir.warnings, ...whatsappAuth.warnings],
changes: [
...sessions.changes,
...agentDir.changes,
...whatsappAuth.changes,
...telegramPairingAllowFrom.changes,
],
warnings: [
...sessions.warnings,
...agentDir.warnings,
...whatsappAuth.warnings,
...telegramPairingAllowFrom.warnings,
],
};
}

View File

@@ -184,4 +184,38 @@ describe("pairing store", () => {
expect(channelScoped).not.toContain("12345");
});
});
it("reads legacy channel-scoped allowFrom for default account", async () => {
await withTempStateDir(async (stateDir) => {
const oauthDir = resolveOAuthDir(process.env, stateDir);
await fs.mkdir(oauthDir, { recursive: true });
await fs.writeFile(
path.join(oauthDir, "telegram-allowFrom.json"),
JSON.stringify(
{
version: 1,
allowFrom: ["1001"],
},
null,
2,
) + "\n",
"utf8",
);
await fs.writeFile(
path.join(oauthDir, "telegram-default-allowFrom.json"),
JSON.stringify(
{
version: 1,
allowFrom: ["1002"],
},
null,
2,
) + "\n",
"utf8",
);
const scoped = await readChannelAllowFromStore("telegram", process.env, "default");
expect(scoped).toEqual(["1002", "1001"]);
});
});
});

View File

@@ -234,6 +234,31 @@ function normalizeAllowFromInput(channel: PairingChannel, entry: string | number
return normalizeAllowEntry(channel, normalizeId(entry));
}
function dedupePreserveOrder(entries: string[]): string[] {
const seen = new Set<string>();
const out: string[] = [];
for (const entry of entries) {
const normalized = String(entry).trim();
if (!normalized || seen.has(normalized)) {
continue;
}
seen.add(normalized);
out.push(normalized);
}
return out;
}
async function readAllowFromStateForPath(
channel: PairingChannel,
filePath: string,
): Promise<string[]> {
const { value } = await readJsonFile<AllowFromStore>(filePath, {
version: 1,
allowFrom: [],
});
return normalizeAllowFromList(channel, value);
}
async function readAllowFromState(params: {
channel: PairingChannel;
entry: string | number;
@@ -291,12 +316,19 @@ export async function readChannelAllowFromStore(
env: NodeJS.ProcessEnv = process.env,
accountId?: string,
): Promise<string[]> {
const filePath = resolveAllowFromPath(channel, env, accountId);
const { value } = await readJsonFile<AllowFromStore>(filePath, {
version: 1,
allowFrom: [],
});
return normalizeAllowFromList(channel, value);
const normalizedAccountId = accountId?.trim().toLowerCase() ?? "";
if (!normalizedAccountId) {
const filePath = resolveAllowFromPath(channel, env);
return await readAllowFromStateForPath(channel, filePath);
}
const scopedPath = resolveAllowFromPath(channel, env, accountId);
const scopedEntries = await readAllowFromStateForPath(channel, scopedPath);
// Backward compatibility: legacy channel-level allowFrom store was unscoped.
// Keep honoring it alongside account-scoped files to prevent re-pair prompts after upgrades.
const legacyPath = resolveAllowFromPath(channel, env);
const legacyEntries = await readAllowFromStateForPath(channel, legacyPath);
return dedupePreserveOrder([...scopedEntries, ...legacyEntries]);
}
export async function addChannelAllowFromStoreEntry(params: {