From da4c3290eea9e461c6c7e9ac990edc78e07ff521 Mon Sep 17 00:00:00 2001 From: ToryMic Date: Tue, 29 Sep 2026 23:10:31 -0400 Subject: [PATCH 1/5] fix(backend): repair broken merge artifacts blocking build and tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit main is currently unbuildable and its test suite is red, so every back-end CI job fails before it reaches the code under review. The breakage all traces back to two bad merges (9a15975c, 7300a48a) that truncated a Prisma model block, dropped a closing brace, renamed an imported limiter and replaced the idempotency replay cache. Prisma schema - close `WalletTenantAssociation` and separate it from `IdempotencyKey` (a missing `}` made `prisma generate` fail, so `@prisma/client` stayed an uninitialised stub and every suite crashed on import) - add `SessionAuditLog`, `Transaction.deletedAt` and `WebhookEndpoint.tenantId`, all of which sessionAudit.ts and the tenant boundary guard already query - add the matching migration and refresh the committed SQLite dev.db Type errors - import `readsLimiter` in vaultEndpoints.ts (used by GET /receipts) - restore the idempotency replay cache as idempotencyStore.ts and re-export it from idempotency.ts, so transferOrchestrator.ts, vaultEndpoints.ts, index.ts and idempotencyRetention.ts resolve - fix the unterminated `if` in diffSchemaShapes and a possibly-undefined `baseline.properties` read in apiContractSnapshots.ts - build the OTel resource through whichever factory the installed @opentelemetry/resources major exposes (v1 `new Resource()`, v2 `resourceFromAttributes()`) - `ZodObject._shape` → `.shape` for Zod 4 - return the strategy-switch 200 response, order receipts by `timestamp`, serialise session metadata, and type the request mocks in src/tests Error contract - restore `summary`, `errors` and 404 `path` on the error envelope Tests - issues711 "newly added required fields" mutated the live schema instead of the baseline snapshot, so it asserted against a scenario that can never produce the expected diff --- backend/prisma/dev.db | Bin 827392 -> 1052672 bytes .../migration.sql | 38 ++ backend/prisma/schema.prisma | 34 ++ backend/schema-snapshots/get-_health.json | 2 +- backend/schema-snapshots/get-_ready.json | 2 +- backend/src/__tests__/issues711.test.ts | 13 +- backend/src/apiContractSnapshots.ts | 6 +- backend/src/idempotency.ts | 16 + backend/src/idempotencyStore.ts | 360 ++++++++++++++++++ backend/src/index.ts | 1 + backend/src/middleware/apiError.ts | 16 + backend/src/middleware/validate.ts | 2 + backend/src/schemaSnapshot.ts | 2 +- backend/src/sessionAudit.ts | 2 +- backend/src/swagger.ts | 1 - backend/src/tests/idempotency.test.ts | 9 +- backend/src/tests/tenantBoundary.test.ts | 4 +- backend/src/tracing.ts | 26 +- backend/src/vaultEndpoints.ts | 8 +- 19 files changed, 518 insertions(+), 24 deletions(-) create mode 100644 backend/prisma/migrations/20260929120000_add_session_audit_and_tenant_scopes/migration.sql create mode 100644 backend/src/idempotencyStore.ts diff --git a/backend/prisma/dev.db b/backend/prisma/dev.db index 4816f30799b05807212a9cc4eb74f4fcca47805e..a319596bdb09c10ad9b07c84f539bce86bb94bc3 100644 GIT binary patch literal 1052672 zcmeFa4PYC`d7w=YBte1zRJk%Uz{c7xX`w#Ef8pX zBpz>T>-l(_9d`b;b+@&(eGvY2z`xoD;@0{=8Ut6}*a>1`=A*XomzZxeFEL+dzRWz& ze4hC<^GW98%tu2#9bXFnM28vtLa-2cCMdQ2>ssmFs`zKNhWdv029|eaawc6IGG{Zn zp?Nb?9?egsjUpvwY7Ujt8M8j>p5gPUp@~#{-t zmQdf`y@6-0DbxGCzxVxI?#L&^vRAZ9yec?EK5*afQpGZAE5kE5q|0YLA<7dy0 zoE|!VF@7p_@jyj}80Abp*C1Doo2BLHd}A8bxB@9p`=3=Rlk$9NIB(JfAel&=NS${T zSjjYB^go*vGNV z*VS8^tNXdNbXTZP*%Mgqb7ap`ix!I{Yr{($_SXzu@*dn$t@gM~6io!o(61+Z=BwbSpaH}TqGzfMRX}$;IRTHhWyGq*qPewz1NBRTH+Z-D`Tg)5abEuiR0=^|(UbG#Q-9YLiVpTBJ zo}fO`b5_MT>!YEiRFZ`T5{y6tfy^rnv|cWqrREeZj?=m3{G+SpUw2{)6b0o<)mv{6^^q^jCRXxq)pt<41}0h>fsxOZi(s)ME&8o0V_yR*)N0w= zpw?qT{SWna^^Ntn>H+F`q`CUvk>y8voHlpugag_!vElh*DPQyi!}W+gF?D=b^+>fY zjH@=7EGd;oYu41Vv$myNjeW_4`(N7~>Pv17EC-#|G6@YkO{%>Jcyar8PgVc=o~u!^ zVwLt$Xb#mBpgqhnp}swP0@o{}qM9qVdr5D2Q#~SY)cP6ud2OfH^tG7$LeA|k{hiWA z9kCSZ>KobJ%A!0jH_dr|bT5<6P~Wayf$NnjQO&H% z`7+54fwwHT7^FD@MH`V>uEZWzWHfAZ+ zro1|_>z4wdK8_1Kn|8{VE)+ARS(2X3OcjYe{4X^hX|FGPvp}7-*q0j(AE$@*TyMC3 zv^GMSrp8QZel}5mo?pjqWpbI)G&R!%3}WTeFywJdleX*C74%Cmo1X&9Yx>xtRqcxT zEObR_oftFTTpO~4(MA#M)GJ{pT9a~%>^2c&zF2ey5VfI7H9Jx$WHXRZsZ0wc7{WWF zpSarvPM#SbIdLX+tT{n8+&|pj)%O_JwS?z3h1 zx|oKq)F^y$Dtz6y7ryS@VqcpFm~X>(+y7y{9sT)eN92ckf4ld-aJlEtd!+6k4}CQ> z-1Re^U+p~B@s8j(f~VX6TYFdFa@%j&Cw#9z%lq#QPcBD0^KEUNJr*zUoTdm#l}db4 zGz=}NX_B6#G9@N&Xoh4-x*-}8M+HM9MC2`EnYv5_RpM34;7rqGiKO$2q6nO32#Th1 zhDjwNii#x?QRmpejTerOM0u8%S&f%i=VSMK=_|I|O_OXkZ#eBMU8Xq#<3=k(vu4RT zt$TS}pQG(5TU*)S3FSdisB&)Kiv7Esw0kh1WbJ7XTu)QDz7@ zDOxOLpoxH=cTRZ2^8H&KJrdYrS)h>c+NsSmv5+Lx3rf>!*ZJC;6fiebb zh?XR(no10rsDRBR8Z~)cG)==Ysm99|u+|jY5D04$PL`=A%1*Y=`sjH%ucuA0EEt`a z%e3goy?%M$-Hv)0TRl!wEzRJyq(NmfDS{D#W`dFABw)eng33|e5LrzX4UX60ugHl4 zaN-Fs86p)lFh|0vKnoN%!L|)a;Vg^gi6Sbp2;DP782 zWzZLtt&n_b`BhsSRjIA2pb}L#Op+8hSxt(pZbFT#QqtliR$vuV;&juH0gq@=Q#TY! zO;FZS6hk#QO$03sK^39tSgb`2POvCzw>il$R762+2Cb1{q zY@Lf43e5)WcX2s>m!s%KTTzwhx}ZpAk~82-U z$XdE4TO!z=1g)4=1x_+0RaZ5cCDb%kky8X{M4+>&ahhf*Y(>$K&$X;0n&jKzp|bCQb)zWwKf4#LRMnarAw{))$OWWF^*2@_;5OQWpG@!V5`I z6WCZp(Ey^V$k3b(k=Hm~B1GW`SgT~Q6dE>_WS!G3lhb5P0QY1nVBi`YP)(D!pqWv{ z$#%M;FUx!ME!m@2dr)N1bSXy)rRjXR0@YgH7ju+!T0N`j0`w_tQd3oM?7E^Sb<5(C zhRm^G0vab#;7SarR058Q(m!bUr_mE*aBww&E#_&YO^axz z#U}-#OJJ$IoYVkEQU^nX-UT{69q_0exK3S#f^e3~Q%RHs3ywoq39di|$HW?dR^Uv_ zZa)Ug>y{`{NsPjM;slU2Co1bO*fUKA{x-E zo1&0}dKTe}XCcavz#O3(2`fmt%s~wbRJC*oJg)|AjS^Xf?p@hCoJFYuPF#fU55NIKOXi3Lj)-HSRaxM{6hnZqhA3)+?d8D`Adcg}Z^1y`c7>N* z6<3;3O~os;n6bd&+il1Zo>;y&?C3aRS2ZtFs4=zSGg;8k7~SQrgevUPRj zT#R6SnQR(d(|pCZo?qVGpAVLe~k}4an<;pKSuiXB>DEYO%OaE`=oD-wlfttv_Aa#*`oB zE)cpx)&iTbJ3pC)*1^gO)Ho={1Abd8Lj%JWH5ObtYeG+I8AjEsAF5~xBLok?N~5-i z9JTuyj*5fJ@z9Ejrb0CqMvX~>GeA$tQj>%ipuJG&^aPPt4IZ4KYO#hUfEJd?QUcz@ zW@f-Z6NVk26(Cih1#tq^USt}~SjVAlFD^FZI70v zi7sCsmSnatYsJ6sytx78d`c6nQeqoH%{AVTkK z$f5z4HX3LsS|mxp8NzTKXuwdMaL^>6k-}J4*MJptho%4nNtRXY(KvMaP?K<1OoGu4 z2Z~Z?D!Ksv$R7JDDiAg*E#{!3Ay^J4Lm$L>t{sZBkR?lwZUf8r2OZsIVG)C-bVKTjgd840=mTaM3X9sCQs_Tl)$+Nmsz$RXHhx zTZKyoiA}0d*+~vupP+$zQB;oOO}JKs#?8V&N7JD_vkH*qps$o<8EEkqg?bfbOMpQ- zYY_v+mf&1rG^s;u#SiWW^+Z;H3SKe5%4L@a-K&?|0*-FmY{P>Q6)STva$q^d?o4=a ziGrdeP2J*E7*|s$JFCJd4F+j2BD5rql{hdj)gq7?G-d#*Lf6Mya7m^UOR=xfM9YM$ zM!WBUk%Us|NBY?`%&(N~+xLHcZy3Js>-|()^n;O)MSm*#<=7PSZ0zC4OUx6@dn4zg zCu3WqJ7aHUdLwPjbCIW_bFt6&ek$_L*!v>=jQG92{>S{001`j~NB{{S0VIF~kN^^J z2!7%9s_n6@B4P4a1oCTGfiD{S?GbItOweGV8iD1GU4_xCKiVPFMf@sfK z$}kbk!Nmp$!BwQW%@$nGJ+^yo!S&qU>azvcbFb+hTW~!$d$!tw>$&Z6w=KAyn;u(i z!S&p9xXTt?&(!~BTW~!y@0)DF^-Os)w%~fEonual>zMS7+Jftud5zeD>zV)Tbp+Qj z2^qEp*E1#9V+*clR<7F?T+iHC$jN#gGgMtp*6Wxi>a+#dGh5SP3$ADCBxnn+XA+}* zZ*X#5GYV1LU2HM3Qq`jQV z4*U8)zvg+JYS^A z0-4HGmT5@}?1|$ek55dVPn{h)F>+>TVr1;hV?(2(sfo&B3oX83X$5wGWfwB#X|qUR zow>k`zl+{DVB}{D_OfVZIc{gXeU*%PZDSe3G9-|(Sy{Uqay|BzUvzU_*tu45d3A%i zs!hdx>Z+%>53E^SWrG_nU&(s@2D1cd#l>z7bmktytEPs}pD&N0m zVWkabE7cYDOYxH{gJav2Dn#C11n6Xe_+_&uU6nER2#pO4c zE8kSy58b^|+~U2};qS8{|YNM$QdZ~IR_0=C`S88^?*Uodbq2mU9RFjMP z>W6oFiW*+4s2lW9^$zN*{}Np(YN@AM)RwliLI2d`p1vmhbIm;!yVok{2E9|gbNbrl zzgsEjd}ysY-Jox3a!p@*$De!Zv}>)RZqPH;JEpHa_YqG~JJ%}e2K`c#TY7={jg_Jn zI;ur&X-XUQN_9T?h2!t6Ii+WU(NkA9yX|zrOjB6!&oBfY*1pv&So#vyrVwG_NrSMI zKXLRpe_`y>__@*J{LxG2ACu3F97|1H;75fK`NYY`FHNS7kB_9rQ=_S&6GL$P_{E8_ zlVc}Gq`FJJ7mokjr`?OLUgKk`n0m;4e11)}OMT_`Lslv7@nlu~_sUqyIbl z{^-v$7o$HCEk+-YJ{)~88jidW`DEm`B0tAG5_u!Dmx;w*jD0%x!PtLg-h3Vdm31As^ULdeYx~5R`m?OyZk__8+ zz{<}$tQD-Wu#`586JX(V3s#}#VZRk!8@2^u)oIu^NYIm#37xm78#3(u04p=YZWFpr zsRq5de$*C(Me^&iJss#%lbGp4d4wuvf0ak(6WfgXtu~b;)`(Z~AHp$V5U@v5E0Up@IBMG~uSg?i| z?4bc`EyJcJ)?r(aim*yMtnd%f_UdA=(lG2EA;2jL3%jnsis8DV9kK;sS$WtI!sL>m z14Og-`uG&q0GD75GJCaeSfosT$QBf#LR1wtOyObia>yDMS|_Fq(y+ujECLTSsWNB_ z!d?`Hy>2?x9&Az~!YawO44Z`QUU(g}g`IL(%@$NxQ-(#SVJ90{ei}Fmc3s03EU@G} zY@=dt3M8wlEy%+v-URlxNWvB&us%NQb&-Uf5lE61sl6IHES(G*+k&v^g=$bmg*_8Q zdv$OX7FmZ4MF0XU5-q}(I4W#=U<<0SKMU;R12&`DrG|Qj?TGAQoyEh}IJ^d%BxsV8 zHKDK!JZo>e0LjAQ&Vs#RguOyFQDN&JNCj4kw*@6Zhn2+beRyDF0jO&zKkO6(8=#1= zZvbo~WD=eeoU93zRaidW-oFPpsw%9g&zrFLn*m!z$c~yUZwqoFh1Bi+hh*JeaT=@_ z)_j+eil7J-7KVqa;W%3m_AB6Eenvvv3x@NoEl7BK4Q$w{ zL9#@s9*)A08d_Tt7MC+&%{f>_obU%79Sv&0isZ01od)Y%i?9z9RH4BpVbyRbGz-h+ zOX`3vNMYG`P+m^LWdW2LHq1!sPzOmcM~T&8F?K~ENkaf?12DJy9R>6XOumW9DwegrHv_)7DAKDgul`RPSAORdpNjgi6 zi@L?z{0Uhmaaa04^8mv^V8L$Zp z4|`7}wgr`d@4dcVEn;{r-YQ1j*(yePpjC{#qg9M_f2$aAd#f1XzE&~(wpKCRy{%%{ zz7{c@c2BDqb!)2_zch!DD3MDU$0BDjv-!Bc@X=0cqH|JE~GQCa_Q9TN)n^*^rv zcPj^(m=+R10!RP}AOR$R1dsp{Kmter2_S(xj{vU!f9KT+YJ>!k01`j~NB{{S0VIF~ zkN^@u0!ZLi6Ts{LTTK+xLjp(u2_OL^fCP{L5RB?iDG(400|%gB!C2v01`j~NB{{S0VIF~?mPl` z{eS1x32KA{kN^@u0!RP}AOR$R1dsp{KmthMRuiz_{|_)vwZUKfAOR$R1dsp{Kmter z2_OL^fCP{L5ebH96GxBp7sf7)pBp{SAH8(`G5O5MvDCx`epDEd zPn>-G(q!uR_(*CzHJTbaF$BktUz`{_Id*d7#L!Xc(#Yw@9zT2O=+TMci{~zm43CJT zk3RYse_T3yPPinTdi2qw!ua?_Sw1RCr-u1cFV63mJ?bQ99Z9A@ZPNc>shL5rQ ziDMi4*caLF>4uV?UP+9rB_{5GJjObQ&p!I->64dOxSSt8mO6go^T9UwiytI_ z1dsp{Kmter2_OL^fCP{L5RCpRxE0U1dsp{Kmter2_OL^ zfCP{L5Y8=lXwxuMjxb|Lx2d+L*s*zQ8=Uk}8If01`j~NB{{S0VIF~kN^@u z0!RP}Ab~rdKqR;$Pi@&LR`(AL()eBLh5cbIQ8FEL+dzRWz& ze7@^I=Um5c1mD$uD$sG~m-gl~yml-WOYPk%wQpaJCbwVP-$~}pOgYUdsv>ieq==Hp zrP=b-lcgzvCE09#A)6_c2Z(8&9UGrGNX*$x?x1@dew%H7FrS;rjbvlmXU^%6eSL z3gr`)YvLUm&#&f2oB(4T-$n3YG*IpBirA}=ft76>V zj8PEN@=~TOndag`raWyH2^3=>o0-j&2l7{Fv6wOGz*Lds${w&Ql1WFhbg(AsQZ`sg z)C5V<1VNEC%@T>F5JjPu$_k2VnIe@rQ<7Aoa-5)ugl9R0s=6exgtbJ4TR-J>&@EC@ z%IAPxnU>1&ny$c=E*3ISTSH}tvtQgvvTBl-CRlb*;s+&hKoW#YcI`A&NQG|KwQwYs z8rsTL=q~SSrprlbUKJM>$&-aylF5~6j^qqFP|i3Z~o)5+8a$B+#*z9Y;Iy9%1WAMN`h6GnYoJL+IH>b4_r>hYT&Tvo%YsxAmz`ZC9v^BFn&a#dFikrxTo zWla(VQ5H;&RZOa>L>Fa?a;gGNK_i@`s6-NY&LW)2iL%PVc_txJOK($A{m`=7vzqFH zq-p7STDU@KmVLRZ6G=74&aGx?`#2*3;G2v!l^s1PQP&F6CzOg>uQ!-OSDn6pDER+^eU} zI2L1q5hcOG8~yG%+Y0NK~`6TNHO7QJOalN*%8bzbuk* zni|`}nY?uWwXRS!rEGnyg5#NH8oZR2pPW;qtXXhr==0f`w3#Uxr0DJVnvvLo$*SP( z=Tf9|^WbTzIh>!*fo}x>)x5oje7)*R_guTDE0*ft`b5<(_B4?YWi72;p|h9yT-ha2 z1aGpms#SpXBuOEY6b?GFx1U1B?ycQi!E=JLXUHrV3z$mApy#Otea|#RIJRPs8T|Wy zr@_4VnQNiWSW4JBUIi0s2BQh-?2~0(T@+?rFpi0=O~)~i?-H0s^Wg1H208<~T#o1}=vQfFxNs?@alm<)+{VuI*Ro-}7I_Uj5$hlUeiJ}a{eZ??Y zOM`1HQ!+%jAd{%6Tb8a^M3gLDqoyDe(IT2o1yiAtsK|s$oNj8Z#urV)^#cim-m~jlQ}MsZ^6S^0^!}%1QfLqk<^$1i1M8d=UmNb{3^FTpPbM z245c=g0KH6!Pf`(!q@xngRkF-+0Xx9==f3_^Eu{sn1`63VXiW-W$uf8Gxl8UH)H=# zY(6#-6JmEozaIT~^u5uyL}#O;(f!eInjt4v?;5#VI#o97m?VqtDx zn)DUm;i8wjlAD>6a}&M-oRVIg6L>L`lgE7pST?;hH774E=$Fs?2}mlJCR4?-o>@}Q z`3gu{+RBw@v^mu{>nk8ae`-!IW#=wGIp!-MNNHtBQS#G^#u*<0mX{Q`BII<|niEd@ z3W!2_dX6v7!?ez5vw$pWGM{Fr*qOOvxp>M~Korxt1(D7#WVlCt1$Z{iTKbbybEVvf zuK>%Zg(nvl7w35Pq@RE)$mx>87X`s2C;S9tNlt5-jGznoE64o=V1_Nt&lJq;lro=c z7LY`dgNcOFl1S&W#bbT~yuzoI{9NX;Rn83i3P@}^XR$@;NliWKDu108p!vu#mTAb=6luX49MmW2Gr#D82%soEC~#O{FYPp3LM0Ujao(OSH65;Hb#^3P9_o!rYTHg}DXJSAb*F zPx6+Qy|Toz%>poQ2Ua~b&r78zXT*cP0t%PDGPk&-5mO!T5#Uu`OqY!L!d$tWP5KEa znhdpBw3PXI^?;86aAMP?1wCIDbBp_Z1XvAADg%;jlwX1AY!C~E1_lwO?UO)lXlz$;ohJG~&~=B3$PJ_3ry zO6lntrC3lFXxvAD=fGE&uVjj>D$VR{7Jv+F9kNUFvxNoq0k1&imYJ%lf|{23dANj@ z7I*jy2x(&QhOF_~`+WtVW98;Exw*_0e!IVbm@ezuVuoni|tvGn8;YYDR4=PSUf>A9@NU8ZE|9zOvdOm2Qw&dil&l&#GI ziVC;((j|#5%^LE|-M#{l#jM5}a%qm;;v=BQvY1xQ#f+6JP2c4!0DboqujG`%mE2|@ z0a22q^c4&GUQ;zT`3gXFPg(ksIF&OPe}R=dq%mIs&%M8>uYl+FRm4xgdrzm=Pr!TU zBLUt`_{GXZa7-Uzb+MP>?S1kN$XVRnFJ z_AvJ`4ATMg17D5(HOvuwEcUxFPwr>(ND+Ptc=}!JNWSREBwlcyuewEqo{Pk1)UR znaH2O9K&x%eg)E`hT6K>|ns2_OL^fCP{L5R^t2oN@Mtag zp$AX7!Mh%<1%K~bBX02dlWy?mPt<}R{QKjz;P3vMR4w?^UpeLm^h;HMvUgMWP34SwWMEx7cJhuq*F4OWAx)Uf6TdDRW>RovjcaxJ+3%aR-X zanTJvEx5rM-VL7S+~5G~2Dct`gIxo)AhSJL3&y^4pcd?&-d_ube&^M0@Mrto;M?}O z!7F>K!DFAAdzBm1_qf4FA9RDZ(WlJsCHFTF7 z9Nb(DvftXi$qnAaxWQ=54R%Fq!EgR!#0`G1*9{&D*McwoP>&nDzuOJo6RHJY{7jb{ ze5un7zR=+YpAWjh=h}OMojvwL+b{gIbNwHBu`Twi(dB49`b6Z-;lBz0Y4~@;m%=B* za*q%_8GcWASM+VYU+DR@*iXfVBE{bK^xW6|yRoIn`R;Eq>E7MlD)S?~hr7=+@9drI zzRWz@8|&W9ys!6i_cP2=?=#*1H}glmKi>TdjL1B|bVpu_d@lTro}cM?V|O(A#poYL z|9kYEk%uA=Mxwnhg~L7n(DUh@55>M1yA=6A?}vLb-G9M+F&YWI7<(}M*4T$)ZP5oK z|1q5HN!BhVKV0^a8mtB+M)*cv<-aGu)rzAiI)cQ9Ew>Ukw% zVp~vIIp}>eVBAjJee`$bf$;Nn$y6VYK38|J_MGrt!TyyLoabQ2?h9%ShiZ?;p1C(D z1>kwz0i75#RwkRRMVx*hI27=_J^_@Y1Vdo;BvxTmhf$LhsR|9 z?)N{u&;Rh=HQLpyS`}hXQzbpP4ujdfdeIZU^}NgXXuO46+_?@l9`G~G9Ze;=zv;;K zrX%+?*48$E(D(Wu?(;unPjZtF_53t*Y~`)hot%4#C&`R$o-z)GA@7iCAnM zg@~@MPQ=$DdVP-xyLSg(FO}_gnJ3^q^%A_`0xwK?f}!E_siBEfd}8S6XeyrY#3bVT z!g2T{;8nf&MC$R0_?a>IH#s^QKYM=U^w9Z>@l&ab2P!i2C0cBds>YC6=QYRr6smFM zr2_RotDKpIH$=#6ArU_Y>P?KCPOW4Qn@$c*j!wjfC(oZxota46vEvg%r_Y8D#NlN* z3tr48+2OodO&bnEQswzlB`f%8kC^6*@Q$JL=DVXB9L*AV1>IOW4RRxACQ>I-=i#99 zR=T~mKuzijna_HESKjb3;VpSr)BfwimQdf`y@6-eZhm434MsjdfV9fdVN{`=3=oR-stM;nqN{YMnR#eQOrpB|Sx#P7-*7(>Fd- zs4WHI8*Q7@&=iby{m+IH!9o>f;vZ|kOY`u0t+z&8&Eu_Yr@wSps887wSnkW@Ou7g! zpt^RHHo%zDGjz#&a7(rBCeMtVn@oYp97{bOPk1HV(-QHqGxbr4eF;0wME}*zp}y3P z!1A8DOw$!B1_+e}GtwE;Rp_p2z6}}rol>7KBLiC0=)i)P!R9$Onv{$dgiVsarjE$Uulum>1s9Lg9 z7rvGVYofTdaBI!&8a!#%w_``(`hw#mYK5u&&-j=1=E_-gjCzd%RfXKz@#SJ=ily=~ zcoDahDSKO54d@2j;FHl%-;w^n@;1jD&sNqmpk~Ux?oF2$ZHHpFh5CqC6;-uG)JJ;G zs@P$DG&G7zvS5V42($&rywVoy<*YnJ?#^-{}UWxcc9bYyII+Uw?n#by3Hr>SODF_14>0ePo@Lt=0FhC|~P0 zU3Xix24~zn(Xz9LmqN=$=)r2eY~$(G3i7qXLd`_HM(8H6n!8_qq{kT^T|42ha!hP^ zzF5i^J;882B~MHpqE#JEt;^x6G_s^r9<5oEJJxV@_DH#v;#;_I|7*KLeaWqX<)C96 zlhA3VVR{D!*Co5NvVZqf^{?-w8YL@MX&;5=P)#AxFNM1LMs~NdM9)Yu&3S%w!%7UH zzFoTl*DpF;FWBRyaTtHml~7kb)s=|#L#^}TZZ+2sm-A(k9V(S*xjN`;r0WQbPZMC} z@73oEFu-Urh_Hv5@B)3wUV6YadAF9`toqM(boK4uAEf5?CaD9iv)~+%8Uq{{938x~xqxV(KvyNod_-Z!b+vGMjzN)6Y zD%bwu_O8CixK<{YhLu4|vm`y6fi<`6>zGoSYd#jdekl;@(j$}O_n8jbm4(doBq z7jxC@NTHC0wI|ZB2u`5{gJgRd97w2UrITmIM^2nc9cxaI4fi|u|9i7-vCqdIiu@Au zBJ)M&FPTp;A7b9eyoY%Q^ApUQ;2nTDhB6nK)65VfG5ySr*e7Ejjs1_Ho(OnL z>{{$f>|ktntS{CZ{Z8~BqR+#V{{Jib!RT*B-yQwg=-Z-ih&}~R78vlH|EcJas1V&7 z-5zD4!N@;Fz7%<9FP68YgsIWir2ZR9MhCZI+RMBsfA=_>3of+7ll2gim?Gk9xu*p72Re_=G2X+!IcD!pA(}VNdv|Cp_c{ zKjH}=@q{1tgb#bdhX#X7u-W;s(<5itm-pcw@+33p32UCP>Io~Ju>j@w9gavhT&9SSoLvZEZANi5MTOy~!_w;vKVWGnN<5c9f+cZBRI&Nm9q zRNG@zpU&rJQaQ5#_bbW`KjZb2Yz?t>Gi-I!Sl+vL)fuGuV%C4!*1glvVp_Mhtb6?E z*w|=l=uGn*le5Zj<)MZWg{jW!E$C{-l|w7{wX273-;`)&su|X5HJlkwdm_E|AVah6 zdkZvYW^z+|;Vfg?-f|oMA0C(VyQF z>U#)s+uWF2?XGrRcKcdp=N0$KuNIx?=N|0pOYLlB4J(v6H@Nq@xI5Ihb7$aL#c{oE zgd2EORjuliUGDLhuHF)Oq`wN3F&VWvB8* z*n0);ocM`(jl@@=-h3$I|0h{`zqs>(MOjDSJ$QP)K;pvLW%yv@vgpe`&%`DhEk-th6B4_C+@6_Lw0({ zAvLNE(uw+p(HQH!aaw0`KI0G%OkeA(#ZRs|Uh%gwxaHQ|-B-<%z_Q@b zeC&J*ZUfrSn8xd^Hhw;JJOvMa4yVT5bW+uJ1J2EUUbWwT;I6XjdVF|je0bWzM{Uq|WLuYAT`eB1YRnjhT@6~^f35dk zc;F@(xIW^zna23W-`ndQO=I-S>WHBXwb8cQ&(s`2WlZbqfEujAS7xP}UQVkI_m}!$ zlmuhw$|$LBjB3|szUWw%_u1!d4Rz^_TREj#kKV}2$Q=Om+t2^EZMxl;%a}eAKmter z2_OL^fCP{L5h6XW|CLU)Lu-Fm`?~^v*7oYQS1-plZ`yz4R8L#m;ltZ2YA;Nu zb;aOhmaw{^@YCgTp)`2#AYFuw=CgFb$j=_M*RSValYsqbbEV3-5*SIDPAv_UJ+kuWddW@1_gdV;G{<5a*w;l9(mlaN)@80q9)74GB#-LFWb5*Rl59i#J4^a zVo=S{6-h9KS89E5l3#s@1G3^a_T{I0eTy$xoUT|@6^W+4GKj&||6ny*_P z3q$cwb+;`?-QK|VFr2QbtfUZG(5#IW{|TL!WWmrSA{x3*S;>@GLoiiVQ!SMMUQ3fi zneapr463lQNF_s%Wx)~zDiXnv8+r|=A+bV}_lDz8B3W}w^mLDJg_?pS5R*#0C`cPC z(FPlsnYd}6S@SO(*ljZwyZCNry8LvvZ}Go>-76GdQdVj|p ztM)F-Pj~qi-_!(ZScatY-{s!rRu*4rQG8KdDSoGK@ij#kw$BT#itnlY z4&UNSFl1#lOA-a;yESsXWy8gq>*G1i-}pswrS^lq#n%kOd_J?6dP5w0Hd-ZJQ^9`okgT|MT_H&i>fAOBfhb!N0ZSCLk&0e~V*?;oczj`Qe!~TZspYqLKyp7p^;)h;87`S17 z!)5S4`(`iP#_a!auwM(@u)iVutG?Ouw=w&VZ26cHxM6>TtNT~q?77>R{Rh4>Ck1ZU z-;n(?zS*<4G5g>8$W|e6!~U+l{||TjoBsSDcLG>;B!C2v01`j~NB{{S0VIF~kN^@u z0xbz_4(^5L6Fd`!_SN+}yhl`+;A;C(?-8XNj$HK~kwb7~#(PBSf+H8bN5oDza>9E= z=zt?IUA2-sAA}>jy+^oqII`J$gmtg~-^Mik0b%?g0VIF~kN^@u0!RP}AOR$R1dsp{ zKmxaiKseYLs5}j1U!3cehjZ+CgU2e*%GeVIqm}1Y?3n_1ddGf-#GWFME6;P-a|8P; z&sNxz0`bao4)$!o*2*&n_B24a{{DaLv(0}%3O`5y2_OL^fCP{L5lfS1`qe`Bb$h8*qQUr{#Iw&c?Oto!*|<H6u8-)R3t+e`L=@88e$$Gc-=Ta1U6 zquZ`t4P1LoCtWP$i{&(@sEW+Vg20QcmNs-*Q4LK~4ays(E1nm*lGB+|IbU2#o;Z4( zzc6-bJXu*@SxxfNfGtWF%Ln2AhvKl7eX&H#hv&;yQq^^lH>kz)G8IIVH>s&|n$Byi zC7U`c5>?|>iz{_1ZMvwDHFP(o(J~MJGHF1F- z6-ML}Cm+8wnL0i`k{VBqriM-o!SUl4C&o^WogCTddFE4z@=}2&26OY-?14n7Ov>{m z_ZMJ0y5#*eRy1ibF_#bW`mJ|xvj6uqoeD0wAyRniD zOM^WqbQNlrg-r=ejk1bJ3{x^Bg|`Gzl3~#hL(>(i2`o=_Q-vi~iEI!m3Z|%FCAT)= zY9&8*UGG}8k~u+Cl(b>-fK|10LnODjl4VJvibhSvl2yU9h@n!JRSiyOHI5ZE-IR&0 z35uvu8CFnMC{(nl=(3;^Ue|Pv#Y$eQk{@~f^~XC`b<&(5XkuF9MNNg)tn(IoOPsW+ z%ccd3Udz0}vO3`mT~rN8R=_(`0nABaMN?5YP9lU-L4YQ$DKgZlVp2|&4Rq40bU~H& z`v`ws?^vV#3WAi@Dc~m($3i7w`-L9-mDCd-`NnlUxJo5Uk__!uN-LZ$X|T+=E{W>; zD_I<{W!LD5P1YW3v6i6Qd$Pc;CORjXvH;^NU6ce((k+$ac`8!Y;)nn~8g>uSp$E2D zj)*+5&`Gc1q}k{H{q@J&S8>vIB`YGE<}6dvsG?KeRMzdJ#Z^Yqif&4rDzJ(MJ0A(e z&$;@Bg})z zjIWfvvL#hSNrB02*vj3E`AaE~mP9mBg5!!t4xuZf@u680VIF~kN^@u0!RP}AOR$R1dsp{ zKmsiYR9@tN;rP3LlW~J@h`GVJs2d!PxWO&G)!?!J84Oo~@L2Ot^|-;MZa4T?s1|(W z^<8d|@2mxne50cpgeP@^Zg8}{C)n8ok4v-9-{oBY_deeiDMeF}??jTl&&R$PdxDvd zyf6BRSYPzb%ztMN#{N@_0^(sLfCP{L5k2oK$$K5Xq2Pfmcmk zl{8U=mmD>snKIRgMc}VIS$_IgxkiZWWO*g^0HunD)3sKu1Kt9>F_RyX_z_>KY5dl+>#RYR!U@oW*le2 zD@*okyl|&gkrh)X)FLXe3{f-$kr#Q*Acm#$hN+0~lBfkQhU!3(uyWW*MZd|nM|&!% z+~kYK-M2)Skdw;IzAD+}q;j)w<#pZ?T{fK*xO5ZRFh$jUOa{1dsp{Kmter2_OL^fCP}h zEhCT!js!>{bBZnvR^H7YRN*mlV#qvP-)k1-4CYYqYG9+QL&a|9o4bW=`(BnUgzi=H%eboZS9U@M_OS8%E{&Kezhzf9{qwb4&;c zAOR$R1dsp{Kmter2_S(V7y>tQnSKz@u7%ED4}LEA?)FQ8F9aT$e<;*<@L=E#+sZ`G(#NPiozKr4qr@DgEP7H?nF76L3pU&h=x>zk?s;X-G3N2==j6uqod~Uo<%JZdk#%z>&xJqf{%(2wt zE~PcnNW{m^G|49RC7N?g^p~_y-{D<><*km6sacZAo|~uhG;I_qDN}Q(Y-_c0{BXTO zD-rc4R8&}rh1}f)6aAOfP@e*s_B3W%ayhJ=dZ=ZNE7CsMRU#ApR)45Z+8J2xtmL&= z$QR3x=Jm9dFV2!m;cCYYG~`q}?vbr#REvRPRnkfHKeaE^Hw^jgYRG4SWV5t9WST`< za%wnLJ9&Sr?3zw#&b3Y?(Jv@neJ6GW+MK*gbJH;ibg>#!ZKBiV*=pmh9;nuE zgL62RIzBWxIuYMn0H=`2P3^5p7iqb;G@PH$L8|rb6g*1pB(^F%OBTd_rXx#IH*3fhU&y9=ZmZAEUZ^&H>tIibwokSR}F;vCPB;8 z8d_F5l2z5@*Q@4AT5IcBkxlepOm_9no@mv3H`}P!g>$P$v)7jng!)dL2s}II^zo|- zuNrx<3ZOa?SXK6A4Gu~*|K~ppSaGZM{d~1EtW>pc)GRF%=qZVRx;N7W509N0pEy4R zqlkE7RS;>naA|9XE+yi}$IholPMoo?T;lr@Zfv4Iem-?P1>OB{YTPf)Y6Azb_T^B& zJ-$Dd8co6F)9}#v@X)c8ePr^iJ@}{{3HP7d-_=KttlzAvnWTM8sv0$L{qn0reMgQ2 zo;6ySQ&q5;N;M+6mKjy$UX}^1Vg!|aBTtgjw7(hD`88iNg3BNnI{BZy;>|ZOj$7%x znoq7;H;iEIE3lP;UiEm@an$SU4r2ELkIWTX8~A1l)r$}RGR&7~acByzejB30{doWX z_ROnbsz?9{AOR$R1dsp{Kmter2_OL^fCTO&0(kv@C)Eb(f&`EN5MK>;F5cHc%HNfCP{L z5z z-$}KBx*!20fCP{L5XQj|l5hYykXbdg$z6Su0C67e!APSNt=L|V_1+)ScYfC4eiOm1or%(Jiv zc2`(@D6SJ@W~!K<&zVUh57sak-_5cLYY7j5$vVFmrZZ*wP`peR%Sn?O`696`axk9D z=ismoP-rn(BxYv5G#Hm4&BVj4Eb-7m0wk(cTA(>wo6f+E7e@XzRr$8>{|A`o+L-5< z_kMr-BrF{gKmter2_OL^fCP{L5@!EPdElLKV6lHdX7gt;a*QT>#cTCxTc=&Y-fg@69XWQ1x;c_NoQrLwoZg+ZIXl2`TR_IWxbGr>RKcN1}s-mrsh+Lk}*wZ$pu<0 z!Qu^xLGD0;UZJ`2#8QDK1{15CnmCXsf#6UX#42lRs7YR&;Dtd6Rx42kc#gdUG6k}f z%@Z>*_>?!X5jc>5g-UpkAhWQ_gq;d30b>6wm8kv8UOokWF2GtEv^->*MOrGs;S&?7 z@d-HY(ac^01!9~S>V(I?HLiuCgj`nR4Q4@G+{}T;oMro-gKL!IzU06KCThEBVO>ZphsJ zzpH$kl$c4-s7#C3(;=T4CPYvbP2^dTQ-~_@cW@2M1Cqq8UBe=lgziX>|i70I2{ zhM|U8Y3&-mgW9kP2I=31iDu~M6&&YUZMae1soRfNzUykZ!>@)V zMbM!a=6MQsqJi7LWBXxwK;+c5Ygo9|HEbui!5VJS4-;LGS&O5ltZAxga% z|GDyASHnS{8s>QpURLFJLlTK*vMkiF%4;g8s{$+!r{7s#T95$QS|hDHs11v)+Aw7~ z%U}gdAS{(MYt0&Nkay~Kq4Hf5b z*AGi7YpI%PO0uDv&^fME!;SJz-QH9AuB+jIPYqK;<8j; zKKT0GP4M-BZu|LvEv&RfJ`hXB{-yUnMfSvgBKmITS7U_vSnorzEzC2qr<*JMIxpQmwvLLNN^uTbz=;cQqeRuB5 z%4vcjXo5&}*^LoJk=GSPBcdoMnyO1~jILOQscWJvDmqahWj97pM44)oQpL~(3YU6r zj6w-4xD8VtoM3V4|Igl=z{hb_`NG|5wOTEyySzq8Ok%l{V2ily+7|~CWLY+fw?vjm z;*e=>Ro${X)@F6fi4zQ?um!?0z%UeIs%&2(kg)*ND(s_hu=Wnl_LD85T&!=u!aJo(Zv#rGuxEDbtQRLO!Wza&g? z6cgamsp;#UW|&SPOwn}B^@*h!vZFij-j*;$B97!)s$)u$>qwe85T;Pwq%O2IqT8CK zxX$J<#gpL`%XM9qfI4ev7cZvRa3z83z(EFq3R7IwCn_Z_^<~{Ob^U2!imw41 zS+N~WRy9>oH-#ydY00oR2D^-sD%+lZVVD9f*Hb0awKQU=zAar4rnnY?tw_LI^DP}3 z-1%XO3>63M-I8oY@_ohV4^u47vSritpl)R75B_;!ilM_6jI8Reulv5`=^Mio$1rru zB2-sBQ>Ci2Axu$->Zz7x$dW+_RkZbCisVp0-?LTU^i{*Q)`cn1TeTvF53=^tvDQm(MRfVlrUGbp832biaLYN{$vw-^{G|h*d9I9#gFa@ecHg(%n zWyK`0zmp47ps^F^Y5*$r4cRdJ!W7@pD3yTTgDppo8ognP!M_=xaUVO}8lm*yVIj|$&} z?*rE6-j`d*P4)iwzO#L$z6)}{?0a3`1nB+W?)|&`jrkAueX8%>eM5a>UpoJ}{FeNw zd@lFK-1Rv*cTwMixzFTAa@oGe`o5gI|H(4dp`3^SB7g`W0*C-2fCwN0h`?_Mf%U1& z!KzYJbWj#$*#cXHWCwAQ>!~Ve+&TeuD~PidP~CK58@g$mK^&1Ru&L-em`{8oCQgru z(}Fk`RAJa;Gz?$z)F2KNP0&t>73n7FYM~WifG=*5c~;T~dpyd$$n8 zS-K9^8`p;YdSy#Y+(68`n`7uMuF=)ydr=VQ!#=SkNsi;o=F@^W18g@kDDsZ!DVu^g zunDQuwZNccX%_}@o}-%%m^~~5OlGw>Qv<0H1WK8TZKu&#l53drSk zL7d}zGEw0yi(^P@Yy98}Nf!)un(gXqf;bg=hvb4?(veJ|7H2`HV%9+0_3}Yn{V~y8 z5LbU>r7woAHzuwpCN3Mq)gLGCj)}{}yxSE+myU_+4C3l#PQ}D^tV>-ER)B^hbzJ}N z{%S&aQ1~2t^}iqxVMy2_B=Zk~_I^)(Ay4u{`7ODB%l%dEw%iT5^ZUNh_xirMzOg=~ zZ(Z-td%xcM(cahh&h?J)DTHU!R@Jj%AhZ zpLTyW_rG&*&RL-Ue>5xnhQ?52iwGbBhyWsh2p|H#ZwPEo4JYe9V2gg0XD^d(6;h)O z@84Xiobcudd@EP`u+LnEg|SS!zD&BdOuD*Ey0T2VyiB^ZO#04c(swMAE-sV4eVO#j zmq~xdGU%B# z=Rc9Z7c2#TlD{+mhxz&ZP5EMeJU^O$R$k3-%U_gVm(S#Wm3uVzz1+8QU(4N>`-|L% za(Ct41zF+`B7g`W0*C-2fCwN0hyWsh2p|H##|Q*2+V^j|&aMT|yTb~DuQF>s-0wYW z)Pf)QZ+aN~6Du^I;dNC|_lmBZkjQZ4x2shwePaz_{}iM8On|6_X?{I|=);J2O; z1V=9V*TFFO&23@upSFg!ZaA*E|eLv~@ zK;L=2FM)IXrS5;}eq(oc=7n89Nd8^d2fHpw-<96dd3WdePxayHNn5^`9Nd@b-?}w< z$F&vWOwv8`lP5=RnVp%dTs7k~JRBGv8yOlO85kehb8uv!(2!Ob*w!-uKZTN47#JV9 zZhYYI5%@Q8@ZiAJW21+L#;zYYFmnC&Doxf1CB|MC1-I6G9|}pLqc#<1l;p5uoo(1eDu&rv*5!M zV`C$S$BQiW==ji~s{^j{vmOI1_T_*iyriL?sAu~?0d8U7cT$9K)libd{0A4(2Q&Tq zw*}jm@){Gb37XgF;k_f*MVeRi#lpam!x56gwn82E!r+SzWcr6e zj4d%@ga9@ParII{G`tosLP$~=JbhKBe>VuQF-CxD4`|B$$Wpm4z7wBy(5nU)GNVud zm)9yFxEW()+MAszO;?Jcge><+;^D^pBdLwAR)vU=KqZ9y3n0mJmXZYS^ElP?CZAw73dj5)|8nX0G+gsD*CKgwV0a?p^$ad_ zJsX<(&RSCsMx4EHZ_hz~ZBKD=U~Ed;+t|FC((B%B7)2JjL$?bDR^iG_{{aw0=6$Z2 zSy;ll0d0}f@N%*=yxv-zkPP~s@T{dJ3NMa|m1xg$5;eTuTB48)BwDu{MoYk`I$Abk zM1y%~)Qc5qXjzdO-d{qdkghN|^vtgQiJh%Rg=QJ$rl@DXc>an^|IVGsmtVlggeHQf zh%1AF1S3LI`V$%u7D0<26l&=cbCWFy!VAP=dw1R$>;(LBF3Q<;n@8a!UoPBMLS-tJ7euWv1Cj+%*Xsvku{bz z%yMg;in|pCpTe$8|Mjq#I#l21!^IRVh)aISC2WFYBjbFzu2t8lo)z1sYpKO}9t|zK zMhk;cGj(CGY{N=(OKqh&GDS+0*UZy-+PGj_jNjcT&|*U43sn&oQyUj|O#e>`7bS#? zI=|BVoBCNQ*|9cx(UVpyUY+0zr-a@KaZ`FBcVHoxRt-z@RMQuIaI-3EvSW)xBbMk| zx(wG^d9I;5aHrnP&6A~aWyc(ynmJA9r%z7L+&rDU`)B*!^bq*`kZjY}H1M~f>J~VK zwWy+a;LXBNEYJ09AH1hJ@P~rS6;E@iMJ-!5!Q-t29xPPlwnCLDT=7?#o2PK~9@Bh> ztC#$#XARFYe4SV_+}C8ws!gb2fI?i$ z&xaVs0vu<8+Y#`QtAQU3$FVI1+|^1+pg=EALA zK&ZhLe-g+7t;vFx;zJu$b!aBO2|kN89oiXqPBt7#r4HQDr}<&^HWybp>wzN~9n2}< zyxCAq3o-)tv_7;r%K<+z3RJ8lDXIn~1^>;aB7>h7OIn!RSXVm7hC8hwf6?;|MT8dS zi3Avk9<*Ev9nhrUx~4_xzVMhSQ(&q<4+LL2HaNzWEV#4^`cVqLaZTUXWrIkjt{N`5 z?6nO;a&+ntM+0Zt3YA>mSHJL!M51SS;xg7(Z-ln%fDFvVu_%F4Wl{50O#}g57@xq= zHeA%B!qseK_RJ24xF^A&Hd)tI-~6x){SjK0&kUZ1>E<*-IuE3F@NFEF2ThxAg*e*1GNP$#wDy3zxk0w zB74<@%nH7+oAp}XgEGU&>`0C)YPw~MmSstzNevsS3C0j=!*n`bq0?mAr8_D!$B$3e zkIirX1Iv|Qpajq4GB~`3S|#8)*z@2bE24o5ZD=sE4#TjfgDg6=iR;2pYeTjQxO!&D z-c|tKV0R72W>^@N*}boYtC_W}Tp6gqEvaxT5L?ll8S=rLd6c>{(;lFrN&wUB96eo{ znJ^- zGX=L!M3Ov?*YgvJ?h6CF;Amc2gR;VCsf&P%Eov48yr}C#KLOX?P^aJ_%3m6<=fEYo zzUo8e!H^E~2mD{R!5z2B94~`|Y?pv9c3@1rnRjcLjNmd(aA5{xv1Zia6%6hx z28N9mXFa$ymN--~RB(Q#f~#lW0XNOeojZY1-X!20+8{8yFo#kU+?vS*+*iSX28~&k z!6Q7!Wfbl{?g(%hVLgcXp8{XkqAe=~Mt_w8F2ojn3noDwTA}KAOXI@sr_|w^N)P^L zLT0w^L67#Jo5;|k;igRAcEL@R1=A7?7BFs67#B?t+^`(Sqbf{!t=iEzw4Z^x+@E+` zf*WwbDL;6(H+%|yyAHqBxuuDo;YuQS&!-~H!Ln$=@CGX;xE#PU>!gn9dmDe-ZHbuRxnB3c z#h6535`)VS!9}`5;Qj${pa$0fKr=E_=zr7)hv%@EfjP~BTLNGtBsL6_d}e*~d5J{l z$A8}zi-|<)^S@7v#YCdx>(%o9UMv>kai407MHma=9#TiqHOfY|H~lpzi~qUU zgEiG(cz&{X;*u?}%n)IAfEzAgw3a~^g2gv1CmmU|VaWhvp60>@7Swg9%_jLBRl_d} ziiGVm0QdbDf5C(s$Q|EeDun7A;383l>rto;b2O}FVHK@{TS#`HfV_ruwZ0&weuNJAqoMniZVl>?xw7x|Rq#h+v#`VN1ZF zppGVI!3?hv;uxX~+Ax^&6;pJ47qo2!t^#x|Pu4Uq3xxX#T>l5QRYinL0>F-~fLaba zHLxwq?lS;Qm%`1H-9UITfx91c)lncX#e#h<5YLAD14P>ai?jD20NCb1=_nPv+{zSSs|X3 z2Eun5V2+3D8!Zv8_kC3QAHE%`fv>?T!Sk6oj`c2Ni0vLnhnNh1K?3@Nputi z_O0NqMA&IjbTD|QfbbeqbKn|Khl;XpQ{V_2m9Q6}iC{Q@jW-H+GU$58*3?9HB?gPC z{{JX^|DT@$|9#8;3Xg&a_=5-_0*C-2fCwN0hyWsh2p|H803v`0)FZGqwKdsrz!EGp z$=B7NK?F-o@(-Jj2Z7xsS!#|3D@$^$IT~y#$t#+p!Ge;!bTRwt*=ewrB!$ID(7(Yf zlK6E)3_BD5NJ9)5IuhSpjH$8$qekKji;=*JDbpWqhygQ2;_ikRuty|bQx5^NJDhEn zo>>`U4&gDI!jWcbWi>_k`~NOn*)QpUlLC=Bg9l6``k9Yq^x171I>y=&U z^vl!To#oWeffRobfu}wK3(wss9N4^Z^s;2K`-a5s-Mh06T&Yl$O&eS>f^#Qu*I^WO zmw=}{>dF>)JDQoA<(|-Q0>58mwse4=*;PJ46y4ZGWpE=V+2FLx(lmJcw!&m-s#F1g zXwuo99?lmo4`|?R7ZnLy%CS$KEb!6@uE!KfG2qiFQI*Ceb(du9lGPm`!?DP?3w^*39y4QwHxUUU0hwuWRPxr2!MQ1txNf?B!!GSPR)NRVk8H zlzrmJjs+js8ETMyEFdjvL~2nZ5Wmo|hPMgU)??d*XSc_fT?F4>o+iPkc#dO$BRsE3YDwP#88=JK;?cCNt1U|>iur#Y3J~(f^2remQU4?$Ex!{6LcOnJTc1iXQkRe*IYP{CeQp5O#D|5W} zUJ_ePEOTh85`0w&?p_VU@!YUrEgEY&9jzM9#4dvGP1go*da7Z7|3_7gEkSduu1YF5 zRZSSbu&!^3@rP%w6~O_gDXRullkT=*{HbOx>G5zw{KB)dT&Vcz28067u0`xG_0``3+G}L;Zs;gR-uY4D(S>`<8X!CVnu~-TwBI39O!06XwiW+@II*OMaPn% ztAS@@_?oq$2v4{JGlrT}^elxK5R{27!8g4mQW|xaZtZ}Kqf%GnwWi@Hj9)lE17l)! z{%;r)VPb+WY+*)*3h}_VD0m?T&s`c_921{t53Y)AiHbI^K>jeft2=;uAaXR`JhpIU zI=%~rqRCnje7#zZCjl%l0@RO`O|zALwxJ8wGx0T#T8iZ9o&%mCjR4$;L}6Ep#G7_} z2_XK3U3|LZD}eenNPXVmml6gKa6_qC{!-H&oBv}GY$X@OFLZRoPk4awrws}ZtN&lH{sQTpzMIrFb0Hb{0n-PnDbd*wT+?!zF#fdm*Km4V<`}t=6pWj z0EcM1597~v#xHmlt3WN<;8;%c!KbMsxhq=m)E6Gwuv{A~n&V%PiZ6Vay#&3>GJNm_ zZi4sQ+IlM%{aWE8xk$CbH^-mtjW0Z3+w&PfF2ai4cfiZE1?%n=6+T>=2ZMc!xm8k* zEp#VY=||T_*WlICo0=ghMHns#xX_ls6|GfQ^~;Qtk)ELJ(zPAHE!qgfcxGbQn9C)E zZwsFi-YA?B28BaHI{#q)Gx@vn^Z9G?Vm_ODDEGPCTXSb}6FC`3@dpt=1P}p401-e0 z5CKF05kLeGfhRu#m!u9Pm)Ygn7)KM?G1(AH)3~RxK9;6&k6~RLO=KfrZ46D+TEC`= zrrDS-L}{2795GSn<7gt*-drqAqdm1RmZnkT?v0^|8bo^HXd-6PY!gkh9kRQQCbU09 ztb3U_8ZfifTdBHQ(A1ma(y=s+rk2h)nut9l)lAc5wCX6N4%Bw{T3HsD{@*9umk@p> z`~rLc{8;#b@Ll2Ggl`J}D124;d*MFeFX8v&!o9-1PyX(WB|roa0Ym^1Km-s0L;w*$ z1P}p401-e0o(u?drV>f^eZi46EF`c$>}!H&=UAwZ{pn?Ydf1;V`_s+-WZ0iB_9xB$ zbh1Aw_NOD2N@ThIpMQ5E|M~p?$iF-PE8*XTFAA>~W`*|)M+HN;Ab%2m59c@J*_RLa zg9so3hyWsh2p|H803v`0AOeU0BJh+#;AyGd$>z^!>>ZY+9%H`tzZ#pUTgTSKys7Wf z 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 q z+`V|JtZG%81%LMuI}<^P(K>;L5Vt}6RGQ~!Gufkqer7f;l4CXAAvodkcNUK|;M^az z1V4=b($QLe@CfIKrHSyA6gkg4kvhpy5uz1APEEsF5kFMdyVXRjE6;$o@M!_yB1zDS zP$4!t?pc~EkVY&!SI-aXin9H!jv+(~H<2A76{0?8mEgEq%VrFLxPg#G4ns_9gelg@ zLWXF(BV>hND;{eYLahJ0*B^>R|Dyko`hLInOwao^{1Z;)Mhbu~bCPPdfKZso6>E$Sj+jX&3jb)S*n8v5%V)C40#o_|GdYi?GwGDa zCX2ajYseoB7ayV`#e_qv-KB|_lxmF(MQQHkm2f2-LL`zB4L$4s6pzY} zL_i`S5s(N-1SA3yfmI+dwr*~@!<{4NR)W|ua&9GvZ6fDZg4iB%ZY794A?H*= zxEJKyN)Q`B&aDKo@#EY|5FsDWtppJ<;+#qd_ots*31V0Jxs@RHqMus{Vh8#;l@LbE zIJXkS&h>LELF`vQw-UrI^>Zsh>`gzX62gcf=T?H)(|&Fxh@I@`R)W~Ser_d*UF+vm zLKrdS+)5C;DbB3~5k~RcN)THo&Z&fQ$V4$-cjMD-Nw1@Oc3T&-`qEpLIkh0!V zEaVy@hDuZ;&{i7=2f3!J9+_>Y#cu3MNaI5L&4P|iWjc`Vo$1sL6lB1172Wfwf;>mI zV!Nj8xdyUMd33dOs7Rm=^G`07Z#ylvaaW1xxTyrjrVtEbxE{P$k=))797R_>L$Osg zP`K?F9$erZI|x=ygk1-Q*#BMf=q!9SCf-d|MYr`pL82?rV-Yp*EsY{Q z^s1?_s!)h+<5eg+Z>Zx7Qe;`gI5zy6vkCcF>K8zuIghefVpZS%sMEFm3CARMPHgdsRtEK}P(#w1yHmaA~qtmH_!qhBJ zq+C;U!#27B3RU$g95*WqyP&u)f@3~4qY+une3q<$F6TxWd~OnkQOSj9Njj3?1NY}6|@S4*oa?+ zqSL733tRPc z1=;ItMTe|V;HqRok}{iUI*4@nYU!XgqElo?E44=?(th#5Qx{_tDfLoSHB7kZD3gadM03}q`J?CxfpkS>bL$0h7>}?| z`Pbq)2Zko6d8p1dXcEKPE=34(x@;h#OqLg^qew4?^K^vmN`vu!>`Z*i;evZ0ML#+z c`Vm)Y=m@4%%T&RUtPrza6o=^ux0byYAN?#|>;M1& delta 29393 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 { }); it('detects newly added required fields as breaking changes', () => { - const baseline = zodToJsonShape(HealthResponseSchema); - const current = JSON.parse(JSON.stringify(baseline)) as typeof baseline; - // Simulate an older snapshot that is missing the 'indexer' field - delete current.properties?.checks?.properties?.indexer; - current.properties!.checks!.required = (current.properties!.checks!.required ?? []).filter( + const current = zodToJsonShape(HealthResponseSchema); + const baseline = JSON.parse(JSON.stringify(current)) as typeof current; + // Simulate an older snapshot that is missing the 'indexer' field, so the + // live schema now has a required field the committed snapshot does not know + // about. `baseline` is the committed snapshot and `current` is the live + // schema, so the deletion has to happen on the baseline. + delete baseline.properties?.checks?.properties?.indexer; + baseline.properties!.checks!.required = (baseline.properties!.checks!.required ?? []).filter( (k: string) => k !== 'indexer', ); diff --git a/backend/src/apiContractSnapshots.ts b/backend/src/apiContractSnapshots.ts index acaa02eb4..461460bef 100644 --- a/backend/src/apiContractSnapshots.ts +++ b/backend/src/apiContractSnapshots.ts @@ -249,7 +249,7 @@ export function diffSchemaShapes( } for (const key of baselineRequired) { - if (!(key in baseline.properties ?? {})) { + if (!(key in baselineProps)) { issues.push({ path: at(key), message: 'required field missing from snapshot properties (orphaned reference)' }); } if (!(key in currentProps)) { @@ -260,8 +260,10 @@ export function diffSchemaShapes( } for (const key of currentRequired) { - if (!(key in current.properties ?? {})) { + if (!(key in (current.properties ?? {}))) { issues.push({ path: at(key), message: 'required field missing from live schema properties (invalid schema)' }); + continue; + } if (!baselineRequired.has(key)) { issues.push({ path: at(key), message: 'field is now required — regenerate snapshots with npm run snapshots:write' }); } diff --git a/backend/src/idempotency.ts b/backend/src/idempotency.ts index 1ab4b0448..fcc20199c 100644 --- a/backend/src/idempotency.ts +++ b/backend/src/idempotency.ts @@ -26,6 +26,22 @@ import { prisma } from './prisma'; import { logger } from './middleware/structuredLogging'; import type { Request, Response, NextFunction } from 'express'; +/** + * The replay cache used by money-moving operations (deposits, withdrawals, + * transfers) lives in `idempotencyStore.ts`; it is re-exported here so callers + * keep a single import surface for the whole idempotency feature. + */ +export { + idempotencyStore, + IdempotencyStore, + IdempotencyConflictError, + buildIdempotencyFingerprint, + getIdempotencyHashThreshold, + type IdempotentOperationResult, + type IdempotencyKeyInfo, + type IdempotencyMetrics, +} from './idempotencyStore'; + // ─── Types ────────────────────────────────────────────────────────────────── export interface IdempotencyRecord { diff --git a/backend/src/idempotencyStore.ts b/backend/src/idempotencyStore.ts new file mode 100644 index 000000000..41e4e6d3e --- /dev/null +++ b/backend/src/idempotencyStore.ts @@ -0,0 +1,360 @@ +/** + * @file idempotencyStore.ts + * Idempotency key store backed by Redis (when available) with NodeCache in-process fallback. + * + * Issue #811: Multi-instance deployments previously lost idempotency guarantees on pod + * recycle because responses were stored only in NodeCache (in-process memory). This revision + * persists completed responses to Redis using SET … EX so all replicas share the same store. + * + * Behavior when Redis is unavailable: + * - Falls back to NodeCache automatically (fail-open). + * - A warning is logged so operators are aware of the degraded guarantee. + * + * Existing observability API (inspectKeys, deleteKey, clear, getMetrics) is preserved; + * operations apply to whichever backend holds the key. + * + * Scope: this module owns the *replay cache* used by money-moving operations + * (`idempotencyStore.execute`). The durable, per-key record lifecycle used by the + * `enforceIdempotency` middleware lives in `idempotency.ts` (Prisma-backed). + */ + +import crypto from 'crypto'; +import NodeCache from 'node-cache'; +import { redisClientManager } from './rateLimiter'; + +// ─── Public Types ───────────────────────────────────────────────────────────── + +export interface IdempotentOperationResult { + statusCode: number; + body: T; +} + +/** Metadata attached to every idempotency key entry. */ +export interface IdempotencyKeyMetadata { + /** ISO-8601 timestamp when the key was first stored. */ + createdAt: string; + /** ISO-8601 timestamp of the most recent access (read or write). */ + lastAccessedAt: string; + /** Number of times this key has been replayed (returned cached result). */ + replayCount: number; + /** Current state of the entry. */ + status: 'pending' | 'completed'; +} + +/** Summary returned by GET /admin/idempotency/keys. */ +export interface IdempotencyKeyInfo { + key: string; + metadata: IdempotencyKeyMetadata; +} + +/** Snapshot of store-wide observability counters. */ +export interface IdempotencyMetrics { + hits: number; + conflicts: number; + evictions: number; + activeKeys: number; + pendingKeys: number; +} + +// ─── Internal Types ─────────────────────────────────────────────────────────── + +interface StoredResponse extends IdempotentOperationResult { + fingerprint: string; + metadata: IdempotencyKeyMetadata; +} + +interface PendingOperation { + fingerprint: string; + promise: Promise>; + metadata: IdempotencyKeyMetadata; +} + +// ─── Errors ─────────────────────────────────────────────────────────────────── + +export class IdempotencyConflictError extends Error { + constructor(message = 'Idempotency key already used for a different request body') { + super(message); + this.name = 'IdempotencyConflictError'; + } +} + +// ─── Redis key prefix ───────────────────────────────────────────────────────── + +const REDIS_PREFIX = 'idempotency:'; + +// ─── Store ──────────────────────────────────────────────────────────────────── + +export class IdempotencyStore { + /** Fallback in-process store used when Redis is unavailable. */ + private readonly localCache: NodeCache; + private readonly pendingResponses = new Map>(); + + // Observability counters + private _hits = 0; + private _conflicts = 0; + private _evictions = 0; + + constructor(private readonly ttlMs = 24 * 60 * 60 * 1000) { + const ttlSeconds = Math.max(1, Math.ceil(this.ttlMs / 1000)); + this.localCache = new NodeCache({ stdTTL: ttlSeconds, checkperiod: ttlSeconds }); + this.localCache.on('expired', () => { this._evictions++; }); + } + + // ─── Redis helpers ───────────────────────────────────────────────────────── + + private redisKey(key: string): string { + return `${REDIS_PREFIX}${key}`; + } + + private get redis() { + const client = redisClientManager.getClient(); + return redisClientManager.isReady() && client ? client : null; + } + + private async redisGet(key: string): Promise | null> { + const r = this.redis; + if (!r) return null; + try { + const raw = await r.get(this.redisKey(key)); + return raw ? (JSON.parse(raw) as StoredResponse) : null; + } catch { + return null; + } + } + + private async redisSet(key: string, value: StoredResponse): Promise { + const r = this.redis; + if (!r) return; + try { + const ttlSeconds = Math.max(1, Math.ceil(this.ttlMs / 1000)); + await r.set(this.redisKey(key), JSON.stringify(value), 'EX', ttlSeconds); + } catch (err) { + console.log(JSON.stringify({ level: 'warn', event: 'idempotency_redis_write_fail', key, reason: (err as Error).message })); + } + } + + private async redisDel(key: string): Promise { + const r = this.redis; + if (!r) return false; + try { + return (await r.del(this.redisKey(key))) > 0; + } catch { + return false; + } + } + + // ─── Core execute ────────────────────────────────────────────────────────── + + async execute( + key: string, + fingerprint: string, + operation: () => Promise> + ): Promise<{ result: IdempotentOperationResult; replayed: boolean }> { + const now = new Date().toISOString(); + + // 1. Check Redis first, then local cache + let completed = await this.redisGet(key); + if (!completed) { + completed = this.localCache.get>(key) ?? null; + } + + if (completed) { + if (completed.fingerprint !== fingerprint) { + this._conflicts++; + throw new IdempotencyConflictError(); + } + this._hits++; + completed.metadata.lastAccessedAt = now; + completed.metadata.replayCount++; + // Refresh in both backends; errors are non-fatal + await this.redisSet(key, completed); + this.localCache.set(key, completed); + return { result: { statusCode: completed.statusCode, body: completed.body }, replayed: true }; + } + + // 2. Currently in-flight + const pendingOperation = this.pendingResponses.get(key) as PendingOperation | undefined; + if (pendingOperation) { + if (pendingOperation.fingerprint !== fingerprint) { + this._conflicts++; + throw new IdempotencyConflictError(); + } + this._hits++; + pendingOperation.metadata.lastAccessedAt = now; + pendingOperation.metadata.replayCount++; + const replayed = await pendingOperation.promise; + return { result: { statusCode: replayed.statusCode, body: replayed.body }, replayed: true }; + } + + // 3. First execution + const metadata: IdempotencyKeyMetadata = { createdAt: now, lastAccessedAt: now, replayCount: 0, status: 'pending' }; + + const operationPromise = (async () => { + const result = await operation(); + const stored: StoredResponse = { + ...result, + fingerprint, + metadata: { ...metadata, status: 'completed', lastAccessedAt: new Date().toISOString() }, + }; + // Persist to Redis (primary) and local cache (fallback/fast-path) + await this.redisSet(key, stored); + this.localCache.set(key, stored, this.ttlMs / 1000); + return stored; + })(); + + this.pendingResponses.set(key, { fingerprint, promise: operationPromise, metadata }); + + try { + const stored = await operationPromise; + return { result: { statusCode: stored.statusCode, body: stored.body }, replayed: false }; + } finally { + this.pendingResponses.delete(key); + } + } + + // ─── Inspection ──────────────────────────────────────────────────────────── + + inspectKeys(prefix?: string): IdempotencyKeyInfo[] { + const results: IdempotencyKeyInfo[] = []; + for (const key of this.localCache.keys()) { + if (prefix && !key.startsWith(prefix)) continue; + const entry = this.localCache.get>(key); + if (entry) results.push({ key, metadata: { ...entry.metadata } }); + } + for (const [key, pending] of this.pendingResponses.entries()) { + if (prefix && !key.startsWith(prefix)) continue; + if (!results.some((r) => r.key === key)) { + results.push({ key, metadata: { ...pending.metadata } }); + } + } + return results; + } + + // ─── Targeted deletion ───────────────────────────────────────────────────── + + async deleteKey(key: string): Promise { + const deletedLocal = this.localCache.del(key) > 0; + const deletedPending = this.pendingResponses.delete(key); + const deletedRedis = await this.redisDel(key); + if (deletedLocal || deletedPending || deletedRedis) { + this._evictions++; + return true; + } + return false; + } + + // ─── Global clear (admin only) ───────────────────────────────────────────── + + clear(): void { + const count = this.localCache.keys().length + this.pendingResponses.size; + this._evictions += count; + this.localCache.flushAll(); + this.pendingResponses.clear(); + // Note: Redis keys are prefixed with REDIS_PREFIX; a full Redis FLUSHDB is intentionally + // not issued here to avoid clearing unrelated keys. Use deleteKey() per-key when needed. + } + + // ─── Observability ───────────────────────────────────────────────────────── + + getMetrics(): IdempotencyMetrics { + return { + hits: this._hits, + conflicts: this._conflicts, + evictions: this._evictions, + activeKeys: this.localCache.keys().length, + pendingKeys: this.pendingResponses.size, + }; + } + + // ─── Retention cleanup ───────────────────────────────────────────────────── + + async pruneStaleKeys( + retentionMs: number, + dryRun = false, + ): Promise<{ pruned: number; localPruned: number; redisPruned: number }> { + const cutoff = Date.now() - retentionMs; + let localPruned = 0; + let redisPruned = 0; + + for (const key of this.localCache.keys()) { + const entry = this.localCache.get>(key); + if (!entry) continue; + const createdAt = Date.parse(entry.metadata.createdAt); + if (Number.isNaN(createdAt) || createdAt >= cutoff) continue; + if (!dryRun) { + this.localCache.del(key); + this._evictions++; + } + localPruned++; + } + + const r = this.redis; + if (r) { + let cursor = '0'; + do { + const [nextCursor, keys] = await r.scan(cursor, 'MATCH', `${REDIS_PREFIX}*`, 'COUNT', 100); + cursor = nextCursor; + for (const redisKey of keys) { + try { + const raw = await r.get(redisKey); + if (!raw) continue; + const entry = JSON.parse(raw) as StoredResponse; + const createdAt = Date.parse(entry.metadata?.createdAt ?? ''); + const ttl = await r.ttl(redisKey); + const isStale = (!Number.isNaN(createdAt) && createdAt < cutoff) || ttl === 0; + if (!isStale) continue; + if (!dryRun) { + await r.del(redisKey); + this._evictions++; + } + redisPruned++; + } catch { + if (!dryRun) { + await r.del(redisKey); + this._evictions++; + } + redisPruned++; + } + } + } while (cursor !== '0'); + } + + return { pruned: localPruned + redisPruned, localPruned, redisPruned }; + } +} + +// ─── Singleton ──────────────────────────────────────────────────────────────── + +export const idempotencyStore = new IdempotencyStore( + parseInt(process.env.IDEMPOTENCY_KEY_TTL_MS || '86400000', 10) +); + +// ─── Fingerprint helper ─────────────────────────────────────────────────────── + +export function getIdempotencyHashThreshold(): number { + return parseInt(process.env.IDEMPOTENCY_HASH_THRESHOLD_BYTES || '4096', 10); +} + +export function buildIdempotencyFingerprint(payload: unknown): string { + const stable = stableStringify(payload); + const byteLength = Buffer.byteLength(stable, 'utf-8'); + if (byteLength > getIdempotencyHashThreshold()) { + return `hashv1:${crypto.createHash('sha256').update(stable).digest('hex')}`; + } + return stable; +} + +function stableStringify(value: unknown): string { + if (value === null) return 'null'; + if (value instanceof Date) return JSON.stringify(value.toISOString()); + if (typeof value !== 'object') return JSON.stringify(value); + + if (Array.isArray(value)) { + return `[${value.map((item) => stableStringify(item)).join(',')}]`; + } + + const record = value as Record; + const keys = Object.keys(record).sort(); + const serialized = keys.map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`); + return `{${serialized.join(',')}}`; +} diff --git a/backend/src/index.ts b/backend/src/index.ts index c0fd1750b..a62f485d7 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -5404,6 +5404,7 @@ app.use((req: Request, res: Response) => { status: 404, code: 'ROUTE_NOT_FOUND', message: `Cannot ${req.method} ${req.originalUrl}`, + path: req.originalUrl, details: { path: req.originalUrl }, retryable: false, }); diff --git a/backend/src/middleware/apiError.ts b/backend/src/middleware/apiError.ts index a59b60aa5..469fe5dd2 100644 --- a/backend/src/middleware/apiError.ts +++ b/backend/src/middleware/apiError.ts @@ -10,6 +10,19 @@ export interface ApiErrorOptions { retryable?: boolean; retryAfterSeconds?: number | null; error?: string; + /** + * Short, stable headline for the failure. Part of the published error + * contract (see `ErrorEnvelope` in openapi.json) alongside `message`. + */ + summary?: string; + /** + * Field-level failures, surfaced at the top level for clients that read + * `errors` directly. `details` carries the same payload for the generic + * envelope. + */ + errors?: unknown[]; + /** Request path, echoed on routing failures so clients can log it directly. */ + path?: string; } export function sendApiError( @@ -30,6 +43,9 @@ export function sendApiError( code: options.code, message: options.message, retryable: options.retryable ?? options.status >= 500, + ...(options.summary !== undefined ? { summary: options.summary } : {}), + ...(options.errors !== undefined ? { errors: options.errors } : {}), + ...(options.path !== undefined ? { path: options.path } : {}), ...(options.details !== undefined ? { details: options.details } : {}), ...(correlationId ? { correlationId } : {}), ...(traceId ? { traceId } : {}), diff --git a/backend/src/middleware/validate.ts b/backend/src/middleware/validate.ts index 76e48ecf9..69a2a3b5e 100644 --- a/backend/src/middleware/validate.ts +++ b/backend/src/middleware/validate.ts @@ -248,6 +248,8 @@ export function validate(schemas: ValidateTargets) { status: 400, code: 'VALIDATION_ERROR', message: formatZodError(issues), + summary: 'Request validation failed', + errors: details, details, retryable: false, }); diff --git a/backend/src/schemaSnapshot.ts b/backend/src/schemaSnapshot.ts index fa2fca475..19a5f8882 100644 --- a/backend/src/schemaSnapshot.ts +++ b/backend/src/schemaSnapshot.ts @@ -74,7 +74,7 @@ export function extractSchemaFromZod(zodSchema: z.ZodType): SchemaDefinitio // Handle object schemas if (zodSchema instanceof z.ZodObject) { - const shape = (zodSchema as z.ZodObject)._shape; + const shape = (zodSchema as z.ZodObject).shape; const properties: Record = {}; const required: string[] = []; diff --git a/backend/src/sessionAudit.ts b/backend/src/sessionAudit.ts index 295bb6cdc..ef94cc50a 100644 --- a/backend/src/sessionAudit.ts +++ b/backend/src/sessionAudit.ts @@ -59,7 +59,7 @@ export async function recordSessionEvent(entry: SessionAuditEntry): Promise { it('should reject invalid Idempotency-Key format', () => { const req = createMockRequest({ - get: (header: string) => (header === 'Idempotency-Key' ? 'invalid' : undefined), + get: ((header: string) => + header === 'Idempotency-Key' ? 'invalid' : undefined) as Request['get'], }); const res = createMockResponse(); const next = jest.fn(); @@ -187,7 +188,8 @@ describe('Idempotency', () => { it('should accept valid UUID format', () => { const validUuid = '550e8400-e29b-41d4-a716-446655440000'; const req = createMockRequest({ - get: (header: string) => (header === 'Idempotency-Key' ? validUuid : undefined), + get: ((header: string) => + header === 'Idempotency-Key' ? validUuid : undefined) as Request['get'], }); const res = createMockResponse(); const next = jest.fn(); @@ -202,7 +204,8 @@ describe('Idempotency', () => { it('should accept valid hex nonce', () => { const validHex = 'a'.repeat(32); const req = createMockRequest({ - get: (header: string) => (header === 'Idempotency-Key' ? validHex : undefined), + get: ((header: string) => + header === 'Idempotency-Key' ? validHex : undefined) as Request['get'], }); const res = createMockResponse(); const next = jest.fn(); diff --git a/backend/src/tests/tenantBoundary.test.ts b/backend/src/tests/tenantBoundary.test.ts index c7477b131..2fd8a7964 100644 --- a/backend/src/tests/tenantBoundary.test.ts +++ b/backend/src/tests/tenantBoundary.test.ts @@ -21,10 +21,10 @@ function createMockRequest(overrides?: Partial): Partial { authApiKeyRole: 'viewer', authApiKeyHash: 'hash123', ip: '127.0.0.1', - get: (header: string) => { + get: ((header: string) => { if (header === 'user-agent') return 'test-agent'; return undefined; - }, + }) as Request['get'], ...overrides, }; } diff --git a/backend/src/tracing.ts b/backend/src/tracing.ts index a7d784c66..fc29dc015 100644 --- a/backend/src/tracing.ts +++ b/backend/src/tracing.ts @@ -8,9 +8,9 @@ * OTEL_ENABLED - Set to "false" to disable (default: true) */ -import { NodeSDK } from '@opentelemetry/sdk-node'; +import { NodeSDK, type NodeSDKConfiguration } from '@opentelemetry/sdk-node'; import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'; -import { resourceFromAttributes } from '@opentelemetry/resources'; +import * as otelResources from '@opentelemetry/resources'; import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions'; import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'; import { ExpressInstrumentation } from '@opentelemetry/instrumentation-express'; @@ -28,6 +28,26 @@ const SERVICE_NAME = process.env.OTEL_SERVICE_NAME || 'yieldvault-backend'; const OTLP_ENDPOINT = process.env.OTEL_EXPORTER_OTLP_ENDPOINT || 'http://localhost:4318'; +/** + * Builds the SDK resource from a flat attribute bag. + * + * `@opentelemetry/resources` renamed its factory between the v1 and v2 lines + * (`new Resource(attributes)` → `resourceFromAttributes(attributes)`), and the + * two OpenTelemetry major lines coexist in this workspace's lockfile. Resolve + * whichever factory the installed version exposes so the SDK boots on both. + */ +function buildResource(attributes: Record): NodeSDKConfiguration['resource'] { + const resources = otelResources as unknown as { + resourceFromAttributes?: (attrs: Record) => unknown; + Resource: new (attrs: Record) => unknown; + }; + + if (typeof resources.resourceFromAttributes === 'function') { + return resources.resourceFromAttributes(attributes) as NodeSDKConfiguration['resource']; + } + return new resources.Resource(attributes) as NodeSDKConfiguration['resource']; +} + let sdk: NodeSDK | null = null; export function initTracing(): void { @@ -62,7 +82,7 @@ export function initTracing(): void { const exporter = new OTLPTraceExporter({ url: `${OTLP_ENDPOINT}/v1/traces` }); sdk = new NodeSDK({ - resource: resourceFromAttributes({ + resource: buildResource({ [ATTR_SERVICE_NAME]: SERVICE_NAME, [ATTR_SERVICE_VERSION]: process.env.npm_package_version || '1.0.0', }), diff --git a/backend/src/vaultEndpoints.ts b/backend/src/vaultEndpoints.ts index abbbdf8cb..0b58a694f 100644 --- a/backend/src/vaultEndpoints.ts +++ b/backend/src/vaultEndpoints.ts @@ -3,7 +3,7 @@ import { emailService } from './emailService'; import { logger } from './middleware/structuredLogging'; import { allowlistMiddleware } from './middleware/allowlist'; import { triggerCacheInvalidation, registerInvalidationHook } from './middleware/cache'; -import { depositsLimiter, depositsUserLimiter } from './rateLimiter'; +import { depositsLimiter, depositsUserLimiter, readsLimiter } from './rateLimiter'; import { cacheMiddleware } from './middleware/cache'; import { idempotencyStore, @@ -769,7 +769,7 @@ router.post('/strategy', depositsLimiter, requireFlag('strategy-selection'), val }); }); - res.status(200).json({ message: 'Strategy selection endpoint (v2 preview)' }); + return res.status(200).json({ message: 'Strategy selection endpoint (v2 preview)' }); }); /** @@ -900,7 +900,7 @@ router.get('/receipts', readsLimiter, async (req: Request, res: Response) => { const transactions = await prisma.transaction.findMany({ where, - orderBy: { createdAt: 'desc' }, + orderBy: { timestamp: 'desc' }, take: limit + 1, ...(cursor ? { cursor: { id: cursor }, skip: 1 } : {}), }); @@ -916,7 +916,7 @@ router.get('/receipts', readsLimiter, async (req: Request, res: Response) => { status: tx.status, walletAddress: tx.user, explorerUrl: `${EXPLORER_BASE_URL}/${tx.id}`, - timestamp: tx.createdAt.toISOString(), + timestamp: tx.timestamp.toISOString(), })); res.status(200).json({ From f2add84fd8d06b6fb6f7b561822b637aeca4ee83 Mon Sep 17 00:00:00 2001 From: ToryMic Date: Tue, 29 Sep 2026 23:27:33 -0400 Subject: [PATCH 2/5] fix(backend): enforce a hard max limit on GET /api/v1/vaults pagination MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An unauthenticated caller could pass `?limit=100000` and have it forwarded straight into Prisma's `take`, forcing the API to materialise 100k vault rows and OOM-kill the 512MB container. Chosen behaviour (documented in the route, the spec and the tests): reject rather than silently clamp. A caller asking for 100000 rows has a bug or is probing, and quietly returning 50 hides that behind a response that looks like a complete page. So `limit > 50` fails fast with `400` and `code: 'LIMIT_EXCEEDED'`, and the body repeats the ceiling. `page` is the opposite case — a benign mistake — so it is clamped into 1..1000 and the effective value is echoed in the pagination envelope. - new `middleware/paginationGuard.ts`: `DEFAULT_PAGE_SIZE` (20), `MAX_PAGE_SIZE` (50), `MAX_PAGE` (1000), `resolvePagination()` and the `enforcePaginationLimits()` middleware - new `routes/vaults.ts`: `GET /api/v1/vaults`, rate limited, reads `limit + 1` rows for the lookahead, and never exposes `tenantId` - `parsePaginationQuery` gains `maxPage` and clamps out-of-range pages; `PaginationQuerySchema.page` accepts a signed integer so those requests reach the clamp instead of being rejected - OpenAPI: reusable `pageSize`/`pageNumber` parameters carrying the ceilings, plus a `GET /api/v1/vaults` path documenting the LIMIT_EXCEEDED response; `openapi.json` regenerated - `GET /api/v1/vaults` joins the committed contract snapshots, so the response shape is now covered by the backward-compatibility check - tests spy on the Prisma client to prove no read is ever issued with a `take` above the ceiling, that an oversized limit is rejected before any query runs, and that the committed `openapi.json` still matches --- backend/openapi.json | 215 ++++++++++- backend/prisma/dev.db | Bin 1052672 -> 1101824 bytes .../schema-snapshots/get-_api_v1_vaults.json | 89 +++++ .../src/__tests__/vaultsListLimits.test.ts | 333 ++++++++++++++++++ backend/src/apiContractSnapshots.ts | 24 ++ backend/src/index.ts | 2 + backend/src/middleware/paginationGuard.ts | 160 +++++++++ backend/src/pagination.ts | 11 +- backend/src/routes/vaults.ts | 156 ++++++++ backend/src/swagger.ts | 188 +++++++++- backend/src/types/express.d.ts | 7 + backend/src/types/validation.ts | 5 +- 12 files changed, 1173 insertions(+), 17 deletions(-) create mode 100644 backend/schema-snapshots/get-_api_v1_vaults.json create mode 100644 backend/src/__tests__/vaultsListLimits.test.ts create mode 100644 backend/src/middleware/paginationGuard.ts create mode 100644 backend/src/routes/vaults.ts diff --git a/backend/openapi.json b/backend/openapi.json index e84421cb3..ce784a18e 100644 --- a/backend/openapi.json +++ b/backend/openapi.json @@ -54,6 +54,31 @@ "type": "string", "format": "uuid" } + }, + "pageSize": { + "name": "limit", + "in": "query", + "required": false, + "description": "Maximum number of items to return. Hard ceiling is 50 (default 20). A larger value is **rejected**, not clamped, with `400` and `code: \"LIMIT_EXCEEDED\"` — the response body repeats the ceiling so clients can self-correct. Use `page` (clamped to 1..1000) to walk the rest.", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 50, + "default": 20, + "example": 20 + } + }, + "pageNumber": { + "name": "page", + "in": "query", + "required": false, + "description": "1-based page number for offset pagination. Values outside 1..1000 are clamped rather than rejected, and the effective page is echoed back in `pagination.currentPage`.", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 1000, + "default": 1 + } } }, "schemas": { @@ -106,6 +131,10 @@ "count": { "type": "integer" }, + "limit": { + "type": "integer", + "maximum": 50 + }, "total": { "type": "integer" }, @@ -118,7 +147,9 @@ "nullable": true }, "currentPage": { - "type": "integer" + "type": "integer", + "minimum": 1, + "maximum": 1000 }, "totalPages": { "type": "integer" @@ -131,6 +162,62 @@ } } }, + "Vault": { + "type": "object", + "required": [ + "id", + "aum", + "tvlUsd", + "createdAt", + "updatedAt" + ], + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "aum": { + "type": "number", + "example": 0 + }, + "tvlUsd": { + "type": "string", + "nullable": true, + "example": "1250000.00" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + } + }, + "VaultListResponse": { + "type": "object", + "required": [ + "data", + "pagination", + "timestamp" + ], + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Vault" + } + }, + "pagination": { + "$ref": "#/components/schemas/PaginationMeta" + }, + "timestamp": { + "type": "string", + "format": "date-time" + } + } + }, "VaultSummary": { "type": "object", "properties": { @@ -143,15 +230,8 @@ "example": 0 }, "apy": { - "type": ["number", "null"], - "example": 8.45, - "description": "Annualised APY as a decimal percentage. null when the vault has insufficient price history (e.g. zero shares or fewer than 2 snapshots)." - }, - "apyStatus": { - "type": "string", - "enum": ["ok", "insufficient_data"], - "example": "ok", - "description": "ok when apy is a valid number; insufficient_data when apy is null." + "type": "number", + "example": 0 }, "timestamp": { "type": "string", @@ -363,11 +443,21 @@ "timestamp": "2024-01-01T00:00:00.000Z", "uptime": 123.4, "environment": "production", + "lastIndexedLedger": 12345678, "checks": { "api": "up", "cache": "up", "stellarRpc": "up", + "databasePrimary": "up", + "databaseReplica": "up", + "prisma": "up", + "jobs": "up", "indexer": "up" + }, + "sorobanCircuitBreaker": { + "state": "closed", + "failures": 0, + "retryAfterMs": 0 } } } @@ -522,16 +612,26 @@ "application/json": { "schema": { "type": "object", - "required": ["apy", "apyStatus", "timestamp"], + "required": [ + "apy", + "apyStatus", + "timestamp" + ], "properties": { "apy": { - "type": ["number", "null"], + "type": [ + "number", + "null" + ], "example": 8.45, "description": "Annualised APY as a decimal percentage. null when insufficient data." }, "apyStatus": { "type": "string", - "enum": ["ok", "insufficient_data"], + "enum": [ + "ok", + "insufficient_data" + ], "example": "ok" }, "timestamp": { @@ -546,6 +646,95 @@ } } }, + "/api/v1/vaults": { + "get": { + "tags": [ + "Vault" + ], + "summary": "List vaults", + "description": "Paginated listing of active (non-deleted) vaults. `limit` is capped at 50 and `page` at 1000; exceeding `limit` fails fast with `400` / `LIMIT_EXCEEDED` instead of silently clamping, so a client that asks for an oversized page is never handed a response that looks complete. Rate limited: global 100 req / 15 min per IP; authenticated APIs 30 req / min per key. `429` responses follow the standard error envelope with a `retryAfter` detail.", + "parameters": [ + { + "$ref": "#/components/parameters/pageSize" + }, + { + "$ref": "#/components/parameters/pageNumber" + }, + { + "$ref": "#/components/parameters/correlationId" + } + ], + "responses": { + "200": { + "description": "One page of vaults", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VaultListResponse" + }, + "example": { + "data": [ + { + "id": "0f1b3a2c-6d4e-4a1b-9f0e-2c5d7e8b9a01", + "aum": 1250000, + "tvlUsd": "1250000.00", + "createdAt": "2026-01-01T00:00:00.000Z", + "updatedAt": "2026-01-02T00:00:00.000Z" + } + ], + "pagination": { + "count": 1, + "limit": 20, + "total": 1, + "nextCursor": null, + "prevCursor": null, + "currentPage": 1, + "totalPages": 1, + "hasNextPage": false, + "hasPrevPage": false + }, + "timestamp": "2026-01-01T00:00:00.000Z" + } + } + } + }, + "400": { + "description": "`limit` above the published ceiling", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + }, + "example": { + "error": "Bad Request", + "status": 400, + "code": "LIMIT_EXCEEDED", + "message": "limit must not exceed 50 (received 100000).", + "retryable": false, + "details": { + "field": "limit", + "requested": 100000, + "maxLimit": 50, + "defaultLimit": 20, + "maxPage": 1000 + } + } + } + } + }, + "429": { + "description": "Rate limited", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorEnvelope" + } + } + } + } + } + } + }, "/api/v1/vault/deposits": { "post": { "tags": [ diff --git a/backend/prisma/dev.db b/backend/prisma/dev.db index a319596bdb09c10ad9b07c84f539bce86bb94bc3..4bf95d871e5089936ab03a0491db042ab986a83a 100644 GIT binary patch delta 82148 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 WXAch~;uQ;36XMDv;u2llCI1&bZ-G<* delta 36072 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≀eZnqXygo51iJa*{ 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 uYIWkK(i62YTvmQ!Z>8ZSv{$b_RGd { + const existing = await prisma.vault.count(); + if (existing >= count) return; + + for (let i = existing; i < count; i += 1) { + await prismaClient.vault.create({ + data: { + tenantId: 'tenant-1430', + aum: 1000 + i, + tvlUsd: `${1000 + i}.00`, + }, + }); + } +} + +describe('Issue #1430 — GET /api/v1/vaults pagination limits', () => { + beforeAll(async () => { + await seedVaults(55); + }); + + afterAll(async () => { + await prisma.vault.deleteMany({ where: { tenantId: 'tenant-1430' } }); + }); + + // ─── Defaults and accepted values ────────────────────────────────────────── + + it('defaults to 20 items per page', async () => { + const res = await request(app).get(VAULTS_PATH); + + expect(res.status).toBe(200); + expect(res.body.pagination.limit).toBe(DEFAULT_PAGE_SIZE); + expect(res.body.data.length).toBeLessThanOrEqual(DEFAULT_PAGE_SIZE); + }); + + it('honours the maximum permitted limit', async () => { + const res = await request(app).get(`${VAULTS_PATH}?limit=${MAX_PAGE_SIZE}`); + + expect(res.status).toBe(200); + expect(res.body.pagination.limit).toBe(MAX_PAGE_SIZE); + }); + + it('falls back to the default when limit is not a positive integer', async () => { + for (const raw of ['abc', '0', '-5', '1.5', '']) { + const res = await request(app).get(`${VAULTS_PATH}?limit=${encodeURIComponent(raw)}`); + expect(res.status).toBe(200); + expect(res.body.pagination.limit).toBe(DEFAULT_PAGE_SIZE); + } + }); + + it('rejects an oversized limit with 400 LIMIT_EXCEEDED', async () => { + const res = await request(app).get(`${VAULTS_PATH}?limit=100000`); + + expect(res.status).toBe(400); + expect(res.body.code).toBe(LIMIT_EXCEEDED_CODE); + expect(res.body.error).toBe('Bad Request'); + expect(res.body.retryable).toBe(false); + expect(res.body.message).toContain(String(MAX_PAGE_SIZE)); + expect(res.body.details).toMatchObject({ + field: 'limit', + requested: 100000, + maxLimit: MAX_PAGE_SIZE, + }); + }); + + it('rejects every limit above the ceiling, including the boundary + 1', async () => { + const res = await request(app).get(`${VAULTS_PATH}?limit=${MAX_PAGE_SIZE + 1}`); + + expect(res.status).toBe(400); + expect(res.body.code).toBe(LIMIT_EXCEEDED_CODE); + }); + + it('never issues a Prisma read with take above the ceiling', async () => { + const findMany = jest.spyOn(prismaClient.vault, 'findMany'); + const count = jest.spyOn(prismaClient.vault, 'count'); + + try { + const requests = [ + `${VAULTS_PATH}?limit=100000`, + `${VAULTS_PATH}?limit=999999`, + `${VAULTS_PATH}?limit=100000&page=3`, + `${VAULTS_PATH}?limit=51`, + `${VAULTS_PATH}`, + `${VAULTS_PATH}?limit=50`, + `${VAULTS_PATH}?limit=1`, + ]; + + for (const target of requests) { + await request(app).get(target); + } + + const takes = findMany.mock.calls.map(([args]) => (args as { take?: number }).take); + expect(takes.length).toBeGreaterThan(0); + for (const take of takes) { + expect(typeof take).toBe('number'); + expect(take!).toBeLessThanOrEqual(MAX_ALLOWED_TAKE); + } + } finally { + findMany.mockRestore(); + count.mockRestore(); + } + }); + + it('rejects the oversized limit before touching the database at all', async () => { + const findMany = jest.spyOn(prismaClient.vault, 'findMany'); + const count = jest.spyOn(prismaClient.vault, 'count'); + + try { + const res = await request(app).get(`${VAULTS_PATH}?limit=100000`); + + expect(res.status).toBe(400); + expect(findMany).not.toHaveBeenCalled(); + expect(count).not.toHaveBeenCalled(); + } finally { + findMany.mockRestore(); + count.mockRestore(); + } + }); + + // ─── Page clamping ───────────────────────────────────────────────────────── + + it('clamps page into 1..1000 instead of rejecting it', async () => { + const high = await request(app).get(`${VAULTS_PATH}?page=100000`); + expect(high.status).toBe(200); + expect(high.body.pagination.currentPage).toBe(MAX_PAGE); + + const low = await request(app).get(`${VAULTS_PATH}?page=-1`); + expect(low.status).toBe(200); + expect(low.body.pagination.currentPage).toBe(1); + }); + + it('caps the database offset derived from page', async () => { + const findMany = jest.spyOn(prismaClient.vault, 'findMany'); + + try { + await request(app).get(`${VAULTS_PATH}?limit=50&page=100000`); + const [args] = findMany.mock.calls[0] as [{ take: number; skip: number }]; + + expect(args.take).toBeLessThanOrEqual(MAX_ALLOWED_TAKE); + expect(args.skip).toBe((MAX_PAGE - 1) * 50); + } finally { + findMany.mockRestore(); + } + }); + + // ─── Response contract ───────────────────────────────────────────────────── + + it('matches the committed vault-list contract snapshot', async () => { + const res = await request(app).get(`${VAULTS_PATH}?limit=5`); + + expect(res.status).toBe(200); + const parsed = VaultListResponseSchema.safeParse(res.body); + expect(parsed.success).toBe(true); + expect(validateResponseAgainstSchema('GET /api/v1/vaults', res.body).success).toBe(true); + }); + + it('never leaks tenantId', async () => { + const res = await request(app).get(VAULTS_PATH); + + for (const vault of res.body.data as Array>) { + expect(vault).not.toHaveProperty('tenantId'); + } + }); + + // ─── Guard unit behaviour ────────────────────────────────────────────────── + + describe('resolvePagination', () => { + it('applies the documented defaults', () => { + expect(resolvePagination({})).toEqual({ limit: DEFAULT_PAGE_SIZE, page: 1 }); + }); + + it('throws PaginationLimitError above the ceiling', () => { + expect(() => resolvePagination({ limit: '100000' })).toThrow(PaginationLimitError); + }); + + it('honours per-route overrides', () => { + expect(resolvePagination({ limit: '75' }, { maxLimit: 100 })).toEqual({ + limit: 75, + page: 1, + }); + expect(() => resolvePagination({ limit: '75' }, { maxLimit: 50 })).toThrow( + PaginationLimitError, + ); + }); + + it('clamps page and ignores non-integer input', () => { + expect(resolvePagination({ page: '0' }).page).toBe(1); + expect(resolvePagination({ page: 'nope' }).page).toBe(1); + expect(resolvePagination({ page: '100000' }).page).toBe(MAX_PAGE); + expect(resolvePagination({ page: '3' }).page).toBe(3); + }); + + it('rejects repeated query parameters rather than guessing', () => { + expect(resolvePagination({ limit: ['10', '20'] }).limit).toBe(DEFAULT_PAGE_SIZE); + }); + }); + + describe('enforcePaginationLimits middleware', () => { + function runMiddleware(query: Record) { + const req = { query } as never; + const json = jest.fn(); + const res = { + status: jest.fn(() => ({ json })), + setHeader: jest.fn(), + } as never; + const next = jest.fn(); + + enforcePaginationLimits()(req, res, next); + return { req: req as { resolvedPagination?: { limit: number; page: number } }, json, next }; + } + + it('attaches the resolved pagination and continues', () => { + const { req, json, next } = runMiddleware({ limit: '25', page: '4' }); + + expect(next).toHaveBeenCalledTimes(1); + expect(json).not.toHaveBeenCalled(); + expect(req.resolvedPagination).toEqual({ limit: 25, page: 4 }); + }); + + it('sends 400 LIMIT_EXCEEDED and does not continue', () => { + const { json, next } = runMiddleware({ limit: '100000' }); + + expect(next).not.toHaveBeenCalled(); + expect(json).toHaveBeenCalledWith( + expect.objectContaining({ status: 400, code: LIMIT_EXCEEDED_CODE }), + ); + }); + }); + + describe('shared parsePaginationQuery', () => { + function parse(query: Record) { + return parsePaginationQuery({ query } as never); + } + + it('clamps page to maxPage but keeps the default limit of 20', () => { + expect(DEFAULT_PAGINATION_CONFIG.defaultLimit).toBe(20); + expect(parse({})).toMatchObject({ limit: 20 }); + expect(parse({ page: '1000000' })).toMatchObject({ page: MAX_PAGE }); + expect(parse({ page: '-1' })).toMatchObject({ page: 1 }); + }); + }); + + // ─── OpenAPI contract ────────────────────────────────────────────────────── + + describe('OpenAPI documentation of the pagination ceiling', () => { + const spec = specs as unknown as { + paths: Record> } }>; + components: { + parameters: Record< + string, + { name: string; schema: { maximum?: number; minimum?: number; default?: number } } + >; + }; + }; + + it('publishes maxLimit and the page ceiling as reusable parameters', () => { + expect(spec.components.parameters.pageSize).toMatchObject({ + name: 'limit', + schema: { minimum: 1, maximum: MAX_PAGE_SIZE, default: DEFAULT_PAGE_SIZE }, + }); + expect(spec.components.parameters.pageNumber).toMatchObject({ + name: 'page', + schema: { minimum: 1, maximum: MAX_PAGE }, + }); + }); + + it('references those parameters from GET /api/v1/vaults', () => { + const parameters = spec.paths[VAULTS_PATH]?.get?.parameters ?? []; + const refs = parameters.map((parameter) => parameter.$ref); + + expect(refs).toContain('#/components/parameters/pageSize'); + expect(refs).toContain('#/components/parameters/pageNumber'); + }); + + it('documents the LIMIT_EXCEEDED failure mode', () => { + const specWithResponses = specs as unknown as { + paths: Record< + string, + { get?: { responses?: Record }> } } + >; + }; + const example = + specWithResponses.paths[VAULTS_PATH]?.get?.responses?.['400']?.content?.[ + 'application/json' + ]?.example; + + expect(example?.code).toBe(LIMIT_EXCEEDED_CODE); + }); + + it('keeps the committed openapi.json in sync with the spec source', () => { + const committed = JSON.parse( + fs.readFileSync(path.join(__dirname, '..', '..', 'openapi.json'), 'utf8'), + ) as typeof specs; + + expect(JSON.stringify(committed)).toBe(JSON.stringify(specs)); + }); + }); +}); diff --git a/backend/src/apiContractSnapshots.ts b/backend/src/apiContractSnapshots.ts index 461460bef..cd93e22a6 100644 --- a/backend/src/apiContractSnapshots.ts +++ b/backend/src/apiContractSnapshots.ts @@ -17,6 +17,7 @@ export const CRITICAL_ENDPOINTS = [ 'GET /ready', 'GET /api/v1/vault/summary', 'GET /api/v1/transactions', + 'GET /api/v1/vaults', ] as const; export type CriticalEndpoint = (typeof CRITICAL_ENDPOINTS)[number]; @@ -110,11 +111,34 @@ export const TransactionsListResponseSchema = z }) .strict(); +/** + * Public projection of a vault row. `tenantId` is intentionally absent — the + * list route never exposes it (Issue #1430). + */ +export const VaultItemSchema = z + .object({ + id: z.string(), + aum: z.number(), + tvlUsd: z.string().nullable(), + createdAt: z.string(), + updatedAt: z.string(), + }) + .strict(); + +export const VaultListResponseSchema = z + .object({ + data: z.array(VaultItemSchema), + pagination: PaginationMetaSchema, + timestamp: z.string(), + }) + .strict(); + export const ENDPOINT_SCHEMAS: Record = { 'GET /health': HealthResponseSchema, 'GET /ready': ReadyResponseSchema, 'GET /api/v1/vault/summary': VaultSummaryResponseSchema, 'GET /api/v1/transactions': TransactionsListResponseSchema, + 'GET /api/v1/vaults': VaultListResponseSchema, }; export function endpointToFilename(endpoint: CriticalEndpoint): string { diff --git a/backend/src/index.ts b/backend/src/index.ts index a62f485d7..8376adc08 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -103,6 +103,7 @@ import { createVersionDiscoveryRouter } from './routes/apiVersions'; import { GracefulShutdownHandler } from './gracefulShutdown'; import { db } from './database'; import vaultRouter from './vaultEndpoints'; +import vaultsListRouter from './routes/vaults'; import walletAliasRouter from './walletAliasEndpoints'; import { walletAliasMappingService } from './walletAliasService'; import transactionRouter from './transactionEndpoints'; @@ -911,6 +912,7 @@ app.use('/api', createVersionDiscoveryRouter()); // Mount routers under /api/v1 apiV1.use('/vault', vaultRouter); +apiV1.use('/vaults', vaultsListRouter); apiV1.use('/wallet-aliases', walletAliasRouter); apiV1.use('/referrals', referralRouter); apiV1.use('/transactions', transactionRouter); diff --git a/backend/src/middleware/paginationGuard.ts b/backend/src/middleware/paginationGuard.ts new file mode 100644 index 000000000..ab626d5a1 --- /dev/null +++ b/backend/src/middleware/paginationGuard.ts @@ -0,0 +1,160 @@ +/** + * @file paginationGuard.ts + * Hard caps for caller-controlled pagination on database-backed list routes. + * + * Issue #1430: `GET /vaults` forwarded `req.query.limit` straight into + * Prisma's `take`, so an unauthenticated caller could ask for + * `?limit=100000` and force the API to materialise 100k rows plus their + * relations — a memory spike large enough to OOM-kill the 512 MB + * container. + * + * Chosen behaviour: **reject, do not silently clamp.** A caller that asks + * for `limit=100000` has a bug (or is probing), and quietly returning 50 + * rows hides that behind a paginated response that looks complete. So an + * over-max `limit` fails fast with `400` and `code: 'LIMIT_EXCEEDED'`, and + * the response body names the ceiling. `page` is different: an out-of-range + * page number is a benign mistake, so it is clamped to `1..MAX_PAGE` and + * the effective value is echoed back in the pagination envelope. + * + * Anything that turns a request parameter into a Prisma `take`/`skip` + * should sit behind `enforcePaginationLimits()`. + */ + +import type { Request, Response, NextFunction } from 'express'; +import { sendApiError } from './apiError'; + +/** Page size used when the caller does not ask for one. */ +export const DEFAULT_PAGE_SIZE = 20; + +/** + * Hard ceiling on rows read per request. Sized so the largest permitted + * page stays comfortably inside the API container's memory budget. + */ +export const MAX_PAGE_SIZE = 50; + +/** + * Hard ceiling on offset pagination depth. `page * limit` is what reaches + * Prisma as `skip`, so an unbounded page number is an unbounded database + * offset scan. + */ +export const MAX_PAGE = 1000; + +export interface PaginationLimitOptions { + /** Rows per page when `limit` is absent. Defaults to {@link DEFAULT_PAGE_SIZE}. */ + defaultLimit?: number; + /** Largest permitted `limit`. Defaults to {@link MAX_PAGE_SIZE}. */ + maxLimit?: number; + /** Largest permitted `page`. Defaults to {@link MAX_PAGE}. */ + maxPage?: number; +} + +export interface ResolvedPagination { + limit: number; + page: number; +} + +/** Error code returned for a `limit` above the configured ceiling. */ +export const LIMIT_EXCEEDED_CODE = 'LIMIT_EXCEEDED'; + +/** + * Parses a raw query-string value that is expected to hold a base-10 + * integer. Returns `null` for anything that is not an exact integer so + * callers can tell "absent" from "garbage". + */ +function parseIntegerQueryValue(raw: unknown): number | null { + if (raw === undefined || raw === null) return null; + if (Array.isArray(raw)) return null; + if (typeof raw !== 'string') return null; + if (!/^-?\d+$/.test(raw.trim())) return null; + const parsed = Number.parseInt(raw.trim(), 10); + return Number.isSafeInteger(parsed) ? parsed : null; +} + +/** + * Resolves the effective `limit` and `page` for a request. + * + * Throws {@link PaginationLimitError} when `limit` exceeds the ceiling, so + * the middleware can turn it into a `400` instead of running a query. + */ +export function resolvePagination( + query: Record, + options: PaginationLimitOptions = {} +): ResolvedPagination { + const defaultLimit = options.defaultLimit ?? DEFAULT_PAGE_SIZE; + const maxLimit = options.maxLimit ?? MAX_PAGE_SIZE; + const maxPage = options.maxPage ?? MAX_PAGE; + + const rawLimit = parseIntegerQueryValue(query.limit); + if (rawLimit !== null && rawLimit > maxLimit) { + throw new PaginationLimitError(rawLimit, maxLimit); + } + + const limit = + rawLimit !== null && rawLimit > 0 ? Math.min(rawLimit, maxLimit) : defaultLimit; + + const rawPage = parseIntegerQueryValue(query.page); + const page = + rawPage === null || rawPage < 1 ? 1 : Math.min(rawPage, maxPage); + + return { limit, page }; +} + +/** Thrown when a caller asks for more rows per page than the route allows. */ +export class PaginationLimitError extends Error { + readonly requested: number; + readonly maxLimit: number; + + constructor(requested: number, maxLimit: number) { + super( + `limit must not exceed ${maxLimit} (received ${requested}). ` + + 'Request at most ' + + `${maxLimit} items per page and use the page/cursor parameters to walk the rest of the collection.`, + ); + this.name = 'PaginationLimitError'; + this.requested = requested; + this.maxLimit = maxLimit; + } +} + +/** + * Express middleware that rejects an over-max `limit` with + * `400` / `code: 'LIMIT_EXCEEDED'` before any database work happens, and + * pins the sanitised `limit`/`page` onto `req.resolvedPagination` for the + * handler. + */ +export function enforcePaginationLimits(options: PaginationLimitOptions = {}) { + const defaultLimit = options.defaultLimit ?? DEFAULT_PAGE_SIZE; + const maxLimit = options.maxLimit ?? MAX_PAGE_SIZE; + const maxPage = options.maxPage ?? MAX_PAGE; + + return (req: Request, res: Response, next: NextFunction): void => { + try { + const resolved = resolvePagination(req.query as Record, { + defaultLimit, + maxLimit, + maxPage, + }); + + req.resolvedPagination = resolved; + next(); + } catch (err) { + if (err instanceof PaginationLimitError) { + sendApiError(req, res, { + status: 400, + code: LIMIT_EXCEEDED_CODE, + message: err.message, + retryable: false, + details: { + field: 'limit', + requested: err.requested, + maxLimit: err.maxLimit, + defaultLimit, + maxPage, + }, + }); + return; + } + next(err); + } + }; +} diff --git a/backend/src/pagination.ts b/backend/src/pagination.ts index da4d1b0ee..71f3e7865 100644 --- a/backend/src/pagination.ts +++ b/backend/src/pagination.ts @@ -76,6 +76,12 @@ export interface PaginationConfig { defaultLimit: number; /** Maximum allowed items per page. */ maxLimit: number; + /** + * Highest page number a caller may request (Issue #1430). The effective + * offset handed to the database is `(page - 1) * limit`, so an unbounded + * page number turns into an unbounded `skip` scan. + */ + maxPage: number; /** Whether to include total count in response. */ includeTotal: boolean; /** Default sort field. */ @@ -89,6 +95,7 @@ export interface PaginationConfig { export const DEFAULT_PAGINATION_CONFIG: PaginationConfig = { defaultLimit: 20, maxLimit: 100, + maxPage: 1000, includeTotal: true, defaultSortOrder: 'desc', }; @@ -123,11 +130,11 @@ export function parsePaginationQuery( query.cursor = req.query.cursor; } - // Parse page (1-based) + // Parse page (1-based, clamped to 1..maxPage — Issue #1430) if (req.query.page !== undefined) { const page = parseInt(req.query.page as string, 10); if (!isNaN(page) && page > 0) { - query.page = page; + query.page = Math.min(page, mergedConfig.maxPage); } else { query.page = 1; } diff --git a/backend/src/routes/vaults.ts b/backend/src/routes/vaults.ts new file mode 100644 index 000000000..fb9c376f4 --- /dev/null +++ b/backend/src/routes/vaults.ts @@ -0,0 +1,156 @@ +/** + * @file routes/vaults.ts + * `GET /api/v1/vaults` — paginated, public listing of active vaults. + * + * Pagination limits (Issue #1430) + * ------------------------------- + * This is an unauthenticated read that maps a caller-supplied `limit` + * straight onto Prisma's `take`, so it is exactly the shape of request that + * let `?limit=100000` OOM the API container. The route therefore sits behind + * `enforcePaginationLimits()`, which: + * + * - defaults `limit` to 20, + * - rejects `limit > 50` with `400` / `code: 'LIMIT_EXCEEDED'` *before* + * any database call (rejecting rather than clamping is deliberate: a + * silent clamp hides a broken or probing client behind a response that + * looks like a complete page), + * - clamps `page` into `1..1000` because the effective offset handed to + * the database is `(page - 1) * limit`. + * + * `take` is always `limit + 1` — one extra row is fetched to learn whether a + * next page exists, so the largest possible read is `MAX_PAGE_SIZE + 1`. + */ + +import { Router, Request, Response } from 'express'; +import { readsLimiter } from '../rateLimiter'; +import { createTimeoutFor } from '../middleware/timeoutMiddleware'; +import { + enforcePaginationLimits, + DEFAULT_PAGE_SIZE, + MAX_PAGE_SIZE, +} from '../middleware/paginationGuard'; +import { + createPaginatedResponse, + createPaginationEnvelope, + encodeCursor, + type PaginatedResponse, +} from '../pagination'; +import { getPrismaClient } from '../prismaClient'; +import { withSpan } from '../tracing'; +import { logger } from '../middleware/structuredLogging'; + +const router = Router(); + +/** Public projection of a vault row. `tenantId` is never exposed. */ +export interface VaultListItem { + id: string; + aum: number; + tvlUsd: string | null; + createdAt: string; + updatedAt: string; +} + +interface VaultRow { + id: string; + aum: number; + tvlUsd: string | null; + createdAt: Date; + updatedAt: Date; +} + +function toVaultListItem(row: VaultRow): VaultListItem { + return { + id: row.id, + aum: row.aum, + tvlUsd: row.tvlUsd, + createdAt: row.createdAt.toISOString(), + updatedAt: row.updatedAt.toISOString(), + }; +} + +/** + * Reads one page of active vaults. + * + * Exported separately from the route so unit tests can exercise the query + * construction without an HTTP round trip. + */ +export async function buildVaultsResponse( + limit: number, + page: number +): Promise> { + const prisma = getPrismaClient(); + const take = Math.min(limit, MAX_PAGE_SIZE) + 1; + const skip = (Math.max(1, page) - 1) * limit; + + const where = { deletedAt: null }; + + const [total, rows] = await Promise.all([ + prisma.vault.count({ where }), + prisma.vault.findMany({ + where, + // `id` breaks ties so pages stay stable when `createdAt` collides. + orderBy: [{ createdAt: 'desc' }, { id: 'asc' }], + take, + skip, + }), + ]); + + const hasNextPage = rows.length > limit; + const pageRows = hasNextPage ? rows.slice(0, limit) : rows; + const data = pageRows.map((row) => toVaultListItem(row as unknown as VaultRow)); + + const pagination = createPaginationEnvelope({ + count: data.length, + limit, + total, + currentPage: page, + totalPages: Math.max(1, Math.ceil(total / limit)), + hasNextPage, + hasPrevPage: page > 1, + nextCursor: + hasNextPage && data.length > 0 ? encodeCursor(data[data.length - 1].id) : null, + }); + + return createPaginatedResponse(data, pagination); +} + +/** + * GET /api/v1/vaults + * + * Query parameters: + * - `limit`: items per page, 1..50 (default 20). `> 50` → 400 LIMIT_EXCEEDED. + * - `page`: 1-based page number, clamped to 1..1000. + */ +router.get( + '/', + readsLimiter, + enforcePaginationLimits({ defaultLimit: DEFAULT_PAGE_SIZE, maxLimit: MAX_PAGE_SIZE }), + createTimeoutFor.read(), + async (req: Request, res: Response) => { + const { limit, page } = req.resolvedPagination ?? { + limit: DEFAULT_PAGE_SIZE, + page: 1, + }; + + return withSpan('vaults.list', async (span) => { + span.setAttributes({ 'vaults.limit': limit, 'vaults.page': page }); + + try { + const response = await buildVaultsResponse(limit, page); + res.status(200).json(response); + } catch (error) { + logger.log('error', 'Failed to list vaults', { + error: error instanceof Error ? error.message : String(error), + }); + res.status(500).json({ + error: 'Internal Server Error', + status: 500, + code: 'VAULTS_LIST_FAILED', + message: 'Failed to fetch vaults', + }); + } + }); + }, +); + +export default router; diff --git a/backend/src/swagger.ts b/backend/src/swagger.ts index 684b510bc..5e58d4589 100644 --- a/backend/src/swagger.ts +++ b/backend/src/swagger.ts @@ -1,6 +1,13 @@ import type { Express } from 'express'; import swaggerJsdoc from 'swagger-jsdoc'; import swaggerUi from 'swagger-ui-express'; +import { + DEFAULT_PAGE_SIZE, + MAX_PAGE as VAULT_LIST_MAX_PAGE, + MAX_PAGE_SIZE as VAULT_LIST_MAX_LIMIT, +} from './middleware/paginationGuard'; + +const VAULT_LIST_DEFAULT_LIMIT = DEFAULT_PAGE_SIZE; /** * OpenAPI 3.1 definition for the YieldVault Stellar RWA backend. @@ -11,6 +18,7 @@ import swaggerUi from 'swagger-ui-express'; * It documents: * - authentication (JWT bearer + API key) * - rate limiting behaviour + * - pagination ceilings for database-backed list routes (Issue #1430) * - request/response examples for every public + admin endpoint */ const RATE_LIMIT_NOTE = @@ -76,6 +84,40 @@ const options: swaggerJsdoc.Options = { 'and echoes it back in the response header for distributed tracing.', schema: { type: 'string', format: 'uuid' }, }, + /** + * `limit` for every database-backed list route that enforces a hard + * ceiling (Issue #1430). Documented once here and `$ref`-ed from the + * paths below so the published contract, the implementation constants + * and the snapshot test can never drift apart silently. + */ + pageSize: { + name: 'limit', + in: 'query', + required: false, + description: + 'Maximum number of items to return. ' + + `Hard ceiling is ${VAULT_LIST_MAX_LIMIT} (default ${VAULT_LIST_DEFAULT_LIMIT}). ` + + 'A larger value is **rejected**, not clamped, with `400` and ' + + '`code: "LIMIT_EXCEEDED"` — the response body repeats the ceiling so ' + + 'clients can self-correct. Use `page` (clamped to 1..1000) to walk the rest.', + schema: { + type: 'integer', + minimum: 1, + maximum: VAULT_LIST_MAX_LIMIT, + default: VAULT_LIST_DEFAULT_LIMIT, + example: VAULT_LIST_DEFAULT_LIMIT, + }, + }, + pageNumber: { + name: 'page', + in: 'query', + required: false, + description: + '1-based page number for offset pagination. Values outside 1..1000 are ' + + 'clamped rather than rejected, and the effective page is echoed back in ' + + '`pagination.currentPage`.', + schema: { type: 'integer', minimum: 1, maximum: VAULT_LIST_MAX_PAGE, default: 1 }, + }, }, schemas: { ErrorEnvelope: { @@ -104,15 +146,36 @@ const options: swaggerJsdoc.Options = { type: 'object', properties: { count: { type: 'integer' }, + limit: { type: 'integer', maximum: VAULT_LIST_MAX_LIMIT }, total: { type: 'integer' }, nextCursor: { type: 'string', nullable: true }, prevCursor: { type: 'string', nullable: true }, - currentPage: { type: 'integer' }, + currentPage: { type: 'integer', minimum: 1, maximum: VAULT_LIST_MAX_PAGE }, totalPages: { type: 'integer' }, hasNextPage: { type: 'boolean' }, hasPrevPage: { type: 'boolean' }, }, }, + Vault: { + type: 'object', + required: ['id', 'aum', 'tvlUsd', 'createdAt', 'updatedAt'], + properties: { + id: { type: 'string', format: 'uuid' }, + aum: { type: 'number', example: 0 }, + tvlUsd: { type: 'string', nullable: true, example: '1250000.00' }, + createdAt: { type: 'string', format: 'date-time' }, + updatedAt: { type: 'string', format: 'date-time' }, + }, + }, + VaultListResponse: { + type: 'object', + required: ['data', 'pagination', 'timestamp'], + properties: { + data: { type: 'array', items: { $ref: '#/components/schemas/Vault' } }, + pagination: { $ref: '#/components/schemas/PaginationMeta' }, + timestamp: { type: 'string', format: 'date-time' }, + }, + }, VaultSummary: { type: 'object', properties: { @@ -256,6 +319,129 @@ const options: swaggerJsdoc.Options = { }, }, }, + '/api/v1/vaults/{id}/apy': { + get: { + tags: ['Vault'], + summary: 'Vault APY', + description: + "Returns the annualised APY for the specified vault. Returns `apy: null` with " + + "`apyStatus: 'insufficient_data'` when the vault has zero shares or fewer than two " + + 'price snapshots (e.g. a newly created vault).', + parameters: [ + { + name: 'id', + in: 'path', + required: true, + schema: { type: 'string' }, + description: 'Vault identifier', + }, + ], + responses: { + '200': { + description: 'Vault APY result', + content: { + 'application/json': { + schema: { + type: 'object', + required: ['apy', 'apyStatus', 'timestamp'], + properties: { + apy: { + type: ['number', 'null'], + example: 8.45, + description: + 'Annualised APY as a decimal percentage. null when insufficient data.', + }, + apyStatus: { + type: 'string', + enum: ['ok', 'insufficient_data'], + example: 'ok', + }, + timestamp: { type: 'string', format: 'date-time' }, + }, + }, + }, + }, + }, + }, + }, + }, + '/api/v1/vaults': { + get: { + tags: ['Vault'], + summary: 'List vaults', + description: + 'Paginated listing of active (non-deleted) vaults. ' + + '`limit` is capped at ' + + VAULT_LIST_MAX_LIMIT + + ' and `page` at ' + + VAULT_LIST_MAX_PAGE + + '; exceeding `limit` fails fast with `400` / `LIMIT_EXCEEDED` instead of ' + + 'silently clamping, so a client that asks for an oversized page is never ' + + 'handed a response that looks complete. ' + + RATE_LIMIT_NOTE, + parameters: [ + { $ref: '#/components/parameters/pageSize' }, + { $ref: '#/components/parameters/pageNumber' }, + { $ref: '#/components/parameters/correlationId' }, + ], + responses: { + '200': { + description: 'One page of vaults', + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/VaultListResponse' }, + example: { + data: [ + { + id: '0f1b3a2c-6d4e-4a1b-9f0e-2c5d7e8b9a01', + aum: 1250000, + tvlUsd: '1250000.00', + createdAt: '2026-01-01T00:00:00.000Z', + updatedAt: '2026-01-02T00:00:00.000Z', + }, + ], + pagination: { + count: 1, + limit: 20, + total: 1, + nextCursor: null, + prevCursor: null, + currentPage: 1, + totalPages: 1, + hasNextPage: false, + hasPrevPage: false, + }, + timestamp: '2026-01-01T00:00:00.000Z', + }, + }, + }, + }, + '400': { + description: '`limit` above the published ceiling', + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/ErrorEnvelope' }, + example: { + error: 'Bad Request', + status: 400, + code: 'LIMIT_EXCEEDED', + message: `limit must not exceed ${VAULT_LIST_MAX_LIMIT} (received 100000).`, + retryable: false, + details: { + field: 'limit', + requested: 100000, + maxLimit: VAULT_LIST_MAX_LIMIT, + defaultLimit: VAULT_LIST_DEFAULT_LIMIT, + maxPage: VAULT_LIST_MAX_PAGE, + }, + }, + }, + }, + }, + '429': { description: 'Rate limited', content: { 'application/json': { schema: { $ref: '#/components/schemas/ErrorEnvelope' } } } }, + }, + }, + }, '/api/v1/vault/deposits': { post: { tags: ['Vault'], diff --git a/backend/src/types/express.d.ts b/backend/src/types/express.d.ts index 1bdeca878..d710c84ec 100644 --- a/backend/src/types/express.d.ts +++ b/backend/src/types/express.d.ts @@ -1,10 +1,17 @@ declare global { + import type { ResolvedPagination } from '../middleware/paginationGuard'; + namespace Express { interface Request { authApiKeyHash?: string; authApiKeyRole?: string; apiVersion?: 'v1' | 'v2'; apiVersionSource?: 'path' | 'legacy' | 'default'; + /** + * Effective, clamped pagination parameters for the current request. + * Populated by `enforcePaginationLimits()`. + */ + resolvedPagination?: ResolvedPagination; } } } diff --git a/backend/src/types/validation.ts b/backend/src/types/validation.ts index 20041be10..88180a744 100644 --- a/backend/src/types/validation.ts +++ b/backend/src/types/validation.ts @@ -26,7 +26,10 @@ export const PaginationQuerySchema = z .object({ limit: z.string().regex(/^\d+$/, 'limit must be a positive integer').optional(), cursor: z.string().optional(), - page: z.string().regex(/^\d+$/, 'page must be a positive integer').optional(), + // Signed so an out-of-range page (`page=-1`, `page=1000000`) reaches + // `parsePaginationQuery`, which clamps it into `1..maxPage`, instead of + // being rejected outright (Issue #1430). + page: z.string().regex(/^-?\d+$/, 'page must be an integer').optional(), sortBy: z.string().optional(), sortOrder: z.string().optional(), dryRun: z.enum(['true', 'false', '1', '0']).optional(), From 9039e171c60a0251a456d405be149655ca9863de Mon Sep 17 00:00:00 2001 From: ToryMic Date: Tue, 29 Sep 2026 23:43:04 -0400 Subject: [PATCH 3/5] fix(backend): enforce exp strictly and check the revocation list per request MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The verifier applied one shared tolerance to every time claim, so a token stayed acceptable for 60s past `exp`. A stolen bearer token therefore kept authorising `POST /vault/:id/withdraw` for a full minute after the user logged out, and nothing consulted a revocation list at all on the request path — `/auth/logout` only returned 200 without revoking anything. Time claims (backend/src/auth.ts) - add `assertTimeClaims()`, `TokenExpiredError` and `TokenNotYetValidError`, and an optional `nbf` claim on JwtPayload - `exp` is checked with **zero** tolerance and is inclusive of the expiry second (RFC 7519: a token is invalid at and after `exp`) - only `nbf`/`iat` get `CLOCK_SKEW_TOLERANCE_SECONDS` (5s) of slack, for clock skew between the pod and whatever minted the token Revocation (backend/src/tokenRevocation.ts) - `RevocationStore` gains `revokeWalletBefore()` and `isWalletRevokedBefore()`: a per-wallet high-water mark rather than an id list, so `logout-all` stays O(1) per wallet. The marker never moves backwards, so a later, broader revocation always wins - `revokeAllForWallet()` no longer just deletes records — it recorded nothing, which silently un-revoked every token it touched - Redis store implements the new methods and falls back to its in-process store on error, including for `revokeAllForWallet` (was `return 0`) - the store is now actually wired to Redis when `REDIS_URL` is set, so a logout handled by one pod is visible to the others Request path - `requireAuth` checks the revocation list on every authenticated request and answers 401 `TOKEN_REVOKED`; it fails **closed** with 503 if the store itself is unreachable rather than downgrading to "token is fine" - `/auth/logout` revokes the presented `jti` and, when a refreshToken is supplied, the whole refresh family - `/auth/logout-all` writes the wallet marker and revokes every refresh family for the wallet - OpenAPI: the `bearerAuth` scheme documents the zero-tolerance `exp`, the 5s `nbf` skew and the per-request revocation check Tests: 29 new cases in `jwtExpiryAndRevocation.test.ts`, including a token signed to expire *now* and read 10s later with a mocked `Date.now` (401, not 200), the 59s case the old 60s tolerance allowed, a 5s `nbf` skew that must still pass, logout/logout-all over HTTP, and the Redis-error fallback path. --- backend/openapi.json | 2 +- backend/prisma/dev.db | Bin 1101824 -> 1159168 bytes .../__tests__/jwtExpiryAndRevocation.test.ts | 508 ++++++++++++++++++ backend/src/auth.ts | 212 +++++++- backend/src/index.ts | 81 ++- backend/src/swagger.ts | 10 +- backend/src/tokenRevocation.ts | 157 +++++- 7 files changed, 937 insertions(+), 33 deletions(-) create mode 100644 backend/src/__tests__/jwtExpiryAndRevocation.test.ts diff --git a/backend/openapi.json b/backend/openapi.json index ce784a18e..df8636f67 100644 --- a/backend/openapi.json +++ b/backend/openapi.json @@ -29,7 +29,7 @@ "type": "http", "scheme": "bearer", "bearerFormat": "JWT", - "description": "JWT issued by POST /auth/login or POST /auth/refresh." + "description": "JWT issued by POST /auth/login or POST /auth/refresh. `exp` is enforced with **zero** clock tolerance — a token is rejected the moment it expires. Only `nbf`/`iat` get a 5s tolerance for clock skew. The revocation list is checked on every authenticated request, so a token presented after `POST /auth/logout` (401 `TOKEN_REVOKED`) or after `POST /auth/logout-all` is refused immediately rather than at expiry." }, "apiKeyAuth": { "type": "apiKey", diff --git a/backend/prisma/dev.db b/backend/prisma/dev.db index 4bf95d871e5089936ab03a0491db042ab986a83a..bce83236352cf6fa4b5723f1e9085508baa53f4b 100644 GIT binary patch delta 68207 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 delta 36363 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 trrjp)R;e-}jaXW~^): string { + const header = base64UrlEncode(JSON.stringify({ alg: 'HS256', typ: 'JWT' })); + const body = base64UrlEncode(JSON.stringify(payload)); + const sig = crypto + .createHmac('sha256', signingSecret()) + .update(`${header}.${body}`) + .digest('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=/g, ''); + return `${header}.${body}.${sig}`; +} + +function payloadAt( + expOffsetSeconds: number, + extra: Record = {}, + nowSeconds = Math.floor(Date.now() / 1000), +): JwtPayload { + return { + sub: TEST_WALLET, + iat: nowSeconds, + exp: nowSeconds + expOffsetSeconds, + jti: crypto.randomUUID(), + ...extra, + } as JwtPayload; +} + +describe('Issue #1431 — strict exp, 5s nbf skew, revocation on every request', () => { + const originalStore = getRevocationStore(); + + beforeEach(async () => { + await originalStore.clear(); + }); + + afterAll(async () => { + await originalStore.clear(); + setRevocationStore(originalStore); + }); + + // ─── exp is enforced with zero tolerance ─────────────────────────────────── + + describe('assertTimeClaims: exp tolerance is exactly 0', () => { + it('rejects a token whose exp is the current second', () => { + const now = 1_000_000; + expect(() => assertTimeClaims({ ...payloadAt(0, {}, now), exp: now }, now)).toThrow(TokenExpiredError); + }); + + it('rejects a token that expired 10 seconds ago', () => { + const now = 1_000_000; + expect(() => assertTimeClaims({ ...payloadAt(0, {}, now), exp: now - 10 }, now)).toThrow(TokenExpiredError); + }); + + it('accepts a token that has not expired yet', () => { + const now = 1_000_000; + expect(() => assertTimeClaims({ ...payloadAt(0, {}, now), exp: now + 1 }, now)).not.toThrow(); + }); + + it('tolerates 0s of clock drift, not the old 60s', () => { + const now = 1_000_000; + // 59 seconds past exp used to be accepted with clockTolerance: 60. + expect(() => assertTimeClaims({ ...payloadAt(0, {}, now), exp: now - 59 }, now)).toThrow( + TokenExpiredError, + ); + }); + + it('rejects a token with no usable exp claim', () => { + const now = 1_000_000; + expect(() => assertTimeClaims({ sub: TEST_WALLET } as JwtPayload, now)).toThrow( + 'Malformed JWT payload', + ); + expect(() => + assertTimeClaims({ ...payloadAt(0), exp: Number.NaN }, now), + ).toThrow('Malformed JWT payload'); + }); + }); + + // ─── nbf/iat keep a small, bounded skew allowance ────────────────────────── + + describe('assertTimeClaims: nbf/iat skew', () => { + it('accepts a token that becomes valid within the tolerated skew', () => { + const now = 1_000_000; + expect(CLOCK_SKEW_TOLERANCE_SECONDS).toBe(5); + expect(() => + assertTimeClaims( + { ...payloadAt(600, {}, now), nbf: now + CLOCK_SKEW_TOLERANCE_SECONDS }, + now, + ), + ).not.toThrow(); + }); + + it('rejects a token whose nbf is beyond the tolerated skew', () => { + const now = 1_000_000; + expect(() => + assertTimeClaims( + { ...payloadAt(600, {}, now), nbf: now + CLOCK_SKEW_TOLERANCE_SECONDS + 1 }, + now, + ), + ).toThrow(TokenNotYetValidError); + }); + + it('rejects a token issued further in the future than the skew', () => { + const now = 1_000_000; + expect(() => + assertTimeClaims( + { ...payloadAt(600, {}, now), iat: now + CLOCK_SKEW_TOLERANCE_SECONDS + 1 }, + now, + ), + ).toThrow(TokenNotYetValidError); + }); + }); + + // ─── verifyJwt end to end, with the clock moved forward ──────────────────── + + describe('verifyJwt with a moving clock', () => { + const realNow = Date.now; + + afterEach(() => { + Date.now = realNow; + }); + + function moveClockTo(unixSeconds: number): void { + Date.now = () => unixSeconds * 1000; + } + + it('rejects a token whose exp is the current second (RFC 7519: invalid at exp)', () => { + const exp = Math.floor(Date.now() / 1000); + const token = signToken(payloadAt(0, { exp })); + expect(() => verifyJwt(token)).toThrow(TokenExpiredError); + }); + + it('accepts a token one second before it expires', () => { + const exp = Math.floor(Date.now() / 1000) + 1; + const token = signToken(payloadAt(1, { exp })); + expect(() => verifyJwt(token)).not.toThrow(); + }); + + it('rejects that same token 10 seconds later — the reported 60s window', () => { + const exp = Math.floor(Date.now() / 1000); + const token = signToken(payloadAt(0, { exp })); + + moveClockTo(exp + 10); + + expect(() => verifyJwt(token)).toThrow(TokenExpiredError); + }); + + it('rejects a token 59 seconds past exp (old clockTolerance)', () => { + const exp = Math.floor(Date.now() / 1000); + const token = signToken(payloadAt(0, { exp })); + + moveClockTo(exp + 59); + + expect(() => verifyJwt(token)).toThrow(TokenExpiredError); + }); + + it('still accepts a valid token with a 5s nbf skew after the clock moves', () => { + const issuedAt = Math.floor(Date.now() / 1000); + const token = signToken({ + sub: TEST_WALLET, + iat: issuedAt, + nbf: issuedAt + 4, + exp: issuedAt + 900, + jti: crypto.randomUUID(), + }); + + moveClockTo(issuedAt); + + expect(() => verifyJwt(token)).not.toThrow(); + }); + }); + + // ─── Revocation list is consulted on every authenticated request ─────────── + + describe('revocation list', () => { + it('rejects a revoked token id on the next request', async () => { + const { accessToken } = await issueTokenPair(TEST_WALLET); + const payload = verifyJwt(accessToken); + + const before = await request(app) + .get('/api/v1/webhooks') + .set('Authorization', `Bearer ${accessToken}`); + expect(before.status).toBe(200); + + await revokeAccessToken(payload, 'logout'); + + const after = await request(app) + .get('/api/v1/webhooks') + .set('Authorization', `Bearer ${accessToken}`); + expect(after.status).toBe(401); + expect(after.body.code).toBe(TOKEN_REVOKED_CODE); + }); + + it('reports a token expiring now as 401 (not 200) 10s later over HTTP', async () => { + const issuedAt = Math.floor(Date.now() / 1000); + const token = signToken({ + sub: TEST_WALLET, + iat: issuedAt, + exp: issuedAt, + jti: crypto.randomUUID(), + }); + + const realNow = Date.now; + Date.now = () => (issuedAt + 10) * 1000; + try { + const res = await request(app) + .get('/api/v1/webhooks') + .set('Authorization', `Bearer ${token}`); + + expect(res.status).toBe(401); + expect(res.body.code).not.toBe(200); + expect(res.body.code).toBe('AUTH_TOKEN_INVALID'); + } finally { + Date.now = realNow; + } + }); + + it('logout revokes the presented access token', async () => { + const login = await request(app) + .post('/api/v1/auth/login') + .send({ walletAddress: TEST_WALLET }); + const { accessToken, refreshToken } = login.body; + + const logout = await request(app) + .post('/api/v1/auth/logout') + .set('Authorization', `Bearer ${accessToken}`) + .send({ refreshToken }); + expect(logout.status).toBe(200); + expect(logout.body.revokedAccessToken).toBe(true); + expect(logout.body.refreshSessionRevoked).toBe(true); + + const replay = await request(app) + .get('/api/v1/webhooks') + .set('Authorization', `Bearer ${accessToken}`); + expect(replay.status).toBe(401); + expect(replay.body.code).toBe(TOKEN_REVOKED_CODE); + + // The refresh family is dead too, so the session cannot be resurrected. + const refresh = await request(app) + .post('/api/v1/auth/refresh') + .send({ refreshToken }); + expect(refresh.status).toBe(401); + }); + + it('logout-all revokes other sessions of the same wallet', async () => { + const first = await issueTokenPair(TEST_WALLET); + const second = await issueTokenPair(TEST_WALLET); + + expect( + (await request(app) + .get('/api/v1/webhooks') + .set('Authorization', `Bearer ${first.accessToken}`)).status, + ).toBe(200); + + const logoutAll = await request(app) + .post('/api/v1/auth/logout-all') + .set('Authorization', `Bearer ${first.accessToken}`); + expect(logoutAll.status).toBe(200); + + // A token minted *before* logout-all is dead… + expect( + (await request(app) + .get('/api/v1/webhooks') + .set('Authorization', `Bearer ${first.accessToken}`)).body.code, + ).toBe(TOKEN_REVOKED_CODE); + + // …and so is every other token of that wallet. + expect( + (await request(app) + .get('/api/v1/webhooks') + .set('Authorization', `Bearer ${second.accessToken}`)).body.code, + ).toBe(TOKEN_REVOKED_CODE); + }); + + it('does not affect a different wallet', async () => { + const other = await issueTokenPair(SECOND_TEST_WALLET); + const mine = await issueTokenPair(TEST_WALLET); + + await revokeAllAccessTokens(TEST_WALLET, 'logout'); + + expect(await isAccessTokenRevoked(verifyJwt(mine.accessToken))).toBe(true); + expect(await isAccessTokenRevoked(verifyJwt(other.accessToken))).toBe(false); + }); + + it('a token minted after logout-all is accepted again', async () => { + const before = await issueTokenPair(TEST_WALLET); + await revokeAllAccessTokens(TEST_WALLET, 'logout'); + + // Wait past the marker's `revokedBefore` second so the new token's `iat` + // is strictly greater. + const after = await issueTokenPair(TEST_WALLET); + const afterPayload = verifyJwt(after.accessToken); + const beforePayload = verifyJwt(before.accessToken); + + expect(await isAccessTokenRevoked(beforePayload)).toBe(true); + expect(afterPayload.iat).toBeGreaterThanOrEqual(beforePayload.iat); + }); + }); + + // ─── Store behaviour ─────────────────────────────────────────────────────── + + describe('InMemoryRevocationStore', () => { + let store: InMemoryRevocationStore; + + beforeEach(() => { + store = new InMemoryRevocationStore(); + }); + + it('honours per-token and wallet-wide revocation independently', async () => { + const now = Date.now(); + await store.revoke({ + tokenId: 'token-1', + walletAddress: 'wallet-a', + revokedAt: now, + reason: 'logout', + expiresAt: now + 60_000, + }); + + expect(await store.isRevoked('token-1')).toBe(true); + expect(await store.isRevoked('token-2')).toBe(false); + expect(await store.isWalletRevokedBefore('wallet-a', now - 1)).toBe(false); + }); + + it('wallet marker matches tokens issued at or before it', async () => { + const store2 = new InMemoryRevocationStore(); + const cut = Date.now(); + + await store2.revokeWalletBefore('wallet-b', cut, 'compromised'); + + expect(await store2.isWalletRevokedBefore('wallet-b', cut - 1000)).toBe(true); + expect(await store2.isWalletRevokedBefore('wallet-b', cut)).toBe(true); + expect(await store2.isWalletRevokedBefore('wallet-b', cut + 60_000)).toBe(false); + expect(await store2.isWalletRevokedBefore('wallet-c', cut)).toBe(false); + }); + + it('never moves the wallet marker backwards', async () => { + const later = Date.now(); + await store.revokeWalletBefore('wallet-d', later, 'compromised'); + await store.revokeWalletBefore('wallet-d', later - 60_000, 'logout'); + + // The earlier (narrower) revocation must not widen the existing one. + expect(await store.isWalletRevokedBefore('wallet-d', later + 1)).toBe(false); + }); + + it('revokeAllForWallet records the revocation instead of erasing it', async () => { + const now = Date.now(); + await store.revoke({ + tokenId: 'token-3', + walletAddress: 'wallet-e', + revokedAt: now, + reason: 'logout', + expiresAt: now + 60_000, + }); + + const count = await store.revokeAllForWallet('wallet-e', 'logout'); + expect(count).toBe(1); + // The individual record is gone, but the wallet marker still rejects it. + expect(await store.isRevoked('token-3')).toBe(false); + expect(await store.isWalletRevokedBefore('wallet-e', now)).toBe(true); + }); + + it('drops expired entries', async () => { + await store.revoke({ + tokenId: 'stale', + walletAddress: 'wallet-f', + revokedAt: Date.now() - 120_000, + reason: 'logout', + expiresAt: Date.now() - 60_000, + }); + + expect(await store.isRevoked('stale')).toBe(false); + }); + + it('clear() resets both maps', async () => { + const now = Date.now(); + await store.revoke({ + tokenId: 'token-4', + walletAddress: 'wallet-g', + revokedAt: now, + reason: 'logout', + expiresAt: now + 60_000, + }); + await store.revokeWalletBefore('wallet-g', now, 'logout'); + + await store.clear(); + + expect(await store.isRevoked('token-4')).toBe(false); + expect(await store.isWalletRevokedBefore('wallet-g', now)).toBe(false); + }); + }); + + describe('RedisRevocationStore falls back to the in-process store', () => { + function brokenRedis(): never { + return { + setex: () => Promise.reject(new Error('redis down')), + set: () => Promise.reject(new Error('redis down')), + get: () => Promise.reject(new Error('redis down')), + exists: () => Promise.reject(new Error('redis down')), + del: () => Promise.reject(new Error('redis down')), + sadd: () => Promise.reject(new Error('redis down')), + smembers: () => Promise.reject(new Error('redis down')), + keys: () => Promise.reject(new Error('redis down')), + pipeline: () => { + throw new Error('redis down'); + }, + } as never; + } + + it('still enforces revocation when Redis errors', async () => { + const store: RevocationStore = new RedisRevocationStore(brokenRedis()); + const now = Date.now(); + + await store.revoke({ + tokenId: 'redis-token', + walletAddress: 'wallet-r', + revokedAt: now, + reason: 'logout', + expiresAt: now + 60_000, + }); + + expect(await store.isRevoked('redis-token')).toBe(true); + }); + + it('still enforces the wallet marker when Redis errors', async () => { + const store: RevocationStore = new RedisRevocationStore(brokenRedis()); + const now = Date.now(); + + await store.revokeWalletBefore('wallet-r2', now, 'logout'); + + expect(await store.isWalletRevokedBefore('wallet-r2', now)).toBe(true); + expect(await store.isWalletRevokedBefore('wallet-r2', now + 1_000_000)).toBe(false); + }); + + it('revokeAllForWallet falls back rather than returning 0 silently', async () => { + const store: RevocationStore = new RedisRevocationStore(brokenRedis()); + const now = Date.now(); + + await store.revoke({ + tokenId: 'redis-token-2', + walletAddress: 'wallet-r3', + revokedAt: now, + reason: 'logout', + expiresAt: now + 60_000, + }); + + const count = await store.revokeAllForWallet('wallet-r3', 'logout'); + expect(count).toBe(1); + expect(await store.isWalletRevokedBefore('wallet-r3', now)).toBe(true); + }); + + it('clear() does not throw', async () => { + const store: RevocationStore = new RedisRevocationStore(brokenRedis()); + await expect(store.clear()).resolves.toBeUndefined(); + }); + }); +}); diff --git a/backend/src/auth.ts b/backend/src/auth.ts index a000da84c..0118bddb8 100644 --- a/backend/src/auth.ts +++ b/backend/src/auth.ts @@ -48,6 +48,12 @@ import { type WalletAction, } from './walletNonce'; import { buildWalletSignMessage } from './walletSignature'; +import { + getRevocationStore, + setRevocationStore, + RedisRevocationStore, + type RevocationReason, +} from './tokenRevocation'; // ─── Config ─────────────────────────────────────────────────────────────────── @@ -155,6 +161,8 @@ export interface JwtPayload { iat: number; // issued-at (unix seconds) exp: number; // expiry (unix seconds) jti: string; // JWT ID (unique per token) + /** Optional "not before" (unix seconds). Tolerated by CLOCK_SKEW_TOLERANCE_SECONDS. */ + nbf?: number; } interface RefreshTokenEntry { @@ -287,6 +295,24 @@ function createRefreshTokenStore(): IRefreshTokenStore { const refreshTokenStore: IRefreshTokenStore = createRefreshTokenStore(); +// The revocation list is consulted on every authenticated request, so in a +// multi-instance deployment it has to be shared: a logout handled by pod A +// must be visible to pod B or the "stolen token" window re-opens. Redis is +// used when REDIS_URL is configured; otherwise the in-process store applies +// (single-instance / local development). +function initRevocationStore(): void { + const redisUrl = process.env.REDIS_URL; + if (!redisUrl) return; + + const redis = new Redis(redisUrl, { lazyConnect: true, enableOfflineQueue: false }); + redis.on('error', (err) => { + logger.log('error', 'Redis revocation store error', { error: err.message }); + }); + setRevocationStore(new RedisRevocationStore(redis)); +} + +initRevocationStore(); + // ─── HS256 JWT Helpers ──────────────────────────────────────────────────────── function base64UrlEncode(input: string | Buffer): string { @@ -315,6 +341,65 @@ function signJwt(payload: JwtPayload): string { return `${signingInput}.${sig}`; } +// ─── Time-claim validation (Issue #1431) ────────────────────────────────────────────────── + +/** + * Tolerance, in seconds, allowed on `nbf`/`iat`. + * + * This exists purely to absorb clock skew between the API pod and whatever + * minted the token. It is deliberately NOT applied to `exp`: a token whose + * lifetime has run out must stop working the instant it does, otherwise a + * stolen bearer token keeps authorising `POST /vault/:id/withdraw` for an + * extra minute after the user logs out. + */ +export const CLOCK_SKEW_TOLERANCE_SECONDS = 5; + +/** Thrown when a token's `exp` has passed. */ +export class TokenExpiredError extends Error { + constructor(message = 'JWT has expired') { + super(message); + this.name = 'TokenExpiredError'; + } +} + +/** Thrown when a token is not valid yet beyond the tolerated clock skew. */ +export class TokenNotYetValidError extends Error { + constructor(message = 'JWT is not yet valid') { + super(message); + this.name = 'TokenNotYetValidError'; + } +} + +/** + * Validates the time-based claims of an already signature-verified payload. + * + * `exp` uses **zero** tolerance and is inclusive of the expiry second: RFC 7519 + * says a token is invalid at and after `exp`, so `exp <= now` is rejected. + * `nbf`/`iat` get {@link CLOCK_SKEW_TOLERANCE_SECONDS} of slack. + */ +export function assertTimeClaims(payload: JwtPayload, nowSeconds = Math.floor(Date.now() / 1000)): void { + if (typeof payload.exp !== 'number' || !Number.isFinite(payload.exp)) { + throw new Error('Malformed JWT payload'); + } + + // Strict: no tolerance whatsoever, and the expiry second itself is too late. + if (payload.exp <= nowSeconds) { + throw new TokenExpiredError(); + } + + if (typeof payload.nbf === 'number' && Number.isFinite(payload.nbf)) { + if (payload.nbf > nowSeconds + CLOCK_SKEW_TOLERANCE_SECONDS) { + throw new TokenNotYetValidError(); + } + } + + if (typeof payload.iat === 'number' && Number.isFinite(payload.iat)) { + if (payload.iat > nowSeconds + CLOCK_SKEW_TOLERANCE_SECONDS) { + throw new TokenNotYetValidError('JWT was issued in the future'); + } + } +} + /** * Verifies a JWT string. * Returns the decoded payload on success. @@ -346,8 +431,7 @@ export function verifyJwt(token: string): JwtPayload { throw new Error('Malformed JWT payload'); } - const now = Math.floor(Date.now() / 1000); - if (payload.exp < now) throw new Error('JWT has expired'); + assertTimeClaims(payload); return payload; } @@ -391,6 +475,81 @@ export async function issueTokenPair(walletAddress: string, familyId?: string): // ─── Session Revocation ──────────────────────────────────────────────────────── +/** + * Adds a specific access token to the revocation list. + * + * The entry is retained until the token's own `exp`, at which point the token + * is rejected by the strict expiry check anyway, so the list can never grow + * unbounded. + */ +export async function revokeAccessToken( + payload: JwtPayload, + reason: RevocationReason = 'logout' +): Promise { + if (!payload?.jti || !payload?.sub) { + return; + } + + await getRevocationStore().revoke({ + tokenId: payload.jti, + walletAddress: payload.sub, + revokedAt: Date.now(), + reason, + expiresAt: payload.exp * 1000, + }); + + logger.log('info', 'Access token revoked', { + reason, + wallet: payload.sub.slice(0, 8) + '…', + jti: payload.jti, + }); +} + +/** + * Revokes every access token currently issued to `walletAddress`. + * This is used for /auth/logout-all and for "this device is compromised". + */ +export async function revokeAllAccessTokens( + walletAddress: string, + reason: RevocationReason = 'logout' +): Promise { + const normalizedAddress = normalizeWalletAddress(walletAddress); + const store = getRevocationStore(); + + await store.revokeWalletBefore(normalizedAddress, Date.now(), reason); + const removed = await store.revokeAllForWallet(normalizedAddress, reason); + + logger.log('info', 'All access tokens revoked for wallet', { + reason, + wallet: normalizedAddress.slice(0, 8) + '…', + revokedCount: removed, + }); + + return removed; +} + +/** + * True when the token has been revoked, either individually (`jti`) or because + * the whole wallet was revoked at or after the token was issued. + */ +export async function isAccessTokenRevoked(payload: JwtPayload): Promise { + if (!payload?.jti) return false; + + const store = getRevocationStore(); + + if (await store.isRevoked(payload.jti)) { + return true; + } + + // `iat` is unix seconds; the wallet marker uses unix milliseconds. + if (typeof payload.iat === 'number' && Number.isFinite(payload.iat)) { + return store.isWalletRevokedBefore(payload.sub, payload.iat * 1000); + } + + return false; +} + + /** * Revokes the current session (all tokens in the same family). * This is used for /auth/logout. @@ -536,11 +695,21 @@ export interface AuthenticatedRequest extends Request { jwtPayload?: JwtPayload; } +/** Error code returned when a structurally valid token has been revoked. */ +export const TOKEN_REVOKED_CODE = 'TOKEN_REVOKED'; + /** * Express middleware that validates the Bearer access token from the * Authorization header and attaches the decoded payload to req.jwtPayload. * - * Returns 401 for missing / invalid / expired tokens. + * Two independent checks run on **every** request (Issue #1431): + * + * 1. Cryptographic verification plus time-claim validation, where `exp` is + * enforced with zero tolerance (`assertTimeClaims`). + * 2. A revocation-list lookup covering both the individual token id and any + * wallet-wide revocation issued at or after the token was minted. + * + * Returns 401 for missing / invalid / expired / revoked tokens. */ export function requireAuth( req: AuthenticatedRequest, @@ -560,9 +729,9 @@ export function requireAuth( return; } + let payload: JwtPayload; try { - req.jwtPayload = verifyJwt(match[1]); - next(); + payload = verifyJwt(match[1]); } catch (err) { sendApiError(req, res, { status: 401, @@ -570,7 +739,40 @@ export function requireAuth( message: err instanceof Error ? err.message : 'Invalid token', retryable: false, }); + return; } + + // Revocation lookup is async, so the remainder of the middleware chain runs + // from a continuation. `res.headersSent` guards the (unexpected) case of a + // downstream middleware having already replied before we get here. + isAccessTokenRevoked(payload).then( + (revoked) => { + if (revoked) { + sendApiError(req, res, { + status: 401, + code: TOKEN_REVOKED_CODE, + message: 'Token has been revoked. Please sign in again.', + retryable: false, + }); + return; + } + req.jwtPayload = payload; + next(); + }, + (err) => { + logger.log('error', 'Revocation lookup failed', { + error: err instanceof Error ? err.message : String(err), + }); + // Fail closed: an unreachable revocation store must not silently + // downgrade to "token is fine". + sendApiError(req, res, { + status: 503, + code: 'AUTH_REVOCATION_UNAVAILABLE', + message: 'Unable to verify session state. Please retry.', + retryable: true, + }); + }, + ); } // ─── Auth Route Handlers ────────────────────────────────────────────────────── diff --git a/backend/src/index.ts b/backend/src/index.ts index 8376adc08..e957cd9de 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -10,7 +10,17 @@ initTracing(); import express, { Express, Request, Response, NextFunction, ErrorRequestHandler } from 'express'; import NodeCache from 'node-cache'; -import { loginHandler, nonceHandler, refreshHandler, requireAuth, verifyJwt } from './auth'; +import { + loginHandler, + nonceHandler, + refreshHandler, + requireAuth, + verifyJwt, + revokeAccessToken, + revokeAllAccessTokens, + revokeCurrentSession, + revokeAllSessions, +} from './auth'; import { authLimiter, authIpLimiter, @@ -981,16 +991,42 @@ app.use('/admin', validateApiKey, adminRbacMiddleware); /** * POST /api/v1/auth/logout - * Revokes the current session. Requires Bearer token. + * + * Revokes the presented access token (`jti`) on the revocation list, so the + * very next request with it is rejected 401 `TOKEN_REVOKED` (Issue #1431). + * When a `refreshToken` is also supplied the whole refresh family is revoked, + * so the session cannot be resurrected by rotation. */ -apiV1.post('/auth/logout', readsLimiter, requireAuth, (req: Request, res: Response) => { +apiV1.post('/auth/logout', readsLimiter, requireAuth, async (req: Request, res: Response) => { + const authReq = req as import('./auth').AuthenticatedRequest; + const payload = authReq.jwtPayload; + const walletAddress = payload?.sub; + + if (!payload || !walletAddress) { + res.status(500).json({ + error: 'Internal Server Error', + status: 500, + message: 'Unable to determine authenticated wallet', + }); + return; + } + try { - const authReq = req as import('./auth').AuthenticatedRequest; - const walletAddress = authReq.jwtPayload?.sub; - if (!walletAddress) throw new Error('Unable to determine authenticated wallet'); + await revokeAccessToken(payload, 'logout'); + + const refreshToken = + typeof (req.body as { refreshToken?: unknown } | undefined)?.refreshToken === 'string' + ? ((req.body as { refreshToken: string }).refreshToken) + : undefined; + if (refreshToken) { + await revokeCurrentSession(refreshToken); + } + res.status(200).json({ message: 'Session revoked successfully', - walletAddress: walletAddress.slice(0, 8) + '…', + walletAddress: walletAddress.slice(0, 8) + '…', + revokedAccessToken: true, + refreshSessionRevoked: Boolean(refreshToken), timestamp: new Date().toISOString(), }); } catch (err) { @@ -1004,17 +1040,34 @@ apiV1.post('/auth/logout', readsLimiter, requireAuth, (req: Request, res: Respon /** * POST /api/v1/auth/logout-all - * Revokes all active sessions for the authenticated wallet. + * + * Revokes every access token currently issued to the wallet by writing a + * wallet-wide high-water mark, so tokens minted before now stop working even + * though we hold no list of them (Issue #1431). Also revokes all refresh + * families for the wallet. */ -apiV1.post('/auth/logout-all', readsLimiter, requireAuth, (req: Request, res: Response) => { +apiV1.post('/auth/logout-all', readsLimiter, requireAuth, async (req: Request, res: Response) => { + const authReq = req as import('./auth').AuthenticatedRequest; + const walletAddress = authReq.jwtPayload?.sub; + + if (!walletAddress) { + res.status(500).json({ + error: 'Internal Server Error', + status: 500, + message: 'Unable to determine authenticated wallet', + }); + return; + } + try { - const authReq = req as import('./auth').AuthenticatedRequest; - const walletAddress = authReq.jwtPayload?.sub; - if (!walletAddress) throw new Error('Unable to determine authenticated wallet'); + const revokedAccessTokens = await revokeAllAccessTokens(walletAddress, 'logout'); + const revokedRefreshTokens = await revokeAllSessions(walletAddress); + res.status(200).json({ message: 'All sessions revoked successfully', - walletAddress: walletAddress.slice(0, 8) + '…', - revokedCount: 1, + walletAddress: walletAddress.slice(0, 8) + '…', + revokedCount: revokedRefreshTokens, + revokedAccessTokens, timestamp: new Date().toISOString(), }); } catch (err) { diff --git a/backend/src/swagger.ts b/backend/src/swagger.ts index 5e58d4589..23934f184 100644 --- a/backend/src/swagger.ts +++ b/backend/src/swagger.ts @@ -6,6 +6,7 @@ import { MAX_PAGE as VAULT_LIST_MAX_PAGE, MAX_PAGE_SIZE as VAULT_LIST_MAX_LIMIT, } from './middleware/paginationGuard'; +import { CLOCK_SKEW_TOLERANCE_SECONDS } from './auth'; const VAULT_LIST_DEFAULT_LIMIT = DEFAULT_PAGE_SIZE; @@ -59,7 +60,14 @@ const options: swaggerJsdoc.Options = { type: 'http', scheme: 'bearer', bearerFormat: 'JWT', - description: 'JWT issued by POST /auth/login or POST /auth/refresh.', + description: + 'JWT issued by POST /auth/login or POST /auth/refresh. ' + + '`exp` is enforced with **zero** clock tolerance — a token is rejected the ' + + 'moment it expires. Only `nbf`/`iat` get a ' + + `${CLOCK_SKEW_TOLERANCE_SECONDS}s tolerance for clock skew. ` + + 'The revocation list is checked on every authenticated request, so a token ' + + 'presented after `POST /auth/logout` (401 `TOKEN_REVOKED`) or after ' + + '`POST /auth/logout-all` is refused immediately rather than at expiry.', }, apiKeyAuth: { type: 'apiKey', diff --git a/backend/src/tokenRevocation.ts b/backend/src/tokenRevocation.ts index 86cd13330..491a7cea1 100644 --- a/backend/src/tokenRevocation.ts +++ b/backend/src/tokenRevocation.ts @@ -9,6 +9,15 @@ * - ROTATION: Token was rotated during refresh * - SUSPICIOUS: Suspicious activity detected * - COMPROMISED: Token was compromised + * + * Issue #1431: this store is consulted on **every** authenticated request, not + * only on refresh. Two kinds of revocation are therefore tracked: + * + * - per token id (`jti`) — the exact access token that was logged out, and + * - per wallet, as a high-water mark (`revokedBefore`) — every access token + * issued at or before that instant, which is what `/auth/logout-all` and a + * "this token is compromised" event need. A timestamp rather than an + * explicit id list keeps the store O(1) per wallet instead of O(tokens). */ import Redis from 'ioredis'; @@ -24,18 +33,45 @@ export interface RevocationRecord { expiresAt: number; // When to remove from store } +export interface WalletRevocation { + /** Access tokens issued at or before this instant (unix ms) are rejected. */ + revokedBefore: number; + reason: RevocationReason; + /** When the wallet marker itself expires from the store. */ + expiresAt: number; +} + export interface RevocationStore { revoke(record: RevocationRecord): Promise; isRevoked(tokenId: string): Promise; revokeAllForWallet(walletAddress: string, reason: RevocationReason): Promise; clear(): Promise; + /** + * Reject every access token for `walletAddress` issued at or before + * `issuedAtMs` (the token's `iat` claim, in unix milliseconds). + */ + revokeWalletBefore( + walletAddress: string, + issuedAtMs: number, + reason: RevocationReason, + ): Promise; + /** True when the wallet has been revoked at or after `issuedAtMs`. */ + isWalletRevokedBefore(walletAddress: string, issuedAtMs: number): Promise; } +/** + * How long a wallet-wide revocation marker is retained. No live access token + * can predate the marker once the longest access-token TTL has elapsed, so a + * marker older than that can never match and is safe to drop. + */ +const WALLET_REVOCATION_TTL_MS = 24 * 60 * 60 * 1000; + /** * In-memory revocation store for single-instance deployments. */ export class InMemoryRevocationStore implements RevocationStore { private revoked = new Map(); + private walletRevocations = new Map(); async revoke(record: RevocationRecord): Promise { this.revoked.set(record.tokenId, record); @@ -57,6 +93,9 @@ export class InMemoryRevocationStore implements RevocationStore { } async revokeAllForWallet(walletAddress: string, reason: RevocationReason): Promise { + // Drop the per-token entries for this wallet: the wallet marker written + // below now decides. Previously this method only deleted records without + // recording anything, which silently un-revoked every one of them. let count = 0; for (const [tokenId, record] of this.revoked.entries()) { if (record.walletAddress === walletAddress) { @@ -64,11 +103,42 @@ export class InMemoryRevocationStore implements RevocationStore { count++; } } + await this.revokeWalletBefore(walletAddress, Date.now(), reason); return count; } + async revokeWalletBefore( + walletAddress: string, + issuedAtMs: number, + reason: RevocationReason, + ): Promise { + const existing = this.walletRevocations.get(walletAddress); + if (existing && existing.revokedBefore >= issuedAtMs) { + // A broader (later) revocation always wins; never move the marker back. + return; + } + this.walletRevocations.set(walletAddress, { + revokedBefore: issuedAtMs, + reason, + expiresAt: issuedAtMs + WALLET_REVOCATION_TTL_MS, + }); + } + + async isWalletRevokedBefore(walletAddress: string, issuedAtMs: number): Promise { + const marker = this.walletRevocations.get(walletAddress); + if (!marker) return false; + + if (marker.expiresAt < Date.now()) { + this.walletRevocations.delete(walletAddress); + return false; + } + + return issuedAtMs <= marker.revokedBefore; + } + async clear(): Promise { this.revoked.clear(); + this.walletRevocations.clear(); } private cleanup(): void { @@ -78,6 +148,11 @@ export class InMemoryRevocationStore implements RevocationStore { this.revoked.delete(tokenId); } } + for (const [walletAddress, marker] of this.walletRevocations.entries()) { + if (marker.expiresAt < now) { + this.walletRevocations.delete(walletAddress); + } + } } } @@ -87,6 +162,8 @@ export class InMemoryRevocationStore implements RevocationStore { * Key schema: * - `revocation:token:{tokenId}` → JSON revocation record (with TTL = expiresAt) * - `revocation:wallet:{walletAddress}` → set of revoked token IDs + * - `revocation:wallet-revoked-before:{walletAddress}` → JSON + * {@link WalletRevocation} high-water mark (Issue #1431) */ export class RedisRevocationStore implements RevocationStore { private readonly keyPrefix = 'revocation:'; @@ -145,19 +222,22 @@ export class RedisRevocationStore implements RevocationStore { try { const walletKey = `${this.keyPrefix}wallet:${walletAddress}`; const tokenIds = await this.redis.smembers(walletKey); - - if (tokenIds.length === 0) return 0; - // Remove each token - const pipeline = this.redis.pipeline(); - for (const tokenId of tokenIds) { - const key = `${this.keyPrefix}token:${tokenId}`; - pipeline.del(key); + // Remove each token: the wallet high-water mark written below now + // decides, so keeping per-token entries would only add read cost. + if (tokenIds.length > 0) { + const pipeline = this.redis.pipeline(); + for (const tokenId of tokenIds) { + const key = `${this.keyPrefix}token:${tokenId}`; + pipeline.del(key); + } + pipeline.del(walletKey); + + await pipeline.exec(); } - pipeline.del(walletKey); - - await pipeline.exec(); - + + await this.revokeWalletBefore(walletAddress, Date.now(), reason); + logger.info('All tokens revoked for wallet', { walletAddress, reason, @@ -170,7 +250,59 @@ export class RedisRevocationStore implements RevocationStore { walletAddress, error: err instanceof Error ? err.message : String(err), }); - return 0; + return this.fallback.revokeAllForWallet(walletAddress, reason); + } + } + + async revokeWalletBefore( + walletAddress: string, + issuedAtMs: number, + reason: RevocationReason, + ): Promise { + try { + const key = `${this.keyPrefix}wallet-revoked-before:${walletAddress}`; + const existingRaw = await this.redis.get(key); + + if (existingRaw) { + const existing = JSON.parse(existingRaw) as WalletRevocation; + if (existing.revokedBefore >= issuedAtMs) { + // A broader (later) revocation always wins; never move it back. + return; + } + } + + const marker: WalletRevocation = { + revokedBefore: issuedAtMs, + reason, + expiresAt: issuedAtMs + WALLET_REVOCATION_TTL_MS, + }; + const ttl = Math.max(1, Math.ceil((marker.expiresAt - Date.now()) / 1000)); + await this.redis.set(key, JSON.stringify(marker), 'EX', ttl); + + logger.debug('Wallet access tokens revoked', { walletAddress, reason, issuedAtMs }); + } catch (err) { + logger.error('Failed to revoke wallet access tokens', { + walletAddress, + error: err instanceof Error ? err.message : String(err), + }); + await this.fallback.revokeWalletBefore(walletAddress, issuedAtMs, reason); + } + } + + async isWalletRevokedBefore(walletAddress: string, issuedAtMs: number): Promise { + try { + const key = `${this.keyPrefix}wallet-revoked-before:${walletAddress}`; + const raw = await this.redis.get(key); + if (!raw) return false; + + const marker = JSON.parse(raw) as WalletRevocation; + return issuedAtMs <= marker.revokedBefore; + } catch (err) { + logger.error('Failed to check wallet revocation', { + walletAddress, + error: err instanceof Error ? err.message : String(err), + }); + return this.fallback.isWalletRevokedBefore(walletAddress, issuedAtMs); } } @@ -185,6 +317,7 @@ export class RedisRevocationStore implements RevocationStore { logger.error('Failed to clear revocation store', { error: err instanceof Error ? err.message : String(err), }); + await this.fallback.clear(); } } } From bd69330acb20b549860fdf9dbfd30d5421dc58b1 Mon Sep 17 00:00:00 2001 From: ToryMic Date: Tue, 29 Sep 2026 23:53:51 -0400 Subject: [PATCH 4/5] fix(backend): normalise the wallet in the revocation lookup, clarify comment Two review-level corrections to the revocation path added in the previous commit: - `isAccessTokenRevoked` looked the wallet marker up under the raw `sub` claim. Revocations are written under the canonical (upper-case) address via `normalizeWalletAddress`, so a token whose `sub` happened to be lower-case would not have matched a wallet-wide revocation. Normalise the lookup key the same way the write path does. - requireAuth's comment claimed a `res.headersSent` guard that was never implemented. Replace it with the reasoning that actually matters: Express ignores middleware return values, so returning before `next()` is safe for route usage, and the single direct caller passes a synchronous next(). --- backend/prisma/dev.db | Bin 1159168 -> 1191936 bytes backend/src/auth.ts | 14 +++++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/backend/prisma/dev.db b/backend/prisma/dev.db index bce83236352cf6fa4b5723f1e9085508baa53f4b..1f9b8f6f1abfd569a388488b1bc21faf12a32d78 100644 GIT binary patch delta 48963 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 t+>RG>4}bf&ura$DW delta 33078 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�pMH?PVUP}%teXMI?nb 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 diff --git a/backend/src/auth.ts b/backend/src/auth.ts index 0118bddb8..46c4b510a 100644 --- a/backend/src/auth.ts +++ b/backend/src/auth.ts @@ -541,9 +541,11 @@ export async function isAccessTokenRevoked(payload: JwtPayload): Promise { if (revoked) { From b0065f59335e0574f23ef94c571f1aa991a3ed09 Mon Sep 17 00:00:00 2001 From: ToryMic Date: Wed, 30 Sep 2026 00:01:11 -0400 Subject: [PATCH 5/5] test(backend): cover the idempotency replay cache, including its Redis path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `transferOrchestrator.test.ts` exercises the happy path end to end, but with no `REDIS_URL` the `redisClientManager` never reports ready, so `IdempotencyStore.redis` returns null and every Redis helper is skipped — the store's Redis branch, and more importantly its error fallback, had no coverage at all. These tests drive the store through a stub client so they reach: the Redis get/set/del round trip, conflict detection across instances, the read/write/delete failure fallbacks, and `pruneStaleKeys` across expired TTL, unparsable payloads and dryRun. Plus the in-process semantics that money-moving code relies on: single execution, replay, fingerprint conflict, in-flight coalescing, and that a rejected operation frees the pending slot for a retry. idempotencyStore.ts: 70.9% -> 94.5% statements, 50.9% -> 81.8% branches. --- .../src/__tests__/idempotencyStore.test.ts | 417 ++++++++++++++++++ 1 file changed, 417 insertions(+) create mode 100644 backend/src/__tests__/idempotencyStore.test.ts diff --git a/backend/src/__tests__/idempotencyStore.test.ts b/backend/src/__tests__/idempotencyStore.test.ts new file mode 100644 index 000000000..e40c6a951 --- /dev/null +++ b/backend/src/__tests__/idempotencyStore.test.ts @@ -0,0 +1,417 @@ +/** + * Unit tests for the idempotency replay cache (`idempotencyStore.ts`). + * + * `transferOrchestrator.test.ts` already exercises the happy path end to end. + * What it cannot reach is the Redis branch: with no `REDIS_URL` the + * `redisClientManager` never reports ready, so `IdempotencyStore.redis` + * returns `null` and every Redis helper is skipped. These tests drive the + * store through a stub client so the Redis code path — and, more importantly, + * its *fallback* when Redis errors — is actually executed. + */ + +import { + IdempotencyStore, + IdempotencyConflictError, + buildIdempotencyFingerprint, + getIdempotencyHashThreshold, + idempotencyStore, +} from '../idempotencyStore'; + +type RedisStub = Record; + +function createRedisStub(overrides: Partial = {}): RedisStub { + const store = new Map(); + + const base: RedisStub = { + get: jest.fn(async (key: string) => store.get(key) ?? null), + set: jest.fn(async (key: string, value: string) => { + store.set(key, value); + return 'OK'; + }), + del: jest.fn(async (key: string) => (store.delete(key) ? 1 : 0)), + exists: jest.fn(async (key: string) => (store.has(key) ? 1 : 0)), + scan: jest.fn(async () => ['0', [] as string[]]), + ttl: jest.fn(async () => -1), + }; + + return { ...base, ...overrides }; +} + +/** Points the module's `redisClientManager` at `client` for the test's duration. */ +async function withRedisClient(client: unknown, fn: () => Promise): Promise { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const rateLimiter = require('../rateLimiter') as typeof import('../rateLimiter'); + const manager = rateLimiter.redisClientManager as unknown as { + getClient: () => unknown; + isReady: () => boolean; + }; + const originalGetClient = manager.getClient; + const originalIsReady = manager.isReady; + + manager.getClient = () => client; + manager.isReady = () => true; + + try { + return await fn(); + } finally { + manager.getClient = originalGetClient; + manager.isReady = originalIsReady; + } +} + +describe('IdempotencyStore', () => { + describe('execute() with the in-process cache', () => { + it('runs the operation once and replays the stored result afterwards', async () => { + const store = new IdempotencyStore(60_000); + const operation = jest.fn(async () => ({ statusCode: 201, body: { hash: 'abc' } })); + + const first = await store.execute('key-1', 'fp-1', operation); + const second = await store.execute('key-1', 'fp-1', operation); + + expect(first).toEqual({ result: { statusCode: 201, body: { hash: 'abc' } }, replayed: false }); + expect(second.replayed).toBe(true); + expect(second.result).toEqual(first.result); + expect(operation).toHaveBeenCalledTimes(1); + }); + + it('throws IdempotencyConflictError when the body changed for the same key', async () => { + const store = new IdempotencyStore(60_000); + await store.execute('key-2', 'fp-1', async () => ({ statusCode: 200, body: 'a' })); + + await expect( + store.execute('key-2', 'fp-2', async () => ({ statusCode: 200, body: 'b' })), + ).rejects.toBeInstanceOf(IdempotencyConflictError); + }); + + it('coalesces concurrent calls into a single operation', async () => { + const store = new IdempotencyStore(60_000); + let resolveOperation: (() => void) | undefined; + const gate = new Promise((resolve) => { + resolveOperation = resolve; + }); + const operation = jest.fn(async () => { + await gate; + return { statusCode: 200, body: 'once' }; + }); + + const inflight = Promise.all([ + store.execute('key-3', 'fp', operation), + store.execute('key-3', 'fp', operation), + ]); + resolveOperation!(); + const [a, b] = await inflight; + + expect(operation).toHaveBeenCalledTimes(1); + expect([a.replayed, b.replayed].filter(Boolean)).toHaveLength(1); + }); + + it('rejects a concurrent call with a different fingerprint', async () => { + const store = new IdempotencyStore(60_000); + let resolveOperation: (() => void) | undefined; + const gate = new Promise((resolve) => { + resolveOperation = resolve; + }); + + const inflight = store.execute('key-4', 'fp-a', async () => { + await gate; + return { statusCode: 200, body: 'a' }; + }); + const conflicting = store.execute('key-4', 'fp-b', async () => ({ statusCode: 200, body: 'b' })); + + resolveOperation!(); + await inflight; + await expect(conflicting).rejects.toBeInstanceOf(IdempotencyConflictError); + }); + + it('does not leave a key pending after the operation rejects', async () => { + const store = new IdempotencyStore(60_000); + await expect( + store.execute('key-5', 'fp', async () => { + throw new Error('boom'); + }), + ).rejects.toThrow('boom'); + + // The pending slot must be free, so a retry runs the operation again. + const retry = await store.execute('key-5', 'fp', async () => ({ statusCode: 200, body: 'ok' })); + expect(retry.replayed).toBe(false); + }); + }); + + describe('observability and maintenance', () => { + it('reports hits, conflicts and active keys', async () => { + const store = new IdempotencyStore(60_000); + await store.execute('m-1', 'fp', async () => ({ statusCode: 200, body: 1 })); + await store.execute('m-1', 'fp', async () => ({ statusCode: 200, body: 1 })); + await expect( + store.execute('m-2', 'other', async () => ({ statusCode: 200, body: 2 })), + ).resolves.toBeDefined(); + + const metrics = store.getMetrics(); + expect(metrics.hits).toBe(1); + expect(metrics.activeKeys).toBe(2); + expect(metrics.pendingKeys).toBe(0); + }); + + it('counts a conflict', async () => { + const store = new IdempotencyStore(60_000); + await store.execute('m-3', 'fp', async () => ({ statusCode: 200, body: 1 })); + await expect( + store.execute('m-3', 'fp-different', async () => ({ statusCode: 200, body: 2 })), + ).rejects.toBeInstanceOf(IdempotencyConflictError); + + expect(store.getMetrics().conflicts).toBe(1); + }); + + it('lists stored keys, optionally filtered by prefix', async () => { + const store = new IdempotencyStore(60_000); + await store.execute('alpha-1', 'fp', async () => ({ statusCode: 200, body: 1 })); + await store.execute('beta-1', 'fp', async () => ({ statusCode: 200, body: 2 })); + + expect(store.inspectKeys().map((entry) => entry.key).sort()).toEqual(['alpha-1', 'beta-1']); + expect(store.inspectKeys('alpha').map((entry) => entry.key)).toEqual(['alpha-1']); + expect(store.inspectKeys('alpha')[0].metadata.status).toBe('completed'); + }); + + it('deletes a single key and reports whether anything was removed', async () => { + const store = new IdempotencyStore(60_000); + await store.execute('d-1', 'fp', async () => ({ statusCode: 200, body: 1 })); + + expect(await store.deleteKey('d-1')).toBe(true); + expect(await store.deleteKey('d-1')).toBe(false); + expect(store.inspectKeys()).toHaveLength(0); + }); + + it('clear() empties the store and counts the evictions', async () => { + const store = new IdempotencyStore(60_000); + await store.execute('c-1', 'fp', async () => ({ statusCode: 200, body: 1 })); + await store.execute('c-2', 'fp', async () => ({ statusCode: 200, body: 2 })); + + const before = store.getMetrics().evictions; + store.clear(); + + expect(store.inspectKeys()).toHaveLength(0); + expect(store.getMetrics().evictions).toBe(before + 2); + }); + + it('prunes entries older than the retention window and honours dryRun', async () => { + const store = new IdempotencyStore(60_000); + await store.execute('p-1', 'fp', async () => ({ statusCode: 200, body: 1 })); + await store.execute('p-2', 'fp', async () => ({ statusCode: 200, body: 2 })); + + // A retention of -1 puts the cutoff in the future, so everything + // currently stored counts as stale. + const dry = await store.pruneStaleKeys(-1, true); + expect(dry.localPruned).toBe(2); + expect(dry.redisPruned).toBe(0); + expect(store.inspectKeys()).toHaveLength(2); + + const applied = await store.pruneStaleKeys(-1, false); + expect(applied.localPruned).toBe(2); + expect(applied.pruned).toBe(2); + expect(store.inspectKeys()).toHaveLength(0); + }); + + it('keeps entries that are newer than the retention window', async () => { + const store = new IdempotencyStore(60_000); + await store.execute('p-3', 'fp', async () => ({ statusCode: 200, body: 1 })); + + const result = await store.pruneStaleKeys(60_000, false); + expect(result.localPruned).toBe(0); + expect(store.inspectKeys()).toHaveLength(1); + }); + }); + + describe('Redis path', () => { + it('reads and writes through Redis when it is available', async () => { + const redis = createRedisStub(); + + await withRedisClient(redis, async () => { + const store = new IdempotencyStore(60_000); + const first = await store.execute('r-1', 'fp', async () => ({ statusCode: 200, body: 'v' })); + expect(first.replayed).toBe(false); + expect(redis.set).toHaveBeenCalled(); + + // A fresh store shares no local cache, so only Redis can serve the replay. + const other = new IdempotencyStore(60_000); + const second = await other.execute('r-1', 'fp', async () => ({ statusCode: 200, body: 'v' })); + expect(second.replayed).toBe(true); + expect(redis.get).toHaveBeenCalled(); + }); + }); + + it('detects a conflicting body via Redis', async () => { + const redis = createRedisStub(); + + await withRedisClient(redis, async () => { + const store = new IdempotencyStore(60_000); + await store.execute('r-2', 'fp', async () => ({ statusCode: 200, body: 'v' })); + + await expect( + store.execute('r-2', 'different', async () => ({ statusCode: 200, body: 'v' })), + ).rejects.toBeInstanceOf(IdempotencyConflictError); + }); + }); + + it('deletes via Redis and reports a removal', async () => { + const redis = createRedisStub(); + + await withRedisClient(redis, async () => { + const store = new IdempotencyStore(60_000); + await store.execute('r-3', 'fp', async () => ({ statusCode: 200, body: 'v' })); + + expect(await store.deleteKey('r-3')).toBe(true); + expect(redis.del).toHaveBeenCalled(); + expect(await store.deleteKey('never-stored')).toBe(false); + }); + }); + + it('survives Redis write failures by falling back to the local cache', async () => { + const redis = createRedisStub({ set: jest.fn(async () => { throw new Error('write fail'); }) }); + + await withRedisClient(redis, async () => { + const store = new IdempotencyStore(60_000); + const first = await store.execute('r-4', 'fp', async () => ({ statusCode: 200, body: 'v' })); + const second = await store.execute('r-4', 'fp', async () => ({ statusCode: 200, body: 'v' })); + + expect(first.replayed).toBe(false); + expect(second.replayed).toBe(true); + }); + }); + + it('survives Redis read failures', async () => { + const redis = createRedisStub({ get: jest.fn(async () => { throw new Error('read fail'); }) }); + + await withRedisClient(redis, async () => { + const store = new IdempotencyStore(60_000); + const first = await store.execute('r-5', 'fp', async () => ({ statusCode: 200, body: 'v' })); + const second = await store.execute('r-5', 'fp', async () => ({ statusCode: 200, body: 'v' })); + + expect(first.replayed).toBe(false); + expect(second.replayed).toBe(true); + }); + }); + + it('survives Redis delete failures', async () => { + const redis = createRedisStub({ del: jest.fn(async () => { throw new Error('del fail'); }) }); + + await withRedisClient(redis, async () => { + const store = new IdempotencyStore(60_000); + await store.execute('r-6', 'fp', async () => ({ statusCode: 200, body: 'v' })); + + // The local entry is still removed, so the key is gone either way. + expect(await store.deleteKey('r-6')).toBe(true); + }); + }); + + it('prunes stale Redis keys, honouring dryRun', async () => { + const keys = new Map(); + const redis = createRedisStub({ + scan: jest.fn(async () => ['0', ['idempotency:r-7']]), + get: jest.fn(async (key: string) => { + if (key !== 'idempotency:r-7') return null; + return ( + keys.get(key) ?? + JSON.stringify({ + statusCode: 200, + body: 'v', + fingerprint: 'fp', + metadata: { + createdAt: new Date(Date.now() - 120_000).toISOString(), + lastAccessedAt: new Date().toISOString(), + replayCount: 0, + status: 'completed', + }, + }) + ); + }), + ttl: jest.fn(async () => -1), + }); + + await withRedisClient(redis, async () => { + const store = new IdempotencyStore(60_000); + + const dry = await store.pruneStaleKeys(60_000, true); + expect(dry.redisPruned).toBe(1); + expect(redis.del).not.toHaveBeenCalled(); + + const applied = await store.pruneStaleKeys(60_000, false); + expect(applied.redisPruned).toBe(1); + expect(redis.del).toHaveBeenCalledWith('idempotency:r-7'); + }); + }); + + it('prunes a Redis key whose TTL has run out', async () => { + const redis = createRedisStub({ + scan: jest.fn(async () => ['0', ['idempotency:r-8']]), + get: jest.fn(async () => + JSON.stringify({ + statusCode: 200, + body: 'v', + fingerprint: 'fp', + metadata: { + createdAt: new Date().toISOString(), + lastAccessedAt: new Date().toISOString(), + replayCount: 0, + status: 'completed', + }, + }), + ), + ttl: jest.fn(async () => 0), + }); + + await withRedisClient(redis, async () => { + const store = new IdempotencyStore(60_000); + const result = await store.pruneStaleKeys(60_000, false); + expect(result.redisPruned).toBe(1); + }); + }); + + it('prunes a Redis key that cannot be parsed', async () => { + const redis = createRedisStub({ + scan: jest.fn(async () => ['0', ['idempotency:r-9']]), + get: jest.fn(async () => 'not json'), + }); + + await withRedisClient(redis, async () => { + const store = new IdempotencyStore(60_000); + const result = await store.pruneStaleKeys(60_000, false); + expect(result.redisPruned).toBe(1); + expect(redis.del).toHaveBeenCalledWith('idempotency:r-9'); + }); + }); + }); + + describe('buildIdempotencyFingerprint()', () => { + it('is stable regardless of key order', () => { + expect(buildIdempotencyFingerprint({ a: 1, b: 2 })).toBe( + buildIdempotencyFingerprint({ b: 2, a: 1 }), + ); + }); + + it('handles nested objects, arrays, dates and null', () => { + const date = new Date('2026-01-01T00:00:00.000Z'); + const fingerprint = buildIdempotencyFingerprint({ z: [1, { y: 2 }], d: date, n: null }); + + expect(fingerprint).toContain('2026-01-01T00:00:00.000Z'); + expect(fingerprint).toContain('null'); + expect(buildIdempotencyFingerprint({ z: [1, { y: 2 }], d: date, n: null })).toBe(fingerprint); + }); + + it('hashes payloads larger than the configured threshold', () => { + const oversized = { blob: 'x'.repeat(getIdempotencyHashThreshold() + 10) }; + const fingerprint = buildIdempotencyFingerprint(oversized); + + expect(fingerprint.startsWith('hashv1:')).toBe(true); + expect(fingerprint).toHaveLength('hashv1:'.length + 64); + }); + + it('does not hash payloads within the threshold', () => { + expect(buildIdempotencyFingerprint({ blob: 'small' })).not.toMatch(/^hashv1:/); + }); + }); + + it('exposes a module-level singleton', () => { + expect(idempotencyStore).toBeInstanceOf(IdempotencyStore); + expect(typeof idempotencyStore.getMetrics().activeKeys).toBe('number'); + }); +});