@rakay-technology/kraft 0.7.4

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 (504) hide show
  1. package/README.md +263 -0
  2. package/dist/bare-HL2GUYE2.js +27 -0
  3. package/dist/bowser-HNOTPGJO.js +2825 -0
  4. package/dist/chunk-3CJLKL6E.js +124 -0
  5. package/dist/chunk-3NQQXMDR.js +2098 -0
  6. package/dist/chunk-42WQABXM.js +439 -0
  7. package/dist/chunk-5GPXMTE4.js +25 -0
  8. package/dist/chunk-AJ4QZLZ2.js +1069 -0
  9. package/dist/chunk-AO252K7Z.js +137 -0
  10. package/dist/chunk-BQMFIXHI.js +975 -0
  11. package/dist/chunk-DM3PKWDC.js +2453 -0
  12. package/dist/chunk-DOD2BXHQ.js +1364 -0
  13. package/dist/chunk-DRK3MEDS.js +2870 -0
  14. package/dist/chunk-EW4PHKB2.js +13914 -0
  15. package/dist/chunk-HMJUFZK7.js +340 -0
  16. package/dist/chunk-I2TOOF2A.js +517 -0
  17. package/dist/chunk-I3DPMTSF.js +29 -0
  18. package/dist/chunk-JSCZT76F.js +2588 -0
  19. package/dist/chunk-MNV2QFHW.js +138 -0
  20. package/dist/chunk-MOLPYPVH.js +2529 -0
  21. package/dist/chunk-N4OALTFK.js +295 -0
  22. package/dist/chunk-OFXBNFNL.js +458 -0
  23. package/dist/chunk-ONKEYSMY.js +1388 -0
  24. package/dist/chunk-PDW4L3NV.js +17 -0
  25. package/dist/chunk-PNNXWA2A.js +1947 -0
  26. package/dist/chunk-PNRPKRJU.js +11105 -0
  27. package/dist/chunk-PVOWHIND.js +26781 -0
  28. package/dist/chunk-PXKCZB2I.js +46 -0
  29. package/dist/chunk-PZVKPCKE.js +475 -0
  30. package/dist/chunk-QWHBRY2K.js +4945 -0
  31. package/dist/chunk-RFOBMHQ7.js +338 -0
  32. package/dist/chunk-RHLNVZMS.js +28 -0
  33. package/dist/chunk-RVER2EOO.js +577 -0
  34. package/dist/chunk-S3QBGGVP.js +81 -0
  35. package/dist/chunk-TAML5WMA.js +198 -0
  36. package/dist/chunk-THTEUDBC.js +3188 -0
  37. package/dist/chunk-WYNM7L22.js +329 -0
  38. package/dist/chunk-XGCG4MD5.js +102 -0
  39. package/dist/chunk-XVKUFMIB.js +148 -0
  40. package/dist/chunk-XZYFZ6A5.js +50 -0
  41. package/dist/chunk-Y7PVJUXJ.js +2275 -0
  42. package/dist/chunk-YRSZPVMB.js +306 -0
  43. package/dist/chunk-YZUKHG3S.js +362 -0
  44. package/dist/chunk-ZVFS4LUA.js +116 -0
  45. package/dist/chunk-ZWLDLTJP.js +1417 -0
  46. package/dist/detect-676SNZK4.js +48 -0
  47. package/dist/dist-es-2YRDXMUU.js +73 -0
  48. package/dist/dist-es-CCVZEFRZ.js +91 -0
  49. package/dist/dist-es-EDUSMF6Q.js +488 -0
  50. package/dist/dist-es-IOH6G4GM.js +380 -0
  51. package/dist/dist-es-KWSMDPIP.js +26 -0
  52. package/dist/dist-es-UYDDOVZ3.js +173 -0
  53. package/dist/dist-es-XUOR4NQN.js +321 -0
  54. package/dist/docker-GOT3Q7DJ.js +41 -0
  55. package/dist/docker-edge-executor-VO6JHHW2.js +19 -0
  56. package/dist/edge-import-NFCHIW7G.js +50 -0
  57. package/dist/edge-preflight-QUJ757C6.js +45 -0
  58. package/dist/ensure-container-edge-Q7IZXHGM.js +38 -0
  59. package/dist/event-streams-TR4GAC53.js +46 -0
  60. package/dist/executor-GKXKN52K.js +42 -0
  61. package/dist/host-channel-preflight-Z6HYSPKO.js +48 -0
  62. package/dist/index.d.ts +2 -0
  63. package/dist/index.js +9469 -0
  64. package/dist/loadSso-UGF4JMRK.js +581 -0
  65. package/dist/local-executor-OMMDKXCM.js +13 -0
  66. package/dist/nginx-VITTWZZU.js +42 -0
  67. package/dist/node-bootstrap.js +277 -0
  68. package/dist/node-entry.js +55 -0
  69. package/dist/noop-CLLJHCPL.js +11 -0
  70. package/dist/openresty-lua-6I3Z6PEQ.js +90 -0
  71. package/dist/privilege-UQAI2HV4.js +20 -0
  72. package/dist/server/engine/ChangeLog +2289 -0
  73. package/dist/server/engine/Documentations +5 -0
  74. package/dist/server/engine/LICENSE +674 -0
  75. package/dist/server/engine/README.md +34 -0
  76. package/dist/server/engine/conf/amavisd +150 -0
  77. package/dist/server/engine/conf/clamav +71 -0
  78. package/dist/server/engine/conf/core +632 -0
  79. package/dist/server/engine/conf/dovecot +151 -0
  80. package/dist/server/engine/conf/fail2ban +72 -0
  81. package/dist/server/engine/conf/global +698 -0
  82. package/dist/server/engine/conf/iredapd +52 -0
  83. package/dist/server/engine/conf/logwatch +28 -0
  84. package/dist/server/engine/conf/postfix +97 -0
  85. package/dist/server/engine/conf/postgresql +93 -0
  86. package/dist/server/engine/conf/spamassassin +39 -0
  87. package/dist/server/engine/dialog/config_via_dialog.sh +258 -0
  88. package/dist/server/engine/dialog/optional_components.sh +122 -0
  89. package/dist/server/engine/dialog/pgsql_config.sh +52 -0
  90. package/dist/server/engine/dialog/virtual_domain_config.sh +94 -0
  91. package/dist/server/engine/functions/amavisd.sh +267 -0
  92. package/dist/server/engine/functions/backend.sh +85 -0
  93. package/dist/server/engine/functions/clamav.sh +100 -0
  94. package/dist/server/engine/functions/cleanup.sh +393 -0
  95. package/dist/server/engine/functions/dovecot.sh +583 -0
  96. package/dist/server/engine/functions/fail2ban.sh +209 -0
  97. package/dist/server/engine/functions/iredapd.sh +356 -0
  98. package/dist/server/engine/functions/optional_components.sh +24 -0
  99. package/dist/server/engine/functions/packages.sh +522 -0
  100. package/dist/server/engine/functions/packages_freebsd.sh +179 -0
  101. package/dist/server/engine/functions/postfix.sh +457 -0
  102. package/dist/server/engine/functions/postgresql.sh +239 -0
  103. package/dist/server/engine/functions/spamassassin.sh +49 -0
  104. package/dist/server/engine/functions/system_accounts.sh +128 -0
  105. package/dist/server/engine/iRedMail.sh +144 -0
  106. package/dist/server/engine/pkgs/get_all.sh +338 -0
  107. package/dist/server/engine/pkgs/pkgs.freebsd.sha256 +3 -0
  108. package/dist/server/engine/pkgs/pkgs.openbsd.sha256 +6 -0
  109. package/dist/server/engine/pkgs/pkgs.sha256 +1 -0
  110. package/dist/server/engine/samples/amavisd/amavisd.conf +702 -0
  111. package/dist/server/engine/samples/amavisd/amavisd.mysql +278 -0
  112. package/dist/server/engine/samples/amavisd/amavisd.pgsql +253 -0
  113. package/dist/server/engine/samples/amavisd/amavisd.schema +942 -0
  114. package/dist/server/engine/samples/amavisd/default_spam_policy.sql +46 -0
  115. package/dist/server/engine/samples/dovecot/dovecot-2.4-mariadb.conf +758 -0
  116. package/dist/server/engine/samples/dovecot/dovecot-2.4-openldap.conf +767 -0
  117. package/dist/server/engine/samples/dovecot/dovecot-2.4-pgsql.conf +762 -0
  118. package/dist/server/engine/samples/dovecot/dovecot-last-login.conf +50 -0
  119. package/dist/server/engine/samples/dovecot/dovecot-ldap.conf +21 -0
  120. package/dist/server/engine/samples/dovecot/dovecot-share-folder.conf +22 -0
  121. package/dist/server/engine/samples/dovecot/dovecot-sql.conf +28 -0
  122. package/dist/server/engine/samples/dovecot/dovecot-used-quota.conf +13 -0
  123. package/dist/server/engine/samples/dovecot/dovecot.conf +484 -0
  124. package/dist/server/engine/samples/dovecot/dovecot.sieve +7 -0
  125. package/dist/server/engine/samples/dovecot/dovecot2-quota-warning.sh +38 -0
  126. package/dist/server/engine/samples/dovecot/sql/imap_share_folder.mysql +16 -0
  127. package/dist/server/engine/samples/dovecot/sql/last_login.mysql +12 -0
  128. package/dist/server/engine/samples/dovecot/sql/used_quota.mysql +26 -0
  129. package/dist/server/engine/samples/dovecot/systemd/override.conf +3 -0
  130. package/dist/server/engine/samples/fail2ban/action.d/banned_db.conf +9 -0
  131. package/dist/server/engine/samples/fail2ban/bin/fail2ban_banned_db +268 -0
  132. package/dist/server/engine/samples/fail2ban/fail2ban.local +17 -0
  133. package/dist/server/engine/samples/fail2ban/filter.d/dovecot.iredmail.conf +5 -0
  134. package/dist/server/engine/samples/fail2ban/filter.d/postfix-pregreet.iredmail.conf +16 -0
  135. package/dist/server/engine/samples/fail2ban/filter.d/postfix.iredmail.conf +18 -0
  136. package/dist/server/engine/samples/fail2ban/filter.d/roundcube.iredmail.conf +9 -0
  137. package/dist/server/engine/samples/fail2ban/jail.d/dovecot.local +8 -0
  138. package/dist/server/engine/samples/fail2ban/jail.d/nginx-http-auth.local +8 -0
  139. package/dist/server/engine/samples/fail2ban/jail.d/postfix-pregreet.local +9 -0
  140. package/dist/server/engine/samples/fail2ban/jail.d/postfix.local +8 -0
  141. package/dist/server/engine/samples/fail2ban/jail.d/roundcube.local +9 -0
  142. package/dist/server/engine/samples/fail2ban/jail.d/sogo.local +8 -0
  143. package/dist/server/engine/samples/fail2ban/jail.d/sshd.local +11 -0
  144. package/dist/server/engine/samples/fail2ban/jail.local +6 -0
  145. package/dist/server/engine/samples/fail2ban/openbsd/rc +27 -0
  146. package/dist/server/engine/samples/fail2ban/sql/fail2ban.mysql +49 -0
  147. package/dist/server/engine/samples/fail2ban/sql/fail2ban.pgsql +48 -0
  148. package/dist/server/engine/samples/firewall/firewalld/services/ssh.xml +6 -0
  149. package/dist/server/engine/samples/firewall/firewalld/zones/iredmail.xml +15 -0
  150. package/dist/server/engine/samples/firewall/iptables/ip6tables.init.debian +362 -0
  151. package/dist/server/engine/samples/firewall/iptables/ip6tables.rules +53 -0
  152. package/dist/server/engine/samples/firewall/iptables/iptables.init.debian +363 -0
  153. package/dist/server/engine/samples/firewall/iptables/iptables.rules +54 -0
  154. package/dist/server/engine/samples/firewall/nftables.conf +58 -0
  155. package/dist/server/engine/samples/freebsd/newsyslog.conf.d/dovecot.conf +3 -0
  156. package/dist/server/engine/samples/freebsd/newsyslog.conf.d/iredapd.conf +1 -0
  157. package/dist/server/engine/samples/freebsd/newsyslog.conf.d/mlmmjadmin.conf +1 -0
  158. package/dist/server/engine/samples/freebsd/newsyslog.conf.d/php-fpm.conf +2 -0
  159. package/dist/server/engine/samples/freebsd/newsyslog.conf.d/slapd.conf +1 -0
  160. package/dist/server/engine/samples/freebsd/syslog.d/dovecot.conf +2 -0
  161. package/dist/server/engine/samples/freebsd/syslog.d/fail2ban.conf +2 -0
  162. package/dist/server/engine/samples/freebsd/syslog.d/iredapd.conf +2 -0
  163. package/dist/server/engine/samples/freebsd/syslog.d/mlmmjadmin.conf +2 -0
  164. package/dist/server/engine/samples/freebsd/syslog.d/php-fpm.conf +2 -0
  165. package/dist/server/engine/samples/freebsd/syslog.d/slapd.conf +2 -0
  166. package/dist/server/engine/samples/iredmail/iredmail.mysql +491 -0
  167. package/dist/server/engine/samples/iredmail/iredmail.pgsql +465 -0
  168. package/dist/server/engine/samples/iredmail/iredmail.schema +456 -0
  169. package/dist/server/engine/samples/iredmail/used_quota_triggers_dovecot_2.3.pgsql +40 -0
  170. package/dist/server/engine/samples/iredmail/used_quota_triggers_dovecot_2.4.pgsql +18 -0
  171. package/dist/server/engine/samples/logrotate/dovecot +13 -0
  172. package/dist/server/engine/samples/logrotate/iredapd +13 -0
  173. package/dist/server/engine/samples/logrotate/mlmmjadmin +13 -0
  174. package/dist/server/engine/samples/logrotate/openldap +19 -0
  175. package/dist/server/engine/samples/logrotate/php-fpm +13 -0
  176. package/dist/server/engine/samples/openbsd/ldapd.conf +58 -0
  177. package/dist/server/engine/samples/openbsd/pf.conf +28 -0
  178. package/dist/server/engine/samples/postfix/command_filter.pcre +3 -0
  179. package/dist/server/engine/samples/postfix/freebsd/mailer.conf +9 -0
  180. package/dist/server/engine/samples/postfix/helo_access.pcre +180 -0
  181. package/dist/server/engine/samples/postfix/ldap/catchall_maps.cf +11 -0
  182. package/dist/server/engine/samples/postfix/ldap/recipient_bcc_maps_domain.cf +11 -0
  183. package/dist/server/engine/samples/postfix/ldap/recipient_bcc_maps_user.cf +11 -0
  184. package/dist/server/engine/samples/postfix/ldap/relay_domains.cf +11 -0
  185. package/dist/server/engine/samples/postfix/ldap/sender_bcc_maps_domain.cf +11 -0
  186. package/dist/server/engine/samples/postfix/ldap/sender_bcc_maps_user.cf +11 -0
  187. package/dist/server/engine/samples/postfix/ldap/sender_dependent_relayhost_maps_domain.cf +11 -0
  188. package/dist/server/engine/samples/postfix/ldap/sender_dependent_relayhost_maps_user.cf +11 -0
  189. package/dist/server/engine/samples/postfix/ldap/sender_login_maps.cf +11 -0
  190. package/dist/server/engine/samples/postfix/ldap/transport_maps_domain.cf +11 -0
  191. package/dist/server/engine/samples/postfix/ldap/transport_maps_user.cf +11 -0
  192. package/dist/server/engine/samples/postfix/ldap/virtual_alias_maps.cf +11 -0
  193. package/dist/server/engine/samples/postfix/ldap/virtual_group_maps.cf +11 -0
  194. package/dist/server/engine/samples/postfix/ldap/virtual_group_members_maps.cf +11 -0
  195. package/dist/server/engine/samples/postfix/ldap/virtual_mailbox_domains.cf +11 -0
  196. package/dist/server/engine/samples/postfix/ldap/virtual_mailbox_maps.cf +12 -0
  197. package/dist/server/engine/samples/postfix/main.cf +282 -0
  198. package/dist/server/engine/samples/postfix/main.cf.amavisd +7 -0
  199. package/dist/server/engine/samples/postfix/main.cf.dovecot +8 -0
  200. package/dist/server/engine/samples/postfix/main.cf.ldap +39 -0
  201. package/dist/server/engine/samples/postfix/main.cf.mlmmj +5 -0
  202. package/dist/server/engine/samples/postfix/main.cf.mysql +39 -0
  203. package/dist/server/engine/samples/postfix/main.cf.pgsql +39 -0
  204. package/dist/server/engine/samples/postfix/main.cf.postscreen +30 -0
  205. package/dist/server/engine/samples/postfix/master.cf +101 -0
  206. package/dist/server/engine/samples/postfix/mysql/catchall_maps.cf +5 -0
  207. package/dist/server/engine/samples/postfix/mysql/domain_alias_catchall_maps.cf +5 -0
  208. package/dist/server/engine/samples/postfix/mysql/domain_alias_maps.cf +5 -0
  209. package/dist/server/engine/samples/postfix/mysql/recipient_bcc_maps_domain.cf +5 -0
  210. package/dist/server/engine/samples/postfix/mysql/recipient_bcc_maps_user.cf +5 -0
  211. package/dist/server/engine/samples/postfix/mysql/relay_domains.cf +5 -0
  212. package/dist/server/engine/samples/postfix/mysql/sender_bcc_maps_domain.cf +5 -0
  213. package/dist/server/engine/samples/postfix/mysql/sender_bcc_maps_user.cf +5 -0
  214. package/dist/server/engine/samples/postfix/mysql/sender_dependent_relayhost_maps.cf +6 -0
  215. package/dist/server/engine/samples/postfix/mysql/sender_login_maps.cf +5 -0
  216. package/dist/server/engine/samples/postfix/mysql/transport_maps_domain.cf +5 -0
  217. package/dist/server/engine/samples/postfix/mysql/transport_maps_maillist.cf +5 -0
  218. package/dist/server/engine/samples/postfix/mysql/transport_maps_user.cf +5 -0
  219. package/dist/server/engine/samples/postfix/mysql/virtual_alias_maps.cf +5 -0
  220. package/dist/server/engine/samples/postfix/mysql/virtual_mailbox_domains.cf +5 -0
  221. package/dist/server/engine/samples/postfix/mysql/virtual_mailbox_maps.cf +5 -0
  222. package/dist/server/engine/samples/postfix/pgsql/catchall_maps.cf +5 -0
  223. package/dist/server/engine/samples/postfix/pgsql/domain_alias_catchall_maps.cf +5 -0
  224. package/dist/server/engine/samples/postfix/pgsql/domain_alias_maps.cf +5 -0
  225. package/dist/server/engine/samples/postfix/pgsql/recipient_bcc_maps_domain.cf +5 -0
  226. package/dist/server/engine/samples/postfix/pgsql/recipient_bcc_maps_user.cf +5 -0
  227. package/dist/server/engine/samples/postfix/pgsql/relay_domains.cf +5 -0
  228. package/dist/server/engine/samples/postfix/pgsql/sender_bcc_maps_domain.cf +5 -0
  229. package/dist/server/engine/samples/postfix/pgsql/sender_bcc_maps_user.cf +5 -0
  230. package/dist/server/engine/samples/postfix/pgsql/sender_dependent_relayhost_maps.cf +6 -0
  231. package/dist/server/engine/samples/postfix/pgsql/sender_login_maps.cf +5 -0
  232. package/dist/server/engine/samples/postfix/pgsql/transport_maps_domain.cf +5 -0
  233. package/dist/server/engine/samples/postfix/pgsql/transport_maps_maillist.cf +5 -0
  234. package/dist/server/engine/samples/postfix/pgsql/transport_maps_user.cf +5 -0
  235. package/dist/server/engine/samples/postfix/pgsql/virtual_alias_maps.cf +5 -0
  236. package/dist/server/engine/samples/postfix/pgsql/virtual_mailbox_domains.cf +5 -0
  237. package/dist/server/engine/samples/postfix/pgsql/virtual_mailbox_maps.cf +5 -0
  238. package/dist/server/engine/samples/postfix/postscreen_access.cidr +6 -0
  239. package/dist/server/engine/samples/postgresql/sql/add_first_domain_and_user.sql +34 -0
  240. package/dist/server/engine/samples/postgresql/sql/grant_permissions.sql +14 -0
  241. package/dist/server/engine/samples/postgresql/sql/init_vmail_db.sql +15 -0
  242. package/dist/server/engine/samples/rsyslog.d/1-iredmail-dovecot.conf +23 -0
  243. package/dist/server/engine/samples/rsyslog.d/1-iredmail-fail2ban.conf +2 -0
  244. package/dist/server/engine/samples/rsyslog.d/1-iredmail-iredapd.conf +12 -0
  245. package/dist/server/engine/samples/rsyslog.d/1-iredmail-mlmmjadmin.conf +12 -0
  246. package/dist/server/engine/samples/rsyslog.d/1-iredmail-openldap.conf +2 -0
  247. package/dist/server/engine/samples/rsyslog.d/1-iredmail-phpfpm.conf +3 -0
  248. package/dist/server/engine/samples/spamassassin/local.cf +186 -0
  249. package/dist/server/engine/samples/spamassassin/razor.conf +1 -0
  250. package/dist/server/engine/samples/systemd/clamd.service.d/override.conf +2 -0
  251. package/dist/server/engine/samples/systemd/mariadb.service.d/override.conf +3 -0
  252. package/dist/server/engine/samples/systemd/slapd.service.d/override.conf +6 -0
  253. package/dist/server/engine/samples/yum/CentOS-Linux-AppStream.repo +6 -0
  254. package/dist/server/engine/samples/yum/CentOS-Linux-PowerTools.repo +6 -0
  255. package/dist/server/engine/samples/yum/CentOS-Stream-AppStream.repo +7 -0
  256. package/dist/server/engine/samples/yum/CentOS-Stream-PowerTools.repo +7 -0
  257. package/dist/server/engine/samples/yum/symas-openldap.repo +6 -0
  258. package/dist/server/engine/tools/add_enabled_service.py +60 -0
  259. package/dist/server/engine/tools/backup_mysql.sh +238 -0
  260. package/dist/server/engine/tools/backup_openldap.sh +230 -0
  261. package/dist/server/engine/tools/backup_pgsql.sh +217 -0
  262. package/dist/server/engine/tools/backup_sogo.sh +156 -0
  263. package/dist/server/engine/tools/create_mail_user_OpenLDAP.py +335 -0
  264. package/dist/server/engine/tools/create_mail_user_OpenLDAP.sh +287 -0
  265. package/dist/server/engine/tools/create_mail_user_SQL.sh +127 -0
  266. package/dist/server/engine/tools/enable_postscreen.sh +144 -0
  267. package/dist/server/engine/tools/fail2ban_unban_ip.sh +39 -0
  268. package/dist/server/engine/tools/find_sasl_login_ip.sh +20 -0
  269. package/dist/server/engine/tools/find_top_sasl_usernames.sh +28 -0
  270. package/dist/server/engine/tools/generate_ssl_keys.sh +85 -0
  271. package/dist/server/engine/tools/kill_high_cpu_sogo_process.sh +32 -0
  272. package/dist/server/engine/tools/ldap_assign_user_to_groups.py +151 -0
  273. package/dist/server/engine/tools/ldap_move_members_to_another_group.py +129 -0
  274. package/dist/server/engine/tools/migrate_sql_alias_table.py +177 -0
  275. package/dist/server/engine/update/0.9.8/amavisd.mysql +29 -0
  276. package/dist/server/engine/update/0.9.8/amavisd.pgsql +39 -0
  277. package/dist/server/engine/update/0.9.8/iredmail.mysql +53 -0
  278. package/dist/server/engine/update/0.9.8/iredmail.pgsql +61 -0
  279. package/dist/server/engine/update/0.9.9/iredmail.mysql +2 -0
  280. package/dist/server/engine/update/0.9.9/iredmail.pgsql +2 -0
  281. package/dist/server/engine/update/1.0/iredmail.mysql +2 -0
  282. package/dist/server/engine/update/1.0/iredmail.pgsql +2 -0
  283. package/dist/server/engine/update/1.2/last_login.mysql +16 -0
  284. package/dist/server/engine/update/1.4.0/iredmail.mysql +22 -0
  285. package/dist/server/engine/update/1.4.0/iredmail.pgsql +20 -0
  286. package/dist/server/engine/update/1.4.1/iredmail.mysql +3 -0
  287. package/dist/server/engine/update/1.4.1/iredmail.pgsql +3 -0
  288. package/dist/server/engine/update/1.4.1/sogo.mysql +11 -0
  289. package/dist/server/engine/update/1.4.1/sogo.pgsql +30 -0
  290. package/dist/server/engine/update/1.4.1/update-ldap.py +66 -0
  291. package/dist/server/engine/update/1.4.2/iredmail.mysql +9 -0
  292. package/dist/server/engine/update/1.4.2/iredmail.pgsql +9 -0
  293. package/dist/server/engine/update/1.4.2/sogo.pgsql +30 -0
  294. package/dist/server/engine/update/1.6.0/sogo_view.pgsql +27 -0
  295. package/dist/server/engine/update/1.6.3/iredmail.mysql +1 -0
  296. package/dist/server/engine/update/1.7.0/fail2ban.mysql +11 -0
  297. package/dist/server/engine/update/1.7.0/fail2ban.pgsql +12 -0
  298. package/dist/server/engine/update/1.7.1/amavisd.mysql +1 -0
  299. package/dist/server/engine/update/1.7.2/vmail.mysql +30 -0
  300. package/dist/server/engine/update/1.7.3/deleted_mailboxes.mysql +43 -0
  301. package/dist/server/engine/update/1.7.3/iredadmin.mysql +26 -0
  302. package/dist/server/engine/update/1.7.3/vmail.mysql +79 -0
  303. package/dist/server/engine/update/1.7.3/vmail.pgsql +9 -0
  304. package/dist/server/engine/update/README.md +1 -0
  305. package/dist/server/engine/update/ldap/README.md +39 -0
  306. package/dist/server/engine/update/ldap/update-ldap-dovecot-2.3.py +69 -0
  307. package/dist/server/index.js +349016 -0
  308. package/dist/server/lua/geo_country.lua +127 -0
  309. package/dist/server/lua/maxminddb.lua +403 -0
  310. package/dist/server/lua/mgmt_api.lua +654 -0
  311. package/dist/server/lua/pipe_stream.lua +103 -0
  312. package/dist/server/lua/rules_guard.lua +132 -0
  313. package/dist/server/lua/rules_lib.lua +176 -0
  314. package/dist/server/lua/site_logger.lua +405 -0
  315. package/dist/server/lua/webhook_handler.lua +126 -0
  316. package/dist/server/migrations/0000_init.sql +666 -0
  317. package/dist/server/migrations/0001_brief_tomorrow_man.sql +47 -0
  318. package/dist/server/migrations/0002_funny_dazzler.sql +2 -0
  319. package/dist/server/migrations/0003_third_killer_shrike.sql +1 -0
  320. package/dist/server/migrations/0004_session_active_org_not_null.sql +10 -0
  321. package/dist/server/migrations/0005_warm_amazoness.sql +3 -0
  322. package/dist/server/migrations/0006_lumpy_joystick.sql +1 -0
  323. package/dist/server/migrations/0007_outgoing_maginty.sql +2 -0
  324. package/dist/server/migrations/0008_frozen_cutover.sql +3 -0
  325. package/dist/server/migrations/0009_credits_billing.sql +142 -0
  326. package/dist/server/migrations/0010_credit_grant_dedupe.sql +14 -0
  327. package/dist/server/migrations/0011_remove_credit_ledger.sql +23 -0
  328. package/dist/server/migrations/0012_smart_deploys_and_git_rollback.sql +81 -0
  329. package/dist/server/migrations/0013_fix_check_run_rollup_unique.sql +38 -0
  330. package/dist/server/migrations/0014_default_rollback_git.sql +9 -0
  331. package/dist/server/migrations/0015_billing_idempotency_grants.sql +41 -0
  332. package/dist/server/migrations/0016_github_auth_high_findings.sql +30 -0
  333. package/dist/server/migrations/0017_spooky_rumiko_fujikawa.sql +6 -0
  334. package/dist/server/migrations/0018_calm_shatterstar.sql +1 -0
  335. package/dist/server/migrations/0019_rename_workspace_install_to_prepare.sql +1 -0
  336. package/dist/server/migrations/0020_uneven_the_renegades.sql +13 -0
  337. package/dist/server/migrations/0021_shiny_firestar.sql +2 -0
  338. package/dist/server/migrations/0022_version_on_success_backfill.sql +6 -0
  339. package/dist/server/migrations/0023_productive_gorilla_man.sql +17 -0
  340. package/dist/server/migrations/0024_sleepy_peter_quill.sql +2 -0
  341. package/dist/server/migrations/0025_smooth_blue_shield.sql +1 -0
  342. package/dist/server/migrations/0026_mail_backup_source.sql +11 -0
  343. package/dist/server/migrations/0027_opposite_anthem.sql +6 -0
  344. package/dist/server/migrations/0028_stale_rogue.sql +18 -0
  345. package/dist/server/migrations/0029_bouncy_loners.sql +16 -0
  346. package/dist/server/migrations/0030_bright_darkstar.sql +6 -0
  347. package/dist/server/migrations/0031_conscious_catseye.sql +13 -0
  348. package/dist/server/migrations/0032_wet_mimic.sql +51 -0
  349. package/dist/server/migrations/0033_eminent_molly_hayes.sql +2 -0
  350. package/dist/server/migrations/0034_nervous_surge.sql +1 -0
  351. package/dist/server/migrations/0035_thankful_morgan_stark.sql +1 -0
  352. package/dist/server/migrations/0036_gorgeous_scalphunter.sql +17 -0
  353. package/dist/server/migrations/0037_clever_makkari.sql +28 -0
  354. package/dist/server/migrations/0038_young_magneto.sql +1 -0
  355. package/dist/server/migrations/0039_secret_lady_ursula.sql +15 -0
  356. package/dist/server/migrations/0040_sharp_red_wolf.sql +16 -0
  357. package/dist/server/migrations/0041_round_spyke.sql +4 -0
  358. package/dist/server/migrations/0042_dapper_havok.sql +1 -0
  359. package/dist/server/migrations/0043_fast_sharon_ventura.sql +15 -0
  360. package/dist/server/migrations/0044_amused_shape.sql +8 -0
  361. package/dist/server/migrations/0045_amusing_korath.sql +1 -0
  362. package/dist/server/migrations/0046_white_harrier.sql +35 -0
  363. package/dist/server/migrations/0047_sloppy_strong_guy.sql +2 -0
  364. package/dist/server/migrations/0048_funny_emma_frost.sql +2 -0
  365. package/dist/server/migrations/0049_nappy_piledriver.sql +3 -0
  366. package/dist/server/migrations/0050_add_service_deployment_image_digest.sql +1 -0
  367. package/dist/server/migrations/0051_add_update_status.sql +19 -0
  368. package/dist/server/migrations/0052_add_notice_target.sql +2 -0
  369. package/dist/server/migrations/0053_colossal_sandman.sql +14 -0
  370. package/dist/server/migrations/0054_add_instance_smtp.sql +5 -0
  371. package/dist/server/migrations/0055_rainy_klaw.sql +22 -0
  372. package/dist/server/migrations/0056_moaning_daredevil.sql +2 -0
  373. package/dist/server/migrations/0057_complete_prism.sql +18 -0
  374. package/dist/server/migrations/0058_add_server_is_local.sql +1 -0
  375. package/dist/server/migrations/0059_legal_fixer.sql +18 -0
  376. package/dist/server/migrations/0060_add_route_strategy_host_port.sql +3 -0
  377. package/dist/server/migrations/0061_notification_default_multi_kind.sql +3 -0
  378. package/dist/server/migrations/0062_add_incoming_webhook.sql +21 -0
  379. package/dist/server/migrations/0063_add_migration_logs.sql +1 -0
  380. package/dist/server/migrations/0064_add_migration_partial.sql +2 -0
  381. package/dist/server/migrations/0065_custom_app_template.sql +14 -0
  382. package/dist/server/migrations/0066_ensure_migration_partial_columns.sql +2 -0
  383. package/dist/server/migrations/0067_webhook_delivery.sql +28 -0
  384. package/dist/server/migrations/0068_add_migration_target_volumes.sql +1 -0
  385. package/dist/server/migrations/0069_add_project_composite_routes.sql +1 -0
  386. package/dist/server/migrations/0070_forward_git_to_server.sql +1 -0
  387. package/dist/server/migrations/0071_gh_device_token.sql +3 -0
  388. package/dist/server/migrations/0072_gh_device_token_method.sql +1 -0
  389. package/dist/server/migrations/0073_project_volumes_object_storage.sql +3 -0
  390. package/dist/server/migrations/0074_project_compose_path.sql +1 -0
  391. package/dist/server/migrations/0075_service_command_argv.sql +1 -0
  392. package/dist/server/migrations/0076_rollback_retention.sql +17 -0
  393. package/dist/server/migrations/0077_domain_redirect.sql +13 -0
  394. package/dist/server/migrations/0078_grant_source_scope.sql +30 -0
  395. package/dist/server/migrations/0079_project_readiness.sql +18 -0
  396. package/dist/server/migrations/0080_deployment_error_classification.sql +26 -0
  397. package/dist/server/migrations/0081_project_readiness_repair.sql +34 -0
  398. package/dist/server/migrations/0082_edge_target_verification.sql +39 -0
  399. package/dist/server/migrations/0083_service_incident.sql +84 -0
  400. package/dist/server/migrations/0084_analytics_daily_rollup.sql +72 -0
  401. package/dist/server/migrations/0085_resource_usage.sql +68 -0
  402. package/dist/server/migrations/0086_remote_infra_updates.sql +41 -0
  403. package/dist/server/migrations/0087_server_container_version_columns.sql +17 -0
  404. package/dist/server/migrations/0088_audit_source_and_settings.sql +27 -0
  405. package/dist/server/migrations/0089_auto_scan_infra.sql +7 -0
  406. package/dist/server/migrations/0090_project_collect_paths.sql +18 -0
  407. package/dist/server/migrations/0091_project_server_id.sql +26 -0
  408. package/dist/server/migrations/0092_project_internal_alias.sql +13 -0
  409. package/dist/server/migrations/0093_service_incident_org_index.sql +13 -0
  410. package/dist/server/migrations/0094_backup_restore_meta.sql +15 -0
  411. package/dist/server/migrations/0095_backup_restore_cancel.sql +19 -0
  412. package/dist/server/migrations/0096_backup_policy_retention_defaults.sql +20 -0
  413. package/dist/server/migrations/0097_update_status_upstream_only.sql +35 -0
  414. package/dist/server/migrations/0098_instance_product_mode.sql +12 -0
  415. package/dist/server/migrations/0099_mail_webmail_project.sql +24 -0
  416. package/dist/server/migrations/0100_server_ssh_private_key.sql +11 -0
  417. package/dist/server/migrations/0101_mail_resume_step.sql +12 -0
  418. package/dist/server/migrations/0102_project_workload_axes.sql +19 -0
  419. package/dist/server/migrations/0103_dns_credential.sql +41 -0
  420. package/dist/server/migrations/0104_instance_host_control.sql +15 -0
  421. package/dist/server/migrations/0105_mail_inbound_rules.sql +80 -0
  422. package/dist/server/migrations/0106_mcp_call_tracking.sql +23 -0
  423. package/dist/server/migrations/0107_build_minute_metering_indexes.sql +20 -0
  424. package/dist/server/migrations/0108_credential.sql +68 -0
  425. package/dist/server/migrations/0109_service_build_args.sql +9 -0
  426. package/dist/server/migrations/0110_service_deployment_host_ports.sql +18 -0
  427. package/dist/server/migrations/0111_host_port_claim.sql +381 -0
  428. package/dist/server/migrations/0112_backup_run_execution_lease.sql +12 -0
  429. package/dist/server/migrations/0113_docker_migration_execution_lease.sql +11 -0
  430. package/dist/server/migrations/0114_build_session_execution_lease_index.sql +6 -0
  431. package/dist/server/migrations/0115_orphan_target_key.sql +3 -0
  432. package/dist/server/migrations/0116_domain_ssl_challenge.sql +2 -0
  433. package/dist/server/migrations/0117_github_installation_workspace_scope.sql +24 -0
  434. package/dist/server/migrations/0118_git_sources.sql +66 -0
  435. package/dist/server/migrations/0119_backup_run_batch.sql +8 -0
  436. package/dist/server/migrations/0120_domain_primary_unique.sql +24 -0
  437. package/dist/server/migrations/0121_host_port_claim_overlap.sql +14 -0
  438. package/dist/server/migrations/0122_data_transfer_staging.sql +45 -0
  439. package/dist/server/migrations/0123_drop_cloud_infra.sql +7 -0
  440. package/dist/server/migrations/0124_mail_dmarc_report.sql +23 -0
  441. package/dist/server/migrations/0125_server_security_snapshot.sql +22 -0
  442. package/dist/server/migrations/meta/0000_snapshot.json +4848 -0
  443. package/dist/server/migrations/meta/0001_snapshot.json +4991 -0
  444. package/dist/server/migrations/meta/0002_snapshot.json +5013 -0
  445. package/dist/server/migrations/meta/0003_snapshot.json +5020 -0
  446. package/dist/server/migrations/meta/0004_snapshot.json +5020 -0
  447. package/dist/server/migrations/meta/0005_snapshot.json +5039 -0
  448. package/dist/server/migrations/meta/0006_snapshot.json +5045 -0
  449. package/dist/server/migrations/meta/0007_snapshot.json +5057 -0
  450. package/dist/server/migrations/meta/0016_snapshot.json +6053 -0
  451. package/dist/server/migrations/meta/0017_snapshot.json +6091 -0
  452. package/dist/server/migrations/meta/0018_snapshot.json +6098 -0
  453. package/dist/server/migrations/meta/0019_snapshot.json +6098 -0
  454. package/dist/server/migrations/meta/0020_snapshot.json +6201 -0
  455. package/dist/server/migrations/meta/0021_snapshot.json +6213 -0
  456. package/dist/server/migrations/meta/0022_snapshot.json +6213 -0
  457. package/dist/server/migrations/meta/0023_snapshot.json +6331 -0
  458. package/dist/server/migrations/meta/0024_snapshot.json +6343 -0
  459. package/dist/server/migrations/meta/0025_snapshot.json +6350 -0
  460. package/dist/server/migrations/meta/0026_snapshot.json +6440 -0
  461. package/dist/server/migrations/meta/0027_snapshot.json +6447 -0
  462. package/dist/server/migrations/meta/0028_snapshot.json +6581 -0
  463. package/dist/server/migrations/meta/0029_snapshot.json +6710 -0
  464. package/dist/server/migrations/meta/0030_snapshot.json +6748 -0
  465. package/dist/server/migrations/meta/0031_snapshot.json +6859 -0
  466. package/dist/server/migrations/meta/0032_snapshot.json +7222 -0
  467. package/dist/server/migrations/meta/0033_snapshot.json +7249 -0
  468. package/dist/server/migrations/meta/0034_snapshot.json +7255 -0
  469. package/dist/server/migrations/meta/0035_snapshot.json +7262 -0
  470. package/dist/server/migrations/meta/0036_snapshot.json +7392 -0
  471. package/dist/server/migrations/meta/0037_snapshot.json +7602 -0
  472. package/dist/server/migrations/meta/0038_snapshot.json +7609 -0
  473. package/dist/server/migrations/meta/0039_snapshot.json +7714 -0
  474. package/dist/server/migrations/meta/0040_snapshot.json +7820 -0
  475. package/dist/server/migrations/meta/0041_snapshot.json +7833 -0
  476. package/dist/server/migrations/meta/0042_snapshot.json +7839 -0
  477. package/dist/server/migrations/meta/0043_snapshot.json +7938 -0
  478. package/dist/server/migrations/meta/0044_snapshot.json +7982 -0
  479. package/dist/server/migrations/meta/0045_snapshot.json +7989 -0
  480. package/dist/server/migrations/meta/0046_snapshot.json +8263 -0
  481. package/dist/server/migrations/meta/0047_snapshot.json +8275 -0
  482. package/dist/server/migrations/meta/0048_snapshot.json +8289 -0
  483. package/dist/server/migrations/meta/0049_snapshot.json +8308 -0
  484. package/dist/server/migrations/meta/0050_snapshot.json +8314 -0
  485. package/dist/server/migrations/meta/0051_snapshot.json +8460 -0
  486. package/dist/server/migrations/meta/0052_snapshot.json +8472 -0
  487. package/dist/server/migrations/meta/0053_snapshot.json +8556 -0
  488. package/dist/server/migrations/meta/0054_snapshot.json +8586 -0
  489. package/dist/server/migrations/meta/0055_snapshot.json +8756 -0
  490. package/dist/server/migrations/meta/0056_snapshot.json +8777 -0
  491. package/dist/server/migrations/meta/0057_snapshot.json +8904 -0
  492. package/dist/server/migrations/meta/0058_snapshot.json +8911 -0
  493. package/dist/server/migrations/meta/0059_snapshot.json +9061 -0
  494. package/dist/server/migrations/meta/0060_snapshot.json +9081 -0
  495. package/dist/server/migrations/meta/_journal.json +888 -0
  496. package/dist/server/pglite/pglite.data +0 -0
  497. package/dist/server/pglite/pglite.wasm +0 -0
  498. package/dist/setup-JWKZIECM.js +25 -0
  499. package/dist/signin-3M3UPB6K.js +667 -0
  500. package/dist/ssh-client-NWQVRCV7.js +29 -0
  501. package/dist/sso-oidc-3VDIYOEJ.js +834 -0
  502. package/dist/sts-B3WIYLXR.js +1426 -0
  503. package/dist/up-P5DMQ5YO.js +55 -0
  504. package/package.json +58 -0
@@ -0,0 +1,3188 @@
1
+ #!/usr/bin/env sh
2
+ ":" //# ; exec "$(command -v node || command -v bun)" "$0" "$@"
3
+ import { createRequire as __ospCreateRequire } from "node:module";
4
+ const require = __ospCreateRequire(import.meta.url);
5
+ import {
6
+ containerCommand,
7
+ edgeContainerExecutor,
8
+ isSafeCertPath,
9
+ readDeclaredPair,
10
+ readMaybeInContainer,
11
+ validateCertFor
12
+ } from "./chunk-Y7PVJUXJ.js";
13
+ import {
14
+ EdgeConflictError,
15
+ EdgeMigrateRequested,
16
+ detectEdgeContainer,
17
+ edgeCrashReason,
18
+ edgeFailureReason,
19
+ edgeIsBroken,
20
+ freeEdgeTargets,
21
+ invalidateEdgeContainer,
22
+ probeEdge,
23
+ resolveOurEdgeContainer,
24
+ sanitizeEdgeVhosts,
25
+ sq as sq2,
26
+ stopTargetsForStatus
27
+ } from "./chunk-YZUKHG3S.js";
28
+ import {
29
+ privilegedExecutor,
30
+ rootChecked,
31
+ rootOrDegrade
32
+ } from "./chunk-S3QBGGVP.js";
33
+ import {
34
+ EDGE_CHALLENGE_HOST_DIR,
35
+ EDGE_CONTAINER_MOUNTS,
36
+ EDGE_HOST_PATHS,
37
+ OPENRESTY_DEFAULT_PATHS
38
+ } from "./chunk-BQMFIXHI.js";
39
+ import {
40
+ waitForPortListening
41
+ } from "./chunk-WYNM7L22.js";
42
+ import {
43
+ EDGE_CONTAINER_NAME,
44
+ probeExec,
45
+ probeExecElevated,
46
+ tryExec,
47
+ withReason
48
+ } from "./chunk-N4OALTFK.js";
49
+ import {
50
+ envOps,
51
+ opScript
52
+ } from "./chunk-YRSZPVMB.js";
53
+ import {
54
+ resolveEnvironment
55
+ } from "./chunk-OFXBNFNL.js";
56
+ import {
57
+ formatDuration,
58
+ systemDebug
59
+ } from "./chunk-3CJLKL6E.js";
60
+ import {
61
+ sq
62
+ } from "./chunk-MNV2QFHW.js";
63
+ import {
64
+ AppError,
65
+ PROXY_DIRECTIVES,
66
+ answered,
67
+ buildEdgeImageRef,
68
+ compareSemver,
69
+ firewallActiveSteps,
70
+ firewallVersionSteps,
71
+ isLoopbackHost,
72
+ parseProxyValue,
73
+ safeErrorMessage,
74
+ sanitizeProxySettings,
75
+ shellQuote
76
+ } from "./chunk-PVOWHIND.js";
77
+
78
+ // ../../packages/adapters/src/system/managed-image.ts
79
+ import { posix } from "path";
80
+ function log(message, level = "info") {
81
+ return { timestamp: (/* @__PURE__ */ new Date()).toISOString(), message, level };
82
+ }
83
+ function titleCase(label) {
84
+ return label.charAt(0).toUpperCase() + label.slice(1);
85
+ }
86
+ var fromSourceManaged = {
87
+ edge: false,
88
+ mail: false
89
+ };
90
+ function managedImagesAreFromSource(kind) {
91
+ return fromSourceManaged[kind];
92
+ }
93
+ function fromSourceImageMissingMessage(label, image) {
94
+ return `The from-source ${label} image ${image} isn't on this server \u2014 the control-plane build or transfer didn't complete (see the log above). It's an unpublished dev tag, so there's nothing to pull; fix the build and retry.`;
95
+ }
96
+ async function dockerAvailable(executor) {
97
+ return executor.exec("docker version --format '{{.Server.Version}}' 2>/dev/null").then((v) => Boolean(v.trim())).catch(() => false);
98
+ }
99
+ async function containerState(executor, container) {
100
+ const out = await executor.exec(
101
+ `docker inspect -f '{{.State.Running}} {{.Config.Image}}' ${sq(container)} 2>/dev/null`
102
+ ).catch(() => "");
103
+ const line = out.split("\n").map((l) => l.trim()).find(Boolean);
104
+ if (!line) return null;
105
+ const [running, image] = line.split(" ");
106
+ return { running: running?.trim() === "true", image: image?.trim() || null };
107
+ }
108
+ async function containerImageRef(executor, container) {
109
+ return (await containerState(executor, container))?.image ?? null;
110
+ }
111
+ async function imageExistsLocally(executor, ref) {
112
+ const out = await executor.exec(`docker image inspect -f '{{.Id}}' ${sq(ref)} 2>/dev/null`).catch(() => "");
113
+ return Boolean(out.trim());
114
+ }
115
+ async function swapManagedImage(executor, opts) {
116
+ const { kind, from, to, label, onLog, start } = opts;
117
+ const Label = titleCase(label);
118
+ onLog(log(`Updating the ${label}: ${from} \u2192 ${to}`));
119
+ if (!await imageExistsLocally(executor, to)) {
120
+ if (managedImagesAreFromSource(kind)) {
121
+ onLog(log(fromSourceImageMissingMessage(label, to), "warn"));
122
+ return { swapped: false, down: false };
123
+ }
124
+ const pull = await executor.streamExec(
125
+ `docker pull ${sq(to)}`,
126
+ onLog
127
+ );
128
+ if (pull.code !== 0) {
129
+ onLog(
130
+ log(`Could not pull ${to} \u2014 the ${label} stays on ${from}.`, "warn")
131
+ );
132
+ return { swapped: false, down: false };
133
+ }
134
+ }
135
+ if (await start(to)) {
136
+ onLog(log(`${Label} updated to ${to}`));
137
+ return { swapped: true, down: false };
138
+ }
139
+ onLog(
140
+ log(
141
+ `${Label} ${to} failed to come up \u2014 rolling back to ${from}...`,
142
+ "error"
143
+ )
144
+ );
145
+ if (await start(from)) {
146
+ return { swapped: false, down: false };
147
+ }
148
+ onLog(
149
+ log(
150
+ `Rollback to ${from} ALSO failed \u2014 the ${label} is down on this server.`,
151
+ "error"
152
+ )
153
+ );
154
+ return { swapped: false, down: true };
155
+ }
156
+
157
+ // ../../packages/adapters/src/system/proxy/takeover-journal.ts
158
+ var JOURNAL_DIR = "/var/lib/kraft";
159
+ var JOURNAL_PATH = `${JOURNAL_DIR}/edge-takeover.json`;
160
+ function log2(message, level = "info") {
161
+ return { timestamp: (/* @__PURE__ */ new Date()).toISOString(), message, level };
162
+ }
163
+ async function buildJournal(executor, status, onLog) {
164
+ const units = /* @__PURE__ */ new Map();
165
+ const containers = /* @__PURE__ */ new Map();
166
+ const processes = /* @__PURE__ */ new Map();
167
+ for (const o of status.occupants) {
168
+ if (o.containerName && !containers.has(o.containerName)) {
169
+ const r = await tryExec(
170
+ executor,
171
+ `docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' ${sq2(o.containerName)} 2>/dev/null`
172
+ );
173
+ if (r === null) {
174
+ onLog?.(
175
+ log2(
176
+ `Could not read ${o.containerName}'s restart policy. If this takeover is rolled back it will be started again, but its policy left untouched \u2014 check it with \`docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' ${o.containerName}\`.`,
177
+ "warn"
178
+ )
179
+ );
180
+ containers.set(o.containerName, { name: o.containerName });
181
+ } else {
182
+ containers.set(o.containerName, {
183
+ name: o.containerName,
184
+ restart: r.trim() || "no"
185
+ });
186
+ }
187
+ } else if (o.systemdUnit && !units.has(o.systemdUnit)) {
188
+ const en = await tryExec(
189
+ executor,
190
+ `systemctl is-enabled ${sq2(o.systemdUnit)} 2>/dev/null`
191
+ );
192
+ units.set(o.systemdUnit, {
193
+ unit: o.systemdUnit,
194
+ wasEnabled: en?.trim() === "enabled"
195
+ });
196
+ } else if (!o.containerName && !o.systemdUnit && o.pid && !processes.has(o.pid)) {
197
+ processes.set(o.pid, { pid: o.pid, command: o.rawCommand });
198
+ }
199
+ }
200
+ return {
201
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
202
+ units: [...units.values()],
203
+ containers: [...containers.values()],
204
+ processes: [...processes.values()]
205
+ };
206
+ }
207
+ function journalFileExecutor(executor) {
208
+ return rootOrDegrade(executor, {
209
+ purpose: "Writing the edge-takeover rollback journal",
210
+ consequence: "An interrupted takeover won't be repaired automatically on the next boot.",
211
+ report: () => {
212
+ }
213
+ });
214
+ }
215
+ async function writeJournal(executor, journal) {
216
+ try {
217
+ const privileged = await journalFileExecutor(executor);
218
+ await privileged.mkdir(JOURNAL_DIR);
219
+ await privileged.writeFile(JOURNAL_PATH, JSON.stringify(journal, null, 2));
220
+ } catch {
221
+ }
222
+ }
223
+ async function clearJournal(executor) {
224
+ await tryExec(await journalFileExecutor(executor), `rm -f ${JOURNAL_PATH}`);
225
+ }
226
+ async function readJournal(executor, onLog) {
227
+ const privileged = await journalFileExecutor(executor);
228
+ const raw = await tryExec(privileged, `cat ${JOURNAL_PATH} 2>/dev/null`);
229
+ if (raw === null) {
230
+ const listed = await tryExec(privileged, `ls ${JOURNAL_PATH} 2>/dev/null`);
231
+ if (listed?.trim()) {
232
+ onLog?.(
233
+ log2(
234
+ `An unfinished edge takeover is recorded at ${JOURNAL_PATH}, but Kraft could not read it as this user \u2014 the previous proxy will NOT be restored automatically. Re-run as root, or restore it by hand.`,
235
+ "error"
236
+ )
237
+ );
238
+ }
239
+ return null;
240
+ }
241
+ if (!raw.trim()) {
242
+ await clearJournal(executor);
243
+ return null;
244
+ }
245
+ try {
246
+ return JSON.parse(raw);
247
+ } catch {
248
+ await clearJournal(executor);
249
+ return null;
250
+ }
251
+ }
252
+ async function rollback(executor, journal, onLog) {
253
+ onLog(log2("Rolling back \u2014 restoring the previous proxy...", "warn"));
254
+ const restore = await rootOrDegrade(executor, {
255
+ purpose: "Restarting the proxy that was holding ports 80/443",
256
+ consequence: "Restarting and re-enabling it will likely be refused \u2014 so if :80 is served after this, it is because nothing was ever stopped, not because Kraft put it back.",
257
+ report: (message) => onLog(log2(message, "warn"))
258
+ });
259
+ await tryExec(
260
+ restore,
261
+ `docker update --restart=no ${sq2(EDGE_CONTAINER_NAME)} 2>/dev/null || true`
262
+ );
263
+ await tryExec(
264
+ restore,
265
+ `docker stop ${sq2(EDGE_CONTAINER_NAME)} 2>/dev/null || true`
266
+ );
267
+ await tryExec(
268
+ restore,
269
+ "systemctl disable --now openresty 2>/dev/null || systemctl stop openresty 2>/dev/null || true; systemctl reset-failed openresty 2>/dev/null || true"
270
+ );
271
+ for (const u of journal.units) {
272
+ await tryExec(
273
+ restore,
274
+ u.wasEnabled ? `systemctl enable --now ${sq2(u.unit)} 2>/dev/null || true` : `systemctl start ${sq2(u.unit)} 2>/dev/null || true`
275
+ );
276
+ }
277
+ for (const c of journal.containers) {
278
+ if (c.restart) {
279
+ await tryExec(
280
+ restore,
281
+ `docker update --restart=${sq2(c.restart)} ${sq2(c.name)} 2>/dev/null || true`
282
+ );
283
+ } else {
284
+ onLog(
285
+ log2(
286
+ `Starting ${c.name} without setting a restart policy \u2014 Kraft never managed to read the one it had. Confirm it still comes back on reboot.`,
287
+ "warn"
288
+ )
289
+ );
290
+ }
291
+ await tryExec(restore, `docker start ${sq2(c.name)} 2>/dev/null || true`);
292
+ }
293
+ for (const p of journal.processes ?? []) {
294
+ if (p.command) {
295
+ await tryExec(
296
+ restore,
297
+ `setsid -f sh -c ${sq2(p.command)} 2>/dev/null || (nohup sh -c ${sq2(p.command)} >/dev/null 2>&1 &) || true`
298
+ );
299
+ } else {
300
+ onLog(
301
+ log2(
302
+ `Could not restore process ${p.pid} \u2014 no command captured.`,
303
+ "warn"
304
+ )
305
+ );
306
+ }
307
+ }
308
+ const restored = await portIsServed(executor, 80);
309
+ if (!restored) {
310
+ onLog(
311
+ log2(
312
+ "Nothing is listening on :80 after the restore \u2014 the box is NOT serving. Start your proxy by hand (e.g. `systemctl start nginx`, or `docker start <name>`).",
313
+ "error"
314
+ )
315
+ );
316
+ }
317
+ return restored;
318
+ }
319
+ async function portIsServed(executor, port) {
320
+ const hex = port.toString(16).toUpperCase().padStart(4, "0");
321
+ const out = await tryExec(
322
+ executor,
323
+ `(command -v ss >/dev/null 2>&1 && ss -ltn 2>/dev/null | grep -qE ':${port}[[:space:]]' && echo yes) || (grep -qiE '^[[:space:]]*[0-9]+:[[:space:]]*[0-9A-F]{8}:${hex}[[:space:]]+[0-9A-F]{8}:0000[[:space:]]+0A' /proc/net/tcp 2>/dev/null && echo yes) || true`
324
+ );
325
+ return (out ?? "").includes("yes");
326
+ }
327
+ var EDGE_STOP_ELEVATION = {
328
+ purpose: "Stopping the proxy that holds ports 80/443",
329
+ consequence: "Stopping and disabling it will likely be refused, leaving the ports bound."
330
+ };
331
+ async function beginEdgeTakeover(executor, status, onLog, targets) {
332
+ await writeJournal(executor, await buildJournal(executor, status, onLog));
333
+ let privilegeDegraded = false;
334
+ const stopExecutor = await rootOrDegrade(executor, {
335
+ ...EDGE_STOP_ELEVATION,
336
+ report: (message) => {
337
+ privilegeDegraded = true;
338
+ onLog(log2(message, "warn"));
339
+ }
340
+ });
341
+ const freed = await freeEdgeTargets(
342
+ stopExecutor,
343
+ targets?.length ? targets : stopTargetsForStatus(status),
344
+ (m, l) => onLog(log2(m, l))
345
+ );
346
+ return { ...freed, privilegeDegraded };
347
+ }
348
+ async function rollbackEdgeTakeover(executor, onLog) {
349
+ const journal = await readJournal(executor, onLog);
350
+ if (!journal || journal.completed) return false;
351
+ const restored = await rollback(executor, journal, onLog);
352
+ await clearJournal(executor);
353
+ return restored;
354
+ }
355
+ async function completeEdgeTakeover(executor) {
356
+ const journal = await readJournal(executor);
357
+ if (journal) {
358
+ journal.completed = true;
359
+ await writeJournal(executor, journal);
360
+ }
361
+ await clearJournal(executor);
362
+ }
363
+ async function recoverInterruptedTakeover(executor, onLog, isEdgeHealthy) {
364
+ const journal = await readJournal(executor, onLog);
365
+ if (!journal) return;
366
+ if (journal.completed) {
367
+ await clearJournal(executor);
368
+ return;
369
+ }
370
+ if (isEdgeHealthy && await isEdgeHealthy()) {
371
+ onLog(
372
+ log2(
373
+ "Found an unfinished edge takeover, but our edge is serving \u2014 keeping it.",
374
+ "warn"
375
+ )
376
+ );
377
+ await clearJournal(executor);
378
+ return;
379
+ }
380
+ onLog(
381
+ log2(
382
+ "Found an interrupted edge takeover \u2014 restoring the previous proxy.",
383
+ "warn"
384
+ )
385
+ );
386
+ await rollback(executor, journal, onLog);
387
+ await clearJournal(executor);
388
+ }
389
+
390
+ // ../../packages/adapters/src/system/proxy/import/parse-utils.ts
391
+ function stripComments(text) {
392
+ return text.split("\n").map((line) => {
393
+ let quote;
394
+ for (let i = 0; i < line.length; i++) {
395
+ const ch = line[i];
396
+ if (quote) {
397
+ if (ch === "\\")
398
+ i++;
399
+ else if (ch === quote) quote = void 0;
400
+ } else if (ch === '"' || ch === "'") {
401
+ quote = ch;
402
+ } else if (ch === "#") {
403
+ return line.slice(0, i);
404
+ }
405
+ }
406
+ return line;
407
+ }).join("\n");
408
+ }
409
+ function extractBlocks(text, keyword) {
410
+ const blocks = [];
411
+ const re = new RegExp(`(?:^|[\\s;}])${keyword}\\b[^{;]*\\{`, "g");
412
+ let m;
413
+ while ((m = re.exec(text)) !== null) {
414
+ const openIdx = m.index + m[0].length - 1;
415
+ let depth = 1;
416
+ let i = openIdx + 1;
417
+ for (; i < text.length && depth > 0; i++) {
418
+ if (text[i] === "{") depth++;
419
+ else if (text[i] === "}") depth--;
420
+ }
421
+ if (depth === 0) {
422
+ blocks.push(text.slice(openIdx + 1, i - 1));
423
+ re.lastIndex = i;
424
+ } else {
425
+ break;
426
+ }
427
+ }
428
+ return blocks;
429
+ }
430
+ function collapseByHost(items, hostsOf, score) {
431
+ const groups = /* @__PURE__ */ new Map();
432
+ for (const item of items) {
433
+ const key = [...hostsOf(item)].sort().join(" ");
434
+ const bag = groups.get(key);
435
+ if (bag) bag.push(item);
436
+ else groups.set(key, [item]);
437
+ }
438
+ const kept = [];
439
+ const dropped = [];
440
+ for (const bag of groups.values()) {
441
+ let winner = bag[0];
442
+ for (const cand of bag) if (score(cand) > score(winner)) winner = cand;
443
+ kept.push(winner);
444
+ for (const loser of bag) if (loser !== winner) dropped.push(loser);
445
+ }
446
+ return { kept, dropped };
447
+ }
448
+
449
+ // ../../packages/adapters/src/system/proxy/import/nginx.ts
450
+ async function dumpResolvedConfig(executor, bins) {
451
+ for (const bin of bins) {
452
+ const dumped = await tryExec(executor, `${bin} -T 2>/dev/null`);
453
+ if (dumped && /server\s*\{/.test(dumped)) return dumped;
454
+ }
455
+ return null;
456
+ }
457
+ async function nginxPrefix(executor, bins) {
458
+ for (const bin of bins) {
459
+ const out = await tryExec(executor, `${bin} -V 2>&1`);
460
+ const prefix = out?.match(/--prefix=(\S+)/)?.[1];
461
+ if (prefix) return prefix;
462
+ }
463
+ return null;
464
+ }
465
+ async function loadNginxConfig(executor) {
466
+ const dumped = await dumpResolvedConfig(executor, ["nginx", "openresty"]);
467
+ if (dumped) return dumped;
468
+ const cat = await tryExec(
469
+ executor,
470
+ "cat /etc/nginx/nginx.conf /etc/nginx/sites-enabled/* /etc/nginx/conf.d/*.conf 2>/dev/null"
471
+ );
472
+ return cat ?? "";
473
+ }
474
+ function firstDirective(body, name) {
475
+ const m = body.match(new RegExp(`(?:^|[;{\\s])${name}\\s+([^;]+);`));
476
+ return m?.[1]?.trim();
477
+ }
478
+ function parseUpstreams(config) {
479
+ const map = /* @__PURE__ */ new Map();
480
+ const re = /(?:^|[\s;}])upstream\s+(\S+)\s*\{([^}]*)\}/g;
481
+ let m;
482
+ while ((m = re.exec(config)) !== null) {
483
+ const name = m[1];
484
+ const body = m[2] ?? "";
485
+ const targets = [...body.matchAll(/(?:^|[\s;{])server\s+([^;\s]+)/g)].map((server) => server[1]?.trim()).filter((target) => Boolean(target));
486
+ if (name && targets.length > 0) map.set(name, targets);
487
+ }
488
+ return map;
489
+ }
490
+ var HTTP_PROXY_TARGET_RE = /^(https?:\/\/)([^/?#]+)([/?#].*)?$/i;
491
+ function resolveProxyTarget(proxyPass, upstreams, opts = {}) {
492
+ const raw = proxyPass.replace(/;$/, "").trim();
493
+ const parsed = raw.match(HTTP_PROXY_TARGET_RE);
494
+ if (raw.includes("$")) {
495
+ const authority2 = parsed?.[2];
496
+ if (!opts.allowVariablesAfterAuthority || !authority2 || authority2.includes("$")) {
497
+ return { reason: `proxy_pass "${raw}" uses an nginx variable` };
498
+ }
499
+ }
500
+ if (/\/\/unix:/i.test(raw))
501
+ return { reason: `proxy_pass "${raw}" targets a unix socket` };
502
+ const m = parsed;
503
+ if (!m) return { reason: `unrecognized proxy_pass "${raw}"` };
504
+ const scheme = m[1];
505
+ const authority = m[2];
506
+ const host = authority.replace(/:\d+$/, "");
507
+ const declaredUpstream = upstreams.get(authority);
508
+ if (declaredUpstream?.[0]) return { url: `${scheme}${declaredUpstream[0]}` };
509
+ const isIp = /^\d{1,3}(\.\d{1,3}){3}$/.test(host);
510
+ if (host === "localhost" || host === "ip6-localhost") {
511
+ return { url: raw.replace(/^(https?:\/\/)[^/:]+/i, "$1127.0.0.1") };
512
+ }
513
+ if (isIp || host.includes(".") || host.startsWith("[") && host.endsWith("]")) {
514
+ return { url: raw };
515
+ }
516
+ return {
517
+ reason: `proxy_pass host "${host}" is an undeclared upstream \u2014 not migratable`
518
+ };
519
+ }
520
+ function strictLoopbackUpstreamPorts(config) {
521
+ const normalized = stripComments(config);
522
+ const upstreams = parseUpstreams(normalized);
523
+ const ports = /* @__PURE__ */ new Set();
524
+ for (const match of normalized.matchAll(
525
+ /(?:^|[;{}\s])proxy_pass\s+([^;]+);/g
526
+ )) {
527
+ const raw = match[1]?.trim();
528
+ if (!raw) continue;
529
+ const resolved = resolveProxyTarget(raw, upstreams, {
530
+ allowVariablesAfterAuthority: true
531
+ });
532
+ if ("reason" in resolved) {
533
+ if (/\/\/unix:/i.test(raw)) continue;
534
+ throw new Error(`Cannot inventory Kraft edge routes: ${resolved.reason}`);
535
+ }
536
+ const parsed = raw.match(HTTP_PROXY_TARGET_RE);
537
+ const authority = parsed?.[2];
538
+ const scheme = parsed?.[1];
539
+ const targets = authority && scheme && upstreams.has(authority) ? upstreams.get(authority).map((target) => `${scheme}${target}`) : [resolved.url];
540
+ for (const target of targets) {
541
+ let url;
542
+ try {
543
+ url = new URL(target);
544
+ } catch {
545
+ throw new Error(
546
+ `Cannot inventory Kraft edge routes: invalid proxy_pass "${raw}"`
547
+ );
548
+ }
549
+ if (!isLoopbackHost(url.hostname)) continue;
550
+ const port = url.port ? Number(url.port) : url.protocol === "https:" ? 443 : 80;
551
+ if (!Number.isSafeInteger(port) || port < 1 || port > 65535) {
552
+ throw new Error(
553
+ `Cannot inventory Kraft edge routes: invalid loopback port in "${raw}"`
554
+ );
555
+ }
556
+ ports.add(port);
557
+ }
558
+ }
559
+ return ports;
560
+ }
561
+ function parseStrictEdgeConfig(raw) {
562
+ return {
563
+ ...parseNginxConfig(raw),
564
+ loopbackUpstreamPorts: strictLoopbackUpstreamPorts(raw)
565
+ };
566
+ }
567
+ function locationBlocks(serverBody) {
568
+ const out = [];
569
+ const re = /(?:^|[\s;}])location\s+([^{]+?)\s*\{/g;
570
+ let m;
571
+ while ((m = re.exec(serverBody)) !== null) {
572
+ const path = m[1].trim();
573
+ const openIdx = m.index + m[0].length - 1;
574
+ let depth = 1;
575
+ let i = openIdx + 1;
576
+ for (; i < serverBody.length && depth > 0; i++) {
577
+ if (serverBody[i] === "{") depth++;
578
+ else if (serverBody[i] === "}") depth--;
579
+ }
580
+ if (depth !== 0) break;
581
+ out.push({ path, body: serverBody.slice(openIdx + 1, i - 1) });
582
+ re.lastIndex = i;
583
+ }
584
+ return out;
585
+ }
586
+ function extractLocationProxies(serverBody) {
587
+ const routes = [];
588
+ const unsupported = [];
589
+ let sawProxyLocation = false;
590
+ for (const loc of locationBlocks(serverBody)) {
591
+ const pp = firstDirective(loc.body, "proxy_pass");
592
+ if (!pp) continue;
593
+ sawProxyLocation = true;
594
+ const exact = loc.path.match(/^=\s+(.+)$/);
595
+ if (exact) {
596
+ routes.push({ path: exact[1].trim(), proxyPass: pp, exact: true });
597
+ continue;
598
+ }
599
+ const strongPrefix = loc.path.match(/^\^~\s+(.+)$/);
600
+ if (strongPrefix) {
601
+ routes.push({ path: strongPrefix[1].trim(), proxyPass: pp });
602
+ continue;
603
+ }
604
+ if (/^~\*?(?:\s|$)/.test(loc.path) || loc.path.startsWith("@")) {
605
+ unsupported.push(loc.path);
606
+ continue;
607
+ }
608
+ routes.push({ path: loc.path, proxyPass: pp });
609
+ }
610
+ return { routes, unsupported, sawProxyLocation };
611
+ }
612
+ function withoutQuotedValues(body) {
613
+ return body.replace(/'[^'\n]*'/g, "''").replace(/"[^"\n]*"/g, '""');
614
+ }
615
+ var SERVING_DIRECTIVE_RE = /(?:^|[;{}\s])(?:proxy_pass|root|alias|fastcgi_pass|uwsgi_pass|scgi_pass|grpc_pass)\s/;
616
+ function servesNothing(locationBody) {
617
+ return !SERVING_DIRECTIVE_RE.test(withoutQuotedValues(locationBody));
618
+ }
619
+ function isHttpsUpgradeForSelf(body, names) {
620
+ const targets = [
621
+ ...body.matchAll(/(?:^|[\s;{])return\s+30[1-8]\s+([^;]+);/g),
622
+ ...body.matchAll(/(?:^|[\s;{])rewrite\s+\S+\s+([^;]+?)\s+permanent\s*;/g)
623
+ ].map((m) => m[1].trim());
624
+ if (targets.length === 0) return false;
625
+ return targets.every((raw) => {
626
+ if (!/^https:\/\//i.test(raw)) return false;
627
+ const authority = raw.replace(/^https:\/\//i, "").split(/[/\s]/)[0] ?? "";
628
+ const host = (authority.match(/^(\$[a-z_]+|[^$:]+)/i)?.[1] ?? "").replace(
629
+ /:\d+$/,
630
+ ""
631
+ );
632
+ if (host === "$host" || host === "$server_name" || host === "$http_host")
633
+ return true;
634
+ return names.includes(host);
635
+ });
636
+ }
637
+ var CHALLENGE_LOCATION_RE = /\.well-known\/(acme-challenge|oblien-proxy-challenge)/i;
638
+ function isAcmeWebrootOnly(body) {
639
+ const locations = locationBlocks(body);
640
+ if (locations.length === 0) {
641
+ const root = firstDirective(body, "root") ?? "";
642
+ return /letsencrypt|acme|certbot/i.test(root);
643
+ }
644
+ if (!locations.some((l) => CHALLENGE_LOCATION_RE.test(l.path))) return false;
645
+ return locations.every(
646
+ (l) => CHALLENGE_LOCATION_RE.test(l.path) || servesNothing(l.body)
647
+ );
648
+ }
649
+ function parseProxyDirectives(body) {
650
+ const raw = {};
651
+ const candidate = {};
652
+ for (const spec of PROXY_DIRECTIVES) {
653
+ const found = firstDirective(body, spec.directive);
654
+ if (found === void 0) continue;
655
+ raw[spec.key] = found;
656
+ const parsed = parseProxyValue(spec, found);
657
+ if (parsed !== void 0) candidate[spec.key] = parsed;
658
+ }
659
+ const settings = sanitizeProxySettings(candidate);
660
+ return {
661
+ ...settings ? { settings } : {},
662
+ ...Object.keys(raw).length > 0 ? { raw } : {}
663
+ };
664
+ }
665
+ var LOOPBACK_SERVER_NAMES = /* @__PURE__ */ new Set([
666
+ "localhost",
667
+ "localhost.localdomain",
668
+ "ip6-localhost",
669
+ "ip6-loopback"
670
+ ]);
671
+ function resolveStaticRoot(root, prefix) {
672
+ if (root.startsWith("/")) return { root };
673
+ if (!prefix) {
674
+ return {
675
+ reason: `static root "${root}" is relative to nginx's compiled prefix, which this host didn't report`
676
+ };
677
+ }
678
+ return { root: `${prefix.replace(/\/+$/, "")}/${root}` };
679
+ }
680
+ function parseServer(body, source, upstreams, prefix) {
681
+ const warnings = [];
682
+ const names = firstDirective(body, "server_name")?.split(/\s+/).filter(
683
+ (n) => n && n !== "_" && !n.startsWith("~") && !LOOPBACK_SERVER_NAMES.has(n.toLowerCase())
684
+ ) ?? [];
685
+ const listens = [...body.matchAll(/(?:^|[;{\s])listen\s+([^;]+);/g)].map(
686
+ (m) => m[1]
687
+ );
688
+ const ssl = listens.some((l) => /\bssl\b/.test(l) || /\b443\b/.test(l));
689
+ const root = firstDirective(body, "root");
690
+ const certPath = firstDirective(body, "ssl_certificate");
691
+ const keyPath = firstDirective(body, "ssl_certificate_key");
692
+ if (names.length === 0) return { warnings: [] };
693
+ const extracted = extractLocationProxies(body);
694
+ const rawTargets = extracted.routes;
695
+ for (const selector of extracted.unsupported) {
696
+ warnings.push(
697
+ `nginx: ${names[0]} location "${selector}" uses an unsupported regex or named selector (skipped)`
698
+ );
699
+ }
700
+ if (rawTargets.length === 0 && !extracted.sawProxyLocation) {
701
+ const serverLevel = firstDirective(body, "proxy_pass");
702
+ if (serverLevel) rawTargets.push({ path: "/", proxyPass: serverLevel });
703
+ }
704
+ const resolved = [];
705
+ for (const t of rawTargets) {
706
+ const r = resolveProxyTarget(t.proxyPass, upstreams);
707
+ if ("reason" in r)
708
+ warnings.push(`nginx: ${names[0]} ${t.path} \u2014 ${r.reason} (skipped)`);
709
+ else
710
+ resolved.push({
711
+ path: t.path,
712
+ url: r.url,
713
+ ...t.exact ? { exact: true } : {}
714
+ });
715
+ }
716
+ let target;
717
+ let routes;
718
+ if (resolved.length > 0) {
719
+ const primary = resolved.find((r) => r.path === "/" && !r.exact) ?? resolved[0];
720
+ target = { kind: "proxy", url: primary.url };
721
+ routes = resolved;
722
+ } else if (isHttpsUpgradeForSelf(body, names)) {
723
+ return { warnings: [] };
724
+ } else if (root && !isAcmeWebrootOnly(body)) {
725
+ const resolved2 = resolveStaticRoot(root.replace(/;$/, ""), prefix);
726
+ if ("reason" in resolved2) {
727
+ warnings.push(`nginx: ${names[0]} \u2014 ${resolved2.reason} (skipped)`);
728
+ return { warnings };
729
+ }
730
+ target = { kind: "static", root: resolved2.root };
731
+ } else if (root) {
732
+ return { warnings: [] };
733
+ } else {
734
+ warnings.push(
735
+ `nginx: ${names[0]} has neither proxy_pass nor root \u2014 skipped (${source})`
736
+ );
737
+ return { warnings };
738
+ }
739
+ const site = { serverNames: names, ssl, target, source };
740
+ if (routes) site.routes = routes;
741
+ if (certPath && keyPath) site.tls = { certPath, keyPath };
742
+ const tunables = parseProxyDirectives(body);
743
+ if (tunables.settings) site.proxy = tunables.settings;
744
+ if (tunables.raw) site.proxyRaw = tunables.raw;
745
+ return { site, warnings };
746
+ }
747
+ function parseNginxConfig(raw, prefix) {
748
+ const warnings = [];
749
+ const sites = [];
750
+ if (!raw.trim()) {
751
+ return {
752
+ proxy: "nginx",
753
+ sites,
754
+ warnings: ["nginx: no readable configuration found"]
755
+ };
756
+ }
757
+ const config = stripComments(raw);
758
+ const upstreams = parseUpstreams(config);
759
+ const blocks = extractBlocks(config, "server");
760
+ if (blocks.length === 0) {
761
+ warnings.push("nginx: no server blocks found");
762
+ }
763
+ for (const body of blocks) {
764
+ const { site, warnings: blockWarnings } = parseServer(
765
+ body,
766
+ "nginx",
767
+ upstreams,
768
+ prefix
769
+ );
770
+ warnings.push(...blockWarnings);
771
+ if (site) sites.push(site);
772
+ }
773
+ const { kept } = collapseByHost(
774
+ sites,
775
+ (s) => s.serverNames,
776
+ (s) => (s.ssl ? 2 : 0) + (s.target.kind === "proxy" ? 1 : 0)
777
+ );
778
+ return { proxy: "nginx", sites: kept, warnings };
779
+ }
780
+ async function scanNginx(executor) {
781
+ const raw = await loadNginxConfig(executor);
782
+ const relativeRoot = /(?:^|[;{\s])root\s+[^/;\s]/.test(raw);
783
+ const prefix = relativeRoot ? await nginxPrefix(executor, ["nginx", "openresty"]) : null;
784
+ return parseNginxConfig(raw, prefix ?? void 0);
785
+ }
786
+ var OUR_EDGE_SITE_GLOBS = [
787
+ `${EDGE_HOST_PATHS.sitesDir}/*.conf`,
788
+ `${OPENRESTY_DEFAULT_PATHS.sitesDir}/*.conf`,
789
+ "/etc/openresty/sites-enabled/*.conf"
790
+ ];
791
+ async function scanForeignOpenResty(executor) {
792
+ const dump = await dumpResolvedConfig(executor, ["openresty"]);
793
+ if (dump) return parseNginxConfig(dump);
794
+ return scanKraftEdge(executor);
795
+ }
796
+ async function scanKraftEdge(executor) {
797
+ const cat = `cat ${OUR_EDGE_SITE_GLOBS.join(" ")} 2>/dev/null`;
798
+ const raw = await tryExec(executor, cat);
799
+ if (raw?.trim()) return parseNginxConfig(raw);
800
+ const container = await resolveOurEdgeContainer(executor);
801
+ if (!container) return parseNginxConfig("");
802
+ const fromContainer = await tryExec(
803
+ executor,
804
+ containerCommand(container, cat)
805
+ );
806
+ return parseNginxConfig(fromContainer ?? "");
807
+ }
808
+ async function scanKraftEdgeStrict(executor, knownContainer) {
809
+ const detected = await detectEdgeContainer(executor);
810
+ if (!detected) {
811
+ const bareConfig = await dumpResolvedConfig(executor, ["openresty"]);
812
+ if (bareConfig) return parseStrictEdgeConfig(bareConfig);
813
+ throw new Error(
814
+ "Cannot read Kraft edge routes: neither Docker nor bare OpenResty inventory is available"
815
+ );
816
+ }
817
+ if (!detected.exists) {
818
+ const bareConfig = await dumpResolvedConfig(executor, ["openresty"]);
819
+ if (bareConfig) return parseStrictEdgeConfig(bareConfig);
820
+ throw new Error(
821
+ "Cannot read Kraft edge routes: no running Kraft edge or bare OpenResty inventory is available"
822
+ );
823
+ }
824
+ if (!detected.running) {
825
+ throw new Error(
826
+ "Cannot read Kraft edge routes: the edge container is not running"
827
+ );
828
+ }
829
+ const container = knownContainer ?? detected.name;
830
+ if (!container)
831
+ throw new Error(
832
+ "Cannot read Kraft edge routes: container identity missing"
833
+ );
834
+ const fromContainer = await executor.exec(
835
+ containerCommand(container, "openresty -T 2>/dev/null")
836
+ );
837
+ if (!fromContainer.trim() || !/server\s*\{/.test(fromContainer)) {
838
+ throw new Error(
839
+ "Cannot read Kraft edge routes: active OpenResty config dump was empty"
840
+ );
841
+ }
842
+ return parseStrictEdgeConfig(fromContainer);
843
+ }
844
+
845
+ // ../../packages/adapters/src/system/proxy/import/caddy.ts
846
+ var CADDYFILE_PATHS = ["/etc/caddy/Caddyfile", "/etc/Caddyfile"];
847
+ async function loadCaddyAdminConfig(executor) {
848
+ const endpoint = "http://localhost:2019/config/";
849
+ const out = await tryExec(executor, `curl -fsS ${endpoint} 2>/dev/null`) || await tryExec(executor, `wget -qO- ${endpoint} 2>/dev/null`);
850
+ return out && out.trim().startsWith("{") ? out : null;
851
+ }
852
+ async function loadCaddyJson(executor) {
853
+ for (const p of CADDYFILE_PATHS) {
854
+ const out = await tryExec(
855
+ executor,
856
+ `caddy adapt --adapter caddyfile --config ${p} 2>/dev/null`
857
+ );
858
+ if (out && out.trim().startsWith("{")) return out;
859
+ }
860
+ return null;
861
+ }
862
+ function flattenHandlers(handlers, scoped = false, out = []) {
863
+ for (const h of handlers ?? []) {
864
+ if (h.handler === "subroute") {
865
+ for (const r of h.routes ?? []) {
866
+ flattenHandlers(r.handle, scoped || (r.match?.length ?? 0) > 0, out);
867
+ }
868
+ } else {
869
+ out.push({ h, scoped });
870
+ }
871
+ }
872
+ return out;
873
+ }
874
+ var UNMIGRATABLE_HANDLERS = {
875
+ php_fastcgi: "a PHP-FPM site (php_fastcgi)",
876
+ fastcgi: "a FastCGI backend",
877
+ static_response: "a static inline response (respond)",
878
+ templates: "Caddy templates",
879
+ encode: "",
880
+ headers: "",
881
+ authentication: "Caddy authentication",
882
+ rewrite: "",
883
+ acme_server: "an embedded ACME server"
884
+ };
885
+ function targetFromRoute(route) {
886
+ const flat = flattenHandlers(route.handle);
887
+ const proxies = flat.filter((f) => f.h.handler === "reverse_proxy");
888
+ const chosen = proxies.find((f) => !f.scoped) ?? proxies[0];
889
+ const proxy = chosen?.h;
890
+ if (proxy?.transport?.protocol === "fastcgi") {
891
+ return {
892
+ note: "serves a PHP-FPM/FastCGI site (php_fastcgi) \u2014 not an HTTP upstream"
893
+ };
894
+ }
895
+ const pathScopedOnly = Boolean(chosen?.scoped);
896
+ const hasScopedSibling = proxies.some((f) => f.scoped);
897
+ const dials = (proxy?.upstreams ?? []).map((u) => u.dial).filter((d) => Boolean(d));
898
+ if (dials.length > 0) {
899
+ const dial = dials[0];
900
+ if (/^unix\//i.test(dial)) {
901
+ return {
902
+ note: `proxies to the unix socket "${dial}", which needs a manual upstream`
903
+ };
904
+ }
905
+ return {
906
+ target: {
907
+ kind: "proxy",
908
+ url: /^https?:\/\//.test(dial) ? dial : `http://${dial}`
909
+ },
910
+ extraUpstreams: dials.length - 1,
911
+ pathScopedOnly,
912
+ hasScopedSibling
913
+ };
914
+ }
915
+ const root = flat.find(
916
+ (f) => f.h.handler === "vars" && typeof f.h.root === "string"
917
+ )?.h.root;
918
+ if (root && flat.some((f) => f.h.handler === "file_server")) {
919
+ return { target: { kind: "static", root } };
920
+ }
921
+ for (const { h } of flat) {
922
+ const label = h.handler ? UNMIGRATABLE_HANDLERS[h.handler] : void 0;
923
+ if (label) return { note: `serves ${label}` };
924
+ }
925
+ return {};
926
+ }
927
+ function parseCaddyJson(raw) {
928
+ let cfg;
929
+ try {
930
+ cfg = JSON.parse(raw);
931
+ } catch {
932
+ return null;
933
+ }
934
+ const servers = cfg.apps?.http?.servers;
935
+ if (!servers) return null;
936
+ const sites = [];
937
+ const warnings = [];
938
+ for (const srv of Object.values(servers)) {
939
+ const ssl = (srv.listen ?? []).some((l) => l.endsWith(":443"));
940
+ for (const route of srv.routes ?? []) {
941
+ const hosts = (route.match ?? []).flatMap((m) => m.host ?? []).filter((h) => h && h !== "*");
942
+ if (hosts.length === 0) continue;
943
+ const outcome = targetFromRoute(route);
944
+ if (outcome.note) {
945
+ warnings.push(
946
+ `caddy: ${hosts.join(", ")} ${outcome.note} \u2014 re-add it manually`
947
+ );
948
+ continue;
949
+ }
950
+ if (!outcome.target) continue;
951
+ if (outcome.extraUpstreams) {
952
+ warnings.push(
953
+ `caddy: ${hosts.join(", ")} load-balances across ${outcome.extraUpstreams + 1} upstreams \u2014 only the first is migrated`
954
+ );
955
+ }
956
+ if (outcome.hasScopedSibling || (route.match ?? []).some((m) => m.path?.length)) {
957
+ warnings.push(
958
+ `caddy: ${hosts.join(", ")} routes some paths to other upstreams \u2014 only the catch-all is migrated; re-add path rules manually`
959
+ );
960
+ }
961
+ if (outcome.pathScopedOnly) {
962
+ warnings.push(
963
+ `caddy: ${hosts.join(", ")} has no catch-all upstream \u2014 used its path-scoped one; verify the target`
964
+ );
965
+ }
966
+ sites.push({
967
+ serverNames: hosts,
968
+ ssl,
969
+ target: outcome.target,
970
+ source: "caddy (adapt)"
971
+ });
972
+ }
973
+ }
974
+ const { kept, dropped } = collapseByHost(
975
+ sites,
976
+ (s) => s.serverNames,
977
+ (s) => s.ssl ? 1 : 0
978
+ );
979
+ for (const d of dropped) {
980
+ warnings.push(
981
+ `caddy: ${d.serverNames.join(", ")} had a second (non-TLS) block \u2014 kept the TLS one`
982
+ );
983
+ }
984
+ return { sites: kept, warnings };
985
+ }
986
+ async function loadCaddyfile(executor) {
987
+ for (const p of CADDYFILE_PATHS) {
988
+ const out = await tryExec(executor, `cat ${p} 2>/dev/null`);
989
+ if (out && out.trim()) return out;
990
+ }
991
+ return "";
992
+ }
993
+ function caddyBlocks(text) {
994
+ const out = [];
995
+ let i = 0;
996
+ let headerStart = 0;
997
+ while (i < text.length) {
998
+ if (text[i] === "{") {
999
+ const header = text.slice(headerStart, i).trim();
1000
+ let depth = 1;
1001
+ let j = i + 1;
1002
+ for (; j < text.length && depth > 0; j++) {
1003
+ if (text[j] === "{") depth++;
1004
+ else if (text[j] === "}") depth--;
1005
+ }
1006
+ if (depth !== 0) break;
1007
+ out.push({ header, body: text.slice(i + 1, j - 1) });
1008
+ i = j;
1009
+ headerStart = j;
1010
+ } else {
1011
+ i++;
1012
+ }
1013
+ }
1014
+ return out;
1015
+ }
1016
+ function parseAddresses(header) {
1017
+ return header.split(/[\s,]+/).filter(Boolean).map((addr) => {
1018
+ const httpScheme = /^http:\/\//i.test(addr);
1019
+ const host = addr.replace(/^https?:\/\//i, "").replace(/:\d+$/, "");
1020
+ const isPort80 = /:80$/.test(addr);
1021
+ return { host, ssl: !httpScheme && !isPort80 };
1022
+ }).filter((a) => a.host && !a.host.startsWith(":") && a.host !== "*");
1023
+ }
1024
+ async function scanCaddy(executor) {
1025
+ for (const load of [loadCaddyAdminConfig, loadCaddyJson]) {
1026
+ const json = await load(executor);
1027
+ if (!json) continue;
1028
+ const parsed = parseCaddyJson(json);
1029
+ if (parsed && (parsed.sites.length > 0 || parsed.warnings.length > 0)) {
1030
+ return { proxy: "caddy", sites: parsed.sites, warnings: parsed.warnings };
1031
+ }
1032
+ }
1033
+ return scanCaddyText(executor);
1034
+ }
1035
+ async function scanCaddyText(executor) {
1036
+ const raw = await loadCaddyfile(executor);
1037
+ const warnings = [];
1038
+ const sites = [];
1039
+ if (!raw.trim()) {
1040
+ return {
1041
+ proxy: "caddy",
1042
+ sites,
1043
+ warnings: ["caddy: no readable Caddyfile found"]
1044
+ };
1045
+ }
1046
+ const text = stripComments(raw);
1047
+ let blocks = caddyBlocks(text);
1048
+ if (blocks.length === 0) {
1049
+ const lines = text.split("\n").map((l) => l.trim()).filter(Boolean);
1050
+ if (lines.length > 0) {
1051
+ blocks = [{ header: lines[0], body: lines.slice(1).join("\n") }];
1052
+ } else {
1053
+ warnings.push("caddy: config present but no parseable site blocks");
1054
+ }
1055
+ }
1056
+ for (const { header, body } of blocks) {
1057
+ if (!header) continue;
1058
+ const addrs = parseAddresses(header);
1059
+ if (addrs.length === 0) {
1060
+ warnings.push(
1061
+ `caddy: skipped a block with no usable address (${header.slice(0, 40)})`
1062
+ );
1063
+ continue;
1064
+ }
1065
+ const inlineProxy = body.match(/(?:^|\s)reverse_proxy\s+([^\n{]+)/);
1066
+ const blockProxy = body.match(/(?:^|\s)reverse_proxy\s*\{([\s\S]*?)\}/);
1067
+ const blockTo = blockProxy?.[1].match(/(?:^|\n)\s*to\s+([^\n]+)/);
1068
+ const rootMatch = body.match(/(?:^|\s)root\s+\*?\s*([^\n\s]+)/);
1069
+ const tlsMatch = body.match(/(?:^|\s)tls\s+(\S+)\s+(\S+)/);
1070
+ const upstreams = (inlineProxy?.[1] ?? blockTo?.[1] ?? "").trim().split(/\s+/).filter(Boolean);
1071
+ const host = addrs[0].host;
1072
+ let target;
1073
+ if (upstreams.length > 0) {
1074
+ const upstream = upstreams[0];
1075
+ if (/^unix\//i.test(upstream)) {
1076
+ warnings.push(
1077
+ `caddy: ${host} proxies to the unix socket "${upstream}" \u2014 needs a manual upstream, skipped`
1078
+ );
1079
+ continue;
1080
+ }
1081
+ if (upstreams.length > 1) {
1082
+ warnings.push(
1083
+ `caddy: ${host} load-balances across ${upstreams.length} upstreams \u2014 only the first is migrated`
1084
+ );
1085
+ }
1086
+ target = {
1087
+ kind: "proxy",
1088
+ url: /^https?:\/\//.test(upstream) ? upstream : `http://${upstream}`
1089
+ };
1090
+ } else if (/(?:^|\s)php_fastcgi\s/.test(body)) {
1091
+ warnings.push(
1092
+ `caddy: ${host} serves a PHP-FPM site (php_fastcgi) \u2014 not migratable as a static root; re-add it manually`
1093
+ );
1094
+ continue;
1095
+ } else if (rootMatch) {
1096
+ target = { kind: "static", root: rootMatch[1] };
1097
+ } else {
1098
+ warnings.push(`caddy: ${host} has no reverse_proxy or root \u2014 skipped`);
1099
+ continue;
1100
+ }
1101
+ if (/(?:^|\s)(handle_path|handle)\s+\//.test(body)) {
1102
+ warnings.push(
1103
+ `caddy: ${host} routes by path (handle/handle_path) \u2014 only one upstream is migrated; re-add path rules manually`
1104
+ );
1105
+ }
1106
+ const site = {
1107
+ serverNames: addrs.map((a) => a.host),
1108
+ ssl: addrs.some((a) => a.ssl),
1109
+ target,
1110
+ source: "caddy"
1111
+ };
1112
+ if (tlsMatch && tlsMatch[1].includes("/")) {
1113
+ site.tls = { certPath: tlsMatch[1], keyPath: tlsMatch[2] };
1114
+ }
1115
+ sites.push(site);
1116
+ }
1117
+ const { kept, dropped } = collapseByHost(
1118
+ sites,
1119
+ (s) => s.serverNames,
1120
+ (s) => s.ssl ? 1 : 0
1121
+ );
1122
+ for (const d of dropped) {
1123
+ warnings.push(
1124
+ `caddy: ${d.serverNames.join(", ")} had a second (non-TLS) block \u2014 kept the TLS one`
1125
+ );
1126
+ }
1127
+ return { proxy: "caddy", sites: kept, warnings };
1128
+ }
1129
+
1130
+ // ../../packages/adapters/src/system/proxy/import/apache.ts
1131
+ async function vhostFilesFromDump(executor) {
1132
+ const dump = await tryExec(
1133
+ executor,
1134
+ "apachectl -S 2>/dev/null || apache2ctl -S 2>/dev/null || httpd -S 2>/dev/null"
1135
+ );
1136
+ if (!dump) return [];
1137
+ const files = /* @__PURE__ */ new Set();
1138
+ for (const m of dump.matchAll(/\(([^()]+?):\d+\)/g)) {
1139
+ const f = m[1].trim();
1140
+ if (f.startsWith("/")) files.add(f);
1141
+ }
1142
+ return [...files];
1143
+ }
1144
+ async function loadApacheConfig(executor) {
1145
+ const files = await vhostFilesFromDump(executor);
1146
+ if (files.length > 0) {
1147
+ const cat2 = await tryExec(
1148
+ executor,
1149
+ `cat ${files.map(shellQuote).join(" ")} 2>/dev/null`
1150
+ );
1151
+ if (cat2 && cat2.trim()) return cat2;
1152
+ }
1153
+ const cat = await tryExec(
1154
+ executor,
1155
+ "cat /etc/apache2/sites-enabled/*.conf /etc/apache2/apache2.conf /etc/httpd/conf.d/*.conf /etc/httpd/conf/httpd.conf 2>/dev/null"
1156
+ );
1157
+ return cat ?? "";
1158
+ }
1159
+ function unquote(value) {
1160
+ return value.replace(/^["']|["']$/g, "");
1161
+ }
1162
+ function directive(body, name) {
1163
+ const m = body.match(new RegExp(`(?:^|\\n)\\s*${name}\\s+([^\\n]+)`, "i"));
1164
+ const value = m?.[1]?.trim();
1165
+ return value === void 0 ? void 0 : unquote(value);
1166
+ }
1167
+ function directiveAll(body, name) {
1168
+ return [
1169
+ ...body.matchAll(new RegExp(`(?:^|\\n)\\s*${name}\\s+([^\\n]+)`, "gi"))
1170
+ ].map((m) => m[1].trim());
1171
+ }
1172
+ function isHttpsRedirectOnly(body) {
1173
+ const hasProxy = /(?:^|\n)\s*ProxyPass\s+\S+\s+(?!!)\S/i.test(body);
1174
+ if (hasProxy) return false;
1175
+ const redirects = [
1176
+ ...body.matchAll(
1177
+ /(?:^|\n)\s*Redirect(?:Permanent|Match)?\s+(?:\d{3}\s+|permanent\s+|temp\s+)?\S*\s*(\S+)/gi
1178
+ ),
1179
+ ...body.matchAll(
1180
+ /(?:^|\n)\s*RewriteRule\s+\S+\s+(\S+)\s*\[[^\]]*R=?3\d\d[^\]]*\]/gi
1181
+ )
1182
+ ].map((m) => m[1]);
1183
+ return redirects.length > 0 && redirects.some((t) => /^https:\/\//i.test(t));
1184
+ }
1185
+ function hostOnly(name) {
1186
+ return unquote(name.trim()).replace(/^https?:\/\//i, "").replace(/:\d+$/, "");
1187
+ }
1188
+ function proxyRoutes(body) {
1189
+ const routes = [];
1190
+ const scopes = [];
1191
+ let regexScoped = false;
1192
+ for (const line of body.split("\n")) {
1193
+ const open = line.match(/^\s*<Location(Match)?\s+([^>]*)>/i);
1194
+ if (open) {
1195
+ const regex = Boolean(open[1]);
1196
+ const arg = unquote(open[2].trim());
1197
+ scopes.push({ path: !regex && arg.startsWith("/") ? arg : null, regex });
1198
+ continue;
1199
+ }
1200
+ if (/^\s*<\/Location(?:Match)?\s*>/i.test(line)) {
1201
+ scopes.pop();
1202
+ continue;
1203
+ }
1204
+ const m = line.match(/^\s*ProxyPass\s+(\S+)(?:\s+(\S+))?/i);
1205
+ if (!m) continue;
1206
+ const first = unquote(m[1]);
1207
+ if (first.startsWith("/")) {
1208
+ routes.push({ path: first, url: m[2] ? unquote(m[2]) : "" });
1209
+ continue;
1210
+ }
1211
+ const scope = scopes[scopes.length - 1];
1212
+ if (scope?.path) routes.push({ path: scope.path, url: first });
1213
+ else if (scope?.regex) regexScoped = true;
1214
+ }
1215
+ return { routes, regexScoped };
1216
+ }
1217
+ function parseVhost(body, portHint) {
1218
+ const serverName = directive(body, "ServerName")?.split(/\s+/)[0];
1219
+ const aliases = directiveAll(body, "ServerAlias").flatMap(
1220
+ (line) => line.split(/\s+/)
1221
+ );
1222
+ const names = [serverName, ...aliases].filter((n) => Boolean(n)).map(hostOnly).filter(Boolean);
1223
+ if (names.length === 0) {
1224
+ return { warning: "apache: skipped a VirtualHost with no ServerName" };
1225
+ }
1226
+ if (isHttpsRedirectOnly(body)) return null;
1227
+ const proxy = proxyRoutes(body);
1228
+ const routes = proxy.routes.filter((p) => p.url && p.url !== "!");
1229
+ const docRoot = directive(body, "DocumentRoot");
1230
+ const fcgiMatch = /(?:^|\n)\s*ProxyPassMatch\s+[^\n]*(fcgi:|php-fpm|php\d*-fpm|\.sock)/i.test(
1231
+ body
1232
+ );
1233
+ const certPath = directive(body, "SSLCertificateFile");
1234
+ const keyPath = directive(body, "SSLCertificateKeyFile");
1235
+ const ssl = /:443/.test(portHint) || /SSLEngine\s+on/i.test(body) || Boolean(certPath);
1236
+ let target;
1237
+ if (routes.length > 0) {
1238
+ const primary = routes.find((r) => r.path === "/") ?? routes[0];
1239
+ if (/^balancer:\/\//i.test(primary.url)) {
1240
+ return {
1241
+ warning: `apache: ${names[0]} proxies to an mod_proxy_balancer cluster (${primary.url}) \u2014 re-add its members manually`
1242
+ };
1243
+ }
1244
+ target = { kind: "proxy", url: primary.url };
1245
+ } else if (fcgiMatch) {
1246
+ return {
1247
+ warning: `apache: ${names[0]} is a PHP-FPM site (ProxyPassMatch \u2192 FastCGI) \u2014 not migratable as a static root; re-add it manually`
1248
+ };
1249
+ } else if (docRoot) {
1250
+ target = { kind: "static", root: docRoot };
1251
+ } else if (/(?:^|\n)\s*ProxyPassMatch\s/i.test(body)) {
1252
+ return {
1253
+ warning: `apache: ${names[0]} uses ProxyPassMatch (regex proxying) \u2014 re-add it manually`
1254
+ };
1255
+ } else if (proxy.regexScoped) {
1256
+ return {
1257
+ warning: `apache: ${names[0]} proxies from inside a <LocationMatch> (regex proxying) \u2014 re-add it manually`
1258
+ };
1259
+ } else {
1260
+ return {
1261
+ warning: `apache: ${names[0]} has neither ProxyPass nor DocumentRoot \u2014 skipped`
1262
+ };
1263
+ }
1264
+ const site = {
1265
+ serverNames: names,
1266
+ ssl,
1267
+ target,
1268
+ source: "apache"
1269
+ };
1270
+ if (routes.length > 0) site.routes = routes;
1271
+ if (certPath && keyPath) site.tls = { certPath, keyPath };
1272
+ return site;
1273
+ }
1274
+ function redirectVhostNames(body) {
1275
+ const serverName = directive(body, "ServerName")?.split(/\s+/)[0];
1276
+ const aliases = directiveAll(body, "ServerAlias").flatMap(
1277
+ (l) => l.split(/\s+/)
1278
+ );
1279
+ return [serverName, ...aliases].filter((n) => Boolean(n)).map(hostOnly).filter(Boolean);
1280
+ }
1281
+ function vhostBlocks(text) {
1282
+ const out = [];
1283
+ const re = /<VirtualHost([^>]*)>([\s\S]*?)<\/VirtualHost>/gi;
1284
+ let m;
1285
+ while ((m = re.exec(text)) !== null) {
1286
+ out.push({ args: m[1].trim(), body: m[2] });
1287
+ }
1288
+ return out;
1289
+ }
1290
+ async function scanApache(executor) {
1291
+ const raw = await loadApacheConfig(executor);
1292
+ const warnings = [];
1293
+ const sites = [];
1294
+ if (!raw.trim()) {
1295
+ return {
1296
+ proxy: "apache",
1297
+ sites,
1298
+ warnings: ["apache: no readable configuration found"]
1299
+ };
1300
+ }
1301
+ const text = stripComments(raw);
1302
+ const blocks = vhostBlocks(text);
1303
+ if (blocks.length === 0)
1304
+ warnings.push("apache: no <VirtualHost> blocks found");
1305
+ const redirectOnlyHosts = [];
1306
+ for (const { args, body } of blocks) {
1307
+ const parsed = parseVhost(body, args);
1308
+ if (parsed === null) {
1309
+ redirectOnlyHosts.push(redirectVhostNames(body));
1310
+ continue;
1311
+ }
1312
+ if ("warning" in parsed) warnings.push(parsed.warning);
1313
+ else sites.push(parsed);
1314
+ }
1315
+ const served = new Set(sites.flatMap((s) => s.serverNames));
1316
+ for (const names of redirectOnlyHosts) {
1317
+ const orphaned = names.filter((n) => !served.has(n));
1318
+ if (orphaned.length > 0) {
1319
+ warnings.push(
1320
+ `apache: ${orphaned.join(", ")} only redirected to HTTPS and has no TLS VirtualHost here \u2014 nothing to serve it after migration`
1321
+ );
1322
+ }
1323
+ }
1324
+ const { kept, dropped } = collapseByHost(
1325
+ sites,
1326
+ (s) => s.serverNames,
1327
+ (s) => (s.ssl ? 2 : 0) + (s.target.kind === "proxy" ? 1 : 0)
1328
+ );
1329
+ for (const d of dropped) {
1330
+ warnings.push(
1331
+ `apache: ${d.serverNames.join(", ")} had a second VirtualHost (${d.ssl ? "TLS" : "plain HTTP"}, ${d.target.kind}) \u2014 kept the TLS/proxy one`
1332
+ );
1333
+ }
1334
+ return { proxy: "apache", sites: kept, warnings };
1335
+ }
1336
+
1337
+ // ../../packages/adapters/src/system/proxy/import/traefik.ts
1338
+ function lowerKeys(labels) {
1339
+ const out = {};
1340
+ for (const [k, v] of Object.entries(labels)) out[k.toLowerCase()] = v;
1341
+ return out;
1342
+ }
1343
+ function extractHosts(rule) {
1344
+ const hosts = [];
1345
+ for (const call of rule.matchAll(/\bHost\(([^)]*)\)/gi)) {
1346
+ for (const m of call[1].matchAll(/[`'"]([^`'"]+)[`'"]/g)) {
1347
+ const h = m[1].trim();
1348
+ if (h) hosts.push(h);
1349
+ }
1350
+ }
1351
+ return [...new Set(hosts)];
1352
+ }
1353
+ function extraMatchers(rule) {
1354
+ const found = /* @__PURE__ */ new Set();
1355
+ for (const m of rule.matchAll(
1356
+ /\b(PathPrefix|PathRegexp|Path|HeadersRegexp|Headers|Method|Query|ClientIP|HostRegexp)\b/gi
1357
+ )) {
1358
+ found.add(m[1]);
1359
+ }
1360
+ return [...found];
1361
+ }
1362
+ function collectDefinitions(containers) {
1363
+ const services = /* @__PURE__ */ new Map();
1364
+ const redirectMiddlewares = /* @__PURE__ */ new Set();
1365
+ const streamRouters = [];
1366
+ let fileProvider = false;
1367
+ let exposedByDefault = true;
1368
+ for (const c of containers) {
1369
+ const labels = lowerKeys(c.labels);
1370
+ for (const [k, v] of Object.entries(labels)) {
1371
+ let m = k.match(
1372
+ /^traefik\.http\.services\.([^.]+)\.loadbalancer\.server\.(port|scheme)$/
1373
+ );
1374
+ if (m) {
1375
+ const def = services.get(m[1]) ?? {};
1376
+ if (m[2] === "port") def.port = v;
1377
+ else def.scheme = v;
1378
+ services.set(m[1], def);
1379
+ continue;
1380
+ }
1381
+ m = k.match(/^traefik\.http\.middlewares\.([^.]+)\.redirectscheme\./);
1382
+ if (m) {
1383
+ redirectMiddlewares.add(m[1]);
1384
+ continue;
1385
+ }
1386
+ m = k.match(/^traefik\.(tcp|udp)\.routers\.([^.]+)\.rule$/);
1387
+ if (m) streamRouters.push(`${m[1]} router "${m[2]}"`);
1388
+ }
1389
+ const cmd = c.cmd ?? "";
1390
+ if (/--?providers\.file/i.test(cmd)) fileProvider = true;
1391
+ if (/--?providers\.docker\.exposedbydefault[= ]?false/i.test(cmd))
1392
+ exposedByDefault = false;
1393
+ }
1394
+ return {
1395
+ services,
1396
+ redirectMiddlewares,
1397
+ streamRouters,
1398
+ fileProvider,
1399
+ exposedByDefault
1400
+ };
1401
+ }
1402
+ function resolveIp(c, labels) {
1403
+ const wanted = labels["traefik.docker.network"];
1404
+ if (wanted && c.networks?.[wanted]) return c.networks[wanted];
1405
+ if (c.ip) return c.ip;
1406
+ const first = Object.values(c.networks ?? {}).find(Boolean);
1407
+ return first || void 0;
1408
+ }
1409
+ function parseTraefikLabels(containers) {
1410
+ const warnings = [];
1411
+ const defs = collectDefinitions(containers);
1412
+ const candidates = [];
1413
+ for (const c of containers) {
1414
+ const labels = lowerKeys(c.labels);
1415
+ if (labels["traefik.enable"] === "false") continue;
1416
+ if (!defs.exposedByDefault && labels["traefik.enable"] !== "true") continue;
1417
+ const routers = /* @__PURE__ */ new Map();
1418
+ for (const [key, val] of Object.entries(labels)) {
1419
+ const r = key.match(/^traefik\.http\.routers\.([^.]+)\.(.+)$/);
1420
+ if (!r) continue;
1421
+ const bag = routers.get(r[1]) ?? {};
1422
+ bag[r[2]] = val;
1423
+ routers.set(r[1], bag);
1424
+ }
1425
+ for (const [rname, props] of routers) {
1426
+ const rule = props.rule;
1427
+ if (!rule) continue;
1428
+ const hosts = extractHosts(rule);
1429
+ if (hosts.length === 0) {
1430
+ warnings.push(
1431
+ /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`
1432
+ );
1433
+ continue;
1434
+ }
1435
+ const ip = resolveIp(c, labels);
1436
+ if (!ip) {
1437
+ warnings.push(
1438
+ `traefik: ${hosts.join(", ")} \u2014 couldn't resolve container ${c.name}'s IP; re-add the upstream manually`
1439
+ );
1440
+ continue;
1441
+ }
1442
+ const svc = props.service?.replace(/@.*$/, "");
1443
+ const def = (svc ? defs.services.get(svc) : void 0) ?? (defs.services.size === 1 ? [...defs.services.values()][0] : void 0);
1444
+ const onlyExposed = c.exposedPorts?.length === 1 ? c.exposedPorts[0].split("/")[0] : void 0;
1445
+ const rawPort = def?.port ?? onlyExposed;
1446
+ const port = /^\d{1,5}$/.test(String(rawPort)) ? String(rawPort) : "80";
1447
+ const scheme = def?.scheme?.toLowerCase() === "https" ? "https" : "http";
1448
+ const middlewares = (props.middlewares ?? "").split(",").map((m) => m.trim().replace(/@.*$/, "")).filter(Boolean);
1449
+ candidates.push({
1450
+ hosts,
1451
+ ssl: Object.keys(props).some(
1452
+ (p) => p === "tls" || p.startsWith("tls.")
1453
+ ),
1454
+ url: `${scheme}://${ip}:${port}`,
1455
+ container: c.name,
1456
+ router: rname,
1457
+ rule,
1458
+ middlewares,
1459
+ redirectOnly: middlewares.length > 0 && middlewares.every((m) => defs.redirectMiddlewares.has(m)),
1460
+ portGuessed: rawPort === void 0
1461
+ });
1462
+ }
1463
+ }
1464
+ const { kept: sites, dropped } = collapseByHost(
1465
+ candidates,
1466
+ (c) => c.hosts,
1467
+ (c) => c.redirectOnly ? 0 : c.ssl ? 2 : 1
1468
+ );
1469
+ for (const cand of sites) {
1470
+ const extras = extraMatchers(cand.rule);
1471
+ if (extras.length > 0) {
1472
+ warnings.push(
1473
+ `traefik: ${cand.hosts.join(", ")} also matches ${extras.join(", ")} \u2014 only the Host() part is migrated; re-add path/header rules manually`
1474
+ );
1475
+ }
1476
+ const carried = cand.middlewares.filter(
1477
+ (m) => !defs.redirectMiddlewares.has(m)
1478
+ );
1479
+ if (carried.length > 0) {
1480
+ warnings.push(
1481
+ `traefik: ${cand.hosts.join(", ")} uses middleware(s) "${carried.join(", ")}" \u2014 not migrated`
1482
+ );
1483
+ }
1484
+ if (cand.portGuessed) {
1485
+ warnings.push(
1486
+ `traefik: ${cand.hosts.join(", ")} \u2014 no service port label and no single exposed port on ${cand.container}; assumed :80, check the upstream`
1487
+ );
1488
+ }
1489
+ }
1490
+ for (const d of dropped) {
1491
+ warnings.push(
1492
+ `traefik: ${d.hosts.join(", ")} router "${d.router}" is an http\u2192https redirect \u2014 Kraft's edge does that itself (nothing to migrate)`
1493
+ );
1494
+ }
1495
+ for (const s of defs.streamRouters) {
1496
+ warnings.push(
1497
+ `traefik: ${s} can't migrate \u2014 Kraft's edge routes HTTP(S), not raw TCP/UDP`
1498
+ );
1499
+ }
1500
+ if (defs.fileProvider) {
1501
+ warnings.push(
1502
+ "traefik: a file provider is configured \u2014 routes defined in its YAML/TOML files are NOT read here, only container labels. Re-add those domains manually."
1503
+ );
1504
+ }
1505
+ return {
1506
+ proxy: "traefik",
1507
+ sites: sites.map((cand) => ({
1508
+ serverNames: cand.hosts,
1509
+ ssl: cand.ssl,
1510
+ target: { kind: "proxy", url: cand.url },
1511
+ source: `traefik container ${cand.container}`
1512
+ })),
1513
+ warnings
1514
+ };
1515
+ }
1516
+ async function scanTraefik(executor) {
1517
+ const out = await tryExec(
1518
+ executor,
1519
+ `docker ps -q 2>/dev/null | xargs -r docker inspect --format '{{.Name}} {{json .Config.Labels}} {{range $n, $v := .NetworkSettings.Networks}}{{$n}}={{$v.IPAddress}} {{end}} {{range $p, $v := .Config.ExposedPorts}}{{$p}} {{end}} {{if .Config.Cmd}}{{join .Config.Cmd " "}}{{end}}' 2>/dev/null`
1520
+ );
1521
+ if (!out) {
1522
+ return {
1523
+ proxy: "traefik",
1524
+ sites: [],
1525
+ warnings: ["traefik: couldn't read container labels via docker inspect"]
1526
+ };
1527
+ }
1528
+ const containers = [];
1529
+ for (const line of out.split("\n")) {
1530
+ const [rawName, rawLabels, rawNets, rawPorts, rawCmd] = line.split(" ");
1531
+ if (!rawName || !rawLabels) continue;
1532
+ let labels;
1533
+ try {
1534
+ labels = JSON.parse(rawLabels) ?? {};
1535
+ } catch {
1536
+ continue;
1537
+ }
1538
+ const networks = {};
1539
+ let bareIp;
1540
+ for (const pair of (rawNets ?? "").trim().split(/\s+/)) {
1541
+ if (!pair) continue;
1542
+ const eq = pair.indexOf("=");
1543
+ if (eq > 0) {
1544
+ const ip = pair.slice(eq + 1);
1545
+ if (ip) networks[pair.slice(0, eq)] = ip;
1546
+ } else if (!bareIp) {
1547
+ bareIp = pair;
1548
+ }
1549
+ }
1550
+ const exposedPorts = (rawPorts ?? "").trim().split(/\s+/).filter(Boolean);
1551
+ const hasTraefikLabels = Object.keys(labels).some(
1552
+ (k) => k.toLowerCase().startsWith("traefik.")
1553
+ );
1554
+ const looksLikeTraefik = /traefik/i.test(rawCmd ?? "") || /--?providers\./i.test(rawCmd ?? "");
1555
+ if (!hasTraefikLabels && !looksLikeTraefik) continue;
1556
+ containers.push({
1557
+ name: rawName.replace(/^\//, ""),
1558
+ labels,
1559
+ ip: Object.values(networks)[0] ?? bareIp,
1560
+ networks,
1561
+ exposedPorts,
1562
+ cmd: rawCmd ?? void 0
1563
+ });
1564
+ }
1565
+ return parseTraefikLabels(containers);
1566
+ }
1567
+
1568
+ // ../../packages/adapters/src/system/proxy/import/index.ts
1569
+ var INSTALLED_MARKERS = [
1570
+ { proxy: "nginx", paths: ["/etc/nginx/nginx.conf"] },
1571
+ { proxy: "caddy", paths: ["/etc/caddy/Caddyfile"] },
1572
+ {
1573
+ proxy: "apache",
1574
+ paths: ["/etc/apache2/apache2.conf", "/etc/httpd/conf/httpd.conf"]
1575
+ },
1576
+ {
1577
+ proxy: "openresty",
1578
+ paths: [
1579
+ "/usr/local/openresty/nginx/conf/nginx.conf",
1580
+ "/etc/openresty/nginx.conf"
1581
+ ]
1582
+ }
1583
+ ];
1584
+ async function detectInstalledProxy(executor) {
1585
+ const probe = INSTALLED_MARKERS.map(({ proxy, paths }) => {
1586
+ const test = paths.map((p) => `[ -f '${p}' ]`).join(" || ");
1587
+ return `{ ${test}; } && echo ${proxy}`;
1588
+ }).join("; ");
1589
+ let out = "";
1590
+ try {
1591
+ out = await executor.exec(`{ ${probe}; } 2>/dev/null; true`, {
1592
+ timeout: 1e4
1593
+ });
1594
+ } catch {
1595
+ return null;
1596
+ }
1597
+ const found = new Set(
1598
+ out.split("\n").map((l) => l.trim()).filter(Boolean)
1599
+ );
1600
+ return INSTALLED_MARKERS.find((m) => found.has(m.proxy))?.proxy ?? null;
1601
+ }
1602
+ async function scanImportableSites(executor, proxy) {
1603
+ switch (proxy) {
1604
+ case "nginx":
1605
+ return scanNginx(executor);
1606
+ case "caddy":
1607
+ return scanCaddy(executor);
1608
+ case "apache":
1609
+ return scanApache(executor);
1610
+ case "traefik":
1611
+ return scanTraefik(executor);
1612
+ case "openresty":
1613
+ return scanForeignOpenResty(executor);
1614
+ default:
1615
+ return {
1616
+ proxy,
1617
+ sites: [],
1618
+ warnings: [`${proxy}: config import not supported \u2014 takeover only`]
1619
+ };
1620
+ }
1621
+ }
1622
+ function canImportProxy(proxy) {
1623
+ return proxy === "nginx" || proxy === "caddy" || proxy === "apache" || proxy === "traefik" || proxy === "openresty";
1624
+ }
1625
+ function unreachableStaticRoots(sites, opts) {
1626
+ if (!opts.containerEdge) return [];
1627
+ const visible = EDGE_CONTAINER_MOUNTS.map((m) => m.host.replace(/\/+$/, ""));
1628
+ const out = [];
1629
+ for (const site of sites) {
1630
+ if (site.target.kind !== "static") continue;
1631
+ const root = site.target.root.replace(/\/+$/, "");
1632
+ const reachable = visible.some(
1633
+ (v) => root === v || root.startsWith(`${v}/`)
1634
+ );
1635
+ if (!reachable)
1636
+ out.push({ host: site.serverNames[0] ?? root, root: site.target.root });
1637
+ }
1638
+ return out;
1639
+ }
1640
+ var EDGE_STATIC_MOUNT = EDGE_CONTAINER_MOUNTS.find((m) => m.host === "/opt/kraft/static")?.host ?? "/opt/kraft/static";
1641
+ async function copyStaticRootIntoEdge(executor, opts) {
1642
+ const slug = opts.host.replace(/[^a-zA-Z0-9._-]/g, "-").replace(/^-+|-+$/g, "").toLowerCase() || "site";
1643
+ const src = opts.root.replace(/\/+$/, "");
1644
+ const dest = `${EDGE_STATIC_MOUNT}/_adopted/${slug}`;
1645
+ await executor.exec(`mkdir -p ${sq2(dest)}`);
1646
+ await executor.exec(`cp -a ${sq2(`${src}/.`)} ${sq2(`${dest}/`)}`);
1647
+ return dest;
1648
+ }
1649
+
1650
+ // ../../packages/adapters/src/system/proxy/import/caddy-certs.ts
1651
+ var CADDY_DATA_ROOTS = [
1652
+ "/var/lib/caddy/.local/share/caddy",
1653
+ "/root/.local/share/caddy",
1654
+ "/home/caddy/.local/share/caddy",
1655
+ "/data/caddy"
1656
+ ];
1657
+ function candidateNames(host) {
1658
+ const names = [host];
1659
+ const dot = host.indexOf(".");
1660
+ if (dot > 0) names.push(`wildcard_${host.slice(dot)}`);
1661
+ return names;
1662
+ }
1663
+ async function findPairIn(exec, root, host, container) {
1664
+ for (const name of candidateNames(host)) {
1665
+ const glob = `${root}/certificates/*/${name}/${name}.crt`;
1666
+ let certPath = firstCrt(await tryExec(exec, `ls -1 ${glob} 2>/dev/null`));
1667
+ if (!certPath && container) {
1668
+ certPath = firstCrt(
1669
+ await tryExec(
1670
+ exec,
1671
+ containerCommand(container, `ls -1 ${glob} 2>/dev/null`)
1672
+ )
1673
+ );
1674
+ }
1675
+ if (certPath)
1676
+ return { certPath, keyPath: certPath.replace(/\.crt$/, ".key") };
1677
+ }
1678
+ return null;
1679
+ }
1680
+ function firstCrt(listing) {
1681
+ return (listing ?? "").split("\n").map((l) => l.trim()).find((l) => l.endsWith(".crt"));
1682
+ }
1683
+ async function caddyStoreCert(exec, host, container) {
1684
+ for (const root of CADDY_DATA_ROOTS) {
1685
+ const pair = await findPairIn(exec, root, host, container);
1686
+ if (!pair) continue;
1687
+ const certPem = await readMaybeInContainer(exec, pair.certPath, container);
1688
+ const keyPem = await readMaybeInContainer(exec, pair.keyPath, container);
1689
+ if (certPem.trim() && keyPem.trim()) {
1690
+ return { certPem, keyPem, source: `caddy store ${pair.certPath}` };
1691
+ }
1692
+ }
1693
+ return null;
1694
+ }
1695
+
1696
+ // ../../packages/adapters/src/system/proxy/import/traefik-certs.ts
1697
+ var DEFAULT_ACME_PATHS = [
1698
+ "/acme.json",
1699
+ "/etc/traefik/acme.json",
1700
+ "/data/acme.json",
1701
+ "/letsencrypt/acme.json"
1702
+ ];
1703
+ var TRAEFIK_STATIC_CONFIGS = [
1704
+ "/etc/traefik/traefik.yml",
1705
+ "/etc/traefik/traefik.yaml",
1706
+ "/etc/traefik/traefik.toml"
1707
+ ];
1708
+ function prop(obj, name) {
1709
+ const hit = Object.keys(obj).find(
1710
+ (k) => k.toLowerCase() === name.toLowerCase()
1711
+ );
1712
+ return hit === void 0 ? void 0 : obj[hit];
1713
+ }
1714
+ async function candidateStoragePaths(exec, container) {
1715
+ const found = [];
1716
+ const collect = (text) => {
1717
+ for (const m of (text ?? "").matchAll(
1718
+ /acme\.storage[=:\s]+["']?(\/[^\s"',}]+)/gi
1719
+ )) {
1720
+ if (isSafeCertPath(m[1])) found.push(m[1]);
1721
+ }
1722
+ for (const m of (text ?? "").matchAll(
1723
+ /(?:^|\s)storage:\s*["']?(\/[^\s"',}]+\.json)/gi
1724
+ )) {
1725
+ if (isSafeCertPath(m[1])) found.push(m[1]);
1726
+ }
1727
+ };
1728
+ if (container) {
1729
+ collect(
1730
+ await tryExec(
1731
+ exec,
1732
+ `docker inspect ${sq2(container)} --format '{{json .Config.Cmd}} {{json .Args}} {{json .Config.Env}}' 2>/dev/null`
1733
+ )
1734
+ );
1735
+ for (const cfg of TRAEFIK_STATIC_CONFIGS) {
1736
+ collect(await readMaybeInContainer(exec, cfg, container));
1737
+ }
1738
+ }
1739
+ return [.../* @__PURE__ */ new Set([...found, ...DEFAULT_ACME_PATHS])];
1740
+ }
1741
+ async function loadAcmeJson(exec, container) {
1742
+ for (const path of await candidateStoragePaths(exec, container)) {
1743
+ const raw = await readMaybeInContainer(exec, path, container);
1744
+ if (raw.trim().startsWith("{")) return { raw, path };
1745
+ }
1746
+ return null;
1747
+ }
1748
+ function certFromAcmeJson(raw, host) {
1749
+ let parsed;
1750
+ try {
1751
+ parsed = JSON.parse(raw);
1752
+ } catch {
1753
+ return null;
1754
+ }
1755
+ const target = host.toLowerCase();
1756
+ const matches = (name) => {
1757
+ const n = (name ?? "").toLowerCase();
1758
+ if (!n) return false;
1759
+ if (n === target) return true;
1760
+ if (!n.startsWith("*.")) return false;
1761
+ const suffix = n.slice(1);
1762
+ return target.endsWith(suffix) && !target.slice(0, -suffix.length).includes(".");
1763
+ };
1764
+ for (const [resolver, value] of Object.entries(parsed)) {
1765
+ if (!value || typeof value !== "object") continue;
1766
+ const certs = prop(value, "certificates");
1767
+ if (!Array.isArray(certs)) continue;
1768
+ for (const entry of certs) {
1769
+ const domain = entry && prop(entry, "domain");
1770
+ const names = [domain?.main, ...domain?.sans ?? []];
1771
+ if (!names.some(matches)) continue;
1772
+ const certB64 = prop(entry, "certificate");
1773
+ const keyB64 = prop(entry, "key");
1774
+ if (typeof certB64 !== "string" || typeof keyB64 !== "string") continue;
1775
+ const certPem = Buffer.from(certB64, "base64").toString("utf-8");
1776
+ const keyPem = Buffer.from(keyB64, "base64").toString("utf-8");
1777
+ if (!certPem.includes("-----BEGIN") || !keyPem.includes("-----BEGIN"))
1778
+ continue;
1779
+ return { certPem, keyPem, resolver };
1780
+ }
1781
+ }
1782
+ return null;
1783
+ }
1784
+ async function traefikAcmeCert(exec, host, container) {
1785
+ const loaded = await loadAcmeJson(exec, container);
1786
+ if (!loaded) return null;
1787
+ const hit = certFromAcmeJson(loaded.raw, host);
1788
+ if (!hit) return null;
1789
+ return {
1790
+ certPem: hit.certPem,
1791
+ keyPem: hit.keyPem,
1792
+ source: `traefik ${loaded.path} (resolver ${hit.resolver})`
1793
+ };
1794
+ }
1795
+ async function traefikDeclaredCertPaths(exec, container) {
1796
+ const pairs = [];
1797
+ for (const cfg of TRAEFIK_STATIC_CONFIGS) {
1798
+ const text = await readMaybeInContainer(exec, cfg, container);
1799
+ if (!text.trim()) continue;
1800
+ const certFiles = [
1801
+ ...text.matchAll(/certFile:\s*["']?(\/[^\s"',}]+)/gi)
1802
+ ].map((m) => m[1]);
1803
+ const keyFiles = [...text.matchAll(/keyFile:\s*["']?(\/[^\s"',}]+)/gi)].map(
1804
+ (m) => m[1]
1805
+ );
1806
+ for (let i = 0; i < Math.min(certFiles.length, keyFiles.length); i++) {
1807
+ if (isSafeCertPath(certFiles[i]) && isSafeCertPath(keyFiles[i])) {
1808
+ pairs.push({ certPath: certFiles[i], keyPath: keyFiles[i] });
1809
+ }
1810
+ }
1811
+ }
1812
+ return pairs;
1813
+ }
1814
+
1815
+ // ../../packages/adapters/src/system/proxy/api.ts
1816
+ function buildProxyRouteIndex(sites) {
1817
+ const byPort = /* @__PURE__ */ new Map();
1818
+ for (const site of sites) {
1819
+ const upstreams = site.routes ?? (site.target.kind === "proxy" ? [{ path: "/", url: site.target.url }] : []);
1820
+ for (const up of upstreams) {
1821
+ let port;
1822
+ try {
1823
+ port = Number(new URL(up.url).port);
1824
+ } catch {
1825
+ continue;
1826
+ }
1827
+ if (!Number.isFinite(port) || port <= 0) continue;
1828
+ const list = byPort.get(port) ?? [];
1829
+ const existing = list.find(
1830
+ (r) => r.path === up.path && Boolean(r.exact) === Boolean(up.exact)
1831
+ );
1832
+ if (existing) {
1833
+ existing.domains = [
1834
+ .../* @__PURE__ */ new Set([...existing.domains, ...site.serverNames])
1835
+ ];
1836
+ if (site.ssl) {
1837
+ existing.ssl.enabled = true;
1838
+ if (!existing.ssl.certPath && site.tls) {
1839
+ existing.ssl.certPath = site.tls.certPath;
1840
+ existing.ssl.keyPath = site.tls.keyPath;
1841
+ }
1842
+ }
1843
+ if (site.proxy && (site.ssl || !existing.proxy))
1844
+ existing.proxy = site.proxy;
1845
+ } else {
1846
+ list.push({
1847
+ port,
1848
+ path: up.path,
1849
+ ...up.exact ? { exact: true } : {},
1850
+ domains: [...site.serverNames],
1851
+ ...site.proxy ? { proxy: site.proxy } : {},
1852
+ ssl: site.ssl ? {
1853
+ enabled: true,
1854
+ certPath: site.tls?.certPath,
1855
+ keyPath: site.tls?.keyPath
1856
+ } : { enabled: false },
1857
+ source: site.source
1858
+ });
1859
+ }
1860
+ byPort.set(port, list);
1861
+ }
1862
+ }
1863
+ return byPort;
1864
+ }
1865
+ async function declaredCert(ctx) {
1866
+ const tls = ctx.site?.tls;
1867
+ if (!tls) return null;
1868
+ const pems = await readDeclaredPair(
1869
+ ctx.exec,
1870
+ tls.certPath,
1871
+ tls.keyPath,
1872
+ ctx.container
1873
+ );
1874
+ return pems ? { ...pems, source: tls.certPath } : null;
1875
+ }
1876
+ var CERT_SOURCES = {
1877
+ nginx: [declaredCert],
1878
+ openresty: [declaredCert],
1879
+ apache: [declaredCert],
1880
+ caddy: [declaredCert, (c) => caddyStoreCert(c.exec, c.host, c.container)],
1881
+ traefik: [
1882
+ (c) => traefikAcmeCert(c.exec, c.host, c.container),
1883
+ // File provider: paths aren't bound to a hostname (traefik picks by SNI), so
1884
+ // hand every declared pair to the validator and let coverage decide.
1885
+ async (c) => {
1886
+ for (const pair of await traefikDeclaredCertPaths(c.exec, c.container)) {
1887
+ const pems = await readDeclaredPair(
1888
+ c.exec,
1889
+ pair.certPath,
1890
+ pair.keyPath,
1891
+ c.container
1892
+ );
1893
+ if (!pems) continue;
1894
+ if (validateCertFor(c.host, pems, pair.certPath).cert) {
1895
+ return { ...pems, source: pair.certPath };
1896
+ }
1897
+ }
1898
+ return null;
1899
+ }
1900
+ ],
1901
+ // Takeover-only: no importable config, so no cert to find either.
1902
+ haproxy: []
1903
+ };
1904
+ async function edgeProxy(exec, opts = {}) {
1905
+ const status = opts.status ?? await probeEdge(exec).catch(() => null);
1906
+ if (!status) return null;
1907
+ let kind = null;
1908
+ let ours = false;
1909
+ if (status.classification === "ours") {
1910
+ kind = "openresty";
1911
+ ours = true;
1912
+ } else if (status.classification === "known") {
1913
+ const found = status.occupants.find((o) => o.proxy)?.proxy;
1914
+ kind = found && canImportProxy(found) ? found : null;
1915
+ }
1916
+ if (!kind) kind = await detectInstalledProxy(exec).catch(() => null);
1917
+ if (!kind) return null;
1918
+ const container = status.occupants.find((o) => o.proxy === kind)?.containerName ?? null;
1919
+ return makeApi(exec, kind, ours, container);
1920
+ }
1921
+ function edgeProxyFor(exec, kind, opts = {}) {
1922
+ return makeApi(exec, kind, opts.ours ?? false, opts.container ?? null);
1923
+ }
1924
+ async function collectProxyCerts(api, sites) {
1925
+ const certPems = {};
1926
+ const warnings = [];
1927
+ const hosts = [
1928
+ ...new Set(sites.filter((s) => s.ssl).flatMap((s) => s.serverNames))
1929
+ ];
1930
+ for (const host of hosts) {
1931
+ const candidate = await api.certCandidateFor(host).catch(() => null);
1932
+ if (candidate?.cert) {
1933
+ certPems[host] = {
1934
+ certPem: candidate.cert.certPem,
1935
+ keyPem: candidate.cert.keyPem
1936
+ };
1937
+ } else if (candidate?.reason) {
1938
+ warnings.push(`${host}: no reusable certificate \u2014 ${candidate.reason}`);
1939
+ }
1940
+ }
1941
+ return { certPems, warnings };
1942
+ }
1943
+ function makeApi(exec, kind, ours, container) {
1944
+ let scan = null;
1945
+ const listSites = () => {
1946
+ scan ??= (ours ? scanKraftEdge(exec) : scanImportableSites(exec, kind)).catch(() => ({
1947
+ proxy: kind,
1948
+ sites: [],
1949
+ warnings: [`${kind}: config scan failed`]
1950
+ }));
1951
+ return scan;
1952
+ };
1953
+ let strictLoopbackPorts = null;
1954
+ const listLoopbackUpstreamPortsStrict = (opts = {}) => {
1955
+ if (!ours) {
1956
+ return Promise.reject(
1957
+ new Error(
1958
+ "Strict loopback upstream inventory is only available for the Kraft edge"
1959
+ )
1960
+ );
1961
+ }
1962
+ if (opts.refresh || !strictLoopbackPorts) {
1963
+ const nextScan = scanKraftEdgeStrict(exec, container).then(
1964
+ ({ loopbackUpstreamPorts }) => loopbackUpstreamPorts
1965
+ );
1966
+ strictLoopbackPorts = nextScan;
1967
+ void nextScan.catch(() => {
1968
+ if (strictLoopbackPorts === nextScan) strictLoopbackPorts = null;
1969
+ });
1970
+ }
1971
+ return strictLoopbackPorts;
1972
+ };
1973
+ const siteFor = async (host) => {
1974
+ const target = host.toLowerCase();
1975
+ const { sites } = await listSites();
1976
+ return sites.find(
1977
+ (s) => s.serverNames.some((n) => n.toLowerCase() === target)
1978
+ ) ?? // A wildcard vhost (`*.example.com`) serves the host without naming it.
1979
+ sites.find(
1980
+ (s) => s.serverNames.some((n) => {
1981
+ const name = n.toLowerCase();
1982
+ if (!name.startsWith("*.")) return false;
1983
+ const suffix = name.slice(1);
1984
+ return target.endsWith(suffix) && !target.slice(0, -suffix.length).includes(".");
1985
+ })
1986
+ ) ?? null;
1987
+ };
1988
+ const certCandidateFor = async (host) => {
1989
+ const site = await siteFor(host);
1990
+ const ctx = { exec, host, site, container };
1991
+ let lastReason = null;
1992
+ for (const source of CERT_SOURCES[kind] ?? []) {
1993
+ const raw = await source(ctx).catch(() => null);
1994
+ if (!raw) continue;
1995
+ const candidate = validateCertFor(host, raw, raw.source);
1996
+ if (candidate.cert) return candidate;
1997
+ lastReason = candidate.reason;
1998
+ }
1999
+ return {
2000
+ cert: null,
2001
+ reason: lastReason ?? `${kind}: no certificate found for ${host}`
2002
+ };
2003
+ };
2004
+ return {
2005
+ kind,
2006
+ ours,
2007
+ container,
2008
+ listSites,
2009
+ listLoopbackUpstreamPortsStrict,
2010
+ sitesByPort: async () => buildProxyRouteIndex((await listSites()).sites),
2011
+ siteFor,
2012
+ certFor: async (host) => (await certCandidateFor(host)).cert,
2013
+ certCandidateFor
2014
+ };
2015
+ }
2016
+
2017
+ // ../../packages/adapters/src/system/catalog.ts
2018
+ var MIN_DOCKER_VERSION = "20.10";
2019
+ function dockerInstallPlan(profile) {
2020
+ const ops = envOps(profile);
2021
+ const install = ops.dockerInstall();
2022
+ if (!install.supported) return install;
2023
+ const start = ops.dockerStart();
2024
+ return answered({
2025
+ installCommand: opScript(install.value),
2026
+ service: start.supported ? answered(opScript(start.value)) : start,
2027
+ verifyCommand: "docker --version"
2028
+ });
2029
+ }
2030
+ function packagePlan(profile, pkg, verifyCommand) {
2031
+ const install = envOps(profile).pkgInstall([pkg]);
2032
+ return install.supported ? answered({ installCommand: opScript(install.value), verifyCommand }) : install;
2033
+ }
2034
+ var systemCatalog = {
2035
+ checks: {
2036
+ docker: {
2037
+ versionCommand: "docker --version",
2038
+ // A POSITIVE signal, not a bare exit code: only a daemon that answered can
2039
+ // name its version. `docker info >/dev/null && echo ok` prints ok on ANY
2040
+ // exit-0, so a daemon answering nothing would read as healthy — trading #408's
2041
+ // false negative for a false positive. Leave stderr alone too; `checkDocker`
2042
+ // puts the failure text in the component message.
2043
+ daemonCommand: "docker info --format '{{.ServerVersion}}'",
2044
+ parseVersion: (output) => output.match(/Docker version ([^\s,]+)/)?.[1] ?? output,
2045
+ missingMessage: "Docker is not installed",
2046
+ notRunningMessage: "Docker is installed but the daemon is not running",
2047
+ // Deliberately does NOT claim the daemon is up or down: the socket's permission
2048
+ // is checked before the daemon is asked, so this arm proves only that we may not
2049
+ // ask. What it can state is the fix, which is the part "not running" got wrong.
2050
+ deniedMessage: "Docker is installed but this connection is not allowed to use its socket \u2014 add the login user to the `docker` group, or connect as root"
2051
+ },
2052
+ openresty: {
2053
+ versionCommand: "openresty -v 2>&1 || /usr/local/openresty/bin/openresty -v 2>&1",
2054
+ runningCommands: [
2055
+ "pgrep -f 'nginx.*openresty' || pgrep -f '/usr/local/openresty'"
2056
+ ],
2057
+ parseVersion: (output) => output.match(/openresty\/(\S+)/)?.[1] ?? output.match(/nginx\/(\S+)/)?.[1] ?? output,
2058
+ missingMessage: "OpenResty is not installed",
2059
+ notRunningMessage: "OpenResty is installed but not running"
2060
+ },
2061
+ certbot: {
2062
+ versionCommand: "certbot --version 2>/dev/null",
2063
+ parseVersion: (output) => output.match(/certbot\s+(\S+)/)?.[1] ?? output,
2064
+ missingMessage: "Certbot is not installed"
2065
+ },
2066
+ git: {
2067
+ versionCommand: "git --version",
2068
+ parseVersion: (output) => output.match(/git version (\S+)/)?.[1] ?? output,
2069
+ missingMessage: "Git is not installed"
2070
+ },
2071
+ rsync: {
2072
+ versionCommand: "rsync --version | head -n 1",
2073
+ parseVersion: (output) => output.match(/rsync\s+version\s+(\S+)/i)?.[1] ?? output,
2074
+ missingMessage: "rsync is not installed"
2075
+ },
2076
+ ufw: {
2077
+ versionCommand: firewallVersionSteps("ufw")[0],
2078
+ parseVersion: (output) => output.match(/ufw\s+(\S+)/i)?.[1] ?? output,
2079
+ missingMessage: "UFW is not installed"
2080
+ },
2081
+ fail2ban: {
2082
+ versionCommand: "fail2ban-client --version 2>&1",
2083
+ parseVersion: (output) => output.match(/fail2ban\s+v?([\d.]+)/i)?.[1] ?? output,
2084
+ missingMessage: "fail2ban is not installed"
2085
+ }
2086
+ },
2087
+ // No `openresty` / `certbot` plans: nothing apt-installs an edge any more. Both
2088
+ // shipped inside the `kraft-edge` image, and their host-package plans (with the
2089
+ // openresty.org apt-repo codename probing that came with them) went with the
2090
+ // installers. `checks.openresty` stays — it parses the version out of the
2091
+ // CONTAINER. See installer.ts `installContainerEdge`.
2092
+ installs: {
2093
+ docker: dockerInstallPlan,
2094
+ git: (profile) => packagePlan(profile, "git", "git --version"),
2095
+ rsync: (profile) => packagePlan(profile, "rsync", "rsync --version | head -n 1"),
2096
+ // Package only, deliberately NOT enabled: `ufw enable` with no allow rule
2097
+ // severs the very SSH connection that ran it. Rules (and enabling) stay an
2098
+ // explicit operator act in the Firewall tab.
2099
+ ufw: (profile) => packagePlan(profile, "ufw", firewallVersionSteps("ufw")[0]),
2100
+ fail2ban: (profile) => packagePlan(profile, "fail2ban", "fail2ban-client --version 2>&1")
2101
+ }
2102
+ };
2103
+
2104
+ // ../../packages/adapters/src/system/available-version.ts
2105
+ var PACKAGE_NAMES = {
2106
+ git: "git",
2107
+ rsync: "rsync",
2108
+ ufw: "ufw",
2109
+ fail2ban: "fail2ban",
2110
+ // The edge is an IMAGE, not a repo package — its "newer version available" comes
2111
+ // from the registry (see the update scanner's imageDigest), never from apt.
2112
+ edge: null,
2113
+ docker: null
2114
+ };
2115
+ function normalizePkgVersion(raw) {
2116
+ let v = raw.trim();
2117
+ const colon = v.lastIndexOf(":");
2118
+ if (colon !== -1) v = v.slice(colon + 1);
2119
+ const dash = v.indexOf("-");
2120
+ if (dash !== -1) v = v.slice(0, dash);
2121
+ return v.trim();
2122
+ }
2123
+ var READ_CANDIDATE = {
2124
+ // `apt-cache policy` prints " Candidate: <version>".
2125
+ apt: (out) => out.match(/Candidate:\s*(\S+)/)?.[1]?.trim() ?? null,
2126
+ // Cached list; the last "pkg.arch version repo" row carries the newest avail.
2127
+ dnf: (out) => out.trim().split(/\s+/)[1]?.trim() ?? null,
2128
+ yum: (out) => out.trim().split(/\s+/)[1]?.trim() ?? null,
2129
+ // `apk policy` prints one ` <version>:` line per available version (colon at end
2130
+ // of line), each followed by indented source lines (installed marker, repo URLs);
2131
+ // the last version line is newest.
2132
+ apk: (out) => {
2133
+ const versions = [...out.matchAll(/^\s+([\w.+~-]+):\s*$/gm)].map(
2134
+ (m) => m[1]
2135
+ );
2136
+ return versions.length ? versions[versions.length - 1] : null;
2137
+ },
2138
+ // Both refuse the probe itself, so there is no output to read. Stated rather than
2139
+ // omitted — `null` here is paired with a refusal, not with a missing branch.
2140
+ brew: null,
2141
+ none: null
2142
+ };
2143
+ async function probeCandidate(executor, profile, pkg) {
2144
+ const read = READ_CANDIDATE[profile.packageManager];
2145
+ const probe = envOps(profile).pkgAvailableVersion(pkg);
2146
+ if (!read || !probe.supported) return null;
2147
+ const out = await tryExec(executor, opScript(probe.value));
2148
+ return out ? read(out) : null;
2149
+ }
2150
+ async function enrichAvailableVersions(executor, profile, statuses) {
2151
+ const targets = statuses.filter(
2152
+ (s) => s.installed && s.version && PACKAGE_NAMES[s.name]
2153
+ );
2154
+ await Promise.all(
2155
+ targets.map(async (s) => {
2156
+ try {
2157
+ const pkg = PACKAGE_NAMES[s.name];
2158
+ const candidate = await probeCandidate(executor, profile, pkg);
2159
+ if (!candidate) return;
2160
+ const avail = normalizePkgVersion(candidate);
2161
+ const installed = normalizePkgVersion(s.version);
2162
+ if (avail && compareSemver(avail, installed) > 0) {
2163
+ s.availableVersion = avail;
2164
+ s.updateAvailable = true;
2165
+ }
2166
+ } catch {
2167
+ }
2168
+ })
2169
+ );
2170
+ }
2171
+
2172
+ // ../../packages/adapters/src/system/components.ts
2173
+ var SYSTEM_COMPONENTS = [
2174
+ {
2175
+ name: "docker",
2176
+ label: "Docker",
2177
+ description: "Container runtime for deployments",
2178
+ installable: true,
2179
+ category: "core"
2180
+ },
2181
+ {
2182
+ name: "git",
2183
+ label: "Git",
2184
+ description: "Version control for source code",
2185
+ installable: true,
2186
+ category: "core"
2187
+ },
2188
+ {
2189
+ // ONE component for the edge, because it is ONE artifact: the kraft-edge
2190
+ // image. OpenResty, its Lua, and certbot all ship inside it — they were three
2191
+ // rows in this list for a thing that is installed, checked, and removed as a
2192
+ // unit, and neither "openresty" nor "certbot" was independently installable
2193
+ // on a converted box.
2194
+ name: "edge",
2195
+ label: "Edge",
2196
+ description: "Routing + TLS \u2014 the kraft-edge container (OpenResty, Lua, certbot)",
2197
+ installable: true,
2198
+ category: "infrastructure"
2199
+ },
2200
+ {
2201
+ name: "rsync",
2202
+ label: "rsync",
2203
+ description: "Fast directory sync for remote local-build transfers",
2204
+ installable: true,
2205
+ category: "infrastructure"
2206
+ },
2207
+ {
2208
+ name: "ufw",
2209
+ label: "UFW",
2210
+ description: "Host firewall frontend",
2211
+ installable: true,
2212
+ category: "infrastructure",
2213
+ proposeWhenMissing: true
2214
+ },
2215
+ {
2216
+ name: "fail2ban",
2217
+ label: "fail2ban",
2218
+ description: "Brute-force protection",
2219
+ installable: true,
2220
+ category: "infrastructure",
2221
+ proposeWhenMissing: true
2222
+ }
2223
+ ];
2224
+ var SYSTEM_COMPONENTS_BY_NAME = new Map(
2225
+ SYSTEM_COMPONENTS.map((component) => [component.name, component])
2226
+ );
2227
+ function getSystemComponentDefinition(name) {
2228
+ return SYSTEM_COMPONENTS_BY_NAME.get(name) ?? {
2229
+ name,
2230
+ label: name,
2231
+ description: `${name} component`,
2232
+ installable: false,
2233
+ category: "infrastructure"
2234
+ };
2235
+ }
2236
+
2237
+ // ../../packages/adapters/src/system/checks.ts
2238
+ function isSocketDenied(error) {
2239
+ return error != null && /permission denied|eacces/i.test(error);
2240
+ }
2241
+ function healthy(name, version, running) {
2242
+ const component = getSystemComponentDefinition(name);
2243
+ return {
2244
+ name,
2245
+ label: component.label,
2246
+ description: component.description,
2247
+ installable: component.installable,
2248
+ installed: true,
2249
+ version,
2250
+ running,
2251
+ healthy: running !== void 0 ? running : true,
2252
+ message: running ? `${name} ${version} - running` : `${name} ${version} - installed`
2253
+ };
2254
+ }
2255
+ function unhealthy(name, message, opts) {
2256
+ const component = getSystemComponentDefinition(name);
2257
+ return {
2258
+ name,
2259
+ label: component.label,
2260
+ description: component.description,
2261
+ installable: component.installable,
2262
+ installed: !!opts?.version,
2263
+ version: opts?.version,
2264
+ running: opts?.running,
2265
+ healthy: false,
2266
+ message
2267
+ };
2268
+ }
2269
+ async function checkDocker(executor) {
2270
+ const startedAt = Date.now();
2271
+ const recipe = systemCatalog.checks.docker;
2272
+ const version = await probeExec(executor, recipe.versionCommand, "checks");
2273
+ if (!version.output) {
2274
+ systemDebug("checks", `docker:missing (${formatDuration(startedAt)})`);
2275
+ return unhealthy(
2276
+ "docker",
2277
+ withReason(recipe.missingMessage, version.error)
2278
+ );
2279
+ }
2280
+ const parsed = recipe.parseVersion(version.output);
2281
+ const info = await probeExec(executor, recipe.daemonCommand, "checks");
2282
+ if (!info.output) {
2283
+ const denied = isSocketDenied(info.error);
2284
+ systemDebug(
2285
+ "checks",
2286
+ `docker:${denied ? "denied" : "not-running"} (${formatDuration(startedAt)}) ${info.error ?? "probe exited 0 with no server version"}`
2287
+ );
2288
+ return unhealthy(
2289
+ "docker",
2290
+ withReason(
2291
+ denied && recipe.deniedMessage || recipe.notRunningMessage,
2292
+ info.error
2293
+ ),
2294
+ {
2295
+ version: parsed,
2296
+ // Left UNSET when denied: `running: false` would be a fact we don't have. The
2297
+ // permission check happens before the daemon is consulted, so a refusal is
2298
+ // equally consistent with a healthy daemon, and asserting it is stopped is the
2299
+ // same class of guess the message above stopped making.
2300
+ ...denied ? {} : { running: false }
2301
+ }
2302
+ );
2303
+ }
2304
+ systemDebug("checks", `docker:healthy (${formatDuration(startedAt)})`);
2305
+ return healthy("docker", parsed, true);
2306
+ }
2307
+ async function checkGit(executor) {
2308
+ const startedAt = Date.now();
2309
+ const recipe = systemCatalog.checks.git;
2310
+ const version = await probeExec(executor, recipe.versionCommand, "checks");
2311
+ if (!version.output) {
2312
+ systemDebug("checks", `git:missing (${formatDuration(startedAt)})`);
2313
+ return unhealthy("git", withReason(recipe.missingMessage, version.error));
2314
+ }
2315
+ const parsed = recipe.parseVersion(version.output);
2316
+ systemDebug("checks", `git:healthy (${formatDuration(startedAt)})`);
2317
+ return healthy("git", parsed);
2318
+ }
2319
+ async function checkRsync(executor) {
2320
+ const startedAt = Date.now();
2321
+ const recipe = systemCatalog.checks.rsync;
2322
+ const version = await probeExec(executor, recipe.versionCommand, "checks");
2323
+ if (!version.output) {
2324
+ systemDebug("checks", `rsync:missing (${formatDuration(startedAt)})`);
2325
+ return unhealthy("rsync", withReason(recipe.missingMessage, version.error));
2326
+ }
2327
+ const parsed = recipe.parseVersion(version.output);
2328
+ systemDebug("checks", `rsync:healthy (${formatDuration(startedAt)})`);
2329
+ return healthy("rsync", parsed);
2330
+ }
2331
+ async function checkUfw(executor) {
2332
+ const startedAt = Date.now();
2333
+ const recipe = systemCatalog.checks.ufw;
2334
+ const version = await probeExec(executor, recipe.versionCommand, "checks");
2335
+ if (!version.output) {
2336
+ systemDebug("checks", `ufw:missing (${formatDuration(startedAt)})`);
2337
+ return unhealthy("ufw", withReason(recipe.missingMessage, version.error));
2338
+ }
2339
+ const parsed = recipe.parseVersion(version.output);
2340
+ const status = await probeExecElevated(
2341
+ executor,
2342
+ firewallActiveSteps("ufw")[0],
2343
+ "checks"
2344
+ );
2345
+ const observed = (status.output ?? "").split("\n")[0]?.trim() ?? "";
2346
+ if (/Status:\s*active/i.test(observed)) {
2347
+ systemDebug("checks", `ufw:healthy (${formatDuration(startedAt)})`);
2348
+ return healthy("ufw", parsed, true);
2349
+ }
2350
+ if (/Status:\s*inactive/i.test(observed)) {
2351
+ systemDebug("checks", `ufw:inactive (${formatDuration(startedAt)})`);
2352
+ return unhealthy(
2353
+ "ufw",
2354
+ `UFW is installed but reports "${observed}" \u2014 enable it on the host after allowing SSH.`,
2355
+ {
2356
+ version: parsed,
2357
+ running: false
2358
+ }
2359
+ );
2360
+ }
2361
+ systemDebug("checks", `ufw:unreadable (${formatDuration(startedAt)})`);
2362
+ return unhealthy(
2363
+ "ufw",
2364
+ withReason(
2365
+ "UFW is installed but its status could not be read here",
2366
+ status.error
2367
+ ),
2368
+ { version: parsed }
2369
+ );
2370
+ }
2371
+ async function checkFail2ban(executor) {
2372
+ const startedAt = Date.now();
2373
+ const recipe = systemCatalog.checks.fail2ban;
2374
+ const version = await probeExec(executor, recipe.versionCommand, "checks");
2375
+ if (!version.output) {
2376
+ systemDebug("checks", `fail2ban:missing (${formatDuration(startedAt)})`);
2377
+ return unhealthy(
2378
+ "fail2ban",
2379
+ withReason(recipe.missingMessage, version.error)
2380
+ );
2381
+ }
2382
+ const parsed = recipe.parseVersion(version.output);
2383
+ const pong = await probeExecElevated(
2384
+ executor,
2385
+ "fail2ban-client ping 2>&1",
2386
+ "checks"
2387
+ );
2388
+ if (/pong/i.test(pong.output ?? "")) {
2389
+ systemDebug("checks", `fail2ban:healthy (${formatDuration(startedAt)})`);
2390
+ return healthy("fail2ban", parsed, true);
2391
+ }
2392
+ const cause = (pong.error ?? "").replace(
2393
+ /^.*fail2ban\[\d+\]:\s*(ERROR\s*)?/i,
2394
+ ""
2395
+ );
2396
+ systemDebug("checks", `fail2ban:not-running (${formatDuration(startedAt)})`);
2397
+ return unhealthy(
2398
+ "fail2ban",
2399
+ withReason("fail2ban is installed but not running", cause),
2400
+ {
2401
+ version: parsed,
2402
+ running: false
2403
+ }
2404
+ );
2405
+ }
2406
+ async function checkEdge(executor) {
2407
+ const startedAt = Date.now();
2408
+ const container = await resolveOurEdgeContainer(executor);
2409
+ if (!container) {
2410
+ systemDebug("checks", `edge:missing (${formatDuration(startedAt)})`);
2411
+ return unhealthy(
2412
+ "edge",
2413
+ "No edge on this server. The edge is the kraft-edge container \u2014 install it (requires Docker)."
2414
+ );
2415
+ }
2416
+ const verdict = await verifyEdgeServing(executor, container);
2417
+ if (!verdict.serving) {
2418
+ systemDebug("checks", `edge:not-serving (${formatDuration(startedAt)})`);
2419
+ return unhealthy(
2420
+ "edge",
2421
+ `The edge container ${container} is not serving \u2014 ${verdict.reason ?? "it is not listening on :80"}`,
2422
+ { running: false }
2423
+ );
2424
+ }
2425
+ const version = await probeExec(
2426
+ executor,
2427
+ containerCommand(container, "openresty -v 2>&1"),
2428
+ "checks"
2429
+ );
2430
+ if (!version.output) {
2431
+ systemDebug(
2432
+ "checks",
2433
+ `edge:container-unresponsive (${formatDuration(startedAt)})`
2434
+ );
2435
+ return unhealthy(
2436
+ "edge",
2437
+ withReason(
2438
+ `The edge container ${container} is serving on :80 but not answering \u2014 check \`docker logs ${container}\``,
2439
+ version.error
2440
+ ),
2441
+ { running: false }
2442
+ );
2443
+ }
2444
+ systemDebug("checks", `edge:healthy (${formatDuration(startedAt)})`);
2445
+ return healthy(
2446
+ "edge",
2447
+ systemCatalog.checks.openresty.parseVersion(version.output),
2448
+ true
2449
+ );
2450
+ }
2451
+ var COMPONENT_CHECKS = {
2452
+ docker: checkDocker,
2453
+ edge: checkEdge,
2454
+ git: checkGit,
2455
+ rsync: checkRsync,
2456
+ ufw: checkUfw,
2457
+ fail2ban: checkFail2ban
2458
+ };
2459
+ async function mapWithConcurrency(items, limit, fn) {
2460
+ const results = new Array(items.length);
2461
+ let cursor = 0;
2462
+ const worker = async () => {
2463
+ while (cursor < items.length) {
2464
+ const index = cursor++;
2465
+ results[index] = await fn(items[index]);
2466
+ }
2467
+ };
2468
+ await Promise.all(
2469
+ Array.from({ length: Math.min(limit, items.length) }, worker)
2470
+ );
2471
+ return results;
2472
+ }
2473
+ var CHECK_CONCURRENCY = 4;
2474
+ async function checkAll(executor) {
2475
+ const startedAt = Date.now();
2476
+ const entries = SYSTEM_COMPONENTS.map(
2477
+ (component) => [component.name, COMPONENT_CHECKS[component.name]]
2478
+ ).filter((entry) => Boolean(entry[1]));
2479
+ systemDebug(
2480
+ "checks",
2481
+ `checkAll:start [${entries.map(([name]) => name).join(", ")}]`
2482
+ );
2483
+ const results = await mapWithConcurrency(
2484
+ entries,
2485
+ CHECK_CONCURRENCY,
2486
+ ([, fn]) => fn(executor)
2487
+ );
2488
+ await enrichAvailable(executor, results);
2489
+ systemDebug("checks", `checkAll:done (${formatDuration(startedAt)})`);
2490
+ return results;
2491
+ }
2492
+ async function enrichAvailable(executor, results) {
2493
+ try {
2494
+ const profile = await resolveEnvironment(executor);
2495
+ await enrichAvailableVersions(executor, profile, results);
2496
+ } catch {
2497
+ }
2498
+ }
2499
+ async function checkComponents(executor, names) {
2500
+ const startedAt = Date.now();
2501
+ const fns = names.map((name) => COMPONENT_CHECKS[name]).filter((fn) => Boolean(fn));
2502
+ systemDebug("checks", `checkComponents:start [${names.join(", ")}]`);
2503
+ const results = await mapWithConcurrency(
2504
+ fns,
2505
+ CHECK_CONCURRENCY,
2506
+ (fn) => fn(executor)
2507
+ );
2508
+ await enrichAvailable(executor, results);
2509
+ systemDebug(
2510
+ "checks",
2511
+ `checkComponents:done [${names.join(", ")}] (${formatDuration(startedAt)})`
2512
+ );
2513
+ return results;
2514
+ }
2515
+
2516
+ // ../../packages/adapters/src/system/installer.ts
2517
+ function log3(message, level = "info") {
2518
+ return { timestamp: (/* @__PURE__ */ new Date()).toISOString(), message, level };
2519
+ }
2520
+ async function prepareExecutor(executor, component) {
2521
+ const grant = await privilegedExecutor(executor, `Installing ${component}`);
2522
+ if (!grant.supported) {
2523
+ return {
2524
+ ok: false,
2525
+ result: { component, success: false, error: grant.reason }
2526
+ };
2527
+ }
2528
+ return {
2529
+ ok: true,
2530
+ executor: grant.value.executor,
2531
+ profile: grant.value.profile
2532
+ };
2533
+ }
2534
+ function describeInstallFailure(label, code, output) {
2535
+ const lines = output.split("\n").map((line) => line.replace(/\r/g, "").trim()).filter(Boolean);
2536
+ const flagged = lines.filter(
2537
+ (line) => /^(e:|err:|error:|dpkg:|fatal:)/i.test(line)
2538
+ );
2539
+ const picked = (flagged.length ? flagged : lines).slice(-3).join(" \xB7 ");
2540
+ const detail = picked.length > 400 ? `${picked.slice(0, 399)}\u2026` : picked;
2541
+ const held = /held packages?|--allow-change-held-packages/i.test(output) ? " Packages are held on this host (`apt-mark hold`) and Kraft does not override a hold \u2014 unhold them first if you do want this install." : "";
2542
+ return `${label} install failed (exit ${code})${detail ? `: ${detail}` : ""}.${held}`;
2543
+ }
2544
+ async function probeDocker(executor) {
2545
+ const status = await checkDocker(executor);
2546
+ const version = status.version;
2547
+ if (!status.installed) return { verdict: "missing", message: status.message };
2548
+ if (!status.healthy)
2549
+ return { verdict: "stopped", version, message: status.message };
2550
+ if (version && compareSemver(version, MIN_DOCKER_VERSION) < 0) {
2551
+ return { verdict: "outdated", version, message: status.message };
2552
+ }
2553
+ return { verdict: "ok", version, message: status.message };
2554
+ }
2555
+ function dockerAlreadyThere(present, onLog) {
2556
+ const named = present.version ? `Docker ${present.version}` : "Docker";
2557
+ onLog(
2558
+ log3(
2559
+ `${named} is installed and its daemon is answering \u2014 nothing to install`
2560
+ )
2561
+ );
2562
+ return { component: "docker", success: true, version: present.version };
2563
+ }
2564
+ async function installDocker(executor, onLog, config) {
2565
+ const forced = config?.reinstall === true;
2566
+ let present = forced ? null : await probeDocker(executor);
2567
+ if (present?.verdict === "ok") return dockerAlreadyThere(present, onLog);
2568
+ const prep = await prepareExecutor(executor, "docker");
2569
+ if (!prep.ok) return prep.result;
2570
+ executor = prep.executor;
2571
+ const profile = prep.profile;
2572
+ if (present && present.verdict !== "missing" && !profile.isRoot) {
2573
+ present = await probeDocker(executor);
2574
+ if (present.verdict === "ok") return dockerAlreadyThere(present, onLog);
2575
+ }
2576
+ const plan = systemCatalog.installs.docker(profile);
2577
+ const service = plan.supported ? plan.value.service : plan;
2578
+ if (present?.verdict === "stopped" && service?.supported) {
2579
+ onLog(log3(`${present.message} \u2014 starting the Docker service`, "warn"));
2580
+ await executor.streamExec(service.value, onLog).catch(() => void 0);
2581
+ present = await probeDocker(executor);
2582
+ if (present.verdict === "ok") {
2583
+ onLog(log3(`Docker ${present.version} is running`));
2584
+ return { component: "docker", success: true, version: present.version };
2585
+ }
2586
+ }
2587
+ if (present && present.verdict !== "missing") {
2588
+ const cannotStart = present.verdict === "stopped" && service && !service.supported ? ` Kraft could not start it for you: ${service.reason}` : "";
2589
+ const error = (present.verdict === "outdated" ? `Docker ${present.version} is installed but Kraft needs ${MIN_DOCKER_VERSION} or newer. Upgrading Docker restarts the daemon and every container on this server, so it is never done as part of another step \u2014 upgrade it on the host, or use Reinstall on the Docker component to run the official installer.` : `${present.message}. Start Docker on this server and retry. Kraft will not reinstall Docker over a daemon that is merely unreachable: the installer would also upgrade the engine and restart every container on this host. Use Reinstall on the Docker component if that is what you want.`) + cannotStart;
2590
+ onLog(log3(error, "error"));
2591
+ return {
2592
+ component: "docker",
2593
+ success: false,
2594
+ version: present.version,
2595
+ error
2596
+ };
2597
+ }
2598
+ if (!plan.supported) {
2599
+ onLog(log3(plan.reason, "error"));
2600
+ return { component: "docker", success: false, error: plan.reason };
2601
+ }
2602
+ onLog(
2603
+ forced ? log3(
2604
+ "Running the Docker installer \u2014 it may upgrade the engine, which restarts the daemon and every container on this host...",
2605
+ "warn"
2606
+ ) : log3("Installing Docker Engine...")
2607
+ );
2608
+ try {
2609
+ const { code, output } = await executor.streamExec(
2610
+ plan.value.installCommand,
2611
+ onLog
2612
+ );
2613
+ if (code !== 0) {
2614
+ const error = describeInstallFailure("Docker", code, output);
2615
+ onLog(log3(error, "error"));
2616
+ return { component: "docker", success: false, error };
2617
+ }
2618
+ if (service?.supported) {
2619
+ onLog(log3("Starting Docker service..."));
2620
+ await executor.streamExec(
2621
+ service.value,
2622
+ onLog
2623
+ );
2624
+ } else if (service) {
2625
+ onLog(
2626
+ log3(
2627
+ `Docker is installed but Kraft cannot start it here: ${service.reason}`,
2628
+ "warn"
2629
+ )
2630
+ );
2631
+ }
2632
+ const version = await executor.exec(plan.value.verifyCommand);
2633
+ const parsed = systemCatalog.checks.docker.parseVersion(version);
2634
+ onLog(log3(`Docker ${parsed} installed`));
2635
+ return { component: "docker", success: true, version: parsed };
2636
+ } catch (err) {
2637
+ const msg = safeErrorMessage(err);
2638
+ onLog(log3(`Docker installation failed: ${msg}`, "error"));
2639
+ return { component: "docker", success: false, error: msg };
2640
+ }
2641
+ }
2642
+ async function installPackaged(executor, onLog, component, label, where) {
2643
+ const prep = await prepareExecutor(executor, component);
2644
+ if (!prep.ok) return prep.result;
2645
+ executor = prep.executor;
2646
+ const plan = systemCatalog.installs[component](prep.profile);
2647
+ if (!plan.supported) {
2648
+ onLog(log3(plan.reason, "error"));
2649
+ return { component, success: false, error: plan.reason };
2650
+ }
2651
+ const inside = where ? ` in ${where}` : "";
2652
+ onLog(log3(`Installing ${label}${inside}...`));
2653
+ try {
2654
+ const { code, output } = await executor.streamExec(
2655
+ plan.value.installCommand,
2656
+ onLog
2657
+ );
2658
+ if (code !== 0) {
2659
+ const error = describeInstallFailure(label, code, output);
2660
+ onLog(log3(error, "error"));
2661
+ return { component, success: false, error };
2662
+ }
2663
+ const version = await executor.exec(plan.value.verifyCommand);
2664
+ const parsed = systemCatalog.checks[component].parseVersion(version);
2665
+ onLog(log3(`${label} ${parsed} installed${inside}`));
2666
+ return { component, success: true, version: parsed };
2667
+ } catch (err) {
2668
+ const msg = safeErrorMessage(err);
2669
+ onLog(log3(`${label} installation failed: ${msg}`, "error"));
2670
+ return { component, success: false, error: msg };
2671
+ }
2672
+ }
2673
+ function installGit(executor, onLog, opts) {
2674
+ return installPackaged(executor, onLog, "git", "Git", opts?.label);
2675
+ }
2676
+ function installRsync(executor, onLog) {
2677
+ return installPackaged(executor, onLog, "rsync", "rsync");
2678
+ }
2679
+ function installUfw(executor, onLog) {
2680
+ return installPackaged(executor, onLog, "ufw", "UFW");
2681
+ }
2682
+ function installFail2ban(executor, onLog) {
2683
+ return installPackaged(executor, onLog, "fail2ban", "fail2ban");
2684
+ }
2685
+ async function installContainerEdge(executor, onLog, config) {
2686
+ const prep = await prepareExecutor(executor, "edge");
2687
+ if (!prep.ok) return prep.result;
2688
+ executor = prep.executor;
2689
+ if (!await dockerAvailable(executor)) {
2690
+ const error = "The edge is a container image and needs Docker on this server. Install the Docker component first, then install the edge.";
2691
+ onLog(log3(error, "error"));
2692
+ return { component: "edge", success: false, error };
2693
+ }
2694
+ try {
2695
+ const result = await ensureContainerEdge(executor, {
2696
+ onLog,
2697
+ config,
2698
+ image: config?.edgeImage
2699
+ });
2700
+ if (result.converted) {
2701
+ onLog(
2702
+ log3("Edge moved from host OpenResty to the container edge", "warn")
2703
+ );
2704
+ }
2705
+ const version = await executor.exec(containerCommand(result.container, "openresty -v 2>&1")).catch(() => "");
2706
+ return {
2707
+ component: "edge",
2708
+ success: true,
2709
+ version: version ? systemCatalog.checks.openresty.parseVersion(version) : void 0
2710
+ };
2711
+ } catch (err) {
2712
+ if (err instanceof EdgeMigrateRequested) throw err;
2713
+ const msg = safeErrorMessage(err);
2714
+ onLog(log3(`Edge setup failed: ${msg}`, "error"));
2715
+ return { component: "edge", success: false, error: msg };
2716
+ }
2717
+ }
2718
+ var COMPONENT_INSTALLERS = {
2719
+ docker: (exec, log6, config) => installDocker(exec, log6, config),
2720
+ // The edge is ONE component and it is a container image — there is no
2721
+ // host-OpenResty or host-certbot installer to reach for any more.
2722
+ edge: (exec, log6, config) => installContainerEdge(exec, log6, config),
2723
+ git: (exec, log6) => installGit(exec, log6),
2724
+ rsync: (exec, log6) => installRsync(exec, log6),
2725
+ ufw: (exec, log6) => installUfw(exec, log6),
2726
+ fail2ban: (exec, log6) => installFail2ban(exec, log6)
2727
+ };
2728
+
2729
+ // ../../packages/adapters/src/system/proxy/index.ts
2730
+ async function importSites(executor, status) {
2731
+ const proxy = status.occupants.find((o) => o.proxy)?.proxy;
2732
+ if (!proxy) return { proxy: "nginx", sites: [], warnings: [] };
2733
+ if (canImportProxy(proxy)) return scanImportableSites(executor, proxy);
2734
+ return {
2735
+ proxy,
2736
+ sites: [],
2737
+ warnings: [`${proxy}: config import not supported \u2014 takeover only`]
2738
+ };
2739
+ }
2740
+ async function foreignProxyOnEdge(executor) {
2741
+ const status = await probeEdge(executor);
2742
+ const blocked = !status.canProceedClean && status.occupants.length > 0;
2743
+ return { status, blocked, owner: describeEdgeOwner(status.occupants) };
2744
+ }
2745
+ function describeEdgeOwner(occupants) {
2746
+ const labels = [];
2747
+ const seen = /* @__PURE__ */ new Set();
2748
+ for (const o of occupants) {
2749
+ const identity = o.systemdUnit ?? o.containerName ?? (o.pid ? `pid:${o.pid}` : o.command ?? `port:${o.port}`);
2750
+ if (seen.has(identity)) continue;
2751
+ seen.add(identity);
2752
+ labels.push(
2753
+ o.systemdUnit ?? (o.containerName ? `docker container ${o.containerName}` : void 0) ?? (o.proxy && o.pid ? `${o.proxy} (PID ${o.pid})` : void 0) ?? o.proxy ?? o.command ?? `port ${o.port}`
2754
+ );
2755
+ }
2756
+ return labels.join(", ");
2757
+ }
2758
+
2759
+ // ../../packages/adapters/src/system/proxy/consent.ts
2760
+ function log4(message, level = "info") {
2761
+ return { timestamp: (/* @__PURE__ */ new Date()).toISOString(), message, level };
2762
+ }
2763
+ async function ensureEdgeClear(executor, config, onLog) {
2764
+ const status = await probeEdge(executor);
2765
+ if (status.canProceedClean) return { tookOver: false, status };
2766
+ const takeover = async () => {
2767
+ onLog(
2768
+ log4(
2769
+ `Taking over ports from ${status.occupants.map((o) => o.command ?? o.port).join(", ")}...`,
2770
+ "warn"
2771
+ )
2772
+ );
2773
+ const freed = await beginEdgeTakeover(
2774
+ executor,
2775
+ status,
2776
+ onLog,
2777
+ config?.edgePolicy?.stopTargets ?? []
2778
+ );
2779
+ if (!freed.freed) {
2780
+ await rollbackEdgeTakeover(executor, onLog);
2781
+ const plural = freed.stillBound.length > 1;
2782
+ const ports = `port${plural ? "s" : ""} ${freed.stillBound.join(" and ")} ${plural ? "are" : "is"} still in use, so the edge can't bind. Nothing was installed`;
2783
+ throw new AppError(
2784
+ // The advice has to match the actual cause. Told to hunt for another holder, an
2785
+ // operator retries the same unprivileged takeover forever — and "stopped the
2786
+ // existing proxy" is untrue on this arm, since the stop is what was refused.
2787
+ freed.privilegeDegraded ? `The ${ports} \u2014 and Kraft could not run the stop as root on this host (see the privilege warning above), so stopping the existing proxy was almost certainly refused. Retrying as this user will fail the same way: reconnect as root, or as a user with passwordless sudo.` : `Stopped the existing proxy, but ${ports} and the previous proxy has been restored \u2014 find what else is holding the port and retry.`,
2788
+ 409,
2789
+ "EDGE_PORTS_STILL_BOUND"
2790
+ );
2791
+ }
2792
+ };
2793
+ if (config?.edgePolicy?.mode === "takeover") {
2794
+ await takeover();
2795
+ return { tookOver: true, status };
2796
+ }
2797
+ if (config?.promptUser) {
2798
+ const known = status.classification === "known";
2799
+ const owner = status.occupants.map((o) => o.command ?? `port ${o.port}`).join(", ");
2800
+ const scan = await importSites(executor, status);
2801
+ const migratable = scan.sites.length > 0;
2802
+ const message = migratable ? `Kraft runs its own load balancer (OpenResty) on ports 80 and 443, but ${owner} is already serving them (${scan.sites.length} site${scan.sites.length === 1 ? "" : "s"}). Migrate those sites into Kraft and take over, just stop it and take over, or cancel?` : known ? `Kraft runs its own load balancer (OpenResty) on ports 80 and 443, but ${owner} is already serving them. Stop it and take over, or cancel and leave it running?` : `Kraft runs its own load balancer (OpenResty) on ports 80 and 443, but ${owner} is already using them and we can't identify it. Stop it and take over, or cancel and leave it running?`;
2803
+ const details = {
2804
+ edge: status,
2805
+ sites: scan.sites,
2806
+ warnings: scan.warnings
2807
+ };
2808
+ const action = await config.promptUser({
2809
+ promptId: "edge_conflict",
2810
+ title: known ? "Existing reverse proxy detected" : "Ports 80/443 are in use",
2811
+ message,
2812
+ actions: [
2813
+ ...migratable ? [
2814
+ {
2815
+ id: "migrate",
2816
+ label: `Migrate ${scan.sites.length} site(s) & take over`,
2817
+ variant: "primary"
2818
+ }
2819
+ ] : [],
2820
+ { id: "override", label: "Stop it & take over", variant: "danger" },
2821
+ { id: "cancel", label: "Cancel", variant: "secondary" }
2822
+ ],
2823
+ details
2824
+ });
2825
+ if (action === "migrate") {
2826
+ throw new EdgeMigrateRequested(status, scan.sites, scan.warnings);
2827
+ }
2828
+ if (action === "override") {
2829
+ await takeover();
2830
+ return { tookOver: true, status };
2831
+ }
2832
+ throw new EdgeConflictError(status);
2833
+ }
2834
+ throw new EdgeConflictError(status);
2835
+ }
2836
+
2837
+ // ../../packages/adapters/src/system/proxy/ensure-container-edge.ts
2838
+ function log5(message, level = "info") {
2839
+ return { timestamp: (/* @__PURE__ */ new Date()).toISOString(), message, level };
2840
+ }
2841
+ var injectedDefaultImage;
2842
+ function setDefaultEdgeImage(image) {
2843
+ injectedDefaultImage = image?.trim() || void 0;
2844
+ }
2845
+ function resolveEdgeImage(explicit) {
2846
+ return buildEdgeImageRef({ explicit, injectedDefault: injectedDefaultImage });
2847
+ }
2848
+ function edgeHostExecutor(executor) {
2849
+ return rootOrDegrade(executor, {
2850
+ purpose: "Configuring routing and TLS",
2851
+ consequence: "Vhosts and certificates may not be writable, and routing will be incomplete \u2014 the deploy continues and the app still runs on its port.",
2852
+ report: (message) => console.error(`[edge] ${message}`)
2853
+ });
2854
+ }
2855
+ async function containerEdgeProvider(executor, container, opts) {
2856
+ const { NginxProvider: Provider } = await import("./nginx-VITTWZZU.js");
2857
+ return new Provider({
2858
+ ...opts,
2859
+ // Vhosts go to the BIND-MOUNTED HOST dir (so every host-side reader — the
2860
+ // migrate proxy scan, cert reuse, the mail cert symlinks — keeps working),
2861
+ // while reload/certbot run inside the container.
2862
+ paths: EDGE_HOST_PATHS,
2863
+ executor: edgeContainerExecutor(
2864
+ await edgeHostExecutor(executor),
2865
+ container
2866
+ ),
2867
+ // Challenge tokens go to the HOST side of the ACME mount, for the same reason
2868
+ // vhosts do. This is the ONE mount whose two sides differ
2869
+ // (/var/lib/kraft/edge/acme → /var/www/acme), so the write path and the
2870
+ // vhost's `root` are genuinely different strings — `root` stays the container
2871
+ // path. Overridden here and nowhere else, next to the `paths` override it
2872
+ // mirrors, so the two cannot drift.
2873
+ challengeDir: EDGE_CHALLENGE_HOST_DIR,
2874
+ // MUST be pinned: re-detection answers from inside the container and would
2875
+ // repoint sitesDir at a host dir the edge never reads.
2876
+ pinPaths: true,
2877
+ // Reload runs INSIDE the container, where the master is pid 1 — so a failed
2878
+ // reload must never fall back to killing it (#292).
2879
+ containerEdge: true
2880
+ });
2881
+ }
2882
+ async function localContainerEdgeProvider(container, opts) {
2883
+ const { DockerEdgeExecutor } = await import("./docker-edge-executor-VO6JHHW2.js");
2884
+ const { NginxProvider: Provider } = await import("./nginx-VITTWZZU.js");
2885
+ return new Provider({
2886
+ ...opts,
2887
+ paths: OPENRESTY_DEFAULT_PATHS,
2888
+ // The one place the privilege gate is the WRONG instrument, so the reason is stated
2889
+ // instead: this executor's commands run inside the EDGE container, so probing
2890
+ // through it would measure that container's interior and brand the result with a
2891
+ // verdict about the wrong machine. Privilege here comes from the deployment shape —
2892
+ // the api process is root in its own container, the vhost dir is a bind mount it
2893
+ // owns, and the daemon socket is mounted in. There is no login to elevate.
2894
+ executor: rootChecked(
2895
+ new DockerEdgeExecutor({ containerName: container }),
2896
+ "compose: api is root in its own container, writing shared mounts over a mounted socket"
2897
+ ),
2898
+ // nginx.conf + the Lua are BAKED into the image — nothing to detect, install
2899
+ // or patch, and detection would answer from inside the container anyway.
2900
+ pinPaths: true,
2901
+ // `docker exec`s into the edge, so the master is pid 1 there too (#292).
2902
+ containerEdge: true
2903
+ });
2904
+ }
2905
+ async function startEdgeContainer(executor, container, image, onLog, mounts) {
2906
+ await sanitizeEdgeVhosts(executor, EDGE_HOST_PATHS.sitesDir, onLog).catch(
2907
+ () => {
2908
+ }
2909
+ );
2910
+ await executor.exec(`docker rm -f ${sq(container)} 2>/dev/null || true`).catch(() => {
2911
+ });
2912
+ const run = await executor.streamExec(
2913
+ buildEdgeRunCommand(container, image, mounts),
2914
+ onLog
2915
+ );
2916
+ invalidateEdgeContainer(executor);
2917
+ return run.code === 0;
2918
+ }
2919
+ var MOUNT_RESOLVE_MARKER = "__KRAFT_MOUNT__";
2920
+ async function resolveEdgeContainerMounts(executor) {
2921
+ const script = EDGE_CONTAINER_MOUNTS.map(
2922
+ (mount) => `echo ${MOUNT_RESOLVE_MARKER}; (cd ${sq(mount.host)} && pwd -P) 2>&1`
2923
+ ).join("; ") + "; true";
2924
+ let raw;
2925
+ try {
2926
+ raw = await executor.exec(script);
2927
+ } catch (err) {
2928
+ throw new Error(
2929
+ `Could not resolve edge bind-mount sources on the target host: ${safeErrorMessage(err)}`
2930
+ );
2931
+ }
2932
+ const segments = raw.split(MOUNT_RESOLVE_MARKER).slice(1);
2933
+ if (segments.length !== EDGE_CONTAINER_MOUNTS.length) {
2934
+ throw new Error(
2935
+ `Could not resolve edge bind-mount sources on the target host: expected ${EDGE_CONTAINER_MOUNTS.length} paths, received ${JSON.stringify(raw)}.`
2936
+ );
2937
+ }
2938
+ return EDGE_CONTAINER_MOUNTS.map((mount, i) => {
2939
+ const host = segments[i].trim();
2940
+ if (!host.startsWith("/") || host.includes("\n")) {
2941
+ throw new Error(
2942
+ `Could not resolve edge bind-mount source ${mount.host} on the target host: expected one absolute path, received ${JSON.stringify(host)}.`
2943
+ );
2944
+ }
2945
+ return { host, container: mount.container };
2946
+ });
2947
+ }
2948
+ function buildEdgeRunCommand(container, image, edgeMounts = EDGE_CONTAINER_MOUNTS) {
2949
+ const mounts = edgeMounts.map(
2950
+ // `:z` relabels for SELinux-enforcing hosts; a no-op elsewhere.
2951
+ (m) => `-v ${sq(`${m.host}:${m.container}:z`)}`
2952
+ ).join(" ");
2953
+ return [
2954
+ "docker run -d",
2955
+ `--name ${sq(container)}`,
2956
+ "--network host",
2957
+ "--restart unless-stopped",
2958
+ mounts,
2959
+ sq(image)
2960
+ ].join(" ");
2961
+ }
2962
+ async function edgeContainerMountsMatch(executor, container, expected) {
2963
+ let raw;
2964
+ try {
2965
+ raw = await executor.exec(
2966
+ `docker inspect -f '{{range .HostConfig.Binds}}{{println .}}{{end}}' ${sq(container)}`
2967
+ );
2968
+ } catch {
2969
+ return null;
2970
+ }
2971
+ const actual = /* @__PURE__ */ new Map();
2972
+ for (const line of raw.split("\n")) {
2973
+ const [source, destination] = line.trim().split(":");
2974
+ if (!source || !destination) continue;
2975
+ actual.set(destination, source);
2976
+ }
2977
+ return expected.every((mount) => actual.get(mount.container) === mount.host);
2978
+ }
2979
+ function makeEdgeStart(executor, container, opts, mounts) {
2980
+ const { onLog } = opts;
2981
+ return async (image) => {
2982
+ if (!await startEdgeContainer(executor, container, image, onLog, mounts))
2983
+ return false;
2984
+ const verdict = await verifyEdgeServing(executor, container, {
2985
+ timeoutMs: opts.verifyTimeoutMs ?? 3e4
2986
+ });
2987
+ if (!verdict.serving && verdict.reason)
2988
+ onLog(log5(`Edge ${image}: ${verdict.reason}`, "warn"));
2989
+ return verdict.serving;
2990
+ };
2991
+ }
2992
+ async function verifyEdgeServing(executor, container, opts = {}) {
2993
+ if (await edgeIsBroken(executor, container)) {
2994
+ const reason = await edgeCrashReason(executor, container);
2995
+ return {
2996
+ serving: false,
2997
+ reason: reason ?? "the edge container is not running"
2998
+ };
2999
+ }
3000
+ if (opts.configTest) {
3001
+ try {
3002
+ await executor.exec(containerCommand(container, "openresty -t 2>&1"));
3003
+ } catch (err) {
3004
+ const out = safeErrorMessage(err);
3005
+ return { serving: false, reason: edgeFailureReason(out) ?? out };
3006
+ }
3007
+ }
3008
+ const listening = await waitForPortListening(executor, 80, {
3009
+ timeoutMs: opts.timeoutMs ?? 0
3010
+ });
3011
+ if (listening.checked && !listening.listening) {
3012
+ const reason = await edgeCrashReason(executor, container);
3013
+ return {
3014
+ serving: false,
3015
+ reason: reason ?? "the edge container is up but nothing is listening on :80"
3016
+ };
3017
+ }
3018
+ return { serving: true };
3019
+ }
3020
+ async function ensureContainerEdge(executor, opts) {
3021
+ const { onLog } = opts;
3022
+ const container = opts.container?.trim() || EDGE_CONTAINER_NAME;
3023
+ const image = resolveEdgeImage(opts.image);
3024
+ let resolvedMounts = null;
3025
+ const targetMounts = async (checked) => {
3026
+ if (resolvedMounts) return resolvedMounts;
3027
+ const host = checked ?? await rootOrDegrade(executor, {
3028
+ purpose: "Resolving the edge's bind-mount sources",
3029
+ consequence: "Docker may mount a different host directory than Kraft writes.",
3030
+ report: (message) => onLog(log5(message, "warn"))
3031
+ });
3032
+ resolvedMounts = await resolveEdgeContainerMounts(host);
3033
+ return resolvedMounts;
3034
+ };
3035
+ const existing = await resolveOurEdgeContainer(executor, { fresh: true });
3036
+ if (existing) {
3037
+ const verdict = await verifyEdgeServing(executor, existing);
3038
+ if (!verdict.serving) {
3039
+ onLog(
3040
+ log5(
3041
+ `The edge container exists but isn't serving: ${verdict.reason}`,
3042
+ "warn"
3043
+ )
3044
+ );
3045
+ onLog(log5("Recreating it...", "warn"));
3046
+ } else {
3047
+ const mounts2 = await targetMounts();
3048
+ const mountsMatch = await edgeContainerMountsMatch(
3049
+ executor,
3050
+ existing,
3051
+ mounts2
3052
+ );
3053
+ if (mountsMatch === false) {
3054
+ onLog(
3055
+ log5(
3056
+ "The edge container uses stale or non-canonical bind mounts; recreating it...",
3057
+ "warn"
3058
+ )
3059
+ );
3060
+ } else {
3061
+ const current = await containerImageRef(executor, existing);
3062
+ const stale = current != null && current !== image;
3063
+ if (stale) {
3064
+ const start = makeEdgeStart(executor, existing, opts, mounts2);
3065
+ const swap = await swapManagedImage(executor, {
3066
+ kind: "edge",
3067
+ from: current ?? image,
3068
+ to: image,
3069
+ label: "edge",
3070
+ onLog,
3071
+ start
3072
+ });
3073
+ return {
3074
+ container: existing,
3075
+ image,
3076
+ converted: false,
3077
+ updated: swap.swapped,
3078
+ edgeDown: swap.down
3079
+ };
3080
+ }
3081
+ return { container: existing, image, converted: false };
3082
+ }
3083
+ }
3084
+ }
3085
+ if (!await dockerAvailable(executor)) {
3086
+ throw new Error(
3087
+ "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."
3088
+ );
3089
+ }
3090
+ if (!await imageExistsLocally(executor, image)) {
3091
+ if (managedImagesAreFromSource("edge")) {
3092
+ throw new Error(fromSourceImageMissingMessage("edge", image));
3093
+ }
3094
+ onLog(log5(`Pulling edge image ${image}...`));
3095
+ const pull = await executor.streamExec(
3096
+ `docker pull ${sq(image)}`,
3097
+ onLog
3098
+ );
3099
+ if (pull.code !== 0) {
3100
+ throw new Error(
3101
+ `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.`
3102
+ );
3103
+ }
3104
+ }
3105
+ const hostState = await rootOrDegrade(executor, {
3106
+ purpose: "Creating the edge's state directories",
3107
+ consequence: "Docker will create them empty, so vhosts and certificates may not persist.",
3108
+ report: (message) => onLog(log5(message, "warn"))
3109
+ });
3110
+ for (const mount of EDGE_CONTAINER_MOUNTS) {
3111
+ await hostState.exec(`mkdir -p ${sq(mount.host)}`).catch((err) => {
3112
+ onLog(
3113
+ log5(
3114
+ `Could not create the edge state directory ${mount.host}: ${safeErrorMessage(err)}. Docker will create it empty, so vhosts and certificates may not persist.`,
3115
+ "warn"
3116
+ )
3117
+ );
3118
+ });
3119
+ }
3120
+ const mounts = await targetMounts(hostState);
3121
+ const clear = await ensureEdgeClear(executor, opts.config, onLog);
3122
+ try {
3123
+ onLog(log5("Starting the edge container..."));
3124
+ if (!await startEdgeContainer(executor, container, image, onLog, mounts)) {
3125
+ throw new Error("the edge container failed to start");
3126
+ }
3127
+ const verdict = await verifyEdgeServing(executor, container, {
3128
+ configTest: true,
3129
+ timeoutMs: opts.verifyTimeoutMs ?? 3e4
3130
+ });
3131
+ if (!verdict.serving) {
3132
+ throw new Error(
3133
+ verdict.reason ?? "the edge container started but isn't serving"
3134
+ );
3135
+ }
3136
+ if (clear.tookOver) await completeEdgeTakeover(executor);
3137
+ const version = await executor.exec(containerCommand(container, "openresty -v 2>&1")).catch(() => "");
3138
+ onLog(
3139
+ log5(
3140
+ `Edge container running${version.trim() ? ` (${version.trim()})` : ""}`
3141
+ )
3142
+ );
3143
+ return { container, image, converted: clear.tookOver };
3144
+ } catch (err) {
3145
+ const msg = safeErrorMessage(err);
3146
+ onLog(log5(`Edge container setup failed: ${msg}`, "error"));
3147
+ const logs = await executor.exec(`docker logs --tail 40 ${sq(container)} 2>&1 || true`).catch(() => "");
3148
+ if (logs.trim()) onLog(log5(`Edge container logs:
3149
+ ${logs}`, "error"));
3150
+ await executor.exec(`docker rm -f ${sq(container)} 2>/dev/null || true`).catch(() => {
3151
+ });
3152
+ invalidateEdgeContainer(executor);
3153
+ if (clear.tookOver) await rollbackEdgeTakeover(executor, onLog);
3154
+ const reason = edgeFailureReason(logs);
3155
+ throw new Error(
3156
+ `Edge container setup failed: ${reason ? `${reason} (${msg})` : msg}`
3157
+ );
3158
+ }
3159
+ }
3160
+
3161
+ export {
3162
+ beginEdgeTakeover,
3163
+ rollbackEdgeTakeover,
3164
+ completeEdgeTakeover,
3165
+ recoverInterruptedTakeover,
3166
+ detectInstalledProxy,
3167
+ scanImportableSites,
3168
+ unreachableStaticRoots,
3169
+ copyStaticRootIntoEdge,
3170
+ edgeProxy,
3171
+ edgeProxyFor,
3172
+ collectProxyCerts,
3173
+ systemCatalog,
3174
+ COMPONENT_CHECKS,
3175
+ checkAll,
3176
+ checkComponents,
3177
+ COMPONENT_INSTALLERS,
3178
+ importSites,
3179
+ foreignProxyOnEdge,
3180
+ setDefaultEdgeImage,
3181
+ resolveEdgeImage,
3182
+ containerEdgeProvider,
3183
+ localContainerEdgeProvider,
3184
+ resolveEdgeContainerMounts,
3185
+ buildEdgeRunCommand,
3186
+ verifyEdgeServing,
3187
+ ensureContainerEdge
3188
+ };