fix: bind Telegram pairing to expiring single-use account challenges

This commit is contained in:
2026-09-06 14:50:11 +03:00
parent aca1f5c3d1
commit 283bfe5990
9 changed files with 137 additions and 23 deletions
+7
View File
@@ -287,3 +287,10 @@ test('weekly brief review is admin-only and persists evidence without sending me
assert.equal(saved.review.actor,'weekly-admin@example.test');assert.equal(saved.providerId,input.providerId);
assert.equal(saved.state,'sent_manual');
});
test('Telegram cannot be linked through generic data writes or legacy client codes',async()=>{
assert.equal((await request('/api/me/data','PUT',{anahtar:'telegram',deger:{chatId:'123'}})).status,403);
assert.equal((await request('/api/me/telegram','POST',{kod:'123456'})).status,400);
assert.equal((await request('/api/me/telegram','PATCH',{})).status,405);
assert.equal((await request('/api/me/telegram','POST',{islem:'kod'},null)).status,401);
});
+56
View File
@@ -0,0 +1,56 @@
import {test,after} from 'node:test';
import assert from 'node:assert/strict';
import {mkdtempSync,rmSync} from 'node:fs';
import {join} from 'node:path';
import {tmpdir} from 'node:os';
import {createTelegramLinker} from '../server/telegram-link.mjs';
import {kodIleBagla} from '../server/telegram.mjs';
const directory=mkdtempSync(join(tmpdir(),'telegram-link-'));
process.env.IHALEIST_USER_DATA_DIR=directory;
const store=await import('../server/user-store.mjs');
after(()=>rmSync(directory,{recursive:true,force:true}));
const found={chatId:'123',ad:'Test'};
test('server challenge belongs to its account, expires, rotates and is consumed once',async()=>{
let time=1000000;const linker=createTelegramLinker(store,()=>time),email='link@example.test';
const first=linker.issue(email);assert.match(first.kod,/^[a-f0-9]{48}$/);
assert.ok(!JSON.stringify(store.getData(email,'telegram')).includes(first.kod));
await assert.rejects(linker.link('other@example.test',first.kod,async()=>found),{status:400});
const second=linker.issue(email);assert.notEqual(second.kod,first.kod);
await assert.rejects(linker.link(email,first.kod,async()=>found),{status:400});
time=second.expiresAt;
await assert.rejects(linker.link(email,second.kod,async()=>found),{status:400});
const current=linker.issue(email);
assert.deepEqual(await linker.link(email,current.kod,async()=>found),found);
await assert.rejects(linker.link(email,current.kod,async()=>found),{status:400});
assert.equal(store.getData(email,'telegram').chatId,'123');
assert.throws(()=>linker.issue(email),{status:409});
});
test('disconnect or expiration during provider lookup cannot restore a removed link',async()=>{
let time=1000000;const linker=createTelegramLinker(store,()=>time),email='disconnect@example.test';
const challenge=linker.issue(email);
await assert.rejects(linker.link(email,challenge.kod,async()=>{store.setData(email,'telegram',null);return found;}),{status:400});
assert.equal(store.getData(email,'telegram'),null);
const next=linker.issue(email);
await assert.rejects(linker.link(email,next.kod,async()=>{time=next.expiresAt;return found;}),{status:400});
});
test('matching accepts only a fresh exact code from a private non-bot sender',async()=>{
const oldFetch=globalThis.fetch,oldToken=process.env.TELEGRAM_BOT_TOKEN;
process.env.TELEGRAM_BOT_TOKEN='synthetic-token';
const code='a'.repeat(48),window={issuedAt:1000000,expiresAt:1600000,now:1100000};
const good={text:code,date:1050,chat:{id:123,type:'private',first_name:'QA'},from:{id:123,is_bot:false}};
let updates=[];
globalThis.fetch=async()=>({ok:true,json:async()=>({ok:true,result:updates})});
try {
for(const message of [{...good,text:'prefix '+code},{...good,text:code+' suffix'},{...good,date:999},
{...good,date:1101},{...good,chat:{id:123,type:'group'}},{...good,from:{id:123,is_bot:true}},
{...good,from:{id:999,is_bot:false}}]) {
updates=[{message}];assert.equal(await kodIleBagla(code,window),null);
}
updates=[{edited_message:good}];assert.equal(await kodIleBagla(code,window),null);
updates=[{message:good}];assert.equal((await kodIleBagla(code,window)).chatId,'123');
updates=[{message:{...good,text:'/start '+code}}];assert.equal((await kodIleBagla(code,window)).ad,'QA');
} finally {globalThis.fetch=oldFetch;if(oldToken===undefined)delete process.env.TELEGRAM_BOT_TOKEN;else process.env.TELEGRAM_BOT_TOKEN=oldToken;}
});