diff --git a/package.json b/package.json index aa3ed97..bb8de14 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pezkuwi-telegram-miniapp", - "version": "1.0.119", + "version": "1.0.120", "type": "module", "description": "Pezkuwichain Telegram Mini App - Forum, Announcements, Rewards", "author": "Pezkuwichain Team", diff --git a/src/version.json b/src/version.json index da64dcd..501b53b 100644 --- a/src/version.json +++ b/src/version.json @@ -1,5 +1,5 @@ { - "version": "1.0.119", - "buildTime": "2026-02-06T01:34:49.512Z", - "buildNumber": 1770341689513 + "version": "1.0.120", + "buildTime": "2026-02-06T01:55:02.762Z", + "buildNumber": 1770342902762 } diff --git a/supabase/functions/create-offer-telegram/index.ts b/supabase/functions/create-offer-telegram/index.ts index 548e8d0..000cd2f 100644 --- a/supabase/functions/create-offer-telegram/index.ts +++ b/supabase/functions/create-offer-telegram/index.ts @@ -1,11 +1,21 @@ import { serve } from 'https://deno.land/std@0.177.0/http/server.ts'; import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'; +import { createHmac } from 'https://deno.land/std@0.177.0/node/crypto.ts'; -const corsHeaders = { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Headers': - 'authorization, x-client-info, apikey, content-type, x-supabase-client-platform', -}; +// CORS - Production domain only +const ALLOWED_ORIGINS = ['https://telegram.pezkuwichain.io', 'https://t.me']; + +function getCorsHeaders(origin: string | null): Record { + const allowedOrigin = + origin && ALLOWED_ORIGINS.some((o) => origin.startsWith(o)) ? origin : ALLOWED_ORIGINS[0]; + + return { + 'Access-Control-Allow-Origin': allowedOrigin, + 'Access-Control-Allow-Headers': + 'authorization, x-client-info, apikey, content-type, x-supabase-client-platform', + 'Access-Control-Allow-Methods': 'POST, OPTIONS', + }; +} interface CreateOfferRequest { sessionToken: string; @@ -18,11 +28,49 @@ interface CreateOfferRequest { minOrderAmount?: number; maxOrderAmount?: number; timeLimitMinutes?: number; - adType?: 'buy' | 'sell'; // Default: 'sell' + adType?: 'buy' | 'sell'; } -// Verify session token and get telegram_id -function verifySessionToken(token: string): number | null { +// Session token secret (derived from bot token) +function getSessionSecret(botToken: string): Uint8Array { + return createHmac('sha256', 'SessionTokenSecret').update(botToken).digest(); +} + +// Verify HMAC-signed session token +function verifySessionToken(token: string, botToken: string): number | null { + try { + const parts = token.split('.'); + if (parts.length !== 2) { + // Try legacy format for backwards compatibility + return verifyLegacyToken(token); + } + + const [payloadB64, signature] = parts; + + // Verify signature + const secret = getSessionSecret(botToken); + const expectedSig = createHmac('sha256', secret).update(payloadB64).digest('hex'); + + if (signature !== expectedSig) { + return null; + } + + // Parse payload + const payload = JSON.parse(atob(payloadB64)); + + // Check expiration + if (Date.now() > payload.exp) { + return null; + } + + return payload.tgId; + } catch { + return null; + } +} + +// Legacy token format (Base64 only) - for backwards compatibility +function verifyLegacyToken(token: string): number | null { try { const decoded = atob(token); const [telegramId, timestamp] = decoded.split(':'); @@ -38,6 +86,9 @@ function verifySessionToken(token: string): number | null { } serve(async (req) => { + const origin = req.headers.get('origin'); + const corsHeaders = getCorsHeaders(origin); + // Handle CORS if (req.method === 'OPTIONS') { return new Response('ok', { headers: corsHeaders }); @@ -59,6 +110,15 @@ serve(async (req) => { adType = 'sell', } = body; + // Get bot token for session verification + const botToken = Deno.env.get('TELEGRAM_BOT_TOKEN'); + if (!botToken) { + return new Response(JSON.stringify({ error: 'Server configuration error' }), { + status: 500, + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + }); + } + // Validate session token if (!sessionToken) { return new Response(JSON.stringify({ error: 'Missing session token' }), { @@ -67,7 +127,7 @@ serve(async (req) => { }); } - const telegramId = verifySessionToken(sessionToken); + const telegramId = verifySessionToken(sessionToken, botToken); if (!telegramId) { return new Response(JSON.stringify({ error: 'Invalid or expired session' }), { status: 401, @@ -140,7 +200,7 @@ serve(async (req) => { .from('p2p_fiat_offers') .insert({ seller_id: userId, - seller_wallet: '', // No longer needed with internal ledger + seller_wallet: '', token, amount_crypto: amountCrypto, fiat_currency: fiatCurrency, @@ -208,11 +268,12 @@ serve(async (req) => { ); } catch (error) { console.error('Error:', error); + const origin = req.headers.get('origin'); return new Response( JSON.stringify({ error: error instanceof Error ? error.message : 'Internal server error' }), { status: 500, - headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + headers: { ...getCorsHeaders(origin), 'Content-Type': 'application/json' }, } ); } diff --git a/supabase/functions/get-my-offers/index.ts b/supabase/functions/get-my-offers/index.ts index 34a5523..adb0aff 100644 --- a/supabase/functions/get-my-offers/index.ts +++ b/supabase/functions/get-my-offers/index.ts @@ -1,19 +1,67 @@ import { serve } from 'https://deno.land/std@0.177.0/http/server.ts'; import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'; +import { createHmac } from 'https://deno.land/std@0.177.0/node/crypto.ts'; -const corsHeaders = { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Headers': - 'authorization, x-client-info, apikey, content-type, x-supabase-client-platform', -}; +// CORS - Production domain only +const ALLOWED_ORIGINS = ['https://telegram.pezkuwichain.io', 'https://t.me']; + +function getCorsHeaders(origin: string | null): Record { + const allowedOrigin = + origin && ALLOWED_ORIGINS.some((o) => origin.startsWith(o)) ? origin : ALLOWED_ORIGINS[0]; + + return { + 'Access-Control-Allow-Origin': allowedOrigin, + 'Access-Control-Allow-Headers': + 'authorization, x-client-info, apikey, content-type, x-supabase-client-platform', + 'Access-Control-Allow-Methods': 'POST, OPTIONS', + }; +} interface GetMyOffersRequest { sessionToken: string; status?: string; // Optional: filter by status ('open', 'paused', etc.) } -// Verify session token and get telegram_id -function verifySessionToken(token: string): number | null { +// Session token secret (derived from bot token) +function getSessionSecret(botToken: string): Uint8Array { + return createHmac('sha256', 'SessionTokenSecret').update(botToken).digest(); +} + +// Verify HMAC-signed session token +function verifySessionToken(token: string, botToken: string): number | null { + try { + const parts = token.split('.'); + if (parts.length !== 2) { + // Try legacy format for backwards compatibility + return verifyLegacyToken(token); + } + + const [payloadB64, signature] = parts; + + // Verify signature + const secret = getSessionSecret(botToken); + const expectedSig = createHmac('sha256', secret).update(payloadB64).digest('hex'); + + if (signature !== expectedSig) { + return null; + } + + // Parse payload + const payload = JSON.parse(atob(payloadB64)); + + // Check expiration + if (Date.now() > payload.exp) { + return null; + } + + return payload.tgId; + } catch { + return null; + } +} + +// Legacy token format (Base64 only) - for backwards compatibility +function verifyLegacyToken(token: string): number | null { try { const decoded = atob(token); const [telegramId, timestamp] = decoded.split(':'); @@ -29,6 +77,9 @@ function verifySessionToken(token: string): number | null { } serve(async (req) => { + const origin = req.headers.get('origin'); + const corsHeaders = getCorsHeaders(origin); + // Handle CORS if (req.method === 'OPTIONS') { return new Response('ok', { headers: corsHeaders }); @@ -38,6 +89,15 @@ serve(async (req) => { const body: GetMyOffersRequest = await req.json(); const { sessionToken, status } = body; + // Get bot token for session verification + const botToken = Deno.env.get('TELEGRAM_BOT_TOKEN'); + if (!botToken) { + return new Response(JSON.stringify({ error: 'Server configuration error' }), { + status: 500, + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + }); + } + // Validate session token if (!sessionToken) { return new Response(JSON.stringify({ error: 'Missing session token' }), { @@ -46,7 +106,7 @@ serve(async (req) => { }); } - const telegramId = verifySessionToken(sessionToken); + const telegramId = verifySessionToken(sessionToken, botToken); if (!telegramId) { return new Response(JSON.stringify({ error: 'Invalid or expired session' }), { status: 401, @@ -113,11 +173,12 @@ serve(async (req) => { ); } catch (error) { console.error('Error:', error); + const origin = req.headers.get('origin'); return new Response( JSON.stringify({ error: error instanceof Error ? error.message : 'Internal server error' }), { status: 500, - headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + headers: { ...getCorsHeaders(origin), 'Content-Type': 'application/json' }, } ); } diff --git a/supabase/functions/process-withdraw/index.ts b/supabase/functions/process-withdraw/index.ts index 8ec0f30..0505eaf 100644 --- a/supabase/functions/process-withdraw/index.ts +++ b/supabase/functions/process-withdraw/index.ts @@ -1,16 +1,26 @@ // process-withdraw Edge Function // Processes pending withdrawal requests by sending tokens from platform wallet to user wallets // This should be called by a cron job or manually by admins +// SECURITY: This is a backend-only function - no user session needed import { serve } from 'https://deno.land/std@0.177.0/http/server.ts'; import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'; import { ApiPromise, WsProvider, Keyring } from 'npm:@pezkuwi/api@16.5.36'; import { cryptoWaitReady } from 'npm:@pezkuwi/util-crypto@14.0.25'; -const corsHeaders = { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', -}; +// CORS - Restricted for security (cron/admin only) +const ALLOWED_ORIGINS = ['https://telegram.pezkuwichain.io', 'https://supabase.com']; + +function getCorsHeaders(origin: string | null): Record { + const allowedOrigin = + origin && ALLOWED_ORIGINS.some((o) => origin.startsWith(o)) ? origin : ALLOWED_ORIGINS[0]; + + return { + 'Access-Control-Allow-Origin': allowedOrigin, + 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', + 'Access-Control-Allow-Methods': 'POST, OPTIONS', + }; +} // RPC endpoint for PezkuwiChain const RPC_ENDPOINT = 'wss://rpc.pezkuwichain.io'; @@ -41,6 +51,9 @@ interface ProcessWithdrawRequest { } serve(async (req) => { + const origin = req.headers.get('origin'); + const corsHeaders = getCorsHeaders(origin); + // Handle CORS preflight if (req.method === 'OPTIONS') { return new Response(null, { headers: corsHeaders }); @@ -286,12 +299,13 @@ serve(async (req) => { ); } catch (error) { console.error('Process withdraw error:', error); + const origin = req.headers.get('origin'); return new Response( JSON.stringify({ success: false, error: error instanceof Error ? error.message : 'Internal server error', }), - { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } + { status: 500, headers: { ...getCorsHeaders(origin), 'Content-Type': 'application/json' } } ); } }); diff --git a/supabase/functions/verify-deposit-telegram/index.ts b/supabase/functions/verify-deposit-telegram/index.ts index cc13107..ef15ffa 100644 --- a/supabase/functions/verify-deposit-telegram/index.ts +++ b/supabase/functions/verify-deposit-telegram/index.ts @@ -5,11 +5,21 @@ import { serve } from 'https://deno.land/std@0.177.0/http/server.ts'; import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'; import { ApiPromise, WsProvider } from 'npm:@pezkuwi/api@16.5.36'; +import { createHmac } from 'https://deno.land/std@0.177.0/node/crypto.ts'; -const corsHeaders = { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', -}; +// CORS - Production domain only +const ALLOWED_ORIGINS = ['https://telegram.pezkuwichain.io', 'https://t.me']; + +function getCorsHeaders(origin: string | null): Record { + const allowedOrigin = + origin && ALLOWED_ORIGINS.some((o) => origin.startsWith(o)) ? origin : ALLOWED_ORIGINS[0]; + + return { + 'Access-Control-Allow-Origin': allowedOrigin, + 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', + 'Access-Control-Allow-Methods': 'POST, OPTIONS', + }; +} // Platform hot wallet address (PRODUCTION) - Treasury_3 const PLATFORM_WALLET = '5H18ZZBU4LwPYbeEZ1JBGvibCU2edhhM8HNUtFi7GgC36CgS'; @@ -31,8 +41,46 @@ interface DepositRequest { blockNumber?: number; // Optional: for faster verification of old transactions } -// Verify session token (same logic as telegram-auth) -function verifySessionToken(token: string): number | null { +// Session token secret (derived from bot token) +function getSessionSecret(botToken: string): Uint8Array { + return createHmac('sha256', 'SessionTokenSecret').update(botToken).digest(); +} + +// Verify HMAC-signed session token +function verifySessionToken(token: string, botToken: string): number | null { + try { + const parts = token.split('.'); + if (parts.length !== 2) { + // Try legacy format for backwards compatibility + return verifyLegacyToken(token); + } + + const [payloadB64, signature] = parts; + + // Verify signature + const secret = getSessionSecret(botToken); + const expectedSig = createHmac('sha256', secret).update(payloadB64).digest('hex'); + + if (signature !== expectedSig) { + return null; + } + + // Parse payload + const payload = JSON.parse(atob(payloadB64)); + + // Check expiration + if (Date.now() > payload.exp) { + return null; + } + + return payload.tgId; + } catch { + return null; + } +} + +// Legacy token format (Base64 only) - for backwards compatibility +function verifyLegacyToken(token: string): number | null { try { const decoded = atob(token); const [telegramId, timestamp] = decoded.split(':'); @@ -243,6 +291,9 @@ async function verifyTransactionOnChain( } serve(async (req) => { + const origin = req.headers.get('origin'); + const corsHeaders = getCorsHeaders(origin); + // Handle CORS preflight if (req.method === 'OPTIONS') { return new Response(null, { headers: corsHeaders }); @@ -263,6 +314,15 @@ serve(async (req) => { } const { sessionToken, txHash, token, expectedAmount, blockNumber } = body; + // Get bot token for session verification + const botToken = Deno.env.get('TELEGRAM_BOT_TOKEN'); + if (!botToken) { + return new Response(JSON.stringify({ success: false, error: 'Server configuration error' }), { + status: 500, + headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + }); + } + // Validate session token if (!sessionToken) { return new Response(JSON.stringify({ success: false, error: 'Missing session token' }), { @@ -271,7 +331,7 @@ serve(async (req) => { }); } - const telegramId = verifySessionToken(sessionToken); + const telegramId = verifySessionToken(sessionToken, botToken); if (!telegramId) { return new Response( JSON.stringify({ success: false, error: 'Invalid or expired session token' }), @@ -500,9 +560,10 @@ serve(async (req) => { ); } catch (error) { console.error('Edge function error:', error); + const origin = req.headers.get('origin'); return new Response(JSON.stringify({ success: false, error: 'Internal server error' }), { status: 500, - headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + headers: { ...getCorsHeaders(origin), 'Content-Type': 'application/json' }, }); } });