import {directoryFor,DirectoryBusy} from './directory.mjs'; import {callRpc,relayRpc,rpcReady,allowRpcRequest} from './rpc.mjs'; import {ASSETS, SOURCE_FINGERPRINT, RELEASE_FINGERPRINT, PUBLIC_CONFIG} from './generated-assets.mjs'; import {sha256,validateCoin,validateSocials,detectImage,ENCODER,DEFAULT_CLUSTER} from './shared.mjs'; import {createUploadLimiter,claimUploadQuota} from './upload-limits.mjs'; import {assetEncoding} from './asset-encoding.mjs'; const CSP="default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self' blob: data:; connect-src 'self' https:; font-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'self'"; const headers={'Strict-Transport-Security':'max-age=31536000','Content-Security-Policy':CSP,'X-Content-Type-Options':'nosniff','Referrer-Policy':'no-referrer','Permissions-Policy':'camera=(), microphone=(), geolocation=()','X-Solcurity-Release':RELEASE_FINGERPRINT,'Cross-Origin-Opener-Policy':'same-origin-allow-popups'}; const json=(value,status=200,extra={})=>new Response(JSON.stringify(value),{status,headers:{...headers,'Content-Type':'application/json; charset=utf-8','Cache-Control':'no-store',...extra}}); const allowUploadAttempt=createUploadLimiter(); function retiredHost(hostname){return hostname==='sshsol.fun'||hostname==='www.sshsol.fun'||(hostname.startsWith('safe-solana-hub.')&&hostname.endsWith('.chatgpt.site'))||hostname==='phpsol.tech'||hostname==='www.phpsol.tech'||(hostname.startsWith('patch-launch.')&&hostname.endsWith('.chatgpt.site'));} function configuredOrigin(env){try{const u=new URL(env.PUBLIC_ORIGIN||PUBLIC_CONFIG.publicOrigin);return u.protocol==='https:'&&!retiredHost(u.hostname)&&!u.username&&!u.password&&u.pathname==='/'?u.origin:null;}catch{return null;}} async function readLimited(request,limit){const reader=request.body?.getReader();if(!reader)throw new Error('Request body is required.');let size=0;const chunks=[];while(true){const {done,value}=await reader.read();if(done)break;size+=value.length;if(size>limit){await reader.cancel();throw new Error('Request body is too large.');}chunks.push(value);}const bytes=new Uint8Array(size);let offset=0;for(const chunk of chunks){bytes.set(chunk,offset);offset+=chunk.length;}return JSON.parse(new TextDecoder().decode(bytes));} export async function publishMetadata(request,env){ const origin=new URL(request.url).origin; if(request.headers.get('Origin')!==origin)return json({error:'Upload origin does not match this application.'},403); if(!request.headers.get('Content-Type')?.startsWith('application/json'))return json({error:'JSON upload required.'},415); const publicOrigin=configuredOrigin(env); if(!env.BUCKET||!publicOrigin)return json({error:'Public metadata storage is not configured.'},503); const identity=request.headers.get('CF-Connecting-IP')||'unknown'; if(!allowUploadAttempt(identity))return json({error:'Too many upload attempts. Please retry in a minute.'},429,{'Retry-After':'60'}); const {payload}=await readLimited(request,1500000); if(!payload||typeof payload!=='object'||Array.isArray(payload))throw new Error('Invalid metadata payload.'); const details={...validateCoin(payload),...validateSocials(payload)}; if(typeof payload.imageBase64!=='string'||payload.imageBase64.length>1400000)throw new Error('Invalid image.'); const bytes=Uint8Array.from(atob(payload.imageBase64),c=>c.charCodeAt(0)),image=detectImage(bytes); if(image.type!==payload.imageType)throw new Error('Image MIME type does not match its contents.'); const imageKey=`media/${await sha256(bytes)}.${image.ext}`; const metadata=JSON.stringify({...details,image:`${publicOrigin}/${imageKey}`,show_name:true}); const metadataKey=`metadata/${await sha256(metadata)}.json`,uri=`${publicOrigin}/${metadataKey}`; if(ENCODER.encode(uri).length>200)throw new Error('Public origin is too long for Pump.fun metadata.'); if(await env.BUCKET.head(metadataKey))return json({uri,digest:metadataKey.split('/')[1].slice(0,-5)}); const quota=await claimUploadQuota(env.BUCKET,identity); if(!quota.allowed)return json({error:quota.message},429,{'Retry-After':String(quota.retryAfter)}); await env.BUCKET.put(imageKey,bytes,{httpMetadata:{contentType:image.type},onlyIf:{etagDoesNotMatch:'*'}}); await env.BUCKET.put(metadataKey,metadata,{httpMetadata:{contentType:'application/json'},onlyIf:{etagDoesNotMatch:'*'}}); return json({uri,digest:metadataKey.split('/')[1].slice(0,-5)}); } export default {async fetch(request,env={}){ const url=new URL(request.url),path=url.pathname; if(retiredHost(url.hostname))return new Response(request.method==='HEAD'?null:'Gone', {status:410,headers:{...headers,'Content-Type':'text/plain; charset=utf-8','Cache-Control':'no-store'}}); try{ const publicOrigin=configuredOrigin(env); if(publicOrigin){ const canonical=new URL(publicOrigin); if(url.hostname.endsWith('.chatgpt.site')||url.hostname==='www.'+canonical.hostname||(url.hostname===canonical.hostname&&url.protocol!=='https:')){ const target=new URL(publicOrigin);target.pathname=url.pathname;target.search=url.search; return new Response(null,{status:308,headers:{...headers,'Location':target.href,'Cache-Control':'no-store'}}); } } if(path==='/api/config'&&request.method==='GET')return json({publicMetadataReady:!!env.BUCKET&&!!configuredOrigin(env),mainnetRpcReady:rpcReady(env),defaultCluster:DEFAULT_CLUSTER,publicOrigin:configuredOrigin(env),sourceFingerprint:SOURCE_FINGERPRINT,releaseFingerprint:RELEASE_FINGERPRINT}); if(path.startsWith('/api/rpc/')){ if(request.method!=='POST')return json({error:'POST required.'},405,{'Allow':'POST'}); if(request.headers.get('Origin')!==url.origin)return json({error:'RPC origin does not match this application.'},403); if(!request.headers.get('Content-Type')?.startsWith('application/json'))return json({error:'JSON RPC required.'},415); const cluster=path.slice('/api/rpc/'.length);if(!rpcReady(env,cluster))return json({error:'The launch connection is not configured.'},503); if(!allowRpcRequest(request.headers.get('CF-Connecting-IP')))return json({error:'Please wait before making more connection requests.'},429,{'Retry-After':'60'}); const payload=await readLimited(request,12000); try{return json(await relayRpc(env,cluster,payload));}catch(error){return json({jsonrpc:'2.0',id:payload?.id??null,error:{code:-32603,message:error.message}},400);} } if(path==='/api/metadata'&&request.method==='POST')return await publishMetadata(request,env); if(path==='/api/launches'&&request.method==='GET'){ try{return json(await directoryFor(env,configuredOrigin(env),(cluster,method,params)=>callRpc(env,cluster,method,params)).list(url.searchParams.get('cluster')||DEFAULT_CLUSTER,request.headers.get('CF-Connecting-IP')));} catch(error){if(error instanceof DirectoryBusy)return json({error:error.message},429,{'Retry-After':String(error.retryAfter)});return json({error:'The launch directory is temporarily unavailable.'},503);} } if(path==='/api/launches'&&request.method==='POST'){ if(request.headers.get('Origin')!==url.origin)return json({error:'Directory origin does not match this application.'},403); if(!request.headers.get('Content-Type')?.startsWith('application/json'))return json({error:'JSON receipt required.'},415); return json(await directoryFor(env,configuredOrigin(env),(cluster,method,params)=>callRpc(env,cluster,method,params)).register(await readLimited(request,2000),request.headers.get('CF-Connecting-IP'))); } if(request.method!=='GET'&&request.method!=='HEAD')return json({error:'Method not allowed.'},405,{'Allow':'GET, HEAD'}); if(/^\/(media\/[a-f0-9]{64}\.(png|jpg|webp)|metadata\/[a-f0-9]{64}\.json)$/.test(path)){ const object=await env.BUCKET?.get(path.slice(1));if(!object)return json({error:'File not found.'},404); const type=path.startsWith('/metadata/')?'application/json':path.endsWith('.png')?'image/png':path.endsWith('.jpg')?'image/jpeg':'image/webp'; return new Response(request.method==='HEAD'?null:object.body,{headers:{...headers,'Content-Type':type,'Cache-Control':'public, max-age=31536000, immutable','Access-Control-Allow-Origin':'*','ETag':object.httpEtag||'','Content-Disposition':'inline'}}); } const assetPath=['/','/launch','/verify'].includes(path)?'/index.html':path; const asset=ASSETS[assetPath]; if(!asset)return json({error:'Not found.'},404); const encoding=asset.compressed?assetEncoding(request.cf?.clientAcceptEncoding??request.headers.get('Accept-Encoding')):null; if(encoding==='unacceptable')return new Response(null,{status:406,headers:{...headers,'Vary':'Accept-Encoding'}}); const etag=`"${asset.hash}${encoding?'-'+encoding:''}"`,assetHeaders={...headers,'Content-Type':asset.type,'Cache-Control':'no-cache, no-transform','ETag':etag,...(asset.compressed?{'Vary':'Accept-Encoding'}:{}),...(encoding?{'Content-Encoding':encoding}:{}),...(asset.download?{'Content-Disposition':`attachment; filename="${asset.download}"`}:{})}; if(request.headers.get('If-None-Match')?.split(',').some(value=>value.trim().replace(/^W\//,'')===etag||value.trim()==='*'))return new Response(null,{status:304,headers:assetHeaders}); const body=request.method==='HEAD'?null:encoding||asset.encoding==='base64'?Uint8Array.from(atob(encoding?asset.compressed[encoding]:asset.body),character=>character.charCodeAt(0)):asset.body; return new Response(body,{headers:assetHeaders,...(encoding?{encodeBody:'manual'}:{})}); }catch(error){if(error instanceof DirectoryBusy)return json({error:error.message},429,{'Retry-After':String(error.retryAfter)});return json({error:error instanceof Error?error.message:'Request failed.'},400);} }};