// Node.js 18+ standard library. Server side only. v2 default; never retry purchases. import https from 'node:https'; import {createHash,createHmac,randomBytes} from 'node:crypto'; const PATHS=Object.freeze({connect:'authentication/connect',items:'commodity/items',item:'commodity/item', inventory:'commodity/inventory',inventoryState:'commodity/inventoryState',draftCard:'commodity/draftCard', trade:'commodity/trade',orderStatus:'integration212/orderStatus',quote:'integration212/quote',query:'commodity/query',query2:'commodity/query2'}); const encode=s=>encodeURIComponent(s).replace(/[!'()*]/g,c=>'%'+c.charCodeAt(0).toString(16).toUpperCase()); export function signature(params,key,path=null){ const keys=Object.keys(params).filter(k=>k!=='sign').sort(); if(params.signature_version==='2'){ const canonical=keys.map(k=>encode(k)+'='+encode(String(params[k]))).join('&'); return createHmac('sha256',key).update('POST\n'+path+'\n'+params.timestamp+'\n'+params.nonce+'\n'+canonical).digest('hex'); } const query=new URLSearchParams(keys.filter(k=>params[k]!=='').map(k=>[k,String(params[k])])).toString(); const encoded=Buffer.from((query+'&key='+key).replace(/\+/g,' '),'utf8'),decoded=[]; for(let i=0;i67108864)throw new Error('Invalid HTTPS URL, credentials or limits'); this.base=u.origin;this.appId=String(appId);this.key=key;this.version=signatureVersion;this.limit=maxResponseBytes; } static requestNo(v){if(typeof v!=='string'||v==='0'||!/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/.test(v))throw new Error('request_no must be 1..64 ASCII letters/digits/_/-, not 0');} async call(operation,fields={}){ if(!Object.hasOwn(PATHS,operation))throw new Error('Unknown operation');const p={...fields}; if(operation==='trade'){ShopClient.requestNo(p.request_no);for(const [k,v] of Object.entries({contact:'',card_id:0,device:0,password:'',race:'',from:0,coupon:''}))if(!(k in p))p[k]=v;} if(operation==='orderStatus'&&Object.hasOwn(p,'request_no'))ShopClient.requestNo(p.request_no); if(operation==='query2')ShopClient.requestNo(p.requestNo); if(operation==='inventoryState'){p.card_id??=0;p.race??='';} if(['inventory','draftCard','quote'].includes(operation))p.race??=''; for(const k of ['sign','app_key','signature_version','timestamp','nonce'])delete p[k];p.app_id=this.appId; for(const [k,v] of Object.entries(p)){ if(!/^[A-Za-z][A-Za-z0-9_]{0,63}$/.test(k)||!(typeof v==='string'||(typeof v==='number'&&Number.isSafeInteger(v))))throw new Error('Invalid field name/value; decimals/large IDs as strings');p[k]=String(v); } const path='/shared/'+PATHS[operation]; if(this.version===2)Object.assign(p,{signature_version:'2',timestamp:String(Math.floor(Date.now()/1000)),nonce:randomBytes(16).toString('hex')}); p.sign=signature(p,this.key,path);const body=new URLSearchParams(p).toString();if(Buffer.byteLength(body)>131072)throw new Error('Request body too large'); return new Promise((resolve,reject)=>{ let timer,finished=false;const finish=(error,data)=>{if(finished)return;finished=true;clearTimeout(timer);error?reject(error):resolve(data);}; const unknown=()=>new Error('Transport/size/HTTP error: payment outcome may be unknown; query original order'); const req=https.request(this.base+path,{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded','Accept':'application/json','Content-Length':Buffer.byteLength(body)}},res=>{ let size=0;const chunks=[]; res.on('aborted',()=>finish(unknown()));res.on('error',()=>finish(unknown())); res.on('data',chunk=>{size+=chunk.length;if(size>this.limit){finish(unknown());req.destroy();}else chunks.push(chunk);}); res.on('end',()=>{ if(res.statusCode<200||res.statusCode>=300)return finish(unknown()); try{const data=JSON.parse(Buffer.concat(chunks).toString('utf8'));if(!data||data.code!==200)return finish(new Error(data?.msg||'Business error'));finish(null,data.data);} catch(_){finish(unknown());} }); }); timer=setTimeout(()=>{finish(unknown());req.destroy();},30000);req.on('error',()=>finish(unknown()));req.end(body); }); } } // const api=new ShopClient(process.env.SHOP_BASE_URL,process.env.SHOP_APP_ID,process.env.SHOP_APP_KEY); // const balance=await api.call('connect'); // No purchase. // Persist request_no first; query original order after any timeout. // const result=await api.call('orderStatus',{request_no:'1001-UUID'}); // Deliver secret only when state==='delivered' && result_available===true.