From 8f2ef2cc982d078d00720f9a7106540a85ee54bd Mon Sep 17 00:00:00 2001 From: chahinebrini Date: Mon, 25 May 2026 07:11:47 +0200 Subject: [PATCH] feat(mdm,vip): MDM-VPN-Pivot + Layer-2-Country-Curated + Custom-Domain-Refactor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MDM-VPN-Pivot (Phase F.2 done): - ops/mdm/profiles/rebreak-iphone-protection.mobileconfig auf v5 mit com.apple.vpn.managed Payload + OnDemandUserOverrideDisabled. iPhone-User kann ReBreak-VPN-Profile nicht entfernen und "Bedarf verbinden"-Toggle ist disabled. allowEnablingRestrictions empirisch widerlegt für FC-Toggle- Lock — out. - DEV-removable Variante als Test-Profile dazu. - Bootstrap-Tool (rebreak-supervise.sh) + Supervision-Identity-Setup-Doc. - PHASES.md updated mit empirischen Befunden. App-side MDM-Detect (Pfad-a Banner-Logic): - modules/rebreak-protection: getDeviceState() returnt mdmManaged via Heuristik NETunnelProviderManager.count > 1 (App selbst kann nur einen eigenen erstellen, MDM-Push fügt einen zweiten hinzu). - DeviceLayers.mdmManaged?: boolean Type. - blocker.tsx: lockedIn-Bedingung erweitert um mdmManaged. Bei MDM-managed iPhones wird der App-Lock-Card (FC-Authorization-Toggle UI) ausgeblendet weil der per-App FC-Toggle nicht lockbar ist und durch den MDM-VPN-Layer redundant. Layer-2-Country-Curated-Pivot: - backend: vip-swap.post.ts raus, suggest.post.ts rein. Curated-domains durch admin (separate Tabelle/Pfad), getrennt von User-Custom-Domains. - Admin-APIs für curated-domain Pflege (index.get + [id].patch). - seed-country-blocklists Script für initiale Curated-Domain-Liste. - protection/webcontent-domains.get refactored für Country-Curated-Pfad. - Migration drop_vip_swap_fields.sql + schema.prisma adjusted. - docs/concepts/layer2-country-pivot.md mit Architektur + Decision-Trail. Co-Authored-By: Claude Opus 4.7 --- apps/rebreak-native/app/(app)/blocker.tsx | 30 +- .../assets/onboarding/de/vpn_permission.jpeg | Bin 0 -> 128155 bytes apps/rebreak-native/lib/protection.ts | 38 +- .../ios/RebreakProtectionModule.swift | 72 +++- .../src/RebreakProtection.types.ts | 11 + .../src/RebreakProtectionModule.ts | 16 + .../src/RebreakProtectionModule.web.ts | 3 + .../migrations/drop_vip_swap_fields.sql | 17 + backend/prisma/schema.prisma | 10 +- backend/scripts/seed-country-blocklists.ts | 113 +++++ .../api/admin/curated-domains/[id].patch.ts | 50 +++ .../api/admin/curated-domains/index.get.ts | 36 ++ .../server/api/custom-domains/index.post.ts | 77 +--- .../server/api/custom-domains/suggest.post.ts | 53 +++ .../api/custom-domains/vip-swap.post.ts | 63 --- .../api/protection/webcontent-domains.get.ts | 69 +-- backend/server/db/curatedDomains.ts | 107 +++++ backend/server/db/domains.ts | 42 -- docs/concepts/layer2-country-pivot.md | 321 ++++++++++++++ ops/mdm/PHASES.md | 85 +++- ops/mdm/bootstrap-tool/README.md | 101 +++++ .../SUPERVISION-IDENTITY-SETUP.md | 81 ++++ ops/mdm/bootstrap-tool/rebreak-supervise.sh | 407 ++++++++++++++++++ ...hone-protection.DEV-removable.mobileconfig | 89 ++++ .../rebreak-iphone-protection.mobileconfig | 232 ++++++++++ 25 files changed, 1861 insertions(+), 262 deletions(-) create mode 100644 apps/rebreak-native/assets/onboarding/de/vpn_permission.jpeg create mode 100644 backend/prisma/migrations/drop_vip_swap_fields.sql create mode 100644 backend/scripts/seed-country-blocklists.ts create mode 100644 backend/server/api/admin/curated-domains/[id].patch.ts create mode 100644 backend/server/api/admin/curated-domains/index.get.ts create mode 100644 backend/server/api/custom-domains/suggest.post.ts delete mode 100644 backend/server/api/custom-domains/vip-swap.post.ts create mode 100644 backend/server/db/curatedDomains.ts create mode 100644 docs/concepts/layer2-country-pivot.md create mode 100644 ops/mdm/bootstrap-tool/README.md create mode 100644 ops/mdm/bootstrap-tool/SUPERVISION-IDENTITY-SETUP.md create mode 100755 ops/mdm/bootstrap-tool/rebreak-supervise.sh create mode 100644 ops/mdm/profiles/rebreak-iphone-protection.DEV-removable.mobileconfig create mode 100644 ops/mdm/profiles/rebreak-iphone-protection.mobileconfig diff --git a/apps/rebreak-native/app/(app)/blocker.tsx b/apps/rebreak-native/app/(app)/blocker.tsx index 139c892..0918d15 100644 --- a/apps/rebreak-native/app/(app)/blocker.tsx +++ b/apps/rebreak-native/app/(app)/blocker.tsx @@ -78,15 +78,24 @@ export default function BlockerScreen() { const urlFilterActive = state?.layers.urlFilter === true; const familyControlsActive = state?.layers.familyControls === true; const appDeletionLockActive = (state?.layers.appDeletionLock ?? familyControlsActive) === true; + // MDM-Managed: iOS hat einen zusätzlichen MDM-pushed Tunnel-Provider mit + // unserer PacketTunnel-Bundle-ID. Detection erfolgt nativ in getDeviceState + // via Count der NETunnelProviderManager-Instances mit unserem Bundle-ID. + // Konsequenz: FC-Authorization-Toggle ist UI-only irrelevant (Schutz läuft + // via MDM-managed VPN), App-Lock-Card wird ausgeblendet, einziger relevanter + // Layer ist der VPN-Toggle. + const mdmManaged = state?.layers.mdmManaged === true; // "lockedIn" = beide Layer aktiv: URL-Filter (echter Schutz) UND App-Lock // (Hardening). Family-Controls ALLEINE = kein Schutz, nur denyAppRemoval — // ohne URL-Filter sieht der User trotzdem Glücksspielseiten. Daher BEIDE // müssen an sein damit der "Schutz aktiv"-Banner gezeigt wird. - // "lockedIn" normal = URL-Filter UND App-Lock aktiv. Wenn Family Controls - // build-seitig nicht verfügbar ist (Distribution-Entitlement pending), kann - // es keinen App-Lock geben → dann reicht der URL-Filter allein für "geschützt". + // Ausnahmen: + // - !FAMILY_CONTROLS_AVAILABLE (Distribution-Build ohne FC-Entitlement) → + // es kann gar keinen App-Lock geben, URL-Filter allein reicht. + // - mdmManaged → der App-Lock wird MDM-seitig durch nicht-entfernbares + // Profile + non-removable App enforced, FC-Toggle ist irrelevant. const lockedIn = - urlFilterActive && (appDeletionLockActive || !FAMILY_CONTROLS_AVAILABLE); + urlFilterActive && (mdmManaged || appDeletionLockActive || !FAMILY_CONTROLS_AVAILABLE); const urlFilterActiveRef = useRef(urlFilterActive); useEffect(() => { urlFilterActiveRef.current = urlFilterActive; }, [urlFilterActive]); @@ -294,11 +303,14 @@ export default function BlockerScreen() { onActivate={handleActivateFamilyControls} warning={t('blocker.layers_app_lock_warning')} /> - ) : FAMILY_CONTROLS_AVAILABLE ? ( - /* iOS App-Lock nur zeigen wenn das Family-Controls-Entitlement - im Build aktiv ist. Distribution-Builds ohne Apple-Approval - → Card ausblenden statt ein sandbox-blockiertes Feature - anzubieten (NSCocoaErrorDomain:4099). */ + ) : FAMILY_CONTROLS_AVAILABLE && !mdmManaged ? ( + /* iOS App-Lock nur zeigen wenn (a) das Family-Controls- + Entitlement im Build aktiv ist (Distribution-Builds ohne + Apple-Approval → ausblenden statt sandbox-blockiertes + Feature, NSCocoaErrorDomain:4099) UND (b) wir nicht + MDM-managed sind (dann ist der per-App-FC-Authorization- + Toggle UI-irrelevant — Schutz läuft via MDM-VPN, App-Lock + wird MDM-seitig durch nicht-entfernbares Profile enforced). */ 99fVrq-e&>aN zd}!)3|HvEV4AB0W_ceG03~hbv?LB;*JpB&ufahQYYxv_TBPAoJBy~wiMuuBTUPQo%nAL&%oG(h~L-}Gs)Fuu&c_@$+!GO7O2l1%Eq zT0*;;N%I%Y@FC~OpFChChvC0!#TCPAnwsFVe{MZxh9S_&&(BLqQqsd$!p6??uDyh< zr@Lg3jhCdfgp?$p3=i_Mv30fgwjr*zigrA37`JkeQjoisyS<+ccaXcAhp$qQ3eWGtl|cG1S(1nQH;bRE3XiFd z9``j*AA4>&2`LFF9x%6$or99Wb@e~O5OFi;}!qJ*c9qolN=qN1dfjHHZ= zILIOH8|>j{6D028%llUe*X@06eVo1goIO3b4@*?>O!UNX)yGdT)>+=NPq1WGq z|H}yc{rLAd{FMpp)qlK zZT#%7`PhTM;LskvJ#nc^;xaNuf6bx8a=nyvJnftvf)9EBYbJ2)>}-|n{?YM22lI-X z-=CD5^I`Xu+-y7?Rd|BL?d%pD%94kJ<>m}#_)YzhSMq-=_xHnp8soqC z!54Q({yhU=-2Vyx8G-+dz<);IKO^v;5%|vt{QqwR{vku!dw|kSASfXM6fCun=Cx}G zJp)~J&6{cf6(~Scv)XuhxgAQ{?jC+V1{znnP0h@?=@&rZ`cS+EgN?1P*A+dzn}_sY z*F*XrVQ2IY-XZbBvi_0&KcW~8B`1)R2u3MeFCS36rxF8cy+A*&Lpl_sAwFQEK>ASV zhdBO0=N!^^|DdaW^B5Ui19?7yG`FpjjU7m@g0$G(e@b`!C)(Z3|FGV}aM;rm_8zwl z!Rw(S!U0?doB?0J3vdH$fM9?d&;u_18-Cf}{MP_akUJRg0rNQmPJkbn=L(o|Sb{xB zYXTmCEg%7ifwT-D1xA^}aR&2(VQ1ve@e}(;TEr9ps5DY2l-hr!X*B}CM|l8XsQyQq zkOBZOgNj#IwU>>L&F|t4)h}uXP(jhTkFd_rPUa>|RBsae@Mxo`9G3*MEK zmX%jjR#n%uw6?W(bar+33=R#CjE;T%Ha>%$ots}+Tv}eiZ~ofa-q|JW?Sl&EAKd|e z|LD;_^@kbk4>c_<4K4j)f2gPf4?D|DOLsx~$muKk^fumScx3J$gOKGyM3&F8%J&|Exm~{*QX}cZdG&4`m!UK|=+;FdAk64v_M%7^0uiX`6}1 zm=@8C3oolKUs=BSCJ{gQQtPA;)m9?4&=(;frz>Y}Alg5ZMmAAjl@9WEYzH9v?Hqbk zY8-zh8N>)t3FTsAgn(?`!5ATI_&J_L+wzO(KnPF`(Z8=w2e3j62JQ5y2h<>d{{O{j zaK~Ukm2q6CIZt2p#GHPDZcgTrh3J8H)=b(gv=DF@b3gM#0Gyh*JY)qB%0dIeN3qFh zAo7um7~AW#iU;O!N1JaWx`{%+%?qz4RsN%n+(8BHn|bk z+FmQzk*XSRHlY>QMGRZ0+k>w9*Rj1ZGWDqak#)Pe%g;V7c1J!+-{(?yPM`5&u6M-P zh0*)ovvsJhN$1^v4%eDrCkD@?!Ppj&@G5vG3vRt|7IVra{$kxc4OjR=^%GBwz~$VR z6J$C>!JSZnJUjet|4rC&7A&2^TGZQk*m4GT?9ttszzUUNV^^XhZlCn^2?eMKM`;m) zl0EWdlh#{XR^Ld5Y%B!(%tm*w4(>^A!Y1JCvKI!Y;5)-8yzCUgY8*;DK+V}ufHsr1 zVdF;pi4xCw)IADt5xzYpOZMLdvwWifkrQtyKwl+U9ZFP#;%dtz_jV~jS1Sd0jCqou zO}>z@U?HOU2q0x!{Dg;()ok7bYF z_s*N_rtyBBC)9)rzGXz_X~@J0c09Xm`7Mv|w70azjW#{#J_Y#E_Z?2y7|IPj5;ikx zlhQnzeb6N_fl1j5>l~fuaDCX`ZScODpAfcKGi@+PeU#RR>85@=GBoT(nZD*=_zAR( zaCJ6p7+6K$MBj|n(gKil9EL&J^O^K^`f)ng1T=tD6TgUtnFC>v@3C4kLR7;B;1kv3 zY69RlS*EWBe%Oam^LGjd__=C=-}%4)Xf+2Dc)~d~b+W=a+r>4sJ97aT`i^Wh1XvN4 z5k4&4Fi^iHoKsDnCxs{cEZ8W8Ou8)eEg>qWoZL)mw9vmq+NPXw;bA?R0JI$19|BC4 zxr5*AvHRNpvRcS)WgY?7kfW3VQl6n94f@^Nw}z$y-f9U0`Vq^wKAUCP=6@DYI}bTw zIU;^dzj8>NYjgQQ7SpE4f?DVsd|qcU_@eGQZ-&-IR1Q^RvS&gcymfyT!v$xU>c{;e zV$UO4h7WXY6A5v6xm~N;_>PvV&o4|Qk+0Sn#oyAL8)jdcLJU`Xm|UZ|st~H|7Du=x zdft#(f8^bI_fo$6KtpZyt$PUrdY;)~9QS=fL>RX`vlAtB)vh!=+w!3US(JO^2YN*R zP8{GfE^T=Yac40<3wyIwz3(J{9j^mvc`X9rV1*ET`- zi83#TJW<4k*QxOdvN3xPnvc^x)ss!`Yg0k?_2%sbhsK&`p|5%)Vl#4{s$CR_PCZt& zFI-#TBgENX_IrX@nhV$FPpVq_L%ySBgpS8#rNybMQTZu>uN1s}2)rw`RgL6=(1l*s!piEENS6xRQoPzXr0|`3dUPbjS_wz-xIS}B~ z^>#p&DK~$)sh=i42OEtB%!R8VFEiKpGZ0R+H3n@Gg zigjzAjT~{lcx&d94WZJQ4HoI`|GcQug$yUT5Ld=7DPGbsz^-3Q|Dw9jaBybU zOXRI%#&_XY5R4m`rM_dET|>ROXje0MGc?9~RB^cDfGbX|Xv_0mEjZH(H=#_Q%qP|N z3lHZHy|il>`^ww>ZQVzW$CPj5Wqy+R<47qwPb&IQQlvptNyPiu*cncRZhgxa->sqa zYFKGQ{Rb-)0MoS8GV>1W@=lgF820#=KmQAn=!7Qf`n(nt(7+#5U@&dUC^E8l#)Ypn zZjJQ_BCj{%C&G(BGrjnrfvY3Ic{FQ7pJt)4hH9ZN?3H`;Jt2Bd&Ekaij8n?&(?Qo8 zght#~Uqt8dZKQV|NOMbQR9TwLe#=9%;8~D`as9PR$;>HWsZ5~KS_59!)7+VT=K&` ze!Pd)n@{icYv0S2U%qp`F>(6Y-9qdLev18miSFqqSFO|#{!dv`Ikt{@>9O}jTD|67 z8cmuF48eicUf4Q_(WC&5u@A+ntWMA_@=tBNPm7K7Y~JJ4Xfk_u?Zi9Xyq8PwEBV^SUY~~W z%(Gh$XpJrVyNAX;QPwyR4tm{g?s>wcVmvj!>yf`)Nrt_mpWup-$rl(eF7HX*g}@!j z)61pHn;en9Oy)vS-W~d#2RUdGij|vrsN4~;ynm}PuRb~3la7@5^OuIm`;Q81&0YH; zCjIkVA`7x?F-iwo`QZCLx_h)$hD1d+8KooU@Sj~f#mKAN$rPn{ON2I$U@AL;!9=y+^WnM;<&naIC z|Gacy3EBB^`O->b@3qbE6a4M-r)ZYU_fQorVDT@X8cuNwZW?0tQ;4!FYGGdhBhaS3hP5~0uPIs!TuEE8m_ z!G*Oe^H_6kS3WXvb%?Y3qGrO)&jL=zW5N2>)VQl9bk64&fXjKpdiJH0VaxJ{Rla8y z5-uG9w^K{Sr|kKYw>O18pF8Yf+B>m9yTNnP3NA=B<0Bafw`Qwh&N3n#0|{!KK9*>Z z8uZA*E(jiWZc=?kf}?E@Qq6+!mv91$oHK}6*0URo(0vgoMsq9w%9<`cWI!`E3oV>@ zxOKb$)q@j4S-Z4jb)svGvkV{%^E6FP18$9J>Zyid7U-~7uommj=a~BV zvxsiHd{ty)pBNJ@z*Q_z=A>_uFu=Lk1j@H22qHZ-n9ljLIU;sXjjIr^y_4J681H^&romnOh<>@^x!ui}jB0h(XJM?*$oFqxp08sh zwvWG*^jwU`Vhw9F?eDIy-b+?aFq!Bas#R&TctWyA;THei72m9F1c zWfWFH-g(r=RyVS<*IUW|dBg}0Z%jJ{nB*oY;xn+HV_PKM|oMX^UzU5peyp}OYE}Os0GK# zbA(@$e2`+q9xwY znJpk?#^RWq3sS5xN-MmeRfD#;&|7Hh8~YvRE(cn__Z#1SHP5!dPx!Sb$yu!wlCr&j zzHHj;toZuiM1Ud&K(R1E$s;wl-+L}N?{ie1O`atLnqLv^`tj=1QTl#yZ`*~^_Y{En ziS{n5sb=or@aWTuk};wZr@{@CgWZ-Bs`vZ#_%PB>#Cpu@ys;n3WAK@?xLcLQ9amK% zA8uaz8vN#HG-@h{WzT(D@+s^RA<&1f-}7bBagXGV&~A4@Om{|N#)iNU!5a~V9Fkw? zoZcy7d&OSu&do{zin|uKZko)ul!r+^+>>KxP8drTV9@d@PhEx-?UlCS>1B#Eq;uI$ z&wD-F8VV$9Lf^v~$g%`mY*=NS8?yQR^|ZG`^LH$03a@`Zc4mo+vuRdwij~aZ3_bnc zLhmETB5n_V&~hP`t_@mVba=KA^k%W}qKOo^Q`CD{=g^$xmhewnX1L zhPI(WS51vl3LrSNzJv^HGvl``ge+;BHb(9u7HU%D^=sa(XZwd~T$l!zn)l|KPXPpqaxaeJl&Xv`>E!+Z_iBu8tlDE zqW$|Oc)4KRZwsc6B@*I^W+cJH-V7Rbs~KoF^N^H_RYT&Br1|t@-mILQrc5yzG?GqW z;EHlikZe{)#j`Scp>bgXq>PbA*3kjtnJlCk7~Bm%uL}vkn+{?>RoU(|9}*3w2P(opBpOx>RNOapXO+cCYhSU< z{$1Qj{fO~&Z8fSM4K+CA``!Ddv5*%9LhaPN?GlWzXgz8ww0Xd|9Gc#y%&-D2*MF8> z?Xo+&({W~O!jYVDr{+vCjR?pB}FAclyurWaT#Jv{QfNWD!uwpIB4%i5u$F zvb<-omg#&Gy9@-_MbCcqKgH+!UgBI}(Dw zaYRH_+W6@WbD`Y!i&uoQA$p>83JiME+Ny22Al^e6CKb@51I>%sau`iHG-D^!r?hSk z&go}%U{l;L^CUlqo+1zEJ`1?LwTarI!@QIq8$^`3qS`g2z^?_qFD|;EvrS5B@}d9_ zIphf@bFqssJ`^qwLDc;vwvp{`Jf&IF(D)oLVNUx>WLrgaJ8e3!wTjFcbau1{D^-(A zu&BS%_*iT9F*#1+9OuP&rLo10)$JKtd`df`%<+d)j&B|Ht5|2u9Fy4ch}5psT0OKpgM$eYtu)L*0j}^9tfoPRL|~xDVd4ZwY;o*Py?7hjLlM}wysoC+W6Nvv*xYBfHy)#ub>oMoo8Dzu~c` z3L(MirEV3-flHO~WW?y@sdX6jB;qZil&N)QMwcd*c}k;FAAeMHkE7EGK8ztA5qhK_{ zbs8Tb&Z86JSsg67ZhXS`C%kE5B9^VJ1@I8*xZJpTU&AC#2@m58qioL0^~f8G({8Dz z&PJcT`@N&jrWh|5?uo-3+osAr+e(WFkKll_!9u6Tg$=okWbGE%#vqFV1qk|}LKiwe zZADdQGRw%`>T*(Y<3c2UoTv2@90%zx)>_$FAg{X~d^<6A?zC)4*QC|=`6s)4V}ltb zkD@Q>q6?5elLp;9B(7`)wgE^3Xioz91vdqVP(fyWeDA^HqVgBwH{Ev8D zzK@m9Q}>_l)Q^2h!(qqNo_yG^c*CZ&-%9LYzcH`R*k{e7W$i-2?B-Kfg0=(_l zWF?uJED#wJeQ=N9ES@1bj=SpOk{yfV-o`dao)*0W+3)bPjoMHb^60&&mW-6l;UeEH z5DT<4S#T*w<|2o71b$8bz=%Yb=Ts!zUlqI#e{PFNa@pSc!PKqG6K^Fb*(3M_D{JX% zbkK~TchLh%98-q6)3m@|*(( zCns+;^E`q`tcOwO^NlT!6c{Ecs=u?W7(KA)>mryNymCla86Ab&k)ebk9YVy+WhRJo zhRCGGle=GTUKBj7j+*pHmyrr#BQBG&$()#QqTkrQpS5*Tu}4iMgVe{KqrbAb!lh5p ztG)-85tigH&=bm5xLMT(@N>_aKeZCe(VZvwdB;J|s8r!{U{%uvA3(p)ChtI#pcra~ ztJQlAnGDXFH3+}|<*UYqMt_^>>@Wk_apu#|@i@Fu{+)NeHhB`-Mejj~v&lMT?Vbm6 zCjE}+!Yepi)eE0d^g`uZ3J|iq{FPV=>RSb(!)ZVF1D_M6N*i(`!3T}z*$p|7jo>wk z5g)LxsIa1`eY105S!>uP+{wjymlvQ{jmQ@c>Ys8GK3Nk3)4; zGLes>mW3RoHFZH*z3$=&Z&NmP4i*i>Qb#i+fRVb*LQR5B^S-vKDLOd=1OsUfA#$Ni zZ{4oUBL+rgFCajVm$|jD6IHEqRe@0E(Pb~^Ej?OO5XxJGBr>$h4c~e zU{&{THD#mC?+AeVns&Q5An%mhl$%SP9_~TM$|69=3h{|)3xeDM)q*>QAAI7b4cu8I zz*IA)#HUW#?%JRM;w;N!aNj|vfPE5s@MovM)GIw2_chCmsvvjtZ8qcFb0!S$7`Q`x zKuJFg+;a@U1P!70Y8B~aMrq(UoMn5$rBX2Ayk!U#3cs5iEAV;KKc1XMMGfzMCE@y`rVjiYU8>YS)C5K2m@IO@#D1CKv8} zgaV+GQ3tjZAS=GXb`mijN&_XRwt(sdJRbh4>KO%SNk`$~pFV=16T+Xpa8;4%?BbH- zs!is*Wh4*&a=w7;V>ezmzf*ZHro%$ph>|aQowsa)OGZPxg_`&2_L=s)Qc0CPEDPwI;!ms8{-tey()z4k!|OX!6_cyAuod zehNqJ#6I?S(RJ}a?D5Pad_Nu>Q9>9@?|Vf}Xj1@z(5d98FKj0p-v>DwMOgGp%zvz} za3A>0+{$G8wP498nz@-3GdLBFfZ7vs#TwW~SL>cR^bCKm!=XCs@Vq={$8YIx{@mQW z9PNkc^=f7LiK2yFXvGa-8Iv2sY7byW_byLHh;gStr8swF3h(&SP&eM1{Q`NDW%g^-IHGXI?Oy!zTLC%a3s&M&bI5iRdF;mVW#mO1C?7s5fBdkbvcJqR zM!zv~Y#{D~Nk97QhsM$oH}`FEC8O7)E0u~AQ$RCmzNBf1sX6e`itQgO%;6^L=ARs1UI>fM!@WX|4GgKtMG z$3GFH@RARdEv6@4iic|CT79Fvn`L&_{t3QAA738TKQ=b?(F$GPhb~v=$TCxn`WF3u zs@wJF1`|;g$Mo#N@ywq}$n3g1EhTRfxSlcZMfIbXWQ*vp3`B1_C7-QOpL_IT;(A^^h`|8yLr>^qIt%~WA3S_<0cjszQ2gE51eMax9|j)6~B?KJu{aw zV(dedyNWKdeC8Pkm-t$-2WUAd{)C2anPHL|pt||Zvlu4boTgoQZQruC+7ImdBOxCb&x0z=u~0gK&Ga)D zrc(rzb`WMQ;%II>);Yk_Adg|)M1jw*m>ALaDo$ z^sCnMX}xTDEJwyn{F0JK%ePdHw{vj}5p8CADFCC(#ORp*{mkbbS8E=v=RBaH8RCw0 zA<$zzS2K!(Jifaisb0JLOoe2B>v|_Kd%}yAWxQA=$g18;-Fsg}$HB;8)eB-rqC3l5ewKOW^Cef8s>tuE1g?bLxK1z5gWzG63)Kx9%RSiB)gptjzcZ(mzRv|57ZMc!8k zQfLM_v2WS(DOv3W3U`eHXkIH3>Uyhh&AeiX1YxZnq2uuebHhd;N>`mNkd+m_z?akY z*1!+67|6c7#04UIY4Hx}ar!j3G6VTQyOIo=-Wx=+@|Q9tG*c~8(P)|%DFP2%#0FF? z1Dr2vYR0J+!+Fy62UOQJHQQytr+-!w)0Gz{q6Ajvke2EWp=Q0=UgWfl@9(ZVKF{<42i$*aK}QB|O{gE<5E($j+#h-tRhhwB?u061R(i!VFzpu2 zXu298ApNhlwcnO`D#|D+Q&eZd7(FC6rV^{tRD>3BKh&)Z#Mr#@BJ*s=!}VduNI5ae zI)Ck6QXZJMjn1~Qknc?UpiTj9URINciBKvip%d3Q9SbT%5Vf$w>jM7G49s{gylbozTE37_Fdk|^9(g@}y`-+>{+(Bt^~vlm zd&VKr4h;{{Wz^SMlXFwY*ZtO})*mS&u!$BH)HbwhXy}f z8PpvQi?h?%KR9rD3}5RiL=2ia+`IqDE0+C!elDcJwYxZKR^fZ$nF!3b+f7A#k{fbt z`MCwbu-Ulc%hhW+%lT0pHg_JMgoT@6<%-_Bn3J<%g@u;d{=S#bI`jLi^(l0(UOqW; zAdh!gT5e<@>U%YYaI)cqDS&JD9k@ABM;+6x%-s5GsKeGCugJewxtEa{lwz116e<=F zjp{~5KG)i;15x)}_{Z0U_ws|}Eg$W#SR{Dn=SOSv!rG)IkH4CB{DAo^KC-*JdM%Vq zl4ZGiL9X-_({39YwZ)9Ez;-}09u;a$9e+vzy5nJ}@OPVzMk)6&#H6G~=f0;_?4AIP%W563j=uhUdP!O+56> z9o^Z4o>>UmKVKf=Rm7Jt;Cnf&^Ji2mbX>a$#mrYSFgbN|%%efyUMc?lN^NY45c|=l z*~^;bHFt7q%GVyrHJkErineW!1K`7q+Mdb8SPb7?fixbjC`(Hla` z+qaye6G&JnUp2uDJ0_?9vA(Kp zF`IjCeVzC!r7Eq>)-mHmKQRw%qaBP!eIS$@_W_0d@GNSeL}M8deNfttoFyE;;dzn( zy)wYZ>dGiLdSt(oErM8xpW+bdP(-rDnh22RJx8EfK{lFo_AcoV_uWPU{k-Rzph#l} z1vrzm#9xkRcei1|Ua2bni6s|bYD8NL4d1RTbn)#dTKQ0!cyvD!qr%vZEN}hls3Foa zkc`ix+g0P8_H}vpR;gUAG%*8fK520E&xQQ=HKdr0D|r72?qWuy`T3oiLw&?`qzfZN zUiGlpQuTQTE^-O(R5rpnhB#-bLx>3-k3RO1i&6Q6!Qgbiv@q-t>C(G=^rE&p z2v9KyWrZ_<09bQo0IQk+qh^Gr>U-|o+~(hamtn=Ad!`saWGxF^Ho=ChjDu16-3Axwgi=7yGVz(9oR>Thrh1h}Z~ zN-@GxjwCWrx5Olg%Q6H(2F01O&?29y6B+1W;lJB**q;BF5l$H+VVd5$7YM!o;(BT= z+o_;HO4!sg=TF^D?YU3JYgE~{&ZiQppgs@X7Z#mQAK5;)2J^-qWE=#7I*2er)C055 z#579*Sjp#z%)~h2NE>2Pwha}7cxek7O&EtgFeUTKwrf*gG41+a35DjkSF6BqQT)QG zATImr@uqtvRm8Im)dW1&^K1}rbN8(7v69-QUFqIHSDy74>1*)Cnc`bs5o3 zTX3Uzx*mFLR)@De;G3Xsl}Vh2@k3&X(Eb~4leJL%GM5~#^m>4mbIs@13iEtuzwIsV zv0obKU^kf`Ja5uQ@;c=}o=+I*E$B_ofk8!A8Ww_~Esf)X zO^s*X#PohybA>Av_=#x7ZO96}b^8mVD9SSjhPc%5AfpJ+lf+S2znN8%~pl zp{IzF`KC^6pL&a(6LcV~wYH0G#mzn?F}E(hlH!{^&$8lxJbzbefIKoIrj{)^q$-n!`6m~J&f)oYM4m{^N;ZR$gAz4K_L6~w4zK6?bKbsn=tGu~9B%*dYC9r4f zwtjqABvMqSPGUeSm-FPPF)MdWTL5NC7$bhC#Pw(1jiOPw^yC+2L(aZjFBmVu6I7lc z)((|9rYro3bX98lLYCeY*ITK!F5!Q01x4ycXuo# z>FAr=!u+oApIh)V^4J^^44>ulGuZ}@_Ut!L4TS)s;Sc6 znfhA?nm$O=7f&A>g|T{4fX+hO>CV-9cU)_`K~SN|h>1zneK}{ckvs6X>cY1%Ul0CU z17Co?^Br(?tS2+U_hQ#a)*z$M&1Z?0RvSYHHJAM-Y-cyEPkzRkrL&bhn1A|XWt6zH zsTez9(rOYC`)G?3`Gx+~@~%OvU~(i{P02&dd%&0N0WxW?imegFB^wuT<4sC`G@1~# z^MLfy0dJh^j@5lMYP_>McG8$U<@HFgk4Fz^sR;7ia8#+@n>tQZZ<)qzZQgA5X-6an z4j?RGKk_ggM5uMW*@7t|xYk;3tRiC$>Ai^FK|Fw;^CX`MD)RiY?Dups&PDDgQ9q&$ z4R7SSrJ}C!hASbD+>ybu?lqReb~`KZ)A~A`E*4r8;_>8w3Fsp8;;)Dxw*ritO~bg< zZ74v@*s|gVJ1)sh4EN&Gb;o0(vI(g8!O6=|&SbCAd$-D5XY#9hF3a2Recj(}vxeeO z9dH_U(MK@{+QTzu?u!qjvzZg)*y&wCfq+^%4v|deC%q;MEFonQT9g73V1)@aT`vb z$%mI!v$2|6yhW@>>hHfIClR8K;w79D$1vA;A}~=XCYj3YA;*EjpSu=YF&BEgSjUat zEtDD+@)xCVt&y!_GaAm3JAKS8$A4xpd){}l;s@Bp4NOU>FIE(B zgmBRqPfK849`)&Yr)z)?z+k=Tf{Yr=C8PAP;u0pEB)LxV(4{#zD^kiV)DV{tq&3`~ z)havC?JdCA_macWOQ*4>^AluI@U653wxoftuT<%hZ`UQwOJPN<+a=~}zZ91ig1K(r zFVGcDS{O^8SS~asG2Q11_7J%DWWq0AI>2@^E#5iMRE5M!XSu%X5Nc9JB-}F-P z$K$i)khTE|z%vX=I8vT!lEh@hoM2>r@>JaZIoz3yhc!%tKPiCLTa5LD^*P){s{wIk z=3K%pizRs3nlf&{qhmuRm^0w?k&UD%nz&`_HALHYD|N3gA8&b=ujmws`^~M6#gW)s zOCN52QhIu;A3c<ucoOMIv?wp_>?+11N5MaNQ{LTCFl*mm6sl#m3`u7d)=-<~6+Pi@hb_Q)w zK2gG)tOY`OcjnK7o2Wk6vZsF{jh0#SJYZ!amY~)WqxX?(Tqi3RycaeV(>ScA%wXE^ zgXv$5&T9VMx?Hd;8A7H{K{OED@SHxF-hu23y*0R^2FcWuZp{^kfSS?&S)>P@em`WT ztJAxDT4V>cD}h5_VV{2bMU%DbNAZz;RiMQl1nz{#Ml;Hl?}eIOL^*&wU3@(x!jnd9SSq=U8h*T0ADHbWAcJHlxE2D>{<+lZ$OKD{D ztGb_|)zPov-2!1Jz|mY9vR9slOFp>a>Zkq2pz>5 zk3%nI6sw(cs@w_9*tK3>+nm@sDLk_hoW%di-h9#}1#xMiCVM5!nBDUXQTg@J7qUt* z$+vagG|$3|Me!!Bua)tg59T|gE~QOg?)A7EV>s`@^^xNZ(YVpNf!5`m(pWQz?h&6V~3GBjvRls9RZct~AFZ2@q^m{r#f~M9p zWe?kU?CE%2M~C`REU}i=ds4HHDdLl}bI{h1-^(>?F470G*fN#va6@|`_liSQ?{m@# zyF*K?=o%_~3c5;`R=ekV&+%;1JJTe%gIQgloWL^AywR*8?VYvR*J?;rY`LtCuPJUc z??`E;Rj=(mH(!qa^!E)v9&%0N!bH~3I3* zWSNy&_0UR&r{V4_Pu(?aUnlBlD37ax|8P}pb=_pikAB`8%po67bWv}e@3T%nsZ6uu zLV82gnDsoZJ-xwUsV27B+|XgC6Zs9cU!fq#Dq1E4$d}(HmBBbvb~0@7=X7IbOz)2F zIxTkDZpsgEP=MLC9Za)N@zK18So2^BRE~Y6=)mN?iZIOEi6vxuEH)S`ZbAVn1s{dOzgcrMBn4>w>warJu=J+ zCpkS8L3Ev7(=fu{Jkxz|=i7U|oX^m2Z8tJxBBvV`;P-rRFzf2QUup#RS=4DW7xTQR zvTo1!Wz~FW=fte}?74@=cfK^;SyR0+J8bgu5mt=Od>gkO+R&c!{e5z+*;LP@a?JiW z0f}_B+tr?HXPm{YQv7Wbzn7&Ym(mV5_=lbQsO;fuEj&(t`g*(Btt*@#5jI42Lb~MR z6f$$YkJT=|*nPpHBrE22y_tsMx=JVf^xjDIOp(v*wK1`B3kOmDvTp9qu)Mtf7+BIT zWkYZsaXXRUeQ)_526SYQSS*cOye&gCa%n%PH}MB?iVS`Ib`jmvI_G`zdXue5TW_|EQ+D}Wdl6ukS+-E{`;3-{7+ByzdY9e`zs1`7ykgB z^I&=TMdG*PaaktX9YTfEU9)gALBv=OnqQ{?2{II5wH#j*ws&CSY`^ALuW51_n|eg$ zQOkMM7Vn-(lRqhU#+u)F=F?>>mWD;oezGriMIzbT8WwmkXrl%(S<3qy3GvGcSAsGG zVXSfLm7Z8Ao-cl_ zTeEo(n>|D3Z@0e<_kYiYU*25pTcQ?vgN94c-Wba_q5@HXSD>B0vITK!D(Ec}dyu0} z>i&;=Hk?ZpR%`4XNaJysZM7y;Na6Sw(FB6yfmKjmtLxd9v!*c^yBGRiFU}p!j?rKs zqH4l?5q^Y=vyQD9oNe%r<)&cBUFiZd$9fDY6ebg>*bM`-X5LgpvfB%{8WqSIJtnwWwu}Rn8{J`3*>It{%NQ4~vtw?dYB|;D4c&CX$1#f5ot!Vc!k!@q z34x}#sSe#JX=`_jG;!b0D60)BYxU)2LE3RVRQ2Ix<0>?x9zBkxLGk@JvOXh67 z_X;K_#%XWXcJ<^4Ehj7^UQmECdbDgT3jwPWc(q!)_V`0Jv>kV0@Qt{NqtoF%YGD~i z@n0M>u+B4hh>>%Pyie(-zv4*F2A7=WOGSS0(#syWjK{Hg=u1%25jJw@ z?5hnHbnc5gE)(&I%bUwT$r`JcqDgwKM$Om);_VBUiSIA(*t`FrABiR1JlDE~&LgE- z1h)PJHzWNt3P3vM617@0QFpA60(@;nQGm>LW4OcFN!-d31sHar0DDED8_eoc;~CqV zDf1V4H5ZL7J|A>~?i_=`BP6Z-j0oS<26s&eA<271l>JLy zr~H>jea-%U%f+8e58ujjK?WRc69h6`7Rhv{Q6K9MaSP1-6T8pjCiNsL#b7LdDN7g9gGl z423f3@fx5tgjx%bUhADeR8;SgVaTQ2=X!$t;} z`PD-ZZXP^mBoMUCc0dcelRqXlH$3!$J?Htg(gSC#r{Ii&OipLS!FnLgK51V z2yf}%Q8|8vP>+{y)jFerXIbH%=8F;3Qvnj?=s#ezKa$?Vq`rn11v>lC6L-SFogljW zSCW8eb33j<|4HcAXjyg&zyKPR`ro9^Iu#le^3|Jd5gRM$+rOOwCzON6dh({@cH$W4 zJMticbz85;zt!!%RLc)O%ZT;n$3(`UN8^?zDkmAcmTHGuu7S38_413KTC&OtwKrt_ z!%)_E+8@a3d@Dqi;t{NI0#vn=23Pzh!!fGt_7)n%yW3~su<{flD))EDNVSyQ?Q&E?u zP6ip8Jw=oxw`fnDLj1VG(dt@uI@qPC^4$Zy=~E{aJC0(MNKe@VTh>e_5LF;3Yhk{> zS|Lg}h>`x4owmTLkn8+Kesg|&DwuSpg`hCpiwjELerS$>6Kp?|&xT$`JPM)v@d(t6 z#^El*f{ikJuvPch``j6|E_s^Jjq(jB$#H1TneA92qfhWPEC zP0$R4TwfJBZ+x~{kT#B6uz@bj;y|7#&`7YEaUH{-5I$R3XmwtKTBb3D*38uQdo_I_ z9Qvcvg-8Mg?Yqi0_(03rM>;z-p%FS2i`@K}uCF^Xo;I=OaHX~NAj-} zL2Cuew+2?WHqjpmKWvi>dKCg-KaNk_j-PaelvgG{MObhdE*uPHrz|4!-(%#`IU1En zYRHzQC1en?o(NxTLbAiRj~+UR9JBKGfuF=7p#;1Ar5))0t)#EhCOCCh@__}>fY`U< zMoj_i^T;fFC$cg?R~IkNfb$?y4La|%3gv+sxT>i=XbY4QI{wFFDw{+@oY0?cqO1_Q zNBX*;8Xf+j2lNL`Q=|IBK@&~g>XhdWsruK)cl6HwyOPsE1^wSYJ#5y0SMPsYuzs1| zp}Q!EFl36!p#uig2UJZ#Q|BQh|6yEsLXG{uYxI=3V6UpFmN3%X5*!bwc6h9o@K?p_ z$HBvj;*dS9kKiGLGH~@3OayJI##cXp4nzh6YR?}w71LlfS|>%Z@F?i@M%c}+&|dB5 znPoEf!@VbU)vaYzj?#wEu?#(EA8j#tB*-XX);@Wuzm+92_i?TWOLkDXLu@A#8-dpn zbjyZ4EwXsw_;AiPF^*|6=*y8gH!l=5741opM!P!4X~`3WT^;ggJWGC}L}>U zXZX(kd0j8pX~biAsb_SZBwuB!vWD4E3ZJ%1^~-bQ)N8wqEZ?QS`(akd$MGgqFNb2F zTq7bsLe#Bew@{)P553RPVsWSarFvhdB>x;i7V2JG2oYHwJtrltaV=>&=0#`uZRr}E z5t`(zsS^9R=DeDON`c5Di2fkx*>&jMm7NO#j7bNx8iU^Bij7;Y#&qu1oa3;J*y&%O z`Ue%L{)q|UYRsk)4bJ{_iG`(7gk}q~10BuGXyM|;ku}nt3p9e%gTMJQbSP{cVK}g< z8JneVYD60l1@V&&wNinD>n5u;(VE>-iu69Q8|qC`+d$0r0cW;r2>gj`ZtT~?1x-YAd>i~End>J^I4)7g z-J{znkFajHcY$@|S6qfFBa-{&eA2kHX>GOGr(nL z^|+ULh-!cTU2VztP6~!od|U0IS#&5Xf=)|mk58V(8TQ+S4r5N3NTlbYN7OWj?`sXI zP9OciJYBV)BjgMotA%uqhczU%D~1+t_hzq5Mj~?tZxFI~1ZPH3Gl(Eta??19oRAw} zId*xv*P#T5jC7b~Jd)=D+nCORQ3pPcc>cv{Aw$h8 z{hDHSY8V9Q`x5&lLk*DsmD?^30bXQ7%Jiu3MuVr}kD)=kKOi9Vza{H-fKCDOvE~Zf z;K5tQ+0hiPt^Y?J;lCcms5pZsb%VCEW6i;%y8ou|L*?+_JUUD^{P4RaXQ`ysg+b_% zww)DBn+IJ1L7%`Fy))qYWiIsRZTjDH#6AkFXefSX{ zrm;K#j}=?K0bxIm-#>X5FhMlUNWt&p>(3kmdHoYR&UzjYY7yHQq2Wdc_@i^5%*QWJ zFzvXn%QF$9OQ53-kFx6P=ua0ZLB3LM}exV7hk-wetZ}l8?#E!#bY1VsHYl;szpu)^LNBs$dGbMV0Y z4GN%F0^h@fN-CHo9wrWYv8hy0fH$DP`@eX5^LVJEwtrkniX^g2Q^+1=tt>;bC)s6b z5@id?o-u<$_7FmhED-oLDzuzA| zX3m`R`JB%=*SXGh-sigBM-oRH(xYBnZ=kV3e&6`Run|i!jLpEKxR771=?3idYaU_M za{q&(Q1IZZe;8h(sVOJ`TtilQbU?^Gbj$4z!0eBwgHYJg8^VP=Dq1LO@?TTJ}sLJ2%ZOhLG0>bwVt|AT;Xe^ z;TBqu>P5~Wvdp}L>%HrHkiBVZC)Lh~Y&ppkUBfB;>8(ds`@<-sr=?|%gv$8z!8Es( z>4nF9@tYUW>@HTduR`oy8NkdF%D)&H_=n*b^<3aQl+SHXshYF;Q@*Yc&&7U7yB0Lk zcaJ$r3C)QEuFpKxCNhOmPtA}r)+>SS&?n>N!{%R?4i&tYrc$ zX(?#I8VJba5dtGBzx@svTa`9odXV7(_H`W7|0%I>|L3quF!uMeFYu(}px{l7o8S@0|KkFY05>`Y+;IP;75=lf z_&1*=Z4Smt6n$dy=$BT%iZzlZi@a}m;h7@&T0n)ar$j- z<~(G#UFHeF^!B{Y<68*dr2ZL#QjrPuJi2eL=iW;-+j_AV_g0g)xlq;Ko#L16a&AFq zav?ttlwjsk5CX`-4PezFLBR0u({1_rZQ()TT~;8(4D~%2wwh@Che4C5;Q&Us0U3-r zXj3nZ4ovEW$IyRM7Dc^wS_G_b-y>mXk)`}9G{&yqp-5*;l)Epf)t+|$$)4T#4;1Ah zRi^UJ?eANge0Y!e3FIw!5xD=6WljgQMI*>*BMaW@RKfnoN0EOs z{7rcM5ENZxjpQ=blc)eWc(nc|2>s2mf47(50sB?T{{ViKJ`V2pKN>;o`{RY}DrkrR zN6>ye`xnsC5|aCuzvFfP{)51`q&EM#fbBnQ!~E2J4v)+%Lrnt%01Ala7vw+ZG`$Wy z8p9wuDu8DpY@hqXwGkB6c@8fB>f3+!-||{I$YpvKJomu=&wsXB;DNj>5`}rP=eaVu zKvefWc?Af5jsrMz?S3!i?Kg+?eha*d0N>nu!H!_Xz$J)b>1kA3m<-^Mxan1VIRArD z>UB{TH}}ZgFfp9&YVi*i#;;5RAVRu=XuQqDU_WEj^fsDn{nr6kB~^b5c9Y{02YYn* z0q@5{cdLVs6>ct=hZlZ53F4r`iy8Hqhjh4h8Gt{KCXk|kw)cPe|5thbhbono-tVc# zeaSO|ZPE<@>9#5~ZU5tTd}(yNm_zSyP*;l6Fv0LAtH*hm%F{pe!T?YC=lP7T4yYzR- z?oB3~{0jpMmEq_-HjZ-!Kdb`wOwTd69hIE>c}ZRWbeJNe`B-V#;~d*CIYu*<(l^D2 z-+Z{PE;%No@thTO_6JaZO(2l}qpgMi%lTdJgBu5^8vBd10^+vYz#Np^wp)9-s%}1G6HX8 z1o|oKD!4q~XD~^0E~(G@>bfc-#9+xV+Mk8^lx!Au^6<-ks|Yi$JQgNNWy^sV!F~ty z-$;qsewDoWa`|A?h*9krS{8$Xl4UfdiM}76G*CrS0BCFf;sEV{3+<%6GYKO(eQ~1X2Pvr)p0f)Ya95mR>ZApIDHio=hU)h_D_ZEAI#h z??%sSyr73l^N@e~krbS^cak!8R+e#4KxpNReTzGVe(xIyahj80u&s>9Dl;$rdML(< z%dceAn&=MaFM}J9L9{yiFUd1WW<-_PX@lC7mCQ2+o8H$YhO5geH9xa*TU}aaH)NoX zuHZ3Qmyxqcn7U&=ipf6_ENL&Ch244EvnRbq#*p&TOGbky5EHOUWLIO0dT^P0F8}Bm z)_`=e&GjthVs`^gy46xct~l2E5~ps^?^8a?jC;%ZEl`p-dC-ZXMdc!3y33>;ov|}z zp`~AiI=*jVbtCXf;)nA41s{l~WSczRWr6I^l!XWX(@e!KN3J2F<}yBDj@ywN@b&K@ z(ZAxFCx0z_-HUR2GGlNm<(=c5MBR`2_Dg)fAO9-R-~ioXO`N{gLgeiTeJppv4I4T! zSs@ay^px6|3i?m=}r|I*o|HJah#*Qu1Fsat8*;o!xXn0&`@Y zi2%8SuCAan?jcpzs*F^Kup3IF>cv2b_M_+wKsU{ z+9%#vg{k+rg`KQ#SG%)#`~0N&llMm*R+}?4>WCq8(a~)RR7E0t2acMQp0bZ^jCGDkk0v=2^E?K};8$RK+#KI_h7mmbt~5-EVpG~W-60{$9G{m=X}=VLuy)YPP` zs%!B7VHj&2(baJ1-#( zKi4|N0n5VF?ov@CaeJ z4guDIWOj#~&^YVvRzL;|AjCw7=p)0b(0mIJuO=F(T9nKisU9j_kA8}oEdq;2XA}om zoihJnC@y1R*u^}B)Mm8HB?SuP6n8+mwDJ6Y&QV5-vEP5NpYvip@{w}wB4cAg=(l-@ z;jR?*D3A&nzyjA#w5GTCnb7zHVQr(6OkCDrCEYO5jfPM;Y1v9i5UTuWPZHV|w^FI? z=Q-OneQK8_g zvJ>iMX^LzUuelq-)R-tATK4-jsB#eeM)n-L-XBn z<@FXDR+l{U;Vez$tzV`v6qI0&jKvT@+|)fbq)F||?h^7ao}F#l^XH=dM*5}*rQG9K zRq9vX(;d$td85|bz#0D#lB^3iA!c_$c;)5>P!YAb>ZvLavlo+C-gM#9W1;l7@|Ej9 zn0~d^nPBR) zjhiM#!R<`W)okcQe)x@l;5#C8#mc)2HdRLIrX|cq_hyKge^s0mvk+K{s#6Yeb$-Ns zZNO`lDDyP(w7b;hvzNuW)|hw7bP%?G7}_sGJ!ia0@gqCzQBbbal4{;_6s-_8s_8pz zi;=Vx@#LY0aX!ZqUz3=utj)L_%kW8LnIR~lL5)Eb*>M?4PivL-W86ENk6}a3rorl& zr#pn7ey8^tZ)>meJ(Jaf|Udfowd z%N65;g$IFs%ym;6X!IPgh%u@MQ}PI{Skm<6>JKI{EJM-aNcxq!`movqJni~s@9qHd z1*x^7th0Rw!u*+0eaxvvvP?_C(f_N|TBst>8Po0*F_A9pCU2VoipOKPI z51;{jjGa=P7S#8L!_(N$pp+BH(AXjH3>e9Kb`%O zg%$}qFcL8hpy!%^jC+Kib8jPCg3(x|3GAXynqnO1bvJ1Ghr-lPs~zX=m8!lxJ^zaeIO_J1Z{Nm(Almqc_nw>^E`IeBEZa|LC8S7mErMjC!p!DBdyWO6t9uX zf-Anqv0bCR>DcXR8t6K_<~+Cl+EiN99HJW>Z20%Iw+@uQr-R(o<2HB$0 z71Ll}2UmgaskZv}RcX&ou1x3@Zf)1q^G)9;SXq4I*TcBM2sZ0_*G6Q1If&04M|n6o z94-LES{2fhnO2cUz)Vw5ddy`H9;;i*@Mqh0Xc@YF zq(S#X=kbHJnu(1|d>{Lmcc8N{{-8j-%9C1{5ZQ2P{jo;tJwW|6iM8b)mESP`RQZM9 zMC!wnKJ-G!-09*s%b0rD)4I_68-PVv?iopiSH)Uh=7QES+x(i3qKQ}YrQ*{?8JG}c zAd6B|t-x}6-(#J$k(!E_cdJcs)#hvWc+arkak=zs<*W5JCO?P=8iT7%DDDiFJN_zH zf|#|u9#HkW=Nk<6TTJWp!c0v-zy^Phyd)q-x`lCrP7M!>C=GT)nXWx4Q+eh`fc^?a z96`1lwP^&M1gg7e7R3bTki%yX)~wU-@0vvDQ9?J0M9+GzN6l;>p@!F`1=Q#VEcn6< z`oRFO8b0>m47uSUI_X72^w0~)L%9j)OZ1C2Ira1ytRd2JLnVB-YjJ*LW~-63$6c@0ogL~BS%(%v7mhq)V^ zWY+hRdlUTmO#G9DGMNV!X)=ZAL)P03coQrPpV5iKPFPITneYYWQ#!v3kmGy;eG2u; zW`Y$aZQS2&x>qT-S@p&ob38KjB;q0rgQ;R(cXFXv!(}sUF?C5dh_Gnq*j`v|G)*i0 z+IjBJjOWq?!^$JQOov8fxKxo>poAp1+p@G~%-qR|J9aq=w&Vs{PB7!Kcj>#fOKDOZ z%Y9wunjsah2^2bu66Nl79Jk|0i&k)sf3&H}gO=_(p_a{Akh9r^{s0S9ef z1_jlVjr3vi=KMQU)zX^x?B+?TVOfT*~_^+LpB==?#~RLgT(ql!yj z_8!?96uo#bgfotV@$1E%w~ULMEWMNfWV;sJxOCN4w;8FN<|1hq?J+S7S1-;)I5#9p zBnoxRI-1?8Uzb1mya!l)E~Is!B#`b{SPUAjjXa#-f2EymEH`iO%@cpA2(Uk&`~3ma z-PnK_VO{CQE}S&?GUmPsS<-lLZHVy1h-_L2SFRpw>r`JH_)_SvSC_Ko@iy0Zldt~y z!Q>au;Jwm(D_`B|Ux5}hpT=N$XXzM1>MgR%y<+Y1LP0s*ciXM+>PLF7=*N(3wzQHU zK@as;x^F^&9Z}7^u2O&)1?EM0i@&G$>74S(&wjMP8}TXW6ICS-y6jSqm3$)R`lLKf z*M=p)>XO#GiJSA$J!P11R82X(sL-|29sPPz@3sD&(Ymt6!mTW;_wg|z;hA=KMe>7T zitm?mlBgWfUA)iXn&hOpo9iUI8G+uL&h!K4UafPYoa$$jkRPnSLm++s5N9T5-Nf!llgWu(_&tUyvXo64+skMZ55 zuB-sekLeYG+`=Dr6zS#ULYtzT8qp!++PrO6`ODW&WO5JVknQd^=}CrUZQPMqSYCV-ieN&r$Ba1+d7`_^>%K%P*6(8Jd5^MF^kc7J#Widfq1q0;CswTd`mH_A_SS(SDgJ7|3bL9bmf ziPeLyR)uVU->7NyP$h2+X}dM~VOEV_e*9$5a4m%}{h~LlKpufL3k{Q{9b>2k=Vp3#tlB?|f1OyT zaoMwf@g|>J0?Q+~3@vk^hL{_N%yZwHcvi=D!%pbNxRGkfS5cm-jo5p7H~YQcKRT?{ zH#@OBz-7j`gu(V&jMkb=W#bX(0*67zP?tGRGmnt$TLS#Alh52m3%))UU;O$I2W((s zt)FrWF?9PR5z$Z_ge!L8-8CTm~)VjI0NHiQ@y(nrjJAjmW?BjS5auqlk2+#vCvoH266JJwcZoJk$ z(W%#m{5{H?Xdi7MXBudfwBC75ew-lmK8h>>iBeM!Bxd7#Njp2O&piM=!Nf3q$;9db zuWR1>0R4%}Vkewp4&NBNz0cqsVNaieaH#pr3!=lI+;wrN6EP3YVF>ghuvEyo`J7dFBtPL4}J6$uBM;CV=@NG zVhS7-_n>2pVUFYNyO1es4IeD56A}Z^%4J)pwj-^V>_O*N3Wk;y^{XMZ(aCuC12+u zj`gH7CvM$&8re4!UlB4$md<$Q?GMAI<8(7|Ygq9pKT41Y^dbLux0x4y{lQ@;>3u?x zh)qQA_mjhLUWzPLcBnSUjIWSTpIKsfk}srO_fT^q;>u>0H8er+3^Z4c!yVILvc@*g zcH!sVczLnyOg-gV`ma^&;Wy9Iwnet4a@E2DD!a_{UsZf7v;HMc5km|=W?t7L#17(l zG7qloz2dA#m9M;x3r(b-v%2$iw$@BUCK-s+#~fYJI7dr_!;nZ8^@(UN z#a6QpUB(N8q(0AA=+~~|X_x3b2qdzdm#Q7Yo}h6q$Rs3NwOsDTXn?;?`na2L-x{k7 z`O+%a<6}Y$JA-5iG?k0B8_C*Y4E=x%pNK2|8SuPokKR;LZS1-&k$nEVc0`=`3(LZc zW7a_y*x59)M?0n}X?>kI(h24$E?GH$p{!Weetx(P+DK^U+oNOeN)k7PnVF^TZS5Y{ z`=q5I_y*ffj3zBMR1;lQtlFf>xmB=kC~?}sy)i|YogrB7&h4-}2NI?CD3%sCpB*fX zXSl?`qRjX^jI2d*q>nTKj-01J!ScmCR_r8(RJ_XQ$C(vv!2-|jRC1%qpI^9 zeV&V_0*_-VVFXi8r|U%aHU9Y=q)wwztEyQ&VSTZv3tl+By}B3D^ON3x_E3oRdn$~U zf^k75A{b$K0%-?JXX5pJ5Wy50r= zLlyOsmse%N9Riy1mr~*J{gp0N)Ca`rCEr7Mhcxm&qQ_dR}_V3ABIvX8rIiNll$$=(K}8_G$@ub3v{`y=i}A$>N`S=R&7Z6o%ER(CT@!58L!s7l~v$PPaoPO zgd+gB7ZFKir@j3JI}!AU0bd+b7xJ-sYf+2Qpku6!S7CDx?3S?Nc>@?9yFzKEoKngD8z zU}3`XeHl`<6jQ$DfWI<8&v?@}M1U8+H*hcz;(lspQgUfidyRP^sSAbeC!uML2x(L_ z>=4Xq=$A(^a&41+c>;3RySjUP-mF>oIzcOR>&S_9O9v2ZwM0^8`>2sOsABjl0DANuo$eEyNE47N-rW$M3?0nmJFio2?Yj4z%bpTNHj9j9o1&(FF9esD7&2vsj61~Z7 zVzh4Y_Q?F$`MH>kuZA8!F=SDd?w2iE1l}$vo4;!0trq4|r7qPz%ed=f+BaHIMlUyR zC28_mFB~f?L0}>EHHBi%ay@subB1 zkX!(ZMD_N>d&G!o&|~kZ3A{5xnj=S=&Y|x7EjauJL+$go4&Xz_OX3$=;}SO&4?hWR z`GOiuW+QZ7Bb&_9L^>L5H6moKunxLkHtgQ;s&Ia*C&iMuE(Pn`pZTr18e9>G?_Eby zY!H)HM55T>rTPh6#}+fK=aFQY32GXS;UDN#tB@uc(t|sw z7D5P_Yv=&(!wOn0FPG^u30`YEtat8a8;n0FPJM80l+dNUq;&>+gWreJIzSUgbiwMZ zGAL%$bI~>}L3fCW5yI8PbMZm;H?+1k3T}LzVUj7}vu1~=%ENa_Hso7#kOUaP1a=rM zPwZ~M`sH|QH%1sYa%!Dv$;r?_aWzi56hJ*|+scq7|RRmhbQhn%g#Z22MpSYO52`G?_#Z4P1v(SzG^ zN3uZIEQSH>c6DMFP32bUMeqP0A=C%R2{rD=Sy3?+)XO7sG8-r33s!C4SZ0)7xgfC| ztY=nu@FJ`QSw&6YT}va>Tdd}u7_mX{{M4RgFLp)$5TT!*q1Ye;K}(UP0kD}O0Sx>G z#5iPzfT6Q20AG>V%bN6@`rX*WdVpvZ9E=maAL$F?hVzd6i%*Z?Z~U!m z-r)l#c`QK;fa}$u9;I`rR*Z?^ZhGudfOtlvK!J;1Wqx`}%|8IvjKe8_=rWcbPr&8XFf4>VB^o+Yo0q)+s6z$@#;yX}S8!s4U>cmoG zUAahvWED}`?I2GZg~ONRnA=nUq7o^Ut2#tVy({ibDyp zNXHd)@o&f!1gjD7`+D;(t$O;tLPE32M4gc4Pv~rR0sR7c4nuqNPb4=NL$F3p^{!n> zPo)?J-JjT1p<5;39R+?h>U5_~-$9=8AO$*wDPJu}M^E6Unr8>83b=L{8x=yIq~idA zWw6%-MQ;t`v`rriRGQdBDSKwa*D*f@RegINszPI7$KjWWfpfTL8UjJpaw>*Iw)lGU z*@HtF{D~HW2lbsX1l{F8P9h=5HDFyD`<QmX8snAEGMJUc=d;VFfO9%|+ZXJvK3j6*x(aOqIn{n~e;a<~;GvS~r zwd_=`!CCQkp~n-QR|HkMSa+>MM&>9lo$IO@1-Uom?*w zPgT0P-B(;!ee4?-zGQN)!;t_Oxa!z{xU7OFOpnaWX23A{a8 zH%PXl>yaWCc5ho#MO3Mn%l+#!G!UEX^NKz1wIcJX9UZ`5HK$zdsrCd}E|*23*ejFM zVJ4}7DcPIKHBTA?>lSir;t#`S`QyM&G+89N!-3Hp?4IIH024gVbjK$!7HjH_Pamz% zHM8RUHc%A}X--iVdTHv%38sh2*+;vA45}YYw*gt{6PpZhF#r zM#VKeS}<**L>MFA0`MhCj$!0l?KE;#vySFShIP(qs=9BvDQ^Nx7x0K&=*MKC&Bj2KczL7;Ok!Ki!0LWCUsBN%l^uT8m|ytsQ5JyLj& z&*JY~Yl(m1VyB?2R>GjNWZMgXjXK27s6M45%H!fXH_N!0bqe*T8vKt9H z6UG(?~|Ji$cL+i|4GnkX#^)DC%vV z(Oj!Cp6$wy4Vxdlwq@9UQJ<^A%s-wzi$hLa`*3;wH;MLak*pxMExz+CB`2*`_t5}Z?4Ve5O3*(v!YQXQtvM2DGq^Leqv)nq+^askNm#!VeAp;#VF^MHW~KS=sCG= zC^OZK9Fk8pBAlPV)+V@rGd}(BT$I+<$3vw@r)6YWt}HPm9_3Too>-5h2$G?sMnX@f zQ;9p2kU&nlmW=>R7p}51=NEc&JY_VTH6ECReNyX|n7wdGw)`Wa2Q*qkJk&WP$HOEJ z?@c|2;2ZytWh`KPV)B{yhST3~M8|=c-`2+EzNG{CWU1W#R;>v^-#F5@bIgA)-SlUL+`0NJ!ni?#5 z4XUswH;?}~%mryhR z-19ZaukNySZ?ZUUJ(+;&fgWs8Ak6n@h>pP3@wmg2+WD=yZ+*F6HrLjea=#lNkhFXe z%o4j={J7~=#c(103H2J;e-;xH#T9f}=zJK#^K!AsjtLND6|Xq6n4+#$WhF>qy=-HUI~2fH}DRCdLC7 zWt-GtfH+9KJn;^0P)3ej9XG!1An(^yUsY9|<^@g&>K8|e5__MGrj(Rpye#BFu~P`n zRjk|-H5X#4Rrp*zQHF%Q@!TQccct48-S=t8mM9mS_EU#rdcCCKQlK^}WE1`IGh-ST z$lsACraJnT42I9&K%JVtVQBPd%iwH66NkVx^786qM1N37=N{zgFIC7f^8Fq*Im3}N zMjooFHeEN{k2$eCiwk4mdR1G^3i-smt~FN1^0KT2n5E9VI@5wfm5Jwh z+;lk!g|}Had{~5B;E2T(q9Oq)0A}qs(YoCVp)>^d%%9XzWIaEYSd8>`33e1aOs)`4 z8-17ukjl5igWcew7$NYs5G82=T;YIr{rAUfJwC&vBgU~X-TNQeAKeP>cdZa+*&tK#2?6Z z1nI7N4JfgqTs*PG=#75O3f;f5cH}`ZtXXP$^F_am3(c;NViC3)J7-8spcqlPL5M*N z`5Yel1O?971dv^a770RrNy}Jo)0;$l0yoLg;7V`Y!UMn?-1sH9hoxzm2MXZ5**NHy zJk=67SqcF(Lf2tp0|{Jdu1{dr_=Can#B8;e*Ef~7ovm?mapKZ1(fK?KZ2oNby?G&r<+}}B2_&5z9@_cN6WT5 z{RWSUxCcBF*Wl`Z7<}#jFpQ|LBc_q=xkZDKx!p zLfIGH3&^<~;G-2X&;ECOLo0O%^e^;w3U3an|8hBW0_kc}TkoIYDnE#9T`>zpI6pD#n%&NL%gv@ zBt4>6CIP!O-6d~tt5@b)dVDj0K64gCmaF1yt# z1jR@-X&L^t^q|x2?aEQ4qQ|nAa8n&yBJMq89Ag2Ft3y2%nU!ltAESy0ph-BYNd2}p z3E1DX)IkLk(cfF}j{#1frxVtP_gj{rKtJk6F(wPndMCk+!MY^t=@qer`AhGxIE!X) z+3|7uhfmyMzLBw)Q|^C`zgi~zB+LNPrg4c@sBs8>neh3$3=x)CEA6QvasA^WOxs7P zBQehV*!AHXz12PMkSt^rDGhX;Jyo1MjhDs=k$EjMKHNS#TBraM<>?PBnz`O9Cs~hU z)saE<4E3Erz3j26f{M9f2s${+eU!aD~Mfu^r~VTEbi zE^BiX8MqiN1FqupntG`wZByx_pu27Bc8vF??Zv0ZS4-atIIkrtS@4T9#fUHOrBj$< zq(8M?qRLm1`w36Fajcu9Lqwi59~ag>`O0r;GoH%OmH5>Z_Fa!r*Zesr@+eu0SkMEl zshj9>IQ|8zUqQ8;D>v%ktyJa9`rTg96|bdZNXSSZ4Y} z`MY4Xv7m7P?8Wz0h}uSUjkY(RHhrLX@mF-O99gs{ndpnh#cGHxqc|{g{Pjd7io0_cDzWIF@%0qY*WOLUZUX)ef zxO7zFRB6r93T%op2c=4&pXBn9jc8>w5U+J)v1~dN1NMZSw|75nZGeGS?}WJ6+JxX< z3s!s|(Mza%+(0mB`9s@kL{>_v1&m8T3y1=vG~O&B0)4$;ZOXz;NY& z$(xO$L*|VjlH$Jv=l;{9QZf(VfbR~b9t04g*N`suA1C~RqcaXPg8=?3u?&DQP}23^ zgk_BZ+%TX0#y9C?OWj1qxsp7Vk&frh?Kjt&Mih4U>HENsA1POzu7Zri4*i-lNmOgy;`ED3&%JvdLU) z`gl_3WA*eb2i-%}c3p2pTi++Q`)+s)xxFtOFuC+w&uj#o4O#T%$%_CGq46!t@bcG3 zDW;}FdbJSAbKF9*uB7l!8nH>jslpkT9;FPK`WR(h?tFpd@-YQmEFqeUqWJjOq}WSE z>x?f_LA(d}Sb$}g6a&FS`w<@@T3ad3X3pmF5`9HqPCeNIO!43VNxe&t*ugko^D6tm=7>lK$%}NU8m;ci+D%3vzIRcQsuw?bdj> zC<@N)^|g3o6eDIVp>*19%>7zp3ZNaTyHa;8lFRfuK(haa$MFC;l-v(Cv=rWt1Kbam zyQvB?XsX-+j@|o7El)w{&BgsB;O_hP4+3HWU$fu9SxP?gSR6lFFz%P^8&rg|Xb^B1 zD;|I5XO!Iy4&+@m9;MgH-L?BB@lC2?&ta5aeYy{vEIDQejyu+^M9P^BD<%Q;Oy%*Zg6gP%$f~5Yj#mK{n{zI`gHJI6a6u%Id9LA^ z(->U6$bP^rB=lTscZ$~eTGK%Z|K?cU1H#FC^)rp?L6_PBJ*KrDl_xoaXOR`=c&S`= zB!BV<@dotickXPPx>0Y@^z-t&rZq+(LOOV+`VSq}X{`Av5kdK1lxbUBw3*avh4TfP z`MF)qB?CICy2f&@r=Gs&a<&M0tFSMD`-g$54Q0Xc3!;BJRgcRzT7nA1@`aDdzNDEg zm`fDAvM<<|Ul7Fh57|QlQhD#`lXJ{#<;S06IF1*EmUtkXY~7mFI0pUhL?8HpRJIY; z?>tf98_}z(g`IYn-8-w~wE9H?-_kI<3Cv+esy{jO4cv$v60mEl@#d67rrO)tbTLU$ z1-&mHMI0mKE#l=fPkaRbxyV4-RC~~!5t$}vRwR`XFskn@y@@!qI2mo z^f45J#^IpY=?)|}+~Ih5nZuM=0b6Z)gKv6Wih=Mg1@A+#2Z9_P>O;Vh zXZgQx!V2y(Wb{(z?@1JwqaCTH-SV-QBRa`IJ%HDB1Vh{UGxQ|p0ct9BcB4Tqs_hJ! zVp2KgIqgic8v5q0!m}P0Fd+0oq4s>l@d*|mY%A~VABMjM?`f)n&phO5)7J^VIHQS? zH;p_E%Gt8{iI;7mva1r#bLX_5HwVw|se6XF%885vJ&jIADAT7gH4|&Jc@Md09EVcb z%euIi^SO9c>C3X?9Y--^A0IhEWM}0$7(X_!)f_9NOvJT`2eE&^uzS&^$k{dxb%gS? z6n9Q{!|PQ_EzR&ZN&Uy1tP^6B)0}E&FW6TgQi?-dJ7!`eKaAc|oOOTk(dS$-qbXx- zsGqK`F{gQWNpI1WhByV+lvphzTTY-HZ1;6Y7*G}e3O)LN`hNh#&gNVk39y{0&6ZwT zj8x~gDXJW*9UF*9z4MM*kogZ6YwBXT;DPs36Vlxh-+R- zh8<3*BcG)Jq^VuI1pyB!g>%qQJD2_J*f z^dSJfF^ypEYy$s;F026jY7V+!v?y-<1lIlZnTsCBPkAv!XK<+cY#B*iP`hyGgnS~i z(Z|VRSiat`k@|!>>NNc`q+z!Q9R}ejAm5{Ct(F z%L*8*w^%oS^X}T2uu)Pe7C(8L0@u2U(lEl#uk?lsLFQ)fYg%hw^a@{SxNjhwF3*x1 zdCO$@jg20!W9i$1Wwu&R^dc+Ci&lYjWoHa3vwxyBuBJT?JxNAsBiYIBpZ2yw&G?k; z`umlQl0DTD%S9jdO7(7`a$a1RYP1d|I+4tPusmJH8smB(&mhx}3+qjlXuk*^KSPoC z1mbIgee4xH6COWPkvg*$*Ml*wsc{YoRn*%#P~UE75FO)16y4`Un6!-tXW*V+!z$FNHYaDSpvM^oz>jJ4cPU#z*t*nc*x-NUua zk1gpX_Y?VgYnMT@%5;=W^m^!Y!S*%@Ld&W3#x~CvO+2Lrmcq2&*sNIF(u>a5B-J~- zT#De}OV@cW87#}Fns(RH<`^d*5~#`}$ouS(utRhK56n?De$Sxqr--7vfdWre8pMKy z(4P6Vnc8!PWjdVk$D=Qy7n@1<$WFuwJSK6IdYPhu`?m7^Y{Rfv>-&@mbI)@FPkpC* zR_|LIcMhb8)Tqs#g4fZ`(0gEPU^ysGW-AXu&W{)PBJnO`{;Yjsjib{R$ui%uOflCM zzm*(My!xca_j}XD=boFzPFK#KtJE*K%+iYN*?E=m^moDHQ!`w+@?aEamf^EZZ_2o;q zaPAG(%3Ev0XraJv(fEYA7Z>_w!$Pco7E`&{Ngd6!3@WQ*KKYE6Tw;sml5FlsNs7A0 z@R>sm3H(eBo$`giX+T8TCd%fMvVRGP&)(>d)Gt|1`f4w5!Q;0c*@>`A!qX~t1P29z ztSyN=4MIg<&W(HTL|4~D%`^OdsF;$!p(C>UP;zTDk&EN@{QPDDvOaokkWkZwI|y|( zuL#H60my<2oC?DlS7wy$(ReS-X>{&IOS0nGv;wzdRreSy<7bPX*2hHGZ$S&;0?HF} z)>KX?Uam_{ka~@{gN_HWVL!i4yVj>@pDr%3zb$&h>xfL8LZz7=zff5>bQY}HxDgU$ zJ<=P@P>bZe5K?DkL}z%x&Oz1ueO;Jp&-6<@uGqZXrBlN;CdlXN4b8rLn$t%0Etpw{ znIA<2qq&WO5|eXdV-t~nw~BLJ}$Uu7Z50<>M^Bt^KRgD zL;$c{o3OpZH#d&eU5)D`9)U7d>^oz*v?{zd-&N&1^L*uJqndrzj7n4VcAmsukp3M$ zssTZ^>mVQSXX6X6cws{RBd$aHpK%>ib~E;sY&>4lq) zlx^`j-dyUQb-5`dVAMaq z{j6Mr@mWNOqGbTZpnR(`MfZSx44n8T-!IDIz-~%aAq8jAab7 z{GN~Bb=}W%UHARmuV?vV$>Vo7R zKs(i@&?TPRmOmSP`Kr(0=8~RCRJZ3oN5@E;R9lq#RZt>f9QZalS zvRTQ&%%t3uak_MOHKIg?{pSu{-u^F2jVd)E-5gigwi6m8i4*Td9|U@LqzM(m-7Bu` zxgu|TNwc5k{wB2g%WJ(Qx*?Js$YOYUij$>&-QLjqB!%7-sAG-5m{7gxrm9-OVcKG)8Hltl)zhQ@L z8(kQ^i#n*}R~$AH(kbVc7FF!K;1!eaNSceYPyQOUe)_BTI`Hdn8K&OSfx#xpxlzu`CLiV&w zxA%$R;;t7}_C?$6!js-UzkORdMhqv7cOid;JBd`o*-2Lj7GHfnbZ0?vL)2D34Cl0I$|?I8X6DaH&+oEbSI|tQ&diM6@Q9!ot4dLR!t0>zDsr&M zR@M4RzH?@JD-E{DnuG4*fi`NTUWLp%FR6v(T-(GGq>n+OhNg{N5#WyVS~VsJ;w%hO z%*Xcw6+FdxjO&le?5l!DxE2~xN-D1=$Z5W8I!&C~Vq3mDyz)TDZ;Bj}(<(@AfI1iJ zPwQjGODAI87;~2-Sd?qgf*D7*x~@KY(UfuXd<1Xb>C^O0*CE%Rp&}qmLCit3h&oX# z!agDPQR6WBMVOQMT%lmd=iT1J-0IqQI$K7QeQ)oV#%aBbF!ho#_|)f%vxdidQrOh&%9fTuBen7Dk6)9X{qFtY`6Fcd&*0lH{Ei^SuMRsE zBn{fS-V%(mNsqB#CCxXm2-WdHyQ-#-N8TLUucU0_wHV&=zG|}xX3hP9{KYS}=C^&l z8!k+_3Wkkc4^SnS9H{s|!G%Uh1gKz;VTrT9*O{FTRUZR`TI5}0nr6W99}RavDQqW5 ziXZ`Y7ld3|lD>4d$l0GSwir1h6L{h8#r4{Cdtu4IwC}6x>LO9fGj^Y-x@zwfAJ^2` z72!4aBO@eXKSDPTi8drZa&oJfdNyItcmgRyuX0BQ*dd?SW(gLdbrHhTl65g2=5vcz z6PoBkuZS(X$8I#9$27cGO7~)7`&t&&!ewg5ys3VFZ8wR_($Zw_HMktG(-S{qhsa^I zkYkUrzsEYrl^F^>fSSV67zB~|C3JteXyUU2+HQ=XKchbdroxDkL_n=sUk7N0(!;bS z_iQlY7We^jSE^m%lrbf>lbbSiHRlNZ?Zb#cv|JzRPcHaMbe)1L|?Y2Olz))q7vW6?sc5+4#e zaqcKLe>aA@Xp+X84>&(1)~BA%s7fCFtzk&~pQBb-(lRMsvd-;Nx1OlSxcF&32e`o} zlVvv&du~tAEv$Y!>>P|BZ#h=YlS+D2-Y=KxEuIkznJTlemAUb;OzpO^9UW$6V#l)-*2U^X^Be83GWX-$(}?4hM=t!VnqmzXN_4Mvc?{-j zGWqN=j7{|JEp z>56TqO5#=$qgO42#ehx3dy3y?L^92xtTN**>mn7VV8bKZ70Ih-_c!5f?D z272c!vKIm<-7zHhrD*!aHvs4|7B8NyY8qB=7?*g_d@Gz~=T+>WH=4&`P%?u_hb%+R zmT;3ny`5)~6_@gLkq}qrFB@`mr~bSgV!HXl?v3Zwq@=f^8t3>U1|J_;MMRawnj`th zUzBZ&NU8)@9Bt3BK-RMY+{EycO&-WCv9r%#mJMYgrvz1>=vR-dk%;6_l)V)XzKpQ$iGlKNr2KMB zRy3YN73scQ+Ddt$u<%hsFy0Prt^0Ox*)-`1HBAU)l(tP`dIvMVIfJ1eq9#el?5ao+ z4HJz&S^o|!(68>la7&KI3FD=f;kSZHf=#DjVtZi`6eSdPb(08xUFZ`sua$;h(Y4a6 zPLX3Ca^opVwUTF&(~23`d+WY?l3kDrP$Fz$Kj2Mpwb8ht$4yPRM}e-Ci4`k@1Cg8> zIp%X-zd4^Z*+us~3Njkd)6 zt9y@gF@B~QjCN=p5>EJXUQCqq&Ff(Hps*`;`fodgxs}Q?Xs}M)-3HT{H?=O8pf*!j zqj#fY!j(R7kEfmM3K@G&#TjcvIp2=62mD}^bL{!GEMXiXOg2!_-^#Mz5&q4}a;8D| z{Is{GKvbwRm4+tG1(W^$>L;>Suh;NwTADkpf^Mg}O$p5W^tHdRNzSe+0@8BwS;Vhd z+0Ubxx~lNHBgb=T9%l1ret#c+EQ(ZW#Uhsxi*7xT$YgjOZCHSc;}V>?Grt=r<*&7A zJ6<+2Egn)_e!t@U-E$oQO;j^6k`d%pawX`a?q`~|#n8-2h0Fo4!tC@qT1l-Oq58P) zWqz8rmre6${xegj<#{W%tuXHt4PE5N_+_1MhECird>28{Qx*a--sD!r62o0L#=m@5 zaI)P++GWd>UzEX-5^C1vw@sLuj~OiSJzvskf^Q48=BTQY8&J>O7@hg}2)k*66$@@` z>*mRc&%Z|B@su0&R7%%onXs@J=GT4sU1(z0D&CB`+PV2f^>IsegX%9zL|ftHo;&c# z-2@~~w|l-uB_?`t=o096%R&nkUacMj2lz^#b8}*MIX*>fYAJ&3L~ejPH|bAnVt)Qt z#K4(=6Ir(bs;>r`;qE-NnId%0_`_9(u(%=^*Wi)pgTvstiKk>0ntB(?I+wlUV*bkb3t2dEQu0}KfML<=WBeCZN|&6?od31fD> zx&0=r#vdURsM=|K6JN}#b8QNDHR>wQtyez9TABx`>To$#X@WTL(fs7H8|h1og)V`N z(f>k*bAOo~;{-E4yn(%_^c^f4d(mYzz8Kuv>b^R*Htyv(WovF?@5U}JGAvgLpf3x#5 z#T>ry?H;7|Eb!1zqxr$qO_MUb-2*8QJ{fZs<|S>8fS*J13=uyqA6W0pnU)j5u4U+R7E zq)8y2b_q&i>BqP6@r>J$Qz_Txf=4{HQ=5f7rJq{Bbh@+J65C>yW7hnhJu^xa4JI$( z3QC1T19-~_?@y)5{FK_oQCTjRVjO9CDLXn8JNUvEXrcv^0(HDzX|d^hjf(NSn}ucN&efK zV=w=}M3we=p+2f+@1q2oF~&>mtOa%IA`tLMu=+!crUcMeKnQeMG{8?y91~3v2bIve z>GVpGeU{sUi#M-nLUkxLwRDub0SrZi4|oAGaJV})9NXHOyQ1OM&sLVu4d^&o zu3y@h*d7N@sZWvEz)REQqpJ;a2wMsTf79$lF0=|@Tavc$zfL7=oZof{=!`wKxd38^ zQ^-+ziHp{RT*`S9Rpi&E@4yt$b!?1h>9@{5tMz}{btrJ3(f^b%4Nx+Q~- z?Tqw25SP1D(ky}L()z3)#9|`4&U~s$WngEy=u|CRAMG-v8BC0&@X2?;3Bs{q6rKlv z1t`1(7k8W2*T|C%5q;s)@M%YH<}JP6`|qT2JCyTWm@Or9@;3=t`nAsfY=W-g*p2U0 zo0;3B8XMw*WXV$B**toNb>0oPKY7AM!*`B<1eG33;4dU)OopR2?}NBvG&uz} z;wc)}bgqg2=ps2;yyqR!)rV7mR69mVpGPnQ?A|O@|GOr*hhoR3u%=dY%TU$%JedM+>2(WzHvfGd5 ze#&`p6yZ|rJbB9=n8m|!Jm5aiFKE<|>o7xTPW2k!{i^4X(1Y4&U3#v@n!_G{Iw$PcKP7ypLYiCIkoiUo}L{f(~93H z@z4l#?ggFfb8Npz+C)B|!9C*;x%ywQhhzIk8FuX+b+6iV{5V4#uN|ud@DstE0$G;J z0k;W8d8Dg^C3bQo!Ij^+z*)`21vW|)QMVwf36T8|Y@5Q2~q@Q2> zDdz^Diz@-@pwE;&h4Lv48%q$Kgwzb?0!t$<3?j!G{~;1{B1H$*1tXp^gRg5N!QZ~G zC-@!ekiTwuhUeP2l=|4~XPuYaO{7U|+hF>3^fZ3z)&YA??9NRwVl6Z~cACIMcr?#| z+BEou+7IFWmq0%f<%l>3))5s3-{)0r?IOp*UowH_5;AB;vOyA-?qavWm@USB&GDDA zEgJLs_1e{aEVBj1^$wk#qSg&(6X4*1$CWf`C(?BQF7>pFo~sviD1XeL+MmRQDTuD z`UK5tBxlep92@?(TzoLKG{BrP*!-?_^IzqcYmgTj*HGR-GV2~il3D{ivL3LBa{pq9 zozUHQ1~E#%dL1Q?-u$lI%jA8{10>tUc9uJ*Z@Bk-VZL|<5Id+&wNbrcUq)P_+zhxy zxVrGY*&Ao^_=E1qQ}+uR_oaTizgkENnG-zoC;ok6jV2wv9J@AOO|{A19`KAAO*kf! zFp6RSXyC(Lt7{ZzgT83wgS9%o?&Pbqzxbw0Y<0G~8?vevK{H@o=d9fF7U?{8$aq}* zA_ihG_H<+_7IyQJn;M|=6@z-fcXDGH*iO;3ooJd?y3meaYn)Zq0=Wi?A1k?RnKy@~ zOFQ00Wik4!N034G;QecB$`3#9&A(qZxL<+++_=Ez*Ye>jW7ZQYPn6RaNZ<#3w#;@hI<0115>_jie{W~1gDjyM`H(KwAwE@9CNfV@6X45`ur_L zitUF8Iy;$>RZBzE{F;TADo8fHS5J1EenyZKkcjlWlKU$#YhWQr2n9IvrPPobIJsXD zMhR940`YVEHCoa$P@g78gM3(`+2qP9iTy{<;wb)GP1f@cYajgwX{;NODr2x?wwKpG zx)?6ZH`yNc<1MKvTqih7k zJK!HTo1MS3FN#5!*7+bQ>WyPp>c(2Cb7~OlDt0OlKb0*U{^N2@pRs?R#6B+z3|)ym9VX?rcyZ(Pca5oo~=o@W*GjA>Easx zN@>kUG(+{31LjVD*9axA)2&c&9?=WHjQ;tDssYf!JgPOIO%6PVv6Dlw+{Rf|Sm+{S zV$geCe-yp*t-d_!-I|QtJ>C|vqjvB`)8BebTfy1ttn6m{rPc~m6akKs1>JQ1M&W zT>_T9I|qj%N2b+L5_DldHAyUE@E%(qXG(ixFD3r_eg5rox5r*E?2{d4>y7yu!PZD= z)O0nJ!4Z$u2wISCo@D%?_`JF8JV^8~b~FLDBi6Wh{v(8*B#!OWB$(OA)`e6zxj=`k ztT4m}aoH@tFLmdhar`sOe<4GfPwx#1Ya08pu{IS4=|FIcKlL6yD3yF8)?gf@<+hn+ zX3IDm^-Ft~k)nHSvIf}{CR&0wHw7!-k;OcrtXb$lxrMDwa?XXVrv(pqjXtPlE@A%N zCLp#dP?1yha+VDz|=2%ft-Tcua(}wj6K8EHhA8g7$HGDFKoG7q6EF_tp{&(A4D&v24-zQ;VgjfH1->np_n~P}iP#%j<`P!Jb@{s1TM54$H{enb zZ0!2n4RRw2!#@5?+optMMj2|jE5%J;>h{AuCZm`cHd${>P4$$Hq8%U&TG=j+gp!{` zx{#y3{1GQz*&Jf4~bnjc81ylxEGE1f2i^nQF#*Y`v0NgFabdv zIRY&~iNAM$X**lrXtZ4H2te8QT{z+cf<2{6mx-+4#epwe6k|3Q|3Eq?J@iPX7E4 z)xj0^;}eKS;|sw>1ziP)SOQ6q(V%o#oT5D`@8Kw={{j3sj&6khiO|00#wv+J?G#q5 z9fp@fQsQO};q5#mr9ul5J+3ffNloH_i9-${y*wwVD@HV%AptUfocNymyeE5{0cWcaiTocjOC{0zkB1P zDlO(L`ab%3ppt@&ftBW7O%0Ot*&VhFv%`)VN_mO%{=TEXzzc>>y&$)QlE5Pe`7R=j z1m)8`SOVx(MIDJ-1Cnor8GOHz&L9@xKci#88Q2Fn)+l!7uAce!4*n@Y*Kg#0LL{66 zW|om!KpTBi$oh)~Mb+!zb9Q>w;g zQ(mdqC$s(+?Ub*WkhZ{WxC96gZYkdbVk`8xnVz6a6^I9oK(16K(89DTVi{@cmTpiV zVs;Pt>9<8FmlXBQvs2SyM!IuPmxm!aDC|S9cfLdIW5J4cwBxpN;NWa3jYoY;;(4a3 zyf?=1Nm%ZM*hTtF7Vu46@!QL@#?Ot=s+qogr(WH3Ys` zM+RaM$2WIT#wdpcpnM#39X^%1$V8SOf(1jcI{Iia461#H!)LS|!YZ2%V=^by@~STB zk9nSdGS_|c%}zCKh*|o1=46H4Qhw2(9anFu562QjH_G%pb+b5@seA4s77XY+fEwI{afX*cr0E&hc`e1_$Gr-=1cvidpA1W?KAXw3eq>LcvP&4fCCH7V^@MH_Y zG1fx974ZyQ+pi#mgT3zs!6mBxw`k`MXw3oLd6hVC^4l5-vQPpF0U_{z9omvzbR#lL zfVXTpe{$b=k<4uc?=YYHaH=aUIn&!Av(<6Yp?9ySn7;3om*y_m2{dgGTxu2w%8)7` z+R>Y2CqD*80v^;0W7~&M6Y&UaXiT$*Q1wW4aZ?k6#oBPi+3Cd6ly{y8RW9z~*+kcugu0qQKhZUs(WlZSPeDCb^1i zKH*I2<}!o)gm=3gK@lwQX=4Nk;|>lvNgS_DS`&L=3~$cfxUuynQ)63_bk-t#DH>EG z%L_5EG(UK>5FwHH2u-?I|6#Mz2hkhv%y4##qCZn}6`IULeN}$zG9#@H83f4h>jTbB zH2Y%fz&Juox=9nF&Cs%-m`%qFgrGakEu&jh3AH2l^D7PwW`}-!j9jh?Lw?zUVReTt ztaCGMdSYDkADU8z7wKm$9-(}C=m%4(%x^Be)i!m>wGO{(v zF7}G&`q&yZ8eRXYfYkG)s@-dYX2*_)_HiweJld6wY`pAnSuBz#cK+WmTrx+9>Dltk zG_;UDt)AOS-R~2pu{zMN7Q527R&-gZB3VKE&HSy`4gsNHU4c(bY$yk=g0UC`&4RA* zc1vYV|Kgyr+T$C8fbRH80Kba z0Z9W}*psfsyC3NMz>03$*fLKZuYaQ=)b;I8n$V{^e^B|(Za!I|3fK=6x(0tN9OE6_ zddUIAUN?klu!AWsg}#4_o1u;gY^Tizo(;dR-gS)+dB_zj`v+A6xHQ+`=sHYIVJF!F zb5QU>Fr~uyrWj+y!-T#XOJppo5B<`mNZJDY${$e<4IHfDn(aqDJ>4i^{3a>Q{7g zDf@->1pYn#id@d6(1BGK{!j|c2}BFHHTMTh>(UD?U6XLyfb-rSE7~ux6R`p@epRv= z_Yqx_`IE^cU;`4wKthYs?ZOEp=)WKh^Iw6iXVS^E1`|NY6nzVRc?#H>|J$(*C)1z7 zXctjmH4)HakpC2n7W2vn6KG}u)6zhTsUM0irku|QK1UY!{%{dRy5UC9piJA_JK~u( zp>;2b0?$S(;Dws_*pyju$mjS(`5l*;PJw&AP9!~-Tii&=9~pi{oj182 z$4+ryr(S}i6Y>dG9_A7&hLH?ypH)AK?b z<=GH;o2+nY8>f|E;m4NADl3WFX$loHhj+bTq;S|TcD8NWRn%q$_+RS;b)u{jmVn;a zhI9G+V4FT$7)W==#ZK#L+TghEk!#yR(|7)N9c*Xb9q)nWV;}|rp=&n74gnKB4Z|j` zbNZRg1R$yd5nBeINkd`*{`gfT@3KI3zvpakN#o=g)B+F}dO%1oB7yA0MXe3to^6izs)UV!j77>=9KBus+$ zS?T1gs#myo@b)BrihgQ%RI5IMoK5sw@=k`&Zo%@QmneMjj%kANA}R#tM)?6wiO_Bw zq0Iat=Wc@fO;q83?U2^xXZy4%hDe8043StzDO}Qvu7CkaVcj9dEv>jC4F+bBmd%Lm7h8 zU}~_jz3KDUhPt14izX*JnkYt>E>}KNSaN5R>e0A-R%5d>iu?Gr?4JJCJ)#7G7XKA{ z8l<$v#e=s#mU`y?<@i?WZu6?YCd}xafD{!?1q=1tbD|qjh1a(O92byNx_BybUbRE3 zSip6?`wreI(qeb==Z9SA^jO&wM8vn_SL;?+&J$5_MR&?epOC94e3T(rW$32GA`l@u zkxeLOI1O-xOW~AxZ%e^gjdo6Rh_~a5xaJXoizPAgkekX|R!wbCE&vX10U?s$wDF&l z9T3hbUA96eYe(*uiD+*DogkP3T@TwKy0R~qI9`ZB5d=B6Byl`l?#>XNX6HiF4zK*~ zvAR0$xv#dgN{H2HS5R+0EP8Sy|Q>iCn z?D{dMhyK(rsnxH@aVR?FM3R#D#7A+ab57E+ddo=1S8zl&UHwUYrSIKvPUnEidqUlN z{I>^x-7!4Hd)S6}carb0cMDAhJkZN%84ixk(chL2@6E!j`q;EzzuNf5Z9ZlI?n2bPSQdOCzN!KZJU{fb}vVJ^S3>)p@iM-kIP|)c0YfTttN3>gTc0 zaeXtZ^Xr?nsin(ItI*TOxr_qQ?@TVO{U|}*9e$_N7DS68F}qnRR%vo}JFAcEdJUlY z)8mw=FE*bGYJGZaGCT(``1?lzQezYr2h@e;GmDM^D*n&E$rXQdK5X=G@@~usVWsP7 z7p}h8$)O!1D`+BIEUiF(24>Y}Wd|UMS1CVX*dHjmRRREr=OH9Pz98i5Z{)g7=rn5T2imyf*J$JtGq%eCa>6&3uIJ9VKVuU<$b*FF5p zZ)%z!l^L7Qe@`DDg5i@-sBy>zFe>7teYF7t3;(bfi_hb`G+ydyTuEsoD*fg$qYjHg zxWx&%N6#=3Yxd{23W*nioD*!$M`wq^s!zC&!f-@TR8Z3}G|YkM{&p4WT(gV!g>2cY zR1D4EW=nS%W1=Caww{M~Dh9A(4?*7}f}jO0*TYeUi4s%IJGIt&-jv**h!;aj_Q#!> z4NJEnNwMvM_1d^UN55QWtK8-<4f?KIOAy&8-B4^V3Y!Y=ehwB5NMWF+ z;XuRjCs6|LhDNP#tPbp{j#36^TlRWqXVh;}<)BS(zUGqzewHMd#XPAjwn|PR+Uqrq zlE_*1N%*g6;zX&Y+U-Fp=_oss6~ty0a0$Sr^*%G6Ajii7z>5ip!Qawco&c9Q#=9d>_)m<{pt6-vG z$gObkZQt6xt5ij2QCzK8;O$)E1!F6X1S=eSY4zcXn7CK(U^3L!(-jxWhp4yopWM!8 z6PRcV5)A-x6A(m@P@ix)0HnU#^*HfQrY}ZhnxqSQ`9B=mHA?G>0ltDpJTy0g9QR9x zfwiMiFf^oD-u7&IzGE^DJv}{hCFatV@VhzJVora(#9pIuhH)WC<3w9XVsi(5ZFFg# zbYn+)vsES*0ETJk6py#B-`iHAJQ`ZfC><*?u!}-ZlVp!ECnKICZ$WRsosa~fLXym6 z5B+=u*hv*A3=i>nW@h0Ow45?qwOJ8%=sWLLH@ICtCAkC3v!=yPlCEaafd%+L8O9H7 zir?(lBWetZ4H7SlFp|^LUu>lUK52oTCm5V;@YV8p(957H6jooK;0z6KwC{2w=!3)* zW$;(&e|k%Yo!y(RWymt6*Z+sLAAdFbof*$pcQT2(#(R|^Qn80d+M z2Dy)qFzxDpT{L0~@;piyc<96zoA$2w-cldPRtgcspPiOI5_hqoB*7B}wP#MDhQNk- z<73l5TSFx5(Jr-Q(yIGzs5GkA_r`S}j;fGcp1n5xKNOx-mOv5^d500T2Ar;OsqM z<4?q#LdjKo%1C@IoFS;*rCTl))(C4zC#|iRHAT$;s#}+gEhj0yA)%8m9 zzrr=O^9QN3_4_?45<$JtO0e0b>^sHvF+(H))QKp|->Sc`mqc2;cgpIF`OJ~E3Hs*i zy3Lt7H;DhqwN>OP8+)wVSne{#BIrPLL%M>4cEoP!Z^8)LH@XXFC_{K;)#ClYIsH@s zrW>N%%#VcevgCJ~oAFCluX`X^kp1c~CfDRg%L5PuYywc!5z_YFAwD16X^3vpv~DTh z*NJetwgMG_I-&?d!Xz2{o>Kq+5KYkptd)uH{~{NFqGCpPPE=DqfRIDX`?#jZeNfsO zq<AnmU>hy5> z?m}ld7v0UuF!6#g9FS0%VJHafpkj{ zOcB_`m!oAMnOP3B#sWWpq*nmNZKh0hdoEq!fc>zp2O5gN)&{ZrATcR3AWlYMghyPf zQME@<2O<=0E3)Eiu{2+&Fw^pSLu{nTYphxckp?m`|38rCh@aQ6?mO*ls4(1#33Qpf zr^xz@J|e_pm%52cq*g$Gm_0Ak`( zhGKSDrkz3VN7=5Gzr4!x(^{HUu6gIHwYK226>Y8o;Y)9{hQrqW!fJ1`JvAC*>VvYt z+eH>8gOuh29BK)lk}u8@zBFs{#X>^gX+4?^%y&t)`Fg!0Htu%Vh1XRzLBS+MY8bCe%Y>_gNHC()=E zer~b8h9l)YgU?jkFW-6Ovd0x<*trL^DNghb{V}L; zfeF>~KBEs&!5+zzan`>b{Ia7PLyZ}@V?Hd`EA-wm7Kfk6GDEw?pP~OQ(MDAhY$C6g z9k^KkHE#;%;c3o8Z|GBhkfM&Mc|siqOpFG>|Is+yNjKAJPFOBYA_TcopW--G;2m=A zSDo5`WfHHJM(@4YwV6eU&e$^nra-xV0TO2Zn0qkzo96Ow^MP}hJ<6i;WIV2(ivJaI z_mm-n&+29qQIw=WPEux<4<@k*wvKrU$GF&1xAv@2WhDuC%oL z=wnl+j`=VMRzu9OMxm^ggQVeXrE(O}Z&k6EA3HT`(QMOOxXsCTFh=^*!uNJ&Y4ce*N%laY)grlI7E5;qMB^uHXwJzUSWW9ad+*oDVwN zHLrt<)oBA)f8}4|n22dPiXcQQS z@`X0S(p;U5AN(9h_Y~Ur06s`NeFfXVn;Xc+PqKV{PR}z&-|c^E8+%tQ{~%wg4RqQv z>z}P~mwAvD;dE^DO+k+pTuVVG_;E56m|gqiO1{^h%nWs$GJ&W|gEo4FyGv^Yb|Wj1 zXL=2^<8`%8#^av3lJm)Mi|v978;S1L^;4gWut1actU*N3nM-U`1J5E7(%-2u zB*gZqxSMG{>_Ax`X3*QTWySC)Df8q9+(dYWTlKjX((K*FbH5*t>7$Lb%?qBC{PC-f zw)7O?y_NcgJC3`oAiqpRqxZ4dt-{B)=q$x24BSHEYzA88;G!x|l3o^61&QlgdV>G4 zj*r~6)v1&X$7~}`WOYv%f!TCa*Sn#Y^0Uw3)U~xEz(%W=`80C!&5~2v=;SoVyn8$b zX};?Q+!K;lD@vTMIck43oqTY`M&y)JRL!lL`{s>H584Ml)d=B?BMse3jrnhS%x%1K z%5TkklDE$AppiSz=yB)cAI@2Ce&$LorhW1^HkOr2`;+l7tn!uP!q~ltz}M9;!>{M% z7gc0~1uX&Z&ZqA$&lecz36GRsQ}1I-Eg1C>JyLf+%9Z7F_+I$q=CcQBmY@;4A@`F) zxCiLTzB1Zg73mw&Nw-LN58d&*x+l={}USV3b)=dr@zhSkBAXbHu%7bUqO1xKw1Bjqn7E;{>; z`Hm+D7W#&ycL+$h!huZj|IOl){0WCkOdLL|i^wq0z3-J-jW|8z?3xHFW=*xXbe9F{ z2N{V8L@mghn2xtl8_}#UUrin5q&LhnyPhN`S5#N&ONTAS$Z*{lc*U-^p?|tz{*+JU zXs`8!;byDn@A!)C+1m~Np}O&hp9R1vs#lN6Vkv}GFf7Ea2Z5()0ks)DEylyUIF5IV zkxX0q3>$Kd`B{4qguRqL$H7KZTzvfc<3(J`NBhY?*bMIR4}T3#!ya0E{yFmYOT$Or zCAvH7Bdspv2-Kg5LUOPh^poc@AIRe-{+)#WjY1vYW4{HxTU3y#C%_05bx785${o)& zwr(uCd_ZStEG+A#L$a7q74=ESq-=36y0JP=vJ5d4h8^54tn%ggku_dsscgD-8zR#C zqwTW0bqnj@x1e5xj>}VmMHgB=ilkz*h+rDLDMQv^PvhiOQ)aiBj(cCCbWzi3PaiF6 z_LLyJxSs75J<+*rr=)c>`h|V2#6xZoCoX8ALqOuI2~A5w!3KoKglps*4+}h*s(0!l0_W{Hh>|P( zwtTj4x&wsKbPIHZj24Q=Fgst5%%J z?fcbx>sB)-qshyNAF2+M8TJuW$Ilu9y?a_OVq2Dz5sNA>dLJ~5!Jg-t80G0puJ4BM zM=jVFXz^-&gEPuTf=U-j8QY5b)+g99R_&s?z1;DUK?c9JefKM!R2PAELL z_rCa8=%$$Cc*(8lwv6&audl@1(xcMX8~y?oVcTD_ z=FjA&-THCfsPpE;l^B(U2!&qet;ys`FHtwxs z{Tzo}V98`}$L<-9r#OflQ5>!qf^N(ob?Ga|B^kGo+~2>Y|P`1wtw%cTTonk>17 zXr{Zv)Xj{UO|b@6XM8jwv9ulLoZbC9k@z}q(BVSh4X8A0pVH%&wJfnZzOO0eNAIE^!0@YJKnv1aL zY6(7PPVe$rha7fhi^j0RnDY;Yi+&`alrAS-Nm}~wUZuU*fvfNB{#Wi+6;R*v?#&V> ze`$7GVDKUf56c*QBS*}-EJpR>E!MIlSMM`UMTh;#n4HqHic?ro8$VwJiyG6+_i9^- zXa1|L<2UkTtxfd)mceNz>KdKQBB>mm8b$n9&QeqJ<*#n1-)uduak1CJ;xpwckk&Kn z6LQ3Wne-x#OSW_EoUbdu4mq{0Rve+$u zezQ-z(Zp0rEvw!1zS7C}_RWy&cm6Lg11~BXjxc_*v*#s?GH`SZtFv7YDa^mA;XXeS z_bbd+$w-9!O*v|@ChVgO8o2GQ0UMQ~>r1zH02sW@?Q%VvcIQF@yt_~!tNMBTm9#&*UCDa1!}pP@0`_&A7{wmTf}&(u zFN4Ps8I!*ZYGV0HdxkZxcfVPC#%35SgjgP{jnGE?GyXrhsn7cPf>gdKJ#Lo^I}st~ zhY{l)mNVwnPl67OC>wlAm?=rf?AcaVkZoi!-+)$8su-)&?Qx60IpTZFt{6L%&H%J# z4Betzg1QW`!~SxogtW2{f<%xvSKPKlwo2prZzRaAx#T`p(*_y$6Uw%EAzd&!+2-D_K2Aq}VuX}SJS4T_ zn6y?6^_!FS-FiV>&hdYFIanqi2W~Z@nU|X5hd>kH)?!CHAo(6hJO|?ymD6S zYCI`#w|znP20|2qp_^qqw-*VWN?0Bc< z7C5h1L6(33J~#EV{ z${Apob@_Fa*N_vkgn3inp_Lp~_9l|`8-c$k1xtuIvv(~`eV4`T@hj@CIXg3GcS>)* zAAGY02k+ujtQ>D6C`7NCJ!9!!PeyYok^YBCVgL+&>`R{NPX-nu><&nQmQh~KT- zQpdiS#~Am7`kt-xFXr{Ix~rkv{ESJU#;QyKAGR@jHts75LK!(hZxQ0ipGI60hfCsdKO2QYDk<;20Z|r$^_X zXy5f7Q@{#)@Ba*Szb4aG7rPSgUFK|Z-qFUEUf*3NLsa8~08kvfoY;m8R%XrFK-Fk= z)e@2xXS_q(f_Oi7=B|&BXNuT9>`B~9pnl*7NxmA#c)O7Q+jWqPwFP;)IiX+5kgtTo z9@Yo**iV|Y(=eR57%7eC>O5S z9+WT`$^nhc%-7}(BP3z$WXI2|0$CvHmPy9$ z(AL9=2b-c2@BM>#gMI-lK;hQ8FK3=5+jP-K-SQ~iXq~r*iR^(dYS4g0m$?O9p-iD^ zKW@1SS}AqlvHISZUGG27HuCX4)&6^k%`S+24O)HJqtEOkq{5Hc=>+VU>o{qei2GFE zX{4Z^vP_lFKTlk~N4je9rTwzzCIaNc2_JBrOKclD8=OPW?Ru*{zOOdI+a9UW0{GWU z2(a-bnygLCF=x@c*l*&gb(7abf-j%HMNobiSKyXxmMTRn1C>y-e%(M^&YPQGi@0iW_n z1N|bJCdj9*YDzi5Tk)FGO|I6vy~P)U3|ff3z+$IjVu|Mgme0`(EW&&;z_bXewQ2|NzDGjzUBX(Aqd&Jt9XzEG#sMs?h~u+uTb1KevD9NX9` ztA9Ifp``IiXhUwurBF$kcze1C)j#%$hX{eiCNPSLQ9p(|$FY@}aL-=gqLTu5a#=zz zt6Et*JA3Mfg0p7RY_*bgE&ibPf3PLottgHNd&BdwX>@1NEIUaiO^A9z5E3|Qh>l`h)f?$lAj_qw#7zw&U2wi~1x*;AXYG`K-u9!$ErrSTZ zW1N@PSi}61pQYx#%`G`a`K{O({_sn-L;BHf>Qj%b-|Ft88_s={Q)ogNCv=sRpaG}|QV!n zd@%9?3z?bo$mwSptI>BOmgPemIbokUeMwFc?Es=LIB~d!E5v%9z18e_lPuHcCicGP zC0F46qu`%GpzDK2leg-P}}>H}QQyMZ!NIPsD|wl@lRee(BP{U*iVANXv3R zaaLu=YQqeQN5nEb>T(I$xT|{#X>B`~gg?jX(42DPv*y;;8eA4;QN=pug}%_ml)_z3 zf)L{`wxPFym_VXFI1I99Gmbb(xr^I91A3d3e9Q##Ct`n)iDZd5XUqQQK$W^EXjdyc zBm_mj5RsJ=V0_}1_q}1iH{hW_UyeOgA1{rC*{WeE!&h*>7K)!~3wFE_+mScSC(N3q zW6Dr0LB4%GM7Bv)@nVJ(5A$(1eR}Dq4+}LJ{1pZM(p=D<$xa{2wg~q?Y5dO^p+Odp zlRKq)XVz}teoXvrzz&p|iH9JdQ*(W5VUBIy_q~o|# zw79snV9hmX7}7hgzZD4oh*bdk)UW`c9YL=gMoGKXpGj?LKAwL|sd_=|59V%In%`t- zxQZq460#9%(Kr=i2f$2{feHi6oTEU#50{_}TMZMmoOk4|`{#^w5~DuSP|TtZ_W5rmZ)i z;SZ6?Svdfiqb;h*J$W$wvWtuL1Ns*?tl>usb?%L^4Ztgfcq6Fo%(k`~7l)bXu}GOI z{OkSPYFIwL_2ETq(LjcL?>J3Z5gWk2*?WZOszcQ%bUwPNiFlpcSJLBD;#Jd;%+{+P zm)%|oj)1+!UTG4(?zeLNg~A?VDUO_&nZ?05<5J~1NQ38GXa(V+)eYKs(e=Ywz>&oUZ+Pj}cIg+^l6f|$K#Nhn zcyX10aJUT^*$@5#IObu~PX%9ypp*db)hHlP`s*DcA50g^-J6cSC<6T2I!AADiaEIj zKL#xP@{+5A=?|W6es!%+R0pzJ2_L5BSE#z$82`G$!HFx16?+oz7{9%)h0RO?ZhR04 z4C7dIUzs0-#!qr?*f`3HE}L@NPvtGezNTf5s`uxc{N3obvCJ=1<{!Pb2y&L7reH94+Q-DIgjzJx6-)x3a}kSFWRXr*cKHrK(<-%H|gZo(6A?bu%{P zgf|l6A2m{gyW?rT1TtS#U#Yl2Zo%(tD_3+auv(+EMyJ*#{maa_v)7X+9ONsn|JHcT z$oTSG?lEkUoZ(VoWM};XF1! zL8#Ty`jNWu71IBniUPg#w~}Fv-Uq9(XP{aPYJWw7DqQv6cVhmP(Q_cSD%xmd!vFQK zzP&l@&;b zWRz9xkShvvcRpraMMX3^jkbqeNcAy zKK;_q_2_g@sa~mAEOc&GoS_}UNGRg`xJRe7yYaT1@#t!Q0q5ATDUc^6i+BZK(JTy| zh|}MP|9~EDR*(@sNLG}$O2HmRZmc}BPjm?oZF~hiX`}*S6e|#fk5PC_2(aXz7HtEJ z(^~@xDUh{lm=Q2@Paw7(K8^^vC(jarMwJQgQ;gd+#L;5118Sz^cq55t@>2abfBl?f z(Z0xg8Xa6EoiOjs+Mo!*bR`qw_Lnf3OTd7nS&x{*C$9&$yzx5r$-X*X;{N_ijuRRK zOzgGwvA57Y3x(;?=GNg%^|@A%>Qa#g&ec?uBwOdnSw&9U4}=FWakAmn{sF-O+-&XP zQ1yLOb$X8^=PYj803F63xHSCJw71ZM=2bFY2d*}9JRNQa#pPEK2fZQb z7guceQev$()xHMm=;qBL8zv|0LfTTlI?~Q9v#Oo-@sGk#!ES~v+9iOgpHrt#NQsGA z@vn3#y~XHB?mbms$V`-e*sn!v+=vCTo8tA^`ZfMzpJ_Y70YI92^xC%YN)4RQ^i&!3 z9xgGbFU#Rc`T4uPm~U;TD3gPuxx^~xGxm0O2c8)$Fh@jA;LPad=YysVYruPpdXEu@ zL#tefme}*QP$0vOJ{_CExu6_EId-})rTKT3MOr{NtJT5X%=miO9d=vgN$ULH2xmmB zt>PcN|5q3V&RE6DjU!1D5yHu~7US@ICypOKc2|q;zYk9pONd;(Vx8|x0PYT&Z@z6G zu6NYD*rIUJF!J`|FmZA%<6}|xkmgxE_5n=cXH6BBX2APp8ctOA%s+ya4CecKeU;5N zd-8gbKF0hr1!8t!B|Tve@A4f_jr*?hd{2|~%nE`I- z`azajxbx-Nr`!YgICjST;U#JLyG4=c5AhY;u8k4D-%{1jrF-btrF%%&t+TOYQ;4h5 zr1KThvd77ykJG*@TRi7T zQ`*t!PPtq1qO2}4_ulk(V`lp>KRSfqD4$%}+9+QcvCiq#URKjIST2vR=)&wn<*QO9 zz(D=K>omHhfN<2H!immT|V_p z3|Gj#iemhjZidJjH}WIm)Q96OfBIuLXu)>iKZ;|t|7e=xk=Q~GXRZSiCh`fDVp$vBlh<}MZH50`C zRVbZn+MD%;qs_=aIxmhb!ikNmDXRZ~{Jt}9r$YTHOO9qNZ4A?8<~0Q?34KA6pW*N0 zVper8ePb7w6mBCyH8Wdi9OTtyj9WjFcn`@q`j> z_1IgL;y0KphEcQYkX3PYVZ`6pfQBh)dTSAgnVXW4_PNVcF$CDFFMqjI``g9}6X5Le z3b{(~6;Xd)A^b}iXf?^31;fAY@dR?gKYJ9+Ta^@q$gBY^(h3Au`NoL=ha$FCZKZ&w z)K(QofsNc<^$M-&n>@L0Kj>jTB_})}lXP5=Ng@)zb6em0K|P&R7d*}*->VklD(MJg zeSPH@r2I%#A`YEkwQ80e(C2k0f=`+!R4?&B=dHjogA3T zl}8X+H;)J=J7R^Ka@ZQ3GvX_e|IRQ)P{9CvDF03PC7Xu*ywdiUKolv!`+qCCvQf&*B5is&#uB7(EUKH z%c&%fHYV+4;Tg_LFKzs-%^ZbWTwR*%CvrNofIiNI+-S}VrMX%lW|C0dN*`pQRfRbS zM`yWC7*C)rc_ResI`aT+Ib457e=XZs?!4`xRe!Y>k{YLiz3N=z@bou;($7F=5Vwhx-yq$MC^l6Y5);CSPPZ^!<%pju0jSt7N$|Ip*6a z9(W({mO6}R*2OAVJ|^U@ayy1ZdMi5TI&SZ8>WlRKs8AwQq9>afR}QZOE{kXU4UH3r zCrWF5WP`EV4}9Y5`q$>SOx{>*Jz@sI2!NQtfXE0A%La#O&H5Dg&K2P8g)oQ6qo@IJ zMDWtSJ_gyNE`ZX;6=Ak{jBf_n<=)dG$*_EZP?nED#H^$WJ7Fn#eL;|yV;W$pZAiUj zRGI8y6>hqVqcFSCQ}t)Ghn6E)E(o%ltt2pV{;<;avs^XiXI~o^Q5LTm)x&}vLPMkK zGA-wur7ByfzwjFfTL1mE^frGkIvjM+3m-@~{?YJRLbxtc-~K8Q`(2tq&Ccu_s!so} z=5EaZ(?^H^+U_x-)+vC~8kb=~2(|_UjkRS>B4p^L`JY7n-gm&-pcYUi1%KbBMNn-4 z*IV~%(39>Y+Bi-EM?aUJ!wVW_KS<}6_tP_h@s@|NApA7$#mYVc2-!*Z!KAKm_25N| zhc;%Q^^DpZ+{NB)3kle|S42woy+9B6sFiS>ItR>syv_MJr;&1R0!PPL(wFFG{PKGh zp#h>jzx5MxFV9A{{P{rfJReA~yo4Irwwr=)6uOYt+?4C1%acl3stR5*r?ZiNDoCDX zItnsUsd&5w1&OKZec2fG@a-oQP>RB!Sxl`T}o(gXZ8ZF+hyFe~!TGt9g~2 z#t`d|V8OWk+4O|pdaGe-qJ`ohIE*;FbnA2}f+&`G7J@5*51$=hC~FT!ty#d$Z}hQ8 z+66ucxmDjlyN{|ZdUra)g%?Ph@0zNzU33Y=xbn|2i;Kl@^xt}KBHb-kSeK~B_4*z3 z(PIrTEqoYN^EIou2MrB>Q%L=Lnx$6e{1*i()CBsr^Z_Sr)c z$!sk4fx%c%@(CbH9RL%!YEQ+P+Of#lC8Az>I1qjYJ_fwGYs+?*s-5<4uSjz3a)=Kd z#H>&?i^^)c8c>|Xc*7d6&2H%OhyK#rp1I+6dNb*pomnU^Hher({mq|>ec(j|4t-OQ z%^GXXusV>`Xgi9>x0sRcF|_bU&5f1edrxL4CY2*fuM77xSW#TWEgrY1rgf?3Hny zE)&T`6!sWcBNJutO6lEi;BoTrmOZ3*W`1nxdO`+x|2-MZ{q0!S-soHt zipTAMgTa(EUpIaL7lK6s8CzVEn7)b@_J7xo2Y zWh$tT*h2uWlDvAsy)jdGn&``L;nC&yNcpzWvTC}h+=K! zsC|We95H@w6o^)zxW+5W(=qm0WfTjpegN3f45N;f-}zH$x55F!Am(aJNE5Sp}XV zQIJ<&7reH&fWglKacQ9jJG=s*wSGrj=0Bk4KwCB5FM1*!JG@>16TNzXSHg}&s0(Z_ z)v$K%>a_i=xhAKgH~MRHz*rQK)+W;@2>F>LIhf%oFwR%eX1XzPPlC zWr0+)!{OqJP?kWlR20>~>v5nSQY}+Ji=;@}dC^8@mG} zr)xO?R@!slhyxG?ypzu=>K4os8xii@)yOM@O#_j5-M);GO7`jAQ z%~Ab@+p(&t=2asp{x{{pmlDmZz#tKqoW&p~bL3O9>+T8Ep^Ez(X%@YR?Q}HMe-g5H z#ZUt%q(%ujhykd0;6I=x%ONSt3xj3z7T~ND{y)wt*CaCNeXlI^Q{!E3Y>me+M);_$ z2yPePmN$7Nz?jBe7wHr1iQ^LjwUL^fy+(usewT5jTWsFm@k;IO5a?s0m34J+>e)8V zhmavyJ6c;TCuAd@tH%OS;F8R^E$kWeD;{;`n!Zu#ESQ z_KuwOPodXlH2CFaN+P00QbI`Ki_b+33{Z8qvzq?$z{YL@RiL#pqfNW#MW#zYtUr^D z*llqpQ?Q819gQM88XA_^JMTxys_sbNsWBm|f83O*P1>**YkTK|iS)xepL-NJKvszb z1^l7xhN7l+<4=sb6+ix_aZ0;8MIkh1RT2q{{=OcoT4`PvD<50oEa@1FrciOJH3k^G zlFsUyK)OpE5HyuvI0RDs@RjlJj55Jc(_b^C{p&p*Mp?bd0)rIhZttE|{e4-T zy}8&z6J)P_%kOV2V5VF4+QZKU<*Hsg`Xj=Z8!{Im(#TUg5SPDeY@M&!8FlARS^M`p za=!hqEmlx+?=fe>AGS>&R+U*M8(2MV@%~fvl0&k%XwqL!`qDy1D-j@uGQV#xm^aF2 zI*@q$(|O(~FD^<1kT^5#NNIO{5Xq-Zr%W2Z87rl2U=I-7{v)6Mm&OcIa``h^WJ03< z&OjTa;KAwg$AnCNeEG?@|1FB`4s{QReC4w?vx_BZEWjCl*--ODM(2_GW1F{3gBdns zb`3isdC7W!4x6+?Pg|Uc@x`Z}+xprT_eypDLvvON|L}_G(h3M4k}@J=sxfV}$<=0h zDsKX?eR==q^qFJ&^aJRxsYlV6(XtGa-NaUrH+#6g>0|Bo#B^X@ZJ_HBVcNO#mo(#% zxb!CqrYHJ6qiKYss*l+B_Eu(C}@RltBfW;OR6mpJ1b(mdCslW*3AitkMA z-&v+hS{-M!H_0tU#S}bFR*kG$)HE-W`|@VG{^8x;e#V-FM~jn3MNzFBFxMZ1Tsa0# z*H%86pa+f(4z*WL<15IcJ86B~;ZN^XKKc2s_x|nZ@{qG8TlBh ztn?n}EaWV4sG7%D8F?D79C*e&ma99PIag@milZGOH0$DjOd#w0mWX$&OMz%C07#%! zz3);03&D&RMvS%ptdB7@vyI3P%G%$UUtpzEdqcw=N?=LUwDQ)sfu3c3Ekv)Dy2zBC zO`AKe%mpETW1B;g|0H(b(1v}yVhAegUKAcxHI|H#bK8Hm++1gwak*iT%JFHQ&oF-e z{t5Gl>D}%c_`CTQcdf}0YdOycZ!D5+g$yUHvO>(9FKM#ehEt@40A?}BccPf;p;cTc z!1w)^DV?_rJY3#ewbH1$&;K)14IrRv(A$34aXkSpoi^Yf=o<9Bo57z)%qD1)yoR+)TMATCRnZ|CRH7|2Fb$s zS>ZXs#;!XaN0OhuuYD)Y(P{P>wR76SPaeIKaJm1j+Q8rg^_wRh-`-V3Kpfkcarss_ zG?vp~N-pl|O7xCrPi@Q4V5%m_dOSxbvF8bGdpjGKxd+L40`pON9FY^|Itf?bBFc_7 zKk&fccAqK}ub(T>PGqYi-N=Uc8{9NMel4%o`T=$6Aom-9ms2*27Q0SAv0WH(w6czi;dtht zZXaJ=Dfor{q@3!<*nh-=p}(b!3k$v3W2J`FV=YKjp4@$ynDn1-`)@iPjq=3qIv5E^ zp=EIsJ&T-wh#a{0+gAQbv_5Q2DKpxNzJ`0n-8o-xc?sIHb#YwQIDi<0LjMmUT&yS> zObp0~`oksnP4|ALc~9-@29a zZAkUfA_n#rgF-gDV9ws&LJ#Np?VJ477FcntlbQeKfr+s=IJj+W_h4U@qalyM>FY-G zF4`z+WrPl|HdX`S+5yjHOcsdMTd2&L&IUL<5r7Rprun)$&AgOqE%Ld_aI7g85H`48 zQ21FcJiKl(q{_iXx}M~+ypr=|u8>=lT|mn~VZD0<#q)>176jG+ zXEbYUYw?wd8-h;aAkTxeS1E%{>smRDHYdX(NyAZJOE6>~hx&<;oHhrB9_63Q&*Nu)ytb&rjWeK zD$-Jc|ELa6oLui)P2YD)RA+mj1{nq125_T- z_cjxQvWXdhGYOqIiwS15m9lc9*tRFE9`GZcHm!)kq- z_`1ij!m&VW)fOR-zPb5;hrXw%-zj|-`*-2SNY55-B~It zZSUMw?O(9YX1pDTH*AR`KP-N$(`9fFBMgLKE<#vri+jaaC7w_EsbnQQkQI+*? zF3!*Bl1Dlh9HrrA)9bu%9OvfoqJ!OR`rPTgZF8c}F!iw>L>9d0goPFM)o@R$?`QjZ z7fSPcu@-)ra9_*!+8s)3lFZV?1!cKhEP1^ZJdYnLb@FSaDpkyW9r zz!$|<<>@Pr1R3gP%LA01yfSZieG>8d_+c%#z5iE*5U%4&D<~f@g$F>T5oHu@&nxDk zq0)e@pn@tQIO6Ss#jOa#8InrtBVqlEw6-Q;pt6lbdD#6P12`-5)m?hHB*#=@q<1$M^^(_-7t; z0cZb!s7`TtF%S5KGfGPGK_D>Jvz8qP`_%~qO#y=Co$AUWLMcvkzGtBB$z?e!gC z7cV+*-J;f;L$a_2{?_r;otwN+z%KRiDBwCm&;)A(-0Si!tHk%?9~+N^K4)3x^R6sN ztykStlXg3G{q59PRo{?I=OL!Ieyzi_oy)#Xgw(}sQCJT(5MavOPURvH9^S|E@yKg$ z0{gnRRcQ(aMKeUFc1cvAci(FrlWd64nQt$u$IERA)L^GVQ~zI}b{>*u(~%LHjC^#GUE@OL<0bU(*$az;KEJst8}4ZSUp z*q_Fs&>Q7MVVJ~t^Wzd_^#a4G>3Jsp3yhM@KG*gB+8=6eUCmA$czr^7vjRGR)I+8H z(&tZk(h&9y&-rXwBQfJ=X+5E`ih08kLx($Wookl_493 zs%J^V$pZ4-fm(cjf5IyBmMf`}Lm86rBvC{bDj9*nhMK8)^wy9oS7=^gLK_n{9LY2ZYRD84O(H!E&c;^N5aBpABfR-_$an z8R1Zph~T8L*)! z-hXlAbTG|Elp<6RMf6~gFqfU+>p0)l^Ncq}buXXgjQkopOFFvQ^(8_}pols7>gAmE z65b5wWA7KHC&8x4&l8XI=SaqdL-SlqN|-f3bRb0#4G4okBDiV}!&*5b*Le;0-AtX3 z#x?fuFF*@o4IM`lymr3bp;G-|$d(1Fo*~9V0BhFV1V?!&UyN?od(U-R$*)~c;C%SS z@1|bat|fxi4%yRIr|=%8^c^YPJ0unW>>+k$gg^V;=e`zjVf<~SO~;w+Ncb!sm-{{F zipVH_tLIX4%T^S#zJOP*O`KSV1Ag6K*D-% zZumEAb}%X;QDpkH)EUTP6S_Z@5~Q3RJ<+zgS@WprzfB0f@bpZ3>^ zL?2f$@?Ow6WB7V+ya{89&UHZ)C&XFu-Q{FFgzE$+mq1Fq&w3V7;3Ha2rEc9Wfb{0N z@a0bO{khKALjj%4feRZ_&;!tFPK5v=tY+64 zudvc$BYCNHJ~cIn*#06_Edq{@I?18)0>Wy}dT^Sqs@AL2rE+B~cLc8V5;+}X@2!bg z9N9-cCC|!@)KENSzp@7SFxO>P5Mi=Q?AtDDg*%M0tYOc1B`CegRjZCWpU3>N3uxA6${HW_+{ zIN8Rm`U>;C>kUu-^1!DuriDFB_Po9Z5Z4W*lZx-6%OmpxM6D4z;B$sI$WkoQEuw;X$5g$Ut|5r`#O@kUqb7r=sORc>UVrc^@UREKK^o{R-fL z{d_l@9=BTf({0Yd(49DREzvqNTV7+AO=`t3Qs2a5erC<-K$608-X)uUv}RszmZCh< zh0b6rxwC|7O>}uFp5tv+-~Q+2Z|gA+f5{equ|HHb{rwd6wFggO$$8nEf936ZX1r1+ zPbCIC-86w)fHXFQ@A_IPLvAh5Cz((HQk<584FJ^)iZb=C1xhHmud_vZB?d4d_H(#8Cd4ujzY>)rsRcCxSV zFWB=$KS-4NO@NZVLM7_*f+s*owI3j3@@sh0s@TzY6}K~E4)+J#Gvuq6y*OPOqMk+? zeCi(PLgdxRX#N9&smQC>ju98EK2HCuY`^}jr&0V+PDVDrXo!i*4st%bI#(K=h1u+` znCn}8{WmBa{o4{3azD|Y>5-r}+e{JWup#l8pq-+l_R|qIZc#vnQA5>>isWILgvnv= zb}kuV4Lay9=_@=q9=W_>S-G9XuCvuuYS@>zY=4*9bjdl0OmCtFOt!_LlM=^WSf1sc zOwL9g?3T9r6Z|QAZKK)ZtYz$E!C=|8_H76U`$?mFxrd;@r**ezwBkRYV8K^-7g^ai z;C2M78(alvoD)>h?OQOHaB0K7z-Vu3-;gHP`3(dsQMULEt?I)AD>80AcLW| z3|FFveliS-;0q9b#~XJyk+V>u0YEWkxZDSry@mVFWS{K-c_%Ha39Nu;|6!M!`i|hJ zKU-Nwkm0pq!?5QwNeNvCiIJ_JuodyL#vmNj1gKlDj!ONun)~L%4i;|8Qh|O}(&Q%^ zzkt%=^#SQAK#e2#RQ&@Qbt*49+n`Pxa9lxWs&9sbuW%Bf12RWA%So6tCBrTzAGi>(^ zF?mpy^(xZEZ(G1m$y}!)x|kdgdy2NJvj&~7LgUoAw`?hK?Dk%6^x=+`t{Ll>MQQa$ zdn;^OxgYi)D7@f0#tTriES;53l-3|;ryq~|&Aewx(>1IQyWP*&!`%HaR%V!4gR}`X z``V~(0#1&5CO!>zjtq%m2ncpcPK19U*`v?^OD*6%33)`ypmd{p+yptta2$N*I4_ps zHY?}g&G}OML5=&f{6}gs4e;pMB`fI6{o=WYRaSo=r1pqhmEHRG$}88_ZOv=`%@lhp z{k+i*?fkBJ8T-is({BVF$70Krmmg~8lG|u<)|0g(pi-b>5h_2+_v@Ub=`4pqdb-US z90z8?+lMSK&CVOYpLHp#d+X?z&}H}Wm?ZsNs(VI-#5|CUmtN!7B=I(J5ZvN0%r zc9D5o#f(|qI+*I9bCt6xC1~hPQ3ArbjVWlPQ@Jlfg2?E$B5wZM?fIaPT>}3NB(ln1 zr;EWy1MG%V>1k2`k}ryas=JOZ>Nq~|TgJ4%Qe$VM3*Bn}PTy^@c@S}Binqw@h6`b` zvPy!&tNq-1E{7A3l$aT5W)HWNk(%XoIwolg+MKY9X zJyc9Wf~^#%cwac^#?*ZS`M&~WGSd@s7~OU~1lk7BRox)6D;$)`-2ldbx<5X$ciZ$N zjExUWs0^;m@%~t+&Vd#(O^jT(3Dma7CosSvLa6(i8GpN-Mn6L;apQrVB&qVIb+PBGM|cqybq>sA^pxteQv1EMASb=# z%$F*8Hx8b+T8?>JhW~1(8bH`zm)Ha>zur%;w0=cxtMg8BFxmgH%Z#bq8<9rhD1y9V zVS#V+h||g=vGZN$ws$ghKQGr#?ALqUzuweYC1SaTmbN}PRjJMZogP6v+W-o6r{?Ai zU^z`lU7xwge6!z|KF!^{ztA*Y4Icq?eY!oL4x80hRBn%}TI4(Jr-e!jMweA)%lba2 zk!R6z-xqv6OqC6oQ(5Brh%v0u2&J}*o6RBU#ln@P8}LbU=#5-VKpDCPV(G%)5*re1 zsOg`p=h$M8N5K42fCbi6X%6@t3%;@iiP@1+_zW$VRQGaC8)J>b`L5@7O%RM!=B(be_la6c7mY#i4M z=V}>;sT3@1SgW|?y{FFGvlxtAd0tfMXmiT-@~3eGa3rdMPFQ9*J>GMIziL{7;gb3E z2Q5>h=`XfBB3hsLxUaEaJPiL5>;-&-Gb9#4=4Sw=z?)&j|E5_PN&z#6?kBx4zFJoJ zbeKm8X;^tP$(M}FbO5hFL?BozSe727_fO>`2d|j%qTHieJN7Rcxg>1_Qm6!JK)bE~ z0s(+z_5Uv*!1(_F0uq(VGp3}U4j<_y?~#)oGe61?Pxwo7VhQj=S*$g=TiQAZaFINd z=$MU?l5gz*I1i!%nUJ^90>9IkYr9oF4h`;0kXKW9)+oyT5${@AP^Aw0fu@dmU*Mq| zNBeVKKuKWV6`zSaTt#x??OHIzvO>Jlw4b={1DA(?FK!tQmp)GN@%(f&OU?6H?y~lk z%-`7#AAYtu%jDmTVTGZJeeNa{CHQ(QD-ytIBHL=0Vqq2t@_Jh>N#>T)yN2nk(z6ee zweHa?NRcTn2dL5z^eTpi6tpU*0L4ekeXlVG19tda^h`$@?KOP$sV-ksQ!Hrzu!ztg zhhvDY>6wsKvbU`*dN}e3H7+XEJ_g@ zY%QIr&l;jsCn4sJ^t+#A>#xW|8#*oz+LVZp`(WUjxQ>8=kor82dJ8UJG|ea_5sJ%D zki+}$FKG&>W)aM-!p=}?7}#fJ$eTSty35V=y;!;e!@0d_5ErTCzmh@kAm$^Ugcanx z%t}@YuD-KvmdH6Sfe3b@OSb-&A&C|}i{AJU28rG`-)T0N+#f12A_`-S!Z7{*E*!>w zQfkDt)Tw9Q``j@~`&R4m&_ z@l=5cq1GRS@{JJG?B&slmQ+nW$LkLQ6b5=lZ2PfOLSFVW>NkQ?&9G5jlJr5DHp#Er z{OEZop3>{nyFI|QUN23G-ShVX7{U*DTi^P(4*VTl>+aIVq*)K z#joEgGpc>=>SHq@8*^{l;1RHx*>tSr`Hu>|CKlKe{?|?>FBAg!8U0|seb2vW^uM5X z5o7o87Ynt(#U>?S+aI7Pw}no~d7W}vQtRmx{?zSHU6cdk1$G%|t=YR)ye?P&Wj0{` zKgG95t10Q+g!xOIw)SBW#FsmIXx|7Bl zFLFTGIa?Pfllj^H_$$h<(2Um6DLLwQ>^*wLZQB6e_0O1vI-u%SGwk0kwZ+9!TXbfN z9TymU`qNZkag$o}8&_q0s>qJIT@)`@Ieh#NfG~tzIwsEj$(9)3jCm3iIp?3T-hkGk z{!F5F@2P#OXgyXTyjgPft;klZ_=NbWfso4kK!NIwp6fE|qAXq@FK^yo#TgPbC6W)-HUrVD zRuGwRR8O%zMcpC$l7m?0B<(QZ3Y52pmJqic7`Z6bOsgESW0fs70a8RN9(>yO56HXr zlHC?y^3oKwLog_yOpg%A0{$D=I>PS5=i-Un*7^<} z!(vWpa{|CNBL4*kAj&s$CQF%WbIrPY-j`t!3TN7eVm>WE^#n3Dlrk3QZtAUD_rKkW zOG|F2$?-N;yc};nZ(pKm<-yCx>fxF5tha|m<^{X_>gn4STnZ)mY>+>HI`4^&egjp+ zK->+ePgpRJo-#G+2Fz3?hkkvrj;JhfI1aM65zGE(3$N8BF@cfz;2fJE`<}Fy0EfLv zs^$l4P}NJj9nJ1vyOEDakw#xksDQEXgKtpVa{y{h+h+sc*cozjYJQPlA9tP!&$PV_ zuuc8DiKCZ2(;dh}D`?MiW%;ApeVVgQyDZzCYwe$YiPiDGZr4$Su)0QT6C*t30IcC-i$%B5;sQG^ZQck+ zob0}p?v*g-bncXR%C{|;DpM{OtC97@l%3*{$33jpa zs~hp|IB@}ye`Cs&kQW5Y<5IkMnate!ifV)Es6sP`VWeWNOM6=NI#!?)!Cj;BTCF8~ zz9WD#ptNqBRb!3eqfDUTOyIP5toXow!ze8leDD>HdK@EFQ`>hGs}#6%{|GcbX|&pQ^Ult z{*lN+w+wPgryOd^EH5v?DIe>YI@Yh?i}b#6zFiXt4_U2h&oSxI!g73{(G5Q8|}svO!F(k z-j%gUyL-YiIP!^CtI*x}9+Lywgf8bfUlohrOFz8u(L~^{`J)tZ$>(-p{oPWs%(1HJ zIp(j>gl&cw%s;cH=Po*VXW?QuE!*zl%aoZ*`ACv zlphBSJc8A|&@;&$)TO~~*!mR!e)jYILG0Nz026-LQ&awQaO zPzwkr_Qg8WM|DAuBwV1U>t2A)eyWuo2hg>JQOk(S)gr`J|LzIP+#;unbD8+Gg9(2h z$EM;_HnQv>(grnav~j5a2!|#9!K7@PN;ijVZ(TjYIZL&Y@g?f#Gjd%D?Vi0Dxp;NdSC~HQS}aX_kNwi4Hu*r#ZGy^_xWG8YPhRnX(^-{ zC1^(no{Bl&XT-R5TZz5-9aLoW7)*U?eg3t!N1zXr?pBzg$l0U3 zI>St`C`@;H1#kqD4|?6PNUJXalgnjs`#v5YJ*JZX{LwFprv+|YA)#W6y{$h9`EV8+ zyd{v>n(uPosO*LG(l$E!v0$a>oj9L%o|nM*z*)EX&Ngd#2^6G&KFjc{pxl z!_j?7K3kF$o!fukjaDj_9>(&BPr;~`vow1$c#wpj@)_U7(q2BHWs@dPWCJSWcj%}1 zx1+A*kB~9MoA4=?e?2a)wSr;L2|3ZXx(^;ZF<~|6vJExv>Z*EhT5ImA@t%i(MVzR3cJ=zbIF}2YD(`GxTs^}3;t-`UNGcl9wfoIK zbHUcyId*%X^%{Td(+_v665Fcm%+d!`#*f9psN4&07uYP z5r9g+M39s_t!M{ysZbmk@Ud=|Dv?YNc5!N2fn02gchw9=Ezs_(HLTP&i(-n})v2dX zjhTX+46b;JO4Hr8iDRm*F3I?*3&G@MUp&4yKi;pDCewzYv&$>@n8Ikf8XR*XqU$6{sM!5MIwnCoN7W=l5LlzNvN^FTGvVO z&u#0VO-m;JhbfKM3`987c_T+Sy$+U^_?8xKi&sI`!-1lsn zX-tJON8*CD1(SYO*WC0LZ6stu)*^A5#Aah+!kZHWD^8^=TWwSr(-$X3T=;4u>mkPD zkaQq*&;KIN!6y<}Vm@p{qv)L%8Atc76VqmWcRhEE6& z)P(>r&cBF*5J;^~z?4BRCE1BX8IteUU$9L!3wt<&uM%#Ty!XP<+7X$zv*j`12zCCg z{H{gmO5*55cRwU`V}R;Vfc(w^ZN4}4s$yBoE$s=IQvR$nhb+YD$3#xz)SUPr(MC$- z>GNR&E)b=@P$)zl*xexFE-gAAm*6>YCBPu3V9t8fQghB5ZH6LQ2_U19R_NVav+GMf z+T1#fhSr>SBFVR2FKstXrbqj8qqYEZrCo(gJ}#;hwe6dUYbV?!supioN5Yq`6L0UO z_F4(D)-#G|HJXmv+)a$z=zjb=@wYzIbrmLabr0}fE<1ViF+ED4sqy_m!r|An?Wbpi zKk+Yr?FBfPPeO&W)^O=PU82iaAX|qV22?st1u~_ouqY!icxG ze>fB6Cn5`7JQN#aRA=o(Ma&H&BUZhdy6qLumhH$oAogwae`oOj7h&%K)l|E!3x^^d zB1&&TK4G#7X(C7q3P^8K3=$HG6saOoL@Ck{H1r}!???w}Qj#D|kc1jc$ak0f z>~qe(cZ`4hg8?JPC@X8d>&=|ce9HA^d`L-V2i~zHu-r)4f0Re%P0ODzt>Dwj4QPt( z=xP@#0m6Dj6bY^;#C?#|xg2q|CDPw{z|Y?Cc$ zh(LjsCFqdB1ZTaV1+|CLKRXA6yuOWf5=B;uf5Ob%*%NzO9=eP4E z{rUC(C}eLDwc#AiQ(4xgT-(MJ4l8zJm)5|hb&c1?8;8+kPQ=7C+#XLZLIWKZEJPRm zNr}67?%LaQ^~cu*51Kt7zbpJhD0`WF%9?f)aqK9~8Mv09Mc_K)uMeEtGUy{(_I&|f zTt^%nQZi5ogWhYJmU{g2QDc(OX6eR&)Bs=2kj54jT5E}sd?&8Q^beqVX}WGb^ACuZ zEWXo0r!EOjl!9vp|1y7C?GV#KR9VRATDT%4{FB@{+|!E8O#VM6)wu_9bD(ohvn$+f}@os(($n$&--!FumJR?z~D} zR;7y#x_#E z@4yO07pV5kcbnOF$h1IhqZNCC@>u)IvubiF;%b`wp&_mI!Jw@^Rkd{Fycz8`NFy>b zIM+aDf-FJ^QGUJIovvX2g7>XpT3V(Q<0>5>0}ERQ>GFDv)@uF4Js za&>ss%!t}ONifrNiq)wU+Weg%n&d*&`G+~f;m!@Fs_VCa*z|c+Uc>$Zbj>ebcF8=y zr_w_zxuq}ZVCgn;?~S)Gl)&%_^wf54&$7gkuy2e5cjroyFY{JzQwQ1)DA+CO81Yqx znto+{I)?qv9u@JwJ?i-XSC2ZZ#X~5v#u@B|eGRmm=WZxGZL1oUA(Jrkl+?Mf33z^K7PYcY19Z&9zU5}i*Uo_9Y-Pm)-v z;&DYJh*_y?&=H(@6C#fKsDxUuUVGA7p;-}tp7fFW)PB3JIcJeWl>Am7eKAbX_87PY z2rAv=3saW5xRiG^SuSzUvM8~EPRwHiQhO0J8zOiUiUZTV+%*R;^ND(`mzw;^#Vxqv z-8o9Rmdq6$0AKhmltvRy<$}9$3W#yd zO1NG>`eGLc{artm_3E?1@^XC$dj(eO0?cjT4_`YEJ~)5rXd&;W07TjF>16Kkv)hAM z>JX)>yKgt4{{=T9{|jxbNf^ZebJidvgL@`|cS4#xQs#k=_xZ4L;C*Yt_vts}C9nGl zTZszp@u_$2@}zSW_j!CXiH$U+OyYs@S12pSl~=^*7nay=zAZN5EK&VSw&kueNO@Ba z&~2~8u$f!_4S`E>0)CW(0mtqd+i<~cV{N+U_f4is%Ln&#coa6+*`Jx^ zCO>|9xz|gvKhQya;z5{Qm~{<9!oYoz$Gbwwr47t2N2THK|5&;Emb&>UdMg#31a6yh zY)<9;)ZTEbdQvYKKQrR%NiuV8xVTzzbR?alw^`)8RYNB)0s<1K;;DBty6bN_Gg z#YYoRkC7g`p$QX^MRp+G+-K`DpUUR5XIl6Xqt4jdQODUO%kDT-0VO<@40~(j7RK#b z|3I`tu6IPh^z-5hFcb89u?m*uv;;Qfwb(PwpEJ9s3Rkw@c)UH~Lk9XhCyRGJPJadB zDl^y98#aV(8yl)x%=}xJu@+|OUO+Ea%rEZ#-#+LRpG?Y>l>Y}4{ue$B8}M{xqqr){ zHY`T5z~%z{8(J6hG?T&z{x8Whi@EId>U%ZG$}Vu>uD*CP`^Pkn zD}7uQA(M@j*$;%5?lErPTNLlK*no&pEauUOQL(K_Di8CiT1@IavE{4ZNeZ|}^C2l1 z@IeRE1%FRbX*L8f?5;1&W)T|&X=Xp4%((+{W~dONvSyKxzhLkBI9+c2h1G+mlTVGe ztV;CnJM+Z)v&@Qu@_KGy4?btl)fA?s_HeB? zDfqc&HQ%O7b(mE-n%!Mr;{L6N!!<0r&Z+D^4{|#!6~)2`&JuN=zo-8%3;kab38jkq4OGTutY4ztNq+iTS5q~)$MBRMUZFXczl#n& zC5`6)V$qsCUjaUacXIv%;Ai(Z>H^d+|2nbaU^b8&a6y4TiWmPeqdY1eRZalePv-dQ zU2~_Hv6T0aNT*mR9);uaTG;cf?hb(Fz&fYs;W%J_@y#qpuuUOYHsjj5c=}KO(O6=! z^~9_;n>kdS00v&%E1SSG-t!5p|7K|`-{Y{sQZ+lEe>G3 zX15)Sm&HKH)f^DoJM=cVc3@s6B?i5$}>0L3}Weyui zI3|#!(LT<-Q#!tv4XeL&xZmHCXLQdoDOkg*1-uWinCItkA426K=n#_#9>TN|`JLC3 zKl){lE5+(_tlo_J`ZL9MS^oxn4*IRsm8&i@CJ$KKRAUix}#0G$JxD9mQoj9~CrToj|cBMxY3 zK!BzSK`&_(@@#?g;`vo=m<1 zqX_;7B(NQA(m}lcs`mkBq_2~nP95Ek*Yd`)eEco!-Eelo5@t0%pH1Cu?d#K(Zb=B( z$UhV1!}d~=?(<#A4UV)KQqg;f!!tA5+56U4u%-eqWAsXfKoeH_R?rf+o8A44qSx9_ zg54P7XH%E;HRSsIwpB}FTS^}OdVaq!Oj-wUP50225Q%kVNumPj-;a)>C}94d36!|1 zwFV}YZOf5q;xpVB<5lBa`q2Au)76cBL$$v{LQ+Ml(_f70114RlWtK8@dsC`J+7TL2i*>#;nbwi4^VaZM!ozifekjAJr#wKk3N_2PoopSN5Cx+)v@9I56C8fjQWk=hg!LqQI(he zE*`aYQWLZkE3A+Y)kzR5L=UTj#64O{27yP?Vx{ZmeOHQ&^{(?-{4Q@E0HQ5q^aME) z?k32KQMeZm771lDrW>c5U@~(%0b}RNWs?<&vbRR|Rn0dFp7UIhDmV^L!`F^vYSjiWSWmX%-n z@hvL1@^GkzFIa9+6Ibp=Q=Cu?N>tIR`O|+!c0&cUp5#*#o{#is^5OYEl_V#5@wqoP z3D!_$Jr(55O_dsW?X{l22*HT<7_|!$oH1G5(iXhQp5L9X6-uKsDXuiRxaXesM`T&x zJ1mAl>I~NGSVaFKa*~lPHT1kF{`JY1@|nG zq>DCAv{ARZx^5xZve8%dTRn623!8Kc!vA`kOUP)rKeyq9dw}R3xY}K~G9Z17MizxO znd7y+y9b+OTL+sWot~XTgUI7@ zDm8#{Qa=!@R+#{MmSZf9^fp{Jc_~7kBF5NAKRKA|N^^gp{%Kx<=1t5hSOW z{nUkjmbGSLEy?QBrJH0%#5Zy2FK5<&OykyD1yftYpO#wYJJcDa7WBb8h!U{VwWbI~ z2ZP@B8{g4!eK{FF62=3l4@TI_K@p#8u768;_&)j&UU0q$I`N@8nVgKRp-IDYcITwN z=;LosX+`i2C1m7XN(bT@tql3yc45Q<5Al+>ezQHn?E8&e|`D z*-wUY0{fEROC-y2I3<$R_6JnDzHU4D#4cZ6a&V^Jj;rV3w-f6ZS0iA>a1aU)p#+^p z*P~edi3aYf4^mGNV}t2}f5g+?undFPC}iFe9bs~*2~fxBvS}gQOyRPe#JKcPzd^Ed zjB+X0nahDctw@AqT+4Z*O#RSQcp&hWrty#F?vt7FXsY4Tsh9Zgc&i5 zT+85ddDKrKkW`i-{7PFWLX5yWAizgRL{~!D3AWfh4wZ*gPj_Fd=*A|`5gwZQ<^p5b zxm>t2;d~7hZKlgovjN?iGup7*Zfu2j=TZ$0#A63L)zecm?y+bFoJXNPv!r6Bd31@D z#;xKrZV`vYuYSgCZzOvumSNF&xa%gwNfWmX}BiQ7uE=m4te-4eZtays+ZdbC6 zfV0*=zJFiordIjG>5VCMAVs8*r1P|}5>9jv;427%>5y|jH_3@9?MDDB9NWD_ZU)e~ zf&>uIs3ooPS0Pw-90jI7AH57#K(!4f^Nn0g9CFWkWmT82vQ+ZiFBSffGg6(m64oB} zq&3y3(JMBR>&h#c8%(#T4p8wOKKp0fWWIl)-`pNhOY%g=!EF)%`Z={AOF*H#hiYPgO6`78HQXo$`3g zg%&ZG z>`6vn;!Ue|niQ7w)D9juyoix#*>cqGOZ8_9DPWlcP`5qnB3M?havmGLDGt@fTj9x6 z;dU}*yW#<+P^tH?&Ah?*X3>ddE=d9_ z0R82#-`f%Q1=71u=SZ-&duekw=2IzI%_{@angM=XhWfZJz#@r-%=7c8LHUG} zHRw1MDgN*}>f+yF-odZniV@Q323gjP#QP!BQkHagg|wIKf7@(hC7@S)?g*6EUv~GKw1opz=-;E5GgtMK~`e49r8YHo`y2R$lUKOmh&roDRt@ zcyj|Q_O$GWH77m`GnR$(>o&q9-<16$rV$b;ru%-L@!w0W=}pt75rvNeqCt;Jaa0Z zY<^-FBx6P~UwvykGT{^)%*0ogVf%;*}xy;kJ`9WB#3PQguz9ulH;n(e-7nZ>v@ zy}iMda${{RO7)_)AWmDTp~)K9%xaWNG&Z~98na(F^7JjY>1tdoR&aXGJeBk3P?6%m zdM|6uk8lS8Y((99J_5hSneP2}B^)f!vmUsc@k@&W4uJfOv}l7k(k;oetjF`Vjt~sd z1oiuU{~rd_KviX6+K zRVxO;^5B!D-IIj@+;4fgUFB$~56tgYH30e?tuk*XHclHi#7?o2d&MqZ{vD9MuMMF_ zOhOMrkW89iz~tN~sL8u28E5VR*7uJhtHT8znUHCqqm9FrrAAdfD4W086a+a(I0 z%FaRdT@*2Z1i3HOsCxM7hPfALd=m)93O(KXduwV>)Gq@OVQ~eicV~g>Z)yx$0IKUB2-&?J3#W%)6kn|h+{%r@xqoCwYtfN z?UCBKd$^MGVusKqHjaDoS{DFEvGhDD;K-8USJ1rBMaHh_-{kJ^E8V|Aai@ktG0`ow z8?k;jda+b~e7UQ#yWk$bcplN-eOen>kB+xqNOtC>rzeMHmvqQmNV%vRLhZKa3H3|N zZfhy6#XGOAj^=K(gi_-Mh?iEsz1h`u{o-SL)|A;((KxMj$0=a$JO;Sq>U{$F&Sk>y6Z+OxqyMNs&ri7a zEN8uT(Pk-Pj+3c*Z1$?Q-r$ekh#UNdv@;Z_(NER}jf80+S7J-x+Gu|=dR;===*(79 zY~~F4`)kk7fnX6c+JbF>>{WaiS+N6QIkpLsN?v?%nY+6zqzkZ}ruDlX?ltX)^pvFs z?ja9g7|pQ7VOnWR=$15+KBn7Xt%7(qzn-t`HEY~jRA_d2ZavM~cS%QjlKrt2-9-?a z7#D~odw>fyG9C{FDb%1JQVxapT$|o4hK1AzM&$fqw@ao&UFx zmW@S#rrOk;F={q4h*7pLFQTT_jg+wE@^$&5qRbg)h@A*p$_UktnEVE4bo&*r3N-(d zZfp3Q)0hzBe={!>hRXOk>HzD z5v*EN`Q|qC3V8(bzg!4UV3E&{kPTzX&R@tPkK`P05v7nr2*L;8zegdDzLryEoTCuZ zKL|P9SoAc(_oQ2c*TLGs!U8cKu&$>FyY(~j&~NZ8mC>;a2Ox+WDB5UE9+l`|HTR-v zpDHaN|E7-B!>hOXAYw^0>wvV}XG#R_sUe|ht=&hB0Cmak;~(L*ebQ?!YUF%NF~FM> zxer=>*}>k%kIT~KSxxkP2%oa}{v9N+@pF#bW+DS3CLDN)9X7PiaAry7^*kK;n?fEF zSb==(M%DtRv3p|*K%KDyMQYBhT8`i)Ym<(U}8!9PzT)b`mBi;H@tt%1b^-tIZTVYlw- zYe%V#73D>LTP2$(S2egBH>c`4yiyaJl>dO^+u2f?SK1W%oB(qY!Tj2?{YUxKp)fLk zepm%bau31`bGwBLdFh2-Wqzpl=#hJ*Q0$e+fv3;w1WHmx4!n9hJjt3MSmpT=7zl+c z0Ke06GhL)S?8#%np?Y56e9OXFTf?AxHZO_Fva?M~?cpvZmT#f~m0_;>)9m+Tsd34h9YP|{TA^r}x*_p))^wOh;P zytGlbxKL063X_;hLsG_zzN|`FddI9BDm{MsM6ywVaFhLDbJrAbMugBhl8oBp?Y&eR z)+Y=CWyeKAaBJ5es7H&uOH4bBfZhS+o18azdmoEQwO=7f-p74YIA1t@i@R}_^if0A}!d zRb1oH+s(~=%PSO5Y%#7ss{R0YqIiUUAIvdVC^+{76F)bjAMO>?JHXXP_Agsg(*wvP8o@`~J)^UHPrMOG_`z5hR>_eH>O^vJ>P9?%yT z|4(1+F8c4j7{D3NDd*aBi(pWLFLR_@&-hdFogr`458g|NVeukspqru~CQOHXJ2ydj z8oj?)1Xwxey;ojXGz!1^|MbVt!FwZ^icVZv1bOzvn0MQyy&Yk=#48o>x^hIqH)PO*c zeT@a+!$ZoCoqM`0b2(Fy>F!v75*23I+o z&q>Gp+*|m607NT)jb~`T6d8mN?DAcF@<#R_^*J(vC>khy+bPd5^l@5x>&Wt{zC!Br zBmP-4C3PiE%CQ}XSvy7F!Zn*^ee_@0x`J{HeQ()H#k@U22Iv{As4SbzE>unVeq7pC znYfTU13V~S)#hljtd%I;3Hbv8Y{RpPH;R{xYhSS@O^$n}yWY4eC{(1Ryl7HS0Z|OJ z*emiV^OrkLzBb}GB^#u1M@xCytSR$*5c$YbTpyB#8t$MY8FdhZ*N_QFx?`gR&&+9c zRqMx7W2>&Y)6=ZpZ#7@c{qkKrS zW*T1wKivS+S8h`Yeej)ib+;1oFu2-MQ#GTl zhuh(1=Mu?Wgm*n5jO{z8Cb>S-+{^Nx33GT?%68@RS0oKwYCrknSKMiWBD2hC?-#2d zse)~h0vO|may9+v*Q~WKz^(lO&DLA-T&VQ+tB9d?2>x08YJVQ%cNw_wXOk;A9@3U@ zS#jc&L}f32aJAP8_k3^62y~=%iFExuAI|^50hZd^&N<;7^h;z_aq3|yXqPhPYW9(p znUOk$gKRe=D&|jqtjjF<^21vmML&LgscEVUQ_OUac;J%~Z^tLI>zovVdb_9Poa^QI zwJ9Hy)hlh;tK7Jk=BeI2z{g~wKDCRDjz3I!VZHc=Cc20K)vw2qBfa=D^PRa8=!CfL zo0EprUx$jOkAeW!JOrrc9uVFF1!F2X!vy*v_;9=nkUg*BKB`*-nbd!St2~k2w~3_G z+VK3r^It>BD?#tbWX&DG$EBR=aP@@g7k2f}UMX^K*%{6-v)9gsm}-~DLE@Lar)_iG ztBTXttvT6Ke^JMM>yVyi}9-cKsRCzb>XZxA& zA#$N#OB|s@9s`(U!_}+Hqu0_uEe`zDEng}KpG=_C{gsbtf>Npwmdcup6oV zf95`Wbku4k?6-ZA9Y9_>4!6^ePbFmbPD>ajP9RP~FUbwu;tiN)-&Td}E|n1cu(u)X z^(Ob!jCqZN8l!I~j5G3Fjj!X%AKNTFB(|k@9T$Lw&c8}+BI#ka=l9R9@2FgCb&iw3a=Gw@v8%JrCp|#+f+=8O80(nNBw>tu zjf!grX^kx3E%&12=>*fD{6G&`vzl^5n{I*|@8j1sp8Wavt}=d{zKeVm zU|;MVPWWR+%Se0zD>I%&T!U%#JWvpsHa*?y6+HCaC__GO|W#wO&#L3I%mNV7$B zdq3Wh6~LSiyHp3F+hU>1@T&n5w$l7X;DdBz|O(PC^l6rz#*vRv%KZ86Of4@{y_br`k`?OLQ zgNgtwpq>z@tZ*tz9d*e1@9aeD^vyHG-N{ih3LZN4EgYRhW}{eJr6EbJD4oCBC5f`q z%aoORHsWg5r22QTBck&+xKB_L=jjN!tvtHg8O@6%-M5O8BfJa64LQdz-38R^($>?OUy7!0c%LkvWJjJn z^jzJoaaKu12*Nh8pG&y=KZIV5&20-GoUg2g9FJXLpj30QZXtydI3M9V45y`vXF6*3 z6)WR1B|GZQ%T6smizK?e~=6SRPL z4gq@yciL^&!Pqu*UWO6B5=BSB+begccH|25H|U+hPWKl8Rpn~K#qF+^ZT(4lB3V@A z=rEk5T>EZZc}+ws-3Nl}5&=v2!DNgnkx`T!Z~LbS zGeCMh{`Z#c_=wz=%IGE05X1qb$Ut|d9=ewgd@(kBIn*3Ead>BTZMogMW&8blxa4fx z!w<-AbF`0JkLI(5j)r<-B}DDXKOo6HImNIO*JnPtVwiKwQYCvj^1 zG4X=B&ZWSa2|(A!0PW_~WFya#=ZX3sq)+ot{A%|Og%dR1sSiC5eK;0|<+HBcE1;R% zJl(r+8GeoM4+yYWI{D~aGn4BREMG(prC=&1HdH1TeOEoFT5>eS=cD!F`eiR@z4#1J z4eEZ{#ru9jspo4AX3EeXoDQ@Gc@Le&D&)-rht$qeP8cEHgB4-Y$v~1cZvMK*_$ts@lqmzWpw>6$n1? zK2O1MNE`A#zdqyZ*VR31-1n*9B02Q2aeQfpAJQfpjG`sQ*P?C+;uDBfa19*YI?T%e zIM-!#T7-lH)A$Zh2U?R*hejr7Ep-5m_zUlOWWjmr*de(u=v3<$e0`xKWVLy|KXe9w>axgT&Py;_g*CoJId zO$cAtfN;kj-ic;$Dm<>!;A2jS8$*0&zM9qo@3DebevwEzB-$9J^Q*gKadipeG2_y4 zNFF_LXm>F;sB?QO6Y$gR=6+Iu{8F(O&}((Rp5 zoTu@hFQza!A?e2Vp#L{g3ZKJwoY+nRJbb$;ku=ZNVn~iM&73L94_>7lJCKcOu(2z${UK14rR@&tIH(NdBtNwM!1*A zB5nNr)_Ui3z8WcOhO$yZF{MMB>z$M{)Kuj6Hc1RJiA=Y;gM(is6ySyeD?(*2G|aY* z2t2te9Mo(@&pjyFtcp!b)8 z93GU=6;pre?S!piU>`h@cBIF%);l0insMORHg>*it38rswnNN0kxY=sId&DoX3_4f zxAnD&#jG=rBQ&f}R&jkBDBOVQ+%8t08EkId+&MNcbFtrU3*tlyuS5}y)=YuVK2}Z0 z){4k%Zi!Y&QGzfU_=h&q+$|r|d5O*x75fTu8kkK6J-1FJ zX%S`NvW*R2fZ^zo+J&}2K008bs#;6P8>WanSyXGh&O#diy#;&9FR-dTjlrGQE^J3F z3VTrYIXO418x>Jgm2a=z z-iWVL4!3{Q3y;%=lQ~4%<*4$y&%E4t&;ml#JF9&jx}TP76SUqO$QW zK-|*;c{$8_U_o zU3U%{9cmT$TaSQu0d<#%&NR83v*QmJO7J{WYrO+*2K?r^jz5mG$EdE1${$|+Mq{p>obb_;pcf@tObjXEhw~-xHz)I4O!#(dZE_E+F z02sc>bbsIMX}@}YWE#%p73@_WRh8WOlfw7n2Msp<0m}9@2xU9db*6TvN#OIeE|rT^ zvC5&z0@K2>Bno6<+*fYy87t);w=f3yjwUY|QL}k759_n2U8FmU(pq4|NVh@eALO$) za0O{Af2tkuzbNW@aA7``>krrybRtVXLR9L*%aU1TX*_Ig@Q&FoKHpWt9#iqaH}@DJ zZ1QN zM9_>nU^BM}#rv|eAKbSO^c3|ZXLKt2-=+n))ZI6BfT;sfbW#1hwyJq`)1dA*_IJ}! z0k|L;{)_ma{{T*bx9ykLd8i_g80Krbej%LW`2+Avb&V2OopuNAdlNYSQHA!&;lQ*Q zmQSEJ*m6F?oeF3|7X)?~QzH=k37kydr#xBj(Bf{kz3tSD^Qr&{D26$NvL0v+xnvE+ zX6w1ajwgTcDCr7ko@o@&701o?5KX812tN4?Ci^@}4xF)Daev{G0;-LKlgCz*CutBl_eN0C-o*t#D^f}jzyDH0KW0gK{x zCxdXMMVQg2E(wd>H2z!>uIUgFqn+RLR_yrkU1{{5jx=qbrc%33%`cT#cAbX(`?d5u z`cNzB>(GyBi=XWf1FD8G|~TVLRaYe*)JlB^NRC~)(^EIxj@e!3{;Dd`lcjcLaJh?_`NhoQe5PWef_ZQ zm-!gz@KIpXd6n2NXi?qxg{n*TKL%g*FmS#$xMI$#Z!C8ML@{$j*#^N{@9v=?NtMj! zGgBU_%T~lGk`~&5;deLr zY9_bElyDb67$PZ-esfUlbo5!WKL-i~e*6Qnt^r~P3f!wbZ8J4TJy^1RpZI)Ze_GxD z^O;=tCU48Yox_3ilr|IeuZlTzQ*W~A?djjV(tgAl_zsRTTQzR{1DoL=NVX_T!k`GLnTx<=zPEuuu z4}tL5(ebA3<_TI`@uOCcE_aITB6gWYM5re zfvQc8s&ZaTr;HF>&>zmHOk#LI$(%RieNurFS5A8&F*QRn}xg zs9LN~{g2w>zeOnplzE$w?uMXNB%T{BdC2^Q~|Iegb0-}ilJ5=mP zW;amP_;Buc1|W-zU7@!C~oyV4WRxv3DaP!qV1|A z3X$%_QWV>gY-ZAps2+D-S`)h$5vsHAW?F!HPEaAL!z9->-yuuuzvR{V=h=Tro}kE7 zm=)5)DBa-R8EEOh5VkDM@mEb#4I~Eqw~FO|&+Fee1(YUwcMM9DBZP$BCcN{x#;is` zGpWCr#pY!0B(wxP0Is$awVnHt#hL{sk%9&IyL>jMNhiT2qsFp68L?{iM)TUgi$h?> zhAzso@&1zgqF6Pn4_m9AF_2rsLCIg3qp=zG{Lfx4Gl^KR1p(#ttAjYpSZqe$` z=>w~m+HT^0by>ppA1auX^;c(qU!XFUdmqWA-DhxfT*L`+|7pzlt-@x&aA5N`Ar&nN z=7xoha;QeN=;i9%Nxod~TSf6G!&NxSpp3njkdDoYA<5x&4GHF9ZpdpDRZ`5OgF&V@A5B95Vw&r(lanl*9Nj} zek3@Gu{?4wOnAWa2nejHzh2np*n>@tIff)YL(*QU?Qn1$eVB)SEn9oBy6VUEbmX#= z=Gw;B!vBoPu>N}^Wq*zWbT!rtx>G40v61$FgL2S+19vC_nvaUR%BIXXxf8?*MU)ZE z{}oHa$^Ea~97?R5arC{;k^&hOpKFWZY!~`{N`PFSi015)!UURZGeEs__jQ81Vhh`a zK44ApPXOhLQ^LMQ;vpHEzY6sE-%R!p$!oT17>&)CJ zv>uze)8F%9V+|a2A`2S0x6Fq*ITG%PVSp1=ZxNuc$M+$ib86l4zpk@Un!cU^@Xhz( z={<2zsRY}bI_qz;OD#4MY2|FzCIv7md(&KxYqOqq?`ba=>LYq<7faNGW3?E1~RqO0H9ssbw$tE_S zgnEVSHz1k+E&8*_@4XVw%OByX0b)}}oV2>=&4%wX05~2dK38RFqCQ#aM$7xm?FJU* zsU6;g83&3&+BVh-%~XumWxHEptV<#ni83AzIaF#dO5uqmzQ z_r*_NE`E0M%aMOhe-Vup<)bn03pI_^a$SvnPLn9`F8qUT)iQ8IM(rKTB8P^E!{gpi z4N7N$&73pgffYVTZE8OX3`2DtnsCM(cRvU-2=T8j`uL;M=;y4}cf&-N*a3$YK}ea^ zx_}Jf3>Viqw_gHF?`@oLyd5&+M`;`^;(cTBm+x!uM_cnsgNT`qODG_OMmi^)#ge9? zYV1pJ)lg5j$O8`%3sw2TSH(e3sDnO>uEp&FPK7;P&qG?VpJX%w9cK5iZOtk%c{iwc z_-@y1`rc=4i5F?~4~W!;+$I2xa0EigK$swF-!QXy<*iWCn_SM)jacnkiIX9FYxB=u z&!MsPiJzX?H`^IAa(%1&ww94h`;_hS83XAE7@V>%!0Bd!w371ab=@}@kD6NQ4W7JE zlT>|0KbWAM^!i%JD{W99)C}%0=hJ3MRPjPwt>bl?g{x=PRP0o$3P%#$*eK@gN!|o& z53~*3BLJgjcog>1kxPaSJ}P3bI>0e5H+;HS#{SL+$XOHof*U4uscXM1YB=;L+_3YC z&)W0g8FpTrq#4ibitN`CPEF|mYWE0)03i)c*9OJ2m&_^>AC}ck1$D@wqpn)7N>w?F z_0}h8bNAEUT$BAu3L&VVO|;g)@kgc?w#_{z=kF*T#qtC0El!mRq}~IcjI0D}s+~mR z%CKKHe%|Ysx&nAcE~A!R{29`A^#ZIrE|V^l!P>!~m0#EOP(Qp`ZC(-Q2U}mc$aETR zDcFndj$;63V>>G*GmyKA0ho6H`|~AC_;2E`RaLbeUq$5A*_9D zWtwVVkLB)NyXxBs7Y2|Diy58gX`dn8T9?#Wu_Nz@rgWY)o=>-3AMEOUrq&y|KwTR- zq~EV0sWyc|e+pQ*kB2=uS5cpQvP%B*#1@B~FYAWRBk7CrOhYe8y5Umj zRO+QAG6sMx!>e9|DREM5p}0tfIMJT1Lco7wwlrfi>ao!~`9aPKn;A|g4INB#DhqjX z^@6%pd1*!wp>%%vPM5H8c4Ss!nBeA^G zC-(|)G$yNmjY9L>3M%7T-t=9u30_azOOXJbXn-9j;U*EBAMt0_*SeX zrL5r$%wVWd@RBel?uEMN3TGqkUVYP)M{32@ovsaopPa-S+DjRMxyeaxB_WX7yE*xh zWI5O-nkh?KBPRPxhKfJacNKcdLC(UrLg~_}zflFBbx)=V$hC3meEJ_SQ^Uq-k(qo; zuf|A21upV8>t`BGO`F;fIDjZXdb9+9obSGOzkBbm_oJ7qY1dk{s%Fiq8a3va|IpSf zjwg%@GvOJ7B-2^y_N&E{Eb62@rQ7g#v~^WU5tZfsKpvyWsVJSqO-%PDo9>B!rH-P8 z|7l<-cUBS|BwnvMhYjV=B$@vxHS9D5yRzprc(g8G*D50`HZcRtTtK3=Umw(S1I2T% zY(dDpKbS8371Yfe2g%#j%z8ty9G4Osc3(+RMI}I%REhnX#y(RUUB?XF!azD-;j|u=w&=H z?#_$(doU(na^;9(T$HPAljI^TyRVKXZt{;+b_M&)E8iviwl1O;Fm2d4#_iz-i+s=P zYPGBeTlr)~lZRX(7ZB;!ovgZbi(Z`MuFErrY?M56D)9XVr$KBQ%SdqIZARUAIIvc1 z+c%6B@(@;;z9UWNhHhLm-pqWhx@%UeZ&aHs0hO=&wAbN9x-qq_)d?q;~#F_x7#sSE1g#E;EHkK7riO5F8SA2T(vV~Dxe zLfcNiENH-V>niUh<<<-PtBU@|KK4A*Z9RX^!mybY&e$h1k?Rw(@Z-!% z`h`Z00#6wHwu8{Z#7CE=OAK!o)%Ps#pWPfqcX%siU`;XQSgKf0rxy2NbHVKm$4wjd z@l{5PG&a#T8Owd{OWCB7vMTcKMzAtc|1@DC8NJeSqON52%dXgtuzjsCbEb-IkBzUT zo~Bx9YN-xpk}ea%?z?^>4Ue}3+w_+ClbCXQPNygPdM=_iyO)os%xH@%H zm@%GNgXz_oG)J`@7ie78ZinwHyNeg=fhcaWcbZkzq|lj918z@v44J)Ngb`cpddzKw zqKz;mbwfBKFrZ9f{n{`|@JGRIn*6lB-|!|`^@0~J`FS3XE?3I&<3HigZVR=QcyxNm zu?;&p?)5r}61ehT<4jK0p$~~S4*X0)M5Jd62g-#uARB@C;C-x?18JDIRDm-4g~`Wk zY(cx%b|1uW=(O1MbwaGmOTUK6G2;7?mvBOb=H41RVrXjz{uHGUvDJsK?!lSQPP^~2 z5MJr~wiB1WwHuunxsEX|OVv9`g=*Byn`Rd^zgbw0E{_@SB=+5tVQiEa#Q-!TloKlre|K4xzBVd^=yCC!z) z12I3$MbKOrA8g2+)2r*=CRid5W`>c+JLa~JKb^OZBau&gbj@=KcKyzhdkwD_asG#> zx)Ay=Sl7<9^XO-Ftn|Z=pZb+0FhC!^%N{=MaPd_8eU zh`?5S%Tu3o%GB|b5vV-uMWsjdP;dqSi>e9w>Tt>My%YQRV0uH zvHW2JArpO`w$G8hq(u&(viHHp39%Y z1N?&b!3a$rBnW=8=nzoSqN6b3&M>?$LqTSuYC?9JYM0cK?hRd~w#KWKT(*xKgk;XE z8o6{AgJhVVilHJC-kbsgV1!o%`Hj=R>G2M?f_VUedH_g@tlNJ}g%~_%GGDi4k@3jMB3zN;0KF zHpomIZEY#Z!Q{d$5a1hzw1jas`8t|&mVQ8yr+7*wM!h??v{Wzkjr*GVfN%V?Z9%3h z&7+7Rs95|V;bKFq36yZ|DU}_P|U|n{!pjv&>4N3v#fF}-v zb@WsklKMLl%8BtzuXAM#9Xaw;B@21!S9OI{KV^Me9Y?s#MU0H z8@}WK5Q#c>-g2sb3=v5Gs>=K4TnjW0Q9!=F+w61wN}@u;*Z^G<_6!|G1x7R~FaXjbnX-NpWaH*wpk=kynaVfK`l;qR_>v*53k~l zdoH?UeL4c79dtwahMWc9VI-$@;s^9|3W!iiBst`2l|~B5fuR@2g-=mp9@nUN|1ih# zvg*_*yIj#{XN7HZ48qAYs#G5?@qyW$-X#*elzuwX)Ip64io3{bbp6g>FB9*Sq4ZqBNl%i+?tr;j$vAHiam=drYVGmi^ZIoZL8P2F82{C-z&~C`QAdE7TQVmrP|1h; zi+B=eK;F%3adPp=m~1hs`8{w8;Nf&Mgqak@(^a}q1tk4y)v<5hbArt3AUFBzc*k+^ zfr5PJrON;!Obpj?ULxahQKAc@c{dQI9;ND>L)foK58>X+T?CR+IbF-)15a!|Wjx$f z(j=A;`&E>SpeEuN_2Y-~^?wO!>6yG!R-%LSaA%b~21V7o&LCSj*u%|afz3(&*(*mfD@w@bw6xH0oMBaFLVtc4gG|| z)k?i>?&jRjfo&kcIM`ni-uQDZFGv(xt}1@EBq`6e-)j_Ta$Xi#g+ofV**6PPBM&6r#8_<f!xQ-iqI~5{I_16JgCed^h;J=*O*6D}f!Ap?0y_F~9w7=E9 z2Y(TH0$N@E^o-zv)F#LVzjcu&4yeTXY{0^rGV+$W`S?DiCBn+xMM+UMm(It4ia z{n%042=)j4oYzRZ6Du|}bhzGX^V$lDCb1N-x_h+Q1@nA)btZ4`E}iriG)@9VQz*vZ+jr7d3`oM^@TLWh@V3=Zbx zpWZEJ4+uVGlU?iwVhQ_hGNi4!#~Y@;F>3qPp?dA(Kd^2zfRoj%wDAX$S?)sHL8C!F z^uTfoXgj>nhhqG(QqXC%ECNT&;k3l!tDs|s-WKA9(|e^K3IPf3CrFwKs`yO6hD$!0 zo~g8Maa*4^Mv~jd;WCT9V-m2OAH9`f7SG-T# zRubT=_=03^ZR*|^enK(<^v&+%cgPxrwT%jd?E&a-G2+Kh2;0cw5J0f!NiAW!7{m-U z-VlnWafKZoP}jq8F-N^1<(|m45#*QVip9<*WHKily4+l?%Kr1NX1 z?DqS(z+5(Y@~(y=n`z_&cRPe42UZWPQilaQuvRT@m1?r z`u*Y5_-lsNKOu5+2Wp^^VJ4u-o6h=u`}9|f|I7W>cg(|J^CXLZnU6gDck}tT7j?pk zb^TVD^_@q*U;KY>dj@{(RN%e$Pv84JY@U7DZ+8pohCF23Pr@@<%toD1`2E>Ldw&1Y z50IP%`Tzei-2a8nDeluT{-^^F&)MB7V0CfMS{r=q@_-g0? z`^Ehyo#(&yeB?aoKP>qF){L_8S9}$ILSS|$fIPEEgukZwTkpB+_b(0;bf3Sy&-#uX z=mP)!r|167hzS7clMBQ_r#l~mA3j0fBmeFzv;W>J_786;LeWI!(8FURK%@!*azy_F z_wTpO?YHyaD1i_*pzfdE|G#NJzq`NR?MDJ9_O%5R>iU0-9yjq!_Ol@;6#wwuLMkzT z^MC*1e8C9%|KWXWRMo*y_v-SW0}$v##v`G3|1o~q{hPz(0v@>;Bn$v9`Sz(xUf+R> z^}x|YqNmO912h636!%BtKL_!ue=7oTZhwCVE>uolBgKDoq4&TE@BU*9|2HQL9J=ps z4jmr5r}www6Zp0NOAU6lP))P{HQa(w{eKyc|LlPO+359O>wu@@7xEwD*YA)2uR4H@ z*I@Jn^R3K(8$bVRCHTiA2v7XqodvIJ8-@LzLdODDO$=W4WQ zs5^VM{Wi&PZ%(IRht#USrf~n-g-h0t?|3u#rg6MRSJy%Nm@3mNNn1Y2{RrJu8sdO| zXGC<{@o~*Ms-Zq=fnoR+F#aSH=i~I#`Da@Px^0db>Cs{0{1f7(k$!ZZzRUrwHTQ1j zBA&0`_$S0;bDQXpfdt9OUFwxayi|Q4avct4bFmU3p`Kr>?!ZEiV=PKnyz)qr3rwcI z=f_B^fcbNrNi7D^dgEb^=-Uh5B1O#7UZXE}HL=`xgP*q(_xB^bhXj*fZyJ5P!Tf^E z*@A@a+ICznxi1g0druh8rDQkiSBFnL(HejI94z;7J=-;#E@(C-%h&M8fqVXtbFm^$ z;-uN_gx}YKn<}>%Hu<&}I=`|cv~OnF(Kugex@z%gPbH+=k0pN`DgD)3?CVZ#eFBeb z%hTZEt$sv(*Ra)FtAqunm)5<4r~y3bybp1hh-&U#1)eh(qLtKjN1Dyj^*I3{uJUww zr&Ik-k%8ciSON#V>g_T><*LjI;}bpw6UY{ERtrv4p5%&gX6PHJ3DbPCkCq7y?DjYf zyk`+!7Cjxbz226@$~KbI?m}61yLOi={$%u$S`pwzpMNL$H5mh8MuCr+3fW+2eGO1mYr9Az0+^X#{WCt=OY208p%EHPX@S9zfLmgu+``%tNX_Wo(*hP>aaQv>NBmD&u6}e`z z3OECP1V&@3iV|pIS$(ouJ2-d`o*+|F{+8XEG)g2`xl8c?@jO_Rtgg>WO(;8G{70qX zk45Ak&z=oiQ#6pu=pQ|9XnK9L0`&YU=Bo}mgy>b=!~0h-idh4jUm={&VnPmP3ilXU ziaQIkkhMFXYp9dv9=vGnN<43=6ensiE(*)W3k2fBE;6(rM5G z5Ez|*9CZKBG|#`3fAf~ZzX9>!KaY>z5(5L!`KJ~&6aOQgWU;#Jc;e=qYw=7b@=A)5VGa-> zJJsLp^ewUYJCr{m5O+T%AmnyHIOu9S7&FcQs$Lvm6X@dU8f*wiY~`=TQ|Nvpbjx_I z6Up7lP2i<#seQ**927W*h^`W%%gSQ|wm<;f_ojmbA_yKHuT6*on9y^H7NM|%8FFtU zmVpMjC@({S+mlbktwZVjeC4uPl<1Gvt)JN!m?-#JT>Ih#I8Jn-J>`-;faBy#W3-aA z5sK!iGGqNdH(e;`C{}f>7+}$B5*HE60_fZL_L_ z)j~(vb$C21`VH==91*}@r_*@DVUakeV$ZaPs%DcC(K#pGv`e8%{Sd^U);M*0#9Y+N zl>JFf-RHH2oirak%!DJ#_HK7=Bf6#WG22`mVcD;~SXMd3c=1VqP5P6r1w7{?%AwqV z$WN}YPd>wbo}e%%#1=ar0LSzRK+=9OHUC8L{xl@sSV-P-bc$+@|Gg9*9#P&p`Cn|> z{2oHoa%zm4T0H5ac%b$E{!F|sk(%~5U~RgDx{qFHWROyW{0J0AQ+|+BP8uQ30P8{e zy6Qy9u(~03ZJvnbZ8X2>!z&b|0ewEt5ah2UUz5hp8459v2ci1m@Kx4CgZTODWHUx> z9AE4)qgcZ?_5;5B-eSXYbe^c))|67q2-tHg*KmO^#c@mBvecK2{m2P#8oe1Qe9_V0 zp&iUc&DW|THewO>ku2U$`i4PHr42U%*+h!(F(<$#Z}%#rOKS@4FCHW zyr)98NljEUbt^p;u>@!HzM0yZBGJCd1iy5L^P9sBv160&hLN5-&4L-;-BjeIQrdTJ z8ohWi|M{Y*XT9VfOER*93H8OrO2}4}tc<48tG3P{Rg6w?#m<25M0Q^R0tKHS>P$Z; za|2Y!VoOh zOMYzQ5H4}v^qO)|={3D7|L&exxr7~Wg^qa9aC6#ohLrU?ONP}c9rj?^ED+_y+r75% zVINJm7@Jdy+k5WQwKxUtZtNMLsAcbX;2}KT;lC^0S+|-f2@UdLK@(w&uo2r6trFAM zM}RczV>d9_QBAD)ypt}iALL#v|N7vqbxhZi`6CIbmiz0vZS!{Aec6?IYkP)pubaYS zzSllV4P}#elAF50QF3-*$RChw{o9arAT(BUyT4HlUEG$!iL|E5eDK7{d&2%DHSI>l zyAfVbNvku@!;VNXU$+?oaSRr765ENdt#WQm*37%wl65Sf9j0uk4tQ55c{K8AL9#sf zvTSB_4|Q73vp7^QpeZbBIR+~kmfqF7u6yr;dkwq-Z`~5Fqv&I&^ooTiIV-jVjMh2b zMN0%Wd0U8Lq{UY!8$z$P{w5x4Js!6-sBf zZLTG2kz{6EQj{F9lm80{2h#35@)<;Qn^{S@bddWx0jHxhIBc9c|NCH&NceNh@K4zM z6gvO);Rz{`dt*ZYz9u$g7LB&4Y+ojlUN$1u^LpZP{4Q$E zo8;<4=Rvf{xC-neO}{!&;k;+3yjlh}achCWTvu4|o4Bd(DMO#RO6DwdgY7!CjuS0~ z?-MfI0^*~eL;f`(Ql-)AJii|?cBH$Az~W=JRwj`U@^7)nnod62gy5~FlX6fuJbHfB*}`F-)Mk)eM2cWuBReTMS_GQEwm1=iKk=E z11$Mlhc9eogLDIcCI5`8>}~|hlA1LFAn`%4oqLUZKOq)3lJE%v zO2h{5_y=C`4)hnQAmWN2*2ccI4(!Pkc&m%M;x*D0H5NHC+x22oMvwZHuAlm65G@}r>2PP>6FAXv<-e;NP3XOag+0C)xiUq`PI`NLs@;y4w4*q zP-Ef_<_7Mwnmj*ewbTSuJ^9!=ksoqT($YD;`vVZ9X7WzX>&w_gTF#sNJ?_C9L|0hf z69b2`DTGey@D;#Dy!Z6_s#9C}ZSQ6w|CP$XJCRcCHFEc2yBRKzy%nE48ER(Pb)99{ zlq$jr*Vhj;T8n3wtk5bPzoBm*Fug~@D3WX@ACcs2)ptFR^j>re8r7^Mp8r}mRn@#V zR=i^Mxq!of7Wz!__Ql2~ za#^m;IgoH3s)@omCsqr=l6SeH_4E3s0#M!D^kyCm9u zqslJ`Uer7nl<%Tn!p1~&3?gD5zGF3+{IUaE{wU;uI`U#^WDCh;l1iYq_42M>hWFa8 zJ86b=veKNqoJ~>-Tj-p=mU8*<{PhW2^wmfjrKw%ZzMY%VMY6K?-A&@YpT@tpQC@^! z^z_rlPxf!3F&X{OjKq7C3A%|B24s!zBMw|*WvHA$z%k9%(@r#V6W5SmC6kuK7t=Zh zQmJO`+b>xN2`Z^XBKa_E&Eo?&eJq<*;d)pUXG2ym@e0w|EuGo;SvQL2w^Z-=e6N0y zEDkRI_E}`KU?^A0L`O$Ou-XJ}KXzyrUjFu?%3Hm{mu=c^Yj-3x4|YcK?TfvjZ;=&< zjYB82Z{I;Xt-Vw3JEo(v?P`y?c0Fl)eu#+gbt%B}q4eGa&<<*XHFMI$z17}=lXM9_ zhPQg%X;Mwgi+$dWPZ_`XUiCCG9j^Rn*vBG@UqeYbW(r2Dqxrm>pV0YxB1V3Ur(t$A zv9$AQALX72sm*lr0=y9_W17kA1d z_6HQR%F7R-DHcWF3eEM}6k=KEe>f#SS0A`RX|G+4a!Y+C_(O2f_gBO_kkMrlAiuEphA@nr!yj`)t41CvKLJ*ZytChR@r6T&cVTP5bsn|XAOl)aC& z`POD{U2APw^ZcOxNH{|Lgn1ZrKy650V9rjVZ#^roC=b9H6paJa$v@}W9apiLir14W*!GX1t(Rn}S)%aCKG z?2}*6dr=;&xMR)8(AT>Nlnna;+B0 zhg-A#+wVzm4~NJyX`sGjaq5PNiB8|j*A97qc)NM4L`7Y>M0dq5m3WAm;9K6O!SkFgYTKkcsRPaaK z#9hGL8(mbd*1OHX*8D|LG0W?9%w=4cRv-{+TK+ysgw$vBp*#8cOfpydW^Yq|Zg}sG43hX|$2^aes^WALfa$Ia(m_Y|-eO`Z(>7l;$^Eh8@Xp!1H`Nx~>M{g^72CfDUZ_d5;uu0H=Z|2q2E)Qa^7LS(ZLA(K}t~F$yF8DCC{CM&> z$*^9{LQ+e(tD)(0smp2%`~$2ad|IX!Yh>kw=AFg}7*5-Q{GIcb;WeVBg(qXt$`Y(p z-%Mj6h|fQQs?XcPD~A(u$NfKyVN*;!2b~EO`HG^tjgnfu;mIRy%f`%Biqf?`d1{>ln-;Yf^V;t~KU^LP%s_>%wdZwqaHW zW?#jsxbOV)6m1f-Z~K}o^wM-3`TnS^bn9nZLrWPl5VRXh8!PYR#Z7&5JU(LWJy!L; ziZ<$mMtE_C%vD8lmo&ndYm(v?Z#iA6e@ih-V}=YM!Orb{vO4o? zVh_%aUnqBUcQPDzZ|D~&sgwHR{hB((AXqf6kaORoJStXY-3mFYg!Y(4zQ$A9i>yvx z)vv8=AbyY7++EaUxWw#lFv9kP^6X?90dnHhgl}{*xcHYd;}HRj%sl48uIVV-uE(o7RqD69_JQ z8|QhfICMgPTi@!qu+$!7W_0M4&Cmw>@U*TXQi~D$u48~YjYXVbDgQ&Fxhejn#1(oT z_F&3YX~1d_NATY7aUtntSaynYv3;Sf3MhO@X#F^OZ$3_CIGo5PP!<-5|9aEw>cj#bZuTsVL z%s{}Ajc9uH7DXV zzRy_d&P|@FAJY@2N9C{^Ts0}@XJM`o z{9K;jyO~y2?lCbnBv)_7{EU?HfmKAuoJbG;i_N~_Zhz@J>eo1eW6FDWCSpuUQTugn z6Adq^PtvVuDw?SkV~yCI_6_b)>)rF{_%Zt3WE&c?Q4W3KNu)84`aH?R?y)DvHkFQ>Pk2}F!7i+nq+eKF&B4CVLuD^L z{vjwZvPu`EwDaEX1od`sHlHQ=@O{?&p|SppTw%Snb~?A5RrMle(noQev0)j%HZ1O? zsDfxNKse40gZKdQQn18zeqfN zxR%fZ^mo^a>m!EbMy0e+LZbsl&u|PSs}y4wy<$ISf4lcbHuUXNjHDM9is4CaL^3Lk zX{IR63o7j)>e~}=H|KMe`{x@i*`|elyo9g$scwszslTS?8bj1;_y+rX#XYqS<<;0n zpYLE>ouUOi;y=NHxvvxE{po7kx!X{F35GK0`{E1GBS9prc%K1N{jINXJ7eua-ZIK* zb4!IK=HRv;MK_EWSC81BCPBLtx9P7U%6OE}%RoUqs#!)pWA*#>xMNkJ?V1$;;`$bN zYXji)6kJI>J;;NG=!k}J?-e?s6z&~oc`A&95z?0#wT zJd|eT$ZUSbtghxGGH_b^eVgqVTk(C&Jdzt7MQvEEz^L;&?t5e?Jf{KbGIwIE_eH8m zEQxPt!}qc0cf1n(yx*uUxU-V1Z!*3o{nBO=JJt{$j2CvR8`|r&pgF`o@XKGY{!Be> z(=s}Hvefc@xoa5~o*%g0akTWMGL&C*&B%#4Cc1kCRi>Nce2eAm0zE@eqsDuzM7uaE z*EVh$3!B)r@vm-9t z`(gve(ZmEofS{LTB#Pko+MKLOUpa6l&Kv0mOC~O@uPR&z$4BL>y z54j7Is|D-dZI3>-)>CC#DNm_jq^|hL7KfdbH$IznqVpCS0N^PF_BBv$&K_}Cw&8cW zUl(IxYM&seEAf!-B9UaLGX0L$#tFK5eYPvis-ZS?YfnQ%SK!XDDAC-xF|lXOGpw2f zSA^MJT4>{)JB37NrS7k`4p~UBrzO>Bcq5#ytG7vIzLpFeU2(f{ z`+EoCqHR)l_ssZ+URMJ)vo87ztzUEY(;Htsxtiq!v9F7oLT5ip?emRh*oQ=zOSZP> z>X+GGqNDi0@_b}1|5e7x8xYaNDDc!l%V5A!a6wCuC+PdAP`~ts=VW%mnQfM{%$s?} z*d&bo$=k}C6?5zBLY10CYLp4hgEz?YwCeMzCY18#W5N!&JyS!2;S}@~^h)6>N(n7H zQ?@<>H3>-$RWkK&S#HhezA6k2Y@2E*GNANT#pz%}ioA8v)tL(dI%han z6Ry;dJ^{%)#Jd<}tcePndug1n!}E}p@ODIq^h}Eo&8oCeSf$$W*lhI$QA#01`l)&-T|7&&i*VmBV7RF4C+Cf>Xgh&Gx3;#>5q4^A*dQ0{iAHtLg>? z=8_q|@DOBsP@Fq+{{pI^(Yno`B>r0jqZG`h+UQtx;|cbe;X4^0C7ql)Xp(VyJQEi# z_%UpK8G1rtv|+UQFt-VUO}FWTReYTlDp^OzT*|(`;$9woi{%w!6i(**4N8pX_vAa~ z>-0)Y&;RoL@drDvM}=3aqn<&9RoKVF8-jHbYN+H^3(#uhVZjmtUu<+;gDy?0@^0Fb zG~o>!7+MSp)TqOo2UEg}?({dkwiOzJM$s7!q*vAiHBoz->i1v4D!Ew6TyJw8nRz%! zl~FJk*Gr*ZzOf*A=@R12`aW~2SpJ*Yxrkg+dcjyjyS{Nm(xkUJjF_L}*@NW)m(7~) z6X;3czNzbj`Av%Q&|%Y?xmxpmzr^FYAO`l_EKQW!-iV*O=o5z)YUXOUG(4pAX4m+9L(sG7G zFY=NShzHMQ%4;|l^X(s(%^AmJTeB>Zoq4!6YdaoM=BsYu0vEZpA=ACvjp&d>aO5Vr z@b7weon4>f?J)1b#i|HYZmwGA6?Ls7vs%d*YA}<~)aRO~qP8Y`&0x^BvYjF1W&=8M z*(z{C>D!|itT76;njH0k;VwcH`w87JuhhZ9rG;vA$uY70aCh=XJEfDoY{T`9@jH(a zS{P*^^arqzy{Iwk*~?$%Zwi=d`DmW)2{qq;O1?xPY{+fFUU=VGHDe*r1x|wh24!;e zz3y3lbKTQ?KyiS6cEDlrC**Ebw-EmucG5VaE6$nvuHos{FBcLmt{V^alq0QG$+6}4 zN-^VnrYFbVQ8#51MT3b4Iv1Yh3w@|E-1Rcu0uK2Iek=qQ5k%XF6KCL{QH8~5bL2gs z5C;o|QB}eMeVI^I2)dPepN;dAp$?^0#s(L4Uq&tbgYI>?CT7h+4YRKbebaCPpinJWEqsy34bka*1 zBa1oq?WVfCbxH?;!#BoeZBHIjeQ=mry6(_Nb$x_xxABJ2JQZQ({X+wZl1dYU^$%7< zwxx9K0}k+awm%_(N336tg>k|l(;l9|#9M7}W1)6fXp>{ms=>28*3?vRKBgtT;kc^T zf;hC(8Nzy@6hDO*jm5yRcf4=rnVL9WyXyU@Hp%JX^u6c;xn`FE&X436_(_(`CF;OU zxy#rs3=8kN1~qX>$JjZ0*T);G*~-3uSoTed6KD&obeCwI$8nxSR$flV=@qY4t2p2r zj^$dWF7)(6ilo2F=VyVV^$0)=nN% zDusrp^XeQt#arjb`aB-0g3>MyieS?>r9EHqYVfU>zDqKbwk-|$i0}V!89sL0ak2e- z&%m7e(PrXRI0e2n{r5$DDfR%ZG&hahmT33#O`R{H+SO?&3$ZGx&U9k$!&_Pwb(UXb zzrRgthaJIy=-d!*0koR#po~V*UE7O@jLF+^GxwAnXq6oujIY>U(k~-ZW^ty@b#aa) zrKckPysDo!`1W1GyO{>cn4x#`+)W%UeKhxq_;zDf;WghlwOUV-7OVmX1}17_BR{M@7w$mNq`1 zZu`pkCD-#sOMO!^Gvc}JwdpQ(!cpFSGp-)V4|$Q;3`>TfG;%$3-VzkHoaP4^eAlRPuK9xZ-08W>|;MK+fLy zGVVJ8XA;UimQ4)Jyw`K*wxYHBX21;j)A6QsG;7kakngwKl)eW>AMoEs*>)EinS3xE zE3t}#sL>6R>){VHd1bjwPLhBh*N_HozU?R?{$N)qiEB*INOv%gh_v_6_|;%E6?I}V zk<5JUip36fkpfluUA2p`n#>V=8~am2#~!#ZMGtnh9M}A8I<%+MlEEGk7Pjd7+}jF^ zSV#{`iA;XF5FtLHk**c2JZt*=S;+x=_&IhBwzwX2IN6E9AXwuJJNj+h@;ebXT4Ly! z2_4VxKwh5mtkDfO?d++>xHoTI#FTx1XkE8h!AkJZ8PW{RWr_mxGp%FsNgI{dt8UoNDWY^Q; z(z%B&RlEkvjc!hAdnUdiiyFR2e5c>e9}cjM)1a12GMqmGly2<$JcGZC)V9}zI+{Av z%|{UJR602^J=5aEZTO&aBWCEsa~Jc2a8IwQa>my1d+9Q6swW(yyE3q1vS;}>?!+$n znnis^~Gql zmgBLk*s)MIw&y3LJ*i+RUyoR>K0G+Z0c`o7uDaF160`Vptw{k+3J7{N^Gc-Sz?L=d zKX>*^H1kp*4#ESg!DZ3&@^(zgMNIR_@9*g9pu$?>!IjETZ+r*leTsD$!eysdHeO+! zrpFKCb?nO7WO)*|r;v-|Gd<8KDVmwQ7x+BOA;^!S(*>?!w5=729SWU)>!7@sB!nl7 z_ft4Z3KX91Vr#pFK~7JcCpiD+cVHMGae8&MR!13DFn z#G4^}J5Vh7eLOuj>8XQk!sluaZi^IBn;eCqyHsa!XAx85xT=%FCMfbH{z?<^F_`ic>=o2yud|GN@mu9u!gE+Ty4fydG_gRl zC${{OcL9qHKA=^G9R*8u+0!bD{*8bMA}#59Vp9}N2(O7~bf{@`I9(#3q~~Cy&w6vA z|5UXxU6LY~Fg-v~E&418&#d;vcX!1J6uIeUMed3d?&Za)dg=X1T4fdnR7{>h0v7ty zcwdO>-}s)ng#Pp(s1cCqbZNsN=JAsQHW{Rk!hap(PMK=U;V2z>@;FUE9e}9l47B82 zD-^!0GZ_D-^q(a{ygk*E%*|OOBnUGR(ITQxjfODhX2p|z`atLM zMLhmU*YK3YkOPp0+Ke;#VUCnUVGwtoHytX9V$G3Z?&IoAr_qt8v6;1EkoXJ`(yLnw z%nHOK|BmQHhnawIXIr`*5fvTwv;JYPO731uuCB`^`lDwwZ?o9cw!xl3x%6T6c-k=P zdGDL|n@QdS^<7L<@B5q~INJ%nRlhOUxpi#CJfZ!Fjw%$|q`KO5i{3U26o@{&4mu5RMX0E`$^PMdn zW#m@zZhA+#ecptTyB@_;yywFq)qN}T((*Wmql%-i)Spz}+Ugmquz$Ru=OF(6&6;dg z3$F%?OEL(B{bh>s2mzq3xjZuO6zI4|LRM3T$G7T|Zf%|S&X!FG>G6rG@=Bl|@$WKh zdHCRUTZ1O$jU6epr-^5ZXxp$+ctTXGkAl`mlvAnLmpd10>Z@Y161f;H-6XGs<-@Z8 zLOjwujHaXUzP0_X=_w!BpPzr=z_>im4qd0CUbr>FVzdf>C)q_BZiM-M*2VC$T<^l` z%$(|Yzl5KVBqz4R8o5t=R-XAG#WhM(ex{*-Ks=bntplW%01@*u>II09{s+;MH;y33 zOV3VK-GGybN0{f>1_W~x33E5+-|v5+^@e0t7d4C8%>7Jcd#bN6Sbb_KJdG*A&8n)u z>_Yn#c!B^w0O2H-pgRKX41!$=rSIvjWRPs;|N2Q?3{rO*GYtVGV3ck0UWP5rqqwIE zDeB+kbbSUL3nX;v19m`Aza2%Gvlw}NTG%V5XeQzZw#}Hle)*sd4oRkaMEY+ia+>ou zb6SdPDaX0Tc}BD84o^d3iEjddP-hxoiuVY#M=1&8w?%Hr#g79PQubdl#QEHL16@EX z;x=G)l04yQMxpMXvXWmYi9srLU02oqNlX2mqWahQStZb=0e7@Voy>h)M0cLZJxrr= zP5YcAVSc;^|LT#$a`~&=~+~skc=(a(gf2IjWYi33j za=67z0kF8IB=kN(Kv<`D`$pr@7ewAt@ zmD`1*hso+)Ivez!B@*{bZRcX%+oglR;!~r&_yzRlLI~m&Ue@;uYtD@=e5pd2pxAKNoD3+3u29kW)}LvHpJ@S%EELzlP*6}+r%G#{Y` zE57Hyo*5pH{rK5|t}M1Rvf-}s#;wH*+O(_iQYc*$0rvX5o$N;8N%DJ#)|rWdUaxa{ z{tG*D`-l4rr|GliVgBYSrTmw52L^ICUy0_;7->u7&x`zk4>R-3Khk;IDmIzGJM&2Z zw#`t1!$q}S@1yEkLOogfo{#I@H-oXY7i8cXSw%Fa;92RMfF!);4vbrp$$^f5gRXy7 z^LO@ozB6#suTP1o_x|!`+C*MVU<|Z5WoP|4>iPfX$^Saf&V^i+%mXG&e@QH7kl%($ z-Xu#%{ZY}z9uvE!d1_P9C7v{^YlmtLD6^mNJ2+L%b|C=#-y+zXTEaSwx>>WtcC+hh;{{+D=|FgL+i11+%H9A3I8#w8U^) z9o@E;PZ(vi=arOY{hI2C+{D}T7Z_YcWFbVcKE->avl1I9w`2#L1)65^#{5GyS&_~^ zxGFwNUu22P__1m}vRyF?eIdt~W!?eBq*X+v;OFk>jnl0%{)BXWT%TP3e|r1!c&Ng* z;X$QH!ib2;zJ&-$Wm4Jqy{r|=k|ld$h9nwWB(gP_vG3W67}*oq36(8-$_Nd{`0i;{ z@3Z~B=l8yU;GCJ`oa?^!bKTdnRu$=M_E?Vb)sjO~VpqpCzClmR$kg?(w2M%er3-mY zuhngrEvh$VRzR2I9_=(7ZSlHo8=G1*>WF_tVyVHSjPdkZFE}XrATu~ysqnAExI>F5+ji4Ng8)fKDSGre5t$#6{9zMtMJ)71oZ)tpeJn>WiyI3 zt0fzKk+(mC+%AsGU`aOKkNa&FRzr>NY(XX>`P__Q#h)9V;wDCPyRN}jK;w`+)=h_H zGj)C5hb!L5gWd@Vr8!L&MUgX6<3@`Ed~x-=s(H5{rs(*N4Su3&tAEAb1 zBpwrJ0;P@zz?lz7VT|ksAP@mEejx-=*7I-f0n{+!I0%6KgyO;fB?go~zQjLAw*7o| zGQj97JZ>8gb%6rfLtj3xeF5x60czg(VR$-8+%$RwMmkS?^o?CWIPWLB{Dn}QNwAs; zd|d9@e?vZvuOvUc|2^_G)BmPk*Ix{^vOWNo&vERZEI;ONX|el@lsHp+IrV0Zf5v}6 zod5>hPWuGpRb+TQ$>x^`zp*a?yBzM1g9RZz5>$VAYY(D@XA9agHce zbKoA!`tvRlKn+O0g8$G02<^d+3E`E+uOuyChVbhk#iXR8iEE%9u>46NHqG-Z@{{ws zZVCh6;KViDPgzVKtGQ6~_PZ!dF(EoeSel8o@FiCOaioFeVv4*tvXdG>Aqpp~AYxSo zq|K_u2@wZB6W4z#6A=>EYtx@9EfKz$f)E3e@biCUeIbOZEyx)dF{u*x&5aiM%FOfB?f0QgCX%0_<@EVcF56Vz>L(BxH5j5Ni;A>+pVPPHx0K<<>5?) z?gA6{?4Ox9^}jXxF|vL}{xX^0EtK4UWug9K{3pS$T83{yew)j}{sgjl`4@a!nS4#+4!-c&MB{{m6bkL9cpgkE=B>P^q&$Pb5`=Pj`}p^FX)nPI-?2dY}CK@QiB_na^r z5yZ7Gz-^B7Uk3RfRVJR63))OWwP1V8{tLqNvD|Vs%c|TWb3A>%eL@6Qc`;Spwqukz zap|(9N#GFHQf=`@jR0PT%k{aoFljCf2`wZ%e$0M*$=+mxeZ3sa`x;(6^`&lPcMZF@ zEH+_RfZg5}2vbXq?^`cMwTr={iasX2SCZrgUQFD0nq;W$fXbQ{Tc}t=YX;SIrS4PP zuWKHyzQhY&YrKIOYuMAEc&t$AJce=7O6M-N%wrg_ z;X^qGvavZ$Mb~n*wT7_+ZBJ3&8q) zV0!LhNQ{28B*c#kc8{bL!N&wTansXV5H8#ngh`pmyUs!+0+kCC^0+M(0#yr|;-k-k zmkl)W?iPfo`RDyo=+;H3{@)oMHr(L;z+3jud;C*OVEz9MGVR{}OI+P&f0zC6uCI3L1WEn} zc{;#4fKw;_16Q_m_5Hc0ZD33@$Y1o#=@|E;|4C126}p2CY*VRm4eOlniJ$npFd#3u zjlvqcKUIrR+N-0t-zIMlQ8yKMVBg7BO8Ma-MdOv1t_4hq&&%C$%`vJX}hHczz z&&shob3pEt_U}ib|6)f4tN(3=r)=6(zJUINw=$4*n!iE19eq425B8^2Z)fh?*PZ{h zQ11-?TO z9WK0la3tZUp6fe&TOG}<{*!NJfPd4+e_KK?rvQmL$=y=Oms$EyOXEs#3 z;3ivx_pG{#oL(=K9M=E&LmqhW!|G(FlV^Eh?4z?sEKDz)aS}7Nk8InZP|#&qHx|ey zJkToUqq^C>Po}xkPg`wD8omGB9B<=+%Yhr1Y|iO*W5HJ!OFep`3jN04sa+5CP+@uF zSO_G<~aJ6uTHKb`R8p8oDRLp6Ke*G1fYr6yNI zwRYM@HJ+f?)f3DJ7{cZ%%YyK3TOCR;ogD0qI|??S)JA{6et;I7X^l!}mriAL`H}bS zj46~&{Em8LcGiLuMRM<12>92O`yd=F%%vfETk?9<=MErHFM%2yN4&efq!`#+8)1`hVYN1^W;GN0zJ|#ob0>}R&WHI=~1|MWPQG)sCo+izSxu()Tz)D_)! zi(v?x1{wqW+IWHesK!nLeWDXS6{mWg1br)(Dg6cKSUT3BAxvbDj25)+j z0&QeP)dSQHgwiIRkGnq_m7SO91v_KB0DIY;zNw(~EV(-{nMtVk z0~#Cw2YYNC^zAhHC>$vPhEFJR*e$=%f>NFBQiT4LkZ%OoD=c?YHbaN&q=Be@FR%W2 zDBu{wI-?#`s-#gA)x&3@`EFn897-OuH7AUm&Q04Jdn3BmKHGmlpRcgYP$sy${F%Us z4{WZr4qhK0_fD3|$J92e(Ag4QNNOVerPL3VaS)Vey3kD))d`f1GFW+kuZwOtOCm0v;q4-Y)RO6Bh2NQA^ogOSN3c)F*C2S~q>rl{mSo zTe~Sp8LKGy0PL_pOw=@c+fa}Ppo8>PMJS7_28~HXShtGUJ;AJ*c1b5yWwonzgUrYG zFWU8S$a41aZ~)Yh8+kK!l7HO?zo6fKL3LG!{6Qb~@3_;lh#7KSPN4>0|3EdDYzmcZ z##A)mA#4k|XOeDrk1CX=e)o8;TwhLjkkrfKINmEIAN+lc<`3tzv*sJzxpcas?2Myw zx;x$eEMlrr4o%C84bUF?0H(X*RFoPQbTj25YhAC8VN6A{eGdao)D>t_U88L(WhqZ2 zG@CV-=4oD1@Z}i^ebJ+P_4`YNxD`Qd#j}*uWqmKb4?0vOS{b_C{=8}T^*Vo$TZf8{ zPYC;Ki;S8|9dyd}XOW7R(z|hn7HkvqlUw8;Q%wLp3CP`dcKbowx*nXNPm6z`s&K^9 zt`xN7W>yXH;1_Jcw&ZFU(Uo3_11R>bn*>e#o$Oo%r#SAH;x*FSML^r0s;m~Q#G2IA z_$c+LGxYB)!pG>eUk8=0(YLbzi}h@lV4P@exGA>$#`2sy{qB+D8<~OYI@A>(x~DKk zMien0IAi9>tr3zdzqJMj4V5&;pSACG-v4$pPUWeK%SBIm0t>A5xoyHSnyuBSX*Uul z2KqH@K?VvXtj^GwB-W&g>cpkcnl|#UXII8~Kuku|D|4JOolzrW6q$WC(vk^^+&Z`PbL3o7Qh}EEWekCqg+P z=lfUPK!Uz=2Bp4`ri}l-hXr$qi@Hd#1xzb1Nyr`R2ri}H`|g6RPvFI2 zRWC>HuloWF!lzEQ^j5&@FtZlmeV<-+69b>oTP!3&=3vBR3v)aOd32X+e!pRF6Whp!`|09M^mw9 z8X&GNd&Uc#3Yj)jNk=Pck#`b08ll}vZ^)q^y7D5 z2~$grX#P+@@p{+DNA!j(A#gR4fVnk#TD`8^hcAy!vnlgXr@88>_}dqF-V8@UZ}YuA z!y%{JQXdZ!A@!j-NyogESDPvqjqmze<=Q3CSj_CM^s9cM?ox4JHDBzxR^=-1RylLbyNdg$ zuUqawzA&7KGeGr>9}lQ`b>-s$^=kf)FL`-V;n(6>yg6?hp7P3Ut()1rTbs^W;9Ms! zy7)DMD4pqztMe+JlT*s2d~0IX_|-yYRaUrF!AOt7Xs61KD>aM+(~X3y<=-LOd5(zzOLN~R}^GA7cxA2D}z6`ZduC<~Y zXm|o%zr#q2(Lu41Pb&02<(XTVbU%luo6jdqzLjC~yCZ(X!_8$}HubJVte2I4PKujl z43Y^xnuFlzF1uG+7$nd5z@jWDK_c7Uc-QL_sH9z}cMKl{-zj*87x(StJFv=b2P+uP zK4<(vyI!0ij1KlNzseynUU3K8>3G#6Y|h8=MO6f&7OlF|O!N*5Eas$D8r!Bt*Gl*? zi|z&K>H~A^3-Tn^PohqaZ&jh4t*KF6akulZ`xWuYBT<`ZdHTVLa9UMPZ^i>UZ`Ksd*iI)^(#FH`c~Bk*3nB%`Pmd$O z$Gt+Y`_ki?>YM9~E3w5UZ$|1pR;z@ZY#ehBr8Kj@cosr4BKLvVs4TsXkj;uQd26={ zVp76N-|Qfp59o|`(jQnqf8Z#2fqVtK<+M>Q`IxsTJ81^{BK~)(l_~Eu-ko9k1WgP& zV*+%ns1vj*Cu3cwm%_n=J*Q^wxXz9>s1x(9InCbPi5~px1tt$Xb{weCuP{8*JKFnc zXArU&Cs+u@Iy%8nyLHLTgpV&HN z6=+cN&0LiVS64$%7=aqrT5--8?JYc-craDqlR7XElvG5K3gZ;oljjY+L zx=;MMUS!h5*@z86!t`f7LAsO+tJ_e#z9V*6ZB$V&|FB7v5tf15V7@y|S3f|(YW9^T z7{alOJQ{`UQU3TzcP9^d>CsLX@gMq;u2MjuW#UK~8o)=+ri+eE@X?{N%OFpJ__Tvg zC!xrmQP`hkjkFV0Zjgg7V8+8u>yB18Re@oPx56b^Q;*a0ebl5qle>RM3z&{`X5Xhr zg&>46mW~_6;+Fa5M+i#VAWFCL0nVsQyl$?%Y2XKfbZQH^>&J#oTaDDu31aZ#b+A%n zA`~c+#!9}3>d{SCQOOD2Qq;;w9aBU0^`)Fv_06Om6noa5Hk)iXlW3U0-fjGZ`1(8+ zIosM_^JdtRc?|MAu?jUl&9B3vjqq05k(iW@{zTgX4+||z%zIt(%gg88{cBn~t zr&C%{&+;(#;*lY*$hO#vwDka&{U&HL8b9;s*CA+_Px zuF+NTD(^^p)a8N=Sc;HkUIQV>9qBNm7@ldq)kZ3h^;x~v^u1HloT$2HIC^|-gIjyl9kWpm#h>)&(0P_w&H^5c=*sEQ=x zvBHJq=7o1Rw;-44>5#Lc$)M&DgfKaMbr;W#iNWRtb26QlBdXIfx#Ol)5(w^EU8w7t=NQ)x zrXbyDE!EoQR@|aayYs3qexz=XbTVy&1xuf=UAm{;RAv1gN9g&mm2lqdVN~0NW z;elkGGQI~k0E-MPOIZR$5t2khJR&2oLGrDt^UjPxXI>RPZl&rlVubg7(K#Y{=Uu`1OY2?JNYISCsF-AJQ{5Y137{Jf*U*Vh;*$&9@*icR=ZB z<2ZVguUF(#B!SOrCE4#Taet7$44)vpl zwNxqAE}YRgQnCNk%e+ZdHfey;Rxtv{{dHaHPgOLgnAP|5V{!F~9sAIG%m}7R56(-h z!3U@6=%0k(!_!0)&p>VjG~1KBNpb`w=Qm$gi|&4|jXWYFXWsW1hspTz{Gt}(*3~Pj z)~&B7-m2P?zUo_c7l^FA11JL%+iIjXvm%01u6&wS?si{b=szSgUw-r2Qj=WKhFU7e zBdaoDZPR_XV3(0EbmT?e&*JEOd4R1rmpopM59^*|$qROlzjmcq>c;U6@guE-l@O!m zk8tXbjf;U}V7RlULz6sRaMSuS%(2%=+~d}c>e7pDlS6{hI*Yp}hNt)`B(HfX(vHIx z4dB!xGecf=e##EjWsj3@V%7OwbEQPCx6oIG6IO=&L`n%D@B|FnW->{f$gzlSxafgq zOw6~^LaFzaRhM0`s*e2XC5Y<3-kK{}1SvjD@iM0;iZ*)~@%H70B+-D8aKVTcZKjt~gNLNVUD{S!7ZottHl@~Yk;-@Wl z&+S*eYF;uqWbeSU+qlc<0^em4>7-Eu;CFTOU=yp!#j=hAXG0`kJAaH35zH>ei;n(Ol-Jcehgd_Dt)^`vr7nR2f>$G>7c=OcBiRO74Erv|T zUa-0q+DsC1vL24-=nyzXUzs8ZK^!1;BbTC1fN8vTixs4hp8547RAtPZA>3u~Y`W$IbQK)JOUOs+xHs4W4k;bB4M46`{1afA+I0QmBczXV8=on{}+c-Li zlNK*Mam>t~#Mxb#e(l7SuQJ6d4QR76gdAXI6ZnS|Z)#HQSM>U6hn135pxvUW7CT4{ zo77tnmBZ=q0Lr(k3SbVHi;x3j-TmeCjK?t$V4r^HONQsDT~67}mMVRMBZphKV24zX zUX95~kXDy=xTDyRn7^Y`ZTb>^%OIVT?TeowLH15K|tzlLL3+v@5OxAO)K-d z)Mkv??yOn;>r&S))QSSB^0g!^25*Qe3JgD?y)<~wS{66z)wAMbq2PlCri((LJ?a|Y+=N|Gx(nIaM+K?Y+BY#Z%!p2~=UV2Bs^Rvgtwjhv-++aT z_etd|^_;D$Dz_62b5-O%8ZaJpE@eeO%ot=Z&;gq6lm&@nZjDI7dF0ofh_!{~jQ*N| zmI(dD<0LkE5)Ih2J3EcSErx7CKGAF*+AIOh5`eBFwNy?;rrEKqH{os_2$`yD>Q?>q zUh#QZw0Y6_AkrDuBHAT?P)X>ZtJ*uPfJN9NUmWw@GvQ1@6vbPhC(!ivH5`>s2O2tG z_C8pg(x#oHA%S{r~^a~BszoCtG=5j@MT&shJf{6IY#}}Cub?xxl zJGzng$=f`zd; zVd(lFIdU&{+%640$fmy<-{&N73;N}181MNlNPb&$O|K67*GS4RnnH*Ex=Hv6_-F!R z7d|sUo|X`-j&BOc@%CJGX6m@XT%LPPI;>+)Nr^5uwk1sG$$i$n)(k^6vYStE*2qu< zEuqm5UopBG@0T;1kWT2YpTU<_3|VA8SLb0(FFNkb=j5rdyXhn_d~FYhtM1JiIhsXn6#R*E zQ{)a`yON@!#jmJS)~DJ*o|OK;Lg}ljR}SH4KPaD`7p*&gPFSN&vf1mPlbdC?=T2y! z>GhzNo6b%7avKH&>4|o6_43%IrO)+pjn!pkl?QDSE*_aQh>+5`nEc2;XP4@z@o4bJ zuNyF5H@sqVNSTA5K!XJ(-tz8(V;Ow!dwU~eIWrcP;x;OUsdmQ8VMzDv(Fw0t38ecs z)b$T8<#IiJOXR{UFMzG!oh+S>yo)j=W2PIL8drxm)Q0PwMceXZMfVnq45>bDhuR>k zhAOq+46O1J!gGjQ|PhCxQ_4z)Jvz=DK_fo8z`#fbt7lQ zDPuE^Jt>k!`y-CxBNt{jE`v1t6Q}sRFa)2KzE3>mJ=eOWo}E2vbDYI1tg&D6`0j>a zUP^`#Gei-h6YHsaLp9OV*i4mD(GywCnJNU67j_tSN;1gPSQe^u8wjNN1tvmKZRt)p&gmUf}LDTYC8e59!EtmpB8_>y9ai1~SPGAI(J4lK2Q4&76Cx76tB9jO;j0o3ztz=x!9Sc%L5Rnrhnk*^HSpzMmQzmqFR`qgtWfAHw~u-7gu& z_$#nV&&B9WrYT%rF734Q&Sov`&(t^ZXa5;nCI3|cW7Y}|aC6633}tf1qs$sz?~_L~ zk~`DZ#{@c;u?zdsXwZlq!k!cyT+nxfIzr7(sCaJZEo2u7$)HXI#zcZigz5q6%12%A zU;nPXZKw9>(Zy5=JEp}^MqHZY8de%g1S0BNSG=oL^Pzl~QkV9lBMRmB^Dh9SgFc#P zIDAH}q+-awrgzQXpAT-M3FgV^x`0V?Z-lSY`0$*}vz5HyPyH-y)JOSFzHk0v6{S-H zSBccGHbv5`8&z%WMURY?TdrLVKr-kv=V%xmYiT?#ZP%T8Ti!;cDyTC*8;1ZZavTDI_D|w<$M}R>ec9?%z7fGN1Z!0OqO_Q6>>6GYoGUCM`mT7ld(@G!J2( zl3S`6p!(Tbr+fWiD@c>;shg(=IKR@)R0N+-A5mLY}2rpw-JOAbxak+KJ|2? z3|mD~L2F}dyS%O&eHy0yASUL-Jt5hwB~aE7b0T;uj64&aeDpzQcZRiYl(L{3qqV+w z23`q(*d`ZE4$7OQwvk5z+yLLvHXhOgLlRwskA4#JUdbRI1~qrz&;%0+CTT;pSigVr zQWnEaUZSg-u$ZQI9Nx<$05s6R*0Qa{^ik__c-dJ9PTe&QH zjaiY{mMk!I)X3?H$YAsb-6)+;P^a{T`((*I&(T+26Q&w zH>Naqx{{@YAM`>_1#t_+J8*dm1{LhV9t0=f|AspGxtQU@tJZzJ1byudi`3O%SXKEN zZqz);HA>_acjJD(NB)qN!mC|*Od3zrZ-h@zxy_Ai+CS|Rxc>&*cvx!gz2vbQTsAq4 z_Or|fM(q0}k08kt2SA_USj;J7ImO~VjbN;&L5OSz)6CULtyEiFweChEtbrPtRVXu6 zz4SEyRz2qphxQXLns8V+bm{3`(!KG< z21*3uQ_YTiANcqzxK$GMfV20?!@Bwt>1){psY~6J9Cv+teS0@c9n)saF!J^%G8Ztr zLQ~&3ibC(`-U!v&m6{o-8XyY29a~HuE1NN)0>VaOD5g|TpJKEpN6tI%LnzowV~$TFa}?~oK{OW14BZpxQ1s= z+scQ5hBu!w&nLZ9oigO%JLY$DsVP~aK91O`Etz#Q=H^560BXG$*_QrAARQ6pr$(z^U}n6c9LZ$D%F zU>5Ybm~(B=n}Pi$#MV=-^3TO{ z<|7TlVGn94tC{La97}5N?M1#s=!t*YKriM*NTG8R9GBCX-&2~p+ci@(5*9O2I*^-k(LV4Ny_i$C_zK5-M&ktrtR`H4wK$P!E_Ze4j=u>8ph+W5(8*!Uw*_l)(K;%vUd1SG+$k8hgsFKnu zVFbl8uxoO}n6zPu&j6Q7fXH?SjwFwsP_eB|tH6yN><1}e!=QLXn1SWhQ#3KM)JeqL1N)}j?*zp6bKdFu>{H4XlJ4yoR9+_V;O+| zyy3%I@C*=>+V)Z8AGb>zbzAP1FlQlhuTK*L-3CRDhUTF@;r_M7CJ+FV zA0ezG@dosJ9nfDEKn6i@rKlETFLjyz1-$Zh zsEv%pWBwgVCnk>y{!KOzAiZN{23z`xG))PqOPBN!CL{8!zo`Y_-A3Cxm%$WVQ#1e& z_Y2or;5OHO(;D&ulz&$s#|{o4e^Q*hW-q`WOWc4?6hQ&%aocA4Gs$O7g`1y_i3EFpFw8#Xoom58G!lKjA@YkpV?;fT9z!PNLu&ZQ(G$ zG+LYox2^lqWnI&e#jttguZ4nwooyf)9%ob9$&CBsvp>LO5pMZ6R7t<#`Uv`{AaFa1 zv;LGEFWN)XYL^Sb2v)x_e0dw&8)7deMfsPmHyC7eE=e$$i+sS(VHa{P8HGuOq zkh&kjLSFb!bXwbov56r61q`uGmvsMTKvi(R-@<3O*LIp-t&pqW5rDIqcfrmWS4Z zJ=fRU)C5!%3d2VoNK*K*)7D#%S5mM=3GJjK5nkZ?49@+19k#(lTJYknR~YEQ^A#I} zWJIo-oTzJ=1SRHG7l_P4ptRB*v)S9R!kkGRSh_rA$NOM72-X_m^5oQnhtur(u5{$> z>`5hLghBveUq&@|k8016BFBX;6%zMxSX+O(w1g{$U9B^(w3b6T{`lA7icZh`QF~!9 ztwE)it075RE8LKpS<={pCLw9%uq{`vt^FN8;U(kF(Mh*g1)(C{2`cR;Q*zSR3Oy4# zzhIFd@hM-aC0%+ihcKe=6_e6k{n$jcvRp2D;9}4M_vpbTkZ7O=0sfE%^CfwdVBWWM zwevzo(J`J`f}nM#9{GdlJa83Nk-=4LR;1d~`%QW%HN*Ua2{1i_E01p(>B*>^^%E-9 z)zi_{vUeo+l+3tiMW4UThJWidrA!GP5G3ehaM!@t>j-he?Mm3 z=9GJmb*kNXyq?jKUI6fYny{5@Ffv2~6;f}AJ!FXK&aJ+2MMh5OU^h+LN-Cd@&zkR* z)4nGzMD*6{ah4q{WHfyo<_Q{2DgP1G|6(klzGbLy$tO-Rg8Vh ztT|Ec%pN|*eWQo-yr15qiz*V{dE1LUTISNNAlb}zi(p_AT4Y8xUs!xN5RB$WZVmoF Drk6ho literal 0 HcmV?d00001 diff --git a/apps/rebreak-native/lib/protection.ts b/apps/rebreak-native/lib/protection.ts index 5aca364..4a50bee 100644 --- a/apps/rebreak-native/lib/protection.ts +++ b/apps/rebreak-native/lib/protection.ts @@ -269,15 +269,37 @@ export const protection = { return RebreakProtection.syncWebContentDomains(opts); }, - /** Android: VpnService neu starten falls er laufen sollte (`filter_enabled`) - * aber tot ist (Reinstall / OS-Kill). Bei App-Start/Foreground aufrufen, - * damit der State nicht „an aber tot" bleibt. No-op auf iOS/Web. */ + /** Self-Heal Layer-1-Filter. Bei App-Start/Foreground/Poll aufrufen. + * + * Android: VpnService neu starten falls er laufen sollte (`filter_enabled`) + * aber tot ist (Reinstall / OS-Kill). + * iOS: prüft ob unser NETunnelProviderManager noch da ist; falls User + * „VPN löschen" in Settings getippt hat → silent recreate + * (loadOrCreateTunnelManager + saveToPreferences + startVPNTunnel). + * Wenn iOS Permission-Dialog zeigt: akzeptierte Friktion. + * Web: no-op. + */ async reconcileVpn(): Promise { - if (Platform.OS !== "android") return; - try { - await RebreakProtection.reconcileVpn(); - } catch (e) { - console.warn("[protection] reconcileVpn failed:", e); + if (Platform.OS === "android") { + try { + await RebreakProtection.reconcileVpn(); + } catch (e) { + console.warn("[protection] reconcileVpn (android) failed:", e); + } + return; + } + if (Platform.OS === "ios") { + try { + const res = await RebreakProtection.reconcileUrlFilter(); + if (res?.recreated) { + console.log("[protection] iOS Packet-Tunnel auto-recreated nach VPN-Delete"); + } else if (res?.error) { + console.warn(`[protection] reconcileUrlFilter (ios) error: ${res.error}`); + } + } catch (e) { + console.warn("[protection] reconcileUrlFilter (ios) failed:", e); + } + return; } }, diff --git a/apps/rebreak-native/modules/rebreak-protection/ios/RebreakProtectionModule.swift b/apps/rebreak-native/modules/rebreak-protection/ios/RebreakProtectionModule.swift index 17c9efc..6425da1 100644 --- a/apps/rebreak-native/modules/rebreak-protection/ios/RebreakProtectionModule.swift +++ b/apps/rebreak-native/modules/rebreak-protection/ios/RebreakProtectionModule.swift @@ -356,6 +356,61 @@ public class RebreakProtectionModule: Module { return result } + // ───────── reconcileUrlFilter: Self-Heal nach „VPN löschen" in Settings ───────── + // + // User-Bypass-Pfad: Settings → VPN → ReBreak Schutz → „VPN löschen" entfernt + // unsere NETunnelProviderManager-Config. iOS lässt diesen Button bei app-managed + // VPNs immer zu — kein MDM-Key blockt selektiv nur diesen einen Button (Apple- + // Limitation, verifiziert 2026-05-24). + // + // Counter-Strategie: bei jedem Foreground/Polling-Tick prüfen ob unser + // Tunnel-Manager noch in loadAllFromPreferences enthalten ist. Falls weg → + // silent recreate via loadOrCreateTunnelManager + saveToPreferences. Wenn iOS + // wegen frischem Manager den Permission-Dialog zeigt: akzeptierte Friktion — + // der User sieht dass sein Delete erkannt wurde. + // + // Wird vom JS-Wrapper `protection.reconcileVpn()` (iOS-Branch) gerufen, der + // wiederum aus `enforceProtection()` in app/(app)/_layout.tsx (mount + + // foreground + 15s-Poll) feuert. + + AsyncFunction("reconcileUrlFilter") { () async -> [String: Any] in + do { + let managers = try await NETunnelProviderManager.loadAllFromPreferences() + if let existing = Self.findRebreakTunnel(in: managers) { + // Config noch da — kein recreate nötig. OnDemand-Regel kümmert sich + // um Reconnect bei Netzwerk-Events, hier kein explizites startVPNTunnel. + let statusName = Self.tunnelStatusName(existing.connection.status) + return ["recreated": false, "status": statusName] + } + + // Config WEG — wahrscheinlich „VPN löschen" durch User. Silent recreate. + SharedLogStore.append("⚠️ [reconcileUrlFilter] tunnel MISSING — recreating after user-delete") + let manager = try await Self.loadOrCreateTunnelManager() + try await manager.saveToPreferences() + try await manager.loadFromPreferences() + + // Tunnel sofort starten — OnDemand fängt sonst erst beim nächsten + // Netzwerk-Event. Bewusst nicht warten/timeout: das Polling sieht den + // Connected-State spätestens beim nächsten Tick. + if let session = manager.connection as? NETunnelProviderSession { + try? session.startVPNTunnel() + } + + // App-Group-Flag spiegeln (siehe activateUrlFilter — getDeviceState liest hier). + if let d = UserDefaults(suiteName: APP_GROUP) { + d.set(true, forKey: VPN_TUNNEL_RUNNING_KEY) + d.removeObject(forKey: VPN_TUNNEL_REVOKED_KEY) + } + + SharedLogStore.append("✅ [reconcileUrlFilter] tunnel recreated") + return ["recreated": true] + } catch let e as NSError { + let errStr = "\(e.domain):\(e.code) \(e.localizedDescription)" + SharedLogStore.append("❌ [reconcileUrlFilter] failed: \(errStr)") + return ["recreated": false, "error": errStr] + } + } + // ───────── activateFamilyControls: NUR FC + denyAppRemoval ───────── AsyncFunction("activateFamilyControls") { () async -> [String: Any] in @@ -633,13 +688,25 @@ public class RebreakProtectionModule: Module { // (NEURLFilter ist nicht mehr der Default-Filter; sein Status fließt // bewusst NICHT mehr in den `urlFilter`-Slot ein.) var urlFilter = false + var mdmManaged = false do { let managers = try await NETunnelProviderManager.loadAllFromPreferences() - if let manager = Self.findRebreakTunnel(in: managers) { + // MDM-Detection: zähle wie viele Manager unsere PacketTunnel-Bundle-ID + // referenzieren. App selbst erstellt nur einen einzigen über + // `loadOrCreateTunnelManager`. Wenn der Count > 1 ist, hat MDM + // mindestens einen weiteren via `com.apple.vpn.managed`-Payload + // gepushed → MDM-managed VPN aktiv, FC-Toggle ist UI-only irrelevant. + let rebreakTunnels = managers.filter { manager in + guard let proto = manager.protocolConfiguration as? NETunnelProviderProtocol + else { return false } + return proto.providerBundleIdentifier == PACKET_TUNNEL_BUNDLE_ID + } + mdmManaged = rebreakTunnels.count > 1 + if let manager = rebreakTunnels.first { urlFilter = (manager.connection.status == .connected) } } catch { - // ignore — kein Tunnel konfiguriert → urlFilter bleibt false. + // ignore — kein Tunnel konfiguriert → urlFilter + mdmManaged bleiben false. } // FamilyControls @@ -668,6 +735,7 @@ public class RebreakProtectionModule: Module { "familyControls": familyControls, "appDeletionLock": appDeletionLock, "webContentFilter": webContentFilter, + "mdmManaged": mdmManaged, "blocklistCount": count, "blocklistLastSyncAt": lastSync ?? NSNull(), ] diff --git a/apps/rebreak-native/modules/rebreak-protection/src/RebreakProtection.types.ts b/apps/rebreak-native/modules/rebreak-protection/src/RebreakProtection.types.ts index 6e1d2fc..0bbfc84 100644 --- a/apps/rebreak-native/modules/rebreak-protection/src/RebreakProtection.types.ts +++ b/apps/rebreak-native/modules/rebreak-protection/src/RebreakProtection.types.ts @@ -14,6 +14,17 @@ export type DeviceLayers = { * FilterPolicy ≠ .none gesetzt hat (kuratierte Gambling-Domain-Liste aktiv). */ webContentFilter?: boolean; + /** + * iOS-only. True wenn MDM einen managed VPN/Tunnel-Provider mit unserer + * PacketTunnelExtension Bundle-ID pushed hat. Erkannt heuristisch via + * `NETunnelProviderManager.loadAllFromPreferences().count > 1` — App selbst + * kann nur einen eigenen Manager erstellen, ein zusätzlicher MDM-Push + * fügt einen zweiten hinzu. Konsequenz für UI: bei mdmManaged=true ist + * der per-App-FC-Authorization-Toggle irrelevant für den Schutz (Schutz + * läuft via MDM-managed VPN-Layer), die Locked-In-Card kann unabhängig + * vom familyControls/appDeletionLock-Status angezeigt werden. + */ + mdmManaged?: boolean; // Android vpn?: boolean; accessibility?: boolean; diff --git a/apps/rebreak-native/modules/rebreak-protection/src/RebreakProtectionModule.ts b/apps/rebreak-native/modules/rebreak-protection/src/RebreakProtectionModule.ts index 2101ac9..8a5bc40 100644 --- a/apps/rebreak-native/modules/rebreak-protection/src/RebreakProtectionModule.ts +++ b/apps/rebreak-native/modules/rebreak-protection/src/RebreakProtectionModule.ts @@ -55,6 +55,22 @@ declare class RebreakProtectionModule extends NativeModule; + /** + * iOS: prüft ob unser NETunnelProviderManager noch in loadAllFromPreferences + * vorhanden ist; falls nicht (User hat „VPN löschen" in Settings getippt) + * silent recreate via loadOrCreateTunnelManager + saveToPreferences + + * startVPNTunnel. Bei jedem Foreground-/Polling-Tick durch + * `protection.reconcileVpn()` aufgerufen. + * + * Wenn iOS wegen frischem Manager den Permission-Dialog zeigt: akzeptierte + * Friktion. Idempotent: wenn Tunnel noch da ist → no-op + recreated=false. + */ + reconcileUrlFilter(): Promise<{ + recreated: boolean; + status?: string; + error?: string; + }>; + /** * iOS: aktiviert NUR Family Controls (Auth + denyAppRemoval = der Lock). * Triggert iOS-Dialog "Bildschirmzeit verwalten". diff --git a/apps/rebreak-native/modules/rebreak-protection/src/RebreakProtectionModule.web.ts b/apps/rebreak-native/modules/rebreak-protection/src/RebreakProtectionModule.web.ts index 601551d..05529d2 100644 --- a/apps/rebreak-native/modules/rebreak-protection/src/RebreakProtectionModule.web.ts +++ b/apps/rebreak-native/modules/rebreak-protection/src/RebreakProtectionModule.web.ts @@ -80,6 +80,9 @@ class RebreakProtectionModuleWeb extends NativeModule { async reconcileVpn() { return { restarted: false }; } + async reconcileUrlFilter() { + return { recreated: false }; + } } export default registerWebModule(RebreakProtectionModuleWeb, 'RebreakProtection'); diff --git a/backend/prisma/migrations/drop_vip_swap_fields.sql b/backend/prisma/migrations/drop_vip_swap_fields.sql new file mode 100644 index 0000000..1d474c0 --- /dev/null +++ b/backend/prisma/migrations/drop_vip_swap_fields.sql @@ -0,0 +1,17 @@ +-- Drop VIP-Swap-Cooldown-Felder von UserCustomDomain +-- Layer 2 wird nicht mehr aus User-Custom-Domains gespeist (Pure Country-Curated). +-- Siehe docs/concepts/layer2-country-pivot.md +-- +-- ⚠️ MUSS ERST nach Code-Refactor durchgeführt werden: +-- 1. backend/server/api/custom-domains/vip-swap.post.ts gelöscht +-- 2. backend/server/api/custom-domains/index.post.ts VIP-Logic entfernt +-- 3. backend/server/api/protection/webcontent-domains.get.ts ohne User-Custom-Lookup +-- Sonst Backend startet nicht (referenziert dann nicht-existente Felder). + +-- Schritt 1: Drop vip_defer_until column +ALTER TABLE rebreak.user_custom_domains + DROP COLUMN IF EXISTS vip_defer_until; + +-- Schritt 2: Drop vip_evict_at column +ALTER TABLE rebreak.user_custom_domains + DROP COLUMN IF EXISTS vip_evict_at; diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 6ae8dbe..e6cdfc0 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -438,13 +438,9 @@ model UserCustomDomain { postId String? @map("post_id") @db.Uuid addedAt DateTime @default(now()) @map("added_at") - // VIP-Slot-Replace (Layer-2-Swap mit 24h-Cooldown): - // vipDeferUntil — die NEUE Domain ist erst ab hier Teil der VIP-Liste - // (während des Cooldowns nur via Layer 1 geschützt). - // vipEvictAt — die ERSETZTE Domain fällt ab hier aus der VIP-Liste. - // Beide NULL = kein laufender Swap. - vipDeferUntil DateTime? @map("vip_defer_until") - vipEvictAt DateTime? @map("vip_evict_at") + // Layer-2-Country-Pivot (2026-05-25): vipDeferUntil + vipEvictAt entfernt. + // Layer 2 ist nicht mehr User-Custom-gespeist — Pure Country-Curated. + // DB-Columns werden via drop_vip_swap_fields.sql gedroppt (nach Code-Deploy). submission DomainSubmission? diff --git a/backend/scripts/seed-country-blocklists.ts b/backend/scripts/seed-country-blocklists.ts new file mode 100644 index 0000000..b8d9b0d --- /dev/null +++ b/backend/scripts/seed-country-blocklists.ts @@ -0,0 +1,113 @@ +/** + * seed-country-blocklists.ts + * + * Befüllt die `curated_domains`-Tabelle mit Initial-Curation für DE/FR/GB/TN. + * Wird einmalig auf dem Server ausgeführt — idempotent (upsert). + * + * Ausführen (nach pnpm build oder direkt mit tsx): + * DATABASE_URL=... tsx scripts/seed-country-blocklists.ts + * + * HINWEIS: Diese Liste ist ein Platzhalter-Skelett. + * Die echten Top-25-50 Domains pro Land müssen vom Rebreak-Team recherchiert + * und hier eingetragen werden (Quellen: GambleAware UK, Bundeszentrale für + * gesundheitliche Aufklärung, ANJ Frankreich, öffentliche Sperrlisten). + * + * Admin-ID: UUID des Admin-Profils das die Domains "suggested" hat. + * Setze SEED_ADMIN_ID als Env-Variable oder trage hier einen Wert ein. + */ + +import { PrismaClient } from "../server/generated/prisma"; + +const db = new PrismaClient(); + +// ─── Konfiguration ───────────────────────────────────────────────────────────── + +// Admin-UUID der als "suggestedByUserId" gesetzt wird. +// Verwende den Rebreak-Admin-User aus admin_users-Tabelle. +const SEED_ADMIN_ID = process.env.SEED_ADMIN_ID ?? null; + +// ─── Country-Listen (INITIAL CURATION — von Rebreak-Team zu befüllen) ───────── + +// Format: { domain: string, country: "DE" | "FR" | "GB" | "TN" }[] +// Alle Einträge werden mit status="approved" gespeichert (direkte Admin-Curation, +// kein Review-Flow nötig für Initial-Seed). +// +// Diese Liste ergänzt die statische gambling-domains.json. Domains die bereits +// dort vorhanden sind, können hier trotzdem eingetragen werden — die +// webcontent-domains.get.ts dedupliziert automatisch. + +const INITIAL_DOMAINS: { domain: string; country: string }[] = [ + // ── DE — Top Gambling-Domains für Deutschland ───────────────────────────── + // TODO: Rebreak-Team ergänzt hier 25-50 Domains + // Quellen: https://www.bzga.de, https://www.gluecksspielsucht.de, + // öffentliche GLÜSTV-Sperrlisten + // { domain: "beispielcasino.de", country: "DE" }, + + // ── FR — Top Gambling-Domains für Frankreich ───────────────────────────── + // TODO: Rebreak-Team ergänzt hier 25-50 Domains + // Quellen: https://www.anj.fr (Autorité nationale des jeux), + // Liste noire ANJ + // { domain: "exempleecasino.fr", country: "FR" }, + + // ── GB — Top Gambling-Domains für Grossbritannien ──────────────────────── + // TODO: Rebreak-Team ergänzt hier 25-50 Domains + // Quellen: https://www.gambleaware.org, https://www.gamblingcommission.gov.uk + // GamStop-Mitgliederliste (https://www.gamstop.co.uk) + // { domain: "examplecasino.co.uk", country: "GB" }, + + // ── TN — Top Gambling-Domains für Tunesien ─────────────────────────────── + // TODO: Rebreak-Team ergänzt hier verfügbare Domains + // Quellen: eigene Recherche + Community-Feedback + // { domain: "مثال-كازينو.tn", country: "TN" }, +]; + +// ─── Seed-Logik ──────────────────────────────────────────────────────────────── + +async function main() { + if (INITIAL_DOMAINS.length === 0) { + console.log( + "[seed] Keine Domains definiert — Skelett-Script. Bitte INITIAL_DOMAINS befüllen.", + ); + return; + } + + console.log(`[seed] Starte mit ${INITIAL_DOMAINS.length} Domains...`); + + let created = 0; + let skipped = 0; + + for (const { domain, country } of INITIAL_DOMAINS) { + try { + const result = await db.curatedDomain.upsert({ + where: { country_domain: { country, domain } }, + create: { + domain, + country, + status: "approved", + suggestedByUserId: SEED_ADMIN_ID, + reviewedAt: new Date(), + }, + update: { + // Bestehende Einträge: nur Status auf approved upgraden falls noch suggested + status: "approved", + reviewedAt: new Date(), + }, + select: { id: true, domain: true, country: true, status: true }, + }); + console.log(` [${result.country}] ${result.domain} → ${result.status}`); + created++; + } catch (err) { + console.error(` FEHLER für ${domain} (${country}):`, err); + skipped++; + } + } + + console.log(`\n[seed] Fertig: ${created} verarbeitet, ${skipped} Fehler.`); +} + +main() + .catch((err) => { + console.error("[seed] Fataler Fehler:", err); + process.exit(1); + }) + .finally(() => db.$disconnect()); diff --git a/backend/server/api/admin/curated-domains/[id].patch.ts b/backend/server/api/admin/curated-domains/[id].patch.ts new file mode 100644 index 0000000..80fa10c --- /dev/null +++ b/backend/server/api/admin/curated-domains/[id].patch.ts @@ -0,0 +1,50 @@ +import { decideCuratedDomain } from "../../../db/curatedDomains"; + +/** + * PATCH /api/admin/curated-domains/[id] + * + * Admin entscheidet über einen User-Vorschlag für die Country-Curated-Liste. + * + * Body: { decision: "approved" | "rejected", note?: string } + * + * Bei "approved": Domain wird sofort von GET /api/protection/webcontent-domains + * zurückgegeben (kein Deploy nötig — Live-Query auf CuratedDomain). + * Bei "rejected": Domain verschwindet aus der Inbox. + */ +export default defineEventHandler(async (event) => { + const config = useRuntimeConfig(); + const adminSecret = getHeader(event, "x-admin-secret"); + if (adminSecret !== config.adminSecret) { + throw createError({ statusCode: 401, message: "Unauthorized" }); + } + + const id = getRouterParam(event, "id"); + if (!id) throw createError({ statusCode: 400, data: { error: "MISSING_ID" } }); + + const body = await readBody(event).catch(() => ({})); + const decision = body?.decision as string; + const note = body?.note as string | undefined; + + if (decision !== "approved" && decision !== "rejected") { + throw createError({ + statusCode: 400, + data: { error: "INVALID_DECISION", valid: ["approved", "rejected"] }, + }); + } + + try { + const result = await decideCuratedDomain(id, decision, note); + return { ok: true, ...result }; + } catch (err: any) { + if (err.code === "NOT_FOUND") { + throw createError({ statusCode: 404, data: { error: "CURATED_DOMAIN_NOT_FOUND" } }); + } + if (err.code === "ALREADY_DECIDED") { + throw createError({ + statusCode: 409, + data: { error: "ALREADY_DECIDED", currentStatus: err.currentStatus }, + }); + } + throw createError({ statusCode: 500, message: err.message ?? "Unbekannter Fehler" }); + } +}); diff --git a/backend/server/api/admin/curated-domains/index.get.ts b/backend/server/api/admin/curated-domains/index.get.ts new file mode 100644 index 0000000..5e304be --- /dev/null +++ b/backend/server/api/admin/curated-domains/index.get.ts @@ -0,0 +1,36 @@ +import { getCuratedDomains, type CuratedDomainStatus } from "../../../db/curatedDomains"; + +/** + * GET /api/admin/curated-domains?status=suggested&country=DE + * + * Admin-Inbox für User-vorgeschlagene Country-Curated-Domains. + * Query-Params: + * status — "suggested" | "approved" | "rejected" (optional, default: alle) + * country — Ländercode filtern (optional) + */ +export default defineEventHandler(async (event) => { + const config = useRuntimeConfig(); + const adminSecret = getHeader(event, "x-admin-secret"); + if (adminSecret !== config.adminSecret) { + throw createError({ statusCode: 401, message: "Unauthorized" }); + } + + const query = getQuery(event); + const status = query.status as CuratedDomainStatus | undefined; + const country = query.country as string | undefined; + + const validStatuses: CuratedDomainStatus[] = ["suggested", "approved", "rejected"]; + if (status && !validStatuses.includes(status)) { + throw createError({ + statusCode: 400, + data: { error: "INVALID_STATUS", valid: validStatuses }, + }); + } + + const rows = await getCuratedDomains({ + ...(status ? { status } : {}), + ...(country ? { country: country.toUpperCase() } : {}), + }); + + return { items: rows, total: rows.length }; +}); diff --git a/backend/server/api/custom-domains/index.post.ts b/backend/server/api/custom-domains/index.post.ts index 3f26dc4..c0a3c33 100644 --- a/backend/server/api/custom-domains/index.post.ts +++ b/backend/server/api/custom-domains/index.post.ts @@ -2,35 +2,16 @@ import { awardPoints } from "../../utils/scoring"; import { addUserCustomDomain, countActiveCustomDomains, - getWebCustomDomains, CUSTOM_DOMAIN_TYPES, type CustomDomainType, } from "../../db/domains"; import { getProfile } from "../../db/profile"; import { getPlanLimits } from "../../utils/plan-features"; import { usePrisma } from "../../utils/prisma"; -import gamblingDomains from "../../data/gambling-domains.json"; // Regex: Domain muss mindestens eine TLD haben (z.B. "casino.de", "x.co.uk") const DOMAIN_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/; -// Kuratierte Layer-2-VIP-Listen pro Land (gambling-domains.json). -const CURATED_LISTS = gamblingDomains as unknown as Record; -const VIP_COUNTRIES = ["DE", "GB", "FR", "TN"] as const; - -// Die VIP-Layer-2-Liste fasst max. 50 Domains; 20 davon sind für die -// kuratierte Liste reserviert (RESERVED_CURATED in webcontent-domains.get.ts) -// → max. 30 eigene Custom-Domains. Wird die überschritten, greift der -// VIP-Slot-Replace-Flow (Swap mit 24h-Cooldown). -const MAX_VIP_CUSTOM = 30; -const SWAP_COOLDOWN_MS = 24 * 60 * 60 * 1000; - -/** Client-`country` (Geräte-Region) → unterstützter VIP-Ländercode. Fallback DE. */ -function resolveVipCountry(raw: unknown): string { - const c = typeof raw === "string" ? raw.toUpperCase() : ""; - return (VIP_COUNTRIES as readonly string[]).includes(c) ? c : "DE"; -} - /** * Leitet Frontend-`kind` auf internen `CustomDomainType` ab. * @@ -201,37 +182,16 @@ export default defineEventHandler(async (event) => { return { alreadyGlobal: true, domain: value }; } - // ─── Web: 3-Fall-Check gegen Layer 1 (global) + Layer 2 (kuratierte VIP) ── - // - // Layer 1 (VPN/URL-Filter) = globale Blocklist. Layer 2 (webContent/VIP) = - // kuratierte gambling-domains.json + eigene Custom-Domains; greift als - // Zweitschutz, falls Layer 1 aus ist. - // 1. weder global noch kuratiert → normaler Custom-Eintrag ('active') - // 2. global UND kuratiert → schon komplett geschützt, kein Slot - // 3. global, aber NICHT kuratiert → Hinweis an User; bei addToVip=true wird - // die Domain als 'approved' gespeichert (kein Slot, erscheint nur in der - // VIP-Liste — 'approved' ist semantisch korrekt: sie IST in Layer 1). - let webAddAsApproved = false; - if (type === "web") { - const country = resolveVipCountry(body?.country); - const curatedList: string[] = CURATED_LISTS[country] ?? []; - const inVipCurated = curatedList.includes(value); - const addToVip = body?.addToVip === true; - - if (inGlobal && !addToVip) { - return inVipCurated - ? { alreadyProtected: true, domain: value } - : { inGlobalNotVip: true, domain: value }; - } - if (inGlobal && addToVip) { - webAddAsApproved = true; - } - // !inGlobal → normaler Add unten + // ─── Web: bereits in globaler Layer-1-Blocklist → kein Slot verbrennen ── + // Layer 2 (webContent) wird ab 2026-05-25 ausschliesslich Country-Curated + // gespeist — User-Custom-Domains landen NUR noch in Layer 1. Ein Custom-Slot + // für eine bereits global geblocknte Domain ist daher sinnlos. + if (type === "web" && inGlobal) { + return { alreadyProtected: true, domain: value }; } - // Slot-Limit prüfen — EIN gemeinsamer Pool für web + mail (Pro 10 / Legend - // 20). Entfällt für webAddAsApproved (approved belegt keinen Slot). - if (!webAddAsApproved) { + // Slot-Limit prüfen — EIN gemeinsamer Pool für web + mail (Pro 10 / Legend 20). + { const profile = await getProfile(user.id); const limit = getPlanLimits(profile?.plan ?? "pro").customDomains; @@ -257,7 +217,7 @@ export default defineEventHandler(async (event) => { value, "manual", type, - webAddAsApproved ? "approved" : "active", + "active", ); await awardPoints(user.id, "custom_domain_submitted", { domain: value }).catch( @@ -284,25 +244,6 @@ export default defineEventHandler(async (event) => { }); } - if (webAddAsApproved) { - return { ...data, addedToVip: true }; - } - - // VIP-Slot-Replace: bringt die neue Web-Domain die VIP-Liste (Layer 2) - // über ihr 30er-Cap, wird sie zunächst zurückgestellt (vipDeferUntil) — - // der User wählt dann im Swap-Dialog, welche eigene Domain sie ersetzt. - // Layer 1 schützt die neue Domain bereits ab sofort. - if (type === "web") { - const vipDomains = await getWebCustomDomains(user.id); - if (vipDomains.length > MAX_VIP_CUSTOM) { - await db.userCustomDomain.update({ - where: { id: data.id }, - data: { vipDeferUntil: new Date(Date.now() + SWAP_COOLDOWN_MS) }, - }); - return { ...data, vipFull: true }; - } - } - return data; } catch (err: any) { const msg = diff --git a/backend/server/api/custom-domains/suggest.post.ts b/backend/server/api/custom-domains/suggest.post.ts new file mode 100644 index 0000000..61ec614 --- /dev/null +++ b/backend/server/api/custom-domains/suggest.post.ts @@ -0,0 +1,53 @@ +import { usePrisma } from "../../utils/prisma"; +import { suggestCuratedDomain } from "../../db/curatedDomains"; + +// Unterstützte Ländercodes für Layer-2-Listen +const SUPPORTED_COUNTRIES = ["DE", "GB", "FR", "TN"] as const; +type SupportedCountry = (typeof SUPPORTED_COUNTRIES)[number]; + +// Regex: Domain muss mindestens eine TLD haben +const DOMAIN_RE = + /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$/; + +/** + * POST /api/custom-domains/suggest + * + * User schlägt eine Domain für die Country-Curated-Layer-2-Liste vor. + * Erstellt einen CuratedDomain-Eintrag mit status="suggested". + * Admin entscheidet via PATCH /api/admin/curated-domains/[id] (approve/reject). + * + * Body: { domain: string, country: string } + * + * Response: + * { ok: true, id: string, domain: string, country: string } + * oder 409 wenn domain+country-Kombination bereits existiert + */ +export default defineEventHandler(async (event) => { + const user = await requireUser(event); + const body = await readBody(event); + + const rawDomain = (body?.domain as string)?.trim().toLowerCase() ?? ""; + const rawCountry = (body?.country as string)?.trim().toUpperCase() ?? ""; + + if (!rawDomain || !DOMAIN_RE.test(rawDomain)) { + throw createError({ statusCode: 400, data: { error: "INVALID_DOMAIN" } }); + } + + if (!(SUPPORTED_COUNTRIES as readonly string[]).includes(rawCountry)) { + throw createError({ + statusCode: 400, + data: { + error: "INVALID_COUNTRY", + supported: SUPPORTED_COUNTRIES, + }, + }); + } + + const result = await suggestCuratedDomain( + user.id, + rawDomain, + rawCountry as SupportedCountry, + ); + + return { ok: true, ...result }; +}); diff --git a/backend/server/api/custom-domains/vip-swap.post.ts b/backend/server/api/custom-domains/vip-swap.post.ts deleted file mode 100644 index 4a775e7..0000000 --- a/backend/server/api/custom-domains/vip-swap.post.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { usePrisma } from "../../utils/prisma"; - -// 24h-Cooldown — identisch zu SWAP_COOLDOWN_MS in index.post.ts. -const SWAP_COOLDOWN_MS = 24 * 60 * 60 * 1000; - -/** - * POST /api/custom-domains/vip-swap - * - * VIP-Slot-Replace: die VIP-Liste (Layer 2) ist voll. Der User hat gerade eine - * neue Custom-Domain hinzugefügt (`newDomainId` — steht via `vipDeferUntil` - * bereits zurückgestellt) und wählt jetzt eine seiner EIGENEN Domains - * (`evictedDomainId`), die sie ersetzt. - * - * Beide bekommen denselben `effectiveAt` = jetzt + 24h: - * - die ersetzte Domain fällt dann aus der VIP-Liste (`vipEvictAt`), - * - die neue Domain kommt dann rein (`vipDeferUntil`). - * Layer 1 bleibt für beide unberührt — die neue Domain ist dort sofort aktiv. - */ -export default defineEventHandler(async (event) => { - const user = await requireUser(event); - const body = await readBody(event); - const newDomainId = - typeof body?.newDomainId === "string" ? body.newDomainId : ""; - const evictedDomainId = - typeof body?.evictedDomainId === "string" ? body.evictedDomainId : ""; - - if (!newDomainId || !evictedDomainId) { - throw createError({ statusCode: 400, data: { error: "MISSING_IDS" } }); - } - if (newDomainId === evictedDomainId) { - throw createError({ statusCode: 400, data: { error: "SAME_DOMAIN" } }); - } - - const db = usePrisma(); - // Beide Domains müssen dem User gehören und web-Typ sein. - const [newDomain, evicted] = await Promise.all([ - db.userCustomDomain.findFirst({ - where: { id: newDomainId, userId: user.id, type: "web" }, - select: { id: true }, - }), - db.userCustomDomain.findFirst({ - where: { id: evictedDomainId, userId: user.id, type: "web" }, - select: { id: true }, - }), - ]); - if (!newDomain || !evicted) { - throw createError({ statusCode: 404, data: { error: "DOMAIN_NOT_FOUND" } }); - } - - const effectiveAt = new Date(Date.now() + SWAP_COOLDOWN_MS); - await db.$transaction([ - db.userCustomDomain.update({ - where: { id: newDomainId }, - data: { vipDeferUntil: effectiveAt }, - }), - db.userCustomDomain.update({ - where: { id: evictedDomainId }, - data: { vipEvictAt: effectiveAt }, - }), - ]); - - return { ok: true, effectiveAt: effectiveAt.toISOString() }; -}); diff --git a/backend/server/api/protection/webcontent-domains.get.ts b/backend/server/api/protection/webcontent-domains.get.ts index 695595c..0f926ae 100644 --- a/backend/server/api/protection/webcontent-domains.get.ts +++ b/backend/server/api/protection/webcontent-domains.get.ts @@ -1,5 +1,4 @@ import gamblingDomains from "../../data/gambling-domains.json"; -import { getWebCustomDomains } from "../../db/domains"; import { usePrisma } from "../../utils/prisma"; const COUNTRY_KEYS = ["DE", "GB", "FR", "TN"] as const; @@ -9,87 +8,53 @@ const GLOBAL_LISTS = gamblingDomains as unknown as Record; const MAX_PER_COUNTRY = 50; -// Hybrid-Reservierung: die Top-N kuratierten Gambling-Domains pro Land sind -// FEST garantiert — ein User kann sie nicht mit eigenen Custom-Domains aus -// seinem Layer-2-Zweitschutz verdrängen. Custom-Domains werden daher hart auf -// (50 − RESERVED_CURATED) gekappt. Voraussetzung: gambling-domains.json ist -// nach Relevanz sortiert (die ersten RESERVED_CURATED = die wichtigsten). -const RESERVED_CURATED = 20; -const MAX_CUSTOM = MAX_PER_COUNTRY - RESERVED_CURATED; // 30 - /** * GET /api/protection/webcontent-domains * - * Liefert die VIP-Domain-Liste für den WebKit-webContent-Filter (Layer 2). - * Pro User personalisiert, Hybrid-Komposition pro Land: - * 1. Custom-Web-Domains (pending zuerst, dann approved) — gekappt auf 30 - * 2. kuratierte Gambling-Liste — füllt den Rest bis 50 auf - * → dedupliziert → hart auf 50 gekappt (Apple-Limit). + * Liefert die Country-Curated-Domain-Liste für den WebKit-webContent-Filter + * (Layer 2). Nach Layer-2-Country-Pivot (2026-05-25) ist Layer 2 vollständig + * entkoppelt von User-Custom-Domains: * - * Damit sind immer ≥ 20 kuratierte Top-Domains im Zweitschutz garantiert, - * egal wie viele Custom-Domains der User angesammelt hat. - * Response-Shape ist identisch mit der statischen Version — iOS parst das unverändert. + * Layer 1 (VPN/blocklist.bin) = User-Custom-Domains + globale Blocklist + * Layer 2 (iOS NEFilter) = ausschliesslich Country-Curated (Admin-managed) * - * Lade-Mechanismus: direkter JSON-Import (build-time gebundelt via Nitro-Bundler). - * Kein serverAssets/useStorage — kein extra Laufzeit-I/O, kein globales - * backend/data/-Verzeichnis nötig. + * Zusammensetzung pro Land: + * 1. Statische gambling-domains.json (build-time gebundelt) + * 2. DB-approved CuratedDomain-Rows (Admin-kuratiert + User-Vorschläge mit status="approved") + * → dedupliziert → hart auf 50 gekappt (Apple-Limit) * - * Pflege: backend/server/data/gambling-domains.json editieren, - * _meta.version hochzählen, _meta.updatedAt setzen, dann neu deployen. + * Optional: Query-Param ?travel=FR für Travel-Detection (Server-side Merge). + * iOS sendet origin (OS-Region) + travel (Cellular-MCC-Land) wenn verfügbar. + * Ohne Params: alle COUNTRY_KEYS werden zurückgegeben — iOS filtert selbst. + * + * Response-Shape unverändert: { _meta, DE: [], GB: [], FR: [], TN: [] } */ export default defineEventHandler(async (event) => { - const user = await requireUser(event); + await requireUser(event); // Auth bleibt — kein User-Lookup, nur Authentifizierung - // Custom Web-Domains des Users laden — parallel zu allen Country-Listen - const userWebDomains = await getWebCustomDomains(user.id); - - // Custom-Domains hart auf 30 kappen — die ersten 30 sind die höchst- - // priorisierten (getWebCustomDomains liefert pending zuerst, dann approved - // neueste-zuerst). Die restlichen 20 Slots bleiben für die kuratierte Liste. - const cappedCustom = userWebDomains.slice(0, MAX_CUSTOM); - // Dedup-Set NUR über die gekappten Customs — eine kuratierte Domain, die - // einer aus dem 30-Cap GEFLOGENEN Custom-Domain entspricht, soll über die - // kuratierte Auffüllung wieder reinkommen (sie ist ja eine Top-Domain). - const cappedCustomSet = new Set(cappedCustom); - - // DB-approved Curated-Domains (User-Vorschläge, admin-freigegeben) ergänzen - // die statische gambling-domains.json pro Land — wichtig für Länder mit - // kurzer Starter-Liste (z.B. TN). const db = usePrisma(); const approvedCurated = await db.curatedDomain.findMany({ where: { status: "approved" }, select: { domain: true, country: true }, }); + const curatedByCountry: Record = {}; for (const c of approvedCurated) { (curatedByCountry[c.country] ??= []).push(c.domain); } - // Pro Country: Custom-Domains vorne, dann globale Auffüllung, dedup, cap 50 const composed: Record = {} as Record< CountryKey, string[] >; for (const country of COUNTRY_KEYS) { - // statische JSON-Liste + DB-approved Curated des Landes, dedupliziert - const globalList: string[] = [ + const merged = [ ...new Set([ ...(GLOBAL_LISTS[country] ?? []), ...(curatedByCountry[country] ?? []), ]), ]; - - // Gekappte Custom-Domains zuerst (bereits dedupliziert da aus DB) - const merged: string[] = [...cappedCustom]; - - // Kuratierte Domains auffüllen — nur wenn noch nicht durch Custom drin - for (const domain of globalList) { - if (!cappedCustomSet.has(domain)) { - merged.push(domain); - } - } - composed[country] = merged.slice(0, MAX_PER_COUNTRY); } diff --git a/backend/server/db/curatedDomains.ts b/backend/server/db/curatedDomains.ts new file mode 100644 index 0000000..4e698ec --- /dev/null +++ b/backend/server/db/curatedDomains.ts @@ -0,0 +1,107 @@ +import { usePrisma } from "../utils/prisma"; + +export type CuratedDomainStatus = "suggested" | "approved" | "rejected"; + +/** + * User schlägt eine Domain für die Country-Curated-Layer-2-Liste vor. + * Wirft 409 wenn domain+country bereits existiert (egal welcher Status). + */ +export async function suggestCuratedDomain( + suggestedByUserId: string, + domain: string, + country: string, +) { + const db = usePrisma(); + + // Existiert bereits? Statusabhängige Antwort + const existing = await db.curatedDomain.findUnique({ + where: { country_domain: { country, domain } }, + select: { id: true, status: true }, + }); + + if (existing) { + // Bereits approved → User muss wissen dass es schon aktiv ist + if (existing.status === "approved") { + return { id: existing.id, domain, country, alreadyApproved: true }; + } + // Bereits suggested oder rejected → idempotent zurückgeben + return { id: existing.id, domain, country, alreadySuggested: true }; + } + + const row = await db.curatedDomain.create({ + data: { + domain, + country, + status: "suggested", + suggestedByUserId, + }, + select: { id: true, domain: true, country: true, status: true, createdAt: true }, + }); + + return row; +} + +/** + * Holt alle CuratedDomain-Einträge für die Admin-Inbox. + * Ohne status-Filter: alle. Mit status="suggested" → nur offene Vorschläge. + */ +export async function getCuratedDomains( + filters: { status?: CuratedDomainStatus; country?: string } = {}, +) { + const db = usePrisma(); + return db.curatedDomain.findMany({ + where: { + ...(filters.status ? { status: filters.status } : {}), + ...(filters.country ? { country: filters.country } : {}), + }, + orderBy: [{ status: "asc" }, { createdAt: "asc" }], + select: { + id: true, + domain: true, + country: true, + status: true, + suggestedByUserId: true, + createdAt: true, + reviewedAt: true, + }, + }); +} + +/** + * Admin: Domain-Vorschlag genehmigen oder ablehnen. + * reviewNote ist optional (für Reject-Begründung). + */ +export async function decideCuratedDomain( + id: string, + decision: "approved" | "rejected", + reviewNote?: string, +) { + const db = usePrisma(); + + const existing = await db.curatedDomain.findUnique({ + where: { id }, + select: { id: true, status: true, domain: true, country: true }, + }); + + if (!existing) { + throw Object.assign(new Error("CuratedDomain not found"), { code: "NOT_FOUND" }); + } + + if (existing.status !== "suggested") { + throw Object.assign( + new Error("Domain already decided"), + { code: "ALREADY_DECIDED", currentStatus: existing.status }, + ); + } + + const updated = await db.curatedDomain.update({ + where: { id }, + data: { + status: decision, + reviewedAt: new Date(), + }, + select: { id: true, domain: true, country: true, status: true, reviewedAt: true }, + }); + + return updated; +} diff --git a/backend/server/db/domains.ts b/backend/server/db/domains.ts index 3f6279b..9a2c235 100644 --- a/backend/server/db/domains.ts +++ b/backend/server/db/domains.ts @@ -21,46 +21,6 @@ export const CUSTOM_DOMAIN_TYPES: CustomDomainType[] = [ // ─── Custom Domains ─────────────────────────────────────────────────────────── -/** - * Web-Custom-Domains eines Users für die Layer-2-VIP-Komposition (type='web'). - * Nur 'rejected' wird ausgeschlossen — 'approved' Domains BLEIBEN in der VIP: - * Layer 2 ist der Zweitschutz für den Fall, dass Layer 1 (VPN/URL-Filter) aus - * ist. Eine approved Domain ist zwar in der globalen Layer-1-Blocklist, muss - * aber auch in Layer 2 gedeckt sein. - * - * Reihenfolge = Priorität für den 50er-Cap im Endpoint: - * 1. pending zuerst — KEINE Layer-1-Deckung, die VIP ist ihre einzige - * Absicherung → dürfen nie aus dem Cap fallen (≤ Slot-Limit, passen immer). - * 2. approved danach, neueste zuerst — bei Überlauf fallen die ältesten - * approved weg (via Layer 1 weiter gedeckt, daher vertretbar). - * - * Wird von GET /api/protection/webcontent-domains genutzt. - */ -export async function getWebCustomDomains(userId: string): Promise { - const db = usePrisma(); - const now = new Date(); - // VIP-Sichtbarkeit (VIP-Slot-Replace): eine Domain mit `vipDeferUntil` in der - // Zukunft ist noch NICHT in der VIP (Swap-Cooldown läuft); eine mit - // `vipEvictAt` in der Vergangenheit ist aus der VIP RAUS. - const inVip = (r: { vipDeferUntil: Date | null; vipEvictAt: Date | null }) => - !(r.vipDeferUntil && r.vipDeferUntil > now) && - !(r.vipEvictAt && r.vipEvictAt <= now); - - // pending = alles außer approved/rejected — älteste zuerst (passen alle rein) - const pending = await db.userCustomDomain.findMany({ - where: { userId, type: "web", status: { notIn: ["approved", "rejected"] } }, - orderBy: { addedAt: "asc" }, - select: { domain: true, vipDeferUntil: true, vipEvictAt: true }, - }); - // approved — neueste zuerst, damit bei Cap-Überlauf die ältesten wegfallen - const approved = await db.userCustomDomain.findMany({ - where: { userId, type: "web", status: "approved" }, - orderBy: { addedAt: "desc" }, - select: { domain: true, vipDeferUntil: true, vipEvictAt: true }, - }); - return [...pending, ...approved].filter(inVip).map((r) => r.domain); -} - export async function getUserCustomDomains(userId: string) { const db = usePrisma(); const rows = await db.userCustomDomain.findMany({ @@ -73,8 +33,6 @@ export async function getUserCustomDomains(userId: string) { type: true, postId: true, addedAt: true, - vipDeferUntil: true, - vipEvictAt: true, submission: { select: { id: true, yesVotes: true, noVotes: true, status: true }, }, diff --git a/docs/concepts/layer2-country-pivot.md b/docs/concepts/layer2-country-pivot.md new file mode 100644 index 0000000..66e492a --- /dev/null +++ b/docs/concepts/layer2-country-pivot.md @@ -0,0 +1,321 @@ +# Layer-2 Country-Pivot + +**Status:** Plan, awaiting implementation +**Decided:** 2026-05-25 (during MDM-Sandwich-Test Restore-wait) +**Owner:** Backend + iOS coordinated rollout + +--- + +## Was wir ändern + +Layer 1 und Layer 2 werden **komplett entkoppelt**. Custom-Domains fließen nur noch in Layer 1, Layer 2 wird Pure-Country-Curated. + +### Vorher (aktuell) + +``` +User Custom Domain → Layer 1 (VPN blocklist.bin) + Layer 2 (webcontent-domains, 30-Cap mit Swap) + ↑ Verwirrend für gestresste User +``` + +### Nachher (Ziel) + +``` +User Custom Domain → Layer 1 only (VPN blocklist.bin) + └ Pro: 10 Slots / Legend: 20 Slots (rückfüllbar nach Admin-Decision) + +Country-Curated Liste → Layer 2 only (webcontent-domains, 50-Cap pro Land, hard read-only für User) + └ Travel-Detection: OS-Region (Origin) + Cellular-MCC (Travel) + └ Merge wenn Travel ≠ Origin und Travel-Country-Liste existiert + +User-Suggestion → Admin-Inbox (24h-SLA wie Legend-Support) + └ Quelle: BlockerPage-Button + Lyra-Reply-Chip + └ Admin entscheidet add to country_blocklists[country] +``` + +--- + +## Konkrete Code-Änderungen + +### A) Backend + +#### A1. Schema-Migration: drop VIP-Swap-Fields +**File:** `backend/prisma/migrations/2026XXXXXX_drop_vip_swap_fields/migration.sql` + +```sql +-- Drop the VIP-Swap-Cooldown fields — Layer 2 ist nicht mehr User-Custom-gespeist +ALTER TABLE rebreak.user_custom_domains DROP COLUMN IF EXISTS vip_defer_until; +ALTER TABLE rebreak.user_custom_domains DROP COLUMN IF EXISTS vip_evict_at; +``` + +Plus Schema-Update: +- `backend/prisma/schema.prisma` — `UserCustomDomain.vipDeferUntil` und `vipEvictAt` entfernen + Kommentar-Block "VIP-Slot-Replace..." + +#### A2. webcontent-domains.get.ts — komplett vereinfachen +**File:** `backend/server/api/protection/webcontent-domains.get.ts` + +**Aktuelle Logik (raus):** +- Lädt User-Custom-Web-Domains +- Kapt auf 30 +- Merged Custom + Curated pro Land + +**Neue Logik:** +- KEIN User-Lookup mehr für Custom-Domains +- Nur Country-Curated: + - Statische `gambling-domains.json` + - Plus DB-approved `CuratedDomain` pro Country +- Optional: Query-Param `?origin=DE&travel=FR` für Multi-Country-Merge +- Hard-Cap 50 pro Country (Apple-Limit) +- Response-Shape unverändert: `{_meta, DE: [], GB: [], FR: [], TN: []}` — iOS parst weiter + +```typescript +// Skelett (Draft): +export default defineEventHandler(async (event) => { + const user = await requireUser(event); // Auth bleibt, aber kein User-Custom-Lookup mehr + + const db = usePrisma(); + const approvedCurated = await db.curatedDomain.findMany({ + where: { status: "approved" }, + select: { domain: true, country: true }, + }); + + const curatedByCountry: Record = {}; + for (const c of approvedCurated) { + (curatedByCountry[c.country] ??= []).push(c.domain); + } + + const composed: Record = {} as Record; + for (const country of COUNTRY_KEYS) { + const merged = [ + ...new Set([ + ...(GLOBAL_LISTS[country] ?? []), + ...(curatedByCountry[country] ?? []), + ]), + ]; + composed[country] = merged.slice(0, MAX_PER_COUNTRY); + } + + return { _meta: gamblingDomains._meta, ...composed }; +}); +``` + +#### A3. custom-domains/index.post.ts — VIP-Logic raus +**File:** `backend/server/api/custom-domains/index.post.ts` + +Raus: +- `MAX_VIP_CUSTOM` constant +- `vipDeferUntil` setzen bei vollem VIP-Cap +- `vipDomains.length > MAX_VIP_CUSTOM` check +- `vipFull: true` response field + +Bleibt: +- Slot-Check via `getPlanLimits(user.plan).customDomains` (Pro=10, Legend=20) +- `countActiveCustomDomains(userId)` check +- Domain-Validation + DB-Insert in `user_custom_domains` +- Refill-Logic über `domainRefill`-Flag (existing) + +#### A4. vip-swap.post.ts — DELETE +**File:** `backend/server/api/custom-domains/vip-swap.post.ts` + +Komplett löschen. ⚠️ iOS-Coordination: nach Delete müssen alle Client-Calls weg sein (sonst 404). + +#### A5. Suggestion-Endpoint: BleibtDomainSubmission? +**Question:** Aktuell hat `DomainSubmission` ein `customDomainId @unique` field — ein Submission ist an eine existing Custom-Domain gekoppelt. + +User-Wunsch: User kann Domain vorschlagen **ohne** sie selbst custom zu haben. + +→ Schema-Change nötig: +- Option A: `customDomainId` nullable machen → "freier Vorschlag" möglich +- Option B: Neuer Table `domain_suggestions` (cleaner, decoupled von User-Custom-Pool) +- Option C: `CuratedDomain.status="suggested"` reaktivieren (Tabelle existiert schon, hat `suggestedByUserId`!) + +**Empfehlung: Option C** — `CuratedDomain` mit `status="suggested"` ist bereits da. Workflow: +- User klickt "Domain vorschlagen" mit Country + Domain +- POST creates `CuratedDomain` row mit `status="suggested"`, `suggestedByUserId=user.id` +- Admin sieht in Inbox alle `status="suggested"` Einträge +- Admin entscheidet: `status="approved"` (in Liste) oder `status="rejected"` +- Approved-Liste wird automatisch von webcontent-domains.get.ts gepullt (siehe A2 — `findMany({where:{status:"approved"}})`) + +→ Nur 1 neuer Endpoint nötig: +- `POST /api/custom-domains/suggest` (User-Submit) +- `GET /api/admin/curated-domains?status=suggested` (Admin-Inbox — falls noch nicht da) +- `PATCH /api/admin/curated-domains/[id]` (Admin Approve/Reject — falls noch nicht da) + +#### A6. DomainVote-Tabelle: Decision +User hat gesagt: "Voting später, jetzt nur Admin-Decision". + +→ `DomainVote` Tabelle behalten (nichts droppen), aber Voting-API erstmal deaktivieren. Phase 2 später. + +--- + +### B) iOS (apps/rebreak-native) + +#### B1. Travel-Detection einbauen +**New file:** `apps/rebreak-native/lib/countryDetection.ts` + +```typescript +import { getLocales } from "expo-localization"; +// + NativeModule für CTTelephonyNetworkInfo (eigenes Native-Modul oder community package) + +export function getOriginCountry(): string { + return getLocales()[0]?.regionCode ?? "DE"; // Fallback +} + +export async function getTravelCountry(): Promise { + // CTTelephonyNetworkInfo.serviceSubscriberCellularProviders → MCC → CountryCode + // null wenn WiFi-only oder keine SIM +} + +export async function getActiveCountries(): Promise<{ origin: string; travel: string | null }> { + const [origin, travel] = await Promise.all([ + getOriginCountry(), + getTravelCountry(), + ]); + return { origin, travel: travel === origin ? null : travel }; +} +``` + +**Native-Modul nötig:** CTTelephony ist iOS-native, kein Expo-default. Entweder: +- expo-cellular nutzen (bietet `getCellularGenerationAsync()` aber nicht MCC direkt — prüfen) +- Eigenes Expo-Modul schreiben (in `apps/rebreak-native/modules/rebreak-protection/`) +- Community-Package wie `react-native-carrier-info` + +#### B2. useWebContentDomains.ts — Country-Param +**File:** `apps/rebreak-native/hooks/useWebContentDomains.ts` + +- Aktuell: GET `/api/protection/webcontent-domains` ohne Param +- Neu: optional `?origin=DE&travel=FR` für Server-side Multi-Country-Merge +- Sync-Trigger: bei App-Foreground + Network-Change-Event (Cellular-MCC könnte sich geändert haben) + +#### B3. BlockerPage UI-Refactor +**Files vermutlich:** `apps/rebreak-native/app/(tabs)/protection/` o.ä. + +Raus: +- VIP-Swap-Dialog +- VIP-Cap-Indicator ("30 von 30 belegt") +- "Domain swap"-Action + +Rein: +- Klare Sektion "Meine zusätzlichen Domains" (= Layer 1 Custom) + - Slot-Indicator "7 von 10" (Pro) oder "12 von 20" (Legend) +- Klare Sektion "Deutschland-Schutzliste" (= Layer 2 Country) + - Read-only Liste + - "Domain vorschlagen"-Button → Sheet/Modal +- Travel-Notice (wenn Travel ≠ Origin): + - "Du bist in Frankreich — Französische Schutzliste zusätzlich aktiv" + +#### B4. Lyra-Reply-Chip +- Chip-Text: "Domain vorschlagen" +- On-Tap: opens same Submit-Sheet + +#### B5. useCustomDomains hook — VIP-Swap raus +- Drop VIP-related state +- Add Suggest-Mutation + +--- + +### C) Admin-UI (apps/rebreak) + +**Vermutet:** Existing admin-Pfad in Nuxt-App `apps/rebreak/`. Wo genau noch zu prüfen. + +- Inbox-View für `CuratedDomain` mit `status="suggested"` +- Approve / Reject Buttons mit Reason-Input +- (Optional) Filter nach Country +- (Optional) Migration-Tooling: Existing-User-Custom-Domains als "Migration-Backlog" für Team-Review + +--- + +### D) Country-Listen Curation + +Initial-Recherche durch Rebreak-Team: +- **DE Top-25-50**: GambleAware-ähnliche Quellen + Google-Suche + Memory-Liste aus aktuellem `gambling-domains.json` +- **FR Top-25-50**: ähnlich +- **GB Top-25-50**: ähnlich (UK ist regulierter — Liste eventuell kürzer aber präziser) +- **TN Top-X**: aktuell sehr kurz, weiter mit Recherche + +Wie eingespielt: +- Manuell via Admin-UI (siehe C) ODER +- Seed-Script `backend/scripts/seed-country-blocklists.ts` + +--- + +## Migration Plan für Existing-User + +Aktuell haben User Custom-Domains die SOWOHL in Layer 1 als auch (über Hybrid-Composition) in Layer 2 landen. + +Nach Pivot: +- Layer 1: User-Custom-Domains bleiben unverändert (Pro=10/Legend=20 Slots, refillable) +- Layer 2: User-Custom-Domains werden NICHT mehr gepullt — nur Country-Curated +- Wenn ein User eine Custom-Domain hatte die "good idea for Country-List" ist → Admin-Migration-Backlog (manual review) + +**User-Communication:** In-App-Notification "Wir haben unseren Schutz vereinfacht — deine eigenen Domains bleiben, Layer 2 zeigt jetzt die Schutzliste für dein Land." + +--- + +## Bug: 5-10min Sync-Lag — Hypothese & Test-Plan + +User-Beobachtung: Custom-Domain-Add ist 5-10min delayed (sollte sofort sein). + +**Memory** sagt: Server-side instant, Client-Lag <60s. + +**Mögliche Lag-Quellen:** +1. `blocklist.bin` Rebuild-Frequency (cron-based?) — `backend/server/plugins/blocklist-cron.ts` +2. iOS-NEFilter Content-Reload-Trigger +3. Server-side Cache vor Endpoint +4. iOS DNS-Cache der OS (Apple-side, kaum kontrollierbar) + +**Hypothese:** Cron-Build von `blocklist.bin` läuft alle N Minuten. Bei N=10 wäre das genau das beobachtete Verhalten. + +**Test:** +- Check `backend/server/plugins/blocklist-cron.ts` für Interval +- Add Domain → log timestamp +- Tail Server-Log: wann wird blocklist.bin rebuild? +- Diff = lag-source + +**Fix (wenn Cron-Lag):** +- Cron-Frequency erhöhen (z.B. 1min) +- ODER Event-Driven Rebuild bei Custom-Add (POST trigger) + +--- + +## Aufwand-Schätzung pro Block + +| Block | Effort | Risk | +|---|---|---| +| A1 Schema-Migration | 1h | low (reviewable) | +| A2 webcontent-domains refactor | 2h | medium (response shape change → iOS coordination) | +| A3 custom-domains/index.post simplify | 2h | medium (regression-risk auf existing slots-logic) | +| A4 vip-swap.post.ts delete | 5min | high (iOS-coordinated) | +| A5 Suggest-Endpoint | 4h | low (greenfield) | +| A6 DomainVote behalten | 0h | low | +| B1 Travel-Detection iOS | 8h | high (native module work) | +| B2 useWebContentDomains | 2h | low | +| B3 BlockerPage UI-Refactor | 8h | high (UX-heavy) | +| B4 Lyra-Reply-Chip | 2h | low | +| B5 useCustomDomains | 3h | medium | +| C Admin-UI | 6h | low | +| D Country-Listen Curation | 6h | low (mostly research) | +| Bug: 5-10min-Lag-Root-Cause | 4h | low (Recherche + Fix) | +| Migration + Comms | 4h | low | +| **Total** | **~50h = ~6-8 Arbeitstage** | | + +--- + +## Vorgeschlagene Rollout-Reihenfolge (sicherste) + +1. **Phase 0** — Schema-Migration (A1) — non-breaking +2. **Phase 1** — Suggest-Endpoint (A5) + Admin-UI (C) — additive, kein Breakage +3. **Phase 2** — Country-Listen initial befüllen (D) +4. **Phase 3** — iOS Travel-Detection + UI (B1-B5) — koordiniert mit Backend +5. **Phase 4** — Backend Refactor (A2 + A3 + A4) — gleichzeitig mit iOS-Release +6. **Phase 5** — Migration-Comms an existing User +7. **Phase 6** — Bug 5-10min Lag analysieren + fixen + +--- + +## Offene Fragen für User-Klärung + +- **Cellular-MCC NativeModule**: expo-cellular reicht oder eigenes Modul? (= recherche-Aufgabe) +- **Admin-Team**: Wer hat Zugriff auf Admin-Inbox? Nur du, oder auch Olfa/Rayén? +- **Travel-List Edge-Case**: was wenn User in DE mit Roaming auf US-Provider (z.B. AT&T-SIM in DE) — Cellular-MCC sagt US, OS-Region sagt DE. Was tun? +- **Notification bei Admin-Decision**: Push + In-App-Toast, oder nur In-App? +- **Existing-User-Custom-Migrations-Inbox**: alle Domains durchschauen oder nur Top-N pro Country (z.B. die häufigsten 50)? diff --git a/ops/mdm/PHASES.md b/ops/mdm/PHASES.md index ce1083c..af54699 100644 --- a/ops/mdm/PHASES.md +++ b/ops/mdm/PHASES.md @@ -1,5 +1,13 @@ # MDM Setup — Phasen +## Revisions-Log + +| Datum | Was geändert | +|-------------|-----------------------------------------------------------------------------------------------| +| 2026-05-10 | Initial: Phasen A–G mit Factory-Reset-Approach für Phase F | +| 2026-05-24 | Phase F pivotiert auf Backup-Sandwich (TechLockdown-Stil); Scope erweitert um DNS-/VPN-Lock | +| 2026-05-24-late | DEV-Test zeigt: VPN-Restrictions blocken Rebreak-eigene NEVPNManager-Calls. Scope korrigiert: VPN-Restrictions raus, DNS bleibt als Fallback-Layer. Saubere MDM-VPN-Lösung als Phase F.2 | + ## Phase A ✅ Server-Bootstrap Erledigt vor 2026-05-10. @@ -201,21 +209,76 @@ Server-Status: Reaktivierung: User sagt „Phase E GO", wir verifizieren Domain in Resend, senden, fertig. Files bleiben bis dahin auf Server. -## Phase F ⏳ Device-Enrollment +## Phase F ⏳ Device-Enrollment via Backup-Sandwich -Wartet auf Phase E. +**Revidiert 2026-05-24** — alter Plan (Factory-Reset + Apple Configurator) war User-Friction-Killer. Niemand reset sich freiwillig sein iPhone. Neuer Plan: Backup-Sandwich-Approach wie TechLockdown / iMazing Configurator Edition. -Was passiert: -1. iPhone auf Werkseinstellungen zurücksetzen (Backup vorher!) -2. Während Setup: iPhone via USB-C mit Mac verbinden, Apple Configurator 2 öffnen -3. In Apple Configurator 2: Gerät preparieren (Supervised Mode aktivieren) -4. MDM-Enrollment-Profil von `https://mdm.rebreak.org/enroll` auf Gerät installieren -5. Verifyieren dass Profil als "nicht entfernbar" markiert ist -6. Apps installieren (ReBreak, etc.) +Phase F ist NICHT mehr auf Phase E blockiert (Ina-Email-Distribution kann nachgeholt werden). -**Hinweis zum Supervised Mode:** Ohne Supervision kann das MDM-Profil vom User entfernt werden. Supervision braucht einmalig USB + Apple Configurator. Danach ist OTA-MDM-Update möglich. +### Mechanismus -**Scope-Constraint (User-bestätigt 2026-05-10):** Profil enthält NUR `allowAppRemoval=false` für Bundle-ID `org.rebreak.app` + `allowMDMProfileRemoval=false`. KEIN App-Store-Block, keine weiteren Restrictions. iOS-App-Store hat keine Echtgeld-Casino-Apps (Apple-Policy), Browser-Casinos werden von ReBreak's NEFilter geblockt. +``` +1. Backup (idevicebackup2 encrypted) → vollständig auf Mac +2. Supervise (cfgutil prepare) → wiped Gerät, Supervised-Flag wird gesetzt +3. Restore (idevicebackup2 restore) → Daten zurück, Supervised-Flag bleibt persistent +4. Enroll (mobileconfig install) → via QR-Code aus Rebreak-App, OTA über mdm.rebreak.org +``` + +Find-My-Disable ist Voraussetzung für Step 2 (Activation Lock blockt sonst den Wipe). Apple-ID-Passwort des Users wird live abgefragt — nicht automatisierbar. + +### Komponenten dieser Phase + +- `ops/mdm/bootstrap-tool/` — Bash-Scripts orchestrieren Backup → Supervise → Restore auf User-Mac (Mac-only Phase 1; Windows = Phase 2 via iMazing-Lizenz oder libimobiledevice-Erweiterung) +- `ops/mdm/profiles/rebreak-iphone-protection.mobileconfig` — Profil-Template mit den unten genannten Restrictions +- `backend/server/api/mdm/enroll.get.ts` — User-spezifisches signed Profil ausliefern, plus QR-Code-Endpoint +- `apps/rebreak-native/lib/mdm.ts` + `app/(protection)/mdm-setup.tsx` — Lyra-geführter Onboarding-Flow in der App + +### Scope (erweitert 2026-05-24, revidiert 2026-05-24-late nach DEV-Test) + +Profil enthält: + +| Restriction | Wirkung | +|----------------------------------------------|--------------------------------------------------------------| +| `allowAppRemoval = false` | Rebreak (und alle anderen Apps) nicht löschbar via Long-Press — zeigt nur "Vom Home-Screen entfernen", App bleibt in Mediathek (verifiziert auf TechLockdown-supervisem iPhone 2026-05-24) | +| `PayloadRemovalDisallowed = true` | Profil nicht via Settings → Allgemein → VPN/Geräteverwaltung entfernbar | +| `allowEraseContentAndSettings = false` | User kann iPhone nicht via Settings → Reset wipen | +| `allowUIConfigurationProfileInstallation = false` | User kann keine konkurrierenden Profile installieren | +| DNS-Settings-Payload (DoH) | System-DNS auf `dns.rebreak.org/dns-query` gelocked — always-on Fallback-Schicht | + +**VPN-Restrictions bewusst RAUS** (Test-Befund 2026-05-24): +`allowVPNCreation=false` blockt auch Rebreak-eigene `NEVPNManager`-Aufrufe ("Permission denied"). Apple unterscheidet im API-Call nicht zwischen User und App. Konsequenz: +- App-VPN (Rebreak NEPacketTunnel) bleibt App-managed + user-toggleable — wie heute +- MDM-DNS-Payload ist always-on Fallback: auch wenn User Rebreak-VPN ausschaltet, DNS-Filter greift weiterhin +- Bypass-Vektor: User installiert 3rd-Party-VPN (z.B. ExpressVPN). Akzeptiert für Prototype — 5-min-Friktion, trifft planenden Rückfall nicht impulsiven +- Saubere Lösung wäre **Phase F.2**: MDM-pushed-VPN mit `ProviderBundleIdentifier=org.rebreak.app.PacketTunnelExtension`, dann braucht App-Code kein eigenes `NEVPNManager.saveToPreferences` mehr → echtes "VPN nur via MDM" + +Bewusste Trade-offs: +- `allowAppRemoval=false` ist GLOBAL — kein per-Bundle-ID-Lock möglich ohne MDM-managed-Convert (zusätzlicher InstallApplication-Command, Phase F.5 später). Für Prototype akzeptiert: User der sich self-bindet darf auch andere Support-Apps nicht löschen — Feature, kein Bug. +- Determinierter User kann via zweitem Mac unsupervisen (ABM-ADE wäre der einzige echte Hard-Lock, ist aber strukturell nicht erreichbar für Consumer-iPhones). Akzeptabel für DiGA-Sucht-Kontext: wir hoben Friktion, nicht Festung. + +Bewusst NICHT im Scope: +- KEIN App-Store-Block (Casino-Apps gibt's eh nicht im iOS-App-Store) +- KEINE Web-Content-Filter-Payload (Browser-Casinos werden vom Rebreak-NEFilter geblockt) +- KEINE Restriktionen die nicht direkt mit Casino-Bypass-Prevention zu tun haben + +### Hardware-/Tool-Voraussetzungen + +- Mac mit macOS (User-Mac, NICHT Server-Mac) — für cfgutil + libimobiledevice +- USB-Kabel iPhone↔Mac +- Apple Configurator 2 (kostenlos, Mac App Store) — für `cfgutil` CLI +- libimobiledevice via `brew install libimobiledevice` — für `idevicebackup2` +- Supervision-Identity einmalig generiert via cfgutil (persistent, gleicher Mac reused) +- iPhone mit deaktiviertem Find-My (live während Setup) + +### Akzeptanz-Test (M2) + +Auf einem physischen Test-iPhone nach kompletter Sandwich-Sequenz: +- [ ] `Settings → Allgemein → Info` zeigt "Dieses iPhone wird verwaltet/beaufsichtigt" +- [ ] Long-Press auf Rebreak-Icon → kein "App löschen" mehr +- [ ] `Settings → VPN → Rebreak` → Toggle disabled / nicht entfernbar +- [ ] `Settings → Allgemein → VPN, DNS und Gerätemanagement` → Profil zeigt "Nicht entfernbar" +- [ ] Daten/Apps/Login-States/iMessage-History intakt nach Sandwich +- [ ] Rebreak-App erkennt MDM-Enrollment-Status via Backend-Check und unlockt Pro/Legend-Schutz-UI ## Phase G ⏳ iPad-Enrollment (optional, später) diff --git a/ops/mdm/bootstrap-tool/README.md b/ops/mdm/bootstrap-tool/README.md new file mode 100644 index 0000000..97665e3 --- /dev/null +++ b/ops/mdm/bootstrap-tool/README.md @@ -0,0 +1,101 @@ +# ReBreak Supervise Bootstrap-Tool + +CLI-Wrapper das ein **unsupervised iPhone** in den Zustand **supervised + Rebreak-Schutz-Profil installiert** überführt, ohne sichtbaren Daten-Verlust. + +Mechanismus: TechLockdown-Stil **Backup → Wipe → Supervise → Restore → Install-Profile**. Aus User-Perspektive bleiben Apps + Daten + Login-States intakt; technisch wird das iPhone aber kurz gewiped. + +## Status + +Prototype, Mac-only. Einige Steps (cfgutil-Syntax, idevicebackup2-Restore-Flags) sind erst nach Hardware-in-Loop-Test final verifiziert — markiert mit `VERIFY ON DEVICE` im Code. + +## Voraussetzungen (Mac des Users) + +| Tool | Installation | +|-------------------------------|---------------------------------------------------------------| +| macOS | (jede aktuelle Version) | +| Apple Configurator 2 | `xcrun simctl install ...` nein — Mac App Store, kostenlos | +| `cfgutil` CLI | Wird mit Apple Configurator 2 mitgeliefert (in `.app/Contents/MacOS/`) | +| libimobiledevice | `brew install libimobiledevice` | +| OpenSSL | Auf macOS vorhanden (`/usr/bin/openssl`) | +| Supervision-Identity (.p12) | Einmalig: siehe `SUPERVISION-IDENTITY-SETUP.md` | + +## Voraussetzungen (iPhone des Users) + +- iPhone wird per USB-C mit dem Mac verbunden +- iPhone ist entsperrt +- "Diesem Computer vertrauen?" wurde auf dem iPhone bestätigt +- **Find My iPhone ist DEAKTIVIERT** (sonst blockt Activation Lock den Wipe-Step) + - `Settings → [Name] → iCloud → Wo ist? → Mein iPhone suchen → Aus` + - Verlangt Apple-ID-Passwort des Users +- iCloud-Backup-Sync ist NICHT mitten in der Sitzung aktiv (idle) + +## Schnellstart + +```bash +# Einmalig: Supervision-Identity generieren (siehe separates Doc) +cat SUPERVISION-IDENTITY-SETUP.md + +# Dry-Run zum Check ob alle Deps + iPhone gefunden werden +./rebreak-supervise.sh --dry-run + +# Echter Lauf +./rebreak-supervise.sh + +# Falls's mittendrin failt: ab dem letzten OK-Step weitermachen +./rebreak-supervise.sh --resume +``` + +## Was das Script macht — Schritt für Schritt + +1. **Preflight** — Deps prüfen, iPhone-UDID detecten, Supervision-Identity laden, Profil-Plist validieren +2. **Backup** — `idevicebackup2 backup --encryption on` mit zufälligem Passwort (in `~/.rebreak-supervise/backup-pass-.txt`) +3. **Supervise** — `cfgutil prepare --supervised --supervisor-host-certs ` wiped Gerät, setzt Supervised-Flag, rebootet +4. **Restore** — Nach Reconnect: `idevicebackup2 restore` mit selbem Passwort. Supervised-Flag bleibt persistent über Restore (Apple-Verhalten: Restore preserved supervision/enrollment-state, nicht den User-State der vorher unsupervised war) +5. **Install-Profile** — `cfgutil install-profile rebreak-iphone-protection.mobileconfig` + +## State + Logs + +| Artefakt | Zweck | +|-------------------------------------------------------|-----------------------------------------------| +| `~/.rebreak-supervise/state-.env` | Welche Steps erledigt (für `--resume`) | +| `~/.rebreak-supervise/backups//` | Encrypted Backup | +| `~/.rebreak-supervise/backup-pass-.txt` | Backup-Encryption-Passwort (chmod 600) | +| `~/.rebreak-supervise/supervision-identity.p12` | Persistent über alle Devices/Sessions | +| `~/.rebreak-supervise/log-.txt` | Komplettes Log dieser Session | + +Alles unter `~/.rebreak-supervise/` (chmod 700). Niemals committen. + +## Failure-Pfade + +| Wann | Was tun | +|---------------------------------------|-------------------------------------------------------------------------------------------| +| Preflight fail (Deps fehlen) | Deps installieren, neu starten | +| Backup fail (zB iPhone-Disconnect) | USB-Kabel checken, Trust-Dialog erneut. Backup neu starten (Script ist idempotent) | +| Supervise fail (Find-My noch aktiv) | iPhone ist NICHT gewiped, läuft normal weiter. Find-My deaktivieren, dann `--resume` | +| Supervise fail (Identity ungültig) | Neue Identity generieren (siehe Setup-Doc), dann `--resume` | +| Restore fail (Passwort verloren) | **NICHT recoverbar.** iPhone ist gewiped + Setup-Assistant aktiv. User muss neu einrichten oder iCloud-Backup nutzen | +| Profil-Install fail | Manuell via `cfgutil install-profile ` oder per AirDrop des `.mobileconfig` | + +**Kritisch:** Das Backup-Passwort darf nicht verloren gehen zwischen Step 2 und Step 4. Es liegt automatisch in `~/.rebreak-supervise/backup-pass-.txt` — falls der Mac dazwischen neu startet ist es noch da, aber für Disaster-Recovery sollte der User es notieren. + +## Post-Supervise: iOS-Setup-Stage + +Nach Step 3 (Wipe) zeigt das iPhone wieder den "Hallo"-Screen. Folgender Stand wird vom Script erwartet bevor Restore startet: + +- iPhone-Sprache wählen, Land/Region bestätigen +- Bis zum Screen "Apps & Daten übertragen" durchklicken +- **NICHT** "Aus iCloud-Backup wiederherstellen" wählen — wir restoren via libimobiledevice vom Mac +- "Nicht übertragen oder zurücksetzen" wählen ODER zurück bis zum Stage "Computer-Verbindung" + +Das ist die einzige Stelle wo der User aktiv interagieren muss. Das Script prompted bevor es weiter macht. + +## Cross-Plattform-Pfad (Phase 2) + +- **Windows**: braucht entweder iMazing-Lizenz (kommerzielle Whitelabel-Option, ähnlich TechLockdown) oder substantielle libimobiledevice-Erweiterung (Supervise ist heute nicht supported, GitHub-Issue offen). Aktuell out-of-scope. +- **GUI** (Tauri/Electron Wrapper): wenn der Bash-Flow stabil läuft. Vorerst CLI reicht für Validierung. + +## Bekannte Tücken + +- **iOS 18+ cfgutil-Syntax** ist nicht 100% verifiziert. Apple-Doku-Lücken — `cfgutil` ist intern bei Apple priorisiert. `VERIFY ON DEVICE`-Marker im Script-Code zeigen wo's exakt zu testen ist. +- **idevicebackup2 + iOS 18**: libimobiledevice-Maintainer holen auf, neuere iOS-Versionen können Edge-Cases haben (z.B. neue Backup-Formate). Falls Backup-Tool failed mit "unknown response": auf libimobiledevice HEAD updaten (`brew install --HEAD libimobiledevice`). +- **Supervised-State-Persistenz nach Restore**: Apple-Doku sagt ja, Community-Reports sagen meistens ja, Edge-Cases existieren. Im Akzeptanz-Test (M2) explizit zu verifizieren. diff --git a/ops/mdm/bootstrap-tool/SUPERVISION-IDENTITY-SETUP.md b/ops/mdm/bootstrap-tool/SUPERVISION-IDENTITY-SETUP.md new file mode 100644 index 0000000..7173323 --- /dev/null +++ b/ops/mdm/bootstrap-tool/SUPERVISION-IDENTITY-SETUP.md @@ -0,0 +1,81 @@ +# Supervision-Identity einmaliger Setup + +Eine Supervision-Identity ist ein Cert+Key-Paar (im `.p12`-Container), das beweist dass DEIN Mac der "Supervisor" eines iPhones ist. Apple verlangt das, damit ein iPhone nicht von beliebigen Macs supervised werden kann. + +**Einmal pro Mac generieren. Persistent für alle Devices und alle Sessions.** + +Ohne Identity scheitert `cfgutil prepare --supervised` mit einem Cert-Fehler. + +## Wo die Identity liegen muss + +Das Bootstrap-Tool erwartet die Identity standardmäßig hier: + +``` +~/.rebreak-supervise/supervision-identity.p12 +``` + +Override via Env-Variable `SUPERVISION_IDENTITY_P12`. + +## Pfad A — Via Apple Configurator 2 (GUI, empfohlen) + +1. **Apple Configurator 2** öffnen +2. Menü-Bar → `Apple Configurator 2 → Einstellungen → Server` (oder neuere Versionen: `Organizations`) +3. `+` → `New Supervision Identity...` +4. Name vergeben (z.B. `ReBreak Supervision Chahine 2026`) +5. Cert wird im **macOS-Schlüsselbund** gespeichert +6. Schlüsselbund öffnen (`Schlüsselbundverwaltung.app`): + - Suche nach dem Namen aus Schritt 4 + - Cert + Private-Key sollten beide sichtbar sein + - Rechtsklick auf Cert → **„2 Objekte exportieren..."** + - Format: **„Personal Information Exchange (.p12)"** + - Speichern als `~/.rebreak-supervise/supervision-identity.p12` + - Passwort vergeben — leer lassen für einfachen Bootstrap-Tool-Zugriff, ODER Passwort setzen und manuell in `~/.rebreak-supervise/supervision-identity.pass` ablegen (chmod 600) + +## Pfad B — Via cfgutil CLI + +VERIFY ON DEVICE: exakte Flags noch zu validieren. Apple's CLI-Doku ist hier dünn. + +```bash +mkdir -p ~/.rebreak-supervise +CFGUTIL=/Applications/Apple\ Configurator.app/Contents/MacOS/cfgutil + +# Identity in Schlüsselbund generieren (cfgutil-eigener Aufruf) +"$CFGUTIL" generate-supervision-identity \ + --name "ReBreak Supervision $(whoami) $(date +%Y)" + +# Dann via Pfad-A Schritt 6 exportieren (das CLI-Tool kann nicht direkt .p12 schreiben) +``` + +## Verifikation + +```bash +ls -l ~/.rebreak-supervise/supervision-identity.p12 +# Sollte: -rw------- 1 ... + +# Cert-Inhalt prüfen (Passwort eingeben falls gesetzt) +openssl pkcs12 -info -in ~/.rebreak-supervise/supervision-identity.p12 -nokeys +``` + +Erwartete Ausgabe enthält etwa: +``` +subject=/CN=Apple Configurator/.../CN=ReBreak Supervision ... +``` + +## Sicherheit + +- Die `.p12` ist **gleichwertig zur Macht** ein iPhone zu supervisen — schützen wie einen SSH-Key +- Niemals in Git committen +- Bei Mac-Wechsel: Identity exportieren + auf neuen Mac übertragen (oder neu generieren — alte Devices akzeptieren dann nur die alte) +- Bei Identity-Verlust: alte enrollte Devices bleiben weiter supervised, aber dieser Mac kann sie nicht mehr managen. Neuen Mac → neue Identity → User-Devices brauchen Re-Setup-Sandwich + +## Bei Fehler "supervision identity not trusted" + +Apple verlangt manchmal dass die Identity im **System-Schlüsselbund** sitzt (nicht User). Workaround: + +```bash +sudo security add-trusted-cert -d -r trustRoot \ + -k /Library/Keychains/System.keychain \ + ~/Path/to/exported-cert-only.cer +``` + +Oder via Apple Configurator GUI re-generieren — die GUI handhabt das korrekt. diff --git a/ops/mdm/bootstrap-tool/rebreak-supervise.sh b/ops/mdm/bootstrap-tool/rebreak-supervise.sh new file mode 100755 index 0000000..439dac8 --- /dev/null +++ b/ops/mdm/bootstrap-tool/rebreak-supervise.sh @@ -0,0 +1,407 @@ +#!/usr/bin/env bash +# +# rebreak-supervise.sh +# -------------------- +# Backup-Sandwich-Bootstrap für iPhone-Supervision ohne sichtbaren Daten-Verlust. +# +# 1. idevicebackup2 encrypted Backup -> ~/.rebreak-supervise/backups// +# 2. cfgutil prepare --supervised -> wiped iPhone, Supervised-Flag setzen +# 3. idevicebackup2 restore -> Daten zurück, Supervised-Flag persistiert +# 4. cfgutil install-profile -> ReBreak-Schutz-Profil installieren +# +# Voraussetzungen (siehe README.md): +# - macOS +# - Apple Configurator 2 (App Store) + cfgutil im PATH +# - libimobiledevice (brew install libimobiledevice) +# - Supervision-Identity einmalig generiert (siehe SUPERVISION-IDENTITY-SETUP.md) +# - iPhone via USB-C verbunden, Find-My DEAKTIVIERT, Code entsperrt +# - Vertrauenshandshake (Trust-Dialog auf iPhone) bestätigt +# +# CLI: +# rebreak-supervise.sh [--dry-run] [--state-dir DIR] [--profile PATH] [--resume] +# +# Status: PROTOTYPE. Einige Steps (cfgutil-Aufruf, Verify-Pfad) sind erst auf +# physischem Test-iPhone final verifiziert. Markierungen "VERIFY ON DEVICE" im +# Code zeigen wo Hardware-in-Loop noch nachgehärtet werden muss. + +set -euo pipefail + +# ------------------------------------------------------------------------------ +# Konfiguration + Defaults +# ------------------------------------------------------------------------------ + +STATE_DIR="${REBREAK_STATE_DIR:-$HOME/.rebreak-supervise}" +PROFILE_PATH_DEFAULT="$(cd "$(dirname "$0")/.." && pwd)/profiles/rebreak-iphone-protection.mobileconfig" +PROFILE_PATH="" +DRY_RUN=0 +RESUME=0 + +CFGUTIL="/Applications/Apple Configurator.app/Contents/MacOS/cfgutil" +SUPERVISION_IDENTITY_P12="${SUPERVISION_IDENTITY_P12:-$STATE_DIR/supervision-identity.p12}" +SUPERVISION_IDENTITY_PASS_FILE="${SUPERVISION_IDENTITY_PASS_FILE:-$STATE_DIR/supervision-identity.pass}" + +# Farben (nur wenn TTY) +if [[ -t 1 ]]; then + C_RESET="\033[0m"; C_RED="\033[1;31m"; C_GREEN="\033[1;32m" + C_YELLOW="\033[1;33m"; C_BLUE="\033[1;34m"; C_DIM="\033[2m" +else + C_RESET=""; C_RED=""; C_GREEN=""; C_YELLOW=""; C_BLUE=""; C_DIM="" +fi + +# ------------------------------------------------------------------------------ +# Helpers: Logging +# ------------------------------------------------------------------------------ + +LOG_FILE="" + +log() { printf "%b\n" "$1" | tee -a "${LOG_FILE:-/dev/null}"; } +ok() { log "${C_GREEN}✓${C_RESET} $1"; } +warn() { log "${C_YELLOW}⚠${C_RESET} $1"; } +err() { log "${C_RED}✗${C_RESET} $1" >&2; } +info() { log "${C_BLUE}→${C_RESET} $1"; } +dim() { log "${C_DIM} $1${C_RESET}"; } + +die() { err "$1"; exit "${2:-1}"; } + +confirm() { + local prompt="$1" + [[ "$DRY_RUN" == 1 ]] && { dim "[dry-run] auto-yes: $prompt"; return 0; } + read -r -p "$(printf "%b" "${C_YELLOW}?${C_RESET} $prompt [y/N] ")" reply + [[ "$reply" =~ ^[yY]$ ]] +} + +run() { + # Wraps a command: in dry-run, just echo. Otherwise execute. + if [[ "$DRY_RUN" == 1 ]]; then + dim "[dry-run] $*" + return 0 + fi + "$@" +} + +# ------------------------------------------------------------------------------ +# State-Management: simples JSON-ähnliches Format pro UDID +# +# Datei: $STATE_DIR/state-.env (key=value, source-bar) +# Keys: STEP_PREFLIGHT_AT, STEP_BACKUP_AT, BACKUP_PATH, +# STEP_SUPERVISE_AT, STEP_RESTORE_AT, STEP_PROFILE_AT +# ------------------------------------------------------------------------------ + +state_file_for() { printf "%s/state-%s.env" "$STATE_DIR" "$1"; } + +state_load() { + local f; f="$(state_file_for "$1")" + if [[ -f "$f" ]]; then + # shellcheck disable=SC1090 + source "$f" + fi +} + +state_set() { + # state_set UDID KEY VALUE + local f; f="$(state_file_for "$1")" + local key="$2"; local val="$3" + # In-Memory-Update + eval "$key=\"\$val\"" + # Persist (re-write each time, ist klein) + local tmpf="${f}.tmp" + { + for k in STEP_PREFLIGHT_AT STEP_BACKUP_AT BACKUP_PATH STEP_SUPERVISE_AT STEP_RESTORE_AT STEP_PROFILE_AT BACKUP_PASSWORD_FILE; do + local v="${!k:-}" + [[ -n "$v" ]] && printf "%s=%q\n" "$k" "$v" + done + } > "$tmpf" + mv "$tmpf" "$f" + chmod 600 "$f" +} + +# ------------------------------------------------------------------------------ +# Step 0: Argumente parsen +# ------------------------------------------------------------------------------ + +usage() { + cat </dev/null 2>&1 \ + || die "Fehlt: $bin → brew install libimobiledevice" +done +ok "libimobiledevice tools im PATH" + +[[ -x "$CFGUTIL" ]] || die "cfgutil nicht gefunden: $CFGUTIL → Apple Configurator 2 aus App Store installieren" +ok "cfgutil gefunden" + +# Supervision-Identity +if [[ ! -f "$SUPERVISION_IDENTITY_P12" ]]; then + die "Supervision-Identity fehlt: $SUPERVISION_IDENTITY_P12 + → siehe SUPERVISION-IDENTITY-SETUP.md (einmaliger Setup-Step)" +fi +ok "Supervision-Identity vorhanden" + +# Profil +[[ -f "$PROFILE_PATH" ]] || die "Profil nicht gefunden: $PROFILE_PATH" +if ! plutil -lint "$PROFILE_PATH" >/dev/null 2>&1; then + die "Profil ist kein gültiges Plist: $PROFILE_PATH" +fi +ok "Profil ist gültig" + +# Connected device(s) +UDIDS="$(idevice_id -l 2>/dev/null || true)" +if [[ -z "$UDIDS" ]]; then + die "Kein iPhone via USB erkannt. Stelle sicher: + - USB-C-Kabel ist eingesteckt + - iPhone ist entsperrt + - 'Diesem Computer vertrauen?' wurde auf dem iPhone bestätigt" +fi + +# Bei mehreren: erste, ggf. später interaktiv erweitern +UDID="$(echo "$UDIDS" | head -n1)" +log "" +info "Gerät: $UDID" + +DEVICE_NAME="$(ideviceinfo -u "$UDID" -k DeviceName 2>/dev/null || echo "?")" +IOS_VERSION="$(ideviceinfo -u "$UDID" -k ProductVersion 2>/dev/null || echo "?")" +ACTIVATION="$(ideviceinfo -u "$UDID" -k ActivationState 2>/dev/null || echo "?")" +log "Name: $DEVICE_NAME" +log "iOS: $IOS_VERSION" +log "Activation: $ACTIVATION" + +# Activation-Check +if [[ "$ACTIVATION" != "Activated" ]]; then + warn "ActivationState ist '$ACTIVATION' — Backup könnte scheitern" +fi + +# State laden falls --resume +state_load "$UDID" +if [[ "$RESUME" == 1 ]]; then + [[ -n "${STEP_BACKUP_AT:-}" ]] && ok "[resume] Backup bereits erledigt: $STEP_BACKUP_AT" + [[ -n "${STEP_SUPERVISE_AT:-}" ]] && ok "[resume] Supervise bereits erledigt: $STEP_SUPERVISE_AT" + [[ -n "${STEP_RESTORE_AT:-}" ]] && ok "[resume] Restore bereits erledigt: $STEP_RESTORE_AT" + [[ -n "${STEP_PROFILE_AT:-}" ]] && ok "[resume] Profil bereits installiert: $STEP_PROFILE_AT" +fi + +state_set "$UDID" STEP_PREFLIGHT_AT "$(date -Iseconds)" +log "" + +# ------------------------------------------------------------------------------ +# Step 2: BACKUP (encrypted) +# ------------------------------------------------------------------------------ + +if [[ "$RESUME" == 1 && -n "${STEP_BACKUP_AT:-}" ]]; then + info "[2/5] Backup übersprungen (resume)" +else + info "[2/5] Backup (idevicebackup2, encrypted)" + + BACKUP_ROOT="$STATE_DIR/backups/$UDID" + mkdir -p "$BACKUP_ROOT" + + # Encryption-Passwort: generieren wenn nicht vorhanden, persistieren + BACKUP_PASSWORD_FILE="${BACKUP_PASSWORD_FILE:-$STATE_DIR/backup-pass-$UDID.txt}" + if [[ ! -f "$BACKUP_PASSWORD_FILE" ]]; then + if [[ "$DRY_RUN" != 1 ]]; then + openssl rand -base64 24 > "$BACKUP_PASSWORD_FILE" + chmod 600 "$BACKUP_PASSWORD_FILE" + ok "Backup-Passwort generiert: $BACKUP_PASSWORD_FILE" + warn "WICHTIG: dieses Passwort wird zum Restore gebraucht. Sicher aufheben." + else + dim "[dry-run] würde openssl rand -base64 24 > $BACKUP_PASSWORD_FILE" + fi + fi + BACKUP_PASSWORD="$([[ -f "$BACKUP_PASSWORD_FILE" ]] && cat "$BACKUP_PASSWORD_FILE" || echo "DRYRUN")" + + warn "Backup kann je nach iPhone-Größe 15-60 Minuten dauern." + warn "Diese Session NICHT abbrechen, USB-Kabel NICHT abziehen." + confirm "Backup jetzt starten?" || die "Abgebrochen vom User" 0 + + # Encryption aktivieren falls noch nicht + # idevicebackup2 will die Backup-Encryption-Konfiguration auf dem Gerät selbst setzen + # VERIFY ON DEVICE: ob 'encryption on' idempotent ist oder vorher 'encryption off' nötig + run idevicebackup2 -u "$UDID" -i backup encryption on "$BACKUP_PASSWORD" "$BACKUP_ROOT" \ + || warn "Encryption-Setup hat returned non-zero — möglicherweise bereits aktiv, fahre fort" + + # Backup + run idevicebackup2 -u "$UDID" -i backup "$BACKUP_ROOT" + + state_set "$UDID" BACKUP_PATH "$BACKUP_ROOT" + state_set "$UDID" BACKUP_PASSWORD_FILE "$BACKUP_PASSWORD_FILE" + state_set "$UDID" STEP_BACKUP_AT "$(date -Iseconds)" + ok "Backup fertig: $BACKUP_ROOT" +fi +log "" + +# ------------------------------------------------------------------------------ +# Step 3: SUPERVISE — WIPED das Gerät, setzt Supervised-Flag +# ------------------------------------------------------------------------------ + +if [[ "$RESUME" == 1 && -n "${STEP_SUPERVISE_AT:-}" ]]; then + info "[3/5] Supervise übersprungen (resume)" +else + info "[3/5] Supervise (cfgutil prepare)" + + warn "DIESER SCHRITT WIPED DAS GERÄT." + warn "Backup MUSS in Step 2 erfolgreich gewesen sein." + warn "Find-My ist deaktiviert? Apple-ID-Passwort eingegeben? Falls nein: JETZT abbrechen." + confirm "Wirklich fortfahren mit Wipe+Supervise?" || die "Abgebrochen vom User" 0 + + # cfgutil-Aufruf — VERIFY ON DEVICE: exakte Syntax + ECID vs UDID + # Apple-Doku-Stand: cfgutil unterstützt --ecid; UDID-Filter via --ecid in 0x-Hex + # Für unsupervised+activated devices ist der einfachste Weg: alle connected devices + # (es sollte nur eins angeschlossen sein zu diesem Zeitpunkt) + run "$CFGUTIL" \ + --foreach \ + prepare \ + --supervised \ + --organization-name "ReBreak" \ + --supervisor-host-certs "$SUPERVISION_IDENTITY_P12" + + state_set "$UDID" STEP_SUPERVISE_AT "$(date -Iseconds)" + ok "Supervise-Aufruf abgesetzt. Gerät reboots gerade — warte auf Wieder-Verbindung." +fi +log "" + +# ------------------------------------------------------------------------------ +# Step 4: WAIT FOR RECONNECT + RESTORE +# ------------------------------------------------------------------------------ + +if [[ "$RESUME" == 1 && -n "${STEP_RESTORE_AT:-}" ]]; then + info "[4/5] Restore übersprungen (resume)" +else + info "[4/5] Restore (warte auf Re-Verbindung, dann idevicebackup2 restore)" + + if [[ "$DRY_RUN" != 1 ]]; then + log "Warte auf iPhone-Reconnect (max 5 min)..." + for i in $(seq 1 60); do + if idevice_id -l 2>/dev/null | grep -q "$UDID"; then + ok "iPhone ist wieder verbunden" + break + fi + sleep 5 + [[ $i -eq 60 ]] && die "Timeout: iPhone nicht innerhalb 5 min wieder verbunden" + done + + # iOS-Setup-Assistant muss der User auf dem iPhone bis zum Punkt "Vom Backup wiederherstellen?" + # bringen — oder wir restoren direkt via libimobiledevice (was wir hier tun) + # VERIFY ON DEVICE: ob restore direkt nach Wipe geht (vor Setup-Assistant) oder + # erst nach Setup-Assistant + initial-activation + warn "iPhone zeigt jetzt 'Hallo'/Setup-Assistant." + warn "Folge der Anleitung in README.md → 'Post-Supervise iPhone-Setup'." + warn "Sobald iPhone die 'Mit Computer verbinden'-Stage erreicht: hier weiter." + confirm "iPhone ist im Recovery-Setup-Stadium und bereit für Restore?" \ + || die "Abgebrochen vom User" 0 + fi + + BACKUP_PASSWORD="$(cat "$BACKUP_PASSWORD_FILE")" + + # VERIFY ON DEVICE: '-i restore' nimmt $BACKUP_ROOT als positional arg + # Encryption-Passwort wird via stdin oder Env erwartet — idevicebackup2 v1.3+ unterstützt --password + run idevicebackup2 -u "$UDID" -i restore \ + --system --reboot \ + --password "$BACKUP_PASSWORD" \ + "$BACKUP_PATH" + + state_set "$UDID" STEP_RESTORE_AT "$(date -Iseconds)" + ok "Restore abgesetzt. iPhone reboots." +fi +log "" + +# ------------------------------------------------------------------------------ +# Step 5: INSTALL PROFIL + VERIFY +# ------------------------------------------------------------------------------ + +info "[5/5] Profil installieren + Verify" + +if [[ "$DRY_RUN" != 1 ]]; then + log "Warte auf iPhone-Reconnect post-restore (max 10 min)..." + for i in $(seq 1 120); do + if idevice_id -l 2>/dev/null | grep -q "$UDID"; then + ok "iPhone ist wieder verbunden" + break + fi + sleep 5 + [[ $i -eq 120 ]] && die "Timeout: iPhone nicht innerhalb 10 min wieder verbunden" + done + + warn "User muss iOS jetzt entsperren + Setup-Assistant abschließen falls noch nicht." + confirm "iPhone ist entsperrt und im Home-Screen?" || die "Abgebrochen vom User" 0 +fi + +# Profil installieren via cfgutil +# VERIFY ON DEVICE: 'cfgutil install-profile ' Syntax +run "$CFGUTIL" --foreach install-profile "$PROFILE_PATH" + +state_set "$UDID" STEP_PROFILE_AT "$(date -Iseconds)" + +# Verify supervised +if [[ "$DRY_RUN" != 1 ]]; then + IS_SUPERVISED="$("$CFGUTIL" --foreach get isSupervised 2>/dev/null || echo "?")" + log "Supervised-State (cfgutil get isSupervised): $IS_SUPERVISED" +fi + +log "" +log "${C_GREEN}═══════════════════════════════════════════════════${C_RESET}" +log "${C_GREEN} Bootstrap fertig${C_RESET}" +log "${C_GREEN}═══════════════════════════════════════════════════${C_RESET}" +log "" +log "Manueller Verify auf dem iPhone:" +log " 1. Settings → Allgemein → Info → 'Dieses iPhone wird beaufsichtigt' sichtbar?" +log " 2. Long-Press auf Rebreak-Icon → kein 'App löschen' mehr?" +log " 3. Settings → VPN → Rebreak → Toggle disabled?" +log " 4. Settings → Allgemein → VPN, DNS und Gerätemanagement → Profil 'Nicht entfernbar'?" +log "" +log "Backend-Enrollment (separater Step):" +log " → in der Rebreak-App: Profil-Tab → 'Schutz aktivieren' → QR scannen" +log "" +log "Logs: $LOG_FILE" +log "State: $(state_file_for "$UDID")" +log "Backup: $BACKUP_PATH" +log "Backup-Pass: $BACKUP_PASSWORD_FILE ${C_YELLOW}(sicher aufheben!)${C_RESET}" diff --git a/ops/mdm/profiles/rebreak-iphone-protection.DEV-removable.mobileconfig b/ops/mdm/profiles/rebreak-iphone-protection.DEV-removable.mobileconfig new file mode 100644 index 0000000..d981eb5 --- /dev/null +++ b/ops/mdm/profiles/rebreak-iphone-protection.DEV-removable.mobileconfig @@ -0,0 +1,89 @@ + + + + + + PayloadType + Configuration + PayloadVersion + 1 + PayloadIdentifier + org.rebreak.protection.iphone.DEV.20260524 + PayloadUUID + D1B2C3D4-E5F6-4789-ABCD-EF1234567899 + PayloadDisplayName + ReBreak Schutz (DEV-Test) + PayloadDescription + TEST-Profil — via Settings entfernbar. NICHT für Produktion. + PayloadOrganization + ReBreak DEV + PayloadRemovalDisallowed + + + PayloadContent + + + + PayloadType + com.apple.applicationaccess + PayloadVersion + 1 + PayloadIdentifier + org.rebreak.protection.iphone.DEV.restrictions + PayloadUUID + D2234567-ABCD-4F01-9345-67890ABCDED1 + PayloadDisplayName + ReBreak Restrictions (DEV) + + + allowAppRemoval + + allowUIConfigurationProfileInstallation + + + + + + + + + + PayloadType + com.apple.dnsSettings.managed + PayloadVersion + 1 + PayloadIdentifier + org.rebreak.protection.iphone.DEV.dns + PayloadUUID + D3234567-ABCD-4F01-9345-67890ABCDED2 + PayloadDisplayName + ReBreak DNS-Filter (DEV) + + DNSSettings + + DNSProtocol + HTTPS + ServerURL + https://dns.rebreak.org/dns-query + + + + + + diff --git a/ops/mdm/profiles/rebreak-iphone-protection.mobileconfig b/ops/mdm/profiles/rebreak-iphone-protection.mobileconfig new file mode 100644 index 0000000..28b9433 --- /dev/null +++ b/ops/mdm/profiles/rebreak-iphone-protection.mobileconfig @@ -0,0 +1,232 @@ + + + + + + PayloadType + Configuration + PayloadVersion + 1 + PayloadIdentifier + org.rebreak.protection.iphone.20260525 + PayloadUUID + A1B2C3D8-E5F6-4789-ABCD-EF1234567894 + PayloadDisplayName + ReBreak Schutz + PayloadDescription + Schützt dich vor Glücksspiel-Rückfall: Rebreak nicht entfernbar, DNS-Filter auf dns.rebreak.org gelocked, VPN-Toggle deaktiviert. Profil kann nicht selbst entfernt werden — Notfall-Entfernung via deinem Trustee. + PayloadOrganization + ReBreak + PayloadRemovalDisallowed + + ConsentText + + default + Du installierst hiermit das ReBreak-Schutz-Profil. Dieses Profil bindet dich freiwillig an folgende Einschränkungen: + +• Apps können während des Schutzes nicht über Long-Press gelöscht werden +• Das ReBreak-VPN kann nicht in den iPhone-Einstellungen deaktiviert werden +• System-DNS-Anfragen laufen über dns.rebreak.org (verschlüsselt via DoH) +• Du kannst dieses Profil nicht selbst entfernen + +Das ist gewollt. Der Schutz wirkt, weil er gegen deine impulsive Selbst-Override-Tendenz steht. Die Entfernung läuft über deinen Trustee oder deinen 7-Tage-Cooldown in der App. + +Bei Verlust deines iPhones: das Profil verschwindet mit Factory-Reset, das ist normal. + + + PayloadContent + + + + + PayloadType + com.apple.applicationaccess + PayloadVersion + 1 + PayloadIdentifier + org.rebreak.protection.iphone.restrictions + PayloadUUID + B3234567-ABCD-4F01-9345-67890ABCDEF6 + PayloadDisplayName + ReBreak Restrictions + PayloadDescription + Verhindert App-Löschung und VPN-Eingriffe. + + + allowAppRemoval + + + + allowEraseContentAndSettings + + + + allowUIConfigurationProfileInstallation + + + + + + + + PayloadType + com.apple.dnsSettings.managed + PayloadVersion + 1 + PayloadIdentifier + org.rebreak.protection.iphone.dns + PayloadUUID + C1234567-ABCD-4F01-9345-67890ABCDEF2 + PayloadDisplayName + ReBreak DNS-Filter + PayloadDescription + Leitet alle DNS-Anfragen verschlüsselt über dns.rebreak.org. Glücksspiel-Domains werden blockiert. + + DNSSettings + + DNSProtocol + HTTPS + ServerURL + https://dns.rebreak.org/dns-query + + + + + + PayloadType + com.apple.vpn.managed + PayloadVersion + 1 + PayloadIdentifier + org.rebreak.protection.iphone.vpn + PayloadUUID + D2234567-ABCD-4F01-9345-67890ABCDEF4 + PayloadDisplayName + ReBreak Schutz-VPN + PayloadDescription + Aktiviert den ReBreak-DNS-Filter als nicht-abschaltbaren VPN. Verhindert dass du dich impulsiv selbst aussperrst-vom-Schutz. + + UserDefinedName + ReBreak Schutz + VPNType + VPN + + VPNSubType + org.rebreak.app.PacketTunnelExtension + + VPN + + + RemoteAddress + ReBreak DNS-Filter (lokal) + AuthenticationMethod + Password + ProviderBundleIdentifier + org.rebreak.app.PacketTunnelExtension + ProviderType + packet-tunnel + DisconnectOnIdle + 0 + + OnDemandUserOverrideDisabled + + + + + VendorConfig + + + + OnDemandEnabled + 1 + OnDemandRules + + + Action + Connect + + + + + + +