From 5b09f3a742244f27879f35bb4a9af3371f1c1e63 Mon Sep 17 00:00:00 2001 From: Conrad Kramer Date: Thu, 19 Mar 2026 04:08:10 -0700 Subject: [PATCH] Stabilize forgejo namespace auth and secrets --- Scripts/_burrow-secrets.sh | 21 +++++--- Scripts/provision-forgejo-nsc.sh | 50 ++++++++++++------ Scripts/sync-forgejo-nsc-config.sh | 24 ++------- secrets/forgejo/nsc-autoscaler-config.age | Bin 1395 -> 1395 bytes secrets/forgejo/nsc-dispatcher-config.age | Bin 2015 -> 2015 bytes secrets/forgejo/nsc-token.age | Bin 1238 -> 1861 bytes services/forgejo-nsc/README.md | 9 ++-- .../forgejo-nsc/internal/nsc/dispatcher.go | 4 ++ 8 files changed, 59 insertions(+), 49 deletions(-) diff --git a/Scripts/_burrow-secrets.sh b/Scripts/_burrow-secrets.sh index e08bf2a..6f1bc28 100644 --- a/Scripts/_burrow-secrets.sh +++ b/Scripts/_burrow-secrets.sh @@ -107,18 +107,25 @@ burrow_encrypt_secret_from_file() { local secret_path="$2" local source_path="$3" local agenix_path - local identity_path + local backup_file="" if [[ ! -s "${source_path}" ]]; then echo "secret source missing or empty: ${source_path}" >&2 return 1 fi agenix_path="$(burrow_secret_repo_path "${repo_root}" "${secret_path}")" - identity_path="$(burrow_agenix_identity_path "${repo_root}")" - - if [[ -n "${identity_path}" ]]; then - nix --extra-experimental-features "nix-command flakes" run "${repo_root}#agenix" -- -e "${agenix_path}" -i "${identity_path}" < "${source_path}" - else - nix --extra-experimental-features "nix-command flakes" run "${repo_root}#agenix" -- -e "${agenix_path}" < "${source_path}" + if [[ -f "${secret_path}" ]]; then + backup_file="$(mktemp "${TMPDIR:-/tmp}/burrow-secret-backup.XXXXXX")" + cp "${secret_path}" "${backup_file}" fi + rm -f "${secret_path}" + + if ! nix --extra-experimental-features "nix-command flakes" run "${repo_root}#agenix" -- -e "${agenix_path}" < "${source_path}"; then + if [[ -n "${backup_file}" && -f "${backup_file}" ]]; then + mv "${backup_file}" "${secret_path}" + fi + return 1 + fi + + [[ -n "${backup_file}" ]] && rm -f "${backup_file}" } diff --git a/Scripts/provision-forgejo-nsc.sh b/Scripts/provision-forgejo-nsc.sh index b8c9f12..537107e 100755 --- a/Scripts/provision-forgejo-nsc.sh +++ b/Scripts/provision-forgejo-nsc.sh @@ -146,25 +146,36 @@ dispatcher_secret="${REPO_ROOT}/secrets/forgejo/nsc-dispatcher-config.age" autoscaler_secret="${REPO_ROOT}/secrets/forgejo/nsc-autoscaler-config.age" if [[ "${REFRESH_TOKEN}" -eq 1 ]]; then - "${NSC_BIN}" auth check-login --duration 20m >/dev/null - raw_token_file="$(mktemp)" - trap 'rm -f "${raw_token_file}"; cleanup' EXIT - "${NSC_BIN}" auth generate-dev-token --output_to "${raw_token_file}" >/dev/null - RAW_NSC_TOKEN_FILE="${raw_token_file}" TOKEN_FILE="${token_file}" python3 - <<'PY' + ssh \ + -i "${SSH_KEY}" \ + -o IdentitiesOnly=yes \ + -o UserKnownHostsFile="${KNOWN_HOSTS_FILE}" \ + -o StrictHostKeyChecking=accept-new \ + "${HOST}" \ + 'sudo -u forgejo-nsc python3 - <<'"'"'PY'"'"' import json -import os from pathlib import Path -raw = Path(os.environ["RAW_NSC_TOKEN_FILE"]).read_text(encoding="utf-8").strip() -if not raw: - raise SystemExit("generated Namespace token is empty") +payload = {} -Path(os.environ["TOKEN_FILE"]).write_text( - json.dumps({"bearer_token": raw}, indent=2) + "\n", - encoding="utf-8", -) -PY - rm -f "${raw_token_file}" +token_json = Path("/var/lib/forgejo-nsc/.config/ns/token.json") +if token_json.exists(): + data = json.loads(token_json.read_text(encoding="utf-8")) + session = str(data.get("session_token", "")).strip() + if session: + payload["session_token"] = session + +token_cache = Path("/var/lib/forgejo-nsc/.config/ns/token.cache") +if token_cache.exists(): + bearer = token_cache.read_text(encoding="utf-8").strip() + if bearer: + payload["bearer_token"] = bearer + +if not payload: + raise SystemExit("forgejo-nsc host does not have a usable Namespace session") + +print(json.dumps(payload, indent=2)) +PY' > "${token_file}" chmod 600 "${token_file}" elif [[ -f "${token_secret}" ]]; then burrow_decrypt_age_secret_to_temp "${REPO_ROOT}" "${token_secret}" > "${token_file}" @@ -186,8 +197,13 @@ try: except json.JSONDecodeError: parsed = None -if isinstance(parsed, dict) and isinstance(parsed.get("bearer_token"), str) and parsed["bearer_token"].strip(): - raise SystemExit(0) +if isinstance(parsed, dict): + bearer = parsed.get("bearer_token") + session = parsed.get("session_token") + if isinstance(bearer, str) and bearer.strip(): + raise SystemExit(0) + if isinstance(session, str) and session.strip(): + raise SystemExit(0) path.write_text(json.dumps({"bearer_token": raw}, indent=2) + "\n", encoding="utf-8") PY diff --git a/Scripts/sync-forgejo-nsc-config.sh b/Scripts/sync-forgejo-nsc-config.sh index 431f832..d6ac48c 100755 --- a/Scripts/sync-forgejo-nsc-config.sh +++ b/Scripts/sync-forgejo-nsc-config.sh @@ -88,27 +88,9 @@ if [[ "${ROTATE_PAT}" -eq 1 ]]; then "${SCRIPT_DIR}/provision-forgejo-nsc.sh" --host "${HOST}" --ssh-key "${SSH_KEY}" fi -token_file="$( - burrow_resolve_secret_file \ - "${REPO_ROOT}" \ - "" \ - "" \ - "${REPO_ROOT}/secrets/forgejo/nsc-token.age" -)" -dispatcher_file="$( - burrow_resolve_secret_file \ - "${REPO_ROOT}" \ - "" \ - "" \ - "${REPO_ROOT}/secrets/forgejo/nsc-dispatcher-config.age" -)" -autoscaler_file="$( - burrow_resolve_secret_file \ - "${REPO_ROOT}" \ - "" \ - "" \ - "${REPO_ROOT}/secrets/forgejo/nsc-autoscaler-config.age" -)" +token_file="${REPO_ROOT}/secrets/forgejo/nsc-token.age" +dispatcher_file="${REPO_ROOT}/secrets/forgejo/nsc-dispatcher-config.age" +autoscaler_file="${REPO_ROOT}/secrets/forgejo/nsc-autoscaler-config.age" for path in "${token_file}" "${dispatcher_file}" "${autoscaler_file}"; do if [[ ! -s "${path}" ]]; then diff --git a/secrets/forgejo/nsc-autoscaler-config.age b/secrets/forgejo/nsc-autoscaler-config.age index 5d9aa28de8171c62917d52908acd821b19402659..460d194950b9d0da16732cbea12f8c05529cdeeb 100644 GIT binary patch delta 1348 zcmey&^_gpePJNPLhOnMH(q zGM9f~W@5N`YOr%vmA0pLNkoy4YrbzpMx;e?X?R##Sg}h*x<`Jk0xq)dG=8-``mPLW}l}Q0%$rfq8T>8qj9BaFe>}*|g@xG4I z>~6-T*2X7hH*(E$4|LE!QzhCpX@ykkm5shr#3>uS`yD zn!Cj`>sI}oxjnqr%{D8RrDbN>>ym_yUQl6Bd5Je zSr}b5Cq6m%`;?xqvC-rWX(4W>gMWE$DUm%YZD1ODB)jDQC28fiO_NMLo0Jwg^6pPf zJY=kM)cflPZM__u^lNL}j})X`TIs>RZkyA=wOH{vW!Yazgv*^Q)t9{I?cR!fD>)au~ zNwGIJ{5g{RJCOh0z0QZnk2`riou%*FuT{^uc+akFPd}{sFa0NtZC>ifDbY+OM>3=8 zK5Dsp2&eA8`j2~C($f8hoTRo1y}NQ)a>+Y6o|2aeA`1gP>r7&3;Vk{KXSzrCht1QT ziMsyh5)ab;DjU@iYjyY61;aBdy!&PxDrepOH{Z4LA@{FehMv=W#h;&M-yYVj?tkM+ z-v{RUlzNxH+jn-Jm7BBh;fWvm22$N0nWZ$e?lI;6TKafu_Q!BG*&C^D zvs(M(>MJe$Cvv3g#ozQ_%uDs;FwRzq?EY4IG~r@-f6ULB zo0*F_IAwpo-hT6o$H9-OZ&sdIfB(gf*?(@l$#yz%Phy7bLzPQ4w@<8FI;*Al{1t~^ I{f}1w04_Lrd;kCd delta 1348 zcmey&^_gpePJKXRc0fd6MW9ExNmWi{d2YIEuz`W8abkX-r6%Tn8D1FyCGHijiA7;yfq~^*y1KdwerDND+1?h~ z#^&BerKzPEfmPW~u08?ohF)Q%6_$qPp_T#lrJimEX#rXJT*e3Q_HXf6lYVx=x^>g1 zr16K9FH^X&(6C_t-nlp9-6S^J+pqo2vDME)oIhdn=C7{Qeb-i@cw`V&3$Xv+2m45cSEOnv?BcruKTO z_*5~>kYuV)Furpzsi@XNOj26l=Yn~Q_B?zr#UxaVz3SM*%Lj8lYAj7JmRoMP+~C{8 zb*qAcdw2sK!sHn1S0=olzPqipY*rbE%t7P7;!V0<_fKA0dNIRQVdvk_e)c&tOw7cq zKWqtc_1EliVGPy$;-~JR{iONEoBL1HzA^B)7G_QOUDS7syZ)ZCthM7Kqj=AXi<=Wz zTdr-3lG*$s;AqV4SsNc-H~iBmD*9H&mM82+U-8$u-}RO>%wImQCScilrt=r~c?gJy z{a$df)kgo)hh=+`=Si;K<-H=&^8U|`)2FS3pIn}?Jgi}EcHQqeF@KJ_zuD~|? z6SFV2Fn9cue-*%SMI_1Wx{75%o71lJFYM(KmR}^yWD+K+%xx2?eqa4GdZYHGx#4rQ zwS|HoEV-iRnKH$cJ7V>xCoNCvjONMRWr|_*pZM_D;XBDm-uHGN+H^UNKkCh^`adVH zTAz&b6pHXG@p-sR#6&#XiY;eXTJEu3vNKocm#~{}(tdH|<(agnmU@Bn)F)rdUN`YH z$6o83Y+Ww=5slOt>!5IQ+4*B^Ckp@O z3vKN9f2&6A_>Xw$$qJp0KPM=yHSYCw^-8Xfe0bF`M3Qy?mbC%GwQv5nF18Gu9C@4X z;f{0@%bDVG@78NI%Uj*KD6seAZO!bs+p%XfZKiB&7J4n*m_Ps4iJfUvYHz+(xmIs6 z!;qaJq0?>43WGq~=PUO8QB;|BrCunYWuA~>W8kdQ5!07GolvzqrPW_=mqp3<&jnYR zr@fNUm{9MuOP$lO?y(z3lj@G}`>T=@vcGq!D=0LsZpr4%Guv#~79Q`v;z;AphgIHR z7SDH@^Z7g9s^-a^DL1zqsXop)d*+%+CzCYH1s@mNIbSc@#vOdz{6`h5@%QJ;QbX2$ z|01<0K|w)r_DT+&Qvv+4FZr+iGVD-(e{{8Hg0kPE))yWdi*INzyS{pFY-7;#e37}D zZ3frnY$NkuVx+l(xwotH|b-uiSu-g}@W zXUWbDLXLtqd!F37pZoBFSo4)TkM?scXzh1oV7Os?C38VcWzB@|&55!9k0dkZ&N;f1 HEl~ylJb7fW diff --git a/secrets/forgejo/nsc-dispatcher-config.age b/secrets/forgejo/nsc-dispatcher-config.age index f3306aba0a1f8ca5956d67453d835359c57da274..7c752b14a6a62fe8b6d91a46ff8fc14a8a5c50a1 100644 GIT binary patch delta 1973 zcmcc5f1iJXPJL!bNnV&isAq0Pq?uuep<7CdVV0AJS-!rjPqMy|qe*0Peo$maKxA%s zC|6KrkU?;6rB6YSVPTMVk+z??Q&o_DN|<+EMOjL5a)^ajpn<7VUTUzrCzr0BLUD11 zZfc5=si~o*f@e`wu4B4_ufI`px?iS8fSY%2SZ=;)u3KnPW_pH!yQ51^P*!@9W4W1r zVVa|pzkg^TSF(qcX;i6Nm}y1Kdwk;Wwkh34hq z`B~<<6;)XUKB*O^0i{kwW~OPyiH;VYsh(!_LE%OLu3m-)Ts&U9H)5nzlZ=;K3pF`D zUCq(t;Hs6Yrs;g#{=%?W>YKLzTyKX^M~i8j&+q1nTh7fZ`PMk=&X=AnHo@QFLYa#b z13Pk^)-K7@XXT%<+V{Bomvzog`TXi1k1ssZYWI5a<_c%O!d8z@d;K<6cCQYuoM(M2 z|Nr`g$ogHn8E;m5R(`t?Xq4W$$<(~y!an4@$$lh;X*NQ(o&sf_fw7m1~3#?mS-*Ia9`9EPvTu!2smXt~_ z`ntt^eowS>#M5-uyf4`i$2b~i-t=mhc$^h+T4>6FZ*R4tRv79yxXjOG+rQAJVN1To zmzma$jJIw6uU4A+aQhG4Ux{7}-nUn}8UI>yQ9tL-_1E9tpZKK48YdhwwXu0|RBG~i z>C6AzBd$v9dU^4QrRe4Q>J@*pJC>wYC{8XBuAf(R^5ed9^E(X1Hu=doJoDz#iMN_` zc5-_9FO3-mueBTf?6wuXH!_;5V23!LzU=P{b#Dvpo<)E4zjiS*Z)eRRnd|KO ze`i0cKAo#`an|9sALa}nA8lT=%K7cF5BaNI&de(~zB$%#;lY1P%-4Dv*>={c-FT|U zRdkcFvfggbeRr4Ah_={AlCNzN4^Q;9 z7jv?kYCYK1Kf^oWXnLc_znQs**Ut>iT{%Bz;eCN${Gz9Bbnf31E9V>Sv|>k8I%h@4 zezlW|7Kh_sC%qTYU6E{4A{A6HH{nJ;x4W^p+z<8%$~#!<_ios0ytdEgsjXj;&4CX= zxmCBm2%YU&#;!B9w`h=EX1uH8<$YC>5&C-VNYG{gMQT>iO_9qHrGWJ3ne|}rOV@63skZ4-v@$!`> zQ|vA~O%45eUs+-QSIhdP75nzRU&`F7yzW|06GNH&GO>cjgQ^a5x}#<st^D7iG6&#Su5hzoBDt$g8k_) zYYr)&JG?gNG@s^Owm&8%>vv6JbP0XwztlbLH)l`u|5Fz`A6{N^IsEoHt^@vcryAb~ zG|SF-E)-_{I75oZ#=*DWK`;7TVs_JTi$Cc6`H5)-Ms8;NsRlM zoo`phsQq@ovwYINimXS%Dr}Z93p_0!)&GBFbN$96?K8XE_U@|QEZjUZyIp)=<>7+* zM+Z9lR3nTpem!$ZxcB4~y}9(sHTK(M!L0ROR%bL`lx+q!u`*pbVfn|w*eNMmL+WdaW)3ObbF(UG|OeK?@ zC*&DiGv3)EcVxj;zTRarRy@xc+}?0&v?ttQEL`zyuC(5^d2b*1+by3RRJw+B*PVk8 zmTrM~_~|7C+oJ&8?UMU@}>_g|m2X`*C`an!plF;_h-lo+QJeksy0 zG+Nc^bM3xP_xgmnKX^{Z+Hb90S7N$br+&#PqfCB%_9#|%m^_H>`%%;}!LzMPfpj{Z_UpS#7Nes}JhjA=*K0{~Pzo;m;k delta 1973 zcmcc5f1iJXPQ8U`hM#k2sCjmJh)afhxSyGkVO3;~Wu>cmSb1cWZ+T*Igu7FeZ-&2P zGM7$-RiFcB(X{u9kRk&ePX{K4BZ1y1KdwL5>-rnMtNW z;bp$AiQ2BENu`l~F2=r@IR%F0X`ZIpW#u9D7HNJt&LI`~T>XYSIpgJS%iCDHs=Ri5 zIsf~_?|1erX;m|MGUd}{%~$HFmWfPfHYG?!*-X<_di%*nQ0l_Qsj@8P_I$RlYdb6- z+)+w6$#RSR?Xr`n)0IBB8NEBYCT2Zn#lzf%tK+pFwR7~mXL7sQ?-a|szvo(haGRFx zTLbBW>Us}{yYE%>-->2iZt0S}`%F~&80+DmTmC$1=1IpX673P|_Z|vbxP0*!-xB{@ zwYk=6{=S8gazaPqsa?6d$LqASsnW}?e!k*e|8;u)6VseMM(;l;Ub)Pv z)%yRx?#(9VMKffd7(VEq~-#&56cv0Oace^2F}Bw3f~N*~i>yR08Fn=4)xo-}<^U+(3P!V&>d%JtKB=CRHy zxG*c`Fw4JrOK;40wLbS*qhd^3!lhSpCdfRDuPRyf`pTA!9pNV}9L0OTILf1M{DFxsVw@;<&f_lDZc2ZrN6J140~eIyx4+m#Y?xeotb#Kh>u@v^`pJ7 z_g~^{tTp_uaoi!}=Z%a{?_aNbX*1zcLw(@&BV2dY{x~?Qd|;T^FnzgxqGama5O%)% zT)GKW|7Kh0I6f6(x_bOp*ioNZiz+hZS4``7*51y4$!e!`;3<2vwa1zMe-S&ATD?QZ z^!SPoc0FQO3LeY9*ltyS{OgV@#r=$oGhW2V>5I%t^JpqN>Zx&Y$Bo@{Pi>cKvddq3 z?BmBv^gJI%RSX=$D0>r1^pUs8MKPP%{SK#%&PqeqXh z{Vof8Sn8X)WQWV1%_Vk1^~-H)^>Wk}ugZPWqpSP(li-&6BcJw_2wpH(Shsn75bL3K zi{9_b#-VQu!#kqZK6IMR(965~hMgycx+I(w>SbdCn8~wtcPvQ4O>E$Qj@KaBszp9r9BjR-RmKzegm- zAab!?N2jyI)<~t5H5D&5%kfYSL=)a zmA(x6ouYSR>rx+$33ZuYw$x4LGc{GR5T7WGM@SZ7&W@eERC;9W^%YjIyfA z-5(ow7{(`^__|Go@llP;lKoamAN{P}f3ovD$8`Q!e*99W2eP>B$cfvD<>Syx-5Up<>bX{wLlm<-#|uPCWQ1_Q;Fh z^8!+Pc;-Gh^X1LnzN;q11@V%Jn$}+-I4#Lc$&nWU#*)u{#(sgJ+1U(*T=Um4zgWFeZbd4yrtQQzwD z^1V!5x{;S>U%v9nN8^pI&T@{5u;?;pgRmcbdi?9=#HGx8{qIih)8~A=`;r`WX50O^ zlW=(!E6>rJTA${eY%988FE{t%{hxCJs;;b#V~bf3oqTv+ZU3$Z6X!5}DsJVSYX5c0 p)m8sjoxRQ~&%iYG#;ZDKqtu-?b%HuG4yC`A|0^_O&(UVt zFPBrgk8!bqV_tSap=ol7ccr&)h_8itslJP)XH|uEPNHdOVTomAS$cqnCzr0BLUD11 zZfc5=si~o*f@e`wu4B4_Uzt&ge}t2Xzjl6plu>eIfLpeyQC3QJNO_e-cz&o)u#>5Y zQL3Y}Z>~`wS81V%d8AoRW<{xcguj=zzN>zAX;yM!Zh>V=ly6#id17v=zPWd5n0JK9 z#E;_j`uTa;StWsfIVFXqxoQ61`ax#7=6;4AhN0PMz8<*+&gSkW2Ii^35g7(t;YNm~ zS%yaLNhwBYNrnbR{`#fnc@`Oo;ogM?De2~kre;AomTqo2;clT^y1KdwjwSAah5FuR zMS;$RE@@F=uFgTu;fbd1W(HMWdB$Fu{>JY0+J3o3K5mJgT&fnQ=gGbGS38&xy|gA~ z^Pl)fRkl?V;u3b2Jx}!dHE9~>e7{$HmZm+g*4X}iaM^LPf{1^~Nv)=$WxKZ8*f<;w z7F#nx>UR0f`MyOpr`u|m8P9Nb;9hMZX{I&l>Z;F6m$WgT33U*^^?Mfc$0vN*clS^5 zjJRhvC%8V4Bk$JjAZ|gw9TP4uz8No~-*5C^hDf1Q6pPTGn=T!E$|IUsEH})^< zk0ma8<9BCokL$w4Qdd4VI^RCX=>2m}Xk}1&+Rn7}sWWc8?GB%<_WM}xlj%A8raVf@ zwb3zLyDRVq)7j(xt*dk@SD0QEUefyf)Y3))@#$J=PX*Yy{Oh-C99Vzn5!33$TI|xg z>B~1&1blR!w)VsHixWcA|DJKL`nS5KaQUP4tFM*h-kA0x@7v^m&-e3GNpwB4bJ|nC zB=wKK?u9)UG?N`>FHm~J^KEKPz1m3FDsm|y#U z@$h`+2Knu;=FPpry<_(~(P{PE-&l_2*WJ8!!1DXMJ(K=sZN}uub%5c zR;sqvIUiy5pT8E}i0Rie6q~X(sR@_(Q*m$`=Hj+^}H?H zgQXEM;g5q})-Sy$YRfp6`(<^Wf}80(5o6=y0YR=8j-|d8b9tMWP$qu4h{??79Fw(PZvKmKPrbyqu%9 zd(E*t>5*;dWZhAJgE#NR|GHQ2cI(*xyQE;x(`@JAw&}t*^M$!8r&bi5o-BTFxa_F+zuC3m{I>Op*&A40JsMVD zx%@UYuO^4{v-@Q6_XS`4^@DhVMRJ~7ROMW|d+@66**y%Ndn5PAgvP9oG=8~4Vb2eN z%E>>RZYh7Baxvia)}0I=xIUlY-Wg-Y$GtLmy}?(D@9w-izj*NTb?ie%WPJ z?u#5&VHZ>D-X`;&6pVV^{9=A&;5ElP*Wb;G`myv}#dKx|r6lde<~RG5pGk2~TAjV{ zz`IMLopLD+-@Uw6&ssOfan;8Sb#FW`%spOSe;{-`18#7&y4#8RcG{_TCB3_<~EbF^@-j4p7DtZS^ikJPq-q7Gp1q}_3vC*H|)2b-=*a*E-O7(FZG$R@s-C%nS18Hnf!(iSONh>9O zX*d5aY1>(ztex+=OnjZHZZm(A;FQ|7Yg3TIeeS6rUM`;?DfhzW1th9AKtogW`q mFIu+jIp>n4OXoccy0ORW{!PE5fj=cg`tO{LyXr0bd?^6-7Fo;y delta 1190 zcmX@gca3v`PQ8y$UU81QX^E+4VsUA4N^oSJp|6W!c}QuFr*n3dUy*)*XPT#Fx@VAc zGMBGspi8ieONvE7l51tQM@Cd;u5o2zP-;MEV!ENTqjQ0CRe7*au3KT0BbTn7LUD11 zZfc5=si~o*f@e`wu4B4_d!)W=pqH_;NkmDZmy20)p__JPNMX2hU_rREOGbKLqG@ER zX?ki_a8RWMm#ee6MOIabcbHeDb9t_FSgO94X+dFHhN-`4dSF&bQfaZ5rC(@OmW!vw z#E;_jPPsuTg#{@=&K9oz*{S+wRpmaJ0ilMDPT86IE>+G+mj0QJo_T@VZWZ}le#Ob5 z5vIvmA=YbxXxO2M}A)L!3voRl2c0`H?rvmJ#5y!$DaRbl|kH;bI-aW&ptMc`YgEn z@3goN`SlaGZOGg?W$LZRhT7LJwOeY>{Q5QLl^@5h51I>uZmef+&DU7+tGX)b317rK z(HWutMRvLL9{r%IUijwGOe?{LC4EP=R_#dAHmWq}KQzZpQt*)3+Gs7F9>Y-kB8HXg z_n7lMo%&__oSvpFzJ0HhQ;*)~%x!$vc1&LFvReAHkMlNp)!!7(*ZT7^AS>}r)2+V~ z6C`eZ<}G=)opFPJmE2_&+q4N+SKVA3Um~E<5zv>C+4hI^M`+p_rBH*jSqvXxUS*^ z70!nKiBnd%p79I$bck`%q2HGh8QATP*zBA*b>G~bHk;bFH**|yVEU`S=T*ig&Eqqt zd!LauefA>iX|P2U%LV1)aMg3tmgj3%y#Et(ts(8Zb1vtS3cXY{?!_{;~6OzQ}?|()>CP!{v`I2?p>D$3)BRn|5xviwh)az zF@4GMS3!0cCX`JxT~HbI(sRq)zj?}{{Ef@G70;D3UYO=$@bhb(tHJp*(fN1M&wf0> z5&84k>a*HMZEAd<1fF`^^4>T3)ca)aXZ3PhD-R@PDA$XgX)@3g4jJ^hZ}AWYN;eJzp&s%*m5fX5;g-_