From 8adf9f7577cb0237f91fe4cfad741f1b4d38f810 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristoffer=20S=C3=B6dersten?= <60147923+kristoffersodersten@users.noreply.github.com> Date: Fri, 21 Aug 2026 18:30:36 +0200 Subject: [PATCH 1/4] feat(release): add fail-closed store package Add SOD-836 listing, privacy, asset validation, and deterministic package tooling. Store packaging remains blocked until SOD-837 configures production entitlement signatures.\n\nVerification: pnpm validate:store; node --test tests/panel/store-package.test.mjs Refs: SOD-836, SOD-837 --- .github/workflows/ci.yml | 3 + .gitignore | 1 + README.md | 11 ++ assets/marketing/README.md | 6 + assets/marketing/selectpilot-marquee.png | Bin 0 -> 84315 bytes assets/marketing/selectpilot-marquee.svg | 39 +++++++ assets/marketing/selectpilot-small-promo.png | Bin 0 -> 26747 bytes assets/marketing/selectpilot-small-promo.svg | 28 +++++ docs/CHROME_WEB_STORE_RELEASE_CHECKLIST.md | 67 +++++++++++ docs/CHROME_WEB_STORE_SUBMISSION.md | 109 ++++++++++++++++++ docs/PRIVACY_POLICY.md | 68 ++++++++++++ package.json | 2 + scripts/package-chrome-store.mjs | 111 +++++++++++++++++++ scripts/validate-store-assets.mjs | 45 ++++++++ tests/panel/store-package.test.mjs | 35 ++++++ 15 files changed, 525 insertions(+) create mode 100644 assets/marketing/selectpilot-marquee.png create mode 100644 assets/marketing/selectpilot-marquee.svg create mode 100644 assets/marketing/selectpilot-small-promo.png create mode 100644 assets/marketing/selectpilot-small-promo.svg create mode 100644 docs/CHROME_WEB_STORE_RELEASE_CHECKLIST.md create mode 100644 docs/CHROME_WEB_STORE_SUBMISSION.md create mode 100644 docs/PRIVACY_POLICY.md create mode 100644 scripts/package-chrome-store.mjs create mode 100644 scripts/validate-store-assets.mjs create mode 100644 tests/panel/store-package.test.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c4226ab..e01de27 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,6 +64,9 @@ jobs: - name: Build run: pnpm build + - name: Validate Chrome Web Store assets + run: pnpm validate:store + - name: Smoke direct server entrypoint run: python3 server/nano_server.py --help diff --git a/.gitignore b/.gitignore index 91f43eb..2da88e8 100644 --- a/.gitignore +++ b/.gitignore @@ -6,5 +6,6 @@ npm-debug.log* test-results/ playwright-report/ reports/code_admission_evidence.json +dist/ /usr/local/var/log/chromeai/ /usr/local/var/run/chromeai/ diff --git a/README.md b/README.md index 4e2e8de..78451ea 100644 --- a/README.md +++ b/README.md @@ -202,6 +202,17 @@ Current CI enforces: - typecheck (`pnpm typecheck`) - build (`pnpm build`) - baseline tests (`pnpm test`) +- Chrome Web Store asset dimensions (`pnpm validate:store`) + +## Chrome Web Store Release + +Store listing copy, privacy language, and the release checklist live in: + +- [`docs/CHROME_WEB_STORE_SUBMISSION.md`](docs/CHROME_WEB_STORE_SUBMISSION.md) +- [`docs/CHROME_WEB_STORE_RELEASE_CHECKLIST.md`](docs/CHROME_WEB_STORE_RELEASE_CHECKLIST.md) +- [`docs/PRIVACY_POLICY.md`](docs/PRIVACY_POLICY.md) + +`pnpm package:store` builds and audits the upload ZIP. It intentionally fails closed until production entitlement signature verification is configured; no unsigned commercial package is release-eligible. ## Compute Distribution diff --git a/assets/marketing/README.md b/assets/marketing/README.md index d62df50..3eb058e 100644 --- a/assets/marketing/README.md +++ b/assets/marketing/README.md @@ -6,6 +6,8 @@ Primary launch visuals: - `selectpilot-screenshot-extract.svg` - `selectpilot-screenshot-runtime.svg` - `selectpilot-screenshot-privacy.svg` +- `selectpilot-small-promo.svg` / `.png` (`440x280`) +- `selectpilot-marquee.svg` / `.png` (`1400x560`) Recommended use: @@ -13,6 +15,10 @@ Recommended use: - `screenshot-extract`: primary product screenshot - `screenshot-runtime`: onboarding/runtime proof screenshot - `screenshot-privacy`: privacy boundary screenshot +- `small-promo`: Chrome Web Store small promotional tile +- `marquee`: Chrome Web Store marquee promotional tile + +Run `pnpm validate:store` to verify every required raster dimension. Store graphics are listing assets and are not included in the extension upload ZIP. These visuals follow the same MUE direction as the product: diff --git a/assets/marketing/selectpilot-marquee.png b/assets/marketing/selectpilot-marquee.png new file mode 100644 index 0000000000000000000000000000000000000000..7df04c40498ef755d8299298aafdb4fc902328aa GIT binary patch literal 84315 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 + + + + + + + + + + + SelectPilot + Deterministic local transformation layer for selected text + + Turn selected text into + structured local output. + Action briefs, rewrites, summaries, and extraction artifacts + through local Ollama, not cloud inference on the core path. + + + Local-first + + + + + ENVIRONMENT TRUTH + + SELECTION + + Turn the selected text into + a practical plan. + + STRUCTURED RESULT + + { + "summary": "Create an action-ready brief", + "actions": ["Ship MVP", "Record demo"] + + diff --git a/assets/marketing/selectpilot-small-promo.png b/assets/marketing/selectpilot-small-promo.png new file mode 100644 index 0000000000000000000000000000000000000000..0562eb6cea889c651cb1aa2aaed47c9c68553f6b GIT binary patch literal 26747 zcmcF~V{jzj_xC2**tW3~Yh&BCZQI(|_68GdY89)PX}TL7Mq6b zqklO*7RP79!^5nnf>_wZw26BTy1u@;dj?^`K*f3_Jv~A_Lc&`O1q}&`nPCGe!V^oX z6vZK&x)ezY5uhSq4KYQTVrE{Gd7dt%4o%B6CgY4eRCqK_kUCZj94N>r=Tj^M7*y6W zZPb*eOcDl~x@fljM1grwp-nGCla8cALW}G{2o(mt)59D%Q%i>GmSc~VYD$^Lgn$N5 z=|QzDPL#~|##a-LRNI|3=A^`h3l)Cx5&Av6pjcX%Z2bF&7%*jUFjQ0$Ks7hM^%et)#^}%$&*Fpbi~n`aW~vCh>Mdel{&CnBP%YJC=9fe<}J(+Bo*%)EAA%3^7GT9 zLXSvaPlXkxp89RYXj%!R3Bw#+#DRH;6$RH#>aMQt&aD%~ng(|0?qNnRncND~8d|YZ2rSk3UilC)6 z1rr*qn_C&3G9V=ej2j)7HZCSjlmezQ21NRaD?DmSP^&N%gu?$$EN8z&V37}8iqfVpWC3n;Fn(LHjOz0m&FXHPvXUvC#$ z@nK1lty)Ky^$g{b|3-8UdG&6qk|UK)_;cbqY-lL=r{$bJtu{DAam37wW@1JX9kdWE zV-)kDm4v~-F=^9SP6L|&nXZ51Orp#e*M>GmMg;$Y9vQXtO{t!lLzI+8JV~C|q&r&el=DEGpe68Jh}&E2_cdePX>B)&Z6SXUK5%D=!mQd)JiQ{qMd zs^yP1f-*(Wg0KxrjKHMjL}0`JQ5302m642l-^T@dX;b$0cJ6`NWJ`>ik@ij zCI%ek8wuJ7>$W9DN|vml&=h&T5dml$_>p+I?XjJC+mNLk$a=S{0;n;hn}a0N%)Q#! z>H&wsWnaTT;DpojCMhvWjG&x3Rh>%iUENL+sNfEXLtKCDfBjY*n=bm2ppyoec$&(3 zn%Z(3EUNxhq+nu2&BVbynf0~680!%dUe+OJmAR90XXPm_&?JLLh&(`pGNVh-qw5Yr zr4lE@hc|h}q6)oTE~F(czB{HHn}9MZ3)~s09l@y=301j+FjofT~odIU$QclKxW%v@DdOmSD|H8&}2!QX=^8XXYkgXwr61DV;U5hX$m9 zgF-;1XB@1cEk9X%9T$_Vi*Qh^5a|eE;iMZA;7m%PmqoOrk7Z@NHYSau`>RZ>JV45h79K@zy%W+xZ@m!s?8j9>y|dU3b{3_-dj)LI~eK(c*7L4knZ z=i`2fg`lA!P#AJ7h0ff}thb0TQ}*%n&LiwW>;!}Qx51M_tl`7s4LmHYN@-&)(|GE< zJW!b?m59^#b}>8N&&Q2MuO7#``^zyt(g=Nri}Qe;eJn6r3TR5o?N6F$wfTcHMy*l! zQC$`a6|ASo0MK7vlv~)PrjEEO^!qqm10~a7V2eTTb1Wlcr3*T4+Mtt)j3vt8>LkJ9 zD8OXB*?z1DlGxx2?eIMLcDdz=j(##zpl`&96^Vd9wR3A=rBtXZEBZ+85M%zZZ;>Bj zI6r|$BaUOb8mQQoHoxp3pu}10qjyL3fD0!E|9nde6F@)DA8o&+lj7v$#1B@v`fEmR z>-Q0LS&=5)%k`;^rDeuk6D0gWze38}$>D{JfAJ_Y2M4WsQNChnkpXgmWiA1hK<6)1 zVu|E~q^i|_(oP~0Umin)Y%k@+m$@I|F<wZC@v=br3pZX$(^%cKY>WGfZ8p$qcRfd29}laK+LX>rHNuB0x(pEyffu zQl$em;)oQ*{7$|%7PAe7&hM7Vf7jG%qxX zirZUL6Z0IZp!|=47i)=@?CyTWGgd}%&>>aWP8p5ufBSDSRAxCMsLk_bhzjvMjaDaV z7RlITaKlCHkFB{1Z@4xWn-D1--FxVIHa6pSJ^>1fYL8V3HnQFw*Gh}^tmR5RKgtH_ z6kp~&DjJ$Jo(TyWGboBA3<^h%z&gp1E&W#3z#bZpsGDCTg4@Hvz#vPMR;!?hh>61E zHI9mmtW#-#8ePu9iJXu_6z(_KVHZn~fN(f1rfrb~`a1PUw`m= z`$YFq!mhTCS1d2qPK9GFi96!YJBhL?tQ&XM!fuCcOIOX;Aeb_L{Z0bP;ipsAJZ03pxK%FQ} zzXTQiU)4^ZFIYGp58})1j_5?Cq~lh*ia(mq_~ifYwks)5s$AGmVCZBt>|MF)I?NxW zsdEl@_m4rlSLiFfBFN%#?l8M52hU-%HpQjSxFJa7hcV_$#D$ZLQ#<*vp1Xq%q{l&9 zw8jf`o9_erC%c1oypb}7v|_ob5?M-aZslI#yx$SQxg9{OQ&L*^c6WOR1Xn~!V%+YHfuE+ z*e80K%7b)tDL5tgEtk8hXklujXkw&*1Ct6Qzes4N7yVjFo+q5X&;?Yb7f_VAM|w50 zXt6#0^`+|Ie)kx>1tFft-(DzNw#Q`BwyxGFDFl%?GbcRBM8)VJpzNr)#9NGbx!}jt zL&;pX7oXQXhJ3i*=r(J1E`&-4}`QLT5Mq_O4@fe9u4 zzWgom8?z%}h$~qkr4{BFl3kAc@9q&X-n6Q<#u!AFI=qESr>@-1HF9?0=KIH+>>f{g zYK0t5--TK*vP6`=+2Uixy|ImYKY?7tQH46=Np>>s)(O^O_q{;nGS}^1^!<_vk;6wl z=;1hm4BC>-dNmzRJMI8$hlg^^+SRg#^qm5+$%~HonNxm$$MKe{%T()Tl59QBCXI`0 za^vJA=hLLYOx3?l-;sk&pj%X{baNA-TYVn!#V+T7lLjQ ziY~`nM39ldF@&LZUvIcA^qk)-jQTJVhi^VR8C(l;z52WN3 zH%JvKgkb5U$sH`)*`K{!K0I|`ijAps4S-QyepzEOc=`dsQevuUhsZyF+$P8=kkd#+cV8zvTCVv0 z-wz<$cQ@7$b=5e*GjOl0tQv0+t2F#|!Y*zNazaM#aW1&Oy#&fU8jNUgj>KWvsuead|n2 zgoT7@Wn_Z_{9~e-Ftyx-XmwMApmWWkWHC#Z>h#@^_SmgGTa4o83d!3BmjuFTHc!ds zU(s>YS>N%2Yx!b0Gwq*hpSgL(#gJtC?|3dQD%COkO^W#k^o4taQJR_*k@A|F-*M6M zaV_wjLg5cQCdPZX7fUT+ztm`(9KI_7w8YY2Dbt_D-D;X0f$#6}VsO}o zb3$2hLx`g(#gumIUC;_T9{+|%Hsgpl>~DslzVxPf5%Hl)c&s#QZA#k5Z;;yf2F1U;JpAKuS424J`zzDrc+;=G2 z#)Fq=jiSJBEevuR60_=!A9+--F?QPQq=(H_*)*;j^^ca)Wy)m)xcsqO_XA&8A)ykE z6~e;8=~Zj8V-f9tYs=M(!CxXYhe08zm0quAqkc>eP9~AXEooX8S;%T3pPib~peq!8 zU_}$@!f^P1T!7OZd)G*u4>HlPx$cbl>6jcR4|Ru+XKc>Tr9oS)APjz==9*u@$O2aOMMaMU^jx** zJRY`PF4IOS$b@{c#lCIIW-{rHJ|A%iIAjUAbnT4~XR1A3Z(w6?_#UDH+<79o5(@gx z!4BT8b~OuoL>yyWq}3ODcmxD1`^UY<3Yng%SuP)zdZG{_K`Z#Xif; zTl}BH@~sJWI~EkuIsFWy!CV%-<)8pn$ZxWl)zegDbw9w>`#xE&&LH44VOU%em>`$U>)e~< z7E_;a-<@MKT0xgeo0h|2(-*^K`yIAK@cU`YqWo*#;0Z z*$L2SIvTZIjZw1i+GYz1@}rS~_SY)F}c(278!U+%7kX`wX98L7IwxC8*As z*qFE4%wFn4=kxiC@Tb56_Ac!z3>TdVWhKIStB;Wo@MeO0D904n(Zie3NcTNrx!#=L zPkK9=s}F`K&?+VA2#n-9;Xl6Y3wt7SX^v*Q$T3CB`X;Q?vWGNs_ z>oYRM`dbAAVberHL5!2)g5dw_aGf4gsG6Y=Bwr&4RAfo$87Qz_sC^ur$9^FOB~nbu zqPbR1aV+S){mh>e{=!PpNQH$J6@?|^-Ekx%N90CdXh7Yc9&p{TpR_Rup-JEIJN19^lZ;M&e zbXeQ4TF>89LtoB^cjy4wvjW1Bks)$6bZBSUc#+C^+ZjnjhPA`eMM}(j5v87qg4+`V zfuriC5LQ>Lj<%7vh_(5Di?U<*@F?+7i7@vGQT`Ny_zHVdBWp_lnqum^$&UJdm`)a+ zb~ZjU_l^E15ClbKABEOK=av}*W#(dq6%}EUkysurQ~rS?NPX>Gb87j@+o+(6aGU60 znHU4cjLgWAN8f|VGi-&^V2kbdLVh~Y93y|-zu+6N!~38k*DnvWjGknDwrg1w1^U%hM+hGHXYwQjtSeO`eG;MT^qFgODpJ z^^AhurRNwigaKT4)=n+JS1!gq*$gc|-SUrAlLi1C?~eri*gsg=)chGq{OE}+#<(QX z7457Yn>ptWmhli}34RiYP@KAuS!v_Je;m-MvNp;Nm;(b*d|e-uW6HhxbHj|1SS7{r zh?oOWlwd-@vTglAxe>vgbOe~lf%pWVCEs%v_#FTCejWvFI5aK{BxtfnESW#$Eb?*8 zU@VnQQ*a<5l-A4(5Ree_>K$#;mmcsJ-tB0vY9mS~-==&&dd*vh&EiTML0__^6Cx7wu;?LjcJwk*2&^!&Ukha+k{zvE)(CEY>5L6b?`3csMhWN2zD zUy;kna#=X;apk5=M;MFMggn?^PdkMnEhY2zbDqN|t__R!fN7}>T_aL2x95}7VecD0 zEqmMLjY~`QrcblgEN6;HKe-1{_w{Zx4|(3YuW8TYN;(gcM+o?fvjWkQ+=CnrhlL2J z$frJ@GUBUL(9yAhcXt2jDOt00D1%95Gi~7G_;~x#EN8>6u#vv*_j;o5<AWScY@3gZ*_c|YG+LYmxA!&YN5JQ8D8o+rKkvI6+-0AoG;_7_^uOiv>6gNkN(?Hz zUrj!YB;UC%=qgfZ<=uU+V{(FUj9(x7pI9j2s0bMJ)xO8bIWCnG!D(Fa2D3LgOm&xt zxkoUXfs~ z0YBJNUY8u2bn40ZDO;r)YN*Cl52?EN7(sh`I~#4fc<;grO4_o9M-5u&s^~gu;%x1M zwFbSdU7@CU}2*@UoL6KUjCue-3&8s?>pSRs*z}V5O*Cp#LIhU z?m^D%hU(s)SEMjPK_9NHp3lxAA;8(FcA%9c?Hp!L$Ln|Y?-An^yi%d;?d{JnG3I?M zC9RZ^r&J0f*iP2z;JEAwVH+c^h5P}lt>3MRx>0doox!;PkJXnFlX@Ec`6 zbeJpt57GXk$~AN%+{I!!WiqLE|C!Y8haE~$+^sqBrT~}E?ytSL*LN=$6zsELi>vG3 zg@t1c)Si92=h5@s3FcVvlzUvJIF6Q{veGp_A5LeT_Ol?FzNrzH+nxV8DJkTLSdctFNaACg;8cD3kwakQaK+Q62dT|nIWBBKkj?TxS)H!k4(QUzSV3Do$PM; zXY7P^e5J4zrrGm0K5^PmZTsDZNpaLB9xLDp8q0+e+$Bh_RGn;wONdTtcf$j57XG3g zT$x#t5Nx#&vUfaOQ)3g}D zWkQP`&3kIuIDfE7fk%qv^gZH!he{p8*sp2-(eqDPsx>2qZw*5R5Nl3 zv0~y?in($kCSMNlehWYD;83QT-R@zvUY9ff7H`nk*Oy9EWSI!=-VD1X>q(;mcT`)_ zP!G^$3Xp!$HW^fA{0d2AIK>S(-aSPAA|d};lEIubC=%2MfKyNz|S zN?>obNzkH`Lm)FQ?IL9VEKV+Oh5eqW7}4NivyOrcHQ=SVqfUAR(Sezcj7_tR3x0>i zZoY~J3H@No8Y~h@UPwtsniI#3Z$dH?ADN01)ku3~we*$TCt$-vi{6tDbKlGBD@<@v zm8$(cp}D>|y5u$^BG8733#9;>sGGFtM+6ltWR}m5n`A0?+?9G$-W1EZTKkBDOMdT2 zRrVT#Ci@5HqKyWT{fre&&u?{q9V-k9eWRf&6$`Kfth7BH~C>E zfEh;kw}s~9LI*H;+9J#@MUh_Q2!e3dtxSN(#fk{j(W)ioFBnI(QGb4%uMnENeUIJt zeU5tzTuRbWtu`gS3S>$QgfnA@`h}9lsVM6+Qs21+_x-yBRHzv9?GxnPQRDNSIGC-} z$WJf);jsy4{4W@doSay*+oxe8-hy=)mFQ9X{C=dGq);vEaQBK<*i%um#IO(v zop}wXdafYSL@aahOy+jl_2Y+TP{pZ3{53|Z1|pdMT}*tL?x0=H8u$YT(i)lACMZ(B zpZNS~giA#9&nL$R_C={iMP(<*@9p6d#tv|hv2e>0b?U4nSMXW>L6<9l zi2kf7VXL|mdCK!OEDINZMJwC%F$81AZ2Pa{9tEXcpdw4kKUf4Ey;%L5cBd~&V@pNf zI?Z1=-s^wZvd3~tTr@7seklJm%v-&di6P$LqEhelUWY|?Ffbt($(RFeL)@9kNLwwS zs?jkKJ?`TsF#P(fI+qXp6Vab<#cyJ9Ss(%-e%NJpXwG;n!XDw!!*ROlLQntV_DHY@ zB*5u^8adr&me&21470K_C7D>!AOH^Dj;5IeqeG|dcZ}NE*qi3-F_=nc1>z>c(NaD0$zbf6BKdwEz1*cE|I!B4cyI5U`~BF{+grxU!vmyx*B(c= zde1N2B(<9_}O0C*gB_eU48z@hIdg7DSj7YGdB< z9P~}&0KdYgx6>Ka^Sy#;$~2^;X?h9cmg{*Pf}1?O{>KHZ;Sh^@@RT%_rDmcYB;o7A zTB#_u+(XbVt|*$C82!%u#&K9Mpez1?r{?YmlyFcTa3-U> zM`)Kh-zM7TN&o%&nn9TaSwriOCYb;7`mn&rI!CEPi8+XXAD-Imbov@>$=ecxKUxeQ zAK^%_4gQ8iPP{U$_=(l{Aq7)7J)k(!l?b(6{EqlT?m!u2K_Ho#U#x%G0IWf@FXqt6 zrQ2~kKi=}86&S5(EI=4?u~gDqsTS)p6ZhoXK~JaB?P-?T&MIv4UT?Tf^mmJ@a`t?@ zOxt!k+<7Q`AK+3z^7xKV8x5+cm{M#d+0R)7j}T6Luv26U14S-7+b(juUl*#q z2x;Nn?M1f(cTA4$nQu`aP{!s4;Z-XX_jd0*90WcD<(8KaM=0F$_n^~&z-?#&)c?oB1=`s2v!}q2e}M!Unr# zA!P>bR>S!rw@g_QHm%@KSs4sY&jVFKLnd;K>rcOSmettdX#<0TY zk2MRBre<7Mw7ug*T}ApOy5!7K433+<%z&JYrp;;nbNPxtg-*Z7cGOrDu7l-oB$!o4dg>@z+MV*f<9hhV&oO)KP!0x5Y|zFq*tIzrFp` z#bS;!=W+!i#?*3YcLi@ETNi%^B}vPSHSr|G>+l}R%lM8G4*&XAjdG8t>wCYC%Ld`% zL||`3(B|Z%Y=*!u(Ii(FI7~;JnTFssJyRzq2JC8vOdhj={H-P%b<)(6-)3E6F9&7# z8e8@LP9=}9r{{3kWAVje4PlrS9PtsRh0Gc?+d?QJ6v|0$um+vg)zz70VFuqmn;^qO zw6!xzSQgL;2xu^1V`7y*T%@0*M1+MQ`|fj`d2sPQ^j7_{1l zIv&sV*=&Z`4)Dfe9Mgun67Ua2O&2i8W!&E2xZr)?d+_t!i%(UJJ zqP=FdbD5FkWcN^n(>an1Qit7)2l2d_a3Y&WLZ>8*n(g)?oDo8N5@)H$=BnDNfw}i% zm{FU<;rjOBt%aDse}^npLcxIB?K7*!K(L4C!rRS0Hp_kr{f3!_`)HPxgO9JPkY_bl zt_O9#zpq~@GX4+Mkm?SiB9FFQs%4a=X%P`P;W)dT;=VB4>FRu^Zgx zH?7xsfMhqoJpekDpWTwfpg zjGFKB$!g&bMoGl>$KY2Fhl>S@Hq)K9_OF@IyYy0N{e#!9OA^z1?fGk6;*@5#SW>)1kOKhBul@04DXwF5^DXW=Ee~%ZgZmH2DX_upalu#r^w&w*e)-Q! z|L}Aj<*{%t*g3~Vi17N-q&Tw1yxcsL{g$8e(OwffJ>E`acGks>nOd2mHULAlN;B-f zayVK*-XYhM(MNL*OU%Fc@cN(vvy|FV^uh++) zH}R~ngy*m!X%_pQ7M3sy18(m5+%CsH>19T6;jtZS>*^g{N6gHv$JqMU#=u!}`lrMn zAosSvY1`OYsaHBGM{QQL5FwqcZcPlFJboOD0(RX)DC{)Z`vocf@!47*-WEq=PQ^p% zw)(zJhELBh+HdQfAMZN^vwv`+WA=nX(y}|~viA?IIUg0cIg;;1{^D_eS=$_J_@R}B z|3tuBCU*zr<$Nqzme|wj_hhSmzdQd_H;=XLx*D8!kVr`Tdw4=Lx!uE+MyDL7{RIlD zK;>;|H<8ulhTDEP@3KceucO}c(iG+};--Nng%;Al;`mYiLYu>G-E1aKP6l|j0ny;S z7zGN~OX_L5FVAHz$wP=LD~5d;q?#BsCdS6c^x6$p>-zqb{=e5f2nXt&CVy4uEb!9j z@w%LISpL>1fJ4#m^ksEj2*_@^;BhnU2o2@Zh@TXX&4blb=iJ;19#+V0s|{Fg2a)^Y z5)vI<{xyzR%IFvvmAz6n+lkkaJsm^>EM$&oAEApSG$*qHiO2 zC(qQzru3R;1xQZT6%zm{dlZ2T2LzS7Rz7&q$GdIe6vhi&qj7{ zUV0(Ct6poh82D@7de9M{yX$?oc02&Z5YCR~qpP5y@p%pMExasvJ>O&c<_|{I3UB-o z6L~Bn;poC&stF2@Evti>+GE`IFGrEKjn5t;DsuevAns9@c4b(u(rVKZQQK_|k2lE3 z&fW@mMlhq5fq&Pj(y5@yP4ssDL8Y_PJ4JNVmWXYj%E~$)MnjFP}+u#-^$R? zariR$g=Uo5>y>N@ZxKOP>50D|Gz!EIyT$J5C*n&$f1uq0$u2QLS&rJa&oM$X_*)6~i!USZ=s_3$LfG+Dt5qGX0xzCsT*viyW@A8o(w@Usl zo^Iq+^q#MKu_Cz}qv6A;3|c9)g~MwFPMbPUdHJ+w3ynmDNKHiI#k`*X%&m-?9S@g8 zelX;Zf8C5DKPsY?b^8m4d`Vbc&Your2_j+`X78N82FpN=6Mo zE|_s{V`5@$4Nay;KWFDqrOR&#-Yk!Z@TL0>WJK{eXWgi?Y&v^bi^=B2_7ndK6b#kl z4|wUepJz~RuB+qr^3(}DrrpkCT&m6(w`29#rO_Ju!aHKrA5VRIJbQnj$LIHU{{lx4 zQHEXR_PJ!CCgyj?5+`o?c#M@nwfBB|e+8*eZDW(`Psc7JcW0xi701X`@YkzCA}quCGfi=JM`%ca(OPP6Z&;RL2{9tG?e`TyLt-FrFKE8E>g$ zv>oNR5=f`XJ#a)Xy?aZi*ZY^Aj{K=!qrA1*FY||mYREEJv7o{0=AS?QOOFyiv4G!Z z*H!6V?WoxJ0wKToE3$`z{bJapPwVa0Ma8abzl$sh%Kg73|E0#dBBG<|hwP`+T-3`* z5ebE4mf!Xf4LaKOgj?@%NF6QJ2ikpB%#|AU+sP8m{UgHJHz9dEF1)H?3*<8xXlPLV zGzz&C>thQ8KOJ&)HFqeb=B11lD)n1pd8R(~Y{C)GRO&qwwb^KhH@c9@hfBYEh=|GH zHxnV0j&D z;?eU)q+|mL5aJ;r0pDG1TYkjNKe_}&1`xkQQ;G_pxV5?OG6n3X+dDk{Q-HBVz~8`+ zf;iTztACJ`3vZTRXQygioOhiI{JZm?$1?sJbIXx$Xd7o)evcw%jKf9Q8dex?1G6_I zg!A@^d*8Y3E8(Wd_hy3smLgCj-|9Tqw;_wk<0u4GD#^aEIuVN1(K%Dfkh=)ZN?6^`g-voHHq5zQ4FJ6Crlwy zgz8YcsG~cM)9A26es~3WfOEwP_7VH1v!5dhDq%(cNuR`M5qK#uu3Fw2O7a(Dk0wWZ zaVF`qtskFXkf2|wxQ@Qjz?JnzsgP#AO+;R|qmK{BEYxi&ECr36_kP_B--)K%f~sE9 z|aP%?fd~ zP{c7R?q#-pALHW)BR;6N+M>qdj$uZ!lxMI?E&dYu!tF5NZXL6!p~BHsf%jp(W`=8^ zf$c4E)ez0-2%=!qy65{X?oW5Djg&GaDJpb=VQRg6eHz^~-^hi)g%MrpqEDaMc3iTN z>rws9)oqGB*y|J^vZOlj$!~w@1MX1e%{k5>@b;v(qXRpc8-@Jw`Eb&2B6B`$Hsxmy zh7s}Nk5KB^@b87FM7d>lQs1sfmC8kDrwFwP`F8sjV~F^7t}=`c7)d-BIvSwaizHY8 z<+Eoz@Lz;vZu@RWg+(1!RQrmDWk4&oQMF>0s1tA2R5$ zI@jk!nZr0NGDY+5LAC&za%$0~oLG8X@%&2ZaI+7t_PDj9QKRdJrF=&Pnbd%4EF*sm zKYd}KS*^Yc8C7p|NQAdHm6xq((#XN(^!R)!8C4xKIxN$2NVRDtzdp9Pu;BVBw`e$E zE|D=5X*C8Va45|Aen&iC&f@jgw0_LuwF~N#BIfg|yPApQ=i_F^7~7V|f-la=K|GGo z?Xve7cBd!w&r=trO3vv>Fda#u))W40;Y5{wR~PVo*RHgjd)t9AoYZU0>fBrFwDJqh zfCWkdi#QOGi1@4Wwce0&J)b#^QueYA8(a<>L%&ii2M#M*K<;Ywkf(yan6~lh*48BF zi>#=rfssuEH$D&MU7f(%%N3W582UVBAFX-u2j`<&Ie+mDW>-kd?ccW^34=%NZJAb8c&Spj z0#)MjdLWzwjvB%bVtM$yj+Q_^;fm!e$Mxj&?@$j{y^Ea4*)R<$DFI$D_ijW!JJ)5P zoMIwnWe6u4j=$vc@1PBooD)&gjq45uPVWkTOpt+GN1sW1H^t)|ZCt_g-g6*(kIS&tX5BU@X@Q)@Fs9R4Ex%mW%BhOe zc6QK*sRp(W9SA0c34SL^2m?oQ+L5W~dy{1N0caZB*6#@QU48;wO}n-R%NLT#kp4N*4bwL2RkJ~&n9u*N{K>~m1PvQfZnX1m_h?R{$Ha#0Q;nU zqrwtGe)lj{qK&R@tz_?n1uQ!s7kYt43kZ(zb&ys4I)4vIxkP<2?&9}zL0(x=fG3I= zzU2IM{!btiFW|wn3j!y~{ppojA{SnP8sg3EST)Hg;P*Kf1Y)P(S7c4ne|Gd5jX@k3 z(U4ZB&=}uK0?1zN_8kHHlcwV0mqqyS7m+OyNeX-3AItFX_YFk-IfazvJb9~1KH`bx7wWv$ogtP zodE()mgm25_koe`-ULQlm9Q-g#4XM888#yE9HnvwjH6~tz$$cEd3$vZB*{g@Ws?~* zf6Den4!!tT+|$bIHIWzRVtK;NY9@1^=Y($R$@Jtu$&crogNFl`Nu7yKp1=PB^f@h* zX`p|VT1u6niGg@K7GFn$P};uJM_Yp~mpkW;me$r*W^@ae3ap9TiPT-Glg%!2>cX#X zuA8p6Gd0>u12%=zCrO~GLF3^h0l0srfWJ2x2|6yW*+e%)I!|~`Y;HUA({^_UG!!KK zckpoDwyH|HjGt&&L^mJd;hJ>K&Gi%6KhgycuQeh?f5q0pgOj}69jYa+!1D_!C^*=b z)s$%D3-WuKfbfT>D8;}3Fb;Gq)r`g_T8J*c-%A{R94*bxNdjrv*pBDJ?ISD#LS%IO zxJjPOzORH;sC(BJQe*9Tm7cH^uqgZ=LyB@{Wyjrs|K_@t$WShbTfE>N=qA=J(;bz! zTaXRHLr0Gua_IsQFWXqu2P2bHybxrqYNr~<~R-0qIY`%O{ik81b>4j-csJD!NRl9&8~ zMfzv=aXBg#=Aqcs2o%W@r}XQAV(Fx@YlwffS@iCQawL3xQ#-LsoTh%$#J20UI%lUi zJu0bS@%w+an`{?p1bGl~wpq{X*Hdb>BWFDyX7gzIyjPz8F3zsr^l$aln%n4Fq)8{{ zt_`nC6cQAyU4v2t7AejJ{c))m6Bjd3WA9MAskXCT~uy*RE}f66Q8H1u{p((Ue*e}U^*Je))P+3kC}2Nu+us}I}IvDNw3 z%FZ*-H2%_`UAs`JY%~!NEg3uaF5q|xqHS^U{*>d(4@0N`aEBgv;vWf}UQTZ+RMyRd z-(1PAJg5|?8(c4@@I6+!2u3kXqb=fSb(yx$Cf_c3J>ekiUPDOLXeeYKK-Qj?k73>F zO>fN#?aIo~G=w%PdVnuT24+ z<|Z`USv22eJN3Pq2%OuDA)CfRvZp%sysn1u$8_fY<57PvI<{h~+m%#*`yy4`N&4K1 z1>m0oGUG-a-lo|>-Fh)Tvhff^!GL&?{wN!rsO(FiUB^{nTC(te%ntn zOTJ|LQZXr@rSR=j zs^4x&Lqi>RyA38*DFw|i!l|3_bk-at9682Nj@irW<|0j6m$!9zuwV6}&Wcy=0NUwb zg@^Xf^5BN=(BWHtBXLY`ySr}h{|-gg)33Jc5BAfEP4`&B(pkg59#v^vjsTP|VE7Bw zXWk!|D^}6dZCmyAKac0wJH4K~#>Z9*p&oQw+yv_O-K+BA;bUSkUCi|=HNhi8T6zz6 z!ZJ=Yb$>;vv{zSF43yDlnRLGI z;N4&Mma|1j?7@jJCHQ5G`bCD5<^KfNxY=m@USA8v_SXi^TTCAwty9Ni3D>f-Tj}_? zTvC$W14H3BcPvk^`>nTX_jk%6RU*#wT{_H}K4u7Wc`YdEglJ`4ilGOFipDp()%Lq2 zrjY(EDxI3wir_C-Fng?uyS5xF;D1b95Ykkr(y>}SMn#O2@*wB6>{0&BXtN~b#jj9za;tF$J;v$ z?USZyas5iT)?BH3x~Jj5C%D1OO(p)22%?}0F}~X&Ad2d(v2EA6+s{KaO&N5Hok>{` zEJy3R@Fo`ss4_ZPvJ$bDZ~kd>sSBPsTdM4f#U4mJUo3Zi+HDw~)>k{4>|SZ|i+1XN z%@wF`6Ek6J`=*SHDGf(yBAAEGu6pb~8I9rmp7flnDNSugV&@MgR2#2Kkl<80Ni#jC+=z7zahIJIXju@zm#c(1?)0Vcm?TX*mO|-$5|Fl6@cvT1zmH&lHNt~zzntzw{p)?YHDJSKxI0B5@wd1wMJIQP! za>MlA-1_Na>yUsmm!pA!p}wHNm6j+@x4de*KlGaGMG!oZU@7uqNh{s`U-02k<1R<} zIl~?`<_Gp6zY+dgG8saJHebsi*iOr06(d?Ut1P39J5=y{5jl3 z143g@16{_pgV_-{haILz6q$PkLG!zJ&~K;eLt`W#HMqAxK_3wrxj1a4Y$R1oWq|~K zx&BoDl5dFNakW$Ay`M0{bYf*)uYT?I{?3LlhbU7b!XN8Z6Y`M>cC1mo-396m>YuWm zX%$`#$$fKa;Q%#5t<}2+^~Eg|6q({M`4+LTU*&+LMA8~zbBUx1iezW|w1rkW(!w&? zizM$HX}Kwal?K`)1!LuK^EP%TD=6W|GV|Ys~$*t-(?+Iy0KshdDI$qf5VSASF`$GAW^JKRpe{(%OGn_Y2)PBxXH`5%3C{E4ZcRb}P?weY zw1LzZNanQa0kydX&@M|(-sH@F)-|9_hONQQMJ6j7t4K%quuCW1P7we5U z+@;u~-XPQWcGVLTiwWi~2nc|B%|wir8l6C$0mLlMXTu7%ajb6}ZI4&l<2_s9@p%e4 ziA+{wjiZHlY^7DNXd}4&edIgeyq-^R({sQ*tQ5a4t8%h%sNnZMpHJ&Ry@wr7#L2oD z^w}J?+s$I@>eheg4j?l1d%Tu6-xT!QyC|(FoR7z5J0zfeso%BycM+sME(Uvw&lW50 zPiHehHt#Gttg(`51L}i3C!dd8qkv-Ah7!-W=h$4P=)8!;#B=HpsPTL1!jwPY$C_RC z>lq-%Z}?{u;15z1$tV zz$YXe7l=?LJ9MYhE9cZ;RPg1zd!6)w(`bC!xk24&3?v(Cs7~pK) zcb&iBoS#^0*lhOf`rdI}pW7z%`NRVUDJyH&>j#8mZ9@{skj=@q5^iEM8K^Z5khgo3XSn(J(t4EVGK!w!4n-9miui@a zqz5KT>{Gc4^RYf$AFoXM5Z+%MciD@7S&7KXdXtCR4tV{v4Fx>TZ*Xuj#9J;;XLQ)E zrsLjPL3>|sD|Zo(Q*H49hO*bTY>kK7-g7ny@}@!QsoHakJ3w`H&CJT^v>MN0wc(xi z<@MdwTF`?FB(?rzbSHLaI)PbxlwzGJm8V$xSUJ)XE7`5P9qDN@+p%KAXm#cFx%b84 z6Eaa>RTOo?^g9L@GQec-JZZx79Pat}G=^Q9j}w8;9d|@brO4thXm3)g-~TB>{nA1;O0T z$bcgX1=~rTC5!GaH(B}^y9(`Q5+B*vnB#^A2pD6@RJd`4cz zvQzf8f(P~y^tmiUm8H+7LpvUBbFE)3AVOt7%#bXFTVg#6J0P88sBPl}Dx|S4wM<)U zb6euciCyAkZ3jbasH|Im93jja2xM@Qs|C2zjrnG}wWn^$r=pHrWVqf3#F5%q*^>Ib z{oRk`q7uD)ZBKzf6ipk6Z_1T9R*5Dp9S62hY92;>+PcYXb53~6Y|$H$TJf*<{wC{$mNHcWd1VUe#aPJ{<)Li zQ22{xS#D*W{(SuB-^y}pcy0dAosczbt*V@)`pk+<@tZ2G=wb{~2_fRIzIWH@%t@-( z+zi@uHcFkD$A-#vlp&cP{IgHxUH=$QO3oUBt zn_*^L?~d@5JVtXfl}6GcCTBKCL!4#2Z5h#NOZnWmA8^P=al|OQf-Lq1qXCcke6@*U zxM@hTX2M8$L_fP@c^W+82@@mfUxuxyR1KL4#c|HWH6g3|=|({X6ZZFVe(e#H5>F&y zLx;EQ>=bm(XTwL!@w|&?Unuyp9#UXFj|kt%4aMBHwu4~J*dUsTx{p{7Hwq~rHQ9(% z!itr$WpAf}LuYqB#*YOY79)c@4kshzOC1(JRA^%%Y)2`QF&Koy^}t@F??gxd-N&)0 z%k}!SumuP|RcI!8GrraRVLVQQ(5Urs?vE?;&I*~V)LQ4p4iNHZwtL?IS*z{6+ZT`} zlhB&W-R(THY1Le#M}Z~Qmpr$9pBO{CRS$&LJo0qi;x;p&l@za(d!)xpPMHZNI~4Lf z@6mBbfRP8$VIbYxr||DCwi+b;w}DIABkFs5N&Ehb`AVI#luyXS0+Y>gV+SRzbPgiI zTe&_;y4VTHF_^FC;?`}^1)+f>LytKw+2C6`vw__2|ep^T@*dBD^oQV(_p>3Vjx|( zpi8&5{st#O#Oh;jHSM*%p?u-31ovFFl2qeGKdLXK309J? zG(`(zN7CXQ|A%6dQtR10c$W~aL?`|lsvz0_FV?AzP>)eRf|OfBLw&O9>qc_qjHusP zz5{Gwmxj2p2*#JZ$1f_E=&hwbQ7IWk&DjJ{otjeOR?;%pE2QP@tLM$!y>IyouxF}EGlf{hK-oQ~>u$Az|C21j;4Z*^}{ znJQ6H`~+EvVeR*!5YoU-MeIT`gGKj~xoY=FL;9Pt5W09$XQ##L*GZ18-Z#l7gNdtE zTn*gQOc0m^qK;1pmt#imyFEZ)qWSr?6@?W&0ekV_GZy=1p~+(v<Q(dKDRufujAd3_k!Q63 z)=Y?*{RcERr^-I5^!E9pyMxjsf)Qu3CrqRn(~mcHDlo8DRYktkC{W4MMgQ+*!IrPb zW*tmogqp4_odi1V zZX}pIM9$^?58WzDe>9jcorw7iW=jAu8exL4?Lv*UC%{xdqaFOHvL9c^YY!eef}x^q zrK76izHa>*bM-~?Q)Zhl$c2sR)hsj)D%mYm+Mlm+KnI#RHlJ|mpJS1BIe>upd;VJ4 zP-_rg&|mhDrQT2+uo*Sqiqmb3l^bLG+bqSlE;SxDI3RZw0TJ$wW24!qNx6QhwMkwW ziI}C3PTkPXzs{U|K!*~!qhe6&MpvI)9}|BT1iY`5_baPDsxtuH|J#zEnN3-h&gE%! zaOQP+z7C+2PXAQ@jU{g29X;26{rsuRY5e|jLpskfUh8i&`Vr2ed<7^?(pG180shRf zu;=&Yuv^11e}`#6xMA4zm=F2FcW6A7QSKyJ-ePkg7QR`}PY7sE`XR6J&EIO~?OogA zX@l!!fcI~>_BE!Qirl685q__W>3~!HGiC1dN|F*;{+8qC*O|uRy=vVqr zChRi5cb$ehNqm(Z;W8GW|%5nBBLgRHwtC`MEEi~G8 z)%)E6kb1`~x65bo@-`qzpb)DjK&>$bV)>F8x@e8Pg`%(LW>WEnt^OXnyEZHd`K+7I z#mj~P{|1*-XS+lg5qyre4d}jZy8a!4sHTas#teVq4v8|71WJ5MOU{w_oN&{m+Qi_XoIUv*NPw z)f%M3cP1ot>GJdDelUnI^OL1O5|e_tp*t2N2`| zI;&z)G6b<&Tbf{Aw^Z z2>?>soNuat_WQ@cN|&R3^eTA!6g z+#91Z)yVND{hKNE>~MZ-f_%Zr-gM3pmw5Taj38}h(9F6>sTr3T&&ciYRhjoYK`Gw{7{ab1BQBx4Sv77 zV7zr*YoH8q&PltW5E`H{bOf|6$vj*(fQ0DgZAJj0egz?ev*;b!=QdB2v$njeYfDz~ zr2v(bR)mm|$9@Ql7TQPtM0SJbX8RLPhS!5Y2qg7B$8V^wSYt`~{d{}sMbE-Clcutl z4DOW5p5}gWV5fjR{GZLmZ0YBA5Di|P{di?8yq-JPXvgBfy<*j!0}v(IY|MFl#~meJ zs&`$?pf+KhsrXd;(X?q6&{nJdZcJ0jUTk3FLsMG>=)d+)oi0fnP2#Vg(8w_o z_k+ipyxp!MJ8hcQT&-gD`5906!*R*$2UtB!OGUlu!&0_uIX`JV#W(kih5DZZJT<1F z7pO5A!nF@a=@D(MLKXU5?y=+I_VZ9jiE927^mO5xE=xuWvTx{oriYj#>Y$N0zf=@5YAp>3?2W3aEQ@5YSiq!l~J|CEzDy>V^+&0{U| zc6{|dGs*r^YQ+dg+79c}QPd(ooBe7XGBtmdi6xzQSEZ!i7>)W$ivzAh+ydxZ)wYNo zR{{cv5YAs|q^?_H;1d&-$;EzWIr8Y!5loL!;vIVNXQu+_yQNMnc>SR}UDZ&UJc1c= zrxl3xMyvCX!-B{V5rWocx0|njMBCV!4zH@S+(bJqAC>vUyhipG6|N7hUy!(5R0+M2 zFjA#B$f~{kzqNp-iu)@2Bd`BlmGJOe3a z;K$vvq5l4#E?Pt=YJl{^Pk2@eO>@(dlaFhm)&kirkOGO)&HBI%(DC%OL^=QZwGD_A zKw?C(&yI0JlEbK7xnsB4vKS+oqRvxeZTeWxMkwXG`3Icv5&UvAl#`;IMSQzC0LtO= zhk>!uYLN*IUU@BHdRnNA-pA znLZ_|9AYLw;1vg=2@oMJarQ2)W(L?Gsu+nWjugU{eM!Cfpjntc<79T4;Ay0n*8>tt zXisndR@G>4rbA@uP-+gH3rXV;eE0{8~ zF`4n1(s8BwKexZ~_ZMDk@2ulLv&f0OXmJoA8**#R!QwrG6J1mxE5CG|kUV0HZq@DV z%gD|*way$xA@xby!*{}CU1@?mjYL}~ff!q>|EaLa0mZ3#oc}-Wfg=@d5Y!sqQQUXF zTJLeQXF`7yMHXGkpE~96^_rXI~trkWW}a?14%j+ z6a)&sfFyby4|ahIqOoE(47kX^9|Ux#t6sOtCW!yL+mv?S$s<(Nn+q5Q9RUxe!%7V2cBk1`*)=;*HJhw2mLaqM=cz`d5e7(4AgCYcjM zxac8lsQkGXN`HoqlxTrMB^Zz4Q?}EETrJ@U5QAHvDrcA%eZ5(W)N(u-v${-SeHhhm zSh48v)Wjy|x9SaDj%tZqaO}B^7M`zJo-6g*>8z9+U!@7hG*LHURi4U_rju?ou35Jh zK%>sQd25jb;&0T*6bh(yeRdYU&g{T#5>$2bc?c}gZ*ri=#v^)vzIJi*UyfyVRJ-F! zzz1Q&HfIW$^wuFQsqZ6Vo;stuI?ips>cg{_c`V|DhuT?`R5~wt ze)>;IQ6n(M@s)f3g4Ylqqxqg>!e8E>Db-^evyMe`jf=A7rboXX=j;w^dNEQNTj8_k zCEW%F4YqodKS!=~ujX40(2kit&Q>3cT`gi~nv72zNzbUIUJ^Sx!`^e zT+Km885tQl+5XLe^q{yK)rzHtSwLf&k7X+-C;xnYZMHdIIBH}$ndW!5jm=xCS-$y5 z&o0{PD<^P6G*{AH9#kMUc=4rtp@$9vSj$}25bo$kbRA610kly1!B;-V<%Atar%k1; z1oicQX~5cjBJM{jo}H;or`KvDu1!orQfIti9VkJ;M4h0RG?Hv=X3yz#$x`}wzD#Ct zsNVaUJ(>3yXeB+v^l}Oc`0Yhf{}zIpw}C|K-_Ohz7+w*QeTUKQhx%hdz6Qr6$OZl5 z`L!Rjs%IE;?^V3ts9}>@yVe#Uq)%^m0fb_?=1RLz)s)($sUGX@YGRZRYrmff&K#*d zn*VL1uFWb32~B#l&C6k>h}3Uhqz50_sD zt*t~wm!r>NBL+=XJEwc$Irpj*;^y;B2D1*L#$5j`T;;<>cR7TrZ+YmQx8?$-wz?pT zl~2ZMb=LY}no`bVkuqy{B_6r?YRmh#%Wd8_d$5PmUj&x;gTKaRCxKR+_t}1WfH4~1 z=`~hOb&coM^~U!-Gh0nY?JE87C(#AzE?vH&X5+12W0gMuHE$B@Cl*5X9S)QAzfXd8 zWg8poI~8;9-w-J?75pSKH?>1DV-ldT>pJ!STCDB%H{oEWm0s1Tv=n#w+?%T{GzeYpcn>5Wn$jk&{R?8*SCL>>Kr z6-~I8k)ke5A!c)+F9iI=Ps5{3Gio3W|Auh4T3LyvXHmozU!SbEwQC&UbWnZZLB7)P zK-76m`=@_aWw`P8Trm>l4F)pjdhS2Wfk}HadkAv@)-R}+r;)0_w!w1D-pe@G3 zWB%n7!r1DolV*1k$1Qx5*`pEBT!Iw8*dLq5EVd$n6m0!U4C{E2IEI1lZI*!}(D2D9 zchGOymeR>lx^o~)v(>I(cvm>^7pY8Am7ouog;#C`5{f)KQ$$6$*(=Y2#@r3);I1SR zut#=BQj=3U-s`m=y&B|5il-pRXpkQ@BolHL3uqI;xTa&_eb9FoplnG1`fVng373MId8r!^&XX9*6_KOC|gh zVKvGRmM}WibO`4z%5iSpudJ@RJepy<+Y;U!u+QX1Z7ey`#HWO8M5gyyP%+QYUb-GFKwzsJb92H_gG``!bRc~&nHc~KB8<%_tY*rP}n zy>GySR}X35`qCw%Mwxy$t@ZClq?wE3>RBo+$f>(s5PjF z`$0#$`umIcf=GjE!5Q~2_hWv*m{)9>o%kgUc=5k1@fBUdlv6WZ!K=^*Zpyg`aJWL@ z?^#T8#gA`ss|EI4ka4X%w*Xk@;3{UiN5c_N77Lb$hXyj_1t&-C-cGHh7(!Wr?M~17fTP&T#Ij&lV#lpA`rE z@+&_ee|Ot^M?KmAD->~5TDDbi7y1-`_jsOTN`umcWT3QHPDg*$>JJ-}i{#>AfoC6F z0R?xz`*T=-KhCI0Xg21ho6FXqS}yqM7b^+x7xm1{lZQ-NAuT+7>N*}3$B2vJSNRm{ z_{58}@P}_LM8p`}dulgHn^)HtIG-Ec7c(CFP5*R9n(87W1OPx%a$%RO^fIgqoS+P2 zuyrD&5m$O311VHaCzUjB(^r9>&%ECIzO4L9x`4@hHb?)G8=; zk=?Ev(->|dlXz>?hB*Ju*8+Fkl>4o{TdH*km|WP)0Vs=N(guA7vcpd5g5&@q4QgDw zx1sT6XfYAIyab%OiZidb0IFVSO!5d|udYhiUTW~mywm37<$D9NSQ=t|cZ3?pR>U$g z#o!t=OOkLUgGLpm=$s;M;q z6#)Lu_>YgHV@6|Q$Nc;m*V8*Lxfg)8v|mW1==9mm*!o=?f-ml^(C!rJ%^QVQ2&8xGA`E=;ctLwOR&6oIqq^JZxZH5KaUeW* zdv$TJ+*~(#cuO0B@BDmzxjp^1Am-%$>QQhop$}){B*fc)Yox5?)cHPf9CIt?8$~fx z;Tw?@8>uUZ+f1P#5KWCjluRy(jkw|8ul2t5Yu(@LTB&QH8+BIl5c4PR$n$MTf}rbM zmn%!0!iC_bm)ni+Ra4p0a=>zf@AoNrS$W}2nBUrPn`z#c(OXBmR4tWp11xm(KYwa< zYVAhSePVb7+5)LkdvZL-yP{eyBF}F*r2%E?*0neMRu&*bF4j7|^THYXaNnUr=0pnk;N=Cq!h9eC#b8>>J;?!i!ry`Ro0CsXV1P&vxh_J{ql%NzlPE*LD8x5Ef9iXw)oo*#JF>m8k4%||`=NzksT4{KzZZjr#c zs1sXmW?H_+^ud~AEzYX17|F!(Rm|!fR#ElvGX}pSlTqhnlZ*Rx0t|8ct%_=fBfByq zvBp8a!yOTVMjn@YDR2}IOsK$>rX_D#y5o%3wZp20b(&a(QftsAd*Xv{Phyk9%!>7HdsCP@yzG)e2ab`Wek|(gee<*dr1;ObduvBWgn*wBoM>Wap2%Hb z?=K&Qv86Oyu9F9lgaP`!B5|~l$&?(R0%b@XrfhCIPyxR4!i#PUAgv?*zx@c3`-8ey z8L+4Q#_PzpmHom5fIP=+Fb{0nW+ob-qJ z-u@FKlqOyo5DSLHM^nb<<*2RdB^YB@F>Jhu?ZDMI0Snnvvt~CDxI@5MlNz9VGw|UY zqQx^WPbsb;QzMWe*9Ktj-CvpFUAaH#cKy$+fVa5a!1M;Am~k|$5OoIQQ9=TIX;gGIhIg!}T=?YpLEqcjLL&;+ z9Cp1A_Vzjwu`nV{UWNfg1}ew-*IQyM$(ccscp(;`f+-pY7d zV;)&oXfR&_wr7?I>}8CFHI<}G;P=madp2wyDJSTdvZYe;GLhXmrZ1;#oU7}D85LML z`?F1y9Mr|Z%7Dxic8?-2*FS+K0Q~v`r4qn=XCNZQ_&Q1TRj70(55z_pV&k7< zU=nIr(E?jx_uvuBpd$lg9-Q%PF=f=X|0sst5|N-G-8Z2zc_9bqIKK=*FlUsZ1qLnr zb#rebzT=VDt7Sm1w7YWqnpZ7=^9)cG7f=hp3_2(@Xi`XmQkS8aBZ~CIW@-pzWMmp< zRO|oF&dyc>*QjvaoPd8hfbhLLr&ULfi`>QBmSz~u%Zyct>{hsYOAU%iB_t+r54K-PW1X^{}^Sfr~|f!H1ww?-D`|d1;jV7Xr)7-lpJ0P!@Rnh z;6)1ukd&I=4BaKHd6M6w0ih|^jLaV{-m$=FlK14)urckrjrH|xvt0c*OMfMd?I<~b zsS6gt;)yRqG8)C?`hEbQCADy~x5)S2awzv`EgZzCha#Z2jo_{uVhDN;r8DvFEGSPA z57X!OQS`yVy1J9MJ_P&IN8l#J01#$WbYT3?Ul}B-Zzo0e5B7*jcCF-g@Z*t%WAK&I zgaszM*tzaLbvA7>(*dV6m*TSlg1e-z@E^Dj2mSm!57C{b1jiwlO|^^uw+yg0%~aS2 z5M>e8Im{sP!oK-W&H%oW%e) zZx?F11NEKf6_U?1xKMRD$FCKMDBC7yY z_VP%_HPc~vYS_@A)7DJ(#F9W>{_^+fu%evFx?!AzYGT{n58!5|ZAK*4{GF`gd>U5- z1!t+Q3IMN%XNFVB6uJXFX4?;?D%{lftV*)Rei6i52Mm0@EKIED?uw)04y-bFI zO5Nk3q|wV05E2p)Ge_rCi~#SKb_zdH1N`*8cOiJjpFC>>epz~Cff)jLf2>p{e9T{p z)}l>Bx_1XA;M_%Wa=synm>M=JGGqt@ra)*ZnY-On06V?`pun7|q-u}9|7xPsO0pz@ z7$G3w;=sm}K)GP?+U~wd%0#3FmT}?&mzTE$b*ut)9A%l~Os)kb&d8 pdiDIAx!9Wuj1WS3S@RKJC2ov#tlfd$0iV25l>I1EE@k}v{{Zg4Zl3@E literal 0 HcmV?d00001 diff --git a/assets/marketing/selectpilot-small-promo.svg b/assets/marketing/selectpilot-small-promo.svg new file mode 100644 index 0000000..4821888 --- /dev/null +++ b/assets/marketing/selectpilot-small-promo.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + SelectPilot + LOCAL TRANSFORMATION LAYER + + Selected text + to structured output + + + Local-only + + + RESULT + { + "summary": "Action brief", + "actions": ["Ship MVP"], + "risk": "Scope drift" + Privacy proof visible in-panel + diff --git a/docs/CHROME_WEB_STORE_RELEASE_CHECKLIST.md b/docs/CHROME_WEB_STORE_RELEASE_CHECKLIST.md new file mode 100644 index 0000000..7ddce7b --- /dev/null +++ b/docs/CHROME_WEB_STORE_RELEASE_CHECKLIST.md @@ -0,0 +1,67 @@ +# Chrome Web Store Release Checklist + +Last updated: 2026-08-21 + +## Automated Gate + +- [ ] `pnpm lint` +- [ ] `pnpm typecheck` +- [ ] `pnpm test` +- [ ] `pnpm test:privacy` +- [ ] `pnpm build` +- [ ] `pnpm lint:manifest` +- [ ] `pnpm validate:store` +- [ ] `pnpm test:e2e` +- [ ] `pnpm package:store` +- [ ] Package SHA-256 and inventory are preserved with the release evidence. + +`pnpm package:store` must fail while production entitlement verification is absent. Never bypass that failure with a development key or an unsigned entitlement. + +## Product Truth + +- [ ] The listing describes one purpose: selected text to structured local output. +- [ ] Ollama and the local bridge requirements are visible before installation. +- [ ] Trial, paid tiers, and feature boundaries match the exact production configuration. +- [ ] Experimental capabilities are excluded from the core claim. +- [ ] No Team or self-hosted availability is claimed. + +## Privacy And Security + +- [ ] Store disclosures match [`PRIVACY_POLICY.md`](./PRIVACY_POLICY.md). +- [ ] Core content traffic is limited to user-owned localhost services. +- [ ] Any non-content commerce traffic is disclosed separately. +- [ ] Production entitlements are signed and verified fail-closed. +- [ ] No secret, private key, token, log, report, source map, or test artifact exists in the ZIP. +- [ ] Optional retained data can be inspected, exported, and deleted. + +## Assets + +- [ ] Store icon is `128x128`. +- [ ] Three screenshots are `1280x800` and show actual product behavior. +- [ ] Small promo tile is `440x280`. +- [ ] Marquee tile is `1400x560`. +- [ ] Copy and visuals match the submitted extension version. + +## Manual Runtime Acceptance + +- [ ] Fresh Chrome profile installation succeeds. +- [ ] Missing Ollama produces one calm, actionable installation path. +- [ ] Consent-gated model provisioning succeeds on supported hardware. +- [ ] First selected-text extraction reaches a validated result. +- [ ] JSON, Markdown, and plain-text exports open correctly. +- [ ] Failure and recovery paths are verified without silent fallback. +- [ ] Uninstall and retained-data deletion behavior are verified. + +## Dashboard Gates + +- [ ] Homepage URL is public and verified. +- [ ] Support URL is public and verified. +- [ ] Privacy policy URL is public and verified. +- [ ] Listing, Privacy, Distribution, and Test Instructions fields are complete. +- [ ] Reviewer-only setup information contains no repository secret. +- [ ] Saved dashboard fields are reopened and checked for persistence. +- [ ] Submission and publication strategy are explicitly authorized. + +## Completion + +The release is complete only after the exact package is accepted, published, installable from the Store, and reverified through the real first-use path. A local ZIP or green CI run is not publication evidence. diff --git a/docs/CHROME_WEB_STORE_SUBMISSION.md b/docs/CHROME_WEB_STORE_SUBMISSION.md new file mode 100644 index 0000000..f1dd969 --- /dev/null +++ b/docs/CHROME_WEB_STORE_SUBMISSION.md @@ -0,0 +1,109 @@ +# Chrome Web Store Submission Pack + +Last updated: 2026-08-21 + +This document is the source of truth for SelectPilot's store listing. It does not prove that the extension has been submitted or published. + +## Release State + +`BLOCKED`: production entitlement signatures and public policy/support URLs are not yet configured. Do not upload a package until `pnpm package:store` succeeds without overrides. + +## Listing Basics + +- Product name: `SelectPilot` +- Category: `Productivity` +- Primary language: `English` +- Store install: free +- Product access: seven-day trial followed by a paid tier, subject to production checkout and entitlement activation + +## Single Purpose + +SelectPilot turns text you select on a web page into structured, reusable output using a local Ollama runtime. + +## Short Description + +Turn selected text into structured, reusable output with local processing through Ollama. + +## Detailed Description + +SelectPilot helps you turn selected text into useful, consistent output without sending the core transformation flow to a cloud model. + +Select text on a page, open SelectPilot, and choose the result you need: + +- extract people, organizations, dates, decisions, and actions +- create a concise summary +- turn unstructured material into reusable JSON or Markdown +- copy or export the result into another tool + +The core flow runs through a bridge and Ollama on your own computer. SelectPilot shows whether that local runtime is ready before processing begins. + +Important requirements and limits: + +- Ollama and the SelectPilot local bridge are required. +- Initial setup may download a model after explicit user consent. +- Production checkout and signed entitlement issuance must be operational before paid access is advertised as available. +- Experimental audio and vision surfaces are not part of the core selected-text claim. + +## Privacy Tab Mapping + +These answers must be rechecked against the exact submitted SHA and the hosted privacy policy. + +### Data handled for core functionality + +- Website content: selected text and active-page context chosen by the user +- User activity: the user's explicit SelectPilot actions and locally stored feature state +- Authentication information: local entitlement token and signed entitlement metadata + +### Purpose + +- Core functionality only + +### Sale, advertising, and lending + +- Data is not sold. +- Data is not used for personalized advertising. +- Data is not used for creditworthiness or lending. + +### Permission justifications + +- `activeTab`: access the current page only after a user action. +- `tabs`: identify the active content tab for the invoked flow. +- `storage`: retain local settings, entitlement state, runtime state, and user-controlled local history. +- `scripting`: recover the current selection when content-script messaging is unavailable. +- `sidePanel`: provide the primary SelectPilot workspace. +- `http://127.0.0.1/*` and `http://localhost/*`: communicate with user-owned local services only. + +## Store Assets + +- Icon: [`assets/icon128.png`](../assets/icon128.png), `128x128` +- Screenshots: three PNG files in [`assets/marketing`](../assets/marketing), each `1280x800` +- Small promo tile: `selectpilot-small-promo.png`, `440x280` +- Marquee tile: `selectpilot-marquee.png`, `1400x560` + +Run `pnpm validate:store` before uploading any asset. + +## Required Public URLs + +These values are external release gates and must be publicly reachable before submission: + +- Homepage URL: `TBD` +- Support URL: `TBD` +- Privacy policy URL: `TBD` + +The repository copy of the policy is [`docs/PRIVACY_POLICY.md`](./PRIVACY_POLICY.md). A repository file is not a substitute for verifying the final public URL in the Store dashboard. + +## Reviewer Instructions + +1. Install Ollama from its official distribution and start it. +2. Install and start the SelectPilot local bridge. +3. Install the extension package. +4. Follow the in-product setup flow and approve the recommended local model download. +5. Open a web page, select text, and open SelectPilot. +6. Run a structured extraction and export the result. +7. Confirm that the core content flow uses only `127.0.0.1` or `localhost`. + +Reviewer credentials or production entitlement instructions must be supplied through the Store's private reviewer field, never committed to this repository. + +## Submission Rule + +Do not describe SelectPilot as published, purchasable, or fully local until the corresponding behavior has been verified on the exact upload package. Dashboard entry, reviewer approval, and publication remain external actions. diff --git a/docs/PRIVACY_POLICY.md b/docs/PRIVACY_POLICY.md new file mode 100644 index 0000000..ae4746d --- /dev/null +++ b/docs/PRIVACY_POLICY.md @@ -0,0 +1,68 @@ +# SelectPilot Privacy Policy + +Last updated: 2026-08-21 + +SelectPilot turns text selected in a browser into structured output using services running on the user's own computer. + +## Core Processing + +When the user invokes a core text action, SelectPilot may handle: + +- selected text from the active page +- the active page title and URL +- instructions entered in SelectPilot +- generated summaries and structured extraction results + +The extension sends this content to the SelectPilot bridge on `127.0.0.1` or `localhost`. The bridge sends model requests to the user's configured local Ollama runtime. The core selected-text flow has no cloud-model fallback. + +## Local Storage + +SelectPilot may store the following on the user's device: + +- settings and runtime state +- entitlement token and signed entitlement metadata +- model provisioning consent and status +- exported or retained results when the user enables a feature that requires them + +Essential and Plus are designed without retained knowledge history. Pro features that retain local state must make that state visible and provide inspect, export, and delete controls. + +## Commerce And Entitlements + +Payment processing and entitlement issuance are separate from the core text-processing path. Production providers, endpoints, and data handling must be disclosed here before commerce is activated. Reviewer credentials and private tokens are never part of this public policy. + +SelectPilot must not unlock paid features from an unsigned or unverifiable entitlement. The store release remains blocked until production signature verification is configured. + +## Data Sharing + +- Core selected text and generated output are not sold. +- Core selected text and generated output are not used for advertising. +- SelectPilot does not include product telemetry or analytics in the core runtime flow. +- SelectPilot does not send core selected text to an external inference provider by default. + +The user may explicitly export a result to another application or service. That user-directed export is governed by the destination's own privacy terms. + +## Experimental Capabilities + +Audio transcription and vision OCR are experimental and are not included in the core selected-text privacy claim unless their exact runtime path has been separately verified and disclosed. + +## User Control + +Users can: + +- choose when SelectPilot receives selected text +- approve or decline local model downloads +- inspect runtime and privacy state +- inspect, export, or delete supported retained data +- remove local data and uninstall the extension + +## Security + +SelectPilot minimizes data movement and validates local execution boundaries. No software can guarantee absolute security. Security reports should use the public support channel listed in the Chrome Web Store. + +## Public Contact + +`TBD before submission`: publish the same support URL here and in the Chrome Web Store listing. + +## Changes + +Material policy changes must be published no later than the extension version that introduces the corresponding behavior. diff --git a/package.json b/package.json index 7a1853d..da6fcc0 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,8 @@ "build": "pnpm sync:manifest-version && node ./scripts/generate-extraction-presets.mjs && tsc -p tsconfig.json && node ./scripts/build-extension.mjs", "lint": "node ./scripts/generate-extraction-presets.mjs --check && eslint --max-warnings=0 .", "lint:manifest": "node ./scripts/lint-manifest.mjs", + "validate:store": "node ./scripts/validate-store-assets.mjs", + "package:store": "pnpm build && pnpm lint:manifest && node ./scripts/package-chrome-store.mjs", "test:e2e": "playwright test --config tests/e2e/playwright.config.mjs", "test": "tsc -p tsconfig.json && node --test tests/panel/*.test.mjs && python3 -m unittest discover -s tests/server -p 'test_*.py'", "test:privacy": "node --test tests/panel/network-boundary.test.mjs && python3 -m unittest tests/server/test_privacy_proof.py", diff --git a/scripts/package-chrome-store.mjs b/scripts/package-chrome-store.mjs new file mode 100644 index 0000000..ba203f2 --- /dev/null +++ b/scripts/package-chrome-store.mjs @@ -0,0 +1,111 @@ +#!/usr/bin/env node + +import { createHash } from 'node:crypto'; +import { cp, mkdir, readFile, readdir, rm, stat, utimes, writeFile } from 'node:fs/promises'; +import path from 'node:path'; +import { spawnSync } from 'node:child_process'; +import { fileURLToPath } from 'node:url'; +import { validateStoreAssets } from './validate-store-assets.mjs'; + +const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const fixedDate = new Date('2026-01-01T00:00:00Z'); +const runtimeRoots = ['agent', 'api', 'background', 'billing', 'content', 'licensing', 'panel', 'popup', 'pricing', 'shared', 'utils']; +const runtimeAssets = [ + 'assets/icon16.png', + 'assets/icon32.png', + 'assets/icon48.png', + 'assets/icon128.png', + 'assets/icon256.png', + 'assets/icon512.png', +]; +const allowedExtensions = new Set(['.css', '.html', '.js', '.json', '.png', '.svg']); +const forbiddenPath = /(^|\/)(?:\.env|tests?|reports?|logs?|node_modules|test-results|playwright-report)(\/|$)|\.(?:map|log|pem|key)$/i; +const secretPatterns = [ + /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/, + /(?:api|secret|private)[_-]?key\s*[:=]\s*["'][^"']{12,}["']/i, +]; + +async function walk(directory, base = directory) { + const entries = await readdir(directory, { withFileTypes: true }); + const files = []; + for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) { + const absolute = path.join(directory, entry.name); + if (entry.isDirectory()) files.push(...await walk(absolute, base)); + if (entry.isFile()) files.push(path.relative(base, absolute)); + } + return files; +} + +export async function collectRuntimeFiles(root = projectRoot) { + const files = ['manifest.json', ...runtimeAssets]; + for (const runtimeRoot of runtimeRoots) { + const absoluteRoot = path.join(root, runtimeRoot); + for (const nested of await walk(absoluteRoot)) { + const relative = path.posix.join(runtimeRoot, nested.split(path.sep).join(path.posix.sep)); + if (allowedExtensions.has(path.extname(relative)) && !forbiddenPath.test(relative)) files.push(relative); + } + } + return files.sort(); +} + +export async function assertReleaseSafe(files, root = projectRoot) { + const entitlement = await readFile(path.join(root, 'background/entitlement-service.js'), 'utf8'); + if (/const PUBLIC_KEY_HEX = ['"]{2}/.test(entitlement) || entitlement.includes('accepting as unsigned MVP')) { + throw new Error('Store package blocked: production entitlement signature verification is not configured (SOD-837).'); + } + + for (const relative of files) { + if (forbiddenPath.test(relative)) throw new Error(`Forbidden release path: ${relative}`); + const absolute = path.join(root, relative); + if (!(await stat(absolute)).isFile()) throw new Error(`Missing release file: ${relative}`); + if (!['.png'].includes(path.extname(relative))) { + const content = await readFile(absolute, 'utf8'); + for (const pattern of secretPatterns) { + if (pattern.test(content)) throw new Error(`Potential secret in release file: ${relative}`); + } + } + } +} + +async function sha256(filePath) { + return createHash('sha256').update(await readFile(filePath)).digest('hex'); +} + +export async function packageChromeStore(root = projectRoot) { + await validateStoreAssets(root); + const files = await collectRuntimeFiles(root); + await assertReleaseSafe(files, root); + + const packageJson = JSON.parse(await readFile(path.join(root, 'package.json'), 'utf8')); + const outputRoot = path.join(root, 'dist', 'chrome-web-store'); + const stageRoot = path.join(outputRoot, `selectpilot-${packageJson.version}`); + const zipPath = `${stageRoot}.zip`; + await rm(outputRoot, { recursive: true, force: true }); + await mkdir(stageRoot, { recursive: true }); + + for (const relative of files) { + const destination = path.join(stageRoot, relative); + await mkdir(path.dirname(destination), { recursive: true }); + await cp(path.join(root, relative), destination); + await utimes(destination, fixedDate, fixedDate); + } + + const zip = spawnSync('zip', ['-X', '-q', zipPath, ...files], { cwd: stageRoot, encoding: 'utf8' }); + if (zip.error || zip.status !== 0) throw new Error(`zip failed: ${zip.error?.message || zip.stderr || zip.status}`); + + const report = { + schema_version: 1, + version: packageJson.version, + artifact: path.basename(zipPath), + sha256: await sha256(zipPath), + file_count: files.length, + files, + }; + await writeFile(`${zipPath}.json`, `${JSON.stringify(report, null, 2)}\n`); + return report; +} + +if (process.argv[1] === fileURLToPath(import.meta.url)) { + const report = await packageChromeStore(); + console.log(`${report.artifact} ${report.sha256}`); +} diff --git a/scripts/validate-store-assets.mjs b/scripts/validate-store-assets.mjs new file mode 100644 index 0000000..34354ec --- /dev/null +++ b/scripts/validate-store-assets.mjs @@ -0,0 +1,45 @@ +#!/usr/bin/env node + +import { readFile } from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); + +export const REQUIRED_IMAGES = new Map([ + ['assets/icon128.png', [128, 128]], + ['assets/marketing/selectpilot-screenshot-extract.png', [1280, 800]], + ['assets/marketing/selectpilot-screenshot-runtime.png', [1280, 800]], + ['assets/marketing/selectpilot-screenshot-privacy.png', [1280, 800]], + ['assets/marketing/selectpilot-small-promo.png', [440, 280]], + ['assets/marketing/selectpilot-marquee.png', [1400, 560]], +]); + +export function readPngDimensions(buffer) { + const signature = buffer.subarray(0, 8).toString('hex'); + if (signature !== '89504e470d0a1a0a' || buffer.subarray(12, 16).toString('ascii') !== 'IHDR') { + throw new Error('not a PNG with an IHDR header'); + } + return [buffer.readUInt32BE(16), buffer.readUInt32BE(20)]; +} + +export async function validateStoreAssets(root = projectRoot) { + const errors = []; + for (const [relativePath, expected] of REQUIRED_IMAGES) { + try { + const actual = readPngDimensions(await readFile(path.join(root, relativePath))); + if (actual[0] !== expected[0] || actual[1] !== expected[1]) { + errors.push(`${relativePath}: expected ${expected.join('x')}, got ${actual.join('x')}`); + } + } catch (error) { + errors.push(`${relativePath}: ${error.message}`); + } + } + if (errors.length) throw new Error(`Store asset validation failed:\n- ${errors.join('\n- ')}`); + return [...REQUIRED_IMAGES.keys()]; +} + +if (process.argv[1] === fileURLToPath(import.meta.url)) { + await validateStoreAssets(); + console.log(`Validated ${REQUIRED_IMAGES.size} Chrome Web Store images.`); +} diff --git a/tests/panel/store-package.test.mjs b/tests/panel/store-package.test.mjs new file mode 100644 index 0000000..766957a --- /dev/null +++ b/tests/panel/store-package.test.mjs @@ -0,0 +1,35 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { readFile } from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { collectRuntimeFiles, assertReleaseSafe } from '../../scripts/package-chrome-store.mjs'; +import { REQUIRED_IMAGES, readPngDimensions, validateStoreAssets } from '../../scripts/validate-store-assets.mjs'; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); + +test('Chrome Web Store image dimensions match the submission contract', async () => { + const files = await validateStoreAssets(root); + assert.deepEqual(files, [...REQUIRED_IMAGES.keys()]); + const icon = await readFile(path.join(root, 'assets/icon128.png')); + assert.deepEqual(readPngDimensions(icon), [128, 128]); +}); + +test('runtime inventory excludes source, tests, reports, and transient files', async () => { + const files = await collectRuntimeFiles(root); + assert.ok(files.includes('manifest.json')); + assert.ok(files.includes('background/background.js')); + assert.ok(files.includes('content/content-script.bundle.js')); + assert.ok(files.includes('assets/icon128.png')); + assert.ok(!files.some((file) => file.startsWith('assets/marketing/'))); + assert.ok(files.every((file) => !file.endsWith('.ts'))); + assert.ok(files.every((file) => !/(^|\/)(tests|reports|node_modules)(\/|$)/.test(file))); +}); + +test('store packaging fails closed while entitlement verification is unsigned', async () => { + const files = await collectRuntimeFiles(root); + await assert.rejects( + assertReleaseSafe(files, root), + /production entitlement signature verification is not configured/ + ); +}); From 671b5c8cf93d2a5b3f704f00ccf1f1105cae55e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristoffer=20S=C3=B6dersten?= <60147923+kristoffersodersten@users.noreply.github.com> Date: Fri, 21 Aug 2026 18:35:45 +0200 Subject: [PATCH 2/4] fix(governance): map store release evidence Annotate SOD-836 package and validation surfaces for fail-closed specification coverage.\n\nVerification: spec compiler will run on Hetzner exact SHA. Refs: SOD-836 --- scripts/package-chrome-store.mjs | 2 ++ scripts/validate-store-assets.mjs | 3 +++ tests/panel/store-package.test.mjs | 3 +++ 3 files changed, 8 insertions(+) diff --git a/scripts/package-chrome-store.mjs b/scripts/package-chrome-store.mjs index ba203f2..15826da 100644 --- a/scripts/package-chrome-store.mjs +++ b/scripts/package-chrome-store.mjs @@ -1,4 +1,6 @@ #!/usr/bin/env node +// module_name: scripts_package-chrome-store_mjs +// spec_ref: "reporting" import { createHash } from 'node:crypto'; import { cp, mkdir, readFile, readdir, rm, stat, utimes, writeFile } from 'node:fs/promises'; diff --git a/scripts/validate-store-assets.mjs b/scripts/validate-store-assets.mjs index 34354ec..05c7034 100644 --- a/scripts/validate-store-assets.mjs +++ b/scripts/validate-store-assets.mjs @@ -1,4 +1,6 @@ #!/usr/bin/env node +// module_name: scripts_validate-store-assets_mjs +// spec_ref: "reporting" import { readFile } from 'node:fs/promises'; import path from 'node:path'; @@ -15,6 +17,7 @@ export const REQUIRED_IMAGES = new Map([ ['assets/marketing/selectpilot-marquee.png', [1400, 560]], ]); +// @spec_ref reporting export function readPngDimensions(buffer) { const signature = buffer.subarray(0, 8).toString('hex'); if (signature !== '89504e470d0a1a0a' || buffer.subarray(12, 16).toString('ascii') !== 'IHDR') { diff --git a/tests/panel/store-package.test.mjs b/tests/panel/store-package.test.mjs index 766957a..a4012d9 100644 --- a/tests/panel/store-package.test.mjs +++ b/tests/panel/store-package.test.mjs @@ -1,3 +1,6 @@ +// module_name: tests_panel_store-package_test_mjs +// spec_ref: "testing_strategy.integration_tests" + import assert from 'node:assert/strict'; import test from 'node:test'; import { readFile } from 'node:fs/promises'; From c2cb42a29e143463c7c9ef68f202cbc45fbdc24d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristoffer=20S=C3=B6dersten?= <60147923+kristoffersodersten@users.noreply.github.com> Date: Fri, 21 Aug 2026 18:37:37 +0200 Subject: [PATCH 3/4] fix(release): make store tooling reproducible Force UTC ZIP metadata and resolve relative CLI entry paths. Add regression coverage proving both commands execute instead of silently no-op.\n\nVerification: node --test tests/panel/store-package.test.mjs Refs: SOD-836 --- scripts/package-chrome-store.mjs | 8 ++++++-- scripts/validate-store-assets.mjs | 2 +- tests/panel/store-package.test.mjs | 16 ++++++++++++++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/scripts/package-chrome-store.mjs b/scripts/package-chrome-store.mjs index 15826da..5b549ed 100644 --- a/scripts/package-chrome-store.mjs +++ b/scripts/package-chrome-store.mjs @@ -92,7 +92,11 @@ export async function packageChromeStore(root = projectRoot) { await utimes(destination, fixedDate, fixedDate); } - const zip = spawnSync('zip', ['-X', '-q', zipPath, ...files], { cwd: stageRoot, encoding: 'utf8' }); + const zip = spawnSync('zip', ['-X', '-q', zipPath, ...files], { + cwd: stageRoot, + encoding: 'utf8', + env: { ...process.env, TZ: 'UTC' }, + }); if (zip.error || zip.status !== 0) throw new Error(`zip failed: ${zip.error?.message || zip.stderr || zip.status}`); const report = { @@ -107,7 +111,7 @@ export async function packageChromeStore(root = projectRoot) { return report; } -if (process.argv[1] === fileURLToPath(import.meta.url)) { +if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { const report = await packageChromeStore(); console.log(`${report.artifact} ${report.sha256}`); } diff --git a/scripts/validate-store-assets.mjs b/scripts/validate-store-assets.mjs index 05c7034..321acf5 100644 --- a/scripts/validate-store-assets.mjs +++ b/scripts/validate-store-assets.mjs @@ -42,7 +42,7 @@ export async function validateStoreAssets(root = projectRoot) { return [...REQUIRED_IMAGES.keys()]; } -if (process.argv[1] === fileURLToPath(import.meta.url)) { +if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { await validateStoreAssets(); console.log(`Validated ${REQUIRED_IMAGES.size} Chrome Web Store images.`); } diff --git a/tests/panel/store-package.test.mjs b/tests/panel/store-package.test.mjs index a4012d9..b55a213 100644 --- a/tests/panel/store-package.test.mjs +++ b/tests/panel/store-package.test.mjs @@ -2,6 +2,7 @@ // spec_ref: "testing_strategy.integration_tests" import assert from 'node:assert/strict'; +import { execFileSync, spawnSync } from 'node:child_process'; import test from 'node:test'; import { readFile } from 'node:fs/promises'; import path from 'node:path'; @@ -36,3 +37,18 @@ test('store packaging fails closed while entitlement verification is unsigned', /production entitlement signature verification is not configured/ ); }); + +test('relative CLI paths execute validation and fail-closed packaging', () => { + const validation = execFileSync(process.execPath, ['./scripts/validate-store-assets.mjs'], { + cwd: root, + encoding: 'utf8', + }); + assert.match(validation, /Validated 6 Chrome Web Store images/); + + const packaging = spawnSync(process.execPath, ['./scripts/package-chrome-store.mjs'], { + cwd: root, + encoding: 'utf8', + }); + assert.equal(packaging.status, 1); + assert.match(packaging.stderr, /production entitlement signature verification is not configured/); +}); From 52bdd27e4f0d3e78d12bbad7a17e1537c4358e5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristoffer=20S=C3=B6dersten?= <60147923+kristoffersodersten@users.noreply.github.com> Date: Sun, 23 Aug 2026 00:52:36 +0200 Subject: [PATCH 4/4] fix(entitlements): require Ed25519 release signatures Reject unsigned, expired, token-mismatched, unknown-key, and tampered entitlements. Keep private signer material outside Git and store packaging blocked until a production public key ring is injected. Verified: pnpm lint, pnpm typecheck, pnpm test, pnpm build, pnpm lint:manifest, pnpm validate:store, negative pnpm package:store, pnpm test:privacy, pnpm test:e2e, pnpm bench:frontier, pnpm runtime:compile-policy, pnpm runtime:validate-policy, node scripts/stress-runner.mjs --- background/entitlement-service.ts | 107 +++++++++++------- docs/CHROME_WEB_STORE_RELEASE_CHECKLIST.md | 7 ++ scripts/build-extension.mjs | 26 +++++ scripts/package-chrome-store.mjs | 2 +- server/entitlement_signer.py | 69 +++++++++++ server/monero_payment_server.py | 21 ++-- server/nano_server.py | 14 ++- tests/e2e/extension-first-run-flow.spec.mjs | 46 ++++++-- tests/panel/entitlement-signature.test.mjs | 33 ++++++ tests/server/test_entitlement_signer.py | 58 ++++++++++ tests/server/test_entitlement_verification.py | 12 +- 11 files changed, 328 insertions(+), 67 deletions(-) create mode 100644 server/entitlement_signer.py create mode 100644 tests/panel/entitlement-signature.test.mjs create mode 100644 tests/server/test_entitlement_signer.py diff --git a/background/entitlement-service.ts b/background/entitlement-service.ts index 1a3de4a..0fcfe66 100644 --- a/background/entitlement-service.ts +++ b/background/entitlement-service.ts @@ -41,7 +41,10 @@ export type CachedEntitlement = { const OFFLINE_GRACE_MS = 7 * 24 * 60 * 60 * 1000; const REFRESH_INTERVAL_MS = 10 * 60 * 1000; -const PUBLIC_KEY_HEX = ''; +const ENTITLEMENT_PUBLIC_KEYS: Readonly> = { + __SELECTPILOT_ENTITLEMENT_KEY_ID__: '__SELECTPILOT_ENTITLEMENT_PUBLIC_KEY_HEX__', +}; +const SIGNATURE_ALGORITHM = 'Ed25519'; let cachedFeatureMap: Record | null = null; @@ -92,12 +95,18 @@ function canonicalizeEntitlement(payload: EntitlementPayload): string { }); } -async function verifySignature(payload: EntitlementPayload, signature: string): Promise { - if (!PUBLIC_KEY_HEX) return false; +export async function verifyEntitlementSignature( + payload: EntitlementPayload, + signature: string, + kid: string, + publicKeys: Readonly> = ENTITLEMENT_PUBLIC_KEYS, +): Promise { + const publicKeyHex = publicKeys[kid]; + if (!publicKeyHex || !signature || !kid) return false; try { const key = await crypto.subtle.importKey( 'raw', - bytesToArrayBuffer(hexToBytes(PUBLIC_KEY_HEX)), + bytesToArrayBuffer(hexToBytes(publicKeyHex)), { name: 'Ed25519' }, false, ['verify'] @@ -135,7 +144,25 @@ async function isFeatureAllowedByTier(feature: string, tier: EntitlementTier): P function isWithinOfflineGrace(record: LicenseRecord): boolean { const baseline = record.cachedAt || record.issuedAt; - return nowMs() <= (baseline + OFFLINE_GRACE_MS); + return (!record.expiresAt || nowMs() < record.expiresAt) + && nowMs() <= (baseline + OFFLINE_GRACE_MS); +} + +async function isVerifiedCachedEntitlement(record: LicenseRecord, token: string): Promise { + if ( + record.token !== token + || record.alg !== SIGNATURE_ALGORITHM + || !record.signature + || !record.kid + || !isWithinOfflineGrace(record) + ) return false; + return verifyEntitlementSignature({ + token: record.token, + tier: record.tier, + features: record.features, + issuedAt: record.issuedAt, + expiresAt: record.expiresAt ?? null, + }, record.signature, record.kid); } async function readCachedEntitlement(): Promise { @@ -162,44 +189,38 @@ async function writeCachedEntitlement(record: CachedEntitlement): Promise } async function normalizeRemoteResponse(token: string, response: SignedEntitlementResponse): Promise { - if (response.entitlement) { - const entitlement = response.entitlement; - if (entitlement.token !== token) { - warn('entitlement', 'token mismatch in signed response'); - return null; - } - if (response.signature) { - if (!PUBLIC_KEY_HEX) { - warn('entitlement', 'signature returned but PUBLIC_KEY_HEX is not configured; accepting as unsigned MVP'); - } else { - const valid = await verifySignature(entitlement, response.signature); - if (!valid) return null; - } - } - return { - token: entitlement.token, - tier: entitlement.tier, - features: entitlement.features, - issuedAt: entitlement.issuedAt, - expiresAt: entitlement.expiresAt ?? undefined, - cachedAt: nowMs(), - signature: response.signature, - alg: response.alg, - kid: response.kid, - }; + const entitlement = response.entitlement; + if ( + !entitlement + || entitlement.token !== token + || !['essential', 'plus', 'pro'].includes(entitlement.tier) + || !Number.isInteger(entitlement.issuedAt) + || (entitlement.expiresAt != null && !Number.isInteger(entitlement.expiresAt)) + ) { + warn('entitlement', 'missing or token-mismatched signed entitlement'); + return null; } - - if (response.tier && response.issuedAt) { - return { - token, - tier: response.tier, - issuedAt: response.issuedAt, - expiresAt: response.expiresAt, - cachedAt: nowMs(), - }; + if (response.alg !== SIGNATURE_ALGORITHM || !response.signature || !response.kid) { + warn('entitlement', 'unsigned or unsupported entitlement response'); + return null; } - - return null; + if (entitlement.expiresAt != null && entitlement.expiresAt <= nowMs()) { + warn('entitlement', 'expired entitlement response'); + return null; + } + const valid = await verifyEntitlementSignature(entitlement, response.signature, response.kid); + if (!valid) return null; + return { + token: entitlement.token, + tier: entitlement.tier, + features: entitlement.features, + issuedAt: entitlement.issuedAt, + expiresAt: entitlement.expiresAt ?? undefined, + cachedAt: nowMs(), + signature: response.signature, + alg: response.alg, + kid: response.kid, + }; } async function remoteVerify(token: string): Promise { @@ -245,7 +266,7 @@ export async function refreshEntitlement(force = false): Promise REFRESH_INTERVAL_MS; if (!shouldAttemptRemote && cached) { log('entitlement', 'using cached entitlement within offline grace'); @@ -270,7 +291,7 @@ export async function refreshEntitlement(force = false): Promise !/^[A-Za-z0-9._-]{1,64}$/.test(kid) || !/^[0-9a-f]{64}$/i.test(String(key)))) { + throw new Error('Entitlement public key ring must contain valid key IDs and 32-byte Ed25519 public keys'); + } + const target = path.join(projectRoot, 'background/entitlement-service.js'); + const source = await readFile(target, 'utf8'); + const configured = source.replace( + /const ENTITLEMENT_PUBLIC_KEYS = \{[\s\S]*?\n\};/, + `const ENTITLEMENT_PUBLIC_KEYS = ${JSON.stringify(keys)};`, + ); + if (configured === source) throw new Error('Entitlement public key injection marker not found'); + await writeFile(target, configured); +} + +await injectEntitlementPublicKeys(); + await build({ entryPoints: [path.join(projectRoot, 'content/content-script.ts')], outfile: path.join(projectRoot, 'content/content-script.bundle.js'), diff --git a/scripts/package-chrome-store.mjs b/scripts/package-chrome-store.mjs index 5b549ed..e9efb97 100644 --- a/scripts/package-chrome-store.mjs +++ b/scripts/package-chrome-store.mjs @@ -52,7 +52,7 @@ export async function collectRuntimeFiles(root = projectRoot) { export async function assertReleaseSafe(files, root = projectRoot) { const entitlement = await readFile(path.join(root, 'background/entitlement-service.js'), 'utf8'); - if (/const PUBLIC_KEY_HEX = ['"]{2}/.test(entitlement) || entitlement.includes('accepting as unsigned MVP')) { + if (entitlement.includes('__SELECTPILOT_ENTITLEMENT_PUBLIC_KEY_HEX__') || entitlement.includes('__SELECTPILOT_ENTITLEMENT_KEY_ID__')) { throw new Error('Store package blocked: production entitlement signature verification is not configured (SOD-837).'); } diff --git a/server/entitlement_signer.py b/server/entitlement_signer.py new file mode 100644 index 0000000..47f2186 --- /dev/null +++ b/server/entitlement_signer.py @@ -0,0 +1,69 @@ +"""module_name: entitlement_signer; spec_ref: "validation_layer".""" + +from __future__ import annotations + +import base64 +import json +import os +import subprocess +import tempfile +from dataclasses import dataclass +from pathlib import Path + + +class SigningError(RuntimeError): + """Explicit fail-closed entitlement signing failure.""" + + +def canonical_entitlement(entitlement: dict) -> bytes: + return json.dumps( + { + "token": entitlement["token"], + "tier": entitlement["tier"], + "features": entitlement.get("features") or [], + "issuedAt": entitlement["issuedAt"], + "expiresAt": entitlement.get("expiresAt"), + }, + separators=(",", ":"), + ensure_ascii=False, + ).encode("utf-8") + + +@dataclass(frozen=True) +class EntitlementSigner: + key_file: str + key_id: str + + @classmethod + def from_environment(cls) -> "EntitlementSigner": + return cls( + key_file=os.environ.get("SELECTPILOT_ENTITLEMENT_SIGNING_KEY_FILE", ""), + key_id=os.environ.get("SELECTPILOT_ENTITLEMENT_SIGNING_KEY_ID", ""), + ) + + def sign(self, entitlement: dict) -> dict: + if not self.key_file or not self.key_id: + raise SigningError("entitlement_signer_not_configured") + key_path = Path(self.key_file) + if not key_path.is_file(): + raise SigningError("entitlement_signing_key_unavailable") + try: + with tempfile.NamedTemporaryFile() as payload_file: + payload_file.write(canonical_entitlement(entitlement)) + payload_file.flush() + result = subprocess.run( + ["openssl", "pkeyutl", "-sign", "-rawin", "-inkey", str(key_path), "-in", payload_file.name], + capture_output=True, + check=False, + timeout=5, + ) + except (OSError, subprocess.TimeoutExpired) as exc: + raise SigningError("entitlement_signing_failed") from exc + if result.returncode != 0 or len(result.stdout) != 64: + raise SigningError("entitlement_signing_failed") + return { + "entitlement": entitlement, + "signature": base64.b64encode(result.stdout).decode("ascii"), + "alg": "Ed25519", + "kid": self.key_id, + } diff --git a/server/monero_payment_server.py b/server/monero_payment_server.py index cd42f3b..0d5d854 100644 --- a/server/monero_payment_server.py +++ b/server/monero_payment_server.py @@ -23,6 +23,7 @@ import requests from flask import Flask, jsonify, request +from entitlement_signer import EntitlementSigner, SigningError app = Flask(__name__) @@ -33,6 +34,7 @@ CONFIRMATIONS_REQUIRED = int(os.environ.get("CHROMEAI_MONERO_CONFIRMATIONS", "10")) POLL_INTERVAL_SECONDS = int(os.environ.get("CHROMEAI_MONERO_POLL_SECONDS", "20")) ORDER_EXPIRY_MS = int(os.environ.get("CHROMEAI_MONERO_ORDER_EXPIRY_MS", str(30 * 60 * 1000))) +ENTITLEMENT_SIGNER = EntitlementSigner.from_environment() db_lock = Lock() @@ -236,13 +238,18 @@ def verify_license(): if not record or record.get("revoked"): return jsonify({"error": "invalid"}), 401 - return jsonify( - { - "tier": record["tier"], - "issuedAt": record["issuedAt"], - "expiresAt": record.get("expiresAt"), - } - ) + entitlement = { + "token": token, + "tier": record["tier"], + "features": record.get("features") or [], + "issuedAt": record["issuedAt"], + "expiresAt": record.get("expiresAt"), + } + try: + return jsonify(ENTITLEMENT_SIGNER.sign(entitlement)) + except SigningError as exc: + app.logger.error("Entitlement signing unavailable: %s", exc) + return jsonify({"error": str(exc)}), 503 # ---- ADMIN REVOKE ---- diff --git a/server/nano_server.py b/server/nano_server.py index b491679..45c43c5 100644 --- a/server/nano_server.py +++ b/server/nano_server.py @@ -1308,14 +1308,22 @@ def license_verify(payload: dict) -> dict: except (URLError, TimeoutError, json.JSONDecodeError) as exc: raise ValidationError("entitlement_verifier_unavailable", "Local entitlement verifier is unavailable", status=503) from exc - issued_at = result.get("issuedAt") if isinstance(result, dict) else None - expires_at = result.get("expiresAt") if isinstance(result, dict) else None + entitlement = result.get("entitlement") if isinstance(result, dict) else None + issued_at = entitlement.get("issuedAt") if isinstance(entitlement, dict) else None + expires_at = entitlement.get("expiresAt") if isinstance(entitlement, dict) else None if ( not isinstance(result, dict) - or result.get("tier") not in {"essential", "plus", "pro"} + or not isinstance(entitlement, dict) + or entitlement.get("token") != token + or entitlement.get("tier") not in {"essential", "plus", "pro"} or not isinstance(issued_at, int) or isinstance(issued_at, bool) or (expires_at is not None and (not isinstance(expires_at, int) or isinstance(expires_at, bool))) + or result.get("alg") != "Ed25519" + or not isinstance(result.get("kid"), str) + or not result.get("kid") + or not isinstance(result.get("signature"), str) + or not result.get("signature") ): raise ValidationError("invalid_entitlement_response", "Local entitlement verifier returned an invalid contract", status=503) return result diff --git a/tests/e2e/extension-first-run-flow.spec.mjs b/tests/e2e/extension-first-run-flow.spec.mjs index 9e8778f..98cde3f 100644 --- a/tests/e2e/extension-first-run-flow.spec.mjs +++ b/tests/e2e/extension-first-run-flow.spec.mjs @@ -1,16 +1,34 @@ // module_name: extension_privacy_integration // spec_ref: "privacy_and_debug_policy" import { test, expect, chromium } from '@playwright/test'; +import { cp, mkdir, readFile, writeFile } from 'node:fs/promises'; +import path from 'node:path'; +import { collectRuntimeFiles } from '../../scripts/package-chrome-store.mjs'; test('real extension preserves privacy from selected text through rendered output', async () => { const executablePath = process.env.SELECTPILOT_CHROME_EXECUTABLE || chromium.executablePath(); + const keyId = 'e2e-ephemeral'; + const keyPair = await crypto.subtle.generateKey('Ed25519', true, ['sign', 'verify']); + const publicKeyHex = Buffer.from(await crypto.subtle.exportKey('raw', keyPair.publicKey)).toString('hex'); + const extensionRoot = test.info().outputPath('extension'); + for (const relative of await collectRuntimeFiles(process.cwd())) { + const destination = path.join(extensionRoot, relative); + await mkdir(path.dirname(destination), { recursive: true }); + await cp(path.join(process.cwd(), relative), destination); + } + const entitlementScript = path.join(extensionRoot, 'background/entitlement-service.js'); + const entitlementSource = await readFile(entitlementScript, 'utf8'); + await writeFile(entitlementScript, entitlementSource.replace( + /const ENTITLEMENT_PUBLIC_KEYS = \{[\s\S]*?\n\};/, + `const ENTITLEMENT_PUBLIC_KEYS = ${JSON.stringify({ [keyId]: publicKeyHex })};`, + )); const context = await chromium.launchPersistentContext(test.info().outputPath('extension-user-data'), { executablePath, headless: process.env.SELECTPILOT_HEADED !== '1', args: [ - `--disable-extensions-except=${process.cwd()}`, - `--load-extension=${process.cwd()}`, + `--disable-extensions-except=${extensionRoot}`, + `--load-extension=${extensionRoot}`, '--disable-web-security', '--disable-background-networking', '--disable-component-update', @@ -74,6 +92,18 @@ test('real extension preserves privacy from selected text through rendered outpu if (path === '/runtime-meta/health') { return fulfill({ ok: false, stream_enabled: false, active_streams: 0, event_version: '1' }); } + if (path === '/license/verify') { + const token = request.postDataJSON().token; + const now = Date.now(); + const entitlement = { + token, tier: 'essential', features: ['structured_extraction'], issuedAt: now, + expiresAt: now + 86_400_000, + }; + const signature = Buffer.from(await crypto.subtle.sign( + 'Ed25519', keyPair.privateKey, new TextEncoder().encode(JSON.stringify(entitlement)), + )).toString('base64'); + return fulfill({ entitlement, signature, alg: 'Ed25519', kid: keyId }); + } if (path === '/extract') { extractRequests.push(request.postDataJSON()); return fulfill({ @@ -107,14 +137,10 @@ test('real extension preserves privacy from selected text through rendered outpu expect(lockedResponse.error).toBe('Paid license required for deterministic extraction'); expect(extractRequests).toEqual([]); - await panelPage.evaluate(async () => { - const storage = await import(globalThis.chrome.runtime.getURL('licensing/license-storage.js')); - const now = Date.now(); - await storage.saveLicense({ - token: 'sp_e2e_paid_token', tier: 'essential', issuedAt: now, - expiresAt: now + 86_400_000, cachedAt: now, - }); - }); + const attached = await panelPage.evaluate(() => globalThis.chrome.runtime.sendMessage({ + type: 'license:attach_token', token: 'sp_e2e_paid_token', + })); + expect(attached).toMatchObject({ token: 'sp_e2e_paid_token', tier: 'essential' }); await panelPage.reload(); await expect(panelPage.locator('#btn-first-run-example')).toBeVisible(); diff --git a/tests/panel/entitlement-signature.test.mjs b/tests/panel/entitlement-signature.test.mjs new file mode 100644 index 0000000..5dbc532 --- /dev/null +++ b/tests/panel/entitlement-signature.test.mjs @@ -0,0 +1,33 @@ +// module_name: tests_panel_entitlement_signature_test_mjs +// spec_ref: "testing_strategy.integration_tests" + +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { verifyEntitlementSignature } from '../../background/entitlement-service.js'; + +function toHex(bytes) { + return Buffer.from(bytes).toString('hex'); +} + +test('only a matching Ed25519 key ID and exact payload verify', async () => { + const pair = await crypto.subtle.generateKey('Ed25519', true, ['sign', 'verify']); + const publicKey = toHex(await crypto.subtle.exportKey('raw', pair.publicKey)); + const entitlement = { + token: 'opaque-token', + tier: 'pro', + features: ['image_ocr'], + issuedAt: 1_700_000_000_000, + expiresAt: 4_000_000_000_000, + }; + const canonical = JSON.stringify(entitlement); + const signature = Buffer.from(await crypto.subtle.sign( + 'Ed25519', + pair.privateKey, + new TextEncoder().encode(canonical), + )).toString('base64'); + const keyRing = { 'rotation-1': publicKey }; + + assert.equal(await verifyEntitlementSignature(entitlement, signature, 'rotation-1', keyRing), true); + assert.equal(await verifyEntitlementSignature(entitlement, signature, 'unknown', keyRing), false); + assert.equal(await verifyEntitlementSignature({ ...entitlement, tier: 'plus' }, signature, 'rotation-1', keyRing), false); +}); diff --git a/tests/server/test_entitlement_signer.py b/tests/server/test_entitlement_signer.py new file mode 100644 index 0000000..b6a0767 --- /dev/null +++ b/tests/server/test_entitlement_signer.py @@ -0,0 +1,58 @@ +"""module_name: entitlement_signer_tests; spec_ref: "testing_strategy.integration_tests".""" + +from __future__ import annotations + +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +SERVER_DIR = ROOT / "server" +if str(SERVER_DIR) not in sys.path: + sys.path.insert(0, str(SERVER_DIR)) + +from entitlement_signer import EntitlementSigner, SigningError, canonical_entitlement # noqa: E402 + + +class EntitlementSignerTests(unittest.TestCase): + entitlement = { + "token": "opaque-token", + "tier": "pro", + "features": ["image_ocr"], + "issuedAt": 1_700_000_000_000, + "expiresAt": 1_700_086_400_000, + } + + def test_unconfigured_signer_fails_closed(self) -> None: + with self.assertRaises(SigningError) as ctx: + EntitlementSigner("", "").sign(self.entitlement) + self.assertEqual(str(ctx.exception), "entitlement_signer_not_configured") + + def test_ephemeral_ed25519_identity_signs_exact_canonical_payload(self) -> None: + with tempfile.TemporaryDirectory() as directory: + key_file = Path(directory) / "signer.pem" + public_file = Path(directory) / "signer-public.pem" + subprocess.run(["openssl", "genpkey", "-algorithm", "Ed25519", "-out", key_file], check=True) + subprocess.run(["openssl", "pkey", "-in", key_file, "-pubout", "-out", public_file], check=True) + + signed = EntitlementSigner(str(key_file), "test-rotation-1").sign(self.entitlement) + signature_file = Path(directory) / "signature.bin" + payload_file = Path(directory) / "payload.json" + import base64 + signature_file.write_bytes(base64.b64decode(signed["signature"])) + payload_file.write_bytes(canonical_entitlement(self.entitlement)) + verified = subprocess.run( + ["openssl", "pkeyutl", "-verify", "-rawin", "-pubin", "-inkey", public_file, + "-sigfile", signature_file, "-in", payload_file], + capture_output=True, + check=False, + ) + self.assertEqual(verified.returncode, 0) + self.assertEqual(signed["alg"], "Ed25519") + self.assertEqual(signed["kid"], "test-rotation-1") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/server/test_entitlement_verification.py b/tests/server/test_entitlement_verification.py index 21bd2fa..d2f6d48 100644 --- a/tests/server/test_entitlement_verification.py +++ b/tests/server/test_entitlement_verification.py @@ -47,10 +47,13 @@ def test_invalid_authority_token_remains_unauthorized(self) -> None: self.assertEqual(ctx.exception.status, 401) def test_verified_local_authority_response_is_returned(self) -> None: - response = _Response(b'{"tier":"pro","issuedAt":1700000000000,"expiresAt":null}') + response = _Response( + b'{"entitlement":{"token":"opaque-token","tier":"pro","features":[],"issuedAt":1700000000000,' + b'"expiresAt":null},"signature":"c2lnbmF0dXJl","alg":"Ed25519","kid":"rotation-1"}' + ) with patch("nano_server.urlopen", return_value=response): result = license_verify({"token": "opaque-token"}) - self.assertEqual(result["tier"], "pro") + self.assertEqual(result["entitlement"]["tier"], "pro") def test_non_loopback_verifier_configuration_is_rejected(self) -> None: with patch.dict(os.environ, {"SELECTPILOT_BILLING_VERIFY_URL": "https://example.com/license/verify"}): @@ -59,7 +62,10 @@ def test_non_loopback_verifier_configuration_is_rejected(self) -> None: self.assertEqual(ctx.exception.code, "invalid_entitlement_verifier") def test_malformed_authority_response_is_rejected(self) -> None: - response = _Response(b'{"tier":"pro","issuedAt":"not-a-timestamp"}') + response = _Response( + b'{"entitlement":{"token":"opaque-token","tier":"pro","issuedAt":"not-a-timestamp"},' + b'"signature":"bad","alg":"Ed25519","kid":"rotation-1"}' + ) with patch("nano_server.urlopen", return_value=response): with self.assertRaises(ValidationError) as ctx: license_verify({"token": "opaque-token"})