'use strict'; const fs=require('fs'),http=require('http'),crypto=require('crypto'),{createRequire}=require('module'); const r=createRequire('/usr/local/lib/node_modules/n8n/package.json'),{Pool}=r('pg'); const {Collaboration,safeEqual,requireThat}=require('./core.cjs'); const secret=name=>fs.readFileSync('/run/secrets/'+name,'utf8').trim(); const apiKey=secret('collaboration_key'),botToken=secret('telegram_token'),renderToken=secret('render_token'); const db=new Pool({host:process.env.PGHOST||'postgres',port:5432,database:process.env.PGDATABASE||'n8n',user:'bfl_collaboration',password:secret('database_password'),max:8,statement_timeout:15000}); const methods=new Set(['sendMessage','sendPhoto','editMessageText','editMessageReplyMarkup','answerCallbackQuery']); async function telegram(method,input){ requireThat(methods.has(method),'telegram_method_forbidden');const body={...input};const asset=body._asset;delete body._asset;delete body._timeout_ms;let data,headers; if(asset&&!body.photo){try{ requireThat(/^\/v1\/assets\/[a-f0-9]{64}\.png$/.test(asset),'invalid_asset_path'); const rr=await fetch('http://bfl-cover-renderer:8090'+asset,{headers:{Authorization:'Bearer '+renderToken},signal:AbortSignal.timeout(20000)});requireThat(rr.ok,'asset_read_failed'); const chunks=[];let total=0;for await(const chunk of rr.body){total+=chunk.length;requireThat(total<=10000000,'asset_size_invalid');chunks.push(chunk);}const bytes=Buffer.concat(chunks);requireThat(crypto.createHash('sha256').update(bytes).digest('hex')===asset.slice(11,-4),'asset_hash_mismatch'); data=new FormData();for(const [key,value] of Object.entries(body))if(value!==undefined&&value!==null)data.append(key,typeof value==='object'?JSON.stringify(value):String(value));data.append('photo',new Blob([bytes],{type:'image/png'}),'cover.png'); }catch{return {ok:false,error_code:503,not_sent:true,description:'Saved image unavailable before Telegram request'};}}else{data=JSON.stringify(body);headers={'content-type':'application/json'};} const response=await fetch('https://api.telegram.org/bot'+botToken+'/'+method,{method:'POST',body:data,headers,signal:AbortSignal.timeout(method==='answerCallbackQuery'?8000:25000)});const json=await response.json(); // Do not return provider diagnostics that might include URLs, request bodies or secrets. if(json.ok!==true)return {ok:false,error_code:Number(json.error_code||response.status),description:json.description==='Bad Request: message is not modified'?json.description:'Telegram request rejected',...(json.parameters?.retry_after?{parameters:{retry_after:Number(json.parameters.retry_after)}}:{})};return json; } const app=new Collaboration({db,owner:process.env.BFL_OWNER_ID,channel:process.env.BFL_CHANNEL_ID,secret:apiKey,transport:telegram}); let ready=false; const server=http.createServer(async(req,res)=>{ res.setHeader('Content-Type','application/json; charset=utf-8');res.setHeader('Cache-Control','no-store'); if(req.method==='GET'&&req.url==='/healthz'){res.statusCode=ready?200:503;res.end(JSON.stringify({ok:ready,service:'bfl-collaboration',version:'1.0.1'}));return;} if(req.method!=='POST'||!safeEqual(req.headers['x-bfl-key']||'',apiKey)){res.statusCode=401;res.end('{"error":"unauthorized"}');return;} try{ let chunks=[],bytes=0;for await(const c of req){bytes+=c.length;requireThat(bytes<=2*1024*1024,'payload_too_large');chunks.push(c);}const input=JSON.parse(Buffer.concat(chunks).toString('utf8'));let result; if(req.url==='/event')result=await app.event(input.update,String(input.execution_id||'')); else if(req.url==='/internal')result=await app.internal(input.context||{},String(input.execution_id||'')); else if(req.url==='/execution-context')result=await app.executionContext(input); else if(req.url==='/execution-failure')result=await app.executionFailure(input); else if(req.url==='/authorize')result=await app.authorize(input); else if(req.url==='/telegram')result=await app.telegram(input); else if(req.url==='/onboarding')result=await app.onboarding(input); else if(req.url==='/decision')result=await app.decision(input); else throw Object.assign(new Error('not_found'),{code:'not_found'}); res.end(JSON.stringify(result)); }catch(e){const safe=/^[a-z0-9_]{1,80}$/.test(e.code||'')?e.code:'internal_error';res.statusCode=/revoked|ticket|unauthorized|actor/.test(safe)?403:safe==='not_found'?404:422;res.end(JSON.stringify({error:safe}));} }); server.requestTimeout=180000;server.headersTimeout=10000;server.keepAliveTimeout=5000; db.query('SELECT count(*) FROM bfl_collab_members').then(()=>{ready=true;server.listen(8084,'0.0.0.0');process.stdout.write('bfl-collaboration 1.0.1 ready\n');}).catch(()=>{process.stderr.write('database_startup_failed\n');process.exitCode=1;db.end();}); process.on('SIGTERM',()=>{ready=false;server.close(()=>db.end().then(()=>process.exit(0)));});