openship 0.4.4 → 0.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (318) hide show
  1. package/README.md +2 -24
  2. package/dist/bare-2HYDXQEH.js +23 -0
  3. package/dist/{bowser-MFQA6OS4.js → bowser-O3AEA6AN.js} +5 -1
  4. package/dist/{chunk-KD7VO4LG.js → chunk-2ZLR7ULL.js} +255 -65
  5. package/dist/{chunk-YHXCHW7Q.js → chunk-3JQTL4R4.js} +236 -99
  6. package/dist/{chunk-E77LJWJK.js → chunk-4TJC4JZQ.js} +20 -9
  7. package/dist/{chunk-YL67EWYZ.js → chunk-4Z5QO7N6.js} +6 -2
  8. package/dist/{chunk-WJPCBU7Z.js → chunk-5FNIWBFI.js} +10 -6
  9. package/dist/{chunk-Y37KHU7L.js → chunk-5MZ624VX.js} +5 -0
  10. package/dist/{chunk-QKFRZNKN.js → chunk-6APIPIJY.js} +8 -4
  11. package/dist/{chunk-JZLNHGLW.js → chunk-6APULWSU.js} +153 -133
  12. package/dist/{chunk-F4DFVG6Q.js → chunk-6B5YCFFV.js} +8 -4
  13. package/dist/{chunk-XVSAUO2I.js → chunk-AVIB7CT7.js} +5 -1
  14. package/dist/{chunk-TEYJYSMT.js → chunk-C5OHTYEF.js} +12 -6
  15. package/dist/{chunk-7UOB5GGQ.js → chunk-DYPH4HCI.js} +5 -0
  16. package/dist/{chunk-XIFRMKYT.js → chunk-EI575GHN.js} +6 -2
  17. package/dist/{chunk-4CQWT2WL.js → chunk-FPRHYBY2.js} +5 -0
  18. package/dist/chunk-FQGJZ2HS.js +248 -0
  19. package/dist/{chunk-JR2OCMJA.js → chunk-FRNAR33O.js} +6 -2
  20. package/dist/{chunk-JUJBBF5U.js → chunk-FXTBHTWY.js} +362 -42
  21. package/dist/chunk-ILFETZOM.js +131 -0
  22. package/dist/{chunk-5ZCKQ2MI.js → chunk-IXCHURVK.js} +7 -3
  23. package/dist/{chunk-6ERIMTBC.js → chunk-JYGD5WSN.js} +5 -0
  24. package/dist/{chunk-4VNS5WPM.js → chunk-KI2EY3WB.js} +4 -0
  25. package/dist/{chunk-3U4SPZD5.js → chunk-L2VOGB2X.js} +7 -104
  26. package/dist/{chunk-NB2VEIWG.js → chunk-MZ2WEHHY.js} +7 -3
  27. package/dist/{chunk-BUBSSKO6.js → chunk-O6ZZF52V.js} +5 -1
  28. package/dist/{chunk-LBFH7THC.js → chunk-OWED2GRM.js} +5 -1
  29. package/dist/{chunk-4MG3BVHE.js → chunk-OXWDY22G.js} +6 -2
  30. package/dist/{chunk-4FABQGQP.js → chunk-Q2CIRAOK.js} +5 -0
  31. package/dist/{chunk-4LEL3H2Z.js → chunk-RPHRPFEH.js} +5 -0
  32. package/dist/{chunk-46YJFP4S.js → chunk-SQ77DZOI.js} +1728 -1484
  33. package/dist/{chunk-S6DSRLNE.js → chunk-TGIINPVM.js} +15 -2
  34. package/dist/{chunk-U2DPZSXA.js → chunk-TYVME3XD.js} +9 -1
  35. package/dist/{chunk-HFYDYK4D.js → chunk-V6AN6XLZ.js} +18 -6
  36. package/dist/{chunk-ZKJOFWBJ.js → chunk-VDMQJHPI.js} +38 -15
  37. package/dist/{chunk-E6W2JMMU.js → chunk-VFINNTUL.js} +22 -1
  38. package/dist/{chunk-OJE6YWPQ.js → chunk-VWPEKOH6.js} +6 -2
  39. package/dist/{chunk-5N7VUGL6.js → chunk-W73X43R7.js} +6 -2
  40. package/dist/cloud-7QYOGF7F.js +11 -0
  41. package/dist/cloud-ZSKOS4GY.js +30 -0
  42. package/dist/detect-SH4FHFBI.js +36 -0
  43. package/dist/{dist-L3MPOWCA.js → dist-IQJ6H6UZ.js} +6 -2
  44. package/dist/{dist-es-KE56MWYO.js → dist-es-5DY7K32L.js} +9 -5
  45. package/dist/dist-es-JSTDUS7Q.js +26 -0
  46. package/dist/{dist-es-GTAMF674.js → dist-es-OSNJBLXB.js} +8 -4
  47. package/dist/{dist-es-NRZIQY5B.js → dist-es-RYHIPOTN.js} +17 -13
  48. package/dist/{dist-es-XQCOTGC7.js → dist-es-VNKHQVIB.js} +7 -3
  49. package/dist/{dist-es-WZCR7DHQ.js → dist-es-WV4AMOII.js} +11 -7
  50. package/dist/{dist-es-ZW3HFQUM.js → dist-es-X3B5QYKB.js} +9 -5
  51. package/dist/docker-U7AF5PTA.js +16 -0
  52. package/dist/docker-edge-executor-VHW3JPJS.js +13 -0
  53. package/dist/edge-import-V5CBA47C.js +45 -0
  54. package/dist/ensure-container-edge-CMYROUSI.js +33 -0
  55. package/dist/{event-streams-7LGDL56D.js → event-streams-EHRAKL75.js} +7 -3
  56. package/dist/executor-YPT6RH6O.js +30 -0
  57. package/dist/index.d.ts +2 -1
  58. package/dist/index.js +258 -245
  59. package/dist/{loadSso-N273C5I7.js → loadSso-46FQRIZS.js} +13 -9
  60. package/dist/local-executor-V7U5VOZW.js +12 -0
  61. package/dist/nginx-2GTRUIDE.js +24 -0
  62. package/dist/noop-FMMFG2EZ.js +11 -0
  63. package/dist/{openresty-lua-HAO4EUDY.js → openresty-lua-4WLGDSER.js} +13 -3
  64. package/dist/server/engine/ChangeLog +2289 -0
  65. package/dist/server/engine/Documentations +5 -0
  66. package/dist/server/engine/LICENSE +674 -0
  67. package/dist/server/engine/README.md +34 -0
  68. package/dist/server/engine/conf/amavisd +150 -0
  69. package/dist/server/engine/conf/clamav +71 -0
  70. package/dist/server/engine/conf/core +632 -0
  71. package/dist/server/engine/conf/dovecot +151 -0
  72. package/dist/server/engine/conf/fail2ban +72 -0
  73. package/dist/server/engine/conf/global +698 -0
  74. package/dist/server/engine/conf/iredapd +52 -0
  75. package/dist/server/engine/conf/logwatch +28 -0
  76. package/dist/server/engine/conf/postfix +97 -0
  77. package/dist/server/engine/conf/postgresql +93 -0
  78. package/dist/server/engine/conf/spamassassin +39 -0
  79. package/dist/server/engine/dialog/config_via_dialog.sh +258 -0
  80. package/dist/server/engine/dialog/optional_components.sh +122 -0
  81. package/dist/server/engine/dialog/pgsql_config.sh +52 -0
  82. package/dist/server/engine/dialog/virtual_domain_config.sh +94 -0
  83. package/dist/server/engine/functions/amavisd.sh +267 -0
  84. package/dist/server/engine/functions/backend.sh +85 -0
  85. package/dist/server/engine/functions/clamav.sh +100 -0
  86. package/dist/server/engine/functions/cleanup.sh +393 -0
  87. package/dist/server/engine/functions/dovecot.sh +583 -0
  88. package/dist/server/engine/functions/fail2ban.sh +209 -0
  89. package/dist/server/engine/functions/iredapd.sh +356 -0
  90. package/dist/server/engine/functions/optional_components.sh +24 -0
  91. package/dist/server/engine/functions/packages.sh +522 -0
  92. package/dist/server/engine/functions/packages_freebsd.sh +179 -0
  93. package/dist/server/engine/functions/postfix.sh +457 -0
  94. package/dist/server/engine/functions/postgresql.sh +239 -0
  95. package/dist/server/engine/functions/spamassassin.sh +49 -0
  96. package/dist/server/engine/functions/system_accounts.sh +128 -0
  97. package/dist/server/engine/iRedMail.sh +144 -0
  98. package/dist/server/engine/pkgs/get_all.sh +338 -0
  99. package/dist/server/engine/pkgs/pkgs.freebsd.sha256 +3 -0
  100. package/dist/server/engine/pkgs/pkgs.openbsd.sha256 +6 -0
  101. package/dist/server/engine/pkgs/pkgs.sha256 +1 -0
  102. package/dist/server/engine/samples/amavisd/amavisd.conf +677 -0
  103. package/dist/server/engine/samples/amavisd/amavisd.mysql +278 -0
  104. package/dist/server/engine/samples/amavisd/amavisd.pgsql +253 -0
  105. package/dist/server/engine/samples/amavisd/amavisd.schema +942 -0
  106. package/dist/server/engine/samples/amavisd/default_spam_policy.sql +46 -0
  107. package/dist/server/engine/samples/dovecot/dovecot-2.4-mariadb.conf +758 -0
  108. package/dist/server/engine/samples/dovecot/dovecot-2.4-openldap.conf +767 -0
  109. package/dist/server/engine/samples/dovecot/dovecot-2.4-pgsql.conf +762 -0
  110. package/dist/server/engine/samples/dovecot/dovecot-last-login.conf +50 -0
  111. package/dist/server/engine/samples/dovecot/dovecot-ldap.conf +21 -0
  112. package/dist/server/engine/samples/dovecot/dovecot-share-folder.conf +22 -0
  113. package/dist/server/engine/samples/dovecot/dovecot-sql.conf +28 -0
  114. package/dist/server/engine/samples/dovecot/dovecot-used-quota.conf +13 -0
  115. package/dist/server/engine/samples/dovecot/dovecot.conf +484 -0
  116. package/dist/server/engine/samples/dovecot/dovecot.sieve +7 -0
  117. package/dist/server/engine/samples/dovecot/dovecot2-quota-warning.sh +38 -0
  118. package/dist/server/engine/samples/dovecot/sql/imap_share_folder.mysql +16 -0
  119. package/dist/server/engine/samples/dovecot/sql/last_login.mysql +12 -0
  120. package/dist/server/engine/samples/dovecot/sql/used_quota.mysql +26 -0
  121. package/dist/server/engine/samples/dovecot/systemd/override.conf +3 -0
  122. package/dist/server/engine/samples/fail2ban/action.d/banned_db.conf +9 -0
  123. package/dist/server/engine/samples/fail2ban/bin/fail2ban_banned_db +268 -0
  124. package/dist/server/engine/samples/fail2ban/fail2ban.local +17 -0
  125. package/dist/server/engine/samples/fail2ban/filter.d/dovecot.iredmail.conf +5 -0
  126. package/dist/server/engine/samples/fail2ban/filter.d/postfix-pregreet.iredmail.conf +16 -0
  127. package/dist/server/engine/samples/fail2ban/filter.d/postfix.iredmail.conf +18 -0
  128. package/dist/server/engine/samples/fail2ban/filter.d/roundcube.iredmail.conf +9 -0
  129. package/dist/server/engine/samples/fail2ban/jail.d/dovecot.local +8 -0
  130. package/dist/server/engine/samples/fail2ban/jail.d/nginx-http-auth.local +8 -0
  131. package/dist/server/engine/samples/fail2ban/jail.d/postfix-pregreet.local +9 -0
  132. package/dist/server/engine/samples/fail2ban/jail.d/postfix.local +8 -0
  133. package/dist/server/engine/samples/fail2ban/jail.d/roundcube.local +9 -0
  134. package/dist/server/engine/samples/fail2ban/jail.d/sogo.local +8 -0
  135. package/dist/server/engine/samples/fail2ban/jail.d/sshd.local +8 -0
  136. package/dist/server/engine/samples/fail2ban/jail.local +6 -0
  137. package/dist/server/engine/samples/fail2ban/openbsd/rc +27 -0
  138. package/dist/server/engine/samples/fail2ban/sql/fail2ban.mysql +49 -0
  139. package/dist/server/engine/samples/fail2ban/sql/fail2ban.pgsql +48 -0
  140. package/dist/server/engine/samples/firewall/firewalld/services/ssh.xml +6 -0
  141. package/dist/server/engine/samples/firewall/firewalld/zones/iredmail.xml +15 -0
  142. package/dist/server/engine/samples/firewall/iptables/ip6tables.init.debian +362 -0
  143. package/dist/server/engine/samples/firewall/iptables/ip6tables.rules +53 -0
  144. package/dist/server/engine/samples/firewall/iptables/iptables.init.debian +363 -0
  145. package/dist/server/engine/samples/firewall/iptables/iptables.rules +54 -0
  146. package/dist/server/engine/samples/firewall/nftables.conf +58 -0
  147. package/dist/server/engine/samples/freebsd/newsyslog.conf.d/dovecot.conf +3 -0
  148. package/dist/server/engine/samples/freebsd/newsyslog.conf.d/iredapd.conf +1 -0
  149. package/dist/server/engine/samples/freebsd/newsyslog.conf.d/mlmmjadmin.conf +1 -0
  150. package/dist/server/engine/samples/freebsd/newsyslog.conf.d/php-fpm.conf +2 -0
  151. package/dist/server/engine/samples/freebsd/newsyslog.conf.d/slapd.conf +1 -0
  152. package/dist/server/engine/samples/freebsd/syslog.d/dovecot.conf +2 -0
  153. package/dist/server/engine/samples/freebsd/syslog.d/fail2ban.conf +2 -0
  154. package/dist/server/engine/samples/freebsd/syslog.d/iredapd.conf +2 -0
  155. package/dist/server/engine/samples/freebsd/syslog.d/mlmmjadmin.conf +2 -0
  156. package/dist/server/engine/samples/freebsd/syslog.d/php-fpm.conf +2 -0
  157. package/dist/server/engine/samples/freebsd/syslog.d/slapd.conf +2 -0
  158. package/dist/server/engine/samples/iredmail/iredmail.mysql +491 -0
  159. package/dist/server/engine/samples/iredmail/iredmail.pgsql +465 -0
  160. package/dist/server/engine/samples/iredmail/iredmail.schema +456 -0
  161. package/dist/server/engine/samples/iredmail/used_quota_triggers_dovecot_2.3.pgsql +40 -0
  162. package/dist/server/engine/samples/iredmail/used_quota_triggers_dovecot_2.4.pgsql +18 -0
  163. package/dist/server/engine/samples/logrotate/dovecot +13 -0
  164. package/dist/server/engine/samples/logrotate/iredapd +13 -0
  165. package/dist/server/engine/samples/logrotate/mlmmjadmin +13 -0
  166. package/dist/server/engine/samples/logrotate/openldap +19 -0
  167. package/dist/server/engine/samples/logrotate/php-fpm +13 -0
  168. package/dist/server/engine/samples/openbsd/ldapd.conf +58 -0
  169. package/dist/server/engine/samples/openbsd/pf.conf +28 -0
  170. package/dist/server/engine/samples/postfix/command_filter.pcre +3 -0
  171. package/dist/server/engine/samples/postfix/freebsd/mailer.conf +9 -0
  172. package/dist/server/engine/samples/postfix/helo_access.pcre +180 -0
  173. package/dist/server/engine/samples/postfix/ldap/catchall_maps.cf +11 -0
  174. package/dist/server/engine/samples/postfix/ldap/recipient_bcc_maps_domain.cf +11 -0
  175. package/dist/server/engine/samples/postfix/ldap/recipient_bcc_maps_user.cf +11 -0
  176. package/dist/server/engine/samples/postfix/ldap/relay_domains.cf +11 -0
  177. package/dist/server/engine/samples/postfix/ldap/sender_bcc_maps_domain.cf +11 -0
  178. package/dist/server/engine/samples/postfix/ldap/sender_bcc_maps_user.cf +11 -0
  179. package/dist/server/engine/samples/postfix/ldap/sender_dependent_relayhost_maps_domain.cf +11 -0
  180. package/dist/server/engine/samples/postfix/ldap/sender_dependent_relayhost_maps_user.cf +11 -0
  181. package/dist/server/engine/samples/postfix/ldap/sender_login_maps.cf +11 -0
  182. package/dist/server/engine/samples/postfix/ldap/transport_maps_domain.cf +11 -0
  183. package/dist/server/engine/samples/postfix/ldap/transport_maps_user.cf +11 -0
  184. package/dist/server/engine/samples/postfix/ldap/virtual_alias_maps.cf +11 -0
  185. package/dist/server/engine/samples/postfix/ldap/virtual_group_maps.cf +11 -0
  186. package/dist/server/engine/samples/postfix/ldap/virtual_group_members_maps.cf +11 -0
  187. package/dist/server/engine/samples/postfix/ldap/virtual_mailbox_domains.cf +11 -0
  188. package/dist/server/engine/samples/postfix/ldap/virtual_mailbox_maps.cf +12 -0
  189. package/dist/server/engine/samples/postfix/main.cf +282 -0
  190. package/dist/server/engine/samples/postfix/main.cf.amavisd +7 -0
  191. package/dist/server/engine/samples/postfix/main.cf.dovecot +8 -0
  192. package/dist/server/engine/samples/postfix/main.cf.ldap +39 -0
  193. package/dist/server/engine/samples/postfix/main.cf.mlmmj +5 -0
  194. package/dist/server/engine/samples/postfix/main.cf.mysql +39 -0
  195. package/dist/server/engine/samples/postfix/main.cf.pgsql +39 -0
  196. package/dist/server/engine/samples/postfix/main.cf.postscreen +30 -0
  197. package/dist/server/engine/samples/postfix/master.cf +87 -0
  198. package/dist/server/engine/samples/postfix/mysql/catchall_maps.cf +5 -0
  199. package/dist/server/engine/samples/postfix/mysql/domain_alias_catchall_maps.cf +5 -0
  200. package/dist/server/engine/samples/postfix/mysql/domain_alias_maps.cf +5 -0
  201. package/dist/server/engine/samples/postfix/mysql/recipient_bcc_maps_domain.cf +5 -0
  202. package/dist/server/engine/samples/postfix/mysql/recipient_bcc_maps_user.cf +5 -0
  203. package/dist/server/engine/samples/postfix/mysql/relay_domains.cf +5 -0
  204. package/dist/server/engine/samples/postfix/mysql/sender_bcc_maps_domain.cf +5 -0
  205. package/dist/server/engine/samples/postfix/mysql/sender_bcc_maps_user.cf +5 -0
  206. package/dist/server/engine/samples/postfix/mysql/sender_dependent_relayhost_maps.cf +6 -0
  207. package/dist/server/engine/samples/postfix/mysql/sender_login_maps.cf +5 -0
  208. package/dist/server/engine/samples/postfix/mysql/transport_maps_domain.cf +5 -0
  209. package/dist/server/engine/samples/postfix/mysql/transport_maps_maillist.cf +5 -0
  210. package/dist/server/engine/samples/postfix/mysql/transport_maps_user.cf +5 -0
  211. package/dist/server/engine/samples/postfix/mysql/virtual_alias_maps.cf +5 -0
  212. package/dist/server/engine/samples/postfix/mysql/virtual_mailbox_domains.cf +5 -0
  213. package/dist/server/engine/samples/postfix/mysql/virtual_mailbox_maps.cf +5 -0
  214. package/dist/server/engine/samples/postfix/pgsql/catchall_maps.cf +5 -0
  215. package/dist/server/engine/samples/postfix/pgsql/domain_alias_catchall_maps.cf +5 -0
  216. package/dist/server/engine/samples/postfix/pgsql/domain_alias_maps.cf +5 -0
  217. package/dist/server/engine/samples/postfix/pgsql/recipient_bcc_maps_domain.cf +5 -0
  218. package/dist/server/engine/samples/postfix/pgsql/recipient_bcc_maps_user.cf +5 -0
  219. package/dist/server/engine/samples/postfix/pgsql/relay_domains.cf +5 -0
  220. package/dist/server/engine/samples/postfix/pgsql/sender_bcc_maps_domain.cf +5 -0
  221. package/dist/server/engine/samples/postfix/pgsql/sender_bcc_maps_user.cf +5 -0
  222. package/dist/server/engine/samples/postfix/pgsql/sender_dependent_relayhost_maps.cf +6 -0
  223. package/dist/server/engine/samples/postfix/pgsql/sender_login_maps.cf +5 -0
  224. package/dist/server/engine/samples/postfix/pgsql/transport_maps_domain.cf +5 -0
  225. package/dist/server/engine/samples/postfix/pgsql/transport_maps_maillist.cf +5 -0
  226. package/dist/server/engine/samples/postfix/pgsql/transport_maps_user.cf +5 -0
  227. package/dist/server/engine/samples/postfix/pgsql/virtual_alias_maps.cf +5 -0
  228. package/dist/server/engine/samples/postfix/pgsql/virtual_mailbox_domains.cf +5 -0
  229. package/dist/server/engine/samples/postfix/pgsql/virtual_mailbox_maps.cf +5 -0
  230. package/dist/server/engine/samples/postfix/postscreen_access.cidr +6 -0
  231. package/dist/server/engine/samples/postgresql/sql/add_first_domain_and_user.sql +34 -0
  232. package/dist/server/engine/samples/postgresql/sql/grant_permissions.sql +14 -0
  233. package/dist/server/engine/samples/postgresql/sql/init_vmail_db.sql +15 -0
  234. package/dist/server/engine/samples/rsyslog.d/1-iredmail-dovecot.conf +23 -0
  235. package/dist/server/engine/samples/rsyslog.d/1-iredmail-fail2ban.conf +2 -0
  236. package/dist/server/engine/samples/rsyslog.d/1-iredmail-iredapd.conf +12 -0
  237. package/dist/server/engine/samples/rsyslog.d/1-iredmail-mlmmjadmin.conf +12 -0
  238. package/dist/server/engine/samples/rsyslog.d/1-iredmail-openldap.conf +2 -0
  239. package/dist/server/engine/samples/rsyslog.d/1-iredmail-phpfpm.conf +3 -0
  240. package/dist/server/engine/samples/spamassassin/local.cf +186 -0
  241. package/dist/server/engine/samples/spamassassin/razor.conf +1 -0
  242. package/dist/server/engine/samples/systemd/clamd.service.d/override.conf +2 -0
  243. package/dist/server/engine/samples/systemd/mariadb.service.d/override.conf +3 -0
  244. package/dist/server/engine/samples/systemd/slapd.service.d/override.conf +6 -0
  245. package/dist/server/engine/samples/yum/CentOS-Linux-AppStream.repo +6 -0
  246. package/dist/server/engine/samples/yum/CentOS-Linux-PowerTools.repo +6 -0
  247. package/dist/server/engine/samples/yum/CentOS-Stream-AppStream.repo +7 -0
  248. package/dist/server/engine/samples/yum/CentOS-Stream-PowerTools.repo +7 -0
  249. package/dist/server/engine/samples/yum/symas-openldap.repo +6 -0
  250. package/dist/server/engine/tools/add_enabled_service.py +60 -0
  251. package/dist/server/engine/tools/backup_mysql.sh +238 -0
  252. package/dist/server/engine/tools/backup_openldap.sh +230 -0
  253. package/dist/server/engine/tools/backup_pgsql.sh +217 -0
  254. package/dist/server/engine/tools/backup_sogo.sh +156 -0
  255. package/dist/server/engine/tools/create_mail_user_OpenLDAP.py +335 -0
  256. package/dist/server/engine/tools/create_mail_user_OpenLDAP.sh +287 -0
  257. package/dist/server/engine/tools/create_mail_user_SQL.sh +127 -0
  258. package/dist/server/engine/tools/enable_postscreen.sh +144 -0
  259. package/dist/server/engine/tools/fail2ban_unban_ip.sh +39 -0
  260. package/dist/server/engine/tools/find_sasl_login_ip.sh +20 -0
  261. package/dist/server/engine/tools/find_top_sasl_usernames.sh +28 -0
  262. package/dist/server/engine/tools/generate_ssl_keys.sh +85 -0
  263. package/dist/server/engine/tools/kill_high_cpu_sogo_process.sh +32 -0
  264. package/dist/server/engine/tools/ldap_assign_user_to_groups.py +151 -0
  265. package/dist/server/engine/tools/ldap_move_members_to_another_group.py +129 -0
  266. package/dist/server/engine/tools/migrate_sql_alias_table.py +177 -0
  267. package/dist/server/engine/update/0.9.8/amavisd.mysql +29 -0
  268. package/dist/server/engine/update/0.9.8/amavisd.pgsql +39 -0
  269. package/dist/server/engine/update/0.9.8/iredmail.mysql +53 -0
  270. package/dist/server/engine/update/0.9.8/iredmail.pgsql +61 -0
  271. package/dist/server/engine/update/0.9.9/iredmail.mysql +2 -0
  272. package/dist/server/engine/update/0.9.9/iredmail.pgsql +2 -0
  273. package/dist/server/engine/update/1.0/iredmail.mysql +2 -0
  274. package/dist/server/engine/update/1.0/iredmail.pgsql +2 -0
  275. package/dist/server/engine/update/1.2/last_login.mysql +16 -0
  276. package/dist/server/engine/update/1.4.0/iredmail.mysql +22 -0
  277. package/dist/server/engine/update/1.4.0/iredmail.pgsql +20 -0
  278. package/dist/server/engine/update/1.4.1/iredmail.mysql +3 -0
  279. package/dist/server/engine/update/1.4.1/iredmail.pgsql +3 -0
  280. package/dist/server/engine/update/1.4.1/sogo.mysql +11 -0
  281. package/dist/server/engine/update/1.4.1/sogo.pgsql +30 -0
  282. package/dist/server/engine/update/1.4.1/update-ldap.py +66 -0
  283. package/dist/server/engine/update/1.4.2/iredmail.mysql +9 -0
  284. package/dist/server/engine/update/1.4.2/iredmail.pgsql +9 -0
  285. package/dist/server/engine/update/1.4.2/sogo.pgsql +30 -0
  286. package/dist/server/engine/update/1.6.0/sogo_view.pgsql +27 -0
  287. package/dist/server/engine/update/1.6.3/iredmail.mysql +1 -0
  288. package/dist/server/engine/update/1.7.0/fail2ban.mysql +11 -0
  289. package/dist/server/engine/update/1.7.0/fail2ban.pgsql +12 -0
  290. package/dist/server/engine/update/1.7.1/amavisd.mysql +1 -0
  291. package/dist/server/engine/update/1.7.2/vmail.mysql +30 -0
  292. package/dist/server/engine/update/1.7.3/deleted_mailboxes.mysql +43 -0
  293. package/dist/server/engine/update/1.7.3/iredadmin.mysql +26 -0
  294. package/dist/server/engine/update/1.7.3/vmail.mysql +79 -0
  295. package/dist/server/engine/update/1.7.3/vmail.pgsql +9 -0
  296. package/dist/server/engine/update/README.md +1 -0
  297. package/dist/server/engine/update/ldap/README.md +40 -0
  298. package/dist/server/engine/update/ldap/update-ldap-dovecot-2.3.py +69 -0
  299. package/dist/server/index.js +55574 -53675
  300. package/dist/server/migrations/0071_gh_device_token.sql +3 -0
  301. package/dist/server/migrations/0072_gh_device_token_method.sql +1 -0
  302. package/dist/server/migrations/meta/_journal.json +14 -0
  303. package/dist/setup-HQIZLUOJ.js +20 -0
  304. package/dist/{signin-DR462ERO.js → signin-ITOPKKUB.js} +13 -9
  305. package/dist/{sso-oidc-VBZLJUK5.js → sso-oidc-NON2PD7Y.js} +13 -9
  306. package/dist/{sts-EJLZ4Z77.js → sts-HHFFSAY2.js} +12 -8
  307. package/package.json +1 -1
  308. package/dist/bare-ITM45QBX.js +0 -18
  309. package/dist/cloud-JMPHO3CW.js +0 -7
  310. package/dist/cloud-W5G6YXAY.js +0 -24
  311. package/dist/dist-es-4C322O5D.js +0 -22
  312. package/dist/docker-3FVJMFC3.js +0 -12
  313. package/dist/docker-edge-executor-ZIWAZWPL.js +0 -9
  314. package/dist/edge-import-Y34YL6JK.js +0 -39
  315. package/dist/executor-J7BHW5HF.js +0 -26
  316. package/dist/nginx-YZWKYKQU.js +0 -16
  317. package/dist/noop-RRNCIUI5.js +0 -7
  318. package/dist/setup-5X3WEEHA.js +0 -14
@@ -1,1470 +1,1724 @@
1
+ #!/usr/bin/env sh
2
+ ":" //# ; exec "$(command -v bun || command -v node)" "$0" "$@"
3
+ import { createRequire as __ospCreateRequire } from "node:module";
4
+ const require = __ospCreateRequire(import.meta.url);
1
5
  import {
2
- describeProcess,
3
- probeListeningPort,
4
6
  resolveEnvironment
5
- } from "./chunk-3U4SPZD5.js";
7
+ } from "./chunk-L2VOGB2X.js";
6
8
  import {
7
9
  formatDuration,
8
10
  isRemoteConnectionError,
9
11
  systemDebug
10
- } from "./chunk-4LEL3H2Z.js";
12
+ } from "./chunk-RPHRPFEH.js";
13
+ import {
14
+ containerCommand,
15
+ edgeContainerExecutor,
16
+ isSafeCertPath,
17
+ readDeclaredPair,
18
+ readMaybeInContainer,
19
+ validateCertFor
20
+ } from "./chunk-FXTBHTWY.js";
11
21
  import {
12
22
  sq
13
- } from "./chunk-4CQWT2WL.js";
23
+ } from "./chunk-FPRHYBY2.js";
24
+ import {
25
+ EdgeConflictError,
26
+ EdgeMigrateRequested,
27
+ freeEdgeTargets,
28
+ invalidateEdgeContainer,
29
+ ourLuaOnHost,
30
+ probeEdge,
31
+ resolveOurEdgeContainer,
32
+ sq as sq2,
33
+ stopTargetsForStatus
34
+ } from "./chunk-FQGJZ2HS.js";
14
35
  import {
15
- OPENRESTY_LUA_DIR,
16
- buildReloadCommand,
17
- deployLuaScripts,
18
- detectOpenRestyPaths,
19
- ensureOpenRestyConfig
20
- } from "./chunk-E6W2JMMU.js";
36
+ waitForPortListening
37
+ } from "./chunk-ILFETZOM.js";
21
38
  import {
22
- AppError,
39
+ EDGE_CONTAINER_MOUNTS,
40
+ EDGE_HOST_PATHS,
41
+ OPENRESTY_DEFAULT_PATHS,
42
+ detectOpenRestyPaths
43
+ } from "./chunk-VFINNTUL.js";
44
+ import {
45
+ buildEdgeImageRef,
23
46
  compareSemver,
24
47
  safeErrorMessage
25
- } from "./chunk-JZLNHGLW.js";
48
+ } from "./chunk-6APULWSU.js";
26
49
 
27
- // ../../packages/adapters/src/system/catalog.ts
28
- function dockerInstallPlan(profile) {
29
- if (profile.os !== "linux") {
30
- return {
31
- supported: false,
32
- unsupportedReason: "Docker installation is only supported on Linux servers"
33
- };
50
+ // ../../packages/adapters/src/system/proxy/import/parse-utils.ts
51
+ async function tryExec(executor, command) {
52
+ try {
53
+ return await executor.exec(command);
54
+ } catch {
55
+ return null;
34
56
  }
35
- return {
36
- supported: true,
37
- installCommand: "curl -fsSL https://get.docker.com | sh",
38
- startCommand: profile.serviceManager === "systemd" ? "systemctl enable docker && systemctl start docker" : void 0,
39
- verifyCommand: "docker --version"
40
- };
41
57
  }
42
- function gitInstallPlan(profile) {
43
- const commands = {
44
- apt: "apt-get update -qq && apt-get install -y -qq git",
45
- dnf: "dnf install -y git",
46
- yum: "yum install -y git",
47
- apk: "apk add --no-cache git",
48
- brew: "brew install git"
49
- };
50
- const installCommand = commands[profile.packageManager];
51
- if (!installCommand) {
52
- return {
53
- supported: false,
54
- unsupportedReason: "No supported package manager found for Git installation"
55
- };
56
- }
57
- return {
58
- supported: true,
59
- installCommand,
60
- verifyCommand: "git --version"
61
- };
58
+ function stripComments(text) {
59
+ return text.replace(/#.*$/gm, "");
62
60
  }
63
- function rsyncInstallPlan(profile) {
64
- const commands = {
65
- apt: "apt-get update -qq && apt-get install -y -qq rsync",
66
- dnf: "dnf install -y rsync",
67
- yum: "yum install -y rsync",
68
- apk: "apk add --no-cache rsync",
69
- brew: "brew install rsync"
70
- };
71
- const installCommand = commands[profile.packageManager];
72
- if (!installCommand) {
73
- return {
74
- supported: false,
75
- unsupportedReason: "No supported package manager found for rsync installation"
76
- };
61
+ function extractBlocks(text, keyword) {
62
+ const blocks = [];
63
+ const re = new RegExp(`(?:^|[\\s;}])${keyword}\\b[^{;]*\\{`, "g");
64
+ let m;
65
+ while ((m = re.exec(text)) !== null) {
66
+ const openIdx = m.index + m[0].length - 1;
67
+ let depth = 1;
68
+ let i = openIdx + 1;
69
+ for (; i < text.length && depth > 0; i++) {
70
+ if (text[i] === "{") depth++;
71
+ else if (text[i] === "}") depth--;
72
+ }
73
+ if (depth === 0) {
74
+ blocks.push(text.slice(openIdx + 1, i - 1));
75
+ re.lastIndex = i;
76
+ } else {
77
+ break;
78
+ }
77
79
  }
78
- return {
79
- supported: true,
80
- installCommand,
81
- verifyCommand: "rsync --version | head -n 1"
82
- };
80
+ return blocks;
83
81
  }
84
- var OPENRESTY_APT_SOURCES = [
85
- // ARM64: openresty.org publishes aarch64 debs under a SEPARATE path
86
- // (openresty.org/package/arm64/<repo>); the default path is amd64-only, so on
87
- // aarch64 `apt-get install openresty` finds no candidate and `set -e` aborts
88
- // the whole setup (issue #109). Switch the base URL by arch and tag the deb
89
- // line with `arch=` so apt fetches the right index. `dpkg --print-architecture`
90
- // returns exactly amd64/arm64 (matches OpenResty's path + arch qualifier).
91
- 'OR_ARCH="$(dpkg --print-architecture 2>/dev/null || echo amd64)"',
92
- 'if [ "$OR_ARCH" = arm64 ]; then OR_BASE="http://openresty.org/package/arm64/$REPO"; else OR_BASE="http://openresty.org/package/$REPO"; fi',
93
- // `|| REPO_CODENAME=""` so a failed substitution (no lsb_release AND no
94
- // /etc/os-release, e.g. a stripped container) doesn't trip `set -e` before
95
- // the empty-var fallback below can pick a default.
96
- 'REPO_CODENAME="$(lsb_release -sc 2>/dev/null || { . /etc/os-release 2>/dev/null && echo "$VERSION_CODENAME"; })" || REPO_CODENAME=""',
97
- 'if [ "$REPO" = debian ]; then OR_FALLBACKS="bookworm bullseye"; else OR_FALLBACKS="noble jammy focal"; fi',
98
- 'OR_CODENAME=""',
99
- "for c in $REPO_CODENAME $OR_FALLBACKS; do",
100
- ' if wget -q --spider --tries=2 --timeout=15 "$OR_BASE/dists/$c/Release"; then OR_CODENAME="$c"; break; fi',
101
- "done",
102
- 'if [ -z "$OR_CODENAME" ]; then case "$REPO" in debian) OR_CODENAME=bookworm ;; *) OR_CODENAME=noble ;; esac; fi',
103
- '[ "$OR_CODENAME" = "$REPO_CODENAME" ] || echo "[openresty] apt repo has no codename $REPO_CODENAME; using nearest supported LTS $OR_CODENAME" >&2',
104
- 'echo "deb [arch=$OR_ARCH signed-by=/usr/share/keyrings/openresty.gpg] $OR_BASE $OR_CODENAME main" > /etc/apt/sources.list.d/openresty.list'
105
- ];
106
- function openrestyInstallPlan(profile) {
107
- if (profile.os !== "linux") {
108
- return {
109
- supported: false,
110
- unsupportedReason: "OpenResty installation is only supported on Linux servers"
111
- };
112
- }
113
- let installCommand;
114
- if (profile.packageManager === "apt") {
115
- const distro = profile.distro === "debian" ? "debian" : "ubuntu";
116
- installCommand = [
117
- "set -e",
118
- // Heal a stale/bad openresty.list left by a prior run (e.g. the #86
119
- // 'resolute' pin) BEFORE the first apt-get update reads it — otherwise
120
- // set -e aborts here and the repo never gets rewritten.
121
- "rm -f /etc/apt/sources.list.d/openresty.list",
122
- "apt-get update -qq && apt-get install -y -qq wget gnupg2 lsb-release",
123
- "wget -qO /tmp/openresty-pubkey.gpg https://openresty.org/package/pubkey.gpg",
124
- "gpg --yes --dearmor -o /usr/share/keyrings/openresty.gpg /tmp/openresty-pubkey.gpg",
125
- `REPO=${distro}`,
126
- ...OPENRESTY_APT_SOURCES,
127
- "apt-get update -qq && apt-get install -y -qq openresty"
128
- ].join("\n");
129
- } else if (profile.packageManager === "dnf") {
130
- const distro = profile.distro === "fedora" ? "fedora" : "centos";
131
- installCommand = `wget -qO /etc/yum.repos.d/openresty.repo https://openresty.org/package/${distro}/openresty.repo && dnf install -y openresty`;
132
- } else if (profile.packageManager === "yum") {
133
- installCommand = "wget -qO /etc/yum.repos.d/openresty.repo https://openresty.org/package/centos/openresty.repo && yum install -y openresty";
134
- } else if (profile.packageManager === "apk") {
135
- installCommand = [
136
- "apk add --no-cache wget",
137
- "wget -qO /etc/apk/keys/admin@openresty.com-5ea678a6.rsa.pub https://openresty.org/package/alpine/admin@openresty.com-5ea678a6.rsa.pub",
138
- `. /etc/os-release && echo "https://openresty.org/package/alpine/v$( echo $VERSION_ID | cut -d. -f1,2 )/main" >> /etc/apk/repositories`,
139
- "apk update && apk add openresty"
140
- ].join(" && ");
141
- } else {
142
- installCommand = [
143
- "set -e",
144
- "if command -v apt-get >/dev/null 2>&1; then",
145
- " rm -f /etc/apt/sources.list.d/openresty.list",
146
- " apt-get update -qq && apt-get install -y -qq wget gnupg2 lsb-release",
147
- " wget -qO /tmp/openresty-pubkey.gpg https://openresty.org/package/pubkey.gpg",
148
- " gpg --yes --dearmor -o /usr/share/keyrings/openresty.gpg /tmp/openresty-pubkey.gpg",
149
- ' DISTRO=$(. /etc/os-release 2>/dev/null && echo "$ID" || echo "ubuntu")',
150
- ' case "$DISTRO" in debian) REPO=debian ;; *) REPO=ubuntu ;; esac',
151
- ...OPENRESTY_APT_SOURCES.map((l) => " " + l),
152
- " apt-get update -qq && apt-get install -y -qq openresty",
153
- "elif command -v dnf >/dev/null 2>&1; then",
154
- " wget -qO /etc/yum.repos.d/openresty.repo https://openresty.org/package/centos/openresty.repo && dnf install -y openresty",
155
- "elif command -v yum >/dev/null 2>&1; then",
156
- " wget -qO /etc/yum.repos.d/openresty.repo https://openresty.org/package/centos/openresty.repo && yum install -y openresty",
157
- "elif command -v apk >/dev/null 2>&1; then",
158
- " apk add --no-cache wget \\",
159
- " && wget -qO /etc/apk/keys/admin@openresty.com-5ea678a6.rsa.pub https://openresty.org/package/alpine/admin@openresty.com-5ea678a6.rsa.pub \\",
160
- ' && . /etc/os-release && echo "https://openresty.org/package/alpine/v$(echo $VERSION_ID | cut -d. -f1,2)/main" >> /etc/apk/repositories \\',
161
- " && apk update && apk add openresty",
162
- "else",
163
- ' echo "No supported package manager found (tried apt-get, dnf, yum, apk)" >&2 && exit 1',
164
- "fi"
165
- ].join("\n");
82
+ function sq3(value) {
83
+ return `'${value.replace(/'/g, "'\\''")}'`;
84
+ }
85
+
86
+ // ../../packages/adapters/src/system/proxy/import/nginx.ts
87
+ async function loadNginxConfig(executor) {
88
+ const dumped = await tryExec(executor, "nginx -T 2>/dev/null");
89
+ if (dumped && /server\s*\{/.test(dumped)) return dumped;
90
+ const cat = await tryExec(
91
+ executor,
92
+ "cat /etc/nginx/nginx.conf /etc/nginx/sites-enabled/* /etc/nginx/conf.d/*.conf 2>/dev/null"
93
+ );
94
+ return cat ?? "";
95
+ }
96
+ function firstDirective(body, name) {
97
+ const m = body.match(new RegExp(`(?:^|[;{\\s])${name}\\s+([^;]+);`));
98
+ return m?.[1]?.trim();
99
+ }
100
+ function parseUpstreams(config) {
101
+ const map = /* @__PURE__ */ new Map();
102
+ const re = /(?:^|[\s;}])upstream\s+(\S+)\s*\{([^}]*)\}/g;
103
+ let m;
104
+ while ((m = re.exec(config)) !== null) {
105
+ const name = m[1];
106
+ const server = m[2].match(/(?:^|[\s;{])server\s+([^;\s]+)/);
107
+ if (name && server?.[1]) map.set(name, server[1].trim());
166
108
  }
167
- return {
168
- supported: true,
169
- installCommand,
170
- startCommand: profile.serviceManager === "systemd" ? "systemctl enable openresty && systemctl start openresty" : void 0,
171
- verifyCommand: "openresty -v 2>&1"
172
- };
109
+ return map;
173
110
  }
174
- function certbotInstallPlan(profile) {
175
- const commands = {
176
- apt: "apt-get update -qq && apt-get install -y -qq certbot",
177
- dnf: "dnf install -y certbot",
178
- yum: "yum install -y certbot",
179
- apk: "apk add --no-cache certbot"
180
- };
181
- const installCommand = commands[profile.packageManager];
182
- if (!installCommand) {
183
- return {
184
- supported: false,
185
- unsupportedReason: "Certbot installation is only supported on Linux with apt, dnf, or yum"
186
- };
111
+ function resolveProxyTarget(proxyPass, upstreams) {
112
+ const raw = proxyPass.replace(/;$/, "").trim();
113
+ if (raw.includes("$")) return { reason: `proxy_pass "${raw}" uses an nginx variable` };
114
+ if (/\/\/unix:/i.test(raw)) return { reason: `proxy_pass "${raw}" targets a unix socket` };
115
+ const m = raw.match(/^(https?:\/\/)([^/]+)(\/.*)?$/i);
116
+ if (!m) return { reason: `unrecognized proxy_pass "${raw}"` };
117
+ const scheme = m[1];
118
+ const authority = m[2];
119
+ const host = authority.replace(/:\d+$/, "");
120
+ if (upstreams.has(host)) return { url: `${scheme}${upstreams.get(host)}` };
121
+ const isIp = /^\d{1,3}(\.\d{1,3}){3}$/.test(host);
122
+ if (isIp || host === "localhost" || host.includes(".")) return { url: raw };
123
+ return { reason: `proxy_pass host "${host}" is an undeclared upstream \u2014 not migratable` };
124
+ }
125
+ function extractLocationProxies(serverBody) {
126
+ const out = [];
127
+ const re = /(?:^|[\s;}])location\s+([^{]+?)\s*\{/g;
128
+ let m;
129
+ while ((m = re.exec(serverBody)) !== null) {
130
+ const path = m[1].trim();
131
+ const openIdx = m.index + m[0].length - 1;
132
+ let depth = 1;
133
+ let i = openIdx + 1;
134
+ for (; i < serverBody.length && depth > 0; i++) {
135
+ if (serverBody[i] === "{") depth++;
136
+ else if (serverBody[i] === "}") depth--;
137
+ }
138
+ if (depth !== 0) break;
139
+ const locBody = serverBody.slice(openIdx + 1, i - 1);
140
+ re.lastIndex = i;
141
+ const pp = firstDirective(locBody, "proxy_pass");
142
+ if (pp) out.push({ path, proxyPass: pp });
187
143
  }
188
- return {
189
- supported: true,
190
- installCommand,
191
- verifyCommand: "certbot --version 2>/dev/null"
192
- };
144
+ return out;
193
145
  }
194
- var systemCatalog = {
195
- checks: {
196
- docker: {
197
- versionCommand: "docker --version",
198
- daemonCommand: "docker info --format '{{.ServerVersion}}'",
199
- parseVersion: (output) => output.match(/Docker version ([^\s,]+)/)?.[1] ?? output,
200
- missingMessage: "Docker is not installed",
201
- notRunningMessage: "Docker is installed but the daemon is not running"
202
- },
203
- openresty: {
204
- versionCommand: "openresty -v 2>&1 || /usr/local/openresty/bin/openresty -v 2>&1",
205
- runningCommands: [
206
- "pgrep -f 'nginx.*openresty' || pgrep -f '/usr/local/openresty'"
207
- ],
208
- parseVersion: (output) => output.match(/openresty\/(\S+)/)?.[1] ?? output.match(/nginx\/(\S+)/)?.[1] ?? output,
209
- missingMessage: "OpenResty is not installed",
210
- notRunningMessage: "OpenResty is installed but not running"
211
- },
212
- certbot: {
213
- versionCommand: "certbot --version 2>/dev/null",
214
- parseVersion: (output) => output.match(/certbot\s+(\S+)/)?.[1] ?? output,
215
- missingMessage: "Certbot is not installed"
216
- },
217
- git: {
218
- versionCommand: "git --version",
219
- parseVersion: (output) => output.match(/git version (\S+)/)?.[1] ?? output,
220
- missingMessage: "Git is not installed"
221
- },
222
- rsync: {
223
- versionCommand: "rsync --version | head -n 1",
224
- parseVersion: (output) => output.match(/rsync\s+version\s+(\S+)/i)?.[1] ?? output,
225
- missingMessage: "rsync is not installed"
226
- }
227
- },
228
- installs: {
229
- docker: dockerInstallPlan,
230
- git: gitInstallPlan,
231
- rsync: rsyncInstallPlan,
232
- openresty: openrestyInstallPlan,
233
- certbot: certbotInstallPlan
234
- }
235
- };
236
-
237
- // ../../packages/adapters/src/system/available-version.ts
238
- var PACKAGE_NAMES = {
239
- git: "git",
240
- rsync: "rsync",
241
- openresty: "openresty",
242
- certbot: "certbot",
243
- docker: null
244
- };
245
- function normalizePkgVersion(raw) {
246
- let v = raw.trim();
247
- const colon = v.lastIndexOf(":");
248
- if (colon !== -1) v = v.slice(colon + 1);
249
- const dash = v.indexOf("-");
250
- if (dash !== -1) v = v.slice(0, dash);
251
- return v.trim();
146
+ function isHttpsUpgradeForSelf(body, names) {
147
+ const targets = [
148
+ ...body.matchAll(/(?:^|[\s;{])return\s+30[1-8]\s+([^;]+);/g),
149
+ ...body.matchAll(/(?:^|[\s;{])rewrite\s+\S+\s+([^;]+?)\s+permanent\s*;/g)
150
+ ].map((m) => m[1].trim());
151
+ if (targets.length === 0) return false;
152
+ return targets.every((raw) => {
153
+ if (!/^https:\/\//i.test(raw)) return false;
154
+ const authority = raw.replace(/^https:\/\//i, "").split(/[/\s]/)[0] ?? "";
155
+ const host = (authority.match(/^(\$[a-z_]+|[^$:]+)/i)?.[1] ?? "").replace(/:\d+$/, "");
156
+ if (host === "$host" || host === "$server_name" || host === "$http_host") return true;
157
+ return names.includes(host);
158
+ });
252
159
  }
253
- async function tryExec(executor, cmd) {
254
- try {
255
- return await executor.exec(cmd);
256
- } catch {
257
- return null;
160
+ function parseServer(body, source, upstreams) {
161
+ const warnings = [];
162
+ const names = firstDirective(body, "server_name")?.split(/\s+/).filter((n) => n && n !== "_" && !n.startsWith("~")) ?? [];
163
+ const listens = [...body.matchAll(/(?:^|[;{\s])listen\s+([^;]+);/g)].map((m) => m[1]);
164
+ const ssl = listens.some((l) => /\bssl\b/.test(l) || /\b443\b/.test(l));
165
+ const root = firstDirective(body, "root");
166
+ const certPath = firstDirective(body, "ssl_certificate");
167
+ const keyPath = firstDirective(body, "ssl_certificate_key");
168
+ if (names.length === 0) return { warnings: [] };
169
+ const rawTargets = extractLocationProxies(body);
170
+ if (rawTargets.length === 0) {
171
+ const serverLevel = firstDirective(body, "proxy_pass");
172
+ if (serverLevel) rawTargets.push({ path: "/", proxyPass: serverLevel });
173
+ }
174
+ const resolved = [];
175
+ for (const t of rawTargets) {
176
+ const r = resolveProxyTarget(t.proxyPass, upstreams);
177
+ if ("reason" in r) warnings.push(`nginx: ${names[0]} ${t.path} \u2014 ${r.reason} (skipped)`);
178
+ else resolved.push({ path: t.path, url: r.url });
179
+ }
180
+ let target;
181
+ let routes;
182
+ if (resolved.length > 0) {
183
+ const primary = resolved.find((r) => r.path === "/") ?? resolved[0];
184
+ target = { kind: "proxy", url: primary.url };
185
+ routes = resolved;
186
+ } else if (root) {
187
+ target = { kind: "static", root: root.replace(/;$/, "") };
188
+ } else if (isHttpsUpgradeForSelf(body, names)) {
189
+ return { warnings: [] };
190
+ } else {
191
+ warnings.push(`nginx: ${names[0]} has neither proxy_pass nor root \u2014 skipped (${source})`);
192
+ return { warnings };
258
193
  }
194
+ const site = { serverNames: names, ssl, target, source };
195
+ if (routes) site.routes = routes;
196
+ if (certPath && keyPath) site.tls = { certPath, keyPath };
197
+ return { site, warnings };
259
198
  }
260
- async function probeCandidate(executor, pm, pkg) {
261
- const q = `'${pkg.replace(/'/g, "'\\''")}'`;
262
- if (pm === "apt") {
263
- const out = await tryExec(executor, `apt-cache policy ${q} 2>/dev/null`);
264
- return out?.match(/Candidate:\s*(\S+)/)?.[1]?.trim() ?? null;
199
+ function parseNginxConfig(raw) {
200
+ const warnings = [];
201
+ const sites = [];
202
+ if (!raw.trim()) {
203
+ return { proxy: "nginx", sites, warnings: ["nginx: no readable configuration found"] };
265
204
  }
266
- if (pm === "dnf" || pm === "yum") {
267
- const out = await tryExec(
268
- executor,
269
- `${pm} -q --cacheonly list available ${q} 2>/dev/null | tail -n1`
270
- );
271
- return out?.trim().split(/\s+/)[1]?.trim() ?? null;
205
+ const config = stripComments(raw);
206
+ const upstreams = parseUpstreams(config);
207
+ const blocks = extractBlocks(config, "server");
208
+ if (blocks.length === 0) {
209
+ warnings.push("nginx: no server blocks found");
272
210
  }
273
- if (pm === "apk") {
274
- const out = await tryExec(executor, `apk policy ${q} 2>/dev/null`);
275
- const versions = [...out?.matchAll(/^\s+([\w.+~-]+):\s*$/gm) ?? []].map((m) => m[1]);
276
- return versions.length ? versions[versions.length - 1] : null;
211
+ for (const body of blocks) {
212
+ const { site, warnings: blockWarnings } = parseServer(body, "nginx", upstreams);
213
+ warnings.push(...blockWarnings);
214
+ if (site) sites.push(site);
277
215
  }
278
- return null;
216
+ return { proxy: "nginx", sites, warnings };
279
217
  }
280
- async function enrichAvailableVersions(executor, profile, statuses) {
281
- const targets = statuses.filter(
282
- (s) => s.installed && s.version && PACKAGE_NAMES[s.name]
283
- );
284
- await Promise.all(
285
- targets.map(async (s) => {
286
- try {
287
- const pkg = PACKAGE_NAMES[s.name];
288
- const candidate = await probeCandidate(executor, profile.packageManager, pkg);
289
- if (!candidate) return;
290
- const avail = normalizePkgVersion(candidate);
291
- const installed = normalizePkgVersion(s.version);
292
- if (avail && compareSemver(avail, installed) > 0) {
293
- s.availableVersion = avail;
294
- s.updateAvailable = true;
295
- }
296
- } catch {
297
- }
298
- })
299
- );
218
+ async function scanNginx(executor) {
219
+ return parseNginxConfig(await loadNginxConfig(executor));
300
220
  }
301
-
302
- // ../../packages/adapters/src/system/components.ts
303
- var SYSTEM_COMPONENTS = [
304
- {
305
- name: "docker",
306
- label: "Docker",
307
- description: "Container runtime for deployments",
308
- installable: true,
309
- category: "core"
310
- },
311
- {
312
- name: "git",
313
- label: "Git",
314
- description: "Version control for source code",
315
- installable: true,
316
- category: "core"
317
- },
318
- {
319
- name: "openresty",
320
- label: "OpenResty",
321
- description: "Reverse proxy with Lua scripting for routing and traffic control",
322
- installable: true,
323
- category: "infrastructure"
324
- },
325
- {
326
- name: "certbot",
327
- label: "Certbot",
328
- description: "Let's Encrypt certificate provisioning",
329
- installable: true,
330
- category: "infrastructure"
331
- },
332
- {
333
- name: "rsync",
334
- label: "rsync",
335
- description: "Fast directory sync for remote local-build transfers",
336
- installable: true,
337
- category: "infrastructure"
338
- }
221
+ var OUR_EDGE_SITE_GLOBS = [
222
+ `${EDGE_HOST_PATHS.sitesDir}/*.conf`,
223
+ `${OPENRESTY_DEFAULT_PATHS.sitesDir}/*.conf`,
224
+ "/etc/openresty/sites-enabled/*.conf"
339
225
  ];
340
- var SYSTEM_COMPONENTS_BY_NAME = new Map(
341
- SYSTEM_COMPONENTS.map((component) => [component.name, component])
342
- );
343
- function getSystemComponentDefinition(name) {
344
- return SYSTEM_COMPONENTS_BY_NAME.get(name) ?? {
345
- name,
346
- label: name,
347
- description: `${name} component`,
348
- installable: false,
349
- category: "infrastructure"
350
- };
226
+ async function scanOpenshipEdge(executor) {
227
+ const cat = `cat ${OUR_EDGE_SITE_GLOBS.join(" ")} 2>/dev/null`;
228
+ const raw = await tryExec(executor, cat);
229
+ if (raw?.trim()) return parseNginxConfig(raw);
230
+ const container = await resolveOurEdgeContainer(executor);
231
+ if (!container) return parseNginxConfig("");
232
+ const fromContainer = await tryExec(executor, containerCommand(container, cat));
233
+ return parseNginxConfig(fromContainer ?? "");
351
234
  }
352
235
 
353
- // ../../packages/adapters/src/system/checks.ts
354
- async function tryExec2(executor, command) {
355
- const startedAt = Date.now();
356
- systemDebug("checks", `exec:start ${command}`);
357
- try {
358
- const result = await executor.exec(command, { timeout: 1e4 });
359
- systemDebug(
360
- "checks",
361
- `exec:ok ${command} (${formatDuration(startedAt)})`
362
- );
363
- return result;
364
- } catch (err) {
365
- if (isRemoteConnectionError(err)) {
366
- systemDebug(
367
- "checks",
368
- `exec:abort ${command} (${formatDuration(startedAt)}) ${safeErrorMessage(err)}`
369
- );
370
- throw err;
236
+ // ../../packages/adapters/src/system/proxy/import/caddy.ts
237
+ var CADDYFILE_PATHS = ["/etc/caddy/Caddyfile", "/etc/Caddyfile"];
238
+ async function loadCaddyJson(executor) {
239
+ for (const p of CADDYFILE_PATHS) {
240
+ const out = await tryExec(executor, `caddy adapt --adapter caddyfile --config ${p} 2>/dev/null`);
241
+ if (out && out.trim().startsWith("{")) return out;
242
+ }
243
+ return null;
244
+ }
245
+ function flattenHandlers(handlers, out = []) {
246
+ for (const h of handlers ?? []) {
247
+ if (h.handler === "subroute") {
248
+ for (const r of h.routes ?? []) flattenHandlers(r.handle, out);
249
+ } else {
250
+ out.push(h);
371
251
  }
372
- const msg = safeErrorMessage(err);
373
- systemDebug(
374
- "checks",
375
- `exec:fail ${command} (${formatDuration(startedAt)}) ${msg}`
376
- );
377
- return null;
378
252
  }
253
+ return out;
379
254
  }
380
- function healthy(name, version, running) {
381
- const component = getSystemComponentDefinition(name);
382
- return {
383
- name,
384
- label: component.label,
385
- description: component.description,
386
- installable: component.installable,
387
- installed: true,
388
- version,
389
- running,
390
- healthy: running !== void 0 ? running : true,
391
- message: running ? `${name} ${version} - running` : `${name} ${version} - installed`
392
- };
255
+ function targetFromRoute(route) {
256
+ const flat = flattenHandlers(route.handle);
257
+ const dial = flat.find((h) => h.handler === "reverse_proxy")?.upstreams?.find((u) => u.dial)?.dial;
258
+ if (dial) return { kind: "proxy", url: /^https?:\/\//.test(dial) ? dial : `http://${dial}` };
259
+ const root = flat.find((h) => h.handler === "vars" && typeof h.root === "string")?.root;
260
+ if (root && flat.some((h) => h.handler === "file_server")) return { kind: "static", root };
261
+ return null;
393
262
  }
394
- function unhealthy(name, message, opts) {
395
- const component = getSystemComponentDefinition(name);
396
- return {
397
- name,
398
- label: component.label,
399
- description: component.description,
400
- installable: component.installable,
401
- installed: !!opts?.version,
402
- version: opts?.version,
403
- running: opts?.running,
404
- healthy: false,
405
- message
406
- };
407
- }
408
- async function checkDocker(executor) {
409
- const startedAt = Date.now();
410
- const recipe = systemCatalog.checks.docker;
411
- const version = await tryExec2(executor, recipe.versionCommand);
412
- if (!version) {
413
- systemDebug("checks", `docker:missing (${formatDuration(startedAt)})`);
414
- return unhealthy("docker", recipe.missingMessage);
263
+ function parseCaddyJson(raw) {
264
+ let cfg;
265
+ try {
266
+ cfg = JSON.parse(raw);
267
+ } catch {
268
+ return null;
415
269
  }
416
- const parsed = recipe.parseVersion(version);
417
- const info = await tryExec2(executor, recipe.daemonCommand);
418
- if (!info) {
419
- systemDebug("checks", `docker:not-running (${formatDuration(startedAt)})`);
420
- return unhealthy("docker", recipe.notRunningMessage, {
421
- version: parsed,
422
- running: false
423
- });
270
+ const servers = cfg.apps?.http?.servers;
271
+ if (!servers) return null;
272
+ const sites = [];
273
+ for (const srv of Object.values(servers)) {
274
+ const ssl = (srv.listen ?? []).some((l) => l.endsWith(":443"));
275
+ for (const route of srv.routes ?? []) {
276
+ const hosts = (route.match ?? []).flatMap((m) => m.host ?? []).filter((h) => h && h !== "*");
277
+ if (hosts.length === 0) continue;
278
+ const target = targetFromRoute(route);
279
+ if (!target) continue;
280
+ sites.push({ serverNames: hosts, ssl, target, source: "caddy (adapt)" });
281
+ }
424
282
  }
425
- systemDebug("checks", `docker:healthy (${formatDuration(startedAt)})`);
426
- return healthy("docker", parsed, true);
283
+ return { sites, warnings: [] };
427
284
  }
428
- async function checkGit(executor) {
429
- const startedAt = Date.now();
430
- const recipe = systemCatalog.checks.git;
431
- const version = await tryExec2(executor, recipe.versionCommand);
432
- if (!version) {
433
- systemDebug("checks", `git:missing (${formatDuration(startedAt)})`);
434
- return unhealthy("git", recipe.missingMessage);
285
+ async function loadCaddyfile(executor) {
286
+ for (const p of CADDYFILE_PATHS) {
287
+ const out = await tryExec(executor, `cat ${p} 2>/dev/null`);
288
+ if (out && out.trim()) return out;
435
289
  }
436
- const parsed = recipe.parseVersion(version);
437
- systemDebug("checks", `git:healthy (${formatDuration(startedAt)})`);
438
- return healthy("git", parsed);
290
+ return "";
439
291
  }
440
- async function checkRsync(executor) {
441
- const startedAt = Date.now();
442
- const recipe = systemCatalog.checks.rsync;
443
- const version = await tryExec2(executor, recipe.versionCommand);
444
- if (!version) {
445
- systemDebug("checks", `rsync:missing (${formatDuration(startedAt)})`);
446
- return unhealthy("rsync", recipe.missingMessage);
292
+ function caddyBlocks(text) {
293
+ const out = [];
294
+ let i = 0;
295
+ let headerStart = 0;
296
+ while (i < text.length) {
297
+ if (text[i] === "{") {
298
+ const header = text.slice(headerStart, i).trim();
299
+ let depth = 1;
300
+ let j = i + 1;
301
+ for (; j < text.length && depth > 0; j++) {
302
+ if (text[j] === "{") depth++;
303
+ else if (text[j] === "}") depth--;
304
+ }
305
+ if (depth !== 0) break;
306
+ out.push({ header, body: text.slice(i + 1, j - 1) });
307
+ i = j;
308
+ headerStart = j;
309
+ } else {
310
+ i++;
311
+ }
447
312
  }
448
- const parsed = recipe.parseVersion(version);
449
- systemDebug("checks", `rsync:healthy (${formatDuration(startedAt)})`);
450
- return healthy("rsync", parsed);
313
+ return out;
451
314
  }
452
- async function checkOpenResty(executor) {
453
- const startedAt = Date.now();
454
- const recipe = systemCatalog.checks.openresty;
455
- const version = await tryExec2(executor, recipe.versionCommand);
456
- if (!version) {
457
- systemDebug("checks", `openresty:missing (${formatDuration(startedAt)})`);
458
- return unhealthy("openresty", recipe.missingMessage);
315
+ function parseAddresses(header) {
316
+ return header.split(/[\s,]+/).filter(Boolean).map((addr) => {
317
+ const httpScheme = /^http:\/\//i.test(addr);
318
+ const host = addr.replace(/^https?:\/\//i, "").replace(/:\d+$/, "");
319
+ const isPort80 = /:80$/.test(addr);
320
+ return { host, ssl: !httpScheme && !isPort80 };
321
+ }).filter((a) => a.host && !a.host.startsWith(":") && a.host !== "*");
322
+ }
323
+ async function scanCaddy(executor) {
324
+ const json = await loadCaddyJson(executor);
325
+ if (json) {
326
+ const parsed = parseCaddyJson(json);
327
+ if (parsed && parsed.sites.length > 0) {
328
+ return { proxy: "caddy", sites: parsed.sites, warnings: parsed.warnings };
329
+ }
459
330
  }
460
- const parsed = recipe.parseVersion(version);
461
- const runningChecks = await Promise.all(
462
- recipe.runningCommands.map((command) => tryExec2(executor, command))
463
- );
464
- const running = runningChecks.some(Boolean);
465
- if (!running) {
466
- systemDebug("checks", `openresty:not-running (${formatDuration(startedAt)})`);
467
- return unhealthy("openresty", recipe.notRunningMessage, {
468
- version: parsed,
469
- running: false
470
- });
331
+ return scanCaddyText(executor);
332
+ }
333
+ async function scanCaddyText(executor) {
334
+ const raw = await loadCaddyfile(executor);
335
+ const warnings = [];
336
+ const sites = [];
337
+ if (!raw.trim()) {
338
+ return { proxy: "caddy", sites, warnings: ["caddy: no readable Caddyfile found"] };
339
+ }
340
+ const text = stripComments(raw);
341
+ let blocks = caddyBlocks(text);
342
+ if (blocks.length === 0) {
343
+ const lines = text.split("\n").map((l) => l.trim()).filter(Boolean);
344
+ if (lines.length > 0) {
345
+ blocks = [{ header: lines[0], body: lines.slice(1).join("\n") }];
346
+ } else {
347
+ warnings.push("caddy: config present but no parseable site blocks");
348
+ }
349
+ }
350
+ for (const { header, body } of blocks) {
351
+ if (!header) continue;
352
+ const addrs = parseAddresses(header);
353
+ if (addrs.length === 0) {
354
+ warnings.push(`caddy: skipped a block with no usable address (${header.slice(0, 40)})`);
355
+ continue;
356
+ }
357
+ const proxyMatch = body.match(/(?:^|\s)reverse_proxy\s+([^\n{]+)/);
358
+ const rootMatch = body.match(/(?:^|\s)root\s+\*?\s*([^\n\s]+)/);
359
+ const tlsMatch = body.match(/(?:^|\s)tls\s+(\S+)\s+(\S+)/);
360
+ let target;
361
+ if (proxyMatch) {
362
+ const upstream = proxyMatch[1].trim().split(/\s+/)[0];
363
+ target = { kind: "proxy", url: /^https?:\/\//.test(upstream) ? upstream : `http://${upstream}` };
364
+ } else if (rootMatch) {
365
+ target = { kind: "static", root: rootMatch[1] };
366
+ } else {
367
+ warnings.push(`caddy: ${addrs[0].host} has no reverse_proxy or root \u2014 skipped`);
368
+ continue;
369
+ }
370
+ const site = {
371
+ serverNames: addrs.map((a) => a.host),
372
+ ssl: addrs.some((a) => a.ssl),
373
+ target,
374
+ source: "caddy"
375
+ };
376
+ if (tlsMatch && tlsMatch[1].includes("/")) {
377
+ site.tls = { certPath: tlsMatch[1], keyPath: tlsMatch[2] };
378
+ }
379
+ sites.push(site);
471
380
  }
472
- const hasLua = await tryExec2(
381
+ return { proxy: "caddy", sites, warnings };
382
+ }
383
+
384
+ // ../../packages/adapters/src/system/proxy/import/apache.ts
385
+ async function vhostFilesFromDump(executor) {
386
+ const dump = await tryExec(
473
387
  executor,
474
- `test -f ${OPENRESTY_LUA_DIR}/site_logger.lua && test -f ${OPENRESTY_LUA_DIR}/pipe_stream.lua && echo ok`
388
+ "apachectl -S 2>/dev/null || apache2ctl -S 2>/dev/null || httpd -S 2>/dev/null"
475
389
  );
476
- if (!hasLua) {
477
- systemDebug("checks", `openresty:missing-lua (${formatDuration(startedAt)})`);
478
- return unhealthy(
479
- "openresty",
480
- "OpenResty is running but analytics scripts are not deployed - reinstall to fix",
481
- { version: parsed, running: true }
482
- );
390
+ if (!dump) return [];
391
+ const files = /* @__PURE__ */ new Set();
392
+ for (const m of dump.matchAll(/\(([^()]+?):\d+\)/g)) {
393
+ const f = m[1].trim();
394
+ if (f.startsWith("/")) files.add(f);
483
395
  }
484
- systemDebug("checks", `openresty:healthy (${formatDuration(startedAt)})`);
485
- return healthy("openresty", parsed, true);
396
+ return [...files];
486
397
  }
487
- async function checkCertbot(executor) {
488
- const startedAt = Date.now();
489
- const recipe = systemCatalog.checks.certbot;
490
- const version = await tryExec2(executor, recipe.versionCommand);
491
- if (!version) {
492
- systemDebug("checks", `certbot:missing (${formatDuration(startedAt)})`);
493
- return unhealthy("certbot", recipe.missingMessage);
398
+ async function loadApacheConfig(executor) {
399
+ const files = await vhostFilesFromDump(executor);
400
+ if (files.length > 0) {
401
+ const cat2 = await tryExec(executor, `cat ${files.map(sq3).join(" ")} 2>/dev/null`);
402
+ if (cat2 && cat2.trim()) return cat2;
494
403
  }
495
- const parsed = recipe.parseVersion(version);
496
- systemDebug("checks", `certbot:healthy (${formatDuration(startedAt)})`);
497
- return healthy("certbot", parsed);
498
- }
499
- var COMPONENT_CHECKS = {
500
- docker: checkDocker,
501
- openresty: checkOpenResty,
502
- certbot: checkCertbot,
503
- git: checkGit,
504
- rsync: checkRsync
505
- };
506
- async function mapWithConcurrency(items, limit, fn) {
507
- const results = new Array(items.length);
508
- let cursor = 0;
509
- const worker = async () => {
510
- while (cursor < items.length) {
511
- const index = cursor++;
512
- results[index] = await fn(items[index]);
513
- }
514
- };
515
- await Promise.all(
516
- Array.from({ length: Math.min(limit, items.length) }, worker)
404
+ const cat = await tryExec(
405
+ executor,
406
+ "cat /etc/apache2/sites-enabled/*.conf /etc/apache2/apache2.conf /etc/httpd/conf.d/*.conf /etc/httpd/conf/httpd.conf 2>/dev/null"
517
407
  );
518
- return results;
408
+ return cat ?? "";
519
409
  }
520
- var CHECK_CONCURRENCY = 4;
521
- async function checkAll(executor) {
522
- const startedAt = Date.now();
523
- const entries = SYSTEM_COMPONENTS.map((component) => [component.name, COMPONENT_CHECKS[component.name]]).filter((entry) => Boolean(entry[1]));
524
- systemDebug(
525
- "checks",
526
- `checkAll:start [${entries.map(([name]) => name).join(", ")}]`
527
- );
528
- const results = await mapWithConcurrency(
529
- entries,
530
- CHECK_CONCURRENCY,
531
- ([, fn]) => fn(executor)
410
+ function directive(body, name) {
411
+ const m = body.match(new RegExp(`(?:^|\\n)\\s*${name}\\s+([^\\n]+)`, "i"));
412
+ return m?.[1]?.trim();
413
+ }
414
+ function directiveAll(body, name) {
415
+ return [...body.matchAll(new RegExp(`(?:^|\\n)\\s*${name}\\s+([^\\n]+)`, "gi"))].map(
416
+ (m) => m[1].trim()
532
417
  );
533
- await enrichAvailable(executor, results);
534
- systemDebug("checks", `checkAll:done (${formatDuration(startedAt)})`);
535
- return results;
536
418
  }
537
- async function enrichAvailable(executor, results) {
538
- try {
539
- const profile = await resolveEnvironment(executor);
540
- await enrichAvailableVersions(executor, profile, results);
541
- } catch {
419
+ function parseVhost(body, portHint) {
420
+ const serverName = directive(body, "ServerName")?.split(/\s+/)[0];
421
+ const aliases = directiveAll(body, "ServerAlias").flatMap((line) => line.split(/\s+/));
422
+ const names = [serverName, ...aliases].filter((n) => Boolean(n));
423
+ if (names.length === 0) {
424
+ return { warning: "apache: skipped a VirtualHost with no ServerName" };
425
+ }
426
+ const routes = [...body.matchAll(/(?:^|\n)\s*ProxyPass\s+(\S+)\s+(\S+)/gi)].map((m) => ({ path: m[1], url: m[2] })).filter((p) => p.url && p.url !== "!");
427
+ const docRoot = directive(body, "DocumentRoot")?.replace(/^["']|["']$/g, "");
428
+ const certPath = directive(body, "SSLCertificateFile");
429
+ const keyPath = directive(body, "SSLCertificateKeyFile");
430
+ const ssl = /:443/.test(portHint) || /SSLEngine\s+on/i.test(body) || Boolean(certPath);
431
+ let target;
432
+ if (routes.length > 0) {
433
+ const primary = routes.find((r) => r.path === "/") ?? routes[0];
434
+ target = { kind: "proxy", url: primary.url };
435
+ } else if (docRoot) {
436
+ target = { kind: "static", root: docRoot };
437
+ } else {
438
+ return { warning: `apache: ${names[0]} has neither ProxyPass nor DocumentRoot \u2014 skipped` };
542
439
  }
440
+ const site = { serverNames: names, ssl, target, source: "apache" };
441
+ if (routes.length > 0) site.routes = routes;
442
+ if (certPath && keyPath) site.tls = { certPath, keyPath };
443
+ return site;
543
444
  }
544
- async function checkComponents(executor, names) {
545
- const startedAt = Date.now();
546
- const fns = names.map((name) => COMPONENT_CHECKS[name]).filter((fn) => Boolean(fn));
547
- systemDebug("checks", `checkComponents:start [${names.join(", ")}]`);
548
- const results = await mapWithConcurrency(
549
- fns,
550
- CHECK_CONCURRENCY,
551
- (fn) => fn(executor)
552
- );
553
- await enrichAvailable(executor, results);
554
- systemDebug(
555
- "checks",
556
- `checkComponents:done [${names.join(", ")}] (${formatDuration(startedAt)})`
557
- );
558
- return results;
445
+ function vhostBlocks(text) {
446
+ const out = [];
447
+ const re = /<VirtualHost([^>]*)>([\s\S]*?)<\/VirtualHost>/gi;
448
+ let m;
449
+ while ((m = re.exec(text)) !== null) {
450
+ out.push({ args: m[1].trim(), body: m[2] });
451
+ }
452
+ return out;
453
+ }
454
+ async function scanApache(executor) {
455
+ const raw = await loadApacheConfig(executor);
456
+ const warnings = [];
457
+ const sites = [];
458
+ if (!raw.trim()) {
459
+ return { proxy: "apache", sites, warnings: ["apache: no readable configuration found"] };
460
+ }
461
+ const text = stripComments(raw);
462
+ const blocks = vhostBlocks(text);
463
+ if (blocks.length === 0) warnings.push("apache: no <VirtualHost> blocks found");
464
+ for (const { args, body } of blocks) {
465
+ const parsed = parseVhost(body, args);
466
+ if ("warning" in parsed) warnings.push(parsed.warning);
467
+ else sites.push(parsed);
468
+ }
469
+ return { proxy: "apache", sites, warnings };
559
470
  }
560
471
 
561
- // ../../packages/adapters/src/system/elevated-executor.ts
562
- var INSTALL_ENV = "DEBIAN_FRONTEND=noninteractive DPKG_FORCE=confnew";
563
- function elevateCommand(command) {
564
- return `sudo -n sh -c ${sq(`export ${INSTALL_ENV}; ${command}`)}`;
472
+ // ../../packages/adapters/src/system/proxy/import/traefik.ts
473
+ function extractHosts(rule) {
474
+ const hosts = [];
475
+ for (const call of rule.matchAll(/\bHost\(([^)]*)\)/gi)) {
476
+ for (const m of call[1].matchAll(/[`'"]([^`'"]+)[`'"]/g)) {
477
+ const h = m[1].trim();
478
+ if (h) hosts.push(h);
479
+ }
480
+ }
481
+ return hosts;
565
482
  }
566
- function dirOf(path) {
567
- const idx = path.lastIndexOf("/");
568
- if (idx < 0) return ".";
569
- return idx === 0 ? "/" : path.slice(0, idx);
483
+ function extraMatchers(rule) {
484
+ const found = /* @__PURE__ */ new Set();
485
+ for (const m of rule.matchAll(
486
+ /\b(PathPrefix|PathRegexp|Path|HeadersRegexp|Headers|Method|Query|ClientIP|HostRegexp)\b/gi
487
+ )) {
488
+ found.add(m[1]);
489
+ }
490
+ return [...found];
570
491
  }
571
- function elevatedExecutor(inner) {
572
- const writeFileElevated = async (path, content) => {
573
- const tmp = `/tmp/.openship-elev-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
574
- await inner.writeFile(tmp, content);
575
- await inner.exec(
576
- elevateCommand(`mkdir -p ${sq(dirOf(path))} && mv -f ${sq(tmp)} ${sq(path)}`)
577
- );
578
- };
579
- const overrides = {
580
- exec: (command, opts) => inner.exec(elevateCommand(command), opts),
581
- streamExec: (command, onLog) => inner.streamExec(elevateCommand(command), onLog),
582
- writeFile: writeFileElevated,
583
- mkdir: async (path) => {
584
- await inner.exec(elevateCommand(`mkdir -p ${sq(path)}`));
585
- },
586
- rm: async (path) => {
587
- await inner.exec(elevateCommand(`rm -rf ${sq(path)}`));
492
+ function parseTraefikLabels(containers) {
493
+ const sites = [];
494
+ const warnings = [];
495
+ for (const c of containers) {
496
+ if (c.labels["traefik.enable"] === "false") continue;
497
+ const routers = /* @__PURE__ */ new Map();
498
+ const servicePorts = /* @__PURE__ */ new Map();
499
+ for (const [key, val] of Object.entries(c.labels)) {
500
+ const r = key.match(/^traefik\.http\.routers\.([^.]+)\.(.+)$/);
501
+ if (r) {
502
+ const bag = routers.get(r[1]) ?? {};
503
+ bag[r[2]] = val;
504
+ routers.set(r[1], bag);
505
+ continue;
506
+ }
507
+ const s = key.match(/^traefik\.http\.services\.([^.]+)\.loadbalancer\.server\.port$/);
508
+ if (s) servicePorts.set(s[1], val);
588
509
  }
589
- };
590
- return new Proxy(inner, {
591
- get(target, prop) {
592
- if (Object.prototype.hasOwnProperty.call(overrides, prop)) {
593
- return overrides[prop];
510
+ for (const [rname, props] of routers) {
511
+ const rule = props.rule;
512
+ if (!rule) continue;
513
+ const hosts = extractHosts(rule);
514
+ if (hosts.length === 0) {
515
+ warnings.push(
516
+ /HostRegexp/i.test(rule) ? `traefik: router "${rname}" uses HostRegexp (${rule}) \u2014 regex hosts aren't migratable` : `traefik: router "${rname}" has no Host() rule (${rule}) \u2014 skipped`
517
+ );
518
+ continue;
594
519
  }
595
- const value = Reflect.get(target, prop, target);
596
- return typeof value === "function" ? value.bind(target) : value;
520
+ if (!c.ip) {
521
+ warnings.push(
522
+ `traefik: ${hosts.join(", ")} \u2014 couldn't resolve container ${c.name}'s IP; re-add the upstream manually`
523
+ );
524
+ continue;
525
+ }
526
+ const svc = props.service;
527
+ const rawPort = (svc && servicePorts.get(svc)) ?? (servicePorts.size === 1 ? [...servicePorts.values()][0] : void 0) ?? "80";
528
+ const port = /^\d{1,5}$/.test(String(rawPort)) ? String(rawPort) : "80";
529
+ const url = `http://${c.ip}:${port}`;
530
+ const extras = extraMatchers(rule);
531
+ if (extras.length > 0) {
532
+ warnings.push(
533
+ `traefik: ${hosts.join(", ")} also matches ${extras.join(", ")} \u2014 only the Host() part is migrated; re-add path/header rules manually`
534
+ );
535
+ }
536
+ if (props.middlewares) {
537
+ warnings.push(`traefik: ${hosts.join(", ")} uses middleware(s) "${props.middlewares}" \u2014 not migrated`);
538
+ }
539
+ const ssl = Object.keys(props).some((p) => p === "tls" || p.startsWith("tls."));
540
+ sites.push({
541
+ serverNames: hosts,
542
+ ssl,
543
+ target: { kind: "proxy", url },
544
+ source: `traefik container ${c.name}`
545
+ });
597
546
  }
598
- });
547
+ }
548
+ return { proxy: "traefik", sites, warnings };
599
549
  }
600
-
601
- // ../../packages/adapters/src/system/proxy/detect.ts
602
- var EDGE_PORTS = [80, 443];
603
- var EdgeConflictError = class extends AppError {
604
- constructor(status) {
605
- super(
606
- `Ports ${status.occupants.map((o) => o.port).join("/") || "80/443"} are in use by another service (${status.classification}). Accept a migrate or takeover to continue.`,
607
- 409,
608
- "EDGE_CONFLICT"
609
- );
610
- this.status = status;
611
- this.name = "EdgeConflictError";
550
+ async function scanTraefik(executor) {
551
+ const out = await tryExec(
552
+ executor,
553
+ "docker ps -q 2>/dev/null | xargs -r docker inspect --format '{{.Name}} {{json .Config.Labels}} {{range .NetworkSettings.Networks}}{{.IPAddress}} {{end}}' 2>/dev/null"
554
+ );
555
+ if (!out) {
556
+ return { proxy: "traefik", sites: [], warnings: ["traefik: couldn't read container labels via docker inspect"] };
612
557
  }
613
- };
614
- var EdgeMigrateRequested = class extends Error {
615
- constructor(status, sites, warnings = []) {
616
- super("Edge migration requested by user");
617
- this.status = status;
618
- this.sites = sites;
619
- this.warnings = warnings;
620
- this.name = "EdgeMigrateRequested";
558
+ const containers = [];
559
+ for (const line of out.split("\n")) {
560
+ const [rawName, rawLabels, rawIps] = line.split(" ");
561
+ if (!rawName || !rawLabels) continue;
562
+ let labels;
563
+ try {
564
+ labels = JSON.parse(rawLabels) ?? {};
565
+ } catch {
566
+ continue;
567
+ }
568
+ if (!Object.keys(labels).some((k) => k.startsWith("traefik."))) continue;
569
+ const ip = (rawIps ?? "").trim().split(/\s+/)[0] || void 0;
570
+ containers.push({ name: rawName.replace(/^\//, ""), labels, ip });
621
571
  }
622
- };
623
- async function tryExec3(executor, command) {
572
+ return parseTraefikLabels(containers);
573
+ }
574
+
575
+ // ../../packages/adapters/src/system/proxy/import/index.ts
576
+ var INSTALLED_MARKERS = [
577
+ { proxy: "nginx", paths: ["/etc/nginx/nginx.conf"] },
578
+ { proxy: "caddy", paths: ["/etc/caddy/Caddyfile"] },
579
+ { proxy: "apache", paths: ["/etc/apache2/apache2.conf", "/etc/httpd/conf/httpd.conf"] },
580
+ {
581
+ proxy: "openresty",
582
+ paths: ["/usr/local/openresty/nginx/conf/nginx.conf", "/etc/openresty/nginx.conf"]
583
+ }
584
+ ];
585
+ async function detectInstalledProxy(executor) {
586
+ const probe = INSTALLED_MARKERS.map(({ proxy, paths }) => {
587
+ const test = paths.map((p) => `[ -f '${p}' ]`).join(" || ");
588
+ return `{ ${test}; } && echo ${proxy}`;
589
+ }).join("; ");
590
+ let out = "";
624
591
  try {
625
- return await executor.exec(command);
592
+ out = await executor.exec(`{ ${probe}; } 2>/dev/null; true`, { timeout: 1e4 });
626
593
  } catch {
627
594
  return null;
628
595
  }
629
- }
630
- function classifyProxy(text) {
631
- if (!text) return void 0;
632
- const t = text.toLowerCase();
633
- if (t.includes("openresty")) return "openresty";
634
- if (/(^|[\s/:])nginx/.test(t)) return "nginx";
635
- if (/(^|[\s/:])caddy/.test(t)) return "caddy";
636
- if (/(apache2|httpd)/.test(t)) return "apache";
637
- if (/(^|[\s/:])traefik/.test(t)) return "traefik";
638
- if (/(^|[\s/:])haproxy/.test(t)) return "haproxy";
639
- return void 0;
640
- }
641
- function sq2(value) {
642
- return `'${value.replace(/'/g, "'\\''")}'`;
643
- }
644
- function isOurEdgeContainer(name, image) {
645
- return /openship-edge/i.test(`${name ?? ""} ${image ?? ""}`);
646
- }
647
- async function ourEdgeContainerRunning(executor) {
648
- const byName = await tryExec3(
649
- executor,
650
- `docker ps --filter name=openship-edge --format '{{.Names}}' 2>/dev/null | head -1`
651
- );
652
- if (byName && byName.trim()) return true;
653
- const all = await tryExec3(executor, `docker ps --format '{{.Names}} {{.Image}}' 2>/dev/null`);
654
- if (!all) return false;
655
- return all.split("\n").map((line) => line.trim()).filter(Boolean).some((line) => {
656
- const [name, image] = line.split(" ");
657
- return isOurEdgeContainer(name, image);
658
- });
659
- }
660
- async function ourLuaOnHost(executor) {
661
- const lua = await tryExec3(
662
- executor,
663
- `test -f ${OPENRESTY_LUA_DIR}/site_logger.lua && echo ok`
596
+ const found = new Set(
597
+ out.split("\n").map((l) => l.trim()).filter(Boolean)
664
598
  );
665
- return Boolean(lua && lua.includes("ok"));
599
+ return INSTALLED_MARKERS.find((m) => found.has(m.proxy))?.proxy ?? null;
666
600
  }
667
- async function detectDockerOnPort(executor, port) {
668
- const out = await tryExec3(
669
- executor,
670
- `docker ps --filter publish=${port} --format '{{.Names}} {{.Image}}' 2>/dev/null | head -1`
671
- );
672
- const line = out?.trim();
673
- if (!line) return null;
674
- const [name, image] = line.split(" ");
675
- if (!name) return null;
676
- return { name, image: image ?? "" };
677
- }
678
- async function listenerIsOurOpenResty(executor, listener) {
679
- if (!listener) return false;
680
- if (/openresty/i.test(`${listener.rawCommand ?? ""} ${listener.command ?? ""}`)) return true;
681
- if (listener.pid) {
682
- const exe = await tryExec3(executor, `readlink -f /proc/${listener.pid}/exe 2>/dev/null || true`);
683
- if (exe && /openresty/i.test(exe)) return true;
684
- }
685
- return false;
686
- }
687
- var WORKER_RE = /\b(?:nginx|openresty)\s*:\s*worker process/i;
688
- var MASTER_RE = /\b(?:nginx|openresty)\s*:\s*master process/i;
689
- async function resolveProxyMaster(executor, listener) {
690
- if (!listener?.pid) return listener;
691
- if (!WORKER_RE.test(`${listener.rawCommand ?? ""} ${listener.command ?? ""}`)) return listener;
692
- const ppidRaw = await tryExec3(
693
- executor,
694
- `awk '/^PPid:/{print $2}' /proc/${listener.pid}/status 2>/dev/null || true`
695
- );
696
- const ppid = Number.parseInt((ppidRaw ?? "").trim(), 10);
697
- if (!Number.isInteger(ppid) || ppid <= 1) return listener;
698
- const master = await describeProcess(executor, ppid);
699
- if (!master || !MASTER_RE.test(`${master.rawCommand ?? ""} ${master.command ?? ""}`)) {
700
- return listener;
701
- }
702
- return master;
703
- }
704
- async function probeEdgePort(executor, port, ours) {
705
- const listener = await resolveProxyMaster(executor, await probeListeningPort(executor, port));
706
- const docker = await detectDockerOnPort(executor, port);
707
- if (!listener && !docker) return null;
708
- const proxy = classifyProxy(
709
- [
710
- docker?.image,
711
- docker?.name,
712
- listener?.rawCommand,
713
- listener?.command,
714
- listener?.systemdUnit
715
- ].filter(Boolean).join(" ")
716
- );
717
- const foreignDockerProxy = Boolean(docker) && !isOurEdgeContainer(docker?.name, docker?.image);
718
- const managedByOpenship = isOurEdgeContainer(docker?.name, docker?.image) || ours.containerRunning && !foreignDockerProxy || !docker && ours.luaOnHost && await listenerIsOurOpenResty(executor, listener);
719
- return {
720
- port,
721
- pid: listener?.pid ?? void 0,
722
- command: docker ? `docker container ${docker.name} (${docker.image})` : listener?.command,
723
- rawCommand: listener?.rawCommand,
724
- systemdUnit: listener?.systemdUnit,
725
- systemdDescription: listener?.systemdDescription,
726
- isDocker: Boolean(docker),
727
- containerName: docker?.name,
728
- proxy,
729
- managedByOpenship
730
- };
601
+ async function scanImportableSites(executor, proxy) {
602
+ switch (proxy) {
603
+ case "nginx":
604
+ return scanNginx(executor);
605
+ case "caddy":
606
+ return scanCaddy(executor);
607
+ case "apache":
608
+ return scanApache(executor);
609
+ case "traefik":
610
+ return scanTraefik(executor);
611
+ case "openresty":
612
+ return scanOpenshipEdge(executor);
613
+ default:
614
+ return {
615
+ proxy,
616
+ sites: [],
617
+ warnings: [`${proxy}: config import not supported \u2014 takeover only`]
618
+ };
619
+ }
731
620
  }
732
- async function probeEdge(executor) {
733
- const containerRunning = await ourEdgeContainerRunning(executor);
734
- const luaOnHost = await ourLuaOnHost(executor);
735
- const all = [];
736
- for (const port of EDGE_PORTS) {
737
- const occ = await probeEdgePort(executor, port, { containerRunning, luaOnHost });
738
- if (occ) all.push(occ);
739
- }
740
- const foreign = all.filter((o) => !o.managedByOpenship);
741
- let classification;
742
- if (all.length === 0) classification = "free";
743
- else if (foreign.length === 0) classification = "ours";
744
- else if (foreign.every((o) => o.proxy && o.proxy !== "openresty")) classification = "known";
745
- else classification = "unknown";
746
- return {
747
- classification,
748
- occupants: foreign,
749
- canProceedClean: classification === "free" || classification === "ours"
750
- };
621
+ function canImportProxy(proxy) {
622
+ return proxy === "nginx" || proxy === "caddy" || proxy === "apache" || proxy === "traefik" || proxy === "openresty";
751
623
  }
752
- function stopTargetsForStatus(status) {
753
- const out = [];
754
- const seen = /* @__PURE__ */ new Set();
755
- for (const o of status.occupants) {
756
- const identity = o.systemdUnit ?? o.containerName ?? (o.pid ? `pid:${o.pid}` : `port:${o.port}`);
757
- if (seen.has(identity)) continue;
758
- seen.add(identity);
759
- out.push({
760
- port: o.port,
761
- unit: o.systemdUnit,
762
- pid: o.pid,
763
- container: o.containerName,
764
- // Prefer the proxy kind over the raw cmdline — `nginx (PID 123)` reads
765
- // better in "Stopping …" than `nginx: master process /usr/sbin/nginx …`.
766
- label: o.proxy && o.pid ? `${o.proxy} (PID ${o.pid})` : o.command
767
- });
624
+
625
+ // ../../packages/adapters/src/system/proxy/import/caddy-certs.ts
626
+ var CADDY_DATA_ROOTS = [
627
+ "/var/lib/caddy/.local/share/caddy",
628
+ "/root/.local/share/caddy",
629
+ "/home/caddy/.local/share/caddy",
630
+ "/data/caddy"
631
+ ];
632
+ function candidateNames(host) {
633
+ const names = [host];
634
+ const dot = host.indexOf(".");
635
+ if (dot > 0) names.push(`wildcard_${host.slice(dot)}`);
636
+ return names;
637
+ }
638
+ async function findPairIn(exec, root, host, container) {
639
+ for (const name of candidateNames(host)) {
640
+ const glob = `${root}/certificates/*/${name}/${name}.crt`;
641
+ let certPath = firstCrt(await tryExec(exec, `ls -1 ${glob} 2>/dev/null`));
642
+ if (!certPath && container) {
643
+ certPath = firstCrt(
644
+ await tryExec(exec, containerCommand(container, `ls -1 ${glob} 2>/dev/null`))
645
+ );
646
+ }
647
+ if (certPath) return { certPath, keyPath: certPath.replace(/\.crt$/, ".key") };
768
648
  }
769
- return out;
649
+ return null;
770
650
  }
771
- async function freeEdgeTargets(executor, targets, onLog) {
772
- for (const t of targets) {
773
- const where = t.port ? ` (port ${t.port})` : "";
774
- if (t.container) {
775
- onLog(`Stopping container ${t.container}${where}...`, "warn");
776
- await tryExec3(executor, `docker update --restart=no ${sq2(t.container)} 2>/dev/null || true`);
777
- await tryExec3(executor, `docker stop ${sq2(t.container)} 2>/dev/null || true`);
778
- } else if (t.unit) {
779
- onLog(`Stopping & disabling service ${t.unit}${where}...`, "warn");
780
- await tryExec3(
781
- executor,
782
- `systemctl disable --now ${sq2(t.unit)} 2>/dev/null || systemctl stop ${sq2(t.unit)} 2>/dev/null || true; systemctl reset-failed ${sq2(t.unit)} 2>/dev/null || true`
783
- );
784
- } else if (t.pid) {
785
- onLog(`Stopping ${t.label ?? `process ${t.pid}`}${where}...`, "warn");
786
- await tryExec3(executor, `kill ${t.pid} 2>/dev/null || true`);
787
- await new Promise((r) => setTimeout(r, 800));
788
- await tryExec3(executor, `kill -9 ${t.pid} 2>/dev/null || true`);
651
+ function firstCrt(listing) {
652
+ return (listing ?? "").split("\n").map((l) => l.trim()).find((l) => l.endsWith(".crt"));
653
+ }
654
+ async function caddyStoreCert(exec, host, container) {
655
+ for (const root of CADDY_DATA_ROOTS) {
656
+ const pair = await findPairIn(exec, root, host, container);
657
+ if (!pair) continue;
658
+ const certPem = await readMaybeInContainer(exec, pair.certPath, container);
659
+ const keyPem = await readMaybeInContainer(exec, pair.keyPath, container);
660
+ if (certPem.trim() && keyPem.trim()) {
661
+ return { certPem, keyPem, source: `caddy store ${pair.certPath}` };
789
662
  }
790
663
  }
791
- await new Promise((r) => setTimeout(r, 1e3));
664
+ return null;
792
665
  }
793
666
 
794
- // ../../packages/adapters/src/system/proxy/import/parse-utils.ts
795
- async function tryExec4(executor, command) {
667
+ // ../../packages/adapters/src/system/proxy/import/traefik-certs.ts
668
+ var DEFAULT_ACME_PATHS = ["/acme.json", "/etc/traefik/acme.json", "/data/acme.json", "/letsencrypt/acme.json"];
669
+ var TRAEFIK_STATIC_CONFIGS = [
670
+ "/etc/traefik/traefik.yml",
671
+ "/etc/traefik/traefik.yaml",
672
+ "/etc/traefik/traefik.toml"
673
+ ];
674
+ function prop(obj, name) {
675
+ const hit = Object.keys(obj).find((k) => k.toLowerCase() === name.toLowerCase());
676
+ return hit === void 0 ? void 0 : obj[hit];
677
+ }
678
+ async function candidateStoragePaths(exec, container) {
679
+ const found = [];
680
+ const collect = (text) => {
681
+ for (const m of (text ?? "").matchAll(/acme\.storage[=:\s]+["']?(\/[^\s"',}]+)/gi)) {
682
+ if (isSafeCertPath(m[1])) found.push(m[1]);
683
+ }
684
+ for (const m of (text ?? "").matchAll(/(?:^|\s)storage:\s*["']?(\/[^\s"',}]+\.json)/gi)) {
685
+ if (isSafeCertPath(m[1])) found.push(m[1]);
686
+ }
687
+ };
688
+ if (container) {
689
+ collect(
690
+ await tryExec(
691
+ exec,
692
+ `docker inspect ${sq2(container)} --format '{{json .Config.Cmd}} {{json .Args}} {{json .Config.Env}}' 2>/dev/null`
693
+ )
694
+ );
695
+ for (const cfg of TRAEFIK_STATIC_CONFIGS) {
696
+ collect(await readMaybeInContainer(exec, cfg, container));
697
+ }
698
+ }
699
+ return [.../* @__PURE__ */ new Set([...found, ...DEFAULT_ACME_PATHS])];
700
+ }
701
+ async function loadAcmeJson(exec, container) {
702
+ for (const path of await candidateStoragePaths(exec, container)) {
703
+ const raw = await readMaybeInContainer(exec, path, container);
704
+ if (raw.trim().startsWith("{")) return { raw, path };
705
+ }
706
+ return null;
707
+ }
708
+ function certFromAcmeJson(raw, host) {
709
+ let parsed;
796
710
  try {
797
- return await executor.exec(command);
711
+ parsed = JSON.parse(raw);
798
712
  } catch {
799
713
  return null;
800
714
  }
715
+ const target = host.toLowerCase();
716
+ const matches = (name) => {
717
+ const n = (name ?? "").toLowerCase();
718
+ if (!n) return false;
719
+ if (n === target) return true;
720
+ if (!n.startsWith("*.")) return false;
721
+ const suffix = n.slice(1);
722
+ return target.endsWith(suffix) && !target.slice(0, -suffix.length).includes(".");
723
+ };
724
+ for (const [resolver, value] of Object.entries(parsed)) {
725
+ if (!value || typeof value !== "object") continue;
726
+ const certs = prop(value, "certificates");
727
+ if (!Array.isArray(certs)) continue;
728
+ for (const entry of certs) {
729
+ const domain = entry && prop(entry, "domain");
730
+ const names = [domain?.main, ...domain?.sans ?? []];
731
+ if (!names.some(matches)) continue;
732
+ const certB64 = prop(entry, "certificate");
733
+ const keyB64 = prop(entry, "key");
734
+ if (typeof certB64 !== "string" || typeof keyB64 !== "string") continue;
735
+ const certPem = Buffer.from(certB64, "base64").toString("utf-8");
736
+ const keyPem = Buffer.from(keyB64, "base64").toString("utf-8");
737
+ if (!certPem.includes("-----BEGIN") || !keyPem.includes("-----BEGIN")) continue;
738
+ return { certPem, keyPem, resolver };
739
+ }
740
+ }
741
+ return null;
801
742
  }
802
- function stripComments(text) {
803
- return text.replace(/#.*$/gm, "");
743
+ async function traefikAcmeCert(exec, host, container) {
744
+ const loaded = await loadAcmeJson(exec, container);
745
+ if (!loaded) return null;
746
+ const hit = certFromAcmeJson(loaded.raw, host);
747
+ if (!hit) return null;
748
+ return {
749
+ certPem: hit.certPem,
750
+ keyPem: hit.keyPem,
751
+ source: `traefik ${loaded.path} (resolver ${hit.resolver})`
752
+ };
804
753
  }
805
- function extractBlocks(text, keyword) {
806
- const blocks = [];
807
- const re = new RegExp(`(?:^|[\\s;}])${keyword}\\b[^{;]*\\{`, "g");
808
- let m;
809
- while ((m = re.exec(text)) !== null) {
810
- const openIdx = m.index + m[0].length - 1;
811
- let depth = 1;
812
- let i = openIdx + 1;
813
- for (; i < text.length && depth > 0; i++) {
814
- if (text[i] === "{") depth++;
815
- else if (text[i] === "}") depth--;
816
- }
817
- if (depth === 0) {
818
- blocks.push(text.slice(openIdx + 1, i - 1));
819
- re.lastIndex = i;
820
- } else {
821
- break;
754
+ async function traefikDeclaredCertPaths(exec, container) {
755
+ const pairs = [];
756
+ for (const cfg of TRAEFIK_STATIC_CONFIGS) {
757
+ const text = await readMaybeInContainer(exec, cfg, container);
758
+ if (!text.trim()) continue;
759
+ const certFiles = [...text.matchAll(/certFile:\s*["']?(\/[^\s"',}]+)/gi)].map((m) => m[1]);
760
+ const keyFiles = [...text.matchAll(/keyFile:\s*["']?(\/[^\s"',}]+)/gi)].map((m) => m[1]);
761
+ for (let i = 0; i < Math.min(certFiles.length, keyFiles.length); i++) {
762
+ if (isSafeCertPath(certFiles[i]) && isSafeCertPath(keyFiles[i])) {
763
+ pairs.push({ certPath: certFiles[i], keyPath: keyFiles[i] });
764
+ }
822
765
  }
823
766
  }
824
- return blocks;
825
- }
826
- function sq3(value) {
827
- return `'${value.replace(/'/g, "'\\''")}'`;
767
+ return pairs;
828
768
  }
829
769
 
830
- // ../../packages/adapters/src/system/proxy/import/nginx.ts
831
- async function loadNginxConfig(executor) {
832
- const dumped = await tryExec4(executor, "nginx -T 2>/dev/null");
833
- if (dumped && /server\s*\{/.test(dumped)) return dumped;
834
- const cat = await tryExec4(
835
- executor,
836
- "cat /etc/nginx/nginx.conf /etc/nginx/sites-enabled/* /etc/nginx/conf.d/*.conf 2>/dev/null"
837
- );
838
- return cat ?? "";
839
- }
840
- function firstDirective(body, name) {
841
- const m = body.match(new RegExp(`(?:^|[;{\\s])${name}\\s+([^;]+);`));
842
- return m?.[1]?.trim();
843
- }
844
- function parseUpstreams(config) {
845
- const map = /* @__PURE__ */ new Map();
846
- const re = /(?:^|[\s;}])upstream\s+(\S+)\s*\{([^}]*)\}/g;
847
- let m;
848
- while ((m = re.exec(config)) !== null) {
849
- const name = m[1];
850
- const server = m[2].match(/(?:^|[\s;{])server\s+([^;\s]+)/);
851
- if (name && server?.[1]) map.set(name, server[1].trim());
852
- }
853
- return map;
854
- }
855
- function resolveProxyTarget(proxyPass, upstreams) {
856
- const raw = proxyPass.replace(/;$/, "").trim();
857
- if (raw.includes("$")) return { reason: `proxy_pass "${raw}" uses an nginx variable` };
858
- if (/\/\/unix:/i.test(raw)) return { reason: `proxy_pass "${raw}" targets a unix socket` };
859
- const m = raw.match(/^(https?:\/\/)([^/]+)(\/.*)?$/i);
860
- if (!m) return { reason: `unrecognized proxy_pass "${raw}"` };
861
- const scheme = m[1];
862
- const authority = m[2];
863
- const host = authority.replace(/:\d+$/, "");
864
- if (upstreams.has(host)) return { url: `${scheme}${upstreams.get(host)}` };
865
- const isIp = /^\d{1,3}(\.\d{1,3}){3}$/.test(host);
866
- if (isIp || host === "localhost" || host.includes(".")) return { url: raw };
867
- return { reason: `proxy_pass host "${host}" is an undeclared upstream \u2014 not migratable` };
868
- }
869
- function extractLocationProxies(serverBody) {
870
- const out = [];
871
- const re = /(?:^|[\s;}])location\s+([^{]+?)\s*\{/g;
872
- let m;
873
- while ((m = re.exec(serverBody)) !== null) {
874
- const path = m[1].trim();
875
- const openIdx = m.index + m[0].length - 1;
876
- let depth = 1;
877
- let i = openIdx + 1;
878
- for (; i < serverBody.length && depth > 0; i++) {
879
- if (serverBody[i] === "{") depth++;
880
- else if (serverBody[i] === "}") depth--;
770
+ // ../../packages/adapters/src/system/proxy/api.ts
771
+ function buildProxyRouteIndex(sites) {
772
+ const byPort = /* @__PURE__ */ new Map();
773
+ for (const site of sites) {
774
+ const upstreams = site.routes ?? (site.target.kind === "proxy" ? [{ path: "/", url: site.target.url }] : []);
775
+ for (const up of upstreams) {
776
+ let port;
777
+ try {
778
+ port = Number(new URL(up.url).port);
779
+ } catch {
780
+ continue;
781
+ }
782
+ if (!Number.isFinite(port) || port <= 0) continue;
783
+ const list = byPort.get(port) ?? [];
784
+ const existing = list.find((r) => r.path === up.path);
785
+ if (existing) {
786
+ existing.domains = [.../* @__PURE__ */ new Set([...existing.domains, ...site.serverNames])];
787
+ if (site.ssl) {
788
+ existing.ssl.enabled = true;
789
+ if (!existing.ssl.certPath && site.tls) {
790
+ existing.ssl.certPath = site.tls.certPath;
791
+ existing.ssl.keyPath = site.tls.keyPath;
792
+ }
793
+ }
794
+ } else {
795
+ list.push({
796
+ port,
797
+ path: up.path,
798
+ domains: [...site.serverNames],
799
+ ssl: site.ssl ? { enabled: true, certPath: site.tls?.certPath, keyPath: site.tls?.keyPath } : { enabled: false },
800
+ source: site.source
801
+ });
802
+ }
803
+ byPort.set(port, list);
881
804
  }
882
- if (depth !== 0) break;
883
- const locBody = serverBody.slice(openIdx + 1, i - 1);
884
- re.lastIndex = i;
885
- const pp = firstDirective(locBody, "proxy_pass");
886
- if (pp) out.push({ path, proxyPass: pp });
887
- }
888
- return out;
889
- }
890
- function isHttpsUpgradeForSelf(body, names) {
891
- const targets = [
892
- ...body.matchAll(/(?:^|[\s;{])return\s+30[1-8]\s+([^;]+);/g),
893
- ...body.matchAll(/(?:^|[\s;{])rewrite\s+\S+\s+([^;]+?)\s+permanent\s*;/g)
894
- ].map((m) => m[1].trim());
895
- if (targets.length === 0) return false;
896
- return targets.every((raw) => {
897
- if (!/^https:\/\//i.test(raw)) return false;
898
- const authority = raw.replace(/^https:\/\//i, "").split(/[/\s]/)[0] ?? "";
899
- const host = (authority.match(/^(\$[a-z_]+|[^$:]+)/i)?.[1] ?? "").replace(/:\d+$/, "");
900
- if (host === "$host" || host === "$server_name" || host === "$http_host") return true;
901
- return names.includes(host);
902
- });
903
- }
904
- function parseServer(body, source, upstreams) {
905
- const warnings = [];
906
- const names = firstDirective(body, "server_name")?.split(/\s+/).filter((n) => n && n !== "_" && !n.startsWith("~")) ?? [];
907
- const listens = [...body.matchAll(/(?:^|[;{\s])listen\s+([^;]+);/g)].map((m) => m[1]);
908
- const ssl = listens.some((l) => /\bssl\b/.test(l) || /\b443\b/.test(l));
909
- const root = firstDirective(body, "root");
910
- const certPath = firstDirective(body, "ssl_certificate");
911
- const keyPath = firstDirective(body, "ssl_certificate_key");
912
- if (names.length === 0) return { warnings: [] };
913
- const rawTargets = extractLocationProxies(body);
914
- if (rawTargets.length === 0) {
915
- const serverLevel = firstDirective(body, "proxy_pass");
916
- if (serverLevel) rawTargets.push({ path: "/", proxyPass: serverLevel });
917
- }
918
- const resolved = [];
919
- for (const t of rawTargets) {
920
- const r = resolveProxyTarget(t.proxyPass, upstreams);
921
- if ("reason" in r) warnings.push(`nginx: ${names[0]} ${t.path} \u2014 ${r.reason} (skipped)`);
922
- else resolved.push({ path: t.path, url: r.url });
923
- }
924
- let target;
925
- let routes;
926
- if (resolved.length > 0) {
927
- const primary = resolved.find((r) => r.path === "/") ?? resolved[0];
928
- target = { kind: "proxy", url: primary.url };
929
- routes = resolved;
930
- } else if (root) {
931
- target = { kind: "static", root: root.replace(/;$/, "") };
932
- } else if (isHttpsUpgradeForSelf(body, names)) {
933
- return { warnings: [] };
934
- } else {
935
- warnings.push(`nginx: ${names[0]} has neither proxy_pass nor root \u2014 skipped (${source})`);
936
- return { warnings };
937
805
  }
938
- const site = { serverNames: names, ssl, target, source };
939
- if (routes) site.routes = routes;
940
- if (certPath && keyPath) site.tls = { certPath, keyPath };
941
- return { site, warnings };
942
- }
943
- function parseNginxConfig(raw) {
806
+ return byPort;
807
+ }
808
+ async function declaredCert(ctx) {
809
+ const tls = ctx.site?.tls;
810
+ if (!tls) return null;
811
+ const pems = await readDeclaredPair(ctx.exec, tls.certPath, tls.keyPath, ctx.container);
812
+ return pems ? { ...pems, source: tls.certPath } : null;
813
+ }
814
+ var CERT_SOURCES = {
815
+ nginx: [declaredCert],
816
+ openresty: [declaredCert],
817
+ apache: [declaredCert],
818
+ caddy: [declaredCert, (c) => caddyStoreCert(c.exec, c.host, c.container)],
819
+ traefik: [
820
+ (c) => traefikAcmeCert(c.exec, c.host, c.container),
821
+ // File provider: paths aren't bound to a hostname (traefik picks by SNI), so
822
+ // hand every declared pair to the validator and let coverage decide.
823
+ async (c) => {
824
+ for (const pair of await traefikDeclaredCertPaths(c.exec, c.container)) {
825
+ const pems = await readDeclaredPair(c.exec, pair.certPath, pair.keyPath, c.container);
826
+ if (!pems) continue;
827
+ if (validateCertFor(c.host, pems, pair.certPath).cert) {
828
+ return { ...pems, source: pair.certPath };
829
+ }
830
+ }
831
+ return null;
832
+ }
833
+ ],
834
+ // Takeover-only: no importable config, so no cert to find either.
835
+ haproxy: []
836
+ };
837
+ async function edgeProxy(exec, opts = {}) {
838
+ const status = opts.status ?? await probeEdge(exec).catch(() => null);
839
+ if (!status) return null;
840
+ let kind = null;
841
+ let ours = false;
842
+ if (status.classification === "ours") {
843
+ kind = "openresty";
844
+ ours = true;
845
+ } else if (status.classification === "known") {
846
+ const found = status.occupants.find((o) => o.proxy)?.proxy;
847
+ kind = found && canImportProxy(found) ? found : null;
848
+ }
849
+ if (!kind) kind = await detectInstalledProxy(exec).catch(() => null);
850
+ if (!kind) return null;
851
+ const container = status.occupants.find((o) => o.proxy === kind)?.containerName ?? null;
852
+ return makeApi(exec, kind, ours, container);
853
+ }
854
+ function edgeProxyFor(exec, kind, opts = {}) {
855
+ return makeApi(exec, kind, opts.ours ?? kind === "openresty", opts.container ?? null);
856
+ }
857
+ async function collectProxyCerts(api, sites) {
858
+ const certPems = {};
944
859
  const warnings = [];
945
- const sites = [];
946
- if (!raw.trim()) {
947
- return { proxy: "nginx", sites, warnings: ["nginx: no readable configuration found"] };
948
- }
949
- const config = stripComments(raw);
950
- const upstreams = parseUpstreams(config);
951
- const blocks = extractBlocks(config, "server");
952
- if (blocks.length === 0) {
953
- warnings.push("nginx: no server blocks found");
954
- }
955
- for (const body of blocks) {
956
- const { site, warnings: blockWarnings } = parseServer(body, "nginx", upstreams);
957
- warnings.push(...blockWarnings);
958
- if (site) sites.push(site);
860
+ const hosts = [...new Set(sites.filter((s) => s.ssl).flatMap((s) => s.serverNames))];
861
+ for (const host of hosts) {
862
+ const candidate = await api.certCandidateFor(host).catch(() => null);
863
+ if (candidate?.cert) {
864
+ certPems[host] = { certPem: candidate.cert.certPem, keyPem: candidate.cert.keyPem };
865
+ } else if (candidate?.reason) {
866
+ warnings.push(`${host}: no reusable certificate \u2014 ${candidate.reason}`);
867
+ }
959
868
  }
960
- return { proxy: "nginx", sites, warnings };
961
- }
962
- async function scanNginx(executor) {
963
- return parseNginxConfig(await loadNginxConfig(executor));
869
+ return { certPems, warnings };
870
+ }
871
+ function makeApi(exec, kind, ours, container) {
872
+ let scan = null;
873
+ const listSites = () => {
874
+ scan ??= (ours ? scanOpenshipEdge(exec) : scanImportableSites(exec, kind)).catch(() => ({
875
+ proxy: kind,
876
+ sites: [],
877
+ warnings: [`${kind}: config scan failed`]
878
+ }));
879
+ return scan;
880
+ };
881
+ const siteFor = async (host) => {
882
+ const target = host.toLowerCase();
883
+ const { sites } = await listSites();
884
+ return sites.find((s) => s.serverNames.some((n) => n.toLowerCase() === target)) ?? // A wildcard vhost (`*.example.com`) serves the host without naming it.
885
+ sites.find(
886
+ (s) => s.serverNames.some((n) => {
887
+ const name = n.toLowerCase();
888
+ if (!name.startsWith("*.")) return false;
889
+ const suffix = name.slice(1);
890
+ return target.endsWith(suffix) && !target.slice(0, -suffix.length).includes(".");
891
+ })
892
+ ) ?? null;
893
+ };
894
+ const certCandidateFor = async (host) => {
895
+ const site = await siteFor(host);
896
+ const ctx = { exec, host, site, container };
897
+ let lastReason = null;
898
+ for (const source of CERT_SOURCES[kind] ?? []) {
899
+ const raw = await source(ctx).catch(() => null);
900
+ if (!raw) continue;
901
+ const candidate = validateCertFor(host, raw, raw.source);
902
+ if (candidate.cert) return candidate;
903
+ lastReason = candidate.reason;
904
+ }
905
+ return { cert: null, reason: lastReason ?? `${kind}: no certificate found for ${host}` };
906
+ };
907
+ return {
908
+ kind,
909
+ ours,
910
+ container,
911
+ listSites,
912
+ sitesByPort: async () => buildProxyRouteIndex((await listSites()).sites),
913
+ siteFor,
914
+ certFor: async (host) => (await certCandidateFor(host)).cert,
915
+ certCandidateFor
916
+ };
964
917
  }
965
918
 
966
- // ../../packages/adapters/src/system/proxy/import/caddy.ts
967
- var CADDYFILE_PATHS = ["/etc/caddy/Caddyfile", "/etc/Caddyfile"];
968
- async function loadCaddyJson(executor) {
969
- for (const p of CADDYFILE_PATHS) {
970
- const out = await tryExec4(executor, `caddy adapt --adapter caddyfile --config ${p} 2>/dev/null`);
971
- if (out && out.trim().startsWith("{")) return out;
919
+ // ../../packages/adapters/src/system/proxy/takeover-journal.ts
920
+ var JOURNAL_DIR = "/var/lib/openship";
921
+ var JOURNAL_PATH = `${JOURNAL_DIR}/edge-takeover.json`;
922
+ function log(message, level = "info") {
923
+ return { timestamp: (/* @__PURE__ */ new Date()).toISOString(), message, level };
924
+ }
925
+ async function tryExec2(executor, cmd) {
926
+ try {
927
+ return await executor.exec(cmd);
928
+ } catch {
929
+ return null;
972
930
  }
973
- return null;
974
931
  }
975
- function flattenHandlers(handlers, out = []) {
976
- for (const h of handlers ?? []) {
977
- if (h.handler === "subroute") {
978
- for (const r of h.routes ?? []) flattenHandlers(r.handle, out);
979
- } else {
980
- out.push(h);
932
+ async function buildJournal(executor, status) {
933
+ const units = /* @__PURE__ */ new Map();
934
+ const containers = /* @__PURE__ */ new Map();
935
+ const processes = /* @__PURE__ */ new Map();
936
+ for (const o of status.occupants) {
937
+ if (o.containerName && !containers.has(o.containerName)) {
938
+ const r = await tryExec2(
939
+ executor,
940
+ `docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' ${sq2(o.containerName)} 2>/dev/null`
941
+ );
942
+ containers.set(o.containerName, { name: o.containerName, restart: r?.trim() || "no" });
943
+ } else if (o.systemdUnit && !units.has(o.systemdUnit)) {
944
+ const en = await tryExec2(executor, `systemctl is-enabled ${sq2(o.systemdUnit)} 2>/dev/null`);
945
+ units.set(o.systemdUnit, { unit: o.systemdUnit, wasEnabled: en?.trim() === "enabled" });
946
+ } else if (!o.containerName && !o.systemdUnit && o.pid && !processes.has(o.pid)) {
947
+ processes.set(o.pid, { pid: o.pid, command: o.rawCommand });
981
948
  }
982
949
  }
983
- return out;
950
+ return {
951
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
952
+ units: [...units.values()],
953
+ containers: [...containers.values()],
954
+ processes: [...processes.values()]
955
+ };
984
956
  }
985
- function targetFromRoute(route) {
986
- const flat = flattenHandlers(route.handle);
987
- const dial = flat.find((h) => h.handler === "reverse_proxy")?.upstreams?.find((u) => u.dial)?.dial;
988
- if (dial) return { kind: "proxy", url: /^https?:\/\//.test(dial) ? dial : `http://${dial}` };
989
- const root = flat.find((h) => h.handler === "vars" && typeof h.root === "string")?.root;
990
- if (root && flat.some((h) => h.handler === "file_server")) return { kind: "static", root };
991
- return null;
957
+ async function writeJournal(executor, journal) {
958
+ try {
959
+ await executor.mkdir(JOURNAL_DIR);
960
+ await executor.writeFile(JOURNAL_PATH, JSON.stringify(journal, null, 2));
961
+ } catch {
962
+ }
992
963
  }
993
- function parseCaddyJson(raw) {
994
- let cfg;
964
+ async function clearJournal(executor) {
965
+ await tryExec2(executor, `rm -f ${JOURNAL_PATH}`);
966
+ }
967
+ async function readJournal(executor) {
968
+ const raw = await tryExec2(executor, `cat ${JOURNAL_PATH} 2>/dev/null`);
969
+ if (!raw?.trim()) return null;
995
970
  try {
996
- cfg = JSON.parse(raw);
971
+ return JSON.parse(raw);
997
972
  } catch {
973
+ await clearJournal(executor);
998
974
  return null;
999
975
  }
1000
- const servers = cfg.apps?.http?.servers;
1001
- if (!servers) return null;
1002
- const sites = [];
1003
- for (const srv of Object.values(servers)) {
1004
- const ssl = (srv.listen ?? []).some((l) => l.endsWith(":443"));
1005
- for (const route of srv.routes ?? []) {
1006
- const hosts = (route.match ?? []).flatMap((m) => m.host ?? []).filter((h) => h && h !== "*");
1007
- if (hosts.length === 0) continue;
1008
- const target = targetFromRoute(route);
1009
- if (!target) continue;
1010
- sites.push({ serverNames: hosts, ssl, target, source: "caddy (adapt)" });
1011
- }
1012
- }
1013
- return { sites, warnings: [] };
1014
976
  }
1015
- async function loadCaddyfile(executor) {
1016
- for (const p of CADDYFILE_PATHS) {
1017
- const out = await tryExec4(executor, `cat ${p} 2>/dev/null`);
1018
- if (out && out.trim()) return out;
977
+ async function rollback(executor, journal, onLog) {
978
+ onLog(log("Rolling back \u2014 restoring the previous proxy...", "warn"));
979
+ await tryExec2(
980
+ executor,
981
+ "systemctl disable --now openresty 2>/dev/null || systemctl stop openresty 2>/dev/null || true; systemctl reset-failed openresty 2>/dev/null || true"
982
+ );
983
+ for (const u of journal.units) {
984
+ await tryExec2(
985
+ executor,
986
+ u.wasEnabled ? `systemctl enable --now ${sq2(u.unit)} 2>/dev/null || true` : `systemctl start ${sq2(u.unit)} 2>/dev/null || true`
987
+ );
1019
988
  }
1020
- return "";
1021
- }
1022
- function caddyBlocks(text) {
1023
- const out = [];
1024
- let i = 0;
1025
- let headerStart = 0;
1026
- while (i < text.length) {
1027
- if (text[i] === "{") {
1028
- const header = text.slice(headerStart, i).trim();
1029
- let depth = 1;
1030
- let j = i + 1;
1031
- for (; j < text.length && depth > 0; j++) {
1032
- if (text[j] === "{") depth++;
1033
- else if (text[j] === "}") depth--;
1034
- }
1035
- if (depth !== 0) break;
1036
- out.push({ header, body: text.slice(i + 1, j - 1) });
1037
- i = j;
1038
- headerStart = j;
989
+ for (const c of journal.containers) {
990
+ await tryExec2(executor, `docker update --restart=${sq2(c.restart)} ${sq2(c.name)} 2>/dev/null || true`);
991
+ await tryExec2(executor, `docker start ${sq2(c.name)} 2>/dev/null || true`);
992
+ }
993
+ for (const p of journal.processes ?? []) {
994
+ if (p.command) {
995
+ await tryExec2(
996
+ executor,
997
+ `setsid -f sh -c ${sq2(p.command)} 2>/dev/null || (nohup sh -c ${sq2(p.command)} >/dev/null 2>&1 &) || true`
998
+ );
1039
999
  } else {
1040
- i++;
1000
+ onLog(log(`Could not restore process ${p.pid} \u2014 no command captured.`, "warn"));
1041
1001
  }
1042
1002
  }
1043
- return out;
1044
1003
  }
1045
- function parseAddresses(header) {
1046
- return header.split(/[\s,]+/).filter(Boolean).map((addr) => {
1047
- const httpScheme = /^http:\/\//i.test(addr);
1048
- const host = addr.replace(/^https?:\/\//i, "").replace(/:\d+$/, "");
1049
- const isPort80 = /:80$/.test(addr);
1050
- return { host, ssl: !httpScheme && !isPort80 };
1051
- }).filter((a) => a.host && !a.host.startsWith(":") && a.host !== "*");
1004
+ async function beginEdgeTakeover(executor, status, onLog) {
1005
+ await writeJournal(executor, await buildJournal(executor, status));
1006
+ await freeEdgeTargets(executor, stopTargetsForStatus(status), (m, l) => onLog(log(m, l)));
1052
1007
  }
1053
- async function scanCaddy(executor) {
1054
- const json = await loadCaddyJson(executor);
1055
- if (json) {
1056
- const parsed = parseCaddyJson(json);
1057
- if (parsed && parsed.sites.length > 0) {
1058
- return { proxy: "caddy", sites: parsed.sites, warnings: parsed.warnings };
1059
- }
1060
- }
1061
- return scanCaddyText(executor);
1008
+ async function rollbackEdgeTakeover(executor, onLog) {
1009
+ const journal = await readJournal(executor);
1010
+ if (!journal || journal.completed) return false;
1011
+ await rollback(executor, journal, onLog);
1012
+ await clearJournal(executor);
1013
+ return true;
1062
1014
  }
1063
- async function scanCaddyText(executor) {
1064
- const raw = await loadCaddyfile(executor);
1065
- const warnings = [];
1066
- const sites = [];
1067
- if (!raw.trim()) {
1068
- return { proxy: "caddy", sites, warnings: ["caddy: no readable Caddyfile found"] };
1015
+ async function completeEdgeTakeover(executor) {
1016
+ const journal = await readJournal(executor);
1017
+ if (journal) {
1018
+ journal.completed = true;
1019
+ await writeJournal(executor, journal);
1069
1020
  }
1070
- const text = stripComments(raw);
1071
- let blocks = caddyBlocks(text);
1072
- if (blocks.length === 0) {
1073
- const lines = text.split("\n").map((l) => l.trim()).filter(Boolean);
1074
- if (lines.length > 0) {
1075
- blocks = [{ header: lines[0], body: lines.slice(1).join("\n") }];
1076
- } else {
1077
- warnings.push("caddy: config present but no parseable site blocks");
1078
- }
1021
+ await clearJournal(executor);
1022
+ }
1023
+ async function recoverInterruptedTakeover(executor, onLog, isEdgeHealthy) {
1024
+ const journal = await readJournal(executor);
1025
+ if (!journal) return;
1026
+ if (journal.completed) {
1027
+ await clearJournal(executor);
1028
+ return;
1079
1029
  }
1080
- for (const { header, body } of blocks) {
1081
- if (!header) continue;
1082
- const addrs = parseAddresses(header);
1083
- if (addrs.length === 0) {
1084
- warnings.push(`caddy: skipped a block with no usable address (${header.slice(0, 40)})`);
1085
- continue;
1086
- }
1087
- const proxyMatch = body.match(/(?:^|\s)reverse_proxy\s+([^\n{]+)/);
1088
- const rootMatch = body.match(/(?:^|\s)root\s+\*?\s*([^\n\s]+)/);
1089
- const tlsMatch = body.match(/(?:^|\s)tls\s+(\S+)\s+(\S+)/);
1090
- let target;
1091
- if (proxyMatch) {
1092
- const upstream = proxyMatch[1].trim().split(/\s+/)[0];
1093
- target = { kind: "proxy", url: /^https?:\/\//.test(upstream) ? upstream : `http://${upstream}` };
1094
- } else if (rootMatch) {
1095
- target = { kind: "static", root: rootMatch[1] };
1096
- } else {
1097
- warnings.push(`caddy: ${addrs[0].host} has no reverse_proxy or root \u2014 skipped`);
1098
- continue;
1099
- }
1100
- const site = {
1101
- serverNames: addrs.map((a) => a.host),
1102
- ssl: addrs.some((a) => a.ssl),
1103
- target,
1104
- source: "caddy"
1105
- };
1106
- if (tlsMatch && tlsMatch[1].includes("/")) {
1107
- site.tls = { certPath: tlsMatch[1], keyPath: tlsMatch[2] };
1108
- }
1109
- sites.push(site);
1030
+ if (isEdgeHealthy && await isEdgeHealthy()) {
1031
+ onLog(log("Found an unfinished edge takeover, but our edge is serving \u2014 keeping it.", "warn"));
1032
+ await clearJournal(executor);
1033
+ return;
1110
1034
  }
1111
- return { proxy: "caddy", sites, warnings };
1035
+ onLog(log("Found an interrupted edge takeover \u2014 restoring the previous proxy.", "warn"));
1036
+ await rollback(executor, journal, onLog);
1037
+ await clearJournal(executor);
1112
1038
  }
1113
1039
 
1114
- // ../../packages/adapters/src/system/proxy/import/apache.ts
1115
- async function vhostFilesFromDump(executor) {
1116
- const dump = await tryExec4(
1117
- executor,
1118
- "apachectl -S 2>/dev/null || apache2ctl -S 2>/dev/null || httpd -S 2>/dev/null"
1119
- );
1120
- if (!dump) return [];
1121
- const files = /* @__PURE__ */ new Set();
1122
- for (const m of dump.matchAll(/\(([^()]+?):\d+\)/g)) {
1123
- const f = m[1].trim();
1124
- if (f.startsWith("/")) files.add(f);
1040
+ // ../../packages/adapters/src/system/catalog.ts
1041
+ function dockerInstallPlan(profile) {
1042
+ if (profile.os !== "linux") {
1043
+ return {
1044
+ supported: false,
1045
+ unsupportedReason: "Docker installation is only supported on Linux servers"
1046
+ };
1125
1047
  }
1126
- return [...files];
1048
+ return {
1049
+ supported: true,
1050
+ installCommand: "curl -fsSL https://get.docker.com | sh",
1051
+ startCommand: profile.serviceManager === "systemd" ? "systemctl enable docker && systemctl start docker" : void 0,
1052
+ verifyCommand: "docker --version"
1053
+ };
1127
1054
  }
1128
- async function loadApacheConfig(executor) {
1129
- const files = await vhostFilesFromDump(executor);
1130
- if (files.length > 0) {
1131
- const cat2 = await tryExec4(executor, `cat ${files.map(sq3).join(" ")} 2>/dev/null`);
1132
- if (cat2 && cat2.trim()) return cat2;
1055
+ function gitInstallPlan(profile) {
1056
+ const commands = {
1057
+ apt: "apt-get update -qq && apt-get install -y -qq git",
1058
+ dnf: "dnf install -y git",
1059
+ yum: "yum install -y git",
1060
+ apk: "apk add --no-cache git",
1061
+ brew: "brew install git"
1062
+ };
1063
+ const installCommand = commands[profile.packageManager];
1064
+ if (!installCommand) {
1065
+ return {
1066
+ supported: false,
1067
+ unsupportedReason: "No supported package manager found for Git installation"
1068
+ };
1133
1069
  }
1134
- const cat = await tryExec4(
1135
- executor,
1136
- "cat /etc/apache2/sites-enabled/*.conf /etc/apache2/apache2.conf /etc/httpd/conf.d/*.conf /etc/httpd/conf/httpd.conf 2>/dev/null"
1137
- );
1138
- return cat ?? "";
1139
- }
1140
- function directive(body, name) {
1141
- const m = body.match(new RegExp(`(?:^|\\n)\\s*${name}\\s+([^\\n]+)`, "i"));
1142
- return m?.[1]?.trim();
1143
- }
1144
- function directiveAll(body, name) {
1145
- return [...body.matchAll(new RegExp(`(?:^|\\n)\\s*${name}\\s+([^\\n]+)`, "gi"))].map(
1146
- (m) => m[1].trim()
1147
- );
1070
+ return {
1071
+ supported: true,
1072
+ installCommand,
1073
+ verifyCommand: "git --version"
1074
+ };
1148
1075
  }
1149
- function parseVhost(body, portHint) {
1150
- const serverName = directive(body, "ServerName")?.split(/\s+/)[0];
1151
- const aliases = directiveAll(body, "ServerAlias").flatMap((line) => line.split(/\s+/));
1152
- const names = [serverName, ...aliases].filter((n) => Boolean(n));
1153
- if (names.length === 0) {
1154
- return { warning: "apache: skipped a VirtualHost with no ServerName" };
1155
- }
1156
- const routes = [...body.matchAll(/(?:^|\n)\s*ProxyPass\s+(\S+)\s+(\S+)/gi)].map((m) => ({ path: m[1], url: m[2] })).filter((p) => p.url && p.url !== "!");
1157
- const docRoot = directive(body, "DocumentRoot")?.replace(/^["']|["']$/g, "");
1158
- const certPath = directive(body, "SSLCertificateFile");
1159
- const keyPath = directive(body, "SSLCertificateKeyFile");
1160
- const ssl = /:443/.test(portHint) || /SSLEngine\s+on/i.test(body) || Boolean(certPath);
1161
- let target;
1162
- if (routes.length > 0) {
1163
- const primary = routes.find((r) => r.path === "/") ?? routes[0];
1164
- target = { kind: "proxy", url: primary.url };
1165
- } else if (docRoot) {
1166
- target = { kind: "static", root: docRoot };
1167
- } else {
1168
- return { warning: `apache: ${names[0]} has neither ProxyPass nor DocumentRoot \u2014 skipped` };
1076
+ function rsyncInstallPlan(profile) {
1077
+ const commands = {
1078
+ apt: "apt-get update -qq && apt-get install -y -qq rsync",
1079
+ dnf: "dnf install -y rsync",
1080
+ yum: "yum install -y rsync",
1081
+ apk: "apk add --no-cache rsync",
1082
+ brew: "brew install rsync"
1083
+ };
1084
+ const installCommand = commands[profile.packageManager];
1085
+ if (!installCommand) {
1086
+ return {
1087
+ supported: false,
1088
+ unsupportedReason: "No supported package manager found for rsync installation"
1089
+ };
1169
1090
  }
1170
- const site = { serverNames: names, ssl, target, source: "apache" };
1171
- if (routes.length > 0) site.routes = routes;
1172
- if (certPath && keyPath) site.tls = { certPath, keyPath };
1173
- return site;
1091
+ return {
1092
+ supported: true,
1093
+ installCommand,
1094
+ verifyCommand: "rsync --version | head -n 1"
1095
+ };
1174
1096
  }
1175
- function vhostBlocks(text) {
1176
- const out = [];
1177
- const re = /<VirtualHost([^>]*)>([\s\S]*?)<\/VirtualHost>/gi;
1178
- let m;
1179
- while ((m = re.exec(text)) !== null) {
1180
- out.push({ args: m[1].trim(), body: m[2] });
1097
+ var OPENRESTY_APT_SOURCES = [
1098
+ // ARM64: openresty.org publishes aarch64 debs under a SEPARATE path
1099
+ // (openresty.org/package/arm64/<repo>); the default path is amd64-only, so on
1100
+ // aarch64 `apt-get install openresty` finds no candidate and `set -e` aborts
1101
+ // the whole setup (issue #109). Switch the base URL by arch and tag the deb
1102
+ // line with `arch=` so apt fetches the right index. `dpkg --print-architecture`
1103
+ // returns exactly amd64/arm64 (matches OpenResty's path + arch qualifier).
1104
+ 'OR_ARCH="$(dpkg --print-architecture 2>/dev/null || echo amd64)"',
1105
+ 'if [ "$OR_ARCH" = arm64 ]; then OR_BASE="http://openresty.org/package/arm64/$REPO"; else OR_BASE="http://openresty.org/package/$REPO"; fi',
1106
+ // `|| REPO_CODENAME=""` so a failed substitution (no lsb_release AND no
1107
+ // /etc/os-release, e.g. a stripped container) doesn't trip `set -e` before
1108
+ // the empty-var fallback below can pick a default.
1109
+ 'REPO_CODENAME="$(lsb_release -sc 2>/dev/null || { . /etc/os-release 2>/dev/null && echo "$VERSION_CODENAME"; })" || REPO_CODENAME=""',
1110
+ 'if [ "$REPO" = debian ]; then OR_FALLBACKS="bookworm bullseye"; else OR_FALLBACKS="noble jammy focal"; fi',
1111
+ 'OR_CODENAME=""',
1112
+ "for c in $REPO_CODENAME $OR_FALLBACKS; do",
1113
+ ' if wget -q --spider --tries=2 --timeout=15 "$OR_BASE/dists/$c/Release"; then OR_CODENAME="$c"; break; fi',
1114
+ "done",
1115
+ 'if [ -z "$OR_CODENAME" ]; then case "$REPO" in debian) OR_CODENAME=bookworm ;; *) OR_CODENAME=noble ;; esac; fi',
1116
+ '[ "$OR_CODENAME" = "$REPO_CODENAME" ] || echo "[openresty] apt repo has no codename $REPO_CODENAME; using nearest supported LTS $OR_CODENAME" >&2',
1117
+ 'echo "deb [arch=$OR_ARCH signed-by=/usr/share/keyrings/openresty.gpg] $OR_BASE $OR_CODENAME main" > /etc/apt/sources.list.d/openresty.list'
1118
+ ];
1119
+ function openrestyInstallPlan(profile) {
1120
+ if (profile.os !== "linux") {
1121
+ return {
1122
+ supported: false,
1123
+ unsupportedReason: "OpenResty installation is only supported on Linux servers"
1124
+ };
1181
1125
  }
1182
- return out;
1183
- }
1184
- async function scanApache(executor) {
1185
- const raw = await loadApacheConfig(executor);
1186
- const warnings = [];
1187
- const sites = [];
1188
- if (!raw.trim()) {
1189
- return { proxy: "apache", sites, warnings: ["apache: no readable configuration found"] };
1126
+ let installCommand;
1127
+ if (profile.packageManager === "apt") {
1128
+ const distro = profile.distro === "debian" ? "debian" : "ubuntu";
1129
+ installCommand = [
1130
+ "set -e",
1131
+ // Heal a stale/bad openresty.list left by a prior run (e.g. the #86
1132
+ // 'resolute' pin) BEFORE the first apt-get update reads it — otherwise
1133
+ // set -e aborts here and the repo never gets rewritten.
1134
+ "rm -f /etc/apt/sources.list.d/openresty.list",
1135
+ "apt-get update -qq && apt-get install -y -qq wget gnupg2 lsb-release",
1136
+ "wget -qO /tmp/openresty-pubkey.gpg https://openresty.org/package/pubkey.gpg",
1137
+ "gpg --yes --dearmor -o /usr/share/keyrings/openresty.gpg /tmp/openresty-pubkey.gpg",
1138
+ `REPO=${distro}`,
1139
+ ...OPENRESTY_APT_SOURCES,
1140
+ "apt-get update -qq && apt-get install -y -qq openresty"
1141
+ ].join("\n");
1142
+ } else if (profile.packageManager === "dnf") {
1143
+ const distro = profile.distro === "fedora" ? "fedora" : "centos";
1144
+ installCommand = `wget -qO /etc/yum.repos.d/openresty.repo https://openresty.org/package/${distro}/openresty.repo && dnf install -y openresty`;
1145
+ } else if (profile.packageManager === "yum") {
1146
+ installCommand = "wget -qO /etc/yum.repos.d/openresty.repo https://openresty.org/package/centos/openresty.repo && yum install -y openresty";
1147
+ } else if (profile.packageManager === "apk") {
1148
+ installCommand = [
1149
+ "apk add --no-cache wget",
1150
+ "wget -qO /etc/apk/keys/admin@openresty.com-5ea678a6.rsa.pub https://openresty.org/package/alpine/admin@openresty.com-5ea678a6.rsa.pub",
1151
+ `. /etc/os-release && echo "https://openresty.org/package/alpine/v$( echo $VERSION_ID | cut -d. -f1,2 )/main" >> /etc/apk/repositories`,
1152
+ "apk update && apk add openresty"
1153
+ ].join(" && ");
1154
+ } else {
1155
+ installCommand = [
1156
+ "set -e",
1157
+ "if command -v apt-get >/dev/null 2>&1; then",
1158
+ " rm -f /etc/apt/sources.list.d/openresty.list",
1159
+ " apt-get update -qq && apt-get install -y -qq wget gnupg2 lsb-release",
1160
+ " wget -qO /tmp/openresty-pubkey.gpg https://openresty.org/package/pubkey.gpg",
1161
+ " gpg --yes --dearmor -o /usr/share/keyrings/openresty.gpg /tmp/openresty-pubkey.gpg",
1162
+ ' DISTRO=$(. /etc/os-release 2>/dev/null && echo "$ID" || echo "ubuntu")',
1163
+ ' case "$DISTRO" in debian) REPO=debian ;; *) REPO=ubuntu ;; esac',
1164
+ ...OPENRESTY_APT_SOURCES.map((l) => " " + l),
1165
+ " apt-get update -qq && apt-get install -y -qq openresty",
1166
+ "elif command -v dnf >/dev/null 2>&1; then",
1167
+ " wget -qO /etc/yum.repos.d/openresty.repo https://openresty.org/package/centos/openresty.repo && dnf install -y openresty",
1168
+ "elif command -v yum >/dev/null 2>&1; then",
1169
+ " wget -qO /etc/yum.repos.d/openresty.repo https://openresty.org/package/centos/openresty.repo && yum install -y openresty",
1170
+ "elif command -v apk >/dev/null 2>&1; then",
1171
+ " apk add --no-cache wget \\",
1172
+ " && wget -qO /etc/apk/keys/admin@openresty.com-5ea678a6.rsa.pub https://openresty.org/package/alpine/admin@openresty.com-5ea678a6.rsa.pub \\",
1173
+ ' && . /etc/os-release && echo "https://openresty.org/package/alpine/v$(echo $VERSION_ID | cut -d. -f1,2)/main" >> /etc/apk/repositories \\',
1174
+ " && apk update && apk add openresty",
1175
+ "else",
1176
+ ' echo "No supported package manager found (tried apt-get, dnf, yum, apk)" >&2 && exit 1',
1177
+ "fi"
1178
+ ].join("\n");
1190
1179
  }
1191
- const text = stripComments(raw);
1192
- const blocks = vhostBlocks(text);
1193
- if (blocks.length === 0) warnings.push("apache: no <VirtualHost> blocks found");
1194
- for (const { args, body } of blocks) {
1195
- const parsed = parseVhost(body, args);
1196
- if ("warning" in parsed) warnings.push(parsed.warning);
1197
- else sites.push(parsed);
1180
+ return {
1181
+ supported: true,
1182
+ installCommand,
1183
+ startCommand: profile.serviceManager === "systemd" ? "systemctl enable openresty && systemctl start openresty" : void 0,
1184
+ verifyCommand: "openresty -v 2>&1"
1185
+ };
1186
+ }
1187
+ function certbotInstallPlan(profile) {
1188
+ const commands = {
1189
+ apt: "apt-get update -qq && apt-get install -y -qq certbot",
1190
+ dnf: "dnf install -y certbot",
1191
+ yum: "yum install -y certbot",
1192
+ apk: "apk add --no-cache certbot"
1193
+ };
1194
+ const installCommand = commands[profile.packageManager];
1195
+ if (!installCommand) {
1196
+ return {
1197
+ supported: false,
1198
+ unsupportedReason: "Certbot installation is only supported on Linux with apt, dnf, or yum"
1199
+ };
1198
1200
  }
1199
- return { proxy: "apache", sites, warnings };
1201
+ return {
1202
+ supported: true,
1203
+ installCommand,
1204
+ verifyCommand: "certbot --version 2>/dev/null"
1205
+ };
1200
1206
  }
1201
-
1202
- // ../../packages/adapters/src/system/proxy/import/traefik.ts
1203
- function extractHosts(rule) {
1204
- const hosts = [];
1205
- for (const call of rule.matchAll(/\bHost\(([^)]*)\)/gi)) {
1206
- for (const m of call[1].matchAll(/[`'"]([^`'"]+)[`'"]/g)) {
1207
- const h = m[1].trim();
1208
- if (h) hosts.push(h);
1207
+ var systemCatalog = {
1208
+ checks: {
1209
+ docker: {
1210
+ versionCommand: "docker --version",
1211
+ daemonCommand: "docker info --format '{{.ServerVersion}}'",
1212
+ parseVersion: (output) => output.match(/Docker version ([^\s,]+)/)?.[1] ?? output,
1213
+ missingMessage: "Docker is not installed",
1214
+ notRunningMessage: "Docker is installed but the daemon is not running"
1215
+ },
1216
+ openresty: {
1217
+ versionCommand: "openresty -v 2>&1 || /usr/local/openresty/bin/openresty -v 2>&1",
1218
+ runningCommands: [
1219
+ "pgrep -f 'nginx.*openresty' || pgrep -f '/usr/local/openresty'"
1220
+ ],
1221
+ parseVersion: (output) => output.match(/openresty\/(\S+)/)?.[1] ?? output.match(/nginx\/(\S+)/)?.[1] ?? output,
1222
+ missingMessage: "OpenResty is not installed",
1223
+ notRunningMessage: "OpenResty is installed but not running"
1224
+ },
1225
+ certbot: {
1226
+ versionCommand: "certbot --version 2>/dev/null",
1227
+ parseVersion: (output) => output.match(/certbot\s+(\S+)/)?.[1] ?? output,
1228
+ missingMessage: "Certbot is not installed"
1229
+ },
1230
+ git: {
1231
+ versionCommand: "git --version",
1232
+ parseVersion: (output) => output.match(/git version (\S+)/)?.[1] ?? output,
1233
+ missingMessage: "Git is not installed"
1234
+ },
1235
+ rsync: {
1236
+ versionCommand: "rsync --version | head -n 1",
1237
+ parseVersion: (output) => output.match(/rsync\s+version\s+(\S+)/i)?.[1] ?? output,
1238
+ missingMessage: "rsync is not installed"
1209
1239
  }
1240
+ },
1241
+ installs: {
1242
+ docker: dockerInstallPlan,
1243
+ git: gitInstallPlan,
1244
+ rsync: rsyncInstallPlan,
1245
+ openresty: openrestyInstallPlan,
1246
+ certbot: certbotInstallPlan
1210
1247
  }
1211
- return hosts;
1248
+ };
1249
+
1250
+ // ../../packages/adapters/src/system/elevated-executor.ts
1251
+ var INSTALL_ENV = "DEBIAN_FRONTEND=noninteractive DPKG_FORCE=confnew";
1252
+ function elevateCommand(command) {
1253
+ return `sudo -n sh -c ${sq(`export ${INSTALL_ENV}; ${command}`)}`;
1212
1254
  }
1213
- function extraMatchers(rule) {
1214
- const found = /* @__PURE__ */ new Set();
1215
- for (const m of rule.matchAll(
1216
- /\b(PathPrefix|PathRegexp|Path|HeadersRegexp|Headers|Method|Query|ClientIP|HostRegexp)\b/gi
1217
- )) {
1218
- found.add(m[1]);
1219
- }
1220
- return [...found];
1255
+ function dirOf(path) {
1256
+ const idx = path.lastIndexOf("/");
1257
+ if (idx < 0) return ".";
1258
+ return idx === 0 ? "/" : path.slice(0, idx);
1221
1259
  }
1222
- function parseTraefikLabels(containers) {
1223
- const sites = [];
1224
- const warnings = [];
1225
- for (const c of containers) {
1226
- if (c.labels["traefik.enable"] === "false") continue;
1227
- const routers = /* @__PURE__ */ new Map();
1228
- const servicePorts = /* @__PURE__ */ new Map();
1229
- for (const [key, val] of Object.entries(c.labels)) {
1230
- const r = key.match(/^traefik\.http\.routers\.([^.]+)\.(.+)$/);
1231
- if (r) {
1232
- const bag = routers.get(r[1]) ?? {};
1233
- bag[r[2]] = val;
1234
- routers.set(r[1], bag);
1235
- continue;
1236
- }
1237
- const s = key.match(/^traefik\.http\.services\.([^.]+)\.loadbalancer\.server\.port$/);
1238
- if (s) servicePorts.set(s[1], val);
1260
+ function elevatedExecutor(inner) {
1261
+ const writeFileElevated = async (path, content) => {
1262
+ const tmp = `/tmp/.openship-elev-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
1263
+ await inner.writeFile(tmp, content);
1264
+ await inner.exec(
1265
+ elevateCommand(`mkdir -p ${sq(dirOf(path))} && mv -f ${sq(tmp)} ${sq(path)}`)
1266
+ );
1267
+ };
1268
+ const overrides = {
1269
+ exec: (command, opts) => inner.exec(elevateCommand(command), opts),
1270
+ streamExec: (command, onLog) => inner.streamExec(elevateCommand(command), onLog),
1271
+ writeFile: writeFileElevated,
1272
+ // Elevated for the same reason as writeFile: the vhost/conf dirs are root-owned,
1273
+ // and the Proxy would otherwise forward this to the inner executor unelevated —
1274
+ // EACCES immediately after an elevated write succeeded.
1275
+ rename: async (from, to) => {
1276
+ await inner.exec(elevateCommand(`mv -f ${sq(from)} ${sq(to)}`));
1277
+ },
1278
+ mkdir: async (path) => {
1279
+ await inner.exec(elevateCommand(`mkdir -p ${sq(path)}`));
1280
+ },
1281
+ rm: async (path) => {
1282
+ await inner.exec(elevateCommand(`rm -rf ${sq(path)}`));
1239
1283
  }
1240
- for (const [rname, props] of routers) {
1241
- const rule = props.rule;
1242
- if (!rule) continue;
1243
- const hosts = extractHosts(rule);
1244
- if (hosts.length === 0) {
1245
- warnings.push(
1246
- /HostRegexp/i.test(rule) ? `traefik: router "${rname}" uses HostRegexp (${rule}) \u2014 regex hosts aren't migratable` : `traefik: router "${rname}" has no Host() rule (${rule}) \u2014 skipped`
1247
- );
1248
- continue;
1249
- }
1250
- if (!c.ip) {
1251
- warnings.push(
1252
- `traefik: ${hosts.join(", ")} \u2014 couldn't resolve container ${c.name}'s IP; re-add the upstream manually`
1253
- );
1254
- continue;
1255
- }
1256
- const svc = props.service;
1257
- const rawPort = (svc && servicePorts.get(svc)) ?? (servicePorts.size === 1 ? [...servicePorts.values()][0] : void 0) ?? "80";
1258
- const port = /^\d{1,5}$/.test(String(rawPort)) ? String(rawPort) : "80";
1259
- const url = `http://${c.ip}:${port}`;
1260
- const extras = extraMatchers(rule);
1261
- if (extras.length > 0) {
1262
- warnings.push(
1263
- `traefik: ${hosts.join(", ")} also matches ${extras.join(", ")} \u2014 only the Host() part is migrated; re-add path/header rules manually`
1264
- );
1265
- }
1266
- if (props.middlewares) {
1267
- warnings.push(`traefik: ${hosts.join(", ")} uses middleware(s) "${props.middlewares}" \u2014 not migrated`);
1284
+ };
1285
+ return new Proxy(inner, {
1286
+ get(target, prop2) {
1287
+ if (Object.prototype.hasOwnProperty.call(overrides, prop2)) {
1288
+ return overrides[prop2];
1268
1289
  }
1269
- const ssl = Object.keys(props).some((p) => p === "tls" || p.startsWith("tls."));
1270
- sites.push({
1271
- serverNames: hosts,
1272
- ssl,
1273
- target: { kind: "proxy", url },
1274
- source: `traefik container ${c.name}`
1275
- });
1290
+ const value = Reflect.get(target, prop2, target);
1291
+ return typeof value === "function" ? value.bind(target) : value;
1276
1292
  }
1293
+ });
1294
+ }
1295
+
1296
+ // ../../packages/adapters/src/system/installer.ts
1297
+ function log2(message, level = "info") {
1298
+ return { timestamp: (/* @__PURE__ */ new Date()).toISOString(), message, level };
1299
+ }
1300
+ async function prepareExecutor(executor, component) {
1301
+ const profile = await resolveEnvironment(executor);
1302
+ if (profile.isRoot) return { ok: true, executor, profile };
1303
+ if (profile.canSudo) return { ok: true, executor: elevatedExecutor(executor), profile };
1304
+ return {
1305
+ ok: false,
1306
+ result: {
1307
+ component,
1308
+ success: false,
1309
+ error: `Installing ${component} needs root. Connect this server as root, or as a user with passwordless sudo.`
1310
+ }
1311
+ };
1312
+ }
1313
+ async function installDocker(executor, onLog) {
1314
+ const prep = await prepareExecutor(executor, "docker");
1315
+ if (!prep.ok) return prep.result;
1316
+ executor = prep.executor;
1317
+ const profile = prep.profile;
1318
+ const plan = systemCatalog.installs.docker(profile);
1319
+ if (!plan.supported || !plan.installCommand || !plan.verifyCommand) {
1320
+ return { component: "docker", success: false, error: plan.unsupportedReason ?? "Docker install not supported" };
1321
+ }
1322
+ onLog(log2("Installing Docker Engine..."));
1323
+ try {
1324
+ const { code } = await executor.streamExec(plan.installCommand, onLog);
1325
+ if (code !== 0) return { component: "docker", success: false, error: "Docker install failed" };
1326
+ if (plan.startCommand) {
1327
+ onLog(log2("Starting Docker service..."));
1328
+ await executor.streamExec(plan.startCommand, onLog);
1329
+ }
1330
+ const version = await executor.exec(plan.verifyCommand);
1331
+ const parsed = systemCatalog.checks.docker.parseVersion(version);
1332
+ onLog(log2(`Docker ${parsed} installed`));
1333
+ return { component: "docker", success: true, version: parsed };
1334
+ } catch (err) {
1335
+ const msg = safeErrorMessage(err);
1336
+ onLog(log2(`Docker installation failed: ${msg}`, "error"));
1337
+ return { component: "docker", success: false, error: msg };
1338
+ }
1339
+ }
1340
+ async function installGit(executor, onLog, opts) {
1341
+ const prep = await prepareExecutor(executor, "git");
1342
+ if (!prep.ok) return prep.result;
1343
+ executor = prep.executor;
1344
+ const profile = prep.profile;
1345
+ const plan = systemCatalog.installs.git(profile);
1346
+ if (!plan.supported || !plan.installCommand || !plan.verifyCommand) {
1347
+ return { component: "git", success: false, error: plan.unsupportedReason ?? "Git install not supported" };
1348
+ }
1349
+ onLog(log2(opts?.label ? `Installing Git in ${opts.label}...` : "Installing Git..."));
1350
+ try {
1351
+ const { code } = await executor.streamExec(plan.installCommand, onLog);
1352
+ if (code !== 0) return { component: "git", success: false, error: "Git installation failed" };
1353
+ const version = await executor.exec(plan.verifyCommand);
1354
+ const parsed = systemCatalog.checks.git.parseVersion(version);
1355
+ onLog(log2(opts?.label ? `Git ${parsed} installed in ${opts.label}` : `Git ${parsed} installed`));
1356
+ return { component: "git", success: true, version: parsed };
1357
+ } catch (err) {
1358
+ const msg = safeErrorMessage(err);
1359
+ onLog(log2(`Git installation failed: ${msg}`, "error"));
1360
+ return { component: "git", success: false, error: msg };
1361
+ }
1362
+ }
1363
+ async function installRsync(executor, onLog) {
1364
+ const prep = await prepareExecutor(executor, "rsync");
1365
+ if (!prep.ok) return prep.result;
1366
+ executor = prep.executor;
1367
+ const profile = prep.profile;
1368
+ const plan = systemCatalog.installs.rsync(profile);
1369
+ if (!plan.supported || !plan.installCommand || !plan.verifyCommand) {
1370
+ return { component: "rsync", success: false, error: plan.unsupportedReason ?? "rsync install not supported" };
1371
+ }
1372
+ onLog(log2("Installing rsync..."));
1373
+ try {
1374
+ const { code } = await executor.streamExec(plan.installCommand, onLog);
1375
+ if (code !== 0) return { component: "rsync", success: false, error: "rsync installation failed" };
1376
+ const version = await executor.exec(plan.verifyCommand);
1377
+ const parsed = systemCatalog.checks.rsync.parseVersion(version);
1378
+ onLog(log2(`rsync ${parsed} installed`));
1379
+ return { component: "rsync", success: true, version: parsed };
1380
+ } catch (err) {
1381
+ const msg = safeErrorMessage(err);
1382
+ onLog(log2(`rsync installation failed: ${msg}`, "error"));
1383
+ return { component: "rsync", success: false, error: msg };
1277
1384
  }
1278
- return { proxy: "traefik", sites, warnings };
1279
1385
  }
1280
- async function scanTraefik(executor) {
1281
- const out = await tryExec4(
1282
- executor,
1283
- "docker ps -q 2>/dev/null | xargs -r docker inspect --format '{{.Name}} {{json .Config.Labels}} {{range .NetworkSettings.Networks}}{{.IPAddress}} {{end}}' 2>/dev/null"
1284
- );
1285
- if (!out) {
1286
- return { proxy: "traefik", sites: [], warnings: ["traefik: couldn't read container labels via docker inspect"] };
1386
+ async function installContainerEdge(executor, onLog, config) {
1387
+ const prep = await prepareExecutor(executor, "edge");
1388
+ if (!prep.ok) return prep.result;
1389
+ executor = prep.executor;
1390
+ if (!await dockerAvailable(executor)) {
1391
+ const error = "The edge is a container image and needs Docker on this server. Install the Docker component first, then install the edge.";
1392
+ onLog(log2(error, "error"));
1393
+ return { component: "edge", success: false, error };
1287
1394
  }
1288
- const containers = [];
1289
- for (const line of out.split("\n")) {
1290
- const [rawName, rawLabels, rawIps] = line.split(" ");
1291
- if (!rawName || !rawLabels) continue;
1292
- let labels;
1293
- try {
1294
- labels = JSON.parse(rawLabels) ?? {};
1295
- } catch {
1296
- continue;
1395
+ try {
1396
+ const result = await ensureContainerEdge(executor, {
1397
+ onLog,
1398
+ config,
1399
+ image: config?.edgeImage
1400
+ });
1401
+ if (result.converted) {
1402
+ onLog(log2("Edge moved from host OpenResty to the container edge", "warn"));
1297
1403
  }
1298
- if (!Object.keys(labels).some((k) => k.startsWith("traefik."))) continue;
1299
- const ip = (rawIps ?? "").trim().split(/\s+/)[0] || void 0;
1300
- containers.push({ name: rawName.replace(/^\//, ""), labels, ip });
1404
+ const version = await executor.exec(containerCommand(result.container, "openresty -v 2>&1")).catch(() => "");
1405
+ return {
1406
+ component: "edge",
1407
+ success: true,
1408
+ version: version ? systemCatalog.checks.openresty.parseVersion(version) : void 0
1409
+ };
1410
+ } catch (err) {
1411
+ if (err instanceof EdgeMigrateRequested) throw err;
1412
+ const msg = safeErrorMessage(err);
1413
+ onLog(log2(`Edge setup failed: ${msg}`, "error"));
1414
+ return { component: "edge", success: false, error: msg };
1301
1415
  }
1302
- return parseTraefikLabels(containers);
1303
1416
  }
1417
+ var COMPONENT_INSTALLERS = {
1418
+ docker: (exec, log5) => installDocker(exec, log5),
1419
+ // The edge is ONE component and it is a container image. `installOpenResty` /
1420
+ // `installCertbot` are NOT reachable from here — they remain only for the mail
1421
+ // server's own proxy (mail.service.ts) and for converting a legacy host edge.
1422
+ edge: (exec, log5, config) => installContainerEdge(exec, log5, config),
1423
+ git: (exec, log5) => installGit(exec, log5),
1424
+ rsync: (exec, log5) => installRsync(exec, log5)
1425
+ };
1304
1426
 
1305
- // ../../packages/adapters/src/system/proxy/import/index.ts
1306
- var INSTALLED_MARKERS = [
1307
- { proxy: "nginx", paths: ["/etc/nginx/nginx.conf"] },
1308
- { proxy: "caddy", paths: ["/etc/caddy/Caddyfile"] },
1309
- { proxy: "apache", paths: ["/etc/apache2/apache2.conf", "/etc/httpd/conf/httpd.conf"] }
1310
- ];
1311
- async function detectInstalledProxy(executor) {
1312
- const probe = INSTALLED_MARKERS.map(({ proxy, paths }) => {
1313
- const test = paths.map((p) => `[ -f '${p}' ]`).join(" || ");
1314
- return `{ ${test}; } && echo ${proxy}`;
1315
- }).join("; ");
1316
- let out = "";
1427
+ // ../../packages/adapters/src/system/available-version.ts
1428
+ var PACKAGE_NAMES = {
1429
+ git: "git",
1430
+ rsync: "rsync",
1431
+ // The edge is an IMAGE, not a repo package — its "newer version available" comes
1432
+ // from the registry (see the update scanner's imageDigest), never from apt.
1433
+ edge: null,
1434
+ docker: null
1435
+ };
1436
+ function normalizePkgVersion(raw) {
1437
+ let v = raw.trim();
1438
+ const colon = v.lastIndexOf(":");
1439
+ if (colon !== -1) v = v.slice(colon + 1);
1440
+ const dash = v.indexOf("-");
1441
+ if (dash !== -1) v = v.slice(0, dash);
1442
+ return v.trim();
1443
+ }
1444
+ async function tryExec3(executor, cmd) {
1317
1445
  try {
1318
- out = await executor.exec(`{ ${probe}; } 2>/dev/null; true`, { timeout: 1e4 });
1446
+ return await executor.exec(cmd);
1319
1447
  } catch {
1320
1448
  return null;
1321
1449
  }
1322
- const found = new Set(
1323
- out.split("\n").map((l) => l.trim()).filter(Boolean)
1324
- );
1325
- return INSTALLED_MARKERS.find((m) => found.has(m.proxy))?.proxy ?? null;
1326
1450
  }
1327
- async function scanImportableSites(executor, proxy) {
1328
- switch (proxy) {
1329
- case "nginx":
1330
- return scanNginx(executor);
1331
- case "caddy":
1332
- return scanCaddy(executor);
1333
- case "apache":
1334
- return scanApache(executor);
1335
- case "traefik":
1336
- return scanTraefik(executor);
1337
- default:
1338
- return {
1339
- proxy,
1340
- sites: [],
1341
- warnings: [`${proxy}: config import not supported \u2014 takeover only`]
1342
- };
1451
+ async function probeCandidate(executor, pm, pkg) {
1452
+ const q = `'${pkg.replace(/'/g, "'\\''")}'`;
1453
+ if (pm === "apt") {
1454
+ const out = await tryExec3(executor, `apt-cache policy ${q} 2>/dev/null`);
1455
+ return out?.match(/Candidate:\s*(\S+)/)?.[1]?.trim() ?? null;
1456
+ }
1457
+ if (pm === "dnf" || pm === "yum") {
1458
+ const out = await tryExec3(
1459
+ executor,
1460
+ `${pm} -q --cacheonly list available ${q} 2>/dev/null | tail -n1`
1461
+ );
1462
+ return out?.trim().split(/\s+/)[1]?.trim() ?? null;
1463
+ }
1464
+ if (pm === "apk") {
1465
+ const out = await tryExec3(executor, `apk policy ${q} 2>/dev/null`);
1466
+ const versions = [...out?.matchAll(/^\s+([\w.+~-]+):\s*$/gm) ?? []].map((m) => m[1]);
1467
+ return versions.length ? versions[versions.length - 1] : null;
1343
1468
  }
1469
+ return null;
1344
1470
  }
1345
- function canImportProxy(proxy) {
1346
- return proxy === "nginx" || proxy === "caddy" || proxy === "apache" || proxy === "traefik";
1471
+ async function enrichAvailableVersions(executor, profile, statuses) {
1472
+ const targets = statuses.filter(
1473
+ (s) => s.installed && s.version && PACKAGE_NAMES[s.name]
1474
+ );
1475
+ await Promise.all(
1476
+ targets.map(async (s) => {
1477
+ try {
1478
+ const pkg = PACKAGE_NAMES[s.name];
1479
+ const candidate = await probeCandidate(executor, profile.packageManager, pkg);
1480
+ if (!candidate) return;
1481
+ const avail = normalizePkgVersion(candidate);
1482
+ const installed = normalizePkgVersion(s.version);
1483
+ if (avail && compareSemver(avail, installed) > 0) {
1484
+ s.availableVersion = avail;
1485
+ s.updateAvailable = true;
1486
+ }
1487
+ } catch {
1488
+ }
1489
+ })
1490
+ );
1347
1491
  }
1348
1492
 
1349
- // ../../packages/adapters/src/system/proxy/takeover-journal.ts
1350
- var JOURNAL_DIR = "/var/lib/openship";
1351
- var JOURNAL_PATH = `${JOURNAL_DIR}/edge-takeover.json`;
1352
- function log(message, level = "info") {
1353
- return { timestamp: (/* @__PURE__ */ new Date()).toISOString(), message, level };
1354
- }
1355
- async function tryExec5(executor, cmd) {
1356
- try {
1357
- return await executor.exec(cmd);
1358
- } catch {
1359
- return null;
1493
+ // ../../packages/adapters/src/system/components.ts
1494
+ var SYSTEM_COMPONENTS = [
1495
+ {
1496
+ name: "docker",
1497
+ label: "Docker",
1498
+ description: "Container runtime for deployments",
1499
+ installable: true,
1500
+ category: "core"
1501
+ },
1502
+ {
1503
+ name: "git",
1504
+ label: "Git",
1505
+ description: "Version control for source code",
1506
+ installable: true,
1507
+ category: "core"
1508
+ },
1509
+ {
1510
+ // ONE component for the edge, because it is ONE artifact: the openship-edge
1511
+ // image. OpenResty, its Lua, and certbot all ship inside it — they were three
1512
+ // rows in this list for a thing that is installed, checked, and removed as a
1513
+ // unit, and neither "openresty" nor "certbot" was independently installable
1514
+ // on a converted box.
1515
+ name: "edge",
1516
+ label: "Edge",
1517
+ description: "Routing + TLS \u2014 the openship-edge container (OpenResty, Lua, certbot)",
1518
+ installable: true,
1519
+ category: "infrastructure"
1520
+ },
1521
+ {
1522
+ name: "rsync",
1523
+ label: "rsync",
1524
+ description: "Fast directory sync for remote local-build transfers",
1525
+ installable: true,
1526
+ category: "infrastructure"
1360
1527
  }
1528
+ ];
1529
+ var SYSTEM_COMPONENTS_BY_NAME = new Map(
1530
+ SYSTEM_COMPONENTS.map((component) => [component.name, component])
1531
+ );
1532
+ function getSystemComponentDefinition(name) {
1533
+ return SYSTEM_COMPONENTS_BY_NAME.get(name) ?? {
1534
+ name,
1535
+ label: name,
1536
+ description: `${name} component`,
1537
+ installable: false,
1538
+ category: "infrastructure"
1539
+ };
1361
1540
  }
1362
- async function buildJournal(executor, status) {
1363
- const units = /* @__PURE__ */ new Map();
1364
- const containers = /* @__PURE__ */ new Map();
1365
- const processes = /* @__PURE__ */ new Map();
1366
- for (const o of status.occupants) {
1367
- if (o.containerName && !containers.has(o.containerName)) {
1368
- const r = await tryExec5(
1369
- executor,
1370
- `docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' ${sq2(o.containerName)} 2>/dev/null`
1541
+
1542
+ // ../../packages/adapters/src/system/checks.ts
1543
+ async function tryExec4(executor, command) {
1544
+ const startedAt = Date.now();
1545
+ systemDebug("checks", `exec:start ${command}`);
1546
+ try {
1547
+ const result = await executor.exec(command, { timeout: 1e4 });
1548
+ systemDebug(
1549
+ "checks",
1550
+ `exec:ok ${command} (${formatDuration(startedAt)})`
1551
+ );
1552
+ return result;
1553
+ } catch (err) {
1554
+ if (isRemoteConnectionError(err)) {
1555
+ systemDebug(
1556
+ "checks",
1557
+ `exec:abort ${command} (${formatDuration(startedAt)}) ${safeErrorMessage(err)}`
1371
1558
  );
1372
- containers.set(o.containerName, { name: o.containerName, restart: r?.trim() || "no" });
1373
- } else if (o.systemdUnit && !units.has(o.systemdUnit)) {
1374
- const en = await tryExec5(executor, `systemctl is-enabled ${sq2(o.systemdUnit)} 2>/dev/null`);
1375
- units.set(o.systemdUnit, { unit: o.systemdUnit, wasEnabled: en?.trim() === "enabled" });
1376
- } else if (!o.containerName && !o.systemdUnit && o.pid && !processes.has(o.pid)) {
1377
- processes.set(o.pid, { pid: o.pid, command: o.rawCommand });
1559
+ throw err;
1378
1560
  }
1561
+ const msg = safeErrorMessage(err);
1562
+ systemDebug(
1563
+ "checks",
1564
+ `exec:fail ${command} (${formatDuration(startedAt)}) ${msg}`
1565
+ );
1566
+ return null;
1379
1567
  }
1568
+ }
1569
+ function healthy(name, version, running) {
1570
+ const component = getSystemComponentDefinition(name);
1380
1571
  return {
1381
- startedAt: (/* @__PURE__ */ new Date()).toISOString(),
1382
- units: [...units.values()],
1383
- containers: [...containers.values()],
1384
- processes: [...processes.values()]
1572
+ name,
1573
+ label: component.label,
1574
+ description: component.description,
1575
+ installable: component.installable,
1576
+ installed: true,
1577
+ version,
1578
+ running,
1579
+ healthy: running !== void 0 ? running : true,
1580
+ message: running ? `${name} ${version} - running` : `${name} ${version} - installed`
1385
1581
  };
1386
1582
  }
1387
- async function writeJournal(executor, journal) {
1388
- try {
1389
- await executor.mkdir(JOURNAL_DIR);
1390
- await executor.writeFile(JOURNAL_PATH, JSON.stringify(journal, null, 2));
1391
- } catch {
1583
+ function unhealthy(name, message, opts) {
1584
+ const component = getSystemComponentDefinition(name);
1585
+ return {
1586
+ name,
1587
+ label: component.label,
1588
+ description: component.description,
1589
+ installable: component.installable,
1590
+ installed: !!opts?.version,
1591
+ version: opts?.version,
1592
+ running: opts?.running,
1593
+ healthy: false,
1594
+ message
1595
+ };
1596
+ }
1597
+ async function checkDocker(executor) {
1598
+ const startedAt = Date.now();
1599
+ const recipe = systemCatalog.checks.docker;
1600
+ const version = await tryExec4(executor, recipe.versionCommand);
1601
+ if (!version) {
1602
+ systemDebug("checks", `docker:missing (${formatDuration(startedAt)})`);
1603
+ return unhealthy("docker", recipe.missingMessage);
1604
+ }
1605
+ const parsed = recipe.parseVersion(version);
1606
+ const info = await tryExec4(executor, recipe.daemonCommand);
1607
+ if (!info) {
1608
+ systemDebug("checks", `docker:not-running (${formatDuration(startedAt)})`);
1609
+ return unhealthy("docker", recipe.notRunningMessage, {
1610
+ version: parsed,
1611
+ running: false
1612
+ });
1613
+ }
1614
+ systemDebug("checks", `docker:healthy (${formatDuration(startedAt)})`);
1615
+ return healthy("docker", parsed, true);
1616
+ }
1617
+ async function checkGit(executor) {
1618
+ const startedAt = Date.now();
1619
+ const recipe = systemCatalog.checks.git;
1620
+ const version = await tryExec4(executor, recipe.versionCommand);
1621
+ if (!version) {
1622
+ systemDebug("checks", `git:missing (${formatDuration(startedAt)})`);
1623
+ return unhealthy("git", recipe.missingMessage);
1392
1624
  }
1625
+ const parsed = recipe.parseVersion(version);
1626
+ systemDebug("checks", `git:healthy (${formatDuration(startedAt)})`);
1627
+ return healthy("git", parsed);
1393
1628
  }
1394
- async function clearJournal(executor) {
1395
- await tryExec5(executor, `rm -f ${JOURNAL_PATH}`);
1396
- }
1397
- async function readJournal(executor) {
1398
- const raw = await tryExec5(executor, `cat ${JOURNAL_PATH} 2>/dev/null`);
1399
- if (!raw?.trim()) return null;
1400
- try {
1401
- return JSON.parse(raw);
1402
- } catch {
1403
- await clearJournal(executor);
1404
- return null;
1629
+ async function checkRsync(executor) {
1630
+ const startedAt = Date.now();
1631
+ const recipe = systemCatalog.checks.rsync;
1632
+ const version = await tryExec4(executor, recipe.versionCommand);
1633
+ if (!version) {
1634
+ systemDebug("checks", `rsync:missing (${formatDuration(startedAt)})`);
1635
+ return unhealthy("rsync", recipe.missingMessage);
1405
1636
  }
1637
+ const parsed = recipe.parseVersion(version);
1638
+ systemDebug("checks", `rsync:healthy (${formatDuration(startedAt)})`);
1639
+ return healthy("rsync", parsed);
1406
1640
  }
1407
- async function rollback(executor, journal, onLog) {
1408
- onLog(log("Rolling back \u2014 restoring the previous proxy...", "warn"));
1409
- await tryExec5(
1410
- executor,
1411
- "systemctl disable --now openresty 2>/dev/null || systemctl stop openresty 2>/dev/null || true; systemctl reset-failed openresty 2>/dev/null || true"
1412
- );
1413
- for (const u of journal.units) {
1414
- await tryExec5(
1415
- executor,
1416
- u.wasEnabled ? `systemctl enable --now ${sq2(u.unit)} 2>/dev/null || true` : `systemctl start ${sq2(u.unit)} 2>/dev/null || true`
1641
+ async function checkEdge(executor) {
1642
+ const startedAt = Date.now();
1643
+ const container = await resolveOurEdgeContainer(executor);
1644
+ if (!container) {
1645
+ systemDebug("checks", `edge:missing (${formatDuration(startedAt)})`);
1646
+ return unhealthy(
1647
+ "edge",
1648
+ "No edge on this server. The edge is the openship-edge container \u2014 install it (requires Docker)."
1417
1649
  );
1418
1650
  }
1419
- for (const c of journal.containers) {
1420
- await tryExec5(executor, `docker update --restart=${sq2(c.restart)} ${sq2(c.name)} 2>/dev/null || true`);
1421
- await tryExec5(executor, `docker start ${sq2(c.name)} 2>/dev/null || true`);
1422
- }
1423
- for (const p of journal.processes ?? []) {
1424
- if (p.command) {
1425
- await tryExec5(
1426
- executor,
1427
- `setsid -f sh -c ${sq2(p.command)} 2>/dev/null || (nohup sh -c ${sq2(p.command)} >/dev/null 2>&1 &) || true`
1428
- );
1429
- } else {
1430
- onLog(log(`Could not restore process ${p.pid} \u2014 no command captured.`, "warn"));
1431
- }
1651
+ const version = await tryExec4(executor, containerCommand(container, "openresty -v 2>&1"));
1652
+ if (!version) {
1653
+ systemDebug("checks", `edge:container-unresponsive (${formatDuration(startedAt)})`);
1654
+ return unhealthy(
1655
+ "edge",
1656
+ `The edge container ${container} is running but not responding \u2014 check \`docker logs ${container}\``,
1657
+ { running: false }
1658
+ );
1432
1659
  }
1660
+ systemDebug("checks", `edge:healthy (${formatDuration(startedAt)})`);
1661
+ return healthy("edge", systemCatalog.checks.openresty.parseVersion(version), true);
1433
1662
  }
1434
- async function beginEdgeTakeover(executor, status, onLog) {
1435
- await writeJournal(executor, await buildJournal(executor, status));
1436
- await freeEdgeTargets(executor, stopTargetsForStatus(status), (m, l) => onLog(log(m, l)));
1663
+ var COMPONENT_CHECKS = {
1664
+ docker: checkDocker,
1665
+ edge: checkEdge,
1666
+ git: checkGit,
1667
+ rsync: checkRsync
1668
+ };
1669
+ async function mapWithConcurrency(items, limit, fn) {
1670
+ const results = new Array(items.length);
1671
+ let cursor = 0;
1672
+ const worker = async () => {
1673
+ while (cursor < items.length) {
1674
+ const index = cursor++;
1675
+ results[index] = await fn(items[index]);
1676
+ }
1677
+ };
1678
+ await Promise.all(
1679
+ Array.from({ length: Math.min(limit, items.length) }, worker)
1680
+ );
1681
+ return results;
1437
1682
  }
1438
- async function rollbackEdgeTakeover(executor, onLog) {
1439
- const journal = await readJournal(executor);
1440
- if (!journal || journal.completed) return false;
1441
- await rollback(executor, journal, onLog);
1442
- await clearJournal(executor);
1443
- return true;
1683
+ var CHECK_CONCURRENCY = 4;
1684
+ async function checkAll(executor) {
1685
+ const startedAt = Date.now();
1686
+ const entries = SYSTEM_COMPONENTS.map((component) => [component.name, COMPONENT_CHECKS[component.name]]).filter((entry) => Boolean(entry[1]));
1687
+ systemDebug(
1688
+ "checks",
1689
+ `checkAll:start [${entries.map(([name]) => name).join(", ")}]`
1690
+ );
1691
+ const results = await mapWithConcurrency(
1692
+ entries,
1693
+ CHECK_CONCURRENCY,
1694
+ ([, fn]) => fn(executor)
1695
+ );
1696
+ await enrichAvailable(executor, results);
1697
+ systemDebug("checks", `checkAll:done (${formatDuration(startedAt)})`);
1698
+ return results;
1444
1699
  }
1445
- async function completeEdgeTakeover(executor) {
1446
- const journal = await readJournal(executor);
1447
- if (journal) {
1448
- journal.completed = true;
1449
- await writeJournal(executor, journal);
1700
+ async function enrichAvailable(executor, results) {
1701
+ try {
1702
+ const profile = await resolveEnvironment(executor);
1703
+ await enrichAvailableVersions(executor, profile, results);
1704
+ } catch {
1450
1705
  }
1451
- await clearJournal(executor);
1452
1706
  }
1453
- async function recoverInterruptedTakeover(executor, onLog, isEdgeHealthy) {
1454
- const journal = await readJournal(executor);
1455
- if (!journal) return;
1456
- if (journal.completed) {
1457
- await clearJournal(executor);
1458
- return;
1459
- }
1460
- if (isEdgeHealthy && await isEdgeHealthy()) {
1461
- onLog(log("Found an unfinished edge takeover, but our edge is serving \u2014 keeping it.", "warn"));
1462
- await clearJournal(executor);
1463
- return;
1464
- }
1465
- onLog(log("Found an interrupted edge takeover \u2014 restoring the previous proxy.", "warn"));
1466
- await rollback(executor, journal, onLog);
1467
- await clearJournal(executor);
1707
+ async function checkComponents(executor, names) {
1708
+ const startedAt = Date.now();
1709
+ const fns = names.map((name) => COMPONENT_CHECKS[name]).filter((fn) => Boolean(fn));
1710
+ systemDebug("checks", `checkComponents:start [${names.join(", ")}]`);
1711
+ const results = await mapWithConcurrency(
1712
+ fns,
1713
+ CHECK_CONCURRENCY,
1714
+ (fn) => fn(executor)
1715
+ );
1716
+ await enrichAvailable(executor, results);
1717
+ systemDebug(
1718
+ "checks",
1719
+ `checkComponents:done [${names.join(", ")}] (${formatDuration(startedAt)})`
1720
+ );
1721
+ return results;
1468
1722
  }
1469
1723
 
1470
1724
  // ../../packages/adapters/src/system/proxy/index.ts
@@ -1494,20 +1748,20 @@ function describeEdgeOwner(occupants) {
1494
1748
  }
1495
1749
 
1496
1750
  // ../../packages/adapters/src/system/proxy/consent.ts
1497
- function log2(message, level = "info") {
1751
+ function log3(message, level = "info") {
1498
1752
  return { timestamp: (/* @__PURE__ */ new Date()).toISOString(), message, level };
1499
1753
  }
1500
1754
  async function ensureEdgeClear(executor, config, onLog) {
1501
1755
  const status = await probeEdge(executor);
1502
1756
  if (status.canProceedClean) return { tookOver: false };
1503
1757
  const takeover = async () => {
1504
- onLog(log2(
1758
+ onLog(log3(
1505
1759
  `Taking over ports from ${status.occupants.map((o) => o.command ?? o.port).join(", ")}...`,
1506
1760
  "warn"
1507
1761
  ));
1508
1762
  const configured = config?.edgePolicy?.stopTargets ?? [];
1509
1763
  const targets = configured.length ? configured : stopTargetsForStatus(status);
1510
- await freeEdgeTargets(executor, targets, (m, l) => onLog(log2(m, l)));
1764
+ await freeEdgeTargets(executor, targets, (m, l) => onLog(log3(m, l)));
1511
1765
  };
1512
1766
  if (config?.edgePolicy?.mode === "takeover") {
1513
1767
  await takeover();
@@ -1547,223 +1801,213 @@ async function ensureEdgeClear(executor, config, onLog) {
1547
1801
  throw new EdgeConflictError(status);
1548
1802
  }
1549
1803
 
1550
- // ../../packages/adapters/src/system/installer.ts
1551
- function log3(message, level = "info") {
1804
+ // ../../packages/adapters/src/system/proxy/ensure-container-edge.ts
1805
+ var EDGE_CONTAINER_NAME = "openship-edge";
1806
+ function log4(message, level = "info") {
1552
1807
  return { timestamp: (/* @__PURE__ */ new Date()).toISOString(), message, level };
1553
1808
  }
1554
- function describeEnvironment(profile) {
1555
- return `Detected environment: os=${profile.os}, arch=${profile.arch}, distro=${profile.distro ?? "n/a"}, packageManager=${profile.packageManager}, serviceManager=${profile.serviceManager}`;
1556
- }
1557
- async function execSafe(executor, cmd) {
1558
- try {
1559
- await executor.exec(cmd);
1560
- } catch {
1561
- }
1809
+ var injectedDefaultImage;
1810
+ function setDefaultEdgeImage(image) {
1811
+ injectedDefaultImage = image?.trim() || void 0;
1812
+ }
1813
+ function resolveEdgeImage(explicit) {
1814
+ return buildEdgeImageRef({ explicit, injectedDefault: injectedDefaultImage });
1815
+ }
1816
+ async function dockerAvailable(executor) {
1817
+ return executor.exec("docker version --format '{{.Server.Version}}' 2>/dev/null").then((v) => Boolean(v.trim())).catch(() => false);
1818
+ }
1819
+ async function runningImage(executor, container) {
1820
+ const out = await executor.exec(`docker inspect -f '{{.Config.Image}}' ${sq(container)} 2>/dev/null`).catch(() => "");
1821
+ return out.trim() || null;
1822
+ }
1823
+ async function containerEdgeProvider(executor, container, opts) {
1824
+ const { NginxProvider: Provider } = await import("./nginx-2GTRUIDE.js");
1825
+ return new Provider({
1826
+ ...opts,
1827
+ // Vhosts go to the BIND-MOUNTED HOST dir (so every host-side reader — the
1828
+ // migrate proxy scan, cert reuse, the mail cert symlinks — keeps working),
1829
+ // while reload/certbot run inside the container.
1830
+ paths: EDGE_HOST_PATHS,
1831
+ executor: edgeContainerExecutor(executor, container),
1832
+ // MUST be pinned: re-detection answers from inside the container and would
1833
+ // repoint sitesDir at a host dir the edge never reads.
1834
+ pinPaths: true
1835
+ });
1562
1836
  }
1563
- async function ensureAptReady(executor, onLog) {
1564
- const broken = await executor.exec("dpkg --audit 2>&1 | head -1").catch(() => "");
1565
- if (!broken) return;
1566
- onLog(log3("Fixing interrupted package state..."));
1567
- await execSafe(executor, "fuser -k /var/lib/dpkg/lock-frontend /var/lib/dpkg/lock /var/cache/apt/archives/lock 2>/dev/null || true");
1568
- await execSafe(executor, "rm -f /var/lib/dpkg/lock-frontend /var/lib/dpkg/lock /var/cache/apt/archives/lock 2>/dev/null || true");
1569
- await executor.streamExec("dpkg --configure -a 2>&1", onLog);
1837
+ async function localContainerEdgeProvider(container, opts) {
1838
+ const { DockerEdgeExecutor } = await import("./docker-edge-executor-VHW3JPJS.js");
1839
+ const { NginxProvider: Provider } = await import("./nginx-2GTRUIDE.js");
1840
+ return new Provider({
1841
+ ...opts,
1842
+ paths: OPENRESTY_DEFAULT_PATHS,
1843
+ executor: new DockerEdgeExecutor({ containerName: container }),
1844
+ // nginx.conf + the Lua are BAKED into the image — nothing to detect, install
1845
+ // or patch, and detection would answer from inside the container anyway.
1846
+ pinPaths: true
1847
+ });
1570
1848
  }
1571
- async function prepareExecutor(executor, component) {
1572
- const profile = await resolveEnvironment(executor);
1573
- if (profile.isRoot) return { ok: true, executor, profile };
1574
- if (profile.canSudo) return { ok: true, executor: elevatedExecutor(executor), profile };
1575
- return {
1576
- ok: false,
1577
- result: {
1578
- component,
1579
- success: false,
1580
- error: `Installing ${component} needs root. Connect this server as root, or as a user with passwordless sudo.`
1581
- }
1849
+ function buildEdgeRunCommand(container, image) {
1850
+ const mounts = EDGE_CONTAINER_MOUNTS.map(
1851
+ // `:z` relabels for SELinux-enforcing hosts; a no-op elsewhere.
1852
+ (m) => `-v ${sq(`${m.host}:${m.container}:z`)}`
1853
+ ).join(" ");
1854
+ return [
1855
+ "docker run -d",
1856
+ `--name ${sq(container)}`,
1857
+ "--network host",
1858
+ "--restart unless-stopped",
1859
+ mounts,
1860
+ sq(image)
1861
+ ].join(" ");
1862
+ }
1863
+ async function swapEdgeImage(executor, container, from, to, opts) {
1864
+ const { onLog } = opts;
1865
+ onLog(log4(`Updating the edge: ${from} \u2192 ${to}`));
1866
+ const pull = await executor.streamExec(`docker pull ${sq(to)}`, onLog);
1867
+ if (pull.code !== 0) {
1868
+ onLog(log4(`Could not pull ${to} \u2014 the edge stays on ${from}.`, "warn"));
1869
+ return { swapped: false, edgeDown: false };
1870
+ }
1871
+ const start = async (image) => {
1872
+ await executor.exec(`docker rm -f ${sq(container)} 2>/dev/null || true`).catch(() => {
1873
+ });
1874
+ const run = await executor.streamExec(
1875
+ buildEdgeRunCommand(container, image),
1876
+ onLog
1877
+ );
1878
+ invalidateEdgeContainer(executor);
1879
+ if (run.code !== 0) return false;
1880
+ const listening = await waitForPortListening(executor, 80, {
1881
+ timeoutMs: opts.verifyTimeoutMs ?? 3e4
1882
+ });
1883
+ return !(listening.checked && !listening.listening);
1582
1884
  };
1583
- }
1584
- async function installDocker(executor, onLog) {
1585
- const prep = await prepareExecutor(executor, "docker");
1586
- if (!prep.ok) return prep.result;
1587
- executor = prep.executor;
1588
- const profile = prep.profile;
1589
- const plan = systemCatalog.installs.docker(profile);
1590
- if (!plan.supported || !plan.installCommand || !plan.verifyCommand) {
1591
- return { component: "docker", success: false, error: plan.unsupportedReason ?? "Docker install not supported" };
1592
- }
1593
- onLog(log3("Installing Docker Engine..."));
1594
- try {
1595
- const { code } = await executor.streamExec(plan.installCommand, onLog);
1596
- if (code !== 0) return { component: "docker", success: false, error: "Docker install failed" };
1597
- if (plan.startCommand) {
1598
- onLog(log3("Starting Docker service..."));
1599
- await executor.streamExec(plan.startCommand, onLog);
1885
+ if (await start(to)) {
1886
+ onLog(log4(`Edge updated to ${to}`));
1887
+ return { swapped: true, edgeDown: false };
1888
+ }
1889
+ onLog(log4(`Edge ${to} failed to come up \u2014 rolling back to ${from}...`, "error"));
1890
+ if (await start(from)) {
1891
+ return { swapped: false, edgeDown: false };
1892
+ }
1893
+ onLog(log4(`Rollback to ${from} ALSO failed \u2014 the edge is down on this server.`, "error"));
1894
+ return { swapped: false, edgeDown: true };
1895
+ }
1896
+ async function ensureContainerEdge(executor, opts) {
1897
+ const { onLog } = opts;
1898
+ const container = opts.container?.trim() || EDGE_CONTAINER_NAME;
1899
+ const image = resolveEdgeImage(opts.image);
1900
+ const existing = await resolveOurEdgeContainer(executor, { fresh: true });
1901
+ if (existing) {
1902
+ const current = await runningImage(executor, existing);
1903
+ if (current && current !== image) {
1904
+ const swap = await swapEdgeImage(executor, existing, current, image, opts);
1905
+ return {
1906
+ container: existing,
1907
+ image,
1908
+ converted: false,
1909
+ updated: swap.swapped,
1910
+ edgeDown: swap.edgeDown
1911
+ };
1600
1912
  }
1601
- const version = await executor.exec(plan.verifyCommand);
1602
- const parsed = systemCatalog.checks.docker.parseVersion(version);
1603
- onLog(log3(`Docker ${parsed} installed`));
1604
- return { component: "docker", success: true, version: parsed };
1605
- } catch (err) {
1606
- const msg = safeErrorMessage(err);
1607
- onLog(log3(`Docker installation failed: ${msg}`, "error"));
1608
- return { component: "docker", success: false, error: msg };
1609
- }
1610
- }
1611
- async function installGit(executor, onLog, opts) {
1612
- const prep = await prepareExecutor(executor, "git");
1613
- if (!prep.ok) return prep.result;
1614
- executor = prep.executor;
1615
- const profile = prep.profile;
1616
- const plan = systemCatalog.installs.git(profile);
1617
- if (!plan.supported || !plan.installCommand || !plan.verifyCommand) {
1618
- return { component: "git", success: false, error: plan.unsupportedReason ?? "Git install not supported" };
1619
- }
1620
- onLog(log3(opts?.label ? `Installing Git in ${opts.label}...` : "Installing Git..."));
1621
- try {
1622
- const { code } = await executor.streamExec(plan.installCommand, onLog);
1623
- if (code !== 0) return { component: "git", success: false, error: "Git installation failed" };
1624
- const version = await executor.exec(plan.verifyCommand);
1625
- const parsed = systemCatalog.checks.git.parseVersion(version);
1626
- onLog(log3(opts?.label ? `Git ${parsed} installed in ${opts.label}` : `Git ${parsed} installed`));
1627
- return { component: "git", success: true, version: parsed };
1628
- } catch (err) {
1629
- const msg = safeErrorMessage(err);
1630
- onLog(log3(`Git installation failed: ${msg}`, "error"));
1631
- return { component: "git", success: false, error: msg };
1632
- }
1633
- }
1634
- async function installRsync(executor, onLog) {
1635
- const prep = await prepareExecutor(executor, "rsync");
1636
- if (!prep.ok) return prep.result;
1637
- executor = prep.executor;
1638
- const profile = prep.profile;
1639
- const plan = systemCatalog.installs.rsync(profile);
1640
- if (!plan.supported || !plan.installCommand || !plan.verifyCommand) {
1641
- return { component: "rsync", success: false, error: plan.unsupportedReason ?? "rsync install not supported" };
1913
+ return { container: existing, image, converted: false };
1642
1914
  }
1643
- onLog(log3("Installing rsync..."));
1644
- try {
1645
- const { code } = await executor.streamExec(plan.installCommand, onLog);
1646
- if (code !== 0) return { component: "rsync", success: false, error: "rsync installation failed" };
1647
- const version = await executor.exec(plan.verifyCommand);
1648
- const parsed = systemCatalog.checks.rsync.parseVersion(version);
1649
- onLog(log3(`rsync ${parsed} installed`));
1650
- return { component: "rsync", success: true, version: parsed };
1651
- } catch (err) {
1652
- const msg = safeErrorMessage(err);
1653
- onLog(log3(`rsync installation failed: ${msg}`, "error"));
1654
- return { component: "rsync", success: false, error: msg };
1915
+ await ensureEdgeClear(executor, opts.config, onLog);
1916
+ if (!await dockerAvailable(executor)) {
1917
+ throw new Error(
1918
+ "Docker isn't available on this server, and the edge now runs as a container. Install the Docker component first, then set up the edge."
1919
+ );
1655
1920
  }
1656
- }
1657
- async function installCertbot(executor, onLog) {
1658
- const prep = await prepareExecutor(executor, "certbot");
1659
- if (!prep.ok) return prep.result;
1660
- executor = prep.executor;
1661
- const profile = prep.profile;
1662
- const plan = systemCatalog.installs.certbot(profile);
1663
- if (!plan.supported || !plan.installCommand || !plan.verifyCommand) {
1664
- return { component: "certbot", success: false, error: plan.unsupportedReason ?? "Certbot install not supported" };
1921
+ const bareWasOurs = await ourLuaOnHost(executor);
1922
+ onLog(log4(`Pulling edge image ${image}...`));
1923
+ const pull = await executor.streamExec(`docker pull ${sq(image)}`, onLog);
1924
+ if (pull.code !== 0) {
1925
+ throw new Error(
1926
+ `Could not pull the edge image ${image} \u2014 the box is unchanged and still serving. Check the server's network access to the registry, then retry.`
1927
+ );
1665
1928
  }
1666
- onLog(log3("Installing certbot..."));
1667
- try {
1668
- const { code } = await executor.streamExec(plan.installCommand, onLog);
1669
- if (code !== 0) return { component: "certbot", success: false, error: "Certbot installation failed" };
1670
- const version = await executor.exec(plan.verifyCommand);
1671
- const parsed = systemCatalog.checks.certbot.parseVersion(version);
1672
- onLog(log3(`Certbot ${parsed} installed`));
1673
- return { component: "certbot", success: true, version: parsed };
1674
- } catch (err) {
1675
- const msg = safeErrorMessage(err);
1676
- onLog(log3(`Certbot installation failed: ${msg}`, "error"));
1677
- return { component: "certbot", success: false, error: msg };
1929
+ for (const mount of EDGE_CONTAINER_MOUNTS) {
1930
+ await executor.exec(`mkdir -p ${sq(mount.host)}`).catch(() => {
1931
+ });
1678
1932
  }
1679
- }
1680
- async function installOpenResty(executor, onLog, config) {
1681
- const prep = await prepareExecutor(executor, "openresty");
1682
- if (!prep.ok) return prep.result;
1683
- executor = prep.executor;
1684
- const profile = prep.profile;
1685
- onLog(log3(describeEnvironment(profile)));
1686
- const plan = systemCatalog.installs.openresty(profile);
1687
- if (!plan.supported || !plan.installCommand || !plan.verifyCommand) {
1688
- return { component: "openresty", success: false, error: plan.unsupportedReason ?? "OpenResty install not supported" };
1933
+ const sitesTarget = EDGE_HOST_PATHS.sitesDir;
1934
+ if (bareWasOurs) {
1935
+ const barePaths = await detectOpenRestyPaths(executor).catch(() => OPENRESTY_DEFAULT_PATHS);
1936
+ if (barePaths.sitesDir !== sitesTarget && await executor.exists(barePaths.sitesDir)) {
1937
+ onLog(log4(`Carrying vhosts from ${barePaths.sitesDir}...`));
1938
+ await executor.exec(`cp -a ${sq(`${barePaths.sitesDir}/.`)} ${sq(`${sitesTarget}/`)} 2>/dev/null || true`).catch(() => {
1939
+ });
1940
+ }
1689
1941
  }
1690
- onLog(log3("Installing OpenResty..."));
1942
+ const restoreBare = async () => {
1943
+ if (!bareWasOurs) return;
1944
+ onLog(log4("Restoring the host OpenResty edge...", "warn"));
1945
+ await executor.exec("systemctl enable --now openresty 2>/dev/null || true").catch(() => {
1946
+ });
1947
+ };
1691
1948
  try {
1692
- const { tookOver } = await ensureEdgeClear(executor, config, onLog);
1693
- const hasIt = await executor.exec("command -v openresty >/dev/null 2>&1 && echo y || echo n").then((r) => r.trim() === "y");
1694
- if (hasIt && await ourLuaOnHost(executor)) {
1695
- onLog(log3("Stopping existing OpenResty..."));
1696
- await execSafe(executor, "systemctl stop openresty 2>/dev/null || true");
1697
- await execSafe(executor, "pkill -f '[o]penresty' 2>/dev/null || true");
1698
- }
1699
- if (profile.packageManager === "apt") {
1700
- await ensureAptReady(executor, onLog);
1701
- }
1702
- const { code } = await executor.streamExec(plan.installCommand, onLog);
1703
- if (code !== 0) return { component: "openresty", success: false, error: "OpenResty installation failed" };
1704
- await execSafe(executor, "systemctl stop openresty 2>/dev/null || true");
1705
- await execSafe(executor, "pkill -f '[o]penresty' 2>/dev/null || true");
1706
- await execSafe(executor, "systemctl reset-failed openresty 2>/dev/null || true");
1707
- const paths = await detectOpenRestyPaths(executor);
1708
- await ensureOpenRestyConfig(executor, paths);
1709
- onLog(log3("Validating config..."));
1710
- const { code: testCode } = await executor.streamExec(`${paths.bin} -t 2>&1`, onLog);
1711
- if (testCode !== 0) {
1712
- return { component: "openresty", success: false, error: "OpenResty config invalid - see logs above" };
1949
+ if (bareWasOurs) {
1950
+ onLog(log4("Stopping the host OpenResty edge..."));
1951
+ await executor.exec("systemctl disable --now openresty 2>/dev/null || true").catch(() => {
1952
+ });
1953
+ await executor.exec("systemctl reset-failed openresty 2>/dev/null || true").catch(() => {
1954
+ });
1713
1955
  }
1714
- if (plan.startCommand) {
1715
- onLog(log3("Starting OpenResty..."));
1716
- let start = await executor.streamExec(plan.startCommand, onLog);
1717
- if (start.code !== 0 && tookOver) {
1718
- onLog(log3("Start failed - reclaiming authorized ports...", "warn"));
1719
- const targets = config?.edgePolicy?.stopTargets?.length ? config.edgePolicy.stopTargets : stopTargetsForStatus(await probeEdge(executor));
1720
- await freeEdgeTargets(executor, targets, (m, l) => onLog(log3(m, l)));
1721
- await execSafe(executor, "systemctl reset-failed openresty 2>/dev/null || true");
1722
- start = await executor.streamExec(plan.startCommand, onLog);
1723
- }
1724
- if (start.code !== 0) {
1725
- const journal = await executor.exec(
1726
- "journalctl -xeu openresty.service --no-pager -n 30 2>/dev/null || echo '(unavailable)'"
1727
- ).catch(() => "(could not read journal)");
1728
- onLog(log3(`Service journal:
1729
- ${journal}`, "error"));
1730
- return { component: "openresty", success: false, error: "OpenResty installed but failed to start" };
1731
- }
1956
+ await executor.exec(`docker rm -f ${sq(container)} 2>/dev/null || true`).catch(() => {
1957
+ });
1958
+ onLog(log4("Starting the edge container..."));
1959
+ const run = await executor.streamExec(
1960
+ buildEdgeRunCommand(container, image),
1961
+ onLog
1962
+ );
1963
+ invalidateEdgeContainer(executor);
1964
+ if (run.code !== 0) throw new Error("the edge container failed to start");
1965
+ await executor.exec(containerCommand(container, "openresty -t"));
1966
+ const listening = await waitForPortListening(executor, 80, {
1967
+ timeoutMs: opts.verifyTimeoutMs ?? 3e4
1968
+ });
1969
+ if (listening.checked && !listening.listening) {
1970
+ throw new Error("the edge container started but nothing is listening on :80");
1732
1971
  }
1733
- await executor.exec(buildReloadCommand(paths));
1734
- onLog(log3("Deploying analytics scripts..."));
1735
- await deployLuaScripts(executor, paths);
1736
- const version = await executor.exec(plan.verifyCommand);
1737
- const parsed = systemCatalog.checks.openresty.parseVersion(version);
1738
- onLog(log3(`OpenResty ${parsed} installed`));
1739
- return { component: "openresty", success: true, version: parsed };
1972
+ const version = await executor.exec(containerCommand(container, "openresty -v 2>&1")).catch(() => "");
1973
+ onLog(log4(`Edge container running${version.trim() ? ` (${version.trim()})` : ""}`));
1974
+ return { container, image, converted: bareWasOurs };
1740
1975
  } catch (err) {
1741
- if (err instanceof EdgeMigrateRequested) throw err;
1742
1976
  const msg = safeErrorMessage(err);
1743
- onLog(log3(`OpenResty installation failed: ${msg}`, "error"));
1744
- return { component: "openresty", success: false, error: msg };
1977
+ onLog(log4(`Edge container setup failed: ${msg}`, "error"));
1978
+ const logs = await executor.exec(`docker logs --tail 40 ${sq(container)} 2>&1 || true`).catch(() => "");
1979
+ if (logs.trim()) onLog(log4(`Edge container logs:
1980
+ ${logs}`, "error"));
1981
+ await executor.exec(`docker rm -f ${sq(container)} 2>/dev/null || true`).catch(() => {
1982
+ });
1983
+ invalidateEdgeContainer(executor);
1984
+ await restoreBare();
1985
+ throw new Error(`Edge container setup failed: ${msg}`);
1745
1986
  }
1746
1987
  }
1747
- var COMPONENT_INSTALLERS = {
1748
- docker: (exec, log4) => installDocker(exec, log4),
1749
- openresty: (exec, log4, config) => installOpenResty(exec, log4, config),
1750
- certbot: (exec, log4) => installCertbot(exec, log4),
1751
- git: (exec, log4) => installGit(exec, log4),
1752
- rsync: (exec, log4) => installRsync(exec, log4)
1753
- };
1754
1988
 
1755
1989
  export {
1756
1990
  systemCatalog,
1757
1991
  checkGit,
1758
1992
  checkAll,
1759
1993
  checkComponents,
1760
- ourEdgeContainerRunning,
1761
1994
  detectInstalledProxy,
1762
1995
  scanImportableSites,
1996
+ edgeProxy,
1997
+ edgeProxyFor,
1998
+ collectProxyCerts,
1763
1999
  beginEdgeTakeover,
1764
2000
  rollbackEdgeTakeover,
1765
2001
  completeEdgeTakeover,
1766
2002
  recoverInterruptedTakeover,
2003
+ EDGE_CONTAINER_NAME,
2004
+ setDefaultEdgeImage,
2005
+ resolveEdgeImage,
2006
+ dockerAvailable,
2007
+ containerEdgeProvider,
2008
+ localContainerEdgeProvider,
2009
+ buildEdgeRunCommand,
2010
+ ensureContainerEdge,
1767
2011
  importSites,
1768
2012
  foreignProxyOnEdge,
1769
2013
  installGit,