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); 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 () => { it("no-ops when nothing detected", async () => {
const root = await makeTempRoot(); const root = await makeTempRoot();
const cfg: OpenClawConfig = {}; const cfg: OpenClawConfig = {};

View File

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

View File

@@ -55,6 +55,11 @@ export type LegacyStateDetection = {
targetDir: string; targetDir: string;
hasLegacy: boolean; hasLegacy: boolean;
}; };
pairingAllowFrom: {
legacyTelegramPath: string;
targetTelegramPath: string;
hasLegacyTelegram: boolean;
};
preview: string[]; preview: string[];
}; };
@@ -612,6 +617,13 @@ export async function detectLegacyStateMigrations(params: {
const hasLegacyWhatsAppAuth = const hasLegacyWhatsAppAuth =
fileExists(path.join(oauthDir, "creds.json")) && fileExists(path.join(oauthDir, "creds.json")) &&
!fileExists(path.join(targetWhatsAppAuthDir, "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[] = []; const preview: string[] = [];
if (hasLegacySessions) { if (hasLegacySessions) {
@@ -626,6 +638,11 @@ export async function detectLegacyStateMigrations(params: {
if (hasLegacyWhatsAppAuth) { if (hasLegacyWhatsAppAuth) {
preview.push(`- WhatsApp auth: ${oauthDir}${targetWhatsAppAuthDir} (keep oauth.json)`); preview.push(`- WhatsApp auth: ${oauthDir}${targetWhatsAppAuthDir} (keep oauth.json)`);
} }
if (hasLegacyTelegramAllowFrom) {
preview.push(
`- Telegram pairing allowFrom: ${legacyTelegramAllowFromPath}${targetTelegramAllowFromPath}`,
);
}
return { return {
targetAgentId, targetAgentId,
@@ -651,6 +668,11 @@ export async function detectLegacyStateMigrations(params: {
targetDir: targetWhatsAppAuthDir, targetDir: targetWhatsAppAuthDir,
hasLegacy: hasLegacyWhatsAppAuth, hasLegacy: hasLegacyWhatsAppAuth,
}, },
pairingAllowFrom: {
legacyTelegramPath: legacyTelegramAllowFromPath,
targetTelegramPath: targetTelegramAllowFromPath,
hasLegacyTelegram: hasLegacyTelegramAllowFrom,
},
preview, preview,
}; };
} }
@@ -867,6 +889,28 @@ async function migrateLegacyWhatsAppAuth(
return { changes, warnings }; 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: { export async function runLegacyStateMigrations(params: {
detected: LegacyStateDetection; detected: LegacyStateDetection;
now?: () => number; now?: () => number;
@@ -876,9 +920,20 @@ export async function runLegacyStateMigrations(params: {
const sessions = await migrateLegacySessions(detected, now); const sessions = await migrateLegacySessions(detected, now);
const agentDir = await migrateLegacyAgentDir(detected, now); const agentDir = await migrateLegacyAgentDir(detected, now);
const whatsappAuth = await migrateLegacyWhatsAppAuth(detected); const whatsappAuth = await migrateLegacyWhatsAppAuth(detected);
const telegramPairingAllowFrom = await migrateLegacyTelegramPairingAllowFrom(detected);
return { return {
changes: [...sessions.changes, ...agentDir.changes, ...whatsappAuth.changes], changes: [
warnings: [...sessions.warnings, ...agentDir.warnings, ...whatsappAuth.warnings], ...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"); 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)); 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: { async function readAllowFromState(params: {
channel: PairingChannel; channel: PairingChannel;
entry: string | number; entry: string | number;
@@ -291,12 +316,19 @@ export async function readChannelAllowFromStore(
env: NodeJS.ProcessEnv = process.env, env: NodeJS.ProcessEnv = process.env,
accountId?: string, accountId?: string,
): Promise<string[]> { ): Promise<string[]> {
const filePath = resolveAllowFromPath(channel, env, accountId); const normalizedAccountId = accountId?.trim().toLowerCase() ?? "";
const { value } = await readJsonFile<AllowFromStore>(filePath, { if (!normalizedAccountId) {
version: 1, const filePath = resolveAllowFromPath(channel, env);
allowFrom: [], return await readAllowFromStateForPath(channel, filePath);
}); }
return normalizeAllowFromList(channel, value);
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: { export async function addChannelAllowFromStoreEntry(params: {