From c48fbef0af32da17b3875272a2af9f64e31e1e17 Mon Sep 17 00:00:00 2001 From: Chris Date: Tue, 10 Dec 2024 11:13:25 +0100 Subject: [PATCH] chore: add initial cloud chore: cloud updates --- bun.lockb | Bin 189861 -> 193456 bytes package.json | 2 + src/cloud/connect.ts | 35 +++++++ src/cloud/disconnect.ts | 25 +++++ src/cloud/https.ts | 89 +++++++++++++++++ src/cloud/index.ts | 13 +++ src/cloud/tunnel-stack.ts | 112 ++++++++++++++++++++++ src/config.ts | 2 +- localtunnel.config.ts => tunnel.config.ts | 0 9 files changed, 277 insertions(+), 1 deletion(-) create mode 100644 src/cloud/connect.ts create mode 100644 src/cloud/disconnect.ts create mode 100644 src/cloud/https.ts create mode 100644 src/cloud/index.ts create mode 100644 src/cloud/tunnel-stack.ts rename localtunnel.config.ts => tunnel.config.ts (100%) diff --git a/bun.lockb b/bun.lockb index 9a219cd00abff62a5c7a5d0bed0c86f001b6c5b6..989e24f1faacec5b197b20474411c6b9e1c3a5a9 100755 GIT binary patch delta 36525 zcmeIbcU)B0_BTA|$OxmLC?Fsp#fAl>cNnniD2feBEQq5bAV^VA5$wJ0=(fk2SYof( zd$5a%y&H|)*cI#ZU8e}iHTVAB=Y8Klp67EX`OezwyZhQ}mzg=Zmme>?{$AN>p0ysg zto{4gZTZ%1FB&iQ{L!$h+o@I677j_tdfR!!tfaRmTD!ayHT1eVJ=Qt*@p(Z~>>UMu}{Q)D@uLRAhNYo`S3heliLu3%MUUsxkl&eU zH02=;EJ~=EdlYT3g|-Q@6669!j#cCUMScfqgZQS9RGOb6T@-1q$aiQ8#otln8Aa|= z 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 a8!vT7bfbQA_dDWG{J?OyMJ+;(G( ziN+W;7OYW|STNQ^V~HhdjNRCK`QCMkU~Y1s`+eVY|J`|bXYKXwwcFbD%$(t@y;^-~atgI2aI^8|g z#tHT(ARQsMK{kY3tn!l~?V+bbHiB#m=?>`z>7pyr73V z&;yccE}<0ape$AHR%Jg($|x@_J*^NKk3hy$&|cJ%M%tKBnFXmCI^8@M#e;2Km3%!R z?GQf+JaugjB-&iu7cz}pJ#uhn77f~kXg+0l6Ovro3zA06PWX#5_y&^Vqrs!m#o>_X zjpD|T)bbCSDr020DtkgwI?i^RH7KeMD;jBU;9P3I3B^gb5O8h}cw4!Zs z1tQ2#ySxaQUjlUUUT$7mN-ElS6+C%&MS#L51u5fdK%nA>Q0U}6 z2T1gMu>^^7i;5q^fZYE(Bvs_FnqW1gEA$T`Y2c28qzs}VX?%IBG6EH;3H^N(Of#Wn zYo*{J(5b?@Fr5xADn1LH#^DV3hB{&#WG$L89uW@^mW@Q9?41@ISeK`2|DHbSdG^~muSWeN=wW2%FNd- zMTESRLCQVXLDAD8DPd1YYUvEbQ$^Atn?Ryx)3b60>P|;1c74E8g)`HK<>X-|B&qt~ z%wdDPu*lSoQR4NGMP!)km6em4Qh-`yWoBm<==8CQK_n7VpU#0!6&ReAQc#dKNH;WX z^hjz^lu5DkhaFWsEq_EoW>&r~Er0lkw7k)}@Hi!1T0TYzD&h>gy0F^}o*I~vnq$l( z-_%6}wdg{;67eQV8Owvx298Kad^YtBUJt=jg=;1%1*RslFD;rDE$^f_CL<-^3w2DN z51q`ila#Tz9g^ywGDNPnu6<`^X!t?ef*+BW>4hn&%N;RnbZX9^v`Z*P9*Sx;)Ger8ui9#-Ouic1lo1XCcXpiz+IxHH|91`o;i8l94zrAx_6&rQk8Pdf~r+H0Py z$S2*E3_pfWj-l>Mfg^_^AF@k^q>9eOn5zeAfKGG31+o#ETC90?6tk(;AapQ>B#q4c zwCs^-c~}-z|7&jUig>DSdr0cHHjq^8FeXUNiaelEFKLyi37yu%R}o6C<^7cSTdF>( zzmi)PB+Y}~1C-vtZl0E(s?(){r`!kTddr0ceAxeI_nyPN5N-ZO-M1#C?ho)=#Pz*Ua{a~fyFQGdjpR~NZoIJ0z!kQRb zlfuOZJ3EL0V>@*~~Juy@1)(A-Q+ad5&(I`mL^V0HjvPP!q4i8b5 ztREn$0-GRd;$nQ}jKX-g%u>2r>z@#{V)6vlV}|Nu*q2s~ik=TtJft=JI&`Y&R^&nD zorO;0@h~LiKPN}2k5jHvf4Dy-D`ikxZRlk867e*Q?y7d*Asy9Un{KI@UYWxN=jc|V zCon#Wipvn71o^4?S+tUPVv^9%$V(fH&O;n@>hfxkRDm(Dr=e1BgtAs|g>;8L5cAF% z@>PMtcZW^|&p;mJpt)_8hMK7>UOeI@U0Yr8@Iu9)98w-bc`2!BD9n4b!WTeN!FwSo zK07ThJ{zAPeURk8B;-odbUv!-0(l+PqvrNko6!l9>?+17 z>FSMF?8~9UKgC}_Qu>0FT-w5QVv#bS!jX>Lls|e{>d?#rwxotsbRBV2!()(kklHM2 z@vg0|=0v4s+L&86K^-euDft;_`X$6uofd(ox|XQY7WveNKJtB~4!t3%E7T>lNH-`a zwIDBZSlSz-GMKf5x1dvHb3afTc>+51=?+M$=u$|U1V2qtT>XP8XMm?kkq>DP*#*)C zvU7*|WytQYVQh*tH-xd0YkW}+?({;M0kXbX8`0dasehH)#v zw$i=+$hbN5a#WZGS?(OAqP4h^&vZyX@GrnqS6qW6Po9A!`!0}_?-HahB3E3Rr;LFn z^A%SQg`_cIR5N%Aof5i1CzscNq>A1yQ>M@0g-V}l^Yx`cr}IJl4M-Y~1)0M}rxm89 z)>y2#zL`!LJ0)Vdb+&VA!dma1R{hq-l`UMKQ7b~vHrqC@-B`c%_}60`4^3`$CEG3T zdg6M!dUHj~t6z^UO};ez3A<}+XX$W%wSDxst~J)LZLn5kwH;m9)P^f8pO)Vn+C3)2 zmH9gSwm!u1sK8trWms=ViLFr+2%RM>ZRE*{8(E6g1U9XaLG%*Yfkp=Xr=m{RfgNfT zrZ<>rq4(d04j|M~j;&*kyeK5bnX_0IgYY3MaWP6qL5H9Sb7UVZI_a5}t3gO(F|J18 zEGuy}>Z7XZbUoNi*Dzr|lNuX^Us+6Jqu9iPO>1lrda{bfM*SuWovt&Ri7HrH>U42( zaa|E=Cxs4muBh3o$|vLM7g^&^(gZYszb88VwlSB0|i~Gx=g14cc z5mHk4Hq>fRf{k`6~0E%)`?m9 z8T6@`tDPxQzY(FX6cX(ku>*bvX>22SP9AefF2T}KRTG(wUx=kMoP{WX9SaJUdO`Dt zre~!t!TK4{T2nsKNrYm-OYE3$u(ZlWrwfE8vQlUlpv6JMfQHrwOIj~zW|W_>iNyrM z?TzL7^k`#=Kqo|2qp>f1!=xaiG{j9QvYMRrN@!6^W-h^^r8_$iWRQlqWB7p7%Xyw; zF|CY}50+9gL^9NSIJ5-KLfQ$f4Ky>B)GAnd0j(c2QPvh0UsiHVZ5%Fj(?wu6cm`kqRwQj9uH(3ev>>znfjSAE6K`4&RM98KU`a%w+Ak;?=?Lep< zg(M+ZnKA+^jS7~cp((QjntnPoIS=V1LIdUMCcPhGj$Y{}XTuSavzD$RM8i$b$N+7@ zvW>E*BNQsv?*|ss!Km*F&-P&P9m4cqA=Fh4d7)`zC?tHrVxo=0b5;^<6kD}n2ciwq z9E@-34IMic87v%SF&&LU4JO4H1rv+G`z%(1_YA%$f@qw zNT88hENBQwPFMobJ5)y$nG229XJw`P1{%2x?LudmcS4?Qrc0RAib8MGWT+Y(6cH?~ zhejQb6%dpE3A7NoMm|YOsd7!FG0>X=RMV5uI=sIr)kvB6SLXw(;yym)*FjT~(zJNh;>_)T$z6w_VlSTq^q zr3f0i2^(vd5KAa(GW1C8WN32xJ0V0Z#!iGmzW|y#gOI@qXu*i9Nqr#J=)tD-H;Cjw@q41**KLCg(W z9k4JTfQGZ|K)v6~F9^|qf}^q6*{3UcBPC9E$r1`VBIK1^|+vL+j#Q5>3r^*8nmoLS`? zggz`L-zY5`u6RanzjPTImNe=aL$DN(SG8%G(Bct?#iTWzp-T#kk|AFiqgZbIf`vRL zjW9~P!1>8`NdvGoLZcC`xFEVf8{t^stfxbxNn@tN6+4z1`MOr>Iznk6u5@w6`4bx1 z$eMn_NNU)j_uE(^KweOc97bu4z>Ji7LsJIdF;sv+AW<%=%O`hq}Eocd{M%~nUf^xOuFRwbMSV^%_vYV(B3VZC&3D7#r>%k`oMIj|T z;u0*}V?DJfI9Z!B=u&aYWM(zlAf86tWP`r(2eg*YEC>_d`+yyoY>+mAp#DG|1B3N7 zrqGGm8V9;T2n~>V^o8^rw06+sS*CZNis3|kYdruVd85$bkjqL_JU?zHS%T{bTV=$* zgw~xMn$*U!L}`*tiziE1>{Nr)WSZh%vAbQWp*-2V^DLZU7zC|gn#N|^~6gCZdlB$%Ay3_`u1gs`XUj-GC)wSzXBoY zMUsL+s!p;A^j?r|kmDe$K~7TRNm4$OA&L4x<^P=|lPLneG@^thYJz_!$z+;p_ji(_ zrsIwBo2lB7q~M3DoTbW-h{Wq70W*`rrFf%+OpPE(!P%<*caoyY)OeC+Y{!BI5=h-V zXd*kepaC;qXvf?a*2eOw(=7!{=AY7=DyhuRRGw}mpchFBey++dRGy?c_*JT2RTA}; z%Kwd&%a;wPz^_%qs*Kyv02qgQgDl^lcapMsX9ptZddhxm7eep40fspRV8J# zTjfcbvA&BM6j_2jsInv}^$|$2J_boqKhc{ii9ZgV>`tmYN%GgPs$NwRe?jF*Qu>RK zG^(#^qw+ck^22@AkR%yCRQ0Nos7ESKl7f#_og}4u3Q0S)1cjokh*Tx%)m6PZeH}(G zBB(_*)d-SQ^E#?dlJxqjUR6?t4OE^a<+XB{Y%p7fEBKD|lp}>j_EeQy|GSmEKfI=?AGg zNz&6)IT%up@^wQIz@Lt8*P|CnG90GL998D3@l_?+4F^wA`Kn!kYDbcSBUOHs%2$>0 z_#3T8RF%}iiE6@0YCK7D{S-)|N>sk8q;wyHr;0H(zN(}yn**Mr=HjiW1_Ga{#OK7~ zMUsLmRQaXK|DB`^S0lbU0of6f`ZfuY_->G7-(8i-kkq1{kn|!+c6}kKi!&iBd6RtB3;B z!eW&mNx?}fKUI}8At}RBic}>9XXA|uo}==Am1Ij5C(w(eE#L&eqwAB)!vA+PQu^e7 zMf~rR%8Wg0{4)VXF(f^J{|Lvnu&;Q!dh&}tJAmVt_ z6A^Jc-S%zI3Gj)W{Is1*gWue%hb~Nv>Dth_bbI~6V}6dw2@{7`KRE2rVn+*O@Zi&j zx^=&Fta-q*J-kW7pWWH&^>!?Ky-6@*Th=GDj1AVzX@g17v&;?2%x6l{ z`=O27Xc8n=0j+S8HS^kJ5~{P&o03`ca%*-Dnl?nxGc**v_5uzPq9Wifk`h1P5- z-ow~4ytiRV`;vulwsId@upcehZxW2G=l*0Nf~~`QB$FzVg(#MS_qJ>c-rF&o1Ia>r zmWlTcY&YJcnf;IGl^@Y7KbnLXRspT>AbRDX3HNr4K8Rj9gkFIb&pZyHSD?*2WD*kD zX=qaqqgM`_@EujjVf4xo^a`{tEc6I^1=^w`CZQX<4sGsH^vY3_kj&;CMXww~uR!a` zVvZ%Vr_fd(GYP%fGiWP*vSxjMGU3b5l|P|fKcihgn}q(X=g-N^^0+nI0WF0|$CKG+ zXxYb2LMqz=E#m}6-3b$JZp=J^QTGc*9kg_2{|nj&ZQL&=A(K@=D?EwzoiqtU+31sK z-zl^YS~l}Ih4w+4dCDZ@u+z|{o<{pln}p%4V25rSTwC|ir7{^wgL;KF7edkU1(z54y zwC@7i2W#9oA6cWX=qb_NBe#^38k#$ceL*c+6RrX&?{&k zv_)4;_%`%9w7GwveSeq)&gT7r_FYB$pv_}3SJ6IbtFM}b1?(BL71z+dYbIe4TX_xb zyN>o`=EWn>~Eoc z(8k>|2`gE}tz_X#cI$)AG%%ey_+z+y0z|X+qR8^xJSPq<8Bo+IX+$= z-L=EhFP=r~O3z)0K9_L0$YRUcnukUPx`!mZ2>YErwiI-)YrTeC$L`&Ov+r87Zud>X zdbadFoDEI?z$9#BNe|%cd)90tv~ngqgtPBkv$TgMVGCOa?J2Z+k4(ZgmhuSBeqhb^ zK-duWt=WjjCVcC)8=B=KYu4n6N!ZPXKY_EMoq+Z|bA1YDKelF*pPF!!!!c-f zPpnzcGn257O?U=pL%Ra4g84niV0voJ%AT8qAK67{&7WDb_AgBMwzKpF+zss!v?DC? zB?i-TYqtEQ2{-oLgBJ1v?R#ayt)xp|p?%QwuT8i`Ea^4c_Y&=cc9IEi(7sn_-y4%~ znyrKO^p$m4J;5aWT9$&vV#RCgvOUnwaT@_@k2ltQgaG{l-wnOYQn2PtMCcd!a1r`u z=qE_O%w5fdWS${_m}~~(3O`1Kod_bx9K=;V!5qYXBCZf|o%`uQ6qzH3#|3s?%d8A`l7og(L!K1 zpzkMr`918=d^Fh?)qJ~5BEAlYdVF3T5OeE*ctu159#a=Y$GRX^*9BqEpAqqth`zQU z9QaCG5G!m!Skq8(;yvqu=ur>E4kDbnR3C(8eGu98LAdfQL~JI)sR0N#p4k9IMgtIs zh-kv??LgSsff#28q8YCsVm}dH_8>g?XnPQa_8`s?;mJK3f@t0l#LR{uy!dG%P7x9A z0K$itIDnYy0OB?gemv9>M2I7ZMUEf>_;n(#5fSeMB8bm(0x{PK#493#c}yb^9UFmI z-3UY|e@4VpBKkUm2;(cAL9B2FVeJAUocDAA(ZdDA4kCI5$(CX8wfi$5aZlHMDq$F_7mad4kCt+b_Y@D4&odUChpM$MDr#f zW;Ov4&rcI^iiq&0AQE{=QxH>|g1AjY5)W+#BBU9JMa@8T;n#_{Mnrsb5Z(B^<{;)a z2l0xCWFF&zw~ihlR(pWx$)6GNl!(49K=kG-TYy;60)(|Eh`zk1Cx{-Nf_2=ZLHR|? z&zRX1w)W0`Q*fjC>-^4sHj`Ib%v~2fZQR`NKacq0UagnyBTk=rQq9)$s}U0y%w8Y2 zxBt;(x0<JK8sAH*Xf@_A$ch-*YF4*)TO-y>pf0EljZAV%?LRP~O5a8ZJB zN?c%x`{(l(J^lLoYx5iY>R#}?%d(W!b^2^^AN@=6)BZaLxxG0ayLMomsGlyLnK6o& z>KtmexnsY3^zD`|i`UTqHqcx&nm-FfwofTr|Jo5P+CBWm*)eofU43ZgmPWt+Fg`)w zHZy6|l5bwt=^D5B`reQQ_t&o((z27gkJ)<_wuWh&o6QUid-yc>POrqnxmB_q%U1>= z+Z92mzI7{9e;n`G3Pg`qAa)Q@#HC;mmcbyhgF#H-TZq_9gi{EJNjx(IL`DdRLqtsG z_Msr`LP3lR6-@E?qWbO2d{1c0$&ZI7ZT{FPH#hxF-#@PRJayHgzwMd@4-%KnDs+C@ zt6)RSt{Yq0>?yE`jr(%%tK{pom;D(jT5e6>RI>4C<|)1-RIuY-tx-@3AKe-S6}Cn} z=ZKijJ;FdV4+AkX48%-+nut?Ggtq}PigplOjOOHGtR{0fKWO5(Mtt#zqqfVjf>d#8V>bMS)nrQ=&kuhybyNh(+9{Er=eG zf_2fBW){DH^=#+3HQye$v&dL6a(=BznI8y^zwP>EYT2QworS;`{k|UbWmyZaHEr$g zHCt6f5_hkkkz(FGZQe&&hbxwNRO!AYRULb0{J0gp2E0GaduZ!Dk5_eQUO#!s`t_H- zpRr(HZqR_Espt8e>=pgLK5sv5{=#9MEmw8gc*SmACx#e|;8NG01{GLfAlC7FM1*t%(JdCldcHIk#5E%HSht0ZJc$<8 zxiKI%5>d{DI1n9UL8Qfj*uvKl@sx;q@gTPGlz0#;=oGMrh#lM}0Yr~D5F-*m?Bu(N zu#5-MBoV}JK0Fb`W+F}y@jZ9#1R^5=#NApN&>NuPe=l>pNK0&RB*q} zAPPHyDC-R3M}Cor=1CyhcL8yTmv#Yhiik%b;^__o?PZ?0eZm#94!{1gWOCY|?q8oQ zoFoh_Gk$wyaQ`(uR({0~uw(OP9AD6;*}&eJvq#U`xY(kN$Ln?C;=VbSm$!?>5c^LW$yf7_O=TTk{-N#5*`u0tN-aD+x4dGdnc?eb!*zMtBL6Q{S4 zD}e&#I=V)pehtvT>9}e_6Yu6g8bhSQgLsq^fq}XIqpWH z^0VQGkhR~&{f#@!Wp}(c1nvmKomz@#-?l$aan>MwLPK858Va*pz5b!LXse@a_A(1* zs|TP4Z%b}N5q{b)_cx^~wWFRNqW+5i4LBMB#MML?UojWAF29&9j1`0e-Zw`WrBBY% zveewlvvN_CoRo^{LZt|E__17}uIQ9o{ztB`SSX*HC!Dq1^qvsV)2h>9WlYyN#vlJ8 z7?c}QUcXUZC|JHZh)i%Jnf%gj5~5nQRXOcOA^H-wqsnPF3(>c>CY7TbhA2qi_{OQ+ z4wa+tw-W{RhL~?bQUUbcb7$3Xmug5~cz01bx{-+N=tKFgD)+r=X9cdO%Kf0)(YN$# zDFI%4RXh5ATDyl~Uy({$gPZ|FvfZz8H4z@GCazF9%vRkCgz0ra<>(uDGu7@#m7_1e z^(sgAIZ+bKHQfya$U}!zuBa}QJ`{@=bqxjS3+sL=r`;S?4_u1MX*WvM2R9lVwOsl1 zL7rhc`d3u+`WbPQ#15caj;Nw^GZk_6Kn=Pff?g+7vLV9M6;!ETRL%k6O$bw^=*}zr z(ND2;)2Z|EI;C>-4f_za;L|GC2%HBvs(^B4lz+A@|LY%6xPvQB$!B z5q45DJg0Jv!O_i4^g6F{A?hl<;S`^9M(7|03H;0%VLUDxQhpV>xY8X1WdPkt@G(F) zXWRsC0k?tQxo}pfTXX`!Ux1UqY2Xa-D{vN|`#z2UKLPYb$UcDX8Nm%DI=abZ4M3x8 z9qt88 zqyPhfRA3O01`Gz$feauDXbZFh+5;VcXn^jg`4hMc+ym|d4}gcj9F((Az)wnO@NWUO z0`$Ed-Fi9*lI|{A3DDg}UjUy0bZgU8zz6UKFd=l-fCSJ@fcJq1JmtLLkV3k2IwZvmB5z(-3Yo2SOhEv=rBxImD7P4z&Kz$K#NZ?FcApgCFg|~MWJw+ z5r_n$0J>3U2PEB4lMM_5a)4YQ9~c3Q1V#ZgBgX-;fC-2P5`n>JO*)VX&}0dNEw0nUIMFbntypxfEH@YvJ)jlpunl?B4YUmS z5by@*7&QyH2#kPTK0vqCVWlY2*&$FLm<7X;ki!5kAP1mZ(q;f30igh`&X)jMf@sO1 zm4&vAeE@AeLjhWF>5j9VkhEaaZE|!sVs*d@pqoU$1-=0`3GI~IcRzrVfk+R)1|XgA zAv}-;-4kdCv;>@i#=vj1vf>qTmDaV!ML$7Z3w;gnC9o3s0{9$&xx6rc0!f~j33LT! z05l^@fT_TH0PUxvfssG~kPqYm9Rb<^P$ivu9(Y5xv{7THDWd;vBJW)kNt-BbJG4{K zwju$vd20!1$JfFnDUQ<6hD_L>FPtq2FZ54#E!t=!a3VlQiB0 zox zvVb8#8ZZb*1qJ|p0m`(WD*LOFxPd^5s#BOaLI#iy3|5gztMgDO6iEe+015#b5~Bce zUNe65vLF=^O`T00Kw*j=3ycBA0aJhvfXTr7z$Bm;7!T0FVgfJ`&_>mCgr@-=fe(RF z;3HreuoUJYhMo0$2rn4SWTxRwZpRw6@UVN}CBSuC$5JX0ia7Pv_=&2ymbbpxtE( zFaa0?&?40j=m8`HZa`ze9%um62kHT~Kuy3Jr~z04df=_+)xbJb%nZB;&_eTuy7w{g z2zUtG1MUKU0yN$30M~)bz&YSo;0$mIH~}07egcjGM}Z%KJ-`pZ_rQ0+E?_6{EwB~X z0(=8dor*Rguo)-^HUb-f^#JjtliUW-=D8i%4eSE;0egY{Km~9BI0zgD4gp7ipMjIW zF921R!lwb^31?||Uqs+H;5={vxCHzG{0>|Jt^(J9TL882CU66|4Lktu15|195_yL_ z@*1GlKL?%y)aoa|GvFog0(b=o)c=%-49w9V)ITIGfNB86Rfiq zX!?zW%ms3QVL%2j1jq#FM3n_(1G`|G4>=sj11MbqFapr>rwWY+ipJq>EI|EFQ-&%+ zHKSU-2T21_1SkP<#gH^;N&f(t4A206AD~r`2JA#gvYiA;V~fHRr{y~f;ST|LyGVA$ z85n3Q*>u%#8st=f5|=fT*NnFyBZ{ONY8<)tN8kWZ0qg_5 z1GWQn!ZZQ95#9yt1js8pfC1pU1C-yl&?!H(R+f~GFpwn8f2tJ~R5epFCZ00=0iaAM zW6gEs+Wx>Ea5P>tdvdYn1@a8_JEcvBjpjjWpytIQ%4jbPs76%aeuS%5rfRrqMJY2% zGh9t~5IVJ%xI+k2>rw!k+BEuUupR?{lkzF_B&fI3rU zCJcJf-EUteZieX>;N6O{=*?##iy&HF$>7HFPg;HTs{!AW_4D)g_lE_ohxHJ%z2t+b zYf417${5biQT1r%)`7w88`-u_LndA#gFtVj)|GRc2ZBAm%khM8I0L)tuzUSzSE#dR z{k?J>g1r6wbys+Q7&zX8fh`Q0?46g`lkG3UazeyE7Qw=a21G~!IbOJw~8@-9O@*PwluH6~{Pf1rILa zx6v|TE4O$e1PI4?82qNTqMo@QE+}{oc~YshR$adRi4dsOc`mR16wcEgEU>)kg$yBf zZI6FxvD)yJ<@rwqGr>XoH{qo|y=FGu7#?aXd?U%97!)7nrO#00S-uLwAr(`AJbC{7 z&PA*IcZ8Y?aJV0h;Su}-41%;r1YDg{aDS=s@wQ3>?I8lYuavH++aUT$Wy~t>^&EL? zPaW`$n%}>?S}n2C;3!Xl0q!Cj3*o3ew!qTH;JI0teX!C_s?OIw7wlST4?3`W{@kzg zfmUgi7TTi_zSG@_ZhBpesElb}oxes}2kqerE$TE65T;*VT4{khJzk&|6SxV&L3{YZ z{>R16?|s&dRoboOMKB1`p4QOWaMaV$$>xhn1MPVZ)(zTSU;OpReU&jc_#sM4AHURx zm$L?Z`sSC?Es>Q5+9M*4Ev@bJL-Xs2l`&Vi!%K{r`#kZbVBbo65W>bcx9(dtJkZ2k z2=w+3_QoXhsEZRj@=lt6=T5{Y$6i;)bgau4zk~<0hbvSo_-Rnq;RomkE$u{p44Df{ z`ArCiUC601a+*Ht)xvT&n;VsOm$>sQR6={2!yl1NEFZOP_d}(D^1uf?Z=l)g5dRlP zJ5K-5&4PKn!|{s=J$?djlUSLMysTL$$|LOtMcu zc%WJThDaHTJZX6Rrwx_8W+xosNoJyb*>7S)(bc8wrD*3E?4s-sOUIvW96jE=Hu4To z>T7c0zJh2M)E5@Ch@=m!J@DYwG25s`ib;-|HgU`14Gm9p%|r~AMQZCLK28wragz)e z!~oILl^;Y-j%{6)|CYS3c-4uy>z4aemZetj4Nr`P^Q*h@KvDE>)!0q>AH>;1$J`sa zKKV!3)0Tn!!x2N*$im}0 zhkI;+7xIuNd z9^S#eeO52pIrK+D^1+#3YL5A|``-6#;eBkkcxCch)kOQCeAOTiLlHnb;*%o*NxWy+@c14Kbfs#M<2L4B4;@s%H*#P$)lV2jh5cb0oupvYZ3+Coeh z>wELH7NT7%?a?9A;%Y}1Hoic|1ss5UF=Nn3Xa^_ot5I?3+WYT+;)3WJ{MxlC|;fFli z3I^JvPagGda$I-36gPrv28a1l7zn@fZ4i#yqf?xFMHmLG|HEB&gj}K&#P6a+(IJTM zs*Z7~J$+?f(l0AMyI9h?GRGtyjykDT`&;pUo5}y||Gks{YyJ^iw&n@7M7tq}v1iiZ zXQXSry&Zqp>{wgq*jgFge;xmoG5;@VbYc7`%G1i#Bu=_cwK0sf2kXp#cGaf)?1Xqs zHo98Fgoxrp$v}IO&axlpj$Sh4C+t<@aWUuZ=d0Vpm%yM%d*qIk(`N6Dr1{w9#s|?c zKT!AA({|+|q5U)bFEfTVuwP?=A5)-IT&CeOWrOnX^7*s5bvHT=s)m?A#GpRfqX{M& zo;TUQ_3lc<;F{692vOQ&4BCJG!QOA(B9am1?~R=vA7L7GIv03(|G~pI8g}aEjhGN6 zMtdZ};M2x$x_O0Msf^JcnlLka&_`0MBaVon3qLGf+G7@muY7;!k%K;d2_l_%&W9Twccgx6Y)Di89w1+)>=6~p$C4BWM z*}~r&dy;L0GDg0x+iLtLKjdyeOdz&4xTytV>Y`nlLmS29_B=iXF+qw4!_^qiYRmFY zdX7;p7yam}s}o{sAx(#d<;(M)e)|>DU`6)JLgV@)$^lX9#@9XAYx|->hzd~l&@pOE z?WaS><<96q{SlxzunaMDf&AF3)+g~rhr$qpN>jsDB8FDuMXt}!KkZli95L8`P>gOH zuT@uUkmRd73KK_|7|bH7MTTB{4<>Y#jyZ7)F*b;CeckDDlZpl*hza%fr_Ow(#vE+< zRr@!MCU!;)@}#j*laHz^`kKS59=PXRwBQHh z)Wv$*mAiUkgb#)tm8d;KK`s$~#z%?V%T}}>Tml1Xsc*HPzG^sgwhwmw0A;i;LJWP- za(dG*%{PjJ6dF;S}Yo+m9HU_=kokCY)T?buXPRus;X! z-&{poD>T-RPHHYue3w6_ts#mR*Tc4+9HrFZysLd3tMHnl+!#M{%TUD7rf6R++1;V} z$k}p?JoraN@xzpM3M}ZrQ(xcHDZyzH1}6%U9Z`xH>Z$y=q1^_woj6%e>rYKz7R4Rw zvHD?IFC8EJ9FiN#YidD>fuVw@BM4^ZeA>{VqA!58`LsVNI!v{1(Git~1 zF%88yN9~zlK3^Di{V{pqIOL^_Io+ICehC(0l!^N~AVqi_k99x}o8$O+($N=dz2Shh z4q_dvn0WaMay;paA5@IzILc`t20Loc^@^x>VQ%Z4mQ;v(agmzHCpyAC+M~S^e7^hC z{=oOPu%O9}CN1y8kCH`pk}_rn4tEcas9uXEIpu-A+n&T5IlSiGOjjl8L`CjF-BI3lw3`JGhF0l$V_U z-axS~|H4&t#Yc!0uA-;DcRws(_|cEp(A*#20Px!_L_c$XKXs7fS2k&lae;sp#l;V` zxZGHD(k1|YE9BsYl`xA3x#7_M&nw|1KFLk=#Lqj{xS`zM{20u=%2Jexvb#>$qyBLp z(`6<;CZmrg|GMqk;`d{I?zryhKT!EpRDQH5I_-~6W6a{a%_~Pzd^Q+}O;)SsBzMtC z_>M0{D&a8S<}SuNoJC?*y3XC2hyksh!+>V4WO{GQ!{xziVSu9y z4Lq|{<-)C7^1-CETjS}9CRiD8HBdv{`@p<-MXv3KReGF=JdG)8g+>KG>3uqT&vb= z;`&~Vk1S9PQ1uADE(ldm*uonjzt;!A>&pZ1KkNo&)Nv+wD1f^yur~kyKuQLUc~KymJd| zFWNJ1^GiCu-mu)APAJ&esr&v~Y;9pmt}^bh!EHra{E!7xFhDt91kl9Ow(?w0EH~PN z*!C`snebvx%~^8J@)6fIpMU0w^q%~rrx;Irf76y&;j|~Z6+R4iwZ77`B@)1SR29X- zw~-%0%11;Or~X>7F5!SBYyy>YJXXE!ez?5U9-O8<6soe=SA2I%(Y8oFGiXnRvq%^_ zD%ax~ApcrI^3VMxDj;S|3iFSXUeg{9E zl{+5&8i5#EwLg9E>E|tPyn1dcSfEjXa52BvL~Nj^)USAow`gl2&n9(QFh`^3b0HjO zyu=Xvn0PDi7J~78o=*r7yE!V4q|+7q89!_C-Xt6PypN8tC=O2W#{Pznf~tpN{3phxqh{!oU0nOFt2P=&A3gf&uzSBoeh&Y0_mVV zA#aL>$Kv>9-+Uo=q#u?6-9CPdQvFTq6!+N$X-~t;_;bUbodXLzkQUc+C{(9CFV8=s zpZCRm@6D0xB3}qr@N7SfMr9nuwbCB2*C?Pv%C#|NjpgFybxnI*-uS?;rJqtK?m-M4 zxX~PpQyTg-6?*t%9~F!E43g^9aQNGl^0mY<{=gp|xXw*&&@%aWZ?ElMydXfF2<}M$ zHiwc4%78MBnc%wSKqD9B#6@Fz5$_*}E~u8zYq!Ss{Ba;Y#8r->arl+mi$G+5n)?Ka z?XA8=9t|-lYfj|jgV0;=taidBekBNgQ1cNnSrb~JP4!0bj8A27KK^n2 zK)Lm}R>Ifa{75T&nhOV-2P4;!JUCcvuZ-D-mKdJ25XviMI)0_OJM?eL$o+t~Yb_2I zf0)9*LL0=NrtrP3#W-OGuN#J(QH73SVhjDMsaS{TAmH>jV?g-Vd80p%S!lz(+8}}I zVP%479(MfKXxCsfy9ZZ)VpKGE*MrBGiEwB7?DAR}EEC)#Oj+1O71uKDH0gN)j8);%hr%$cD_~pR_~v@8)j!u?d%$UjRTHzO6z-*6T5XwHQ^$X9 z*}IcUo1w~gfMS#Rd@4;bZK^3#^e@9y8_SOWyi{opQ@#}Z>!b<#=MncGOec9zYmano zpV!s-^B!RnD?gX6;`pcY`A6}Xv;S!a{Ktmq)_NB5g@iUI{<&{8e=GIkdlT?6z~F`a zWkTgTtwcGz!TF!g(;qJVrrWD=N4lX`u}x#^)4lU$to9ikUz;elbXm4YdGxdPEZS3f zj&ql6igiX}T*Z6)^6{NSdu!xzx1ETiFl2lFStl`yU+W}B`2X&yd~7oIQ9C==8|7c# zg-)MICzbJGN1b{Ho3&Iv(z|r$*|ruN?vJd~lJ`my4Znx$sC3%XJyfY&iUHA zqEGplQn8=dEh0BBt)L(?Ezc)6YeahHFt5Vwtjg-DjZ>oDQM?A{G&MXjGd~la@fw;oI=_7695J^(U%w9DsJnb3w&0g$i|+gZ z6a9F3Cb-VC#lgH<8LIlnSK>o^C;E`zk6JV=rC>x}N)~*cotu)E=9Put_4C8~aFBkn zSG2QF$uG#u@zQFWl9!*BmoHx^;@=vG9^}hveAh|Qnol{2B$xN%qpMYip(2MM?sZRe zQ##8cjt<>mQ)Wfd}F}x ztwA}pPotcdhsBX*I8T?4I3iv!6N1XS{VY10^H27QHM#d`bgI*7@lSk;%B|0e2}<6T z_50{7J|GF==k|(D{K#1`ozFcfx|R1lC*G~gYu* { + const connectionId = event.requestContext.connectionId + const subdomain = event.queryStringParameters?.subdomain + + if (!subdomain) { + return { + statusCode: 400, + body: 'Subdomain is required', + } + } + + try { + await dynamoDB.send(new PutItemCommand({ + TableName: TABLE_NAME, + Item: { + connectionId: { S: connectionId }, + subdomain: { S: subdomain }, + timestamp: { N: Date.now().toString() }, + }, + })) + + return { statusCode: 200, body: 'Connected' } + } + catch (error) { + console.error('Connection error:', error) + return { statusCode: 500, body: 'Failed to connect' } + } +} diff --git a/src/cloud/disconnect.ts b/src/cloud/disconnect.ts new file mode 100644 index 0000000..6e2d5aa --- /dev/null +++ b/src/cloud/disconnect.ts @@ -0,0 +1,25 @@ +import type { APIGatewayProxyWebsocketHandlerV2 } from 'aws-lambda' +import process from 'node:process' +import { DeleteItemCommand, DynamoDBClient } from '@aws-sdk/client-dynamodb' + +const dynamoDB = new DynamoDBClient({}) +const TABLE_NAME = process.env.TABLE_NAME! + +export const handler: APIGatewayProxyWebsocketHandlerV2 = async (event) => { + const connectionId = event.requestContext.connectionId + + try { + await dynamoDB.send(new DeleteItemCommand({ + TableName: TABLE_NAME, + Key: { + connectionId: { S: connectionId }, + }, + })) + + return { statusCode: 200, body: 'Disconnected' } + } + catch (error) { + console.error('Disconnection error:', error) + return { statusCode: 500, body: 'Failed to disconnect' } + } +} diff --git a/src/cloud/https.ts b/src/cloud/https.ts new file mode 100644 index 0000000..727e70f --- /dev/null +++ b/src/cloud/https.ts @@ -0,0 +1,89 @@ +import type { APIGatewayProxyHandlerV2 } from 'aws-lambda' +import process from 'node:process' +import { ApiGatewayManagementApiClient, PostToConnectionCommand } from '@aws-sdk/client-apigatewaymanagementapi' +import { DynamoDBClient, QueryCommand } from '@aws-sdk/client-dynamodb' + +const dynamoDB = new DynamoDBClient({}) +const TABLE_NAME = process.env.TABLE_NAME! + +export const handler: APIGatewayProxyHandlerV2 = async (event) => { + const host = event.headers.host || '' + const subdomain = host.split('.')[0] + + if (!subdomain) { + return { + statusCode: 400, + body: 'Invalid subdomain', + } + } + + try { + // Find connection for the subdomain + const connections = await dynamoDB.send(new QueryCommand({ + TableName: TABLE_NAME, + IndexName: 'subdomain-index', + KeyConditionExpression: 'subdomain = :subdomain', + ExpressionAttributeValues: { + ':subdomain': { S: subdomain }, + }, + })) + + if (!connections.Items || connections.Items.length === 0) { + return { + statusCode: 404, + body: 'Tunnel not found', + } + } + + const connection = connections.Items[0] + const connectionId = connection.connectionId.S! + + // Create API Gateway Management API client + const endpoint = new URL(process.env.WEBSOCKET_ENDPOINT!) + const apiGateway = new ApiGatewayManagementApiClient({ + endpoint: `https://${endpoint.host}`, + }) + + // Forward request to WebSocket client + const message = { + action: 'request', + connectionId, + data: { + method: event.requestContext.http.method, + path: event.rawPath, + headers: event.headers, + queryStringParameters: event.queryStringParameters, + body: event.body, + }, + } + + try { + await apiGateway.send(new PostToConnectionCommand({ + ConnectionId: connectionId, + Data: Buffer.from(JSON.stringify(message)), + })) + + // Wait for response (in a real implementation, you'd use a response queue or callback) + await new Promise(resolve => setTimeout(resolve, 1000)) + + return { + statusCode: 200, + body: 'Request forwarded', + } + } + catch (error) { + console.error('WebSocket error:', error) + return { + statusCode: 502, + body: 'Failed to forward request', + } + } + } + catch (error) { + console.error('Request handling error:', error) + return { + statusCode: 500, + body: 'Internal server error', + } + } +} diff --git a/src/cloud/index.ts b/src/cloud/index.ts new file mode 100644 index 0000000..62bd19f --- /dev/null +++ b/src/cloud/index.ts @@ -0,0 +1,13 @@ +import process from 'node:process' +import * as cdk from 'aws-cdk-lib' +import { TunnelStack } from './tunnel-stack' +import 'source-map-support/register' + +const app = new cdk.App() + +new TunnelStack(app, 'TunnelStack', { + env: { + account: process.env.CDK_DEFAULT_ACCOUNT, + region: process.env.CDK_DEFAULT_REGION, + }, +}) diff --git a/src/cloud/tunnel-stack.ts b/src/cloud/tunnel-stack.ts new file mode 100644 index 0000000..adfe27c --- /dev/null +++ b/src/cloud/tunnel-stack.ts @@ -0,0 +1,112 @@ +import type { Construct } from 'constructs' +import * as path from 'node:path' +import * as cdk from 'aws-cdk-lib' +import * as apigatewayv2 from 'aws-cdk-lib/aws-apigatewayv2' +import * as integrations from 'aws-cdk-lib/aws-apigatewayv2-integrations' +import * as dynamodb from 'aws-cdk-lib/aws-dynamodb' +import * as iam from 'aws-cdk-lib/aws-iam' +import * as lambda from 'aws-cdk-lib/aws-lambda' +import * as nodeLambda from 'aws-cdk-lib/aws-lambda-nodejs' + +export class TunnelStack extends cdk.Stack { + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props) + + // DynamoDB table for connection tracking + const connectionsTable = new dynamodb.Table(this, 'TunnelConnections', { + partitionKey: { + name: 'connectionId', + type: dynamodb.AttributeType.STRING, + }, + billingMode: dynamodb.BillingMode.PAY_PER_REQUEST, + removalPolicy: cdk.RemovalPolicy.DESTROY, // For development - change for production + }) + + // Add GSI for subdomain lookup + connectionsTable.addGlobalSecondaryIndex({ + indexName: 'subdomain-index', + partitionKey: { + name: 'subdomain', + type: dynamodb.AttributeType.STRING, + }, + projectionType: dynamodb.ProjectionType.ALL, + }) + + // WebSocket API + const webSocketApi = new apigatewayv2.WebSocketApi(this, 'TunnelWebSocketApi', { + connectRouteOptions: { + integration: new integrations.WebSocketLambdaIntegration('ConnectIntegration', new nodeLambda.NodejsFunction(this, 'ConnectHandler', { + entry: path.join(__dirname, '../lambda/connect.ts'), + handler: 'handler', + runtime: lambda.Runtime.NODEJS_18_X, + environment: { + TABLE_NAME: connectionsTable.tableName, + }, + })), + }, + disconnectRouteOptions: { + integration: new integrations.WebSocketLambdaIntegration('DisconnectIntegration', new nodeLambda.NodejsFunction(this, 'DisconnectHandler', { + entry: path.join(__dirname, '../lambda/disconnect.ts'), + handler: 'handler', + runtime: lambda.Runtime.NODEJS_18_X, + environment: { + TABLE_NAME: connectionsTable.tableName, + }, + })), + }, + }) + + // WebSocket Stage + const webSocketStage = new apigatewayv2.WebSocketStage(this, 'TunnelWebSocketStage', { + webSocketApi, + stageName: 'prod', + autoDeploy: true, + }) + + // HTTP API + const httpApi = new apigatewayv2.HttpApi(this, 'TunnelHttpApi') + + // Lambda for handling HTTP requests + const httpHandler = new nodeLambda.NodejsFunction(this, 'HttpHandler', { + entry: path.join(__dirname, '../lambda/http.ts'), + handler: 'handler', + runtime: lambda.Runtime.NODEJS_18_X, + environment: { + TABLE_NAME: connectionsTable.tableName, + WEBSOCKET_ENDPOINT: webSocketStage.url, + }, + timeout: cdk.Duration.seconds(30), + }) + + // Add route to HTTP API + httpApi.addRoutes({ + path: '/{proxy+}', + methods: [apigatewayv2.HttpMethod.ANY], + integration: new integrations.HttpLambdaIntegration('HttpIntegration', httpHandler), + }) + + // Grant DynamoDB permissions + connectionsTable.grantReadWriteData(httpHandler) + + // Grant permissions to manage WebSocket connections + httpHandler.addToRolePolicy(new iam.PolicyStatement({ + actions: ['execute-api:ManageConnections'], + resources: [ + `arn:aws:execute-api:${this.region}:${this.account}:${webSocketApi.apiId}/${webSocketStage.stageName}/*`, + ], + })) + + // Outputs + // eslint-disable-next-line no-new + new cdk.CfnOutput(this, 'WebSocketApiUrl', { + value: webSocketStage.url, + description: 'WebSocket API URL', + }) + + // eslint-disable-next-line no-new + new cdk.CfnOutput(this, 'HttpApiUrl', { + value: httpApi.url!, + description: 'HTTP API URL', + }) + } +} diff --git a/src/config.ts b/src/config.ts index f6a9417..899d548 100644 --- a/src/config.ts +++ b/src/config.ts @@ -7,6 +7,6 @@ export const defaultConfig: TunnelOptions = { // eslint-disable-next-line antfu/no-top-level-await export const config: TunnelOptions = await loadConfig({ - name: 'localtunnel', + name: 'tunnel', defaultConfig, }) diff --git a/localtunnel.config.ts b/tunnel.config.ts similarity index 100% rename from localtunnel.config.ts rename to tunnel.config.ts