From c3746d05973bab2125db5b573ff8501a3916469c Mon Sep 17 00:00:00 2001 From: evilfps <208441420+evilfps@users.noreply.github.com> Date: Sat, 16 May 2026 14:29:00 +0530 Subject: [PATCH] Add enterprise data residency guard --- README.md | 2 + enterprise-data-residency-guard/README.md | 33 ++ .../data/sample-residency-input.json | 168 +++++++++ enterprise-data-residency-guard/docs/demo.gif | Bin 0 -> 59687 bytes enterprise-data-residency-guard/docs/demo.svg | 35 ++ .../docs/requirement-map.md | 17 + enterprise-data-residency-guard/package.json | 12 + .../scripts/demo.js | 12 + .../src/data-residency-guard.js | 318 ++++++++++++++++++ .../test/data-residency-guard.test.js | 59 ++++ 10 files changed, 656 insertions(+) create mode 100644 enterprise-data-residency-guard/README.md create mode 100644 enterprise-data-residency-guard/data/sample-residency-input.json create mode 100644 enterprise-data-residency-guard/docs/demo.gif create mode 100644 enterprise-data-residency-guard/docs/demo.svg create mode 100644 enterprise-data-residency-guard/docs/requirement-map.md create mode 100644 enterprise-data-residency-guard/package.json create mode 100644 enterprise-data-residency-guard/scripts/demo.js create mode 100644 enterprise-data-residency-guard/src/data-residency-guard.js create mode 100644 enterprise-data-residency-guard/test/data-residency-guard.test.js diff --git a/README.md b/README.md index d338cf68..27d674bf 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,4 @@ # deepevents.ai deepevents.ai main codebase + +- `enterprise-data-residency-guard/` adds enterprise data residency and cross-border transfer controls for institutional research operations. diff --git a/enterprise-data-residency-guard/README.md b/enterprise-data-residency-guard/README.md new file mode 100644 index 00000000..fbe56513 --- /dev/null +++ b/enterprise-data-residency-guard/README.md @@ -0,0 +1,33 @@ +# Enterprise Data Residency Guard + +Institutional customers need proof that research artifacts, identities, exports, and webhook deliveries stay inside approved data regions unless a transfer impact review says otherwise. This module turns tenant policy, research records, and destination metadata into deterministic decisions for admins. + +## What It Covers + +- Region-aware transfer decisions for repository exports, LMS sync, journals, funder portals, and lab notebooks. +- Research data classifications including public metadata, unpublished manuscripts, controlled human-subject data, PHI, grant reports, and embargoed preprints. +- Admin dashboard metrics for approved, review, and blocked transfers. +- Webhook-safe event envelopes with deterministic digests. +- Export manifest entries that preserve residency evidence without credentials. + +## Run It + +```bash +npm run check +npm test +npm run demo +``` + +## Reviewer Notes + +- Synthetic data only. No credentials, protected health data, or real institution records. +- Zero dependencies. The logic uses Node built-ins so reviewers can run it offline. +- The sample shows one blocked PHI transfer, two manual reviews, and approved in-region exports. + +## Files + +- `src/data-residency-guard.js` - residency policy evaluator and helpers. +- `data/sample-residency-input.json` - synthetic tenants, destinations, and records. +- `test/data-residency-guard.test.js` - coverage for decisions, dashboard metrics, digest stability, and manifest output. +- `docs/requirement-map.md` - issue #19 acceptance mapping. +- `docs/demo.svg` and `docs/demo.gif` - short visual proof artifacts for the demo run. diff --git a/enterprise-data-residency-guard/data/sample-residency-input.json b/enterprise-data-residency-guard/data/sample-residency-input.json new file mode 100644 index 00000000..445a3bbb --- /dev/null +++ b/enterprise-data-residency-guard/data/sample-residency-input.json @@ -0,0 +1,168 @@ +{ + "generatedAt": "2026-05-16T08:50:00.000Z", + "tenants": [ + { + "id": "tenant-eu-biology", + "name": "Helmholtz Biology Institute", + "homeRegion": "EU", + "allowedRegions": ["EU", "EEA"], + "regimes": ["GDPR", "HorizonEU"], + "policy": { + "requiresDpaForCrossBorder": true, + "requiresSccForNonAdequateRegion": true, + "requiresHumanReviewForSensitiveData": true, + "blockedClassifications": ["phi"], + "embargoExportsRequireRelease": true + } + }, + { + "id": "tenant-us-clinical", + "name": "Midwest Translational Lab", + "homeRegion": "US", + "allowedRegions": ["US"], + "regimes": ["HIPAA", "NIH"], + "policy": { + "requiresDpaForCrossBorder": true, + "requiresSccForNonAdequateRegion": false, + "requiresHumanReviewForSensitiveData": true, + "blockedClassifications": ["phi"], + "embargoExportsRequireRelease": true + } + }, + { + "id": "tenant-uk-social", + "name": "Northshire Social Science Centre", + "homeRegion": "UK", + "allowedRegions": ["UK", "EU", "US"], + "regimes": ["UK-GDPR", "UKRI"], + "policy": { + "requiresDpaForCrossBorder": false, + "requiresSccForNonAdequateRegion": true, + "requiresHumanReviewForSensitiveData": false, + "blockedClassifications": [], + "embargoExportsRequireRelease": true + } + } + ], + "destinations": [ + { + "id": "zenodo-eu", + "name": "Zenodo", + "type": "repository", + "region": "EU", + "adequacy": true, + "hasDpa": true, + "supportsRestrictedAccess": true + }, + { + "id": "pubmed-us", + "name": "PubMed Central", + "type": "repository", + "region": "US", + "adequacy": true, + "hasDpa": true, + "supportsRestrictedAccess": true + }, + { + "id": "canvas-us", + "name": "Canvas LMS", + "type": "lms", + "region": "US", + "adequacy": true, + "hasDpa": false, + "supportsRestrictedAccess": false + }, + { + "id": "journal-apac", + "name": "Pacific Journal Portal", + "type": "journal", + "region": "APAC", + "adequacy": false, + "hasDpa": false, + "supportsRestrictedAccess": true + }, + { + "id": "dspace-uk", + "name": "Institutional DSpace", + "type": "repository", + "region": "UK", + "adequacy": true, + "hasDpa": true, + "supportsRestrictedAccess": true + } + ], + "records": [ + { + "id": "rec-astro-open-data", + "tenantId": "tenant-uk-social", + "title": "Open survey response codebook", + "classification": "public-metadata", + "sourceRegion": "UK", + "destinationId": "dspace-uk", + "workflow": "repository-sync", + "embargoUntil": null, + "containsHumanSubjects": false, + "deidentified": true + }, + { + "id": "rec-eu-manuscript", + "tenantId": "tenant-eu-biology", + "title": "Unpublished microscopy manuscript", + "classification": "unpublished-manuscript", + "sourceRegion": "EU", + "destinationId": "zenodo-eu", + "workflow": "preprint-deposit", + "embargoUntil": null, + "containsHumanSubjects": false, + "deidentified": true + }, + { + "id": "rec-eu-clinical-supplement", + "tenantId": "tenant-eu-biology", + "title": "Clinical supplement with rare disease cohort", + "classification": "phi", + "sourceRegion": "EU", + "destinationId": "journal-apac", + "workflow": "journal-submission", + "embargoUntil": null, + "containsHumanSubjects": true, + "deidentified": false + }, + { + "id": "rec-us-patient-dashboard", + "tenantId": "tenant-us-clinical", + "title": "Patient dashboard screenshots for teaching", + "classification": "controlled-human-data", + "sourceRegion": "US", + "destinationId": "canvas-us", + "workflow": "lms-sync", + "embargoUntil": null, + "containsHumanSubjects": true, + "deidentified": false + }, + { + "id": "rec-uk-grant-report", + "tenantId": "tenant-uk-social", + "title": "UKRI project outcome packet", + "classification": "grant-report", + "sourceRegion": "UK", + "destinationId": "pubmed-us", + "workflow": "funder-export", + "embargoUntil": null, + "containsHumanSubjects": false, + "deidentified": true + }, + { + "id": "rec-eu-embargoed-preprint", + "tenantId": "tenant-eu-biology", + "title": "Embargoed CRISPR preprint package", + "classification": "embargoed-preprint", + "sourceRegion": "EU", + "destinationId": "pubmed-us", + "workflow": "preprint-deposit", + "embargoUntil": "2026-07-01", + "containsHumanSubjects": false, + "deidentified": true + } + ] +} diff --git a/enterprise-data-residency-guard/docs/demo.gif b/enterprise-data-residency-guard/docs/demo.gif new file mode 100644 index 0000000000000000000000000000000000000000..c4b1591008c912c1742533db6d32cef02e94ac22 GIT binary patch literal 59687 zcmV)EK)}C8Nk%w1VZZ|%0*3$q|NZ#?{P+I+_x}9${`>X)`}X|$_51nt`uX(w`1ASq z^Z54i_x17m^zZld?)CKW_4Dxb^Y8TW?eg*N^6~BQ@a*vL?C|gG@9*sB@Bi`c>hJFB z?(OOA?dt99>h0|5?Ck08>*ed}=<4a`>FMX`=;iO^*yip2;_Cb3=Te#;^E`y+T-``-`C>d-{Ill;o#rg;OX7o-rwKd+}+&b+t=IL+342G+1lCH*x1+C z*3#Yc(%9qD*4@(8*wWL}(bLq@($UM);^NN2(9qD%&(O@z(9F!u#Lwo+&Cbcp&dbcq z$;-^f%FN%!xyZ@N$jHgX#>d3R$->6S*uJa8!oI=9$HBtGz02ml!^ywF!@R)5u)yEG zzrnq~z_h>AyS>1=yS=l#$+)?^wz#^qxxKZwxw5ysv9`OVx7oU}t*^7Ut+BSLueGVI zvZ<@DpR(1Tt;V3LyQZkDq^7H(r>@VUo4cBFp`@surL3Q!rktaynxUzjpQW0eqL`bZ zkE6+!o~4MM$cLWDl$)cAnzNOdo{X5Oke8m2lbVZ^or{l_ij9Pn)fQpoUh?9JTkA8xSe1M60gO7TChjNCdcY%#{fQ)l~ zig$d4a(#(#e28;-gl&0-Zgzofa(rilwP1RyX?22Vbb(iOtY&h6RC20jaD8KMeQ0fX zUv7a=Z>VK!c4BIGVrOz)Yk6B}cUot5SZI4#W_3zxrAB9>om@AAS^hLnkmnCNDrHE zoJq5$&6_xL>fFh*r_Y~2g9;r=w5ZXeNRujE%CxD|r%fOt?uiw9b0}CEZxUk{Fh!ZPb%($`R z$B-jSo=my2<;$2eYu?Pcv**vCLyI0wy0q!js8g$6&APSg*RW&Do=v;9?c2C>>)y?~ zx9{J;g9{%{ytwh>$dfBy&b+zv=g^}|pH98H_3PNPYv0bjyZ7(l!;AkPPrkhQ^XSv7 zU(de1`}gqU%b!obzWw`<@9E#qzrX+g00t=FfCLt3;DHDxsNjMOHt67k5Jo8BgcMe2 z;e{AxsNsejcIe@UAa+Prh$NP1;)y7xsN#w&w&>!EFvck3j23>?9{@P!sN;@2_UPk} zKn5w~kVF<~Y{=9y@w zspgt&w&~`ZaKZz!vs_Lq&w(2U7q(1*;>Z`QYYU{1I=BjI# zu`-qDuD}K>?6AZZ8>*#C&1&qj%r@)nv(T2=Yf{QaYwfkzW~;5W(;ijrw%~>3T5EVt}3jwtJ-^2;>WZ1ch~ z=QQ)pJooIgyE)^u^Up*VZFICj-!$~mOgHT`!}oZ5&(l;_ZFR0m+qCr6TzBpDqFL9p z_19#VZMK|Y(=_(kY`5(;uTdMd_SGYdRQ!WXan^12>>Qu544FFmWyn*{yz*k|u)^(9^J zG9UkhAN~^Hq9K0xVi*bj`Q$&nzWD8HpCWXNqzSx1 z6xv9KJoo_*bMRsiJmAp<&i4;`=xBlz6yHALfsSWbVhTmVLk9n$10vy1fBf?w00l_E z9bs^UBlJO(-Z#GTQ80Z)qTxT{*EAjSP=02j-$?)Lrvq!ikb)8Pkq9m!k0XXp8$5hr z_$-(sDLU~V+DKxMjF>@72&sry44)QpWW_7W5K2CT(l*Yw4OW@&d^jXh8r?_19|Dm~ zLKKPjeDIDihVP6#@Vzckb+bsBO${F4;cgr2QXZR0f59uLIP5ei@ao$=IEp~ zim#2UdZYN_I3!N?u#R^`QyxQdy#jC}lRc7yKNe}oDkAcbi~NT!f@Fy!l5$6`ge5L5 zIYU%F$&=v&WwT7lG-jsKOREIQ^}IkqZm2;82Y`SK)bNRP{E zOQA)B=rgqx&5vB~66cG@1$;z>WQ-922sj4?2LraBd>Ql+X@v3ev?q7MK!xI{VP@ef|`hzF2}#yaX@4|OEN4-o*;5G-Mhx$?su z*0_Wn0btgD{1Qh(C8|-8s#IgXHLi25>s|B8qr#@I0*cTEJn(@IR(zC4Do_M6&JmA& zu!9@7m_rG0q(e>Os1fJmhYE~@gQ5QxbsLS?Kx5gbqeoECk-eqhYIWq*r$%+ERlRDD zEQ?mPvem6RN^DV&npBBS6iXHL$n|t_K5ZPy4;1uANsGi%Tb>W0I6?;nZ}6jAoR1xl zJf9zY#9i@u*GT1sk|6YvL3KdElqCowJ?5Jut%i}S00{6E2aF`k%3+=2gD#Fnz>8Uq z&>eE<(E?iG#B<=`o;hOfj`rK%|2j5D6t-{%F4ubZ6|h$G5F z6(^SR6%|q7twx!Uj5bnXPtgwwO4tX3yy7Ex#0o!VIK(&HQK4aY=o13~(_!4SjtYB; zB9BlXuDyYJotR@Bf#Q7bNWhM0KtVO+(9nqHZIgeyBPWLs&5Cwnq(OZ?N?STdqaIOFMyGR};`-uFADF6(7MFjsFh5?s!7@;cyefseYIuZf6!}w_&i5N%w&Ls~UPDqF| zn~-eA(FR0%#aaUZ2P*&mhp%;{i1WQe*F(~Qs|m?NWBD~+a6JYg8Ep%xCdQECz|5}KjL%vYVFcI_h_aRRJtXY{-dhH5Wz&M zulUX(h6h|=4t+fF2+VN`H55Sx5jcPdwvY@8`dCK{b*PT!UiZ7_eeZog-`|N;*zl>s z2MeIV9P~VJ3Zg>|6qtYUH(BO;K2WpFe0u9Z=tqF~Q-qX(L6U{d#v>CrSUw^O=aUa6sDel$isD0xMN(Mk zV0=3wiT^-(I${pz6Ad{s1m}YeWcVW&$BE)|3ZCd#uqcZ;LW|;Ki#&KfLdJ3wL(m_z%FiBcF(e zV4{Eg!hc!90FB@dVe||-0*mTUfK-=D|L_bTrXvLaW8%Y)IidiZr%OJ9kNS8;3Ta-B zMkFnFKCFi$jR=l9as!Hx4CVk2QpWD2Nmk z22Dvpb6}CVn3DkDe}YgXM;SgzsZ;QyhQ~IJYq%pR8ImJOK`m*6;scXLNkK`ul%Ob& zCqs`xVLv#M0A(Nw+%Rz?v<3GAb&J?$=cQ6PasV=b2bm^5)~F+4z?C}^Z0Fc?JtCNd zX_y&=m=x(wPO_BZHex5X0IUF-PQ(EwS%Mn2nuO$wKI;~3ZDk}&*@W%+qGw1!kccB_xt2Mi zp-yC>9=e{yIiwuXqj+V>^i zBXF;$b2##+;uEMMkf#*nq&>JJn93u1Nu_ycr9UyBKEi`Aa6gStpC8QqX|z=(vI(_Bk_nMn;Jfynj_B8tFP&XJd&=9sw3@sK86Y; zi297@g9J}<1K$@uc#xuTM6SU|ZLr9%wON!p0;&odiO^^y-}nfX7C!8707gOv6jZJ` z@{O^2sV8;-5!sdiAg}9+BlRjiz8b3Rn6U**q{KQU@wpM#GYG1Hus#2B1}~{2AvIEm zg(D|bh0`joL5p4h0CwT?09>lA#7MI?%Og1Jb>T|1NwS~vK&b#=ht#H-;lpGA00k3^ zvgTTEJ)*V$z_niMt_drgE(Ds%PyjyS1Ff*C9FPoP$RpJF50D_R`U#pwr2aad#bl_ z$hTIDwJ!UzQ4+HnaXm_=4yP~&Lm&bOKmk0^2)y}AS}%Rajz$!<&IdZs`I;i2r3QG_H3~;;dP>4990L&Y2NK&B9 z>jeyO0e8Tgms4$P$WJOmsb=&p_fY>3R_2nb#1&z z_sWa+J9r4`M+Ce`Nx;FUrp8XhLnDyKVYGvW`>Pyqw=n;N!Wub3*N`MKr*Q^JP=o|! zb4*A?thPIn#*)0meR*wJBF3cF$2}4TlZnOKdx0Yq4Hk_^BDoF9xMygX85`$MRv z$9@a|9{fu<;BZI8$N=C1=E(<2qC;ku%{!90Bcu*HDzeBNNy}W7)m)swc88vfH%)vo zPfR6VU=5-RL)YM1gZByl3PalvMFB}j`=E$Aa>?HsB$WIQ`OtI$JM#%d5D(^n42rU(eoabY;IkE-3unzj*4$*K27E9K^E!(us4z`Wk zG@abhwc7U3+W!sLaZMz1jSDsB-h9_7r! zP&;*|NYd5&WaYIo;4$6FfFrL&8?vMLeU62+sdt;P$`{+n@?L{O7I$<`g03kY4H0Bk2=C>6X6f z#DnP)q3N6+>bK+R5drF=e(J76>JVY-sNU+Kqv{XA>aITPlmqJyA?viB>x*OS4T0;r z{_B3j>kQ%RIidv6-~>2g?8q)9%6>JePzqG?>{Qb1RYL8+&N*%_GjI+iJpc{f4h_B# z3Q!p&)NbuiV(wNF1<;@bPonPZt|smdDbhYA^S&hUUhK!-?(422=zi@iBkT(SJ>CB8 z-yR1-((i$C?@glcXu|M}Qtwd$@k!$FQ9|+lUNZo%5CRVkd4MAr@CMNE3q#o>2oES0 zk0dAmB`H5B68|JEegIb6@v&pZ7x|_Fs?+%kTw$5BP$QtstfJOz-qL^7BDI^kl{Po)7vW z;0V3Y3yfd^^M3hukM~6q`?FvB5RU+VANYg+m~@}}dB69S4*=92`G=n)0-*Se@Ax`` z{NEn;)~@lQK|_d8%fL6#R16%rD%3zZ2@xU!@W&!FWe{g| zgc!1-MT`bIZS2)zU_nF{n+Y65mtniLyJa;Wzp2CQUiM?{23G|R;^qi9<`X#>&A{cF9J{k%}Ldk317yXS(D;M zjB)2ajcRc!U7Txuibb4Q@nXh}9Y2N~S@LAcl`UV!oLTc`&Ye9!zIV^TXws!kpGKWp z^=j6pLw^q016u!{ffm>_`=y{FDI5!Mgql6juW-2xp{-If!9tWm1J^+70r3yB4v2>n zgtjaVh6r$mR%ONpLxnWVdL;nR@F7Hr5Tig-5^~n89ttQV;p8za(D(5nKA!{!1uR&D z8Aqzpjyvxdfyf3%e3;+?B4GK20R*?}?mGbblaD_8@N=x7@HTJ`ng9nZ@WAwrOK!R5 z0NAa!--5$##DVgFhLaf<@FA+M0GO}7`|@iHM;&?e@jnvl}A#tD$e1SzwM*%=37!t&qAQ+4QP@oqT#j29VfnZ6csu$NHV#W0$a%w~P zLNbNO$if7m6k7@i36WoDE$GZoLq*I9XiCLSR{%f-b5)31ovffyW1Y3uTOpNHtV0t8 zV9`dgTQ-$ms2XSnXnsUiGHtm9z}p|0r4=GuX$_Y@V;{{mBx1$#63jcU+qvhTLxcuqh=d-xXps@a7^saqemNv%Tph@kA9d#0=f}VaFebJaWk=r(CRs9mYKK(;$XTHen$$93sT8 zD|}?cc$gUgPgd#Jx{nJ&nUh$m(t047X=cNrkYY&{R!ozSW0GI~XniqQgP|LQlcSz} zpxkHrqN>=N_gw+tg(to-*h4~oXyt=3J*dz%=6HFC;0{UdfvgXH_~I2qA0qVu=zb>U ze{Vl~SUa8G`Jto7Sx5?(G1ZlCdx~ow{;>DH$~Dk|4}@R@B^Z>;Y0iR}kQyRlM#w>Geb8kygvhucVlGZmu`y!o z*0+L~MaJ+D08*sV>%jQL=B)yWN(2BCAp*yIb#NeFlwJ!{X2uB?(vXKlWFp^G!3#F> zVfTR327@vOXoSKB3b28hLin+Uq#_B*VNOWCkfUHsV1;rtqKMdF0rHp!J*Xo^3lzWu zQ&bWnLV(5}6fh73gyIS%IDiKtQkM|Pog;YgfCbFp4VSti6eBVV931~30!qkA z6X(dX<{5LB0O;i}gE>s?Z8Mq6Z00kK<4I7yh?17Xq$bUGqa5w12T2MiFol`IBGq#N z_f(_*l*vqIVkDpABT1k@MJn7P-N$;_l#nc-7tpxW6dAfrX%^%l7I_)~NMVssz#>%-;2KU7 zFq2;-P`S)08Y$=NE@Z6MTnwJBS!TiOWjCQpR}KlT#>3)4IuMr%hFe!8 zOF~d#7O+}GtYbw?ToE$YbDeb~`KfC{#OhU??romj%xXfm3K7EgEVRrr!1@vb+ELJ#c~-+~ASL!8+H)K6a*Co!wz4d)n2WbF+&Z?Q4g7+(q7YY`F#X1=iTpr z*Z1Cs-FLtj-tguYJgy0Uc*Zwg+KF%V;vGME%8%UZUtf*nE5CWps~Ym6)_mthKl)97 z-qN8bed<*YXwwrK^{a<{?AP3SIlo@^x5vGYXumu)Yl7zz+1l4+Oyw6u}WB!4fpV6GXujRKXP_LF;>y4P?O>l))LK!5Xx| z6m&s1fx#Q(!5;L%ALPLtgpC~p!Xh-nBSbLB&yX#LKwGY|O@L zl#Fl`$7v+T$S}uqY{qnijCEwkV|2%W=|;_wJa~*pdE7>2%*TG5N5;U%e&oe}T#SGe zNLwUGJE=!k`$m2|$a74{#4yN*Y{mbGM2v}~NK&*&vB1cT^u&#nlZC{fhJ;3s#72+= zijFkNOGL?#P|1~y#Fm7JmxReflu0)c$y_5zYmCO4?8Tb|0GjkkKm?@$jPJI$#PW6rL4$(EJ>_Z%fD31zI;c$6wE!OOCT%DKDkH3Jj^_d z%g02-$dpV$q|C}BO2QNvyR#X!#OWI^Wy&nOhn@pMIQ98TiwIPEmgEG*CVWXRG z#LZJoPvUG${G7u3^iL|3&x@qb0Hr|yHP8bc&^f_Q#x%~Qj zPYO-R2-Q#=Z1dH zlv6=81nP5z!-Uf|P13qt(z0w)6|9EtV}dl~0_y9Aj4abOxIS;723MHWJ^WK}2-JMc z)J^TwPwl}%1&2cw%u=;JPW99utW^K~2rRWA#B*W!1kd)=fp$9u!tYy;aw2RAGG77?cF+lLkG^R*EcE6gUAc zzy?E2R#9D6K8-;mlQCltz1f`IS+s>!R4rIZ=s;$E`kL@LG+$}##1zZ0TBwVt6h0zUF>oZ*rBwgzx1mhhBV})F~r9N4p2I`C2AJEsFU0!!BTMPz1pkC_}hU*>Gxy=9=xZYQ7UfD%ougzWS+g|SN-4Xm=a0uV_ zec$TyhUQ(|#(mt#J=yjhU+OF0`mMhE6~{BJI?w$&&|SfTJ=pA}g&%-{f|Z54t-gl^ zgKwZd2Sx!VP+t$GJ`fgRaJbffJpmCmT@?5MI*T81zB-;$$;O8A% z>cfUMfPonvRkD=^CJ171$Oed2)u!#hSCC!}&fP$LVQ`4y8LnYw#MgQShaMhcZ~$Ts z9^?Ndp4jSJUwc(zCN9_~cHR_*fe;?y49MawrePZ%$nHf!9R_1D&R#TrV~8DK4piOd z?bk6bVcd!7r3SA(;SeMQdj(}tj$0S* zSme&<~N0dq!YbN$!roMfM?>T_k)O=9cI z=#Adv{1ri2u0D|Nz>?0sR)%Q;#@@L_=?+wBmIhy=j$4e@Xm##r-~H&24r&fO>I{%) zdd`4E_G#>cT&A|^2mWUp4d|L1XcR2yt-ihPy}mwv3MK>w^7jspY=GUTm30 z;X^g-s$T21=3B?kYmVM)5UgzKyKIAA?5dV$Uqxh#R_yKT;e@^G(vC+6wv7KY&AeTm zPZ})fdroH$3~gn7>Bs%pLy+j=#$J&g)(?b%QU-@y&}$r~+3Ne)6S!XLu4PXS!F#sp zdj@7@{csr;@wg`2B0ge5m}zhr1~;ZY zI9_WGq+%ntVl|HO8JBLeb?>Lv-vQ469rth$|M4*%h9|#oHkMu_cVho2W^vJGYZ-?D z8ZTP_pX?XS@#@?0F>hixmhNg`0;iR84t#N7<#8?_@hD&ODUamN8f%U#>k}+!|CQ#> zp6gMyU2ymYvR(0grRumnf#|J1Y{-D>wZ5;u=>Yd$EN5&oR^CQOVC;)*7?@oEj$an% zb5Bp-xb<`>mu!c;Zt6Q))un|~$3AL^b@Ux&_GNYEb@WTe-dc~_TffR&pI4#o_4aLb z>I-1w_4L#QXH6$!UoTw_RCesm?XX7ELoYesCQ#t+@X^i%Y~WN{AcWIB^SGT@YUqYs z-~#Ec^l3$NgQkVY=JX#pgj}eGZ@>mw0Ax4EbphA)jNNmjwQK)&rv`U-W!q-=j=fwI zFa%-fhHtRj^6qJ>wOXv@^MD`tfukCIdkX8K0Q*$2U07CLN0IxY48SY5C%iI zT)x+Q!p{K6XSRTK`@uK;7#;f7$I{n_eGbI@Di!w#R(=1$x5C+vd)c4;+P~M*Cw$Zg z4at8x%CAt%S9{@yd*HuN=a*38cZ|SaxVPv1(`Wv<&;G;T{yCNY#-RSvNOahT{sb+5 z13mu${eH#>|KhZM-cNu3jDP%`|N312#c2Q082{FYxb-I znqh5b?dtX|+=gG*wvB7|Zq&J9>*DR}x2ImPeEtXjn)2OF+z z*&*VllrM8GoLT7R&Z6;tW=nK5YR^n1vqs4HG3?kZBe%wSIyG+6s}qOL?OST;n!SG$ zM;e^u@Z!pS9we0AyX**YVdpY)`%_m>a?)~TXq2I@Y-%YnW`ScsZS04#oI(*3Y z+8<1={yt{&d(q?XFOhvh_W7qCfVK^I;D7qj7uI|6IT#al?FoqBcLmnRABFx|XwZZi zVz;4n9%cyQf&?}AAd0iiS0Vr%idY5BhAT?*6uEV;5?NPsGgn}@`5=(AT z=5lmGx}~z4Ewac>D^az%UOO*O+E&MHx3PUItxl52Vh9*a{D8s_V9=7MH@xx-RJ|h| znSms5b|VQFFr4rM6JQ)73=y06O|`Rg3O0Y5gdWEW4w zC7fW?>=nx5<-`S?)RZ|iy#)94LpNPg182)jYXoye`MzRxxkXcHP|i9x#3Z8uC9LN(_W2~Mq-o0s0v&9#NCxLIWL_V%)QF1`9vo^K>G z9ZX0f3^w0X!}smDM|3(xKV;J-AsLMBxA6(;zPp#y*v@0Msb59O#RPy`M39w7ZI5S>Km?*nEf))y4Hwq zd=O$BeH_P-6Tkor`V$B?=$1eMK13Gk(p>#E2)+>Mqzzs22HpH85CBGxgAXB~prp~g z0FIDF4Rqj3%D11Mtn5*_(4YwMcaeB~@DK@9U=3xY6Bux6Pz_O`o{q>67dixng5uv1 z7s#3$;;WsKj+qHebAo*~S>CY!D)ZXDnM98G+k($ZmLnJAvalsOeIU!<~fQT_T@p651M8O)t zVMK?VQi^=E$osrx#yu)hR*aIny(`sgp(!v2&Y_-bB7!m z<1o_cJWlo!lS)AvoPJE*c_WSwZzTjB~u`>sHZ4963s6`#DuPFCm1`T%MZ%2 zbkD5F4{dlysxF%n1xy8O(<)G>Z14V@!uN)5+CzB2E3qG8e*{QdoN(_JiP{Mpm`zOPz$)OQCD+QIFD0;8K<&RuoiAID!H$5W*F?kVZAU@eOQHgBF04Zwu!-&`#39 z719V^HH81cF;q@b!${%pQ%&5F6EL9-hj@iBKKzYtgcy+L9q>;71>4xd1i?<(Fo`cx z)gV*ur9=jdijSnYC~akeHOp#=(T=@oj}n^v~7J$&NKK28@ z)}{Yw%f(;Y<~9;@{B3S`NubomF_U$S?QCn?)##3(yPes(aWA{v+dQ`j)2;4x=P=p# zcDB9Yjc*$2TSf!VaKY&f>VN|}(ENt7zwdo;nlfDA6Q^p$CBE>CoBQH;<#@vnLTcFI zdMLI%Im%P6@|Cl^<&TP`%VRF{nbW-Hf`YltbFTB9^E^*D?>W$eF7%X#yh^n`cFDOj z^rbVM>49xJ)T7Sxr%S!+R(Cnoudem2tE=cX8M)$;?)9;gz3gT`JKEE(_O-LU?QVa& zm%%PJvBSOYcE3B`^RD;3^S$qW|2x=@Zfm*&zVL=WJmM3t_{B56@jsHg&Is@L$y5Kn z@|M3m<}!Z~E1_CER3um1J3fB4cL68hUO|M}Cu{+fTkkm7Iu{qw*7{>MB1rHKCh9|0Di z0Un@h1>oW!AOkj_13n!kz}kM+?TF4c;IQV%`kaN(ttm5B?w!;@%EQM-2v{5gz{`60Y44 zdP@%`p%XqK6e8XdI#Lrxp%q>s7QWpS3QQGdp%;E37y{ZBPDdApp&6bb8d}o`idh=A zp&P!T8m?h2l_4C~p&i2E7-rKQ?jaxcVL{}fV-%qu4k95QARq!%Ats_CX5k^Cg&-~> zBt|0nHR4Z6q9tCU4N~HuVIn7Xq6uo^SJ9y-jv^_}A1ESQDW;+-7T_t$mME?wEXE@A zwIaL8qAlKH_|amIy&^95qA$jtF813m4kIxZUod)JF(#uj-rg~K5ic$yG)AN1H6t5J zqcvV*=20UFVk0+pqvL7g9D1WTj$`40B03<&WBtj~qL84cH~7uq(_FNLxQA8mgGQ^q)DdaJ))#aw&Xgpq)W!+Kel6RiJcDo15W+}KhVQE zSOX{w!GfScPWpgN2qjJurA#Je1;S)|++0InaYVY=a_H070x}W;*{vJM6=iD zTNcC=+=4nRra6ED4HQIHZh8{p;v`@a#3kgU8WaR1_ybh_ zLpu~fK{NtCq~|1%=1~%aAEYNvE`fYnWkFbiebPf2Q1&BleI0IB7gF5^s z066DAZ~$={r+EtKQ?6%o<|Kqhr-Ty3d8Vg@ZYPK?S6|k~UmnC9Fyu zq?*R)Kg?)|<|#*wXlsN{LOLTKhY;N%^QCPHW? zp`L>P5CjeM1D8%|PFg}izyo-S#HD5`06;;Qjwwz$s+m$Jp&BYdASzBOs++DVoVqGb z!YTmB>Oat`p8o3Nb>~Mhq)7N_LEHg~0>A{==Tlz7vgRsCXl69zq&;xMEd&B{dg^>q z09_iyVE%(PXzM?0K|uiNKk!0GB!Gajfpuu;t^Py2&a1N8 zYoG!vz?KM~vWBsWCwT&Z4EXCmoI=9lB&a@WX%2yOKIJ^%r??Vj!~Vm=5`;G3T5x`?3u!>eadXW?rf87BuBDH!3ri} z8fLw2>_OD4t{#MD0>B2~K`U58q~c^WBq_I!YC(K!xGt^8o@=^t>OZh+LAdKbh-tn; z=K|y;y?&}lIH=j8ZNEA!&&I953M*j|Ye*C(H%p*)2$vC`J%7-QYZVmukP}0hN>?AejV$wm+K}3SlXvl9>gS2C{#jcBY5po#^>>FgMIeH z8z3$K7%o)S!x>QU){-np9B6_r==c)EGeD&(@GC(aFtb7@0F3ZcmhgrG01Dfu3imG! ziy%FICrFjgAnUOI@|&j zxMpb*L>1_QI_N_^n1d+X02a?DL2U3y+$K8c<|Fhbg8~2tutGcNgE{blyuL610I?7g zu@P(K8^`e+*KwSZZx9c05g&04AF>V0;XzWKZ-y`;KXTd)EcP`r*+Q}g%DH;1!1vtBlzvpTz8I)nd2CXfR;=z%-Kvph@2JI6C?NC7+_!{_0_KS(J6_%lF@ zgg`4sK{wn%f8ZBD0||u1LN~PVu>mv`XnG|9GzbA0wsS(=-mf-k5*L2$w>K*JRqfr?p~;c4CZlNo!tQ|Hx`{RBLlo zU7O%!6NCUTLNg%cY+H055CjP{!xw}?CouL?V>bCh!*0(uK~(luclAMNc12%wXZx(! zakLa1!#IRPM~ik0WI{NM!#Es+9w>l8m;fepw>WIV5rpyeT_<#h=YxYMc!uL-J#51q6hvHqz$vH$KAgiR zWT=H>xP}7&f2;q1D7*tbAcIi@d4H7sXH*m69{BrC5=shz&^v_Cn^dI=NkFO6L;(e) zSSS`iq?gb;1f)yvy>~(By^DefNRti%q9P(W?7jczoO{=Ob=R5~d7H9kp84gO?}x@S zp4$_z(-Ocdk0bj-eBUF;KmdmaDvhd%Hk1FVG++Fgn-p$-W5L7R9cu>ph9{CcIyK=pp@)Ix!=%fzzZhL`mx5c%6 zed*9l;s3Rp<0-&7f>knyTwXbuDj-AC1SO{)23-QRc_1?(xRbuUi&q(U;(>d38Y&pv zEz{nkrW_7w2>0LTFvikV^vIxkaqd1nsy@B+&tXD%)x;-*LBTSqJt#2%2@S-UpNBj- zkNfRC#0-+FP)mdRlA}7N%FoU9eY&`nhmPGv+dyIOM^uw-ue))kRK8-7d_*&o_UHoqzxig81llt;9B4eZP%zue%T4 znY{E``2vWH2IhR|lyJf$bg^ubJqy3Ps+iTQNoUK<7b}7)xMhO0JRW9B;J{v(G6w@s zJK1G>7$Hi$9yl_e0H71^#2m~kbhXLbZ7$#%uuw0r835S=DaQqo`H=2~L6mI+DSA7l zDpYYLstQlLsbxE9AA;ZR;=Er&1Q>$P;viTnLllNy!Nm&*mI>GDrVd$;1rg=rZEi#{ zGyV%wKf2paLqBnTA5554rezj)I~S5#*EIMel>A4I{L#>B#-sDsJW`u_04w_~hLbo7 zLKfY#yo%FNR{cz&BeS+sY0}1MHi_%j`pS#7j}>&yZr9;he$99j##`+-BuaYk9?W>j zD$$h~D2AS#1S5KPRV#{0k-+eOa$bh3+*C)%JJ#;!hzeFw)-zK-DPzFfl4Mt0>mFGH~K zpl%RW-K9UiGmmOOiUj3}xGOt&Gf3JjUOnPOQYa5mjStN8B<2bQ`cLN-4@UZ&4h;DZ z6T>xmlm|*Pn_Q)}pDi8r zghW;)3XPOU<_vN;@(l}3Mo?tl9_Hs)lopo~^jgnW1KF)-!r~La9OGaftgN_nt zE%7^@-H%hD)OJr(w~%%$L?IafUmFtrMxgBHr^v^PRO!b27%&?KK%rUGg#yK{Miu36 zHJD}w6|vsuuz#hbCqS3WDir8_(7I;K`+VM^(6%y9B`4y_wU?#!ck}Kj3twZ=5Td(_ z0QyD0EB_^sldz^XF*n>W((8~M;F|8A5P8$Ro`5`9;1La=V0!iAYLwKtJeEv&Z*DCP z=hoRBjmxm3XH`}IqwZpqXh#{;D@D5zCC%X+77^QGxOvM(K=5(I-HW5{=y?bb=zqRa zob7k+e4LQ4Ut6Ll6m#>QeNko;oq$+ndK*>AdtI4Pf2SL(+*cn&bv{0=3K#F z?1QPFqP`;$qcz)71OF8CIB4c$=AGc)oB)0rU699O%G{HbtI~fBQZ!qH08=1cS!nv% z=g9O-k`UvA;b)b3)Tu{JD(^2Ozg4dbc5_+0s%@TtL@Ju`d)Qqj9VqPPI#b#sM%5-} zmq4e!KC6N*dCSVlf54Dsj)=Z@AB6+MEroJ`CZQYKs8lRDIKr0aOEJ!viw3S|WCp zn;#5tSJ=d}EjeMVv_BwJ-J2R1M?!_12gHTCk!|9J!jwYYynD3Bh{MS+Y(YdW*$p9V zh@qiE2G7k4klKj#bJxe}Fgkv)Vte%SfL9y_)Ct)%6R?~Wc9Pk*5vZSwN z23CO>2-VXE?}qcg=YZNZrd`aMtjKQYuBhI>t1kb*^j6|ts_vsJw=u}zWtrS`#k1ii8t|1%?)9$P7GKjSe*t)Ucd=ohBFh6YW5PNUj_p&UEowQm9m3c$<^<~)%XI5;H5u}i)#@wF~^ zVmzR@Xdj9RvFg_pU7d-)LdVt|{ou+CnJ6>uUfOt$LLu_b@W(AQ-eAHF9uXUYrCpdr z&WXc+)M#tT2QvKA^!sYHH@V#DqM{TU%z%5>5rk*o8_WlxU3- z^XR+6v4&D_%%T)z(ldQGWW|$n({rD`p;N#|yShIX+8BrLZ)Fc%G-CCxz#74Xb5H(-DA`AW=9*Vdr8F62}0W)yx zz~znT6z}o1SZ$6d_oNZ?O)!D+Clo?fk`rU+A$yB)rBh}+2TV>9p9`eNbP_mNaC2qXribKB zLwS8T!q7SesQ2h@uoo&)59>D0c+~CWr)6_Pf(@JTb1$FQgC-_G6F$JAA^+I;9@`^4 z#~RCk)X5|enF`Rau!PBbP<){RW1E1>oQ|&VwytIX383o=ER^DGSnM{y@XM1c{;H*Z zk^_==cO+LjHl)50-GX^X(Nq&59xwbi$oqG5{LV12U3(EItibh5%JxxGQ#OR+8E04Sl(|h9LBaq~ALiTxoPwzMD+g$KR zqc+I46Q?$~Xrm)wIsOiwlOT`U0f{%Ih+49SWtw^EUo{KUqTqxtWm;i{eHcVB!? z-`}qw?o*5R@$G?f=PNRILscaXyf*)SD-^aLev$Qqu=(VM%2&^+ zI7>{Bz`t$M8pKYF;%B8G@tYL4cf9@5&&KACKD=uD!q=?dX^+%tSPTMHv9=u@@BcyM zrx1zk*?*VreCnT1gP26Nt42Bpe4IQ0df@ zAmJLY_#s$I5<*S}=0ybx7(j56WH+41NLs`T`5Fl47DUI1+^mM&;*gx?JDH*6v{eno zvqK7JNy=SKiYJq{#s-uDlF$$*s56n$n~3)#LX(_eX*DqBNobq_K4B78S_7{&b(`pBvSZMZ0`cyi+&hwDlDP2dqeGI>=Nr)PhtprDg}#EXfD4#Oc*a{& ze=FiB_V zfi%OD@mg$F{7If6n;em?d4Z4m+7Duy60n|BTYI5j*V|kUNJ_yudB&44gbn^1!nRQB7 zqH|fKjch44>E;8t`2xvJ12}(yI0d@G%n6J$JlvzXUy5uT&wE8wmKkP1+~c_~p!($M zD_<|yYLU}gJJ#$XYM3i4T9->tm0DyDMFsJ3Op6`9&M)jKyr=RUqyeYZTE? zpk!J^a8>BFi!g~EiKk?0c?i>*y*B|b<`TlM4%w!5ub#riRM7*_HxK_$Sw!^cn#N^A(4tF zYHF_Pv|)@JIp72<&MeSs;0cC^=Jp7nPzUIUIB8)U4R2XjbB3Fb5^u_YwUQFGDkp9_ zyK(|{(47@lR|140fLlON$8$!5mQw911CZycU4Ei%LJLCVquQt_AA|1m;pJ}M9!GV- zxzMn{6(k&I7_rQ}^#ns^1zEDXUqNrcjWvLMyn45bf-Z+_8tp6lQcmIJNZTld0ng|? zJu)|HN+tsIac+`kCxK8+eoQch8wrd2G-~ZLX&D{4o8$)PHNBH`C*UIjvy33wEi=n^ zjZQ}U!R@^G0%^Uhd%YNMGYT`Uyac|zRI!(|b(O-HAOK7QO92`rn?t8Kr8e3~ZwmnU zWgO0h`75Kv$0UoNKP+N+$k=jNxQuD=)A*}_yz7Dd;~3%dz#Ekqk&D&BhCoq+K;evr z@aY0ERPvRJTyelksDO@}zngVFpc>DlBOYi2>a_NDu-(Ut@Gyz-F##DoTou}krz?v> z;72lfqIkE5g59jTpE!`bujYFUK+C&6g)#tI%9I9RvsEd%bDcQ6DX=On)@>KxBsm9H zYREUK_*nLo8sO++7VE^gHA-yts5D)x_IdQIF<Msh&C*|3bvNcqtk!4=bf6F9`> z`$2V{FzaQg>j8vUzk@Lz%r!pDZu}U-;1qR18^lGfbZoO*P_R*03WiXkAJX*TqR?P= z(?GxR!QDO!Oa8mm0@h6ZibL+E<6-iNgu72ea%%358`&qF-^KXkCh_4tsCfwzt51sTn3%|~AG-th>D$;G>O@+uh8U{0W>2`qHsb^_hbRRS(q8{B!JS_Z#ggiAYB zfM1-1zBu1_MPKYhO$p3zJX?MI{Hc3HIEzc20Pk>rkJprAET0}f7+E75wQ}=$5j665 zsPOgTcKj2kiWUd;u2<#3G0h&4!KUt0EWnJZ59H9bd)!Cvwfl)?>7j=IsYdFO;NFhA z{K2f7lc3zhZ#z$VpWaRXkYd)o({zXmqc6yMhSFZ9+5SJ@9!oX0cb;DIU>(4(yuw%b+9Nw$Q66nBSOxH zADktKj}!uD);vqnSaP1YZO!XrsuH^3wXV!u&_c5pGIl`+o^!Fc&v!SKthrlBi zdinXxtm{B06U#{L&9eq;J>dd)4Vk0T`d-yslLrg;3o`qjDsk+Nf6)k(G7FB z^Q=6m^>b5AOVC45XSLI#1JnwQ*lv;S-`c>|(m#R0PYzQh0?QZ^U3Tp;-SaHalXA*y zxW6xPAKvl`Up}vmR*k|z0#DCmqr0OKt<$3ISHPV$I&UAZzB_h&e-?GfM%Bd-UA2gK zx2}_g)9roU{JvPU&cCcXEe1nMz;b)wDAbo}PZ_$uioV-G04VeKrz7vhV!NnDyO=AW zhh3~rRuFIiw@Ozpzj7b%-yW=auU2~lDXt*%>Dc_<*lKiRJiJf4bmBs6QkQ1Wavbw` zw9hKqAbw$NS$8~|an!EZ#NjFffd&#zFn4HV@Uj9qC7A~EB4a;bayTZ*VkX~lm_=Q^ z)0Hs{vKk%U8=bDe04vO1JX5Em6I(vRG<5Ar_;y2LyLe!kMGOXXm;M*wlh7*Pmn4jV z2cPJ_X~$cZKVIJKZtH-3Vqv77k9@Y!QrUA9GheZ@?F>ULYkch^VrRb%6{h4(*I?Jtchz8oF87O}2-g(jJ1 z>FpKA>L1&ARd$b^Ar}BsvNe-H7c57cjLlZzjZEQ_T-Kd&%wgvKLRAv2E8uji~$1Oz7PRHR+*E@>LWoA52 zQu@XAkDmPr%Pmr&-wt>1-rB{%k;*|I7imxB`@f6tUEbektG|QZA0i*Hk#nCxNr$ZO zkC0MFk6s*c`X3AXQ}d|);(LF5)&ED=;jxs&4+Yhe8`-eys%HgWCx)s&|J0oPp+31o z`f2S?ZkGK|PxaR`(yvOVUp!+!o!|fVCDCnW9f%hE@_&CC!%gu@l@v}oO?&@GP4dt6 zsNb3XXQl6d=X?cH-k&x4|0R7t%hx(3kI%1dn zv998vF|PVK-!wce#iMs?%PRjsU_xgA8G=nYTr#&iJ_x`3OxWuGd|-+KLnUlaQf$~uyMJ?c|ef0d9%pl(`yrC##7d1S}# zM*+QdSMTVEUnq9^(pAj z1ih`_4O(4g_qmn#yp~tz8XUj8vq(Drw%9KCcq!6oxYl`P`FU)-@Is68z=;sr4*c51 z`AsbJ=^HEh!CMoz_#P}RbYfR}e+FAR-0mxMJ148N>U+-JrUj#Dvnsrjt#IQf?YlVD z2mU$hY6h$%l5YH@|M8TdfmdGpk8weAA#|kDkB06}EcH7z8fjUohs&y@nffsfwu^i& zZ}6R5q{Agj+rjU^mgms%=OV*x`uKkkLF{k*6F0mh0iTRC1`Gc2fIL%apr6y<{M*p( zF~?!Irs#g{&~}wlVSEK&X0PG|RWDn$Ijp9OeGJQkf;A;hLm=YoxL|Az?4ck)J|_*s zMV}aDLz{vFd%)_Wt5Ccjj`J3^#K9YBd+&A9I>7EzDr~@NaEN=O|Q5ew0+kjR(D>+e!gxQ&lk%;y#<<8BPgUeARRxx>`!*mrkS!QxUE}BPySf5%EdyTT_<&rDdjvk(7%SmPl_Myk$19^ z#6W<=kMPO(^OG-es(}H6R+L9yv4AQ64XSmf6T4pQy_Hz{>o#cRL&|gNW2z@C+ks$b z3MEN$!o%KC)*~w_z_^PhkHr8DB_qhp*AfEcc^_;19Uvn&>O#De@cW^({IASa4On*y zA{)Aw#36*jo#$&>uUXuYkCy}FyP26h#%||HGX|^&ueO{zOsBc3njjP zs>2!`eMrth5eU5>eTDj$Jq0HXxNmAG0R6g#984h%n*&+Vb$E1Qfi9{G1?TwaikR;N zO08&Uxd|8E;S}C^8EW#m{@x#~21>KskDD)WI~@4U57F_Vmx; zy>B)P3dJ+EMhSGmId|~I*l3oq0J{}#D=?OxUHCP)hSbIIK9ydKY&ELej8+OLVF!$X zn5&n6mrM#7X~OCViCpW2kcd+4YU!YWP7vEQmO=KM_}uanS9JMdF!n#0(^zUs2rX6; zg+rD04wqsWq1ltcQxy(BrQ)IsOniBjn`HITOY~azRTPMv1a%G|2XSAar^0#93aw#K zSX);A>8OD(nV$tpcCkX^caqP2ftupK@Pb>O(WUw$_pA_qX);;qfk85Ys{d!xF1k-O zcZ~P|3Tyrv&OS{Mw5IGR4^d}tjhYulBRy?h@NQnFj;uCY|3U;Fcw$7G<}3EZxwJkY zCpSB)(#W3DWq$6Au_isFVtm^6-ppOCCK3&2WV~`zuz%ucUjCwp?r}}DgjEh;6ip7; z!29EErfvgUy&_~rkw#FyqA{zRG6y;W3tbFsmyeLk94ocKsd|NVA;8nBRZw7yNz@AG z3r`J`<if&R>yUy2LN@ndz<<9be8veqk8Cs&V{k zq9c-kGZrJ7H0cW#X%$~Tfz^3&xblEcrSRyZxC?-g4VBi64=`6d3rKFWzHKM>9tOBC z1fmxR&yu!MCfv@{GuzPN%A1D|gM&S@W%>X2Fk>KvGv!PLQLVwV-xEYMsGn^Ijoy4dDRda%sZGp-t4{`B zdr_pjHKQxzRo10>atDOmHT^xzgQ>czE-=Kf%KANN_`^f($uJm9ccORe3)wHXn(GhV z8*#|od?27ZNno+ErZ)nBnvVnTOs8ppHam>~*ndX(cX<{seqsMJ%BNBfzI9Erbyw`$ zB)nkw8b>(wnKRBE_)K|mSeIeZLd_p_C^1gZAfs@J^h14Hq&z}HpV-OD&E7}<{$!m+ z*?q`HQM-C`{+hZ3H9-ZjPEBeFsDNZ{v�&+^)#OwFAhVbOr#vMUJ^ZMF$~zX`3eh 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
=h;4rpW-RZ`6x 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 + SCIBASE data residency guard demo + Dashboard preview showing approved, review, and blocked cross-border research transfers. + + + Enterprise data residency guard + SCIBASE issue #19: cross-border transfer decisions before export or webhook delivery + + + Approved + 3 + + Review + 1 + + Blocked + 2 + + Cross-border + 3 + + + Decision queue + blocked + rec-eu-clinical-supplement + Missing DPA, no adequacy, PHI boundary + review + rec-us-patient-dashboard + Human-subject data needs approval + blocked + rec-eu-embargoed-preprint + Active embargo until 2026-07-01 + + Run: npm run check && npm test && npm run demo + diff --git a/enterprise-data-residency-guard/docs/requirement-map.md b/enterprise-data-residency-guard/docs/requirement-map.md new file mode 100644 index 00000000..4acd9483 --- /dev/null +++ b/enterprise-data-residency-guard/docs/requirement-map.md @@ -0,0 +1,17 @@ +# Requirement Map + +Issue: SCIBASE-AI/SCIBASE.AI#19 Enterprise Tooling + +| Requirement area | Evidence in this module | +| --- | --- | +| Admin dashboard controls | `dashboard.metrics`, destination breakdowns, and `dashboard.queue` in `src/data-residency-guard.js`. | +| Compliance tracking | GDPR, HorizonEU, HIPAA, NIH, UK-GDPR, and UKRI regimes in sample tenant policy. | +| API and webhooks | `webhookEvents` emits signed `scibase.residency.*` envelopes with deterministic digests. | +| Export pipelines | `exportManifest` records residency routes, decision states, finding codes, and evidence digests. | +| Institutional repository and LMS integrations | Synthetic routes include Zenodo, PubMed Central, Canvas LMS, DSpace, and a journal portal. | +| Custom tags or flags for internal initiatives | Classification and workflow metadata produce decision queues for restricted, embargoed, and public records. | +| Reviewer proof | `npm run check`, `npm test`, `npm run demo`, `docs/demo.svg`, and `docs/demo.gif`. | + +## Distinct Slice + +Existing #19 attempts focus on dashboards, export packaging, compliance packets, webhooks, identity drift, and retention holds. This module focuses on data residency and transfer impact decisions before exports or webhooks leave an institutional boundary. diff --git a/enterprise-data-residency-guard/package.json b/enterprise-data-residency-guard/package.json new file mode 100644 index 00000000..83d1b8e8 --- /dev/null +++ b/enterprise-data-residency-guard/package.json @@ -0,0 +1,12 @@ +{ + "name": "enterprise-data-residency-guard", + "version": "1.0.0", + "private": true, + "type": "module", + "description": "Enterprise data residency and transfer impact guard for SCIBASE institutional tooling.", + "scripts": { + "check": "node --check src/data-residency-guard.js && node --check scripts/demo.js && node --check test/data-residency-guard.test.js", + "test": "node --test test/data-residency-guard.test.js", + "demo": "node scripts/demo.js" + } +} diff --git a/enterprise-data-residency-guard/scripts/demo.js b/enterprise-data-residency-guard/scripts/demo.js new file mode 100644 index 00000000..e133580f --- /dev/null +++ b/enterprise-data-residency-guard/scripts/demo.js @@ -0,0 +1,12 @@ +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { evaluateResidency, renderTextReport } from "../src/data-residency-guard.js"; + +const moduleDir = path.dirname(fileURLToPath(import.meta.url)); +const rootDir = path.resolve(moduleDir, ".."); +const inputPath = path.join(rootDir, "data", "sample-residency-input.json"); +const input = JSON.parse(fs.readFileSync(inputPath, "utf8")); + +const report = evaluateResidency(input); +console.log(renderTextReport(report)); diff --git a/enterprise-data-residency-guard/src/data-residency-guard.js b/enterprise-data-residency-guard/src/data-residency-guard.js new file mode 100644 index 00000000..496b2e9c --- /dev/null +++ b/enterprise-data-residency-guard/src/data-residency-guard.js @@ -0,0 +1,318 @@ +import crypto from "node:crypto"; + +const DECISION_RANK = { + approved: 0, + review: 1, + blocked: 2 +}; + +const CLASSIFICATION_LABELS = { + "public-metadata": "Public metadata", + "unpublished-manuscript": "Unpublished manuscript", + "controlled-human-data": "Controlled human-subject data", + phi: "Protected health information", + "grant-report": "Grant and funder report", + "embargoed-preprint": "Embargoed preprint" +}; + +function isoDateOnly(value) { + return new Date(value).toISOString().slice(0, 10); +} + +function stableDigest(value) { + return crypto + .createHash("sha256") + .update(stableStringify(value)) + .digest("hex"); +} + +function stableDigestDeep(value) { + return crypto + .createHash("sha256") + .update(stableStringify(value)) + .digest("hex"); +} + +function stableStringify(value) { + if (Array.isArray(value)) { + return `[${value.map(stableStringify).join(",")}]`; + } + + if (value && typeof value === "object") { + return `{${Object.keys(value) + .sort() + .map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`) + .join(",")}}`; + } + + return JSON.stringify(value); +} + +function raiseDecision(current, next) { + return DECISION_RANK[next] > DECISION_RANK[current] ? next : current; +} + +function requireRecord(map, id, type) { + const record = map.get(id); + if (!record) { + throw new Error(`Missing ${type}: ${id}`); + } + return record; +} + +function evaluateTransfer(record, tenant, destination, generatedAt) { + const findings = []; + let decision = "approved"; + const crossBorder = record.sourceRegion !== destination.region; + const inAllowedRegion = tenant.allowedRegions.includes(destination.region); + + if (!inAllowedRegion) { + decision = raiseDecision(decision, "review"); + findings.push({ + code: "REGION_OUTSIDE_POLICY", + severity: "high", + message: `${destination.region} is outside ${tenant.name} allowed regions`, + evidence: { + allowedRegions: tenant.allowedRegions, + destinationRegion: destination.region + } + }); + } + + if (crossBorder && tenant.policy.requiresDpaForCrossBorder && !destination.hasDpa) { + decision = raiseDecision(decision, "blocked"); + findings.push({ + code: "MISSING_DPA", + severity: "critical", + message: "Cross-border transfer lacks a destination DPA", + evidence: { + sourceRegion: record.sourceRegion, + destinationRegion: destination.region, + destination: destination.name + } + }); + } + + if (crossBorder && tenant.policy.requiresSccForNonAdequateRegion && !destination.adequacy) { + decision = raiseDecision(decision, "blocked"); + findings.push({ + code: "NO_ADEQUACY_OR_SCC", + severity: "critical", + message: "Destination has no adequacy decision and no SCC evidence", + evidence: { + regimes: tenant.regimes, + destinationRegion: destination.region + } + }); + } + + if (tenant.policy.blockedClassifications.includes(record.classification)) { + decision = raiseDecision(decision, "blocked"); + findings.push({ + code: "BLOCKED_CLASSIFICATION", + severity: "critical", + message: `${CLASSIFICATION_LABELS[record.classification]} cannot leave tenant policy boundary`, + evidence: { + classification: record.classification, + destination: destination.name + } + }); + } + + if (record.containsHumanSubjects && !record.deidentified) { + const severity = record.classification === "phi" ? "critical" : "high"; + decision = raiseDecision(decision, tenant.policy.requiresHumanReviewForSensitiveData ? "review" : decision); + findings.push({ + code: "HUMAN_SUBJECT_REVIEW", + severity, + message: "Human-subject material needs de-identification or reviewer approval", + evidence: { + containsHumanSubjects: true, + deidentified: false + } + }); + } + + if (record.embargoUntil && tenant.policy.embargoExportsRequireRelease) { + const embargoDate = new Date(`${record.embargoUntil}T00:00:00.000Z`); + const generatedDate = new Date(generatedAt); + if (embargoDate > generatedDate) { + decision = raiseDecision(decision, "blocked"); + findings.push({ + code: "ACTIVE_EMBARGO", + severity: "critical", + message: `Embargo active until ${record.embargoUntil}`, + evidence: { + embargoUntil: record.embargoUntil, + generatedAt: isoDateOnly(generatedAt) + } + }); + } + } + + if (decision === "approved" && crossBorder) { + findings.push({ + code: "CROSS_BORDER_APPROVED", + severity: "info", + message: "Cross-border transfer has required safeguards", + evidence: { + hasDpa: destination.hasDpa, + adequacy: destination.adequacy + } + }); + } + + return { + id: record.id, + title: record.title, + tenantId: tenant.id, + tenantName: tenant.name, + regimes: tenant.regimes, + workflow: record.workflow, + classification: record.classification, + sourceRegion: record.sourceRegion, + destination: { + id: destination.id, + name: destination.name, + type: destination.type, + region: destination.region + }, + crossBorder, + decision, + findings, + digest: stableDigestDeep({ + recordId: record.id, + tenantId: tenant.id, + destinationId: destination.id, + decision, + findings + }) + }; +} + +function summarizeDashboard(results) { + const metrics = { + total: results.length, + approved: 0, + review: 0, + blocked: 0, + crossBorder: 0, + criticalFindings: 0 + }; + const byRegime = {}; + const byDestinationType = {}; + + for (const result of results) { + metrics[result.decision] += 1; + if (result.crossBorder) { + metrics.crossBorder += 1; + } + metrics.criticalFindings += result.findings.filter((finding) => finding.severity === "critical").length; + byDestinationType[result.destination.type] = (byDestinationType[result.destination.type] ?? 0) + 1; + for (const regime of result.regimes) { + byRegime[regime] = (byRegime[regime] ?? 0) + 1; + } + } + + return { + metrics, + byRegime, + byDestinationType, + queue: results + .filter((result) => result.decision !== "approved") + .map((result) => ({ + id: result.id, + tenantName: result.tenantName, + decision: result.decision, + topFinding: result.findings[0]?.code ?? "NONE" + })) + }; +} + +function buildWebhookEvents(results, generatedAt) { + return results.map((result) => { + const payload = { + event: `scibase.residency.${result.decision}`, + generatedAt, + recordId: result.id, + tenantId: result.tenantId, + destinationId: result.destination.id, + decision: result.decision, + digest: result.digest + }; + + return { + ...payload, + signature: `sha256=${stableDigest(payload)}` + }; + }); +} + +function buildExportManifest(results, generatedAt) { + return { + generatedAt, + packageId: `residency-${stableDigestDeep(results).slice(0, 12)}`, + entries: results.map((result) => ({ + recordId: result.id, + workflow: result.workflow, + destination: result.destination.name, + residencyRoute: `${result.sourceRegion}->${result.destination.region}`, + decision: result.decision, + findingCodes: result.findings.map((finding) => finding.code), + evidenceDigest: result.digest + })) + }; +} + +export function evaluateResidency(input) { + const generatedAt = input.generatedAt ?? new Date().toISOString(); + const tenants = new Map(input.tenants.map((tenant) => [tenant.id, tenant])); + const destinations = new Map(input.destinations.map((destination) => [destination.id, destination])); + + const results = input.records.map((record) => { + const tenant = requireRecord(tenants, record.tenantId, "tenant"); + const destination = requireRecord(destinations, record.destinationId, "destination"); + return evaluateTransfer(record, tenant, destination, generatedAt); + }); + + const dashboard = summarizeDashboard(results); + const webhookEvents = buildWebhookEvents(results, generatedAt); + const exportManifest = buildExportManifest(results, generatedAt); + + return { + generatedAt, + results, + dashboard, + webhookEvents, + exportManifest, + auditDigest: stableDigestDeep({ + generatedAt, + results, + dashboard, + exportManifest + }) + }; +} + +export function renderTextReport(report) { + const lines = [ + "SCIBASE data residency guard", + `Generated: ${report.generatedAt}`, + `Transfers: ${report.dashboard.metrics.total}`, + `Approved: ${report.dashboard.metrics.approved}`, + `Review: ${report.dashboard.metrics.review}`, + `Blocked: ${report.dashboard.metrics.blocked}`, + `Cross-border: ${report.dashboard.metrics.crossBorder}`, + `Critical findings: ${report.dashboard.metrics.criticalFindings}`, + "", + "Decision queue:" + ]; + + for (const item of report.dashboard.queue) { + lines.push(`- ${item.id}: ${item.decision} (${item.topFinding})`); + } + + lines.push(""); + lines.push(`Audit digest: ${report.auditDigest}`); + return lines.join("\n"); +} diff --git a/enterprise-data-residency-guard/test/data-residency-guard.test.js b/enterprise-data-residency-guard/test/data-residency-guard.test.js new file mode 100644 index 00000000..8515ac5a --- /dev/null +++ b/enterprise-data-residency-guard/test/data-residency-guard.test.js @@ -0,0 +1,59 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import test from "node:test"; +import { fileURLToPath } from "node:url"; +import { evaluateResidency, renderTextReport } from "../src/data-residency-guard.js"; + +const moduleDir = path.dirname(fileURLToPath(import.meta.url)); +const rootDir = path.resolve(moduleDir, ".."); +const input = JSON.parse( + fs.readFileSync(path.join(rootDir, "data", "sample-residency-input.json"), "utf8") +); + +test("evaluates data residency transfer decisions", () => { + const report = evaluateResidency(input); + const byId = new Map(report.results.map((result) => [result.id, result])); + + assert.equal(report.dashboard.metrics.total, 6); + assert.equal(report.dashboard.metrics.approved, 3); + assert.equal(report.dashboard.metrics.review, 1); + assert.equal(report.dashboard.metrics.blocked, 2); + assert.equal(report.dashboard.metrics.crossBorder, 3); + assert.equal(report.dashboard.metrics.criticalFindings, 5); + assert.equal(report.dashboard.byRegime.GDPR, 3); + assert.equal(report.dashboard.byRegime.HIPAA, 1); + assert.equal(report.dashboard.byRegime.UKRI, 2); + + assert.equal(byId.get("rec-eu-clinical-supplement").decision, "blocked"); + assert.deepEqual( + byId.get("rec-eu-clinical-supplement").findings.map((finding) => finding.code), + ["REGION_OUTSIDE_POLICY", "MISSING_DPA", "NO_ADEQUACY_OR_SCC", "BLOCKED_CLASSIFICATION", "HUMAN_SUBJECT_REVIEW"] + ); + + assert.equal(byId.get("rec-us-patient-dashboard").decision, "review"); + assert.equal(byId.get("rec-uk-grant-report").decision, "approved"); + assert.equal(byId.get("rec-eu-embargoed-preprint").decision, "blocked"); +}); + +test("builds stable webhook signatures and export manifest evidence", () => { + const first = evaluateResidency(input); + const second = evaluateResidency(input); + + assert.equal(first.auditDigest, second.auditDigest); + assert.equal(first.webhookEvents.length, input.records.length); + assert.ok(first.webhookEvents.every((event) => event.signature.startsWith("sha256="))); + assert.match(first.exportManifest.packageId, /^residency-[a-f0-9]{12}$/); + assert.equal(first.exportManifest.entries[2].decision, "blocked"); + assert.ok(first.exportManifest.entries[2].findingCodes.includes("BLOCKED_CLASSIFICATION")); +}); + +test("renders a reviewer-friendly text report", () => { + const report = evaluateResidency(input); + const output = renderTextReport(report); + + assert.match(output, /SCIBASE data residency guard/); + assert.match(output, /Transfers: 6/); + assert.match(output, /Blocked: 2/); + assert.match(output, /rec-eu-embargoed-preprint: blocked/); +});