From e8ef57bf001deed590d3554168db57b173d9192b Mon Sep 17 00:00:00 2001 From: leeguooooo Date: Tue, 9 Jun 2026 18:27:51 +0900 Subject: [PATCH] feat(connect): force-install ab-connect via Chrome config profile (no Load-unpacked GUI) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Chrome 149 killed every GUI-free way to load an *unpacked* extension into the real profile: --load-extension removed in Chrome 142 (incl. the --disable-features workaround), local-.crx external install blocked on macOS since Chrome 44, remote-debugging-port killed in Chrome 136. So agents were stuck automating the chrome://extensions Load-unpacked native file dialog — unworkable. `extension install` now writes a macOS configuration profile that force-installs the signed .crx from a hosted update_url (ExtensionInstallForcelist policy). One approval in System Settings (a single fixed Install button — cua-driver-friendly, unlike a file dialog) → Chrome force-installs + auto-updates the extension on next launch. No token, no per-use confirmation, and binary-install users no longer need the extensions/ folder (crx is fetched from the URL). - pin a stable signing key; new extension id ciiljdlhdpfckdcfkphgmfalanpdejep - ship signed extensions/ab-connect.crx + extensions/updates.xml (raw GH host) - scripts/pack-extension.sh re-signs with the stable key; .secrets/*.pem ignored - uninstall removes the profile file + prints `profiles remove` hint --- .gitignore | 4 + cli/src/connect.rs | 144 ++++++++++++++++++++++++++-- extensions/ab-connect.crx | Bin 0 -> 7684 bytes extensions/ab-connect/manifest.json | 22 ++++- extensions/updates.xml | 13 +++ scripts/pack-extension.sh | 34 +++++++ 6 files changed, 203 insertions(+), 14 deletions(-) create mode 100644 extensions/ab-connect.crx create mode 100644 extensions/updates.xml create mode 100755 scripts/pack-extension.sh diff --git a/.gitignore b/.gitignore index 7bd71de..fc8bb44 100644 --- a/.gitignore +++ b/.gitignore @@ -67,3 +67,7 @@ docs/package-lock.json # next .next/ out/ + +# extension signing key (never commit) + local-only id record +.secrets/ +*.pem diff --git a/cli/src/connect.rs b/cli/src/connect.rs index caf28f8..11c59fb 100644 --- a/cli/src/connect.rs +++ b/cli/src/connect.rs @@ -20,8 +20,24 @@ use std::path::PathBuf; pub const HOST_NAME: &str = "com.agent_browser.connect"; /// Stable id of the `ab-connect` extension, pinned by the `key` in its -/// manifest.json. Chrome only lets that extension talk to this host. -pub const EXTENSION_ID: &str = "bdoiejojpjogcjojeladhioioijhgade"; +/// manifest.json (and the signing key of the published `.crx`). Chrome only lets +/// that extension talk to this host, and the force-install policy references it. +pub const EXTENSION_ID: &str = "ciiljdlhdpfckdcfkphgmfalanpdejep"; + +/// Omaha/gupdate update manifest for the signed `ab-connect.crx`. The macOS +/// configuration profile force-installs the extension from here, so no +/// `chrome://extensions` "Load unpacked" GUI step is ever needed. Chrome 142+ +/// removed `--load-extension`, and macOS has blocked local-`.crx` external +/// installs since Chrome 44 — a policy `update_url` is the only GUI-free path +/// left into the real, logged-in profile. +pub const UPDATE_URL: &str = + "https://raw.githubusercontent.com/leeguooooo/agent-browser-stealth/main/extensions/updates.xml"; + +/// Stable identifiers for the generated Chrome configuration profile, so a +/// re-install replaces (rather than duplicates) it in System Settings. +const PROFILE_ID: &str = "work.pwtk.agent-browser.ab-connect"; +const PROFILE_UUID: &str = "A1B2C3D4-AB00-4CCE-9E10-AAAABBBBCCCC"; +const PROFILE_PAYLOAD_UUID: &str = "A1B2C3D4-AB01-4CCE-9E10-DDDDEEEEFFFF"; /// `agent-browser extension ` (local; no daemon). /// `args` is the cleaned argv including the leading "extension". @@ -31,18 +47,40 @@ pub fn run_connect(args: &[String], json: bool) { if uninstall { let removed = remove_host_manifests(); - report(json, true, &format!("removed {removed} native-host manifest(s)")); + let profile_removed = remove_force_install_profile(); + if json { + report(json, true, &format!("removed {removed} native-host manifest(s)")); + } else { + println!("✓ removed {removed} native-host manifest(s)."); + if profile_removed { + println!("✓ removed ~/.agent-browser/ab-connect.mobileconfig"); + } + if cfg!(target_os = "macos") { + println!( + " To fully remove the extension, delete the \"agent-browser connect\" profile\n\ + in System Settings → Profiles (or run: profiles remove -identifier {PROFILE_ID})." + ); + } + } return; } if install { + let no_open = args.iter().any(|a| a == "--no-open"); match install_native_host() { Ok(paths) => { + let profile = install_force_install_profile(no_open); if json { println!( "{}", serde_json::to_string(&serde_json::json!({ "success": true, - "data": { "installed": paths, "extensionId": EXTENSION_ID } + "data": { + "installed": paths, + "extensionId": EXTENSION_ID, + "profile": profile.as_ref().ok().map(|p| p.display().to_string()), + "profileError": profile.as_ref().err(), + "updateUrl": UPDATE_URL, + } })) .unwrap_or_default() ); @@ -51,11 +89,27 @@ pub fn run_connect(args: &[String], json: bool) { for p in &paths { println!(" {p}"); } - println!( - "\nNext: load the ab-connect extension in Chrome (chrome://extensions →\n\ - Developer mode → Load unpacked → extensions/ab-connect), then this host\n\ - is reachable with no token and no per-use confirmation." - ); + match profile { + Ok(path) => { + println!("\n✓ Chrome force-install profile written:\n {}", path.display()); + if cfg!(target_os = "macos") { + println!( + "\nOne-time step (no file dialog, ever): approve the profile, then restart Chrome.\n\ + System Settings → General → Device Management (or Privacy & Security →\n\ + Profiles) → double-click \"agent-browser connect\" → Install.\n\ + After approval Chrome force-installs the extension on next launch and\n\ + keeps it up to date — no token, no per-use confirmation." + ); + } + } + Err(e) => { + println!("\n! could not write the force-install profile: {e}"); + println!( + " Fallback: load extensions/ab-connect via chrome://extensions →\n\ + Developer mode → Load unpacked." + ); + } + } } } Err(e) => report(json, false, &format!("install failed: {e}")), @@ -136,6 +190,78 @@ fn install_native_host() -> Result, String> { Ok(written) } +/// Write a Chrome configuration profile that force-installs `ab-connect` from +/// [`UPDATE_URL`], and (unless `no_open`) `open` it so the user approves it once +/// in System Settings. Returns the profile path. macOS only — elsewhere it +/// returns an error and the caller prints the manual fallback. +fn install_force_install_profile(no_open: bool) -> Result { + if !cfg!(target_os = "macos") { + return Err("force-install profile is macOS-only; on Linux set Chrome's \ + ExtensionInstallForcelist policy JSON, or Load unpacked from chrome://extensions" + .into()); + } + let home = dirs::home_dir().ok_or("no home dir")?; + let ab_dir = home.join(".agent-browser"); + std::fs::create_dir_all(&ab_dir).map_err(|e| e.to_string())?; + let path = ab_dir.join("ab-connect.mobileconfig"); + std::fs::write(&path, force_install_mobileconfig()).map_err(|e| e.to_string())?; + if !no_open { + // `open` queues the profile in System Settings for one-time approval. + let _ = std::process::Command::new("open").arg(&path).status(); + } + Ok(path) +} + +/// The `.mobileconfig` payload: a user-scope Chrome policy that force-installs +/// the extension by id from our hosted update manifest. User scope installs +/// without admin — just a one-time approval click. +fn force_install_mobileconfig() -> String { + let forcelist = format!("{EXTENSION_ID};{UPDATE_URL}"); + format!( + r#" + + + + PayloadContent + + + PayloadTypecom.google.Chrome + PayloadVersion1 + PayloadIdentifier{PROFILE_ID}.chrome + PayloadUUID{PROFILE_PAYLOAD_UUID} + PayloadEnabled + PayloadDisplayNameagent-browser connect (Chrome) + ExtensionInstallForcelist + + {forcelist} + + + + PayloadTypeConfiguration + PayloadVersion1 + PayloadIdentifier{PROFILE_ID} + PayloadUUID{PROFILE_UUID} + PayloadDisplayNameagent-browser connect + PayloadDescriptionForce-installs the agent-browser connect extension so agent-browser can drive your logged-in Chrome. No token, no per-use confirmation. + PayloadOrganizationagent-browser-stealth + PayloadScopeUser + PayloadRemovalDisallowed + + +"# + ) +} + +/// Remove the generated `.mobileconfig` file (the profile itself is removed by +/// the user from System Settings, or via `profiles remove`). +fn remove_force_install_profile() -> bool { + dirs::home_dir() + .map(|h| h.join(".agent-browser").join("ab-connect.mobileconfig")) + .filter(|p| p.exists()) + .map(|p| std::fs::remove_file(&p).is_ok()) + .unwrap_or(false) +} + fn remove_host_manifests() -> usize { let mut n = 0; for dir in native_messaging_dirs() { diff --git a/extensions/ab-connect.crx b/extensions/ab-connect.crx new file mode 100644 index 0000000000000000000000000000000000000000..dddfddc329f6bcb1fae0cfe9742fae5e26a1b6ed GIT binary patch literal 7684 zcmb7JbzD_T*FJQ2cS(qVba#3X4&B{(=#~YUa7#F&z@g>`%xg?|)Hv6A#5mutvsbn+(&n1uHluDf)&q+hywt^nKAJ+On z^A!i=pQOIA_>ffCjoXo0U#IAG6LP8lsi33DJO?U{pK}hRMniSjsiWEA8k<@D27^)L zrAXh%&K*033cWd@(P|6Sv`9=fIFnB@dkC^dMZBFc1Y(q(Ixu z-Bfh`HR0K`W3(A3IY?EjRR~9%%yQXAO0ol2^k~N34a3r9Osr~f4Ef5o^tR*0a1Tc4 zw_$XSUaFXvLkY=#t->xPM;z^s##*n>iO1#)H~f^OD#!wgr(GPv9y)X6vPH5(2_V=h zC5lVDZVg(}thHJe@^bBi8$BMXImNaHVL`={IVB6tYj>1T$tzmUZDE(@f7t$t&4v#SlIhKs1l*qN~)>OBT6h4mmIO#2wek4cjl4} z##&Z{0_J1psjx2&_OEk0(?QSK)n<+wwGBea6&V>*i34vpCH9vqOoF8X9x1*k9~CH$ z8SxO#04+JNpW|aq5lMqp6R>P8iA0U;YUL{(ogl1dpVS#k{W4v`9T=tN3y|e~%;rN; zlJs0-D&;Mv)O*%KQB*&?ex)nLl!9XL(88?nV|K8n={Q!Oo+K@s_AHU&BSmYfuns_L z4NIIVH~t#!x(Kf5ysE;lLspuNIn)Spw1QJ~qkW06KR-Z;Bbp`ix!fk| zPcdpw&2d6PK-TV|%4>!yf?ecM7tgLPOzxNzO_86qGgrztsr#SyN^i1{U2oRtsEqp! zJh-!jr+7O((|;N`*d+nNBp2=?lAsi2%g1{kic?9hY&0$MReuwK@}^g@4bOH?v*}uJ zBC+{Fo1W4j}6lUa1Jq;@V-9Pvr5le2E4Z+UJTugSVmMCy0GSeQTiRQg<8B#}T|6 z^i>H=-i~OXF#w`(W4i|}uQhD7JRZ-;y>RJjt52IP+_h%?`H2cgj@uE=L+8P`r_a$orD~T|`4oL@N%qvDT5R{w z%olKK0Fb*6;V(=J7|$-kQwyzn{upb@TdC2S&#GWWF&s7MA5^ko)l*RHDA zdByA!579#Rr_`vQbx&^+4h{_sp~z=W-CVQ;;Y%Ua3Dq9J+w;j4me+3YKO!|8;za1l zr2^}%h{ro<+1DJ;ZA&M)_LW$w4urVgGMlj}eodeJ5mT|KjfOo-)Hm64&hcd~FKOXa zQ+UXrS=+rS(P`7Q`mF}4yp7=-u2d~l))yEwH!q$SC7lbRVdWi&B6A}s%9l-OFN$<2 zFSK=cg=pm2t&bwFp-;bc;lwBcPKT6!TFxULv>_5`;L*#$DRY`5hfbk|2C5}?zE`yK z`bLsyUSaf79gL@26xC8`UO%NOkgvrp>c_^^NgicDuVH~%s$!05<(@n;2yzMBNq&#s z88H%>p3Ro8)|<~l(IRS)$v8I;7lu*`@Dpxo2(HSw3#C^ts2+RRJcmnJx`^!B$?6&- z7p#|QwWNX9khezBohOe+F7YX$kt9|?k%OXeuXx)7eMppoC(d_K(2bx zyr}`xwxlTn@-V1e61}}e$-dW>?!h6K|0&rqknBMX7O=b~3bbJJ-UScU%@-$-0C3I( z=n~{l%wMdL)K&5;q%`>c#`Bw_pPOqWV=*799a}PE_JvSf2O}2AH+F{p#kg^44l$QG zcKP0oFGkp8+>RXmpGVw+9*cXdrw+8K2{)9N0a>p{n)`)&fmO1Zpr&R@YYK&RK^kAu z?X9sZBL_}_X`?2O|2O?H7)FMAV(qJ+MI8oZ z<~vJK@2Vp%EJ46hb5(c4cn+4T3sr*@@^rboVciyqr{$%mga-g8PR5&>p$?I=AKU@2 z+ozCfrkl+Vt+ONz(gdI3B#myC^qzh^Lu7Gif|9Z6T5%2>46XqOLml#Mu3uwwekb$tlV#X?oDY!Lix=H#nhZ?auab(eWxEk5(S)*Hb3?>K$O?W4-xP0!E zG0?l~6X^&{G0WkOs;AI<$5Sgs1OulCPr{RpUXFfFCz$G#PN|im_MMrx3F=YltAid% zj;Jq0jHSFI$@8`!nl&T=-hLha@ou%XNcLgqd#9HKW6^3V=uV;hqWthe_@OO^EWwv@ zXJ5nPgP*5CZ7ssX++!(_;Ly@-R-5@ z9ZWE!dmm_SM2D;LkbPkj4{uZn2rIgc@>KrnXHh0!H<%z71JG@t+PC$a8cAZQ@1P}` z7N6LFQ5FpZ1q<}2OoE*rk0+eJWS-FfE<0+9> zbAjNQkpQ>jmkqZ(=W~ZT1dEbRK81I6S;BAO`sWD*HSz~pg(BlV8|2%M5h8s!zFLja z{GjV#8Wttz*%kfv3sv^f%!J%YMLWQM{ok>LeM%5cVAmb=VkzPe3`b8pMXg}>5*ifp+`=;wnG|(Zb ze(id$`r^X*6zF}s)Y&;Z5%8q?Bt`?v?+|HKOX--Epe>|sOS6WlT$6d&Coj;TzxI;D ztZemlQmJt=8~a_h7oS+(H>^pYb|kYry`W_$J4$B$Ckp4ZB+ft~IH*V1Zc*1LfY|w{IRSGl-qY?G zSgM}OsJ$v$@q906L*7L|Q0d2=Ug0;ZtKER^%FZdUf3iNOqTg(0IS0-LC3YZGsVrj_ zokf{;sYULz(*Jngo!|RmN0$1WaWg_uK23ySSXEKx27QOu)pINr6;BJ4?2n6Z3H+mh zNaaqTE9xDm^|{%W`ANr5)D>Wt^QZ&gWzBTqBS&EG@k1R{nX#xSH;Fx&4b{R{!{TC%)9KA|Pb@Mq|<4d z)DYA?ynA8N#I#;@l-qhk{lFb$?n__Vs6L@Pz`tMFf^%b=&Q~2RaHnXjHe#R^5Rx#G z;qQ?6UFMB|X53_^NhCpQz3QPIV_#+hbA%w@b^J*1)p;pD#jC8=5HoyT=rMWScDS(a z?vb3i?Uj}!<7(WM@l&_gJZBdx(uI0NSS5(VbE2^nuCXN*jZ*A{ZAp(`l(A=!`5XHK zL3vHuz0AidXNhHMzUn^OPwM1@NM@a0`dr~Og!5EvWaG%rJ)Ptf^Gn-n9&bN7Oz5X~ zWYu0))XM7fM~a!5AaAEAcEHGSFx+XdzBM3ZGAsIug{pTvHjp|Pwr$ynkq+IR^e$+I zoA5_lP1gKGiDC_5@jEps2uDtIxO;8>Rc!eV@K?$Qe9~)prq}Ty6SkXh1>07*Px-)R zPIlH-?jEp&&)MnUQoev>HTMEe-0p=NLNenPR(jeUio6{)_Gsermc~uQNG|CRCOAbA z0e6~Z@uh$yievrRXANonOl|Ww&ehYC!~pcLSjWUf6~^U|Ri#Gwo5xtDiS%peEVv9z zkLh!C;9YUQ$L8y@Dx|qb7I;<;C+1@$5V_k|QXfKig!B$SpypII7dF&hkW zG!O&tu8~j)@U6*u^kQ29sR-%sBB{#7mPx`SmB}bN9y-bjGfxwa$IWGb7y34MN|i-j z_pq_COtTwXezH!?a|cpQvAbO5fWk1#hEPF#5GP#?Tn7M1 zZl){8#7E3q@1$pobt>opUbUX4pVlU5){AS*Up{#@Bhm0K+Xo|2^u2k>3-+*xsEmZ5t$EufPf7 z8p24;T9BqSP9hc45>ml)j+S`l>9t2@)glp3H;_y(^L-0)z8BomI&X;(qa+Tbues|7 z6D(YgTq6AC**ZeKhPh?+7J6_106EON(O{kpQqcs;$Z>!z|IMqxFWW(LoVek8fzc+% z@Mgf>;}^u(VzgRTjPcuD)Q&a1N|q|RrxOty1cwi_)Md3w1RS<6ZkaDBY(E(SfnkN7gAzaz|y6cS12ba{DuWgtdA}s)rgLhDxS&=mKP~pZsmkEU2oej zW?IgK>qmw^i_J;kGxlIRi$beiNEB;ZWUk#@}y>X4j7|5$OcJc z@0biZK|O@ozf=r5m=aQ-q{wUq{$i8Y9KDA3yGJl{{85l$HnFpCc5;9GZw5j5lcxZH zdt|GJS1KCJn)em=PvkwGi=V^A$>!hsh*AY9V4;(v!^B|$pUA4i?)O_*xTwgmUz(#k zGuRE;{HcO0;O_p)Yk8Lpd(sM&mC_W9voIyKg+y|gLZMeQx#1QI;)NShOS-NHw-Tp2 zJS#y#C}rWCPNEQ04Fm#(bCk^R1yc}a~ulk$hayLgAf6L zE^qJQk4{_(n+X6Y2a^3DZ5s05h4E9BQqwVUZZI|8*WXpiEBHUCk`pF?^AA<(z+Xhb z8vjrw9P_;@ZJ)zbiBxDx7#s2&G4_pp>0Q59GQUnFgEmi;nvA_0`5_k!7>v)} zqvpjcfJ*1OLMIg}h+`aD(>CkyvK$%7wx=_FB6+fXx~JE!ryG)Z22jMmG@(vtF{h_T z8$qOHq_c!fHM;PkVUa^Z`lSXpS&D{f0X)K5#b$>jCOoIxgNsfA)P|Ei1V;Y^u&{>3e%xDekGPhep45d zI4kjHCJZy&9~0I~thOZr7HnGb?JRjPm$aL6YabTlqk}2&|FN0+gPSyK*vh%Dc)z

s9KaAEXZuSM%_=nNpDejHtleIH17ELGx2p%Eha&6~%9bJ-6QQZDH+j^k!~H*y3oT3l?H_*X?&o(5Yy87c0Y~?K zsvCAsE(}zh`0Dpeor{kVA8-{riOr;l`3@PfDg1iKO#B?iDC$q{$vSd?(u4C{IyiBt zt$cb2Mz;_M(Q=PutYCg3sVE)5eF;X`k&w6uT|O7;=Fj)9_vejcSX>2%6K!BjYg(Dp z9RK_|93FlsGvaMpci~$%N;*mY;Bdg)@?cmf1EQ`ertTbM3=jwCrzR!c$E9tgq@50- z)4(ghb-d2`LJ)=D(_3#(&Ch zM>?uc|APKA8Tg6#2|a?H3jLNE{0jYNwDl`=07h_s#$La|{uvPbcaZmYGkh2?{t+Jh zh55Ho;8#rMpP0V_gTJGH|M&Y9y-x67iTCKA|Al`?|IWj|qBDqoMRWaR<-fy!Umd@~ cWk`R8|6D7oPm%5;6#y#int|D&lkC3rf4?R4M*si- literal 0 HcmV?d00001 diff --git a/extensions/ab-connect/manifest.json b/extensions/ab-connect/manifest.json index cf1eb00..3ba7998 100644 --- a/extensions/ab-connect/manifest.json +++ b/extensions/ab-connect/manifest.json @@ -1,16 +1,28 @@ { "manifest_version": 3, "name": "agent-browser connect", - "version": "0.2.0", + "version": "0.3.0", "description": "Let agent-browser drive your logged-in Chrome — install once, no token, no per-use confirmation.", - "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvPGcSmMx7dSfq9gRBDbQuAgx/+TEavrDxP/4jLa2+Ycexf/FEmq1MN8gAHoTRjSyp66YKD+1qI1CF6bk0rH5ZtxpRO7DYRTUPcsA1IpHbgEn5mppx3YxNGZilfkEZyrxdhBqUIzq3J74+/kpZzEVsO+DQTbAZSsFfdUkoCb5mbJid2VQYeqeBnYGAhbpGvN1P99jdT9EA1nKINb3ji6tLobCpyQ1fjf2uWm4mUirWkkF/nbUFVFEAh33Q/IYZmtUHgDYea5LsM9xH4KAG2kxMvFGj6vHR39sZd5/+gnvwScTcItUWQ9lFIyWYiwrSB25Lu0FshfllevXUFrG5vrvRwIDAQAB", + "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6vQIyscGIPYPZdSpPwPL0+0gxUROyRgCpmvCSDoc8XUm4qm97VbKnD9Ijc1lV22lNWZtE78gaRjt6BeSfuMgnBymnhLKjN1gU6AI5QUU0mrJyeHdWKvrKQR5FmsM2A7Xr1ykE2SiiS8zNUS3Y/6O5l+Nva7wrVy6E4a2dkBVQkOsu+DV+nEZvhIyuDY5D5SPXqNwUTWTaglwj5mjvHz36xSwCWlPmrtJ+ED0AUyrb2z4GIOmvk4kqtBVrh/UD058klLo4CkYOnIybB5aV6WYuwarfPY4bF/dLggPem+ewLNTUNBuwrxj/A4nUv0LJTuRO8rR7f8WR9qnRCY0Ic5saQIDAQAB", "icons": { "16": "icons/icon16.png", "32": "icons/icon32.png", "48": "icons/icon48.png", "128": "icons/icon128.png" }, - "permissions": ["debugger", "tabs", "nativeMessaging", "storage", "alarms", "webNavigation"], - "background": { "service_worker": "background.js", "type": "module" }, - "action": { "default_title": "agent-browser connect" } + "permissions": [ + "debugger", + "tabs", + "nativeMessaging", + "storage", + "alarms", + "webNavigation" + ], + "background": { + "service_worker": "background.js", + "type": "module" + }, + "action": { + "default_title": "agent-browser connect" + } } diff --git a/extensions/updates.xml b/extensions/updates.xml new file mode 100644 index 0000000..fdc8b88 --- /dev/null +++ b/extensions/updates.xml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/scripts/pack-extension.sh b/scripts/pack-extension.sh new file mode 100755 index 0000000..10b7bed --- /dev/null +++ b/scripts/pack-extension.sh @@ -0,0 +1,34 @@ +#!/bin/sh +# Re-pack and sign extensions/ab-connect into extensions/ab-connect.crx using the +# stable signing key, then print the extension id. Keeps the crx id (and thus the +# native-messaging allowed_origins + force-install policy) constant across versions. +# +# The private key lives at .secrets/ab-connect.pem and is git-ignored. To re-pack +# on another machine / in CI, restore it from a secret first (see RELEASING). +# +# After changing the extension: +# 1. bump "version" in extensions/ab-connect/manifest.json +# 2. bump in extensions/updates.xml to match +# 3. run this script +# 4. commit extensions/ab-connect.crx + updates.xml + manifest.json +set -e +cd "$(dirname "$0")/.." +KEY=.secrets/ab-connect.pem +EXT=extensions/ab-connect +CHROME="${CHROME_BIN:-/Applications/Google Chrome.app/Contents/MacOS/Google Chrome}" + +if [ ! -f "$KEY" ]; then + echo "error: $KEY missing. Restore the signing key (CI secret AB_CONNECT_PEM) before packing." >&2 + exit 1 +fi + +rm -f extensions/ab-connect.crx +"$CHROME" --pack-extension="$PWD/$EXT" --pack-extension-key="$PWD/$KEY" >/dev/null 2>&1 || true +[ -f extensions/ab-connect.crx ] || { echo "error: pack failed" >&2; exit 1; } + +ID=$(openssl rsa -in "$KEY" -pubout -outform DER 2>/dev/null \ + | openssl dgst -sha256 -binary | xxd -p -c256 | head -c32 | tr '0-9a-f' 'a-p') +echo "packed extensions/ab-connect.crx" +echo "extension id: $ID" +echo "manifest version: $(grep -o '"version"[^,]*' "$EXT/manifest.json" | head -1)" +echo "updates.xml version: $(grep -o "version='[^']*'" extensions/updates.xml | tail -1)"