From 22b31c52bb6e2724e2edcdde5a80c9414e100e5c Mon Sep 17 00:00:00 2001 From: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Date: Thu, 13 Aug 2026 10:50:16 -0700 Subject: [PATCH 01/32] Update image in deploy-ota.md (#11462) --- docs/deploy-ota.md | 2 +- docs/img/ota_action.png | Bin 86622 -> 51054 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/deploy-ota.md b/docs/deploy-ota.md index d582402672..52a319bf38 100644 --- a/docs/deploy-ota.md +++ b/docs/deploy-ota.md @@ -44,7 +44,7 @@ Double check yourself by ensuring that the `version` in `package.json` matches what's in the latest release. This new OTA branch should follow the format `1.X.0-ota-1`. If one or more OTAs -have already been deployed for this release, incremement the branch name e.g. +have already been deployed for this release, increment the branch name e.g. `1.x.0-ota-2`. ### 4. Add commits to the OTA branch diff --git a/docs/img/ota_action.png b/docs/img/ota_action.png index 56911400aa543fb3fb8de5897d6b0596608b1110..0d14bb0bc862a845535e9d1526d4f5ed992cfc83 100644 GIT binary patch literal 51054 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 Zyz4%Ba9>z&$-#g>QZMA6mxvh#{x2}sW3B)I literal 86622 zcma(2bzD?y_Xdp9Euf@yN_R^QpmcW%NOyP5kRm7zN+aD!cT0DJlyr9w1MlED&pAHd z=l6R*?|-xB-h0iyS6pk|*IE;o71vqMnY%CN^fS6z|O}tn7uUj+@%3D6C9{ zskC?$*cBWl%q*>>y`9a}ycN|=ylqSbOsPag(1bi+Lj~BGxf)Y=+S%H>y!I5P`Yrc0 z^z+l#Y*ZA#Z*jE|rqWhWrjT%OHlyHWWoKol5<#O7ayB)8t@=jt?}wpR!c>;7u8yzS z*gQNuSUtE{9h@!LI0OU)*w{JQI5}CMcd)p4*}EEhve>&&KZ*ED#v3yi6K5+&S1Sj5 ziYJ-I?;YG+g{i2Z>lA+rceOJASGv8+-`j^Og6(OAjf0h)?a$lIJgxr!-2Sxk@9j?y zzt&bYb8)bBd-{1{Dh_ULHjdw-ey{2LzpVYcq+xFbeP7Psk3IdsYh^1>Gh3ZER(59g zF2BX{aq#}1Rqa1kIoLV*|6SH_F>{u1u!BAe6{=?9Yz7r#>EKGm_M~&Rzm*QP9@{_W zgI@gO0A}`3t2;x@yPb^h0Rw|pVkIG=EF&R7q3rBnZe?o*10x-tkc6xrH~#{7{Fonx z0EdbqjoKpP0Gop#j{4RcFCiKMg*Fsm>2B(TI2-s8B@xd6PsNc1Sc0h1-AR>6&)6u2 z|J==h`NPKnFi2u<$z$1PTVW&>Z1&K=Mj3`B?HSX;3Ns;Hq?@)^2fOJm$Yo;*L zcOTNyyA&kXqP0=|DD9mx2==4%f!M+|qzxiWTee8j2DmwN3;x@(-2z;-h3;^K z8y*1IF(AGM>Ouh^Ht7zBzq9+7&;j&10ft+2mKAqD+-iOG1Cudg<=cQu%<}!0F1t8XQ6Oe z?DY7uz*tI38jz;=ewJzhMo};(fM;xUOkju86@D+23;;ida)Ez`k!|!sK8VLyV(x`` zyF5S4XJAr$zvC-?7=n(JUugA2BA9&bNx$?Q*_cBNJ5A_;u@Z%uuF6~VNCW72ksl>! zv$_kW3tp%T6cK5JSxeSrA7$ngXcZ_~GADn`%N`zQwG?%wNsbtKQ!eQsuJtYI8}XRd zxW^d$j>-ie#>;FFM;E`z+!sQ5&Q}C#kqo^gJ*hvcjlDS}zF=uHYa-eNyLI0gn|v>v z!<$o>Ynk)5MXx0m{6O0E%h8&jE0u|ru(!i({i9QpQ?FB!qi{XtqEs`yF-E zAvfZ_0uO`_RISLOkr@=qC`bqv;_M))J~~@U5-M&Sdz?@doHuX3Js(dX6qBbdqt?XU z|6tecza|I;UjqTU24w-zqHY6PL!9&MCensTOuTd+4V$WvjQE25YjJBzVTu;&xc*R6 zEN2pdl;*Gc60PF?;t%vn33bsHgXbHweOmo|eb;?$niTYe^z3+vjJWjiOsSgnr6(W7 zGYM6j=}88xW$`viHbuZ}@;%FhA`Zsy?+J$bnkvQ~7KY1Jxbv}S^v^qWGPjGO2Ks(A4qv(WTt4a_12 z7!rIKGq$ZpOh(p6fAW{xzZ-3Kn!YU}EApLwG3`D5ZhF+(mB*omyT;D?lXcYe_+H*# z)?ULNcEVCtlPI9h$4vUT#0f-Qfkc^m3yi0US{K@>2rA=pEFISJW(%ZDu^w7b;a>*7l z(=!!nqh{tZn`pal($rJifAuNtE9r`ThCzn#v`C+SkY6!G3t0;JyF^;ohwN69z;&`- zFa{BI@b_@1CDiqe-Rhp*=1DpWdXRd2_Nf7@VF_cU_ZmkT=b>^Xc%|MhOb)0Wc+I#h4$j*oXrZJ3I5SJA zc-@lFOx?oNl5w+hM+2U^i@aL^mz~(1cHJP}*qt|x&X~H)^BkxINIhyiXu(Uv{(?<_ zRf1Q5{{b2nbBqocyUKicixd$YK@=p$F6ilVvW}e{Jrr#y{ymE_OCxKvv!qKiv?ffg zD=CB={So`u%kQ*nDmhL$G?hHEl#C)pudIcV@eJ@;K7EhW!E<5K6xPK4i5nIX@llY3 zuXfJobmWJ`>DB3v)9XY+mVP~Flft`+i(G<42Ih5U!8D-E2SxtGlMtMt!mUW!N}Ow& z&(1CNP4#SEWX+__&s&+=B1Ka-lGPJ0gywm?e9ojl`p9|>FoBXXRZm_`v$+NAdFnbY z#8mfR>|ESo_)3cR2xP^wD|os!%$zlEE>Ur*e3wq%O@-!DbrK4ZR75kDe(Qle$}W|# zZtb%)Q;J@ee8z1Pd-bDTp&vrV@1$C-9(wM7!DR$yXl80TYE_p_=``4Hwli)O!53#u zhZpP3?vygB4;n2#@f>p1ppD`rV#!zWl-S*}!3ZMp11l~ij@QH9X|B?N$-UTdzs2t8 z=jTL*M7;QC_E-zc6+x}proG^_LHA|)I;NGjhlE-@xR~TYLTSMKu9_5i{lxA7PRW7Jpu4?H0A~inM_td3x z^jZ-y)mnI%-@v11btt`Rs*3iRvV?z^rHO2IHA8J7IQ6k%#yn}Os!jXed82vcwg`QI zpjMye9djeUSK;b}c0|GE^ybE3Q0bs9xj}e?^R@NP^hhnRc7N8l%)rW`*0Ax&8*=|_ zn|Rxv)1G(Pxqf=gc*W^T3v4**{@Rsy(Y<+TYIACOv84k1KFViwQ?NvE_NelBdCy?# zrytwdyA{X{ctYge$~p2b*0Ru$7yqfHooHU$2FIq;EzT#p>9();nrv>-?+A^axe7fC zxU-BFyg9orS!u(-6Q&krBA+2E^Rhp`zyB~QJsa_ZE%e>TcZ$u74-%(Q!sVX~Gj1o> zM?a5$8PDQdQ=w}<2gAYSIW$Kl1Y>K%G&z6k;jNkRV!#PT=v+rL3pCN%QL{?U$<_Oq zdlXOxz`u0ZZ-dEpLRP0?YDtEH0l>(-5mWbsJxD_alGMz%xh)}1+7>bJPWKoEaj>|m zqTa?HXTYFhQ=n$$;CT05EUU?6hiu59?4st3OB}a+d56)1YXbJ&W(t7hj2{doHZ8TZ z{A~FtwMu;X)l1S_%X>Q^&v?MR>4Jy^fdU2=1potw4TSm6M>jE}gj16xKVClWodVGb zbjM)$L!dv91b|6_91Y(Gy8+`2Bkczy=s1r!31QDA*x+ZpC)L%t`&*j@?Byl5Avh+akc zzb}Se01qdC4<|O+A5+6g?Nf>|nzWtbRTa{l{afB2Z#I=ra-xgHjIaj)cd>~7*Y2J+ z3J3CML8zSVHnka%Rx1ArQqrxG250)w%&L!z?vTq23!cH=0)6T4B@-XS9y0^RL@Rq0 zC?kCVBI=qVp3^YUS9sg`{7;qr)}Asl!d(qEN;3WDT|S=^pwyWH-67}-;6s1^D(r7L z|EN#7*ty+CxB|MPL#(?0-6#rzGc)uBbFdZ5RY1r>u{Q3OSk=f25?h~;A)p5URDr>K zz&`j5*xw2~!H2D3#R_^4gAM`&Qvb({|5!8=6J@&dX2|85W)<-&$Cx@C(}?;POm{fW?bf=G7(`H;FHG#a`; z>DCT_2nBgGR>+FwlNv9u?xe2j8G>W5`oabVkjsgnMlF!$|6gnD0Re4LmE!#1fS6;E zok+Lt<_M3GSwiJ(jQu4C`5%X~*1+^EMaGA7%Y^*G|6hj#0X~NSu99&(cX=407H}TM z{B420=F`7i?jr023HxuCi$-3i%ADCWR>m-*#Flmp{>T6Sqqr$p5D7Xcv;UWNv5inY z>rAE@_sE6X9X!+m|0fufTmQefn~AZvQCIu z?vNotP*wJk@rAHMeNb8Fc2xr&YCAksss9tC*gya&_Rg$0h2=N?yp@9vc(Q)JG^mTf zA7-7Tko+y@pNIvN4#1XQwu@(CR*NoD{f~!GVE28;?t2#}&fg-52(>`jeAaIZeB~p% zK>od9>}41#tZ3xlF4wMyFx4o8@e!68dVK!;A8kEh2%#84ax&3y!OW}4g$dK-(zfp^ zaT-vFVb@0R_V!lL@`&2iU$9Gbbx*P1FV{#ip~U7EAhB`#ggpC$s5-LetH11TX!tuy zGNVZ>t*zNjr0TTI04}8?Z|hl6BBkL}8*6yIX%{(7A~n!lzrH<`D}4`W)2X>?+9Zd? zhoi1!a8~={oi3$~E(*-64(4iaW|CM9tQ#8-QU=oPv+I)hQ2<6KDAJT;rg(6}!wPx1 zxwInVfp1Ck{A9F9$`F;HJ~?kgR>?z@lRx{RgRb=So8Op7tPRR8`9;)5iagX}Py^iz z^Fl4g5&gi?!w8@Fa&m8rNEN}=)%_bnLC9BvS!F(i9M}EbT~4{CM*T+{8#}j;unfCI z#6&~y^Kqr4(7X7(b~f7rm=$m|(}bGr)3N!`{s2ADb7?>#>^jvTkd~A*FFSkk;ypGy zTn2X?w?0b2WB=>?*@xN1#e=J}oul3F1Az;3S`_qHDW%^!8#|NtGQY2WF^q#Js2>Z6 zCjSE()zvU9M}ZacfC1phNN$(eNVl7~Q8x|>#7SK*Wj|jPWvV9~j!YP;$yQY7pg%N8 z{k5IHNj|foVZ7c9ZiW*E85U&JY9kTTvZ}+n$%ryD7o(?K<4mgaBvW`t zfdC?Q_#cdq4!b8i@lpQS7PioAtDf?h47|c<7lVPTk)gOyy41SVYp@>FGJwa2`vwg9 zBN*G+Y1kJOS#|Z2Q{CDpa;F3K`7xe5_hS>jfLl1@sP=k^35Q)^2e+4hN0N1SxJJ9MpPk(1l8%QDSF>bZ z|N74$!==fDz?-@+j?Z^!A)={L!NS#uXUgk#1aa98xnUOTqgh#D)N}9OImWXK<518jCBea(84S1yTZHDlsy_|gwTiU zpH-dWnYBMLw4mDjCkulD5jyo+D~xqIB59ys@`z=YmQEY=cK+b8r^H_7b{u*{LSl6+ zJW3|N>1sP6X5Ya1jT!*VsW_5?Z~;sLSXh*JX^NlXMmw9eZAQ!pvc8U{VSlI;P>K!3 z0?l-USy<2-%2de{W+Fl{^TQKnD!lp|GeNn((<&4*&C$KsFU~US8BiLaThOIZ9$p!3 zhyR~!6Z#Sg38C}w>(2Y>0BCNzEyqAmVC(hP4NCOt>Tua^s6zVsFLZ)_Q6TU6`y;}j zV93E8P_i?V6B9i9Qws}}z!1QpP=szGxRzW(4a8Cir=q z_DY`azavyE|96CbgXSJ+gqr^qp>t(NOF?Emcn7u(`|Vx`6G?vxvs)WSlwH)}+B2VFG!Rqdm2m!0U=~2_8 z|9J#c8S|H@;0Y9NUfGx(0~0m1lPjM^$Dn6Jd~K(0>v#$c^C>0M07nJdDdGV~<_tiq z*Ui|Vhe>i356**Be#KU@D+Hw@%pD+@N&ZLmri|{Kd5_v}6hGDNvRH36G$MFtscGPb z=Q1C%dFHKdtS zcCO#)e+$G+_YI)Kr|BDTPMY?z%W=!mlqy0=^@ObX%vMTo-l1B)JO|j{iQMIqr>&%C z(@9azXd=_A4{H`jw5D2GNw))9ShBecjy=2i z$vm^#T|YAXw{*6?THFWU+^m1*(8XfAhLAt>9aNqxnh%5$_iUYf47lA)8ur>Yv|Ki< z0Cb2feU~CP%Tr8SJJSaj8>SH3-%F(i9M;To=BIJ~3=w?&dI*FZz@iwpJ*B3|8#nXr z=MMwt-RmI;mJoJ6`OP%^(8X@bm(4^2-p_iCu7zIECx?^*{6m_0_&dB~uP@gJ1FS_L zqD}L;_{CTK^g8bzAlE-_9RlQ%a)J>YwHDPHaL!0(7ojL*EO5V1Fc-*mmtjB#`7@I zehYBQ$~%jPv)Q%~8jsQKbiX|GHt}^_@HG7JUH8Z+{slI37{RM#! z$Yj3kiYXj^-Umq*VTW~5UEW=-l?G@ruD>Szl81yZPP&{Xo`?C(+O{p#GME8hn6a?; zD;6k;oO*H@2R^C-=;NEq*^a7`bcX%$*}<*i%f7u!qSy2BoU8ZfY|gS_8?Sz6 z_eJ)HCQ0{mI00stKj3sgsa|(hrG0lgIY89nWhM|8QAy}D2RzZKYg1-b zJ8y2oCpLosj+|8F2qSZUBjn^Bk}aAH@bI|)Lh#PBqxobl!fB~rNam5w0}@`gix-zo`Tx}Inr z6tuv7=HL6`NeWKTGBg2 zV9GCZn^dsqMgx`BE>bC?$2q*UZG*Tw-Df3(4JT(s&X9VQTI%a_w?s^3|Y8|sP@da)gFk`&i*Vf)7*A9p`gZ!8Df7!cKumoo;l zj6w^GWSEaF=VjWI74zm???V8wQH;|fs5ZK0;aA$x=!0)^uw|Ux!f7Ao`~82t-%fL9 z{2o8*KRI0YCB>n&p` zjftGOWIcMaR%0z7x3y74w~>yCp1Ghluc|rtHdhz)tSB8`$z%s~c$sOUyu1ztTeQ_v z;iyn%3_+^+Y9?R%w{Rq($+)`895J* zQaGrqnpgAm?A`LCe+0g&cWx|$Mxv80PQbe8!%M@JYOBdJMuelbN6@!;*ec$P-R1jY z`^)PHwuT{AgFbqATv6q^dGnMWjlKoSpOJ=!NuHYtN)0kCoFiX2x=x$G={`eZ$YiU8 zWn5m)`n}bk@?Jkiu(g=Z)XE8Vd*+kY)fr~i&oD4|#s)VyjC>l5-*1>#+}o7ZKwC7$ zgUzyS*k;X5&0x&ioE{rI7GXxXkCK(77SqN;C##14uR^vDq#FyLD=)^F!Y7}4K3&8& zzHO+yh!k);aE*6;G1;Jre7-dt5y8#F_v4{esBXnyss#HBk4s+4n0?b>ZP|ws?1zi| zHvSOc@_|D|$DO?@2`|pn&wGMWowmiTT3g5SWq(0WH7SLnpw9QsYihuL`6!VqPfqQz zwr#SeUaLw;lo=Q&Q$!)+w>X{}x#cn&F5W%fgKYWbGSXR8tmi9Uy_UP7C2ZAn!3+H# znaXy~mfU8S>>ptd2P^t}9^`QK>^TN=cSnP;yaz-1zsW0onIkC1JxPSEi}$KM`)HaW|p(h!@VIS-YQSqtAa&w(Y-0@7UCb+QK>eNi%>vH<- z;T1UneMj8!dEn-O8U>C|OU!hzTg&B)n-;rS*Rw^Zk#m3>F6zI z$e>lCYDAxeN^{JT@l-1&bZa5GI(aK!J>Gzxhaw+>9EE!K=rXlJvTQ`Un*mT_r4&g{Pn7d! z(h?b4fpCRGa2X46lCYyyDN@-CzgsiFch;!urSH8a%KeIBgXc;ECI5l~&ff++(S0#t zeG(g4NOHdqgoAC!*4Z^{{%4aJy-DA~zw?d`x(f@xE9=8B6r8eX5_cG_I@e$%5fLp2 zpI^Rm33c8(UjQl{i8PfNDxwAHxX<3NUgQb#9~xpW%Iaj`VVug~+!K`Kb^sP>ll(_n zd+X`T#4{N0&yud9x)M$-!zCOSxY;x0{L_BLNRfZ45_-Hxx8DU)@m3fTXa_(p^%vB} zRk7@n=jM0YTq_{%KO1rp2PldoI~+se;iff_cHR|3z~5{fGg{@XUv&~wK-oT+6Cg5# z=3FxPH#Ij#{+qx6*!96a+p+l}3_hrmOzA$>{c>+e;KZ@5xN zE=w>p;HIdcxLJ*A#JaMP0ChOe%Yt;3k<`H++!f|nuu`2ijHRDn zt#ytE0pUsB{4O>~$FTm|+2H`}%4?g=tn!om#Wbn$7sA;|m)T(Jjki8aB54RWX_0HP10Hir-Bt ziN?8j)#*fl(ma;?f9$uqied?0ROl@Ew<;yRsQeIZQX| z#_(w0P_WzYv(v-UVsC%G*BvkM+LhL><^x%+V;LA#s-8AxCrPzv(7`rBzchxtb9$JQ%j z%FMw@c;=i23R013tUKg0$~F>!$kz6Y1$5ulu4CjjaxpSf@))e#L1d0%+Whd1I3*E0 z!-+@#t;timTin}}{W2Hxi8DETOUTW#k6(vv%d{rz;`n%04yD^@+llXgoM~dO&ibLe`9CCjBs(0-6;UVhysfr;yO4*oO$i>0BfyWY#)NYlR z%T^8^g2$qlMQRxs3yj$v6SEC1r#b6%s&3fBF?UrFc5p)SQ{6-R{e4ze{6b!HPJP;y zag1^6f*$IL&)@mqZVq)IlM;UXN)tgd8clhyo9XC)ua)1<|A3Ic+VB4kdYE$_#Q!XM znsc+5rAXR^6i9g_X2TNWS{Xm48nGIy`I6E;hdgKzU25rA`9TDs^_iN*(fglCGj;`E z|9_l!jq3YA30frkJ(uVQm`7d1Rx%x-#P+MN`gL8>)p;@MeEM6L6BF^d+y-#ZdlvZ^ zTV4FG8bTR`>=*g7pCM&@_jxN#31o_DuUt=bLR|5d!YID>X*&Dp-}X6_Aw`DrzX&lH zgivCTMh})@NV}uS@TGX>3xq)N)#2M|jUt5k@sb+!33+Mi{7Y6bYDGe_YAAnu*4{=b zK}Pi#>QbInX!Z^!e945iD=3sn;igFpg9YuAEA}?#Dck>>^CuKRc%n2}YpL(bz$XpP zWw>ycGMS~r*Yi-;wHS?}@ADj#e{L11G%riG(XkAu7F{shQeghOL-9m6Kn6iYBMkfk z&|i85GAc9_7C2^ua_XGq6hHp#J1N`2{bC`pd?> z+g7^;4mleznP#4F%aJ~N0(}M&a{gO_0$@D~Ndm_-(4S4JoJMR_GQb{piTlFg0dUm7d=0$LYzcsM#e^)HHkk_TSRkrNZ$p-sNq zEx(xEyV8O(fUnn)6__`YQ&JdT>tu4G05aL&yq~Kbw8;x+WrX_c9o~kPVW5MC&O4qs z#!N|+VDc|S4J54azZ+QAiGVsDVKT0wAG)aA^L3x)6xfhVc)%s~6?d=3ELprHqlh*s zn6;;@z}9YpHWk>GGsJksleLeoVkE!*B9)5hkS`K@Q8%;VdRv5!_k^xb*HP*r$fQVD;*EfOH}d*Qgewtsc8v5ZvM?m@}X^)Rv{6K+U5 z_6vH4RrSv=vZfi~C2MLMe0;zV_quJVE!r{cW#={Z;z;s+aYPm<=_>fao8qN{!1k=} zFN`SLXRm!%CBTHJ5bCzKfR2Yl`9Y%8aGmAtmw(1P|Hll-v>|q4aidTK>J+;XycK#C}RSJ&t4s=j6!eTpvgREi9 z9qGZK852hA01yk`qj6Yt(pkMGf!Dt{()ZE>6ap|*isOFr(jf87WAfNkwY(xGkK?Xfn$}A~#Q?L+cn~UtT`G#)o|Gc(<$?49jC} z-6_xE3?KR}(($j*Q+Uu{q0&LEysAYF4)&_LHOyC6O*d>1Qkk#834LO$L9d|=nrmF` z9$oZEYn!q-!7W}&_G$#x^-9M*p%}T67e-k%Xgj-C8OUxr5oFbPX&m}uGQCiYIp6^W zPdY5F!bWWnYd^Kc>aSK=;HsxejQnj-vFq~tUQl4_rB%`%P7+^cXDU;*O$sF895MI` z;}vFHshoIq@l7rd+P77+EhHs{RqD$R$aTIX6P_V5>i9-x@l9xf5mEQx=YTnQt8yHY z7|(4%!G*9F2Dn;#6gd()R$r3b#1x2I7}ftSNAXeL`}B{uHQz2xkQ!s%(_Z`tM6{Kh%oC|EO2r zeUBE6{BPqqlLihFHK0me`G&%v(nuUsER)0i136i@8?5U913MPN}ksPt16MA4Qv z!<|K8DTQ4kUCh4?oN5##J1Z)@{0EE22teQ6CFC34UU4OBIDMsDfKkfOQ?W+DN1SLl zle}^w-JS3=9ZH!h4*{4D2eQi3$&t$_we<&K9a7IL?T}wii3Uav_w_fA8WR2LCWD>9 zZSSA<#fRNJD{AY0FqduRTIm z2p$DTj-^&0CPRea`JwT5l$n~T2D6rIi0H6bfo)IiGFiGz@*98s)VFZ+k2(BNSg$#b zI}1gfvea#t0irrfQHslfl$?#x*c<=0{X@UBbDAs+acWXlcsezuFh!9GFZdUY9*wot zn?#rA9D~$*s@vMl;<+OShDkDn&rF>+-r&~5ytRlLfHJJVPu#@P&V>VxNl0GDQi*&9dSU&UkH#tUW(jHJ@#xD5UJI-JDb z7I6QtJwSSg2nqBTYb(ekA>3z%Z|2NwX<4egwExPHo#x!mgm_2j@T@=>pG#3Ts@#>< z?=$(`5;}#NNF=gPmnUm2A^jo=KIs5E+R-Hw07J}pfzq5U&G~A0JKYrRK z<_9|J5xXFfS53D16oMVIaS0`L4-cZW`=V_Sqr9toi~iD7!hd{4Sr~3G(yAsAwseG( zq7Xy7v3igJz9-V&gVzbaUz8m|G z_HBGjUdh!MCzDq+qUBB_g=`JTfcL`z4izwt+oP6&U|%xL$%Aq!(H9TB-;^Ih3m(LG zAqjLX4y`93tTQf#$G^rkS{|1_*MJcthqE!pXfzZDkvE!i0YN(TmNE~gTB7SI2P=j$ z8UA2HmG%4$&#`pcWF@eb*Hr5gjc}|75gw8paLV(wi|{9+>$mu@E$%`Ej*eLmMjY5- zYS)GvH)94#E z;fMZb1VkNpA_0WPR#W&rIw3CCDesFX<+9}!ibU~Z@;}NS356FHkf6f`6z$O1-xLj? zbcOt;<-eSmFNbdk3EeY2NcUOam6GG7j9PnkEmX)0s@4iY7rs@ zGHPRn)k%p~d13oEg}y~AiRZJ~7nZ&=+skl8N+X5=4<_+JgW*9tXMbbSNDzCq?s1cGwt?^eElaL+cWD-$@ z(dY4vLz6PI9BaoVn4FpMd>`Hq12mDH@7m)^f4#@qmEY=or~j@Yx_~-g`ZZOV;F+na z-J4g@1?;;lsaY%K7^dYiCqVVAvhF>)l02#gB&zN(>fMtpKhrz89fDudkA##E>a6E8 z#9sLgpST-#ugI6HJ{44eh6s-v+Gf55k~CAUqV9!mxIAmQ2zh|tce zQk3G-%RX$SzQLjya=7&6{DplSbys|9V=j~+?|`u`sJF;Q5G{4NaBr*xbA7MTMoutb zfpaLj!alLil~HuT{;)rOn|=SV!zevvxOcOV2_rY zMJreC+Ela3gOr4f4((ETGFxoI8r8b)Qj|g~NvrLhgL`XzBEO!!d}{ousaBlK@A~$y zSubal$SNPC4RA6*v4@H(Ft=S?2aQ>g9`Pe#_Xc^VX)nG?Op{^?+r)|KZ01HizLzt% zmfh|KJP>{~nwRa>YDG~RXGhePY78$D&?v>0j3W9=C+DZ zltgrb6UGR)h(>yKwsWxNwUh?5w*T(_-S9i@^i;~ zipWR(z*14P{2r^*w+r$QLQa0;%W4meonF}{^`CDFT!Q!I$?zQ1DzHx|e%Bim3KR^% zI1EnJ84pdkOnS8MCRs;zvT-`*;+b{Y8D9Fnsbn=AdT>iOm7O5R#Rx}>>*sxqfTLx= z??Q$6bl~~R=joGxxUeRrZonbP&z}$rguzm#CjAQBv2-}}xJ0=5m*}B5K~?lo0nc%s zHN|dLR^B;j5E?L;a0XpA(TEygO?L}y^qX)*52TeWXheT@bbE_;7q3o{%IPGZq$OT~ z?U=!PVMFU`aK&l+u9J*V$nt4~P0Zh`p!NA>l%!9!I1i@asd0d!cq36v!Z;<#BY@su zbI~wmWya*ueT?GP@7J@(fiH<2Z?np@Mwmpu+d z$OGk^-Yqyi5&pgfH_=D?sK%n@3`qBF<5PC_ z^}&l(9i26Kr*|2Y=a`}@RF!Db-EZM+Plr$g0LR(U%zpBFbuaVo4(HO+0$}XHC8&jc_`0Sc zEByiC5E$Y9T@~j>{+9zur100p+7D5eW>pcPjN)b3nD}tR5BEi#2G6AU=Xe1=nW7X} zXxBlJNLq*8=m+ggcget%9g91aatLJ~iaRkmch3a&&&M@d1otSIj8R6ogN=ZLFXmj0 zrL=u^{|=KG&0!OwnE{P~)c6xZY2nau#-5DCZ%gV$J=a-n^s+5J+>meX%L-m0|fPx#??v zh5dow@oD50`+0Qq(7Jv#K}`oS`nx1y71m7+iZr!A{_dL+71$ga+J9chz^qjx6Bz3bmjH*Qkqylo#FddbXT+Op3Tl< z_c(u*X(mSv9XGB|J6U5u%a7%P)tY&b`C$r|RhcRaof{n^!YEbgv&42SlKXDY1&pn~3xI2C?m_wWR+_JmC%X7}8eN<3CC&Gh)w=I$B=F}=a(ToARC;zKg#ON%y z@pOMxHWkTJ=uZj2=9>(gVZUt(N*H*@9%id!w$^-><017-Qr`WIQ zhZ1k@12Cu#e@q^?pHO9Tq%sS+x#GjSGk|cB=nKFw>qUP`bPaxWQ z(GZvm(%|Kn3$jcl8a-B*ylv_Fdy*NdAwCHut`FJGXzl&0uAk-U2U5n@%n?ep=x$0x zQXs^K1lSNF{y`vqQ;d=JtLVZ%3JGAlLpF*ke?L&W03Z9*7Y-AK%$w?5 zV8(%z#Be6L;}1y2(3i>0aIVIWFK#E6jVM<|jd!?_RVn893~NQ&cvIQ`!b{L2YssdO z$ETV{x{d7ne#gqTK8Hu0hW#_?2T8IYGA~yHubOHGrfr$X@ca}g4&sY#p`()jz(mQC#dtc53|iOMIsPt1r1@*+fRp)Ez+imoanfq;I>1Ic)mxzBV7{jU>TbPTk; z&Nr_w>*21j@ZmrR&{5XR&!{vSh0l=)HbZRu*~2+w_)3R07!+<=&UFq}`ZzN?l6@N7 z-d*vVyB0%T^2-*G4{^3Ob>W|4+*|c1~E#l_^!ervxV-(2IV;CSh*da9Of+T zX{Bjj2?bWv-Y$~CiwQABLAG-ni#v-E4h-E2Ek|JpDmICT{DezcCeVoMP9x7x{y2ym zbw3|_6?C@;?C~F$^W|%Zs>QCjPO(se&O&Y0KM9D4&NqiX5~fb|9~6i38r z-Avetb|;pDnT44&n0up6e1&MoP7mw*aWuyjJeppe83L3iy4OXR#%1g$kqLtywkOGb zqXvU_$-C+U=J>9YyNmt8vVCO)zCe5-T8LB%r2~4;N@g4z&KQ$#l7V&fJEvzFol8Z* zkk=ewJtNV#L4OQ#5`IPK)E+IpOhN)EB7;YVZx0i6 zpJ$L{ycUH0F-XNj)Z6H4_W>`;rgXMZgatvpujyWD;@ssl`q1!Z+@Az=e;9lflQB4U zmErizTRCRsQV-QYVH@X^HNO8iJ>!||%T)vEsyIL5UxQ5;yrw3p%2%iFb}YDCeqB=( zeSrTb*n#aRuQIu|F>{05CjV?H4sHH?h z>ejo+9#xC9*fR8O34<7udZNDew;9$Fb&6X|;*4x-y}>PyWAs7M%g}4wn+^QQUCa?Z z0j$VIz*>NZWU+37G_7?6EN8Xx;=^Kd=OGeiG_McHwU%Y|2x& zxY#rtJpW_v6*??^@4KS(z*FYB{?^AfIv0o(e#H{o^Vxo52RVkPp zW-BQRgubtTmn1`QkcV2Vn9hO)<&}W|zvs~v!_GP?iZD&g;lX_=wV2AU%I7b67UX{w z!zl`{PO9OS_2V-R{GUmhOW{940&{3e2dg2z+}e06=5 z8SXP73n+p~`T-ff?~kPCa%x?B<3GI}%SERLg{TN)ELc7of^-xSsX;ih>adt)a#Qbw z=@B&_IB9F|XSRy1>H_^LdP<#(gQ$)Kc9(U}ba{>TtuCarrXF?`j(8}nqW<&-HaK^b z9lBfp*rTxE@mARJ4!?o{TB&u-rfncI!Wu&{vHo7Jx^&cMR_4^N6-)%3;incEhA&Sn zRNp>O33Q5;f}n$8gfWNhR;GU7t42_!!?5?VN^4{YP{~2MM{pmeZFz80EwSF80r}w3 zo(ncWWHjzKarJlZbCUq9i+#d6Of^`Sen!BhNSTu9nY7B;;5)G1n`B&hY;vK+u>5v2 z+fj&2As3tqrPN)W{13${D;-r^eo>K%e7#yFHhd)y&J4g9eaK2Y`02EDN8Lb#YNmsE zF~@iR%w9{Ii9F~7#%2kxn92fiI|J&P{V!ooNGxZj7^9($07VPTlPUks#hI-rma|36 zl7eJY+RFH_!tTC1SBW2yl8!cGhRv>VEyr%gHzRrqPwUkYsVyeca)HG9p|AxLetyXr zJdLNtegYaLAwV>*q(^CQe^#=_|C&20lzntc`X-t@^fNml%PT`El`ZXv?vl}p>x~bx zv^Dr3`Cje=lj*u^#id-~>Kj*PI(?I0SpT91^bZ&(5&&=-j3DpS#gYIE({$hXr?+?} zIG1gMwh8dw44cjtJzvXGhvQ#5NN(x}BBk3S>_r(bzWmHv3q@K%7gx@J@FMx7f#;aj z@OK@KiSUYKTQL@jN;ZfUIQc4#1OpIZhjPh^O&Wc>+Z!Bq#j1U9#9jisN3x+=)dCQS zY3l8ZKf}iX;hyH5B-~MmYV~$}JgoB(wnEdQ^9;7ldZ23i_Sw3#p?tW=L2r3k^x$z) z@m;LQ%%d3=Y%e87zrdY2EHw83(yD{BC5Wta(U&~pmEs_gCkdlTE~l|Gj1E{GRPBWX zB(j3H9#UW_9b14{#;CXBKURW)bCaMDV7*wE^VuKwRKxBZq_I;jm1%uk1F9|pRmt|{ zz&tP9RF-{t5I6ki~Y$UUfhJ5+nlc&?gVTBIpJx-ef9T>;+^Auix2mp4rq%r3ea*p;=&~{Ule^S2`83{k^GOs9V@SDE9(10? z5o8#rthk*iMT}WbH6GQ(n$u#`|dK7lNa<`dH|JiJKcbBOsAX;;mKjq} zO1V-ZTBLj##*9N(_Uk=Y1oAie-{%Yf1vxC8_&Ke1L$XX9#Z{KHcX4nAq!%rierHd1 zV?d8D<_|MaM*)P8s>%3xtLt{wUpzmm4f;mGwTzKHe6P-VdzPPXqJom+g&hb5n&>bf zK?!OKP@XIaH{A8MbT%Zml3zA4uRX#QuTDhfF*>pE`MaTI=gv25iF1;w_mWQ@LAZl$_rIviJY6_mxpqc3Ze~3ra|rNQ0DgDhdM9 zB@I%No9<01BHbMurKF|1k?xf4ZZ^#Z?z{Cn=X~dUW8D9Dj63+Vi}kK|t~KYH^OkoZ!o5&?-bwSE1ZgT2Uz=ZNyF=kno7HHz2eQ0(uJ+;~-lJN%8ig^$@h06U+ zgeXl9&**Q1Jd&pg#cEF+UMvige~JPk(Wx8K-pq_HergczZ_k8+^^Nv7Y!`&$&pG%8 zr1=8ldhHd%?t`!RL$6%^HjL5RmN3M5NdFtIP;AM!UY@j1Oui7H9lW6BwEH{`4<9N0 z-muh5YIEmGQoUsvMD==Gk(_a`W#9hq)yz1+{IzV@@JX%Y3+!mvNXE_hZLK{Y0}~c^ zx`0n96(Q2k=qJDmael*G!L(@9Lsujda}olI`?_>k%<==ih_%MU-z`?=2;g zsQBx$oYZj3?m7$ec5EyNkpwY)Fj=P%JBvOK|!2#)) zzekC*C@0}WG05Zi=n=Q3ifUmm4L?=T+5(w%5+Ni^Qp?TjY5b{Nv#y+y;=^$#{g?l~ z9Rwu06p7_hBL;dwq=}oHm(1BwFKLy?I2Fqx8-hJ|4 z-YRVdm*HRyH$fDYtgBs5ZeQ_U*$0%h7*@%)pPOsQlL6ac&<%6+#AonX}tJ!SW+PrGjsS zZ2WGzC{+!8*`XNeT;_**J4BIx+xmti1WW=9{bbTPor%ld*pG2ZuSNMTQa-^43+n#5l}f#8F(D4uv8!D-DO=hom0HzkBZ&j z8tB!SC5tO|NmD*wMa?}hkOa9!Nh!M5lH#)Lqo~^x-Prx`j4mKCnJsz60%b6xhph( zW|zWCVV@=?^Ndm#adDA;L(ae`fT&+tn(7gwp8e0bjZPmJ4=$r<%3z{V6EqF0kYXKFv{VAYEQz{>rJ#=s!~>X}nkD?^%_ z828Q^W6}&ms*eA~1uzg0GgW4{{KWM>EG*1yvT$GkD6al+P`7IfUGO|yXHN;$1xnub zb3SJ3s=2yGxc50=UVOdw_Z%xOFtDjqmj@}9=onM4gb3!Rlk(({A5ZC{luXOtREWQl zmNw6KJ4r|T2ojhEAf>rc5`7#hkz}(R#OW0#Aut9%3di9E+G)6(BwXpGAThMjFmhm-ls?S$< zSP-Mb=<`<5t1N>3SMC9+&y$)VA~)mXYod&*g!Y$4fyoc}=Y|8ad zV6Pq>-XBRrQx;)-xYTY-h#Qi#)~jvFE#NCwgRrrxx%b$P-BY-HnMI>CvyJC7yXV&m zL5$yNZ{aN^R>ieB*iF|nR#GXv2W8N~<4(8CnVEj-yUW~2(L?(bn<~D=yGj1l=lg~C zVIXf|XKLDG9>`V2h0~n2i9Ksg}&L8p12Vaz5G`e*Ku-pc{8BpWktaI$7Ltu z7m+~k%~`V;c|L{Laz>8JsGIdQx#=D=sSZ0pz0F7faR)ief?a!<*-S;)`-9%27G%86 z-~?HbuKI(9KCc5uNEjq@@c4WdltQ4GY(pf=nwS|O)W*USuj;f%1=NhfoA^47ME=;a z5|nFddD@WN1ANe4gYUhuF*GeqsWY{F;B-15+JV68u_H1D!m$#+?Nb zN6*=tRh0XD(FHZ0`SVwz`NV|r^952}?>F=!5G4xOpLz2)=~%SMZnW3$MJK=NWMo}R z;x?6p7J zw$G(h?e#T@i*pMn=#1d6KfJ3=-4rltLve&1cWGx&VhZ*m=(Ri5pDvRd3^C%c`ArZPwH4`J%nu!`6z%JC1MVYBes3Gz53Xzb9N?QvDG9^b_j1X zXhScv$U*EF{`EHpo~fD)r_$756A{GmvqqpRYeV5C{<8660dG32|9VL9rcfYtA5*vD z7_z`Hl)^v69@ITZ+*VN1R42H6xw-=FegC2tff*HOpYPXmU9>r<-CT&Ar>tB~Uvb*! zE;$?m_luPnDrYp*@s;{)eLYq21FUv(iPySZah)vGId0?1adyAD-qZbJzTe?d>D2A^ zVo~)%d~U3!5c3N;u@1>T`L&X9$JjXk;??W~eTRM3I{=*KvpV|<4a#Jbj#iL+{V&U? zP16$Xr|pKN3%DYjY?vS9%N&I|@+bSV4BsuN1w8X8D5?s!O6CWpUyVdOb6rNKa%&N} z{eNu{A$hWPYuX-k6GgI+U@2JLZ`K37v$1Xyx zmImh0uq2+~y|z_O0@sHk7|_PG&TyIBPcOd#YTS3x+=!{)PY!Ng=`H9N$A_4R04<06 z=wCZ`6lo9SP@8@kHbG(E3LeAk+`wNH;W#^Y8c+J!$1B>4E`~5>eJESpd zXWR<8fkgZ!U!TD}uHYI?`6}*Rd2m`<+QzpKj-K8m@`k$$A5LKJIOlx=pHH)H7s@mNpGCb9@?E9ny};Jify%A>drJT=`7wUG#GS7NmJRH*;XaVC2+scCFanl*zEet zSKUgHc;7B}#OE}==)uKcbEXQeR_jv6`ew^QM%~y;$F^&*! zy-fiJ8{8CKCh^>w$lL)=Q7q8rBn}&iaJEw*aFL(8aa5eHyl%Dm(TX-$Ad1zdb6s6n z_N3-Vs9`}7PcKf<*i>b=g=5W<+Os?yC0Jf=8ddbXeTY`M%JReES=W7@NUuza(P(LQ zdx68b^7$0ApM7O30)6sW@v`;DlQ7X5ojvl43>>9YhN9+cAk$qoV&Veytv%C~X*$Ma zS&LGasN$AT*W3}z*F!P&-I@3t&|`iLbOLtvB4jVR6lk*baPWJ3bz=SOI{FKrrW^Nok$|>gZ3yN17j8;lofF@i@#T7Gaq4sQ0>JKrule}Er z)Vsp>M3}9OCnv0}`FH~-MEQE{{yD9-Ss!A(Sh9Lxhfh1;0Rdn{^Iv3y zmRc@H>GRt9I{qekkx<8c%2-hs`JgDNkq@bVGe;pQI#|EKO_>Fj&Z{y#py1E)A^p47*bX_nyQMSSJ4l_G30XXM`x^Zn+h{JvZ7xt z=R(}2(5v%|vM=kf0+$pkA*&DPp5(%#1i;G#s>uT+M!t8$3;8k-{J1MhQ7!X5DhQF1 zF?I(UQpH>~yKSOP@x@&aQUtXs_3sYPJ&&##tEE0DIhXgFVv&G;6@O+Z>}*nJ zYF+0&|%sD{^2C46?skGET9kmD1qX0H;hicE5H+|J`k>CvQ(ExLk z_UQZ4uNZn{=Si0uJ>YiHFw~U3;C0S7T<;XvS9;Kid!EbK3%G>~z&Ojx=Giy}z+xPN z?oDgaA#;PLGii4rH+z(2wTczDCo}zM&a&@j)~DwCyu@opTvqE$1b

-a&Nl=<3^V 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 LO{PZ5JoNtorc5#T From 97b3978fb86591227d7677882a294d0ec3b1583f Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 13 Aug 2026 22:26:12 +0300 Subject: [PATCH 02/32] [SDK] Rebuild the session core on PasswordSession behind an AtpAgent compat shim (#11343) Co-authored-by: Claude Fable 5 --- oxlint-suppressions.json | 5 - package.json | 2 + pnpm-lock.yaml | 2841 +++++++++-------- .../EmailDialog/data/useAccountEmailState.ts | 19 +- src/lib/strings/__tests__/errors.test.ts | 126 + src/lib/strings/errors.ts | 39 +- src/screens/Login/LoginForm.tsx | 15 +- src/screens/Signup/state.ts | 45 +- src/state/persisted/index.ts | 12 + src/state/persisted/index.web.ts | 23 + src/state/persisted/types.ts | 9 + src/state/queries/preferences/index.ts | 4 +- .../session/__tests__/bridge-agent-test.ts | 573 ++++ .../session/__tests__/expiry-rescue-test.ts | 104 + src/state/session/__tests__/mock-fetch.ts | 126 + .../session/__tests__/provider-abort-test.tsx | 145 + .../provider-session-events-test.tsx | 559 ++++ .../session/__tests__/session-core-test.ts | 1027 ++++++ src/state/session/__tests__/session-test.ts | 1103 ++++--- src/state/session/agent-config.ts | 12 - src/state/session/agent.ts | 404 +-- src/state/session/bridge-agent.ts | 442 +++ src/state/session/create-account.ts | 243 ++ src/state/session/expiry-rescue.ts | 30 + src/state/session/index.tsx | 419 ++- src/state/session/logging.ts | 164 +- src/state/session/moderation.ts | 36 +- src/state/session/network.ts | 23 + src/state/session/reducer.ts | 123 +- src/state/session/session-core.ts | 438 +++ src/state/session/session-data.ts | 107 + src/state/session/types.ts | 11 +- src/state/session/util.ts | 26 +- src/storage/schema.ts | 11 + src/view/com/composer/drafts/state/api.ts | 2 +- 35 files changed, 6871 insertions(+), 2397 deletions(-) create mode 100644 src/lib/strings/__tests__/errors.test.ts create mode 100644 src/state/session/__tests__/bridge-agent-test.ts create mode 100644 src/state/session/__tests__/expiry-rescue-test.ts create mode 100644 src/state/session/__tests__/mock-fetch.ts create mode 100644 src/state/session/__tests__/provider-abort-test.tsx create mode 100644 src/state/session/__tests__/provider-session-events-test.tsx create mode 100644 src/state/session/__tests__/session-core-test.ts delete mode 100644 src/state/session/agent-config.ts create mode 100644 src/state/session/bridge-agent.ts create mode 100644 src/state/session/create-account.ts create mode 100644 src/state/session/expiry-rescue.ts create mode 100644 src/state/session/network.ts create mode 100644 src/state/session/session-core.ts create mode 100644 src/state/session/session-data.ts diff --git a/oxlint-suppressions.json b/oxlint-suppressions.json index ae3513a254..197bd43b1a 100644 --- a/oxlint-suppressions.json +++ b/oxlint-suppressions.json @@ -1456,11 +1456,6 @@ "count": 1 } }, - "src/state/session/agent.ts": { - "typescript/no-explicit-any": { - "count": 1 - } - }, "src/state/shell/color-mode.tsx": { "typescript/no-floating-promises": { "count": 2 diff --git a/package.json b/package.json index 51febee03a..93d671ff31 100644 --- a/package.json +++ b/package.json @@ -98,6 +98,8 @@ "dependencies": { "@atproto/api": "0.20.39", "@atproto/common-web": "0.5.7", + "@atproto/lex-client": "0.3.0", + "@atproto/lex-password-session": "0.1.8", "@atproto/syntax": "0.7.2", "@bitdrift/react-native": "^0.6.8", "@braintree/sanitize-url": "^6.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6ffd6a36f6..b4baf4a374 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -248,51 +248,57 @@ importers: '@atproto/common-web': specifier: 0.5.7 version: 0.5.7 + '@atproto/lex-client': + specifier: 0.3.0 + version: 0.3.0 + '@atproto/lex-password-session': + specifier: 0.1.8 + version: 0.1.8 '@atproto/syntax': specifier: 0.7.2 version: 0.7.2 '@bitdrift/react-native': specifier: ^0.6.8 - version: 0.6.14(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + version: 0.6.14(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) '@braintree/sanitize-url': specifier: ^6.0.2 version: 6.0.4 '@bsky.app/alf': specifier: ^0.1.15 - version: 0.1.15(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + version: 0.1.15(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0) '@bsky.app/expo-dynamic-app-icon': specifier: ^1.8.5 - version: 1.8.5(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + version: 1.8.5(83e0f64f4281e4798012214ad5b2d72d) '@bsky.app/expo-guess-language': specifier: ^0.2.8 - version: 0.2.8(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + version: 0.2.8(83e0f64f4281e4798012214ad5b2d72d) '@bsky.app/expo-image-crop-tool': specifier: ^0.5.1 - version: 0.5.1(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + version: 0.5.1(83e0f64f4281e4798012214ad5b2d72d) '@bsky.app/expo-scroll-edge-effect': specifier: ^0.1.9 - version: 0.1.9(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + version: 0.1.9(83e0f64f4281e4798012214ad5b2d72d) '@bsky.app/expo-translate-text': specifier: ^0.2.9 - version: 0.2.9(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + version: 0.2.9(83e0f64f4281e4798012214ad5b2d72d) '@bsky.app/peek-menu': specifier: ^0.3.2 - version: 0.3.2(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + version: 0.3.2(83e0f64f4281e4798012214ad5b2d72d) '@bsky.app/react-native-uitextview': specifier: ^2.7.0 - version: 2.7.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + version: 2.7.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0) '@bsky.app/sift': specifier: ^0.3.9 - version: 0.3.9(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + version: 0.3.9(2d5c14016450c7e55c8cd0dffcca6f69) '@bsky.app/tapper': specifier: ^0.6.1 - version: 0.6.1(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)(tlds@1.261.0) + version: 0.6.1(8d6e1ad3cb49b1a6214533c496326d11) '@bsky.app/video': specifier: 0.3.6 - version: 0.3.6(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + version: 0.3.6(83e0f64f4281e4798012214ad5b2d72d) '@bsky.app/video-compressor': specifier: 0.2.0 - version: 0.2.0(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + version: 0.2.0(83e0f64f4281e4798012214ad5b2d72d) '@emoji-mart/data': specifier: ^1.2.1 version: 1.2.1 @@ -304,7 +310,7 @@ importers: version: 0.12.5 '@expo/webpack-config': specifier: ^19.0.1 - version: 19.0.1(clean-css@5.3.3)(csso@5.0.5)(esbuild@0.25.12)(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(html-minifier-terser@6.1.0)(postcss@8.5.14) + version: 19.0.1(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.25.12)(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1))(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14)(supports-color@8.1.1) '@floating-ui/dom': specifier: ^1.6.3 version: 1.7.6 @@ -334,25 +340,25 @@ importers: version: 9.2.7 '@lingui/core': specifier: ^5.9.2 - version: 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(@typescript/typescript6@6.0.2)) + version: 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(@typescript/typescript6@6.0.2)(supports-color@8.1.1)) '@lingui/react': specifier: ^5.9.2 - version: 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(@typescript/typescript6@6.0.2))(react@19.1.0) + version: 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(@typescript/typescript6@6.0.2)(supports-color@8.1.1))(react@19.1.0) '@react-native-async-storage/async-storage': specifier: 2.2.0 - version: 2.2.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0)) + version: 2.2.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1)) '@react-navigation/bottom-tabs': specifier: ^7.15.5 - version: 7.16.0(91eab7bd987834c4c877d5cbf9d3796c) + version: 7.16.0(15d086c577324dcb87d77ea7fa3207d2) '@react-navigation/native': specifier: ^7.1.33 - version: 7.2.4(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + version: 7.2.4(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0) '@react-navigation/native-stack': specifier: ^7.14.4 - version: 7.15.0(91eab7bd987834c4c877d5cbf9d3796c) + version: 7.15.0(15d086c577324dcb87d77ea7fa3207d2) '@sentry/react-native': specifier: ~8.18.0 - version: 8.18.0(@expo/env@2.0.11)(dotenv@16.6.1)(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + version: 8.18.0(61f03fc01fabd5cbd0e83cc2a57309d9) '@tanstack/query-async-storage-persister': specifier: ^5.96.2 version: 5.100.10 @@ -433,115 +439,115 @@ importers: version: 5.0.4 expo: specifier: 54.0.35 - version: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + version: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) expo-age-range: specifier: 0.2.18 - version: 0.2.18(patch_hash=c325225749993424461eeece1d97a99b19c00da2ebc958a73c12e4eca0c39306)(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0)) + version: 0.2.18(patch_hash=c325225749993424461eeece1d97a99b19c00da2ebc958a73c12e4eca0c39306)(83f21d12a6c1aa99b4a65d070875cc68) expo-application: specifier: ~7.0.8 - version: 7.0.8(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)) + version: 7.0.8(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1)) expo-asset: specifier: ~12.0.13 - version: 12.0.13(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + version: 12.0.13(93a6abd34ab5ae64aff6a80525489f34) expo-blur: specifier: ~15.0.8 - version: 15.0.8(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + version: 15.0.8(83e0f64f4281e4798012214ad5b2d72d) expo-build-properties: specifier: ~1.0.10 - version: 1.0.10(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)) + version: 1.0.10(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1)) expo-camera: specifier: ~17.0.10 - version: 17.0.10(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-web@0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + version: 17.0.10(8d9f18414d5ddcf91a3e8d2e3aa26778) expo-clipboard: specifier: ~8.0.8 - version: 8.0.8(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + version: 8.0.8(83e0f64f4281e4798012214ad5b2d72d) expo-contacts: specifier: ^15.0.10 - version: 15.0.11(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + version: 15.0.11(83e0f64f4281e4798012214ad5b2d72d) expo-dev-client: specifier: ~6.0.20 - version: 6.0.21(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)) + version: 6.0.21(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1))(supports-color@8.1.1) expo-device: specifier: ~8.0.10 - version: 8.0.10(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)) + version: 8.0.10(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1)) expo-file-system: specifier: ~19.0.21 - version: 19.0.22(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0)) + version: 19.0.22(83f21d12a6c1aa99b4a65d070875cc68) expo-font: specifier: ~14.0.11 - version: 14.0.11(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + version: 14.0.11(83e0f64f4281e4798012214ad5b2d72d) expo-glass-effect: specifier: 0.1.10 - version: 0.1.10(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + version: 0.1.10(83e0f64f4281e4798012214ad5b2d72d) expo-haptics: specifier: ~15.0.8 - version: 15.0.8(patch_hash=b33910ba2753c4eccdc885b449e6e33aa90b9cefa75ca8639762a26013141410)(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)) + version: 15.0.8(patch_hash=b33910ba2753c4eccdc885b449e6e33aa90b9cefa75ca8639762a26013141410)(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1)) expo-image: specifier: ~3.0.11 - version: 3.0.11(patch_hash=6d46ffac33426c5285777da5bf5f88c68c8ce27290595dfa65f38c478930c2c4)(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-web@0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + version: 3.0.11(patch_hash=6d46ffac33426c5285777da5bf5f88c68c8ce27290595dfa65f38c478930c2c4)(8d9f18414d5ddcf91a3e8d2e3aa26778) expo-image-manipulator: specifier: ~14.0.8 - version: 14.0.8(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)) + version: 14.0.8(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1)) expo-image-picker: specifier: ~17.0.10 - version: 17.0.11(patch_hash=47b28f6ddb8bcaa6483f8714c82daaa0001122f2b0dac6c05d19e98a61a6ceab)(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)) + version: 17.0.11(patch_hash=47b28f6ddb8bcaa6483f8714c82daaa0001122f2b0dac6c05d19e98a61a6ceab)(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1)) expo-intent-launcher: specifier: ~13.0.8 - version: 13.0.8(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)) + version: 13.0.8(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1)) expo-keep-awake: specifier: ~15.0.8 - version: 15.0.8(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react@19.1.0) + version: 15.0.8(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1))(react@19.1.0) expo-linear-gradient: specifier: ~15.0.8 - version: 15.0.8(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + version: 15.0.8(83e0f64f4281e4798012214ad5b2d72d) expo-linking: specifier: ~8.0.11 - version: 8.0.12(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + version: 8.0.12(93a6abd34ab5ae64aff6a80525489f34) expo-localization: specifier: ~17.0.8 - version: 17.0.8(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react@19.1.0) + version: 17.0.8(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1))(react@19.1.0) expo-location: specifier: ~19.0.8 - version: 19.0.8(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)) + version: 19.0.8(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1)) expo-media-library: specifier: ~18.2.1 - version: 18.2.1(patch_hash=2b84c17999bb0c977eaef7fa8ac297f7074d91c94fb63726b42d2a277b26b39a)(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0)) + version: 18.2.1(patch_hash=2b84c17999bb0c977eaef7fa8ac297f7074d91c94fb63726b42d2a277b26b39a)(83f21d12a6c1aa99b4a65d070875cc68) expo-notifications: specifier: ~0.32.17 - version: 0.32.17(patch_hash=a45a8dcf3d8c4b5df4ee0e62bc79b755fcf9d28ca51a6f685b85830bf4afc2e5)(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + version: 0.32.17(patch_hash=a45a8dcf3d8c4b5df4ee0e62bc79b755fcf9d28ca51a6f685b85830bf4afc2e5)(93a6abd34ab5ae64aff6a80525489f34) expo-paste-input: specifier: ^0.2.1 - version: 0.2.1(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + version: 0.2.1(83e0f64f4281e4798012214ad5b2d72d) expo-privacy-sensitive: specifier: ^0.2.0 - version: 0.2.0(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + version: 0.2.0(83e0f64f4281e4798012214ad5b2d72d) expo-screen-orientation: specifier: ~9.0.8 - version: 9.0.9(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0)) + version: 9.0.9(83f21d12a6c1aa99b4a65d070875cc68) expo-sharing: specifier: ~14.0.8 - version: 14.0.8(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)) + version: 14.0.8(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1)) expo-sms: specifier: ^14.0.7 - version: 14.0.8(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)) + version: 14.0.8(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1)) expo-splash-screen: specifier: ~31.0.13 - version: 31.0.13(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)) + version: 31.0.13(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1))(supports-color@8.1.1) expo-system-ui: specifier: ~6.0.9 - version: 6.0.9(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-web@0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0)) + version: 6.0.9(41b38c851305f17c9d2f63b3bbab71a9) expo-updates: specifier: ~29.0.17 - version: 29.0.17(patch_hash=04f28cb005b770e9ae8f0065eab96e43cbb1e58107f5f6ad1bdd18f6deb66487)(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + version: 29.0.17(patch_hash=04f28cb005b770e9ae8f0065eab96e43cbb1e58107f5f6ad1bdd18f6deb66487)(93a6abd34ab5ae64aff6a80525489f34) expo-video: specifier: ~3.0.16 - version: 3.0.16(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + version: 3.0.16(83e0f64f4281e4798012214ad5b2d72d) expo-video-thumbnails: specifier: ^10.0.8 - version: 10.0.8(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)) + version: 10.0.8(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1)) expo-web-browser: specifier: ~15.0.10 - version: 15.0.11(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0)) + version: 15.0.11(83f21d12a6c1aa99b4a65d070875cc68) fast-deep-equal: specifier: ^3.1.3 version: 3.1.3 @@ -622,73 +628,73 @@ importers: version: 5.1.2(react-is@19.2.6)(react@19.1.0) react-native: specifier: 0.81.5 - version: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + version: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) react-native-compressor: specifier: 1.13.0 - version: 1.13.0(patch_hash=58379dfaace6ced8590cb341c77f2ca8099dfa8f7df6297032ec51de767a9925)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + version: 1.13.0(patch_hash=58379dfaace6ced8590cb341c77f2ca8099dfa8f7df6297032ec51de767a9925)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0) react-native-date-picker: specifier: ^5.0.13 - version: 5.0.13(patch_hash=92943fb79d17d7342a29bbb12b0d8ee3cf6f7bca12ed322dc8d124a3e9fb75bd)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + version: 5.0.13(patch_hash=92943fb79d17d7342a29bbb12b0d8ee3cf6f7bca12ed322dc8d124a3e9fb75bd)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0) react-native-device-attest: specifier: ^0.1.6 - version: 0.1.6(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + version: 0.1.6(83e0f64f4281e4798012214ad5b2d72d) react-native-drawer-layout: specifier: ^4.2.3 - version: 4.2.3(patch_hash=74f2c043cc22ab87054f219e7c7373a509b779b18bfa79d27e7d051d73355130)(f8669ab0fde0772e831a7516fba74dd4) + version: 4.2.3(patch_hash=74f2c043cc22ab87054f219e7c7373a509b779b18bfa79d27e7d051d73355130)(80d195d8b17891585aee78a1fb66460a) react-native-edge-to-edge: specifier: ^1.8.1 - version: 1.8.1(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + version: 1.8.1(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0) react-native-gesture-handler: specifier: ~2.30.0 - version: 2.30.1(patch_hash=39b9e51b3977fae8ff61764b8c32934fe3fbddaf21587b4661fd73b7c6dd325b)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + version: 2.30.1(patch_hash=39b9e51b3977fae8ff61764b8c32934fe3fbddaf21587b4661fd73b7c6dd325b)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0) react-native-keyboard-controller: specifier: ^1.21.8 - version: 1.21.8(patch_hash=ac52bf7502ff3ad34a8594b5e1a916b96bf87a2523b6abc87c31f03607d52271)(react-native-reanimated@4.3.2(patch_hash=ab1c095bc473ec5bfd9ead76e870c566660e355e11c5078cfdeb35922d7b2f05)(react-native-worklets@0.8.3(patch_hash=f7263a59ae1c0170af8dfe9a5704cb374d29214a430b4b2841f9b7886c9ba8ad)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + version: 1.21.8(patch_hash=ac52bf7502ff3ad34a8594b5e1a916b96bf87a2523b6abc87c31f03607d52271)(a7f993fe7a53ab427398397086882821) react-native-mmkv: specifier: ^3.3.3 - version: 3.3.3(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + version: 3.3.3(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0) react-native-pager-view: specifier: 6.8.0 - version: 6.8.0(patch_hash=c8316acd33c9aef6531e8c272c2bfab7bd02af1255969eeaab2bad5ab48531cc)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + version: 6.8.0(patch_hash=c8316acd33c9aef6531e8c272c2bfab7bd02af1255969eeaab2bad5ab48531cc)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0) react-native-progress: specifier: ^5.0.1 - version: 5.0.1(react-native-svg@15.12.1(patch_hash=ae309e9542214f5ae54935e253dedd236765feedac7be343f81d532d7fe09310)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)) + version: 5.0.1(react-native-svg@15.12.1(patch_hash=ae309e9542214f5ae54935e253dedd236765feedac7be343f81d532d7fe09310)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)) react-native-qrcode-styled: specifier: ^0.3.3 - version: 0.3.3(react-native-svg@15.12.1(patch_hash=ae309e9542214f5ae54935e253dedd236765feedac7be343f81d532d7fe09310)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + version: 0.3.3(react-native-svg@15.12.1(patch_hash=ae309e9542214f5ae54935e253dedd236765feedac7be343f81d532d7fe09310)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0) react-native-reanimated: specifier: 4.3.2 - version: 4.3.2(patch_hash=ab1c095bc473ec5bfd9ead76e870c566660e355e11c5078cfdeb35922d7b2f05)(react-native-worklets@0.8.3(patch_hash=f7263a59ae1c0170af8dfe9a5704cb374d29214a430b4b2841f9b7886c9ba8ad)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + version: 4.3.2(patch_hash=ab1c095bc473ec5bfd9ead76e870c566660e355e11c5078cfdeb35922d7b2f05)(react-native-worklets@0.8.3(patch_hash=f7263a59ae1c0170af8dfe9a5704cb374d29214a430b4b2841f9b7886c9ba8ad)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0) react-native-safe-area-context: specifier: ~5.6.0 - version: 5.6.2(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + version: 5.6.2(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0) react-native-screens: specifier: 4.24.0 - version: 4.24.0(patch_hash=74a74b489f8074a1974c2792c9c97aec30d8e251669aa37fc5c99993bb4a19e6)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + version: 4.24.0(patch_hash=74a74b489f8074a1974c2792c9c97aec30d8e251669aa37fc5c99993bb4a19e6)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0) react-native-scroll-forwarder: specifier: link:./modules/react-native-scroll-forwarder version: link:modules/react-native-scroll-forwarder react-native-svg: specifier: 15.12.1 - version: 15.12.1(patch_hash=ae309e9542214f5ae54935e253dedd236765feedac7be343f81d532d7fe09310)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + version: 15.12.1(patch_hash=ae309e9542214f5ae54935e253dedd236765feedac7be343f81d532d7fe09310)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0) react-native-uuid: specifier: ^2.0.3 version: 2.0.4 react-native-view-shot: specifier: ^4.0.3 - version: 4.0.3(patch_hash=a2457dc30a82cc8c21f371a6f860d9396d450a2a1b4265b1a4ee13bdb24d3268)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + version: 4.0.3(patch_hash=a2457dc30a82cc8c21f371a6f860d9396d450a2a1b4265b1a4ee13bdb24d3268)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0) react-native-web: specifier: ^0.21.0 version: 0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react-native-web-webview: specifier: ^1.0.2 - version: 1.0.2(file-loader@6.2.0(webpack@5.106.2(postcss@8.5.14)))(react-native-web@0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)) + version: 1.0.2(file-loader@6.2.0(webpack@5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14)))(react-native-web@0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)) react-native-webview: specifier: ^13.15.0 - version: 13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + version: 13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0) react-native-worklets: specifier: 0.8.3 - version: 0.8.3(patch_hash=f7263a59ae1c0170af8dfe9a5704cb374d29214a430b4b2841f9b7886c9ba8ad)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + version: 0.8.3(patch_hash=f7263a59ae1c0170af8dfe9a5704cb374d29214a430b4b2841f9b7886c9ba8ad)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) react-remove-scroll-bar: specifier: ^2.3.8 version: 2.3.8(@types/react@19.1.17)(react@19.1.0) @@ -709,7 +715,7 @@ importers: version: 2.0.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0) sonner-native: specifier: 0.26.4 - version: 0.26.4(c538f2bb3fa64bd80a9a262f75d56c17) + version: 0.26.4(dd52c15d65a2604bcf203dfed917879b) tippy.js: specifier: ^6.3.7 version: 6.3.7 @@ -728,10 +734,10 @@ importers: devDependencies: '@babel/core': specifier: ^7.26.0 - version: 7.29.0 + version: 7.29.0(supports-color@8.1.1) '@babel/preset-env': specifier: ^7.26.0 - version: 7.29.5(@babel/core@7.29.0) + version: 7.29.5(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) '@babel/runtime': specifier: ^7.26.0 version: 7.29.2 @@ -740,25 +746,25 @@ importers: version: 4.14.2 '@lingui/babel-plugin-lingui-macro': specifier: ^5.9.2 - version: 5.9.5(@typescript/typescript6@6.0.2) + version: 5.9.5(@typescript/typescript6@6.0.2)(supports-color@8.1.1) '@lingui/cli': specifier: ^5.9.2 - version: 5.9.5(@typescript/typescript6@6.0.2) + version: 5.9.5(@typescript/typescript6@6.0.2)(supports-color@8.1.1) '@pmmmwh/react-refresh-webpack-plugin': specifier: ^0.5.15 - version: 0.5.17(react-refresh@0.14.2)(type-fest@1.4.0)(webpack-dev-server@4.15.2(webpack@5.106.2(postcss@8.5.14)))(webpack@5.106.2(postcss@8.5.14)) + version: 0.5.17(react-refresh@0.14.2)(type-fest@1.4.0)(webpack-dev-server@4.15.2(debug@4.4.3(supports-color@8.1.1))(supports-color@8.1.1)(webpack@5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14)))(webpack@5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14)) '@react-native/babel-preset': specifier: 0.81.5 - version: 0.81.5(@babel/core@7.29.0) + version: 0.81.5(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) '@react-native/typescript-config': specifier: ^0.81.5 version: 0.81.6 '@sentry/webpack-plugin': specifier: ^3.2.2 - version: 3.6.1(webpack@5.106.2(postcss@8.5.14)) + version: 3.6.1(supports-color@8.1.1)(webpack@5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14)) '@testing-library/react-native': specifier: ^13.2.0 - version: 13.3.3(jest@29.7.0(@types/node@24.12.4))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0) + version: 13.3.3(jest@29.7.0(@types/node@24.12.4)(supports-color@8.1.1))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0) '@types/jest': specifier: 29.5.14 version: 29.5.14 @@ -785,7 +791,7 @@ importers: version: typescript@7.0.2 babel-jest: specifier: ^29.7.0 - version: 29.7.0(@babel/core@7.29.0) + version: 29.7.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) babel-plugin-module-resolver: specifier: ^5.0.2 version: 5.0.3 @@ -794,25 +800,25 @@ importers: version: 19.1.0-rc.3 babel-preset-expo: specifier: ~54.0.10 - version: 54.0.10(@babel/core@7.29.0)(@babel/runtime@7.29.2)(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-refresh@0.14.2) + version: 54.0.10(@babel/core@7.29.0(supports-color@8.1.1))(@babel/runtime@7.29.2)(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1))(react-refresh@0.14.2)(supports-color@8.1.1) eslint-plugin-bsky-internal: specifier: link:eslint version: link:eslint eslint-plugin-react-compiler: specifier: 19.1.0-rc.2 - version: 19.1.0-rc.2(eslint@eslint) + version: 19.1.0-rc.2(eslint@8.57.1(supports-color@8.1.1))(supports-color@8.1.1) eslint-plugin-react-native: specifier: ^5.0.0 - version: 5.0.0(eslint@eslint) + version: 5.0.0(eslint@8.57.1(supports-color@8.1.1)) eslint-plugin-react-native-a11y: specifier: ^3.5.1 - version: 3.5.1(eslint@eslint) + version: 3.5.1(eslint@8.57.1(supports-color@8.1.1)) eslint-plugin-simple-import-sort: specifier: ^13.0.0 - version: 13.0.0(eslint@eslint) + version: 13.0.0(eslint@8.57.1(supports-color@8.1.1)) file-loader: specifier: 6.2.0 - version: 6.2.0(webpack@5.106.2(postcss@8.5.14)) + version: 6.2.0(webpack@5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14)) globals: specifier: ^17.0.0 version: 17.6.0 @@ -824,10 +830,10 @@ importers: version: 3.0.1 jest: specifier: ^29.7.0 - version: 29.7.0(@types/node@24.12.4) + version: 29.7.0(@types/node@24.12.4)(supports-color@8.1.1) jest-expo: specifier: ~54.0.17 - version: 54.0.17(@babel/core@7.29.0)(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(jest@29.7.0(@types/node@24.12.4))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + version: 54.0.17(ff9d3150c2730c411fa3a2f331cc40ee) jest-junit: specifier: ^16.0.0 version: 16.0.0 @@ -887,6 +893,10 @@ packages: resolution: {integrity: sha512-2c5C6YV8352JJz9Z5fNfcsstllaKGAb3cQW9aO7unMAGh/FzTGFq+xOwNMW2lTeagO6z530uxYm8AiSRM6O+DQ==} engines: {node: '>=22'} + '@atproto/lex-client@0.3.0': + resolution: {integrity: sha512-P+S3hWdIIOgNTun7J42dvxSyCkQog3Q2ShTjvcwP761TqiRex7zDWaRAaT9gvCy6I4pRuNdBCStysjSMlJeSdg==} + engines: {node: '>=22'} + '@atproto/lex-data@0.1.6': resolution: {integrity: sha512-rvovRrYWDEuerX+ZeQbdI5dKs0bYvuMjhwDOtT+pkOvdY4MkfN41DJCz/RAYVAQ7noPdaalB6pAX9euukheE/w==} engines: {node: '>=22'} @@ -903,6 +913,14 @@ packages: resolution: {integrity: sha512-mvrAd0lbyuecIHjyld8QN6MN6CBf4j0GCxLzegsvLh0SvDf+GbYWklkcQqmITL44yFQOwmA/QNIQj0Uvh7+R/g==} engines: {node: '>=22'} + '@atproto/lex-password-session@0.1.8': + resolution: {integrity: sha512-Zr3HLu6rgadiq2w82VSThOQcNfwTJNlrzlT8n9oBXDBw3CUEq3cTMatzTpdk4HYqv0unaqhaEowcKhJ4eWvSxQ==} + engines: {node: '>=22'} + + '@atproto/lex-schema@0.2.5': + resolution: {integrity: sha512-/tP3dRqaZu0EIoq0BSVzhvFGL6u0uu2KFDV21i73DpFsg6TXOTddKJ9bvgK83XdAJYL7PWi703XsaR31v9u5Qg==} + engines: {node: '>=22'} + '@atproto/lexicon@0.7.11': resolution: {integrity: sha512-ukCZMCWmiu7fAlssBbLs+IMaf2K2R9mADaTKz2jEeC/xeZ3jT77Ka0S2G+ELBB+Dc9ijBACU0iPMQLhquOkuzQ==} engines: {node: '>=22'} @@ -1893,6 +1911,24 @@ packages: cpu: [x64] os: [win32] + '@eslint-community/eslint-utils@4.10.1': + resolution: {integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/eslintrc@2.1.4': + resolution: {integrity: sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + '@eslint/js@8.57.1': + resolution: {integrity: sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@expo/cli@54.0.25': resolution: {integrity: sha512-WnUqIb8oMBhtwSfIqdCHCzcaDIpLNXItRVd5miuvWi4GO0SGo89PSsAkbVJ+LJgcaY+v5rbgMELJS9I/CqOulA==} hasBin: true @@ -2083,6 +2119,19 @@ packages: resolution: {integrity: sha512-mUaMsgeUTpRIUOTn33EUXHRK6j7pxBjwqH4WpQyq+pukjd1AIzWlEa6w7i6bInJUcweGgP2beXZmaP6b6UPn7A==} engines: {node: '>=10'} + '@humanwhocodes/config-array@0.13.0': + resolution: {integrity: sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==} + engines: {node: '>=10.10.0'} + deprecated: Use @eslint/config-array instead + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/object-schema@2.0.3': + resolution: {integrity: sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==} + deprecated: Use @eslint/object-schema instead + '@ide/backoff@1.0.0': resolution: {integrity: sha512-F0YfUDjvT+Mtt/R4xdl2X0EYCHMMiJqNLdxHD++jDT5ydEFIyqbCHh51Qx2E211dgZprPKhV7sHmnXKpLuvc5g==} @@ -3500,6 +3549,9 @@ packages: '@sinonjs/fake-timers@10.3.0': resolution: {integrity: sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==} + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@tanstack/query-async-storage-persister@5.100.10': resolution: {integrity: sha512-NbxsCI9qyMbEooFnFsAR2dbVQ6cmxZpOWSjLFy62qfMkxXnNjkskqex7YgtzWSaD/ozOOLgrbDiyYWEPYKmMyQ==} @@ -3972,10 +4024,12 @@ packages: '@xmldom/xmldom@0.8.13': resolution: {integrity: sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==} engines: {node: '>=10.0.0'} + deprecated: this version has critical issues, please update to the latest version '@xmldom/xmldom@0.9.10': resolution: {integrity: sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==} engines: {node: '>=14.6'} + deprecated: this version has critical issues, please update to the latest version '@xtuc/ieee754@1.2.0': resolution: {integrity: sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==} @@ -4008,6 +4062,11 @@ packages: peerDependencies: acorn: ^8.14.0 + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + acorn-walk@8.3.5: resolution: {integrity: sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==} engines: {node: '>=0.4.0'} @@ -4875,6 +4934,9 @@ packages: resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} engines: {node: '>=4.0.0'} + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + deepmerge@4.3.1: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} @@ -4951,6 +5013,10 @@ packages: resolution: {integrity: sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==} engines: {node: '>=6'} + doctrine@3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + dom-converter@0.2.0: resolution: {integrity: sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==} @@ -5167,15 +5233,37 @@ packages: resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} engines: {node: '>=8.0.0'} + eslint-scope@7.2.2: + resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint@8.57.1: + resolution: {integrity: sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. + hasBin: true + esm@3.2.25: resolution: {integrity: sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==} engines: {node: '>=6'} + espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + esprima@4.0.1: resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} engines: {node: '>=4'} hasBin: true + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + esrecurse@4.3.0: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} engines: {node: '>=4.0'} @@ -5571,6 +5659,9 @@ packages: fast-json-stringify@6.4.0: resolution: {integrity: sha512-ibRCQ0GZKJIQ+P3Et1h0LhPgp3PMTYk0MH8O+kW3lNYsvmaQww5Nn3f1jf73Q0jR1Yz3a1CDP4/NZD3vOajWJQ==} + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-text-encoding@1.0.6: resolution: {integrity: sha512-VhXlQgj9ioXCqGstD37E/HBeqEGV/qOD/kmbVG8h5xKBYvM1L3lR1Zn4555cQ8GkYbJa8aJSipLPndE1k6zK2w==} @@ -5602,6 +5693,10 @@ packages: picomatch: optional: true + file-entry-cache@6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + file-loader@6.2.0: resolution: {integrity: sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==} engines: {node: '>= 10.13.0'} @@ -5646,6 +5741,13 @@ packages: find-yarn-workspace-root@2.0.0: resolution: {integrity: sha512-1IMnbjt4KzsQfnhnzNd8wUEgXZ44IzZaZmnLYx7D5FZlaHt2gW20Cri8Q+E/t5tIj4+epTBub+2Zxu/vNILzqQ==} + flat-cache@3.2.0: + resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==} + engines: {node: ^10.12.0 || >=12.0.0} + + flatted@3.4.4: + resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==} + flow-enums-runtime@0.0.6: resolution: {integrity: sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==} @@ -5797,6 +5899,10 @@ packages: engines: {node: '>=16 || 14 >=14.17'} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + globals@13.24.0: + resolution: {integrity: sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==} + engines: {node: '>=8'} + globals@17.6.0: resolution: {integrity: sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==} engines: {node: '>=18'} @@ -5820,6 +5926,9 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + gzip-size@6.0.0: resolution: {integrity: sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==} engines: {node: '>=10'} @@ -6196,6 +6305,10 @@ packages: resolution: {integrity: sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==} engines: {node: '>=6'} + is-path-inside@3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + is-plain-obj@2.1.0: resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} engines: {node: '>=8'} @@ -6529,6 +6642,9 @@ packages: engines: {node: '>=6'} hasBin: true + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} @@ -6541,6 +6657,9 @@ packages: json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} @@ -6557,6 +6676,9 @@ packages: resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==} engines: {node: '>=18'} + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + kleur@3.0.3: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} @@ -6575,6 +6697,10 @@ packages: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} engines: {node: '>=6'} + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + libphonenumber-js@1.13.1: resolution: {integrity: sha512-GEw0GLL7YUUA6nv21IsCvVjtI5Ejn84sjbdfQ9KxdbqEVOk1PZh7xejn01EEiniKw+dBeCfim+8MGeuvVuE2BA==} @@ -6703,6 +6829,9 @@ packages: lodash.memoize@4.1.2: resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash.shuffle@4.2.0: resolution: {integrity: sha512-V/rTAABKLFjoecTZjKSv+A1ZomG8hZg8hlgeG6wwQVD9AGv+10zqqSf6mFq2tVA703Zd5R0YhSuSlXA+E/Ei+Q==} @@ -7331,6 +7460,10 @@ packages: resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==} hasBin: true + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + ora@3.4.0: resolution: {integrity: sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg==} engines: {node: '>=6'} @@ -7725,6 +7858,10 @@ packages: resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==} engines: {node: ^10 || ^12 || >=14} + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + prettier@3.8.3: resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==} engines: {node: '>=14'} @@ -8823,6 +8960,9 @@ packages: text-segmentation@1.0.3: resolution: {integrity: sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==} + text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} engines: {node: '>=0.8'} @@ -8902,10 +9042,18 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + type-detect@4.0.8: resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} engines: {node: '>=4'} + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + type-fest@0.21.3: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} engines: {node: '>=10'} @@ -9265,6 +9413,10 @@ packages: wonka@6.3.6: resolution: {integrity: sha512-MXH+6mDHAZ2GuMpgKS055FR6v0xVP3XwquxIMYXgiW+FejHQlMGlvVRZT4qMCxR+bEo/FCtIdKxwej9WV3YQag==} + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + wrap-ansi@10.0.0: resolution: {integrity: sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ==} engines: {node: '>=20'} @@ -9441,6 +9593,13 @@ snapshots: '@atproto/syntax': 0.7.4 zod: 3.25.76 + '@atproto/lex-client@0.3.0': + dependencies: + '@atproto/lex-data': 0.1.7 + '@atproto/lex-json': 0.1.6 + '@atproto/lex-schema': 0.2.5 + tslib: 2.8.1 + '@atproto/lex-data@0.1.6': dependencies: multiformats: 13.4.2 @@ -9463,6 +9622,19 @@ snapshots: '@atproto/lex-data': 0.1.7 tslib: 2.8.1 + '@atproto/lex-password-session@0.1.8': + dependencies: + '@atproto/lex-client': 0.3.0 + '@atproto/lex-schema': 0.2.5 + tslib: 2.8.1 + + '@atproto/lex-schema@0.2.5': + dependencies: + '@atproto/lex-data': 0.1.7 + '@atproto/syntax': 0.7.4 + '@standard-schema/spec': 1.1.0 + tslib: 2.8.1 + '@atproto/lexicon@0.7.11': dependencies: '@atproto/common-web': 0.5.9 @@ -9497,20 +9669,20 @@ snapshots: '@babel/compat-data@7.29.3': {} - '@babel/core@7.29.0': + '@babel/core@7.29.0(supports-color@8.1.1)': dependencies: '@babel/code-frame': 7.29.0 '@babel/generator': 7.29.1 '@babel/helper-compilation-targets': 7.28.6 - '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) '@babel/helpers': 7.29.2 '@babel/parser': 7.29.3 '@babel/template': 7.28.6 - '@babel/traverse': 7.29.0 + '@babel/traverse': 7.29.0(supports-color@8.1.1) '@babel/types': 7.29.0 '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -9537,32 +9709,32 @@ snapshots: lru-cache: 5.1.1 semver: 6.3.1 - '@babel/helper-create-class-features-plugin@7.29.3(@babel/core@7.29.0)': + '@babel/helper-create-class-features-plugin@7.29.3(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/helper-member-expression-to-functions': 7.28.5(supports-color@8.1.1) '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0) - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/traverse': 7.29.0 + '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1(supports-color@8.1.1) + '@babel/traverse': 7.29.0(supports-color@8.1.1) semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/helper-create-regexp-features-plugin@7.28.5(@babel/core@7.29.0)': + '@babel/helper-create-regexp-features-plugin@7.28.5(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-annotate-as-pure': 7.27.3 regexpu-core: 6.4.0 semver: 6.3.1 - '@babel/helper-define-polyfill-provider@0.6.8(@babel/core@7.29.0)': + '@babel/helper-define-polyfill-provider@0.6.8(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-compilation-targets': 7.28.6 '@babel/helper-plugin-utils': 7.28.6 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) lodash.debounce: 4.0.8 resolve: 1.22.12 transitivePeerDependencies: @@ -9570,26 +9742,26 @@ snapshots: '@babel/helper-globals@7.28.0': {} - '@babel/helper-member-expression-to-functions@7.28.5': + '@babel/helper-member-expression-to-functions@7.28.5(supports-color@8.1.1)': dependencies: - '@babel/traverse': 7.29.0 + '@babel/traverse': 7.29.0(supports-color@8.1.1) '@babel/types': 7.29.0 transitivePeerDependencies: - supports-color - '@babel/helper-module-imports@7.28.6': + '@babel/helper-module-imports@7.28.6(supports-color@8.1.1)': dependencies: - '@babel/traverse': 7.29.0 + '@babel/traverse': 7.29.0(supports-color@8.1.1) '@babel/types': 7.29.0 transitivePeerDependencies: - supports-color - '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-module-imports': 7.28.6 + '@babel/core': 7.29.0(supports-color@8.1.1) + '@babel/helper-module-imports': 7.28.6(supports-color@8.1.1) '@babel/helper-validator-identifier': 7.28.5 - '@babel/traverse': 7.29.0 + '@babel/traverse': 7.29.0(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -9599,27 +9771,27 @@ snapshots: '@babel/helper-plugin-utils@7.28.6': {} - '@babel/helper-remap-async-to-generator@7.27.1(@babel/core@7.29.0)': + '@babel/helper-remap-async-to-generator@7.27.1(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-wrap-function': 7.28.6 - '@babel/traverse': 7.29.0 + '@babel/helper-wrap-function': 7.28.6(supports-color@8.1.1) + '@babel/traverse': 7.29.0(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/helper-replace-supers@7.28.6(@babel/core@7.29.0)': + '@babel/helper-replace-supers@7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-member-expression-to-functions': 7.28.5 + '@babel/core': 7.29.0(supports-color@8.1.1) + '@babel/helper-member-expression-to-functions': 7.28.5(supports-color@8.1.1) '@babel/helper-optimise-call-expression': 7.27.1 - '@babel/traverse': 7.29.0 + '@babel/traverse': 7.29.0(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + '@babel/helper-skip-transparent-expression-wrappers@7.27.1(supports-color@8.1.1)': dependencies: - '@babel/traverse': 7.29.0 + '@babel/traverse': 7.29.0(supports-color@8.1.1) '@babel/types': 7.29.0 transitivePeerDependencies: - supports-color @@ -9630,10 +9802,10 @@ snapshots: '@babel/helper-validator-option@7.27.1': {} - '@babel/helper-wrap-function@7.28.6': + '@babel/helper-wrap-function@7.28.6(supports-color@8.1.1)': dependencies: '@babel/template': 7.28.6 - '@babel/traverse': 7.29.0 + '@babel/traverse': 7.29.0(supports-color@8.1.1) '@babel/types': 7.29.0 transitivePeerDependencies: - supports-color @@ -9654,696 +9826,696 @@ snapshots: dependencies: '@babel/types': 7.29.0 - '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5(@babel/core@7.29.0)': + '@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.28.5(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/traverse': 7.29.0 + '@babel/traverse': 7.29.0(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-bugfix-safari-class-field-initializer-scope@7.27.1(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.27.1(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-bugfix-safari-rest-destructuring-rhs-array@7.29.3(@babel/core@7.29.0)': + '@babel/plugin-bugfix-safari-rest-destructuring-rhs-array@7.29.3(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.27.1(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/plugin-transform-optional-chaining': 7.28.6(@babel/core@7.29.0) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1(supports-color@8.1.1) + '@babel/plugin-transform-optional-chaining': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/traverse': 7.29.0 + '@babel/traverse': 7.29.0(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-proposal-decorators@7.29.0(@babel/core@7.29.0)': + '@babel/plugin-proposal-decorators@7.29.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.29.0) + '@babel/core': 7.29.0(supports-color@8.1.1) + '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-decorators': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-decorators': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1)) transitivePeerDependencies: - supports-color - '@babel/plugin-proposal-export-default-from@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-proposal-export-default-from@7.27.1(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-proposal-private-methods@7.18.6(@babel/core@7.29.0)': + '@babel/plugin-proposal-private-methods@7.18.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.29.0) + '@babel/core': 7.29.0(supports-color@8.1.1) + '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 transitivePeerDependencies: - supports-color - '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.0)': + '@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) - '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.29.0)': + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.29.0)': + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.29.0)': + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.29.0)': + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-decorators@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-syntax-decorators@7.28.6(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.29.0)': + '@babel/plugin-syntax-dynamic-import@7.8.3(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-export-default-from@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-syntax-export-default-from@7.28.6(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-flow@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-syntax-flow@7.28.6(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-import-assertions@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-syntax-import-assertions@7.28.6(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-import-attributes@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-syntax-import-attributes@7.28.6(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.0)': + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.29.0)': + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.29.0)': + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.29.0)': + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.29.0)': + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.29.0)': + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.29.0)': + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.29.0)': + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.29.0)': + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.29.0)': + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-syntax-typescript@7.28.6(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.29.0)': + '@babel/plugin-syntax-unicode-sets-regex@7.18.6(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/core': 7.29.0(supports-color@8.1.1) + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0(supports-color@8.1.1)) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-arrow-functions@7.27.1(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-async-generator-functions@7.29.0(@babel/core@7.29.0)': + '@babel/plugin-transform-async-generator-functions@7.29.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.29.0) - '@babel/traverse': 7.29.0 + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/traverse': 7.29.0(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-async-to-generator@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-async-to-generator@7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-module-imports': 7.28.6 + '@babel/core': 7.29.0(supports-color@8.1.1) + '@babel/helper-module-imports': 7.28.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.29.0) + '@babel/helper-remap-async-to-generator': 7.27.1(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-block-scoped-functions@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-block-scoped-functions@7.27.1(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-block-scoping@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-block-scoping@7.28.6(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-class-properties@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-class-properties@7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.29.0) + '@babel/core': 7.29.0(supports-color@8.1.1) + '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-class-static-block@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-class-static-block@7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.29.0) + '@babel/core': 7.29.0(supports-color@8.1.1) + '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-classes@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-classes@7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-annotate-as-pure': 7.27.3 '@babel/helper-compilation-targets': 7.28.6 '@babel/helper-globals': 7.28.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0) - '@babel/traverse': 7.29.0 + '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/traverse': 7.29.0(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-computed-properties@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-computed-properties@7.28.6(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 '@babel/template': 7.28.6 - '@babel/plugin-transform-destructuring@7.28.5(@babel/core@7.29.0)': + '@babel/plugin-transform-destructuring@7.28.5(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/traverse': 7.29.0 + '@babel/traverse': 7.29.0(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-dotall-regex@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-dotall-regex@7.28.6(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/core': 7.29.0(supports-color@8.1.1) + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0(supports-color@8.1.1)) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-duplicate-keys@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-duplicate-keys@7.27.1(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.29.0(@babel/core@7.29.0)': + '@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.29.0(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/core': 7.29.0(supports-color@8.1.1) + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0(supports-color@8.1.1)) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-dynamic-import@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-dynamic-import@7.27.1(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-explicit-resource-management@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-explicit-resource-management@7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.0) + '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-exponentiation-operator@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-exponentiation-operator@7.28.6(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-export-namespace-from@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-export-namespace-from@7.27.1(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-flow-strip-types@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-flow-strip-types@7.27.1(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-flow': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-flow': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1)) - '@babel/plugin-transform-for-of@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-for-of@7.27.1(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-function-name@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-function-name@7.27.1(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-compilation-targets': 7.28.6 '@babel/helper-plugin-utils': 7.28.6 - '@babel/traverse': 7.29.0 + '@babel/traverse': 7.29.0(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-json-strings@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-json-strings@7.28.6(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-literals@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-literals@7.27.1(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-logical-assignment-operators@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-logical-assignment-operators@7.28.6(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-member-expression-literals@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-member-expression-literals@7.27.1(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-modules-amd@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-modules-amd@7.27.1(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/core': 7.29.0(supports-color@8.1.1) + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-commonjs@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-modules-commonjs@7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/core': 7.29.0(supports-color@8.1.1) + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-systemjs@7.29.4(@babel/core@7.29.0)': + '@babel/plugin-transform-modules-systemjs@7.29.4(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/core': 7.29.0(supports-color@8.1.1) + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 '@babel/helper-validator-identifier': 7.28.5 - '@babel/traverse': 7.29.0 + '@babel/traverse': 7.29.0(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-modules-umd@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-modules-umd@7.27.1(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/core': 7.29.0(supports-color@8.1.1) + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-named-capturing-groups-regex@7.29.0(@babel/core@7.29.0)': + '@babel/plugin-transform-named-capturing-groups-regex@7.29.0(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/core': 7.29.0(supports-color@8.1.1) + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0(supports-color@8.1.1)) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-new-target@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-new-target@7.27.1(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-nullish-coalescing-operator@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-nullish-coalescing-operator@7.28.6(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-numeric-separator@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-numeric-separator@7.28.6(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-object-rest-spread@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-object-rest-spread@7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-compilation-targets': 7.28.6 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.0) - '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.29.0) - '@babel/traverse': 7.29.0 + '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/traverse': 7.29.0(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-object-super@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-object-super@7.27.1(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0) + '@babel/helper-replace-supers': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-optional-catch-binding@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-optional-catch-binding@7.28.6(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-optional-chaining@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-optional-chaining@7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-parameters@7.27.7(@babel/core@7.29.0)': + '@babel/plugin-transform-parameters@7.27.7(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-private-methods@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-private-methods@7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.29.0) + '@babel/core': 7.29.0(supports-color@8.1.1) + '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-private-property-in-object@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-private-property-in-object@7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.29.0) + '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-property-literals@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-property-literals@7.27.1(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-react-display-name@7.28.0(@babel/core@7.29.0)': + '@babel/plugin-transform-react-display-name@7.28.0(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-react-jsx-development@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-react-jsx-development@7.27.1(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.0 - '@babel/plugin-transform-react-jsx': 7.28.6(@babel/core@7.29.0) + '@babel/core': 7.29.0(supports-color@8.1.1) + '@babel/plugin-transform-react-jsx': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-react-jsx@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-react-jsx@7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-module-imports': 7.28.6 + '@babel/helper-module-imports': 7.28.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1)) '@babel/types': 7.29.0 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-react-pure-annotations@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-react-pure-annotations@7.27.1(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-annotate-as-pure': 7.27.3 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-regenerator@7.29.0(@babel/core@7.29.0)': + '@babel/plugin-transform-regenerator@7.29.0(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-regexp-modifiers@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-regexp-modifiers@7.28.6(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/core': 7.29.0(supports-color@8.1.1) + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0(supports-color@8.1.1)) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-reserved-words@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-reserved-words@7.27.1(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-runtime@7.29.0(@babel/core@7.29.0)': + '@babel/plugin-transform-runtime@7.29.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-module-imports': 7.28.6 + '@babel/core': 7.29.0(supports-color@8.1.1) + '@babel/helper-module-imports': 7.28.6(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - babel-plugin-polyfill-corejs2: 0.4.17(@babel/core@7.29.0) - babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.29.0) - babel-plugin-polyfill-regenerator: 0.6.8(@babel/core@7.29.0) + babel-plugin-polyfill-corejs2: 0.4.17(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + babel-plugin-polyfill-corejs3: 0.13.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + babel-plugin-polyfill-regenerator: 0.6.8(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/plugin-transform-shorthand-properties@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-shorthand-properties@7.27.1(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-spread@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-spread@7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-sticky-regex@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-sticky-regex@7.27.1(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-template-literals@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-template-literals@7.27.1(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-typeof-symbol@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-typeof-symbol@7.27.1(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-typescript@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-typescript@7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-annotate-as-pure': 7.27.3 - '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.29.0) + '@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 - '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1(supports-color@8.1.1) + '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1)) transitivePeerDependencies: - supports-color - '@babel/plugin-transform-unicode-escapes@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-unicode-escapes@7.27.1(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-unicode-property-regex@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-unicode-property-regex@7.28.6(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/core': 7.29.0(supports-color@8.1.1) + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0(supports-color@8.1.1)) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-unicode-regex@7.27.1(@babel/core@7.29.0)': + '@babel/plugin-transform-unicode-regex@7.27.1(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/core': 7.29.0(supports-color@8.1.1) + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0(supports-color@8.1.1)) '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-unicode-sets-regex@7.28.6(@babel/core@7.29.0)': + '@babel/plugin-transform-unicode-sets-regex@7.28.6(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 - '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0) + '@babel/core': 7.29.0(supports-color@8.1.1) + '@babel/helper-create-regexp-features-plugin': 7.28.5(@babel/core@7.29.0(supports-color@8.1.1)) '@babel/helper-plugin-utils': 7.28.6 - '@babel/preset-env@7.29.5(@babel/core@7.29.0)': + '@babel/preset-env@7.29.5(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: '@babel/compat-data': 7.29.3 - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-compilation-targets': 7.28.6 '@babel/helper-plugin-utils': 7.28.6 '@babel/helper-validator-option': 7.27.1 - '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.28.5(@babel/core@7.29.0) - '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-bugfix-safari-rest-destructuring-rhs-array': 7.29.3(@babel/core@7.29.0) - '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.0) - '@babel/plugin-syntax-import-assertions': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.29.0) - '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-async-generator-functions': 7.29.0(@babel/core@7.29.0) - '@babel/plugin-transform-async-to-generator': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-block-scoped-functions': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-block-scoping': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-class-properties': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-class-static-block': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-classes': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-computed-properties': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.0) - '@babel/plugin-transform-dotall-regex': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-duplicate-keys': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.29.0(@babel/core@7.29.0) - '@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-explicit-resource-management': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-exponentiation-operator': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-export-namespace-from': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-function-name': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-json-strings': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-literals': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-logical-assignment-operators': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-member-expression-literals': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-modules-amd': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-modules-systemjs': 7.29.4(@babel/core@7.29.0) - '@babel/plugin-transform-modules-umd': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-named-capturing-groups-regex': 7.29.0(@babel/core@7.29.0) - '@babel/plugin-transform-new-target': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-nullish-coalescing-operator': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-numeric-separator': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-object-rest-spread': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-object-super': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-optional-catch-binding': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-optional-chaining': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.29.0) - '@babel/plugin-transform-private-methods': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-private-property-in-object': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-property-literals': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-regenerator': 7.29.0(@babel/core@7.29.0) - '@babel/plugin-transform-regexp-modifiers': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-reserved-words': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-spread': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-sticky-regex': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-template-literals': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-typeof-symbol': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-unicode-escapes': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-unicode-property-regex': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-unicode-sets-regex': 7.28.6(@babel/core@7.29.0) - '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.29.0) - babel-plugin-polyfill-corejs2: 0.4.17(@babel/core@7.29.0) - babel-plugin-polyfill-corejs3: 0.14.2(@babel/core@7.29.0) - babel-plugin-polyfill-regenerator: 0.6.8(@babel/core@7.29.0) + '@babel/plugin-bugfix-firefox-class-in-computed-class-key': 7.28.5(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-bugfix-safari-class-field-initializer-scope': 7.27.1(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression': 7.27.1(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-bugfix-safari-rest-destructuring-rhs-array': 7.29.3(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining': 7.27.1(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-proposal-private-property-in-object': 7.21.0-placeholder-for-preset-env.2(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-syntax-import-assertions': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-syntax-unicode-sets-regex': 7.18.6(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-async-generator-functions': 7.29.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-async-to-generator': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-block-scoped-functions': 7.27.1(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-block-scoping': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-class-properties': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-class-static-block': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-classes': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-computed-properties': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-dotall-regex': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-duplicate-keys': 7.27.1(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-duplicate-named-capturing-groups-regex': 7.29.0(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-dynamic-import': 7.27.1(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-explicit-resource-management': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-exponentiation-operator': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-export-namespace-from': 7.27.1(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-function-name': 7.27.1(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-json-strings': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-literals': 7.27.1(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-logical-assignment-operators': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-member-expression-literals': 7.27.1(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-modules-amd': 7.27.1(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-modules-systemjs': 7.29.4(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-modules-umd': 7.27.1(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-named-capturing-groups-regex': 7.29.0(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-new-target': 7.27.1(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-nullish-coalescing-operator': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-numeric-separator': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-object-rest-spread': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-object-super': 7.27.1(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-optional-catch-binding': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-optional-chaining': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-private-methods': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-private-property-in-object': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-property-literals': 7.27.1(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-regenerator': 7.29.0(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-regexp-modifiers': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-reserved-words': 7.27.1(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-spread': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-sticky-regex': 7.27.1(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-template-literals': 7.27.1(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-typeof-symbol': 7.27.1(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-unicode-escapes': 7.27.1(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-unicode-property-regex': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-unicode-sets-regex': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/preset-modules': 0.1.6-no-external-plugins(@babel/core@7.29.0(supports-color@8.1.1)) + babel-plugin-polyfill-corejs2: 0.4.17(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + babel-plugin-polyfill-corejs3: 0.14.2(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + babel-plugin-polyfill-regenerator: 0.6.8(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) core-js-compat: 3.49.0 semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.29.0)': + '@babel/preset-modules@0.1.6-no-external-plugins(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 '@babel/types': 7.29.0 esutils: 2.0.3 - '@babel/preset-react@7.28.5(@babel/core@7.29.0)': + '@babel/preset-react@7.28.5(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 '@babel/helper-validator-option': 7.27.1 - '@babel/plugin-transform-react-display-name': 7.28.0(@babel/core@7.29.0) - '@babel/plugin-transform-react-jsx': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-react-jsx-development': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-react-pure-annotations': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-react-display-name': 7.28.0(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-react-jsx': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-react-jsx-development': 7.27.1(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-react-pure-annotations': 7.27.1(@babel/core@7.29.0(supports-color@8.1.1)) transitivePeerDependencies: - supports-color - '@babel/preset-typescript@7.28.5(@babel/core@7.29.0)': + '@babel/preset-typescript@7.28.5(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/helper-plugin-utils': 7.28.6 '@babel/helper-validator-option': 7.27.1 - '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -10355,7 +10527,7 @@ snapshots: '@babel/parser': 7.29.3 '@babel/types': 7.29.0 - '@babel/traverse@7.29.0': + '@babel/traverse@7.29.0(supports-color@8.1.1)': dependencies: '@babel/code-frame': 7.29.0 '@babel/generator': 7.29.1 @@ -10363,7 +10535,7 @@ snapshots: '@babel/parser': 7.29.3 '@babel/template': 7.28.6 '@babel/types': 7.29.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -10374,94 +10546,94 @@ snapshots: '@bcoe/v8-coverage@0.2.3': {} - '@bitdrift/react-native@0.6.14(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)': + '@bitdrift/react-native@0.6.14(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1)': dependencies: - '@expo/config-plugins': 9.1.7 + '@expo/config-plugins': 9.1.7(supports-color@8.1.1) fast-json-stringify: 6.4.0 react: 19.1.0 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) transitivePeerDependencies: - supports-color '@braintree/sanitize-url@6.0.4': {} - '@bsky.app/alf@0.1.15(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)': + '@bsky.app/alf@0.1.15(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)': dependencies: react: 19.1.0 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) react-responsive: 10.0.1(react@19.1.0) - '@bsky.app/expo-dynamic-app-icon@1.8.5(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)': + '@bsky.app/expo-dynamic-app-icon@1.8.5(83e0f64f4281e4798012214ad5b2d72d)': dependencies: '@expo/image-utils': 0.8.12 - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) react: 19.1.0 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) xcode: 3.0.1 - '@bsky.app/expo-guess-language@0.2.8(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)': + '@bsky.app/expo-guess-language@0.2.8(83e0f64f4281e4798012214ad5b2d72d)': dependencies: - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) lande: 1.0.10 react: 19.1.0 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) - '@bsky.app/expo-image-crop-tool@0.5.1(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)': + '@bsky.app/expo-image-crop-tool@0.5.1(83e0f64f4281e4798012214ad5b2d72d)': dependencies: - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) react: 19.1.0 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) - '@bsky.app/expo-scroll-edge-effect@0.1.9(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)': + '@bsky.app/expo-scroll-edge-effect@0.1.9(83e0f64f4281e4798012214ad5b2d72d)': dependencies: '@types/jest': 30.0.0 - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) react: 19.1.0 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) - '@bsky.app/expo-translate-text@0.2.9(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)': + '@bsky.app/expo-translate-text@0.2.9(83e0f64f4281e4798012214ad5b2d72d)': dependencies: - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) react: 19.1.0 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) - '@bsky.app/peek-menu@0.3.2(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)': + '@bsky.app/peek-menu@0.3.2(83e0f64f4281e4798012214ad5b2d72d)': dependencies: - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) react: 19.1.0 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) - '@bsky.app/react-native-uitextview@2.7.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)': + '@bsky.app/react-native-uitextview@2.7.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)': dependencies: react: 19.1.0 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) - '@bsky.app/sift@0.3.9(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)': + '@bsky.app/sift@0.3.9(2d5c14016450c7e55c8cd0dffcca6f69)': dependencies: - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) react: 19.1.0 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) - react-native-safe-area-context: 5.6.2(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) + react-native-safe-area-context: 5.6.2(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0) - '@bsky.app/tapper@0.6.1(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)(tlds@1.261.0)': + '@bsky.app/tapper@0.6.1(8d6e1ad3cb49b1a6214533c496326d11)': dependencies: - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) react: 19.1.0 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) tlds: 1.261.0 - '@bsky.app/video-compressor@0.2.0(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)': + '@bsky.app/video-compressor@0.2.0(83e0f64f4281e4798012214ad5b2d72d)': dependencies: - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) mediabunny: 1.49.0 react: 19.1.0 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) - '@bsky.app/video@0.3.6(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)': + '@bsky.app/video@0.3.6(83e0f64f4281e4798012214ad5b2d72d)': dependencies: - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) react: 19.1.0 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) '@crowdin/cli@4.14.2': dependencies: @@ -10564,27 +10736,50 @@ snapshots: '@esbuild/win32-x64@0.25.12': optional: true - '@expo/cli@54.0.25(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))': + '@eslint-community/eslint-utils@4.10.1(eslint@8.57.1(supports-color@8.1.1))': + dependencies: + eslint: 8.57.1(supports-color@8.1.1) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/eslintrc@2.1.4(supports-color@8.1.1)': + dependencies: + ajv: 6.15.0 + debug: 4.4.3(supports-color@8.1.1) + espree: 9.6.1 + globals: 13.24.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@8.57.1': {} + + '@expo/cli@54.0.25(c1408f750868568d2dc75671b50084a7)': dependencies: '@0no-co/graphql.web': 1.2.0 '@expo/code-signing-certificates': 0.0.6 - '@expo/config': 12.0.13 - '@expo/config-plugins': 54.0.4 - '@expo/devcert': 1.2.1 - '@expo/env': 2.0.11 + '@expo/config': 12.0.13(supports-color@8.1.1) + '@expo/config-plugins': 54.0.4(supports-color@8.1.1) + '@expo/devcert': 1.2.1(supports-color@8.1.1) + '@expo/env': 2.0.11(supports-color@8.1.1) '@expo/image-utils': 0.8.12 '@expo/json-file': 10.2.0 - '@expo/metro': 54.2.0 - '@expo/metro-config': 54.0.16(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)) + '@expo/metro': 54.2.0(supports-color@8.1.1) + '@expo/metro-config': 54.0.16(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1))(supports-color@8.1.1) '@expo/osascript': 2.4.3 '@expo/package-manager': 1.10.5 '@expo/plist': 0.4.9 - '@expo/prebuild-config': 54.0.8(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)) + '@expo/prebuild-config': 54.0.8(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1))(supports-color@8.1.1) '@expo/schema-utils': 0.1.8 '@expo/spawn-async': 1.7.2 '@expo/ws-tunnel': 1.0.6 '@expo/xcpretty': 4.4.4 - '@react-native/dev-middleware': 0.81.5 + '@react-native/dev-middleware': 0.81.5(supports-color@8.1.1) '@urql/core': 5.2.0 '@urql/exchange-retry': 1.3.2(@urql/core@5.2.0) accepts: 1.3.8 @@ -10594,11 +10789,11 @@ snapshots: bplist-parser: 0.3.2 chalk: 4.1.2 ci-info: 3.9.0 - compression: 1.8.1 - connect: 3.7.0 - debug: 4.4.3 + compression: 1.8.1(supports-color@8.1.1) + connect: 3.7.0(supports-color@8.1.1) + debug: 4.4.3(supports-color@8.1.1) env-editor: 0.4.2 - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) expo-server: 1.0.7 freeport-async: 2.0.0 getenv: 2.0.0 @@ -10620,7 +10815,7 @@ snapshots: resolve-from: 5.0.0 resolve.exports: 2.0.3 semver: 7.8.0 - send: 0.19.2 + send: 0.19.2(supports-color@8.1.1) slugify: 1.6.9 source-map-support: 0.5.21 stacktrace-parser: 0.1.11 @@ -10631,7 +10826,7 @@ snapshots: wrap-ansi: 7.0.0 ws: 8.20.1 optionalDependencies: - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) transitivePeerDependencies: - bufferutil - graphql @@ -10642,14 +10837,14 @@ snapshots: dependencies: node-forge: 1.4.0 - '@expo/config-plugins@54.0.4': + '@expo/config-plugins@54.0.4(supports-color@8.1.1)': dependencies: '@expo/config-types': 54.0.10 '@expo/json-file': 10.0.14 '@expo/plist': 0.4.8 '@expo/sdk-runtime-versions': 1.0.0 chalk: 4.1.2 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) getenv: 2.0.0 glob: 13.0.6 resolve-from: 5.0.0 @@ -10661,14 +10856,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@expo/config-plugins@9.1.7': + '@expo/config-plugins@9.1.7(supports-color@8.1.1)': dependencies: '@expo/config-types': 53.0.5 '@expo/json-file': 9.1.5 '@expo/plist': 0.3.5 '@expo/sdk-runtime-versions': 1.0.0 chalk: 4.1.2 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) getenv: 1.0.0 glob: 10.5.0 resolve-from: 5.0.0 @@ -10684,10 +10879,10 @@ snapshots: '@expo/config-types@54.0.10': {} - '@expo/config@12.0.13': + '@expo/config@12.0.13(supports-color@8.1.1)': dependencies: '@babel/code-frame': 7.10.4 - '@expo/config-plugins': 54.0.4 + '@expo/config-plugins': 54.0.4(supports-color@8.1.1) '@expo/config-types': 54.0.10 '@expo/json-file': 10.0.14 deepmerge: 4.3.1 @@ -10702,36 +10897,36 @@ snapshots: transitivePeerDependencies: - supports-color - '@expo/devcert@1.2.1': + '@expo/devcert@1.2.1(supports-color@8.1.1)': dependencies: '@expo/sudo-prompt': 9.3.2 - debug: 3.2.7 + debug: 3.2.7(supports-color@8.1.1) transitivePeerDependencies: - supports-color - '@expo/devtools@0.1.8(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)': + '@expo/devtools@0.1.8(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)': dependencies: chalk: 4.1.2 optionalDependencies: react: 19.1.0 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) - '@expo/env@2.0.11': + '@expo/env@2.0.11(supports-color@8.1.1)': dependencies: chalk: 4.1.2 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) dotenv: 16.4.7 dotenv-expand: 11.0.7 getenv: 2.0.0 transitivePeerDependencies: - supports-color - '@expo/fingerprint@0.15.5': + '@expo/fingerprint@0.15.5(supports-color@8.1.1)': dependencies: '@expo/spawn-async': 1.7.2 arg: 5.0.2 chalk: 4.1.2 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) getenv: 2.0.0 glob: 13.0.6 ignore: 5.3.2 @@ -10774,19 +10969,19 @@ snapshots: '@babel/code-frame': 7.10.4 json5: 2.2.3 - '@expo/metro-config@54.0.16(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))': + '@expo/metro-config@54.0.16(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: '@babel/code-frame': 7.29.0 - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/generator': 7.29.1 - '@expo/config': 12.0.13 - '@expo/env': 2.0.11 + '@expo/config': 12.0.13(supports-color@8.1.1) + '@expo/env': 2.0.11(supports-color@8.1.1) '@expo/json-file': 10.0.16 - '@expo/metro': 54.2.0 + '@expo/metro': 54.2.0(supports-color@8.1.1) '@expo/spawn-async': 1.7.2 browserslist: 4.28.2 chalk: 4.1.2 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) dotenv: 16.4.7 dotenv-expand: 11.0.7 getenv: 2.0.0 @@ -10798,28 +10993,28 @@ snapshots: postcss: 8.4.49 resolve-from: 5.0.0 optionalDependencies: - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate - '@expo/metro@54.2.0': + '@expo/metro@54.2.0(supports-color@8.1.1)': dependencies: - metro: 0.83.3 - metro-babel-transformer: 0.83.3 - metro-cache: 0.83.3 + metro: 0.83.3(supports-color@8.1.1) + metro-babel-transformer: 0.83.3(supports-color@8.1.1) + metro-cache: 0.83.3(supports-color@8.1.1) metro-cache-key: 0.83.3 - metro-config: 0.83.3 + metro-config: 0.83.3(supports-color@8.1.1) metro-core: 0.83.3 - metro-file-map: 0.83.3 + metro-file-map: 0.83.3(supports-color@8.1.1) metro-minify-terser: 0.83.3 metro-resolver: 0.83.3 metro-runtime: 0.83.3 - metro-source-map: 0.83.3 - metro-symbolicate: 0.83.3 - metro-transform-plugins: 0.83.3 - metro-transform-worker: 0.83.3 + metro-source-map: 0.83.3(supports-color@8.1.1) + metro-symbolicate: 0.83.3(supports-color@8.1.1) + metro-transform-plugins: 0.83.3(supports-color@8.1.1) + metro-transform-worker: 0.83.3(supports-color@8.1.1) transitivePeerDependencies: - bufferutil - supports-color @@ -10856,16 +11051,16 @@ snapshots: base64-js: 1.5.1 xmlbuilder: 15.1.1 - '@expo/prebuild-config@54.0.8(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))': + '@expo/prebuild-config@54.0.8(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@expo/config': 12.0.13 - '@expo/config-plugins': 54.0.4 + '@expo/config': 12.0.13(supports-color@8.1.1) + '@expo/config-plugins': 54.0.4(supports-color@8.1.1) '@expo/config-types': 54.0.10 '@expo/image-utils': 0.8.12 '@expo/json-file': 10.0.14 '@react-native/normalize-colors': 0.81.5 - debug: 4.4.3 - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + debug: 4.4.3(supports-color@8.1.1) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) resolve-from: 5.0.0 semver: 7.8.0 xml2js: 0.6.0 @@ -10882,38 +11077,38 @@ snapshots: '@expo/sudo-prompt@9.3.2': {} - '@expo/vector-icons@15.1.1(expo-font@14.0.12(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)': + '@expo/vector-icons@15.1.1(expo-font@14.0.12(83e0f64f4281e4798012214ad5b2d72d))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)': dependencies: - expo-font: 14.0.12(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo-font: 14.0.12(83e0f64f4281e4798012214ad5b2d72d) react: 19.1.0 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) - '@expo/webpack-config@19.0.1(clean-css@5.3.3)(csso@5.0.5)(esbuild@0.25.12)(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(html-minifier-terser@6.1.0)(postcss@8.5.14)': + '@expo/webpack-config@19.0.1(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(debug@4.4.3(supports-color@8.1.1))(esbuild@0.25.12)(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1))(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14)(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.0 - babel-loader: 8.4.1(@babel/core@7.29.0)(webpack@5.106.2(postcss@8.5.14)) + '@babel/core': 7.29.0(supports-color@8.1.1) + babel-loader: 8.4.1(@babel/core@7.29.0(supports-color@8.1.1))(webpack@5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14)) chalk: 4.1.2 - clean-webpack-plugin: 4.0.0(webpack@5.106.2(postcss@8.5.14)) - copy-webpack-plugin: 10.2.4(webpack@5.106.2(postcss@8.5.14)) - css-loader: 6.11.0(webpack@5.106.2(postcss@8.5.14)) - css-minimizer-webpack-plugin: 3.4.1(clean-css@5.3.3)(csso@5.0.5)(esbuild@0.25.12)(webpack@5.106.2(postcss@8.5.14)) - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) - expo-pwa: 0.0.127(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)) + clean-webpack-plugin: 4.0.0(webpack@5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14)) + copy-webpack-plugin: 10.2.4(webpack@5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14)) + css-loader: 6.11.0(webpack@5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14)) + css-minimizer-webpack-plugin: 3.4.1(clean-css@5.3.3)(csso@5.0.5)(esbuild@0.25.12)(webpack@5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14)) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) + expo-pwa: 0.0.127(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1)) find-up: 5.0.0 find-yarn-workspace-root: 2.0.0 fs-extra: 11.3.5 getenv: 1.0.0 - html-webpack-plugin: 5.6.7(webpack@5.106.2(postcss@8.5.14)) + html-webpack-plugin: 5.6.7(webpack@5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14)) is-wsl: 2.2.0 - mini-css-extract-plugin: 2.10.2(webpack@5.106.2(postcss@8.5.14)) + mini-css-extract-plugin: 2.10.2(webpack@5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14)) node-html-parser: 5.4.2 semver: 7.5.4 - source-map-loader: 3.0.2(webpack@5.106.2(postcss@8.5.14)) - style-loader: 3.3.4(webpack@5.106.2(postcss@8.5.14)) - terser-webpack-plugin: 5.6.0(clean-css@5.3.3)(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(postcss@8.5.14)(webpack@5.106.2(postcss@8.5.14)) - webpack: 5.106.2(postcss@8.5.14) - webpack-dev-server: 4.15.2(webpack@5.106.2(postcss@8.5.14)) - webpack-manifest-plugin: 4.1.1(webpack@5.106.2(postcss@8.5.14)) + source-map-loader: 3.0.2(webpack@5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14)) + style-loader: 3.3.4(webpack@5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14)) + terser-webpack-plugin: 5.6.0(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14)(webpack@5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14)) + webpack: 5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14) + webpack-dev-server: 4.15.2(debug@4.4.3(supports-color@8.1.1))(supports-color@8.1.1)(webpack@5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14)) + webpack-manifest-plugin: 4.1.1(webpack@5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14)) transitivePeerDependencies: - '@minify-html/node' - '@parcel/css' @@ -11006,6 +11201,18 @@ snapshots: dependencies: dom-mutator: 0.6.0 + '@humanwhocodes/config-array@0.13.0(supports-color@8.1.1)': + dependencies: + '@humanwhocodes/object-schema': 2.0.3 + debug: 4.4.3(supports-color@8.1.1) + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/object-schema@2.0.3': {} + '@ide/backoff@1.0.0': {} '@ipld/dag-cbor@9.2.7': @@ -11049,12 +11256,12 @@ snapshots: jest-util: 29.7.0 slash: 3.0.0 - '@jest/core@29.7.0': + '@jest/core@29.7.0(supports-color@8.1.1)': dependencies: '@jest/console': 29.7.0 - '@jest/reporters': 29.7.0 + '@jest/reporters': 29.7.0(supports-color@8.1.1) '@jest/test-result': 29.7.0 - '@jest/transform': 29.7.0 + '@jest/transform': 29.7.0(supports-color@8.1.1) '@jest/types': 29.6.3 '@types/node': 24.12.4 ansi-escapes: 4.3.2 @@ -11063,15 +11270,15 @@ snapshots: exit: 0.1.2 graceful-fs: 4.2.11 jest-changed-files: 29.7.0 - jest-config: 29.7.0(@types/node@24.12.4) + jest-config: 29.7.0(@types/node@24.12.4)(supports-color@8.1.1) jest-haste-map: 29.7.0 jest-message-util: 29.7.0 jest-regex-util: 29.6.3 jest-resolve: 29.7.0 - jest-resolve-dependencies: 29.7.0 - jest-runner: 29.7.0 - jest-runtime: 29.7.0 - jest-snapshot: 29.7.0 + jest-resolve-dependencies: 29.7.0(supports-color@8.1.1) + jest-runner: 29.7.0(supports-color@8.1.1) + jest-runtime: 29.7.0(supports-color@8.1.1) + jest-snapshot: 29.7.0(supports-color@8.1.1) jest-util: 29.7.0 jest-validate: 29.7.0 jest-watcher: 29.7.0 @@ -11105,10 +11312,10 @@ snapshots: dependencies: '@jest/get-type': 30.1.0 - '@jest/expect@29.7.0': + '@jest/expect@29.7.0(supports-color@8.1.1)': dependencies: expect: 29.7.0 - jest-snapshot: 29.7.0 + jest-snapshot: 29.7.0(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -11123,10 +11330,10 @@ snapshots: '@jest/get-type@30.1.0': {} - '@jest/globals@29.7.0': + '@jest/globals@29.7.0(supports-color@8.1.1)': dependencies: '@jest/environment': 29.7.0 - '@jest/expect': 29.7.0 + '@jest/expect': 29.7.0(supports-color@8.1.1) '@jest/types': 29.6.3 jest-mock: 29.7.0 transitivePeerDependencies: @@ -11137,12 +11344,12 @@ snapshots: '@types/node': 24.12.4 jest-regex-util: 30.4.0 - '@jest/reporters@29.7.0': + '@jest/reporters@29.7.0(supports-color@8.1.1)': dependencies: '@bcoe/v8-coverage': 0.2.3 '@jest/console': 29.7.0 '@jest/test-result': 29.7.0 - '@jest/transform': 29.7.0 + '@jest/transform': 29.7.0(supports-color@8.1.1) '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.31 '@types/node': 24.12.4 @@ -11152,9 +11359,9 @@ snapshots: glob: 7.2.3 graceful-fs: 4.2.11 istanbul-lib-coverage: 3.2.2 - istanbul-lib-instrument: 6.0.3 + istanbul-lib-instrument: 6.0.3(supports-color@8.1.1) istanbul-lib-report: 3.0.1 - istanbul-lib-source-maps: 4.0.1 + istanbul-lib-source-maps: 4.0.1(supports-color@8.1.1) istanbul-reports: 3.2.0 jest-message-util: 29.7.0 jest-util: 29.7.0 @@ -11194,12 +11401,12 @@ snapshots: jest-haste-map: 29.7.0 slash: 3.0.0 - '@jest/transform@29.7.0': + '@jest/transform@29.7.0(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@jest/types': 29.6.3 '@jridgewell/trace-mapping': 0.3.31 - babel-plugin-istanbul: 6.1.1 + babel-plugin-istanbul: 6.1.1(supports-color@8.1.1) chalk: 4.1.2 convert-source-map: 2.0.0 fast-json-stable-stringify: 2.1.0 @@ -11261,29 +11468,29 @@ snapshots: '@lingui/babel-plugin-extract-messages@5.9.5': {} - '@lingui/babel-plugin-lingui-macro@5.9.5(@typescript/typescript6@6.0.2)': + '@lingui/babel-plugin-lingui-macro@5.9.5(@typescript/typescript6@6.0.2)(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/runtime': 7.29.2 '@babel/types': 7.29.0 '@lingui/conf': 5.9.5(@typescript/typescript6@6.0.2) - '@lingui/core': 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(@typescript/typescript6@6.0.2)) + '@lingui/core': 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(@typescript/typescript6@6.0.2)(supports-color@8.1.1)) '@lingui/message-utils': 5.9.5 transitivePeerDependencies: - supports-color - typescript - '@lingui/cli@5.9.5(@typescript/typescript6@6.0.2)': + '@lingui/cli@5.9.5(@typescript/typescript6@6.0.2)(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/generator': 7.29.1 '@babel/parser': 7.29.3 '@babel/runtime': 7.29.2 '@babel/types': 7.29.0 '@lingui/babel-plugin-extract-messages': 5.9.5 - '@lingui/babel-plugin-lingui-macro': 5.9.5(@typescript/typescript6@6.0.2) + '@lingui/babel-plugin-lingui-macro': 5.9.5(@typescript/typescript6@6.0.2)(supports-color@8.1.1) '@lingui/conf': 5.9.5(@typescript/typescript6@6.0.2) - '@lingui/core': 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(@typescript/typescript6@6.0.2)) + '@lingui/core': 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(@typescript/typescript6@6.0.2)(supports-color@8.1.1)) '@lingui/format-po': 5.9.5(@typescript/typescript6@6.0.2) '@lingui/message-utils': 5.9.5 chokidar: 3.5.1 @@ -11301,7 +11508,7 @@ snapshots: pofile: 1.1.4 pseudolocale: 2.2.0 source-map: 0.7.6 - threads: 1.7.0 + threads: 1.7.0(supports-color@8.1.1) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -11317,12 +11524,12 @@ snapshots: transitivePeerDependencies: - typescript - '@lingui/core@5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(@typescript/typescript6@6.0.2))': + '@lingui/core@5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(@typescript/typescript6@6.0.2)(supports-color@8.1.1))': dependencies: '@babel/runtime': 7.29.2 '@lingui/message-utils': 5.9.5 optionalDependencies: - '@lingui/babel-plugin-lingui-macro': 5.9.5(@typescript/typescript6@6.0.2) + '@lingui/babel-plugin-lingui-macro': 5.9.5(@typescript/typescript6@6.0.2)(supports-color@8.1.1) '@lingui/format-po@5.9.5(@typescript/typescript6@6.0.2)': dependencies: @@ -11338,13 +11545,13 @@ snapshots: '@messageformat/parser': 5.1.1 js-sha256: 0.10.1 - '@lingui/react@5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(@typescript/typescript6@6.0.2))(react@19.1.0)': + '@lingui/react@5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(@typescript/typescript6@6.0.2)(supports-color@8.1.1))(react@19.1.0)': dependencies: '@babel/runtime': 7.29.2 - '@lingui/core': 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(@typescript/typescript6@6.0.2)) + '@lingui/core': 5.9.5(@lingui/babel-plugin-lingui-macro@5.9.5(@typescript/typescript6@6.0.2)(supports-color@8.1.1)) react: 19.1.0 optionalDependencies: - '@lingui/babel-plugin-lingui-macro': 5.9.5(@typescript/typescript6@6.0.2) + '@lingui/babel-plugin-lingui-macro': 5.9.5(@typescript/typescript6@6.0.2)(supports-color@8.1.1) '@messageformat/parser@5.1.1': dependencies: @@ -11440,7 +11647,7 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true - '@pmmmwh/react-refresh-webpack-plugin@0.5.17(react-refresh@0.14.2)(type-fest@1.4.0)(webpack-dev-server@4.15.2(webpack@5.106.2(postcss@8.5.14)))(webpack@5.106.2(postcss@8.5.14))': + '@pmmmwh/react-refresh-webpack-plugin@0.5.17(react-refresh@0.14.2)(type-fest@1.4.0)(webpack-dev-server@4.15.2(debug@4.4.3(supports-color@8.1.1))(supports-color@8.1.1)(webpack@5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14)))(webpack@5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14))': dependencies: ansi-html: 0.0.9 core-js-pure: 3.49.0 @@ -11450,10 +11657,10 @@ snapshots: react-refresh: 0.14.2 schema-utils: 4.3.3 source-map: 0.7.6 - webpack: 5.106.2(postcss@8.5.14) + webpack: 5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14) optionalDependencies: type-fest: 1.4.0 - webpack-dev-server: 4.15.2(webpack@5.106.2(postcss@8.5.14)) + webpack-dev-server: 4.15.2(debug@4.4.3(supports-color@8.1.1))(supports-color@8.1.1)(webpack@5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14)) '@polka/url@1.0.0-next.29': {} @@ -12206,74 +12413,74 @@ snapshots: '@radix-ui/rect@1.1.1': {} - '@react-native-async-storage/async-storage@2.2.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))': + '@react-native-async-storage/async-storage@2.2.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))': dependencies: merge-options: 3.0.4 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) '@react-native/assets-registry@0.81.5': {} - '@react-native/babel-plugin-codegen@0.81.5(@babel/core@7.29.0)': + '@react-native/babel-plugin-codegen@0.81.5(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/traverse': 7.29.0 - '@react-native/codegen': 0.81.5(@babel/core@7.29.0) + '@babel/traverse': 7.29.0(supports-color@8.1.1) + '@react-native/codegen': 0.81.5(@babel/core@7.29.0(supports-color@8.1.1)) transitivePeerDependencies: - '@babel/core' - supports-color - '@react-native/babel-preset@0.81.5(@babel/core@7.29.0)': + '@react-native/babel-preset@0.81.5(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.0 - '@babel/plugin-proposal-export-default-from': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-syntax-export-default-from': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-async-generator-functions': 7.29.0(@babel/core@7.29.0) - '@babel/plugin-transform-async-to-generator': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-block-scoping': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-class-properties': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-classes': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-computed-properties': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.0) - '@babel/plugin-transform-flow-strip-types': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-function-name': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-literals': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-logical-assignment-operators': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-named-capturing-groups-regex': 7.29.0(@babel/core@7.29.0) - '@babel/plugin-transform-nullish-coalescing-operator': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-numeric-separator': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-object-rest-spread': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-optional-catch-binding': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-optional-chaining': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.29.0) - '@babel/plugin-transform-private-methods': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-private-property-in-object': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-react-display-name': 7.28.0(@babel/core@7.29.0) - '@babel/plugin-transform-react-jsx': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-regenerator': 7.29.0(@babel/core@7.29.0) - '@babel/plugin-transform-runtime': 7.29.0(@babel/core@7.29.0) - '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-spread': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-sticky-regex': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.29.0) + '@babel/core': 7.29.0(supports-color@8.1.1) + '@babel/plugin-proposal-export-default-from': 7.27.1(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-syntax-export-default-from': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-async-generator-functions': 7.29.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-async-to-generator': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-block-scoping': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-class-properties': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-classes': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-computed-properties': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-destructuring': 7.28.5(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-flow-strip-types': 7.27.1(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-for-of': 7.27.1(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-function-name': 7.27.1(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-literals': 7.27.1(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-logical-assignment-operators': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-named-capturing-groups-regex': 7.29.0(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-nullish-coalescing-operator': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-numeric-separator': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-object-rest-spread': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-optional-catch-binding': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-optional-chaining': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-private-methods': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-private-property-in-object': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-react-display-name': 7.28.0(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-react-jsx': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-regenerator': 7.29.0(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-runtime': 7.29.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-spread': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-sticky-regex': 7.27.1(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.29.0(supports-color@8.1.1)) '@babel/template': 7.28.6 - '@react-native/babel-plugin-codegen': 0.81.5(@babel/core@7.29.0) + '@react-native/babel-plugin-codegen': 0.81.5(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) babel-plugin-syntax-hermes-parser: 0.29.1 - babel-plugin-transform-flow-enums: 0.0.2(@babel/core@7.29.0) + babel-plugin-transform-flow-enums: 0.0.2(@babel/core@7.29.0(supports-color@8.1.1)) react-refresh: 0.14.2 transitivePeerDependencies: - supports-color - '@react-native/codegen@0.81.5(@babel/core@7.29.0)': + '@react-native/codegen@0.81.5(@babel/core@7.29.0(supports-color@8.1.1))': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/parser': 7.29.3 glob: 7.2.3 hermes-parser: 0.29.1 @@ -12281,17 +12488,17 @@ snapshots: nullthrows: 1.1.1 yargs: 17.7.2 - '@react-native/community-cli-plugin@0.81.5(@react-native/metro-config@0.86.0(@babel/core@7.29.0))': + '@react-native/community-cli-plugin@0.81.5(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@react-native/dev-middleware': 0.81.5 - debug: 4.4.3 + '@react-native/dev-middleware': 0.81.5(supports-color@8.1.1) + debug: 4.4.3(supports-color@8.1.1) invariant: 2.2.4 - metro: 0.83.3 - metro-config: 0.83.3 + metro: 0.83.3(supports-color@8.1.1) + metro-config: 0.83.3(supports-color@8.1.1) metro-core: 0.83.3 semver: 7.8.0 optionalDependencies: - '@react-native/metro-config': 0.86.0(@babel/core@7.29.0) + '@react-native/metro-config': 0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) transitivePeerDependencies: - bufferutil - supports-color @@ -12299,18 +12506,18 @@ snapshots: '@react-native/debugger-frontend@0.81.5': {} - '@react-native/dev-middleware@0.81.5': + '@react-native/dev-middleware@0.81.5(supports-color@8.1.1)': dependencies: '@isaacs/ttlcache': 1.4.1 '@react-native/debugger-frontend': 0.81.5 - chrome-launcher: 0.15.2 - chromium-edge-launcher: 0.2.0 - connect: 3.7.0 - debug: 4.4.3 + chrome-launcher: 0.15.2(supports-color@8.1.1) + chromium-edge-launcher: 0.2.0(supports-color@8.1.1) + connect: 3.7.0(supports-color@8.1.1) + debug: 4.4.3(supports-color@8.1.1) invariant: 2.2.4 nullthrows: 1.1.1 open: 7.4.2 - serve-static: 1.16.3 + serve-static: 1.16.3(supports-color@8.1.1) ws: 6.2.3 transitivePeerDependencies: - bufferutil @@ -12323,20 +12530,20 @@ snapshots: '@react-native/js-polyfills@0.86.0': {} - '@react-native/metro-babel-transformer@0.86.0(@babel/core@7.29.0)': + '@react-native/metro-babel-transformer@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.0 - '@react-native/babel-preset': 0.81.5(@babel/core@7.29.0) + '@babel/core': 7.29.0(supports-color@8.1.1) + '@react-native/babel-preset': 0.81.5(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) hermes-parser: 0.36.0 nullthrows: 1.1.1 transitivePeerDependencies: - supports-color - '@react-native/metro-config@0.86.0(@babel/core@7.29.0)': + '@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: '@react-native/js-polyfills': 0.86.0 - '@react-native/metro-babel-transformer': 0.86.0(@babel/core@7.29.0) - metro-config: 0.84.4 + '@react-native/metro-babel-transformer': 0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + metro-config: 0.84.4(supports-color@8.1.1) metro-runtime: 0.84.4 transitivePeerDependencies: - '@babel/core' @@ -12348,24 +12555,24 @@ snapshots: '@react-native/typescript-config@0.81.6': {} - '@react-native/virtualized-lists@0.81.5(@types/react@19.1.17)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)': + '@react-native/virtualized-lists@0.81.5(@types/react@19.1.17)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)': dependencies: invariant: 2.2.4 nullthrows: 1.1.1 react: 19.1.0 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) optionalDependencies: '@types/react': 19.1.17 - '@react-navigation/bottom-tabs@7.16.0(91eab7bd987834c4c877d5cbf9d3796c)': + '@react-navigation/bottom-tabs@7.16.0(15d086c577324dcb87d77ea7fa3207d2)': dependencies: - '@react-navigation/elements': 2.9.17(@react-navigation/native@7.2.4(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) - '@react-navigation/native': 7.2.4(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + '@react-navigation/elements': 2.9.17(0db73217b8c0536280a44f0cfa3926ed) + '@react-navigation/native': 7.2.4(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0) color: 4.2.3 react: 19.1.0 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) - react-native-safe-area-context: 5.6.2(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) - react-native-screens: 4.24.0(patch_hash=74a74b489f8074a1974c2792c9c97aec30d8e251669aa37fc5c99993bb4a19e6)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) + react-native-safe-area-context: 5.6.2(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0) + react-native-screens: 4.24.0(patch_hash=74a74b489f8074a1974c2792c9c97aec30d8e251669aa37fc5c99993bb4a19e6)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0) sf-symbols-typescript: 2.2.0 transitivePeerDependencies: - '@react-native-masked-view/masked-view' @@ -12382,38 +12589,38 @@ snapshots: use-latest-callback: 0.2.6(react@19.1.0) use-sync-external-store: 1.6.0(react@19.1.0) - '@react-navigation/elements@2.9.17(@react-navigation/native@7.2.4(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)': + '@react-navigation/elements@2.9.17(0db73217b8c0536280a44f0cfa3926ed)': dependencies: - '@react-navigation/native': 7.2.4(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + '@react-navigation/native': 7.2.4(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0) color: 4.2.3 react: 19.1.0 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) - react-native-safe-area-context: 5.6.2(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) + react-native-safe-area-context: 5.6.2(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0) use-latest-callback: 0.2.6(react@19.1.0) use-sync-external-store: 1.6.0(react@19.1.0) - '@react-navigation/native-stack@7.15.0(91eab7bd987834c4c877d5cbf9d3796c)': + '@react-navigation/native-stack@7.15.0(15d086c577324dcb87d77ea7fa3207d2)': dependencies: - '@react-navigation/elements': 2.9.17(@react-navigation/native@7.2.4(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) - '@react-navigation/native': 7.2.4(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + '@react-navigation/elements': 2.9.17(0db73217b8c0536280a44f0cfa3926ed) + '@react-navigation/native': 7.2.4(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0) color: 4.2.3 react: 19.1.0 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) - react-native-safe-area-context: 5.6.2(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) - react-native-screens: 4.24.0(patch_hash=74a74b489f8074a1974c2792c9c97aec30d8e251669aa37fc5c99993bb4a19e6)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) + react-native-safe-area-context: 5.6.2(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0) + react-native-screens: 4.24.0(patch_hash=74a74b489f8074a1974c2792c9c97aec30d8e251669aa37fc5c99993bb4a19e6)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0) sf-symbols-typescript: 2.2.0 warn-once: 0.1.1 transitivePeerDependencies: - '@react-native-masked-view/masked-view' - '@react-navigation/native@7.2.4(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)': + '@react-navigation/native@7.2.4(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)': dependencies: '@react-navigation/core': 7.17.4(react@19.1.0) escape-string-regexp: 4.0.0 fast-deep-equal: 3.1.3 nanoid: 3.3.12 react: 19.1.0 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) use-latest-callback: 0.2.6(react@19.1.0) '@react-navigation/routers@7.5.5': @@ -12438,11 +12645,11 @@ snapshots: '@sentry/replay': 10.64.0 '@sentry/replay-canvas': 10.64.0 - '@sentry/bundler-plugin-core@3.6.1': + '@sentry/bundler-plugin-core@3.6.1(supports-color@8.1.1)': dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@sentry/babel-plugin-component-annotate': 3.6.1 - '@sentry/cli': 2.58.5 + '@sentry/cli': 2.58.5(supports-color@8.1.1) dotenv: 16.6.1 find-up: 5.0.0 glob: 9.3.5 @@ -12500,9 +12707,9 @@ snapshots: '@sentry/cli-win32-x64@3.6.0': optional: true - '@sentry/cli@2.58.5': + '@sentry/cli@2.58.5(supports-color@8.1.1)': dependencies: - https-proxy-agent: 5.0.1 + https-proxy-agent: 5.0.1(supports-color@8.1.1) node-fetch: 2.7.0 progress: 2.0.3 proxy-from-env: 1.1.0 @@ -12542,29 +12749,29 @@ snapshots: dependencies: '@sentry/conventions': 0.15.1 - '@sentry/expo-upload-sourcemaps@8.18.0(patch_hash=2368e1e1986165e7ead3c60017b78b2f9ade1b6a6b2f5ad27cd428608c6f12ae)(@expo/env@2.0.11)(dotenv@16.6.1)': + '@sentry/expo-upload-sourcemaps@8.18.0(patch_hash=2368e1e1986165e7ead3c60017b78b2f9ade1b6a6b2f5ad27cd428608c6f12ae)(@expo/env@2.0.11(supports-color@8.1.1))(dotenv@16.6.1)': dependencies: '@sentry/cli': 3.6.0 optionalDependencies: - '@expo/env': 2.0.11 + '@expo/env': 2.0.11(supports-color@8.1.1) dotenv: 16.6.1 '@sentry/feedback@10.64.0': dependencies: '@sentry/core': 10.64.0 - '@sentry/react-native@8.18.0(@expo/env@2.0.11)(dotenv@16.6.1)(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)': + '@sentry/react-native@8.18.0(61f03fc01fabd5cbd0e83cc2a57309d9)': dependencies: '@sentry/babel-plugin-component-annotate': 5.3.0 '@sentry/browser': 10.64.0 '@sentry/cli': 3.6.0 '@sentry/core': 10.64.0 - '@sentry/expo-upload-sourcemaps': 8.18.0(patch_hash=2368e1e1986165e7ead3c60017b78b2f9ade1b6a6b2f5ad27cd428608c6f12ae)(@expo/env@2.0.11)(dotenv@16.6.1) + '@sentry/expo-upload-sourcemaps': 8.18.0(patch_hash=2368e1e1986165e7ead3c60017b78b2f9ade1b6a6b2f5ad27cd428608c6f12ae)(@expo/env@2.0.11(supports-color@8.1.1))(dotenv@16.6.1) '@sentry/react': 10.64.0(react@19.1.0) react: 19.1.0 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) optionalDependencies: - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) transitivePeerDependencies: - '@expo/env' - dotenv @@ -12585,12 +12792,12 @@ snapshots: '@sentry/browser-utils': 10.64.0 '@sentry/core': 10.64.0 - '@sentry/webpack-plugin@3.6.1(webpack@5.106.2(postcss@8.5.14))': + '@sentry/webpack-plugin@3.6.1(supports-color@8.1.1)(webpack@5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14))': dependencies: - '@sentry/bundler-plugin-core': 3.6.1 + '@sentry/bundler-plugin-core': 3.6.1(supports-color@8.1.1) unplugin: 1.0.1 uuid: 9.0.1 - webpack: 5.106.2(postcss@8.5.14) + webpack: 5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14) transitivePeerDependencies: - encoding - supports-color @@ -12607,6 +12814,8 @@ snapshots: dependencies: '@sinonjs/commons': 3.0.1 + '@standard-schema/spec@1.1.0': {} + '@tanstack/query-async-storage-persister@5.100.10': dependencies: '@tanstack/query-core': 5.100.10 @@ -12629,17 +12838,17 @@ snapshots: '@tanstack/query-core': 5.100.10 react: 19.1.0 - '@testing-library/react-native@13.3.3(jest@29.7.0(@types/node@24.12.4))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)': + '@testing-library/react-native@13.3.3(jest@29.7.0(@types/node@24.12.4)(supports-color@8.1.1))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: jest-matcher-utils: 30.4.1 picocolors: 1.1.1 pretty-format: 30.4.1 react: 19.1.0 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) react-test-renderer: 19.1.0(react@19.1.0) redent: 3.0.0 optionalDependencies: - jest: 29.7.0(@types/node@24.12.4) + jest: 29.7.0(@types/node@24.12.4)(supports-color@8.1.1) '@tiptap/core@2.27.2(@tiptap/pm@2.27.2)': dependencies: @@ -13139,15 +13348,19 @@ snapshots: dependencies: acorn: 8.16.0 + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + acorn-walk@8.3.5: dependencies: acorn: 8.16.0 acorn@8.16.0: {} - agent-base@6.0.2: + agent-base@6.0.2(supports-color@8.1.1): dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -13315,34 +13528,34 @@ snapshots: await-lock@3.0.0: {} - babel-jest@29.7.0(@babel/core@7.29.0): + babel-jest@29.7.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1): dependencies: - '@babel/core': 7.29.0 - '@jest/transform': 29.7.0 + '@babel/core': 7.29.0(supports-color@8.1.1) + '@jest/transform': 29.7.0(supports-color@8.1.1) '@types/babel__core': 7.20.5 - babel-plugin-istanbul: 6.1.1 - babel-preset-jest: 29.6.3(@babel/core@7.29.0) + babel-plugin-istanbul: 6.1.1(supports-color@8.1.1) + babel-preset-jest: 29.6.3(@babel/core@7.29.0(supports-color@8.1.1)) chalk: 4.1.2 graceful-fs: 4.2.11 slash: 3.0.0 transitivePeerDependencies: - supports-color - babel-loader@8.4.1(@babel/core@7.29.0)(webpack@5.106.2(postcss@8.5.14)): + babel-loader@8.4.1(@babel/core@7.29.0(supports-color@8.1.1))(webpack@5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14)): dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) find-cache-dir: 3.3.2 loader-utils: 2.0.4 make-dir: 3.1.0 schema-utils: 2.7.1 - webpack: 5.106.2(postcss@8.5.14) + webpack: 5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14) - babel-plugin-istanbul@6.1.1: + babel-plugin-istanbul@6.1.1(supports-color@8.1.1): dependencies: '@babel/helper-plugin-utils': 7.28.6 '@istanbuljs/load-nyc-config': 1.1.0 '@istanbuljs/schema': 0.1.6 - istanbul-lib-instrument: 5.2.1 + istanbul-lib-instrument: 5.2.1(supports-color@8.1.1) test-exclude: 6.0.0 transitivePeerDependencies: - supports-color @@ -13362,35 +13575,35 @@ snapshots: reselect: 4.1.8 resolve: 1.22.12 - babel-plugin-polyfill-corejs2@0.4.17(@babel/core@7.29.0): + babel-plugin-polyfill-corejs2@0.4.17(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1): dependencies: '@babel/compat-data': 7.29.3 - '@babel/core': 7.29.0 - '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.0) + '@babel/core': 7.29.0(supports-color@8.1.1) + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) semver: 6.3.1 transitivePeerDependencies: - supports-color - babel-plugin-polyfill-corejs3@0.13.0(@babel/core@7.29.0): + babel-plugin-polyfill-corejs3@0.13.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1): dependencies: - '@babel/core': 7.29.0 - '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.0) + '@babel/core': 7.29.0(supports-color@8.1.1) + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) core-js-compat: 3.49.0 transitivePeerDependencies: - supports-color - babel-plugin-polyfill-corejs3@0.14.2(@babel/core@7.29.0): + babel-plugin-polyfill-corejs3@0.14.2(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1): dependencies: - '@babel/core': 7.29.0 - '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.0) + '@babel/core': 7.29.0(supports-color@8.1.1) + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) core-js-compat: 3.49.0 transitivePeerDependencies: - supports-color - babel-plugin-polyfill-regenerator@0.6.8(@babel/core@7.29.0): + babel-plugin-polyfill-regenerator@0.6.8(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1): dependencies: - '@babel/core': 7.29.0 - '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.0) + '@babel/core': 7.29.0(supports-color@8.1.1) + '@babel/helper-define-polyfill-provider': 0.6.8(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -13408,102 +13621,102 @@ snapshots: dependencies: hermes-parser: 0.29.1 - babel-plugin-transform-flow-enums@0.0.2(@babel/core@7.29.0): + babel-plugin-transform-flow-enums@0.0.2(@babel/core@7.29.0(supports-color@8.1.1)): dependencies: - '@babel/plugin-syntax-flow': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-flow': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1)) transitivePeerDependencies: - '@babel/core' babel-plugin-transform-remove-console@6.9.4: {} - babel-preset-current-node-syntax@1.2.0(@babel/core@7.29.0): + babel-preset-current-node-syntax@1.2.0(@babel/core@7.29.0(supports-color@8.1.1)): dependencies: - '@babel/core': 7.29.0 - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.29.0) - '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.29.0) - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.29.0) - '@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.0) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.29.0) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.29.0) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.0) - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.29.0) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.29.0) + '@babel/core': 7.29.0(supports-color@8.1.1) + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.29.0(supports-color@8.1.1)) - babel-preset-expo@54.0.10(@babel/core@7.29.0)(@babel/runtime@7.29.2)(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-refresh@0.14.2): + babel-preset-expo@54.0.10(@babel/core@7.29.0(supports-color@8.1.1))(@babel/runtime@7.29.2)(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1))(react-refresh@0.14.2)(supports-color@8.1.1): dependencies: - '@babel/helper-module-imports': 7.28.6 - '@babel/plugin-proposal-decorators': 7.29.0(@babel/core@7.29.0) - '@babel/plugin-proposal-export-default-from': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-syntax-export-default-from': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-class-static-block': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-export-namespace-from': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-flow-strip-types': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-object-rest-spread': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.29.0) - '@babel/plugin-transform-private-methods': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-private-property-in-object': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-runtime': 7.29.0(@babel/core@7.29.0) - '@babel/preset-react': 7.28.5(@babel/core@7.29.0) - '@babel/preset-typescript': 7.28.5(@babel/core@7.29.0) - '@react-native/babel-preset': 0.81.5(@babel/core@7.29.0) + '@babel/helper-module-imports': 7.28.6(supports-color@8.1.1) + '@babel/plugin-proposal-decorators': 7.29.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-proposal-export-default-from': 7.27.1(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-syntax-export-default-from': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-class-static-block': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-export-namespace-from': 7.27.1(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-flow-strip-types': 7.27.1(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-object-rest-spread': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-private-methods': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-private-property-in-object': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-runtime': 7.29.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/preset-react': 7.28.5(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/preset-typescript': 7.28.5(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@react-native/babel-preset': 0.81.5(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) babel-plugin-react-compiler: 1.0.0 babel-plugin-react-native-web: 0.21.2 babel-plugin-syntax-hermes-parser: 0.29.1 - babel-plugin-transform-flow-enums: 0.0.2(@babel/core@7.29.0) - debug: 4.4.3 + babel-plugin-transform-flow-enums: 0.0.2(@babel/core@7.29.0(supports-color@8.1.1)) + debug: 4.4.3(supports-color@8.1.1) react-refresh: 0.14.2 resolve-from: 5.0.0 optionalDependencies: '@babel/runtime': 7.29.2 - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) transitivePeerDependencies: - '@babel/core' - supports-color - babel-preset-expo@54.0.12(@babel/core@7.29.0)(@babel/runtime@7.29.2)(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-refresh@0.14.2): + babel-preset-expo@54.0.12(@babel/core@7.29.0(supports-color@8.1.1))(@babel/runtime@7.29.2)(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1))(react-refresh@0.14.2)(supports-color@8.1.1): dependencies: - '@babel/helper-module-imports': 7.28.6 - '@babel/plugin-proposal-decorators': 7.29.0(@babel/core@7.29.0) - '@babel/plugin-proposal-export-default-from': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-syntax-export-default-from': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-class-static-block': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-export-namespace-from': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-flow-strip-types': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-object-rest-spread': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.29.0) - '@babel/plugin-transform-private-methods': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-private-property-in-object': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-runtime': 7.29.0(@babel/core@7.29.0) - '@babel/preset-react': 7.28.5(@babel/core@7.29.0) - '@babel/preset-typescript': 7.28.5(@babel/core@7.29.0) - '@react-native/babel-preset': 0.81.5(@babel/core@7.29.0) + '@babel/helper-module-imports': 7.28.6(supports-color@8.1.1) + '@babel/plugin-proposal-decorators': 7.29.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-proposal-export-default-from': 7.27.1(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-syntax-export-default-from': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-class-static-block': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-export-namespace-from': 7.27.1(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-flow-strip-types': 7.27.1(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-object-rest-spread': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-parameters': 7.27.7(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-private-methods': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-private-property-in-object': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-runtime': 7.29.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/preset-react': 7.28.5(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/preset-typescript': 7.28.5(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@react-native/babel-preset': 0.81.5(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) babel-plugin-react-compiler: 1.0.0 babel-plugin-react-native-web: 0.21.2 babel-plugin-syntax-hermes-parser: 0.29.1 - babel-plugin-transform-flow-enums: 0.0.2(@babel/core@7.29.0) - debug: 4.4.3 + babel-plugin-transform-flow-enums: 0.0.2(@babel/core@7.29.0(supports-color@8.1.1)) + debug: 4.4.3(supports-color@8.1.1) react-refresh: 0.14.2 resolve-from: 5.0.0 optionalDependencies: '@babel/runtime': 7.29.2 - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) transitivePeerDependencies: - '@babel/core' - supports-color - babel-preset-jest@29.6.3(@babel/core@7.29.0): + babel-preset-jest@29.6.3(@babel/core@7.29.0(supports-color@8.1.1)): dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) babel-plugin-jest-hoist: 29.6.3 - babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0) + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0(supports-color@8.1.1)) badgin@1.2.3: {} @@ -13543,11 +13756,11 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 - body-parser@1.20.5: + body-parser@1.20.5(supports-color@8.1.1): dependencies: bytes: 3.1.2 content-type: 1.0.5 - debug: 2.6.9 + debug: 2.6.9(supports-color@8.1.1) depd: 2.0.0 destroy: 1.2.0 http-errors: 2.0.1 @@ -13704,23 +13917,23 @@ snapshots: chownr@3.0.0: {} - chrome-launcher@0.15.2: + chrome-launcher@0.15.2(supports-color@8.1.1): dependencies: '@types/node': 24.12.4 escape-string-regexp: 4.0.0 is-wsl: 2.2.0 - lighthouse-logger: 1.4.2 + lighthouse-logger: 1.4.2(supports-color@8.1.1) transitivePeerDependencies: - supports-color chrome-trace-event@1.0.4: {} - chromium-edge-launcher@0.2.0: + chromium-edge-launcher@0.2.0(supports-color@8.1.1): dependencies: '@types/node': 24.12.4 escape-string-regexp: 4.0.0 is-wsl: 2.2.0 - lighthouse-logger: 1.4.2 + lighthouse-logger: 1.4.2(supports-color@8.1.1) mkdirp: 1.0.4 rimraf: 3.0.2 transitivePeerDependencies: @@ -13738,10 +13951,10 @@ snapshots: dependencies: source-map: 0.6.1 - clean-webpack-plugin@4.0.0(webpack@5.106.2(postcss@8.5.14)): + clean-webpack-plugin@4.0.0(webpack@5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14)): dependencies: del: 4.1.1 - webpack: 5.106.2(postcss@8.5.14) + webpack: 5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14) cli-cursor@2.1.0: dependencies: @@ -13840,11 +14053,11 @@ snapshots: dependencies: mime-db: 1.54.0 - compression@1.8.1: + compression@1.8.1(supports-color@8.1.1): dependencies: bytes: 3.1.2 compressible: 2.0.18 - debug: 2.6.9 + debug: 2.6.9(supports-color@8.1.1) negotiator: 0.6.4 on-headers: 1.1.0 safe-buffer: 5.2.1 @@ -13856,10 +14069,10 @@ snapshots: connect-history-api-fallback@2.0.0: {} - connect@3.7.0: + connect@3.7.0(supports-color@8.1.1): dependencies: - debug: 2.6.9 - finalhandler: 1.1.2 + debug: 2.6.9(supports-color@8.1.1) + finalhandler: 1.1.2(supports-color@8.1.1) parseurl: 1.3.3 utils-merge: 1.0.1 transitivePeerDependencies: @@ -13877,7 +14090,7 @@ snapshots: cookie@0.7.2: {} - copy-webpack-plugin@10.2.4(webpack@5.106.2(postcss@8.5.14)): + copy-webpack-plugin@10.2.4(webpack@5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14)): dependencies: fast-glob: 3.3.3 glob-parent: 6.0.2 @@ -13885,7 +14098,7 @@ snapshots: normalize-path: 3.0.0 schema-utils: 4.3.3 serialize-javascript: 6.0.2 - webpack: 5.106.2(postcss@8.5.14) + webpack: 5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14) core-js-compat@3.49.0: dependencies: @@ -13904,13 +14117,13 @@ snapshots: optionalDependencies: typescript: '@typescript/typescript6@6.0.2' - create-jest@29.7.0(@types/node@24.12.4): + create-jest@29.7.0(@types/node@24.12.4)(supports-color@8.1.1): dependencies: '@jest/types': 29.6.3 chalk: 4.1.2 exit: 0.1.2 graceful-fs: 4.2.11 - jest-config: 29.7.0(@types/node@24.12.4) + jest-config: 29.7.0(@types/node@24.12.4)(supports-color@8.1.1) jest-util: 29.7.0 prompts: 2.4.2 transitivePeerDependencies: @@ -13945,7 +14158,7 @@ snapshots: dependencies: utrie: 1.0.2 - css-loader@6.11.0(webpack@5.106.2(postcss@8.5.14)): + css-loader@6.11.0(webpack@5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14)): dependencies: icss-utils: 5.1.0(postcss@8.5.14) postcss: 8.5.14 @@ -13956,11 +14169,11 @@ snapshots: postcss-value-parser: 4.2.0 semver: 7.8.0 optionalDependencies: - webpack: 5.106.2(postcss@8.5.14) + webpack: 5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14) css-mediaquery@0.1.2: {} - css-minimizer-webpack-plugin@3.4.1(clean-css@5.3.3)(csso@5.0.5)(esbuild@0.25.12)(webpack@5.106.2(postcss@8.5.14)): + css-minimizer-webpack-plugin@3.4.1(clean-css@5.3.3)(csso@5.0.5)(esbuild@0.25.12)(webpack@5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14)): dependencies: cssnano: 5.1.15(postcss@8.5.14) jest-worker: 27.5.1 @@ -13968,7 +14181,7 @@ snapshots: schema-utils: 4.3.3 serialize-javascript: 6.0.2 source-map: 0.6.1 - webpack: 5.106.2(postcss@8.5.14) + webpack: 5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14) optionalDependencies: clean-css: 5.3.3 csso: 5.0.5 @@ -14101,17 +14314,23 @@ snapshots: debounce@1.2.1: {} - debug@2.6.9: + debug@2.6.9(supports-color@8.1.1): dependencies: ms: 2.0.0 + optionalDependencies: + supports-color: 8.1.1 - debug@3.2.7: + debug@3.2.7(supports-color@8.1.1): dependencies: ms: 2.1.3 + optionalDependencies: + supports-color: 8.1.1 - debug@4.4.3: + debug@4.4.3(supports-color@8.1.1): dependencies: ms: 2.1.3 + optionalDependencies: + supports-color: 8.1.1 decamelize@1.2.0: {} @@ -14123,6 +14342,8 @@ snapshots: deep-extend@0.6.0: {} + deep-is@0.1.4: {} + deepmerge@4.3.1: {} default-gateway@6.0.3: @@ -14187,6 +14408,10 @@ snapshots: dependencies: '@leichtgewicht/ip-codec': 2.0.5 + doctrine@3.0.0: + dependencies: + esutils: 2.0.3 + dom-converter@0.2.0: dependencies: utila: 0.4.0 @@ -14430,46 +14655,106 @@ snapshots: optionalDependencies: source-map: 0.6.1 - eslint-plugin-react-compiler@19.1.0-rc.2(eslint@eslint): + eslint-plugin-react-compiler@19.1.0-rc.2(eslint@8.57.1(supports-color@8.1.1))(supports-color@8.1.1): dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/parser': 7.29.3 - '@babel/plugin-proposal-private-methods': 7.18.6(@babel/core@7.29.0) - eslint: link:eslint + '@babel/plugin-proposal-private-methods': 7.18.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + eslint: 8.57.1(supports-color@8.1.1) hermes-parser: 0.25.1 zod: 3.25.76 zod-validation-error: 3.5.4(zod@3.25.76) transitivePeerDependencies: - supports-color - eslint-plugin-react-native-a11y@3.5.1(eslint@eslint): + eslint-plugin-react-native-a11y@3.5.1(eslint@8.57.1(supports-color@8.1.1)): dependencies: '@babel/runtime': 7.29.2 ast-types-flow: 0.0.7 - eslint: link:eslint + eslint: 8.57.1(supports-color@8.1.1) jsx-ast-utils: 3.3.5 eslint-plugin-react-native-globals@0.1.2: {} - eslint-plugin-react-native@5.0.0(eslint@eslint): + eslint-plugin-react-native@5.0.0(eslint@8.57.1(supports-color@8.1.1)): dependencies: - eslint: link:eslint + eslint: 8.57.1(supports-color@8.1.1) eslint-plugin-react-native-globals: 0.1.2 - eslint-plugin-simple-import-sort@13.0.0(eslint@eslint): + eslint-plugin-simple-import-sort@13.0.0(eslint@8.57.1(supports-color@8.1.1)): dependencies: - eslint: link:eslint + eslint: 8.57.1(supports-color@8.1.1) eslint-scope@5.1.1: dependencies: esrecurse: 4.3.0 estraverse: 4.3.0 + eslint-scope@7.2.2: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint@8.57.1(supports-color@8.1.1): + dependencies: + '@eslint-community/eslint-utils': 4.10.1(eslint@8.57.1(supports-color@8.1.1)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/eslintrc': 2.1.4(supports-color@8.1.1) + '@eslint/js': 8.57.1 + '@humanwhocodes/config-array': 0.13.0(supports-color@8.1.1) + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + '@ungap/structured-clone': 1.3.1 + ajv: 6.15.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3(supports-color@8.1.1) + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.2.2 + eslint-visitor-keys: 3.4.3 + espree: 9.6.1 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.24.0 + graphemer: 1.4.0 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-yaml: 4.1.1 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + strip-ansi: 6.0.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + esm@3.2.25: optional: true + espree@9.6.1: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 3.4.3 + esprima@4.0.1: {} + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + esrecurse@4.3.0: dependencies: estraverse: 5.3.0 @@ -14521,208 +14806,208 @@ snapshots: jest-mock: 30.4.1 jest-util: 30.4.1 - expo-age-range@0.2.18(patch_hash=c325225749993424461eeece1d97a99b19c00da2ebc958a73c12e4eca0c39306)(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0)): + expo-age-range@0.2.18(patch_hash=c325225749993424461eeece1d97a99b19c00da2ebc958a73c12e4eca0c39306)(83f21d12a6c1aa99b4a65d070875cc68): dependencies: - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) - expo-application@7.0.8(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)): + expo-application@7.0.8(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1)): dependencies: - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) - expo-asset@12.0.13(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + expo-asset@12.0.13(93a6abd34ab5ae64aff6a80525489f34): dependencies: '@expo/image-utils': 0.8.12 - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) - expo-constants: 18.0.13(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0)) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) + expo-constants: 18.0.13(c1408f750868568d2dc75671b50084a7) react: 19.1.0 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) transitivePeerDependencies: - supports-color - expo-blur@15.0.8(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + expo-blur@15.0.8(83e0f64f4281e4798012214ad5b2d72d): dependencies: - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) react: 19.1.0 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) - expo-build-properties@1.0.10(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)): + expo-build-properties@1.0.10(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1)): dependencies: ajv: 8.20.0 - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) semver: 7.8.0 - expo-camera@17.0.10(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-web@0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + expo-camera@17.0.10(8d9f18414d5ddcf91a3e8d2e3aa26778): dependencies: - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) invariant: 2.2.4 react: 19.1.0 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) optionalDependencies: react-native-web: 0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - expo-clipboard@8.0.8(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + expo-clipboard@8.0.8(83e0f64f4281e4798012214ad5b2d72d): dependencies: - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) react: 19.1.0 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) - expo-constants@18.0.13(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0)): + expo-constants@18.0.13(c1408f750868568d2dc75671b50084a7): dependencies: - '@expo/config': 12.0.13 - '@expo/env': 2.0.11 - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + '@expo/config': 12.0.13(supports-color@8.1.1) + '@expo/env': 2.0.11(supports-color@8.1.1) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) transitivePeerDependencies: - supports-color - expo-contacts@15.0.11(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + expo-contacts@15.0.11(83e0f64f4281e4798012214ad5b2d72d): dependencies: - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) react: 19.1.0 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) - expo-dev-client@6.0.21(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)): + expo-dev-client@6.0.21(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1))(supports-color@8.1.1): dependencies: - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) - expo-dev-launcher: 6.0.21(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)) - expo-dev-menu: 7.0.19(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)) - expo-dev-menu-interface: 2.0.0(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)) - expo-manifests: 1.0.11(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)) - expo-updates-interface: 2.0.0(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) + expo-dev-launcher: 6.0.21(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1))(supports-color@8.1.1) + expo-dev-menu: 7.0.19(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1)) + expo-dev-menu-interface: 2.0.0(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1)) + expo-manifests: 1.0.11(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1))(supports-color@8.1.1) + expo-updates-interface: 2.0.0(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1)) transitivePeerDependencies: - supports-color - expo-dev-launcher@6.0.21(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)): + expo-dev-launcher@6.0.21(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1))(supports-color@8.1.1): dependencies: ajv: 8.20.0 - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) - expo-dev-menu: 7.0.19(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)) - expo-manifests: 1.0.11(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) + expo-dev-menu: 7.0.19(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1)) + expo-manifests: 1.0.11(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1))(supports-color@8.1.1) transitivePeerDependencies: - supports-color - expo-dev-menu-interface@2.0.0(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)): + expo-dev-menu-interface@2.0.0(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1)): dependencies: - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) - expo-dev-menu@7.0.19(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)): + expo-dev-menu@7.0.19(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1)): dependencies: - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) - expo-dev-menu-interface: 2.0.0(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) + expo-dev-menu-interface: 2.0.0(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1)) - expo-device@8.0.10(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)): + expo-device@8.0.10(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1)): dependencies: - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) ua-parser-js: 0.7.41 expo-eas-client@1.0.8: {} - expo-file-system@19.0.22(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0)): + expo-file-system@19.0.22(83f21d12a6c1aa99b4a65d070875cc68): dependencies: - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) - expo-file-system@19.0.23(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0)): + expo-file-system@19.0.23(83f21d12a6c1aa99b4a65d070875cc68): dependencies: - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) - expo-font@14.0.11(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + expo-font@14.0.11(83e0f64f4281e4798012214ad5b2d72d): dependencies: - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) fontfaceobserver: 2.3.0 react: 19.1.0 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) - expo-font@14.0.12(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + expo-font@14.0.12(83e0f64f4281e4798012214ad5b2d72d): dependencies: - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) fontfaceobserver: 2.3.0 react: 19.1.0 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) - expo-glass-effect@0.1.10(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + expo-glass-effect@0.1.10(83e0f64f4281e4798012214ad5b2d72d): dependencies: - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) react: 19.1.0 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) - expo-haptics@15.0.8(patch_hash=b33910ba2753c4eccdc885b449e6e33aa90b9cefa75ca8639762a26013141410)(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)): + expo-haptics@15.0.8(patch_hash=b33910ba2753c4eccdc885b449e6e33aa90b9cefa75ca8639762a26013141410)(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1)): dependencies: - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) - expo-image-loader@6.0.0(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)): + expo-image-loader@6.0.0(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1)): dependencies: - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) - expo-image-manipulator@14.0.8(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)): + expo-image-manipulator@14.0.8(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1)): dependencies: - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) - expo-image-loader: 6.0.0(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) + expo-image-loader: 6.0.0(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1)) - expo-image-picker@17.0.11(patch_hash=47b28f6ddb8bcaa6483f8714c82daaa0001122f2b0dac6c05d19e98a61a6ceab)(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)): + expo-image-picker@17.0.11(patch_hash=47b28f6ddb8bcaa6483f8714c82daaa0001122f2b0dac6c05d19e98a61a6ceab)(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1)): dependencies: - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) - expo-image-loader: 6.0.0(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) + expo-image-loader: 6.0.0(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1)) - expo-image@3.0.11(patch_hash=6d46ffac33426c5285777da5bf5f88c68c8ce27290595dfa65f38c478930c2c4)(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-web@0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + expo-image@3.0.11(patch_hash=6d46ffac33426c5285777da5bf5f88c68c8ce27290595dfa65f38c478930c2c4)(8d9f18414d5ddcf91a3e8d2e3aa26778): dependencies: - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) react: 19.1.0 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) optionalDependencies: react-native-web: 0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - expo-intent-launcher@13.0.8(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)): + expo-intent-launcher@13.0.8(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1)): dependencies: - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) expo-json-utils@0.15.0: {} - expo-keep-awake@15.0.8(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react@19.1.0): + expo-keep-awake@15.0.8(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1))(react@19.1.0): dependencies: - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) react: 19.1.0 - expo-linear-gradient@15.0.8(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + expo-linear-gradient@15.0.8(83e0f64f4281e4798012214ad5b2d72d): dependencies: - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) react: 19.1.0 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) - expo-linking@8.0.12(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + expo-linking@8.0.12(93a6abd34ab5ae64aff6a80525489f34): dependencies: - expo-constants: 18.0.13(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0)) + expo-constants: 18.0.13(c1408f750868568d2dc75671b50084a7) invariant: 2.2.4 react: 19.1.0 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) transitivePeerDependencies: - expo - supports-color - expo-localization@17.0.8(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react@19.1.0): + expo-localization@17.0.8(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1))(react@19.1.0): dependencies: - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) react: 19.1.0 rtl-detect: 1.1.2 - expo-location@19.0.8(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)): + expo-location@19.0.8(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1)): dependencies: - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) - expo-manifests@1.0.11(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)): + expo-manifests@1.0.11(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1))(supports-color@8.1.1): dependencies: - '@expo/config': 12.0.13 - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + '@expo/config': 12.0.13(supports-color@8.1.1) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) expo-json-utils: 0.15.0 transitivePeerDependencies: - supports-color - expo-media-library@18.2.1(patch_hash=2b84c17999bb0c977eaef7fa8ac297f7074d91c94fb63726b42d2a277b26b39a)(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0)): + expo-media-library@18.2.1(patch_hash=2b84c17999bb0c977eaef7fa8ac297f7074d91c94fb63726b42d2a277b26b39a)(83f21d12a6c1aa99b4a65d070875cc68): dependencies: - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) expo-modules-autolinking@3.0.26: dependencies: @@ -14732,150 +15017,150 @@ snapshots: require-from-string: 2.0.2 resolve-from: 5.0.0 - expo-modules-core@3.0.30(patch_hash=952fb1c8cb6dc8a0d8ef4e1114942a341d89b025e011f1acfff788929997346e)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + expo-modules-core@3.0.30(patch_hash=952fb1c8cb6dc8a0d8ef4e1114942a341d89b025e011f1acfff788929997346e)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0): dependencies: invariant: 2.2.4 react: 19.1.0 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) - expo-notifications@0.32.17(patch_hash=a45a8dcf3d8c4b5df4ee0e62bc79b755fcf9d28ca51a6f685b85830bf4afc2e5)(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + expo-notifications@0.32.17(patch_hash=a45a8dcf3d8c4b5df4ee0e62bc79b755fcf9d28ca51a6f685b85830bf4afc2e5)(93a6abd34ab5ae64aff6a80525489f34): dependencies: '@expo/image-utils': 0.8.12 '@ide/backoff': 1.0.0 abort-controller: 3.0.0 assert: 2.1.0 badgin: 1.2.3 - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) - expo-application: 7.0.8(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)) - expo-constants: 18.0.13(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0)) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) + expo-application: 7.0.8(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1)) + expo-constants: 18.0.13(c1408f750868568d2dc75671b50084a7) react: 19.1.0 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) transitivePeerDependencies: - supports-color - expo-paste-input@0.2.1(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + expo-paste-input@0.2.1(83e0f64f4281e4798012214ad5b2d72d): dependencies: - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) react: 19.1.0 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) - expo-privacy-sensitive@0.2.0(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + expo-privacy-sensitive@0.2.0(83e0f64f4281e4798012214ad5b2d72d): dependencies: - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) react: 19.1.0 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) - expo-pwa@0.0.127(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)): + expo-pwa@0.0.127(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1)): dependencies: '@expo/image-utils': 0.8.12 chalk: 4.1.2 commander: 2.20.0 - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) update-check: 1.5.3 - expo-screen-orientation@9.0.9(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0)): + expo-screen-orientation@9.0.9(83f21d12a6c1aa99b4a65d070875cc68): dependencies: - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) expo-server@1.0.7: {} - expo-sharing@14.0.8(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)): + expo-sharing@14.0.8(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1)): dependencies: - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) - expo-sms@14.0.8(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)): + expo-sms@14.0.8(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1)): dependencies: - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) - expo-splash-screen@31.0.13(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)): + expo-splash-screen@31.0.13(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1))(supports-color@8.1.1): dependencies: - '@expo/prebuild-config': 54.0.8(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)) - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + '@expo/prebuild-config': 54.0.8(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1))(supports-color@8.1.1) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) transitivePeerDependencies: - supports-color expo-structured-headers@5.0.0: {} - expo-system-ui@6.0.9(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-web@0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0)): + expo-system-ui@6.0.9(41b38c851305f17c9d2f63b3bbab71a9): dependencies: '@react-native/normalize-colors': 0.81.5 - debug: 4.4.3 - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + debug: 4.4.3(supports-color@8.1.1) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) optionalDependencies: react-native-web: 0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) transitivePeerDependencies: - supports-color - expo-updates-interface@2.0.0(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)): + expo-updates-interface@2.0.0(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1)): dependencies: - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) - expo-updates@29.0.17(patch_hash=04f28cb005b770e9ae8f0065eab96e43cbb1e58107f5f6ad1bdd18f6deb66487)(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + expo-updates@29.0.17(patch_hash=04f28cb005b770e9ae8f0065eab96e43cbb1e58107f5f6ad1bdd18f6deb66487)(93a6abd34ab5ae64aff6a80525489f34): dependencies: '@expo/code-signing-certificates': 0.0.6 '@expo/plist': 0.4.8 '@expo/spawn-async': 1.7.2 arg: 4.1.0 chalk: 4.1.2 - debug: 4.4.3 - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + debug: 4.4.3(supports-color@8.1.1) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) expo-eas-client: 1.0.8 - expo-manifests: 1.0.11(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)) + expo-manifests: 1.0.11(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1))(supports-color@8.1.1) expo-structured-headers: 5.0.0 - expo-updates-interface: 2.0.0(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)) + expo-updates-interface: 2.0.0(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1)) getenv: 2.0.0 glob: 13.0.6 ignore: 5.3.2 react: 19.1.0 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) resolve-from: 5.0.0 transitivePeerDependencies: - supports-color - expo-video-thumbnails@10.0.8(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)): + expo-video-thumbnails@10.0.8(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1)): dependencies: - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) - expo-video@3.0.16(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + expo-video@3.0.16(83e0f64f4281e4798012214ad5b2d72d): dependencies: - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) react: 19.1.0 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) - expo-web-browser@15.0.11(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0)): + expo-web-browser@15.0.11(83f21d12a6c1aa99b4a65d070875cc68): dependencies: - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) - expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1): dependencies: '@babel/runtime': 7.29.2 - '@expo/cli': 54.0.25(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0)) - '@expo/config': 12.0.13 - '@expo/config-plugins': 54.0.4 - '@expo/devtools': 0.1.8(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) - '@expo/fingerprint': 0.15.5 - '@expo/metro': 54.2.0 - '@expo/metro-config': 54.0.16(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)) - '@expo/vector-icons': 15.1.1(expo-font@14.0.12(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + '@expo/cli': 54.0.25(c1408f750868568d2dc75671b50084a7) + '@expo/config': 12.0.13(supports-color@8.1.1) + '@expo/config-plugins': 54.0.4(supports-color@8.1.1) + '@expo/devtools': 0.1.8(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0) + '@expo/fingerprint': 0.15.5(supports-color@8.1.1) + '@expo/metro': 54.2.0(supports-color@8.1.1) + '@expo/metro-config': 54.0.16(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1))(supports-color@8.1.1) + '@expo/vector-icons': 15.1.1(expo-font@14.0.12(83e0f64f4281e4798012214ad5b2d72d))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0) '@ungap/structured-clone': 1.3.1 - babel-preset-expo: 54.0.12(@babel/core@7.29.0)(@babel/runtime@7.29.2)(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-refresh@0.14.2) - expo-asset: 12.0.13(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) - expo-constants: 18.0.13(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0)) - expo-file-system: 19.0.23(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0)) - expo-font: 14.0.12(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) - expo-keep-awake: 15.0.8(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react@19.1.0) + babel-preset-expo: 54.0.12(@babel/core@7.29.0(supports-color@8.1.1))(@babel/runtime@7.29.2)(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1))(react-refresh@0.14.2)(supports-color@8.1.1) + expo-asset: 12.0.13(93a6abd34ab5ae64aff6a80525489f34) + expo-constants: 18.0.13(c1408f750868568d2dc75671b50084a7) + expo-file-system: 19.0.23(83f21d12a6c1aa99b4a65d070875cc68) + expo-font: 14.0.12(83e0f64f4281e4798012214ad5b2d72d) + expo-keep-awake: 15.0.8(expo@54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1))(react@19.1.0) expo-modules-autolinking: 3.0.26 - expo-modules-core: 3.0.30(patch_hash=952fb1c8cb6dc8a0d8ef4e1114942a341d89b025e011f1acfff788929997346e)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo-modules-core: 3.0.30(patch_hash=952fb1c8cb6dc8a0d8ef4e1114942a341d89b025e011f1acfff788929997346e)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0) pretty-format: 29.7.0 react: 19.1.0 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) react-refresh: 0.14.2 whatwg-url-without-unicode: 8.0.0-3 optionalDependencies: - react-native-webview: 13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + react-native-webview: 13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0) transitivePeerDependencies: - '@babel/core' - bufferutil @@ -14886,21 +15171,21 @@ snapshots: exponential-backoff@3.1.3: {} - express@4.22.2: + express@4.22.2(supports-color@8.1.1): dependencies: accepts: 1.3.8 array-flatten: 1.1.1 - body-parser: 1.20.5 + body-parser: 1.20.5(supports-color@8.1.1) content-disposition: 0.5.4 content-type: 1.0.5 cookie: 0.7.2 cookie-signature: 1.0.7 - debug: 2.6.9 + debug: 2.6.9(supports-color@8.1.1) depd: 2.0.0 encodeurl: 2.0.0 escape-html: 1.0.3 etag: 1.8.1 - finalhandler: 1.3.2 + finalhandler: 1.3.2(supports-color@8.1.1) fresh: 0.5.2 http-errors: 2.0.1 merge-descriptors: 1.0.3 @@ -14912,8 +15197,8 @@ snapshots: qs: 6.15.1 range-parser: 1.2.1 safe-buffer: 5.2.1 - send: 0.19.2 - serve-static: 1.16.3 + send: 0.19.2(supports-color@8.1.1) + serve-static: 1.16.3(supports-color@8.1.1) setprototypeof: 1.2.0 statuses: 2.0.2 type-is: 1.6.18 @@ -14943,6 +15228,8 @@ snapshots: json-schema-ref-resolver: 3.0.0 rfdc: 1.4.1 + fast-levenshtein@2.0.6: {} + fast-text-encoding@1.0.6: {} fast-uri@3.1.2: {} @@ -14977,11 +15264,15 @@ snapshots: optionalDependencies: picomatch: 4.0.4 - file-loader@6.2.0(webpack@5.106.2(postcss@8.5.14)): + file-entry-cache@6.0.1: + dependencies: + flat-cache: 3.2.0 + + file-loader@6.2.0(webpack@5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14)): dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 - webpack: 5.106.2(postcss@8.5.14) + webpack: 5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14) fill-range@7.1.1: dependencies: @@ -14989,9 +15280,9 @@ snapshots: filter-obj@1.1.0: {} - finalhandler@1.1.2: + finalhandler@1.1.2(supports-color@8.1.1): dependencies: - debug: 2.6.9 + debug: 2.6.9(supports-color@8.1.1) encodeurl: 1.0.2 escape-html: 1.0.3 on-finished: 2.3.0 @@ -15001,9 +15292,9 @@ snapshots: transitivePeerDependencies: - supports-color - finalhandler@1.3.2: + finalhandler@1.3.2(supports-color@8.1.1): dependencies: - debug: 2.6.9 + debug: 2.6.9(supports-color@8.1.1) encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 @@ -15041,9 +15332,19 @@ snapshots: dependencies: micromatch: 4.0.8 + flat-cache@3.2.0: + dependencies: + flatted: 3.4.4 + keyv: 4.5.4 + rimraf: 3.0.2 + + flatted@3.4.4: {} + flow-enums-runtime@0.0.6: {} - follow-redirects@1.16.0: {} + follow-redirects@1.16.0(debug@4.4.3(supports-color@8.1.1)): + optionalDependencies: + debug: 4.4.3(supports-color@8.1.1) fontfaceobserver@2.3.0: {} @@ -15190,6 +15491,10 @@ snapshots: minipass: 4.2.8 path-scurry: 1.11.1 + globals@13.24.0: + dependencies: + type-fest: 0.20.2 + globals@17.6.0: {} globalthis@1.0.4: @@ -15218,6 +15523,8 @@ snapshots: graceful-fs@4.2.11: {} + graphemer@1.4.0: {} + gzip-size@6.0.0: dependencies: duplexer: 0.1.2 @@ -15315,7 +15622,7 @@ snapshots: relateurl: 0.2.7 terser: 5.47.1 - html-webpack-plugin@5.6.7(webpack@5.106.2(postcss@8.5.14)): + html-webpack-plugin@5.6.7(webpack@5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14)): dependencies: '@types/html-minifier-terser': 6.1.0 html-minifier-terser: 6.1.0 @@ -15323,7 +15630,7 @@ snapshots: pretty-error: 4.0.0 tapable: 2.3.3 optionalDependencies: - webpack: 5.106.2(postcss@8.5.14) + webpack: 5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14) html2canvas@1.4.1: dependencies: @@ -15357,18 +15664,18 @@ snapshots: http-parser-js@0.5.10: {} - http-proxy-agent@5.0.0: + http-proxy-agent@5.0.0(supports-color@8.1.1): dependencies: '@tootallnate/once': 2.0.1 - agent-base: 6.0.2 - debug: 4.4.3 + agent-base: 6.0.2(supports-color@8.1.1) + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color - http-proxy-middleware@2.0.9(@types/express@4.17.25): + http-proxy-middleware@2.0.9(@types/express@4.17.25)(debug@4.4.3(supports-color@8.1.1)): dependencies: '@types/http-proxy': 1.17.17 - http-proxy: 1.18.1 + http-proxy: 1.18.1(debug@4.4.3(supports-color@8.1.1)) is-glob: 4.0.3 is-plain-obj: 3.0.0 micromatch: 4.0.8 @@ -15377,25 +15684,25 @@ snapshots: transitivePeerDependencies: - debug - http-proxy@1.18.1: + http-proxy@1.18.1(debug@4.4.3(supports-color@8.1.1)): dependencies: eventemitter3: 4.0.7 - follow-redirects: 1.16.0 + follow-redirects: 1.16.0(debug@4.4.3(supports-color@8.1.1)) requires-port: 1.0.0 transitivePeerDependencies: - debug - https-proxy-agent@5.0.1: + https-proxy-agent@5.0.1(supports-color@8.1.1): dependencies: - agent-base: 6.0.2 - debug: 4.4.3 + agent-base: 6.0.2(supports-color@8.1.1) + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color - https-proxy-agent@7.0.6: + https-proxy-agent@7.0.6(supports-color@8.1.1): dependencies: agent-base: 7.1.4 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -15592,6 +15899,8 @@ snapshots: dependencies: path-is-inside: 1.0.2 + is-path-inside@3.0.3: {} + is-plain-obj@2.1.0: {} is-plain-obj@3.0.0: {} @@ -15655,9 +15964,9 @@ snapshots: istanbul-lib-coverage@3.2.2: {} - istanbul-lib-instrument@5.2.1: + istanbul-lib-instrument@5.2.1(supports-color@8.1.1): dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/parser': 7.29.3 '@istanbuljs/schema': 0.1.6 istanbul-lib-coverage: 3.2.2 @@ -15665,9 +15974,9 @@ snapshots: transitivePeerDependencies: - supports-color - istanbul-lib-instrument@6.0.3: + istanbul-lib-instrument@6.0.3(supports-color@8.1.1): dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/parser': 7.29.3 '@istanbuljs/schema': 0.1.6 istanbul-lib-coverage: 3.2.2 @@ -15681,9 +15990,9 @@ snapshots: make-dir: 4.0.0 supports-color: 7.2.0 - istanbul-lib-source-maps@4.0.1: + istanbul-lib-source-maps@4.0.1(supports-color@8.1.1): dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) istanbul-lib-coverage: 3.2.2 source-map: 0.6.1 transitivePeerDependencies: @@ -15710,10 +16019,10 @@ snapshots: jest-util: 29.7.0 p-limit: 3.1.0 - jest-circus@29.7.0: + jest-circus@29.7.0(supports-color@8.1.1): dependencies: '@jest/environment': 29.7.0 - '@jest/expect': 29.7.0 + '@jest/expect': 29.7.0(supports-color@8.1.1) '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 '@types/node': 24.12.4 @@ -15724,8 +16033,8 @@ snapshots: jest-each: 29.7.0 jest-matcher-utils: 29.7.0 jest-message-util: 29.7.0 - jest-runtime: 29.7.0 - jest-snapshot: 29.7.0 + jest-runtime: 29.7.0(supports-color@8.1.1) + jest-snapshot: 29.7.0(supports-color@8.1.1) jest-util: 29.7.0 p-limit: 3.1.0 pretty-format: 29.7.0 @@ -15736,16 +16045,16 @@ snapshots: - babel-plugin-macros - supports-color - jest-cli@29.7.0(@types/node@24.12.4): + jest-cli@29.7.0(@types/node@24.12.4)(supports-color@8.1.1): dependencies: - '@jest/core': 29.7.0 + '@jest/core': 29.7.0(supports-color@8.1.1) '@jest/test-result': 29.7.0 '@jest/types': 29.6.3 chalk: 4.1.2 - create-jest: 29.7.0(@types/node@24.12.4) + create-jest: 29.7.0(@types/node@24.12.4)(supports-color@8.1.1) exit: 0.1.2 import-local: 3.2.0 - jest-config: 29.7.0(@types/node@24.12.4) + jest-config: 29.7.0(@types/node@24.12.4)(supports-color@8.1.1) jest-util: 29.7.0 jest-validate: 29.7.0 yargs: 17.7.2 @@ -15755,23 +16064,23 @@ snapshots: - supports-color - ts-node - jest-config@29.7.0(@types/node@24.12.4): + jest-config@29.7.0(@types/node@24.12.4)(supports-color@8.1.1): dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@jest/test-sequencer': 29.7.0 '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.29.0) + babel-jest: 29.7.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) chalk: 4.1.2 ci-info: 3.9.0 deepmerge: 4.3.1 glob: 7.2.3 graceful-fs: 4.2.11 - jest-circus: 29.7.0 + jest-circus: 29.7.0(supports-color@8.1.1) jest-environment-node: 29.7.0 jest-get-type: 29.6.3 jest-regex-util: 29.6.3 jest-resolve: 29.7.0 - jest-runner: 29.7.0 + jest-runner: 29.7.0(supports-color@8.1.1) jest-util: 29.7.0 jest-validate: 29.7.0 micromatch: 4.0.8 @@ -15811,7 +16120,7 @@ snapshots: jest-util: 29.7.0 pretty-format: 29.7.0 - jest-environment-jsdom@29.7.0: + jest-environment-jsdom@29.7.0(supports-color@8.1.1): dependencies: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 @@ -15820,7 +16129,7 @@ snapshots: '@types/node': 24.12.4 jest-mock: 29.7.0 jest-util: 29.7.0 - jsdom: 20.0.3 + jsdom: 20.0.3(supports-color@8.1.1) transitivePeerDependencies: - bufferutil - supports-color @@ -15835,21 +16144,21 @@ snapshots: jest-mock: 29.7.0 jest-util: 29.7.0 - jest-expo@54.0.17(@babel/core@7.29.0)(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(jest@29.7.0(@types/node@24.12.4))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + jest-expo@54.0.17(ff9d3150c2730c411fa3a2f331cc40ee): dependencies: - '@expo/config': 12.0.13 + '@expo/config': 12.0.13(supports-color@8.1.1) '@expo/json-file': 10.0.14 '@jest/create-cache-key-function': 29.7.0 - '@jest/globals': 29.7.0 - babel-jest: 29.7.0(@babel/core@7.29.0) - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) - jest-environment-jsdom: 29.7.0 - jest-snapshot: 29.7.0 + '@jest/globals': 29.7.0(supports-color@8.1.1) + babel-jest: 29.7.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) + jest-environment-jsdom: 29.7.0(supports-color@8.1.1) + jest-snapshot: 29.7.0(supports-color@8.1.1) jest-watch-select-projects: 2.0.0 - jest-watch-typeahead: 2.2.1(jest@29.7.0(@types/node@24.12.4)) + jest-watch-typeahead: 2.2.1(jest@29.7.0(@types/node@24.12.4)(supports-color@8.1.1)) json5: 2.2.3 lodash: 4.18.1 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) react-test-renderer: 19.1.0(react@19.1.0) server-only: 0.0.1 stacktrace-js: 2.0.2 @@ -15951,10 +16260,10 @@ snapshots: jest-regex-util@30.4.0: {} - jest-resolve-dependencies@29.7.0: + jest-resolve-dependencies@29.7.0(supports-color@8.1.1): dependencies: jest-regex-util: 29.6.3 - jest-snapshot: 29.7.0 + jest-snapshot: 29.7.0(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -15970,12 +16279,12 @@ snapshots: resolve.exports: 2.0.3 slash: 3.0.0 - jest-runner@29.7.0: + jest-runner@29.7.0(supports-color@8.1.1): dependencies: '@jest/console': 29.7.0 '@jest/environment': 29.7.0 '@jest/test-result': 29.7.0 - '@jest/transform': 29.7.0 + '@jest/transform': 29.7.0(supports-color@8.1.1) '@jest/types': 29.6.3 '@types/node': 24.12.4 chalk: 4.1.2 @@ -15987,7 +16296,7 @@ snapshots: jest-leak-detector: 29.7.0 jest-message-util: 29.7.0 jest-resolve: 29.7.0 - jest-runtime: 29.7.0 + jest-runtime: 29.7.0(supports-color@8.1.1) jest-util: 29.7.0 jest-watcher: 29.7.0 jest-worker: 29.7.0 @@ -15996,14 +16305,14 @@ snapshots: transitivePeerDependencies: - supports-color - jest-runtime@29.7.0: + jest-runtime@29.7.0(supports-color@8.1.1): dependencies: '@jest/environment': 29.7.0 '@jest/fake-timers': 29.7.0 - '@jest/globals': 29.7.0 + '@jest/globals': 29.7.0(supports-color@8.1.1) '@jest/source-map': 29.6.3 '@jest/test-result': 29.7.0 - '@jest/transform': 29.7.0 + '@jest/transform': 29.7.0(supports-color@8.1.1) '@jest/types': 29.6.3 '@types/node': 24.12.4 chalk: 4.1.2 @@ -16016,24 +16325,24 @@ snapshots: jest-mock: 29.7.0 jest-regex-util: 29.6.3 jest-resolve: 29.7.0 - jest-snapshot: 29.7.0 + jest-snapshot: 29.7.0(supports-color@8.1.1) jest-util: 29.7.0 slash: 3.0.0 strip-bom: 4.0.0 transitivePeerDependencies: - supports-color - jest-snapshot@29.7.0: + jest-snapshot@29.7.0(supports-color@8.1.1): dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/generator': 7.29.1 - '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1)) '@babel/types': 7.29.0 '@jest/expect-utils': 29.7.0 - '@jest/transform': 29.7.0 + '@jest/transform': 29.7.0(supports-color@8.1.1) '@jest/types': 29.6.3 - babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0) + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0(supports-color@8.1.1)) chalk: 4.1.2 expect: 29.7.0 graceful-fs: 4.2.11 @@ -16081,11 +16390,11 @@ snapshots: chalk: 3.0.0 prompts: 2.4.2 - jest-watch-typeahead@2.2.1(jest@29.7.0(@types/node@24.12.4)): + jest-watch-typeahead@2.2.1(jest@29.7.0(@types/node@24.12.4)(supports-color@8.1.1)): dependencies: ansi-escapes: 6.2.1 chalk: 4.1.2 - jest: 29.7.0(@types/node@24.12.4) + jest: 29.7.0(@types/node@24.12.4)(supports-color@8.1.1) jest-regex-util: 29.6.3 jest-watcher: 29.7.0 slash: 5.1.0 @@ -16116,12 +16425,12 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jest@29.7.0(@types/node@24.12.4): + jest@29.7.0(@types/node@24.12.4)(supports-color@8.1.1): dependencies: - '@jest/core': 29.7.0 + '@jest/core': 29.7.0(supports-color@8.1.1) '@jest/types': 29.6.3 import-local: 3.2.0 - jest-cli: 29.7.0(@types/node@24.12.4) + jest-cli: 29.7.0(@types/node@24.12.4)(supports-color@8.1.1) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -16149,7 +16458,7 @@ snapshots: jsc-safe-url@0.2.4: {} - jsdom@20.0.3: + jsdom@20.0.3(supports-color@8.1.1): dependencies: abab: 2.0.6 acorn: 8.16.0 @@ -16162,8 +16471,8 @@ snapshots: escodegen: 2.1.0 form-data: 4.0.5 html-encoding-sniffer: 3.0.0 - http-proxy-agent: 5.0.0 - https-proxy-agent: 5.0.1 + http-proxy-agent: 5.0.0(supports-color@8.1.1) + https-proxy-agent: 5.0.1(supports-color@8.1.1) is-potential-custom-element-name: 1.0.1 nwsapi: 2.2.23 parse5: 7.3.0 @@ -16184,6 +16493,8 @@ snapshots: jsesc@3.1.0: {} + json-buffer@3.0.1: {} + json-parse-even-better-errors@2.3.1: {} json-schema-ref-resolver@3.0.0: @@ -16194,6 +16505,8 @@ snapshots: json-schema-traverse@1.0.0: {} + json-stable-stringify-without-jsonify@1.0.1: {} + json5@2.2.3: {} jsonfile@6.2.1: @@ -16211,6 +16524,10 @@ snapshots: jwt-decode@4.0.0: {} + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + kleur@3.0.3: {} lan-network@0.2.1: {} @@ -16226,11 +16543,16 @@ snapshots: leven@3.1.0: {} + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + libphonenumber-js@1.13.1: {} - lighthouse-logger@1.4.2: + lighthouse-logger@1.4.2(supports-color@8.1.1): dependencies: - debug: 2.6.9 + debug: 2.6.9(supports-color@8.1.1) marky: 1.3.0 transitivePeerDependencies: - supports-color @@ -16336,6 +16658,8 @@ snapshots: lodash.memoize@4.1.2: {} + lodash.merge@4.6.2: {} + lodash.shuffle@4.2.0: {} lodash.throttle@4.1.1: {} @@ -16449,18 +16773,18 @@ snapshots: methods@1.1.2: {} - metro-babel-transformer@0.83.3: + metro-babel-transformer@0.83.3(supports-color@8.1.1): dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) flow-enums-runtime: 0.0.6 hermes-parser: 0.32.0 nullthrows: 1.1.1 transitivePeerDependencies: - supports-color - metro-babel-transformer@0.84.4: + metro-babel-transformer@0.84.4(supports-color@8.1.1): dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) flow-enums-runtime: 0.0.6 hermes-parser: 0.35.0 metro-cache-key: 0.84.4 @@ -16476,31 +16800,31 @@ snapshots: dependencies: flow-enums-runtime: 0.0.6 - metro-cache@0.83.3: + metro-cache@0.83.3(supports-color@8.1.1): dependencies: exponential-backoff: 3.1.3 flow-enums-runtime: 0.0.6 - https-proxy-agent: 7.0.6 + https-proxy-agent: 7.0.6(supports-color@8.1.1) metro-core: 0.83.3 transitivePeerDependencies: - supports-color - metro-cache@0.84.4: + metro-cache@0.84.4(supports-color@8.1.1): dependencies: exponential-backoff: 3.1.3 flow-enums-runtime: 0.0.6 - https-proxy-agent: 7.0.6 + https-proxy-agent: 7.0.6(supports-color@8.1.1) metro-core: 0.84.4 transitivePeerDependencies: - supports-color - metro-config@0.83.3: + metro-config@0.83.3(supports-color@8.1.1): dependencies: - connect: 3.7.0 + connect: 3.7.0(supports-color@8.1.1) flow-enums-runtime: 0.0.6 jest-validate: 29.7.0 - metro: 0.83.3 - metro-cache: 0.83.3 + metro: 0.83.3(supports-color@8.1.1) + metro-cache: 0.83.3(supports-color@8.1.1) metro-core: 0.83.3 metro-runtime: 0.83.3 yaml: 2.9.0 @@ -16509,13 +16833,13 @@ snapshots: - supports-color - utf-8-validate - metro-config@0.84.4: + metro-config@0.84.4(supports-color@8.1.1): dependencies: - connect: 3.7.0 + connect: 3.7.0(supports-color@8.1.1) flow-enums-runtime: 0.0.6 jest-validate: 29.7.0 - metro: 0.84.4 - metro-cache: 0.84.4 + metro: 0.84.4(supports-color@8.1.1) + metro-cache: 0.84.4(supports-color@8.1.1) metro-core: 0.84.4 metro-runtime: 0.84.4 yaml: 2.9.0 @@ -16536,9 +16860,9 @@ snapshots: lodash.throttle: 4.1.1 metro-resolver: 0.84.4 - metro-file-map@0.83.3: + metro-file-map@0.83.3(supports-color@8.1.1): dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) fb-watchman: 2.0.2 flow-enums-runtime: 0.0.6 graceful-fs: 4.2.11 @@ -16550,9 +16874,9 @@ snapshots: transitivePeerDependencies: - supports-color - metro-file-map@0.84.4: + metro-file-map@0.84.4(supports-color@8.1.1): dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) fb-watchman: 2.0.2 flow-enums-runtime: 0.0.6 graceful-fs: 4.2.11 @@ -16592,14 +16916,14 @@ snapshots: '@babel/runtime': 7.29.2 flow-enums-runtime: 0.0.6 - metro-source-map@0.83.3: + metro-source-map@0.83.3(supports-color@8.1.1): dependencies: - '@babel/traverse': 7.29.0 - '@babel/traverse--for-generate-function-map': '@babel/traverse@7.29.0' + '@babel/traverse': 7.29.0(supports-color@8.1.1) + '@babel/traverse--for-generate-function-map': '@babel/traverse@7.29.0(supports-color@8.1.1)' '@babel/types': 7.29.0 flow-enums-runtime: 0.0.6 invariant: 2.2.4 - metro-symbolicate: 0.83.3 + metro-symbolicate: 0.83.3(supports-color@8.1.1) nullthrows: 1.1.1 ob1: 0.83.3 source-map: 0.5.7 @@ -16607,13 +16931,13 @@ snapshots: transitivePeerDependencies: - supports-color - metro-source-map@0.84.4: + metro-source-map@0.84.4(supports-color@8.1.1): dependencies: - '@babel/traverse': 7.29.0 + '@babel/traverse': 7.29.0(supports-color@8.1.1) '@babel/types': 7.29.0 flow-enums-runtime: 0.0.6 invariant: 2.2.4 - metro-symbolicate: 0.84.4 + metro-symbolicate: 0.84.4(supports-color@8.1.1) nullthrows: 1.1.1 ob1: 0.84.4 source-map: 0.5.7 @@ -16621,104 +16945,104 @@ snapshots: transitivePeerDependencies: - supports-color - metro-symbolicate@0.83.3: + metro-symbolicate@0.83.3(supports-color@8.1.1): dependencies: flow-enums-runtime: 0.0.6 invariant: 2.2.4 - metro-source-map: 0.83.3 + metro-source-map: 0.83.3(supports-color@8.1.1) nullthrows: 1.1.1 source-map: 0.5.7 vlq: 1.0.1 transitivePeerDependencies: - supports-color - metro-symbolicate@0.84.4: + metro-symbolicate@0.84.4(supports-color@8.1.1): dependencies: flow-enums-runtime: 0.0.6 invariant: 2.2.4 - metro-source-map: 0.84.4 + metro-source-map: 0.84.4(supports-color@8.1.1) nullthrows: 1.1.1 source-map: 0.5.7 vlq: 1.0.1 transitivePeerDependencies: - supports-color - metro-transform-plugins@0.83.3: + metro-transform-plugins@0.83.3(supports-color@8.1.1): dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/generator': 7.29.1 '@babel/template': 7.28.6 - '@babel/traverse': 7.29.0 + '@babel/traverse': 7.29.0(supports-color@8.1.1) flow-enums-runtime: 0.0.6 nullthrows: 1.1.1 transitivePeerDependencies: - supports-color - metro-transform-plugins@0.84.4: + metro-transform-plugins@0.84.4(supports-color@8.1.1): dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/generator': 7.29.1 '@babel/template': 7.28.6 - '@babel/traverse': 7.29.0 + '@babel/traverse': 7.29.0(supports-color@8.1.1) flow-enums-runtime: 0.0.6 nullthrows: 1.1.1 transitivePeerDependencies: - supports-color - metro-transform-worker@0.83.3: + metro-transform-worker@0.83.3(supports-color@8.1.1): dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/generator': 7.29.1 '@babel/parser': 7.29.3 '@babel/types': 7.29.0 flow-enums-runtime: 0.0.6 - metro: 0.83.3 - metro-babel-transformer: 0.83.3 - metro-cache: 0.83.3 + metro: 0.83.3(supports-color@8.1.1) + metro-babel-transformer: 0.83.3(supports-color@8.1.1) + metro-cache: 0.83.3(supports-color@8.1.1) metro-cache-key: 0.83.3 metro-minify-terser: 0.83.3 - metro-source-map: 0.83.3 - metro-transform-plugins: 0.83.3 + metro-source-map: 0.83.3(supports-color@8.1.1) + metro-transform-plugins: 0.83.3(supports-color@8.1.1) nullthrows: 1.1.1 transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate - metro-transform-worker@0.84.4: + metro-transform-worker@0.84.4(supports-color@8.1.1): dependencies: - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/generator': 7.29.1 '@babel/parser': 7.29.3 '@babel/types': 7.29.0 flow-enums-runtime: 0.0.6 - metro: 0.84.4 - metro-babel-transformer: 0.84.4 - metro-cache: 0.84.4 + metro: 0.84.4(supports-color@8.1.1) + metro-babel-transformer: 0.84.4(supports-color@8.1.1) + metro-cache: 0.84.4(supports-color@8.1.1) metro-cache-key: 0.84.4 metro-minify-terser: 0.84.4 - metro-source-map: 0.84.4 - metro-transform-plugins: 0.84.4 + metro-source-map: 0.84.4(supports-color@8.1.1) + metro-transform-plugins: 0.84.4(supports-color@8.1.1) nullthrows: 1.1.1 transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate - metro@0.83.3: + metro@0.83.3(supports-color@8.1.1): dependencies: '@babel/code-frame': 7.29.0 - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/generator': 7.29.1 '@babel/parser': 7.29.3 '@babel/template': 7.28.6 - '@babel/traverse': 7.29.0 + '@babel/traverse': 7.29.0(supports-color@8.1.1) '@babel/types': 7.29.0 accepts: 1.3.8 chalk: 4.1.2 ci-info: 2.0.0 - connect: 3.7.0 - debug: 4.4.3 + connect: 3.7.0(supports-color@8.1.1) + debug: 4.4.3(supports-color@8.1.1) error-stack-parser: 2.1.4 flow-enums-runtime: 0.0.6 graceful-fs: 4.2.11 @@ -16728,18 +17052,18 @@ snapshots: jest-worker: 29.7.0 jsc-safe-url: 0.2.4 lodash.throttle: 4.1.1 - metro-babel-transformer: 0.83.3 - metro-cache: 0.83.3 + metro-babel-transformer: 0.83.3(supports-color@8.1.1) + metro-cache: 0.83.3(supports-color@8.1.1) metro-cache-key: 0.83.3 - metro-config: 0.83.3 + metro-config: 0.83.3(supports-color@8.1.1) metro-core: 0.83.3 - metro-file-map: 0.83.3 + metro-file-map: 0.83.3(supports-color@8.1.1) metro-resolver: 0.83.3 metro-runtime: 0.83.3 - metro-source-map: 0.83.3 - metro-symbolicate: 0.83.3 - metro-transform-plugins: 0.83.3 - metro-transform-worker: 0.83.3 + metro-source-map: 0.83.3(supports-color@8.1.1) + metro-symbolicate: 0.83.3(supports-color@8.1.1) + metro-transform-plugins: 0.83.3(supports-color@8.1.1) + metro-transform-worker: 0.83.3(supports-color@8.1.1) mime-types: 2.1.35 nullthrows: 1.1.1 serialize-error: 2.1.0 @@ -16752,19 +17076,19 @@ snapshots: - supports-color - utf-8-validate - metro@0.84.4: + metro@0.84.4(supports-color@8.1.1): dependencies: '@babel/code-frame': 7.29.0 - '@babel/core': 7.29.0 + '@babel/core': 7.29.0(supports-color@8.1.1) '@babel/generator': 7.29.1 '@babel/parser': 7.29.3 '@babel/template': 7.28.6 - '@babel/traverse': 7.29.0 + '@babel/traverse': 7.29.0(supports-color@8.1.1) '@babel/types': 7.29.0 accepts: 2.0.0 ci-info: 2.0.0 - connect: 3.7.0 - debug: 4.4.3 + connect: 3.7.0(supports-color@8.1.1) + debug: 4.4.3(supports-color@8.1.1) error-stack-parser: 2.1.4 flow-enums-runtime: 0.0.6 graceful-fs: 4.2.11 @@ -16774,18 +17098,18 @@ snapshots: jest-worker: 29.7.0 jsc-safe-url: 0.2.4 lodash.throttle: 4.1.1 - metro-babel-transformer: 0.84.4 - metro-cache: 0.84.4 + metro-babel-transformer: 0.84.4(supports-color@8.1.1) + metro-cache: 0.84.4(supports-color@8.1.1) metro-cache-key: 0.84.4 - metro-config: 0.84.4 + metro-config: 0.84.4(supports-color@8.1.1) metro-core: 0.84.4 - metro-file-map: 0.84.4 + metro-file-map: 0.84.4(supports-color@8.1.1) metro-resolver: 0.84.4 metro-runtime: 0.84.4 - metro-source-map: 0.84.4 - metro-symbolicate: 0.84.4 - metro-transform-plugins: 0.84.4 - metro-transform-worker: 0.84.4 + metro-source-map: 0.84.4(supports-color@8.1.1) + metro-symbolicate: 0.84.4(supports-color@8.1.1) + metro-transform-plugins: 0.84.4(supports-color@8.1.1) + metro-transform-worker: 0.84.4(supports-color@8.1.1) mime-types: 3.0.2 nullthrows: 1.1.1 serialize-error: 2.1.0 @@ -16825,11 +17149,11 @@ snapshots: min-indent@1.0.1: {} - mini-css-extract-plugin@2.10.2(webpack@5.106.2(postcss@8.5.14)): + mini-css-extract-plugin@2.10.2(webpack@5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14)): dependencies: schema-utils: 4.3.3 tapable: 2.3.3 - webpack: 5.106.2(postcss@8.5.14) + webpack: 5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14) minimalistic-assert@1.0.1: {} @@ -17023,6 +17347,15 @@ snapshots: opener@1.5.2: {} + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + ora@3.4.0: dependencies: chalk: 2.4.2 @@ -17407,6 +17740,8 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + prelude-ls@1.2.1: {} + prettier@3.8.3: {} pretty-bytes@5.6.0: {} @@ -17729,123 +18064,123 @@ snapshots: react: 19.1.0 react-is: 19.2.6 - react-native-compressor@1.13.0(patch_hash=58379dfaace6ced8590cb341c77f2ca8099dfa8f7df6297032ec51de767a9925)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + react-native-compressor@1.13.0(patch_hash=58379dfaace6ced8590cb341c77f2ca8099dfa8f7df6297032ec51de767a9925)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0): dependencies: react: 19.1.0 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) - react-native-date-picker@5.0.13(patch_hash=92943fb79d17d7342a29bbb12b0d8ee3cf6f7bca12ed322dc8d124a3e9fb75bd)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + react-native-date-picker@5.0.13(patch_hash=92943fb79d17d7342a29bbb12b0d8ee3cf6f7bca12ed322dc8d124a3e9fb75bd)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0): dependencies: react: 19.1.0 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) - react-native-device-attest@0.1.6(expo@54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + react-native-device-attest@0.1.6(83e0f64f4281e4798012214ad5b2d72d): dependencies: - expo: 54.0.35(@babel/core@7.29.0)(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + expo: 54.0.35(@babel/core@7.29.0(supports-color@8.1.1))(react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) react: 19.1.0 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) react-native-dotenv@3.4.11(@babel/runtime@7.29.2): dependencies: '@babel/runtime': 7.29.2 dotenv: 16.6.1 - react-native-drawer-layout@4.2.3(patch_hash=74f2c043cc22ab87054f219e7c7373a509b779b18bfa79d27e7d051d73355130)(f8669ab0fde0772e831a7516fba74dd4): + react-native-drawer-layout@4.2.3(patch_hash=74f2c043cc22ab87054f219e7c7373a509b779b18bfa79d27e7d051d73355130)(80d195d8b17891585aee78a1fb66460a): dependencies: color: 4.2.3 react: 19.1.0 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) - react-native-gesture-handler: 2.30.1(patch_hash=39b9e51b3977fae8ff61764b8c32934fe3fbddaf21587b4661fd73b7c6dd325b)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) - react-native-reanimated: 4.3.2(patch_hash=ab1c095bc473ec5bfd9ead76e870c566660e355e11c5078cfdeb35922d7b2f05)(react-native-worklets@0.8.3(patch_hash=f7263a59ae1c0170af8dfe9a5704cb374d29214a430b4b2841f9b7886c9ba8ad)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) + react-native-gesture-handler: 2.30.1(patch_hash=39b9e51b3977fae8ff61764b8c32934fe3fbddaf21587b4661fd73b7c6dd325b)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0) + react-native-reanimated: 4.3.2(patch_hash=ab1c095bc473ec5bfd9ead76e870c566660e355e11c5078cfdeb35922d7b2f05)(react-native-worklets@0.8.3(patch_hash=f7263a59ae1c0170af8dfe9a5704cb374d29214a430b4b2841f9b7886c9ba8ad)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0) use-latest-callback: 0.2.6(react@19.1.0) - react-native-edge-to-edge@1.8.1(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + react-native-edge-to-edge@1.8.1(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0): dependencies: react: 19.1.0 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) - react-native-gesture-handler@2.30.1(patch_hash=39b9e51b3977fae8ff61764b8c32934fe3fbddaf21587b4661fd73b7c6dd325b)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + react-native-gesture-handler@2.30.1(patch_hash=39b9e51b3977fae8ff61764b8c32934fe3fbddaf21587b4661fd73b7c6dd325b)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0): dependencies: '@egjs/hammerjs': 2.0.17 hoist-non-react-statics: 3.3.2 invariant: 2.2.4 react: 19.1.0 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) - react-native-is-edge-to-edge@1.3.1(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + react-native-is-edge-to-edge@1.3.1(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0): dependencies: react: 19.1.0 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) - ? react-native-keyboard-controller@1.21.8(patch_hash=ac52bf7502ff3ad34a8594b5e1a916b96bf87a2523b6abc87c31f03607d52271)(react-native-reanimated@4.3.2(patch_hash=ab1c095bc473ec5bfd9ead76e870c566660e355e11c5078cfdeb35922d7b2f05)(react-native-worklets@0.8.3(patch_hash=f7263a59ae1c0170af8dfe9a5704cb374d29214a430b4b2841f9b7886c9ba8ad)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) - : dependencies: - react: 19.1.0 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) - react-native-is-edge-to-edge: 1.3.1(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) - react-native-reanimated: 4.3.2(patch_hash=ab1c095bc473ec5bfd9ead76e870c566660e355e11c5078cfdeb35922d7b2f05)(react-native-worklets@0.8.3(patch_hash=f7263a59ae1c0170af8dfe9a5704cb374d29214a430b4b2841f9b7886c9ba8ad)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) - - react-native-mmkv@3.3.3(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + react-native-keyboard-controller@1.21.8(patch_hash=ac52bf7502ff3ad34a8594b5e1a916b96bf87a2523b6abc87c31f03607d52271)(a7f993fe7a53ab427398397086882821): dependencies: react: 19.1.0 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) + react-native-is-edge-to-edge: 1.3.1(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0) + react-native-reanimated: 4.3.2(patch_hash=ab1c095bc473ec5bfd9ead76e870c566660e355e11c5078cfdeb35922d7b2f05)(react-native-worklets@0.8.3(patch_hash=f7263a59ae1c0170af8dfe9a5704cb374d29214a430b4b2841f9b7886c9ba8ad)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0) - react-native-pager-view@6.8.0(patch_hash=c8316acd33c9aef6531e8c272c2bfab7bd02af1255969eeaab2bad5ab48531cc)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + react-native-mmkv@3.3.3(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0): dependencies: react: 19.1.0 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) - react-native-progress@5.0.1(react-native-svg@15.12.1(patch_hash=ae309e9542214f5ae54935e253dedd236765feedac7be343f81d532d7fe09310)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)): + react-native-pager-view@6.8.0(patch_hash=c8316acd33c9aef6531e8c272c2bfab7bd02af1255969eeaab2bad5ab48531cc)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0): + dependencies: + react: 19.1.0 + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) + + react-native-progress@5.0.1(react-native-svg@15.12.1(patch_hash=ae309e9542214f5ae54935e253dedd236765feedac7be343f81d532d7fe09310)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)): dependencies: prop-types: 15.8.1 - react-native-svg: 15.12.1(patch_hash=ae309e9542214f5ae54935e253dedd236765feedac7be343f81d532d7fe09310)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + react-native-svg: 15.12.1(patch_hash=ae309e9542214f5ae54935e253dedd236765feedac7be343f81d532d7fe09310)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0) - react-native-qrcode-styled@0.3.3(react-native-svg@15.12.1(patch_hash=ae309e9542214f5ae54935e253dedd236765feedac7be343f81d532d7fe09310)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + react-native-qrcode-styled@0.3.3(react-native-svg@15.12.1(patch_hash=ae309e9542214f5ae54935e253dedd236765feedac7be343f81d532d7fe09310)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0): dependencies: qrcode: 1.5.4 react: 19.1.0 react-fast-compare: 3.2.2 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) - react-native-svg: 15.12.1(patch_hash=ae309e9542214f5ae54935e253dedd236765feedac7be343f81d532d7fe09310)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) + react-native-svg: 15.12.1(patch_hash=ae309e9542214f5ae54935e253dedd236765feedac7be343f81d532d7fe09310)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0) - react-native-reanimated@4.3.2(patch_hash=ab1c095bc473ec5bfd9ead76e870c566660e355e11c5078cfdeb35922d7b2f05)(react-native-worklets@0.8.3(patch_hash=f7263a59ae1c0170af8dfe9a5704cb374d29214a430b4b2841f9b7886c9ba8ad)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + react-native-reanimated@4.3.2(patch_hash=ab1c095bc473ec5bfd9ead76e870c566660e355e11c5078cfdeb35922d7b2f05)(react-native-worklets@0.8.3(patch_hash=f7263a59ae1c0170af8dfe9a5704cb374d29214a430b4b2841f9b7886c9ba8ad)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0): dependencies: react: 19.1.0 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) - react-native-is-edge-to-edge: 1.3.1(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) - react-native-worklets: 0.8.3(patch_hash=f7263a59ae1c0170af8dfe9a5704cb374d29214a430b4b2841f9b7886c9ba8ad)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) + react-native-is-edge-to-edge: 1.3.1(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0) + react-native-worklets: 0.8.3(patch_hash=f7263a59ae1c0170af8dfe9a5704cb374d29214a430b4b2841f9b7886c9ba8ad)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) semver: 7.8.0 - react-native-safe-area-context@5.6.2(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + react-native-safe-area-context@5.6.2(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0): dependencies: react: 19.1.0 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) - react-native-screens@4.24.0(patch_hash=74a74b489f8074a1974c2792c9c97aec30d8e251669aa37fc5c99993bb4a19e6)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + react-native-screens@4.24.0(patch_hash=74a74b489f8074a1974c2792c9c97aec30d8e251669aa37fc5c99993bb4a19e6)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0): dependencies: react: 19.1.0 react-freeze: 1.0.4(react@19.1.0) - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) warn-once: 0.1.1 - react-native-svg@15.12.1(patch_hash=ae309e9542214f5ae54935e253dedd236765feedac7be343f81d532d7fe09310)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + react-native-svg@15.12.1(patch_hash=ae309e9542214f5ae54935e253dedd236765feedac7be343f81d532d7fe09310)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0): dependencies: css-select: 5.2.2 css-tree: 1.1.3 react: 19.1.0 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) warn-once: 0.1.1 react-native-uuid@2.0.4: {} - react-native-view-shot@4.0.3(patch_hash=a2457dc30a82cc8c21f371a6f860d9396d450a2a1b4265b1a4ee13bdb24d3268)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + react-native-view-shot@4.0.3(patch_hash=a2457dc30a82cc8c21f371a6f860d9396d450a2a1b4265b1a4ee13bdb24d3268)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0): dependencies: html2canvas: 1.4.1 react: 19.1.0 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) - react-native-web-webview@1.0.2(file-loader@6.2.0(webpack@5.106.2(postcss@8.5.14)))(react-native-web@0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)): + react-native-web-webview@1.0.2(file-loader@6.2.0(webpack@5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14)))(react-native-web@0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)): dependencies: - file-loader: 6.2.0(webpack@5.106.2(postcss@8.5.14)) + file-loader: 6.2.0(webpack@5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14)) qs: 6.15.1 react-native-web: 0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -17864,47 +18199,47 @@ snapshots: transitivePeerDependencies: - encoding - react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + react-native-webview@13.15.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0): dependencies: escape-string-regexp: 4.0.0 invariant: 2.2.4 react: 19.1.0 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) - react-native-worklets@0.8.3(patch_hash=f7263a59ae1c0170af8dfe9a5704cb374d29214a430b4b2841f9b7886c9ba8ad)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0): + react-native-worklets@0.8.3(patch_hash=f7263a59ae1c0170af8dfe9a5704cb374d29214a430b4b2841f9b7886c9ba8ad)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1): dependencies: - '@babel/core': 7.29.0 - '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-class-properties': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-classes': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-nullish-coalescing-operator': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-optional-chaining': 7.28.6(@babel/core@7.29.0) - '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-template-literals': 7.27.1(@babel/core@7.29.0) - '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.29.0) - '@babel/preset-typescript': 7.28.5(@babel/core@7.29.0) - '@react-native/metro-config': 0.86.0(@babel/core@7.29.0) + '@babel/core': 7.29.0(supports-color@8.1.1) + '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-class-properties': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-classes': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-nullish-coalescing-operator': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-optional-chaining': 7.28.6(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@babel/plugin-transform-shorthand-properties': 7.27.1(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-template-literals': 7.27.1(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/plugin-transform-unicode-regex': 7.27.1(@babel/core@7.29.0(supports-color@8.1.1)) + '@babel/preset-typescript': 7.28.5(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) + '@react-native/metro-config': 0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) convert-source-map: 2.0.0 react: 19.1.0 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) semver: 7.8.0 transitivePeerDependencies: - supports-color - react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0): + react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1): dependencies: '@jest/create-cache-key-function': 29.7.0 '@react-native/assets-registry': 0.81.5 - '@react-native/codegen': 0.81.5(@babel/core@7.29.0) - '@react-native/community-cli-plugin': 0.81.5(@react-native/metro-config@0.86.0(@babel/core@7.29.0)) + '@react-native/codegen': 0.81.5(@babel/core@7.29.0(supports-color@8.1.1)) + '@react-native/community-cli-plugin': 0.81.5(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(supports-color@8.1.1) '@react-native/gradle-plugin': 0.81.5 '@react-native/js-polyfills': 0.81.5 '@react-native/normalize-colors': 0.81.5 - '@react-native/virtualized-lists': 0.81.5(@types/react@19.1.17)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + '@react-native/virtualized-lists': 0.81.5(@types/react@19.1.17)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0) abort-controller: 3.0.0 anser: 1.4.10 ansi-regex: 5.0.1 - babel-jest: 29.7.0(@babel/core@7.29.0) + babel-jest: 29.7.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1) babel-plugin-syntax-hermes-parser: 0.29.1 base64-js: 1.5.1 commander: 12.1.0 @@ -17914,7 +18249,7 @@ snapshots: jest-environment-node: 29.7.0 memoize-one: 5.2.1 metro-runtime: 0.83.3 - metro-source-map: 0.83.3 + metro-source-map: 0.83.3(supports-color@8.1.1) nullthrows: 1.1.1 pretty-format: 29.7.0 promise: 8.3.0 @@ -18226,9 +18561,9 @@ snapshots: semver@7.8.0: {} - send@0.19.2: + send@0.19.2(supports-color@8.1.1): dependencies: - debug: 2.6.9 + debug: 2.6.9(supports-color@8.1.1) depd: 2.0.0 destroy: 1.2.0 encodeurl: 2.0.0 @@ -18250,11 +18585,11 @@ snapshots: dependencies: randombytes: 2.1.0 - serve-index@1.9.2: + serve-index@1.9.2(supports-color@8.1.1): dependencies: accepts: 1.3.8 batch: 0.6.1 - debug: 2.6.9 + debug: 2.6.9(supports-color@8.1.1) escape-html: 1.0.3 http-errors: 1.8.1 mime-types: 2.1.35 @@ -18262,12 +18597,12 @@ snapshots: transitivePeerDependencies: - supports-color - serve-static@1.16.3: + serve-static@1.16.3(supports-color@8.1.1): dependencies: encodeurl: 2.0.0 escape-html: 1.0.3 parseurl: 1.3.3 - send: 0.19.2 + send: 0.19.2(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -18392,16 +18727,16 @@ snapshots: uuid: 8.3.2 websocket-driver: 0.7.4 - sonner-native@0.26.4(c538f2bb3fa64bd80a9a262f75d56c17): + sonner-native@0.26.4(dd52c15d65a2604bcf203dfed917879b): dependencies: react: 19.1.0 - react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0) - react-native-gesture-handler: 2.30.1(patch_hash=39b9e51b3977fae8ff61764b8c32934fe3fbddaf21587b4661fd73b7c6dd325b)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) - react-native-reanimated: 4.3.2(patch_hash=ab1c095bc473ec5bfd9ead76e870c566660e355e11c5078cfdeb35922d7b2f05)(react-native-worklets@0.8.3(patch_hash=f7263a59ae1c0170af8dfe9a5704cb374d29214a430b4b2841f9b7886c9ba8ad)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) - react-native-safe-area-context: 5.6.2(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) - react-native-screens: 4.24.0(patch_hash=74a74b489f8074a1974c2792c9c97aec30d8e251669aa37fc5c99993bb4a19e6)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) - react-native-svg: 15.12.1(patch_hash=ae309e9542214f5ae54935e253dedd236765feedac7be343f81d532d7fe09310)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) - react-native-worklets: 0.8.3(patch_hash=f7263a59ae1c0170af8dfe9a5704cb374d29214a430b4b2841f9b7886c9ba8ad)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0)(@react-native/metro-config@0.86.0(@babel/core@7.29.0))(@types/react@19.1.17)(react@19.1.0))(react@19.1.0) + react-native: 0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1) + react-native-gesture-handler: 2.30.1(patch_hash=39b9e51b3977fae8ff61764b8c32934fe3fbddaf21587b4661fd73b7c6dd325b)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0) + react-native-reanimated: 4.3.2(patch_hash=ab1c095bc473ec5bfd9ead76e870c566660e355e11c5078cfdeb35922d7b2f05)(react-native-worklets@0.8.3(patch_hash=f7263a59ae1c0170af8dfe9a5704cb374d29214a430b4b2841f9b7886c9ba8ad)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0) + react-native-safe-area-context: 5.6.2(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0) + react-native-screens: 4.24.0(patch_hash=74a74b489f8074a1974c2792c9c97aec30d8e251669aa37fc5c99993bb4a19e6)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0) + react-native-svg: 15.12.1(patch_hash=ae309e9542214f5ae54935e253dedd236765feedac7be343f81d532d7fe09310)(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0) + react-native-worklets: 0.8.3(patch_hash=f7263a59ae1c0170af8dfe9a5704cb374d29214a430b4b2841f9b7886c9ba8ad)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) sonner@2.0.7(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: @@ -18412,12 +18747,12 @@ snapshots: source-map-js@1.2.1: {} - source-map-loader@3.0.2(webpack@5.106.2(postcss@8.5.14)): + source-map-loader@3.0.2(webpack@5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14)): dependencies: abab: 2.0.6 iconv-lite: 0.6.3 source-map-js: 1.2.1 - webpack: 5.106.2(postcss@8.5.14) + webpack: 5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14) source-map-support@0.5.13: dependencies: @@ -18437,9 +18772,9 @@ snapshots: source-map@0.7.6: {} - spdy-transport@3.0.0: + spdy-transport@3.0.0(supports-color@8.1.1): dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) detect-node: 2.1.0 hpack.js: 2.1.6 obuf: 1.1.2 @@ -18448,13 +18783,13 @@ snapshots: transitivePeerDependencies: - supports-color - spdy@4.0.2: + spdy@4.0.2(supports-color@8.1.1): dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) handle-thing: 2.0.1 http-deceiver: 1.2.7 select-hose: 2.0.0 - spdy-transport: 3.0.0 + spdy-transport: 3.0.0(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -18594,9 +18929,9 @@ snapshots: structured-headers@0.4.1: {} - style-loader@3.3.4(webpack@5.106.2(postcss@8.5.14)): + style-loader@3.3.4(webpack@5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14)): dependencies: - webpack: 5.106.2(postcss@8.5.14) + webpack: 5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14) stylehacks@5.1.1(postcss@8.5.14): dependencies: @@ -18672,18 +19007,20 @@ snapshots: ansi-escapes: 4.3.2 supports-hyperlinks: 2.3.0 - terser-webpack-plugin@5.6.0(clean-css@5.3.3)(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(postcss@8.5.14)(webpack@5.106.2(postcss@8.5.14)): + terser-webpack-plugin@5.6.0(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14)(webpack@5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14)): dependencies: '@jridgewell/trace-mapping': 0.3.31 jest-worker: 27.5.1 schema-utils: 4.3.3 terser: 5.47.1 - webpack: 5.106.2(postcss@8.5.14) + webpack: 5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14) optionalDependencies: clean-css: 5.3.3 + cssnano: 5.1.15(postcss@8.5.14) csso: 5.0.5 esbuild: 0.25.12 html-minifier-terser: 6.1.0 + lightningcss: 1.32.0 postcss: 8.5.14 terser@5.47.1: @@ -18703,6 +19040,8 @@ snapshots: dependencies: utrie: 1.0.2 + text-table@0.2.0: {} + thenify-all@1.6.0: dependencies: thenify: 3.3.1 @@ -18711,10 +19050,10 @@ snapshots: dependencies: any-promise: 1.3.0 - threads@1.7.0: + threads@1.7.0(supports-color@8.1.1): dependencies: callsites: 3.1.0 - debug: 4.4.3 + debug: 4.4.3(supports-color@8.1.1) is-observable: 2.1.0 observable-fns: 0.6.1 optionalDependencies: @@ -18781,8 +19120,14 @@ snapshots: tslib@2.8.1: {} + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + type-detect@4.0.8: {} + type-fest@0.20.2: {} + type-fest@0.21.3: {} type-fest@0.7.1: {} @@ -19043,16 +19388,16 @@ snapshots: - bufferutil - utf-8-validate - webpack-dev-middleware@5.3.4(webpack@5.106.2(postcss@8.5.14)): + webpack-dev-middleware@5.3.4(webpack@5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14)): dependencies: colorette: 2.0.20 memfs: 3.5.3 mime-types: 2.1.35 range-parser: 1.2.1 schema-utils: 4.3.3 - webpack: 5.106.2(postcss@8.5.14) + webpack: 5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14) - webpack-dev-server@4.15.2(webpack@5.106.2(postcss@8.5.14)): + webpack-dev-server@4.15.2(debug@4.4.3(supports-color@8.1.1))(supports-color@8.1.1)(webpack@5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14)): dependencies: '@types/bonjour': 3.5.13 '@types/connect-history-api-fallback': 1.5.4 @@ -19065,13 +19410,13 @@ snapshots: bonjour-service: 1.3.0 chokidar: 3.6.0 colorette: 2.0.20 - compression: 1.8.1 + compression: 1.8.1(supports-color@8.1.1) connect-history-api-fallback: 2.0.0 default-gateway: 6.0.3 - express: 4.22.2 + express: 4.22.2(supports-color@8.1.1) graceful-fs: 4.2.11 html-entities: 2.6.0 - http-proxy-middleware: 2.0.9(@types/express@4.17.25) + http-proxy-middleware: 2.0.9(@types/express@4.17.25)(debug@4.4.3(supports-color@8.1.1)) ipaddr.js: 2.4.0 launch-editor: 2.13.2 open: 8.4.2 @@ -19079,23 +19424,23 @@ snapshots: rimraf: 3.0.2 schema-utils: 4.3.3 selfsigned: 2.4.1 - serve-index: 1.9.2 + serve-index: 1.9.2(supports-color@8.1.1) sockjs: 0.3.24 - spdy: 4.0.2 - webpack-dev-middleware: 5.3.4(webpack@5.106.2(postcss@8.5.14)) + spdy: 4.0.2(supports-color@8.1.1) + webpack-dev-middleware: 5.3.4(webpack@5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14)) ws: 8.20.1 optionalDependencies: - webpack: 5.106.2(postcss@8.5.14) + webpack: 5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14) transitivePeerDependencies: - bufferutil - debug - supports-color - utf-8-validate - webpack-manifest-plugin@4.1.1(webpack@5.106.2(postcss@8.5.14)): + webpack-manifest-plugin@4.1.1(webpack@5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14)): dependencies: tapable: 2.3.3 - webpack: 5.106.2(postcss@8.5.14) + webpack: 5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14) webpack-sources: 2.3.1 webpack-sources@2.3.1: @@ -19107,7 +19452,7 @@ snapshots: webpack-virtual-modules@0.5.0: {} - webpack@5.106.2(postcss@8.5.14): + webpack@5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14): dependencies: '@types/eslint-scope': 3.7.7 '@types/estree': 1.0.6 @@ -19130,7 +19475,7 @@ snapshots: neo-async: 2.6.2 schema-utils: 4.3.3 tapable: 2.3.3 - terser-webpack-plugin: 5.6.0(clean-css@5.3.3)(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(postcss@8.5.14)(webpack@5.106.2(postcss@8.5.14)) + terser-webpack-plugin: 5.6.0(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14)(webpack@5.106.2(clean-css@5.3.3)(cssnano@5.1.15(postcss@8.5.14))(csso@5.0.5)(esbuild@0.25.12)(html-minifier-terser@6.1.0)(lightningcss@1.32.0)(postcss@8.5.14)) watchpack: 2.5.1 webpack-sources: 3.4.1 transitivePeerDependencies: @@ -19228,6 +19573,8 @@ snapshots: wonka@6.3.6: {} + word-wrap@1.2.5: {} + wrap-ansi@10.0.0: dependencies: ansi-styles: 6.2.3 diff --git a/src/components/dialogs/EmailDialog/data/useAccountEmailState.ts b/src/components/dialogs/EmailDialog/data/useAccountEmailState.ts index f25369f8db..b9edd1629d 100644 --- a/src/components/dialogs/EmailDialog/data/useAccountEmailState.ts +++ b/src/components/dialogs/EmailDialog/data/useAccountEmailState.ts @@ -1,7 +1,7 @@ import {useEffect, useMemo, useState} from 'react' import {useQuery} from '@tanstack/react-query' -import {useAgent, useSessionApi} from '#/state/session' +import {useSession, useSessionApi} from '#/state/session' import {emitEmailVerified} from '#/components/dialogs/EmailDialog/events' export type AccountEmailState = { @@ -12,24 +12,29 @@ export type AccountEmailState = { export const accountEmailStateQueryKey = ['accountEmailState'] as const export function useAccountEmailState() { - const agent = useAgent() + /* + * Read from the account rather than the agent's session: `partialRefreshSession` + * patches the email fields on the account in the reducer (`PasswordSession` + * has no setter for them), so the account is the fresh source of truth. + */ + const {currentAccount} = useSession() const {partialRefreshSession} = useSessionApi() const [prevIsEmailVerified, setPrevEmailIsVerified] = useState( - !!agent.session?.emailConfirmed, + !!currentAccount?.emailConfirmed, ) const state: AccountEmailState = useMemo( () => ({ - isEmailVerified: !!agent.session?.emailConfirmed, - email2FAEnabled: !!agent.session?.emailAuthFactor, + isEmailVerified: !!currentAccount?.emailConfirmed, + email2FAEnabled: !!currentAccount?.emailAuthFactor, }), - [agent.session], + [currentAccount], ) /** * Only here to refetch on focus, when necessary */ useQuery({ - enabled: !!agent.session, + enabled: !!currentAccount, /** * Only refetch if the email verification s incomplete. */ diff --git a/src/lib/strings/__tests__/errors.test.ts b/src/lib/strings/__tests__/errors.test.ts new file mode 100644 index 0000000000..03cdd422c8 --- /dev/null +++ b/src/lib/strings/__tests__/errors.test.ts @@ -0,0 +1,126 @@ +import {XRPCError} from '@atproto/api' +import {LexError, XrpcResponseError} from '@atproto/lex-client' +import {beforeAll, describe, expect, it} from '@jest/globals' +import {i18n} from '@lingui/core' + +import {cleanError} from '../errors' + +/* + * `cleanError` returns translated copy, so a locale has to be active. With no + * catalog loaded, Lingui falls back to the source message. + */ +beforeAll(() => { + i18n.loadAndActivate({locale: 'en', messages: {}}) +}) + +/** + * A stand-in for the method schema an `XrpcResponseError` is built against. + * The generated lexicons are not available yet, and `XrpcResponseError` only + * reads `method` back out for `matchesSchemaErrors()`, which these tests never + * call - so a minimal object is enough. + */ +const method = { + nsid: 'com.atproto.server.createAccount', + type: 'procedure', + errors: ['HandleNotAvailable'], +} as unknown as ConstructorParameters[0] + +/** An error as the lex client builds one from a JSON error response body. */ +function xrpcResponseError( + error: string, + message: string, + status: number = 400, +) { + return new XrpcResponseError(method, new Response(null, {status}), { + encoding: 'application/json', + body: {error, message}, + }) +} + +/** + * An error as the lex client builds one from a response with no XRPC error + * payload: the code is derived from the HTTP status, and the message is a + * generic overview of the response. + */ +function xrpcStatusError(status: number) { + return new XrpcResponseError(method, new Response(null, {status}), undefined) +} + +describe('cleanError', () => { + it('surfaces the clean message of a lex error', () => { + const e = xrpcResponseError('HandleNotAvailable', 'Handle already taken') + // The raw stringification is class- and code-prefixed, so it must not leak. + expect(e.toString()).toBe( + 'XrpcResponseError: [HandleNotAvailable] Handle already taken', + ) + expect(cleanError(e)).toBe('Handle already taken') + }) + + it('falls back to the lexicon code when a lex error has no message', () => { + // `Error` defaults an absent message to the empty string. + const e = new LexError('InvalidRequest') + expect(e.toString()).toBe('LexError: [InvalidRequest] ') + expect(cleanError(e)).toBe('InvalidRequest') + }) + + it('matches the upstream-failure branch on a lex error code', () => { + // 502 maps to the space-free `UpstreamFailure` lexicon code. + const e = xrpcStatusError(502) + expect(e.error).toBe('UpstreamFailure') + expect(cleanError(e)).toBe( + 'The server appears to be experiencing issues. Please try again in a few moments.', + ) + }) + + it('matches the upstream-failure branch on a legacy XRPC error', () => { + const e = new XRPCError(502) + expect(cleanError(e)).toBe( + 'The server appears to be experiencing issues. Please try again in a few moments.', + ) + }) + + it('matches NotEnoughResources on both error shapes', () => { + expect(cleanError(xrpcStatusError(503))).toBe( + 'The server appears to be experiencing issues. Please try again in a few moments.', + ) + expect(cleanError(new XRPCError(503))).toBe( + 'The server appears to be experiencing issues. Please try again in a few moments.', + ) + }) + + it('matches the app-password branch on a lex error message', () => { + const e = xrpcResponseError('InvalidToken', 'Bad token scope') + expect(cleanError(e)).toBe( + 'This feature is not available while using an App Password. Please sign in with your main password.', + ) + }) + + it('matches the network-error branch on a lex error message', () => { + const e = xrpcResponseError('InternalServerError', 'Failed to fetch', 500) + expect(cleanError(e)).toBe( + 'Unable to connect. Please check your internet connection and try again.', + ) + }) + + it('surfaces the authentication-required code of a lex error', () => { + /* + * The lex client derives `AuthenticationRequired` from a 401 with no XRPC + * payload, where `@atproto/api` used the spaced "Authentication Required". + * Neither is special-cased in `cleanError`, so what matters is that the + * class- and code-prefixed stringification does not reach the user. + */ + const e = xrpcStatusError(401) + expect(e.error).toBe('AuthenticationRequired') + expect(cleanError(e)).toBe('Upstream server responded with a 401 error') + }) + + it('strips a leading "Error: " from a plain error', () => { + expect(cleanError(new Error('Something broke'))).toBe('Something broke') + }) + + it('passes strings through', () => { + expect(cleanError('Something broke')).toBe('Something broke') + expect(cleanError('')).toBe('') + expect(cleanError(undefined)).toBe('') + }) +}) diff --git a/src/lib/strings/errors.ts b/src/lib/strings/errors.ts index 11e7c11362..4f793ec195 100644 --- a/src/lib/strings/errors.ts +++ b/src/lib/strings/errors.ts @@ -1,17 +1,51 @@ import {XRPCError} from '@atproto/api' +import {LexError} from '@atproto/lex-client' import {t} from '@lingui/core/macro' +/** + * The text to show the user when no special case applies. + * + * A `LexError` stringifies as `Class: [ErrorCode] message` (for example + * `XrpcResponseError: [HandleNotAvailable] Handle already taken`), so its + * `toString()` is never fit for display. Its `message` is the server's + * human-readable text, so prefer that and fall back to the lexicon code for the + * errors that carry no message. + * + * Everything else keeps the historical behaviour of stringifying and dropping a + * leading `Error: `. + */ +function toDisplayString(e: unknown, str: string): string { + if (e instanceof LexError) { + return e.message || e.error + } + if (str.startsWith('Error: ')) { + return str.slice('Error: '.length) + } + return str +} + export function cleanError(e: unknown): string { if (!e) { return '' } + /* + * Match against the full stringification rather than the display text: it + * contains the error name and the lexicon code as well as the message, so a + * single check covers both error shapes. + */ // oxlint-disable-next-line typescript/no-base-to-string const str = typeof e === 'string' ? e : e.toString() if (isNetworkError(str)) { return t`Unable to connect. Please check your internet connection and try again.` } + /* + * `@atproto/api` names these with spaces ("Upstream Failure"); lexicon error + * codes are space-free ("UpstreamFailure"). Match both while the app throws + * both shapes. + */ if ( str.includes('Upstream Failure') || + str.includes('UpstreamFailure') || str.includes('NotEnoughResources') || str.includes('pipethrough network error') ) { @@ -41,10 +75,7 @@ export function cleanError(e: unknown): string { if (str.includes('Unable to resolve handle')) { return t`Unable to resolve handle` } - if (str.startsWith('Error: ')) { - return str.slice('Error: '.length) - } - return str + return toDisplayString(e, str) } const NETWORK_ERRORS = [ diff --git a/src/screens/Login/LoginForm.tsx b/src/screens/Login/LoginForm.tsx index 27361219ec..ad50179bee 100644 --- a/src/screens/Login/LoginForm.tsx +++ b/src/screens/Login/LoginForm.tsx @@ -1,9 +1,7 @@ import {useRef, useState} from 'react' import {Keyboard, type TextInput, View} from 'react-native' -import { - ComAtprotoServerCreateSession, - type ComAtprotoServerDescribeServer, -} from '@atproto/api' +import {type ComAtprotoServerDescribeServer} from '@atproto/api' +import {LexAuthFactorError} from '@atproto/lex-password-session' import {Trans, useLingui} from '@lingui/react/macro' import {DEFAULT_SERVICE, HITSLOP_10, HITSLOP_20} from '#/lib/constants' @@ -142,10 +140,11 @@ export const LoginForm = ({ } catch (err) { const errMsg = String(err) setIsProcessing(false) - if ( - err instanceof - ComAtprotoServerCreateSession.AuthFactorTokenRequiredError - ) { + /* + * `LexAuthFactorError` is what `PasswordSession.login` throws when the + * server demands an email 2FA token. + */ + if (err instanceof LexAuthFactorError) { setIsAuthFactorTokenNeeded(true) } else { onAttemptFailed() diff --git a/src/screens/Signup/state.ts b/src/screens/Signup/state.ts index 95b3185758..33a5d26a64 100644 --- a/src/screens/Signup/state.ts +++ b/src/screens/Signup/state.ts @@ -1,9 +1,7 @@ import {createContext, useCallback, useContext} from 'react' import {LayoutAnimation} from 'react-native' -import { - ComAtprotoServerCreateAccount, - type ComAtprotoServerDescribeServer, -} from '@atproto/api' +import {type ComAtprotoServerDescribeServer} from '@atproto/api' +import {XrpcResponseError} from '@atproto/lex-client' import {useLingui} from '@lingui/react/macro' import * as EmailValidator from 'email-validator' @@ -260,14 +258,35 @@ export const useSignupContext = () => useContext(SignupContext) * failure is unexpected and should be reported to Sentry. */ function classifyExpectedSignupError(e: unknown): string | undefined { - if (e instanceof ComAtprotoServerCreateAccount.InvalidHandleError) - return 'InvalidHandle' - if (e instanceof ComAtprotoServerCreateAccount.HandleNotAvailableError) - return 'HandleNotAvailable' - if (e instanceof ComAtprotoServerCreateAccount.InvalidPasswordError) - return 'InvalidPassword' - if (e instanceof ComAtprotoServerCreateAccount.UnsupportedDomainError) - return 'UnsupportedDomain' + /* + * TODO: `XrpcResponseError.error` is the open `LexErrorCode` union, so these + * codes are compared as plain strings and a typo silently never matches. + * Once the generated lexicons land, replace this (and every multi-code error + * site) with a shared helper that narrows against the method schema: + * + * function matchXrpcError( + * e: unknown, + * method: Main, + * ): InferMethodError | undefined + * + * switch (matchXrpcError(e, com.atproto.server.createAccount)) { + * case 'InvalidHandle': ... + * } + * + * The return type is the method's declared-errors union, so a typo'd case is + * a compile error, and undeclared codes fall through to `undefined`. The + * helper should also match `e.method.nsid` so a declared code from a + * different call cannot match, mirroring the old per-method error classes. + */ + if (e instanceof XrpcResponseError) { + switch (e.error) { + case 'InvalidHandle': + case 'HandleNotAvailable': + case 'InvalidPassword': + case 'UnsupportedDomain': + return e.error + } + } /* the server sends no typed error for this case */ if (String(e).includes('Email already taken')) return 'EmailTaken' if (isNetworkError(e)) return 'NetworkError' @@ -357,7 +376,7 @@ export function useSubmitSignup() { } catch (err) { const e = err as Error let errMsg = e.toString() - if (e instanceof ComAtprotoServerCreateAccount.InvalidInviteCodeError) { + if (e instanceof XrpcResponseError && e.error === 'InvalidInviteCode') { dispatch({ type: 'setError', value: l`Invite code not accepted. Check that you input it correctly and try again.`, diff --git a/src/state/persisted/index.ts b/src/state/persisted/index.ts index 8c043a3420..0659e7817b 100644 --- a/src/state/persisted/index.ts +++ b/src/state/persisted/index.ts @@ -31,6 +31,18 @@ export function get(key: K): Schema[K] { } get satisfies PersistedApi['get'] +/** + * Native is single-instance: there is no other tab that could have written + * newer data behind our back, so the in-memory `_state` is already the truth + * and a synchronous fresh read is impossible anyway (AsyncStorage is async). + * This mirrors {@link get}; the web implementation is the one that actually + * re-reads the store. + */ +export function readLatest(key: K): Schema[K] { + return _state[key] +} +readLatest satisfies PersistedApi['readLatest'] + export async function write( key: K, value: Schema[K], diff --git a/src/state/persisted/index.web.ts b/src/state/persisted/index.web.ts index 35e796810d..a4c5d1b247 100644 --- a/src/state/persisted/index.web.ts +++ b/src/state/persisted/index.web.ts @@ -39,6 +39,29 @@ export function get(key: K): Schema[K] { } get satisfies PersistedApi['get'] +/** + * Force a fresh synchronous re-read of localStorage and return the requested + * key from it, WITHOUT adopting it as `_state`. + * + * This exists for the cross-tab expiry-rescue path. A frozen tab may not have + * processed a queued broadcast yet, so {@link get} (and persisted's in-memory + * `_state`) can be stale even though another tab already wrote healthy tokens + * to storage. Reading through storage directly here is the only way to see the + * true cross-tab-latest tokens on web. + * + * Crucially we do NOT adopt into `_state`. {@link readFromStorage} memoizes by + * raw string and returns the same object reference for unchanged data, so + * adopting here would make the later queued broadcast/storage event for that + * same write see `next === _state` and suppress its listener notification - + * leaving non-current-account changes (removals, other tokens, metadata) stale + * indefinitely. Leaving `_state` alone lets that queued event still fire. + */ +export function readLatest(key: K): Schema[K] { + const next = readFromStorage() + return next?.[key] ?? _state[key] +} +readLatest satisfies PersistedApi['readLatest'] + // eslint-disable-next-line @typescript-eslint/require-await export async function write( key: K, diff --git a/src/state/persisted/types.ts b/src/state/persisted/types.ts index d1fdfc26cb..4b5a29d938 100644 --- a/src/state/persisted/types.ts +++ b/src/state/persisted/types.ts @@ -3,6 +3,15 @@ import {type Schema} from './schema' export type PersistedApi = { init(): Promise get(key: K): Schema[K] + /** + * Like {@link get}, but on web forces a fresh synchronous re-read of the + * backing store before returning (without adopting it as the in-memory + * state). This exists for the cross-tab expiry-rescue path: a frozen tab may + * not have processed a queued broadcast yet, so {@link get} can be stale + * while another tab has already written healthy tokens to storage. On native + * it is identical to {@link get} (single-instance, no other writer). + */ + readLatest(key: K): Schema[K] write(key: K, value: Schema[K]): Promise onUpdate( key: K, diff --git a/src/state/queries/preferences/index.ts b/src/state/queries/preferences/index.ts index 41adfc9a04..a5803a9c7b 100644 --- a/src/state/queries/preferences/index.ts +++ b/src/state/queries/preferences/index.ts @@ -21,7 +21,7 @@ import { } from '#/state/queries/preferences/types' import {createQueryKey} from '#/state/queries/util' import {useAgent} from '#/state/session' -import {saveLabelers} from '#/state/session/agent-config' +import {saveLabelers} from '#/state/session/moderation' import {useAgeAssurance} from '#/ageAssurance' import {makeAgeRestrictedModerationPrefs} from '#/ageAssurance/util' import {useAnalytics} from '#/analytics' @@ -53,7 +53,7 @@ export function usePreferencesQuery() { const res = await agent.getPreferences() // save to local storage to ensure there are labels on initial requests - void saveLabelers( + saveLabelers( agent.did, res.moderationPrefs.labelers.map(l => l.did), ) diff --git a/src/state/session/__tests__/bridge-agent-test.ts b/src/state/session/__tests__/bridge-agent-test.ts new file mode 100644 index 0000000000..cbd606d12a --- /dev/null +++ b/src/state/session/__tests__/bridge-agent-test.ts @@ -0,0 +1,573 @@ +import { + PasswordSession, + type PasswordSessionOptions, + type SessionData, +} from '@atproto/lex-password-session' +import {beforeEach, describe, expect, it, jest} from '@jest/globals' + +jest.mock('#/state/events', () => ({ + emitNetworkConfirmed: jest.fn(), + emitNetworkLost: jest.fn(), +})) + +jest.mock('jwt-decode', () => ({ + jwtDecode() { + return {scope: 'com.atproto.access'} + }, +})) + +import {BskyAppAgent, PasswordSessionManager} from '../bridge-agent' +import {sessionAccountToSessionData} from '../session-data' +import {type SessionAccount} from '../types' +import { + asFetch, + DID, + DIDDOC_PDS_HOST, + HANDLE, + json, + makeAccount, + makeDidDoc, + makeMockFetch, + type MockFetch, + PDS_HOST, + SERVICE, + urlsOf, +} from './mock-fetch' + +/** + * Build the manager + agent pair under test. + * + * The mock fetch is installed in both places it can be reached from: as the + * inner `PasswordSession`'s fetch (the authenticated path) and, via + * `setFetch`, as the manager's own fetch (the unauthenticated bypass path, + * which would otherwise use the real network-aware fetch). + */ +function setup({ + account = makeAccount(), + didDoc, + pdsUrl, + fetchMock = makeMockFetch(), + sessionOptions, +}: { + account?: SessionAccount + didDoc?: SessionData['didDoc'] + pdsUrl?: string + fetchMock?: MockFetch + sessionOptions?: PasswordSessionOptions +} = {}) { + const data: SessionData = { + ...sessionAccountToSessionData(account), + ...(didDoc ? {didDoc} : {}), + } + const inner = new PasswordSession(data, { + fetch: asFetch(fetchMock), + ...sessionOptions, + }) + const manager = new PasswordSessionManager(inner, { + service: account.service, + pdsUrl, + }) + manager.setFetch(asFetch(fetchMock)) + const agent = new BskyAppAgent(manager) + return {inner, manager, agent, fetchMock} +} + +function setupPublic(fetchMock: MockFetch = makeMockFetch()) { + const manager = new PasswordSessionManager(null, {service: SERVICE}) + manager.setFetch(asFetch(fetchMock)) + return {manager, agent: new BskyAppAgent(manager), fetchMock} +} + +describe('PasswordSessionManager getters', () => { + it('reads live SessionData through .session', () => { + const {agent} = setup() + expect(agent.session?.did).toBe(DID) + expect(agent.session?.handle).toBe(HANDLE) + expect(agent.session?.email).toBe('alice@example.com') + expect(agent.session?.emailConfirmed).toBe(true) + expect(agent.did).toBe(DID) + expect(agent.hasSession).toBe(true) + }) + + it('defaults active to true when the payload omits it', () => { + const {agent} = setup({account: makeAccount({active: undefined})}) + expect(agent.session?.active).toBe(true) + }) + + it('exposes serviceUrl from the constructor service', () => { + const {agent} = setup() + expect(agent.serviceUrl.toString()).toBe('https://bsky.social/') + }) + + it('derives pdsUrl/dispatchUrl from the didDoc', () => { + const {agent} = setup({didDoc: makeDidDoc(PDS_HOST)}) + expect(agent.pdsUrl?.toString()).toBe(`${PDS_HOST}/`) + expect(agent.dispatchUrl.toString()).toBe(`${PDS_HOST}/`) + }) + + it('falls back to the stored pdsUrl when there is no didDoc', () => { + const {agent} = setup({pdsUrl: PDS_HOST}) + expect(agent.pdsUrl?.toString()).toBe(`${PDS_HOST}/`) + expect(agent.dispatchUrl.toString()).toBe(`${PDS_HOST}/`) + }) + + it('prefers the didDoc PDS over the stored pdsUrl', () => { + const {agent} = setup({ + didDoc: makeDidDoc(DIDDOC_PDS_HOST), + pdsUrl: PDS_HOST, + }) + expect(agent.pdsUrl?.toString()).toBe(`${DIDDOC_PDS_HOST}/`) + }) + + it('dispatchUrl falls back to serviceUrl with no PDS at all', () => { + const {agent} = setup() + expect(agent.pdsUrl).toBe(undefined) + expect(agent.dispatchUrl.toString()).toBe('https://bsky.social/') + }) + + it('ignores an unparseable stored pdsUrl', () => { + const {agent} = setup({pdsUrl: 'not a url'}) + expect(agent.pdsUrl).toBe(undefined) + expect(agent.dispatchUrl.toString()).toBe('https://bsky.social/') + }) + + it('matches the inner session on didDocs a strict validator would reject', () => { + /* + * No `id` on the document and a non-canonical service `type`: enough for + * isValidDidDoc/getPdsEndpoint to bail, but PasswordSession still routes + * here, so the bridge must agree or dispatchUrl lies about where requests + * go (and service-auth aud gets minted for the wrong host). + */ + const {agent} = setup({ + didDoc: { + service: [ + { + id: '#atproto_pds', + type: 'SomethingElse', + serviceEndpoint: DIDDOC_PDS_HOST, + }, + ], + }, + pdsUrl: PDS_HOST, + }) + expect(agent.pdsUrl?.toString()).toBe(`${DIDDOC_PDS_HOST}/`) + expect(agent.dispatchUrl.toString()).toBe(`${DIDDOC_PDS_HOST}/`) + }) + + it('falls back to the stored pdsUrl when the didDoc has no PDS service', () => { + const {agent} = setup({ + didDoc: { + id: DID, + service: [ + { + id: '#bsky_notif', + type: 'BskyNotificationService', + serviceEndpoint: DIDDOC_PDS_HOST, + }, + ], + }, + pdsUrl: PDS_HOST, + }) + expect(agent.pdsUrl?.toString()).toBe(`${PDS_HOST}/`) + }) + + it('falls back to the stored pdsUrl when the PDS endpoint does not parse', () => { + const {agent} = setup({ + didDoc: { + id: DID, + service: [ + { + id: '#atproto_pds', + type: 'AtprotoPersonalDataServer', + serviceEndpoint: 'not a url', + }, + ], + }, + pdsUrl: PDS_HOST, + }) + expect(agent.pdsUrl?.toString()).toBe(`${PDS_HOST}/`) + }) +}) + +describe('PasswordSessionManager.session identity', () => { + it('is stable across consecutive reads', () => { + const {agent} = setup() + expect(agent.session).toBe(agent.session) + }) + + it('is a new object after a refresh rotates tokens', async () => { + const {agent} = setup() + const before = agent.session + expect(before?.accessJwt).toBe('access-jwt') + await agent.sessionManager.refreshSession() + const after = agent.session + expect(after).not.toBe(before) + expect(after?.accessJwt).toBe('access-jwt-2') + expect(after).toBe(agent.session) + }) + + it('rejects writes to .session', () => { + const {agent} = setup() + expect(() => { + /* the whole point of the accessor: writes must not silently drift */ + agent.sessionManager.session = agent.session + }).toThrow('read-only') + }) + + it('rejects writes to .pdsUrl', () => { + const {agent} = setup() + expect(() => { + agent.sessionManager.pdsUrl = new URL(PDS_HOST) + }).toThrow('read-only') + }) +}) + +describe('PasswordSessionManager.fetchHandler routing', () => { + it('dispatches to the stored PDS before a refresh, then to the didDoc PDS', async () => { + const {manager, fetchMock} = setup({pdsUrl: PDS_HOST}) + + await manager.fetchHandler('/xrpc/app.bsky.actor.getProfile') + expect(urlsOf(fetchMock).at(-1)).toBe( + `${PDS_HOST}/xrpc/app.bsky.actor.getProfile`, + ) + + /* the refresh response carries a didDoc pointing at a different host */ + await manager.refreshSession() + expect(manager.pdsUrl?.toString()).toBe(`${DIDDOC_PDS_HOST}/`) + + await manager.fetchHandler('/xrpc/app.bsky.actor.getProfile') + expect(urlsOf(fetchMock).at(-1)).toBe( + `${DIDDOC_PDS_HOST}/xrpc/app.bsky.actor.getProfile`, + ) + }) + + it('attaches the session bearer token', async () => { + const seen: Headers[] = [] + const fetchMock = makeMockFetch({ + 'app.bsky.actor.getProfile': (_url, init) => { + seen.push(new Headers(init.headers)) + return json({}) + }, + }) + const {manager} = setup({fetchMock}) + await manager.fetchHandler('/xrpc/app.bsky.actor.getProfile') + expect(seen[0].get('authorization')).toBe('Bearer access-jwt') + }) + + it('bypasses the inner session when authorization is pre-set', async () => { + const seen: Headers[] = [] + const fetchMock = makeMockFetch({ + 'com.atproto.server.describeServer': (_url, init) => { + seen.push(new Headers(init.headers)) + return json({}) + }, + }) + const {manager} = setup({fetchMock, pdsUrl: PDS_HOST}) + + /* + * PasswordSession throws TypeError on a pre-set authorization header, so + * this path must never reach it. + */ + await expect( + manager.fetchHandler('/xrpc/com.atproto.server.describeServer', { + headers: {authorization: 'Bearer caller-supplied'}, + }), + ).resolves.toBeDefined() + + expect(seen.length).toBe(1) + /* the caller's header survives, and there is exactly one of them */ + expect(seen[0].get('authorization')).toBe('Bearer caller-supplied') + expect(urlsOf(fetchMock).at(-1)).toBe( + `${PDS_HOST}/xrpc/com.atproto.server.describeServer`, + ) + }) +}) + +describe('BskyAppAgent namespace requests', () => { + it('carries proxy, labeler and bearer headers to the dispatch host', async () => { + const seen: {url: string; headers: Headers}[] = [] + const fetchMock = makeMockFetch({ + 'app.bsky.actor.getProfile': (url, init) => { + seen.push({url, headers: new Headers(init.headers)}) + return json({did: DID, handle: HANDLE}) + }, + }) + const {agent} = setup({fetchMock, pdsUrl: PDS_HOST}) + agent.configureProxy('did:web:api.bsky.app#bsky_appview') + agent.configureLabelers(['did:plc:custom-labeler']) + + /* + * The request headers (what we assert) are captured by the fetch mock + * before the agent parses the response body. Response-body lexicon + * validation can throw in the jest environment (a multiformats CID mock + * quirk unrelated to the header composition under test), so we ignore any + * parse error here. + */ + await agent.app.bsky.actor.getProfile({actor: HANDLE}).catch(() => {}) + + expect(seen.length).toBe(1) + expect(seen[0].url.startsWith(`${PDS_HOST}/xrpc/`)).toBe(true) + expect(seen[0].headers.get('atproto-proxy')).toBe( + 'did:web:api.bsky.app#bsky_appview', + ) + expect(seen[0].headers.get('atproto-accept-labelers')).toContain( + 'did:plc:custom-labeler', + ) + expect(seen[0].headers.get('authorization')).toBe('Bearer access-jwt') + }) +}) + +describe('PasswordSessionManager.refreshSession', () => { + it('returns an old-shaped XRPC envelope with fresh tokens', async () => { + const {manager, fetchMock} = setup() + const res = await manager.refreshSession() + expect(res.success).toBe(true) + expect(res.data.accessJwt).toBe('access-jwt-2') + expect(res.data.refreshJwt).toBe('refresh-jwt-2') + expect(res.data.did).toBe(DID) + expect(res.data.handle).toBe(HANDLE) + expect( + urlsOf(fetchMock).some(u => + u.includes('com.atproto.server.refreshSession'), + ), + ).toBe(true) + }) + + it('throws when there is no live session', async () => { + const {manager} = setupPublic() + await expect(manager.refreshSession()).rejects.toThrow( + 'No session to refresh', + ) + }) +}) + +describe('PasswordSessionManager.resumeSession', () => { + const staleData = { + accessJwt: 'stale-access', + refreshJwt: 'stale-refresh', + handle: 'stale.test', + did: DID, + active: true, + } + + it('ignores its argument and returns fresh tokens from a refresh', async () => { + const {manager} = setup() + const res = await manager.resumeSession(staleData) + expect(res.data.accessJwt).toBe('access-jwt-2') + expect(res.data.refreshJwt).toBe('refresh-jwt-2') + expect(manager.session?.accessJwt).toBe('access-jwt-2') + }) + + it('is reachable through the agent and does not install the stale data', async () => { + const {agent} = setup() + await agent.resumeSession(staleData) + expect(agent.session?.accessJwt).toBe('access-jwt-2') + expect(agent.session?.handle).toBe(HANDLE) + }) +}) + +describe('PasswordSessionManager unsupported methods', () => { + it('refuses login()', async () => { + const {agent} = setup() + await expect( + agent.login({identifier: HANDLE, password: 'hunter2'}), + ).rejects.toThrow('Not supported on PasswordSessionManager') + }) + + it('refuses createAccount()', async () => { + const {agent} = setup() + await expect( + agent.createAccount({handle: HANDLE, email: 'a@b.c', password: 'x'}), + ).rejects.toThrow('Not supported on PasswordSessionManager') + }) +}) + +describe('PasswordSessionManager destroyed inner session', () => { + it('getters return undefined rather than throwing after logout', async () => { + const {agent, inner} = setup() + await agent.logout() + expect(inner.destroyed).toBe(true) + /* PasswordSession.did/.session throw once destroyed; the bridge must not */ + expect(() => agent.did).not.toThrow() + expect(agent.did).toBe(undefined) + expect(agent.session).toBe(undefined) + expect(agent.hasSession).toBe(false) + expect(agent.pdsUrl).toBe(undefined) + }) + + it('logout() is idempotent', async () => { + const {agent} = setup() + await agent.logout() + await expect(agent.logout()).resolves.toBeUndefined() + }) + + it('fetchHandler stops attaching auth once destroyed', async () => { + const seen: Headers[] = [] + const fetchMock = makeMockFetch({ + 'app.bsky.actor.getProfile': (_url, init) => { + seen.push(new Headers(init.headers)) + return json({}) + }, + }) + const {agent, manager} = setup({fetchMock}) + await agent.logout() + await manager.fetchHandler('/xrpc/app.bsky.actor.getProfile') + expect(seen.length).toBe(1) + expect(seen[0].get('authorization')).toBe(null) + }) +}) + +describe('BskyAppAgent.dispose', () => { + let ctx: ReturnType + + beforeEach(() => { + ctx = setup({pdsUrl: PDS_HOST}) + }) + + it('makes the session read as logged out', () => { + expect(ctx.agent.session).toBeDefined() + ctx.agent.dispose() + expect(ctx.agent.session).toBe(undefined) + expect(ctx.agent.did).toBe(undefined) + expect(ctx.agent.pdsUrl).toBe(undefined) + expect(ctx.agent.hasSession).toBe(false) + }) + + it('routes requests through the plain unauthenticated fetch', async () => { + const seen: Headers[] = [] + const fetchMock = makeMockFetch({ + 'app.bsky.actor.getProfile': (_url, init) => { + seen.push(new Headers(init.headers)) + return json({}) + }, + }) + const {agent, manager} = setup({fetchMock, pdsUrl: PDS_HOST}) + agent.dispose() + await manager.fetchHandler('/xrpc/app.bsky.actor.getProfile') + expect(seen.length).toBe(1) + expect(seen[0].get('authorization')).toBe(null) + /* dispatch falls back to the service, since pdsUrl now reads undefined */ + expect(urlsOf(fetchMock).at(-1)).toBe( + `${SERVICE}/xrpc/app.bsky.actor.getProfile`, + ) + }) + + it('leaves refreshSession unusable', async () => { + ctx.agent.dispose() + await expect(ctx.agent.sessionManager.refreshSession()).rejects.toThrow( + 'No session to refresh', + ) + }) +}) + +describe('public PasswordSessionManager (no inner session)', () => { + it('reads as logged out', () => { + const {agent} = setupPublic() + expect(agent.session).toBe(undefined) + expect(agent.did).toBe(undefined) + expect(agent.hasSession).toBe(false) + expect(agent.pdsUrl).toBe(undefined) + expect(agent.dispatchUrl.toString()).toBe('https://bsky.social/') + }) + + it('dispatches to the service unauthenticated', async () => { + const seen: Headers[] = [] + const fetchMock = makeMockFetch({ + 'app.bsky.feed.getFeed': (_url, init) => { + seen.push(new Headers(init.headers)) + return json({}) + }, + }) + const {manager} = setupPublic(fetchMock) + await manager.fetchHandler('/xrpc/app.bsky.feed.getFeed') + expect(seen.length).toBe(1) + expect(seen[0].get('authorization')).toBe(null) + expect(urlsOf(fetchMock).at(-1)).toBe( + `${SERVICE}/xrpc/app.bsky.feed.getFeed`, + ) + }) +}) + +describe('PasswordSession lifecycle over mocked fetch', () => { + it('resume fast path: constructing does not hit the network', () => { + const fetchMock = makeMockFetch() + setup({fetchMock}) + expect(fetchMock.mock.calls.length).toBe(0) + }) + + it('a refresh fires onUpdated with fresh tokens', async () => { + const onUpdated = + jest.fn>() + const {manager} = setup({sessionOptions: {onUpdated}}) + await manager.refreshSession() + expect(onUpdated).toHaveBeenCalledTimes(1) + expect(manager.session?.accessJwt).toBe('access-jwt-2') + }) + + it('onDeleted fires when refresh returns a declared invalid-token error', async () => { + const onDeleted = + jest.fn>() + const onUpdated = + jest.fn>() + const fetchMock = makeMockFetch({ + 'com.atproto.server.refreshSession': () => + json({error: 'ExpiredToken', message: 'Token expired'}, 400), + }) + const {manager} = setup({fetchMock, sessionOptions: {onDeleted, onUpdated}}) + await expect(manager.refreshSession()).rejects.toBeDefined() + expect(onDeleted).toHaveBeenCalledTimes(1) + expect(onUpdated).not.toHaveBeenCalled() + /* and the bridge reads as logged out afterwards */ + expect(manager.session).toBe(undefined) + }) + + it('onUpdateFailure fires on a transient (500) refresh error, session preserved', async () => { + const onDeleted = + jest.fn>() + const onUpdateFailure = + jest.fn>() + const fetchMock = makeMockFetch({ + 'com.atproto.server.refreshSession': () => + json({error: 'InternalServerError'}, 500), + }) + const {manager} = setup({ + fetchMock, + sessionOptions: {onDeleted, onUpdateFailure}, + }) + /* + * PasswordSession.refresh() resolves with the unchanged data here; the + * bridge restores the old CredentialSession contract by rejecting. + */ + await expect(manager.refreshSession()).rejects.toThrow( + 'Failed to refresh session', + ) + expect(onUpdateFailure).toHaveBeenCalledTimes(1) + expect(onDeleted).not.toHaveBeenCalled() + expect(manager.session?.accessJwt).toBe('access-jwt') + }) + + it('rejects on a network error rather than reporting a no-op success', async () => { + const fetchMock = makeMockFetch({ + 'com.atproto.server.refreshSession': () => { + throw new TypeError('Network request failed') + }, + }) + const {manager} = setup({fetchMock}) + await expect(manager.refreshSession()).rejects.toThrow( + 'Failed to refresh session', + ) + /* the session survives, exactly as the old transient-failure path did */ + expect(manager.session?.accessJwt).toBe('access-jwt') + }) + + it('resumeSession rejects on a transient failure too', async () => { + const fetchMock = makeMockFetch({ + 'com.atproto.server.refreshSession': () => + json({error: 'InternalServerError'}, 500), + }) + const {agent} = setup({fetchMock}) + await expect(agent.resumeSession(agent.session!)).rejects.toThrow( + 'Failed to refresh session', + ) + }) +}) diff --git a/src/state/session/__tests__/expiry-rescue-test.ts b/src/state/session/__tests__/expiry-rescue-test.ts new file mode 100644 index 0000000000..c33eb527de --- /dev/null +++ b/src/state/session/__tests__/expiry-rescue-test.ts @@ -0,0 +1,104 @@ +import {describe, expect, it} from '@jest/globals' + +import { + MAX_EXPIRY_RESCUE_GENERATIONS, + pickExpiryRescueCandidate, +} from '../expiry-rescue' +import {type SessionAccount} from '../types' + +function makeAccount(overrides: Partial = {}): SessionAccount { + return { + service: 'https://bsky.social', + did: 'did:plc:example123', + handle: 'alice.test', + email: 'alice@example.com', + emailConfirmed: true, + emailAuthFactor: false, + refreshJwt: 'refresh-jwt', + accessJwt: 'access-jwt', + signupQueued: false, + active: true, + status: undefined, + pdsUrl: undefined, + isSelfHosted: false, + ...overrides, + } +} + +describe('pickExpiryRescueCandidate', () => { + it('picks a candidate whose refreshJwt differs from the dying one', () => { + const fresh = makeAccount({refreshJwt: 'refresh-jwt-2'}) + const picked = pickExpiryRescueCandidate({ + dyingRefreshJwt: 'refresh-jwt-1', + candidates: [fresh], + failedRefreshJwts: new Set(), + }) + expect(picked).toBe(fresh) + }) + + it('rejects a candidate carrying the dying refreshJwt (equally dead)', () => { + const picked = pickExpiryRescueCandidate({ + dyingRefreshJwt: 'refresh-jwt-1', + candidates: [makeAccount({refreshJwt: 'refresh-jwt-1'})], + failedRefreshJwts: new Set(), + }) + expect(picked).toBe(undefined) + }) + + it('rejects a candidate with no refreshJwt', () => { + const picked = pickExpiryRescueCandidate({ + dyingRefreshJwt: 'refresh-jwt-1', + candidates: [makeAccount({refreshJwt: undefined}), undefined], + failedRefreshJwts: new Set(), + }) + expect(picked).toBe(undefined) + }) + + it('rejects a candidate already recorded as failed (loop guard)', () => { + const picked = pickExpiryRescueCandidate({ + dyingRefreshJwt: 'refresh-jwt-1', + candidates: [makeAccount({refreshJwt: 'refresh-jwt-2'})], + failedRefreshJwts: new Set(['refresh-jwt-2']), + }) + expect(picked).toBe(undefined) + }) + + it('tries candidates in order, preferring the first qualifying one', () => { + const persistedCandidate = makeAccount({ + refreshJwt: 'refresh-jwt-persisted', + }) + const reducerCandidate = makeAccount({refreshJwt: 'refresh-jwt-reducer'}) + const picked = pickExpiryRescueCandidate({ + dyingRefreshJwt: 'refresh-jwt-1', + candidates: [persistedCandidate, reducerCandidate], + failedRefreshJwts: new Set(), + }) + expect(picked).toBe(persistedCandidate) + }) + + it('skips an unusable first candidate and falls back to a later one', () => { + const reducerCandidate = makeAccount({refreshJwt: 'refresh-jwt-reducer'}) + const picked = pickExpiryRescueCandidate({ + dyingRefreshJwt: 'refresh-jwt-1', + candidates: [ + makeAccount({refreshJwt: 'refresh-jwt-1'}), + reducerCandidate, + ], + failedRefreshJwts: new Set(), + }) + expect(picked).toBe(reducerCandidate) + }) + + it('gives up once the failed-generation set hits the hard cap', () => { + const failed = new Set() + for (let i = 0; i < MAX_EXPIRY_RESCUE_GENERATIONS; i++) { + failed.add(`refresh-jwt-failed-${i}`) + } + const picked = pickExpiryRescueCandidate({ + dyingRefreshJwt: 'refresh-jwt-dying', + candidates: [makeAccount({refreshJwt: 'refresh-jwt-brand-new'})], + failedRefreshJwts: failed, + }) + expect(picked).toBe(undefined) + }) +}) diff --git a/src/state/session/__tests__/mock-fetch.ts b/src/state/session/__tests__/mock-fetch.ts new file mode 100644 index 0000000000..a107d8dc3c --- /dev/null +++ b/src/state/session/__tests__/mock-fetch.ts @@ -0,0 +1,126 @@ +import {jest} from '@jest/globals' + +import {type SessionAccount} from '../types' + +/* + * Shared fixtures for the suites that drive a real `PasswordSession` over a + * stubbed network. Not a suite itself - the filename deliberately avoids the + * `-test` suffix so jest does not collect it. + */ + +export const DID = 'did:plc:example123' +export const HANDLE = 'alice.test' +export const SERVICE = 'https://bsky.social' +/** A PDS host an account may be pinned to by its stored `pdsUrl`. */ +export const PDS_HOST = 'https://shimeji.us-east.host.bsky.network' +/** A different PDS host, delivered by the didDoc a refresh returns. */ +export const DIDDOC_PDS_HOST = 'https://morel.us-west.host.bsky.network' + +export function json(body: unknown, status = 200) { + return new Response(JSON.stringify(body), { + status, + headers: {'content-type': 'application/json'}, + }) +} + +/** A minimal valid DID document whose only service entry is a PDS. */ +export function makeDidDoc(pdsUrl: string, did: string = DID) { + return { + id: did, + service: [ + { + id: '#atproto_pds', + type: 'AtprotoPersonalDataServer', + serviceEndpoint: pdsUrl, + }, + ], + } +} + +export function makeAccount( + overrides: Partial = {}, +): SessionAccount { + return { + service: SERVICE, + did: DID, + handle: HANDLE, + email: 'alice@example.com', + emailConfirmed: true, + emailAuthFactor: false, + refreshJwt: 'refresh-jwt', + accessJwt: 'access-jwt', + signupQueued: false, + active: true, + status: undefined, + pdsUrl: undefined, + isSelfHosted: false, + ...overrides, + } +} + +/** + * Build a mock `fetch` that returns canned XRPC responses keyed by the last + * path segment (nsid). `refreshSession` returns fresh tokens; `getSession` + * echoes the account; anything else returns an empty 200. + * + * The refresh response carries both `emailConfirmed` and a `didDoc` so the + * library has no reason to make a `getSession` follow-up call, which keeps the + * recorded request list assertable. Its didDoc points at + * {@link DIDDOC_PDS_HOST}, a different host from {@link PDS_HOST}, so PDS + * re-routing after a refresh is observable. + */ +export function makeMockFetch( + overrides: Record< + string, + (url: string, init: RequestInit) => Response | Promise + > = {}, +) { + return jest.fn( + /* + * PasswordSession calls fetch with a URL object (new URL(path, service)); + * asFetch() below widens the mock to the full fetch signature it expects. + */ + async (input: URL | string, init: RequestInit = {}): Promise => { + const url = input instanceof URL ? input.href : input + const nsid = url.split('/xrpc/')[1]?.split('?')[0] + const handler = nsid ? overrides[nsid] : undefined + if (handler) { + return handler(url, init) + } + if (nsid === 'com.atproto.server.refreshSession') { + return json({ + accessJwt: 'access-jwt-2', + refreshJwt: 'refresh-jwt-2', + handle: HANDLE, + did: DID, + email: 'alice@example.com', + emailConfirmed: true, + didDoc: makeDidDoc(DIDDOC_PDS_HOST), + active: true, + }) + } + if (nsid === 'com.atproto.server.getSession') { + return json({ + did: DID, + handle: HANDLE, + email: 'alice@example.com', + emailConfirmed: true, + active: true, + }) + } + return json({}) + }, + ) +} + +export type MockFetch = ReturnType + +/** Cast a jest fetch mock to the `fetch` type PasswordSession options expect. */ +export function asFetch(mock: MockFetch): typeof fetch { + return mock as unknown as typeof fetch +} + +/** The URLs a mock fetch was called with, in order. */ +export function urlsOf(mock: MockFetch): string[] { + return mock.mock.calls.map(c => (c[0] instanceof URL ? c[0].href : c[0])) +} diff --git a/src/state/session/__tests__/provider-abort-test.tsx b/src/state/session/__tests__/provider-abort-test.tsx new file mode 100644 index 0000000000..fdc1585981 --- /dev/null +++ b/src/state/session/__tests__/provider-abort-test.tsx @@ -0,0 +1,145 @@ +import {beforeEach, describe, expect, it, jest} from '@jest/globals' +import {act, render} from '@testing-library/react-native' + +/* + * The provider pulls the whole app shell in through `#/state/util` and the + * account factories. These mocks cut the tree back to the session lifecycle + * itself, which is all these tests drive. + */ +jest.mock('#/state/persisted', () => { + const { + defaults, + }: typeof import('#/state/persisted/schema') = require('#/state/persisted/schema') + return { + defaults, + get: (key: keyof typeof defaults) => defaults[key], + write: () => Promise.resolve(), + readLatest: (key: keyof typeof defaults) => defaults[key], + onUpdate: () => () => {}, + } +}) +jest.mock('#/state/util', () => ({useCloseAllActiveElements: () => () => {}})) +jest.mock('#/components/dialogs/Context', () => ({ + useGlobalDialogsControlContext: () => ({signinDialogControl: {open() {}}}), +})) +jest.mock('#/analytics', () => ({ + AnalyticsContext: ({children}: {children: React.ReactNode}) => children, + useAnalyticsBase: () => ({metric() {}, logger: {debug() {}, error() {}}}), + utils: {accountToSessionMetadata: () => ({}), useMeta: () => undefined}, +})) +jest.mock('#/state/shell/onboarding', () => ({ + useOnboardingDispatch: () => () => {}, +})) +jest.mock('#/ageAssurance/data', () => ({ + clearAgeAssuranceServerDataForAll: () => {}, + clearAgeAssuranceServerDataForDid: () => {}, +})) +jest.mock('#/lib/persisted-query-storage', () => ({ + clearPersistedQueryStorage: () => Promise.resolve(), +})) +jest.mock('#/lib/notifications/notifications', () => ({ + unregisterPushToken: () => Promise.resolve(), +})) +jest.mock('jwt-decode', () => ({jwtDecode: () => ({})})) + +/* + * The factories are stubbed so a test controls exactly when each one resolves, + * which is what lets a second call abort the first while it is in flight. + * `disposeBundle` is spied on rather than replaced wholesale: the rest of + * session-core stays real so the provider's own module graph is unchanged. + */ +const mockLogin = jest.fn<(...args: unknown[]) => Promise>() +const mockCreateAccount = jest.fn<(...args: unknown[]) => Promise>() +const mockDisposeBundle = jest.fn() +jest.mock('../session-core', () => ({ + ...jest.requireActual('../session-core'), + createSessionBundleAndLogin: (...args: unknown[]) => mockLogin(...args), + disposeBundle: (bundle: unknown) => mockDisposeBundle(bundle), +})) +jest.mock('../create-account', () => ({ + createSessionBundleAndCreateAccount: (...args: unknown[]) => + mockCreateAccount(...args), +})) + +import {Provider, useSessionApi} from '#/state/session' +import {type SessionApiContext} from '#/state/session/types' + +/** Render the provider and hand back its api context. */ +function renderProvider(): SessionApiContext { + let api!: SessionApiContext + function Probe() { + api = useSessionApi() + return null + } + render( + + + , + ) + return api +} + +/* + * Every factory returns an ARMED bundle, so a call whose result is thrown away + * because a newer call superseded it must dispose that bundle. Leaving it armed + * leaves a live session auto-refreshing and rotating refresh tokens server-side + * for an account the app is no longer tracking. + */ +describe('superseded session tasks dispose their bundle', () => { + /* + * Without this, a recorded call from an earlier test satisfies a later + * assertion. The tagged bundles below are the other half of that guard: two + * `{}` literals are structurally equal, so `toHaveBeenCalledWith` could not + * tell one test's bundle from the other's even within a cleared mock. + */ + beforeEach(() => { + mockLogin.mockReset() + mockCreateAccount.mockReset() + mockDisposeBundle.mockReset() + }) + + it('disposes the bundle of an aborted login', async () => { + const bundle = {tag: 'login-bundle'} as never + let resolveLogin!: (value: unknown) => void + mockLogin.mockReturnValueOnce( + new Promise(resolve => { + resolveLogin = resolve + }), + ) + const api = renderProvider() + + const superseded = api.login({} as never, 'LoginForm') + /* the second call aborts the first task's signal, and never settles */ + mockLogin.mockReturnValueOnce(new Promise(() => {})) + void api.login({} as never, 'LoginForm') + + await act(async () => { + resolveLogin({bundle, account: {did: 'did:plc:example'}}) + await superseded + }) + + expect(mockDisposeBundle).toHaveBeenCalledWith(bundle) + }) + + it('disposes the bundle of an aborted createAccount', async () => { + const bundle = {tag: 'create-account-bundle'} as never + let resolveCreate!: (value: unknown) => void + mockCreateAccount.mockReturnValueOnce( + new Promise(resolve => { + resolveCreate = resolve + }), + ) + const api = renderProvider() + + const superseded = api.createAccount({} as never, {} as never) + mockCreateAccount.mockReturnValueOnce(new Promise(() => {})) + void api.createAccount({} as never, {} as never) + + await act(async () => { + resolveCreate({bundle, account: {did: 'did:plc:example'}}) + await superseded + }) + + expect(mockDisposeBundle).toHaveBeenCalledWith(bundle) + }) +}) diff --git a/src/state/session/__tests__/provider-session-events-test.tsx b/src/state/session/__tests__/provider-session-events-test.tsx new file mode 100644 index 0000000000..e6a51a5414 --- /dev/null +++ b/src/state/session/__tests__/provider-session-events-test.tsx @@ -0,0 +1,559 @@ +import {type SessionData} from '@atproto/lex-password-session' +import {beforeEach, describe, expect, it, jest} from '@jest/globals' +import {act, render} from '@testing-library/react-native' + +import {type Schema} from '#/state/persisted/schema' +import {type SessionAccount} from '../types' + +/* + * The provider pulls the whole app shell in through `#/state/util` and the + * account factories. These mocks cut the tree back to the session lifecycle + * itself, which is all these tests drive. They mirror provider-abort-test.tsx, + * plus a stateful `#/state/persisted` (this suite drives cross-tab updates and + * the expiry rescue's fresh persisted read) and an observable + * `emitSessionDropped`. + */ +const mockPersisted: {session: Schema['session']; latest: Schema['session']} = { + session: {accounts: [], currentAccount: undefined}, + latest: {accounts: [], currentAccount: undefined}, +} +/* + * Every registered listener is kept, not just the newest. The provider's + * subscription effect re-runs on every state change, so a callback captured + * before a dispatch is exactly the stale-closure case the shouldActivate guard + * exists to catch. + */ +const mockPersistedListeners: ((value: Schema['session']) => void)[] = [] +jest.mock('#/state/persisted', () => { + const { + defaults, + }: typeof import('#/state/persisted/schema') = require('#/state/persisted/schema') + return { + defaults, + get: (key: string) => + key === 'session' + ? mockPersisted.session + : defaults[key as keyof typeof defaults], + readLatest: (key: string) => + key === 'session' + ? mockPersisted.latest + : defaults[key as keyof typeof defaults], + write: () => Promise.resolve(), + onUpdate: (_key: string, cb: (value: Schema['session']) => void) => { + mockPersistedListeners.push(cb) + return () => {} + }, + } +}) +jest.mock('#/state/util', () => ({useCloseAllActiveElements: () => () => {}})) +jest.mock('#/components/dialogs/Context', () => ({ + useGlobalDialogsControlContext: () => ({signinDialogControl: {open() {}}}), +})) +jest.mock('#/analytics', () => ({ + AnalyticsContext: ({children}: {children: React.ReactNode}) => children, + useAnalyticsBase: () => ({metric() {}, logger: {debug() {}, error() {}}}), + utils: {accountToSessionMetadata: () => ({}), useMeta: () => undefined}, +})) +jest.mock('#/state/shell/onboarding', () => ({ + useOnboardingDispatch: () => () => {}, +})) +jest.mock('#/ageAssurance/data', () => ({ + clearAgeAssuranceServerDataForAll: () => {}, + clearAgeAssuranceServerDataForDid: () => {}, +})) +jest.mock('#/lib/persisted-query-storage', () => ({ + clearPersistedQueryStorage: () => Promise.resolve(), +})) +jest.mock('#/lib/notifications/notifications', () => ({ + unregisterPushToken: () => Promise.resolve(), +})) +jest.mock('jwt-decode', () => ({jwtDecode: () => ({})})) + +const mockEmitSessionDropped = jest.fn() +jest.mock('#/state/events', () => ({ + emitSessionDropped: () => mockEmitSessionDropped(), + emitNetworkConfirmed: () => {}, + emitNetworkLost: () => {}, +})) + +/* + * The factories are stubbed so a test controls exactly what each one returns + * and when. `createSessionBundleFromStoredAccount` is stubbed faithfully rather + * than replaced by a constant: it must still consult `shouldActivate` and + * decline to hand back a bundle when the guard rejects, because that decision + * is what these tests observe. Its disposal of a rejected bundle is pinned by + * session-core-test; here we only assert what the provider does with the + * result. + */ +const mockLogin = jest.fn<(...args: unknown[]) => Promise>() +const mockResume = jest.fn<(...args: unknown[]) => Promise>() +const mockDisposeBundle = jest.fn() +type Rebuild = { + account: SessionAccount + shouldActivate: boolean + bundle: FakeBundle +} +const mockRebuilds: Rebuild[] = [] +const mockRebuild = jest.fn( + ( + account: SessionAccount, + _onSessionChange: unknown, + shouldActivate: ( + bundle: unknown, + account: SessionAccount, + ) => boolean = () => true, + ) => { + const bundle = makeBundle(account) + const activated = shouldActivate(bundle, account) + mockRebuilds.push({account, shouldActivate: activated, bundle}) + return activated ? {bundle, account} : undefined + }, +) +jest.mock('../session-core', () => ({ + ...jest.requireActual('../session-core'), + createSessionBundleAndLogin: (...args: unknown[]) => mockLogin(...args), + createSessionBundleAndResume: (...args: unknown[]) => mockResume(...args), + createSessionBundleFromStoredAccount: (...args: unknown[]) => + // @ts-expect-error the stub's arity is checked by its own signature + mockRebuild(...args), + disposeBundle: (bundle: unknown) => mockDisposeBundle(bundle), +})) +jest.mock('../create-account', () => ({ + createSessionBundleAndCreateAccount: () => new Promise(() => {}), +})) + +import {Provider, useSession, useSessionApi} from '#/state/session' +import { + type OnSessionChange, + type SessionBundle, +} from '#/state/session/session-core' +import {type SessionApiContext} from '#/state/session/types' + +const DID = 'did:plc:example123' +const SERVICE = 'https://bsky.social/' + +function makeAccount(overrides: Partial = {}): SessionAccount { + return { + service: SERVICE, + did: DID, + handle: 'alice.test', + email: 'alice@example.com', + emailConfirmed: true, + emailAuthFactor: false, + refreshJwt: 'refresh-jwt-1', + accessJwt: 'access-jwt-1', + signupQueued: false, + active: true, + status: undefined, + pdsUrl: undefined, + isSelfHosted: false, + ...overrides, + } +} + +/* + * The provider only ever reads `bundle.agent` (for context) and + * `bundle.session.destroyed` / `bundle.session.session` (for the cross-tab + * token comparison), and otherwise treats a bundle as an opaque identity. A + * literal with those fields is enough, and keeps a real PasswordSession - with + * its network and refresh machinery - out of a suite about provider dispatch. + */ +type FakeBundle = { + session: {destroyed: boolean; session: SessionData} + agent: object + service: URL +} + +function makeBundle(account: SessionAccount): FakeBundle { + return { + session: { + destroyed: false, + session: { + accessJwt: account.accessJwt ?? '', + refreshJwt: account.refreshJwt ?? '', + /* SessionData types these as branded strings; the values are fixtures */ + handle: account.handle as `${string}.${string}`, + did: account.did as `did:${string}:${string}`, + active: true, + service: account.service, + }, + }, + agent: {}, + service: new URL(account.service), + } +} + +type Harness = { + api: SessionApiContext + /** The provider's own onSessionChange, as handed to a session factory. */ + onSessionChange: OnSessionChange + currentAccount: () => SessionAccount | undefined + hasSession: () => boolean +} + +/** + * Render the provider, log an account in through the stubbed login factory, and + * hand back the api plus the `onSessionChange` the factory received. Firing + * that callback is how a test synthesizes a session event from a live bundle. + */ +async function renderLoggedIn( + account: SessionAccount, + bundle: FakeBundle, +): Promise { + let api!: SessionApiContext + let session!: ReturnType + function Probe() { + api = useSessionApi() + session = useSession() + return null + } + render( + + + , + ) + + let captured!: OnSessionChange + mockLogin.mockImplementationOnce((...args: unknown[]) => { + captured = args[1] as OnSessionChange + return Promise.resolve({bundle, account}) + }) + await act(async () => { + await api.login({} as never, 'LoginForm') + }) + + return { + api, + onSessionChange: captured, + currentAccount: () => session.currentAccount, + hasSession: () => session.hasSession, + } +} + +/** The dying payload PasswordSession threads through its `onDeleted` hook. */ +function dyingData(refreshJwt: string): SessionData { + return { + accessJwt: 'dead-access-jwt', + refreshJwt, + handle: 'alice.test', + did: DID, + active: true, + service: SERVICE, + } +} + +/** The rotated payload PasswordSession threads through its `onUpdated` hook. */ +function refreshedData( + refreshJwt: string, + didDoc?: SessionData['didDoc'], +): SessionData { + return { + accessJwt: 'fresh-access-jwt', + refreshJwt, + handle: 'alice.test', + did: DID, + active: true, + service: SERVICE, + ...(didDoc ? {didDoc} : {}), + } +} + +/** A minimal valid DID document whose only service entry is a PDS. */ +function makeDidDoc(pdsUrl: string): SessionData['didDoc'] { + return { + id: DID, + service: [ + { + id: '#atproto_pds', + type: 'AtprotoPersonalDataServer', + serviceEndpoint: pdsUrl, + }, + ], + } +} + +beforeEach(() => { + mockPersisted.session = {accounts: [], currentAccount: undefined} + mockPersisted.latest = {accounts: [], currentAccount: undefined} + mockPersistedListeners.length = 0 + mockRebuilds.length = 0 + mockLogin.mockReset() + mockResume.mockReset() + mockRebuild.mockClear() + mockDisposeBundle.mockReset() + mockEmitSessionDropped.mockReset() +}) + +/* + * A stale tab can expire a refresh token that another tab has already rotated + * past. Logging every tab out on that event is the known-worst failure in this + * subsystem, so the provider first looks for a newer token generation and + * rebuilds onto it, only falling through to logout when there is nothing left + * to try. + */ +describe('expiry rescue', () => { + it('rebuilds onto a fresher persisted generation instead of logging out', async () => { + const account = makeAccount() + const bundle = makeBundle(account) + const {onSessionChange, hasSession, currentAccount} = await renderLoggedIn( + account, + bundle, + ) + + /* another tab already rotated to generation 2 and wrote it to storage */ + const fresher = makeAccount({ + accessJwt: 'access-jwt-2', + refreshJwt: 'refresh-jwt-2', + }) + mockPersisted.latest = {accounts: [fresher], currentAccount: fresher} + + act(() => { + onSessionChange( + bundle as unknown as SessionBundle, + DID, + 'expired', + dyingData('refresh-jwt-1'), + ) + }) + + /* the rescue rebuilt onto the fresher generation ... */ + expect(mockRebuilds.length).toBe(1) + expect(mockRebuilds[0].account.refreshJwt).toBe('refresh-jwt-2') + /* ... and adopted it, without ever reporting the session as dropped */ + expect(hasSession()).toBe(true) + expect(currentAccount()?.refreshJwt).toBe('refresh-jwt-2') + expect(mockEmitSessionDropped).not.toHaveBeenCalled() + }) + + it('drops the session and logs out when there is no fresher generation', async () => { + const account = makeAccount() + const bundle = makeBundle(account) + const {onSessionChange, hasSession, currentAccount} = await renderLoggedIn( + account, + bundle, + ) + + /* storage agrees the dying token is the newest one anybody has */ + mockPersisted.latest = {accounts: [account], currentAccount: account} + + act(() => { + onSessionChange( + bundle as unknown as SessionBundle, + DID, + 'expired', + dyingData('refresh-jwt-1'), + ) + }) + + expect(mockRebuilds.length).toBe(0) + expect(mockEmitSessionDropped).toHaveBeenCalledTimes(1) + expect(hasSession()).toBe(false) + /* the reducer cleared the dead credentials rather than keeping them */ + expect(currentAccount()).toBe(undefined) + }) + + it('does not retry a generation that already failed', async () => { + const account = makeAccount() + const bundle = makeBundle(account) + const {onSessionChange, hasSession} = await renderLoggedIn(account, bundle) + + const gen2 = makeAccount({ + accessJwt: 'access-jwt-2', + refreshJwt: 'refresh-jwt-2', + }) + mockPersisted.latest = {accounts: [gen2], currentAccount: gen2} + + /* generation 1 dies and is rescued onto generation 2 */ + act(() => { + onSessionChange( + bundle as unknown as SessionBundle, + DID, + 'expired', + dyingData('refresh-jwt-1'), + ) + }) + expect(mockRebuilds.length).toBe(1) + const rescued = mockRebuilds[0].bundle + + /* + * Generation 2 dies too, and a stale tab has meanwhile written generation 1 + * back to storage. It differs from the dying token, so only the record of + * its earlier failure can reject it. + */ + mockPersisted.latest = {accounts: [account], currentAccount: account} + act(() => { + onSessionChange( + rescued as unknown as SessionBundle, + DID, + 'expired', + dyingData('refresh-jwt-2'), + ) + }) + + expect(mockRebuilds.length).toBe(1) + expect(mockEmitSessionDropped).toHaveBeenCalledTimes(1) + expect(hasSession()).toBe(false) + }) +}) + +/* + * A refresh payload only carries a didDoc when the server sends one, but + * `pdsUrl` is never derived from the login service. If the provider does not + * thread the stored value through, an ordinary refresh persists + * `pdsUrl: undefined` and the next cold start routes pre-refresh requests to + * the entryway instead of the account's PDS. + */ +describe('refresh persistence', () => { + const PDS_HOST = 'https://shimeji.us-east.host.bsky.network' + const DIDDOC_PDS_HOST = 'https://morel.us-west.host.bsky.network' + + it('keeps the stored pdsUrl when the refresh carries no didDoc', async () => { + const account = makeAccount({pdsUrl: `${PDS_HOST}/`}) + const bundle = makeBundle(account) + const {onSessionChange, currentAccount} = await renderLoggedIn( + account, + bundle, + ) + + act(() => { + onSessionChange( + bundle as unknown as SessionBundle, + DID, + 'update', + refreshedData('refresh-jwt-2'), + ) + }) + + expect(currentAccount()?.refreshJwt).toBe('refresh-jwt-2') + expect(currentAccount()?.pdsUrl).toBe(`${PDS_HOST}/`) + }) + + it('prefers the didDoc endpoint over the stored pdsUrl', async () => { + const account = makeAccount({pdsUrl: `${PDS_HOST}/`}) + const bundle = makeBundle(account) + const {onSessionChange, currentAccount} = await renderLoggedIn( + account, + bundle, + ) + + act(() => { + onSessionChange( + bundle as unknown as SessionBundle, + DID, + 'update', + refreshedData('refresh-jwt-2', makeDidDoc(DIDDOC_PDS_HOST)), + ) + }) + + expect(currentAccount()?.pdsUrl).toBe(`${DIDDOC_PDS_HOST}/`) + }) +}) + +/** Deliver a cross-tab `persisted` update to the provider's newest listener. */ +function emitSynced(session: Schema['session']) { + mockPersistedListeners[mockPersistedListeners.length - 1](session) +} + +/* + * A `PasswordSession` cannot be patched in place, so adopting tokens another + * tab refreshed means rebuilding the bundle. Doing that for every broadcast + * would churn the agent (and the React tree under it) constantly, so the + * provider rebuilds only when the tokens actually moved, and guards the swap + * against the store having advanced underneath it. + */ +describe('cross-tab sync', () => { + it('short-circuits an update carrying the tokens the live session already has', async () => { + const account = makeAccount() + const bundle = makeBundle(account) + const {hasSession} = await renderLoggedIn(account, bundle) + + act(() => { + emitSynced({accounts: [account], currentAccount: account}) + }) + + /* identical tokens: nothing to adopt, so no rebuild */ + expect(mockRebuilds.length).toBe(0) + expect(hasSession()).toBe(true) + }) + + it('rebuilds onto tokens another tab rotated', async () => { + const account = makeAccount() + const bundle = makeBundle(account) + const {currentAccount} = await renderLoggedIn(account, bundle) + + const rotated = makeAccount({ + accessJwt: 'access-jwt-2', + refreshJwt: 'refresh-jwt-2', + }) + act(() => { + emitSynced({accounts: [rotated], currentAccount: rotated}) + }) + + expect(mockRebuilds.length).toBe(1) + expect(mockRebuilds[0].account.refreshJwt).toBe('refresh-jwt-2') + expect(currentAccount()?.refreshJwt).toBe('refresh-jwt-2') + }) + + it('declines to activate a rebuild once the store has moved past the bundle it was built for', async () => { + const account = makeAccount() + const bundle = makeBundle(account) + await renderLoggedIn(account, bundle) + + const gen2 = makeAccount({ + accessJwt: 'access-jwt-2', + refreshJwt: 'refresh-jwt-2', + }) + const gen3 = makeAccount({ + accessJwt: 'access-jwt-3', + refreshJwt: 'refresh-jwt-3', + }) + + /* + * Two broadcasts land back to back inside one act(), so React does not + * commit (and the effect does not re-subscribe) between them: the second + * runs the listener registered while the ORIGINAL bundle was current, even + * though the store has since advanced to the generation-2 rebuild. + */ + const listener = mockPersistedListeners[mockPersistedListeners.length - 1] + act(() => { + listener({accounts: [gen2], currentAccount: gen2}) + listener({accounts: [gen3], currentAccount: gen3}) + }) + + expect(mockRebuilds.length).toBe(2) + expect(mockRebuilds[0].shouldActivate).toBe(true) + /* the stale closure's bundle is no longer current, so the swap is refused */ + expect(mockRebuilds[1].account.refreshJwt).toBe('refresh-jwt-3') + expect(mockRebuilds[1].shouldActivate).toBe(false) + }) + + it('cancels pending work when another tab logs the account out', async () => { + const account = makeAccount() + const bundle = makeBundle(account) + const {api} = await renderLoggedIn(account, bundle) + + /* a resume is in flight and will resolve only after the cross-tab logout */ + const resumedBundle = makeBundle(account) + let finishResume!: (value: unknown) => void + mockResume.mockReturnValueOnce( + new Promise(resolve => { + finishResume = resolve + }), + ) + const pending = api.resumeSession(account) + + const loggedOut = makeAccount({accessJwt: undefined, refreshJwt: undefined}) + act(() => { + emitSynced({accounts: [loggedOut], currentAccount: loggedOut}) + }) + + await act(async () => { + finishResume({bundle: resumedBundle, account}) + await pending + }) + + /* the superseded resume disposed its bundle rather than signing back in */ + expect(mockDisposeBundle).toHaveBeenCalledWith(resumedBundle) + expect(mockRebuilds.length).toBe(0) + }) +}) diff --git a/src/state/session/__tests__/session-core-test.ts b/src/state/session/__tests__/session-core-test.ts new file mode 100644 index 0000000000..9dbc646c9f --- /dev/null +++ b/src/state/session/__tests__/session-core-test.ts @@ -0,0 +1,1027 @@ +import { + PasswordSession, + type PasswordSessionOptions, + type SessionData, +} from '@atproto/lex-password-session' +import {beforeEach, describe, expect, it, jest} from '@jest/globals' + +import {type SessionAccount} from '../types' + +jest.mock('#/state/events', () => ({ + emitNetworkConfirmed: jest.fn(), + emitNetworkLost: jest.fn(), +})) + +/* + * `makeSessionHooks` reports a throwing `onSessionChange` through the logger + * rather than letting it escape into PasswordSession's session promise. Stub + * the module so that report is assertable (and so nothing reaches the real + * transports). + */ +const mockLoggerError = jest.fn() +jest.mock('#/logger', () => { + const noopLogger = { + error: (...args: unknown[]) => mockLoggerError(...args), + warn: () => {}, + info: () => {}, + log: () => {}, + debug: () => {}, + } + return { + logger: noopLogger, + Logger: { + create: () => noopLogger, + Context: new Proxy({}, {get: (_t, key) => String(key)}), + Level: {}, + }, + } +}) + +/* + * `prefetchAgeAssuranceServerData` is a genuine prep await in each factory + * (moderation config is synchronous, so the AA prefetch is where the factory + * tests inject a mid-prep token rotation). The default is a no-op; + * individual tests install behavior via `mockImplementationOnce`. + */ +const mockPrefetchAgeAssuranceServerData = jest.fn<() => void | Promise>() +jest.mock('#/ageAssurance/data', () => ({ + prefetchAgeAssuranceServerData: () => mockPrefetchAgeAssuranceServerData(), +})) +/* + * The factory tail awaits `features.refresh(...)`; stub the analytics module so + * the factory does not pull GrowthBook (and its native deps) into this + * lightweight suite. + */ +jest.mock('#/analytics', () => ({ + features: {refresh: () => Promise.resolve()}, +})) + +/* + * `configureModerationForAccount` is synchronous (the labeler cache is a local + * MMKV read), so it is not a prep await - but it still runs inside each factory + * with the freshly built bridge agent, before the awaited prep steps. The + * factory tests capture the agent with this mock, then inject a real refresh + * into the awaited AA prefetch so a token rotation happens during prep, before + * arm(). The default is a no-op so other tests are unaffected. + * (jest requires out-of-scope factory references to be `mock`-prefixed.) + */ +const mockConfigureModerationForAccount = + jest.fn<(agent: unknown, account: unknown) => void>() +jest.mock('../moderation', () => ({ + configureModerationForAccount: (agent: unknown, account: unknown) => + mockConfigureModerationForAccount(agent, account), + configureModerationForGuest: () => {}, +})) + +jest.mock('jwt-decode', () => ({ + jwtDecode(token: string) { + if (token === 'queued-access-jwt') { + return {scope: 'com.atproto.signupQueued'} + } + /* + * A far-future exp so isSessionExpired() reads this stored token as still + * valid, which routes resume() through the sync (no-network) fast path. + * That isolates the prep-time refresh as the ONLY token rotation. + */ + if (token === 'valid-access-jwt') { + return {scope: 'com.atproto.access', exp: 4102444800} + } + return {scope: 'com.atproto.access'} + }, +})) + +import {type BskyAppAgent} from '../bridge-agent' +import { + type AtpSessionEvent, + buildBundle, + createSessionBundleFromStoredAccount, + disposeBundle, + finishPreparation, + makeSessionHooks, + registerBundleKillSwitch, + sessionAccountToSessionData, + type SessionBundle, + sessionDataToSessionAccount, +} from '../session-core' +import { + asFetch, + DID, + HANDLE, + makeAccount, + makeDidDoc, + makeMockFetch, + PDS_HOST as PDS_URL, + SERVICE, +} from './mock-fetch' + +function synthDidDoc( + did: string, + pdsUrl: string, +): NonNullable { + return makeDidDoc(pdsUrl, did) +} + +function makeSessionData(overrides: Partial = {}): SessionData { + return { + accessJwt: 'access-jwt', + refreshJwt: 'refresh-jwt', + handle: HANDLE, + did: DID, + email: 'alice@example.com', + emailConfirmed: true, + emailAuthFactor: false, + active: true, + service: 'https://bsky.social', + ...overrides, + } +} + +describe('sessionDataToSessionAccount', () => { + it('returns undefined for a missing session', () => { + expect(sessionDataToSessionAccount(undefined, 'https://bsky.social')).toBe( + undefined, + ) + expect(sessionDataToSessionAccount(null, 'https://bsky.social')).toBe( + undefined, + ) + }) + + it('maps fields for a hosted account (no didDoc)', () => { + const account = sessionDataToSessionAccount( + makeSessionData(), + 'https://bsky.social', + )! + expect(account).toEqual({ + service: 'https://bsky.social/', + did: DID, + handle: HANDLE, + email: 'alice@example.com', + emailConfirmed: true, + emailAuthFactor: false, + refreshJwt: 'refresh-jwt', + accessJwt: 'access-jwt', + signupQueued: false, + active: true, + status: undefined, + pdsUrl: undefined, + isSelfHosted: false, + }) + }) + + it('serializes service as a normalized URL', () => { + const account = sessionDataToSessionAccount( + makeSessionData(), + 'https://bsky.social', + )! + expect(account.service).toBe('https://bsky.social/') + }) + + it('serializes the didDoc PDS endpoint as a normalized URL', () => { + const account = sessionDataToSessionAccount( + makeSessionData({didDoc: synthDidDoc(DID, PDS_URL)}), + 'https://bsky.social', + )! + expect(account.pdsUrl).toBe(`${PDS_URL}/`) + }) + + it('leaves pdsUrl undefined for hosted accounts (no service fallback)', () => { + const account = sessionDataToSessionAccount( + makeSessionData({didDoc: undefined}), + 'https://bsky.social', + )! + expect(account.pdsUrl).toBe(undefined) + }) + + it('retains the stored PDS when a valid didDoc has no PDS service', () => { + const account = sessionDataToSessionAccount( + makeSessionData({didDoc: {id: DID}}), + 'https://bsky.social', + PDS_URL, + )! + expect(account.pdsUrl).toBe(`${PDS_URL}/`) + }) + + it('derives isSelfHosted from the service URL', () => { + const hosted = sessionDataToSessionAccount( + makeSessionData(), + 'https://bsky.social', + )! + expect(hosted.isSelfHosted).toBe(false) + + const selfHosted = sessionDataToSessionAccount( + makeSessionData({service: 'https://pds.example.com'}), + 'https://pds.example.com', + )! + expect(selfHosted.isSelfHosted).toBe(true) + }) + + it('derives signupQueued from the access token scope', () => { + const queued = sessionDataToSessionAccount( + makeSessionData({accessJwt: 'queued-access-jwt'}), + 'https://bsky.social', + )! + expect(queued.signupQueued).toBe(true) + + const notQueued = sessionDataToSessionAccount( + makeSessionData(), + 'https://bsky.social', + )! + expect(notQueued.signupQueued).toBe(false) + }) + + it('coerces missing email flags to false', () => { + const account = sessionDataToSessionAccount( + makeSessionData({ + email: undefined, + emailConfirmed: undefined, + emailAuthFactor: undefined, + }), + 'https://bsky.social', + )! + expect(account.email).toBe(undefined) + expect(account.emailConfirmed).toBe(false) + expect(account.emailAuthFactor).toBe(false) + }) + + it('preserves the exact SessionAccount field order', () => { + /* + * Byte-stability guard: the reducer's JSON.stringify fast path and the + * session test snapshots depend on this exact persisted key order. + */ + const account = sessionDataToSessionAccount( + makeSessionData({didDoc: synthDidDoc(DID, PDS_URL)}), + 'https://bsky.social', + )! + const golden: SessionAccount = { + service: 'https://bsky.social/', + did: DID, + handle: HANDLE, + email: 'alice@example.com', + emailConfirmed: true, + emailAuthFactor: false, + refreshJwt: 'refresh-jwt', + accessJwt: 'access-jwt', + signupQueued: false, + active: true, + status: undefined, + pdsUrl: `${PDS_URL}/`, + isSelfHosted: false, + } + expect(Object.keys(account)).toEqual(Object.keys(golden)) + expect(JSON.stringify(account)).toBe(JSON.stringify(golden)) + }) +}) + +describe('sessionAccountToSessionData', () => { + const baseAccount: SessionAccount = { + service: 'https://bsky.social/', + did: DID, + handle: HANDLE, + email: 'alice@example.com', + emailConfirmed: true, + emailAuthFactor: false, + refreshJwt: 'refresh-jwt', + accessJwt: 'access-jwt', + signupQueued: false, + active: true, + status: undefined, + pdsUrl: undefined, + isSelfHosted: false, + } + + it('maps fields with empty-string token fallbacks and active default', () => { + const data = sessionAccountToSessionData({ + ...baseAccount, + accessJwt: undefined, + refreshJwt: undefined, + active: undefined, + }) + expect(data.accessJwt).toBe('') + expect(data.refreshJwt).toBe('') + expect(data.active).toBe(true) + expect(data.did).toBe(DID) + expect(data.handle).toBe(HANDLE) + expect(data.service).toBe('https://bsky.social/') + }) + + it('omits didDoc when the account has no stored pdsUrl', () => { + const data = sessionAccountToSessionData(baseAccount) + expect('didDoc' in data).toBe(false) + }) + + it('does not synthesize protocol data from a stored pdsUrl', () => { + const data = sessionAccountToSessionData({ + ...baseAccount, + pdsUrl: `${PDS_URL}/`, + }) + expect('didDoc' in data).toBe(false) + }) + + it('round-trips account -> SessionData -> account preserving all fields', () => { + const withPds: SessionAccount = { + ...baseAccount, + pdsUrl: `${PDS_URL}/`, + } + for (const account of [baseAccount, withPds]) { + const data = sessionAccountToSessionData(account) + const roundTripped = sessionDataToSessionAccount( + data, + account.service, + account.pdsUrl, + )! + expect(roundTripped).toEqual(account) + expect(JSON.stringify(roundTripped)).toBe(JSON.stringify(account)) + } + }) + + it('round-trips signupQueued via the access token scope', () => { + const queued: SessionAccount = { + ...baseAccount, + accessJwt: 'queued-access-jwt', + signupQueued: true, + } + const roundTripped = sessionDataToSessionAccount( + sessionAccountToSessionData(queued), + queued.service, + )! + expect(roundTripped.signupQueued).toBe(true) + expect(roundTripped).toEqual(queued) + }) + + it('round-trips a self-hosted account', () => { + const selfHosted: SessionAccount = { + ...baseAccount, + service: 'https://pds.example.com/', + pdsUrl: 'https://pds.example.com/', + isSelfHosted: true, + } + const roundTripped = sessionDataToSessionAccount( + sessionAccountToSessionData(selfHosted), + selfHosted.service, + selfHosted.pdsUrl, + )! + expect(roundTripped).toEqual(selfHosted) + }) +}) + +describe('createSessionBundleFromStoredAccount', () => { + it('builds a bridge agent over one session', () => { + const result = createSessionBundleFromStoredAccount( + makeAccount(), + jest.fn(), + )! + + /* the agent reads its identity straight through the shared session */ + expect(result.bundle.agent.session?.accessJwt).toBe('access-jwt') + expect(result.bundle.agent.did).toBe(DID) + expect(result.bundle.agent.sessionManager.session).toBe( + result.bundle.agent.session, + ) + expect(result.bundle.service.toString()).toBe(`${SERVICE}/`) + disposeBundle(result.bundle) + /* disposal detaches the agent from the session */ + expect(result.bundle.agent.session).toBe(undefined) + }) + + it('disposes a bundle rejected by the activation guard', async () => { + const onSessionChange = jest.fn() + let rejectedBundle: SessionBundle | undefined + const result = createSessionBundleFromStoredAccount( + makeAccount(), + onSessionChange, + bundle => { + rejectedBundle = bundle + return false + }, + ) + + expect(result).toBeUndefined() + await expect( + rejectedBundle!.session.fetchHandler('/xrpc/test', {}), + ).rejects.toThrow('session disposed') + expect(onSessionChange).not.toHaveBeenCalled() + }) +}) + +describe('makeSessionHooks arm-latch + event mapping', () => { + /* + * The hooks read neither `this` (the PasswordSession) nor their data + * argument, so we invoke them with empty stand-ins cast to the declared + * parameter types. This keeps the test focused on the arm-latch + event + * mapping. + */ + const fakeSession = {} as PasswordSession + const fakeData = {} as SessionData + + function setup() { + const onSessionChange = + jest.fn< + ( + bundle: SessionBundle, + did: string, + event: AtpSessionEvent, + sessionData?: SessionData, + ) => void + >() + /* the hook only passes this through by identity; a stub bundle suffices */ + const bundle = {} as SessionBundle + const hooks = makeSessionHooks({ + onSessionChange, + getBundle: () => bundle, + getDid: () => DID, + }) + return {onSessionChange, bundle, hooks} + } + + it('swallows events before arm()', () => { + const {onSessionChange, hooks} = setup() + void hooks.onUpdated?.call(fakeSession, fakeData) + expect(onSessionChange).not.toHaveBeenCalled() + }) + + it("maps onUpdated -> 'update' after arm(), passing the bundle + payload through", () => { + const {onSessionChange, bundle, hooks} = setup() + hooks.arm() + void hooks.onUpdated?.call(fakeSession, fakeData) + expect(onSessionChange).toHaveBeenCalledTimes(1) + expect(onSessionChange.mock.calls[0][0]).toBe(bundle) + expect(onSessionChange.mock.calls[0][1]).toBe(DID) + expect(onSessionChange.mock.calls[0][2]).toBe('update') + /* the fresh SessionData the library delivers is threaded through verbatim */ + expect(onSessionChange.mock.calls[0][3]).toBe(fakeData) + }) + + it("maps onDeleted -> 'expired' after arm()", () => { + const {onSessionChange, hooks} = setup() + hooks.arm() + void hooks.onDeleted?.call(fakeSession, fakeData) + expect(onSessionChange.mock.calls[0][2]).toBe('expired') + }) + + it("maps onUpdateFailure -> 'network-error' after arm()", () => { + const {onSessionChange, hooks} = setup() + hooks.arm() + void hooks.onUpdateFailure?.call( + fakeSession, + fakeData, + {} as Parameters>[1], + ) + expect(onSessionChange.mock.calls[0][2]).toBe('network-error') + }) + + it("threads the dying session payload on the 'expired' path", () => { + /* + * onDeleted maps to 'expired' AND threads the dying SessionData through + * (the library hands onDeleted the session being destroyed, before it nulls + * its internal state). The provider reads the dying refreshJwt from this + * payload to drive the cross-tab expiry rescue. The provider still guards + * `refreshedAccount` on `event === 'update' && sessionData`, so the payload + * on 'expired' does NOT produce a refreshedAccount (reducer still clears + * tokens + logs out when no rescue applies) - see the provider test below. + */ + const {onSessionChange, hooks} = setup() + hooks.arm() + void hooks.onDeleted?.call(fakeSession, fakeData) + expect(onSessionChange.mock.calls[0][2]).toBe('expired') + expect(onSessionChange.mock.calls[0][3]).toBe(fakeData) + }) +}) + +/* + * The exact derivation from the provider's onSessionChange (index.tsx). Pinned + * here because payload threading and this mapping together read tokens from + * the delivered payload on 'update' and force + * undefined on the drop paths so the reducer logs the user out. + */ +function deriveRefreshedAccount( + event: AtpSessionEvent, + sessionData?: SessionData, +): SessionAccount | undefined { + return event === 'update' && sessionData + ? sessionDataToSessionAccount(sessionData, sessionData.service) + : undefined +} + +/* + * `PasswordSession` fires onUpdated with the fresh session BEFORE committing it + * internally, so the live getter is still stale at hook time. + */ +describe('session-hook payload threading (pre-commit ordering)', () => { + it('delivers the NEW tokens via the payload even though the live getter is still pre-commit stale', async () => { + const fetchMock = makeMockFetch() + let session!: PasswordSession + let liveGetterAtHookTime: SessionAccount | undefined + let refreshedAccountAtHookTime: SessionAccount | undefined + const onSessionChange = jest.fn( + ( + _bundle: SessionBundle, + _did: string, + event: AtpSessionEvent, + sessionData?: SessionData, + ) => { + /* Capture the live getter to demonstrate its pre-commit state. */ + liveGetterAtHookTime = sessionDataToSessionAccount( + session.session, + session.session.service, + ) + /* Derive fresh data from the delivered payload. */ + refreshedAccountAtHookTime = deriveRefreshedAccount(event, sessionData) + }, + ) + const hooks = makeSessionHooks({ + onSessionChange, + getBundle: () => ({}) as SessionBundle, + getDid: () => DID, + }) + session = new PasswordSession(sessionAccountToSessionData(makeAccount()), { + ...hooks, + fetch: asFetch(fetchMock), + }) + hooks.arm() + + await session.refresh() + + /* at hook time the live getter still held the previous tokens */ + expect(liveGetterAtHookTime?.accessJwt).toBe('access-jwt') + /* the payload already contains the fresh tokens */ + expect(refreshedAccountAtHookTime?.accessJwt).toBe('access-jwt-2') + expect(refreshedAccountAtHookTime?.refreshJwt).toBe('refresh-jwt-2') + /* and the session does eventually commit those same tokens */ + expect(session.session.accessJwt).toBe('access-jwt-2') + }) + + it("yields refreshedAccount === undefined on the 'expired' path (forces logout)", async () => { + const fetchMock = makeMockFetch({ + 'com.atproto.server.refreshSession': () => + new Response( + JSON.stringify({error: 'ExpiredToken', message: 'Token expired'}), + {status: 400, headers: {'content-type': 'application/json'}}, + ), + }) + let refreshedAccountAtHookTime: SessionAccount | undefined = makeAccount() + let observedEvent: AtpSessionEvent | undefined + let observedSessionData: SessionData | undefined + const onSessionChange = jest.fn( + ( + _bundle: SessionBundle, + _did: string, + event: AtpSessionEvent, + sessionData?: SessionData, + ) => { + observedEvent = event + observedSessionData = sessionData + refreshedAccountAtHookTime = deriveRefreshedAccount(event, sessionData) + }, + ) + const hooks = makeSessionHooks({ + onSessionChange, + getBundle: () => ({}) as SessionBundle, + getDid: () => DID, + }) + const session = new PasswordSession( + sessionAccountToSessionData(makeAccount()), + {...hooks, fetch: asFetch(fetchMock)}, + ) + hooks.arm() + + await expect(session.refresh()).rejects.toBeDefined() + + expect(observedEvent).toBe('expired') + /* + * The dying SessionData IS threaded on 'expired' (its refreshJwt drives the + * provider's cross-tab rescue), but it does NOT become a refreshedAccount: + * deriveRefreshedAccount only maps the 'update' path, so the reducer still + * sees `undefined` and logs out when no rescue applies. + */ + expect(observedSessionData?.refreshJwt).toBe('refresh-jwt') + expect(refreshedAccountAtHookTime).toBe(undefined) + }) +}) + +/* + * `PasswordSession` exposes no local + * destroy, so disposeBundle neutralizes the session by tripping the flag inside + * the injected fetch - after disposal every request (direct or auto-refresh, + * which shares this same captured fetch) throws before touching the network. + */ +describe('disposeBundle kill-switch', () => { + it('the injected fetch throws after disposeBundle', () => { + const hooks = makeSessionHooks({ + onSessionChange: jest.fn(), + getBundle: () => ({}) as SessionBundle, + getDid: () => DID, + }) + /* the injected fetch is the kill-switch wrapper makeSessionHooks bakes in */ + const injectedFetch = hooks.fetch! + + /* + * A live session is required for disposeBundle to act (it early-returns on + * a null/destroyed session). + */ + const session = new PasswordSession( + sessionAccountToSessionData(makeAccount()), + {...hooks}, + ) + const bundle = buildBundle(session) + registerBundleKillSwitch(bundle, hooks.kill) + + /* + * Before disposal the wrapper does NOT throw synchronously - it delegates + * to the async networkAwareFetch and returns a promise. Swallow that + * promise's rejection (the real network is unavailable under jest); we only + * care that no synchronous throw happened here. + */ + const pending = injectedFetch('https://bsky.social/xrpc/x') + expect(pending).toBeInstanceOf(Promise) + void pending.catch(() => {}) + + disposeBundle(bundle) + + /* after disposal every call through the injected fetch throws */ + expect(() => injectedFetch('https://bsky.social/xrpc/x')).toThrow( + 'session disposed', + ) + }) +}) + +/* + * PasswordSession lifecycle over a mocked fetch, covering the resume fast path + * and the hooks that makeSessionHooks maps into reducer events. + */ +describe('PasswordSession lifecycle over mocked fetch', () => { + it('resume fast path: constructing does not hit the network', () => { + const fetchMock = makeMockFetch() + /* not expired -> new PasswordSession(...) with no refresh */ + void new PasswordSession(sessionAccountToSessionData(makeAccount()), { + fetch: asFetch(fetchMock), + }) + expect(fetchMock.mock.calls.length).toBe(0) + }) + + it('resume network path fires onUpdated with fresh tokens', async () => { + const fetchMock = makeMockFetch() + const onUpdated = + jest.fn>() + const session = await PasswordSession.resume( + sessionAccountToSessionData(makeAccount()), + {fetch: asFetch(fetchMock), onUpdated}, + ) + expect(onUpdated).toHaveBeenCalledTimes(1) + expect(session.session.accessJwt).toBe('access-jwt-2') + }) + + it('onDeleted fires when refresh returns a declared invalid-token error', async () => { + const onDeleted = + jest.fn>() + const onUpdated = + jest.fn>() + const fetchMock = makeMockFetch({ + 'com.atproto.server.refreshSession': () => + new Response( + JSON.stringify({error: 'ExpiredToken', message: 'Token expired'}), + {status: 400, headers: {'content-type': 'application/json'}}, + ), + }) + const session = new PasswordSession( + sessionAccountToSessionData(makeAccount()), + {fetch: asFetch(fetchMock), onDeleted, onUpdated}, + ) + await expect(session.refresh()).rejects.toBeDefined() + expect(onDeleted).toHaveBeenCalledTimes(1) + expect(onUpdated).not.toHaveBeenCalled() + }) + + it('onUpdateFailure fires on a transient (500) refresh error, session preserved', async () => { + const onDeleted = + jest.fn>() + const onUpdateFailure = + jest.fn>() + const fetchMock = makeMockFetch({ + 'com.atproto.server.refreshSession': () => + new Response(JSON.stringify({error: 'InternalServerError'}), { + status: 500, + headers: {'content-type': 'application/json'}, + }), + }) + const session = new PasswordSession( + sessionAccountToSessionData(makeAccount()), + { + fetch: asFetch(fetchMock), + onDeleted, + onUpdateFailure, + }, + ) + await session.refresh() + expect(onUpdateFailure).toHaveBeenCalledTimes(1) + expect(onDeleted).not.toHaveBeenCalled() + /* session data is preserved (still the original tokens) */ + expect(session.session.accessJwt).toBe('access-jwt') + }) +}) + +/* + * A refresh through a bundle's armed hooks: the app has no `refreshSession` api + * of its own, so every rotation the reducer sees originates here, in + * `PasswordSession.refresh()`. On success the armed hooks dispatch exactly one + * 'update' event and the post-refresh snapshot carries the rotated tokens; + * rejections propagate to the caller. + */ +describe('PasswordSession.refresh through armed hooks', () => { + it('refresh() resolves updated data and the armed hooks dispatch exactly one update', async () => { + const fetchMock = makeMockFetch() + const onSessionChange = + jest.fn< + (bundle: SessionBundle, did: string, event: AtpSessionEvent) => void + >() + const bundle = {} as SessionBundle + const hooks = makeSessionHooks({ + onSessionChange, + getBundle: () => bundle, + getDid: () => DID, + }) + /* + * makeSessionHooks bakes in networkAwareFetch (the real global fetch); + * override it with the mock while keeping the arm-latched callbacks (they + * close over the same `armed` flag, so hooks.arm() below still applies). + */ + const session = new PasswordSession( + sessionAccountToSessionData(makeAccount()), + {...hooks, fetch: asFetch(fetchMock)}, + ) + hooks.arm() + + await session.refresh() + + /* the refreshed tokens are live on the session */ + expect(session.session.accessJwt).toBe('access-jwt-2') + /* exactly one 'update' event reached the reducer via the armed hooks */ + expect(onSessionChange).toHaveBeenCalledTimes(1) + expect(onSessionChange.mock.calls[0][2]).toBe('update') + + /* the post-refresh SessionAccount snapshot carries the rotated tokens */ + const snapshot = sessionDataToSessionAccount( + session.session, + session.session.service, + )! + expect(snapshot.accessJwt).toBe('access-jwt-2') + expect(snapshot.refreshJwt).toBe('refresh-jwt-2') + }) + + it('propagates a rejection from refresh() (invalid session)', async () => { + const fetchMock = makeMockFetch({ + 'com.atproto.server.refreshSession': () => + new Response( + JSON.stringify({error: 'ExpiredToken', message: 'Token expired'}), + {status: 400, headers: {'content-type': 'application/json'}}, + ), + }) + const session = new PasswordSession( + sessionAccountToSessionData(makeAccount()), + {fetch: asFetch(fetchMock)}, + ) + await expect(session.refresh()).rejects.toBeDefined() + }) +}) + +/** + * Load a fresh factory graph whose network leaf captures `fetch`. + * + * `session-core`'s network leaf reads `globalThis.fetch` at module load, and + * that captured fetch is what `PasswordSession`'s auto-refresh routes through, + * so the module has to be re-required after the override is in place. + */ +async function withFreshFactory( + fetch: typeof globalThis.fetch, + run: (core: typeof import('../session-core')) => Promise, +) { + const realFetch = globalThis.fetch + globalThis.fetch = fetch + try { + await jest.isolateModulesAsync(async () => { + const core = + require('../session-core') as typeof import('../session-core') + await run(core) + }) + } finally { + globalThis.fetch = realFetch + } +} + +/* + * The resume/login factories must snapshot the returned account after + * the prep awaits, not before. A 401 during prep triggers PasswordSession's + * internal auto-refresh (rotating BOTH tokens and firing an onUpdated the + * disarmed latch drops); an early snapshot would persist the stale refreshJwt, + * which is dead on the next cold start. + * + * We simulate the mid-prep rotation by capturing the bundle from the mocked + * `configureModerationForAccount` and making the mocked + * `prefetchAgeAssuranceServerData` (a genuine prep await in each factory) run a + * real `session.refresh()`. The factory itself is re-required inside + * `jest.isolateModulesAsync` AFTER overriding `globalThis.fetch`, because + * the network leaf captures `globalThis.fetch` at module load - and that + * captured fetch is what PasswordSession's auto-refresh routes through. + */ +describe('factory account snapshot after preparation', () => { + beforeEach(() => { + mockConfigureModerationForAccount.mockReset() + mockPrefetchAgeAssuranceServerData.mockReset() + }) + + it('resume: returned account carries the tokens rotated DURING prep', async () => { + /* + * A refresh mid-prep rotates the session to access-jwt-2/refresh-jwt-2. + * `valid-access-jwt` decodes as non-expired, so resume() takes the sync + * fast path and the only refresh is the one prep triggers. The bundle is + * captured from the (synchronous) moderation call, and the rotation is + * injected into the awaited AA prefetch. + */ + let capturedAgent: BskyAppAgent | undefined + mockConfigureModerationForAccount.mockImplementationOnce( + (agent: unknown) => { + capturedAgent = agent as BskyAppAgent + }, + ) + mockPrefetchAgeAssuranceServerData.mockImplementationOnce(async () => { + /* routes through the bridge into the shared PasswordSession's refresh */ + await capturedAgent!.sessionManager.refreshSession() + }) + const fetchMock = makeMockFetch() + + await withFreshFactory(asFetch(fetchMock), async core => { + const {account, bundle} = await core.createSessionBundleAndResume( + makeAccount({accessJwt: 'valid-access-jwt'}), + jest.fn(), + ) + /* the moderation prep step ran the refresh */ + expect(mockConfigureModerationForAccount).toHaveBeenCalledTimes(1) + /* the RETURNED account carries the POST-prep (rotated) tokens */ + expect(account.accessJwt).toBe('access-jwt-2') + expect(account.refreshJwt).toBe('refresh-jwt-2') + /* and it matches the session's committed state */ + expect(bundle.session.session.accessJwt).toBe('access-jwt-2') + }) + }) + + it('resume: returned account falls back to the stored account when the fast path yields no live token change', async () => { + /* + * With no mid-prep refresh, the snapshot still reflects the valid stored + * tokens. + */ + mockConfigureModerationForAccount.mockReturnValueOnce(undefined) + const fetchMock = makeMockFetch() + + await withFreshFactory(asFetch(fetchMock), async core => { + const {account} = await core.createSessionBundleAndResume( + makeAccount({accessJwt: 'valid-access-jwt'}), + jest.fn(), + ) + expect(account.accessJwt).toBe('valid-access-jwt') + expect(account.refreshJwt).toBe('refresh-jwt') + }) + }) +}) + +/* + * Preparation is the window between building the bundle and arming its hooks. + * The session is live but its events are swallowed, so a session that dies in + * here would otherwise leave the app holding a bundle that looks signed in and + * can only make unauthenticated requests, with nothing left to log it out. + * Both failure modes must therefore dispose the bundle and reject. + */ +describe('a session destroyed or rejected during preparation', () => { + beforeEach(() => { + mockConfigureModerationForAccount.mockReset() + mockPrefetchAgeAssuranceServerData.mockReset() + }) + + it('resume: rejects with the revoked-during-prep error and disposes the bundle', async () => { + /* + * A revoked refresh token: the session's own refresh during prep gets a + * declared invalid-token error, which destroys it. `logout()` is the only + * way to drive a session to `destroyed` from outside, and it takes the same + * `deleteSession` -> onDeleted -> destroyed path the real 401 rescue does. + */ + let capturedAgent: BskyAppAgent | undefined + mockConfigureModerationForAccount.mockImplementationOnce( + (agent: unknown) => { + capturedAgent = agent as BskyAppAgent + }, + ) + mockPrefetchAgeAssuranceServerData.mockImplementationOnce(async () => { + await capturedAgent!.logout() + }) + const fetchMock = makeMockFetch() + + await withFreshFactory(asFetch(fetchMock), async core => { + /* + * The clean error, not PasswordSession's opaque `Logged out` getter + * throw, which is what a naive `snapshot()` on a destroyed session would + * surface. + */ + await expect( + core.createSessionBundleAndResume( + makeAccount({accessJwt: 'valid-access-jwt'}), + jest.fn(), + ), + ).rejects.toThrow('Session was revoked while it was being prepared') + + /* the bundle the caller never received reads as logged out */ + expect(capturedAgent!.session).toBe(undefined) + }) + }) + + it('resume: a prep rejection propagates and disposes the bundle', async () => { + let capturedAgent: BskyAppAgent | undefined + mockConfigureModerationForAccount.mockImplementationOnce( + (agent: unknown) => { + capturedAgent = agent as BskyAppAgent + }, + ) + mockPrefetchAgeAssuranceServerData.mockImplementationOnce(() => + Promise.reject(new Error('prefetch blew up')), + ) + const fetchMock = makeMockFetch() + + await withFreshFactory(asFetch(fetchMock), async core => { + await expect( + core.createSessionBundleAndResume( + makeAccount({accessJwt: 'valid-access-jwt'}), + jest.fn(), + ), + ).rejects.toThrow('prefetch blew up') + + /* the still-live session was disposed rather than left refreshing */ + expect(capturedAgent!.session).toBe(undefined) + }) + }) + + it('finishPreparation disposes and rethrows without running the snapshot', async () => { + const hooks = makeSessionHooks({ + onSessionChange: jest.fn(), + getBundle: () => bundle, + getDid: () => DID, + }) + const session = new PasswordSession( + sessionAccountToSessionData(makeAccount()), + {...hooks, fetch: asFetch(makeMockFetch())}, + ) + const bundle = buildBundle(session) + registerBundleKillSwitch(bundle, hooks.kill) + const snapshot = jest.fn(() => 'never') + + await expect( + finishPreparation(bundle, Promise.reject(new Error('nope')), snapshot), + ).rejects.toThrow('nope') + + expect(snapshot).not.toHaveBeenCalled() + expect(bundle.agent.session).toBe(undefined) + }) +}) + +/* + * A hook must never throw: PasswordSession awaits its hooks inside the + * assignment to its internal session promise, so an escaping throw would leave + * that promise permanently rejected - every later request fails, while the + * session is never marked destroyed, so disposeBundle cannot even see that the + * bundle is dead. + */ +describe('a throwing onSessionChange does not brick the session', () => { + beforeEach(() => { + mockLoggerError.mockClear() + }) + + it('reports through logger.error and leaves the session usable', async () => { + const fetchMock = makeMockFetch() + const onSessionChange = jest.fn(() => { + throw new Error('reducer side effect exploded') + }) + let bundle!: SessionBundle + const hooks = makeSessionHooks({ + onSessionChange, + getBundle: () => bundle, + getDid: () => DID, + }) + const session = new PasswordSession( + sessionAccountToSessionData(makeAccount()), + {...hooks, fetch: asFetch(fetchMock)}, + ) + bundle = buildBundle(session) + hooks.arm() + + await session.refresh() + + expect(onSessionChange).toHaveBeenCalledTimes(1) + expect(mockLoggerError).toHaveBeenCalledTimes(1) + expect(mockLoggerError.mock.calls[0][1]).toEqual({ + message: "session: onSessionChange threw for a 'update' event", + }) + + /* the session still committed the rotation, and can still refresh again */ + expect(session.session.accessJwt).toBe('access-jwt-2') + await expect(session.refresh()).resolves.toBeDefined() + await expect( + session.fetchHandler('/xrpc/app.bsky.actor.getProfile', {}), + ).resolves.toBeDefined() + }) +}) diff --git a/src/state/session/__tests__/session-test.ts b/src/state/session/__tests__/session-test.ts index eebcfcf8d2..9cb2db6a59 100644 --- a/src/state/session/__tests__/session-test.ts +++ b/src/state/session/__tests__/session-test.ts @@ -1,8 +1,10 @@ -import {AtpAgent} from '@atproto/api' +import {type AtpAgent} from '@atproto/api' +import {type SessionData} from '@atproto/lex-password-session' import {describe, expect, it, jest} from '@jest/globals' -import {agentToSessionAccountOrThrow} from '../agent' import {type Action, getInitialState, reducer, type State} from '../reducer' +import {sessionDataToSessionAccount} from '../session-core' +import {type SessionAccount} from '../types' jest.mock('jwt-decode', () => ({ jwtDecode(_token: string) { @@ -20,6 +22,46 @@ jest.mock('#/lib/notifications/notifications', () => ({ return Promise.resolve() }, })) +/* + * The logout and account-removal reducer cases fire a push-token side effect + * whose first step, `createTemporaryAgentsAndResume`, builds real `AtpAgent`s + * and resumes them over the real network. Under jest that request outlives the + * suite: it rejects after teardown, and the resulting `logger.error` reaches + * for `nanoid` in an environment that no longer has it, failing whichever suite + * happens to be running at that moment. Stubbing the module keeps the side + * effect synchronous and offline. + */ +jest.mock('../util', () => ({ + createTemporaryAgentsAndResume: () => Promise.resolve([]), +})) + +// Reuse a bundle within each test: session events are scoped by bundle identity. +function makeBundle(service: string) { + return {service: new URL(service)} +} + +function makeAccount( + service: string, + session: { + active: boolean + did: string + handle: string + accessJwt: string + refreshJwt: string + email?: string + emailAuthFactor?: boolean + emailConfirmed?: boolean + }, +): SessionAccount { + const account = sessionDataToSessionAccount( + session as unknown as SessionData, + service, + ) + if (!account) { + throw new Error('Expected an account') + } + return account +} describe('session', () => { it('can log in and out', () => { @@ -27,8 +69,8 @@ describe('session', () => { expect(printState(state)).toMatchInlineSnapshot(` { "accounts": [], - "currentAgentState": { - "agent": { + "currentBundleState": { + "bundle": { "service": "https://public.api.bsky.app/", }, "did": undefined, @@ -37,22 +79,22 @@ describe('session', () => { } `) - const agent = new AtpAgent({service: 'https://alice.com'}) - agent.sessionManager.session = { + const aliceBundle = makeBundle('https://alice.com') + const aliceAccount = makeAccount('https://alice.com', { active: true, did: 'alice-did', handle: 'alice.test', accessJwt: 'alice-access-jwt-1', refreshJwt: 'alice-refresh-jwt-1', - } + }) state = run(state, [ { type: 'switched-to-account', - newAgent: agent, - newAccount: agentToSessionAccountOrThrow(agent), + newBundle: aliceBundle, + newAccount: aliceAccount, }, ]) - expect(state.currentAgentState.did).toBe('alice-did') + expect(state.currentBundleState.did).toBe('alice-did') expect(state.accounts.length).toBe(1) expect(state.accounts[0].did).toBe('alice-did') expect(state.accounts[0].accessJwt).toBe('alice-access-jwt-1') @@ -76,8 +118,8 @@ describe('session', () => { "status": undefined, }, ], - "currentAgentState": { - "agent": { + "currentBundleState": { + "bundle": { "service": "https://alice.com/", }, "did": "alice-did", @@ -92,7 +134,7 @@ describe('session', () => { }, ]) // Should keep the account but clear out the tokens. - expect(state.currentAgentState.did).toBe(undefined) + expect(state.currentBundleState.did).toBe(undefined) expect(state.accounts.length).toBe(1) expect(state.accounts[0].did).toBe('alice-did') expect(state.accounts[0].accessJwt).toBe(undefined) @@ -116,8 +158,8 @@ describe('session', () => { "status": undefined, }, ], - "currentAgentState": { - "agent": { + "currentBundleState": { + "bundle": { "service": "https://public.api.bsky.app/", }, "did": undefined, @@ -130,26 +172,26 @@ describe('session', () => { it('switches to the latest account, stores all of them', () => { let state = getInitialState([]) - const agent1 = new AtpAgent({service: 'https://alice.com'}) - agent1.sessionManager.session = { + const aliceBundle = makeBundle('https://alice.com') + const aliceAccount = makeAccount('https://alice.com', { active: true, did: 'alice-did', handle: 'alice.test', accessJwt: 'alice-access-jwt-1', refreshJwt: 'alice-refresh-jwt-1', - } + }) state = run(state, [ { // Switch to Alice. type: 'switched-to-account', - newAgent: agent1, - newAccount: agentToSessionAccountOrThrow(agent1), + newBundle: aliceBundle, + newAccount: aliceAccount, }, ]) expect(state.accounts.length).toBe(1) expect(state.accounts[0].did).toBe('alice-did') - expect(state.currentAgentState.did).toBe('alice-did') - expect(state.currentAgentState.agent).toBe(agent1) + expect(state.currentBundleState.did).toBe('alice-did') + expect(state.currentBundleState.bundle).toBe(aliceBundle) expect(printState(state)).toMatchInlineSnapshot(` { "accounts": [ @@ -169,8 +211,8 @@ describe('session', () => { "status": undefined, }, ], - "currentAgentState": { - "agent": { + "currentBundleState": { + "bundle": { "service": "https://alice.com/", }, "did": "alice-did", @@ -179,28 +221,28 @@ describe('session', () => { } `) - const agent2 = new AtpAgent({service: 'https://bob.com'}) - agent2.sessionManager.session = { + const bobBundle = makeBundle('https://bob.com') + const bobAccount = makeAccount('https://bob.com', { active: true, did: 'bob-did', handle: 'bob.test', accessJwt: 'bob-access-jwt-1', refreshJwt: 'bob-refresh-jwt-1', - } + }) state = run(state, [ { // Switch to Bob. type: 'switched-to-account', - newAgent: agent2, - newAccount: agentToSessionAccountOrThrow(agent2), + newBundle: bobBundle, + newAccount: bobAccount, }, ]) expect(state.accounts.length).toBe(2) // Bob should float upwards. expect(state.accounts[0].did).toBe('bob-did') expect(state.accounts[1].did).toBe('alice-did') - expect(state.currentAgentState.did).toBe('bob-did') - expect(state.currentAgentState.agent).toBe(agent2) + expect(state.currentBundleState.did).toBe('bob-did') + expect(state.currentBundleState.bundle).toBe(bobBundle) expect(printState(state)).toMatchInlineSnapshot(` { "accounts": [ @@ -235,8 +277,8 @@ describe('session', () => { "status": undefined, }, ], - "currentAgentState": { - "agent": { + "currentBundleState": { + "bundle": { "service": "https://bob.com/", }, "did": "bob-did", @@ -245,28 +287,28 @@ describe('session', () => { } `) - const agent3 = new AtpAgent({service: 'https://alice.com'}) - agent3.sessionManager.session = { + const aliceBundle2 = makeBundle('https://alice.com') + const aliceAccount2 = makeAccount('https://alice.com', { active: true, did: 'alice-did', handle: 'alice-updated.test', accessJwt: 'alice-access-jwt-2', refreshJwt: 'alice-refresh-jwt-2', - } + }) state = run(state, [ { // Switch back to Alice. type: 'switched-to-account', - newAgent: agent3, - newAccount: agentToSessionAccountOrThrow(agent3), + newBundle: aliceBundle2, + newAccount: aliceAccount2, }, ]) expect(state.accounts.length).toBe(2) // Alice should float upwards. expect(state.accounts[0].did).toBe('alice-did') expect(state.accounts[0].handle).toBe('alice-updated.test') - expect(state.currentAgentState.did).toBe('alice-did') - expect(state.currentAgentState.agent).toBe(agent3) + expect(state.currentBundleState.did).toBe('alice-did') + expect(state.currentBundleState.bundle).toBe(aliceBundle2) expect(printState(state)).toMatchInlineSnapshot(` { "accounts": [ @@ -301,8 +343,8 @@ describe('session', () => { "status": undefined, }, ], - "currentAgentState": { - "agent": { + "currentBundleState": { + "bundle": { "service": "https://alice.com/", }, "did": "alice-did", @@ -311,26 +353,26 @@ describe('session', () => { } `) - const agent4 = new AtpAgent({service: 'https://jay.com'}) - agent4.sessionManager.session = { + const jayBundle = makeBundle('https://jay.com') + const jayAccount = makeAccount('https://jay.com', { active: true, did: 'jay-did', handle: 'jay.test', accessJwt: 'jay-access-jwt-1', refreshJwt: 'jay-refresh-jwt-1', - } + }) state = run(state, [ { // Switch to Jay. type: 'switched-to-account', - newAgent: agent4, - newAccount: agentToSessionAccountOrThrow(agent4), + newBundle: jayBundle, + newAccount: jayAccount, }, ]) expect(state.accounts.length).toBe(3) expect(state.accounts[0].did).toBe('jay-did') - expect(state.currentAgentState.did).toBe('jay-did') - expect(state.currentAgentState.agent).toBe(agent4) + expect(state.currentBundleState.did).toBe('jay-did') + expect(state.currentBundleState.bundle).toBe(jayBundle) expect(printState(state)).toMatchInlineSnapshot(` { "accounts": [ @@ -380,8 +422,8 @@ describe('session', () => { "status": undefined, }, ], - "currentAgentState": { - "agent": { + "currentBundleState": { + "bundle": { "service": "https://jay.com/", }, "did": "jay-did", @@ -397,7 +439,7 @@ describe('session', () => { }, ]) expect(state.accounts.length).toBe(3) - expect(state.currentAgentState.did).toBe(undefined) + expect(state.currentBundleState.did).toBe(undefined) // All tokens should be gone. expect(state.accounts[0].accessJwt).toBe(undefined) expect(state.accounts[0].refreshJwt).toBe(undefined) @@ -454,8 +496,8 @@ describe('session', () => { "status": undefined, }, ], - "currentAgentState": { - "agent": { + "currentBundleState": { + "bundle": { "service": "https://public.api.bsky.app/", }, "did": undefined, @@ -468,25 +510,25 @@ describe('session', () => { it('can log back in after logging out', () => { let state = getInitialState([]) - const agent1 = new AtpAgent({service: 'https://alice.com'}) - agent1.sessionManager.session = { + const aliceBundle = makeBundle('https://alice.com') + const aliceAccount = makeAccount('https://alice.com', { active: true, did: 'alice-did', handle: 'alice.test', accessJwt: 'alice-access-jwt-1', refreshJwt: 'alice-refresh-jwt-1', - } + }) state = run(state, [ { type: 'switched-to-account', - newAgent: agent1, - newAccount: agentToSessionAccountOrThrow(agent1), + newBundle: aliceBundle, + newAccount: aliceAccount, }, ]) expect(state.accounts.length).toBe(1) expect(state.accounts[0].accessJwt).toBe('alice-access-jwt-1') expect(state.accounts[0].refreshJwt).toBe('alice-refresh-jwt-1') - expect(state.currentAgentState.did).toBe('alice-did') + expect(state.currentBundleState.did).toBe('alice-did') state = run(state, [ { @@ -496,7 +538,7 @@ describe('session', () => { expect(state.accounts.length).toBe(1) expect(state.accounts[0].accessJwt).toBe(undefined) expect(state.accounts[0].refreshJwt).toBe(undefined) - expect(state.currentAgentState.did).toBe(undefined) + expect(state.currentBundleState.did).toBe(undefined) expect(printState(state)).toMatchInlineSnapshot(` { "accounts": [ @@ -516,8 +558,8 @@ describe('session', () => { "status": undefined, }, ], - "currentAgentState": { - "agent": { + "currentBundleState": { + "bundle": { "service": "https://public.api.bsky.app/", }, "did": undefined, @@ -526,25 +568,25 @@ describe('session', () => { } `) - const agent2 = new AtpAgent({service: 'https://alice.com'}) - agent2.sessionManager.session = { + const aliceBundle2 = makeBundle('https://alice.com') + const aliceAccount2 = makeAccount('https://alice.com', { active: true, did: 'alice-did', handle: 'alice.test', accessJwt: 'alice-access-jwt-2', refreshJwt: 'alice-refresh-jwt-2', - } + }) state = run(state, [ { type: 'switched-to-account', - newAgent: agent2, - newAccount: agentToSessionAccountOrThrow(agent2), + newBundle: aliceBundle2, + newAccount: aliceAccount2, }, ]) expect(state.accounts.length).toBe(1) expect(state.accounts[0].accessJwt).toBe('alice-access-jwt-2') expect(state.accounts[0].refreshJwt).toBe('alice-refresh-jwt-2') - expect(state.currentAgentState.did).toBe('alice-did') + expect(state.currentBundleState.did).toBe('alice-did') expect(printState(state)).toMatchInlineSnapshot(` { "accounts": [ @@ -564,8 +606,8 @@ describe('session', () => { "status": undefined, }, ], - "currentAgentState": { - "agent": { + "currentBundleState": { + "bundle": { "service": "https://alice.com/", }, "did": "alice-did", @@ -578,25 +620,25 @@ describe('session', () => { it('can remove active account', () => { let state = getInitialState([]) - const agent1 = new AtpAgent({service: 'https://alice.com'}) - agent1.sessionManager.session = { + const aliceBundle = makeBundle('https://alice.com') + const aliceAccount = makeAccount('https://alice.com', { active: true, did: 'alice-did', handle: 'alice.test', accessJwt: 'alice-access-jwt-1', refreshJwt: 'alice-refresh-jwt-1', - } + }) state = run(state, [ { type: 'switched-to-account', - newAgent: agent1, - newAccount: agentToSessionAccountOrThrow(agent1), + newBundle: aliceBundle, + newAccount: aliceAccount, }, ]) expect(state.accounts.length).toBe(1) expect(state.accounts[0].accessJwt).toBe('alice-access-jwt-1') expect(state.accounts[0].refreshJwt).toBe('alice-refresh-jwt-1') - expect(state.currentAgentState.did).toBe('alice-did') + expect(state.currentBundleState.did).toBe('alice-did') state = run(state, [ { @@ -605,12 +647,12 @@ describe('session', () => { }, ]) expect(state.accounts.length).toBe(0) - expect(state.currentAgentState.did).toBe(undefined) + expect(state.currentBundleState.did).toBe(undefined) expect(printState(state)).toMatchInlineSnapshot(` { "accounts": [], - "currentAgentState": { - "agent": { + "currentBundleState": { + "bundle": { "service": "https://public.api.bsky.app/", }, "did": undefined, @@ -623,36 +665,36 @@ describe('session', () => { it('can remove inactive account', () => { let state = getInitialState([]) - const agent1 = new AtpAgent({service: 'https://alice.com'}) - agent1.sessionManager.session = { + const aliceBundle = makeBundle('https://alice.com') + const aliceAccount = makeAccount('https://alice.com', { active: true, did: 'alice-did', handle: 'alice.test', accessJwt: 'alice-access-jwt-1', refreshJwt: 'alice-refresh-jwt-1', - } - const agent2 = new AtpAgent({service: 'https://bob.com'}) - agent2.sessionManager.session = { + }) + const bobBundle = makeBundle('https://bob.com') + const bobAccount = makeAccount('https://bob.com', { active: true, did: 'bob-did', handle: 'bob.test', accessJwt: 'bob-access-jwt-1', refreshJwt: 'bob-refresh-jwt-1', - } + }) state = run(state, [ { type: 'switched-to-account', - newAgent: agent1, - newAccount: agentToSessionAccountOrThrow(agent1), + newBundle: aliceBundle, + newAccount: aliceAccount, }, { type: 'switched-to-account', - newAgent: agent2, - newAccount: agentToSessionAccountOrThrow(agent2), + newBundle: bobBundle, + newAccount: bobAccount, }, ]) expect(state.accounts.length).toBe(2) - expect(state.currentAgentState.did).toBe('bob-did') + expect(state.currentBundleState.did).toBe('bob-did') state = run(state, [ { @@ -661,7 +703,7 @@ describe('session', () => { }, ]) expect(state.accounts.length).toBe(1) - expect(state.currentAgentState.did).toBe('bob-did') + expect(state.currentBundleState.did).toBe('bob-did') expect(printState(state)).toMatchInlineSnapshot(` { "accounts": [ @@ -681,8 +723,8 @@ describe('session', () => { "status": undefined, }, ], - "currentAgentState": { - "agent": { + "currentBundleState": { + "bundle": { "service": "https://bob.com/", }, "did": "bob-did", @@ -698,51 +740,51 @@ describe('session', () => { }, ]) expect(state.accounts.length).toBe(0) - expect(state.currentAgentState.did).toBe(undefined) + expect(state.currentBundleState.did).toBe(undefined) }) it('can log out of the current account', () => { let state = getInitialState([]) - const agent1 = new AtpAgent({service: 'https://alice.com'}) - agent1.sessionManager.session = { + const aliceBundle = makeBundle('https://alice.com') + const aliceAccount = makeAccount('https://alice.com', { active: true, did: 'alice-did', handle: 'alice.test', accessJwt: 'alice-access-jwt-1', refreshJwt: 'alice-refresh-jwt-1', - } + }) state = run(state, [ { type: 'switched-to-account', - newAgent: agent1, - newAccount: agentToSessionAccountOrThrow(agent1), + newBundle: aliceBundle, + newAccount: aliceAccount, }, ]) expect(state.accounts.length).toBe(1) expect(state.accounts[0].accessJwt).toBe('alice-access-jwt-1') expect(state.accounts[0].refreshJwt).toBe('alice-refresh-jwt-1') - expect(state.currentAgentState.did).toBe('alice-did') + expect(state.currentBundleState.did).toBe('alice-did') - const agent2 = new AtpAgent({service: 'https://bob.com'}) - agent2.sessionManager.session = { + const bobBundle = makeBundle('https://bob.com') + const bobAccount = makeAccount('https://bob.com', { active: true, did: 'bob-did', handle: 'bob.test', accessJwt: 'bob-access-jwt-1', refreshJwt: 'bob-refresh-jwt-1', - } + }) state = run(state, [ { type: 'switched-to-account', - newAgent: agent2, - newAccount: agentToSessionAccountOrThrow(agent2), + newBundle: bobBundle, + newAccount: bobAccount, }, ]) expect(state.accounts.length).toBe(2) expect(state.accounts[0].accessJwt).toBe('bob-access-jwt-1') expect(state.accounts[0].refreshJwt).toBe('bob-refresh-jwt-1') - expect(state.currentAgentState.did).toBe('bob-did') + expect(state.currentBundleState.did).toBe('bob-did') state = run(state, [ { @@ -754,7 +796,7 @@ describe('session', () => { expect(state.accounts[0].refreshJwt).toBe(undefined) expect(state.accounts[1].accessJwt).toBe('alice-access-jwt-1') expect(state.accounts[1].refreshJwt).toBe('alice-refresh-jwt-1') - expect(state.currentAgentState.did).toBe(undefined) + expect(state.currentBundleState.did).toBe(undefined) expect(printState(state)).toMatchInlineSnapshot(` { "accounts": [ @@ -789,8 +831,8 @@ describe('session', () => { "status": undefined, }, ], - "currentAgentState": { - "agent": { + "currentBundleState": { + "bundle": { "service": "https://public.api.bsky.app/", }, "did": undefined, @@ -803,40 +845,38 @@ describe('session', () => { it('updates stored account with refreshed tokens', () => { let state = getInitialState([]) - const agent1 = new AtpAgent({service: 'https://alice.com'}) - agent1.sessionManager.session = { - active: true, - did: 'alice-did', - handle: 'alice.test', - accessJwt: 'alice-access-jwt-1', - refreshJwt: 'alice-refresh-jwt-1', - } + const aliceBundle = makeBundle('https://alice.com') state = run(state, [ { type: 'switched-to-account', - newAgent: agent1, - newAccount: agentToSessionAccountOrThrow(agent1), + newBundle: aliceBundle, + newAccount: makeAccount('https://alice.com', { + active: true, + did: 'alice-did', + handle: 'alice.test', + accessJwt: 'alice-access-jwt-1', + refreshJwt: 'alice-refresh-jwt-1', + }), }, ]) expect(state.accounts.length).toBe(1) - expect(state.currentAgentState.did).toBe('alice-did') + expect(state.currentBundleState.did).toBe('alice-did') - agent1.sessionManager.session = { - active: true, - did: 'alice-did', - handle: 'alice-updated.test', - accessJwt: 'alice-access-jwt-2', - refreshJwt: 'alice-refresh-jwt-2', - email: 'alice@foo.bar', - emailAuthFactor: false, - emailConfirmed: false, - } state = run(state, [ { - type: 'received-agent-event', + type: 'received-session-event', accountDid: 'alice-did', - agent: agent1, - refreshedAccount: agentToSessionAccountOrThrow(agent1), + bundle: aliceBundle, + refreshedAccount: makeAccount('https://alice.com', { + active: true, + did: 'alice-did', + handle: 'alice-updated.test', + accessJwt: 'alice-access-jwt-2', + refreshJwt: 'alice-refresh-jwt-2', + email: 'alice@foo.bar', + emailAuthFactor: false, + emailConfirmed: false, + }), sessionEvent: 'update', }, ]) @@ -845,7 +885,7 @@ describe('session', () => { expect(state.accounts[0].handle).toBe('alice-updated.test') expect(state.accounts[0].accessJwt).toBe('alice-access-jwt-2') expect(state.accounts[0].refreshJwt).toBe('alice-refresh-jwt-2') - expect(state.currentAgentState.did).toBe('alice-did') + expect(state.currentBundleState.did).toBe('alice-did') expect(printState(state)).toMatchInlineSnapshot(` { "accounts": [ @@ -865,8 +905,8 @@ describe('session', () => { "status": undefined, }, ], - "currentAgentState": { - "agent": { + "currentBundleState": { + "bundle": { "service": "https://alice.com/", }, "did": "alice-did", @@ -875,22 +915,21 @@ describe('session', () => { } `) - agent1.sessionManager.session = { - active: true, - did: 'alice-did', - handle: 'alice-updated.test', - accessJwt: 'alice-access-jwt-3', - refreshJwt: 'alice-refresh-jwt-3', - email: 'alice@foo.baz', - emailAuthFactor: true, - emailConfirmed: true, - } state = run(state, [ { - type: 'received-agent-event', + type: 'received-session-event', accountDid: 'alice-did', - agent: agent1, - refreshedAccount: agentToSessionAccountOrThrow(agent1), + bundle: aliceBundle, + refreshedAccount: makeAccount('https://alice.com', { + active: true, + did: 'alice-did', + handle: 'alice-updated.test', + accessJwt: 'alice-access-jwt-3', + refreshJwt: 'alice-refresh-jwt-3', + email: 'alice@foo.baz', + emailAuthFactor: true, + emailConfirmed: true, + }), sessionEvent: 'update', }, ]) @@ -899,7 +938,7 @@ describe('session', () => { expect(state.accounts[0].handle).toBe('alice-updated.test') expect(state.accounts[0].accessJwt).toBe('alice-access-jwt-3') expect(state.accounts[0].refreshJwt).toBe('alice-refresh-jwt-3') - expect(state.currentAgentState.did).toBe('alice-did') + expect(state.currentBundleState.did).toBe('alice-did') expect(printState(state)).toMatchInlineSnapshot(` { "accounts": [ @@ -919,8 +958,8 @@ describe('session', () => { "status": undefined, }, ], - "currentAgentState": { - "agent": { + "currentBundleState": { + "bundle": { "service": "https://alice.com/", }, "did": "alice-did", @@ -929,22 +968,21 @@ describe('session', () => { } `) - agent1.sessionManager.session = { - active: true, - did: 'alice-did', - handle: 'alice-updated.test', - accessJwt: 'alice-access-jwt-4', - refreshJwt: 'alice-refresh-jwt-4', - email: 'alice@foo.baz', - emailAuthFactor: false, - emailConfirmed: false, - } state = run(state, [ { - type: 'received-agent-event', + type: 'received-session-event', accountDid: 'alice-did', - agent: agent1, - refreshedAccount: agentToSessionAccountOrThrow(agent1), + bundle: aliceBundle, + refreshedAccount: makeAccount('https://alice.com', { + active: true, + did: 'alice-did', + handle: 'alice-updated.test', + accessJwt: 'alice-access-jwt-4', + refreshJwt: 'alice-refresh-jwt-4', + email: 'alice@foo.baz', + emailAuthFactor: false, + emailConfirmed: false, + }), sessionEvent: 'update', }, ]) @@ -953,7 +991,7 @@ describe('session', () => { expect(state.accounts[0].handle).toBe('alice-updated.test') expect(state.accounts[0].accessJwt).toBe('alice-access-jwt-4') expect(state.accounts[0].refreshJwt).toBe('alice-refresh-jwt-4') - expect(state.currentAgentState.did).toBe('alice-did') + expect(state.currentBundleState.did).toBe('alice-did') expect(printState(state)).toMatchInlineSnapshot(` { "accounts": [ @@ -973,8 +1011,8 @@ describe('session', () => { "status": undefined, }, ], - "currentAgentState": { - "agent": { + "currentBundleState": { + "bundle": { "service": "https://alice.com/", }, "did": "alice-did", @@ -987,37 +1025,35 @@ describe('session', () => { it('bails out of update on identical objects', () => { let state = getInitialState([]) - const agent1 = new AtpAgent({service: 'https://alice.com'}) - agent1.sessionManager.session = { - active: true, - did: 'alice-did', - handle: 'alice.test', - accessJwt: 'alice-access-jwt-1', - refreshJwt: 'alice-refresh-jwt-1', - } + const aliceBundle = makeBundle('https://alice.com') state = run(state, [ { type: 'switched-to-account', - newAgent: agent1, - newAccount: agentToSessionAccountOrThrow(agent1), + newBundle: aliceBundle, + newAccount: makeAccount('https://alice.com', { + active: true, + did: 'alice-did', + handle: 'alice.test', + accessJwt: 'alice-access-jwt-1', + refreshJwt: 'alice-refresh-jwt-1', + }), }, ]) expect(state.accounts.length).toBe(1) - expect(state.currentAgentState.did).toBe('alice-did') + expect(state.currentBundleState.did).toBe('alice-did') - agent1.sessionManager.session = { - active: true, - did: 'alice-did', - handle: 'alice-updated.test', - accessJwt: 'alice-access-jwt-2', - refreshJwt: 'alice-refresh-jwt-2', - } state = run(state, [ { - type: 'received-agent-event', + type: 'received-session-event', accountDid: 'alice-did', - agent: agent1, - refreshedAccount: agentToSessionAccountOrThrow(agent1), + bundle: aliceBundle, + refreshedAccount: makeAccount('https://alice.com', { + active: true, + did: 'alice-did', + handle: 'alice-updated.test', + accessJwt: 'alice-access-jwt-2', + refreshJwt: 'alice-refresh-jwt-2', + }), sessionEvent: 'update', }, ]) @@ -1027,28 +1063,33 @@ describe('session', () => { const lastState = state state = run(state, [ { - type: 'received-agent-event', + type: 'received-session-event', accountDid: 'alice-did', - agent: agent1, - refreshedAccount: agentToSessionAccountOrThrow(agent1), + bundle: aliceBundle, + refreshedAccount: makeAccount('https://alice.com', { + active: true, + did: 'alice-did', + handle: 'alice-updated.test', + accessJwt: 'alice-access-jwt-2', + refreshJwt: 'alice-refresh-jwt-2', + }), sessionEvent: 'update', }, ]) expect(lastState === state).toBe(true) - agent1.sessionManager.session = { - active: true, - did: 'alice-did', - handle: 'alice-updated.test', - accessJwt: 'alice-access-jwt-3', - refreshJwt: 'alice-refresh-jwt-3', - } state = run(state, [ { - type: 'received-agent-event', + type: 'received-session-event', accountDid: 'alice-did', - agent: agent1, - refreshedAccount: agentToSessionAccountOrThrow(agent1), + bundle: aliceBundle, + refreshedAccount: makeAccount('https://alice.com', { + active: true, + did: 'alice-did', + handle: 'alice-updated.test', + accessJwt: 'alice-access-jwt-3', + refreshJwt: 'alice-refresh-jwt-3', + }), sessionEvent: 'update', }, ]) @@ -1056,127 +1097,86 @@ describe('session', () => { expect(state.accounts[0].accessJwt).toBe('alice-access-jwt-3') }) - it('accepts updates from a stale agent', () => { + it('ignores updates from a stale bundle', () => { let state = getInitialState([]) - const agent1 = new AtpAgent({service: 'https://alice.com'}) - agent1.sessionManager.session = { - active: true, - did: 'alice-did', - handle: 'alice.test', - accessJwt: 'alice-access-jwt-1', - refreshJwt: 'alice-refresh-jwt-1', - } - - const agent2 = new AtpAgent({service: 'https://bob.com'}) - agent2.sessionManager.session = { - active: true, - did: 'bob-did', - handle: 'bob.test', - accessJwt: 'bob-access-jwt-1', - refreshJwt: 'bob-refresh-jwt-1', - } + const aliceBundle = makeBundle('https://alice.com') + const bobBundle = makeBundle('https://bob.com') state = run(state, [ { // Switch to Alice. type: 'switched-to-account', - newAgent: agent1, - newAccount: agentToSessionAccountOrThrow(agent1), + newBundle: aliceBundle, + newAccount: makeAccount('https://alice.com', { + active: true, + did: 'alice-did', + handle: 'alice.test', + accessJwt: 'alice-access-jwt-1', + refreshJwt: 'alice-refresh-jwt-1', + }), }, { // Switch to Bob. type: 'switched-to-account', - newAgent: agent2, - newAccount: agentToSessionAccountOrThrow(agent2), + newBundle: bobBundle, + newAccount: makeAccount('https://bob.com', { + active: true, + did: 'bob-did', + handle: 'bob.test', + accessJwt: 'bob-access-jwt-1', + refreshJwt: 'bob-refresh-jwt-1', + }), }, ]) expect(state.accounts.length).toBe(2) - expect(state.currentAgentState.did).toBe('bob-did') + expect(state.currentBundleState.did).toBe('bob-did') - agent1.sessionManager.session = { - active: true, - did: 'alice-did', - handle: 'alice-updated.test', - accessJwt: 'alice-access-jwt-2', - refreshJwt: 'alice-refresh-jwt-2', - email: 'alice@foo.bar', - emailAuthFactor: false, - emailConfirmed: false, - } + /* + * An 'update' from the stale (background) Alice bundle is dropped + * ENTIRELY - identical state object, no token write. A refresh completing + * after switching away must not resurrect fresh tokens into the + * switched-away account entry. + */ + const beforeStaleUpdate = state state = run(state, [ { - type: 'received-agent-event', + type: 'received-session-event', accountDid: 'alice-did', - agent: agent1, - refreshedAccount: agentToSessionAccountOrThrow(agent1), + bundle: aliceBundle, + refreshedAccount: makeAccount('https://alice.com', { + active: true, + did: 'alice-did', + handle: 'alice-updated.test', + accessJwt: 'alice-access-jwt-2', + refreshJwt: 'alice-refresh-jwt-2', + email: 'alice@foo.bar', + emailAuthFactor: false, + emailConfirmed: false, + }), sessionEvent: 'update', }, ]) - expect(state.accounts.length).toBe(2) + expect(beforeStaleUpdate === state).toBe(true) expect(state.accounts[1].did).toBe('alice-did') - // Should update Alice's tokens because otherwise they'll be stale. - expect(state.accounts[1].handle).toBe('alice-updated.test') - expect(state.accounts[1].accessJwt).toBe('alice-access-jwt-2') - expect(state.accounts[1].refreshJwt).toBe('alice-refresh-jwt-2') - expect(printState(state)).toMatchInlineSnapshot(` - { - "accounts": [ - { - "accessJwt": "bob-access-jwt-1", - "active": true, - "did": "bob-did", - "email": undefined, - "emailAuthFactor": false, - "emailConfirmed": false, - "handle": "bob.test", - "isSelfHosted": true, - "pdsUrl": undefined, - "refreshJwt": "bob-refresh-jwt-1", - "service": "https://bob.com/", - "signupQueued": false, - "status": undefined, - }, - { - "accessJwt": "alice-access-jwt-2", - "active": true, - "did": "alice-did", - "email": "alice@foo.bar", - "emailAuthFactor": false, - "emailConfirmed": false, - "handle": "alice-updated.test", - "isSelfHosted": true, - "pdsUrl": undefined, - "refreshJwt": "alice-refresh-jwt-2", - "service": "https://alice.com/", - "signupQueued": false, - "status": undefined, - }, - ], - "currentAgentState": { - "agent": { - "service": "https://bob.com/", - }, - "did": "bob-did", - }, - "needsPersist": true, - } - `) + // Alice's stored tokens are untouched (the stale update did not land). + expect(state.accounts[1].handle).toBe('alice.test') + expect(state.accounts[1].accessJwt).toBe('alice-access-jwt-1') + expect(state.accounts[1].refreshJwt).toBe('alice-refresh-jwt-1') - agent2.sessionManager.session = { - active: true, - did: 'bob-did', - handle: 'bob-updated.test', - accessJwt: 'bob-access-jwt-2', - refreshJwt: 'bob-refresh-jwt-2', - } state = run(state, [ { - // Update Bob. - type: 'received-agent-event', + // Update Bob (the current bundle) - this still applies. + type: 'received-session-event', accountDid: 'bob-did', - agent: agent2, - refreshedAccount: agentToSessionAccountOrThrow(agent2), + bundle: bobBundle, + refreshedAccount: makeAccount('https://bob.com', { + active: true, + did: 'bob-did', + handle: 'bob-updated.test', + accessJwt: 'bob-access-jwt-2', + refreshJwt: 'bob-refresh-jwt-2', + }), sessionEvent: 'update', }, ]) @@ -1205,23 +1205,23 @@ describe('session', () => { "status": undefined, }, { - "accessJwt": "alice-access-jwt-2", + "accessJwt": "alice-access-jwt-1", "active": true, "did": "alice-did", - "email": "alice@foo.bar", + "email": undefined, "emailAuthFactor": false, "emailConfirmed": false, - "handle": "alice-updated.test", + "handle": "alice.test", "isSelfHosted": true, "pdsUrl": undefined, - "refreshJwt": "alice-refresh-jwt-2", + "refreshJwt": "alice-refresh-jwt-1", "service": "https://alice.com/", "signupQueued": false, "status": undefined, }, ], - "currentAgentState": { - "agent": { + "currentBundleState": { + "bundle": { "service": "https://bob.com/", }, "did": "bob-did", @@ -1230,14 +1230,13 @@ describe('session', () => { } `) - // Ignore other events for inactive agent. + // Ignore other events for the inactive bundle too (network-error, expired). const lastState = state - agent1.sessionManager.session = undefined state = run(state, [ { - type: 'received-agent-event', + type: 'received-session-event', accountDid: 'alice-did', - agent: agent1, + bundle: aliceBundle, refreshedAccount: undefined, sessionEvent: 'network-error', }, @@ -1245,9 +1244,9 @@ describe('session', () => { expect(lastState === state).toBe(true) state = run(state, [ { - type: 'received-agent-event', + type: 'received-session-event', accountDid: 'alice-did', - agent: agent1, + bundle: aliceBundle, refreshedAccount: undefined, sessionEvent: 'expired', }, @@ -1255,37 +1254,128 @@ describe('session', () => { expect(lastState === state).toBe(true) }) - it('ignores updates from a removed agent', () => { + it('drops an update from a stale bundle even when its account entry still exists (no resurrection)', () => { let state = getInitialState([]) - const agent1 = new AtpAgent({service: 'https://alice.com'}) - agent1.sessionManager.session = { - active: true, - did: 'alice-did', - handle: 'alice.test', - accessJwt: 'alice-access-jwt-1', - refreshJwt: 'alice-refresh-jwt-1', - } + const aliceBundle = makeBundle('https://alice.com') + state = run(state, [ + { + type: 'switched-to-account', + newBundle: aliceBundle, + newAccount: makeAccount('https://alice.com', { + active: true, + did: 'alice-did', + handle: 'alice.test', + accessJwt: 'alice-access-jwt-1', + refreshJwt: 'alice-refresh-jwt-1', + }), + }, + ]) + expect(state.currentBundleState.did).toBe('alice-did') - const agent2 = new AtpAgent({service: 'https://bob.com'}) - agent2.sessionManager.session = { - active: true, - did: 'bob-did', - handle: 'bob.test', - accessJwt: 'bob-access-jwt-1', - refreshJwt: 'bob-refresh-jwt-1', - } + // Alice logs out: her account entry stays, but tokens are cleared and the + // current bundle becomes the public (logged-out) bundle. + state = run(state, [{type: 'logged-out-current-account'}]) + expect(state.currentBundleState.did).toBe(undefined) + expect(state.accounts[0].did).toBe('alice-did') + expect(state.accounts[0].accessJwt).toBe(undefined) + expect(state.accounts[0].refreshJwt).toBe(undefined) + + /* + * A refresh that was already in flight on the (now stale) Alice bundle + * completes and delivers fresh tokens. It must NOT resurrect them into the + * soft-logged-out account entry - the bundle no longer matches the current + * (public) bundle, so the event is dropped entirely. + */ + const afterLogout = state + state = run(state, [ + { + type: 'received-session-event', + accountDid: 'alice-did', + bundle: aliceBundle, + refreshedAccount: makeAccount('https://alice.com', { + active: true, + did: 'alice-did', + handle: 'alice.test', + accessJwt: 'alice-access-jwt-2', + refreshJwt: 'alice-refresh-jwt-2', + }), + sessionEvent: 'update', + }, + ]) + expect(afterLogout === state).toBe(true) + expect(state.accounts[0].accessJwt).toBe(undefined) + expect(state.accounts[0].refreshJwt).toBe(undefined) + expect(state.currentBundleState.did).toBe(undefined) + }) + + it('applies an update from the current bundle', () => { + let state = getInitialState([]) + + const aliceBundle = makeBundle('https://alice.com') + state = run(state, [ + { + type: 'switched-to-account', + newBundle: aliceBundle, + newAccount: makeAccount('https://alice.com', { + active: true, + did: 'alice-did', + handle: 'alice.test', + accessJwt: 'alice-access-jwt-1', + refreshJwt: 'alice-refresh-jwt-1', + }), + }, + ]) + + state = run(state, [ + { + type: 'received-session-event', + accountDid: 'alice-did', + bundle: aliceBundle, + refreshedAccount: makeAccount('https://alice.com', { + active: true, + did: 'alice-did', + handle: 'alice.test', + accessJwt: 'alice-access-jwt-2', + refreshJwt: 'alice-refresh-jwt-2', + }), + sessionEvent: 'update', + }, + ]) + // The current bundle's update lands and rotates the stored tokens. + expect(state.accounts[0].accessJwt).toBe('alice-access-jwt-2') + expect(state.accounts[0].refreshJwt).toBe('alice-refresh-jwt-2') + expect(state.currentBundleState.did).toBe('alice-did') + }) + + it('ignores updates from a removed bundle', () => { + let state = getInitialState([]) + + const aliceBundle = makeBundle('https://alice.com') + const bobBundle = makeBundle('https://bob.com') state = run(state, [ { type: 'switched-to-account', - newAgent: agent1, - newAccount: agentToSessionAccountOrThrow(agent1), + newBundle: aliceBundle, + newAccount: makeAccount('https://alice.com', { + active: true, + did: 'alice-did', + handle: 'alice.test', + accessJwt: 'alice-access-jwt-1', + refreshJwt: 'alice-refresh-jwt-1', + }), }, { type: 'switched-to-account', - newAgent: agent2, - newAccount: agentToSessionAccountOrThrow(agent2), + newBundle: bobBundle, + newAccount: makeAccount('https://bob.com', { + active: true, + did: 'bob-did', + handle: 'bob.test', + accessJwt: 'bob-access-jwt-1', + refreshJwt: 'bob-refresh-jwt-1', + }), }, { type: 'removed-account', @@ -1293,58 +1383,55 @@ describe('session', () => { }, ]) expect(state.accounts.length).toBe(1) - expect(state.currentAgentState.did).toBe('bob-did') + expect(state.currentBundleState.did).toBe('bob-did') - agent1.sessionManager.session = { - active: true, - did: 'alice-did', - handle: 'alice.test', - accessJwt: 'alice-access-jwt-2', - refreshJwt: 'alice-refresh-jwt-2', - } state = run(state, [ { - type: 'received-agent-event', + type: 'received-session-event', accountDid: 'alice-did', - agent: agent1, - refreshedAccount: agentToSessionAccountOrThrow(agent1), + bundle: aliceBundle, + refreshedAccount: makeAccount('https://alice.com', { + active: true, + did: 'alice-did', + handle: 'alice.test', + accessJwt: 'alice-access-jwt-2', + refreshJwt: 'alice-refresh-jwt-2', + }), sessionEvent: 'update', }, ]) expect(state.accounts.length).toBe(1) expect(state.accounts[0].did).toBe('bob-did') expect(state.accounts[0].accessJwt).toBe('bob-access-jwt-1') - expect(state.currentAgentState.did).toBe('bob-did') + expect(state.currentBundleState.did).toBe('bob-did') }) it('ignores network errors', () => { let state = getInitialState([]) - const agent1 = new AtpAgent({service: 'https://alice.com'}) - agent1.sessionManager.session = { - active: true, - did: 'alice-did', - handle: 'alice.test', - accessJwt: 'alice-access-jwt-1', - refreshJwt: 'alice-refresh-jwt-1', - } + const aliceBundle = makeBundle('https://alice.com') state = run(state, [ { // Switch to Alice. type: 'switched-to-account', - newAgent: agent1, - newAccount: agentToSessionAccountOrThrow(agent1), + newBundle: aliceBundle, + newAccount: makeAccount('https://alice.com', { + active: true, + did: 'alice-did', + handle: 'alice.test', + accessJwt: 'alice-access-jwt-1', + refreshJwt: 'alice-refresh-jwt-1', + }), }, ]) expect(state.accounts.length).toBe(1) - expect(state.currentAgentState.did).toBe('alice-did') + expect(state.currentBundleState.did).toBe('alice-did') - agent1.sessionManager.session = undefined state = run(state, [ { - type: 'received-agent-event', + type: 'received-session-event', accountDid: 'alice-did', - agent: agent1, + bundle: aliceBundle, refreshedAccount: undefined, sessionEvent: 'network-error', }, @@ -1352,7 +1439,7 @@ describe('session', () => { expect(state.accounts.length).toBe(1) expect(state.accounts[0].accessJwt).toBe('alice-access-jwt-1') expect(state.accounts[0].refreshJwt).toBe('alice-refresh-jwt-1') - expect(state.currentAgentState.did).toBe('alice-did') + expect(state.currentBundleState.did).toBe('alice-did') expect(printState(state)).toMatchInlineSnapshot(` { "accounts": [ @@ -1372,8 +1459,8 @@ describe('session', () => { "status": undefined, }, ], - "currentAgentState": { - "agent": { + "currentBundleState": { + "bundle": { "service": "https://alice.com/", }, "did": "alice-did", @@ -1386,31 +1473,29 @@ describe('session', () => { it('resets tokens on expired event', () => { let state = getInitialState([]) - const agent1 = new AtpAgent({service: 'https://alice.com'}) - agent1.sessionManager.session = { - active: true, - did: 'alice-did', - handle: 'alice.test', - accessJwt: 'alice-access-jwt-1', - refreshJwt: 'alice-refresh-jwt-1', - } + const aliceBundle = makeBundle('https://alice.com') state = run(state, [ { type: 'switched-to-account', - newAgent: agent1, - newAccount: agentToSessionAccountOrThrow(agent1), + newBundle: aliceBundle, + newAccount: makeAccount('https://alice.com', { + active: true, + did: 'alice-did', + handle: 'alice.test', + accessJwt: 'alice-access-jwt-1', + refreshJwt: 'alice-refresh-jwt-1', + }), }, ]) expect(state.accounts.length).toBe(1) expect(state.accounts[0].accessJwt).toBe('alice-access-jwt-1') - expect(state.currentAgentState.did).toBe('alice-did') + expect(state.currentBundleState.did).toBe('alice-did') - agent1.sessionManager.session = undefined state = run(state, [ { - type: 'received-agent-event', + type: 'received-session-event', accountDid: 'alice-did', - agent: agent1, + bundle: aliceBundle, refreshedAccount: undefined, sessionEvent: 'expired', }, @@ -1418,7 +1503,7 @@ describe('session', () => { expect(state.accounts.length).toBe(1) expect(state.accounts[0].accessJwt).toBe(undefined) expect(state.accounts[0].refreshJwt).toBe(undefined) - expect(state.currentAgentState.did).toBe(undefined) + expect(state.currentBundleState.did).toBe(undefined) expect(printState(state)).toMatchInlineSnapshot(` { "accounts": [ @@ -1438,74 +1523,8 @@ describe('session', () => { "status": undefined, }, ], - "currentAgentState": { - "agent": { - "service": "https://public.api.bsky.app/", - }, - "did": undefined, - }, - "needsPersist": true, - } - `) - }) - - it('resets tokens on created-failed event', () => { - let state = getInitialState([]) - - const agent1 = new AtpAgent({service: 'https://alice.com'}) - agent1.sessionManager.session = { - active: true, - did: 'alice-did', - handle: 'alice.test', - accessJwt: 'alice-access-jwt-1', - refreshJwt: 'alice-refresh-jwt-1', - } - state = run(state, [ - { - type: 'switched-to-account', - newAgent: agent1, - newAccount: agentToSessionAccountOrThrow(agent1), - }, - ]) - expect(state.accounts.length).toBe(1) - expect(state.accounts[0].accessJwt).toBe('alice-access-jwt-1') - expect(state.currentAgentState.did).toBe('alice-did') - - agent1.sessionManager.session = undefined - state = run(state, [ - { - type: 'received-agent-event', - accountDid: 'alice-did', - agent: agent1, - refreshedAccount: undefined, - sessionEvent: 'create-failed', - }, - ]) - expect(state.accounts.length).toBe(1) - expect(state.accounts[0].accessJwt).toBe(undefined) - expect(state.accounts[0].refreshJwt).toBe(undefined) - expect(state.currentAgentState.did).toBe(undefined) - expect(printState(state)).toMatchInlineSnapshot(` - { - "accounts": [ - { - "accessJwt": undefined, - "active": true, - "did": "alice-did", - "email": undefined, - "emailAuthFactor": false, - "emailConfirmed": false, - "handle": "alice.test", - "isSelfHosted": true, - "pdsUrl": undefined, - "refreshJwt": undefined, - "service": "https://alice.com/", - "signupQueued": false, - "status": undefined, - }, - ], - "currentAgentState": { - "agent": { + "currentBundleState": { + "bundle": { "service": "https://public.api.bsky.app/", }, "did": undefined, @@ -1518,59 +1537,53 @@ describe('session', () => { it('replaces local accounts with synced accounts', () => { let state = getInitialState([]) - const agent1 = new AtpAgent({service: 'https://alice.com'}) - agent1.sessionManager.session = { - active: true, - did: 'alice-did', - handle: 'alice.test', - accessJwt: 'alice-access-jwt-1', - refreshJwt: 'alice-refresh-jwt-1', - } - const agent2 = new AtpAgent({service: 'https://bob.com'}) - agent2.sessionManager.session = { - active: true, - did: 'bob-did', - handle: 'bob.test', - accessJwt: 'bob-access-jwt-1', - refreshJwt: 'bob-refresh-jwt-1', - } + const aliceBundle = makeBundle('https://alice.com') + const bobBundle = makeBundle('https://bob.com') state = run(state, [ { type: 'switched-to-account', - newAgent: agent1, - newAccount: agentToSessionAccountOrThrow(agent1), + newBundle: aliceBundle, + newAccount: makeAccount('https://alice.com', { + active: true, + did: 'alice-did', + handle: 'alice.test', + accessJwt: 'alice-access-jwt-1', + refreshJwt: 'alice-refresh-jwt-1', + }), }, { type: 'switched-to-account', - newAgent: agent2, - newAccount: agentToSessionAccountOrThrow(agent2), + newBundle: bobBundle, + newAccount: makeAccount('https://bob.com', { + active: true, + did: 'bob-did', + handle: 'bob.test', + accessJwt: 'bob-access-jwt-1', + refreshJwt: 'bob-refresh-jwt-1', + }), }, ]) expect(state.accounts.length).toBe(2) - expect(state.currentAgentState.did).toBe('bob-did') + expect(state.currentBundleState.did).toBe('bob-did') - const anotherTabAgent1 = new AtpAgent({service: 'https://jay.com'}) - anotherTabAgent1.sessionManager.session = { - active: true, - did: 'jay-did', - handle: 'jay.test', - accessJwt: 'jay-access-jwt-1', - refreshJwt: 'jay-refresh-jwt-1', - } - const anotherTabAgent2 = new AtpAgent({service: 'https://alice.com'}) - anotherTabAgent2.sessionManager.session = { - active: true, - did: 'bob-did', - handle: 'bob.test', - accessJwt: 'bob-access-jwt-2', - refreshJwt: 'bob-refresh-jwt-2', - } state = run(state, [ { type: 'synced-accounts', syncedAccounts: [ - agentToSessionAccountOrThrow(anotherTabAgent1), - agentToSessionAccountOrThrow(anotherTabAgent2), + makeAccount('https://jay.com', { + active: true, + did: 'jay-did', + handle: 'jay.test', + accessJwt: 'jay-access-jwt-1', + refreshJwt: 'jay-refresh-jwt-1', + }), + makeAccount('https://alice.com', { + active: true, + did: 'bob-did', + handle: 'bob.test', + accessJwt: 'bob-access-jwt-2', + refreshJwt: 'bob-refresh-jwt-2', + }), ], syncedCurrentDid: 'bob-did', }, @@ -1580,8 +1593,8 @@ describe('session', () => { expect(state.accounts[1].did).toBe('bob-did') expect(state.accounts[1].accessJwt).toBe('bob-access-jwt-2') // Keep Bob logged in. - // (We patch up agent.session outside the reducer for this to work.) - expect(state.currentAgentState.did).toBe('bob-did') + // (The bundle is rebuilt from the synced tokens outside the reducer.) + expect(state.currentBundleState.did).toBe('bob-did') expect(state.needsPersist).toBe(false) expect(printState(state)).toMatchInlineSnapshot(` { @@ -1617,8 +1630,8 @@ describe('session', () => { "status": undefined, }, ], - "currentAgentState": { - "agent": { + "currentBundleState": { + "bundle": { "service": "https://bob.com/", }, "did": "bob-did", @@ -1627,18 +1640,18 @@ describe('session', () => { } `) - const anotherTabAgent3 = new AtpAgent({service: 'https://clarence.com'}) - anotherTabAgent3.sessionManager.session = { - active: true, - did: 'clarence-did', - handle: 'clarence.test', - accessJwt: 'clarence-access-jwt-2', - refreshJwt: 'clarence-refresh-jwt-2', - } state = run(state, [ { type: 'synced-accounts', - syncedAccounts: [agentToSessionAccountOrThrow(anotherTabAgent3)], + syncedAccounts: [ + makeAccount('https://clarence.com', { + active: true, + did: 'clarence-did', + handle: 'clarence.test', + accessJwt: 'clarence-access-jwt-2', + refreshJwt: 'clarence-refresh-jwt-2', + }), + ], syncedCurrentDid: 'clarence-did', }, ]) @@ -1646,7 +1659,7 @@ describe('session', () => { expect(state.accounts[0].did).toBe('clarence-did') // Log out because we have no matching user. // (In practice, we'll resume this session outside the reducer.) - expect(state.currentAgentState.did).toBe(undefined) + expect(state.currentBundleState.did).toBe(undefined) expect(state.needsPersist).toBe(false) expect(printState(state)).toMatchInlineSnapshot(` { @@ -1667,8 +1680,8 @@ describe('session', () => { "status": undefined, }, ], - "currentAgentState": { - "agent": { + "currentBundleState": { + "bundle": { "service": "https://public.api.bsky.app/", }, "did": undefined, @@ -1677,6 +1690,94 @@ describe('session', () => { } `) }) + + it('replaces the current bundle on same-did cross-tab sync', () => { + let state = getInitialState([]) + + const aliceBundle = makeBundle('https://alice.com') + state = run(state, [ + { + type: 'switched-to-account', + newBundle: aliceBundle, + newAccount: makeAccount('https://alice.com', { + active: true, + did: 'alice-did', + handle: 'alice.test', + accessJwt: 'alice-access-jwt-1', + refreshJwt: 'alice-refresh-jwt-1', + }), + }, + ]) + expect(state.currentBundleState.bundle).toBe(aliceBundle) + expect(state.accounts[0].accessJwt).toBe('alice-access-jwt-1') + + // A fresh bundle rebuilt from synced tokens (no network). + const aliceBundle2 = makeBundle('https://alice.com') + const aliceAccount2 = makeAccount('https://alice.com', { + active: true, + did: 'alice-did', + handle: 'alice.test', + accessJwt: 'alice-access-jwt-2', + refreshJwt: 'alice-refresh-jwt-2', + }) + state = run(state, [ + { + type: 'replaced-current-bundle', + newBundle: aliceBundle2, + newAccount: aliceAccount2, + }, + ]) + // The bundle is swapped in place, the did is preserved. + expect(state.currentBundleState.bundle).toBe(aliceBundle2) + expect(state.currentBundleState.did).toBe('alice-did') + // The matching account entry is replaced with the synced one. + expect(state.accounts.length).toBe(1) + expect(state.accounts[0].did).toBe('alice-did') + expect(state.accounts[0].accessJwt).toBe('alice-access-jwt-2') + expect(state.accounts[0].refreshJwt).toBe('alice-refresh-jwt-2') + // Synced from another tab - don't persist. + expect(state.needsPersist).toBe(false) + }) + + it('does not touch the bundle on partial-refresh-session', () => { + let state = getInitialState([]) + + const aliceBundle = makeBundle('https://alice.com') + state = run(state, [ + { + type: 'switched-to-account', + newBundle: aliceBundle, + newAccount: makeAccount('https://alice.com', { + active: true, + did: 'alice-did', + handle: 'alice.test', + accessJwt: 'alice-access-jwt-1', + refreshJwt: 'alice-refresh-jwt-1', + email: 'alice@foo.bar', + emailAuthFactor: false, + emailConfirmed: false, + }), + }, + ]) + expect(state.currentBundleState.bundle).toBe(aliceBundle) + expect(state.accounts[0].emailConfirmed).toBe(false) + expect(state.accounts[0].emailAuthFactor).toBe(false) + + state = run(state, [ + { + type: 'partial-refresh-session', + accountDid: 'alice-did', + patch: {emailConfirmed: true, emailAuthFactor: true}, + }, + ]) + // The account email fields are patched. + expect(state.accounts[0].emailConfirmed).toBe(true) + expect(state.accounts[0].emailAuthFactor).toBe(true) + // The bundle is untouched - no session mutation, same reference. + expect(state.currentBundleState.bundle).toBe(aliceBundle) + expect(state.currentBundleState.did).toBe('alice-did') + expect(state.needsPersist).toBe(true) + }) }) function run(initialState: State, actions: Action[]): State { @@ -1690,9 +1791,9 @@ function run(initialState: State, actions: Action[]): State { function printState(state: State) { return { accounts: state.accounts, - currentAgentState: { - agent: {service: state.currentAgentState.agent.service}, - did: state.currentAgentState.did, + currentBundleState: { + bundle: {service: state.currentBundleState.bundle.service}, + did: state.currentBundleState.did, }, needsPersist: state.needsPersist, } diff --git a/src/state/session/agent-config.ts b/src/state/session/agent-config.ts deleted file mode 100644 index 3ee2718a39..0000000000 --- a/src/state/session/agent-config.ts +++ /dev/null @@ -1,12 +0,0 @@ -import AsyncStorage from '@react-native-async-storage/async-storage' - -const PREFIX = 'agent-labelers' - -export async function saveLabelers(did: string, value: string[]) { - await AsyncStorage.setItem(`${PREFIX}:${did}`, JSON.stringify(value)) -} - -export async function readLabelers(did: string): Promise { - const rawData = await AsyncStorage.getItem(`${PREFIX}:${did}`) - return rawData ? JSON.parse(rawData) : undefined -} diff --git a/src/state/session/agent.ts b/src/state/session/agent.ts index 78e9f79610..7246454ed2 100644 --- a/src/state/session/agent.ts +++ b/src/state/session/agent.ts @@ -1,338 +1,17 @@ import { Agent as BaseAgent, - type AppBskyActorProfile, - AtpAgent, type AtprotoServiceType, - type AtpSessionData, - type AtpSessionEvent, type Did, - type Un$Typed, } from '@atproto/api' -import {TID} from '@atproto/common-web' - -import {networkRetry} from '#/lib/async/retry' -import { - BLUESKY_PROXY_HEADER, - BSKY_SERVICE, - DISCOVER_SAVED_FEED, - IS_PROD_SERVICE, - PUBLIC_BSKY_SERVICE, - TIMELINE_SAVED_FEED, -} from '#/lib/constants' -import {logger} from '#/logger' -import {snoozeBirthdateUpdateAllowedForDid} from '#/state/birthdate' -import {restrictChatSettings} from '#/state/queries/messages/restrictChatSettings' -import {snoozeEmailConfirmationPrompt} from '#/state/shell/reminders' -import { - prefetchAgeAssuranceServerData, - setBirthdateForDid, - setCreatedAtForDid, -} from '#/ageAssurance/data' -import {unsafeGetAndComputeAgeAssurance} from '#/ageAssurance/state' -import {features} from '#/analytics' -import {emitNetworkConfirmed, emitNetworkLost} from '../events' -import {addSessionErrorLog} from './logging' -import { - configureModerationForAccount, - configureModerationForGuest, -} from './moderation' -import {type SessionAccount} from './types' -import {isSessionExpired, isSignupQueued} from './util' export type ProxyHeaderValue = `${Did}#${AtprotoServiceType}` -export function createPublicAgent() { - configureModerationForGuest() // Side effect but only relevant for tests - - const agent = new BskyAppAgent({service: PUBLIC_BSKY_SERVICE}) - agent.configureProxy(BLUESKY_PROXY_HEADER.get()) - return agent -} - -export async function createAgentAndResume( - storedAccount: SessionAccount, - onSessionChange: ( - agent: AtpAgent, - did: string, - event: AtpSessionEvent, - ) => void, -) { - const agent = new BskyAppAgent({service: storedAccount.service}) - if (storedAccount.pdsUrl) { - agent.sessionManager.pdsUrl = new URL(storedAccount.pdsUrl) - } - const gates = features.refresh({ - strategy: 'prefer-low-latency', - }) - const moderation = configureModerationForAccount(agent, storedAccount) - const prevSession: AtpSessionData = sessionAccountToSession(storedAccount) - if (isSessionExpired(storedAccount)) { - await networkRetry(1, () => agent.resumeSession(prevSession)) - } else { - agent.sessionManager.session = prevSession - } - - // after session is attached - const aa = prefetchAgeAssuranceServerData({agent}) - - agent.configureProxy(BLUESKY_PROXY_HEADER.get()) - - return agent.prepare({ - resolvers: [gates, moderation, aa], - onSessionChange, - }) -} - -export async function createAgentAndLogin( - { - service, - identifier, - password, - authFactorToken, - }: { - service: string - identifier: string - password: string - authFactorToken?: string - }, - onSessionChange: ( - agent: AtpAgent, - did: string, - event: AtpSessionEvent, - ) => void, -) { - const agent = new BskyAppAgent({service}) - await agent.login({ - identifier, - password, - authFactorToken, - allowTakendown: true, - }) - - const account = agentToSessionAccountOrThrow(agent) - const gates = features.refresh({strategy: 'prefer-fresh-gates'}) - const moderation = configureModerationForAccount(agent, account) - const aa = prefetchAgeAssuranceServerData({agent}) - - agent.configureProxy(BLUESKY_PROXY_HEADER.get()) - - return agent.prepare({ - resolvers: [gates, moderation, aa], - onSessionChange, - }) -} - -export async function createAgentAndCreateAccount( - { - service, - email, - password, - handle, - birthDate, - inviteCode, - verificationPhone, - verificationCode, - }: { - service: string - email: string - password: string - handle: string - birthDate: Date - inviteCode?: string - verificationPhone?: string - verificationCode?: string - }, - onSessionChange: ( - agent: AtpAgent, - did: string, - event: AtpSessionEvent, - ) => void, -) { - const agent = new BskyAppAgent({service}) - await agent.createAccount({ - email, - password, - handle, - inviteCode, - verificationPhone, - verificationCode, - }) - const account = agentToSessionAccountOrThrow(agent) - const gates = features.refresh({strategy: 'prefer-fresh-gates'}) - const moderation = configureModerationForAccount(agent, account) - - const createdAt = new Date().toISOString() - const birthdate = birthDate.toISOString() - - /* - * Since we have a race with account creation, profile creation, and AA - * state, set these values locally to ensure sync reads. Values are written - * to the server in the next step, so on subsequent reloads, the server will - * be the source of truth. - */ - setCreatedAtForDid({did: account.did, createdAt}) - setBirthdateForDid({did: account.did, birthdate}) - snoozeBirthdateUpdateAllowedForDid(account.did) - // do this last - const aa = prefetchAgeAssuranceServerData({agent}) - - // Not awaited so that we can still get into onboarding. - // This is OK because we won't let you toggle adult stuff until you set the date. - if (IS_PROD_SERVICE(service)) { - void Promise.allSettled([ - networkRetry(3, () => { - return agent.setPersonalDetails({ - birthDate: birthdate, - }) - }).catch(e => { - logger.info(`createAgentAndCreateAccount: failed to set birthDate`) - throw e - }), - networkRetry(3, () => { - return agent.upsertProfile(prev => { - const next: Un$Typed = prev || {} - next.displayName = handle - next.createdAt = createdAt - return next - }) - }).catch(e => { - logger.info( - `createAgentAndCreateAccount: failed to set initial profile`, - ) - throw e - }), - networkRetry(1, () => { - return agent.overwriteSavedFeeds([ - { - ...DISCOVER_SAVED_FEED, - id: TID.nextStr(), - }, - { - ...TIMELINE_SAVED_FEED, - id: TID.nextStr(), - }, - ]) - }).catch(e => { - logger.info(`createAgentAndCreateAccount: failed to set initial feeds`) - throw e - }), - // wait for AA data to load first, then check state - aa.then(() => { - const {flags} = unsafeGetAndComputeAgeAssurance({did: account.did}) - if (flags?.chatDisabled || flags?.groupChatDisabled) { - void restrictChatSettings({ - agent, - restrictIncoming: flags.chatDisabled, - restrictGroupInvites: flags.groupChatDisabled, - }) - } - }), - ]).then(promises => { - const rejected = promises.filter(p => p.status === 'rejected') - if (rejected.length > 0) { - logger.error( - `session: createAgentAndCreateAccount failed to save personal details and feeds`, - ) - } - }) - } else { - void Promise.allSettled([ - networkRetry(3, () => { - return agent.setPersonalDetails({ - birthDate: birthDate.toISOString(), - }) - }).catch(e => { - logger.info(`createAgentAndCreateAccount: failed to set birthDate`) - throw e - }), - networkRetry(3, () => { - return agent.upsertProfile(prev => { - const next: Un$Typed = prev || {} - next.createdAt = prev?.createdAt || new Date().toISOString() - return next - }) - }).catch(e => { - logger.info( - `createAgentAndCreateAccount: failed to set initial profile`, - ) - throw e - }), - ]).then(promises => { - const rejected = promises.filter(p => p.status === 'rejected') - if (rejected.length > 0) { - logger.error( - `session: createAgentAndCreateAccount failed to save personal details and feeds`, - ) - } - }) - } - - try { - // snooze first prompt after signup, defer to next prompt - snoozeEmailConfirmationPrompt() - } catch (e: any) { - logger.error(e, {message: `session: failed snoozeEmailConfirmationPrompt`}) - } - - agent.configureProxy(BLUESKY_PROXY_HEADER.get()) - - return agent.prepare({ - resolvers: [gates, moderation, aa], - onSessionChange, - }) -} - -export function agentToSessionAccountOrThrow(agent: AtpAgent): SessionAccount { - const account = agentToSessionAccount(agent) - if (!account) { - throw Error('Expected an active session') - } - return account -} - -export function agentToSessionAccount( - agent: AtpAgent, -): SessionAccount | undefined { - if (!agent.session) { - return undefined - } - return { - service: agent.serviceUrl.toString(), - did: agent.session.did, - handle: agent.session.handle, - email: agent.session.email, - emailConfirmed: agent.session.emailConfirmed || false, - emailAuthFactor: agent.session.emailAuthFactor || false, - refreshJwt: agent.session.refreshJwt, - accessJwt: agent.session.accessJwt, - signupQueued: isSignupQueued(agent.session.accessJwt), - active: agent.session.active, - status: agent.session.status, - pdsUrl: agent.pdsUrl?.toString(), - isSelfHosted: !agent.serviceUrl.toString().startsWith(BSKY_SERVICE), - } -} - -export function sessionAccountToSession( - account: SessionAccount, -): AtpSessionData { - return { - // Sorted in the same property order as when returned by BskyAgent (alphabetical). - accessJwt: account.accessJwt ?? '', - did: account.did, - email: account.email, - emailAuthFactor: account.emailAuthFactor, - emailConfirmed: account.emailConfirmed, - handle: account.handle, - refreshJwt: account.refreshJwt ?? '', - /** - * @see https://github.com/bluesky-social/atproto/blob/c5d36d5ba2a2c2a5c4f366a5621c06a5608e361e/packages/api/src/agent.ts#L188 - */ - active: account.active ?? true, - status: account.status, - } -} - +/** + * A bare `Agent` that applies a service-proxy header on construction. + * + * Used for the unauthenticated, service-specific calls that cannot go through + * the session agent (PDS detection, password reset, handle availability). + */ export class Agent extends BaseAgent { constructor( proxyHeader: ProxyHeaderValue | null, @@ -344,74 +23,3 @@ export class Agent extends BaseAgent { } } } - -// Not exported. Use factories above to create it. -// WARN: In the factories above, we _manually set a proxy header_ for the agent after we do whatever it is we are supposed to do. -// Ideally, we wouldn't be doing this. However, since there is so much logic that requires making calls to the PDS right now, it -// feels safer to just let those run as-is and set the header afterward. -let realFetch = globalThis.fetch -class BskyAppAgent extends AtpAgent { - persistSessionHandler: ((event: AtpSessionEvent) => void) | undefined = - undefined - - constructor({service}: {service: string}) { - super({ - service, - async fetch(...args) { - let success = false - try { - const result = await realFetch(...args) - success = true - return result - } catch (e) { - success = false - throw e - } finally { - if (success) { - emitNetworkConfirmed() - } else { - emitNetworkLost() - } - } - }, - persistSession: (event: AtpSessionEvent) => { - if (this.persistSessionHandler) { - this.persistSessionHandler(event) - } - }, - }) - } - - async prepare({ - resolvers, - onSessionChange, - }: { - // Not awaited in the calling code so we can delay blocking on them. - resolvers: Promise[] - onSessionChange: ( - agent: AtpAgent, - did: string, - event: AtpSessionEvent, - ) => void - }) { - // There's nothing else left to do, so block on them here. - await Promise.all(resolvers) - - // Now the agent is ready. - const account = agentToSessionAccountOrThrow(this) - this.persistSessionHandler = event => { - onSessionChange(this, account.did, event) - if (event !== 'create' && event !== 'update') { - addSessionErrorLog(account.did, event) - } - } - return {account, agent: this} - } - - dispose() { - this.sessionManager.session = undefined - this.persistSessionHandler = undefined - } -} - -export type {BskyAppAgent} diff --git a/src/state/session/bridge-agent.ts b/src/state/session/bridge-agent.ts new file mode 100644 index 0000000000..0c692a6c17 --- /dev/null +++ b/src/state/session/bridge-agent.ts @@ -0,0 +1,442 @@ +import { + AtpAgent, + type AtpAgentLoginOpts, + type AtpSessionData, + type ComAtprotoServerCreateAccount, + type ComAtprotoServerCreateSession, + type ComAtprotoServerRefreshSession, + CredentialSession, +} from '@atproto/api' +import { + type PasswordSession, + type SessionData, +} from '@atproto/lex-password-session' + +import {BLUESKY_PROXY_HEADER, PUBLIC_BSKY_SERVICE} from '#/lib/constants' +import {configureModerationForGuest} from './moderation' +import {networkAwareFetch} from './network' + +const UNSUPPORTED = + 'Not supported on PasswordSessionManager; use the session factories in session-core' + +/** + * Convert live `PasswordSession` session data into the `AtpSessionData` shape + * that `CredentialSession.session` consumers expect. + * + * The only real adaptation is `active`: `AtpSessionData` requires it, while the + * lexicon payload leaves it optional (absent means active, per the lexicon + * docs). + */ +function toAtpSessionData(d: SessionData): AtpSessionData { + return { + refreshJwt: d.refreshJwt, + accessJwt: d.accessJwt, + handle: d.handle, + did: d.did, + email: d.email, + emailConfirmed: d.emailConfirmed, + emailAuthFactor: d.emailAuthFactor, + active: d.active ?? true, + status: d.status, + } +} + +/** + * Parse a URL without throwing. + */ +function parseUrl(input: string): URL | undefined { + try { + return new URL(input) + } catch { + return undefined + } +} + +/** + * Read a property off an unknown value the way JS optional chaining would, + * without narrowing assumptions about the shape of a `LexMap`. + */ +function prop(value: unknown, key: string): unknown { + return typeof value === 'object' && value !== null + ? (value as Record)[key] + : undefined +} + +/** + * The PDS endpoint declared by a DID document, or `undefined`. + * + * This deliberately mirrors `extractPdsUrl` in `@atproto/lex-password-session`, + * which is the predicate `PasswordSession` uses to route its own requests: the + * first service entry whose `id` ends with `#atproto_pds`, taking its + * `serviceEndpoint` if it parses as a URL. It is looser than the + * `isValidDidDoc` + `getPdsEndpoint` pair from `@atproto/common-web` (no doc + * schema validation, no `type` check), and that is the point - a stricter + * predicate here would let `dispatchUrl` disagree with the host requests + * actually go to, which in turn mints service-auth tokens (video upload) for + * the wrong audience. + */ +function extractPdsUrl(didDoc: SessionData['didDoc']): URL | undefined { + const services = prop(didDoc, 'service') + if (!Array.isArray(services)) { + return undefined + } + /* + * `find`, not a scan: the inner session stops at the first `#atproto_pds` + * entry and gives up if its endpoint does not parse, rather than falling + * through to a later entry. + */ + const pds = services.find(service => { + const id = prop(service, 'id') + return typeof id === 'string' && id.endsWith('#atproto_pds') + }) + const endpoint = prop(pds, 'serviceEndpoint') + return typeof endpoint === 'string' ? parseUrl(endpoint) : undefined +} + +/** + * A `CredentialSession` whose auth core is a `PasswordSession`. + * + * This is the compat shim that lets a `PasswordSession` sit under `AtpAgent`: + * every call site that reads `agent.session`, `agent.pdsUrl`, + * `agent.dispatchUrl`, `agent.did` or calls `agent.resumeSession()` keeps + * working, while the actual tokens, refresh serialization and PDS routing live + * in the `PasswordSession` underneath. + * + * A `null` inner session means "logged out" - the public/guest agent. In that + * mode requests still go out (unauthenticated) through the inherited `fetch`. + */ +export class PasswordSessionManager extends CredentialSession { + #inner: PasswordSession | null + #storedPdsUrl: URL | undefined + #disposed = false + + /* + * Identity caches for the two pull-through accessors. Each holds the + * `SessionData` it was derived from so a repeated read returns the very same + * object (see the note on identity stability below). Keying on the whole + * `SessionData` rather than the individual field works because + * `PasswordSession` replaces the object wholesale on every rotation. + */ + #sessionSource: SessionData | undefined + #sessionValue: AtpSessionData | undefined + #pdsSource: SessionData | undefined + #pdsValue: URL | undefined + + constructor( + inner: PasswordSession | null, + {service, pdsUrl}: {service: string; pdsUrl?: string}, + ) { + /* + * `persistSession` is deliberately undefined: the inner `PasswordSession` + * owns persistence through its own hooks, and none of the inherited methods + * that would call this handler survive the overrides below. + */ + super(new URL(service), networkAwareFetch, undefined) + + this.#inner = inner + this.#storedPdsUrl = pdsUrl ? parseUrl(pdsUrl) : undefined + + /* + * `session` and `pdsUrl` are pull-through accessors over the inner session + * rather than mirrored values, installed here with `defineProperty` for two + * reasons. + * + * Why accessors at all: a mirror has to be written on every token rotation, + * and any missed write silently serves stale tokens. Pulling through cannot + * drift. + * + * Why `defineProperty` and not `get session()` in the class body: the + * parent declares `session` and `pdsUrl` as *properties*, and TypeScript + * rejects overriding a property with an accessor (TS2611). Installing them + * at runtime sidesteps that, and it is safe as long as + * `CredentialSession`'s emitted constructor does not assign either one + * (both are declaration-only), so there is nothing to clobber and no + * ordering hazard. + * + * For the same reason this class must NOT redeclare `session`/`pdsUrl` as + * fields: under `useDefineForClassFields` semantics (target esnext) a field + * declaration emits an own-property definition that would overwrite these + * accessors with `undefined`. + */ + Object.defineProperty(this, 'session', { + configurable: true, + get: () => this.#readSession(), + set: () => { + throw new Error('PasswordSessionManager.session is read-only') + }, + }) + Object.defineProperty(this, 'pdsUrl', { + configurable: true, + get: () => this.#readPdsUrl(), + set: () => { + throw new Error('PasswordSessionManager.pdsUrl is read-only') + }, + }) + } + + /** + * The inner session's live data, or `undefined` when there is nothing to read + * from. + * + * `PasswordSession`'s `session`/`did`/`handle` getters *throw* `Logged out` + * once the session has been destroyed. Every read in this class funnels + * through here so that failure mode can never escape into the app, which + * reads `agent.session` from render paths. + */ + #liveData(): SessionData | undefined { + if (this.#disposed || !this.#inner || this.#inner.destroyed) { + return undefined + } + return this.#inner.session + } + + /** + * The `session` accessor's implementation. + * + * Identity-cached on the source `SessionData`: consecutive reads with no + * intervening token rotation return the same object, and a rotation produces + * a new one. `CredentialSession` declares `session` as a plain field, so + * consumers are entitled to treat it as a value whose identity changes only + * when the session does; this class is read from render paths, and returning + * a freshly allocated object on every read would break that expectation for + * any memo, dependency array or reference comparison built on top of it. + */ + #readSession(): AtpSessionData | undefined { + const live = this.#liveData() + if (!live) { + this.#sessionSource = undefined + this.#sessionValue = undefined + return undefined + } + if (live !== this.#sessionSource) { + this.#sessionSource = live + this.#sessionValue = toAtpSessionData(live) + } + return this.#sessionValue + } + + /** + * The `pdsUrl` accessor's implementation. + * + * The DID document's PDS endpoint wins when there is one, derived with + * {@link extractPdsUrl} so this agrees exactly with the inner session's own + * routing. Before the first refresh delivers a didDoc (the non-expired resume + * fast path, which makes no network call) we fall back to the `pdsUrl` + * persisted on the account, so the very first requests still reach the right + * host - entryway accounts have `service: bsky.social` but live on a + * different PDS. + * + * Identity-cached on the didDoc for the same reason as `session`. + */ + #readPdsUrl(): URL | undefined { + const live = this.#liveData() + if (!live) { + this.#pdsSource = undefined + this.#pdsValue = undefined + return undefined + } + if (live !== this.#pdsSource) { + this.#pdsSource = live + this.#pdsValue = extractPdsUrl(live.didDoc) ?? this.#storedPdsUrl + } + return this.#pdsValue + } + + /* + * `did`, `hasSession` and `dispatchUrl` are deliberately NOT overridden: the + * inherited getters read `this.session` / `this.pdsUrl`, which resolve + * through the accessors above, so they are already live. + */ + + override async fetchHandler( + url: string, + init?: RequestInit, + ): Promise { + /* + * Absolutizing against `dispatchUrl` routes to the stored PDS on the resume + * fast path and to the didDoc PDS from the first refresh onwards, since + * `PasswordSession` resolves an already absolute URL against its own base + * as a no-op. + */ + const target = new URL(url, this.dispatchUrl) + const inner = this.#disposed ? null : this.#inner + + /* + * A caller that set its own `authorization` header bypasses the inner + * session entirely. This is mandatory: `PasswordSession.fetchHandler` + * throws `TypeError` on a pre-set authorization header rather than + * deferring to it. + * + * Bypassing also means these requests get no refresh-on-401 retry, since + * that lives in `PasswordSession.fetchHandler`. That is intentional: the + * caller supplied its own credential (a service-auth token, say), so + * rotating the session's tokens would not make the request any more likely + * to succeed on a retry. + */ + if ( + !inner || + inner.destroyed || + new Headers(init?.headers).has('authorization') + ) { + return (0, this.fetch)(target, init) + } + + /* + * `init ?? {}` because `PasswordSession.fetchHandler` reads `init.headers` + * unguarded, while the inherited signature makes `init` optional. + */ + return inner.fetchHandler(target.href, init ?? {}) + } + + /** + * Refresh the session, rejecting if nothing was refreshed. + * + * This restores the contract of the `CredentialSession.refreshSession` this + * class replaces, which rejected on any refresh failure. + * `PasswordSession.refresh()` does not: on a transient failure (a 500, a + * network error) it reports through `onUpdateFailure` and then *resolves* + * with the unchanged session data, reserving rejection for the cases where + * the session is definitively gone. Callers here read resolution as "tokens + * rotated" - `SignupQueued` refreshes and then re-checks the token scope, and + * the various verification dialogs refresh and then close - so a resolved + * no-op would silently loop or report success. + * + * The signal is the identity of the returned `SessionData`, not a field + * comparison: `PasswordSession` builds a brand new object on every successful + * rotation and returns the existing one untouched on a transient failure, so + * identity separates the two exactly. Comparing against the data captured + * immediately before the call also gets concurrent refreshes right - if + * another caller's refresh rotated the tokens while ours was queued behind it + * (`PasswordSession` serializes refreshes), the data we get back still + * differs from what we captured, which is a success for our caller. + */ + override async refreshSession(): Promise { + const inner = this.#disposed ? null : this.#inner + if (!inner || inner.destroyed) { + throw new Error('No session to refresh') + } + const before = this.#liveData() + const data = await inner.refresh() + if (data === before) { + throw new Error('Failed to refresh session') + } + /* + * Re-shape the lex payload into the `@atproto/api` XRPC response envelope. + * `headers` is empty because the inner session does not surface response + * headers, and no caller in this app reads them off a refresh. + */ + return { + success: true, + headers: {}, + data: { + accessJwt: data.accessJwt, + refreshJwt: data.refreshJwt, + handle: data.handle, + did: data.did, + didDoc: data.didDoc, + email: data.email, + emailConfirmed: data.emailConfirmed, + emailAuthFactor: data.emailAuthFactor, + active: data.active, + status: data.status, + }, + } + } + + /** + * Force a refresh, ignoring the passed-in session data. + * + * The inner session already owns its tokens, so there is nothing to install; + * every call site in the app uses `resumeSession` as "refresh my session + * now". The returned envelope is the refresh one, which is structurally a + * superset of `ComAtprotoServerGetSession.Response` (the shape `AtpAgent` + * advertises), so both layers stay type-correct. + * + * It inherits {@link PasswordSessionManager.refreshSession}'s contract, so it + * rejects rather than resolving when no tokens were rotated. + */ + override resumeSession( + _session: AtpSessionData, + ): Promise { + return this.refreshSession() + } + + override async logout(): Promise { + const inner = this.#inner + if (!inner || inner.destroyed) { + return + } + try { + await inner.logout() + } catch { + /* matches the parent, which swallows delete-session failures */ + } + } + + override login( + _opts: AtpAgentLoginOpts, + ): Promise { + return Promise.reject(new Error(UNSUPPORTED)) + } + + override createAccount( + _data: ComAtprotoServerCreateAccount.InputSchema, + _opts?: ComAtprotoServerCreateAccount.CallOptions, + ): Promise { + return Promise.reject(new Error(UNSUPPORTED)) + } + + /** + * Detach this manager from its inner session. + * + * All reads then behave as logged out and requests fall back to the + * unauthenticated `fetch` path. The inner session is left alone: it may still + * be shared, and logging out is a separate, explicit operation. + */ + dispose() { + this.#disposed = true + } +} + +/* + * Declaration merging to narrow the inherited `sessionManager` (typed as + * `CredentialSession` by `AtpAgent`) to the manager `BskyAppAgent` actually + * receives. A `declare` class field would be the direct way to say this, but + * babel's TypeScript transform rejects `declare` fields in this config, and a + * `get sessionManager()` override is forbidden because the parent declares it + * as a property (TS2611). The merge is sound: the constructor passes the + * manager straight to `super`, which assigns it. + */ +// eslint-disable-next-line typescript/no-unsafe-declaration-merging +export interface BskyAppAgent { + readonly sessionManager: PasswordSessionManager +} + +/** + * The app's `AtpAgent`, backed by a `PasswordSession`. + * + * Everything interesting lives in {@link PasswordSessionManager}; this exists + * so `useAgent()` consumers keep getting a real `AtpAgent` (proxy headers, + * labeler headers, the `app`/`com`/`chat` namespaces) and so the agent can be + * disposed alongside its session. + */ +export class BskyAppAgent extends AtpAgent { + constructor(manager: PasswordSessionManager) { + super(manager) + } + + dispose() { + this.sessionManager.dispose() + } +} + +/** Build the logged-out agent used for public/guest browsing. */ +export function createPublicAgent() { + configureModerationForGuest() // Side effect but only relevant for tests + + const agent = new BskyAppAgent( + new PasswordSessionManager(null, {service: PUBLIC_BSKY_SERVICE}), + ) + agent.configureProxy(BLUESKY_PROXY_HEADER.get()) + return agent +} diff --git a/src/state/session/create-account.ts b/src/state/session/create-account.ts new file mode 100644 index 0000000000..fd67681482 --- /dev/null +++ b/src/state/session/create-account.ts @@ -0,0 +1,243 @@ +import {type AppBskyActorProfile, type Un$Typed} from '@atproto/api' +import {TID} from '@atproto/common-web' +import {PasswordSession} from '@atproto/lex-password-session' + +import {networkRetry} from '#/lib/async/retry' +import { + BLUESKY_PROXY_HEADER, + DISCOVER_SAVED_FEED, + IS_PROD_SERVICE, + TIMELINE_SAVED_FEED, +} from '#/lib/constants' +import {logger} from '#/logger' +import {snoozeBirthdateUpdateAllowedForDid} from '#/state/birthdate' +import {restrictChatSettings} from '#/state/queries/messages/restrictChatSettings' +import {snoozeEmailConfirmationPrompt} from '#/state/shell/reminders' +import { + prefetchAgeAssuranceServerData, + setBirthdateForDid, + setCreatedAtForDid, +} from '#/ageAssurance/data' +import {unsafeGetAndComputeAgeAssurance} from '#/ageAssurance/state' +import {features} from '#/analytics' +import {type BskyAppAgent} from './bridge-agent' +import {configureModerationForAccount} from './moderation' +import { + buildBundle, + finishPreparation, + makeSessionHooks, + type OnSessionChange, + registerBundleKillSwitch, + type SessionBundle, +} from './session-core' +import {sessionDataToSessionAccount} from './session-data' +import {type SessionAccount} from './types' + +/** Create an account, prepare its session, and start post-signup writes. */ +export async function createSessionBundleAndCreateAccount( + { + service, + email, + password, + handle, + birthDate, + inviteCode, + verificationPhone, + verificationCode, + }: { + service: string + email: string + password: string + handle: string + birthDate: Date + inviteCode?: string + verificationPhone?: string + verificationCode?: string + }, + onSessionChange: OnSessionChange, +): Promise<{account: SessionAccount; bundle: SessionBundle}> { + let bundle!: SessionBundle + let accountDid = '' + const hooks = makeSessionHooks({ + onSessionChange, + getBundle: () => bundle, + getDid: () => accountDid, + }) + + const session = await PasswordSession.createAccount( + { + email, + password, + /* the lexicon types handle as `${string}.${string}`; user input is a plain string */ + handle: handle as `${string}.${string}`, + inviteCode, + verificationPhone, + verificationCode, + }, + {...hooks, service}, + ) + + bundle = buildBundle(session) + registerBundleKillSwitch(bundle, hooks.kill) + // Seed the hook and the deferred writes with refresh-stable account fields. + const earlyAccount = snapshotNewAccount(session, email) + accountDid = earlyAccount.did + + const gates = features.refresh({strategy: 'prefer-fresh-gates'}) + configureModerationForAccount(bundle.agent, earlyAccount) + + const createdAt = new Date().toISOString() + const birthdate = birthDate.toISOString() + + /* + * Since we have a race with account creation, profile creation, and AA + * state, set these values locally to ensure sync reads. Values are written + * to the server in the next step, so on subsequent reloads, the server will + * be the source of truth. + */ + setCreatedAtForDid({did: earlyAccount.did, createdAt}) + setBirthdateForDid({did: earlyAccount.did, birthdate}) + snoozeBirthdateUpdateAllowedForDid(earlyAccount.did) + // Start the prefetch after seeding its synchronous birthdate inputs. + const aa = prefetchAgeAssuranceServerData({agent: bundle.agent}) + + const isProd = Boolean(IS_PROD_SERVICE(service)) + const postSignupTasks: Promise[] = [ + savePersonalDetails(bundle.agent, birthdate), + initializeProfile(bundle.agent, {handle, createdAt, isProd}), + ] + if (isProd) { + postSignupTasks.push( + initializeSavedFeeds(bundle.agent), + restrictChatAfterAgeAssurance(aa, bundle.agent, earlyAccount.did), + ) + } + // Post-signup writes are not required to enter onboarding. + void reportPostSignupFailures(postSignupTasks) + + try { + // snooze first prompt after signup, defer to next prompt + snoozeEmailConfirmationPrompt() + } catch (e) { + logger.error(e instanceof Error ? e : String(e), { + message: `session: failed snoozeEmailConfirmationPrompt`, + }) + } + + bundle.agent.configureProxy(BLUESKY_PROXY_HEADER.get()) + + // Preparation may auto-refresh the session while hooks are still disarmed. + const account = await finishPreparation( + bundle, + Promise.all([gates, aa]), + () => snapshotNewAccount(session, email), + ) + hooks.arm() + return {account, bundle} +} + +/** + * Snapshot a just-created session as a `SessionAccount`. + * + * `com.atproto.server.createAccount` returns only tokens, handle, did and + * didDoc, so the session carries no email state or `active` flag until the + * first refresh. Synthesize those fields from the creation input so the + * persisted account does not briefly claim the email is unknown. + */ +function snapshotNewAccount( + session: PasswordSession, + email: string, +): SessionAccount { + const account = sessionDataToSessionAccount( + session.session, + session.session.service, + ) + if (!account) { + throw Error('Expected an active session') + } + return { + ...account, + email: account.email ?? email, + emailConfirmed: account.emailConfirmed ?? false, + emailAuthFactor: account.emailAuthFactor ?? false, + active: account.active ?? true, + } +} + +function savePersonalDetails(agent: BskyAppAgent, birthDate: string) { + return retryPostSignupTask('set birthDate', 3, () => + agent.setPersonalDetails({birthDate}), + ) +} + +function initializeProfile( + agent: BskyAppAgent, + { + handle, + createdAt, + isProd, + }: { + handle: string + createdAt: string + isProd: boolean + }, +) { + return retryPostSignupTask('set initial profile', 3, () => + agent.upsertProfile(prev => { + const next: Un$Typed = prev || {} + if (isProd) { + next.displayName = handle + next.createdAt = createdAt + } else { + next.createdAt = prev?.createdAt || new Date().toISOString() + } + return next + }), + ) +} + +function initializeSavedFeeds(agent: BskyAppAgent) { + return retryPostSignupTask('set initial feeds', 1, () => + agent.overwriteSavedFeeds([ + {...DISCOVER_SAVED_FEED, id: TID.nextStr()}, + {...TIMELINE_SAVED_FEED, id: TID.nextStr()}, + ]), + ) +} + +function restrictChatAfterAgeAssurance( + ageAssurance: Promise, + agent: BskyAppAgent, + did: string, +) { + return ageAssurance.then(() => { + const {flags} = unsafeGetAndComputeAgeAssurance({did}) + if (flags?.chatDisabled || flags?.groupChatDisabled) { + void restrictChatSettings({ + agent, + restrictIncoming: flags.chatDisabled, + restrictGroupInvites: flags.groupChatDisabled, + }) + } + }) +} + +function retryPostSignupTask( + description: string, + retries: number, + task: () => Promise, +) { + return networkRetry(retries, task).catch(e => { + logger.info(`createSessionBundleAndCreateAccount: failed to ${description}`) + throw e + }) +} + +async function reportPostSignupFailures(tasks: Promise[]) { + const results = await Promise.allSettled(tasks) + if (results.some(result => result.status === 'rejected')) { + logger.error( + `session: createSessionBundleAndCreateAccount failed to save post-signup settings`, + ) + } +} diff --git a/src/state/session/expiry-rescue.ts b/src/state/session/expiry-rescue.ts new file mode 100644 index 0000000000..f750a1ba5c --- /dev/null +++ b/src/state/session/expiry-rescue.ts @@ -0,0 +1,30 @@ +import {type SessionAccount} from './types' + +/** Maximum failed token generations considered during one expiry rescue. */ +export const MAX_EXPIRY_RESCUE_GENERATIONS = 5 + +/** Pick the first unfailed token generation newer than the one that expired. */ +export function pickExpiryRescueCandidate({ + dyingRefreshJwt, + candidates, + failedRefreshJwts, +}: { + dyingRefreshJwt: string + candidates: (SessionAccount | undefined)[] + failedRefreshJwts: ReadonlySet +}): SessionAccount | undefined { + if (failedRefreshJwts.size >= MAX_EXPIRY_RESCUE_GENERATIONS) { + return undefined + } + for (const candidate of candidates) { + const refreshJwt = candidate?.refreshJwt + if ( + refreshJwt && + refreshJwt !== dyingRefreshJwt && + !failedRefreshJwts.has(refreshJwt) + ) { + return candidate + } + } + return undefined +} diff --git a/src/state/session/index.tsx b/src/state/session/index.tsx index fd31261a9d..1a22a0e215 100644 --- a/src/state/session/index.tsx +++ b/src/state/session/index.tsx @@ -3,12 +3,14 @@ import { useCallback, useContext, useEffect, + useInsertionEffect, useMemo, useRef, useState, useSyncExternalStore, } from 'react' -import {type AtpAgent, type AtpSessionEvent} from '@atproto/api' +import {type AtpAgent} from '@atproto/api' +import {type SessionData} from '@atproto/lex-password-session' import * as persisted from '#/state/persisted' import {useCloseAllActiveElements} from '#/state/util' @@ -16,17 +18,28 @@ import {useGlobalDialogsControlContext} from '#/components/dialogs/Context' import {AnalyticsContext, useAnalyticsBase, utils} from '#/analytics' import {IS_WEB} from '#/env' import {emitSessionDropped} from '../events' -import { - agentToSessionAccount, - type BskyAppAgent, - createAgentAndCreateAccount, - createAgentAndLogin, - createAgentAndResume, - sessionAccountToSession, -} from './agent' +import {createSessionBundleAndCreateAccount} from './create-account' +import {pickExpiryRescueCandidate} from './expiry-rescue' import {type Action, getInitialState, reducer, type State} from './reducer' -export {isSignupQueued} from './util' -import {addSessionDebugLog} from './logging' +import { + type AtpSessionEvent, + createSessionBundleAndLogin, + createSessionBundleAndResume, + createSessionBundleFromStoredAccount, + disposeBundle, + type PublicSessionBundle, + type SessionBundle, + sessionDataToSessionAccount, +} from './session-core' +export {isSignupQueued} from './session-data' +import { + addSessionDebugLog, + getBundleId, + redactAccount, + redactPersistedSession, + redactSessionData, + redactState, +} from './logging' export type {SessionAccount} from '#/state/session/types' import {clearPersistedQueryStorage} from '#/lib/persisted-query-storage' @@ -47,8 +60,11 @@ const StateContext = createContext({ }) StateContext.displayName = 'SessionStateContext' -const AgentContext = createContext(null) -AgentContext.displayName = 'SessionAgentContext' +/** Active account bundle, or the public bundle when logged out. */ +const BundleContext = createContext( + null, +) +BundleContext.displayName = 'SessionBundleContext' const ApiContext = createContext({ createAccount: async () => {}, @@ -68,7 +84,7 @@ class SessionStore { constructor() { // Careful: By the time this runs, `persisted` needs to already be filled. const initialState = getInitialState(persisted.get('session').accounts) - addSessionDebugLog({type: 'reducer:init', state: initialState}) + addSessionDebugLog({type: 'reducer:init', state: redactState(initialState)}) this.state = initialState } @@ -92,10 +108,13 @@ class SessionStore { const persistedData = { accounts: nextState.accounts, currentAccount: nextState.accounts.find( - a => a.did === nextState.currentAgentState.did, + a => a.did === nextState.currentBundleState.did, ), } - addSessionDebugLog({type: 'persisted:broadcast', data: persistedData}) + addSessionDebugLog({ + type: 'persisted:broadcast', + data: redactPersistedSession(persistedData), + }) void persisted.write('session', persistedData) } this.listeners.forEach(listener => listener()) @@ -110,15 +129,118 @@ export function Provider({children}: React.PropsWithChildren<{}>) { const state = useSyncExternalStore(store.subscribe, store.getState) const onboardingDispatch = useOnboardingDispatch() - const onAgentSessionChange = useCallback( - (agent: AtpAgent, accountDid: string, sessionEvent: AtpSessionEvent) => { - const refreshedAccount = agentToSessionAccount(agent) // Mutable, so snapshot it right away. - if (sessionEvent === 'expired' || sessionEvent === 'create-failed') { + // Refresh-token generations that have already failed during expiry rescue. + const failedExpiryTokensRef = useRef>>(new Map()) + /* + * Rescued bundles need this callback for their own events. A ref avoids a + * self-reference in the callback's dependency list. It is filled by the + * insertion effect below, which commits well before any session hook can + * fire: hooks are armed only after an asynchronous session factory resolves. + */ + const onSessionChangeRef = useRef< + | (( + bundle: SessionBundle, + accountDid: string, + sessionEvent: AtpSessionEvent, + sessionData?: SessionData, + ) => void) + | null + >(null) + + const onSessionChange = useCallback( + ( + bundle: SessionBundle, + accountDid: string, + sessionEvent: AtpSessionEvent, + sessionData?: SessionData, + ) => { + if (sessionEvent === 'update' && sessionData) { + failedExpiryTokensRef.current.get(accountDid)?.clear() + } + + /* + * PasswordSession invokes its hooks before updating its live getter. Use + * the delivered payload so a refresh persists the newly rotated tokens. + * + * A refresh payload carries no didDoc unless the server sends one, so the + * stored account's `pdsUrl` is threaded in as the fallback. Without it the + * refresh would persist `pdsUrl: undefined` and the next cold start would + * route pre-refresh requests to the entryway instead of the PDS. + */ + const refreshedAccount = + sessionEvent === 'update' && sessionData + ? sessionDataToSessionAccount( + sessionData, + sessionData.service, + store.getState().accounts.find(a => a.did === accountDid)?.pdsUrl, + ) + : undefined + + /* + * A stale tab may expire a token after another tab has already rotated it. + * Prefer a newer persisted or reducer generation over logging every tab + * out. Failed generations are recorded and bounded to guarantee that a + * repeatedly expiring session eventually falls through to logout. + */ + if (sessionEvent === 'expired') { + const current = store.getState() + const currentBundle = current.currentBundleState.bundle as unknown as + | SessionBundle + | PublicSessionBundle + const dyingRefreshJwt = sessionData?.refreshJwt + // Stale bundle events are handled by the reducer's identity guard. + if ( + currentBundle === bundle && + current.currentBundleState.did === accountDid && + dyingRefreshJwt + ) { + let failedSet = failedExpiryTokensRef.current.get(accountDid) + if (!failedSet) { + failedSet = new Set() + failedExpiryTokensRef.current.set(accountDid, failedSet) + } + failedSet.add(dyingRefreshJwt) + + const persistedCandidate = persisted + .readLatest('session') + .accounts.find(a => a.did === accountDid) + const reducerCandidate = current.accounts.find( + a => a.did === accountDid, + ) + const candidate = pickExpiryRescueCandidate({ + dyingRefreshJwt, + candidates: [persistedCandidate, reducerCandidate], + failedRefreshJwts: failedSet, + }) + + if (candidate) { + const rebuilt = createSessionBundleFromStoredAccount( + candidate, + onSessionChangeRef.current!, + ) + if (rebuilt) { + store.dispatch({ + type: 'replaced-current-bundle', + newBundle: rebuilt.bundle, + newAccount: rebuilt.account, + }) + return + } + } + } + } + + // Only the current bundle may report that its session was dropped. + if ( + sessionEvent === 'expired' && + store.getState().currentBundleState.bundle === bundle + ) { emitSessionDropped() } + // Bundle identity prevents stale sessions from changing the active account. store.dispatch({ - type: 'received-agent-event', - agent, + type: 'received-session-event', + bundle, refreshedAccount, accountDid, sessionEvent, @@ -126,48 +248,65 @@ export function Provider({children}: React.PropsWithChildren<{}>) { }, [store], ) + /* + * Writing the ref during render is forbidden under React Compiler. An + * insertion effect is the earliest commit-time slot, and the only reader + * (`onSessionChange`'s expiry-rescue path) runs from armed session hooks, + * which cannot fire before the first commit. + */ + useInsertionEffect(() => { + onSessionChangeRef.current = onSessionChange + }, [onSessionChange]) const createAccount = useCallback( async (params, metrics) => { addSessionDebugLog({type: 'method:start', method: 'createAccount'}) const signal = cancelPendingTask() ax.metric('account:create:begin', {}) - const {agent, account} = await createAgentAndCreateAccount( + const {bundle, account} = await createSessionBundleAndCreateAccount( params, - onAgentSessionChange, + onSessionChange, ) if (signal.aborted) { + // The factory returns an armed bundle, so a superseded signup must dispose it. + disposeBundle(bundle) return } store.dispatch({ type: 'switched-to-account', - newAgent: agent, + newBundle: bundle, newAccount: account, }) ax.metric('account:create:success', metrics, { session: utils.accountToSessionMetadata(account), }) - addSessionDebugLog({type: 'method:end', method: 'createAccount', account}) + addSessionDebugLog({ + type: 'method:end', + method: 'createAccount', + account: redactAccount(account), + }) }, - [ax, store, onAgentSessionChange, cancelPendingTask], + [ax, store, onSessionChange, cancelPendingTask], ) const login = useCallback( async (params, logContext) => { addSessionDebugLog({type: 'method:start', method: 'login'}) const signal = cancelPendingTask() - const {agent, account} = await createAgentAndLogin( + const {bundle, account} = await createSessionBundleAndLogin( params, - onAgentSessionChange, + onSessionChange, ) if (signal.aborted) { + // The factory returns an armed bundle, so a superseded login must dispose it. + disposeBundle(bundle) return } store.dispatch({ type: 'switched-to-account', - newAgent: agent, + newBundle: bundle, newAccount: account, }) ax.metric( @@ -175,9 +314,13 @@ export function Provider({children}: React.PropsWithChildren<{}>) { {logContext, withPassword: true}, {session: utils.accountToSessionMetadata(account)}, ) - addSessionDebugLog({type: 'method:end', method: 'login', account}) + addSessionDebugLog({ + type: 'method:end', + method: 'login', + account: redactAccount(account), + }) }, - [ax, store, onAgentSessionChange, cancelPendingTask], + [ax, store, onSessionChange, cancelPendingTask], ) const logoutCurrentAccount = useCallback< @@ -196,17 +339,17 @@ export function Provider({children}: React.PropsWithChildren<{}>) { { session: utils.accountToSessionMetadata( prevState.accounts.find( - a => a.did === prevState.currentAgentState.did, + a => a.did === prevState.currentBundleState.did, ), ), }, ) addSessionDebugLog({type: 'method:end', method: 'logout'}) - if (prevState.currentAgentState.did) { + if (prevState.currentBundleState.did) { clearAgeAssuranceServerDataForDid({ - did: prevState.currentAgentState.did, + did: prevState.currentBundleState.did, }) - void clearPersistedQueryStorage(prevState.currentAgentState.did) + void clearPersistedQueryStorage(prevState.currentBundleState.did) } // reset onboarding flow on logout onboardingDispatch({type: 'skip'}) @@ -230,7 +373,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { { session: utils.accountToSessionMetadata( prevState.accounts.find( - a => a.did === prevState.currentAgentState.did, + a => a.did === prevState.currentBundleState.did, ), ), }, @@ -251,61 +394,94 @@ export function Provider({children}: React.PropsWithChildren<{}>) { addSessionDebugLog({ type: 'method:start', method: 'resumeSession', - account: storedAccount, + account: redactAccount(storedAccount), }) const signal = cancelPendingTask() - const {agent, account} = await createAgentAndResume( + const {bundle, account} = await createSessionBundleAndResume( storedAccount, - onAgentSessionChange, + onSessionChange, ) if (signal.aborted) { + // The factory returns an armed bundle, so a superseded resume must dispose it. + disposeBundle(bundle) + return + } + /* + * A cross-tab logout may clear or remove the account while resume is in + * flight. Check the account entry rather than the current did so ordinary + * account switching remains valid. + */ + const latest = store.getState() + const latestEntry = latest.accounts.find(a => a.did === account.did) + if (!latestEntry || !latestEntry.refreshJwt) { + disposeBundle(bundle) return } store.dispatch({ type: 'switched-to-account', - newAgent: agent, + newBundle: bundle, newAccount: account, }) - addSessionDebugLog({type: 'method:end', method: 'resumeSession', account}) + addSessionDebugLog({ + type: 'method:end', + method: 'resumeSession', + account: redactAccount(account), + }) if (isSwitchingAccounts) { // reset onboarding flow on switch account onboardingDispatch({type: 'skip'}) } }, - [store, onAgentSessionChange, cancelPendingTask, onboardingDispatch], + [store, onSessionChange, cancelPendingTask, onboardingDispatch], ) const partialRefreshSession = useCallback< SessionApiContext['partialRefreshSession'] >(async () => { - const agent = state.currentAgentState.agent as BskyAppAgent + /* + * Read the live bundle rather than the one captured by this render: a + * dispatch that lands before the next render would otherwise leave this + * holding a disposed bundle, whose agent dispatches unauthenticated. + */ + const bundle = store.getState().currentBundleState + .bundle as unknown as SessionBundle const signal = cancelPendingTask() - const {data} = await agent.com.atproto.server.getSession() + /* getSession targets the PDS; only the persisted account fields are patched. */ + const {data} = await bundle.agent.com.atproto.server.getSession() if (signal.aborted) return store.dispatch({ type: 'partial-refresh-session', - accountDid: agent.session!.did, + /* + * Read the did off the response rather than the session: the bundle may + * have been disposed while the request was in flight, and the live + * getters throw in that state. + */ + accountDid: data.did, patch: { emailConfirmed: data.emailConfirmed, emailAuthFactor: data.emailAuthFactor, }, }) - }, [store, state, cancelPendingTask]) + }, [store, cancelPendingTask]) const removeAccount = useCallback( account => { addSessionDebugLog({ type: 'method:start', method: 'removeAccount', - account, + account: redactAccount(account), }) cancelPendingTask() store.dispatch({ type: 'removed-account', accountDid: account.did, }) - addSessionDebugLog({type: 'method:end', method: 'removeAccount', account}) + addSessionDebugLog({ + type: 'method:end', + method: 'removeAccount', + account: redactAccount(account), + }) clearAgeAssuranceServerDataForDid({did: account.did}) }, [store, cancelPendingTask], @@ -313,7 +489,10 @@ export function Provider({children}: React.PropsWithChildren<{}>) { useEffect(() => { return persisted.onUpdate('session', nextSession => { const synced = nextSession - addSessionDebugLog({type: 'persisted:receive', data: synced}) + addSessionDebugLog({ + type: 'persisted:receive', + data: redactPersistedSession(synced), + }) store.dispatch({ type: 'synced-accounts', syncedAccounts: synced.accounts, @@ -322,38 +501,91 @@ export function Provider({children}: React.PropsWithChildren<{}>) { const syncedAccount = synced.accounts.find( a => a.did === synced.currentAccount?.did, ) + /* + * Cancel pending work when another tab logs out the account this tab + * considers current. Do not cancel unrelated work between logged-out tabs. + */ + const syncedDid = syncedAccount?.refreshJwt + ? syncedAccount.did + : undefined + if ( + syncedDid === undefined && + state.currentBundleState.did !== undefined + ) { + cancelPendingTask() + } if (syncedAccount && syncedAccount.refreshJwt) { - if (syncedAccount.did !== state.currentAgentState.did) { - /* - * Web handling: if leader tab has switched to a diff account that is - * stale, it will refresh the session before triggering the update to - * follower tabs. Follower tabs will therefore receive the fresh - * session. See APP-1960, or ask Eric. - */ + if (syncedAccount.did !== state.currentBundleState.did) { + // The leader refreshes before broadcasting, so followers receive fresh tokens. void resumeSession(syncedAccount) } else { - const agent = state.currentAgentState.agent as AtpAgent - const prevSession = agent.session - // eslint-disable-next-line react-compiler/react-compiler - agent.sessionManager.session = sessionAccountToSession(syncedAccount) - addSessionDebugLog({ - type: 'agent:patch', - agent, - prevSession, - nextSession: agent.session, + /* + * PasswordSession cannot be patched in place. Rebuild from the tokens + * the leader already refreshed, then dispose the previous bundle. + */ + const prevBundle = state.currentBundleState.bundle as unknown as + | SessionBundle + | PublicSessionBundle + // Avoid replacing the live bundle for an unrelated account update. + const live = + prevBundle.session && !prevBundle.session.destroyed + ? prevBundle.session.session + : undefined + if ( + live && + live.accessJwt === syncedAccount.accessJwt && + live.refreshJwt === syncedAccount.refreshJwt + ) { + return + } + const rebuilt = createSessionBundleFromStoredAccount( + syncedAccount, + onSessionChange, + newBundle => { + const current = store.getState() + const latestAccount = current.accounts.find( + account => account.did === syncedAccount.did, + ) + const isCurrent = + current.currentBundleState.bundle === prevBundle && + latestAccount?.accessJwt === syncedAccount.accessJwt && + latestAccount?.refreshJwt === syncedAccount.refreshJwt + if (isCurrent) { + addSessionDebugLog({ + type: 'bundle:patch', + bundleId: getBundleId(newBundle), + prevSession: redactSessionData( + prevBundle.session && !prevBundle.session.destroyed + ? prevBundle.session.session + : undefined, + ), + nextSession: redactSessionData(newBundle.session.session), + }) + } + return isCurrent + }, + ) + if (!rebuilt) { + return + } + const {bundle: newBundle, account: newAccount} = rebuilt + store.dispatch({ + type: 'replaced-current-bundle', + newBundle, + newAccount, }) } } }) - }, [store, state, resumeSession]) + }, [store, state, resumeSession, onSessionChange, cancelPendingTask]) const stateContext = useMemo( () => ({ accounts: state.accounts, currentAccount: state.accounts.find( - a => a.did === state.currentAgentState.did, + a => a.did === state.currentBundleState.did, ), - hasSession: !!state.currentAgentState.did, + hasSession: !!state.currentBundleState.did, }), [state], ) @@ -379,26 +611,38 @@ export function Provider({children}: React.PropsWithChildren<{}>) { ], ) + const bundle = state.currentBundleState.bundle as unknown as + | SessionBundle + | PublicSessionBundle + // @ts-expect-error window type is not declared, debug only // eslint-disable-next-line react-hooks/immutability - if (__DEV__ && IS_WEB) window.agent = state.currentAgentState.agent + if (__DEV__ && IS_WEB) window.agent = bundle.agent - const agent = state.currentAgentState.agent as BskyAppAgent - const currentAgentRef = useRef(agent) + const currentBundleRef = useRef(bundle) + /* + * Disposal is deferred to this post-commit effect deliberately: components may + * still render against the outgoing bundle during the commit that swaps it, so + * tearing its agent down inline would pull the agent out from under them. The + * reducer's bundle-identity guard drops any events the not-yet-disposed session + * emits in that window. + */ useEffect(() => { - if (currentAgentRef.current !== agent) { - // Read the previous value and immediately advance the pointer. - const prevAgent = currentAgentRef.current - currentAgentRef.current = agent - addSessionDebugLog({type: 'agent:switch', prevAgent, nextAgent: agent}) - // We never reuse agents so let's fully neutralize the previous one. - // This ensures it won't try to consume any refresh tokens. - prevAgent.dispose() + if (currentBundleRef.current !== bundle) { + const prevBundle = currentBundleRef.current + currentBundleRef.current = bundle + addSessionDebugLog({ + type: 'bundle:switch', + prevBundleId: getBundleId(prevBundle), + nextBundleId: getBundleId(bundle), + }) + // Replaced bundles must never consume another refresh token. + disposeBundle(prevBundle) } - }, [agent]) + }, [bundle]) return ( - + ) { - + ) } @@ -453,10 +697,13 @@ export function useRequireAuth() { ) } +/** + * The active session's agent, or the public agent when logged out. + */ export function useAgent(): AtpAgent { - const agent = useContext(AgentContext) - if (!agent) { + const bundle = useContext(BundleContext) + if (!bundle) { throw Error('useAgent() must be below .') } - return agent + return bundle.agent } diff --git a/src/state/session/logging.ts b/src/state/session/logging.ts index da017d823f..adc495d54d 100644 --- a/src/state/session/logging.ts +++ b/src/state/session/logging.ts @@ -1,21 +1,140 @@ -import {type AtpSessionData, type AtpSessionEvent} from '@atproto/api' +import {type SessionData} from '@atproto/lex-password-session' import {type Schema} from '../persisted' import {type Action, type State} from './reducer' -import {type SessionAccount} from './types' +import {type AtpSessionEvent, type SessionAccount} from './types' type Reducer = (state: State, action: Action) => State +/** + * An account reduced to the fields that are safe to ship off-device. + * + * Credentials become presence booleans and PII (email, service, pdsUrl) is + * dropped entirely. The log only ever needs to answer "which account was live, + * and did it still hold credentials?". The stubs at the bottom of this file are + * expected to be revived against Sentry or Bitdrift, so no payload type may be + * capable of carrying a live JWT in the first place. + */ +export type RedactedAccount = { + did: string + handle: string + active: boolean | undefined + status: string | undefined + signupQueued: boolean | undefined + hasAccessJwt: boolean + hasRefreshJwt: boolean +} + +/** The account list plus the current did, in redacted form. */ +export type RedactedSessionSnapshot = { + accounts: RedactedAccount[] + currentDid: string | undefined +} + +/** Live session data reduced to identity plus credential presence. */ +export type RedactedSessionData = { + did: string + handle: string + hasAccessJwt: boolean + hasRefreshJwt: boolean +} + +function redact(account: SessionAccount): RedactedAccount { + return { + did: account.did, + handle: account.handle, + active: account.active, + status: account.status, + signupQueued: account.signupQueued, + hasAccessJwt: !!account.accessJwt, + hasRefreshJwt: !!account.refreshJwt, + } +} + +export function redactAccount( + account: SessionAccount | undefined, +): RedactedAccount | undefined { + return account ? redact(account) : undefined +} + +export function redactState(state: State): RedactedSessionSnapshot { + return { + accounts: state.accounts.map(redact), + currentDid: state.currentBundleState.did, + } +} + +export function redactPersistedSession( + data: Schema['session'], +): RedactedSessionSnapshot { + return { + accounts: data.accounts.map(redact), + currentDid: data.currentAccount?.did, + } +} + +export function redactSessionData( + data: SessionData | undefined, +): RedactedSessionData | undefined { + if (!data) return undefined + return { + did: data.did, + handle: data.handle, + hasAccessJwt: !!data.accessJwt, + hasRefreshJwt: !!data.refreshJwt, + } +} + +/* + * Bundles are logged for identity only - which one was live, and which one + * replaced it. Logging the object itself would reach its session, and through + * it the tokens, so each is mapped to an opaque per-run id instead. + */ +const bundleIds = new WeakMap() +const runId = Math.random().toString(36).slice(2) +let nextBundleId = 1 + +export function getBundleId(bundle: object): string { + let id = bundleIds.get(bundle) + if (id === undefined) { + id = runId + '::' + nextBundleId++ + bundleIds.set(bundle, id) + } + return id +} + +/** An action reduced to its discriminant plus the did it targets, if any. */ +type RedactedAction = { + type: Action['type'] + accountDid?: string +} + +function redactAction(action: Action): RedactedAction { + switch (action.type) { + case 'received-session-event': + case 'removed-account': + case 'partial-refresh-session': + return {type: action.type, accountDid: action.accountDid} + case 'switched-to-account': + case 'replaced-current-bundle': + return {type: action.type, accountDid: action.newAccount.did} + case 'synced-accounts': + return {type: action.type, accountDid: action.syncedCurrentDid} + default: + return {type: action.type} + } +} + type Log = | { type: 'reducer:init' - state: State + state: RedactedSessionSnapshot } | { type: 'reducer:call' - action: Action - prevState: State - nextState: State + action: RedactedAction + prevState: RedactedSessionSnapshot + nextState: RedactedSessionSnapshot } | { type: 'method:start' @@ -25,7 +144,7 @@ type Log = | 'logout' | 'resumeSession' | 'removeAccount' - account?: SessionAccount + account?: RedactedAccount } | { type: 'method:end' @@ -35,32 +154,41 @@ type Log = | 'logout' | 'resumeSession' | 'removeAccount' - account?: SessionAccount + account?: RedactedAccount } | { type: 'persisted:broadcast' - data: Schema['session'] + data: RedactedSessionSnapshot } | { type: 'persisted:receive' - data: Schema['session'] + data: RedactedSessionSnapshot } | { - type: 'agent:switch' - prevAgent: object - nextAgent: object + type: 'bundle:switch' + prevBundleId: string + nextBundleId: string } | { - type: 'agent:patch' - agent: object - prevSession: AtpSessionData | undefined - nextSession: AtpSessionData | undefined + /* + * Dev-only bundle-swap log. Bundles are identified by their opaque ids; + * the session snapshots record only identity and credential presence. + */ + type: 'bundle:patch' + bundleId: string + prevSession: RedactedSessionData | undefined + nextSession: RedactedSessionData | undefined } export function wrapSessionReducerForLogging(reducer: Reducer): Reducer { return function loggingWrapper(prevState: State, action: Action): State { const nextState = reducer(prevState, action) - addSessionDebugLog({type: 'reducer:call', prevState, action, nextState}) + addSessionDebugLog({ + type: 'reducer:call', + prevState: redactState(prevState), + action: redactAction(action), + nextState: redactState(nextState), + }) return nextState } } diff --git a/src/state/session/moderation.ts b/src/state/session/moderation.ts index 8fc234d732..2354fab4fa 100644 --- a/src/state/session/moderation.ts +++ b/src/state/session/moderation.ts @@ -1,10 +1,31 @@ import {AtpAgent, BSKY_LABELER_DID} from '@atproto/api' import {IS_TEST_USER} from '#/lib/constants' +import {account as accountStorage} from '#/storage' import {configureAdditionalModerationAuthorities} from './additional-moderation-authorities' -import {readLabelers} from './agent-config' import {type SessionAccount} from './types' +/** + * Cache an account's subscribed labeler DIDs. Called on every preferences + * fetch, so the cache is eventually consistent with the server. + */ +export function saveLabelers(did: string, value: string[]) { + accountStorage.set([did, 'labelers'], value) +} + +/** + * Read the cached labeler DIDs for an account, or `undefined` if none have + * been cached yet (first session on this device) or the entry is unreadable. + */ +export function readLabelers(did: string): string[] | undefined { + try { + return accountStorage.get([did, 'labelers']) + } catch { + /* a corrupt entry fails JSON.parse inside Storage.get; treat as no cache */ + return undefined + } +} + export function configureModerationForGuest() { // This global mutation is *only* OK because this code is only relevant for testing. // Don't add any other global behavior here! @@ -12,7 +33,12 @@ export function configureModerationForGuest() { configureAdditionalModerationAuthorities() } -export async function configureModerationForAccount( +/** + * Configure global app labelers and the account's cached labeler + * subscriptions. Fully synchronous so session setup can apply labeler headers + * in the same tick, before any request goes out. + */ +export function configureModerationForAccount( agent: AtpAgent, account: SessionAccount, ) { @@ -20,11 +46,12 @@ export async function configureModerationForAccount( // Don't add any other global behavior here! switchToBskyAppLabeler() if (IS_TEST_USER(account.handle)) { - await trySwitchToTestAppLabeler(agent) + // Test accounts may briefly use the production authority while this resolves. + void trySwitchToTestAppLabeler(agent) } // The code below is actually relevant to production (and isn't global). - const labelerDids = await readLabelers(account.did).catch(_ => {}) + const labelerDids = readLabelers(account.did) if (labelerDids) { agent.configureLabelersHeader( labelerDids.filter(did => did !== BSKY_LABELER_DID), @@ -41,6 +68,7 @@ function switchToBskyAppLabeler() { AtpAgent.configure({appLabelers: [BSKY_LABELER_DID]}) } +/** Resolve and install the test environment's moderation authority. */ async function trySwitchToTestAppLabeler(agent: AtpAgent) { const did = ( await agent diff --git a/src/state/session/network.ts b/src/state/session/network.ts new file mode 100644 index 0000000000..84e92d564b --- /dev/null +++ b/src/state/session/network.ts @@ -0,0 +1,23 @@ +import {emitNetworkConfirmed, emitNetworkLost} from '#/state/events' + +/* + * Captured once at module load so the wrapper below is immune to later + * monkey-patching of globalThis.fetch. + */ +const realFetch = globalThis.fetch + +/** + * Fetch wrapper that reports network reachability to the app-wide event bus. + * Any resolved response (including HTTP errors) confirms the network is up; a + * thrown error (DNS failure, timeout, offline) reports it as lost. + */ +export const networkAwareFetch: typeof fetch = async (...args) => { + try { + const res = await realFetch(...args) + emitNetworkConfirmed() + return res + } catch (e) { + emitNetworkLost() + throw e + } +} diff --git a/src/state/session/reducer.ts b/src/state/session/reducer.ts index d22dd4a021..8571539f87 100644 --- a/src/state/session/reducer.ts +++ b/src/state/session/reducer.ts @@ -1,43 +1,43 @@ -import {type AtpAgent, type AtpSessionEvent} from '@atproto/api' - import {unregisterPushToken} from '#/lib/notifications/notifications' import {logger} from '#/lib/notifications/util' -import {createPublicAgent} from './agent' import {wrapSessionReducerForLogging} from './logging' -import {type SessionAccount} from './types' +import {createPublicSessionBundle} from './session-core' +import {type AtpSessionEvent, type SessionAccount} from './types' import {createTemporaryAgentsAndResume} from './util' -// A hack so that the reducer can't read anything from the agent. -// From the reducer's point of view, it should be a completely opaque object. -type OpaqueBskyAgent = { +// Keep session internals outside the reducer's static view of a bundle. +type OpaqueSessionBundle = { readonly service: URL - readonly api: unknown - readonly app: unknown - readonly com: unknown } -type AgentState = { - readonly agent: OpaqueBskyAgent +type BundleState = { + readonly bundle: OpaqueSessionBundle readonly did: string | undefined } export type State = { readonly accounts: SessionAccount[] - readonly currentAgentState: AgentState - needsPersist: boolean // Mutated in an effect. + readonly currentBundleState: BundleState + needsPersist: boolean // Cleared after persistence is scheduled. } export type Action = | { - type: 'received-agent-event' - agent: OpaqueBskyAgent + type: 'received-session-event' + bundle: OpaqueSessionBundle accountDid: string refreshedAccount: SessionAccount | undefined sessionEvent: AtpSessionEvent } | { type: 'switched-to-account' - newAgent: OpaqueBskyAgent + newBundle: OpaqueSessionBundle + newAccount: SessionAccount + } + | { + // Replace an immutable session from synced or rescued tokens without rebroadcasting. + type: 'replaced-current-bundle' + newBundle: OpaqueSessionBundle newAccount: SessionAccount } | { @@ -61,9 +61,9 @@ export type Action = patch: Pick } -function createPublicAgentState(): AgentState { +function createPublicBundleState(): BundleState { return { - agent: createPublicAgent(), + bundle: createPublicSessionBundle(), did: undefined, } } @@ -71,22 +71,20 @@ function createPublicAgentState(): AgentState { export function getInitialState(persistedAccounts: SessionAccount[]): State { return { accounts: persistedAccounts, - currentAgentState: createPublicAgentState(), + currentBundleState: createPublicBundleState(), needsPersist: false, } } let reducer = (state: State, action: Action): State => { switch (action.type) { - case 'received-agent-event': { - const {agent, accountDid, refreshedAccount, sessionEvent} = action - if ( - refreshedAccount === undefined && - agent !== state.currentAgentState.agent - ) { - // If the session got cleared out (e.g. due to expiry or network error) but - // this account isn't the active one, don't clear it out at this time. - // This way, if the problem is transient, it'll work on next resume. + case 'received-session-event': { + const {bundle, accountDid, refreshedAccount, sessionEvent} = action + if (bundle !== state.currentBundleState.bundle) { + /* + * Stale bundles must neither log out the current account nor restore + * tokens after logout or an account switch. + */ return state } if (sessionEvent === 'network-error') { @@ -98,7 +96,6 @@ let reducer = (state: State, action: Action): State => { !existingAccount || JSON.stringify(existingAccount) === JSON.stringify(refreshedAccount) ) { - // Fast path without a state update. return state } return { @@ -118,26 +115,40 @@ let reducer = (state: State, action: Action): State => { return a } }), - currentAgentState: refreshedAccount - ? state.currentAgentState - : createPublicAgentState(), // Log out if expired. + currentBundleState: refreshedAccount + ? state.currentBundleState + : createPublicBundleState(), // Log out if expired. needsPersist: true, } } case 'switched-to-account': { - const {newAccount, newAgent} = action + const {newAccount, newBundle} = action return { accounts: [ newAccount, ...state.accounts.filter(a => a.did !== newAccount.did), ], - currentAgentState: { + currentBundleState: { did: newAccount.did, - agent: newAgent, + bundle: newBundle, }, needsPersist: true, } } + case 'replaced-current-bundle': { + const {newBundle, newAccount} = action + return { + ...state, + currentBundleState: { + did: state.currentBundleState.did, + bundle: newBundle, + }, + accounts: state.accounts.map(a => + a.did === newAccount.did ? newAccount : a, + ), + needsPersist: false, // Synced from another tab. Don't persist to avoid cycles. + } + } case 'removed-account': { const {accountDid} = action @@ -159,16 +170,16 @@ let reducer = (state: State, action: Action): State => { return { accounts: state.accounts.filter(a => a.did !== accountDid), - currentAgentState: - state.currentAgentState.did === accountDid - ? createPublicAgentState() // Log out if removing the current one. - : state.currentAgentState, + currentBundleState: + state.currentBundleState.did === accountDid + ? createPublicBundleState() // Log out if removing the current one. + : state.currentBundleState, needsPersist: true, } } case 'logged-out-current-account': { - const {currentAgentState} = state - const accountDid = currentAgentState.did + const {currentBundleState} = state + const accountDid = currentBundleState.did // side effect const account = state.accounts.find(a => a.did === accountDid) if (account && accountDid) { @@ -195,7 +206,7 @@ let reducer = (state: State, action: Action): State => { } : a, ), - currentAgentState: createPublicAgentState(), + currentBundleState: createPublicBundleState(), needsPersist: true, } } @@ -216,7 +227,7 @@ let reducer = (state: State, action: Action): State => { refreshJwt: undefined, accessJwt: undefined, })), - currentAgentState: createPublicAgentState(), + currentBundleState: createPublicBundleState(), needsPersist: true, } } @@ -224,33 +235,19 @@ let reducer = (state: State, action: Action): State => { const {syncedAccounts, syncedCurrentDid} = action return { accounts: syncedAccounts, - currentAgentState: - syncedCurrentDid === state.currentAgentState.did - ? state.currentAgentState - : createPublicAgentState(), // Log out if different user. + currentBundleState: + syncedCurrentDid === state.currentBundleState.did + ? state.currentBundleState + : createPublicBundleState(), // Log out if different user. needsPersist: false, // Synced from another tab. Don't persist to avoid cycles. } } case 'partial-refresh-session': { const {accountDid, patch} = action - const agent = state.currentAgentState.agent as AtpAgent - - /* - * Only mutating values that are safe. Be very careful with this. - */ - if (agent.session) { - agent.session.emailConfirmed = - patch.emailConfirmed ?? agent.session.emailConfirmed - agent.session.emailAuthFactor = - patch.emailAuthFactor ?? agent.session.emailAuthFactor - } + // PasswordSession has no setter; consumers read these fields from the account. return { ...state, - currentAgentState: { - ...state.currentAgentState, - agent, - }, accounts: state.accounts.map(a => { if (a.did === accountDid) { return { diff --git a/src/state/session/session-core.ts b/src/state/session/session-core.ts new file mode 100644 index 0000000000..5e3a9e8a8f --- /dev/null +++ b/src/state/session/session-core.ts @@ -0,0 +1,438 @@ +import { + PasswordSession, + type PasswordSessionOptions, + type SessionData, +} from '@atproto/lex-password-session' + +import {BLUESKY_PROXY_HEADER, PUBLIC_BSKY_SERVICE} from '#/lib/constants' +import {logger} from '#/logger' +import {prefetchAgeAssuranceServerData} from '#/ageAssurance/data' +import {features} from '#/analytics' +import { + BskyAppAgent, + createPublicAgent, + PasswordSessionManager, +} from './bridge-agent' +import {addSessionErrorLog} from './logging' +import {configureModerationForAccount} from './moderation' +import {networkAwareFetch} from './network' +import { + isSessionExpired, + sessionAccountToSessionData, + sessionDataToSessionAccount, +} from './session-data' +import {type AtpSessionEvent, type SessionAccount} from './types' + +export {networkAwareFetch} from './network' +export { + isSignupQueued, + sessionAccountToSessionData, + sessionDataToSessionAccount, +} from './session-data' +export type {AtpSessionEvent} from './types' + +/** + * The service the bundle authenticated against. + * + * `PasswordSession`'s getters throw once the session is destroyed, so the read + * is guarded and falls back to the public service. + */ +function deriveServiceUrl(session: PasswordSession | null): URL { + return new URL( + session && !session.destroyed + ? session.session.service + : PUBLIC_BSKY_SERVICE, + ) +} + +/** An `AtpAgent` bridged over one `PasswordSession`, the bundle's sole auth core. */ +export type SessionBundle = { + session: PasswordSession + agent: BskyAppAgent + readonly service: URL +} + +/** + * `PasswordSession` exposes no local (logout-free) destroy, so disposal is + * implemented by disabling its injected fetch and hooks. Keep that lifecycle + * state private and tied to bundle identity. + */ +const bundleKillSwitches = new WeakMap void>() + +/** + * Register the lifecycle closure used by {@link disposeBundle}. + * + * Disposing also detaches the bridge agent from its session, so a stale + * bundle's `agent.session` / `agent.pdsUrl` read as `undefined` rather than + * serving tokens the app has stopped tracking. + */ +export function registerBundleKillSwitch( + bundle: SessionBundle, + kill: () => void, +) { + bundleKillSwitches.set(bundle, () => { + kill() + bundle.agent.dispose() + }) +} + +/** + * Wrap a session in the bridge agent. + * + * `storedPdsUrl` seeds {@link PasswordSessionManager}'s PDS routing so requests + * made before the first refresh delivers a didDoc still reach the right host. + * Once a didDoc arrives the manager prefers its endpoint. + */ +export function buildBundle( + session: PasswordSession, + storedPdsUrl?: string, +): SessionBundle { + const manager = new PasswordSessionManager(session, { + service: deriveServiceUrl(session).toString(), + pdsUrl: storedPdsUrl, + }) + return { + session, + agent: new BskyAppAgent(manager), + get service() { + return deriveServiceUrl(session) + }, + } +} + +/** + * PasswordSession delivers `sessionData` before updating its live getter. The + * provider uses that payload for rotated tokens and expiry rescue. + */ +export type OnSessionChange = ( + bundle: SessionBundle, + did: string, + event: AtpSessionEvent, + sessionData?: SessionData, +) => void + +/** + * Hooks stay inert during initial session preparation. `kill()` disarms them + * and disables the injected fetch so a disposed session cannot refresh or + * dispatch. + */ +export function makeSessionHooks({ + onSessionChange, + getBundle, + getDid, +}: { + onSessionChange: OnSessionChange + /** Deferred: hooks are created before the bundle exists. */ + getBundle: () => SessionBundle + /** Deferred: hooks are created before the bundle exists. */ + getDid: () => string +}) { + let armed = false + let killed = false + const dispatch = (event: AtpSessionEvent, sessionData?: SessionData) => { + if (!armed) { + return + } + /* + * A hook must never throw. PasswordSession awaits its hooks inside the + * assignment to its internal session promise, so a synchronous throw here + * leaves that promise permanently rejected: every later request fails, and + * because the session is never marked destroyed, disposeBundle cannot even + * see that the bundle is dead. The dispatch path reaches reducer side + * effects and event emitters, so treat it as capable of throwing. + */ + try { + const did = getDid() + onSessionChange(getBundle(), did, event, sessionData) + if (event !== 'update') { + addSessionErrorLog(did, event) + } + } catch (e) { + logger.error(e instanceof Error ? e : String(e), { + message: `session: onSessionChange threw for a '${event}' event`, + }) + } + } + const hooks: PasswordSessionOptions = { + fetch: (input, init) => { + if (killed) { + throw new Error('session disposed') + } + return networkAwareFetch(input, init) + }, + onUpdated(data) { + dispatch('update', data) + }, + onDeleted(data) { + dispatch('expired', data) + }, + onUpdateFailure() { + dispatch('network-error') + }, + } + return Object.assign(hooks, { + arm() { + armed = true + }, + kill() { + killed = true + armed = false + }, + }) +} + +/** The agent exposed while logged out. */ +export type PublicSessionBundle = { + session: null + agent: BskyAppAgent + readonly service: URL +} + +/** + * Build the logged-out bundle. `createPublicAgent` installs the guest + * moderation authorities as part of building the agent. + */ +export function createPublicSessionBundle(): PublicSessionBundle { + return { + session: null, + agent: createPublicAgent(), + service: new URL(PUBLIC_BSKY_SERVICE), + } +} + +/** + * Run the prepare tail shared by the asynchronous factories. + * + * Preparation does real network work, so it can both reject and - when a + * request gets a 401 and the session's own refresh then fails definitively - + * destroy the session underneath us. + * + * A session destroyed during preparation is fatal rather than recoverable. The + * hooks are still disarmed at that point, so the session's `expired` event was + * swallowed and nothing will ever tell the reducer to log the account out; + * returning the bundle anyway would leave the app looking signed in over a + * session that can only make unauthenticated requests. Failing instead matches + * what `CredentialSession.resumeSession` did on a revoked token, and every + * caller already handles a rejected factory. Checking `destroyed` first also + * keeps `PasswordSession`'s `Logged out` getter throw from escaping as the + * opaque rejection a caller would surface, so `snapshot` only ever runs against + * a live session. + * + * Both failure modes dispose: the bundle is fully built by this point, and a + * still-live session left behind would keep its refresh and dispatch paths + * alive with nothing tracking it. (Disposal is a no-op for the destroyed case, + * where the session already refuses to refresh and the bridge agent already + * reads as logged out - but the two paths are indistinguishable to the caller, + * so both go through it.) + */ +export async function finishPreparation( + bundle: SessionBundle, + preparation: Promise, + snapshot: () => T, +): Promise { + try { + await preparation + if (bundle.session.destroyed) { + throw new Error('Session was revoked while it was being prepared') + } + return snapshot() + } catch (e) { + disposeBundle(bundle) + throw e + } +} + +/** + * Resume a stored account into a {@link SessionBundle}. Expired sessions take a + * network resume; still-valid stored tokens take a synchronous no-network fast + * path. Hooks are armed only after the prepare tail resolves. + */ +export async function createSessionBundleAndResume( + storedAccount: SessionAccount, + onSessionChange: OnSessionChange, +): Promise<{account: SessionAccount; bundle: SessionBundle}> { + const gates = features.refresh({strategy: 'prefer-low-latency'}) + let bundle!: SessionBundle + const hooks = makeSessionHooks({ + onSessionChange, + getBundle: () => bundle, + getDid: () => storedAccount.did, + }) + + let session: PasswordSession + const sessionData = sessionAccountToSessionData(storedAccount) + if (isSessionExpired(storedAccount)) { + /* + * The arm latch swallows resume's initial onUpdated event. + * + * There is deliberately no network retry here: `resume` rejects only when + * the session is definitively invalid, and it swallows everything else - + * a failed refresh reports through `onUpdateFailure` and resolves with the + * stale tokens. So an offline cold start now stays signed in with dead + * tokens (requests fail until connectivity returns) rather than throwing + * the way the old `CredentialSession.resumeSession` did, and retrying a + * definitive rejection would only repeat a request that cannot succeed. + */ + session = await PasswordSession.resume(sessionData, hooks) + } else { + // Sync fast path: trust the stored tokens, no network. + session = new PasswordSession(sessionData, hooks) + } + + bundle = buildBundle(session, storedAccount.pdsUrl) + registerBundleKillSwitch(bundle, hooks.kill) + // The returned account is captured again after asynchronous preparation. + const earlyAccount = + sessionDataToSessionAccount( + session.session, + session.session.service, + storedAccount.pdsUrl, + ) ?? storedAccount + + configureModerationForAccount(bundle.agent, earlyAccount) + const aa = prefetchAgeAssuranceServerData({agent: bundle.agent}) + + /* + * The proxy header is applied after the PDS-targeting setup above, so those + * calls run without it. + */ + bundle.agent.configureProxy(BLUESKY_PROXY_HEADER.get()) + + // Preparation may auto-refresh the session while hooks are still disarmed. + const account = await finishPreparation( + bundle, + Promise.all([gates, aa]), + () => + sessionDataToSessionAccount( + session.session, + session.session.service, + storedAccount.pdsUrl, + ) ?? storedAccount, + ) + hooks.arm() + return {account, bundle} +} + +/** + * Log in with credentials and build a {@link SessionBundle}. + */ +export async function createSessionBundleAndLogin( + { + service, + identifier, + password, + authFactorToken, + }: { + service: string + identifier: string + password: string + authFactorToken?: string + }, + onSessionChange: OnSessionChange, +): Promise<{account: SessionAccount; bundle: SessionBundle}> { + let bundle!: SessionBundle + let accountDid = '' + const hooks = makeSessionHooks({ + onSessionChange, + getBundle: () => bundle, + getDid: () => accountDid, + }) + + const session = await PasswordSession.login({ + ...hooks, + service, + identifier, + password, + authFactorToken, + allowTakendown: true, + }) + + bundle = buildBundle(session) + registerBundleKillSwitch(bundle, hooks.kill) + // Seed the hook's did before it is armed. + const earlyAccount = sessionDataToSessionAccountOrThrow(session) + accountDid = earlyAccount.did + + const gates = features.refresh({strategy: 'prefer-fresh-gates'}) + configureModerationForAccount(bundle.agent, earlyAccount) + const aa = prefetchAgeAssuranceServerData({agent: bundle.agent}) + + bundle.agent.configureProxy(BLUESKY_PROXY_HEADER.get()) + + // Preparation may auto-refresh the session while hooks are still disarmed. + const account = await finishPreparation( + bundle, + Promise.all([gates, aa]), + () => sessionDataToSessionAccountOrThrow(session), + ) + hooks.arm() + return {account, bundle} +} + +/** + * Rebuild a bundle synchronously from stored tokens. The optional guard runs + * after construction but before hooks are armed; rejected bundles are disposed. + */ +export function createSessionBundleFromStoredAccount( + storedAccount: SessionAccount, + onSessionChange: OnSessionChange, + shouldActivate: ( + bundle: SessionBundle, + account: SessionAccount, + ) => boolean = () => true, +): {account: SessionAccount; bundle: SessionBundle} | undefined { + let bundle!: SessionBundle + const hooks = makeSessionHooks({ + onSessionChange, + getBundle: () => bundle, + getDid: () => storedAccount.did, + }) + const session = new PasswordSession( + sessionAccountToSessionData(storedAccount), + hooks, + ) + bundle = buildBundle(session, storedAccount.pdsUrl) + registerBundleKillSwitch(bundle, hooks.kill) + configureModerationForAccount(bundle.agent, storedAccount) + bundle.agent.configureProxy(BLUESKY_PROXY_HEADER.get()) + + const account = session.destroyed + ? storedAccount + : (sessionDataToSessionAccount( + session.session, + session.session.service, + storedAccount.pdsUrl, + ) ?? storedAccount) + if (!shouldActivate(bundle, account)) { + disposeBundle(bundle) + return undefined + } + hooks.arm() + return {account, bundle} +} + +export function sessionDataToSessionAccountOrThrow( + session: PasswordSession, +): SessionAccount { + const account = sessionDataToSessionAccount( + session.session, + session.session.service, + ) + if (!account) { + throw Error('Expected an active session') + } + return account +} + +/** + * Disable a replaced bundle without revoking its server session. PasswordSession + * has no local destroy operation, so the registered lifecycle closure disables + * its fetch and hooks instead. + */ +export function disposeBundle(bundle: SessionBundle | PublicSessionBundle) { + const session = bundle.session + if (!session || session.destroyed) { + return + } + bundleKillSwitches.get(bundle)?.() +} diff --git a/src/state/session/session-data.ts b/src/state/session/session-data.ts new file mode 100644 index 0000000000..2fab1cbd99 --- /dev/null +++ b/src/state/session/session-data.ts @@ -0,0 +1,107 @@ +import {type AtpSessionData} from '@atproto/api' +import {getPdsEndpoint, isValidDidDoc} from '@atproto/common-web' +import {type SessionData} from '@atproto/lex-password-session' +import {jwtDecode} from 'jwt-decode' + +import {BSKY_SERVICE} from '#/lib/constants' +import {isJwtExpired} from '#/lib/jwt' +import {hasProp} from '#/lib/type-guards' +import {type SessionAccount} from './types' + +/** Whether an access token was issued for a queued (waitlisted) signup. */ +export function isSignupQueued(accessJwt: string | undefined) { + if (accessJwt) { + const sessData = jwtDecode(accessJwt) + return ( + hasProp(sessData, 'scope') && + sessData.scope === 'com.atproto.signupQueued' + ) + } + return false +} + +/** + * Convert live `PasswordSession` session data into the persisted + * `SessionAccount` snapshot. + * + * The object literal's field order is load-bearing: the reducer's + * `JSON.stringify` fast path and the session test snapshots depend on + * byte-stable serialization. `service` and `pdsUrl` are normalized through + * `new URL().toString()` for a stable trailing slash. + * + * `pdsUrl` comes from the DID document or a pre-refresh stored value. It does + * not fall back to the login service. + */ +export function sessionDataToSessionAccount( + session: SessionData | null | undefined, + service: string, + storedPdsUrl?: string, +): SessionAccount | undefined { + if (!session) { + return undefined + } + const normalizedService = new URL(service).toString() + const didDocPdsUrl = + session.didDoc && isValidDidDoc(session.didDoc) + ? getPdsEndpoint(session.didDoc) + : undefined + const pdsUrl = didDocPdsUrl ?? storedPdsUrl + return { + service: normalizedService, + did: session.did, + handle: session.handle, + email: session.email, + emailConfirmed: session.emailConfirmed || false, + emailAuthFactor: session.emailAuthFactor || false, + refreshJwt: session.refreshJwt, + accessJwt: session.accessJwt, + signupQueued: isSignupQueued(session.accessJwt), + active: session.active, + status: session.status, + pdsUrl: pdsUrl ? new URL(pdsUrl).toString() : undefined, + isSelfHosted: !normalizedService.startsWith(BSKY_SERVICE), + } +} + +/** Convert a persisted account into data suitable for `PasswordSession`. */ +export function sessionAccountToSessionData( + account: SessionAccount, +): SessionData { + return { + accessJwt: account.accessJwt ?? '', + active: account.active ?? true, + did: account.did as SessionData['did'], + email: account.email, + emailAuthFactor: account.emailAuthFactor, + emailConfirmed: account.emailConfirmed, + handle: account.handle as SessionData['handle'], + refreshJwt: account.refreshJwt ?? '', + status: account.status, + service: account.service, + } +} + +/** Convert a persisted account into data suitable for `AtpAgent`. */ +export function sessionAccountToSession( + account: SessionAccount, +): AtpSessionData { + return { + // Sorted in the same property order as when returned by BskyAgent (alphabetical). + accessJwt: account.accessJwt ?? '', + did: account.did, + email: account.email, + emailAuthFactor: account.emailAuthFactor, + emailConfirmed: account.emailConfirmed, + handle: account.handle, + refreshJwt: account.refreshJwt ?? '', + /** + * @see https://github.com/bluesky-social/atproto/blob/c5d36d5ba2a2c2a5c4f366a5621c06a5608e361e/packages/api/src/agent.ts#L188 + */ + active: account.active ?? true, + status: account.status, + } +} + +export function isSessionExpired(account: SessionAccount) { + return account.accessJwt ? isJwtExpired(account.accessJwt) : true +} diff --git a/src/state/session/types.ts b/src/state/session/types.ts index 8a9afba42c..ca1bca62f1 100644 --- a/src/state/session/types.ts +++ b/src/state/session/types.ts @@ -3,6 +3,9 @@ import {type Metrics} from '#/analytics/metrics' export type SessionAccount = PersistedAccount +/** Session-change events understood by the reducer and logging hooks. */ +export type AtpSessionEvent = 'update' | 'expired' | 'network-error' + export type SessionStateContext = { accounts: SessionAccount[] currentAccount: SessionAccount | undefined @@ -44,11 +47,9 @@ export type SessionApiContext = { ) => Promise removeAccount: (account: SessionAccount) => void /** - * Calls `getSession` and updates select fields on the current account and - * `BskyAgent`. This is an alternative to `resumeSession`, which updates - * current account/agent using the `persistSessionHandler`, but is more load - * bearing. This patches in updates without causing any side effects via - * `persistSessionHandler`. + * Calls `getSession` and patches the email fields of the current account. + * Unlike `resumeSession`, this does not rotate tokens or rebuild the session, + * so it produces no session-change side effects. */ partialRefreshSession: () => Promise } diff --git a/src/state/session/util.ts b/src/state/session/util.ts index ea6d817f36..af1c2846c7 100644 --- a/src/state/session/util.ts +++ b/src/state/session/util.ts @@ -1,36 +1,16 @@ import AtpAgent from '@atproto/api' -import {jwtDecode} from 'jwt-decode' -import {isJwtExpired} from '#/lib/jwt' -import {hasProp} from '#/lib/type-guards' import * as persisted from '#/state/persisted' -import {sessionAccountToSession} from './agent' +import {sessionAccountToSession} from './session-data' import {type SessionAccount} from './types' +export {isSessionExpired, isSignupQueued} from './session-data' + export function readLastActiveAccount() { const {currentAccount, accounts} = persisted.get('session') return accounts.find(a => a.did === currentAccount?.did) } -export function isSignupQueued(accessJwt: string | undefined) { - if (accessJwt) { - const sessData = jwtDecode(accessJwt) - return ( - hasProp(sessData, 'scope') && - sessData.scope === 'com.atproto.signupQueued' - ) - } - return false -} - -export function isSessionExpired(account: SessionAccount) { - if (account.accessJwt) { - return isJwtExpired(account.accessJwt) - } else { - return true - } -} - /** * Creates and attempted to resumeSession for every stored session. * Intended to be used to send push token revokations just before logout. diff --git a/src/storage/schema.ts b/src/storage/schema.ts index 2f4424792d..53b4ffb022 100644 --- a/src/storage/schema.ts +++ b/src/storage/schema.ts @@ -108,4 +108,15 @@ export type Account = { * account after a switch, until that account's preferences loaded. */ isBetaUser?: boolean + + /** + * The account's subscribed labeler DIDs, cached from preferences so the + * `atproto-accept-labelers` header can be configured synchronously at + * session start, before preferences load. Eventually consistent: rewritten + * on every preferences fetch (see `saveLabelers` in + * `#/state/session/moderation`). Until the first fetch lands there is + * simply no cache entry and initial requests go out without per-account + * labeler headers. + */ + labelers?: string[] } diff --git a/src/view/com/composer/drafts/state/api.ts b/src/view/com/composer/drafts/state/api.ts index 12ed4acf5a..b8fddc618f 100644 --- a/src/view/com/composer/drafts/state/api.ts +++ b/src/view/com/composer/drafts/state/api.ts @@ -11,7 +11,7 @@ import {mimeToExt} from '#/lib/media/video/util' import {shortenLinks} from '#/lib/strings/rich-text-manip' import {type ComposerImage} from '#/state/gallery' import {threadgateAllowUISettingToAllowRecordValue} from '#/state/queries/threadgate/util' -import {createPublicAgent} from '#/state/session/agent' +import {createPublicAgent} from '#/state/session/bridge-agent' import { type ComposerState, type EmbedDraft, From 532681d5c0f4c53c35908e53ebff2b07ba4380e8 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 13 Aug 2026 22:26:12 +0300 Subject: [PATCH 03/32] [SDK] Add lexicon codegen pipeline (`@atproto/lex`) (#11346) Co-authored-by: Claude Fable 5 --- .github/workflows/lint.yml | 9 +- .gitignore | 3 + .oxlintrc.json | 1 + .prettierignore | 1 + lexicons.json | 1332 +++++++++++++++++ lexicons/app/bsky/actor/defs.json | 994 ++++++++++++ lexicons/app/bsky/actor/getPreferences.json | 30 + lexicons/app/bsky/actor/getProfile.json | 31 + lexicons/app/bsky/actor/getProfiles.json | 45 + lexicons/app/bsky/actor/getSuggestions.json | 54 + lexicons/app/bsky/actor/profile.json | 75 + lexicons/app/bsky/actor/putPreferences.json | 26 + lexicons/app/bsky/actor/searchActors.json | 54 + .../app/bsky/actor/searchActorsTypeahead.json | 48 + lexicons/app/bsky/actor/status.json | 48 + lexicons/app/bsky/ageassurance/begin.json | 61 + lexicons/app/bsky/ageassurance/defs.json | 333 +++++ lexicons/app/bsky/ageassurance/getConfig.json | 18 + lexicons/app/bsky/ageassurance/getState.json | 45 + .../app/bsky/bookmark/createBookmark.json | 37 + lexicons/app/bsky/bookmark/defs.json | 47 + .../app/bsky/bookmark/deleteBookmark.json | 32 + lexicons/app/bsky/bookmark/getBookmarks.json | 46 + lexicons/app/bsky/contact/defs.json | 67 + lexicons/app/bsky/contact/dismissMatch.json | 42 + lexicons/app/bsky/contact/getMatches.json | 60 + lexicons/app/bsky/contact/getSyncStatus.json | 36 + lexicons/app/bsky/contact/importContacts.json | 72 + lexicons/app/bsky/contact/removeData.json | 33 + .../app/bsky/contact/sendNotification.json | 40 + .../bsky/contact/startPhoneVerification.json | 47 + lexicons/app/bsky/contact/verifyPhone.json | 63 + lexicons/app/bsky/draft/createDraft.json | 48 + lexicons/app/bsky/draft/defs.json | 296 ++++ lexicons/app/bsky/draft/deleteDraft.json | 26 + lexicons/app/bsky/draft/getDrafts.json | 46 + lexicons/app/bsky/draft/updateDraft.json | 26 + lexicons/app/bsky/embed/defs.json | 25 + lexicons/app/bsky/embed/external.json | 210 +++ lexicons/app/bsky/embed/gallery.json | 99 ++ .../app/bsky/embed/getEmbedExternalView.json | 62 + lexicons/app/bsky/embed/images.json | 93 ++ lexicons/app/bsky/embed/record.json | 162 ++ lexicons/app/bsky/embed/recordWithMedia.json | 52 + lexicons/app/bsky/embed/video.json | 108 ++ lexicons/app/bsky/feed/defs.json | 545 +++++++ .../app/bsky/feed/describeFeedGenerator.json | 61 + lexicons/app/bsky/feed/generator.json | 73 + lexicons/app/bsky/feed/getActorFeeds.json | 53 + lexicons/app/bsky/feed/getActorLikes.json | 61 + lexicons/app/bsky/feed/getAuthorFeed.json | 77 + lexicons/app/bsky/feed/getFeed.json | 58 + lexicons/app/bsky/feed/getFeedGenerator.json | 49 + lexicons/app/bsky/feed/getFeedGenerators.json | 44 + lexicons/app/bsky/feed/getFeedSkeleton.json | 64 + lexicons/app/bsky/feed/getLikes.json | 90 ++ lexicons/app/bsky/feed/getListFeed.json | 59 + lexicons/app/bsky/feed/getPostThread.json | 66 + lexicons/app/bsky/feed/getPosts.json | 46 + lexicons/app/bsky/feed/getQuotes.json | 68 + lexicons/app/bsky/feed/getRepostedBy.json | 68 + lexicons/app/bsky/feed/getSuggestedFeeds.json | 46 + lexicons/app/bsky/feed/getTimeline.json | 50 + lexicons/app/bsky/feed/like.json | 33 + lexicons/app/bsky/feed/post.json | 146 ++ lexicons/app/bsky/feed/postgate.json | 55 + lexicons/app/bsky/feed/repost.json | 33 + lexicons/app/bsky/feed/searchPosts.json | 113 ++ lexicons/app/bsky/feed/searchPostsV2.json | 251 ++++ lexicons/app/bsky/feed/sendInteractions.json | 40 + lexicons/app/bsky/feed/threadgate.json | 81 + lexicons/app/bsky/graph/block.json | 30 + lexicons/app/bsky/graph/defs.json | 353 +++++ lexicons/app/bsky/graph/follow.json | 33 + .../app/bsky/graph/getActorStarterPacks.json | 53 + lexicons/app/bsky/graph/getBlocks.json | 46 + lexicons/app/bsky/graph/getFollowers.json | 66 + lexicons/app/bsky/graph/getFollows.json | 66 + .../app/bsky/graph/getKnownFollowers.json | 58 + lexicons/app/bsky/graph/getList.json | 59 + lexicons/app/bsky/graph/getListBlocks.json | 46 + lexicons/app/bsky/graph/getListMutes.json | 46 + lexicons/app/bsky/graph/getLists.json | 65 + .../bsky/graph/getListsWithMembership.json | 82 + lexicons/app/bsky/graph/getMutes.json | 46 + lexicons/app/bsky/graph/getRelationships.json | 64 + lexicons/app/bsky/graph/getStarterPack.json | 39 + lexicons/app/bsky/graph/getStarterPacks.json | 45 + .../graph/getStarterPacksWithMembership.json | 71 + .../graph/getSuggestedFollowsByActor.json | 54 + lexicons/app/bsky/graph/list.json | 63 + lexicons/app/bsky/graph/listblock.json | 30 + lexicons/app/bsky/graph/listitem.json | 36 + lexicons/app/bsky/graph/muteActor.json | 34 + lexicons/app/bsky/graph/muteActorList.json | 26 + lexicons/app/bsky/graph/muteThread.json | 26 + .../app/bsky/graph/searchStarterPacks.json | 53 + .../app/bsky/graph/searchStarterPacksV2.json | 57 + lexicons/app/bsky/graph/starterpack.json | 70 + lexicons/app/bsky/graph/unmuteActor.json | 26 + lexicons/app/bsky/graph/unmuteActorList.json | 26 + lexicons/app/bsky/graph/unmuteThread.json | 26 + lexicons/app/bsky/graph/verification.json | 42 + lexicons/app/bsky/labeler/defs.json | 153 ++ lexicons/app/bsky/labeler/getServices.json | 51 + lexicons/app/bsky/labeler/service.json | 59 + .../app/bsky/notification/declaration.json | 29 + lexicons/app/bsky/notification/defs.json | 175 +++ .../app/bsky/notification/getPreferences.json | 30 + .../app/bsky/notification/getUnreadCount.json | 37 + .../listActivitySubscriptions.json | 46 + .../bsky/notification/listNotifications.json | 139 ++ .../notification/putActivitySubscription.json | 50 + .../app/bsky/notification/putPreferences.json | 25 + .../bsky/notification/putPreferencesV2.json | 87 ++ .../app/bsky/notification/registerPush.json | 47 + .../app/bsky/notification/unregisterPush.json | 43 + .../app/bsky/notification/updateSeen.json | 26 + lexicons/app/bsky/richtext/facet.json | 90 ++ lexicons/app/bsky/unspecced/defs.json | 259 ++++ .../bsky/unspecced/getAgeAssuranceState.json | 17 + lexicons/app/bsky/unspecced/getConfig.json | 45 + .../getOnboardingSuggestedStarterPacks.json | 37 + .../unspecced/getPopularFeedGenerators.json | 46 + .../bsky/unspecced/getPostThreadOtherV2.json | 56 + .../app/bsky/unspecced/getPostThreadV2.json | 94 ++ .../app/bsky/unspecced/getSuggestedFeeds.json | 37 + .../unspecced/getSuggestedFeedsSkeleton.json | 42 + .../getSuggestedOnboardingUsers.json | 49 + .../unspecced/getSuggestedStarterPacks.json | 37 + .../getSuggestedStarterPacksSkeleton.json | 42 + .../app/bsky/unspecced/getSuggestedUsers.json | 49 + .../getSuggestedUsersForDiscover.json | 41 + .../getSuggestedUsersForExplore.json | 45 + .../getSuggestedUsersForSeeMore.json | 45 + .../unspecced/getSuggestedUsersSkeleton.json | 54 + .../unspecced/getSuggestionsSkeleton.json | 66 + .../bsky/unspecced/getTaggedSuggestions.json | 47 + .../app/bsky/unspecced/getTrendingTopics.json | 49 + lexicons/app/bsky/unspecced/getTrends.json | 41 + .../app/bsky/unspecced/getTrendsSkeleton.json | 42 + .../app/bsky/unspecced/initAgeAssurance.json | 49 + .../bsky/unspecced/searchActorsSkeleton.json | 67 + .../bsky/unspecced/searchPostsSkeleton.json | 110 ++ .../unspecced/searchStarterPacksSkeleton.json | 63 + lexicons/app/bsky/video/defs.json | 58 + lexicons/app/bsky/video/getJobStatus.json | 37 + lexicons/app/bsky/video/getUploadLimits.json | 37 + lexicons/app/bsky/video/uploadVideo.json | 29 + lexicons/chat/bsky/actor/declaration.json | 37 + lexicons/chat/bsky/actor/defs.json | 105 ++ lexicons/chat/bsky/actor/deleteAccount.json | 17 + .../chat/bsky/actor/exportAccountData.json | 13 + lexicons/chat/bsky/actor/getStatus.json | 36 + lexicons/chat/bsky/convo/acceptConvo.json | 42 + lexicons/chat/bsky/convo/addReaction.json | 73 + lexicons/chat/bsky/convo/defs.json | 1312 ++++++++++++++++ .../chat/bsky/convo/deleteMessageForSelf.json | 45 + lexicons/chat/bsky/convo/getConvo.json | 42 + .../chat/bsky/convo/getConvoAvailability.json | 46 + .../chat/bsky/convo/getConvoForMembers.json | 63 + lexicons/chat/bsky/convo/getConvoMembers.json | 57 + lexicons/chat/bsky/convo/getLog.json | 71 + lexicons/chat/bsky/convo/getMessages.json | 69 + lexicons/chat/bsky/convo/getUnreadCounts.json | 41 + lexicons/chat/bsky/convo/leaveConvo.json | 52 + .../chat/bsky/convo/listConvoRequests.json | 49 + lexicons/chat/bsky/convo/listConvos.json | 77 + lexicons/chat/bsky/convo/lockConvo.json | 51 + lexicons/chat/bsky/convo/muteConvo.json | 45 + lexicons/chat/bsky/convo/removeReaction.json | 69 + lexicons/chat/bsky/convo/sendMessage.json | 48 + .../chat/bsky/convo/sendMessageBatch.json | 75 + lexicons/chat/bsky/convo/unlockConvo.json | 51 + lexicons/chat/bsky/convo/unmuteConvo.json | 45 + lexicons/chat/bsky/convo/updateAllRead.json | 41 + lexicons/chat/bsky/convo/updateRead.json | 48 + lexicons/chat/bsky/embed/joinLink.json | 36 + lexicons/chat/bsky/group/addMembers.json | 88 ++ .../chat/bsky/group/approveJoinRequest.json | 56 + lexicons/chat/bsky/group/createGroup.json | 76 + lexicons/chat/bsky/group/createJoinLink.json | 60 + lexicons/chat/bsky/group/defs.json | 192 +++ lexicons/chat/bsky/group/disableJoinLink.json | 51 + lexicons/chat/bsky/group/editGroup.json | 58 + lexicons/chat/bsky/group/editJoinLink.json | 58 + lexicons/chat/bsky/group/enableJoinLink.json | 54 + .../chat/bsky/group/getJoinLinkPreviews.json | 49 + .../chat/bsky/group/listJoinRequests.json | 60 + .../chat/bsky/group/listMutualGroups.json | 53 + .../chat/bsky/group/rejectJoinRequest.json | 46 + lexicons/chat/bsky/group/removeMembers.json | 57 + lexicons/chat/bsky/group/requestJoin.json | 68 + .../bsky/group/updateJoinRequestsRead.json | 41 + .../chat/bsky/group/withdrawJoinRequest.json | 38 + lexicons/chat/bsky/moderation/defs.json | 81 + .../bsky/moderation/getActorMetadata.json | 70 + lexicons/chat/bsky/moderation/getConvo.json | 42 + .../chat/bsky/moderation/getConvoMembers.json | 57 + lexicons/chat/bsky/moderation/getConvos.json | 45 + .../bsky/moderation/getMessageContext.json | 63 + .../bsky/moderation/subscribeModEvents.json | 675 +++++++++ .../bsky/moderation/updateActorAccess.json | 32 + lexicons/chat/bsky/notification/defs.json | 43 + .../bsky/notification/getPreferences.json | 26 + .../bsky/notification/putPreferences.json | 42 + lexicons/com/atproto/admin/defs.json | 132 ++ lexicons/com/atproto/admin/deleteAccount.json | 26 + .../atproto/admin/disableAccountInvites.json | 30 + .../com/atproto/admin/disableInviteCodes.json | 31 + .../atproto/admin/enableAccountInvites.json | 30 + .../com/atproto/admin/getAccountInfo.json | 30 + .../com/atproto/admin/getAccountInfos.json | 44 + .../com/atproto/admin/getInviteCodes.json | 54 + .../com/atproto/admin/getSubjectStatus.json | 55 + .../com/atproto/admin/searchAccounts.json | 49 + lexicons/com/atproto/admin/sendEmail.json | 56 + .../com/atproto/admin/updateAccountEmail.json | 31 + .../atproto/admin/updateAccountHandle.json | 31 + .../atproto/admin/updateAccountPassword.json | 30 + .../admin/updateAccountSigningKey.json | 32 + .../atproto/admin/updateSubjectStatus.json | 61 + lexicons/com/atproto/identity/defs.json | 30 + .../getRecommendedDidCredentials.json | 38 + .../com/atproto/identity/refreshIdentity.json | 47 + .../requestPlcOperationSignature.json | 11 + lexicons/com/atproto/identity/resolveDid.json | 49 + .../com/atproto/identity/resolveHandle.json | 45 + .../com/atproto/identity/resolveIdentity.json | 45 + .../atproto/identity/signPlcOperation.json | 56 + .../atproto/identity/submitPlcOperation.json | 25 + .../com/atproto/identity/updateHandle.json | 27 + lexicons/com/atproto/label/defs.json | 190 +++ lexicons/com/atproto/label/queryLabels.json | 64 + .../com/atproto/label/subscribeLabels.json | 70 + .../com/atproto/lexicon/resolveLexicon.json | 58 + lexicons/com/atproto/lexicon/schema.json | 24 + .../com/atproto/moderation/createReport.json | 104 ++ lexicons/com/atproto/moderation/defs.json | 96 ++ lexicons/com/atproto/repo/applyWrites.json | 196 +++ lexicons/com/atproto/repo/createRecord.json | 90 ++ lexicons/com/atproto/repo/defs.json | 24 + lexicons/com/atproto/repo/deleteRecord.json | 66 + lexicons/com/atproto/repo/describeRepo.json | 63 + lexicons/com/atproto/repo/getRecord.json | 69 + lexicons/com/atproto/repo/importRepo.json | 14 + .../com/atproto/repo/listMissingBlobs.json | 63 + lexicons/com/atproto/repo/listRecords.json | 86 ++ lexicons/com/atproto/repo/putRecord.json | 98 ++ lexicons/com/atproto/repo/strongRef.json | 25 + lexicons/com/atproto/repo/uploadBlob.json | 28 + .../com/atproto/server/activateAccount.json | 11 + .../atproto/server/checkAccountStatus.json | 58 + lexicons/com/atproto/server/confirmEmail.json | 43 + .../com/atproto/server/createAccount.json | 113 ++ .../com/atproto/server/createAppPassword.json | 65 + .../com/atproto/server/createInviteCode.json | 43 + .../com/atproto/server/createInviteCodes.json | 73 + .../com/atproto/server/createSession.json | 97 ++ .../com/atproto/server/deactivateAccount.json | 24 + lexicons/com/atproto/server/defs.json | 64 + .../com/atproto/server/deleteAccount.json | 42 + .../com/atproto/server/deleteSession.json | 19 + .../com/atproto/server/describeServer.json | 73 + .../atproto/server/getAccountInviteCodes.json | 48 + .../com/atproto/server/getServiceAuth.json | 53 + lexicons/com/atproto/server/getSession.json | 55 + .../com/atproto/server/listAppPasswords.json | 53 + .../com/atproto/server/refreshSession.json | 74 + .../atproto/server/requestAccountDelete.json | 11 + .../server/requestEmailConfirmation.json | 11 + .../atproto/server/requestEmailUpdate.json | 25 + .../atproto/server/requestPasswordReset.json | 25 + .../com/atproto/server/reserveSigningKey.json | 39 + .../com/atproto/server/resetPassword.json | 37 + .../com/atproto/server/revokeAppPassword.json | 25 + lexicons/com/atproto/server/updateEmail.json | 43 + lexicons/com/atproto/sync/defs.json | 17 + lexicons/com/atproto/sync/getBlob.json | 50 + lexicons/com/atproto/sync/getBlocks.json | 52 + lexicons/com/atproto/sync/getCheckout.json | 27 + lexicons/com/atproto/sync/getHead.json | 44 + lexicons/com/atproto/sync/getHostStatus.json | 54 + .../com/atproto/sync/getLatestCommit.json | 58 + lexicons/com/atproto/sync/getRecord.json | 55 + lexicons/com/atproto/sync/getRepo.json | 46 + lexicons/com/atproto/sync/getRepoStatus.json | 65 + lexicons/com/atproto/sync/listBlobs.json | 73 + lexicons/com/atproto/sync/listHosts.json | 70 + lexicons/com/atproto/sync/listRepos.json | 84 ++ .../atproto/sync/listReposByCollection.json | 66 + lexicons/com/atproto/sync/notifyOfUpdate.json | 26 + lexicons/com/atproto/sync/requestCrawl.json | 31 + lexicons/com/atproto/sync/subscribeRepos.json | 276 ++++ .../atproto/temp/checkHandleAvailability.json | 88 ++ .../com/atproto/temp/checkSignupQueue.json | 22 + lexicons/com/germnetwork/declaration.json | 75 + lexicons/tools/ozone/report/defs.json | 212 +++ package.json | 14 +- pnpm-lock.yaml | 410 ++++- pnpm-workspace.yaml | 5 +- src/lib/strings/__tests__/errors.test.ts | 2 +- src/lib/strings/errors.ts | 2 +- src/screens/Signup/state.ts | 2 +- 304 files changed, 22682 insertions(+), 55 deletions(-) create mode 100644 lexicons.json create mode 100644 lexicons/app/bsky/actor/defs.json create mode 100644 lexicons/app/bsky/actor/getPreferences.json create mode 100644 lexicons/app/bsky/actor/getProfile.json create mode 100644 lexicons/app/bsky/actor/getProfiles.json create mode 100644 lexicons/app/bsky/actor/getSuggestions.json create mode 100644 lexicons/app/bsky/actor/profile.json create mode 100644 lexicons/app/bsky/actor/putPreferences.json create mode 100644 lexicons/app/bsky/actor/searchActors.json create mode 100644 lexicons/app/bsky/actor/searchActorsTypeahead.json create mode 100644 lexicons/app/bsky/actor/status.json create mode 100644 lexicons/app/bsky/ageassurance/begin.json create mode 100644 lexicons/app/bsky/ageassurance/defs.json create mode 100644 lexicons/app/bsky/ageassurance/getConfig.json create mode 100644 lexicons/app/bsky/ageassurance/getState.json create mode 100644 lexicons/app/bsky/bookmark/createBookmark.json create mode 100644 lexicons/app/bsky/bookmark/defs.json create mode 100644 lexicons/app/bsky/bookmark/deleteBookmark.json create mode 100644 lexicons/app/bsky/bookmark/getBookmarks.json create mode 100644 lexicons/app/bsky/contact/defs.json create mode 100644 lexicons/app/bsky/contact/dismissMatch.json create mode 100644 lexicons/app/bsky/contact/getMatches.json create mode 100644 lexicons/app/bsky/contact/getSyncStatus.json create mode 100644 lexicons/app/bsky/contact/importContacts.json create mode 100644 lexicons/app/bsky/contact/removeData.json create mode 100644 lexicons/app/bsky/contact/sendNotification.json create mode 100644 lexicons/app/bsky/contact/startPhoneVerification.json create mode 100644 lexicons/app/bsky/contact/verifyPhone.json create mode 100644 lexicons/app/bsky/draft/createDraft.json create mode 100644 lexicons/app/bsky/draft/defs.json create mode 100644 lexicons/app/bsky/draft/deleteDraft.json create mode 100644 lexicons/app/bsky/draft/getDrafts.json create mode 100644 lexicons/app/bsky/draft/updateDraft.json create mode 100644 lexicons/app/bsky/embed/defs.json create mode 100644 lexicons/app/bsky/embed/external.json create mode 100644 lexicons/app/bsky/embed/gallery.json create mode 100644 lexicons/app/bsky/embed/getEmbedExternalView.json create mode 100644 lexicons/app/bsky/embed/images.json create mode 100644 lexicons/app/bsky/embed/record.json create mode 100644 lexicons/app/bsky/embed/recordWithMedia.json create mode 100644 lexicons/app/bsky/embed/video.json create mode 100644 lexicons/app/bsky/feed/defs.json create mode 100644 lexicons/app/bsky/feed/describeFeedGenerator.json create mode 100644 lexicons/app/bsky/feed/generator.json create mode 100644 lexicons/app/bsky/feed/getActorFeeds.json create mode 100644 lexicons/app/bsky/feed/getActorLikes.json create mode 100644 lexicons/app/bsky/feed/getAuthorFeed.json create mode 100644 lexicons/app/bsky/feed/getFeed.json create mode 100644 lexicons/app/bsky/feed/getFeedGenerator.json create mode 100644 lexicons/app/bsky/feed/getFeedGenerators.json create mode 100644 lexicons/app/bsky/feed/getFeedSkeleton.json create mode 100644 lexicons/app/bsky/feed/getLikes.json create mode 100644 lexicons/app/bsky/feed/getListFeed.json create mode 100644 lexicons/app/bsky/feed/getPostThread.json create mode 100644 lexicons/app/bsky/feed/getPosts.json create mode 100644 lexicons/app/bsky/feed/getQuotes.json create mode 100644 lexicons/app/bsky/feed/getRepostedBy.json create mode 100644 lexicons/app/bsky/feed/getSuggestedFeeds.json create mode 100644 lexicons/app/bsky/feed/getTimeline.json create mode 100644 lexicons/app/bsky/feed/like.json create mode 100644 lexicons/app/bsky/feed/post.json create mode 100644 lexicons/app/bsky/feed/postgate.json create mode 100644 lexicons/app/bsky/feed/repost.json create mode 100644 lexicons/app/bsky/feed/searchPosts.json create mode 100644 lexicons/app/bsky/feed/searchPostsV2.json create mode 100644 lexicons/app/bsky/feed/sendInteractions.json create mode 100644 lexicons/app/bsky/feed/threadgate.json create mode 100644 lexicons/app/bsky/graph/block.json create mode 100644 lexicons/app/bsky/graph/defs.json create mode 100644 lexicons/app/bsky/graph/follow.json create mode 100644 lexicons/app/bsky/graph/getActorStarterPacks.json create mode 100644 lexicons/app/bsky/graph/getBlocks.json create mode 100644 lexicons/app/bsky/graph/getFollowers.json create mode 100644 lexicons/app/bsky/graph/getFollows.json create mode 100644 lexicons/app/bsky/graph/getKnownFollowers.json create mode 100644 lexicons/app/bsky/graph/getList.json create mode 100644 lexicons/app/bsky/graph/getListBlocks.json create mode 100644 lexicons/app/bsky/graph/getListMutes.json create mode 100644 lexicons/app/bsky/graph/getLists.json create mode 100644 lexicons/app/bsky/graph/getListsWithMembership.json create mode 100644 lexicons/app/bsky/graph/getMutes.json create mode 100644 lexicons/app/bsky/graph/getRelationships.json create mode 100644 lexicons/app/bsky/graph/getStarterPack.json create mode 100644 lexicons/app/bsky/graph/getStarterPacks.json create mode 100644 lexicons/app/bsky/graph/getStarterPacksWithMembership.json create mode 100644 lexicons/app/bsky/graph/getSuggestedFollowsByActor.json create mode 100644 lexicons/app/bsky/graph/list.json create mode 100644 lexicons/app/bsky/graph/listblock.json create mode 100644 lexicons/app/bsky/graph/listitem.json create mode 100644 lexicons/app/bsky/graph/muteActor.json create mode 100644 lexicons/app/bsky/graph/muteActorList.json create mode 100644 lexicons/app/bsky/graph/muteThread.json create mode 100644 lexicons/app/bsky/graph/searchStarterPacks.json create mode 100644 lexicons/app/bsky/graph/searchStarterPacksV2.json create mode 100644 lexicons/app/bsky/graph/starterpack.json create mode 100644 lexicons/app/bsky/graph/unmuteActor.json create mode 100644 lexicons/app/bsky/graph/unmuteActorList.json create mode 100644 lexicons/app/bsky/graph/unmuteThread.json create mode 100644 lexicons/app/bsky/graph/verification.json create mode 100644 lexicons/app/bsky/labeler/defs.json create mode 100644 lexicons/app/bsky/labeler/getServices.json create mode 100644 lexicons/app/bsky/labeler/service.json create mode 100644 lexicons/app/bsky/notification/declaration.json create mode 100644 lexicons/app/bsky/notification/defs.json create mode 100644 lexicons/app/bsky/notification/getPreferences.json create mode 100644 lexicons/app/bsky/notification/getUnreadCount.json create mode 100644 lexicons/app/bsky/notification/listActivitySubscriptions.json create mode 100644 lexicons/app/bsky/notification/listNotifications.json create mode 100644 lexicons/app/bsky/notification/putActivitySubscription.json create mode 100644 lexicons/app/bsky/notification/putPreferences.json create mode 100644 lexicons/app/bsky/notification/putPreferencesV2.json create mode 100644 lexicons/app/bsky/notification/registerPush.json create mode 100644 lexicons/app/bsky/notification/unregisterPush.json create mode 100644 lexicons/app/bsky/notification/updateSeen.json create mode 100644 lexicons/app/bsky/richtext/facet.json create mode 100644 lexicons/app/bsky/unspecced/defs.json create mode 100644 lexicons/app/bsky/unspecced/getAgeAssuranceState.json create mode 100644 lexicons/app/bsky/unspecced/getConfig.json create mode 100644 lexicons/app/bsky/unspecced/getOnboardingSuggestedStarterPacks.json create mode 100644 lexicons/app/bsky/unspecced/getPopularFeedGenerators.json create mode 100644 lexicons/app/bsky/unspecced/getPostThreadOtherV2.json create mode 100644 lexicons/app/bsky/unspecced/getPostThreadV2.json create mode 100644 lexicons/app/bsky/unspecced/getSuggestedFeeds.json create mode 100644 lexicons/app/bsky/unspecced/getSuggestedFeedsSkeleton.json create mode 100644 lexicons/app/bsky/unspecced/getSuggestedOnboardingUsers.json create mode 100644 lexicons/app/bsky/unspecced/getSuggestedStarterPacks.json create mode 100644 lexicons/app/bsky/unspecced/getSuggestedStarterPacksSkeleton.json create mode 100644 lexicons/app/bsky/unspecced/getSuggestedUsers.json create mode 100644 lexicons/app/bsky/unspecced/getSuggestedUsersForDiscover.json create mode 100644 lexicons/app/bsky/unspecced/getSuggestedUsersForExplore.json create mode 100644 lexicons/app/bsky/unspecced/getSuggestedUsersForSeeMore.json create mode 100644 lexicons/app/bsky/unspecced/getSuggestedUsersSkeleton.json create mode 100644 lexicons/app/bsky/unspecced/getSuggestionsSkeleton.json create mode 100644 lexicons/app/bsky/unspecced/getTaggedSuggestions.json create mode 100644 lexicons/app/bsky/unspecced/getTrendingTopics.json create mode 100644 lexicons/app/bsky/unspecced/getTrends.json create mode 100644 lexicons/app/bsky/unspecced/getTrendsSkeleton.json create mode 100644 lexicons/app/bsky/unspecced/initAgeAssurance.json create mode 100644 lexicons/app/bsky/unspecced/searchActorsSkeleton.json create mode 100644 lexicons/app/bsky/unspecced/searchPostsSkeleton.json create mode 100644 lexicons/app/bsky/unspecced/searchStarterPacksSkeleton.json create mode 100644 lexicons/app/bsky/video/defs.json create mode 100644 lexicons/app/bsky/video/getJobStatus.json create mode 100644 lexicons/app/bsky/video/getUploadLimits.json create mode 100644 lexicons/app/bsky/video/uploadVideo.json create mode 100644 lexicons/chat/bsky/actor/declaration.json create mode 100644 lexicons/chat/bsky/actor/defs.json create mode 100644 lexicons/chat/bsky/actor/deleteAccount.json create mode 100644 lexicons/chat/bsky/actor/exportAccountData.json create mode 100644 lexicons/chat/bsky/actor/getStatus.json create mode 100644 lexicons/chat/bsky/convo/acceptConvo.json create mode 100644 lexicons/chat/bsky/convo/addReaction.json create mode 100644 lexicons/chat/bsky/convo/defs.json create mode 100644 lexicons/chat/bsky/convo/deleteMessageForSelf.json create mode 100644 lexicons/chat/bsky/convo/getConvo.json create mode 100644 lexicons/chat/bsky/convo/getConvoAvailability.json create mode 100644 lexicons/chat/bsky/convo/getConvoForMembers.json create mode 100644 lexicons/chat/bsky/convo/getConvoMembers.json create mode 100644 lexicons/chat/bsky/convo/getLog.json create mode 100644 lexicons/chat/bsky/convo/getMessages.json create mode 100644 lexicons/chat/bsky/convo/getUnreadCounts.json create mode 100644 lexicons/chat/bsky/convo/leaveConvo.json create mode 100644 lexicons/chat/bsky/convo/listConvoRequests.json create mode 100644 lexicons/chat/bsky/convo/listConvos.json create mode 100644 lexicons/chat/bsky/convo/lockConvo.json create mode 100644 lexicons/chat/bsky/convo/muteConvo.json create mode 100644 lexicons/chat/bsky/convo/removeReaction.json create mode 100644 lexicons/chat/bsky/convo/sendMessage.json create mode 100644 lexicons/chat/bsky/convo/sendMessageBatch.json create mode 100644 lexicons/chat/bsky/convo/unlockConvo.json create mode 100644 lexicons/chat/bsky/convo/unmuteConvo.json create mode 100644 lexicons/chat/bsky/convo/updateAllRead.json create mode 100644 lexicons/chat/bsky/convo/updateRead.json create mode 100644 lexicons/chat/bsky/embed/joinLink.json create mode 100644 lexicons/chat/bsky/group/addMembers.json create mode 100644 lexicons/chat/bsky/group/approveJoinRequest.json create mode 100644 lexicons/chat/bsky/group/createGroup.json create mode 100644 lexicons/chat/bsky/group/createJoinLink.json create mode 100644 lexicons/chat/bsky/group/defs.json create mode 100644 lexicons/chat/bsky/group/disableJoinLink.json create mode 100644 lexicons/chat/bsky/group/editGroup.json create mode 100644 lexicons/chat/bsky/group/editJoinLink.json create mode 100644 lexicons/chat/bsky/group/enableJoinLink.json create mode 100644 lexicons/chat/bsky/group/getJoinLinkPreviews.json create mode 100644 lexicons/chat/bsky/group/listJoinRequests.json create mode 100644 lexicons/chat/bsky/group/listMutualGroups.json create mode 100644 lexicons/chat/bsky/group/rejectJoinRequest.json create mode 100644 lexicons/chat/bsky/group/removeMembers.json create mode 100644 lexicons/chat/bsky/group/requestJoin.json create mode 100644 lexicons/chat/bsky/group/updateJoinRequestsRead.json create mode 100644 lexicons/chat/bsky/group/withdrawJoinRequest.json create mode 100644 lexicons/chat/bsky/moderation/defs.json create mode 100644 lexicons/chat/bsky/moderation/getActorMetadata.json create mode 100644 lexicons/chat/bsky/moderation/getConvo.json create mode 100644 lexicons/chat/bsky/moderation/getConvoMembers.json create mode 100644 lexicons/chat/bsky/moderation/getConvos.json create mode 100644 lexicons/chat/bsky/moderation/getMessageContext.json create mode 100644 lexicons/chat/bsky/moderation/subscribeModEvents.json create mode 100644 lexicons/chat/bsky/moderation/updateActorAccess.json create mode 100644 lexicons/chat/bsky/notification/defs.json create mode 100644 lexicons/chat/bsky/notification/getPreferences.json create mode 100644 lexicons/chat/bsky/notification/putPreferences.json create mode 100644 lexicons/com/atproto/admin/defs.json create mode 100644 lexicons/com/atproto/admin/deleteAccount.json create mode 100644 lexicons/com/atproto/admin/disableAccountInvites.json create mode 100644 lexicons/com/atproto/admin/disableInviteCodes.json create mode 100644 lexicons/com/atproto/admin/enableAccountInvites.json create mode 100644 lexicons/com/atproto/admin/getAccountInfo.json create mode 100644 lexicons/com/atproto/admin/getAccountInfos.json create mode 100644 lexicons/com/atproto/admin/getInviteCodes.json create mode 100644 lexicons/com/atproto/admin/getSubjectStatus.json create mode 100644 lexicons/com/atproto/admin/searchAccounts.json create mode 100644 lexicons/com/atproto/admin/sendEmail.json create mode 100644 lexicons/com/atproto/admin/updateAccountEmail.json create mode 100644 lexicons/com/atproto/admin/updateAccountHandle.json create mode 100644 lexicons/com/atproto/admin/updateAccountPassword.json create mode 100644 lexicons/com/atproto/admin/updateAccountSigningKey.json create mode 100644 lexicons/com/atproto/admin/updateSubjectStatus.json create mode 100644 lexicons/com/atproto/identity/defs.json create mode 100644 lexicons/com/atproto/identity/getRecommendedDidCredentials.json create mode 100644 lexicons/com/atproto/identity/refreshIdentity.json create mode 100644 lexicons/com/atproto/identity/requestPlcOperationSignature.json create mode 100644 lexicons/com/atproto/identity/resolveDid.json create mode 100644 lexicons/com/atproto/identity/resolveHandle.json create mode 100644 lexicons/com/atproto/identity/resolveIdentity.json create mode 100644 lexicons/com/atproto/identity/signPlcOperation.json create mode 100644 lexicons/com/atproto/identity/submitPlcOperation.json create mode 100644 lexicons/com/atproto/identity/updateHandle.json create mode 100644 lexicons/com/atproto/label/defs.json create mode 100644 lexicons/com/atproto/label/queryLabels.json create mode 100644 lexicons/com/atproto/label/subscribeLabels.json create mode 100644 lexicons/com/atproto/lexicon/resolveLexicon.json create mode 100644 lexicons/com/atproto/lexicon/schema.json create mode 100644 lexicons/com/atproto/moderation/createReport.json create mode 100644 lexicons/com/atproto/moderation/defs.json create mode 100644 lexicons/com/atproto/repo/applyWrites.json create mode 100644 lexicons/com/atproto/repo/createRecord.json create mode 100644 lexicons/com/atproto/repo/defs.json create mode 100644 lexicons/com/atproto/repo/deleteRecord.json create mode 100644 lexicons/com/atproto/repo/describeRepo.json create mode 100644 lexicons/com/atproto/repo/getRecord.json create mode 100644 lexicons/com/atproto/repo/importRepo.json create mode 100644 lexicons/com/atproto/repo/listMissingBlobs.json create mode 100644 lexicons/com/atproto/repo/listRecords.json create mode 100644 lexicons/com/atproto/repo/putRecord.json create mode 100644 lexicons/com/atproto/repo/strongRef.json create mode 100644 lexicons/com/atproto/repo/uploadBlob.json create mode 100644 lexicons/com/atproto/server/activateAccount.json create mode 100644 lexicons/com/atproto/server/checkAccountStatus.json create mode 100644 lexicons/com/atproto/server/confirmEmail.json create mode 100644 lexicons/com/atproto/server/createAccount.json create mode 100644 lexicons/com/atproto/server/createAppPassword.json create mode 100644 lexicons/com/atproto/server/createInviteCode.json create mode 100644 lexicons/com/atproto/server/createInviteCodes.json create mode 100644 lexicons/com/atproto/server/createSession.json create mode 100644 lexicons/com/atproto/server/deactivateAccount.json create mode 100644 lexicons/com/atproto/server/defs.json create mode 100644 lexicons/com/atproto/server/deleteAccount.json create mode 100644 lexicons/com/atproto/server/deleteSession.json create mode 100644 lexicons/com/atproto/server/describeServer.json create mode 100644 lexicons/com/atproto/server/getAccountInviteCodes.json create mode 100644 lexicons/com/atproto/server/getServiceAuth.json create mode 100644 lexicons/com/atproto/server/getSession.json create mode 100644 lexicons/com/atproto/server/listAppPasswords.json create mode 100644 lexicons/com/atproto/server/refreshSession.json create mode 100644 lexicons/com/atproto/server/requestAccountDelete.json create mode 100644 lexicons/com/atproto/server/requestEmailConfirmation.json create mode 100644 lexicons/com/atproto/server/requestEmailUpdate.json create mode 100644 lexicons/com/atproto/server/requestPasswordReset.json create mode 100644 lexicons/com/atproto/server/reserveSigningKey.json create mode 100644 lexicons/com/atproto/server/resetPassword.json create mode 100644 lexicons/com/atproto/server/revokeAppPassword.json create mode 100644 lexicons/com/atproto/server/updateEmail.json create mode 100644 lexicons/com/atproto/sync/defs.json create mode 100644 lexicons/com/atproto/sync/getBlob.json create mode 100644 lexicons/com/atproto/sync/getBlocks.json create mode 100644 lexicons/com/atproto/sync/getCheckout.json create mode 100644 lexicons/com/atproto/sync/getHead.json create mode 100644 lexicons/com/atproto/sync/getHostStatus.json create mode 100644 lexicons/com/atproto/sync/getLatestCommit.json create mode 100644 lexicons/com/atproto/sync/getRecord.json create mode 100644 lexicons/com/atproto/sync/getRepo.json create mode 100644 lexicons/com/atproto/sync/getRepoStatus.json create mode 100644 lexicons/com/atproto/sync/listBlobs.json create mode 100644 lexicons/com/atproto/sync/listHosts.json create mode 100644 lexicons/com/atproto/sync/listRepos.json create mode 100644 lexicons/com/atproto/sync/listReposByCollection.json create mode 100644 lexicons/com/atproto/sync/notifyOfUpdate.json create mode 100644 lexicons/com/atproto/sync/requestCrawl.json create mode 100644 lexicons/com/atproto/sync/subscribeRepos.json create mode 100644 lexicons/com/atproto/temp/checkHandleAvailability.json create mode 100644 lexicons/com/atproto/temp/checkSignupQueue.json create mode 100644 lexicons/com/germnetwork/declaration.json create mode 100644 lexicons/tools/ozone/report/defs.json diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index f07079b5c0..e63de19c73 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -22,7 +22,14 @@ jobs: fail-fast: false matrix: job: - [lint, prettier, 'typecheck:ios', 'typecheck:android', 'typecheck:web'] + [ + lint, + prettier, + 'lexicons:verify', + 'typecheck:ios', + 'typecheck:android', + 'typecheck:web', + ] steps: - name: ⬇️ Check out Git repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 diff --git a/.gitignore b/.gitignore index 8a38445859..a9d64c266c 100644 --- a/.gitignore +++ b/.gitignore @@ -117,6 +117,9 @@ src/locale/locales/**/messages.js src/locale/locales/**/messages.mjs src/locale/locales/**/messages.ts +# generated lexicon schemas (pnpm lexicons:generate) +src/lexicons/ + # local builds *.apk *.aab diff --git a/.oxlintrc.json b/.oxlintrc.json index 63739f1f48..5868f74bb8 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -62,6 +62,7 @@ "bskyogcard/**", "src/locale/locales/_build/**", "src/locale/locales/**/*.js", + "src/lexicons/**", "*.e2e.ts", "*.e2e.tsx", "eslint.config.mjs", diff --git a/.prettierignore b/.prettierignore index afc71dbac1..be1658124a 100644 --- a/.prettierignore +++ b/.prettierignore @@ -18,6 +18,7 @@ android ios src/locale/locales +src/lexicons lib/react-compiler-runtime bskyweb/static coverage diff --git a/lexicons.json b/lexicons.json new file mode 100644 index 0000000000..614e09e817 --- /dev/null +++ b/lexicons.json @@ -0,0 +1,1332 @@ +{ + "version": 1, + "lexicons": [ + "app.bsky.actor.defs", + "app.bsky.actor.getPreferences", + "app.bsky.actor.getProfile", + "app.bsky.actor.getProfiles", + "app.bsky.actor.getSuggestions", + "app.bsky.actor.profile", + "app.bsky.actor.putPreferences", + "app.bsky.actor.searchActors", + "app.bsky.actor.searchActorsTypeahead", + "app.bsky.actor.status", + "app.bsky.ageassurance.begin", + "app.bsky.ageassurance.defs", + "app.bsky.ageassurance.getConfig", + "app.bsky.ageassurance.getState", + "app.bsky.bookmark.createBookmark", + "app.bsky.bookmark.defs", + "app.bsky.bookmark.deleteBookmark", + "app.bsky.bookmark.getBookmarks", + "app.bsky.contact.defs", + "app.bsky.contact.dismissMatch", + "app.bsky.contact.getMatches", + "app.bsky.contact.getSyncStatus", + "app.bsky.contact.importContacts", + "app.bsky.contact.removeData", + "app.bsky.contact.sendNotification", + "app.bsky.contact.startPhoneVerification", + "app.bsky.contact.verifyPhone", + "app.bsky.draft.createDraft", + "app.bsky.draft.defs", + "app.bsky.draft.deleteDraft", + "app.bsky.draft.getDrafts", + "app.bsky.draft.updateDraft", + "app.bsky.embed.defs", + "app.bsky.embed.external", + "app.bsky.embed.gallery", + "app.bsky.embed.getEmbedExternalView", + "app.bsky.embed.images", + "app.bsky.embed.record", + "app.bsky.embed.recordWithMedia", + "app.bsky.embed.video", + "app.bsky.feed.defs", + "app.bsky.feed.describeFeedGenerator", + "app.bsky.feed.generator", + "app.bsky.feed.getActorFeeds", + "app.bsky.feed.getActorLikes", + "app.bsky.feed.getAuthorFeed", + "app.bsky.feed.getFeed", + "app.bsky.feed.getFeedGenerator", + "app.bsky.feed.getFeedGenerators", + "app.bsky.feed.getFeedSkeleton", + "app.bsky.feed.getLikes", + "app.bsky.feed.getListFeed", + "app.bsky.feed.getPostThread", + "app.bsky.feed.getPosts", + "app.bsky.feed.getQuotes", + "app.bsky.feed.getRepostedBy", + "app.bsky.feed.getSuggestedFeeds", + "app.bsky.feed.getTimeline", + "app.bsky.feed.like", + "app.bsky.feed.post", + "app.bsky.feed.postgate", + "app.bsky.feed.repost", + "app.bsky.feed.searchPosts", + "app.bsky.feed.searchPostsV2", + "app.bsky.feed.sendInteractions", + "app.bsky.feed.threadgate", + "app.bsky.graph.block", + "app.bsky.graph.defs", + "app.bsky.graph.follow", + "app.bsky.graph.getActorStarterPacks", + "app.bsky.graph.getBlocks", + "app.bsky.graph.getFollowers", + "app.bsky.graph.getFollows", + "app.bsky.graph.getKnownFollowers", + "app.bsky.graph.getList", + "app.bsky.graph.getListBlocks", + "app.bsky.graph.getListMutes", + "app.bsky.graph.getLists", + "app.bsky.graph.getListsWithMembership", + "app.bsky.graph.getMutes", + "app.bsky.graph.getRelationships", + "app.bsky.graph.getStarterPack", + "app.bsky.graph.getStarterPacks", + "app.bsky.graph.getStarterPacksWithMembership", + "app.bsky.graph.getSuggestedFollowsByActor", + "app.bsky.graph.list", + "app.bsky.graph.listblock", + "app.bsky.graph.listitem", + "app.bsky.graph.muteActor", + "app.bsky.graph.muteActorList", + "app.bsky.graph.muteThread", + "app.bsky.graph.searchStarterPacks", + "app.bsky.graph.searchStarterPacksV2", + "app.bsky.graph.starterpack", + "app.bsky.graph.unmuteActor", + "app.bsky.graph.unmuteActorList", + "app.bsky.graph.unmuteThread", + "app.bsky.graph.verification", + "app.bsky.labeler.defs", + "app.bsky.labeler.getServices", + "app.bsky.labeler.service", + "app.bsky.notification.declaration", + "app.bsky.notification.defs", + "app.bsky.notification.getPreferences", + "app.bsky.notification.getUnreadCount", + "app.bsky.notification.listActivitySubscriptions", + "app.bsky.notification.listNotifications", + "app.bsky.notification.putActivitySubscription", + "app.bsky.notification.putPreferences", + "app.bsky.notification.putPreferencesV2", + "app.bsky.notification.registerPush", + "app.bsky.notification.unregisterPush", + "app.bsky.notification.updateSeen", + "app.bsky.richtext.facet", + "app.bsky.video.defs", + "app.bsky.video.getJobStatus", + "app.bsky.video.getUploadLimits", + "app.bsky.video.uploadVideo", + "chat.bsky.actor.declaration", + "chat.bsky.actor.defs", + "chat.bsky.actor.deleteAccount", + "chat.bsky.actor.exportAccountData", + "chat.bsky.actor.getStatus", + "chat.bsky.convo.acceptConvo", + "chat.bsky.convo.addReaction", + "chat.bsky.convo.defs", + "chat.bsky.convo.deleteMessageForSelf", + "chat.bsky.convo.getConvo", + "chat.bsky.convo.getConvoAvailability", + "chat.bsky.convo.getConvoForMembers", + "chat.bsky.convo.getConvoMembers", + "chat.bsky.convo.getLog", + "chat.bsky.convo.getMessages", + "chat.bsky.convo.getUnreadCounts", + "chat.bsky.convo.leaveConvo", + "chat.bsky.convo.listConvoRequests", + "chat.bsky.convo.listConvos", + "chat.bsky.convo.lockConvo", + "chat.bsky.convo.muteConvo", + "chat.bsky.convo.removeReaction", + "chat.bsky.convo.sendMessage", + "chat.bsky.convo.sendMessageBatch", + "chat.bsky.convo.unlockConvo", + "chat.bsky.convo.unmuteConvo", + "chat.bsky.convo.updateAllRead", + "chat.bsky.convo.updateRead", + "chat.bsky.embed.joinLink", + "chat.bsky.group.addMembers", + "chat.bsky.group.approveJoinRequest", + "chat.bsky.group.createGroup", + "chat.bsky.group.createJoinLink", + "chat.bsky.group.defs", + "chat.bsky.group.disableJoinLink", + "chat.bsky.group.editGroup", + "chat.bsky.group.editJoinLink", + "chat.bsky.group.enableJoinLink", + "chat.bsky.group.getJoinLinkPreviews", + "chat.bsky.group.listJoinRequests", + "chat.bsky.group.listMutualGroups", + "chat.bsky.group.rejectJoinRequest", + "chat.bsky.group.removeMembers", + "chat.bsky.group.requestJoin", + "chat.bsky.group.updateJoinRequestsRead", + "chat.bsky.group.withdrawJoinRequest", + "chat.bsky.moderation.defs", + "chat.bsky.moderation.getActorMetadata", + "chat.bsky.moderation.getConvo", + "chat.bsky.moderation.getConvoMembers", + "chat.bsky.moderation.getConvos", + "chat.bsky.moderation.getMessageContext", + "chat.bsky.moderation.subscribeModEvents", + "chat.bsky.moderation.updateActorAccess", + "chat.bsky.notification.defs", + "chat.bsky.notification.getPreferences", + "chat.bsky.notification.putPreferences", + "com.atproto.admin.defs", + "com.atproto.admin.deleteAccount", + "com.atproto.admin.disableAccountInvites", + "com.atproto.admin.disableInviteCodes", + "com.atproto.admin.enableAccountInvites", + "com.atproto.admin.getAccountInfo", + "com.atproto.admin.getAccountInfos", + "com.atproto.admin.getInviteCodes", + "com.atproto.admin.getSubjectStatus", + "com.atproto.admin.searchAccounts", + "com.atproto.admin.sendEmail", + "com.atproto.admin.updateAccountEmail", + "com.atproto.admin.updateAccountHandle", + "com.atproto.admin.updateAccountPassword", + "com.atproto.admin.updateAccountSigningKey", + "com.atproto.admin.updateSubjectStatus", + "com.atproto.identity.defs", + "com.atproto.identity.getRecommendedDidCredentials", + "com.atproto.identity.refreshIdentity", + "com.atproto.identity.requestPlcOperationSignature", + "com.atproto.identity.resolveDid", + "com.atproto.identity.resolveHandle", + "com.atproto.identity.resolveIdentity", + "com.atproto.identity.signPlcOperation", + "com.atproto.identity.submitPlcOperation", + "com.atproto.identity.updateHandle", + "com.atproto.label.defs", + "com.atproto.label.queryLabels", + "com.atproto.label.subscribeLabels", + "com.atproto.lexicon.resolveLexicon", + "com.atproto.lexicon.schema", + "com.atproto.moderation.createReport", + "com.atproto.moderation.defs", + "com.atproto.repo.applyWrites", + "com.atproto.repo.createRecord", + "com.atproto.repo.defs", + "com.atproto.repo.deleteRecord", + "com.atproto.repo.describeRepo", + "com.atproto.repo.getRecord", + "com.atproto.repo.importRepo", + "com.atproto.repo.listMissingBlobs", + "com.atproto.repo.listRecords", + "com.atproto.repo.putRecord", + "com.atproto.repo.strongRef", + "com.atproto.repo.uploadBlob", + "com.atproto.server.activateAccount", + "com.atproto.server.checkAccountStatus", + "com.atproto.server.confirmEmail", + "com.atproto.server.createAccount", + "com.atproto.server.createAppPassword", + "com.atproto.server.createInviteCode", + "com.atproto.server.createInviteCodes", + "com.atproto.server.createSession", + "com.atproto.server.deactivateAccount", + "com.atproto.server.defs", + "com.atproto.server.deleteAccount", + "com.atproto.server.deleteSession", + "com.atproto.server.describeServer", + "com.atproto.server.getAccountInviteCodes", + "com.atproto.server.getServiceAuth", + "com.atproto.server.getSession", + "com.atproto.server.listAppPasswords", + "com.atproto.server.refreshSession", + "com.atproto.server.requestAccountDelete", + "com.atproto.server.requestEmailConfirmation", + "com.atproto.server.requestEmailUpdate", + "com.atproto.server.requestPasswordReset", + "com.atproto.server.reserveSigningKey", + "com.atproto.server.resetPassword", + "com.atproto.server.revokeAppPassword", + "com.atproto.server.updateEmail", + "com.atproto.sync.defs", + "com.atproto.sync.getBlob", + "com.atproto.sync.getBlocks", + "com.atproto.sync.getCheckout", + "com.atproto.sync.getHead", + "com.atproto.sync.getHostStatus", + "com.atproto.sync.getLatestCommit", + "com.atproto.sync.getRecord", + "com.atproto.sync.getRepo", + "com.atproto.sync.getRepoStatus", + "com.atproto.sync.listBlobs", + "com.atproto.sync.listHosts", + "com.atproto.sync.listRepos", + "com.atproto.sync.listReposByCollection", + "com.atproto.sync.notifyOfUpdate", + "com.atproto.sync.requestCrawl", + "com.atproto.sync.subscribeRepos", + "com.germnetwork.declaration", + "tools.ozone.report.defs" + ], + "resolutions": { + "app.bsky.actor.defs": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.actor.defs", + "cid": "bafyreiaxduxpwdpjgvve3klfs4flkwjwfqiurszw4o6jvjarpqqmeqwiza" + }, + "app.bsky.actor.getPreferences": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.actor.getPreferences", + "cid": "bafyreiftuoh6n5npkyelejxbzettltmgl46xm3qot5nkn7lm365ej2ahii" + }, + "app.bsky.actor.getProfile": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.actor.getProfile", + "cid": "bafyreigrtosreva7e5m7bwbbfsmw77gkdnieizgxwpobw5iobuck3j54xa" + }, + "app.bsky.actor.getProfiles": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.actor.getProfiles", + "cid": "bafyreihphrndnt2cvnv3nk442f6kohdnpagk4xip74f2jn36uoal25vhta" + }, + "app.bsky.actor.getSuggestions": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.actor.getSuggestions", + "cid": "bafyreig7dhef4df3n72bj6pvklzija26yk5szxdzlixm5x72bingowmf3m" + }, + "app.bsky.actor.profile": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.actor.profile", + "cid": "bafyreia6umzg3a6d7mjbow4p57tviey45muohklhgsvjoamcctoiusr4pe" + }, + "app.bsky.actor.putPreferences": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.actor.putPreferences", + "cid": "bafyreietfhhwbwb3ltcffsonhw5p62z233frhe6edls67su5a3evqrykju" + }, + "app.bsky.actor.searchActors": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.actor.searchActors", + "cid": "bafyreidfkzp3tcbxvqo3aty2prhnaiqoxxbjhrapaqmko2gtnymtgxyqdi" + }, + "app.bsky.actor.searchActorsTypeahead": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.actor.searchActorsTypeahead", + "cid": "bafyreigydtxvpnrndvk7iws4uypd6adqzohcdafeuqajiqmmbcqq5vatmu" + }, + "app.bsky.actor.status": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.actor.status", + "cid": "bafyreifdg4b64wohpwkh5lydc6tckvol2rspnpni6dec6recy2rhvlnz4a" + }, + "app.bsky.ageassurance.begin": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.ageassurance.begin", + "cid": "bafyreieqogxqzohb5fdtw5jiqckscgipkbrtvluoodscm45ajw2ba26zpm" + }, + "app.bsky.ageassurance.defs": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.ageassurance.defs", + "cid": "bafyreig2g6pata4a2yjc7aiqoyybi3jzu76trduxlswte2gzhqdx252sie" + }, + "app.bsky.ageassurance.getConfig": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.ageassurance.getConfig", + "cid": "bafyreifpfw7n3y6vqhiyqaqiyrmxm3lypkamyfk7dk6jzho7ujjujou6xq" + }, + "app.bsky.ageassurance.getState": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.ageassurance.getState", + "cid": "bafyreialq63q3obxzsrzbwx25stjmtha3frlifzbgkvtthr535oal3467y" + }, + "app.bsky.bookmark.createBookmark": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.bookmark.createBookmark", + "cid": "bafyreiegg2yu2jl5kfswirsylpn56ro2pj7lkfsufvrnv7xik7xdw27kfi" + }, + "app.bsky.bookmark.defs": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.bookmark.defs", + "cid": "bafyreifi3xao6wlbsnrzt36xnrfcvl5pil7jw7owrgqpmppfzy52dpy7e4" + }, + "app.bsky.bookmark.deleteBookmark": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.bookmark.deleteBookmark", + "cid": "bafyreig6rcgoqayi4fwgx5k5va3cnfsjykvc3fes7rmcfpszdd745hvatu" + }, + "app.bsky.bookmark.getBookmarks": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.bookmark.getBookmarks", + "cid": "bafyreicnazdriby4i3uu3nelt5ur4o3575ivjuw5pxlfj46tvfmglpny2e" + }, + "app.bsky.contact.defs": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.contact.defs", + "cid": "bafyreidsqrf3mf6g3zphne4t2swm6hbgd3ojk2nzlmwj6ezsvq62o3abgu" + }, + "app.bsky.contact.dismissMatch": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.contact.dismissMatch", + "cid": "bafyreihtnfxggnq2n6ijlvrmtmpspwz3pzceprcfzqpa6iwjftpoknkory" + }, + "app.bsky.contact.getMatches": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.contact.getMatches", + "cid": "bafyreihk7c54sgsxyh3bhtwd3cf5kdzjh4lauwwpbogau3zm65acfc3b2u" + }, + "app.bsky.contact.getSyncStatus": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.contact.getSyncStatus", + "cid": "bafyreicozb7ibbr4h6xeh23jdjpfs6thxvodudypurdwqajwdnnj7lrioi" + }, + "app.bsky.contact.importContacts": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.contact.importContacts", + "cid": "bafyreihzcos4jxddkpsk3isoxpgmw2x4rqbvp4b3f475cxwnfdzngnl7by" + }, + "app.bsky.contact.removeData": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.contact.removeData", + "cid": "bafyreidtbulghr5cv27rzcato5esrgigddaafde2kgkz6hnivaimclynla" + }, + "app.bsky.contact.sendNotification": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.contact.sendNotification", + "cid": "bafyreietl2pnpkuxeip5ftk7xvvrkuzdu4ndsn2nldt4wf2vbteyj5v6ju" + }, + "app.bsky.contact.startPhoneVerification": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.contact.startPhoneVerification", + "cid": "bafyreif6jrd6gkonvwlvfkv2wyx5tnk4ayuv2xihcao7kyzdms42s34bhe" + }, + "app.bsky.contact.verifyPhone": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.contact.verifyPhone", + "cid": "bafyreiapgsesqmksilfc6nxndzglbxquxtmk3gwiikie44rangg2drplti" + }, + "app.bsky.draft.createDraft": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.draft.createDraft", + "cid": "bafyreid43zfsfg4wk4p3mf3gihwhpqh4d5rmpw7l5n3zks3kl53bsrsaba" + }, + "app.bsky.draft.defs": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.draft.defs", + "cid": "bafyreiewy3wgajj4al6xifjc2e5gpg4yyj4laf2gl7pe3liekulfzxnlcu" + }, + "app.bsky.draft.deleteDraft": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.draft.deleteDraft", + "cid": "bafyreicxw6nrrrikakfdaue3x45vfp4w62mncscdkpivpeom2orx2ae3su" + }, + "app.bsky.draft.getDrafts": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.draft.getDrafts", + "cid": "bafyreiewac2qwebwwtm4mtvk3aobtsyhbqug3hplhftjlbbn6bvuuxyumy" + }, + "app.bsky.draft.updateDraft": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.draft.updateDraft", + "cid": "bafyreicjy2aatoptybrtogj3nckokmkjpbmelvkej5bkfvxfcs5enu2fhu" + }, + "app.bsky.embed.defs": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.embed.defs", + "cid": "bafyreia42uud4qil67wknywzbxfyxc3b7woewsii54cakq2ould3ldetei" + }, + "app.bsky.embed.external": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.embed.external", + "cid": "bafyreigwach36azu4xg3jvcmb6fe53umfln3wkuj5srqsvcjaypa6rw534" + }, + "app.bsky.embed.gallery": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.embed.gallery", + "cid": "bafyreia4tixag4sadkirfo63qfma6fz3ifbqanify4e6ce6swkl5lwjnfq" + }, + "app.bsky.embed.getEmbedExternalView": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.embed.getEmbedExternalView", + "cid": "bafyreicnv2umobvrfrlo6tqw7umcsxv77owcwit3vld7l46fphpy5uuzky" + }, + "app.bsky.embed.images": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.embed.images", + "cid": "bafyreifw77mkqkzlvm5csjwutkrnaivsjvlhg2qxdlwwabqpahlyyjrpwi" + }, + "app.bsky.embed.record": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.embed.record", + "cid": "bafyreia7yq4c6rqkz32fjf6gj5rcvxsirguducn545s3dvls7qutnn2ali" + }, + "app.bsky.embed.recordWithMedia": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.embed.recordWithMedia", + "cid": "bafyreidszvpeqf7copdmowm4ln4eyhahlqxktlvootjjjdmtkwy6s62jya" + }, + "app.bsky.embed.video": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.embed.video", + "cid": "bafyreie3nug4ezpwodl6yrpgv5edkazzn22t7ea4yaeuun4rctyekkngai" + }, + "app.bsky.feed.defs": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.feed.defs", + "cid": "bafyreibg2zvz4n25vtd5yfqlodqra72s4h4lz7vwdexoutv3523wbw6jca" + }, + "app.bsky.feed.describeFeedGenerator": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.feed.describeFeedGenerator", + "cid": "bafyreigzxk3um5hbi7gbuh5bhmob75bxlfh5bonfsl7h5dmt7woxdhxq54" + }, + "app.bsky.feed.generator": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.feed.generator", + "cid": "bafyreig2rppxkltsbe3523gavulrgprwjkyyb7mvmor22fdkoas6hrc2ju" + }, + "app.bsky.feed.getActorFeeds": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.feed.getActorFeeds", + "cid": "bafyreihqvune35emnr3vby6ldv2tzy57cfcgst2qy5icwpeay4mdvughfq" + }, + "app.bsky.feed.getActorLikes": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.feed.getActorLikes", + "cid": "bafyreiaxgdm2fv6bjzh2hdixgc3teprs7z46ja273kzevjnsp3hi224xoy" + }, + "app.bsky.feed.getAuthorFeed": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.feed.getAuthorFeed", + "cid": "bafyreiha5ca2lkguxvh22smjuehn2evxhyqk7isuiyqy6b23raljriv5a4" + }, + "app.bsky.feed.getFeed": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.feed.getFeed", + "cid": "bafyreihnhxqdfxojwo4xnedj55kskjki3opd2oiz3pfa5nm2ijr5janlq4" + }, + "app.bsky.feed.getFeedGenerator": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.feed.getFeedGenerator", + "cid": "bafyreibdzaan7vboli3ywi7wqzkibc2puukhfpg5rcdgizxlr7fbxx5gcy" + }, + "app.bsky.feed.getFeedGenerators": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.feed.getFeedGenerators", + "cid": "bafyreiervdq422lralzyssoewrfstcmqxsqimtmdskkqeflllszjnq32em" + }, + "app.bsky.feed.getFeedSkeleton": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.feed.getFeedSkeleton", + "cid": "bafyreidmn4nltqygzpd3vzobfkrkxcypbfirremfhzuluhlavwp5ht637m" + }, + "app.bsky.feed.getLikes": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.feed.getLikes", + "cid": "bafyreib6ylalknwxizbtbwsdmja4rhjswwczut5ivjdavfgew27m7knqou" + }, + "app.bsky.feed.getListFeed": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.feed.getListFeed", + "cid": "bafyreifvox2w2c6ybgioi6ofgp2i6pz44guwvo76w26miv4hpy7fgiwvlq" + }, + "app.bsky.feed.getPostThread": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.feed.getPostThread", + "cid": "bafyreif37upfzfu7t4rv3pqzz3swu2oc4nrxdxfr3xqjsrrmgyckneitta" + }, + "app.bsky.feed.getPosts": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.feed.getPosts", + "cid": "bafyreihy4tanaizkynghab46n3qztsspvoaqtbrkzinvxi4igeylg5ghji" + }, + "app.bsky.feed.getQuotes": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.feed.getQuotes", + "cid": "bafyreiftjmrnfpwtri5v47e7yfdgjgpfqds6kx3duf3t2x7vn7qdtbttfy" + }, + "app.bsky.feed.getRepostedBy": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.feed.getRepostedBy", + "cid": "bafyreif4v4aucwv4xvwnub6w2npmioqpa6nn4t5dsbqophoyfnimhsuweq" + }, + "app.bsky.feed.getSuggestedFeeds": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.feed.getSuggestedFeeds", + "cid": "bafyreif4v4aldmcoc2nkyanu6t3ks4jazqsiffq5gp54tuyn5o6rk2ff5e" + }, + "app.bsky.feed.getTimeline": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.feed.getTimeline", + "cid": "bafyreihumvisufuf6t7ibkyhg4psri37pj535mhmyr4t6mn7ogvgcxpul4" + }, + "app.bsky.feed.like": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.feed.like", + "cid": "bafyreiasvjidnd3sazbx46rzgipfkpejq4mgfuzwv2xrdaesp64t4dl2ry" + }, + "app.bsky.feed.post": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.feed.post", + "cid": "bafyreia7rgxu7467phwpcgzaw62l5bzmxufkpikrc5zm4q3xhjmuhq4pti" + }, + "app.bsky.feed.postgate": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.feed.postgate", + "cid": "bafyreiai5efexyluyptv5tbl6kqbqlnneczqzexcqnxmitmulyjfaftgva" + }, + "app.bsky.feed.repost": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.feed.repost", + "cid": "bafyreihks43jtwanviarzctwx2vvcyloivuxo2kdi7ex24gog5dfn4s5au" + }, + "app.bsky.feed.searchPosts": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.feed.searchPosts", + "cid": "bafyreif3xl6jj27tktxmxr3gtmf74wuxj6y6xnoycs7cclvifh4ncsi6mi" + }, + "app.bsky.feed.searchPostsV2": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.feed.searchPostsV2", + "cid": "bafyreiemtjvwfo6xuab6vkwbji46d7wms5xdzad6vygjymmcqr326quwqu" + }, + "app.bsky.feed.sendInteractions": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.feed.sendInteractions", + "cid": "bafyreicaoiskxpn2lmbzxuvpzawbsqxoewko3drbsxms222obabj2dkque" + }, + "app.bsky.feed.threadgate": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.feed.threadgate", + "cid": "bafyreiht77wd6duduz4yqp62m6dwma5dy7gdihps4g2nd73acfzqlglvdi" + }, + "app.bsky.graph.block": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.graph.block", + "cid": "bafyreifalnveckyi2375ek5b5uyg4ef23mktcdktmgiwwsb3rygdls7owm" + }, + "app.bsky.graph.defs": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.graph.defs", + "cid": "bafyreifcipomli7yggtl46xufgxlnrw7se6xmsdxmzgfcz2tiu76ljatxm" + }, + "app.bsky.graph.follow": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.graph.follow", + "cid": "bafyreiav7u7jchqtsxr2yy3gsazfqqziw5tcrfniuprrmoariruj3cft5u" + }, + "app.bsky.graph.getActorStarterPacks": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.graph.getActorStarterPacks", + "cid": "bafyreihm57pc7dmeig4lxcv3x53z53y27obkkxnopimax7dbtqx4enqf7i" + }, + "app.bsky.graph.getBlocks": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.graph.getBlocks", + "cid": "bafyreic3yp4tuxthww5sidvv27gmg3ha3argl3moconhmcccwvooobggve" + }, + "app.bsky.graph.getFollowers": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.graph.getFollowers", + "cid": "bafyreiaboxa4aux2y5jxut5kw4iqlnctlswklofvgkhxl2wn7rkbjjrzru" + }, + "app.bsky.graph.getFollows": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.graph.getFollows", + "cid": "bafyreicun6mwmjgrdmrtmnfqv3ws37g5okcmo3uiaf2qwopk3bhtj5rqae" + }, + "app.bsky.graph.getKnownFollowers": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.graph.getKnownFollowers", + "cid": "bafyreia3bjqeaj5uk2zlkhk5pipefehy2mlrdp3hoqtgu3np3tvbg6xjuq" + }, + "app.bsky.graph.getList": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.graph.getList", + "cid": "bafyreigplofixg524nkirazfh3loe36lvqqeg6jfqvrggzztspkefwm4re" + }, + "app.bsky.graph.getListBlocks": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.graph.getListBlocks", + "cid": "bafyreiagod73i7ecuzpyxwlibrwimo7u4ew75nzc2b3ajdocc5sg6eszpy" + }, + "app.bsky.graph.getListMutes": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.graph.getListMutes", + "cid": "bafyreictd7aefsbizbn66bmhfbyvxbirbuz4uiqcp4o5tb3eiyun42wm4a" + }, + "app.bsky.graph.getLists": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.graph.getLists", + "cid": "bafyreiffrqdk7vdses2qzrmmfotwyvncuhvq7anaw64x7wmlz5cix5nihy" + }, + "app.bsky.graph.getListsWithMembership": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.graph.getListsWithMembership", + "cid": "bafyreifgvypq6ahfxtkbdmudh7bb7vib6c7kvkit6ks3gjoagor747cxfy" + }, + "app.bsky.graph.getMutes": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.graph.getMutes", + "cid": "bafyreibnvq62dmchuulpzvvl6w2x6z4mofnqdrfbsvc5ynoo6cazr7wove" + }, + "app.bsky.graph.getRelationships": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.graph.getRelationships", + "cid": "bafyreiabszfuqanet2tbyyx7plxmknfegygnfyaydhwyialt4cbo24k5dq" + }, + "app.bsky.graph.getStarterPack": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.graph.getStarterPack", + "cid": "bafyreia77gh56vqe3ws26ntm7pffj43uhvl6l66vpb7uhxrhpih6rvne6m" + }, + "app.bsky.graph.getStarterPacks": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.graph.getStarterPacks", + "cid": "bafyreighxbnsgugwwjnu37nzoca7qtpipvf2l5ijz2z74ghdfrgppqfoaa" + }, + "app.bsky.graph.getStarterPacksWithMembership": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.graph.getStarterPacksWithMembership", + "cid": "bafyreigpeb22avssmmfqiveh3hem6qnxrduyyhkxc3ixpfgehmhkskgssy" + }, + "app.bsky.graph.getSuggestedFollowsByActor": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.graph.getSuggestedFollowsByActor", + "cid": "bafyreih4zuul6xlijankc73dx7bax3gbeva2dlfnzabk335y7iaptxbmr4" + }, + "app.bsky.graph.list": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.graph.list", + "cid": "bafyreihqckm5nkbamel2pljhk5lbjfqfmczmb7a5eglpic7ontdx3r4sey" + }, + "app.bsky.graph.listblock": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.graph.listblock", + "cid": "bafyreieomtmr6mpvgnbbgwfkdqlyatwymvu4vdnhldpqetzczlj4eywxqq" + }, + "app.bsky.graph.listitem": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.graph.listitem", + "cid": "bafyreiegh2vyadmieea52lnt574lfcddprhpuz5moqhqiij56egfvyf62m" + }, + "app.bsky.graph.muteActor": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.graph.muteActor", + "cid": "bafyreiebgeqbzitzytuvuu23nxgiyxjdh3iawcxonsiu6t2aubqd6ugt6m" + }, + "app.bsky.graph.muteActorList": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.graph.muteActorList", + "cid": "bafyreihadi2x64bpzncb2xfo5ww4mvajftf4qd3nxmsjud7assloxzfm44" + }, + "app.bsky.graph.muteThread": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.graph.muteThread", + "cid": "bafyreib6ppci3qzye6wktkm4byxtb5mnl2vg22fm7oawcdvof2tfogx4dy" + }, + "app.bsky.graph.searchStarterPacks": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.graph.searchStarterPacks", + "cid": "bafyreia446ip6mbnwpml6hlvxab7jtsud7zczde3u6zmnsa3op4bpxu7um" + }, + "app.bsky.graph.searchStarterPacksV2": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.graph.searchStarterPacksV2", + "cid": "bafyreiftevu66nkth5uprlwhoxtxloz4aqym25hu3wj2bal2vcuupa6uky" + }, + "app.bsky.graph.starterpack": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.graph.starterpack", + "cid": "bafyreiejgf46gibbkpmjtgt5yyiir3wyxgrtvnfxtcdeyxrdrdnsvwgjc4" + }, + "app.bsky.graph.unmuteActor": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.graph.unmuteActor", + "cid": "bafyreihqc6o54652e56jmvzpaf7ix4liws4bmnd2bklc2qkuqjozfcvka4" + }, + "app.bsky.graph.unmuteActorList": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.graph.unmuteActorList", + "cid": "bafyreiamqj5gh6x36dzdk6ve7kfjvzotqjfcg2nplgch7woshqrubbe2ee" + }, + "app.bsky.graph.unmuteThread": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.graph.unmuteThread", + "cid": "bafyreiahpkqj4v7b3ujk45ufdcvynu2dxjtunzi2oipu7kzdupybmysgzu" + }, + "app.bsky.graph.verification": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.graph.verification", + "cid": "bafyreidn7abfzt5p4gcujys2w4seigbhcfnscos6fmh4hnce2gdormbwpa" + }, + "app.bsky.labeler.defs": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.labeler.defs", + "cid": "bafyreicxx5i36v5dbqk5vvfzhnta5gajrvc544mnepux4wksrkid7mw3q4" + }, + "app.bsky.labeler.getServices": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.labeler.getServices", + "cid": "bafyreife7p4pjij5jvmngdlxfq4cjzqe2cx7himnkf7zgxsykcxg6klpl4" + }, + "app.bsky.labeler.service": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.labeler.service", + "cid": "bafyreihr336i3ys6rc4ezhk2wwpeyufnp2ma2n6dxljd2ogksi4sjo6aqe" + }, + "app.bsky.notification.declaration": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.notification.declaration", + "cid": "bafyreibjnupqb7xke5eeeehpcm2gecacduwd4ebifcovgigzdpo42jq3kq" + }, + "app.bsky.notification.defs": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.notification.defs", + "cid": "bafyreifzktxnol5rafjoznauk5j7ub2gsxxm7qjskg3vqmcgn6krrsz55u" + }, + "app.bsky.notification.getPreferences": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.notification.getPreferences", + "cid": "bafyreibaz7cyl7ytpextx7bv6oaguadj5bdvpf4hksw6t737zkfw5y3rue" + }, + "app.bsky.notification.getUnreadCount": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.notification.getUnreadCount", + "cid": "bafyreih2vxe7vy3hcp7fwka7cwenxbkx2d7nd2s3whffh23dn3wxt65s7m" + }, + "app.bsky.notification.listActivitySubscriptions": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.notification.listActivitySubscriptions", + "cid": "bafyreien2hslzbpoypvz3shv6tphkmdtccejieg5lricwl6stdjomiu3zi" + }, + "app.bsky.notification.listNotifications": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.notification.listNotifications", + "cid": "bafyreigqqfk3gvslu36fvmx3fc3yi4ibmsmbfvty55rq4vw7cvfhecpxge" + }, + "app.bsky.notification.putActivitySubscription": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.notification.putActivitySubscription", + "cid": "bafyreifvbgf53yncsrjpzwa5xwnlax7vyesflkg6fmxb2367vfa6fv7kwu" + }, + "app.bsky.notification.putPreferences": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.notification.putPreferences", + "cid": "bafyreihbqjdbhra45czd5m5665btugrie7bwsiwzehutjdwygyy5dsq4fi" + }, + "app.bsky.notification.putPreferencesV2": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.notification.putPreferencesV2", + "cid": "bafyreiefun5klfddbnd7gfodnlcbn7ing45qo6lbmsahau7qgw4zlgv42i" + }, + "app.bsky.notification.registerPush": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.notification.registerPush", + "cid": "bafyreiayl6ngk3ckmaorocgbwxf2ruqgficcbkm2xvoja2crnyul66u3hy" + }, + "app.bsky.notification.unregisterPush": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.notification.unregisterPush", + "cid": "bafyreiboilq7jye3s7isayixm6iqjbsxrfzizaj727obtdda6qzh56at6y" + }, + "app.bsky.notification.updateSeen": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.notification.updateSeen", + "cid": "bafyreigzds576ji7wzi6alzn4wegtfzk2iqagwtugbw3byb2v3xvqho4hi" + }, + "app.bsky.richtext.facet": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.richtext.facet", + "cid": "bafyreidg56eo7zynf6ihz4xb627vwoqf5idnevkmwp7sxc4tijg6xngbu4" + }, + "app.bsky.video.defs": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.video.defs", + "cid": "bafyreibxabe2aejhtnjbwsnt7va4bvge35p7lzcjt2nwmf2c4taww46yyq" + }, + "app.bsky.video.getJobStatus": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.video.getJobStatus", + "cid": "bafyreia5xkrbastzzbpodjm242orufiswrz5aasjqrqtuvb6lmmfwixmqy" + }, + "app.bsky.video.getUploadLimits": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.video.getUploadLimits", + "cid": "bafyreidlw3fozroigsbrz73jtqdoc7dkdpxi4oie3df7nb3vnycy2kkz2y" + }, + "app.bsky.video.uploadVideo": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/app.bsky.video.uploadVideo", + "cid": "bafyreibvsdivxr54ooykqhsy3i6n6f7223hbfnnnzs4h6xrzr5kvvztuvy" + }, + "chat.bsky.actor.declaration": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/chat.bsky.actor.declaration", + "cid": "bafyreih4ivey4vqreb2ebujlt7iranqyizxkv5kpm3ksdlgkinkgcu53pu" + }, + "chat.bsky.actor.defs": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/chat.bsky.actor.defs", + "cid": "bafyreigiv5fo5iufd3ahma6tgtozm35xibvx276nhjsjwuvsvxh3zj7u7q" + }, + "chat.bsky.actor.deleteAccount": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/chat.bsky.actor.deleteAccount", + "cid": "bafyreierykzslwmkkugpyio3yiaxguttd5his5i4ow7re7wzcbxfogerrq" + }, + "chat.bsky.actor.exportAccountData": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/chat.bsky.actor.exportAccountData", + "cid": "bafyreigwllvtmdetzfvgrzwhtzjilnj3gfxaj45yd32oe436etzp7yghai" + }, + "chat.bsky.actor.getStatus": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/chat.bsky.actor.getStatus", + "cid": "bafyreib7f6wo5xsnfz37ywggtvqbcuxigf325q6pezshyhzyp2n5pdlzhu" + }, + "chat.bsky.convo.acceptConvo": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/chat.bsky.convo.acceptConvo", + "cid": "bafyreih2ob7rsibl3h3so356rcdo2fop3dosxkna75w2u45qflbtw2r7qm" + }, + "chat.bsky.convo.addReaction": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/chat.bsky.convo.addReaction", + "cid": "bafyreia2odzz7wk5vfdjxchttbr44ojcxks72ytdmeybpub6zqt3l327vi" + }, + "chat.bsky.convo.defs": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/chat.bsky.convo.defs", + "cid": "bafyreibpb3r5pqc3yrjslx7ntfl5vvgdlgqanuwbcvq3q7sfhayi75x7uu" + }, + "chat.bsky.convo.deleteMessageForSelf": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/chat.bsky.convo.deleteMessageForSelf", + "cid": "bafyreicsfbucwa3y5jdtzv6focgl5dao2lhmaa22jljllwjp6rz4noxowe" + }, + "chat.bsky.convo.getConvo": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/chat.bsky.convo.getConvo", + "cid": "bafyreidjox5oa467rn7x6xjuko23w5ekypjbkdryia4hz5s7isswhxrilu" + }, + "chat.bsky.convo.getConvoAvailability": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/chat.bsky.convo.getConvoAvailability", + "cid": "bafyreidnzzp2b7w4pedgnspgwlzl7oc4uuwxlbh3asteu6s4rgcuagnzl4" + }, + "chat.bsky.convo.getConvoForMembers": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/chat.bsky.convo.getConvoForMembers", + "cid": "bafyreicheufh7jkrstr6hgpggylildyh3k5tihbh5747dumqe5nabp24fi" + }, + "chat.bsky.convo.getConvoMembers": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/chat.bsky.convo.getConvoMembers", + "cid": "bafyreigup3axpdj3r3p4s3x7qgarrysft3vognwzgetl4bn3n2bgocxl4m" + }, + "chat.bsky.convo.getLog": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/chat.bsky.convo.getLog", + "cid": "bafyreicc4wwkmh5rskpzyriwad4lte64rk2w7vnzzzofy6tnhbisxsint4" + }, + "chat.bsky.convo.getMessages": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/chat.bsky.convo.getMessages", + "cid": "bafyreif2fjad4zrbjwhbuk3ny7yfnv6sjyr6llrveh2dr4i2aovwe77yia" + }, + "chat.bsky.convo.getUnreadCounts": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/chat.bsky.convo.getUnreadCounts", + "cid": "bafyreibuu7abhy74gxfprerxpiimjit43fhcrrewe43zhili7rmr4crrsi" + }, + "chat.bsky.convo.leaveConvo": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/chat.bsky.convo.leaveConvo", + "cid": "bafyreigdjs5t2jod6zojwnj5giqucsad257vhocykv6fftkj3qkqzqlqua" + }, + "chat.bsky.convo.listConvoRequests": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/chat.bsky.convo.listConvoRequests", + "cid": "bafyreieoce2dsrg6czlm7tkfi7glxjvqh6wwvfq777s5zxi6hyzqjeqo3e" + }, + "chat.bsky.convo.listConvos": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/chat.bsky.convo.listConvos", + "cid": "bafyreifxh52sx6i7vrlj2xi5h3yd56dmawkplcfdsfiu4npou6rutz4pge" + }, + "chat.bsky.convo.lockConvo": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/chat.bsky.convo.lockConvo", + "cid": "bafyreidnrgfxtyg233wfvasho34p34k2tiekrpiticcl4sau7k2masydby" + }, + "chat.bsky.convo.muteConvo": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/chat.bsky.convo.muteConvo", + "cid": "bafyreihpyxtotf6ltd6w6ayoirar7obxoqlijljov6yrb7r5qztq3ag74e" + }, + "chat.bsky.convo.removeReaction": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/chat.bsky.convo.removeReaction", + "cid": "bafyreiagvcoqprw5jew2m4uoeg6i32wu6ld2ltgjgb2d2s36k4o6i7arb4" + }, + "chat.bsky.convo.sendMessage": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/chat.bsky.convo.sendMessage", + "cid": "bafyreih7yu45wtptd7qjg6iomrugxucfw7zkkz7ijjj7o3bddee3od2q3m" + }, + "chat.bsky.convo.sendMessageBatch": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/chat.bsky.convo.sendMessageBatch", + "cid": "bafyreifvinecphums77xvuuspqgjkgfa7cpre3zavtrkh5tffmi643q2rq" + }, + "chat.bsky.convo.unlockConvo": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/chat.bsky.convo.unlockConvo", + "cid": "bafyreiaind2trvtuo2bgqupx4acxx5vvfhvvo4xwencwcf2trnrithwkya" + }, + "chat.bsky.convo.unmuteConvo": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/chat.bsky.convo.unmuteConvo", + "cid": "bafyreifbip4w4q4dnm2bvhtaxr4ut3yrzck4xkdu2ifnkoggyqigsuasbe" + }, + "chat.bsky.convo.updateAllRead": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/chat.bsky.convo.updateAllRead", + "cid": "bafyreiewx6wmllc4nujgtakvu4jiapnrjtbqyxvugohuu7xfy3bhrn4wq4" + }, + "chat.bsky.convo.updateRead": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/chat.bsky.convo.updateRead", + "cid": "bafyreihba5bopb5vnlqqvtavipybqgemp6itwrz4xysnp6yi6iow5ntnv4" + }, + "chat.bsky.embed.joinLink": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/chat.bsky.embed.joinLink", + "cid": "bafyreib3z65a2myai4hc55vxa7woue4bmelo2a4dlgnawps746w3l2y72q" + }, + "chat.bsky.group.addMembers": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/chat.bsky.group.addMembers", + "cid": "bafyreiftelalu5yc3zctgch7req6syovskusqgma5vyyd4elas5rs47tvy" + }, + "chat.bsky.group.approveJoinRequest": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/chat.bsky.group.approveJoinRequest", + "cid": "bafyreicykc372f7ebk7c3wrco75wgox3poyop7w3duovvn2piqzsvhzthq" + }, + "chat.bsky.group.createGroup": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/chat.bsky.group.createGroup", + "cid": "bafyreidhalpchhospn2hwox54navotl4svw4pmt33aynj22gp52ofoxzau" + }, + "chat.bsky.group.createJoinLink": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/chat.bsky.group.createJoinLink", + "cid": "bafyreicxjzsyjrqbkwd7vztmaxstgulx5naln55gsppix27pjnoz5okv7i" + }, + "chat.bsky.group.defs": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/chat.bsky.group.defs", + "cid": "bafyreiexck6gwns5nesq44mqgsikwrjkgplz33cemxaq365xwdcxeoqjje" + }, + "chat.bsky.group.disableJoinLink": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/chat.bsky.group.disableJoinLink", + "cid": "bafyreiholtxhmuqku64fkv6rjl2rn3jyicutkcuieoviys3zdzx6lh43n4" + }, + "chat.bsky.group.editGroup": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/chat.bsky.group.editGroup", + "cid": "bafyreifwwj2mzridii6sm6wlc5sxgu7g6k67bijvylus4jnbvfcup7ecfe" + }, + "chat.bsky.group.editJoinLink": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/chat.bsky.group.editJoinLink", + "cid": "bafyreiafmbnein4xpbr76jzofk53ut6u5opeuwyorilkhpe3u2kzgavlde" + }, + "chat.bsky.group.enableJoinLink": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/chat.bsky.group.enableJoinLink", + "cid": "bafyreib45q6ccuobjlws7sysjojabo6gvww6wlgy7dif5ejkoasb23l7su" + }, + "chat.bsky.group.getJoinLinkPreviews": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/chat.bsky.group.getJoinLinkPreviews", + "cid": "bafyreigrglu52cdsrc7sxxflwoxr6eg6paw4zsd7ioy7kyue2kqfdjyqn4" + }, + "chat.bsky.group.listJoinRequests": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/chat.bsky.group.listJoinRequests", + "cid": "bafyreigoj66wx3sx7nbav4ywxckk7ggjklxvy2w5tmmnrapib7nfpgh3ci" + }, + "chat.bsky.group.listMutualGroups": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/chat.bsky.group.listMutualGroups", + "cid": "bafyreihp23gsqmaie6uzouxfqi6zwhr3wqglqjblbcawkp6asirzmqq4oi" + }, + "chat.bsky.group.rejectJoinRequest": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/chat.bsky.group.rejectJoinRequest", + "cid": "bafyreiaztaygpnfglcldsdbh5dunovp2nz3kenetahycpymoxxrxkafiha" + }, + "chat.bsky.group.removeMembers": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/chat.bsky.group.removeMembers", + "cid": "bafyreidfkumfvrtsseaavuhs2lfblcsmrjbkeaeojh2u576jze5xrfcasy" + }, + "chat.bsky.group.requestJoin": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/chat.bsky.group.requestJoin", + "cid": "bafyreiatzqdp7zlnbqmbti6xt3orag7yqyuqr6tqzwzzp52gq7hlu3it6q" + }, + "chat.bsky.group.updateJoinRequestsRead": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/chat.bsky.group.updateJoinRequestsRead", + "cid": "bafyreieod4o3cdfkhev6tia63q6yrdk3pgwvrrpyxggswxpp6pzwjj2jyi" + }, + "chat.bsky.group.withdrawJoinRequest": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/chat.bsky.group.withdrawJoinRequest", + "cid": "bafyreicdn25bcyya4for6vtdhhkjaeyfbkldgaxqtqoh5hfwljbrxszezy" + }, + "chat.bsky.moderation.defs": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/chat.bsky.moderation.defs", + "cid": "bafyreiee3r5j2psnahumolqrbr6gj6adqt3ov3xflbeibnyfzlwwc3e5km" + }, + "chat.bsky.moderation.getActorMetadata": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/chat.bsky.moderation.getActorMetadata", + "cid": "bafyreifo2qp6ozex6phd5dd2sewpyltlnzijjqmfl5xpujxk3mkx6b4pze" + }, + "chat.bsky.moderation.getConvo": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/chat.bsky.moderation.getConvo", + "cid": "bafyreie6ds2blosf5zeg7eralqp5jmbbvsv4befevms3nyqoihblzxwzsy" + }, + "chat.bsky.moderation.getConvoMembers": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/chat.bsky.moderation.getConvoMembers", + "cid": "bafyreicm4fvldxqk5zdwopjjrsukl7wpib2ixqgkfc3nrszorx46i4np34" + }, + "chat.bsky.moderation.getConvos": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/chat.bsky.moderation.getConvos", + "cid": "bafyreif5k5jqz4546omvosnx2sey4apq4nebiizwo3xit6q2knf2tteoey" + }, + "chat.bsky.moderation.getMessageContext": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/chat.bsky.moderation.getMessageContext", + "cid": "bafyreih436hnrsenvuaelqk5bz4esvjku2kz3blwh7lk5fmmjfzn5uxaei" + }, + "chat.bsky.moderation.subscribeModEvents": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/chat.bsky.moderation.subscribeModEvents", + "cid": "bafyreifb7xeucadmhsbzezibkn4hxyezzajfydjfyy3tjmwm7bwjmiam3u" + }, + "chat.bsky.moderation.updateActorAccess": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/chat.bsky.moderation.updateActorAccess", + "cid": "bafyreif3kv6qzqf533kjoodzu6s2cf3qypyafsntemeir6uuac4dnnzlwe" + }, + "chat.bsky.notification.defs": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/chat.bsky.notification.defs", + "cid": "bafyreifv3xy2nhjxvr4uqfheezyp3bo2eczvihqto4kp4mmkirnpkmmt4u" + }, + "chat.bsky.notification.getPreferences": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/chat.bsky.notification.getPreferences", + "cid": "bafyreicfjpp5ldakcwyo7wmq6e5kpwtizn5owdd7cux4pbif6s2u6dbxne" + }, + "chat.bsky.notification.putPreferences": { + "uri": "at://did:plc:4v4y5r3lwsbtmsxhile2ljac/com.atproto.lexicon.schema/chat.bsky.notification.putPreferences", + "cid": "bafyreiatnvy5wssiuzrjtdtv3sd2yarrd3xiaijpeuvsnopq3neh52bfay" + }, + "com.atproto.admin.defs": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.admin.defs", + "cid": "bafyreicuagseedq4ko6lx6ublh6agdyeaox4pkvbaq7yj54s2d66yjk2h4" + }, + "com.atproto.admin.deleteAccount": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.admin.deleteAccount", + "cid": "bafyreigzxdce4ztwo2n5xxahr6fwd4bozc4budnaepl26mbejpa3q2ess4" + }, + "com.atproto.admin.disableAccountInvites": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.admin.disableAccountInvites", + "cid": "bafyreihi35g5ngllvyykzlq3fdfqs4xjiuao7nxg37ealsdtqcdquvj2ce" + }, + "com.atproto.admin.disableInviteCodes": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.admin.disableInviteCodes", + "cid": "bafyreiatqxhcmu23qc2nkttjf2e7gjsdhbh6x5nrqambamo5fcwq523eey" + }, + "com.atproto.admin.enableAccountInvites": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.admin.enableAccountInvites", + "cid": "bafyreid5xvqhkbpywbk5eaumneqzdcxtkj5lh45hzmcbed4exzvqk37p2i" + }, + "com.atproto.admin.getAccountInfo": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.admin.getAccountInfo", + "cid": "bafyreifcp3jeg332nfwatctxwjiokqv72tyuppb2mzwjxih7qdtr2glou4" + }, + "com.atproto.admin.getAccountInfos": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.admin.getAccountInfos", + "cid": "bafyreib7kdehrlwtxbpdkdk3hxswvjdgw63yadellwf4n32qofh5pbf2zy" + }, + "com.atproto.admin.getInviteCodes": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.admin.getInviteCodes", + "cid": "bafyreiec5v6odvpqm3qfcxmuq5nihskij7xmb5nij32vve426z6s7tii24" + }, + "com.atproto.admin.getSubjectStatus": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.admin.getSubjectStatus", + "cid": "bafyreicx3n74uhu3jtolm6vvtxx4xy3vo57iu4boxn7chocuokx3q7fazm" + }, + "com.atproto.admin.searchAccounts": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.admin.searchAccounts", + "cid": "bafyreigqi7z4ic2ljca6zvmtzha4gdnsa25gsn4pw2upqymrh6lelkzqjy" + }, + "com.atproto.admin.sendEmail": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.admin.sendEmail", + "cid": "bafyreig7vffo7s4diefuoylvl6crosfo5mcc74hlnpkd24fb3xs2wyewbi" + }, + "com.atproto.admin.updateAccountEmail": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.admin.updateAccountEmail", + "cid": "bafyreidlzhtftznouwxruqumookeupzk4iytlvm5giwziuv4m7wzi76lf4" + }, + "com.atproto.admin.updateAccountHandle": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.admin.updateAccountHandle", + "cid": "bafyreidsj323nackffydrz3buggrubayxwnhxu2v2sf3saoiao44im6dnm" + }, + "com.atproto.admin.updateAccountPassword": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.admin.updateAccountPassword", + "cid": "bafyreieptac2cku6iazjjbgn6oy2ivaleofleprdhiysprato336fapghq" + }, + "com.atproto.admin.updateAccountSigningKey": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.admin.updateAccountSigningKey", + "cid": "bafyreibzf73rasuo5rhzjjugexi5l7tpyahcmmjg2rl2bweoxyb3672cca" + }, + "com.atproto.admin.updateSubjectStatus": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.admin.updateSubjectStatus", + "cid": "bafyreieirresnt6qdfshzsaaikwpstoql5q5n7vlbfadkmjwzuweeojuyu" + }, + "com.atproto.identity.defs": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.identity.defs", + "cid": "bafyreiftikhen7lwpyay4e4sycmmflexkroqjwwpfuqtmog3vjhs2qnz3m" + }, + "com.atproto.identity.getRecommendedDidCredentials": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.identity.getRecommendedDidCredentials", + "cid": "bafyreidkfpbromp7kjmpbzkbcamycj3722sxvcm47pabju744ytk2ljhay" + }, + "com.atproto.identity.refreshIdentity": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.identity.refreshIdentity", + "cid": "bafyreiabglay3heljv6azsioza4lbbq7ocfnllbugb4ep4cmoofvhbz74q" + }, + "com.atproto.identity.requestPlcOperationSignature": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.identity.requestPlcOperationSignature", + "cid": "bafyreiefkbiptbpxy4ijubutlp2lbjzlkdqbbvt77uguxwg2lns7ba2anm" + }, + "com.atproto.identity.resolveDid": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.identity.resolveDid", + "cid": "bafyreicy6cgn4agn5jnteswy624asmf74k34do54sptozrcmyrlglvnufu" + }, + "com.atproto.identity.resolveHandle": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.identity.resolveHandle", + "cid": "bafyreigckmqtt3jrtzd7tvigjatnz6ajqyafs26h5pwcudwag2anedxnmu" + }, + "com.atproto.identity.resolveIdentity": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.identity.resolveIdentity", + "cid": "bafyreibc3gr2wcoogckav25quul7dz24jnq7oevwvrve4fhanmyypujpai" + }, + "com.atproto.identity.signPlcOperation": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.identity.signPlcOperation", + "cid": "bafyreicjgnk7lmloumjsgwpk6fbnplcu7or23edto4vgdow2quxq2okjha" + }, + "com.atproto.identity.submitPlcOperation": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.identity.submitPlcOperation", + "cid": "bafyreigu67ehin43bdmkt5oyrwsttvzon3lsmp4fkfpzlyptpfnx2hozau" + }, + "com.atproto.identity.updateHandle": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.identity.updateHandle", + "cid": "bafyreibzqwkofzc7g5j7vatyyzolgnarm3zd2g6swegkc3ivz4xivm2buu" + }, + "com.atproto.label.defs": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.label.defs", + "cid": "bafyreidp2wpcbzl2qiob2uwoc7lhntojx3tjcr553mmahw2f5cumtm3wpm" + }, + "com.atproto.label.queryLabels": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.label.queryLabels", + "cid": "bafyreig424opkctuzynvqvbwcoqafbqzqu6tl2kilwif7ivnwcdmafbhni" + }, + "com.atproto.label.subscribeLabels": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.label.subscribeLabels", + "cid": "bafyreigeyxddcuwebpvscyy2l7s3urxsbdtzpjrljncjo4az62woytutx4" + }, + "com.atproto.lexicon.resolveLexicon": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.lexicon.resolveLexicon", + "cid": "bafyreihyejjwx2cpbv3ikfojbq57jmtmilhjjsamqwfeiwkg2vanxxaz6m" + }, + "com.atproto.lexicon.schema": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.lexicon.schema", + "cid": "bafyreigtmtzisxiuao4kcswgnbud7wmpkcnsdb6h2kenhbnaolsx7bpiq4" + }, + "com.atproto.moderation.createReport": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.moderation.createReport", + "cid": "bafyreib5kl3klngngi47k7nx2g2bmrg2roj52q3dobvuf73kr3exvn73n4" + }, + "com.atproto.moderation.defs": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.moderation.defs", + "cid": "bafyreideawy4rlpgces2oebk5q4kpurbonhb5qtl4pes7dvxsc5osaiksy" + }, + "com.atproto.repo.applyWrites": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.repo.applyWrites", + "cid": "bafyreidwfwif7k5uncwqxhpxpxhvdkgedbvp6kqy6r4nozii433lwlajba" + }, + "com.atproto.repo.createRecord": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.repo.createRecord", + "cid": "bafyreihyvpmy2l4ou2v5etx25j5lbqty6tvna7gsxdqge76rbb7gltulei" + }, + "com.atproto.repo.defs": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.repo.defs", + "cid": "bafyreigmp5xfpkhnmwr3l5o626ued2gc2ouioykj2tpgqjqqajvbvm3wlm" + }, + "com.atproto.repo.deleteRecord": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.repo.deleteRecord", + "cid": "bafyreibwdxbyhuuljujl7g4ylwdso2x2ov6yv6nk3ext3sr7kjvvubkula" + }, + "com.atproto.repo.describeRepo": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.repo.describeRepo", + "cid": "bafyreif5ho6rtknjmiqjclrfogiqyzxnix2fzpwxyftfebplkj4q23pnrm" + }, + "com.atproto.repo.getRecord": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.repo.getRecord", + "cid": "bafyreihdnm3v25uaptt5vjvnpfsfgb6fbhxew2ye3l6q4ndm7xi5wpc4vy" + }, + "com.atproto.repo.importRepo": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.repo.importRepo", + "cid": "bafyreihymzggr6n33y3ile7hiy5zekxkgqc6ypkoby5og7tr6buqiasabu" + }, + "com.atproto.repo.listMissingBlobs": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.repo.listMissingBlobs", + "cid": "bafyreiayrrhk77vn3az63mor2bepw2qnkskr5awxvpzb5xtgwtzr5z6v3u" + }, + "com.atproto.repo.listRecords": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.repo.listRecords", + "cid": "bafyreihv666jp6lxncj6amcgsu5qrvwq4rwmtc7xbxzndxqdwxamlsatee" + }, + "com.atproto.repo.putRecord": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.repo.putRecord", + "cid": "bafyreigevzo2cypxpaj5re2u35so36df4rkgx6oh4zpi4zwicfvzqfdqpa" + }, + "com.atproto.repo.strongRef": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.repo.strongRef", + "cid": "bafyreifrkdbnkvfjujntdaeigolnrjj3srrs53tfixjhmacclps72qlov4" + }, + "com.atproto.repo.uploadBlob": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.repo.uploadBlob", + "cid": "bafyreifj5rusrt7rd763rqcnyh7srwukrdn427m7qrhwyb5hiayyyq4lxe" + }, + "com.atproto.server.activateAccount": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.server.activateAccount", + "cid": "bafyreih4z7qtht643ddxszglbworpyttngm4iahyne5vp5n6puvysckc2u" + }, + "com.atproto.server.checkAccountStatus": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.server.checkAccountStatus", + "cid": "bafyreiggejdmd3ezjwphxo6jgcaf4cnwwc2jxq5so7gvet3suozhb2a52q" + }, + "com.atproto.server.confirmEmail": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.server.confirmEmail", + "cid": "bafyreigsd23jlgjwtbbvv2pueoiuji3qini232nwzhjiex6tj7ehc57cdy" + }, + "com.atproto.server.createAccount": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.server.createAccount", + "cid": "bafyreie6o2sq47ukysnnnwxcb7eocpyyazetp5n3m4u3n5f7v5xr7qn62u" + }, + "com.atproto.server.createAppPassword": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.server.createAppPassword", + "cid": "bafyreickddwu7udrphtmvpcajiittidvobrror7tztxkshnnhcxyexylbu" + }, + "com.atproto.server.createInviteCode": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.server.createInviteCode", + "cid": "bafyreicce55wknsshpysqc2torzhkuo6v555piprncrkw7glptinbssbli" + }, + "com.atproto.server.createInviteCodes": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.server.createInviteCodes", + "cid": "bafyreidpwhayukm2ytzncekfinio4dehp5y6ksmwpvrxwl4fhdnqh62ewa" + }, + "com.atproto.server.createSession": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.server.createSession", + "cid": "bafyreieftxrfcsudzvfk5l73r2cukzwa6axzkfjs2l7dxtieyqnkdngwsq" + }, + "com.atproto.server.deactivateAccount": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.server.deactivateAccount", + "cid": "bafyreicewuflgo55i4yr7wblmnfcfsrqehy6zrq5jcqap2igkniantarwu" + }, + "com.atproto.server.defs": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.server.defs", + "cid": "bafyreie6rctvespvjvdsway3keyu5yy25eqordhq44jmkxbf6roirlidwm" + }, + "com.atproto.server.deleteAccount": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.server.deleteAccount", + "cid": "bafyreie44rxc4ax3u2hg54636cgjedydzvy2q2cnifsayo3kxlpockkhzu" + }, + "com.atproto.server.deleteSession": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.server.deleteSession", + "cid": "bafyreiave4iopcsa62d5hm42ctf4n6z2ajkdqhz5evq53hmw26hghoz35q" + }, + "com.atproto.server.describeServer": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.server.describeServer", + "cid": "bafyreidjve4qbhebxzppq23ogsto6luddd63mmn7a57rmml6qi3yf7n3ee" + }, + "com.atproto.server.getAccountInviteCodes": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.server.getAccountInviteCodes", + "cid": "bafyreibeczlag36upzn67uqsheo4st47z7nbpvfrdgko777633saur7jui" + }, + "com.atproto.server.getServiceAuth": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.server.getServiceAuth", + "cid": "bafyreigjuxsvldkifmwzdrzvziq2ldomzoqizsv5umwgumjv77ld56wxyi" + }, + "com.atproto.server.getSession": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.server.getSession", + "cid": "bafyreihjh3lkkbdznrs5jfnt6mhxtnaitviqqma6zmoltt3weknwxlc5ae" + }, + "com.atproto.server.listAppPasswords": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.server.listAppPasswords", + "cid": "bafyreic7ipzaep5tjfw6pzvcl5goidfhkvquzvwyfftqnlgnc6eu2sjqjy" + }, + "com.atproto.server.refreshSession": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.server.refreshSession", + "cid": "bafyreie5cdpsl3ibp473xuycfxakryp5svq4gzdcwnfdzwm5xkxyzux2ly" + }, + "com.atproto.server.requestAccountDelete": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.server.requestAccountDelete", + "cid": "bafyreietqywku2g5whu4hehlopx3mwwrnkhupuw3udhp76ovdsqrhbhzgm" + }, + "com.atproto.server.requestEmailConfirmation": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.server.requestEmailConfirmation", + "cid": "bafyreidwpigpyhtbka5bgc3qkyr6ifp2j22bwdpjuitb2b3sjy5fae6cj4" + }, + "com.atproto.server.requestEmailUpdate": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.server.requestEmailUpdate", + "cid": "bafyreibd4flizyrui5mlyv5icokqhgocw2txvgjpmxkjrd6ok43jpjbleq" + }, + "com.atproto.server.requestPasswordReset": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.server.requestPasswordReset", + "cid": "bafyreihcycgtiyv5ah2nnpbbez3qleo2y2a3cf5uvz34tcaliicke2osfa" + }, + "com.atproto.server.reserveSigningKey": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.server.reserveSigningKey", + "cid": "bafyreig4anpzivr7yutnqobuqdtta6ls6i4xbarupyud5bwztmmpnvcr2u" + }, + "com.atproto.server.resetPassword": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.server.resetPassword", + "cid": "bafyreighhpnpsmoauobk4gqxpa5qtirodft25gcxnfxme7ic5zpv73tu6u" + }, + "com.atproto.server.revokeAppPassword": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.server.revokeAppPassword", + "cid": "bafyreif7fouud77wtwkbbxrgz2ixc7pofd7klamsrxotvs4cw63dzcqqvi" + }, + "com.atproto.server.updateEmail": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.server.updateEmail", + "cid": "bafyreia57t6r2crv7hinanwkcfyhqfcbt3qnfqponrj3j5owwlepnbxb7u" + }, + "com.atproto.sync.defs": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.sync.defs", + "cid": "bafyreia6vzfbj2nfmr7kn7fwy6lnugozgimpdcg2f3vovgsa4fk6fsfhse" + }, + "com.atproto.sync.getBlob": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.sync.getBlob", + "cid": "bafyreigievymssvpqn7v5gb6q7hrlepcyzy7xko5rnfrk2ywcjcfbd3574" + }, + "com.atproto.sync.getBlocks": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.sync.getBlocks", + "cid": "bafyreie5jvieoangaotlziavns65njosj5pklfqv6u5psgtkhdgmk25lpa" + }, + "com.atproto.sync.getCheckout": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.sync.getCheckout", + "cid": "bafyreiba6hvjo2chopkgwsy5n2sumywx2qhqvqycd5a6tqfsnbf5nodkzm" + }, + "com.atproto.sync.getHead": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.sync.getHead", + "cid": "bafyreiecq2pcytdy56ucv22a7snlt5lgacmfvemioj5jjyco6uo7t7akwi" + }, + "com.atproto.sync.getHostStatus": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.sync.getHostStatus", + "cid": "bafyreiazetpylsjgp5qfnu6qfb6ahthgjw7spbcl7hpyiz3srl7wnebdsq" + }, + "com.atproto.sync.getLatestCommit": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.sync.getLatestCommit", + "cid": "bafyreifewzvfus7z6rnmtv74lpulhldvikqmhzepwqdlaqmhllgyg4oejq" + }, + "com.atproto.sync.getRecord": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.sync.getRecord", + "cid": "bafyreiermob6nqcfc6hlhmvuo7jv3imo7hfy3og34oom2p3qe6tldajoru" + }, + "com.atproto.sync.getRepo": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.sync.getRepo", + "cid": "bafyreieyt5x6wf4pgcn56tb2724uplmwcqjivkubmi25km3xrvroz6dw5q" + }, + "com.atproto.sync.getRepoStatus": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.sync.getRepoStatus", + "cid": "bafyreidv752rhe2abwz2rp2akok2gnopmsk4wlwrwsf22uwhfzst6nh4ry" + }, + "com.atproto.sync.listBlobs": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.sync.listBlobs", + "cid": "bafyreidjsbhbe5rstmx5tjbbjjrznqr2ehlqkdklvlnhlcndhdjycnuc4a" + }, + "com.atproto.sync.listHosts": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.sync.listHosts", + "cid": "bafyreigxo77zttzvqzmhhvttfs7kom5npwjxkapmnmpirjyg7oui5tpwbe" + }, + "com.atproto.sync.listRepos": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.sync.listRepos", + "cid": "bafyreic6q46ptlr4kv4eonzw3isiqegom5ldr32onhrv2kyrmppgakss2i" + }, + "com.atproto.sync.listReposByCollection": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.sync.listReposByCollection", + "cid": "bafyreicl2azqcl4poa5cqyp3ghlomqkzswmu4uqff6cpwk2c7bjrhth75y" + }, + "com.atproto.sync.notifyOfUpdate": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.sync.notifyOfUpdate", + "cid": "bafyreigohysvh6tjme6ihyygbrnjckr7e3rgmsoes6tx7xxhszulbek6gm" + }, + "com.atproto.sync.requestCrawl": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.sync.requestCrawl", + "cid": "bafyreiepdcevsxqy6matus3wn2n3htnyihgc3etubne2ay3idq7h523hte" + }, + "com.atproto.sync.subscribeRepos": { + "uri": "at://did:plc:6msi3pj7krzih5qxqtryxlzw/com.atproto.lexicon.schema/com.atproto.sync.subscribeRepos", + "cid": "bafyreid5fdkjulxpnbfqwi4hjehx7ibzzj2h5ln7nachurdzqcpihytbuu" + }, + "com.germnetwork.declaration": { + "uri": "at://did:plc:qyqmmncrm6qx33kpy7vqndik/com.atproto.lexicon.schema/com.germnetwork.declaration", + "cid": "bafyreickqsspgkujwucg3low5or64ywjryf44eua6xtszd6zsl2yteumoi" + }, + "tools.ozone.report.defs": { + "uri": "at://did:plc:33dt5kftu3jq2h5h4jjlqezt/com.atproto.lexicon.schema/tools.ozone.report.defs", + "cid": "bafyreic3l2rmh2ugirt3jz372wcvy333m7t2ynlyzj2k54oshijs6lxdfu" + } + } +} diff --git a/lexicons/app/bsky/actor/defs.json b/lexicons/app/bsky/actor/defs.json new file mode 100644 index 0000000000..2e6403ff51 --- /dev/null +++ b/lexicons/app/bsky/actor/defs.json @@ -0,0 +1,994 @@ +{ + "id": "app.bsky.actor.defs", + "defs": { + "nux": { + "type": "object", + "required": [ + "id", + "completed" + ], + "properties": { + "id": { + "type": "string", + "maxLength": 100 + }, + "data": { + "type": "string", + "maxLength": 3000, + "description": "Arbitrary data for the NUX. The structure is defined by the NUX itself. Limited to 300 characters.", + "maxGraphemes": 300 + }, + "completed": { + "type": "boolean", + "default": false + }, + "expiresAt": { + "type": "string", + "format": "datetime", + "description": "The date and time at which the NUX will expire and should be considered completed." + } + }, + "description": "A new user experiences (NUX) storage object" + }, + "mutedWord": { + "type": "object", + "required": [ + "value", + "targets" + ], + "properties": { + "id": { + "type": "string" + }, + "value": { + "type": "string", + "maxLength": 10000, + "description": "The muted word itself.", + "maxGraphemes": 1000 + }, + "targets": { + "type": "array", + "items": { + "ref": "app.bsky.actor.defs#mutedWordTarget", + "type": "ref" + }, + "description": "The intended targets of the muted word." + }, + "expiresAt": { + "type": "string", + "format": "datetime", + "description": "The date and time at which the muted word will expire and no longer be applied." + }, + "actorTarget": { + "type": "string", + "default": "all", + "description": "Groups of users to apply the muted word to. If undefined, applies to all users.", + "knownValues": [ + "all", + "exclude-following" + ] + } + }, + "description": "A word that the account owner has muted." + }, + "savedFeed": { + "type": "object", + "required": [ + "id", + "type", + "value", + "pinned" + ], + "properties": { + "id": { + "type": "string" + }, + "type": { + "type": "string", + "knownValues": [ + "feed", + "list", + "timeline" + ] + }, + "value": { + "type": "string" + }, + "pinned": { + "type": "boolean" + } + } + }, + "statusView": { + "type": "object", + "required": [ + "status", + "record" + ], + "properties": { + "cid": { + "type": "string", + "format": "cid" + }, + "uri": { + "type": "string", + "format": "at-uri" + }, + "embed": { + "refs": [ + "app.bsky.embed.external#view" + ], + "type": "union", + "description": "An optional embed associated with the status." + }, + "labels": { + "type": "array", + "items": { + "ref": "com.atproto.label.defs#label", + "type": "ref" + } + }, + "record": { + "type": "unknown" + }, + "status": { + "type": "string", + "description": "The status for the account.", + "knownValues": [ + "app.bsky.actor.status#live" + ] + }, + "isActive": { + "type": "boolean", + "description": "True if the status is not expired, false if it is expired. Only present if expiration was set." + }, + "expiresAt": { + "type": "string", + "format": "datetime", + "description": "The date when this status will expire. The application might choose to no longer return the status after expiration." + }, + "isDisabled": { + "type": "boolean", + "description": "True if the user's go-live access has been disabled by a moderator, false otherwise." + } + } + }, + "preferences": { + "type": "array", + "items": { + "refs": [ + "#adultContentPref", + "#contentLabelPref", + "#savedFeedsPref", + "#savedFeedsPrefV2", + "#personalDetailsPref", + "#declaredAgePref", + "#feedViewPref", + "#threadViewPref", + "#interestsPref", + "#mutedWordsPref", + "#hiddenPostsPref", + "#bskyAppStatePref", + "#labelersPref", + "#postInteractionSettingsPref", + "#verificationPrefs", + "#liveEventPreferences" + ], + "type": "union" + } + }, + "profileView": { + "type": "object", + "required": [ + "did", + "handle" + ], + "properties": { + "did": { + "type": "string", + "format": "did" + }, + "debug": { + "type": "unknown", + "description": "Debug information for internal development" + }, + "avatar": { + "type": "string", + "format": "uri" + }, + "handle": { + "type": "string", + "format": "handle" + }, + "labels": { + "type": "array", + "items": { + "ref": "com.atproto.label.defs#label", + "type": "ref" + } + }, + "status": { + "ref": "#statusView", + "type": "ref" + }, + "viewer": { + "ref": "#viewerState", + "type": "ref" + }, + "pronouns": { + "type": "string" + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "indexedAt": { + "type": "string", + "format": "datetime" + }, + "associated": { + "ref": "#profileAssociated", + "type": "ref" + }, + "description": { + "type": "string", + "maxLength": 2560, + "maxGraphemes": 256 + }, + "displayName": { + "type": "string", + "maxLength": 640, + "maxGraphemes": 64 + }, + "verification": { + "ref": "#verificationState", + "type": "ref" + } + } + }, + "viewerState": { + "type": "object", + "properties": { + "muted": { + "type": "boolean", + "description": "Whether the account is fully muted, directly or via a mutelist. False when the mute is scoped to specific kinds; see mutedOnlyReposts and mutedOnlyQuoteposts." + }, + "blocking": { + "type": "string", + "format": "at-uri" + }, + "blockedBy": { + "type": "boolean" + }, + "following": { + "type": "string", + "format": "at-uri" + }, + "followedBy": { + "type": "string", + "format": "at-uri" + }, + "mutedByList": { + "ref": "app.bsky.graph.defs#listViewBasic", + "type": "ref" + }, + "blockingByList": { + "ref": "app.bsky.graph.defs#listViewBasic", + "type": "ref" + }, + "knownFollowers": { + "ref": "#knownFollowers", + "type": "ref", + "description": "This property is present only in selected cases, as an optimization." + }, + "mutedOnlyReposts": { + "type": "boolean", + "description": "Whether the account's reposts are muted. Scoped mutes are exclusive with muted: this can be true while muted is false. If muted is true, this will be false." + }, + "mutedOnlyQuoteposts": { + "type": "boolean", + "description": "Whether the account's quote posts are muted. Scoped mutes are exclusive with muted: this can be true while muted is false. If muted is true, this will be false." + }, + "activitySubscription": { + "ref": "app.bsky.notification.defs#activitySubscription", + "type": "ref", + "description": "This property is present only in selected cases, as an optimization." + } + }, + "description": "Metadata about the requesting account's relationship with the subject account. Only has meaningful content for authed requests." + }, + "feedViewPref": { + "type": "object", + "required": [ + "feed" + ], + "properties": { + "feed": { + "type": "string", + "description": "The URI of the feed, or an identifier which describes the feed." + }, + "hideReplies": { + "type": "boolean", + "description": "Hide replies in the feed." + }, + "hideReposts": { + "type": "boolean", + "description": "Hide reposts in the feed." + }, + "hideQuotePosts": { + "type": "boolean", + "description": "Hide quote posts in the feed." + }, + "hideRepliesByLikeCount": { + "type": "integer", + "description": "Hide replies in the feed if they do not have this number of likes." + }, + "hideRepliesByUnfollowed": { + "type": "boolean", + "default": true, + "description": "Hide replies in the feed if they are not by followed users." + } + } + }, + "labelersPref": { + "type": "object", + "required": [ + "labelers" + ], + "properties": { + "labelers": { + "type": "array", + "items": { + "ref": "#labelerPrefItem", + "type": "ref" + } + } + } + }, + "interestsPref": { + "type": "object", + "required": [ + "tags" + ], + "properties": { + "tags": { + "type": "array", + "items": { + "type": "string", + "maxLength": 640, + "maxGraphemes": 64 + }, + "maxLength": 100, + "description": "A list of tags which describe the account owner's interests gathered during onboarding." + } + } + }, + "knownFollowers": { + "type": "object", + "required": [ + "count", + "followers" + ], + "properties": { + "count": { + "type": "integer" + }, + "followers": { + "type": "array", + "items": { + "ref": "#profileViewBasic", + "type": "ref" + }, + "maxLength": 5, + "minLength": 0 + } + }, + "description": "The subject's followers whom you also follow" + }, + "mutedWordsPref": { + "type": "object", + "required": [ + "items" + ], + "properties": { + "items": { + "type": "array", + "items": { + "ref": "app.bsky.actor.defs#mutedWord", + "type": "ref" + }, + "description": "A list of words the account owner has muted." + } + } + }, + "savedFeedsPref": { + "type": "object", + "required": [ + "pinned", + "saved" + ], + "properties": { + "saved": { + "type": "array", + "items": { + "type": "string", + "format": "at-uri" + } + }, + "pinned": { + "type": "array", + "items": { + "type": "string", + "format": "at-uri" + } + }, + "timelineIndex": { + "type": "integer" + } + } + }, + "threadViewPref": { + "type": "object", + "properties": { + "sort": { + "type": "string", + "description": "Sorting mode for threads.", + "knownValues": [ + "oldest", + "newest", + "most-likes", + "random", + "hotness" + ] + } + } + }, + "declaredAgePref": { + "type": "object", + "properties": { + "isOverAge13": { + "type": "boolean", + "description": "Indicates if the user has declared that they are over 13 years of age." + }, + "isOverAge16": { + "type": "boolean", + "description": "Indicates if the user has declared that they are over 16 years of age." + }, + "isOverAge18": { + "type": "boolean", + "description": "Indicates if the user has declared that they are over 18 years of age." + } + }, + "description": "Read-only preference containing value(s) inferred from the user's declared birthdate. Absence of this preference object in the response indicates that the user has not made a declaration." + }, + "hiddenPostsPref": { + "type": "object", + "required": [ + "items" + ], + "properties": { + "items": { + "type": "array", + "items": { + "type": "string", + "format": "at-uri" + }, + "description": "A list of URIs of posts the account owner has hidden." + } + } + }, + "labelerPrefItem": { + "type": "object", + "required": [ + "did" + ], + "properties": { + "did": { + "type": "string", + "format": "did" + } + } + }, + "mutedWordTarget": { + "type": "string", + "maxLength": 640, + "knownValues": [ + "content", + "tag" + ], + "maxGraphemes": 64 + }, + "adultContentPref": { + "type": "object", + "required": [ + "enabled" + ], + "properties": { + "enabled": { + "type": "boolean", + "default": false + } + } + }, + "bskyAppStatePref": { + "type": "object", + "properties": { + "nuxs": { + "type": "array", + "items": { + "ref": "app.bsky.actor.defs#nux", + "type": "ref" + }, + "maxLength": 100, + "description": "Storage for NUXs the user has encountered." + }, + "isBetaUser": { + "type": "boolean", + "description": "Indicates if the user is participating in the beta features program." + }, + "queuedNudges": { + "type": "array", + "items": { + "type": "string", + "maxLength": 100 + }, + "maxLength": 1000, + "description": "An array of tokens which identify nudges (modals, popups, tours, highlight dots) that should be shown to the user." + }, + "activeProgressGuide": { + "ref": "#bskyAppProgressGuide", + "type": "ref" + } + }, + "description": "A grab bag of state that's specific to the bsky.app program. Third-party apps shouldn't use this." + }, + "contentLabelPref": { + "type": "object", + "required": [ + "label", + "visibility" + ], + "properties": { + "label": { + "type": "string" + }, + "labelerDid": { + "type": "string", + "format": "did", + "description": "Which labeler does this preference apply to? If undefined, applies globally." + }, + "visibility": { + "type": "string", + "knownValues": [ + "ignore", + "show", + "warn", + "hide" + ] + } + } + }, + "profileViewBasic": { + "type": "object", + "required": [ + "did", + "handle" + ], + "properties": { + "did": { + "type": "string", + "format": "did" + }, + "debug": { + "type": "unknown", + "description": "Debug information for internal development" + }, + "avatar": { + "type": "string", + "format": "uri" + }, + "handle": { + "type": "string", + "format": "handle" + }, + "labels": { + "type": "array", + "items": { + "ref": "com.atproto.label.defs#label", + "type": "ref" + } + }, + "status": { + "ref": "#statusView", + "type": "ref" + }, + "viewer": { + "ref": "#viewerState", + "type": "ref" + }, + "pronouns": { + "type": "string" + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "associated": { + "ref": "#profileAssociated", + "type": "ref" + }, + "displayName": { + "type": "string", + "maxLength": 640, + "maxGraphemes": 64 + }, + "verification": { + "ref": "#verificationState", + "type": "ref" + } + } + }, + "savedFeedsPrefV2": { + "type": "object", + "required": [ + "items" + ], + "properties": { + "items": { + "type": "array", + "items": { + "ref": "app.bsky.actor.defs#savedFeed", + "type": "ref" + } + } + } + }, + "verificationView": { + "type": "object", + "required": [ + "issuer", + "uri", + "isValid", + "createdAt" + ], + "properties": { + "uri": { + "type": "string", + "format": "at-uri", + "description": "The AT-URI of the verification record." + }, + "issuer": { + "type": "string", + "format": "did", + "description": "The user who issued this verification." + }, + "isValid": { + "type": "boolean", + "description": "True if the verification passes validation, otherwise false." + }, + "createdAt": { + "type": "string", + "format": "datetime", + "description": "Timestamp when the verification was created." + }, + "issuerHandle": { + "type": "string", + "format": "handle", + "description": "The handle of the issuer." + }, + "issuerDisplayName": { + "type": "string", + "description": "The display name of the issuer." + } + }, + "description": "An individual verification for an associated subject." + }, + "profileAssociated": { + "type": "object", + "properties": { + "chat": { + "ref": "#profileAssociatedChat", + "type": "ref" + }, + "germ": { + "ref": "#profileAssociatedGerm", + "type": "ref" + }, + "lists": { + "type": "integer" + }, + "labeler": { + "type": "boolean" + }, + "feedgens": { + "type": "integer" + }, + "starterPacks": { + "type": "integer" + }, + "activitySubscription": { + "ref": "#profileAssociatedActivitySubscription", + "type": "ref" + } + } + }, + "verificationPrefs": { + "type": "object", + "required": [], + "properties": { + "hideBadges": { + "type": "boolean", + "default": false, + "description": "Hide the blue check badges for verified accounts and trusted verifiers." + } + }, + "description": "Preferences for how verified accounts appear in the app." + }, + "verificationState": { + "type": "object", + "required": [ + "verifications", + "verifiedStatus", + "trustedVerifierStatus" + ], + "properties": { + "verifications": { + "type": "array", + "items": { + "ref": "#verificationView", + "type": "ref" + }, + "description": "All verifications issued by trusted verifiers on behalf of this user. Verifications by untrusted verifiers are not included." + }, + "verifiedStatus": { + "type": "string", + "description": "The user's status as a verified account.", + "knownValues": [ + "valid", + "invalid", + "none" + ] + }, + "trustedVerifierStatus": { + "type": "string", + "description": "The user's status as a trusted verifier.", + "knownValues": [ + "valid", + "invalid", + "none" + ] + } + }, + "description": "Represents the verification information about the user this object is attached to." + }, + "personalDetailsPref": { + "type": "object", + "properties": { + "birthDate": { + "type": "string", + "format": "datetime", + "description": "The birth date of account owner." + } + } + }, + "profileViewDetailed": { + "type": "object", + "required": [ + "did", + "handle" + ], + "properties": { + "did": { + "type": "string", + "format": "did" + }, + "debug": { + "type": "unknown", + "description": "Debug information for internal development" + }, + "avatar": { + "type": "string", + "format": "uri" + }, + "banner": { + "type": "string", + "format": "uri" + }, + "handle": { + "type": "string", + "format": "handle" + }, + "labels": { + "type": "array", + "items": { + "ref": "com.atproto.label.defs#label", + "type": "ref" + } + }, + "status": { + "ref": "#statusView", + "type": "ref" + }, + "viewer": { + "ref": "#viewerState", + "type": "ref" + }, + "website": { + "type": "string", + "format": "uri" + }, + "pronouns": { + "type": "string" + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "indexedAt": { + "type": "string", + "format": "datetime" + }, + "associated": { + "ref": "#profileAssociated", + "type": "ref" + }, + "pinnedPost": { + "ref": "com.atproto.repo.strongRef", + "type": "ref" + }, + "postsCount": { + "type": "integer" + }, + "description": { + "type": "string", + "maxLength": 2560, + "maxGraphemes": 256 + }, + "displayName": { + "type": "string", + "maxLength": 640, + "maxGraphemes": 64 + }, + "followsCount": { + "type": "integer" + }, + "verification": { + "ref": "#verificationState", + "type": "ref" + }, + "followersCount": { + "type": "integer" + }, + "joinedViaStarterPack": { + "ref": "app.bsky.graph.defs#starterPackViewBasic", + "type": "ref" + } + } + }, + "bskyAppProgressGuide": { + "type": "object", + "required": [ + "guide" + ], + "properties": { + "guide": { + "type": "string", + "maxLength": 100 + } + }, + "description": "If set, an active progress guide. Once completed, can be set to undefined. Should have unspecced fields tracking progress." + }, + "liveEventPreferences": { + "type": "object", + "properties": { + "hideAllFeeds": { + "type": "boolean", + "default": false, + "description": "Whether to hide all feeds from live events." + }, + "hiddenFeedIds": { + "type": "array", + "items": { + "type": "string" + }, + "description": "A list of feed IDs that the user has hidden from live events." + } + }, + "description": "Preferences for live events." + }, + "profileAssociatedChat": { + "type": "object", + "required": [ + "allowIncoming" + ], + "properties": { + "allowIncoming": { + "type": "string", + "knownValues": [ + "all", + "none", + "following" + ] + }, + "allowGroupInvites": { + "type": "string", + "knownValues": [ + "all", + "none", + "following" + ] + } + } + }, + "profileAssociatedGerm": { + "type": "object", + "required": [ + "showButtonTo", + "messageMeUrl" + ], + "properties": { + "messageMeUrl": { + "type": "string", + "format": "uri" + }, + "showButtonTo": { + "type": "string", + "knownValues": [ + "usersIFollow", + "everyone" + ] + } + } + }, + "postInteractionSettingsPref": { + "type": "object", + "required": [], + "properties": { + "threadgateAllowRules": { + "type": "array", + "items": { + "refs": [ + "app.bsky.feed.threadgate#mentionRule", + "app.bsky.feed.threadgate#followerRule", + "app.bsky.feed.threadgate#followingRule", + "app.bsky.feed.threadgate#listRule" + ], + "type": "union" + }, + "maxLength": 5, + "description": "Matches threadgate record. List of rules defining who can reply to this users posts. If value is an empty array, no one can reply. If value is undefined, anyone can reply." + }, + "postgateEmbeddingRules": { + "type": "array", + "items": { + "refs": [ + "app.bsky.feed.postgate#disableRule" + ], + "type": "union" + }, + "maxLength": 5, + "description": "Matches postgate record. List of rules defining who can embed this users posts. If value is an empty array or is undefined, no particular rules apply and anyone can embed." + } + }, + "description": "Default post interaction settings for the account. These values should be applied as default values when creating new posts. These refs should mirror the threadgate and postgate records exactly." + }, + "profileAssociatedActivitySubscription": { + "type": "object", + "required": [ + "allowSubscriptions" + ], + "properties": { + "allowSubscriptions": { + "type": "string", + "knownValues": [ + "followers", + "mutuals", + "none" + ] + } + } + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/actor/getPreferences.json b/lexicons/app/bsky/actor/getPreferences.json new file mode 100644 index 0000000000..513d27e6a9 --- /dev/null +++ b/lexicons/app/bsky/actor/getPreferences.json @@ -0,0 +1,30 @@ +{ + "id": "app.bsky.actor.getPreferences", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "preferences" + ], + "properties": { + "preferences": { + "ref": "app.bsky.actor.defs#preferences", + "type": "ref" + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "properties": {} + }, + "description": "Get private preferences attached to the current account. Expected use is synchronization between multiple devices, and import/export during account migration. Requires auth." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/actor/getProfile.json b/lexicons/app/bsky/actor/getProfile.json new file mode 100644 index 0000000000..cbdde68df6 --- /dev/null +++ b/lexicons/app/bsky/actor/getProfile.json @@ -0,0 +1,31 @@ +{ + "id": "app.bsky.actor.getProfile", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "ref": "app.bsky.actor.defs#profileViewDetailed", + "type": "ref" + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "actor" + ], + "properties": { + "actor": { + "type": "string", + "format": "at-identifier", + "description": "Handle or DID of account to fetch profile of." + } + } + }, + "description": "Get detailed profile view of an actor. Does not require auth, but contains relevant metadata with auth." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/actor/getProfiles.json b/lexicons/app/bsky/actor/getProfiles.json new file mode 100644 index 0000000000..23f0693417 --- /dev/null +++ b/lexicons/app/bsky/actor/getProfiles.json @@ -0,0 +1,45 @@ +{ + "id": "app.bsky.actor.getProfiles", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "profiles" + ], + "properties": { + "profiles": { + "type": "array", + "items": { + "ref": "app.bsky.actor.defs#profileViewDetailed", + "type": "ref" + } + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "actors" + ], + "properties": { + "actors": { + "type": "array", + "items": { + "type": "string", + "format": "at-identifier" + }, + "maxLength": 25 + } + } + }, + "description": "Get detailed profile views of multiple actors." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/actor/getSuggestions.json b/lexicons/app/bsky/actor/getSuggestions.json new file mode 100644 index 0000000000..84afa86b36 --- /dev/null +++ b/lexicons/app/bsky/actor/getSuggestions.json @@ -0,0 +1,54 @@ +{ + "id": "app.bsky.actor.getSuggestions", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "actors" + ], + "properties": { + "recId": { + "type": "integer", + "description": "DEPRECATED: use recIdStr instead." + }, + "actors": { + "type": "array", + "items": { + "ref": "app.bsky.actor.defs#profileView", + "type": "ref" + } + }, + "cursor": { + "type": "string" + }, + "recIdStr": { + "type": "string", + "description": "Snowflake for this recommendation, use when submitting recommendation events." + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "properties": { + "limit": { + "type": "integer", + "default": 50, + "maximum": 100, + "minimum": 1 + }, + "cursor": { + "type": "string" + } + } + }, + "description": "Get a list of suggested actors. Expected use is discovery of accounts to follow during new account onboarding." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/actor/profile.json b/lexicons/app/bsky/actor/profile.json new file mode 100644 index 0000000000..256405db65 --- /dev/null +++ b/lexicons/app/bsky/actor/profile.json @@ -0,0 +1,75 @@ +{ + "id": "app.bsky.actor.profile", + "defs": { + "main": { + "key": "literal:self", + "type": "record", + "record": { + "type": "object", + "properties": { + "avatar": { + "type": "blob", + "accept": [ + "image/png", + "image/jpeg" + ], + "maxSize": 1000000, + "description": "Small image to be displayed next to posts from account. AKA, 'profile picture'" + }, + "banner": { + "type": "blob", + "accept": [ + "image/png", + "image/jpeg" + ], + "maxSize": 1000000, + "description": "Larger horizontal image to display behind profile view." + }, + "labels": { + "refs": [ + "com.atproto.label.defs#selfLabels" + ], + "type": "union", + "description": "Self-label values, specific to the Bluesky application, on the overall account." + }, + "website": { + "type": "string", + "format": "uri" + }, + "pronouns": { + "type": "string", + "maxLength": 200, + "description": "Free-form pronouns text.", + "maxGraphemes": 20 + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "pinnedPost": { + "ref": "com.atproto.repo.strongRef", + "type": "ref" + }, + "description": { + "type": "string", + "maxLength": 2560, + "description": "Free-form profile description text.", + "maxGraphemes": 256 + }, + "displayName": { + "type": "string", + "maxLength": 640, + "maxGraphemes": 64 + }, + "joinedViaStarterPack": { + "ref": "com.atproto.repo.strongRef", + "type": "ref" + } + } + }, + "description": "A declaration of a Bluesky account profile." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/actor/putPreferences.json b/lexicons/app/bsky/actor/putPreferences.json new file mode 100644 index 0000000000..824d5a2e80 --- /dev/null +++ b/lexicons/app/bsky/actor/putPreferences.json @@ -0,0 +1,26 @@ +{ + "id": "app.bsky.actor.putPreferences", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "preferences" + ], + "properties": { + "preferences": { + "ref": "app.bsky.actor.defs#preferences", + "type": "ref" + } + } + }, + "encoding": "application/json" + }, + "description": "Set the private preferences attached to the account." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/actor/searchActors.json b/lexicons/app/bsky/actor/searchActors.json new file mode 100644 index 0000000000..1e097e505f --- /dev/null +++ b/lexicons/app/bsky/actor/searchActors.json @@ -0,0 +1,54 @@ +{ + "id": "app.bsky.actor.searchActors", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "actors" + ], + "properties": { + "actors": { + "type": "array", + "items": { + "ref": "app.bsky.actor.defs#profileView", + "type": "ref" + } + }, + "cursor": { + "type": "string" + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "properties": { + "q": { + "type": "string", + "description": "Search query string. Syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended." + }, + "term": { + "type": "string", + "description": "DEPRECATED: use 'q' instead." + }, + "limit": { + "type": "integer", + "default": 25, + "maximum": 100, + "minimum": 1 + }, + "cursor": { + "type": "string" + } + } + }, + "description": "Find actors (profiles) matching search criteria. Does not require auth." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/actor/searchActorsTypeahead.json b/lexicons/app/bsky/actor/searchActorsTypeahead.json new file mode 100644 index 0000000000..9f11920c7e --- /dev/null +++ b/lexicons/app/bsky/actor/searchActorsTypeahead.json @@ -0,0 +1,48 @@ +{ + "id": "app.bsky.actor.searchActorsTypeahead", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "actors" + ], + "properties": { + "actors": { + "type": "array", + "items": { + "ref": "app.bsky.actor.defs#profileViewBasic", + "type": "ref" + } + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "properties": { + "q": { + "type": "string", + "description": "Search query prefix; not a full query string." + }, + "term": { + "type": "string", + "description": "DEPRECATED: use 'q' instead." + }, + "limit": { + "type": "integer", + "default": 10, + "maximum": 100, + "minimum": 1 + } + } + }, + "description": "Find actor suggestions for a prefix search term. Expected use is for auto-completion during text field entry. Does not require auth." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/actor/status.json b/lexicons/app/bsky/actor/status.json new file mode 100644 index 0000000000..7fd193c4d5 --- /dev/null +++ b/lexicons/app/bsky/actor/status.json @@ -0,0 +1,48 @@ +{ + "id": "app.bsky.actor.status", + "defs": { + "live": { + "type": "token", + "description": "Advertises an account as currently offering live content." + }, + "main": { + "key": "literal:self", + "type": "record", + "record": { + "type": "object", + "required": [ + "status", + "createdAt" + ], + "properties": { + "embed": { + "refs": [ + "app.bsky.embed.external" + ], + "type": "union", + "description": "An optional embed associated with the status." + }, + "status": { + "type": "string", + "description": "The status for the account.", + "knownValues": [ + "app.bsky.actor.status#live" + ] + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "durationMinutes": { + "type": "integer", + "minimum": 1, + "description": "The duration of the status in minutes. Applications can choose to impose minimum and maximum limits." + } + } + }, + "description": "A declaration of a Bluesky account status." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/ageassurance/begin.json b/lexicons/app/bsky/ageassurance/begin.json new file mode 100644 index 0000000000..0354e12190 --- /dev/null +++ b/lexicons/app/bsky/ageassurance/begin.json @@ -0,0 +1,61 @@ +{ + "id": "app.bsky.ageassurance.begin", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "email", + "language", + "countryCode" + ], + "properties": { + "email": { + "type": "string", + "description": "The user's email address to receive Age Assurance instructions." + }, + "language": { + "type": "string", + "description": "The user's preferred language for communication during the Age Assurance process." + }, + "regionCode": { + "type": "string", + "description": "An optional ISO 3166-2 code of the user's region or state within the country." + }, + "countryCode": { + "type": "string", + "description": "An ISO 3166-1 alpha-2 code of the user's location." + } + } + }, + "encoding": "application/json" + }, + "errors": [ + { + "name": "InvalidEmail" + }, + { + "name": "DidTooLong" + }, + { + "name": "InvalidInitiation" + }, + { + "name": "RegionNotSupported" + } + ], + "output": { + "schema": { + "ref": "app.bsky.ageassurance.defs#state", + "type": "ref" + }, + "encoding": "application/json" + }, + "description": "Initiate Age Assurance for an account." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/ageassurance/defs.json b/lexicons/app/bsky/ageassurance/defs.json new file mode 100644 index 0000000000..2589abfd0f --- /dev/null +++ b/lexicons/app/bsky/ageassurance/defs.json @@ -0,0 +1,333 @@ +{ + "id": "app.bsky.ageassurance.defs", + "defs": { + "event": { + "type": "object", + "required": [ + "createdAt", + "status", + "access", + "attemptId", + "countryCode" + ], + "properties": { + "email": { + "type": "string", + "description": "The email used for Age Assurance." + }, + "access": { + "type": "string", + "description": "The access level granted based on Age Assurance data we've processed.", + "knownValues": [ + "unknown", + "none", + "safe", + "full" + ] + }, + "initIp": { + "type": "string", + "description": "The IP address used when initiating the Age Assurance flow." + }, + "initUa": { + "type": "string", + "description": "The user agent used when initiating the Age Assurance flow." + }, + "status": { + "type": "string", + "description": "The status of the Age Assurance process.", + "knownValues": [ + "unknown", + "pending", + "assured", + "blocked" + ] + }, + "attemptId": { + "type": "string", + "description": "The unique identifier for this instance of the Age Assurance flow, in UUID format." + }, + "createdAt": { + "type": "string", + "format": "datetime", + "description": "The date and time of this write operation." + }, + "completeIp": { + "type": "string", + "description": "The IP address used when completing the Age Assurance flow." + }, + "completeUa": { + "type": "string", + "description": "The user agent used when completing the Age Assurance flow." + }, + "regionCode": { + "type": "string", + "description": "The ISO 3166-2 region code provided when beginning the Age Assurance flow." + }, + "countryCode": { + "type": "string", + "description": "The ISO 3166-1 alpha-2 country code provided when beginning the Age Assurance flow." + } + }, + "description": "Object used to store Age Assurance data in stash." + }, + "state": { + "type": "object", + "required": [ + "status", + "access" + ], + "properties": { + "access": { + "ref": "app.bsky.ageassurance.defs#access", + "type": "ref" + }, + "status": { + "ref": "app.bsky.ageassurance.defs#status", + "type": "ref" + }, + "lastInitiatedAt": { + "type": "string", + "format": "datetime", + "description": "The timestamp when this state was last updated." + } + }, + "description": "The user's computed Age Assurance state." + }, + "access": { + "type": "string", + "description": "The access level granted based on Age Assurance data we've processed.", + "knownValues": [ + "unknown", + "none", + "safe", + "full" + ] + }, + "config": { + "type": "object", + "required": [ + "regions" + ], + "properties": { + "regions": { + "type": "array", + "items": { + "ref": "app.bsky.ageassurance.defs#configRegion", + "type": "ref" + }, + "description": "The per-region Age Assurance configuration." + } + }, + "description": "" + }, + "status": { + "type": "string", + "description": "The status of the Age Assurance process.", + "knownValues": [ + "unknown", + "pending", + "assured", + "blocked" + ] + }, + "configRegion": { + "type": "object", + "required": [ + "countryCode", + "minAccessAge", + "rules" + ], + "properties": { + "rules": { + "type": "array", + "items": { + "refs": [ + "#configRegionRuleDefault", + "#configRegionRuleIfDeclaredOverAge", + "#configRegionRuleIfDeclaredUnderAge", + "#configRegionRuleIfAssuredOverAge", + "#configRegionRuleIfAssuredUnderAge", + "#configRegionRuleIfAccountNewerThan", + "#configRegionRuleIfAccountOlderThan" + ], + "type": "union" + }, + "description": "The ordered list of Age Assurance rules that apply to this region. Rules should be applied in order, and the first matching rule determines the access level granted. The rules array should always include a default rule as the last item." + }, + "platforms": { + "type": "array", + "items": { + "type": "string", + "knownValues": [ + "web", + "ios", + "android" + ] + }, + "description": "The platforms this configuration applies to. If omitted, the configuration applies to all platforms." + }, + "regionCode": { + "type": "string", + "description": "The ISO 3166-2 region code this configuration applies to. If omitted, the configuration applies to the entire country." + }, + "countryCode": { + "type": "string", + "description": "The ISO 3166-1 alpha-2 country code this configuration applies to." + }, + "minAccessAge": { + "type": "integer", + "description": "The minimum age (as a whole integer) required to use Bluesky in this region." + }, + "additionalVerificationMethods": { + "type": "array", + "items": { + "type": "string", + "knownValues": [ + "device" + ] + }, + "description": "Verification methods permitted in this region in addition to the third-party (KWS) flow, which is always supported. `device` permits using the native on-device age APIs (e.g. Apple Declared Age Range, Google Play Age Signals)." + } + }, + "description": "The Age Assurance configuration for a specific region." + }, + "stateMetadata": { + "type": "object", + "required": [], + "properties": { + "accountCreatedAt": { + "type": "string", + "format": "datetime", + "description": "The account creation timestamp." + } + }, + "description": "Additional metadata needed to compute Age Assurance state client-side." + }, + "configRegionRuleDefault": { + "type": "object", + "required": [ + "access" + ], + "properties": { + "access": { + "ref": "app.bsky.ageassurance.defs#access", + "type": "ref" + } + }, + "description": "Age Assurance rule that applies by default." + }, + "configRegionRuleIfAssuredOverAge": { + "type": "object", + "required": [ + "age", + "access" + ], + "properties": { + "age": { + "type": "integer", + "description": "The age threshold as a whole integer." + }, + "access": { + "ref": "app.bsky.ageassurance.defs#access", + "type": "ref" + } + }, + "description": "Age Assurance rule that applies if the user has been assured to be equal-to or over a certain age." + }, + "configRegionRuleIfAssuredUnderAge": { + "type": "object", + "required": [ + "age", + "access" + ], + "properties": { + "age": { + "type": "integer", + "description": "The age threshold as a whole integer." + }, + "access": { + "ref": "app.bsky.ageassurance.defs#access", + "type": "ref" + } + }, + "description": "Age Assurance rule that applies if the user has been assured to be under a certain age." + }, + "configRegionRuleIfDeclaredOverAge": { + "type": "object", + "required": [ + "age", + "access" + ], + "properties": { + "age": { + "type": "integer", + "description": "The age threshold as a whole integer." + }, + "access": { + "ref": "app.bsky.ageassurance.defs#access", + "type": "ref" + } + }, + "description": "Age Assurance rule that applies if the user has declared themselves equal-to or over a certain age." + }, + "configRegionRuleIfAccountNewerThan": { + "type": "object", + "required": [ + "date", + "access" + ], + "properties": { + "date": { + "type": "string", + "format": "datetime", + "description": "The date threshold as a datetime string." + }, + "access": { + "ref": "app.bsky.ageassurance.defs#access", + "type": "ref" + } + }, + "description": "Age Assurance rule that applies if the account is equal-to or newer than a certain date." + }, + "configRegionRuleIfAccountOlderThan": { + "type": "object", + "required": [ + "date", + "access" + ], + "properties": { + "date": { + "type": "string", + "format": "datetime", + "description": "The date threshold as a datetime string." + }, + "access": { + "ref": "app.bsky.ageassurance.defs#access", + "type": "ref" + } + }, + "description": "Age Assurance rule that applies if the account is older than a certain date." + }, + "configRegionRuleIfDeclaredUnderAge": { + "type": "object", + "required": [ + "age", + "access" + ], + "properties": { + "age": { + "type": "integer", + "description": "The age threshold as a whole integer." + }, + "access": { + "ref": "app.bsky.ageassurance.defs#access", + "type": "ref" + } + }, + "description": "Age Assurance rule that applies if the user has declared themselves under a certain age." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/ageassurance/getConfig.json b/lexicons/app/bsky/ageassurance/getConfig.json new file mode 100644 index 0000000000..793d5b9519 --- /dev/null +++ b/lexicons/app/bsky/ageassurance/getConfig.json @@ -0,0 +1,18 @@ +{ + "id": "app.bsky.ageassurance.getConfig", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "ref": "app.bsky.ageassurance.defs#config", + "type": "ref" + }, + "encoding": "application/json" + }, + "description": "Returns Age Assurance configuration for use on the client." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/ageassurance/getState.json b/lexicons/app/bsky/ageassurance/getState.json new file mode 100644 index 0000000000..42c7cd0ba9 --- /dev/null +++ b/lexicons/app/bsky/ageassurance/getState.json @@ -0,0 +1,45 @@ +{ + "id": "app.bsky.ageassurance.getState", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "state", + "metadata" + ], + "properties": { + "state": { + "ref": "app.bsky.ageassurance.defs#state", + "type": "ref" + }, + "metadata": { + "ref": "app.bsky.ageassurance.defs#stateMetadata", + "type": "ref" + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "countryCode" + ], + "properties": { + "regionCode": { + "type": "string" + }, + "countryCode": { + "type": "string" + } + } + }, + "description": "Returns server-computed Age Assurance state, if available, and any additional metadata needed to compute Age Assurance state client-side." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/bookmark/createBookmark.json b/lexicons/app/bsky/bookmark/createBookmark.json new file mode 100644 index 0000000000..378d494a2f --- /dev/null +++ b/lexicons/app/bsky/bookmark/createBookmark.json @@ -0,0 +1,37 @@ +{ + "id": "app.bsky.bookmark.createBookmark", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "uri", + "cid" + ], + "properties": { + "cid": { + "type": "string", + "format": "cid" + }, + "uri": { + "type": "string", + "format": "at-uri" + } + } + }, + "encoding": "application/json" + }, + "errors": [ + { + "name": "UnsupportedCollection", + "description": "The URI to be bookmarked is for an unsupported collection." + } + ], + "description": "Creates a private bookmark for the specified record. Currently, only `app.bsky.feed.post` records are supported. Requires authentication." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/bookmark/defs.json b/lexicons/app/bsky/bookmark/defs.json new file mode 100644 index 0000000000..f499e55cd5 --- /dev/null +++ b/lexicons/app/bsky/bookmark/defs.json @@ -0,0 +1,47 @@ +{ + "id": "app.bsky.bookmark.defs", + "defs": { + "bookmark": { + "type": "object", + "required": [ + "subject" + ], + "properties": { + "subject": { + "ref": "com.atproto.repo.strongRef", + "type": "ref", + "description": "A strong ref to the record to be bookmarked. Currently, only `app.bsky.feed.post` records are supported." + } + }, + "description": "Object used to store bookmark data in stash." + }, + "bookmarkView": { + "type": "object", + "required": [ + "subject", + "item" + ], + "properties": { + "item": { + "refs": [ + "app.bsky.feed.defs#blockedPost", + "app.bsky.feed.defs#notFoundPost", + "app.bsky.feed.defs#postView" + ], + "type": "union" + }, + "subject": { + "ref": "com.atproto.repo.strongRef", + "type": "ref", + "description": "A strong ref to the bookmarked record." + }, + "createdAt": { + "type": "string", + "format": "datetime" + } + } + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/bookmark/deleteBookmark.json b/lexicons/app/bsky/bookmark/deleteBookmark.json new file mode 100644 index 0000000000..52d5c3c2f5 --- /dev/null +++ b/lexicons/app/bsky/bookmark/deleteBookmark.json @@ -0,0 +1,32 @@ +{ + "id": "app.bsky.bookmark.deleteBookmark", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "uri" + ], + "properties": { + "uri": { + "type": "string", + "format": "at-uri" + } + } + }, + "encoding": "application/json" + }, + "errors": [ + { + "name": "UnsupportedCollection", + "description": "The URI to be bookmarked is for an unsupported collection." + } + ], + "description": "Deletes a private bookmark for the specified record. Currently, only `app.bsky.feed.post` records are supported. Requires authentication." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/bookmark/getBookmarks.json b/lexicons/app/bsky/bookmark/getBookmarks.json new file mode 100644 index 0000000000..301914d4c2 --- /dev/null +++ b/lexicons/app/bsky/bookmark/getBookmarks.json @@ -0,0 +1,46 @@ +{ + "id": "app.bsky.bookmark.getBookmarks", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "bookmarks" + ], + "properties": { + "cursor": { + "type": "string" + }, + "bookmarks": { + "type": "array", + "items": { + "ref": "app.bsky.bookmark.defs#bookmarkView", + "type": "ref" + } + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "properties": { + "limit": { + "type": "integer", + "default": 50, + "maximum": 100, + "minimum": 1 + }, + "cursor": { + "type": "string" + } + } + }, + "description": "Gets views of records bookmarked by the authenticated user. Requires authentication." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/contact/defs.json b/lexicons/app/bsky/contact/defs.json new file mode 100644 index 0000000000..68f0179fb3 --- /dev/null +++ b/lexicons/app/bsky/contact/defs.json @@ -0,0 +1,67 @@ +{ + "id": "app.bsky.contact.defs", + "defs": { + "syncStatus": { + "type": "object", + "required": [ + "syncedAt", + "matchesCount" + ], + "properties": { + "syncedAt": { + "type": "string", + "format": "datetime", + "description": "Last date when contacts where imported." + }, + "matchesCount": { + "type": "integer", + "minimum": 0, + "description": "Number of existing contact matches resulting of the user imports and of their imported contacts having imported the user. Matches stop being counted when the user either follows the matched contact or dismisses the match." + } + } + }, + "notification": { + "type": "object", + "required": [ + "from", + "to" + ], + "properties": { + "to": { + "type": "string", + "format": "did", + "description": "The DID of who this notification should go to." + }, + "from": { + "type": "string", + "format": "did", + "description": "The DID of who this notification comes from." + } + }, + "description": "A stash object to be sent via bsync representing a notification to be created." + }, + "matchAndContactIndex": { + "type": "object", + "required": [ + "match", + "contactIndex" + ], + "properties": { + "match": { + "ref": "app.bsky.actor.defs#profileView", + "type": "ref", + "description": "Profile of the matched user." + }, + "contactIndex": { + "type": "integer", + "maximum": 999, + "minimum": 0, + "description": "The index of this match in the import contact input." + } + }, + "description": "Associates a profile with the positional index of the contact import input in the call to `app.bsky.contact.importContacts`, so clients can know which phone caused a particular match." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/contact/dismissMatch.json b/lexicons/app/bsky/contact/dismissMatch.json new file mode 100644 index 0000000000..27cb2b3d7a --- /dev/null +++ b/lexicons/app/bsky/contact/dismissMatch.json @@ -0,0 +1,42 @@ +{ + "id": "app.bsky.contact.dismissMatch", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "subject" + ], + "properties": { + "subject": { + "type": "string", + "format": "did", + "description": "The subject's DID to dismiss the match with." + } + } + }, + "encoding": "application/json" + }, + "errors": [ + { + "name": "InvalidDid" + }, + { + "name": "InternalError" + } + ], + "output": { + "schema": { + "type": "object", + "properties": {} + }, + "encoding": "application/json" + }, + "description": "Removes a match that was found via contact import. It shouldn't appear again if the same contact is re-imported. Requires authentication." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/contact/getMatches.json b/lexicons/app/bsky/contact/getMatches.json new file mode 100644 index 0000000000..cce3cac464 --- /dev/null +++ b/lexicons/app/bsky/contact/getMatches.json @@ -0,0 +1,60 @@ +{ + "id": "app.bsky.contact.getMatches", + "defs": { + "main": { + "type": "query", + "errors": [ + { + "name": "InvalidDid" + }, + { + "name": "InvalidLimit" + }, + { + "name": "InvalidCursor" + }, + { + "name": "InternalError" + } + ], + "output": { + "schema": { + "type": "object", + "required": [ + "matches" + ], + "properties": { + "cursor": { + "type": "string" + }, + "matches": { + "type": "array", + "items": { + "ref": "app.bsky.actor.defs#profileView", + "type": "ref" + } + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "properties": { + "limit": { + "type": "integer", + "default": 50, + "maximum": 100, + "minimum": 1 + }, + "cursor": { + "type": "string" + } + } + }, + "description": "Returns the matched contacts (contacts that were mutually imported). Excludes dismissed matches. Requires authentication." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/contact/getSyncStatus.json b/lexicons/app/bsky/contact/getSyncStatus.json new file mode 100644 index 0000000000..53dc538f78 --- /dev/null +++ b/lexicons/app/bsky/contact/getSyncStatus.json @@ -0,0 +1,36 @@ +{ + "id": "app.bsky.contact.getSyncStatus", + "defs": { + "main": { + "type": "query", + "errors": [ + { + "name": "InvalidDid" + }, + { + "name": "InternalError" + } + ], + "output": { + "schema": { + "type": "object", + "properties": { + "syncStatus": { + "ref": "app.bsky.contact.defs#syncStatus", + "type": "ref", + "description": "If present, indicates the user has imported their contacts. If not present, indicates the user never used the feature or called `app.bsky.contact.removeData` and didn't import again since." + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "properties": {} + }, + "description": "Gets the user's current contact import status. Requires authentication." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/contact/importContacts.json b/lexicons/app/bsky/contact/importContacts.json new file mode 100644 index 0000000000..70092e2583 --- /dev/null +++ b/lexicons/app/bsky/contact/importContacts.json @@ -0,0 +1,72 @@ +{ + "id": "app.bsky.contact.importContacts", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "token", + "contacts" + ], + "properties": { + "token": { + "type": "string", + "description": "JWT to authenticate the call. Use the JWT received as a response to the call to `app.bsky.contact.verifyPhone`." + }, + "contacts": { + "type": "array", + "items": { + "type": "string" + }, + "maxLength": 1000, + "minLength": 1, + "description": "List of phone numbers in global E.164 format (e.g., '+12125550123'). Phone numbers that cannot be normalized into a valid phone number will be discarded. Should not repeat the 'phone' input used in `app.bsky.contact.verifyPhone`." + } + } + }, + "encoding": "application/json" + }, + "errors": [ + { + "name": "InvalidDid" + }, + { + "name": "InvalidContacts" + }, + { + "name": "TooManyContacts" + }, + { + "name": "InvalidToken" + }, + { + "name": "InternalError" + } + ], + "output": { + "schema": { + "type": "object", + "required": [ + "matchesAndContactIndexes" + ], + "properties": { + "matchesAndContactIndexes": { + "type": "array", + "items": { + "ref": "app.bsky.contact.defs#matchAndContactIndex", + "type": "ref" + }, + "description": "The users that matched during import and their indexes on the input contacts, so the client can correlate with its local list." + } + } + }, + "encoding": "application/json" + }, + "description": "Import contacts for securely matching with other users. This follows the protocol explained in https://docs.bsky.app/blog/contact-import-rfc. Requires authentication." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/contact/removeData.json b/lexicons/app/bsky/contact/removeData.json new file mode 100644 index 0000000000..de8a938336 --- /dev/null +++ b/lexicons/app/bsky/contact/removeData.json @@ -0,0 +1,33 @@ +{ + "id": "app.bsky.contact.removeData", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "properties": {} + }, + "encoding": "application/json" + }, + "errors": [ + { + "name": "InvalidDid" + }, + { + "name": "InternalError" + } + ], + "output": { + "schema": { + "type": "object", + "properties": {} + }, + "encoding": "application/json" + }, + "description": "Removes all stored hashes used for contact matching, existing matches, and sync status. Requires authentication." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/contact/sendNotification.json b/lexicons/app/bsky/contact/sendNotification.json new file mode 100644 index 0000000000..400120ce6f --- /dev/null +++ b/lexicons/app/bsky/contact/sendNotification.json @@ -0,0 +1,40 @@ +{ + "id": "app.bsky.contact.sendNotification", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "from", + "to" + ], + "properties": { + "to": { + "type": "string", + "format": "did", + "description": "The DID of who this notification should go to." + }, + "from": { + "type": "string", + "format": "did", + "description": "The DID of who this notification comes from." + } + } + }, + "encoding": "application/json" + }, + "output": { + "schema": { + "type": "object", + "properties": {} + }, + "encoding": "application/json" + }, + "description": "System endpoint to send notifications related to contact imports. Requires role authentication." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/contact/startPhoneVerification.json b/lexicons/app/bsky/contact/startPhoneVerification.json new file mode 100644 index 0000000000..d2dcad8769 --- /dev/null +++ b/lexicons/app/bsky/contact/startPhoneVerification.json @@ -0,0 +1,47 @@ +{ + "id": "app.bsky.contact.startPhoneVerification", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "phone" + ], + "properties": { + "phone": { + "type": "string", + "description": "The phone number to receive the code via SMS." + } + } + }, + "encoding": "application/json" + }, + "errors": [ + { + "name": "RateLimitExceeded" + }, + { + "name": "InvalidDid" + }, + { + "name": "InvalidPhone" + }, + { + "name": "InternalError" + } + ], + "output": { + "schema": { + "type": "object", + "properties": {} + }, + "encoding": "application/json" + }, + "description": "Starts a phone verification flow. The phone passed will receive a code via SMS that should be passed to `app.bsky.contact.verifyPhone`. Requires authentication." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/contact/verifyPhone.json b/lexicons/app/bsky/contact/verifyPhone.json new file mode 100644 index 0000000000..20386c25f6 --- /dev/null +++ b/lexicons/app/bsky/contact/verifyPhone.json @@ -0,0 +1,63 @@ +{ + "id": "app.bsky.contact.verifyPhone", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "phone", + "code" + ], + "properties": { + "code": { + "type": "string", + "description": "The code received via SMS as a result of the call to `app.bsky.contact.startPhoneVerification`." + }, + "phone": { + "type": "string", + "description": "The phone number to verify. Should be the same as the one passed to `app.bsky.contact.startPhoneVerification`." + } + } + }, + "encoding": "application/json" + }, + "errors": [ + { + "name": "RateLimitExceeded" + }, + { + "name": "InvalidDid" + }, + { + "name": "InvalidPhone" + }, + { + "name": "InvalidCode" + }, + { + "name": "InternalError" + } + ], + "output": { + "schema": { + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "string", + "description": "JWT to be used in a call to `app.bsky.contact.importContacts`. It is only valid for a single call." + } + } + }, + "encoding": "application/json" + }, + "description": "Verifies control over a phone number with a code received via SMS and starts a contact import session. Requires authentication." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/draft/createDraft.json b/lexicons/app/bsky/draft/createDraft.json new file mode 100644 index 0000000000..d3c5c830bf --- /dev/null +++ b/lexicons/app/bsky/draft/createDraft.json @@ -0,0 +1,48 @@ +{ + "id": "app.bsky.draft.createDraft", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "draft" + ], + "properties": { + "draft": { + "ref": "app.bsky.draft.defs#draft", + "type": "ref" + } + } + }, + "encoding": "application/json" + }, + "errors": [ + { + "name": "DraftLimitReached", + "description": "Trying to insert a new draft when the limit was already reached." + } + ], + "output": { + "schema": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "string", + "format": "tid", + "description": "The ID of the created draft." + } + } + }, + "encoding": "application/json" + }, + "description": "Inserts a draft using private storage (stash). An upper limit of drafts might be enforced. Requires authentication." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/draft/defs.json b/lexicons/app/bsky/draft/defs.json new file mode 100644 index 0000000000..507432944d --- /dev/null +++ b/lexicons/app/bsky/draft/defs.json @@ -0,0 +1,296 @@ +{ + "id": "app.bsky.draft.defs", + "defs": { + "draft": { + "type": "object", + "required": [ + "posts" + ], + "properties": { + "langs": { + "type": "array", + "items": { + "type": "string", + "format": "language" + }, + "maxLength": 3, + "description": "Indicates human language of posts primary text content." + }, + "posts": { + "type": "array", + "items": { + "ref": "#draftPost", + "type": "ref" + }, + "maxLength": 100, + "minLength": 1, + "description": "Array of draft posts that compose this draft." + }, + "deviceId": { + "type": "string", + "maxLength": 100, + "description": "UUIDv4 identifier of the device that created this draft." + }, + "deviceName": { + "type": "string", + "maxLength": 100, + "description": "The device and/or platform on which the draft was created." + }, + "threadgateAllow": { + "type": "array", + "items": { + "refs": [ + "app.bsky.feed.threadgate#mentionRule", + "app.bsky.feed.threadgate#followerRule", + "app.bsky.feed.threadgate#followingRule", + "app.bsky.feed.threadgate#listRule" + ], + "type": "union" + }, + "maxLength": 5, + "description": "Allow-rules for the threadgate to be created when this draft is published." + }, + "postgateEmbeddingRules": { + "type": "array", + "items": { + "refs": [ + "app.bsky.feed.postgate#disableRule" + ], + "type": "union" + }, + "maxLength": 5, + "description": "Embedding rules for the postgates to be created when this draft is published." + } + }, + "description": "A draft containing an array of draft posts." + }, + "draftPost": { + "type": "object", + "required": [ + "text" + ], + "properties": { + "text": { + "type": "string", + "maxLength": 10000, + "description": "The primary post content. It has a higher limit than post contents to allow storing a larger text that can later be refined into smaller posts.", + "maxGraphemes": 1000 + }, + "labels": { + "refs": [ + "com.atproto.label.defs#selfLabels" + ], + "type": "union", + "description": "Self-label values for this post. Effectively content warnings." + }, + "embedImages": { + "type": "array", + "items": { + "ref": "#draftEmbedImage", + "type": "ref" + }, + "maxLength": 4 + }, + "embedVideos": { + "type": "array", + "items": { + "ref": "#draftEmbedVideo", + "type": "ref" + }, + "maxLength": 1 + }, + "embedGallery": { + "ref": "#draftEmbedGallery", + "type": "ref" + }, + "embedRecords": { + "type": "array", + "items": { + "ref": "#draftEmbedRecord", + "type": "ref" + }, + "maxLength": 1 + }, + "embedExternals": { + "type": "array", + "items": { + "ref": "#draftEmbedExternal", + "type": "ref" + }, + "maxLength": 1 + } + }, + "description": "One of the posts that compose a draft." + }, + "draftView": { + "type": "object", + "required": [ + "id", + "draft", + "createdAt", + "updatedAt" + ], + "properties": { + "id": { + "type": "string", + "format": "tid", + "description": "A TID to be used as a draft identifier." + }, + "draft": { + "ref": "#draft", + "type": "ref" + }, + "createdAt": { + "type": "string", + "format": "datetime", + "description": "The time the draft was created." + }, + "updatedAt": { + "type": "string", + "format": "datetime", + "description": "The time the draft was last updated." + } + }, + "description": "View to present drafts data to users." + }, + "draftWithId": { + "type": "object", + "required": [ + "id", + "draft" + ], + "properties": { + "id": { + "type": "string", + "format": "tid", + "description": "A TID to be used as a draft identifier." + }, + "draft": { + "ref": "#draft", + "type": "ref" + } + }, + "description": "A draft with an identifier, used to store drafts in private storage (stash)." + }, + "draftEmbedImage": { + "type": "object", + "required": [ + "localRef" + ], + "properties": { + "alt": { + "type": "string", + "maxGraphemes": 2000 + }, + "localRef": { + "ref": "#draftEmbedLocalRef", + "type": "ref" + } + } + }, + "draftEmbedVideo": { + "type": "object", + "required": [ + "localRef" + ], + "properties": { + "alt": { + "type": "string", + "maxGraphemes": 2000 + }, + "captions": { + "type": "array", + "items": { + "ref": "#draftEmbedCaption", + "type": "ref" + }, + "maxLength": 20 + }, + "localRef": { + "ref": "#draftEmbedLocalRef", + "type": "ref" + } + } + }, + "draftEmbedRecord": { + "type": "object", + "required": [ + "record" + ], + "properties": { + "record": { + "ref": "com.atproto.repo.strongRef", + "type": "ref" + } + } + }, + "draftEmbedCaption": { + "type": "object", + "required": [ + "lang", + "content" + ], + "properties": { + "lang": { + "type": "string", + "format": "language" + }, + "content": { + "type": "string", + "maxLength": 10000 + } + } + }, + "draftEmbedGallery": { + "type": "object", + "required": [ + "items" + ], + "properties": { + "items": { + "ref": "#draftEmbedGalleryItems", + "type": "ref" + } + } + }, + "draftEmbedExternal": { + "type": "object", + "required": [ + "uri" + ], + "properties": { + "uri": { + "type": "string", + "format": "uri" + } + } + }, + "draftEmbedLocalRef": { + "type": "object", + "required": [ + "path" + ], + "properties": { + "path": { + "type": "string", + "maxLength": 1024, + "minLength": 1, + "description": "Local, on-device ref to file to be embedded. Embeds are currently device-bound for drafts." + } + } + }, + "draftEmbedGalleryItems": { + "type": "array", + "items": { + "refs": [ + "#draftEmbedImage" + ], + "type": "union" + }, + "maxLength": 20, + "description": "The schema-level maxLength of 20 is a future-proof ceiling. Clients should currently enforce a soft limit of 10 items in authoring UIs." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/draft/deleteDraft.json b/lexicons/app/bsky/draft/deleteDraft.json new file mode 100644 index 0000000000..a88f3d48d2 --- /dev/null +++ b/lexicons/app/bsky/draft/deleteDraft.json @@ -0,0 +1,26 @@ +{ + "id": "app.bsky.draft.deleteDraft", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "string", + "format": "tid" + } + } + }, + "encoding": "application/json" + }, + "description": "Deletes a draft by ID. Requires authentication." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/draft/getDrafts.json b/lexicons/app/bsky/draft/getDrafts.json new file mode 100644 index 0000000000..aba35d59f8 --- /dev/null +++ b/lexicons/app/bsky/draft/getDrafts.json @@ -0,0 +1,46 @@ +{ + "id": "app.bsky.draft.getDrafts", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "drafts" + ], + "properties": { + "cursor": { + "type": "string" + }, + "drafts": { + "type": "array", + "items": { + "ref": "app.bsky.draft.defs#draftView", + "type": "ref" + } + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "properties": { + "limit": { + "type": "integer", + "default": 50, + "maximum": 100, + "minimum": 1 + }, + "cursor": { + "type": "string" + } + } + }, + "description": "Gets views of user drafts. Requires authentication." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/draft/updateDraft.json b/lexicons/app/bsky/draft/updateDraft.json new file mode 100644 index 0000000000..a99f331e1b --- /dev/null +++ b/lexicons/app/bsky/draft/updateDraft.json @@ -0,0 +1,26 @@ +{ + "id": "app.bsky.draft.updateDraft", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "draft" + ], + "properties": { + "draft": { + "ref": "app.bsky.draft.defs#draftWithId", + "type": "ref" + } + } + }, + "encoding": "application/json" + }, + "description": "Updates a draft using private storage (stash). If the draft ID points to a non-existing ID, the update will be silently ignored. This is done because updates don't enforce draft limit, so it accepts all writes, but will ignore invalid ones. Requires authentication." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/embed/defs.json b/lexicons/app/bsky/embed/defs.json new file mode 100644 index 0000000000..9aff68864c --- /dev/null +++ b/lexicons/app/bsky/embed/defs.json @@ -0,0 +1,25 @@ +{ + "id": "app.bsky.embed.defs", + "defs": { + "aspectRatio": { + "type": "object", + "required": [ + "width", + "height" + ], + "properties": { + "width": { + "type": "integer", + "minimum": 1 + }, + "height": { + "type": "integer", + "minimum": 1 + } + }, + "description": "width:height represents an aspect ratio. It may be approximate, and may not correspond to absolute dimensions in any given unit." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/embed/external.json b/lexicons/app/bsky/embed/external.json new file mode 100644 index 0000000000..d4bd886490 --- /dev/null +++ b/lexicons/app/bsky/embed/external.json @@ -0,0 +1,210 @@ +{ + "id": "app.bsky.embed.external", + "defs": { + "main": { + "type": "object", + "required": [ + "external" + ], + "properties": { + "external": { + "ref": "#external", + "type": "ref" + } + }, + "description": "A representation of some externally linked content (eg, a URL and 'card'), embedded in a Bluesky record (eg, a post)." + }, + "view": { + "type": "object", + "required": [ + "external" + ], + "properties": { + "external": { + "ref": "#viewExternal", + "type": "ref" + } + } + }, + "colorRGB": { + "type": "object", + "required": [ + "r", + "g", + "b" + ], + "properties": { + "b": { + "type": "integer", + "maximum": 255, + "minimum": 0 + }, + "g": { + "type": "integer", + "maximum": 255, + "minimum": 0 + }, + "r": { + "type": "integer", + "maximum": 255, + "minimum": 0 + } + }, + "description": "RGB color definition, inspired by site.standard.theme.color#rgb" + }, + "external": { + "type": "object", + "required": [ + "uri", + "title", + "description" + ], + "properties": { + "uri": { + "type": "string", + "format": "uri" + }, + "thumb": { + "type": "blob", + "accept": [ + "image/*" + ], + "maxSize": 1000000 + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "associatedRefs": { + "type": "array", + "items": { + "ref": "com.atproto.repo.strongRef", + "type": "ref" + }, + "description": "StrongRefs (uri+cid) of the Atmosphere records that backed this view." + } + } + }, + "viewExternal": { + "type": "object", + "required": [ + "uri", + "title", + "description" + ], + "properties": { + "uri": { + "type": "string", + "format": "uri" + }, + "thumb": { + "type": "string", + "format": "uri" + }, + "title": { + "type": "string" + }, + "labels": { + "type": "array", + "items": { + "ref": "com.atproto.label.defs#label", + "type": "ref" + } + }, + "source": { + "ref": "#viewExternalSource", + "type": "ref" + }, + "createdAt": { + "type": "string", + "format": "datetime", + "description": "When the external content was created, if available. Example: a publication date, for an article." + }, + "updatedAt": { + "type": "string", + "format": "datetime", + "description": "When the external content was updated, if available." + }, + "description": { + "type": "string" + }, + "readingTime": { + "type": "integer", + "description": "Estimated reading time in minutes, if applicable and available." + }, + "associatedRefs": { + "type": "array", + "items": { + "ref": "com.atproto.repo.strongRef", + "type": "ref" + }, + "description": "StrongRefs (uri+cid) of the Atmosphere records that backed this view." + }, + "associatedProfiles": { + "type": "array", + "items": { + "ref": "app.bsky.actor.defs#profileViewBasic", + "type": "ref" + }, + "description": "Profiles of the owners of the Atmosphere records that backed this view." + } + } + }, + "viewExternalSource": { + "type": "object", + "required": [ + "uri", + "title" + ], + "properties": { + "uri": { + "type": "string", + "format": "uri", + "description": "URI of the source, if available. Example: the https:// URL of a site.standard.publication record." + }, + "icon": { + "type": "string", + "format": "uri", + "description": "Fully-qualified URL where an icon representing the source can be fetched. For example, CDN location provided by the App View." + }, + "theme": { + "ref": "#viewExternalSourceTheme", + "type": "ref" + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "description": "The source of an external embed, such as a standard.site publication." + }, + "viewExternalSourceTheme": { + "type": "object", + "properties": { + "accentRGB": { + "ref": "#colorRGB", + "type": "ref" + }, + "backgroundRGB": { + "ref": "#colorRGB", + "type": "ref" + }, + "foregroundRGB": { + "ref": "#colorRGB", + "type": "ref" + }, + "accentForegroundRGB": { + "ref": "#colorRGB", + "type": "ref" + } + }, + "description": "The theme colors of an external source, such as a site.standard.publication. These colors may be used when rendering an embed from that source." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/embed/gallery.json b/lexicons/app/bsky/embed/gallery.json new file mode 100644 index 0000000000..39f8a3024e --- /dev/null +++ b/lexicons/app/bsky/embed/gallery.json @@ -0,0 +1,99 @@ +{ + "id": "app.bsky.embed.gallery", + "defs": { + "main": { + "type": "object", + "required": [ + "items" + ], + "properties": { + "items": { + "type": "array", + "items": { + "refs": [ + "#image" + ], + "type": "union", + "description": "The media items in the gallery. Each item may be of a different type, but all types must be supported by the client." + }, + "maxLength": 20, + "description": "The schema-level maxLength of 20 is a future-proof ceiling. Clients should currently enforce a soft limit of 10 items in authoring UIs." + } + } + }, + "view": { + "type": "object", + "required": [ + "items" + ], + "properties": { + "items": { + "type": "array", + "items": { + "refs": [ + "#viewImage" + ], + "type": "union" + } + } + } + }, + "image": { + "type": "object", + "required": [ + "image", + "alt", + "aspectRatio" + ], + "properties": { + "alt": { + "type": "string", + "description": "Alt text description of the image, for accessibility." + }, + "image": { + "type": "blob", + "accept": [ + "image/*" + ], + "maxSize": 2000000 + }, + "aspectRatio": { + "ref": "app.bsky.embed.defs#aspectRatio", + "type": "ref" + } + } + }, + "viewImage": { + "type": "object", + "required": [ + "thumbnail", + "fullsize", + "alt", + "aspectRatio" + ], + "properties": { + "alt": { + "type": "string", + "description": "Alt text description of the image, for accessibility." + }, + "fullsize": { + "type": "string", + "format": "uri", + "description": "Fully-qualified URL where a large version of the image can be fetched. May or may not be the exact original blob. For example, CDN location provided by the App View." + }, + "thumbnail": { + "type": "string", + "format": "uri", + "description": "Fully-qualified URL where a thumbnail of the image can be fetched. For example, CDN location provided by the App View." + }, + "aspectRatio": { + "ref": "app.bsky.embed.defs#aspectRatio", + "type": "ref" + } + } + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1, + "description": "An assortment of media embedded in a Bluesky record (eg, a post)." +} diff --git a/lexicons/app/bsky/embed/getEmbedExternalView.json b/lexicons/app/bsky/embed/getEmbedExternalView.json new file mode 100644 index 0000000000..7b60eb3fc9 --- /dev/null +++ b/lexicons/app/bsky/embed/getEmbedExternalView.json @@ -0,0 +1,62 @@ +{ + "id": "app.bsky.embed.getEmbedExternalView", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "properties": { + "view": { + "ref": "app.bsky.embed.external#view", + "type": "ref", + "description": "Hydrated view of the embed. Present only when the resolved records back the requested URL and supply enough information to populate the required `viewExternal` fields. Omitted alongside the rest of the response when no records resolved or validation failed." + }, + "associatedRefs": { + "type": "array", + "items": { + "ref": "com.atproto.repo.strongRef", + "type": "ref" + }, + "description": "StrongRefs (URI+CID) of the Atmosphere records that backed this view, suitable for embedding into a post's external.associatedRefs." + }, + "associatedRecords": { + "type": "array", + "items": { + "type": "unknown", + "description": "The raw record data of the Atmosphere records that backed this view. This is returned for convenience, to avoid the need for the client to separately fetch the record data for the associatedRefs. Example: the site.standard.document and site.standard.publication records that backed this view." + } + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "url", + "uris" + ], + "properties": { + "url": { + "type": "string", + "format": "uri", + "description": "The canonical web URL the embed represents (typically the URL the user pasted into the composer). Used as the returned view's `uri`. May be used for validation in the future." + }, + "uris": { + "type": "array", + "items": { + "type": "string", + "format": "at-uri" + }, + "maxLength": 4, + "description": "AT-URIs of any Atmosphere records that can be resolved and used to construct #externalView views. Example: a site.standard.document and optionally its associated site.standard.publication." + } + } + }, + "description": "Resolve one or more AT-URIs into the data needed to render an enhanced external embed. Returns `associatedRefs` (strongRefs to embed into a post's external.associatedRefs), the raw `associatedRecords`, and a hydrated `view`. The response is empty (`{}`) when no records were resolvable, or when validation determined the resolved records don't actually back the requested URL; clients should fall back to their own link-card rendering in that case and skip writing strongRefs to the post." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/embed/images.json b/lexicons/app/bsky/embed/images.json new file mode 100644 index 0000000000..7f9c30c263 --- /dev/null +++ b/lexicons/app/bsky/embed/images.json @@ -0,0 +1,93 @@ +{ + "id": "app.bsky.embed.images", + "defs": { + "main": { + "type": "object", + "required": [ + "images" + ], + "properties": { + "images": { + "type": "array", + "items": { + "ref": "#image", + "type": "ref" + }, + "maxLength": 4 + } + } + }, + "view": { + "type": "object", + "required": [ + "images" + ], + "properties": { + "images": { + "type": "array", + "items": { + "ref": "#viewImage", + "type": "ref" + }, + "maxLength": 4 + } + } + }, + "image": { + "type": "object", + "required": [ + "image", + "alt" + ], + "properties": { + "alt": { + "type": "string", + "description": "Alt text description of the image, for accessibility." + }, + "image": { + "type": "blob", + "accept": [ + "image/*" + ], + "maxSize": 2000000, + "description": "The raw image file. May be up to 2 MB, formerly limited to 1 MB." + }, + "aspectRatio": { + "ref": "app.bsky.embed.defs#aspectRatio", + "type": "ref" + } + } + }, + "viewImage": { + "type": "object", + "required": [ + "thumb", + "fullsize", + "alt" + ], + "properties": { + "alt": { + "type": "string", + "description": "Alt text description of the image, for accessibility." + }, + "thumb": { + "type": "string", + "format": "uri", + "description": "Fully-qualified URL where a thumbnail of the image can be fetched. For example, CDN location provided by the App View." + }, + "fullsize": { + "type": "string", + "format": "uri", + "description": "Fully-qualified URL where a large version of the image can be fetched. May or may not be the exact original blob. For example, CDN location provided by the App View." + }, + "aspectRatio": { + "ref": "app.bsky.embed.defs#aspectRatio", + "type": "ref" + } + } + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1, + "description": "A set of images embedded in a Bluesky record (eg, a post)." +} diff --git a/lexicons/app/bsky/embed/record.json b/lexicons/app/bsky/embed/record.json new file mode 100644 index 0000000000..3aef654207 --- /dev/null +++ b/lexicons/app/bsky/embed/record.json @@ -0,0 +1,162 @@ +{ + "id": "app.bsky.embed.record", + "defs": { + "main": { + "type": "object", + "required": [ + "record" + ], + "properties": { + "record": { + "ref": "com.atproto.repo.strongRef", + "type": "ref" + } + } + }, + "view": { + "type": "object", + "required": [ + "record" + ], + "properties": { + "record": { + "refs": [ + "#viewRecord", + "#viewNotFound", + "#viewBlocked", + "#viewDetached", + "app.bsky.feed.defs#generatorView", + "app.bsky.graph.defs#listView", + "app.bsky.labeler.defs#labelerView", + "app.bsky.graph.defs#starterPackViewBasic" + ], + "type": "union" + } + } + }, + "viewRecord": { + "type": "object", + "required": [ + "uri", + "cid", + "author", + "value", + "indexedAt" + ], + "properties": { + "cid": { + "type": "string", + "format": "cid" + }, + "uri": { + "type": "string", + "format": "at-uri" + }, + "value": { + "type": "unknown", + "description": "The record data itself." + }, + "author": { + "ref": "app.bsky.actor.defs#profileViewBasic", + "type": "ref" + }, + "embeds": { + "type": "array", + "items": { + "refs": [ + "app.bsky.embed.images#view", + "app.bsky.embed.video#view", + "app.bsky.embed.gallery#view", + "app.bsky.embed.external#view", + "app.bsky.embed.record#view", + "app.bsky.embed.recordWithMedia#view" + ], + "type": "union" + } + }, + "labels": { + "type": "array", + "items": { + "ref": "com.atproto.label.defs#label", + "type": "ref" + } + }, + "indexedAt": { + "type": "string", + "format": "datetime" + }, + "likeCount": { + "type": "integer" + }, + "quoteCount": { + "type": "integer" + }, + "replyCount": { + "type": "integer" + }, + "repostCount": { + "type": "integer" + } + } + }, + "viewBlocked": { + "type": "object", + "required": [ + "uri", + "blocked", + "author" + ], + "properties": { + "uri": { + "type": "string", + "format": "at-uri" + }, + "author": { + "ref": "app.bsky.feed.defs#blockedAuthor", + "type": "ref" + }, + "blocked": { + "type": "boolean", + "const": true + } + } + }, + "viewDetached": { + "type": "object", + "required": [ + "uri", + "detached" + ], + "properties": { + "uri": { + "type": "string", + "format": "at-uri" + }, + "detached": { + "type": "boolean", + "const": true + } + } + }, + "viewNotFound": { + "type": "object", + "required": [ + "uri", + "notFound" + ], + "properties": { + "uri": { + "type": "string", + "format": "at-uri" + }, + "notFound": { + "type": "boolean", + "const": true + } + } + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1, + "description": "A representation of a record embedded in a Bluesky record (eg, a post). For example, a quote-post, or sharing a feed generator record." +} diff --git a/lexicons/app/bsky/embed/recordWithMedia.json b/lexicons/app/bsky/embed/recordWithMedia.json new file mode 100644 index 0000000000..76da14a364 --- /dev/null +++ b/lexicons/app/bsky/embed/recordWithMedia.json @@ -0,0 +1,52 @@ +{ + "id": "app.bsky.embed.recordWithMedia", + "defs": { + "main": { + "type": "object", + "required": [ + "record", + "media" + ], + "properties": { + "media": { + "refs": [ + "app.bsky.embed.images", + "app.bsky.embed.video", + "app.bsky.embed.gallery", + "app.bsky.embed.external" + ], + "type": "union" + }, + "record": { + "ref": "app.bsky.embed.record", + "type": "ref" + } + } + }, + "view": { + "type": "object", + "required": [ + "record", + "media" + ], + "properties": { + "media": { + "refs": [ + "app.bsky.embed.images#view", + "app.bsky.embed.video#view", + "app.bsky.embed.gallery#view", + "app.bsky.embed.external#view" + ], + "type": "union" + }, + "record": { + "ref": "app.bsky.embed.record#view", + "type": "ref" + } + } + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1, + "description": "A representation of a record embedded in a Bluesky record (eg, a post), alongside other compatible embeds. For example, a quote post and image, or a quote post and external URL card." +} diff --git a/lexicons/app/bsky/embed/video.json b/lexicons/app/bsky/embed/video.json new file mode 100644 index 0000000000..67278da9c2 --- /dev/null +++ b/lexicons/app/bsky/embed/video.json @@ -0,0 +1,108 @@ +{ + "id": "app.bsky.embed.video", + "defs": { + "main": { + "type": "object", + "required": [ + "video" + ], + "properties": { + "alt": { + "type": "string", + "maxLength": 10000, + "description": "Alt text description of the video, for accessibility.", + "maxGraphemes": 1000 + }, + "video": { + "type": "blob", + "accept": [ + "video/mp4" + ], + "maxSize": 100000000, + "description": "The mp4 video file. May be up to 100mb, formerly limited to 50mb." + }, + "captions": { + "type": "array", + "items": { + "ref": "#caption", + "type": "ref" + }, + "maxLength": 20 + }, + "aspectRatio": { + "ref": "app.bsky.embed.defs#aspectRatio", + "type": "ref" + }, + "presentation": { + "type": "string", + "description": "A hint to the client about how to present the video.", + "knownValues": [ + "default", + "gif" + ] + } + } + }, + "view": { + "type": "object", + "required": [ + "cid", + "playlist" + ], + "properties": { + "alt": { + "type": "string", + "maxLength": 10000, + "maxGraphemes": 1000 + }, + "cid": { + "type": "string", + "format": "cid" + }, + "playlist": { + "type": "string", + "format": "uri" + }, + "thumbnail": { + "type": "string", + "format": "uri" + }, + "aspectRatio": { + "ref": "app.bsky.embed.defs#aspectRatio", + "type": "ref" + }, + "presentation": { + "type": "string", + "description": "A hint to the client about how to present the video.", + "knownValues": [ + "default", + "gif" + ] + } + } + }, + "caption": { + "type": "object", + "required": [ + "lang", + "file" + ], + "properties": { + "file": { + "type": "blob", + "accept": [ + "text/vtt" + ], + "maxSize": 20000 + }, + "lang": { + "type": "string", + "format": "language" + } + } + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1, + "description": "A video embedded in a Bluesky record (eg, a post)." +} diff --git a/lexicons/app/bsky/feed/defs.json b/lexicons/app/bsky/feed/defs.json new file mode 100644 index 0000000000..436782d4ec --- /dev/null +++ b/lexicons/app/bsky/feed/defs.json @@ -0,0 +1,545 @@ +{ + "id": "app.bsky.feed.defs", + "defs": { + "postView": { + "type": "object", + "required": [ + "uri", + "cid", + "author", + "record", + "indexedAt" + ], + "properties": { + "cid": { + "type": "string", + "format": "cid" + }, + "uri": { + "type": "string", + "format": "at-uri" + }, + "debug": { + "type": "unknown", + "description": "Debug information for internal development" + }, + "embed": { + "refs": [ + "app.bsky.embed.images#view", + "app.bsky.embed.video#view", + "app.bsky.embed.gallery#view", + "app.bsky.embed.external#view", + "app.bsky.embed.record#view", + "app.bsky.embed.recordWithMedia#view" + ], + "type": "union" + }, + "author": { + "ref": "app.bsky.actor.defs#profileViewBasic", + "type": "ref" + }, + "labels": { + "type": "array", + "items": { + "ref": "com.atproto.label.defs#label", + "type": "ref" + } + }, + "record": { + "type": "unknown" + }, + "viewer": { + "ref": "#viewerState", + "type": "ref" + }, + "indexedAt": { + "type": "string", + "format": "datetime" + }, + "likeCount": { + "type": "integer" + }, + "quoteCount": { + "type": "integer" + }, + "replyCount": { + "type": "integer" + }, + "threadgate": { + "ref": "#threadgateView", + "type": "ref" + }, + "repostCount": { + "type": "integer" + }, + "bookmarkCount": { + "type": "integer" + } + } + }, + "replyRef": { + "type": "object", + "required": [ + "root", + "parent" + ], + "properties": { + "root": { + "refs": [ + "#postView", + "#notFoundPost", + "#blockedPost" + ], + "type": "union" + }, + "parent": { + "refs": [ + "#postView", + "#notFoundPost", + "#blockedPost" + ], + "type": "union" + }, + "grandparentAuthor": { + "ref": "app.bsky.actor.defs#profileViewBasic", + "type": "ref", + "description": "When parent is a reply to another post, this is the author of that post." + } + } + }, + "reasonPin": { + "type": "object", + "properties": {} + }, + "blockedPost": { + "type": "object", + "required": [ + "uri", + "blocked", + "author" + ], + "properties": { + "uri": { + "type": "string", + "format": "at-uri" + }, + "author": { + "ref": "#blockedAuthor", + "type": "ref" + }, + "blocked": { + "type": "boolean", + "const": true + } + } + }, + "interaction": { + "type": "object", + "properties": { + "item": { + "type": "string", + "format": "at-uri" + }, + "event": { + "type": "string", + "knownValues": [ + "app.bsky.feed.defs#requestLess", + "app.bsky.feed.defs#requestMore", + "app.bsky.feed.defs#clickthroughItem", + "app.bsky.feed.defs#clickthroughAuthor", + "app.bsky.feed.defs#clickthroughReposter", + "app.bsky.feed.defs#clickthroughEmbed", + "app.bsky.feed.defs#interactionSeen", + "app.bsky.feed.defs#interactionLike", + "app.bsky.feed.defs#interactionRepost", + "app.bsky.feed.defs#interactionReply", + "app.bsky.feed.defs#interactionQuote", + "app.bsky.feed.defs#interactionShare" + ] + }, + "reqId": { + "type": "string", + "maxLength": 100, + "description": "Unique identifier per request that may be passed back alongside interactions." + }, + "feedContext": { + "type": "string", + "maxLength": 2000, + "description": "Context on a feed item that was originally supplied by the feed generator on getFeedSkeleton." + } + } + }, + "requestLess": { + "type": "token", + "description": "Request that less content like the given feed item be shown in the feed" + }, + "requestMore": { + "type": "token", + "description": "Request that more content like the given feed item be shown in the feed" + }, + "viewerState": { + "type": "object", + "properties": { + "like": { + "type": "string", + "format": "at-uri" + }, + "pinned": { + "type": "boolean" + }, + "repost": { + "type": "string", + "format": "at-uri" + }, + "bookmarked": { + "type": "boolean" + }, + "threadMuted": { + "type": "boolean" + }, + "replyDisabled": { + "type": "boolean" + }, + "embeddingDisabled": { + "type": "boolean" + } + }, + "description": "Metadata about the requesting account's relationship with the subject content. Only has meaningful content for authed requests." + }, + "feedViewPost": { + "type": "object", + "required": [ + "post" + ], + "properties": { + "post": { + "ref": "#postView", + "type": "ref" + }, + "reply": { + "ref": "#replyRef", + "type": "ref" + }, + "reqId": { + "type": "string", + "maxLength": 100, + "description": "Unique identifier per request that may be passed back alongside interactions." + }, + "reason": { + "refs": [ + "#reasonRepost", + "#reasonPin" + ], + "type": "union" + }, + "feedContext": { + "type": "string", + "maxLength": 2000, + "description": "Context provided by feed generator that may be passed back alongside interactions." + } + } + }, + "notFoundPost": { + "type": "object", + "required": [ + "uri", + "notFound" + ], + "properties": { + "uri": { + "type": "string", + "format": "at-uri" + }, + "notFound": { + "type": "boolean", + "const": true + } + } + }, + "reasonRepost": { + "type": "object", + "required": [ + "by", + "indexedAt" + ], + "properties": { + "by": { + "ref": "app.bsky.actor.defs#profileViewBasic", + "type": "ref" + }, + "cid": { + "type": "string", + "format": "cid" + }, + "uri": { + "type": "string", + "format": "at-uri" + }, + "indexedAt": { + "type": "string", + "format": "datetime" + } + } + }, + "blockedAuthor": { + "type": "object", + "required": [ + "did" + ], + "properties": { + "did": { + "type": "string", + "format": "did" + }, + "viewer": { + "ref": "app.bsky.actor.defs#viewerState", + "type": "ref" + } + } + }, + "generatorView": { + "type": "object", + "required": [ + "uri", + "cid", + "did", + "creator", + "displayName", + "indexedAt" + ], + "properties": { + "cid": { + "type": "string", + "format": "cid" + }, + "did": { + "type": "string", + "format": "did" + }, + "uri": { + "type": "string", + "format": "at-uri" + }, + "avatar": { + "type": "string", + "format": "uri" + }, + "labels": { + "type": "array", + "items": { + "ref": "com.atproto.label.defs#label", + "type": "ref" + } + }, + "viewer": { + "ref": "#generatorViewerState", + "type": "ref" + }, + "creator": { + "ref": "app.bsky.actor.defs#profileView", + "type": "ref" + }, + "indexedAt": { + "type": "string", + "format": "datetime" + }, + "likeCount": { + "type": "integer", + "minimum": 0 + }, + "contentMode": { + "type": "string", + "knownValues": [ + "app.bsky.feed.defs#contentModeUnspecified", + "app.bsky.feed.defs#contentModeVideo" + ] + }, + "description": { + "type": "string", + "maxLength": 3000, + "maxGraphemes": 300 + }, + "displayName": { + "type": "string" + }, + "descriptionFacets": { + "type": "array", + "items": { + "ref": "app.bsky.richtext.facet", + "type": "ref" + } + }, + "acceptsInteractions": { + "type": "boolean" + } + } + }, + "threadContext": { + "type": "object", + "properties": { + "rootAuthorLike": { + "type": "string", + "format": "at-uri" + } + }, + "description": "Metadata about this post within the context of the thread it is in." + }, + "threadViewPost": { + "type": "object", + "required": [ + "post" + ], + "properties": { + "post": { + "ref": "#postView", + "type": "ref" + }, + "parent": { + "refs": [ + "#threadViewPost", + "#notFoundPost", + "#blockedPost" + ], + "type": "union" + }, + "replies": { + "type": "array", + "items": { + "refs": [ + "#threadViewPost", + "#notFoundPost", + "#blockedPost" + ], + "type": "union" + } + }, + "threadContext": { + "ref": "#threadContext", + "type": "ref" + } + } + }, + "threadgateView": { + "type": "object", + "properties": { + "cid": { + "type": "string", + "format": "cid" + }, + "uri": { + "type": "string", + "format": "at-uri" + }, + "lists": { + "type": "array", + "items": { + "ref": "app.bsky.graph.defs#listViewBasic", + "type": "ref" + } + }, + "record": { + "type": "unknown" + } + } + }, + "interactionLike": { + "type": "token", + "description": "User liked the feed item" + }, + "interactionSeen": { + "type": "token", + "description": "Feed item was seen by user" + }, + "clickthroughItem": { + "type": "token", + "description": "User clicked through to the feed item" + }, + "contentModeVideo": { + "type": "token", + "description": "Declares the feed generator returns posts containing app.bsky.embed.video embeds." + }, + "interactionQuote": { + "type": "token", + "description": "User quoted the feed item" + }, + "interactionReply": { + "type": "token", + "description": "User replied to the feed item" + }, + "interactionShare": { + "type": "token", + "description": "User shared the feed item" + }, + "skeletonFeedPost": { + "type": "object", + "required": [ + "post" + ], + "properties": { + "post": { + "type": "string", + "format": "at-uri" + }, + "reason": { + "refs": [ + "#skeletonReasonRepost", + "#skeletonReasonPin" + ], + "type": "union" + }, + "feedContext": { + "type": "string", + "maxLength": 2000, + "description": "Context that will be passed through to client and may be passed to feed generator back alongside interactions." + } + } + }, + "clickthroughEmbed": { + "type": "token", + "description": "User clicked through to the embedded content of the feed item" + }, + "interactionRepost": { + "type": "token", + "description": "User reposted the feed item" + }, + "skeletonReasonPin": { + "type": "object", + "properties": {} + }, + "clickthroughAuthor": { + "type": "token", + "description": "User clicked through to the author of the feed item" + }, + "clickthroughReposter": { + "type": "token", + "description": "User clicked through to the reposter of the feed item" + }, + "generatorViewerState": { + "type": "object", + "properties": { + "like": { + "type": "string", + "format": "at-uri" + } + } + }, + "skeletonReasonRepost": { + "type": "object", + "required": [ + "repost" + ], + "properties": { + "repost": { + "type": "string", + "format": "at-uri" + } + } + }, + "contentModeUnspecified": { + "type": "token", + "description": "Declares the feed generator returns any types of posts." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/feed/describeFeedGenerator.json b/lexicons/app/bsky/feed/describeFeedGenerator.json new file mode 100644 index 0000000000..1549935910 --- /dev/null +++ b/lexicons/app/bsky/feed/describeFeedGenerator.json @@ -0,0 +1,61 @@ +{ + "id": "app.bsky.feed.describeFeedGenerator", + "defs": { + "feed": { + "type": "object", + "required": [ + "uri" + ], + "properties": { + "uri": { + "type": "string", + "format": "at-uri" + } + } + }, + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "did", + "feeds" + ], + "properties": { + "did": { + "type": "string", + "format": "did" + }, + "feeds": { + "type": "array", + "items": { + "ref": "#feed", + "type": "ref" + } + }, + "links": { + "ref": "#links", + "type": "ref" + } + } + }, + "encoding": "application/json" + }, + "description": "Get information about a feed generator, including policies and offered feed URIs. Does not require auth; implemented by Feed Generator services (not App View)." + }, + "links": { + "type": "object", + "properties": { + "privacyPolicy": { + "type": "string" + }, + "termsOfService": { + "type": "string" + } + } + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/feed/generator.json b/lexicons/app/bsky/feed/generator.json new file mode 100644 index 0000000000..082a6ca372 --- /dev/null +++ b/lexicons/app/bsky/feed/generator.json @@ -0,0 +1,73 @@ +{ + "id": "app.bsky.feed.generator", + "defs": { + "main": { + "key": "any", + "type": "record", + "record": { + "type": "object", + "required": [ + "did", + "displayName", + "createdAt" + ], + "properties": { + "did": { + "type": "string", + "format": "did" + }, + "avatar": { + "type": "blob", + "accept": [ + "image/png", + "image/jpeg" + ], + "maxSize": 1000000 + }, + "labels": { + "refs": [ + "com.atproto.label.defs#selfLabels" + ], + "type": "union", + "description": "Self-label values" + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "contentMode": { + "type": "string", + "knownValues": [ + "app.bsky.feed.defs#contentModeUnspecified", + "app.bsky.feed.defs#contentModeVideo" + ] + }, + "description": { + "type": "string", + "maxLength": 3000, + "maxGraphemes": 300 + }, + "displayName": { + "type": "string", + "maxLength": 240, + "maxGraphemes": 24 + }, + "descriptionFacets": { + "type": "array", + "items": { + "ref": "app.bsky.richtext.facet", + "type": "ref" + } + }, + "acceptsInteractions": { + "type": "boolean", + "description": "Declaration that a feed accepts feedback interactions from a client through app.bsky.feed.sendInteractions" + } + } + }, + "description": "Record declaring of the existence of a feed generator, and containing metadata about it. The record can exist in any repository." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/feed/getActorFeeds.json b/lexicons/app/bsky/feed/getActorFeeds.json new file mode 100644 index 0000000000..f0a41e2cb4 --- /dev/null +++ b/lexicons/app/bsky/feed/getActorFeeds.json @@ -0,0 +1,53 @@ +{ + "id": "app.bsky.feed.getActorFeeds", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "feeds" + ], + "properties": { + "feeds": { + "type": "array", + "items": { + "ref": "app.bsky.feed.defs#generatorView", + "type": "ref" + } + }, + "cursor": { + "type": "string" + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "actor" + ], + "properties": { + "actor": { + "type": "string", + "format": "at-identifier" + }, + "limit": { + "type": "integer", + "default": 50, + "maximum": 100, + "minimum": 1 + }, + "cursor": { + "type": "string" + } + } + }, + "description": "Get a list of feeds (feed generator records) created by the actor (in the actor's repo)." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/feed/getActorLikes.json b/lexicons/app/bsky/feed/getActorLikes.json new file mode 100644 index 0000000000..532b7df2f4 --- /dev/null +++ b/lexicons/app/bsky/feed/getActorLikes.json @@ -0,0 +1,61 @@ +{ + "id": "app.bsky.feed.getActorLikes", + "defs": { + "main": { + "type": "query", + "errors": [ + { + "name": "BlockedActor" + }, + { + "name": "BlockedByActor" + } + ], + "output": { + "schema": { + "type": "object", + "required": [ + "feed" + ], + "properties": { + "feed": { + "type": "array", + "items": { + "ref": "app.bsky.feed.defs#feedViewPost", + "type": "ref" + } + }, + "cursor": { + "type": "string" + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "actor" + ], + "properties": { + "actor": { + "type": "string", + "format": "at-identifier" + }, + "limit": { + "type": "integer", + "default": 50, + "maximum": 100, + "minimum": 1 + }, + "cursor": { + "type": "string" + } + } + }, + "description": "Get a list of posts liked by an actor. Requires auth, actor must be the requesting account." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/feed/getAuthorFeed.json b/lexicons/app/bsky/feed/getAuthorFeed.json new file mode 100644 index 0000000000..4d113c3123 --- /dev/null +++ b/lexicons/app/bsky/feed/getAuthorFeed.json @@ -0,0 +1,77 @@ +{ + "id": "app.bsky.feed.getAuthorFeed", + "defs": { + "main": { + "type": "query", + "errors": [ + { + "name": "BlockedActor" + }, + { + "name": "BlockedByActor" + } + ], + "output": { + "schema": { + "type": "object", + "required": [ + "feed" + ], + "properties": { + "feed": { + "type": "array", + "items": { + "ref": "app.bsky.feed.defs#feedViewPost", + "type": "ref" + } + }, + "cursor": { + "type": "string" + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "actor" + ], + "properties": { + "actor": { + "type": "string", + "format": "at-identifier" + }, + "limit": { + "type": "integer", + "default": 50, + "maximum": 100, + "minimum": 1 + }, + "cursor": { + "type": "string" + }, + "filter": { + "type": "string", + "default": "posts_with_replies", + "description": "Combinations of post/repost types to include in response.", + "knownValues": [ + "posts_with_replies", + "posts_no_replies", + "posts_with_media", + "posts_and_author_threads", + "posts_with_video" + ] + }, + "includePins": { + "type": "boolean", + "default": false + } + } + }, + "description": "Get a view of an actor's 'author feed' (post and reposts by the author). Does not require auth." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/feed/getFeed.json b/lexicons/app/bsky/feed/getFeed.json new file mode 100644 index 0000000000..6d19d64b96 --- /dev/null +++ b/lexicons/app/bsky/feed/getFeed.json @@ -0,0 +1,58 @@ +{ + "id": "app.bsky.feed.getFeed", + "defs": { + "main": { + "type": "query", + "errors": [ + { + "name": "UnknownFeed" + } + ], + "output": { + "schema": { + "type": "object", + "required": [ + "feed" + ], + "properties": { + "feed": { + "type": "array", + "items": { + "ref": "app.bsky.feed.defs#feedViewPost", + "type": "ref" + } + }, + "cursor": { + "type": "string" + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "feed" + ], + "properties": { + "feed": { + "type": "string", + "format": "at-uri" + }, + "limit": { + "type": "integer", + "default": 50, + "maximum": 100, + "minimum": 1 + }, + "cursor": { + "type": "string" + } + } + }, + "description": "Get a hydrated feed from an actor's selected feed generator. Implemented by App View." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/feed/getFeedGenerator.json b/lexicons/app/bsky/feed/getFeedGenerator.json new file mode 100644 index 0000000000..9403bbc5fa --- /dev/null +++ b/lexicons/app/bsky/feed/getFeedGenerator.json @@ -0,0 +1,49 @@ +{ + "id": "app.bsky.feed.getFeedGenerator", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "view", + "isOnline", + "isValid" + ], + "properties": { + "view": { + "ref": "app.bsky.feed.defs#generatorView", + "type": "ref" + }, + "isValid": { + "type": "boolean", + "description": "Indicates whether the feed generator service is compatible with the record declaration." + }, + "isOnline": { + "type": "boolean", + "description": "Indicates whether the feed generator service has been online recently, or else seems to be inactive." + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "feed" + ], + "properties": { + "feed": { + "type": "string", + "format": "at-uri", + "description": "AT-URI of the feed generator record." + } + } + }, + "description": "Get information about a feed generator. Implemented by AppView." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/feed/getFeedGenerators.json b/lexicons/app/bsky/feed/getFeedGenerators.json new file mode 100644 index 0000000000..79938c932b --- /dev/null +++ b/lexicons/app/bsky/feed/getFeedGenerators.json @@ -0,0 +1,44 @@ +{ + "id": "app.bsky.feed.getFeedGenerators", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "feeds" + ], + "properties": { + "feeds": { + "type": "array", + "items": { + "ref": "app.bsky.feed.defs#generatorView", + "type": "ref" + } + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "feeds" + ], + "properties": { + "feeds": { + "type": "array", + "items": { + "type": "string", + "format": "at-uri" + } + } + } + }, + "description": "Get information about a list of feed generators." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/feed/getFeedSkeleton.json b/lexicons/app/bsky/feed/getFeedSkeleton.json new file mode 100644 index 0000000000..f9ca591405 --- /dev/null +++ b/lexicons/app/bsky/feed/getFeedSkeleton.json @@ -0,0 +1,64 @@ +{ + "id": "app.bsky.feed.getFeedSkeleton", + "defs": { + "main": { + "type": "query", + "errors": [ + { + "name": "UnknownFeed" + } + ], + "output": { + "schema": { + "type": "object", + "required": [ + "feed" + ], + "properties": { + "feed": { + "type": "array", + "items": { + "ref": "app.bsky.feed.defs#skeletonFeedPost", + "type": "ref" + } + }, + "reqId": { + "type": "string", + "maxLength": 100, + "description": "Unique identifier per request that may be passed back alongside interactions." + }, + "cursor": { + "type": "string" + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "feed" + ], + "properties": { + "feed": { + "type": "string", + "format": "at-uri", + "description": "Reference to feed generator record describing the specific feed being requested." + }, + "limit": { + "type": "integer", + "default": 50, + "maximum": 100, + "minimum": 1 + }, + "cursor": { + "type": "string" + } + } + }, + "description": "Get a skeleton of a feed provided by a feed generator. Auth is optional, depending on provider requirements, and provides the DID of the requester. Implemented by Feed Generator Service." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/feed/getLikes.json b/lexicons/app/bsky/feed/getLikes.json new file mode 100644 index 0000000000..db5c83ecda --- /dev/null +++ b/lexicons/app/bsky/feed/getLikes.json @@ -0,0 +1,90 @@ +{ + "id": "app.bsky.feed.getLikes", + "defs": { + "like": { + "type": "object", + "required": [ + "indexedAt", + "createdAt", + "actor" + ], + "properties": { + "actor": { + "ref": "app.bsky.actor.defs#profileView", + "type": "ref" + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "indexedAt": { + "type": "string", + "format": "datetime" + } + } + }, + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "uri", + "likes" + ], + "properties": { + "cid": { + "type": "string", + "format": "cid" + }, + "uri": { + "type": "string", + "format": "at-uri" + }, + "likes": { + "type": "array", + "items": { + "ref": "#like", + "type": "ref" + } + }, + "cursor": { + "type": "string" + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "uri" + ], + "properties": { + "cid": { + "type": "string", + "format": "cid", + "description": "CID of the subject record (aka, specific version of record), to filter likes." + }, + "uri": { + "type": "string", + "format": "at-uri", + "description": "AT-URI of the subject (eg, a post record)." + }, + "limit": { + "type": "integer", + "default": 50, + "maximum": 100, + "minimum": 1 + }, + "cursor": { + "type": "string" + } + } + }, + "description": "Get like records which reference a subject (by AT-URI and CID)." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/feed/getListFeed.json b/lexicons/app/bsky/feed/getListFeed.json new file mode 100644 index 0000000000..7d9311799f --- /dev/null +++ b/lexicons/app/bsky/feed/getListFeed.json @@ -0,0 +1,59 @@ +{ + "id": "app.bsky.feed.getListFeed", + "defs": { + "main": { + "type": "query", + "errors": [ + { + "name": "UnknownList" + } + ], + "output": { + "schema": { + "type": "object", + "required": [ + "feed" + ], + "properties": { + "feed": { + "type": "array", + "items": { + "ref": "app.bsky.feed.defs#feedViewPost", + "type": "ref" + } + }, + "cursor": { + "type": "string" + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "list" + ], + "properties": { + "list": { + "type": "string", + "format": "at-uri", + "description": "Reference (AT-URI) to the list record." + }, + "limit": { + "type": "integer", + "default": 50, + "maximum": 100, + "minimum": 1 + }, + "cursor": { + "type": "string" + } + } + }, + "description": "Get a feed of recent posts from a list (posts and reposts from any actors on the list). Does not require auth." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/feed/getPostThread.json b/lexicons/app/bsky/feed/getPostThread.json new file mode 100644 index 0000000000..b62a78a00a --- /dev/null +++ b/lexicons/app/bsky/feed/getPostThread.json @@ -0,0 +1,66 @@ +{ + "id": "app.bsky.feed.getPostThread", + "defs": { + "main": { + "type": "query", + "errors": [ + { + "name": "NotFound" + } + ], + "output": { + "schema": { + "type": "object", + "required": [ + "thread" + ], + "properties": { + "thread": { + "refs": [ + "app.bsky.feed.defs#threadViewPost", + "app.bsky.feed.defs#notFoundPost", + "app.bsky.feed.defs#blockedPost" + ], + "type": "union" + }, + "threadgate": { + "ref": "app.bsky.feed.defs#threadgateView", + "type": "ref" + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "uri" + ], + "properties": { + "uri": { + "type": "string", + "format": "at-uri", + "description": "Reference (AT-URI) to post record." + }, + "depth": { + "type": "integer", + "default": 6, + "maximum": 1000, + "minimum": 0, + "description": "How many levels of reply depth should be included in response." + }, + "parentHeight": { + "type": "integer", + "default": 80, + "maximum": 1000, + "minimum": 0, + "description": "How many levels of parent (and grandparent, etc) post to include." + } + } + }, + "description": "Get posts in a thread. Does not require auth, but additional metadata and filtering will be applied for authed requests." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/feed/getPosts.json b/lexicons/app/bsky/feed/getPosts.json new file mode 100644 index 0000000000..c52de1aecd --- /dev/null +++ b/lexicons/app/bsky/feed/getPosts.json @@ -0,0 +1,46 @@ +{ + "id": "app.bsky.feed.getPosts", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "posts" + ], + "properties": { + "posts": { + "type": "array", + "items": { + "ref": "app.bsky.feed.defs#postView", + "type": "ref" + } + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "uris" + ], + "properties": { + "uris": { + "type": "array", + "items": { + "type": "string", + "format": "at-uri" + }, + "maxLength": 25, + "description": "List of post AT-URIs to return hydrated views for." + } + } + }, + "description": "Gets post views for a specified list of posts (by AT-URI). This is sometimes referred to as 'hydrating' a 'feed skeleton'." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/feed/getQuotes.json b/lexicons/app/bsky/feed/getQuotes.json new file mode 100644 index 0000000000..cc507ec925 --- /dev/null +++ b/lexicons/app/bsky/feed/getQuotes.json @@ -0,0 +1,68 @@ +{ + "id": "app.bsky.feed.getQuotes", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "uri", + "posts" + ], + "properties": { + "cid": { + "type": "string", + "format": "cid" + }, + "uri": { + "type": "string", + "format": "at-uri" + }, + "posts": { + "type": "array", + "items": { + "ref": "app.bsky.feed.defs#postView", + "type": "ref" + } + }, + "cursor": { + "type": "string" + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "uri" + ], + "properties": { + "cid": { + "type": "string", + "format": "cid", + "description": "If supplied, filters to quotes of specific version (by CID) of the post record." + }, + "uri": { + "type": "string", + "format": "at-uri", + "description": "Reference (AT-URI) of post record" + }, + "limit": { + "type": "integer", + "default": 50, + "maximum": 100, + "minimum": 1 + }, + "cursor": { + "type": "string" + } + } + }, + "description": "Get a list of quotes for a given post." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/feed/getRepostedBy.json b/lexicons/app/bsky/feed/getRepostedBy.json new file mode 100644 index 0000000000..3df7fa8f53 --- /dev/null +++ b/lexicons/app/bsky/feed/getRepostedBy.json @@ -0,0 +1,68 @@ +{ + "id": "app.bsky.feed.getRepostedBy", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "uri", + "repostedBy" + ], + "properties": { + "cid": { + "type": "string", + "format": "cid" + }, + "uri": { + "type": "string", + "format": "at-uri" + }, + "cursor": { + "type": "string" + }, + "repostedBy": { + "type": "array", + "items": { + "ref": "app.bsky.actor.defs#profileView", + "type": "ref" + } + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "uri" + ], + "properties": { + "cid": { + "type": "string", + "format": "cid", + "description": "If supplied, filters to reposts of specific version (by CID) of the post record." + }, + "uri": { + "type": "string", + "format": "at-uri", + "description": "Reference (AT-URI) of post record" + }, + "limit": { + "type": "integer", + "default": 50, + "maximum": 100, + "minimum": 1 + }, + "cursor": { + "type": "string" + } + } + }, + "description": "Get a list of reposts for a given post." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/feed/getSuggestedFeeds.json b/lexicons/app/bsky/feed/getSuggestedFeeds.json new file mode 100644 index 0000000000..26cb0d1b49 --- /dev/null +++ b/lexicons/app/bsky/feed/getSuggestedFeeds.json @@ -0,0 +1,46 @@ +{ + "id": "app.bsky.feed.getSuggestedFeeds", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "feeds" + ], + "properties": { + "feeds": { + "type": "array", + "items": { + "ref": "app.bsky.feed.defs#generatorView", + "type": "ref" + } + }, + "cursor": { + "type": "string" + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "properties": { + "limit": { + "type": "integer", + "default": 50, + "maximum": 100, + "minimum": 1 + }, + "cursor": { + "type": "string" + } + } + }, + "description": "Get a list of suggested feeds (feed generators) for the requesting account." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/feed/getTimeline.json b/lexicons/app/bsky/feed/getTimeline.json new file mode 100644 index 0000000000..e35dbbc1cb --- /dev/null +++ b/lexicons/app/bsky/feed/getTimeline.json @@ -0,0 +1,50 @@ +{ + "id": "app.bsky.feed.getTimeline", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "feed" + ], + "properties": { + "feed": { + "type": "array", + "items": { + "ref": "app.bsky.feed.defs#feedViewPost", + "type": "ref" + } + }, + "cursor": { + "type": "string" + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "properties": { + "limit": { + "type": "integer", + "default": 50, + "maximum": 100, + "minimum": 1 + }, + "cursor": { + "type": "string" + }, + "algorithm": { + "type": "string", + "description": "Variant 'algorithm' for timeline. Implementation-specific. NOTE: most feed flexibility has been moved to feed generator mechanism." + } + } + }, + "description": "Get a view of the requesting account's home timeline. This is expected to be some form of reverse-chronological feed." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/feed/like.json b/lexicons/app/bsky/feed/like.json new file mode 100644 index 0000000000..2b941f743c --- /dev/null +++ b/lexicons/app/bsky/feed/like.json @@ -0,0 +1,33 @@ +{ + "id": "app.bsky.feed.like", + "defs": { + "main": { + "key": "tid", + "type": "record", + "record": { + "type": "object", + "required": [ + "subject", + "createdAt" + ], + "properties": { + "via": { + "ref": "com.atproto.repo.strongRef", + "type": "ref" + }, + "subject": { + "ref": "com.atproto.repo.strongRef", + "type": "ref" + }, + "createdAt": { + "type": "string", + "format": "datetime" + } + } + }, + "description": "Record declaring a 'like' of a piece of subject content." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/feed/post.json b/lexicons/app/bsky/feed/post.json new file mode 100644 index 0000000000..50b8a53c88 --- /dev/null +++ b/lexicons/app/bsky/feed/post.json @@ -0,0 +1,146 @@ +{ + "id": "app.bsky.feed.post", + "defs": { + "main": { + "key": "tid", + "type": "record", + "record": { + "type": "object", + "required": [ + "text", + "createdAt" + ], + "properties": { + "tags": { + "type": "array", + "items": { + "type": "string", + "maxLength": 640, + "maxGraphemes": 64 + }, + "maxLength": 8, + "description": "Additional hashtags, in addition to any included in post text and facets." + }, + "text": { + "type": "string", + "maxLength": 3000, + "description": "The primary post content. May be an empty string, if there are embeds.", + "maxGraphemes": 300 + }, + "embed": { + "refs": [ + "app.bsky.embed.images", + "app.bsky.embed.video", + "app.bsky.embed.gallery", + "app.bsky.embed.external", + "app.bsky.embed.record", + "app.bsky.embed.recordWithMedia" + ], + "type": "union" + }, + "langs": { + "type": "array", + "items": { + "type": "string", + "format": "language" + }, + "maxLength": 3, + "description": "Indicates human language of post primary text content." + }, + "reply": { + "ref": "#replyRef", + "type": "ref" + }, + "facets": { + "type": "array", + "items": { + "ref": "app.bsky.richtext.facet", + "type": "ref" + }, + "description": "Annotations of text (mentions, URLs, hashtags, etc)" + }, + "labels": { + "refs": [ + "com.atproto.label.defs#selfLabels" + ], + "type": "union", + "description": "Self-label values for this post. Effectively content warnings." + }, + "entities": { + "type": "array", + "items": { + "ref": "#entity", + "type": "ref" + }, + "description": "DEPRECATED: replaced by app.bsky.richtext.facet." + }, + "createdAt": { + "type": "string", + "format": "datetime", + "description": "Client-declared timestamp when this post was originally created." + } + } + }, + "description": "Record containing a Bluesky post." + }, + "entity": { + "type": "object", + "required": [ + "index", + "type", + "value" + ], + "properties": { + "type": { + "type": "string", + "description": "Expected values are 'mention' and 'link'." + }, + "index": { + "ref": "#textSlice", + "type": "ref" + }, + "value": { + "type": "string" + } + }, + "description": "Deprecated: use facets instead." + }, + "replyRef": { + "type": "object", + "required": [ + "root", + "parent" + ], + "properties": { + "root": { + "ref": "com.atproto.repo.strongRef", + "type": "ref" + }, + "parent": { + "ref": "com.atproto.repo.strongRef", + "type": "ref" + } + } + }, + "textSlice": { + "type": "object", + "required": [ + "start", + "end" + ], + "properties": { + "end": { + "type": "integer", + "minimum": 0 + }, + "start": { + "type": "integer", + "minimum": 0 + } + }, + "description": "Deprecated. Use app.bsky.richtext instead -- A text segment. Start is inclusive, end is exclusive. Indices are for utf16-encoded strings." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/feed/postgate.json b/lexicons/app/bsky/feed/postgate.json new file mode 100644 index 0000000000..9fa0775a68 --- /dev/null +++ b/lexicons/app/bsky/feed/postgate.json @@ -0,0 +1,55 @@ +{ + "id": "app.bsky.feed.postgate", + "defs": { + "main": { + "key": "tid", + "type": "record", + "record": { + "type": "object", + "required": [ + "post", + "createdAt" + ], + "properties": { + "post": { + "type": "string", + "format": "at-uri", + "description": "Reference (AT-URI) to the post record." + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "embeddingRules": { + "type": "array", + "items": { + "refs": [ + "#disableRule" + ], + "type": "union" + }, + "maxLength": 5, + "description": "List of rules defining who can embed this post. If value is an empty array or is undefined, no particular rules apply and anyone can embed." + }, + "detachedEmbeddingUris": { + "type": "array", + "items": { + "type": "string", + "format": "at-uri" + }, + "maxLength": 50, + "description": "List of AT-URIs embedding this post that the author has detached from." + } + } + }, + "description": "Record defining interaction rules for a post. The record key (rkey) of the postgate record must match the record key of the post, and that record must be in the same repository." + }, + "disableRule": { + "type": "object", + "properties": {}, + "description": "Disables embedding of this post." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/feed/repost.json b/lexicons/app/bsky/feed/repost.json new file mode 100644 index 0000000000..6563b1f5e4 --- /dev/null +++ b/lexicons/app/bsky/feed/repost.json @@ -0,0 +1,33 @@ +{ + "id": "app.bsky.feed.repost", + "defs": { + "main": { + "key": "tid", + "type": "record", + "record": { + "type": "object", + "required": [ + "subject", + "createdAt" + ], + "properties": { + "via": { + "ref": "com.atproto.repo.strongRef", + "type": "ref" + }, + "subject": { + "ref": "com.atproto.repo.strongRef", + "type": "ref" + }, + "createdAt": { + "type": "string", + "format": "datetime" + } + } + }, + "description": "Record representing a 'repost' of an existing Bluesky post." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/feed/searchPosts.json b/lexicons/app/bsky/feed/searchPosts.json new file mode 100644 index 0000000000..23ba045c24 --- /dev/null +++ b/lexicons/app/bsky/feed/searchPosts.json @@ -0,0 +1,113 @@ +{ + "id": "app.bsky.feed.searchPosts", + "defs": { + "main": { + "type": "query", + "errors": [ + { + "name": "BadQueryString" + } + ], + "output": { + "schema": { + "type": "object", + "required": [ + "posts" + ], + "properties": { + "posts": { + "type": "array", + "items": { + "ref": "app.bsky.feed.defs#postView", + "type": "ref" + } + }, + "cursor": { + "type": "string" + }, + "hitsTotal": { + "type": "integer", + "description": "Count of search hits. Optional, may be rounded/truncated, and may not be possible to paginate through all hits." + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "q" + ], + "properties": { + "q": { + "type": "string", + "description": "Search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended." + }, + "tag": { + "type": "array", + "items": { + "type": "string", + "maxLength": 640, + "maxGraphemes": 64 + }, + "description": "Filter to posts with the given tag (hashtag), based on rich-text facet or tag field. Do not include the hash (#) prefix. Multiple tags can be specified, with 'AND' matching." + }, + "url": { + "type": "string", + "format": "uri", + "description": "Filter to posts with links (facet links or embeds) pointing to this URL. Server may apply URL normalization or fuzzy matching." + }, + "lang": { + "type": "string", + "format": "language", + "description": "Filter to posts in the given language. Expected to be based on post language field, though server may override language detection." + }, + "sort": { + "type": "string", + "default": "latest", + "description": "Specifies the ranking order of results.", + "knownValues": [ + "top", + "latest" + ] + }, + "limit": { + "type": "integer", + "default": 25, + "maximum": 100, + "minimum": 1 + }, + "since": { + "type": "string", + "description": "Filter results for posts after the indicated datetime (inclusive). Expected to use 'sortAt' timestamp, which may not match 'createdAt'. Can be a datetime, or just an ISO date (YYYY-MM-DD)." + }, + "until": { + "type": "string", + "description": "Filter results for posts before the indicated datetime (not inclusive). Expected to use 'sortAt' timestamp, which may not match 'createdAt'. Can be a datetime, or just an ISO date (YYY-MM-DD)." + }, + "author": { + "type": "string", + "format": "at-identifier", + "description": "Filter to posts by the given account. Handles are resolved to DID before query-time." + }, + "cursor": { + "type": "string", + "description": "Optional pagination mechanism; may not necessarily allow scrolling through entire result set." + }, + "domain": { + "type": "string", + "description": "Filter to posts with URLs (facet links or embeds) linking to the given domain (hostname). Server may apply hostname normalization." + }, + "mentions": { + "type": "string", + "format": "at-identifier", + "description": "Filter to posts which mention the given account. Handles are resolved to DID before query-time. Only matches rich-text facet mentions." + } + } + }, + "description": "Find posts matching search criteria, returning views of those posts. Note that this API endpoint may require authentication (eg, not public) for some service providers and implementations." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/feed/searchPostsV2.json b/lexicons/app/bsky/feed/searchPostsV2.json new file mode 100644 index 0000000000..0d3ba5cc16 --- /dev/null +++ b/lexicons/app/bsky/feed/searchPostsV2.json @@ -0,0 +1,251 @@ +{ + "id": "app.bsky.feed.searchPostsV2", + "defs": { + "main": { + "type": "query", + "errors": [ + { + "name": "BadQueryString" + } + ], + "output": { + "schema": { + "type": "object", + "required": [ + "posts" + ], + "properties": { + "posts": { + "type": "array", + "items": { + "ref": "app.bsky.feed.defs#postView", + "type": "ref" + }, + "description": "Hydrated views of matching posts." + }, + "cursor": { + "type": "string", + "description": "Cursor for the next page of results." + }, + "hitsTotal": { + "type": "integer", + "description": "Estimated total number of matching hits. May be rounded or truncated." + }, + "detectedQueryLanguages": { + "type": "array", + "items": { + "type": "string", + "knownValues": [ + "ja", + "zh", + "ko", + "th", + "ar" + ] + }, + "description": "Query languages detected for CJK, Thai, or Arabic text. Empty or omitted for other scripts." + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [], + "properties": { + "sort": { + "type": "string", + "description": "Ranking order for results. 'recent' sorts by recency; 'top' uses search ranking.", + "knownValues": [ + "recent", + "top" + ] + }, + "urls": { + "type": "array", + "items": { + "type": "string", + "format": "uri" + }, + "description": "Include posts that link to any of these URLs." + }, + "limit": { + "type": "integer", + "default": 25, + "maximum": 100, + "minimum": 1, + "description": "Maximum number of results to return." + }, + "query": { + "type": "string", + "description": "Search query string. A query or at least one filter is required." + }, + "since": { + "type": "string", + "description": "Include posts indexed at or after this timestamp. Can be a datetime, or just an ISO date (YYYY-MM-DD)." + }, + "until": { + "type": "string", + "description": "Include posts indexed before this timestamp. Defaults to the current time. Can be a datetime, or just an ISO date (YYYY-MM-DD)." + }, + "cursor": { + "type": "string", + "description": "Optional pagination cursor." + }, + "allTime": { + "type": "boolean", + "description": "Search the full index instead of the recent-post window." + }, + "authors": { + "type": "array", + "items": { + "type": "string", + "format": "at-identifier" + }, + "description": "Include posts by any of these authors. Handles are resolved to DIDs before searching." + }, + "domains": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Include posts that link to any of these domains." + }, + "hasMedia": { + "type": "boolean", + "description": "Include only posts with media." + }, + "hasVideo": { + "type": "boolean", + "description": "Include only posts with video." + }, + "hashtags": { + "type": "array", + "items": { + "type": "string", + "maxLength": 640, + "maxGraphemes": 64 + }, + "description": "Include posts tagged with any of these hashtags. Do not include the hash (#) prefix." + }, + "mentions": { + "type": "array", + "items": { + "type": "string", + "format": "at-identifier" + }, + "description": "Include posts that mention any of these accounts. Handles are resolved to DIDs before searching." + }, + "following": { + "type": "boolean", + "description": "Include only posts from accounts followed by the viewer." + }, + "languages": { + "type": "array", + "items": { + "type": "string", + "format": "language" + }, + "description": "Include posts whose language matches any of these language codes." + }, + "excludeUrls": { + "type": "array", + "items": { + "type": "string", + "format": "uri" + }, + "description": "Exclude posts that link to any of these URLs." + }, + "repliesOnly": { + "type": "boolean", + "description": "Include only replies. Mutually exclusive with excludeReplies." + }, + "queryLanguage": { + "type": "string", + "description": "Language analyzer hint for the query text. If unset, the server auto-detects when possible.", + "knownValues": [ + "ja", + "zh", + "ko", + "th", + "ar" + ] + }, + "threadRootUri": { + "type": "string", + "format": "at-uri", + "description": "Include only posts in the thread rooted at this post URI." + }, + "embeddedAtUris": { + "type": "array", + "items": { + "type": "string", + "format": "at-uri" + }, + "description": "Include posts that embed any of these AT URIs." + }, + "excludeAuthors": { + "type": "array", + "items": { + "type": "string", + "format": "at-identifier" + }, + "description": "Exclude posts by any of these authors. Handles are resolved to DIDs before searching." + }, + "excludeDomains": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Exclude posts that link to any of these domains." + }, + "excludeReplies": { + "type": "boolean", + "description": "Exclude replies from results. Mutually exclusive with repliesOnly." + }, + "replyParentUri": { + "type": "string", + "format": "at-uri", + "description": "Include only direct replies to this parent post URI." + }, + "excludeHashtags": { + "type": "array", + "items": { + "type": "string", + "maxLength": 640, + "maxGraphemes": 64 + }, + "description": "Exclude posts tagged with any of these hashtags. Do not include the hash (#) prefix." + }, + "excludeMentions": { + "type": "array", + "items": { + "type": "string", + "format": "at-identifier" + }, + "description": "Exclude posts that mention any of these accounts. Handles are resolved to DIDs before searching." + }, + "excludeLanguages": { + "type": "array", + "items": { + "type": "string", + "format": "language" + }, + "description": "Exclude posts whose language matches any of these language codes." + }, + "excludeEmbeddedAtUris": { + "type": "array", + "items": { + "type": "string", + "format": "at-uri" + }, + "description": "Exclude posts that embed any of these AT URIs." + } + } + }, + "description": "Find posts matching a search query or filters, returning search hits for matching post records." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/feed/sendInteractions.json b/lexicons/app/bsky/feed/sendInteractions.json new file mode 100644 index 0000000000..eea9f6c1fc --- /dev/null +++ b/lexicons/app/bsky/feed/sendInteractions.json @@ -0,0 +1,40 @@ +{ + "id": "app.bsky.feed.sendInteractions", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "interactions" + ], + "properties": { + "feed": { + "type": "string", + "format": "at-uri" + }, + "interactions": { + "type": "array", + "items": { + "ref": "app.bsky.feed.defs#interaction", + "type": "ref" + } + } + } + }, + "encoding": "application/json" + }, + "output": { + "schema": { + "type": "object", + "properties": {} + }, + "encoding": "application/json" + }, + "description": "Send information about interactions with feed items back to the feed generator that served them." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/feed/threadgate.json b/lexicons/app/bsky/feed/threadgate.json new file mode 100644 index 0000000000..8b4d3c6117 --- /dev/null +++ b/lexicons/app/bsky/feed/threadgate.json @@ -0,0 +1,81 @@ +{ + "id": "app.bsky.feed.threadgate", + "defs": { + "main": { + "key": "tid", + "type": "record", + "record": { + "type": "object", + "required": [ + "post", + "createdAt" + ], + "properties": { + "post": { + "type": "string", + "format": "at-uri", + "description": "Reference (AT-URI) to the post record." + }, + "allow": { + "type": "array", + "items": { + "refs": [ + "#mentionRule", + "#followerRule", + "#followingRule", + "#listRule" + ], + "type": "union" + }, + "maxLength": 5, + "description": "List of rules defining who can reply to this post. If value is an empty array, no one can reply. If value is undefined, anyone can reply." + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "hiddenReplies": { + "type": "array", + "items": { + "type": "string", + "format": "at-uri" + }, + "maxLength": 300, + "description": "List of hidden reply URIs." + } + } + }, + "description": "Record defining interaction gating rules for a thread (aka, reply controls). The record key (rkey) of the threadgate record must match the record key of the thread's root post, and that record must be in the same repository." + }, + "listRule": { + "type": "object", + "required": [ + "list" + ], + "properties": { + "list": { + "type": "string", + "format": "at-uri" + } + }, + "description": "Allow replies from actors on a list." + }, + "mentionRule": { + "type": "object", + "properties": {}, + "description": "Allow replies from actors mentioned in your post." + }, + "followerRule": { + "type": "object", + "properties": {}, + "description": "Allow replies from actors who follow you." + }, + "followingRule": { + "type": "object", + "properties": {}, + "description": "Allow replies from actors you follow." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/graph/block.json b/lexicons/app/bsky/graph/block.json new file mode 100644 index 0000000000..bc0cd214df --- /dev/null +++ b/lexicons/app/bsky/graph/block.json @@ -0,0 +1,30 @@ +{ + "id": "app.bsky.graph.block", + "defs": { + "main": { + "key": "tid", + "type": "record", + "record": { + "type": "object", + "required": [ + "subject", + "createdAt" + ], + "properties": { + "subject": { + "type": "string", + "format": "did", + "description": "DID of the account to be blocked." + }, + "createdAt": { + "type": "string", + "format": "datetime" + } + } + }, + "description": "Record declaring a 'block' relationship against another account. NOTE: blocks are public in Bluesky; see blog posts for details." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/graph/defs.json b/lexicons/app/bsky/graph/defs.json new file mode 100644 index 0000000000..eca42fc875 --- /dev/null +++ b/lexicons/app/bsky/graph/defs.json @@ -0,0 +1,353 @@ +{ + "id": "app.bsky.graph.defs", + "defs": { + "modlist": { + "type": "token", + "description": "A list of actors to apply an aggregate moderation action (mute/block) on." + }, + "listView": { + "type": "object", + "required": [ + "uri", + "cid", + "creator", + "name", + "purpose", + "indexedAt" + ], + "properties": { + "cid": { + "type": "string", + "format": "cid" + }, + "uri": { + "type": "string", + "format": "at-uri" + }, + "name": { + "type": "string", + "maxLength": 64, + "minLength": 1 + }, + "avatar": { + "type": "string", + "format": "uri" + }, + "labels": { + "type": "array", + "items": { + "ref": "com.atproto.label.defs#label", + "type": "ref" + } + }, + "viewer": { + "ref": "#listViewerState", + "type": "ref" + }, + "creator": { + "ref": "app.bsky.actor.defs#profileView", + "type": "ref" + }, + "purpose": { + "ref": "#listPurpose", + "type": "ref" + }, + "indexedAt": { + "type": "string", + "format": "datetime" + }, + "description": { + "type": "string", + "maxLength": 3000, + "maxGraphemes": 300 + }, + "listItemCount": { + "type": "integer", + "minimum": 0 + }, + "descriptionFacets": { + "type": "array", + "items": { + "ref": "app.bsky.richtext.facet", + "type": "ref" + } + } + } + }, + "curatelist": { + "type": "token", + "description": "A list of actors used for curation purposes such as list feeds or interaction gating." + }, + "listPurpose": { + "type": "string", + "knownValues": [ + "app.bsky.graph.defs#modlist", + "app.bsky.graph.defs#curatelist", + "app.bsky.graph.defs#referencelist" + ] + }, + "listItemView": { + "type": "object", + "required": [ + "uri", + "subject" + ], + "properties": { + "uri": { + "type": "string", + "format": "at-uri" + }, + "subject": { + "ref": "app.bsky.actor.defs#profileView", + "type": "ref" + } + } + }, + "relationship": { + "type": "object", + "required": [ + "did" + ], + "properties": { + "did": { + "type": "string", + "format": "did" + }, + "blocking": { + "type": "string", + "format": "at-uri", + "description": "if the actor blocks this DID, this is the AT-URI of the block record" + }, + "blockedBy": { + "type": "string", + "format": "at-uri", + "description": "if the actor is blocked by this DID, contains the AT-URI of the block record" + }, + "following": { + "type": "string", + "format": "at-uri", + "description": "if the actor follows this DID, this is the AT-URI of the follow record" + }, + "followedBy": { + "type": "string", + "format": "at-uri", + "description": "if the actor is followed by this DID, contains the AT-URI of the follow record" + }, + "blockedByList": { + "type": "string", + "format": "at-uri", + "description": "if the actor is blocked by this DID via a block list, contains the AT-URI of the listblock record" + }, + "blockingByList": { + "type": "string", + "format": "at-uri", + "description": "if the actor blocks this DID via a block list, this is the AT-URI of the listblock record" + } + }, + "description": "lists the bi-directional graph relationships between one actor (not indicated in the object), and the target actors (the DID included in the object)" + }, + "listViewBasic": { + "type": "object", + "required": [ + "uri", + "cid", + "name", + "purpose" + ], + "properties": { + "cid": { + "type": "string", + "format": "cid" + }, + "uri": { + "type": "string", + "format": "at-uri" + }, + "name": { + "type": "string", + "maxLength": 64, + "minLength": 1 + }, + "avatar": { + "type": "string", + "format": "uri" + }, + "labels": { + "type": "array", + "items": { + "ref": "com.atproto.label.defs#label", + "type": "ref" + } + }, + "viewer": { + "ref": "#listViewerState", + "type": "ref" + }, + "purpose": { + "ref": "#listPurpose", + "type": "ref" + }, + "indexedAt": { + "type": "string", + "format": "datetime" + }, + "listItemCount": { + "type": "integer", + "minimum": 0 + } + } + }, + "notFoundActor": { + "type": "object", + "required": [ + "actor", + "notFound" + ], + "properties": { + "actor": { + "type": "string", + "format": "at-identifier" + }, + "notFound": { + "type": "boolean", + "const": true + } + }, + "description": "indicates that a handle or DID could not be resolved" + }, + "referencelist": { + "type": "token", + "description": "A list of actors used for only for reference purposes such as within a starter pack." + }, + "listViewerState": { + "type": "object", + "properties": { + "muted": { + "type": "boolean" + }, + "blocked": { + "type": "string", + "format": "at-uri" + } + } + }, + "starterPackView": { + "type": "object", + "required": [ + "uri", + "cid", + "record", + "creator", + "indexedAt" + ], + "properties": { + "cid": { + "type": "string", + "format": "cid" + }, + "uri": { + "type": "string", + "format": "at-uri" + }, + "list": { + "ref": "#listViewBasic", + "type": "ref" + }, + "feeds": { + "type": "array", + "items": { + "ref": "app.bsky.feed.defs#generatorView", + "type": "ref" + }, + "maxLength": 3 + }, + "labels": { + "type": "array", + "items": { + "ref": "com.atproto.label.defs#label", + "type": "ref" + } + }, + "record": { + "type": "unknown" + }, + "creator": { + "ref": "app.bsky.actor.defs#profileViewBasic", + "type": "ref" + }, + "indexedAt": { + "type": "string", + "format": "datetime" + }, + "joinedWeekCount": { + "type": "integer", + "minimum": 0 + }, + "listItemsSample": { + "type": "array", + "items": { + "ref": "#listItemView", + "type": "ref" + }, + "maxLength": 12 + }, + "joinedAllTimeCount": { + "type": "integer", + "minimum": 0 + } + } + }, + "starterPackViewBasic": { + "type": "object", + "required": [ + "uri", + "cid", + "record", + "creator", + "indexedAt" + ], + "properties": { + "cid": { + "type": "string", + "format": "cid" + }, + "uri": { + "type": "string", + "format": "at-uri" + }, + "labels": { + "type": "array", + "items": { + "ref": "com.atproto.label.defs#label", + "type": "ref" + } + }, + "record": { + "type": "unknown" + }, + "creator": { + "ref": "app.bsky.actor.defs#profileViewBasic", + "type": "ref" + }, + "indexedAt": { + "type": "string", + "format": "datetime" + }, + "listItemCount": { + "type": "integer", + "minimum": 0 + }, + "joinedWeekCount": { + "type": "integer", + "minimum": 0 + }, + "joinedAllTimeCount": { + "type": "integer", + "minimum": 0 + } + } + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/graph/follow.json b/lexicons/app/bsky/graph/follow.json new file mode 100644 index 0000000000..1918ba075d --- /dev/null +++ b/lexicons/app/bsky/graph/follow.json @@ -0,0 +1,33 @@ +{ + "id": "app.bsky.graph.follow", + "defs": { + "main": { + "key": "tid", + "type": "record", + "record": { + "type": "object", + "required": [ + "subject", + "createdAt" + ], + "properties": { + "via": { + "ref": "com.atproto.repo.strongRef", + "type": "ref" + }, + "subject": { + "type": "string", + "format": "did" + }, + "createdAt": { + "type": "string", + "format": "datetime" + } + } + }, + "description": "Record declaring a social 'follow' relationship of another account. Duplicate follows will be ignored by the AppView." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/graph/getActorStarterPacks.json b/lexicons/app/bsky/graph/getActorStarterPacks.json new file mode 100644 index 0000000000..8cccf2b909 --- /dev/null +++ b/lexicons/app/bsky/graph/getActorStarterPacks.json @@ -0,0 +1,53 @@ +{ + "id": "app.bsky.graph.getActorStarterPacks", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "starterPacks" + ], + "properties": { + "cursor": { + "type": "string" + }, + "starterPacks": { + "type": "array", + "items": { + "ref": "app.bsky.graph.defs#starterPackViewBasic", + "type": "ref" + } + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "actor" + ], + "properties": { + "actor": { + "type": "string", + "format": "at-identifier" + }, + "limit": { + "type": "integer", + "default": 50, + "maximum": 100, + "minimum": 1 + }, + "cursor": { + "type": "string" + } + } + }, + "description": "Get a list of starter packs created by the actor." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/graph/getBlocks.json b/lexicons/app/bsky/graph/getBlocks.json new file mode 100644 index 0000000000..35e2b289fa --- /dev/null +++ b/lexicons/app/bsky/graph/getBlocks.json @@ -0,0 +1,46 @@ +{ + "id": "app.bsky.graph.getBlocks", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "blocks" + ], + "properties": { + "blocks": { + "type": "array", + "items": { + "ref": "app.bsky.actor.defs#profileView", + "type": "ref" + } + }, + "cursor": { + "type": "string" + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "properties": { + "limit": { + "type": "integer", + "default": 50, + "maximum": 100, + "minimum": 1 + }, + "cursor": { + "type": "string" + } + } + }, + "description": "Enumerates which accounts the requesting account is currently blocking. Requires auth." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/graph/getFollowers.json b/lexicons/app/bsky/graph/getFollowers.json new file mode 100644 index 0000000000..80374a1331 --- /dev/null +++ b/lexicons/app/bsky/graph/getFollowers.json @@ -0,0 +1,66 @@ +{ + "id": "app.bsky.graph.getFollowers", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "subject", + "followers" + ], + "properties": { + "cursor": { + "type": "string" + }, + "subject": { + "ref": "app.bsky.actor.defs#profileView", + "type": "ref" + }, + "followers": { + "type": "array", + "items": { + "ref": "app.bsky.actor.defs#profileView", + "type": "ref" + } + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "actor" + ], + "properties": { + "sort": { + "type": "string", + "description": "Sort order of the returned followers.", + "knownValues": [ + "latest", + "top" + ] + }, + "actor": { + "type": "string", + "format": "at-identifier" + }, + "limit": { + "type": "integer", + "default": 50, + "maximum": 100, + "minimum": 1 + }, + "cursor": { + "type": "string" + } + } + }, + "description": "Enumerates accounts which follow a specified account (actor)." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/graph/getFollows.json b/lexicons/app/bsky/graph/getFollows.json new file mode 100644 index 0000000000..eab30c336c --- /dev/null +++ b/lexicons/app/bsky/graph/getFollows.json @@ -0,0 +1,66 @@ +{ + "id": "app.bsky.graph.getFollows", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "subject", + "follows" + ], + "properties": { + "cursor": { + "type": "string" + }, + "follows": { + "type": "array", + "items": { + "ref": "app.bsky.actor.defs#profileView", + "type": "ref" + } + }, + "subject": { + "ref": "app.bsky.actor.defs#profileView", + "type": "ref" + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "actor" + ], + "properties": { + "sort": { + "type": "string", + "description": "Sort order of the returned follows.", + "knownValues": [ + "latest", + "top" + ] + }, + "actor": { + "type": "string", + "format": "at-identifier" + }, + "limit": { + "type": "integer", + "default": 50, + "maximum": 100, + "minimum": 1 + }, + "cursor": { + "type": "string" + } + } + }, + "description": "Enumerates accounts which a specified account (actor) follows." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/graph/getKnownFollowers.json b/lexicons/app/bsky/graph/getKnownFollowers.json new file mode 100644 index 0000000000..2c705fc3c1 --- /dev/null +++ b/lexicons/app/bsky/graph/getKnownFollowers.json @@ -0,0 +1,58 @@ +{ + "id": "app.bsky.graph.getKnownFollowers", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "subject", + "followers" + ], + "properties": { + "cursor": { + "type": "string" + }, + "subject": { + "ref": "app.bsky.actor.defs#profileView", + "type": "ref" + }, + "followers": { + "type": "array", + "items": { + "ref": "app.bsky.actor.defs#profileView", + "type": "ref" + } + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "actor" + ], + "properties": { + "actor": { + "type": "string", + "format": "at-identifier" + }, + "limit": { + "type": "integer", + "default": 50, + "maximum": 100, + "minimum": 1 + }, + "cursor": { + "type": "string" + } + } + }, + "description": "Enumerates accounts which follow a specified account (actor) and are followed by the viewer." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/graph/getList.json b/lexicons/app/bsky/graph/getList.json new file mode 100644 index 0000000000..b5a6e226ae --- /dev/null +++ b/lexicons/app/bsky/graph/getList.json @@ -0,0 +1,59 @@ +{ + "id": "app.bsky.graph.getList", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "list", + "items" + ], + "properties": { + "list": { + "ref": "app.bsky.graph.defs#listView", + "type": "ref" + }, + "items": { + "type": "array", + "items": { + "ref": "app.bsky.graph.defs#listItemView", + "type": "ref" + } + }, + "cursor": { + "type": "string" + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "list" + ], + "properties": { + "list": { + "type": "string", + "format": "at-uri", + "description": "Reference (AT-URI) of the list record to hydrate." + }, + "limit": { + "type": "integer", + "default": 50, + "maximum": 100, + "minimum": 1 + }, + "cursor": { + "type": "string" + } + } + }, + "description": "Gets a 'view' (with additional context) of a specified list." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/graph/getListBlocks.json b/lexicons/app/bsky/graph/getListBlocks.json new file mode 100644 index 0000000000..03477a13b6 --- /dev/null +++ b/lexicons/app/bsky/graph/getListBlocks.json @@ -0,0 +1,46 @@ +{ + "id": "app.bsky.graph.getListBlocks", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "lists" + ], + "properties": { + "lists": { + "type": "array", + "items": { + "ref": "app.bsky.graph.defs#listView", + "type": "ref" + } + }, + "cursor": { + "type": "string" + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "properties": { + "limit": { + "type": "integer", + "default": 50, + "maximum": 100, + "minimum": 1 + }, + "cursor": { + "type": "string" + } + } + }, + "description": "Get mod lists that the requesting account (actor) is blocking. Requires auth." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/graph/getListMutes.json b/lexicons/app/bsky/graph/getListMutes.json new file mode 100644 index 0000000000..ea54aab6d2 --- /dev/null +++ b/lexicons/app/bsky/graph/getListMutes.json @@ -0,0 +1,46 @@ +{ + "id": "app.bsky.graph.getListMutes", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "lists" + ], + "properties": { + "lists": { + "type": "array", + "items": { + "ref": "app.bsky.graph.defs#listView", + "type": "ref" + } + }, + "cursor": { + "type": "string" + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "properties": { + "limit": { + "type": "integer", + "default": 50, + "maximum": 100, + "minimum": 1 + }, + "cursor": { + "type": "string" + } + } + }, + "description": "Enumerates mod lists that the requesting account (actor) currently has muted. Requires auth." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/graph/getLists.json b/lexicons/app/bsky/graph/getLists.json new file mode 100644 index 0000000000..03c960da01 --- /dev/null +++ b/lexicons/app/bsky/graph/getLists.json @@ -0,0 +1,65 @@ +{ + "id": "app.bsky.graph.getLists", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "lists" + ], + "properties": { + "lists": { + "type": "array", + "items": { + "ref": "app.bsky.graph.defs#listView", + "type": "ref" + } + }, + "cursor": { + "type": "string" + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "actor" + ], + "properties": { + "actor": { + "type": "string", + "format": "at-identifier", + "description": "The account (actor) to enumerate lists from." + }, + "limit": { + "type": "integer", + "default": 50, + "maximum": 100, + "minimum": 1 + }, + "cursor": { + "type": "string" + }, + "purposes": { + "type": "array", + "items": { + "type": "string", + "knownValues": [ + "modlist", + "curatelist" + ] + }, + "description": "Optional filter by list purpose. If not specified, all supported types are returned." + } + } + }, + "description": "Enumerates the lists created by a specified account (actor)." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/graph/getListsWithMembership.json b/lexicons/app/bsky/graph/getListsWithMembership.json new file mode 100644 index 0000000000..7e0b34a666 --- /dev/null +++ b/lexicons/app/bsky/graph/getListsWithMembership.json @@ -0,0 +1,82 @@ +{ + "id": "app.bsky.graph.getListsWithMembership", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "listsWithMembership" + ], + "properties": { + "cursor": { + "type": "string" + }, + "listsWithMembership": { + "type": "array", + "items": { + "ref": "#listWithMembership", + "type": "ref" + } + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "actor" + ], + "properties": { + "actor": { + "type": "string", + "format": "at-identifier", + "description": "The account (actor) to check for membership." + }, + "limit": { + "type": "integer", + "default": 50, + "maximum": 100, + "minimum": 1 + }, + "cursor": { + "type": "string" + }, + "purposes": { + "type": "array", + "items": { + "type": "string", + "knownValues": [ + "modlist", + "curatelist" + ] + }, + "description": "Optional filter by list purpose. If not specified, all supported types are returned." + } + } + }, + "description": "Enumerates the lists created by the session user, and includes membership information about `actor` in those lists. Only supports curation and moderation lists (no reference lists, used in starter packs). Requires auth." + }, + "listWithMembership": { + "type": "object", + "required": [ + "list" + ], + "properties": { + "list": { + "ref": "app.bsky.graph.defs#listView", + "type": "ref" + }, + "listItem": { + "ref": "app.bsky.graph.defs#listItemView", + "type": "ref" + } + }, + "description": "A list and an optional list item indicating membership of a target user to that list." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/graph/getMutes.json b/lexicons/app/bsky/graph/getMutes.json new file mode 100644 index 0000000000..ad6303cd0d --- /dev/null +++ b/lexicons/app/bsky/graph/getMutes.json @@ -0,0 +1,46 @@ +{ + "id": "app.bsky.graph.getMutes", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "mutes" + ], + "properties": { + "mutes": { + "type": "array", + "items": { + "ref": "app.bsky.actor.defs#profileView", + "type": "ref" + } + }, + "cursor": { + "type": "string" + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "properties": { + "limit": { + "type": "integer", + "default": 50, + "maximum": 100, + "minimum": 1 + }, + "cursor": { + "type": "string" + } + } + }, + "description": "Enumerates accounts that the requesting account (actor) currently has fully muted. Mutes scoped to specific kinds of content (only reposts, only quote posts) are not included. Responses may contain more items than the requested limit. Requires auth." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/graph/getRelationships.json b/lexicons/app/bsky/graph/getRelationships.json new file mode 100644 index 0000000000..84cc69fe59 --- /dev/null +++ b/lexicons/app/bsky/graph/getRelationships.json @@ -0,0 +1,64 @@ +{ + "id": "app.bsky.graph.getRelationships", + "defs": { + "main": { + "type": "query", + "errors": [ + { + "name": "ActorNotFound", + "description": "the primary actor at-identifier could not be resolved" + } + ], + "output": { + "schema": { + "type": "object", + "required": [ + "relationships" + ], + "properties": { + "actor": { + "type": "string", + "format": "did" + }, + "relationships": { + "type": "array", + "items": { + "refs": [ + "app.bsky.graph.defs#relationship", + "app.bsky.graph.defs#notFoundActor" + ], + "type": "union" + } + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "actor" + ], + "properties": { + "actor": { + "type": "string", + "format": "at-identifier", + "description": "Primary account requesting relationships for." + }, + "others": { + "type": "array", + "items": { + "type": "string", + "format": "at-identifier" + }, + "maxLength": 30, + "description": "List of 'other' accounts to be related back to the primary." + } + } + }, + "description": "Enumerates public relationships between one account, and a list of other accounts. Does not require auth." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/graph/getStarterPack.json b/lexicons/app/bsky/graph/getStarterPack.json new file mode 100644 index 0000000000..aa8ef6ce2a --- /dev/null +++ b/lexicons/app/bsky/graph/getStarterPack.json @@ -0,0 +1,39 @@ +{ + "id": "app.bsky.graph.getStarterPack", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "starterPack" + ], + "properties": { + "starterPack": { + "ref": "app.bsky.graph.defs#starterPackView", + "type": "ref" + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "starterPack" + ], + "properties": { + "starterPack": { + "type": "string", + "format": "at-uri", + "description": "Reference (AT-URI) of the starter pack record." + } + } + }, + "description": "Gets a view of a starter pack." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/graph/getStarterPacks.json b/lexicons/app/bsky/graph/getStarterPacks.json new file mode 100644 index 0000000000..15d73a1e55 --- /dev/null +++ b/lexicons/app/bsky/graph/getStarterPacks.json @@ -0,0 +1,45 @@ +{ + "id": "app.bsky.graph.getStarterPacks", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "starterPacks" + ], + "properties": { + "starterPacks": { + "type": "array", + "items": { + "ref": "app.bsky.graph.defs#starterPackViewBasic", + "type": "ref" + } + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "uris" + ], + "properties": { + "uris": { + "type": "array", + "items": { + "type": "string", + "format": "at-uri" + }, + "maxLength": 25 + } + } + }, + "description": "Get views for a list of starter packs." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/graph/getStarterPacksWithMembership.json b/lexicons/app/bsky/graph/getStarterPacksWithMembership.json new file mode 100644 index 0000000000..c57d254ce3 --- /dev/null +++ b/lexicons/app/bsky/graph/getStarterPacksWithMembership.json @@ -0,0 +1,71 @@ +{ + "id": "app.bsky.graph.getStarterPacksWithMembership", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "starterPacksWithMembership" + ], + "properties": { + "cursor": { + "type": "string" + }, + "starterPacksWithMembership": { + "type": "array", + "items": { + "ref": "#starterPackWithMembership", + "type": "ref" + } + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "actor" + ], + "properties": { + "actor": { + "type": "string", + "format": "at-identifier", + "description": "The account (actor) to check for membership." + }, + "limit": { + "type": "integer", + "default": 50, + "maximum": 100, + "minimum": 1 + }, + "cursor": { + "type": "string" + } + } + }, + "description": "Enumerates the starter packs created by the session user, and includes membership information about `actor` in those starter packs. Requires auth." + }, + "starterPackWithMembership": { + "type": "object", + "required": [ + "starterPack" + ], + "properties": { + "listItem": { + "ref": "app.bsky.graph.defs#listItemView", + "type": "ref" + }, + "starterPack": { + "ref": "app.bsky.graph.defs#starterPackView", + "type": "ref" + } + }, + "description": "A starter pack and an optional list item indicating membership of a target user to that starter pack." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/graph/getSuggestedFollowsByActor.json b/lexicons/app/bsky/graph/getSuggestedFollowsByActor.json new file mode 100644 index 0000000000..12b617d0a4 --- /dev/null +++ b/lexicons/app/bsky/graph/getSuggestedFollowsByActor.json @@ -0,0 +1,54 @@ +{ + "id": "app.bsky.graph.getSuggestedFollowsByActor", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "suggestions" + ], + "properties": { + "recId": { + "type": "integer", + "description": "DEPRECATED: use recIdStr instead." + }, + "recIdStr": { + "type": "string", + "description": "Snowflake for this recommendation, use when submitting recommendation events." + }, + "isFallback": { + "type": "boolean", + "default": false, + "description": "DEPRECATED, unused. Previously: if true, response has fallen-back to generic results, and is not scoped using relativeToDid" + }, + "suggestions": { + "type": "array", + "items": { + "ref": "app.bsky.actor.defs#profileView", + "type": "ref" + } + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "actor" + ], + "properties": { + "actor": { + "type": "string", + "format": "at-identifier" + } + } + }, + "description": "Enumerates follows similar to a given account (actor). Expected use is to recommend additional accounts immediately after following one account." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/graph/list.json b/lexicons/app/bsky/graph/list.json new file mode 100644 index 0000000000..dd67d4e99b --- /dev/null +++ b/lexicons/app/bsky/graph/list.json @@ -0,0 +1,63 @@ +{ + "id": "app.bsky.graph.list", + "defs": { + "main": { + "key": "tid", + "type": "record", + "record": { + "type": "object", + "required": [ + "name", + "purpose", + "createdAt" + ], + "properties": { + "name": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "description": "Display name for list; can not be empty." + }, + "avatar": { + "type": "blob", + "accept": [ + "image/png", + "image/jpeg" + ], + "maxSize": 1000000 + }, + "labels": { + "refs": [ + "com.atproto.label.defs#selfLabels" + ], + "type": "union" + }, + "purpose": { + "ref": "app.bsky.graph.defs#listPurpose", + "type": "ref", + "description": "Defines the purpose of the list (aka, moderation-oriented or curration-oriented)" + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "description": { + "type": "string", + "maxLength": 3000, + "maxGraphemes": 300 + }, + "descriptionFacets": { + "type": "array", + "items": { + "ref": "app.bsky.richtext.facet", + "type": "ref" + } + } + } + }, + "description": "Record representing a list of accounts (actors). Scope includes both moderation-oriented lists and curration-oriented lists." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/graph/listblock.json b/lexicons/app/bsky/graph/listblock.json new file mode 100644 index 0000000000..cef4b85255 --- /dev/null +++ b/lexicons/app/bsky/graph/listblock.json @@ -0,0 +1,30 @@ +{ + "id": "app.bsky.graph.listblock", + "defs": { + "main": { + "key": "tid", + "type": "record", + "record": { + "type": "object", + "required": [ + "subject", + "createdAt" + ], + "properties": { + "subject": { + "type": "string", + "format": "at-uri", + "description": "Reference (AT-URI) to the mod list record." + }, + "createdAt": { + "type": "string", + "format": "datetime" + } + } + }, + "description": "Record representing a block relationship against an entire an entire list of accounts (actors)." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/graph/listitem.json b/lexicons/app/bsky/graph/listitem.json new file mode 100644 index 0000000000..edfb268b35 --- /dev/null +++ b/lexicons/app/bsky/graph/listitem.json @@ -0,0 +1,36 @@ +{ + "id": "app.bsky.graph.listitem", + "defs": { + "main": { + "key": "tid", + "type": "record", + "record": { + "type": "object", + "required": [ + "subject", + "list", + "createdAt" + ], + "properties": { + "list": { + "type": "string", + "format": "at-uri", + "description": "Reference (AT-URI) to the list record (app.bsky.graph.list)." + }, + "subject": { + "type": "string", + "format": "did", + "description": "The account which is included on the list." + }, + "createdAt": { + "type": "string", + "format": "datetime" + } + } + }, + "description": "Record representing an account's inclusion on a specific list. The AppView will ignore duplicate listitem records." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/graph/muteActor.json b/lexicons/app/bsky/graph/muteActor.json new file mode 100644 index 0000000000..3f5866cca0 --- /dev/null +++ b/lexicons/app/bsky/graph/muteActor.json @@ -0,0 +1,34 @@ +{ + "id": "app.bsky.graph.muteActor", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "actor" + ], + "properties": { + "actor": { + "type": "string", + "format": "at-identifier" + }, + "onlyReposts": { + "type": "boolean", + "description": "Restrict the mute to the account's reposts. When any 'only' scope is set, just the scoped content is muted; when none are set, the account is fully muted. Repeat calls replace the stored scope rather than adding to it." + }, + "onlyQuoteposts": { + "type": "boolean", + "description": "Restrict the mute to the account's quote posts. See onlyReposts." + } + } + }, + "encoding": "application/json" + }, + "description": "Creates a mute relationship for the specified account. If a mute already exists for the account, it is updated in place: the stored scope is replaced with the scope in this request. Mutes are private in Bluesky. Requires auth." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/graph/muteActorList.json b/lexicons/app/bsky/graph/muteActorList.json new file mode 100644 index 0000000000..7a2d060d03 --- /dev/null +++ b/lexicons/app/bsky/graph/muteActorList.json @@ -0,0 +1,26 @@ +{ + "id": "app.bsky.graph.muteActorList", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "list" + ], + "properties": { + "list": { + "type": "string", + "format": "at-uri" + } + } + }, + "encoding": "application/json" + }, + "description": "Creates a mute relationship for the specified list of accounts. Mutes are private in Bluesky. Requires auth." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/graph/muteThread.json b/lexicons/app/bsky/graph/muteThread.json new file mode 100644 index 0000000000..7cfb6890f2 --- /dev/null +++ b/lexicons/app/bsky/graph/muteThread.json @@ -0,0 +1,26 @@ +{ + "id": "app.bsky.graph.muteThread", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "root" + ], + "properties": { + "root": { + "type": "string", + "format": "at-uri" + } + } + }, + "encoding": "application/json" + }, + "description": "Mutes a thread preventing notifications from the thread and any of its children. Mutes are private in Bluesky. Requires auth." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/graph/searchStarterPacks.json b/lexicons/app/bsky/graph/searchStarterPacks.json new file mode 100644 index 0000000000..20ac032f3f --- /dev/null +++ b/lexicons/app/bsky/graph/searchStarterPacks.json @@ -0,0 +1,53 @@ +{ + "id": "app.bsky.graph.searchStarterPacks", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "starterPacks" + ], + "properties": { + "cursor": { + "type": "string" + }, + "starterPacks": { + "type": "array", + "items": { + "ref": "app.bsky.graph.defs#starterPackViewBasic", + "type": "ref" + } + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "q" + ], + "properties": { + "q": { + "type": "string", + "description": "Search query string. Syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended." + }, + "limit": { + "type": "integer", + "default": 25, + "maximum": 100, + "minimum": 1 + }, + "cursor": { + "type": "string" + } + } + }, + "description": "Find starter packs matching search criteria. Does not require auth." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/graph/searchStarterPacksV2.json b/lexicons/app/bsky/graph/searchStarterPacksV2.json new file mode 100644 index 0000000000..9d6949e0e2 --- /dev/null +++ b/lexicons/app/bsky/graph/searchStarterPacksV2.json @@ -0,0 +1,57 @@ +{ + "id": "app.bsky.graph.searchStarterPacksV2", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "starterPacks" + ], + "properties": { + "cursor": { + "type": "string" + }, + "hitsTotal": { + "type": "integer", + "description": "Estimated total number of matching hits. May be rounded or truncated." + }, + "starterPacks": { + "type": "array", + "items": { + "ref": "app.bsky.graph.defs#starterPackView", + "type": "ref" + } + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "q" + ], + "properties": { + "q": { + "type": "string", + "description": "Search query string. Syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended." + }, + "limit": { + "type": "integer", + "default": 25, + "maximum": 100, + "minimum": 1 + }, + "cursor": { + "type": "string" + } + } + }, + "description": "Find starter packs matching search criteria. Does not require auth." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/graph/starterpack.json b/lexicons/app/bsky/graph/starterpack.json new file mode 100644 index 0000000000..add9530335 --- /dev/null +++ b/lexicons/app/bsky/graph/starterpack.json @@ -0,0 +1,70 @@ +{ + "id": "app.bsky.graph.starterpack", + "defs": { + "main": { + "key": "tid", + "type": "record", + "record": { + "type": "object", + "required": [ + "name", + "list", + "createdAt" + ], + "properties": { + "list": { + "type": "string", + "format": "at-uri", + "description": "Reference (AT-URI) to the list record." + }, + "name": { + "type": "string", + "maxLength": 500, + "minLength": 1, + "description": "Display name for starter pack; can not be empty.", + "maxGraphemes": 50 + }, + "feeds": { + "type": "array", + "items": { + "ref": "#feedItem", + "type": "ref" + }, + "maxLength": 3 + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "description": { + "type": "string", + "maxLength": 3000, + "maxGraphemes": 300 + }, + "descriptionFacets": { + "type": "array", + "items": { + "ref": "app.bsky.richtext.facet", + "type": "ref" + } + } + } + }, + "description": "Record defining a starter pack of actors and feeds for new users." + }, + "feedItem": { + "type": "object", + "required": [ + "uri" + ], + "properties": { + "uri": { + "type": "string", + "format": "at-uri" + } + } + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/graph/unmuteActor.json b/lexicons/app/bsky/graph/unmuteActor.json new file mode 100644 index 0000000000..8b9ca6cdb2 --- /dev/null +++ b/lexicons/app/bsky/graph/unmuteActor.json @@ -0,0 +1,26 @@ +{ + "id": "app.bsky.graph.unmuteActor", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "actor" + ], + "properties": { + "actor": { + "type": "string", + "format": "at-identifier" + } + } + }, + "encoding": "application/json" + }, + "description": "Unmutes the specified account. Requires auth." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/graph/unmuteActorList.json b/lexicons/app/bsky/graph/unmuteActorList.json new file mode 100644 index 0000000000..b770634928 --- /dev/null +++ b/lexicons/app/bsky/graph/unmuteActorList.json @@ -0,0 +1,26 @@ +{ + "id": "app.bsky.graph.unmuteActorList", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "list" + ], + "properties": { + "list": { + "type": "string", + "format": "at-uri" + } + } + }, + "encoding": "application/json" + }, + "description": "Unmutes the specified list of accounts. Requires auth." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/graph/unmuteThread.json b/lexicons/app/bsky/graph/unmuteThread.json new file mode 100644 index 0000000000..5b86269582 --- /dev/null +++ b/lexicons/app/bsky/graph/unmuteThread.json @@ -0,0 +1,26 @@ +{ + "id": "app.bsky.graph.unmuteThread", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "root" + ], + "properties": { + "root": { + "type": "string", + "format": "at-uri" + } + } + }, + "encoding": "application/json" + }, + "description": "Unmutes the specified thread. Requires auth." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/graph/verification.json b/lexicons/app/bsky/graph/verification.json new file mode 100644 index 0000000000..6d55ebf909 --- /dev/null +++ b/lexicons/app/bsky/graph/verification.json @@ -0,0 +1,42 @@ +{ + "id": "app.bsky.graph.verification", + "defs": { + "main": { + "key": "tid", + "type": "record", + "record": { + "type": "object", + "required": [ + "subject", + "handle", + "displayName", + "createdAt" + ], + "properties": { + "handle": { + "type": "string", + "format": "handle", + "description": "Handle of the subject the verification applies to at the moment of verifying, which might not be the same at the time of viewing. The verification is only valid if the current handle matches the one at the time of verifying." + }, + "subject": { + "type": "string", + "format": "did", + "description": "DID of the subject the verification applies to." + }, + "createdAt": { + "type": "string", + "format": "datetime", + "description": "Date of when the verification was created." + }, + "displayName": { + "type": "string", + "description": "Display name of the subject the verification applies to at the moment of verifying, which might not be the same at the time of viewing. The verification is only valid if the current displayName matches the one at the time of verifying." + } + } + }, + "description": "Record declaring a verification relationship between two accounts. Verifications are only considered valid by an app if issued by an account the app considers trusted." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/labeler/defs.json b/lexicons/app/bsky/labeler/defs.json new file mode 100644 index 0000000000..20de6e051b --- /dev/null +++ b/lexicons/app/bsky/labeler/defs.json @@ -0,0 +1,153 @@ +{ + "id": "app.bsky.labeler.defs", + "defs": { + "labelerView": { + "type": "object", + "required": [ + "uri", + "cid", + "creator", + "indexedAt" + ], + "properties": { + "cid": { + "type": "string", + "format": "cid" + }, + "uri": { + "type": "string", + "format": "at-uri" + }, + "labels": { + "type": "array", + "items": { + "ref": "com.atproto.label.defs#label", + "type": "ref" + } + }, + "viewer": { + "ref": "#labelerViewerState", + "type": "ref" + }, + "creator": { + "ref": "app.bsky.actor.defs#profileView", + "type": "ref" + }, + "indexedAt": { + "type": "string", + "format": "datetime" + }, + "likeCount": { + "type": "integer", + "minimum": 0 + } + } + }, + "labelerPolicies": { + "type": "object", + "required": [ + "labelValues" + ], + "properties": { + "labelValues": { + "type": "array", + "items": { + "ref": "com.atproto.label.defs#labelValue", + "type": "ref" + }, + "description": "The label values which this labeler publishes. May include global or custom labels." + }, + "labelValueDefinitions": { + "type": "array", + "items": { + "ref": "com.atproto.label.defs#labelValueDefinition", + "type": "ref" + }, + "description": "Label values created by this labeler and scoped exclusively to it. Labels defined here will override global label definitions for this labeler." + } + } + }, + "labelerViewerState": { + "type": "object", + "properties": { + "like": { + "type": "string", + "format": "at-uri" + } + } + }, + "labelerViewDetailed": { + "type": "object", + "required": [ + "uri", + "cid", + "creator", + "policies", + "indexedAt" + ], + "properties": { + "cid": { + "type": "string", + "format": "cid" + }, + "uri": { + "type": "string", + "format": "at-uri" + }, + "labels": { + "type": "array", + "items": { + "ref": "com.atproto.label.defs#label", + "type": "ref" + } + }, + "viewer": { + "ref": "#labelerViewerState", + "type": "ref" + }, + "creator": { + "ref": "app.bsky.actor.defs#profileView", + "type": "ref" + }, + "policies": { + "ref": "app.bsky.labeler.defs#labelerPolicies", + "type": "ref" + }, + "indexedAt": { + "type": "string", + "format": "datetime" + }, + "likeCount": { + "type": "integer", + "minimum": 0 + }, + "reasonTypes": { + "type": "array", + "items": { + "ref": "com.atproto.moderation.defs#reasonType", + "type": "ref" + }, + "description": "The set of report reason 'codes' which are in-scope for this service to review and action. These usually align to policy categories. If not defined (distinct from empty array), all reason types are allowed." + }, + "subjectTypes": { + "type": "array", + "items": { + "ref": "com.atproto.moderation.defs#subjectType", + "type": "ref" + }, + "description": "The set of subject types (account, record, etc) this service accepts reports on." + }, + "subjectCollections": { + "type": "array", + "items": { + "type": "string", + "format": "nsid" + }, + "description": "Set of record types (collection NSIDs) which can be reported to this service. If not defined (distinct from empty array), default is any record type." + } + } + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/labeler/getServices.json b/lexicons/app/bsky/labeler/getServices.json new file mode 100644 index 0000000000..84a8856569 --- /dev/null +++ b/lexicons/app/bsky/labeler/getServices.json @@ -0,0 +1,51 @@ +{ + "id": "app.bsky.labeler.getServices", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "views" + ], + "properties": { + "views": { + "type": "array", + "items": { + "refs": [ + "app.bsky.labeler.defs#labelerView", + "app.bsky.labeler.defs#labelerViewDetailed" + ], + "type": "union" + } + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "dids" + ], + "properties": { + "dids": { + "type": "array", + "items": { + "type": "string", + "format": "did" + } + }, + "detailed": { + "type": "boolean", + "default": false + } + } + }, + "description": "Get information about a list of labeler services." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/labeler/service.json b/lexicons/app/bsky/labeler/service.json new file mode 100644 index 0000000000..65f20e1fee --- /dev/null +++ b/lexicons/app/bsky/labeler/service.json @@ -0,0 +1,59 @@ +{ + "id": "app.bsky.labeler.service", + "defs": { + "main": { + "key": "literal:self", + "type": "record", + "record": { + "type": "object", + "required": [ + "policies", + "createdAt" + ], + "properties": { + "labels": { + "refs": [ + "com.atproto.label.defs#selfLabels" + ], + "type": "union" + }, + "policies": { + "ref": "app.bsky.labeler.defs#labelerPolicies", + "type": "ref" + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "reasonTypes": { + "type": "array", + "items": { + "ref": "com.atproto.moderation.defs#reasonType", + "type": "ref" + }, + "description": "The set of report reason 'codes' which are in-scope for this service to review and action. These usually align to policy categories. If not defined (distinct from empty array), all reason types are allowed." + }, + "subjectTypes": { + "type": "array", + "items": { + "ref": "com.atproto.moderation.defs#subjectType", + "type": "ref" + }, + "description": "The set of subject types (account, record, etc) this service accepts reports on." + }, + "subjectCollections": { + "type": "array", + "items": { + "type": "string", + "format": "nsid" + }, + "description": "Set of record types (collection NSIDs) which can be reported to this service. If not defined (distinct from empty array), default is any record type." + } + } + }, + "description": "A declaration of the existence of labeler service." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/notification/declaration.json b/lexicons/app/bsky/notification/declaration.json new file mode 100644 index 0000000000..185af94404 --- /dev/null +++ b/lexicons/app/bsky/notification/declaration.json @@ -0,0 +1,29 @@ +{ + "id": "app.bsky.notification.declaration", + "defs": { + "main": { + "key": "literal:self", + "type": "record", + "record": { + "type": "object", + "required": [ + "allowSubscriptions" + ], + "properties": { + "allowSubscriptions": { + "type": "string", + "description": "A declaration of the user's preference for allowing activity subscriptions from other users. Absence of a record implies 'followers'.", + "knownValues": [ + "followers", + "mutuals", + "none" + ] + } + } + }, + "description": "A declaration of the user's choices related to notifications that can be produced by them." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/notification/defs.json b/lexicons/app/bsky/notification/defs.json new file mode 100644 index 0000000000..3e767ca298 --- /dev/null +++ b/lexicons/app/bsky/notification/defs.json @@ -0,0 +1,175 @@ +{ + "id": "app.bsky.notification.defs", + "defs": { + "preference": { + "type": "object", + "required": [ + "list", + "push" + ], + "properties": { + "list": { + "type": "boolean" + }, + "push": { + "type": "boolean" + } + } + }, + "preferences": { + "type": "object", + "required": [ + "chat", + "follow", + "like", + "likeViaRepost", + "mention", + "quote", + "reply", + "repost", + "repostViaRepost", + "starterpackJoined", + "subscribedPost", + "unverified", + "verified" + ], + "properties": { + "chat": { + "ref": "#chatPreference", + "type": "ref", + "description": "Deprecated: use chat.bsky.notification preferences instead. This will only return a default value." + }, + "like": { + "ref": "#filterablePreference", + "type": "ref" + }, + "quote": { + "ref": "#filterablePreference", + "type": "ref" + }, + "reply": { + "ref": "#filterablePreference", + "type": "ref" + }, + "follow": { + "ref": "#filterablePreference", + "type": "ref" + }, + "repost": { + "ref": "#filterablePreference", + "type": "ref" + }, + "mention": { + "ref": "#filterablePreference", + "type": "ref" + }, + "verified": { + "ref": "#preference", + "type": "ref" + }, + "unverified": { + "ref": "#preference", + "type": "ref" + }, + "likeViaRepost": { + "ref": "#filterablePreference", + "type": "ref" + }, + "subscribedPost": { + "ref": "#preference", + "type": "ref" + }, + "repostViaRepost": { + "ref": "#filterablePreference", + "type": "ref" + }, + "starterpackJoined": { + "ref": "#preference", + "type": "ref" + } + } + }, + "recordDeleted": { + "type": "object", + "properties": {} + }, + "chatPreference": { + "type": "object", + "required": [ + "include", + "push" + ], + "properties": { + "push": { + "type": "boolean" + }, + "include": { + "type": "string", + "knownValues": [ + "all", + "accepted" + ] + } + }, + "description": "Deprecated: use chat.bsky.notification preferences instead. This will only return a default value." + }, + "activitySubscription": { + "type": "object", + "required": [ + "post", + "reply" + ], + "properties": { + "post": { + "type": "boolean" + }, + "reply": { + "type": "boolean" + } + } + }, + "filterablePreference": { + "type": "object", + "required": [ + "include", + "list", + "push" + ], + "properties": { + "list": { + "type": "boolean" + }, + "push": { + "type": "boolean" + }, + "include": { + "type": "string", + "knownValues": [ + "all", + "follows" + ] + } + } + }, + "subjectActivitySubscription": { + "type": "object", + "required": [ + "subject", + "activitySubscription" + ], + "properties": { + "subject": { + "type": "string", + "format": "did" + }, + "activitySubscription": { + "ref": "#activitySubscription", + "type": "ref" + } + }, + "description": "Object used to store activity subscription data in stash." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/notification/getPreferences.json b/lexicons/app/bsky/notification/getPreferences.json new file mode 100644 index 0000000000..54482fb455 --- /dev/null +++ b/lexicons/app/bsky/notification/getPreferences.json @@ -0,0 +1,30 @@ +{ + "id": "app.bsky.notification.getPreferences", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "preferences" + ], + "properties": { + "preferences": { + "ref": "app.bsky.notification.defs#preferences", + "type": "ref" + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "properties": {} + }, + "description": "Get notification-related preferences for an account. Requires auth." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/notification/getUnreadCount.json b/lexicons/app/bsky/notification/getUnreadCount.json new file mode 100644 index 0000000000..87a37810d9 --- /dev/null +++ b/lexicons/app/bsky/notification/getUnreadCount.json @@ -0,0 +1,37 @@ +{ + "id": "app.bsky.notification.getUnreadCount", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "count" + ], + "properties": { + "count": { + "type": "integer" + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "properties": { + "seenAt": { + "type": "string", + "format": "datetime" + }, + "priority": { + "type": "boolean" + } + } + }, + "description": "Count the number of unread notifications for the requesting account. Requires auth." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/notification/listActivitySubscriptions.json b/lexicons/app/bsky/notification/listActivitySubscriptions.json new file mode 100644 index 0000000000..350b822ac9 --- /dev/null +++ b/lexicons/app/bsky/notification/listActivitySubscriptions.json @@ -0,0 +1,46 @@ +{ + "id": "app.bsky.notification.listActivitySubscriptions", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "subscriptions" + ], + "properties": { + "cursor": { + "type": "string" + }, + "subscriptions": { + "type": "array", + "items": { + "ref": "app.bsky.actor.defs#profileView", + "type": "ref" + } + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "properties": { + "limit": { + "type": "integer", + "default": 50, + "maximum": 100, + "minimum": 1 + }, + "cursor": { + "type": "string" + } + } + }, + "description": "Enumerate all accounts to which the requesting account is subscribed to receive notifications for. Requires auth." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/notification/listNotifications.json b/lexicons/app/bsky/notification/listNotifications.json new file mode 100644 index 0000000000..2c2aa20207 --- /dev/null +++ b/lexicons/app/bsky/notification/listNotifications.json @@ -0,0 +1,139 @@ +{ + "id": "app.bsky.notification.listNotifications", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "notifications" + ], + "properties": { + "cursor": { + "type": "string" + }, + "seenAt": { + "type": "string", + "format": "datetime" + }, + "priority": { + "type": "boolean" + }, + "notifications": { + "type": "array", + "items": { + "ref": "#notification", + "type": "ref" + } + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "properties": { + "limit": { + "type": "integer", + "default": 50, + "maximum": 100, + "minimum": 1 + }, + "cursor": { + "type": "string" + }, + "seenAt": { + "type": "string", + "format": "datetime" + }, + "reasons": { + "type": "array", + "items": { + "type": "string", + "description": "A reason that matches the reason property of #notification." + }, + "description": "Notification reasons to include in response." + }, + "priority": { + "type": "boolean" + } + } + }, + "description": "Enumerate notifications for the requesting account. Requires auth." + }, + "notification": { + "type": "object", + "required": [ + "uri", + "cid", + "author", + "reason", + "record", + "isRead", + "indexedAt" + ], + "properties": { + "cid": { + "type": "string", + "format": "cid" + }, + "uri": { + "type": "string", + "format": "at-uri" + }, + "author": { + "ref": "app.bsky.actor.defs#profileView", + "type": "ref" + }, + "isRead": { + "type": "boolean" + }, + "labels": { + "type": "array", + "items": { + "ref": "com.atproto.label.defs#label", + "type": "ref" + } + }, + "reason": { + "type": "string", + "description": "The reason why this notification was delivered - e.g. your post was liked, or you received a new follower.", + "knownValues": [ + "like", + "repost", + "follow", + "mention", + "reply", + "quote", + "starterpack-joined", + "verified", + "unverified", + "like-via-repost", + "repost-via-repost", + "subscribed-post", + "contact-match" + ] + }, + "record": { + "type": "unknown" + }, + "indexedAt": { + "type": "string", + "format": "datetime" + }, + "starterPack": { + "ref": "app.bsky.graph.defs#starterPackViewBasic", + "type": "ref", + "description": "The starter pack associated with this notification. Present when the notification is for a follow originating from a starter pack." + }, + "reasonSubject": { + "type": "string", + "format": "at-uri" + } + } + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/notification/putActivitySubscription.json b/lexicons/app/bsky/notification/putActivitySubscription.json new file mode 100644 index 0000000000..65cc67b335 --- /dev/null +++ b/lexicons/app/bsky/notification/putActivitySubscription.json @@ -0,0 +1,50 @@ +{ + "id": "app.bsky.notification.putActivitySubscription", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "subject", + "activitySubscription" + ], + "properties": { + "subject": { + "type": "string", + "format": "did" + }, + "activitySubscription": { + "ref": "app.bsky.notification.defs#activitySubscription", + "type": "ref" + } + } + }, + "encoding": "application/json" + }, + "output": { + "schema": { + "type": "object", + "required": [ + "subject" + ], + "properties": { + "subject": { + "type": "string", + "format": "did" + }, + "activitySubscription": { + "ref": "app.bsky.notification.defs#activitySubscription", + "type": "ref" + } + } + }, + "encoding": "application/json" + }, + "description": "Puts an activity subscription entry. The key should be omitted for creation and provided for updates. Requires auth." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/notification/putPreferences.json b/lexicons/app/bsky/notification/putPreferences.json new file mode 100644 index 0000000000..2875de467b --- /dev/null +++ b/lexicons/app/bsky/notification/putPreferences.json @@ -0,0 +1,25 @@ +{ + "id": "app.bsky.notification.putPreferences", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "priority" + ], + "properties": { + "priority": { + "type": "boolean" + } + } + }, + "encoding": "application/json" + }, + "description": "Set notification-related preferences for an account. Requires auth." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/notification/putPreferencesV2.json b/lexicons/app/bsky/notification/putPreferencesV2.json new file mode 100644 index 0000000000..6a5d98fbda --- /dev/null +++ b/lexicons/app/bsky/notification/putPreferencesV2.json @@ -0,0 +1,87 @@ +{ + "id": "app.bsky.notification.putPreferencesV2", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "properties": { + "chat": { + "ref": "app.bsky.notification.defs#chatPreference", + "type": "ref", + "description": "Deprecated: use chat.bsky.notification preferences instead. Setting this won't stick and the default values will be returned." + }, + "like": { + "ref": "app.bsky.notification.defs#filterablePreference", + "type": "ref" + }, + "quote": { + "ref": "app.bsky.notification.defs#filterablePreference", + "type": "ref" + }, + "reply": { + "ref": "app.bsky.notification.defs#filterablePreference", + "type": "ref" + }, + "follow": { + "ref": "app.bsky.notification.defs#filterablePreference", + "type": "ref" + }, + "repost": { + "ref": "app.bsky.notification.defs#filterablePreference", + "type": "ref" + }, + "mention": { + "ref": "app.bsky.notification.defs#filterablePreference", + "type": "ref" + }, + "verified": { + "ref": "app.bsky.notification.defs#preference", + "type": "ref" + }, + "unverified": { + "ref": "app.bsky.notification.defs#preference", + "type": "ref" + }, + "likeViaRepost": { + "ref": "app.bsky.notification.defs#filterablePreference", + "type": "ref" + }, + "subscribedPost": { + "ref": "app.bsky.notification.defs#preference", + "type": "ref" + }, + "repostViaRepost": { + "ref": "app.bsky.notification.defs#filterablePreference", + "type": "ref" + }, + "starterpackJoined": { + "ref": "app.bsky.notification.defs#preference", + "type": "ref" + } + } + }, + "encoding": "application/json" + }, + "output": { + "schema": { + "type": "object", + "required": [ + "preferences" + ], + "properties": { + "preferences": { + "ref": "app.bsky.notification.defs#preferences", + "type": "ref" + } + } + }, + "encoding": "application/json" + }, + "description": "Set notification-related preferences for an account. Requires auth." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/notification/registerPush.json b/lexicons/app/bsky/notification/registerPush.json new file mode 100644 index 0000000000..db1fff3ab7 --- /dev/null +++ b/lexicons/app/bsky/notification/registerPush.json @@ -0,0 +1,47 @@ +{ + "id": "app.bsky.notification.registerPush", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "serviceDid", + "token", + "platform", + "appId" + ], + "properties": { + "appId": { + "type": "string" + }, + "token": { + "type": "string" + }, + "platform": { + "type": "string", + "knownValues": [ + "ios", + "android", + "web" + ] + }, + "serviceDid": { + "type": "string", + "format": "did" + }, + "ageRestricted": { + "type": "boolean", + "description": "Set to true when the actor is age restricted" + } + } + }, + "encoding": "application/json" + }, + "description": "Register to receive push notifications, via a specified service, for the requesting account. Requires auth." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/notification/unregisterPush.json b/lexicons/app/bsky/notification/unregisterPush.json new file mode 100644 index 0000000000..a73bd6afde --- /dev/null +++ b/lexicons/app/bsky/notification/unregisterPush.json @@ -0,0 +1,43 @@ +{ + "id": "app.bsky.notification.unregisterPush", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "serviceDid", + "token", + "platform", + "appId" + ], + "properties": { + "appId": { + "type": "string" + }, + "token": { + "type": "string" + }, + "platform": { + "type": "string", + "knownValues": [ + "ios", + "android", + "web" + ] + }, + "serviceDid": { + "type": "string", + "format": "did" + } + } + }, + "encoding": "application/json" + }, + "description": "The inverse of registerPush - inform a specified service that push notifications should no longer be sent to the given token for the requesting account. Requires auth." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/notification/updateSeen.json b/lexicons/app/bsky/notification/updateSeen.json new file mode 100644 index 0000000000..128b8611e3 --- /dev/null +++ b/lexicons/app/bsky/notification/updateSeen.json @@ -0,0 +1,26 @@ +{ + "id": "app.bsky.notification.updateSeen", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "seenAt" + ], + "properties": { + "seenAt": { + "type": "string", + "format": "datetime" + } + } + }, + "encoding": "application/json" + }, + "description": "Notify server that the requesting account has seen notifications. Requires auth." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/richtext/facet.json b/lexicons/app/bsky/richtext/facet.json new file mode 100644 index 0000000000..39b039b790 --- /dev/null +++ b/lexicons/app/bsky/richtext/facet.json @@ -0,0 +1,90 @@ +{ + "id": "app.bsky.richtext.facet", + "defs": { + "tag": { + "type": "object", + "required": [ + "tag" + ], + "properties": { + "tag": { + "type": "string", + "maxLength": 640, + "maxGraphemes": 64 + } + }, + "description": "Facet feature for a hashtag. The text usually includes a '#' prefix, but the facet reference should not (except in the case of 'double hash tags')." + }, + "link": { + "type": "object", + "required": [ + "uri" + ], + "properties": { + "uri": { + "type": "string", + "format": "uri" + } + }, + "description": "Facet feature for a URL. The text URL may have been simplified or truncated, but the facet reference should be a complete URL." + }, + "main": { + "type": "object", + "required": [ + "index", + "features" + ], + "properties": { + "index": { + "ref": "#byteSlice", + "type": "ref" + }, + "features": { + "type": "array", + "items": { + "refs": [ + "#mention", + "#link", + "#tag" + ], + "type": "union" + } + } + }, + "description": "Annotation of a sub-string within rich text." + }, + "mention": { + "type": "object", + "required": [ + "did" + ], + "properties": { + "did": { + "type": "string", + "format": "did" + } + }, + "description": "Facet feature for mention of another account. The text is usually a handle, including a '@' prefix, but the facet reference is a DID." + }, + "byteSlice": { + "type": "object", + "required": [ + "byteStart", + "byteEnd" + ], + "properties": { + "byteEnd": { + "type": "integer", + "minimum": 0 + }, + "byteStart": { + "type": "integer", + "minimum": 0 + } + }, + "description": "Specifies the sub-string range a facet feature applies to. Start index is inclusive, end index is exclusive. Indices are zero-indexed, counting bytes of the UTF-8 encoded text. NOTE: some languages, like Javascript, use UTF-16 or Unicode codepoints for string slice indexing; in these languages, convert to byte arrays before working with facets." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/unspecced/defs.json b/lexicons/app/bsky/unspecced/defs.json new file mode 100644 index 0000000000..609de81f8f --- /dev/null +++ b/lexicons/app/bsky/unspecced/defs.json @@ -0,0 +1,259 @@ +{ + "lexicon": 1, + "id": "app.bsky.unspecced.defs", + "defs": { + "skeletonSearchPost": { + "type": "object", + "required": ["uri"], + "properties": { + "uri": { + "type": "string", + "format": "at-uri" + } + } + }, + "skeletonSearchActor": { + "type": "object", + "required": ["did"], + "properties": { + "did": { + "type": "string", + "format": "did" + } + } + }, + "skeletonSearchStarterPack": { + "type": "object", + "required": ["uri"], + "properties": { + "uri": { + "type": "string", + "format": "at-uri" + } + } + }, + "trendingTopic": { + "type": "object", + "required": ["topic", "link"], + "properties": { + "topic": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "description": { + "type": "string" + }, + "link": { + "type": "string" + } + } + }, + "skeletonTrend": { + "type": "object", + "required": [ + "topic", + "displayName", + "link", + "startedAt", + "postCount", + "dids" + ], + "properties": { + "topic": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "description": { + "type": "string" + }, + "link": { + "type": "string" + }, + "startedAt": { + "type": "string", + "format": "datetime" + }, + "postCount": { + "type": "integer" + }, + "status": { + "type": "string", + "knownValues": ["hot"] + }, + "category": { + "type": "string" + }, + "dids": { + "type": "array", + "items": { + "type": "string", + "format": "did" + } + } + } + }, + "trendView": { + "type": "object", + "required": [ + "topic", + "displayName", + "link", + "startedAt", + "postCount", + "actors" + ], + "properties": { + "topic": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "description": { + "type": "string" + }, + "link": { + "type": "string" + }, + "startedAt": { + "type": "string", + "format": "datetime" + }, + "postCount": { + "type": "integer" + }, + "status": { + "type": "string", + "knownValues": ["hot"] + }, + "category": { + "type": "string" + }, + "actors": { + "type": "array", + "items": { + "type": "ref", + "ref": "app.bsky.actor.defs#profileViewBasic" + } + } + } + }, + "threadItemPost": { + "type": "object", + "required": [ + "post", + "moreParents", + "moreReplies", + "opThread", + "hiddenByThreadgate", + "mutedByViewer" + ], + "properties": { + "post": { + "type": "ref", + "ref": "app.bsky.feed.defs#postView" + }, + "moreParents": { + "type": "boolean", + "description": "This post has more parents that were not present in the response. This is just a boolean, without the number of parents." + }, + "moreReplies": { + "type": "integer", + "description": "This post has more replies that were not present in the response. This is a numeric value, which is best-effort and might not be accurate." + }, + "opThread": { + "type": "boolean", + "description": "This post is part of a contiguous thread by the OP from the thread root. Many different OP threads can happen in the same thread." + }, + "hiddenByThreadgate": { + "type": "boolean", + "description": "The threadgate created by the author indicates this post as a reply to be hidden for everyone consuming the thread." + }, + "mutedByViewer": { + "type": "boolean", + "description": "This is by an account muted by the viewer requesting it." + } + } + }, + "threadItemNoUnauthenticated": { + "type": "object", + "properties": {} + }, + "threadItemNotFound": { + "type": "object", + "properties": {} + }, + "threadItemBlocked": { + "type": "object", + "required": ["author"], + "properties": { + "author": { + "type": "ref", + "ref": "app.bsky.feed.defs#blockedAuthor" + } + } + }, + "ageAssuranceState": { + "type": "object", + "description": "The computed state of the age assurance process, returned to the user in question on certain authenticated requests.", + "required": ["status"], + "properties": { + "lastInitiatedAt": { + "type": "string", + "format": "datetime", + "description": "The timestamp when this state was last updated." + }, + "status": { + "type": "string", + "description": "The status of the age assurance process.", + "knownValues": ["unknown", "pending", "assured", "blocked"] + } + } + }, + "ageAssuranceEvent": { + "type": "object", + "description": "Object used to store age assurance data in stash.", + "required": ["createdAt", "status", "attemptId"], + "properties": { + "createdAt": { + "type": "string", + "format": "datetime", + "description": "The date and time of this write operation." + }, + "status": { + "type": "string", + "description": "The status of the age assurance process.", + "knownValues": ["unknown", "pending", "assured"] + }, + "attemptId": { + "type": "string", + "description": "The unique identifier for this instance of the age assurance flow, in UUID format." + }, + "email": { + "type": "string", + "description": "The email used for AA." + }, + "initIp": { + "type": "string", + "description": "The IP address used when initiating the AA flow." + }, + "initUa": { + "type": "string", + "description": "The user agent used when initiating the AA flow." + }, + "completeIp": { + "type": "string", + "description": "The IP address used when completing the AA flow." + }, + "completeUa": { + "type": "string", + "description": "The user agent used when completing the AA flow." + } + } + } + } +} diff --git a/lexicons/app/bsky/unspecced/getAgeAssuranceState.json b/lexicons/app/bsky/unspecced/getAgeAssuranceState.json new file mode 100644 index 0000000000..3bd42b3246 --- /dev/null +++ b/lexicons/app/bsky/unspecced/getAgeAssuranceState.json @@ -0,0 +1,17 @@ +{ + "lexicon": 1, + "id": "app.bsky.unspecced.getAgeAssuranceState", + "defs": { + "main": { + "type": "query", + "description": "Returns the current state of the age assurance process for an account. This is used to check if the user has completed age assurance or if further action is required.", + "output": { + "encoding": "application/json", + "schema": { + "type": "ref", + "ref": "app.bsky.unspecced.defs#ageAssuranceState" + } + } + } + } +} diff --git a/lexicons/app/bsky/unspecced/getConfig.json b/lexicons/app/bsky/unspecced/getConfig.json new file mode 100644 index 0000000000..1e40f80dc2 --- /dev/null +++ b/lexicons/app/bsky/unspecced/getConfig.json @@ -0,0 +1,45 @@ +{ + "lexicon": 1, + "id": "app.bsky.unspecced.getConfig", + "defs": { + "main": { + "type": "query", + "description": "Get miscellaneous runtime configuration.", + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": [], + "properties": { + "checkEmailConfirmed": { + "type": "boolean" + }, + "liveNow": { + "type": "array", + "items": { + "type": "ref", + "ref": "#liveNowConfig" + } + } + } + } + } + }, + "liveNowConfig": { + "type": "object", + "required": ["did", "domains"], + "properties": { + "did": { + "type": "string", + "format": "did" + }, + "domains": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } +} diff --git a/lexicons/app/bsky/unspecced/getOnboardingSuggestedStarterPacks.json b/lexicons/app/bsky/unspecced/getOnboardingSuggestedStarterPacks.json new file mode 100644 index 0000000000..60b1490137 --- /dev/null +++ b/lexicons/app/bsky/unspecced/getOnboardingSuggestedStarterPacks.json @@ -0,0 +1,37 @@ +{ + "lexicon": 1, + "id": "app.bsky.unspecced.getOnboardingSuggestedStarterPacks", + "defs": { + "main": { + "type": "query", + "description": "Get a list of suggested starterpacks for onboarding", + "parameters": { + "type": "params", + "properties": { + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 25, + "default": 10 + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["starterPacks"], + "properties": { + "starterPacks": { + "type": "array", + "items": { + "type": "ref", + "ref": "app.bsky.graph.defs#starterPackView" + } + } + } + } + } + } + } +} diff --git a/lexicons/app/bsky/unspecced/getPopularFeedGenerators.json b/lexicons/app/bsky/unspecced/getPopularFeedGenerators.json new file mode 100644 index 0000000000..407ff66e3a --- /dev/null +++ b/lexicons/app/bsky/unspecced/getPopularFeedGenerators.json @@ -0,0 +1,46 @@ +{ + "lexicon": 1, + "id": "app.bsky.unspecced.getPopularFeedGenerators", + "defs": { + "main": { + "type": "query", + "description": "An unspecced view of globally popular feed generators.", + "parameters": { + "type": "params", + "properties": { + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 50 + }, + "cursor": { + "type": "string" + }, + "query": { + "type": "string" + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["feeds"], + "properties": { + "cursor": { + "type": "string" + }, + "feeds": { + "type": "array", + "items": { + "type": "ref", + "ref": "app.bsky.feed.defs#generatorView" + } + } + } + } + } + } + } +} diff --git a/lexicons/app/bsky/unspecced/getPostThreadOtherV2.json b/lexicons/app/bsky/unspecced/getPostThreadOtherV2.json new file mode 100644 index 0000000000..7fab28bfeb --- /dev/null +++ b/lexicons/app/bsky/unspecced/getPostThreadOtherV2.json @@ -0,0 +1,56 @@ +{ + "lexicon": 1, + "id": "app.bsky.unspecced.getPostThreadOtherV2", + "defs": { + "main": { + "type": "query", + "description": "(NOTE: this endpoint is under development and WILL change without notice. Don't use it until it is moved out of `unspecced` or your application WILL break) Get additional posts under a thread e.g. replies hidden by threadgate. Based on an anchor post at any depth of the tree, returns top-level replies below that anchor. It does not include ancestors nor the anchor itself. This should be called after exhausting `app.bsky.unspecced.getPostThreadV2`. Does not require auth, but additional metadata and filtering will be applied for authed requests.", + "parameters": { + "type": "params", + "required": ["anchor"], + "properties": { + "anchor": { + "type": "string", + "format": "at-uri", + "description": "Reference (AT-URI) to post record. This is the anchor post." + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["thread"], + "properties": { + "thread": { + "type": "array", + "description": "A flat list of other thread items. The depth of each item is indicated by the depth property inside the item.", + "items": { + "type": "ref", + "ref": "#threadItem" + } + } + } + } + } + }, + "threadItem": { + "type": "object", + "required": ["uri", "depth", "value"], + "properties": { + "uri": { + "type": "string", + "format": "at-uri" + }, + "depth": { + "type": "integer", + "description": "The nesting level of this item in the thread. Depth 0 means the anchor item. Items above have negative depths, items below have positive depths." + }, + "value": { + "type": "union", + "refs": ["app.bsky.unspecced.defs#threadItemPost"] + } + } + } + } +} diff --git a/lexicons/app/bsky/unspecced/getPostThreadV2.json b/lexicons/app/bsky/unspecced/getPostThreadV2.json new file mode 100644 index 0000000000..e38dfde964 --- /dev/null +++ b/lexicons/app/bsky/unspecced/getPostThreadV2.json @@ -0,0 +1,94 @@ +{ + "lexicon": 1, + "id": "app.bsky.unspecced.getPostThreadV2", + "defs": { + "main": { + "type": "query", + "description": "(NOTE: this endpoint is under development and WILL change without notice. Don't use it until it is moved out of `unspecced` or your application WILL break) Get posts in a thread. It is based in an anchor post at any depth of the tree, and returns posts above it (recursively resolving the parent, without further branching to their replies) and below it (recursive replies, with branching to their replies). Does not require auth, but additional metadata and filtering will be applied for authed requests.", + "parameters": { + "type": "params", + "required": ["anchor"], + "properties": { + "anchor": { + "type": "string", + "format": "at-uri", + "description": "Reference (AT-URI) to post record. This is the anchor post, and the thread will be built around it. It can be any post in the tree, not necessarily a root post." + }, + "above": { + "type": "boolean", + "description": "Whether to include parents above the anchor.", + "default": true + }, + "below": { + "type": "integer", + "description": "How many levels of replies to include below the anchor.", + "default": 6, + "minimum": 0, + "maximum": 20 + }, + "branchingFactor": { + "type": "integer", + "description": "Maximum of replies to include at each level of the thread, except for the direct replies to the anchor, which are (NOTE: currently, during unspecced phase) all returned (NOTE: later they might be paginated).", + "default": 10, + "minimum": 0, + "maximum": 100 + }, + "sort": { + "type": "string", + "description": "Sorting for the thread replies.", + "knownValues": ["newest", "oldest", "top"], + "default": "oldest" + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["thread", "hasOtherReplies"], + "properties": { + "thread": { + "type": "array", + "description": "A flat list of thread items. The depth of each item is indicated by the depth property inside the item.", + "items": { + "type": "ref", + "ref": "#threadItem" + } + }, + "threadgate": { + "type": "ref", + "ref": "app.bsky.feed.defs#threadgateView" + }, + "hasOtherReplies": { + "type": "boolean", + "description": "Whether this thread has additional replies. If true, a call can be made to the `getPostThreadOtherV2` endpoint to retrieve them." + } + } + } + } + }, + "threadItem": { + "type": "object", + "required": ["uri", "depth", "value"], + "properties": { + "uri": { + "type": "string", + "format": "at-uri" + }, + "depth": { + "type": "integer", + "description": "The nesting level of this item in the thread. Depth 0 means the anchor item. Items above have negative depths, items below have positive depths." + }, + "value": { + "type": "union", + "refs": [ + "app.bsky.unspecced.defs#threadItemPost", + "app.bsky.unspecced.defs#threadItemNoUnauthenticated", + "app.bsky.unspecced.defs#threadItemNotFound", + "app.bsky.unspecced.defs#threadItemBlocked" + ] + } + } + } + } +} diff --git a/lexicons/app/bsky/unspecced/getSuggestedFeeds.json b/lexicons/app/bsky/unspecced/getSuggestedFeeds.json new file mode 100644 index 0000000000..1a723a0bbb --- /dev/null +++ b/lexicons/app/bsky/unspecced/getSuggestedFeeds.json @@ -0,0 +1,37 @@ +{ + "lexicon": 1, + "id": "app.bsky.unspecced.getSuggestedFeeds", + "defs": { + "main": { + "type": "query", + "description": "Get a list of suggested feeds", + "parameters": { + "type": "params", + "properties": { + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 25, + "default": 10 + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["feeds"], + "properties": { + "feeds": { + "type": "array", + "items": { + "type": "ref", + "ref": "app.bsky.feed.defs#generatorView" + } + } + } + } + } + } + } +} diff --git a/lexicons/app/bsky/unspecced/getSuggestedFeedsSkeleton.json b/lexicons/app/bsky/unspecced/getSuggestedFeedsSkeleton.json new file mode 100644 index 0000000000..c0b72da680 --- /dev/null +++ b/lexicons/app/bsky/unspecced/getSuggestedFeedsSkeleton.json @@ -0,0 +1,42 @@ +{ + "lexicon": 1, + "id": "app.bsky.unspecced.getSuggestedFeedsSkeleton", + "defs": { + "main": { + "type": "query", + "description": "Get a skeleton of suggested feeds. Intended to be called and hydrated by app.bsky.unspecced.getSuggestedFeeds", + "parameters": { + "type": "params", + "properties": { + "viewer": { + "type": "string", + "format": "did", + "description": "DID of the account making the request (not included for public/unauthenticated queries)." + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 25, + "default": 10 + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["feeds"], + "properties": { + "feeds": { + "type": "array", + "items": { + "type": "string", + "format": "at-uri" + } + } + } + } + } + } + } +} diff --git a/lexicons/app/bsky/unspecced/getSuggestedOnboardingUsers.json b/lexicons/app/bsky/unspecced/getSuggestedOnboardingUsers.json new file mode 100644 index 0000000000..24e45566ef --- /dev/null +++ b/lexicons/app/bsky/unspecced/getSuggestedOnboardingUsers.json @@ -0,0 +1,49 @@ +{ + "lexicon": 1, + "id": "app.bsky.unspecced.getSuggestedOnboardingUsers", + "defs": { + "main": { + "type": "query", + "description": "Get a list of suggested users for onboarding", + "parameters": { + "type": "params", + "properties": { + "category": { + "type": "string", + "description": "Category of users to get suggestions for." + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 50, + "default": 25 + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["actors"], + "properties": { + "actors": { + "type": "array", + "items": { + "type": "ref", + "ref": "app.bsky.actor.defs#profileView" + } + }, + "recId": { + "type": "string", + "description": "DEPRECATED: use recIdStr instead." + }, + "recIdStr": { + "type": "string", + "description": "Snowflake for this recommendation, use when submitting recommendation events." + } + } + } + } + } + } +} diff --git a/lexicons/app/bsky/unspecced/getSuggestedStarterPacks.json b/lexicons/app/bsky/unspecced/getSuggestedStarterPacks.json new file mode 100644 index 0000000000..d4bdd22643 --- /dev/null +++ b/lexicons/app/bsky/unspecced/getSuggestedStarterPacks.json @@ -0,0 +1,37 @@ +{ + "lexicon": 1, + "id": "app.bsky.unspecced.getSuggestedStarterPacks", + "defs": { + "main": { + "type": "query", + "description": "Get a list of suggested starterpacks", + "parameters": { + "type": "params", + "properties": { + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 25, + "default": 10 + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["starterPacks"], + "properties": { + "starterPacks": { + "type": "array", + "items": { + "type": "ref", + "ref": "app.bsky.graph.defs#starterPackView" + } + } + } + } + } + } + } +} diff --git a/lexicons/app/bsky/unspecced/getSuggestedStarterPacksSkeleton.json b/lexicons/app/bsky/unspecced/getSuggestedStarterPacksSkeleton.json new file mode 100644 index 0000000000..cf5d88e060 --- /dev/null +++ b/lexicons/app/bsky/unspecced/getSuggestedStarterPacksSkeleton.json @@ -0,0 +1,42 @@ +{ + "lexicon": 1, + "id": "app.bsky.unspecced.getSuggestedStarterPacksSkeleton", + "defs": { + "main": { + "type": "query", + "description": "Get a skeleton of suggested starterpacks. Intended to be called and hydrated by app.bsky.unspecced.getSuggestedStarterpacks", + "parameters": { + "type": "params", + "properties": { + "viewer": { + "type": "string", + "format": "did", + "description": "DID of the account making the request (not included for public/unauthenticated queries)." + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 25, + "default": 10 + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["starterPacks"], + "properties": { + "starterPacks": { + "type": "array", + "items": { + "type": "string", + "format": "at-uri" + } + } + } + } + } + } + } +} diff --git a/lexicons/app/bsky/unspecced/getSuggestedUsers.json b/lexicons/app/bsky/unspecced/getSuggestedUsers.json new file mode 100644 index 0000000000..705e21c9d6 --- /dev/null +++ b/lexicons/app/bsky/unspecced/getSuggestedUsers.json @@ -0,0 +1,49 @@ +{ + "lexicon": 1, + "id": "app.bsky.unspecced.getSuggestedUsers", + "defs": { + "main": { + "type": "query", + "description": "Get a list of suggested users", + "parameters": { + "type": "params", + "properties": { + "category": { + "type": "string", + "description": "Category of users to get suggestions for." + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 50, + "default": 25 + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["actors"], + "properties": { + "actors": { + "type": "array", + "items": { + "type": "ref", + "ref": "app.bsky.actor.defs#profileView" + } + }, + "recId": { + "type": "string", + "description": "DEPRECATED: use recIdStr instead." + }, + "recIdStr": { + "type": "string", + "description": "Snowflake for this recommendation, use when submitting recommendation events." + } + } + } + } + } + } +} diff --git a/lexicons/app/bsky/unspecced/getSuggestedUsersForDiscover.json b/lexicons/app/bsky/unspecced/getSuggestedUsersForDiscover.json new file mode 100644 index 0000000000..7d0f289d12 --- /dev/null +++ b/lexicons/app/bsky/unspecced/getSuggestedUsersForDiscover.json @@ -0,0 +1,41 @@ +{ + "lexicon": 1, + "id": "app.bsky.unspecced.getSuggestedUsersForDiscover", + "defs": { + "main": { + "type": "query", + "description": "Get a list of suggested users for the Discover page", + "parameters": { + "type": "params", + "properties": { + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 50, + "default": 25 + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["actors"], + "properties": { + "actors": { + "type": "array", + "items": { + "type": "ref", + "ref": "app.bsky.actor.defs#profileView" + } + }, + "recIdStr": { + "type": "string", + "description": "Snowflake for this recommendation, use when submitting recommendation events." + } + } + } + } + } + } +} diff --git a/lexicons/app/bsky/unspecced/getSuggestedUsersForExplore.json b/lexicons/app/bsky/unspecced/getSuggestedUsersForExplore.json new file mode 100644 index 0000000000..a37a21a7eb --- /dev/null +++ b/lexicons/app/bsky/unspecced/getSuggestedUsersForExplore.json @@ -0,0 +1,45 @@ +{ + "lexicon": 1, + "id": "app.bsky.unspecced.getSuggestedUsersForExplore", + "defs": { + "main": { + "type": "query", + "description": "Get a list of suggested users for the Explore page", + "parameters": { + "type": "params", + "properties": { + "category": { + "type": "string", + "description": "Category of users to get suggestions for." + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 50, + "default": 25 + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["actors"], + "properties": { + "actors": { + "type": "array", + "items": { + "type": "ref", + "ref": "app.bsky.actor.defs#profileView" + } + }, + "recIdStr": { + "type": "string", + "description": "Snowflake for this recommendation, use when submitting recommendation events." + } + } + } + } + } + } +} diff --git a/lexicons/app/bsky/unspecced/getSuggestedUsersForSeeMore.json b/lexicons/app/bsky/unspecced/getSuggestedUsersForSeeMore.json new file mode 100644 index 0000000000..1938df7e55 --- /dev/null +++ b/lexicons/app/bsky/unspecced/getSuggestedUsersForSeeMore.json @@ -0,0 +1,45 @@ +{ + "lexicon": 1, + "id": "app.bsky.unspecced.getSuggestedUsersForSeeMore", + "defs": { + "main": { + "type": "query", + "description": "Get a list of suggested users for the See More page", + "parameters": { + "type": "params", + "properties": { + "category": { + "type": "string", + "description": "Category of users to get suggestions for." + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 50, + "default": 25 + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["actors"], + "properties": { + "actors": { + "type": "array", + "items": { + "type": "ref", + "ref": "app.bsky.actor.defs#profileView" + } + }, + "recIdStr": { + "type": "string", + "description": "Snowflake for this recommendation, use when submitting recommendation events." + } + } + } + } + } + } +} diff --git a/lexicons/app/bsky/unspecced/getSuggestedUsersSkeleton.json b/lexicons/app/bsky/unspecced/getSuggestedUsersSkeleton.json new file mode 100644 index 0000000000..3fed8dd95d --- /dev/null +++ b/lexicons/app/bsky/unspecced/getSuggestedUsersSkeleton.json @@ -0,0 +1,54 @@ +{ + "lexicon": 1, + "id": "app.bsky.unspecced.getSuggestedUsersSkeleton", + "defs": { + "main": { + "type": "query", + "description": "Get a skeleton of suggested users. Intended to be called and hydrated by app.bsky.unspecced.getSuggestedUsers", + "parameters": { + "type": "params", + "properties": { + "viewer": { + "type": "string", + "format": "did", + "description": "DID of the account making the request (not included for public/unauthenticated queries)." + }, + "category": { + "type": "string", + "description": "Category of users to get suggestions for." + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 50, + "default": 25 + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["dids"], + "properties": { + "dids": { + "type": "array", + "items": { + "type": "string", + "format": "did" + } + }, + "recId": { + "type": "string", + "description": "DEPRECATED: use recIdStr instead." + }, + "recIdStr": { + "type": "string", + "description": "Snowflake for this recommendation, use when submitting recommendation events." + } + } + } + } + } + } +} diff --git a/lexicons/app/bsky/unspecced/getSuggestionsSkeleton.json b/lexicons/app/bsky/unspecced/getSuggestionsSkeleton.json new file mode 100644 index 0000000000..466815ed47 --- /dev/null +++ b/lexicons/app/bsky/unspecced/getSuggestionsSkeleton.json @@ -0,0 +1,66 @@ +{ + "lexicon": 1, + "id": "app.bsky.unspecced.getSuggestionsSkeleton", + "defs": { + "main": { + "type": "query", + "description": "Get a skeleton of suggested actors. Intended to be called and then hydrated through app.bsky.actor.getSuggestions", + "parameters": { + "type": "params", + "properties": { + "viewer": { + "type": "string", + "format": "did", + "description": "DID of the account making the request (not included for public/unauthenticated queries). Used to boost followed accounts in ranking." + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 50 + }, + "cursor": { + "type": "string" + }, + "relativeToDid": { + "type": "string", + "format": "did", + "description": "DID of the account to get suggestions relative to. If not provided, suggestions will be based on the viewer." + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["actors"], + "properties": { + "cursor": { + "type": "string" + }, + "actors": { + "type": "array", + "items": { + "type": "ref", + "ref": "app.bsky.unspecced.defs#skeletonSearchActor" + } + }, + "relativeToDid": { + "type": "string", + "format": "did", + "description": "DID of the account these suggestions are relative to. If this is returned undefined, suggestions are based on the viewer." + }, + "recId": { + "type": "integer", + "description": "DEPRECATED: use recIdStr instead." + }, + "recIdStr": { + "type": "string", + "description": "Snowflake for this recommendation, use when submitting recommendation events." + } + } + } + } + } + } +} diff --git a/lexicons/app/bsky/unspecced/getTaggedSuggestions.json b/lexicons/app/bsky/unspecced/getTaggedSuggestions.json new file mode 100644 index 0000000000..f81459098d --- /dev/null +++ b/lexicons/app/bsky/unspecced/getTaggedSuggestions.json @@ -0,0 +1,47 @@ +{ + "lexicon": 1, + "id": "app.bsky.unspecced.getTaggedSuggestions", + "defs": { + "main": { + "type": "query", + "description": "Get a list of suggestions (feeds and users) tagged with categories", + "parameters": { + "type": "params", + "properties": {} + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["suggestions"], + "properties": { + "suggestions": { + "type": "array", + "items": { + "type": "ref", + "ref": "#suggestion" + } + } + } + } + } + }, + "suggestion": { + "type": "object", + "required": ["tag", "subjectType", "subject"], + "properties": { + "tag": { + "type": "string" + }, + "subjectType": { + "type": "string", + "knownValues": ["actor", "feed"] + }, + "subject": { + "type": "string", + "format": "uri" + } + } + } + } +} diff --git a/lexicons/app/bsky/unspecced/getTrendingTopics.json b/lexicons/app/bsky/unspecced/getTrendingTopics.json new file mode 100644 index 0000000000..097dc1c54d --- /dev/null +++ b/lexicons/app/bsky/unspecced/getTrendingTopics.json @@ -0,0 +1,49 @@ +{ + "lexicon": 1, + "id": "app.bsky.unspecced.getTrendingTopics", + "defs": { + "main": { + "type": "query", + "description": "Get a list of trending topics", + "parameters": { + "type": "params", + "properties": { + "viewer": { + "type": "string", + "format": "did", + "description": "DID of the account making the request (not included for public/unauthenticated queries). Used to boost followed accounts in ranking." + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 25, + "default": 10 + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["topics", "suggested"], + "properties": { + "topics": { + "type": "array", + "items": { + "type": "ref", + "ref": "app.bsky.unspecced.defs#trendingTopic" + } + }, + "suggested": { + "type": "array", + "items": { + "type": "ref", + "ref": "app.bsky.unspecced.defs#trendingTopic" + } + } + } + } + } + } + } +} diff --git a/lexicons/app/bsky/unspecced/getTrends.json b/lexicons/app/bsky/unspecced/getTrends.json new file mode 100644 index 0000000000..b857c8681b --- /dev/null +++ b/lexicons/app/bsky/unspecced/getTrends.json @@ -0,0 +1,41 @@ +{ + "lexicon": 1, + "id": "app.bsky.unspecced.getTrends", + "defs": { + "main": { + "type": "query", + "description": "Get the current trends on the network", + "parameters": { + "type": "params", + "properties": { + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 25, + "default": 10 + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["trends"], + "properties": { + "trends": { + "type": "array", + "items": { + "type": "ref", + "ref": "app.bsky.unspecced.defs#trendView" + } + }, + "recIdStr": { + "type": "string", + "description": "Snowflake for this recommendation, use when submitting recommendation events." + } + } + } + } + } + } +} diff --git a/lexicons/app/bsky/unspecced/getTrendsSkeleton.json b/lexicons/app/bsky/unspecced/getTrendsSkeleton.json new file mode 100644 index 0000000000..d0b6272bb6 --- /dev/null +++ b/lexicons/app/bsky/unspecced/getTrendsSkeleton.json @@ -0,0 +1,42 @@ +{ + "lexicon": 1, + "id": "app.bsky.unspecced.getTrendsSkeleton", + "defs": { + "main": { + "type": "query", + "description": "Get the skeleton of trends on the network. Intended to be called and then hydrated through app.bsky.unspecced.getTrends", + "parameters": { + "type": "params", + "properties": { + "viewer": { + "type": "string", + "format": "did", + "description": "DID of the account making the request (not included for public/unauthenticated queries)." + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 25, + "default": 10 + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["trends"], + "properties": { + "trends": { + "type": "array", + "items": { + "type": "ref", + "ref": "app.bsky.unspecced.defs#skeletonTrend" + } + } + } + } + } + } + } +} diff --git a/lexicons/app/bsky/unspecced/initAgeAssurance.json b/lexicons/app/bsky/unspecced/initAgeAssurance.json new file mode 100644 index 0000000000..236c07cf36 --- /dev/null +++ b/lexicons/app/bsky/unspecced/initAgeAssurance.json @@ -0,0 +1,49 @@ +{ + "lexicon": 1, + "id": "app.bsky.unspecced.initAgeAssurance", + "defs": { + "main": { + "type": "procedure", + "description": "Initiate age assurance for an account. This is a one-time action that will start the process of verifying the user's age.", + "input": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["email", "language", "countryCode"], + "properties": { + "email": { + "type": "string", + "description": "The user's email address to receive assurance instructions." + }, + "language": { + "type": "string", + "description": "The user's preferred language for communication during the assurance process." + }, + "countryCode": { + "type": "string", + "description": "An ISO 3166-1 alpha-2 code of the user's location." + } + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "ref", + "ref": "app.bsky.unspecced.defs#ageAssuranceState" + } + }, + "errors": [ + { + "name": "InvalidEmail" + }, + { + "name": "DidTooLong" + }, + { + "name": "InvalidInitiation" + } + ] + } + } +} diff --git a/lexicons/app/bsky/unspecced/searchActorsSkeleton.json b/lexicons/app/bsky/unspecced/searchActorsSkeleton.json new file mode 100644 index 0000000000..71784336a3 --- /dev/null +++ b/lexicons/app/bsky/unspecced/searchActorsSkeleton.json @@ -0,0 +1,67 @@ +{ + "lexicon": 1, + "id": "app.bsky.unspecced.searchActorsSkeleton", + "defs": { + "main": { + "type": "query", + "description": "Backend Actors (profile) search, returns only skeleton.", + "parameters": { + "type": "params", + "required": ["q"], + "properties": { + "q": { + "type": "string", + "description": "Search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended. For typeahead search, only simple term match is supported, not full syntax." + }, + "viewer": { + "type": "string", + "format": "did", + "description": "DID of the account making the request (not included for public/unauthenticated queries). Used to boost followed accounts in ranking." + }, + "typeahead": { + "type": "boolean", + "description": "If true, acts as fast/simple 'typeahead' query." + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 25 + }, + "cursor": { + "type": "string", + "description": "Optional pagination mechanism; may not necessarily allow scrolling through entire result set." + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["actors"], + "properties": { + "cursor": { + "type": "string" + }, + "hitsTotal": { + "type": "integer", + "description": "Count of search hits. Optional, may be rounded/truncated, and may not be possible to paginate through all hits." + }, + "actors": { + "type": "array", + "items": { + "type": "ref", + "ref": "app.bsky.unspecced.defs#skeletonSearchActor" + } + } + } + } + }, + "errors": [ + { + "name": "BadQueryString" + } + ] + } + } +} diff --git a/lexicons/app/bsky/unspecced/searchPostsSkeleton.json b/lexicons/app/bsky/unspecced/searchPostsSkeleton.json new file mode 100644 index 0000000000..63c3cf4858 --- /dev/null +++ b/lexicons/app/bsky/unspecced/searchPostsSkeleton.json @@ -0,0 +1,110 @@ +{ + "lexicon": 1, + "id": "app.bsky.unspecced.searchPostsSkeleton", + "defs": { + "main": { + "type": "query", + "description": "Backend Posts search, returns only skeleton", + "parameters": { + "type": "params", + "required": ["q"], + "properties": { + "q": { + "type": "string", + "description": "Search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended." + }, + "sort": { + "type": "string", + "knownValues": ["top", "latest"], + "default": "latest", + "description": "Specifies the ranking order of results." + }, + "since": { + "type": "string", + "description": "Filter results for posts after the indicated datetime (inclusive). Expected to use 'sortAt' timestamp, which may not match 'createdAt'. Can be a datetime, or just an ISO date (YYYY-MM-DD)." + }, + "until": { + "type": "string", + "description": "Filter results for posts before the indicated datetime (not inclusive). Expected to use 'sortAt' timestamp, which may not match 'createdAt'. Can be a datetime, or just an ISO date (YYY-MM-DD)." + }, + "mentions": { + "type": "string", + "format": "at-identifier", + "description": "Filter to posts which mention the given account. Handles are resolved to DID before query-time. Only matches rich-text facet mentions." + }, + "author": { + "type": "string", + "format": "at-identifier", + "description": "Filter to posts by the given account. Handles are resolved to DID before query-time." + }, + "lang": { + "type": "string", + "format": "language", + "description": "Filter to posts in the given language. Expected to be based on post language field, though server may override language detection." + }, + "domain": { + "type": "string", + "description": "Filter to posts with URLs (facet links or embeds) linking to the given domain (hostname). Server may apply hostname normalization." + }, + "url": { + "type": "string", + "format": "uri", + "description": "Filter to posts with links (facet links or embeds) pointing to this URL. Server may apply URL normalization or fuzzy matching." + }, + "tag": { + "type": "array", + "items": { + "type": "string", + "maxLength": 640, + "maxGraphemes": 64 + }, + "description": "Filter to posts with the given tag (hashtag), based on rich-text facet or tag field. Do not include the hash (#) prefix. Multiple tags can be specified, with 'AND' matching." + }, + "viewer": { + "type": "string", + "format": "did", + "description": "DID of the account making the request (not included for public/unauthenticated queries). Used for 'from:me' queries." + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 25 + }, + "cursor": { + "type": "string", + "description": "Optional pagination mechanism; may not necessarily allow scrolling through entire result set." + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["posts"], + "properties": { + "cursor": { + "type": "string" + }, + "hitsTotal": { + "type": "integer", + "description": "Count of search hits. Optional, may be rounded/truncated, and may not be possible to paginate through all hits." + }, + "posts": { + "type": "array", + "items": { + "type": "ref", + "ref": "app.bsky.unspecced.defs#skeletonSearchPost" + } + } + } + } + }, + "errors": [ + { + "name": "BadQueryString" + } + ] + } + } +} diff --git a/lexicons/app/bsky/unspecced/searchStarterPacksSkeleton.json b/lexicons/app/bsky/unspecced/searchStarterPacksSkeleton.json new file mode 100644 index 0000000000..80c1b88179 --- /dev/null +++ b/lexicons/app/bsky/unspecced/searchStarterPacksSkeleton.json @@ -0,0 +1,63 @@ +{ + "lexicon": 1, + "id": "app.bsky.unspecced.searchStarterPacksSkeleton", + "defs": { + "main": { + "type": "query", + "description": "Backend Starter Pack search, returns only skeleton.", + "parameters": { + "type": "params", + "required": ["q"], + "properties": { + "q": { + "type": "string", + "description": "Search query string; syntax, phrase, boolean, and faceting is unspecified, but Lucene query syntax is recommended." + }, + "viewer": { + "type": "string", + "format": "did", + "description": "DID of the account making the request (not included for public/unauthenticated queries)." + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "default": 25 + }, + "cursor": { + "type": "string", + "description": "Optional pagination mechanism; may not necessarily allow scrolling through entire result set." + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["starterPacks"], + "properties": { + "cursor": { + "type": "string" + }, + "hitsTotal": { + "type": "integer", + "description": "Count of search hits. Optional, may be rounded/truncated, and may not be possible to paginate through all hits." + }, + "starterPacks": { + "type": "array", + "items": { + "type": "ref", + "ref": "app.bsky.unspecced.defs#skeletonSearchStarterPack" + } + } + } + } + }, + "errors": [ + { + "name": "BadQueryString" + } + ] + } + } +} diff --git a/lexicons/app/bsky/video/defs.json b/lexicons/app/bsky/video/defs.json new file mode 100644 index 0000000000..a5bcb22bcd --- /dev/null +++ b/lexicons/app/bsky/video/defs.json @@ -0,0 +1,58 @@ +{ + "id": "app.bsky.video.defs", + "defs": { + "jobStatus": { + "type": "object", + "required": [ + "jobId", + "did", + "state" + ], + "properties": { + "did": { + "type": "string", + "format": "did" + }, + "blob": { + "type": "blob" + }, + "error": { + "type": "string" + }, + "jobId": { + "type": "string" + }, + "state": { + "type": "string", + "description": "The state of the video processing job. All values not listed as a known value indicate that the job is in process.", + "knownValues": [ + "JOB_STATE_COMPLETED", + "JOB_STATE_FAILED" + ] + }, + "message": { + "type": "string" + }, + "progress": { + "type": "integer", + "maximum": 100, + "minimum": 0, + "description": "Progress within the current processing state." + }, + "failureCode": { + "type": "string", + "description": "A machine-readable code for why the video processing job failed.", + "knownValues": [ + "validation_failure", + "encoding_failure", + "pds_upload_failure", + "pds_upload_unsupported_blob_size", + "generic_failure" + ] + } + } + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/video/getJobStatus.json b/lexicons/app/bsky/video/getJobStatus.json new file mode 100644 index 0000000000..7fb3ae4a46 --- /dev/null +++ b/lexicons/app/bsky/video/getJobStatus.json @@ -0,0 +1,37 @@ +{ + "id": "app.bsky.video.getJobStatus", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "jobStatus" + ], + "properties": { + "jobStatus": { + "ref": "app.bsky.video.defs#jobStatus", + "type": "ref" + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "jobId" + ], + "properties": { + "jobId": { + "type": "string" + } + } + }, + "description": "Get status details for a video processing job." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/video/getUploadLimits.json b/lexicons/app/bsky/video/getUploadLimits.json new file mode 100644 index 0000000000..fdc11082ec --- /dev/null +++ b/lexicons/app/bsky/video/getUploadLimits.json @@ -0,0 +1,37 @@ +{ + "id": "app.bsky.video.getUploadLimits", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "canUpload" + ], + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "canUpload": { + "type": "boolean" + }, + "remainingDailyBytes": { + "type": "integer" + }, + "remainingDailyVideos": { + "type": "integer" + } + } + }, + "encoding": "application/json" + }, + "description": "Get video upload limits for the authenticated user." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/app/bsky/video/uploadVideo.json b/lexicons/app/bsky/video/uploadVideo.json new file mode 100644 index 0000000000..01330b1b9a --- /dev/null +++ b/lexicons/app/bsky/video/uploadVideo.json @@ -0,0 +1,29 @@ +{ + "id": "app.bsky.video.uploadVideo", + "defs": { + "main": { + "type": "procedure", + "input": { + "encoding": "video/mp4" + }, + "output": { + "schema": { + "type": "object", + "required": [ + "jobStatus" + ], + "properties": { + "jobStatus": { + "ref": "app.bsky.video.defs#jobStatus", + "type": "ref" + } + } + }, + "encoding": "application/json" + }, + "description": "Upload a video to be processed then stored on the PDS." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/chat/bsky/actor/declaration.json b/lexicons/chat/bsky/actor/declaration.json new file mode 100644 index 0000000000..de04440ac7 --- /dev/null +++ b/lexicons/chat/bsky/actor/declaration.json @@ -0,0 +1,37 @@ +{ + "id": "chat.bsky.actor.declaration", + "defs": { + "main": { + "key": "literal:self", + "type": "record", + "record": { + "type": "object", + "required": [ + "allowIncoming" + ], + "properties": { + "allowIncoming": { + "type": "string", + "knownValues": [ + "all", + "none", + "following" + ] + }, + "allowGroupInvites": { + "type": "string", + "description": "Declaration about group chat invitation preferences for the record owner.", + "knownValues": [ + "all", + "none", + "following" + ] + } + } + }, + "description": "A declaration of a Bluesky chat account." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/chat/bsky/actor/defs.json b/lexicons/chat/bsky/actor/defs.json new file mode 100644 index 0000000000..87b18d714b --- /dev/null +++ b/lexicons/chat/bsky/actor/defs.json @@ -0,0 +1,105 @@ +{ + "id": "chat.bsky.actor.defs", + "defs": { + "memberRole": { + "type": "string", + "knownValues": [ + "owner", + "standard" + ] + }, + "groupConvoMember": { + "type": "object", + "required": [ + "role" + ], + "properties": { + "role": { + "ref": "#memberRole", + "type": "ref", + "description": "The member's role within this conversation. Only present in group conversation member lists." + }, + "addedBy": { + "ref": "#profileViewBasic", + "type": "ref", + "description": "Who added this member. Only present if the member was added (instead of joining via link)." + } + }, + "description": "A current group convo member." + }, + "profileViewBasic": { + "type": "object", + "required": [ + "did", + "handle" + ], + "properties": { + "did": { + "type": "string", + "format": "did" + }, + "kind": { + "refs": [ + "#directConvoMember", + "#groupConvoMember", + "#pastGroupConvoMember" + ], + "type": "union", + "description": "Union field that has data specific to different kinds of convos." + }, + "avatar": { + "type": "string", + "format": "uri" + }, + "handle": { + "type": "string", + "format": "handle" + }, + "labels": { + "type": "array", + "items": { + "ref": "com.atproto.label.defs#label", + "type": "ref" + } + }, + "viewer": { + "ref": "app.bsky.actor.defs#viewerState", + "type": "ref" + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "associated": { + "ref": "app.bsky.actor.defs#profileAssociated", + "type": "ref" + }, + "displayName": { + "type": "string", + "maxLength": 640, + "maxGraphemes": 64 + }, + "chatDisabled": { + "type": "boolean", + "description": "Set to true when the actor cannot actively participate in conversations" + }, + "verification": { + "ref": "app.bsky.actor.defs#verificationState", + "type": "ref" + } + } + }, + "directConvoMember": { + "type": "object", + "properties": {} + }, + "pastGroupConvoMember": { + "type": "object", + "required": [], + "properties": {}, + "description": "A past group convo member." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/chat/bsky/actor/deleteAccount.json b/lexicons/chat/bsky/actor/deleteAccount.json new file mode 100644 index 0000000000..f8955411db --- /dev/null +++ b/lexicons/chat/bsky/actor/deleteAccount.json @@ -0,0 +1,17 @@ +{ + "id": "chat.bsky.actor.deleteAccount", + "defs": { + "main": { + "type": "procedure", + "output": { + "schema": { + "type": "object", + "properties": {} + }, + "encoding": "application/json" + } + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/chat/bsky/actor/exportAccountData.json b/lexicons/chat/bsky/actor/exportAccountData.json new file mode 100644 index 0000000000..dd8b9bfdda --- /dev/null +++ b/lexicons/chat/bsky/actor/exportAccountData.json @@ -0,0 +1,13 @@ +{ + "id": "chat.bsky.actor.exportAccountData", + "defs": { + "main": { + "type": "query", + "output": { + "encoding": "application/jsonl" + } + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/chat/bsky/actor/getStatus.json b/lexicons/chat/bsky/actor/getStatus.json new file mode 100644 index 0000000000..4e1311a97b --- /dev/null +++ b/lexicons/chat/bsky/actor/getStatus.json @@ -0,0 +1,36 @@ +{ + "id": "chat.bsky.actor.getStatus", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "chatDisabled", + "canCreateGroups", + "groupMemberLimit" + ], + "properties": { + "chatDisabled": { + "type": "boolean", + "description": "True when the viewer's account is disabled and cannot actively participate in chat." + }, + "canCreateGroups": { + "type": "boolean", + "description": "Whether the viewer's account is allowed to create group chats. New accounts are restricted from creating groups." + }, + "groupMemberLimit": { + "type": "integer", + "description": "The maximum number of members allowed in a group conversation." + } + } + }, + "encoding": "application/json" + }, + "description": "Get the authenticated viewer's chat status: whether their account is chat-disabled and whether their group-membership additions are restricted to accounts they follow." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/chat/bsky/convo/acceptConvo.json b/lexicons/chat/bsky/convo/acceptConvo.json new file mode 100644 index 0000000000..bafc162b11 --- /dev/null +++ b/lexicons/chat/bsky/convo/acceptConvo.json @@ -0,0 +1,42 @@ +{ + "id": "chat.bsky.convo.acceptConvo", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "convoId" + ], + "properties": { + "convoId": { + "type": "string" + } + } + }, + "encoding": "application/json" + }, + "errors": [ + { + "name": "InvalidConvo" + } + ], + "output": { + "schema": { + "type": "object", + "properties": { + "rev": { + "type": "string", + "description": "Rev when the convo was accepted. If not present, the convo was already accepted." + } + } + }, + "encoding": "application/json" + }, + "description": "Marks a conversation as accepted, so it is shown in the list of accepted convos instead on the request convos." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/chat/bsky/convo/addReaction.json b/lexicons/chat/bsky/convo/addReaction.json new file mode 100644 index 0000000000..7f317c84a9 --- /dev/null +++ b/lexicons/chat/bsky/convo/addReaction.json @@ -0,0 +1,73 @@ +{ + "id": "chat.bsky.convo.addReaction", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "convoId", + "messageId", + "value" + ], + "properties": { + "value": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "maxGraphemes": 1, + "minGraphemes": 1 + }, + "convoId": { + "type": "string" + }, + "messageId": { + "type": "string" + } + } + }, + "encoding": "application/json" + }, + "errors": [ + { + "name": "InvalidConvo" + }, + { + "name": "ReactionNotAllowed", + "description": "Indicates that reactions are not allowed on this message, e.g. because it is a system message." + }, + { + "name": "ReactionMessageDeleted", + "description": "Indicates that the message has been deleted and reactions can no longer be added/removed." + }, + { + "name": "ReactionLimitReached", + "description": "Indicates that the message has the maximum number of reactions allowed for a single user, and the requested reaction wasn't yet present. If it was already present, the request will not fail since it is idempotent." + }, + { + "name": "ReactionInvalidValue", + "description": "Indicates the value for the reaction is not acceptable. In general, this means it is not an emoji." + } + ], + "output": { + "schema": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "ref": "chat.bsky.convo.defs#messageView", + "type": "ref" + } + } + }, + "encoding": "application/json" + }, + "description": "Adds an emoji reaction to a message. Requires authentication. It is idempotent, so multiple calls from the same user with the same emoji result in a single reaction." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/chat/bsky/convo/defs.json b/lexicons/chat/bsky/convo/defs.json new file mode 100644 index 0000000000..eecef5ac5c --- /dev/null +++ b/lexicons/chat/bsky/convo/defs.json @@ -0,0 +1,1312 @@ +{ + "id": "chat.bsky.convo.defs", + "defs": { + "convoRef": { + "type": "object", + "required": [ + "did", + "convoId" + ], + "properties": { + "did": { + "type": "string", + "format": "did" + }, + "convoId": { + "type": "string" + } + } + }, + "replyRef": { + "type": "object", + "required": [ + "messageId" + ], + "properties": { + "messageId": { + "type": "string" + } + }, + "description": "A reference to another message within the same convo, used to indicate that a message is a reply to it." + }, + "convoKind": { + "type": "string", + "knownValues": [ + "direct", + "group" + ] + }, + "convoView": { + "type": "object", + "required": [ + "id", + "rev", + "members", + "muted", + "unreadCount" + ], + "properties": { + "id": { + "type": "string" + }, + "rev": { + "type": "string" + }, + "kind": { + "refs": [ + "#directConvo", + "#groupConvo" + ], + "type": "union", + "description": "Union field that has data specific to different kinds of convos." + }, + "muted": { + "type": "boolean" + }, + "status": { + "ref": "#convoStatus", + "type": "ref", + "description": "Convo status for the viewer member (not the convo itself)." + }, + "members": { + "type": "array", + "items": { + "ref": "chat.bsky.actor.defs#profileViewBasic", + "type": "ref" + }, + "description": "Members of this conversation. For direct convos, it will be an immutable list of the 2 members. For group convos, it will a list of important members (the first few members, the viewer, the member who added the viewer, the member who sent the last message, the member who sent the last reaction), but will not contain the full list of members. Use chat.bsky.convo.getConvoMembers to list all members." + }, + "lastMessage": { + "refs": [ + "#messageView", + "#deletedMessageView", + "#systemMessageView" + ], + "type": "union" + }, + "unreadCount": { + "type": "integer" + }, + "lastReaction": { + "refs": [ + "#messageAndReactionView" + ], + "type": "union" + } + } + }, + "groupConvo": { + "type": "object", + "required": [ + "createdAt", + "lockStatus", + "lockStatusModerationOverride", + "memberCount", + "memberLimit", + "name" + ], + "properties": { + "name": { + "type": "string", + "maxLength": 500, + "description": "The display name of the group conversation.", + "maxGraphemes": 50 + }, + "joinLink": { + "ref": "chat.bsky.group.defs#joinLinkView", + "type": "ref" + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "lockStatus": { + "ref": "#convoLockStatus", + "type": "ref", + "description": "The lock status of the conversation." + }, + "memberCount": { + "type": "integer", + "description": "The total number of members in the group conversation." + }, + "memberLimit": { + "type": "integer", + "description": "The maximum number of members allowed in the group conversation." + }, + "joinRequestCount": { + "type": "integer", + "description": "The total number of pending join requests for the group conversation. Only present for the owner. Capped at 21." + }, + "unreadJoinRequestCount": { + "type": "integer", + "description": "The number of unread join requests for the group conversation. Only present for the owner." + }, + "lockStatusModerationOverride": { + "type": "boolean", + "description": "Whether the lock status is being forced by a moderation override (account inactivation or convo takedown) rather than the owner's own setting." + } + } + }, + "messageRef": { + "type": "object", + "required": [ + "did", + "messageId", + "convoId" + ], + "properties": { + "did": { + "type": "string", + "format": "did" + }, + "convoId": { + "type": "string" + }, + "messageId": { + "type": "string" + } + } + }, + "convoStatus": { + "type": "string", + "knownValues": [ + "request", + "accepted" + ] + }, + "directConvo": { + "type": "object", + "properties": {} + }, + "messageView": { + "type": "object", + "required": [ + "id", + "rev", + "text", + "sender", + "sentAt" + ], + "properties": { + "id": { + "type": "string" + }, + "rev": { + "type": "string" + }, + "text": { + "type": "string", + "maxLength": 10000, + "maxGraphemes": 1000 + }, + "embed": { + "refs": [ + "app.bsky.embed.record#view", + "chat.bsky.embed.joinLink#view" + ], + "type": "union" + }, + "facets": { + "type": "array", + "items": { + "ref": "app.bsky.richtext.facet", + "type": "ref" + }, + "description": "Annotations of text (mentions, URLs, hashtags, etc)" + }, + "sender": { + "ref": "#messageViewSender", + "type": "ref" + }, + "sentAt": { + "type": "string", + "format": "datetime" + }, + "replyTo": { + "refs": [ + "#messageView", + "#deletedMessageView", + "#messageBeforeUserJoinedGroupView" + ], + "type": "union", + "description": "If set, the message this message is replying to. The full view of the referenced message is embedded so the client can render it inline. Only a single level is embedded: the embedded message will not itself have a populated 'replyTo' field even if it was also a reply." + }, + "reactions": { + "type": "array", + "items": { + "ref": "#reactionView", + "type": "ref" + }, + "description": "Reactions to this message, in ascending order of creation time." + } + } + }, + "logAddMember": { + "type": "object", + "required": [ + "rev", + "convoId", + "message", + "relatedProfiles" + ], + "properties": { + "rev": { + "type": "string" + }, + "convoId": { + "type": "string" + }, + "message": { + "ref": "#systemMessageView", + "type": "ref", + "description": "A system message with data of type #systemMessageDataAddMember" + }, + "relatedProfiles": { + "type": "array", + "items": { + "ref": "chat.bsky.actor.defs#profileViewBasic", + "type": "ref" + }, + "description": "Profiles referred in the system message." + } + }, + "description": "Event indicating a member was added to a group convo. The member who was added gets a logBeginConvo (to create the convo) but also a logAddMember (to show the system message as the first message the user sees)." + }, + "logEditGroup": { + "type": "object", + "required": [ + "rev", + "convoId", + "message" + ], + "properties": { + "rev": { + "type": "string" + }, + "convoId": { + "type": "string" + }, + "message": { + "ref": "#systemMessageView", + "type": "ref", + "description": "A system message with data of type #systemMessageDataEditGroup" + } + }, + "description": "Event indicating info about group convo was edited." + }, + "logLockConvo": { + "type": "object", + "required": [ + "rev", + "convoId", + "message", + "relatedProfiles" + ], + "properties": { + "rev": { + "type": "string" + }, + "convoId": { + "type": "string" + }, + "message": { + "ref": "#systemMessageView", + "type": "ref", + "description": "A system message with data of type #systemMessageDataLockConvo" + }, + "relatedProfiles": { + "type": "array", + "items": { + "ref": "chat.bsky.actor.defs#profileViewBasic", + "type": "ref" + }, + "description": "Profiles referred in the system message." + } + }, + "description": "Event indicating a group convo was locked." + }, + "logMuteConvo": { + "type": "object", + "required": [ + "rev", + "convoId" + ], + "properties": { + "rev": { + "type": "string" + }, + "convoId": { + "type": "string" + } + }, + "description": "Event indicating the viewer muted a convo. Can be direct or group." + }, + "logReadConvo": { + "type": "object", + "required": [ + "rev", + "convoId", + "message" + ], + "properties": { + "rev": { + "type": "string" + }, + "convoId": { + "type": "string" + }, + "message": { + "refs": [ + "#messageView", + "#deletedMessageView", + "#systemMessageView" + ], + "type": "union" + } + }, + "description": "Event indicating a convo was read up to a certain message." + }, + "messageInput": { + "type": "object", + "required": [ + "text" + ], + "properties": { + "text": { + "type": "string", + "maxLength": 10000, + "maxGraphemes": 1000 + }, + "embed": { + "refs": [ + "app.bsky.embed.record", + "chat.bsky.embed.joinLink" + ], + "type": "union" + }, + "facets": { + "type": "array", + "items": { + "ref": "app.bsky.richtext.facet", + "type": "ref" + }, + "description": "Annotations of text (mentions, URLs, hashtags, etc)" + }, + "replyTo": { + "ref": "#replyRef", + "type": "ref", + "description": "If set, the message this message is replying to. The referenced message must be in the same convo." + } + } + }, + "reactionView": { + "type": "object", + "required": [ + "value", + "sender", + "createdAt" + ], + "properties": { + "value": { + "type": "string" + }, + "sender": { + "ref": "#reactionViewSender", + "type": "ref" + }, + "createdAt": { + "type": "string", + "format": "datetime" + } + } + }, + "logBeginConvo": { + "type": "object", + "required": [ + "rev", + "convoId" + ], + "properties": { + "rev": { + "type": "string" + }, + "convoId": { + "type": "string" + } + }, + "description": "Event indicating a convo containing the viewer was started. Can be direct or group. When a member is added to a group convo, they also get this event." + }, + "logLeaveConvo": { + "type": "object", + "required": [ + "rev", + "convoId" + ], + "properties": { + "rev": { + "type": "string" + }, + "convoId": { + "type": "string" + } + }, + "description": "Event indicating the viewer left a convo. Can be direct or group." + }, + "logMemberJoin": { + "type": "object", + "required": [ + "rev", + "convoId", + "message", + "relatedProfiles" + ], + "properties": { + "rev": { + "type": "string" + }, + "convoId": { + "type": "string" + }, + "message": { + "ref": "#systemMessageView", + "type": "ref", + "description": "A system message with data of type #systemMessageDataMemberJoin" + }, + "relatedProfiles": { + "type": "array", + "items": { + "ref": "chat.bsky.actor.defs#profileViewBasic", + "type": "ref" + }, + "description": "Profiles referred in the system message." + } + }, + "description": "Event indicating a member joined a group convo via join link. The member who was added gets a logBeginConvo (to create the convo) but also a logMemberJoin (to show the system message as the first message the user sees)." + }, + "logAcceptConvo": { + "type": "object", + "required": [ + "rev", + "convoId" + ], + "properties": { + "rev": { + "type": "string" + }, + "convoId": { + "type": "string" + } + }, + "description": "Event indicating the viewer accepted a convo, and it can be moved out of the request inbox. Can be direct or group." + }, + "logAddReaction": { + "type": "object", + "required": [ + "rev", + "convoId", + "message", + "reaction" + ], + "properties": { + "rev": { + "type": "string" + }, + "convoId": { + "type": "string" + }, + "message": { + "refs": [ + "#messageView", + "#deletedMessageView" + ], + "type": "union" + }, + "reaction": { + "ref": "#reactionView", + "type": "ref" + }, + "relatedProfiles": { + "type": "array", + "items": { + "ref": "chat.bsky.actor.defs#profileViewBasic", + "type": "ref" + }, + "description": "Profiles referred in the message and reaction views. This isn't required for compatibility, because it was added later, but should generally be present." + } + }, + "description": "Event indicating a reaction was added to a message." + }, + "logMemberLeave": { + "type": "object", + "required": [ + "rev", + "convoId", + "message", + "relatedProfiles" + ], + "properties": { + "rev": { + "type": "string" + }, + "convoId": { + "type": "string" + }, + "message": { + "ref": "#systemMessageView", + "type": "ref", + "description": "A system message with data of type #systemMessageDataMemberLeave" + }, + "relatedProfiles": { + "type": "array", + "items": { + "ref": "chat.bsky.actor.defs#profileViewBasic", + "type": "ref" + }, + "description": "Profiles referred in the system message." + } + }, + "description": "Event indicating a member voluntarily left a group convo. The member who was removed gets a logLeaveConvo (to leave the convo) but not a logMemberLeave (because they already left, so can't see the system message)." + }, + "logReadMessage": { + "type": "object", + "required": [ + "rev", + "convoId", + "message" + ], + "properties": { + "rev": { + "type": "string" + }, + "convoId": { + "type": "string" + }, + "message": { + "refs": [ + "#messageView", + "#deletedMessageView", + "#systemMessageView" + ], + "type": "union" + } + }, + "description": "DEPRECATED: use logReadConvo instead. Event indicating a convo was read up to a certain message." + }, + "logUnlockConvo": { + "type": "object", + "required": [ + "rev", + "convoId", + "message", + "relatedProfiles" + ], + "properties": { + "rev": { + "type": "string" + }, + "convoId": { + "type": "string" + }, + "message": { + "ref": "#systemMessageView", + "type": "ref", + "description": "A system message with data of type #systemMessageDataUnlockConvo" + }, + "relatedProfiles": { + "type": "array", + "items": { + "ref": "chat.bsky.actor.defs#profileViewBasic", + "type": "ref" + }, + "description": "Profiles referred in the system message." + } + }, + "description": "Event indicating a group convo was unlocked." + }, + "logUnmuteConvo": { + "type": "object", + "required": [ + "rev", + "convoId" + ], + "properties": { + "rev": { + "type": "string" + }, + "convoId": { + "type": "string" + } + }, + "description": "Event indicating the viewer unmuted a convo. Can be direct or group." + }, + "convoLockStatus": { + "type": "string", + "knownValues": [ + "unlocked", + "locked", + "locked-permanently" + ] + }, + "logEditJoinLink": { + "type": "object", + "required": [ + "rev", + "convoId", + "message" + ], + "properties": { + "rev": { + "type": "string" + }, + "convoId": { + "type": "string" + }, + "message": { + "ref": "#systemMessageView", + "type": "ref", + "description": "A system message with data of type #systemMessageDataEditJoinLink" + } + }, + "description": "Event indicating a settings about a join link for a group convo were edited." + }, + "logRemoveMember": { + "type": "object", + "required": [ + "rev", + "convoId", + "message", + "relatedProfiles" + ], + "properties": { + "rev": { + "type": "string" + }, + "convoId": { + "type": "string" + }, + "message": { + "ref": "#systemMessageView", + "type": "ref", + "description": "A system message with data of type #systemMessageDataRemoveMember" + }, + "relatedProfiles": { + "type": "array", + "items": { + "ref": "chat.bsky.actor.defs#profileViewBasic", + "type": "ref" + }, + "description": "Profiles referred in the system message." + } + }, + "description": "Event indicating a member was removed from a group convo. The member who was removed gets a logLeaveConvo (to leave the convo) but not a logRemoveMember (because they already left, so can't see the system message)." + }, + "logCreateMessage": { + "type": "object", + "required": [ + "rev", + "convoId", + "message" + ], + "properties": { + "rev": { + "type": "string" + }, + "convoId": { + "type": "string" + }, + "message": { + "refs": [ + "#messageView", + "#deletedMessageView" + ], + "type": "union" + }, + "relatedProfiles": { + "type": "array", + "items": { + "ref": "chat.bsky.actor.defs#profileViewBasic", + "type": "ref" + }, + "description": "Profiles referred to in the message view. This isn't required for compatibility, because it was added later, but should generally be present." + } + }, + "description": "Event indicating a user-originated message was created. Is not emitted for system messages." + }, + "logDeleteMessage": { + "type": "object", + "required": [ + "rev", + "convoId", + "message" + ], + "properties": { + "rev": { + "type": "string" + }, + "convoId": { + "type": "string" + }, + "message": { + "refs": [ + "#messageView", + "#deletedMessageView" + ], + "type": "union" + } + }, + "description": "Event indicating a user-originated message was deleted. Is not emitted for system messages." + }, + "logCreateJoinLink": { + "type": "object", + "required": [ + "rev", + "convoId", + "message" + ], + "properties": { + "rev": { + "type": "string" + }, + "convoId": { + "type": "string" + }, + "message": { + "ref": "#systemMessageView", + "type": "ref", + "description": "A system message with data of type #systemMessageDataCreateJoinLink" + } + }, + "description": "Event indicating a join link was created for a group convo." + }, + "logEnableJoinLink": { + "type": "object", + "required": [ + "rev", + "convoId", + "message" + ], + "properties": { + "rev": { + "type": "string" + }, + "convoId": { + "type": "string" + }, + "message": { + "ref": "#systemMessageView", + "type": "ref", + "description": "A system message with data of type #systemMessageDataEnableJoinLink" + } + }, + "description": "Event indicating a join link was enabled for a group convo." + }, + "logRemoveReaction": { + "type": "object", + "required": [ + "rev", + "convoId", + "message", + "reaction" + ], + "properties": { + "rev": { + "type": "string" + }, + "convoId": { + "type": "string" + }, + "message": { + "refs": [ + "#messageView", + "#deletedMessageView" + ], + "type": "union" + }, + "reaction": { + "ref": "#reactionView", + "type": "ref" + }, + "relatedProfiles": { + "type": "array", + "items": { + "ref": "chat.bsky.actor.defs#profileViewBasic", + "type": "ref" + }, + "description": "Profiles referred in the message and reaction views. This isn't required for compatibility, because it was added later, but should generally be present." + } + }, + "description": "Event indicating a reaction was removed from a message." + }, + "messageViewSender": { + "type": "object", + "required": [ + "did" + ], + "properties": { + "did": { + "type": "string", + "format": "did" + } + } + }, + "systemMessageView": { + "type": "object", + "required": [ + "id", + "rev", + "sentAt", + "data" + ], + "properties": { + "id": { + "type": "string" + }, + "rev": { + "type": "string" + }, + "data": { + "refs": [ + "#systemMessageDataAddMember", + "#systemMessageDataRemoveMember", + "#systemMessageDataMemberJoin", + "#systemMessageDataMemberLeave", + "#systemMessageDataLockConvo", + "#systemMessageDataUnlockConvo", + "#systemMessageDataLockConvoPermanently", + "#systemMessageDataEditGroup", + "#systemMessageDataCreateJoinLink", + "#systemMessageDataEditJoinLink", + "#systemMessageDataEnableJoinLink", + "#systemMessageDataDisableJoinLink" + ], + "type": "union" + }, + "sentAt": { + "type": "string", + "format": "datetime" + } + } + }, + "deletedMessageView": { + "type": "object", + "required": [ + "id", + "rev", + "sender", + "sentAt" + ], + "properties": { + "id": { + "type": "string" + }, + "rev": { + "type": "string" + }, + "sender": { + "ref": "#messageViewSender", + "type": "ref" + }, + "sentAt": { + "type": "string", + "format": "datetime" + } + } + }, + "logDisableJoinLink": { + "type": "object", + "required": [ + "rev", + "convoId", + "message" + ], + "properties": { + "rev": { + "type": "string" + }, + "convoId": { + "type": "string" + }, + "message": { + "ref": "#systemMessageView", + "type": "ref", + "description": "A system message with data of type #systemMessageDataDisableJoinLink" + } + }, + "description": "Event indicating a join link was disabled for a group convo." + }, + "reactionViewSender": { + "type": "object", + "required": [ + "did" + ], + "properties": { + "did": { + "type": "string", + "format": "did" + } + } + }, + "logReadJoinRequests": { + "type": "object", + "required": [ + "rev", + "convoId" + ], + "properties": { + "rev": { + "type": "string" + }, + "convoId": { + "type": "string" + } + }, + "description": "Event indicating the group owner marked join requests as read. Only the owner gets this." + }, + "logRejectJoinRequest": { + "type": "object", + "required": [ + "rev", + "convoId", + "member" + ], + "properties": { + "rev": { + "type": "string" + }, + "member": { + "ref": "chat.bsky.actor.defs#profileViewBasic", + "type": "ref", + "description": "Prospective member who requested to join." + }, + "convoId": { + "type": "string" + } + }, + "description": "Event indicating a join request was rejected by the viewer. Only the owner gets this." + }, + "logApproveJoinRequest": { + "type": "object", + "required": [ + "rev", + "convoId", + "member" + ], + "properties": { + "rev": { + "type": "string" + }, + "member": { + "ref": "chat.bsky.actor.defs#profileViewBasic", + "type": "ref", + "description": "Prospective member who requested to join." + }, + "convoId": { + "type": "string" + } + }, + "description": "Event indicating a join request was approved by the viewer. Only the owner gets this. The approved member gets a logBeginConvo." + }, + "logIncomingJoinRequest": { + "type": "object", + "required": [ + "rev", + "convoId", + "member" + ], + "properties": { + "rev": { + "type": "string" + }, + "member": { + "ref": "chat.bsky.actor.defs#profileViewBasic", + "type": "ref", + "description": "Prospective member who requested to join." + }, + "convoId": { + "type": "string" + } + }, + "description": "Event indicating a join request was made to a group the viewer owns. Only the owner gets this." + }, + "logOutgoingJoinRequest": { + "type": "object", + "required": [ + "rev", + "convoId" + ], + "properties": { + "rev": { + "type": "string" + }, + "convoId": { + "type": "string" + } + }, + "description": "Event indicating a join request was made by the requester. Only requester actor gets this." + }, + "messageAndReactionView": { + "type": "object", + "required": [ + "message", + "reaction" + ], + "properties": { + "message": { + "ref": "#messageView", + "type": "ref" + }, + "reaction": { + "ref": "#reactionView", + "type": "ref" + } + } + }, + "logLockConvoPermanently": { + "type": "object", + "required": [ + "rev", + "convoId", + "message", + "relatedProfiles" + ], + "properties": { + "rev": { + "type": "string" + }, + "convoId": { + "type": "string" + }, + "message": { + "ref": "#systemMessageView", + "type": "ref", + "description": "A system message with data of type #systemMessageDataLockConvoPermanently" + }, + "relatedProfiles": { + "type": "array", + "items": { + "ref": "chat.bsky.actor.defs#profileViewBasic", + "type": "ref" + }, + "description": "Profiles referred in the system message." + } + }, + "description": "Event indicating a group convo was locked permanently." + }, + "systemMessageReferredUser": { + "type": "object", + "required": [ + "did" + ], + "properties": { + "did": { + "type": "string", + "format": "did" + } + } + }, + "systemMessageDataAddMember": { + "type": "object", + "required": [ + "member", + "role", + "addedBy" + ], + "properties": { + "role": { + "ref": "chat.bsky.actor.defs#memberRole", + "type": "ref", + "description": "Role the user was added to the group with. The role from 'member' will reflect the current data, not historical." + }, + "member": { + "ref": "#systemMessageReferredUser", + "type": "ref", + "description": "Current view of the member who was added." + }, + "addedBy": { + "ref": "#systemMessageReferredUser", + "type": "ref" + } + }, + "description": "System message indicating a user was added to the group convo." + }, + "systemMessageDataEditGroup": { + "type": "object", + "properties": { + "newName": { + "type": "string", + "description": "Group name that replaced the old." + }, + "oldName": { + "type": "string", + "description": "Group name that was replaced." + } + }, + "description": "System message indicating the group info was edited." + }, + "systemMessageDataLockConvo": { + "type": "object", + "required": [ + "lockedBy" + ], + "properties": { + "lockedBy": { + "ref": "#systemMessageReferredUser", + "type": "ref", + "description": "Current view of the member who locked the group." + } + }, + "description": "System message indicating the group convo was locked." + }, + "systemMessageDataMemberJoin": { + "type": "object", + "required": [ + "member", + "role" + ], + "properties": { + "role": { + "ref": "chat.bsky.actor.defs#memberRole", + "type": "ref", + "description": "Role the user was added to the group with. The role from 'member' will reflect the current data, not historical." + }, + "member": { + "ref": "#systemMessageReferredUser", + "type": "ref", + "description": "Current view of the member who joined." + }, + "approvedBy": { + "ref": "#systemMessageReferredUser", + "type": "ref", + "description": "If join link was configured to require approval, this will be set to who approved the request. Undefined if approval was not required." + } + }, + "description": "System message indicating a user joined the group convo via join link." + }, + "systemMessageDataMemberLeave": { + "type": "object", + "required": [ + "member" + ], + "properties": { + "member": { + "ref": "#systemMessageReferredUser", + "type": "ref", + "description": "Current view of the member who left the group." + } + }, + "description": "System message indicating a user voluntarily left the group convo." + }, + "systemMessageDataUnlockConvo": { + "type": "object", + "required": [ + "unlockedBy" + ], + "properties": { + "unlockedBy": { + "ref": "#systemMessageReferredUser", + "type": "ref", + "description": "Current view of the member who unlocked the group." + } + }, + "description": "System message indicating the group convo was unlocked." + }, + "systemMessageDataEditJoinLink": { + "type": "object", + "properties": {}, + "description": "System message indicating the group join link was edited." + }, + "systemMessageDataRemoveMember": { + "type": "object", + "required": [ + "member", + "removedBy" + ], + "properties": { + "member": { + "ref": "#systemMessageReferredUser", + "type": "ref", + "description": "Current view of the member who was removed." + }, + "removedBy": { + "ref": "#systemMessageReferredUser", + "type": "ref" + } + }, + "description": "System message indicating a user was removed from the group convo." + }, + "logWithdrawIncomingJoinRequest": { + "type": "object", + "required": [ + "rev", + "convoId", + "member" + ], + "properties": { + "rev": { + "type": "string" + }, + "member": { + "ref": "chat.bsky.actor.defs#profileViewBasic", + "type": "ref", + "description": "Prospective member who withdrew their join request." + }, + "convoId": { + "type": "string" + } + }, + "description": "Event indicating a prospective member withdrew their join request. Only the owner gets this." + }, + "logWithdrawOutgoingJoinRequest": { + "type": "object", + "required": [ + "rev", + "convoId" + ], + "properties": { + "rev": { + "type": "string" + }, + "convoId": { + "type": "string" + } + }, + "description": "Event indicating the viewer withdrew their own join request. Only requester actor gets this." + }, + "systemMessageDataCreateJoinLink": { + "type": "object", + "properties": {}, + "description": "System message indicating the group join link was created." + }, + "systemMessageDataEnableJoinLink": { + "type": "object", + "properties": {}, + "description": "System message indicating the group join link was enabled." + }, + "messageBeforeUserJoinedGroupView": { + "type": "object", + "properties": {}, + "description": "Placeholder embedded in place of a reply's parent message when that parent was sent before the viewer joined the group convo. The viewer has no access to that history, so no message data is carried." + }, + "systemMessageDataDisableJoinLink": { + "type": "object", + "properties": {}, + "description": "System message indicating the group join link was disabled." + }, + "systemMessageDataLockConvoPermanently": { + "type": "object", + "required": [ + "lockedBy" + ], + "properties": { + "lockedBy": { + "ref": "#systemMessageReferredUser", + "type": "ref", + "description": "Current view of the member who locked the group." + } + }, + "description": "System message indicating the group convo was locked permanently." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/chat/bsky/convo/deleteMessageForSelf.json b/lexicons/chat/bsky/convo/deleteMessageForSelf.json new file mode 100644 index 0000000000..39e4c109a0 --- /dev/null +++ b/lexicons/chat/bsky/convo/deleteMessageForSelf.json @@ -0,0 +1,45 @@ +{ + "id": "chat.bsky.convo.deleteMessageForSelf", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "convoId", + "messageId" + ], + "properties": { + "convoId": { + "type": "string" + }, + "messageId": { + "type": "string" + } + } + }, + "encoding": "application/json" + }, + "errors": [ + { + "name": "InvalidConvo" + }, + { + "name": "MessageDeleteNotAllowed", + "description": "Indicates that this message cannot be deleted, e.g. because it is a system message." + } + ], + "output": { + "schema": { + "ref": "chat.bsky.convo.defs#deletedMessageView", + "type": "ref" + }, + "encoding": "application/json" + }, + "description": "Marks a message as deleted for the viewer, so they won't see that message in future enumerations." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/chat/bsky/convo/getConvo.json b/lexicons/chat/bsky/convo/getConvo.json new file mode 100644 index 0000000000..cbe3b2684e --- /dev/null +++ b/lexicons/chat/bsky/convo/getConvo.json @@ -0,0 +1,42 @@ +{ + "id": "chat.bsky.convo.getConvo", + "defs": { + "main": { + "type": "query", + "errors": [ + { + "name": "InvalidConvo" + } + ], + "output": { + "schema": { + "type": "object", + "required": [ + "convo" + ], + "properties": { + "convo": { + "ref": "chat.bsky.convo.defs#convoView", + "type": "ref" + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "convoId" + ], + "properties": { + "convoId": { + "type": "string" + } + } + }, + "description": "Gets an existing conversation by its ID." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/chat/bsky/convo/getConvoAvailability.json b/lexicons/chat/bsky/convo/getConvoAvailability.json new file mode 100644 index 0000000000..37c3c745cf --- /dev/null +++ b/lexicons/chat/bsky/convo/getConvoAvailability.json @@ -0,0 +1,46 @@ +{ + "id": "chat.bsky.convo.getConvoAvailability", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "canChat" + ], + "properties": { + "convo": { + "ref": "chat.bsky.convo.defs#convoView", + "type": "ref" + }, + "canChat": { + "type": "boolean" + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "members" + ], + "properties": { + "members": { + "type": "array", + "items": { + "type": "string", + "format": "did" + }, + "maxLength": 10, + "minLength": 1 + } + } + }, + "description": "Check whether the requester and the other members can start a 1-1 chat. Only applicable to direct (non-group) conversations. If an existing convo is found for these members, it is returned. Does not create a new convo if it doesn't exist." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/chat/bsky/convo/getConvoForMembers.json b/lexicons/chat/bsky/convo/getConvoForMembers.json new file mode 100644 index 0000000000..21c12ead32 --- /dev/null +++ b/lexicons/chat/bsky/convo/getConvoForMembers.json @@ -0,0 +1,63 @@ +{ + "id": "chat.bsky.convo.getConvoForMembers", + "defs": { + "main": { + "type": "query", + "errors": [ + { + "name": "AccountSuspended" + }, + { + "name": "BlockedActor" + }, + { + "name": "BlockedSubject" + }, + { + "name": "MessagesDisabled" + }, + { + "name": "NotFollowedBySender" + }, + { + "name": "RecipientNotFound" + } + ], + "output": { + "schema": { + "type": "object", + "required": [ + "convo" + ], + "properties": { + "convo": { + "ref": "chat.bsky.convo.defs#convoView", + "type": "ref" + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "members" + ], + "properties": { + "members": { + "type": "array", + "items": { + "type": "string", + "format": "did" + }, + "maxLength": 10, + "minLength": 1 + } + } + }, + "description": "Get or create a 1-1 conversation for the given members. Always returns the same direct (non-group) conversation. To create a group conversation, use createGroup." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/chat/bsky/convo/getConvoMembers.json b/lexicons/chat/bsky/convo/getConvoMembers.json new file mode 100644 index 0000000000..0e89f0e916 --- /dev/null +++ b/lexicons/chat/bsky/convo/getConvoMembers.json @@ -0,0 +1,57 @@ +{ + "id": "chat.bsky.convo.getConvoMembers", + "defs": { + "main": { + "type": "query", + "errors": [ + { + "name": "InvalidConvo" + } + ], + "output": { + "schema": { + "type": "object", + "required": [ + "members" + ], + "properties": { + "cursor": { + "type": "string" + }, + "members": { + "type": "array", + "items": { + "ref": "chat.bsky.actor.defs#profileViewBasic", + "type": "ref" + } + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "convoId" + ], + "properties": { + "limit": { + "type": "integer", + "default": 50, + "maximum": 100, + "minimum": 1 + }, + "cursor": { + "type": "string" + }, + "convoId": { + "type": "string" + } + } + }, + "description": "Returns a paginated list of members from a conversation." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/chat/bsky/convo/getLog.json b/lexicons/chat/bsky/convo/getLog.json new file mode 100644 index 0000000000..42c738d46d --- /dev/null +++ b/lexicons/chat/bsky/convo/getLog.json @@ -0,0 +1,71 @@ +{ + "id": "chat.bsky.convo.getLog", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "logs" + ], + "properties": { + "logs": { + "type": "array", + "items": { + "refs": [ + "chat.bsky.convo.defs#logBeginConvo", + "chat.bsky.convo.defs#logAcceptConvo", + "chat.bsky.convo.defs#logLeaveConvo", + "chat.bsky.convo.defs#logMuteConvo", + "chat.bsky.convo.defs#logUnmuteConvo", + "chat.bsky.convo.defs#logCreateMessage", + "chat.bsky.convo.defs#logDeleteMessage", + "chat.bsky.convo.defs#logReadMessage", + "chat.bsky.convo.defs#logAddReaction", + "chat.bsky.convo.defs#logRemoveReaction", + "chat.bsky.convo.defs#logReadConvo", + "chat.bsky.convo.defs#logAddMember", + "chat.bsky.convo.defs#logRemoveMember", + "chat.bsky.convo.defs#logMemberJoin", + "chat.bsky.convo.defs#logMemberLeave", + "chat.bsky.convo.defs#logLockConvo", + "chat.bsky.convo.defs#logUnlockConvo", + "chat.bsky.convo.defs#logLockConvoPermanently", + "chat.bsky.convo.defs#logEditGroup", + "chat.bsky.convo.defs#logCreateJoinLink", + "chat.bsky.convo.defs#logEditJoinLink", + "chat.bsky.convo.defs#logEnableJoinLink", + "chat.bsky.convo.defs#logDisableJoinLink", + "chat.bsky.convo.defs#logIncomingJoinRequest", + "chat.bsky.convo.defs#logApproveJoinRequest", + "chat.bsky.convo.defs#logRejectJoinRequest", + "chat.bsky.convo.defs#logOutgoingJoinRequest", + "chat.bsky.convo.defs#logWithdrawIncomingJoinRequest", + "chat.bsky.convo.defs#logWithdrawOutgoingJoinRequest", + "chat.bsky.convo.defs#logReadJoinRequests" + ], + "type": "union" + } + }, + "cursor": { + "type": "string" + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [], + "properties": { + "cursor": { + "type": "string" + } + } + } + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/chat/bsky/convo/getMessages.json b/lexicons/chat/bsky/convo/getMessages.json new file mode 100644 index 0000000000..b157290de8 --- /dev/null +++ b/lexicons/chat/bsky/convo/getMessages.json @@ -0,0 +1,69 @@ +{ + "id": "chat.bsky.convo.getMessages", + "defs": { + "main": { + "type": "query", + "errors": [ + { + "name": "InvalidConvo" + } + ], + "output": { + "schema": { + "type": "object", + "required": [ + "messages" + ], + "properties": { + "cursor": { + "type": "string" + }, + "messages": { + "type": "array", + "items": { + "refs": [ + "chat.bsky.convo.defs#messageView", + "chat.bsky.convo.defs#deletedMessageView", + "chat.bsky.convo.defs#systemMessageView" + ], + "type": "union" + } + }, + "relatedProfiles": { + "type": "array", + "items": { + "ref": "chat.bsky.actor.defs#profileViewBasic", + "type": "ref" + }, + "description": "Set of all members who authored or reacted to the returned messages. Members referred to by system messages are also included." + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "convoId" + ], + "properties": { + "limit": { + "type": "integer", + "default": 50, + "maximum": 100, + "minimum": 1 + }, + "cursor": { + "type": "string" + }, + "convoId": { + "type": "string" + } + } + }, + "description": "Returns a page of messages from a conversation." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/chat/bsky/convo/getUnreadCounts.json b/lexicons/chat/bsky/convo/getUnreadCounts.json new file mode 100644 index 0000000000..ddfe955a2b --- /dev/null +++ b/lexicons/chat/bsky/convo/getUnreadCounts.json @@ -0,0 +1,41 @@ +{ + "id": "chat.bsky.convo.getUnreadCounts", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "unreadAcceptedConvos", + "unreadRequestConvos" + ], + "properties": { + "unreadRequestConvos": { + "type": "integer", + "description": "Number of unread, unlocked request convos. Includes convos with unread messages, but not with unread join request, since only the owner of a group has join requests to read, and the group would necessarily be accepted. Capped at 100, where 100 means more than 99." + }, + "unreadAcceptedConvos": { + "type": "integer", + "description": "Number of unread, unlocked accepted convos. Counts convos with unread messages and unread join requests. Capped at 100, where 100 means more than 99." + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "properties": { + "includeGroupChats": { + "type": "boolean", + "default": true, + "description": "When false, group convos are excluded from the counts." + } + } + }, + "description": "Returns unread conversation counts for conversations that are unlocked, not muted, split by convo status. Direct convos are excluded when a block relationship exists between the actor and the other member, or when the other member's account is deleted or deactivated. Group convos are considered unread if they have unread join request counts." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/chat/bsky/convo/leaveConvo.json b/lexicons/chat/bsky/convo/leaveConvo.json new file mode 100644 index 0000000000..b1e74ca2d2 --- /dev/null +++ b/lexicons/chat/bsky/convo/leaveConvo.json @@ -0,0 +1,52 @@ +{ + "id": "chat.bsky.convo.leaveConvo", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "convoId" + ], + "properties": { + "convoId": { + "type": "string" + } + } + }, + "encoding": "application/json" + }, + "errors": [ + { + "name": "InvalidConvo" + }, + { + "name": "OwnerCannotLeave", + "description": "The owner of a group conversation cannot leave before locking the group." + } + ], + "output": { + "schema": { + "type": "object", + "required": [ + "convoId", + "rev" + ], + "properties": { + "rev": { + "type": "string" + }, + "convoId": { + "type": "string" + } + } + }, + "encoding": "application/json" + }, + "description": "Leaves a conversation (direct or group). For group, this effectively removes membership. For direct, membership is never removed, only changed to remove from enumerations by the user who left." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/chat/bsky/convo/listConvoRequests.json b/lexicons/chat/bsky/convo/listConvoRequests.json new file mode 100644 index 0000000000..6be7fea5fc --- /dev/null +++ b/lexicons/chat/bsky/convo/listConvoRequests.json @@ -0,0 +1,49 @@ +{ + "id": "chat.bsky.convo.listConvoRequests", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "requests" + ], + "properties": { + "cursor": { + "type": "string" + }, + "requests": { + "type": "array", + "items": { + "refs": [ + "chat.bsky.convo.defs#convoView", + "chat.bsky.group.defs#joinRequestConvoView" + ], + "type": "union" + } + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "properties": { + "limit": { + "type": "integer", + "default": 50, + "maximum": 100, + "minimum": 1 + }, + "cursor": { + "type": "string" + } + } + }, + "description": "Returns a page of incoming conversation requests for the user. Direct convo requests are returned as convoView; group join requests made by the user are returned as joinRequestConvoView." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/chat/bsky/convo/listConvos.json b/lexicons/chat/bsky/convo/listConvos.json new file mode 100644 index 0000000000..2ac41d00b3 --- /dev/null +++ b/lexicons/chat/bsky/convo/listConvos.json @@ -0,0 +1,77 @@ +{ + "id": "chat.bsky.convo.listConvos", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "convos" + ], + "properties": { + "convos": { + "type": "array", + "items": { + "ref": "chat.bsky.convo.defs#convoView", + "type": "ref" + } + }, + "cursor": { + "type": "string" + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "properties": { + "kind": { + "type": "string", + "description": "Filter by conversation kind.", + "knownValues": [ + "direct", + "group" + ] + }, + "limit": { + "type": "integer", + "default": 50, + "maximum": 100, + "minimum": 1 + }, + "cursor": { + "type": "string" + }, + "status": { + "type": "string", + "description": "Filter convos by their status. It is discouraged to call with \"request\" and preferred to call chat.bsky.convo.listConvoRequests, which also includes group join requests made by the user.", + "knownValues": [ + "request", + "accepted" + ] + }, + "readState": { + "type": "string", + "knownValues": [ + "unread" + ] + }, + "lockStatus": { + "type": "string", + "description": "Filter by conversation lock status. Values follow chat.bsky.convo.defs#convoLockStatus.", + "knownValues": [ + "unlocked", + "locked", + "locked-permanently" + ] + } + } + }, + "description": "Returns a page of conversations (direct or group) for the user." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/chat/bsky/convo/lockConvo.json b/lexicons/chat/bsky/convo/lockConvo.json new file mode 100644 index 0000000000..fb0177b0b6 --- /dev/null +++ b/lexicons/chat/bsky/convo/lockConvo.json @@ -0,0 +1,51 @@ +{ + "id": "chat.bsky.convo.lockConvo", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "convoId" + ], + "properties": { + "convoId": { + "type": "string" + } + } + }, + "encoding": "application/json" + }, + "errors": [ + { + "name": "ConvoLocked" + }, + { + "name": "InvalidConvo" + }, + { + "name": "InsufficientRole" + } + ], + "output": { + "schema": { + "type": "object", + "required": [ + "convo" + ], + "properties": { + "convo": { + "ref": "chat.bsky.convo.defs#convoView", + "type": "ref" + } + } + }, + "encoding": "application/json" + }, + "description": "Locks a group convo so no more content (messages, reactions) can be added to it." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/chat/bsky/convo/muteConvo.json b/lexicons/chat/bsky/convo/muteConvo.json new file mode 100644 index 0000000000..091136aaf6 --- /dev/null +++ b/lexicons/chat/bsky/convo/muteConvo.json @@ -0,0 +1,45 @@ +{ + "id": "chat.bsky.convo.muteConvo", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "convoId" + ], + "properties": { + "convoId": { + "type": "string" + } + } + }, + "encoding": "application/json" + }, + "errors": [ + { + "name": "InvalidConvo" + } + ], + "output": { + "schema": { + "type": "object", + "required": [ + "convo" + ], + "properties": { + "convo": { + "ref": "chat.bsky.convo.defs#convoView", + "type": "ref" + } + } + }, + "encoding": "application/json" + }, + "description": "Mutes a conversation, preventing notifications related to it." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/chat/bsky/convo/removeReaction.json b/lexicons/chat/bsky/convo/removeReaction.json new file mode 100644 index 0000000000..8bcdd28e21 --- /dev/null +++ b/lexicons/chat/bsky/convo/removeReaction.json @@ -0,0 +1,69 @@ +{ + "id": "chat.bsky.convo.removeReaction", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "convoId", + "messageId", + "value" + ], + "properties": { + "value": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "maxGraphemes": 1, + "minGraphemes": 1 + }, + "convoId": { + "type": "string" + }, + "messageId": { + "type": "string" + } + } + }, + "encoding": "application/json" + }, + "errors": [ + { + "name": "InvalidConvo" + }, + { + "name": "ReactionNotAllowed", + "description": "Indicates that reactions are not allowed on this message, e.g. because it is a system message." + }, + { + "name": "ReactionMessageDeleted", + "description": "Indicates that the message has been deleted and reactions can no longer be added/removed." + }, + { + "name": "ReactionInvalidValue", + "description": "Indicates the value for the reaction is not acceptable. In general, this means it is not an emoji." + } + ], + "output": { + "schema": { + "type": "object", + "required": [ + "message" + ], + "properties": { + "message": { + "ref": "chat.bsky.convo.defs#messageView", + "type": "ref" + } + } + }, + "encoding": "application/json" + }, + "description": "Removes an emoji reaction from a message. Requires authentication. It is idempotent, so multiple calls from the same user with the same emoji result in that reaction not being present, even if it already wasn't." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/chat/bsky/convo/sendMessage.json b/lexicons/chat/bsky/convo/sendMessage.json new file mode 100644 index 0000000000..612bc292ca --- /dev/null +++ b/lexicons/chat/bsky/convo/sendMessage.json @@ -0,0 +1,48 @@ +{ + "id": "chat.bsky.convo.sendMessage", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "convoId", + "message" + ], + "properties": { + "convoId": { + "type": "string" + }, + "message": { + "ref": "chat.bsky.convo.defs#messageInput", + "type": "ref" + } + } + }, + "encoding": "application/json" + }, + "errors": [ + { + "name": "ConvoLocked" + }, + { + "name": "InvalidConvo" + }, + { + "name": "ReplyTargetNotFound" + } + ], + "output": { + "schema": { + "ref": "chat.bsky.convo.defs#messageView", + "type": "ref" + }, + "encoding": "application/json" + }, + "description": "Sends a message to a conversation." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/chat/bsky/convo/sendMessageBatch.json b/lexicons/chat/bsky/convo/sendMessageBatch.json new file mode 100644 index 0000000000..843443f167 --- /dev/null +++ b/lexicons/chat/bsky/convo/sendMessageBatch.json @@ -0,0 +1,75 @@ +{ + "id": "chat.bsky.convo.sendMessageBatch", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "items" + ], + "properties": { + "items": { + "type": "array", + "items": { + "ref": "#batchItem", + "type": "ref" + }, + "maxLength": 100 + } + } + }, + "encoding": "application/json" + }, + "errors": [ + { + "name": "ConvoLocked" + }, + { + "name": "InvalidConvo" + }, + { + "name": "ReplyTargetNotFound" + } + ], + "output": { + "schema": { + "type": "object", + "required": [ + "items" + ], + "properties": { + "items": { + "type": "array", + "items": { + "ref": "chat.bsky.convo.defs#messageView", + "type": "ref" + } + } + } + }, + "encoding": "application/json" + }, + "description": "Sends a batch of messages to a conversation." + }, + "batchItem": { + "type": "object", + "required": [ + "convoId", + "message" + ], + "properties": { + "convoId": { + "type": "string" + }, + "message": { + "ref": "chat.bsky.convo.defs#messageInput", + "type": "ref" + } + } + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/chat/bsky/convo/unlockConvo.json b/lexicons/chat/bsky/convo/unlockConvo.json new file mode 100644 index 0000000000..83c3c8fe8a --- /dev/null +++ b/lexicons/chat/bsky/convo/unlockConvo.json @@ -0,0 +1,51 @@ +{ + "id": "chat.bsky.convo.unlockConvo", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "convoId" + ], + "properties": { + "convoId": { + "type": "string" + } + } + }, + "encoding": "application/json" + }, + "errors": [ + { + "name": "InvalidConvo" + }, + { + "name": "InsufficientRole" + }, + { + "name": "ConvoLockedByModeration" + } + ], + "output": { + "schema": { + "type": "object", + "required": [ + "convo" + ], + "properties": { + "convo": { + "ref": "chat.bsky.convo.defs#convoView", + "type": "ref" + } + } + }, + "encoding": "application/json" + }, + "description": "Unlocks a group convo so it is able to receive new content." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/chat/bsky/convo/unmuteConvo.json b/lexicons/chat/bsky/convo/unmuteConvo.json new file mode 100644 index 0000000000..813146cd53 --- /dev/null +++ b/lexicons/chat/bsky/convo/unmuteConvo.json @@ -0,0 +1,45 @@ +{ + "id": "chat.bsky.convo.unmuteConvo", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "convoId" + ], + "properties": { + "convoId": { + "type": "string" + } + } + }, + "encoding": "application/json" + }, + "errors": [ + { + "name": "InvalidConvo" + } + ], + "output": { + "schema": { + "type": "object", + "required": [ + "convo" + ], + "properties": { + "convo": { + "ref": "chat.bsky.convo.defs#convoView", + "type": "ref" + } + } + }, + "encoding": "application/json" + }, + "description": "Unmutes a conversation, allowing notifications related to it." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/chat/bsky/convo/updateAllRead.json b/lexicons/chat/bsky/convo/updateAllRead.json new file mode 100644 index 0000000000..288e873196 --- /dev/null +++ b/lexicons/chat/bsky/convo/updateAllRead.json @@ -0,0 +1,41 @@ +{ + "id": "chat.bsky.convo.updateAllRead", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "properties": { + "status": { + "type": "string", + "knownValues": [ + "request", + "accepted" + ] + } + } + }, + "encoding": "application/json" + }, + "output": { + "schema": { + "type": "object", + "required": [ + "updatedCount" + ], + "properties": { + "updatedCount": { + "type": "integer", + "description": "The count of updated convos." + } + } + }, + "encoding": "application/json" + }, + "description": "Sets conversations from a user as read to the latest message, with filters." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/chat/bsky/convo/updateRead.json b/lexicons/chat/bsky/convo/updateRead.json new file mode 100644 index 0000000000..1aa6d1152c --- /dev/null +++ b/lexicons/chat/bsky/convo/updateRead.json @@ -0,0 +1,48 @@ +{ + "id": "chat.bsky.convo.updateRead", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "convoId" + ], + "properties": { + "convoId": { + "type": "string" + }, + "messageId": { + "type": "string" + } + } + }, + "encoding": "application/json" + }, + "errors": [ + { + "name": "InvalidConvo" + } + ], + "output": { + "schema": { + "type": "object", + "required": [ + "convo" + ], + "properties": { + "convo": { + "ref": "chat.bsky.convo.defs#convoView", + "type": "ref" + } + } + }, + "encoding": "application/json" + }, + "description": "Updates the read state of a conversation from, optionally specifying the last read message." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/chat/bsky/embed/joinLink.json b/lexicons/chat/bsky/embed/joinLink.json new file mode 100644 index 0000000000..0dc09657b9 --- /dev/null +++ b/lexicons/chat/bsky/embed/joinLink.json @@ -0,0 +1,36 @@ +{ + "id": "chat.bsky.embed.joinLink", + "defs": { + "main": { + "type": "object", + "required": [ + "code" + ], + "properties": { + "code": { + "type": "string", + "description": "The join link code." + } + } + }, + "view": { + "type": "object", + "required": [ + "joinLinkPreview" + ], + "properties": { + "joinLinkPreview": { + "refs": [ + "chat.bsky.group.defs#joinLinkPreviewView", + "chat.bsky.group.defs#disabledJoinLinkPreviewView", + "chat.bsky.group.defs#invalidJoinLinkPreviewView" + ], + "type": "union" + } + } + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1, + "description": "A join link embedded in a chat message." +} diff --git a/lexicons/chat/bsky/group/addMembers.json b/lexicons/chat/bsky/group/addMembers.json new file mode 100644 index 0000000000..39a14a5b83 --- /dev/null +++ b/lexicons/chat/bsky/group/addMembers.json @@ -0,0 +1,88 @@ +{ + "id": "chat.bsky.group.addMembers", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "convoId", + "members" + ], + "properties": { + "convoId": { + "type": "string" + }, + "members": { + "type": "array", + "items": { + "type": "string", + "format": "did" + }, + "minLength": 1 + } + } + }, + "encoding": "application/json" + }, + "errors": [ + { + "name": "AccountSuspended" + }, + { + "name": "BlockedActor" + }, + { + "name": "BlockedSubject" + }, + { + "name": "ConvoLocked" + }, + { + "name": "InsufficientRole" + }, + { + "name": "InvalidConvo" + }, + { + "name": "MemberLimitReached" + }, + { + "name": "NotFollowedBySender" + }, + { + "name": "RecipientNotFound" + }, + { + "name": "UserForbidsGroups" + } + ], + "output": { + "schema": { + "type": "object", + "required": [ + "convo" + ], + "properties": { + "convo": { + "ref": "chat.bsky.convo.defs#convoView", + "type": "ref" + }, + "addedMembers": { + "type": "array", + "items": { + "ref": "chat.bsky.actor.defs#profileViewBasic", + "type": "ref" + } + } + } + }, + "encoding": "application/json" + }, + "description": "Adds members to a group. The members are added in 'request' status, so they have to accept it. This creates convo memberships." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/chat/bsky/group/approveJoinRequest.json b/lexicons/chat/bsky/group/approveJoinRequest.json new file mode 100644 index 0000000000..c8186db259 --- /dev/null +++ b/lexicons/chat/bsky/group/approveJoinRequest.json @@ -0,0 +1,56 @@ +{ + "id": "chat.bsky.group.approveJoinRequest", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "convoId", + "member" + ], + "properties": { + "member": { + "type": "string", + "format": "did" + }, + "convoId": { + "type": "string" + } + } + }, + "encoding": "application/json" + }, + "errors": [ + { + "name": "InvalidConvo" + }, + { + "name": "InsufficientRole" + }, + { + "name": "MemberLimitReached" + } + ], + "output": { + "schema": { + "type": "object", + "required": [ + "convo" + ], + "properties": { + "convo": { + "ref": "chat.bsky.convo.defs#convoView", + "type": "ref" + } + } + }, + "encoding": "application/json" + }, + "description": "Approves a request to join a group (via join link) the user owns. Action taken by the group owner." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/chat/bsky/group/createGroup.json b/lexicons/chat/bsky/group/createGroup.json new file mode 100644 index 0000000000..efe5e72041 --- /dev/null +++ b/lexicons/chat/bsky/group/createGroup.json @@ -0,0 +1,76 @@ +{ + "id": "chat.bsky.group.createGroup", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "members", + "name" + ], + "properties": { + "name": { + "type": "string", + "maxLength": 500, + "minLength": 1, + "maxGraphemes": 50 + }, + "members": { + "type": "array", + "items": { + "type": "string", + "format": "did" + }, + "maxLength": 10000, + "description": "The members to add to the group. The owner is automatically added. Implementations may enforce a lower maximum than the 10,000-item schema limit; Bluesky currently supports up to 100 total members. If the owner is included in this list, the list may contain up to the implementation's total member limit. Otherwise, it may contain one fewer." + } + } + }, + "encoding": "application/json" + }, + "errors": [ + { + "name": "AccountSuspended" + }, + { + "name": "BlockedActor" + }, + { + "name": "BlockedSubject" + }, + { + "name": "NewAccountCannotCreateGroup" + }, + { + "name": "NotFollowedBySender" + }, + { + "name": "RecipientNotFound" + }, + { + "name": "UserForbidsGroups" + } + ], + "output": { + "schema": { + "type": "object", + "required": [ + "convo" + ], + "properties": { + "convo": { + "ref": "chat.bsky.convo.defs#convoView", + "type": "ref" + } + } + }, + "encoding": "application/json" + }, + "description": "Creates a group convo, specifying the members to be added to it. Unlike getConvoForMembers, this isn't idempotent. It will create new groups even if the membership is identical to pre-existing groups. Will create 'request' membership for all members, except the owner who is 'accepted'." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/chat/bsky/group/createJoinLink.json b/lexicons/chat/bsky/group/createJoinLink.json new file mode 100644 index 0000000000..679f1bc3b3 --- /dev/null +++ b/lexicons/chat/bsky/group/createJoinLink.json @@ -0,0 +1,60 @@ +{ + "id": "chat.bsky.group.createJoinLink", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "convoId", + "joinRule" + ], + "properties": { + "convoId": { + "type": "string" + }, + "joinRule": { + "ref": "chat.bsky.group.defs#joinRule", + "type": "ref" + }, + "requireApproval": { + "type": "boolean", + "default": false + } + } + }, + "encoding": "application/json" + }, + "errors": [ + { + "name": "EnabledJoinLinkAlreadyExists" + }, + { + "name": "InvalidConvo" + }, + { + "name": "InsufficientRole" + } + ], + "output": { + "schema": { + "type": "object", + "required": [ + "joinLink" + ], + "properties": { + "joinLink": { + "ref": "chat.bsky.group.defs#joinLinkView", + "type": "ref" + } + } + }, + "encoding": "application/json" + }, + "description": "Creates a join link for the group convo." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/chat/bsky/group/defs.json b/lexicons/chat/bsky/group/defs.json new file mode 100644 index 0000000000..3b1cc4bef3 --- /dev/null +++ b/lexicons/chat/bsky/group/defs.json @@ -0,0 +1,192 @@ +{ + "id": "chat.bsky.group.defs", + "defs": { + "joinRule": { + "type": "string", + "knownValues": [ + "anyone", + "followedByOwner" + ] + }, + "joinLinkView": { + "type": "object", + "required": [ + "code", + "enabledStatus", + "requireApproval", + "joinRule", + "createdAt" + ], + "properties": { + "code": { + "type": "string" + }, + "joinRule": { + "ref": "#joinRule", + "type": "ref" + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "enabledStatus": { + "ref": "#linkEnabledStatus", + "type": "ref" + }, + "requireApproval": { + "type": "boolean" + } + }, + "description": "Join link view to be used within a group view, so the convo is surrounding, not specified inside this view." + }, + "joinRequestView": { + "type": "object", + "required": [ + "convoId", + "requestedBy", + "requestedAt" + ], + "properties": { + "convoId": { + "type": "string" + }, + "requestedAt": { + "type": "string", + "format": "datetime" + }, + "requestedBy": { + "ref": "chat.bsky.actor.defs#profileViewBasic", + "type": "ref" + } + }, + "description": "A join request from the perspective of the group owner." + }, + "linkEnabledStatus": { + "type": "string", + "knownValues": [ + "enabled", + "disabled" + ] + }, + "joinLinkPreviewView": { + "type": "object", + "required": [ + "convoId", + "code", + "name", + "owner", + "memberCount", + "memberLimit", + "requireApproval", + "joinRule" + ], + "properties": { + "code": { + "type": "string" + }, + "name": { + "type": "string" + }, + "convo": { + "ref": "chat.bsky.convo.defs#convoView", + "type": "ref", + "description": "Present only if the request is authenticated and the user is a member of the group." + }, + "owner": { + "ref": "chat.bsky.actor.defs#profileViewBasic", + "type": "ref" + }, + "viewer": { + "ref": "#joinLinkViewerState", + "type": "ref" + }, + "convoId": { + "type": "string" + }, + "joinRule": { + "ref": "#joinRule", + "type": "ref" + }, + "memberCount": { + "type": "integer" + }, + "memberLimit": { + "type": "integer" + }, + "requireApproval": { + "type": "boolean" + } + }, + "description": "Preview that can be shown in feeds, including to unauthenticated viewers." + }, + "joinLinkViewerState": { + "type": "object", + "properties": { + "requestedAt": { + "type": "string", + "format": "datetime" + } + } + }, + "joinRequestConvoView": { + "type": "object", + "required": [ + "convoId", + "name", + "owner", + "memberCount", + "memberLimit", + "viewer" + ], + "properties": { + "name": { + "type": "string" + }, + "owner": { + "ref": "chat.bsky.actor.defs#profileViewBasic", + "type": "ref" + }, + "viewer": { + "ref": "#joinLinkViewerState", + "type": "ref" + }, + "convoId": { + "type": "string" + }, + "memberCount": { + "type": "integer" + }, + "memberLimit": { + "type": "integer" + } + }, + "description": "A join request from the perspective of the requester, including enough group context to render the request in a list (e.g. group name, owner, member count)." + }, + "invalidJoinLinkPreviewView": { + "type": "object", + "required": [ + "code" + ], + "properties": { + "code": { + "type": "string" + } + }, + "description": "Preview for a join link code that does not map to an existing link. Carries only the code so clients can correlate with the input and render an invalid state." + }, + "disabledJoinLinkPreviewView": { + "type": "object", + "required": [ + "code" + ], + "properties": { + "code": { + "type": "string" + } + }, + "description": "Preview for a disabled join link. Carries only the code so clients can correlate with the input and render a disabled state." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/chat/bsky/group/disableJoinLink.json b/lexicons/chat/bsky/group/disableJoinLink.json new file mode 100644 index 0000000000..e240344a17 --- /dev/null +++ b/lexicons/chat/bsky/group/disableJoinLink.json @@ -0,0 +1,51 @@ +{ + "id": "chat.bsky.group.disableJoinLink", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "convoId" + ], + "properties": { + "convoId": { + "type": "string" + } + } + }, + "encoding": "application/json" + }, + "errors": [ + { + "name": "InvalidConvo" + }, + { + "name": "InsufficientRole" + }, + { + "name": "NoJoinLink" + } + ], + "output": { + "schema": { + "type": "object", + "required": [ + "joinLink" + ], + "properties": { + "joinLink": { + "ref": "chat.bsky.group.defs#joinLinkView", + "type": "ref" + } + } + }, + "encoding": "application/json" + }, + "description": "Disables the active join link for the group convo." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/chat/bsky/group/editGroup.json b/lexicons/chat/bsky/group/editGroup.json new file mode 100644 index 0000000000..2ce53704cd --- /dev/null +++ b/lexicons/chat/bsky/group/editGroup.json @@ -0,0 +1,58 @@ +{ + "id": "chat.bsky.group.editGroup", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "convoId", + "name" + ], + "properties": { + "name": { + "type": "string", + "maxLength": 500, + "minLength": 1, + "maxGraphemes": 50 + }, + "convoId": { + "type": "string" + } + } + }, + "encoding": "application/json" + }, + "errors": [ + { + "name": "ConvoLocked" + }, + { + "name": "InvalidConvo" + }, + { + "name": "InsufficientRole" + } + ], + "output": { + "schema": { + "type": "object", + "required": [ + "convo" + ], + "properties": { + "convo": { + "ref": "chat.bsky.convo.defs#convoView", + "type": "ref" + } + } + }, + "encoding": "application/json" + }, + "description": "Edits group settings." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/chat/bsky/group/editJoinLink.json b/lexicons/chat/bsky/group/editJoinLink.json new file mode 100644 index 0000000000..17bc143555 --- /dev/null +++ b/lexicons/chat/bsky/group/editJoinLink.json @@ -0,0 +1,58 @@ +{ + "id": "chat.bsky.group.editJoinLink", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "convoId" + ], + "properties": { + "convoId": { + "type": "string" + }, + "joinRule": { + "ref": "chat.bsky.group.defs#joinRule", + "type": "ref" + }, + "requireApproval": { + "type": "boolean" + } + } + }, + "encoding": "application/json" + }, + "errors": [ + { + "name": "InvalidConvo" + }, + { + "name": "InsufficientRole" + }, + { + "name": "NoJoinLink" + } + ], + "output": { + "schema": { + "type": "object", + "required": [ + "joinLink" + ], + "properties": { + "joinLink": { + "ref": "chat.bsky.group.defs#joinLinkView", + "type": "ref" + } + } + }, + "encoding": "application/json" + }, + "description": "Edits the existing join link settings for the group convo." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/chat/bsky/group/enableJoinLink.json b/lexicons/chat/bsky/group/enableJoinLink.json new file mode 100644 index 0000000000..ed82a96495 --- /dev/null +++ b/lexicons/chat/bsky/group/enableJoinLink.json @@ -0,0 +1,54 @@ +{ + "id": "chat.bsky.group.enableJoinLink", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "convoId" + ], + "properties": { + "convoId": { + "type": "string" + } + } + }, + "encoding": "application/json" + }, + "errors": [ + { + "name": "InvalidConvo" + }, + { + "name": "InsufficientRole" + }, + { + "name": "NoJoinLink" + }, + { + "name": "LinkAlreadyEnabled" + } + ], + "output": { + "schema": { + "type": "object", + "required": [ + "joinLink" + ], + "properties": { + "joinLink": { + "ref": "chat.bsky.group.defs#joinLinkView", + "type": "ref" + } + } + }, + "encoding": "application/json" + }, + "description": "Re-enables a previously disabled join link for the group convo." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/chat/bsky/group/getJoinLinkPreviews.json b/lexicons/chat/bsky/group/getJoinLinkPreviews.json new file mode 100644 index 0000000000..c9c8491cad --- /dev/null +++ b/lexicons/chat/bsky/group/getJoinLinkPreviews.json @@ -0,0 +1,49 @@ +{ + "id": "chat.bsky.group.getJoinLinkPreviews", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "joinLinkPreviews" + ], + "properties": { + "joinLinkPreviews": { + "type": "array", + "items": { + "refs": [ + "chat.bsky.group.defs#joinLinkPreviewView", + "chat.bsky.group.defs#disabledJoinLinkPreviewView", + "chat.bsky.group.defs#invalidJoinLinkPreviewView" + ], + "type": "union" + } + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "codes" + ], + "properties": { + "codes": { + "type": "array", + "items": { + "type": "string" + }, + "maxLength": 50, + "minLength": 1 + } + } + }, + "description": "Get public information about groups from join links. The output array matches the input codes one-to-one by position (and each view also carries its 'code'). Disabled codes return a disabledJoinLinkPreviewView, and codes that do not map to a previewable link return an invalidJoinLinkPreviewView." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/chat/bsky/group/listJoinRequests.json b/lexicons/chat/bsky/group/listJoinRequests.json new file mode 100644 index 0000000000..b8ecd295dd --- /dev/null +++ b/lexicons/chat/bsky/group/listJoinRequests.json @@ -0,0 +1,60 @@ +{ + "id": "chat.bsky.group.listJoinRequests", + "defs": { + "main": { + "type": "query", + "errors": [ + { + "name": "InvalidConvo" + }, + { + "name": "InsufficientRole" + } + ], + "output": { + "schema": { + "type": "object", + "required": [ + "requests" + ], + "properties": { + "cursor": { + "type": "string" + }, + "requests": { + "type": "array", + "items": { + "ref": "chat.bsky.group.defs#joinRequestView", + "type": "ref" + } + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "convoId" + ], + "properties": { + "limit": { + "type": "integer", + "default": 50, + "maximum": 100, + "minimum": 1 + }, + "cursor": { + "type": "string" + }, + "convoId": { + "type": "string" + } + } + }, + "description": "Lists a page of request to join a group (via join link) the user owns. Shows the data from the owner's point of view." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/chat/bsky/group/listMutualGroups.json b/lexicons/chat/bsky/group/listMutualGroups.json new file mode 100644 index 0000000000..26d04f1edd --- /dev/null +++ b/lexicons/chat/bsky/group/listMutualGroups.json @@ -0,0 +1,53 @@ +{ + "id": "chat.bsky.group.listMutualGroups", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "convos" + ], + "properties": { + "convos": { + "type": "array", + "items": { + "ref": "chat.bsky.convo.defs#convoView", + "type": "ref" + } + }, + "cursor": { + "type": "string" + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "subject" + ], + "properties": { + "limit": { + "type": "integer", + "default": 50, + "maximum": 100, + "minimum": 1 + }, + "cursor": { + "type": "string" + }, + "subject": { + "type": "string", + "format": "did" + } + } + }, + "description": "Returns a page of group conversations that both the requester and the specified actor are members of." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/chat/bsky/group/rejectJoinRequest.json b/lexicons/chat/bsky/group/rejectJoinRequest.json new file mode 100644 index 0000000000..fb8edf64ea --- /dev/null +++ b/lexicons/chat/bsky/group/rejectJoinRequest.json @@ -0,0 +1,46 @@ +{ + "id": "chat.bsky.group.rejectJoinRequest", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "convoId", + "member" + ], + "properties": { + "member": { + "type": "string", + "format": "did" + }, + "convoId": { + "type": "string" + } + } + }, + "encoding": "application/json" + }, + "errors": [ + { + "name": "InvalidConvo" + }, + { + "name": "InsufficientRole" + } + ], + "output": { + "schema": { + "type": "object", + "required": [], + "properties": {} + }, + "encoding": "application/json" + }, + "description": "Rejects a request to join a group (via join link) the user owns. Action taken by the group owner." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/chat/bsky/group/removeMembers.json b/lexicons/chat/bsky/group/removeMembers.json new file mode 100644 index 0000000000..e7e795a6bb --- /dev/null +++ b/lexicons/chat/bsky/group/removeMembers.json @@ -0,0 +1,57 @@ +{ + "id": "chat.bsky.group.removeMembers", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "convoId", + "members" + ], + "properties": { + "convoId": { + "type": "string" + }, + "members": { + "type": "array", + "items": { + "type": "string", + "format": "did" + }, + "minLength": 1 + } + } + }, + "encoding": "application/json" + }, + "errors": [ + { + "name": "InvalidConvo" + }, + { + "name": "InsufficientRole" + } + ], + "output": { + "schema": { + "type": "object", + "required": [ + "convo" + ], + "properties": { + "convo": { + "ref": "chat.bsky.convo.defs#convoView", + "type": "ref" + } + } + }, + "encoding": "application/json" + }, + "description": "Removes members from a group. This deletes convo memberships, doesn't just set a status." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/chat/bsky/group/requestJoin.json b/lexicons/chat/bsky/group/requestJoin.json new file mode 100644 index 0000000000..b3cbbd6b4c --- /dev/null +++ b/lexicons/chat/bsky/group/requestJoin.json @@ -0,0 +1,68 @@ +{ + "id": "chat.bsky.group.requestJoin", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "code" + ], + "properties": { + "code": { + "type": "string" + } + } + }, + "encoding": "application/json" + }, + "errors": [ + { + "name": "ConvoLocked" + }, + { + "name": "FollowRequired" + }, + { + "name": "InvalidCode" + }, + { + "name": "LinkDisabled" + }, + { + "name": "MemberLimitReached" + }, + { + "name": "UserKicked" + } + ], + "output": { + "schema": { + "type": "object", + "required": [ + "status" + ], + "properties": { + "convo": { + "ref": "chat.bsky.convo.defs#convoView", + "type": "ref", + "description": "The group convo joined. This is only present in the case of status=joined" + }, + "status": { + "type": "string", + "knownValues": [ + "joined", + "pending" + ] + } + } + }, + "encoding": "application/json" + }, + "description": "Sends a request to join a group (via join link) to the group owner. Action taken by the prospective group member." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/chat/bsky/group/updateJoinRequestsRead.json b/lexicons/chat/bsky/group/updateJoinRequestsRead.json new file mode 100644 index 0000000000..e80c671d5e --- /dev/null +++ b/lexicons/chat/bsky/group/updateJoinRequestsRead.json @@ -0,0 +1,41 @@ +{ + "id": "chat.bsky.group.updateJoinRequestsRead", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "convoId" + ], + "properties": { + "convoId": { + "type": "string" + } + } + }, + "encoding": "application/json" + }, + "errors": [ + { + "name": "InvalidConvo" + }, + { + "name": "InsufficientRole" + } + ], + "output": { + "schema": { + "type": "object", + "required": [], + "properties": {} + }, + "encoding": "application/json" + }, + "description": "Marks all join requests as read for the group owner." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/chat/bsky/group/withdrawJoinRequest.json b/lexicons/chat/bsky/group/withdrawJoinRequest.json new file mode 100644 index 0000000000..4d13bb4249 --- /dev/null +++ b/lexicons/chat/bsky/group/withdrawJoinRequest.json @@ -0,0 +1,38 @@ +{ + "id": "chat.bsky.group.withdrawJoinRequest", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "convoId" + ], + "properties": { + "convoId": { + "type": "string" + } + } + }, + "encoding": "application/json" + }, + "errors": [ + { + "name": "InvalidJoinRequest" + } + ], + "output": { + "schema": { + "type": "object", + "required": [], + "properties": {} + }, + "encoding": "application/json" + }, + "description": "Withdraws a pending request to join a group. Action taken by the prospective member who originally requested to join." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/chat/bsky/moderation/defs.json b/lexicons/chat/bsky/moderation/defs.json new file mode 100644 index 0000000000..895b3bee77 --- /dev/null +++ b/lexicons/chat/bsky/moderation/defs.json @@ -0,0 +1,81 @@ +{ + "id": "chat.bsky.moderation.defs", + "defs": { + "convoView": { + "type": "object", + "required": [ + "id", + "rev" + ], + "properties": { + "id": { + "type": "string" + }, + "rev": { + "type": "string" + }, + "kind": { + "refs": [ + "#directConvo", + "#groupConvo" + ], + "type": "union", + "description": "Union field that has data specific to different kinds of convos." + } + }, + "description": "A view of a conversation for moderation purposes. Unlike chat.bsky.convo.defs#convoView, it does not include viewer-specific data (such as muted, unreadCount, status, lastMessage, lastReaction), since the requester is a moderator and not a member of the conversation. The member list is not included; use chat.bsky.moderation.getConvoMembers to list members." + }, + "groupConvo": { + "type": "object", + "required": [ + "createdAt", + "joinRequestCount", + "lockStatus", + "memberCount", + "memberLimit", + "name" + ], + "properties": { + "name": { + "type": "string", + "maxLength": 500, + "description": "The display name of the group conversation.", + "maxGraphemes": 50 + }, + "joinLink": { + "ref": "chat.bsky.group.defs#joinLinkView", + "type": "ref" + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "lockStatus": { + "ref": "chat.bsky.convo.defs#convoLockStatus", + "type": "ref", + "description": "The lock status of the conversation." + }, + "memberCount": { + "type": "integer", + "description": "The total number of members in the group conversation." + }, + "memberLimit": { + "type": "integer", + "description": "The maximum number of members allowed in the group conversation." + }, + "joinRequestCount": { + "type": "integer", + "description": "The total number of pending join requests for the group conversation. This information is only visible to the owner and to moderators. Capped at 21." + } + }, + "description": "Data specific to a group conversation, for moderation purposes. Unlike chat.bsky.convo.defs#groupConvo, it does not include viewer-specific data (such as unreadJoinRequestCount), since the requester is a moderator and not a member of the conversation." + }, + "directConvo": { + "type": "object", + "properties": {}, + "description": "Data specific to a direct conversation, for moderation purposes." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/chat/bsky/moderation/getActorMetadata.json b/lexicons/chat/bsky/moderation/getActorMetadata.json new file mode 100644 index 0000000000..3f20fe2170 --- /dev/null +++ b/lexicons/chat/bsky/moderation/getActorMetadata.json @@ -0,0 +1,70 @@ +{ + "id": "chat.bsky.moderation.getActorMetadata", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "day", + "month", + "all" + ], + "properties": { + "all": { + "ref": "#metadata", + "type": "ref" + }, + "day": { + "ref": "#metadata", + "type": "ref" + }, + "month": { + "ref": "#metadata", + "type": "ref" + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "actor" + ], + "properties": { + "actor": { + "type": "string", + "format": "did" + } + } + } + }, + "metadata": { + "type": "object", + "required": [ + "messagesSent", + "messagesReceived", + "convos", + "convosStarted" + ], + "properties": { + "convos": { + "type": "integer" + }, + "messagesSent": { + "type": "integer" + }, + "convosStarted": { + "type": "integer" + }, + "messagesReceived": { + "type": "integer" + } + } + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/chat/bsky/moderation/getConvo.json b/lexicons/chat/bsky/moderation/getConvo.json new file mode 100644 index 0000000000..a9213df585 --- /dev/null +++ b/lexicons/chat/bsky/moderation/getConvo.json @@ -0,0 +1,42 @@ +{ + "id": "chat.bsky.moderation.getConvo", + "defs": { + "main": { + "type": "query", + "errors": [ + { + "name": "InvalidConvo" + } + ], + "output": { + "schema": { + "type": "object", + "required": [ + "convo" + ], + "properties": { + "convo": { + "ref": "chat.bsky.moderation.defs#convoView", + "type": "ref" + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "convoId" + ], + "properties": { + "convoId": { + "type": "string" + } + } + }, + "description": "Gets an existing conversation by its ID, for moderation purposes. Does not require the requester to be a member of the conversation." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/chat/bsky/moderation/getConvoMembers.json b/lexicons/chat/bsky/moderation/getConvoMembers.json new file mode 100644 index 0000000000..236663422c --- /dev/null +++ b/lexicons/chat/bsky/moderation/getConvoMembers.json @@ -0,0 +1,57 @@ +{ + "id": "chat.bsky.moderation.getConvoMembers", + "defs": { + "main": { + "type": "query", + "errors": [ + { + "name": "InvalidConvo" + } + ], + "output": { + "schema": { + "type": "object", + "required": [ + "members" + ], + "properties": { + "cursor": { + "type": "string" + }, + "members": { + "type": "array", + "items": { + "ref": "chat.bsky.actor.defs#profileViewBasic", + "type": "ref" + } + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "convoId" + ], + "properties": { + "limit": { + "type": "integer", + "default": 50, + "maximum": 100, + "minimum": 1 + }, + "cursor": { + "type": "string" + }, + "convoId": { + "type": "string" + } + } + }, + "description": "Returns a paginated list of members from a conversation, for moderation purposes. Does not require the requester to be a member of the conversation." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/chat/bsky/moderation/getConvos.json b/lexicons/chat/bsky/moderation/getConvos.json new file mode 100644 index 0000000000..2169dc64dd --- /dev/null +++ b/lexicons/chat/bsky/moderation/getConvos.json @@ -0,0 +1,45 @@ +{ + "id": "chat.bsky.moderation.getConvos", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "convos" + ], + "properties": { + "convos": { + "type": "array", + "items": { + "ref": "chat.bsky.moderation.defs#convoView", + "type": "ref" + } + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "convoIds" + ], + "properties": { + "convoIds": { + "type": "array", + "items": { + "type": "string" + }, + "maxLength": 100, + "minLength": 1 + } + } + }, + "description": "Gets existing conversations by their IDs, for moderation purposes. Does not require the requester to be a member of the conversations. Unknown IDs are silently omitted from the response." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/chat/bsky/moderation/getMessageContext.json b/lexicons/chat/bsky/moderation/getMessageContext.json new file mode 100644 index 0000000000..f88034c105 --- /dev/null +++ b/lexicons/chat/bsky/moderation/getMessageContext.json @@ -0,0 +1,63 @@ +{ + "id": "chat.bsky.moderation.getMessageContext", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "messages" + ], + "properties": { + "messages": { + "type": "array", + "items": { + "refs": [ + "chat.bsky.convo.defs#messageView", + "chat.bsky.convo.defs#systemMessageView" + ], + "type": "union" + } + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "messageId" + ], + "properties": { + "after": { + "type": "integer", + "default": 5, + "description": "Number of user messages after the target to include. System messages between the target and the latest returned user message are also included, capped per gap by `maxInterleavedSystemMessages`. If there are no user messages after the target, up to `maxInterleavedSystemMessages` system messages immediately following the target are returned instead." + }, + "before": { + "type": "integer", + "default": 5, + "description": "Number of user messages before the target to include. System messages between the earliest returned user message and the target are also included, capped per gap by `maxInterleavedSystemMessages`. If there are no user messages before the target, up to `maxInterleavedSystemMessages` system messages immediately preceding the target are returned instead." + }, + "convoId": { + "type": "string", + "description": "Conversation that the message is from. NOTE: this field will eventually be required." + }, + "messageId": { + "type": "string" + }, + "maxInterleavedSystemMessages": { + "type": "integer", + "default": 10, + "maximum": 1000, + "minimum": 0, + "description": "Maximum number of system messages to include per gap between consecutive returned messages (and per side when there are no user messages on that side). Within a gap, the system messages closest to the earlier message are kept." + } + } + } + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/chat/bsky/moderation/subscribeModEvents.json b/lexicons/chat/bsky/moderation/subscribeModEvents.json new file mode 100644 index 0000000000..277c85b248 --- /dev/null +++ b/lexicons/chat/bsky/moderation/subscribeModEvents.json @@ -0,0 +1,675 @@ +{ + "id": "chat.bsky.moderation.subscribeModEvents", + "defs": { + "main": { + "type": "subscription", + "errors": [ + { + "name": "FutureCursor" + }, + { + "name": "ConsumerTooSlow", + "description": "If the consumer of the stream can not keep up with events, and a backlog gets too large, the server will drop the connection." + } + ], + "message": { + "schema": { + "refs": [ + "#eventConvoFirstMessage", + "#eventGroupChatCreated", + "#eventGroupChatMemberAdded", + "#eventGroupChatMemberJoined", + "#eventGroupChatJoinRequest", + "#eventGroupChatJoinRequestApproved", + "#eventGroupChatJoinRequestRejected", + "#eventChatAccepted", + "#eventGroupChatMemberLeft", + "#eventGroupChatUpdated", + "#eventRateLimitExceeded" + ], + "type": "union" + } + }, + "parameters": { + "type": "params", + "properties": { + "cursor": { + "type": "string", + "description": "The last known event seq number to backfill from. Use '2222222222222' to backfill from the beginning. Don't specify a cursor to listen only for new events." + } + } + }, + "description": "Subscribe to stream of chat events targeted to moderation. Private endpoint." + }, + "eventChatAccepted": { + "type": "object", + "required": [ + "actorDid", + "convoCreatedAt", + "convoId", + "createdAt", + "method", + "rev" + ], + "properties": { + "rev": { + "type": "string" + }, + "method": { + "type": "string", + "description": "How the convo was accepted.", + "knownValues": [ + "explicit", + "message" + ] + }, + "convoId": { + "type": "string" + }, + "actorDid": { + "type": "string", + "format": "did", + "description": "The DID of the person accepting the convo." + }, + "ownerDid": { + "type": "string", + "format": "did", + "description": "The DID of the group chat owner. Only present for group convos." + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "groupName": { + "type": "string", + "description": "The name of the group chat. Only present for group convos." + }, + "convoCreatedAt": { + "type": "string", + "format": "datetime", + "description": "When the convo was originally created." + }, + "groupMemberCount": { + "type": "integer", + "description": "Current member count at the time of the event. Only present for group convos." + } + }, + "description": "Fired when a user accepts a chat convo, either explicitly or by sending a message." + }, + "eventGroupChatCreated": { + "type": "object", + "required": [ + "actorDid", + "convoCreatedAt", + "convoId", + "createdAt", + "groupMemberCount", + "groupName", + "initialMemberDids", + "ownerDid", + "rev" + ], + "properties": { + "rev": { + "type": "string" + }, + "convoId": { + "type": "string" + }, + "actorDid": { + "type": "string", + "format": "did", + "description": "The DID of the actor performing the action. For this event, same as ownerDid." + }, + "ownerDid": { + "type": "string", + "format": "did", + "description": "The DID of the group chat owner." + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "groupName": { + "type": "string", + "description": "The name set at creation time." + }, + "convoCreatedAt": { + "type": "string", + "format": "datetime", + "description": "When the group was originally created." + }, + "groupMemberCount": { + "type": "integer", + "description": "Current member count at the time of the event." + }, + "initialMemberDids": { + "type": "array", + "items": { + "type": "string", + "format": "did" + }, + "description": "DIDs of everyone added at creation time." + } + }, + "description": "Fire when a group chat is created." + }, + "eventGroupChatUpdated": { + "type": "object", + "required": [ + "actorDid", + "convoCreatedAt", + "convoId", + "createdAt", + "groupMemberCount", + "groupName", + "ownerDid", + "rev", + "updateType" + ], + "properties": { + "rev": { + "type": "string" + }, + "convoId": { + "type": "string" + }, + "newName": { + "type": "string", + "description": "The new group name. Only present when updateType is 'name_changed'." + }, + "oldName": { + "type": "string", + "description": "The previous group name. Only present when updateType is 'name_changed'." + }, + "actorDid": { + "type": "string", + "format": "did", + "description": "The DID of the actor performing the action (the owner)." + }, + "ownerDid": { + "type": "string", + "format": "did", + "description": "The DID of the group chat owner." + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "groupName": { + "type": "string", + "description": "Current group name." + }, + "lockReason": { + "type": "string", + "description": "Why the group was locked. Only present when updateType is 'locked'.", + "knownValues": [ + "owner_action", + "owner_left", + "owner_deactivated", + "owner_deleted", + "owner_suspended", + "owner_taken_down", + "label_applied", + "convo_taken_down" + ] + }, + "updateType": { + "type": "string", + "description": "What changed.", + "knownValues": [ + "name_changed", + "locked", + "locked_permanently", + "unlocked", + "join_link_created", + "join_link_disabled", + "join_link_settings_changed" + ] + }, + "joinLinkCode": { + "type": "string", + "description": "The code of the join link. Only present when updateType is join-link-related." + }, + "convoCreatedAt": { + "type": "string", + "format": "datetime", + "description": "When the group was originally created." + }, + "groupMemberCount": { + "type": "integer", + "description": "Current member count at the time of the event." + }, + "joinLinkFollowersOnly": { + "type": "boolean", + "description": "Whether the join link is restricted to followers of the owner. Only present when updateType is join-link-related." + }, + "joinLinkRequiresApproval": { + "type": "boolean", + "description": "Whether the join link requires owner approval to join. Only present when updateType is join-link-related." + } + }, + "description": "Fired when a group chat's metadata or status changes." + }, + "eventConvoFirstMessage": { + "type": "object", + "required": [ + "createdAt", + "rev", + "convoId", + "user", + "recipients" + ], + "properties": { + "rev": { + "type": "string" + }, + "user": { + "type": "string", + "format": "did", + "description": "The DID of the message author." + }, + "convoId": { + "type": "string" + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "messageId": { + "type": "string" + }, + "recipients": { + "type": "array", + "items": { + "type": "string", + "format": "did" + }, + "description": "The list of DIDs message recipients. Does not include the sender, which is in the `user` field" + } + }, + "description": "Fired when the first message was sent on a convo." + }, + "eventRateLimitExceeded": { + "type": "object", + "required": [ + "actorDid", + "createdAt", + "endpoint", + "rev" + ], + "properties": { + "rev": { + "type": "string" + }, + "actorDid": { + "type": "string", + "format": "did", + "description": "The DID of the user who hit the rate limit." + }, + "endpoint": { + "type": "string", + "description": "The NSID of the endpoint that was rate limited." + }, + "createdAt": { + "type": "string", + "format": "datetime" + } + }, + "description": "Fired when a user exceeds a rate limit." + }, + "eventGroupChatMemberLeft": { + "type": "object", + "required": [ + "actorDid", + "convoCreatedAt", + "convoId", + "createdAt", + "groupMemberCount", + "groupName", + "leaveMethod", + "ownerDid", + "rev", + "subjectDid" + ], + "properties": { + "rev": { + "type": "string" + }, + "convoId": { + "type": "string" + }, + "actorDid": { + "type": "string", + "format": "did", + "description": "The DID of the actor. For voluntary: the person leaving. For kicked: the owner." + }, + "ownerDid": { + "type": "string", + "format": "did", + "description": "The DID of the group chat owner." + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "groupName": { + "type": "string" + }, + "subjectDid": { + "type": "string", + "format": "did", + "description": "The DID of the member who left or was removed." + }, + "leaveMethod": { + "type": "string", + "description": "How the member left.", + "knownValues": [ + "voluntary", + "kicked" + ] + }, + "convoCreatedAt": { + "type": "string", + "format": "datetime", + "description": "When the group was originally created." + }, + "groupMemberCount": { + "type": "integer", + "description": "Current member count at the time of the event." + } + }, + "description": "Fired when a member leaves or is removed from a group chat." + }, + "eventGroupChatJoinRequest": { + "type": "object", + "required": [ + "actorDid", + "convoCreatedAt", + "convoId", + "createdAt", + "groupMemberCount", + "groupName", + "joinLinkCode", + "ownerDid", + "rev", + "subjectFollowsOwner" + ], + "properties": { + "rev": { + "type": "string" + }, + "convoId": { + "type": "string" + }, + "actorDid": { + "type": "string", + "format": "did", + "description": "The DID of the person requesting to join." + }, + "ownerDid": { + "type": "string", + "format": "did", + "description": "The DID of the group chat owner." + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "groupName": { + "type": "string" + }, + "joinLinkCode": { + "type": "string", + "description": "The code of the join link used to request joining." + }, + "convoCreatedAt": { + "type": "string", + "format": "datetime", + "description": "When the group was originally created." + }, + "groupMemberCount": { + "type": "integer", + "description": "Current member count at the time of the event." + }, + "subjectFollowsOwner": { + "type": "boolean", + "description": "Whether the requesting member follows the group owner." + } + }, + "description": "Fired when a user requests to join a group chat via an join link that requires approval." + }, + "eventGroupChatMemberAdded": { + "type": "object", + "required": [ + "actorDid", + "convoCreatedAt", + "convoId", + "createdAt", + "groupMemberCount", + "groupName", + "ownerDid", + "requestMembersCount", + "rev", + "subjectDid", + "subjectFollowsOwner" + ], + "properties": { + "rev": { + "type": "string" + }, + "convoId": { + "type": "string" + }, + "actorDid": { + "type": "string", + "format": "did", + "description": "The DID of the actor performing the action. For this event, same as ownerDid." + }, + "ownerDid": { + "type": "string", + "format": "did", + "description": "The DID of the group chat owner." + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "groupName": { + "type": "string" + }, + "subjectDid": { + "type": "string", + "format": "did", + "description": "The DID of the member who was added." + }, + "convoCreatedAt": { + "type": "string", + "format": "datetime", + "description": "When the group was originally created." + }, + "groupMemberCount": { + "type": "integer", + "description": "Current member count at the time of the event." + }, + "requestMembersCount": { + "type": "integer", + "description": "The number of members who have not yet accepted the convo." + }, + "subjectFollowsOwner": { + "type": "boolean", + "description": "Whether the added member follows the group owner." + } + }, + "description": "Fired when a member is added to a group chat. Note that members are added in the 'request' state." + }, + "eventGroupChatMemberJoined": { + "type": "object", + "required": [ + "actorDid", + "convoCreatedAt", + "convoId", + "createdAt", + "groupMemberCount", + "groupName", + "joinLinkCode", + "ownerDid", + "rev", + "subjectFollowsOwner" + ], + "properties": { + "rev": { + "type": "string" + }, + "convoId": { + "type": "string" + }, + "actorDid": { + "type": "string", + "format": "did", + "description": "The DID of the person joining." + }, + "ownerDid": { + "type": "string", + "format": "did", + "description": "The DID of the group chat owner." + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "groupName": { + "type": "string" + }, + "joinLinkCode": { + "type": "string", + "description": "The code of the join link used to join." + }, + "convoCreatedAt": { + "type": "string", + "format": "datetime", + "description": "When the group was originally created." + }, + "groupMemberCount": { + "type": "integer", + "description": "Current member count at the time of the event." + }, + "subjectFollowsOwner": { + "type": "boolean", + "description": "Whether the joining member follows the group owner." + } + }, + "description": "Fired when a member joins a group chat via an join link that does not require approval." + }, + "eventGroupChatJoinRequestApproved": { + "type": "object", + "required": [ + "actorDid", + "convoCreatedAt", + "convoId", + "createdAt", + "groupMemberCount", + "groupName", + "ownerDid", + "rev", + "subjectDid" + ], + "properties": { + "rev": { + "type": "string" + }, + "convoId": { + "type": "string" + }, + "actorDid": { + "type": "string", + "format": "did", + "description": "The DID of the owner approving the request." + }, + "ownerDid": { + "type": "string", + "format": "did", + "description": "The DID of the group chat owner." + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "groupName": { + "type": "string" + }, + "subjectDid": { + "type": "string", + "format": "did", + "description": "The DID of the member whose request was approved." + }, + "convoCreatedAt": { + "type": "string", + "format": "datetime", + "description": "When the group was originally created." + }, + "groupMemberCount": { + "type": "integer", + "description": "Current member count at the time of the event." + } + }, + "description": "Fired when a join request is approved by the group owner." + }, + "eventGroupChatJoinRequestRejected": { + "type": "object", + "required": [ + "actorDid", + "convoCreatedAt", + "convoId", + "createdAt", + "groupMemberCount", + "groupName", + "ownerDid", + "rev", + "subjectDid" + ], + "properties": { + "rev": { + "type": "string" + }, + "convoId": { + "type": "string" + }, + "actorDid": { + "type": "string", + "format": "did", + "description": "The DID of the owner rejecting the request." + }, + "ownerDid": { + "type": "string", + "format": "did", + "description": "The DID of the group chat owner." + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "groupName": { + "type": "string" + }, + "subjectDid": { + "type": "string", + "format": "did", + "description": "The DID of the member whose request was rejected." + }, + "convoCreatedAt": { + "type": "string", + "format": "datetime", + "description": "When the group was originally created." + }, + "groupMemberCount": { + "type": "integer", + "description": "Current member count at the time of the event." + } + }, + "description": "Fired when a join request is rejected by the group owner." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/chat/bsky/moderation/updateActorAccess.json b/lexicons/chat/bsky/moderation/updateActorAccess.json new file mode 100644 index 0000000000..8e2d711a17 --- /dev/null +++ b/lexicons/chat/bsky/moderation/updateActorAccess.json @@ -0,0 +1,32 @@ +{ + "id": "chat.bsky.moderation.updateActorAccess", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "actor", + "allowAccess" + ], + "properties": { + "ref": { + "type": "string" + }, + "actor": { + "type": "string", + "format": "did" + }, + "allowAccess": { + "type": "boolean" + } + } + }, + "encoding": "application/json" + } + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/chat/bsky/notification/defs.json b/lexicons/chat/bsky/notification/defs.json new file mode 100644 index 0000000000..927f7242de --- /dev/null +++ b/lexicons/chat/bsky/notification/defs.json @@ -0,0 +1,43 @@ +{ + "id": "chat.bsky.notification.defs", + "defs": { + "preferences": { + "type": "object", + "required": [ + "chat", + "chatRequest" + ], + "properties": { + "chat": { + "ref": "#chatPreference", + "type": "ref" + }, + "chatRequest": { + "ref": "#chatPreference", + "type": "ref" + } + } + }, + "chatPreference": { + "type": "object", + "required": [ + "include", + "push" + ], + "properties": { + "push": { + "type": "boolean" + }, + "include": { + "type": "string", + "knownValues": [ + "all", + "follows" + ] + } + } + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/chat/bsky/notification/getPreferences.json b/lexicons/chat/bsky/notification/getPreferences.json new file mode 100644 index 0000000000..e1924b0ea9 --- /dev/null +++ b/lexicons/chat/bsky/notification/getPreferences.json @@ -0,0 +1,26 @@ +{ + "id": "chat.bsky.notification.getPreferences", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "preferences" + ], + "properties": { + "preferences": { + "ref": "chat.bsky.notification.defs#preferences", + "type": "ref" + } + } + }, + "encoding": "application/json" + }, + "description": "Get the requesting account's chat notification preferences. Defaults are returned for accounts that have not set any preferences. Requires auth." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/chat/bsky/notification/putPreferences.json b/lexicons/chat/bsky/notification/putPreferences.json new file mode 100644 index 0000000000..1d278ddb8a --- /dev/null +++ b/lexicons/chat/bsky/notification/putPreferences.json @@ -0,0 +1,42 @@ +{ + "id": "chat.bsky.notification.putPreferences", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "properties": { + "chat": { + "ref": "chat.bsky.notification.defs#chatPreference", + "type": "ref" + }, + "chatRequest": { + "ref": "chat.bsky.notification.defs#chatPreference", + "type": "ref" + } + } + }, + "encoding": "application/json" + }, + "output": { + "schema": { + "type": "object", + "required": [ + "preferences" + ], + "properties": { + "preferences": { + "ref": "chat.bsky.notification.defs#preferences", + "type": "ref" + } + } + }, + "encoding": "application/json" + }, + "description": "Set the requesting account's chat notification preferences. Only the provided preferences are updated; omitted preferences are left unchanged." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/admin/defs.json b/lexicons/com/atproto/admin/defs.json new file mode 100644 index 0000000000..ee74d849c7 --- /dev/null +++ b/lexicons/com/atproto/admin/defs.json @@ -0,0 +1,132 @@ +{ + "id": "com.atproto.admin.defs", + "defs": { + "repoRef": { + "type": "object", + "required": [ + "did" + ], + "properties": { + "did": { + "type": "string", + "format": "did" + } + } + }, + "statusAttr": { + "type": "object", + "required": [ + "applied" + ], + "properties": { + "ref": { + "type": "string" + }, + "applied": { + "type": "boolean" + } + } + }, + "accountView": { + "type": "object", + "required": [ + "did", + "handle", + "indexedAt" + ], + "properties": { + "did": { + "type": "string", + "format": "did" + }, + "email": { + "type": "string" + }, + "handle": { + "type": "string", + "format": "handle" + }, + "invites": { + "type": "array", + "items": { + "ref": "com.atproto.server.defs#inviteCode", + "type": "ref" + } + }, + "indexedAt": { + "type": "string", + "format": "datetime" + }, + "invitedBy": { + "ref": "com.atproto.server.defs#inviteCode", + "type": "ref" + }, + "inviteNote": { + "type": "string" + }, + "deactivatedAt": { + "type": "string", + "format": "datetime" + }, + "relatedRecords": { + "type": "array", + "items": { + "type": "unknown" + } + }, + "invitesDisabled": { + "type": "boolean" + }, + "emailConfirmedAt": { + "type": "string", + "format": "datetime" + }, + "threatSignatures": { + "type": "array", + "items": { + "ref": "#threatSignature", + "type": "ref" + } + } + } + }, + "repoBlobRef": { + "type": "object", + "required": [ + "did", + "cid" + ], + "properties": { + "cid": { + "type": "string", + "format": "cid" + }, + "did": { + "type": "string", + "format": "did" + }, + "recordUri": { + "type": "string", + "format": "at-uri" + } + } + }, + "threatSignature": { + "type": "object", + "required": [ + "property", + "value" + ], + "properties": { + "value": { + "type": "string" + }, + "property": { + "type": "string" + } + } + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/admin/deleteAccount.json b/lexicons/com/atproto/admin/deleteAccount.json new file mode 100644 index 0000000000..33497c78c1 --- /dev/null +++ b/lexicons/com/atproto/admin/deleteAccount.json @@ -0,0 +1,26 @@ +{ + "id": "com.atproto.admin.deleteAccount", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "did" + ], + "properties": { + "did": { + "type": "string", + "format": "did" + } + } + }, + "encoding": "application/json" + }, + "description": "Delete a user account as an administrator." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/admin/disableAccountInvites.json b/lexicons/com/atproto/admin/disableAccountInvites.json new file mode 100644 index 0000000000..3263b02e14 --- /dev/null +++ b/lexicons/com/atproto/admin/disableAccountInvites.json @@ -0,0 +1,30 @@ +{ + "id": "com.atproto.admin.disableAccountInvites", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "account" + ], + "properties": { + "note": { + "type": "string", + "description": "Optional reason for disabled invites." + }, + "account": { + "type": "string", + "format": "did" + } + } + }, + "encoding": "application/json" + }, + "description": "Disable an account from receiving new invite codes, but does not invalidate existing codes." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/admin/disableInviteCodes.json b/lexicons/com/atproto/admin/disableInviteCodes.json new file mode 100644 index 0000000000..97ed72bd54 --- /dev/null +++ b/lexicons/com/atproto/admin/disableInviteCodes.json @@ -0,0 +1,31 @@ +{ + "id": "com.atproto.admin.disableInviteCodes", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "properties": { + "codes": { + "type": "array", + "items": { + "type": "string" + } + }, + "accounts": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "encoding": "application/json" + }, + "description": "Disable some set of codes and/or all codes associated with a set of users." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/admin/enableAccountInvites.json b/lexicons/com/atproto/admin/enableAccountInvites.json new file mode 100644 index 0000000000..d7bc1f7dd2 --- /dev/null +++ b/lexicons/com/atproto/admin/enableAccountInvites.json @@ -0,0 +1,30 @@ +{ + "id": "com.atproto.admin.enableAccountInvites", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "account" + ], + "properties": { + "note": { + "type": "string", + "description": "Optional reason for enabled invites." + }, + "account": { + "type": "string", + "format": "did" + } + } + }, + "encoding": "application/json" + }, + "description": "Re-enable an account's ability to receive invite codes." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/admin/getAccountInfo.json b/lexicons/com/atproto/admin/getAccountInfo.json new file mode 100644 index 0000000000..d64dd43920 --- /dev/null +++ b/lexicons/com/atproto/admin/getAccountInfo.json @@ -0,0 +1,30 @@ +{ + "id": "com.atproto.admin.getAccountInfo", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "ref": "com.atproto.admin.defs#accountView", + "type": "ref" + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "did" + ], + "properties": { + "did": { + "type": "string", + "format": "did" + } + } + }, + "description": "Get details about an account." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/admin/getAccountInfos.json b/lexicons/com/atproto/admin/getAccountInfos.json new file mode 100644 index 0000000000..b23ec065f8 --- /dev/null +++ b/lexicons/com/atproto/admin/getAccountInfos.json @@ -0,0 +1,44 @@ +{ + "id": "com.atproto.admin.getAccountInfos", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "infos" + ], + "properties": { + "infos": { + "type": "array", + "items": { + "ref": "com.atproto.admin.defs#accountView", + "type": "ref" + } + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "dids" + ], + "properties": { + "dids": { + "type": "array", + "items": { + "type": "string", + "format": "did" + } + } + } + }, + "description": "Get details about some accounts." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/admin/getInviteCodes.json b/lexicons/com/atproto/admin/getInviteCodes.json new file mode 100644 index 0000000000..085de7dfdf --- /dev/null +++ b/lexicons/com/atproto/admin/getInviteCodes.json @@ -0,0 +1,54 @@ +{ + "id": "com.atproto.admin.getInviteCodes", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "codes" + ], + "properties": { + "codes": { + "type": "array", + "items": { + "ref": "com.atproto.server.defs#inviteCode", + "type": "ref" + } + }, + "cursor": { + "type": "string" + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "properties": { + "sort": { + "type": "string", + "default": "recent", + "knownValues": [ + "recent", + "usage" + ] + }, + "limit": { + "type": "integer", + "default": 100, + "maximum": 500, + "minimum": 1 + }, + "cursor": { + "type": "string" + } + } + }, + "description": "Get an admin view of invite codes." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/admin/getSubjectStatus.json b/lexicons/com/atproto/admin/getSubjectStatus.json new file mode 100644 index 0000000000..ad10fd9d45 --- /dev/null +++ b/lexicons/com/atproto/admin/getSubjectStatus.json @@ -0,0 +1,55 @@ +{ + "id": "com.atproto.admin.getSubjectStatus", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "subject" + ], + "properties": { + "subject": { + "refs": [ + "com.atproto.admin.defs#repoRef", + "com.atproto.repo.strongRef", + "com.atproto.admin.defs#repoBlobRef" + ], + "type": "union" + }, + "takedown": { + "ref": "com.atproto.admin.defs#statusAttr", + "type": "ref" + }, + "deactivated": { + "ref": "com.atproto.admin.defs#statusAttr", + "type": "ref" + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "properties": { + "did": { + "type": "string", + "format": "did" + }, + "uri": { + "type": "string", + "format": "at-uri" + }, + "blob": { + "type": "string", + "format": "cid" + } + } + }, + "description": "Get the service-specific admin status of a subject (account, record, or blob)." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/admin/searchAccounts.json b/lexicons/com/atproto/admin/searchAccounts.json new file mode 100644 index 0000000000..57770a2f87 --- /dev/null +++ b/lexicons/com/atproto/admin/searchAccounts.json @@ -0,0 +1,49 @@ +{ + "id": "com.atproto.admin.searchAccounts", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "accounts" + ], + "properties": { + "cursor": { + "type": "string" + }, + "accounts": { + "type": "array", + "items": { + "ref": "com.atproto.admin.defs#accountView", + "type": "ref" + } + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "properties": { + "email": { + "type": "string" + }, + "limit": { + "type": "integer", + "default": 50, + "maximum": 100, + "minimum": 1 + }, + "cursor": { + "type": "string" + } + } + }, + "description": "Get list of accounts that matches your search query." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/admin/sendEmail.json b/lexicons/com/atproto/admin/sendEmail.json new file mode 100644 index 0000000000..d619342572 --- /dev/null +++ b/lexicons/com/atproto/admin/sendEmail.json @@ -0,0 +1,56 @@ +{ + "id": "com.atproto.admin.sendEmail", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "recipientDid", + "content", + "senderDid" + ], + "properties": { + "comment": { + "type": "string", + "description": "Additional comment by the sender that won't be used in the email itself but helpful to provide more context for moderators/reviewers" + }, + "content": { + "type": "string" + }, + "subject": { + "type": "string" + }, + "senderDid": { + "type": "string", + "format": "did" + }, + "recipientDid": { + "type": "string", + "format": "did" + } + } + }, + "encoding": "application/json" + }, + "output": { + "schema": { + "type": "object", + "required": [ + "sent" + ], + "properties": { + "sent": { + "type": "boolean" + } + } + }, + "encoding": "application/json" + }, + "description": "Send email to a user's account email address." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/admin/updateAccountEmail.json b/lexicons/com/atproto/admin/updateAccountEmail.json new file mode 100644 index 0000000000..e42c2320fb --- /dev/null +++ b/lexicons/com/atproto/admin/updateAccountEmail.json @@ -0,0 +1,31 @@ +{ + "id": "com.atproto.admin.updateAccountEmail", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "account", + "email" + ], + "properties": { + "email": { + "type": "string" + }, + "account": { + "type": "string", + "format": "at-identifier", + "description": "The handle or DID of the repo." + } + } + }, + "encoding": "application/json" + }, + "description": "Administrative action to update an account's email." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/admin/updateAccountHandle.json b/lexicons/com/atproto/admin/updateAccountHandle.json new file mode 100644 index 0000000000..aa1cc6d593 --- /dev/null +++ b/lexicons/com/atproto/admin/updateAccountHandle.json @@ -0,0 +1,31 @@ +{ + "id": "com.atproto.admin.updateAccountHandle", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "did", + "handle" + ], + "properties": { + "did": { + "type": "string", + "format": "did" + }, + "handle": { + "type": "string", + "format": "handle" + } + } + }, + "encoding": "application/json" + }, + "description": "Administrative action to update an account's handle." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/admin/updateAccountPassword.json b/lexicons/com/atproto/admin/updateAccountPassword.json new file mode 100644 index 0000000000..c87cf4e198 --- /dev/null +++ b/lexicons/com/atproto/admin/updateAccountPassword.json @@ -0,0 +1,30 @@ +{ + "id": "com.atproto.admin.updateAccountPassword", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "did", + "password" + ], + "properties": { + "did": { + "type": "string", + "format": "did" + }, + "password": { + "type": "string" + } + } + }, + "encoding": "application/json" + }, + "description": "Update the password for a user account as an administrator." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/admin/updateAccountSigningKey.json b/lexicons/com/atproto/admin/updateAccountSigningKey.json new file mode 100644 index 0000000000..47f3584b8a --- /dev/null +++ b/lexicons/com/atproto/admin/updateAccountSigningKey.json @@ -0,0 +1,32 @@ +{ + "id": "com.atproto.admin.updateAccountSigningKey", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "did", + "signingKey" + ], + "properties": { + "did": { + "type": "string", + "format": "did" + }, + "signingKey": { + "type": "string", + "format": "did", + "description": "Did-key formatted public key" + } + } + }, + "encoding": "application/json" + }, + "description": "Administrative action to update an account's signing key in their Did document." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/admin/updateSubjectStatus.json b/lexicons/com/atproto/admin/updateSubjectStatus.json new file mode 100644 index 0000000000..35ba17b451 --- /dev/null +++ b/lexicons/com/atproto/admin/updateSubjectStatus.json @@ -0,0 +1,61 @@ +{ + "id": "com.atproto.admin.updateSubjectStatus", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "subject" + ], + "properties": { + "subject": { + "refs": [ + "com.atproto.admin.defs#repoRef", + "com.atproto.repo.strongRef", + "com.atproto.admin.defs#repoBlobRef" + ], + "type": "union" + }, + "takedown": { + "ref": "com.atproto.admin.defs#statusAttr", + "type": "ref" + }, + "deactivated": { + "ref": "com.atproto.admin.defs#statusAttr", + "type": "ref" + } + } + }, + "encoding": "application/json" + }, + "output": { + "schema": { + "type": "object", + "required": [ + "subject" + ], + "properties": { + "subject": { + "refs": [ + "com.atproto.admin.defs#repoRef", + "com.atproto.repo.strongRef", + "com.atproto.admin.defs#repoBlobRef" + ], + "type": "union" + }, + "takedown": { + "ref": "com.atproto.admin.defs#statusAttr", + "type": "ref" + } + } + }, + "encoding": "application/json" + }, + "description": "Update the service-specific admin status of a subject (account, record, or blob)." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/identity/defs.json b/lexicons/com/atproto/identity/defs.json new file mode 100644 index 0000000000..3ee467f515 --- /dev/null +++ b/lexicons/com/atproto/identity/defs.json @@ -0,0 +1,30 @@ +{ + "id": "com.atproto.identity.defs", + "defs": { + "identityInfo": { + "type": "object", + "required": [ + "did", + "handle", + "didDoc" + ], + "properties": { + "did": { + "type": "string", + "format": "did" + }, + "didDoc": { + "type": "unknown", + "description": "The complete DID document for the identity." + }, + "handle": { + "type": "string", + "format": "handle", + "description": "The validated handle of the account; or 'handle.invalid' if the handle did not bi-directionally match the DID document." + } + } + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/identity/getRecommendedDidCredentials.json b/lexicons/com/atproto/identity/getRecommendedDidCredentials.json new file mode 100644 index 0000000000..057c62e481 --- /dev/null +++ b/lexicons/com/atproto/identity/getRecommendedDidCredentials.json @@ -0,0 +1,38 @@ +{ + "id": "com.atproto.identity.getRecommendedDidCredentials", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "properties": { + "services": { + "type": "unknown" + }, + "alsoKnownAs": { + "type": "array", + "items": { + "type": "string" + } + }, + "rotationKeys": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Recommended rotation keys for PLC dids. Should be undefined (or ignored) for did:webs." + }, + "verificationMethods": { + "type": "unknown" + } + } + }, + "encoding": "application/json" + }, + "description": "Describe the credentials that should be included in the DID doc of an account that is migrating to this service." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/identity/refreshIdentity.json b/lexicons/com/atproto/identity/refreshIdentity.json new file mode 100644 index 0000000000..5dd7fe46b6 --- /dev/null +++ b/lexicons/com/atproto/identity/refreshIdentity.json @@ -0,0 +1,47 @@ +{ + "id": "com.atproto.identity.refreshIdentity", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "identifier" + ], + "properties": { + "identifier": { + "type": "string", + "format": "at-identifier" + } + } + }, + "encoding": "application/json" + }, + "errors": [ + { + "name": "HandleNotFound", + "description": "The resolution process confirmed that the handle does not resolve to any DID." + }, + { + "name": "DidNotFound", + "description": "The DID resolution process confirmed that there is no current DID." + }, + { + "name": "DidDeactivated", + "description": "The DID previously existed, but has been deactivated." + } + ], + "output": { + "schema": { + "ref": "com.atproto.identity.defs#identityInfo", + "type": "ref" + }, + "encoding": "application/json" + }, + "description": "Request that the server re-resolve an identity (DID and handle). The server may ignore this request, or require authentication, depending on the role, implementation, and policy of the server." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/identity/requestPlcOperationSignature.json b/lexicons/com/atproto/identity/requestPlcOperationSignature.json new file mode 100644 index 0000000000..0d79ffc7aa --- /dev/null +++ b/lexicons/com/atproto/identity/requestPlcOperationSignature.json @@ -0,0 +1,11 @@ +{ + "id": "com.atproto.identity.requestPlcOperationSignature", + "defs": { + "main": { + "type": "procedure", + "description": "Request an email with a code to in order to request a signed PLC operation. Requires Auth." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/identity/resolveDid.json b/lexicons/com/atproto/identity/resolveDid.json new file mode 100644 index 0000000000..e6e8c88d90 --- /dev/null +++ b/lexicons/com/atproto/identity/resolveDid.json @@ -0,0 +1,49 @@ +{ + "id": "com.atproto.identity.resolveDid", + "defs": { + "main": { + "type": "query", + "errors": [ + { + "name": "DidNotFound", + "description": "The DID resolution process confirmed that there is no current DID." + }, + { + "name": "DidDeactivated", + "description": "The DID previously existed, but has been deactivated." + } + ], + "output": { + "schema": { + "type": "object", + "required": [ + "didDoc" + ], + "properties": { + "didDoc": { + "type": "unknown", + "description": "The complete DID document for the identity." + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "did" + ], + "properties": { + "did": { + "type": "string", + "format": "did", + "description": "DID to resolve." + } + } + }, + "description": "Resolves DID to DID document. Does not bi-directionally verify handle." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/identity/resolveHandle.json b/lexicons/com/atproto/identity/resolveHandle.json new file mode 100644 index 0000000000..855e1b9827 --- /dev/null +++ b/lexicons/com/atproto/identity/resolveHandle.json @@ -0,0 +1,45 @@ +{ + "id": "com.atproto.identity.resolveHandle", + "defs": { + "main": { + "type": "query", + "errors": [ + { + "name": "HandleNotFound", + "description": "The resolution process confirmed that the handle does not resolve to any DID." + } + ], + "output": { + "schema": { + "type": "object", + "required": [ + "did" + ], + "properties": { + "did": { + "type": "string", + "format": "did" + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "handle" + ], + "properties": { + "handle": { + "type": "string", + "format": "handle", + "description": "The handle to resolve." + } + } + }, + "description": "Resolves an atproto handle (hostname) to a DID. Does not necessarily bi-directionally verify against the the DID document." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/identity/resolveIdentity.json b/lexicons/com/atproto/identity/resolveIdentity.json new file mode 100644 index 0000000000..4da92470e3 --- /dev/null +++ b/lexicons/com/atproto/identity/resolveIdentity.json @@ -0,0 +1,45 @@ +{ + "id": "com.atproto.identity.resolveIdentity", + "defs": { + "main": { + "type": "query", + "errors": [ + { + "name": "HandleNotFound", + "description": "The resolution process confirmed that the handle does not resolve to any DID." + }, + { + "name": "DidNotFound", + "description": "The DID resolution process confirmed that there is no current DID." + }, + { + "name": "DidDeactivated", + "description": "The DID previously existed, but has been deactivated." + } + ], + "output": { + "schema": { + "ref": "com.atproto.identity.defs#identityInfo", + "type": "ref" + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "identifier" + ], + "properties": { + "identifier": { + "type": "string", + "format": "at-identifier", + "description": "Handle or DID to resolve." + } + } + }, + "description": "Resolves an identity (DID or Handle) to a full identity (DID document and verified handle)." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/identity/signPlcOperation.json b/lexicons/com/atproto/identity/signPlcOperation.json new file mode 100644 index 0000000000..0922c60e43 --- /dev/null +++ b/lexicons/com/atproto/identity/signPlcOperation.json @@ -0,0 +1,56 @@ +{ + "id": "com.atproto.identity.signPlcOperation", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "properties": { + "token": { + "type": "string", + "description": "A token received through com.atproto.identity.requestPlcOperationSignature" + }, + "services": { + "type": "unknown" + }, + "alsoKnownAs": { + "type": "array", + "items": { + "type": "string" + } + }, + "rotationKeys": { + "type": "array", + "items": { + "type": "string" + } + }, + "verificationMethods": { + "type": "unknown" + } + } + }, + "encoding": "application/json" + }, + "output": { + "schema": { + "type": "object", + "required": [ + "operation" + ], + "properties": { + "operation": { + "type": "unknown", + "description": "A signed DID PLC operation." + } + } + }, + "encoding": "application/json" + }, + "description": "Signs a PLC operation to update some value(s) in the requesting DID's document." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/identity/submitPlcOperation.json b/lexicons/com/atproto/identity/submitPlcOperation.json new file mode 100644 index 0000000000..ee0a6b2206 --- /dev/null +++ b/lexicons/com/atproto/identity/submitPlcOperation.json @@ -0,0 +1,25 @@ +{ + "id": "com.atproto.identity.submitPlcOperation", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "operation" + ], + "properties": { + "operation": { + "type": "unknown" + } + } + }, + "encoding": "application/json" + }, + "description": "Validates a PLC operation to ensure that it doesn't violate a service's constraints or get the identity into a bad state, then submits it to the PLC registry" + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/identity/updateHandle.json b/lexicons/com/atproto/identity/updateHandle.json new file mode 100644 index 0000000000..5ad0daabf7 --- /dev/null +++ b/lexicons/com/atproto/identity/updateHandle.json @@ -0,0 +1,27 @@ +{ + "id": "com.atproto.identity.updateHandle", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "handle" + ], + "properties": { + "handle": { + "type": "string", + "format": "handle", + "description": "The new handle." + } + } + }, + "encoding": "application/json" + }, + "description": "Updates the current account's handle. Verifies handle validity, and updates did:plc document if necessary. Implemented by PDS, and requires auth." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/label/defs.json b/lexicons/com/atproto/label/defs.json new file mode 100644 index 0000000000..4c2a357295 --- /dev/null +++ b/lexicons/com/atproto/label/defs.json @@ -0,0 +1,190 @@ +{ + "id": "com.atproto.label.defs", + "defs": { + "label": { + "type": "object", + "required": [ + "src", + "uri", + "val", + "cts" + ], + "properties": { + "cid": { + "type": "string", + "format": "cid", + "description": "Optionally, CID specifying the specific version of 'uri' resource this label applies to." + }, + "cts": { + "type": "string", + "format": "datetime", + "description": "Timestamp when this label was created." + }, + "exp": { + "type": "string", + "format": "datetime", + "description": "Timestamp at which this label expires (no longer applies)." + }, + "neg": { + "type": "boolean", + "description": "If true, this is a negation label, overwriting a previous label." + }, + "sig": { + "type": "bytes", + "description": "Signature of dag-cbor encoded label." + }, + "src": { + "type": "string", + "format": "did", + "description": "DID of the actor who created this label." + }, + "uri": { + "type": "string", + "format": "uri", + "description": "AT URI of the record, repository (account), or other resource that this label applies to." + }, + "val": { + "type": "string", + "maxLength": 128, + "description": "The short string name of the value or type of this label." + }, + "ver": { + "type": "integer", + "description": "The AT Protocol version of the label object." + } + }, + "description": "Metadata tag on an atproto resource (eg, repo or record)." + }, + "selfLabel": { + "type": "object", + "required": [ + "val" + ], + "properties": { + "val": { + "type": "string", + "maxLength": 128, + "description": "The short string name of the value or type of this label." + } + }, + "description": "Metadata tag on an atproto record, published by the author within the record. Note that schemas should use #selfLabels, not #selfLabel." + }, + "labelValue": { + "type": "string", + "knownValues": [ + "!hide", + "!warn", + "!no-unauthenticated", + "porn", + "sexual", + "nudity", + "graphic-media", + "bot" + ] + }, + "selfLabels": { + "type": "object", + "required": [ + "values" + ], + "properties": { + "values": { + "type": "array", + "items": { + "ref": "#selfLabel", + "type": "ref" + }, + "maxLength": 10 + } + }, + "description": "Metadata tags on an atproto record, published by the author within the record." + }, + "labelValueDefinition": { + "type": "object", + "required": [ + "identifier", + "severity", + "blurs", + "locales" + ], + "properties": { + "blurs": { + "type": "string", + "description": "What should this label hide in the UI, if applied? 'content' hides all of the target; 'media' hides the images/video/audio; 'none' hides nothing.", + "knownValues": [ + "content", + "media", + "none" + ] + }, + "locales": { + "type": "array", + "items": { + "ref": "#labelValueDefinitionStrings", + "type": "ref" + } + }, + "severity": { + "type": "string", + "description": "How should a client visually convey this label? 'inform' means neutral and informational; 'alert' means negative and warning; 'none' means show nothing.", + "knownValues": [ + "inform", + "alert", + "none" + ] + }, + "adultOnly": { + "type": "boolean", + "description": "Does the user need to have adult content enabled in order to configure this label?" + }, + "identifier": { + "type": "string", + "maxLength": 100, + "description": "The value of the label being defined. Must only include lowercase ascii and the '-' character ([a-z-]+).", + "maxGraphemes": 100 + }, + "defaultSetting": { + "type": "string", + "default": "warn", + "description": "The default setting for this label.", + "knownValues": [ + "ignore", + "warn", + "hide" + ] + } + }, + "description": "Declares a label value and its expected interpretations and behaviors." + }, + "labelValueDefinitionStrings": { + "type": "object", + "required": [ + "lang", + "name", + "description" + ], + "properties": { + "lang": { + "type": "string", + "format": "language", + "description": "The code of the language these strings are written in." + }, + "name": { + "type": "string", + "maxLength": 640, + "description": "A short human-readable name for the label.", + "maxGraphemes": 64 + }, + "description": { + "type": "string", + "maxLength": 100000, + "description": "A longer description of what the label means and why it might be applied.", + "maxGraphemes": 10000 + } + }, + "description": "Strings which describe the label in the UI, localized into a specific language." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/label/queryLabels.json b/lexicons/com/atproto/label/queryLabels.json new file mode 100644 index 0000000000..d147ecbcbc --- /dev/null +++ b/lexicons/com/atproto/label/queryLabels.json @@ -0,0 +1,64 @@ +{ + "id": "com.atproto.label.queryLabels", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "labels" + ], + "properties": { + "cursor": { + "type": "string" + }, + "labels": { + "type": "array", + "items": { + "ref": "com.atproto.label.defs#label", + "type": "ref" + } + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "uriPatterns" + ], + "properties": { + "limit": { + "type": "integer", + "default": 50, + "maximum": 250, + "minimum": 1 + }, + "cursor": { + "type": "string" + }, + "sources": { + "type": "array", + "items": { + "type": "string", + "format": "did" + }, + "description": "Optional list of label sources (DIDs) to filter on." + }, + "uriPatterns": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of AT URI patterns to match (boolean 'OR'). Each may be a prefix (ending with '*'; will match inclusive of the string leading to '*'), or a full URI." + } + } + }, + "description": "Find labels relevant to the provided AT-URI patterns. Public endpoint for moderation services, though may return different or additional results with auth." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/label/subscribeLabels.json b/lexicons/com/atproto/label/subscribeLabels.json new file mode 100644 index 0000000000..984ed19220 --- /dev/null +++ b/lexicons/com/atproto/label/subscribeLabels.json @@ -0,0 +1,70 @@ +{ + "id": "com.atproto.label.subscribeLabels", + "defs": { + "info": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "knownValues": [ + "OutdatedCursor" + ] + }, + "message": { + "type": "string" + } + } + }, + "main": { + "type": "subscription", + "errors": [ + { + "name": "FutureCursor" + } + ], + "message": { + "schema": { + "refs": [ + "#labels", + "#info" + ], + "type": "union" + } + }, + "parameters": { + "type": "params", + "properties": { + "cursor": { + "type": "integer", + "description": "The last known event seq number to backfill from." + } + } + }, + "description": "Subscribe to stream of labels (and negations). Public endpoint implemented by mod services. Uses same sequencing scheme as repo event stream." + }, + "labels": { + "type": "object", + "required": [ + "seq", + "labels" + ], + "properties": { + "seq": { + "type": "integer" + }, + "labels": { + "type": "array", + "items": { + "ref": "com.atproto.label.defs#label", + "type": "ref" + } + } + } + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/lexicon/resolveLexicon.json b/lexicons/com/atproto/lexicon/resolveLexicon.json new file mode 100644 index 0000000000..6807a4dcf8 --- /dev/null +++ b/lexicons/com/atproto/lexicon/resolveLexicon.json @@ -0,0 +1,58 @@ +{ + "id": "com.atproto.lexicon.resolveLexicon", + "defs": { + "main": { + "type": "query", + "errors": [ + { + "name": "LexiconNotFound", + "description": "No lexicon was resolved for the NSID." + } + ], + "output": { + "schema": { + "type": "object", + "required": [ + "uri", + "cid", + "schema" + ], + "properties": { + "cid": { + "type": "string", + "format": "cid", + "description": "The CID of the lexicon schema record." + }, + "uri": { + "type": "string", + "format": "at-uri", + "description": "The AT-URI of the lexicon schema record." + }, + "schema": { + "ref": "com.atproto.lexicon.schema#main", + "type": "ref", + "description": "The resolved lexicon schema record." + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "nsid" + ], + "properties": { + "nsid": { + "type": "string", + "format": "nsid", + "description": "The lexicon NSID to resolve." + } + } + }, + "description": "Resolves an atproto lexicon (NSID) to a schema." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/lexicon/schema.json b/lexicons/com/atproto/lexicon/schema.json new file mode 100644 index 0000000000..824b1086d8 --- /dev/null +++ b/lexicons/com/atproto/lexicon/schema.json @@ -0,0 +1,24 @@ +{ + "id": "com.atproto.lexicon.schema", + "defs": { + "main": { + "key": "nsid", + "type": "record", + "record": { + "type": "object", + "required": [ + "lexicon" + ], + "properties": { + "lexicon": { + "type": "integer", + "description": "Indicates the 'version' of the Lexicon language. Must be '1' for the current atproto/Lexicon schema system." + } + } + }, + "description": "Representation of Lexicon schemas themselves, when published as atproto records. Note that the schema language is not defined in Lexicon; this meta schema currently only includes a single version field ('lexicon'). See the atproto specifications for description of the other expected top-level fields ('id', 'defs', etc)." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/moderation/createReport.json b/lexicons/com/atproto/moderation/createReport.json new file mode 100644 index 0000000000..ee6f58525f --- /dev/null +++ b/lexicons/com/atproto/moderation/createReport.json @@ -0,0 +1,104 @@ +{ + "id": "com.atproto.moderation.createReport", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "reasonType", + "subject" + ], + "properties": { + "reason": { + "type": "string", + "maxLength": 20000, + "description": "Additional context about the content and violation.", + "maxGraphemes": 2000 + }, + "modTool": { + "ref": "#modTool", + "type": "ref" + }, + "subject": { + "refs": [ + "com.atproto.admin.defs#repoRef", + "com.atproto.repo.strongRef" + ], + "type": "union" + }, + "reasonType": { + "ref": "com.atproto.moderation.defs#reasonType", + "type": "ref", + "description": "Indicates the broad category of violation the report is for." + } + } + }, + "encoding": "application/json" + }, + "output": { + "schema": { + "type": "object", + "required": [ + "id", + "reasonType", + "subject", + "reportedBy", + "createdAt" + ], + "properties": { + "id": { + "type": "integer" + }, + "reason": { + "type": "string", + "maxLength": 20000, + "maxGraphemes": 2000 + }, + "subject": { + "refs": [ + "com.atproto.admin.defs#repoRef", + "com.atproto.repo.strongRef" + ], + "type": "union" + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "reasonType": { + "ref": "com.atproto.moderation.defs#reasonType", + "type": "ref" + }, + "reportedBy": { + "type": "string", + "format": "did" + } + } + }, + "encoding": "application/json" + }, + "description": "Submit a moderation report regarding an atproto account or record. Implemented by moderation services (with PDS proxying), and requires auth." + }, + "modTool": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "meta": { + "type": "unknown", + "description": "Additional arbitrary metadata about the source" + }, + "name": { + "type": "string", + "description": "Name/identifier of the source (e.g., 'bsky-app/android', 'bsky-web/chrome')" + } + }, + "description": "Moderation tool information for tracing the source of the action" + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/moderation/defs.json b/lexicons/com/atproto/moderation/defs.json new file mode 100644 index 0000000000..57fff90b21 --- /dev/null +++ b/lexicons/com/atproto/moderation/defs.json @@ -0,0 +1,96 @@ +{ + "id": "com.atproto.moderation.defs", + "defs": { + "reasonRude": { + "type": "token", + "description": "Rude, harassing, explicit, or otherwise unwelcoming behavior. Prefer new lexicon definition `tools.ozone.report.defs#reasonHarassmentOther`." + }, + "reasonSpam": { + "type": "token", + "description": "Spam: frequent unwanted promotion, replies, mentions. Prefer new lexicon definition `tools.ozone.report.defs#reasonMisleadingSpam`." + }, + "reasonType": { + "type": "string", + "knownValues": [ + "com.atproto.moderation.defs#reasonSpam", + "com.atproto.moderation.defs#reasonViolation", + "com.atproto.moderation.defs#reasonMisleading", + "com.atproto.moderation.defs#reasonSexual", + "com.atproto.moderation.defs#reasonRude", + "com.atproto.moderation.defs#reasonOther", + "com.atproto.moderation.defs#reasonAppeal", + "tools.ozone.report.defs#reasonAppeal", + "tools.ozone.report.defs#reasonOther", + "tools.ozone.report.defs#reasonViolenceAnimal", + "tools.ozone.report.defs#reasonViolenceThreats", + "tools.ozone.report.defs#reasonViolenceGraphicContent", + "tools.ozone.report.defs#reasonViolenceGlorification", + "tools.ozone.report.defs#reasonViolenceExtremistContent", + "tools.ozone.report.defs#reasonViolenceTrafficking", + "tools.ozone.report.defs#reasonViolenceOther", + "tools.ozone.report.defs#reasonSexualAbuseContent", + "tools.ozone.report.defs#reasonSexualNCII", + "tools.ozone.report.defs#reasonSexualDeepfake", + "tools.ozone.report.defs#reasonSexualAnimal", + "tools.ozone.report.defs#reasonSexualUnlabeled", + "tools.ozone.report.defs#reasonSexualOther", + "tools.ozone.report.defs#reasonChildSafetyCSAM", + "tools.ozone.report.defs#reasonChildSafetyGroom", + "tools.ozone.report.defs#reasonChildSafetyPrivacy", + "tools.ozone.report.defs#reasonChildSafetyHarassment", + "tools.ozone.report.defs#reasonChildSafetyOther", + "tools.ozone.report.defs#reasonHarassmentTroll", + "tools.ozone.report.defs#reasonHarassmentTargeted", + "tools.ozone.report.defs#reasonHarassmentHateSpeech", + "tools.ozone.report.defs#reasonHarassmentDoxxing", + "tools.ozone.report.defs#reasonHarassmentOther", + "tools.ozone.report.defs#reasonMisleadingBot", + "tools.ozone.report.defs#reasonMisleadingImpersonation", + "tools.ozone.report.defs#reasonMisleadingSpam", + "tools.ozone.report.defs#reasonMisleadingScam", + "tools.ozone.report.defs#reasonMisleadingElections", + "tools.ozone.report.defs#reasonMisleadingOther", + "tools.ozone.report.defs#reasonRuleSiteSecurity", + "tools.ozone.report.defs#reasonRuleProhibitedSales", + "tools.ozone.report.defs#reasonRuleBanEvasion", + "tools.ozone.report.defs#reasonRuleOther", + "tools.ozone.report.defs#reasonSelfHarmContent", + "tools.ozone.report.defs#reasonSelfHarmED", + "tools.ozone.report.defs#reasonSelfHarmStunts", + "tools.ozone.report.defs#reasonSelfHarmSubstances", + "tools.ozone.report.defs#reasonSelfHarmOther" + ] + }, + "reasonOther": { + "type": "token", + "description": "Reports not falling under another report category. Prefer new lexicon definition `tools.ozone.report.defs#reasonOther`." + }, + "subjectType": { + "type": "string", + "description": "Tag describing a type of subject that might be reported.", + "knownValues": [ + "account", + "record", + "chat" + ] + }, + "reasonAppeal": { + "type": "token", + "description": "Appeal a previously taken moderation action" + }, + "reasonSexual": { + "type": "token", + "description": "Unwanted or mislabeled sexual content. Prefer new lexicon definition `tools.ozone.report.defs#reasonSexualUnlabeled`." + }, + "reasonViolation": { + "type": "token", + "description": "Direct violation of server rules, laws, terms of service. Prefer new lexicon definition `tools.ozone.report.defs#reasonRuleOther`." + }, + "reasonMisleading": { + "type": "token", + "description": "Misleading identity, affiliation, or content. Prefer new lexicon definition `tools.ozone.report.defs#reasonMisleadingOther`." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/repo/applyWrites.json b/lexicons/com/atproto/repo/applyWrites.json new file mode 100644 index 0000000000..f82bf2ba88 --- /dev/null +++ b/lexicons/com/atproto/repo/applyWrites.json @@ -0,0 +1,196 @@ +{ + "id": "com.atproto.repo.applyWrites", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "repo", + "writes" + ], + "properties": { + "repo": { + "type": "string", + "format": "at-identifier", + "description": "The handle or DID of the repo (aka, current account)." + }, + "writes": { + "type": "array", + "items": { + "refs": [ + "#create", + "#update", + "#delete" + ], + "type": "union", + "closed": true + } + }, + "validate": { + "type": "boolean", + "description": "Can be set to 'false' to skip Lexicon schema validation of record data across all operations, 'true' to require it, or leave unset to validate only for known Lexicons." + }, + "swapCommit": { + "type": "string", + "format": "cid", + "description": "If provided, the entire operation will fail if the current repo commit CID does not match this value. Used to prevent conflicting repo mutations." + } + } + }, + "encoding": "application/json" + }, + "errors": [ + { + "name": "InvalidSwap", + "description": "Indicates that the 'swapCommit' parameter did not match current commit." + } + ], + "output": { + "schema": { + "type": "object", + "required": [], + "properties": { + "commit": { + "ref": "com.atproto.repo.defs#commitMeta", + "type": "ref" + }, + "results": { + "type": "array", + "items": { + "refs": [ + "#createResult", + "#updateResult", + "#deleteResult" + ], + "type": "union", + "closed": true + } + } + } + }, + "encoding": "application/json" + }, + "description": "Apply a batch transaction of repository creates, updates, and deletes. Requires auth, implemented by PDS." + }, + "create": { + "type": "object", + "required": [ + "collection", + "value" + ], + "properties": { + "rkey": { + "type": "string", + "format": "record-key", + "maxLength": 512, + "description": "NOTE: maxLength is redundant with record-key format. Keeping it temporarily to ensure backwards compatibility." + }, + "value": { + "type": "unknown" + }, + "collection": { + "type": "string", + "format": "nsid" + } + }, + "description": "Operation which creates a new record." + }, + "delete": { + "type": "object", + "required": [ + "collection", + "rkey" + ], + "properties": { + "rkey": { + "type": "string", + "format": "record-key" + }, + "collection": { + "type": "string", + "format": "nsid" + } + }, + "description": "Operation which deletes an existing record." + }, + "update": { + "type": "object", + "required": [ + "collection", + "rkey", + "value" + ], + "properties": { + "rkey": { + "type": "string", + "format": "record-key" + }, + "value": { + "type": "unknown" + }, + "collection": { + "type": "string", + "format": "nsid" + } + }, + "description": "Operation which updates an existing record." + }, + "createResult": { + "type": "object", + "required": [ + "uri", + "cid" + ], + "properties": { + "cid": { + "type": "string", + "format": "cid" + }, + "uri": { + "type": "string", + "format": "at-uri" + }, + "validationStatus": { + "type": "string", + "knownValues": [ + "valid", + "unknown" + ] + } + } + }, + "deleteResult": { + "type": "object", + "required": [], + "properties": {} + }, + "updateResult": { + "type": "object", + "required": [ + "uri", + "cid" + ], + "properties": { + "cid": { + "type": "string", + "format": "cid" + }, + "uri": { + "type": "string", + "format": "at-uri" + }, + "validationStatus": { + "type": "string", + "knownValues": [ + "valid", + "unknown" + ] + } + } + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/repo/createRecord.json b/lexicons/com/atproto/repo/createRecord.json new file mode 100644 index 0000000000..2483457b9e --- /dev/null +++ b/lexicons/com/atproto/repo/createRecord.json @@ -0,0 +1,90 @@ +{ + "id": "com.atproto.repo.createRecord", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "repo", + "collection", + "record" + ], + "properties": { + "repo": { + "type": "string", + "format": "at-identifier", + "description": "The handle or DID of the repo (aka, current account)." + }, + "rkey": { + "type": "string", + "format": "record-key", + "maxLength": 512, + "description": "The Record Key." + }, + "record": { + "type": "unknown", + "description": "The record itself. Must contain a $type field." + }, + "validate": { + "type": "boolean", + "description": "Can be set to 'false' to skip Lexicon schema validation of record data, 'true' to require it, or leave unset to validate only for known Lexicons." + }, + "collection": { + "type": "string", + "format": "nsid", + "description": "The NSID of the record collection." + }, + "swapCommit": { + "type": "string", + "format": "cid", + "description": "Compare and swap with the previous commit by CID." + } + } + }, + "encoding": "application/json" + }, + "errors": [ + { + "name": "InvalidSwap", + "description": "Indicates that 'swapCommit' didn't match current repo commit." + } + ], + "output": { + "schema": { + "type": "object", + "required": [ + "uri", + "cid" + ], + "properties": { + "cid": { + "type": "string", + "format": "cid" + }, + "uri": { + "type": "string", + "format": "at-uri" + }, + "commit": { + "ref": "com.atproto.repo.defs#commitMeta", + "type": "ref" + }, + "validationStatus": { + "type": "string", + "knownValues": [ + "valid", + "unknown" + ] + } + } + }, + "encoding": "application/json" + }, + "description": "Create a single new repository record. Requires auth, implemented by PDS." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/repo/defs.json b/lexicons/com/atproto/repo/defs.json new file mode 100644 index 0000000000..f0d13f7d45 --- /dev/null +++ b/lexicons/com/atproto/repo/defs.json @@ -0,0 +1,24 @@ +{ + "id": "com.atproto.repo.defs", + "defs": { + "commitMeta": { + "type": "object", + "required": [ + "cid", + "rev" + ], + "properties": { + "cid": { + "type": "string", + "format": "cid" + }, + "rev": { + "type": "string", + "format": "tid" + } + } + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/repo/deleteRecord.json b/lexicons/com/atproto/repo/deleteRecord.json new file mode 100644 index 0000000000..21a25165de --- /dev/null +++ b/lexicons/com/atproto/repo/deleteRecord.json @@ -0,0 +1,66 @@ +{ + "id": "com.atproto.repo.deleteRecord", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "repo", + "collection", + "rkey" + ], + "properties": { + "repo": { + "type": "string", + "format": "at-identifier", + "description": "The handle or DID of the repo (aka, current account)." + }, + "rkey": { + "type": "string", + "format": "record-key", + "description": "The Record Key." + }, + "collection": { + "type": "string", + "format": "nsid", + "description": "The NSID of the record collection." + }, + "swapCommit": { + "type": "string", + "format": "cid", + "description": "Compare and swap with the previous commit by CID." + }, + "swapRecord": { + "type": "string", + "format": "cid", + "description": "Compare and swap with the previous record by CID." + } + } + }, + "encoding": "application/json" + }, + "errors": [ + { + "name": "InvalidSwap" + } + ], + "output": { + "schema": { + "type": "object", + "properties": { + "commit": { + "ref": "com.atproto.repo.defs#commitMeta", + "type": "ref" + } + } + }, + "encoding": "application/json" + }, + "description": "Delete a repository record, or ensure it doesn't exist. Requires auth, implemented by PDS." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/repo/describeRepo.json b/lexicons/com/atproto/repo/describeRepo.json new file mode 100644 index 0000000000..05e9cb42cd --- /dev/null +++ b/lexicons/com/atproto/repo/describeRepo.json @@ -0,0 +1,63 @@ +{ + "id": "com.atproto.repo.describeRepo", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "handle", + "did", + "didDoc", + "collections", + "handleIsCorrect" + ], + "properties": { + "did": { + "type": "string", + "format": "did" + }, + "didDoc": { + "type": "unknown", + "description": "The complete DID document for this account." + }, + "handle": { + "type": "string", + "format": "handle" + }, + "collections": { + "type": "array", + "items": { + "type": "string", + "format": "nsid" + }, + "description": "List of all the collections (NSIDs) for which this repo contains at least one record." + }, + "handleIsCorrect": { + "type": "boolean", + "description": "Indicates if handle is currently valid (resolves bi-directionally)" + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "repo" + ], + "properties": { + "repo": { + "type": "string", + "format": "at-identifier", + "description": "The handle or DID of the repo." + } + } + }, + "description": "Get information about an account and repository, including the list of collections. Does not require auth." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/repo/getRecord.json b/lexicons/com/atproto/repo/getRecord.json new file mode 100644 index 0000000000..ffbaf8ee74 --- /dev/null +++ b/lexicons/com/atproto/repo/getRecord.json @@ -0,0 +1,69 @@ +{ + "id": "com.atproto.repo.getRecord", + "defs": { + "main": { + "type": "query", + "errors": [ + { + "name": "RecordNotFound" + } + ], + "output": { + "schema": { + "type": "object", + "required": [ + "uri", + "value" + ], + "properties": { + "cid": { + "type": "string", + "format": "cid" + }, + "uri": { + "type": "string", + "format": "at-uri" + }, + "value": { + "type": "unknown" + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "repo", + "collection", + "rkey" + ], + "properties": { + "cid": { + "type": "string", + "format": "cid", + "description": "The CID of the version of the record. If not specified, then return the most recent version." + }, + "repo": { + "type": "string", + "format": "at-identifier", + "description": "The handle or DID of the repo." + }, + "rkey": { + "type": "string", + "format": "record-key", + "description": "The Record Key." + }, + "collection": { + "type": "string", + "format": "nsid", + "description": "The NSID of the record collection." + } + } + }, + "description": "Get a single record from a repository. Does not require auth." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/repo/importRepo.json b/lexicons/com/atproto/repo/importRepo.json new file mode 100644 index 0000000000..897e329160 --- /dev/null +++ b/lexicons/com/atproto/repo/importRepo.json @@ -0,0 +1,14 @@ +{ + "id": "com.atproto.repo.importRepo", + "defs": { + "main": { + "type": "procedure", + "input": { + "encoding": "application/vnd.ipld.car" + }, + "description": "Import a repo in the form of a CAR file. Requires Content-Length HTTP header to be set." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/repo/listMissingBlobs.json b/lexicons/com/atproto/repo/listMissingBlobs.json new file mode 100644 index 0000000000..41c831af66 --- /dev/null +++ b/lexicons/com/atproto/repo/listMissingBlobs.json @@ -0,0 +1,63 @@ +{ + "id": "com.atproto.repo.listMissingBlobs", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "blobs" + ], + "properties": { + "blobs": { + "type": "array", + "items": { + "ref": "#recordBlob", + "type": "ref" + } + }, + "cursor": { + "type": "string" + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "properties": { + "limit": { + "type": "integer", + "default": 500, + "maximum": 1000, + "minimum": 1 + }, + "cursor": { + "type": "string" + } + } + }, + "description": "Returns a list of missing blobs for the requesting account. Intended to be used in the account migration flow." + }, + "recordBlob": { + "type": "object", + "required": [ + "cid", + "recordUri" + ], + "properties": { + "cid": { + "type": "string", + "format": "cid" + }, + "recordUri": { + "type": "string", + "format": "at-uri" + } + } + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/repo/listRecords.json b/lexicons/com/atproto/repo/listRecords.json new file mode 100644 index 0000000000..c34f519ed8 --- /dev/null +++ b/lexicons/com/atproto/repo/listRecords.json @@ -0,0 +1,86 @@ +{ + "id": "com.atproto.repo.listRecords", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "records" + ], + "properties": { + "cursor": { + "type": "string" + }, + "records": { + "type": "array", + "items": { + "ref": "#record", + "type": "ref" + } + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "repo", + "collection" + ], + "properties": { + "repo": { + "type": "string", + "format": "at-identifier", + "description": "The handle or DID of the repo." + }, + "limit": { + "type": "integer", + "default": 50, + "maximum": 100, + "minimum": 1, + "description": "The number of records to return." + }, + "cursor": { + "type": "string" + }, + "reverse": { + "type": "boolean", + "description": "Flag to reverse the order of the returned records." + }, + "collection": { + "type": "string", + "format": "nsid", + "description": "The NSID of the record type." + } + } + }, + "description": "List a range of records in a repository, matching a specific collection. Does not require auth." + }, + "record": { + "type": "object", + "required": [ + "uri", + "cid", + "value" + ], + "properties": { + "cid": { + "type": "string", + "format": "cid" + }, + "uri": { + "type": "string", + "format": "at-uri" + }, + "value": { + "type": "unknown" + } + } + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/repo/putRecord.json b/lexicons/com/atproto/repo/putRecord.json new file mode 100644 index 0000000000..6584a2048a --- /dev/null +++ b/lexicons/com/atproto/repo/putRecord.json @@ -0,0 +1,98 @@ +{ + "id": "com.atproto.repo.putRecord", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "nullable": [ + "swapRecord" + ], + "required": [ + "repo", + "collection", + "rkey", + "record" + ], + "properties": { + "repo": { + "type": "string", + "format": "at-identifier", + "description": "The handle or DID of the repo (aka, current account)." + }, + "rkey": { + "type": "string", + "format": "record-key", + "maxLength": 512, + "description": "The Record Key." + }, + "record": { + "type": "unknown", + "description": "The record to write." + }, + "validate": { + "type": "boolean", + "description": "Can be set to 'false' to skip Lexicon schema validation of record data, 'true' to require it, or leave unset to validate only for known Lexicons." + }, + "collection": { + "type": "string", + "format": "nsid", + "description": "The NSID of the record collection." + }, + "swapCommit": { + "type": "string", + "format": "cid", + "description": "Compare and swap with the previous commit by CID." + }, + "swapRecord": { + "type": "string", + "format": "cid", + "description": "Compare and swap with the previous record by CID. WARNING: nullable and optional field; may cause problems with golang implementation" + } + } + }, + "encoding": "application/json" + }, + "errors": [ + { + "name": "InvalidSwap" + } + ], + "output": { + "schema": { + "type": "object", + "required": [ + "uri", + "cid" + ], + "properties": { + "cid": { + "type": "string", + "format": "cid" + }, + "uri": { + "type": "string", + "format": "at-uri" + }, + "commit": { + "ref": "com.atproto.repo.defs#commitMeta", + "type": "ref" + }, + "validationStatus": { + "type": "string", + "knownValues": [ + "valid", + "unknown" + ] + } + } + }, + "encoding": "application/json" + }, + "description": "Write a repository record, creating or updating it as needed. Requires auth, implemented by PDS." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/repo/strongRef.json b/lexicons/com/atproto/repo/strongRef.json new file mode 100644 index 0000000000..f45cca3908 --- /dev/null +++ b/lexicons/com/atproto/repo/strongRef.json @@ -0,0 +1,25 @@ +{ + "id": "com.atproto.repo.strongRef", + "defs": { + "main": { + "type": "object", + "required": [ + "uri", + "cid" + ], + "properties": { + "cid": { + "type": "string", + "format": "cid" + }, + "uri": { + "type": "string", + "format": "at-uri" + } + } + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1, + "description": "A URI with a content-hash fingerprint." +} diff --git a/lexicons/com/atproto/repo/uploadBlob.json b/lexicons/com/atproto/repo/uploadBlob.json new file mode 100644 index 0000000000..9b1bdd4a93 --- /dev/null +++ b/lexicons/com/atproto/repo/uploadBlob.json @@ -0,0 +1,28 @@ +{ + "id": "com.atproto.repo.uploadBlob", + "defs": { + "main": { + "type": "procedure", + "input": { + "encoding": "*/*" + }, + "output": { + "schema": { + "type": "object", + "required": [ + "blob" + ], + "properties": { + "blob": { + "type": "blob" + } + } + }, + "encoding": "application/json" + }, + "description": "Upload a new blob, to be referenced from a repository record. The blob will be deleted if it is not referenced within a time window (eg, minutes). Blob restrictions (mimetype, size, etc) are enforced when the reference is created. Requires auth, implemented by PDS." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/server/activateAccount.json b/lexicons/com/atproto/server/activateAccount.json new file mode 100644 index 0000000000..459607c827 --- /dev/null +++ b/lexicons/com/atproto/server/activateAccount.json @@ -0,0 +1,11 @@ +{ + "id": "com.atproto.server.activateAccount", + "defs": { + "main": { + "type": "procedure", + "description": "Activates a currently deactivated account. Used to finalize account migration after the account's repo is imported and identity is setup." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/server/checkAccountStatus.json b/lexicons/com/atproto/server/checkAccountStatus.json new file mode 100644 index 0000000000..289ca52c2e --- /dev/null +++ b/lexicons/com/atproto/server/checkAccountStatus.json @@ -0,0 +1,58 @@ +{ + "id": "com.atproto.server.checkAccountStatus", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "activated", + "validDid", + "repoCommit", + "repoRev", + "repoBlocks", + "indexedRecords", + "privateStateValues", + "expectedBlobs", + "importedBlobs" + ], + "properties": { + "repoRev": { + "type": "string" + }, + "validDid": { + "type": "boolean" + }, + "activated": { + "type": "boolean" + }, + "repoBlocks": { + "type": "integer" + }, + "repoCommit": { + "type": "string", + "format": "cid" + }, + "expectedBlobs": { + "type": "integer" + }, + "importedBlobs": { + "type": "integer" + }, + "indexedRecords": { + "type": "integer" + }, + "privateStateValues": { + "type": "integer" + } + } + }, + "encoding": "application/json" + }, + "description": "Returns the status of an account, especially as pertaining to import or recovery. Can be called many times over the course of an account migration. Requires auth and can only be called pertaining to oneself." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/server/confirmEmail.json b/lexicons/com/atproto/server/confirmEmail.json new file mode 100644 index 0000000000..4412676285 --- /dev/null +++ b/lexicons/com/atproto/server/confirmEmail.json @@ -0,0 +1,43 @@ +{ + "id": "com.atproto.server.confirmEmail", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "email", + "token" + ], + "properties": { + "email": { + "type": "string" + }, + "token": { + "type": "string" + } + } + }, + "encoding": "application/json" + }, + "errors": [ + { + "name": "AccountNotFound" + }, + { + "name": "ExpiredToken" + }, + { + "name": "InvalidToken" + }, + { + "name": "InvalidEmail" + } + ], + "description": "Confirm an email using a token from com.atproto.server.requestEmailConfirmation." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/server/createAccount.json b/lexicons/com/atproto/server/createAccount.json new file mode 100644 index 0000000000..5b14ca30ce --- /dev/null +++ b/lexicons/com/atproto/server/createAccount.json @@ -0,0 +1,113 @@ +{ + "id": "com.atproto.server.createAccount", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "handle" + ], + "properties": { + "did": { + "type": "string", + "format": "did", + "description": "Pre-existing atproto DID, being imported to a new account." + }, + "email": { + "type": "string" + }, + "plcOp": { + "type": "unknown", + "description": "A signed DID PLC operation to be submitted as part of importing an existing account to this instance. NOTE: this optional field may be updated when full account migration is implemented." + }, + "handle": { + "type": "string", + "format": "handle", + "description": "Requested handle for the account." + }, + "password": { + "type": "string", + "description": "Initial account password. May need to meet instance-specific password strength requirements." + }, + "inviteCode": { + "type": "string" + }, + "recoveryKey": { + "type": "string", + "description": "DID PLC rotation key (aka, recovery key) to be included in PLC creation operation." + }, + "verificationCode": { + "type": "string" + }, + "verificationPhone": { + "type": "string" + } + } + }, + "encoding": "application/json" + }, + "errors": [ + { + "name": "InvalidHandle" + }, + { + "name": "InvalidPassword" + }, + { + "name": "InvalidInviteCode" + }, + { + "name": "HandleNotAvailable" + }, + { + "name": "UnsupportedDomain" + }, + { + "name": "UnresolvableDid" + }, + { + "name": "IncompatibleDidDoc" + } + ], + "output": { + "schema": { + "type": "object", + "required": [ + "accessJwt", + "refreshJwt", + "handle", + "did" + ], + "properties": { + "did": { + "type": "string", + "format": "did", + "description": "The DID of the new account." + }, + "didDoc": { + "type": "unknown", + "description": "Complete DID document." + }, + "handle": { + "type": "string", + "format": "handle" + }, + "accessJwt": { + "type": "string" + }, + "refreshJwt": { + "type": "string" + } + }, + "description": "Account login session returned on successful account creation." + }, + "encoding": "application/json" + }, + "description": "Create an account. Implemented by PDS." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/server/createAppPassword.json b/lexicons/com/atproto/server/createAppPassword.json new file mode 100644 index 0000000000..72e24ba7b2 --- /dev/null +++ b/lexicons/com/atproto/server/createAppPassword.json @@ -0,0 +1,65 @@ +{ + "id": "com.atproto.server.createAppPassword", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "description": "A short name for the App Password, to help distinguish them." + }, + "privileged": { + "type": "boolean", + "description": "If an app password has 'privileged' access to possibly sensitive account state. Meant for use with trusted clients." + } + } + }, + "encoding": "application/json" + }, + "errors": [ + { + "name": "AccountTakedown" + } + ], + "output": { + "schema": { + "ref": "#appPassword", + "type": "ref" + }, + "encoding": "application/json" + }, + "description": "Create an App Password." + }, + "appPassword": { + "type": "object", + "required": [ + "name", + "password", + "createdAt" + ], + "properties": { + "name": { + "type": "string" + }, + "password": { + "type": "string" + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "privileged": { + "type": "boolean" + } + } + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/server/createInviteCode.json b/lexicons/com/atproto/server/createInviteCode.json new file mode 100644 index 0000000000..d355ddfec3 --- /dev/null +++ b/lexicons/com/atproto/server/createInviteCode.json @@ -0,0 +1,43 @@ +{ + "id": "com.atproto.server.createInviteCode", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "useCount" + ], + "properties": { + "useCount": { + "type": "integer" + }, + "forAccount": { + "type": "string", + "format": "did" + } + } + }, + "encoding": "application/json" + }, + "output": { + "schema": { + "type": "object", + "required": [ + "code" + ], + "properties": { + "code": { + "type": "string" + } + } + }, + "encoding": "application/json" + }, + "description": "Create an invite code." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/server/createInviteCodes.json b/lexicons/com/atproto/server/createInviteCodes.json new file mode 100644 index 0000000000..4843e28dc0 --- /dev/null +++ b/lexicons/com/atproto/server/createInviteCodes.json @@ -0,0 +1,73 @@ +{ + "id": "com.atproto.server.createInviteCodes", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "codeCount", + "useCount" + ], + "properties": { + "useCount": { + "type": "integer" + }, + "codeCount": { + "type": "integer", + "default": 1 + }, + "forAccounts": { + "type": "array", + "items": { + "type": "string", + "format": "did" + } + } + } + }, + "encoding": "application/json" + }, + "output": { + "schema": { + "type": "object", + "required": [ + "codes" + ], + "properties": { + "codes": { + "type": "array", + "items": { + "ref": "#accountCodes", + "type": "ref" + } + } + } + }, + "encoding": "application/json" + }, + "description": "Create invite codes." + }, + "accountCodes": { + "type": "object", + "required": [ + "account", + "codes" + ], + "properties": { + "codes": { + "type": "array", + "items": { + "type": "string" + } + }, + "account": { + "type": "string" + } + } + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/server/createSession.json b/lexicons/com/atproto/server/createSession.json new file mode 100644 index 0000000000..1b353cbb59 --- /dev/null +++ b/lexicons/com/atproto/server/createSession.json @@ -0,0 +1,97 @@ +{ + "id": "com.atproto.server.createSession", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "identifier", + "password" + ], + "properties": { + "password": { + "type": "string" + }, + "identifier": { + "type": "string", + "description": "Handle or other identifier supported by the server for the authenticating user." + }, + "allowTakendown": { + "type": "boolean", + "description": "When true, instead of throwing error for takendown accounts, a valid response with a narrow scoped token will be returned" + }, + "authFactorToken": { + "type": "string" + } + } + }, + "encoding": "application/json" + }, + "errors": [ + { + "name": "AccountTakedown" + }, + { + "name": "AuthFactorTokenRequired" + } + ], + "output": { + "schema": { + "type": "object", + "required": [ + "accessJwt", + "refreshJwt", + "handle", + "did" + ], + "properties": { + "did": { + "type": "string", + "format": "did" + }, + "email": { + "type": "string" + }, + "active": { + "type": "boolean" + }, + "didDoc": { + "type": "unknown" + }, + "handle": { + "type": "string", + "format": "handle" + }, + "status": { + "type": "string", + "description": "If active=false, this optional field indicates a possible reason for why the account is not active. If active=false and no status is supplied, then the host makes no claim for why the repository is no longer being hosted.", + "knownValues": [ + "takendown", + "suspended", + "deactivated" + ] + }, + "accessJwt": { + "type": "string" + }, + "refreshJwt": { + "type": "string" + }, + "emailConfirmed": { + "type": "boolean" + }, + "emailAuthFactor": { + "type": "boolean" + } + } + }, + "encoding": "application/json" + }, + "description": "Create an authentication session." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/server/deactivateAccount.json b/lexicons/com/atproto/server/deactivateAccount.json new file mode 100644 index 0000000000..0607e0efc6 --- /dev/null +++ b/lexicons/com/atproto/server/deactivateAccount.json @@ -0,0 +1,24 @@ +{ + "id": "com.atproto.server.deactivateAccount", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "properties": { + "deleteAfter": { + "type": "string", + "format": "datetime", + "description": "A recommendation to server as to how long they should hold onto the deactivated account before deleting." + } + } + }, + "encoding": "application/json" + }, + "description": "Deactivates a currently active account. Stops serving of repo, and future writes to repo until reactivated. Used to finalize account migration with the old host after the account has been activated on the new host." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/server/defs.json b/lexicons/com/atproto/server/defs.json new file mode 100644 index 0000000000..60cdf91571 --- /dev/null +++ b/lexicons/com/atproto/server/defs.json @@ -0,0 +1,64 @@ +{ + "id": "com.atproto.server.defs", + "defs": { + "inviteCode": { + "type": "object", + "required": [ + "code", + "available", + "disabled", + "forAccount", + "createdBy", + "createdAt", + "uses" + ], + "properties": { + "code": { + "type": "string" + }, + "uses": { + "type": "array", + "items": { + "ref": "#inviteCodeUse", + "type": "ref" + } + }, + "disabled": { + "type": "boolean" + }, + "available": { + "type": "integer" + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "createdBy": { + "type": "string" + }, + "forAccount": { + "type": "string" + } + } + }, + "inviteCodeUse": { + "type": "object", + "required": [ + "usedBy", + "usedAt" + ], + "properties": { + "usedAt": { + "type": "string", + "format": "datetime" + }, + "usedBy": { + "type": "string", + "format": "did" + } + } + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/server/deleteAccount.json b/lexicons/com/atproto/server/deleteAccount.json new file mode 100644 index 0000000000..6743a74431 --- /dev/null +++ b/lexicons/com/atproto/server/deleteAccount.json @@ -0,0 +1,42 @@ +{ + "id": "com.atproto.server.deleteAccount", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "did", + "password", + "token" + ], + "properties": { + "did": { + "type": "string", + "format": "did" + }, + "token": { + "type": "string" + }, + "password": { + "type": "string" + } + } + }, + "encoding": "application/json" + }, + "errors": [ + { + "name": "ExpiredToken" + }, + { + "name": "InvalidToken" + } + ], + "description": "Delete an actor's account with a token and password. Can only be called after requesting a deletion token. Requires auth." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/server/deleteSession.json b/lexicons/com/atproto/server/deleteSession.json new file mode 100644 index 0000000000..e2cc504c39 --- /dev/null +++ b/lexicons/com/atproto/server/deleteSession.json @@ -0,0 +1,19 @@ +{ + "id": "com.atproto.server.deleteSession", + "defs": { + "main": { + "type": "procedure", + "errors": [ + { + "name": "InvalidToken" + }, + { + "name": "ExpiredToken" + } + ], + "description": "Delete the current session. Requires auth using the 'refreshJwt' (not the 'accessJwt')." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/server/describeServer.json b/lexicons/com/atproto/server/describeServer.json new file mode 100644 index 0000000000..25dc2d2912 --- /dev/null +++ b/lexicons/com/atproto/server/describeServer.json @@ -0,0 +1,73 @@ +{ + "id": "com.atproto.server.describeServer", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "did", + "availableUserDomains" + ], + "properties": { + "did": { + "type": "string", + "format": "did" + }, + "links": { + "ref": "#links", + "type": "ref", + "description": "URLs of service policy documents." + }, + "contact": { + "ref": "#contact", + "type": "ref", + "description": "Contact information" + }, + "inviteCodeRequired": { + "type": "boolean", + "description": "If true, an invite code must be supplied to create an account on this instance." + }, + "availableUserDomains": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of domain suffixes that can be used in account handles." + }, + "phoneVerificationRequired": { + "type": "boolean", + "description": "If true, a phone verification token must be supplied to create an account on this instance." + } + } + }, + "encoding": "application/json" + }, + "description": "Describes the server's account creation requirements and capabilities. Implemented by PDS." + }, + "links": { + "type": "object", + "properties": { + "privacyPolicy": { + "type": "string", + "format": "uri" + }, + "termsOfService": { + "type": "string", + "format": "uri" + } + } + }, + "contact": { + "type": "object", + "properties": { + "email": { + "type": "string" + } + } + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/server/getAccountInviteCodes.json b/lexicons/com/atproto/server/getAccountInviteCodes.json new file mode 100644 index 0000000000..6b6bb1fa32 --- /dev/null +++ b/lexicons/com/atproto/server/getAccountInviteCodes.json @@ -0,0 +1,48 @@ +{ + "id": "com.atproto.server.getAccountInviteCodes", + "defs": { + "main": { + "type": "query", + "errors": [ + { + "name": "DuplicateCreate" + } + ], + "output": { + "schema": { + "type": "object", + "required": [ + "codes" + ], + "properties": { + "codes": { + "type": "array", + "items": { + "ref": "com.atproto.server.defs#inviteCode", + "type": "ref" + } + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "properties": { + "includeUsed": { + "type": "boolean", + "default": true + }, + "createAvailable": { + "type": "boolean", + "default": true, + "description": "Controls whether any new 'earned' but not 'created' invites should be created." + } + } + }, + "description": "Get all invite codes for the current account. Requires auth." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/server/getServiceAuth.json b/lexicons/com/atproto/server/getServiceAuth.json new file mode 100644 index 0000000000..80f5a1148c --- /dev/null +++ b/lexicons/com/atproto/server/getServiceAuth.json @@ -0,0 +1,53 @@ +{ + "id": "com.atproto.server.getServiceAuth", + "defs": { + "main": { + "type": "query", + "errors": [ + { + "name": "BadExpiration", + "description": "Indicates that the requested expiration date is not a valid. May be in the past or may be reliant on the requested scopes." + } + ], + "output": { + "schema": { + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "string" + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "aud" + ], + "properties": { + "aud": { + "type": "string", + "format": "did", + "description": "The DID of the service that the token will be used to authenticate with" + }, + "exp": { + "type": "integer", + "description": "The time in Unix Epoch seconds that the JWT expires. Defaults to 60 seconds in the future. The service may enforce certain time bounds on tokens depending on the requested scope." + }, + "lxm": { + "type": "string", + "format": "nsid", + "description": "Lexicon (XRPC) method to bind the requested token to" + } + } + }, + "description": "Get a signed token on behalf of the requesting DID for the requested service." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/server/getSession.json b/lexicons/com/atproto/server/getSession.json new file mode 100644 index 0000000000..8e9810d72e --- /dev/null +++ b/lexicons/com/atproto/server/getSession.json @@ -0,0 +1,55 @@ +{ + "id": "com.atproto.server.getSession", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "handle", + "did" + ], + "properties": { + "did": { + "type": "string", + "format": "did" + }, + "email": { + "type": "string" + }, + "active": { + "type": "boolean" + }, + "didDoc": { + "type": "unknown" + }, + "handle": { + "type": "string", + "format": "handle" + }, + "status": { + "type": "string", + "description": "If active=false, this optional field indicates a possible reason for why the account is not active. If active=false and no status is supplied, then the host makes no claim for why the repository is no longer being hosted.", + "knownValues": [ + "takendown", + "suspended", + "deactivated" + ] + }, + "emailConfirmed": { + "type": "boolean" + }, + "emailAuthFactor": { + "type": "boolean" + } + } + }, + "encoding": "application/json" + }, + "description": "Get information about the current auth session. Requires auth." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/server/listAppPasswords.json b/lexicons/com/atproto/server/listAppPasswords.json new file mode 100644 index 0000000000..8ff2678ea3 --- /dev/null +++ b/lexicons/com/atproto/server/listAppPasswords.json @@ -0,0 +1,53 @@ +{ + "id": "com.atproto.server.listAppPasswords", + "defs": { + "main": { + "type": "query", + "errors": [ + { + "name": "AccountTakedown" + } + ], + "output": { + "schema": { + "type": "object", + "required": [ + "passwords" + ], + "properties": { + "passwords": { + "type": "array", + "items": { + "ref": "#appPassword", + "type": "ref" + } + } + } + }, + "encoding": "application/json" + }, + "description": "List all App Passwords." + }, + "appPassword": { + "type": "object", + "required": [ + "name", + "createdAt" + ], + "properties": { + "name": { + "type": "string" + }, + "createdAt": { + "type": "string", + "format": "datetime" + }, + "privileged": { + "type": "boolean" + } + } + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/server/refreshSession.json b/lexicons/com/atproto/server/refreshSession.json new file mode 100644 index 0000000000..3924c2b665 --- /dev/null +++ b/lexicons/com/atproto/server/refreshSession.json @@ -0,0 +1,74 @@ +{ + "id": "com.atproto.server.refreshSession", + "defs": { + "main": { + "type": "procedure", + "errors": [ + { + "name": "AccountTakedown" + }, + { + "name": "InvalidToken" + }, + { + "name": "ExpiredToken" + } + ], + "output": { + "schema": { + "type": "object", + "required": [ + "accessJwt", + "refreshJwt", + "handle", + "did" + ], + "properties": { + "did": { + "type": "string", + "format": "did" + }, + "email": { + "type": "string" + }, + "active": { + "type": "boolean" + }, + "didDoc": { + "type": "unknown" + }, + "handle": { + "type": "string", + "format": "handle" + }, + "status": { + "type": "string", + "description": "Hosting status of the account. If not specified, then assume 'active'.", + "knownValues": [ + "takendown", + "suspended", + "deactivated" + ] + }, + "accessJwt": { + "type": "string" + }, + "refreshJwt": { + "type": "string" + }, + "emailConfirmed": { + "type": "boolean" + }, + "emailAuthFactor": { + "type": "boolean" + } + } + }, + "encoding": "application/json" + }, + "description": "Refresh an authentication session. Requires auth using the 'refreshJwt' (not the 'accessJwt')." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/server/requestAccountDelete.json b/lexicons/com/atproto/server/requestAccountDelete.json new file mode 100644 index 0000000000..94c21ae5a6 --- /dev/null +++ b/lexicons/com/atproto/server/requestAccountDelete.json @@ -0,0 +1,11 @@ +{ + "id": "com.atproto.server.requestAccountDelete", + "defs": { + "main": { + "type": "procedure", + "description": "Initiate a user account deletion via email." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/server/requestEmailConfirmation.json b/lexicons/com/atproto/server/requestEmailConfirmation.json new file mode 100644 index 0000000000..808c2aa4f7 --- /dev/null +++ b/lexicons/com/atproto/server/requestEmailConfirmation.json @@ -0,0 +1,11 @@ +{ + "id": "com.atproto.server.requestEmailConfirmation", + "defs": { + "main": { + "type": "procedure", + "description": "Request an email with a code to confirm ownership of email." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/server/requestEmailUpdate.json b/lexicons/com/atproto/server/requestEmailUpdate.json new file mode 100644 index 0000000000..158ba0f990 --- /dev/null +++ b/lexicons/com/atproto/server/requestEmailUpdate.json @@ -0,0 +1,25 @@ +{ + "id": "com.atproto.server.requestEmailUpdate", + "defs": { + "main": { + "type": "procedure", + "output": { + "schema": { + "type": "object", + "required": [ + "tokenRequired" + ], + "properties": { + "tokenRequired": { + "type": "boolean" + } + } + }, + "encoding": "application/json" + }, + "description": "Request a token in order to update email." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/server/requestPasswordReset.json b/lexicons/com/atproto/server/requestPasswordReset.json new file mode 100644 index 0000000000..fdb81f40e4 --- /dev/null +++ b/lexicons/com/atproto/server/requestPasswordReset.json @@ -0,0 +1,25 @@ +{ + "id": "com.atproto.server.requestPasswordReset", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "email" + ], + "properties": { + "email": { + "type": "string" + } + } + }, + "encoding": "application/json" + }, + "description": "Initiate a user account password reset via email." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/server/reserveSigningKey.json b/lexicons/com/atproto/server/reserveSigningKey.json new file mode 100644 index 0000000000..cd54f637fa --- /dev/null +++ b/lexicons/com/atproto/server/reserveSigningKey.json @@ -0,0 +1,39 @@ +{ + "id": "com.atproto.server.reserveSigningKey", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "properties": { + "did": { + "type": "string", + "format": "did", + "description": "The DID to reserve a key for." + } + } + }, + "encoding": "application/json" + }, + "output": { + "schema": { + "type": "object", + "required": [ + "signingKey" + ], + "properties": { + "signingKey": { + "type": "string", + "description": "The public key for the reserved signing key, in did:key serialization." + } + } + }, + "encoding": "application/json" + }, + "description": "Reserve a repo signing key, for use with account creation. Necessary so that a DID PLC update operation can be constructed during an account migraiton. Public and does not require auth; implemented by PDS. NOTE: this endpoint may change when full account migration is implemented." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/server/resetPassword.json b/lexicons/com/atproto/server/resetPassword.json new file mode 100644 index 0000000000..b8857cfca0 --- /dev/null +++ b/lexicons/com/atproto/server/resetPassword.json @@ -0,0 +1,37 @@ +{ + "id": "com.atproto.server.resetPassword", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "token", + "password" + ], + "properties": { + "token": { + "type": "string" + }, + "password": { + "type": "string" + } + } + }, + "encoding": "application/json" + }, + "errors": [ + { + "name": "ExpiredToken" + }, + { + "name": "InvalidToken" + } + ], + "description": "Reset a user account password using a token." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/server/revokeAppPassword.json b/lexicons/com/atproto/server/revokeAppPassword.json new file mode 100644 index 0000000000..362306cc96 --- /dev/null +++ b/lexicons/com/atproto/server/revokeAppPassword.json @@ -0,0 +1,25 @@ +{ + "id": "com.atproto.server.revokeAppPassword", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string" + } + } + }, + "encoding": "application/json" + }, + "description": "Revoke an App Password by name." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/server/updateEmail.json b/lexicons/com/atproto/server/updateEmail.json new file mode 100644 index 0000000000..a5390b4de3 --- /dev/null +++ b/lexicons/com/atproto/server/updateEmail.json @@ -0,0 +1,43 @@ +{ + "id": "com.atproto.server.updateEmail", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "email" + ], + "properties": { + "email": { + "type": "string" + }, + "token": { + "type": "string", + "description": "Requires a token from com.atproto.sever.requestEmailUpdate if the account's email has been confirmed." + }, + "emailAuthFactor": { + "type": "boolean" + } + } + }, + "encoding": "application/json" + }, + "errors": [ + { + "name": "ExpiredToken" + }, + { + "name": "InvalidToken" + }, + { + "name": "TokenRequired" + } + ], + "description": "Update an account's email." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/sync/defs.json b/lexicons/com/atproto/sync/defs.json new file mode 100644 index 0000000000..3f2f42fe8c --- /dev/null +++ b/lexicons/com/atproto/sync/defs.json @@ -0,0 +1,17 @@ +{ + "id": "com.atproto.sync.defs", + "defs": { + "hostStatus": { + "type": "string", + "knownValues": [ + "active", + "idle", + "offline", + "throttled", + "banned" + ] + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/sync/getBlob.json b/lexicons/com/atproto/sync/getBlob.json new file mode 100644 index 0000000000..b571d31736 --- /dev/null +++ b/lexicons/com/atproto/sync/getBlob.json @@ -0,0 +1,50 @@ +{ + "id": "com.atproto.sync.getBlob", + "defs": { + "main": { + "type": "query", + "errors": [ + { + "name": "BlobNotFound" + }, + { + "name": "RepoNotFound" + }, + { + "name": "RepoTakendown" + }, + { + "name": "RepoSuspended" + }, + { + "name": "RepoDeactivated" + } + ], + "output": { + "encoding": "*/*" + }, + "parameters": { + "type": "params", + "required": [ + "did", + "cid" + ], + "properties": { + "cid": { + "type": "string", + "format": "cid", + "description": "The CID of the blob to fetch" + }, + "did": { + "type": "string", + "format": "did", + "description": "The DID of the account." + } + } + }, + "description": "Get a blob associated with a given account. Returns the full blob as originally uploaded. Does not require auth; implemented by PDS." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/sync/getBlocks.json b/lexicons/com/atproto/sync/getBlocks.json new file mode 100644 index 0000000000..8cd269ecea --- /dev/null +++ b/lexicons/com/atproto/sync/getBlocks.json @@ -0,0 +1,52 @@ +{ + "id": "com.atproto.sync.getBlocks", + "defs": { + "main": { + "type": "query", + "errors": [ + { + "name": "BlockNotFound" + }, + { + "name": "RepoNotFound" + }, + { + "name": "RepoTakendown" + }, + { + "name": "RepoSuspended" + }, + { + "name": "RepoDeactivated" + } + ], + "output": { + "encoding": "application/vnd.ipld.car" + }, + "parameters": { + "type": "params", + "required": [ + "did", + "cids" + ], + "properties": { + "did": { + "type": "string", + "format": "did", + "description": "The DID of the repo." + }, + "cids": { + "type": "array", + "items": { + "type": "string", + "format": "cid" + } + } + } + }, + "description": "Get data blocks from a given repo, by CID. For example, intermediate MST nodes, or records. Does not require auth; implemented by PDS." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/sync/getCheckout.json b/lexicons/com/atproto/sync/getCheckout.json new file mode 100644 index 0000000000..9e5b0b3066 --- /dev/null +++ b/lexicons/com/atproto/sync/getCheckout.json @@ -0,0 +1,27 @@ +{ + "id": "com.atproto.sync.getCheckout", + "defs": { + "main": { + "type": "query", + "output": { + "encoding": "application/vnd.ipld.car" + }, + "parameters": { + "type": "params", + "required": [ + "did" + ], + "properties": { + "did": { + "type": "string", + "format": "did", + "description": "The DID of the repo." + } + } + }, + "description": "DEPRECATED - please use com.atproto.sync.getRepo instead" + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/sync/getHead.json b/lexicons/com/atproto/sync/getHead.json new file mode 100644 index 0000000000..fc5af39d45 --- /dev/null +++ b/lexicons/com/atproto/sync/getHead.json @@ -0,0 +1,44 @@ +{ + "id": "com.atproto.sync.getHead", + "defs": { + "main": { + "type": "query", + "errors": [ + { + "name": "HeadNotFound" + } + ], + "output": { + "schema": { + "type": "object", + "required": [ + "root" + ], + "properties": { + "root": { + "type": "string", + "format": "cid" + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "did" + ], + "properties": { + "did": { + "type": "string", + "format": "did", + "description": "The DID of the repo." + } + } + }, + "description": "DEPRECATED - please use com.atproto.sync.getLatestCommit instead" + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/sync/getHostStatus.json b/lexicons/com/atproto/sync/getHostStatus.json new file mode 100644 index 0000000000..86d563fa24 --- /dev/null +++ b/lexicons/com/atproto/sync/getHostStatus.json @@ -0,0 +1,54 @@ +{ + "id": "com.atproto.sync.getHostStatus", + "defs": { + "main": { + "type": "query", + "errors": [ + { + "name": "HostNotFound" + } + ], + "output": { + "schema": { + "type": "object", + "required": [ + "hostname" + ], + "properties": { + "seq": { + "type": "integer", + "description": "Recent repo stream event sequence number. May be delayed from actual stream processing (eg, persisted cursor not in-memory cursor)." + }, + "status": { + "ref": "com.atproto.sync.defs#hostStatus", + "type": "ref" + }, + "hostname": { + "type": "string" + }, + "accountCount": { + "type": "integer", + "description": "Number of accounts on the server which are associated with the upstream host. Note that the upstream may actually have more accounts." + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "hostname" + ], + "properties": { + "hostname": { + "type": "string", + "description": "Hostname of the host (eg, PDS or relay) being queried." + } + } + }, + "description": "Returns information about a specified upstream host, as consumed by the server. Implemented by relays." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/sync/getLatestCommit.json b/lexicons/com/atproto/sync/getLatestCommit.json new file mode 100644 index 0000000000..e8ce2cd800 --- /dev/null +++ b/lexicons/com/atproto/sync/getLatestCommit.json @@ -0,0 +1,58 @@ +{ + "id": "com.atproto.sync.getLatestCommit", + "defs": { + "main": { + "type": "query", + "errors": [ + { + "name": "RepoNotFound" + }, + { + "name": "RepoTakendown" + }, + { + "name": "RepoSuspended" + }, + { + "name": "RepoDeactivated" + } + ], + "output": { + "schema": { + "type": "object", + "required": [ + "cid", + "rev" + ], + "properties": { + "cid": { + "type": "string", + "format": "cid" + }, + "rev": { + "type": "string", + "format": "tid" + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "did" + ], + "properties": { + "did": { + "type": "string", + "format": "did", + "description": "The DID of the repo." + } + } + }, + "description": "Get the current commit CID & revision of the specified repo. Does not require auth." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/sync/getRecord.json b/lexicons/com/atproto/sync/getRecord.json new file mode 100644 index 0000000000..330826046d --- /dev/null +++ b/lexicons/com/atproto/sync/getRecord.json @@ -0,0 +1,55 @@ +{ + "id": "com.atproto.sync.getRecord", + "defs": { + "main": { + "type": "query", + "errors": [ + { + "name": "RecordNotFound" + }, + { + "name": "RepoNotFound" + }, + { + "name": "RepoTakendown" + }, + { + "name": "RepoSuspended" + }, + { + "name": "RepoDeactivated" + } + ], + "output": { + "encoding": "application/vnd.ipld.car" + }, + "parameters": { + "type": "params", + "required": [ + "did", + "collection", + "rkey" + ], + "properties": { + "did": { + "type": "string", + "format": "did", + "description": "The DID of the repo." + }, + "rkey": { + "type": "string", + "format": "record-key", + "description": "Record Key" + }, + "collection": { + "type": "string", + "format": "nsid" + } + } + }, + "description": "Get data blocks needed to prove the existence or non-existence of record in the current version of repo. Does not require auth." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/sync/getRepo.json b/lexicons/com/atproto/sync/getRepo.json new file mode 100644 index 0000000000..3ec4c0489b --- /dev/null +++ b/lexicons/com/atproto/sync/getRepo.json @@ -0,0 +1,46 @@ +{ + "id": "com.atproto.sync.getRepo", + "defs": { + "main": { + "type": "query", + "errors": [ + { + "name": "RepoNotFound" + }, + { + "name": "RepoTakendown" + }, + { + "name": "RepoSuspended" + }, + { + "name": "RepoDeactivated" + } + ], + "output": { + "encoding": "application/vnd.ipld.car" + }, + "parameters": { + "type": "params", + "required": [ + "did" + ], + "properties": { + "did": { + "type": "string", + "format": "did", + "description": "The DID of the repo." + }, + "since": { + "type": "string", + "format": "tid", + "description": "The revision ('rev') of the repo to create a diff from." + } + } + }, + "description": "Download a repository export as CAR file. Optionally only a 'diff' since a previous revision. Does not require auth; implemented by PDS." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/sync/getRepoStatus.json b/lexicons/com/atproto/sync/getRepoStatus.json new file mode 100644 index 0000000000..698b80d509 --- /dev/null +++ b/lexicons/com/atproto/sync/getRepoStatus.json @@ -0,0 +1,65 @@ +{ + "id": "com.atproto.sync.getRepoStatus", + "defs": { + "main": { + "type": "query", + "errors": [ + { + "name": "RepoNotFound" + } + ], + "output": { + "schema": { + "type": "object", + "required": [ + "did", + "active" + ], + "properties": { + "did": { + "type": "string", + "format": "did" + }, + "rev": { + "type": "string", + "format": "tid", + "description": "Optional field, the current rev of the repo, if active=true" + }, + "active": { + "type": "boolean" + }, + "status": { + "type": "string", + "description": "If active=false, this optional field indicates a possible reason for why the account is not active. If active=false and no status is supplied, then the host makes no claim for why the repository is no longer being hosted.", + "knownValues": [ + "takendown", + "suspended", + "deleted", + "deactivated", + "desynchronized", + "throttled" + ] + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "did" + ], + "properties": { + "did": { + "type": "string", + "format": "did", + "description": "The DID of the repo." + } + } + }, + "description": "Get the hosting status for a repository, on this server. Expected to be implemented by PDS and Relay." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/sync/listBlobs.json b/lexicons/com/atproto/sync/listBlobs.json new file mode 100644 index 0000000000..ee98c6ecb6 --- /dev/null +++ b/lexicons/com/atproto/sync/listBlobs.json @@ -0,0 +1,73 @@ +{ + "id": "com.atproto.sync.listBlobs", + "defs": { + "main": { + "type": "query", + "errors": [ + { + "name": "RepoNotFound" + }, + { + "name": "RepoTakendown" + }, + { + "name": "RepoSuspended" + }, + { + "name": "RepoDeactivated" + } + ], + "output": { + "schema": { + "type": "object", + "required": [ + "cids" + ], + "properties": { + "cids": { + "type": "array", + "items": { + "type": "string", + "format": "cid" + } + }, + "cursor": { + "type": "string" + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "did" + ], + "properties": { + "did": { + "type": "string", + "format": "did", + "description": "The DID of the repo." + }, + "limit": { + "type": "integer", + "default": 500, + "maximum": 1000, + "minimum": 1 + }, + "since": { + "type": "string", + "format": "tid", + "description": "Optional revision of the repo to list blobs since." + }, + "cursor": { + "type": "string" + } + } + }, + "description": "List blob CIDs for an account, since some repo revision. Does not require auth; implemented by PDS." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/sync/listHosts.json b/lexicons/com/atproto/sync/listHosts.json new file mode 100644 index 0000000000..66f78df6e1 --- /dev/null +++ b/lexicons/com/atproto/sync/listHosts.json @@ -0,0 +1,70 @@ +{ + "id": "com.atproto.sync.listHosts", + "defs": { + "host": { + "type": "object", + "required": [ + "hostname" + ], + "properties": { + "seq": { + "type": "integer", + "description": "Recent repo stream event sequence number. May be delayed from actual stream processing (eg, persisted cursor not in-memory cursor)." + }, + "status": { + "ref": "com.atproto.sync.defs#hostStatus", + "type": "ref" + }, + "hostname": { + "type": "string", + "description": "hostname of server; not a URL (no scheme)" + }, + "accountCount": { + "type": "integer" + } + } + }, + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "hosts" + ], + "properties": { + "hosts": { + "type": "array", + "items": { + "ref": "#host", + "type": "ref" + }, + "description": "Sort order is not formally specified. Recommended order is by time host was first seen by the server, with oldest first." + }, + "cursor": { + "type": "string" + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "properties": { + "limit": { + "type": "integer", + "default": 200, + "maximum": 1000, + "minimum": 1 + }, + "cursor": { + "type": "string" + } + } + }, + "description": "Enumerates upstream hosts (eg, PDS or relay instances) that this service consumes from. Implemented by relays." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/sync/listRepos.json b/lexicons/com/atproto/sync/listRepos.json new file mode 100644 index 0000000000..0341bccc9d --- /dev/null +++ b/lexicons/com/atproto/sync/listRepos.json @@ -0,0 +1,84 @@ +{ + "id": "com.atproto.sync.listRepos", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "repos" + ], + "properties": { + "repos": { + "type": "array", + "items": { + "ref": "#repo", + "type": "ref" + } + }, + "cursor": { + "type": "string" + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "properties": { + "limit": { + "type": "integer", + "default": 500, + "maximum": 1000, + "minimum": 1 + }, + "cursor": { + "type": "string" + } + } + }, + "description": "Enumerates all the DID, rev, and commit CID for all repos hosted by this service. Does not require auth; implemented by PDS and Relay." + }, + "repo": { + "type": "object", + "required": [ + "did", + "head", + "rev" + ], + "properties": { + "did": { + "type": "string", + "format": "did" + }, + "rev": { + "type": "string", + "format": "tid" + }, + "head": { + "type": "string", + "format": "cid", + "description": "Current repo commit CID" + }, + "active": { + "type": "boolean" + }, + "status": { + "type": "string", + "description": "If active=false, this optional field indicates a possible reason for why the account is not active. If active=false and no status is supplied, then the host makes no claim for why the repository is no longer being hosted.", + "knownValues": [ + "takendown", + "suspended", + "deleted", + "deactivated", + "desynchronized", + "throttled" + ] + } + } + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/sync/listReposByCollection.json b/lexicons/com/atproto/sync/listReposByCollection.json new file mode 100644 index 0000000000..c472ae9102 --- /dev/null +++ b/lexicons/com/atproto/sync/listReposByCollection.json @@ -0,0 +1,66 @@ +{ + "id": "com.atproto.sync.listReposByCollection", + "defs": { + "main": { + "type": "query", + "output": { + "schema": { + "type": "object", + "required": [ + "repos" + ], + "properties": { + "repos": { + "type": "array", + "items": { + "ref": "#repo", + "type": "ref" + } + }, + "cursor": { + "type": "string" + } + } + }, + "encoding": "application/json" + }, + "parameters": { + "type": "params", + "required": [ + "collection" + ], + "properties": { + "limit": { + "type": "integer", + "default": 500, + "maximum": 2000, + "minimum": 1, + "description": "Maximum size of response set. Recommend setting a large maximum (1000+) when enumerating large DID lists." + }, + "cursor": { + "type": "string" + }, + "collection": { + "type": "string", + "format": "nsid" + } + } + }, + "description": "Enumerates all the DIDs which have records with the given collection NSID." + }, + "repo": { + "type": "object", + "required": [ + "did" + ], + "properties": { + "did": { + "type": "string", + "format": "did" + } + } + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/sync/notifyOfUpdate.json b/lexicons/com/atproto/sync/notifyOfUpdate.json new file mode 100644 index 0000000000..aa674c7380 --- /dev/null +++ b/lexicons/com/atproto/sync/notifyOfUpdate.json @@ -0,0 +1,26 @@ +{ + "id": "com.atproto.sync.notifyOfUpdate", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "hostname" + ], + "properties": { + "hostname": { + "type": "string", + "description": "Hostname of the current service (usually a PDS) that is notifying of update." + } + } + }, + "encoding": "application/json" + }, + "description": "Notify a crawling service of a recent update, and that crawling should resume. Intended use is after a gap between repo stream events caused the crawling service to disconnect. Does not require auth; implemented by Relay. DEPRECATED: just use com.atproto.sync.requestCrawl" + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/sync/requestCrawl.json b/lexicons/com/atproto/sync/requestCrawl.json new file mode 100644 index 0000000000..6245282cc8 --- /dev/null +++ b/lexicons/com/atproto/sync/requestCrawl.json @@ -0,0 +1,31 @@ +{ + "id": "com.atproto.sync.requestCrawl", + "defs": { + "main": { + "type": "procedure", + "input": { + "schema": { + "type": "object", + "required": [ + "hostname" + ], + "properties": { + "hostname": { + "type": "string", + "description": "Hostname of the current service (eg, PDS) that is requesting to be crawled." + } + } + }, + "encoding": "application/json" + }, + "errors": [ + { + "name": "HostBanned" + } + ], + "description": "Request a service to persistently crawl hosted repos. Expected use is new PDS instances declaring their existence to Relays. Does not require auth." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/sync/subscribeRepos.json b/lexicons/com/atproto/sync/subscribeRepos.json new file mode 100644 index 0000000000..4408da07e6 --- /dev/null +++ b/lexicons/com/atproto/sync/subscribeRepos.json @@ -0,0 +1,276 @@ +{ + "id": "com.atproto.sync.subscribeRepos", + "defs": { + "info": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "knownValues": [ + "OutdatedCursor" + ] + }, + "message": { + "type": "string" + } + } + }, + "main": { + "type": "subscription", + "errors": [ + { + "name": "FutureCursor" + }, + { + "name": "ConsumerTooSlow", + "description": "If the consumer of the stream can not keep up with events, and a backlog gets too large, the server will drop the connection." + } + ], + "message": { + "schema": { + "refs": [ + "#commit", + "#sync", + "#identity", + "#account", + "#info" + ], + "type": "union" + } + }, + "parameters": { + "type": "params", + "properties": { + "cursor": { + "type": "integer", + "description": "The last known event seq number to backfill from." + } + } + }, + "description": "Repository event stream, aka Firehose endpoint. Outputs repo commits with diff data, and identity update events, for all repositories on the current server. See the atproto specifications for details around stream sequencing, repo versioning, CAR diff format, and more. Public and does not require auth; implemented by PDS and Relay." + }, + "sync": { + "type": "object", + "required": [ + "seq", + "did", + "blocks", + "rev", + "time" + ], + "properties": { + "did": { + "type": "string", + "format": "did", + "description": "The account this repo event corresponds to. Must match that in the commit object." + }, + "rev": { + "type": "string", + "description": "The rev of the commit. This value must match that in the commit object." + }, + "seq": { + "type": "integer", + "description": "The stream sequence number of this message." + }, + "time": { + "type": "string", + "format": "datetime", + "description": "Timestamp of when this message was originally broadcast." + }, + "blocks": { + "type": "bytes", + "maxLength": 10000, + "description": "CAR file containing the commit, as a block. The CAR header must include the commit block CID as the first 'root'." + } + }, + "description": "Updates the repo to a new state, without necessarily including that state on the firehose. Used to recover from broken commit streams, data loss incidents, or in situations where upstream host does not know recent state of the repository." + }, + "commit": { + "type": "object", + "nullable": [ + "since" + ], + "required": [ + "seq", + "rebase", + "tooBig", + "repo", + "commit", + "rev", + "since", + "blocks", + "ops", + "blobs", + "time" + ], + "properties": { + "ops": { + "type": "array", + "items": { + "ref": "#repoOp", + "type": "ref", + "description": "List of repo mutation operations in this commit (eg, records created, updated, or deleted)." + }, + "maxLength": 200 + }, + "rev": { + "type": "string", + "format": "tid", + "description": "The rev of the emitted commit. Note that this information is also in the commit object included in blocks, unless this is a tooBig event." + }, + "seq": { + "type": "integer", + "description": "The stream sequence number of this message." + }, + "repo": { + "type": "string", + "format": "did", + "description": "The repo this event comes from. Note that all other message types name this field 'did'." + }, + "time": { + "type": "string", + "format": "datetime", + "description": "Timestamp of when this message was originally broadcast." + }, + "blobs": { + "type": "array", + "items": { + "type": "cid-link", + "description": "DEPRECATED -- will soon always be empty. List of new blobs (by CID) referenced by records in this commit." + } + }, + "since": { + "type": "string", + "format": "tid", + "description": "The rev of the last emitted commit from this repo (if any)." + }, + "blocks": { + "type": "bytes", + "maxLength": 2000000, + "description": "CAR file containing relevant blocks, as a diff since the previous repo state. The commit must be included as a block, and the commit block CID must be the first entry in the CAR header 'roots' list." + }, + "commit": { + "type": "cid-link", + "description": "Repo commit object CID." + }, + "rebase": { + "type": "boolean", + "description": "DEPRECATED -- unused" + }, + "tooBig": { + "type": "boolean", + "description": "DEPRECATED -- replaced by #sync event and data limits. Indicates that this commit contained too many ops, or data size was too large. Consumers will need to make a separate request to get missing data." + }, + "prevData": { + "type": "cid-link", + "description": "The root CID of the MST tree for the previous commit from this repo (indicated by the 'since' revision field in this message). Corresponds to the 'data' field in the repo commit object. NOTE: this field is effectively required for the 'inductive' version of firehose." + } + }, + "description": "Represents an update of repository state. Note that empty commits are allowed, which include no repo data changes, but an update to rev and signature." + }, + "repoOp": { + "type": "object", + "nullable": [ + "cid" + ], + "required": [ + "action", + "path", + "cid" + ], + "properties": { + "cid": { + "type": "cid-link", + "description": "For creates and updates, the new record CID. For deletions, null." + }, + "path": { + "type": "string" + }, + "prev": { + "type": "cid-link", + "description": "For updates and deletes, the previous record CID (required for inductive firehose). For creations, field should not be defined." + }, + "action": { + "type": "string", + "knownValues": [ + "create", + "update", + "delete" + ] + } + }, + "description": "A repo operation, ie a mutation of a single record." + }, + "account": { + "type": "object", + "required": [ + "seq", + "did", + "time", + "active" + ], + "properties": { + "did": { + "type": "string", + "format": "did" + }, + "seq": { + "type": "integer" + }, + "time": { + "type": "string", + "format": "datetime" + }, + "active": { + "type": "boolean", + "description": "Indicates that the account has a repository which can be fetched from the host that emitted this event." + }, + "status": { + "type": "string", + "description": "If active=false, this optional field indicates a reason for why the account is not active.", + "knownValues": [ + "takendown", + "suspended", + "deleted", + "deactivated", + "desynchronized", + "throttled" + ] + } + }, + "description": "Represents a change to an account's status on a host (eg, PDS or Relay). The semantics of this event are that the status is at the host which emitted the event, not necessarily that at the currently active PDS. Eg, a Relay takedown would emit a takedown with active=false, even if the PDS is still active." + }, + "identity": { + "type": "object", + "required": [ + "seq", + "did", + "time" + ], + "properties": { + "did": { + "type": "string", + "format": "did" + }, + "seq": { + "type": "integer" + }, + "time": { + "type": "string", + "format": "datetime" + }, + "handle": { + "type": "string", + "format": "handle", + "description": "The current handle for the account, or 'handle.invalid' if validation fails. This field is optional, might have been validated or passed-through from an upstream source. Semantics and behaviors for PDS vs Relay may evolve in the future; see atproto specs for more details." + } + }, + "description": "Represents a change to an account's identity. Could be an updated handle, signing key, or pds hosting endpoint. Serves as a prod to all downstream services to refresh their identity cache." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/com/atproto/temp/checkHandleAvailability.json b/lexicons/com/atproto/temp/checkHandleAvailability.json new file mode 100644 index 0000000000..ffbdd6f732 --- /dev/null +++ b/lexicons/com/atproto/temp/checkHandleAvailability.json @@ -0,0 +1,88 @@ +{ + "lexicon": 1, + "id": "com.atproto.temp.checkHandleAvailability", + "defs": { + "main": { + "type": "query", + "description": "Checks whether the provided handle is available. If the handle is not available, available suggestions will be returned. Optional inputs will be used to generate suggestions.", + "parameters": { + "type": "params", + "required": ["handle"], + "properties": { + "handle": { + "type": "string", + "format": "handle", + "description": "Tentative handle. Will be checked for availability or used to build handle suggestions." + }, + "email": { + "type": "string", + "description": "User-provided email. Might be used to build handle suggestions." + }, + "birthDate": { + "type": "string", + "format": "datetime", + "description": "User-provided birth date. Might be used to build handle suggestions." + } + } + }, + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["handle", "result"], + "properties": { + "handle": { + "type": "string", + "format": "handle", + "description": "Echo of the input handle." + }, + "result": { + "type": "union", + "refs": ["#resultAvailable", "#resultUnavailable"] + } + } + } + }, + "errors": [ + { + "name": "InvalidEmail", + "description": "An invalid email was provided." + } + ] + }, + "resultAvailable": { + "type": "object", + "description": "Indicates the provided handle is available.", + "properties": {} + }, + "resultUnavailable": { + "type": "object", + "description": "Indicates the provided handle is unavailable and gives suggestions of available handles.", + "required": ["suggestions"], + "properties": { + "suggestions": { + "type": "array", + "description": "List of suggested handles based on the provided inputs.", + "items": { + "type": "ref", + "ref": "#suggestion" + } + } + } + }, + "suggestion": { + "type": "object", + "required": ["handle", "method"], + "properties": { + "handle": { + "type": "string", + "format": "handle" + }, + "method": { + "type": "string", + "description": "Method used to build this suggestion. Should be considered opaque to clients. Can be used for metrics." + } + } + } + } +} diff --git a/lexicons/com/atproto/temp/checkSignupQueue.json b/lexicons/com/atproto/temp/checkSignupQueue.json new file mode 100644 index 0000000000..b7d9e65d7c --- /dev/null +++ b/lexicons/com/atproto/temp/checkSignupQueue.json @@ -0,0 +1,22 @@ +{ + "lexicon": 1, + "id": "com.atproto.temp.checkSignupQueue", + "defs": { + "main": { + "type": "query", + "description": "Check accounts location in signup queue.", + "output": { + "encoding": "application/json", + "schema": { + "type": "object", + "required": ["activated"], + "properties": { + "activated": { "type": "boolean" }, + "placeInQueue": { "type": "integer" }, + "estimatedTimeMs": { "type": "integer" } + } + } + } + } + } +} diff --git a/lexicons/com/germnetwork/declaration.json b/lexicons/com/germnetwork/declaration.json new file mode 100644 index 0000000000..2c9e42486d --- /dev/null +++ b/lexicons/com/germnetwork/declaration.json @@ -0,0 +1,75 @@ +{ + "id": "com.germnetwork.declaration", + "defs": { + "main": { + "key": "literal:self", + "type": "record", + "record": { + "type": "object", + "required": [ + "version", + "currentKey" + ], + "properties": { + "version": { + "type": "string", + "maxLength": 14, + "minLength": 5, + "description": "Semver version number, without pre-release or build information, for the format of opaque content" + }, + "messageMe": { + "ref": "#messageMe", + "type": "ref", + "description": "Controls who can message this account" + }, + "currentKey": { + "type": "bytes", + "description": "Opaque value, an ed25519 public key prefixed with a byte enum" + }, + "keyPackage": { + "type": "bytes", + "description": "Opaque value, contains MLS KeyPackage(s), and other signature data, and is signed by the currentKey" + }, + "continuityProofs": { + "type": "array", + "items": { + "type": "bytes" + }, + "maxLength": 1000, + "description": "Array of opaque values to allow for key rolling" + } + } + }, + "description": "A declaration of a Germ Network account" + }, + "messageMe": { + "type": "object", + "required": [ + "showButtonTo", + "messageMeUrl" + ], + "properties": { + "messageMeUrl": { + "type": "string", + "format": "uri", + "maxLength": 2047, + "minLength": 1, + "description": "A URL to present to an account that does not have its own com.germnetwork.declaration record, must have an empty fragment component, where the app should fill in the fragment component with the DIDs of the two accounts who wish to message each other" + }, + "showButtonTo": { + "type": "string", + "maxLength": 100, + "minLength": 1, + "description": "The policy of who can message the account, this value is included in the keyPackage, but is duplicated here to allow applications to decide if they should show a 'Message on Germ' button to the viewer.", + "knownValues": [ + "none", + "usersIFollow", + "everyone" + ] + } + } + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/lexicons/tools/ozone/report/defs.json b/lexicons/tools/ozone/report/defs.json new file mode 100644 index 0000000000..2742fbd1ff --- /dev/null +++ b/lexicons/tools/ozone/report/defs.json @@ -0,0 +1,212 @@ +{ + "id": "tools.ozone.report.defs", + "defs": { + "reasonType": { + "type": "string", + "knownValues": [ + "tools.ozone.report.defs#reasonAppeal", + "tools.ozone.report.defs#reasonOther", + "tools.ozone.report.defs#reasonViolenceAnimal", + "tools.ozone.report.defs#reasonViolenceThreats", + "tools.ozone.report.defs#reasonViolenceGraphicContent", + "tools.ozone.report.defs#reasonViolenceGlorification", + "tools.ozone.report.defs#reasonViolenceExtremistContent", + "tools.ozone.report.defs#reasonViolenceTrafficking", + "tools.ozone.report.defs#reasonViolenceOther", + "tools.ozone.report.defs#reasonSexualAbuseContent", + "tools.ozone.report.defs#reasonSexualNCII", + "tools.ozone.report.defs#reasonSexualDeepfake", + "tools.ozone.report.defs#reasonSexualAnimal", + "tools.ozone.report.defs#reasonSexualUnlabeled", + "tools.ozone.report.defs#reasonSexualOther", + "tools.ozone.report.defs#reasonChildSafetyCSAM", + "tools.ozone.report.defs#reasonChildSafetyGroom", + "tools.ozone.report.defs#reasonChildSafetyPrivacy", + "tools.ozone.report.defs#reasonChildSafetyHarassment", + "tools.ozone.report.defs#reasonChildSafetyOther", + "tools.ozone.report.defs#reasonHarassmentTroll", + "tools.ozone.report.defs#reasonHarassmentTargeted", + "tools.ozone.report.defs#reasonHarassmentHateSpeech", + "tools.ozone.report.defs#reasonHarassmentDoxxing", + "tools.ozone.report.defs#reasonHarassmentOther", + "tools.ozone.report.defs#reasonMisleadingBot", + "tools.ozone.report.defs#reasonMisleadingImpersonation", + "tools.ozone.report.defs#reasonMisleadingSpam", + "tools.ozone.report.defs#reasonMisleadingScam", + "tools.ozone.report.defs#reasonMisleadingElections", + "tools.ozone.report.defs#reasonMisleadingOther", + "tools.ozone.report.defs#reasonRuleSiteSecurity", + "tools.ozone.report.defs#reasonRuleProhibitedSales", + "tools.ozone.report.defs#reasonRuleBanEvasion", + "tools.ozone.report.defs#reasonRuleOther", + "tools.ozone.report.defs#reasonSelfHarmContent", + "tools.ozone.report.defs#reasonSelfHarmED", + "tools.ozone.report.defs#reasonSelfHarmStunts", + "tools.ozone.report.defs#reasonSelfHarmSubstances", + "tools.ozone.report.defs#reasonSelfHarmOther" + ] + }, + "reasonOther": { + "type": "token", + "description": "An issue not included in these options" + }, + "reasonAppeal": { + "type": "token", + "description": "Appeal a previously taken moderation action" + }, + "reasonRuleOther": { + "type": "token", + "description": "Other" + }, + "reasonSelfHarmED": { + "type": "token", + "description": "Eating disorders" + }, + "reasonSexualNCII": { + "type": "token", + "description": "Non-consensual intimate imagery" + }, + "reasonSexualOther": { + "type": "token", + "description": "Other sexual violence content" + }, + "reasonSexualAnimal": { + "type": "token", + "description": "Animal sexual abuse" + }, + "reasonMisleadingBot": { + "type": "token", + "description": "Fake account or bot" + }, + "reasonSelfHarmOther": { + "type": "token", + "description": "Other dangerous content" + }, + "reasonViolenceOther": { + "type": "token", + "description": "Other violent content" + }, + "reasonMisleadingScam": { + "type": "token", + "description": "Scam" + }, + "reasonMisleadingSpam": { + "type": "token", + "description": "Spam" + }, + "reasonRuleBanEvasion": { + "type": "token", + "description": "Banned user returning" + }, + "reasonSelfHarmStunts": { + "type": "token", + "description": "Dangerous challenges or activities" + }, + "reasonSexualDeepfake": { + "type": "token", + "description": "Deepfake adult content" + }, + "reasonViolenceAnimal": { + "type": "token", + "description": "Animal welfare violations" + }, + "reasonChildSafetyCSAM": { + "type": "token", + "description": "Child sexual abuse material (CSAM). These reports will be sent only be sent to the application's Moderation Authority." + }, + "reasonHarassmentOther": { + "type": "token", + "description": "Other harassing or hateful content" + }, + "reasonHarassmentTroll": { + "type": "token", + "description": "Trolling" + }, + "reasonMisleadingOther": { + "type": "token", + "description": "Other misleading content" + }, + "reasonSelfHarmContent": { + "type": "token", + "description": "Content promoting or depicting self-harm" + }, + "reasonSexualUnlabeled": { + "type": "token", + "description": "Unlabelled adult content" + }, + "reasonViolenceThreats": { + "type": "token", + "description": "Threats or incitement" + }, + "reasonChildSafetyGroom": { + "type": "token", + "description": "Grooming or predatory behavior. These reports will be sent only be sent to the application's Moderation Authority." + }, + "reasonChildSafetyOther": { + "type": "token", + "description": "Other child safety. These reports will be sent only be sent to the application's Moderation Authority." + }, + "reasonRuleSiteSecurity": { + "type": "token", + "description": "Hacking or system attacks" + }, + "reasonHarassmentDoxxing": { + "type": "token", + "description": "Doxxing" + }, + "reasonChildSafetyPrivacy": { + "type": "token", + "description": "Privacy violation involving a minor" + }, + "reasonHarassmentTargeted": { + "type": "token", + "description": "Targeted harassment" + }, + "reasonSelfHarmSubstances": { + "type": "token", + "description": "Dangerous substances or drug abuse" + }, + "reasonSexualAbuseContent": { + "type": "token", + "description": "Adult sexual abuse content" + }, + "reasonMisleadingElections": { + "type": "token", + "description": "False information about elections" + }, + "reasonRuleProhibitedSales": { + "type": "token", + "description": "Promoting or selling prohibited items or services" + }, + "reasonViolenceTrafficking": { + "type": "token", + "description": "Human trafficking" + }, + "reasonHarassmentHateSpeech": { + "type": "token", + "description": "Hate speech" + }, + "reasonChildSafetyHarassment": { + "type": "token", + "description": "Harassment or bullying of minors" + }, + "reasonViolenceGlorification": { + "type": "token", + "description": "Glorification of violence" + }, + "reasonViolenceGraphicContent": { + "type": "token", + "description": "Graphic violent content" + }, + "reasonMisleadingImpersonation": { + "type": "token", + "description": "Impersonation" + }, + "reasonViolenceExtremistContent": { + "type": "token", + "description": "Extremist content. These reports will be sent only be sent to the application's Moderation Authority." + } + }, + "$type": "com.atproto.lexicon.schema", + "lexicon": 1 +} diff --git a/package.json b/package.json index 93d671ff31..e084d19641 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ }, "scripts": { "prepare": "is-ci || husky", - "postinstall": "pnpm intl:compile-if-needed", + "postinstall": "pnpm lexicons:generate && pnpm intl:compile-if-needed", "prebuild": "EXPO_NO_GIT_STATUS=1 expo prebuild --clean", "android": "expo run:android --variant debugOptimized", "android:prod": "expo run:android --variant release", @@ -62,6 +62,10 @@ "lint": "oxlint --quiet src modules", "lint-native": "swiftlint ./modules && ktlint ./modules", "lint-native:fix": "swiftlint --fix ./modules && ktlint --format ./modules", + "lexicons:generate": "lex build --clear --index-file --import-ext ''", + "lexicons:install": "lex install", + "lexicons:update": "lex install --update", + "lexicons:verify": "lex install --ci", "typecheck": "pnpm run typecheck:ios && pnpm run typecheck:android && pnpm run typecheck:web", "typecheck:ios": "tsc --project ./tsconfig.check.ios.json", "typecheck:android": "tsc --project ./tsconfig.check.android.json", @@ -97,10 +101,10 @@ }, "dependencies": { "@atproto/api": "0.20.39", - "@atproto/common-web": "0.5.7", - "@atproto/lex-client": "0.3.0", - "@atproto/lex-password-session": "0.1.8", - "@atproto/syntax": "0.7.2", + "@atproto/common-web": "0.5.9", + "@atproto/lex": "0.3.6", + "@atproto/lex-password-session": "0.2.0", + "@atproto/syntax": "0.7.4", "@bitdrift/react-native": "^0.6.8", "@braintree/sanitize-url": "^6.0.2", "@bsky.app/alf": "^0.1.15", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b4baf4a374..fa725d0bff 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -246,17 +246,17 @@ importers: specifier: 0.20.39 version: 0.20.39 '@atproto/common-web': - specifier: 0.5.7 - version: 0.5.7 - '@atproto/lex-client': - specifier: 0.3.0 - version: 0.3.0 + specifier: 0.5.9 + version: 0.5.9 + '@atproto/lex': + specifier: 0.3.6 + version: 0.3.6 '@atproto/lex-password-session': - specifier: 0.1.8 - version: 0.1.8 + specifier: 0.2.0 + version: 0.2.0 '@atproto/syntax': - specifier: 0.7.2 - version: 0.7.2 + specifier: 0.7.4 + version: 0.7.4 '@bitdrift/react-native': specifier: ^0.6.8 version: 0.6.14(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0)(supports-color@8.1.1) @@ -881,52 +881,97 @@ packages: graphql: optional: true - '@atproto/api@0.20.39': - resolution: {integrity: sha512-FRjwAmYe0kKlWsAe8IvfbmAAKyjpXnl2NV67vWEEjux0FwIEIG6JbOo6YkD5tjK2xjL0Daa429R0E8phmxRrrQ==} + '@atproto-labs/did-resolver@0.3.7': + resolution: {integrity: sha512-F3M3U5cU1QSAXX3J5auGEc5N2pgDgpirAjvyDFsytXuyWfrjWfTPd/H+DZrrED7gK+noW0cAWtxxjdX7/cTSVQ==} engines: {node: '>=22'} - '@atproto/common-web@0.5.7': - resolution: {integrity: sha512-lvU8lFO5UByacsl3AReXkpTWjcPBS8mBqt40B45HwCt+s58a+xT/YI/+AUpmzSmsPVZCpeCmP6wgsOSC8RILXg==} + '@atproto-labs/fetch@0.3.5': + resolution: {integrity: sha512-iDFZEoNqfL17EVKE+uNzdUD7YF8LWhEhxeBtP6x+PNuAxoXv3ip9vLmB8nNzNxFwVn++FipfDtSiPqmziv1bdA==} + engines: {node: '>=22'} + + '@atproto-labs/pipe@0.2.4': + resolution: {integrity: sha512-n67jCcrC+ouAeO10cWkpPzzLMlDi/lDCU30Us+LGqhOPhT6c4t5ASdBLQi9W3jUQtRzQBt3G9zipF+xKWNvVbw==} + engines: {node: '>=22'} + + '@atproto-labs/simple-store-memory@0.2.6': + resolution: {integrity: sha512-DD1v7MEfYF3BAcEpMTTpzfBLWLoI2HuyBhku2YpjHoqPrRrhCtE1alkxfPHjtjJVIjuU/XNU0cF/wB3+5FiKsA==} + engines: {node: '>=22'} + + '@atproto-labs/simple-store@0.5.1': + resolution: {integrity: sha512-vfvoDhu6ds6BT3Pqe+d2/LBU1WpDRtt69y6zltlfMCoucPm82m1j50/Wb6xTvv+oZ+2j0JyZVXsmXQ12SWYQJg==} + engines: {node: '>=22'} + + '@atproto/api@0.20.39': + resolution: {integrity: sha512-FRjwAmYe0kKlWsAe8IvfbmAAKyjpXnl2NV67vWEEjux0FwIEIG6JbOo6YkD5tjK2xjL0Daa429R0E8phmxRrrQ==} engines: {node: '>=22'} '@atproto/common-web@0.5.9': resolution: {integrity: sha512-2c5C6YV8352JJz9Z5fNfcsstllaKGAb3cQW9aO7unMAGh/FzTGFq+xOwNMW2lTeagO6z530uxYm8AiSRM6O+DQ==} engines: {node: '>=22'} - '@atproto/lex-client@0.3.0': - resolution: {integrity: sha512-P+S3hWdIIOgNTun7J42dvxSyCkQog3Q2ShTjvcwP761TqiRex7zDWaRAaT9gvCy6I4pRuNdBCStysjSMlJeSdg==} + '@atproto/common@0.8.0': + resolution: {integrity: sha512-fBtd08LdouLqNaL8SaGLEIa0l3Ue27yR810jhUu+iBv/xdoXp+1G0pxvPe6JUStH7YGtf/v/v0w/cRGh1xUOaA==} engines: {node: '>=22'} - '@atproto/lex-data@0.1.6': - resolution: {integrity: sha512-rvovRrYWDEuerX+ZeQbdI5dKs0bYvuMjhwDOtT+pkOvdY4MkfN41DJCz/RAYVAQ7noPdaalB6pAX9euukheE/w==} + '@atproto/crypto@0.5.4': + resolution: {integrity: sha512-UR0BkuYNYuFtw+dA+y/oPPxzX0SWRnGJ+1Cfh/jGP1BvjRUyezK3omjpeLms5fYrXbM9vnfX+ckJFJqkBgLOdw==} + engines: {node: '>=22'} + + '@atproto/did@0.5.4': + resolution: {integrity: sha512-BlnwQ+obL+4ZA71KH/EzZ3TY+cpSxnLiUI85mjBJIQwvDF/oN2sQA18wIj9jpduviIt2b/cMtlJuzHjzBkfXvw==} + engines: {node: '>=22'} + + '@atproto/lex-builder@0.1.12': + resolution: {integrity: sha512-uo3mcdoOgqBfQnf+PhYYLNEBdUTgoNe2ARYb3cUaiec8O0oritOeZndUDgmths06ACrlqFkZNTV1YJX6CsH71g==} + engines: {node: '>=22'} + + '@atproto/lex-cbor@0.1.6': + resolution: {integrity: sha512-PxLQy7jzV8u9zTGURNF8ZczLPk+ZH+WNx/2z/z7RacJbu7sWNpx26U2pC7cIkWtt4jyDaruaLhssw3w3atLotA==} + engines: {node: '>=22'} + + '@atproto/lex-client@0.3.3': + resolution: {integrity: sha512-sWChe7Jukm9uxDfxT6ElICp6vMTqAFmwcHdSLdMDtsKBZSrwO7vWL9F6rpoMPyxS29Ob9nNAhMg30kKfZrb+uw==} engines: {node: '>=22'} '@atproto/lex-data@0.1.7': resolution: {integrity: sha512-kW/dPLqo/WgCLV+XESR4JKwV6c1rZWJGOfuPupZGTjEDAKoBbKXdaEzX9/1vKQYbZ9U3j0DS/n7OFFK7wBugyQ==} engines: {node: '>=22'} - '@atproto/lex-json@0.1.5': - resolution: {integrity: sha512-qOp6+Za5p/xDXACYIP3vySbKaf9CJDwiZEN78ghPT1gRoJohKG3ONt/WmvOxBVJqc6VTRaqEpbG9vX9oLF2qsA==} + '@atproto/lex-document@0.1.9': + resolution: {integrity: sha512-TmpwttQ9pKCXlvwUlJ/5HMZPtJMFBvgZ1bunEeQD5JfU6n/sgK/fob8tfjhTdL2QrpZMtM6opMEk3w5STm1RDQ==} + engines: {node: '>=22'} + + '@atproto/lex-installer@0.1.15': + resolution: {integrity: sha512-4/xDyY7yxxs66vF0WZB+7fcuEy4I5IRDyDoQFzLQx7C6PlQ76/e/PxvYTff7x0/N6hknmOlANODnnxo3rtdnCA==} engines: {node: '>=22'} '@atproto/lex-json@0.1.6': resolution: {integrity: sha512-mvrAd0lbyuecIHjyld8QN6MN6CBf4j0GCxLzegsvLh0SvDf+GbYWklkcQqmITL44yFQOwmA/QNIQj0Uvh7+R/g==} engines: {node: '>=22'} - '@atproto/lex-password-session@0.1.8': - resolution: {integrity: sha512-Zr3HLu6rgadiq2w82VSThOQcNfwTJNlrzlT8n9oBXDBw3CUEq3cTMatzTpdk4HYqv0unaqhaEowcKhJ4eWvSxQ==} + '@atproto/lex-password-session@0.2.0': + resolution: {integrity: sha512-u958Etax/bPVr9FWko+/B5nRcU1klQHvrdiVtYb4lkgrM6uvV7BvaFJZYEu5DzlEeqNAZ9a65PmbC1F5oyUBjQ==} + engines: {node: '>=22'} + + '@atproto/lex-resolver@0.2.8': + resolution: {integrity: sha512-j3dIxeEAStp/RzJS4MECRDPuOFX41mX9fnteGgrQhe9053eRuTnl2oTyJbV7rKgzy+hEI4jH6X6Y4wOStjUb/Q==} engines: {node: '>=22'} '@atproto/lex-schema@0.2.5': resolution: {integrity: sha512-/tP3dRqaZu0EIoq0BSVzhvFGL6u0uu2KFDV21i73DpFsg6TXOTddKJ9bvgK83XdAJYL7PWi703XsaR31v9u5Qg==} engines: {node: '>=22'} + '@atproto/lex@0.3.6': + resolution: {integrity: sha512-8Hpc6MbfkPJKaO5CvvIerEylbihI5GfrY5DzapZD4rV2TWfm3RU+DzeRpsxQJ0W9WOZRYvp1JnBcq/gZ48Qb0g==} + engines: {node: '>=22'} + hasBin: true + '@atproto/lexicon@0.7.11': resolution: {integrity: sha512-ukCZMCWmiu7fAlssBbLs+IMaf2K2R9mADaTKz2jEeC/xeZ3jT77Ka0S2G+ELBB+Dc9ijBACU0iPMQLhquOkuzQ==} engines: {node: '>=22'} - '@atproto/syntax@0.7.2': - resolution: {integrity: sha512-tZ1Tr0R9pK4bI4Zs69t29cjMlCFQvRNBeNkqJC5pGeNuCt64D0eoF7s/AlqeVmYppClmQ0xJquggGgsMDP6j7w==} + '@atproto/repo@0.10.11': + resolution: {integrity: sha512-QqDzEv8d6NUuqZ0xvWG+n8DfWd4Vuka04nyuD9oglKsj5avB1zbEsWx3Oit1oYtZFUf9JSVWMDqMXhu3UbB+lA==} engines: {node: '>=22'} '@atproto/syntax@0.7.4': @@ -2336,6 +2381,14 @@ packages: '@messageformat/parser@5.1.1': resolution: {integrity: sha512-3p0YRGCcTUCYvBKLIxtDDyrJ0YijGIwrTRu1DT8gIviIDZru8H23+FkY6MJBzM1n9n20CiM4VeDYuBsrrwnLjg==} + '@noble/curves@1.9.7': + resolution: {integrity: sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==} + engines: {node: ^14.21.3 || >=16} + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -2500,6 +2553,9 @@ packages: cpu: [x64] os: [win32] + '@pinojs/redact@0.4.0': + resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -3661,6 +3717,9 @@ packages: resolution: {integrity: sha512-HqmEUIGRJ5fSXchkVgR5F7qn48bDBzv0kWj/Kfu5e6uci4UlEeng4331LnBkWffb++Ei3FOVLxo8JJWMFBDMeQ==} engines: {node: '>= 10'} + '@ts-morph/common@0.28.1': + resolution: {integrity: sha512-W74iWf7ILp1ZKNYXY5qbddNaml7e9Sedv5lvU1V8lftlitkc9Pq1A+jlH23ltDgWYeZFFEqGCD1Ies9hqu3O+g==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -4246,6 +4305,10 @@ packages: asynckit@0.4.0: resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} @@ -4488,6 +4551,10 @@ packages: caniuse-lite@1.0.30001792: resolution: {integrity: sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==} + cborg@4.5.8: + resolution: {integrity: sha512-6/viltD51JklRhq4L7jC3zgy6gryuG5xfZ3kzpE+PravtyeQLeQmCYLREhQH7pWENg5pY4Yu/XCd6a7dKScVlw==} + hasBin: true + cborg@5.1.1: resolution: {integrity: sha512-BDbSRIp6XrQXkTc7g+DN0RB9RrDPTUfals2ecWUlt3juPLjbAvy/V72mJcXY0Ehu0Dq/3WpNCOCT68HUTbW+lw==} hasBin: true @@ -4591,6 +4658,10 @@ packages: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} + cliui@9.0.1: + resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==} + engines: {node: '>=20'} + clone@1.0.4: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} @@ -4599,6 +4670,9 @@ packages: resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} + code-block-writer@13.0.3: + resolution: {integrity: sha512-Oofo0pq3IKnsFtuHqSF7TqBfr71aeyZDVJ0HpmqB7FBM2qEigL0iPONSCZSO9pE9dZTAxANe5XHG9Uy0YMv8cg==} + collect-v8-coverage@1.0.3: resolution: {integrity: sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==} @@ -4722,6 +4796,9 @@ packages: core-js-pure@3.49.0: resolution: {integrity: sha512-XM4RFka59xATyJv/cS3O3Kml72hQXUeGRuuTmMYFxwzc9/7C8OYTaIR/Ji+Yt8DXzsFLNhat15cE/JP15HrCgw==} + core-js@3.50.0: + resolution: {integrity: sha512-BRWgOLKkFeCgRudR6zrs8p9XJZcE14grzKMMssoYrk6krtuEZ7MTKPIY5RzOnqsEKIR9kst7wNzphttraT+Yqw==} + core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} @@ -7421,6 +7498,10 @@ packages: obuf@1.1.2: resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + on-finished@2.3.0: resolution: {integrity: sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==} engines: {node: '>= 0.8'} @@ -7556,6 +7637,9 @@ packages: pascal-case@3.1.2: resolution: {integrity: sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==} + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + path-exists@3.0.0: resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} engines: {node: '>=4'} @@ -7623,6 +7707,16 @@ packages: resolution: {integrity: sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==} engines: {node: '>=0.10.0'} + pino-abstract-transport@3.0.0: + resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==} + + pino-std-serializers@7.1.0: + resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==} + + pino@10.3.1: + resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==} + hasBin: true + pirates@4.0.7: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} @@ -7867,6 +7961,11 @@ packages: engines: {node: '>=14'} hasBin: true + prettier@3.9.6: + resolution: {integrity: sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==} + engines: {node: '>=14'} + hasBin: true + pretty-bytes@5.6.0: resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==} engines: {node: '>=6'} @@ -7889,6 +7988,9 @@ packages: process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + process-warning@5.1.0: + resolution: {integrity: sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw==} + progress@2.0.3: resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} engines: {node: '>=0.4.0'} @@ -8016,6 +8118,9 @@ packages: queue@6.0.2: resolution: {integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==} + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + radix-ui@1.4.3: resolution: {integrity: sha512-aWizCQiyeAenIdUbqEpXgRA1ya65P13NKn/W8rWkcN0OPkRDxdBVLWnIEDsS2RpwCK2nobI7oMUSmexzTDyAmA==} peerDependencies: @@ -8317,6 +8422,13 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + + real-require@1.0.0: + resolution: {integrity: sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==} + redent@3.0.0: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} engines: {node: '>=8'} @@ -8472,6 +8584,10 @@ packages: resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} engines: {node: '>= 0.4'} + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} @@ -8648,6 +8764,9 @@ packages: sockjs@0.3.24: resolution: {integrity: sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==} + sonic-boom@4.2.1: + resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} + sonner-native@0.26.4: resolution: {integrity: sha512-wBE/pctNfz+ieXjpYX7uu5hH38ws8xRmH0AFmEdEgkOS363k6N8QfFoyjGqDmdUDy15m4AAvOjd2Wz/jATukIA==} peerDependencies: @@ -8712,6 +8831,10 @@ packages: resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==} engines: {node: '>=6'} + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} @@ -8970,6 +9093,10 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + thread-stream@4.2.0: + resolution: {integrity: sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==} + engines: {node: '>=20'} + threads@1.7.0: resolution: {integrity: sha512-Mx5NBSHX3sQYR6iI9VYbgHKBLisyB+xROCBGjjWm1O9wb9vfLxdaGtmT/KCjUqMsSNW6nERzCW3T6H43LqjDZQ==} @@ -9036,6 +9163,9 @@ packages: ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + ts-morph@27.0.2: + resolution: {integrity: sha512-fhUhgeljcrdZ+9DZND1De1029PrE+cMkIP7ooqkLRTrRLTqcki2AstsyJm0vRNbTbVCNJ0idGlbBrfqc7/nA8w==} + ts-plugin-sort-import-suggestions@1.0.4: resolution: {integrity: sha512-85n5lm2OQQ+b7aRNK9omU1gmjMNXRsgeLwojm5u4OSY5sVBkAHTcgMQPEeHMNlyyfFW0uXnwgqAU0pNfhD96Bw==} @@ -9107,6 +9237,9 @@ packages: uc.micro@2.1.0: resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} + uint8arrays@5.1.1: + resolution: {integrity: sha512-9muQwa4wZG4dKi9gMAIBtnk2Pw87SRpvWTH6lOGm19V2Uqxr4uomUf2PGqPnWc+qs06sN8owUU4jfcoWOcfwVQ==} + unbox-primitive@1.1.0: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} @@ -9263,6 +9396,9 @@ packages: resolution: {integrity: sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + varint@6.0.0: + resolution: {integrity: sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==} + vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -9539,6 +9675,10 @@ packages: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} + yargs-parser@22.0.0: + resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + yargs@15.4.1: resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} engines: {node: '>=8'} @@ -9547,6 +9687,10 @@ packages: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} + yargs@18.1.0: + resolution: {integrity: sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + yauzl@3.3.0: resolution: {integrity: sha512-PtGEvEP30p7sbIBJKUBjUnqgTVOyMURc4dLo9iNyAJnNIEz9pm88cCXF21w94Kg3k6RXkeZh5DHOGS0qEONvNQ==} engines: {node: '>=12'} @@ -9568,6 +9712,28 @@ snapshots: '@0no-co/graphql.web@1.2.0': {} + '@atproto-labs/did-resolver@0.3.7': + dependencies: + '@atproto-labs/fetch': 0.3.5 + '@atproto-labs/pipe': 0.2.4 + '@atproto-labs/simple-store': 0.5.1 + '@atproto-labs/simple-store-memory': 0.2.6 + '@atproto/did': 0.5.4 + zod: 3.25.76 + + '@atproto-labs/fetch@0.3.5': + dependencies: + '@atproto-labs/pipe': 0.2.4 + + '@atproto-labs/pipe@0.2.4': {} + + '@atproto-labs/simple-store-memory@0.2.6': + dependencies: + '@atproto-labs/simple-store': 0.5.1 + lru-cache: 10.4.3 + + '@atproto-labs/simple-store@0.5.1': {} + '@atproto/api@0.20.39': dependencies: '@atproto/common-web': 0.5.9 @@ -9579,13 +9745,6 @@ snapshots: tlds: 1.261.0 zod: 3.25.76 - '@atproto/common-web@0.5.7': - dependencies: - '@atproto/lex-data': 0.1.6 - '@atproto/lex-json': 0.1.5 - '@atproto/syntax': 0.7.2 - zod: 3.25.76 - '@atproto/common-web@0.5.9': dependencies: '@atproto/lex-data': 0.1.7 @@ -9593,28 +9752,66 @@ snapshots: '@atproto/syntax': 0.7.4 zod: 3.25.76 - '@atproto/lex-client@0.3.0': + '@atproto/common@0.8.0': + dependencies: + '@atproto/common-web': 0.5.9 + '@atproto/lex-cbor': 0.1.6 + '@atproto/lex-data': 0.1.7 + multiformats: 13.4.2 + pino: 10.3.1 + + '@atproto/crypto@0.5.4': + dependencies: + '@noble/curves': 1.9.7 + '@noble/hashes': 1.8.0 + uint8arrays: 5.1.1 + + '@atproto/did@0.5.4': + dependencies: + zod: 3.25.76 + + '@atproto/lex-builder@0.1.12': + dependencies: + '@atproto/lex-document': 0.1.9 + '@atproto/lex-schema': 0.2.5 + prettier: 3.9.6 + ts-morph: 27.0.2 + tslib: 2.8.1 + + '@atproto/lex-cbor@0.1.6': + dependencies: + '@atproto/lex-data': 0.1.7 + cborg: 4.5.8 + tslib: 2.8.1 + + '@atproto/lex-client@0.3.3': dependencies: '@atproto/lex-data': 0.1.7 '@atproto/lex-json': 0.1.6 '@atproto/lex-schema': 0.2.5 tslib: 2.8.1 - '@atproto/lex-data@0.1.6': - dependencies: - multiformats: 13.4.2 - tslib: 2.8.1 - unicode-segmenter: 0.14.5 - '@atproto/lex-data@0.1.7': dependencies: multiformats: 13.4.2 tslib: 2.8.1 unicode-segmenter: 0.14.5 - '@atproto/lex-json@0.1.5': + '@atproto/lex-document@0.1.9': dependencies: - '@atproto/lex-data': 0.1.6 + '@atproto/lex-schema': 0.2.5 + core-js: 3.50.0 + tslib: 2.8.1 + + '@atproto/lex-installer@0.1.15': + dependencies: + '@atproto/lex-builder': 0.1.12 + '@atproto/lex-cbor': 0.1.6 + '@atproto/lex-data': 0.1.7 + '@atproto/lex-document': 0.1.9 + '@atproto/lex-resolver': 0.2.8 + '@atproto/lex-schema': 0.2.5 + '@atproto/syntax': 0.7.4 tslib: 2.8.1 '@atproto/lex-json@0.1.6': @@ -9622,12 +9819,24 @@ snapshots: '@atproto/lex-data': 0.1.7 tslib: 2.8.1 - '@atproto/lex-password-session@0.1.8': + '@atproto/lex-password-session@0.2.0': dependencies: - '@atproto/lex-client': 0.3.0 + '@atproto/lex-client': 0.3.3 '@atproto/lex-schema': 0.2.5 tslib: 2.8.1 + '@atproto/lex-resolver@0.2.8': + dependencies: + '@atproto-labs/did-resolver': 0.3.7 + '@atproto/crypto': 0.5.4 + '@atproto/lex-client': 0.3.3 + '@atproto/lex-data': 0.1.7 + '@atproto/lex-document': 0.1.9 + '@atproto/lex-schema': 0.2.5 + '@atproto/repo': 0.10.11 + '@atproto/syntax': 0.7.4 + tslib: 2.8.1 + '@atproto/lex-schema@0.2.5': dependencies: '@atproto/lex-data': 0.1.7 @@ -9635,6 +9844,17 @@ snapshots: '@standard-schema/spec': 1.1.0 tslib: 2.8.1 + '@atproto/lex@0.3.6': + dependencies: + '@atproto/lex-builder': 0.1.12 + '@atproto/lex-client': 0.3.3 + '@atproto/lex-data': 0.1.7 + '@atproto/lex-installer': 0.1.15 + '@atproto/lex-json': 0.1.6 + '@atproto/lex-schema': 0.2.5 + tslib: 2.8.1 + yargs: 18.1.0 + '@atproto/lexicon@0.7.11': dependencies: '@atproto/common-web': 0.5.9 @@ -9642,10 +9862,16 @@ snapshots: multiformats: 13.4.2 zod: 3.25.76 - '@atproto/syntax@0.7.2': + '@atproto/repo@0.10.11': dependencies: - iso-datestring-validator: 2.2.2 - tslib: 2.8.1 + '@atproto/common': 0.8.0 + '@atproto/common-web': 0.5.9 + '@atproto/crypto': 0.5.4 + '@atproto/lex-cbor': 0.1.6 + '@atproto/lex-data': 0.1.7 + '@atproto/syntax': 0.7.4 + varint: 6.0.0 + zod: 3.25.76 '@atproto/syntax@0.7.4': dependencies: @@ -11557,6 +11783,12 @@ snapshots: dependencies: moo: 0.5.3 + '@noble/curves@1.9.7': + dependencies: + '@noble/hashes': 1.8.0 + + '@noble/hashes@1.8.0': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -11644,6 +11876,8 @@ snapshots: '@oxlint/binding-win32-x64-msvc@1.73.0': optional: true + '@pinojs/redact@0.4.0': {} + '@pkgjs/parseargs@0.11.0': optional: true @@ -12938,6 +13172,12 @@ snapshots: '@tootallnate/once@2.0.1': {} + '@ts-morph/common@0.28.1': + dependencies: + minimatch: 10.2.5 + path-browserify: 1.0.1 + tinyglobby: 0.2.16 + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.29.3 @@ -13522,6 +13762,8 @@ snapshots: asynckit@0.4.0: {} + atomic-sleep@1.0.0: {} + available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.1.0 @@ -13869,6 +14111,8 @@ snapshots: caniuse-lite@1.0.30001792: {} + cborg@4.5.8: {} + cborg@5.1.1: {} chalk@2.4.2: @@ -13991,10 +14235,18 @@ snapshots: strip-ansi: 6.0.1 wrap-ansi: 7.0.0 + cliui@9.0.1: + dependencies: + string-width: 7.2.0 + strip-ansi: 7.2.0 + wrap-ansi: 9.0.2 + clone@1.0.4: {} co@4.6.0: {} + code-block-writer@13.0.3: {} + collect-v8-coverage@1.0.3: {} color-convert@1.9.3: @@ -14106,6 +14358,8 @@ snapshots: core-js-pure@3.49.0: {} + core-js@3.50.0: {} + core-util-is@1.0.3: {} cosmiconfig@8.3.6(@typescript/typescript6@6.0.2): @@ -17308,6 +17562,8 @@ snapshots: obuf@1.1.2: {} + on-exit-leak-free@2.1.2: {} + on-finished@2.3.0: dependencies: ee-first: 1.1.1 @@ -17479,6 +17735,8 @@ snapshots: no-case: 3.0.4 tslib: 2.8.1 + path-browserify@1.0.1: {} + path-exists@3.0.0: {} path-exists@4.0.0: {} @@ -17523,6 +17781,26 @@ snapshots: pinkie@2.0.4: {} + pino-abstract-transport@3.0.0: + dependencies: + split2: 4.2.0 + + pino-std-serializers@7.1.0: {} + + pino@10.3.1: + dependencies: + '@pinojs/redact': 0.4.0 + atomic-sleep: 1.0.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 3.0.0 + pino-std-serializers: 7.1.0 + process-warning: 5.1.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.1 + thread-stream: 4.2.0 + pirates@4.0.7: {} pkg-dir@4.2.0: @@ -17744,6 +18022,8 @@ snapshots: prettier@3.8.3: {} + prettier@3.9.6: {} + pretty-bytes@5.6.0: {} pretty-error@4.0.0: @@ -17768,6 +18048,8 @@ snapshots: process-nextick-args@2.0.1: {} + process-warning@5.1.0: {} + progress@2.0.3: {} promise@7.3.1: @@ -17938,6 +18220,8 @@ snapshots: dependencies: inherits: 2.0.4 + quick-format-unescaped@4.0.4: {} + radix-ui@1.4.3(@types/react-dom@19.1.11(@types/react@19.1.17))(@types/react@19.1.17)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: '@radix-ui/primitive': 1.1.3 @@ -18351,6 +18635,10 @@ snapshots: dependencies: picomatch: 2.3.2 + real-require@0.2.0: {} + + real-require@1.0.0: {} + redent@3.0.0: dependencies: indent-string: 4.0.0 @@ -18517,6 +18805,8 @@ snapshots: es-errors: 1.3.0 is-regex: 1.2.1 + safe-stable-stringify@2.5.0: {} + safer-buffer@2.1.2: {} sax@1.6.0: {} @@ -18727,6 +19017,10 @@ snapshots: uuid: 8.3.2 websocket-driver: 0.7.4 + sonic-boom@4.2.1: + dependencies: + atomic-sleep: 1.0.0 + sonner-native@0.26.4(dd52c15d65a2604bcf203dfed917879b): dependencies: react: 19.1.0 @@ -18795,6 +19089,8 @@ snapshots: split-on-first@1.1.0: {} + split2@4.2.0: {} + sprintf-js@1.0.3: {} stable@0.1.8: {} @@ -19050,6 +19346,10 @@ snapshots: dependencies: any-promise: 1.3.0 + thread-stream@4.2.0: + dependencies: + real-require: 1.0.0 + threads@1.7.0(supports-color@8.1.1): dependencies: callsites: 3.1.0 @@ -19116,6 +19416,11 @@ snapshots: ts-interface-checker@0.1.13: {} + ts-morph@27.0.2: + dependencies: + '@ts-morph/common': 0.28.1 + code-block-writer: 13.0.3 + ts-plugin-sort-import-suggestions@1.0.4: {} tslib@2.8.1: {} @@ -19204,6 +19509,10 @@ snapshots: uc.micro@2.1.0: {} + uint8arrays@5.1.1: + dependencies: + multiformats: 13.4.2 + unbox-primitive@1.1.0: dependencies: call-bound: 1.0.4 @@ -19335,6 +19644,8 @@ snapshots: validate-npm-package-name@5.0.1: {} + varint@6.0.0: {} + vary@1.1.2: {} vlq@1.0.1: {} @@ -19661,6 +19972,8 @@ snapshots: yargs-parser@21.1.1: {} + yargs-parser@22.0.0: {} + yargs@15.4.1: dependencies: cliui: 6.0.0 @@ -19685,6 +19998,15 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 + yargs@18.1.0: + dependencies: + cliui: 9.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + string-width: 8.2.1 + y18n: 5.0.8 + yargs-parser: 22.0.0 + yauzl@3.3.0: dependencies: buffer-crc32: 0.2.13 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index aa9a37f930..372e071c73 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -16,8 +16,9 @@ overrides: 'react-native-screens': '4.24.0' allowBuilds: '@sentry/cli': true - 'core-js-pure': true - 'esbuild': true + 'core-js': false + 'core-js-pure': false + 'esbuild': false 'unrs-resolver': true patchedDependencies: '@sentry/expo-upload-sourcemaps@8.18.0': patches/@sentry__expo-upload-sourcemaps@8.18.0.patch diff --git a/src/lib/strings/__tests__/errors.test.ts b/src/lib/strings/__tests__/errors.test.ts index 03cdd422c8..1d9ccd8a3d 100644 --- a/src/lib/strings/__tests__/errors.test.ts +++ b/src/lib/strings/__tests__/errors.test.ts @@ -1,5 +1,5 @@ import {XRPCError} from '@atproto/api' -import {LexError, XrpcResponseError} from '@atproto/lex-client' +import {LexError, XrpcResponseError} from '@atproto/lex' import {beforeAll, describe, expect, it} from '@jest/globals' import {i18n} from '@lingui/core' diff --git a/src/lib/strings/errors.ts b/src/lib/strings/errors.ts index 4f793ec195..fb2b0c5b9d 100644 --- a/src/lib/strings/errors.ts +++ b/src/lib/strings/errors.ts @@ -1,5 +1,5 @@ import {XRPCError} from '@atproto/api' -import {LexError} from '@atproto/lex-client' +import {LexError} from '@atproto/lex' import {t} from '@lingui/core/macro' /** diff --git a/src/screens/Signup/state.ts b/src/screens/Signup/state.ts index 33a5d26a64..8acbd47908 100644 --- a/src/screens/Signup/state.ts +++ b/src/screens/Signup/state.ts @@ -1,7 +1,7 @@ import {createContext, useCallback, useContext} from 'react' import {LayoutAnimation} from 'react-native' import {type ComAtprotoServerDescribeServer} from '@atproto/api' -import {XrpcResponseError} from '@atproto/lex-client' +import {XrpcResponseError} from '@atproto/lex' import {useLingui} from '@lingui/react/macro' import * as EmailValidator from 'email-validator' From 837771a31180038d6b6e9a92b940dd867a5ba0cb Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 13 Aug 2026 22:26:13 +0300 Subject: [PATCH 04/32] [SDK] Add lex client seam and typed xrpc error matching (#11348) Co-authored-by: Claude Fable 5 --- src/lib/__tests__/xrpc-error.test.ts | 91 +++++++++ src/lib/lexClient.ts | 26 +++ src/lib/xrpc-error.ts | 59 ++++++ src/screens/Signup/state.ts | 43 ++--- .../queries/trending/useGetTrendsQuery.ts | 12 +- src/state/session/__tests__/clients-test.ts | 179 ++++++++++++++++++ src/state/session/clients.ts | 71 +++++++ src/state/session/index.tsx | 21 ++ 8 files changed, 467 insertions(+), 35 deletions(-) create mode 100644 src/lib/__tests__/xrpc-error.test.ts create mode 100644 src/lib/lexClient.ts create mode 100644 src/lib/xrpc-error.ts create mode 100644 src/state/session/__tests__/clients-test.ts create mode 100644 src/state/session/clients.ts diff --git a/src/lib/__tests__/xrpc-error.test.ts b/src/lib/__tests__/xrpc-error.test.ts new file mode 100644 index 0000000000..5c09fb4f08 --- /dev/null +++ b/src/lib/__tests__/xrpc-error.test.ts @@ -0,0 +1,91 @@ +import { + getMain, + type Procedure, + type Query, + XrpcInternalError, + XrpcResponseError, +} from '@atproto/lex' +import {describe, expect, it} from '@jest/globals' + +import {app, com} from '#/lexicons' +import {matchXrpcError} from '../xrpc-error' + +const createAccount = com.atproto.server.createAccount +const getTrends = app.bsky.unspecced.getTrends + +/** + * An `XrpcResponseError` as a lex `Client` would construct it: the method + * schema it was thrown for, plus the server's error response and its parsed + * payload. + */ +function responseError(method: Procedure | Query, error: string, status = 400) { + return new XrpcResponseError( + method, + new Response(JSON.stringify({error}), { + status, + headers: {'content-type': 'application/json'}, + }), + {encoding: 'application/json', body: {error}}, + ) +} + +describe('matchXrpcError', () => { + it('returns a code declared by the method', () => { + const e = responseError(getMain(createAccount), 'InvalidHandle') + expect(matchXrpcError(e, createAccount)).toBe('InvalidHandle') + }) + + it('accepts the .main schema as well as the namespace', () => { + const e = responseError(getMain(createAccount), 'InvalidInviteCode') + expect(matchXrpcError(e, createAccount.main)).toBe('InvalidInviteCode') + }) + + it('returns undefined for a code the method does not declare', () => { + const e = responseError(getMain(createAccount), 'RateLimitExceeded') + expect(matchXrpcError(e, createAccount)).toBeUndefined() + }) + + it('returns undefined for a method that declares no errors at all', () => { + const e = responseError(getMain(getTrends), 'InvalidHandle') + expect(matchXrpcError(e, getTrends)).toBeUndefined() + }) + + it('does not match a declared code thrown for a different method', () => { + /* + * `InvalidHandle` is declared by createAccount but this error came from a + * getTrends call, so scoping must reject it. + */ + const e = responseError(getMain(getTrends), 'InvalidHandle') + expect(matchXrpcError(e, createAccount)).toBeUndefined() + }) + + it('returns undefined for a lex error carrying no server code', () => { + const e = new XrpcInternalError(getMain(createAccount), 'boom') + expect(matchXrpcError(e, createAccount)).toBeUndefined() + }) + + it('returns undefined for non-lex errors and non-errors', () => { + expect(matchXrpcError(new Error('InvalidHandle'), createAccount)).toBe( + undefined, + ) + expect(matchXrpcError('InvalidHandle', createAccount)).toBeUndefined() + expect(matchXrpcError(undefined, createAccount)).toBeUndefined() + }) + + it('narrows the result to the declared-errors union', () => { + const e = responseError(getMain(createAccount), 'InvalidHandle') + const code = matchXrpcError(e, createAccount) + + /* + * A misspelled or undeclared code is not comparable to the narrowed union, + * which is what makes a typo'd `switch` case a compile error (TS2678 / + * TS2367) rather than a branch that never runs. + */ + // @ts-expect-error 'InvalidHandel' is not a declared createAccount error + expect(code === 'InvalidHandel').toBe(false) + // @ts-expect-error 'RateLimitExceeded' is not declared by createAccount + expect(code === 'RateLimitExceeded').toBe(false) + + expect(code === 'UnsupportedDomain').toBe(false) + }) +}) diff --git a/src/lib/lexClient.ts b/src/lib/lexClient.ts new file mode 100644 index 0000000000..40b0640665 --- /dev/null +++ b/src/lib/lexClient.ts @@ -0,0 +1,26 @@ +import { + type Agent, + type AgentOptions, + Client, + type ClientOptions, +} from '@atproto/lex' + +/** + * App-standard factory for lex {@link Client}s. Use this instead of `new + * Client(...)` so every client shares the same lenient response processing. + * + * lex-client defaults to strict Lex processing, which rejects responses + * containing the LEGACY blob reference format (objects with `cid` and + * `mimeType` properties instead of `$type: 'blob'`). Older records on the + * network still carry these, and rejecting them would drop records the app + * currently renders. Lenient mode also relaxes datetime format checks (e.g. + * missing timezones) and blob MIME/size constraints. `Client.configure` only + * accepts `appLabelers` globally, so the option is defaulted here, per + * constructed client. + */ +export function createLexClient( + agent: Agent | AgentOptions, + options?: ClientOptions, +): Client { + return new Client(agent, {strictResponseProcessing: false, ...options}) +} diff --git a/src/lib/xrpc-error.ts b/src/lib/xrpc-error.ts new file mode 100644 index 0000000000..25f9a1277e --- /dev/null +++ b/src/lib/xrpc-error.ts @@ -0,0 +1,59 @@ +import { + getMain, + type InferMethodError, + type Main, + type Procedure, + type Query, + XrpcResponseError, +} from '@atproto/lex' + +/** + * Same nsid means `e` was thrown for this method schema, so `e` can be + * treated as an `XrpcResponseError` - which is what lets the SDK's + * `matchesSchemaErrors()` narrow `e.error` to M's declared errors. + */ +function isThrownFor( + e: XrpcResponseError, + schema: M, +): e is XrpcResponseError { + return e.method.nsid === schema.nsid +} + +/** + * The lexicon error code carried by `e`, narrowed to the errors DECLARED by + * `method`, or `undefined` when `e` is not such an error. + * + * `XrpcResponseError.error` is the open `LexErrorCode` union, so comparing it + * as a plain string lets a typo silently never match. Narrowing the return type + * to `InferMethodError` makes a `switch` over the result reject an + * undeclared or misspelled `case` at compile time: + * + * ```ts + * switch (matchXrpcError(e, com.atproto.server.createAccount)) { + * case 'InvalidHandle': + * ... + * } + * ``` + * + * Matching is scoped to `method`: `XrpcError` records the method schema it was + * thrown for, so a declared code arriving from a DIFFERENT call does not match. + * Undeclared codes, non-XRPC errors, and the internal/fetch lex errors (which + * carry no server error code) all return `undefined`. + * + * `method` accepts the same value passed to `client.call` - either the + * generated method namespace (`com.atproto.server.createAccount`) or its + * `.main` schema - via lex's `Main`. + */ +export function matchXrpcError( + e: unknown, + method: Main, +): InferMethodError | undefined { + if (!(e instanceof XrpcResponseError)) { + return undefined + } + const schema = getMain(method) + if (isThrownFor(e, schema) && e.matchesSchemaErrors()) { + return e.error + } + return undefined +} diff --git a/src/screens/Signup/state.ts b/src/screens/Signup/state.ts index 8acbd47908..9f0ce29bcf 100644 --- a/src/screens/Signup/state.ts +++ b/src/screens/Signup/state.ts @@ -1,7 +1,6 @@ import {createContext, useCallback, useContext} from 'react' import {LayoutAnimation} from 'react-native' import {type ComAtprotoServerDescribeServer} from '@atproto/api' -import {XrpcResponseError} from '@atproto/lex' import {useLingui} from '@lingui/react/macro' import * as EmailValidator from 'email-validator' @@ -9,9 +8,11 @@ import {DEFAULT_SERVICE} from '#/lib/constants' import {cleanError, isNetworkError} from '#/lib/strings/errors' import {createFullHandle} from '#/lib/strings/handles' import {getAge} from '#/lib/strings/time' +import {matchXrpcError} from '#/lib/xrpc-error' import {useSessionApi} from '#/state/session' import {useOnboardingDispatch} from '#/state/shell' import {type AnalyticsContextType, useAnalytics} from '#/analytics' +import {com} from '#/lexicons' export type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema @@ -258,34 +259,13 @@ export const useSignupContext = () => useContext(SignupContext) * failure is unexpected and should be reported to Sentry. */ function classifyExpectedSignupError(e: unknown): string | undefined { - /* - * TODO: `XrpcResponseError.error` is the open `LexErrorCode` union, so these - * codes are compared as plain strings and a typo silently never matches. - * Once the generated lexicons land, replace this (and every multi-code error - * site) with a shared helper that narrows against the method schema: - * - * function matchXrpcError( - * e: unknown, - * method: Main, - * ): InferMethodError | undefined - * - * switch (matchXrpcError(e, com.atproto.server.createAccount)) { - * case 'InvalidHandle': ... - * } - * - * The return type is the method's declared-errors union, so a typo'd case is - * a compile error, and undeclared codes fall through to `undefined`. The - * helper should also match `e.method.nsid` so a declared code from a - * different call cannot match, mirroring the old per-method error classes. - */ - if (e instanceof XrpcResponseError) { - switch (e.error) { - case 'InvalidHandle': - case 'HandleNotAvailable': - case 'InvalidPassword': - case 'UnsupportedDomain': - return e.error - } + const code = matchXrpcError(e, com.atproto.server.createAccount) + switch (code) { + case 'InvalidHandle': + case 'HandleNotAvailable': + case 'InvalidPassword': + case 'UnsupportedDomain': + return code } /* the server sends no typed error for this case */ if (String(e).includes('Email already taken')) return 'EmailTaken' @@ -376,7 +356,10 @@ export function useSubmitSignup() { } catch (err) { const e = err as Error let errMsg = e.toString() - if (e instanceof XrpcResponseError && e.error === 'InvalidInviteCode') { + if ( + matchXrpcError(e, com.atproto.server.createAccount) === + 'InvalidInviteCode' + ) { dispatch({ type: 'setError', value: l`Invite code not accepted. Check that you input it correctly and try again.`, diff --git a/src/state/queries/trending/useGetTrendsQuery.ts b/src/state/queries/trending/useGetTrendsQuery.ts index 757e868d1f..e7d6806b3e 100644 --- a/src/state/queries/trending/useGetTrendsQuery.ts +++ b/src/state/queries/trending/useGetTrendsQuery.ts @@ -1,5 +1,5 @@ import {useCallback, useMemo} from 'react' -import {type AppBskyUnspeccedGetTrends, hasMutedWord} from '@atproto/api' +import {hasMutedWord} from '@atproto/api' import {useQuery} from '@tanstack/react-query' import { @@ -10,7 +10,8 @@ import {logger} from '#/logger' import {getContentLanguages} from '#/state/preferences/languages' import {STALE} from '#/state/queries' import {usePreferencesQuery} from '#/state/queries/preferences' -import {useAgent} from '#/state/session' +import {useLexClient} from '#/state/session' +import {app} from '#/lexicons' export const DEFAULT_LIMIT = 5 @@ -32,7 +33,7 @@ export const createGetTrendsQueryKey = (limit?: number) => limit === undefined ? ['trends'] : ['trends', {limit}] export function useGetTrendsQuery(props: QueryProps = {}) { - const agent = useAgent() + const client = useLexClient() const {data: preferences} = usePreferencesQuery() const limit = props.limit ?? DEFAULT_LIMIT const mutedWords = useMemo(() => { @@ -46,7 +47,8 @@ export function useGetTrendsQuery(props: QueryProps = {}) { queryKey: createGetTrendsQueryKey(limit), queryFn: async () => { const contentLangs = getContentLanguages().join(',') - const {data} = await agent.app.bsky.unspecced.getTrends( + const data = await client.call( + app.bsky.unspecced.getTrends, { limit, }, @@ -63,7 +65,7 @@ export function useGetTrendsQuery(props: QueryProps = {}) { return data }, select: useCallback( - (data: AppBskyUnspeccedGetTrends.OutputSchema) => { + (data: app.bsky.unspecced.getTrends.$OutputBody) => { return { recId: data.recIdStr, trends: dedupe( diff --git a/src/state/session/__tests__/clients-test.ts b/src/state/session/__tests__/clients-test.ts new file mode 100644 index 0000000000..f90841dbc8 --- /dev/null +++ b/src/state/session/__tests__/clients-test.ts @@ -0,0 +1,179 @@ +import {Client} from '@atproto/lex' +import {PasswordSession} from '@atproto/lex-password-session' +import {beforeEach, describe, expect, it, jest} from '@jest/globals' + +jest.mock('#/state/events', () => ({ + emitNetworkConfirmed: jest.fn(), + emitNetworkLost: jest.fn(), +})) + +jest.mock('jwt-decode', () => ({ + jwtDecode() { + return {scope: 'com.atproto.access'} + }, +})) + +import {app} from '#/lexicons' +import {BskyAppAgent, PasswordSessionManager} from '../bridge-agent' +import {agentToLexClient} from '../clients' +import {sessionAccountToSessionData} from '../session-data' +import { + asFetch, + DID, + HANDLE, + json, + makeAccount, + makeMockFetch, + type MockFetch, + SERVICE, +} from './mock-fetch' + +const PROFILE_BODY = { + did: DID, + handle: HANDLE, +} + +/** A mock fetch that answers `getProfile` and records every request. */ +function makeProfileFetch(): MockFetch { + return makeMockFetch({ + 'app.bsky.actor.getProfile': () => json(PROFILE_BODY), + }) +} + +/** An authenticated agent whose whole network path is the mock fetch. */ +function setup(fetchMock: MockFetch = makeProfileFetch()) { + const account = makeAccount() + const inner = new PasswordSession(sessionAccountToSessionData(account), { + fetch: asFetch(fetchMock), + }) + const manager = new PasswordSessionManager(inner, { + service: account.service, + }) + manager.setFetch(asFetch(fetchMock)) + const agent = new BskyAppAgent(manager) + return {agent, fetchMock} +} + +/** A logged-out agent whose whole network path is the mock fetch. */ +function setupPublic(fetchMock: MockFetch = makeProfileFetch()) { + const manager = new PasswordSessionManager(null, {service: SERVICE}) + manager.setFetch(asFetch(fetchMock)) + return {agent: new BskyAppAgent(manager), fetchMock} +} + +/** The `init` a mock fetch was called with for a given nsid. */ +function initFor(mock: MockFetch, nsid: string): RequestInit | undefined { + const call = mock.mock.calls.find(c => { + const url = c[0] instanceof URL ? c[0].href : String(c[0]) + return url.includes(`/xrpc/${nsid}`) + }) + return call?.[1] +} + +describe('agentToLexClient', () => { + let fetchMock: MockFetch + + beforeEach(() => { + fetchMock = makeProfileFetch() + }) + + it('memoizes one client per agent', () => { + const {agent: agentA} = setup(fetchMock) + const {agent: agentB} = setup(fetchMock) + + const clientA1 = agentToLexClient(agentA) + const clientA2 = agentToLexClient(agentA) + const clientB = agentToLexClient(agentB) + + expect(clientA1).toBeInstanceOf(Client) + expect(clientA1).toBe(clientA2) + expect(clientA1).not.toBe(clientB) + }) + + it('passes through the agent did', () => { + const {agent} = setup(fetchMock) + expect(agentToLexClient(agent).did).toBe(DID) + }) + + it('reflects an undefined did on a logged-out agent', () => { + const {agent} = setupPublic(fetchMock) + expect(agentToLexClient(agent).did).toBeUndefined() + }) + + it('routes client.call through the agent to the network', async () => { + const {agent} = setup(fetchMock) + + const body = await agentToLexClient(agent).call(app.bsky.actor.getProfile, { + actor: HANDLE, + }) + + expect(body.handle).toBe(HANDLE) + const call = fetchMock.mock.calls.find(c => { + const url = c[0] instanceof URL ? c[0].href : String(c[0]) + return url.includes('/xrpc/app.bsky.actor.getProfile') + }) + expect(call).toBeDefined() + const url = call![0] instanceof URL ? call![0].href : String(call![0]) + expect(url).toContain(`actor=${HANDLE}`) + }) + + it('emits the agent proxy header', async () => { + const {agent} = setup(fetchMock) + agent.configureProxy('did:web:api.bsky.app#bsky_appview') + + await agentToLexClient(agent).call(app.bsky.actor.getProfile, { + actor: HANDLE, + }) + + const init = initFor(fetchMock, 'app.bsky.actor.getProfile') + expect(new Headers(init?.headers).get('atproto-proxy')).toBe( + 'did:web:api.bsky.app#bsky_appview', + ) + }) + + it('emits the agent labeler header exactly once', async () => { + const {agent} = setup(fetchMock) + agent.configureLabelersHeader(['did:plc:labeler']) + + await agentToLexClient(agent).call(app.bsky.actor.getProfile, { + actor: HANDLE, + }) + + const init = initFor(fetchMock, 'app.bsky.actor.getProfile') + const labelers = new Headers(init?.headers).get('atproto-accept-labelers') + expect(labelers).toContain('did:plc:labeler') + /* + * The client contributes no labelers of its own, so the agent's single + * entry must not be duplicated. + */ + const entries = labelers! + .split(',') + .filter(l => l.includes('did:plc:labeler')) + expect(entries).toHaveLength(1) + }) + + it('sends the session access token', async () => { + const {agent} = setup(fetchMock) + + await agentToLexClient(agent).call(app.bsky.actor.getProfile, { + actor: HANDLE, + }) + + const init = initFor(fetchMock, 'app.bsky.actor.getProfile') + expect(new Headers(init?.headers).get('authorization')).toBe( + 'Bearer access-jwt', + ) + }) + + it('falls back to unauthenticated requests once the agent is disposed', async () => { + const {agent} = setup(fetchMock) + const client = agentToLexClient(agent) + agent.dispose() + + await client.call(app.bsky.actor.getProfile, {actor: HANDLE}) + + const init = initFor(fetchMock, 'app.bsky.actor.getProfile') + expect(new Headers(init?.headers).has('authorization')).toBe(false) + expect(client.did).toBeUndefined() + }) +}) diff --git a/src/state/session/clients.ts b/src/state/session/clients.ts new file mode 100644 index 0000000000..ff6a597ac7 --- /dev/null +++ b/src/state/session/clients.ts @@ -0,0 +1,71 @@ +import {type Client} from '@atproto/lex' + +import {PUBLIC_BSKY_SERVICE} from '#/lib/constants' +import {createLexClient} from '#/lib/lexClient' +import {type BskyAppAgent} from './bridge-agent' +import {networkAwareFetch} from './network' + +/* + * One client per agent, so that repeated reads for the same agent return the + * same instance. Client identity is observable: a lex `Client` is passed to + * React Query `queryFn`s and read from render paths, so a freshly allocated + * client on every read would break any dependency array or reference + * comparison built on top of it. + * + * Keying on the agent also ties client lifetime to agent lifetime. A disposed + * agent's `fetchHandler` falls back to unauthenticated fetch, and session + * rotation builds a new agent rather than mutating the old one, so a client + * derived from a stale agent becomes unreachable exactly when its agent does. + */ +const lexClients = new WeakMap() + +/** + * The lex {@link Client} for an agent, memoized per agent. + * + * The wrapped handler is `agent.fetchHandler`, NOT + * `agent.sessionManager.fetchHandler`. The agent-level handler is where + * `atproto-proxy` and `atproto-accept-labelers` are set before the request is + * passed down to the session manager, which only adds authorization and PDS + * routing. Because the agent already emits both headers, the client is + * deliberately built with neither a `service` option nor labelers - setting + * either here would emit them a second time. + */ +export function agentToLexClient(agent: BskyAppAgent): Client { + const existing = lexClients.get(agent) + if (existing) { + return existing + } + const client = createLexClient({ + get did() { + return agent.did + }, + fetchHandler: (path, init) => agent.fetchHandler(path, init), + }) + lexClients.set(agent, client) + return client +} + +let publicLexClient: Client | undefined + +/** + * The unauthenticated {@link Client} for public reads, pointed at the public + * appview. + * + * A single module-level instance for the same identity-stability reason as + * {@link agentToLexClient}: there is no session to scope it to, so it lives for + * the lifetime of the process. Requests go through {@link networkAwareFetch} so + * public reads feed the app's reachability signal like authenticated ones do. + * + * Unlike the public agent it parallels, this client sends neither + * `atproto-proxy` nor `atproto-accept-labelers`. `createPublicAgent` configures + * the app labeler and the proxy header, so a logged-out appview *agent* read + * does carry labelers while the same read through this client does not. A + * consumer that needs moderation labels on public reads must configure labelers + * itself before issuing the request. + */ +export function getPublicLexClient(): Client { + return (publicLexClient ??= createLexClient({ + service: PUBLIC_BSKY_SERVICE, + fetch: networkAwareFetch, + })) +} diff --git a/src/state/session/index.tsx b/src/state/session/index.tsx index 1a22a0e215..618804ee7b 100644 --- a/src/state/session/index.tsx +++ b/src/state/session/index.tsx @@ -10,6 +10,7 @@ import { useSyncExternalStore, } from 'react' import {type AtpAgent} from '@atproto/api' +import {type Client} from '@atproto/lex' import {type SessionData} from '@atproto/lex-password-session' import * as persisted from '#/state/persisted' @@ -18,6 +19,7 @@ import {useGlobalDialogsControlContext} from '#/components/dialogs/Context' import {AnalyticsContext, useAnalyticsBase, utils} from '#/analytics' import {IS_WEB} from '#/env' import {emitSessionDropped} from '../events' +import {agentToLexClient, getPublicLexClient} from './clients' import {createSessionBundleAndCreateAccount} from './create-account' import {pickExpiryRescueCandidate} from './expiry-rescue' import {type Action, getInitialState, reducer, type State} from './reducer' @@ -707,3 +709,22 @@ export function useAgent(): AtpAgent { } return bundle.agent } + +/** + * The lex client for the active session, or for the public agent when logged + * out. + */ +export function useLexClient(): Client { + const bundle = useContext(BundleContext) + if (!bundle) { + throw Error('useLexClient() must be below .') + } + return agentToLexClient(bundle.agent) +} + +/** + * The unauthenticated lex client for public reads. + */ +export function usePublicLexClient(): Client { + return getPublicLexClient() +} From e1ba8fe1b055f33026f93a7237a48887b89d12ef Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 13 Aug 2026 22:26:13 +0300 Subject: [PATCH 05/32] [SDK] Add lexicon schema helpers to `#/types/bsky` (#11349) Co-authored-by: Claude Fable 5 --- src/types/bsky/__tests__/index.test.ts | 208 +++++++++++++++++++++++++ src/types/bsky/index.ts | 168 +++++++++++++++++++- 2 files changed, 374 insertions(+), 2 deletions(-) create mode 100644 src/types/bsky/__tests__/index.test.ts diff --git a/src/types/bsky/__tests__/index.test.ts b/src/types/bsky/__tests__/index.test.ts new file mode 100644 index 0000000000..b0b4466b2c --- /dev/null +++ b/src/types/bsky/__tests__/index.test.ts @@ -0,0 +1,208 @@ +import {AppBskyFeedPost} from '@atproto/api' + +import {app} from '#/lexicons' +import * as bsky from '#/types/bsky' + +const now = () => new Date().toISOString() + +/** + * A structurally valid `app.bsky.feed.post` record. + */ +const validPost = { + $type: 'app.bsky.feed.post', + text: 'hello world', + createdAt: now(), +} + +/** + * Right `$type`, but the body is invalid (`text` is not a string, `createdAt` + * is not a datetime). Passes a `$type`-only guard, fails full validation. + */ +const invalidPost = { + $type: 'app.bsky.feed.post', + text: 123, + createdAt: 'not-a-datetime', +} + +/** + * A different record type entirely - should fail even the fast guard. + */ +const wrongType = { + $type: 'app.bsky.feed.like', + subject: {uri: 'at://x', cid: 'y'}, + createdAt: now(), +} + +describe('types/bsky lexicon schema helpers (#/lexicons)', () => { + describe('isType (fast, $type-only)', () => { + it('accepts a valid record', () => { + expect(bsky.isType(app.bsky.feed.post, validPost)).toBe(true) + }) + + it('rejects a $type mismatch', () => { + expect(bsky.isType(app.bsky.feed.post, wrongType)).toBe(false) + }) + + it('accepts an invalid body that has the right $type (dangerous semantics)', () => { + /* + * Mirrors `dangerousIsType`: only the `$type` is checked, so a + * structurally invalid record still passes. + */ + expect(bsky.isType(app.bsky.feed.post, invalidPost)).toBe(true) + }) + + it('rejects a record with no $type at all', () => { + expect( + bsky.isType(app.bsky.feed.post, {text: 'hi', createdAt: now()}), + ).toBe(false) + }) + + it('accepts a schema passed as its namespace module or its main schema', () => { + expect(bsky.isType(app.bsky.feed.post, validPost)).toBe(true) + expect(bsky.isType(app.bsky.feed.post.main, validPost)).toBe(true) + }) + + it('returns false (does not throw) for null, undefined and non-objects', () => { + /* + * Mirrors the `dangerousIsType`/`is$typed` behavior - call sites pass + * e.g. `post.record` which may be undefined. + */ + expect(bsky.isType(app.bsky.feed.post, null)).toBe(false) + expect(bsky.isType(app.bsky.feed.post, undefined)).toBe(false) + expect(bsky.isType(app.bsky.feed.post, 'string')).toBe(false) + }) + + it('requires a present $type for typed-object def schemas', () => { + /* + * The generated TypedObjectSchema.isTypeOf treats a missing $type as a + * match (maybe-typed semantics). Our helper must NOT: when + * discriminating unions by $type, an object without $type would + * otherwise satisfy every branch. + */ + expect(bsky.isType(app.bsky.feed.defs.postView, {foo: 1})).toBe(false) + expect(bsky.isType(app.bsky.feed.defs.postView, {})).toBe(false) + expect( + bsky.isType(app.bsky.feed.defs.postView, { + $type: 'app.bsky.feed.defs#postView', + }), + ).toBe(true) + }) + + it('rejects what the schema own isTypeOf would accept or throw on', () => { + /* + * Pins the upstream behavior that justifies the hand-rolled check in + * `isType`. If either of these expectations starts failing, upstream has + * changed and `isType` can consider delegating to `isTypeOf`. + */ + expect(app.bsky.feed.defs.postView.isTypeOf({})).toBe(true) + expect(() => + app.bsky.feed.post.main.isTypeOf( + null as unknown as {$type?: 'app.bsky.feed.post'}, + ), + ).toThrow() + }) + }) + + describe('matches (full validation guard)', () => { + it('accepts a valid record', () => { + expect(bsky.matches(app.bsky.feed.post, validPost)).toBe(true) + }) + + it('accepts a schema passed as its namespace module or its main schema', () => { + expect(bsky.matches(app.bsky.feed.post, validPost)).toBe(true) + expect(bsky.matches(app.bsky.feed.post.main, validPost)).toBe(true) + }) + + it('rejects a $type mismatch', () => { + expect(bsky.matches(app.bsky.feed.post, wrongType)).toBe(false) + }) + + it('rejects an invalid body even though the $type matches', () => { + expect(bsky.matches(app.bsky.feed.post, invalidPost)).toBe(false) + }) + + it('rejects null and undefined', () => { + expect(bsky.matches(app.bsky.feed.post, null)).toBe(false) + expect(bsky.matches(app.bsky.feed.post, undefined)).toBe(false) + }) + }) + + describe('safeParse (full validation, no throw)', () => { + it('succeeds and returns the value for a valid record', () => { + const result = bsky.safeParse(app.bsky.feed.post, validPost) + expect(result.success).toBe(true) + if (result.success) { + expect(result.value.text).toBe('hello world') + } + }) + + it('fails with a reason for an invalid record', () => { + const result = bsky.safeParse(app.bsky.feed.post, invalidPost) + expect(result.success).toBe(false) + if (!result.success) { + expect(result.reason).toBeDefined() + } + }) + + it('accepts a schema passed as its main schema', () => { + const result = bsky.safeParse(app.bsky.feed.post.main, validPost) + expect(result.success).toBe(true) + }) + }) + + describe('parse (full validation, throws)', () => { + it('returns the typed value for a valid record', () => { + const record = bsky.parse(app.bsky.feed.post, validPost) + expect(record.text).toBe('hello world') + }) + + it('throws for an invalid record', () => { + expect(() => bsky.parse(app.bsky.feed.post, invalidPost)).toThrow() + }) + + it('accepts a schema passed as its main schema', () => { + expect(bsky.parse(app.bsky.feed.post.main, validPost).text).toBe( + 'hello world', + ) + }) + }) +}) + +describe('types/bsky validator helpers (@atproto/api)', () => { + it('dangerousIsType accepts the right $type without validating the body', () => { + expect( + bsky.dangerousIsType( + validPost, + AppBskyFeedPost.isRecord, + ), + ).toBe(true) + // Right $type, invalid body - still passes the fast guard. + expect( + bsky.dangerousIsType( + invalidPost, + AppBskyFeedPost.isRecord, + ), + ).toBe(true) + expect( + bsky.dangerousIsType( + wrongType, + AppBskyFeedPost.isRecord, + ), + ).toBe(false) + }) + + it('validate fully validates the body', () => { + expect( + bsky.validate( + validPost, + AppBskyFeedPost.validateRecord, + ), + ).toBe(true) + expect( + bsky.validate( + invalidPost, + AppBskyFeedPost.validateRecord, + ), + ).toBe(false) + }) +}) diff --git a/src/types/bsky/index.ts b/src/types/bsky/index.ts index d482f9515b..1d498bc130 100644 --- a/src/types/bsky/index.ts +++ b/src/types/bsky/index.ts @@ -1,4 +1,16 @@ -import {type ValidationResult} from '@atproto/lexicon' +import { + getMain, + type InferInput, + type InferOutput, + type Main, + type ParseOptions, + type RecordSchema, + type Schema, + type TypedObjectSchema, + type ValidateOptions, + type ValidationResult, +} from '@atproto/lex' +import {type ValidationResult as LegacyValidationResult} from '@atproto/lexicon' export * as post from '#/types/bsky/post' export * as profile from '#/types/bsky/profile' @@ -45,7 +57,159 @@ export function dangerousIsType( */ export function validate( record: unknown, - validator: (v: unknown) => ValidationResult, + validator: (v: unknown) => LegacyValidationResult, ): record is R { return validator(record).success } + +/* + * Two families of helpers live in this file, distinguished by the validation + * surface they consume. + * + * `dangerousIsType` and `validate` (above) operate on the standalone + * `is*`/`validate*` functions exported by '@atproto/api' - the record and the + * validator are passed separately, e.g. + * `dangerousIsType(v, AppBskyFeedPost.isRecord)`. + * + * `isType`, `matches`, `parse`, and `safeParse` (below) operate on the + * generated lexicon schema objects from '#/lexicons'. That codegen attaches + * the validation surface directly to each schema, so these helpers take the + * schema object itself: + * + * ```ts + * import {app} from '#/lexicons' + * import * as bsky from '#/types/bsky' + * + * bsky.isType(app.bsky.feed.post, v) + * bsky.matches(app.bsky.feed.post, v) + * ``` + * + * Every generated namespace module (e.g. `app.bsky.feed.post`) re-exports its + * `main` schema, and bare defs (e.g. `app.bsky.feed.defs.postView`) are schema + * objects directly. Both forms are accepted here - `getMain` unwraps a module + * to its `main` schema and passes a bare schema through unchanged - so call + * sites can pass whichever is in scope. + */ + +/** + * A generated lexicon schema that carries a `$type` and therefore supports the + * fast, `$type`-only {@link isType} check: a record schema (`app.bsky.feed.post`) + * or a typed-object def schema (`app.bsky.feed.defs.postView`). + */ +type TypedSchema = RecordSchema | TypedObjectSchema + +/** + * Fast type checking without full schema validation, for use with data we + * trust, or for non-critical path use cases. This only compares the `$type` + * string; it does NOT assert the rest of the object matches the schema. An + * invalid record with the right `$type` will pass. + * + * This is the '#/lexicons' equivalent of {@link dangerousIsType}. For full + * validation of the object schema, use {@link matches}, {@link parse}, or + * {@link safeParse} from this same file. + * + * Usage: + * ```ts + * import {app} from '#/lexicons' + * import * as bsky from '#/types/bsky' + * + * if (bsky.isType(app.bsky.feed.post, item)) { + * // `item` is narrowed to the post record type here + * } + * ``` + */ +export function isType( + schema: Main, + value: unknown, +): value is InferInput { + /* + * Deliberately NOT delegating to the schema's `isTypeOf`: the generated + * `TypedObjectSchema.isTypeOf` treats a MISSING `$type` as a match + * (maybe-typed semantics), which would let any plain object satisfy any + * def-schema check and break `$type`-discriminated unions. The old + * `dangerousIsType` required a present, matching `$type`, and so do we. + * The nullish/object guard also mirrors the old `is$typed` behavior of + * returning false (not throwing) for null/undefined input. + */ + return ( + value != null && + typeof value === 'object' && + (value as {$type?: unknown}).$type === getMain(schema).$type + ) +} + +/** + * Fully validates the object against the schema (strict, no coercion), which + * has a performance cost, and narrows the value on success. + * + * This is the '#/lexicons' equivalent of {@link validate}. For faster checks + * with data we trust, like that from our app view, use {@link isType} from this + * same file. + * + * Usage: + * ```ts + * import {app} from '#/lexicons' + * import * as bsky from '#/types/bsky' + * + * if (bsky.matches(app.bsky.feed.post, item)) { + * // `item` is narrowed to the post record type here + * } + * ``` + */ +export function matches( + schema: Main, + value: unknown, + options?: ValidateOptions, +): value is InferInput { + return getMain(schema).matches(value, options) +} + +/** + * Fully validates and parses the value against the schema, returning the typed + * value or throwing a `LexValidationError` on failure. Parsing may apply + * schema transformations such as default values. + * + * Prefer {@link safeParse} where you want to branch on failure without a + * try/catch, or {@link matches} where you only need a type guard. + * + * Usage: + * ```ts + * import {app} from '#/lexicons' + * import * as bsky from '#/types/bsky' + * + * const record = bsky.parse(app.bsky.feed.post, item) // typed, or throws + * ``` + */ +export function parse( + schema: Main, + value: unknown, + options?: ParseOptions, +): InferOutput { + return getMain(schema).parse(value, options) +} + +/** + * Fully validates and parses the value against the schema, returning a + * discriminated `ValidationResult` instead of throwing. Parsing may apply + * schema transformations such as default values. + * + * Usage: + * ```ts + * import {app} from '#/lexicons' + * import * as bsky from '#/types/bsky' + * + * const result = bsky.safeParse(app.bsky.feed.post, item) + * if (result.success) { + * // `result.value` is the typed post record + * } else { + * // `result.reason` is the LexValidationError + * } + * ``` + */ +export function safeParse( + schema: Main, + value: unknown, + options?: ParseOptions, +): ValidationResult> { + return getMain(schema).safeParse(value, options) +} From c52e730112eaf3917687e31e963ef65951a7f7db Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 13 Aug 2026 22:26:14 +0300 Subject: [PATCH 06/32] [SDK] Add pds and chat clients and the canonical client hooks (#11350) Co-authored-by: Claude Fable 5 --- src/lib/constants.ts | 16 ++ .../queries/trending/useGetTrendsQuery.ts | 4 +- src/state/session/__tests__/clients-test.ts | 196 ++++++++++++++++-- .../__tests__/provider-clients-test.tsx | 193 +++++++++++++++++ src/state/session/clients.ts | 122 +++++++++-- src/state/session/index.tsx | 74 ++++++- 6 files changed, 565 insertions(+), 40 deletions(-) create mode 100644 src/state/session/__tests__/provider-clients-test.tsx diff --git a/src/lib/constants.ts b/src/lib/constants.ts index bc079c7af5..1a89fa5b5e 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -1,5 +1,6 @@ import {type Insets, Platform} from 'react-native' import {type AppBskyActorDefs, BSKY_LABELER_DID} from '@atproto/api' +import {type Service} from '@atproto/lex' import {type ProxyHeaderValue} from '#/state/session/agent' import {BLUESKY_PROXY_DID, CHAT_PROXY_DID, IS_DEV} from '#/env' @@ -247,6 +248,21 @@ export const BLUESKY_PROXY_HEADER = { }, } +/** + * The chat service's proxy target, in the `did#service_id` form a lex client's + * `service` option takes. A client constructed with it emits `atproto-proxy: + * ` on every request, which is what routes `chat.bsky.*` calls to + * the chat service. + * + * The DID comes from the env-configurable `CHAT_PROXY_DID` (via + * `EXPO_PUBLIC_CHAT_PROXY_DID`) rather than a hard-coded constant, so the + * target can be retargeted per environment. + * + * This is the client-level equivalent of {@link DM_SERVICE_HEADERS}, which + * carries the same value as a per-call header. + */ +export const CHAT_PROXY_SERVICE: Service = `${CHAT_PROXY_DID}#bsky_chat` + export const DM_SERVICE_HEADERS = { 'atproto-proxy': `${CHAT_PROXY_DID}#bsky_chat`, } diff --git a/src/state/queries/trending/useGetTrendsQuery.ts b/src/state/queries/trending/useGetTrendsQuery.ts index e7d6806b3e..8fe0ad845e 100644 --- a/src/state/queries/trending/useGetTrendsQuery.ts +++ b/src/state/queries/trending/useGetTrendsQuery.ts @@ -10,7 +10,7 @@ import {logger} from '#/logger' import {getContentLanguages} from '#/state/preferences/languages' import {STALE} from '#/state/queries' import {usePreferencesQuery} from '#/state/queries/preferences' -import {useLexClient} from '#/state/session' +import {useAppviewClient} from '#/state/session' import {app} from '#/lexicons' export const DEFAULT_LIMIT = 5 @@ -33,7 +33,7 @@ export const createGetTrendsQueryKey = (limit?: number) => limit === undefined ? ['trends'] : ['trends', {limit}] export function useGetTrendsQuery(props: QueryProps = {}) { - const client = useLexClient() + const client = useAppviewClient() const {data: preferences} = usePreferencesQuery() const limit = props.limit ?? DEFAULT_LIMIT const mutedWords = useMemo(() => { diff --git a/src/state/session/__tests__/clients-test.ts b/src/state/session/__tests__/clients-test.ts index f90841dbc8..452de11d9d 100644 --- a/src/state/session/__tests__/clients-test.ts +++ b/src/state/session/__tests__/clients-test.ts @@ -13,9 +13,16 @@ jest.mock('jwt-decode', () => ({ }, })) -import {app} from '#/lexicons' +import {CHAT_PROXY_SERVICE} from '#/lib/constants' +import {app, chat, com} from '#/lexicons' import {BskyAppAgent, PasswordSessionManager} from '../bridge-agent' -import {agentToLexClient} from '../clients' +import { + agentToAppviewClient, + agentToChatClient, + agentToPdsClient, + getUnauthenticatedThrowingClient, + NotAuthenticatedError, +} from '../clients' import {sessionAccountToSessionData} from '../session-data' import { asFetch, @@ -26,6 +33,7 @@ import { makeMockFetch, type MockFetch, SERVICE, + urlsOf, } from './mock-fetch' const PROFILE_BODY = { @@ -70,7 +78,7 @@ function initFor(mock: MockFetch, nsid: string): RequestInit | undefined { return call?.[1] } -describe('agentToLexClient', () => { +describe('agentToAppviewClient', () => { let fetchMock: MockFetch beforeEach(() => { @@ -81,9 +89,9 @@ describe('agentToLexClient', () => { const {agent: agentA} = setup(fetchMock) const {agent: agentB} = setup(fetchMock) - const clientA1 = agentToLexClient(agentA) - const clientA2 = agentToLexClient(agentA) - const clientB = agentToLexClient(agentB) + const clientA1 = agentToAppviewClient(agentA) + const clientA2 = agentToAppviewClient(agentA) + const clientB = agentToAppviewClient(agentB) expect(clientA1).toBeInstanceOf(Client) expect(clientA1).toBe(clientA2) @@ -92,20 +100,23 @@ describe('agentToLexClient', () => { it('passes through the agent did', () => { const {agent} = setup(fetchMock) - expect(agentToLexClient(agent).did).toBe(DID) + expect(agentToAppviewClient(agent).did).toBe(DID) }) it('reflects an undefined did on a logged-out agent', () => { const {agent} = setupPublic(fetchMock) - expect(agentToLexClient(agent).did).toBeUndefined() + expect(agentToAppviewClient(agent).did).toBeUndefined() }) it('routes client.call through the agent to the network', async () => { const {agent} = setup(fetchMock) - const body = await agentToLexClient(agent).call(app.bsky.actor.getProfile, { - actor: HANDLE, - }) + const body = await agentToAppviewClient(agent).call( + app.bsky.actor.getProfile, + { + actor: HANDLE, + }, + ) expect(body.handle).toBe(HANDLE) const call = fetchMock.mock.calls.find(c => { @@ -121,7 +132,7 @@ describe('agentToLexClient', () => { const {agent} = setup(fetchMock) agent.configureProxy('did:web:api.bsky.app#bsky_appview') - await agentToLexClient(agent).call(app.bsky.actor.getProfile, { + await agentToAppviewClient(agent).call(app.bsky.actor.getProfile, { actor: HANDLE, }) @@ -135,7 +146,7 @@ describe('agentToLexClient', () => { const {agent} = setup(fetchMock) agent.configureLabelersHeader(['did:plc:labeler']) - await agentToLexClient(agent).call(app.bsky.actor.getProfile, { + await agentToAppviewClient(agent).call(app.bsky.actor.getProfile, { actor: HANDLE, }) @@ -155,7 +166,7 @@ describe('agentToLexClient', () => { it('sends the session access token', async () => { const {agent} = setup(fetchMock) - await agentToLexClient(agent).call(app.bsky.actor.getProfile, { + await agentToAppviewClient(agent).call(app.bsky.actor.getProfile, { actor: HANDLE, }) @@ -167,7 +178,7 @@ describe('agentToLexClient', () => { it('falls back to unauthenticated requests once the agent is disposed', async () => { const {agent} = setup(fetchMock) - const client = agentToLexClient(agent) + const client = agentToAppviewClient(agent) agent.dispose() await client.call(app.bsky.actor.getProfile, {actor: HANDLE}) @@ -177,3 +188,158 @@ describe('agentToLexClient', () => { expect(client.did).toBeUndefined() }) }) + +describe('agentToPdsClient', () => { + let fetchMock: MockFetch + + beforeEach(() => { + fetchMock = makeProfileFetch() + }) + + it('memoizes one client per agent', () => { + const {agent: agentA} = setup(fetchMock) + const {agent: agentB} = setup(fetchMock) + + const clientA1 = agentToPdsClient(agentA) + const clientA2 = agentToPdsClient(agentA) + + expect(clientA1).toBeInstanceOf(Client) + expect(clientA1).toBe(clientA2) + expect(clientA1).not.toBe(agentToPdsClient(agentB)) + }) + + it('is a distinct client from the appview client for the same agent', () => { + const {agent} = setup(fetchMock) + expect(agentToPdsClient(agent)).not.toBe(agentToAppviewClient(agent)) + }) + + it('passes through the agent did', () => { + const {agent} = setup(fetchMock) + expect(agentToPdsClient(agent).did).toBe(DID) + }) + + it('sends the session access token', async () => { + const {agent} = setup(fetchMock) + + await agentToPdsClient(agent).call(com.atproto.server.getSession, {}) + + const init = initFor(fetchMock, 'com.atproto.server.getSession') + expect(new Headers(init?.headers).get('authorization')).toBe( + 'Bearer access-jwt', + ) + }) + + it('emits neither the proxy nor the labeler header the agent is configured with', async () => { + /* + * The load-bearing difference from the appview client: this client wraps the + * session manager, below the agent layer that sets both headers, so a + * request reaches the account's PDS instead of being proxied onward. + */ + const {agent} = setup(fetchMock) + agent.configureProxy('did:web:api.bsky.app#bsky_appview') + agent.configureLabelersHeader(['did:plc:labeler']) + + await agentToPdsClient(agent).call(com.atproto.server.getSession, {}) + + const headers = new Headers( + initFor(fetchMock, 'com.atproto.server.getSession')?.headers, + ) + expect(headers.get('atproto-proxy')).toBeNull() + expect(headers.get('atproto-accept-labelers')).toBeNull() + }) + + it('resolves the relative xrpc path against the account host', async () => { + /* + * lex-client hands its fetchHandler an origin-less `/xrpc/` path; the + * session manager absolutizes it against dispatchUrl. + */ + const {agent} = setup(fetchMock) + + await agentToPdsClient(agent).call(com.atproto.server.getSession, {}) + + expect(urlsOf(fetchMock)).toContain( + `${SERVICE}/xrpc/com.atproto.server.getSession`, + ) + }) +}) + +describe('agentToChatClient', () => { + let fetchMock: MockFetch + + beforeEach(() => { + fetchMock = makeProfileFetch() + }) + + it('memoizes one client per agent, distinct from the pds client', () => { + const {agent} = setup(fetchMock) + + const client = agentToChatClient(agent) + + expect(client).toBeInstanceOf(Client) + expect(client).toBe(agentToChatClient(agent)) + expect(client).not.toBe(agentToPdsClient(agent)) + }) + + it('emits the chat proxy header exactly once, with the session token', async () => { + const {agent} = setup(fetchMock) + + /* the stub body fails listConvos output validation; headers are recorded pre-parse */ + await agentToChatClient(agent) + .call(chat.bsky.convo.listConvos, {}) + .catch(() => {}) + + const headers = new Headers( + initFor(fetchMock, 'chat.bsky.convo.listConvos')?.headers, + ) + /* + * An exact match, not `toContain`: `Headers` comma-joins repeated entries + * for the same name, so a second contributor would show up here. + */ + expect(headers.get('atproto-proxy')).toBe(CHAT_PROXY_SERVICE) + expect(headers.get('authorization')).toBe('Bearer access-jwt') + }) + + it('does not emit the agent labeler header', async () => { + const {agent} = setup(fetchMock) + agent.configureLabelersHeader(['did:plc:labeler']) + + await agentToChatClient(agent) + .call(chat.bsky.convo.listConvos, {}) + .catch(() => {}) + + const headers = new Headers( + initFor(fetchMock, 'chat.bsky.convo.listConvos')?.headers, + ) + expect(headers.get('atproto-accept-labelers')).toBeNull() + }) +}) + +describe('getUnauthenticatedThrowingClient', () => { + it('is a stable singleton with no did', () => { + const client = getUnauthenticatedThrowingClient() + + expect(client.did).toBeUndefined() + /* identity is stable so it is safe in React Query keys */ + expect(getUnauthenticatedThrowingClient()).toBe(client) + }) + + it('rejects any call with NotAuthenticatedError as the cause, with no fetch', async () => { + /* + * The throwing fetchHandler fires before any network I/O. lex-client wraps a + * fetchHandler throw in an internal error whose `cause` is the original, so + * the NotAuthenticatedError surfaces there. + */ + const fetchMock = makeProfileFetch() + const err = await getUnauthenticatedThrowingClient() + .call(com.atproto.server.getSession, {}) + .then(() => undefined) + .catch((e: unknown) => e) + + expect((err as Error).cause).toBeInstanceOf(NotAuthenticatedError) + expect(((err as Error).cause as Error).name).toBe('NotAuthenticatedError') + expect(((err as Error).cause as Error).message).toBe( + 'Not authenticated: this operation requires an active session', + ) + expect(fetchMock).not.toHaveBeenCalled() + }) +}) diff --git a/src/state/session/__tests__/provider-clients-test.tsx b/src/state/session/__tests__/provider-clients-test.tsx new file mode 100644 index 0000000000..58ea9ad6fe --- /dev/null +++ b/src/state/session/__tests__/provider-clients-test.tsx @@ -0,0 +1,193 @@ +import {type Client} from '@atproto/lex' +import {PasswordSession} from '@atproto/lex-password-session' +import {beforeEach, describe, expect, it, jest} from '@jest/globals' +import {act, render} from '@testing-library/react-native' + +import {type SessionAccount} from '../types' + +/* + * The provider pulls the whole app shell in through `#/state/util` and the + * account factories. These mocks cut the tree back to the session lifecycle + * itself, mirroring provider-abort-test.tsx. + */ +jest.mock('#/state/persisted', () => { + const { + defaults, + }: typeof import('#/state/persisted/schema') = require('#/state/persisted/schema') + return { + defaults, + get: (key: keyof typeof defaults) => defaults[key], + write: () => Promise.resolve(), + readLatest: (key: keyof typeof defaults) => defaults[key], + onUpdate: () => () => {}, + } +}) +jest.mock('#/state/util', () => ({useCloseAllActiveElements: () => () => {}})) +jest.mock('#/components/dialogs/Context', () => ({ + useGlobalDialogsControlContext: () => ({signinDialogControl: {open() {}}}), +})) +jest.mock('#/analytics', () => ({ + AnalyticsContext: ({children}: {children: React.ReactNode}) => children, + useAnalyticsBase: () => ({metric() {}, logger: {debug() {}, error() {}}}), + utils: {accountToSessionMetadata: () => ({}), useMeta: () => undefined}, +})) +jest.mock('#/state/shell/onboarding', () => ({ + useOnboardingDispatch: () => () => {}, +})) +jest.mock('#/ageAssurance/data', () => ({ + clearAgeAssuranceServerDataForAll: () => {}, + clearAgeAssuranceServerDataForDid: () => {}, +})) +jest.mock('#/lib/persisted-query-storage', () => ({ + clearPersistedQueryStorage: () => Promise.resolve(), +})) +jest.mock('#/lib/notifications/notifications', () => ({ + unregisterPushToken: () => Promise.resolve(), +})) +jest.mock('jwt-decode', () => ({ + jwtDecode: () => ({scope: 'com.atproto.access'}), +})) +jest.mock('#/state/events', () => ({ + emitSessionDropped: () => {}, + emitNetworkConfirmed: () => {}, + emitNetworkLost: () => {}, +})) + +const mockLogin = jest.fn<(...args: unknown[]) => Promise>() +jest.mock('../session-core', () => ({ + ...jest.requireActual('../session-core'), + createSessionBundleAndLogin: (...args: unknown[]) => mockLogin(...args), +})) +jest.mock('../create-account', () => ({ + createSessionBundleAndCreateAccount: () => new Promise(() => {}), +})) + +import { + Provider, + useAppviewClient, + useChatClient, + useMaybeChatClient, + useMaybePdsClient, + usePdsClient, + useSessionApi, +} from '#/state/session' +import {type SessionApiContext} from '#/state/session/types' +import {BskyAppAgent, PasswordSessionManager} from '../bridge-agent' +import { + agentToAppviewClient, + agentToChatClient, + agentToPdsClient, + getUnauthenticatedThrowingClient, +} from '../clients' +import {type SessionBundle} from '../session-core' +import {sessionAccountToSessionData} from '../session-data' +import {asFetch, makeAccount, makeMockFetch} from './mock-fetch' + +type Clients = { + appview: Client + pds: Client + chat: Client + maybePds: Client | null + maybeChat: Client | null +} + +/** + * Build a bundle whose agent is a real `BskyAppAgent` over a real + * `PasswordSession`, since the client builders derive from the agent. Only the + * fields the provider reads are populated. + */ +function makeBundle(account: SessionAccount): SessionBundle { + const fetchMock = makeMockFetch() + const session = new PasswordSession(sessionAccountToSessionData(account), { + fetch: asFetch(fetchMock), + }) + const manager = new PasswordSessionManager(session, { + service: account.service, + }) + manager.setFetch(asFetch(fetchMock)) + return { + session, + agent: new BskyAppAgent(manager), + service: new URL(account.service), + } +} + +/** Render the provider and hand back the client hooks' current values. */ +function renderClients(): {api: SessionApiContext; clients: () => Clients} { + let api!: SessionApiContext + let clients!: Clients + function Probe() { + api = useSessionApi() + clients = { + appview: useAppviewClient(), + pds: usePdsClient(), + chat: useChatClient(), + maybePds: useMaybePdsClient(), + maybeChat: useMaybeChatClient(), + } + return null + } + render( + + + , + ) + return {api, clients: () => clients} +} + +beforeEach(() => { + mockLogin.mockReset() +}) + +describe('client hooks while logged out', () => { + it('serves the public agent for appview reads', () => { + const {clients} = renderClients() + expect(clients().appview).toBeDefined() + /* the logged-out bundle's agent IS the public agent, so no separate branch */ + expect(clients().appview.did).toBeUndefined() + }) + + it('serves the throwing client for the write surfaces', () => { + const {clients} = renderClients() + const throwing = getUnauthenticatedThrowingClient() + expect(clients().pds).toBe(throwing) + expect(clients().chat).toBe(throwing) + }) + + it('serves null from the maybe variants', () => { + const {clients} = renderClients() + expect(clients().maybePds).toBeNull() + expect(clients().maybeChat).toBeNull() + }) +}) + +describe('client hooks with a session', () => { + it('derives every surface from the session bundle agent', async () => { + const account = makeAccount() + const bundle = makeBundle(account) + const {api, clients} = renderClients() + + mockLogin.mockResolvedValueOnce({bundle, account}) + await act(async () => { + await api.login({} as never, 'LoginForm') + }) + + expect(clients().appview).toBe(agentToAppviewClient(bundle.agent)) + expect(clients().pds).toBe(agentToPdsClient(bundle.agent)) + expect(clients().chat).toBe(agentToChatClient(bundle.agent)) + }) + + it('serves the same clients from the maybe variants', async () => { + const account = makeAccount() + const bundle = makeBundle(account) + const {api, clients} = renderClients() + + mockLogin.mockResolvedValueOnce({bundle, account}) + await act(async () => { + await api.login({} as never, 'LoginForm') + }) + + expect(clients().maybePds).toBe(clients().pds) + expect(clients().maybeChat).toBe(clients().chat) + }) +}) diff --git a/src/state/session/clients.ts b/src/state/session/clients.ts index ff6a597ac7..4daf5368b3 100644 --- a/src/state/session/clients.ts +++ b/src/state/session/clients.ts @@ -1,15 +1,15 @@ import {type Client} from '@atproto/lex' -import {PUBLIC_BSKY_SERVICE} from '#/lib/constants' +import {CHAT_PROXY_SERVICE, PUBLIC_BSKY_SERVICE} from '#/lib/constants' import {createLexClient} from '#/lib/lexClient' import {type BskyAppAgent} from './bridge-agent' import {networkAwareFetch} from './network' /* - * One client per agent, so that repeated reads for the same agent return the - * same instance. Client identity is observable: a lex `Client` is passed to - * React Query `queryFn`s and read from render paths, so a freshly allocated - * client on every read would break any dependency array or reference + * One client per agent, per surface, so that repeated reads for the same agent + * return the same instance. Client identity is observable: a lex `Client` is + * passed to React Query `queryFn`s and read from render paths, so a freshly + * allocated client on every read would break any dependency array or reference * comparison built on top of it. * * Keying on the agent also ties client lifetime to agent lifetime. A disposed @@ -17,10 +17,12 @@ import {networkAwareFetch} from './network' * rotation builds a new agent rather than mutating the old one, so a client * derived from a stale agent becomes unreachable exactly when its agent does. */ -const lexClients = new WeakMap() +const appviewClients = new WeakMap() +const pdsClients = new WeakMap() +const chatClients = new WeakMap() /** - * The lex {@link Client} for an agent, memoized per agent. + * The appview {@link Client} for an agent, memoized per agent. * * The wrapped handler is `agent.fetchHandler`, NOT * `agent.sessionManager.fetchHandler`. The agent-level handler is where @@ -30,8 +32,8 @@ const lexClients = new WeakMap() * deliberately built with neither a `service` option nor labelers - setting * either here would emit them a second time. */ -export function agentToLexClient(agent: BskyAppAgent): Client { - const existing = lexClients.get(agent) +export function agentToAppviewClient(agent: BskyAppAgent): Client { + const existing = appviewClients.get(agent) if (existing) { return existing } @@ -41,10 +43,101 @@ export function agentToLexClient(agent: BskyAppAgent): Client { }, fetchHandler: (path, init) => agent.fetchHandler(path, init), }) - lexClients.set(agent, client) + appviewClients.set(agent, client) return client } +/** + * The account-host {@link Client} for an agent, memoized per agent. + * + * This wraps `agent.sessionManager.fetchHandler`, one layer below + * {@link agentToAppviewClient}. That layer does authorization and refresh-on-401 + * and resolves the request against `dispatchUrl` (the account's PDS), but it + * does NOT set `atproto-proxy` or `atproto-accept-labelers`, so requests reach + * the PDS itself rather than being proxied onward. That is the right transport + * for `com.atproto.*` repo/server/identity calls. + * + * No `service` option for the same reason: adding one would reintroduce the + * proxy header this client exists to avoid. + * + * The handler is wrapped in a closure rather than passed by reference because + * `PasswordSessionManager.fetchHandler` reads `this`. Relative paths are + * intentional: lex-client hands its handler an origin-less + * `/xrpc/[?query]` path, which the session manager absolutizes against + * `dispatchUrl`. + */ +export function agentToPdsClient(agent: BskyAppAgent): Client { + const existing = pdsClients.get(agent) + if (existing) { + return existing + } + const client = createLexClient({ + get did() { + return agent.did + }, + fetchHandler: (path, init) => agent.sessionManager.fetchHandler(path, init), + }) + pdsClients.set(agent, client) + return client +} + +/** + * The chat {@link Client} for an agent, memoized per agent. + * + * Same session-manager transport as {@link agentToPdsClient} - authorization + * and PDS routing, no agent-level proxy or labeler headers - but constructed + * with {@link CHAT_PROXY_SERVICE} as its `service`, so lex-client emits + * `atproto-proxy: ` on every request and `chat.bsky.*` + * calls are proxied to the chat service. + */ +export function agentToChatClient(agent: BskyAppAgent): Client { + const existing = chatClients.get(agent) + if (existing) { + return existing + } + const client = createLexClient( + { + get did() { + return agent.did + }, + fetchHandler: (path, init) => + agent.sessionManager.fetchHandler(path, init), + }, + {service: CHAT_PROXY_SERVICE}, + ) + chatClients.set(agent, client) + return client +} + +/** Thrown when a write/auth-only client is used with no active session. */ +export class NotAuthenticatedError extends Error { + constructor(op = 'this operation') { + super(`Not authenticated: ${op} requires an active session`) + this.name = 'NotAuthenticatedError' + } +} + +let unauthedClient: Client | undefined + +/** + * A {@link Client} that throws {@link NotAuthenticatedError} on any request, + * before any network I/O. It is the logged-out value of the write/auth-only + * hooks (`usePdsClient`/`useChatClient`) so an unauthenticated call fails + * immediately and legibly instead of silently hitting public infrastructure, + * which would answer with an opaque 4xx. + * + * A single module-level instance, so its identity is stable across renders - + * safe to use in React Query keys and as a hook return value. + */ +export function getUnauthenticatedThrowingClient(): Client { + return (unauthedClient ??= createLexClient({ + did: undefined, + fetchHandler: () => { + throw new NotAuthenticatedError() + }, + })) +} + let publicLexClient: Client | undefined /** @@ -52,9 +145,10 @@ let publicLexClient: Client | undefined * appview. * * A single module-level instance for the same identity-stability reason as - * {@link agentToLexClient}: there is no session to scope it to, so it lives for - * the lifetime of the process. Requests go through {@link networkAwareFetch} so - * public reads feed the app's reachability signal like authenticated ones do. + * {@link agentToAppviewClient}: there is no session to scope it to, so it lives + * for the lifetime of the process. Requests go through + * {@link networkAwareFetch} so public reads feed the app's reachability signal + * like authenticated ones do. * * Unlike the public agent it parallels, this client sends neither * `atproto-proxy` nor `atproto-accept-labelers`. `createPublicAgent` configures @@ -63,7 +157,7 @@ let publicLexClient: Client | undefined * consumer that needs moderation labels on public reads must configure labelers * itself before issuing the request. */ -export function getPublicLexClient(): Client { +export function getPublicAppviewClient(): Client { return (publicLexClient ??= createLexClient({ service: PUBLIC_BSKY_SERVICE, fetch: networkAwareFetch, diff --git a/src/state/session/index.tsx b/src/state/session/index.tsx index 618804ee7b..01bca93c36 100644 --- a/src/state/session/index.tsx +++ b/src/state/session/index.tsx @@ -19,7 +19,13 @@ import {useGlobalDialogsControlContext} from '#/components/dialogs/Context' import {AnalyticsContext, useAnalyticsBase, utils} from '#/analytics' import {IS_WEB} from '#/env' import {emitSessionDropped} from '../events' -import {agentToLexClient, getPublicLexClient} from './clients' +import { + agentToAppviewClient, + agentToChatClient, + agentToPdsClient, + getPublicAppviewClient, + getUnauthenticatedThrowingClient, +} from './clients' import {createSessionBundleAndCreateAccount} from './create-account' import {pickExpiryRescueCandidate} from './expiry-rescue' import {type Action, getInitialState, reducer, type State} from './reducer' @@ -711,20 +717,70 @@ export function useAgent(): AtpAgent { } /** - * The lex client for the active session, or for the public agent when logged - * out. + * Client for appview reads. + * + * When logged out the bundle's agent is the public agent built by + * `createPublicAgent`, which is configured with the appview proxy and dispatches + * unauthenticated, so the logged-out fallback is the agent itself - there is no + * separate public branch here. */ -export function useLexClient(): Client { +export function useAppviewClient(): Client { const bundle = useContext(BundleContext) if (!bundle) { - throw Error('useLexClient() must be below .') + throw Error('useAppviewClient() must be below .') } - return agentToLexClient(bundle.agent) + return agentToAppviewClient(bundle.agent) } /** - * The unauthenticated lex client for public reads. + * Client for account-host requests. It shares the active session's auth + * lifecycle but sets no proxy or labeler headers, so calls target the PDS. + * Logged out, calls throw `NotAuthenticatedError` before network I/O. Use + * {@link useMaybePdsClient} when the caller must branch on authentication. */ -export function usePublicLexClient(): Client { - return getPublicLexClient() +export function usePdsClient(): Client { + const bundle = useContext(BundleContext) + if (!bundle) { + throw Error('usePdsClient() must be below .') + } + return bundle.session + ? agentToPdsClient(bundle.agent) + : getUnauthenticatedThrowingClient() +} + +/** + * Client for `chat.bsky.*` calls. Logged-out calls throw + * `NotAuthenticatedError`; use {@link useMaybeChatClient} to branch on auth. + */ +export function useChatClient(): Client { + const bundle = useContext(BundleContext) + if (!bundle) { + throw Error('useChatClient() must be below .') + } + return bundle.session + ? agentToChatClient(bundle.agent) + : getUnauthenticatedThrowingClient() +} + +/** + * Account-host client for the active session, or `null` when logged out. + */ +export function useMaybePdsClient(): Client | null { + const bundle = useContext(BundleContext) + return bundle?.session ? agentToPdsClient(bundle.agent) : null +} + +/** + * Chat client for the active session, or `null` when logged out. + */ +export function useMaybeChatClient(): Client | null { + const bundle = useContext(BundleContext) + return bundle?.session ? agentToChatClient(bundle.agent) : null +} + +/** + * The unauthenticated client for public appview reads. + */ +export function usePublicAppviewClient(): Client { + return getPublicAppviewClient() } From b804f267ccc50591b4f53fb42efc76becccb7575 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 13 Aug 2026 22:26:14 +0300 Subject: [PATCH 07/32] [SDK] Widen `#/types/bsky` views to dual-world unions (#11353) Co-authored-by: Claude Fable 5 --- src/types/bsky/__tests__/post.test.ts | 277 +++++++++++++++++++ src/types/bsky/__tests__/starterPack.test.ts | 99 +++++++ src/types/bsky/post.ts | 132 ++++++--- src/types/bsky/profile.ts | 15 +- src/types/bsky/starterPack.ts | 49 +++- 5 files changed, 529 insertions(+), 43 deletions(-) create mode 100644 src/types/bsky/__tests__/post.test.ts create mode 100644 src/types/bsky/__tests__/starterPack.test.ts diff --git a/src/types/bsky/__tests__/post.test.ts b/src/types/bsky/__tests__/post.test.ts new file mode 100644 index 0000000000..6db1d23878 --- /dev/null +++ b/src/types/bsky/__tests__/post.test.ts @@ -0,0 +1,277 @@ +import { + type $Typed as $TypedApi, + type AppBskyEmbedRecord, + type AppBskyFeedDefs, +} from '@atproto/api' +import {type $Typed} from '@atproto/lex' + +import {type app} from '#/lexicons' +import { + type Embed, + type EmbedType, + parseEmbed, + parseEmbedRecordView, +} from '#/types/bsky/post' + +const now = () => new Date().toISOString() + +const imagesView = { + $type: 'app.bsky.embed.images#view', + images: [ + { + $type: 'app.bsky.embed.images#viewImage', + thumb: 'https://example.com/thumb.jpg', + fullsize: 'https://example.com/full.jpg', + alt: 'alt text', + }, + ], +} + +const galleryView = { + $type: 'app.bsky.embed.gallery#view', + items: [ + { + $type: 'app.bsky.embed.gallery#viewImage', + thumbnail: 'https://example.com/thumb.jpg', + fullsize: 'https://example.com/full.jpg', + alt: 'alt text', + aspectRatio: {width: 1, height: 1}, + }, + ], +} + +const externalView = { + $type: 'app.bsky.embed.external#view', + external: { + $type: 'app.bsky.embed.external#viewExternal', + uri: 'https://example.com', + title: 'title', + description: 'description', + }, +} + +const videoView = { + $type: 'app.bsky.embed.video#view', + cid: 'bafyvideo', + playlist: 'https://example.com/playlist.m3u8', +} + +const viewRecord = { + $type: 'app.bsky.embed.record#viewRecord', + uri: 'at://did:plc:abc/app.bsky.feed.post/123', + cid: 'bafypost', + author: { + $type: 'app.bsky.actor.defs#profileViewBasic', + did: 'did:plc:abc', + handle: 'alice.test', + }, + value: {$type: 'app.bsky.feed.post', text: 'hello', createdAt: now()}, + indexedAt: now(), +} + +const recordView = { + $type: 'app.bsky.embed.record#view', + record: viewRecord, +} + +const generatorView = { + $type: 'app.bsky.feed.defs#generatorView', + uri: 'at://did:plc:abc/app.bsky.feed.generator/feed', + cid: 'bafyfeed', + did: 'did:web:example.com', + creator: { + $type: 'app.bsky.actor.defs#profileView', + did: 'did:plc:abc', + handle: 'alice.test', + }, + displayName: 'Cool feed', + indexedAt: now(), +} + +const starterPackViewBasic = { + $type: 'app.bsky.graph.defs#starterPackViewBasic', + uri: 'at://did:plc:abc/app.bsky.graph.starterpack/123', + cid: 'bafypack', + record: {}, + creator: { + $type: 'app.bsky.actor.defs#profileViewBasic', + did: 'did:plc:abc', + handle: 'alice.test', + }, + indexedAt: now(), +} + +/** + * Casts a fixture into the `parseEmbed` input position. The fixtures are plain + * objects standing in for app view responses; the guards under test only read + * `$type`, so the structural detail beyond that is not load-bearing. + */ +const asEmbed = (v: unknown) => v as app.bsky.feed.defs.PostView['embed'] + +/* + * Type-level assertions for the dual-world widening. These are compile-time + * only: each widened arm must accept both the `#/lexicons` view and the + * `@atproto/api` view, and `parseEmbed` must accept a `PostView.embed` from + * either world, because both worlds have live producers. + */ +type Assignable = From extends To ? true : false +type Expect = T + +type _PostArmAcceptsNewWorld = Expect< + Assignable< + {type: 'post'; view: $Typed}, + EmbedType<'post'> + > +> +type _PostArmAcceptsOldWorld = Expect< + Assignable< + {type: 'post'; view: $TypedApi}, + EmbedType<'post'> + > +> +type _ParseEmbedAcceptsNewWorld = Expect< + Assignable< + app.bsky.feed.defs.PostView['embed'], + Parameters[0] + > +> +type _ParseEmbedAcceptsOldWorld = Expect< + Assignable< + AppBskyFeedDefs.PostView['embed'], + Parameters[0] + > +> +type _ParseEmbedReturnsEmbed = Expect< + Assignable, Embed> +> + +describe('types/bsky/post parseEmbed', () => { + it('parses an images view', () => { + const embed = parseEmbed(asEmbed(imagesView)) + expect(embed.type).toBe('images') + if (embed.type === 'images') { + expect(embed.view.images).toHaveLength(1) + } + }) + + it('parses a gallery view', () => { + const embed = parseEmbed(asEmbed(galleryView)) + expect(embed.type).toBe('gallery') + if (embed.type === 'gallery') { + expect(embed.view.items).toHaveLength(1) + } + }) + + it('parses an external view', () => { + const embed = parseEmbed(asEmbed(externalView)) + expect(embed.type).toBe('link') + if (embed.type === 'link') { + expect(embed.view.external.uri).toBe('https://example.com') + } + }) + + it('parses a video view', () => { + const embed = parseEmbed(asEmbed(videoView)) + expect(embed.type).toBe('video') + if (embed.type === 'video') { + expect(embed.view.playlist).toBe('https://example.com/playlist.m3u8') + } + }) + + it('parses a record view through to its inner record', () => { + const embed = parseEmbed(asEmbed(recordView)) + expect(embed.type).toBe('post') + if (embed.type === 'post') { + expect(embed.view.uri).toBe('at://did:plc:abc/app.bsky.feed.post/123') + } + }) + + it('parses a recordWithMedia view into both halves', () => { + const embed = parseEmbed( + asEmbed({ + $type: 'app.bsky.embed.recordWithMedia#view', + record: recordView, + media: imagesView, + }), + ) + expect(embed.type).toBe('post_with_media') + if (embed.type === 'post_with_media') { + expect(embed.view.type).toBe('post') + expect(embed.media.type).toBe('images') + } + }) + + it('returns the unknown arm for an unrecognised $type', () => { + const embed = parseEmbed(asEmbed({$type: 'com.example.someEmbed#view'})) + expect(embed).toEqual({type: 'unknown', view: null}) + }) + + it('returns the unknown arm for undefined', () => { + expect(parseEmbed(undefined)).toEqual({type: 'unknown', view: null}) + }) + + it('does not match an embed with no $type', () => { + expect(parseEmbed(asEmbed({images: []}))).toEqual({ + type: 'unknown', + view: null, + }) + }) +}) + +describe('types/bsky/post parseEmbedRecordView', () => { + const asRecordView = (record: unknown) => + ({record}) as app.bsky.embed.record.View + + it('parses each known record variant', () => { + expect(parseEmbedRecordView(asRecordView(viewRecord)).type).toBe('post') + expect( + parseEmbedRecordView( + asRecordView({$type: 'app.bsky.embed.record#viewNotFound'}), + ).type, + ).toBe('post_not_found') + expect( + parseEmbedRecordView( + asRecordView({$type: 'app.bsky.embed.record#viewBlocked'}), + ).type, + ).toBe('post_blocked') + expect( + parseEmbedRecordView( + asRecordView({$type: 'app.bsky.embed.record#viewDetached'}), + ).type, + ).toBe('post_detached') + expect(parseEmbedRecordView(asRecordView(generatorView)).type).toBe('feed') + expect( + parseEmbedRecordView( + asRecordView({$type: 'app.bsky.graph.defs#listView'}), + ).type, + ).toBe('list') + expect( + parseEmbedRecordView( + asRecordView({$type: 'app.bsky.labeler.defs#labelerView'}), + ).type, + ).toBe('labeler') + expect(parseEmbedRecordView(asRecordView(starterPackViewBasic)).type).toBe( + 'starter_pack', + ) + }) + + it('returns the unknown arm for an unrecognised record', () => { + expect( + parseEmbedRecordView(asRecordView({$type: 'com.example.thing'})), + ).toEqual({type: 'unknown', view: null}) + }) +}) + +describe('types/bsky/post Embed dual-world types', () => { + it('has the compile-time dual-world assertions above satisfied', () => { + /* + * The assertions are the `_*` types declared at module scope; a failure + * surfaces as a typecheck error, not a test failure. This case exists so the + * intent is visible when reading the suite. + */ + const parsedNewWorld: Embed = parseEmbed( + asEmbed({$type: 'app.bsky.embed.images#view', images: []}), + ) + expect(parsedNewWorld.type).toBe('images') + }) +}) diff --git a/src/types/bsky/__tests__/starterPack.test.ts b/src/types/bsky/__tests__/starterPack.test.ts new file mode 100644 index 0000000000..b973766f2e --- /dev/null +++ b/src/types/bsky/__tests__/starterPack.test.ts @@ -0,0 +1,99 @@ +import {type AppBskyGraphDefs} from '@atproto/api' + +import {type app} from '#/lexicons' +import { + type AnyStarterPackView, + isBasicView, + isView, +} from '#/types/bsky/starterPack' + +const now = () => new Date().toISOString() + +const creator = { + $type: 'app.bsky.actor.defs#profileViewBasic', + did: 'did:plc:abc', + handle: 'alice.test', +} + +const basicView = { + $type: 'app.bsky.graph.defs#starterPackViewBasic', + uri: 'at://did:plc:abc/app.bsky.graph.starterpack/123', + cid: 'bafypack', + record: {}, + creator, + indexedAt: now(), +} + +const fullView = { + ...basicView, + $type: 'app.bsky.graph.defs#starterPackView', +} + +/* + * Type-level assertions for the dual-world widening: the alias must accept a + * starter pack view from either world, because both have live producers. These + * are compile-time only - a failure surfaces as a typecheck error. + */ +type Assignable = From extends To ? true : false +type Expect = T + +type _AcceptsNewBasicView = Expect< + Assignable +> +type _AcceptsNewFullView = Expect< + Assignable +> +type _AcceptsOldBasicView = Expect< + Assignable +> +type _AcceptsOldFullView = Expect< + Assignable +> + +describe('types/bsky/starterPack guards', () => { + describe('isBasicView', () => { + it('accepts a basic view', () => { + expect(isBasicView(basicView)).toBe(true) + }) + + it('rejects the full view', () => { + expect(isBasicView(fullView)).toBe(false) + }) + + it('rejects a missing or absent $type', () => { + expect(isBasicView({uri: 'at://x', cid: 'y'})).toBe(false) + expect(isBasicView(null)).toBe(false) + expect(isBasicView(undefined)).toBe(false) + expect(isBasicView('string')).toBe(false) + }) + }) + + describe('isView', () => { + it('accepts the full view', () => { + expect(isView(fullView)).toBe(true) + }) + + it('rejects the basic view', () => { + expect(isView(basicView)).toBe(false) + }) + + it('rejects a missing or absent $type', () => { + expect(isView({uri: 'at://x', cid: 'y'})).toBe(false) + expect(isView(null)).toBe(false) + expect(isView(undefined)).toBe(false) + }) + }) + + it('narrows a view from either world to a readable shape', () => { + /* + * The `$type` string is world-independent, so one guard narrows values from + * both producers; the narrowed union stays structurally readable. + */ + for (const view of [basicView, fullView]) { + if (isBasicView(view) || isView(view)) { + expect(typeof view.uri).toBe('string') + expect(typeof view.creator.did).toBe('string') + } + } + }) +}) diff --git a/src/types/bsky/post.ts b/src/types/bsky/post.ts index 43621ef63b..2c8e3f4d87 100644 --- a/src/types/bsky/post.ts +++ b/src/types/bsky/post.ts @@ -1,64 +1,110 @@ import { - type $Typed, - AppBskyEmbedExternal, - AppBskyEmbedGallery, - AppBskyEmbedImages, - AppBskyEmbedRecord, - AppBskyEmbedRecordWithMedia, - AppBskyEmbedVideo, - AppBskyFeedDefs, - AppBskyGraphDefs, - AppBskyLabelerDefs, + type $Typed as $TypedApi, + type AppBskyEmbedExternal, + type AppBskyEmbedGallery, + type AppBskyEmbedImages, + type AppBskyEmbedRecord, + type AppBskyEmbedVideo, + type AppBskyFeedDefs, + type AppBskyGraphDefs, + type AppBskyLabelerDefs, } from '@atproto/api' +import {type $Typed} from '@atproto/lex' +import {app} from '#/lexicons' +import {isType} from '#/types/bsky' + +/* + * Each `view` slot below accepts both the generated `#/lexicons` view and the + * `@atproto/api` view of the same def, because both worlds have live producers: + * `parseEmbed` narrows with the `#/lexicons` schemas and so returns new-world + * views, while call sites that build an `Embed` by hand still pass views + * produced through the `@atproto/api` agent. + * + * The two worlds share the same `$type` strings, so the guards in this file + * narrow a value from either producer; only the static type differs. + * + * TODO: remove the @atproto/api arms once all producers emit #/lexicons views + */ export type Embed = | { type: 'post' - view: $Typed + view: + | $Typed + | $TypedApi } | { type: 'post_not_found' - view: $Typed + view: + | $Typed + | $TypedApi } | { type: 'post_blocked' - view: $Typed + view: + | $Typed + | $TypedApi } | { type: 'post_detached' - view: $Typed + view: + | $Typed + | $TypedApi } | { type: 'feed' - view: $Typed + view: + | $Typed + | $TypedApi } | { type: 'list' - view: $Typed + view: + | $Typed + | $TypedApi } | { type: 'labeler' - view: $Typed + view: + | $Typed + | $TypedApi } | { type: 'starter_pack' - view: $Typed + view: + | $Typed + | $TypedApi } | { type: 'images' - view: $Typed + /* + * Only the `@atproto/api` view, unlike the other arms: the ImageEmbed + * consumer reads `view.images` directly, and the `#/lexicons` view is + * assignable to this slot, so `parseEmbed`'s new-world value flows in + * while the consumer keeps a single structural shape to read from. + */ + view: $TypedApi } | { type: 'gallery' - view: $Typed + /* + * Old-world only for the same reason as the `images` arm above: the + * consumer narrows `view.items` with `AppBskyEmbedGallery.isViewImage`, + * which cannot narrow the new view's `Unknown$TypedObject` arm. + */ + view: $TypedApi } | { type: 'link' - view: $Typed + view: + | $Typed + | $TypedApi } | { type: 'video' - view: $Typed + view: + | $Typed + | $TypedApi } | { type: 'post_with_media' @@ -72,43 +118,45 @@ export type Embed = export type EmbedType = Extract -export function parseEmbedRecordView({record}: AppBskyEmbedRecord.View): Embed { - if (AppBskyEmbedRecord.isViewRecord(record)) { +export function parseEmbedRecordView({ + record, +}: app.bsky.embed.record.View): Embed { + if (isType(app.bsky.embed.record.viewRecord, record)) { return { type: 'post', view: record, } - } else if (AppBskyEmbedRecord.isViewNotFound(record)) { + } else if (isType(app.bsky.embed.record.viewNotFound, record)) { return { type: 'post_not_found', view: record, } - } else if (AppBskyEmbedRecord.isViewBlocked(record)) { + } else if (isType(app.bsky.embed.record.viewBlocked, record)) { return { type: 'post_blocked', view: record, } - } else if (AppBskyEmbedRecord.isViewDetached(record)) { + } else if (isType(app.bsky.embed.record.viewDetached, record)) { return { type: 'post_detached', view: record, } - } else if (AppBskyFeedDefs.isGeneratorView(record)) { + } else if (isType(app.bsky.feed.defs.generatorView, record)) { return { type: 'feed', view: record, } - } else if (AppBskyGraphDefs.isListView(record)) { + } else if (isType(app.bsky.graph.defs.listView, record)) { return { type: 'list', view: record, } - } else if (AppBskyLabelerDefs.isLabelerView(record)) { + } else if (isType(app.bsky.labeler.defs.labelerView, record)) { return { type: 'labeler', view: record, } - } else if (AppBskyGraphDefs.isStarterPackViewBasic(record)) { + } else if (isType(app.bsky.graph.defs.starterPackViewBasic, record)) { return { type: 'starter_pack', view: record, @@ -121,30 +169,38 @@ export function parseEmbedRecordView({record}: AppBskyEmbedRecord.View): Embed { } } -export function parseEmbed(embed: AppBskyFeedDefs.PostView['embed']): Embed { - if (AppBskyEmbedImages.isView(embed)) { +export function parseEmbed( + /* + * Accepts a `PostView.embed` from either world; the `#/lexicons` guards below + * narrow on `$type`, which is world-independent. + */ + embed: + | app.bsky.feed.defs.PostView['embed'] + | AppBskyFeedDefs.PostView['embed'], +): Embed { + if (isType(app.bsky.embed.images.view, embed)) { return { type: 'images', view: embed, } - } else if (AppBskyEmbedGallery.isView(embed)) { + } else if (isType(app.bsky.embed.gallery.view, embed)) { return { type: 'gallery', view: embed, } - } else if (AppBskyEmbedExternal.isView(embed)) { + } else if (isType(app.bsky.embed.external.view, embed)) { return { type: 'link', view: embed, } - } else if (AppBskyEmbedVideo.isView(embed)) { + } else if (isType(app.bsky.embed.video.view, embed)) { return { type: 'video', view: embed, } - } else if (AppBskyEmbedRecord.isView(embed)) { + } else if (isType(app.bsky.embed.record.view, embed)) { return parseEmbedRecordView(embed) - } else if (AppBskyEmbedRecordWithMedia.isView(embed)) { + } else if (isType(app.bsky.embed.recordWithMedia.view, embed)) { return { type: 'post_with_media', view: parseEmbedRecordView(embed.record), diff --git a/src/types/bsky/profile.ts b/src/types/bsky/profile.ts index 12c8146ae1..4231a6d0fe 100644 --- a/src/types/bsky/profile.ts +++ b/src/types/bsky/profile.ts @@ -1,9 +1,22 @@ import {type AppBskyActorDefs, type ChatBskyActorDefs} from '@atproto/api' +import {type app, type chat} from '#/lexicons' + /** - * Matches any profile view exported by our SDK + * Matches any profile view exported by our SDK, in either world. + * + * Both the generated `#/lexicons` views and the `@atproto/api` views are + * accepted because both are live producers: queries migrated to the lexicon + * client emit the former, unmigrated ones emit the latter, and consumers of + * this alias take profiles from both. + * + * TODO: remove the @atproto/api arms once all producers emit #/lexicons views */ export type AnyProfileView = + | app.bsky.actor.defs.ProfileViewBasic + | app.bsky.actor.defs.ProfileView + | app.bsky.actor.defs.ProfileViewDetailed + | chat.bsky.actor.defs.ProfileViewBasic | AppBskyActorDefs.ProfileViewBasic | AppBskyActorDefs.ProfileView | AppBskyActorDefs.ProfileViewDetailed diff --git a/src/types/bsky/starterPack.ts b/src/types/bsky/starterPack.ts index 0064e16bc6..23df0e4631 100644 --- a/src/types/bsky/starterPack.ts +++ b/src/types/bsky/starterPack.ts @@ -1,11 +1,52 @@ -import {AppBskyGraphDefs} from '@atproto/api' +import {type AppBskyGraphDefs} from '@atproto/api' -export const isBasicView = AppBskyGraphDefs.isStarterPackViewBasic -export const isView = AppBskyGraphDefs.isStarterPackView +import {app} from '#/lexicons' + +/* + * `$type`-only guards for starter pack views. They compare against the + * `#/lexicons` schema's `$type` string rather than delegating to the schema's + * `isTypeOf` (which treats a missing `$type` as a match), matching the + * present-and-equal semantics of the old `@atproto/api` + * `AppBskyGraphDefs.isStarterPackView*` helpers. + * + * The `$type` string is identical in both worlds, so a single check narrows a + * value from either producer; the narrowed type is the union of both worlds' + * views for the same reason {@link AnyStarterPackView} is. + */ +export function isBasicView( + v: unknown, +): v is + | app.bsky.graph.defs.StarterPackViewBasic + | AppBskyGraphDefs.StarterPackViewBasic { + return ( + v != null && + typeof v === 'object' && + (v as {$type?: unknown}).$type === + app.bsky.graph.defs.starterPackViewBasic.$type + ) +} + +export function isView( + v: unknown, +): v is app.bsky.graph.defs.StarterPackView | AppBskyGraphDefs.StarterPackView { + return ( + v != null && + typeof v === 'object' && + (v as {$type?: unknown}).$type === app.bsky.graph.defs.starterPackView.$type + ) +} /** - * Matches any starter pack view exported by our SDK + * Matches any starter pack view exported by our SDK, in either world. + * + * Both the generated `#/lexicons` views and the `@atproto/api` views are + * accepted because both are live producers: queries migrated to the lexicon + * client emit the former, unmigrated ones emit the latter. + * + * TODO: remove the @atproto/api arms once all producers emit #/lexicons views */ export type AnyStarterPackView = + | app.bsky.graph.defs.StarterPackViewBasic + | app.bsky.graph.defs.StarterPackView | AppBskyGraphDefs.StarterPackViewBasic | AppBskyGraphDefs.StarterPackView From 855457c9faa8bfb1a66025894f2feaa3f55d22a2 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 13 Aug 2026 22:26:14 +0300 Subject: [PATCH 08/32] [SDK] Migrate discovery and post-thread queries to the appview client (#11355) Co-authored-by: Claude Fable 5 --- src/state/queries/search-posts-v2.ts | 38 +++++++++-------- src/state/queries/service-config.ts | 9 ++-- src/state/queries/starter-pack-search.ts | 15 ++++--- .../trending/useGetSuggestedFeedsQuery.ts | 8 ++-- .../useGetSuggestedOnboardingUsersQuery.ts | 16 ++++--- .../useGetSuggestedUsersForDiscoverQuery.ts | 35 +++++++--------- .../useGetSuggestedUsersForExploreQuery.ts | 16 ++++--- .../useGetSuggestedUsersForSeeMoreQuery.ts | 16 ++++--- ...useOnboardingSuggestedStarterPacksQuery.ts | 32 +++++++------- src/state/queries/usePostThread/index.ts | 25 +++++++---- .../queries/useSuggestedStarterPacksQuery.ts | 11 ++--- src/view/com/composer/Composer.tsx | 42 +++++++++++-------- 12 files changed, 137 insertions(+), 126 deletions(-) diff --git a/src/state/queries/search-posts-v2.ts b/src/state/queries/search-posts-v2.ts index e33328b5a0..185212f8ef 100644 --- a/src/state/queries/search-posts-v2.ts +++ b/src/state/queries/search-posts-v2.ts @@ -1,10 +1,5 @@ import {useCallback, useMemo, useRef} from 'react' -import { - type AppBskyFeedDefs, - type AppBskyFeedSearchPostsV2, - AtUri, - moderatePost, -} from '@atproto/api' +import {type AppBskyFeedDefs, AtUri, moderatePost} from '@atproto/api' import { type InfiniteData, type QueryClient, @@ -13,8 +8,9 @@ import { } from '@tanstack/react-query' import {useModerationOpts} from '#/state/preferences/moderation-opts' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' import {type SearchFilters} from '#/screens/Search/searchParams' +import {app} from '#/lexicons' import { appendFromMe, buildSearchPostsV2Filters, @@ -48,7 +44,7 @@ export function useSearchPostsV2Query({ enabled?: boolean filters?: SearchFilters }) { - const agent = useAgent() + const client = useAppviewClient() const moderationOpts = useModerationOpts() const selectArgs = useMemo( () => ({ @@ -59,15 +55,15 @@ export function useSearchPostsV2Query({ [query, filters?.author, filters?.from, moderationOpts], ) const lastRun = useRef<{ - data: InfiniteData + data: InfiniteData args: typeof selectArgs - result: InfiniteData + result: InfiniteData } | null>(null) return useInfiniteQuery< - AppBskyFeedSearchPostsV2.OutputSchema, + app.bsky.feed.searchPostsV2.$OutputBody, Error, - InfiniteData, + InfiniteData, QueryKey, string | undefined >({ @@ -79,9 +75,18 @@ export function useSearchPostsV2Query({ * dialog; see buildSearchPostsV2Filters for how the two sources combine. */ const {q, ...embedded} = extractSearchPostsParams(query) - const builtFilters = buildSearchPostsV2Filters(embedded, filters) + /* + * The generated params brand format-constrained strings (uri, + * at-identifier, language) as template-literal types. The builder emits + * plain strings parsed from user input, which are runtime-identical, so + * the brand is asserted rather than re-validated here. + */ + const builtFilters = buildSearchPostsV2Filters( + embedded, + filters, + ) as app.bsky.feed.searchPostsV2.$Params const finalQuery = appendFromMe(q, filters?.from === 'me') - const res = await agent.app.bsky.feed.searchPostsV2({ + return await client.call(app.bsky.feed.searchPostsV2, { ...builtFilters, query: finalQuery, limit: 25, @@ -93,13 +98,12 @@ export function useSearchPostsV2Query({ sort: sort === 'latest' ? 'recent' : sort, allTime: true, }) - return res.data }, initialPageParam: undefined, getNextPageParam: lastPage => lastPage.cursor, enabled: enabled ?? !!moderationOpts, select: useCallback( - (data: InfiniteData) => { + (data: InfiniteData) => { const {moderationOpts, isSearchingSpecificUser} = selectArgs /* @@ -177,7 +181,7 @@ export function* findAllPostsInQueryData( uri: string, ): Generator { const queryDatas = queryClient.getQueriesData< - InfiniteData + InfiniteData >({ queryKey: [searchPostsQueryKeyRoot], }) diff --git a/src/state/queries/service-config.ts b/src/state/queries/service-config.ts index 890a49a5cf..e9f8277b46 100644 --- a/src/state/queries/service-config.ts +++ b/src/state/queries/service-config.ts @@ -1,7 +1,8 @@ import {useQuery} from '@tanstack/react-query' import {STALE} from '#/state/queries' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' +import {app} from '#/lexicons' type ServiceConfig = { checkEmailConfirmed: boolean @@ -13,17 +14,17 @@ type ServiceConfig = { } export function useServiceConfigQuery() { - const agent = useAgent() + const client = useAppviewClient() return useQuery({ refetchOnWindowFocus: true, staleTime: STALE.MINUTES.FIVE, queryKey: ['service-config'], queryFn: async () => { try { - const {data} = await agent.api.app.bsky.unspecced.getConfig() + const data = await client.call(app.bsky.unspecced.getConfig) return { checkEmailConfirmed: Boolean(data.checkEmailConfirmed), - // @ts-expect-error not included in types atm + // @ts-expect-error not included in the lexicon atm topicsEnabled: Boolean(data.topicsEnabled), liveNow: data.liveNow ?? [], } diff --git a/src/state/queries/starter-pack-search.ts b/src/state/queries/starter-pack-search.ts index 7987d1e07f..fd6c2ed51c 100644 --- a/src/state/queries/starter-pack-search.ts +++ b/src/state/queries/starter-pack-search.ts @@ -1,4 +1,3 @@ -import {type AppBskyGraphSearchStarterPacksV2} from '@atproto/api' import { type InfiniteData, keepPreviousData, @@ -7,7 +6,8 @@ import { } from '@tanstack/react-query' import {STALE} from '#/state/queries' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' +import {app} from '#/lexicons' export const RQKEY_ROOT = 'starter-pack-search' export const RQKEY = (query: string, limit?: number) => [ @@ -27,23 +27,22 @@ export function useStarterPackSearch({ maintainData?: boolean limit?: number }) { - const agent = useAgent() + const client = useAppviewClient() return useInfiniteQuery< - AppBskyGraphSearchStarterPacksV2.OutputSchema, + app.bsky.graph.searchStarterPacksV2.$OutputBody, Error, - InfiniteData, + InfiniteData, QueryKey, string | undefined >({ staleTime: STALE.MINUTES.FIVE, queryKey: RQKEY(query, limit), queryFn: async ({pageParam}) => { - const res = await agent.app.bsky.graph.searchStarterPacksV2({ + return await client.call(app.bsky.graph.searchStarterPacksV2, { q: query, limit, cursor: pageParam, }) - return res.data }, enabled: enabled && !!query, initialPageParam: undefined, @@ -54,7 +53,7 @@ export function useStarterPackSearch({ } function select( - data: InfiniteData, + data: InfiniteData, ) { // enforce uniqueness const uris = new Set() diff --git a/src/state/queries/trending/useGetSuggestedFeedsQuery.ts b/src/state/queries/trending/useGetSuggestedFeedsQuery.ts index 42d27d9f39..85cf0be794 100644 --- a/src/state/queries/trending/useGetSuggestedFeedsQuery.ts +++ b/src/state/queries/trending/useGetSuggestedFeedsQuery.ts @@ -7,14 +7,15 @@ import { import {getContentLanguages} from '#/state/preferences/languages' import {STALE} from '#/state/queries' import {usePreferencesQuery} from '#/state/queries/preferences' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' +import {app} from '#/lexicons' export const DEFAULT_LIMIT = 15 export const createGetSuggestedFeedsQueryKey = () => ['suggested-feeds'] export function useGetSuggestedFeedsQuery({enabled}: {enabled?: boolean}) { - const agent = useAgent() + const client = useAppviewClient() const {data: preferences} = usePreferencesQuery() const savedFeeds = preferences?.savedFeeds @@ -24,7 +25,8 @@ export function useGetSuggestedFeedsQuery({enabled}: {enabled?: boolean}) { queryKey: createGetSuggestedFeedsQueryKey(), queryFn: async () => { const contentLangs = getContentLanguages().join(',') - const {data} = await agent.app.bsky.unspecced.getSuggestedFeeds( + const data = await client.call( + app.bsky.unspecced.getSuggestedFeeds, { limit: DEFAULT_LIMIT, }, diff --git a/src/state/queries/trending/useGetSuggestedOnboardingUsersQuery.ts b/src/state/queries/trending/useGetSuggestedOnboardingUsersQuery.ts index 3697c51a7c..f8ffefe68d 100644 --- a/src/state/queries/trending/useGetSuggestedOnboardingUsersQuery.ts +++ b/src/state/queries/trending/useGetSuggestedOnboardingUsersQuery.ts @@ -1,7 +1,3 @@ -import { - type AppBskyActorDefs, - type AppBskyUnspeccedGetSuggestedOnboardingUsers, -} from '@atproto/api' import {type QueryClient, useQuery} from '@tanstack/react-query' import {createBskyTopicsHeader} from '#/lib/api/feed/utils' @@ -9,7 +5,8 @@ import {logger} from '#/logger' import {getContentLanguages} from '#/state/preferences/languages' import {STALE} from '#/state/queries' import {usePreferencesQuery} from '#/state/queries/preferences' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' +import {app} from '#/lexicons' export type QueryProps = { category?: string | null @@ -30,7 +27,7 @@ export const createGetSuggestedOnboardingUsersQueryKey = ( ] export function useGetSuggestedOnboardingUsersQuery(props: QueryProps) { - const agent = useAgent() + const client = useAppviewClient() const {data: preferences} = usePreferencesQuery() return useQuery({ @@ -42,7 +39,8 @@ export function useGetSuggestedOnboardingUsersQuery(props: QueryProps) { const overrideInterests = props.overrideInterests.join(',') - const {data} = await agent.app.bsky.unspecced.getSuggestedOnboardingUsers( + const data = await client.call( + app.bsky.unspecced.getSuggestedOnboardingUsers, { category: props.category ?? undefined, limit: props.limit || 10, @@ -66,9 +64,9 @@ export function useGetSuggestedOnboardingUsersQuery(props: QueryProps) { export function* findAllProfilesInQueryData( queryClient: QueryClient, did: string, -): Generator { +): Generator { const responses = - queryClient.getQueriesData( + queryClient.getQueriesData( { queryKey: [getSuggestedOnboardingUsersQueryKeyRoot], }, diff --git a/src/state/queries/trending/useGetSuggestedUsersForDiscoverQuery.ts b/src/state/queries/trending/useGetSuggestedUsersForDiscoverQuery.ts index c18a4d05c4..6e46e073ce 100644 --- a/src/state/queries/trending/useGetSuggestedUsersForDiscoverQuery.ts +++ b/src/state/queries/trending/useGetSuggestedUsersForDiscoverQuery.ts @@ -1,7 +1,3 @@ -import { - type AppBskyActorDefs, - type AppBskyUnspeccedGetSuggestedUsersForDiscover, -} from '@atproto/api' import {type QueryClient, useQuery} from '@tanstack/react-query' import { @@ -12,7 +8,8 @@ import {logger} from '#/logger' import {getContentLanguages} from '#/state/preferences/languages' import {STALE} from '#/state/queries' import {usePreferencesQuery} from '#/state/queries/preferences' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' +import {app} from '#/lexicons' export type QueryProps = { limit?: number @@ -25,7 +22,7 @@ export const createGetSuggestedUsersForDiscoverQueryKey = (props: { }) => [getSuggestedUsersForDiscoverQueryKeyRoot, props.limit] export function useGetSuggestedUsersForDiscoverQuery(props: QueryProps = {}) { - const agent = useAgent() + const client = useAppviewClient() const {data: preferences} = usePreferencesQuery() return useQuery({ @@ -35,18 +32,18 @@ export function useGetSuggestedUsersForDiscoverQuery(props: QueryProps = {}) { const contentLangs = getContentLanguages().join(',') const userInterests = aggregateUserInterests(preferences) - const {data} = - await agent.app.bsky.unspecced.getSuggestedUsersForDiscover( - { - limit: props.limit || 10, + const data = await client.call( + app.bsky.unspecced.getSuggestedUsersForDiscover, + { + limit: props.limit || 10, + }, + { + headers: { + ...createBskyTopicsHeader(userInterests), + 'Accept-Language': contentLangs, }, - { - headers: { - ...createBskyTopicsHeader(userInterests), - 'Accept-Language': contentLangs, - }, - }, - ) + }, + ) if (!data.recIdStr) { logger.debug('getSuggestedUsersForDiscover response missing recIdStr') } @@ -58,9 +55,9 @@ export function useGetSuggestedUsersForDiscoverQuery(props: QueryProps = {}) { export function* findAllProfilesInQueryData( queryClient: QueryClient, did: string, -): Generator { +): Generator { const responses = - queryClient.getQueriesData( + queryClient.getQueriesData( { queryKey: [getSuggestedUsersForDiscoverQueryKeyRoot], }, diff --git a/src/state/queries/trending/useGetSuggestedUsersForExploreQuery.ts b/src/state/queries/trending/useGetSuggestedUsersForExploreQuery.ts index a2e56573e5..1f729bb269 100644 --- a/src/state/queries/trending/useGetSuggestedUsersForExploreQuery.ts +++ b/src/state/queries/trending/useGetSuggestedUsersForExploreQuery.ts @@ -1,7 +1,3 @@ -import { - type AppBskyActorDefs, - type AppBskyUnspeccedGetSuggestedUsersForExplore, -} from '@atproto/api' import {type QueryClient, useQuery} from '@tanstack/react-query' import { @@ -12,7 +8,8 @@ import {logger} from '#/logger' import {getContentLanguages} from '#/state/preferences/languages' import {STALE} from '#/state/queries' import {usePreferencesQuery} from '#/state/queries/preferences' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' +import {app} from '#/lexicons' export type QueryProps = { category?: string | null @@ -26,7 +23,7 @@ export const createGetSuggestedUsersForExploreQueryKey = ( ) => [getSuggestedUsersForExploreQueryKeyRoot, props.category, props.limit] export function useGetSuggestedUsersForExploreQuery(props: QueryProps = {}) { - const agent = useAgent() + const client = useAppviewClient() const {data: preferences} = usePreferencesQuery() return useQuery({ @@ -36,7 +33,8 @@ export function useGetSuggestedUsersForExploreQuery(props: QueryProps = {}) { const contentLangs = getContentLanguages().join(',') const userInterests = aggregateUserInterests(preferences) - const {data} = await agent.app.bsky.unspecced.getSuggestedUsersForExplore( + const data = await client.call( + app.bsky.unspecced.getSuggestedUsersForExplore, { category: props.category ?? undefined, limit: props.limit || 10, @@ -60,9 +58,9 @@ export function useGetSuggestedUsersForExploreQuery(props: QueryProps = {}) { export function* findAllProfilesInQueryData( queryClient: QueryClient, did: string, -): Generator { +): Generator { const responses = - queryClient.getQueriesData( + queryClient.getQueriesData( { queryKey: [getSuggestedUsersForExploreQueryKeyRoot], }, diff --git a/src/state/queries/trending/useGetSuggestedUsersForSeeMoreQuery.ts b/src/state/queries/trending/useGetSuggestedUsersForSeeMoreQuery.ts index 73fdf6b451..94c0b7735d 100644 --- a/src/state/queries/trending/useGetSuggestedUsersForSeeMoreQuery.ts +++ b/src/state/queries/trending/useGetSuggestedUsersForSeeMoreQuery.ts @@ -1,7 +1,3 @@ -import { - type AppBskyActorDefs, - type AppBskyUnspeccedGetSuggestedUsersForSeeMore, -} from '@atproto/api' import {type QueryClient, useQuery} from '@tanstack/react-query' import { @@ -12,7 +8,8 @@ import {logger} from '#/logger' import {getContentLanguages} from '#/state/preferences/languages' import {STALE} from '#/state/queries' import {usePreferencesQuery} from '#/state/queries/preferences' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' +import {app} from '#/lexicons' export type QueryProps = { category?: string | null @@ -28,7 +25,7 @@ export const createGetSuggestedUsersForSeeMoreQueryKey = (props: { }) => [getSuggestedUsersForSeeMoreQueryKeyRoot, props.category, props.limit] export function useGetSuggestedUsersForSeeMoreQuery(props: QueryProps = {}) { - const agent = useAgent() + const client = useAppviewClient() const {data: preferences} = usePreferencesQuery() return useQuery({ @@ -42,7 +39,8 @@ export function useGetSuggestedUsersForSeeMoreQuery(props: QueryProps = {}) { const contentLangs = getContentLanguages().join(',') const userInterests = aggregateUserInterests(preferences) - const {data} = await agent.app.bsky.unspecced.getSuggestedUsersForSeeMore( + const data = await client.call( + app.bsky.unspecced.getSuggestedUsersForSeeMore, { category: props.category ?? undefined, limit: props.limit || 50, @@ -66,9 +64,9 @@ export function useGetSuggestedUsersForSeeMoreQuery(props: QueryProps = {}) { export function* findAllProfilesInQueryData( queryClient: QueryClient, did: string, -): Generator { +): Generator { const responses = - queryClient.getQueriesData( + queryClient.getQueriesData( { queryKey: [getSuggestedUsersForSeeMoreQueryKeyRoot], }, diff --git a/src/state/queries/useOnboardingSuggestedStarterPacksQuery.ts b/src/state/queries/useOnboardingSuggestedStarterPacksQuery.ts index 5e76ebf100..f0b5a053e3 100644 --- a/src/state/queries/useOnboardingSuggestedStarterPacksQuery.ts +++ b/src/state/queries/useOnboardingSuggestedStarterPacksQuery.ts @@ -7,7 +7,8 @@ import { import {getContentLanguages} from '#/state/preferences/languages' import {STALE} from '#/state/queries' import {usePreferencesQuery} from '#/state/queries/preferences' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' +import {app} from '#/lexicons' export const createOnboardingSuggestedStarterPacksQueryKey = ( interests?: string[], @@ -20,7 +21,7 @@ export function useOnboardingSuggestedStarterPacksQuery({ enabled?: boolean overrideInterests?: string[] }) { - const agent = useAgent() + const client = useAppviewClient() const {data: preferences} = usePreferencesQuery() const contentLangs = getContentLanguages().join(',') @@ -29,21 +30,20 @@ export function useOnboardingSuggestedStarterPacksQuery({ staleTime: STALE.MINUTES.THREE, queryKey: createOnboardingSuggestedStarterPacksQueryKey(overrideInterests), queryFn: async () => { - const {data} = - await agent.app.bsky.unspecced.getOnboardingSuggestedStarterPacks( - {limit: 6}, - { - headers: { - ...createBskyTopicsHeader( - overrideInterests - ? overrideInterests.join(',') - : aggregateUserInterests(preferences), - ), - 'Accept-Language': contentLangs, - }, + return await client.call( + app.bsky.unspecced.getOnboardingSuggestedStarterPacks, + {limit: 6}, + { + headers: { + ...createBskyTopicsHeader( + overrideInterests + ? overrideInterests.join(',') + : aggregateUserInterests(preferences), + ), + 'Accept-Language': contentLangs, }, - ) - return data + }, + ) }, }) } diff --git a/src/state/queries/usePostThread/index.ts b/src/state/queries/usePostThread/index.ts index ceb87fe47b..b4e217f4bd 100644 --- a/src/state/queries/usePostThread/index.ts +++ b/src/state/queries/usePostThread/index.ts @@ -1,4 +1,5 @@ import {useCallback, useMemo, useState} from 'react' +import {type AtUriString} from '@atproto/syntax' import {useQuery, useQueryClient} from '@tanstack/react-query' import {useModerationOpts} from '#/state/preferences/moderation-opts' @@ -27,10 +28,11 @@ import { } from '#/state/queries/usePostThread/types' import {getThreadgateRecord} from '#/state/queries/usePostThread/utils' import * as views from '#/state/queries/usePostThread/views' -import {useAgent, useSession} from '#/state/session' +import {useAppviewClient, useSession} from '#/state/session' import {useMergeThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies' import {useBreakpoints} from '#/alf' import {IS_WEB} from '#/env' +import {app} from '#/lexicons' export * from '#/state/queries/usePostThread/context' export {useUpdatePostThreadThreadgateQueryCache} from '#/state/queries/usePostThread/queryCache' @@ -38,7 +40,7 @@ export * from '#/state/queries/usePostThread/types' export function usePostThread({anchor}: {anchor?: string}) { const qc = useQueryClient() - const agent = useAgent() + const client = useAppviewClient() const {hasSession} = useSession() const {gtPhone} = useBreakpoints() const moderationOpts = useModerationOpts() @@ -71,8 +73,8 @@ export function usePostThread({anchor}: {anchor?: string}) { enabled: isThreadPreferencesLoaded && !!anchor && !!moderationOpts, queryKey: postThreadQueryKey, async queryFn(ctx) { - const {data} = await agent.app.bsky.unspecced.getPostThreadV2({ - anchor: anchor!, + const data = await client.call(app.bsky.unspecced.getPostThreadV2, { + anchor: anchor! as AtUriString, branchingFactor: view === 'linear' ? LINEAR_VIEW_BF : TREE_VIEW_BF, below, sort: sort, @@ -93,18 +95,24 @@ export function usePostThread({anchor}: {anchor?: string}) { ctx.meta.hasOtherReplies = true } + /* + * The generated views type `threadgate.record` as an opaque lex map and + * brand at-uris, so the response is asserted to the exported result type + * here; the record swap-in below and every downstream consumer of the + * thread read that narrower contract. + */ const result = { thread: data.thread || [], threadgate: data.threadgate, hasOtherReplies: !!ctx.meta.hasOtherReplies, - } + } as UsePostThreadQueryResult const record = getThreadgateRecord(result.threadgate) if (result.threadgate && record) { result.threadgate.record = record } - return result as UsePostThreadQueryResult + return result }, placeholderData() { if (!anchor) return @@ -161,10 +169,9 @@ export function usePostThread({anchor}: {anchor?: string}) { enabled: additionalQueryEnabled, queryKey: postThreadOtherQueryKey, async queryFn() { - const {data} = await agent.app.bsky.unspecced.getPostThreadOtherV2({ - anchor: anchor!, + return await client.call(app.bsky.unspecced.getPostThreadOtherV2, { + anchor: anchor! as AtUriString, }) - return data }, }) const serverOtherThreadItems: ThreadItem[] = useMemo(() => { diff --git a/src/state/queries/useSuggestedStarterPacksQuery.ts b/src/state/queries/useSuggestedStarterPacksQuery.ts index 6cb1dd0869..bcd394312e 100644 --- a/src/state/queries/useSuggestedStarterPacksQuery.ts +++ b/src/state/queries/useSuggestedStarterPacksQuery.ts @@ -7,7 +7,8 @@ import { import {getContentLanguages} from '#/state/preferences/languages' import {STALE} from '#/state/queries' import {usePreferencesQuery} from '#/state/queries/preferences' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' +import {app} from '#/lexicons' export const createSuggestedStarterPacksQueryKey = (interests?: string[]) => [ 'suggested-starter-packs', @@ -21,7 +22,7 @@ export function useSuggestedStarterPacksQuery({ enabled?: boolean overrideInterests?: string[] }) { - const agent = useAgent() + const client = useAppviewClient() const {data: preferences} = usePreferencesQuery() const contentLangs = getContentLanguages().join(',') @@ -30,8 +31,9 @@ export function useSuggestedStarterPacksQuery({ staleTime: STALE.MINUTES.THREE, queryKey: createSuggestedStarterPacksQueryKey(overrideInterests), queryFn: async () => { - const {data} = await agent.app.bsky.unspecced.getSuggestedStarterPacks( - undefined, + return await client.call( + app.bsky.unspecced.getSuggestedStarterPacks, + {}, { headers: { ...createBskyTopicsHeader( @@ -43,7 +45,6 @@ export function useSuggestedStarterPacksQuery({ }, }, ) - return data }, }) } diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index f9129c56e9..6c7c2280c2 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -47,12 +47,12 @@ import {type ImagePickerAsset} from 'expo-image-picker' import { AppBskyDraftCreateDraft, AppBskyUnspeccedDefs, - type AppBskyUnspeccedGetPostThreadV2, - type AtpAgent, AtUri, ChatBskyGroupDefs, type RichText, } from '@atproto/api' +import {type Client} from '@atproto/lex' +import {type AtUriString} from '@atproto/syntax' import {plural} from '@lingui/core/macro' import {Trans, useLingui} from '@lingui/react/macro' import {useNavigation} from '@react-navigation/native' @@ -96,7 +96,7 @@ import { import {usePreferencesQuery} from '#/state/queries/preferences' import {useProfileQuery} from '#/state/queries/profile' import {resolveLinkQueryOptions} from '#/state/queries/resolve-link' -import {useAgent, useSession} from '#/state/session' +import {useAgent, useAppviewClient, useSession} from '#/state/session' import {useComposerControls} from '#/state/shell/composer' import {type ComposerOpts, type OnPostSuccessData} from '#/state/shell/composer' import {CharProgress} from '#/view/com/composer/char-progress/CharProgress' @@ -144,6 +144,7 @@ import { IS_WEB_SAFARI, } from '#/env' import {type Gif} from '#/features/gifPicker/types' +import {app} from '#/lexicons' import {BottomSheetPortalProvider} from '../../../../modules/bottom-sheet' import { draftToComposerPosts, @@ -275,6 +276,7 @@ export const ComposePost = ({ ? VIDEO_10_MINUTE_MAX_DURATION_MS : VIDEO_MAX_DURATION_MS const agent = useAgent() + const client = useAppviewClient() const queryClient = useQueryClient() const currentDid = currentAccount!.did const {closeComposer} = useComposerControls() @@ -1105,23 +1107,26 @@ export const ComposePost = ({ 5, _e => true, async () => { - const res = await agent.app.bsky.unspecced.getPostThreadV2({ - anchor: postUri!, - above: false, - below: filteredThread.posts.length - 1, - branchingFactor: 1, - }) - if (res.data.thread.length !== filteredThread.posts.length) { + const res = await client.call( + app.bsky.unspecced.getPostThreadV2, + { + anchor: postUri! as AtUriString, + above: false, + below: filteredThread.posts.length - 1, + branchingFactor: 1, + }, + ) + if (res.thread.length !== filteredThread.posts.length) { throw new Error(`composer: app view is not ready`) } if ( - !res.data.thread.every(p => + !res.thread.every(p => AppBskyUnspeccedDefs.isThreadItemPost(p.value), ) ) { throw new Error(`composer: app view returned non-post items`) } - return res.data.thread + return res.thread }, 1e3, ) @@ -1224,8 +1229,8 @@ export const ComposePost = ({ setLangPrefs.savePostLanguageToHistory() if (initQuote) { // We want to wait for the quote count to update before we call `onPost`, which will refetch data - void whenAppViewReady(agent, initQuote.uri, res => { - const anchor = res.data.thread.at(0) + void whenAppViewReady(client, initQuote.uri, res => { + const anchor = res.thread.at(0) if ( AppBskyUnspeccedDefs.isThreadItemPost(anchor?.value) && anchor.value.post.quoteCount !== initQuote.quoteCount @@ -1272,6 +1277,7 @@ export const ComposePost = ({ l, ax, agent, + client, canPost, isPublishing, currentLanguages, @@ -2486,17 +2492,17 @@ function useKeyboardVerticalOffset() { } async function whenAppViewReady( - agent: AtpAgent, + client: Client, uri: string, - fn: (res: AppBskyUnspeccedGetPostThreadV2.Response) => boolean, + fn: (res: app.bsky.unspecced.getPostThreadV2.$OutputBody) => boolean, ) { await until( 5, // 5 tries 1e3, // 1s delay between tries fn, () => - agent.app.bsky.unspecced.getPostThreadV2({ - anchor: uri, + client.call(app.bsky.unspecced.getPostThreadV2, { + anchor: uri as AtUriString, above: false, below: 0, branchingFactor: 0, From 0912119c7790f761f0f4e59574dcd4a8cfdd4c65 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 13 Aug 2026 22:26:15 +0300 Subject: [PATCH 09/32] [SDK] Migrate profile and graph read queries to the appview client (#11356) Co-authored-by: Claude Fable 5 --- src/state/queries/actor-starter-packs.ts | 20 +++++----- src/state/queries/known-followers.ts | 23 ++++++----- src/state/queries/list-members.ts | 23 ++++++----- src/state/queries/lists-with-membership.ts | 35 +++++++++-------- src/state/queries/my-blocked-accounts.ts | 16 ++++---- src/state/queries/my-lists.ts | 38 ++++++++++--------- src/state/queries/my-muted-accounts.ts | 16 ++++---- src/state/queries/pinned-post.ts | 10 +++-- src/state/queries/profile-feedgens.ts | 23 ++++++----- src/state/queries/profile-followers.ts | 32 +++++++++------- src/state/queries/profile-follows.ts | 29 ++++++++------ src/state/queries/profile-lists.ts | 18 ++++----- src/state/queries/suggested-follows.ts | 29 +++++++------- .../useUpdateProfileVerificationCache.ts | 16 +++++--- 14 files changed, 180 insertions(+), 148 deletions(-) diff --git a/src/state/queries/actor-starter-packs.ts b/src/state/queries/actor-starter-packs.ts index 97cc3b5e05..cef91c6bd6 100644 --- a/src/state/queries/actor-starter-packs.ts +++ b/src/state/queries/actor-starter-packs.ts @@ -1,6 +1,8 @@ +import {type DidString} from '@atproto/syntax' import {type QueryClient, useInfiniteQuery} from '@tanstack/react-query' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' +import {app} from '#/lexicons' export const RQKEY_ROOT = 'actor-starter-packs' export const RQKEY_WITH_MEMBERSHIP_ROOT = 'actor-starter-packs-with-membership' @@ -17,17 +19,17 @@ export function useActorStarterPacksQuery({ did?: string enabled?: boolean }) { - const agent = useAgent() + const client = useAppviewClient() return useInfiniteQuery({ queryKey: RQKEY(did), queryFn: async ({pageParam}: {pageParam?: string}) => { - const res = await agent.app.bsky.graph.getActorStarterPacks({ - actor: did!, + return await client.call(app.bsky.graph.getActorStarterPacks, { + // the enabled flag prevents this from running until did is set + actor: did! as DidString, limit: 10, cursor: pageParam, }) - return res.data }, enabled: Boolean(did) && enabled, initialPageParam: undefined, @@ -42,17 +44,17 @@ export function useActorStarterPacksWithMembershipsQuery({ did?: string enabled?: boolean }) { - const agent = useAgent() + const client = useAppviewClient() return useInfiniteQuery({ queryKey: RQKEY_WITH_MEMBERSHIP(did), queryFn: async ({pageParam}: {pageParam?: string}) => { - const res = await agent.app.bsky.graph.getStarterPacksWithMembership({ - actor: did!, + return await client.call(app.bsky.graph.getStarterPacksWithMembership, { + // the enabled flag prevents this from running until did is set + actor: did! as DidString, limit: 10, cursor: pageParam, }) - return res.data }, enabled: Boolean(did) && enabled, initialPageParam: undefined, diff --git a/src/state/queries/known-followers.ts b/src/state/queries/known-followers.ts index 05bbd5e67c..3106456f9d 100644 --- a/src/state/queries/known-followers.ts +++ b/src/state/queries/known-followers.ts @@ -1,7 +1,5 @@ -import { - type AppBskyActorDefs, - type AppBskyGraphGetKnownFollowers, -} from '@atproto/api' +import {type AppBskyActorDefs} from '@atproto/api' +import {type DidString} from '@atproto/syntax' import { type InfiniteData, type QueryClient, @@ -9,7 +7,8 @@ import { useInfiniteQuery, } from '@tanstack/react-query' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' +import {app} from '#/lexicons' const PAGE_SIZE = 50 type RQPageParam = string | undefined @@ -18,22 +17,22 @@ const RQKEY_ROOT = 'profile-known-followers' export const RQKEY = (did: string) => [RQKEY_ROOT, did] export function useProfileKnownFollowersQuery(did: string | undefined) { - const agent = useAgent() + const client = useAppviewClient() return useInfiniteQuery< - AppBskyGraphGetKnownFollowers.OutputSchema, + app.bsky.graph.getKnownFollowers.$OutputBody, Error, - InfiniteData, + InfiniteData, QueryKey, RQPageParam >({ queryKey: RQKEY(did || ''), async queryFn({pageParam}: {pageParam: RQPageParam}) { - const res = await agent.app.bsky.graph.getKnownFollowers({ - actor: did!, + return await client.call(app.bsky.graph.getKnownFollowers, { + // the enabled flag prevents this from running until did is set + actor: did! as DidString, limit: PAGE_SIZE, cursor: pageParam, }) - return res.data }, initialPageParam: undefined, getNextPageParam: lastPage => lastPage.cursor, @@ -46,7 +45,7 @@ export function* findAllProfilesInQueryData( did: string, ): Generator { const queryDatas = queryClient.getQueriesData< - InfiniteData + InfiniteData >({ queryKey: [RQKEY_ROOT], }) diff --git a/src/state/queries/list-members.ts b/src/state/queries/list-members.ts index c43c7bb983..bde6666dc6 100644 --- a/src/state/queries/list-members.ts +++ b/src/state/queries/list-members.ts @@ -1,9 +1,9 @@ import { type AppBskyActorDefs, type AppBskyGraphDefs, - type AppBskyGraphGetList, type AtpAgent, } from '@atproto/api' +import {type AtUriString} from '@atproto/syntax' import { type InfiniteData, type QueryClient, @@ -13,7 +13,8 @@ import { } from '@tanstack/react-query' import {STALE} from '#/state/queries' -import {useAgent} from '#/state/session' +import {useAgent, useAppviewClient} from '#/state/session' +import {app} from '#/lexicons' const PAGE_SIZE = 30 type RQPageParam = string | undefined @@ -24,23 +25,23 @@ export const RQKEY = (uri: string) => [RQKEY_ROOT, uri] export const RQKEY_ALL = (uri: string) => [RQKEY_ROOT_ALL, uri] export function useListMembersQuery(uri?: string, limit: number = PAGE_SIZE) { - const agent = useAgent() + const client = useAppviewClient() return useInfiniteQuery< - AppBskyGraphGetList.OutputSchema, + app.bsky.graph.getList.$OutputBody, Error, - InfiniteData, + InfiniteData, QueryKey, RQPageParam >({ staleTime: STALE.MINUTES.ONE, queryKey: RQKEY(uri ?? ''), async queryFn({pageParam}: {pageParam: RQPageParam}) { - const res = await agent.app.bsky.graph.getList({ - list: uri!, // the enabled flag will prevent this from running until uri is set + return await client.call(app.bsky.graph.getList, { + // the enabled flag will prevent this from running until uri is set + list: uri! as AtUriString, limit, cursor: pageParam, }) - return res.data }, initialPageParam: undefined, getNextPageParam: lastPage => lastPage.cursor, @@ -60,6 +61,10 @@ export function useAllListMembersQuery(uri?: string) { }) } +/* + * Still on the legacy agent: four call sites outside this cluster pass their + * own `AtpAgent`, so the parameter type is migrated with those consumers. + */ export async function getAllListMembers(agent: AtpAgent, uri: string) { let hasMore = true let cursor: string | undefined @@ -95,7 +100,7 @@ export function* findAllProfilesInQueryData( did: string, ): Generator { const queryDatas = queryClient.getQueriesData< - InfiniteData + InfiniteData >({ queryKey: [RQKEY_ROOT], }) diff --git a/src/state/queries/lists-with-membership.ts b/src/state/queries/lists-with-membership.ts index 3147b8845f..1bd09704b9 100644 --- a/src/state/queries/lists-with-membership.ts +++ b/src/state/queries/lists-with-membership.ts @@ -1,7 +1,5 @@ -import { - type AppBskyActorDefs, - type AppBskyGraphGetListsWithMembership, -} from '@atproto/api' +import {type AppBskyActorDefs} from '@atproto/api' +import {type AtIdentifierString} from '@atproto/syntax' import { type InfiniteData, type QueryClient, @@ -10,10 +8,11 @@ import { } from '@tanstack/react-query' import {createQueryKey} from '#/state/queries/util' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' +import {app} from '#/lexicons' export type ListWithMembership = - AppBskyGraphGetListsWithMembership.ListWithMembership + app.bsky.graph.getListsWithMembership.ListWithMembership const listsWithMembershipQueryKeyRoot = 'lists-with-membership' export const createListsWithMembershipQueryKey = (args: {actor: string}) => @@ -26,23 +25,23 @@ export function useListsWithMembershipQuery({ actor: string | undefined enabled?: boolean }) { - const agent = useAgent() + const client = useAppviewClient() return useInfiniteQuery< - AppBskyGraphGetListsWithMembership.OutputSchema, + app.bsky.graph.getListsWithMembership.$OutputBody, Error, - InfiniteData, + InfiniteData, QueryKey, string | undefined >({ queryKey: createListsWithMembershipQueryKey({actor: actor ?? ''}), queryFn: async ({pageParam}: {pageParam?: string}) => { - const res = await agent.app.bsky.graph.getListsWithMembership({ - actor: actor!, // the enabled flag prevents this from running until actor is set + return await client.call(app.bsky.graph.getListsWithMembership, { + // the enabled flag prevents this from running until actor is set + actor: actor! as AtIdentifierString, limit: 50, cursor: pageParam, }) - return res.data }, enabled: Boolean(actor) && enabled, initialPageParam: undefined, @@ -64,7 +63,7 @@ export function updateListMembershipOptimistically({ subject: AppBskyActorDefs.ProfileView }) { queryClient.setQueryData< - InfiniteData + InfiniteData >(createListsWithMembershipQueryKey({actor}), old => { if (!old) return old @@ -74,12 +73,18 @@ export function updateListMembershipOptimistically({ ...page, listsWithMembership: page.listsWithMembership.map(lwm => { if (lwm.list.uri === listUri) { + /* + * Callers hand over a plain at-uri and an old-world ProfileView, + * whose `uri`/`did` are unbranded strings. They are runtime + * identical to the generated branded forms, so the synthesised + * listItem is asserted rather than re-validated. + */ return { ...lwm, listItem: { uri: membershipUri, subject, - }, + } as app.bsky.graph.defs.ListItemView, } } return lwm @@ -99,7 +104,7 @@ export function removeListMembershipOptimistically({ listUri: string }) { queryClient.setQueryData< - InfiniteData + InfiniteData >(createListsWithMembershipQueryKey({actor}), old => { if (!old) return old diff --git a/src/state/queries/my-blocked-accounts.ts b/src/state/queries/my-blocked-accounts.ts index a2e29136f5..2cbe1dea29 100644 --- a/src/state/queries/my-blocked-accounts.ts +++ b/src/state/queries/my-blocked-accounts.ts @@ -1,4 +1,4 @@ -import {type AppBskyActorDefs, type AppBskyGraphGetBlocks} from '@atproto/api' +import {type AppBskyActorDefs} from '@atproto/api' import { type InfiniteData, type QueryClient, @@ -6,28 +6,28 @@ import { useInfiniteQuery, } from '@tanstack/react-query' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' +import {app} from '#/lexicons' const RQKEY_ROOT = 'my-blocked-accounts' export const RQKEY = () => [RQKEY_ROOT] type RQPageParam = string | undefined export function useMyBlockedAccountsQuery() { - const agent = useAgent() + const client = useAppviewClient() return useInfiniteQuery< - AppBskyGraphGetBlocks.OutputSchema, + app.bsky.graph.getBlocks.$OutputBody, Error, - InfiniteData, + InfiniteData, QueryKey, RQPageParam >({ queryKey: RQKEY(), async queryFn({pageParam}: {pageParam: RQPageParam}) { - const res = await agent.app.bsky.graph.getBlocks({ + return await client.call(app.bsky.graph.getBlocks, { limit: 30, cursor: pageParam, }) - return res.data }, initialPageParam: undefined, getNextPageParam: lastPage => lastPage.cursor, @@ -39,7 +39,7 @@ export function* findAllProfilesInQueryData( did: string, ): Generator { const queryDatas = queryClient.getQueriesData< - InfiniteData + InfiniteData >({ queryKey: [RQKEY_ROOT], }) diff --git a/src/state/queries/my-lists.ts b/src/state/queries/my-lists.ts index aeb9cf4568..d4538104dd 100644 --- a/src/state/queries/my-lists.ts +++ b/src/state/queries/my-lists.ts @@ -1,9 +1,11 @@ import {type AppBskyGraphDefs} from '@atproto/api' +import {type DidString} from '@atproto/syntax' import {type QueryClient, useQuery} from '@tanstack/react-query' import {accumulate} from '#/lib/async/accumulate' import {STALE} from '#/state/queries' -import {useAgent, useSession} from '#/state/session' +import {useAppviewClient, useSession} from '#/state/session' +import {app} from '#/lexicons' export type MyListsFilter = | 'all' @@ -16,7 +18,7 @@ export const RQKEY = (filter: MyListsFilter) => [RQKEY_ROOT, filter] export function useMyListsQuery(filter: MyListsFilter) { const {currentAccount} = useSession() - const agent = useAgent() + const client = useAppviewClient() return useQuery({ staleTime: STALE.MINUTES.ONE, queryKey: RQKEY(filter), @@ -24,42 +26,42 @@ export function useMyListsQuery(filter: MyListsFilter) { let lists: AppBskyGraphDefs.ListView[] = [] const promises = [ accumulate(cursor => - agent.app.bsky.graph - .getLists({ - actor: currentAccount!.did, + client + .call(app.bsky.graph.getLists, { + actor: currentAccount!.did as DidString, cursor, limit: 50, }) - .then(res => ({ - cursor: res.data.cursor, - items: res.data.lists, + .then(data => ({ + cursor: data.cursor, + items: data.lists, })), ), ] if (filter === 'all-including-subscribed' || filter === 'mod') { promises.push( accumulate(cursor => - agent.app.bsky.graph - .getListMutes({ + client + .call(app.bsky.graph.getListMutes, { cursor, limit: 50, }) - .then(res => ({ - cursor: res.data.cursor, - items: res.data.lists, + .then(data => ({ + cursor: data.cursor, + items: data.lists, })), ), ) promises.push( accumulate(cursor => - agent.app.bsky.graph - .getListBlocks({ + client + .call(app.bsky.graph.getListBlocks, { cursor, limit: 50, }) - .then(res => ({ - cursor: res.data.cursor, - items: res.data.lists, + .then(data => ({ + cursor: data.cursor, + items: data.lists, })), ), ) diff --git a/src/state/queries/my-muted-accounts.ts b/src/state/queries/my-muted-accounts.ts index bf36b90296..cd1c6bc6ba 100644 --- a/src/state/queries/my-muted-accounts.ts +++ b/src/state/queries/my-muted-accounts.ts @@ -1,4 +1,4 @@ -import {type AppBskyActorDefs, type AppBskyGraphGetMutes} from '@atproto/api' +import {type AppBskyActorDefs} from '@atproto/api' import { type InfiniteData, type QueryClient, @@ -6,28 +6,28 @@ import { useInfiniteQuery, } from '@tanstack/react-query' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' +import {app} from '#/lexicons' const RQKEY_ROOT = 'my-muted-accounts' export const RQKEY = () => [RQKEY_ROOT] type RQPageParam = string | undefined export function useMyMutedAccountsQuery() { - const agent = useAgent() + const client = useAppviewClient() return useInfiniteQuery< - AppBskyGraphGetMutes.OutputSchema, + app.bsky.graph.getMutes.$OutputBody, Error, - InfiniteData, + InfiniteData, QueryKey, RQPageParam >({ queryKey: RQKEY(), async queryFn({pageParam}: {pageParam: RQPageParam}) { - const res = await agent.app.bsky.graph.getMutes({ + return await client.call(app.bsky.graph.getMutes, { limit: 30, cursor: pageParam, }) - return res.data }, initialPageParam: undefined, getNextPageParam: lastPage => lastPage.cursor, @@ -39,7 +39,7 @@ export function* findAllProfilesInQueryData( did: string, ): Generator { const queryDatas = queryClient.getQueriesData< - InfiniteData + InfiniteData >({ queryKey: [RQKEY_ROOT], }) diff --git a/src/state/queries/pinned-post.ts b/src/state/queries/pinned-post.ts index 09a0b4c9b9..d132127e68 100644 --- a/src/state/queries/pinned-post.ts +++ b/src/state/queries/pinned-post.ts @@ -1,3 +1,4 @@ +import {type DidString} from '@atproto/syntax' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {useMutation, useQueryClient} from '@tanstack/react-query' @@ -5,14 +6,15 @@ import {useMutation, useQueryClient} from '@tanstack/react-query' import {logger} from '#/logger' import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed' import * as Toast from '#/components/Toast' +import {app} from '#/lexicons' import {updatePostShadow} from '../cache/post-shadow' -import {useAgent, useSession} from '../session' +import {useAppviewClient, useSession} from '../session' import {useProfileUpdateMutation} from './profile' export function usePinnedPostMutation() { const {_} = useLingui() const {currentAccount} = useSession() - const agent = useAgent() + const client = useAppviewClient() const queryClient = useQueryClient() const {mutateAsync: profileUpdateMutate} = useProfileUpdateMutation() @@ -33,8 +35,8 @@ export function usePinnedPostMutation() { // get the currently pinned post so we can optimistically remove the pin from it if (!currentAccount) throw new Error('Not signed in') - const {data: profile} = await agent.getProfile({ - actor: currentAccount.did, + const profile = await client.call(app.bsky.actor.getProfile, { + actor: currentAccount.did as DidString, }) prevPinnedPost = profile.pinnedPost?.uri if (prevPinnedPost && prevPinnedPost !== postUri) { diff --git a/src/state/queries/profile-feedgens.ts b/src/state/queries/profile-feedgens.ts index 56e7c70182..7bdcd62b64 100644 --- a/src/state/queries/profile-feedgens.ts +++ b/src/state/queries/profile-feedgens.ts @@ -1,14 +1,13 @@ -import { - type AppBskyFeedGetActorFeeds, - moderateFeedGenerator, -} from '@atproto/api' +import {moderateFeedGenerator} from '@atproto/api' +import {type DidString} from '@atproto/syntax' import { type InfiniteData, type QueryKey, useInfiniteQuery, } from '@tanstack/react-query' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' +import {app} from '#/lexicons' import {useModerationOpts} from '../preferences/moderation-opts' const PAGE_SIZE = 50 @@ -24,25 +23,25 @@ export function useProfileFeedgensQuery( ) { const moderationOpts = useModerationOpts() const enabled = opts?.enabled !== false && Boolean(moderationOpts) - const agent = useAgent() + const client = useAppviewClient() return useInfiniteQuery< - AppBskyFeedGetActorFeeds.OutputSchema, + app.bsky.feed.getActorFeeds.$OutputBody, Error, - InfiniteData, + InfiniteData, QueryKey, RQPageParam >({ queryKey: RQKEY(did), async queryFn({pageParam}: {pageParam: RQPageParam}) { - const res = await agent.app.bsky.feed.getActorFeeds({ - actor: did, + const data = await client.call(app.bsky.feed.getActorFeeds, { + actor: did as DidString, limit: PAGE_SIZE, cursor: pageParam, }) - res.data.feeds.sort((a, b) => { + data.feeds.sort((a, b) => { return (b.likeCount || 0) - (a.likeCount || 0) }) - return res.data + return data }, initialPageParam: undefined, getNextPageParam: lastPage => lastPage.cursor, diff --git a/src/state/queries/profile-followers.ts b/src/state/queries/profile-followers.ts index 62968af2d8..5022ae9576 100644 --- a/src/state/queries/profile-followers.ts +++ b/src/state/queries/profile-followers.ts @@ -1,7 +1,5 @@ -import { - type AppBskyActorDefs, - type AppBskyGraphGetFollowers, -} from '@atproto/api' +import {type AppBskyActorDefs} from '@atproto/api' +import {type DidString} from '@atproto/syntax' import { type InfiniteData, type QueryClient, @@ -9,8 +7,9 @@ import { useInfiniteQuery, } from '@tanstack/react-query' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' import {useAnalytics} from '#/analytics' +import {app} from '#/lexicons' const DEFAULT_SORT = 'latest' const PAGE_SIZE = 30 @@ -33,26 +32,31 @@ export function useProfileFollowersQuery( ) { const ax = useAnalytics() const isSortEnabled = ax.features.enabled(ax.features.FollowSortEnable) - const agent = useAgent() + const client = useAppviewClient() const sortParam = isSortEnabled ? sort || DEFAULT_SORT : undefined return useInfiniteQuery< - AppBskyGraphGetFollowers.OutputSchema, + app.bsky.graph.getFollowers.$OutputBody, Error, - InfiniteData, + InfiniteData, QueryKey, RQPageParam >({ queryKey: RQKEY(did || '', sortParam), async queryFn({pageParam}: {pageParam: RQPageParam}) { - const res = await agent.app.bsky.graph.getFollowers({ - actor: did || '', + /* + * The vendored lexicon does not declare `sort`, so it is spread in only + * when set and the whole params object is asserted. lex forwards + * undeclared params verbatim but rejects an undeclared key whose value + * is `undefined`, hence the conditional spread. + */ + return await client.call(app.bsky.graph.getFollowers, { + actor: (did || '') as DidString, limit: PAGE_SIZE, cursor: pageParam, - sort: sortParam, - }) - return res.data + ...(sortParam ? {sort: sortParam} : {}), + } as app.bsky.graph.getFollowers.$Params) }, initialPageParam: undefined, getNextPageParam: lastPage => lastPage.cursor, @@ -65,7 +69,7 @@ export function* findAllProfilesInQueryData( did: string, ): Generator { const queryDatas = queryClient.getQueriesData< - InfiniteData + InfiniteData >({ queryKey: [RQKEY_ROOT], }) diff --git a/src/state/queries/profile-follows.ts b/src/state/queries/profile-follows.ts index 1c4d5dc69c..1cd857af16 100644 --- a/src/state/queries/profile-follows.ts +++ b/src/state/queries/profile-follows.ts @@ -1,4 +1,5 @@ -import {type AppBskyActorDefs, type AppBskyGraphGetFollows} from '@atproto/api' +import {type AppBskyActorDefs} from '@atproto/api' +import {type DidString} from '@atproto/syntax' import { type InfiniteData, type QueryClient, @@ -7,8 +8,9 @@ import { } from '@tanstack/react-query' import {STALE} from '#/state/queries' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' import {useAnalytics} from '#/analytics' +import {app} from '#/lexicons' const DEFAULT_SORT = 'latest' const PAGE_SIZE = 30 @@ -34,27 +36,32 @@ export function useProfileFollowsQuery( ) { const ax = useAnalytics() const isSortEnabled = ax.features.enabled(ax.features.FollowSortEnable) - const agent = useAgent() + const client = useAppviewClient() const sortParam = isSortEnabled ? sort || DEFAULT_SORT : undefined return useInfiniteQuery< - AppBskyGraphGetFollows.OutputSchema, + app.bsky.graph.getFollows.$OutputBody, Error, - InfiniteData, + InfiniteData, QueryKey, RQPageParam >({ staleTime: STALE.MINUTES.ONE, queryKey: RQKEY(did || '', sortParam), async queryFn({pageParam}: {pageParam: RQPageParam}) { - const res = await agent.app.bsky.graph.getFollows({ - actor: did || '', + /* + * The vendored lexicon does not declare `sort`, so it is spread in only + * when set and the whole params object is asserted. lex forwards + * undeclared params verbatim but rejects an undeclared key whose value + * is `undefined`, hence the conditional spread. + */ + return await client.call(app.bsky.graph.getFollows, { + actor: (did || '') as DidString, limit: limit || PAGE_SIZE, cursor: pageParam, - sort: sortParam, - }) - return res.data + ...(sortParam ? {sort: sortParam} : {}), + } as app.bsky.graph.getFollows.$Params) }, initialPageParam: undefined, getNextPageParam: lastPage => lastPage.cursor, @@ -67,7 +74,7 @@ export function* findAllProfilesInQueryData( did: string, ): Generator { const queryDatas = queryClient.getQueriesData< - InfiniteData + InfiniteData >({ queryKey: [RQKEY_ROOT], }) diff --git a/src/state/queries/profile-lists.ts b/src/state/queries/profile-lists.ts index 965da72e51..0237876513 100644 --- a/src/state/queries/profile-lists.ts +++ b/src/state/queries/profile-lists.ts @@ -1,11 +1,13 @@ -import {type AppBskyGraphGetLists, moderateUserList} from '@atproto/api' +import {moderateUserList} from '@atproto/api' +import {type DidString} from '@atproto/syntax' import { type InfiniteData, type QueryKey, useInfiniteQuery, } from '@tanstack/react-query' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' +import {app} from '#/lexicons' import {useModerationOpts} from '../preferences/moderation-opts' const PAGE_SIZE = 30 @@ -17,23 +19,21 @@ export const RQKEY = (did: string) => [RQKEY_ROOT, did] export function useProfileListsQuery(did: string, opts?: {enabled?: boolean}) { const moderationOpts = useModerationOpts() const enabled = opts?.enabled !== false && Boolean(moderationOpts) - const agent = useAgent() + const client = useAppviewClient() return useInfiniteQuery< - AppBskyGraphGetLists.OutputSchema, + app.bsky.graph.getLists.$OutputBody, Error, - InfiniteData, + InfiniteData, QueryKey, RQPageParam >({ queryKey: RQKEY(did), async queryFn({pageParam}: {pageParam: RQPageParam}) { - const res = await agent.app.bsky.graph.getLists({ - actor: did, + return await client.call(app.bsky.graph.getLists, { + actor: did as DidString, limit: PAGE_SIZE, cursor: pageParam, }) - - return res.data }, initialPageParam: undefined, getNextPageParam: lastPage => lastPage.cursor, diff --git a/src/state/queries/suggested-follows.ts b/src/state/queries/suggested-follows.ts index 7197eb2980..12f726baea 100644 --- a/src/state/queries/suggested-follows.ts +++ b/src/state/queries/suggested-follows.ts @@ -1,9 +1,6 @@ import {useCallback, useMemo} from 'react' -import { - type AppBskyActorDefs, - type AppBskyActorGetSuggestions, - type AppBskyGraphGetSuggestedFollowsByActor, -} from '@atproto/api' +import {type AppBskyActorDefs} from '@atproto/api' +import {type DidString} from '@atproto/syntax' import { type InfiniteData, type QueryClient, @@ -12,7 +9,8 @@ import { } from '@tanstack/react-query' import {STALE} from '#/state/queries' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' +import {app} from '#/lexicons' import type * as bsky from '#/types/bsky' const suggestedFollowsQueryKeyRoot = 'suggested-follows' @@ -32,18 +30,21 @@ export function useSuggestedFollowsByActorQuery({ enabled?: boolean staleTime?: number }) { - const agent = useAgent() + const client = useAppviewClient() return useQuery({ staleTime, queryKey: suggestedFollowsByActorQueryKey(did), queryFn: async () => { - const res = await agent.app.bsky.graph.getSuggestedFollowsByActor({ - actor: did, - }) - const suggestions = res.data.suggestions.filter( + const data = await client.call( + app.bsky.graph.getSuggestedFollowsByActor, + { + actor: did as DidString, + }, + ) + const suggestions = data.suggestions.filter( profile => !profile.viewer?.following, ) - return {suggestions, recId: res.data.recIdStr} + return {suggestions, recId: data.recIdStr} }, enabled, }) @@ -112,7 +113,7 @@ function* findAllProfilesInSuggestedFollowsQueryData( did: string, ) { const queryDatas = queryClient.getQueriesData< - InfiniteData + InfiniteData >({ queryKey: [suggestedFollowsQueryKeyRoot], }) @@ -135,7 +136,7 @@ function* findAllProfilesInSuggestedFollowsByActorQueryData( did: string, ) { const queryDatas = - queryClient.getQueriesData( + queryClient.getQueriesData( { queryKey: [suggestedFollowsByActorQueryKeyRoot], }, diff --git a/src/state/queries/verification/useUpdateProfileVerificationCache.ts b/src/state/queries/verification/useUpdateProfileVerificationCache.ts index f5ccf1458b..08da1244c5 100644 --- a/src/state/queries/verification/useUpdateProfileVerificationCache.ts +++ b/src/state/queries/verification/useUpdateProfileVerificationCache.ts @@ -1,9 +1,11 @@ import {useCallback} from 'react' +import {type DidString} from '@atproto/syntax' import {useQueryClient} from '@tanstack/react-query' import {logger} from '#/logger' import {updateProfileShadow} from '#/state/cache/profile-shadow' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' +import {app} from '#/lexicons' import type * as bsky from '#/types/bsky' /** @@ -13,13 +15,17 @@ import type * as bsky from '#/types/bsky' */ export function useUpdateProfileVerificationCache() { const qc = useQueryClient() - const agent = useAgent() + const client = useAppviewClient() return useCallback( async ({profile}: {profile: bsky.profile.AnyProfileView}) => { try { - const {data: updated} = await agent.getProfile({ - actor: profile.did ?? '', + if (!profile.did) { + logger.warn(`useUpdateProfileVerificationCache: no did`, {profile}) + return + } + const updated = await client.call(app.bsky.actor.getProfile, { + actor: profile.did as DidString, }) updateProfileShadow(qc, profile.did, { verification: updated.verification, @@ -30,6 +36,6 @@ export function useUpdateProfileVerificationCache() { }) } }, - [agent, qc], + [client, qc], ) } From 0e8e0b2ab4755caebc5be7133f90db950dcdb9b9 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 13 Aug 2026 22:26:15 +0300 Subject: [PATCH 10/32] [SDK] Migrate chat convo queries to the chat client (#11357) Co-authored-by: Claude Fable 5 Co-authored-by: Matthieu Sieben --- src/components/dms/LeaveConvoPrompt.tsx | 18 +++++--- src/components/dms/dialogs/NewChatDialog.tsx | 46 +++++++++---------- .../dms/dialogs/ShareViaChatDialog.tsx | 46 +++++++++---------- src/components/moderation/BlockDialog.tsx | 23 +++++----- .../Messages/ConversationSettings/index.tsx | 6 ++- .../Messages/components/ChatLocked.tsx | 8 +++- .../queries/messages/accept-conversation.ts | 13 ++---- src/state/queries/messages/conversation.ts | 27 ++++------- .../messages/get-convo-availability.ts | 20 ++++---- .../queries/messages/get-convo-for-members.ts | 17 ++++--- src/state/queries/messages/get-status.ts | 15 +++--- .../queries/messages/get-unread-counts.ts | 14 +++--- .../queries/messages/leave-conversation.ts | 13 ++---- .../messages/list-conversation-requests.tsx | 15 +++--- .../queries/messages/list-conversations.tsx | 26 +++++------ .../queries/messages/list-convo-members.ts | 26 +++++++---- .../queries/messages/lock-conversation.ts | 18 ++------ .../queries/messages/mute-conversation.ts | 18 ++------ src/state/queries/messages/update-all-read.ts | 13 ++---- 19 files changed, 171 insertions(+), 211 deletions(-) diff --git a/src/components/dms/LeaveConvoPrompt.tsx b/src/components/dms/LeaveConvoPrompt.tsx index 5e9943e53f..a69dfcf845 100644 --- a/src/components/dms/LeaveConvoPrompt.tsx +++ b/src/components/dms/LeaveConvoPrompt.tsx @@ -1,14 +1,15 @@ -import {ChatBskyConvoLeaveConvo} from '@atproto/api' import {useLingui} from '@lingui/react/macro' import {StackActions, useNavigation} from '@react-navigation/native' import {type NavigationProp} from '#/lib/routes/types' import {isNetworkError} from '#/lib/strings/errors' +import {matchXrpcError} from '#/lib/xrpc-error' import {useLeaveConvo} from '#/state/queries/messages/leave-conversation' import {type DialogOuterProps} from '#/components/Dialog' import * as Prompt from '#/components/Prompt' import * as Toast from '#/components/Toast' import {IS_NATIVE} from '#/env' +import {chat} from '#/lexicons' export function LeaveConvoPrompt({ control, @@ -36,12 +37,15 @@ export function LeaveConvoPrompt({ let errorMessage = l`Could not leave chat` if (isNetworkError(error)) { errorMessage = l`A network error occurred. Please check your internet connection.` - } else if (error instanceof ChatBskyConvoLeaveConvo.InvalidConvoError) { - errorMessage = l`Conversation not found.` - } else if ( - error instanceof ChatBskyConvoLeaveConvo.OwnerCannotLeaveError - ) { - errorMessage = l`Owner must lock the group before leaving.` + } else { + switch (matchXrpcError(error, chat.bsky.convo.leaveConvo)) { + case 'InvalidConvo': + errorMessage = l`Conversation not found.` + break + case 'OwnerCannotLeave': + errorMessage = l`Owner must lock the group before leaving.` + break + } } Toast.show(errorMessage, {type: 'error'}) }, diff --git a/src/components/dms/dialogs/NewChatDialog.tsx b/src/components/dms/dialogs/NewChatDialog.tsx index fd9eaa8880..4cca14bd0a 100644 --- a/src/components/dms/dialogs/NewChatDialog.tsx +++ b/src/components/dms/dialogs/NewChatDialog.tsx @@ -1,12 +1,10 @@ import {useCallback} from 'react' -import { - ChatBskyConvoGetConvoForMembers, - ChatBskyGroupCreateGroup, -} from '@atproto/api' +import {ChatBskyGroupCreateGroup} from '@atproto/api' import {Trans, useLingui} from '@lingui/react/macro' import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification' import {isNetworkError} from '#/lib/strings/errors' +import {matchXrpcError} from '#/lib/xrpc-error' import {logger} from '#/logger' import {useCreateGroupChat} from '#/state/queries/messages/create-group-chat' import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members' @@ -19,6 +17,7 @@ import {InitiateChatFlow} from '#/components/dms/InitiateChatFlow' import {MessagePlus_Stroke2_Corner0_Rounded as NewChatIcon} from '#/components/icons/Message' import * as Toast from '#/components/Toast' import {useAnalytics} from '#/analytics' +import {chat} from '#/lexicons' export function NewChat({ control, @@ -54,27 +53,24 @@ export function NewChat({ let errorMessage = l`An issue occurred starting the chat, please try again.` if (isNetworkError(error)) { errorMessage = l`A network error occurred. Please check your internet connection.` - } else if ( - error instanceof ChatBskyConvoGetConvoForMembers.AccountSuspendedError - ) { - errorMessage = l`Suspended accounts cannot participate in chat.` - } else if ( - error instanceof ChatBskyConvoGetConvoForMembers.BlockedActorError - ) { - errorMessage = l`This user has blocked you and cannot be messaged.` - } else if ( - error instanceof ChatBskyConvoGetConvoForMembers.MessagesDisabledError - ) { - errorMessage = l`This user has disabled chat and cannot be messaged.` - } else if ( - error instanceof - ChatBskyConvoGetConvoForMembers.NotFollowedBySenderError - ) { - errorMessage = l`Chat recipient is not followed by the sender.` - } else if ( - error instanceof ChatBskyConvoGetConvoForMembers.RecipientNotFoundError - ) { - errorMessage = l`Unable to find the selected recipient.` + } else { + switch (matchXrpcError(error, chat.bsky.convo.getConvoForMembers)) { + case 'AccountSuspended': + errorMessage = l`Suspended accounts cannot participate in chat.` + break + case 'BlockedActor': + errorMessage = l`This user has blocked you and cannot be messaged.` + break + case 'MessagesDisabled': + errorMessage = l`This user has disabled chat and cannot be messaged.` + break + case 'NotFollowedBySender': + errorMessage = l`Chat recipient is not followed by the sender.` + break + case 'RecipientNotFound': + errorMessage = l`Unable to find the selected recipient.` + break + } } Toast.show(errorMessage, { type: 'error', diff --git a/src/components/dms/dialogs/ShareViaChatDialog.tsx b/src/components/dms/dialogs/ShareViaChatDialog.tsx index 0463cc694d..6dfb6145f7 100644 --- a/src/components/dms/dialogs/ShareViaChatDialog.tsx +++ b/src/components/dms/dialogs/ShareViaChatDialog.tsx @@ -1,11 +1,9 @@ import {useCallback, useState} from 'react' -import { - ChatBskyConvoGetConvoForMembers, - ChatBskyGroupCreateGroup, -} from '@atproto/api' +import {ChatBskyGroupCreateGroup} from '@atproto/api' import {useLingui} from '@lingui/react/macro' import {isNetworkError} from '#/lib/strings/errors' +import {matchXrpcError} from '#/lib/xrpc-error' import {logger} from '#/logger' import {useCreateGroupChat} from '#/state/queries/messages/create-group-chat' import {useGetConvoForMembers} from '#/state/queries/messages/get-convo-for-members' @@ -14,6 +12,7 @@ import {SearchablePeopleList} from '#/components/dialogs/SearchablePeopleList' import {InitiateChatFlow} from '#/components/dms/InitiateChatFlow' import * as Toast from '#/components/Toast' import {useAnalytics} from '#/analytics' +import {chat} from '#/lexicons' export function SendViaChatDialog({ control, @@ -69,27 +68,24 @@ function SendViaChatDialogInner({ let errorMessage = l`An issue occurred starting the chat, please try again.` if (isNetworkError(error)) { errorMessage = l`A network error occurred. Please check your internet connection.` - } else if ( - error instanceof ChatBskyConvoGetConvoForMembers.AccountSuspendedError - ) { - errorMessage = l`Suspended accounts cannot participate in chat.` - } else if ( - error instanceof ChatBskyConvoGetConvoForMembers.BlockedActorError - ) { - errorMessage = l`This user has blocked you and cannot be messaged.` - } else if ( - error instanceof ChatBskyConvoGetConvoForMembers.MessagesDisabledError - ) { - errorMessage = l`This user has disabled chat and cannot be messaged.` - } else if ( - error instanceof - ChatBskyConvoGetConvoForMembers.NotFollowedBySenderError - ) { - errorMessage = l`Chat recipient is not followed by the sender.` - } else if ( - error instanceof ChatBskyConvoGetConvoForMembers.RecipientNotFoundError - ) { - errorMessage = l`Unable to find the selected recipient.` + } else { + switch (matchXrpcError(error, chat.bsky.convo.getConvoForMembers)) { + case 'AccountSuspended': + errorMessage = l`Suspended accounts cannot participate in chat.` + break + case 'BlockedActor': + errorMessage = l`This user has blocked you and cannot be messaged.` + break + case 'MessagesDisabled': + errorMessage = l`This user has disabled chat and cannot be messaged.` + break + case 'NotFollowedBySender': + errorMessage = l`Chat recipient is not followed by the sender.` + break + case 'RecipientNotFound': + errorMessage = l`Unable to find the selected recipient.` + break + } } Toast.show(errorMessage, { type: 'error', diff --git a/src/components/moderation/BlockDialog.tsx b/src/components/moderation/BlockDialog.tsx index a575cd7433..5ec9e4d9e4 100644 --- a/src/components/moderation/BlockDialog.tsx +++ b/src/components/moderation/BlockDialog.tsx @@ -1,14 +1,11 @@ import {useState} from 'react' import {View} from 'react-native' -import { - type ChatBskyConvoDefs, - ChatBskyConvoLeaveConvo, - ChatBskyGroupRemoveMembers, -} from '@atproto/api' +import {type ChatBskyConvoDefs, ChatBskyGroupRemoveMembers} from '@atproto/api' import {Trans, useLingui} from '@lingui/react/macro' import {useQueryClient} from '@tanstack/react-query' import {isNetworkError} from '#/lib/strings/errors' +import {matchXrpcError} from '#/lib/xrpc-error' import {logger} from '#/logger' import {type Shadow} from '#/state/cache/types' import {useLeaveConvo} from '#/state/queries/messages/leave-conversation' @@ -27,6 +24,7 @@ import {parseConvoView} from '#/components/dms/util' import {Loader} from '#/components/Loader' import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' +import {chat} from '#/lexicons' import {type AnyProfileView} from '#/types/bsky/profile' type Item = ChatBskyConvoDefs.ConvoView @@ -282,12 +280,15 @@ function MutualGroupChat({ let errorMessage = l`Could not leave chat.` if (isNetworkError(error)) { errorMessage = l`A network error occurred. Please check your internet connection.` - } else if (error instanceof ChatBskyConvoLeaveConvo.InvalidConvoError) { - errorMessage = l`Chat not found.` - } else if ( - error instanceof ChatBskyConvoLeaveConvo.OwnerCannotLeaveError - ) { - errorMessage = l`Chat owners cannot leave a group chat.` + } else { + switch (matchXrpcError(error, chat.bsky.convo.leaveConvo)) { + case 'InvalidConvo': + errorMessage = l`Chat not found.` + break + case 'OwnerCannotLeave': + errorMessage = l`Chat owners cannot leave a group chat.` + break + } } Toast.show(errorMessage, {type: 'error'}) }, diff --git a/src/screens/Messages/ConversationSettings/index.tsx b/src/screens/Messages/ConversationSettings/index.tsx index 5576f28674..5f76a3f565 100644 --- a/src/screens/Messages/ConversationSettings/index.tsx +++ b/src/screens/Messages/ConversationSettings/index.tsx @@ -3,7 +3,6 @@ import {Pressable, View} from 'react-native' import { ChatBskyActorDefs, ChatBskyConvoDefs, - ChatBskyConvoUnlockConvo, type ModerationOpts, } from '@atproto/api' import {Trans, useLingui} from '@lingui/react/macro' @@ -19,6 +18,7 @@ import { type NativeStackScreenProps, type NavigationProp, } from '#/lib/routes/types' +import {matchXrpcError} from '#/lib/xrpc-error' import {logger} from '#/logger' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useConvoQuery} from '#/state/queries/messages/conversation' @@ -59,6 +59,7 @@ import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' import {IS_WEB} from '#/env' +import {chat} from '#/lexicons' import * as bsky from '#/types/bsky' import {InviteLinkDialog} from '../components/InviteLinkDialog' import {AddMembersLink} from './AddMembersLink' @@ -432,7 +433,8 @@ function SettingsHeader({ logger.error('Failed to lock group chat', {message: e}) Toast.show(l`Failed to lock group chat`, {type: 'error'}) } else if ( - e instanceof ChatBskyConvoUnlockConvo.ConvoLockedByModerationError + matchXrpcError(e, chat.bsky.convo.unlockConvo) === + 'ConvoLockedByModeration' ) { Toast.show(l`This chat is locked by a moderation action`, { type: 'error', diff --git a/src/screens/Messages/components/ChatLocked.tsx b/src/screens/Messages/components/ChatLocked.tsx index 8718667e9c..c9dcd93c3d 100644 --- a/src/screens/Messages/components/ChatLocked.tsx +++ b/src/screens/Messages/components/ChatLocked.tsx @@ -1,10 +1,10 @@ import {Pressable} from 'react-native' -import {ChatBskyConvoUnlockConvo} from '@atproto/api' import {Trans, useLingui} from '@lingui/react/macro' import {useNavigation} from '@react-navigation/native' import {HITSLOP_10} from '#/lib/constants' import {type NavigationProp} from '#/lib/routes/types' +import {matchXrpcError} from '#/lib/xrpc-error' import {logger} from '#/logger' import {useLeaveConvo} from '#/state/queries/messages/leave-conversation' import {useLockConvo} from '#/state/queries/messages/lock-conversation' @@ -15,6 +15,7 @@ import {Lock_Stroke2_Corner0_Rounded as LockIcon} from '#/components/icons/Lock' import * as Prompt from '#/components/Prompt' import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' +import {chat} from '#/lexicons' import {LeaveChatPrompt} from '../ConversationSettings/prompts' import {ChatFooter} from './ChatFooter' @@ -41,7 +42,10 @@ export function ChatLocked({ Toast.show(l({message: 'Group chat unlocked', context: 'toast'})) }, onError: e => { - if (e instanceof ChatBskyConvoUnlockConvo.ConvoLockedByModerationError) { + if ( + matchXrpcError(e, chat.bsky.convo.unlockConvo) === + 'ConvoLockedByModeration' + ) { Toast.show(l`This chat is locked by a moderation action`, { type: 'error', }) diff --git a/src/state/queries/messages/accept-conversation.ts b/src/state/queries/messages/accept-conversation.ts index 668c7decc3..391f3e49f5 100644 --- a/src/state/queries/messages/accept-conversation.ts +++ b/src/state/queries/messages/accept-conversation.ts @@ -4,9 +4,9 @@ import { } from '@atproto/api' import {useMutation, useQueryClient} from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import {logger} from '#/logger' -import {useAgent} from '#/state/session' +import {useChatClient} from '#/state/session' +import {chat} from '#/lexicons' import { type ConvoRequestListQueryData, optimisticDelete as optimisticDeleteRequest, @@ -34,16 +34,11 @@ export function useAcceptConversation( }, ) { const queryClient = useQueryClient() - const agent = useAgent() + const client = useChatClient() return useMutation({ mutationFn: async () => { - const {data} = await agent.chat.bsky.convo.acceptConvo( - {convoId}, - {headers: DM_SERVICE_HEADERS}, - ) - - return data + return await client.call(chat.bsky.convo.acceptConvo, {convoId}) }, onMutate: () => { // snapshot every convo-list cache up front so onError can restore them diff --git a/src/state/queries/messages/conversation.ts b/src/state/queries/messages/conversation.ts index 1b7699c0ef..c0bf8f3551 100644 --- a/src/state/queries/messages/conversation.ts +++ b/src/state/queries/messages/conversation.ts @@ -11,10 +11,10 @@ import { useQueryClient, } from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import {STALE} from '#/state/queries' import {useOnMarkAsRead} from '#/state/queries/messages/list-conversations' -import {useAgent} from '#/state/session' +import {useChatClient} from '#/state/session' +import {chat} from '#/lexicons' import { RQKEY_PARTIAL as UNREAD_COUNTS_PARTIAL_KEY, UNREAD_ACCEPTED_CAP, @@ -30,15 +30,12 @@ export const RQKEY_ROOT = 'convo' export const RQKEY = (convoId: string) => [RQKEY_ROOT, convoId] export function useConvoQuery({convoId}: {convoId: string}) { - const agent = useAgent() + const client = useChatClient() return useQuery({ queryKey: RQKEY(convoId), queryFn: async () => { - const {data} = await agent.chat.bsky.convo.getConvo( - {convoId}, - {headers: DM_SERVICE_HEADERS}, - ) + const data = await client.call(chat.bsky.convo.getConvo, {convoId}) return data.convo }, staleTime: STALE.INFINITY, @@ -55,7 +52,7 @@ export function precacheConvoQuery( export function useMarkAsReadMutation() { const optimisticUpdate = useOnMarkAsRead() const queryClient = useQueryClient() - const agent = useAgent() + const client = useChatClient() return useMutation({ mutationFn: async ({ @@ -67,16 +64,10 @@ export function useMarkAsReadMutation() { }) => { if (!convoId) throw new Error('No convoId provided') - await agent.chat.bsky.convo.updateRead( - { - convoId, - messageId, - }, - { - encoding: 'application/json', - headers: DM_SERVICE_HEADERS, - }, - ) + await client.call(chat.bsky.convo.updateRead, { + convoId, + messageId, + }) }, onMutate({convoId}) { if (!convoId) throw new Error('No convoId provided') diff --git a/src/state/queries/messages/get-convo-availability.ts b/src/state/queries/messages/get-convo-availability.ts index b73efe7953..31b4bb4166 100644 --- a/src/state/queries/messages/get-convo-availability.ts +++ b/src/state/queries/messages/get-convo-availability.ts @@ -1,7 +1,8 @@ +import {type DidString} from '@atproto/syntax' import {useQuery} from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' -import {useAgent} from '#/state/session' +import {useChatClient, useSession} from '#/state/session' +import {chat} from '#/lexicons' import {STALE} from '..' const RQKEY_ROOT = 'convo-availability' @@ -11,19 +12,18 @@ export function useGetConvoAvailabilityQuery( did: string, {enabled = true}: {enabled?: boolean} = {}, ) { - const agent = useAgent() + const client = useChatClient() + const {hasSession} = useSession() return useQuery({ queryKey: RQKEY(did), queryFn: async () => { - const {data} = await agent.chat.bsky.convo.getConvoAvailability( - {members: [did]}, - {headers: DM_SERVICE_HEADERS}, - ) - - return data + return client.call(chat.bsky.convo.getConvoAvailability, { + // callers pass an already-resolved actor did + members: [did as DidString], + }) }, staleTime: STALE.INFINITY, - enabled, + enabled: enabled && hasSession, }) } diff --git a/src/state/queries/messages/get-convo-for-members.ts b/src/state/queries/messages/get-convo-for-members.ts index 58c1ab524a..20f5380126 100644 --- a/src/state/queries/messages/get-convo-for-members.ts +++ b/src/state/queries/messages/get-convo-for-members.ts @@ -1,9 +1,10 @@ import {type ChatBskyConvoGetConvoForMembers} from '@atproto/api' +import {type DidString} from '@atproto/syntax' import {useMutation, useQueryClient} from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import {logger} from '#/logger' -import {useAgent} from '#/state/session' +import {useChatClient} from '#/state/session' +import {chat} from '#/lexicons' import {precacheConvoQuery} from './conversation' export function useGetConvoForMembers({ @@ -14,16 +15,14 @@ export function useGetConvoForMembers({ onError?: (error: Error) => void }) { const queryClient = useQueryClient() - const agent = useAgent() + const client = useChatClient() return useMutation({ mutationFn: async (members: string[]) => { - const {data} = await agent.chat.bsky.convo.getConvoForMembers( - {members: members}, - {headers: DM_SERVICE_HEADERS}, - ) - - return data + return await client.call(chat.bsky.convo.getConvoForMembers, { + // callers pass already-resolved actor dids + members: members as DidString[], + }) }, onSuccess: data => { precacheConvoQuery(queryClient, data.convo) diff --git a/src/state/queries/messages/get-status.ts b/src/state/queries/messages/get-status.ts index 89625dca24..465d442cb6 100644 --- a/src/state/queries/messages/get-status.ts +++ b/src/state/queries/messages/get-status.ts @@ -1,7 +1,7 @@ import {useQuery} from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' -import {useAgent} from '#/state/session' +import {useChatClient, useSession} from '#/state/session' +import {chat} from '#/lexicons' import {STALE} from '..' import {createQueryKey} from '../util' @@ -9,19 +9,16 @@ const chatActorStatusQueryKey = () => createQueryKey('chat-actor-status', {}, {persistedVersion: 1}) export function useChatActorStatusQuery() { - const agent = useAgent() + const client = useChatClient() + const {hasSession} = useSession() return useQuery({ gcTime: STALE.INFINITY, staleTime: STALE.SECONDS.FIFTEEN, queryKey: chatActorStatusQueryKey(), queryFn: async () => { - const {data} = await agent.chat.bsky.actor.getStatus( - {}, - {headers: DM_SERVICE_HEADERS}, - ) - - return data + return await client.call(chat.bsky.actor.getStatus) }, + enabled: hasSession, }) } diff --git a/src/state/queries/messages/get-unread-counts.ts b/src/state/queries/messages/get-unread-counts.ts index 8c276d67f7..5ec532d6ed 100644 --- a/src/state/queries/messages/get-unread-counts.ts +++ b/src/state/queries/messages/get-unread-counts.ts @@ -1,8 +1,8 @@ import {useQuery} from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' -import {useAgent, useSession} from '#/state/session' +import {useChatClient, useSession} from '#/state/session' import {useAgeAssurance} from '#/ageAssurance' +import {chat} from '#/lexicons' import {STALE} from '..' const RQKEY_ROOT = 'convo-unread-counts' @@ -18,7 +18,7 @@ export const UNREAD_ACCEPTED_CAP = 100 export const UNREAD_REQUEST_CAP = 100 export function useUnreadCountsQuery() { - const agent = useAgent() + const client = useChatClient() const {hasSession} = useSession() const aa = useAgeAssurance() const includeGroupChats = !aa.flags.groupChatDisabled @@ -26,11 +26,9 @@ export function useUnreadCountsQuery() { return useQuery({ queryKey: RQKEY(includeGroupChats), queryFn: async () => { - const {data} = await agent.chat.bsky.convo.getUnreadCounts( - {includeGroupChats}, - {headers: DM_SERVICE_HEADERS}, - ) - return data + return await client.call(chat.bsky.convo.getUnreadCounts, { + includeGroupChats, + }) }, staleTime: STALE.SECONDS.FIFTEEN, enabled: hasSession, diff --git a/src/state/queries/messages/leave-conversation.ts b/src/state/queries/messages/leave-conversation.ts index 51b51bde80..72379d8b30 100644 --- a/src/state/queries/messages/leave-conversation.ts +++ b/src/state/queries/messages/leave-conversation.ts @@ -4,10 +4,10 @@ import { } from '@atproto/api' import {useMutation, useQueryClient} from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import {logger} from '#/logger' import {invalidateJoinLinkPreviewsForConvo} from '#/state/queries/join-links' -import {useAgent} from '#/state/session' +import {useChatClient} from '#/state/session' +import {chat} from '#/lexicons' import { type ConvoRequestListQueryData, optimisticDelete as optimisticDeleteRequest, @@ -38,19 +38,14 @@ export function useLeaveConvo( }, ) { const queryClient = useQueryClient() - const agent = useAgent() + const client = useChatClient() return useMutation({ mutationKey: RQKEY(convoId), mutationFn: async () => { if (!convoId) throw new Error('No convoId provided') - const {data} = await agent.chat.bsky.convo.leaveConvo( - {convoId}, - {headers: DM_SERVICE_HEADERS, encoding: 'application/json'}, - ) - - return data + return await client.call(chat.bsky.convo.leaveConvo, {convoId}) }, onMutate: () => { const prevConvoListQueries = diff --git a/src/state/queries/messages/list-conversation-requests.tsx b/src/state/queries/messages/list-conversation-requests.tsx index 10023db4eb..38ae8e6c3d 100644 --- a/src/state/queries/messages/list-conversation-requests.tsx +++ b/src/state/queries/messages/list-conversation-requests.tsx @@ -9,8 +9,8 @@ import { useInfiniteQuery, } from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' -import {useAgent} from '#/state/session' +import {useChatClient} from '#/state/session' +import {chat} from '#/lexicons' const DEFAULT_LIMIT = 10 @@ -26,17 +26,16 @@ export function useListConvoRequests({ enabled?: boolean limit?: number } = {}) { - const agent = useAgent() + const client = useChatClient() return useInfiniteQuery({ enabled, queryKey: RQKEY(limit), queryFn: async ({pageParam}) => { - const {data} = await agent.chat.bsky.convo.listConvoRequests( - {limit, cursor: pageParam}, - {headers: DM_SERVICE_HEADERS}, - ) - return data + return await client.call(chat.bsky.convo.listConvoRequests, { + limit, + cursor: pageParam, + }) }, initialPageParam: undefined as RQPageParam, getNextPageParam: lastPage => lastPage.cursor, diff --git a/src/state/queries/messages/list-conversations.tsx b/src/state/queries/messages/list-conversations.tsx index 1f73c720bd..6d81a96ecc 100644 --- a/src/state/queries/messages/list-conversations.tsx +++ b/src/state/queries/messages/list-conversations.tsx @@ -14,11 +14,11 @@ import { } from '@tanstack/react-query' import throttle from 'lodash.throttle' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import {useCurrentConvoId} from '#/state/messages/current-convo-id' import {useMessagesEventBus} from '#/state/messages/events' import {invalidateJoinLinkPreviewsForConvo} from '#/state/queries/join-links' -import {useAgent, useSession} from '#/state/session' +import {useChatClient, useSession} from '#/state/session' +import {chat} from '#/lexicons' import * as bsky from '#/types/bsky' import {RQKEY as CONVO_KEY} from './conversation' import { @@ -119,24 +119,20 @@ export function useListConvosQuery({ limit?: number lockStatus?: 'unlocked' | 'locked' | 'locked-permanently' } = {}) { - const agent = useAgent() + const client = useChatClient() return useInfiniteQuery({ enabled, queryKey: RQKEY(status ?? 'all', readState, kind, lockStatus, limit), queryFn: async ({pageParam}) => { - const {data} = await agent.chat.bsky.convo.listConvos( - { - limit, - cursor: pageParam, - readState: readState === 'unread' ? 'unread' : undefined, - kind: kind === 'all' ? undefined : kind, - lockStatus, - status, - }, - {headers: DM_SERVICE_HEADERS}, - ) - return data + return await client.call(chat.bsky.convo.listConvos, { + limit, + cursor: pageParam, + readState: readState === 'unread' ? 'unread' : undefined, + kind: kind === 'all' ? undefined : kind, + lockStatus, + status, + }) }, initialPageParam: undefined as RQPageParam, getNextPageParam: lastPage => lastPage.cursor, diff --git a/src/state/queries/messages/list-convo-members.ts b/src/state/queries/messages/list-convo-members.ts index d2ff52dd5c..386d82ca82 100644 --- a/src/state/queries/messages/list-convo-members.ts +++ b/src/state/queries/messages/list-convo-members.ts @@ -1,10 +1,10 @@ import {type ChatBskyActorDefs} from '@atproto/api' import {type QueryClient, useQuery} from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import {STALE} from '#/state/queries' import {createQueryKey} from '#/state/queries/util' -import {useAgent} from '#/state/session' +import {useChatClient} from '#/state/session' +import {chat} from '#/lexicons' const RQKEY_ROOT = 'listConvoMembers' export const listConvoMembersQueryKey = (convoId: string) => @@ -20,19 +20,27 @@ export function useListConvoMembersQuery({ convoId: string placeholderData?: ChatBskyActorDefs.ProfileViewBasic[] }) { - const agent = useAgent() + const client = useChatClient() return useQuery({ queryKey: listConvoMembersQueryKey(convoId), queryFn: async () => { - const members = [] - let cursor + /* + * Both locals are annotated because the loop is self-referential: `data` + * is inferred from a call whose params include `cursor`, so leaving + * `cursor` to be inferred from `data.cursor` is circular. Annotating + * `members` with the exported profile type also keeps the hook's result + * type unchanged for consumers. + */ + const members: ChatBskyActorDefs.ProfileViewBasic[] = [] + let cursor: string | undefined do { - const {data} = await agent.chat.bsky.convo.getConvoMembers( - {convoId, cursor, limit: LIMIT}, - {headers: DM_SERVICE_HEADERS}, - ) + const data = await client.call(chat.bsky.convo.getConvoMembers, { + convoId, + cursor, + limit: LIMIT, + }) members.push(...data.members) cursor = data.cursor } while (cursor) diff --git a/src/state/queries/messages/lock-conversation.ts b/src/state/queries/messages/lock-conversation.ts index 122c0633b4..13242d59db 100644 --- a/src/state/queries/messages/lock-conversation.ts +++ b/src/state/queries/messages/lock-conversation.ts @@ -1,8 +1,8 @@ import {ChatBskyConvoDefs, type ChatBskyConvoLockConvo} from '@atproto/api' import {useMutation, useQueryClient} from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' -import {useAgent} from '#/state/session' +import {useChatClient} from '#/state/session' +import {chat} from '#/lexicons' import { rollbackConvoOptimistic, updateConvoOptimistic, @@ -25,23 +25,15 @@ export function useLockConvo( }, ) { const queryClient = useQueryClient() - const agent = useAgent() + const client = useChatClient() return useMutation({ mutationFn: async ({lock}: {lock: boolean; silent?: boolean}) => { if (!convoId) throw new Error('No convoId provided') if (lock) { - const {data} = await agent.chat.bsky.convo.lockConvo( - {convoId}, - {headers: DM_SERVICE_HEADERS, encoding: 'application/json'}, - ) - return data + return await client.call(chat.bsky.convo.lockConvo, {convoId}) } else { - const {data} = await agent.chat.bsky.convo.unlockConvo( - {convoId}, - {headers: DM_SERVICE_HEADERS, encoding: 'application/json'}, - ) - return data + return await client.call(chat.bsky.convo.unlockConvo, {convoId}) } }, onMutate: ({lock}) => { diff --git a/src/state/queries/messages/mute-conversation.ts b/src/state/queries/messages/mute-conversation.ts index 03a9ab0b4a..2e9e911b5e 100644 --- a/src/state/queries/messages/mute-conversation.ts +++ b/src/state/queries/messages/mute-conversation.ts @@ -1,8 +1,8 @@ import {type ChatBskyConvoMuteConvo} from '@atproto/api' import {useMutation, useQueryClient} from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' -import {useAgent} from '#/state/session' +import {useChatClient} from '#/state/session' +import {chat} from '#/lexicons' import { rollbackConvoOptimistic, updateConvoOptimistic, @@ -19,23 +19,15 @@ export function useMuteConvo( }, ) { const queryClient = useQueryClient() - const agent = useAgent() + const client = useChatClient() return useMutation({ mutationFn: async ({mute}: {mute: boolean}) => { if (!convoId) throw new Error('No convoId provided') if (mute) { - const {data} = await agent.chat.bsky.convo.muteConvo( - {convoId}, - {headers: DM_SERVICE_HEADERS, encoding: 'application/json'}, - ) - return data + return await client.call(chat.bsky.convo.muteConvo, {convoId}) } else { - const {data} = await agent.chat.bsky.convo.unmuteConvo( - {convoId}, - {headers: DM_SERVICE_HEADERS, encoding: 'application/json'}, - ) - return data + return await client.call(chat.bsky.convo.unmuteConvo, {convoId}) } }, onMutate: ({mute}) => { diff --git a/src/state/queries/messages/update-all-read.ts b/src/state/queries/messages/update-all-read.ts index 6fbffc58cd..afcb062d16 100644 --- a/src/state/queries/messages/update-all-read.ts +++ b/src/state/queries/messages/update-all-read.ts @@ -1,9 +1,9 @@ import {type ChatBskyConvoGetUnreadCounts} from '@atproto/api' import {useMutation, useQueryClient} from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import {logger} from '#/logger' -import {useAgent} from '#/state/session' +import {useChatClient} from '#/state/session' +import {chat} from '#/lexicons' import {RQKEY_PARTIAL as UNREAD_COUNTS_PARTIAL_KEY} from './get-unread-counts' import { type ConvoRequestListQueryData, @@ -29,16 +29,11 @@ export function useUpdateAllRead( }, ) { const queryClient = useQueryClient() - const agent = useAgent() + const client = useChatClient() return useMutation({ mutationFn: async () => { - const {data} = await agent.chat.bsky.convo.updateAllRead( - {status}, - {headers: DM_SERVICE_HEADERS, encoding: 'application/json'}, - ) - - return data + return await client.call(chat.bsky.convo.updateAllRead, {status}) }, onMutate: () => { // snapshot every convo-list cache up front so onError can restore them From ea4f70fb1e0ead090322307497dd5c960093a01c Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 13 Aug 2026 22:26:16 +0300 Subject: [PATCH 11/32] [SDK] Migrate group chat and join links to the chat client (#11358) Co-authored-by: Claude Fable 5 --- src/components/dms/dialogs/NewChatDialog.tsx | 45 ++++++------ .../dms/dialogs/ShareViaChatDialog.tsx | 45 ++++++------ .../intents/GroupChatJoinDialog.tsx | 69 ++++++++++--------- src/components/moderation/BlockDialog.tsx | 19 ++--- src/screens/Messages/JoinRequests.tsx | 50 +++++++------- .../components/OutgoingRequestListItem.tsx | 11 ++- src/state/queries/join-links.ts | 58 ++++++++++------ .../queries/messages/add-group-members.ts | 17 ++--- .../queries/messages/create-group-chat.ts | 18 ++--- .../queries/messages/create-join-link.ts | 16 ++--- .../queries/messages/disable-join-link.ts | 12 ++-- .../queries/messages/edit-group-chat-name.ts | 15 ++-- src/state/queries/messages/edit-join-link.ts | 16 ++--- .../queries/messages/enable-join-link.ts | 12 ++-- src/state/queries/messages/join-requests.ts | 32 +++++---- .../queries/messages/list-join-requests.ts | 17 ++--- .../queries/messages/list-mutual-groups.ts | 18 ++--- .../messages/mark-join-request-read.ts | 11 ++- .../queries/messages/remove-from-group.ts | 17 ++--- .../messages/request-join-group-chat.ts | 12 ++-- .../messages/withdraw-join-group-chat.ts | 12 ++-- 21 files changed, 263 insertions(+), 259 deletions(-) diff --git a/src/components/dms/dialogs/NewChatDialog.tsx b/src/components/dms/dialogs/NewChatDialog.tsx index 4cca14bd0a..7ee103ef9b 100644 --- a/src/components/dms/dialogs/NewChatDialog.tsx +++ b/src/components/dms/dialogs/NewChatDialog.tsx @@ -1,5 +1,4 @@ import {useCallback} from 'react' -import {ChatBskyGroupCreateGroup} from '@atproto/api' import {Trans, useLingui} from '@lingui/react/macro' import {useRequireEmailVerification} from '#/lib/hooks/useRequireEmailVerification' @@ -88,29 +87,27 @@ export function NewChat({ let errorMessage = l`An issue occurred starting the group chat, please try again.` if (isNetworkError(error)) { errorMessage = l`A network error occurred. Please check your internet connection.` - } else if ( - error instanceof ChatBskyGroupCreateGroup.AccountSuspendedError - ) { - errorMessage = l`Suspended accounts cannot participate in a group chat.` - } else if (error instanceof ChatBskyGroupCreateGroup.BlockedActorError) { - errorMessage = l`One of the selected recipients has blocked you and cannot be messaged.` - } else if ( - error instanceof - ChatBskyGroupCreateGroup.NewAccountCannotCreateGroupError - ) { - errorMessage = l`You cannot create a group chat yet.` - } else if ( - error instanceof ChatBskyGroupCreateGroup.NotFollowedBySenderError - ) { - errorMessage = l`A selected recipient is not followed by the sender.` - } else if ( - error instanceof ChatBskyGroupCreateGroup.RecipientNotFoundError - ) { - errorMessage = l`Unable to find a selected recipient.` - } else if ( - error instanceof ChatBskyGroupCreateGroup.UserForbidsGroupsError - ) { - errorMessage = l`One of the selected recipients does not allow group chats.` + } else { + switch (matchXrpcError(error, chat.bsky.group.createGroup)) { + case 'AccountSuspended': + errorMessage = l`Suspended accounts cannot participate in a group chat.` + break + case 'BlockedActor': + errorMessage = l`One of the selected recipients has blocked you and cannot be messaged.` + break + case 'NewAccountCannotCreateGroup': + errorMessage = l`You cannot create a group chat yet.` + break + case 'NotFollowedBySender': + errorMessage = l`A selected recipient is not followed by the sender.` + break + case 'RecipientNotFound': + errorMessage = l`Unable to find a selected recipient.` + break + case 'UserForbidsGroups': + errorMessage = l`One of the selected recipients does not allow group chats.` + break + } } Toast.show(errorMessage, { type: 'error', diff --git a/src/components/dms/dialogs/ShareViaChatDialog.tsx b/src/components/dms/dialogs/ShareViaChatDialog.tsx index 6dfb6145f7..f26dd47a57 100644 --- a/src/components/dms/dialogs/ShareViaChatDialog.tsx +++ b/src/components/dms/dialogs/ShareViaChatDialog.tsx @@ -1,5 +1,4 @@ import {useCallback, useState} from 'react' -import {ChatBskyGroupCreateGroup} from '@atproto/api' import {useLingui} from '@lingui/react/macro' import {isNetworkError} from '#/lib/strings/errors' @@ -104,29 +103,27 @@ function SendViaChatDialogInner({ let errorMessage = l`An issue occurred starting the group chat, please try again.` if (isNetworkError(error)) { errorMessage = l`A network error occurred. Please check your internet connection.` - } else if ( - error instanceof ChatBskyGroupCreateGroup.AccountSuspendedError - ) { - errorMessage = l`Suspended accounts cannot participate in a group chat.` - } else if (error instanceof ChatBskyGroupCreateGroup.BlockedActorError) { - errorMessage = l`One of the selected recipients has blocked you and cannot be messaged.` - } else if ( - error instanceof - ChatBskyGroupCreateGroup.NewAccountCannotCreateGroupError - ) { - errorMessage = l`You cannot create a group chat yet.` - } else if ( - error instanceof ChatBskyGroupCreateGroup.NotFollowedBySenderError - ) { - errorMessage = l`A selected recipient is not followed by the sender.` - } else if ( - error instanceof ChatBskyGroupCreateGroup.RecipientNotFoundError - ) { - errorMessage = l`Unable to find a selected recipient.` - } else if ( - error instanceof ChatBskyGroupCreateGroup.UserForbidsGroupsError - ) { - errorMessage = l`One of the selected recipients does not allow group chats.` + } else { + switch (matchXrpcError(error, chat.bsky.group.createGroup)) { + case 'AccountSuspended': + errorMessage = l`Suspended accounts cannot participate in a group chat.` + break + case 'BlockedActor': + errorMessage = l`One of the selected recipients has blocked you and cannot be messaged.` + break + case 'NewAccountCannotCreateGroup': + errorMessage = l`You cannot create a group chat yet.` + break + case 'NotFollowedBySender': + errorMessage = l`A selected recipient is not followed by the sender.` + break + case 'RecipientNotFound': + errorMessage = l`Unable to find a selected recipient.` + break + case 'UserForbidsGroups': + errorMessage = l`One of the selected recipients does not allow group chats.` + break + } } Toast.show(errorMessage, { type: 'error', diff --git a/src/components/intents/GroupChatJoinDialog.tsx b/src/components/intents/GroupChatJoinDialog.tsx index 7c506a634d..53782ae271 100644 --- a/src/components/intents/GroupChatJoinDialog.tsx +++ b/src/components/intents/GroupChatJoinDialog.tsx @@ -1,11 +1,6 @@ import {useEffect} from 'react' import {View} from 'react-native' -import { - ChatBskyGroupDefs, - ChatBskyGroupRequestJoin, - ChatBskyGroupWithdrawJoinRequest, - moderateProfile, -} from '@atproto/api' +import {ChatBskyGroupDefs, moderateProfile} from '@atproto/api' import {Plural, Trans, useLingui} from '@lingui/react/macro' import {useNavigation} from '@react-navigation/native' import {useQueryClient} from '@tanstack/react-query' @@ -16,6 +11,7 @@ import {makeProfileLink} from '#/lib/routes/links' import {type NavigationProp} from '#/lib/routes/types' import {isNetworkError} from '#/lib/strings/errors' import {sanitizeHandle} from '#/lib/strings/handles' +import {matchXrpcError} from '#/lib/xrpc-error' import {logger} from '#/logger' import {useModerationOpts} from '#/state/preferences/moderation-opts' import { @@ -49,6 +45,7 @@ import {Loader} from '#/components/Loader' import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' +import {chat} from '#/lexicons' import {ProfileBadges} from '../ProfileBadges' export function GroupChatJoinDialog() { @@ -147,33 +144,37 @@ function GroupChatJoinDialogContent({code}: {code?: string}) { let errorMessage = l`Failed to join the group chat. Please try again.` if (isNetworkError(error)) { errorMessage = l`There was a problem with your internet connection, please try again` - } else if (error instanceof ChatBskyGroupRequestJoin.ConvoLockedError) { - errorMessage = l`This conversation is locked.` - } else if ( - error instanceof ChatBskyGroupRequestJoin.FollowRequiredError - ) { - errorMessage = l`Only followers can join this group chat.` - } else if (error instanceof ChatBskyGroupRequestJoin.InvalidCodeError) { - errorMessage = l`Invalid group chat code.` - } else if ( - error instanceof ChatBskyGroupRequestJoin.LinkDisabledError - ) { - errorMessage = l`This invite link has been disabled.` - } else if ( - error instanceof ChatBskyGroupRequestJoin.MemberLimitReachedError - ) { - errorMessage = l`The member limit has been reached.` - const preview = data?.joinLinkPreviews[0] - if ( - ChatBskyGroupDefs.isJoinLinkPreviewView(preview) && - preview.convo?.id - ) { - ax.metric('groupchat:join:memberLimitReached', { - convoId: preview.convo.id, - }) + } else { + switch (matchXrpcError(error, chat.bsky.group.requestJoin)) { + case 'ConvoLocked': + errorMessage = l`This conversation is locked.` + break + case 'FollowRequired': + errorMessage = l`Only followers can join this group chat.` + break + case 'InvalidCode': + errorMessage = l`Invalid group chat code.` + break + case 'LinkDisabled': + errorMessage = l`This invite link has been disabled.` + break + case 'MemberLimitReached': { + errorMessage = l`The member limit has been reached.` + const preview = data?.joinLinkPreviews[0] + if ( + ChatBskyGroupDefs.isJoinLinkPreviewView(preview) && + preview.convo?.id + ) { + ax.metric('groupchat:join:memberLimitReached', { + convoId: preview.convo.id, + }) + } + break + } + case 'UserKicked': + errorMessage = l`You have been previously removed from this group and can’t join it using this link.` + break } - } else if (error instanceof ChatBskyGroupRequestJoin.UserKickedError) { - errorMessage = l`You have been previously removed from this group and can’t join it using this link.` } Toast.show(errorMessage) }, @@ -194,8 +195,8 @@ function GroupChatJoinDialogContent({code}: {code?: string}) { if (isNetworkError(error)) { errorMessage = l`There was a problem with your internet connection, please try again` } else if ( - error instanceof - ChatBskyGroupWithdrawJoinRequest.InvalidJoinRequestError + matchXrpcError(error, chat.bsky.group.withdrawJoinRequest) === + 'InvalidJoinRequest' ) { errorMessage = l`Invalid rescind request.` } diff --git a/src/components/moderation/BlockDialog.tsx b/src/components/moderation/BlockDialog.tsx index 5ec9e4d9e4..d576e071d7 100644 --- a/src/components/moderation/BlockDialog.tsx +++ b/src/components/moderation/BlockDialog.tsx @@ -1,6 +1,6 @@ import {useState} from 'react' import {View} from 'react-native' -import {type ChatBskyConvoDefs, ChatBskyGroupRemoveMembers} from '@atproto/api' +import {type ChatBskyConvoDefs} from '@atproto/api' import {Trans, useLingui} from '@lingui/react/macro' import {useQueryClient} from '@tanstack/react-query' @@ -309,14 +309,15 @@ function MutualGroupChat({ let errorMessage = l`Could not remove member.` if (isNetworkError(error)) { errorMessage = l`A network error occurred. Please check your internet connection.` - } else if ( - error instanceof ChatBskyGroupRemoveMembers.InvalidConvoError - ) { - errorMessage = l`Chat not found.` - } else if ( - error instanceof ChatBskyGroupRemoveMembers.InsufficientRoleError - ) { - errorMessage = l`You must be a chat owner to remove a member.` + } else { + switch (matchXrpcError(error, chat.bsky.group.removeMembers)) { + case 'InvalidConvo': + errorMessage = l`Chat not found.` + break + case 'InsufficientRole': + errorMessage = l`You must be a chat owner to remove a member.` + break + } } Toast.show(errorMessage, {type: 'error'}) }, diff --git a/src/screens/Messages/JoinRequests.tsx b/src/screens/Messages/JoinRequests.tsx index ffe3fb219e..d8d6ff9b06 100644 --- a/src/screens/Messages/JoinRequests.tsx +++ b/src/screens/Messages/JoinRequests.tsx @@ -1,10 +1,6 @@ import {useState} from 'react' import {View} from 'react-native' -import { - ChatBskyGroupApproveJoinRequest, - type ChatBskyGroupListJoinRequests, - ChatBskyGroupRejectJoinRequest, -} from '@atproto/api' +import {type ChatBskyGroupListJoinRequests} from '@atproto/api' import {Plural, Trans, useLingui} from '@lingui/react/macro' import {useNavigation} from '@react-navigation/native' import {type InfiniteData, useQueryClient} from '@tanstack/react-query' @@ -16,6 +12,7 @@ import { type NativeStackScreenProps, type NavigationProp, } from '#/lib/routes/types' +import {matchXrpcError} from '#/lib/xrpc-error' import {logger} from '#/logger' import {ConvoProvider, useConvo} from '#/state/messages/convo' import {ConvoStatus} from '#/state/messages/convo/types' @@ -43,6 +40,7 @@ import * as ProfileCard from '#/components/ProfileCard' import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' +import {chat} from '#/lexicons' import type * as bsky from '#/types/bsky' import {InviteLinkDialog} from './components/InviteLinkDialog' @@ -190,19 +188,18 @@ function JoinRequestsList({ let errorMessage = l`Failed to accept join request` if (isNetworkError(error)) { errorMessage = l`A network error occurred. Please check your internet connection.` - } else if ( - error instanceof ChatBskyGroupApproveJoinRequest.InvalidConvoError - ) { - errorMessage = l`Conversation not found.` - } else if ( - error instanceof ChatBskyGroupApproveJoinRequest.InsufficientRoleError - ) { - errorMessage = l`Only admins can accept join requests.` - } else if ( - error instanceof - ChatBskyGroupApproveJoinRequest.MemberLimitReachedError - ) { - errorMessage = l`The member limit has been reached.` + } else { + switch (matchXrpcError(error, chat.bsky.group.approveJoinRequest)) { + case 'InvalidConvo': + errorMessage = l`Conversation not found.` + break + case 'InsufficientRole': + errorMessage = l`Only admins can accept join requests.` + break + case 'MemberLimitReached': + errorMessage = l`The member limit has been reached.` + break + } } Toast.show(errorMessage, {type: 'error'}) }, @@ -223,14 +220,15 @@ function JoinRequestsList({ let errorMessage = l`Failed to reject join request` if (isNetworkError(error)) { errorMessage = l`A network error occurred. Please check your internet connection.` - } else if ( - error instanceof ChatBskyGroupRejectJoinRequest.InvalidConvoError - ) { - errorMessage = l`Conversation not found.` - } else if ( - error instanceof ChatBskyGroupRejectJoinRequest.InsufficientRoleError - ) { - errorMessage = l`Only admins can reject join requests.` + } else { + switch (matchXrpcError(error, chat.bsky.group.rejectJoinRequest)) { + case 'InvalidConvo': + errorMessage = l`Conversation not found.` + break + case 'InsufficientRole': + errorMessage = l`Only admins can reject join requests.` + break + } } Toast.show(errorMessage, {type: 'error'}) }, diff --git a/src/screens/Messages/components/OutgoingRequestListItem.tsx b/src/screens/Messages/components/OutgoingRequestListItem.tsx index b5dcdf08df..eb62f24b81 100644 --- a/src/screens/Messages/components/OutgoingRequestListItem.tsx +++ b/src/screens/Messages/components/OutgoingRequestListItem.tsx @@ -1,11 +1,9 @@ import {View} from 'react-native' -import { - type ChatBskyGroupDefs, - ChatBskyGroupWithdrawJoinRequest, -} from '@atproto/api' +import {type ChatBskyGroupDefs} from '@atproto/api' import {Trans, useLingui} from '@lingui/react/macro' import {isNetworkError} from '#/lib/strings/errors' +import {matchXrpcError} from '#/lib/xrpc-error' import {useWithdrawJoinGroupChatRequest} from '#/state/queries/messages/withdraw-join-group-chat' import {TimeElapsed} from '#/view/com/util/TimeElapsed' import {atoms as a, useTheme, web} from '#/alf' @@ -14,6 +12,7 @@ import {createStaticClick, Link} from '#/components/Link' import * as Prompt from '#/components/Prompt' import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' +import {chat} from '#/lexicons' export function OutgoingRequestListItem({ convo: convoView, @@ -35,8 +34,8 @@ export function OutgoingRequestListItem({ if (isNetworkError(error)) { errorMessage = l`There was a problem with your internet connection, please try again` } else if ( - error instanceof - ChatBskyGroupWithdrawJoinRequest.InvalidJoinRequestError + matchXrpcError(error, chat.bsky.group.withdrawJoinRequest) === + 'InvalidJoinRequest' ) { errorMessage = l`Invalid rescind request.` } diff --git a/src/state/queries/join-links.ts b/src/state/queries/join-links.ts index ca1db05558..e5dbbc9bed 100644 --- a/src/state/queries/join-links.ts +++ b/src/state/queries/join-links.ts @@ -1,17 +1,19 @@ import {useCallback} from 'react' import { type $Typed, - AtpAgent, ChatBskyGroupDefs, type ChatBskyGroupGetJoinLinkPreviews, } from '@atproto/api' +import {type Client} from '@atproto/lex' import {type QueryClient, useQuery, useQueryClient} from '@tanstack/react-query' -import {CHAT_SERVICE, DM_SERVICE_HEADERS} from '#/lib/constants' +import {CHAT_SERVICE} from '#/lib/constants' +import {createLexClient} from '#/lib/lexClient' import {logger} from '#/logger' import {STALE} from '#/state/queries/index' import {createQueryKey, type StructuredQueryKey} from '#/state/queries/util' -import {useAgent} from '#/state/session' +import {useChatClient} from '#/state/session' +import {chat} from '#/lexicons' /** * The three preview shapes we currently support. Excludes the `{$type: string}` @@ -154,23 +156,39 @@ export function invalidateJoinLinkPreviewsForConvo( }) } +let publicChatClient: Client | undefined + +/** + * The unauthenticated {@link Client} for the chat service, used to resolve join + * links before the viewer has a session. + * + * It talks to {@link CHAT_SERVICE} directly rather than proxying through a PDS, + * so no `atproto-proxy` header is involved. A single module-level instance, + * because there is no session to scope it to and reconstructing it per call + * would be waste. + * + * Unlike the public appview client this does NOT wrap `networkAwareFetch`, + * matching the plain-fetch behavior of the ad-hoc agent it replaces: a + * logged-out link resolution failing says nothing useful about the session's + * reachability, so it should not move the app-wide network signal. + */ +function getPublicChatClient(): Client { + return (publicChatClient ??= createLexClient({service: CHAT_SERVICE})) +} + async function fetchJoinLinkPreviews({ - agent, + client, codes, hasSession, }: { - agent: AtpAgent + client: Client codes: string[] hasSession: boolean }) { - const previewAgent = new AtpAgent({service: CHAT_SERVICE}) - const res = hasSession - ? await agent.chat.bsky.group.getJoinLinkPreviews( - {codes}, - {headers: DM_SERVICE_HEADERS}, - ) - : await previewAgent.chat.bsky.group.getJoinLinkPreviews({codes}) - return res.data + return await (hasSession ? client : getPublicChatClient()).call( + chat.bsky.group.getJoinLinkPreviews, + {codes}, + ) } export function useJoinLinkPreviewsQuery({ @@ -188,14 +206,14 @@ export function useJoinLinkPreviewsQuery({ */ initialData?: ChatBskyGroupGetJoinLinkPreviews.OutputSchema }) { - const agent = useAgent() + const client = useChatClient() return useQuery({ queryKey: createJoinLinkPreviewQueryKey({codes: codes ?? [], hasSession}), queryFn: async () => { if (!codes) throw new Error('No invite code') try { - return await fetchJoinLinkPreviews({agent, codes, hasSession}) + return await fetchJoinLinkPreviews({client, codes, hasSession}) } catch (error) { logger.error('Failed to fetch join link preview', {safeMessage: error}) throw error @@ -208,13 +226,13 @@ export function useJoinLinkPreviewsQuery({ } export function usePrefetchJoinLinkPreviews() { - const agent = useAgent() + const client = useChatClient() const queryClient = useQueryClient() return ({codes, hasSession}: {codes: string[]; hasSession: boolean}) => { return queryClient.prefetchQuery({ queryKey: createJoinLinkPreviewQueryKey({codes, hasSession}), - queryFn: () => fetchJoinLinkPreviews({agent, codes, hasSession}), + queryFn: () => fetchJoinLinkPreviews({client, codes, hasSession}), staleTime: STALE.SECONDS.FIFTEEN, }) } @@ -226,7 +244,7 @@ export function usePrefetchJoinLinkPreviews() { * Returns undefined if the preview can't be resolved. */ export function useGetJoinLinkPreview() { - const agent = useAgent() + const client = useChatClient() const queryClient = useQueryClient() return useCallback( @@ -241,7 +259,7 @@ export function useGetJoinLinkPreview() { const data = await queryClient.fetchQuery({ queryKey: createJoinLinkPreviewQueryKey({codes: [code], hasSession}), queryFn: () => - fetchJoinLinkPreviews({agent, codes: [code], hasSession}), + fetchJoinLinkPreviews({client, codes: [code], hasSession}), staleTime: STALE.SECONDS.FIFTEEN, }) const found = data.joinLinkPreviews[0] @@ -251,6 +269,6 @@ export function useGetJoinLinkPreview() { return undefined } }, - [agent, queryClient], + [client, queryClient], ) } diff --git a/src/state/queries/messages/add-group-members.ts b/src/state/queries/messages/add-group-members.ts index b5ddacbbff..bf4f8a8f4e 100644 --- a/src/state/queries/messages/add-group-members.ts +++ b/src/state/queries/messages/add-group-members.ts @@ -4,16 +4,17 @@ import { type ChatBskyConvoListConvos, type ChatBskyGroupAddMembers, } from '@atproto/api' +import {type DidString} from '@atproto/syntax' import { type InfiniteData, useMutation, useQueryClient, } from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import {logger} from '#/logger' import {useProfileQuery} from '#/state/queries/profile' -import {useAgent, useSession} from '#/state/session' +import {useChatClient, useSession} from '#/state/session' +import {chat} from '#/lexicons' import type * as bsky from '#/types/bsky' import {RQKEY as CONVO_KEY} from './conversation' import {RQKEY_ROOT as CONVO_LIST_KEY} from './list-conversations' @@ -30,7 +31,7 @@ export function useAddGroupMembers( }, ) { const queryClient = useQueryClient() - const agent = useAgent() + const client = useChatClient() const {currentAccount} = useSession() const {data: myProfile} = useProfileQuery({did: currentAccount?.did}) @@ -42,11 +43,11 @@ export function useAddGroupMembers( profiles: bsky.profile.AnyProfileView[] }) => { if (!convoId) throw new Error('No convoId provided') - const {data} = await agent.chat.bsky.group.addMembers( - {convoId, members}, - {headers: DM_SERVICE_HEADERS, encoding: 'application/json'}, - ) - return data + return await client.call(chat.bsky.group.addMembers, { + convoId, + // callers pass already-resolved actor dids + members: members as DidString[], + }) }, onMutate: ({profiles}) => { if (!convoId) return diff --git a/src/state/queries/messages/create-group-chat.ts b/src/state/queries/messages/create-group-chat.ts index 9f8aadc7d0..4a78a2be6a 100644 --- a/src/state/queries/messages/create-group-chat.ts +++ b/src/state/queries/messages/create-group-chat.ts @@ -1,9 +1,10 @@ import {type ChatBskyGroupCreateGroup} from '@atproto/api' +import {type DidString} from '@atproto/syntax' import {useMutation, useQueryClient} from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import {logger} from '#/logger' -import {useAgent} from '#/state/session' +import {useChatClient} from '#/state/session' +import {chat} from '#/lexicons' import {precacheConvoQuery} from './conversation' export function useCreateGroupChat({ @@ -14,16 +15,15 @@ export function useCreateGroupChat({ onError?: (error: Error) => void }) { const queryClient = useQueryClient() - const agent = useAgent() + const client = useChatClient() return useMutation({ mutationFn: async ({name, members}: {name: string; members: string[]}) => { - const {data} = await agent.chat.bsky.group.createGroup( - {name, members}, - {headers: DM_SERVICE_HEADERS}, - ) - - return data + return await client.call(chat.bsky.group.createGroup, { + name, + // callers pass already-resolved actor dids + members: members as DidString[], + }) }, onSuccess: data => { precacheConvoQuery(queryClient, data.convo) diff --git a/src/state/queries/messages/create-join-link.ts b/src/state/queries/messages/create-join-link.ts index fbac855466..57269b93e3 100644 --- a/src/state/queries/messages/create-join-link.ts +++ b/src/state/queries/messages/create-join-link.ts @@ -5,9 +5,9 @@ import { } from '@atproto/api' import {useMutation, useQueryClient} from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import {logger} from '#/logger' -import {useAgent} from '#/state/session' +import {useChatClient} from '#/state/session' +import {chat} from '#/lexicons' import { rollbackConvoOptimistic, updateConvoOptimistic, @@ -24,7 +24,7 @@ export function useCreateJoinLink( }, ) { const queryClient = useQueryClient() - const agent = useAgent() + const client = useChatClient() return useMutation({ mutationFn: async ({ @@ -35,11 +35,11 @@ export function useCreateJoinLink( requireApproval: boolean }) => { if (!convoId) throw new Error('No convoId provided') - const {data} = await agent.chat.bsky.group.createJoinLink( - {convoId, joinRule, requireApproval}, - {headers: DM_SERVICE_HEADERS, encoding: 'application/json'}, - ) - return data + return await client.call(chat.bsky.group.createJoinLink, { + convoId, + joinRule, + requireApproval, + }) }, onMutate: ({joinRule, requireApproval}) => { if (!convoId) return diff --git a/src/state/queries/messages/disable-join-link.ts b/src/state/queries/messages/disable-join-link.ts index c143feb6cc..b995dbcc6f 100644 --- a/src/state/queries/messages/disable-join-link.ts +++ b/src/state/queries/messages/disable-join-link.ts @@ -4,10 +4,10 @@ import { } from '@atproto/api' import {useMutation, useQueryClient} from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import {logger} from '#/logger' import {invalidateJoinLinkPreviewsForCode} from '#/state/queries/join-links' -import {useAgent} from '#/state/session' +import {useChatClient} from '#/state/session' +import {chat} from '#/lexicons' import { rollbackConvoOptimistic, updateConvoOptimistic, @@ -24,16 +24,12 @@ export function useDisableJoinLink( }, ) { const queryClient = useQueryClient() - const agent = useAgent() + const client = useChatClient() return useMutation({ mutationFn: async () => { if (!convoId) throw new Error('No convoId provided') - const {data} = await agent.chat.bsky.group.disableJoinLink( - {convoId}, - {headers: DM_SERVICE_HEADERS, encoding: 'application/json'}, - ) - return data + return await client.call(chat.bsky.group.disableJoinLink, {convoId}) }, onMutate: () => { if (!convoId) return diff --git a/src/state/queries/messages/edit-group-chat-name.ts b/src/state/queries/messages/edit-group-chat-name.ts index ef2a72f4ec..ecc73fd3d3 100644 --- a/src/state/queries/messages/edit-group-chat-name.ts +++ b/src/state/queries/messages/edit-group-chat-name.ts @@ -1,9 +1,9 @@ import {ChatBskyConvoDefs, type ChatBskyGroupEditGroup} from '@atproto/api' import {useMutation, useQueryClient} from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import {logger} from '#/logger' -import {useAgent} from '#/state/session' +import {useChatClient} from '#/state/session' +import {chat} from '#/lexicons' import { rollbackConvoOptimistic, updateConvoOptimistic, @@ -20,16 +20,15 @@ export function useEditGroupChatName( }, ) { const queryClient = useQueryClient() - const agent = useAgent() + const client = useChatClient() return useMutation({ mutationFn: async ({name: groupName}: {name: string}) => { if (!convoId) throw new Error('No convoId provided') - const {data} = await agent.chat.bsky.group.editGroup( - {convoId, name: groupName}, - {headers: DM_SERVICE_HEADERS, encoding: 'application/json'}, - ) - return data + return await client.call(chat.bsky.group.editGroup, { + convoId, + name: groupName, + }) }, onMutate: ({name: groupName}) => { if (!convoId) return diff --git a/src/state/queries/messages/edit-join-link.ts b/src/state/queries/messages/edit-join-link.ts index 7e34ce88d9..7870c1f7e3 100644 --- a/src/state/queries/messages/edit-join-link.ts +++ b/src/state/queries/messages/edit-join-link.ts @@ -5,9 +5,9 @@ import { } from '@atproto/api' import {useMutation, useQueryClient} from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import {logger} from '#/logger' -import {useAgent} from '#/state/session' +import {useChatClient} from '#/state/session' +import {chat} from '#/lexicons' import { rollbackConvoOptimistic, updateConvoOptimistic, @@ -24,7 +24,7 @@ export function useEditJoinLink( }, ) { const queryClient = useQueryClient() - const agent = useAgent() + const client = useChatClient() return useMutation({ mutationFn: async ({ @@ -35,11 +35,11 @@ export function useEditJoinLink( requireApproval: boolean }) => { if (!convoId) throw new Error('No convoId provided') - const {data} = await agent.chat.bsky.group.editJoinLink( - {convoId, joinRule, requireApproval}, - {headers: DM_SERVICE_HEADERS, encoding: 'application/json'}, - ) - return data + return await client.call(chat.bsky.group.editJoinLink, { + convoId, + joinRule, + requireApproval, + }) }, onMutate: ({joinRule, requireApproval}) => { if (!convoId) return diff --git a/src/state/queries/messages/enable-join-link.ts b/src/state/queries/messages/enable-join-link.ts index 4febc85441..ef3b168068 100644 --- a/src/state/queries/messages/enable-join-link.ts +++ b/src/state/queries/messages/enable-join-link.ts @@ -1,10 +1,10 @@ import {ChatBskyConvoDefs, type ChatBskyGroupEnableJoinLink} from '@atproto/api' import {useMutation, useQueryClient} from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import {logger} from '#/logger' import {invalidateJoinLinkPreviewsForCode} from '#/state/queries/join-links' -import {useAgent} from '#/state/session' +import {useChatClient} from '#/state/session' +import {chat} from '#/lexicons' import { rollbackConvoOptimistic, updateConvoOptimistic, @@ -21,16 +21,12 @@ export function useEnableJoinLink( }, ) { const queryClient = useQueryClient() - const agent = useAgent() + const client = useChatClient() return useMutation({ mutationFn: async () => { if (!convoId) throw new Error('No convoId provided') - const {data} = await agent.chat.bsky.group.enableJoinLink( - {convoId}, - {headers: DM_SERVICE_HEADERS, encoding: 'application/json'}, - ) - return data + return await client.call(chat.bsky.group.enableJoinLink, {convoId}) }, onMutate: () => { if (!convoId) return diff --git a/src/state/queries/messages/join-requests.ts b/src/state/queries/messages/join-requests.ts index 179dfafaaf..d36ebd60ae 100644 --- a/src/state/queries/messages/join-requests.ts +++ b/src/state/queries/messages/join-requests.ts @@ -4,15 +4,16 @@ import { type ChatBskyGroupListJoinRequests, type ChatBskyGroupRejectJoinRequest, } from '@atproto/api' +import {type DidString} from '@atproto/syntax' import { type InfiniteData, useMutation, useQueryClient, } from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import {logger} from '#/logger' -import {useAgent} from '#/state/session' +import {useChatClient} from '#/state/session' +import {chat} from '#/lexicons' import {listConvoMembersQueryKey} from './list-convo-members' import {createListJoinRequestsQueryKey} from './list-join-requests' @@ -34,21 +35,28 @@ export function useJoinRequestMutation( }, ) { const queryClient = useQueryClient() - const agent = useAgent() + const client = useChatClient() return useMutation({ mutationFn: async ({member}: {member: string}) => { if (!convoId) throw new Error('No convoId provided') - const {data} = + // callers pass an already-resolved actor did + const memberDid = member as DidString + const data = action === 'approve' - ? await agent.chat.bsky.group.approveJoinRequest( - {convoId, member}, - {headers: DM_SERVICE_HEADERS, encoding: 'application/json'}, - ) - : await agent.chat.bsky.group.rejectJoinRequest( - {convoId, member}, - {headers: DM_SERVICE_HEADERS, encoding: 'application/json'}, - ) + ? await client.call(chat.bsky.group.approveJoinRequest, { + convoId, + member: memberDid, + }) + : await client.call(chat.bsky.group.rejectJoinRequest, { + convoId, + member: memberDid, + }) + /* + * The two branches have different output schemas, so the union cannot be + * narrowed from the `action` value alone - the cast carries the generic's + * mapping through, exactly as the pre-migration code did. + */ return data as JoinRequestOutput }, onMutate: ({member}) => { diff --git a/src/state/queries/messages/list-join-requests.ts b/src/state/queries/messages/list-join-requests.ts index 64a999c118..bbb82eeca5 100644 --- a/src/state/queries/messages/list-join-requests.ts +++ b/src/state/queries/messages/list-join-requests.ts @@ -2,10 +2,10 @@ import {useEffect} from 'react' import {ChatBskyConvoDefs} from '@atproto/api' import {useInfiniteQuery, useQueryClient} from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import {useMessagesEventBus} from '#/state/messages/events' import {createQueryKey} from '#/state/queries/util' -import {useAgent} from '#/state/session' +import {useChatClient} from '#/state/session' +import {chat} from '#/lexicons' import {STALE} from '..' export const JOIN_REQUESTS_THRESHOLD = 20 @@ -22,7 +22,7 @@ export function useListJoinRequestsQuery({ convoId: string | undefined enabled?: boolean }) { - const agent = useAgent() + const client = useChatClient() const queryClient = useQueryClient() const messagesBus = useMessagesEventBus() const isEnabled = enabled !== false && !!convoId @@ -54,11 +54,12 @@ export function useListJoinRequestsQuery({ enabled: isEnabled, queryKey: createListJoinRequestsQueryKey({convoId: convoId ?? ''}), queryFn: async ({pageParam}) => { - const {data} = await agent.chat.bsky.group.listJoinRequests( - {convoId: convoId!, cursor: pageParam, limit: JOIN_REQUESTS_THRESHOLD}, - {headers: DM_SERVICE_HEADERS}, - ) - return data + return await client.call(chat.bsky.group.listJoinRequests, { + // guarded by `isEnabled` + convoId: convoId!, + cursor: pageParam, + limit: JOIN_REQUESTS_THRESHOLD, + }) }, initialPageParam: undefined as string | undefined, getNextPageParam: page => page.cursor, diff --git a/src/state/queries/messages/list-mutual-groups.ts b/src/state/queries/messages/list-mutual-groups.ts index 9ff4a1da09..65743a45e7 100644 --- a/src/state/queries/messages/list-mutual-groups.ts +++ b/src/state/queries/messages/list-mutual-groups.ts @@ -1,8 +1,9 @@ +import {type DidString} from '@atproto/syntax' import {useInfiniteQuery} from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import {createQueryKey} from '#/state/queries/util' -import {useAgent} from '#/state/session' +import {useChatClient} from '#/state/session' +import {chat} from '#/lexicons' const listMutualGroupsQueryKeyRoot = 'list-mutual-groups' @@ -18,7 +19,7 @@ export function useListMutualGroupsQuery({ enabled?: boolean limit?: number }) { - const agent = useAgent() + const client = useChatClient() const isEnabled = enabled !== false && !!subject return useInfiniteQuery({ @@ -27,11 +28,12 @@ export function useListMutualGroupsQuery({ enabled: isEnabled, queryKey: createListMutualGroupsQueryKey({subject: subject ?? ''}), queryFn: async ({pageParam}) => { - const {data} = await agent.chat.bsky.group.listMutualGroups( - {subject: subject!, cursor: pageParam, limit}, - {headers: DM_SERVICE_HEADERS}, - ) - return data + return await client.call(chat.bsky.group.listMutualGroups, { + // guarded by `enabled`, and callers pass a resolved actor did + subject: subject as DidString, + cursor: pageParam, + limit, + }) }, initialPageParam: undefined as string | undefined, getNextPageParam: page => page.cursor, diff --git a/src/state/queries/messages/mark-join-request-read.ts b/src/state/queries/messages/mark-join-request-read.ts index ae6d7e88a7..3dfd995af0 100644 --- a/src/state/queries/messages/mark-join-request-read.ts +++ b/src/state/queries/messages/mark-join-request-read.ts @@ -1,9 +1,9 @@ import {ChatBskyConvoDefs} from '@atproto/api' import {useMutation, useQueryClient} from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import {logger} from '#/logger' -import {useAgent} from '#/state/session' +import {useChatClient} from '#/state/session' +import {chat} from '#/lexicons' import {RQKEY as CONVO_KEY} from './conversation' import { type ConvoListQueryData, @@ -12,15 +12,12 @@ import { export function useMarkJoinRequestsRead(convoId: string | undefined) { const queryClient = useQueryClient() - const agent = useAgent() + const client = useChatClient() return useMutation({ mutationFn: async () => { if (!convoId) throw new Error('No convoId provided') - await agent.chat.bsky.group.updateJoinRequestsRead( - {convoId}, - {headers: DM_SERVICE_HEADERS, encoding: 'application/json'}, - ) + await client.call(chat.bsky.group.updateJoinRequestsRead, {convoId}) }, onMutate: () => { if (!convoId) return diff --git a/src/state/queries/messages/remove-from-group.ts b/src/state/queries/messages/remove-from-group.ts index 95f246be5a..10437ad7e9 100644 --- a/src/state/queries/messages/remove-from-group.ts +++ b/src/state/queries/messages/remove-from-group.ts @@ -4,15 +4,16 @@ import { type ChatBskyConvoListConvos, type ChatBskyGroupRemoveMembers, } from '@atproto/api' +import {type DidString} from '@atproto/syntax' import { type InfiniteData, useMutation, useQueryClient, } from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import {logger} from '#/logger' -import {useAgent} from '#/state/session' +import {useChatClient} from '#/state/session' +import {chat} from '#/lexicons' import {RQKEY as CONVO_KEY} from './conversation' import {RQKEY_ROOT as CONVO_LIST_KEY} from './list-conversations' import {listConvoMembersQueryKey} from './list-convo-members' @@ -28,16 +29,16 @@ export function useRemoveFromGroupChat( }, ) { const queryClient = useQueryClient() - const agent = useAgent() + const client = useChatClient() return useMutation({ mutationFn: async ({members}: {members: string[]}) => { if (!convoId) throw new Error('No convoId provided') - const {data} = await agent.chat.bsky.group.removeMembers( - {convoId, members}, - {headers: DM_SERVICE_HEADERS, encoding: 'application/json'}, - ) - return data + return await client.call(chat.bsky.group.removeMembers, { + convoId, + // callers pass already-resolved actor dids + members: members as DidString[], + }) }, onMutate: ({members}) => { if (!convoId) return diff --git a/src/state/queries/messages/request-join-group-chat.ts b/src/state/queries/messages/request-join-group-chat.ts index f4d5650346..b0a1922007 100644 --- a/src/state/queries/messages/request-join-group-chat.ts +++ b/src/state/queries/messages/request-join-group-chat.ts @@ -1,9 +1,9 @@ import {type ChatBskyGroupRequestJoin} from '@atproto/api' import {useMutation, useQueryClient} from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import {logger} from '#/logger' -import {useAgent, useSession} from '#/state/session' +import {useChatClient, useSession} from '#/state/session' +import {chat} from '#/lexicons' import {RQKEY_ROOT as REQUESTS_RQKEY_ROOT} from './list-conversation-requests' export function useRequestJoinGroupChat({ @@ -13,7 +13,7 @@ export function useRequestJoinGroupChat({ onSuccess?: (data: ChatBskyGroupRequestJoin.OutputSchema) => void onError?: (error: Error) => void } = {}) { - const agent = useAgent() + const client = useChatClient() const queryClient = useQueryClient() const {hasSession} = useSession() @@ -22,11 +22,7 @@ export function useRequestJoinGroupChat({ if (!hasSession) throw new Error('Must be logged in to join') if (!code) throw new Error('No invite code') - const res = await agent.chat.bsky.group.requestJoin( - {code}, - {headers: DM_SERVICE_HEADERS}, - ) - return res.data + return await client.call(chat.bsky.group.requestJoin, {code}) }, onSuccess: data => { void queryClient.invalidateQueries({queryKey: [REQUESTS_RQKEY_ROOT]}) diff --git a/src/state/queries/messages/withdraw-join-group-chat.ts b/src/state/queries/messages/withdraw-join-group-chat.ts index 2ede8c941d..736128ee25 100644 --- a/src/state/queries/messages/withdraw-join-group-chat.ts +++ b/src/state/queries/messages/withdraw-join-group-chat.ts @@ -1,9 +1,9 @@ import {type ChatBskyGroupWithdrawJoinRequest} from '@atproto/api' import {useMutation, useQueryClient} from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import {logger} from '#/logger' -import {useAgent, useSession} from '#/state/session' +import {useChatClient, useSession} from '#/state/session' +import {chat} from '#/lexicons' import { type ConvoRequestListQueryData, optimisticDeleteJoinRequest, @@ -17,7 +17,7 @@ export function useWithdrawJoinGroupChatRequest({ onSuccess?: (data: ChatBskyGroupWithdrawJoinRequest.OutputSchema) => void onError?: (error: Error) => void } = {}) { - const agent = useAgent() + const client = useChatClient() const queryClient = useQueryClient() const {hasSession} = useSession() @@ -27,11 +27,7 @@ export function useWithdrawJoinGroupChatRequest({ throw new Error('Must be logged in to withdraw a join request') if (!convoId) throw new Error('No convoId provided') - const res = await agent.chat.bsky.group.withdrawJoinRequest( - {convoId}, - {headers: DM_SERVICE_HEADERS}, - ) - return res.data + return await client.call(chat.bsky.group.withdrawJoinRequest, {convoId}) }, onSuccess: (data, {convoId}) => { queryClient.setQueriesData( From 4aae6b2c22a6fdd7459cfbbbee0de9a58e10ef6e Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 13 Aug 2026 22:26:16 +0300 Subject: [PATCH 12/32] [SDK] Migrate search, bookmarks, contacts and feed queries to the lex clients (#11360) Co-authored-by: Claude Fable 5 --- .../Autocomplete/useAutocomplete/index.ts | 9 +- .../contacts/screens/PhoneInput.tsx | 40 ++--- .../contacts/screens/VerifyNumber.tsx | 139 +++++++++--------- .../contacts/screens/ViewMatches.tsx | 9 +- src/screens/Settings/FindContactsSettings.tsx | 29 ++-- src/state/feed-feedback.tsx | 35 +++-- src/state/queries/actor-autocomplete.ts | 19 +-- src/state/queries/actor-search.ts | 17 +-- .../queries/bookmarks/useBookmarkMutation.ts | 14 +- .../queries/bookmarks/useBookmarksQuery.ts | 52 ++++--- src/state/queries/feed.ts | 124 +++++++++------- src/state/queries/find-contacts.ts | 41 +++--- src/state/queries/pds-detection.ts | 23 ++- src/state/queries/resolve-uri.ts | 28 ++-- src/view/com/auth/LoggedOut.tsx | 13 +- src/view/com/composer/drafts/state/queries.ts | 45 ++++-- 16 files changed, 350 insertions(+), 287 deletions(-) diff --git a/src/components/Autocomplete/useAutocomplete/index.ts b/src/components/Autocomplete/useAutocomplete/index.ts index 498cc766cd..1dc3100f37 100644 --- a/src/components/Autocomplete/useAutocomplete/index.ts +++ b/src/components/Autocomplete/useAutocomplete/index.ts @@ -6,13 +6,14 @@ import {isJustAMute, moduiContainsHideableOffense} from '#/lib/moderation' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {STALE} from '#/state/queries' import {DEFAULT_LOGGED_OUT_PREFERENCES} from '#/state/queries/preferences' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' import { type AutocompleteApi, type AutocompleteItem, type AutocompleteItemType, type AutocompleteProfile, } from '#/components/Autocomplete/types' +import {app} from '#/lexicons' import {useEmojiSearch} from './useEmojiSearch' const DEFAULT_MOD_OPTS = { @@ -31,7 +32,7 @@ export function useAutocomplete({ limit?: number showSearchFallback?: boolean }): AutocompleteApi { - const agent = useAgent() + const client = useAppviewClient() const moderationOpts = useModerationOpts() const emojiSearch = useEmojiSearch() @@ -52,12 +53,12 @@ export function useAutocomplete({ // Going from "foo" to "foo." should not clear matches. q = q.toLowerCase().trim().replace(/\.$/, '') - const res = await agent.searchActorsTypeahead({ + const data = await client.call(app.bsky.actor.searchActorsTypeahead, { q, limit: limit || 8, }) - return (res?.data.actors || []).map(profile => ({ + return (data?.actors || []).map(profile => ({ key: profile.did, type: 'profile' as const, value: '@' + profile.handle, diff --git a/src/components/contacts/screens/PhoneInput.tsx b/src/components/contacts/screens/PhoneInput.tsx index 6904789a6e..95d63055c0 100644 --- a/src/components/contacts/screens/PhoneInput.tsx +++ b/src/components/contacts/screens/PhoneInput.tsx @@ -2,7 +2,6 @@ import {useState} from 'react' import {Keyboard, View} from 'react-native' import {KeyboardAvoidingView} from 'react-native-keyboard-controller' import {useSafeAreaInsets} from 'react-native-safe-area-context' -import {AppBskyContactStartPhoneVerification} from '@atproto/api' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' @@ -14,8 +13,9 @@ import { getDefaultCountry, } from '#/lib/international-telephone-codes' import {cleanError, isNetworkError} from '#/lib/strings/errors' +import {matchXrpcError} from '#/lib/xrpc-error' import {logger} from '#/logger' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' import {OnboardingPosition} from '#/screens/Onboarding/Layout' import { android, @@ -34,6 +34,7 @@ import {Loader} from '#/components/Loader' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' import {useGeolocation} from '#/geolocation' +import {app} from '#/lexicons' import {isFindContactsFeatureEnabled} from '../country-allowlist' import { constructFullPhoneNumber, @@ -56,7 +57,7 @@ export function PhoneInput({ const {_} = useLingui() const ax = useAnalytics() const t = useTheme() - const agent = useAgent() + const client = useAppviewClient() const location = useGeolocation() const [countryCode, setCountryCode] = useState( () => state.phoneCountryCode ?? getDefaultCountry(location), @@ -78,7 +79,7 @@ export function PhoneInput({ phoneNumber: string }) => { // sends a onetime code to the user's phone number - await agent.app.bsky.contact.startPhoneVerification({ + await client.call(app.bsky.contact.startPhoneVerification, { phone: constructFullPhoneNumber(phoneCountryCode, phoneNumber), }) }, @@ -102,23 +103,22 @@ export function PhoneInput({ msg`A network error occurred. Please check your internet connection`, ), ) - } else if ( - err instanceof - AppBskyContactStartPhoneVerification.RateLimitExceededError - ) { - setError(_(msg`Rate limit exceeded. Please try again later.`)) - } else if ( - err instanceof AppBskyContactStartPhoneVerification.InvalidPhoneError - ) { - setError( - _( - msg`The verification provider was unable to send a code to your phone number. Please check your phone number and try again.`, - ), - ) - } else { - logger.error('Verify phone number failed', {safeMessage: err}) - setError(_(msg`An error occurred. ${cleanError(err)}`)) + return } + switch (matchXrpcError(err, app.bsky.contact.startPhoneVerification)) { + case 'RateLimitExceeded': + setError(_(msg`Rate limit exceeded. Please try again later.`)) + return + case 'InvalidPhone': + setError( + _( + msg`The verification provider was unable to send a code to your phone number. Please check your phone number and try again.`, + ), + ) + return + } + logger.error('Verify phone number failed', {safeMessage: err}) + setError(_(msg`An error occurred. ${cleanError(err)}`)) }, }) diff --git a/src/components/contacts/screens/VerifyNumber.tsx b/src/components/contacts/screens/VerifyNumber.tsx index f9d062f691..0bd5369397 100644 --- a/src/components/contacts/screens/VerifyNumber.tsx +++ b/src/components/contacts/screens/VerifyNumber.tsx @@ -1,9 +1,5 @@ import {useEffect, useMemo, useState} from 'react' import {Text as NestedText, View} from 'react-native' -import { - AppBskyContactStartPhoneVerification, - AppBskyContactVerifyPhone, -} from '@atproto/api' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' @@ -11,8 +7,9 @@ import {useMutation} from '@tanstack/react-query' import {clamp} from '#/lib/numbers' import {cleanError, isNetworkError} from '#/lib/strings/errors' +import {matchXrpcError} from '#/lib/xrpc-error' import {logger} from '#/logger' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' import {OnboardingPosition} from '#/screens/Onboarding/Layout' import {atoms as a, useGutters, useTheme} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' @@ -25,6 +22,7 @@ import {Loader} from '#/components/Loader' import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' +import {app} from '#/lexicons' import {OTPInput} from '../components/OTPInput' import {constructFullPhoneNumber, prettyPhoneNumber} from '../phone-number' import {type Action, type State, useOnPressBackButton} from '../state' @@ -43,7 +41,7 @@ export function VerifyNumber({ const t = useTheme() const {_} = useLingui() const ax = useAnalytics() - const agent = useAgent() + const client = useAppviewClient() const gutters = useGutters([0, 'wide']) const [otpCode, setOtpCode] = useState('') @@ -72,8 +70,11 @@ export function VerifyNumber({ isSuccess, } = useMutation({ mutationFn: async (code: string) => { - const res = await agent.app.bsky.contact.verifyPhone({code, phone}) - return res.data.token + const data = await client.call(app.bsky.contact.verifyPhone, { + code, + phone, + }) + return data.token }, onSuccess: async token => { // let the success state show for a moment @@ -99,44 +100,47 @@ export function VerifyNumber({ msg`A network error occurred. Please check your internet connection.`, ), }) - } else if (err instanceof AppBskyContactVerifyPhone.InvalidCodeError) { - setError({ - retryable: true, - isResendError: true, - message: _(msg`This code is invalid. Resend to get a new code.`), - }) - } else if (err instanceof AppBskyContactVerifyPhone.InvalidPhoneError) { - setError({ - retryable: false, - isResendError: false, - message: _( - msg`The verification provider was unable to send a code to your phone number. Please check your phone number and try again.`, - ), - }) - } else if ( - err instanceof AppBskyContactVerifyPhone.RateLimitExceededError - ) { - setError({ - retryable: true, - isResendError: false, - message: _( - msg`Too many attempts. Please wait a few minutes and try again.`, - ), - }) - } else { - logger.error('Verify phone number failed', {safeMessage: err}) - setError({ - retryable: true, - isResendError: false, - message: _(msg`An error occurred. ${cleanError(err)}`), - }) + return } + switch (matchXrpcError(err, app.bsky.contact.verifyPhone)) { + case 'InvalidCode': + setError({ + retryable: true, + isResendError: true, + message: _(msg`This code is invalid. Resend to get a new code.`), + }) + return + case 'InvalidPhone': + setError({ + retryable: false, + isResendError: false, + message: _( + msg`The verification provider was unable to send a code to your phone number. Please check your phone number and try again.`, + ), + }) + return + case 'RateLimitExceeded': + setError({ + retryable: true, + isResendError: false, + message: _( + msg`Too many attempts. Please wait a few minutes and try again.`, + ), + }) + return + } + logger.error('Verify phone number failed', {safeMessage: err}) + setError({ + retryable: true, + isResendError: false, + message: _(msg`An error occurred. ${cleanError(err)}`), + }) }, }) const {mutate: resendCode, isPending: isResendingCode} = useMutation({ mutationFn: async () => { - await agent.app.bsky.contact.startPhoneVerification({phone: phone}) + await client.call(app.bsky.contact.startPhoneVerification, {phone: phone}) }, onSuccess: () => { dispatch({type: 'RESEND_VERIFICATION_CODE'}) @@ -155,35 +159,34 @@ export function VerifyNumber({ msg`A network error occurred. Please check your internet connection.`, ), }) - } else if ( - err instanceof AppBskyContactStartPhoneVerification.InvalidPhoneError - ) { - setError({ - retryable: false, - isResendError: true, - message: _( - msg`The verification provider was unable to send a code to your phone number. Please check your phone number and try again.`, - ), - }) - } else if ( - err instanceof - AppBskyContactStartPhoneVerification.RateLimitExceededError - ) { - setError({ - retryable: true, - isResendError: true, - message: _( - msg`Too many codes sent. Please wait a few minutes and try again.`, - ), - }) - } else { - logger.error('Resend failed', {safeMessage: err}) - setError({ - retryable: true, - isResendError: true, - message: _(msg`An error occurred. ${cleanError(err)}`), - }) + return } + switch (matchXrpcError(err, app.bsky.contact.startPhoneVerification)) { + case 'InvalidPhone': + setError({ + retryable: false, + isResendError: true, + message: _( + msg`The verification provider was unable to send a code to your phone number. Please check your phone number and try again.`, + ), + }) + return + case 'RateLimitExceeded': + setError({ + retryable: true, + isResendError: true, + message: _( + msg`Too many codes sent. Please wait a few minutes and try again.`, + ), + }) + return + } + logger.error('Resend failed', {safeMessage: err}) + setError({ + retryable: true, + isResendError: true, + message: _(msg`An error occurred. ${cleanError(err)}`), + }) }, }) diff --git a/src/components/contacts/screens/ViewMatches.tsx b/src/components/contacts/screens/ViewMatches.tsx index 3543c604f5..dab3a79c6b 100644 --- a/src/components/contacts/screens/ViewMatches.tsx +++ b/src/components/contacts/screens/ViewMatches.tsx @@ -3,6 +3,7 @@ import {View} from 'react-native' import {useSafeAreaInsets} from 'react-native-safe-area-context' import * as SMS from 'expo-sms' import {type ModerationOpts} from '@atproto/api' +import {type DidString} from '@atproto/syntax' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Plural, Trans} from '@lingui/react/macro' @@ -20,7 +21,7 @@ import { optimisticRemoveMatch, useMatchesPassthroughQuery, } from '#/state/queries/find-contacts' -import {useAgent, useSession} from '#/state/session' +import {useAgent, useAppviewClient, useSession} from '#/state/session' import {List, type ListMethods} from '#/view/com/util/List' import {UserAvatar} from '#/view/com/util/UserAvatar' import {OnboardingPosition} from '#/screens/Onboarding/Layout' @@ -41,6 +42,7 @@ import * as ProfileCard from '#/components/ProfileCard' import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' +import {app} from '#/lexicons' import type * as bsky from '#/types/bsky' import {InviteInfo} from '../components/InviteInfo' import {type Action, type Contact, type Match, type State} from '../state' @@ -90,6 +92,7 @@ export function ViewMatches({ const moderationOpts = useModerationOpts() const queryClient = useQueryClient() const agent = useAgent() + const client = useAppviewClient() const insets = useSafeAreaInsets() const listRef = useRef(null) @@ -218,7 +221,9 @@ export function ViewMatches({ const {mutate: dismissMatch} = useMutation({ mutationFn: async (did: string) => { - await agent.app.bsky.contact.dismissMatch({subject: did}) + await client.call(app.bsky.contact.dismissMatch, { + subject: did as DidString, + }) }, onMutate: did => { ax.metric('contacts:matches:dismiss', {entryPoint: context}) diff --git a/src/screens/Settings/FindContactsSettings.tsx b/src/screens/Settings/FindContactsSettings.tsx index 57f1edde90..6104fd12b7 100644 --- a/src/screens/Settings/FindContactsSettings.tsx +++ b/src/screens/Settings/FindContactsSettings.tsx @@ -1,11 +1,8 @@ import {useCallback, useEffect, useState} from 'react' import {type ListRenderItemInfo, View} from 'react-native' import * as Contacts from 'expo-contacts' -import { - type AppBskyContactDefs, - type AppBskyContactGetSyncStatus, - type ModerationOpts, -} from '@atproto/api' +import {type AppBskyContactDefs, type ModerationOpts} from '@atproto/api' +import {type DidString} from '@atproto/syntax' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Plural, Trans} from '@lingui/react/macro' @@ -32,7 +29,7 @@ import { useContactsMatchesQuery, useContactsSyncStatusQuery, } from '#/state/queries/find-contacts' -import {useAgent, useSession} from '#/state/session' +import {useAgent, useAppviewClient, useSession} from '#/state/session' import {ErrorScreen} from '#/view/com/util/error/ErrorScreen' import {List} from '#/view/com/util/List' import {atoms as a, tokens, useGutters, useTheme} from '#/alf' @@ -52,6 +49,7 @@ import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' import {IS_NATIVE} from '#/env' import {InviteFriendsDialog} from '#/features/inviteFriends' +import {app} from '#/lexicons' import type * as bsky from '#/types/bsky' import {bulkWriteFollows} from '../Onboarding/util' @@ -194,7 +192,7 @@ function SyncStatus({ refetchStatus: () => Promise }) { const ax = useAnalytics() - const agent = useAgent() + const client = useAppviewClient() const queryClient = useQueryClient() const {_} = useLingui() const moderationOpts = useModerationOpts() @@ -219,7 +217,9 @@ function SyncStatus({ const {mutate: dismissMatch} = useMutation({ mutationFn: async (did: string) => { - await agent.app.bsky.contact.dismissMatch({subject: did}) + await client.call(app.bsky.contact.dismissMatch, { + subject: did as DidString, + }) }, onMutate: async (did: string) => { ax.metric('contacts:settings:dismiss', {}) @@ -371,6 +371,7 @@ function StatusHeader({ const {_} = useLingui() const ax = useAnalytics() const agent = useAgent() + const client = useAppviewClient() const queryClient = useQueryClient() const {currentAccount} = useSession() @@ -384,12 +385,12 @@ function StatusHeader({ let cursor: string | undefined do { - const page = await agent.app.bsky.contact.getMatches({ + const page = await client.call(app.bsky.contact.getMatches, { limit: 100, cursor, }) - cursor = page.data.cursor - for (const profile of page.data.matches) { + cursor = page.cursor + for (const profile of page.matches) { if ( profile.did !== currentAccount?.did && !isBlockedOrBlocking(profile) && @@ -487,17 +488,17 @@ function StatusFooter({syncedAt}: {syncedAt: string}) { const {_, i18n} = useLingui() const t = useTheme() const ax = useAnalytics() - const agent = useAgent() + const client = useAppviewClient() const queryClient = useQueryClient() const {mutate: removeData, isPending} = useMutation({ mutationFn: async () => { - await agent.app.bsky.contact.removeData({}) + await client.call(app.bsky.contact.removeData, {}) }, onMutate: () => ax.metric('contacts:settings:removeData', {}), onSuccess: () => { Toast.show(_(msg`Contacts removed`)) - queryClient.setQueryData( + queryClient.setQueryData( findContactsStatusQueryKey, {syncStatus: undefined}, ) diff --git a/src/state/feed-feedback.tsx b/src/state/feed-feedback.tsx index 15c12e732d..dbd10b3c9d 100644 --- a/src/state/feed-feedback.tsx +++ b/src/state/feed-feedback.tsx @@ -8,6 +8,7 @@ import { } from 'react' import {AppState, type AppStateStatus} from 'react-native' import {type AppBskyFeedDefs} from '@atproto/api' +import {type AtUriString, type DidString} from '@atproto/syntax' import throttle from 'lodash.throttle' import {PROD_FEEDS, STAGING_FEEDS} from '#/lib/constants' @@ -22,7 +23,8 @@ import { } from '#/state/queries/post-feed' import {getItemsForFeedback} from '#/view/com/posts/PostFeed' import {useAnalytics} from '#/analytics' -import {useAgent} from './session' +import {app} from '#/lexicons' +import {useAppviewClient} from './session' export const FEEDBACK_FEEDS = [...PROD_FEEDS, ...STAGING_FEEDS] @@ -65,7 +67,7 @@ export function useFeedFeedback( ) { const ax = useAnalytics() const logger = ax.logger.useChild(ax.logger.Context.FeedFeedback) - const agent = useAgent() + const client = useAppviewClient() const feed = !!feedSourceInfo && isFeedSourceFeedInfo(feedSourceInfo) @@ -150,16 +152,19 @@ export function useFeedFeedback( return } - // Send to the feed - agent.app.bsky.feed - .sendInteractions( - {interactions: interactionsToSend, feed: feed?.uri}, + /* + * Send to the feed. Interactions go to the feed generator rather than the + * appview, which the agent did by setting `atproto-proxy` by hand; the + * client's per-call `service` option writes that same header. + */ + client + .call( + app.bsky.feed.sendInteractions, { - encoding: 'application/json', - headers: { - 'atproto-proxy': `${proxyDid}#bsky_fg`, - }, + interactions: interactionsToSend, + feed: feed?.uri as AtUriString | undefined, }, + {service: `${proxyDid as DidString}#bsky_fg`}, ) .catch(() => {}) // ignore upstream errors @@ -172,7 +177,7 @@ export function useFeedFeedback( ) throttledFlushAggregatedStats() logger.debug('flushed') - }, [agent, throttledFlushAggregatedStats, proxyDid, enabled, feed]) + }, [client, throttledFlushAggregatedStats, proxyDid, enabled, feed]) const sendToFeed = useMemo( () => @@ -283,9 +288,13 @@ function toString(interaction: AppBskyFeedDefs.Interaction): string { }|${interaction.reqId || ''}` } -function toInteraction(str: string): AppBskyFeedDefs.Interaction { +function toInteraction(str: string): app.bsky.feed.defs.Interaction { const [item, event, feedContext, reqId] = str.split('|') - return {item, event, feedContext, reqId} + /* + * The fields come from splitting an internally-built key, so neither the + * at-uri nor the event token is narrowed by the compiler here. + */ + return {item, event, feedContext, reqId} as app.bsky.feed.defs.Interaction } type AggregatedStats = { diff --git a/src/state/queries/actor-autocomplete.ts b/src/state/queries/actor-autocomplete.ts index c8acb39f96..14377c5347 100644 --- a/src/state/queries/actor-autocomplete.ts +++ b/src/state/queries/actor-autocomplete.ts @@ -9,7 +9,8 @@ import {keepPreviousData, useQuery, useQueryClient} from '@tanstack/react-query' import {isJustAMute, moduiContainsHideableOffense} from '#/lib/moderation' import {logger} from '#/logger' import {STALE} from '#/state/queries' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' +import {app} from '#/lexicons' import {useModerationOpts} from '../preferences/moderation-opts' import {DEFAULT_LOGGED_OUT_PREFERENCES} from './preferences' @@ -27,7 +28,7 @@ export function useActorAutocompleteQuery( limit?: number, ) { const moderationOpts = useModerationOpts() - const agent = useAgent() + const client = useAppviewClient() prefix = prefix.toLowerCase().trim() if (prefix.endsWith('.')) { @@ -39,13 +40,13 @@ export function useActorAutocompleteQuery( staleTime: STALE.MINUTES.ONE, queryKey: RQKEY(prefix || ''), async queryFn() { - const res = prefix - ? await agent.searchActorsTypeahead({ + const data = prefix + ? await client.call(app.bsky.actor.searchActorsTypeahead, { q: prefix, limit: limit || 8, }) : undefined - return res?.data.actors || [] + return data?.actors || [] }, select: useCallback( (data: AppBskyActorDefs.ProfileViewBasic[]) => { @@ -65,7 +66,7 @@ export type ActorAutocompleteFn = ReturnType export function useActorAutocompleteFn() { const queryClient = useQueryClient() const moderationOpts = useModerationOpts() - const agent = useAgent() + const client = useAppviewClient() return useCallback( async ({query, limit = 8}: {query: string; limit?: number}) => { @@ -77,7 +78,7 @@ export function useActorAutocompleteFn() { staleTime: STALE.MINUTES.ONE, queryKey: RQKEY(query || ''), queryFn: () => - agent.searchActorsTypeahead({ + client.call(app.bsky.actor.searchActorsTypeahead, { q: query, limit, }), @@ -91,11 +92,11 @@ export function useActorAutocompleteFn() { return computeSuggestions({ q: query, - searched: res?.data.actors, + searched: res?.actors, moderationOpts: moderationOpts || DEFAULT_MOD_OPTS, }) }, - [queryClient, moderationOpts, agent], + [queryClient, moderationOpts, client], ) } diff --git a/src/state/queries/actor-search.ts b/src/state/queries/actor-search.ts index 84e6d88f29..547bf39b0f 100644 --- a/src/state/queries/actor-search.ts +++ b/src/state/queries/actor-search.ts @@ -1,4 +1,3 @@ -import {type AppBskyActorSearchActors} from '@atproto/api' import { type InfiniteData, keepPreviousData, @@ -8,7 +7,8 @@ import { } from '@tanstack/react-query' import {STALE} from '#/state/queries' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' +import {app} from '#/lexicons' export const RQKEY_ROOT = 'actor-search' export const RQKEY = (query: string, limit?: number) => [ @@ -28,23 +28,22 @@ export function useActorSearch({ maintainData?: boolean limit?: number }) { - const agent = useAgent() + const client = useAppviewClient() return useInfiniteQuery< - AppBskyActorSearchActors.OutputSchema, + app.bsky.actor.searchActors.$OutputBody, Error, - InfiniteData, + InfiniteData, QueryKey, string | undefined >({ staleTime: STALE.MINUTES.FIVE, queryKey: RQKEY(query, limit), queryFn: async ({pageParam}) => { - const res = await agent.searchActors({ + return await client.call(app.bsky.actor.searchActors, { q: query, limit, cursor: pageParam, }) - return res.data }, enabled: enabled && !!query, initialPageParam: undefined, @@ -54,7 +53,7 @@ export function useActorSearch({ }) } -function select(data: InfiniteData) { +function select(data: InfiniteData) { // enforce uniqueness const dids = new Set() @@ -77,7 +76,7 @@ export function* findAllProfilesInQueryData( did: string, ) { const queryDatas = queryClient.getQueriesData< - InfiniteData + InfiniteData >({ queryKey: [RQKEY_ROOT], }) diff --git a/src/state/queries/bookmarks/useBookmarkMutation.ts b/src/state/queries/bookmarks/useBookmarkMutation.ts index c6e745aa04..af5a4a2d8d 100644 --- a/src/state/queries/bookmarks/useBookmarkMutation.ts +++ b/src/state/queries/bookmarks/useBookmarkMutation.ts @@ -1,4 +1,5 @@ import {type AppBskyFeedDefs} from '@atproto/api' +import {type AtUriString} from '@atproto/syntax' import {useMutation, useQueryClient} from '@tanstack/react-query' import {isNetworkError} from '#/lib/strings/errors' @@ -8,7 +9,8 @@ import { optimisticallyDeleteBookmark, optimisticallySaveBookmark, } from '#/state/queries/bookmarks/useBookmarksQuery' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' +import {app} from '#/lexicons' type MutationArgs = | {action: 'create'; post: AppBskyFeedDefs.PostView} @@ -23,20 +25,20 @@ type MutationArgs = export function useBookmarkMutation() { const qc = useQueryClient() - const agent = useAgent() + const client = useAppviewClient() return useMutation({ async mutationFn(args: MutationArgs) { if (args.action === 'create') { updatePostShadow(qc, args.post.uri, {bookmarked: true}) - await agent.app.bsky.bookmark.createBookmark({ - uri: args.post.uri, + await client.call(app.bsky.bookmark.createBookmark, { + uri: args.post.uri as AtUriString, cid: args.post.cid, }) } else if (args.action === 'delete') { updatePostShadow(qc, args.uri, {bookmarked: false}) - await agent.app.bsky.bookmark.deleteBookmark({ - uri: args.uri, + await client.call(app.bsky.bookmark.deleteBookmark, { + uri: args.uri as AtUriString, }) } }, diff --git a/src/state/queries/bookmarks/useBookmarksQuery.ts b/src/state/queries/bookmarks/useBookmarksQuery.ts index 3e8e87a132..14323625f9 100644 --- a/src/state/queries/bookmarks/useBookmarksQuery.ts +++ b/src/state/queries/bookmarks/useBookmarksQuery.ts @@ -1,9 +1,4 @@ -import { - type $Typed, - type AppBskyBookmarkGetBookmarks, - AppBskyFeedDefs, - AtUri, -} from '@atproto/api' +import {type $Typed, AppBskyFeedDefs, AtUri} from '@atproto/api' import { type InfiniteData, type QueryClient, @@ -16,28 +11,28 @@ import { embedViewRecordToPostView, getEmbeddedPost, } from '#/state/queries/util' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' +import {app} from '#/lexicons' import * as bsky from '#/types/bsky' export const bookmarksQueryKeyRoot = 'bookmarks' export const createBookmarksQueryKey = () => [bookmarksQueryKeyRoot] export function useBookmarksQuery() { - const agent = useAgent() + const client = useAppviewClient() return useInfiniteQuery< - AppBskyBookmarkGetBookmarks.OutputSchema, + app.bsky.bookmark.getBookmarks.$OutputBody, Error, - InfiniteData, + InfiniteData, QueryKey, string | undefined >({ queryKey: createBookmarksQueryKey(), async queryFn({pageParam}) { - const res = await agent.app.bsky.bookmark.getBookmarks({ + return await client.call(app.bsky.bookmark.getBookmarks, { cursor: pageParam, }) - return res.data }, initialPageParam: undefined, getNextPageParam: lastPage => lastPage.cursor, @@ -45,7 +40,7 @@ export function useBookmarksQuery() { } export async function truncateAndInvalidate(qc: QueryClient) { - qc.setQueriesData>( + qc.setQueriesData>( {queryKey: [bookmarksQueryKeyRoot]}, data => { if (data) { @@ -64,7 +59,7 @@ export async function optimisticallySaveBookmark( qc: QueryClient, post: AppBskyFeedDefs.PostView, ) { - qc.setQueriesData>( + qc.setQueriesData>( { queryKey: [bookmarksQueryKeyRoot], }, @@ -75,19 +70,22 @@ export async function optimisticallySaveBookmark( pages: data.pages.map((page, index) => { if (index === 0) { post.$type = 'app.bsky.feed.defs#postView' + /* + * The optimistic entry is synthesized from an `@atproto/api` + * `PostView`, whose string fields are unbranded, so it is asserted + * to the vendored view type the query data is now keyed on. + */ + const bookmark = { + createdAt: new Date().toISOString(), + subject: { + uri: post.uri, + cid: post.cid, + }, + item: post as $Typed, + } as unknown as app.bsky.bookmark.defs.BookmarkView return { ...page, - bookmarks: [ - { - createdAt: new Date().toISOString(), - subject: { - uri: post.uri, - cid: post.cid, - }, - item: post as $Typed, - }, - ...page.bookmarks, - ], + bookmarks: [bookmark, ...page.bookmarks], } } return page @@ -101,7 +99,7 @@ export async function optimisticallyDeleteBookmark( qc: QueryClient, {uri}: {uri: string}, ) { - qc.setQueriesData>( + qc.setQueriesData>( { queryKey: [bookmarksQueryKeyRoot], }, @@ -125,7 +123,7 @@ export function* findAllPostsInQueryData( uri: string, ): Generator { const queryDatas = queryClient.getQueriesData< - InfiniteData + InfiniteData >({ queryKey: [bookmarksQueryKeyRoot], }) diff --git a/src/state/queries/feed.ts b/src/state/queries/feed.ts index c3ac1acd53..9a9a8c09b9 100644 --- a/src/state/queries/feed.ts +++ b/src/state/queries/feed.ts @@ -3,11 +3,11 @@ import { type AppBskyActorDefs, type AppBskyFeedDefs, type AppBskyGraphDefs, - type AppBskyUnspeccedGetPopularFeedGenerators, AtUri, moderateFeedGenerator, RichText, } from '@atproto/api' +import {type AtUriString} from '@atproto/syntax' import {t} from '@lingui/core/macro' import { type InfiniteData, @@ -26,7 +26,8 @@ import {GCTIME, STALE} from '#/state/queries' import {RQKEY as listQueryKey} from '#/state/queries/list' import {usePreferencesQuery} from '#/state/queries/preferences' import {createQueryKey} from '#/state/queries/util' -import {useAgent, useSession} from '#/state/session' +import {useAppviewClient, useSession} from '#/state/session' +import {app} from '#/lexicons' import {router} from '#/routes' import {useModerationOpts} from '../preferences/moderation-opts' import {type FeedDescriptor} from './post-feed' @@ -184,7 +185,7 @@ export function getAvatarTypeFromUri(uri: string) { export function useFeedSourceInfoQuery({uri}: {uri: string}) { const type = getFeedTypeFromUri(uri) - const agent = useAgent() + const client = useAppviewClient() return useQuery({ staleTime: STALE.INFINITY, @@ -193,14 +194,16 @@ export function useFeedSourceInfoQuery({uri}: {uri: string}) { let view: FeedSourceInfo if (type === 'feed') { - const res = await agent.app.bsky.feed.getFeedGenerator({feed: uri}) - view = hydrateFeedGenerator(res.data.view) + const data = await client.call(app.bsky.feed.getFeedGenerator, { + feed: uri as AtUriString, + }) + view = hydrateFeedGenerator(data.view) } else { - const res = await agent.app.bsky.graph.getList({ - list: uri, + const data = await client.call(app.bsky.graph.getList, { + list: uri as AtUriString, limit: 1, }) - view = hydrateList(res.data.list) + view = hydrateList(data.list) } return view @@ -234,7 +237,7 @@ export function createGetPopularFeedsQueryKey( export function useGetPopularFeedsQuery(options?: GetPopularFeedsOptions) { const {hasSession} = useSession() - const agent = useAgent() + const client = useAppviewClient() const limit = options?.limit || 10 const {data: preferences} = usePreferencesQuery() const queryClient = useQueryClient() @@ -255,24 +258,27 @@ export function useGetPopularFeedsQuery(options?: GetPopularFeedsOptions) { enabled: Boolean(moderationOpts) && options?.enabled !== false, queryKey: createGetPopularFeedsQueryKey(options), queryFn: async ({pageParam}) => { - const res = await agent.app.bsky.unspecced.getPopularFeedGenerators({ - limit, - cursor: pageParam, - }) + const data = await client.call( + app.bsky.unspecced.getPopularFeedGenerators, + { + limit, + cursor: pageParam, + }, + ) // precache feeds - for (const feed of res.data.feeds) { + for (const feed of data.feeds) { const hydratedFeed = hydrateFeedGenerator(feed) precacheFeed(queryClient, hydratedFeed) } - return res.data + return data }, initialPageParam: undefined as string | undefined, getNextPageParam: lastPage => lastPage.cursor, select: useCallback( ( - data: InfiniteData, + data: InfiniteData, ) => { const { savedFeeds, @@ -336,24 +342,27 @@ export function useGetPopularFeedsQuery(options?: GetPopularFeedsOptions) { } export function useSearchPopularFeedsMutation() { - const agent = useAgent() + const client = useAppviewClient() const moderationOpts = useModerationOpts() return useMutation({ mutationFn: async (query: string) => { - const res = await agent.app.bsky.unspecced.getPopularFeedGenerators({ - limit: 10, - query: query, - }) + const data = await client.call( + app.bsky.unspecced.getPopularFeedGenerators, + { + limit: 10, + query: query, + }, + ) if (moderationOpts) { - return res.data.feeds.filter(feed => { + return data.feeds.filter(feed => { const decision = moderateFeedGenerator(feed, moderationOpts) return !decision.ui('contentMedia').blur }) } - return res.data.feeds + return data.feeds }, }) } @@ -371,7 +380,7 @@ export function usePopularFeedsSearch({ query: string enabled?: boolean }) { - const agent = useAgent() + const client = useAppviewClient() const moderationOpts = useModerationOpts() const enabledInner = enabled ?? Boolean(moderationOpts) @@ -379,12 +388,15 @@ export function usePopularFeedsSearch({ enabled: enabledInner, queryKey: createPopularFeedsSearchQueryKey(query), queryFn: async () => { - const res = await agent.app.bsky.unspecced.getPopularFeedGenerators({ - limit: 15, - query: query, - }) + const data = await client.call( + app.bsky.unspecced.getPopularFeedGenerators, + { + limit: 15, + query: query, + }, + ) - return res.data.feeds + return data.feeds }, placeholderData: keepPreviousData, select(data) { @@ -444,7 +456,7 @@ const createPinnedFeedInfosQueryKey = ( export function usePinnedFeedsInfos() { const {hasSession} = useSession() - const agent = useAgent() + const client = useAppviewClient() const {data: preferences, isLoading: isLoadingPrefs} = usePreferencesQuery() const pinnedItems = preferences?.savedFeeds.filter(feed => feed.pinned) ?? [] @@ -467,13 +479,13 @@ export function usePinnedFeedsInfos() { const pinnedFeeds = pinnedItems.filter(feed => feed.type === 'feed') let feedsPromise = Promise.resolve() if (pinnedFeeds.length > 0) { - feedsPromise = agent.app.bsky.feed - .getFeedGenerators({ - feeds: pinnedFeeds.map(f => f.value), + feedsPromise = client + .call(app.bsky.feed.getFeedGenerators, { + feeds: pinnedFeeds.map(f => f.value as AtUriString), }) - .then(res => { - for (let i = 0; i < res.data.feeds.length; i++) { - const feedView = res.data.feeds[i] + .then(data => { + for (let i = 0; i < data.feeds.length; i++) { + const feedView = data.feeds[i] resolved.set(feedView.uri, hydrateFeedGenerator(feedView)) } }) @@ -482,13 +494,13 @@ export function usePinnedFeedsInfos() { // Get all lists. This currently has to be done individually. const pinnedLists = pinnedItems.filter(feed => feed.type === 'list') const listsPromises = pinnedLists.map(list => - agent.app.bsky.graph - .getList({ - list: list.value, + client + .call(app.bsky.graph.getList, { + list: list.value as AtUriString, limit: 1, }) - .then(res => { - const listView = res.data.list + .then(data => { + const listView = data.list resolved.set(listView.uri, hydrateList(listView)) }), ) @@ -551,7 +563,7 @@ export type SavedFeedItem = } export function useSavedFeeds() { - const agent = useAgent() + const client = useAppviewClient() const {data: preferences, isLoading: isLoadingPrefs} = usePreferencesQuery() const savedItems = preferences?.savedFeeds ?? [] const queryClient = useQueryClient() @@ -582,25 +594,25 @@ export function useSavedFeeds() { let feedsPromise = Promise.resolve() if (savedFeeds.length > 0) { - feedsPromise = agent.app.bsky.feed - .getFeedGenerators({ - feeds: savedFeeds.map(f => f.value), + feedsPromise = client + .call(app.bsky.feed.getFeedGenerators, { + feeds: savedFeeds.map(f => f.value as AtUriString), }) - .then(res => { - res.data.feeds.forEach(f => { + .then(data => { + data.feeds.forEach(f => { resolvedFeeds.set(f.uri, f) }) }) } const listsPromises = savedLists.map(list => - agent.app.bsky.graph - .getList({ - list: list.value, + client + .call(app.bsky.graph.getList, { + list: list.value as AtUriString, limit: 1, }) - .then(res => { - const listView = res.data.list + .then(data => { + const listView = data.list resolvedLists.set(listView.uri, listView) }), ) @@ -656,7 +668,7 @@ export function useSavedFeeds() { const feedInfoQueryKeyRoot = 'feedInfo' export function useFeedInfo(feedUri: string | undefined) { - const agent = useAgent() + const client = useAppviewClient() return useQuery({ staleTime: STALE.INFINITY, @@ -666,11 +678,11 @@ export function useFeedInfo(feedUri: string | undefined) { return null } - const res = await agent.app.bsky.feed.getFeedGenerator({ - feed: feedUri, + const data = await client.call(app.bsky.feed.getFeedGenerator, { + feed: feedUri as AtUriString, }) - const feedSourceInfo = hydrateFeedGenerator(res.data.view) + const feedSourceInfo = hydrateFeedGenerator(data.view) return feedSourceInfo }, }) diff --git a/src/state/queries/find-contacts.ts b/src/state/queries/find-contacts.ts index b1eb6c9c5e..e462e6a6c1 100644 --- a/src/state/queries/find-contacts.ts +++ b/src/state/queries/find-contacts.ts @@ -1,4 +1,3 @@ -import {type AppBskyContactGetMatches} from '@atproto/api' import { type InfiniteData, type QueryClient, @@ -6,8 +5,9 @@ import { useQuery, } from '@tanstack/react-query' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' import {type Match} from '#/components/contacts/state' +import {app} from '#/lexicons' import type * as bsky from '#/types/bsky' import {STALE} from '.' @@ -15,13 +15,12 @@ const RQ_KEY_ROOT = 'find-contacts' export const findContactsStatusQueryKey = [RQ_KEY_ROOT, 'sync-status'] export function useContactsSyncStatusQuery() { - const agent = useAgent() + const client = useAppviewClient() return useQuery({ queryKey: findContactsStatusQueryKey, queryFn: async () => { - const status = await agent.app.bsky.contact.getSyncStatus() - return status.data + return await client.call(app.bsky.contact.getSyncStatus, {}) }, staleTime: STALE.SECONDS.THIRTY, }) @@ -30,15 +29,14 @@ export function useContactsSyncStatusQuery() { export const findContactsGetMatchesQueryKey = [RQ_KEY_ROOT, 'matches'] export function useContactsMatchesQuery() { - const agent = useAgent() + const client = useAppviewClient() return useInfiniteQuery({ queryKey: findContactsGetMatchesQueryKey, queryFn: async ({pageParam}) => { - const matches = await agent.app.bsky.contact.getMatches({ + return await client.call(app.bsky.contact.getMatches, { cursor: pageParam, }) - return matches.data }, initialPageParam: undefined as string | undefined, getNextPageParam: lastPage => lastPage.cursor, @@ -47,20 +45,19 @@ export function useContactsMatchesQuery() { } export function optimisticRemoveMatch(queryClient: QueryClient, did: string) { - queryClient.setQueryData>( - findContactsGetMatchesQueryKey, - old => { - if (!old) return old + queryClient.setQueryData< + InfiniteData + >(findContactsGetMatchesQueryKey, old => { + if (!old) return old - return { - ...old, - pages: old.pages.map(page => ({ - ...page, - matches: page.matches.filter(match => match.did !== did), - })), - } - }, - ) + return { + ...old, + pages: old.pages.map(page => ({ + ...page, + matches: page.matches.filter(match => match.did !== did), + })), + } + }) } export const findContactsMatchesPassthroughQueryKey = (dids: string[]) => [ @@ -95,7 +92,7 @@ export function* findAllProfilesInQueryData( did: string, ): Generator { const queryDatas = queryClient.getQueriesData< - InfiniteData + InfiniteData >({ queryKey: findContactsGetMatchesQueryKey, }) diff --git a/src/state/queries/pds-detection.ts b/src/state/queries/pds-detection.ts index 697963aa93..1f2b413d6d 100644 --- a/src/state/queries/pds-detection.ts +++ b/src/state/queries/pds-detection.ts @@ -1,13 +1,15 @@ import {useState} from 'react' import {type DidDocument, getPdsEndpoint} from '@atproto/common-web' +import {type HandleString} from '@atproto/syntax' import {useQuery, useQueryClient} from '@tanstack/react-query' -import {DEFAULT_SERVICE, PUBLIC_BSKY_SERVICE} from '#/lib/constants' +import {DEFAULT_SERVICE} from '#/lib/constants' import {useDebouncedValue} from '#/lib/hooks/useDebouncedValue' import {isNetworkError} from '#/lib/strings/errors' import {logger} from '#/logger' import {STALE} from '#/state/queries' -import {Agent} from '#/state/session/agent' +import {getPublicAppviewClient} from '#/state/session/clients' +import {com} from '#/lexicons' const RQKEY_ROOT = 'pds-detection' export const RQKEY = (identifier: string) => [RQKEY_ROOT, identifier] @@ -147,16 +149,25 @@ export async function resolvePdsForIdentifier( identifier: string, ): Promise<{did: string; pdsUrl: string | null} | null> { const norm = normalizeIdentifier(identifier) - const agent = new Agent(null, {service: PUBLIC_BSKY_SERVICE}) + /* + * Resolution runs without a session, so this uses the public appview client + * rather than a session-scoped one. It matches the unauthenticated, + * unproxied public agent this previously constructed by hand. + */ + const client = getPublicAppviewClient() try { let did: string if (norm.startsWith('did:')) { did = norm } else { - const res = await withResolveTimeout(signal => - agent.resolveHandle({handle: norm}, {signal}), + const data = await withResolveTimeout(signal => + client.call( + com.atproto.identity.resolveHandle, + {handle: norm as HandleString}, + {signal}, + ), ) - did = res.data.did + did = data.did } logger.debug('pds-detection: resolved identifier to DID', { identifier: norm, diff --git a/src/state/queries/resolve-uri.ts b/src/state/queries/resolve-uri.ts index 45ef4e2288..9ca0aab36d 100644 --- a/src/state/queries/resolve-uri.ts +++ b/src/state/queries/resolve-uri.ts @@ -1,15 +1,18 @@ -import {type AtpAgent, AtUri} from '@atproto/api' +import {AtUri} from '@atproto/api' +import {type Client} from '@atproto/lex' +import {type HandleString} from '@atproto/syntax' import {type QueryClient, queryOptions, useQuery} from '@tanstack/react-query' import {STALE} from '#/state/queries' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' +import {com} from '#/lexicons' import {useUnstableProfileViewCache} from './profile' const RQKEY_ROOT = 'resolved-did' export const RQKEY = (didOrHandle: string) => [RQKEY_ROOT, didOrHandle] const resolvedDidQueryOptions = ( - agent: AtpAgent, + client: Client, getUnstableProfile: (did: string) => {did: string} | undefined, didOrHandle: string | undefined, ) => @@ -21,8 +24,15 @@ const resolvedDidQueryOptions = ( // Just return the did if it's already one if (didOrHandle.startsWith('did:')) return didOrHandle - const res = await agent.resolveHandle({handle: didOrHandle}) - return res.data.did + /* + * Resolution stays on the appview client: the old agent call was proxied + * to the appview, and the PDS implementation is not equivalent for + * handles hosted elsewhere. + */ + const data = await client.call(com.atproto.identity.resolveHandle, { + handle: didOrHandle as HandleString, + }) + return data.did }, initialData: () => { // Return undefined if no did or handle @@ -37,11 +47,11 @@ export function useResolveUriQuery(uri: string | undefined) { const urip = new AtUri(uri || '') const host = urip.host - const agent = useAgent() + const client = useAppviewClient() const {getUnstableProfile} = useUnstableProfileViewCache() return useQuery({ - ...resolvedDidQueryOptions(agent, getUnstableProfile, host), + ...resolvedDidQueryOptions(client, getUnstableProfile, host), select: did => ({ did, uri: AtUri.make(did, urip.collection, urip.rkey).toString(), @@ -50,11 +60,11 @@ export function useResolveUriQuery(uri: string | undefined) { } export function useResolveDidQuery(didOrHandle: string | undefined) { - const agent = useAgent() + const client = useAppviewClient() const {getUnstableProfile} = useUnstableProfileViewCache() return useQuery( - resolvedDidQueryOptions(agent, getUnstableProfile, didOrHandle), + resolvedDidQueryOptions(client, getUnstableProfile, didOrHandle), ) } diff --git a/src/view/com/auth/LoggedOut.tsx b/src/view/com/auth/LoggedOut.tsx index da0038a9cc..5becdd45d3 100644 --- a/src/view/com/auth/LoggedOut.tsx +++ b/src/view/com/auth/LoggedOut.tsx @@ -1,13 +1,14 @@ import {useCallback, useEffect, useState} from 'react' import {View} from 'react-native' import {useSafeAreaInsets} from 'react-native-safe-area-context' +import {type DidString} from '@atproto/syntax' import {useLingui} from '@lingui/react/macro' import {useQueryClient} from '@tanstack/react-query' import {PressableScale} from '#/lib/custom-animations/PressableScale' import {STALE} from '#/state/queries' import {profilesQueryKey} from '#/state/queries/profile' -import {useAgent, useSession} from '#/state/session' +import {useAppviewClient, useSession} from '#/state/session' import {useSetActiveLanding} from '#/state/shell/landing' import { useLoggedOutView, @@ -23,6 +24,7 @@ import {atoms as a, native, tokens, useTheme} from '#/alf' import {Button, ButtonIcon} from '#/components/Button' import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times' import {useAnalytics} from '#/analytics' +import {app} from '#/lexicons' import {SplashScreen} from './SplashScreen' enum ScreenState { @@ -65,19 +67,18 @@ export function LoggedOut({onDismiss}: {onDismiss?: () => void}) { const queryClient = useQueryClient() const {accounts} = useSession() - const agent = useAgent() + const client = useAppviewClient() useEffect(() => { - const actors = accounts.map(acc => acc.did) + const actors = accounts.map(acc => acc.did as DidString) if (actors.length === 0) return void queryClient.prefetchQuery({ queryKey: profilesQueryKey(actors), staleTime: STALE.MINUTES.FIVE, queryFn: async () => { - const res = await agent.getProfiles({actors}) - return res.data + return await client.call(app.bsky.actor.getProfiles, {actors}) }, }) - }, [accounts, agent, queryClient]) + }, [accounts, client, queryClient]) const onPressDismiss = useCallback(() => { if (onDismiss) { diff --git a/src/view/com/composer/drafts/state/queries.ts b/src/view/com/composer/drafts/state/queries.ts index 07dd67dd88..0108dbbbff 100644 --- a/src/view/com/composer/drafts/state/queries.ts +++ b/src/view/com/composer/drafts/state/queries.ts @@ -1,4 +1,4 @@ -import {AppBskyDraftCreateDraft, AppBskyDraftDefs} from '@atproto/api' +import {AppBskyDraftDefs} from '@atproto/api' import { useInfiniteQuery, useMutation, @@ -6,10 +6,12 @@ import { } from '@tanstack/react-query' import {isNetworkError} from '#/lib/strings/errors' -import {useAgent} from '#/state/session' +import {matchXrpcError} from '#/lib/xrpc-error' +import {useAppviewClient} from '#/state/session' import {type ComposerState} from '#/view/com/composer/state/composer' import {useAnalytics} from '#/analytics' import {getDeviceId} from '#/analytics/identifiers' +import {app} from '#/lexicons' import {composerStateToDraft, draftViewToSummary} from './api' import {logger} from './logger' import * as storage from './storage' @@ -20,7 +22,7 @@ const DRAFTS_QUERY_KEY = ['drafts'] * Hook to list all drafts for the current account */ export function useDraftsQuery() { - const agent = useAgent() + const client = useAppviewClient() const ax = useAnalytics() return useInfiniteQuery({ @@ -28,10 +30,12 @@ export function useDraftsQuery() { queryFn: async ({pageParam}) => { // Ensure media cache is populated before checking which media exists await storage.ensureMediaCachePopulated() - const res = await agent.app.bsky.draft.getDrafts({cursor: pageParam}) + const data = await client.call(app.bsky.draft.getDrafts, { + cursor: pageParam, + }) return { - cursor: res.data.cursor, - drafts: res.data.drafts.map(view => + cursor: data.cursor, + drafts: data.drafts.map(view => draftViewToSummary({ view, analytics: ax, @@ -116,7 +120,7 @@ export async function loadDraftMedia(draft: AppBskyDraftDefs.Draft): Promise<{ * This ensures we don't lose data if the network request fails. */ export function useSaveDraftMutation() { - const agent = useAgent() + const client = useAppviewClient() const queryClient = useQueryClient() return useMutation({ @@ -132,7 +136,14 @@ export function useSaveDraftMutation() { originalLocalRefs: Set | undefined }> => { // Convert composer state to server draft format - const {draft, localRefPaths} = await composerStateToDraft(composerState) + const {draft: apiDraft, localRefPaths} = + await composerStateToDraft(composerState) + /* + * `composerStateToDraft` builds the draft against the `@atproto/api` + * types, whose string fields are unbranded, so it is asserted once here + * to the vendored input type. + */ + const draft = apiDraft as unknown as app.bsky.draft.defs.Draft logger.debug('saving draft', { existingDraftId, @@ -147,7 +158,7 @@ export function useSaveDraftMutation() { logger.debug('updating existing draft on server', { draftId: existingDraftId, }) - await agent.app.bsky.draft.updateDraft({ + await client.call(app.bsky.draft.updateDraft, { draft: { id: existingDraftId, draft, @@ -157,8 +168,8 @@ export function useSaveDraftMutation() { } else { // Create new draft logger.debug('creating new draft on server') - const res = await agent.app.bsky.draft.createDraft({draft}) - draftId = res.data.id + const data = await client.call(app.bsky.draft.createDraft, {draft}) + draftId = data.id logger.debug('created new draft', {draftId}) } @@ -203,7 +214,7 @@ export function useSaveDraftMutation() { }, onError: error => { // Check for draft limit error - if (error instanceof AppBskyDraftCreateDraft.DraftLimitReachedError) { + if (matchXrpcError(error, app.bsky.draft.createDraft)) { logger.error('Draft limit reached', {safeMessage: error.message}) // Error will be handled by caller } else if (!isNetworkError(error)) { @@ -220,7 +231,7 @@ export function useSaveDraftMutation() { * Takes the full draft data to avoid re-fetching for media cleanup. */ export function useDeleteDraftMutation() { - const agent = useAgent() + const client = useAppviewClient() const queryClient = useQueryClient() return useMutation({ @@ -231,7 +242,7 @@ export function useDeleteDraftMutation() { draft: AppBskyDraftDefs.Draft }) => { // Delete from server first - if this fails, we keep local media for retry - await agent.app.bsky.draft.deleteDraft({id: draftId}) + await client.call(app.bsky.draft.deleteDraft, {id: draftId}) }, onSuccess: async (_, {draft}) => { // Only delete local media after server deletion succeeds @@ -264,7 +275,7 @@ export function useDeleteDraftMutation() { * Takes draftId and originalLocalRefs from composer state. */ export function useCleanupPublishedDraftMutation() { - const agent = useAgent() + const client = useAppviewClient() const queryClient = useQueryClient() return useMutation({ @@ -280,7 +291,9 @@ export function useCleanupPublishedDraftMutation() { mediaFileCount: originalLocalRefs.size, }) // Delete from server first - await agent.app.bsky.draft.deleteDraft({id: draftId}) + await client.call(app.bsky.draft.deleteDraft, { + id: draftId, + }) logger.debug('deleted draft from server', {draftId}) }, onSuccess: async (_, {originalLocalRefs}) => { From 6f696e4b737be1ceed45a43ee17a17c1aae6b1e5 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 13 Aug 2026 22:26:17 +0300 Subject: [PATCH 13/32] [SDK] Migrate the feed api classes to the appview client (#11361) Co-authored-by: Claude Fable 5 --- src/lib/api/feed/author.ts | 45 ++++---- src/lib/api/feed/custom.ts | 88 ++++++++-------- src/lib/api/feed/demo.ts | 9 +- src/lib/api/feed/following.ts | 31 +++--- src/lib/api/feed/home.ts | 24 +++-- src/lib/api/feed/likes.ts | 49 +++++---- src/lib/api/feed/list.ts | 45 ++++---- src/lib/api/feed/merge.ts | 111 ++++++++++++-------- src/lib/api/feed/posts.ts | 39 +++---- src/state/queries/explore-feed-previews.tsx | 9 +- src/state/queries/post-feed.ts | 47 ++++++--- src/state/queries/post-liked-by.ts | 30 +++--- src/state/queries/post-quotes.ts | 21 ++-- src/state/queries/post-reposted-by.ts | 23 ++-- 14 files changed, 314 insertions(+), 257 deletions(-) diff --git a/src/lib/api/feed/author.ts b/src/lib/api/feed/author.ts index 3b97b8ef73..92f8d327b4 100644 --- a/src/lib/api/feed/author.ts +++ b/src/lib/api/feed/author.ts @@ -1,23 +1,25 @@ -import { - AppBskyFeedDefs, - type AppBskyFeedGetAuthorFeed as GetAuthorFeed, - type AtpAgent, -} from '@atproto/api' +import {AppBskyFeedDefs} from '@atproto/api' +import {type Client, type XrpcRequestParams} from '@atproto/lex' +import {app} from '#/lexicons' import {type FeedAPI, type FeedAPIResponse} from './types' +type GetAuthorFeedParams = XrpcRequestParams< + typeof app.bsky.feed.getAuthorFeed.main +> + export class AuthorFeedAPI implements FeedAPI { - agent: AtpAgent - _params: GetAuthorFeed.QueryParams + client: Client + _params: GetAuthorFeedParams constructor({ - agent, + client, feedParams, }: { - agent: AtpAgent - feedParams: GetAuthorFeed.QueryParams + client: Client + feedParams: GetAuthorFeedParams }) { - this.agent = agent + this.client = client this._params = feedParams } @@ -28,11 +30,11 @@ export class AuthorFeedAPI implements FeedAPI { } async peekLatest(): Promise { - const res = await this.agent.getAuthorFeed({ + const data = await this.client.call(app.bsky.feed.getAuthorFeed, { ...this.params, limit: 1, }) - return res.data.feed[0] + return data.feed[0] } async fetch({ @@ -42,19 +44,20 @@ export class AuthorFeedAPI implements FeedAPI { cursor: string | undefined limit: number }): Promise { - const res = await this.agent.getAuthorFeed({ + /* + * A failed request rejects rather than resolving, so the error propagates + * to the query and drives the feed error UI (blocked actor, rate limit). + * The agent behaved the same way - its `success` flag was only ever true - + * so the empty-page branch this replaces was unreachable. + */ + const data = await this.client.call(app.bsky.feed.getAuthorFeed, { ...this.params, cursor, limit, }) - if (res.success) { - return { - cursor: res.data.cursor, - feed: this._filter(res.data.feed), - } - } return { - feed: [], + cursor: data.cursor, + feed: this._filter(data.feed), } } diff --git a/src/lib/api/feed/custom.ts b/src/lib/api/feed/custom.ts index 54d9dc9067..3b1ec3bec3 100644 --- a/src/lib/api/feed/custom.ts +++ b/src/lib/api/feed/custom.ts @@ -1,46 +1,46 @@ -import { - type AppBskyFeedDefs, - type AppBskyFeedGetFeed as GetCustomFeed, - AtpAgent, - jsonStringToLex, -} from '@atproto/api' +import {type AppBskyFeedDefs, AtpAgent, jsonStringToLex} from '@atproto/api' +import {type Client, type XrpcRequestParams} from '@atproto/lex' import { getAppLanguageAsContentLanguage, getContentLanguages, } from '#/state/preferences/languages' +import {app} from '#/lexicons' import {type FeedAPI, type FeedAPIResponse} from './types' import {createBskyTopicsHeader, isBlueskyOwnedFeed} from './utils' +type GetCustomFeedParams = XrpcRequestParams + export class CustomFeedAPI implements FeedAPI { - agent: AtpAgent - params: GetCustomFeed.QueryParams + client: Client + params: GetCustomFeedParams userInterests?: string constructor({ - agent, + client, feedParams, userInterests, }: { - agent: AtpAgent - feedParams: GetCustomFeed.QueryParams + client: Client + feedParams: GetCustomFeedParams userInterests?: string }) { - this.agent = agent + this.client = client this.params = feedParams this.userInterests = userInterests } async peekLatest(): Promise { const contentLangs = getContentLanguages().join(',') - const res = await this.agent.app.bsky.feed.getFeed( + const data = await this.client.call( + app.bsky.feed.getFeed, { ...this.params, limit: 1, }, {headers: {'Accept-Language': contentLangs}}, ) - return res.data.feed[0] + return data.feed[0] } async fetch({ @@ -51,11 +51,17 @@ export class CustomFeedAPI implements FeedAPI { limit: number }): Promise { const contentLangs = getContentLanguages().join(',') - const agent = this.agent const isBlueskyOwned = isBlueskyOwnedFeed(this.params.feed) - const res = agent.did - ? await this.agent.app.bsky.feed.getFeed( + /* + * The authed branch rejects on failure, so the error propagates to the + * query and drives the feed error UI (feedgen offline, misconfigured, rate + * limited). Only the logged-out branch can resolve without data, and it + * signals that with a null body. + */ + const data = this.client.did + ? await this.client.call( + app.bsky.feed.getFeed, { ...this.params, cursor, @@ -71,21 +77,22 @@ export class CustomFeedAPI implements FeedAPI { }, ) : await loggedOutFetch({...this.params, cursor, limit}) - if (res.success) { - // NOTE - // some custom feeds fail to enforce the pagination limit - // so we manually truncate here - // -prf - if (res.data.feed.length > limit) { - res.data.feed = res.data.feed.slice(0, limit) - } + + if (!data) { return { - cursor: res.data.feed.length ? res.data.cursor : undefined, - feed: res.data.feed, + feed: [], } } + + // NOTE + // some custom feeds fail to enforce the pagination limit + // so we manually truncate here + // -prf + const feed = + data.feed.length > limit ? data.feed.slice(0, limit) : data.feed return { - feed: [], + cursor: feed.length ? data.cursor : undefined, + feed, } } } @@ -105,7 +112,7 @@ async function loggedOutFetch({ feed: string limit: number cursor?: string -}) { +}): Promise { let contentLangs = getAppLanguageAsContentLanguage() /** @@ -128,14 +135,15 @@ async function loggedOutFetch({ headers: {'Accept-Language': contentLangs, ...labelersHeader}, }, ) + /* + * The response is hand-decoded rather than validated, so the lex output shape + * is asserted here just as the old-world one was. + */ let data = res.ok - ? (jsonStringToLex(await res.text()) as GetCustomFeed.OutputSchema) + ? (jsonStringToLex(await res.text()) as app.bsky.feed.getFeed.$OutputBody) : null if (data?.feed?.length) { - return { - success: true, - data, - } + return data } // no data, try again with language headers removed @@ -146,17 +154,11 @@ async function loggedOutFetch({ {method: 'GET', headers: {'Accept-Language': '', ...labelersHeader}}, ) data = res.ok - ? (jsonStringToLex(await res.text()) as GetCustomFeed.OutputSchema) + ? (jsonStringToLex(await res.text()) as app.bsky.feed.getFeed.$OutputBody) : null if (data?.feed?.length) { - return { - success: true, - data, - } + return data } - return { - success: false, - data: {feed: []}, - } + return null } diff --git a/src/lib/api/feed/demo.ts b/src/lib/api/feed/demo.ts index 42d1046bdc..a4c1c360e9 100644 --- a/src/lib/api/feed/demo.ts +++ b/src/lib/api/feed/demo.ts @@ -1,13 +1,14 @@ -import {type AppBskyFeedDefs, type AtpAgent} from '@atproto/api' +import {type AppBskyFeedDefs} from '@atproto/api' +import {type Client} from '@atproto/lex' import {DEMO_FEED} from '#/lib/demo' import {type FeedAPI, type FeedAPIResponse} from './types' export class DemoFeedAPI implements FeedAPI { - agent: AtpAgent + client: Client - constructor({agent}: {agent: AtpAgent}) { - this.agent = agent + constructor({client}: {client: Client}) { + this.client = client } async peekLatest(): Promise { diff --git a/src/lib/api/feed/following.ts b/src/lib/api/feed/following.ts index 17e96d8e1b..596483577a 100644 --- a/src/lib/api/feed/following.ts +++ b/src/lib/api/feed/following.ts @@ -1,19 +1,21 @@ -import {type AppBskyFeedDefs, type AtpAgent} from '@atproto/api' +import {type AppBskyFeedDefs} from '@atproto/api' +import {type Client} from '@atproto/lex' +import {app} from '#/lexicons' import {type FeedAPI, type FeedAPIResponse} from './types' export class FollowingFeedAPI implements FeedAPI { - agent: AtpAgent + client: Client - constructor({agent}: {agent: AtpAgent}) { - this.agent = agent + constructor({client}: {client: Client}) { + this.client = client } async peekLatest(): Promise { - const res = await this.agent.getTimeline({ + const data = await this.client.call(app.bsky.feed.getTimeline, { limit: 1, }) - return res.data.feed[0] + return data.feed[0] } async fetch({ @@ -23,18 +25,19 @@ export class FollowingFeedAPI implements FeedAPI { cursor: string | undefined limit: number }): Promise { - const res = await this.agent.getTimeline({ + /* + * A failed request rejects rather than resolving, so the error propagates + * to the query and drives the feed error UI. The agent behaved the same + * way - its `success` flag was only ever true - so the empty-page branch + * this replaces was unreachable. + */ + const data = await this.client.call(app.bsky.feed.getTimeline, { cursor, limit, }) - if (res.success) { - return { - cursor: res.data.cursor, - feed: res.data.feed, - } - } return { - feed: [], + cursor: data.cursor, + feed: data.feed, } } } diff --git a/src/lib/api/feed/home.ts b/src/lib/api/feed/home.ts index aa13c70bf0..a5ff0f3b93 100644 --- a/src/lib/api/feed/home.ts +++ b/src/lib/api/feed/home.ts @@ -1,4 +1,6 @@ -import {type AppBskyFeedDefs, type AtpAgent} from '@atproto/api' +import {type AppBskyFeedDefs} from '@atproto/api' +import {type Client} from '@atproto/lex' +import {type AtUriString} from '@atproto/syntax' import {PROD_DEFAULT_FEED} from '#/lib/constants' import {CustomFeedAPI} from './custom' @@ -27,7 +29,7 @@ export const FALLBACK_MARKER_POST: AppBskyFeedDefs.FeedViewPost = { } export class HomeFeedAPI implements FeedAPI { - agent: AtpAgent + client: Client following: FollowingFeedAPI discover: CustomFeedAPI usingDiscover = false @@ -36,25 +38,25 @@ export class HomeFeedAPI implements FeedAPI { constructor({ userInterests, - agent, + client, }: { userInterests?: string - agent: AtpAgent + client: Client }) { - this.agent = agent - this.following = new FollowingFeedAPI({agent}) + this.client = client + this.following = new FollowingFeedAPI({client}) this.discover = new CustomFeedAPI({ - agent, - feedParams: {feed: PROD_DEFAULT_FEED('whats-hot')}, + client, + feedParams: {feed: PROD_DEFAULT_FEED('whats-hot') as AtUriString}, }) this.userInterests = userInterests } reset() { - this.following = new FollowingFeedAPI({agent: this.agent}) + this.following = new FollowingFeedAPI({client: this.client}) this.discover = new CustomFeedAPI({ - agent: this.agent, - feedParams: {feed: PROD_DEFAULT_FEED('whats-hot')}, + client: this.client, + feedParams: {feed: PROD_DEFAULT_FEED('whats-hot') as AtUriString}, userInterests: this.userInterests, }) this.usingDiscover = false diff --git a/src/lib/api/feed/likes.ts b/src/lib/api/feed/likes.ts index 1511dc833a..d7d63fc4db 100644 --- a/src/lib/api/feed/likes.ts +++ b/src/lib/api/feed/likes.ts @@ -1,32 +1,34 @@ -import { - type AppBskyFeedDefs, - type AppBskyFeedGetActorLikes as GetActorLikes, - type AtpAgent, -} from '@atproto/api' +import {type AppBskyFeedDefs} from '@atproto/api' +import {type Client, type XrpcRequestParams} from '@atproto/lex' +import {app} from '#/lexicons' import {type FeedAPI, type FeedAPIResponse} from './types' +type GetActorLikesParams = XrpcRequestParams< + typeof app.bsky.feed.getActorLikes.main +> + export class LikesFeedAPI implements FeedAPI { - agent: AtpAgent - params: GetActorLikes.QueryParams + client: Client + params: GetActorLikesParams constructor({ - agent, + client, feedParams, }: { - agent: AtpAgent - feedParams: GetActorLikes.QueryParams + client: Client + feedParams: GetActorLikesParams }) { - this.agent = agent + this.client = client this.params = feedParams } async peekLatest(): Promise { - const res = await this.agent.getActorLikes({ + const data = await this.client.call(app.bsky.feed.getActorLikes, { ...this.params, limit: 1, }) - return res.data.feed[0] + return data.feed[0] } async fetch({ @@ -36,21 +38,22 @@ export class LikesFeedAPI implements FeedAPI { cursor: string | undefined limit: number }): Promise { - const res = await this.agent.getActorLikes({ + /* + * A failed request rejects rather than resolving, so the error propagates + * to the query and drives the feed error UI. The agent behaved the same + * way - its `success` flag was only ever true - so the empty-page branch + * this replaces was unreachable. + */ + const data = await this.client.call(app.bsky.feed.getActorLikes, { ...this.params, cursor, limit, }) - if (res.success) { - // HACKFIX: the API incorrectly returns a cursor when there are no items -sfn - const isEmptyPage = res.data.feed.length === 0 - return { - cursor: isEmptyPage ? undefined : res.data.cursor, - feed: res.data.feed, - } - } + // HACKFIX: the API incorrectly returns a cursor when there are no items -sfn + const isEmptyPage = data.feed.length === 0 return { - feed: [], + cursor: isEmptyPage ? undefined : data.cursor, + feed: data.feed, } } } diff --git a/src/lib/api/feed/list.ts b/src/lib/api/feed/list.ts index 9697b0aaf3..e4d07d73d7 100644 --- a/src/lib/api/feed/list.ts +++ b/src/lib/api/feed/list.ts @@ -1,32 +1,34 @@ -import { - type Agent, - type AppBskyFeedDefs, - type AppBskyFeedGetListFeed as GetListFeed, -} from '@atproto/api' +import {type AppBskyFeedDefs} from '@atproto/api' +import {type Client, type XrpcRequestParams} from '@atproto/lex' +import {app} from '#/lexicons' import {type FeedAPI, type FeedAPIResponse} from './types' +type GetListFeedParams = XrpcRequestParams< + typeof app.bsky.feed.getListFeed.main +> + export class ListFeedAPI implements FeedAPI { - agent: Agent - params: GetListFeed.QueryParams + client: Client + params: GetListFeedParams constructor({ - agent, + client, feedParams, }: { - agent: Agent - feedParams: GetListFeed.QueryParams + client: Client + feedParams: GetListFeedParams }) { - this.agent = agent + this.client = client this.params = feedParams } async peekLatest(): Promise { - const res = await this.agent.app.bsky.feed.getListFeed({ + const data = await this.client.call(app.bsky.feed.getListFeed, { ...this.params, limit: 1, }) - return res.data.feed[0] + return data.feed[0] } async fetch({ @@ -36,19 +38,20 @@ export class ListFeedAPI implements FeedAPI { cursor: string | undefined limit: number }): Promise { - const res = await this.agent.app.bsky.feed.getListFeed({ + /* + * A failed request rejects rather than resolving, so the error propagates + * to the query and drives the feed error UI. The agent behaved the same + * way - its `success` flag was only ever true - so the empty-page branch + * this replaces was unreachable. + */ + const data = await this.client.call(app.bsky.feed.getListFeed, { ...this.params, cursor, limit, }) - if (res.success) { - return { - cursor: res.data.cursor, - feed: res.data.feed, - } - } return { - feed: [], + cursor: data.cursor, + feed: data.feed, } } } diff --git a/src/lib/api/feed/merge.ts b/src/lib/api/feed/merge.ts index c341dd53a0..2e3abcb8e3 100644 --- a/src/lib/api/feed/merge.ts +++ b/src/lib/api/feed/merge.ts @@ -1,8 +1,6 @@ -import { - type AppBskyFeedDefs, - type AppBskyFeedGetTimeline, - type AtpAgent, -} from '@atproto/api' +import {type AppBskyFeedDefs} from '@atproto/api' +import {type Client} from '@atproto/lex' +import {type AtUriString} from '@atproto/syntax' import shuffle from 'lodash.shuffle' import {bundleAsync} from '#/lib/async/bundle' @@ -10,6 +8,7 @@ import {timeout} from '#/lib/async/timeout' import {feedUriToHref} from '#/lib/strings/url-helpers' import {getContentLanguages} from '#/state/preferences/languages' import {type FeedParams} from '#/state/queries/post-feed' +import {app} from '#/lexicons' import {FeedTuner} from '../feed-manip' import {type FeedTunerFn} from '../feed-manip' import { @@ -22,9 +21,19 @@ import {createBskyTopicsHeader, isBlueskyOwnedFeed} from './utils' const REQUEST_WAIT_MS = 500 // 500ms const POST_AGE_CUTOFF = 60e3 * 60 * 24 // 24hours +/** + * A page of feed items, or `null` when the source could not produce one. Only + * sources that deliberately swallow their own errors return `null`; the rest + * reject so the error reaches the caller. + */ +type MergeFeedPage = { + cursor?: string + feed: AppBskyFeedDefs.FeedViewPost[] +} | null + export class MergeFeedAPI implements FeedAPI { userInterests?: string - agent: AtpAgent + client: Client params: FeedParams feedTuners: FeedTunerFn[] following: MergeFeedSource_Following @@ -34,29 +43,29 @@ export class MergeFeedAPI implements FeedAPI { sampleCursor = 0 constructor({ - agent, + client, feedParams, feedTuners, userInterests, }: { - agent: AtpAgent + client: Client feedParams: FeedParams feedTuners: FeedTunerFn[] userInterests?: string }) { - this.agent = agent + this.client = client this.params = feedParams this.feedTuners = feedTuners this.userInterests = userInterests this.following = new MergeFeedSource_Following({ - agent: this.agent, + client: this.client, feedTuners: this.feedTuners, }) } reset() { this.following = new MergeFeedSource_Following({ - agent: this.agent, + client: this.client, feedTuners: this.feedTuners, }) this.customFeeds = [] @@ -68,7 +77,7 @@ export class MergeFeedAPI implements FeedAPI { this.params.mergeFeedSources.map( feedUri => new MergeFeedSource_Custom({ - agent: this.agent, + client: this.client, feedUri, feedTuners: this.feedTuners, userInterests: this.userInterests, @@ -81,10 +90,10 @@ export class MergeFeedAPI implements FeedAPI { } async peekLatest(): Promise { - const res = await this.agent.getTimeline({ + const data = await this.client.call(app.bsky.feed.getTimeline, { limit: 1, }) - return res.data.feed[0] + return data.feed[0] } async fetch({ @@ -175,7 +184,7 @@ export class MergeFeedAPI implements FeedAPI { } class MergeFeedSource { - agent: AtpAgent + client: Client feedTuners: FeedTunerFn[] sourceInfo: ReasonFeedSource | undefined cursor: string | undefined = undefined @@ -183,13 +192,13 @@ class MergeFeedSource { hasMore = true constructor({ - agent, + client, feedTuners, }: { - agent: AtpAgent + client: Client feedTuners: FeedTunerFn[] }) { - this.agent = agent + this.client = client this.feedTuners = feedTuners } @@ -210,11 +219,11 @@ class MergeFeedSource { } _fetchNextInner = bundleAsync(async (n: number) => { - const res = await this._getFeed(this.cursor, n) - if (res.success) { - this.cursor = res.data.cursor - if (res.data.feed.length) { - this.queue = this.queue.concat(res.data.feed) + const page = await this._getFeed(this.cursor, n) + if (page) { + this.cursor = page.cursor + if (page.feed.length) { + this.queue = this.queue.concat(page.feed) } else { this.hasMore = false } @@ -226,7 +235,7 @@ class MergeFeedSource { protected _getFeed( _cursor: string | undefined, _limit: number, - ): Promise { + ): Promise { throw new Error('Must be overridden') } } @@ -238,42 +247,49 @@ class MergeFeedSource_Following extends MergeFeedSource { return this._fetchNextInner(n) } + /* + * No error handling: a failed timeline read rejects, which is what the agent + * did too, so the error still reaches `MergeFeedAPI.fetch` and the query. + */ protected async _getFeed( cursor: string | undefined, limit: number, - ): Promise { - const res = await this.agent.getTimeline({cursor, limit}) + ): Promise { + const data = await this.client.call(app.bsky.feed.getTimeline, { + cursor, + limit, + }) // run the tuner pre-emptively to ensure better mixing - const slices = this.tuner.tune(res.data.feed, { + const slices = this.tuner.tune(data.feed, { dryRun: false, }) - res.data.feed = slices.map(slice => slice._feedPost) - return res + return { + cursor: data.cursor, + feed: slices.map(slice => slice._feedPost), + } } } class MergeFeedSource_Custom extends MergeFeedSource { - agent: AtpAgent minDate: Date feedUri: string userInterests?: string constructor({ - agent, + client, feedUri, feedTuners, userInterests, }: { - agent: AtpAgent + client: Client feedUri: string feedTuners: FeedTunerFn[] userInterests?: string }) { super({ - agent, + client, feedTuners, }) - this.agent = agent this.feedUri = feedUri this.userInterests = userInterests this.sourceInfo = { @@ -287,15 +303,16 @@ class MergeFeedSource_Custom extends MergeFeedSource { protected async _getFeed( cursor: string | undefined, limit: number, - ): Promise { + ): Promise { try { const contentLangs = getContentLanguages().join(',') const isBlueskyOwned = isBlueskyOwnedFeed(this.feedUri) - const res = await this.agent.app.bsky.feed.getFeed( + const data = await this.client.call( + app.bsky.feed.getFeed, { cursor, limit, - feed: this.feedUri, + feed: this.feedUri as AtUriString, }, { headers: { @@ -310,22 +327,24 @@ class MergeFeedSource_Custom extends MergeFeedSource { // some custom feeds fail to enforce the pagination limit // so we manually truncate here // -prf - if (limit && res.data.feed.length > limit) { - res.data.feed = res.data.feed.slice(0, limit) - } + let feed: AppBskyFeedDefs.FeedViewPost[] = + limit && data.feed.length > limit + ? data.feed.slice(0, limit) + : data.feed // filter out older posts - res.data.feed = res.data.feed.filter( - post => new Date(post.post.indexedAt) > this.minDate, - ) + feed = feed.filter(post => new Date(post.post.indexedAt) > this.minDate) // attach source info - for (const post of res.data.feed) { + for (const post of feed) { // @ts-ignore post.__source = this.sourceInfo } - return res + return { + cursor: data.cursor, + feed, + } } catch { // dont bubble custom-feed errors - return {success: false, headers: {}, data: {feed: []}} + return null } } } diff --git a/src/lib/api/feed/posts.ts b/src/lib/api/feed/posts.ts index 33eff50997..d2bcfc4317 100644 --- a/src/lib/api/feed/posts.ts +++ b/src/lib/api/feed/posts.ts @@ -1,25 +1,25 @@ -import { - type Agent, - type AppBskyFeedDefs, - type AppBskyFeedGetPosts, -} from '@atproto/api' +import {type AppBskyFeedDefs} from '@atproto/api' +import {type Client, type XrpcRequestParams} from '@atproto/lex' import {logger} from '#/logger' +import {app} from '#/lexicons' import {type FeedAPI, type FeedAPIResponse} from './types' +type GetPostsParams = XrpcRequestParams + export class PostListFeedAPI implements FeedAPI { - agent: Agent - params: AppBskyFeedGetPosts.QueryParams + client: Client + params: GetPostsParams peek: AppBskyFeedDefs.FeedViewPost | null = null constructor({ - agent, + client, feedParams, }: { - agent: Agent - feedParams: AppBskyFeedGetPosts.QueryParams + client: Client + feedParams: GetPostsParams }) { - this.agent = agent + this.client = client if (feedParams.uris.length > 25) { logger.warn( `Too many URIs provided - expected 25, got ${feedParams.uris.length}`, @@ -36,17 +36,18 @@ export class PostListFeedAPI implements FeedAPI { } async fetch({}: {}): Promise { - const res = await this.agent.app.bsky.feed.getPosts({ + /* + * A failed request rejects rather than resolving, so the error propagates + * to the query and drives the feed error UI. The agent behaved the same + * way - its `success` flag was only ever true - so the empty-page branch + * this replaces was unreachable. + */ + const data = await this.client.call(app.bsky.feed.getPosts, { ...this.params, }) - if (res.success) { - this.peek = {post: res.data.posts[0]} - return { - feed: res.data.posts.map(post => ({post})), - } - } + this.peek = {post: data.posts[0]} return { - feed: [], + feed: data.posts.map(post => ({post})), } } } diff --git a/src/state/queries/explore-feed-previews.tsx b/src/state/queries/explore-feed-previews.tsx index acbc4ee7e1..c340078603 100644 --- a/src/state/queries/explore-feed-previews.tsx +++ b/src/state/queries/explore-feed-previews.tsx @@ -5,6 +5,7 @@ import { AtUri, moderatePost, } from '@atproto/api' +import {type AtUriString} from '@atproto/syntax' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import { @@ -28,7 +29,7 @@ import { embedViewRecordToPostView, getEmbeddedPost, } from '#/state/queries/util' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' const RQKEY_ROOT = 'feed-previews' const RQKEY = (feeds: string[]) => [RQKEY_ROOT, feeds] @@ -121,7 +122,7 @@ export function useFeedPreviews( const uris = feeds.map(feed => feed.uri) const {_} = useLingui() - const agent = useAgent() + const client = useAppviewClient() const {data: preferences} = usePreferencesQuery() const userInterests = aggregateUserInterests(preferences) const moderationOpts = useModerationOpts() @@ -143,8 +144,8 @@ export function useFeedPreviews( queryFn: async ({pageParam}) => { const feed = feeds[pageParam] const api = new CustomFeedAPI({ - agent, - feedParams: {feed: feed.uri}, + client, + feedParams: {feed: feed.uri as AtUriString}, userInterests, }) const data = await api.fetch({cursor: undefined, limit: LIMIT}) diff --git a/src/state/queries/post-feed.ts b/src/state/queries/post-feed.ts index 959ed81c28..0d5b48acde 100644 --- a/src/state/queries/post-feed.ts +++ b/src/state/queries/post-feed.ts @@ -4,12 +4,13 @@ import { type AppBskyActorDefs, AppBskyFeedDefs, type AppBskyFeedPost, - type AtpAgent, AtUri, moderatePost, type ModerationDecision, type ModerationPrefs, } from '@atproto/api' +import {type Client} from '@atproto/lex' +import {type AtIdentifierString, type AtUriString} from '@atproto/syntax' import { type InfiniteData, type QueryClient, @@ -33,7 +34,7 @@ import {DISCOVER_FEED_URI} from '#/lib/constants' import {logger} from '#/logger' import {STALE} from '#/state/queries' import {DEFAULT_LOGGED_OUT_PREFERENCES} from '#/state/queries/preferences/const' -import {useAgent} from '#/state/session' +import {useAgent, useAppviewClient} from '#/state/session' import * as userActionHistory from '#/state/userActionHistory' import {KnownError} from '#/view/com/posts/PostFeedErrorMessage' import {useFeedTuners} from '../preferences/feed-tuners' @@ -149,6 +150,7 @@ export function usePostFeedQuery( ) ?? -1 const enableFollowingToDiscoverFallback = followingPinnedIndex === 0 const agent = useAgent() + const client = useAppviewClient() const lastRun = useRef<{ data: InfiniteData args: typeof selectArgs @@ -193,7 +195,7 @@ export function usePostFeedQuery( feedDesc, feedParams: params || {}, feedTuners, - agent, + client, // Not in the query key because they don't change: userInterests, // Not in the query key. Reacting to it switching isn't important: @@ -443,55 +445,68 @@ function createApi({ feedParams, feedTuners, userInterests, - agent, + client, enableFollowingToDiscoverFallback, }: { feedDesc: FeedDescriptor feedParams: FeedParams feedTuners: FeedTunerFn[] userInterests?: string - agent: AtpAgent + client: Client enableFollowingToDiscoverFallback: boolean }) { if (feedDesc === 'following') { if (feedParams.mergeFeedEnabled) { return new MergeFeedAPI({ - agent, + client, feedParams, feedTuners, userInterests, }) } else { if (enableFollowingToDiscoverFallback) { - return new HomeFeedAPI({agent, userInterests}) + return new HomeFeedAPI({client, userInterests}) } else { - return new FollowingFeedAPI({agent}) + return new FollowingFeedAPI({client}) } } } else if (feedDesc.startsWith('author')) { const [__, actor, filter] = feedDesc.split('|') - return new AuthorFeedAPI({agent, feedParams: {actor, filter}}) + /* + * The descriptor is split out of an internally-built string, so neither the + * actor identifier nor the filter token is narrowed by the compiler here. + */ + return new AuthorFeedAPI({ + client, + feedParams: {actor: actor as AtIdentifierString, filter}, + }) } else if (feedDesc.startsWith('likes')) { const [__, actor] = feedDesc.split('|') - return new LikesFeedAPI({agent, feedParams: {actor}}) + return new LikesFeedAPI({ + client, + feedParams: {actor: actor as AtIdentifierString}, + }) } else if (feedDesc.startsWith('feedgen')) { const [__, feed] = feedDesc.split('|') return new CustomFeedAPI({ - agent, - feedParams: {feed}, + client, + feedParams: {feed: feed as AtUriString}, userInterests, }) } else if (feedDesc.startsWith('list')) { const [__, list] = feedDesc.split('|') - return new ListFeedAPI({agent, feedParams: {list}}) + return new ListFeedAPI({client, feedParams: {list: list as AtUriString}}) } else if (feedDesc.startsWith('posts')) { const [__, uriList] = feedDesc.split('|') - return new PostListFeedAPI({agent, feedParams: {uris: uriList.split(',')}}) + return new PostListFeedAPI({ + client, + feedParams: {uris: uriList.split(',') as AtUriString[]}, + }) } else if (feedDesc === 'demo') { - return new DemoFeedAPI({agent}) + return new DemoFeedAPI({client}) } else { // shouldnt happen - return new FollowingFeedAPI({agent}) + return new FollowingFeedAPI({client}) } } diff --git a/src/state/queries/post-liked-by.ts b/src/state/queries/post-liked-by.ts index e4f37c14ca..cc18a46426 100644 --- a/src/state/queries/post-liked-by.ts +++ b/src/state/queries/post-liked-by.ts @@ -1,4 +1,5 @@ -import {type AppBskyActorDefs, type AppBskyFeedGetLikes} from '@atproto/api' +import {type AppBskyActorDefs} from '@atproto/api' +import {type AtUriString} from '@atproto/syntax' import { type InfiniteData, type QueryClient, @@ -9,7 +10,8 @@ import { import {STALE} from '#/state/queries' import {createQueryKey} from '#/state/queries/util' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' +import {app} from '#/lexicons' const PAGE_SIZE = 30 type RQPageParam = string | undefined @@ -19,22 +21,22 @@ const RQKEY_ROOT = 'liked-by' export const RQKEY = (resolvedUri: string) => [RQKEY_ROOT, resolvedUri] export function useLikedByQuery(resolvedUri: string | undefined) { - const agent = useAgent() + const client = useAppviewClient() return useInfiniteQuery< - AppBskyFeedGetLikes.OutputSchema, + app.bsky.feed.getLikes.$OutputBody, Error, - InfiniteData, + InfiniteData, QueryKey, RQPageParam >({ queryKey: RQKEY(resolvedUri || ''), async queryFn({pageParam}: {pageParam: RQPageParam}) { - const res = await agent.getLikes({ - uri: resolvedUri || '', + return await client.call(app.bsky.feed.getLikes, { + // the enabled flag prevents this from running until resolvedUri is set + uri: (resolvedUri || '') as AtUriString, limit: PAGE_SIZE, cursor: pageParam, }) - return res.data }, initialPageParam: undefined, getNextPageParam: lastPage => lastPage.cursor, @@ -59,12 +61,14 @@ export const createLikedBySampleQueryKey = (args: {uri: string}) => * perturb the liked-by screen's pagination. */ export function useLikedBySampleQuery({uri}: {uri: string | undefined}) { - const agent = useAgent() + const client = useAppviewClient() return useQuery({ queryKey: createLikedBySampleQueryKey({uri: uri ?? ''}), queryFn: async () => { - const res = await agent.getLikes({uri: uri ?? '', limit: SAMPLE_SIZE}) - return res.data + return await client.call(app.bsky.feed.getLikes, { + uri: (uri ?? '') as AtUriString, + limit: SAMPLE_SIZE, + }) }, staleTime: STALE.MINUTES.FIVE, enabled: !!uri, @@ -81,7 +85,7 @@ export function* findAllProfilesInQueryData( did: string, ): Generator { const queryDatas = queryClient.getQueriesData< - InfiniteData + InfiniteData >({ queryKey: [RQKEY_ROOT], }) @@ -98,7 +102,7 @@ export function* findAllProfilesInQueryData( } } const sampleQueryDatas = - queryClient.getQueriesData({ + queryClient.getQueriesData({ queryKey: [likedBySampleQueryKeyRoot], }) for (const [_queryKey, queryData] of sampleQueryDatas) { diff --git a/src/state/queries/post-quotes.ts b/src/state/queries/post-quotes.ts index 1d0fa07e8e..77b5dabcf8 100644 --- a/src/state/queries/post-quotes.ts +++ b/src/state/queries/post-quotes.ts @@ -2,9 +2,9 @@ import { type AppBskyActorDefs, AppBskyEmbedRecord, type AppBskyFeedDefs, - type AppBskyFeedGetQuotes, AtUri, } from '@atproto/api' +import {type AtUriString} from '@atproto/syntax' import { type InfiniteData, type QueryClient, @@ -12,7 +12,8 @@ import { useInfiniteQuery, } from '@tanstack/react-query' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' +import {app} from '#/lexicons' import { didOrHandleUriMatches, embedViewRecordToPostView, @@ -26,22 +27,22 @@ const RQKEY_ROOT = 'post-quotes' export const RQKEY = (resolvedUri: string) => [RQKEY_ROOT, resolvedUri] export function usePostQuotesQuery(resolvedUri: string | undefined) { - const agent = useAgent() + const client = useAppviewClient() return useInfiniteQuery< - AppBskyFeedGetQuotes.OutputSchema, + app.bsky.feed.getQuotes.$OutputBody, Error, - InfiniteData, + InfiniteData, QueryKey, RQPageParam >({ queryKey: RQKEY(resolvedUri || ''), async queryFn({pageParam}: {pageParam: RQPageParam}) { - const res = await agent.api.app.bsky.feed.getQuotes({ - uri: resolvedUri || '', + return await client.call(app.bsky.feed.getQuotes, { + // the enabled flag prevents this from running until resolvedUri is set + uri: (resolvedUri || '') as AtUriString, limit: PAGE_SIZE, cursor: pageParam, }) - return res.data }, initialPageParam: undefined, getNextPageParam: lastPage => lastPage.cursor, @@ -72,7 +73,7 @@ export function* findAllProfilesInQueryData( did: string, ): Generator { const queryDatas = queryClient.getQueriesData< - InfiniteData + InfiniteData >({ queryKey: [RQKEY_ROOT], }) @@ -99,7 +100,7 @@ export function* findAllPostsInQueryData( uri: string, ): Generator { const queryDatas = queryClient.getQueriesData< - InfiniteData + InfiniteData >({ queryKey: [RQKEY_ROOT], }) diff --git a/src/state/queries/post-reposted-by.ts b/src/state/queries/post-reposted-by.ts index 814a815aae..d2c5e62385 100644 --- a/src/state/queries/post-reposted-by.ts +++ b/src/state/queries/post-reposted-by.ts @@ -1,7 +1,5 @@ -import { - type AppBskyActorDefs, - type AppBskyFeedGetRepostedBy, -} from '@atproto/api' +import {type AppBskyActorDefs} from '@atproto/api' +import {type AtUriString} from '@atproto/syntax' import { type InfiniteData, type QueryClient, @@ -9,7 +7,8 @@ import { useInfiniteQuery, } from '@tanstack/react-query' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' +import {app} from '#/lexicons' const PAGE_SIZE = 30 type RQPageParam = string | undefined @@ -19,22 +18,22 @@ const RQKEY_ROOT = 'post-reposted-by' export const RQKEY = (resolvedUri: string) => [RQKEY_ROOT, resolvedUri] export function usePostRepostedByQuery(resolvedUri: string | undefined) { - const agent = useAgent() + const client = useAppviewClient() return useInfiniteQuery< - AppBskyFeedGetRepostedBy.OutputSchema, + app.bsky.feed.getRepostedBy.$OutputBody, Error, - InfiniteData, + InfiniteData, QueryKey, RQPageParam >({ queryKey: RQKEY(resolvedUri || ''), async queryFn({pageParam}: {pageParam: RQPageParam}) { - const res = await agent.getRepostedBy({ - uri: resolvedUri || '', + return await client.call(app.bsky.feed.getRepostedBy, { + // the enabled flag prevents this from running until resolvedUri is set + uri: (resolvedUri || '') as AtUriString, limit: PAGE_SIZE, cursor: pageParam, }) - return res.data }, initialPageParam: undefined, getNextPageParam: lastPage => lastPage.cursor, @@ -47,7 +46,7 @@ export function* findAllProfilesInQueryData( did: string, ): Generator { const queryDatas = queryClient.getQueriesData< - InfiniteData + InfiniteData >({ queryKey: [RQKEY_ROOT], }) From f5c166a055af390d2c67a4c4d6e9e2789c6e7ad7 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 13 Aug 2026 22:26:17 +0300 Subject: [PATCH 14/32] [SDK] Migrate the notifications cluster to the lex clients (#11362) Co-authored-by: Claude Fable 5 --- .../SubscribeProfileDialog.tsx | 14 ++++--- src/state/cache/thread-mutes.tsx | 15 ++++--- src/state/queries/activity-subscriptions.ts | 17 ++++---- src/state/queries/notifications/feed.ts | 6 +-- src/state/queries/notifications/settings.ts | 35 ++++++++-------- src/state/queries/notifications/unread.tsx | 19 +++++---- src/state/queries/notifications/util.ts | 40 +++++++++++-------- .../notifications/NotificationFeedItem.tsx | 22 +++++----- 8 files changed, 94 insertions(+), 74 deletions(-) diff --git a/src/components/activity-notifications/SubscribeProfileDialog.tsx b/src/components/activity-notifications/SubscribeProfileDialog.tsx index 8a6b8314cd..6731cb3038 100644 --- a/src/components/activity-notifications/SubscribeProfileDialog.tsx +++ b/src/components/activity-notifications/SubscribeProfileDialog.tsx @@ -2,10 +2,10 @@ import {useMemo, useState} from 'react' import {View} from 'react-native' import { type AppBskyNotificationDefs, - type AppBskyNotificationListActivitySubscriptions, type ModerationOpts, type Un$Typed, } from '@atproto/api' +import {type DidString} from '@atproto/syntax' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' @@ -20,7 +20,7 @@ import {cleanError} from '#/lib/strings/errors' import {sanitizeHandle} from '#/lib/strings/handles' import {updateProfileShadow} from '#/state/cache/profile-shadow' import {RQKEY_getActivitySubscriptions} from '#/state/queries/activity-subscriptions' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' import {atoms as a, platform, useTheme, web} from '#/alf' import {Admonition} from '#/components/Admonition' import { @@ -37,6 +37,7 @@ import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' import {IS_WEB} from '#/env' +import {app} from '#/lexicons' import type * as bsky from '#/types/bsky' export function SubscribeProfileDialog({ @@ -74,7 +75,7 @@ function DialogInner({ const ax = useAnalytics() const {_} = useLingui() const t = useTheme() - const agent = useAgent() + const client = useAppviewClient() const control = Dialog.useDialogContext() const queryClient = useQueryClient() const initialState = parseActivitySubscription( @@ -122,8 +123,9 @@ function DialogInner({ mutationFn: async ( activitySubscription: Un$Typed, ) => { - await agent.app.bsky.notification.putActivitySubscription({ - subject: profile.did, + await client.call(app.bsky.notification.putActivitySubscription, { + // the profile view carries an already-resolved did + subject: profile.did as DidString, activitySubscription, }) }, @@ -148,7 +150,7 @@ function DialogInner({ queryClient.setQueryData( RQKEY_getActivitySubscriptions, ( - old?: InfiniteData, + old?: InfiniteData, ) => { if (!old) return old return { diff --git a/src/state/cache/thread-mutes.tsx b/src/state/cache/thread-mutes.tsx index f0d9911348..17f6be9f34 100644 --- a/src/state/cache/thread-mutes.tsx +++ b/src/state/cache/thread-mutes.tsx @@ -5,9 +5,11 @@ import { useEffect, useState, } from 'react' +import {type AtUriString} from '@atproto/syntax' import * as persisted from '#/state/persisted' -import {useAgent, useSession} from '../session' +import {app} from '#/lexicons' +import {useAppviewClient, useSession} from '../session' type StateContext = Map type SetStateContext = (uri: string, value: boolean) => void @@ -56,7 +58,7 @@ export function useSetThreadMute() { } function useMigrateMutes(setThreadMute: SetStateContext) { - const agent = useAgent() + const client = useAppviewClient() const {currentAccount} = useSession() useEffect(() => { @@ -87,8 +89,11 @@ function useMigrateMutes(setThreadMute: SetStateContext) { setThreadMute(root, true) - await agent.api.app.bsky.graph - .muteThread({root}) + await client + .call(app.bsky.graph.muteThread, { + // the persisted list only ever holds post at-uris + root: root as AtUriString, + }) // not a big deal if this fails, since the post might have been deleted .catch(console.error) } @@ -100,5 +105,5 @@ function useMigrateMutes(setThreadMute: SetStateContext) { cancelled = true } } - }, [agent, currentAccount, setThreadMute]) + }, [client, currentAccount, setThreadMute]) } diff --git a/src/state/queries/activity-subscriptions.ts b/src/state/queries/activity-subscriptions.ts index 30964fe7d6..e6dcbd8829 100644 --- a/src/state/queries/activity-subscriptions.ts +++ b/src/state/queries/activity-subscriptions.ts @@ -1,7 +1,6 @@ import { type AppBskyActorDefs, type AppBskyNotificationDeclaration, - type AppBskyNotificationListActivitySubscriptions, } from '@atproto/api' import {t} from '@lingui/core/macro' import { @@ -13,23 +12,23 @@ import { useQueryClient, } from '@tanstack/react-query' -import {useAgent, useSession} from '#/state/session' +import {useAgent, useAppviewClient, useSession} from '#/state/session' import * as Toast from '#/components/Toast' +import {app} from '#/lexicons' export const RQKEY_getActivitySubscriptions = ['activity-subscriptions'] export const RQKEY_getNotificationDeclaration = ['notification-declaration'] export function useActivitySubscriptionsQuery() { - const agent = useAgent() + const client = useAppviewClient() return useInfiniteQuery({ queryKey: RQKEY_getActivitySubscriptions, queryFn: async ({pageParam}) => { - const response = - await agent.app.bsky.notification.listActivitySubscriptions({ - cursor: pageParam, - }) - return response.data + return await client.call( + app.bsky.notification.listActivitySubscriptions, + {cursor: pageParam}, + ) }, initialPageParam: undefined as string | undefined, getNextPageParam: prev => prev.cursor, @@ -111,7 +110,7 @@ export function* findAllProfilesInQueryData( did: string, ): Generator { const queryDatas = queryClient.getQueriesData< - InfiniteData + InfiniteData >({ queryKey: RQKEY_getActivitySubscriptions, }) diff --git a/src/state/queries/notifications/feed.ts b/src/state/queries/notifications/feed.ts index cc24d3d0fc..51740e8d50 100644 --- a/src/state/queries/notifications/feed.ts +++ b/src/state/queries/notifications/feed.ts @@ -33,7 +33,7 @@ import { import {useModerationOpts} from '#/state/preferences/moderation-opts' import {STALE} from '#/state/queries' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' import {useThreadgateHiddenReplyUris} from '#/state/threadgate-hidden-replies' import type * as bsky from '#/types/bsky' import { @@ -60,7 +60,7 @@ export function useNotificationFeedQuery(opts: { enabled?: boolean filter: 'all' | 'mentions' }) { - const agent = useAgent() + const client = useAppviewClient() const queryClient = useQueryClient() const moderationOpts = useModerationOpts() const unreads = useUnreadNotificationsApi() @@ -106,7 +106,7 @@ export function useNotificationFeedQuery(opts: { ] } const {page: fetchedPage} = await fetchPage({ - agent, + client, limit: PAGE_SIZE, cursor: pageParam, queryClient, diff --git a/src/state/queries/notifications/settings.ts b/src/state/queries/notifications/settings.ts index 86e8a6c7f1..2cadb519dc 100644 --- a/src/state/queries/notifications/settings.ts +++ b/src/state/queries/notifications/settings.ts @@ -10,10 +10,10 @@ import { useQueryClient, } from '@tanstack/react-query' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import {logger} from '#/logger' -import {useAgent} from '#/state/session' +import {useAppviewClient, useChatClient} from '#/state/session' import * as Toast from '#/components/Toast' +import {app, chat} from '#/lexicons' const RQKEY_ROOT = 'notification-settings' const RQKEY_APP = [RQKEY_ROOT, 'app'] @@ -67,13 +67,13 @@ type ChatNotificationSettingsUpdate = export function useNotificationSettingsQuery({ enabled, }: {enabled?: boolean} = {}) { - const agent = useAgent() + const client = useAppviewClient() return useQuery({ queryKey: RQKEY_APP, queryFn: async (): Promise => { - const res = await agent.app.bsky.notification.getPreferences() - return appPreferencesWithoutChat(res.data.preferences) + const data = await client.call(app.bsky.notification.getPreferences) + return appPreferencesWithoutChat(data.preferences) }, enabled, }) @@ -82,21 +82,24 @@ export function useNotificationSettingsQuery({ export function useChatNotificationSettingsQuery({ enabled, }: {enabled?: boolean} = {}) { - const agent = useAgent() + const client = useChatClient() return useQuery({ queryKey: RQKEY_CHAT, queryFn: async (): Promise => { - const res = await agent.chat.bsky.notification.getPreferences(undefined, { - headers: DM_SERVICE_HEADERS, - }) - return chatPreferencesForSettings(res.data.preferences) + const data = await client.call(chat.bsky.notification.getPreferences) + return chatPreferencesForSettings(data.preferences) }, enabled, }) } export function useNotificationSettingsUpdateMutation() { - const agent = useAgent() + /* + * App preferences live on the appview and chat preferences on the chat + * service, so a combined update fans out over both clients. + */ + const appviewClient = useAppviewClient() + const chatClient = useChatClient() const queryClient = useQueryClient() return useMutation({ @@ -104,13 +107,13 @@ export function useNotificationSettingsUpdateMutation() { const {appUpdate, chatUpdate} = splitNotificationSettingsUpdate(update) await Promise.all([ hasUpdates(appUpdate) - ? agent.app.bsky.notification.putPreferencesV2(appUpdate) + ? appviewClient.call( + app.bsky.notification.putPreferencesV2, + appUpdate, + ) : undefined, hasUpdates(chatUpdate) - ? agent.chat.bsky.notification.putPreferences(chatUpdate, { - headers: DM_SERVICE_HEADERS, - encoding: 'application/json', - }) + ? chatClient.call(chat.bsky.notification.putPreferences, chatUpdate) : undefined, ]) }, diff --git a/src/state/queries/notifications/unread.tsx b/src/state/queries/notifications/unread.tsx index bf7505f91b..e905b29ac6 100644 --- a/src/state/queries/notifications/unread.tsx +++ b/src/state/queries/notifications/unread.tsx @@ -11,6 +11,7 @@ import { useState, } from 'react' import {AppState} from 'react-native' +import {type ISODatetimeString} from '@atproto/syntax' import {useQueryClient} from '@tanstack/react-query' import {EventEmitter} from 'eventemitter3' @@ -18,7 +19,8 @@ import BroadcastChannel from '#/lib/broadcast' import {resetBadgeCount} from '#/lib/notifications/notifications' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {truncateAndInvalidate} from '#/state/queries/util' -import {useAgent, useSession} from '#/state/session' +import {useAppviewClient, useSession} from '#/state/session' +import {app} from '#/lexicons' import {RQKEY as RQKEY_NOTIFS} from './feed' import {type CachedFeedPage, type FeedPage} from './types' import {fetchPage} from './util' @@ -52,7 +54,7 @@ apiContext.displayName = 'NotificationsUnreadApiContext' export function Provider({children}: React.PropsWithChildren<{}>) { const {hasSession} = useSession() - const agent = useAgent() + const client = useAppviewClient() const queryClient = useQueryClient() const moderationOpts = useModerationOpts() @@ -120,9 +122,10 @@ export function Provider({children}: React.PropsWithChildren<{}>) { return { async markAllRead() { // update server - await agent.updateSeenNotifications( - cacheRef.current.syncedAt.toISOString(), - ) + await client.call(app.bsky.notification.updateSeen, { + // toISOString always emits the Z-suffixed form the format requires + seenAt: cacheRef.current.syncedAt.toISOString() as ISODatetimeString, + }) // update & broadcast setNumUnread('') @@ -135,7 +138,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { isPoll, }: {invalidate?: boolean; isPoll?: boolean} = {}) { try { - if (!agent.session) return + if (!hasSession) return if (AppState.currentState !== 'active') { return } @@ -156,7 +159,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { // count const {page, indexedAt: lastIndexed} = await fetchPage({ - agent, + client, cursor: undefined, limit: 40, queryClient, @@ -207,7 +210,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { } }, } - }, [setNumUnread, queryClient, moderationOpts, agent]) + }, [setNumUnread, queryClient, moderationOpts, client, hasSession]) checkUnreadRef.current = api.checkUnread return ( diff --git a/src/state/queries/notifications/util.ts b/src/state/queries/notifications/util.ts index e5d1d81885..4606507700 100644 --- a/src/state/queries/notifications/util.ts +++ b/src/state/queries/notifications/util.ts @@ -6,15 +6,17 @@ import { type AppBskyGraphDefs, AppBskyGraphStarterpack, type AppBskyNotificationListNotifications, - type AtpAgent, hasMutedWord, moderateNotification, type ModerationOpts, } from '@atproto/api' +import {type Client} from '@atproto/lex' +import {type AtUriString} from '@atproto/syntax' import {type QueryClient} from '@tanstack/react-query' import chunk from 'lodash.chunk' import {labelIsHideableOffense} from '#/lib/moderation' +import {app} from '#/lexicons' import * as bsky from '#/types/bsky' import {precacheProfile} from '../profile' import { @@ -38,7 +40,7 @@ const MS_2DAY = MS_1HR * 48 // = export async function fetchPage({ - agent, + client, cursor, limit, queryClient, @@ -46,7 +48,7 @@ export async function fetchPage({ fetchAdditionalData, reasons, }: { - agent: AtpAgent + client: Client cursor: string | undefined limit: number queryClient: QueryClient @@ -57,16 +59,16 @@ export async function fetchPage({ page: FeedPage indexedAt: string | undefined }> { - const res = await agent.listNotifications({ + const data = await client.call(app.bsky.notification.listNotifications, { limit, cursor, reasons, }) - const indexedAt = res.data.notifications[0]?.indexedAt + const indexedAt = data.notifications[0]?.indexedAt // filter out notifs by mod rules - const notifs = res.data.notifications.filter( + const notifs = data.notifications.filter( notif => !shouldFilterNotif(notif, moderationOpts), ) @@ -76,7 +78,7 @@ export async function fetchPage({ // we fetch subjects of notifications (usually posts) now instead of lazily // in the UI to avoid relayouts if (fetchAdditionalData) { - const subjects = await fetchSubjects(agent, notifsGrouped) + const subjects = await fetchSubjects(client, notifsGrouped) for (const notif of notifsGrouped) { if (notif.subjectUri) { if ( @@ -96,17 +98,17 @@ export async function fetchPage({ } } - let seenAt = res.data.seenAt ? new Date(res.data.seenAt) : new Date() + let seenAt = data.seenAt ? new Date(data.seenAt) : new Date() if (Number.isNaN(seenAt.getTime())) { seenAt = new Date() } return { page: { - cursor: res.data.cursor, + cursor: data.cursor, seenAt, items: notifsGrouped, - priority: res.data.priority ?? false, + priority: data.priority ?? false, }, indexedAt, } @@ -207,7 +209,7 @@ export function groupNotifications( } async function fetchSubjects( - agent: AtpAgent, + client: Client, groupedNotifs: FeedNotification[], ): Promise<{ posts: Map @@ -224,18 +226,22 @@ async function fetchSubjects( packUris.add(notif.notification.reasonSubject) } } - const postUriChunks = chunk(Array.from(postUris), 25) - const packUriChunks = chunk(Array.from(packUris), 25) + /* + * Both uri sets are collected from notification fields the server already + * validated as at-uris, so the branded cast reflects what the values are. + */ + const postUriChunks = chunk(Array.from(postUris) as AtUriString[], 25) + const packUriChunks = chunk(Array.from(packUris) as AtUriString[], 25) const postsChunks = await Promise.all( postUriChunks.map(uris => - agent.app.bsky.feed.getPosts({uris}).then(res => res.data.posts), + client.call(app.bsky.feed.getPosts, {uris}).then(data => data.posts), ), ) const packsChunks = await Promise.all( packUriChunks.map(uris => - agent.app.bsky.graph - .getStarterPacks({uris}) - .then(res => res.data.starterPacks), + client + .call(app.bsky.graph.getStarterPacks, {uris}) + .then(data => data.starterPacks), ), ) const postsMap = new Map() diff --git a/src/view/com/notifications/NotificationFeedItem.tsx b/src/view/com/notifications/NotificationFeedItem.tsx index c11cb8049b..2afb3a0f43 100644 --- a/src/view/com/notifications/NotificationFeedItem.tsx +++ b/src/view/com/notifications/NotificationFeedItem.tsx @@ -21,12 +21,13 @@ import { type ModerationOpts, } from '@atproto/api' import {TID} from '@atproto/common-web' +import {type DidString} from '@atproto/syntax' import {plural} from '@lingui/core/macro' import {Plural, Trans, useLingui} from '@lingui/react/macro' import {useNavigation} from '@react-navigation/native' import {useQueryClient} from '@tanstack/react-query' -import {DM_SERVICE_HEADERS, MAX_POST_LINES} from '#/lib/constants' +import {MAX_POST_LINES} from '#/lib/constants' import {useAnimatedValue} from '#/lib/hooks/useAnimatedValue' import {makeProfileLink} from '#/lib/routes/links' import {type NavigationProp} from '#/lib/routes/types' @@ -38,7 +39,7 @@ import {useProfileShadow} from '#/state/cache/profile-shadow' import {type FeedNotification} from '#/state/queries/notifications/feed' import {useProfileFollowMutationQueue} from '#/state/queries/profile' import {unstableCacheProfileView} from '#/state/queries/unstable-profile-cache' -import {useAgent, useSession} from '#/state/session' +import {useChatClient, useSession} from '#/state/session' import {FeedSourceCard} from '#/view/com/feeds/FeedSourceCard' import {Post} from '#/view/com/post/Post' import {formatCount} from '#/view/com/util/numeric/format' @@ -73,6 +74,7 @@ import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' import {IS_WEB} from '#/env' +import {chat} from '#/lexicons' import * as bsky from '#/types/bsky' const MAX_AUTHORS = 5 @@ -911,21 +913,21 @@ function FollowBackButton({profile}: {profile: AppBskyActorDefs.ProfileView}) { function SayHelloBtn({profile}: {profile: AppBskyActorDefs.ProfileView}) { const {t: l} = useLingui() - const agent = useAgent() + const client = useChatClient() + const {currentAccount} = useSession() const navigation = useNavigation() const [isLoading, setIsLoading] = useState(false) const onPress = async () => { try { setIsLoading(true) - const res = await agent.api.chat.bsky.convo.getConvoForMembers( - { - members: [profile.did, agent.session!.did], - }, - {headers: DM_SERVICE_HEADERS}, - ) + const data = await client.call(chat.bsky.convo.getConvoForMembers, { + // both dids are already resolved - one from the profile view, one from + // the active session + members: [profile.did, currentAccount!.did] as DidString[], + }) navigation.navigate('MessagesConversation', { - conversation: res.data.convo.id, + conversation: data.convo.id, }) } catch (e) { logger.error('Failed to get conversation', {safeMessage: e}) From 34d1c75756b5d4320b870a611d995a17273b37de Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 13 Aug 2026 22:26:17 +0300 Subject: [PATCH 15/32] [SDK] Migrate the account surface to the pds and service clients (#11367) Co-authored-by: Claude Fable 5 --- oxlint-suppressions.json | 6 --- .../EmailDialog/data/useRequestEmailUpdate.ts | 7 +-- .../data/useRequestEmailVerification.ts | 7 +-- .../intents/VerifyEmailIntentDialog.tsx | 7 +-- src/lib/lexClient.ts | 20 +++++++++ src/screens/Login/ForgotPasswordForm.tsx | 14 +++--- src/screens/Login/SetNewPasswordForm.tsx | 11 +++-- src/screens/Settings/AppPasswords.tsx | 4 +- .../components/AddAppPasswordDialog.tsx | 4 +- .../components/ChangePasswordDialog.tsx | 14 +++--- .../components/DeactivateAccountDialog.tsx | 9 ++-- .../components/DeleteAccountDialog.tsx | 38 +++++++++------- src/state/queries/app-passwords.ts | 28 ++++++------ src/state/queries/handle-availability.ts | 43 ++++++++++++------- src/state/queries/handle.ts | 12 ++++-- src/state/queries/service.ts | 12 ++++-- 16 files changed, 148 insertions(+), 88 deletions(-) diff --git a/oxlint-suppressions.json b/oxlint-suppressions.json index 197bd43b1a..3b2304e8fe 100644 --- a/oxlint-suppressions.json +++ b/oxlint-suppressions.json @@ -1099,12 +1099,6 @@ }, "typescript/no-misused-promises": { "count": 2 - }, - "typescript/no-unsafe-call": { - "count": 2 - }, - "typescript/no-unsafe-member-access": { - "count": 2 } }, "src/screens/Settings/components/CopyButton.tsx": { diff --git a/src/components/dialogs/EmailDialog/data/useRequestEmailUpdate.ts b/src/components/dialogs/EmailDialog/data/useRequestEmailUpdate.ts index a442662fcf..d6cd418444 100644 --- a/src/components/dialogs/EmailDialog/data/useRequestEmailUpdate.ts +++ b/src/components/dialogs/EmailDialog/data/useRequestEmailUpdate.ts @@ -1,13 +1,14 @@ import {useMutation} from '@tanstack/react-query' -import {useAgent} from '#/state/session' +import {usePdsClient} from '#/state/session' +import {com} from '#/lexicons' export function useRequestEmailUpdate() { - const agent = useAgent() + const client = usePdsClient() return useMutation({ mutationFn: async () => { - return (await agent.com.atproto.server.requestEmailUpdate()).data + return await client.call(com.atproto.server.requestEmailUpdate) }, }) } diff --git a/src/components/dialogs/EmailDialog/data/useRequestEmailVerification.ts b/src/components/dialogs/EmailDialog/data/useRequestEmailVerification.ts index ae308c7afc..4152e61e58 100644 --- a/src/components/dialogs/EmailDialog/data/useRequestEmailVerification.ts +++ b/src/components/dialogs/EmailDialog/data/useRequestEmailVerification.ts @@ -1,13 +1,14 @@ import {useMutation} from '@tanstack/react-query' -import {useAgent} from '#/state/session' +import {usePdsClient} from '#/state/session' +import {com} from '#/lexicons' export function useRequestEmailVerification() { - const agent = useAgent() + const client = usePdsClient() return useMutation({ mutationFn: async () => { - await agent.com.atproto.server.requestEmailConfirmation() + await client.call(com.atproto.server.requestEmailConfirmation) }, }) } diff --git a/src/components/intents/VerifyEmailIntentDialog.tsx b/src/components/intents/VerifyEmailIntentDialog.tsx index b3504766f8..272804af0e 100644 --- a/src/components/intents/VerifyEmailIntentDialog.tsx +++ b/src/components/intents/VerifyEmailIntentDialog.tsx @@ -4,7 +4,7 @@ import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' -import {useAgent, useSession} from '#/state/session' +import {usePdsClient, useSession} from '#/state/session' import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' @@ -16,6 +16,7 @@ import {useIntentDialogs} from '#/components/intents/IntentDialogs' import {Loader} from '#/components/Loader' import {Text} from '#/components/Typography' import {IS_NATIVE} from '#/env' +import {com} from '#/lexicons' export function VerifyEmailIntentDialog() { const {verifyEmailDialogControl: control} = useIntentDialogs() @@ -37,7 +38,7 @@ function Inner({}: {control: DialogControlProps}) { 'loading' | 'success' | 'failure' | 'resent' >('loading') const [sending, setSending] = useState(false) - const agent = useAgent() + const client = usePdsClient() const {currentAccount} = useSession() const {mutate: confirmEmail} = useConfirmEmail({ onSuccess: () => setStatus('success'), @@ -52,7 +53,7 @@ function Inner({}: {control: DialogControlProps}) { const onPressResendEmail = async () => { setSending(true) - await agent.com.atproto.server.requestEmailConfirmation() + await client.call(com.atproto.server.requestEmailConfirmation) setSending(false) setStatus('resent') } diff --git a/src/lib/lexClient.ts b/src/lib/lexClient.ts index 40b0640665..2a865d763c 100644 --- a/src/lib/lexClient.ts +++ b/src/lib/lexClient.ts @@ -24,3 +24,23 @@ export function createLexClient( ): Client { return new Client(agent, {strictResponseProcessing: false, ...options}) } + +/** + * An unauthenticated {@link Client} pointed at an arbitrary service URL, for + * the PRE-AUTH flows that talk to a host the user typed or picked (hosting + * provider description, password reset, handle availability). Those requests + * cannot go through a session-scoped client, because there is no session yet. + * + * Deliberately NOT memoized: `service` is user-supplied and changes as the user + * edits it, so there is no stable key to cache on. Each call builds a fresh + * client, mirroring the ad-hoc agents these call sites constructed inline. That + * keeps client identity per-render, so callers must not put the returned client + * in a React Query key or a dependency array. + * + * Requests use PLAIN `fetch`, not `networkAwareFetch`: the host is untrusted + * input, and a typo'd or dead service must not be reported as the app losing + * network reachability. + */ +export function createServiceClient(service: string): Client { + return createLexClient({service}) +} diff --git a/src/screens/Login/ForgotPasswordForm.tsx b/src/screens/Login/ForgotPasswordForm.tsx index 79fdbc5d6a..1e9eda430e 100644 --- a/src/screens/Login/ForgotPasswordForm.tsx +++ b/src/screens/Login/ForgotPasswordForm.tsx @@ -1,12 +1,11 @@ import {useCallback, useState} from 'react' import {Keyboard, View} from 'react-native' -import {type ComAtprotoServerDescribeServer} from '@atproto/api' import {Trans, useLingui} from '@lingui/react/macro' import * as EmailValidator from 'email-validator' +import {createServiceClient} from '#/lib/lexClient' import {cleanError, isNetworkError} from '#/lib/strings/errors' import {logger} from '#/logger' -import {Agent} from '#/state/session/agent' import {atoms as a, useTheme, web} from '#/alf' import {Admonition} from '#/components/Admonition' import {Button, ButtonIcon, ButtonText} from '#/components/Button' @@ -16,9 +15,10 @@ import {At_Stroke2_Corner0_Rounded as At} from '#/components/icons/At' import {Loader} from '#/components/Loader' import {Text} from '#/components/Typography' import {IS_WEB} from '#/env' +import {com} from '#/lexicons' import {FormContainer} from './FormContainer' -type ServiceDescription = ComAtprotoServerDescribeServer.OutputSchema +type ServiceDescription = com.atproto.server.describeServer.$OutputBody export const ForgotPasswordForm = ({ error, @@ -55,8 +55,12 @@ export const ForgotPasswordForm = ({ setIsProcessing(true) try { - const agent = new Agent(null, {service: serviceUrl}) - await agent.com.atproto.server.requestPasswordReset({email}) + /* + * Pre-auth request against a user-chosen host, so it goes through a + * one-off service client rather than a session-scoped one. + */ + const client = createServiceClient(serviceUrl) + await client.call(com.atproto.server.requestPasswordReset, {email}) onEmailSent() } catch (err) { logger.warn('Failed to request password reset', {error: err}) diff --git a/src/screens/Login/SetNewPasswordForm.tsx b/src/screens/Login/SetNewPasswordForm.tsx index cdc8fe4e0f..f870171be8 100644 --- a/src/screens/Login/SetNewPasswordForm.tsx +++ b/src/screens/Login/SetNewPasswordForm.tsx @@ -2,10 +2,10 @@ import {useState} from 'react' import {View} from 'react-native' import {Trans, useLingui} from '@lingui/react/macro' +import {createServiceClient} from '#/lib/lexClient' import {cleanError, isNetworkError} from '#/lib/strings/errors' import {checkAndFormatResetCode} from '#/lib/strings/password' import {logger} from '#/logger' -import {Agent} from '#/state/session/agent' import {atoms as a, web} from '#/alf' import {Admonition} from '#/components/Admonition' import {Button, ButtonIcon, ButtonText} from '#/components/Button' @@ -16,6 +16,7 @@ import {Loader} from '#/components/Loader' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' import {IS_WEB} from '#/env' +import {com} from '#/lexicons' import {FormContainer} from './FormContainer' export const SetNewPasswordForm = ({ @@ -61,8 +62,12 @@ export const SetNewPasswordForm = ({ setIsProcessing(true) try { - const agent = new Agent(null, {service: serviceUrl}) - await agent.com.atproto.server.resetPassword({ + /* + * Pre-auth request against a user-chosen host, so it goes through a + * one-off service client rather than a session-scoped one. + */ + const client = createServiceClient(serviceUrl) + await client.call(com.atproto.server.resetPassword, { token: formattedCode, password, }) diff --git a/src/screens/Settings/AppPasswords.tsx b/src/screens/Settings/AppPasswords.tsx index 65ebdf3d4f..49173b9629 100644 --- a/src/screens/Settings/AppPasswords.tsx +++ b/src/screens/Settings/AppPasswords.tsx @@ -6,7 +6,6 @@ import Animated, { LayoutAnimationConfig, LinearTransition, } from 'react-native-reanimated' -import {type ComAtprotoServerListAppPasswords} from '@atproto/api' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' @@ -33,6 +32,7 @@ import {Loader} from '#/components/Loader' import * as Prompt from '#/components/Prompt' import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' +import {type com} from '#/lexicons' import {AddAppPasswordDialog} from './components/AddAppPasswordDialog' import * as SettingsList from './components/SettingsList' @@ -135,7 +135,7 @@ export function AppPasswordsScreen({}: Props) { function AppPasswordCard({ appPassword, }: { - appPassword: ComAtprotoServerListAppPasswords.AppPassword + appPassword: com.atproto.server.listAppPasswords.AppPassword }) { const t = useTheme() const {i18n, _} = useLingui() diff --git a/src/screens/Settings/components/AddAppPasswordDialog.tsx b/src/screens/Settings/components/AddAppPasswordDialog.tsx index 5029ad9f16..21b9f1ea1d 100644 --- a/src/screens/Settings/components/AddAppPasswordDialog.tsx +++ b/src/screens/Settings/components/AddAppPasswordDialog.tsx @@ -8,7 +8,6 @@ import Animated, { SlideInRight, SlideOutLeft, } from 'react-native-reanimated' -import {type ComAtprotoServerCreateAppPassword} from '@atproto/api' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' @@ -25,6 +24,7 @@ import {ChevronRight_Stroke2_Corner0_Rounded as ChevronRight} from '#/components import {SquareBehindSquare4_Stroke2_Corner0_Rounded as CopyIcon} from '#/components/icons/SquareBehindSquare4' import {Text} from '#/components/Typography' import {IS_WEB} from '#/env' +import {type com} from '#/lexicons' import {CopyButton} from './CopyButton' export function AddAppPasswordDialog({ @@ -70,7 +70,7 @@ function CreateDialogInner({passwords}: {passwords: string[]}) { error: validationError, isPending, } = useMutation< - ComAtprotoServerCreateAppPassword.AppPassword, + com.atproto.server.createAppPassword.AppPassword, Error | DisplayableError >({ mutationFn: async () => { diff --git a/src/screens/Settings/components/ChangePasswordDialog.tsx b/src/screens/Settings/components/ChangePasswordDialog.tsx index 91cb671792..3015b402b5 100644 --- a/src/screens/Settings/components/ChangePasswordDialog.tsx +++ b/src/screens/Settings/components/ChangePasswordDialog.tsx @@ -7,8 +7,9 @@ import * as EmailValidator from 'email-validator' import {cleanError, isNetworkError} from '#/lib/strings/errors' import {checkAndFormatResetCode} from '#/lib/strings/password' +import {matchXrpcError} from '#/lib/xrpc-error' import {logger} from '#/logger' -import {useAgent, useSession} from '#/state/session' +import {usePdsClient, useSession} from '#/state/session' import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' import {android, atoms as a, web} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' @@ -17,6 +18,7 @@ import * as TextField from '#/components/forms/TextField' import {Loader} from '#/components/Loader' import {Text} from '#/components/Typography' import {IS_NATIVE} from '#/env' +import {com} from '#/lexicons' enum Stages { RequestCode = 'RequestCode', @@ -44,7 +46,7 @@ export function ChangePasswordDialog({ function Inner() { const {_} = useLingui() const {currentAccount} = useSession() - const agent = useAgent() + const client = usePdsClient() const control = Dialog.useDialogContext() const [stage, setStage] = useState(Stages.RequestCode) @@ -85,7 +87,7 @@ function Inner() { setError('') setIsProcessing(true) try { - await agent.com.atproto.server.requestPasswordReset({ + await client.call(com.atproto.server.requestPasswordReset, { email: currentAccount.email, }) setStage(Stages.ChangePassword) @@ -129,7 +131,7 @@ function Inner() { setError('') setIsProcessing(true) try { - await agent.com.atproto.server.resetPassword({ + await client.call(com.atproto.server.resetPassword, { token: formattedCode, password: newPassword, }) @@ -141,7 +143,9 @@ function Inner() { msg`Unable to contact your service. Please check your internet connection and try again.`, ), ) - } else if (e?.toString().includes('Token is invalid')) { + } else if ( + matchXrpcError(e, com.atproto.server.resetPassword) === 'InvalidToken' + ) { setError(_(msg`This confirmation code is not valid. Please try again.`)) } else { logger.error('Failed to set new password', {safeMessage: e}) diff --git a/src/screens/Settings/components/DeactivateAccountDialog.tsx b/src/screens/Settings/components/DeactivateAccountDialog.tsx index 036ad38035..eaae97659e 100644 --- a/src/screens/Settings/components/DeactivateAccountDialog.tsx +++ b/src/screens/Settings/components/DeactivateAccountDialog.tsx @@ -5,7 +5,7 @@ import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' import {logger} from '#/logger' -import {useAgent, useSessionApi} from '#/state/session' +import {usePdsClient, useSessionApi} from '#/state/session' import {atoms as a, useTheme} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {type DialogOuterProps} from '#/components/Dialog' @@ -14,6 +14,7 @@ import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/ico import {Loader} from '#/components/Loader' import * as Prompt from '#/components/Prompt' import {Text} from '#/components/Typography' +import {com} from '#/lexicons' export function DeactivateAccountDialog({ control, @@ -34,7 +35,7 @@ function DeactivateAccountDialogInner({ }) { const t = useTheme() const {_} = useLingui() - const agent = useAgent() + const client = usePdsClient() const {logoutCurrentAccount} = useSessionApi() const [pending, setPending] = useState(false) const [error, setError] = useState() @@ -42,7 +43,7 @@ function DeactivateAccountDialogInner({ const handleDeactivate = useCallback(async () => { try { setPending(true) - await agent.com.atproto.server.deactivateAccount({}) + await client.call(com.atproto.server.deactivateAccount, {}) control.close(() => { logoutCurrentAccount('Deactivated') }) @@ -66,7 +67,7 @@ function DeactivateAccountDialogInner({ } finally { setPending(false) } - }, [agent, control, logoutCurrentAccount, _, setPending]) + }, [client, control, logoutCurrentAccount, _, setPending]) return ( <> diff --git a/src/screens/Settings/components/DeleteAccountDialog.tsx b/src/screens/Settings/components/DeleteAccountDialog.tsx index 9903fce3ba..0df008280b 100644 --- a/src/screens/Settings/components/DeleteAccountDialog.tsx +++ b/src/screens/Settings/components/DeleteAccountDialog.tsx @@ -1,14 +1,19 @@ import {useCallback, useRef, useState} from 'react' import {type TextInput, View} from 'react-native' +import {type DidString} from '@atproto/syntax' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import {useCleanError} from '#/lib/hooks/useCleanError' import {sanitizeHandle} from '#/lib/strings/handles' import {logger} from '#/logger' -import {useAgent, useSession, useSessionApi} from '#/state/session' +import { + useChatClient, + usePdsClient, + useSession, + useSessionApi, +} from '#/state/session' import {atoms as a, useTheme} from '#/alf' import {Admonition} from '#/components/Admonition' import {type DialogOuterProps} from '#/components/Dialog' @@ -24,6 +29,7 @@ import {Loader} from '#/components/Loader' import * as Prompt from '#/components/Prompt' import * as toast from '#/components/Toast' import {Span, Text} from '#/components/Typography' +import {chat, com} from '#/lexicons' import {resetToTab} from '#/Navigation' const WHITESPACE_RE = /\s/gu @@ -72,7 +78,8 @@ function DeleteAccountDialogInner({ const t = useTheme() const {_} = useLingui() const cleanError = useCleanError() - const agent = useAgent() + const client = usePdsClient() + const chatClient = useChatClient() const {currentAccount} = useSession() const {removeAccount} = useSessionApi() @@ -89,7 +96,7 @@ function DeleteAccountDialogInner({ } try { setEmailState(EmailState.PENDING) - await agent.com.atproto.server.requestAccountDelete() + await client.call(com.atproto.server.requestAccountDelete) setError('') setEmailSentCount(prevCount => prevCount + 1) setStep(Step.VERIFY_CODE) @@ -103,7 +110,7 @@ function DeleteAccountDialogInner({ } finally { setEmailState(EmailState.DEFAULT) } - }, [agent, cleanError, emailState, setEmailState]) + }, [client, cleanError, emailState, setEmailState]) const confirmDeletion = useCallback(async () => { try { @@ -112,15 +119,15 @@ function DeleteAccountDialogInner({ throw new Error('Invalid did') } const token = confirmCode.replace(WHITESPACE_RE, '') - // Inform chat service of intent to delete account. - const {success} = await agent.chat.bsky.actor.deleteAccount(undefined, { - headers: DM_SERVICE_HEADERS, - }) - if (!success) { - throw new Error('Failed to inform chat service of account deletion') - } - await agent.com.atproto.server.deleteAccount({ - did: currentAccount.did, + /* + * Inform chat service of intent to delete account. A non-2xx response + * throws, so reaching the next line means the chat service accepted it - + * the agent's `success` flag has no client-side equivalent. + */ + await chatClient.call(chat.bsky.actor.deleteAccount) + await client.call(com.atproto.server.deleteAccount, { + // the persisted account did is already resolved + did: currentAccount.did as DidString, password, token, }) @@ -142,8 +149,9 @@ function DeleteAccountDialogInner({ } }, [ _, - agent, + chatClient, cleanError, + client, confirmCode, control, currentAccount, diff --git a/src/state/queries/app-passwords.ts b/src/state/queries/app-passwords.ts index 12d66dd2cd..4403b1fbd4 100644 --- a/src/state/queries/app-passwords.ts +++ b/src/state/queries/app-passwords.ts @@ -1,39 +1,37 @@ -import {type ComAtprotoServerCreateAppPassword} from '@atproto/api' import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' import {STALE} from '#/state/queries' -import {useAgent} from '../session' +import {com} from '#/lexicons' +import {usePdsClient} from '../session' const RQKEY_ROOT = 'app-passwords' export const RQKEY = () => [RQKEY_ROOT] export function useAppPasswordsQuery() { - const agent = useAgent() + const client = usePdsClient() return useQuery({ staleTime: STALE.MINUTES.FIVE, queryKey: RQKEY(), queryFn: async () => { - const res = await agent.com.atproto.server.listAppPasswords({}) - return res.data.passwords + const data = await client.call(com.atproto.server.listAppPasswords) + return data.passwords }, }) } export function useAppPasswordCreateMutation() { const queryClient = useQueryClient() - const agent = useAgent() + const client = usePdsClient() return useMutation< - ComAtprotoServerCreateAppPassword.OutputSchema, + com.atproto.server.createAppPassword.$OutputBody, Error, {name: string; privileged: boolean} >({ mutationFn: async ({name, privileged}) => { - return ( - await agent.com.atproto.server.createAppPassword({ - name, - privileged, - }) - ).data + return await client.call(com.atproto.server.createAppPassword, { + name, + privileged, + }) }, onSuccess() { queryClient.invalidateQueries({ @@ -45,10 +43,10 @@ export function useAppPasswordCreateMutation() { export function useAppPasswordDeleteMutation() { const queryClient = useQueryClient() - const agent = useAgent() + const client = usePdsClient() return useMutation({ mutationFn: async ({name}) => { - await agent.com.atproto.server.revokeAppPassword({ + await client.call(com.atproto.server.revokeAppPassword, { name, }) }, diff --git a/src/state/queries/handle-availability.ts b/src/state/queries/handle-availability.ts index fb25697c76..ab6a2e985b 100644 --- a/src/state/queries/handle-availability.ts +++ b/src/state/queries/handle-availability.ts @@ -1,4 +1,4 @@ -import {ComAtprotoTempCheckHandleAvailability} from '@atproto/api' +import {type DatetimeString, type HandleString} from '@atproto/syntax' import {useQuery} from '@tanstack/react-query' import { @@ -7,10 +7,11 @@ import { PUBLIC_BSKY_SERVICE, } from '#/lib/constants' import {useDebouncedValue} from '#/lib/hooks/useDebouncedValue' +import {createServiceClient} from '#/lib/lexClient' import {createFullHandle} from '#/lib/strings/handles' import {useAnalytics} from '#/analytics' +import {com} from '#/lexicons' import * as bsky from '#/types/bsky' -import {Agent} from '../session/agent' export const RQKEY_handleAvailability = ( handle: string, @@ -79,25 +80,33 @@ export async function checkHandleAvailability( }, ) { if (serviceDid === BSKY_SERVICE_DID) { - const agent = new Agent(null, {service: BSKY_SERVICE}) + /* + * Runs pre-auth during signup, so it goes through a one-off service client + * rather than a session-scoped one. The target is the fixed entryway rather + * than a user-supplied host, but there is still no session to hang a client + * off. + */ + const client = createServiceClient(BSKY_SERVICE) // entryway has a special API for handle availability - const {data} = await agent.com.atproto.temp.checkHandleAvailability({ - handle, - birthDate, + const data = await client.call(com.atproto.temp.checkHandleAvailability, { + // the caller assembles this from a validated username and domain + handle: handle as HandleString, + // callers pass an ISO date string built from the birth-date picker + birthDate: birthDate as DatetimeString | undefined, email, }) if ( - bsky.dangerousIsType( + bsky.isType( + com.atproto.temp.checkHandleAvailability.resultAvailable, data.result, - ComAtprotoTempCheckHandleAvailability.isResultAvailable, ) ) { return {available: true} as const } else if ( - bsky.dangerousIsType( + bsky.isType( + com.atproto.temp.checkHandleAvailability.resultUnavailable, data.result, - ComAtprotoTempCheckHandleAvailability.isResultUnavailable, ) ) { return { @@ -110,14 +119,18 @@ export async function checkHandleAvailability( ) } } else { - // 3rd party PDSes won't have this API so just try and resolve the handle - const agent = new Agent(null, {service: PUBLIC_BSKY_SERVICE}) + /* + * 3rd party PDSes won't have this API so just try and resolve the handle. + * This is an unauthenticated public-appview read, not a call against the + * user's chosen host. + */ + const client = createServiceClient(PUBLIC_BSKY_SERVICE) try { - const res = await agent.resolveHandle({ - handle, + const data = await client.call(com.atproto.identity.resolveHandle, { + handle: handle as HandleString, }) - if (res.data.did) { + if (data.did) { return {available: false} as const } } catch {} diff --git a/src/state/queries/handle.ts b/src/state/queries/handle.ts index c53c878d5f..c400e8bd85 100644 --- a/src/state/queries/handle.ts +++ b/src/state/queries/handle.ts @@ -1,8 +1,10 @@ import {useCallback} from 'react' +import {type HandleString} from '@atproto/syntax' import {useMutation, useQueryClient} from '@tanstack/react-query' import {STALE} from '#/state/queries' -import {useAgent} from '#/state/session' +import {useAgent, usePdsClient} from '#/state/session' +import {com} from '#/lexicons' const handleQueryKeyRoot = 'handle' const fetchHandleQueryKey = (handleOrDid: string) => [ @@ -36,11 +38,15 @@ export function useUpdateHandleMutation(opts?: { onSuccess?: (handle: string) => void }) { const queryClient = useQueryClient() - const agent = useAgent() + const client = usePdsClient() return useMutation({ mutationFn: async ({handle}: {handle: string}) => { - await agent.updateHandle({handle}) + // `agent.updateHandle` was a pure alias for this method + await client.call(com.atproto.identity.updateHandle, { + // callers validate the handle before submitting + handle: handle as HandleString, + }) }, onSuccess(_data, variables) { opts?.onSuccess?.(variables.handle) diff --git a/src/state/queries/service.ts b/src/state/queries/service.ts index e9661db9e3..754ba81652 100644 --- a/src/state/queries/service.ts +++ b/src/state/queries/service.ts @@ -1,6 +1,7 @@ import {useQuery} from '@tanstack/react-query' -import {Agent} from '../session/agent' +import {createServiceClient} from '#/lib/lexClient' +import {com} from '#/lexicons' const RQKEY_ROOT = 'service' export const RQKEY = (serviceUrl: string) => [RQKEY_ROOT, serviceUrl] @@ -9,9 +10,12 @@ export function useServiceQuery(serviceUrl: string) { return useQuery({ queryKey: RQKEY(serviceUrl), queryFn: async () => { - const agent = new Agent(null, {service: serviceUrl}) - const res = await agent.com.atproto.server.describeServer() - return res.data + /* + * The host is whatever the user typed or picked, so this describes it + * through a one-off service client rather than a session-scoped one. + */ + const client = createServiceClient(serviceUrl) + return await client.call(com.atproto.server.describeServer) }, enabled: isValidUrl(serviceUrl), }) From d991118215b07d47b84a07705abc0770bfcf3abb Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 13 Aug 2026 22:26:18 +0300 Subject: [PATCH 16/32] [SDK] Route moderation reports through the appview client (#11368) Co-authored-by: Claude Fable 5 --- .../ageAssurance/AgeAssuranceAppealDialog.tsx | 23 ++++--- src/components/moderation/AppealForm.tsx | 48 ++++++++------ .../moderation/ReportDialog/action.ts | 64 +++++++++++-------- .../components/GoLiveDisabledDialog.tsx | 21 +++--- src/lib/constants.ts | 16 +++-- .../Messages/components/ChatDisabled.tsx | 19 +++--- src/screens/Takendown.tsx | 23 +++---- 7 files changed, 127 insertions(+), 87 deletions(-) diff --git a/src/components/ageAssurance/AgeAssuranceAppealDialog.tsx b/src/components/ageAssurance/AgeAssuranceAppealDialog.tsx index 1ee126eb28..59df413324 100644 --- a/src/components/ageAssurance/AgeAssuranceAppealDialog.tsx +++ b/src/components/ageAssurance/AgeAssuranceAppealDialog.tsx @@ -1,13 +1,14 @@ import {useState} from 'react' import {View} from 'react-native' import {ToolsOzoneReportDefs} from '@atproto/api' +import {type DidString} from '@atproto/syntax' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' import {useMutation} from '@tanstack/react-query' -import {BLUESKY_MOD_SERVICE_HEADERS} from '#/lib/constants' -import {useAgent, useSession} from '#/state/session' +import {MOD_PROXY_SERVICE} from '#/lib/constants' +import {useAppviewClient, useSession} from '#/state/session' import {atoms as a, useBreakpoints, web} from '#/alf' import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge' import {Button, ButtonIcon, ButtonText} from '#/components/Button' @@ -17,6 +18,7 @@ import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import {logger} from '#/ageAssurance' import {useAnalytics} from '#/analytics' +import {com} from '#/lexicons' export function AgeAssuranceAppealDialog({ control, @@ -42,7 +44,7 @@ function Inner({control}: {control: Dialog.DialogControlProps}) { const ax = useAnalytics() const {currentAccount} = useSession() const {gtPhone} = useBreakpoints() - const agent = useAgent() + const client = useAppviewClient() const [details, setDetails] = useState('') const isInvalid = details.length > 1000 @@ -51,19 +53,22 @@ function Inner({control}: {control: Dialog.DialogControlProps}) { mutationFn: async () => { ax.metric('ageAssurance:appealDialogSubmit', {}) - await agent.createModerationReport( + if (!currentAccount) { + throw new Error('No current account, should be unreachable') + } + + await client.call( + com.atproto.moderation.createReport, { reasonType: ToolsOzoneReportDefs.REASONAPPEAL, subject: { $type: 'com.atproto.admin.defs#repoRef', - did: currentAccount?.did, + // the persisted account did is already resolved + did: currentAccount.did as DidString, }, reason: `AGE_ASSURANCE_INQUIRY: ` + details, }, - { - encoding: 'application/json', - headers: BLUESKY_MOD_SERVICE_HEADERS, - }, + {service: MOD_PROXY_SERVICE}, ) }, onError: err => { diff --git a/src/components/moderation/AppealForm.tsx b/src/components/moderation/AppealForm.tsx index 5432672d1e..606e8162ec 100644 --- a/src/components/moderation/AppealForm.tsx +++ b/src/components/moderation/AppealForm.tsx @@ -1,7 +1,8 @@ import {useState} from 'react' import {View} from 'react-native' import {type ComAtprotoLabelDefs, ToolsOzoneReportDefs} from '@atproto/api' -import {XRPCError} from '@atproto/api' +import {XrpcResponseError} from '@atproto/lex' +import {type AtUriString, type DidString} from '@atproto/syntax' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' @@ -12,7 +13,7 @@ import {useLabelInfo} from '#/lib/moderation/useLabelInfo' import {makeProfileLink} from '#/lib/routes/links' import {sanitizeHandle} from '#/lib/strings/handles' import {logger} from '#/logger' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' import {atoms as a, useBreakpoints} from '#/alf' import {Admonition} from '#/components/Admonition' import {Button, ButtonIcon, ButtonText} from '#/components/Button' @@ -22,6 +23,7 @@ import {Loader} from '#/components/Loader' import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import {IS_ANDROID} from '#/env' +import {com} from '#/lexicons' export function AppealForm({ label, @@ -38,7 +40,7 @@ export function AppealForm({ const [details, setDetails] = useState('') const {subject} = useLabelSubject({label}) const isAccountReport = 'did' in subject - const agent = useAgent() + const client = useAppviewClient() const sourceName = labeler ? sanitizeHandle(labeler.creator.handle, '@') : label.src @@ -46,28 +48,38 @@ export function AppealForm({ const {mutate, isPending} = useMutation({ mutationFn: async () => { - const $type = !isAccountReport - ? 'com.atproto.repo.strongRef' - : 'com.atproto.admin.defs#repoRef' - await agent.createModerationReport( + await client.call( + com.atproto.moderation.createReport, { reasonType: ToolsOzoneReportDefs.REASONAPPEAL, - subject: { - $type, - ...subject, - }, + /* + * `useLabelSubject` derives one shape or the other from the label's + * `cid`: an at-uri plus cid for a record, or the label's `uri` reused + * as the account did. + */ + subject: isAccountReport + ? { + $type: 'com.atproto.admin.defs#repoRef', + did: subject.did as DidString, + } + : { + $type: 'com.atproto.repo.strongRef', + uri: subject.uri as AtUriString, + cid: subject.cid, + }, reason: details, }, - { - encoding: 'application/json', - headers: { - 'atproto-proxy': `${label.src}#atproto_labeler`, - }, - }, + // the appeal goes to the labeler that applied the label + {service: `${label.src as DidString}#atproto_labeler`}, ) }, onError: err => { - if (err instanceof XRPCError && err.error === 'AlreadyAppealed') { + /* + * `AlreadyAppealed` is real server behavior that createReport's lexicon + * does NOT declare, so `matchXrpcError` cannot see it and the raw error + * code is checked instead. Worth an upstream PR to declare it. + */ + if (err instanceof XrpcResponseError && err.error === 'AlreadyAppealed') { setError( _( msg`You've already appealed this label and it's being reviewed by our moderation team.`, diff --git a/src/components/moderation/ReportDialog/action.ts b/src/components/moderation/ReportDialog/action.ts index 20303ddef5..12b483fcab 100644 --- a/src/components/moderation/ReportDialog/action.ts +++ b/src/components/moderation/ReportDialog/action.ts @@ -1,22 +1,21 @@ -import { - type $Typed, - BSKY_LABELER_DID, - type ChatBskyConvoDefs, - type ComAtprotoModerationCreateReport, -} from '@atproto/api' +import {BSKY_LABELER_DID} from '@atproto/api' +import {type AtUriString, type DidString} from '@atproto/syntax' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {useMutation} from '@tanstack/react-query' import {logger} from '#/logger' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' +import {com} from '#/lexicons' import {NEW_TO_OLD_REASONS_MAP, REPORT_MOD_TOOL_NAME} from './const' import {type ReportState} from './state' import {type ParsedReportSubject} from './types' +type ReportInput = com.atproto.moderation.createReport.$InputBody + export function useSubmitReportMutation() { const {_} = useLingui() - const agent = useAgent() + const client = useAppviewClient() return useMutation({ async mutationFn({ @@ -59,13 +58,7 @@ export function useSubmitReportMutation() { reasonType = backwardsCompatibleReasonType } - let report: - | ComAtprotoModerationCreateReport.InputSchema - | (Omit & { - subject: - | $Typed - | $Typed - }) + let report: ReportInput switch (subject.type) { case 'account': { @@ -74,7 +67,8 @@ export function useSubmitReportMutation() { reason: state.details, subject: { $type: 'com.atproto.admin.defs#repoRef', - did: subject.did, + // the parsed subject carries an already-resolved did + did: subject.did as DidString, }, } break @@ -89,7 +83,8 @@ export function useSubmitReportMutation() { reason: state.details, subject: { $type: 'com.atproto.repo.strongRef', - uri: subject.uri, + // the parsed subject carries an at-uri read off a view + uri: subject.uri as AtUriString, cid: subject.cid, }, } @@ -99,12 +94,12 @@ export function useSubmitReportMutation() { report = { reasonType, reason: state.details, - subject: { + subject: toOpenSubject({ $type: 'chat.bsky.convo.defs#messageRef', messageId: subject.message.id, convoId: subject.convoId, did: subject.message.sender.did, - }, + }), } break } @@ -112,11 +107,11 @@ export function useSubmitReportMutation() { report = { reasonType, reason: state.details, - subject: { + subject: toOpenSubject({ $type: 'chat.bsky.convo.defs#convoRef', convoId: subject.convoId, did: subject.did, - }, + }), } break } @@ -145,13 +140,30 @@ export function useSubmitReportMutation() { report, }) } else { - await agent.createModerationReport(report, { - encoding: 'application/json', - headers: { - 'atproto-proxy': `${labeler.creator.did}#atproto_labeler`, - }, + /* + * Reports go to the labeler the user selected rather than Bluesky's, so + * the proxy target is built per call from that labeler's creator did. + */ + await client.call(com.atproto.moderation.createReport, report, { + service: `${labeler.creator.did as DidString}#atproto_labeler`, }) } }, }) } + +/** + * Widen a chat convo ref into `createReport`'s subject union. + * + * The lexicon declares only `com.atproto.admin.defs#repoRef` and + * `com.atproto.repo.strongRef`, but leaves the union OPEN, and the chat service + * accepts its own refs there. An open union types its unknown arm as + * `{$type: Unknown$Type}`, which a concrete chat ref does not structurally + * satisfy, so the widening is asserted here once rather than at each call site. + */ +function toOpenSubject(ref: { + $type: string + [key: string]: unknown +}): ReportInput['subject'] { + return ref as unknown as ReportInput['subject'] +} diff --git a/src/features/liveNow/components/GoLiveDisabledDialog.tsx b/src/features/liveNow/components/GoLiveDisabledDialog.tsx index 7eeccdc22b..fda33a9067 100644 --- a/src/features/liveNow/components/GoLiveDisabledDialog.tsx +++ b/src/features/liveNow/components/GoLiveDisabledDialog.tsx @@ -1,20 +1,22 @@ import {useCallback, useState} from 'react' import {View} from 'react-native' import {type AppBskyActorDefs, ToolsOzoneReportDefs} from '@atproto/api' +import {type AtUriString} from '@atproto/syntax' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' import {useMutation} from '@tanstack/react-query' -import {BLUESKY_MOD_SERVICE_HEADERS} from '#/lib/constants' +import {MOD_PROXY_SERVICE} from '#/lib/constants' import {logger} from '#/logger' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' import {atoms as a, web} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' import {Loader} from '#/components/Loader' import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' +import {com} from '#/lexicons' export function GoLiveDisabledDialog({ control, @@ -39,12 +41,12 @@ export function DialogInner({ status: AppBskyActorDefs.StatusView }) { const {_} = useLingui() - const agent = useAgent() + const client = useAppviewClient() const [details, setDetails] = useState('') const {mutate, isPending} = useMutation({ mutationFn: async () => { - if (!agent.session?.did) { + if (!client.did) { throw new Error('Not logged in') } if (!status.uri || !status.cid) { @@ -56,20 +58,19 @@ export function DialogInner({ details, }) } else { - await agent.createModerationReport( + await client.call( + com.atproto.moderation.createReport, { reasonType: ToolsOzoneReportDefs.REASONAPPEAL, subject: { $type: 'com.atproto.repo.strongRef', - uri: status.uri, + // a status view's uri is an at-uri produced by the appview + uri: status.uri as AtUriString, cid: status.cid, }, reason: details, }, - { - encoding: 'application/json', - headers: BLUESKY_MOD_SERVICE_HEADERS, - }, + {service: MOD_PROXY_SERVICE}, ) } }, diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 1a89fa5b5e..156712c809 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -263,14 +263,22 @@ export const BLUESKY_PROXY_HEADER = { */ export const CHAT_PROXY_SERVICE: Service = `${CHAT_PROXY_DID}#bsky_chat` +/** + * Bluesky's own moderation service, in the `did#service_id` form a lex client's + * per-call `service` option takes. Passing it emits `atproto-proxy: ` on that one request, routing a `com.atproto.moderation.*` call to + * Bluesky's labeler. + * + * Reports and appeals aimed at a DIFFERENT labeler build their own value from + * that labeler's creator did instead, so this is a per-call option rather than a + * client-level one like {@link CHAT_PROXY_SERVICE}. + */ +export const MOD_PROXY_SERVICE: Service = `${BSKY_LABELER_DID}#atproto_labeler` + export const DM_SERVICE_HEADERS = { 'atproto-proxy': `${CHAT_PROXY_DID}#bsky_chat`, } -export const BLUESKY_MOD_SERVICE_HEADERS = { - 'atproto-proxy': `${BSKY_LABELER_DID}#atproto_labeler`, -} - export const BLUESKY_NOTIF_SERVICE_HEADERS = { 'atproto-proxy': `${BLUESKY_PROXY_DID}#bsky_notif`, } diff --git a/src/screens/Messages/components/ChatDisabled.tsx b/src/screens/Messages/components/ChatDisabled.tsx index 8095c1e0b9..62f353d1a8 100644 --- a/src/screens/Messages/components/ChatDisabled.tsx +++ b/src/screens/Messages/components/ChatDisabled.tsx @@ -1,12 +1,13 @@ import {useCallback, useState} from 'react' import {type StyleProp, View, type ViewStyle} from 'react-native' import {ToolsOzoneReportDefs} from '@atproto/api' +import {type DidString} from '@atproto/syntax' import {Trans, useLingui} from '@lingui/react/macro' import {useMutation} from '@tanstack/react-query' -import {BLUESKY_MOD_SERVICE_HEADERS} from '#/lib/constants' +import {MOD_PROXY_SERVICE} from '#/lib/constants' import {logger} from '#/logger' -import {useAgent, useSession} from '#/state/session' +import {useAppviewClient, useSession} from '#/state/session' import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' @@ -14,6 +15,7 @@ import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons import {Loader} from '#/components/Loader' import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' +import {com} from '#/lexicons' export function ChatDisabled({ shape = 'pill', @@ -92,26 +94,25 @@ function DialogInner() { const control = Dialog.useDialogContext() const [details, setDetails] = useState('') const {gtMobile} = useBreakpoints() - const agent = useAgent() + const client = useAppviewClient() const {currentAccount} = useSession() const {mutate, isPending} = useMutation({ mutationFn: async () => { if (!currentAccount) throw new Error('No current account, should be unreachable') - await agent.createModerationReport( + await client.call( + com.atproto.moderation.createReport, { reasonType: ToolsOzoneReportDefs.REASONAPPEAL, subject: { $type: 'com.atproto.admin.defs#repoRef', - did: currentAccount.did, + // the persisted account did is already resolved + did: currentAccount.did as DidString, }, reason: details, }, - { - encoding: 'application/json', - headers: BLUESKY_MOD_SERVICE_HEADERS, - }, + {service: MOD_PROXY_SERVICE}, ) }, onError: err => { diff --git a/src/screens/Takendown.tsx b/src/screens/Takendown.tsx index 5ad254fd1c..5873b00473 100644 --- a/src/screens/Takendown.tsx +++ b/src/screens/Takendown.tsx @@ -2,7 +2,8 @@ import {useState} from 'react' import {View} from 'react-native' import {KeyboardAwareScrollView} from 'react-native-keyboard-controller' import {useSafeAreaInsets} from 'react-native-safe-area-context' -import {type ComAtprotoAdminDefs, ToolsOzoneReportDefs} from '@atproto/api' +import {ToolsOzoneReportDefs} from '@atproto/api' +import {type DidString} from '@atproto/syntax' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' @@ -10,11 +11,11 @@ import {useMutation} from '@tanstack/react-query' import {countGraphemes} from 'unicode-segmenter/grapheme' import { - BLUESKY_MOD_SERVICE_HEADERS, MAX_REPORT_REASON_GRAPHEME_LENGTH, + MOD_PROXY_SERVICE, } from '#/lib/constants' import {cleanError} from '#/lib/strings/errors' -import {useAgent, useSession, useSessionApi} from '#/state/session' +import {useAppviewClient, useSession, useSessionApi} from '#/state/session' import {CharProgress} from '#/view/com/composer/char-progress/CharProgress' import {Logo} from '#/view/icons/Logo' import {atoms as a, useBreakpoints, useTheme} from '#/alf' @@ -24,6 +25,7 @@ import {SimpleInlineLinkText} from '#/components/Link' import {Loader} from '#/components/Loader' import {P, Text} from '#/components/Typography' import {IS_WEB} from '#/env' +import {com} from '#/lexicons' const COL_WIDTH = 400 @@ -34,7 +36,7 @@ export function Takendown() { const {gtMobile} = useBreakpoints() const {currentAccount} = useSession() const {logoutCurrentAccount} = useSessionApi() - const agent = useAgent() + const client = useAppviewClient() const [isAppealling, setIsAppealling] = useState(false) const [reason, setReason] = useState('') @@ -50,19 +52,18 @@ export function Takendown() { } = useMutation({ mutationFn: async (appealText: string) => { if (!currentAccount) throw new Error('No session') - await agent.com.atproto.moderation.createReport( + await client.call( + com.atproto.moderation.createReport, { reasonType: ToolsOzoneReportDefs.REASONAPPEAL, subject: { $type: 'com.atproto.admin.defs#repoRef', - did: currentAccount.did, - } satisfies ComAtprotoAdminDefs.RepoRef, + // the persisted account did is already resolved + did: currentAccount.did as DidString, + }, reason: appealText, }, - { - encoding: 'application/json', - headers: BLUESKY_MOD_SERVICE_HEADERS, - }, + {service: MOD_PROXY_SERVICE}, ) }, onSuccess: () => setReason(''), From f6a22c4f05a01c76e0b97f8118ae4ad55fac131f Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 13 Aug 2026 22:26:18 +0300 Subject: [PATCH 17/32] [SDK] Adopt @bsky.app/sdk and migrate RichText (#11369) --- package.json | 3 +- pnpm-lock.yaml | 27 +++++++++++++++++ pnpm-workspace.yaml | 2 ++ src/components/FeedCard.tsx | 8 ++--- src/components/Post/Embed/index.tsx | 5 ++-- .../PostControls/PostMenu/PostMenuItems.tsx | 2 +- .../PostControls/PostMenu/index.tsx | 2 +- .../ShareMenu/ShareMenuItems.types.tsx | 2 +- .../PostControls/ShareMenu/index.tsx | 2 +- src/components/PostControls/index.tsx | 2 +- src/components/ProfileCard.tsx | 7 ++--- src/components/RichText.tsx | 10 ++++--- .../dialogs/lists/CreateOrEditListDialog.tsx | 22 +++++++++----- src/components/dms/MessageContextMenu.tsx | 11 +++---- src/components/dms/MessageItem.tsx | 8 +++-- src/components/hooks/useRichText.ts | 15 ++++++---- src/lib/api/index.ts | 15 +++++++--- src/lib/strings/helpers.ts | 2 +- src/lib/strings/rich-text-helpers.ts | 29 +++++++++++++++++-- src/lib/strings/rich-text-manip.ts | 12 ++++++-- .../Messages/components/MessageInputEmbed.tsx | 15 ++++------ .../Messages/components/MessagesList.tsx | 20 ++++++++----- .../components/ThreadItemAnchor.tsx | 5 ++-- .../PostThread/components/ThreadItemPost.tsx | 5 ++-- .../components/ThreadItemTreePost.tsx | 5 ++-- .../Profile/Header/ProfileHeaderLabeler.tsx | 2 +- .../Profile/Header/ProfileHeaderStandard.tsx | 2 +- src/screens/Profile/Header/index.tsx | 2 +- src/screens/ProfileList/components/Header.tsx | 6 ++-- .../Search/modules/ExploreTrendingTopics.tsx | 7 ++--- src/screens/StarterPack/StarterPackScreen.tsx | 5 ++-- src/screens/VideoFeed/index.tsx | 5 ++-- src/state/queries/feed.ts | 7 +++-- src/state/queries/starter-packs.ts | 20 ++++++++----- src/view/com/composer/Composer.tsx | 3 +- src/view/com/composer/drafts/state/api.ts | 3 +- .../select-language/SuggestedLanguage.tsx | 2 +- src/view/com/composer/state/composer.ts | 7 +++-- .../com/composer/text-input/TextInput.tsx | 6 ++-- .../composer/text-input/TextInput.types.ts | 2 +- .../com/composer/text-input/TextInput.web.tsx | 6 ++-- .../composer/text-input/text-input-util.ts | 6 ++-- src/view/com/post/Post.tsx | 5 ++-- src/view/com/posts/PostFeed.tsx | 2 +- src/view/com/posts/PostFeedItem.tsx | 5 ++-- src/view/screens/DebugMod.tsx | 2 +- src/view/screens/Profile.tsx | 14 +++++---- 47 files changed, 231 insertions(+), 124 deletions(-) diff --git a/package.json b/package.json index e084d19641..510955abbf 100644 --- a/package.json +++ b/package.json @@ -115,6 +115,7 @@ "@bsky.app/expo-translate-text": "^0.2.9", "@bsky.app/peek-menu": "^0.3.2", "@bsky.app/react-native-uitextview": "^2.7.0", + "@bsky.app/sdk": "npm:bsky-sdk-preview@0.0.1-preview.2", "@bsky.app/sift": "^0.3.9", "@bsky.app/tapper": "^0.6.1", "@bsky.app/video": "0.3.6", @@ -339,7 +340,7 @@ "^await-lock$": "/node_modules/await-lock/build/AwaitLock.js" }, "transformIgnorePatterns": [ - "node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|nanoid|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|normalize-url|react-native-svg|@sentry/.*|sentry-expo|bcp-47-match|@atproto/.*|multiformats|uint8arrays|@ipld/.*|cborg|await-lock)" + "node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|nanoid|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|normalize-url|react-native-svg|@sentry/.*|sentry-expo|bcp-47-match|@atproto/.*|@bsky.app/sdk|tlds|multiformats|uint8arrays|@ipld/.*|cborg|await-lock)" ], "modulePathIgnorePatterns": [ "__tests__/.*/__mocks__", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fa725d0bff..3b8bcb5f36 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -287,6 +287,9 @@ importers: '@bsky.app/react-native-uitextview': specifier: ^2.7.0 version: 2.7.0(react-native@0.81.5(patch_hash=91fd85363059530dea649a5ca6012b933f79c8491737fbfdc685abc727e48403)(@babel/core@7.29.0(supports-color@8.1.1))(@react-native/metro-config@0.86.0(@babel/core@7.29.0(supports-color@8.1.1))(supports-color@8.1.1))(@types/react@19.1.17)(react@19.1.0)(supports-color@8.1.1))(react@19.1.0) + '@bsky.app/sdk': + specifier: npm:bsky-sdk-preview@0.0.1-preview.2 + version: bsky-sdk-preview@0.0.1-preview.2(@atproto/lex@0.3.6) '@bsky.app/sift': specifier: ^0.3.9 version: 0.3.9(2d5c14016450c7e55c8cd0dffcca6f69) @@ -889,6 +892,10 @@ packages: resolution: {integrity: sha512-iDFZEoNqfL17EVKE+uNzdUD7YF8LWhEhxeBtP6x+PNuAxoXv3ip9vLmB8nNzNxFwVn++FipfDtSiPqmziv1bdA==} engines: {node: '>=22'} + '@atproto-labs/handle-resolver@0.4.8': + resolution: {integrity: sha512-38I+j8Efs+cCgW/NX9RFKKu7qv/AF+FqxlKS8sWjXlNZSHQZqPj5cnKVQhSsJCs4ypPB84iKch8w4/STd33bLg==} + engines: {node: '>=22'} + '@atproto-labs/pipe@0.2.4': resolution: {integrity: sha512-n67jCcrC+ouAeO10cWkpPzzLMlDi/lDCU30Us+LGqhOPhT6c4t5ASdBLQi9W3jUQtRzQBt3G9zipF+xKWNvVbw==} engines: {node: '>=22'} @@ -4505,6 +4512,12 @@ packages: bser@2.1.1: resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} + bsky-sdk-preview@0.0.1-preview.2: + resolution: {integrity: sha512-LOcuvzc6UASiI5os1BUIo9dxAeVPIFiplcVSYbaDCgpH+tkpv2mqSqqsCRTP0g2CaVCwozqaTVy+jnsTO6/9BQ==} + engines: {node: '>=22.12.0'} + peerDependencies: + '@atproto/lex': ^0.3.0 + buffer-crc32@0.2.13: resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} @@ -9725,6 +9738,13 @@ snapshots: dependencies: '@atproto-labs/pipe': 0.2.4 + '@atproto-labs/handle-resolver@0.4.8': + dependencies: + '@atproto-labs/simple-store': 0.5.1 + '@atproto-labs/simple-store-memory': 0.2.6 + '@atproto/did': 0.5.4 + zod: 3.25.76 + '@atproto-labs/pipe@0.2.4': {} '@atproto-labs/simple-store-memory@0.2.6': @@ -14063,6 +14083,13 @@ snapshots: dependencies: node-int64: 0.4.0 + bsky-sdk-preview@0.0.1-preview.2(@atproto/lex@0.3.6): + dependencies: + '@atproto-labs/handle-resolver': 0.4.8 + '@atproto/lex': 0.3.6 + '@atproto/syntax': 0.7.4 + tlds: 1.261.0 + buffer-crc32@0.2.13: {} buffer-from@1.1.2: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 372e071c73..cbb815b851 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -44,6 +44,7 @@ patchedDependencies: 'react-native@0.81.5': patches/react-native@0.81.5.patch minimumReleaseAgeExclude: - '@atproto/*' + - '@atproto-labs/*' - '@bsky.app/*' # todo: remove when old enough - '@oxlint-tsgolint/darwin-arm64@7.0.2001' @@ -53,3 +54,4 @@ minimumReleaseAgeExclude: - '@oxlint-tsgolint/win32-arm64@7.0.2001' - '@oxlint-tsgolint/win32-x64@7.0.2001' - oxlint-tsgolint@7.0.2001 + - bsky-sdk-preview@0.0.1-preview.2 diff --git a/src/components/FeedCard.tsx b/src/components/FeedCard.tsx index efd14be4ba..2e7b070fb1 100644 --- a/src/components/FeedCard.tsx +++ b/src/components/FeedCard.tsx @@ -1,11 +1,7 @@ import {useCallback, useEffect, useMemo} from 'react' import {type GestureResponderEvent, View} from 'react-native' -import { - type AppBskyFeedDefs, - type AppBskyGraphDefs, - AtUri, - RichText as RichTextApi, -} from '@atproto/api' +import {type AppBskyFeedDefs, type AppBskyGraphDefs, AtUri} from '@atproto/api' +import {RichText as RichTextApi} from '@bsky.app/sdk/richtext' import {Plural, Trans, useLingui} from '@lingui/react/macro' import {useQueryClient} from '@tanstack/react-query' diff --git a/src/components/Post/Embed/index.tsx b/src/components/Post/Embed/index.tsx index da930dc6fb..0b143a0df3 100644 --- a/src/components/Post/Embed/index.tsx +++ b/src/components/Post/Embed/index.tsx @@ -6,12 +6,13 @@ import { AppBskyFeedPost, AtUri, moderatePost, - RichText as RichTextAPI, } from '@atproto/api' +import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {Trans} from '@lingui/react/macro' import {useQueryClient} from '@tanstack/react-query' import {makeProfileLink} from '#/lib/routes/links' +import {asSdkFacets} from '#/lib/strings/rich-text-helpers' import {getChatInviteCodeFromUrl} from '#/lib/strings/url-helpers' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {unstableCacheProfileView} from '#/state/queries/profile' @@ -295,7 +296,7 @@ export function QuoteEmbed({ return undefined const {text, facets} = quote.record return text.trim() - ? new RichTextAPI({text: text, facets: facets}) + ? new RichTextAPI({text: text, facets: asSdkFacets(facets)}) : undefined }, [quote.record]) diff --git a/src/components/PostControls/PostMenu/PostMenuItems.tsx b/src/components/PostControls/PostMenu/PostMenuItems.tsx index b51f6cb7cb..43fdaaf1cd 100644 --- a/src/components/PostControls/PostMenu/PostMenuItems.tsx +++ b/src/components/PostControls/PostMenu/PostMenuItems.tsx @@ -11,8 +11,8 @@ import { type AppBskyFeedPost, type AppBskyFeedThreadgate, AtUri, - type RichText as RichTextAPI, } from '@atproto/api' +import {type RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {plural} from '@lingui/core/macro' import {useLingui} from '@lingui/react/macro' import {useNavigation} from '@react-navigation/native' diff --git a/src/components/PostControls/PostMenu/index.tsx b/src/components/PostControls/PostMenu/index.tsx index a744efbaa8..c0826683b6 100644 --- a/src/components/PostControls/PostMenu/index.tsx +++ b/src/components/PostControls/PostMenu/index.tsx @@ -4,8 +4,8 @@ import { type AppBskyFeedDefs, type AppBskyFeedPost, type AppBskyFeedThreadgate, - type RichText as RichTextAPI, } from '@atproto/api' +import {type RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {useLingui} from '@lingui/react/macro' import {type Shadow} from '#/state/cache/post-shadow' diff --git a/src/components/PostControls/ShareMenu/ShareMenuItems.types.tsx b/src/components/PostControls/ShareMenu/ShareMenuItems.types.tsx index 5bc2a8fb6e..ef93865513 100644 --- a/src/components/PostControls/ShareMenu/ShareMenuItems.types.tsx +++ b/src/components/PostControls/ShareMenu/ShareMenuItems.types.tsx @@ -3,8 +3,8 @@ import { type AppBskyFeedDefs, type AppBskyFeedPost, type AppBskyFeedThreadgate, - type RichText as RichTextAPI, } from '@atproto/api' +import {type RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {type Shadow} from '#/state/cache/post-shadow' diff --git a/src/components/PostControls/ShareMenu/index.tsx b/src/components/PostControls/ShareMenu/index.tsx index efac6a3f01..121cd2a779 100644 --- a/src/components/PostControls/ShareMenu/index.tsx +++ b/src/components/PostControls/ShareMenu/index.tsx @@ -5,8 +5,8 @@ import { type AppBskyFeedPost, type AppBskyFeedThreadgate, AtUri, - type RichText as RichTextAPI, } from '@atproto/api' +import {type RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' diff --git a/src/components/PostControls/index.tsx b/src/components/PostControls/index.tsx index 1f6be5ab6c..88f30b0561 100644 --- a/src/components/PostControls/index.tsx +++ b/src/components/PostControls/index.tsx @@ -4,8 +4,8 @@ import { type AppBskyFeedDefs, type AppBskyFeedPost, type AppBskyFeedThreadgate, - type RichText as RichTextAPI, } from '@atproto/api' +import {type RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {plural} from '@lingui/core/macro' import {useLingui} from '@lingui/react/macro' diff --git a/src/components/ProfileCard.tsx b/src/components/ProfileCard.tsx index c37f99c075..8508fecc71 100644 --- a/src/components/ProfileCard.tsx +++ b/src/components/ProfileCard.tsx @@ -6,11 +6,8 @@ import { View, type ViewStyle, } from 'react-native' -import { - moderateProfile, - type ModerationOpts, - RichText as RichTextApi, -} from '@atproto/api' +import {moderateProfile, type ModerationOpts} from '@atproto/api' +import {RichText as RichTextApi} from '@bsky.app/sdk/richtext' import {useLingui} from '@lingui/react/macro' import {getModerationCauseKey} from '#/lib/moderation' diff --git a/src/components/RichText.tsx b/src/components/RichText.tsx index 5501193faf..ce73990570 100644 --- a/src/components/RichText.tsx +++ b/src/components/RichText.tsx @@ -1,6 +1,6 @@ import {useMemo} from 'react' import {type StyleProp, type TextStyle} from 'react-native' -import {AppBskyRichtextFacet, RichText as RichTextAPI} from '@atproto/api' +import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {toShortUrl} from '#/lib/strings/url-helpers' import {android, atoms as a, flatten, type TextStyleProp} from '#/alf' @@ -9,6 +9,8 @@ import {InlineLinkText, type LinkProps} from '#/components/Link' import {ProfileHoverCard} from '#/components/ProfileHoverCard' import {RichTextTag} from '#/components/RichTextTag' import {Text, type TextProps} from '#/components/Typography' +import {app} from '#/lexicons' +import * as bsky from '#/types/bsky' const WORD_WRAP = {wordWrap: 1} // lifted from facet detection in `RichText` impl, _without_ `gm` flags @@ -139,7 +141,7 @@ export function RichText({ if ( mention && (disableMentionFacetValidation || - AppBskyRichtextFacet.validateMention(mention).success) && + bsky.matches(app.bsky.richtext.facet.mention, mention)) && !disableLinks ) { els.push( @@ -156,7 +158,7 @@ export function RichText({ , ) - } else if (link && AppBskyRichtextFacet.validateLink(link).success) { + } else if (link && bsky.matches(app.bsky.richtext.facet.link, link)) { const isValidLink = URL_REGEX.test(link.uri) if (!isValidLink || disableLinks) { els.push(toShortUrl(segment.text)) @@ -181,7 +183,7 @@ export function RichText({ !disableLinks && enableTags && tag && - AppBskyRichtextFacet.validateTag(tag).success + bsky.matches(app.bsky.richtext.facet.tag, tag) ) { els.push( new RichTextAPI({text})) const [resolvedRT, setResolvedRT] = useState(null) - const agent = useAgent() + /* + * Facet/mention resolution is an appview job - it resolves handles via + * `com.atproto.identity.resolveHandle` through the appview. The public + * fallback keeps mentions working on logged-out surfaces. + */ + const client = useAppviewClient() if (text !== prevText) { setPrevText(text) setRawRT(new RichTextAPI({text})) @@ -19,7 +24,7 @@ export function useRichText(text: string): [RichTextAPI, boolean] { async function resolveRTFacets() { // new each time const resolvedRT = new RichTextAPI({text}) - await resolvedRT.detectFacets(agent) + await resolvedRT.detectFacets(client) if (!ignore) { setResolvedRT(resolvedRT) } @@ -28,7 +33,7 @@ export function useRichText(text: string): [RichTextAPI, boolean] { return () => { ignore = true } - }, [text, agent]) + }, [text, client]) const isResolving = resolvedRT === null return [resolvedRT ?? rawRT, isResolving] } diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index 69cc2bf03e..9f81abb77a 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -13,9 +13,10 @@ import { type ComAtprotoLabelDefs, type ComAtprotoRepoApplyWrites, type ComAtprotoRepoStrongRef, - RichText, } from '@atproto/api' import {TID} from '@atproto/common-web' +import {type Client} from '@atproto/lex' +import {RichText} from '@bsky.app/sdk/richtext' import {t} from '@lingui/core/macro' import {type QueryClient} from '@tanstack/react-query' import {sha256} from 'js-sha256' @@ -51,6 +52,12 @@ interface PostOpts { replyTo?: string onStateChange?: (state: string) => void langs?: string[] + /* + * Facet/mention resolution is an appview job - it resolves handles through + * the appview, and the public fallback keeps it working when logged out. + * The rest of this pipeline still writes through the agent. + */ + appviewClient: Client } export async function post( @@ -87,7 +94,7 @@ export async function post( const draft = thread.posts[i] // Not awaited to avoid waterfalls. - const rtPromise = resolveRT(agent, draft.richtext) + const rtPromise = resolveRT(opts.appviewClient, draft.richtext) const embedPromise = resolveEmbed( agent, queryClient, @@ -196,14 +203,14 @@ export async function post( return {uris} } -async function resolveRT(agent: AtpAgent, richtext: RichText) { +async function resolveRT(appviewClient: Client, richtext: RichText) { const trimmedText = richtext.text // Trim leading whitespace-only lines (but don't break ASCII art). .replace(/^(\s*\n)+/, '') // Trim any trailing whitespace. .trimEnd() let rt = new RichText({text: trimmedText}, {cleanNewlines: true}) - await rt.detectFacets(agent) + await rt.detectFacets(appviewClient) rt = shortenLinks(rt) rt = stripInvalidMentions(rt) diff --git a/src/lib/strings/helpers.ts b/src/lib/strings/helpers.ts index 77ef18cb55..402807f50f 100644 --- a/src/lib/strings/helpers.ts +++ b/src/lib/strings/helpers.ts @@ -1,4 +1,4 @@ -import {type RichText} from '@atproto/api' +import {type RichText} from '@bsky.app/sdk/richtext' import {countGraphemes} from 'unicode-segmenter/grapheme' import {shortenLinks} from './rich-text-manip' diff --git a/src/lib/strings/rich-text-helpers.ts b/src/lib/strings/rich-text-helpers.ts index c2b2ceac52..a6341025ff 100644 --- a/src/lib/strings/rich-text-helpers.ts +++ b/src/lib/strings/rich-text-helpers.ts @@ -1,5 +1,7 @@ -import {AppBskyRichtextFacet, type RichText} from '@atproto/api' +import {type RichText} from '@bsky.app/sdk/richtext' +import {app} from '#/lexicons' +import * as bsky from '#/types/bsky' import {linkRequiresWarning} from './url-helpers' export function richTextToString(rt: RichText, loose: boolean): string { @@ -14,7 +16,7 @@ export function richTextToString(rt: RichText, loose: boolean): string { for (const segment of rt.segments()) { const link = segment.link - if (link && AppBskyRichtextFacet.validateLink(link).success) { + if (link && bsky.matches(app.bsky.richtext.facet.link, link)) { const href = link.uri const text = segment.text @@ -28,3 +30,26 @@ export function richTextToString(rt: RichText, loose: boolean): string { return result } + +/** + * Widens facets typed by the legacy `@atproto/api` codegen into the shape the + * SDK's `RichText` accepts. + * + * The two are the same lexicon and identical at runtime; they differ only in + * that the SDK brands `did`/`uri` as template literal types, which makes the + * legacy `string` versions unassignable. Call this where facets read off an + * `@atproto/api` view type are handed to `new RichText(...)`. + * + * Transitional: it goes away once the view types come from the SDK too. + */ +export function asSdkFacets( + facets: {index: {byteStart: number; byteEnd: number}; features: unknown[]}[], +): app.bsky.richtext.facet.Main[] +export function asSdkFacets( + facets: + | {index: {byteStart: number; byteEnd: number}; features: unknown[]}[] + | undefined, +): app.bsky.richtext.facet.Main[] | undefined +export function asSdkFacets(facets: unknown) { + return facets as app.bsky.richtext.facet.Main[] | undefined +} diff --git a/src/lib/strings/rich-text-manip.ts b/src/lib/strings/rich-text-manip.ts index 099fbffb0a..fa7a9b4a82 100644 --- a/src/lib/strings/rich-text-manip.ts +++ b/src/lib/strings/rich-text-manip.ts @@ -1,5 +1,7 @@ -import {AppBskyRichtextFacet, type RichText, UnicodeString} from '@atproto/api' +import {type RichText, UnicodeString} from '@bsky.app/sdk/richtext' +import {app} from '#/lexicons' +import * as bsky from '#/types/bsky' import {toShortUrl} from './url-helpers' export function shortenLinks(rt: RichText): RichText { @@ -10,7 +12,9 @@ export function shortenLinks(rt: RichText): RichText { // enumerate the link facets if (rt.facets) { for (const facet of rt.facets) { - const isLink = !!facet.features.find(AppBskyRichtextFacet.isLink) + const isLink = !!facet.features.find(f => + bsky.isType(app.bsky.richtext.facet.link, f), + ) if (!isLink) { continue } @@ -40,7 +44,9 @@ export function stripInvalidMentions(rt: RichText): RichText { rt = rt.clone() if (rt.facets) { rt.facets = rt.facets?.filter(facet => { - const mention = facet.features.find(AppBskyRichtextFacet.isMention) + const mention = facet.features.find(f => + bsky.isType(app.bsky.richtext.facet.mention, f), + ) if (mention && !mention.did) { return false } diff --git a/src/screens/Messages/components/MessageInputEmbed.tsx b/src/screens/Messages/components/MessageInputEmbed.tsx index 477939cd3e..9d1e4c32f6 100644 --- a/src/screens/Messages/components/MessageInputEmbed.tsx +++ b/src/screens/Messages/components/MessageInputEmbed.tsx @@ -1,12 +1,7 @@ import {useCallback, useEffect, useMemo, useState} from 'react' import {LayoutAnimation, View} from 'react-native' -import { - AppBskyFeedPost, - AppBskyRichtextFacet, - AtUri, - moderatePost, - RichText as RichTextAPI, -} from '@atproto/api' +import {AppBskyFeedPost, AtUri, moderatePost} from '@atproto/api' +import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {Trans, useLingui} from '@lingui/react/macro' import {type RouteProp, useNavigation, useRoute} from '@react-navigation/native' @@ -16,6 +11,7 @@ import { type CommonNavigatorParams, type NavigationProp, } from '#/lib/routes/types' +import {asSdkFacets} from '#/lib/strings/rich-text-helpers' import { convertBskyAppUrlIfNeeded, getChatInviteCodeFromUrl, @@ -36,6 +32,7 @@ import {ContentHider} from '#/components/moderation/ContentHider' import {PostAlerts} from '#/components/moderation/PostAlerts' import {RichText} from '#/components/RichText' import {Text} from '#/components/Typography' +import {app} from '#/lexicons' import * as bsky from '#/types/bsky' /** @@ -106,7 +103,7 @@ export function useExtractEmbedFromFacets( for (const facet of rt.facets ?? []) { for (const feature of facet.features) { if ( - AppBskyRichtextFacet.isLink(feature) && + bsky.isType(app.bsky.richtext.facet.link, feature) && (isBskyPostUrl(feature.uri) || isBskyChatInviteUrl(feature.uri)) ) { uriFromFacet = feature.uri @@ -176,7 +173,7 @@ function MessageInputPostEmbed({ return { rt: new RichTextAPI({ text: post.record.text, - facets: post.record.facets, + facets: asSdkFacets(post.record.facets), }), record: post.record, } diff --git a/src/screens/Messages/components/MessagesList.tsx b/src/screens/Messages/components/MessagesList.tsx index 312a2277ad..e9d336a67a 100644 --- a/src/screens/Messages/components/MessagesList.tsx +++ b/src/screens/Messages/components/MessagesList.tsx @@ -25,13 +25,12 @@ import {scheduleOnRN} from 'react-native-worklets' import { type $Typed, type AppBskyEmbedRecord, - AppBskyRichtextFacet, ChatBskyConvoDefs, type ChatBskyEmbedJoinLink, ChatBskyGroupDefs, - RichText, } from '@atproto/api' import {useScrollEdgeEffectRef} from '@bsky.app/expo-scroll-edge-effect' +import {RichText} from '@bsky.app/sdk/richtext' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' import {mergeRefs} from '#/lib/merge-refs' @@ -52,7 +51,7 @@ import {type ConvoState, ConvoStatus} from '#/state/messages/convo/types' import {useGetJoinLinkPreview} from '#/state/queries/join-links' import {useGetPost} from '#/state/queries/post' import {createEmbedViewRecordFromPost} from '#/state/queries/postgate/util' -import {useAgent, useSession} from '#/state/session' +import {useAppviewClient, useSession} from '#/state/session' import {List, type ListMethods} from '#/view/com/util/List' import {MessageComposer} from '#/screens/Messages/components/MessageComposer' import {MessageListError} from '#/screens/Messages/components/MessageListError' @@ -71,6 +70,8 @@ import {Loader} from '#/components/Loader' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' import {IS_ANDROID, IS_NATIVE, IS_WEB} from '#/env' +import {app} from '#/lexicons' +import * as bsky from '#/types/bsky' import {ChatStatusInfo} from './ChatStatusInfo' import {groupSystemMessages, type RenderItem} from './groupSystemMessages' import {InviteLinkDialogProvider} from './InviteLinkDialogProvider' @@ -144,7 +145,11 @@ export function MessagesList({ }) { const ax = useAnalytics() const convoState = useConvoActive() - const agent = useAgent() + /* + * Facet/mention resolution is an appview job - it resolves handles through + * the appview, and the public fallback keeps it working when logged out. + */ + const appviewClient = useAppviewClient() const {hasSession, currentAccount} = useSession() const getPost = useGetPost() const getJoinLinkPreview = useGetJoinLinkPreview() @@ -550,7 +555,8 @@ export function MessagesList({ const linkFacet = rt.facets?.find(facet => facet.features.find( feature => - AppBskyRichtextFacet.isLink(feature) && predicate(feature.uri), + bsky.isType(app.bsky.richtext.facet.link, feature) && + predicate(feature.uri), ), ) if (linkFacet) { @@ -617,7 +623,7 @@ export function MessagesList({ replyTo = {messageId: reply.id} } - await rt.detectFacets(agent) + await rt.detectFacets(appviewClient) rt = shortenLinks(rt) rt = stripInvalidMentions(rt) @@ -667,7 +673,7 @@ export function MessagesList({ } }, [ - agent, + appviewClient, convoState, getPost, getJoinLinkPreview, diff --git a/src/screens/PostThread/components/ThreadItemAnchor.tsx b/src/screens/PostThread/components/ThreadItemAnchor.tsx index c4d664238b..4a2f6513e5 100644 --- a/src/screens/PostThread/components/ThreadItemAnchor.tsx +++ b/src/screens/PostThread/components/ThreadItemAnchor.tsx @@ -5,8 +5,8 @@ import { AppBskyFeedPost, type AppBskyFeedThreadgate, AtUri, - RichText as RichTextAPI, } from '@atproto/api' +import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {Plural, Trans, useLingui} from '@lingui/react/macro' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' @@ -14,6 +14,7 @@ import {useOpenComposer} from '#/lib/hooks/useOpenComposer' import {makeProfileLink} from '#/lib/routes/links' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeHandle} from '#/lib/strings/handles' +import {asSdkFacets} from '#/lib/strings/rich-text-helpers' import {niceDate} from '#/lib/strings/time' import { POST_TOMBSTONE, @@ -199,7 +200,7 @@ const ThreadItemAnchorInner = memo(function ThreadItemAnchorInner({ () => new RichTextAPI({ text: record.text, - facets: record.facets, + facets: asSdkFacets(record.facets), }), [record], ) diff --git a/src/screens/PostThread/components/ThreadItemPost.tsx b/src/screens/PostThread/components/ThreadItemPost.tsx index c7f5b3b5f7..bd256e43b0 100644 --- a/src/screens/PostThread/components/ThreadItemPost.tsx +++ b/src/screens/PostThread/components/ThreadItemPost.tsx @@ -4,14 +4,15 @@ import { type AppBskyFeedDefs, type AppBskyFeedThreadgate, AtUri, - RichText as RichTextAPI, } from '@atproto/api' +import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {Trans} from '@lingui/react/macro' import {MAX_POST_LINES} from '#/lib/constants' import {useOpenComposer} from '#/lib/hooks/useOpenComposer' import {makeProfileLink} from '#/lib/routes/links' import {countLines} from '#/lib/strings/helpers' +import {asSdkFacets} from '#/lib/strings/rich-text-helpers' import { POST_TOMBSTONE, type Shadow, @@ -211,7 +212,7 @@ const ThreadItemPostInner = memo(function ThreadItemPostInner({ () => new RichTextAPI({ text: record.text, - facets: record.facets, + facets: asSdkFacets(record.facets), }), [record], ) diff --git a/src/screens/PostThread/components/ThreadItemTreePost.tsx b/src/screens/PostThread/components/ThreadItemTreePost.tsx index 1560550329..3f4ae03741 100644 --- a/src/screens/PostThread/components/ThreadItemTreePost.tsx +++ b/src/screens/PostThread/components/ThreadItemTreePost.tsx @@ -4,14 +4,15 @@ import { type AppBskyFeedDefs, type AppBskyFeedThreadgate, AtUri, - RichText as RichTextAPI, } from '@atproto/api' +import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {Trans} from '@lingui/react/macro' import {MAX_POST_LINES} from '#/lib/constants' import {useOpenComposer} from '#/lib/hooks/useOpenComposer' import {makeProfileLink} from '#/lib/routes/links' import {countLines} from '#/lib/strings/helpers' +import {asSdkFacets} from '#/lib/strings/rich-text-helpers' import { POST_TOMBSTONE, type Shadow, @@ -271,7 +272,7 @@ const ThreadItemTreePostInner = memo(function ThreadItemTreePostInner({ () => new RichTextAPI({ text: record.text, - facets: record.facets, + facets: asSdkFacets(record.facets), }), [record], ) diff --git a/src/screens/Profile/Header/ProfileHeaderLabeler.tsx b/src/screens/Profile/Header/ProfileHeaderLabeler.tsx index 96e136da5c..812e65b6e2 100644 --- a/src/screens/Profile/Header/ProfileHeaderLabeler.tsx +++ b/src/screens/Profile/Header/ProfileHeaderLabeler.tsx @@ -5,8 +5,8 @@ import { type AppBskyLabelerDefs, moderateProfile, type ModerationOpts, - type RichText as RichTextAPI, } from '@atproto/api' +import {type RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {msg, plural} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Plural, Trans} from '@lingui/react/macro' diff --git a/src/screens/Profile/Header/ProfileHeaderStandard.tsx b/src/screens/Profile/Header/ProfileHeaderStandard.tsx index da01ded772..3070997b98 100644 --- a/src/screens/Profile/Header/ProfileHeaderStandard.tsx +++ b/src/screens/Profile/Header/ProfileHeaderStandard.tsx @@ -5,8 +5,8 @@ import { moderateProfile, type ModerationDecision, type ModerationOpts, - type RichText as RichTextAPI, } from '@atproto/api' +import {type RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' diff --git a/src/screens/Profile/Header/index.tsx b/src/screens/Profile/Header/index.tsx index 0b7ab16ec4..dbd8bafcab 100644 --- a/src/screens/Profile/Header/index.tsx +++ b/src/screens/Profile/Header/index.tsx @@ -12,8 +12,8 @@ import { type AppBskyLabelerDefs, moderateProfile, type ModerationOpts, - type RichText as RichTextAPI, } from '@atproto/api' +import {type RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {useIsFocused} from '@react-navigation/native' import {sanitizeHandle} from '#/lib/strings/handles' diff --git a/src/screens/ProfileList/components/Header.tsx b/src/screens/ProfileList/components/Header.tsx index 9d6c4f3ff7..55f572c554 100644 --- a/src/screens/ProfileList/components/Header.tsx +++ b/src/screens/ProfileList/components/Header.tsx @@ -1,12 +1,14 @@ import {useMemo} from 'react' import {View} from 'react-native' -import {AppBskyGraphDefs, RichText as RichTextAPI} from '@atproto/api' +import {AppBskyGraphDefs} from '@atproto/api' +import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' import {useHaptics} from '#/lib/haptics' import {makeListLink} from '#/lib/routes/links' +import {asSdkFacets} from '#/lib/strings/rich-text-helpers' import {logger} from '#/logger' import {useListBlockMutation, useListMuteMutation} from '#/state/queries/list' import { @@ -128,7 +130,7 @@ export function Header({ list.description ? new RichTextAPI({ text: list.description, - facets: list.descriptionFacets, + facets: asSdkFacets(list.descriptionFacets), }) : undefined, [list], diff --git a/src/screens/Search/modules/ExploreTrendingTopics.tsx b/src/screens/Search/modules/ExploreTrendingTopics.tsx index 38c8cac0a8..60c42323c3 100644 --- a/src/screens/Search/modules/ExploreTrendingTopics.tsx +++ b/src/screens/Search/modules/ExploreTrendingTopics.tsx @@ -1,11 +1,8 @@ import {useMemo} from 'react' import {Pressable, View} from 'react-native' import {Image} from 'expo-image' -import { - type AppBskyUnspeccedDefs, - moderateProfile, - RichText as RichTextApi, -} from '@atproto/api' +import {type AppBskyUnspeccedDefs, moderateProfile} from '@atproto/api' +import {RichText as RichTextApi} from '@bsky.app/sdk/richtext' import {Plural, Trans, useLingui} from '@lingui/react/macro' import {useModerationOpts} from '#/state/preferences/moderation-opts' diff --git a/src/screens/StarterPack/StarterPackScreen.tsx b/src/screens/StarterPack/StarterPackScreen.tsx index ddbd5cd840..bb9c6506e9 100644 --- a/src/screens/StarterPack/StarterPackScreen.tsx +++ b/src/screens/StarterPack/StarterPackScreen.tsx @@ -6,8 +6,8 @@ import { AppBskyGraphStarterpack, AtUri, type ModerationOpts, - RichText as RichTextAPI, } from '@atproto/api' +import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Plural, Trans} from '@lingui/react/macro' @@ -24,6 +24,7 @@ import { type NavigationProp, } from '#/lib/routes/types' import {cleanError} from '#/lib/strings/errors' +import {asSdkFacets} from '#/lib/strings/rich-text-helpers' import {getStarterPackOgCard} from '#/lib/strings/starter-pack' import {logger} from '#/logger' import {updateProfileShadow} from '#/state/cache/profile-shadow' @@ -418,7 +419,7 @@ function Header({ const richText = record.description ? new RichTextAPI({ text: record.description, - facets: record.descriptionFacets, + facets: asSdkFacets(record.descriptionFacets), }) : undefined diff --git a/src/screens/VideoFeed/index.tsx b/src/screens/VideoFeed/index.tsx index 6e7bb4139a..38c2e5a89d 100644 --- a/src/screens/VideoFeed/index.tsx +++ b/src/screens/VideoFeed/index.tsx @@ -31,8 +31,8 @@ import { AppBskyFeedPost, AtUri, type ModerationDecision, - RichText as RichTextAPI, } from '@atproto/api' +import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {Trans, useLingui} from '@lingui/react/macro' import { type RouteProp, @@ -58,6 +58,7 @@ import { import {sanitizeDisplayName} from '#/lib/strings/display-names' import {cleanError} from '#/lib/strings/errors' import {sanitizeHandle} from '#/lib/strings/handles' +import {asSdkFacets} from '#/lib/strings/rich-text-helpers' import {logger} from '#/logger' import {useA11y} from '#/state/a11y' import { @@ -834,7 +835,7 @@ function Overlay({ : undefined const richText = new RichTextAPI({ text: record?.text || '', - facets: record?.facets, + facets: asSdkFacets(record?.facets), }) const handle = sanitizeHandle(post.author.handle, '@') diff --git a/src/state/queries/feed.ts b/src/state/queries/feed.ts index 9a9a8c09b9..a3b90de32b 100644 --- a/src/state/queries/feed.ts +++ b/src/state/queries/feed.ts @@ -5,9 +5,9 @@ import { type AppBskyGraphDefs, AtUri, moderateFeedGenerator, - RichText, } from '@atproto/api' import {type AtUriString} from '@atproto/syntax' +import {RichText} from '@bsky.app/sdk/richtext' import {t} from '@lingui/core/macro' import { type InfiniteData, @@ -22,6 +22,7 @@ import { import {DISCOVER_FEED_URI, DISCOVER_SAVED_FEED} from '#/lib/constants' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeHandle} from '#/lib/strings/handles' +import {asSdkFacets} from '#/lib/strings/rich-text-helpers' import {GCTIME, STALE} from '#/state/queries' import {RQKEY as listQueryKey} from '#/state/queries/list' import {usePreferencesQuery} from '#/state/queries/preferences' @@ -104,7 +105,7 @@ export function hydrateFeedGenerator( const description = new RichText({ text: view.description || '', - facets: (view.descriptionFacets || [])?.slice(), + facets: asSdkFacets((view.descriptionFacets || [])?.slice()), }) if (!view.descriptionFacets) { @@ -145,7 +146,7 @@ export function hydrateList(view: AppBskyGraphDefs.ListView): FeedSourceInfo { const description = new RichText({ text: view.description || '', - facets: (view.descriptionFacets || [])?.slice(), + facets: asSdkFacets((view.descriptionFacets || [])?.slice()), }) if (!view.descriptionFacets) { diff --git a/src/state/queries/starter-packs.ts b/src/state/queries/starter-packs.ts index ddf8365dda..6a1aecef0e 100644 --- a/src/state/queries/starter-packs.ts +++ b/src/state/queries/starter-packs.ts @@ -3,11 +3,10 @@ import { AppBskyGraphDefs, type AppBskyGraphGetStarterPack, AppBskyGraphStarterpack, - type AppBskyRichtextFacet, type AtpAgent, AtUri, - RichText, } from '@atproto/api' +import {RichText} from '@bsky.app/sdk/richtext' import { type QueryClient, useMutation, @@ -26,7 +25,8 @@ import { import {invalidateActorStarterPacksQuery} from '#/state/queries/actor-starter-packs' import {STALE} from '#/state/queries/index' import {invalidateListMembersQuery} from '#/state/queries/list-members' -import {useAgent} from '#/state/session' +import {useAgent, useAppviewClient} from '#/state/session' +import {type app} from '#/lexicons' import * as bsky from '#/types/bsky' const RQKEY_ROOT = 'starter-pack' @@ -105,6 +105,11 @@ export function useCreateStarterPackMutation({ }) { const queryClient = useQueryClient() const agent = useAgent() + /* + * Facet/mention resolution is an appview job - it resolves handles through + * the appview, and the public fallback keeps it working when logged out. + */ + const appviewClient = useAppviewClient() return useMutation< {uri: string; cid: string}, @@ -112,10 +117,10 @@ export function useCreateStarterPackMutation({ UseCreateStarterPackMutationParams >({ mutationFn: async ({name, description, feeds, profiles}) => { - let descriptionFacets: AppBskyRichtextFacet.Main[] | undefined + let descriptionFacets: app.bsky.richtext.facet.Main[] | undefined if (description) { const rt = new RichText({text: description}) - await rt.detectFacets(agent) + await rt.detectFacets(appviewClient) descriptionFacets = rt.facets } @@ -167,6 +172,7 @@ export function useEditStarterPackMutation({ }) { const queryClient = useQueryClient() const agent = useAgent() + const appviewClient = useAppviewClient() return useMutation< void, @@ -184,10 +190,10 @@ export function useEditStarterPackMutation({ currentStarterPack, currentListItems, }) => { - let descriptionFacets: AppBskyRichtextFacet.Main[] | undefined + let descriptionFacets: app.bsky.richtext.facet.Main[] | undefined if (description) { const rt = new RichText({text: description}) - await rt.detectFacets(agent) + await rt.detectFacets(appviewClient) descriptionFacets = rt.facets } diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 6c7c2280c2..c2e651556d 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -49,10 +49,10 @@ import { AppBskyUnspeccedDefs, AtUri, ChatBskyGroupDefs, - type RichText, } from '@atproto/api' import {type Client} from '@atproto/lex' import {type AtUriString} from '@atproto/syntax' +import {type RichText} from '@bsky.app/sdk/richtext' import {plural} from '@lingui/core/macro' import {Trans, useLingui} from '@lingui/react/macro' import {useNavigation} from '@react-navigation/native' @@ -1083,6 +1083,7 @@ export const ComposePost = ({ replyTo: replyTo?.uri, onStateChange: setPublishingStage, langs: currentLanguages, + appviewClient: client, }) ).uris[0] diff --git a/src/view/com/composer/drafts/state/api.ts b/src/view/com/composer/drafts/state/api.ts index b8fddc618f..3b5ff9cdf2 100644 --- a/src/view/com/composer/drafts/state/api.ts +++ b/src/view/com/composer/drafts/state/api.ts @@ -1,7 +1,8 @@ /** * Type converters for Draft API - convert between ComposerState and server Draft types. */ -import {AppBskyDraftDefs, AtUri, RichText} from '@atproto/api' +import {AppBskyDraftDefs, AtUri} from '@atproto/api' +import {RichText} from '@bsky.app/sdk/richtext' import {nanoid} from 'nanoid/non-secure' import {resolveLink} from '#/lib/api/resolve' diff --git a/src/view/com/composer/select-language/SuggestedLanguage.tsx b/src/view/com/composer/select-language/SuggestedLanguage.tsx index ab731cda22..71a45ec796 100644 --- a/src/view/com/composer/select-language/SuggestedLanguage.tsx +++ b/src/view/com/composer/select-language/SuggestedLanguage.tsx @@ -1,11 +1,11 @@ import {useEffect, useMemo, useRef, useState} from 'react' import {Platform, Text as RNText, View} from 'react-native' -import {RichText} from '@atproto/api' import {parseLanguageString} from '@atproto/syntax' import { guessLanguageAsync, type LanguageResult, } from '@bsky.app/expo-guess-language' +import {RichText} from '@bsky.app/sdk/richtext' import {Trans, useLingui} from '@lingui/react/macro' import debounce from 'lodash.debounce' diff --git a/src/view/com/composer/state/composer.ts b/src/view/com/composer/state/composer.ts index fc8b341270..3d2aa45992 100644 --- a/src/view/com/composer/state/composer.ts +++ b/src/view/com/composer/state/composer.ts @@ -3,9 +3,8 @@ import { type AppBskyActorDefs, type AppBskyDraftDefs, type AppBskyFeedPostgate, - AppBskyRichtextFacet, - RichText, } from '@atproto/api' +import {RichText} from '@bsky.app/sdk/richtext' import {nanoid} from 'nanoid/non-secure' import {type VideoTelemetry} from '#/lib/media/video/telemetry' @@ -28,6 +27,8 @@ import { suggestLinkCardUri, } from '#/view/com/composer/text-input/text-input-util' import {type Gif} from '#/features/gifPicker/types' +import {app} from '#/lexicons' +import * as bsky from '#/types/bsky' import { createVideoState, type VideoAction, @@ -677,7 +678,7 @@ export function createComposerState({ if (initRichText.facets) { for (const facet of initRichText.facets) { for (const feature of facet.features) { - if (AppBskyRichtextFacet.isLink(feature)) { + if (bsky.isType(app.bsky.richtext.facet.link, feature)) { if (isBskyPostUrl(feature.uri)) { detectedPostUris.set(feature.uri, {facet, rt: initRichText}) } else { diff --git a/src/view/com/composer/text-input/TextInput.tsx b/src/view/com/composer/text-input/TextInput.tsx index 88389ffd56..d230b01545 100644 --- a/src/view/com/composer/text-input/TextInput.tsx +++ b/src/view/com/composer/text-input/TextInput.tsx @@ -12,7 +12,7 @@ import { View, } from 'react-native' import {type PasteEventPayload, TextInputWrapper} from 'expo-paste-input' -import {AppBskyRichtextFacet, RichText} from '@atproto/api' +import {RichText} from '@bsky.app/sdk/richtext' import {useLingui} from '@lingui/react/macro' import {IMAGE_SIZE_CONFIG_POSTS} from '#/lib/constants' @@ -27,6 +27,8 @@ import { import {atoms as a, useAlf} from '#/alf' import {normalizeTextStyles} from '#/alf/typography' import {IS_ANDROID} from '#/env' +import {app} from '#/lexicons' +import * as bsky from '#/types/bsky' import {Autocomplete} from './mobile/Autocomplete' import {type TextInputProps} from './TextInput.types' @@ -88,7 +90,7 @@ export function TextInput({ if (newRt.facets) { for (const facet of newRt.facets) { for (const feature of facet.features) { - if (AppBskyRichtextFacet.isLink(feature)) { + if (bsky.isType(app.bsky.richtext.facet.link, feature)) { if (isUriImage(feature.uri)) { const res = await downloadAndResize({ uri: feature.uri, diff --git a/src/view/com/composer/text-input/TextInput.types.ts b/src/view/com/composer/text-input/TextInput.types.ts index fab2bc32f8..e60432b41e 100644 --- a/src/view/com/composer/text-input/TextInput.types.ts +++ b/src/view/com/composer/text-input/TextInput.types.ts @@ -1,5 +1,5 @@ import {type TextInput} from 'react-native' -import {type RichText} from '@atproto/api' +import {type RichText} from '@bsky.app/sdk/richtext' export type TextInputRef = { focus: () => void diff --git a/src/view/com/composer/text-input/TextInput.web.tsx b/src/view/com/composer/text-input/TextInput.web.tsx index e76d9e3f3d..f75d60f4bd 100644 --- a/src/view/com/composer/text-input/TextInput.web.tsx +++ b/src/view/com/composer/text-input/TextInput.web.tsx @@ -8,7 +8,7 @@ import { } from 'react' import {StyleSheet, View} from 'react-native' import Animated, {FadeIn, FadeOut} from 'react-native-reanimated' -import {AppBskyRichtextFacet, RichText} from '@atproto/api' +import {RichText} from '@bsky.app/sdk/richtext' import {Trans} from '@lingui/react/macro' import {getSchema} from '@tiptap/core' import {Document} from '@tiptap/extension-document' @@ -40,6 +40,8 @@ import {normalizeTextStyles} from '#/alf/typography' import {type Emoji} from '#/components/EmojiPicker' import {Portal} from '#/components/Portal' import {Text} from '#/components/Typography' +import {app} from '#/lexicons' +import * as bsky from '#/types/bsky' import {type TextInputProps} from './TextInput.types' import {type AutocompleteRef, createSuggestion} from './web/Autocomplete' import {LinkDecorator} from './web/LinkDecorator' @@ -265,7 +267,7 @@ export function TextInput({ if (newRt.facets) { for (const facet of newRt.facets) { for (const feature of facet.features) { - if (AppBskyRichtextFacet.isLink(feature)) { + if (bsky.isType(app.bsky.richtext.facet.link, feature)) { nextDetectedUris.set(feature.uri, {facet, rt: newRt}) } } diff --git a/src/view/com/composer/text-input/text-input-util.ts b/src/view/com/composer/text-input/text-input-util.ts index 71db8c3238..998d7074d9 100644 --- a/src/view/com/composer/text-input/text-input-util.ts +++ b/src/view/com/composer/text-input/text-input-util.ts @@ -1,8 +1,10 @@ -import {type AppBskyRichtextFacet, type RichText} from '@atproto/api' +import {type RichText} from '@bsky.app/sdk/richtext' + +import {type app} from '#/lexicons' export type LinkFacetMatch = { rt: RichText - facet: AppBskyRichtextFacet.Main + facet: app.bsky.richtext.facet.Main } export function suggestLinkCardUri( diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx index dc17bff725..b67133df65 100644 --- a/src/view/com/post/Post.tsx +++ b/src/view/com/post/Post.tsx @@ -6,14 +6,15 @@ import { AtUri, moderatePost, type ModerationDecision, - RichText as RichTextAPI, } from '@atproto/api' +import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {useQueryClient} from '@tanstack/react-query' import {MAX_POST_LINES} from '#/lib/constants' import {useOpenComposer} from '#/lib/hooks/useOpenComposer' import {makeProfileLink} from '#/lib/routes/links' import {countLines} from '#/lib/strings/helpers' +import {asSdkFacets} from '#/lib/strings/rich-text-helpers' import { POST_TOMBSTONE, type Shadow, @@ -68,7 +69,7 @@ export function Post({ record ? new RichTextAPI({ text: record.text, - facets: record.facets, + facets: asSdkFacets(record.facets), }) : undefined, [record], diff --git a/src/view/com/posts/PostFeed.tsx b/src/view/com/posts/PostFeed.tsx index eea2453739..2ccf3872dc 100644 --- a/src/view/com/posts/PostFeed.tsx +++ b/src/view/com/posts/PostFeed.tsx @@ -25,8 +25,8 @@ import { AppBskyEmbedImages, AppBskyEmbedVideo, type AppBskyFeedDefs, - type RichText as RichTextType, } from '@atproto/api' +import {type RichText as RichTextType} from '@bsky.app/sdk/richtext' import {useLingui} from '@lingui/react/macro' import {useQueryClient} from '@tanstack/react-query' diff --git a/src/view/com/posts/PostFeedItem.tsx b/src/view/com/posts/PostFeedItem.tsx index 57f506c5d1..a95b41635b 100644 --- a/src/view/com/posts/PostFeedItem.tsx +++ b/src/view/com/posts/PostFeedItem.tsx @@ -7,8 +7,8 @@ import { AppBskyFeedThreadgate, AtUri, type ModerationDecision, - RichText as RichTextAPI, } from '@atproto/api' +import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {useQueryClient} from '@tanstack/react-query' import {type ReasonFeedSource} from '#/lib/api/feed/types' @@ -17,6 +17,7 @@ import {useOpenComposer} from '#/lib/hooks/useOpenComposer' import {usePalette} from '#/lib/hooks/usePalette' import {makeProfileLink} from '#/lib/routes/links' import {countLines} from '#/lib/strings/helpers' +import {asSdkFacets} from '#/lib/strings/rich-text-helpers' import { POST_TOMBSTONE, type Shadow, @@ -104,7 +105,7 @@ export function PostFeedItem({ () => new RichTextAPI({ text: record.text, - facets: record.facets, + facets: asSdkFacets(record.facets), }), [record], ) diff --git a/src/view/screens/DebugMod.tsx b/src/view/screens/DebugMod.tsx index c3fe8fba8e..d92043966c 100644 --- a/src/view/screens/DebugMod.tsx +++ b/src/view/screens/DebugMod.tsx @@ -15,8 +15,8 @@ import { type ModerationBehavior, type ModerationDecision, type ModerationOpts, - RichText, } from '@atproto/api' +import {RichText} from '@bsky.app/sdk/richtext' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx index ead74bae94..c796bf9586 100644 --- a/src/view/screens/Profile.tsx +++ b/src/view/screens/Profile.tsx @@ -6,8 +6,8 @@ import { type AppBskyActorDefs, moderateProfile, type ModerationOpts, - RichText as RichTextAPI, } from '@atproto/api' +import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' @@ -33,7 +33,7 @@ import {useLabelerInfoQuery} from '#/state/queries/labeler' import {resetProfilePostsQueries} from '#/state/queries/post-feed' import {useProfileQuery} from '#/state/queries/profile' import {useResolveDidQuery} from '#/state/queries/resolve-uri' -import {useAgent, useSession} from '#/state/session' +import {useAppviewClient, useSession} from '#/state/session' import {ProfileFeedgens} from '#/view/com/feeds/ProfileFeedgens' import {ProfileLists} from '#/view/com/lists/ProfileLists' import {PagerWithHeader} from '#/view/com/pager/PagerWithHeader' @@ -613,7 +613,11 @@ function ProfileScreenLoaded({ } function useRichText(text: string): [RichTextAPI, boolean] { - const agent = useAgent() + /* + * Facet/mention resolution is an appview job - it resolves handles through + * the appview, and the public fallback keeps it working when logged out. + */ + const client = useAppviewClient() const [prevText, setPrevText] = useState(text) const [rawRT, setRawRT] = useState(() => new RichTextAPI({text})) const [resolvedRT, setResolvedRT] = useState(null) @@ -628,7 +632,7 @@ function useRichText(text: string): [RichTextAPI, boolean] { async function resolveRTFacets() { // new each time const resolvedRT = new RichTextAPI({text}) - await resolvedRT.detectFacets(agent) + await resolvedRT.detectFacets(client) if (!ignore) { setResolvedRT(resolvedRT) } @@ -637,7 +641,7 @@ function useRichText(text: string): [RichTextAPI, boolean] { return () => { ignore = true } - }, [text, agent]) + }, [text, client]) const isResolving = resolvedRT === null return [resolvedRT ?? rawRT, isResolving] } From 3568cdc42d62bca5968a1f87c12fbca30601716b Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 13 Aug 2026 22:26:19 +0300 Subject: [PATCH 18/32] [SDK] Migrate getAllListMembers to the appview client (#11377) Co-authored-by: Claude Fable 5 --- .../lists/CreateListFromStarterPackDialog.tsx | 8 +++-- src/screens/Onboarding/StepFinished/index.tsx | 9 ++++-- .../StarterPackCard.tsx | 5 ++-- src/screens/StarterPack/StarterPackScreen.tsx | 5 ++-- src/state/queries/list-members.ts | 29 +++++++------------ 5 files changed, 30 insertions(+), 26 deletions(-) diff --git a/src/components/dialogs/lists/CreateListFromStarterPackDialog.tsx b/src/components/dialogs/lists/CreateListFromStarterPackDialog.tsx index 3e74de5c67..3aabbec9ff 100644 --- a/src/components/dialogs/lists/CreateListFromStarterPackDialog.tsx +++ b/src/components/dialogs/lists/CreateListFromStarterPackDialog.tsx @@ -20,7 +20,7 @@ import {wait} from '#/lib/async/wait' import {type NavigationProp} from '#/lib/routes/types' import {logger} from '#/logger' import {getAllListMembers} from '#/state/queries/list-members' -import {useAgent, useSession} from '#/state/session' +import {useAgent, useAppviewClient, useSession} from '#/state/session' import {atoms as a, platform, useTheme, web} from '#/alf' import {Admonition} from '#/components/Admonition' import {Button, ButtonText} from '#/components/Button' @@ -41,6 +41,7 @@ export function CreateListFromStarterPackDialog({ const {_} = useLingui() const t = useTheme() const agent = useAgent() + const appviewClient = useAppviewClient() const ax = useAnalytics() const {currentAccount} = useSession() const navigation = useNavigation() @@ -73,7 +74,10 @@ export function CreateListFromStarterPackDialog({ const listItems = await wait( 3000, (async () => { - const items = await getAllListMembers(agent, starterPack.list!.uri) + const items = await getAllListMembers( + appviewClient, + starterPack.list!.uri, + ) if (items.length > 0) { const listitemWrites: $Typed[] = diff --git a/src/screens/Onboarding/StepFinished/index.tsx b/src/screens/Onboarding/StepFinished/index.tsx index 11dfd35ceb..05f4ef8626 100644 --- a/src/screens/Onboarding/StepFinished/index.tsx +++ b/src/screens/Onboarding/StepFinished/index.tsx @@ -26,7 +26,7 @@ import {useSetHasCheckedForStarterPack} from '#/state/preferences/used-starter-p import {getAllListMembers} from '#/state/queries/list-members' import {preferencesQueryKey} from '#/state/queries/preferences' import {RQKEY as profileRQKey} from '#/state/queries/profile' -import {useAgent} from '#/state/session' +import {useAgent, useAppviewClient} from '#/state/session' import {useOnboardingDispatch} from '#/state/shell' import { useActiveStarterPack, @@ -58,6 +58,7 @@ export function StepFinished() { const [saving, setSaving] = useState(false) const queryClient = useQueryClient() const agent = useAgent() + const appviewClient = useAppviewClient() const requestNotificationsPermission = useRequestNotificationsPermission() const activeStarterPack = useActiveStarterPack() const setActiveStarterPack = useSetActiveStarterPack() @@ -82,7 +83,10 @@ export function StepFinished() { } try { if (starterPack?.list) { - listItems = await getAllListMembers(agent, starterPack.list.uri) + listItems = await getAllListMembers( + appviewClient, + starterPack.list.uri, + ) } } catch (e) { logger.error('Failed to fetch starter pack list items', { @@ -231,6 +235,7 @@ export function StepFinished() { ax, queryClient, agent, + appviewClient, dispatch, onboardDispatch, activeStarterPack, diff --git a/src/screens/Onboarding/StepSuggestedStarterpacks/StarterPackCard.tsx b/src/screens/Onboarding/StepSuggestedStarterpacks/StarterPackCard.tsx index b016af65db..fa296dfbd2 100644 --- a/src/screens/Onboarding/StepSuggestedStarterpacks/StarterPackCard.tsx +++ b/src/screens/Onboarding/StepSuggestedStarterpacks/StarterPackCard.tsx @@ -11,7 +11,7 @@ import {isBlockedOrBlocking, isMuted} from '#/lib/moderation/blocked-and-muted' import {logger} from '#/logger' import {updateProfileShadow} from '#/state/cache/profile-shadow' import {getAllListMembers} from '#/state/queries/list-members' -import {useAgent, useSession} from '#/state/session' +import {useAgent, useAppviewClient, useSession} from '#/state/session' import {bulkWriteFollows} from '#/screens/Onboarding/util' import {AvatarStack} from '#/screens/Search/components/StarterPackCard' import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' @@ -36,6 +36,7 @@ export function StarterPackCard({ const {currentAccount} = useSession() const {gtPhone} = useBreakpoints() const agent = useAgent() + const appviewClient = useAppviewClient() const queryClient = useQueryClient() const record = view.record const [isProcessing, setIsProcessing] = useState(false) @@ -48,7 +49,7 @@ export function StarterPackCard({ let listItems: AppBskyGraphDefs.ListItemView[] = [] try { - listItems = await getAllListMembers(agent, view.list.uri) + listItems = await getAllListMembers(appviewClient, view.list.uri) } catch (e) { setIsProcessing(false) Toast.show(_(msg`An error occurred while trying to follow all`), { diff --git a/src/screens/StarterPack/StarterPackScreen.tsx b/src/screens/StarterPack/StarterPackScreen.tsx index bb9c6506e9..e7674c00b4 100644 --- a/src/screens/StarterPack/StarterPackScreen.tsx +++ b/src/screens/StarterPack/StarterPackScreen.tsx @@ -37,7 +37,7 @@ import { useDeleteStarterPackMutation, useStarterPackQuery, } from '#/state/queries/starter-packs' -import {useAgent, useSession} from '#/state/session' +import {useAgent, useAppviewClient, useSession} from '#/state/session' import {useSetActiveStarterPack} from '#/state/shell/landing' import {useLoggedOutViewControls} from '#/state/shell/logged-out' import { @@ -312,6 +312,7 @@ function Header({ const t = useTheme() const {currentAccount, hasSession} = useSession() const agent = useAgent() + const appviewClient = useAppviewClient() const queryClient = useQueryClient() const setActiveStarterPack = useSetActiveStarterPack() const {requestSwitchToAccount} = useLoggedOutViewControls() @@ -354,7 +355,7 @@ function Header({ let listItems: AppBskyGraphDefs.ListItemView[] = [] try { - listItems = await getAllListMembers(agent, starterPack.list.uri) + listItems = await getAllListMembers(appviewClient, starterPack.list.uri) } catch (e) { setIsProcessing(false) Toast.show(_(msg`An error occurred while trying to follow all`), { diff --git a/src/state/queries/list-members.ts b/src/state/queries/list-members.ts index bde6666dc6..6e70ff4027 100644 --- a/src/state/queries/list-members.ts +++ b/src/state/queries/list-members.ts @@ -1,8 +1,5 @@ -import { - type AppBskyActorDefs, - type AppBskyGraphDefs, - type AtpAgent, -} from '@atproto/api' +import {type AppBskyActorDefs, type AppBskyGraphDefs} from '@atproto/api' +import {type Client} from '@atproto/lex' import {type AtUriString} from '@atproto/syntax' import { type InfiniteData, @@ -13,7 +10,7 @@ import { } from '@tanstack/react-query' import {STALE} from '#/state/queries' -import {useAgent, useAppviewClient} from '#/state/session' +import {useAppviewClient} from '#/state/session' import {app} from '#/lexicons' const PAGE_SIZE = 30 @@ -50,36 +47,32 @@ export function useListMembersQuery(uri?: string, limit: number = PAGE_SIZE) { } export function useAllListMembersQuery(uri?: string) { - const agent = useAgent() + const client = useAppviewClient() return useQuery({ staleTime: STALE.MINUTES.ONE, queryKey: RQKEY_ALL(uri ?? ''), queryFn: async () => { - return getAllListMembers(agent, uri!) + return getAllListMembers(client, uri!) }, enabled: Boolean(uri), }) } -/* - * Still on the legacy agent: four call sites outside this cluster pass their - * own `AtpAgent`, so the parameter type is migrated with those consumers. - */ -export async function getAllListMembers(agent: AtpAgent, uri: string) { +export async function getAllListMembers(client: Client, uri: string) { let hasMore = true let cursor: string | undefined const listItems: AppBskyGraphDefs.ListItemView[] = [] // We want to cap this at 6 pages, just for anything weird happening with the api let i = 0 while (hasMore && i < 6) { - const res = await agent.app.bsky.graph.getList({ - list: uri, + const res = await client.call(app.bsky.graph.getList, { + list: uri as AtUriString, limit: 50, cursor, }) - listItems.push(...res.data.items) - hasMore = Boolean(res.data.cursor) - cursor = res.data.cursor + listItems.push(...res.items) + hasMore = Boolean(res.cursor) + cursor = res.cursor i++ } return listItems From ca226f589db36803c1be217bd99432be76370cfb Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 13 Aug 2026 22:26:19 +0300 Subject: [PATCH 19/32] [SDK] Migrate the gate records to raw repo calls on the pds client (#11378) Co-authored-by: Claude Fable 5 --- oxlint-suppressions.json | 16 +--- .../dialogs/PostInteractionSettingsDialog.tsx | 22 ++--- src/lib/api/index.ts | 3 +- src/lib/xrpc-error.ts | 20 ++++ .../ModerationInteractionSettings/index.tsx | 17 +++- src/state/queries/postgate/index.ts | 83 +++++++++-------- src/state/queries/postgate/util.ts | 34 +++++-- src/state/queries/threadgate/index.ts | 91 +++++++++---------- src/state/queries/threadgate/util.ts | 67 +++++++++----- src/view/com/composer/state/composer.ts | 38 +++++--- .../com/composer/threadgate/ThreadgateBtn.tsx | 23 +++-- 11 files changed, 234 insertions(+), 180 deletions(-) diff --git a/oxlint-suppressions.json b/oxlint-suppressions.json index 3b2304e8fe..5ffe12abbc 100644 --- a/oxlint-suppressions.json +++ b/oxlint-suppressions.json @@ -1400,16 +1400,13 @@ }, "src/state/queries/postgate/index.ts": { "typescript/no-explicit-any": { - "count": 2 + "count": 1 }, "typescript/no-floating-promises": { "count": 1 }, - "typescript/no-unsafe-call": { - "count": 2 - }, "typescript/no-unsafe-member-access": { - "count": 3 + "count": 1 }, "typescript/require-await": { "count": 2 @@ -1426,18 +1423,9 @@ } }, "src/state/queries/threadgate/index.ts": { - "typescript/no-explicit-any": { - "count": 1 - }, "typescript/no-floating-promises": { "count": 3 }, - "typescript/no-unsafe-call": { - "count": 2 - }, - "typescript/no-unsafe-member-access": { - "count": 2 - }, "typescript/require-await": { "count": 3 } diff --git a/src/components/dialogs/PostInteractionSettingsDialog.tsx b/src/components/dialogs/PostInteractionSettingsDialog.tsx index 8a5c7101cc..63684f4831 100644 --- a/src/components/dialogs/PostInteractionSettingsDialog.tsx +++ b/src/components/dialogs/PostInteractionSettingsDialog.tsx @@ -1,10 +1,7 @@ import {useCallback, useMemo, useState} from 'react' import {LayoutAnimation, Text as NestedText, View} from 'react-native' -import { - type AppBskyFeedDefs, - type AppBskyFeedPostgate, - AtUri, -} from '@atproto/api' +import {type AppBskyFeedDefs} from '@atproto/api' +import {AtUri} from '@atproto/syntax' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Plural, Trans} from '@lingui/react/macro' @@ -36,7 +33,7 @@ import { PostThreadContextProvider, usePostThreadContext, } from '#/state/queries/usePostThread' -import {useAgent, useSession} from '#/state/session' +import {usePdsClient, useSession} from '#/state/session' import {UserAvatar} from '#/view/com/util/UserAvatar' import {atoms as a, useTheme, web} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' @@ -53,6 +50,7 @@ import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' import {IS_IOS} from '#/env' +import {type app} from '#/lexicons' export type PostInteractionSettingsFormProps = { canSave?: boolean @@ -63,8 +61,8 @@ export type PostInteractionSettingsFormProps = { persist?: boolean onChangePersist?: (v: boolean) => void - postgate: AppBskyFeedPostgate.Record - onChangePostgate: (v: AppBskyFeedPostgate.Record) => void + postgate: app.bsky.feed.postgate.Main + onChangePostgate: (v: app.bsky.feed.postgate.Main) => void threadgateAllowUISettings: ThreadgateAllowUISetting[] onChangeThreadgateAllowUISettings: (v: ThreadgateAllowUISetting[]) => void @@ -178,7 +176,7 @@ export function PostInteractionSettingsDialogControlledInner( const {mutateAsync: setThreadgateAllow} = useSetThreadgateAllowMutation() const [editedPostgate, setEditedPostgate] = - useState() + useState() const [editedAllowUISettings, setEditedAllowUISettings] = useState() @@ -697,7 +695,7 @@ export function usePrefetchPostInteractionSettings({ }) { const ax = useAnalytics() const queryClient = useQueryClient() - const agent = useAgent() + const pdsClient = usePdsClient() const getPost = useGetPost() return useCallback(async () => { @@ -706,7 +704,7 @@ export function usePrefetchPostInteractionSettings({ queryClient.prefetchQuery({ queryKey: createPostgateQueryKey(postUri), queryFn: () => - getPostgateRecord({agent, postUri}).then(res => res ?? null), + getPostgateRecord({pdsClient, postUri}).then(res => res ?? null), staleTime: STALE.SECONDS.THIRTY, }), queryClient.prefetchQuery({ @@ -723,5 +721,5 @@ export function usePrefetchPostInteractionSettings({ safeMessage: e.message, }) } - }, [ax, queryClient, agent, postUri, rootPostUri, getPost]) + }, [ax, queryClient, pdsClient, postUri, rootPostUri, getPost]) } diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index 9f81abb77a..aee481669e 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -16,6 +16,7 @@ import { } from '@atproto/api' import {TID} from '@atproto/common-web' import {type Client} from '@atproto/lex' +import {toDatetimeString} from '@atproto/syntax' import {RichText} from '@bsky.app/sdk/richtext' import {t} from '@lingui/core/macro' import {type QueryClient} from '@tanstack/react-query' @@ -145,7 +146,7 @@ export async function post( collection: 'app.bsky.feed.threadgate', rkey: rkey, value: createThreadgateRecord({ - createdAt: now.toISOString(), + createdAt: toDatetimeString(now), post: uri, allow: threadgateAllowUISettingToAllowRecordValue(thread.threadgate), }), diff --git a/src/lib/xrpc-error.ts b/src/lib/xrpc-error.ts index 25f9a1277e..855aaf803e 100644 --- a/src/lib/xrpc-error.ts +++ b/src/lib/xrpc-error.ts @@ -7,6 +7,8 @@ import { XrpcResponseError, } from '@atproto/lex' +import {com} from '#/lexicons' + /** * Same nsid means `e` was thrown for this method schema, so `e` can be * treated as an `XrpcResponseError` - which is what lets the SDK's @@ -57,3 +59,21 @@ export function matchXrpcError( } return undefined } + +/** + * Whether `e` is a `com.atproto.repo.getRecord` failure meaning the record is + * absent, so the caller can treat the read as "no record" instead of an error. + * + * `getRecord` declares `RecordNotFound` and both the PDS and the appview throw + * it by that name, so the declared code is the primary check. The message + * substring is kept as a fallback because that is what the pre-SDK call sites + * matched on, and an older PDS in the network may still answer with an + * unnamed error whose message carries the text. Dropping it would silently + * turn a previously-handled absence into a thrown error. + */ +export function isRecordNotFoundError(e: unknown): boolean { + if (matchXrpcError(e, com.atproto.repo.getRecord) === 'RecordNotFound') { + return true + } + return e instanceof Error && e.message.includes('Could not locate record:') +} diff --git a/src/screens/ModerationInteractionSettings/index.tsx b/src/screens/ModerationInteractionSettings/index.tsx index 261e6b55ae..12d952c872 100644 --- a/src/screens/ModerationInteractionSettings/index.tsx +++ b/src/screens/ModerationInteractionSettings/index.tsx @@ -1,5 +1,6 @@ import {useCallback, useMemo, useState} from 'react' import {View} from 'react-native' +import {type AtUriString, toDatetimeString} from '@atproto/syntax' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' @@ -22,6 +23,7 @@ import {PostInteractionSettingsForm} from '#/components/dialogs/PostInteractionS import * as Layout from '#/components/Layout' import {Loader} from '#/components/Loader' import * as Toast from '#/components/Toast' +import {type app} from '#/lexicons' export function Screen() { const gutters = useGutters(['base']) @@ -68,16 +70,21 @@ function Inner({preferences}: {preferences: UsePreferencesQueryResponse}) { const allowUI = useMemo(() => { return threadgateRecordToAllowUISetting({ $type: 'app.bsky.feed.threadgate', - post: '', - createdAt: new Date().toString(), - allow: preferences.postInteractionSettings.threadgateAllowRules, + post: '' as AtUriString, + createdAt: toDatetimeString(new Date()), + /* + * Preferences are still typed against the legacy client, so the stored + * rules arrive unbranded. Wave B migrates `getPreferences`. + */ + allow: preferences.postInteractionSettings + .threadgateAllowRules as app.bsky.feed.threadgate.Main['allow'], }) }, [preferences.postInteractionSettings.threadgateAllowRules]) const postgate = useMemo(() => { return createPostgateRecord({ post: '', - embeddingRules: - preferences.postInteractionSettings.postgateEmbeddingRules, + embeddingRules: preferences.postInteractionSettings + .postgateEmbeddingRules as app.bsky.feed.postgate.Main['embeddingRules'], }) }, [preferences.postInteractionSettings.postgateEmbeddingRules]) diff --git a/src/state/queries/postgate/index.ts b/src/state/queries/postgate/index.ts index 82a52730bc..e213fa73d5 100644 --- a/src/state/queries/postgate/index.ts +++ b/src/state/queries/postgate/index.ts @@ -3,13 +3,13 @@ import { AppBskyEmbedRecord, AppBskyEmbedRecordWithMedia, type AppBskyFeedDefs, - AppBskyFeedPostgate, - type AtpAgent, - AtUri, } from '@atproto/api' +import {type Client} from '@atproto/lex' +import {AtUri, type HandleString} from '@atproto/syntax' import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' import {networkRetry, retry} from '#/lib/async/retry' +import {isRecordNotFoundError} from '#/lib/xrpc-error' import {logger} from '#/logger' import {updatePostShadow} from '#/state/cache/post-shadow' import {STALE} from '#/state/queries' @@ -20,28 +20,28 @@ import { mergePostgateRecords, POSTGATE_COLLECTION, } from '#/state/queries/postgate/util' -import {useAgent} from '#/state/session' +import {usePdsClient} from '#/state/session' +import {app, com} from '#/lexicons' import * as bsky from '#/types/bsky' export async function getPostgateRecord({ - agent, + pdsClient, postUri, }: { - agent: AtpAgent + pdsClient: Client postUri: string -}): Promise { +}): Promise { const urip = new AtUri(postUri) if (!urip.host.startsWith('did:')) { - const res = await agent.resolveHandle({ - handle: urip.host, + const {did} = await pdsClient.call(com.atproto.identity.resolveHandle, { + handle: urip.host as HandleString, }) - // @ts-expect-error TODO new-sdk-migration - urip.host = res.data.did + urip.host = did } try { - const {data} = await retry( + const data = await retry( 2, e => { /* @@ -49,34 +49,31 @@ export async function getPostgateRecord({ * throwing an error. NB: This will also catch reference errors, such as * a typo in the URI. */ - if (e.message.includes(`Could not locate record:`)) { + if (isRecordNotFoundError(e)) { return false } return true }, () => - agent.api.com.atproto.repo.getRecord({ + pdsClient.call(com.atproto.repo.getRecord, { repo: urip.host, collection: POSTGATE_COLLECTION, - rkey: urip.rkey, + rkey: urip.rkeySafe, }), ) - if ( - data.value && - bsky.validate(data.value, AppBskyFeedPostgate.validateRecord) - ) { + if (data.value && bsky.matches(app.bsky.feed.postgate, data.value)) { return data.value } else { return undefined } - } catch (e: any) { + } catch (e) { /* * If the record doesn't exist, we want to return null instead of * throwing an error. NB: This will also catch reference errors, such as * a typo in the URI. */ - if (e.message.includes(`Could not locate record:`)) { + if (isRecordNotFoundError(e)) { return undefined } else { throw e @@ -85,21 +82,21 @@ export async function getPostgateRecord({ } export async function writePostgateRecord({ - agent, + pdsClient, postUri, postgate, }: { - agent: AtpAgent + pdsClient: Client postUri: string - postgate: AppBskyFeedPostgate.Record + postgate: app.bsky.feed.postgate.Main }) { const postUrip = new AtUri(postUri) await networkRetry(2, () => - agent.api.com.atproto.repo.putRecord({ - repo: agent.session!.did, + pdsClient.call(com.atproto.repo.putRecord, { + repo: pdsClient.assertDid, collection: POSTGATE_COLLECTION, - rkey: postUrip.rkey, + rkey: postUrip.rkeySafe, record: postgate, }), ) @@ -107,24 +104,24 @@ export async function writePostgateRecord({ export async function upsertPostgate( { - agent, + pdsClient, postUri, }: { - agent: AtpAgent + pdsClient: Client postUri: string }, callback: ( - postgate: AppBskyFeedPostgate.Record | undefined, - ) => Promise, + postgate: app.bsky.feed.postgate.Main | undefined, + ) => Promise, ) { const prev = await getPostgateRecord({ - agent, + pdsClient, postUri, }) const next = await callback(prev) if (!next) return await writePostgateRecord({ - agent, + pdsClient, postUri, postgate: next, }) @@ -135,18 +132,20 @@ export const createPostgateQueryKey = (postUri: string) => [ postUri, ] export function usePostgateQuery({postUri}: {postUri: string}) { - const agent = useAgent() + const pdsClient = usePdsClient() return useQuery({ staleTime: STALE.SECONDS.THIRTY, queryKey: createPostgateQueryKey(postUri), async queryFn() { - return await getPostgateRecord({agent, postUri}).then(res => res ?? null) + return await getPostgateRecord({pdsClient, postUri}).then( + res => res ?? null, + ) }, }) } export function useWritePostgateMutation() { - const agent = useAgent() + const pdsClient = usePdsClient() const queryClient = useQueryClient() return useMutation({ mutationFn: async ({ @@ -154,10 +153,10 @@ export function useWritePostgateMutation() { postgate, }: { postUri: string - postgate: AppBskyFeedPostgate.Record + postgate: app.bsky.feed.postgate.Main }) => { return writePostgateRecord({ - agent, + pdsClient, postUri, postgate, }) @@ -171,7 +170,7 @@ export function useWritePostgateMutation() { } export function useToggleQuoteDetachmentMutation() { - const agent = useAgent() + const pdsClient = usePdsClient() const queryClient = useQueryClient() const getPosts = useGetPosts() const prevEmbed = useRef(undefined) @@ -200,7 +199,7 @@ export function useToggleQuoteDetachmentMutation() { }) } - await upsertPostgate({agent, postUri: quoteUri}, async prev => { + await upsertPostgate({pdsClient, postUri: quoteUri}, async prev => { if (prev) { if (action === 'detach') { return mergePostgateRecords(prev, { @@ -264,7 +263,7 @@ export function useToggleQuoteDetachmentMutation() { } export function useToggleQuotepostEnabledMutation() { - const agent = useAgent() + const pdsClient = usePdsClient() return useMutation({ mutationFn: async ({ @@ -274,7 +273,7 @@ export function useToggleQuotepostEnabledMutation() { postUri: string action: 'enable' | 'disable' }) => { - await upsertPostgate({agent, postUri: postUri}, async prev => { + await upsertPostgate({pdsClient, postUri: postUri}, async prev => { if (prev) { if (action === 'disable') { return mergePostgateRecords(prev, { diff --git a/src/state/queries/postgate/util.ts b/src/state/queries/postgate/util.ts index 0952a1ad09..51c84419d0 100644 --- a/src/state/queries/postgate/util.ts +++ b/src/state/queries/postgate/util.ts @@ -3,29 +3,43 @@ import { AppBskyEmbedRecord, AppBskyEmbedRecordWithMedia, type AppBskyFeedDefs, - type AppBskyFeedPostgate, AtUri, } from '@atproto/api' +import {type AtUriString, toDatetimeString} from '@atproto/syntax' + +import {type app} from '#/lexicons' export const POSTGATE_COLLECTION = 'app.bsky.feed.postgate' +/** + * Create a new {@link app.bsky.feed.postgate.Main}. URIs are accepted as plain + * strings (callers hold raw AT-URIs, often from legacy-typed views) and + * asserted to the branded `AtUriString` here. + */ export function createPostgateRecord( - postgate: Partial & { - post: AppBskyFeedPostgate.Record['post'] + postgate: Omit< + Partial, + 'post' | 'detachedEmbeddingUris' + > & { + post: string + detachedEmbeddingUris?: string[] }, -): AppBskyFeedPostgate.Record { +): app.bsky.feed.postgate.Main { return { $type: POSTGATE_COLLECTION, - createdAt: new Date().toISOString(), - post: postgate.post, - detachedEmbeddingUris: postgate.detachedEmbeddingUris || [], + createdAt: toDatetimeString(new Date()), + post: postgate.post as AtUriString, + detachedEmbeddingUris: (postgate.detachedEmbeddingUris || + []) as AtUriString[], embeddingRules: postgate.embeddingRules || [], } } export function mergePostgateRecords( - prev: AppBskyFeedPostgate.Record, - next: Partial, + prev: app.bsky.feed.postgate.Main, + next: Omit, 'detachedEmbeddingUris'> & { + detachedEmbeddingUris?: string[] + }, ) { const detachedEmbeddingUris = Array.from( new Set([ @@ -199,5 +213,5 @@ export function getMaybeDetachedQuoteEmbed({ } export const embeddingRules = { - disableRule: {$type: 'app.bsky.feed.postgate#disableRule'}, + disableRule: {$type: 'app.bsky.feed.postgate#disableRule'} as const, } diff --git a/src/state/queries/threadgate/index.ts b/src/state/queries/threadgate/index.ts index 561275ca66..bd1a0f6b8c 100644 --- a/src/state/queries/threadgate/index.ts +++ b/src/state/queries/threadgate/index.ts @@ -1,12 +1,10 @@ -import { - type AppBskyFeedDefs, - AppBskyFeedThreadgate, - type AtpAgent, - AtUri, -} from '@atproto/api' +import {type AppBskyFeedDefs} from '@atproto/api' +import {type Client} from '@atproto/lex' +import {AtUri, type HandleString} from '@atproto/syntax' import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' import {networkRetry, retry} from '#/lib/async/retry' +import {isRecordNotFoundError} from '#/lib/xrpc-error' import {STALE} from '#/state/queries' import {useGetPost} from '#/state/queries/post' import {type ThreadgateAllowUISetting} from '#/state/queries/threadgate/types' @@ -17,8 +15,9 @@ import { threadgateViewToAllowUISetting, } from '#/state/queries/threadgate/util' import {useUpdatePostThreadThreadgateQueryCache} from '#/state/queries/usePostThread' -import {useAgent} from '#/state/session' +import {usePdsClient} from '#/state/session' import {useThreadgateHiddenReplyUrisAPI} from '#/state/threadgate-hidden-replies' +import {app, com} from '#/lexicons' import * as bsky from '#/types/bsky' export * from '#/state/queries/threadgate/types' @@ -40,9 +39,9 @@ export function useThreadgateRecordQuery({ initialData, }: { postUri?: string - initialData?: AppBskyFeedThreadgate.Record + initialData?: app.bsky.feed.threadgate.Main } = {}) { - const agent = useAgent() + const pdsClient = usePdsClient() return useQuery({ enabled: !!postUri, @@ -51,7 +50,7 @@ export function useThreadgateRecordQuery({ staleTime: STALE.MINUTES.ONE, async queryFn() { return getThreadgateRecord({ - agent, + pdsClient, postUri: postUri!, }) }, @@ -85,24 +84,23 @@ export function useThreadgateViewQuery({ } export async function getThreadgateRecord({ - agent, + pdsClient, postUri, }: { - agent: AtpAgent + pdsClient: Client postUri: string -}): Promise { +}): Promise { const urip = new AtUri(postUri) if (!urip.host.startsWith('did:')) { - const res = await agent.resolveHandle({ - handle: urip.host, + const {did} = await pdsClient.call(com.atproto.identity.resolveHandle, { + handle: urip.host as HandleString, }) - // @ts-expect-error TODO new-sdk-migration - urip.host = res.data.did + urip.host = did } try { - const {data} = await retry( + const data = await retry( 2, e => { /* @@ -110,34 +108,31 @@ export async function getThreadgateRecord({ * throwing an error. NB: This will also catch reference errors, such as * a typo in the URI. */ - if (e.message.includes(`Could not locate record:`)) { + if (isRecordNotFoundError(e)) { return false } return true }, () => - agent.api.com.atproto.repo.getRecord({ + pdsClient.call(com.atproto.repo.getRecord, { repo: urip.host, collection: 'app.bsky.feed.threadgate', - rkey: urip.rkey, + rkey: urip.rkeySafe, }), ) - if ( - data.value && - bsky.validate(data.value, AppBskyFeedThreadgate.validateRecord) - ) { + if (data.value && bsky.matches(app.bsky.feed.threadgate, data.value)) { return data.value } else { return null } - } catch (e: any) { + } catch (e) { /* * If the record doesn't exist, we want to return null instead of * throwing an error. NB: This will also catch reference errors, such as * a typo in the URI. */ - if (e.message.includes(`Could not locate record:`)) { + if (isRecordNotFoundError(e)) { return null } else { throw e @@ -146,13 +141,13 @@ export async function getThreadgateRecord({ } export async function writeThreadgateRecord({ - agent, + pdsClient, postUri, threadgate, }: { - agent: AtpAgent + pdsClient: Client postUri: string - threadgate: AppBskyFeedThreadgate.Record + threadgate: app.bsky.feed.threadgate.Main }) { const postUrip = new AtUri(postUri) const record = createThreadgateRecord({ @@ -162,10 +157,10 @@ export async function writeThreadgateRecord({ }) await networkRetry(2, () => - agent.api.com.atproto.repo.putRecord({ - repo: agent.session!.did, + pdsClient.call(com.atproto.repo.putRecord, { + repo: pdsClient.assertDid, collection: 'app.bsky.feed.threadgate', - rkey: postUrip.rkey, + rkey: postUrip.rkeySafe, record, }), ) @@ -173,25 +168,25 @@ export async function writeThreadgateRecord({ export async function upsertThreadgate( { - agent, + pdsClient, postUri, }: { - agent: AtpAgent + pdsClient: Client postUri: string }, callback: ( - threadgate: AppBskyFeedThreadgate.Record | null, - ) => Promise, + threadgate: app.bsky.feed.threadgate.Main | null, + ) => Promise, ) { const prev = await getThreadgateRecord({ - agent, + pdsClient, postUri, }) const next = await callback(prev) if (!next) return validateThreadgateRecordOrThrow(next) await writeThreadgateRecord({ - agent, + pdsClient, postUri, threadgate: next, }) @@ -201,15 +196,15 @@ export async function upsertThreadgate( * Update the allow list for a threadgate record. */ export async function updateThreadgateAllow({ - agent, + pdsClient, postUri, allow, }: { - agent: AtpAgent + pdsClient: Client postUri: string allow: ThreadgateAllowUISetting[] }) { - return upsertThreadgate({agent, postUri}, async prev => { + return upsertThreadgate({pdsClient, postUri}, async prev => { if (prev) { return { ...prev, @@ -225,7 +220,7 @@ export async function updateThreadgateAllow({ } export function useSetThreadgateAllowMutation() { - const agent = useAgent() + const pdsClient = usePdsClient() const queryClient = useQueryClient() const getPost = useGetPost() const updatePostThreadThreadgate = useUpdatePostThreadThreadgateQueryCache() @@ -238,7 +233,7 @@ export function useSetThreadgateAllowMutation() { postUri: string allow: ThreadgateAllowUISetting[] }) => { - return upsertThreadgate({agent, postUri}, async prev => { + return upsertThreadgate({pdsClient, postUri}, async prev => { if (prev) { return { ...prev, @@ -290,7 +285,7 @@ export function useSetThreadgateAllowMutation() { } export function useToggleReplyVisibilityMutation() { - const agent = useAgent() + const pdsClient = usePdsClient() const queryClient = useQueryClient() const hiddenReplies = useThreadgateHiddenReplyUrisAPI() @@ -310,7 +305,7 @@ export function useToggleReplyVisibilityMutation() { hiddenReplies.removeHiddenReplyUri(replyUri) } - await upsertThreadgate({agent, postUri}, async prev => { + await upsertThreadgate({pdsClient, postUri}, async prev => { if (prev) { if (action === 'hide') { return mergeThreadgateRecords(prev, { @@ -363,9 +358,9 @@ export class InvalidInteractionSettingsError extends Error { } export function validateThreadgateRecordOrThrow( - record: AppBskyFeedThreadgate.Record, + record: app.bsky.feed.threadgate.Main, ) { - const result = AppBskyFeedThreadgate.validateRecord(record) + const result = bsky.safeParse(app.bsky.feed.threadgate, record) if (result.success) { if ((result.value.hiddenReplies?.length ?? 0) > MAX_HIDDEN_REPLIES) { diff --git a/src/state/queries/threadgate/util.ts b/src/state/queries/threadgate/util.ts index 807afef76f..275611df1c 100644 --- a/src/state/queries/threadgate/util.ts +++ b/src/state/queries/threadgate/util.ts @@ -1,26 +1,34 @@ -import {type AppBskyFeedDefs, AppBskyFeedThreadgate} from '@atproto/api' +import {type AppBskyFeedDefs} from '@atproto/api' +import {type AtUriString, toDatetimeString} from '@atproto/syntax' import {type ThreadgateAllowUISetting} from '#/state/queries/threadgate/types' +import {app} from '#/lexicons' import * as bsky from '#/types/bsky' +/* + * Threadgate VIEWS stay on the legacy client types: they are read-only inputs + * from the appview, and branding them here would ripple through every post + * component. Only the threadgate RECORD is migrated, because it is written + * through `com.atproto.repo.putRecord`, whose body is typed as a lex `LexMap`. + */ export function threadgateViewToAllowUISetting( threadgateView: AppBskyFeedDefs.ThreadgateView | undefined, ): ThreadgateAllowUISetting[] { // Validate the record for clarity, since backwards compat code is a little confusing const threadgate = threadgateView && - bsky.validate(threadgateView.record, AppBskyFeedThreadgate.validateRecord) + bsky.matches(app.bsky.feed.threadgate, threadgateView.record) ? threadgateView.record : undefined return threadgateRecordToAllowUISetting(threadgate) } /** - * Converts a full {@link AppBskyFeedThreadgate.Record} to a list of + * Converts a full {@link app.bsky.feed.threadgate.Main} to a list of * {@link ThreadgateAllowUISetting}, for use by app UI. */ export function threadgateRecordToAllowUISetting( - threadgate: AppBskyFeedThreadgate.Record | undefined, + threadgate: app.bsky.feed.threadgate.Main | undefined, ): ThreadgateAllowUISetting[] { /* * If `threadgate` doesn't exist (default), or if `threadgate.allow === undefined`, it means @@ -40,13 +48,13 @@ export function threadgateRecordToAllowUISetting( const settings: ThreadgateAllowUISetting[] = threadgate.allow .map(allow => { let setting: ThreadgateAllowUISetting | undefined - if (AppBskyFeedThreadgate.isMentionRule(allow)) { + if (bsky.isType(app.bsky.feed.threadgate.mentionRule, allow)) { setting = {type: 'mention'} - } else if (AppBskyFeedThreadgate.isFollowingRule(allow)) { + } else if (bsky.isType(app.bsky.feed.threadgate.followingRule, allow)) { setting = {type: 'following'} - } else if (AppBskyFeedThreadgate.isListRule(allow)) { + } else if (bsky.isType(app.bsky.feed.threadgate.listRule, allow)) { setting = {type: 'list', list: allow.list} - } else if (AppBskyFeedThreadgate.isFollowerRule(allow)) { + } else if (bsky.isType(app.bsky.feed.threadgate.followerRule, allow)) { setting = {type: 'followers'} } return setting @@ -57,7 +65,7 @@ export function threadgateRecordToAllowUISetting( /** * Converts an array of {@link ThreadgateAllowUISetting} to the `allow` prop on - * {@link AppBskyFeedThreadgate.Record}. + * {@link app.bsky.feed.threadgate.Main}. * * If the `allow` property on the record is undefined, we infer that to mean * that everyone can reply. If it's an empty array, we infer that to mean that @@ -65,12 +73,12 @@ export function threadgateRecordToAllowUISetting( */ export function threadgateAllowUISettingToAllowRecordValue( threadgate: ThreadgateAllowUISetting[], -): AppBskyFeedThreadgate.Record['allow'] { +): app.bsky.feed.threadgate.Main['allow'] { if (threadgate.find(v => v.type === 'everybody')) { return undefined } - let allow: Exclude = [] + let allow: Exclude = [] if (!threadgate.find(v => v.type === 'nobody')) { for (const rule of threadgate) { @@ -83,7 +91,7 @@ export function threadgateAllowUISettingToAllowRecordValue( } else if (rule.type === 'list') { allow.push({ $type: 'app.bsky.feed.threadgate#listRule', - list: rule.list, + list: rule.list as AtUriString, }) } } @@ -93,18 +101,20 @@ export function threadgateAllowUISettingToAllowRecordValue( } /** - * Merges two {@link AppBskyFeedThreadgate.Record} objects, combining their + * Merges two {@link app.bsky.feed.threadgate.Main} objects, combining their * `allow` and `hiddenReplies` arrays and de-deduplicating them. * * Note: `allow` can be undefined here, be sure you don't accidentally set it * to an empty array. See other comments in this file. */ export function mergeThreadgateRecords( - prev: AppBskyFeedThreadgate.Record, - next: Partial, -): AppBskyFeedThreadgate.Record { + prev: app.bsky.feed.threadgate.Main, + next: Omit, 'hiddenReplies'> & { + hiddenReplies?: string[] + }, +): app.bsky.feed.threadgate.Main { // can be undefined if everyone can reply! - const allow: AppBskyFeedThreadgate.Record['allow'] | undefined = + const allow: app.bsky.feed.threadgate.Main['allow'] | undefined = prev.allow || next.allow ? [...(prev.allow || []), ...(next.allow || [])].filter( (v, i, a) => a.findIndex(t => t.$type === v.$type) === i, @@ -112,7 +122,7 @@ export function mergeThreadgateRecords( : undefined const hiddenReplies = Array.from( new Set([...(prev.hiddenReplies || []), ...(next.hiddenReplies || [])]), - ) + ) as AtUriString[] return createThreadgateRecord({ post: prev.post, @@ -122,21 +132,28 @@ export function mergeThreadgateRecords( } /** - * Create a new {@link AppBskyFeedThreadgate.Record} object with the given - * properties. + * Create a new {@link app.bsky.feed.threadgate.Main} object with the given + * properties. `post` is accepted as a plain string (callers hold raw AT-URIs) + * and asserted to the branded `AtUriString` here. */ export function createThreadgateRecord( - threadgate: Partial, -): AppBskyFeedThreadgate.Record { + threadgate: Omit< + Partial, + 'post' | 'hiddenReplies' + > & { + post?: string + hiddenReplies?: string[] + }, +): app.bsky.feed.threadgate.Main { if (!threadgate.post) { throw new Error('Cannot create a threadgate record without a post URI') } return { $type: 'app.bsky.feed.threadgate', - post: threadgate.post, - createdAt: new Date().toISOString(), + post: threadgate.post as AtUriString, + createdAt: toDatetimeString(new Date()), allow: threadgate.allow, // can be undefined! - hiddenReplies: threadgate.hiddenReplies || [], + hiddenReplies: (threadgate.hiddenReplies || []) as AtUriString[], } } diff --git a/src/view/com/composer/state/composer.ts b/src/view/com/composer/state/composer.ts index 3d2aa45992..34e6b69508 100644 --- a/src/view/com/composer/state/composer.ts +++ b/src/view/com/composer/state/composer.ts @@ -1,9 +1,6 @@ import {type ImagePickerAsset} from 'expo-image-picker' -import { - type AppBskyActorDefs, - type AppBskyDraftDefs, - type AppBskyFeedPostgate, -} from '@atproto/api' +import {type AppBskyActorDefs, type AppBskyDraftDefs} from '@atproto/api' +import {type AtUriString, toDatetimeString} from '@atproto/syntax' import {RichText} from '@bsky.app/sdk/richtext' import {nanoid} from 'nanoid/non-secure' @@ -103,7 +100,7 @@ export type PostAction = export type ThreadDraft = { posts: PostDraft[] - postgate: AppBskyFeedPostgate.Record + postgate: app.bsky.feed.postgate.Main threadgate: ThreadgateAllowUISetting[] } @@ -122,7 +119,7 @@ export type ComposerState = { } export type ComposerAction = - | {type: 'update_postgate'; postgate: AppBskyFeedPostgate.Record} + | {type: 'update_postgate'; postgate: app.bsky.feed.postgate.Main} | {type: 'update_threadgate'; threadgate: ThreadgateAllowUISetting[]} | { type: 'update_post' @@ -319,13 +316,18 @@ export function composerReducer( posts, postgate: createPostgateRecord({ post: '', - embeddingRules: postgateEmbeddingRules, + /* + * Draft records are still typed against the legacy client, so the + * stored rules arrive unbranded. Wave B migrates the draft types. + */ + embeddingRules: + postgateEmbeddingRules as app.bsky.feed.postgate.Main['embeddingRules'], }), threadgate: threadgateRecordToAllowUISetting({ $type: 'app.bsky.feed.threadgate', - post: '', - createdAt: new Date().toString(), - allow: threadgateAllow, + post: '' as AtUriString, + createdAt: toDatetimeString(new Date()), + allow: threadgateAllow as app.bsky.feed.threadgate.Main['allow'], }), }, } @@ -744,13 +746,19 @@ export function createComposerState({ ], postgate: createPostgateRecord({ post: '', - embeddingRules: initInteractionSettings?.postgateEmbeddingRules || [], + /* + * Preferences are still typed against the legacy client, so the stored + * rules arrive unbranded. Wave B migrates `getPreferences`. + */ + embeddingRules: (initInteractionSettings?.postgateEmbeddingRules || + []) as app.bsky.feed.postgate.Main['embeddingRules'], }), threadgate: threadgateRecordToAllowUISetting({ $type: 'app.bsky.feed.threadgate', - post: '', - createdAt: new Date().toString(), - allow: initInteractionSettings?.threadgateAllowRules, + post: '' as AtUriString, + createdAt: toDatetimeString(new Date()), + allow: + initInteractionSettings?.threadgateAllowRules as app.bsky.feed.threadgate.Main['allow'], }), }, } diff --git a/src/view/com/composer/threadgate/ThreadgateBtn.tsx b/src/view/com/composer/threadgate/ThreadgateBtn.tsx index 0f342b41c4..2602b9bc45 100644 --- a/src/view/com/composer/threadgate/ThreadgateBtn.tsx +++ b/src/view/com/composer/threadgate/ThreadgateBtn.tsx @@ -1,7 +1,7 @@ import {useEffect, useMemo, useState} from 'react' import {Keyboard, type StyleProp, type ViewStyle} from 'react-native' import {type AnimatedStyle} from 'react-native-reanimated' -import {type AppBskyFeedPostgate} from '@atproto/api' +import {type AtUriString, toDatetimeString} from '@atproto/syntax' import {Trans, useLingui} from '@lingui/react/macro' import deepEqual from 'fast-deep-equal' @@ -24,6 +24,7 @@ import {Group3_Stroke2_Corner0_Rounded as GroupIcon} from '#/components/icons/Gr import * as Tooltip from '#/components/Tooltip' import {useAnalytics} from '#/analytics' import {IS_NATIVE} from '#/env' +import {type app} from '#/lexicons' import {useThreadgateNudged} from '#/storage/hooks/threadgate-nudged' export function ThreadgateBtn({ @@ -32,8 +33,8 @@ export function ThreadgateBtn({ threadgateAllowUISettings, onChangeThreadgateAllowUISettings, }: { - postgate: AppBskyFeedPostgate.Record - onChangePostgate: (v: AppBskyFeedPostgate.Record) => void + postgate: app.bsky.feed.postgate.Main + onChangePostgate: (v: app.bsky.feed.postgate.Main) => void threadgateAllowUISettings: ThreadgateAllowUISetting[] onChangeThreadgateAllowUISettings: (v: ThreadgateAllowUISetting[]) => void @@ -83,14 +84,20 @@ export function ThreadgateBtn({ const prefThreadgateAllowUISettings = threadgateRecordToAllowUISetting({ $type: 'app.bsky.feed.threadgate', - post: '', - createdAt: new Date().toISOString(), - allow: preferences?.postInteractionSettings.threadgateAllowRules, + post: '' as AtUriString, + createdAt: toDatetimeString(new Date()), + /* + * Preferences are still typed against the legacy client, so the stored + * rules arrive unbranded. Wave B migrates `getPreferences`. + */ + allow: preferences?.postInteractionSettings + .threadgateAllowRules as app.bsky.feed.threadgate.Main['allow'], }) const prefPostgate = createPostgateRecord({ post: '', - embeddingRules: - preferences?.postInteractionSettings?.postgateEmbeddingRules || [], + embeddingRules: (preferences?.postInteractionSettings + ?.postgateEmbeddingRules || + []) as app.bsky.feed.postgate.Main['embeddingRules'], }) const isDirty = useMemo(() => { From 75e4180800757c6ef9ab295477ffbdfce1ad9973 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 13 Aug 2026 22:26:20 +0300 Subject: [PATCH 20/32] [SDK] Migrate the repo writes cluster (#11379) Co-authored-by: Claude Fable 5 --- .../contacts/screens/ViewMatches.tsx | 9 +- .../lists/CreateListFromStarterPackDialog.tsx | 43 ++--- src/features/liveNow/index.tsx | 54 +++--- src/lib/generate-starterpack.ts | 94 +++++------ src/screens/Onboarding/StepFinished/index.tsx | 14 +- .../StepSuggestedAccounts/index.tsx | 10 +- .../StarterPackCard.tsx | 10 +- src/screens/Onboarding/util.ts | 51 +++--- src/screens/Profile/components/GermButton.tsx | 51 +++--- src/screens/Settings/FindContactsSettings.tsx | 9 +- src/screens/StarterPack/StarterPackScreen.tsx | 10 +- src/state/birthdate.ts | 5 +- src/state/queries/activity-subscriptions.ts | 36 ++-- src/state/queries/list-memberships.ts | 37 ++-- src/state/queries/list.ts | 153 +++++++++-------- .../queries/messages/actor-declaration.ts | 22 ++- .../queries/messages/restrictChatSettings.ts | 22 +-- src/state/queries/profile.ts | 27 +-- src/state/queries/starter-packs.ts | 158 ++++++++++-------- .../useVerificationCreateMutation.tsx | 35 ++-- .../useVerificationsRemoveMutation.tsx | 25 +-- src/state/session/create-account.ts | 12 +- 22 files changed, 488 insertions(+), 399 deletions(-) diff --git a/src/components/contacts/screens/ViewMatches.tsx b/src/components/contacts/screens/ViewMatches.tsx index dab3a79c6b..0ef4711919 100644 --- a/src/components/contacts/screens/ViewMatches.tsx +++ b/src/components/contacts/screens/ViewMatches.tsx @@ -21,7 +21,7 @@ import { optimisticRemoveMatch, useMatchesPassthroughQuery, } from '#/state/queries/find-contacts' -import {useAgent, useAppviewClient, useSession} from '#/state/session' +import {useAppviewClient, usePdsClient, useSession} from '#/state/session' import {List, type ListMethods} from '#/view/com/util/List' import {UserAvatar} from '#/view/com/util/UserAvatar' import {OnboardingPosition} from '#/screens/Onboarding/Layout' @@ -91,7 +91,7 @@ export function ViewMatches({ const gutter = useGutters([0, 'wide']) const moderationOpts = useModerationOpts() const queryClient = useQueryClient() - const agent = useAgent() + const pdsClient = usePdsClient() const client = useAppviewClient() const insets = useSafeAreaInsets() const listRef = useRef(null) @@ -127,7 +127,10 @@ export function ViewMatches({ }) } - const uris = await wait(500, bulkWriteFollows(agent, followableDids)) + const uris = await wait( + 500, + bulkWriteFollows(pdsClient, client, followableDids), + ) for (const did of followableDids) { const uri = uris.get(did) diff --git a/src/components/dialogs/lists/CreateListFromStarterPackDialog.tsx b/src/components/dialogs/lists/CreateListFromStarterPackDialog.tsx index 3aabbec9ff..ceb07fb0e0 100644 --- a/src/components/dialogs/lists/CreateListFromStarterPackDialog.tsx +++ b/src/components/dialogs/lists/CreateListFromStarterPackDialog.tsx @@ -1,13 +1,14 @@ import {View} from 'react-native' -import { - type $Typed, - type AppBskyGraphDefs, - type AppBskyGraphListitem, - type AppBskyGraphStarterpack, - AtUri, - type ComAtprotoRepoApplyWrites, -} from '@atproto/api' +import {type AppBskyGraphDefs, type AppBskyGraphStarterpack} from '@atproto/api' import {TID} from '@atproto/common-web' +import {type $Typed} from '@atproto/lex' +import { + type AtIdentifierString, + AtUri, + type AtUriString, + type DidString, + toDatetimeString, +} from '@atproto/syntax' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' @@ -20,7 +21,7 @@ import {wait} from '#/lib/async/wait' import {type NavigationProp} from '#/lib/routes/types' import {logger} from '#/logger' import {getAllListMembers} from '#/state/queries/list-members' -import {useAgent, useAppviewClient, useSession} from '#/state/session' +import {useAppviewClient, usePdsClient, useSession} from '#/state/session' import {atoms as a, platform, useTheme, web} from '#/alf' import {Admonition} from '#/components/Admonition' import {Button, ButtonText} from '#/components/Button' @@ -29,6 +30,7 @@ import {Loader} from '#/components/Loader' import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' +import {app, com} from '#/lexicons' import {CreateOrEditListDialog} from './CreateOrEditListDialog' export function CreateListFromStarterPackDialog({ @@ -40,8 +42,8 @@ export function CreateListFromStarterPackDialog({ }) { const {_} = useLingui() const t = useTheme() - const agent = useAgent() const appviewClient = useAppviewClient() + const pdsClient = usePdsClient() const ax = useAnalytics() const {currentAccount} = useSession() const navigation = useNavigation() @@ -80,13 +82,14 @@ export function CreateListFromStarterPackDialog({ ) if (items.length > 0) { - const listitemWrites: $Typed[] = + const listitemWrites: $Typed[] = items.map(item => { - const listitemRecord: $Typed = { + const listitemRecord: $Typed = { $type: 'app.bsky.graph.listitem', - subject: item.subject.did, - list: listUri, - createdAt: new Date().toISOString(), + // the list view is still legacy-typed, so its strings are unbranded + subject: item.subject.did as DidString, + list: listUri as AtUriString, + createdAt: toDatetimeString(new Date()), } return { $type: 'com.atproto.repo.applyWrites#create', @@ -98,8 +101,8 @@ export function CreateListFromStarterPackDialog({ const chunks = chunk(listitemWrites, 50) for (const c of chunks) { - await agent.com.atproto.repo.applyWrites({ - repo: currentAccount.did, + await pdsClient.call(com.atproto.repo.applyWrites, { + repo: currentAccount.did as AtIdentifierString, writes: c, }) } @@ -107,10 +110,10 @@ export function CreateListFromStarterPackDialog({ await until( 5, 1e3, - (res: {data: {items: unknown[]}}) => res.data.items.length > 0, + (res: {items: unknown[]}) => res.items.length > 0, () => - agent.app.bsky.graph.getList({ - list: listUri, + appviewClient.call(app.bsky.graph.getList, { + list: listUri as AtUriString, limit: 1, }), ) diff --git a/src/features/liveNow/index.tsx b/src/features/liveNow/index.tsx index 23f33a5ffc..3b60b688bb 100644 --- a/src/features/liveNow/index.tsx +++ b/src/features/liveNow/index.tsx @@ -2,13 +2,12 @@ import {useMemo} from 'react' import { type $Typed, type AppBskyActorDefs, - type AppBskyActorStatus, AppBskyEmbedExternal, - AtUri, - ComAtprotoRepoPutRecord, moderateStatus, } from '@atproto/api' import {retry} from '@atproto/common-web' +import {type l} from '@atproto/lex' +import {type AtIdentifierString, AtUri, toDatetimeString} from '@atproto/syntax' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' @@ -17,18 +16,20 @@ import {isAfter, parseISO} from 'date-fns' import {uploadBlob} from '#/lib/api' import {imageToThumb} from '#/lib/api/resolve' import {getLinkMeta, type LinkMeta} from '#/lib/link-meta/link-meta' +import {matchXrpcError} from '#/lib/xrpc-error' import {useAppConfig} from '#/state/appConfig' import { updateProfileShadow, useMaybeProfileShadow, } from '#/state/cache/profile-shadow' import {useModerationOpts} from '#/state/preferences/moderation-opts' -import {useAgent, useSession} from '#/state/session' +import {useAgent, usePdsClient, useSession} from '#/state/session' import {useTickEveryMinute} from '#/state/shell' import {useDialogContext} from '#/components/Dialog' import * as Toast from '#/components/Toast' import {useAnalytics} from '#/analytics' import {getLiveNowHost, getLiveServiceNames} from '#/features/liveNow/utils' +import {app, com} from '#/lexicons' import type * as bsky from '#/types/bsky' export * from '#/features/liveNow/utils' @@ -224,6 +225,7 @@ export function useUpsertLiveStatusMutation( const ax = useAnalytics() const {currentAccount} = useSession() const agent = useAgent() + const pdsClient = usePdsClient() const queryClient = useQueryClient() const control = useDialogContext() const {_} = useLingui() @@ -232,10 +234,10 @@ export function useUpsertLiveStatusMutation( mutationFn: async () => { if (!currentAccount) throw new Error('Not logged in') - let embed: $Typed | undefined + let embed: $Typed | undefined if (linkMeta) { - let thumb + let thumb: l.BlobRef | undefined if (linkMeta.image) { try { @@ -246,7 +248,11 @@ export function useUpsertLiveStatusMutation( img.source.path, img.source.mime, ) - thumb = blob.data.blob + /* + * `uploadBlob` still returns the legacy `BlobRef` class + * instance; it moves to the client with the blob pipeline. + */ + thumb = blob.data.blob as unknown as l.BlobRef } } catch (e: any) { ax.logger.error(`Failed to upload thumbnail for live status`, { @@ -263,7 +269,8 @@ export function useUpsertLiveStatusMutation( $type: 'app.bsky.embed.external#external', title: linkMeta.title ?? '', description: linkMeta.description ?? '', - uri: linkMeta.url, + // `getLinkMeta` returns a plain url string + uri: linkMeta.url as l.UriString, thumb, }, } @@ -271,32 +278,41 @@ export function useUpsertLiveStatusMutation( const record = { $type: 'app.bsky.actor.status', - createdAt: createdAt ?? new Date().toISOString(), + createdAt: toDatetimeString( + createdAt ? new Date(createdAt) : new Date(), + ), status: 'app.bsky.actor.status#live', durationMinutes: duration, embed, - } satisfies AppBskyActorStatus.Record + } satisfies app.bsky.actor.status.Main const upsert = async () => { - const repo = currentAccount.did + // the session account is still legacy-typed, so its did is unbranded + const repo = currentAccount.did as AtIdentifierString const collection = 'app.bsky.actor.status' - const existing = await agent.com.atproto.repo - .getRecord({repo, collection, rkey: 'self'}) + const existing = await pdsClient + .call(com.atproto.repo.getRecord, {repo, collection, rkey: 'self'}) .catch(_e => undefined) - await agent.com.atproto.repo.putRecord({ + /* + * Stays on the raw `putRecord`, not `pdsClient.put`: the lexicon lets + * `swapRecord` be null (meaning "must not already exist"), while the + * record-helper option type is `string | undefined`. + */ + await pdsClient.call(com.atproto.repo.putRecord, { repo, collection, rkey: 'self', record, - swapRecord: existing?.data.cid || null, + swapRecord: existing?.cid || null, }) } await retry(upsert, { maxRetries: 5, - retryable: e => e instanceof ComAtprotoRepoPutRecord.InvalidSwapError, + retryable: e => + matchXrpcError(e, com.atproto.repo.putRecord) === 'InvalidSwap', }) return { @@ -353,7 +369,7 @@ export function useUpsertLiveStatusMutation( export function useRemoveLiveStatusMutation() { const ax = useAnalytics() const {currentAccount} = useSession() - const agent = useAgent() + const pdsClient = usePdsClient() const queryClient = useQueryClient() const control = useDialogContext() const {_} = useLingui() @@ -362,8 +378,8 @@ export function useRemoveLiveStatusMutation() { mutationFn: async () => { if (!currentAccount) throw new Error('Not logged in') - await agent.app.bsky.actor.status.delete({ - repo: currentAccount.did, + await pdsClient.delete(app.bsky.actor.status, { + repo: currentAccount.did as AtIdentifierString, rkey: 'self', }) }, diff --git a/src/lib/generate-starterpack.ts b/src/lib/generate-starterpack.ts index a53e95a6a3..a786fb47ad 100644 --- a/src/lib/generate-starterpack.ts +++ b/src/lib/generate-starterpack.ts @@ -1,11 +1,5 @@ -import { - type $Typed, - type AppBskyActorDefs, - type AppBskyGraphGetStarterPack, - type AtpAgent, - type ComAtprotoRepoApplyWrites, - type Facet, -} from '@atproto/api' +import {type Client} from '@atproto/lex' +import {type AtUriString, toDatetimeString} from '@atproto/syntax' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {useMutation} from '@tanstack/react-query' @@ -14,7 +8,8 @@ import {until} from '#/lib/async/until' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeHandle} from '#/lib/strings/handles' import {enforceLen} from '#/lib/strings/helpers' -import {useAgent} from '#/state/session' +import {useAppviewClient, usePdsClient} from '#/state/session' +import {app, com} from '#/lexicons' import type * as bsky from '#/types/bsky' export const createStarterPackList = async ({ @@ -22,30 +17,27 @@ export const createStarterPackList = async ({ description, descriptionFacets, profiles, - agent, + client, }: { name: string description?: string - descriptionFacets?: Facet[] + descriptionFacets?: app.bsky.richtext.facet.Main[] profiles: bsky.profile.AnyProfileView[] - agent: AtpAgent + client: Client }): Promise<{uri: string; cid: string}> => { if (profiles.length === 0) throw new Error('No profiles given') - const list = await agent.app.bsky.graph.list.create( - {repo: agent.session!.did}, - { - name, - description, - descriptionFacets, - avatar: undefined, - createdAt: new Date().toISOString(), - purpose: 'app.bsky.graph.defs#referencelist', - }, - ) + const list = await client.create(app.bsky.graph.list, { + name, + description, + descriptionFacets, + avatar: undefined, + createdAt: toDatetimeString(new Date()), + purpose: 'app.bsky.graph.defs#referencelist', + }) if (!list) throw new Error('List creation failed') - await agent.com.atproto.repo.applyWrites({ - repo: agent.session!.did, + await client.call(com.atproto.repo.applyWrites, { + repo: client.assertDid, writes: profiles.map(p => createListItem({did: p.did, listUri: list.uri})), }) @@ -60,28 +52,27 @@ export function useGenerateStarterPackMutation({ onError: (e: Error) => void }) { const {_} = useLingui() - const agent = useAgent() + const appviewClient = useAppviewClient() + const pdsClient = usePdsClient() return useMutation<{uri: string; cid: string}, Error, void>({ mutationFn: async () => { - let profile: AppBskyActorDefs.ProfileViewDetailed | undefined - let profiles: AppBskyActorDefs.ProfileView[] | undefined + let profile: app.bsky.actor.defs.ProfileViewDetailed | undefined + let profiles: app.bsky.actor.defs.ProfileView[] | undefined await Promise.all([ (async () => { - profile = ( - await agent.app.bsky.actor.getProfile({ - actor: agent.session!.did, - }) - ).data + profile = await appviewClient.call(app.bsky.actor.getProfile, { + actor: pdsClient.assertDid, + }) })(), (async () => { profiles = ( - await agent.app.bsky.actor.searchActors({ + await appviewClient.call(app.bsky.actor.searchActors, { q: encodeURIComponent('*'), limit: 49, }) - ).data.actors.filter(p => p.viewer?.following) + ).actors.filter(p => p.viewer?.following) })(), ]) @@ -106,23 +97,19 @@ export function useGenerateStarterPackMutation({ const list = await createStarterPackList({ name: starterPackName, profiles, - agent, + client: pdsClient, }) - return await agent.app.bsky.graph.starterpack.create( - { - repo: agent.session!.did, - }, - { - name: starterPackName, - list: list.uri, - createdAt: new Date().toISOString(), - }, - ) + return await pdsClient.create(app.bsky.graph.starterpack, { + name: starterPackName, + // `create` returns a plain string uri + list: list.uri as AtUriString, + createdAt: toDatetimeString(new Date()), + }) }, onSuccess: async data => { - await whenAppViewReady(agent, data.uri, v => { - return typeof v?.data.starterPack.uri === 'string' + await whenAppViewReady(appviewClient, data.uri, v => { + return typeof v?.starterPack.uri === 'string' }) onSuccess(data) }, @@ -138,7 +125,7 @@ function createListItem({ }: { did: string listUri: string -}): $Typed { +}): com.atproto.repo.applyWrites.$InputBody['writes'][number] { return { $type: 'com.atproto.repo.applyWrites#create', collection: 'app.bsky.graph.listitem', @@ -152,14 +139,17 @@ function createListItem({ } async function whenAppViewReady( - agent: AtpAgent, + client: Client, uri: string, - fn: (res?: AppBskyGraphGetStarterPack.Response) => boolean, + fn: (res?: app.bsky.graph.getStarterPack.$OutputBody) => boolean, ) { await until( 5, // 5 tries 1e3, // 1s delay between tries fn, - () => agent.app.bsky.graph.getStarterPack({starterPack: uri}), + () => + client.call(app.bsky.graph.getStarterPack, { + starterPack: uri as AtUriString, + }), ) } diff --git a/src/screens/Onboarding/StepFinished/index.tsx b/src/screens/Onboarding/StepFinished/index.tsx index 05f4ef8626..6f1dea3a4f 100644 --- a/src/screens/Onboarding/StepFinished/index.tsx +++ b/src/screens/Onboarding/StepFinished/index.tsx @@ -8,6 +8,7 @@ import { type Un$Typed, } from '@atproto/api' import {TID} from '@atproto/common-web' +import {type AtUriString} from '@atproto/syntax' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' @@ -26,7 +27,7 @@ import {useSetHasCheckedForStarterPack} from '#/state/preferences/used-starter-p import {getAllListMembers} from '#/state/queries/list-members' import {preferencesQueryKey} from '#/state/queries/preferences' import {RQKEY as profileRQKey} from '#/state/queries/profile' -import {useAgent, useAppviewClient} from '#/state/session' +import {useAgent, useAppviewClient, usePdsClient} from '#/state/session' import {useOnboardingDispatch} from '#/state/shell' import { useActiveStarterPack, @@ -59,6 +60,7 @@ export function StepFinished() { const queryClient = useQueryClient() const agent = useAgent() const appviewClient = useAppviewClient() + const pdsClient = usePdsClient() const requestNotificationsPermission = useRequestNotificationsPermission() const activeStarterPack = useActiveStarterPack() const setActiveStarterPack = useSetActiveStarterPack() @@ -102,10 +104,15 @@ export function StepFinished() { await Promise.all([ bulkWriteFollows( - agent, + pdsClient, + appviewClient, [BSKY_APP_ACCOUNT_DID, ...(listItems?.map(i => i.subject.did) ?? [])], starterPack - ? {uri: starterPack.uri, cid: starterPack.cid} + ? // the starter pack view is still legacy-typed + { + uri: starterPack.uri as AtUriString, + cid: starterPack.cid, + } : undefined, ), (async () => { @@ -236,6 +243,7 @@ export function StepFinished() { queryClient, agent, appviewClient, + pdsClient, dispatch, onboardDispatch, activeStarterPack, diff --git a/src/screens/Onboarding/StepSuggestedAccounts/index.tsx b/src/screens/Onboarding/StepSuggestedAccounts/index.tsx index 514e892895..f43d06cdd4 100644 --- a/src/screens/Onboarding/StepSuggestedAccounts/index.tsx +++ b/src/screens/Onboarding/StepSuggestedAccounts/index.tsx @@ -14,7 +14,7 @@ import {logger} from '#/logger' import {updateProfileShadow} from '#/state/cache/profile-shadow' import {useLanguagePrefs} from '#/state/preferences' import {useModerationOpts} from '#/state/preferences/moderation-opts' -import {useAgent, useSession} from '#/state/session' +import {useAppviewClient, usePdsClient, useSession} from '#/state/session' import { OnboardingControls, OnboardingPosition, @@ -42,7 +42,8 @@ export function StepSuggestedAccounts() { const t = useTheme() const {gtMobile} = useBreakpoints() const moderationOpts = useModerationOpts() - const agent = useAgent() + const appviewClient = useAppviewClient() + const pdsClient = usePdsClient() const {currentAccount} = useSession() const queryClient = useQueryClient() @@ -119,7 +120,10 @@ export function StepSuggestedAccounts() { followingUri: 'pending', }) } - const uris = await wait(1e3, bulkWriteFollows(agent, followableDids)) + const uris = await wait( + 1e3, + bulkWriteFollows(pdsClient, appviewClient, followableDids), + ) for (const did of followableDids) { const uri = uris.get(did) updateProfileShadow(queryClient, did, { diff --git a/src/screens/Onboarding/StepSuggestedStarterpacks/StarterPackCard.tsx b/src/screens/Onboarding/StepSuggestedStarterpacks/StarterPackCard.tsx index fa296dfbd2..dc208917f5 100644 --- a/src/screens/Onboarding/StepSuggestedStarterpacks/StarterPackCard.tsx +++ b/src/screens/Onboarding/StepSuggestedStarterpacks/StarterPackCard.tsx @@ -1,6 +1,7 @@ import {useState} from 'react' import {View} from 'react-native' import {type AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api' +import {type AtUriString} from '@atproto/syntax' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' @@ -11,7 +12,7 @@ import {isBlockedOrBlocking, isMuted} from '#/lib/moderation/blocked-and-muted' import {logger} from '#/logger' import {updateProfileShadow} from '#/state/cache/profile-shadow' import {getAllListMembers} from '#/state/queries/list-members' -import {useAgent, useAppviewClient, useSession} from '#/state/session' +import {useAppviewClient, usePdsClient, useSession} from '#/state/session' import {bulkWriteFollows} from '#/screens/Onboarding/util' import {AvatarStack} from '#/screens/Search/components/StarterPackCard' import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' @@ -35,8 +36,8 @@ export function StarterPackCard({ const ax = useAnalytics() const {currentAccount} = useSession() const {gtPhone} = useBreakpoints() - const agent = useAgent() const appviewClient = useAppviewClient() + const pdsClient = usePdsClient() const queryClient = useQueryClient() const record = view.record const [isProcessing, setIsProcessing] = useState(false) @@ -74,8 +75,9 @@ export function StarterPackCard({ let followUris: Map try { - followUris = await bulkWriteFollows(agent, dids, { - uri: view.uri, + followUris = await bulkWriteFollows(pdsClient, appviewClient, dids, { + // the starter pack view is still legacy-typed + uri: view.uri as AtUriString, cid: view.cid, }) } catch (e) { diff --git a/src/screens/Onboarding/util.ts b/src/screens/Onboarding/util.ts index f4ecde3c5d..af8a40f092 100644 --- a/src/screens/Onboarding/util.ts +++ b/src/screens/Onboarding/util.ts @@ -1,37 +1,34 @@ -import { - type $Typed, - type AppBskyGraphFollow, - type AppBskyGraphGetFollows, - type AtpAgent, - type ComAtprotoRepoApplyWrites, - type ComAtprotoRepoStrongRef, -} from '@atproto/api' import {TID} from '@atproto/common-web' +import {type $Typed, type Client} from '@atproto/lex' +import { + type AtIdentifierString, + type DidString, + toDatetimeString, +} from '@atproto/syntax' import chunk from 'lodash.chunk' import {until} from '#/lib/async/until' +import {app, com} from '#/lexicons' export async function bulkWriteFollows( - agent: AtpAgent, + pdsClient: Client, + appviewClient: Client, dids: string[], - via?: ComAtprotoRepoStrongRef.Main, + via?: com.atproto.repo.strongRef.Main, ) { - const session = agent.session + const did = pdsClient.assertDid - if (!session) { - throw new Error(`bulkWriteFollows failed: no session`) - } - - const followRecords: $Typed[] = dids.map(did => { + const followRecords: $Typed[] = dids.map(did => { return { $type: 'app.bsky.graph.follow', - subject: did, - createdAt: new Date().toISOString(), + // callers hold plain dids read off legacy-typed views + subject: did as DidString, + createdAt: toDatetimeString(new Date()), via, } }) - const followWrites: $Typed[] = + const followWrites: $Typed[] = followRecords.map(r => ({ $type: 'com.atproto.repo.applyWrites#create', collection: 'app.bsky.graph.follow', @@ -41,35 +38,35 @@ export async function bulkWriteFollows( const chunks = chunk(followWrites, 50) for (const chunk of chunks) { - await agent.com.atproto.repo.applyWrites({ - repo: session.did, + await pdsClient.call(com.atproto.repo.applyWrites, { + repo: did, writes: chunk, }) } - await whenFollowsIndexed(agent, session.did, res => !!res.data.follows.length) + await whenFollowsIndexed(appviewClient, did, res => !!res.follows.length) const followUris = new Map() for (const r of followWrites) { followUris.set( r.value.subject as string, - `at://${session.did}/app.bsky.graph.follow/${r.rkey}`, + `at://${did}/app.bsky.graph.follow/${r.rkey}`, ) } return followUris } async function whenFollowsIndexed( - agent: AtpAgent, + appviewClient: Client, actor: string, - fn: (res: AppBskyGraphGetFollows.Response) => boolean, + fn: (res: app.bsky.graph.getFollows.$OutputBody) => boolean, ) { await until( 5, // 5 tries 1e3, // 1s delay between tries fn, () => - agent.app.bsky.graph.getFollows({ - actor, + appviewClient.call(app.bsky.graph.getFollows, { + actor: actor as AtIdentifierString, limit: 1, }), ) diff --git a/src/screens/Profile/components/GermButton.tsx b/src/screens/Profile/components/GermButton.tsx index 7e32d60bcc..5a69ec42c9 100644 --- a/src/screens/Profile/components/GermButton.tsx +++ b/src/screens/Profile/components/GermButton.tsx @@ -1,10 +1,8 @@ import {Platform, View} from 'react-native' import {Image} from 'expo-image' -import { - type AppBskyActorDefs, - type AppBskyActorGetProfile, - type AtpAgent, -} from '@atproto/api' +import {type AppBskyActorDefs} from '@atproto/api' +import {type Client} from '@atproto/lex' +import {type DidString} from '@atproto/syntax' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' @@ -13,7 +11,7 @@ import {useMutation, useQueryClient} from '@tanstack/react-query' import {until} from '#/lib/async/until' import {isNetworkError} from '#/lib/strings/errors' import {RQKEY} from '#/state/queries/profile' -import {useAgent, useSession} from '#/state/session' +import {useAppviewClient, usePdsClient, useSession} from '#/state/session' import {atoms as a, useTheme, web} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' @@ -24,6 +22,7 @@ import {Loader} from '#/components/Loader' import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' +import {app, com} from '#/lexicons' import type * as bsky from '#/types/bsky' export function GermButton({ @@ -119,25 +118,27 @@ function GermSelfButton({did}: {did: string}) { const ax = useAnalytics() const {_} = useLingui() const selfExplanationDialogControl = Dialog.useDialogControl() - const agent = useAgent() + const appviewClient = useAppviewClient() + const pdsClient = usePdsClient() const queryClient = useQueryClient() const {mutate: deleteDeclaration, isPending} = useMutation({ mutationFn: async () => { - const previousRecord = await agent.com.germnetwork.declaration - .get({ - repo: did, + const previousRecord = await pdsClient + // the profile view is still legacy-typed, so its did is unbranded + .get(com.germnetwork.declaration, { + repo: did as DidString, rkey: 'self', }) .then(res => res.value) .catch(() => null) - await agent.com.germnetwork.declaration.delete({ - repo: did, + await pdsClient.delete(com.germnetwork.declaration, { + repo: did as DidString, rkey: 'self', }) - await whenAppViewReady(agent, did, res => !res.data.associated?.germ) + await whenAppViewReady(appviewClient, did, res => !res.associated?.germ) return previousRecord }, @@ -147,14 +148,15 @@ function GermSelfButton({did}: {did: string}) { async function undo() { if (!previousRecord) return try { - await agent.com.germnetwork.declaration.put( - { - repo: did, - rkey: 'self', - }, - previousRecord, + await pdsClient.put(com.germnetwork.declaration, previousRecord, { + repo: did as DidString, + rkey: 'self', + }) + await whenAppViewReady( + appviewClient, + did, + res => !!res.associated?.germ, ) - await whenAppViewReady(agent, did, res => !!res.data.associated?.germ) await queryClient.refetchQueries({queryKey: RQKEY(did)}) Toast.show(_(msg`Germ DM reconnected`)) @@ -323,14 +325,17 @@ function platform() { } async function whenAppViewReady( - agent: AtpAgent, + appviewClient: Client, actor: string, - fn: (res: AppBskyActorGetProfile.Response) => boolean, + fn: (res: app.bsky.actor.getProfile.$OutputBody) => boolean, ) { await until( 5, // 5 tries 1e3, // 1s delay between tries fn, - () => agent.app.bsky.actor.getProfile({actor}), + () => + appviewClient.call(app.bsky.actor.getProfile, { + actor: actor as DidString, + }), ) } diff --git a/src/screens/Settings/FindContactsSettings.tsx b/src/screens/Settings/FindContactsSettings.tsx index 6104fd12b7..c8ecdf4102 100644 --- a/src/screens/Settings/FindContactsSettings.tsx +++ b/src/screens/Settings/FindContactsSettings.tsx @@ -29,7 +29,7 @@ import { useContactsMatchesQuery, useContactsSyncStatusQuery, } from '#/state/queries/find-contacts' -import {useAgent, useAppviewClient, useSession} from '#/state/session' +import {useAppviewClient, usePdsClient, useSession} from '#/state/session' import {ErrorScreen} from '#/view/com/util/error/ErrorScreen' import {List} from '#/view/com/util/List' import {atoms as a, tokens, useGutters, useTheme} from '#/alf' @@ -370,7 +370,7 @@ function StatusHeader({ }) { const {_} = useLingui() const ax = useAnalytics() - const agent = useAgent() + const pdsClient = usePdsClient() const client = useAppviewClient() const queryClient = useQueryClient() const {currentAccount} = useSession() @@ -406,7 +406,10 @@ function StatusHeader({ followCount: didsToFollow.length, }) - const uris = await wait(500, bulkWriteFollows(agent, didsToFollow)) + const uris = await wait( + 500, + bulkWriteFollows(pdsClient, client, didsToFollow), + ) for (const did of didsToFollow) { const uri = uris.get(did) diff --git a/src/screens/StarterPack/StarterPackScreen.tsx b/src/screens/StarterPack/StarterPackScreen.tsx index e7674c00b4..c79e39c190 100644 --- a/src/screens/StarterPack/StarterPackScreen.tsx +++ b/src/screens/StarterPack/StarterPackScreen.tsx @@ -7,6 +7,7 @@ import { AtUri, type ModerationOpts, } from '@atproto/api' +import {type AtUriString} from '@atproto/syntax' import {RichText as RichTextAPI} from '@bsky.app/sdk/richtext' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' @@ -37,7 +38,7 @@ import { useDeleteStarterPackMutation, useStarterPackQuery, } from '#/state/queries/starter-packs' -import {useAgent, useAppviewClient, useSession} from '#/state/session' +import {useAppviewClient, usePdsClient, useSession} from '#/state/session' import {useSetActiveStarterPack} from '#/state/shell/landing' import {useLoggedOutViewControls} from '#/state/shell/logged-out' import { @@ -311,8 +312,8 @@ function Header({ const {_} = useLingui() const t = useTheme() const {currentAccount, hasSession} = useSession() - const agent = useAgent() const appviewClient = useAppviewClient() + const pdsClient = usePdsClient() const queryClient = useQueryClient() const setActiveStarterPack = useSetActiveStarterPack() const {requestSwitchToAccount} = useLoggedOutViewControls() @@ -379,8 +380,9 @@ function Header({ let followUris: Map try { - followUris = await bulkWriteFollows(agent, dids, { - uri: starterPack.uri, + followUris = await bulkWriteFollows(pdsClient, appviewClient, dids, { + // the starter pack view is still legacy-typed + uri: starterPack.uri as AtUriString, cid: starterPack.cid, }) } catch (e) { diff --git a/src/state/birthdate.ts b/src/state/birthdate.ts index 183e51aa45..c405febe4e 100644 --- a/src/state/birthdate.ts +++ b/src/state/birthdate.ts @@ -3,7 +3,7 @@ import {useMutation, useQueryClient} from '@tanstack/react-query' import {restrictChatSettings} from '#/state/queries/messages/restrictChatSettings' import {preferencesQueryKey} from '#/state/queries/preferences' -import {useAgent, useSession} from '#/state/session' +import {useAgent, usePdsClient, useSession} from '#/state/session' import {usePatchAgeAssuranceOtherRequiredData} from '#/ageAssurance' import {isUnderAge} from '#/ageAssurance/util' import {IS_DEV} from '#/env' @@ -55,6 +55,7 @@ export function useIsBirthdateUpdateAllowed() { export function useBirthdateMutation() { const queryClient = useQueryClient() const agent = useAgent() + const pdsClient = usePdsClient() const patchOtherRequiredData = usePatchAgeAssuranceOtherRequiredData() return useMutation({ @@ -68,7 +69,7 @@ export function useBirthdateMutation() { if (isUnderAge(birthDate.toISOString(), 18)) { await restrictChatSettings({ - agent, + client: pdsClient, restrictIncoming: true, restrictGroupInvites: true, }) diff --git a/src/state/queries/activity-subscriptions.ts b/src/state/queries/activity-subscriptions.ts index e6dcbd8829..f77d1001cb 100644 --- a/src/state/queries/activity-subscriptions.ts +++ b/src/state/queries/activity-subscriptions.ts @@ -1,7 +1,5 @@ -import { - type AppBskyActorDefs, - type AppBskyNotificationDeclaration, -} from '@atproto/api' +import {type AppBskyActorDefs} from '@atproto/api' +import {type AtIdentifierString} from '@atproto/syntax' import {t} from '@lingui/core/macro' import { type InfiniteData, @@ -12,7 +10,8 @@ import { useQueryClient, } from '@tanstack/react-query' -import {useAgent, useAppviewClient, useSession} from '#/state/session' +import {isRecordNotFoundError} from '#/lib/xrpc-error' +import {useAppviewClient, usePdsClient, useSession} from '#/state/session' import * as Toast from '#/components/Toast' import {app} from '#/lexicons' @@ -36,27 +35,25 @@ export function useActivitySubscriptionsQuery() { } export function useNotificationDeclarationQuery() { - const agent = useAgent() + const client = usePdsClient() const {currentAccount} = useSession() return useQuery({ queryKey: RQKEY_getNotificationDeclaration, queryFn: async () => { try { - const response = await agent.app.bsky.notification.declaration.get({ - repo: currentAccount!.did, + const response = await client.get(app.bsky.notification.declaration, { + // the session account is still legacy-typed, so its did is unbranded + repo: currentAccount!.did as AtIdentifierString, rkey: 'self', }) return response } catch (err) { - if ( - err instanceof Error && - err.message.startsWith('Could not locate record') - ) { + if (isRecordNotFoundError(err)) { return { value: { $type: 'app.bsky.notification.declaration', allowSubscriptions: 'followers', - } satisfies AppBskyNotificationDeclaration.Record, + } satisfies app.bsky.notification.declaration.Main, } } else { throw err @@ -67,17 +64,18 @@ export function useNotificationDeclarationQuery() { } export function useNotificationDeclarationMutation() { - const agent = useAgent() + const client = usePdsClient() const {currentAccount} = useSession() const queryClient = useQueryClient() return useMutation({ - mutationFn: async (record: AppBskyNotificationDeclaration.Record) => { - const response = await agent.app.bsky.notification.declaration.put( + mutationFn: async (record: app.bsky.notification.declaration.Main) => { + const response = await client.put( + app.bsky.notification.declaration, + record, { - repo: currentAccount!.did, + repo: currentAccount!.did as AtIdentifierString, rkey: 'self', }, - record, ) return response }, @@ -87,7 +85,7 @@ export function useNotificationDeclarationMutation() { (old?: { uri: string cid: string - value: AppBskyNotificationDeclaration.Record + value: app.bsky.notification.declaration.Main }) => { if (!old) return old return { diff --git a/src/state/queries/list-memberships.ts b/src/state/queries/list-memberships.ts index 75adcd5bb8..68a02fd051 100644 --- a/src/state/queries/list-memberships.ts +++ b/src/state/queries/list-memberships.ts @@ -1,8 +1,13 @@ import { type AppBskyActorDefs, type AppBskyGraphGetStarterPacksWithMembership, - AtUri, } from '@atproto/api' +import { + AtUri, + type AtUriString, + type DidString, + toDatetimeString, +} from '@atproto/syntax' import { type InfiniteData, useMutation, @@ -13,7 +18,8 @@ import { RQKEY as LIST_MEMBERS_RQKEY, RQKEY_ALL as LIST_MEMBERS_ALL_RQKEY, } from '#/state/queries/list-members' -import {useAgent, useSession} from '#/state/session' +import {usePdsClient, useSession} from '#/state/session' +import {app} from '#/lexicons' import type * as bsky from '#/types/bsky' import {RQKEY_WITH_MEMBERSHIP as STARTER_PACKS_WITH_MEMBERSHIPS_RKEY} from './actor-starter-packs' @@ -30,7 +36,7 @@ export function useListMembershipAddMutation({ onError?: (error: Error) => void } = {}) { const {currentAccount} = useSession() - const agent = useAgent() + const pdsClient = usePdsClient() const queryClient = useQueryClient() return useMutation< {uri: string; cid: string}, @@ -41,14 +47,15 @@ export function useListMembershipAddMutation({ if (!currentAccount) { throw new Error('Not signed in') } - const res = await agent.app.bsky.graph.listitem.create( - {repo: currentAccount.did}, - { - subject: actorDid, - list: listUri, - createdAt: new Date().toISOString(), - }, - ) + /* + * The mutation's inputs are plain strings held by legacy-typed views, so + * they are asserted to their branded forms here. + */ + const res = await pdsClient.create(app.bsky.graph.listitem, { + subject: actorDid as DidString, + list: listUri as AtUriString, + createdAt: toDatetimeString(new Date()), + }) return res }, onSuccess: (data, variables) => { @@ -129,7 +136,7 @@ export function useListMembershipRemoveMutation({ onError?: (error: Error) => void } = {}) { const {currentAccount} = useSession() - const agent = useAgent() + const pdsClient = usePdsClient() const queryClient = useQueryClient() return useMutation< void, @@ -141,9 +148,9 @@ export function useListMembershipRemoveMutation({ throw new Error('Not signed in') } const membershipUrip = new AtUri(membershipUri) - await agent.app.bsky.graph.listitem.delete({ - repo: currentAccount.did, - rkey: membershipUrip.rkey, + await pdsClient.delete(app.bsky.graph.listitem, { + repo: currentAccount.did as DidString, + rkey: membershipUrip.rkeySafe, }) }, onSuccess: (data, variables) => { diff --git a/src/state/queries/list.ts b/src/state/queries/list.ts index b5deb087c3..ec551bc8e5 100644 --- a/src/state/queries/list.ts +++ b/src/state/queries/list.ts @@ -1,14 +1,11 @@ +import {type AppBskyGraphDefs} from '@atproto/api' +import {type $Typed, type Client, type l} from '@atproto/lex' import { - type $Typed, - type AppBskyGraphDefs, - type AppBskyGraphGetList, - type AppBskyGraphList, - type AtpAgent, + type AtIdentifierString, AtUri, - type ComAtprotoRepoApplyWrites, - type Facet, - type Un$Typed, -} from '@atproto/api' + type AtUriString, + toDatetimeString, +} from '@atproto/syntax' import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' import chunk from 'lodash.chunk' @@ -16,7 +13,13 @@ import {uploadBlob} from '#/lib/api' import {until} from '#/lib/async/until' import {type ImageMeta} from '#/state/gallery' import {STALE} from '#/state/queries' -import {useAgent, useSession} from '#/state/session' +import { + useAgent, + useAppviewClient, + usePdsClient, + useSession, +} from '#/state/session' +import {app, com} from '#/lexicons' import {FEED_INFO_RQKEY_ROOT} from './feed' import {invalidate as invalidateMyLists} from './my-lists' import {RQKEY as PROFILE_LISTS_RQKEY} from './profile-lists' @@ -25,7 +28,7 @@ export const RQKEY_ROOT = 'list' export const RQKEY = (uri: string) => [RQKEY_ROOT, uri] export function useListQuery(uri?: string) { - const agent = useAgent() + const client = useAppviewClient() return useQuery({ staleTime: STALE.MINUTES.ONE, queryKey: RQKEY(uri || ''), @@ -33,11 +36,11 @@ export function useListQuery(uri?: string) { if (!uri) { throw new Error('URI not provided') } - const res = await agent.app.bsky.graph.getList({ - list: uri, + const res = await client.call(app.bsky.graph.getList, { + list: uri as AtUriString, limit: 1, }) - return res.data.list + return res.list }, enabled: !!uri, }) @@ -47,13 +50,15 @@ export interface ListCreateMutateParams { purpose: string name: string description: string - descriptionFacets: Facet[] | undefined + descriptionFacets: app.bsky.richtext.facet.Main[] | undefined avatar: ImageMeta | null | undefined } export function useListCreateMutation() { const {currentAccount} = useSession() const queryClient = useQueryClient() const agent = useAgent() + const appviewClient = useAppviewClient() + const pdsClient = usePdsClient() return useMutation<{uri: string; cid: string}, Error, ListCreateMutateParams>( { async mutationFn({ @@ -72,33 +77,28 @@ export function useListCreateMutation() { ) { throw new Error('Invalid list purpose: must be curatelist or modlist') } - const record: Un$Typed = { + const record: Omit = { purpose, name, description, descriptionFacets, avatar: undefined, - createdAt: new Date().toISOString(), + createdAt: toDatetimeString(new Date()), } if (avatar) { const blobRes = await uploadBlob(agent, avatar.path, avatar.mime) - record.avatar = blobRes.data.blob + /* + * `uploadBlob` still returns the legacy `BlobRef` class instance; + * it moves to the client with the rest of the blob pipeline. + */ + record.avatar = blobRes.data.blob as unknown as l.BlobRef } - const res = await agent.app.bsky.graph.list.create( - { - repo: currentAccount.did, - }, - record, - ) + const res = await pdsClient.create(app.bsky.graph.list, record) // wait for the appview to update - await whenAppViewReady( - agent, - res.uri, - (v: AppBskyGraphGetList.Response) => { - return typeof v?.data?.list.uri === 'string' - }, - ) + await whenAppViewReady(appviewClient, res.uri, v => { + return typeof v?.list.uri === 'string' + }) return res }, onSuccess() { @@ -115,12 +115,14 @@ export interface ListMetadataMutateParams { uri: string name: string description: string - descriptionFacets: Facet[] | undefined + descriptionFacets: app.bsky.richtext.facet.Main[] | undefined avatar: ImageMeta | null | undefined } export function useListMetadataMutation() { const {currentAccount} = useSession() const agent = useAgent() + const appviewClient = useAppviewClient() + const pdsClient = usePdsClient() const queryClient = useQueryClient() return useMutation< {uri: string; cid: string}, @@ -137,7 +139,7 @@ export function useListMetadataMutation() { } // get the current record - const {value: record} = await agent.app.bsky.graph.list.get({ + const {value: record} = await pdsClient.get(app.bsky.graph.list, { repo: currentAccount.did, rkey, }) @@ -148,30 +150,24 @@ export function useListMetadataMutation() { record.descriptionFacets = descriptionFacets if (avatar) { const blobRes = await uploadBlob(agent, avatar.path, avatar.mime) - record.avatar = blobRes.data.blob + record.avatar = blobRes.data.blob as unknown as l.BlobRef } else if (avatar === null) { record.avatar = undefined } - const res = ( - await agent.com.atproto.repo.putRecord({ - repo: currentAccount.did, - collection: 'app.bsky.graph.list', - rkey, - record, - }) - ).data + const res = await pdsClient.call(com.atproto.repo.putRecord, { + repo: currentAccount.did, + collection: 'app.bsky.graph.list', + rkey, + record, + }) // wait for the appview to update - await whenAppViewReady( - agent, - res.uri, - (v: AppBskyGraphGetList.Response) => { - const list = v.data.list - return ( - list.name === record.name && list.description === record.description - ) - }, - ) + await whenAppViewReady(appviewClient, res.uri, v => { + const list = v.list + return ( + list.name === record.name && list.description === record.description + ) + }) return res }, onSuccess(data, variables) { @@ -191,7 +187,8 @@ export function useListMetadataMutation() { export function useListDeleteMutation() { const {currentAccount} = useSession() - const agent = useAgent() + const appviewClient = useAppviewClient() + const pdsClient = usePdsClient() const queryClient = useQueryClient() return useMutation({ mutationFn: async ({uri}) => { @@ -199,11 +196,12 @@ export function useListDeleteMutation() { return } // fetch all the listitem records that belong to this list - let cursor + let cursor: string | undefined let listitemRecordUris: string[] = [] for (let i = 0; i < 100; i++) { - const res = await agent.app.bsky.graph.listitem.list({ - repo: currentAccount.did, + const res = await pdsClient.list(app.bsky.graph.listitem, { + // the session account is still legacy-typed, so its did is unbranded + repo: currentAccount.did as AtIdentifierString, cursor, limit: 100, }) @@ -221,12 +219,12 @@ export function useListDeleteMutation() { // batch delete the list and listitem records const createDel = ( uri: string, - ): $Typed => { + ): $Typed => { const urip = new AtUri(uri) return { $type: 'com.atproto.repo.applyWrites#delete', - collection: urip.collection, - rkey: urip.rkey, + collection: urip.collectionSafe, + rkey: urip.rkeySafe, } } const writes = listitemRecordUris @@ -235,15 +233,20 @@ export function useListDeleteMutation() { // apply in chunks for (const writesChunk of chunk(writes, 10)) { - await agent.com.atproto.repo.applyWrites({ - repo: currentAccount.did, + await pdsClient.call(com.atproto.repo.applyWrites, { + repo: currentAccount.did as AtIdentifierString, writes: writesChunk, }) } - // wait for the appview to update - await whenAppViewReady(agent, uri, (v: AppBskyGraphGetList.Response) => { - return !v?.success + /* + * Wait for the appview to update. Once the list is deleted `getList` + * throws, `until` catches it and passes `undefined` here, so an absent + * body signals a completed delete - the old check read `!v.success` on + * the legacy response envelope, which lex does not expose. + */ + await whenAppViewReady(appviewClient, uri, v => { + return !v }) }, onSuccess() { @@ -259,16 +262,18 @@ export function useListDeleteMutation() { export function useListMuteMutation() { const queryClient = useQueryClient() const agent = useAgent() + const appviewClient = useAppviewClient() return useMutation({ mutationFn: async ({uri, mute}) => { + // `muteModList`/`unmuteModList` are preference writes, migrated in wave B if (mute) { await agent.muteModList(uri) } else { await agent.unmuteModList(uri) } - await whenAppViewReady(agent, uri, (v: AppBskyGraphGetList.Response) => { - return Boolean(v?.data.list.viewer?.muted) === mute + await whenAppViewReady(appviewClient, uri, v => { + return Boolean(v?.list.viewer?.muted) === mute }) }, onSuccess(data, variables) { @@ -282,18 +287,20 @@ export function useListMuteMutation() { export function useListBlockMutation() { const queryClient = useQueryClient() const agent = useAgent() + const appviewClient = useAppviewClient() return useMutation({ mutationFn: async ({uri, block}) => { + // `blockModList`/`unblockModList` write a block record, migrated in wave B if (block) { await agent.blockModList(uri) } else { await agent.unblockModList(uri) } - await whenAppViewReady(agent, uri, (v: AppBskyGraphGetList.Response) => { + await whenAppViewReady(appviewClient, uri, v => { return block - ? typeof v?.data.list.viewer?.blocked === 'string' - : !v?.data.list.viewer?.blocked + ? typeof v?.list.viewer?.blocked === 'string' + : !v?.list.viewer?.blocked }) }, onSuccess(data, variables) { @@ -305,17 +312,17 @@ export function useListBlockMutation() { } async function whenAppViewReady( - agent: AtpAgent, + client: Client, uri: string, - fn: (res: AppBskyGraphGetList.Response) => boolean, + fn: (res: app.bsky.graph.getList.$OutputBody) => boolean, ) { await until( 5, // 5 tries 1e3, // 1s delay between tries fn, () => - agent.app.bsky.graph.getList({ - list: uri, + client.call(app.bsky.graph.getList, { + list: uri as AtUriString, limit: 1, }), ) diff --git a/src/state/queries/messages/actor-declaration.ts b/src/state/queries/messages/actor-declaration.ts index f6cd2d51d5..17f36cbdd8 100644 --- a/src/state/queries/messages/actor-declaration.ts +++ b/src/state/queries/messages/actor-declaration.ts @@ -3,11 +3,13 @@ import { type AppBskyActorDefs, type ChatBskyActorDeclaration, } from '@atproto/api' +import {type DidString} from '@atproto/syntax' import {useMutation, useQueryClient} from '@tanstack/react-query' import {logger} from '#/logger' -import {useAgent, useSession} from '#/state/session' +import {usePdsClient, useSession} from '#/state/session' import {resolveAllowGroupInvites} from '#/components/dms/util' +import {com} from '#/lexicons' import {RQKEY as PROFILE_RKEY} from '../profile' export function useUpdateActorDeclaration({ @@ -19,7 +21,7 @@ export function useUpdateActorDeclaration({ }) { const queryClient = useQueryClient() const {currentAccount} = useSession() - const agent = useAgent() + const pdsClient = usePdsClient() return useMutation({ mutationFn: async (update: { @@ -41,8 +43,9 @@ export function useUpdateActorDeclaration({ update.allowGroupInvites ?? current?.associated?.chat?.allowGroupInvites, }) - const result = await agent.com.atproto.repo.putRecord({ - repo: currentAccount.did, + const result = await pdsClient.call(com.atproto.repo.putRecord, { + // the session account is still legacy-typed, so its did is unbranded + repo: currentAccount.did as DidString, collection: 'chat.bsky.actor.declaration', rkey: 'self', record: { @@ -101,13 +104,13 @@ export function useUpdateActorDeclaration({ // for use in the settings screen for testing export function useDeleteActorDeclaration() { const {currentAccount} = useSession() - const agent = useAgent() + const pdsClient = usePdsClient() return useMutation({ mutationFn: async () => { if (!currentAccount) throw new Error('Not signed in') - const result = await agent.api.com.atproto.repo.deleteRecord({ - repo: currentAccount.did, + const result = await pdsClient.call(com.atproto.repo.deleteRecord, { + repo: currentAccount.did as DidString, collection: 'chat.bsky.actor.declaration', rkey: 'self', }) @@ -116,6 +119,11 @@ export function useDeleteActorDeclaration() { }) } +/* + * Still takes the legacy agent: its only caller is `getOtherRequiredData` in + * `#/ageAssurance/data`, which is blocked on `getPreferences` and so cannot + * hand over a lex client yet. + */ export async function fetchActorDeclarationRecord({ agent, did, diff --git a/src/state/queries/messages/restrictChatSettings.ts b/src/state/queries/messages/restrictChatSettings.ts index ad33385dc8..b47440bf6b 100644 --- a/src/state/queries/messages/restrictChatSettings.ts +++ b/src/state/queries/messages/restrictChatSettings.ts @@ -1,13 +1,12 @@ -import type AtpAgent from '@atproto/api' -import {type ChatBskyActorDeclaration} from '@atproto/api' +import {type Client} from '@atproto/lex' import {networkRetry} from '#/lib/async/retry' import {logger} from '#/logger' import { - getDidFromAgentSession, getOtherRequiredDataFromCache, setOtherRequiredDataActorDeclarationCache, } from '#/ageAssurance/data' +import {chat} from '#/lexicons' /** * Updates the chat actor declaration record to restrict who can contact the @@ -24,15 +23,15 @@ import { * back to the lexicon defaults when the cache is empty. */ export async function restrictChatSettings({ - agent, + client, restrictIncoming = false, restrictGroupInvites = false, }: { - agent: AtpAgent + client: Client restrictIncoming?: boolean restrictGroupInvites?: boolean }): Promise { - const did = getDidFromAgentSession(agent) + const did = client.did if (!did) return const cached = getOtherRequiredDataFromCache({did})?.actorDeclaration @@ -49,7 +48,7 @@ export async function restrictChatSettings({ ) } - const record: ChatBskyActorDeclaration.Main = { + const record: chat.bsky.actor.declaration.Main = { $type: 'chat.bsky.actor.declaration', allowIncoming: restrictIncoming ? 'none' @@ -69,11 +68,14 @@ export async function restrictChatSettings({ try { await networkRetry(3, () => - agent.com.atproto.repo.putRecord({ + /* + * A record helper, not a raw `com.atproto.repo.putRecord`: lex forces + * `service: null` on record helpers, so the write lands on the account's + * PDS even though the collection is `chat.bsky.*`. + */ + client.put(chat.bsky.actor.declaration, record, { repo: did, - collection: 'chat.bsky.actor.declaration', rkey: 'self', - record, }), ) // important, update local cache to avoid running this again diff --git a/src/state/queries/profile.ts b/src/state/queries/profile.ts index 23fd4340ac..dea8d35eea 100644 --- a/src/state/queries/profile.ts +++ b/src/state/queries/profile.ts @@ -10,6 +10,11 @@ import { type ComAtprotoRepoUploadBlob, type Un$Typed, } from '@atproto/api' +import { + type AtIdentifierString, + type DidString, + toDatetimeString, +} from '@atproto/syntax' import { type InfiniteData, keepPreviousData, @@ -33,10 +38,11 @@ import { useUnstableProfileViewCache, } from '#/state/queries/unstable-profile-cache' import {useUpdateProfileVerificationCache} from '#/state/queries/verification/useUpdateProfileVerificationCache' -import {useAgent, useSession} from '#/state/session' +import {useAgent, usePdsClient, useSession} from '#/state/session' import * as userActionHistory from '#/state/userActionHistory' import {useAnalytics} from '#/analytics' import {type Metrics, toClout} from '#/analytics/metrics' +import {app} from '#/lexicons' import type * as bsky from '#/types/bsky' import { ProgressGuideAction, @@ -634,17 +640,18 @@ export function useProfileBlockMutationQueue( function useProfileBlockMutation() { const {currentAccount} = useSession() - const agent = useAgent() + const pdsClient = usePdsClient() const queryClient = useQueryClient() return useMutation<{uri: string; cid: string}, Error, {did: string}>({ mutationFn: async ({did}) => { if (!currentAccount) { throw new Error('Not signed in') } - return await agent.app.bsky.graph.block.create( - {repo: currentAccount.did}, - {subject: did, createdAt: new Date().toISOString()}, - ) + return await pdsClient.create(app.bsky.graph.block, { + // the profile view is still legacy-typed, so its did is unbranded + subject: did as DidString, + createdAt: toDatetimeString(new Date()), + }) }, onSuccess(_, {did}) { void queryClient.invalidateQueries({queryKey: RQKEY_MY_BLOCKED()}) @@ -655,16 +662,16 @@ function useProfileBlockMutation() { function useProfileUnblockMutation() { const {currentAccount} = useSession() - const agent = useAgent() + const pdsClient = usePdsClient() const queryClient = useQueryClient() return useMutation({ mutationFn: async ({blockUri}) => { if (!currentAccount) { throw new Error('Not signed in') } - const {rkey} = new AtUri(blockUri) - await agent.app.bsky.graph.block.delete({ - repo: currentAccount.did, + const {rkeySafe: rkey} = new AtUri(blockUri) + await pdsClient.delete(app.bsky.graph.block, { + repo: currentAccount.did as AtIdentifierString, rkey, }) }, diff --git a/src/state/queries/starter-packs.ts b/src/state/queries/starter-packs.ts index 6a1aecef0e..0c781bcfea 100644 --- a/src/state/queries/starter-packs.ts +++ b/src/state/queries/starter-packs.ts @@ -1,11 +1,10 @@ import { AppBskyFeedDefs, AppBskyGraphDefs, - type AppBskyGraphGetStarterPack, AppBskyGraphStarterpack, - type AtpAgent, - AtUri, } from '@atproto/api' +import {type Client, type LexValue} from '@atproto/lex' +import {AtUri, type AtUriString, toDatetimeString} from '@atproto/syntax' import {RichText} from '@bsky.app/sdk/richtext' import { type QueryClient, @@ -25,8 +24,8 @@ import { import {invalidateActorStarterPacksQuery} from '#/state/queries/actor-starter-packs' import {STALE} from '#/state/queries/index' import {invalidateListMembersQuery} from '#/state/queries/list-members' -import {useAgent, useAppviewClient} from '#/state/session' -import {type app} from '#/lexicons' +import {useAppviewClient, usePdsClient} from '#/state/session' +import {app, com} from '#/lexicons' import * as bsky from '#/types/bsky' const RQKEY_ROOT = 'starter-pack' @@ -56,7 +55,7 @@ export function useStarterPackQuery({ did?: string rkey?: string }) { - const agent = useAgent() + const client = useAppviewClient() return useQuery({ queryKey: RQKEY(uri ? {uri} : {did, rkey}), @@ -67,10 +66,10 @@ export function useStarterPackQuery({ uri = httpStarterPackUriToAtUri(uri) as string } - const res = await agent.app.bsky.graph.getStarterPack({ - starterPack: uri, + const res = await client.call(app.bsky.graph.getStarterPack, { + starterPack: uri as AtUriString, }) - return res.data.starterPack + return res.starterPack }, enabled: Boolean(uri) || Boolean(did && rkey), staleTime: STALE.MINUTES.FIVE, @@ -104,12 +103,12 @@ export function useCreateStarterPackMutation({ onError: (e: Error) => void }) { const queryClient = useQueryClient() - const agent = useAgent() /* * Facet/mention resolution is an appview job - it resolves handles through * the appview, and the public fallback keeps it working when logged out. */ const appviewClient = useAppviewClient() + const pdsClient = usePdsClient() return useMutation< {uri: string; cid: string}, @@ -130,30 +129,26 @@ export function useCreateStarterPackMutation({ description, profiles, descriptionFacets, - agent, + client: pdsClient, }) - return await agent.app.bsky.graph.starterpack.create( - { - repo: agent.assertDid, - }, - { - name, - description, - descriptionFacets, - list: listRes?.uri, - feeds: feeds?.map(f => ({uri: f.uri})), - createdAt: new Date().toISOString(), - }, - ) + return await pdsClient.create(app.bsky.graph.starterpack, { + name, + description, + descriptionFacets, + // `createStarterPackList` returns a plain string uri + list: listRes?.uri as AtUriString, + feeds: feeds?.map(f => ({uri: f.uri as AtUriString})), + createdAt: toDatetimeString(new Date()), + }) }, onSuccess: async data => { - await whenAppViewReady(agent, data.uri, v => { - return typeof v?.data.starterPack.uri === 'string' + await whenAppViewReady(appviewClient, data.uri, v => { + return typeof v?.starterPack.uri === 'string' }) await invalidateActorStarterPacksQuery({ queryClient, - did: agent.session!.did, + did: pdsClient.assertDid, }) onSuccess(data) }, @@ -171,8 +166,8 @@ export function useEditStarterPackMutation({ onError: (error: Error) => void }) { const queryClient = useQueryClient() - const agent = useAgent() const appviewClient = useAppviewClient() + const pdsClient = usePdsClient() return useMutation< void, @@ -197,25 +192,29 @@ export function useEditStarterPackMutation({ descriptionFacets = rt.facets } - if (!AppBskyGraphStarterpack.isRecord(currentStarterPack.record)) { + if (!bsky.isType(app.bsky.graph.starterpack, currentStarterPack.record)) { throw new Error('Invalid starter pack') } const removedItems = currentListItems.filter( i => - i.subject.did !== agent.session?.did && + i.subject.did !== pdsClient.did && !profiles.find(p => p.did === i.subject.did && p.did), ) if (removedItems.length !== 0) { const chunks = chunk(removedItems, 50) for (const chunk of chunks) { - await agent.com.atproto.repo.applyWrites({ - repo: agent.session!.did, - writes: chunk.map(i => ({ - $type: 'com.atproto.repo.applyWrites#delete', - collection: 'app.bsky.graph.listitem', - rkey: new AtUri(i.uri).rkey, - })), + await pdsClient.call(com.atproto.repo.applyWrites, { + repo: pdsClient.assertDid, + writes: chunk.map( + ( + i, + ): com.atproto.repo.applyWrites.$InputBody['writes'][number] => ({ + $type: 'com.atproto.repo.applyWrites#delete', + collection: 'app.bsky.graph.listitem', + rkey: new AtUri(i.uri).rkeySafe, + }), + ), }) } } @@ -226,33 +225,44 @@ export function useEditStarterPackMutation({ if (addedProfiles.length > 0) { const chunks = chunk(addedProfiles, 50) for (const chunk of chunks) { - await agent.com.atproto.repo.applyWrites({ - repo: agent.session!.did, - writes: chunk.map(p => ({ - $type: 'com.atproto.repo.applyWrites#create', - collection: 'app.bsky.graph.listitem', - value: { - $type: 'app.bsky.graph.listitem', - subject: p.did, - list: currentStarterPack.list?.uri, - createdAt: new Date().toISOString(), - }, - })), + await pdsClient.call(com.atproto.repo.applyWrites, { + repo: pdsClient.assertDid, + writes: chunk.map( + ( + p, + ): com.atproto.repo.applyWrites.$InputBody['writes'][number] => ({ + $type: 'com.atproto.repo.applyWrites#create', + collection: 'app.bsky.graph.listitem', + value: { + $type: 'app.bsky.graph.listitem', + subject: p.did, + list: currentStarterPack.list?.uri, + createdAt: new Date().toISOString(), + }, + }), + ), }) } } const rkey = parseStarterPackUri(currentStarterPack.uri)!.rkey - await agent.com.atproto.repo.putRecord({ - repo: agent.session!.did, + await pdsClient.call(com.atproto.repo.putRecord, { + repo: pdsClient.assertDid, collection: 'app.bsky.graph.starterpack', rkey, record: { + $type: 'app.bsky.graph.starterpack', name, description, descriptionFacets, list: currentStarterPack.list?.uri, - feeds, + /* + * Pre-existing quirk preserved verbatim: the edit path writes whole + * `GeneratorView`s where the lexicon declares `feedItem` refs. lex + * types the raw `putRecord` body as a `LexValue`, which the legacy + * view interface does not structurally satisfy, hence the cast. + */ + feeds: feeds as unknown as LexValue, createdAt: currentStarterPack.record.createdAt, updatedAt: new Date().toISOString(), }, @@ -260,12 +270,12 @@ export function useEditStarterPackMutation({ }, onSuccess: async (_, {currentStarterPack}) => { const parsed = parseStarterPackUri(currentStarterPack.uri) - await whenAppViewReady(agent, currentStarterPack.uri, v => { - return currentStarterPack.cid !== v?.data.starterPack.cid + await whenAppViewReady(appviewClient, currentStarterPack.uri, v => { + return currentStarterPack.cid !== v?.starterPack.cid }) await invalidateActorStarterPacksQuery({ queryClient, - did: agent.session!.did, + did: pdsClient.assertDid, }) if (currentStarterPack.list) { await invalidateListMembersQuery({ @@ -275,7 +285,7 @@ export function useEditStarterPackMutation({ } await invalidateStarterPack({ queryClient, - did: agent.session!.did, + did: pdsClient.assertDid, rkey: parsed!.rkey, }) onSuccess() @@ -293,35 +303,34 @@ export function useDeleteStarterPackMutation({ onSuccess: () => void onError: (error: Error) => void }) { - const agent = useAgent() + const appviewClient = useAppviewClient() + const pdsClient = usePdsClient() const queryClient = useQueryClient() return useMutation({ mutationFn: async ({listUri, rkey}: {listUri?: string; rkey: string}) => { - if (!agent.session) { - throw new Error(`Requires signed in user`) - } + const did = pdsClient.assertDid if (listUri) { - await agent.app.bsky.graph.list.delete({ - repo: agent.session.did, - rkey: new AtUri(listUri).rkey, + await pdsClient.delete(app.bsky.graph.list, { + repo: did, + rkey: new AtUri(listUri).rkeySafe, }) } - await agent.app.bsky.graph.starterpack.delete({ - repo: agent.session.did, + await pdsClient.delete(app.bsky.graph.starterpack, { + repo: did, rkey, }) }, onSuccess: async (_, {listUri, rkey}) => { const uri = createStarterPackUri({ - did: agent.session!.did, + did: pdsClient.assertDid, rkey, }) if (uri) { - await whenAppViewReady(agent, uri, v => { - return Boolean(v?.data?.starterPack) === false + await whenAppViewReady(appviewClient, uri, v => { + return Boolean(v?.starterPack) === false }) } @@ -330,11 +339,11 @@ export function useDeleteStarterPackMutation({ } await invalidateActorStarterPacksQuery({ queryClient, - did: agent.session!.did, + did: pdsClient.assertDid, }) await invalidateStarterPack({ queryClient, - did: agent.session!.did, + did: pdsClient.assertDid, rkey, }) onSuccess() @@ -346,15 +355,18 @@ export function useDeleteStarterPackMutation({ } async function whenAppViewReady( - agent: AtpAgent, + client: Client, uri: string, - fn: (res?: AppBskyGraphGetStarterPack.Response) => boolean, + fn: (res?: app.bsky.graph.getStarterPack.$OutputBody) => boolean, ) { await until( 5, // 5 tries 1e3, // 1s delay between tries fn, - () => agent.app.bsky.graph.getStarterPack({starterPack: uri}), + () => + client.call(app.bsky.graph.getStarterPack, { + starterPack: uri as AtUriString, + }), ) } diff --git a/src/state/queries/verification/useVerificationCreateMutation.tsx b/src/state/queries/verification/useVerificationCreateMutation.tsx index 083a6f3b54..4f1a318f48 100644 --- a/src/state/queries/verification/useVerificationCreateMutation.tsx +++ b/src/state/queries/verification/useVerificationCreateMutation.tsx @@ -1,15 +1,22 @@ -import {type AppBskyActorGetProfile} from '@atproto/api' +import { + type AtIdentifierString, + type DidString, + type HandleString, + toDatetimeString, +} from '@atproto/syntax' import {useMutation} from '@tanstack/react-query' import {until} from '#/lib/async/until' import {useUpdateProfileVerificationCache} from '#/state/queries/verification/useUpdateProfileVerificationCache' -import {useAgent, useSession} from '#/state/session' +import {useAppviewClient, usePdsClient, useSession} from '#/state/session' import {useAnalytics} from '#/analytics' +import {app} from '#/lexicons' import type * as bsky from '#/types/bsky' export function useVerificationCreateMutation() { const ax = useAnalytics() - const agent = useAgent() + const appviewClient = useAppviewClient() + const pdsClient = usePdsClient() const {currentAccount} = useSession() const updateProfileVerificationCache = useUpdateProfileVerificationCache() @@ -19,20 +26,18 @@ export function useVerificationCreateMutation() { throw new Error('User not logged in') } - const {uri} = await agent.app.bsky.graph.verification.create( - {repo: currentAccount.did}, - { - subject: profile.did, - createdAt: new Date().toISOString(), - handle: profile.handle, - displayName: profile.displayName || '', - }, - ) + const {uri} = await pdsClient.create(app.bsky.graph.verification, { + // the profile view is still legacy-typed, so its strings are unbranded + subject: profile.did as DidString, + createdAt: toDatetimeString(new Date()), + handle: profile.handle as HandleString, + displayName: profile.displayName || '', + }) await until( 5, 1e3, - ({data: profile}: AppBskyActorGetProfile.Response) => { + (profile: app.bsky.actor.getProfile.$OutputBody) => { if ( profile.verification && profile.verification.verifications.find(v => v.uri === uri) @@ -42,7 +47,9 @@ export function useVerificationCreateMutation() { return false }, () => { - return agent.getProfile({actor: profile.did ?? ''}) + return appviewClient.call(app.bsky.actor.getProfile, { + actor: (profile.did ?? '') as AtIdentifierString, + }) }, ) }, diff --git a/src/state/queries/verification/useVerificationsRemoveMutation.tsx b/src/state/queries/verification/useVerificationsRemoveMutation.tsx index 3fa95497fd..657c50c909 100644 --- a/src/state/queries/verification/useVerificationsRemoveMutation.tsx +++ b/src/state/queries/verification/useVerificationsRemoveMutation.tsx @@ -1,19 +1,18 @@ -import { - type AppBskyActorDefs, - type AppBskyActorGetProfile, - AtUri, -} from '@atproto/api' +import {type AppBskyActorDefs} from '@atproto/api' +import {type AtIdentifierString, AtUri} from '@atproto/syntax' import {useMutation} from '@tanstack/react-query' import {until} from '#/lib/async/until' import {useUpdateProfileVerificationCache} from '#/state/queries/verification/useUpdateProfileVerificationCache' -import {useAgent, useSession} from '#/state/session' +import {useAppviewClient, usePdsClient, useSession} from '#/state/session' import {useAnalytics} from '#/analytics' +import {app} from '#/lexicons' import type * as bsky from '#/types/bsky' export function useVerificationsRemoveMutation() { const ax = useAnalytics() - const agent = useAgent() + const appviewClient = useAppviewClient() + const pdsClient = usePdsClient() const {currentAccount} = useSession() const updateProfileVerificationCache = useUpdateProfileVerificationCache() @@ -33,9 +32,8 @@ export function useVerificationsRemoveMutation() { await Promise.all( uris.map(uri => { - return agent.app.bsky.graph.verification.delete({ - repo: currentAccount.did, - rkey: new AtUri(uri).rkey, + return pdsClient.delete(app.bsky.graph.verification, { + rkey: new AtUri(uri).rkeySafe, }) }), ) @@ -43,7 +41,7 @@ export function useVerificationsRemoveMutation() { await until( 5, 1e3, - ({data: profile}: AppBskyActorGetProfile.Response) => { + (profile: app.bsky.actor.getProfile.$OutputBody) => { if ( !profile.verification?.verifications.some(v => uris.includes(v.uri)) ) { @@ -52,7 +50,10 @@ export function useVerificationsRemoveMutation() { return false }, () => { - return agent.getProfile({actor: profile.did ?? ''}) + return appviewClient.call(app.bsky.actor.getProfile, { + // the profile view is still legacy-typed, so its did is unbranded + actor: (profile.did ?? '') as AtIdentifierString, + }) }, ) }, diff --git a/src/state/session/create-account.ts b/src/state/session/create-account.ts index fd67681482..b44bed4dbd 100644 --- a/src/state/session/create-account.ts +++ b/src/state/session/create-account.ts @@ -1,5 +1,6 @@ import {type AppBskyActorProfile, type Un$Typed} from '@atproto/api' import {TID} from '@atproto/common-web' +import {type Client} from '@atproto/lex' import {PasswordSession} from '@atproto/lex-password-session' import {networkRetry} from '#/lib/async/retry' @@ -21,6 +22,7 @@ import { import {unsafeGetAndComputeAgeAssurance} from '#/ageAssurance/state' import {features} from '#/analytics' import {type BskyAppAgent} from './bridge-agent' +import {agentToPdsClient} from './clients' import {configureModerationForAccount} from './moderation' import { buildBundle, @@ -109,7 +111,11 @@ export async function createSessionBundleAndCreateAccount( if (isProd) { postSignupTasks.push( initializeSavedFeeds(bundle.agent), - restrictChatAfterAgeAssurance(aa, bundle.agent, earlyAccount.did), + restrictChatAfterAgeAssurance( + aa, + agentToPdsClient(bundle.agent), + earlyAccount.did, + ), ) } // Post-signup writes are not required to enter onboarding. @@ -207,14 +213,14 @@ function initializeSavedFeeds(agent: BskyAppAgent) { function restrictChatAfterAgeAssurance( ageAssurance: Promise, - agent: BskyAppAgent, + client: Client, did: string, ) { return ageAssurance.then(() => { const {flags} = unsafeGetAndComputeAgeAssurance({did}) if (flags?.chatDisabled || flags?.groupChatDisabled) { void restrictChatSettings({ - agent, + client, restrictIncoming: flags.chatDisabled, restrictGroupInvites: flags.groupChatDisabled, }) From 0c93d1e416cddfc00e3f5412a37c6f2d60a3c27d Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 13 Aug 2026 22:26:20 +0300 Subject: [PATCH 21/32] [SDK] Migrate the post pipeline and blob uploads (#11380) Co-authored-by: Claude Fable 5 --- package.json | 5 +- .../contacts/screens/GetContacts.tsx | 16 +- src/features/liveNow/index.tsx | 9 +- src/lib/api/__tests__/computeCid.test.ts | 121 +++++++++ src/lib/api/computeCid.ts | 148 ++++++++++ src/lib/api/index.ts | 256 ++++++------------ src/lib/api/legacy-blob.ts | 31 +++ src/lib/api/upload-blob.ts | 40 ++- src/lib/api/upload-blob.web.ts | 28 +- src/screens/Onboarding/StepFinished/index.tsx | 7 +- src/state/queries/list.ts | 16 +- src/state/queries/profile.ts | 19 +- src/view/com/composer/Composer.tsx | 10 +- 13 files changed, 483 insertions(+), 223 deletions(-) create mode 100644 src/lib/api/__tests__/computeCid.test.ts create mode 100644 src/lib/api/computeCid.ts create mode 100644 src/lib/api/legacy-blob.ts diff --git a/package.json b/package.json index 510955abbf..5d92bc833f 100644 --- a/package.json +++ b/package.json @@ -333,11 +333,14 @@ "^multiformats/cid$": "/node_modules/multiformats/dist/src/cid.js", "^multiformats/bases/base32$": "/node_modules/multiformats/dist/src/bases/base32.js", "^multiformats/hashes/digest$": "/node_modules/multiformats/dist/src/hashes/digest.js", + "^multiformats/hashes/hasher$": "/node_modules/multiformats/dist/src/hashes/hasher.js", "^multiformats/hashes/sha2$": "/node_modules/multiformats/dist/src/hashes/sha2.js", "^uint8arrays/from-string$": "/node_modules/uint8arrays/dist/src/from-string.js", "^uint8arrays/to-string$": "/node_modules/uint8arrays/dist/src/to-string.js", "^unicode-segmenter/grapheme$": "/node_modules/unicode-segmenter/grapheme.cjs", - "^await-lock$": "/node_modules/await-lock/build/AwaitLock.js" + "^await-lock$": "/node_modules/await-lock/build/AwaitLock.js", + "^@ipld/dag-cbor$": "/node_modules/@ipld/dag-cbor/src/index.js", + "^cborg$": "/node_modules/cborg/cborg.js" }, "transformIgnorePatterns": [ "node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|nanoid|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|normalize-url|react-native-svg|@sentry/.*|sentry-expo|bcp-47-match|@atproto/.*|@bsky.app/sdk|tlds|multiformats|uint8arrays|@ipld/.*|cborg|await-lock)" diff --git a/src/components/contacts/screens/GetContacts.tsx b/src/components/contacts/screens/GetContacts.tsx index 879864b162..d84e0d1ab0 100644 --- a/src/components/contacts/screens/GetContacts.tsx +++ b/src/components/contacts/screens/GetContacts.tsx @@ -8,16 +8,18 @@ import { AppBskyContactImportContacts, type Un$Typed, } from '@atproto/api' +import {type Client} from '@atproto/lex' import {msg, t} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' import {useMutation, useQueryClient} from '@tanstack/react-query' import {uploadBlob} from '#/lib/api' +import {toLegacyBlobRef} from '#/lib/api/legacy-blob' import {cleanError, isNetworkError} from '#/lib/strings/errors' import {logger} from '#/logger' import {findContactsStatusQueryKey} from '#/state/queries/find-contacts' -import {useAgent} from '#/state/session' +import {useAgent, usePdsClient} from '#/state/session' import { Context as OnboardingContext, type OnboardingAction, @@ -55,6 +57,7 @@ export function GetContacts({ const {_} = useLingui() const ax = useAnalytics() const agent = useAgent() + const pdsClient = usePdsClient() const insets = useSafeAreaInsets() const gutters = useGutters([0, 'wide']) const queryClient = useQueryClient() @@ -72,7 +75,7 @@ export function GetContacts({ */ if (context === 'Onboarding' && maybeOnboardingContext) { try { - await createProfileRecord(agent, maybeOnboardingContext) + await createProfileRecord(agent, pdsClient, maybeOnboardingContext) } catch (error) { logger.debug('Error creating profile record:', {safeMessage: error}) } @@ -326,6 +329,7 @@ function showPermissionDeniedAlert() { */ async function createProfileRecord( agent: AtpAgent, + pdsClient: Client, onboardingContext: { state: OnboardingState dispatch: React.Dispatch @@ -334,15 +338,17 @@ async function createProfileRecord( const profileStepResults = onboardingContext.state.profileStepResults const {imageUri, imageMime} = profileStepResults const blobPromise = - imageUri && imageMime ? uploadBlob(agent, imageUri, imageMime) : undefined + imageUri && imageMime + ? uploadBlob(pdsClient, imageUri, imageMime) + : undefined await agent.upsertProfile(async existing => { let next: Un$Typed = existing ?? {} if (blobPromise) { const res = await blobPromise - if (res.data.blob) { - next.avatar = res.data.blob + if (res.blob) { + next.avatar = toLegacyBlobRef(res.blob) } } diff --git a/src/features/liveNow/index.tsx b/src/features/liveNow/index.tsx index 3b60b688bb..525e189ace 100644 --- a/src/features/liveNow/index.tsx +++ b/src/features/liveNow/index.tsx @@ -224,7 +224,6 @@ export function useUpsertLiveStatusMutation( ) { const ax = useAnalytics() const {currentAccount} = useSession() - const agent = useAgent() const pdsClient = usePdsClient() const queryClient = useQueryClient() const control = useDialogContext() @@ -244,15 +243,11 @@ export function useUpsertLiveStatusMutation( const img = await imageToThumb(linkMeta.image) if (img) { const blob = await uploadBlob( - agent, + pdsClient, img.source.path, img.source.mime, ) - /* - * `uploadBlob` still returns the legacy `BlobRef` class - * instance; it moves to the client with the blob pipeline. - */ - thumb = blob.data.blob as unknown as l.BlobRef + thumb = blob.blob } } catch (e: any) { ax.logger.error(`Failed to upload thumbnail for live status`, { diff --git a/src/lib/api/__tests__/computeCid.test.ts b/src/lib/api/__tests__/computeCid.test.ts new file mode 100644 index 0000000000..ce0adc4a85 --- /dev/null +++ b/src/lib/api/__tests__/computeCid.test.ts @@ -0,0 +1,121 @@ +/* + * The jest suite ships global manual mocks for `multiformats/cid` and + * `multiformats/hashes/hasher` (in root `__mocks__/`) so unrelated tests don't + * pull in real crypto. This test is precisely about the real CID hashing, so we + * opt back into the actual implementations here. + */ +jest.unmock('multiformats/cid') +jest.unmock('multiformats/hashes/hasher') + +import {BlobRef} from '@atproto/api' +import {CID} from 'multiformats/cid' + +import {computeCid} from '#/lib/api/computeCid' +import {type app, type com} from '#/lexicons' + +/* + * Golden-CID regression test for the composer post pipeline. + * + * `computeCid` hashes a post record in the client so a thread's later posts can + * reference earlier posts by CID before the server assigns them. The hash is + * byte-sensitive: any drift in how records (especially blobs) are serialized to + * DAG-CBOR silently produces the wrong CID and breaks reply chains with NO type + * error. These golden values MUST remain byte-identical - they gate the + * structural lex-blob shape check. + * + * The blob CID below is a fixed, deterministic CIDv1/raw/sha256 used purely as a + * stable fixture - it is not derived from any real upload. + */ +const BLOB_CID = 'bafkreieq5jui4j25lacwomsqgjeswwl3y5zcdrresptwgmfylxo2depppq' + +/** + * Build a post record with an image embed whose blob is the given value. + */ +function postWithImageBlob(blob: unknown): app.bsky.feed.post.Main { + return { + $type: 'app.bsky.feed.post', + createdAt: '2024-01-01T00:00:00.001Z', + text: 'post with image', + embed: { + $type: 'app.bsky.embed.images', + images: [ + { + image: blob, + alt: 'alt text', + aspectRatio: {width: 100, height: 200}, + }, + ], + }, + } as app.bsky.feed.post.Main +} + +describe('computeCid', () => { + it('case 1: plain post record with no blob', async () => { + const record: app.bsky.feed.post.Main = { + $type: 'app.bsky.feed.post', + createdAt: '2024-01-01T00:00:00.000Z', + text: 'hello world', + } + expect(await computeCid(record)).toBe( + 'bafyreieawtmh7hwfrqpamqkodza5r62bbfhsepe2iyustgxhgbhi6b2lfi', + ) + }) + + it('case 2: record whose embed carries a plain-JSON lex blob', async () => { + /* + * The blob shape lex `uploadBlob` returns: a plain object + * `{$type: 'blob', ref, mimeType, size}` with `ref` a parsed CID. The + * structural lex-blob guard passes it through `prepareForHashing` + * untouched and DAG-CBOR encodes its CID `ref` as a CID link. The golden + * CID below is the byte-identical value the pre-migration `BlobRef` class + * instance produced via `.ipld()`. + */ + const blob = { + $type: 'blob' as const, + ref: CID.parse(BLOB_CID), + mimeType: 'image/jpeg', + size: 12345, + } + expect(await computeCid(postWithImageBlob(blob))).toBe( + 'bafyreiem7g6vja66nebr7he4fshfnlyndyldbvle2n265oixscmepjcbii', + ) + }) + + it('case 2b: a legacy BlobRef instance hashes to the same CID', async () => { + /* + * The video pipeline still yields legacy `BlobRef` class instances, so + * `prepareForHashing` keeps its `instanceof` guard. Both branches must + * agree: this asserts the SAME golden CID as case 2. + */ + const blob = new BlobRef(CID.parse(BLOB_CID), 'image/jpeg', 12345) + expect(await computeCid(postWithImageBlob(blob))).toBe( + 'bafyreiem7g6vja66nebr7he4fshfnlyndyldbvle2n265oixscmepjcbii', + ) + }) + + it('case 3: three-post thread chains reply StrongRef CIDs', async () => { + const did = 'did:plc:abc123' + const base = new Date('2024-01-01T00:00:00.000Z') + const golden = [ + 'bafyreig62rxs34h5rvznfrracwkjlfgad5b25qxglp2hcziqdfas2nw2ee', + 'bafyreicxcj2tq5jrh5jcaczg3eli5cvxitgzu7kpu3fm5v3njq2byjxirq', + 'bafyreigvaswuhlpd7dllja2xrqswhqbruyv2kar7mvbn7gdm5ldzu6vkti', + ] + + let reply: app.bsky.feed.post.Main['reply'] | undefined + for (let i = 0; i < 3; i++) { + const now = new Date(base.getTime() + i) + const uri = `at://${did}/app.bsky.feed.post/rkey${i}` + const record = { + $type: 'app.bsky.feed.post', + createdAt: now.toISOString(), + text: `post ${i}`, + reply, + } as app.bsky.feed.post.Main + const cid = await computeCid(record) + expect(cid).toBe(golden[i]) + const ref = {cid, uri} as com.atproto.repo.strongRef.Main + reply = {root: reply?.root ?? ref, parent: ref} + } + }) +}) diff --git a/src/lib/api/computeCid.ts b/src/lib/api/computeCid.ts new file mode 100644 index 0000000000..18821793ae --- /dev/null +++ b/src/lib/api/computeCid.ts @@ -0,0 +1,148 @@ +import {BlobRef} from '@atproto/api' +import {sha256} from 'js-sha256' +import {CID} from 'multiformats/cid' +import * as Hasher from 'multiformats/hashes/hasher' + +import {type app} from '#/lexicons' + +/* + * Client-side CID computation for post records, extracted from the post + * pipeline so it can be unit-tested in isolation (importing the pipeline pulls + * in the native gallery/media chain). See `computeCid.test.ts` for the + * golden-CID regression fixtures that gate any change to this serialization. + */ + +// The built-in hashing functions from multiformats (`multiformats/hashes/sha2`) +// are meant for Node.js, this is the cross-platform equivalent. +const mf_sha256 = Hasher.from({ + name: 'sha2-256', + code: 0x12, + encode: input => { + const digest = sha256.arrayBuffer(input) + return new Uint8Array(digest) + }, +}) + +export async function computeCid( + record: app.bsky.feed.post.Main, +): Promise { + /* + * Lazily loaded since it's only needed when posting a thread, and its + * `cborg` dependency is ~190KB that would otherwise be in the initial + * web bundle. + */ + const dcbor = await importDagCbor() + /* + * IMPORTANT: `prepareForHashing` prepares the record to be hashed by + * removing fields with undefined value, and converting blobs to the right + * IPLD representation. + */ + const prepared = prepareForHashing(record) + // 1. Encode the record into DAG-CBOR format + const encoded = dcbor.encode(prepared) + // 2. Hash the record in SHA-256 (code 0x12) + const digest = await mf_sha256.digest(encoded) + // 3. Create a CIDv1, specifying DAG-CBOR as content (code 0x71) + const cid = CID.createV1(0x71, digest) + // 4. Get the Base32 representation of the CID (`b` prefix) + return cid.toString() +} + +/** + * True for a plain-JSON lexicon blob, the shape lex `uploadBlob` returns + * (`{$type: 'blob', ref, mimeType, size}` with `ref` a parsed CID). Lex blobs + * are plain objects, not class instances. + */ +function isLexBlob(v: unknown): boolean { + if (v == null || typeof v !== 'object') return false + const o = v as Record + return o.$type === 'blob' && 'ref' in o && 'mimeType' in o +} + +/* + * Returns a transformed version of the object for use in DAG-CBOR. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function prepareForHashing(v: any): any { + /* + * A plain-JSON lex blob is already in the right IPLD shape (its `ref` is a + * parsed CID that DAG-CBOR encodes as a CID link), so pass it through + * untouched. + */ + if (isLexBlob(v)) { + return v + } + + /* + * The video pipeline still reads its blob off the legacy agent + * (`app.bsky.video.getJobStatus` in composer `state/video`), which yields a + * `BlobRef` class instance. `ipld()` gives the same IPLD shape a lex blob + * already has, so both branches hash identically. Drop this guard once the + * video client is migrated. + */ + if (v instanceof BlobRef) { + return v.ipld() + } + + // Walk through arrays + if (Array.isArray(v)) { + let pure = true + const mapped = v.map(value => { + if (value !== (value = prepareForHashing(value))) { + pure = false + } + return value + }) + return pure ? v : mapped + } + + // Walk through plain objects + if (isPlainObject(v)) { + const rec = v as Record + const obj: Record = {} + let pure = true + for (const key in rec) { + let value = rec[key] + // `value` is undefined + if (value === undefined) { + pure = false + continue + } + /* + * `prepareForHashing` returned a value that's different from what we had + * before + */ + if (value !== (value = prepareForHashing(value))) { + pure = false + } + obj[key] = value + } + // Return as is if we haven't needed to tamper with anything + return pure ? v : obj + } + return v +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function isPlainObject(v: any): boolean { + if (typeof v !== 'object' || v === null) { + return false + } + const proto = Object.getPrototypeOf(v) + return proto === Object.prototype || proto === null +} + +/** + * Load `@ipld/dag-cbor` on demand. The dynamic `import()` lets web bundlers + * emit it (and its ~190KB `cborg` dependency) as a separate chunk that only + * loads when posting a thread. Under jest (which runs without + * `--experimental-vm-modules`) dynamic import throws, so we fall back to a + * lazy `require`, which resolves through the test moduleNameMapper. + */ +function importDagCbor(): Promise { + if (process.env.NODE_ENV === 'test') { + // eslint-disable-next-line @typescript-eslint/no-require-imports + return Promise.resolve(require('@ipld/dag-cbor')) + } + return import('@ipld/dag-cbor') +} diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index aee481669e..e52d4ec4fc 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -1,28 +1,14 @@ -import { - type $Typed, - type AppBskyEmbedExternal, - type AppBskyEmbedGallery, - type AppBskyEmbedImages, - type AppBskyEmbedRecord, - type AppBskyEmbedRecordWithMedia, - type AppBskyEmbedVideo, - AppBskyFeedPost, - type AtpAgent, - BlobRef, - ChatBskyGroupDefs, - type ComAtprotoLabelDefs, - type ComAtprotoRepoApplyWrites, - type ComAtprotoRepoStrongRef, -} from '@atproto/api' +import {type AtpAgent, ChatBskyGroupDefs} from '@atproto/api' import {TID} from '@atproto/common-web' -import {type Client} from '@atproto/lex' -import {toDatetimeString} from '@atproto/syntax' +import {type $Typed, type Client} from '@atproto/lex' +import { + type AtUriString, + toDatetimeString, + type UriString, +} from '@atproto/syntax' import {RichText} from '@bsky.app/sdk/richtext' import {t} from '@lingui/core/macro' import {type QueryClient} from '@tanstack/react-query' -import {sha256} from 'js-sha256' -import {CID} from 'multiformats/cid' -import * as Hasher from 'multiformats/hashes/hasher' import {IMAGE_SIZE_CONFIG_POSTS} from '#/lib/constants' import {isNetworkError} from '#/lib/strings/errors' @@ -42,8 +28,11 @@ import { type PostDraft, type ThreadDraft, } from '#/view/com/composer/state/composer' +import {app, com} from '#/lexicons' import * as bsky from '#/types/bsky' import {createGIFDescription} from '../gif-alt-text' +import {computeCid} from './computeCid' +import {fromLegacyBlobRef} from './legacy-blob' import {uploadBlob} from './upload-blob' export {uploadBlob} @@ -54,13 +43,23 @@ interface PostOpts { onStateChange?: (state: string) => void langs?: string[] /* - * Facet/mention resolution is an appview job - it resolves handles through - * the appview, and the public fallback keeps it working when logged out. - * The rest of this pipeline still writes through the agent. + * Facet/mention resolution and reply-root lookup are appview jobs - they + * resolve handles and read posts through the appview, and the public + * fallback keeps facet detection working when logged out. */ appviewClient: Client + /* + * The repo write itself (applyWrites) plus every record blob (images, + * gallery items, link thumbnails, video captions) goes to the account's own + * PDS, never the appview. + */ + pdsClient: Client } +/** + * The `agent` is only still here for the link/gif resolvers in `resolve.ts`, + * which read through it; drop the parameter when those move to the clients. + */ export async function post( agent: AtpAgent, queryClient: QueryClient, @@ -70,12 +69,12 @@ export async function post( opts.onStateChange?.(t`Processing...`) let replyPromise: - | Promise - | AppBskyFeedPost.Record['reply'] + | Promise + | app.bsky.feed.post.Main['reply'] | undefined if (opts.replyTo) { // Not awaited to avoid waterfalls. - replyPromise = resolveReply(agent, opts.replyTo) + replyPromise = resolveReply(opts.appviewClient, opts.replyTo) } // add top 3 languages from user preferences if langs is provided @@ -84,8 +83,8 @@ export async function post( langs = opts.langs.slice(0, 3) } - const did = agent.assertDid - const writes: $Typed[] = [] + const did = opts.pdsClient.assertDid + const writes: com.atproto.repo.applyWrites.$InputBody['writes'] = [] const uris: string[] = [] let now = new Date() @@ -98,11 +97,12 @@ export async function post( const rtPromise = resolveRT(opts.appviewClient, draft.richtext) const embedPromise = resolveEmbed( agent, + opts.pdsClient, queryClient, draft, opts.onStateChange, ) - let labels: $Typed | undefined + let labels: $Typed | undefined if (draft.labels.length) { labels = { $type: 'com.atproto.label.defs#selfLabels', @@ -115,17 +115,17 @@ export async function post( now.setMilliseconds(now.getMilliseconds() + 1) tid = TID.next(tid) const rkey = tid.toString() - const uri = `at://${did}/app.bsky.feed.post/${rkey}` + const uri = `at://${did}/app.bsky.feed.post/${rkey}` as AtUriString uris.push(uri) const rt = await rtPromise const embed = await embedPromise const reply = await replyPromise - const record: AppBskyFeedPost.Record = { + const record: app.bsky.feed.post.Main = { // IMPORTANT: $type has to exist, CID is calculated with the `$type` field // present and will produce the wrong CID if you omit it. $type: 'app.bsky.feed.post', - createdAt: now.toISOString(), + createdAt: toDatetimeString(now), text: rt.text, facets: rt.facets, reply, @@ -164,7 +164,7 @@ export async function post( value: { ...thread.postgate, $type: 'app.bsky.feed.postgate', - createdAt: now.toISOString(), + createdAt: toDatetimeString(now), post: uri, }, }) @@ -182,8 +182,8 @@ export async function post( } try { - await agent.com.atproto.repo.applyWrites({ - repo: agent.assertDid, + await opts.pdsClient.call(com.atproto.repo.applyWrites, { + repo: did, writes: writes, validate: true, }) @@ -224,9 +224,9 @@ export class ReplyDeletedError extends Error { } } -async function resolveReply(agent: AtpAgent, replyTo: string) { - const {data} = await agent.app.bsky.feed.getPosts({ - uris: [replyTo], +async function resolveReply(appviewClient: Client, replyTo: string) { + const data = await appviewClient.call(app.bsky.feed.getPosts, { + uris: [replyTo as AtUriString], }) const parentPost = data.posts[0] if (!parentPost) { @@ -237,14 +237,9 @@ async function resolveReply(agent: AtpAgent, replyTo: string) { uri: parentPost.uri, cid: parentPost.cid, } - let rootRef = parentRef + let rootRef: com.atproto.repo.strongRef.Main = parentRef - if ( - bsky.dangerousIsType( - parentPost.record, - AppBskyFeedPost.isRecord, - ) - ) { + if (bsky.isType(app.bsky.feed.post, parentPost.record)) { if (parentPost.record.reply) { rootRef = parentPost.record.reply.root } @@ -258,21 +253,14 @@ async function resolveReply(agent: AtpAgent, replyTo: string) { async function resolveEmbed( agent: AtpAgent, + pdsClient: Client, queryClient: QueryClient, draft: PostDraft, onStateChange: ((state: string) => void) | undefined, -): Promise< - | $Typed - | $Typed - | $Typed - | $Typed - | $Typed - | $Typed - | undefined -> { +): Promise { if (draft.embed.quote) { const [resolvedMedia, resolvedQuote] = await Promise.all([ - resolveMedia(agent, queryClient, draft.embed, onStateChange), + resolveMedia(agent, pdsClient, queryClient, draft.embed, onStateChange), resolveRecord(agent, queryClient, draft.embed.quote.uri), ]) if (resolvedMedia) { @@ -292,6 +280,7 @@ async function resolveEmbed( } const resolvedMedia = await resolveMedia( agent, + pdsClient, queryClient, draft.embed, onStateChange, @@ -308,7 +297,12 @@ async function resolveEmbed( if (resolvedLink.type === 'record') { return { $type: 'app.bsky.embed.record', - record: resolvedLink.record, + /* + * `resolve.ts` is still legacy-typed - its strong refs and URIs carry + * unbranded strings. Assert at the boundary until it moves to the + * clients. + */ + record: resolvedLink.record as com.atproto.repo.strongRef.Main, } } } @@ -317,14 +311,15 @@ async function resolveEmbed( async function resolveMedia( agent: AtpAgent, + pdsClient: Client, queryClient: QueryClient, embedDraft: EmbedDraft, onStateChange: ((state: string) => void) | undefined, ): Promise< - | $Typed - | $Typed - | $Typed - | $Typed + | $Typed + | $Typed + | $Typed + | $Typed | undefined > { if (embedDraft.media?.type === 'images') { @@ -333,7 +328,7 @@ async function resolveMedia( count: imagesDraft.length, }) onStateChange?.(t`Uploading images...`) - const images: AppBskyEmbedImages.Image[] = await Promise.all( + const images: app.bsky.embed.images.Image[] = await Promise.all( imagesDraft.map(async (image, i) => { logger.debug(`Compressing image #${i}`) const {path, width, height, mime} = await compressImage( @@ -341,9 +336,9 @@ async function resolveMedia( IMAGE_SIZE_CONFIG_POSTS, ) logger.debug(`Uploading image #${i}`) - const res = await uploadBlob(agent, path, mime) + const res = await uploadBlob(pdsClient, path, mime) return { - image: res.data.blob, + image: res.blob, alt: image.alt, aspectRatio: {width, height}, } @@ -360,7 +355,7 @@ async function resolveMedia( count: imagesDraft.length, }) onStateChange?.(t`Uploading images...`) - const items: $Typed[] = await Promise.all( + const items: $Typed[] = await Promise.all( imagesDraft.map(async (image, i) => { logger.debug(`Compressing image #${i}`) const {path, width, height, mime} = await compressImage( @@ -368,10 +363,10 @@ async function resolveMedia( IMAGE_SIZE_CONFIG_POSTS, ) logger.debug(`Uploading image #${i}`) - const res = await uploadBlob(agent, path, mime) + const res = await uploadBlob(pdsClient, path, mime) return { $type: 'app.bsky.embed.gallery#image' as const, - image: res.data.blob, + image: res.blob, alt: image.alt, aspectRatio: {width, height}, } @@ -391,10 +386,10 @@ async function resolveMedia( videoDraft.captions .filter(caption => caption.lang !== '') .map(async caption => { - const {data} = await agent.uploadBlob(caption.file, { + const res = await pdsClient.uploadBlob(caption.file, { encoding: 'text/vtt', }) - return {lang: caption.lang, file: data.blob} + return {lang: caption.lang, file: res.body.blob} }), ) @@ -414,7 +409,11 @@ async function resolveMedia( return { $type: 'app.bsky.embed.video', - video: videoDraft.pendingPublish.blobRef, + /* + * The video pipeline still reads its blob off the legacy agent, so + * normalize it to the lex shape before it reaches the lex write. + */ + video: fromLegacyBlobRef(videoDraft.pendingPublish.blobRef), alt: videoDraft.altText || undefined, captions: captions.length === 0 ? undefined : captions, aspectRatio, @@ -429,17 +428,17 @@ async function resolveMedia( agent, gifDraft.gif, ) - let blob: BlobRef | undefined + let blob: app.bsky.embed.external.External['thumb'] if (resolvedGif.thumb) { onStateChange?.(t`Uploading link thumbnail...`) const {path, mime} = resolvedGif.thumb.source - const response = await uploadBlob(agent, path, mime) - blob = response.data.blob + const response = await uploadBlob(pdsClient, path, mime) + blob = response.blob } return { $type: 'app.bsky.embed.external', external: { - uri: resolvedGif.uri, + uri: resolvedGif.uri as UriString, title: resolvedGif.title, description: createGIFDescription(resolvedGif.title, gifDraft.alt), thumb: blob, @@ -453,21 +452,23 @@ async function resolveMedia( embedDraft.link.uri, ) if (resolvedLink.type === 'external') { - let blob: BlobRef | undefined + let blob: app.bsky.embed.external.External['thumb'] if (resolvedLink.thumb) { onStateChange?.(t`Uploading link thumbnail...`) const {path, mime} = resolvedLink.thumb.source - const response = await uploadBlob(agent, path, mime) - blob = response.data.blob + const response = await uploadBlob(pdsClient, path, mime) + blob = response.blob } return { $type: 'app.bsky.embed.external', external: { - uri: resolvedLink.uri, + uri: resolvedLink.uri as UriString, title: resolvedLink.title, description: resolvedLink.description, thumb: blob, - associatedRefs: resolvedLink.associatedRefs, + associatedRefs: resolvedLink.associatedRefs as + | com.atproto.repo.strongRef.Main[] + | undefined, }, } } @@ -478,7 +479,7 @@ async function resolveMedia( return { $type: 'app.bsky.embed.external', external: { - uri: resolvedLink.uri, + uri: resolvedLink.uri as UriString, title: resolvedLink.view.name, description: `${resolvedLink.view.memberCount}/${resolvedLink.view.memberLimit}`, }, @@ -488,101 +489,18 @@ async function resolveMedia( return undefined } +/* + * `resolve.ts` still resolves through the agent and returns legacy-typed refs; + * assert at the boundary until it moves to the clients. + */ async function resolveRecord( agent: AtpAgent, queryClient: QueryClient, uri: string, -): Promise { +): Promise { const resolvedLink = await fetchResolveLinkQuery(queryClient, agent, uri) if (resolvedLink.type !== 'record') { throw Error(t`Expected uri to resolve to a record`) } - return resolvedLink.record -} - -// The built-in hashing functions from multiformats (`multiformats/hashes/sha2`) -// are meant for Node.js, this is the cross-platform equivalent. -const mf_sha256 = Hasher.from({ - name: 'sha2-256', - code: 0x12, - encode: input => { - const digest = sha256.arrayBuffer(input) - return new Uint8Array(digest) - }, -}) - -async function computeCid(record: AppBskyFeedPost.Record): Promise { - /* - * Lazily loaded since it's only needed when posting a thread, and its - * `cborg` dependency is ~190KB that would otherwise be in the initial - * web bundle. - */ - const dcbor = await import('@ipld/dag-cbor') - // IMPORTANT: `prepareObject` prepares the record to be hashed by removing - // fields with undefined value, and converting BlobRef instances to the - // right IPLD representation. - const prepared = prepareForHashing(record) - // 1. Encode the record into DAG-CBOR format - const encoded = dcbor.encode(prepared) - // 2. Hash the record in SHA-256 (code 0x12) - const digest = await mf_sha256.digest(encoded) - // 3. Create a CIDv1, specifying DAG-CBOR as content (code 0x71) - const cid = CID.createV1(0x71, digest) - // 4. Get the Base32 representation of the CID (`b` prefix) - return cid.toString() -} - -// Returns a transformed version of the object for use in DAG-CBOR. -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function prepareForHashing(v: any): any { - // IMPORTANT: BlobRef#ipld() returns the correct object we need for hashing, - // the API client will convert this for you but we're hashing in the client, - // so we need it *now*. - if (v instanceof BlobRef) { - return v.ipld() - } - - // Walk through arrays - if (Array.isArray(v)) { - let pure = true - const mapped = v.map(value => { - if (value !== (value = prepareForHashing(value))) { - pure = false - } - return value - }) - return pure ? v : mapped - } - - // Walk through plain objects - if (isPlainObject(v)) { - const obj: Record = {} - let pure = true - for (const key in v) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - let value = v[key] - // `value` is undefined - if (value === undefined) { - pure = false - continue - } - // `prepareObject` returned a value that's different from what we had before - if (value !== (value = prepareForHashing(value))) { - pure = false - } - obj[key] = value - } - // Return as is if we haven't needed to tamper with anything - return pure ? v : obj - } - return v -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function isPlainObject(v: any): boolean { - if (typeof v !== 'object' || v === null) { - return false - } - const proto = Object.getPrototypeOf(v) - return proto === Object.prototype || proto === null + return resolvedLink.record as com.atproto.repo.strongRef.Main } diff --git a/src/lib/api/legacy-blob.ts b/src/lib/api/legacy-blob.ts new file mode 100644 index 0000000000..a98e375d20 --- /dev/null +++ b/src/lib/api/legacy-blob.ts @@ -0,0 +1,31 @@ +import {BlobRef} from '@atproto/api' +import {type BlobRef as LexBlobRef} from '@atproto/lex' + +/** + * Bridge a lex blob ref (the plain-JSON `{$type: 'blob', ref, mimeType, size}` + * that {@link uploadBlob} now returns) back to the legacy `BlobRef` class + * instance. + * + * Only needed where a blob is handed to a legacy agent write: the legacy + * lexicon blob validator checks `value instanceof BlobRef`, so a plain lex + * blob fails validation, and the legacy serializer would put the wrong shape + * on the wire. Drop each call as its write moves to the lex client. + */ +export function toLegacyBlobRef(blob: LexBlobRef): BlobRef { + return BlobRef.fromJsonRef(blob as Parameters[0]) +} + +/** + * Normalize a legacy `BlobRef` class instance to the plain-JSON lex blob shape. + * + * Required for any blob that reaches a lex write: the lex serializer walks + * plain objects, so a class instance goes on the wire with its internal + * `original` field and no `$type`. `ipld()` yields exactly the lex shape, and + * hashes identically (see `computeCid.test.ts` case 2b). + * + * Only the video pipeline still needs this - it reads its blob off the legacy + * agent (`app.bsky.video.getJobStatus`). Drop it when the video client moves. + */ +export function fromLegacyBlobRef(blob: BlobRef): LexBlobRef { + return blob.ipld() +} diff --git a/src/lib/api/upload-blob.ts b/src/lib/api/upload-blob.ts index 0bee39b9b1..c679e15d84 100644 --- a/src/lib/api/upload-blob.ts +++ b/src/lib/api/upload-blob.ts @@ -1,38 +1,62 @@ import {copyAsync} from 'expo-file-system/legacy' -import {type AtpAgent, type ComAtprotoRepoUploadBlob} from '@atproto/api' +import {type BlobRef, type Client, type EncodingString} from '@atproto/lex' import {safeDeleteAsync} from '#/lib/media/manip' /** - * @param encoding Allows overriding the blob's type + * The blob-upload response body: `{blob}`. lex `Client.uploadBlob` returns the + * full XRPC response, so this helper unwraps `res.body` for callers. + */ +type UploadBlobResult = {blob: BlobRef} + +/** + * @param encoding Allows overriding the blob's type. Passed as the lex upload + * option (NEVER a content-type header - lex-client throws if the encoding is + * set via headers). */ export async function uploadBlob( - agent: AtpAgent, + client: Client, input: string | Blob, encoding?: string, -): Promise { +): Promise { if (typeof input === 'string' && input.startsWith('file:')) { const blob = await asBlob(input) - return agent.uploadBlob(blob, {encoding}) + return uploadBlobResult(client, blob, encoding) } if (typeof input === 'string' && input.startsWith('/')) { const blob = await asBlob(`file://${input}`) - return agent.uploadBlob(blob, {encoding}) + return uploadBlobResult(client, blob, encoding) } if (typeof input === 'string' && input.startsWith('data:')) { const blob = await fetch(input).then(r => r.blob()) - return agent.uploadBlob(blob, {encoding}) + return uploadBlobResult(client, blob, encoding) } if (input instanceof Blob) { - return agent.uploadBlob(input, {encoding}) + return uploadBlobResult(client, input, encoding) } throw new TypeError(`Invalid uploadBlob input: ${typeof input}`) } +async function uploadBlobResult( + client: Client, + blob: Blob, + encoding?: string, +): Promise { + const res = await client.uploadBlob(blob, { + /* + * The lex encoding option is a branded mime string + * (`${string}/${string}`); callers pass a plain mime string, so assert the + * brand here. + */ + encoding: encoding as EncodingString | undefined, + }) + return {blob: res.body.blob} +} + async function asBlob(uri: string): Promise { return withSafeFile(uri, async safeUri => { // Note diff --git a/src/lib/api/upload-blob.web.ts b/src/lib/api/upload-blob.web.ts index d74e834647..0a58ad42f1 100644 --- a/src/lib/api/upload-blob.web.ts +++ b/src/lib/api/upload-blob.web.ts @@ -1,28 +1,42 @@ -import {type AtpAgent, type ComAtprotoRepoUploadBlob} from '@atproto/api' +import {type BlobRef, type Client, type EncodingString} from '@atproto/lex' + +/** + * The blob-upload response body: `{blob}`. lex `Client.uploadBlob` returns the + * full XRPC response, so this helper unwraps `res.body` for callers. + */ +type UploadBlobResult = {blob: BlobRef} /** * @note It is recommended, on web, to use the `file` instance of the file * selector input element, rather than a `data:` URL, to avoid * loading the file into memory. `File` extends `Blob` "file" instances can * be passed directly to this function. + * + * @param encoding Passed as the lex upload option (NEVER a content-type header + * - lex-client throws if the encoding is set via headers). */ export async function uploadBlob( - agent: AtpAgent, + client: Client, input: string | Blob, encoding?: string, -): Promise { +): Promise { + /* + * The lex encoding option is a branded mime string (`${string}/${string}`); + * callers pass a plain mime string, so assert the brand here. + */ + const enc = encoding as EncodingString | undefined if ( typeof input === 'string' && (input.startsWith('data:') || input.startsWith('blob:')) ) { const blob = await fetch(input).then(r => r.blob()) - return agent.uploadBlob(blob, {encoding}) + const res = await client.uploadBlob(blob, {encoding: enc}) + return {blob: res.body.blob} } if (input instanceof Blob) { - return agent.uploadBlob(input, { - encoding, - }) + const res = await client.uploadBlob(input, {encoding: enc}) + return {blob: res.body.blob} } throw new TypeError(`Invalid uploadBlob input: ${typeof input}`) diff --git a/src/screens/Onboarding/StepFinished/index.tsx b/src/screens/Onboarding/StepFinished/index.tsx index 6f1dea3a4f..706a160003 100644 --- a/src/screens/Onboarding/StepFinished/index.tsx +++ b/src/screens/Onboarding/StepFinished/index.tsx @@ -15,6 +15,7 @@ import {Trans} from '@lingui/react/macro' import {useQueryClient} from '@tanstack/react-query' import {uploadBlob} from '#/lib/api' +import {toLegacyBlobRef} from '#/lib/api/legacy-blob' import { BSKY_APP_ACCOUNT_DID, DISCOVER_SAVED_FEED, @@ -153,7 +154,7 @@ export function StepFinished() { const {imageUri, imageMime} = profileStepResults const blobPromise = imageUri && imageMime - ? uploadBlob(agent, imageUri, imageMime) + ? uploadBlob(pdsClient, imageUri, imageMime) : undefined await agent.upsertProfile(async existing => { @@ -161,8 +162,8 @@ export function StepFinished() { if (blobPromise) { const res = await blobPromise - if (res.data.blob) { - next.avatar = res.data.blob + if (res.blob) { + next.avatar = toLegacyBlobRef(res.blob) } } diff --git a/src/state/queries/list.ts b/src/state/queries/list.ts index ec551bc8e5..eef0dac585 100644 --- a/src/state/queries/list.ts +++ b/src/state/queries/list.ts @@ -1,5 +1,5 @@ import {type AppBskyGraphDefs} from '@atproto/api' -import {type $Typed, type Client, type l} from '@atproto/lex' +import {type $Typed, type Client} from '@atproto/lex' import { type AtIdentifierString, AtUri, @@ -56,7 +56,6 @@ export interface ListCreateMutateParams { export function useListCreateMutation() { const {currentAccount} = useSession() const queryClient = useQueryClient() - const agent = useAgent() const appviewClient = useAppviewClient() const pdsClient = usePdsClient() return useMutation<{uri: string; cid: string}, Error, ListCreateMutateParams>( @@ -86,12 +85,8 @@ export function useListCreateMutation() { createdAt: toDatetimeString(new Date()), } if (avatar) { - const blobRes = await uploadBlob(agent, avatar.path, avatar.mime) - /* - * `uploadBlob` still returns the legacy `BlobRef` class instance; - * it moves to the client with the rest of the blob pipeline. - */ - record.avatar = blobRes.data.blob as unknown as l.BlobRef + const blobRes = await uploadBlob(pdsClient, avatar.path, avatar.mime) + record.avatar = blobRes.blob } const res = await pdsClient.create(app.bsky.graph.list, record) @@ -120,7 +115,6 @@ export interface ListMetadataMutateParams { } export function useListMetadataMutation() { const {currentAccount} = useSession() - const agent = useAgent() const appviewClient = useAppviewClient() const pdsClient = usePdsClient() const queryClient = useQueryClient() @@ -149,8 +143,8 @@ export function useListMetadataMutation() { record.description = description record.descriptionFacets = descriptionFacets if (avatar) { - const blobRes = await uploadBlob(agent, avatar.path, avatar.mime) - record.avatar = blobRes.data.blob as unknown as l.BlobRef + const blobRes = await uploadBlob(pdsClient, avatar.path, avatar.mime) + record.avatar = blobRes.blob } else if (avatar === null) { record.avatar = undefined } diff --git a/src/state/queries/profile.ts b/src/state/queries/profile.ts index dea8d35eea..ae5d4b1ad6 100644 --- a/src/state/queries/profile.ts +++ b/src/state/queries/profile.ts @@ -7,7 +7,6 @@ import { type AppBskyGraphGetFollows, type AtpAgent, AtUri, - type ComAtprotoRepoUploadBlob, type Un$Typed, } from '@atproto/api' import { @@ -25,6 +24,7 @@ import { } from '@tanstack/react-query' import {uploadBlob} from '#/lib/api' +import {toLegacyBlobRef} from '#/lib/api/legacy-blob' import {until} from '#/lib/async/until' import {useToggleMutationQueue} from '#/lib/hooks/useToggleMutationQueue' import {updateProfileShadow} from '#/state/cache/profile-shadow' @@ -149,6 +149,7 @@ interface ProfileUpdateParams { export function useProfileUpdateMutation() { const queryClient = useQueryClient() const agent = useAgent() + const pdsClient = usePdsClient() const updateProfileVerificationCache = useUpdateProfileVerificationCache() return useMutation({ mutationFn: async ({ @@ -158,22 +159,18 @@ export function useProfileUpdateMutation() { newUserBanner, checkCommitted, }) => { - let newUserAvatarPromise: - | Promise - | undefined + let newUserAvatarPromise: ReturnType | undefined if (newUserAvatar) { newUserAvatarPromise = uploadBlob( - agent, + pdsClient, newUserAvatar.path, newUserAvatar.mime, ) } - let newUserBannerPromise: - | Promise - | undefined + let newUserBannerPromise: ReturnType | undefined if (newUserBanner) { newUserBannerPromise = uploadBlob( - agent, + pdsClient, newUserBanner.path, newUserBanner.mime, ) @@ -191,13 +188,13 @@ export function useProfileUpdateMutation() { } if (newUserAvatarPromise) { const res = await newUserAvatarPromise - next.avatar = res.data.blob + next.avatar = toLegacyBlobRef(res.blob) } else if (newUserAvatar === null) { next.avatar = undefined } if (newUserBannerPromise) { const res = await newUserBannerPromise - next.banner = res.data.blob + next.banner = toLegacyBlobRef(res.blob) } else if (newUserBanner === null) { next.banner = undefined } diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index c2e651556d..1388ce0c45 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -96,7 +96,12 @@ import { import {usePreferencesQuery} from '#/state/queries/preferences' import {useProfileQuery} from '#/state/queries/profile' import {resolveLinkQueryOptions} from '#/state/queries/resolve-link' -import {useAgent, useAppviewClient, useSession} from '#/state/session' +import { + useAgent, + useAppviewClient, + usePdsClient, + useSession, +} from '#/state/session' import {useComposerControls} from '#/state/shell/composer' import {type ComposerOpts, type OnPostSuccessData} from '#/state/shell/composer' import {CharProgress} from '#/view/com/composer/char-progress/CharProgress' @@ -277,6 +282,7 @@ export const ComposePost = ({ : VIDEO_MAX_DURATION_MS const agent = useAgent() const client = useAppviewClient() + const pdsClient = usePdsClient() const queryClient = useQueryClient() const currentDid = currentAccount!.did const {closeComposer} = useComposerControls() @@ -1084,6 +1090,7 @@ export const ComposePost = ({ onStateChange: setPublishingStage, langs: currentLanguages, appviewClient: client, + pdsClient, }) ).uris[0] @@ -1279,6 +1286,7 @@ export const ComposePost = ({ ax, agent, client, + pdsClient, canPost, isPublishing, currentLanguages, From a4f2811f39f8fbbb515dc249f2c95e81918fbd18 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 13 Aug 2026 22:26:20 +0300 Subject: [PATCH 22/32] [SDK] Add refreshSession and migrate the session-pinned infra (#11381) Co-authored-by: Claude Fable 5 --- oxlint-suppressions.json | 3 - src/ageAssurance/useBeginAgeAssurance.ts | 32 +-- .../ageAssurance/AgeAssuranceInitDialog.tsx | 59 ++--- .../EmailDialog/data/useConfirmEmail.ts | 10 +- .../EmailDialog/data/useManageEmail2FA.ts | 10 +- .../EmailDialog/data/useUpdateEmail.ts | 31 ++- src/features/liveNow/index.tsx | 5 +- src/lib/api/index.ts | 8 +- src/lib/api/legacy-blob.ts | 15 -- src/lib/api/resolve.ts | 9 +- src/lib/constants.ts | 11 +- src/lib/link-meta/link-meta.ts | 7 +- src/lib/media/video/multipart/types.ts | 6 +- src/lib/media/video/multipart/upload.ts | 32 ++- src/lib/media/video/upload.shared.ts | 50 +++-- src/lib/media/video/upload.ts | 20 +- src/lib/media/video/upload.web.ts | 22 +- src/lib/media/video/util.ts | 24 ++- src/lib/notifications/notifications.ts | 49 +++-- src/screens/Deactivated.tsx | 39 ++-- .../components/ChangeHandleDialog.tsx | 10 +- .../components/DisableEmail2FADialog.tsx | 24 ++- .../Settings/components/ExportCarDialog.tsx | 51 +++-- src/screens/SignupQueued.tsx | 31 +-- .../provider-refresh-session-test.tsx | 204 ++++++++++++++++++ src/state/session/__tests__/session-test.ts | 9 +- src/state/session/index.tsx | 47 ++++ src/state/session/reducer.ts | 14 +- src/state/session/types.ts | 13 ++ src/state/session/util.ts | 54 +++-- src/view/com/composer/Composer.tsx | 18 +- src/view/com/composer/state/video.ts | 27 ++- 32 files changed, 676 insertions(+), 268 deletions(-) create mode 100644 src/state/session/__tests__/provider-refresh-session-test.tsx diff --git a/oxlint-suppressions.json b/oxlint-suppressions.json index 5ffe12abbc..1d22db2747 100644 --- a/oxlint-suppressions.json +++ b/oxlint-suppressions.json @@ -848,9 +848,6 @@ }, "typescript/no-misused-promises": { "count": 1 - }, - "typescript/no-unsafe-member-access": { - "count": 1 } }, "src/screens/E2E/SharedPreferencesTesterScreen.tsx": { diff --git a/src/ageAssurance/useBeginAgeAssurance.ts b/src/ageAssurance/useBeginAgeAssurance.ts index 28a4591747..e6e79e348d 100644 --- a/src/ageAssurance/useBeginAgeAssurance.ts +++ b/src/ageAssurance/useBeginAgeAssurance.ts @@ -1,5 +1,4 @@ import {Platform} from 'react-native' -import {type AppBskyAgeassuranceBegin, AtpAgent} from '@atproto/api' import {useMutation} from '@tanstack/react-query' import {wait} from '#/lib/async/wait' @@ -9,26 +8,28 @@ import { PUBLIC_APPVIEW_DID, } from '#/lib/constants' import {isNetworkError} from '#/lib/hooks/useCleanError' -import {useAgent} from '#/state/session' +import {createLexClient} from '#/lib/lexClient' +import {usePdsClient} from '#/state/session' import {usePatchAgeAssuranceServerState} from '#/ageAssurance' import {logger} from '#/ageAssurance/logger' import {useAnalytics} from '#/analytics' import {BLUESKY_PROXY_DID} from '#/env' import {useGeolocation} from '#/geolocation' +import {app, com} from '#/lexicons' const IS_DEV_ENV = BLUESKY_PROXY_DID !== PUBLIC_APPVIEW_DID const APPVIEW = IS_DEV_ENV ? DEV_ENV_APPVIEW : PUBLIC_APPVIEW export function useBeginAgeAssurance() { const ax = useAnalytics() - const agent = useAgent() + const pdsClient = usePdsClient() const geolocation = useGeolocation() const patchAgeAssuranceStateResponse = usePatchAgeAssuranceServerState() return useMutation({ async mutationFn( props: Omit< - AppBskyAgeassuranceBegin.InputSchema, + app.bsky.ageassurance.begin.$InputBody, 'countryCode' | 'regionCode' >, ) { @@ -38,17 +39,22 @@ export function useBeginAgeAssurance() { throw new Error(`Geolocation not available, cannot init age assurance.`) } - const { - data: {token}, - } = await agent.com.atproto.server.getServiceAuth({ + const {token} = await pdsClient.call(com.atproto.server.getServiceAuth, { aud: BLUESKY_PROXY_DID, lxm: `app.bsky.ageassurance.begin`, }) - const appView = new AtpAgent({service: APPVIEW}) - appView.sessionManager.session = {...agent.session!} - appView.sessionManager.session.accessJwt = token - appView.sessionManager.session.refreshJwt = '' + /* + * A single-use client scoped to the service-auth token: it has no session, + * so nothing can refresh it, and the request goes straight to the appview + * with the token as a static `authorization` header. A raw client is + * allowed to preset that header where a session-backed one is not, which + * also makes the old `refreshJwt = ''` clone unnecessary. + */ + const scopedClient = createLexClient({ + service: APPVIEW, + headers: {authorization: `Bearer ${token}`}, + }) ax.metric('ageAssurance:api:begin', { platform: Platform.OS, @@ -60,9 +66,9 @@ export function useBeginAgeAssurance() { * 2s wait is good actually. Email sending takes a hot sec and this helps * ensure the email is ready for the user once they open their inbox. */ - const {data} = await wait( + const data = await wait( 2e3, - appView.app.bsky.ageassurance.begin({ + scopedClient.call(app.bsky.ageassurance.begin, { ...props, countryCode, regionCode, diff --git a/src/components/ageAssurance/AgeAssuranceInitDialog.tsx b/src/components/ageAssurance/AgeAssuranceInitDialog.tsx index 9d74b0c6dc..052c3a4731 100644 --- a/src/components/ageAssurance/AgeAssuranceInitDialog.tsx +++ b/src/components/ageAssurance/AgeAssuranceInitDialog.tsx @@ -1,6 +1,6 @@ import {useState} from 'react' import {View} from 'react-native' -import {XRPCError} from '@atproto/api' +import {XrpcResponseError} from '@atproto/lex' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' @@ -14,6 +14,7 @@ import { import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo' import {useTLDs} from '#/lib/hooks/useTLDs' import {isEmailMaybeInvalid} from '#/lib/strings/email' +import {matchXrpcError} from '#/lib/xrpc-error' import {type AppLanguage} from '#/locale/languages' import {useLanguagePrefs} from '#/state/preferences' import {useSession} from '#/state/session' @@ -33,6 +34,7 @@ import {Text} from '#/components/Typography' import {useAgeAssurance} from '#/ageAssurance' import {useBeginAgeAssurance} from '#/ageAssurance/useBeginAgeAssurance' import {useAnalytics} from '#/analytics' +import {app} from '#/lexicons' export {useDialogControl} from '#/components/Dialog/context' @@ -139,30 +141,37 @@ function Inner() { msg`Something went wrong, please try again`, ) - if (e instanceof XRPCError) { - if (e.error === 'InvalidEmail') { - error = _( - msg`Please enter a valid, non-temporary email address. You may need to access this email in the future.`, - ) - ax.metric('ageAssurance:initDialogError', {code: 'InvalidEmail'}) - } else if (e.error === 'DidTooLong') { - error = ( - <> - - We're having issues initializing the age assurance process for - your account. Please{' '} - - contact support - {' '} - for assistance. - - - ) - ax.metric('ageAssurance:initDialogError', {code: 'DidTooLong'}) - } else { - ax.metric('ageAssurance:initDialogError', {code: 'other'}) + if (e instanceof XrpcResponseError) { + switch (matchXrpcError(e, app.bsky.ageassurance.begin)) { + case 'InvalidEmail': + error = _( + msg`Please enter a valid, non-temporary email address. You may need to access this email in the future.`, + ) + ax.metric('ageAssurance:initDialogError', {code: 'InvalidEmail'}) + break + case 'DidTooLong': + error = ( + <> + + We're having issues initializing the age assurance process for + your account. Please{' '} + + contact support + {' '} + for assistance. + + + ) + ax.metric('ageAssurance:initDialogError', {code: 'DidTooLong'}) + break + default: + /* + * An undeclared code keeps the generic message rather than surfacing + * the server's text, as the old `e.error` fallthrough did. + */ + ax.metric('ageAssurance:initDialogError', {code: 'other'}) } } else { const {clean, raw} = cleanError(e) diff --git a/src/components/dialogs/EmailDialog/data/useConfirmEmail.ts b/src/components/dialogs/EmailDialog/data/useConfirmEmail.ts index 67466be926..00472be52f 100644 --- a/src/components/dialogs/EmailDialog/data/useConfirmEmail.ts +++ b/src/components/dialogs/EmailDialog/data/useConfirmEmail.ts @@ -1,13 +1,15 @@ import {useMutation} from '@tanstack/react-query' -import {useAgent, useSession} from '#/state/session' +import {usePdsClient, useSession, useSessionApi} from '#/state/session' +import {com} from '#/lexicons' export function useConfirmEmail({ onSuccess, onError, }: {onSuccess?: () => void; onError?: () => void} = {}) { - const agent = useAgent() + const pdsClient = usePdsClient() const {currentAccount} = useSession() + const {refreshSession} = useSessionApi() return useMutation({ mutationFn: async ({token}: {token: string}) => { @@ -15,12 +17,12 @@ export function useConfirmEmail({ throw new Error('No email found for the current account') } - await agent.com.atproto.server.confirmEmail({ + await pdsClient.call(com.atproto.server.confirmEmail, { email: currentAccount.email.trim(), token: token.trim(), }) // will update session state at root of app - await agent.resumeSession(agent.session!) + await refreshSession() }, onSuccess, onError, diff --git a/src/components/dialogs/EmailDialog/data/useManageEmail2FA.ts b/src/components/dialogs/EmailDialog/data/useManageEmail2FA.ts index 358bf86544..d00e487474 100644 --- a/src/components/dialogs/EmailDialog/data/useManageEmail2FA.ts +++ b/src/components/dialogs/EmailDialog/data/useManageEmail2FA.ts @@ -1,10 +1,12 @@ import {useMutation} from '@tanstack/react-query' -import {useAgent, useSession} from '#/state/session' +import {usePdsClient, useSession, useSessionApi} from '#/state/session' +import {com} from '#/lexicons' export function useManageEmail2FA() { - const agent = useAgent() + const pdsClient = usePdsClient() const {currentAccount} = useSession() + const {refreshSession} = useSessionApi() return useMutation({ mutationFn: async ({ @@ -17,13 +19,13 @@ export function useManageEmail2FA() { throw new Error('No email found for the current account') } - await agent.com.atproto.server.updateEmail({ + await pdsClient.call(com.atproto.server.updateEmail, { email: currentAccount.email, emailAuthFactor: enabled, token, }) // will update session state at root of app - await agent.resumeSession(agent.session!) + await refreshSession() }, }) } diff --git a/src/components/dialogs/EmailDialog/data/useUpdateEmail.ts b/src/components/dialogs/EmailDialog/data/useUpdateEmail.ts index 2ec1eb6dc2..7293227f59 100644 --- a/src/components/dialogs/EmailDialog/data/useUpdateEmail.ts +++ b/src/components/dialogs/EmailDialog/data/useUpdateEmail.ts @@ -1,19 +1,26 @@ +import {type Client} from '@atproto/lex' import {useMutation} from '@tanstack/react-query' -import {useAgent} from '#/state/session' +import {usePdsClient, useSessionApi} from '#/state/session' import {useRequestEmailUpdate} from '#/components/dialogs/EmailDialog/data/useRequestEmailUpdate' +import {com} from '#/lexicons' async function updateEmailAndRefreshSession( - agent: ReturnType, + pdsClient: Client, + refreshSession: () => Promise, email: string, token?: string, ) { - await agent.com.atproto.server.updateEmail({email: email.trim(), token}) - await agent.resumeSession(agent.session!) + await pdsClient.call(com.atproto.server.updateEmail, { + email: email.trim(), + token, + }) + await refreshSession() } export function useUpdateEmail() { - const agent = useAgent() + const pdsClient = usePdsClient() + const {refreshSession} = useSessionApi() const {mutateAsync: requestEmailUpdate} = useRequestEmailUpdate() return useMutation< @@ -23,7 +30,12 @@ export function useUpdateEmail() { >({ mutationFn: async ({email, token}: {email: string; token?: string}) => { if (token) { - await updateEmailAndRefreshSession(agent, email, token) + await updateEmailAndRefreshSession( + pdsClient, + refreshSession, + email, + token, + ) return { status: 'success', } @@ -34,7 +46,12 @@ export function useUpdateEmail() { status: 'tokenRequired', } } else { - await updateEmailAndRefreshSession(agent, email, token) + await updateEmailAndRefreshSession( + pdsClient, + refreshSession, + email, + token, + ) return { status: 'success', } diff --git a/src/features/liveNow/index.tsx b/src/features/liveNow/index.tsx index 525e189ace..a9a14fc2d3 100644 --- a/src/features/liveNow/index.tsx +++ b/src/features/liveNow/index.tsx @@ -23,7 +23,7 @@ import { useMaybeProfileShadow, } from '#/state/cache/profile-shadow' import {useModerationOpts} from '#/state/preferences/moderation-opts' -import {useAgent, usePdsClient, useSession} from '#/state/session' +import {usePdsClient, useSession} from '#/state/session' import {useTickEveryMinute} from '#/state/shell' import {useDialogContext} from '#/components/Dialog' import * as Toast from '#/components/Toast' @@ -194,7 +194,6 @@ export function useLiveLinkMetaQuery(url: string | null) { const liveNowConfig = useLiveNowConfig() const {_} = useLingui() - const agent = useAgent() return useQuery({ enabled: !!url, queryKey: ['link-meta', url], @@ -212,7 +211,7 @@ export function useLiveLinkMetaQuery(url: string | null) { ) } - return await getLinkMeta(agent, url) + return await getLinkMeta(url) }, }) } diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts index e52d4ec4fc..f13c227857 100644 --- a/src/lib/api/index.ts +++ b/src/lib/api/index.ts @@ -32,7 +32,6 @@ import {app, com} from '#/lexicons' import * as bsky from '#/types/bsky' import {createGIFDescription} from '../gif-alt-text' import {computeCid} from './computeCid' -import {fromLegacyBlobRef} from './legacy-blob' import {uploadBlob} from './upload-blob' export {uploadBlob} @@ -410,10 +409,11 @@ async function resolveMedia( return { $type: 'app.bsky.embed.video', /* - * The video pipeline still reads its blob off the legacy agent, so - * normalize it to the lex shape before it reaches the lex write. + * The video blob is a plain lex blob from the video pipeline + * (getJobStatus, in composer state/video). Its structural shape matches + * the lexicon blob field and the CID hasher (see computeCid). */ - video: fromLegacyBlobRef(videoDraft.pendingPublish.blobRef), + video: videoDraft.pendingPublish.blobRef, alt: videoDraft.altText || undefined, captions: captions.length === 0 ? undefined : captions, aspectRatio, diff --git a/src/lib/api/legacy-blob.ts b/src/lib/api/legacy-blob.ts index a98e375d20..b60acadfe4 100644 --- a/src/lib/api/legacy-blob.ts +++ b/src/lib/api/legacy-blob.ts @@ -14,18 +14,3 @@ import {type BlobRef as LexBlobRef} from '@atproto/lex' export function toLegacyBlobRef(blob: LexBlobRef): BlobRef { return BlobRef.fromJsonRef(blob as Parameters[0]) } - -/** - * Normalize a legacy `BlobRef` class instance to the plain-JSON lex blob shape. - * - * Required for any blob that reaches a lex write: the lex serializer walks - * plain objects, so a class instance goes on the wire with its internal - * `original` field and no `$type`. `ipld()` yields exactly the lex shape, and - * hashes identically (see `computeCid.test.ts` case 2b). - * - * Only the video pipeline still needs this - it reads its blob off the legacy - * agent (`app.bsky.video.getJobStatus`). Drop it when the video client moves. - */ -export function fromLegacyBlobRef(blob: BlobRef): LexBlobRef { - return blob.ipld() -} diff --git a/src/lib/api/resolve.ts b/src/lib/api/resolve.ts index 476c0f1a20..67bc46284a 100644 --- a/src/lib/api/resolve.ts +++ b/src/lib/api/resolve.ts @@ -184,7 +184,7 @@ export async function resolveLink( view: res.data.starterPack, } } - return resolveExternal(agent, uri) + return resolveExternal(uri) // Forked from useGetPost. TODO: move into RQ. async function getPost({uri}: {uri: string}) { @@ -258,11 +258,8 @@ function getFileSlug(url: string | undefined): string | undefined { return dotIndex > 0 ? filename.slice(0, dotIndex) : undefined } -async function resolveExternal( - agent: AtpAgent, - uri: string, -): Promise { - const result = await getLinkMeta(agent, uri) +async function resolveExternal(uri: string): Promise { + const result = await getLinkMeta(uri) return { type: 'external', uri: result.url, diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 156712c809..9fd7d12b47 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -279,9 +279,14 @@ export const DM_SERVICE_HEADERS = { 'atproto-proxy': `${CHAT_PROXY_DID}#bsky_chat`, } -export const BLUESKY_NOTIF_SERVICE_HEADERS = { - 'atproto-proxy': `${BLUESKY_PROXY_DID}#bsky_notif`, -} +/** + * The notification service's proxy target, in the `did#service_id` form a lex + * client's per-call `service` option takes. Passing it emits `atproto-proxy: + * ` on that one request, which is what routes push registration to + * the notification service (replaces the old + * `BLUESKY_NOTIF_SERVICE_HEADERS`). + */ +export const NOTIF_SERVICE: Service = `${BLUESKY_PROXY_DID}#bsky_notif` export const webLinks = { tos: `https://bsky.social/about/support/tos`, diff --git a/src/lib/link-meta/link-meta.ts b/src/lib/link-meta/link-meta.ts index c282a0c4c3..ee96d6db43 100644 --- a/src/lib/link-meta/link-meta.ts +++ b/src/lib/link-meta/link-meta.ts @@ -1,4 +1,4 @@ -import {type AppBskyEmbedExternal, type AtpAgent} from '@atproto/api' +import {type AppBskyEmbedExternal} from '@atproto/api' import {LINK_META_PROXY} from '#/lib/constants' import {getGiphyMetaUri} from '#/lib/strings/embed-player' @@ -31,7 +31,6 @@ export interface LinkMeta { } export async function getLinkMeta( - agent: AtpAgent, url: string, timeout = 15e3, ): Promise { @@ -80,9 +79,7 @@ export async function getLinkMeta( try { const response = await fetch( - `${LINK_META_PROXY(agent.serviceUrl.toString() || '')}${encodeURIComponent( - url, - )}`, + `${LINK_META_PROXY('')}${encodeURIComponent(url)}`, {signal: controller.signal}, ) diff --git a/src/lib/media/video/multipart/types.ts b/src/lib/media/video/multipart/types.ts index 16817778c3..a0cfd216e3 100644 --- a/src/lib/media/video/multipart/types.ts +++ b/src/lib/media/video/multipart/types.ts @@ -1,3 +1,5 @@ +import {type app} from '#/lexicons' + /** * One part of a multipart upload. `partNumber` is 1-indexed to match the S3 * convention the backend uses. @@ -57,13 +59,13 @@ export type UploadStatusResponse = { expiresAt: string state: UploadState completedJobId?: string - jobStatus?: import('@atproto/api').AppBskyVideoDefs.JobStatus + jobStatus?: app.bsky.video.defs.JobStatus failureReason?: string } export type FinishUploadResponse = { completedJobId: string - jobStatus: import('@atproto/api').AppBskyVideoDefs.JobStatus + jobStatus: app.bsky.video.defs.JobStatus } export type AbortUploadResponse = Pick< diff --git a/src/lib/media/video/multipart/upload.ts b/src/lib/media/video/multipart/upload.ts index 2b45e6d2c6..d3e5210873 100644 --- a/src/lib/media/video/multipart/upload.ts +++ b/src/lib/media/video/multipart/upload.ts @@ -1,9 +1,10 @@ -import {type AppBskyVideoDefs, type AtpAgent} from '@atproto/api' +import {type Client} from '@atproto/lex' import {nanoid} from 'nanoid/non-secure' import {AbortError} from '#/lib/async/cancelable' import {type CompressedVideo} from '#/lib/media/video/types' import {shouldRetryError} from '#/lib/strings/errors' +import {type app} from '#/lexicons' import {getServiceAuthToken} from '../upload.shared' import {mimeToExt} from '../util' import { @@ -29,19 +30,22 @@ export class MultipartFallbackError extends Error {} export async function uploadVideoMultipart({ video, - agent, + client, + dispatchUrl, setProgress, signal, onStarted, }: { video: CompressedVideo - agent: AtpAgent + client: Client + /** The account's PDS/dispatch URL, for the uploadBlob service-auth token. */ + dispatchUrl: string | URL setProgress: (progress: number) => void signal: AbortSignal onStarted?: () => void -}): Promise { +}): Promise { throwIfAborted(signal) - const tokenProvider = createTokenProvider(agent, signal) + const tokenProvider = createTokenProvider(client, dispatchUrl, signal) const token = await tokenProvider.get() const name = `${nanoid(12)}.${mimeToExt(video.mimeType)}` let session @@ -134,7 +138,7 @@ async function finishAndRecover({ getToken: (forceRefresh?: boolean) => Promise signal: AbortSignal resendMissingParts: (receivedPartNumbers: number[]) => Promise -}): Promise { +}): Promise { let createdFailures = 0 let forceTokenRefresh = true while (true) { @@ -224,7 +228,7 @@ async function abortThenFallbackOrResolve( jobId: string, token: string, cause: unknown, -): Promise { +): Promise { const result = await abortUploadWithRetry(jobId, token) if (result.state === 'aborted') { throw new MultipartFallbackError( @@ -264,7 +268,11 @@ async function abortUploadWithRetry(jobId: string, token: string) { throw lastError } -function createTokenProvider(agent: AtpAgent, signal: AbortSignal) { +function createTokenProvider( + client: Client, + dispatchUrl: string | URL, + signal: AbortSignal, +) { let token: string | undefined let expiresAt = 0 let refresh: Promise | undefined @@ -273,7 +281,7 @@ function createTokenProvider(agent: AtpAgent, signal: AbortSignal) { if (!forceRefresh && token && Date.now() < expiresAt - 60_000) return token if (!refresh) { const exp = Math.floor(Date.now() / 1000) + 60 * 30 - refresh = getServiceAuthTokenWithRetry(agent, exp, signal) + refresh = getServiceAuthTokenWithRetry(client, dispatchUrl, exp, signal) .then(nextToken => { token = nextToken expiresAt = exp * 1000 @@ -290,7 +298,8 @@ function createTokenProvider(agent: AtpAgent, signal: AbortSignal) { } async function getServiceAuthTokenWithRetry( - agent: AtpAgent, + client: Client, + dispatchUrl: string | URL, exp: number, signal: AbortSignal, ) { @@ -299,7 +308,8 @@ async function getServiceAuthTokenWithRetry( throwIfAborted(signal) try { return await getServiceAuthToken({ - agent, + client, + dispatchUrl, lxm: 'com.atproto.repo.uploadBlob', exp, }) diff --git a/src/lib/media/video/upload.shared.ts b/src/lib/media/video/upload.shared.ts index f8aaa1249b..9e8d99a185 100644 --- a/src/lib/media/video/upload.shared.ts +++ b/src/lib/media/video/upload.shared.ts @@ -1,44 +1,62 @@ -import {type AtpAgent} from '@atproto/api' +import {type Client} from '@atproto/lex' +import {type DidString, type NsidString} from '@atproto/syntax' import {type I18n} from '@lingui/core' import {msg} from '@lingui/core/macro' import {VIDEO_SERVICE_DID} from '#/lib/constants' import {UploadLimitError} from '#/lib/media/video/errors' import {getServiceAuthAudFromUrl} from '#/lib/strings/url-helpers' -import {createVideoAgent} from './util' +import {app, com} from '#/lexicons' +import {createVideoServiceClient} from './util' export async function getServiceAuthToken({ - agent, + client, + dispatchUrl, aud, lxm, exp, }: { - agent: AtpAgent + client: Client + /** + * The account's dispatch URL (the old `agent.dispatchUrl`: its PDS, falling + * back to the account service). Only needed when `aud` is omitted, so the + * default audience can be derived from the PDS host. A lex {@link Client} does + * not expose this - it resolves the PDS per request internally - so the caller, + * which holds the session, passes it in. + */ + dispatchUrl?: string | URL aud?: string - lxm: string + lxm: NsidString exp?: number }) { - const pdsAud = getServiceAuthAudFromUrl(agent.dispatchUrl) - if (!pdsAud) { - throw new Error('Agent does not have a PDS URL') + let resolvedAud = aud + if (!resolvedAud) { + if (!dispatchUrl) { + throw new Error('Missing service auth audience: no aud or dispatchUrl') + } + const pdsAud = getServiceAuthAudFromUrl(dispatchUrl) + if (!pdsAud) { + throw new Error('Agent does not have a PDS URL') + } + resolvedAud = pdsAud } - const {data: serviceAuth} = await agent.com.atproto.server.getServiceAuth({ - aud: aud ?? pdsAud, + const {token} = await client.call(com.atproto.server.getServiceAuth, { + aud: resolvedAud as DidString, lxm, exp, }) - return serviceAuth.token + return token } -export async function getVideoUploadLimits(agent: AtpAgent, i18n: I18n) { +export async function getVideoUploadLimits(client: Client, i18n: I18n) { const token = await getServiceAuthToken({ - agent, + client, lxm: 'app.bsky.video.getUploadLimits', aud: VIDEO_SERVICE_DID, }) - const videoAgent = createVideoAgent() - const {data: limits} = await videoAgent.app.bsky.video - .getUploadLimits({}, {headers: {Authorization: `Bearer ${token}`}}) + const videoClient = createVideoServiceClient(token) + const limits = await videoClient + .call(app.bsky.video.getUploadLimits) .catch(err => { if (err instanceof Error) { throw new UploadLimitError(err.message) diff --git a/src/lib/media/video/upload.ts b/src/lib/media/video/upload.ts index b91ad7a153..287c8f09c6 100644 --- a/src/lib/media/video/upload.ts +++ b/src/lib/media/video/upload.ts @@ -1,5 +1,5 @@ import {createUploadTask, FileSystemUploadType} from 'expo-file-system/legacy' -import {type AppBskyVideoDefs, type AtpAgent} from '@atproto/api' +import {type Client} from '@atproto/lex' import {type I18n} from '@lingui/core' import {msg} from '@lingui/core/macro' import {nanoid} from 'nanoid/non-secure' @@ -11,13 +11,15 @@ import { type VideoUploadTransport, } from '#/lib/media/video/types' import {Features, features} from '#/analytics/features' +import {type app} from '#/lexicons' import {MultipartFallbackError, uploadVideoMultipart} from './multipart/upload' import {getServiceAuthToken, getVideoUploadLimits} from './upload.shared' import {createVideoEndpointUrl, mimeToExt} from './util' export async function uploadVideo({ video, - agent, + client, + dispatchUrl, did, setProgress, signal, @@ -25,7 +27,9 @@ export async function uploadVideo({ onTransport, }: { video: CompressedVideo - agent: AtpAgent + client: Client + /** The account's PDS/dispatch URL, for the uploadBlob service-auth token. */ + dispatchUrl: string | URL did: string setProgress: (progress: number) => void signal: AbortSignal @@ -35,13 +39,14 @@ export async function uploadVideo({ if (signal.aborted) { throw new AbortError() } - await getVideoUploadLimits(agent, i18n) + await getVideoUploadLimits(client, i18n) if (features.isOn(Features.VideoMultipartUploadEnable)) { try { return await uploadVideoMultipart({ video, - agent, + client, + dispatchUrl, setProgress, signal, onStarted: () => onTransport?.('multipart'), @@ -64,7 +69,8 @@ export async function uploadVideo({ throw new AbortError() } const token = await getServiceAuthToken({ - agent, + client, + dispatchUrl, lxm: 'com.atproto.repo.uploadBlob', exp: Date.now() / 1000 + 60 * 30, // 30 minutes }) @@ -91,7 +97,7 @@ export async function uploadVideo({ throw new Error('No response') } - const responseBody = JSON.parse(res.body) as AppBskyVideoDefs.JobStatus + const responseBody = JSON.parse(res.body) as app.bsky.video.defs.JobStatus if (!responseBody.jobId) { throw new ServerError( diff --git a/src/lib/media/video/upload.web.ts b/src/lib/media/video/upload.web.ts index cfefbd797d..90c10a0684 100644 --- a/src/lib/media/video/upload.web.ts +++ b/src/lib/media/video/upload.web.ts @@ -1,4 +1,4 @@ -import {type AppBskyVideoDefs, type AtpAgent} from '@atproto/api' +import {type Client} from '@atproto/lex' import {type I18n} from '@lingui/core' import {msg} from '@lingui/core/macro' import {nanoid} from 'nanoid/non-secure' @@ -10,13 +10,15 @@ import { type VideoUploadTransport, } from '#/lib/media/video/types' import {Features, features} from '#/analytics/features' +import {type app} from '#/lexicons' import {MultipartFallbackError, uploadVideoMultipart} from './multipart/upload' import {getServiceAuthToken, getVideoUploadLimits} from './upload.shared' import {createVideoEndpointUrl, mimeToExt} from './util' export async function uploadVideo({ video, - agent, + client, + dispatchUrl, did, setProgress, signal, @@ -24,7 +26,9 @@ export async function uploadVideo({ onTransport, }: { video: CompressedVideo - agent: AtpAgent + client: Client + /** The account's PDS/dispatch URL, for the uploadBlob service-auth token. */ + dispatchUrl: string | URL did: string setProgress: (progress: number) => void signal: AbortSignal @@ -34,13 +38,14 @@ export async function uploadVideo({ if (signal.aborted) { throw new AbortError() } - await getVideoUploadLimits(agent, i18n) + await getVideoUploadLimits(client, i18n) if (features.isOn(Features.VideoMultipartUploadEnable)) { try { return await uploadVideoMultipart({ video, - agent, + client, + dispatchUrl, setProgress, signal, onStarted: () => onTransport?.('multipart'), @@ -71,7 +76,8 @@ export async function uploadVideo({ throw new AbortError() } const token = await getServiceAuthToken({ - agent, + client, + dispatchUrl, lxm: 'com.atproto.repo.uploadBlob', exp: Date.now() / 1000 + 60 * 30, // 30 minutes }) @@ -80,7 +86,7 @@ export async function uploadVideo({ throw new AbortError() } const xhr = new XMLHttpRequest() - const res = await new Promise( + const res = await new Promise( (resolve, reject) => { xhr.upload.addEventListener('progress', e => { const progress = e.loaded / e.total @@ -92,7 +98,7 @@ export async function uploadVideo({ } else if (xhr.readyState === 4) { const uploadRes = JSON.parse( xhr.responseText, - ) as AppBskyVideoDefs.JobStatus + ) as app.bsky.video.defs.JobStatus resolve(uploadRes) } else { reject(new ServerError(i18n._(msg`Failed to upload video`))) diff --git a/src/lib/media/video/util.ts b/src/lib/media/video/util.ts index 236f0cff3e..7c9686a7f4 100644 --- a/src/lib/media/video/util.ts +++ b/src/lib/media/video/util.ts @@ -1,6 +1,5 @@ -import {AtpAgent} from '@atproto/api' - import {type SupportedMimeTypes, VIDEO_SERVICE} from '#/lib/constants' +import {createLexClient} from '#/lib/lexClient' export const createVideoEndpointUrl = ( route: string, @@ -16,12 +15,29 @@ export const createVideoEndpointUrl = ( return url.href } -export function createVideoAgent() { - return new AtpAgent({ +/** + * A non-refreshing single-use lex {@link Client} scoped to the video service and + * authenticated by a per-call service-auth token. It has no session, so nothing + * can refresh it: requests go straight to the video service with the token as a + * static `authorization` header, which a raw client - unlike a session-backed + * one - is allowed to preset. Mirrors the scoped client in + * `#/ageAssurance/useBeginAgeAssurance`. + */ +export function createVideoServiceClient(token: string) { + return createLexClient({ service: VIDEO_SERVICE, + headers: {authorization: `Bearer ${token}`}, }) } +/** + * An unauthenticated lex {@link Client} scoped to the video service, for public + * reads like `getJobStatus` polling. + */ +export function createTokenlessVideoServiceClient() { + return createLexClient({service: VIDEO_SERVICE}) +} + export function mimeToExt(mimeType: SupportedMimeTypes | (string & {})) { switch (mimeType) { case 'video/mp4': diff --git a/src/lib/notifications/notifications.ts b/src/lib/notifications/notifications.ts index 6351a8d8e1..5acc6ab06d 100644 --- a/src/lib/notifications/notifications.ts +++ b/src/lib/notifications/notifications.ts @@ -2,33 +2,47 @@ import {useCallback, useEffect} from 'react' import {Platform} from 'react-native' import * as Notifications from 'expo-notifications' import {getBadgeCountAsync, setBadgeCountAsync} from 'expo-notifications' -import {type AppBskyNotificationRegisterPush, type AtpAgent} from '@atproto/api' +import {type Client} from '@atproto/lex' import debounce from 'lodash.debounce' import { - BLUESKY_NOTIF_SERVICE_HEADERS, + NOTIF_SERVICE, PUBLIC_APPVIEW_DID, PUBLIC_STAGING_APPVIEW_DID, } from '#/lib/constants' import {logger as notyLogger} from '#/lib/notifications/util' import {isNetworkError} from '#/lib/strings/errors' -import {type SessionAccount, useAgent, useSession} from '#/state/session' +import {type SessionAccount, usePdsClient, useSession} from '#/state/session' import BackgroundNotificationHandler from '#/../modules/expo-background-notification-handler' import {useAgeAssurance} from '#/ageAssurance' import {useAnalytics} from '#/analytics' import {IS_DEV, IS_NATIVE} from '#/env' +import {app} from '#/lexicons' + +/** + * A resumed single-use account client paired with the account's service origin + * and handle. Produced by `createTemporaryClientsAndResume` (session util) and + * consumed by {@link unregisterPushToken}, which needs the service host to pick + * the appview DID and the handle for a debug log line without reaching into the + * session internals. + */ +export type TemporaryPushClient = { + client: Client + service: string + handle: string +} /** * @private * Registers the device's push notification token with the Bluesky server. */ async function _registerPushToken({ - agent, + client, currentAccount, token, extra = {}, }: { - agent: AtpAgent + client: Client currentAccount: SessionAccount token: Notifications.DevicePushToken extra?: { @@ -36,7 +50,7 @@ async function _registerPushToken({ } }) { try { - const payload: AppBskyNotificationRegisterPush.InputSchema = { + const payload: app.bsky.notification.registerPush.$InputBody = { serviceDid: currentAccount.service?.includes('staging') ? PUBLIC_STAGING_APPVIEW_DID : PUBLIC_APPVIEW_DID, @@ -48,8 +62,8 @@ async function _registerPushToken({ notyLogger.debug(`registerPushToken: registering`, {...payload}) - await agent.app.bsky.notification.registerPush(payload, { - headers: BLUESKY_NOTIF_SERVICE_HEADERS, + await client.call(app.bsky.notification.registerPush, payload, { + service: NOTIF_SERVICE, }) notyLogger.debug(`registerPushToken: success`) @@ -74,7 +88,7 @@ const _registerPushTokenDebounced = debounce(_registerPushToken, 100) * `_registerPushTokenDebounced` directly. */ export function useRegisterPushToken() { - const agent = useAgent() + const client = usePdsClient() const {currentAccount} = useSession() return useCallback( @@ -87,7 +101,7 @@ export function useRegisterPushToken() { }) => { if (!currentAccount) return return _registerPushTokenDebounced({ - agent, + client, currentAccount, token, extra: { @@ -95,7 +109,7 @@ export function useRegisterPushToken() { }, }) }, - [agent, currentAccount], + [client, currentAccount], ) } @@ -326,16 +340,17 @@ export async function resetBadgeCount() { await setBadgeCountAsync(0) } -export async function unregisterPushToken(agents: AtpAgent[]) { +export async function unregisterPushToken(clients: TemporaryPushClient[]) { if (!IS_NATIVE) return try { const token = await getPushToken() if (token) { - for (const agent of agents) { - await agent.app.bsky.notification.unregisterPush( + for (const {client, service, handle} of clients) { + await client.call( + app.bsky.notification.unregisterPush, { - serviceDid: agent.serviceUrl.hostname.includes('staging') + serviceDid: service.includes('staging') ? PUBLIC_STAGING_APPVIEW_DID : PUBLIC_APPVIEW_DID, platform: Platform.OS, @@ -343,10 +358,10 @@ export async function unregisterPushToken(agents: AtpAgent[]) { appId: 'xyz.blueskyweb.app', }, { - headers: BLUESKY_NOTIF_SERVICE_HEADERS, + service: NOTIF_SERVICE, }, ) - notyLogger.debug(`Push token unregistered for ${agent.session?.handle}`) + notyLogger.debug(`Push token unregistered for ${handle}`) } } else { notyLogger.debug('Tried to unregister push token, but could not find one') diff --git a/src/screens/Deactivated.tsx b/src/screens/Deactivated.tsx index 2782c5ef0d..3be42add78 100644 --- a/src/screens/Deactivated.tsx +++ b/src/screens/Deactivated.tsx @@ -7,10 +7,11 @@ import {Trans} from '@lingui/react/macro' import {useQueryClient} from '@tanstack/react-query' import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher' +import {isErrorMaybeAppPasswordPermissions} from '#/lib/strings/errors' import {logger} from '#/logger' import { type SessionAccount, - useAgent, + usePdsClient, useSession, useSessionApi, } from '#/state/session' @@ -25,6 +26,7 @@ import * as Layout from '#/components/Layout' import {Loader} from '#/components/Loader' import {Text} from '#/components/Typography' import {IS_WEB} from '#/env' +import {com} from '#/lexicons' const COL_WIDTH = 400 @@ -36,8 +38,8 @@ export function Deactivated() { const {onPressSwitchAccount, pendingDid} = useAccountSwitcher() const {setShowLoggedOut} = useLoggedOutViewControls() const hasOtherAccounts = accounts.length > 1 - const {logoutCurrentAccount} = useSessionApi() - const agent = useAgent() + const {logoutCurrentAccount, refreshSession} = useSessionApi() + const pdsClient = usePdsClient() const [pending, setPending] = useState(false) const [error, setError] = useState() const queryClient = useQueryClient() @@ -70,21 +72,24 @@ export function Deactivated() { const handleActivate = useCallback(async () => { try { setPending(true) - await agent.com.atproto.server.activateAccount() + await pdsClient.call(com.atproto.server.activateAccount) await queryClient.resetQueries() - await agent.resumeSession(agent.session!) + await refreshSession() } catch (e: any) { - switch (e.message) { - case 'Bad token scope': - setError( - _( - msg`You're signed in with an App Password. Please sign in with your main password to continue deactivating your account.`, - ), - ) - break - default: - setError(_(msg`Something went wrong, please try again`)) - break + /* + * `activateAccount` declares no lexicon errors, so the app-password case + * arrives as an undeclared code plus a message. The shared helper matches + * both that and the plain-string form the old exact `e.message` switch + * relied on. + */ + if (isErrorMaybeAppPasswordPermissions(e)) { + setError( + _( + msg`You're signed in with an App Password. Please sign in with your main password to continue deactivating your account.`, + ), + ) + } else { + setError(_(msg`Something went wrong, please try again`)) } logger.error(e, { @@ -93,7 +98,7 @@ export function Deactivated() { } finally { setPending(false) } - }, [_, agent, setPending, setError, queryClient]) + }, [_, pdsClient, refreshSession, setPending, setError, queryClient]) return ( diff --git a/src/screens/Settings/components/ChangeHandleDialog.tsx b/src/screens/Settings/components/ChangeHandleDialog.tsx index 1b31a7de2a..a8c9474410 100644 --- a/src/screens/Settings/components/ChangeHandleDialog.tsx +++ b/src/screens/Settings/components/ChangeHandleDialog.tsx @@ -27,7 +27,7 @@ import {useFetchDid, useUpdateHandleMutation} from '#/state/queries/handle' import {RQKEY as RQKEY_PROFILE} from '#/state/queries/profile' import {useServiceQuery} from '#/state/queries/service' import {useCurrentAccountProfile} from '#/state/queries/useCurrentAccountProfile' -import {useAgent, useSession} from '#/state/session' +import {useAgent, useSession, useSessionApi} from '#/state/session' import {ErrorScreen} from '#/view/com/util/error/ErrorScreen' import {atoms as a, native, useBreakpoints, useTheme} from '#/alf' import {Admonition} from '#/components/Admonition' @@ -152,7 +152,7 @@ function ProvidedHandlePage({ }) { const {_} = useLingui() const [subdomain, setSubdomain] = useState('') - const agent = useAgent() + const {refreshSession} = useSessionApi() const control = Dialog.useDialogContext() const {currentAccount} = useSession() const queryClient = useQueryClient() @@ -173,7 +173,7 @@ function ProvidedHandlePage({ queryKey: RQKEY_PROFILE(currentAccount.did), }) } - agent.resumeSession(agent.session!).then(() => control.close()) + refreshSession().then(() => control.close()) }, }) @@ -311,7 +311,7 @@ function OwnHandlePage({goToServiceHandle}: {goToServiceHandle: () => void}) { const {currentAccount} = useSession() const [dnsPanel, setDNSPanel] = useState(true) const [domain, setDomain] = useState('') - const agent = useAgent() + const {refreshSession} = useSessionApi() const control = Dialog.useDialogContext() const fetchDid = useFetchDid() const queryClient = useQueryClient() @@ -328,7 +328,7 @@ function OwnHandlePage({goToServiceHandle}: {goToServiceHandle: () => void}) { queryKey: RQKEY_PROFILE(currentAccount.did), }) } - agent.resumeSession(agent.session!).then(() => control.close()) + refreshSession().then(() => control.close()) }, }) diff --git a/src/screens/Settings/components/DisableEmail2FADialog.tsx b/src/screens/Settings/components/DisableEmail2FADialog.tsx index 8b774b54f7..c633c82029 100644 --- a/src/screens/Settings/components/DisableEmail2FADialog.tsx +++ b/src/screens/Settings/components/DisableEmail2FADialog.tsx @@ -5,7 +5,8 @@ import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' import {cleanError} from '#/lib/strings/errors' -import {useAgent, useSession} from '#/state/session' +import {matchXrpcError} from '#/lib/xrpc-error' +import {usePdsClient, useSession, useSessionApi} from '#/state/session' import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' @@ -16,6 +17,7 @@ import {Loader} from '#/components/Loader' import * as Toast from '#/components/Toast' import {P, Text} from '#/components/Typography' import {IS_NATIVE} from '#/env' +import {com} from '#/lexicons' enum Stages { Email, @@ -31,7 +33,8 @@ export function DisableEmail2FADialog({ const t = useTheme() const {gtMobile} = useBreakpoints() const {currentAccount} = useSession() - const agent = useAgent() + const pdsClient = usePdsClient() + const {refreshSession} = useSessionApi() const [stage, setStage] = useState(Stages.Email) const [confirmationCode, setConfirmationCode] = useState('') @@ -42,7 +45,7 @@ export function DisableEmail2FADialog({ setError('') setIsProcessing(true) try { - await agent.com.atproto.server.requestEmailUpdate() + await pdsClient.call(com.atproto.server.requestEmailUpdate) setStage(Stages.ConfirmCode) } catch (e) { setError(cleanError(String(e))) @@ -56,21 +59,26 @@ export function DisableEmail2FADialog({ setIsProcessing(true) try { if (currentAccount?.email) { - await agent.com.atproto.server.updateEmail({ + await pdsClient.call(com.atproto.server.updateEmail, { email: currentAccount.email, token: confirmationCode.trim(), emailAuthFactor: false, }) - await agent.resumeSession(agent.session!) + await refreshSession() Toast.show(_(msg({message: 'Email 2FA disabled', context: 'toast'}))) } control.close() } catch (e) { - const errMsg = String(e) - if (errMsg.includes('Token is invalid')) { + /* + * The old check matched the PDS message "Token is invalid"; the lexicon + * declares that case as `InvalidToken`, so match the code instead. + */ + if ( + matchXrpcError(e, com.atproto.server.updateEmail) === 'InvalidToken' + ) { setError(_(msg`Invalid 2FA confirmation code.`)) } else { - setError(cleanError(errMsg)) + setError(cleanError(e)) } } finally { setIsProcessing(false) diff --git a/src/screens/Settings/components/ExportCarDialog.tsx b/src/screens/Settings/components/ExportCarDialog.tsx index 13b60dfdaa..6f28aa340f 100644 --- a/src/screens/Settings/components/ExportCarDialog.tsx +++ b/src/screens/Settings/components/ExportCarDialog.tsx @@ -1,11 +1,11 @@ import {useCallback, useState} from 'react' import {View} from 'react-native' +import {type DidString} from '@atproto/syntax' import {Trans, useLingui} from '@lingui/react/macro' -import {DM_SERVICE_HEADERS} from '#/lib/constants' import {saveBytesToDisk} from '#/lib/media/manip' import {logger} from '#/logger' -import {useAgent} from '#/state/session' +import {useChatClient, usePdsClient, useSession} from '#/state/session' import {atoms as a, useTheme, web} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' @@ -14,6 +14,7 @@ import {InlineLinkText} from '#/components/Link' import {Loader} from '#/components/Loader' import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' +import {chat, com} from '#/lexicons' export function ExportCarDialog({ control, @@ -22,21 +23,29 @@ export function ExportCarDialog({ }) { const {t: l} = useLingui() const t = useTheme() - const agent = useAgent() + const {currentAccount} = useSession() + const pdsClient = usePdsClient() + const chatClient = useChatClient() const [loading, setLoading] = useState<'repo' | 'chat' | false>(false) const download = useCallback(async () => { - if (!agent.session) { + if (!currentAccount) { return // shouldn't ever happen } try { setLoading('repo') - const did = agent.session.did - const downloadRes = await agent.com.atproto.sync.getRepo({did}) + const did = currentAccount.did as DidString + const data = await pdsClient.call(com.atproto.sync.getRepo, {did}) + /* + * getRepo declares `application/vnd.ipld.car`, so lex-client hands back + * the raw bytes unparsed and does not surface the response content-type. + * The old code already fell back to this same constant when the header was + * absent, and the endpoint always returns CAR. + */ const saveRes = await saveBytesToDisk( 'repo.car', - downloadRes.data, - downloadRes.headers['content-type'] || 'application/vnd.ipld.car', + data, + 'application/vnd.ipld.car', ) if (saveRes) { @@ -48,28 +57,26 @@ export function ExportCarDialog({ } finally { setLoading(false) } - }, [l, agent]) + }, [l, currentAccount, pdsClient]) const downloadChatData = useCallback(async () => { - if (!agent.session) { + if (!currentAccount) { return } try { setLoading('chat') - // Using raw fetch because the XRPC client incorrectly tries to JSON-parse - // application/jsonl responses (substring match on application/json). - const res = await agent.sessionManager.fetchHandler( - '/xrpc/chat.bsky.actor.exportAccountData', - {headers: DM_SERVICE_HEADERS}, - ) - if (!res.ok) { - throw new Error(`HTTP ${res.status}`) - } - const data = new Uint8Array(await res.arrayBuffer()) + /* + * lex-client only JSON-parses a response when the declared output encoding + * is `application/json`; this endpoint declares `application/jsonl`, so it + * returns the raw bytes. That removes the reason for the old low-level + * fetchHandler workaround, and the chat client emits the proxy header + * itself, so the per-call DM headers go away too. + */ + const data = await chatClient.call(chat.bsky.actor.exportAccountData) const saveRes = await saveBytesToDisk( 'chat.jsonl', data, - res.headers.get('content-type') || 'application/jsonl', + 'application/jsonl', ) if (saveRes) { @@ -81,7 +88,7 @@ export function ExportCarDialog({ } finally { setLoading(false) } - }, [l, agent]) + }, [l, currentAccount, chatClient]) return ( diff --git a/src/screens/SignupQueued.tsx b/src/screens/SignupQueued.tsx index b83e627404..e0bf7f2edd 100644 --- a/src/screens/SignupQueued.tsx +++ b/src/screens/SignupQueued.tsx @@ -7,7 +7,7 @@ import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' import {logger} from '#/logger' -import {isSignupQueued, useAgent, useSessionApi} from '#/state/session' +import {isSignupQueued, usePdsClient, useSessionApi} from '#/state/session' import {useOnboardingDispatch} from '#/state/shell' import {Logo} from '#/view/icons/Logo' import {atoms as a, native, useBreakpoints, useTheme, web} from '#/alf' @@ -15,6 +15,7 @@ import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {Loader} from '#/components/Loader' import {P, Text} from '#/components/Typography' import {IS_IOS, IS_LIQUID_GLASS, IS_WEB} from '#/env' +import {com} from '#/lexicons' const COL_WIDTH = 400 @@ -24,8 +25,8 @@ export function SignupQueued() { const insets = useSafeAreaInsets() const {gtMobile} = useBreakpoints() const onboardingDispatch = useOnboardingDispatch() - const {logoutCurrentAccount} = useSessionApi() - const agent = useAgent() + const {logoutCurrentAccount, refreshSession} = useSessionApi() + const pdsClient = usePdsClient() const [isProcessing, setProcessing] = useState(false) const [estimatedTime, setEstimatedTime] = useState( @@ -38,18 +39,23 @@ export function SignupQueued() { const checkStatus = useCallback(async () => { setProcessing(true) try { - const res = await agent.com.atproto.temp.checkSignupQueue() - if (res.data.activated) { - // ready to go, exchange the access token for a usable one and kick off onboarding - await agent.sessionManager.refreshSession() - if (!isSignupQueued(agent.session?.accessJwt)) { + const res = await pdsClient.call(com.atproto.temp.checkSignupQueue) + if (res.activated) { + /* + * Ready to go, exchange the access token for a usable one and kick off + * onboarding. The refreshed snapshot carries the new scope; reading + * `currentAccount` here would still see the pre-refresh token, since the + * session's update hook dispatches a render away. + */ + const refreshed = await refreshSession() + if (!isSignupQueued(refreshed?.accessJwt)) { onboardingDispatch({type: 'start'}) } } else { // not ready, update UI - setEstimatedTime(msToString(res.data.estimatedTimeMs)) - if (typeof res.data.placeInQueue !== 'undefined') { - setPlaceInQueue(Math.max(res.data.placeInQueue, 1)) + setEstimatedTime(msToString(res.estimatedTimeMs)) + if (typeof res.placeInQueue !== 'undefined') { + setPlaceInQueue(Math.max(res.placeInQueue, 1)) } } } catch (e: any) { @@ -62,7 +68,8 @@ export function SignupQueued() { setEstimatedTime, setPlaceInQueue, onboardingDispatch, - agent, + pdsClient, + refreshSession, ]) useEffect(() => { diff --git a/src/state/session/__tests__/provider-refresh-session-test.tsx b/src/state/session/__tests__/provider-refresh-session-test.tsx new file mode 100644 index 0000000000..6b1e10bdec --- /dev/null +++ b/src/state/session/__tests__/provider-refresh-session-test.tsx @@ -0,0 +1,204 @@ +import {PasswordSession} from '@atproto/lex-password-session' +import {beforeEach, describe, expect, it, jest} from '@jest/globals' +import {act, render} from '@testing-library/react-native' + +import {type SessionAccount} from '../types' + +/* + * The provider pulls the whole app shell in through `#/state/util` and the + * account factories. These mocks cut the tree back to the session lifecycle + * itself, mirroring provider-clients-test.tsx. + */ +jest.mock('#/state/persisted', () => { + const { + defaults, + }: typeof import('#/state/persisted/schema') = require('#/state/persisted/schema') + return { + defaults, + get: (key: keyof typeof defaults) => defaults[key], + write: () => Promise.resolve(), + readLatest: (key: keyof typeof defaults) => defaults[key], + onUpdate: () => () => {}, + } +}) +jest.mock('#/state/util', () => ({useCloseAllActiveElements: () => () => {}})) +jest.mock('#/components/dialogs/Context', () => ({ + useGlobalDialogsControlContext: () => ({signinDialogControl: {open() {}}}), +})) +jest.mock('#/analytics', () => ({ + AnalyticsContext: ({children}: {children: React.ReactNode}) => children, + useAnalyticsBase: () => ({metric() {}, logger: {debug() {}, error() {}}}), + utils: {accountToSessionMetadata: () => ({}), useMeta: () => undefined}, +})) +jest.mock('#/state/shell/onboarding', () => ({ + useOnboardingDispatch: () => () => {}, +})) +jest.mock('#/ageAssurance/data', () => ({ + clearAgeAssuranceServerDataForAll: () => {}, + clearAgeAssuranceServerDataForDid: () => {}, +})) +jest.mock('#/lib/persisted-query-storage', () => ({ + clearPersistedQueryStorage: () => Promise.resolve(), +})) +jest.mock('#/lib/notifications/notifications', () => ({ + unregisterPushToken: () => Promise.resolve(), +})) +jest.mock('jwt-decode', () => ({ + jwtDecode: () => ({scope: 'com.atproto.access'}), +})) +jest.mock('#/state/events', () => ({ + emitSessionDropped: () => {}, + emitNetworkConfirmed: () => {}, + emitNetworkLost: () => {}, +})) + +const mockLogin = jest.fn<(...args: unknown[]) => Promise>() +jest.mock('../session-core', () => ({ + ...jest.requireActual('../session-core'), + createSessionBundleAndLogin: (...args: unknown[]) => mockLogin(...args), +})) +jest.mock('../create-account', () => ({ + createSessionBundleAndCreateAccount: () => new Promise(() => {}), +})) + +import {Provider, useSession, useSessionApi} from '#/state/session' +import {type SessionApiContext} from '#/state/session/types' +import {BskyAppAgent, PasswordSessionManager} from '../bridge-agent' +import {type SessionBundle} from '../session-core' +import {sessionAccountToSessionData} from '../session-data' +import { + asFetch, + DID, + HANDLE, + json, + makeAccount, + makeMockFetch, + type MockFetch, +} from './mock-fetch' + +/** + * Build a bundle whose session is a real `PasswordSession` over the stubbed + * network, since `refreshSession` drives the session's own refresh machinery. + */ +function makeBundle( + account: SessionAccount, + fetchMock: MockFetch, +): SessionBundle { + const session = new PasswordSession(sessionAccountToSessionData(account), { + fetch: asFetch(fetchMock), + }) + const manager = new PasswordSessionManager(session, { + service: account.service, + }) + manager.setFetch(asFetch(fetchMock)) + return { + session, + agent: new BskyAppAgent(manager), + service: new URL(account.service), + } +} + +type Harness = { + api: SessionApiContext + currentAccount: () => SessionAccount | undefined +} + +function renderProvider(): Harness { + let api!: SessionApiContext + let currentAccount: SessionAccount | undefined + function Probe() { + api = useSessionApi() + currentAccount = useSession().currentAccount + return null + } + render( + + + , + ) + return {api, currentAccount: () => currentAccount} +} + +/** Render the provider and log `account` in through the stubbed login factory. */ +async function renderLoggedIn( + account: SessionAccount, + fetchMock: MockFetch, +): Promise { + const bundle = makeBundle(account, fetchMock) + const harness = renderProvider() + mockLogin.mockResolvedValueOnce({bundle, account}) + await act(async () => { + await harness.api.login({} as never, 'LoginForm') + }) + return harness +} + +beforeEach(() => { + mockLogin.mockReset() +}) + +describe('refreshSession', () => { + it('resolves with the rotated account snapshot', async () => { + const fetchMock = makeMockFetch() + const {api} = await renderLoggedIn(makeAccount(), fetchMock) + + let refreshed: SessionAccount | undefined + await act(async () => { + refreshed = await api.refreshSession() + }) + + /* the mock's refresh response rotates both tokens */ + expect(refreshed?.accessJwt).toBe('access-jwt-2') + expect(refreshed?.refreshJwt).toBe('refresh-jwt-2') + expect(refreshed?.did).toBe(DID) + expect(refreshed?.handle).toBe(HANDLE) + }) + + it('exposes the fresh tokens before the store has caught up', async () => { + const fetchMock = makeMockFetch() + const {api, currentAccount} = await renderLoggedIn(makeAccount(), fetchMock) + + /* + * The point of the return value: `SignupQueued` branches on the fresh + * accessJwt synchronously, without waiting for `onUpdated` -> dispatch -> + * re-render. + */ + let refreshed: SessionAccount | undefined + const before = currentAccount()?.accessJwt + await act(async () => { + refreshed = await api.refreshSession() + }) + expect(before).toBe('access-jwt') + expect(refreshed?.accessJwt).toBe('access-jwt-2') + }) + + it('resolves with undefined when logged out', async () => { + const {api} = renderProvider() + + let refreshed: SessionAccount | undefined = makeAccount() + await act(async () => { + refreshed = await api.refreshSession() + }) + + expect(refreshed).toBeUndefined() + }) + + it('rejects when the refresh rotated nothing', async () => { + /* + * A transient failure: `PasswordSession.refresh()` reports through + * `onUpdateFailure` and resolves with the SAME data object. Callers read + * resolution as "tokens rotated", so this must reject. + */ + const fetchMock = makeMockFetch({ + 'com.atproto.server.refreshSession': () => + json({error: 'InternalServerError'}, 500), + }) + const {api} = await renderLoggedIn(makeAccount(), fetchMock) + + await expect( + act(async () => { + await api.refreshSession() + }), + ).rejects.toThrow('Failed to refresh session') + }) +}) diff --git a/src/state/session/__tests__/session-test.ts b/src/state/session/__tests__/session-test.ts index 9cb2db6a59..578b4d26e7 100644 --- a/src/state/session/__tests__/session-test.ts +++ b/src/state/session/__tests__/session-test.ts @@ -1,4 +1,3 @@ -import {type AtpAgent} from '@atproto/api' import {type SessionData} from '@atproto/lex-password-session' import {describe, expect, it, jest} from '@jest/globals' @@ -18,21 +17,21 @@ jest.mock('../../../ageAssurance/state', () => ({ unsafeGetAndComputeAgeAssurance: () => ({state: {}}), })) jest.mock('#/lib/notifications/notifications', () => ({ - unregisterPushToken(_agents: AtpAgent[]) { + unregisterPushToken(_clients: unknown[]) { return Promise.resolve() }, })) /* * The logout and account-removal reducer cases fire a push-token side effect - * whose first step, `createTemporaryAgentsAndResume`, builds real `AtpAgent`s - * and resumes them over the real network. Under jest that request outlives the + * whose first step, `createTemporaryClientsAndResume`, resumes real + * `PasswordSession`s over the real network. Under jest that request outlives the * suite: it rejects after teardown, and the resulting `logger.error` reaches * for `nanoid` in an environment that no longer has it, failing whichever suite * happens to be running at that moment. Stubbing the module keeps the side * effect synchronous and offline. */ jest.mock('../util', () => ({ - createTemporaryAgentsAndResume: () => Promise.resolve([]), + createTemporaryClientsAndResume: () => Promise.resolve([]), })) // Reuse a bundle within each test: session events are scoped by bundle identity. diff --git a/src/state/session/index.tsx b/src/state/session/index.tsx index 01bca93c36..730cdd9808 100644 --- a/src/state/session/index.tsx +++ b/src/state/session/index.tsx @@ -82,6 +82,7 @@ const ApiContext = createContext({ resumeSession: async () => {}, removeAccount: () => {}, partialRefreshSession: async () => {}, + refreshSession: () => Promise.resolve(undefined), }) ApiContext.displayName = 'SessionApiContext' @@ -473,6 +474,50 @@ export function Provider({children}: React.PropsWithChildren<{}>) { }) }, [store, cancelPendingTask]) + /** + * Rotate the session's tokens and hand back the resulting account snapshot. + * + * Rejects when the rotation was a no-op, restoring the contract the + * `agent.resumeSession(agent.session!)` call sites were written against (the + * bridge agent's `refreshSession` override does the same, for the same + * reason). `PasswordSession.refresh()` resolves with the + * unchanged `SessionData` on a transient failure - a 500 or a network error + * reported through `onUpdateFailure` - and reserves rejection for a + * definitively dead session. Callers here all read resolution as "tokens + * rotated": the verification dialogs close, `Deactivated` clears its error + * state, and `SignupQueued` re-checks the token scope, so a resolved no-op + * would report success or loop silently. Identity, not a field comparison, is + * the signal: `PasswordSession` allocates a new object per successful + * rotation and returns the existing one untouched otherwise. Capturing the + * data immediately before the call also handles concurrent refreshes, since a + * rotation another caller's queued refresh performed still differs from what + * we captured. + * + * Like {@link partialRefreshSession}, the bundle comes from + * `store.getState()` rather than the render's `state`: a dispatch landing + * before the next render would otherwise leave this holding a disposed + * bundle, and reading live also keeps the callback's identity stable across + * unrelated state updates. + */ + const refreshSession = useCallback< + SessionApiContext['refreshSession'] + >(async () => { + const bundle = store.getState().currentBundleState.bundle as unknown as + | SessionBundle + | PublicSessionBundle + if (!bundle.session) return undefined // logged out: nothing to refresh + const before = bundle.session.session + const after = await bundle.session.refresh() + if (after === before) { + throw new Error('Failed to refresh session') + } + /* + * The session's `onUpdated` hook dispatches the new tokens into the store, + * but that lands a render away; this snapshot exposes them immediately. + */ + return sessionDataToSessionAccount(after, after.service) + }, [store]) + const removeAccount = useCallback( account => { addSessionDebugLog({ @@ -607,6 +652,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { resumeSession, removeAccount, partialRefreshSession, + refreshSession, }), [ createAccount, @@ -616,6 +662,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { resumeSession, removeAccount, partialRefreshSession, + refreshSession, ], ) diff --git a/src/state/session/reducer.ts b/src/state/session/reducer.ts index 8571539f87..06ceb8ece6 100644 --- a/src/state/session/reducer.ts +++ b/src/state/session/reducer.ts @@ -3,7 +3,7 @@ import {logger} from '#/lib/notifications/util' import {wrapSessionReducerForLogging} from './logging' import {createPublicSessionBundle} from './session-core' import {type AtpSessionEvent, type SessionAccount} from './types' -import {createTemporaryAgentsAndResume} from './util' +import {createTemporaryClientsAndResume} from './util' // Keep session internals outside the reducer's static view of a bundle. type OpaqueSessionBundle = { @@ -155,8 +155,8 @@ let reducer = (state: State, action: Action): State => { // side effect const account = state.accounts.find(a => a.did === accountDid) if (account) { - createTemporaryAgentsAndResume([account]) - .then(agents => unregisterPushToken(agents)) + createTemporaryClientsAndResume([account]) + .then(clients => unregisterPushToken(clients)) .then(() => logger.debug('Push token unregistered', {did: accountDid}), ) @@ -183,8 +183,8 @@ let reducer = (state: State, action: Action): State => { // side effect const account = state.accounts.find(a => a.did === accountDid) if (account && accountDid) { - createTemporaryAgentsAndResume([account]) - .then(agents => unregisterPushToken(agents)) + createTemporaryClientsAndResume([account]) + .then(clients => unregisterPushToken(clients)) .then(() => logger.debug('Push token unregistered', {did: accountDid}), ) @@ -211,8 +211,8 @@ let reducer = (state: State, action: Action): State => { } } case 'logged-out-every-account': { - createTemporaryAgentsAndResume(state.accounts) - .then(agents => unregisterPushToken(agents)) + createTemporaryClientsAndResume(state.accounts) + .then(clients => unregisterPushToken(clients)) .then(() => logger.debug('Push token unregistered')) .catch(err => { logger.error('Failed to unregister push token', { diff --git a/src/state/session/types.ts b/src/state/session/types.ts index ca1bca62f1..c02e9cca7d 100644 --- a/src/state/session/types.ts +++ b/src/state/session/types.ts @@ -52,4 +52,17 @@ export type SessionApiContext = { * so it produces no session-change side effects. */ partialRefreshSession: () => Promise + /** + * Rotates the session's tokens and resolves with the resulting account + * snapshot, or `undefined` when logged out. + * + * Rejects when nothing was rotated, so a resolved promise means "tokens + * rotated". Every caller relies on that: the verification dialogs close on + * resolution, and `SignupQueued` re-checks the token scope. + * + * The snapshot is returned rather than read off `currentAccount`, because the + * session's `onUpdated` hook -> `store.dispatch` path is a render cycle away + * and `SignupQueued` branches synchronously on the fresh `accessJwt`. + */ + refreshSession: () => Promise } diff --git a/src/state/session/util.ts b/src/state/session/util.ts index af1c2846c7..5c3dcfb01c 100644 --- a/src/state/session/util.ts +++ b/src/state/session/util.ts @@ -1,7 +1,10 @@ -import AtpAgent from '@atproto/api' +import {PasswordSession} from '@atproto/lex-password-session' +import {createLexClient} from '#/lib/lexClient' +import {type TemporaryPushClient} from '#/lib/notifications/notifications' import * as persisted from '#/state/persisted' -import {sessionAccountToSession} from './session-data' +import {networkAwareFetch} from './network' +import {sessionAccountToSessionData} from './session-data' import {type SessionAccount} from './types' export {isSessionExpired, isSignupQueued} from './session-data' @@ -12,30 +15,41 @@ export function readLastActiveAccount() { } /** - * Creates and attempted to resumeSession for every stored session. - * Intended to be used to send push token revokations just before logout. + * Resume a single-use session per stored account, for the push-token revocation + * sent just before logout. + * + * The sessions carry no lifecycle hooks - no `onUpdated`, no `onDeleted` - so a + * rotation one of them performs can neither persist over nor race the live + * session's tokens. That isolation is load-bearing: each exists only long enough + * to authenticate one `unregisterPush` call. + * + * PDS routing is left to the session rather than pinned from the stored + * `pdsUrl`, because `resume` refreshes (and fills in a missing didDoc from + * `getSession`) before the client issues anything, so the request already goes + * to the didDoc PDS. + * + * `resume` rejects only when a session is definitively dead; a transient network + * failure resolves with the stored tokens, which are the same ones the old agent + * path would have sent. Definitively dead sessions drop out of the settled list. */ -export async function createTemporaryAgentsAndResume( +export async function createTemporaryClientsAndResume( accounts: SessionAccount[], -) { - const agents = await Promise.allSettled( +): Promise { + const settled = await Promise.allSettled( accounts.map(async account => { - const agent: AtpAgent = new AtpAgent({service: account.service}) - if (account.pdsUrl) { - agent.sessionManager.pdsUrl = new URL(account.pdsUrl) - } - - const session = sessionAccountToSession(account) - const res = await agent.resumeSession(session) - if (!res.success) throw new Error('Failed to resume session') - - agent.assertAuthenticated() // confirm auth success - - return agent + const session = await PasswordSession.resume( + sessionAccountToSessionData(account), + {fetch: networkAwareFetch}, + ) + return { + client: createLexClient(session), + service: session.session.service, + handle: session.session.handle, + } satisfies TemporaryPushClient }), ) - return agents + return settled .filter(x => x.status === 'fulfilled') .map(promise => promise.value) } diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 1388ce0c45..3f44040444 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -285,6 +285,12 @@ export const ComposePost = ({ const pdsClient = usePdsClient() const queryClient = useQueryClient() const currentDid = currentAccount!.did + /* + * The host the video service-auth token is minted for. This is the same value + * that seeds the session's PDS routing, so the audience always matches the host + * the upload actually reaches; a mismatch would 401 the upload. + */ + const currentDispatchUrl = currentAccount!.pdsUrl ?? currentAccount!.service const {closeComposer} = useComposerControls() const {t: l, i18n} = useLingui() const requireAltTextEnabled = useRequireAltTextEnabled() @@ -479,7 +485,8 @@ export const ComposePost = ({ }, }) }, - agent, + pdsClient, + currentDispatchUrl, currentDid, abortController.signal, i18n, @@ -489,7 +496,8 @@ export const ComposePost = ({ [ l, i18n, - agent, + pdsClient, + currentDispatchUrl, currentDid, composerDispatch, ax.metric, @@ -661,7 +669,8 @@ export const ComposePost = ({ }, }) }, - agent, + pdsClient, + currentDispatchUrl, currentDid, abortController.signal, i18n, @@ -677,7 +686,8 @@ export const ComposePost = ({ [ l, i18n, - agent, + pdsClient, + currentDispatchUrl, currentDid, composerDispatch, ax.metric, diff --git a/src/view/com/composer/state/video.ts b/src/view/com/composer/state/video.ts index eeca7dca47..4ae825a7f1 100644 --- a/src/view/com/composer/state/video.ts +++ b/src/view/com/composer/state/video.ts @@ -1,5 +1,5 @@ import {type ImagePickerAsset} from 'expo-image-picker' -import {type AppBskyVideoDefs, type AtpAgent, type BlobRef} from '@atproto/api' +import {type BlobRef, type Client} from '@atproto/lex' import {type I18n} from '@lingui/core' import {msg} from '@lingui/core/macro' @@ -14,9 +14,10 @@ import { import {type VideoTelemetry} from '#/lib/media/video/telemetry' import {type CompressedVideo} from '#/lib/media/video/types' import {uploadVideo} from '#/lib/media/video/upload' -import {createVideoAgent} from '#/lib/media/video/util' +import {createTokenlessVideoServiceClient} from '#/lib/media/video/util' import {isNetworkError} from '#/lib/strings/errors' import {logger} from '#/logger' +import {app} from '#/lexicons' import { advanceVideoProgress, didSkipVideoCompression, @@ -56,7 +57,7 @@ export type VideoAction = } | { type: 'update_job_status' - jobStatus: AppBskyVideoDefs.JobStatus + jobStatus: app.bsky.video.defs.JobStatus signal: AbortSignal } @@ -126,7 +127,7 @@ type ProcessingState = { asset: ImagePickerAsset video: CompressedVideo jobId: string - jobStatus: AppBskyVideoDefs.JobStatus | null + jobStatus: app.bsky.video.defs.JobStatus | null pendingPublish?: undefined telemetry: VideoTelemetry altText: string @@ -295,7 +296,8 @@ function trunc2dp(num: number) { export async function processVideo( asset: ImagePickerAsset, dispatch: (action: VideoAction) => void, - agent: AtpAgent, + client: Client, + dispatchUrl: string | URL, did: string, signal: AbortSignal, i18n: I18n, @@ -339,12 +341,13 @@ export async function processVideo( signal, }) - let uploadResponse: AppBskyVideoDefs.JobStatus | undefined + let uploadResponse: app.bsky.video.defs.JobStatus | undefined try { telemetry.uploadStarted(video.size) uploadResponse = await uploadVideo({ video, - agent, + client, + dispatchUrl, did, signal, i18n, @@ -381,12 +384,14 @@ export async function processVideo( return // Exit async loop } - const videoAgent = createVideoAgent() - let status: AppBskyVideoDefs.JobStatus | undefined + const videoClient = createTokenlessVideoServiceClient() + let status: app.bsky.video.defs.JobStatus | undefined let blob: BlobRef | undefined try { - const response = await videoAgent.app.bsky.video.getJobStatus({jobId}) - status = response.data.jobStatus + const response = await videoClient.call(app.bsky.video.getJobStatus, { + jobId, + }) + status = response.jobStatus pollFailures = 0 if (status.state === 'JOB_STATE_COMPLETED') { From dc31e7fd124571ea9e4cb0c11d46c35052c53ca1 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Thu, 13 Aug 2026 22:26:21 +0300 Subject: [PATCH 23/32] [SDK] Migrate agent sugar to @bsky.app/sdk actions (#11382) --- .../components/RedirectOverlay.tsx | 12 +- src/ageAssurance/data.tsx | 105 ++++++----- .../AgeAssuranceRedirectDialog.tsx | 12 +- .../contacts/screens/GetContacts.tsx | 80 ++++----- .../lists/CreateListFromStarterPackDialog.tsx | 4 +- src/features/liveEvents/preferences.ts | 35 ++-- src/lib/api/legacy-blob.ts | 16 -- src/screens/Onboarding/StepFinished/index.tsx | 64 +++---- src/screens/Settings/InterestsSettings.tsx | 9 +- src/screens/Settings/Settings.tsx | 7 +- src/state/birthdate.ts | 11 +- src/state/queries/labeler.ts | 21 ++- src/state/queries/like.ts | 15 +- src/state/queries/list-members.ts | 2 +- src/state/queries/list.ts | 26 ++- .../queries/messages/actor-declaration.ts | 28 +-- src/state/queries/nuxs/index.ts | 23 ++- src/state/queries/pinned-post.ts | 8 +- .../queries/post-interaction-settings.ts | 11 +- src/state/queries/post.ts | 56 ++++-- src/state/queries/preferences/index.ts | 168 ++++++++++++------ src/state/queries/profile.ts | 135 +++++++------- src/state/session/create-account.ts | 50 +++--- src/state/session/session-core.ts | 11 +- 24 files changed, 520 insertions(+), 389 deletions(-) delete mode 100644 src/lib/api/legacy-blob.ts diff --git a/src/ageAssurance/components/RedirectOverlay.tsx b/src/ageAssurance/components/RedirectOverlay.tsx index 5a6f5b2934..5b31e94365 100644 --- a/src/ageAssurance/components/RedirectOverlay.tsx +++ b/src/ageAssurance/components/RedirectOverlay.tsx @@ -16,7 +16,7 @@ import {Trans} from '@lingui/react/macro' import {retry} from '#/lib/async/retry' import {wait} from '#/lib/async/wait' import {parseLinkingUrl} from '#/lib/parseLinkingUrl' -import {useAgent, useSession} from '#/state/session' +import {useAppviewClient, useSession} from '#/state/session' import {atoms as a, platform, useBreakpoints, useTheme} from '#/alf' import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge' import {Button, ButtonText} from '#/components/Button' @@ -176,7 +176,7 @@ function Inner() { const t = useTheme() const ax = useAnalytics() const {_} = useLingui() - const agent = useAgent() + const appviewClient = useAppviewClient() const polling = useRef(false) const unmounted = useRef(false) const [error, setError] = useState(false) @@ -196,10 +196,10 @@ function Inner() { 5, () => true, async () => { - if (!agent.session) return + if (!appviewClient.did) return if (unmounted.current) return - const data = await refetchAgeAssuranceServerState({agent}) + const data = await refetchAgeAssuranceServerState({appviewClient}) if (data?.state.status !== 'assured') { throw new Error( @@ -214,7 +214,7 @@ function Inner() { ) .then(async data => { if (!data) return - if (!agent.session) return + if (!appviewClient.did) return if (unmounted.current) return setSuccess(true) @@ -230,7 +230,7 @@ function Inner() { return () => { unmounted.current = true } - }, [ax, agent]) + }, [ax, appviewClient]) if (success) { return ( diff --git a/src/ageAssurance/data.tsx b/src/ageAssurance/data.tsx index 9cf6703dfc..2beac738c2 100644 --- a/src/ageAssurance/data.tsx +++ b/src/ageAssurance/data.tsx @@ -7,6 +7,8 @@ import { AtpAgent, type ChatBskyActorDeclaration, } from '@atproto/api' +import {type Client} from '@atproto/lex' +import {getPreferences} from '@bsky.app/sdk' import {createAsyncStoragePersister} from '@tanstack/query-async-storage-persister' import {focusManager, QueryClient, useQuery} from '@tanstack/react-query' import {persistQueryClient} from '@tanstack/react-query-persist-client' @@ -21,7 +23,7 @@ import { snoozeBirthdateUpdateAllowedForDid, } from '#/state/birthdate' import {fetchActorDeclarationRecord} from '#/state/queries/messages/actor-declaration' -import {useAgent, useSession} from '#/state/session' +import {useAppviewClient, usePdsClient, useSession} from '#/state/session' import {DEVICE_SIGNALS_SUPPORTED} from '#/ageAssurance/const' import * as debug from '#/ageAssurance/debug' import {logger} from '#/ageAssurance/logger' @@ -37,6 +39,7 @@ import { } from '#/ageAssurance/util' import {IS_DEV} from '#/env' import {useGeolocation} from '#/geolocation' +import {app} from '#/lexicons' import {device} from '#/storage' /** @@ -63,12 +66,6 @@ const [, cacheHydrationPromise] = persistQueryClient({ persister, }) -export function getDidFromAgentSession(agent: AtpAgent) { - const sessionManager = agent.sessionManager - if (!sessionManager || !sessionManager.did) return - return sessionManager.did -} - /* * Optimistic data */ @@ -187,7 +184,7 @@ export function useConfigQuery() { export function createServerStateQueryKey({did}: {did: string}) { return ['serverState', did] } -export async function getServerState({agent}: {agent: AtpAgent}) { +export async function getServerState({appviewClient}: {appviewClient: Client}) { if (debug.enabled && debug.serverState) return debug.resolve(debug.serverState) const geolocation = device.get(['mergedGeolocation']) @@ -195,17 +192,21 @@ export async function getServerState({agent}: {agent: AtpAgent}) { logger.error(`getServerState: missing geolocation countryCode`) return null } - const {data} = await agent.app.bsky.ageassurance.getState({ + const data = await appviewClient.call(app.bsky.ageassurance.getState, { countryCode: geolocation.countryCode, regionCode: geolocation.regionCode, }) - const did = getDidFromAgentSession(agent) + const did = appviewClient.did if (data && did && createdAtCache.has(did)) { /* * If account was just created, just use the local cache if available. On - * subsequent reloads, the server should have the correct value. + * subsequent reloads, the server should have the correct value. The cache + * holds ISO datetime strings (written from `new Date().toISOString()`), so + * assert the branded DatetimeString at this boundary. */ - data.metadata.accountCreatedAt = createdAtCache.get(did) + data.metadata.accountCreatedAt = createdAtCache.get( + did, + ) as typeof data.metadata.accountCreatedAt } return data ?? null } @@ -218,8 +219,12 @@ export function getServerStateFromCache({ createServerStateQueryKey({did}), ) } -export async function prefetchServerState({agent}: {agent: AtpAgent}) { - const did = getDidFromAgentSession(agent) +export async function prefetchServerState({ + appviewClient, +}: { + appviewClient: Client +}) { + const did = appviewClient.did if (!did) return @@ -234,7 +239,7 @@ export async function prefetchServerState({agent}: {agent: AtpAgent}) { try { logger.debug(`prefetchServerState: resolving...`) - const res = await networkRetry(3, () => getServerState({agent})) + const res = await networkRetry(3, () => getServerState({appviewClient})) if (res) { qc.setQueryData(qk, res) } @@ -245,11 +250,15 @@ export async function prefetchServerState({agent}: {agent: AtpAgent}) { }) } } -export async function refetchServerState({agent}: {agent: AtpAgent}) { - const did = getDidFromAgentSession(agent) +export async function refetchServerState({ + appviewClient, +}: { + appviewClient: Client +}) { + const did = appviewClient.did if (!did) return logger.debug(`refetchServerState: fetching...`) - const res = await networkRetry(3, () => getServerState({agent})) + const res = await networkRetry(3, () => getServerState({appviewClient})) if (res) { qc.setQueryData( createServerStateQueryKey({did}), @@ -279,8 +288,8 @@ export function usePatchServerState() { ) } export function useServerStateQuery() { - const agent = useAgent() - const did = getDidFromAgentSession(agent) + const appviewClient = useAppviewClient() + const did = appviewClient.did const query = useQuery( { enabled: !!did, @@ -290,7 +299,7 @@ export function useServerStateQuery() { }, queryKey: createServerStateQueryKey({did: did!}), async queryFn() { - return getServerState({agent}) + return getServerState({appviewClient}) }, }, qc, @@ -342,15 +351,15 @@ export function createOtherRequiredDataQueryKey({did}: {did: string}) { return ['otherRequiredData', did] } async function getOtherRequiredData({ - agent, + accountClient, }: { - agent: AtpAgent + accountClient: Client }): Promise { if (debug.enabled) return debug.resolve(debug.otherRequiredData) - const did = getDidFromAgentSession(agent) + const did = accountClient.did const [prefs, actorDeclaration] = await Promise.all([ - agent.getPreferences(), - fetchActorDeclarationRecord({did, agent}), + accountClient.call(getPreferences), + fetchActorDeclarationRecord({did, client: accountClient}), ]) const data: OtherRequiredData = { birthdate: prefs.birthDate ? prefs.birthDate.toISOString() : undefined, @@ -426,8 +435,12 @@ export function setOtherRequiredDataActorDeclarationCache({ next, ) } -export async function prefetchOtherRequiredData({agent}: {agent: AtpAgent}) { - const did = getDidFromAgentSession(agent) +export async function prefetchOtherRequiredData({ + accountClient, +}: { + accountClient: Client +}) { + const did = accountClient.did if (!did) return @@ -442,7 +455,9 @@ export async function prefetchOtherRequiredData({agent}: {agent: AtpAgent}) { try { logger.debug(`prefetchOtherRequiredData: resolving...`) - const res = await networkRetry(3, () => getOtherRequiredData({agent})) + const res = await networkRetry(3, () => + getOtherRequiredData({accountClient}), + ) qc.setQueryData(qk, res) } catch (err) { const e = err as Error @@ -471,8 +486,8 @@ export function usePatchOtherRequiredData() { ) } export function useOtherRequiredDataQuery() { - const agent = useAgent() - const did = getDidFromAgentSession(agent) + const accountClient = usePdsClient() + const did = accountClient.did return useQuery( { enabled: !!did, @@ -482,7 +497,7 @@ export function useOtherRequiredDataQuery() { }, queryKey: createOtherRequiredDataQueryKey({did: did!}), async queryFn() { - return getOtherRequiredData({agent}) + return getOtherRequiredData({accountClient}) }, }, qc, @@ -577,8 +592,12 @@ export function setDeviceSignalsForRegion({ prev => ({...prev, [regionKey]: signals}), ) } -export async function prefetchDeviceSignals({agent}: {agent: AtpAgent}) { - const did = getDidFromAgentSession(agent) +export async function prefetchDeviceSignals({ + appviewClient, +}: { + appviewClient: Client +}) { + const did = appviewClient.did if (!did) return /** @@ -613,8 +632,8 @@ export async function prefetchDeviceSignals({agent}: {agent: AtpAgent}) { */ } export function useDeviceSignalsQuery() { - const agent = useAgent() - const did = getDidFromAgentSession(agent) + const appviewClient = useAppviewClient() + const did = appviewClient.did const {data: config} = useConfigQuery() const geolocation = useGeolocation() /* @@ -659,13 +678,19 @@ export function useDeviceSignalsQuery() { /** * Helper to prefetch all age assurance data from the server. */ -export function prefetchAgeAssuranceServerData({agent}: {agent: AtpAgent}) { +export function prefetchAgeAssuranceServerData({ + appviewClient, + accountClient, +}: { + appviewClient: Client + accountClient: Client +}) { return Promise.allSettled([ // config fetch initiated at the top of the App.platform.tsx files, awaited here configPrefetchPromise, - prefetchServerState({agent}), - prefetchOtherRequiredData({agent}), - prefetchDeviceSignals({agent}), + prefetchServerState({appviewClient}), + prefetchOtherRequiredData({accountClient}), + prefetchDeviceSignals({appviewClient}), ]) } diff --git a/src/components/ageAssurance/AgeAssuranceRedirectDialog.tsx b/src/components/ageAssurance/AgeAssuranceRedirectDialog.tsx index ad584ca203..acf20f38c5 100644 --- a/src/components/ageAssurance/AgeAssuranceRedirectDialog.tsx +++ b/src/components/ageAssurance/AgeAssuranceRedirectDialog.tsx @@ -6,7 +6,7 @@ import {Trans} from '@lingui/react/macro' import {retry} from '#/lib/async/retry' import {wait} from '#/lib/async/wait' -import {useAgent} from '#/state/session' +import {useAppviewClient} from '#/state/session' import {atoms as a, useTheme, web} from '#/alf' import {AgeAssuranceBadge} from '#/components/ageAssurance/AgeAssuranceBadge' import {Button, ButtonText} from '#/components/Button' @@ -84,7 +84,7 @@ export function Inner({}: {optimisticState?: AgeAssuranceRedirectDialogState}) { const t = useTheme() const ax = useAnalytics() const {_} = useLingui() - const agent = useAgent() + const appviewClient = useAppviewClient() const polling = useRef(false) const unmounted = useRef(false) const control = useAgeAssuranceRedirectDialogControl() @@ -104,10 +104,10 @@ export function Inner({}: {optimisticState?: AgeAssuranceRedirectDialogState}) { 5, () => true, async () => { - if (!agent.session) return + if (!appviewClient.did) return if (unmounted.current) return - const data = await refetchAgeAssuranceServerState({agent}) + const data = await refetchAgeAssuranceServerState({appviewClient}) if (data?.state.status !== 'assured') { throw new Error( @@ -122,7 +122,7 @@ export function Inner({}: {optimisticState?: AgeAssuranceRedirectDialogState}) { ) .then(async data => { if (!data) return - if (!agent.session) return + if (!appviewClient.did) return if (unmounted.current) return setSuccess(true) @@ -138,7 +138,7 @@ export function Inner({}: {optimisticState?: AgeAssuranceRedirectDialogState}) { return () => { unmounted.current = true } - }, [ax, agent, control]) + }, [ax, appviewClient, control]) if (success) { return ( diff --git a/src/components/contacts/screens/GetContacts.tsx b/src/components/contacts/screens/GetContacts.tsx index d84e0d1ab0..de5d6cf98b 100644 --- a/src/components/contacts/screens/GetContacts.tsx +++ b/src/components/contacts/screens/GetContacts.tsx @@ -2,24 +2,21 @@ import {useContext} from 'react' import {Alert, View} from 'react-native' import {useSafeAreaInsets} from 'react-native-safe-area-context' import * as Contacts from 'expo-contacts' -import type AtpAgent from '@atproto/api' -import { - type AppBskyActorProfile, - AppBskyContactImportContacts, - type Un$Typed, -} from '@atproto/api' +import {type Un$Typed} from '@atproto/lex' import {type Client} from '@atproto/lex' +import {toDatetimeString} from '@atproto/syntax' +import {upsertProfile} from '@bsky.app/sdk' import {msg, t} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' import {useMutation, useQueryClient} from '@tanstack/react-query' import {uploadBlob} from '#/lib/api' -import {toLegacyBlobRef} from '#/lib/api/legacy-blob' import {cleanError, isNetworkError} from '#/lib/strings/errors' +import {matchXrpcError} from '#/lib/xrpc-error' import {logger} from '#/logger' import {findContactsStatusQueryKey} from '#/state/queries/find-contacts' -import {useAgent, usePdsClient} from '#/state/session' +import {useAppviewClient, usePdsClient} from '#/state/session' import { Context as OnboardingContext, type OnboardingAction, @@ -32,6 +29,7 @@ import {Loader} from '#/components/Loader' import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' +import {app} from '#/lexicons' import { contactsWithPhoneNumbersOnly, filterMatchedNumbers, @@ -56,8 +54,8 @@ export function GetContacts({ }) { const {_} = useLingui() const ax = useAnalytics() - const agent = useAgent() const pdsClient = usePdsClient() + const appviewClient = useAppviewClient() const insets = useSafeAreaInsets() const gutters = useGutters([0, 'wide']) const queryClient = useQueryClient() @@ -75,7 +73,7 @@ export function GetContacts({ */ if (context === 'Onboarding' && maybeOnboardingContext) { try { - await createProfileRecord(agent, pdsClient, maybeOnboardingContext) + await createProfileRecord(pdsClient, maybeOnboardingContext) } catch (error) { logger.debug('Error creating profile record:', {safeMessage: error}) } @@ -88,13 +86,13 @@ export function GetContacts({ ) if (phoneNumbers.length > 0) { - const res = await agent.app.bsky.contact.importContacts({ + const res = await appviewClient.call(app.bsky.contact.importContacts, { token: state.token, contacts: phoneNumbers.slice(0, MAX_UPLOAD_COUNT), }) return { - matches: res.data.matchesAndContactIndexes, + matches: res.matchesAndContactIndexes, indexToContactId, } } else { @@ -151,29 +149,30 @@ export function GetContacts({ ), {type: 'error'}, ) - } else if ( - err instanceof AppBskyContactImportContacts.TooManyContactsError - ) { - Toast.show( - _( - msg`Too many contacts - you've exceeded the number of contacts you can import to find your friends`, - ), - {type: 'error'}, - ) - } else if ( - err instanceof AppBskyContactImportContacts.InvalidTokenError - ) { - Toast.show( - _( - msg`Could not upload contacts. You need to re-verify your phone number to proceed`, - ), - {type: 'error'}, - ) - } else { - logger.error('Error uploading contacts', {safeMessage: err}) - Toast.show(_(msg`Could not upload contacts. ${cleanError(err)}`), { - type: 'error', - }) + return + } + switch (matchXrpcError(err, app.bsky.contact.importContacts)) { + case 'TooManyContacts': + Toast.show( + _( + msg`Too many contacts - you've exceeded the number of contacts you can import to find your friends`, + ), + {type: 'error'}, + ) + break + case 'InvalidToken': + Toast.show( + _( + msg`Could not upload contacts. You need to re-verify your phone number to proceed`, + ), + {type: 'error'}, + ) + break + default: + logger.error('Error uploading contacts', {safeMessage: err}) + Toast.show(_(msg`Could not upload contacts. ${cleanError(err)}`), { + type: 'error', + }) } }, }) @@ -328,7 +327,6 @@ function showPermissionDeniedAlert() { * Copied from `#/screens/Onboarding/StepFinished/index.tsx` */ async function createProfileRecord( - agent: AtpAgent, pdsClient: Client, onboardingContext: { state: OnboardingState @@ -342,19 +340,21 @@ async function createProfileRecord( ? uploadBlob(pdsClient, imageUri, imageMime) : undefined - await agent.upsertProfile(async existing => { - let next: Un$Typed = existing ?? {} + await pdsClient.call(upsertProfile, async existing => { + let next: Un$Typed = existing ?? {} if (blobPromise) { const res = await blobPromise if (res.blob) { - next.avatar = toLegacyBlobRef(res.blob) + next.avatar = res.blob } } next.displayName = '' - next.createdAt = new Date().toISOString() + if (!next.createdAt) { + next.createdAt = toDatetimeString(new Date()) + } return next }) } diff --git a/src/components/dialogs/lists/CreateListFromStarterPackDialog.tsx b/src/components/dialogs/lists/CreateListFromStarterPackDialog.tsx index ceb07fb0e0..e6d14b2405 100644 --- a/src/components/dialogs/lists/CreateListFromStarterPackDialog.tsx +++ b/src/components/dialogs/lists/CreateListFromStarterPackDialog.tsx @@ -6,7 +6,6 @@ import { type AtIdentifierString, AtUri, type AtUriString, - type DidString, toDatetimeString, } from '@atproto/syntax' import {msg} from '@lingui/core/macro' @@ -86,8 +85,7 @@ export function CreateListFromStarterPackDialog({ items.map(item => { const listitemRecord: $Typed = { $type: 'app.bsky.graph.listitem', - // the list view is still legacy-typed, so its strings are unbranded - subject: item.subject.did as DidString, + subject: item.subject.did, list: listUri as AtUriString, createdAt: toDatetimeString(new Date()), } diff --git a/src/features/liveEvents/preferences.ts b/src/features/liveEvents/preferences.ts index 6fb2ac6d11..bd8cbbd645 100644 --- a/src/features/liveEvents/preferences.ts +++ b/src/features/liveEvents/preferences.ts @@ -1,12 +1,12 @@ import {useEffect} from 'react' -import {type Agent, AppBskyActorDefs, asPredicate} from '@atproto/api' +import {getPreferences, updateLiveEventPreferences} from '@bsky.app/sdk' import {useMutation, useQueryClient} from '@tanstack/react-query' import { preferencesQueryKey, usePreferencesQuery, } from '#/state/queries/preferences' -import {useAgent} from '#/state/session' +import {usePdsClient} from '#/state/session' import {useAnalytics} from '#/analytics' import * as env from '#/env' import {IS_WEB} from '#/env' @@ -14,10 +14,11 @@ import { type LiveEventFeed, type LiveEventFeedMetricContext, } from '#/features/liveEvents/types' +import {type app} from '#/lexicons' export type LiveEventPreferencesAction = Parameters< - Agent['updateLiveEventPreferences'] ->[0] & { + typeof updateLiveEventPreferences +>[1] & { /** * Flag that is internal to this hook, do not set when updating prefs */ @@ -38,7 +39,7 @@ export function useLiveEventPreferences() { function useWebOnlyDebugLiveEventPreferences() { const queryClient = useQueryClient() - const agent = useAgent() + const pdsClient = usePdsClient() useEffect(() => { if (env.IS_DEV && IS_WEB && typeof window !== 'undefined') { @@ -46,14 +47,14 @@ function useWebOnlyDebugLiveEventPreferences() { window.__updateLiveEventPreferences = async ( action: LiveEventPreferencesAction, ) => { - await agent.updateLiveEventPreferences(action) + await pdsClient.call(updateLiveEventPreferences, action) // triggers a refetch await queryClient.invalidateQueries({ queryKey: preferencesQueryKey, }) } } - }, [agent, queryClient]) + }, [pdsClient, queryClient]) } export function useUpdateLiveEventPreferences(props: { @@ -65,10 +66,10 @@ export function useUpdateLiveEventPreferences(props: { }) { const ax = useAnalytics() const queryClient = useQueryClient() - const agent = useAgent() + const pdsClient = usePdsClient() return useMutation< - AppBskyActorDefs.LiveEventPreferences, + app.bsky.actor.defs.LiveEventPreferences, Error, LiveEventPreferencesAction, {undoAction: LiveEventPreferencesAction | null} @@ -108,10 +109,14 @@ export function useUpdateLiveEventPreferences(props: { } }, mutationFn: async action => { - const updated = await agent.updateLiveEventPreferences(action) - const prefs = updated.find(p => - asPredicate(AppBskyActorDefs.validateLiveEventPreferences)(p), - ) + /* + * The SDK action returns void, so after applying the update we read the + * fresh, interpreted preferences back to obtain the updated + * `liveEventPreferences` (the SDK extracts it from the raw prefs array for + * us, replacing the old `asPredicate(...).find(...)` lookup). + */ + await pdsClient.call(updateLiveEventPreferences, action) + const {liveEventPreferences: prefs} = await pdsClient.call(getPreferences) switch (action.type) { case 'hideFeed': @@ -138,7 +143,7 @@ export function useUpdateLiveEventPreferences(props: { break } case 'toggleHideAllFeeds': { - if (prefs!.hideAllFeeds) { + if (prefs.hideAllFeeds) { ax.metric('liveEvents:hideAllFeedBanners', { context: props.metricContext, }) @@ -156,7 +161,7 @@ export function useUpdateLiveEventPreferences(props: { queryKey: preferencesQueryKey, }) - return prefs! + return prefs }, }) } diff --git a/src/lib/api/legacy-blob.ts b/src/lib/api/legacy-blob.ts deleted file mode 100644 index b60acadfe4..0000000000 --- a/src/lib/api/legacy-blob.ts +++ /dev/null @@ -1,16 +0,0 @@ -import {BlobRef} from '@atproto/api' -import {type BlobRef as LexBlobRef} from '@atproto/lex' - -/** - * Bridge a lex blob ref (the plain-JSON `{$type: 'blob', ref, mimeType, size}` - * that {@link uploadBlob} now returns) back to the legacy `BlobRef` class - * instance. - * - * Only needed where a blob is handed to a legacy agent write: the legacy - * lexicon blob validator checks `value instanceof BlobRef`, so a plain lex - * blob fails validation, and the legacy serializer would put the wrong shape - * on the wire. Drop each call as its write moves to the lex client. - */ -export function toLegacyBlobRef(blob: LexBlobRef): BlobRef { - return BlobRef.fromJsonRef(blob as Parameters[0]) -} diff --git a/src/screens/Onboarding/StepFinished/index.tsx b/src/screens/Onboarding/StepFinished/index.tsx index 706a160003..9eb22efc70 100644 --- a/src/screens/Onboarding/StepFinished/index.tsx +++ b/src/screens/Onboarding/StepFinished/index.tsx @@ -1,21 +1,19 @@ import {useCallback, useState} from 'react' import {View} from 'react-native' -import { - type AppBskyActorDefs, - type AppBskyActorProfile, - type AppBskyGraphDefs, - AppBskyGraphStarterpack, - type Un$Typed, -} from '@atproto/api' import {TID} from '@atproto/common-web' -import {type AtUriString} from '@atproto/syntax' +import {type Un$Typed} from '@atproto/lex' +import {type AtUriString, toDatetimeString} from '@atproto/syntax' +import { + overwriteSavedFeeds, + setInterestsPref, + upsertProfile, +} from '@bsky.app/sdk' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' import {useQueryClient} from '@tanstack/react-query' import {uploadBlob} from '#/lib/api' -import {toLegacyBlobRef} from '#/lib/api/legacy-blob' import { BSKY_APP_ACCOUNT_DID, DISCOVER_SAVED_FEED, @@ -28,7 +26,7 @@ import {useSetHasCheckedForStarterPack} from '#/state/preferences/used-starter-p import {getAllListMembers} from '#/state/queries/list-members' import {preferencesQueryKey} from '#/state/queries/preferences' import {RQKEY as profileRQKey} from '#/state/queries/profile' -import {useAgent, useAppviewClient, usePdsClient} from '#/state/session' +import {useAppviewClient, usePdsClient} from '#/state/session' import {useOnboardingDispatch} from '#/state/shell' import { useActiveStarterPack, @@ -50,6 +48,7 @@ import {ArrowRight_Stroke2_Corner0_Rounded as ArrowRight} from '#/components/ico import {Loader} from '#/components/Loader' import {useAnalytics} from '#/analytics' import {IS_WEB} from '#/env' +import {app} from '#/lexicons' import * as bsky from '#/types/bsky' import {ValuePropositionPager} from './ValuePropositionPager' @@ -59,9 +58,8 @@ export function StepFinished() { const onboardDispatch = useOnboardingDispatch() const [saving, setSaving] = useState(false) const queryClient = useQueryClient() - const agent = useAgent() - const appviewClient = useAppviewClient() const pdsClient = usePdsClient() + const appviewClient = useAppviewClient() const requestNotificationsPermission = useRequestNotificationsPermission() const activeStarterPack = useActiveStarterPack() const setActiveStarterPack = useSetActiveStarterPack() @@ -71,15 +69,15 @@ export function StepFinished() { const finishOnboarding = useCallback(async () => { setSaving(true) - let starterPack: AppBskyGraphDefs.StarterPackView | undefined - let listItems: AppBskyGraphDefs.ListItemView[] | undefined + let starterPack: app.bsky.graph.defs.StarterPackView | undefined + let listItems: app.bsky.graph.defs.ListItemView[] | undefined if (activeStarterPack?.uri) { try { - const spRes = await agent.app.bsky.graph.getStarterPack({ - starterPack: activeStarterPack.uri, + const spRes = await appviewClient.call(app.bsky.graph.getStarterPack, { + starterPack: activeStarterPack.uri as AtUriString, }) - starterPack = spRes.data.starterPack + starterPack = spRes.starterPack } catch (e) { logger.error('Failed to fetch starter pack', {safeMessage: e}) // don't tell the user, just get them through onboarding. @@ -109,19 +107,15 @@ export function StepFinished() { appviewClient, [BSKY_APP_ACCOUNT_DID, ...(listItems?.map(i => i.subject.did) ?? [])], starterPack - ? // the starter pack view is still legacy-typed - { - uri: starterPack.uri as AtUriString, - cid: starterPack.cid, - } + ? {uri: starterPack.uri, cid: starterPack.cid} : undefined, ), (async () => { // Interests need to get saved first, then we can write the feeds to prefs - await agent.setInterestsPref({tags: selectedInterests}) + await pdsClient.call(setInterestsPref, {tags: selectedInterests}) // Default feeds that every user should have pinned when landing in the app - const feedsToSave: AppBskyActorDefs.SavedFeed[] = [ + const feedsToSave: app.bsky.actor.defs.SavedFeed[] = [ { ...DISCOVER_SAVED_FEED, id: TID.nextStr(), @@ -140,7 +134,7 @@ export function StepFinished() { if (starterPack && starterPack.feeds?.length) { feedsToSave.push( ...starterPack.feeds.map(f => ({ - type: 'feed', + type: 'feed' as const, value: f.uri, pinned: true, id: TID.nextStr(), @@ -148,7 +142,7 @@ export function StepFinished() { ) } - await agent.overwriteSavedFeeds(feedsToSave) + await pdsClient.call(overwriteSavedFeeds, feedsToSave) })(), (async () => { const {imageUri, imageMime} = profileStepResults @@ -157,13 +151,13 @@ export function StepFinished() { ? uploadBlob(pdsClient, imageUri, imageMime) : undefined - await agent.upsertProfile(async existing => { - let next: Un$Typed = existing ?? {} + await pdsClient.call(upsertProfile, async existing => { + let next: Un$Typed = existing ?? {} if (blobPromise) { const res = await blobPromise if (res.blob) { - next.avatar = toLegacyBlobRef(res.blob) + next.avatar = res.blob } } @@ -177,7 +171,7 @@ export function StepFinished() { next.displayName = '' if (!next.createdAt) { - next.createdAt = new Date().toISOString() + next.createdAt = toDatetimeString(new Date()) } return next }) @@ -204,7 +198,7 @@ export function StepFinished() { queryKey: preferencesQueryKey, }), queryClient.invalidateQueries({ - queryKey: profileRQKey(agent.session?.did ?? ''), + queryKey: profileRQKey(pdsClient.did ?? ''), }), ]).catch(e => { logger.error(e) @@ -221,10 +215,7 @@ export function StepFinished() { usedStarterPack: Boolean(starterPack), starterPackName: starterPack && - bsky.dangerousIsType( - starterPack.record, - AppBskyGraphStarterpack.isRecord, - ) + bsky.isType(app.bsky.graph.starterpack, starterPack.record) ? starterPack.record.name : undefined, starterPackCreator: starterPack?.creator.did, @@ -242,9 +233,8 @@ export function StepFinished() { }, [ ax, queryClient, - agent, - appviewClient, pdsClient, + appviewClient, dispatch, onboardDispatch, activeStarterPack, diff --git a/src/screens/Settings/InterestsSettings.tsx b/src/screens/Settings/InterestsSettings.tsx index ced7148669..351ee60ffc 100644 --- a/src/screens/Settings/InterestsSettings.tsx +++ b/src/screens/Settings/InterestsSettings.tsx @@ -1,5 +1,6 @@ import {useMemo, useState} from 'react' import {type TextStyle, View, type ViewStyle} from 'react-native' +import {setInterestsPref} from '@bsky.app/sdk' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {Trans} from '@lingui/react/macro' @@ -23,7 +24,7 @@ import {createGetSuggestedUsersForDiscoverQueryKey} from '#/state/queries/trendi import {createGetSuggestedUsersForExploreQueryKey} from '#/state/queries/trending/useGetSuggestedUsersForExploreQuery' import {createGetSuggestedUsersForSeeMoreQueryKey} from '#/state/queries/trending/useGetSuggestedUsersForSeeMoreQuery' import {createSuggestedStarterPacksQueryKey} from '#/state/queries/useSuggestedStarterPacksQuery' -import {useAgent} from '#/state/session' +import {usePdsClient} from '#/state/session' import {atoms as a, useGutters, useTheme} from '#/alf' import {Admonition} from '#/components/Admonition' import {Divider} from '#/components/Divider' @@ -88,7 +89,7 @@ function Inner({ setIsSaving: (isSaving: boolean) => void }) { const {_} = useLingui() - const agent = useAgent() + const pdsClient = usePdsClient() const qc = useQueryClient() const interestsDisplayNames = useInterestsDisplayNames() const preselectedInterests = useMemo( @@ -110,7 +111,7 @@ function Inner({ setIsSaving(true) try { - await agent.setInterestsPref({tags: interests}) + await pdsClient.call(setInterestsPref, {tags: interests}) qc.setQueriesData( {queryKey: preferencesQueryKey}, (old?: UsePreferencesQueryResponse) => { @@ -157,7 +158,7 @@ function Inner({ setIsSaving(false) } }, 1500) - }, [_, agent, setIsSaving, qc, preselectedInterests]) + }, [_, pdsClient, setIsSaving, qc, preselectedInterests]) const onChangeInterests = async (interests: string[]) => { setInterests(interests) diff --git a/src/screens/Settings/Settings.tsx b/src/screens/Settings/Settings.tsx index 37d0e9b5fa..0d5b2bcc73 100644 --- a/src/screens/Settings/Settings.tsx +++ b/src/screens/Settings/Settings.tsx @@ -2,6 +2,7 @@ import {useState} from 'react' import {Alert, LayoutAnimation, Linking, Pressable, View} from 'react-native' import {useReducedMotion} from 'react-native-reanimated' import {type AppBskyActorDefs, moderateProfile} from '@atproto/api' +import {removeNuxs} from '@bsky.app/sdk' import {Trans, useLingui} from '@lingui/react/macro' import {useNavigation} from '@react-navigation/native' import {type NativeStackScreenProps} from '@react-navigation/native-stack' @@ -21,7 +22,7 @@ import {clearStorage} from '#/state/persisted' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useDeleteActorDeclaration} from '#/state/queries/messages/actor-declaration' import {useProfileQuery, useProfilesQuery} from '#/state/queries/profile' -import {useAgent} from '#/state/session' +import {usePdsClient} from '#/state/session' import {type SessionAccount, useSession, useSessionApi} from '#/state/session' import {useOnboardingDispatch} from '#/state/shell' import {useLoggedOutViewControls} from '#/state/shell/logged-out' @@ -385,7 +386,7 @@ function ProfilePreview({ function DevOptions() { const {t: l} = useLingui() - const agent = useAgent() + const pdsClient = usePdsClient() const [override, setOverride] = useStorage(device, [ 'policyUpdateDebugOverride', ]) @@ -560,7 +561,7 @@ function DevOptions() {