fix: complete v2.4.0 security isolation

This commit is contained in:
hicocos committed 2026-09-01 13:52:10 +08:00
1 parent 81b482ce1e
commit 20fdcd433f
7 files changed
+108 -162

No files matched your search

+36 -85
View File
@@ -4431,17 +4431,6 @@ async function buildConsolidatedStatus(singleFiles, batches) {
}
return lines.join("\n");
}
function buildCleanupNotice(deletedCount, freedSpace) {
return [
`\u{1F9F9} **\u7CFB\u7EDF\u542F\u52A8\u6E05\u7406\u5B8C\u6210**`,
``,
`\u{1F4CA} \u6E05\u7406\u7EDF\u8BA1\uFF1A`,
` \u5220\u9664\u5B64\u513F\u6587\u4EF6: ${deletedCount} \u4E2A`,
` \u91CA\u653E\u7A7A\u95F4: ${freedSpace}`,
``,
`\u{1F4A1} \u8FD9\u4E9B\u662F\u4E4B\u524D\u4E0A\u4F20\u5931\u8D25\u6B8B\u7559\u7684\u6587\u4EF6`
].join("\n");
}
// src/services/telegramCommands.ts
init_telegramState();
@@ -4892,9 +4881,6 @@ async function initializeTelegramUserClientPool(credentials) {
currentCredentials = credentials;
await telegramUserClientPool.initialize(credentials);
}
async function reloadTelegramUserClientPool() {
if (currentCredentials) await telegramUserClientPool.initialize(currentCredentials);
}
async function listTelegramUserAccounts() {
const accounts = await telegramAccountRepository.listAccounts();
const ready = new Set(telegramUserClientPool.getReadyAccountIds());
@@ -4904,11 +4890,14 @@ async function listTelegramUserAccounts() {
activeConnections: telegramUserClientPool.getActiveConnections(account.id)
}));
}
async function upsertTelegramUserAccount(input) {
async function upsertTelegramUserAccountWithoutRuntimeRefresh(input) {
const account = await telegramAccountRepository.upsertAccount(input);
await reloadTelegramUserClientPool();
const { session: _session, ...publicAccount } = account;
return { ...publicAccount, connected: telegramUserClientPool.getReadyAccountIds().includes(account.id), activeConnections: 0 };
return {
...publicAccount,
connected: telegramUserClientPool.getReadyAccountIds().includes(account.id),
activeConnections: telegramUserClientPool.getActiveConnections(account.id)
};
}
async function deleteTelegramUserAccount(accountId) {
await telegramUserClientPool.expireAccount(accountId);
@@ -6317,7 +6306,7 @@ var TelegramUserWebLoginFlows = class {
const sent = await client2.sendCode(credentials, phone);
const flowId = crypto10.randomBytes(24).toString("base64url");
const expiresAt = this.now() + this.ttlMs;
this.flows.set(flowId, { id: flowId, owner, phone, phoneCodeHash: sent.phoneCodeHash, expiresAt, errors: 0, step: "code", client: client2 });
this.flows.set(flowId, { id: flowId, owner, phone, phoneCodeHash: sent.phoneCodeHash, expiresAt, errors: 0, step: "code", client: client2, credentials });
return { flowId, delivery: sent.isCodeViaApp ? "app" : "sms", expiresAt: new Date(expiresAt).toISOString() };
} catch (error) {
await this.closeClient(client2);
@@ -6365,7 +6354,7 @@ var TelegramUserWebLoginFlows = class {
async complete(flow) {
try {
const account = normalizeAccount2(await flow.client.getMe());
await this.deps.persistAndActivate(flow.client.saveSession(), account);
await this.deps.persistAndActivate(flow.client.saveSession(), account, flow.credentials);
this.flows.delete(flow.id);
return { step: "complete", account };
} finally {
@@ -6576,14 +6565,14 @@ async function activateTelegramUserAccount(accountId) {
await telegramUserClientPool.activateAccount(accountId, "explicit_enable", credentials);
if (!telegramUserClientPool.getAccountClient(accountId)) throw new Error("Telegram \u7528\u6237\u8D26\u53F7\u8FDE\u63A5\u5931\u8D25");
}
async function persistAndActivate(session, account) {
async function persistAndActivate(session, account, credentials) {
await setSettings([
[TELEGRAM_USER_SESSION_SETTING, session],
[TELEGRAM_USER_ENABLED_SETTING, "true"],
[TELEGRAM_USER_ID_SETTING, account.userId],
[TELEGRAM_USER_USERNAME_SETTING, account.username || ""]
]);
await upsertTelegramUserAccount({
const persisted = await upsertTelegramUserAccountWithoutRuntimeRefresh({
telegramUserId: account.userId,
username: account.username,
displayName: account.displayName,
@@ -6591,7 +6580,8 @@ async function persistAndActivate(session, account) {
enabled: true,
isLegacy: true
});
await initTelegramUserClient();
await telegramUserClientPool.activateAccount(persisted.id, "login_complete", credentials);
if (!telegramUserClientPool.getAccountClient(persisted.id)) throw new Error("Telegram \u7528\u6237\u8D26\u53F7\u8FDE\u63A5\u5931\u8D25");
}
var GramJsWebLoginClient = class {
constructor(client2, credentials) {
@@ -17369,7 +17359,6 @@ async function handleBotHomeCallback(update, data) {
\u8BF7\u6309\u63D0\u793A\u8F93\u5165\uFF0C\u6216\u8FD4\u56DE\u201C\u66F4\u591A\u529F\u80FD\u201D\u9009\u62E9\u5176\u5B83\u5165\u53E3\u3002` : "\u8FD9\u4E2A\u5165\u53E3\u6682\u65F6\u4E0D\u53EF\u7528\u3002"
});
}
var SESSION_FILE = process.env.TELEGRAM_SESSION_FILE || "./data/telegram_session.txt";
var client = null;
var digestTimer = null;
var botLifecycle = Promise.resolve();
@@ -18570,10 +18559,6 @@ async function initTelegramBot(credentialsOverride) {
console.error("\u{1F916} Telegram Bot \u540C\u6B65\u5B58\u50A8\u914D\u7F6E\u5931\u8D25:", e);
}
try {
const sessionDir = path16.dirname(SESSION_FILE);
if (!fs13.existsSync(sessionDir)) {
fs13.mkdirSync(sessionDir, { recursive: true, mode: 448 });
}
client = new TelegramClient8(new StringSession5(""), apiId, apiHash, {
connectionRetries: 5,
reconnectRetries: 5,
@@ -18588,35 +18573,7 @@ async function initTelegramBot(credentialsOverride) {
console.log("\u{1F916} Telegram Bot \u6B63\u5728\u542F\u52A8...");
await client.start({ botAuthToken: botToken });
await client.getMe();
const newSession = client.session.save();
fs13.writeFileSync(SESSION_FILE, newSession, { mode: 384 });
try {
fs13.chmodSync(SESSION_FILE, 384);
} catch (e) {
console.warn("\u{1F916} \u4FEE\u6B63 Telegram Bot session \u6587\u4EF6\u6743\u9650\u5931\u8D25:", e);
}
console.log("\u{1F916} Telegram Bot \u5DF2\u8FDE\u63A5!");
setYtDlpNotifier(async (task, text) => {
if (!client || !task.chatId) return;
const kind = /^✅/.test(text) ? "success" : /^❌|^⚠️/.test(text) ? "failure" : "success";
const userId = resolveNotificationOwnerUserId(task.ownerUserId, task.chatId);
await enqueueTelegramNotification({ userId, chatId: task.chatId, kind, message: text }, {
send: async (targetChat, message) => {
await client.sendMessage(targetChat, { message });
}
});
});
digestTimer = setInterval(() => {
if (!client) return;
void listTelegramNotificationDigestScopes().then((scopes) => Promise.all(scopes.map(
(scope) => flushTelegramNotificationDigest(scope.userId, scope.chatId, {
send: async (chatId, message) => {
await client.sendMessage(chatId, { message });
}
}).catch(() => 0)
))).catch(() => void 0);
}, 6e4);
digestTimer.unref?.();
try {
await query(`
CREATE TABLE IF NOT EXISTS telegram_auth (
@@ -18634,42 +18591,12 @@ async function initTelegramBot(credentialsOverride) {
commands: buildBotCommandMenu().map((command) => new Api9.BotCommand(command))
})), 1e4, "Telegram Bot \u547D\u4EE4\u83DC\u5355\u6CE8\u518C\u8D85\u65F6\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5");
console.log("\u{1F916} Bot \u547D\u4EE4\u83DC\u5355\u5DF2\u66F4\u65B0");
try {
const cleanupSetting = await query("SELECT value FROM system_settings WHERE key = $1", ["auto_cleanup_orphans"]);
if (cleanupSetting.rows[0]?.value !== void 0) {
process.env.AUTO_CLEANUP_ORPHANS = String(cleanupSetting.rows[0].value);
}
} catch (e) {
console.warn("\u{1F9F9} \u8BFB\u53D6\u81EA\u52A8\u6E05\u7406\u8BBE\u7F6E\u5931\u8D25\uFF0C\u4F7F\u7528\u73AF\u5883\u53D8\u91CF\u9ED8\u8BA4\u503C:", e);
}
try {
const fileConcurrency = await loadFileDownloadConcurrencySetting();
console.log(`\u{1F916} Telegram \u6587\u4EF6\u7EA7\u5E76\u53D1: ${fileConcurrency}`);
} catch (e) {
console.warn("\u{1F916} \u8BFB\u53D6\u6587\u4EF6\u7EA7\u5E76\u53D1\u8BBE\u7F6E\u5931\u8D25\uFF0C\u4F7F\u7528\u73AF\u5883\u53D8\u91CF\u9ED8\u8BA4\u503C:", e);
}
if (isAutoCleanupEnabled()) {
try {
const stats = await cleanupOrphanFiles();
if (stats.deletedCount > 0) {
console.log(`\u{1F9F9} \u542F\u52A8\u6E05\u7406: \u5220\u9664\u4E86 ${stats.deletedCount} \u4E2A\u5B64\u513F\u6587\u4EF6\uFF0C\u91CA\u653E ${stats.freedSpace}`);
const allowedUsers = await getConfiguredTelegramAllowedUsers();
const recipients = allowedUsers.length > 0 ? (await reconcileTelegramAllowedUsers(allowedUsers)).recipients : [];
for (const userId of recipients) {
try {
await client.sendMessage(userId, {
message: buildCleanupNotice(stats.deletedCount, stats.freedSpace)
});
} catch (e) {
}
}
}
} catch (e) {
console.error("\u{1F9F9} \u542F\u52A8\u6E05\u7406\u5931\u8D25:", e);
}
} else {
console.log("\u{1F9F9} \u542F\u52A8\u5B64\u513F\u6E05\u7406\u5DF2\u8DF3\u8FC7\uFF1AAUTO_CLEANUP_ORPHANS=false");
}
client.addEventHandler(async (event) => {
if (!client) return;
try {
@@ -19314,8 +19241,32 @@ ${buildPathPreviewLine(appliedPath.folder)}
displayName: [me?.firstName, me?.lastName].filter(Boolean).join(" ") || null
});
markTelegramBotReady();
setYtDlpNotifier(async (task, text) => {
if (!client || !task.chatId) return;
const kind = /^✅/.test(text) ? "success" : /^❌|^⚠️/.test(text) ? "failure" : "success";
const userId = resolveNotificationOwnerUserId(task.ownerUserId, task.chatId);
await enqueueTelegramNotification({ userId, chatId: task.chatId, kind, message: text }, {
send: async (targetChat, message) => {
await client.sendMessage(targetChat, { message });
}
});
});
digestTimer = setInterval(() => {
if (!client) return;
void listTelegramNotificationDigestScopes().then((scopes) => Promise.all(scopes.map(
(scope) => flushTelegramNotificationDigest(scope.userId, scope.chatId, {
send: async (chatId, message) => {
await client.sendMessage(chatId, { message });
}
}).catch(() => 0)
))).catch(() => void 0);
}, 6e4);
digestTimer.unref?.();
console.log("\u{1F916} Telegram Bot \u542F\u52A8\u6210\u529F! (\u6700\u5927 2GB\uFF0C\u8D26\u53F7\u7EA7\u4E0B\u8F7D\u5668\u4E0D\u53D7\u6B64\u9650\u5236)");
} catch (error) {
if (digestTimer) clearInterval(digestTimer);
digestTimer = null;
setYtDlpNotifier(null);
const failedClient = client;
client = null;
if (failedClient) {
+21 -69
View File
@@ -31,8 +31,7 @@ import {
retryTelegramBackgroundJob,
TELEGRAM_COMMENTS_MAX_PER_POST,
} from './telegramChannelJobs.js';
import { cleanupOrphanFiles, isAutoCleanupEnabled } from './orphanCleanup.js';
import { MSG, buildStartPrompt, buildAuthSuccess, build2FASetupCaption, buildCleanupNotice } from '../utils/telegramMessages.js';
import { MSG, buildStartPrompt, buildAuthSuccess, build2FASetupCaption } from '../utils/telegramMessages.js';
import { query } from '../db/index.js';
import { getConfiguredTelegramAllowedUsers, addTelegramAllowedUser, countAuthenticatedTelegramUsers, shouldAutoAllowFirstTelegramUser, verifyTelegramPin } from '../utils/authSettings.js';
import { assertPublicHttpUrl } from '../utils/networkSecurity.js';
@@ -161,9 +160,6 @@ async function handleBotHomeCallback(update: Api.UpdateBotCallbackQuery, data: s
});
}
// Session File Path
const SESSION_FILE = process.env.TELEGRAM_SESSION_FILE || './data/telegram_session.txt';
// GramJS Client
let client: TelegramClient | null = null;
let digestTimer: NodeJS.Timeout | null = null;
@@ -1541,11 +1537,6 @@ export async function initTelegramBot(credentialsOverride?: TelegramBotCredentia
}
try {
const sessionDir = path.dirname(SESSION_FILE);
if (!fs.existsSync(sessionDir)) {
fs.mkdirSync(sessionDir, { recursive: true, mode: 0o700 });
}
client = new TelegramClient(new StringSession(''), apiId, apiHash, {
connectionRetries: 5,
reconnectRetries: 5,
@@ -1562,28 +1553,7 @@ export async function initTelegramBot(credentialsOverride?: TelegramBotCredentia
await client.start({ botAuthToken: botToken });
await client.getMe();
const newSession = client.session.save() as unknown as string;
fs.writeFileSync(SESSION_FILE, newSession, { mode: 0o600 });
try { fs.chmodSync(SESSION_FILE, 0o600); } catch (e) { console.warn('🤖 修正 Telegram Bot session 文件权限失败:', e); }
console.log('🤖 Telegram Bot 已连接!');
setYtDlpNotifier(async (task, text) => {
if (!client || !task.chatId) return;
const kind = /^✅/.test(text) ? 'success' : /^❌|^⚠️/.test(text) ? 'failure' : 'success';
const userId = resolveNotificationOwnerUserId(task.ownerUserId, task.chatId);
await enqueueTelegramNotification({ userId, chatId: task.chatId, kind, message: text }, {
send: async (targetChat, message) => { await client!.sendMessage(targetChat, { message }); },
});
});
digestTimer = setInterval(() => {
if (!client) return;
void listTelegramNotificationDigestScopes().then(scopes => Promise.all(scopes.map(scope =>
flushTelegramNotificationDigest(scope.userId, scope.chatId, {
send: async (chatId, message) => { await client!.sendMessage(chatId, { message }); },
}).catch(() => 0),
))).catch(() => undefined);
}, 60_000);
digestTimer.unref?.();
// Ensure database table exists
try {
@@ -1606,15 +1576,6 @@ export async function initTelegramBot(credentialsOverride?: TelegramBotCredentia
})), 10_000, 'Telegram Bot 命令菜单注册超时,请稍后重试');
console.log('🤖 Bot 命令菜单已更新');
try {
const cleanupSetting = await query('SELECT value FROM system_settings WHERE key = $1', ['auto_cleanup_orphans']);
if (cleanupSetting.rows[0]?.value !== undefined) {
process.env.AUTO_CLEANUP_ORPHANS = String(cleanupSetting.rows[0].value);
}
} catch (e) {
console.warn('🧹 读取自动清理设置失败,使用环境变量默认值:', e);
}
try {
const fileConcurrency = await loadFileDownloadConcurrencySetting();
console.log(`🤖 Telegram 文件级并发: ${fileConcurrency}`);
@@ -1622,35 +1583,6 @@ export async function initTelegramBot(credentialsOverride?: TelegramBotCredentia
console.warn('🤖 读取文件级并发设置失败,使用环境变量默认值:', e);
}
// 启动时清理孤儿文件(默认开启,可通过 /cleanup_settings 关闭)
if (isAutoCleanupEnabled()) {
try {
const stats = await cleanupOrphanFiles();
if (stats.deletedCount > 0) {
console.log(`🧹 启动清理: 删除了 ${stats.deletedCount} 个孤儿文件,释放 ${stats.freedSpace}`);
// 广播收件人始终与当前 allowlist 收敛。
const allowedUsers = await getConfiguredTelegramAllowedUsers();
const recipients = allowedUsers.length > 0
? (await reconcileTelegramAllowedUsers(allowedUsers)).recipients
: [];
for (const userId of recipients) {
try {
await client.sendMessage(userId, {
message: buildCleanupNotice(stats.deletedCount, stats.freedSpace)
});
} catch (e) {
// 用户可能已删除对话或阻止了 Bot
}
}
}
} catch (e) {
console.error('🧹 启动清理失败:', e);
}
} else {
console.log('🧹 启动孤儿清理已跳过:AUTO_CLEANUP_ORPHANS=false');
}
// 后台任务由应用级编排显式启动;Bot 凭证绑定只安装交互处理器,
// 避免仅保存凭证就触发订阅扫描、任务恢复或主动消息。
@@ -2403,9 +2335,29 @@ export async function initTelegramBot(credentialsOverride?: TelegramBotCredentia
displayName: [me?.firstName, me?.lastName].filter(Boolean).join(' ') || null,
});
markTelegramBotReady();
setYtDlpNotifier(async (task, text) => {
if (!client || !task.chatId) return;
const kind = /^✅/.test(text) ? 'success' : /^❌|^⚠️/.test(text) ? 'failure' : 'success';
const userId = resolveNotificationOwnerUserId(task.ownerUserId, task.chatId);
await enqueueTelegramNotification({ userId, chatId: task.chatId, kind, message: text }, {
send: async (targetChat, message) => { await client!.sendMessage(targetChat, { message }); },
});
});
digestTimer = setInterval(() => {
if (!client) return;
void listTelegramNotificationDigestScopes().then(scopes => Promise.all(scopes.map(scope =>
flushTelegramNotificationDigest(scope.userId, scope.chatId, {
send: async (chatId, message) => { await client!.sendMessage(chatId, { message }); },
}).catch(() => 0),
))).catch(() => undefined);
}, 60_000);
digestTimer.unref?.();
console.log('🤖 Telegram Bot 启动成功! (最大 2GB,账号级下载器不受此限制)');
} catch (error) {
if (digestTimer) clearInterval(digestTimer);
digestTimer = null;
setYtDlpNotifier(null);
const failedClient = client;
client = null;
if (failedClient) {
@@ -46,7 +46,8 @@ test('Bot migrate, disable and delete routes remain isolated from Telegram user-
test('Bot replacement avoids stale saved sessions and requires full activation before ready', () => {
assert.doesNotMatch(bot, /fs\.readFileSync\(SESSION_FILE/);
const startupSection = bot.slice(bot.indexOf("console.log('🤖 Telegram Bot 正在启动...')"), bot.indexOf('const newSession = client.session.save()'));
assert.doesNotMatch(bot, /session\.save\(\)[\s\S]*writeFileSync\(SESSION_FILE/);
const startupSection = bot.slice(bot.indexOf("console.log('🤖 Telegram Bot 正在启动...')"), bot.indexOf("console.log('🤖 Telegram Bot 已连接!')"));
assert.doesNotMatch(startupSection, /Promise\.race|withTelegramClientDeadline/);
assert.match(bot, /new TelegramClient\(new StringSession\(''\)/);
assert.match(bot, /await client\.start[\s\S]*SetBotCommands[\s\S]*addEventHandler[\s\S]*markTelegramBotReady\(\)/);
@@ -55,6 +56,20 @@ test('Bot replacement avoids stale saved sessions and requires full activation b
assert.match(bot, /catch \(error\) \{[\s\S]*const failedClient = client;[\s\S]*client = null;[\s\S]*failedClient\.disconnect/);
});
test('Bot startup defers global notifier and timer effects until ready and rolls them back on failure', () => {
const startup = bot.slice(bot.indexOf('export async function initTelegramBot'), bot.indexOf('async function withTelegramClientDeadline'));
const readyIndex = startup.indexOf('markTelegramBotReady()');
assert.ok(readyIndex >= 0);
assert.ok(startup.indexOf('setYtDlpNotifier(', readyIndex) > readyIndex);
assert.ok(startup.indexOf('digestTimer = setInterval(', readyIndex) > readyIndex);
assert.match(startup, /catch \(error\) \{[\s\S]*clearInterval\(digestTimer\)[\s\S]*setYtDlpNotifier\(null\)/);
});
test('Bot startup never performs orphan cleanup or sends cleanup notifications', () => {
const startup = bot.slice(bot.indexOf('export async function initTelegramBot'), bot.indexOf('async function withTelegramClientDeadline'));
assert.doesNotMatch(startup, /cleanupOrphanFiles\(|buildCleanupNotice|启动清理/);
});
test('client teardown waits for in-flight subscription and recovery work', () => {
assert.match(jobs, /while \(subscriptionScanRunning \|\| recoveryRunning\)/);
assert.match(jobs, /拒绝切换 Bot 客户端/);
+9 -4
View File
@@ -11,7 +11,7 @@ import {
deleteTelegramUserAccount,
listTelegramUserAccounts,
telegramUserClientPool,
upsertTelegramUserAccount,
upsertTelegramUserAccountWithoutRuntimeRefresh,
} from './telegramUserClientPool.js';
import { initializeTelegramMultiAccountRuntime } from './telegramMultiAccountRuntime.js';
@@ -142,14 +142,18 @@ export async function activateTelegramUserAccount(accountId: string): Promise<vo
if (!telegramUserClientPool.getAccountClient(accountId)) throw new Error('Telegram 用户账号连接失败');
}
async function persistAndActivate(session: string, account: TelegramUserLoginAccount): Promise<void> {
async function persistAndActivate(
session: string,
account: TelegramUserLoginAccount,
credentials: { apiId: number; apiHash: string },
): Promise<void> {
await setSettings([
[TELEGRAM_USER_SESSION_SETTING, session],
[TELEGRAM_USER_ENABLED_SETTING, 'true'],
[TELEGRAM_USER_ID_SETTING, account.userId],
[TELEGRAM_USER_USERNAME_SETTING, account.username || ''],
]);
await upsertTelegramUserAccount({
const persisted = await upsertTelegramUserAccountWithoutRuntimeRefresh({
telegramUserId: account.userId,
username: account.username,
displayName: account.displayName,
@@ -157,7 +161,8 @@ async function persistAndActivate(session: string, account: TelegramUserLoginAcc
enabled: true,
isLegacy: true,
});
await initTelegramUserClient();
await telegramUserClientPool.activateAccount(persisted.id, 'login_complete', credentials);
if (!telegramUserClientPool.getAccountClient(persisted.id)) throw new Error('Telegram 用户账号连接失败');
}
class GramJsWebLoginClient implements TelegramUserLoginClient {
@@ -307,6 +307,18 @@ export async function upsertTelegramUserAccount(input: Parameters<TelegramAccoun
return { ...publicAccount, connected: telegramUserClientPool.getReadyAccountIds().includes(account.id), activeConnections: 0 };
}
export async function upsertTelegramUserAccountWithoutRuntimeRefresh(
input: Parameters<TelegramAccountRepository['upsertAccount']>[0],
): Promise<PublicTelegramUserAccount> {
const account = await telegramAccountRepository.upsertAccount(input);
const { session: _session, ...publicAccount } = account;
return {
...publicAccount,
connected: telegramUserClientPool.getReadyAccountIds().includes(account.id),
activeConnections: telegramUserClientPool.getActiveConnections(account.id),
};
}
export async function setTelegramUserAccountEnabled(accountId: string, enabled: boolean): Promise<boolean> {
const changed = await telegramAccountRepository.setEnabled(accountId, enabled);
if (changed) await reloadTelegramUserClientPool();
+4 -3
View File
@@ -26,6 +26,7 @@ interface Flow<C> {
errors: number;
step: 'code' | 'password';
client: C;
credentials: { apiId: number; apiHash: string };
}
export type TelegramUserLoginFlowErrorCode =
@@ -64,7 +65,7 @@ export class TelegramUserWebLoginFlows<C extends TelegramUserLoginClient> {
constructor(private readonly deps: {
credentials(): Promise<{ apiId: number; apiHash: string } | null>;
createClient(credentials: { apiId: number; apiHash: string }): C;
persistAndActivate(session: string, account: TelegramUserLoginAccount): Promise<void>;
persistAndActivate(session: string, account: TelegramUserLoginAccount, credentials: { apiId: number; apiHash: string }): Promise<void>;
now?: () => number;
ttlMs?: number;
maxErrors?: number;
@@ -86,7 +87,7 @@ export class TelegramUserWebLoginFlows<C extends TelegramUserLoginClient> {
const sent = await client.sendCode(credentials, phone);
const flowId = crypto.randomBytes(24).toString('base64url');
const expiresAt = this.now() + this.ttlMs;
this.flows.set(flowId, { id: flowId, owner, phone, phoneCodeHash: sent.phoneCodeHash, expiresAt, errors: 0, step: 'code', client });
this.flows.set(flowId, { id: flowId, owner, phone, phoneCodeHash: sent.phoneCodeHash, expiresAt, errors: 0, step: 'code', client, credentials });
return { flowId, delivery: sent.isCodeViaApp ? 'app' : 'sms', expiresAt: new Date(expiresAt).toISOString() };
} catch (error) {
await this.closeClient(client);
@@ -138,7 +139,7 @@ export class TelegramUserWebLoginFlows<C extends TelegramUserLoginClient> {
private async complete(flow: Flow<C>): Promise<{ step: 'complete'; account: TelegramUserLoginAccount }> {
try {
const account = normalizeAccount(await flow.client.getMe());
await this.deps.persistAndActivate(flow.client.saveSession(), account);
await this.deps.persistAndActivate(flow.client.saveSession(), account, flow.credentials);
this.flows.delete(flow.id);
return { step: 'complete', account };
} finally {
@@ -39,6 +39,16 @@ test('explicit legacy enable requires a saved user session before initializing t
assert.match(runtimeSource, /installTelegramMultiAccountRuntimeAdapters[\s\S]*initializeTelegramUserClientPool/);
});
test('legacy login completion activates only the just-authorized account', () => {
const persist = client.slice(
client.indexOf('async function persistAndActivate'),
client.indexOf('class GramJsWebLoginClient'),
);
assert.match(persist, /activateAccount\(persisted\.id, 'login_complete', credentials\)/);
assert.match(persist, /upsertTelegramUserAccountWithoutRuntimeRefresh/);
assert.doesNotMatch(persist, /initTelegramUserClient\(|initializeTelegramMultiAccountRuntime\(|reloadTelegramUserClientPool/);
});
test('disable retains the session, unlink deletes it, and both apply without a restart', () => {
assert.match(route, /router\.post\('\/config\/telegram-user\/disable'/);
assert.match(route, /disableTelegramUserAccount/);