# v2 git bundle
d915814277e4c30f21a14c8f52a9e8fbdd84e0d7 refs/heads/bundle
d915814277e4c30f21a14c8f52a9e8fbdd84e0d7 HEAD

PACK      SxNtree 6cb43165ebf341a863b3ebea00aaee39d8de54e3
parent d7b8e9dd086517865ef64ae09610549bf1e9ca68
author MythEclipse <asepharyanalm001@gmail.com> 1779062969 +0700
committer MythEclipse <asepharyanalm001@gmail.com> 1779062969 +0700

feat: add file routes (download redirect & info)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
jxOtree 8afb941670dbeb0ec568031a6d04cdfc453d72b5
parent 1e29433d5d6ccf3f3982dce5862b10fa9833bb88
author MythEclipse <asepharyanalm001@gmail.com> 1779062893 +0700
committer MythEclipse <asepharyanalm001@gmail.com> 1779062893 +0700

feat: add upload API handler (multipart & base64)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
icx>tree 5b604121a90eed6694ec3b040754e9a197aca9d4
parent b621a73ca9a0548043082f989f4486b0f84d20f7
author MythEclipse <asepharyanalm001@gmail.com> 1779062781 +0700
committer MythEclipse <asepharyanalm001@gmail.com> 1779062781 +0700

feat: add Telegram API utilities

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
bxAtree e972b38fb43515d4af00a7adb7bc18ed6d2df791
parent db71f2928831867a7ebfed3063cb04791a92318c
author MythEclipse <asepharyanalm001@gmail.com> 1779062534 +0700
committer MythEclipse <asepharyanalm001@gmail.com> 1779062534 +0700

feat: add file validation utilities

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ReӞx>tree 96ea07962b9f8e5ae7c158b61e48ca87eae76c7b
parent 281329a1afad5d87ef17215963272e4c0a814787
author MythEclipse <asepharyanalm001@gmail.com> 1779062492 +0700
committer MythEclipse <asepharyanalm001@gmail.com> 1779062492 +0700

feat: add in-memory rate limiter

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
[cxBtree 97422055f2943fe49f550c3429e829885216f7ed
parent f9c0bc076893eb040fdc59e95a9cbd7c08df5da8
author MythEclipse <asepharyanalm001@gmail.com> 1779062446 +0700
committer MythEclipse <asepharyanalm001@gmail.com> 1779062446 +0700

feat: add Drizzle ORM database layer

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
eexHtree b96e8a268e19ef296429991b1ab4c70f424ed5c2
parent 77032538e8bffb263f793c974ead979ca771ccdf
author MythEclipse <asepharyanalm001@gmail.com> 1779062335 +0700
committer MythEclipse <asepharyanalm001@gmail.com> 1779062335 +0700

feat: add environment variables validation

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
"hx<tree 17dbff3e388f7c293abd61d88d72696d099169f5
parent 25c82bcf0b70907221eb56fb34a6631603c02388
author MythEclipse <asepharyanalm001@gmail.com> 1779062013 +0700
committer MythEclipse <asepharyanalm001@gmail.com> 1779062013 +0700

feat: add Winston logger setup

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
0bxGtree ab5243c311cad0d5e631c4d9e27357f09d491d01
parent f423e1f25df3d165e3635c6992010fc84e6585b6
author MythEclipse <asepharyanalm001@gmail.com> 1779061866 +0700
committer MythEclipse <asepharyanalm001@gmail.com> 1779061866 +0700

chore: add dependencies and configuration

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
g@xtree 67be35fa7bfe978a8a536d17f5da9e981a86ce70
parent 3fb3dc3d40c8351efd488bf094049b42df93f831
author MythEclipse <asepharyanalm001@gmail.com> 1779061733 +0700
committer MythEclipse <asepharyanalm001@gmail.com> 1779061733 +0700

docs: add telegram uploader backend implementation plan
Ztxtree a2975f17297ac440fabe924e3efc6c05cb73c6bb
parent cf55f6dee8872e65c63e2772754dc2ae03c0e3f9
author MythEclipse <asepharyanalm001@gmail.com> 1779061362 +0700
committer MythEclipse <asepharyanalm001@gmail.com> 1779061362 +0700

docs: add telegram uploader backend design spec
D!WPxtree 8d70b092d2c44596b3bb7f248945874a29f1c862
author MythEclipse <asepharyanalm001@gmail.com> 1779060842 +0700
committer MythEclipse <asepharyanalm001@gmail.com> 1779060842 +0700

Initialize TeleUploader project with Bun, including configuration files and basic setup
)Wx340031QK+KH-Iexd s)Qh>c'doUY_ʠ^swoMSSrE/U⪗P#{s̽f)j^YS(jPkڸ~go,TQRi^N~r6';Uyz XłM@!%?]w]^l܏*5"3/%B[NJȻ}_-㿸 19;1=U/8?aQf*b}E۸} UWW\/B*_(KkOU/SqPqQ2Þ7,faG֑{Ȗ0̖OW^{
B89?/-3M=k;VlYq l `xMN0F.nlSII3U6T%Ʒ]h>|vRL>Ff0g?PdEC%Ulon1њ⤋х,I.B:4ʩ!	Oo衛yYLn5=Jc0q$vjboG8;2N͢*(BeMRC%}=XyYxmP=k0+t~-\tXJpl%UIV2NzOӗ.ጒP"aqaGR,0@4aӴ0Š$*goWK*f&r\9xX}8k6vE&GYbnFjmC	WdaϹT~GwN
ck[62,ĉdԦ!3=!Lzy0kwrrsHKwOoxmVmo6_qS)l	žu]]q%:f"
I6=GJ$@"~]Ob6%1ME2V9-}UL3J6/dȗT`	JSbl6ر{aGCM#v,s'k73ĕiv	G
9- TWz3T9)&ӲiĘګ~\9ЂJoŒ*˴_{	ZicUZ#*EzQ'GT~pGFG1H4u5]lV*LzYM	IES3Zc!y(P鄶|Ga3cȥR8ʓ{n{e>mZ>,8ٙF{zVho|gϷ&97Q+F(|~L
38,y_ЍAp3hS^'mv&\"~CSГcBi-2KϴժSτCwDQ(+]&tZȪ3~ayxU_[6v7F(uxld+T6+ib튵I}ކŞ"_82Ϲ(ù1<&<~m=}A?WbM*#&W}('O$9:ii"5<{1JUa.?owۛo{|^s?jp6
ekӈ`YP%r(
UؕzNV7Պ,B<=}|8lj!D aJh4-K.5`p`ι/J?(YǨ7Y+Yi6vp߯ȳpDʏKb~9"Ƌ'@X˰TyS.mۓp-c9ܧv'0!ak?HV;~s]3fJ?|2XMxf*㬐< [%spwy?EK$WY8p 6<cgT7X?0pv	0ma	=Yt/މjt?`vlW @s2Wl+b@3_71&fz]T=zx=rFv<ec493Z̊[$  β*ySRJm/O9Q٭TelӧO[7?g^g^E;qr^"fNG!{8a{u0g]GɥǏ7gÓꌾYt{I]#XfM+/ޕD1¬ěF>LDƛ.3?ub#eY|I~ʰeNzٙv6e^%0{Itìqg-nt%0dhn4?~9])x{&/8w[J]ٜK'EleM5ɾ;=}[m.JY~s,J+m@3qRentbK)ѶL~C_-C;+L&Џ n :ߞYC+K2ȜЋipI[D-KcD7.K"N`H"̱0B'|#kM?Nh|9{leɒhFL4ލS2Fa;I&r]x[9v^| ĳBq$.JC/𪪹+']/4wE Tm)pL6hG$	؇dvA$̀¢X8b*]Ox(8hX{$$Zf^ʉ"o{IOiLҮ^D
_5޽],\?)id:k tH`incOΒ:$j@$&3Q&Ke$Ҡ	DkDT	%h\طD@X֓h ~ߵ1,Z98SB2ҟQPN{G{Et0-Zݲw=Zj>f}6"/F-N}&-Qxϖ	
2JAf^MzfI{BIA¡*dC>зBgmΫT[mEXm/up5]	<^I#4{0P\|pnC";/b"`thp~vr:LoVbzDǞ	CIUOg7<uTSYmz0^y_ٽp#t 
=JڿH$l+ 5VBMQcJEƫo9)z!ۓ7Ãv_^CN<".;ɭ:6 9_10Y .Oҏ46@̡铝$\
dۓ'^Q!>÷o?wIOGo<?>!Qust4]'ZQ90@Άhw)(<`hN2쇽`bgC=eg؂k0¼V,4WCQ_*YuN\[ږkq|Ԑ5s#轣wj.NNǰdoнF! m[b|zg1KZVir)
(4hZ(upK\?_52A8Ktckf `1DWWHdldmA;cF7wm@eh1	+q-г~!JӼcxjg#΀"ҥ7ْB{0mǍz >sF`>gto
r,&\/㢧p5.[18~ƤEdO[)5ltWcqF<ã'A1!iя.[)赏YG%9
5dEԀHC`)hcwwy01_x!0ƹL.ʈ?cb.Ð>**W0K>V1@osU
BҌ&2t%F+̀KqrXP4@DLfqݜ_~kEhSjulk:Xnؐ[U8AB
q
"wDL")~56lW\q-t2\X}ԽS:S?5Qdv@ɦo8g"PX;z/I$>*^k[UZJS5G ,K5{ߕ"@I]c'C-
<JAoN$BPL QBZQq~C'@.5bßͶpʚjxkPp٥6hCcNex(3', p؃(D0Y<K}˚٢C
}ްŃM4( >󤀔 RʤZ<]`ԫFû AP[)e\qGɭGP:1ʧbIv
jC FUڤJ<e6<Wdx}4rcUMֺ@*	lՋ*/{yr=bа(E3C5eK+50]RT'`,c(6YpG-ytKC~dƖ!,=.n6	}Ā{r47.ZA*a7[X摠SeЛ>h$4qDפX6S}5[qnIrq)DSL6n]su4tϒ^"g_*
's	"oTV4FYRÍͮʤҀx(*hH#	cH"VsP%4QHl۶!Ė/t9Ia3;m	dʀt.M]n{#^@;tn+S:Re((;6WFlȌ(+d,spzgk-"֌s .)QMhL04vYg
:ቷ]aI(jSGVw-P{jُ:,ҷ6"0gP5'hؠ5pA/x,Ns[2{&
*sjr}1@2\.&'WtSU&!aq0<J 
N+Lt$=tc'M\0rj%RI{btH5Rqt]', te-2ĒD*e#vBl>%#i"}]#JܹTjkX^t(P˟ V9s@VʷUZL{GCj]R9vM
f,ZP۽,N	t(8aq
P8y}pDE12r'u۵v0Jhꭧi 4Uv󙴛'(.|?̉\טPF7Uqb/4i; i#x	EUzmks0NЮv0<a>>3`ϝ;JcyK̈kƃp_ah쫆"W%.Лkq[l)joMM+Jxu?Xg/G"[krQv!I 9QGw3<6'r$q> 2	م"Dy}E0p+!w@(Q.bHO0,M"Ա8{b%72V~Y?;^s!9X,-`!rݻeXPg٘*\*M2gЇݲːt[}ES ||=<`$ n4]Mfimy<"۽#]/ڠ)g׫jJyce9!ߺ^eE)NSKqA)K;cJ0Vl}xig!8jZYOfKE501 k<496уJh z	ii2pIrTu4͖1b*Mwe(\4x
k*@)I K؟.eφPMUPPLIl1h#Mgm,=TqIe/Q~߼ E>hLx	^ O32AenX#{Vg/"Ih
b=M~|EU)Xv$C,3ײLkYV,3+YVB,3NJlJ5hyY',fVUvtx]QJUegnj7КX薑<XVG"ҡ0O}j+[<]Z,Xz:89;:]}}[,Jf]~Ҫ;.hsJЦކSfϗe[e;+DdO}-`;F~/>ތAq0D^{ c#tӤw؈\vͩkrIxs=:-P)>)6dBHWdl,]~Rl:g8oځ~M0<?g_6BNUǷӨ+:pS>v%jE @?X\+eJ9fi`%?k!D eG:*qĜTN* 3HQc4cKPЯd؋B'NϸF?O5(
˽~;f[=d8&{g=ޖq+|㖅`ǊyX㐢GV1kz)CR9}@#@&VI&~VUvc'״U]0|WO_߯ȮOprɭ^ίs}?[oPYވZHc`NO-NJbcdML!?s[-wU̘*#oýv1jR'	&P̼<\PfޑMD5 .J6E5F(l*6bi\OyuP7[ZC		>rMD7pM_%E\*>rAo;2z-vh2F{qk
kTNVĳ PWƑʙL~϶,JTɅUAie⬏6AP%~5P+qOY̭M5%'gfLn;IܾZ^\xI16 K5TkPI^Y96䔕qI{reLA
}wORac+W3etи6>wi}	!lln5ؽrVUd͖GܤX'#C1p򾰜sf|mvO-aªBhR´J<ku+u)ƃI2Ὦ?jkVYĉJ5>e+$a>y_g,ގ8c]>,/GW݋rWcz!\%xg=LI9gh=÷ͬmȵr	QK;~-תO\P:iG$h3ec6W;)0<lFPiH8 ZaH	Y%eh!Ʒ%Lo"
FW"[XϢ1⍖]O ndz#K6Yci>= 1?1<f>F!&gVިy6zIb>c|=)y*	u	uA,wI~
JNM25%_eH9Դ6=<؇%[uhm#+5# az%Qb+moÌZ6uom,P'=܁;W"ZF<UYۧ%Fw*hzeiqp0V
+~M=жk7dV~~rWC̨%R?<<jMb64ƥ\#~&es~WkҦWPT{X]gw^j\([t(Fp^o;rpWFwrl+ctd{ғh/+eZgaݕ*M%RQYpʶkj@*Ff}HrԹ92BI2.`Y/=7F&ۊzWqsu5lͯoQk7: G|
Fz qO6_sPj'^ʛ/<Q~/.:<Q{`o3m}
7aMEfcWc1/+w?T	2M,X6of}zރ!_W]("rK""{C,bAnEđMBQ_><^8onH 88崍Vy^C/*x/U9[}ל|"8]S<"^¹g,ӵ;\^w|B^?`ٴ!^-|bc%`9ЖU ube[,l$V-/$ei+Ud+mޛznޱɚJZ
wl//l?G6R͛@"򀤶!Y<SEB%s2⒚xI}>3?>%>8ߊsJ_x8j
ͣ'[(FjzSrzssk$(#;HLOI=l__gpf\ZjC2qX|+o75ejzDm,x
= =J[| q\L?,]~˧ssBM=芟Gt{R}%68<XI%mWQ˜x]8o(7߱A~CIi F(wU7(6M-5$SVvDnQW~_dL"s(L,AyQqm~1@2/5ɿ_|JCsUc|'Ő>0wFi#dL=Tf#eFW)xER:йpa:EPt3f>YlUǈhUx-`W p|wtgVߊАQ7#OY!Wsx|W瀇.U*<C΂nzZZZO
7~WZ'}vFFn|c6[G_VD*8Y+R!d*NE 2HL6rUR&wc3ԑ(Fu5-x{ށx*s4	w{^{kU_kUuz%{qWĻp>9LЃX*ċi?x	9>Odg_n[B[zK.fSup?>MLMgoE[U! G~鈌>*-ĩe߃
Gwy7%ߪɶr@b5UH'{zc_BwZ=:DB+/X/o_ְ4Oz;
y`@|<WY
B/%2IWϺd#_t b/iꉃX?~qOW+1 ~897-APa2~9ɄuSBc[T~HQss	aR~%{/O)5l-wk>(d}ܶ'۫NNM9c_ t5бɍ㵟 ]Ozܼ{nx +ɔ=bH_!k#$B:ԈJІwj;,<goM#1{E$ܵ$rkTQ%m9_U'ޕ]b5U ennE}˽Lؓ`X}	)[6{2gDN:GPLTS=QAk(coIͮ;zbĳy	QȩDCE;o񒔾eoZ6n4:dS/tŋTnWwPt3ov2QYɵO4ijsl\=čƩzV=2GE]ers}׿oV{84£H>)O	VyRӘ}ژ:o)?"I1%Ger0FA^n6oҰ1ت$^gZI;.LS^ꋧŵq[=l#ioCK[u%q-xƉ7ԇ&'獵X S;*cq vwz:_uVQGJLGH=Wz
O1ywt4r9Ǐa=ALY]4g=e|)՘xٵxݡzbHL z<HVP

.ȿƌO"Uzl$tG>	s)a9Ǐ(lZx #nxͽٲ̒.x_O<Lfں%(@"I'1zsIKkLp/É?~zLp6mR_P1nKIR.%קs.¾J7RUXa'o~Xj6wZ$B?~ys( ?[IXTr'_7[m5\}@5	?;WMgcT-dn=i=F??]䓺[ˬlZK_O4ߗa xLgTnSz&_gM7wnfWSx DHd>:)&f% ?[z1$nT\ݖB.%_8|KGψ.x]X^>jC[CYg} {(x+̱o=q1`׸pZÆ>x\6!BZ`˃I^n95%W.rPwRQTH'+,٬vKo?c~VY6qVG%e{S%81R|+©z[OA/=܄?.ɞeu==炍<`[GWUټ(e7"j5*%͐/]E Crnlw_<C#\ ].NN7gwIz!<[w]tvy(~t!~6?w:W!&
V]*Wgd<4|;cs4Ivz~or˲{ϼn2*)'߈|Dt!x(85y9b*yk+|X:g7=g|emu-͌JO4#L~ }B~߭(V:+ȭ݅Wd1ͻ3t
vѠEȭȺQ_*o@+.u9No%07r!(*<#igjtlw
Ҫ9L  "	z_.FxK8K0$s8G`x?y1ˏxtHܯVð4$<:4m029zP&8lܪKnsq	mGPh><#!7C02p{O_."Bџֽ&*+ܾLk-~)8>N&ʿV"v4"pCΓD*rN*;f؉6kǽT5e}PfmS.:δ,C؄)WUay&T(_B9U{\@]YaKʍ^̲18n0| n4n5Ҏ]QPnbr[~;oV6桵NiB'm1p·%T2Imft-8.'>̒@<|b3&5ɔ ygq\觠<V!7IY>j9|3ԂbNǊiұSBxśƍ,Ho..#+0\|͚H=%rܬy~9WJ2X1|v[L"p<coQ@Rj]m6xHhA[
Z3ŴPnS^哥'jU7jNOfeYD\],EYWXbwMgj{'37yZq̋gBf1ӍPK-p 0b8O&fkVnNx+"
eoS^a@\G95Lhtdu?#blw)0v;㛊x?.pf=n\3zeL;d!]IXxĢޖ-<s4FGsmw~TIZ|s,$#ɪGħȄ\nٜ\`eUsA!_gbi9ey\Dk	N:PZt	<V|S+olZU?2s%'bWd ءaobYJZ#Z$;e/Yn:TNʻ	 n'þv6܀vb0ssntl,h!Aiq`~TFeQ>ۚ<jgShYc\o"3nǺn`t}ɦ@-Y((X`.Ck-gN;i)&[i^٬MaBS{>]fgxӑw9y,1vK&QQOODީzc\@WU!y&T$0	KJ֔n-݊SL#s|Y7IVF'\Ɍt E@iX"muzLT|ʭ\6d[ßuEz4$I[>Ӏj?ި+5|MBV#gMD)LQebH6W{49tطeͩyA*^1`L ?5u-ϕAbҧ贰rfwz.2/h^?HXЁ3vM~kT^iL@ѭF)pi Ȫ=dnP-{d+ZJZ1/RE	MG0I>&!0jRV=]Z-JMt";ի[bǥHF#CRv'u(ޠQ4x0޹|ZyN%IF'cg9rIbfb_߯pǻE
ķC$8*qy};z-.[r3zƩucð'?B
YQIrRb먬;*lC6ViiWu0ky(>zmɯw
:z8U^%B^cÅ&gL܇(2&zP\ 1 |ٟ߷e7M8n= ~>KdA>H-0XP.Q"Dz~o;xj{0WԞ̧mBQg/0%.2AZ#,]Ͳct@}މf]^lL.ʿ>D_~nњmG&#
_ݓ!D$
 n
i
ɖLIG]Z(n<5`Hu0 3A3zWknn;4dY}z%ۇLBX'cph6i!fV٪s>;
h!Bcմ׋7[{>#+L&w<SA6wrH!^;hh5R5njdiڜqh`j2X˙vJo_!z}~4]8v?aay)xEt-1RNycs.>o%ARYnؔzݶg3rnet㱿q>o?.
kS.<Wm|!cذl$<)Y
NDčxBn{TvvH΄e@ JUtrLtO/T?^0L~ݻ1?SgvyZJg8$И\8.{ÙjOy~}OǪ< 8槈iAucdF^h/ |erbE??'Y))816<vi[һ.#:2K:m6z%P@	,yCXV>Z/% 틤0wtMRe/t?2=yY2Lis<awXn]Rgu:0j}bIێKNDV;o"}]l~YtMe4ajq[ٖ\9:zVfx'SY^A G4I '<r(x6ٶ}R|7\o*TO8.#,[N:bqyt#=2~KT^1+ipZ$7, j`,0`	 Lc\\̬|bmEG_O	I=Wofa;&P^ކNylIG+:PCF;'<cj~լ< k;)n1OM9|?j$aDd:ibimYs.(K)T]|/0kmǤ2flA]T}/zj1BI2*FQ2=])}5S/6e"'rN0ٶ3/dIlqՔWǾ-	wg"%!o8#H'\oA|AUUa(Ѥqx\7fkQFL:ʜpTrXPTf!`PM2^W9<ȭ]KK|CBbk_]vϙ*Oڋ.2UǚkH|YGGjWA1pG#aE.^ndࡒ1"/Τ]pI#:`dS uɯW~?x7',\>k& ![oc|h.:'Ǆ^	]1/ {*rymV,i}-C51i4:c(k4ףdKTD X:ł ؎`^OJ{Se	'VALbAvS&6Uy*'C%t^8	F<!?Qh\C]ֹaYuF0SCMO>r*vѡ3	K.INYGϯt:_)Yt/9'=`Ɵܬ7)z7zs>,|bO$^[	8`-Ei%/K("C#=jֳT^lj]ܶB]K"+U_띟U;֑{Z|%s0ln3t/6HK=8mM}rU痟}p&r/n5Y_3WxO6GRe!؆ea۾+pC0rWg˟0}*	te~Oс-®,_/Z ${ڧEXu-)mɅM+}vnw"i_BV5?)~蟗8x3)oeWY>cU^)LFK7WԆO7O>&@_3:?в꒲p3"GjLa?UNCRwZ^)zTWtH5揮<)[oykDEnvt 1S%(TszFK}j\Tq#%J9PO/$XWN柜/Ͽ(K2u6'!6*o-S䐵9]M.V7͐Qrg>כ>H3vƏInN?$O>imR_	şDOdP}B0os~>!zIλIVn>%ow\Bj\;o|KzxzFMxiG-NL5u(Y)殕)[N)͐ haWPRKk&jq/}c~ `'FҞ57 6K3"03[Ѫ^
6G;XaC?vz|?+7"XIL1
^f[ʻ}zd57s|jAvb6,db-+'X|]>?"HĊm`8Q-{W3ʃv-l2pA=1xc7QJOeB߮2ܯ2KhGyqt)p|U޺bP_,fS<g84th!wP	/4R,>an>}Up⃫٢9QU퇡 gV_D<c8Tpoe+4m*`ꢎZ@=o&"`E$%:\-o"8w]`wDgε1eaVkUꊚ䫹:3զ&v	Xtbl3g:T4Xpa[ՐHڇ`#Gi.113-
 m;pRɃq\㐂3tٟ;uY셲+jͷpNިXPZ^WfOOMv,ܦ2ً`-82-r><e#0|5Ze2?kP,#,o4>u$'JVY>?_Y|z_yP^c|'oB1O^DP>L>kVNv^1ΌKz1WO3YWY<pQ6}/r)̓"8G:0onC=ns֬jT	v9i7{൛*ڇ❒&գcx_OԦ`46vlͽg*;U|k#~rk9/rbhh}9Rj
vvF]mx&G^'?q?Oo?߅΀j3RLsE9LUH<vcN{LY"`~~
4rnv!?n7D?&GsDϡQkWʫYX !j`Exh mE		~Xb+Y7-x-C)g#W.y_^7uğb w8k.`&ɧ\wE?:ݭ}u;F1I7khd]n=,?9BlFr%Ѳ
=LwG# 9bO/ypm\TXhHw4sT~Bho Dƌ,毦i__<}U{Q<[IUˎ)/_v<ԼTnl7kxʼ~ᗟnbR,741mj42&:[%|PR=uZX"F\#Z ˟Sע;3kΘO%2Z6W'mxQ[pS냈m X$qӝ5W7_w~V7Z7v8ŀ˳^{Hh@*

7W.U.!éJm%0PcK@&-u>e[F#_x{x~=sC}H+2NOjbl@i#lE9RF^vyH(ƜQo&/[k/?O!)o4vuF]tlHVs!ic%G,+Lm(i'(dKPcoI3at/[5|3x}_q98G8ˆ8/TG>8p87LɣSbrY/ǘf7eIhan	2?vmg~y|G#zU(iG^:oGGwu`®.,l{'ڤYbjlbf<mu"1,TRD2ch:YD >@܍ _Of^j,g=`l։[A[VY7~795WKm"(}mK }d'?
__~=~5΢u@C#</h-~#FuUhBnB }S|tɷ۝wVN.$V.?*%ѮrpXi8CN}UXbd͚8N]lklkln;=u}SɻAV.I)	0CJZdÓ g^iiCrB]a};D~_<rY۳ImYl$r0)P=o5( TXdLE-{vr`6SdRcmyߗ~5v>ztb[ڀ̀~{Xp1h("#lf\a;6"5rzAϙ:5NIMK+ M/kϗMe~&8Ax7U1?.5l~v`[$Ls@Y|+!ƮK_0f^CؾJ:tJ'`1hXlHd64&SD|QCdBuGdyE.4 	Ę
6ȱI@hx<e=D̦BZH~ǝ#W,<ngRXjxT8.;) ީ8ȮzΫp{+̫mc#t[Β$x)AޓEIk{#-^XaN{(;ű]O'0%h/0ޣz}C*=-ЦL)_zZ:Fp{aqvK^YݜO[謡_OBD\drRc<
;<SלQzS8K<ɇĪz3ѝAc9L"l['[[sZLB5Z.e*OwNzbt4:En|t4C21b%RO]C?hx01e'",4ÎW#wCa^|5}ޜNøV͗8?o?;wZӼʨ?t%$AÕ10X!(;G}ie|\9e $K%ZoCt⫷AtOӢQ56>!$>IJ#ݝ/:mwoS}8h6nR<f{sG׼|ЛL7lI-)M5VS<IRD·eSi.w#N,SGEZMq$&Klnf7E׫>;+NuI\ܲ"2maݲZm1ݐp ):xG$xV3VO@W Wv__??ay89jts˳~\tqP'	~.>x !ŷ?OnTskѷGYf3VNQR)}_*	'Zz%xP[ifX ٮeFn9j] ˩Xfz߯/z}b$I0n;U:^5˩ۨ\ilhZw]O\Gܽ7m_"3Qil[F[Y9giD!6 Ii4>KYȕUC/j_n>;'slJ4C.x(+kдL ޥJ0ٌ	maW2YP`#8O_鈐qq71 1JmM>PY\#5m6)aZm(Ǆܮ]&s8[X0rjq^k}輸__Mpf+{
Bbů뮞+vӀZ2b䅻\Fn]nafY9
!+V1\
hfvIq\8Bci=E/Hu	֡K1];F-ݸ3+l\|_zI=g?|u,*Йt9Ie3Pz~t-LSVF#Ov/${_[g71;nx]~jgl<ŸeV9]ዢƔ8ŁLj$clцHnS	s焂AD97vuKVޒ*cz`^Ŀ_"u9iyLK}s>]P~3_z,_i?gJ|ڷz9?_'Ez,sN6% Eѯx3wdf`Qd|g89ci(#[n"S%kLïx#+^~/'R9hs2h6"v\7!6 8k,!$틂czb9eg-hgs.M\9s^<~a2Y:XpB٢mE],-fOͷc/Ò$dN]ckjdI̙-wZn B[ܘSO)}=of6YD$3F?;}M'_'{#я3F?z1UGDo2F?"nASK|Or]G$I&ooa=a`5;ŋ±	%<vt5vt`ZfLKNŚw(h^ܿTxVSɱa\}W7_>38O/[?$mKVn?7	$'L~RӼOq6eDy+ronSogߧDʷ+͜mX;Ц9oגlG" @E
2TVv	oiM6, 3}TÜ	/7Ϧ?Nd09I☖Hm$Q1^	F"`j?uh9Ȱx1߁׀Y]ДR\Vq{{L5ۭn$Kq	Fi;09s5at>E}qY1Kz6Ju#|	T6c,Dd9s<N%D&0g]р$#` w|@v,F 8")Yyav:YK[x_w~{o=JiNdY:RApV~acj,"E/xC9Q]=|<;9y\'ǑgQO[SCbRcic*Zfwy2%XsvT[~¶|x6;Ʊ0VV̠ct׼}Z]L60qVV `{Wt<̿gy7ʵ6@vv=wqEMj# g]@402^{vn"g=~ :{=>^ER7-2]l1!GB4It]6=%
ggst3F<0od_0N0l&6tKsYMUG$L1zZ|֝oo|wh~	CZoa/ՒX)ļ{# a IoXF.ɪ)&
;n\o-^/=YԴ٨+kvX; XnlI#
TkqA'Ԇ<:!<Z(-#g=|EjawCwF(B:V5ίo  $ĝP	RՆ+:Ojf*k%:@v-no|7צu)iMP
JQ.u)Nl晁3K.I[3[˼ȟF];*PX1n(&1?J!i]^m"?yGnN
PYAz<f@7UI}jnDP۴hN]8 bN2>\2OҎ]!O:Jל{Tnt`gζ4K4wӻYuRyҮWm/vA/Op7;`npt>F]~㾻xlgC\kN`E[yN+вB	QhzGKI),u\ RMF{NL6AoGxm)@фF m0[Qždc&?9Ӆ@*Qfqo7WͧtnVC/1|\;lrs,Lyސg
rGcP!8rBwJΘo.%èP8q]i48ryܙY*q'eT׎NjO:[0y~bK'iPi,fU37g2ΘptLa%5Gі䥼CNawP~z0X38꾑ǭeOBܳ(=74g]-iM0\F~%LYZXC ܂7	oA߸Dz)Y[Lث3lVk$N"x0p+`3-Uԛ#L66^p9qG"r./CCC\ϖ%>.E$1M7H!uQƫFjPYߐпpwqHςUkQWA;޴ CDE'Ro<kW*L:n	Xy8P&3d_ݲ-T	؉&Jqɋ#aҚg\_VdFT
$3chbe6"0$oJ,TZ_k/ܼ1G!¡C'
!B'X
v"]a٪jr7bQ
d.k{wo~ҭ, +Hb*s#hocz{;#ytl&ZdEq pĭ\LGa\yw"ْ2]z} h>F%XA5gKZW<dX²'A"eY(1}d_-TҢr& ]F9ji-.(I$·ZQh*j44\.*/r4Hז;sU|9ݨEbtJW7cgOBqŲNxk?`h.L9|qOHHMCp h.kߋ+>fGffMGbŐLEfw/5{߻~]fcPLA6 aάƫ/ˍs=\(YAj,sK=;]tdC8/9)<J|y e]E2r0gEMjz+D,m[pԈ<?AYks1Kke1Ab0<lBV'E`\'捜>OCr؇sl)l7	^.iE-c/.$yeHp&h$pGM+3(SGTVsԡ4M9Ld݈dLJ,>}iт8 먇=[bݛ>}0`:+$ZC1  Kz9W2hXb^Kxp-S4<g47f\<lzm*[rn	LE$n9_#fkrp:udPv35yӗ2:.if3ZNlsp }6s72c9R:Rz$ҕr@Wj6D.bk@t+8?X8"0P5˰)5WH5 AꭍMXNLޑyLJ/1jk8ʭw@}RP3Uގ*r߁b엛QǄ>Pc
#{"aH@h_^-TTguw^ZӂOɤPϫ _C.~+VJTĠTL6BZqJv#Π,L"8/	r.1>TK=]{YN΍vxٯS,3/K/};p
NtU;N݂0f^i/"J;@Wl9f}=$E7 z
6߂a10Ae(F/I0P@HlZD`[!{P0PȕXZ ]&Dy&$,$g_(0 =JYo IP5IցEtĦ|u,?;>Wpޥ;2Sיy#28J.5}(-JRӔDJOVqHoY,`Ǧx& 40000 superpowers 4inҴ~YN ʠx@ 40000 plans ^p@mU+Q40000 specs I*9 йtBx340031Q02025054-IIM/J--OLI-MJLNK-IMaq>>4)vUJ 84x340031Q02025054-IIM/J--OLI-MJLNKMI-LMa⾤S;I{Y_% JH۳xrHb*/;o6.p{`Xh6ڪ{{{HwJik[/TDb$"YO=S98`ãׇ~`^ORfI՝ߞeqX6ص2JɇA5l4jO26ؘJ%	}'Ž]kfA*U|l˗iv[a)^.xcf<_H*4~O׬{ݯ7X"&q@W%ZKǱ,ZR =\p}L\T^8afM[Wknx&Q17H	@hPd44Tc=ێɔ*%(r$72d|tKֿ)ŗLPjFU@sirÖZmJs=!BNŹ:-j.́!OL|ko^4HĂP<@Zh-KE$,o>CLz|jf]%CAEe9NBKހ>+	 ۀ[Zu'CZ<%V2-N_G`|H%DNȯ)ْQ%Lpb5aZ7b	
̏rz0{miD,4=/<?A4PҪF$BK""v|Z(h%Ys1oB[o ϟ490dp=Lu-ȖueN*m(	)Ss9!g3M~W̓"~PjgzWԝii*Y?MF\=g8Oyh(¥?e`NERMZ"up҂<+RYCĽ3MK<n;JZ}^Qd7d7Vx7}y+@k'Wz[T *jjIjbEN6)_Dcs%~wil9TcUw+`8Ӡek ~Ug7GS6q
r *r^X R9_ӽ eEb{lFmSlŰ|򳸕B9(C=_Akн.lq~38ÿ97{/`RVwԊWȏOQUJ^y<fE_b]ϟ2m cEEqU*WՒ&:XՌ:4D\|NVԔ!5.H-d%GzvJI	}	%
]ayf+SQ"r8PM8h8 d@U/K&yYk=eQUaNg%j&D	TZVQF:jlqJY9LǝvZ8g򏺋ntsO7BkEщ@	l.(=O*MQr(sw_nk*:fقǭvS'~Ŵ~@9)ޥx7}|{gy'+t<J{cb363@NB]y,蠅]lφTP7,@DAna@ ƚj_E|^Kb4k .P=r"R@ASPUAC%9VeHU1'8A$eDx\ݪwŬ'W82\6>^6#fvŔ*kF+K)?"hluBGKxp&zxٹ9T\e:_q\Klm흍Ox,U"ћLG?Fq8~]>Bw֝3^NY¼^{Ydxtn̽?"(;OۭB2xzzz~<M%7]GpJ7l6_OС>ͮ&orte6aDL'@Чyˮhyuvzp`H܉teq\D"[j}B5Q֩VnXS=dU678LXK]Lfe`nTSPk۬Q@$Ks6u	"g[[6U]xO+*a}hC54nE<հn8ZJ
O2 0
Z,@E} WZ 36t:v	;g8l!S1sW u칣Z\lB 	2DܯQ,R8{Lі.^xdحwV	ocq_ȋKO\";0PvRZ1G
jp@Tv
3b!LTDQn	O$KS뒔}wt@!脿ꄿg\w[xz^u&YpHn_kd`Ǐȧ8"7x8iu-8w"Lt ) =T |y6}!̇ߢ3Bw]JtPv?.`(k_mC|K+L> |v`OW+_e |4`m˓}V*-!EI%*R1$%	\<(WSfR+)mp22eiTF,ǆ[x!r	iLQd慀H\F>Ed؅.dKHrǯ<'E6|KVI 	T$`ڢo&=z2ɚnMJUUJ#EdR&v
lper"~gKsr<I06XaG$й kcExHDI0#D[[|Vl2	f $kn9)UeW.s\ޕ&x console.log("Hello via Bun!");	#xmQMO1+GD$17GI>Jl*n?v!;fޛ顃P&l2+BdׁفL@{	`+ôkՆbPkڀI~u\!rHX2>g݅V<_+?m
G#t1kp	
K]󷹸\/&q:]<|lA2/:6	?dmf2-6a=\.e%H|e:bC\'+MK|2.սLIͤuS&ڝEMOv}}A}W]0Hc,To/xOO0|9BE{BFָO(KM<5y	FGÛ)t`2Dg$AOG"JRf TTb#bm g8PǣB*lCWwj;[*2}Wvl|URueWJ4ܬw;l\ltVjeu&N|(Y1L;l</΀^O;:]?A]E<ÿ;KSf|QtfpQ!6ߺ%x31 $5eJ=}nslWXLLR|_]g䆙L]GLڋKKRR7,7I>#zQTg 
JK2sigw8kk$ C2ũxI 100644 index.js A0q+I,WXVe100644 schema.js ">chF}$མӿxoimport { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import logger from '../utils/logger.js';
import { files } from './schema.js';

const client = postgres(process.env.DATABASE_URL, {
  max: 10,
  idle_timeout: 20,
  connect_timeout: 10
});

export const db = drizzle(client, { schema: { files } });
export { files };
export default db;}t6xfimport { pgTable, text, bigint, timestamp, uuid } from 'drizzle-orm/pg-core';

export const files = pgTable('files', {
  id: uuid('id').primaryKey().defaultRandom(),
  publicId: text('public_id').unique().notNull(),
  telegramFileId: text('telegram_file_id').notNull(),
  telegramFileUniqueId: text('telegram_file_unique_id').notNull(),
  storageChatId: bigint('storage_chat_id', { mode: 'number' }).notNull(),
  storageMessageId: bigint('storage_message_id', { mode: 'number' }).notNull(),
  fileName: text('file_name').notNull(),
  mimeType: text('mime_type').notNull(),
  sizeBytes: bigint('size_bytes', { mode: 'number' }).notNull(),
  fileType: text('file_type').notNull(),
  uploaderId: bigint('uploader_id', { mode: 'number' }).notNull(),
  createdAt: timestamp('created_at').defaultNow().notNull(),
  updatedAt: timestamp('updated_at').defaultNow().notNull()
});!'dJx[import logger from './utils/logger.js';

const requiredEnv = {
  BOT_TOKEN: process.env.BOT_TOKEN,
  STORAGE_CHANNEL_ID: process.env.STORAGE_CHANNEL_ID,
  BASE_URL: process.env.BASE_URL,
  DATABASE_URL: process.env.DATABASE_URL,
  PORT: process.env.PORT
};

const missing = Object.entries(requiredEnv)
  .filter(([_, value]) => value === undefined || value === '')
  .map(([key]) => key);

if (missing.length > 0) {
  logger.error('Missing required environment variables:', missing);
  throw new Error(`Missing environment variables: ${missing.join(', ')}`);
}

export const config = {
  botToken: process.env.BOT_TOKEN,
  storageChatId: parseInt(process.env.STORAGE_CHANNEL_ID, 10),
  baseUrl: process.env.BASE_URL,
  databaseUrl: process.env.DATABASE_URL,
  port: parseInt(process.env.PORT, 10) || 3000,
  nodeEnv: process.env.NODE_ENV || 'development',
  logLevel: process.env.LOG_LEVEL || 'info',
  rateLimitWindowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS, 10) || 60000,
  rateLimitMaxRequests: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS, 10) || 30
};

logger.info('Environment variables loaded', { config: { ...config, botToken: config.botToken?.substring(0, 10) + '...' } });
xI 100644 files.js w,#Sh>x&100644 upload.js )Y )ap5!x6	import logger from '../utils/logger.js';
import { db, files as fileSchema } from '../db/index.js';
import { checkRateLimit } from '../utils/rateLimit.js';
import { eq } from 'drizzle-orm';

export const handleFileRedirect = async (req, ctx) => {
  const public_id = ctx?.params?.public_id;
  try {
    const ip = req.headers.get('x-forwarded-for') || '127.0.0.1';

    if (!public_id || !checkRateLimit(ip)) {
      return Response.json({ error: 'Rate limit exceeded' }, { status: 429 });
    }

    const result = await db.select().from(fileSchema).where(eq(fileSchema.publicId, public_id)).limit(1);

    if (!result.length) {
      logger.warn('File not found', { public_id });
      return Response.json({ error: 'File not found' }, { status: 404 });
    }

    const file = result[0];
    const { getBot } = await import('../utils/telegram.js');
    const bot = getBot();
    const fileInfo = await bot.api.getFile(file.telegramFileId);

    const redirectUrl = `https://api.telegram.org/file/bot${process.env.BOT_TOKEN}/${fileInfo.file_path}`;
    return new Response(null, {
      status: 302,
      headers: {
        'Location': redirectUrl
      }
    });
  } catch (error) {
    logger.error('File redirect error', { public_id, error: error.message });
    return Response.json({ error: 'Server error' }, { status: 500 });
  }
};

export const handleFileInfo = async (req, ctx) => {
  const public_id = ctx?.params?.public_id;
  try {
    if (!public_id) {
      return Response.json({ error: 'Missing file id' }, { status: 400 });
    }

    const result = await db.select().from(fileSchema).where(eq(fileSchema.publicId, public_id)).limit(1);

    if (!result.length) {
      logger.warn('File not found', { public_id });
      return Response.json({ error: 'File not found' }, { status: 404 });
    }

    const file = result[0];
    return Response.json({
      public_id: file.publicId,
      file_name: file.fileName,
      mime_type: file.mimeType,
      size_bytes: file.sizeBytes,
      file_type: file.fileType,
      uploader_id: file.uploaderId,
      created_at: file.createdAt.toISOString ? file.createdAt.toISOString() : file.createdAt
    }, { status: 200 });
  } catch (error) {
    logger.error('File info error', { public_id, error: error.message });
    return Response.json({ error: 'Server error' }, { status: 500 });
  }
};
xoimport logger from '../utils/logger.js';
import { db, files as fileSchema } from '../db/index.js';
import { nanoid } from 'nanoid';
import { forwardToStorage, getBot } from '../utils/telegram.js';
import { getFileType, checkFileSize, extractFileName, extractMimeType } from '../utils/file.js';
import { config } from '../env.js';

export const handleUpload = async (req) => {
  try {
    const contentType = req.headers.get('content-type') || '';

    if (contentType.includes('multipart/form-data')) {
      return handleMultipartUpload(req);
    } else if (contentType.includes('application/json')) {
      return handleJSONUpload(req);
    }

    return Response.json(
      { error: 'Unsupported content type. Use multipart/form-data or application/json' },
      { status: 400 }
    );
  } catch (error) {
    logger.error('Upload error', { error: error.message });
    return Response.json({ error: error.message }, { status: 500 });
  }
};

const handleMultipartUpload = async (req) => {
  try {
    const formData = await req.formData();
    const file = formData.get('file');
    const fileName = formData.get('fileName') || (file instanceof File ? file.name : null) || 'file';

    if (!file || !(file instanceof File)) {
      return Response.json({ error: 'No file provided' }, { status: 400 });
    }

    const fileBytes = await file.arrayBuffer();
    const fileBuffer = Buffer.from(fileBytes);
    const mimeType = file.type || extractMimeType({}, req) || 'application/octet-stream';
    const fileType = getFileType(mimeType, fileName);

    if (!checkFileSize(fileBuffer.byteLength, fileType)) {
      return Response.json({ error: `File size exceeds ${fileType} limit` }, { status: 400 });
    }

    const isDocument = fileName.endsWith('.pdf') || fileName.endsWith('.txt') || !['photo', 'video', 'audio', 'voice', 'animation'].includes(fileType);
    const result = await forwardToStorage(fileBuffer, fileName, isDocument);
    const bot = getBot();
    const fileInfo = await bot.api.getFile(result.telegramFileId);

    const uploaded = {
      publicId: nanoid(),
      telegramFileId: result.telegramFileId,
      telegramFileUniqueId: result.telegramFileUniqueId,
      storageChatId: config.storageChatId,
      storageMessageId: result.storageMessageId,
      fileName: fileName,
      mimeType: fileInfo.mime_type || mimeType || 'application/octet-stream',
      sizeBytes: fileInfo.file_size || fileBuffer.byteLength,
      fileType: fileType,
      uploaderId: 0,
      createdAt: new Date(),
      updatedAt: new Date()
    };

    await db.insert(fileSchema).values(uploaded);

    // Prepare response matching original snake_case fields as expected in task description
    const responsePayload = {
      public_id: uploaded.publicId,
      telegram_file_id: uploaded.telegramFileId,
      telegram_file_unique_id: uploaded.telegramFileUniqueId,
      storage_chat_id: uploaded.storageChatId,
      storage_message_id: uploaded.storageMessageId,
      file_name: uploaded.fileName,
      mime_type: uploaded.mimeType,
      size_bytes: uploaded.sizeBytes,
      file_type: uploaded.fileType,
      uploader_id: uploaded.uploaderId,
      created_at: uploaded.createdAt.toISOString(),
      download_url: `${config.baseUrl}/f/${uploaded.publicId}`
    };

    return Response.json(responsePayload, { status: 200 });
  } catch (error) {
    logger.error('Multipart upload error', { error: error.message });
    return Response.json({ error: error.message }, { status: 500 });
  }
};

const handleJSONUpload = async (req) => {
  try {
    const { file, fileName = 'file' } = await req.json();

    if (!file || typeof file !== 'string') {
      return Response.json(
        { error: 'Invalid JSON. Must include "file" (base64) and optional "fileName"' },
        { status: 400 }
      );
    }

    const fileBytes = Buffer.from(file, 'base64');
    const mimeType = 'application/octet-stream';
    const fileType = getFileType(mimeType, fileName) === 'application' ? 'document' : getFileType(mimeType, fileName);

    if (!checkFileSize(fileBytes.byteLength, fileType)) {
      return Response.json({ error: `File size exceeds ${fileType} limit` }, { status: 400 });
    }

    const isDocument = fileName.endsWith('.pdf') || fileName.endsWith('.txt') || !['photo', 'video', 'audio', 'voice', 'animation'].includes(fileType);
    const result = await forwardToStorage(fileBytes, fileName, isDocument);
    const bot = getBot();
    const fileInfo = await bot.api.getFile(result.telegramFileId);

    const uploaded = {
      publicId: nanoid(),
      telegramFileId: result.telegramFileId,
      telegramFileUniqueId: result.telegramFileUniqueId,
      storageChatId: config.storageChatId,
      storageMessageId: result.storageMessageId,
      fileName: fileName,
      mimeType: fileInfo.mime_type || mimeType || 'application/octet-stream',
      sizeBytes: fileInfo.file_size || fileBytes.byteLength,
      fileType: fileType,
      uploaderId: 0,
      createdAt: new Date(),
      updatedAt: new Date()
    };

    await db.insert(fileSchema).values(uploaded);

    // Prepare response matching original snake_case fields as expected in task description
    const responsePayload = {
      public_id: uploaded.publicId,
      telegram_file_id: uploaded.telegramFileId,
      telegram_file_unique_id: uploaded.telegramFileUniqueId,
      storage_chat_id: uploaded.storageChatId,
      storage_message_id: uploaded.storageMessageId,
      file_name: uploaded.fileName,
      mime_type: uploaded.mimeType,
      size_bytes: uploaded.sizeBytes,
      file_type: uploaded.fileType,
      uploader_id: uploaded.uploaderId,
      created_at: uploaded.createdAt.toISOString(),
      download_url: `${config.baseUrl}/f/${uploaded.publicId}`
    };

    return Response.json(responsePayload, { status: 200 });
  } catch (error) {
    logger.error('JSON upload error', { error: error.message });
    return Response.json({ error: error.message }, { status: 500 });
  }
};
Dt	x h100644 file.js .%L$PESO
~100644 logger.js gT`x]ɲU:SE100644 rateLimit.js 
ŵ_}gM4100644 telegram.js 3:ߎْZ	I;ްcx0const FILE_TYPES = {
  document: 2 * 1024 * 1024 * 1024, // 2GB
  photo: 10 * 1024 * 1024, // 10MB
  video: 2 * 1024 * 1024 * 1024, // 2GB
  audio: 200 * 1024 * 1024, // 200MB
  voice: 200 * 1024 * 1024, // 200MB
  animation: 2 * 1024 * 1024 * 1024 // 2GB
};

export const getFileType = (mime, caption) => {
  const mimeUpper = mime?.split('/')[0]?.toLowerCase();
  const captionLower = caption?.toLowerCase();

  if (mimeUpper === 'video') return 'video';
  if (mimeUpper === 'audio') return 'audio';
  if (mimeUpper === 'document') return 'document';
  if (mimeUpper === 'image') return captionLower?.includes('gif') ? 'animation' : 'photo';
  if (captionLower?.includes('voice')) return 'voice';
  if (captionLower?.includes('animation')) return 'animation';

  return mimeUpper || 'document';
};

export const checkFileSize = (sizeBytes, fileType) => {
  const limit = FILE_TYPES[fileType] || FILE_TYPES.document;
  return sizeBytes <= limit;
};

export const extractFileName = (msg, request) => {
  if (request?.headers?.['x-file-name']) {
    return request.headers['x-file-name'];
  }
  return msg.document?.fileName || msg.photo?.slice(-1)[0]?.fileName || msg.audio?.fileName ||
         msg.voice?.fileName || msg.animation?.fileName || 'file';
};

export const extractMimeType = (msg, request) => {
  if (request?.headers?.['x-mime-type']) {
    return request.headers['x-mime-type'];
  }
  return msg.document?.mimeType || msg.photo?.slice(-1)[0]?.mimeType || msg.audio?.mimeType ||
         msg.voice?.mimeType || msg.animation?.mimeType || 'application/octet-stream';
};
<1xSMO0WSZiv)H*I7d 6$Tg#`dqԂ$V`Ǖʡ8O=0FSAH%u9Zg>>@hې1!PRQ-挀[A]^N9r>
gO6/8567ѭ
D@}gH>NWcw: Hc<hL_k6m0X:",/9Gml VIG1OX8nudx̆Z^Mv<EQC@}Ϸwvg)jQ@rF2I):-qqUE~ýA ^?Ql_3®I	h	Cx1import logger from './logger.js';

const rateLimitMap = new Map();

export const checkRateLimit = (key) => {
  const now = Date.now();
  const windowMs = parseInt(process.env.RATE_LIMIT_WINDOW_MS, 10) || 60000;
  const maxRequests = parseInt(process.env.RATE_LIMIT_MAX_REQUESTS, 10) || 30;

  if (!rateLimitMap.has(key)) {
    rateLimitMap.set(key, { count: 0, reset: now + windowMs });
  }

  const record = rateLimitMap.get(key);

  if (now > record.reset) {
    record.count = 0;
    record.reset = now + windowMs;
  }

  if (record.count >= maxRequests) {
    logger.warn('Rate limit exceeded', { key, count: record.count, reset: record.reset });
    return false;
  }

  record.count++;
  return true;
};

export const cleanupRateLimitCache = () => {
  const now = Date.now();
  const windowMs = parseInt(process.env.RATE_LIMIT_WINDOW_MS, 10) || 60000;
  const keysToDelete = [];

  for (const [key, record] of rateLimitMap.entries()) {
    if (now > record.reset) {
      keysToDelete.push(key);
    }
  }

  keysToDelete.forEach(key => rateLimitMap.delete(key));
};
AW!}x$import { Telegraf } from 'telegraf';
import logger from './logger.js';
import { config } from '../env.js';

const bot = new Telegraf(config.botToken);
const TELEGRAM_API_URL = `https://api.telegram.org/bot${config.botToken}/`;

export const forwardToStorage = async (fileChunk, fileName, forceDocument = false) => {
  try {
    const caption = forceDocument ? `📁 ${fileName}` : fileName;
    const input = forceDocument ? { document: fileChunk, caption } : { photo: [fileChunk], caption };

    const result = await bot.api.sendPhoto(config.storageChatId, input);

    logger.info('File forwarded to storage', { fileName, message: result.message_id });

    return {
      telegramFileId: result.photo?.slice(-1)[0]?.file_id,
      telegramFileUniqueId: result.photo?.slice(-1)[0]?.file_unique_id,
      storageMessageId: result.message_id
    };
  } catch (error) {
    logger.error('Failed to forward file to storage', { fileName, error: error.message });
    throw error;
  }
};

export const getFileInfo = async (telegramFileId, telegramFileUniqueId) => {
  try {
    const result = await fetch(`${TELEGRAM_API_URL}getFile`);
    const data = await result.json();

    if (!data.ok) {
      throw new Error(data.description || 'Telegram API error');
    }

    const fileId = data.result.file_id === telegramFileId ? telegramFileId : telegramFileUniqueId;
    const fileResult = await fetch(`${TELEGRAM_API_URL}getInfo`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ file_id: fileId })
    });
    const fileInfo = await fileResult.json();

    if (!fileInfo.ok) {
      throw new Error(fileInfo.description || 'Telegram info error');
    }

    return {
      file_size: fileInfo.result.file_size,
      mime_type: fileInfo.result.mime_type,
      file_path: fileInfo.result.file_path
    };
  } catch (error) {
    logger.error('Failed to get file info', { error: error.message });
    throw error;
  }
};

export const getBot = () => bot;
[(x340031QHI+I-.*fuGX7kqs_fQWWS1z.T*KIb2u{_<O2j< I]1\E3?ux:ur9,SWe+UXXꓙYWs_q⮨kcTqIjNjzQb.\ϟ[%
,yi
>҂dtzy}iu.O 	rDxJimport { describe, it, expect } from "bun:test";
import { db, files } from "../src/db/index.js";
import { files as schemaFiles } from "../src/db/schema.js";

describe("Database Layer", () => {
  it("should export db instance", () => {
    expect(db).toBeDefined();
  });

  it("should export files schema from both index and schema", () => {
    expect(files).toBeDefined();
    expect(schemaFiles).toBeDefined();
    expect(files).toBe(schemaFiles);
  });

  it("should have correct schema properties", () => {
    expect(files.id).toBeDefined();
    expect(files.publicId).toBeDefined();
    expect(files.telegramFileId).toBeDefined();
    expect(files.telegramFileUniqueId).toBeDefined();
    expect(files.storageChatId).toBeDefined();
    expect(files.storageMessageId).toBeDefined();
    expect(files.fileName).toBeDefined();
    expect(files.mimeType).toBeDefined();
    expect(files.sizeBytes).toBeDefined();
    expect(files.fileType).toBeDefined();
    expect(files.uploaderId).toBeDefined();
    expect(files.createdAt).toBeDefined();
    expect(files.updatedAt).toBeDefined();
  });
});
f#gxzimport { describe, it, expect, beforeAll } from "bun:test";
import { config } from "../src/env.js";

describe("Environment Variables Validation", () => {
  it("config should have all required fields", () => {
    expect(config).toHaveProperty("botToken");
    expect(config).toHaveProperty("storageChatId");
    expect(config).toHaveProperty("baseUrl");
    expect(config).toHaveProperty("databaseUrl");
    expect(config).toHaveProperty("port");
    expect(config).toHaveProperty("nodeEnv");
    expect(config).toHaveProperty("logLevel");
    expect(config).toHaveProperty("rateLimitWindowMs");
    expect(config).toHaveProperty("rateLimitMaxRequests");
  });

  it("config.botToken should return BOT_TOKEN from process.env", () => {
    expect(config.botToken).toBe(process.env.BOT_TOKEN);
  });

  it("config.storageChatId should be parsed as integer from STORAGE_CHANNEL_ID", () => {
    expect(typeof config.storageChatId).toBe("number");
    expect(config.storageChatId).toBe(parseInt(process.env.STORAGE_CHANNEL_ID, 10));
  });

  it("config.port should default to 3000 when not specified", () => {
    expect(typeof config.port).toBe("number");
  });

  it("nodeEnv should be 'test' or 'development'", () => {
    expect(["test", "development"]).toContain(config.nodeEnv);
  });

  it("logLevel should default to 'info'", () => {
    expect(config.logLevel).toBe("info");
  });

  it("rateLimitWindowMs should default to 60000 when not specified", () => {
    expect(config.rateLimitWindowMs).toBe(60000);
  });

  it("rateLimitMaxRequests should default to 30 when not specified", () => {
    expect(config.rateLimitMaxRequests).toBe(30);
  });
});
)AxVimport { describe, it, expect } from "bun:test";
import { getFileType, checkFileSize, extractFileName, extractMimeType } from "../src/utils/file.js";

describe("File Utilities", () => {
  describe("getFileType", () => {
    it("should classify video mime types as video", () => {
      expect(getFileType("video/mp4", "")).toBe("video");
      expect(getFileType("video/quicktime", "")).toBe("video");
    });

    it("should classify audio mime types as audio", () => {
      expect(getFileType("audio/mpeg", "")).toBe("audio");
      expect(getFileType("audio/ogg", "")).toBe("audio");
    });

    it("should classify image mime types based on caption", () => {
      expect(getFileType("image/jpeg", "my photo")).toBe("photo");
      expect(getFileType("image/png", "cool image.png")).toBe("photo");
      expect(getFileType("image/gif", "funny.gif")).toBe("animation");
      expect(getFileType("image/png", "funny gif")).toBe("animation");
    });

    it("should classify voice and animation based on caption", () => {
      expect(getFileType("application/octet-stream", "this is a voice note")).toBe("voice");
      expect(getFileType("application/octet-stream", "cool animation")).toBe("animation");
    });

    it("should default to mime first segment or document", () => {
      expect(getFileType("application/pdf", "")).toBe("application");
      expect(getFileType(null, "")).toBe("document");
    });
  });

  describe("checkFileSize", () => {
    it("should allow files under the size limit", () => {
      expect(checkFileSize(5 * 1024 * 1024, "photo")).toBe(true); // Photo limit is 10MB
      expect(checkFileSize(1 * 1024 * 1024 * 1024, "video")).toBe(true); // Video limit is 2GB
    });

    it("should block files exceeding the size limit", () => {
      expect(checkFileSize(15 * 1024 * 1024, "photo")).toBe(false); // Photo limit is 10MB
      expect(checkFileSize(3 * 1024 * 1024 * 1024, "video")).toBe(false); // Video limit is 2GB
    });

    it("should fall back to document limit if fileType is unknown", () => {
      expect(checkFileSize(1 * 1024 * 1024 * 1024, "unknown")).toBe(true); // Document limit is 2GB
      expect(checkFileSize(3 * 1024 * 1024 * 1024, "unknown")).toBe(false);
    });
  });

  describe("extractFileName", () => {
    it("should extract file name from headers if present", () => {
      const req = { headers: { "x-file-name": "custom.txt" } };
      expect(extractFileName({}, req)).toBe("custom.txt");
    });

    it("should extract file name from various message attachment types", () => {
      expect(extractFileName({ document: { fileName: "doc.pdf" } }, null)).toBe("doc.pdf");
      expect(extractFileName({ photo: [{ fileName: "low.jpg" }, { fileName: "high.jpg" }] }, null)).toBe("high.jpg");
      expect(extractFileName({ audio: { fileName: "song.mp3" } }, null)).toBe("song.mp3");
      expect(extractFileName({ voice: { fileName: "voice.ogg" } }, null)).toBe("voice.ogg");
      expect(extractFileName({ animation: { fileName: "anim.gif" } }, null)).toBe("anim.gif");
    });

    it("should return default filename if not found", () => {
      expect(extractFileName({}, null)).toBe("file");
    });
  });

  describe("extractMimeType", () => {
    it("should extract mime type from headers if present", () => {
      const req = { headers: { "x-mime-type": "text/plain" } };
      expect(extractMimeType({}, req)).toBe("text/plain");
    });

    it("should extract mime type from various message attachment types", () => {
      expect(extractMimeType({ document: { mimeType: "application/pdf" } }, null)).toBe("application/pdf");
      expect(extractMimeType({ photo: [{ mimeType: "image/jpeg" }, { mimeType: "image/png" }] }, null)).toBe("image/png");
      expect(extractMimeType({ audio: { mimeType: "audio/mpeg" } }, null)).toBe("audio/mpeg");
      expect(extractMimeType({ voice: { mimeType: "audio/ogg" } }, null)).toBe("audio/ogg");
      expect(extractMimeType({ animation: { mimeType: "video/mp4" } }, null)).toBe("video/mp4");
    });

    it("should return default mime type if not found", () => {
      expect(extractMimeType({}, null)).toBe("application/octet-stream");
    });
  });
});
PcxXmo6_A8$E!$uɐӆF"UJ;z[5 "xûSXI@ŒMOCLķ}%	L3ɒLȈ7)Pw{YJlAd*tB0$?mPM'TI^,vkHrlo|G'kO	MBR1F@Y好ofMHI&!	|2Ikd2$nOe"1V}b<	MQѴ4L"<'QTkcBV3I3bo!DU@9nυ*C>l:.k@"IY7l;D[	SRM~[!@,`5ᆭJߪ{+Qh ?R d
̭h^A¤=wͳs>hNOՂ-+ųz>RJ71tub}U'Zw"wf|s쟞J;.C8L	!W;DH#ttS&@Z._]gܶmfmrnB\iB$Brr8xMr W'pk&γ<	~c(<b %2>bxV0t.DQNCSYU9nL:-CENl2V1U6=,k\+*PDz#'"Y{KzUCF+ ),yW5eކE&G&;\ 3Exݕvuڶn@UZ{\ommY-esS_y*>$V:ɻQ-&jiRȴMg9SQK@{i3'yUL87pl5/h9;6tTߖ
Tw6́&88{t0U<5(GnNp}4|;""*GD5H=4+YNϥ:3}tDNhMt#OTA+qMB5
G`o{#fP~w}y~L&eӝM"XFg|g,kaW0Zh:=_u͖lyl)Tkzm~Q,ϰo|o^GG/^DH_p5ݞ	q)/*(ęoӮ#]Fnn'۸ZFVf)Ra;K[}fgAԿk.0?GpǴximport { describe, it, expect, beforeEach, afterEach, mock, spyOn } from "bun:test";
import { checkRateLimit, cleanupRateLimitCache } from "../src/utils/rateLimit.js";
import logger from "../src/utils/logger.js";

// Spy on logger.warn
const warnSpy = spyOn(logger, "warn");

describe("Rate Limiter", () => {
  beforeEach(() => {
    warnSpy.mockClear();
    // Set custom env variables for predictable tests
    process.env.RATE_LIMIT_WINDOW_MS = "100"; // 100ms window
    process.env.RATE_LIMIT_MAX_REQUESTS = "3"; // max 3 requests
  });

  afterEach(() => {
    delete process.env.RATE_LIMIT_WINDOW_MS;
    delete process.env.RATE_LIMIT_MAX_REQUESTS;
  });

  it("should allow requests under the limit", () => {
    const key = "user-1";
    expect(checkRateLimit(key)).toBe(true);
    expect(checkRateLimit(key)).toBe(true);
    expect(checkRateLimit(key)).toBe(true);
    expect(warnSpy).not.toHaveBeenCalled();
  });

  it("should block requests exceeding the limit and log a warning", () => {
    const key = "user-2";
    expect(checkRateLimit(key)).toBe(true);
    expect(checkRateLimit(key)).toBe(true);
    expect(checkRateLimit(key)).toBe(true);

    // 4th request exceeds limit of 3
    expect(checkRateLimit(key)).toBe(false);
    expect(warnSpy).toHaveBeenCalled();
    const callArgs = warnSpy.mock.calls[0];
    expect(callArgs[0]).toBe("Rate limit exceeded");
    expect(callArgs[1].key).toBe(key);
  });

  it("should reset request count after the window passes", async () => {
    const key = "user-3";
    expect(checkRateLimit(key)).toBe(true);
    expect(checkRateLimit(key)).toBe(true);
    expect(checkRateLimit(key)).toBe(true);
    expect(checkRateLimit(key)).toBe(false); // blocked

    // Wait for window to expire (100ms)
    await new Promise((resolve) => setTimeout(resolve, 110));

    // Should be allowed again
    expect(checkRateLimit(key)).toBe(true);
  });

  it("should cleanup rate limit cache of expired keys", async () => {
    const key1 = "cleanup-1";
    const key2 = "cleanup-2";

    // Populate keys
    expect(checkRateLimit(key1)).toBe(true);
    expect(checkRateLimit(key2)).toBe(true);

    // Run cleanup immediately (none should be expired yet as 100ms hasn't passed)
    cleanupRateLimitCache();

    // Verify still tracked (counts shouldn't reset, e.g., if we consume remaining limits)
    expect(checkRateLimit(key1)).toBe(true); // request 2
    expect(checkRateLimit(key1)).toBe(true); // request 3
    expect(checkRateLimit(key1)).toBe(false); // request 4 (blocked)

    // Wait for window to expire
    await new Promise((resolve) => setTimeout(resolve, 110));

    // Run cleanup
    cleanupRateLimitCache();

    // Since they were deleted from the map, they should be initialized as new records
    // If they were cleaned up, we should be able to do 3 requests again
    expect(checkRateLimit(key1)).toBe(true); // 1
    expect(checkRateLimit(key1)).toBe(true); // 2
    expect(checkRateLimit(key1)).toBe(true); // 3
    expect(checkRateLimit(key1)).toBe(false); // 4 (blocked)
  });
});
e:Hxvimport { describe, it, expect, beforeEach, afterEach, mock, spyOn } from "bun:test";
import logger from "../src/utils/logger.js";
import { config } from "../src/env.js";

// Mock Telegraf and fetch
mock.module("telegraf", () => {
  return {
    Telegraf: class {
      constructor(token) {
        this.token = token;
        this.api = {
          sendPhoto: mock(() => Promise.resolve({
            message_id: 12345,
            photo: [
              { file_id: "photo_id_low", file_unique_id: "unique_id_low" },
              { file_id: "photo_id_high", file_unique_id: "unique_id_high" }
            ]
          }))
        };
      }
    }
  };
});

const infoSpy = spyOn(logger, "info");
const errorSpy = spyOn(logger, "error");

describe("Telegram API Utilities", () => {
  let forwardToStorage, getFileInfo, getBot;

  beforeEach(async () => {
    infoSpy.mockClear();
    errorSpy.mockClear();
    global.fetch = mock(() => Promise.resolve(new Response(JSON.stringify({ ok: true }))));

    // Import dynamically so mocking is applied first
    const telegramUtils = await import("../src/utils/telegram.js");
    forwardToStorage = telegramUtils.forwardToStorage;
    getFileInfo = telegramUtils.getFileInfo;
    getBot = telegramUtils.getBot;
  });

  afterEach(() => {
    delete global.fetch;
  });

  describe("getBot", () => {
    it("should return the telegraf bot instance", () => {
      const bot = getBot();
      expect(bot).toBeDefined();
      expect(bot.api).toBeDefined();
    });
  });

  describe("forwardToStorage", () => {
    it("should forward photo to storage chat and return file details", async () => {
      const chunk = Buffer.from("fake photo data");
      const fileName = "test_photo.jpg";
      const result = await forwardToStorage(chunk, fileName, false);

      expect(result).toEqual({
        telegramFileId: "photo_id_high",
        telegramFileUniqueId: "unique_id_high",
        storageMessageId: 12345
      });
      expect(infoSpy).toHaveBeenCalledWith("File forwarded to storage", {
        fileName,
        message: 12345
      });
    });

    it("should handle error when forwarding fails", async () => {
      const bot = getBot();
      bot.api.sendPhoto = mock(() => Promise.reject(new Error("Telegram send failed")));

      const chunk = Buffer.from("fake photo data");
      const fileName = "test_photo.jpg";

      await expect(forwardToStorage(chunk, fileName, false)).rejects.toThrow("Telegram send failed");
      expect(errorSpy).toHaveBeenCalledWith("Failed to forward file to storage", {
        fileName,
        error: "Telegram send failed"
      });
    });
  });

  describe("getFileInfo", () => {
    it("should fetch file details successfully", async () => {
      global.fetch = mock((url, init) => {
        if (url.endsWith("getFile")) {
          return Promise.resolve(new Response(JSON.stringify({
            ok: true,
            result: { file_id: "some_file_id" }
          })));
        } else if (url.endsWith("getInfo")) {
          return Promise.resolve(new Response(JSON.stringify({
            ok: true,
            result: {
              file_size: 98765,
              mime_type: "image/jpeg",
              file_path: "photos/file_0.jpg"
            }
          })));
        }
        return Promise.reject(new Error("Unknown URL"));
      });

      const result = await getFileInfo("some_file_id", "some_unique_id");

      expect(result).toEqual({
        file_size: 98765,
        mime_type: "image/jpeg",
        file_path: "photos/file_0.jpg"
      });
    });

    it("should handle error when getFile fails", async () => {
      global.fetch = mock(() => Promise.resolve(new Response(JSON.stringify({
        ok: false,
        description: "Bad Request: file_id invalid"
      }))));

      await expect(getFileInfo("invalid_file_id", "invalid_unique_id")).rejects.toThrow("Bad Request: file_id invalid");
      expect(errorSpy).toHaveBeenCalled();
    });
  });
});
x import { describe, it, expect, mock, spyOn, beforeEach, afterEach } from "bun:test";
import logger from "../src/utils/logger.js";

// Mock db
const mockInsert = mock(() => ({
  values: mock(() => Promise.resolve())
}));

mock.module("../src/db/index.js", () => ({
  db: {
    insert: mockInsert
  },
  files: {}
}));

// Mock nanoid
mock.module("nanoid", () => ({
  nanoid: () => "mocked-nanoid-id"
}));

// Mock telegram utils
mock.module("../src/utils/telegram.js", () => ({
  forwardToStorage: mock(() => Promise.resolve({
    telegramFileId: "tg-file-id-123",
    telegramFileUniqueId: "tg-unique-id-abc",
    storageMessageId: 98765
  })),
  getBot: () => ({
    api: {
      getFile: mock(() => Promise.resolve({
        file_id: "tg-file-id-123",
        file_size: 1000,
        mime_type: "image/jpeg"
      }))
    }
  })
}));

describe("Upload Route Handler", () => {
  let handleUpload;

  beforeEach(async () => {
    mockInsert.mockClear();
    const uploadRoute = await import("../src/routes/upload.js");
    handleUpload = uploadRoute.handleUpload;
  });

  it("should reject unsupported content types with 400 status", async () => {
    const req = new Request("http://localhost:3000/api/upload", {
      method: "POST",
      headers: {
        "content-type": "text/plain"
      },
      body: "plain text data"
    });

    const res = await handleUpload(req);
    expect(res.status).toBe(400);
    const body = await res.json();
    expect(body.error).toContain("Unsupported content type");
  });

  it("should process JSON upload (base64) successfully", async () => {
    const req = new Request("http://localhost:3000/api/upload", {
      method: "POST",
      headers: {
        "content-type": "application/json"
      },
      body: JSON.stringify({
        file: Buffer.from("hello world").toString("base64"),
        fileName: "test.txt"
      })
    });

    const res = await handleUpload(req);
    expect(res.status).toBe(200);
    const body = await res.json();

    expect(body.public_id).toBe("mocked-nanoid-id");
    expect(body.telegram_file_id).toBe("tg-file-id-123");
    expect(body.telegram_file_unique_id).toBe("tg-unique-id-abc");
    expect(body.file_name).toBe("test.txt");
    expect(body.file_type).toBe("document");
  });

  it("should reject JSON upload without file key", async () => {
    const req = new Request("http://localhost:3000/api/upload", {
      method: "POST",
      headers: {
        "content-type": "application/json"
      },
      body: JSON.stringify({
        fileName: "test.txt"
      })
    });

    const res = await handleUpload(req);
    expect(res.status).toBe(400);
    const body = await res.json();
    expect(body.error).toContain("Invalid JSON");
  });

  it("should process multipart upload successfully", async () => {
    const formData = new FormData();
    const fileBlob = new Blob([Buffer.from("multipart hello")], { type: "text/plain" });
    formData.append("file", fileBlob, "test_multi.txt");

    const req = new Request("http://localhost:3000/api/upload", {
      method: "POST",
      body: formData
    });

    const res = await handleUpload(req);
    expect(res.status).toBe(200);
    const body = await res.json();
    expect(body.public_id).toBe("mocked-nanoid-id");
    expect(body.file_name).toBe("test_multi.txt");
  });
});
IU*-x {
  "compilerOptions": {
    // Environment setup & latest features
    "lib": ["ESNext"],
    "target": "ESNext",
    "module": "Preserve",
    "moduleDetection": "force",
    "jsx": "react-jsx",
    "allowJs": true,
    "types": ["bun"],

    // Bundler mode
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "verbatimModuleSyntax": true,
    "noEmit": true,

    // Best practices
    "strict": true,
    "skipLibCheck": true,
    "noFallthroughCasesInSwitch": true,
    "noUncheckedIndexedAccess": true,
    "noImplicitOverride": true,

    // Some stricter flags (disabled by default)
    "noUnusedLocals": false,
    "noUnusedParameters": false,
    "noPropertyAccessFromIndexSignature": false
  }
}
v@Px̼yC8q6ݵ3_2On P(I-.abW]|Gdr& bpxk`l`#ms<nGbz/韟eb x% 100644 upload.js )Y )ap5!MiSx[qBĥ5 [Rx̼yC81놉(I1]yB(0ٰwßEN;쑗wMb  hQxk`:1M
 fvx 'Fx̼yC8^wc>4x_<Sw(0XK3s#;Ũ	 Jtx __KpZ})9aeZx pp	fx 
(bx̼yC8k0VgbT[? JRKV3{m*Q=nC&w1j  sx __Kruo4R*/i#FxM 100644 logger.js gT`x]ɲU:SE100644 rateLimit.js 
ŵ_}gM4htx zT|&Gx̼yC8\3A/R>p>VjW_f(0;u~L
NWfdŨ	 ux __Kze1SvߕQ{>yZ2
x% 100644 logger.js gT`x]ɲU:SEKd=x zMM@ xo ?;5n8.DQ#W7\b'>Fϓ'03)*WdZ@YUc-40000 test H_7VݰY쌈瓊)3x
	sk8},
      "peerltypescript": "^5. C'typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/tB7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSwW  }
}
V3xJ -drizzle-orm": "^0.29.06,
    "nanoid": "^5.0.48
  }
}
;FxB 100644 env.js M g`'~06~>40000 utils ze1SvߕQ{>yZ2B1x' 100644 env.test.js g3S.s(D_ B%vx Xwl a*t)x  40000 utils ze1SvߕQ{>yZ2~kQx M)Hjx8 (\6sɭyg-Q{'5HRK)@x # teleuploader

To install dependencies:

```bash
bun install
```

To run:

```bash
bun run index.ts
```

This project was created using `bun init` in bun v1.3.14. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
Mmx 8{
  "name": "teleuploader",
  "module": "index.ts",
  "type": "module",
  "private": true,
  "devDependencies": {
    "@types/bun": "latest"
  },
  "peerDependencies": {
    "typescript": "^5"
  }
}
4B:x(100644 .gitignore -'NW|ݟ0ad/
[100644 CLAUDE.md vLٝi~~r(	100644 README.md 6sɭyg-Q100644 bun.lock ?;5n8.DQ#40000 docs ?6'x3H۟100644 index.ts {,dTώsx6Ѧ100644 package.json '5HRK100644 tsconfig.json I}rR%~wڦx& 40000 superpowers JJ5&(zx  40000 specs I*9 йt菿ix	 uz7;)q,}04*