From 06a8dd1c0acbd3be8e64e7e38abc3ca1d1967393 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Sun, 10 Nov 2024 15:24:34 +0100 Subject: [PATCH] refactor: :memo: Move documentation to a custom VitePress site --- .gitignore | 5 + api/api/v1/emojis/index.ts | 6 +- benchmarks/timeline.ts | 6 +- biome.json | 2 +- bun.lockb | Bin 356260 -> 380532 bytes classes/database/emoji.ts | 4 +- docs/.vitepress/config.ts | 105 +++++++++ docs/.vitepress/theme/index.ts | 14 ++ docs/.vitepress/theme/style.css | 138 +++++++++++ docs/api/challenges.md | 48 +++- docs/api/emojis.md | 195 ++++++++++++---- docs/api/federation.md | 22 -- docs/api/frontend.md | 239 ------------------- docs/api/index.md | 40 ---- docs/api/instance.md | 11 - docs/api/mastodon.md | 333 +++++++++++++++++++++++++-- docs/api/moderation.md | 271 ---------------------- docs/api/roles.md | 378 +++++++++++++++++++++++-------- docs/api/sso.md | 164 ++++++++++++++ docs/{cli.md => cli/index.md} | 5 +- docs/frontend/auth.md | 139 ++++++++++++ docs/frontend/routes.md | 53 +++++ docs/index.md | 18 ++ docs/{ => setup}/database.md | 0 docs/{ => setup}/installation.md | 18 +- package.json | 10 +- 26 files changed, 1449 insertions(+), 775 deletions(-) create mode 100644 docs/.vitepress/config.ts create mode 100644 docs/.vitepress/theme/index.ts create mode 100644 docs/.vitepress/theme/style.css delete mode 100644 docs/api/federation.md delete mode 100644 docs/api/frontend.md delete mode 100644 docs/api/index.md delete mode 100644 docs/api/instance.md delete mode 100644 docs/api/moderation.md create mode 100644 docs/api/sso.md rename docs/{cli.md => cli/index.md} (85%) create mode 100644 docs/frontend/auth.md create mode 100644 docs/frontend/routes.md create mode 100644 docs/index.md rename docs/{ => setup}/database.md (100%) rename docs/{ => setup}/installation.md (83%) diff --git a/.gitignore b/.gitignore index 389f31ad..18bf8f62 100644 --- a/.gitignore +++ b/.gitignore @@ -183,3 +183,8 @@ config/extended_description_test.md oclif.manifest.json .direnv/ tsconfig.tsbuildinfo + +# Vitepress Docs + +*/.vitepress/dist +*/.vitepress/cache \ No newline at end of file diff --git a/api/api/v1/emojis/index.ts b/api/api/v1/emojis/index.ts index 6098b562..4ca850be 100644 --- a/api/api/v1/emojis/index.ts +++ b/api/api/v1/emojis/index.ts @@ -73,8 +73,8 @@ const route = createRoute({ }, }, responses: { - 200: { - description: "uploaded emoji", + 201: { + description: "Uploaded emoji", content: { "application/json": { schema: Emoji.schema, @@ -173,6 +173,6 @@ export default apiRoute((app) => alt, }); - return context.json(emoji.toApi(), 200); + return context.json(emoji.toApi(), 201); }), ); diff --git a/benchmarks/timeline.ts b/benchmarks/timeline.ts index 0639a3a9..a7d9647d 100644 --- a/benchmarks/timeline.ts +++ b/benchmarks/timeline.ts @@ -1,7 +1,7 @@ -import type { Status as ApiStatus } from "@versia/client/types"; -import { fakeRequest, getTestStatuses, getTestUsers } from "~/tests/utils"; -import { run, bench } from "mitata"; import { configureLoggers } from "@/loggers"; +import type { Status as ApiStatus } from "@versia/client/types"; +import { bench, run } from "mitata"; +import { fakeRequest, getTestStatuses, getTestUsers } from "~/tests/utils"; await configureLoggers(true); diff --git a/biome.json b/biome.json index b783d3c0..4ba5be90 100644 --- a/biome.json +++ b/biome.json @@ -91,6 +91,6 @@ "globals": ["Bun", "HTMLRewriter", "BufferEncoding"] }, "files": { - "ignore": ["node_modules", "dist"] + "ignore": ["node_modules", "dist", "cache"] } } diff --git a/bun.lockb b/bun.lockb index f4cc5ad0f77e0f037ac8c2c2218bc514f4e2a8d6..4069286d15d715c65f733673eb30bf8d56501bf3 100755 GIT binary patch delta 78414 zcmeFacU%-%yEWR~&`6<(7{QDg6|*+j*o_J*ASx<~0hOenBngTE8?&IOSUQC{j5+6= zGdQT2QBg7HoG{+?)ZWeXJ9pms-gC}(?)}I5*{o-+z4xED&Jrz4zN)bBP|5sd_p{Xsx)!DPtYTOxn#qLyuvRFN zKSC%C92p%Vi-~b@G9EEkCN4n@rvc}S`^{&fk6?W2zd@aR^Z7N z44r|+z-@~t6y<;xz@i2i4R{1IOBUfX1wc;&rvlvslA|h!X9ElzJWS4bFG^Did?kogjEzASW#Oe7_{ZW3 zMOENcU=`pVU}fNZj$?q8z=MDlf$ccf2bKpf4WxX&!#UOb5wI*U$68^Kjr|bHKv>05 z1X7I*9EWi14Wxo*ATrf3J}x3W2nFp7omwm|G;&xddL=kIBAPnqG;*OPFO6JLhU9@j ziZ_(?sN!b1XcnP8iboMz0^ut;EHEy5c$7kMlk>MgO85jwW1um|Ivj_hn$#5!fsm5> z1F5M8MaRg4=mI!ZXp5MyDrXyrMxxM|K`0}x!J=SR#V{33N|j5D4~?QkW5B5_F+eI? z5XYE7PT^7U&WiqMaw-#eOmuXF!syK#I^`7|8z_4>j=M|;rpx0F;UmGR5u<_dV@U49 zBLs&9MhrnxBlm$$1!xVV*3tl9f$@O~ z#e8r|7l}wtAwi0<;AofR2w)jSJw%9$pMc-ov9~Lqo#j6hj)Z zHt!E4|J^vY22$CZ0MQ4AkkHuB z(3rqjn4u${H)U-b5gHgw1&Rizf`mjzCd7wF#BFTG!j}N40)Yr8^al!xZi-}A7&Lc1 zTCh>^LB$+ogHy>%X&CPTBt9ZAVPpg{y3P3&Ahl2o8@!5RqN7C>2bBwJzo*<=K9FL` z6EsetxGaWS)K-;n6=zx0Qa%#*ShNqU=*Fg!4UoFP3`iNq1jdIs4GA6bNzeFmAk}bq zSa@(43KAX{nn-=Iqcy9*lr}8AG2Jm##0v56iGf9GD_4stMH(7XJ=nSv88;{LYF@$N4~vgyd+>+W;|Al1l*_0^cEBRiFX>s7w(FiP2FgOHf2YES8e6 zGpIJ@jW}|+yHf6c15J&1Xo@|EK;vN@kh&$thb_^+fz!e`6-bW7POMUsI1U9;_@>SZ zMRnjTAg#ocy0CR95WEz4JP^wVR`&47NUV@2eJPzmksKQu7acJyRB^d0TiVY6siX&h zRFGgGEofVjfEJbc-B}Aw0G0z^4xQqq0g3lQfvC4aIWFnNbYlUG@kT+fNt@qa1N;zB z3%XNZHWQnHJAhjUu#C>2wd8R&$SHnAOlaIWt{Yd+zpk3mK?wt?g^jD{3B;!gk{<&( zdXJ3kfmX<)*{2BV?&0=2F5f1 zFN*N0Ke&fC^M|9LumYkQ@fb#nI$eyyO|gM90NLQ$&wvT^1D>6&)U;P&DN&&}ITF z*j%8298QHnO*bon1c4Fh`mx+M#jE09>T7svVs^7VaBt5 zWz>YmOc@|K8WtWexBbAl&|#rb@o}MXK?&gzA&Nya*m^nzSQhbOIrin)ZU&m10vbag zhn0Xd=md@*rZb+;F`MHajw?8(a~uPtF*JmuKSvLaO@Y++4jgR^-1wfx0-kYu7C<@$+H+k2r1ki�KXB6B?fyW^|#+D)3Y8e~FBZr5_)6wJTYHsKoPDvkXH*hebrA=|e(? zCIn(-D2{~X5Uv6i1J2+D8pR733Zw!D2S+DF#VZs)Q8UUgGB9>X1FWhaILDoC5Z&zN zgHy$@UO0sqLL z3Wqc}4s!HCL5hP{1+M|zzlUXP0Zs*;xtE!Ehk^zm^{F|i4_!HRQ=jE;>7 z3)|0H&L2n>F+0HGm)&n*O&1Xzf`cb^zqshA=5SIJ5vl2ndqe+&%(3|)=I9YP4ZhIW z*yvcN&_v^{@)hXRRih5GjIx2$HO5`ixEmUG%P<2!ZWvd#zdpryoy_KIH#jZ^M@h#R z#sH~{iXLYfwBZ>WAGbsYM}*T!?&As8qR)ZUGBxi;kNBYIcoa*ai26g0 zZ%EF9K?(W*O95lDSw>#qR8kj?Z_hACMZu|Hlh5)&%H!i^XBas?dyaKo9KuT@p7Bn` zADrgDBhpiT_A+Ar|A1qv(WxAk!9gI^_#w}57ivlw4va%b$HxYizR2PUz@pG;%+T7j z81ZNjhplJ&$xE!sL!#rIf&=41qoQR{jJ(YBJqVZgf7KOMqRkK};}t-1JfAz5$@y?# zG3eugRN%8$nWIP^Z#D`*>0(3UV@FUEgVzFg&Sfn-7f2N;d7b&eZjAYdt3L!Q1bjyX zS`UmhdjL)~Y6zr)scy1@e!RgP%mb$kpK@-TCH?bQx_f$-t|tmYE9l@`Ed5a+b#<>> z$e;SSIU-P>WZq`D8%X*}AT1=naeWfUVI0%**`SJ#jG=lcs@&yW2c0Uk4xCyH(=Rq8 zG+q(H{dpii^4t7`fn{hsTpACN#zW?g`z+!LAQjLU&v+;`9xkhMhmW4Inq7Lzy2$N0 zYl$n+sX{egFg^`PEpp7j6KGztdHN0sscF)I_P~~}*r;6xqy%fBQx`=(VJqMkq^Fv^ zea%+LdEgXp0+6O>0^(N$ro3fKa}1FDh9bNQ&~S)53;fV%qy;xTh!f#6?i?6x@Z~D3W&|hATKP>4;}))=z4i@=b``Ql2x`>%A@r^zjI;p`+s`lV)$?0 zxlCv4!(VS)il8DiJEW=%KLKeS33OmB)B{Le!7FO`5AM6XB$kmlaOskit*Y-!`(sz+&R#+Di=6%^iG`?k`x8QL9hgGSi3e3bM}ZyT7f+ z?vtBm&b%?{rDd(eaz(3@zjZh`yGrU*zhsvit;@J=9=T@b^xUa71;4LX9TWF#2yAVB zTV?a|a*w08PKrHNDLbrC?jP7Q?Pd91ojleSyYk~_+sn=DOO}l2Uft%>#1efYA|@yo zygr^3v*79o}n(MT)jYt-PH+Hhy@`jz+X3Wmn5clW;7aejwyRYz6+IXt@6?gwvmkD}}vH(c+q zxqsgU_E(C#M3pS#?di1W>LWwR{I+%{yH@Y_;d_rOO`fI2dWM+44w)y^_-#(|XmjUa zn`1k4P4*?`lPEbY{sgpjH!9GkG}DY-~7+CTpQ(cyXMh#E2=sqMEGBC9?;S6bJg89 ze0yFg*y(0-wN^#LGwEQXxSZqNe$@3VXcTxUcTB*ZrN)xxJ(8ZdO$N1eOqdTF6AyeUu5Ln29!TJ}n zqyNGjuyq9#hK&L1V`R!)G1E=2Zia=&&ZO4b1(>MmKUu$=!6M42XvA}Uk{D& z1C06z9$Ym-4=j5fSs40n8CWC6ZaSNTbP;{DTA^A+g+dS6M$B>5sN=v|h}Ot$4J;Ze zvMXi2=;@_bRjwqadg+wOVy2f~eH3T5W@5IRR{2r%^wtY?ab}|g7^B`AVF8#I%MI)y zSQ9XFG3T1IIX3tNOJi-ZC$;!Gao!dih)rWhB4SQ zFefleF$Ybs2doPiyc}@0#ObL43-ZzkDPV43MdT9Zfl)-Ih~^ZWs~{WuoV_{wuL-@UW5Ia#tnx-s+F2NsAIvxS};<$iOaWKAh z8eupXRg(3}8ZcIkoK_gbetMyjV_{m9&=0H~^Xse;c7wGAW0n32=3?}uYF1ZF?V(ey z5i@(}RhIQctDZXLAknj@JT36^w3yiw^QgXP<*&nfG?6eGF-taqQ4=%v4vd;u zW~!zQMXNqKb#OzPgV~s(3t_Rg!4y^A5HtJe1p{Uk8*$Wn=fGGW(V}V7m@yOx)1WgL zO-PJeETQwkFvz^MLO!f!(B-97Ri=rU8lV%}qt#JWh*Zirunu5yt=0R$T8n9YT`gfy zZi1NOrcsU*GyCa;|KmD;G<6 z4aT|{ttQmR5Tn*XDNvo5LPnL_4#wIM14aE3tf!dWPpj-MrUdGRiI}0hsj$!Gib4<; z5RK>+q@y)6NKe}jem)b0V7*Wu`I9SD4{bOQ49!U0h*tXzj87e~3p`BT!UP)(C>9gb zR?ve{JHiXLw>U8~M6cW-3ZZ&c6RqeKs#8a5Y0bi7s$42&hU$g8;OKB_cnsdE$b~c1 zyb~B&7h%*6$zYTb)&Z?X*bK&c1MQ&xOfhLORkds-rVi2x!&(`KXwG40OAzvEA*TCl zgtuV4Ra$9OjdWsam`)uArG-387r{aWsX)l|3TN{CQQKj{Vx!QzT9P40Lhe|Ng~57Z z3p6S%dK5Kjh%wO#jGZTvz-S(@X?O^Xh74P^e!_H?v(~g`I-BUj!Kj=?owARZ8KqaQ5QS*Ha2+F)vcSTLN|*E~WUd;Omxq`YtrONmq<+J) zH`v({q^q1Qill1LPE3u_3F}Y+>PvZ*5FB7e6|x!s!oGvW7KTN8n#Rlnizy8A!PXaF z$ZC5l6k%X!e{4`Qz^G--X+jFwVAP&y39MK@!C3cWI;%7t#niYl7{@}qUO0?}gDQe? zkGj4FqiLz2#lf_KvENczBjz34!__V;7_KDi|EIUQ$)-B&H_l)JqVq71I*5 z>Klbt+0KYan(~5}G7Ph(i|sd%9z19eKg8L{IhOm8{rD6RS=tY9&%zgFegNAw!4QycnFkk4qX@|2i4TCeKRSF}pfDKkXRB%GN8 zScaH{?KDb%F%w!EG(OaDOv(dumyb^l{n$B&jpQLZ7m_nxs|L2^F)(Q(FiV^y>FWKalcN_eM|35ro6ABADAyjk-Qg`~&4omx!Jd z^{QKg#4NP1J4Spz8Y;?VqA*D>vaF8nh!=K zd2NbRybWs8_7xN|gH63yk!ClQQ%ViWe#D7T9#)AWKxj6#8} zol3W~ zn^;yhwTNm9ZNZwzvCwyx31Bow`D)X`5_2eJhF3YF3!8l;jPdS@| z;NXoj?pj!^oK)(YVAKj|Lv&ruVWO3&QznR>qFy*Kj0F{;8Lp~6T+9-2C>qY%Nfw3G zU{qGNwgwV~155)3B?)RYF`8ZbT_(Q~hOOl)Qta*iX6gNf1&0;B$y5Bln*U~ck# zP(E4ER&#YiY!Y)K&neaKNn+Mq zox1uM`QCW0R-FtB=V4fTVc{+r7M?J3%O6$;77D8k?GEa1%(~=?<4V?ioiIO{wFG9# zFpaXL=(#|zP8>`7fzJZ1`aCRm*+a8&%4+~@6RjYn$)RfMmut+bA7|;3) z#m0y_2G$Jq!@La8SbqZZ22(2Vwm}@*qMqnBQLvm~Y}Ish-5M~q`A}!Q0duDW@`BZN zBAbvn#T|0C1fd|d0A+!-0mCYb8DD-9-b{eu*wX`#a=_?l!$#3|Fc&cSmQDQ$%vHX# zZ#&sIb8MqE!bULac*OE^ww%Js1a~Nt7noiS^4+Ep)`MxmtVCaw{t=in7=|xcm6XB( zi+i78U}(mET46WW*~vd3mBj+X4T%JX-F>R7B@C(`?ipwdPi2jSBNS%UAkNrvb_G~h z78A#pQq!0_G?|}9RXR<~TBK7CO`}yjZID*AJx%mltW)opfy0UDvskM(n@J;ztj>kj z{6gzdq17~<4j>dS2^RKkSSJcC>sdz4uh3dpXgw&joJ3>1ghJ~8EZl=pS!Nq8&q8ZP zp>-J+_9Vo!pJTKF3azDu)&rxZa{f*9TA@>?WZ?0Y=(9qrz7MOHY_*!JQ25E#ELbe` z1*~qe<~9$HzhrA6tZ><~n2(2NvNZ@63q1~NkgU~PfNfs3X2R+(TdxYOP794G*1=*W zFkfVh7YvJO`(gE@w5n2zMXR+s^@7DTKCRbkg=?_bn5LDw!V)%$*?!j@497;=nsF=i zJ6Lx)jm1*5CmR_Ujvc|8fXU03dJGtzO00J^htX1wUSSzKM+x$Vmj~7i(XqZEL)+ik zgus@Cvl2)b1Yv!}h}!|?17CFUNEY(-bl;fZ#MNLU@9v%9M6 zVE&@Di&ki|lEs&ALxgy+PR3ZmQ7~69cKZGS)(?!`_AFY(<^W@_|An<#%^t{MrReT# z2|`6@5r2SD9ob^lWKCg>QS^ae)Tr_XuHFHLgKjIW@Df%>=Jo_71GIk*S+)ly4YBt$GYB4>8SMtG*;#);nFzH_0~{?pk3QtVT%04nX_C zT7t=^Gr?>#>kO=WxD)c&EL!c-38Nv>@`tBZxFOuPSxnufQ@z_PW+ANB7Pe-gS8*O1 z4CX}xOSOHAn7Ugh*luNOC2RYRU~E9+0isIWDthhF37HUE!8`gGPZdXPGd{b@!K(Lt zJL^Z>G=ur=VBNsjQZVXP#@>MSEo44BjisV=>0s>;h6;mSO4%W3WEc0Jjg+0cZO4aPXM@&7SQ{RV(+pYszwd-D*tYjU4HIgiqW}ldP z5HBhAu>phA7~0|j81*#{7(v)t_A_QJZ-pS<2r5cbPIY|0m~}`elssUpCk;M-FzQ&$ zecW}=Iv}PV)(ICNHi3xVcG0NK4`PcEty^hT;Ri*pBY3v~f#w@}(_5o1bBKB~4Ug{n z!=iRUGCZ3+0!D)tJ0;q)089s_kdOD8!?aFYA9FQ_fxYjjR(%B)JKmN!QkXjiOB*n} zFmTffvtYTiY_Ygp0VDTpeic8;++*4E*Qh!k6|;`(gy|5eoajTWFmLP2rBrjJ&IuDY!S=Hs^qSYy#U~`hakYRTd z;b1Ipnhh&Xidl%FJH-|^xj@1sFgNBC>*z_a=3rPXJv8dCV0dbU1<~m=tF?UlB@6|l z?!tDD6URCp#ttyAz^M2*W1#qrGueuRcSd+ZxfyH#!q7g*t8o?^mTbXX21e@%Dk|c+ zEEr7#%tf?v<1@ylqQk~eFbYEs@VN`DAs9-GvW)*jp=b}rs(&4x9{73v-3%OJF=cnB6&6OG}aNE<2)D*D%Oj|E*ob6^{G1;9cEeB1Tcz;t!<;TB?whQ4pNuAg6hhT z3wy(&!Niix2IFmptS*54nN{(tEDY}S&XyolTTE=^I}Pkl=hwh+EWyM0($|;+R`M=j zr7HzrgR;bDi(JEIb^t-`EH`}{6gHa{1JY)2fyjeH{yE&VKpga3(g%z+UH*`84 zKHQUaEIF#`d7>9y<_^zeeIjQgoC70|@(qScxFx3E&bHJ!g8N2!~G0Wr|z4)GWwk#wx&6a@C^puwh)$4pQ z^_EVkbC;EtHBr!A(dxEN*aeZYLlw~1pTJsxVF$r};&jg>b5-;`(dv#)*a4As2i-ip zyC>Ku+8Jz<*28<;q_6HaZV>AJVzTI0k+7PoBCIVPr=A{=J39 zCt5w$sV}{wtv9=cR;c{mSX1&B1=fSr6i=9sgZ){KY9H9Xz{)WUjI|!6I|(*GPKOGs zYJC)~p6Y~|AC1L9o2Vayg^JdXwSvzlwqc>r{WQWju-+_7wA@WF=7`d`ex@hLNTW`J z6(m|e)e5g*QLnRN)GIJ8lmUztin{w880%6hqU{%Be^J;Hu#m!B&A;+$vnSeMeJL+> zF06rauKwRlQ||*~v!2G9@LkM$sZ+Q6PK!(SCs#`tlrI~X$HAyfSk$o*mj1!IkFg-I z*uuJ80t=QIz53Ps6jNX8RM9`hEWqxcqSYInx|BkxXeDO9)~dU}3J|T|Xw@5Fb(Xbn zu(V_eVfYRq4M%KTeKhKgVDdYC^<7xF(Z(y}x+*2^J;~ElSfOItU9I{xEI-)_z?Uq2 z<at zBv`?6eKz9jk#TaUgP>Hj7t=ntn!~_2fTKOIsA94s;U^fit;|#@R-)Bsop1+V$D{$p z@@k1MVFt>6Pr_n#Dp!pBQrq=|6)xv>3Kol3$C^qaw+t+~jfCg0Xr3{j&iG=cpIn?Y zSX2P{VWjW`j4werxc0!eF)0n!Vhq0nV02u;39hBawnz!|BbWl05}zt~bX8kC+`>|D zM@UPAuowDcz^J-dV3RQ)!Q8-*8#Wtrd<(Oon05nS0)W*7Iy)((fpw8@eT$Y<8s{$- z`OaW$(9)8h2Ih=3Y#^Q}Wa)Uk^&O0+81lh!+@%!rf|BE5V-gsfeyE4A2aJ5O<@pn5 z7dHh1NrwOfYH}Okk9w489852n;(qHWUxeP^$CJwLvHuHIV5Q z8{D8Vk|-vnMBlKf+|yCq+`>+M3z0EZU9@UY@Q?!=R54m{h1Tvu>$}lXwW%t3DV5mHsv=X?0C-|AI7nHf4sl?;%ZBzU zFluNFvZ>BW2Psvh)Tt{~LnBFPDy0@*-9alcbB}ELn4`Sa3&n1KnoD4Mv#8HtdNQ+F zjY2c&PxIuTrcKSl2>ySXOaC;V{%JO?MNK9bY~-Kjkv~n#+HwRcBP zD2{hsw#u-1HwBFDvvAD9cNEUml~S#gI#r2!QWj9{kMG&yD`MBQs_FG4uc8RKP)|xF zELLC20t!C(CT%mM#}~5ZYE-H9C9h&gmxAxx_L6*xp^xsu^kb%`0ri}$PJ-#p%p91$ z%&gXsx=xM|4U@%H9c(Ch6<4-2bj7Tq56%2;_|QeD!pDyYuD?TykK>Dc5vuX&gAeI_ z@u7>*44?i)aG4_MLGtguFhik~d~B5E3=}T}A9H-dh~WAsr2N9=SPZG$Lpc8@EQ>f} zc{~%u+Wz+e|5jH@)+Lk;4OEWFawLYtr{F`F!ZDTOR3Ke{hgAMFd(lfJP?X7pHy7`gw$b6@Ug&WB|emH701;a*8r)b*5gA3+K3Ou+k_8Y zea0G2k@cmzk-zRAU>4O34G`x6!1A)13##wIrz}^cc_xA zODd~K6-z2h$lY}b@u*f;C;=`)%H%2$Tz`iYeoYSjdnA9?@u60|gAZMI(;2zgak!t-EAF3qok>u+?K^iBxbCJ{G9z(v~)2l|hNT3>hAcBjK_(vkROp)|Y z_z-@^hpxXv>JA)z<%^I^9Cqca5ass&O8~_&;}QQ3Y5d^4Dm%nsRPH*QGv&(^sm3?~ z$~DI6N{)}yl6(4O>R+$nr&~l3JDKD;2g;`+B%&~vT7a^J0uIQKB6m2+fOCl~pGTrgR8t4n8;&dbNpE0N~f+egzNI+vQ5J+i* zI0h5JMM#cAIsZGP^g|F%Js3~%q~>LmwGGr>iJ+v6pdTE`9Lf1VAw?O*T-4tnj zE`m-$i+MO9bamMQB%}y?IUXQgdQetb#X#wg@@Rx) z9^;&l%;We$ZchTM1D^oN-E$yi^A3oAiuYXq#PuIs{|Tgg%*Y8x8XkfQO9#RfRtO-6 z)qh4jgL&$xkg6mM#jY%Og4g@lVkZKS*!N z^%k7B1X9oFId2QZKZOU!_CU(e3rOiZaqP;&y8|hGFCZ15w}D6K$A$jDk_Z?IBuDW; zx(F#j0>@!oCnSA1$3(81B889Ox+zi@7^nbr5mLz}07;s}bwYA5nd20WDLmX1DPAfM zr@uu-7a^HxT%XQ$Li{r*X7Ye^9$<>(a2D792`Qe4c+}()_m=^rr1R(pM~XL}hcA%B z(f^CMu$V_Aq=-v6H$~Eya@`b3`km{hNL{!RI;C3$q{VPEk4I=mTjw?|{2h{mod~Cz z?FCZQ{rEu+4-nx<<{{1rDf}>yq@!FXB>fo2<3M@qCW8h;7LYQ`2GT`H<~fczKpG=g zNc;GzE*NstJONSxo&)JJMUr0P2RVAn!wJcJ&p9Dw z@PTteGCy%nNa3G3|4j3rYW{-@gtCcJ5UQv&e?ijCxlTywt$-w1b1Xpw*WV$fFNyHd zz*@8)7K71*Cm<{b?#cQ8HX8mnGN76T@Cy7N(D0u|{J#>9x+)k2qb>*q;`hMuoS2u? z6iEt~BjA^@c{m{@q(4MSj$(mDfk{04?~vyEB%W>x$5bEy>x9JTasE$8@#qh4(nV+kTnnW7t_M>11|UtoEoOYy z!jLaQim(+(yZ0W>_wsN;3g5?({(vl9|AdtBK_34Q$HP3`VOljQ!ci^|(zz%LNaxo7DZ9C?5#lH+<@C!_@RIsZGfL%2H+H%02oj$AiIs<5xDqyL%l&qy`ujs#S& z9vuC-V^gH?-q5MQeSkEp2Xa1$$0wxlFyhFc860WUNAn0VJi*@~IT*^r|0_rZjN|DD zDgS>FjGSsbj7Ky@%6K?*!bBi#ITL{7coN4c98-Z5Zz_=dP3L?D4^OARUO@>(F3jO5 z0V%;ejthb0Xb}%z!ue7lU4%3XR&jkbkkYRMlK%}rx=fM$Z8VqbPlg;WBRSg6IUz;d z!MQ1t<6T@gMUwXMbo+TYA&rGoK=Pjrq(OLr?wasVafRb`AeB6i^L&mExc(SO1$qLc zi;(zJ&P|arddq{6hnYZf@CV0pK#F&f^D97dc#ZQL zoab@84J1EzIKK-de-D6Gz*j&6Id}tun&uObcma@_j6}-N97vCJ`xvP;e^15UvxF<7mzaDT7!b)hLehVVn;K z(q)QNz!A_1NAYkY8jJ=Zg^j}xay*_#B&32(;hd0o8j#{m=Q<%3bQb5PNby7-E&-_^ zbAh-$F(@|k1poga`u~5DG1X)%uhDjnJ9&jnu`J^K!S#PZ+JF8<`2Uzw{{L72>bq=S z@_#~#cb3N^Bz}%_QzZR756>yW_y3DrxWp3>QpC$Znx}bO|L-DP$S)KB-!BwVf9gX& zih=9zkP6a64rNHA|G)7@QT7mopX4u?V~EttMoIrE$FG-*X=nrLieE1mO-7W7{)7k|B6#4wSNN@z=}6fBTa0*UQEK z$-6zAHUCHN2IVi;{7(u%H^skRF49X!I`{uKUM}{NpDX;YyY)MY7ITdMneU zUfIf?l7F_+S~@_&3`sZ(!SXDG$g>dAr9C9=1L=SASC8M$RlCCoeL1sEBOB&U!$F!H%1H^jf;{OTVB!8E;-(zB;eQy@|_q?2KExf6NN-Yt+1I?Fw%L zr{okodSG^_*vS){XVgEmt8eaxW#|T_;)f0WGR9Ks5`LLK{INCkmQZi_wCY0#uddr* zn|kok$K}nge~!P>`f9O?^ESWF70TRCYGQa1)1}YxMs1ds4^0bidS$Ik>FMoC`o>?_ zop0ipGBIA}yQ}W(KI**3)Pu86CpK;Pymaow>z*E2L07LTGe?ZLQ6k-TMgQC_eO52l ztV{cHa{1Va&o}!G>9%ibZuOKQnNxb!cx&SKcljH-#u6WR{b^;$yYllFr`1+YUT9Zw z{qcR_L)Bq^B}Q47A6qWNan<#blalUv5BTF~Eopy@?#`Mq7i;yI-eYEn^6ADW*NSDB zI9_gA;DqBl_Vz!pbI|xtvwxh5){iWCXL#;}<{QpzeSLA4i(%KHs_xqDombvCZ1XHJ zKeT6fhl}cR^_KUXIJ;q4t9`TEPaiqi#PJGK$A36p+}YP>iRJjX#Ja%)Gn9ctUnKmN zvaUh7p#6j)8OO>vFy~35x>i$csw=Eu&J>boTM`i1sFZOEi%jE0tJN61_^mzLiU5fbpR-LKw z$HP9yv_D^Ue&9XpvyQ~M zc70XDmTh6H+N?ioh^)S4#q2vKj@Otv{(W}XnRPxDCQSX-ZMmh{$o9^62e<2bW48Bz z-Zz`v@LaPi;B}dG3v3ow(2bgMt8;XxHT!KhNf{;1S9Xn`8!>0p<4z`i*P8mZxoR`c z`^=nx7lxbD&YW*$*WgX^d&`hJpBna5SJ1TYTK+(9ecK_slFb)3udsW_)7@^{Gm7sn zzb?Vxylzefmy{iEOh+;OewOhve_r96dq9n6c8_1&QhfX7@3cdebZ|jI?|>TxV{g=G zJLkE!=D|SQ#Bny)=8t~)dS&NohP$UW)mw66al^pp?J6`iJg46oG|uPsrjEO`>)>s; zac2Cuw{Jh`2XzauEw$p|nd-qo)#sny*kb(8IZhi}_xy3O&(UTV+?C^gzC5Z*TjOxJ zcRTgUm=f({PmZf@;&+3IUxT?*-jcU_51&4%?rZ11`j2lR$|F<9CGK$w`c}VPBbR%p z{T9ZbIOBKt$;E|jL%$x}b!cAnj!jRh*Ig05we*w%-(4pTnmER9s1;u30UNE4^*iBx z-qEdV(%zuEH5=4fANKIYg4bR5m!ErN=I;1yHuVBO?5%BOy?#O9`I{lywFAtr9vge2 zpT*i<8mB&AKd|4bWGmYyrF7I*)x$u)`DVP#SKr&_-Bal^P<_YtTT;&%#Y#>8SU0lC z(Al;Ntm8fv>m7gjdV=?$9bwG_Z7R2`m_4)EitCCOxg$qy@qV^=Q2$Fi>9-e+ExpCm zuXA{>HVZyxeq3tyKH%~U)$mJGN@i{9*XHul`UZV$F^5IA9j*73DD%*N$*fTklUjS9 z?>jiO?5CCM*A+ZzHDpQC`(28dINoaN_^V$?+WGfxCC@F*Sy!|D+QD1CbyqcXyK_3) zGO3?f;9Y0EVP~B-MTI)=JeIAU+NRGzKbseR^&A>kUa{m|?Khnl>abW8mUx?~W9{ji zX)CV|?m4}}`STa=*ai%2?Ne|rKiF-Zc3F*$52Wde`L?Nt-VSRz;oJiJ#4A^F+b>RPJjZw2@@@9Bj=3qW7j+$fz=wYG*4V^5OdVH_Ju6gt zm3GoK@~L%$tJMcBesnTv-k4zXr~X5V@0!2NYuez&QsjgEi!WVW{`%~ioYaS_EN@MC zZ)aGJ|MqZSiPLjv@i98yY3jK8xl)017JNv|s8rhd=Wv&e&bP;P$ZuG4=WqE_CgxA@ z*w*FoLv5z({K4^StcUe}*WhxOpZ*ydyCILZuA5x`WwodNCVq{-T}k83ykUQ{u=(W- zo}MkfrmZ=YIdxk`X6LZMWm~)7ZMEa@lO>T}>j#B-zY31gFE7&JDn{~ zmA$t;@Me~Hj!^%U)-GSJ!SMj%R6o*GB^E^+{^2sUFR-O;+x&|j^9|T_SlJcLxM)D z)_ogKzwBvjVcMwVOWN{>va@oZl#kPka=+wq32;D4CpaiQAvh$pz6>}l%_TS@y&*U% zd0qh=la>-3mkJ0@NWNDAC#5w6rzGVyoQOK) zvL)N=fU{CK!8z#&!FkF41|UZoN^n8SB)BL!+yq>bMi5+^@ zIzocwD+o0oLUlu}96 z-a-lc3T5J3C}mXA1ybz3L23RDN;#D@?j4lVq&y(SUL`eo4<+$ClsWHF^$Jq{dup5? z5IjCWs3fI-pvEEL0|`~6)*q>HenMFB5rTvC2ErU?6`JM~cy(#%C-BGM>d)XcCEw3T zkfDUIgM`|WvH(H{6@<_N2#(Sg5gxw@uCZVb1@Et;!1%!#;AvBkANU$pcq4^I88fokg2&YMSK!S_Z z_$P!!O9*p*LeNV2B-9a9)^j{m-pW>U(p6{+q8~u@Qfnpn6f4AEp@h&{dP9P9Q3!r2 z2yLaMDhQ8BP@6&UkbKP`WE6w2gM{{y(i}nuYY3s{5Im(VBzz{Jj2eQs6sU%Xe4yQ4~UelwK6VH4;9M&|hj@48oK$ z5LOg}5GcJN!MQ91KWhlV(o$;(k4aD$hY%|H7Ke~g4#Ex+21!aA2p#NH#ifBZDlcWY zw1tGvc8F1?1Y!)40!u(xZ4coD36YYmEd>Aa5E5)5L`(K1Ay`&`kW>=FP$`py-6S}b zf)FQ-CJNGlCtxRgu6X%bqMfiOZ!DFY#~GK3c-jFMc+La0*( z!oso;lB6djTqD7!90Y?jw;Y5iRUv#QVXWk72f^6^!Uj7CxEw1jmx|B)6ZW5d-LlC7Al_7-HhLA_X9Lcc?1iLyA(yBm^q+AkClhCRvgt<~m zRS1cW5MGclUvhDPP^T`0g$@uFN>50*MuJZ@2#ck;)gVl%2jM#jOC`_h5S;5n*iaq9 z?@|E?k4Xrq0bzx-rUrxzCkWOxA*_;m)r8QY0fYl2tdWFT5I&O-Sqs8CX%7jj8$zg5 z8^Q)Dyfy^?Mi8<|*d*E4fneDfLQ)+FTck`9c9Y=b2w|Hv!VyAP69{=E?2sJmLa=KJ zA+0WiT~aOyr%7m455gWPr5=RDW)NPGuupQS51~$T2n*{&I3PVC;Tj1(P7n@BbDbbe zX#wFo2}dN)1`zPauhAY2ARLnlNO(*_Ktl*8q%{p8WYB7B-3Y=dsaGQi9b6zBAR$u{ z8bkO@LS$nIXQVwOtZoURQWFT-Qg{;x{#ppxB%G7%n?kU3g^<(~LXMP4!fq0rnnAcI zjc5iTtQCYj5-v-Q%^}$7Afz>ia8=4B;WP=YT0qE^Qd&Sr)I)ed!VL+3SsLx(24SHF zLZ0-5gli=DI77HC&2@$_r8VaM4`JfcNx3APCZSbp2nA9~ zYY2&65MGe*RdQ(qp^i6%g>4{wm!6PtjRc>z5PnK?+d`Pq5yE!}Dy5m^<*vdx#0Sa- zcPM6N(ic)5lM>(oMQtXn^?;Jm35s<)C`HVq-tC}t=nUlmDT0|~)gH=cQX<adMe9H`2-at7au@HDV?B_^n{?Y z)VdR(iZqv?s`Q4yLGtVjs3t8Xs4f){)R26;0BTBW2x>`6UqEfC7eO6q3xT5~bXD~< z)TJ*c^UK^P>0}=r@axYQO)bHnAC*S)o|NLyqn`*DO)`-70F(557 zr)YzYdp_1GI@N8L&&5)Ymi=7WD1LCaGUctx*68o3xzzW$Z^_J)t>2fj_Mfuwk+iUn z%HA;V%AfETydYq3#f_bw$GqQaCrG^&oA!Tw&p`llQN%H zC5|65etA@G@mUT{DwSydW?^lcEBCfW9PU|Xo6hBx)wb^kBNXc%TpiN5YR*I}L$l`T z_@A!L`}?e{bIqi ze1mi$X<=@;AC;G$s6J|?vp&YB%!xhOO&>nOUv@3L0c~RHxLcnFDGOIy`)#_{^z5VM z+nXOB(PsCA!L}RX^QO078sr>Rw$`WWYTwoumN^&PUG4At`CVLA`MWRotr$IMLc#bA zm%5wyZEE~m2Q<*lOYeWVe%_n=Q$LK_yjWeU{j*bTE{-)nFzQv0ch`#S7j7BDoo0%3k{mb~1w*op=@|?6z<*W86 z-m2c*8M`a)w{V+UwAFsYy3My^KZF{tjBV>(=}6nLi>-e4{L;K%+r5wXwA@^8;WM)z zFJ7BCZf@#$+JiasoA*szX<&1a5i;(?AEyHlb^f_cBwige7aQy3&#xGFE%qO z`JHm>+W+FGoB;u$k-t@4(X4ZqfD0WzCSFNO?U0o`?D*;Rqs$=ESj211DaQ zY{a+K{#cbZb^gXXlZ%&aY~r`2so(CSl{Z&Ci@CSlFw)l|S6qB@S#Cl>HR;%kS`BPo z=S^!oYQew_TN^AYu<{G)+HKCUx)t2y=#efqw`{m zYx;$mI@XyuHjf+Yo8);YVOQBfJ9L)^Z#PqHEq!R$qP+Q~tX+w50al@&v(?Gy~N z#-?n|y;P}BwQ*NBC6?Isw8^<^+3sn-H~E)or#E%HWvR={QEN_i+H`Z6=F#JKMJ&o+ z*gETsN6zcnTSC57ZE*1Iwl>PMwwtC{8+=-)Gzgiq_MBt%`P%!FrIPn1H<)*=vq?+1 z$$ufrxXoUhS^KtL(SGf{ddFA3P0cCV!4R2gD8B6Lj#7E{i!3kI$t#y%zR{kH;8o;}~&vuFPa^C~qTJ>u!LK?63K zIBsL=*th!q#`&dg_deUC*1FB5I#il9wS3X6xVO)yPd>cBzSGXD1%sPKudU(srfB7b zyXq`G{a z*wrer!?8^zCmuR=w)ioJtWh8P#_2V!x_>@1b6xDg;phEEwh&UQ_ugdRWR7(CYEP2_ zyPNu5{q?s4Tkq^|ylnO=7mFzi+D(06=seyjP?s`#!C2?R&+ASRKZkA9&)d5G^{Vzc zH(lMP)R;SWaz@U=n`xcw^Cup9WjZ@NOdY!v{1h6;xrbalW#v=yTxWgWoq#&+hqm1L zXzn|Y38|wF*XZ$^|BDy>SBBiy*0ns?w4h>CZmy{HYuMSKD{awda3^|AXKaafrjAE$ zu!yhlrhAX4y@he}w`uwgXE@=C^1FGf1M(>)%#RiK%_qteL z+;;T+{Ii`ZFLL;Oc`eh!O?y+nm73QZTXNK<$<7fWD_WL!S(8+9hxXvoPdi69e!rta z>#*!9tILI3Jn4Dsb$ZKlR`;XrmYl7ADm5nMi(+&T}BKPoBat*Div~9PF?!X&N(Z+U3+kURyOg){ z>DH&8MkgQLy3^iQT$V8*G|%w4^%=i44HYRFC)QP;wEo16pVA8|UQKb9*|s_#kF_gu zZgyIort$m2(g)Ny)Az>Ni<({~k47$f|8saLMefXuN4~F|mV91VIXCp}F{fDX>HBxg zn6mNd1ojsa+4%G}ZN2gJXNYU>EQ>u+qVehxb|soGEq&lZ;;c&#=XNkm9x`g%J=Y~) zI_T}J?G-iWeO+3z{i6H*&cv+#JZ^7Znb()X<19-*mlh60@v0V?c;MKoo|of;I?pd# z*7oAIU8k?D8gkyc>aC7TW4s0|&T$(5azoz2i}jn|Ih?=pkYQfRnw-H^I`=r&wNk#Q z38HuY#@6#OE#BQ#kOjLz8yXYF_Esf49$2 z`|pdd*F3o_wRfMq>#d^tdA<4`_#tM`k-`4oB%eVlyN>hK7utrO8{yZ~@~HXE5#9%z z1uaVrT35}xMMnIp`orJV{yuNs#NwseEy!}~+uvXo`s%`ziMc0Y$KQ+@9Z;lMvG)&6 ziq}~R#9f7C8HU@S6J2rV<}2BD!`((#DV(62bcCS0WZxa&Ck-X&A!QQulpOp3{?Z77 zUQ!N0Z^^L-ppP_`ps$om5Fj<~3Fs%K5cHSw2?j_m{(wL!oghehLJ%yq?ga>u<`RTT zZwLlTp1lEsq@@I5QUO7@asQ8fU;5RuGa zsCdL+n5Y$qV7QpUV1#(VV5DdmgkY3dz+kj^$6$_I7 zqR1YCAW8T$m?X9`NEUfR5lj}L45o;^45kXlo(QIi-V9R2Q3gV|_ChdS^k*x)ZU&pii6e@q~X504GxM$ z*i*b1WEf}2Q&YqbHk3m1;x=K*Bp-W4+JRci2_uQ85ZY}-a2RS1HZ&)be&^<@qYT9j1$IWOQmIEz|6V~H z=yq=IJqEgxxgWAGBiz>-Og1$VaR@34cV1B^!apK1G!O?K4Z{&GBqx!aw`Yu{zkhg` zF4suqWEcD_#My|75Rywt6dV@WojG`ol4WpK%HWFMW8inSiN^8PTImxSt7wxQ-}~nj zLk1h1h3`aE@-Mt+aZan{cbNoY*%VeWi1SRsH_>1c4$}>Gn{Q1uR59xF2*>FLXPZOY zaTE*{9jK_%GbS>k5>z>YkLYtSoSA8wQydFT&2W@UnXq!TWQE)v>8$XLa5gH&1e$$#YzkPzx59qwN#FWQ`8~8#9Yy{ zjj?*dr_bsi8@Vi^(5^Id<&XE|rm59dNxBM>hx9s<#{CYa<7cEzB+X6IY;fO9(zx+J zK7M)2gF^6^pmUc*-n&5p8Mp<&OnE>^MM=XdTcPo5Zg=K{KdxiR$8&sqBp;WGl)*!W zzL$Jl<&l;Ps3mD!peZkon>>}L0iNG_~X%@ELmtS3S?Q3Zf!)bei# z4~e8U7T}B|4wW=F(0H6L|At9gMO?20jm9`!(%f;)-@TwQj*v7DT*rWq#yC>aJaNr8 zNccBO(h|IIb3zh%_$V*EfisdeM$$O)o|Ck(lI8>2c}e3zrM#>RT#&T!l2!$@OOiH0 z(yD@XS$lDlhnq5b%1!# z*!@<6#*FI%+H3o3C9NLqFHthC2a&St1EHX?pJKy7>9Y-ReT1DIf173EhPXZs8oTLM znYIzG%OWkiq;{lgV_fstHU4SGt8$ob2%2L5JY$s^HwE6os@ZpUN}8XfxggvnX&leV zR}A59No$VlsVHVCgnJ~d1+J&D==j?!X)SS`BKh`lK}$X)`P$&RC1^C{!;;n(*F0^VhI~ZQ+Tq$4G#c_zNo&vYR|Aojd`uEK z)cSx%OFk}X9dYdr8ZG&Rq_N9cK%*s}l(f#cE)5zD`IMw}!L=)BH00Bg))m)0Z@xO~ z|BNJd!?hELwB)mr=8tP0T0u)bCu!Yrokh~lOIiSEpHY=H5nhnAKwQ5D%@^TCNejaD z6VPfQyaXCHPH>DV5X=kwT?P^V>wnQ$09P3oRe-9%cR)3uIzW%HhR}x?^IK};GAob`$PVNL zasl~J$ozmkPyi?h6cYYn#zF~2a9I>^0bGG%0FS5t12_oKr92EA0eB|HCSWr_w{io( zUkg|W@C=r2fIrY32m*Ql!9WNQ3iJf%dWHiLK%@b~BK=POK*&sB7BCx_0|)>=Y@&4c zM1(_uVL%;`yF2u5jLRlKQ-CkKb90&7s*ZpY;0zc66JP^m0r-CM-)OdXz(2qT;3Mz} z_zcjD?;^Md+y@>24}nL(72qmx4Ywyix3ScEL z3Fw6#3}ej%8Z0fJmQKq~1UNXcS8?FtJi&Q^^8n`&c3+%Qs4EOTZ{zw7kQ>#P2e1X~ zfV@CHAV0v*z35%@bjz8*EMPV;2bjy#4(H)wKCl2-2>b*r0u}>5151FVz%RfuU^%b? zSP85GRs-Dte}FDKUG(-q2cRQB@0{K^y>AQP22=#x0gtRAI>6YaU_)Ft0vZENfTn<7 zR&grKSR{cSx*d=g$OqU1g#ZV@87K~v07?R-fwDk(;4j$xYk*GmHh_-w&%k_uzmLJA zqWc2_fPuguU@#C5@C@WcV2BCB;80u`P}@8&r!TH~NOdd_2Xq2D16_cwKsUf2pfer- z&;btuS^}+rIzU~Z9#9`>2s9RYe;9LaT;>7xp!3n|pobC$gaZ*k6wn)>54I9m1<*%Z z1Iz?U0cC))0DlAG9qM?yG3gS?f`U79|98q{z%av zfY13`18o34zo4`F2V}HF=n518iUI|J`~aVi^B1mOgXb^c32+`b4Qv8d0m;B*cB(13 zmTX~Kz=NlgfmT3A#L@eux7!5ZQOWy& z{eTWI06SnVyMW!mUSJ=vA2FM%^aYg_=sN z0eS+xfG{8&=mB&9S_3}l1KDtT@G;58@&Shy~(+K0sffAJ88d z01N_xfpq}a6Pp087q$R=_Rs`q3itudfaX98pr!EWX)K=55tp3+{>I8uF`zi$0`Pl~9KZ!M;0|CGFb9|i2w*xe0|)@6rq8|)!6`u%jLEafS z3S;4-`v6cH_grlMjf~y_{{WwWH^9H89R%+hfNSRzcB@d_1OrU~u8BQS0x!TDs08=` zm4PZiRp2|I8c-eh9;gA-1bl&7zz;xepbk(Ms0Y*s8UXz1qw~NHpd)G^f&VA~fs1M` zqJ05njf~J8umGh2SAc6+E?JF07T`15lS@u6F}nafD0?%o1=tE~1GWP@fStfDU^lP_ z*h?pBA1?L-2Y`dXA>c4@1UL#D1C9fefMj4KFbaqR!hmof0^q^7X=z>{eJ>yk;9E=qfIpCgOvggGaRAp{L~no!@?`(( z3HWCv+Po;>0_1@*v{-7L1J|_N^cIWR0D>uqH9#T7k*)+#63|M^xYD>U1(X5$(dWV6 zQ;t&j%TYbf%d>`yw=hQ-?b*c?e}IiX4GfcjBw!-I4$h7~4j2otuhN4ajga$+A5aeH zgtXNG%3z1*NHkx+IXc3)Ek^=ZdXoUIPWZhw9T+}^NJ7ZC@iHV2?~?((Gcgc~O+h#r zAblDjfE0kM9nxk3GvqbzdCjy;N55qbXBi5hP%T4dG!K{yFm5@p09XR>Bk-Sqg#f?X z=KaqAKQQJ8gv)?60H?n?z$#!RumZS<46(_f#PjtRPLJ!j=I0!l3lUC04qyZBYXbbJ zk8z|En{mAfU=^}fDxo?{0L1`W8T;TPgk1Qz;JP5dQs+l#2XHaNMNKY%t|G_X@y_^1 zKK1#05!X8bREshoi@niC7sm^@I}e-#&H`tE)4(aQa)7aR0w-0D-VcEL!0*64fDS1g z)7!u;;3mMW*6YAE;3{wh_zn0KxC~qZo&t{nmgqU~2f$KkbdDyxex}kBbbm@hTBet3 zq>`9ID3A$rqUbq*>_9f)4QQ`{zW_4|@(Lk&>7sB5GXWgNJ^>$r_W(OKW&N#$SV_K< z1QI_0|Hyk@lSUXINDt^FI7Zq4jAMpeWU`&|0bFRZ>b(=#%Dm%vLO)uIEre@&QbmBb z5V{Z0MqdZe906y*3@EJ(Ic#xyV5NYPKo!6TpcQch*Ni|5y*UGWA)+kc33vb;@+$&v z0EhkxKzX1XPzE4xT4ap-@9xnz{<{R<=C2IpSW*^13rVXAF#Hba2-E>;13z#Et_6G# z)C6h(^gWuQDtiE&3_AdAfOD0Gn$u?%6zZfVsdTAYmc?nFlNY<^w+gWL|-A8So3R1o#yNm2y-qAiG=@5Dk;I({w*y;&ZNLtL zn44&HN?=C4f&HNE1qP^zl|9YDxE=^_TBG27i02$g{zH=YAVOrOJAhE*(PU{Mw_v!R zS{N_`&H&dxj(lk3fQv#v0l*%}599;#0=WS@fD0zBL2?1y?9T$^1h}5%y7mDw*CW&c zpK<*LxD31kUIH(G=fI!9GvE&J2k<+M^foT80q22Bz%Af7a2GfSJO!QrkAR!NRp1Kn zD{uxl2^<4x7bg%hEw2gf`jn)dP7_B3&$Anmi5ZdUEI@&p0MfL33a5e>08KE%i-5+j z(bMu#HsgQGkdC|wj9}@tgg0<~Jl^V1bsPFmoe>T|*Pdnjix$TdQg<1FcOtHjt(X&Zs08T$%zJkCwS+GiTWUwG1?^ zwbE-$8ePjk%QWr%*RHh!Q3jj2Ae7Q-kp;fS{;x4Hjr6{EFBfq(o$KbD-Ak# zy-NZm09U{XpbU1~jNLmy(}odRDL6i-9k0oQv053PwV_!Xk|{XtaI6i*9Nk_59Cn`p zX^k<>h?}64Kx>zOGvsQ`r78G-PC2yyH?;d7E5{m4+k~9?T++~r;`#$>CDln`+Z6|q zr6~rmS~Ip)W;9NxX={Qr=zr5h3IC#v{>QbD*0!`Up9LkoGyp?Q_*Yh{ISi};W!gm5 zv36+vn(b(lP1;N~gU+<-p-2&K6J_F+{3}aO>v;T2Yfq=JV(jey|BZve?ZfhbC*T3N z0~LUZfE!=|ngUG#ZWVTfd~P>#>#zpEZAES`a_f;>kO@ms4b^bLJxK0Fau1R;?ng4z zt{KN`?niP?sD;oMV0+d@NdCq^J%BRm0Hikn>I0e_=F>>hkfx)OaYIQ=%ft-(K=>$x zp#UFk@DT?+XkLc^!9Win2nYlMfbM`l(9J0Bk24laXp8GMz>h#{pcT*(7y zqKC%~nbUDxjjo9HaI56zj^2To0*JXYd11+0?frrgT)I^?%Mh9mu7K$S6%L$FECe4Kxt#1QL8Bpek zS`&?qmAGSt6-VOb4|Y@5_9_!$&~p=nWxIjU4&lYkaeJSf$-2!zRoz*}XCetn&7Z(P zy@GO&N;&M&&J7HR_HpyZ4volTG&vYt4B`f~s8kXCf}E8;^xu-JU*K|ZR)#28a};8j z$+7$68#mhU`ldne3ks?uF;SFHGCDes!);F7dRM;N#nvM@4oR!Hc~yoQDWWSGmV+S= z7+Q?oW>Y;W)I-&RHM&(yNiurjc!|SF#v#u1Zm@HhxaCpy>g9gjGaYywP0X0DHz;CP|nKO0O*ID!N; z0#B~6gm<#h(}yD=^YnbY&f%5w!#CjY28WkAicsq^i~2`z*l}&OHRd~`n4N5NFmpU% z+B{2!6+ZdHl%a|`RamZOMsWrll{$bUA2{mFs6TjS-hZBhqpBMVr1UJ>(gTmk5A$#B z6^e9s#tadKCmS8{;m7U`Mn}FdBwjyxe0in5(v}>;IOV7{vn2e-Q zn5}+PxK2URE20{Mr{c#c###C|;>8ry-4^VP)2uJ$sTz^5K;wo;<>}^$>eKBNHK&4o zk!U~F=w&FHMNFKE!nxsz0-1_V+Z~0tM%T_ zivlBh3)N8>DrLbAJsaU^V22;CSKm_y45%a)H7{ZcA!bFS$A++H0do=KrN;a%JdVTk zjoH*1zU%3m^Sev+r+@*q_wA$;#B!>24N0m0*>=m$H2iDc?*@H5dOs{*C&bNZsF0sg z8yp6H8(r#CjgF(ULpD1P)Y>L2DVZ0{2Mj3KO5qO%9HcTZ#n{x)EVp>R-dLarY^DR+ zg}vz3-U6S7{~B`7q(=p#zhgudA1N~d49s-MX+!B&!wiG13{!-MfXpSLF#_iu;G=m& z{ajMVpaaM+ph+(Dvvc22mpN976V~w%TBTfle zWd)cucHk5LYniBCOxYJ>g&3T@EOo69t?%S(9Gmq zI6UV*f0Y51s+%R6O^5zlMN#ry1|J8t+ME8les;&a7%Sf!F$a8=3fjW214FmgEghb= z-tK2*_#H!f{>pO@&GZ9KUj^67y3l;uI;lcWCnVms0(~C(QsuX4HK2yr&qtIVcq_cDwkV?8UZ%1M?*F%4CrA zz}SJPqhcrXcq%T>M+0{hITzq=s3^GrcNav(v$$&|VsK|3U0R)O{&*a2U%2PcJ9heV zbUpBg`Dot)V-pV93z6)NsJ##sJ4+-lMCv7CIRfWcFtfe(CHUQryt%y#bmL%*_8KB? zB9*iD1>x)tNp4Tdt2NF2)bkJ|u(QyCdY6^o7yTLoL`X^+V zEd19Py@dNBqaRu*VG-)g;-=tCu2yclk(`(wS<9`&*~LctEXoYrRm@q8 zV)p`>z2HcMYo4>`H*EwnT?CEU%egvgfMcTGZuYp+E8evQ1114-v=c>tM*aDV@0TEm z6s>+X2BV6%lQ>_DBXO1JjX-2uV)WMd(#2?eX^92J#U;kVTGCugk#xVPwG<6}5(5T@ z5tkx?1se{VI?+U3t8zxlUs+6A3Il6{frA{`BdZ^J++yZ-bPTvh@Hups5X1SuPIBR$ z<*H9lk@F$Sc~e|picVsvDk}V9>|y?^s@lPAf*bX^nX~+Ddwq@XQ3d$R^_^Jti_yWS zOQE?eYS%1_u8?WP8iw5xd0a&9&IRQumA)-Hnu{|W}yj#Gh>d508lbwZI)g%a+G&ye8r0t|Lw z=+nA_P03RuCaT)erPEha`}WQg8|!78QLm#_wp}&hw;T>r5pb}A2R;oxIXd5yVpfjQ z)iw`ZZo~lj31od&8y~4rO>tt` z>NTjD{b+p-9%WwK_%o)^^UW|s`cbHYr!`d%t9g9bj%#bza}uHRj#g6#2%ooLV8M@P zdsM2C-?0;lkt<`I(N~S>)7G=_gh77C5QEhT&EM8nG+B%OXZ966)?)0~DAuntN;yB6J<@#){3n3l>G!qxxRe!-@>qyiaLRE$@M!_Z#$;)M-$czdn}ah*`fqhhap5qXIZwJtmDgWN004<(MTFKsXMi+m68eTJkxrv0r&$-KU{e zKGhHF;mkJ`XhBu}_-;n$tfTT4WB2#;LUp_my*I!NzV5dxPB+I#xXh0DP(AlXjF#<1 zt&O+~5*-mZ_x4k*&>^(DZztHgK`$3Ox{)G@d{ac;O$g+jOq@Q>U)0)%$*$%mqm8qF zGxT0)Ydg&n*VUYz@b^c-{k~)uUu6DNKy_fBpcxLJ+1Kc@Jrqrz#x5<#Pv)SqAB8e?zsuFO_ z;eX)!j|>xp$1V)H-pqB8Si2cjvrg!DBRC|Scf*AI#qIqVBAaZ17Z4y?Y{hjVuXl=B zTcG0cma5ecTN8A;T-9crGGU%>7|kw<(_4^!xG0P?&a+ypUDx*Rv{J>~+g?}r6dz0x z>LoplESyp^G?nUmWaV;Q=i5d++X{2c*et3zwOL%ZLHq8a8Up9hZLn>J`r7{6AK9)B zn0wd&kKbGAS*t`O7a@k?7KFdsVw9u}+lh(Wp+u}qd;C%G$KiJh z2PhoMDtD&X3*k6%@+t!J@8By5NtRr54o#oZu${uE6emY}VYdU5lXrXUzk|cQMe~je zZo5rVIFzdR5izXABIUN6FPtM;twm*)9w<8RKncIJWcCu8@??<=1{msD*p%~p2r2|Y z9}J^kjbGfY21g)Fe;6axj`**;uVSP2SuoFgqBp@eb;L;y$faYnG{4jlW1> zOsvSO#&Dp>!Y-|o9Z6}MZ*-(|(o``9ODExX5L1m>Nm{lt4@vw>T;GST*K&^$XW(`g z5qrQdyXO2oMsI`kFyNH`xd&tD!Om*8oOs5s(hnsAmvCrN3W}BD_j}P2QPM?kc*dN) z)4)sd@He133lcASg8{zzI0mQ1;=S-b(uv2wg!)Z8Dk|MiF(zf~Os$~*T%t@%>)hI3 zo%XiA*jTLSg^Ab=fzyY{$_I_jHc8bdeS2igP8kU@5@pNDrcK+N89Be2{3|UchxIRw zPwVdLXe=HcR8~~~Rs(5<9vY`M=-RKtCdd=%{|*K*>(QMl7D#Ji)|>cY`AZpW}om?u>A-1?bA&)go}jm`MLZM<1I5fx2>;g{uw{`ai1e`4WDjJ+sFo@#MEmYhCr*l;)>&Np?OALs#YYz^( zVlaDkON*kK9M^Ww3fdq7l*Hpmy0oV{^|s&r%x3nDKe%A#G81FhM{yFnVVDsg95)W} z=^Li1>rr!2$w9;NIw|U69RR%$!=1KM_jX22pUACRZ7pLA7jsX*NzEUw&XT(;%(_zT zM~6n>Ktbs#77rKaPN1l6;UddPY>dW&lkV(~-JX7aKgpjPqu`@9PY^>t^!rzP)*iE~ z6oMEoViDsJAtFwqY{SH;lW?3O#P*X=`<1wO65EzWnf189scUy!ISS=iI_@oP5Z|A| zh`|p%>3UQv+MvsT$&GMSt)d&6;e_aY3j4!_WZf1u-09Nd*)#4m%63{Ra-T*{vYwWT zTDUWy@j9Kx1oriMRhq;;t1_;*goy;s#Th`>5sLo3GmIu1zK%M`jq`xHJ4w z=QWaDwBw=zi4_64%8Dd>zr{Tc9&b6N3lu|7GdXMDFJvi!`8^r_+|e36{DIft)YzY z=LU-|m#{FnK3FvW4dJuF;@U;j*k`1$LyByxD(s(}HwC-%@=1rzR@h!be~XM4W+qSt zc*@W!NE2KyLEK^n^AkkkuSN%v?=t#vtgu{$m$5QI-7hGA@u%psz1DS7)KWYJEp<|& z7<3s0xR{6+tDsKFgFm+_?cCD@&dPMGtLmO7iZz$f)G~$MUVNnJl0(%JFD`X6MBiu) z92iZ1O|%TZ!6{Xi+TzS9?3@%5$9^@MzoEHS-t_gO)r+S&a+uoTV#YN1v|(|HK}wkw zqg|OG=AuRuw4;oAw4LzGsl8ua%*Jp-*|~lguG-l2QXR_`nAjJKMmSvT?>g;pq=~Q^XdQKuVw5W|gcbi{Fx#_{jann{zcj(nC)(Zt;? z0me52QEdJ0xsP_*QM>LI8HJciBh*9zCyS09_hVvJ#K>{&D;XipPNc~VX;*6%@LeK=QEHFh<5V)s_O9J-A-No0G8uT_h4Wd$ zV5H%QJHoD0SlDp=Y-+>JArIYX#MmLmF<-v0*2QfvNNG@JI${bUW?8OFpFY>V=v`e& zr0DsTg**~nD4}Mwt7YhZ!>9AC5SGn@C9uyiX0*Da|I_G{cSidJ_J{Uk)Eb<-Z1R(W z1^m#vZ!9s$1wexsA6@lAPqMIkS6ez_5fpCWWHL)uQJc3pIN0zW zIbJ*|SI-{3wmK->p!++kS_6I+_bqh9!OBn$TU%`1jA|z+dMheqqMcOk>oJ*Z#qyyY zYT>dhQ2RqZEgv^}_Z0b{MyW!&hsI^`)Ywk{cAVT<$?yait90x|xWR8|I$q4jQ0voi zyjtcVf&aAeI2?KeV!hmyebW%cIHBxkI(W6c95$*LVmMuUxa)?F7tfHkQW7{=qdUfo z9KCuB4pCD$sO=)eu=Wd`cviP@n`EVI36N)CDMZ4kJ3P`_t<^Rg0D{ND*>Kyoj{95VLZ2&Ba6A3tN~itR7Wyd%W0%w1(H? z#jh7I$bKF#a$Pj`FzEFn_M*{KZ2Hak*qk4|m4cHic9>9VrtxvsAkYV#gJX z-hsHM9TZBb(Es|%kB_apX-_7I=iu=92o9`4kKDGsZmIiE7M)l-QRG zYQDjFd0LF?L{Z}^(t1e_pC%_ZEttGvfR&>zVz}^d|6yj=%~@;>)|hq^#dxGO_mUi= zO)EmW=KZdpl_P$l*hA^b;Go4G$=Y1hdlwY zSBLee=aF({ua)Drj479|jF*1g-38W|*Aqo2q%~(xQuCcs?P>9z6K0OKauiMylPKK- z9JH3X&hFdJKMLqz<*0`k4oWR+oGEWEoOg*erc;u*!L-qmV|l&M&e1hX&$e=mmN8-7 z7Jj~zycGkAR^M}zM1|`x28)}hcO6b;c#1kFZ?`X)uXEB2Y~FcuN=647jTr8-Z~Xl9 zWe%rfC9N@wQp76A@YydpE_rVMJmFK%?9?l?nI`pPiui!EX0uQq zM^<@0uvs3vQD?0j)r4@r0qLE=!7Y@(yUd99o_*}Il_L=`lzzWf$!xD{*EwK~nJYv* z)9wNXhUdh|L(J{t`@Ak=<+vl$w!X1@!KlB6I9X%x%;W^pnjNRByL*KPG}He+=whOk z!)JQRO$cue23CyiowFVlJ{peSLJp+}4c^Yxlz~f`VUrNUQ6uM*uB`@s zpX;_YX2C*{>ke9Ey-eF|>)Z`a7h8vdgWeBp^zcIA3l4bk?eD;yl^U20i`21jLrSY@ z2R5Ckhy>E@$g@c7yMqO0NpR2=4evGJMW6L;xt5>@1d+argdMauHv$KToo6*G9qnhc z(H|W0A!O%8qRw56qq3aldEjJ24*xMJR`iqcAwUhfZ>>c6x8AFE~QLiT|Lsqg-xZ~2 zB+EQziTFg>S~zp5aQIyvYNtcrD|7igKd`0a5{0tUS9?j&SV+i^Rm;|Un!bAFgm2_ zcY39Wz`gk{Qc(Ku2i`X7WY~&j#@9-;f0elO08^)YQsm>fTFq;06TABTirwTBS`Wnu z)RjO?cF63sb$rZuhphRO%7&kS_a9e_@efh9H><^-hsH#+ah>WFc-j`z#qDh2Y|xie zi>4d4PIP(%&MfQ2q(|sOF6-4%ZC}Bhll2!9nsUVOsLVIQnqn-;gWT>9%h_*`cVaCr zl$0$>U7hvf2GaKY@(wO(|4j5@sx4o7fYtomY)Z<%e_&VwhTEOMW( zPkD+a&ay$Q0;5ln4eCVDXIqocC!*`=t&HCzhMUu`JC?klyO7-58q;Ni_`pnJ!NDTL zHN9IY{7BKYN`)wyjNBmHpFsKy$uS{rQ25j6G0&_rmLrCf$f8?k9^CvTbiOs_=mrsw zw1(Rogda7P3TGCW|63{lo8aHn_5Yo;M( zpC5LqbNiJ|y;c_LAOAB3Cpka1LQF1bd1KRZ+klpf(JQdNP=>9pyTm7?#qXJ7=g(2N zzr^HzX_sj75{f+7B@$mku4%U_cfi%6b|(TX{W%8$`WN&RI>YIvIdn3XR?`d3T@z0bn4|Wq2C>|=-NN@HK9^9GS6qU@`-RJ2C|=G3sy0t@ zxj5z=TGA67tXRx^*$=9#!u4xXh9-&d(MpU@70imo4~ht;wKb?!bKLu?TgP)*`Y0Tj zH$9av={c5G-&CSlsWAq1&(V|3Rq3GEfwVX-op1FT>K+u&DXJ|pLv<#0GI(xSmv;#_ z9#~{gZ(Rgp_|R^^4whxeNgiobY&@SynN z4W{O+kdPM=jvs0M{!N3iWgvlzTQtHF?2gD z2D~*U!leIs3&(E6QQ`hKGM{=>b-iaj&%b>CwHdh~K$^DQG1YJf_?ExpWoeyL#z3_B zn22ZEVc=jrXRR4FcALvC^_?+gw_-A4IRBSAUSjL+TMq|)k=Fcm4rPA_OCi{J4v0we zCJ6mD*!TV8s$%|^ZlB9C=ynUM@R!Gh-#aMw2^<`Wnp7yY{-;u|G%M6Ow-j=oP^a~S zPEnU%*Kn+3P3w3<%tl&sS#VJGA+v6Ym*sB!0}j?Xie2@DI78`;z`+*nmTy$aRg(t{ zm(o4);RRyyBPMt6iM5^Xj{h4m($|bQA&R_5+GA3>yKzEna+tqR(ki8jI;m>5{loIS zHN6)FBZfP3$hX}o5r(wpuh&S-DKY;&Y%LQRaugbMN<4!MgRCL**X!ZbX;J+jL28m{k%E?RcUnWnddC0e&E2GRI$DDVi#oiWaPju z9A7*~%esZDt@|YWz^rx;T5z>MO;Ly87euZP(6sLb@#z!7VHbq&2iWhV3nCVw&#Vh- z$~DJVTq$=GALpkU>|(^wO}#g{&YUHe{9Y?D%Ff=J3*s7Nnzw?3cJaQ?vq|+A4X$8K zd+36&`)HhL7;sUn_-ITt7%mC7PsUSHiyrdcLBxN?W8zbn#fHyNY4HW|@H5^TN)+P^ zCa-BBCMQG1-=+;WdHPiOO*MoyA?D9J+MfFj8L>;Fcry?~zw)mii?-RbJ3a?uXqsr| z-+!C-)Z}3P2nKFnretcE!9?sofB@gddusL#oe2#u^u8Qa6FkDkjywAQC@IXymd=uy`I zx{)$w3o;GWB z)Q<0XO6f;s+VDl)7W(bKN?x|q;(Ov61e)Xesm9Z@ z`JK}xE&xzVw0 z9;wUQHwT(e?b9js?QTr_r4UmV+MR3Iab9lA*e9}MV=!v=NX$iAL--@%QVCj(0wXL%ylcp@zIMBqnK91OJ)+9C{kB0iBm{E4t*`k7Be z9y^3f#MeV__kEg@AGd0Do+8l&BZ0gzl=wrq=QBBo zE_p#5|A!b<5;tSbCMT6mR%mJwG?g_~WpZ=nXKID#>_6~jozq*sla+;@ttumPmF!Z( zRLx|?n7{i|-F zp)~EM*_FE4MU60I*|K;TH5`(&1%|Ys+T9+m=eNDhWD`oOlI8-)GxK<%rgJnpTu|Fu9 ziD%AGPfMA;Y+6sTHknp7X|%E_9kgDOYPSLJ#iYU*BjVqyqn~H=$}LU*nW#QGS0*$q zC$*zU^WXH3IOAfq7<0`JYOnFxwEjr+sL8FBYEnFFE%~w!qC)x96b^>U&qTqZ=+f^% zuL3#-*%L+4V+R#Ad8cYCiPUyL6)>aMIk_N2_x5u!HAywl4Ck*lBUMmsF*-K5TnoquD_)!4sK zS+kA|b!M(>)GL!(rAX&^kBd#tSSR|7wef|>^lkg?4y;T%=_ea$;F`u-rK^g5TN;O@ z)K=3>RM6KOSDeDn$c&{ zBb`!k%;Ox24xdPFi)9BfP-$=n%f~9L~$l$=H4mr|2O>F>eG|A|M zYIC&KKQwyBw>hfvU_n&L>e`_#vr&+5(*yN;x$bx@I}V z+*)5IGeqPN-8ex*Jm%mY=2pSKjMV`ux8)oB!=_>KaQk7p*(}hq~8>!Y_wt z^ZyeAv1Zg6wbh0>ZDp!imNrkN_Xbi|mg>?YqcAP8HoE`2F5&rKjQ%J_rduKZR~hA3 zwj#9c1jGMdW`p!TS?UHrLQnZ?s?P&wW-pO4EfMPszF~{i`Sm<{<+*0eiaL$YH<_jG z8!7`^Pz*Nr@psAd$%N>JfhJ{bf~RSfbY*STG!BPa7Vq-ttULJS`zS>|4T zkdW_oZNy+x%`E}jwsjDd4^ban1{V3;H}ZQWk+S3V<#8|a;2GUQq~XJ(iYr{#bnP_T zkuPoGvj#|8ff#Q7Zu$LXjRz49*y826GSEO56BZiK3!!a<_p{xlrwl+o9@Tg>kEbp? zC?X~x1P68At}(~4?$}=y?Yy|99uN`~6df9=D;i(&a>tq;Nd|pHUcK@ZLV3b$pP-bk zKWO&Ibdae^fbIT`HIQCD%hG<8wny=im;bOGVn7TYRB}7!^>>edO;%trwk)38hM!(l&}jwvcATj{G1L*+hSs@Tf#a0aFE zeP5&rj0}&94GoL&`~6Q`zYuj4`CaHa~MkjVNBiU^1d42=jL zK4DtxTO*RU;>{`=A4(k??Tv=TQE5GH|2kvia2RF)(+lbD2 zQRNkPo$?_-C46?eZ~0;u*SIibByH#G6_QC~%h$-);w5fYe7h@^Iyd?8Zx=|D2Y-Fp zBngPZV{n{VjYLI}D5c%;@0LY0rG-cpzOLMkf@=NPUM^)$cp`Qo4G$ngEd<2GSo((e z$8NdWqV1)3n|!U(GSqg0YH(j~e16A~(cn~j!x)9*d?F*my8Fijx%*%EExz;C1xi^J z^ZU9AY3npWS~m6mW=7u?!mmW5e%xcu9%%LH$9Kqxc8)Y(S0O`LGwWMrt({9g#m&&U z)Uv{zRBZNp*|+#J4txyy;oZIs-TK>awVsgj%0V55x;B`9VtAqbMWH9OdG-&i@jz^?FtGLM>BH5=!m&anmZH0%%%a+&+--s+o+!bR+Ah>hI0;NW1B zVwYIUtMjV5iW$I)JalbbMXpB3-``d3y!W1NbK2wb@vOq3%sn3`2w!mcWcK+1K2h>? z*SVt3D1Gnm$xHJWs{dBeOo{QDC?+y(W}hFZX*0D1C2g;X;u^ixk* zr6*4mc8wuD({@*6Y?ZCKPxq{TsYeE1Yub|&MHi;c?DGRvvrH{Pk(xb8tUy|GVYCQs z<;a`FqJtLwIK?X6BT3w%^i11bk={@e_A9S#_B604_Y2O-1BPozWDx*U*_pf{7_@tKlMBAX)#iLRpH3k zbszROZ}GD~?&%rbjU5_l7ufJ>FWl$H{o-C7$MvXR%gelMh5F!p;OjaLu8%h_+k|^E zw3sF$&J^xg`6%w`3Ez44ZhN^6FZ=wuLgZ^^s^JvBT}?N0u(;sR z2urMg_cX+q(7>SX{?VIa-KA0 zcehCXiWX}Q8qI>2Z}>J^Z18~KX#f6}aR1m?q2F#QTqX53C>jaV+*vr&c`-#yP(W;G zWQ39^C^{xKIyB1CUFh4JN)(qjk>M6zMFc}cMv^!iVJcZEy$rIY6AJz4e<8D=ez8IL z9u#E^3lnxNZ3^d?pKT(Iyt4QR|KXA6DKj+2qDJ|54-+0cO=fXs zmnlmHXu8>Gc(_d>tVR5YLZ^(eJguPojo?=(|!5!cUTpO4i6cK~7;84=sVBG!&K z6}D)1J!6p2dZSdI(pG_?F|m*sPG!XE@g^6)u*mM<n|d6HWH|%Hp<@P4Q}}#f7xgYqTBxNr>_djq&#nh^ZJ578)1o8`Ucq)vJsx`1)78 zIAbbYG(C-#uf@mET*TQ#Q?Z0}V!)z^vnZdzGM9*`@bA=KV(A+i7#rdXZvZBmmShR& z8Kbd9S^OhnLVZ>LUhPyQXe@oB{WT7y(0!w`OM)dpu@y0Mi>aW#its3IQ(P2jV=@<1 zKCY!ELH;qZ{xPBc5!6HYm$osxrXz(w`Sb*fyYLu_hPl+n>^*66-|Tn6w5o2^Sb+=%ii;Rm72(pAmgrPn;=XC#)u1r3ekuA#dmU1H^12K;V#Ri0^ z-5@e760?6yfPYjFs@FeO8C@+HZ~ZNC(TX_;ySz3wW{pIHNXHX~(MB0@w+%Bn=_{$% z-(&O@ck0@d5O%{&_C`2bqW5!Hcf;YPLZa9RQwecoxXHzc@0P{c0q6n^hQqB>M=ggm z$|zN*sV3I6F_m&iqcvi!cjES7Qz?-!(_~XpDIFrMMa3M(-i_jOo!&QCE1GBOEsJHn zu_!wq3V%@d(7-TScOT5zn7%oXnaiS%(~~)teMyv6lbib_U|@hDfU~YG_sKwF0PTE4 zax0T-6S?^Yy1Y^+bariZ8q>%zL1CEqmCjem5)vC8ruIWmOKfydki|dHKT7pWYJZbO zF_-_q0WX2An^$FnW|CheyQfzujpC|?(4UrvisudmXri0FO_2iHb_Mz6<_YrQ5L-_2 zp>#4*3R3o36a&UAhFk-Bi3J61%nsTexFfV{R4C>W*j0XQy8^M=rQ%2l#aPuW^Q)Vo zU{NnHDtL+HA~vr1lnqqSm5_6$I$&4*s$)tSUvpYhl@htfn6hoI+Q#OGLOJ^JXMXXQ zctVT_w<(UdAh(rYe&;|DyWK}*%-<($L`^@ugLX08rd5tICsls;lL|kUSg8s6$*9DW aGQo%l8?T&OB=sJswh=~GaWBGV&;J2U7&&JE delta 63531 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 diff --git a/classes/database/emoji.ts b/classes/database/emoji.ts index d37f97fd..55158f2e 100644 --- a/classes/database/emoji.ts +++ b/classes/database/emoji.ts @@ -23,6 +23,7 @@ type EmojiWithInstance = InferSelectModel & { export class Emoji extends BaseInterface { public static schema = z.object({ + id: z.string(), shortcode: z.string(), url: z.string(), visible_in_picker: z.boolean(), @@ -186,9 +187,8 @@ export class Emoji extends BaseInterface { ); } - public toApi(): ApiEmoji { + public toApi(): ApiEmoji & { id: string } { return { - // @ts-expect-error ID is not in regular Mastodon API id: this.id, shortcode: this.data.shortcode, static_url: proxyUrl(this.data.url) ?? "", // TODO: Add static version diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts new file mode 100644 index 00000000..f0410502 --- /dev/null +++ b/docs/.vitepress/config.ts @@ -0,0 +1,105 @@ +import taskLists from "@hackmd/markdown-it-task-lists"; +import implicitFigures from "markdown-it-image-figures"; +import { defineConfig } from "vitepress"; +import { tabsMarkdownPlugin } from "vitepress-plugin-tabs"; + +// https://vitepress.dev/reference/site-config +export default defineConfig({ + title: "Versia Server Docs", + lang: "en-US", + description: "Documentation for Versia Server APIs", + markdown: { + config: (md): void => { + md.use(implicitFigures, { + figcaption: "alt", + copyAttrs: "^class$", + }); + + md.use(taskLists); + + md.use(tabsMarkdownPlugin); + }, + math: true, + }, + themeConfig: { + // https://vitepress.dev/reference/default-theme-config + nav: [ + { text: "Home", link: "/" }, + { + text: "Versia Protocol", + link: "https://versia.pub", + target: "_blank", + }, + ], + + sidebar: [ + { + text: "Setup", + items: [ + { + text: "Installation", + link: "/setup/installation", + }, + { + text: "Database", + link: "/setup/database", + }, + ], + }, + { + text: "CLI", + link: "/cli", + }, + { + text: "API", + items: [ + { + text: "Emojis", + link: "/api/emojis", + }, + { + text: "Roles", + link: "/api/roles", + }, + { + text: "Challenges", + link: "/api/challenges", + }, + { + text: "SSO", + link: "/api/sso", + }, + { + text: "Mastodon Extensions", + link: "/api/mastodon", + }, + ], + }, + { + text: "Frontend", + items: [ + { + text: "Authentication", + link: "/frontend/auth", + }, + { + text: "Routes", + link: "/frontend/routes", + }, + ], + }, + ], + + socialLinks: [ + { icon: "github", link: "https://github.com/versia-pub/server" }, + ], + + search: { + provider: "local", + }, + + logo: "https://cdn.versia.pub/branding/icon.svg", + }, + head: [["link", { rel: "icon", href: "/favicon.png", type: "image/png" }]], + titleTemplate: ":title • Versia Server Docs", +}); diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts new file mode 100644 index 00000000..297bd0cd --- /dev/null +++ b/docs/.vitepress/theme/index.ts @@ -0,0 +1,14 @@ +import type { Theme } from "vitepress"; +import DefaultTheme from "vitepress/theme"; +// https://vitepress.dev/guide/custom-theme +import { type VNode, h } from "vue"; +import "./style.css"; + +export default { + extends: DefaultTheme, + Layout: (): VNode => { + return h(DefaultTheme.Layout, null, { + // https://vitepress.dev/guide/extending-default-theme#layout-slots + }); + }, +} satisfies Theme; diff --git a/docs/.vitepress/theme/style.css b/docs/.vitepress/theme/style.css new file mode 100644 index 00000000..5bc070f4 --- /dev/null +++ b/docs/.vitepress/theme/style.css @@ -0,0 +1,138 @@ +/** + * Customize default theme styling by overriding CSS variables: + * https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css + */ + +/** + * Colors + * + * Each colors have exact same color scale system with 3 levels of solid + * colors with different brightness, and 1 soft color. + * + * - `XXX-1`: The most solid color used mainly for colored text. It must + * satisfy the contrast ratio against when used on top of `XXX-soft`. + * + * - `XXX-2`: The color used mainly for hover state of the button. + * + * - `XXX-3`: The color for solid background, such as bg color of the button. + * It must satisfy the contrast ratio with pure white (#ffffff) text on + * top of it. + * + * - `XXX-soft`: The color used for subtle background such as custom container + * or badges. It must satisfy the contrast ratio when putting `XXX-1` colors + * on top of it. + * + * The soft color must be semi transparent alpha channel. This is crucial + * because it allows adding multiple "soft" colors on top of each other + * to create a accent, such as when having inline code block inside + * custom containers. + * + * - `default`: The color used purely for subtle indication without any + * special meanings attached to it such as bg color for menu hover state. + * + * - `brand`: Used for primary brand colors, such as link text, button with + * brand theme, etc. + * + * - `tip`: Used to indicate useful information. The default theme uses the + * brand color for this by default. + * + * - `warning`: Used to indicate warning to the users. Used in custom + * container, badges, etc. + * + * - `danger`: Used to show error, or dangerous message to the users. Used + * in custom container, badges, etc. + * -------------------------------------------------------------------------- */ + +:root { + --vp-c-default-1: var(--vp-c-gray-1); + --vp-c-default-2: var(--vp-c-gray-2); + --vp-c-default-3: var(--vp-c-gray-3); + --vp-c-default-soft: var(--vp-c-gray-soft); + + --vp-c-brand-1: var(--vp-c-indigo-1); + --vp-c-brand-2: var(--vp-c-indigo-2); + --vp-c-brand-3: var(--vp-c-indigo-3); + --vp-c-brand-soft: var(--vp-c-indigo-soft); + + --vp-c-tip-1: var(--vp-c-brand-1); + --vp-c-tip-2: var(--vp-c-brand-2); + --vp-c-tip-3: var(--vp-c-brand-3); + --vp-c-tip-soft: var(--vp-c-brand-soft); + + --vp-c-warning-1: var(--vp-c-yellow-1); + --vp-c-warning-2: var(--vp-c-yellow-2); + --vp-c-warning-3: var(--vp-c-yellow-3); + --vp-c-warning-soft: var(--vp-c-yellow-soft); + + --vp-c-danger-1: var(--vp-c-red-1); + --vp-c-danger-2: var(--vp-c-red-2); + --vp-c-danger-3: var(--vp-c-red-3); + --vp-c-danger-soft: var(--vp-c-red-soft); +} + +/** + * Component: Button + * -------------------------------------------------------------------------- */ + +:root { + --vp-button-brand-border: transparent; + --vp-button-brand-text: var(--vp-c-white); + --vp-button-brand-bg: var(--vp-c-brand-3); + --vp-button-brand-hover-border: transparent; + --vp-button-brand-hover-text: var(--vp-c-white); + --vp-button-brand-hover-bg: var(--vp-c-brand-2); + --vp-button-brand-active-border: transparent; + --vp-button-brand-active-text: var(--vp-c-white); + --vp-button-brand-active-bg: var(--vp-c-brand-1); +} + +/** + * Component: Home + * -------------------------------------------------------------------------- */ + +:root { + --vp-home-hero-name-color: transparent; + --vp-home-hero-name-background: -webkit-linear-gradient( + 120deg, + #e6a9fe 30%, + #bd34fe + ); + + --vp-home-hero-image-background-image: linear-gradient( + -45deg, + #e6a9fe 50%, + #bd34fe 50% + ); + --vp-home-hero-image-filter: blur(44px); +} + +@media (min-width: 640px) { + :root { + --vp-home-hero-image-filter: blur(56px); + } +} + +@media (min-width: 960px) { + :root { + --vp-home-hero-image-filter: blur(68px); + } +} + +/** + * Component: Custom Block + * -------------------------------------------------------------------------- */ + +:root { + --vp-custom-block-tip-border: transparent; + --vp-custom-block-tip-text: var(--vp-c-text-1); + --vp-custom-block-tip-bg: var(--vp-c-brand-soft); + --vp-custom-block-tip-code-bg: var(--vp-c-brand-soft); +} + +/** + * Component: Algolia + * -------------------------------------------------------------------------- */ + +.DocSearch { + --docsearch-primary-color: var(--vp-c-brand-1) !important; +} diff --git a/docs/api/challenges.md b/docs/api/challenges.md index aef5bcc6..a9ea22cd 100644 --- a/docs/api/challenges.md +++ b/docs/api/challenges.md @@ -8,6 +8,21 @@ This is a form of proof of work CAPTCHA, and should be mostly invisible to users Challenges are powered by the [Altcha](https://altcha.org/) library. You may either reimplement their solution code (which is very simple), or use [`altcha-lib`](https://github.com/altcha-org/altcha-lib) to solve the challenges. +## Challenge + +```typescript +type UUID = string; + +interface Challenge { + id: UUID; + algorithm: "SHA-256" | "SHA-384" | "SHA-512"; + challenge: string; + maxnumber?: number; + salt: string; + signature: string; +} +``` + ## Request Challenge ```http @@ -16,17 +31,32 @@ POST /api/v1/challenges Generates a new challenge for the client to solve. +- **Returns:**: [`Challenge`](#challenge) +- **Authentication:**: Not required +- **Permissions:**: None +- **Version History**: + - `0.7.0`: Added. + +### Example + +```http +POST /api/v1/challenges +``` + ### Response -```ts -// 200 OK +#### `200 OK` + +Challenge data. + +```json { - id: string, - algorithm: "SHA-256" | "SHA-384" | "SHA-512", - challenge: string; - maxnumber?: number; - salt: string; - signature: string; + "id":"01931621-1456-7b5b-be65-c044e6b47cbb", + "salt":"d15e43fa3709d85ce3c74644?challenge_id=01931621-1456-7b5b-be65-c044e6b47cbb&expires=1731243386", + "algorithm":"SHA-256", + "challenge":"5dc6b352632912664583940e14b9dfbdf447459d4517708ce8766a39ac040eb5", + "maxnumber":50000, + "signature":"22c3a687dc2500cbffcb022ae8474360d5c2f63a50ba376325c211bb2ca06b7f" } ``` @@ -52,4 +82,4 @@ A challenge solution is valid for 5 minutes (configurable) after the challenge i If challenges are enabled, the following routes will require a challenge to be solved before the request can be made: - `POST /api/v1/accounts` -Which routes require challenges may eventually be expanded or made configurable. \ No newline at end of file +Routes requiring challenges may eventually be expanded or made configurable. \ No newline at end of file diff --git a/docs/api/emojis.md b/docs/api/emojis.md index 4374eaec..29100b9a 100644 --- a/docs/api/emojis.md +++ b/docs/api/emojis.md @@ -1,6 +1,24 @@ # Emoji API -An Emoji API is made available to users to manage custom emoji on the instance. We recommend using Versia Server's CLI to manage emoji, but this API is available for those who prefer to use it (both admin and non-admin users). +This API allows users to create, read, update, and delete instance custom emojis. + +The **Versia Server CLI** can also be used to manage custom emojis. + +## Emoji + +```typescript +type UUID = string; +type URL = string; + +interface Emoji { + id: UUID; + shortcode: string; + url: URL; + static_url?: URL; + visible_in_picker: boolean; + category?: string; +} +``` ## Create Emoji @@ -8,29 +26,54 @@ An Emoji API is made available to users to manage custom emoji on the instance. POST /api/v1/emojis ``` -Creates a new custom emoji on the instance. If the user is an administrator, they can create global emoji that are visible to all users on the instance. Otherwise, the emoji will only be visible to the user who created it (in `/api/v1/custom_emojis`). +Upload a new custom emoji. -### Parameters +- **Returns:** [`Emoji`](#emoji) +- **Authentication:** Required +- **Permissions:** `owner:emoji`, or `emoji` if uploading a global emoji. +- **Version History**: + - `0.7.0`: Added. -- `Content-Type`: `multipart/form-data`, `application/json` or `application/x-www-form-urlencoded`. If uploading a file, use `multipart/form-data`. +### Request + +- `shortcode` (string, required): The shortcode for the emoji. + - 1-64 characters long, alphanumeric, and may contain dashes or underscores. +- `element` (file/string, required): The image file to upload. + - Can be a URL, or a file upload (`multipart/form-data`). +- `alt` (string): Emoji alt text. +- `category` (string): Emoji category. Can be any string up to 64 characters long. +- `global` (boolean): If set to `true`, the emoji will be visible to all users, not just the uploader. + - Requires `emoji` permission. + +#### Example + +```http +POST /api/v1/emojis +Content-Type: application/json +Authorization: Bearer ... + +{ + "shortcode": "blobfox-coffee", + "element": "https://example.com/blobfox-coffee.png", + "alt": "My emoji", + "category": "Blobmojis" +} +``` -- `shortcode`: string, required. The shortcode for the emoji. Must be 2-64 characters long and contain only alphanumeric characters, dashes, and underscores. -- `element`: string or file, required. The image file for the emoji. This can be a URL or a file upload. -- `alt`: string, optional. The alt text for the emoji. Defaults to the shortcode. -- `global`: boolean, optional. For administrators only. Whether the emoji should be visible to all users on the instance. Defaults to `false`. -- `category`: string, optional. The category for the emoji. Maximum 64 characters. - ### Response -```ts -// 200 OK +#### `201 Created` + +Emoji successfully uploaded. + +```json { - id: string, - shortcode: string, - url: string, - static_url: string, - visible_in_picker: boolean, - category: string | undefined, + "id": "f7b1c1b0-0b1b-4b1b-8b1b-0b1b1b1b1b1b", + "shortcode": "blobfox-coffee", + "url": "https://cdn.yourinstance.com/emojis/f7b1c1b0-0b1b-4b1b-8b1b-0b1b1b1b1b1b.png", + "static_url": "https://cdn.yourinstance.com/emojis/f7b1c1b0-0b1b-4b1b-8b1b-0b1b1b1b1b1b.png", + "visible_in_picker": true, + "category": "Blobmojis" } ``` @@ -40,19 +83,37 @@ Creates a new custom emoji on the instance. If the user is an administrator, the GET /api/v1/emojis/:id ``` -Retrieves information about a custom emoji on the instance. +Get a specific custom emoji. + +- **Returns:** [`Emoji`](#emoji) +- **Authentication:** Required +- **Permissions:** `owner:emoji`, or `emoji` if viewing a global emoji. +- **Version History**: + - `0.7.0`: Added. + +### Request + +#### Example + +```http +GET /api/v1/emojis/f7b1c1b0-0b1b-4b1b-8b1b-0b1b1b1b1b1b +Authorization: Bearer ... +``` ### Response -```ts -// 200 OK +#### `200 OK` + +Custom emoji data. + +```json { - id: string, - shortcode: string, - url: string, - static_url: string, - visible_in_picker: boolean, - category: string | undefined, + "id": "f7b1c1b0-0b1b-4b1b-8b1b-0b1b1b1b1b1b", + "shortcode": "blobfox-coffee", + "url": "https://cdn.yourinstance.com/emojis/f7b1c1b0-0b1b-4b1b-8b1b-0b1b1b1b1b1b.png", + "static_url": "https://cdn.yourinstance.com/emojis/f7b1c1b0-0b1b-4b1b-8b1b-0b1b1b1b1b1b.png", + "visible_in_picker": true, + "category": "Blobmojis" } ``` @@ -62,29 +123,54 @@ Retrieves information about a custom emoji on the instance. PATCH /api/v1/emojis/:id ``` -Edits a custom emoji on the instance. +Edit an existing custom emoji. -### Parameters +- **Returns:** [`Emoji`](#emoji) +- **Authentication:** Required +- **Permissions:** `owner:emoji`, or `emoji` if editing a global emoji. +- **Version History**: + - `0.7.0`: Added. -- `Content-Type`: `application/json`, `multipart/form-data` or `application/x-www-form-urlencoded`. If uploading a file, use `multipart/form-data`. +### Request -- `shortcode`: string, optional. The new shortcode for the emoji. Must be 2-64 characters long and contain only alphanumeric characters, dashes, and underscores. -- `element`: string or file, optional. The new image file for the emoji. This can be a URL or a file upload. -- `alt`: string, optional. The new alt text for the emoji. Defaults to the shortcode. -- `global`: boolean, optional. For administrators only. Whether the emoji should be visible to all users on the instance. Defaults to `false`. -- `category`: string, optional. The new category for the emoji. Maximum 64 characters. +> [!NOTE] +> All fields are optional. + +- `shortcode` (string): The shortcode for the emoji. + - 1-64 characters long, alphanumeric, and may contain dashes or underscores. +- `element` (file/string): The image file to upload. + - Can be a URL, or a file upload (`multipart/form-data`). +- `alt` (string): Emoji alt text. +- `category` (string): Emoji category. Can be any string up to 64 characters long. +- `global` (boolean): If set to `true`, the emoji will be visible to all users, not just the uploader. + - Requires `emoji` permission. + +#### Example + +```http +PATCH /api/v1/emojis/f7b1c1b0-0b1b-4b1b-8b1b-0b1b1b1b1b1b +Content-Type: application/json +Authorization: Bearer ... + +{ + "category": "Blobfoxes" +} +``` ### Response -```ts -// 200 OK +#### `200 OK` + +Emoji successfully edited. + +```json { - id: string, - shortcode: string, - url: string, - static_url: string, - visible_in_picker: boolean, - category: string | undefined, + "id": "f7b1c1b0-0b1b-4b1b-8b1b-0b1b1b1b1b1b", + "shortcode": "blobfox-coffee", + "url": "https://cdn.yourinstance.com/emojis/f7b1c1b0-0b1b-4b1b-8b1b-0b1b1b1b1b1b.png", + "static_url": "https://cdn.yourinstance.com/emojis/f7b1c1b0-0b1b-4b1b-8b1b-0b1b1b1b1b1b.png", + "visible_in_picker": true, + "category": "Blobfoxes" } ``` @@ -94,4 +180,25 @@ Edits a custom emoji on the instance. DELETE /api/v1/emojis/:id ``` -Deletes a custom emoji on the instance. \ No newline at end of file +Delete an existing custom emoji. + +- **Returns:** `204 No Content` +- **Authentication:** Required +- **Permissions:** `owner:emoji`, or `emoji` if deleting a global emoji. +- **Version History**: + - `0.7.0`: Added. + +### Request + +#### Example + +```http +DELETE /api/v1/emojis/f7b1c1b0-0b1b-4b1b-8b1b-0b1b1b1b1 +Authorization: Bearer ... +``` + +### Response + +#### `204 No Content` + +Emoji successfully deleted. \ No newline at end of file diff --git a/docs/api/federation.md b/docs/api/federation.md deleted file mode 100644 index 9e54cdf7..00000000 --- a/docs/api/federation.md +++ /dev/null @@ -1,22 +0,0 @@ -# Federation API - -The Federation API contains a variety of endpoints for interacting with the Versia Server remote network. - -## Refetch User - -```http -POST /api/v1/accounts/:id/refetch -``` - -Refetches the user's account from the remote network. - -### Response - -Returns the updated account object. -```ts -// 200 OK -{ - id: string, - ... // Account object -} -``` \ No newline at end of file diff --git a/docs/api/frontend.md b/docs/api/frontend.md deleted file mode 100644 index f4a6b452..00000000 --- a/docs/api/frontend.md +++ /dev/null @@ -1,239 +0,0 @@ -# Frontend API - -The frontend API contains endpoints that are useful for frontend developers. These endpoints are not part of the Mastodon API, but are specific to Versia Server. - -## Routes that the Frontend must implement - -These routes can be set to a different URL in the Versia Server configuration, at `frontend.routes`. The frontend must implement these routes for the instance to function correctly. - -- `GET /oauth/authorize`: (NOT `POST`): Identifier/password login form, submits to [`POST /api/auth/login`](#sign-in) or OpenID Connect flow. -- `GET /oauth/consent`: Consent form, submits to [`POST /oauth/authorize`](#consent) - -## Get Frontend Configuration - -```http -GET /api/v1/frontend/config -``` - -Retrieves the frontend configuration for the instance. This returns whatever the `frontend.settings` object is set to in the Versia Server configuration. - -This behaves like the `/api/v1/preferences` endpoint in the Mastodon API, but is specific to the frontend. These values are arbitrary and can be used for anything. - -For example, the frontend configuration could contain the following: - -```json -{ - "pub.versia.fe:theme": "dark", - "pub.versia.fe:custom_css": "body { background-color: black; }", - // Googly is an imaginary frontend that uses the `net.googly.frontend` namespace - "net.googly.frontend:spoiler_image": "https://example.com/spoiler.png" -} -``` - -Frontend developers should always namespace their keys to avoid conflicts with other keys. - -### Response - -```ts -// 200 OK -{ - [key: string]: any; -} -``` - -## Sign In - -Allows users to sign in to the instance. Required for the frontend to function. - -```http -POST /api/auth/login -``` - -### Parameters - -- `Content-Type`: `multipart/form-data` - -- `identifier`: string, required. Either the username or the email of the user. Converted to lowercase automatically (case insensitive). -- `password`: string, required. The password of the user. - -#### Query Parameters - -- `client_id`: string, required. Client ID of the Mastodon API application that is making the request. -- `redirect_uri`: string, required. Redirect URI of the Mastodon API application that is making the request. Must match the saved value. -- `response_type`: string, required. Must be `code`. -- `scope`: string, required. Standard Mastodon API OAuth2 scope. Must match the saved value. - -### Response - -Responds with a `302 Found` redirect to `/oauth/consent` with some query parameters. The frontend should redirect the user to this URL. - -This response also has a `Set-Cookie` header with a [JSON Web Token](https://jwt.io/) that contains the user's session information. This JWT is signed with the instance's secret key, and must be included in all subsequent authentication requests. - -## Redirect - -Redirects the user from the consent page to the redirect URI with the authorization code. - -```http -POST /api/auth/redirect -``` - -### Query Parameters - -- `client_id`: string, required. Client ID of the Mastodon API application that is making the request. -- `redirect_uri`: string, required. Redirect URI of the Mastodon API application that is making the request. Must match the saved value. -- `code`: string, required. Authorization code from the previous step. - -### Response - -Responds with a `302 Found` redirect to the `redirect_uri` with the authorization code as a query parameter. - -## SSO Login - -Allows users to sign in to the instance using an external OpenID Connect provider. - -```http -POST /oauth/sso -``` - -### Query Parameters - -- `issuer`: string, required. The issuer ID of the OpenID Connect provider as set in config. -- `client_id`: string, required. Client ID of the Mastodon API application that is making the request. - -### Response - -Responds with a `302 Found` redirect to the OpenID Connect provider's authorization endpoint. The frontend should redirect the user to this URL, without modification. - -## SSO Callback/Redirect - -> [!INFO] -> This endpoint should not be called directly by the frontend. It is an internal route. - -Callback URL for the OpenID Connect provider to redirect to after the user has authenticated. - -```http -GET /oauth/sso/:issuer/callback -``` - -### Query Parameters - -- `client_id`: string, required. Client ID of the Mastodon API application that is making the request. -- `flow_id`: string, required. Flow ID of the OpenID Connect flow. -- `link`: boolean, optional. If `true`, the user is linking their account to the OpenID Connect provider. -- `user_id`: string, optional. User ID of the user that is linking their account. Required if `link` is `true`. - -### Response - -Responds with a `302 Found` redirect to either `/oauth/consent` or `/?oidc_account_linked=true` if the user is linking their account. - -When erroring, responds with a `302 Found` redirect to `/?oidc_account_linking_error=&oidc_account_linking_error_message=`. - -## SSO Link - -Allows users to link their account to an external OpenID Connect provider. - -```http -POST /api/v1/sso -``` - -### Parameters - -This request is authenticated with the user's Mastodon API access token. - -- `Content-Type`: `application/json`, `application/x-www-form-urlencoded` or `multipart/form-data`. - -- `issuer`: string, required. The issuer ID of the OpenID Connect provider as set in config. - -### Response - -The client must redirect the user to the contents of the `link` field in the response. - -```ts -// 200 OK -{ - link: string; -} -``` - -## SSO Unlink - -Allows users to unlink their account from an external OpenID Connect provider. - -```http -DELETE /api/v1/sso/:issuer -``` - -### Parameters - -This request is authenticated with the user's Mastodon API access token. - -### Response - -```ts -// 204 NO CONTENT -``` - -## SSO List - -Lists all external OpenID Connect providers that the user has linked their account to. - -```http -GET /api/v1/sso -``` - -### Parameters - -This request is authenticated with the user's Mastodon API access token. - -### Response - -```ts -// 200 OK -{ - id: string; - name: string; - icon: string; -}[]; -``` - -## SSO Get Linked Provider Data - -Gets the data of an external OpenID Connect provider that the user has linked their account to. The same data is returned as in the `/api/v1/sso` endpoint. - -```http -GET /api/v1/sso/:issuer -``` - -### Parameters - -This request is authenticated with the user's Mastodon API access token. - -### Response - -```ts -// 200 OK -{ - id: string; - name: string; - icon: string; -} -``` - -## Get User By Username - -Gets a user by their username. - -```http -GET /api/v1/users/id?username=myCoolUser -``` - -### Response - -Returns an account object. - -```ts -// 200 OK -{ - id: string; - // Account object -} \ No newline at end of file diff --git a/docs/api/index.md b/docs/api/index.md deleted file mode 100644 index f24c5c40..00000000 --- a/docs/api/index.md +++ /dev/null @@ -1,40 +0,0 @@ -# Versia Server API Documentation - -The Versia Server API strictly follows the latest available Mastodon API version (Glitch-Soc version). This means that the Versia Server API is a superset of the Mastodon API, with additional endpoints and features. - -Some more information about the Mastodon API can be found in the [Mastodon API documentation](https://docs.joinmastodon.org/api/). - -## Emoji API - -For client developers. Please read [the documentation](./emojis.md). - -## Roles API - -For client developers. Please read [the documentation](./roles.md). - -## Challenges API - -For client developers. Please read [the documentation](./challenges.md). - -## Moderation API - -> [!WARNING] -> **Not implemented.** - -For client developers. Please read [the documentation](./moderation.md). - -## Federation API - -For client developers. Please read [the documentation](./federation.md). - -## Frontend API - -For frontend developers. Please read [the documentation](./frontend.md). - -## Mastodon API Extensions - -Extra attributes have been added to some Mastodon API routes. Those changes are [documented in this document](./mastodon.md) - -## Instance API - -Extra endpoints have been added to the API to provide additional information about the instance. Please read [the documentation](./instance.md). \ No newline at end of file diff --git a/docs/api/instance.md b/docs/api/instance.md deleted file mode 100644 index 6609424b..00000000 --- a/docs/api/instance.md +++ /dev/null @@ -1,11 +0,0 @@ -# Instance Endpoints - -Extra endpoints have been added to the API to provide additional information about the instance. - -## `/api/v1/instance/tos` - -Returns the same output as Mastodon's `/api/v1/instance/extended_description`, but with the instance's Terms of Service. Configurable at `instance.tos_path` in config. - -## `/api/v1/instance/privacy_policy` - -Returns the same output as Mastodon's `/api/v1/instance/extended_description`, but with the instance's Privacy Policy. Configurable at `instance.privacy_policy_path` in config. \ No newline at end of file diff --git a/docs/api/mastodon.md b/docs/api/mastodon.md index 5dbc0799..42888943 100644 --- a/docs/api/mastodon.md +++ b/docs/api/mastodon.md @@ -1,34 +1,302 @@ # Mastodon API Extensions -Extra attributes have been added to some Mastodon API routes. Changes are documented in this document. +Versia Server extends several Mastodon API endpoints to provide additional functionality. These endpoints are not part of the official Mastodon API, but are provided by Versia Server to enhance the user experience. + +## Refetch User + +```http +POST /api/v1/accounts/:id/refetch +``` + +Refetches the user's profile information from remote servers. Does not work for local users. + +- **Returns**: [`Account`](https://docs.joinmastodon.org/entities/Account/) +- **Authentication**: Required +- **Permissions**: `read:account` +- **Version History**: + - `0.7.0`: Added. + +### Request + +#### Example + +```http +POST /api/v1/accounts/364fd13f-28b5-4e88-badd-ce3e533f0d02/refetch +Authorization: Bearer ... +``` + +### Response + +#### `400 Bad Request` + +The user is a local user and cannot be refetched. + +#### `200 OK` + +New user data. + +Example from the [Mastodon API documentation](https://docs.joinmastodon.org/entities/Account/): + +```json +{ + "id": "23634", + "username": "noiob", + "acct": "noiob@awoo.space", + "display_name": "ikea shark fan account", + "locked": false, + "bot": false, + "created_at": "2017-02-08T02:00:53.274Z", + "note": "

:ms_rainbow_flag:​ :ms_bisexual_flagweb:​ :ms_nonbinary_flag:​ #awoo#admin#bi#nonbinary#games@dzuk

", + "url": "https://awoo.space/@noiob", + "avatar": "https://files.mastodon.social/accounts/avatars/000/023/634/original/6ca8804dc46800ad.png", + "avatar_static": "https://files.mastodon.social/accounts/avatars/000/023/634/original/6ca8804dc46800ad.png", + "header": "https://files.mastodon.social/accounts/headers/000/023/634/original/256eb8d7ac40f49a.png", + "header_static": "https://files.mastodon.social/accounts/headers/000/023/634/original/256eb8d7ac40f49a.png", + "followers_count": 547, + "following_count": 404, + "statuses_count": 28468, + "last_status_at": "2019-11-17", + "emojis": [ + { + "shortcode": "ms_rainbow_flag", + "url": "https://files.mastodon.social/custom_emojis/images/000/028/691/original/6de008d6281f4f59.png", + "static_url": "https://files.mastodon.social/custom_emojis/images/000/028/691/static/6de008d6281f4f59.png", + "visible_in_picker": true + }, + { + "shortcode": "ms_bisexual_flag", + "url": "https://files.mastodon.social/custom_emojis/images/000/050/744/original/02f94a5fca7eaf78.png", + "static_url": "https://files.mastodon.social/custom_emojis/images/000/050/744/static/02f94a5fca7eaf78.png", + "visible_in_picker": true + }, + { + "shortcode": "ms_nonbinary_flag", + "url": "https://files.mastodon.social/custom_emojis/images/000/105/099/original/8106088bd4782072.png", + "static_url": "https://files.mastodon.social/custom_emojis/images/000/105/099/static/8106088bd4782072.png", + "visible_in_picker": true + } + ], + "fields": [ + { + "name": "Pronouns", + "value": "they/them", + "verified_at": null + }, + { + "name": "Alt", + "value": "@noiob", + "verified_at": null + }, + { + "name": "Bots", + "value": "@darksouls, @nierautomata, @fedi, code for @awoobot", + "verified_at": null + }, + { + "name": "Website", + "value": "http://shork.xyz:ms_rainbow_flag:​ :ms_bisexual_flagweb:​ :ms_nonbinary_flag:​ #awoo#admin#bi#nonbinary#games@dzuk

", + "url": "https://awoo.space/@noiob", + "avatar": "https://files.mastodon.social/accounts/avatars/000/023/634/original/6ca8804dc46800ad.png", + "avatar_static": "https://files.mastodon.social/accounts/avatars/000/023/634/original/6ca8804dc46800ad.png", + "header": "https://files.mastodon.social/accounts/headers/000/023/634/original/256eb8d7ac40f49a.png", + "header_static": "https://files.mastodon.social/accounts/headers/000/023/634/original/256eb8d7ac40f49a.png", + "followers_count": 547, + "following_count": 404, + "statuses_count": 28468, + "last_status_at": "2019-11-17", + "emojis": [ + { + "shortcode": "ms_rainbow_flag", + "url": "https://files.mastodon.social/custom_emojis/images/000/028/691/original/6de008d6281f4f59.png", + "static_url": "https://files.mastodon.social/custom_emojis/images/000/028/691/static/6de008d6281f4f59.png", + "visible_in_picker": true + }, + { + "shortcode": "ms_bisexual_flag", + "url": "https://files.mastodon.social/custom_emojis/images/000/050/744/original/02f94a5fca7eaf78.png", + "static_url": "https://files.mastodon.social/custom_emojis/images/000/050/744/static/02f94a5fca7eaf78.png", + "visible_in_picker": true + }, + { + "shortcode": "ms_nonbinary_flag", + "url": "https://files.mastodon.social/custom_emojis/images/000/105/099/original/8106088bd4782072.png", + "static_url": "https://files.mastodon.social/custom_emojis/images/000/105/099/static/8106088bd4782072.png", + "visible_in_picker": true + } + ], + "fields": [ + { + "name": "Pronouns", + "value": "they/them", + "verified_at": null + }, + { + "name": "Alt", + "value": "
@noiob", + "verified_at": null + }, + { + "name": "Bots", + "value": "@darksouls, @nierautomata, @fedi, code for @awoobot", + "verified_at": null + }, + { + "name": "Website", + "value": "http://shork.xyzTOS\n

These are the terms of service for this instance.

", +} +``` + +## Get Instance Privacy Policy + +```http +GET /api/v1/instance/privacy_policy +``` + +Returns the instance's Privacy Policy, as configured in the instance settings. + +- **Returns**: [`ExtendedDescription`](https://docs.joinmastodon.org/entities/ExtendedDescription/) +- **Authentication**: Not required +- **Permissions**: None +- **Version History**: + - `0.7.0`: Added. + +### Request + +#### Example + +```http +GET /api/v1/instance/privacy_policy +``` + +### Response + +#### `200 OK` + +Instance's Privacy Policy. + +```json +{ + "updated_at": "2019-11-17T00:00:00.000Z", + "content": "

Privacy Policy

\n

This is the privacy policy for this instance.

", +} +``` ## `/api/v1/instance` -Three extra attributes have been added to the `/api/v1/instance` endpoint: +Extra attributes have been added to the `/api/v1/instance` endpoint. ```ts -{ - // ... +interface SSOProvider { + id: string; + name: string; + icon?: string; +} + +type ExtendedInstance = Instance & { banner: string | null; versia_version: string; sso: { forced: boolean; - providers: { - id: string; - name: string; - icon?: string; - }[]; - } + providers: SSOProvider[]; + }; } ``` ### `banner` -The URL of the instance's banner image. `null` if there is no banner set. +The URL of the instance's banner image. ### `versia_version` -The version of the Versia Server instance. +The version of Versia Server running on the instance. The normal `version` field is always set to `"4.3.0+glitch"` or similar, to not confuse clients that expect a Mastodon instance. @@ -44,29 +312,54 @@ Single Sign-On (SSO) settings for the instance. This object contains two fields: ## `/api/v2/instance` -Contains the same extensions as `/api/v1/instance`, except `banner` which uses the normal Mastodon API attribute. +Extra attributes have been added to the `/api/v2/instance` endpoint. These are identical to the `/api/v1/instance` endpoint, except that the `banner` attribute uses the normal Mastodon API attribute. + +```ts +type ExtendedInstanceV2 = Instance & { + versia_version: string; + sso: { + forced: boolean; + providers: SSOProvider[]; + }; +} +``` + +### `versia_version` + +The version of Versia Server running on the instance. + +The normal `version` field is always set to `"4.3.0+glitch"` or similar, to not confuse clients that expect a Mastodon instance. + +### `sso` + +Single Sign-On (SSO) settings for the instance. This object contains two fields: + +- `forced`: If this is enabled, normal identifier/password login is disabled and login must be done through SSO. +- `providers`: An array of external OpenID Connect providers that users can link their accounts to. Each provider object contains the following fields: + - `id`: The issuer ID of the OpenID Connect provider. + - `name`: The name of the provider. + - `icon`: The URL of the provider's icon. Optional. ## `Account` -(`/api/v1/accounts/:id`, `/api/v1/accounts/verify_credentials`, ...) +Two extra attributes have been added to all returned [`Account`](https://docs.joinmastodon.org/entities/Account/) objects. -Two extra attributes has been adding to all returned account objects: +This object is returned on routes such as `/api/v1/accounts/:id`, `/api/v1/accounts/verify_credentials`, etc. ```ts -{ - // ... - roles: VersiaRoles[]; +type ExtendedAccount = Account & { + roles: Role[]; uri: string; } ``` ### `roles` -An array of roles from [Versia Server Roles](./roles.md). +An array of [`Roles`](./roles.md#role) that the user has. ### `uri` -The URI of the account's Versia object (for federation). Similar to Mastodon's `uri` field on notes. +URI of the account's Versia entity (for federation). Similar to Mastodon's `uri` field on notes. ## `/api/v1/accounts/update_credentials` diff --git a/docs/api/moderation.md b/docs/api/moderation.md deleted file mode 100644 index e4603b5b..00000000 --- a/docs/api/moderation.md +++ /dev/null @@ -1,271 +0,0 @@ -# Moderation API - -> [!WARNING] -> **NOT IMPLEMENTED** - -The Versia Server project uses the Mastodon API to interact with clients. However, the moderation API is custom-made for Versia Server Server, as it allows for more fine-grained control over the server's behavior. - -## Flags, ModTags and ModNotes - -Flags are used by Versia Server Server to automatically attribute tags to a status or account based on rules. ModTags and ModNotes are used by moderators to manually tag and take notes on statuses and accounts. - -The difference between flags and modtags is that flags are automatically attributed by the server, while modtags are manually attributed by moderators. - -### Flag Types - -- `content_filter`: (Statuses only) The status contains content that was filtered by the server's content filter. -- `bio_filter`: (Accounts only) The account's bio contains content that was filtered by the server's content filter. -- `emoji_filter`: The status or account contains an emoji that was filtered by the server's content filter. -- `reported`: The status or account was previously reported by a user. -- `suspended`: The status or account was previously suspended by a moderator. -- `silenced`: The status or account was previously silenced by a moderator. - -### ModTag Types - -ModTag do not have set types and can be anything. Versia Server Server autosuggest previously used tags when a moderator is adding a new tag to avoid duplicates. - -### Data Format - -```ts -type Flag = { - id: string, - // One of the following two fields will be present - flaggedStatus?: Status, - flaggedUser?: User, - flagType: string, - createdAt: string, -} - -type ModTag = { - id: string, - // One of the following two fields will be present - taggedStatus?: Status, - taggedUser?: User, - mod: User, - tag: string, - createdAt: string, -} - -type ModNote = { - id: string, - // One of the following two fields will be present - notedStatus?: Status, - notedUser?: User, - mod: User, - note: string, - createdAt: string, -} -``` - -The `User` and `Status` types are the same as the ones in the Mastodon API. - -## Moderation API Routes - -### `GET /api/v1/moderation/accounts/:id` - -Returns full moderation data and flags for the account with the given ID. - -Output format: - -```ts -{ - id: string, // Same ID as in account field - flags: Flag[], - modtags: ModTag[], - modnotes: ModNote[], - account: User, -} -``` - -### `GET /api/v1/moderation/statuses/:id` - -Returns full moderation data and flags for the status with the given ID. - -Output format: - -```ts -{ - id: string, // Same ID as in status field - flags: Flag[], - modtags: ModTag[], - modnotes: ModNote[], - status: Status, -} -``` - -### `POST /api/v1/moderation/accounts/:id/modtags` - -Params: -- `tag`: string - -Adds a modtag to the account with the given ID - -### `POST /api/v1/moderation/statuses/:id/modtags` - -Params: -- `tag`: string - -Adds a modtag to the status with the given ID - -### `POST /api/v1/moderation/accounts/:id/modnotes` - -Params: -- `note`: string - -Adds a modnote to the account with the given ID - -### `POST /api/v1/moderation/statuses/:id/modnotes` - -Params: -- `note`: string - -Adds a modnote to the status with the given ID - -### `DELETE /api/v1/moderation/accounts/:id/modtags/:modtag_id` - -Deletes the modtag with the given ID from the account with the given ID - -### `DELETE /api/v1/moderation/statuses/:id/modtags/:modtag_id` - -Deletes the modtag with the given ID from the status with the given ID - -### `DELETE /api/v1/moderation/accounts/:id/modnotes/:modnote_id` - -Deletes the modnote with the given ID from the account with the given ID - -### `DELETE /api/v1/moderation/statuses/:id/modnotes/:modnote_id` - -Deletes the modnote with the given ID from the status with the given ID - -### `GET /api/v1/moderation/modtags` - -Returns a list of all modtags previously used by moderators - -Output format: - -```ts -{ - tags: string[], -} -``` - -### `GET /api/v1/moderation/accounts/flags/search` - -Allows moderators to search for accounts based on their flags, this can also include status flags - -Params: -- `limit`: Number -- `min_id`: String. Returns results immediately newer than this ID. In effect, sets a cursor at this ID and paginates forward. -- `max_id`: String. All results returned will be lesser than this ID. In effect, sets an upper bound on results. -- `since_id`: String. All results returned will be greater than this ID. In effect, sets a lower bound on results. -- `flags`: String (optional). Comma-separated list of flag types to filter by. Can be left out to return accounts with at least one flag -- `flag_count`: Number (optional). Minimum number of flags to filter by -- `include_statuses`: Boolean (optional). If true, includes status flags in the search results -- `account_id`: Array of strings (optional). Filters accounts by account ID - -This method returns a `Link` header the same way Mastodon does, to allow for pagination. - -Output format: - -```ts -{ - accounts: { - account: User, - modnotes: ModNote[], - flags: Flag[], - statuses?: { - status: Status, - modnotes: ModNote[], - flags: Flag[], - }[], - }[], -} -``` - -### `GET /api/v1/moderation/statuses/flags/search` - -Allows moderators to search for statuses based on their flags - -Params: -- `limit`: Number -- `min_id`: String. Returns results immediately newer than this ID. In effect, sets a cursor at this ID and paginates forward. -- `max_id`: String. All results returned will be lesser than this ID. In effect, sets an upper bound on results. -- `since_id`: String. All results returned will be greater than this ID. In effect, sets a lower bound on results. -- `flags`: String (optional). Comma-separated list of flag types to filter by. Can be left out to return statuses with at least one flag -- `flag_count`: Number (optional). Minimum number of flags to filter by -- `account_id`: Array of strings (optional). Filters statuses by account ID - -This method returns a `Link` header the same way Mastodon does, to allow for pagination. - -Output format: - -```ts -{ - statuses: { - status: Status, - modnotes: ModNote[], - flags: Flag[], - }[], -} -``` - -### `GET /api/v1/moderation/accounts/modtags/search` - -Allows moderators to search for accounts based on their modtags - -Params: -- `limit`: Number -- `min_id`: String. Returns results immediately newer than this ID. In effect, sets a cursor at this ID and paginates forward. -- `max_id`: String. All results returned will be lesser than this ID. In effect, sets an upper bound on results. -- `since_id`: String. All results returned will be greater than this ID. In effect, sets a lower bound on results. -- `tags`: String (optional). Comma-separated list of tags to filter by. Can be left out to return accounts with at least one tag -- `tag_count`: Number (optional). Minimum number of tags to filter by -- `include_statuses`: Boolean (optional). If true, includes status tags in the search results -- `account_id`: Array of strings (optional). Filters accounts by account ID - -This method returns a `Link` header the same way Mastodon does, to allow for pagination. - -Output format: - -```ts -{ - accounts: { - account: User, - modnotes: ModNote[], - modtags: ModTag[], - statuses?: { - status: Status, - modnotes: ModNote[], - modtags: ModTag[], - }[], - }[], -} -``` - -### `GET /api/v1/moderation/statuses/modtags/search` - -Allows moderators to search for statuses based on their modtags - -Params: -- `limit`: Number -- `min_id`: String. Returns results immediately newer than this ID. In effect, sets a cursor at this ID and paginates forward. -- `max_id`: String. All results returned will be lesser than this ID. In effect, sets an upper bound on results. -- `since_id`: String. All results returned will be greater than this ID. In effect, sets a lower bound on results. -- `tags`: String (optional). Comma-separated list of tags to filter by. Can be left out to return statuses with at least one tag -- `tag_count`: Number (optional). Minimum number of tags to filter by -- `account_id`: Array of strings (optional). Filters statuses by account ID -- `include_statuses`: Boolean (optional). If true, includes status tags in the search results - -This method returns a `Link` header the same way Mastodon does, to allow for pagination. - -Output format: - -```ts -{ - statuses: { - status: Status, - modnotes: ModNote[], - modtags: ModTag[], - }[], -} -``` diff --git a/docs/api/roles.md b/docs/api/roles.md index e7c3a5be..b908ea61 100644 --- a/docs/api/roles.md +++ b/docs/api/roles.md @@ -1,79 +1,82 @@ # Roles API -The Roles API lets users manage roles given to them by administrators. This API is available to all users. - > [!WARNING] -> The API for **administrators** is different (and unimplemented): this is the API for **users**. > -> Furthermore, users can only manage roles if they have the `roles` permission, and the role they wish to manage does not have a higher priority than their highest priority role. +> This API is due to be reworked in the future. The current implementation is not final. +> +> Missing features include the ability to create and delete roles, as well as the ability to assign roles to other users. + +This API allows users to create, read, update, and delete instance custom roles. Custom roles can be used to grant users specific permissions, such as managing the instance, uploading custom emojis, or moderating content. ## Priorities -Roles have a priority, which determines the order in which they are applied. Roles with higher priorities take precedence over roles with lower priorities. +Every role has a "priority" value, which determines the order in which roles are applied. Roles with higher priorities take precedence over roles with lower priorities. The default priority is `0`. -Additionally, users cannot remove or add roles with a higher priority than their highest priority role. +Additionally, users cannot edit roles with a higher priority than their highest priority role. ## Visibility -Roles can be visible or invisible. Invisible roles are not shown to users in the UI, but they can still be managed via the API. - -> [!WARNING] -> All roles assigned to a user are public information and can be retrieved via the API. The visibility of a role only affects whether it is shown in the UI, which clients can choose to respect or not. +Roles can be visible or invisible. Invisible roles are not shown to users in the UI, but they can still be managed via the API. This is useful for cosmetic roles that do not grant any permissions, e.g. `#1 Most Prominent Glizzy Eater`. ## Permissions -Default permissions for anonymous users, logged-in users and admins can be set in config. These are always applied in addition to the permissions granted by roles. You may set them to empty arrays to exclusively use roles for permissions (make sure your roles are set up correctly). +Default permissions for anonymous users, logged-in users, and administrators can be set in the configuration. These permissions are always applied in addition to the permissions granted by roles. You may set them to empty arrays to exclusively use roles for permissions (make sure your roles are set up correctly). + +### List of Permissions + +- `Manage` permissions grant the ability to create, read, update, and delete resources. +- `View` permissions grant the ability to read resources. +- `Owner` permissions grant the ability to manage resources that the user owns. ```ts -// Last updated: 2024-06-07 -// Search for "RolePermissions" in the source code (GitHub search bar) for the most up-to-date version -export enum RolePermissions { - MANAGE_NOTES = "notes", - MANAGE_OWN_NOTES = "owner:note", - VIEW_NOTES = "read:note", - VIEW_NOTE_LIKES = "read:note_likes", - VIEW_NOTE_BOOSTS = "read:note_boosts", - MANAGE_ACCOUNTS = "accounts", - MANAGE_OWN_ACCOUNT = "owner:account", - VIEW_ACCOUNT_FOLLOWS = "read:account_follows", - MANAGE_LIKES = "likes", - MANAGE_OWN_LIKES = "owner:like", - MANAGE_BOOSTS = "boosts", - MANAGE_OWN_BOOSTS = "owner:boost", - VIEW_ACCOUNTS = "read:account", - MANAGE_EMOJIS = "emojis", - VIEW_EMOJIS = "read:emoji", - MANAGE_OWN_EMOJIS = "owner:emoji", - MANAGE_MEDIA = "media", - MANAGE_OWN_MEDIA = "owner:media", - MANAGE_BLOCKS = "blocks", - MANAGE_OWN_BLOCKS = "owner:block", - MANAGE_FILTERS = "filters", - MANAGE_OWN_FILTERS = "owner:filter", - MANAGE_MUTES = "mutes", - MANAGE_OWN_MUTES = "owner:mute", - MANAGE_REPORTS = "reports", - MANAGE_OWN_REPORTS = "owner:report", - MANAGE_SETTINGS = "settings", - MANAGE_OWN_SETTINGS = "owner:settings", - MANAGE_ROLES = "roles", - MANAGE_NOTIFICATIONS = "notifications", - MANAGE_OWN_NOTIFICATIONS = "owner:notification", - MANAGE_FOLLOWS = "follows", - MANAGE_OWN_FOLLOWS = "owner:follow", - MANAGE_OWN_APPS = "owner:app", - SEARCH = "search", - VIEW_PUBLIC_TIMELINES = "public_timelines", - VIEW_PRIVATE_TIMELINES = "private_timelines", - IGNORE_RATE_LIMITS = "ignore_rate_limits", - IMPERSONATE = "impersonate", - MANAGE_INSTANCE = "instance", - MANAGE_INSTANCE_FEDERATION = "instance:federation", - MANAGE_INSTANCE_SETTINGS = "instance:settings", - OAUTH = "oauth", -} +ManageNotes: "notes", +ManageOwnNotes: "owner:note", +ViewNotes: "read:note", +ViewNoteLikes: "read:note_likes", +ViewNoteBoosts: "read:note_boosts", +ManageAccounts: "accounts", +ManageOwnAccount: "owner:account", +ViewAccountFollows: "read:account_follows", +ManageLikes: "likes", +ManageOwnLikes: "owner:like", +ManageBoosts: "boosts", +ManageOwnBoosts: "owner:boost", +ViewAccounts: "read:account", +ManageEmojis: "emojis", +ViewEmojis: "read:emoji", +ManageOwnEmojis: "owner:emoji", +ManageMedia: "media", +ManageOwnMedia: "owner:media", +ManageBlocks: "blocks", +ManageOwnBlocks: "owner:block", +ManageFilters: "filters", +ManageOwnFilters: "owner:filter", +ManageMutes: "mutes", +ManageOwnMutes: "owner:mute", +ManageReports: "reports", +ManageOwnReports: "owner:report", +ManageSettings: "settings", +ManageOwnSettings: "owner:settings", +ManageRoles: "roles", +ManageNotifications: "notifications", +ManageOwnNotifications: "owner:notification", +ManageFollows: "follows", +ManageOwnFollows: "owner:follow", +ManageOwnApps: "owner:app", +Search: "search", +ViewPublicTimelines: "public_timelines", +ViewPrimateTimelines: "private_timelines", +IgnoreRateLimits: "ignore_rate_limits", +Impersonate: "impersonate", +ManageInstance: "instance", +ManageInstanceFederation: "instance:federation", +ManageInstanceSettings: "instance:settings", +/** Users who do not have this permission will not be able to login! */ +OAuth: "oauth", ``` +An example usage of these permissions would be to not give the `ViewPublicTimelines` permission to anonymous users, but give it to logged-in users, in order to restrict access to public timelines. + ### Manage Roles The `roles` permission allows the user to manage roles, including adding and removing roles from themselves. This permission is required to use the Roles API. @@ -82,21 +85,29 @@ The `roles` permission allows the user to manage roles, including adding and rem The `impersonate` permission allows the user to impersonate other users (logging in with their credentials). This is a dangerous permission and should be used with caution. -### Manage Instance +Useful for administrators who need to troubleshoot user issues. -The `instance` permission allows the user to manage the instance, including viewing logs, restarting the instance, and more. +### OAuth -### Manage Instance Federation +The `oauth` permission is required for users to log in via OAuth. Users who do not have this permission will not be able to log in via OAuth. -The `instance:federation` permission allows the user to manage the instance's federation settings, including blocking and unblocking other instances. +## Role -### Manage Instance Settings +```ts +type UUID = string; +type URL = string; +type Permission = string; -The `instance:settings` permission allows the user to manage the instance's settings, including changing the instance's name, description, and more. - -### OAuth2 - -The `oauth` permission is required for users to log in to the instance. Users who do not have this permission will not be able to log in. +interface Role { + id: UUID; + name: string; + permissions: Permission[]; + priority: number; + description?: string | null; + visible: boolean; + icon?: URL | null; +} +``` ## Get Roles @@ -104,21 +115,117 @@ The `oauth` permission is required for users to log in to the instance. Users wh GET /api/v1/roles ``` -Retrieves a list of roles that the user has. +Get a list of all roles that the requesting user has. + +- **Returns**: Array of [`Role`](#role) +- **Authentication**: Required +- **Permissions**: None +- **Version History**: + - `0.7.0`: Added. + +### Example + +```http +GET /api/v1/roles +Authorization: Bearer ... +``` ### Response -```ts -// 200 OK -{ - id: string; - name: string; - permissions: RolePermissions[]; - priority: number; - description: string | null; - visible: boolean; - icon: string | null -}[]; +#### `200 OK` + +All roles owned by the user. + +```json +[ + { + "id": "default", + "name": "Default", + "permissions": [ + "owner:note", + "read:note", + "read:note_likes", + "read:note_boosts", + "owner:account", + "read:account_follows", + "owner:like", + "owner:boost", + "read:account", + "owner:emoji", + "read:emoji", + "owner:media", + "owner:block", + "owner:filter", + "owner:mute", + "owner:report", + "owner:settings", + "owner:notification", + "owner:follow", + "owner:app", + "search", + "public_timelines", + "private_timelines", + "oauth" + ], + "priority": 0, + "description": "Default role for all users", + "visible": false, + "icon": null + }, + { + "id": "admin", + "name": "Admin", + "permissions": [ + "owner:note", + "read:note", + "read:note_likes", + "read:note_boosts", + "owner:account", + "read:account_follows", + "owner:like", + "owner:boost", + "read:account", + "owner:emoji", + "read:emoji", + "owner:media", + "owner:block", + "owner:filter", + "owner:mute", + "owner:report", + "owner:settings", + "owner:notification", + "owner:follow", + "owner:app", + "search", + "public_timelines", + "private_timelines", + "oauth", + "notes", + "accounts", + "likes", + "boosts", + "emojis", + "media", + "blocks", + "filters", + "mutes", + "reports", + "settings", + "roles", + "notifications", + "follows", + "impersonate", + "ignore_rate_limits", + "instance", + "instance:federation", + "instance:settings" + ], + "priority": 2147483647, + "description": "Default role for all administrators", + "visible": false, + "icon": null + } +] ``` ## Get Role @@ -127,36 +234,94 @@ Retrieves a list of roles that the user has. GET /api/v1/roles/:id ``` -Retrieves information about a role. +Get a specific role's data. + +- **Returns**: [`Role`](#role) +- **Authentication**: Required +- **Permissions**: None +- **Version History**: + - `0.7.0`: Added. + +### Request + +#### Example + +```http +GET /api/v1/roles/default +Authorization: Bearer ... +``` ### Response -```ts -// 200 OK +#### `200 OK` + +Role data. + +```json { - id: string; - name: string; - permissions: RolePermissions[]; - priority: number; - description: string | null; - visible: boolean; - icon: string | null + "id": "default", + "name": "Default", + "permissions": [ + "owner:note", + "read:note", + "read:note_likes", + "read:note_boosts", + "owner:account", + "read:account_follows", + "owner:like", + "owner:boost", + "read:account", + "owner:emoji", + "read:emoji", + "owner:media", + "owner:block", + "owner:filter", + "owner:mute", + "owner:report", + "owner:settings", + "owner:notification", + "owner:follow", + "owner:app", + "search", + "public_timelines", + "private_timelines", + "oauth" + ], + "priority": 0, + "description": "Default role for all users", + "visible": false, + "icon": null } ``` -## Add Role +## Assign Role ```http POST /api/v1/roles/:id ``` -Adds the role with the given ID to the user making the request. +Assign a role to the user making the request. + +- **Returns**: `204 No Content` +- **Authentication**: Required +- **Permissions**: `roles` +- **Version History**: + - `0.7.0`: Added. + +### Request + +#### Example + +```http +POST /api/v1/roles/364fd13f-28b5-4e88-badd-ce3e533f0d02 +Authorization: Bearer ... +``` ### Response -```ts -// 204 No Content -``` +#### `204 No Content` + +Role successfully assigned. ## Remove Role @@ -164,10 +329,25 @@ Adds the role with the given ID to the user making the request. DELETE /api/v1/roles/:id ``` -Removes the role with the given ID from the user making the request. +Remove a role from the user making the request. + +- **Returns**: `204 No Content` +- **Authentication**: Required +- **Permissions**: `roles` +- **Version History**: + - `0.7.0`: Added. + +### Request + +#### Example + +```http +DELETE /api/v1/roles/364fd13f-28b5-4e88-badd-ce3e533f0d +Authorization: Bearer ... +``` ### Response -```ts -// 204 No Content -``` \ No newline at end of file +#### `204 No Content` + +Role successfully removed. \ No newline at end of file diff --git a/docs/api/sso.md b/docs/api/sso.md new file mode 100644 index 00000000..da234e83 --- /dev/null +++ b/docs/api/sso.md @@ -0,0 +1,164 @@ +# SSO API + +The SSO API is used to link, unlink, and list external OpenID Connect providers that the user has linked their account to. + +## SSO Provider + +```ts +interface SSOProvider { + id: string; + name: string; + icon: string; +} +``` + +## SSO Link + +```http +POST /api/v1/sso +``` + +Allows users to link their account to an external OpenID Connect provider. + +- **Returns**: Link to redirect the user to the external provider. +- **Authentication**: Required +- **Permissions**: `oauth` +- **Version History**: + - `0.6.0`: Added. + - `0.7.0`: Permissions added. + +### Request + +- `issuer` (string, required): The issuer ID of the OpenID Connect provider as set in config. + +#### Example + +```http +POST /api/v1/sso +Authorization: Bearer ... +Content-Type: application/json + +{ + "issuer": "google" +} +``` + +### Response + +#### `200 OK` + +Link to redirect the user to the external provider's page. + +```json +{ + "link": "https://accounts.google.com/o/oauth2/auth?client_id=..." +} +``` + +## SSO Unlink + +```http +DELETE /api/v1/sso/:issuer +``` + +Allows users to unlink their account from an external OpenID Connect provider. + +- **Returns**: `204 No Content` +- **Authentication**: Required +- **Permissions**: `oauth` +- **Version History**: + - `0.6.0`: Added. + - `0.7.0`: Permissions added. + +### Request + +#### Example + +```http +DELETE /api/v1/sso/google +Authorization: Bearer ... +``` + +### Response + +#### `204 No Content` + +Account successfully unlinked. + +## List Connected Providers + +```http +GET /api/v1/sso +``` + +Lists all external OpenID Connect providers that the user has linked their account to. + +- **Returns**: Array of [`SSOProvider`](#ssoprovider) objects. +- **Authentication**: Required +- **Permissions**: `oauth` +- **Version History**: + - `0.6.0`: Added. + - `0.7.0`: Permissions added. + +### Request + +#### Example + +```http +GET /api/v1/sso +Authorization: Bearer ... +``` + +### Response + +#### `200 OK` + +Array of [`SSOProvider`](#ssoprovider) objects. + +```json +[ + { + "id": "google", + "name": "Google", + "icon": "https://cdn.example.com/google.png" + } +] +``` + +## Get Linked Provider Data + +```http +GET /api/v1/sso/:issuer +``` + +Gets the data of an external OpenID Connect provider that the user has linked their account to. + +- **Returns**: [`SSOProvider`](#ssoprovider) object. +- **Authentication**: Required +- **Permissions**: `oauth` +- **Version History**: + - `0.6.0`: Added. + - `0.7.0`: Permissions added. + +### Request + +#### Example + +```http +GET /api/v1/sso/google +Authorization: Bearer ... +``` + +### Response + +#### `200 OK` + +[`SSOProvider`](#ssoprovider) object. + +```json +{ + "id": "google", + "name": "Google", + "icon": "https://cdn.example.com/google.png" +} +``` \ No newline at end of file diff --git a/docs/cli.md b/docs/cli/index.md similarity index 85% rename from docs/cli.md rename to docs/cli/index.md index 332ae782..d97e86a0 100644 --- a/docs/cli.md +++ b/docs/cli/index.md @@ -12,6 +12,7 @@ bun cli help # Source installs bun run dist/cli.js help # Docker +# Replace `versia` with the name of your container docker compose exec -it versia /bin/sh /app/entrypoint.sh cli help ``` @@ -19,6 +20,6 @@ You can use the `help` command to see a list of available commands. These includ ## Scripting with the CLI -Some CLI commands that return data as tables can be used in scripts. To convert them to JSON or CSV, some commands allow you to specify a `--format` flag that can be either `"json"` or `"csv"`. See `bun cli help` or `bun cli -h` for more information. +Some CLI commands that return data as tables can be used in scripts. To convert them to JSON or CSV, some commands allow you to specify a `--format` flag that can be either `"json"` or `"csv"`. See `cli help` or `cli -h` for more information. -Flags can be used in any order and anywhere in the script (except for the `bun cli` command itself). The command arguments themselves must be in the correct order, however.Z \ No newline at end of file +Flags can be used in any order and anywhere in the script (except for the `cli` command itself). \ No newline at end of file diff --git a/docs/frontend/auth.md b/docs/frontend/auth.md new file mode 100644 index 00000000..cc156eb4 --- /dev/null +++ b/docs/frontend/auth.md @@ -0,0 +1,139 @@ +# Frontend Authentication + +Multiple API routes are exposed for authentication, to be used by frontend developers. + +> [!INFO] +> +> These are different from the Client API routes, which are used by clients to interact with the Mastodon API. + +A frontend is a web application that is designed to be the primary user interface for an instance. It is used also used by clients to perform authentication. + +## Get Frontend Configuration + +```http +GET /api/v1/frontend/config +``` + +Retrieves the frontend configuration for the instance. This returns whatever the `frontend.settings` object is set to in the Versia Server configuration. + +This behaves like the `/api/v1/preferences` endpoint in the Mastodon API, but is specific to the frontend. These values are arbitrary and can be used for anything. + +Frontend developers should always namespace their keys to avoid conflicts with other keys. + +- **Returns**: Object with arbitrary keys and values. +- **Authentication**: Not required +- **Permissions**: None +- **Version History**: + - `0.7.0`: Added. + +### Request + +#### Example + +```http +GET /api/v1/frontend/config +``` + +### Response + +#### `200 OK` + +Frontend configuration. + +```json +{ + "pub.versia.fe:theme": "dark", + "pub.versia.fe:custom_css": "body { background-color: black; }", + "net.googly.frontend:spoiler_image": "https://example.com/spoiler.png" +} +``` + +## Sign In + +```http +POST /api/auth/login +``` + +Allows users to sign in to the instance. This is the first step in the authentication process. + +- **Returns**: `302 Found` with a `Location` header to redirect the user to the next step, as well as a `Set-Cookie` header with the session JWT. +- **Authentication**: Not required +- **Permissions**: None +- **Version History**: + - `0.7.0`: First documented. + +### Request + +- `identifier` (string, required): The username or email of the user. Case-insensitive. +- `password` (string, required): The password of the user. + +#### Query Parameters + +- `client_id` (string, required): Client ID of the [application](https://docs.joinmastodon.org/entities/Application/) that is making the request. +- `redirect_uri` (string, required): Redirect URI of the [application](https://docs.joinmastodon.org/entities/Application/) that is making the request. Must match the saved value. +- `response_type` (string, required): Must be `code`. +- `scope` (string, required): OAuth2 scopes. Must match the value indicated in the [application](https://docs.joinmastodon.org/entities/Application/). + +#### Example + +```http +POST /api/auth/login?client_id=123&redirect_uri=https%3A%2F%2Fexample.com%2Fauth&response_type=code&scope=read%20write +Content-Type: application/json + +{ + "identifier": "bobjones@gmail.com", + "password": "hunter2" +} +``` + +### Response + +#### `302 Found` + +Redirects the user to the consent page with some query parameters. The frontend should redirect the user to this URL. + +This response also has a `Set-Cookie` header with a [JSON Web Token](https://jwt.io/) that contains the user's session information. This JWT is signed with the instance's secret key, and must be included in all subsequent authentication requests. + +```http +HTTP/2.0 302 Found +Location: /oauth/consent?client_id=123&redirect_uri=https%3A%2F%2Fexample.com%2Fauth&response_type=code&scope=read%20write +Set-Cookie: jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c; Path=/; HttpOnly; Secure; SameSite=Strict; Max-Age=3600 +``` + +## SSO Sign In + +```http +POST /oauth/sso +``` + +Allows users to sign in to the instance using an external OpenID Connect provider. + +- **Returns**: `302 Found` with a `Location` header to redirect the user to the next step. +- **Authentication**: Not required +- **Permissions**: None +- **Version History**: + - `0.7.0`: First documented. + +### Request + +#### Query Parameters + +- `client_id` (string, required): Client ID of the [application](https://docs.joinmastodon.org/entities/Application/) that is making the request. +- `issuer` (string, required): The ID of the OpenID Connect provider, as found in `/api/{v1,v2}/instance`. + +#### Example + +```http +POST /oauth/sso?client_id=123&issuer=google +``` + +### Response + +#### `302 Found` + +Redirects the user to the OpenID Connect provider's login page. + +```http +HTTP/2.0 302 Found +Location: https://accounts.google.com/o/oauth2/auth?client_id=123&redirect_uri=https%3A%2F%2Fexample.com%2Fauth&response_type=code&scope=openid%20email&state=123 +``` \ No newline at end of file diff --git a/docs/frontend/routes.md b/docs/frontend/routes.md new file mode 100644 index 00000000..e40ad545 --- /dev/null +++ b/docs/frontend/routes.md @@ -0,0 +1,53 @@ +# Frontend Routes + +Frontend implementors must implement these routes for correct operation of the instance. + +The location of these routes can be configured in the Versia Server configuration at `frontend.routes`: + +## Login Form + +```http +GET /oauth/authorize +``` + +This route should display a login form for the user to enter their username and password, as well as a list of OpenID providers to use if available. + +The form should submit to [`POST /api/auth/login`](./auth.md#sign-in), or to the OpenID Connect flow. + +Configurable in the Versia Server configuration at `frontend.routes.login`. + +## Consent Form + +```http +GET /oauth/consent +``` + +This route should display a consent form for the user to approve the requested application permissions, after logging in. + +The form should submit an OpenID Connect authorization request at `POST /oauth/authorize`, with the correct [application](https://docs.joinmastodon.org/entities/Application/) data (client ID, redirect URI, etc.). Do not forget the JWT cookie. + +### Submission Example + +```http +POST /oauth/authorize +Content-Type: application/json +Cookie: jwt=... + +{ + "client_id": "client_id", + "response_type": "code", + "redirect_uri": "https://example.com/callback", + "scope": "read write", + "state": "state123", + "code_challenge": "code_challenge", + "code_challenge_method": "S256", + "response_type": "code" +} +``` + +### Submission Response + +```http +HTTP/2.0 302 Found +Location: https://example.com/callback?code=code&state=state123 +``` diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..93b0e122 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,18 @@ +--- +layout: home +hero: + name: Versia Server Docs +features: + - icon: 🛠️ + title: Installation + details: Details on how to install Versia Server + link: ./setup/installation + - icon: 🖥 + title: API Reference + details: Writing your own client? Check out the API reference + link: ./api/emojis + - icon: 📚 + title: Frontend Building + details: Information on developing your own frontend + link: ./frontend/routes +--- \ No newline at end of file diff --git a/docs/database.md b/docs/setup/database.md similarity index 100% rename from docs/database.md rename to docs/setup/database.md diff --git a/docs/installation.md b/docs/setup/installation.md similarity index 83% rename from docs/installation.md rename to docs/setup/installation.md index d43ae541..11eb7e48 100644 --- a/docs/installation.md +++ b/docs/setup/installation.md @@ -67,10 +67,10 @@ git clone https://github.com/versia-pub/server.git bun install ``` -1. Set up a PostgreSQL database (you need a special extension, please look at [the database documentation](database.md)) +1. Set up a PostgreSQL database (you need a special extension, please look at [the database documentation](./database.md)) 2. (If you want search) -Create a [Sonic](https://github.com/valeriansaliou/sonic) instance (using Docker is recommended). For a [`docker-compose`] file, copy the `sonic` service from the [`docker-compose.yml`](../docker-compose.yml) file. Don't forget to fill in the `config.cfg` for Sonic! +Create a [Sonic](https://github.com/valeriansaliou/sonic) instance (using Docker is recommended). For a [`docker-compose`] file, copy the `sonic` service from the [`docker-compose.yml`](https://github.com/versia-pub/server/blob/main/docker-compose.yml) file. Don't forget to fill in the `config.cfg` for Sonic! 1. Build everything: @@ -80,21 +80,23 @@ bun run build 4. Copy the `config.example.toml` file to `config.toml` inside `dist/config/` and fill in the values (you can leave most things to the default, but you will need to configure things such as the database connection) -CD to the `dist/` directory: `cd dist` +5. Move to the `dist/` directory -You may now start the server with `bun run cli/index.js start`. It lives in the `dist/` directory, all the other code can be removed from this point onwards. +```bash +cd dist +``` + +You may now start the server with `bun run cli/index.js start`. All other code not in the `dist/` directory can be removed. ## Running the Server Database migrations are run automatically on startup. -You may use the environment variables `NO_COLORS=true` and `NO_FANCY_DATES=true` to disable colors and date formatting in the console logs: the file logs will never have colors or fancy dates. - -Please see the [CLI documentation](cli.md) for more information on how to use the CLI. +Please see the [CLI documentation](../cli/index.md) for more information on how to use the CLI. ## Updating the server -Updating the server is as simple as pulling the latest changes from the repository and running `bun prod-build` again. You may need to run `bun install` again if there are new dependencies. +Updating the server is as simple as pulling the latest changes from the repository and running `bun run build` again. You may need to run `bun install` again if there are new dependencies. For Docker, you can run `docker-compose pull` to update the Docker images. diff --git a/package.json b/package.json index 0cbe1de2..43a7b527 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,10 @@ "prune": "ts-prune | grep -v server/ | grep -v dist/ | grep -v '(used in module)'", "schema:generate": "bun run packages/config-manager/json-schema.ts > config/config.schema.json && bun run packages/plugin-kit/json-schema.ts > packages/plugin-kit/manifest.schema.json", "check": "bunx tsc -p .", - "test": "find . -name \"*.test.ts\" -not -path \"./node_modules/*\" | xargs -I {} sh -c 'bun test {} || exit 255'" + "test": "find . -name \"*.test.ts\" -not -path \"./node_modules/*\" | xargs -I {} sh -c 'bun test {} || exit 255'", + "docs:dev": "vitepress dev docs", + "docs:build": "vitepress build docs", + "docs:preview": "vitepress preview docs" }, "trustedDependencies": [ "@biomejs/biome", @@ -88,10 +91,15 @@ "@types/pg": "^8.11.10", "@types/qs": "^6.9.17", "drizzle-kit": "^0.28.0", + "markdown-it-image-figures": "^2.1.1", + "markdown-it-mathjax3": "^4.3.2", "oclif": "^4.15.20", "ts-prune": "^0.10.3", "typescript": "^5.6.3", "vitepress": "^1.5.0", + "vitepress-plugin-tabs": "^0.5.0", + "vitepress-sidebar": "^1.29.0", + "vue": "^3.5.12", "zod-to-json-schema": "^3.23.5" }, "peerDependencies": {